Created
April 25, 2018 23:11
-
-
Save thomcc/a960b98f8a374d9f1d58f0cfe26cacbd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Coord is a way of storing a 2d integer vector (e.g. vec2i or similar) as a | |
// value type by packing it into a number. It will do what you want in a Map or | |
// Set, you can compare them for equality, and it should be free of alloctions | |
// (at least in Spidermonkey and V8, but probably anywhere else too). | |
// | |
// The downside is that it's a pain to debug, use, and it has ugly syntax. It's | |
// type safe (in typescript) tho. | |
// | |
// Each component is stored as a 15 bit (signed, 2's complement) integer. These | |
// must be between COORD_MIN and COORD_MAX, inclusive. It's a bad choice if | |
// there's any chance the values will not fit. | |
// | |
// layout: 0b00YYYYYYYYYYYYYYYXXXXXXXXXXXXXXX | |
// | |
// It uses 15 bits and not 16, as using all 32 bits will overfill SMI_MAX in V8, | |
// so we only use 30[0], and your number will end up allocated on the heap (it | |
// generally will optimize this away, but, well, it seems bad to rely on that | |
// for something that is explicitly trading ease of use for this. | |
// | |
// (Strictly speaking, we could have a pair of one 15 bit number and one 16 bit | |
// number, but that sounds pointless and annoying. It's a little tempting to try | |
// to store a bit of padding data between the two components for better sanity | |
// checking, and so a few helpers could be written more efficiently e.g. add by | |
// just adding numbers directly, then checking for overflow and asserting or | |
// w/e). | |
// | |
// All that said, This is still more expensive to use than just passing around 2 | |
// numbers, and it is doubtful that the compiler can optimize away the overhead. | |
// That said, the reduced number of allocations should reduce GC pressure, the | |
// fact that the coord's data can be stored on the stack which should improve | |
// cache locality, so overall it will probably perform more predictably (and | |
// likely better) than using an `{x: number, y: number}` object or similar. It's | |
// a bit of a pain in the ass though. | |
// | |
// Note: It would be easy to write a version of this for non-integer coordinates | |
// by using fixed point numbers for x and y. I *almost* did this too but talked | |
// myself out of it. | |
// Magic type that isn't convertable to anything else. | |
interface Coord extends Number { | |
// Dummy propery to prevent Coord having implicit conversions between Coords | |
// and empty objects. | |
_coordBrand: number; | |
} | |
const COORD_MIN = -0x4000; | |
const COORD_MAX = 0x3fff; | |
function rawMkCoord(x: number, y: number): Coord { | |
return (((y & 0x7fff) << 15) | (x & 0x7fff)) as any; | |
} | |
// Asserts if x and y aren't in the right range | |
function makeCoord(x: number, y: number) { | |
if (DEBUG) { | |
Assert.int2(x, y); | |
Assert.betweenI(x, COORD_MIN, COORD_MAX); | |
Assert.betweenI(y, COORD_MIN, COORD_MAX); | |
} | |
return rawMakeCoord(x, y) | |
} | |
// rounds and clamps to ensure x and y are in the right range | |
function coord(x: number, y: number) { | |
x = math.clamp(Math.round(x), COORD_MIN, COORD_MAX); | |
y = math.clamp(Math.round(y), COORD_MIN, COORD_MAX); | |
return rawMakeCoord(x, y) | |
} | |
// Assert the coord is not obviously invalid and cast. | |
function coordToNumber(c: Coord) { | |
if (DEBUG) Assert.eq(c, c & 0x3fffffff); | |
return (c as any) as number; | |
} | |
function coordX(c: Coord): number { return (coordToNumber(c) << 17) >> 17; } | |
function coordY(c: Coord): number { return (coordToNumber(c) << 2) >> 17; } | |
function coordWithX(c: Coord, x: number): Coord { | |
const n = coordToNumber(c); | |
return (((n & 0x3fff8000) | (x & 0x7fff)) as any) as Coord; | |
} | |
function coordWithY(c: Coord, y: number): Coord { | |
const n = coordToNumber(c); | |
return (((n & 0x7fff) | ((y & 0x7fff) << 15)) as any) as Coord; | |
} | |
function coordToString(c: Coord) { | |
return `Coord(${coordX(c)}, ${coordY(c)})` | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment