@zakkster/lite-fastbit32 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FastBit32.d.ts +159 -6
- package/Fastbit32.js +257 -0
- package/README.md +99 -82
- package/llms.txt +97 -3
- package/package.json +4 -3
- package/FastBit32.js +0 -134
package/FastBit32.d.ts
CHANGED
|
@@ -84,12 +84,6 @@ export class FastBit32 {
|
|
|
84
84
|
* Equivalent to `(value & mask) === mask`.
|
|
85
85
|
*
|
|
86
86
|
* @param mask - Bitmask to test against.
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* ```js
|
|
90
|
-
* const PHYSICS = (1 << 1) | (1 << 4);
|
|
91
|
-
* if (entity.hasAll(PHYSICS)) { /* run physics *\/ }
|
|
92
|
-
* ```
|
|
93
87
|
*/
|
|
94
88
|
hasAll(mask: number): boolean;
|
|
95
89
|
|
|
@@ -194,6 +188,17 @@ export class FastBit32 {
|
|
|
194
188
|
*/
|
|
195
189
|
clone(): FastBit32;
|
|
196
190
|
|
|
191
|
+
// ── Iteration ───────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* O(k) iteration over active bits, where k is the number of set bits.
|
|
195
|
+
* Visits each active bit index in ascending order.
|
|
196
|
+
*
|
|
197
|
+
* @param callback - Called with the bit index of each active bit.
|
|
198
|
+
* @returns `this` for chaining.
|
|
199
|
+
*/
|
|
200
|
+
forEach(callback: (bit: number) => void): this;
|
|
201
|
+
|
|
197
202
|
// ── Serialization ───────────────────────────────────
|
|
198
203
|
|
|
199
204
|
/**
|
|
@@ -212,3 +217,151 @@ export class FastBit32 {
|
|
|
212
217
|
*/
|
|
213
218
|
static deserialize(value: number): FastBit32;
|
|
214
219
|
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* BitMapper — The Human-to-Hardware Bridge.
|
|
223
|
+
*
|
|
224
|
+
* Translates semantic string names into 32-bit integer indices and raw masks.
|
|
225
|
+
* Protects developers from raw integer math while keeping the engine hot-path fast.
|
|
226
|
+
*/
|
|
227
|
+
export class BitMapper {
|
|
228
|
+
/**
|
|
229
|
+
* Creates a new BitMapper dictionary.
|
|
230
|
+
*
|
|
231
|
+
* @param names - Array of component/flag names (Maximum 32).
|
|
232
|
+
* @throws Error if more than 32 flags are provided.
|
|
233
|
+
*/
|
|
234
|
+
constructor(names?: string[]);
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Gets the raw bit index (0-31) for a specific flag name.
|
|
238
|
+
*
|
|
239
|
+
* @param name - The semantic string name of the flag.
|
|
240
|
+
* @returns The integer bit index.
|
|
241
|
+
* @throws Error if the flag name is not registered.
|
|
242
|
+
*/
|
|
243
|
+
get(name: string): number;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Generates a raw 32-bit integer mask from an array of flag names.
|
|
247
|
+
* Ideal for building System signatures in an ECS.
|
|
248
|
+
*
|
|
249
|
+
* @param names - Array of semantic flag names to combine into a mask.
|
|
250
|
+
* @returns The generated unsigned 32-bit mask.
|
|
251
|
+
*/
|
|
252
|
+
getMask(names: string[]): number;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Helper to extract which semantic strings are active inside a FastBit32 instance.
|
|
256
|
+
* Excellent for console.log debugging.
|
|
257
|
+
*
|
|
258
|
+
* @param fastBit32Instance - The FastBit32 instance to evaluate.
|
|
259
|
+
* @returns An array of active string names.
|
|
260
|
+
*/
|
|
261
|
+
getActiveNames(fastBit32Instance: FastBit32): string[];
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* O(1) reverse lookup — integer bit index to string name.
|
|
265
|
+
*
|
|
266
|
+
* @param bit - The bit index (0–31).
|
|
267
|
+
* @returns The registered name, or `undefined` if no name is registered at that index.
|
|
268
|
+
*/
|
|
269
|
+
getName(bit: number): string | undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── O(k) Iteration Helpers ─────────────────────────────────
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Iterates active bits in a mask, calling back with the corresponding array element.
|
|
276
|
+
*
|
|
277
|
+
* @param mask - FastBit32 instance whose active bits select array indices.
|
|
278
|
+
* @param array - Data array indexed by bit position.
|
|
279
|
+
* @param callback - Called with `(element, bitIndex)` for each active bit.
|
|
280
|
+
*/
|
|
281
|
+
export function forEachArray<T>(
|
|
282
|
+
mask: FastBit32,
|
|
283
|
+
array: T[],
|
|
284
|
+
callback: (element: T, bit: number) => void
|
|
285
|
+
): void;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Iterates active bits, mapping through a keys array to object properties.
|
|
289
|
+
*
|
|
290
|
+
* @param mask - FastBit32 instance whose active bits select key indices.
|
|
291
|
+
* @param keys - Array of property keys indexed by bit position.
|
|
292
|
+
* @param obj - Object to read values from.
|
|
293
|
+
* @param callback - Called with `(value, key, bitIndex)` for each active bit.
|
|
294
|
+
*/
|
|
295
|
+
export function forEachObject<T>(
|
|
296
|
+
mask: FastBit32,
|
|
297
|
+
keys: string[],
|
|
298
|
+
obj: Record<string, T>,
|
|
299
|
+
callback: (value: T, key: string, bit: number) => void
|
|
300
|
+
): void;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Iterates active bits, resolving each to a string name via a BitMapper.
|
|
304
|
+
*
|
|
305
|
+
* @param mask - FastBit32 instance.
|
|
306
|
+
* @param mapper - BitMapper for reverse lookup.
|
|
307
|
+
* @param callback - Called with `(name, bitIndex)` for each active bit.
|
|
308
|
+
*/
|
|
309
|
+
export function forEachMapped(
|
|
310
|
+
mask: FastBit32,
|
|
311
|
+
mapper: BitMapper,
|
|
312
|
+
callback: (name: string, bit: number) => void
|
|
313
|
+
): void;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Iterates active bits, resolving names via BitMapper and reading object values.
|
|
317
|
+
*
|
|
318
|
+
* @param mask - FastBit32 instance.
|
|
319
|
+
* @param mapper - BitMapper for reverse lookup.
|
|
320
|
+
* @param obj - Object to read values from.
|
|
321
|
+
* @param callback - Called with `(value, key, bitIndex)` for each active bit.
|
|
322
|
+
*/
|
|
323
|
+
export function forEachMappedObject<T>(
|
|
324
|
+
mask: FastBit32,
|
|
325
|
+
mapper: BitMapper,
|
|
326
|
+
obj: Record<string, T>,
|
|
327
|
+
callback: (value: T, key: string, bit: number) => void
|
|
328
|
+
): void;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Iterates bits that are active in BOTH masks (intersection / AND).
|
|
332
|
+
*
|
|
333
|
+
* @param maskA - First FastBit32 instance.
|
|
334
|
+
* @param maskB - Second FastBit32 instance.
|
|
335
|
+
* @param callback - Called with `(bitIndex)` for each shared active bit.
|
|
336
|
+
*/
|
|
337
|
+
export function forEachMaskPair(
|
|
338
|
+
maskA: FastBit32,
|
|
339
|
+
maskB: FastBit32,
|
|
340
|
+
callback: (bit: number) => void
|
|
341
|
+
): void;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Iterates bits active in A but not in B (difference / A AND NOT B).
|
|
345
|
+
*
|
|
346
|
+
* @param maskA - First FastBit32 instance.
|
|
347
|
+
* @param maskB - Second FastBit32 instance.
|
|
348
|
+
* @param callback - Called with `(bitIndex)` for each bit in A not in B.
|
|
349
|
+
*/
|
|
350
|
+
export function forEachMaskDiff(
|
|
351
|
+
maskA: FastBit32,
|
|
352
|
+
maskB: FastBit32,
|
|
353
|
+
callback: (bit: number) => void
|
|
354
|
+
): void;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Iterates bits active in either mask (union / OR).
|
|
358
|
+
*
|
|
359
|
+
* @param maskA - First FastBit32 instance.
|
|
360
|
+
* @param maskB - Second FastBit32 instance.
|
|
361
|
+
* @param callback - Called with `(bitIndex)` for each active bit in A or B.
|
|
362
|
+
*/
|
|
363
|
+
export function forEachMaskUnion(
|
|
364
|
+
maskA: FastBit32,
|
|
365
|
+
maskB: FastBit32,
|
|
366
|
+
callback: (bit: number) => void
|
|
367
|
+
): void;
|
package/Fastbit32.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastBit32 — Zero-GC, Monomorphic 32-bit Flag Manager
|
|
3
|
+
* Engineered for 60fps hot-path execution, ECS masking, and object pooling.
|
|
4
|
+
* * ⚠️ ENGINE PRIMITIVE CAVEATS:
|
|
5
|
+
* - SILENT WRAPAROUND: JS bitwise shifts implicitly apply modulo 32.
|
|
6
|
+
* Calling `add(32)` evaluates as `add(0)`. `add(40)` evaluates as `add(8)`.
|
|
7
|
+
* - TRUNCATION: Floats and negative numbers are silently truncated and
|
|
8
|
+
* converted to unsigned 32-bit integers (e.g., `-1 >>> 0` becomes `4294967295`).
|
|
9
|
+
* Sanitize your inputs upstream if your domain logic requires strict bounds!
|
|
10
|
+
*/
|
|
11
|
+
export class Fastbit32 {
|
|
12
|
+
constructor(initial = 0) {
|
|
13
|
+
this.value = initial >>> 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
add(bit) {
|
|
17
|
+
this.value |= (1 << bit);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
remove(bit) {
|
|
22
|
+
this.value &= ~(1 << bit);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
toggle(bit) {
|
|
27
|
+
this.value ^= (1 << bit);
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
has(bit) {
|
|
32
|
+
return (this.value & (1 << bit)) !== 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
hasAll(mask) {
|
|
36
|
+
return (this.value & mask) === mask;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
hasAny(mask) {
|
|
40
|
+
return (this.value & mask) !== 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
hasNone(mask) {
|
|
44
|
+
return (this.value & mask) === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clear() {
|
|
48
|
+
this.value = 0;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
union(mask) {
|
|
53
|
+
this.value |= mask;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
difference(mask) {
|
|
58
|
+
this.value &= ~mask;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
intersect(mask) {
|
|
63
|
+
this.value &= mask;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Advanced AAA Engine Helpers ──────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* O(1) loop-free popcount (Hamming Weight).
|
|
71
|
+
* Returns the number of active bits (e.g., how many flags are active).
|
|
72
|
+
*/
|
|
73
|
+
count() {
|
|
74
|
+
let v = this.value;
|
|
75
|
+
v = v - ((v >>> 1) & 0x55555555);
|
|
76
|
+
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
|
|
77
|
+
return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* O(1) loop-free popcount applying a mask first.
|
|
82
|
+
* Useful for counting active flags within a specific subsystem.
|
|
83
|
+
*/
|
|
84
|
+
countMasked(mask) {
|
|
85
|
+
let v = this.value & mask;
|
|
86
|
+
v = v - ((v >>> 1) & 0x55555555);
|
|
87
|
+
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
|
|
88
|
+
return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Instantly finds the index of the lowest active bit.
|
|
93
|
+
* Incredibly useful for Object Pools (finding the first available slot).
|
|
94
|
+
* Returns -1 if empty.
|
|
95
|
+
*/
|
|
96
|
+
lowest() {
|
|
97
|
+
if (this.value === 0) return -1;
|
|
98
|
+
// Isolate lowest set bit, then use native Count Leading Zeros to find index
|
|
99
|
+
return Math.clz32(this.value & -this.value) ^ 31;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Instantly finds the index of the highest active bit.
|
|
104
|
+
* Useful for determining the bounds of active arrays/spatial grids.
|
|
105
|
+
* Returns -1 if empty.
|
|
106
|
+
*/
|
|
107
|
+
highest() {
|
|
108
|
+
if (this.value === 0) return -1;
|
|
109
|
+
return 31 - Math.clz32(this.value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
isEmpty() {
|
|
113
|
+
return this.value === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
clone() {
|
|
117
|
+
return new Fastbit32(this.value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Serialization (For ECS Save States) ──────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Exports the raw 32-bit integer for ultra-lightweight JSON/binary storage.
|
|
124
|
+
*/
|
|
125
|
+
serialize() {
|
|
126
|
+
return this.value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Instantiates a new FastBit32 from a serialized integer.
|
|
131
|
+
*/
|
|
132
|
+
static deserialize(value) {
|
|
133
|
+
return new Fastbit32(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* O(k) loop-free iteration over active bits.
|
|
138
|
+
* @param {Function} callback - Called with (bit)
|
|
139
|
+
*/
|
|
140
|
+
forEach(callback) {
|
|
141
|
+
let v = this.value;
|
|
142
|
+
while (v !== 0) {
|
|
143
|
+
const bit = Math.clz32(v & -v) ^ 31;
|
|
144
|
+
callback(bit);
|
|
145
|
+
v &= v - 1; // Clear lowest active bit
|
|
146
|
+
}
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class BitMapper {
|
|
152
|
+
constructor(names = []) {
|
|
153
|
+
if (names.length > 32) throw new Error('BitMapper: Maximum 32 flags supported.');
|
|
154
|
+
|
|
155
|
+
this._map = new Map();
|
|
156
|
+
this._reverse = new Array(32); // O(1) array lookup
|
|
157
|
+
|
|
158
|
+
names.forEach((name, index) => {
|
|
159
|
+
this._map.set(name, index);
|
|
160
|
+
this._reverse[index] = name;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get(name) {
|
|
165
|
+
const bit = this._map.get(name);
|
|
166
|
+
if (bit === undefined) throw new Error(`BitMapper: Unknown flag "${name}"`);
|
|
167
|
+
return bit;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getMask(names) {
|
|
171
|
+
let mask = 0;
|
|
172
|
+
for (let i = 0; i < names.length; i++) mask |= (1 << this.get(names[i]));
|
|
173
|
+
return mask >>> 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getActiveNames(fastBit32Instance) {
|
|
177
|
+
const active = [];
|
|
178
|
+
for (const [name, bit] of this._map.entries()) {
|
|
179
|
+
if (fastBit32Instance.has(bit)) active.push(name);
|
|
180
|
+
}
|
|
181
|
+
return active;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* O(1) Reverse lookup. Integer -> String.
|
|
186
|
+
*/
|
|
187
|
+
getName(bit) {
|
|
188
|
+
return this._reverse[bit];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── O(k) Iteration Helpers ─────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export function forEachArray(mask, array, callback) {
|
|
195
|
+
let v = mask.value;
|
|
196
|
+
while (v !== 0) {
|
|
197
|
+
const bit = Math.clz32(v & -v) ^ 31;
|
|
198
|
+
callback(array[bit], bit);
|
|
199
|
+
v &= v - 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function forEachObject(mask, keys, obj, callback) {
|
|
204
|
+
let v = mask.value;
|
|
205
|
+
while (v !== 0) {
|
|
206
|
+
const bit = Math.clz32(v & -v) ^ 31;
|
|
207
|
+
const key = keys[bit];
|
|
208
|
+
callback(obj[key], key, bit);
|
|
209
|
+
v &= v - 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function forEachMapped(mask, mapper, callback) {
|
|
214
|
+
let v = mask.value;
|
|
215
|
+
while (v !== 0) {
|
|
216
|
+
const bit = Math.clz32(v & -v) ^ 31;
|
|
217
|
+
callback(mapper.getName(bit), bit);
|
|
218
|
+
v &= v - 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function forEachMappedObject(mask, mapper, obj, callback) {
|
|
223
|
+
let v = mask.value;
|
|
224
|
+
while (v !== 0) {
|
|
225
|
+
const bit = Math.clz32(v & -v) ^ 31;
|
|
226
|
+
const key = mapper.getName(bit);
|
|
227
|
+
callback(obj[key], key, bit);
|
|
228
|
+
v &= v - 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function forEachMaskPair(maskA, maskB, callback) {
|
|
233
|
+
let v = (maskA.value & maskB.value) >>> 0;
|
|
234
|
+
while (v !== 0) {
|
|
235
|
+
const bit = Math.clz32(v & -v) ^ 31;
|
|
236
|
+
callback(bit);
|
|
237
|
+
v &= v - 1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function forEachMaskDiff(maskA, maskB, callback) {
|
|
242
|
+
let v = (maskA.value & ~maskB.value) >>> 0;
|
|
243
|
+
while (v !== 0) {
|
|
244
|
+
const bit = Math.clz32(v & -v) ^ 31;
|
|
245
|
+
callback(bit);
|
|
246
|
+
v &= v - 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function forEachMaskUnion(maskA, maskB, callback) {
|
|
251
|
+
let v = (maskA.value | maskB.value) >>> 0;
|
|
252
|
+
while (v !== 0) {
|
|
253
|
+
const bit = Math.clz32(v & -v) ^ 31;
|
|
254
|
+
callback(bit);
|
|
255
|
+
v &= v - 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object poo
|
|
|
20
20
|
| **Branchless** | **Yes** | No | No |
|
|
21
21
|
| **O(1) popcount** | **Yes** | No | No |
|
|
22
22
|
| **O(1) lowest/highest** | **Yes** | No | No |
|
|
23
|
+
| **O(k) iteration** | **Yes** | No | No |
|
|
24
|
+
| **BitMapper** | **Yes** | No | No |
|
|
23
25
|
| **BigInt support** | No | No | No |
|
|
24
26
|
| **ECS-ready** | **Yes** | Yes | Yes |
|
|
25
27
|
| **Object pool scan** | **Yes** | No | No |
|
|
@@ -37,7 +39,7 @@ npm install @zakkster/lite-fastbit32
|
|
|
37
39
|
## Quick Start
|
|
38
40
|
|
|
39
41
|
```javascript
|
|
40
|
-
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
42
|
+
import { FastBit32, BitMapper } from '@zakkster/lite-fastbit32';
|
|
41
43
|
|
|
42
44
|
const flags = new FastBit32();
|
|
43
45
|
|
|
@@ -47,6 +49,12 @@ flags.count(); // 2 — O(1) popcount
|
|
|
47
49
|
flags.lowest(); // 1 — O(1) bit-scan forward
|
|
48
50
|
flags.remove(1); // Clear bit 1
|
|
49
51
|
flags.serialize(); // Raw uint32 for storage
|
|
52
|
+
|
|
53
|
+
// Human-readable flag management
|
|
54
|
+
const mapper = new BitMapper(['Physics', 'Render', 'AI', 'Input']);
|
|
55
|
+
const entity = new FastBit32();
|
|
56
|
+
entity.add(mapper.get('Physics')).add(mapper.get('Input'));
|
|
57
|
+
mapper.getActiveNames(entity); // ['Physics', 'Input']
|
|
50
58
|
```
|
|
51
59
|
|
|
52
60
|
---
|
|
@@ -83,6 +91,10 @@ lowest = Math.clz32(value & -value) ^ 31
|
|
|
83
91
|
|
|
84
92
|
`highest` uses `31 - Math.clz32(value)` directly.
|
|
85
93
|
|
|
94
|
+
### O(k) Iteration
|
|
95
|
+
|
|
96
|
+
All iteration helpers visit only active bits using the `v &= v - 1` trick to clear the lowest bit each step. Complexity is O(k) where k is the number of set bits — not 32.
|
|
97
|
+
|
|
86
98
|
### Caveats
|
|
87
99
|
|
|
88
100
|
- **Silent wraparound:** JS bitwise shifts apply modulo 32. `add(32)` evaluates as `add(0)`. `add(40)` evaluates as `add(8)`.
|
|
@@ -131,7 +143,7 @@ Tested on Apple M2 Pro, Node 22, V8 12.x. All values in **ops/ms**.
|
|
|
131
143
|
## Recipes
|
|
132
144
|
|
|
133
145
|
<details>
|
|
134
|
-
<summary><strong
|
|
146
|
+
<summary><strong>ECS Component Masks</strong></summary>
|
|
135
147
|
|
|
136
148
|
```javascript
|
|
137
149
|
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
@@ -152,32 +164,23 @@ if (entity.hasAll(PHYSICS_QUERY)) runPhysics(entity);
|
|
|
152
164
|
if (entity.hasAll(RENDER_QUERY)) drawSprite(entity);
|
|
153
165
|
```
|
|
154
166
|
|
|
155
|
-
>
|
|
167
|
+
> **Bit 31 (Sign Bit) Warning:** In JavaScript, `1 << 31` evaluates to `-2147483648` — a negative number. FastBit32 handles this correctly under the hood, but if you log raw mask values to the console, you will see negative integers and assume a bug. This also affects `serialize()`: masks using bit 31 produce negative numbers in JSON. **Recommendation:** Keep ECS component indices to 0–30 (31 components). If you must use all 32, compare serialized values with `>>> 0` to force unsigned representation.
|
|
156
168
|
|
|
157
169
|
</details>
|
|
158
170
|
|
|
159
171
|
<details>
|
|
160
|
-
<summary><strong
|
|
172
|
+
<summary><strong>Object Pool — First Free Slot</strong></summary>
|
|
161
173
|
|
|
162
174
|
```javascript
|
|
163
175
|
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
164
176
|
|
|
165
|
-
// Bit = 1 means slot is occupied
|
|
166
177
|
const pool = new FastBit32();
|
|
167
178
|
const objects = new Array(32);
|
|
168
179
|
|
|
169
180
|
function allocate() {
|
|
170
|
-
// Invert to find free slots, mask to pool size
|
|
171
181
|
const free = new FastBit32(~pool.value & 0xFFFFFFFF);
|
|
172
182
|
const slot = free.lowest();
|
|
173
|
-
|
|
174
|
-
// ⚠️ CRITICAL: Always check for -1.
|
|
175
|
-
// If the pool is full, lowest() returns -1.
|
|
176
|
-
// Accessing objects[-1] bypasses V8's array bounds optimization,
|
|
177
|
-
// triggering a dictionary-mode fallback on the entire array —
|
|
178
|
-
// a massive de-optimization penalty that persists for the array's lifetime.
|
|
179
|
-
if (slot === -1) return null; // Pool exhausted — expand or drop
|
|
180
|
-
|
|
183
|
+
if (slot === -1) return null;
|
|
181
184
|
pool.add(slot);
|
|
182
185
|
return slot;
|
|
183
186
|
}
|
|
@@ -195,7 +198,7 @@ allocate(); // 0 — immediately reused
|
|
|
195
198
|
</details>
|
|
196
199
|
|
|
197
200
|
<details>
|
|
198
|
-
<summary><strong
|
|
201
|
+
<summary><strong>Input State Manager</strong></summary>
|
|
199
202
|
|
|
200
203
|
```javascript
|
|
201
204
|
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
@@ -219,7 +222,6 @@ window.addEventListener('keyup', e => {
|
|
|
219
222
|
if (e.code === 'Space') input.remove(KEY_JUMP);
|
|
220
223
|
});
|
|
221
224
|
|
|
222
|
-
// In game loop — zero-branch checks
|
|
223
225
|
if (input.has(KEY_JUMP)) jump();
|
|
224
226
|
if (input.hasAny((1 << KEY_LEFT) | (1 << KEY_RIGHT))) move();
|
|
225
227
|
```
|
|
@@ -227,56 +229,7 @@ if (input.hasAny((1 << KEY_LEFT) | (1 << KEY_RIGHT))) move();
|
|
|
227
229
|
</details>
|
|
228
230
|
|
|
229
231
|
<details>
|
|
230
|
-
<summary><strong
|
|
231
|
-
|
|
232
|
-
```javascript
|
|
233
|
-
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
234
|
-
|
|
235
|
-
const LAYER_PLAYER = 0;
|
|
236
|
-
const LAYER_ENEMY = 1;
|
|
237
|
-
const LAYER_BULLET = 2;
|
|
238
|
-
const LAYER_WALL = 3;
|
|
239
|
-
const LAYER_PICKUP = 4;
|
|
240
|
-
|
|
241
|
-
const playerMask = new FastBit32();
|
|
242
|
-
playerMask.add(LAYER_ENEMY).add(LAYER_PICKUP).add(LAYER_WALL);
|
|
243
|
-
|
|
244
|
-
const bulletMask = new FastBit32();
|
|
245
|
-
bulletMask.add(LAYER_ENEMY).add(LAYER_WALL);
|
|
246
|
-
|
|
247
|
-
function canCollide(entityLayer, targetMask) {
|
|
248
|
-
return targetMask.has(entityLayer);
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
</details>
|
|
253
|
-
|
|
254
|
-
<details>
|
|
255
|
-
<summary><strong>🌐 Networking Flags</strong></summary>
|
|
256
|
-
|
|
257
|
-
```javascript
|
|
258
|
-
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
259
|
-
|
|
260
|
-
const FLAG_RELIABLE = 0;
|
|
261
|
-
const FLAG_ORDERED = 1;
|
|
262
|
-
const FLAG_COMPRESSED = 2;
|
|
263
|
-
const FLAG_ENCRYPTED = 3;
|
|
264
|
-
|
|
265
|
-
const packet = new FastBit32();
|
|
266
|
-
packet.add(FLAG_RELIABLE).add(FLAG_ENCRYPTED);
|
|
267
|
-
|
|
268
|
-
const raw = packet.serialize(); // Send as uint32
|
|
269
|
-
// ... network transport ...
|
|
270
|
-
const restored = FastBit32.deserialize(raw);
|
|
271
|
-
if (restored.has(FLAG_RELIABLE)) ack(packet);
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
</details>
|
|
275
|
-
|
|
276
|
-
<details>
|
|
277
|
-
<summary><strong>🧠 State Machine — Mutex Flags</strong></summary>
|
|
278
|
-
|
|
279
|
-
In a strict FSM, only one state should be active at any time. Never use `remove(OLD).add(NEW)` — if the remove and add target the same bit index by mistake, you silently corrupt the state. Use `.clear().add()` to guarantee zero overlapping bits.
|
|
232
|
+
<summary><strong>State Machine — Mutex Flags</strong></summary>
|
|
280
233
|
|
|
281
234
|
```javascript
|
|
282
235
|
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
@@ -290,39 +243,60 @@ const INVINCIBLE = 4;
|
|
|
290
243
|
const state = new FastBit32();
|
|
291
244
|
state.add(IDLE);
|
|
292
245
|
|
|
293
|
-
// ✅ CORRECT — mutex transition: clear ALL bits, then set the new state.
|
|
294
|
-
// Guarantees zero overlap regardless of previous state.
|
|
295
246
|
function startAttack() {
|
|
296
247
|
state.clear().add(ATTACKING).add(INVINCIBLE);
|
|
297
248
|
}
|
|
298
249
|
|
|
299
|
-
// ✅ CORRECT — return to single state after compound state ends.
|
|
300
250
|
function endAttack() {
|
|
301
251
|
state.clear().add(IDLE);
|
|
302
252
|
}
|
|
303
253
|
|
|
304
|
-
// ❌ WRONG — remove/add leaves stale bits if you forget one:
|
|
305
|
-
// state.remove(ATTACKING).remove(INVINCIBLE).add(IDLE);
|
|
306
|
-
// If INVINCIBLE was already cleared by another system, this still "works"
|
|
307
|
-
// but masks a logic error. clear() is unconditional and safe.
|
|
308
|
-
|
|
309
254
|
console.log(state.count()); // 1 — proof of mutex
|
|
310
255
|
```
|
|
311
256
|
|
|
312
257
|
</details>
|
|
313
258
|
|
|
314
259
|
<details>
|
|
315
|
-
<summary><strong
|
|
260
|
+
<summary><strong>BitMapper + Iteration (v1.1.0)</strong></summary>
|
|
316
261
|
|
|
317
262
|
```javascript
|
|
318
|
-
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
263
|
+
import { FastBit32, BitMapper, forEachMapped, forEachMappedObject } from '@zakkster/lite-fastbit32';
|
|
319
264
|
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
265
|
+
const components = new BitMapper(['Position', 'Velocity', 'Sprite', 'AI']);
|
|
266
|
+
const entity = new FastBit32();
|
|
267
|
+
entity.add(components.get('Position')).add(components.get('AI'));
|
|
268
|
+
|
|
269
|
+
console.log(components.getActiveNames(entity)); // ['Position', 'AI']
|
|
323
270
|
|
|
324
|
-
|
|
325
|
-
|
|
271
|
+
forEachMapped(entity, components, (name, bit) => {
|
|
272
|
+
console.log(`Component ${name} at bit ${bit}`);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const systems = { Position: updatePos, Velocity: updateVel, Sprite: draw, AI: think };
|
|
276
|
+
forEachMappedObject(entity, components, systems, (system, name) => {
|
|
277
|
+
system(entity);
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
</details>
|
|
282
|
+
|
|
283
|
+
<details>
|
|
284
|
+
<summary><strong>Mask Set Operations (v1.1.0)</strong></summary>
|
|
285
|
+
|
|
286
|
+
```javascript
|
|
287
|
+
import { FastBit32, forEachMaskPair, forEachMaskDiff, forEachMaskUnion } from '@zakkster/lite-fastbit32';
|
|
288
|
+
|
|
289
|
+
const required = new FastBit32().add(0).add(1).add(3);
|
|
290
|
+
const available = new FastBit32().add(0).add(3).add(5);
|
|
291
|
+
|
|
292
|
+
forEachMaskPair(required, available, bit => console.log('matched:', bit));
|
|
293
|
+
// matched: 0, matched: 3
|
|
294
|
+
|
|
295
|
+
forEachMaskDiff(required, available, bit => console.log('missing:', bit));
|
|
296
|
+
// missing: 1
|
|
297
|
+
|
|
298
|
+
forEachMaskUnion(required, available, bit => console.log('all:', bit));
|
|
299
|
+
// all: 0, all: 1, all: 3, all: 5
|
|
326
300
|
```
|
|
327
301
|
|
|
328
302
|
</details>
|
|
@@ -373,6 +347,12 @@ const restored = JSON.parse(json).map(FastBit32.deserialize);
|
|
|
373
347
|
| `.highest()` | `number` | O(1) index of most significant active bit. `-1` if empty. |
|
|
374
348
|
| `.isEmpty()` | `boolean` | True if value is `0`. |
|
|
375
349
|
|
|
350
|
+
### Iteration
|
|
351
|
+
|
|
352
|
+
| Method | Returns | Description |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| `.forEach(callback)` | `this` | O(k) iteration over active bits. `callback(bit)`. |
|
|
355
|
+
|
|
376
356
|
### Utility
|
|
377
357
|
|
|
378
358
|
| Method | Returns | Description |
|
|
@@ -381,6 +361,43 @@ const restored = JSON.parse(json).map(FastBit32.deserialize);
|
|
|
381
361
|
| `.serialize()` | `number` | Export raw uint32 for JSON/binary storage. |
|
|
382
362
|
| `FastBit32.deserialize(n)` | `FastBit32` | Restore from a serialized uint32. |
|
|
383
363
|
|
|
364
|
+
### `new BitMapper(names?)`
|
|
365
|
+
|
|
366
|
+
| Method | Returns | Description |
|
|
367
|
+
|---|---|---|
|
|
368
|
+
| `.get(name)` | `number` | Bit index for a flag name. Throws if unknown. |
|
|
369
|
+
| `.getMask(names)` | `number` | Combined uint32 mask from flag name array. |
|
|
370
|
+
| `.getActiveNames(fb32)` | `string[]` | Active flag names from a FastBit32 instance. |
|
|
371
|
+
| `.getName(bit)` | `string \| undefined` | O(1) reverse lookup — bit index to name. |
|
|
372
|
+
|
|
373
|
+
### Standalone Iteration Helpers
|
|
374
|
+
|
|
375
|
+
| Function | Description |
|
|
376
|
+
|---|---|
|
|
377
|
+
| `forEachArray(mask, array, cb)` | `cb(element, bit)` for each active bit. |
|
|
378
|
+
| `forEachObject(mask, keys, obj, cb)` | `cb(value, key, bit)` via keys array. |
|
|
379
|
+
| `forEachMapped(mask, mapper, cb)` | `cb(name, bit)` via BitMapper. |
|
|
380
|
+
| `forEachMappedObject(mask, mapper, obj, cb)` | `cb(value, key, bit)` via BitMapper + object. |
|
|
381
|
+
| `forEachMaskPair(maskA, maskB, cb)` | `cb(bit)` for intersection (A & B). |
|
|
382
|
+
| `forEachMaskDiff(maskA, maskB, cb)` | `cb(bit)` for difference (A & ~B). |
|
|
383
|
+
| `forEachMaskUnion(maskA, maskB, cb)` | `cb(bit)` for union (A \| B). |
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Changelog
|
|
388
|
+
|
|
389
|
+
### v1.1.0
|
|
390
|
+
|
|
391
|
+
**New: BitMapper** — Human-to-hardware bridge. Maps semantic string names to bit indices and masks, with O(1) reverse lookup via `getName(bit)`.
|
|
392
|
+
|
|
393
|
+
**New: `forEach(callback)`** — O(k) iteration on FastBit32 instances. Visits only active bits in ascending order using `v &= v - 1` bit-clearing. Returns `this` for chaining.
|
|
394
|
+
|
|
395
|
+
**New: 7 standalone iteration helpers** — `forEachArray`, `forEachObject`, `forEachMapped`, `forEachMappedObject`, `forEachMaskPair`, `forEachMaskDiff`, `forEachMaskUnion`. All O(k). Connect masks directly to arrays, objects, BitMapper dictionaries, and mask set operations without intermediate allocations.
|
|
396
|
+
|
|
397
|
+
### v1.0.0
|
|
398
|
+
|
|
399
|
+
Initial release. FastBit32 core: single-bit ops, bulk mask ops, in-place set math, O(1) popcount, O(1) bit-scan (lowest/highest), clone, serialize/deserialize.
|
|
400
|
+
|
|
384
401
|
---
|
|
385
402
|
|
|
386
403
|
## License
|
|
@@ -389,4 +406,4 @@ MIT
|
|
|
389
406
|
|
|
390
407
|
## Part of the @zakkster ecosystem
|
|
391
408
|
|
|
392
|
-
Zero-GC, deterministic, tree-shakeable micro-libraries for high-performance web applications.
|
|
409
|
+
Zero-GC, deterministic, tree-shakeable micro-libraries for high-performance web applications.
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @zakkster/lite-fastbit32 v1.
|
|
1
|
+
# @zakkster/lite-fastbit32 v1.1.0
|
|
2
2
|
|
|
3
3
|
> Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object pools, and 60fps hot-path engine code.
|
|
4
4
|
|
|
@@ -13,7 +13,13 @@ FastBit32 is a single-class library that wraps a plain unsigned 32-bit integer w
|
|
|
13
13
|
## Import
|
|
14
14
|
|
|
15
15
|
```javascript
|
|
16
|
-
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
16
|
+
import { FastBit32, BitMapper } from '@zakkster/lite-fastbit32';
|
|
17
|
+
|
|
18
|
+
// Standalone iteration helpers
|
|
19
|
+
import {
|
|
20
|
+
forEachArray, forEachObject, forEachMapped,
|
|
21
|
+
forEachMappedObject, forEachMaskPair, forEachMaskDiff, forEachMaskUnion
|
|
22
|
+
} from '@zakkster/lite-fastbit32';
|
|
17
23
|
```
|
|
18
24
|
|
|
19
25
|
## Constructor
|
|
@@ -67,6 +73,12 @@ All mutating methods return `this` for chaining unless noted otherwise.
|
|
|
67
73
|
| `.highest()` | `number` | Index of most significant set bit (0–31). Returns `-1` if empty. |
|
|
68
74
|
| `.isEmpty()` | `boolean` | True if value is 0. |
|
|
69
75
|
|
|
76
|
+
### Iteration
|
|
77
|
+
|
|
78
|
+
| Method | Returns | Description |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `.forEach(callback)` | `this` | O(k) iteration over active bits in ascending order. Callback receives `(bit)`. |
|
|
81
|
+
|
|
70
82
|
### Utility
|
|
71
83
|
|
|
72
84
|
| Method | Returns | Description |
|
|
@@ -81,6 +93,70 @@ All mutating methods return `this` for chaining unless noted otherwise.
|
|
|
81
93
|
|---|---|---|
|
|
82
94
|
| `.value` | `number` | The raw 32-bit integer. Safe to read directly in hot paths. |
|
|
83
95
|
|
|
96
|
+
## BitMapper — Human-to-Hardware Bridge
|
|
97
|
+
|
|
98
|
+
Translates semantic string names into bit indices and masks. Keeps engine hot paths fast while letting developers think in words.
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
const mapper = new BitMapper(['Position', 'Velocity', 'Health', 'Magic']);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Constructor
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
new BitMapper(names?: string[])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- `names` — Array of up to 32 flag names. Each name maps to its array index (0–31).
|
|
111
|
+
- Throws if more than 32 names are provided.
|
|
112
|
+
|
|
113
|
+
### BitMapper Methods
|
|
114
|
+
|
|
115
|
+
| Method | Returns | Description |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `.get(name)` | `number` | Bit index (0–31) for a flag name. Throws if unknown. |
|
|
118
|
+
| `.getMask(names)` | `number` | Combined uint32 mask from an array of flag names. |
|
|
119
|
+
| `.getActiveNames(fb32)` | `string[]` | Array of active flag names from a FastBit32 instance. |
|
|
120
|
+
| `.getName(bit)` | `string \| undefined` | O(1) reverse lookup — bit index to string name. |
|
|
121
|
+
|
|
122
|
+
### BitMapper Example
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
const mapper = new BitMapper(['Physics', 'Render', 'AI', 'Input']);
|
|
126
|
+
const entity = new FastBit32();
|
|
127
|
+
entity.add(mapper.get('Physics')).add(mapper.get('Input'));
|
|
128
|
+
|
|
129
|
+
mapper.getActiveNames(entity); // ['Physics', 'Input']
|
|
130
|
+
|
|
131
|
+
const PHYSICS_QUERY = mapper.getMask(['Physics', 'Render']);
|
|
132
|
+
entity.hasAll(PHYSICS_QUERY); // false — missing Render
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Standalone Iteration Helpers
|
|
136
|
+
|
|
137
|
+
All helpers are O(k) where k = number of active bits. No loops over 32 slots — only active bits are visited.
|
|
138
|
+
|
|
139
|
+
| Function | Signature | Description |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `forEachArray` | `(mask, array, cb)` | Active bits → array indices. `cb(element, bit)` |
|
|
142
|
+
| `forEachObject` | `(mask, keys, obj, cb)` | Active bits → keys array → object values. `cb(value, key, bit)` |
|
|
143
|
+
| `forEachMapped` | `(mask, mapper, cb)` | Active bits → BitMapper names. `cb(name, bit)` |
|
|
144
|
+
| `forEachMappedObject` | `(mask, mapper, obj, cb)` | Active bits → BitMapper names → object values. `cb(value, key, bit)` |
|
|
145
|
+
| `forEachMaskPair` | `(maskA, maskB, cb)` | Intersection (A & B). `cb(bit)` |
|
|
146
|
+
| `forEachMaskDiff` | `(maskA, maskB, cb)` | Difference (A & ~B). `cb(bit)` |
|
|
147
|
+
| `forEachMaskUnion` | `(maskA, maskB, cb)` | Union (A \| B). `cb(bit)` |
|
|
148
|
+
|
|
149
|
+
### Iteration Example
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
const mapper = new BitMapper(['Idle', 'Run', 'Jump', 'Attack']);
|
|
153
|
+
const mask = new FastBit32().add(1).add(3); // Run + Attack
|
|
154
|
+
|
|
155
|
+
forEachMapped(mask, mapper, (name, bit) => {
|
|
156
|
+
console.log(name); // 'Run', then 'Attack'
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
84
160
|
## Critical Caveats
|
|
85
161
|
|
|
86
162
|
1. **Silent modulo-32 wraparound.** JavaScript bitwise shifts apply `% 32` implicitly. `add(32)` is identical to `add(0)`. `add(40)` is identical to `add(8)`. There is no bounds checking.
|
|
@@ -162,6 +238,23 @@ function startAttack() {
|
|
|
162
238
|
}
|
|
163
239
|
```
|
|
164
240
|
|
|
241
|
+
### ECS with BitMapper and Iteration
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
const components = new BitMapper(['Position', 'Velocity', 'Sprite', 'AI']);
|
|
245
|
+
const entity = new FastBit32();
|
|
246
|
+
entity.add(components.get('Position')).add(components.get('AI'));
|
|
247
|
+
|
|
248
|
+
// Debug: see what's active
|
|
249
|
+
console.log(components.getActiveNames(entity)); // ['Position', 'AI']
|
|
250
|
+
|
|
251
|
+
// Iterate only active components
|
|
252
|
+
const systems = { Position: updatePos, Velocity: updateVel, Sprite: draw, AI: think };
|
|
253
|
+
forEachMappedObject(entity, components, systems, (system, name, bit) => {
|
|
254
|
+
system(entity); // Only calls updatePos and think
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
165
258
|
### Serialization Round-Trip
|
|
166
259
|
|
|
167
260
|
```javascript
|
|
@@ -213,5 +306,6 @@ flags.has(10); // false — clone is independent
|
|
|
213
306
|
- All single-bit operations: one bitwise instruction, zero branches.
|
|
214
307
|
- `count()`: 5 bitwise operations (Hacker's Delight parallel popcount), zero loops.
|
|
215
308
|
- `lowest()` / `highest()`: 1–2 operations using `Math.clz32`, zero loops.
|
|
309
|
+
- `forEach` and all standalone helpers: O(k) where k = active bits, not 32.
|
|
216
310
|
- All methods except `clone()` and `deserialize()`: zero allocations.
|
|
217
|
-
- V8 monomorphic: single hidden class for the lifetime of the object.
|
|
311
|
+
- V8 monomorphic: single hidden class for the lifetime of the object.
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-fastbit32",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"description": "Zero-GC, monomorphic 32-bit flag manager and ECS masking primitive for high-performance game loops.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
|
-
"main": "
|
|
8
|
+
"main": "Fastbit32.js",
|
|
9
9
|
"types": "FastBit32.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"
|
|
18
|
+
"Fastbit32.js",
|
|
19
19
|
"FastBit32.d.ts",
|
|
20
20
|
"llms.txt",
|
|
21
21
|
"README.md"
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"keywords": [
|
|
34
34
|
"bitflags",
|
|
35
35
|
"bitmask",
|
|
36
|
+
"bitmap",
|
|
36
37
|
"bitwise",
|
|
37
38
|
"state-machine",
|
|
38
39
|
"ecs",
|
package/FastBit32.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FastBit32 — Zero-GC, Monomorphic 32-bit Flag Manager
|
|
3
|
-
* Engineered for 60fps hot-path execution, ECS masking, and object pooling.
|
|
4
|
-
* * ⚠️ ENGINE PRIMITIVE CAVEATS:
|
|
5
|
-
* - SILENT WRAPAROUND: JS bitwise shifts implicitly apply modulo 32.
|
|
6
|
-
* Calling `add(32)` evaluates as `add(0)`. `add(40)` evaluates as `add(8)`.
|
|
7
|
-
* - TRUNCATION: Floats and negative numbers are silently truncated and
|
|
8
|
-
* converted to unsigned 32-bit integers (e.g., `-1 >>> 0` becomes `4294967295`).
|
|
9
|
-
* Sanitize your inputs upstream if your domain logic requires strict bounds!
|
|
10
|
-
*/
|
|
11
|
-
export class FastBit32 {
|
|
12
|
-
constructor(initial = 0) {
|
|
13
|
-
// Enforce unsigned 32-bit integer immediately for V8 inline caching
|
|
14
|
-
this.value = initial >>> 0;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// ── Single Bit Ops (Zero-Branching) ──────────────────────
|
|
18
|
-
|
|
19
|
-
add(bit) {
|
|
20
|
-
this.value |= (1 << bit);
|
|
21
|
-
return this;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
remove(bit) {
|
|
25
|
-
this.value &= ~(1 << bit);
|
|
26
|
-
return this;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
toggle(bit) {
|
|
30
|
-
this.value ^= (1 << bit);
|
|
31
|
-
return this;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
has(bit) {
|
|
35
|
-
return (this.value & (1 << bit)) !== 0;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── Bulk Mask Ops ────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
hasAll(mask) { return (this.value & mask) === mask; }
|
|
41
|
-
hasAny(mask) { return (this.value & mask) !== 0; }
|
|
42
|
-
hasNone(mask) { return (this.value & mask) === 0; }
|
|
43
|
-
|
|
44
|
-
// ── In-Place Mutations (Zero-GC Set Math) ────────────────
|
|
45
|
-
|
|
46
|
-
clear() {
|
|
47
|
-
this.value = 0;
|
|
48
|
-
return this;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
union(mask) {
|
|
52
|
-
this.value |= mask;
|
|
53
|
-
return this;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
difference(mask) {
|
|
57
|
-
this.value &= ~mask;
|
|
58
|
-
return this;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
intersect(mask) {
|
|
62
|
-
this.value &= mask;
|
|
63
|
-
return this;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Advanced AAA Engine Helpers ──────────────────────────
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* O(1) loop-free popcount (Hamming Weight).
|
|
70
|
-
* Returns the number of active bits (e.g., how many flags are active).
|
|
71
|
-
*/
|
|
72
|
-
count() {
|
|
73
|
-
let v = this.value;
|
|
74
|
-
v = v - ((v >>> 1) & 0x55555555);
|
|
75
|
-
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
|
|
76
|
-
return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* O(1) loop-free popcount applying a mask first.
|
|
81
|
-
* Useful for counting active flags within a specific subsystem.
|
|
82
|
-
*/
|
|
83
|
-
countMasked(mask) {
|
|
84
|
-
let v = this.value & mask;
|
|
85
|
-
v = v - ((v >>> 1) & 0x55555555);
|
|
86
|
-
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
|
|
87
|
-
return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Instantly finds the index of the lowest active bit.
|
|
92
|
-
* Incredibly useful for Object Pools (finding the first available slot).
|
|
93
|
-
* Returns -1 if empty.
|
|
94
|
-
*/
|
|
95
|
-
lowest() {
|
|
96
|
-
if (this.value === 0) return -1;
|
|
97
|
-
// Isolate lowest set bit, then use native Count Leading Zeros to find index
|
|
98
|
-
return Math.clz32(this.value & -this.value) ^ 31;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Instantly finds the index of the highest active bit.
|
|
103
|
-
* Useful for determining the bounds of active arrays/spatial grids.
|
|
104
|
-
* Returns -1 if empty.
|
|
105
|
-
*/
|
|
106
|
-
highest() {
|
|
107
|
-
if (this.value === 0) return -1;
|
|
108
|
-
return 31 - Math.clz32(this.value);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
isEmpty() {
|
|
112
|
-
return this.value === 0;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
clone() {
|
|
116
|
-
return new FastBit32(this.value);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ── Serialization (For ECS Save States) ──────────────────
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Exports the raw 32-bit integer for ultra-lightweight JSON/binary storage.
|
|
123
|
-
*/
|
|
124
|
-
serialize() {
|
|
125
|
-
return this.value;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Instantiates a new FastBit32 from a serialized integer.
|
|
130
|
-
*/
|
|
131
|
-
static deserialize(value) {
|
|
132
|
-
return new FastBit32(value);
|
|
133
|
-
}
|
|
134
|
-
}
|