@zakkster/lite-fastbit32 1.1.3 → 1.2.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 CHANGED
@@ -199,6 +199,74 @@ export class FastBit32 {
199
199
  */
200
200
  forEach(callback: (bit: number) => void): this;
201
201
 
202
+ // ── v1.2.0 AAA Engine Primitives ─────────────────────
203
+
204
+ /**
205
+ * Returns the index of the lowest INACTIVE (0) bit.
206
+ * O(1) via `Math.clz32` on the inverted mask — no loops, no scratch
207
+ * `FastBit32` allocation. Ideal for object pools: instantly finds the
208
+ * first free slot.
209
+ *
210
+ * @returns Bit index (0–31), or `-1` if all 32 bits are active.
211
+ */
212
+ nextClearBit(): number;
213
+
214
+ /**
215
+ * Returns the index of the highest INACTIVE (0) bit.
216
+ * O(1) via `Math.clz32` on the inverted mask.
217
+ *
218
+ * @returns Bit index (0–31), or `-1` if all 32 bits are active.
219
+ */
220
+ highestClearBit(): number;
221
+
222
+ /**
223
+ * Tests whether all 32 bits are active.
224
+ *
225
+ * @returns `true` if the value has every bit set.
226
+ */
227
+ isFull(): boolean;
228
+
229
+ /**
230
+ * Returns the number of active bits within an inclusive range
231
+ * `[start, end]`. Caller must guarantee `0 <= start <= end <= 31`.
232
+ *
233
+ * @param start - Lowest bit of the range (inclusive).
234
+ * @param end - Highest bit of the range (inclusive).
235
+ * @returns Active bit count within the range (0–32).
236
+ */
237
+ countRange(start: number, end: number): number;
238
+
239
+ // ── Debug & Init Helpers (allocate — NOT hot-path safe) ──
240
+
241
+ /**
242
+ * Returns a 32-character binary string of the raw value, LSB on the right.
243
+ * Forces unsigned representation so bit 31 prints as `1`, not a minus sign.
244
+ *
245
+ * ⚠️ Allocates a String. Debug use only.
246
+ *
247
+ * @param padded - Pad to 32 chars with leading zeros. Defaults to `true`.
248
+ */
249
+ toBinaryString(padded?: boolean): string;
250
+
251
+ /**
252
+ * Returns an array of active bit indexes in ascending order.
253
+ *
254
+ * ⚠️ Allocates an Array. Not safe for hot loops.
255
+ *
256
+ * @returns Array of bit indexes (0–31).
257
+ */
258
+ toArray(): number[];
259
+
260
+ /**
261
+ * Replaces the current value with a bitmask built from an array of bit
262
+ * indexes. Overwrites — does NOT OR into the existing value. Intended
263
+ * for initialization and deserialization, not hot paths.
264
+ *
265
+ * @param bits - Array of bit indexes (0–31).
266
+ * @returns `this` for chaining.
267
+ */
268
+ fromArray(bits: number[]): this;
269
+
202
270
  // ── Serialization ───────────────────────────────────
203
271
 
204
272
  /**
package/FastBit32.js CHANGED
@@ -146,6 +146,92 @@ export class FastBit32 {
146
146
  }
147
147
  return this;
148
148
  }
149
+
150
+ // ── v1.2.0 AAA Engine Primitives ─────────────────────────
151
+
152
+ /**
153
+ * O(1) loop-free bit-scan forward for the lowest INACTIVE (0) bit.
154
+ * Essential for Object Pools to find the first available slot WITHOUT
155
+ * allocating a scratch FastBit32 for the inverted mask.
156
+ * Returns -1 if all 32 bits are active.
157
+ */
158
+ nextClearBit() {
159
+ const inv = ~this.value >>> 0;
160
+ if (inv === 0) return -1;
161
+ return Math.clz32(inv & -inv) ^ 31;
162
+ }
163
+
164
+ /**
165
+ * O(1) loop-free bit-scan reverse for the highest INACTIVE (0) bit.
166
+ * Returns -1 if all 32 bits are active.
167
+ */
168
+ highestClearBit() {
169
+ const inv = ~this.value >>> 0;
170
+ if (inv === 0) return -1;
171
+ return 31 - Math.clz32(inv);
172
+ }
173
+
174
+ /**
175
+ * O(1) Returns true if all 32 bits are active.
176
+ * Uses `~this.value === 0` because JS bitwise ops yield signed int32,
177
+ * so a fully-set value reads as -1 rather than 0xFFFFFFFF.
178
+ */
179
+ isFull() {
180
+ return ~this.value === 0;
181
+ }
182
+
183
+ /**
184
+ * O(1) active bit count within an inclusive range [start, end].
185
+ * Mask is built with `>>>` to avoid the `1 << 32` wraparound trap.
186
+ * Caller must guarantee 0 <= start <= end <= 31.
187
+ */
188
+ countRange(start, end) {
189
+ const mask = ((0xFFFFFFFF >>> (31 - (end - start))) << start) >>> 0;
190
+ return this.countMasked(mask);
191
+ }
192
+
193
+ // ── Debug & Init Helpers (allocate — NOT hot-path safe) ──
194
+
195
+ /**
196
+ * Returns the 32-bit binary string representation (LSB on the right).
197
+ * Forces unsigned via `>>> 0` so the sign bit prints as a leading `1`
198
+ * instead of triggering `toString(2)`'s minus-sign formatting.
199
+ * ⚠️ Allocates a String. Debug use only.
200
+ */
201
+ toBinaryString(padded = true) {
202
+ const str = (this.value >>> 0).toString(2);
203
+ return padded ? str.padStart(32, '0') : str;
204
+ }
205
+
206
+ /**
207
+ * Returns an array of active bit indexes in ascending order.
208
+ * Inlined O(k) scan — no closure allocation vs. delegating to forEach.
209
+ * ⚠️ Allocates an Array. Do not use in hot loop!
210
+ */
211
+ toArray() {
212
+ const result = [];
213
+ let v = this.value;
214
+ while (v !== 0) {
215
+ result.push(Math.clz32(v & -v) ^ 31);
216
+ v &= v - 1;
217
+ }
218
+ return result;
219
+ }
220
+
221
+ /**
222
+ * Replaces the mask from an array of bit indexes. Note: this OVERWRITES
223
+ * the current value — it does not OR into it. Accumulates in a local and
224
+ * writes `this.value` once for monomorphic-friendly init.
225
+ * ⚠️ Intended for initialization / deserialization, not hot paths.
226
+ */
227
+ fromArray(bits) {
228
+ let v = 0;
229
+ for (let i = 0; i < bits.length; i++) {
230
+ v |= (1 << bits[i]);
231
+ }
232
+ this.value = v >>> 0;
233
+ return this;
234
+ }
149
235
  }
150
236
 
151
237
  export class BitMapper {
package/README.md CHANGED
@@ -12,9 +12,7 @@ Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object poo
12
12
 
13
13
  ## 🚀 Built with Lite-FastBit32: Lite-Tween Pro
14
14
 
15
- If you are building high-performance WebGL applications or JavaScript games, check out **Lite-Tween Pro**. It is a commercial, zero-allocation ECS tweening engine built directly on top of this bitwise architecture.
16
-
17
- It completely bypasses the JavaScript Garbage Collector to guarantee a flat memory profile and a stable 120fps on mobile devices.
15
+ If you are building high-performance WebGL applications or JavaScript games, check out **Lite-Tween Pro**. It is a commercial, zero-allocation ECS tweening engine built directly on top of this bitwise architecture. It completely bypasses the JavaScript Garbage Collector to guarantee a flat memory profile and a stable 120fps on mobile devices.
18
16
 
19
17
  👉 **[Get the Lite-Tween Pro Source Code here](https://zakkster.lemonsqueezy.com/checkout/buy/05ad56be-4b91-4bef-8218-9e85c65690b4)**
20
18
 
@@ -179,7 +177,7 @@ if (entity.hasAll(RENDER_QUERY)) drawSprite(entity);
179
177
  </details>
180
178
 
181
179
  <details>
182
- <summary><strong>Object Pool — First Free Slot</strong></summary>
180
+ <summary><strong>Object Pool — First Free Slot (zero-allocation, v1.2.0)</strong></summary>
183
181
 
184
182
  ```javascript
185
183
  import { FastBit32 } from '@zakkster/lite-fastbit32';
@@ -188,9 +186,8 @@ const pool = new FastBit32();
188
186
  const objects = new Array(32);
189
187
 
190
188
  function allocate() {
191
- const free = new FastBit32(~pool.value & 0xFFFFFFFF);
192
- const slot = free.lowest();
193
- if (slot === -1) return null;
189
+ const slot = pool.nextClearBit(); // O(1), zero allocation
190
+ if (slot === -1) return null; // Pool full
194
191
  pool.add(slot);
195
192
  return slot;
196
193
  }
@@ -205,6 +202,8 @@ release(0);
205
202
  allocate(); // 0 — immediately reused
206
203
  ```
207
204
 
205
+ > Before v1.2.0 this required constructing a scratch `FastBit32` from the inverted mask and calling `.lowest()` on it — one allocation per `allocate()` call. `nextClearBit` collapses that to a single `Math.clz32` on an inverted-and-isolated bit, with no allocations.
206
+
208
207
  </details>
209
208
 
210
209
  <details>
@@ -353,9 +352,13 @@ forEachMaskUnion(required, available, bit => console.log('all:', bit));
353
352
  |---|---|---|
354
353
  | `.count()` | `number` | O(1) popcount — number of active bits (0–32). |
355
354
  | `.countMasked(mask)` | `number` | O(1) popcount within a masked region. |
355
+ | `.countRange(start, end)` | `number` | O(1) popcount within inclusive range `[start, end]`. |
356
356
  | `.lowest()` | `number` | O(1) index of least significant active bit. `-1` if empty. |
357
357
  | `.highest()` | `number` | O(1) index of most significant active bit. `-1` if empty. |
358
+ | `.nextClearBit()` | `number` | O(1) index of least significant **clear** bit. `-1` if full. Zero-alloc pool slot lookup. |
359
+ | `.highestClearBit()` | `number` | O(1) index of most significant **clear** bit. `-1` if full. |
358
360
  | `.isEmpty()` | `boolean` | True if value is `0`. |
361
+ | `.isFull()` | `boolean` | True if all 32 bits are active. |
359
362
 
360
363
  ### Iteration
361
364
 
@@ -371,6 +374,16 @@ forEachMaskUnion(required, available, bit => console.log('all:', bit));
371
374
  | `.serialize()` | `number` | Export raw uint32 for JSON/binary storage. |
372
375
  | `FastBit32.deserialize(n)` | `FastBit32` | Restore from a serialized uint32. |
373
376
 
377
+ ### Debug & Init Helpers
378
+
379
+ > These methods allocate. Do not call them inside hot loops.
380
+
381
+ | Method | Returns | Description |
382
+ |---|---|---|
383
+ | `.toBinaryString(padded?)` | `string` | 32-char binary representation, LSB on the right. Sign-bit safe. |
384
+ | `.toArray()` | `number[]` | Active bit indexes in ascending order. Inlined O(k) — no closure allocation. |
385
+ | `.fromArray(bits)` | `this` | **Replaces** the mask from a bit-index array. Init/deserialization only. |
386
+
374
387
  ### `new BitMapper(names?)`
375
388
 
376
389
  | Method | Returns | Description |
@@ -396,6 +409,16 @@ forEachMaskUnion(required, available, bit => console.log('all:', bit));
396
409
 
397
410
  ## Changelog
398
411
 
412
+ ### v1.2.0
413
+
414
+ **New: Clear-bit scans** — `nextClearBit()` and `highestClearBit()`. O(1) bit-scan on the inverted mask via `Math.clz32`. The object-pool slot-lookup pattern is now truly zero-allocation; the prior `new FastBit32(~pool.value & 0xFFFFFFFF).lowest()` workaround is retired.
415
+
416
+ **New: `isFull()`** — Companion to `isEmpty()`. Uses `~this.value === 0` for correctness across both signed (`-1`) and unsigned (`0xFFFFFFFF`) representations of an all-set int32.
417
+
418
+ **New: `countRange(start, end)`** — O(1) popcount within an inclusive bit range. Mask is built with `>>>` to sidestep the `1 << 32` wraparound.
419
+
420
+ **New: Debug & init helpers** — `toBinaryString(padded?)`, `toArray()`, `fromArray(bits)`. Clearly documented as allocating; kept outside the hot-path API surface. `toBinaryString` forces unsigned via `>>> 0` so bit 31 does not trigger `toString(2)`'s minus-sign formatting. `toArray` inlines the `v &= v - 1` loop rather than delegating to `forEach`, avoiding a per-call closure allocation. `fromArray` **replaces** the current value (does not OR into it) and writes `this.value` exactly once.
421
+
399
422
  ### v1.1.0
400
423
 
401
424
  **New: BitMapper** — Human-to-hardware bridge. Maps semantic string names to bit indices and masks, with O(1) reverse lookup via `getName(bit)`.
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @zakkster/lite-fastbit32 v1.1.0
1
+ # @zakkster/lite-fastbit32 v1.2.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
 
@@ -69,9 +69,13 @@ All mutating methods return `this` for chaining unless noted otherwise.
69
69
  |---|---|---|
70
70
  | `.count()` | `number` | O(1) popcount (Hamming weight). Active bit count, 0–32. |
71
71
  | `.countMasked(mask)` | `number` | O(1) popcount within masked region only. |
72
+ | `.countRange(start, end)` | `number` | O(1) popcount within inclusive range `[start, end]`. Caller must ensure `0 <= start <= end <= 31`. |
72
73
  | `.lowest()` | `number` | Index of least significant set bit (0–31). Returns `-1` if empty. |
73
74
  | `.highest()` | `number` | Index of most significant set bit (0–31). Returns `-1` if empty. |
75
+ | `.nextClearBit()` | `number` | Index of least significant **clear** bit (0–31). Returns `-1` if full. Zero-alloc object-pool slot lookup. |
76
+ | `.highestClearBit()` | `number` | Index of most significant **clear** bit (0–31). Returns `-1` if full. |
74
77
  | `.isEmpty()` | `boolean` | True if value is 0. |
78
+ | `.isFull()` | `boolean` | True if all 32 bits are set. |
75
79
 
76
80
  ### Iteration
77
81
 
@@ -87,6 +91,16 @@ All mutating methods return `this` for chaining unless noted otherwise.
87
91
  | `.serialize()` | `number` | Export raw uint32 for JSON/binary storage. |
88
92
  | `FastBit32.deserialize(n)` | `FastBit32` | Static. Restore from serialized uint32. |
89
93
 
94
+ ### Debug & Init Helpers
95
+
96
+ These methods allocate — keep them out of hot loops.
97
+
98
+ | Method | Returns | Description |
99
+ |---|---|---|
100
+ | `.toBinaryString(padded?)` | `string` | 32-char (or variable) binary string, LSB on the right. Handles the sign bit correctly. Allocates a String. |
101
+ | `.toArray()` | `number[]` | Active bit indexes in ascending order. Inlined O(k) scan (no closure allocation). Allocates an Array. |
102
+ | `.fromArray(bits)` | `this` | **Replaces** the value with a bitmask built from a bit-index array. Intended for init / deserialization. |
103
+
90
104
  ### Property
91
105
 
92
106
  | Property | Type | Description |
@@ -183,15 +197,14 @@ if (entity.hasAll(PHYSICS_QUERY)) {
183
197
  }
184
198
  ```
185
199
 
186
- ### Object Pool — Find First Free Slot
200
+ ### Object Pool — Find First Free Slot (zero-allocation)
187
201
 
188
202
  ```javascript
189
203
  const occupied = new FastBit32();
190
204
 
191
205
  function allocate() {
192
- const free = new FastBit32(~occupied.value & 0xFFFFFFFF);
193
- const slot = free.lowest();
194
- if (slot === -1) return -1; // Pool full
206
+ const slot = occupied.nextClearBit(); // O(1), zero allocation
207
+ if (slot === -1) return -1; // Pool full
195
208
  occupied.add(slot);
196
209
  return slot;
197
210
  }
@@ -199,8 +212,13 @@ function allocate() {
199
212
  function release(slot) {
200
213
  occupied.remove(slot);
201
214
  }
215
+
216
+ // Bail early when the pool is saturated:
217
+ if (occupied.isFull()) return -1;
202
218
  ```
203
219
 
220
+ > Prior to v1.2.0 this pattern required `new FastBit32(~occupied.value & 0xFFFFFFFF)` followed by `.lowest()`, which allocated a scratch instance per `allocate()` call. `nextClearBit` is the direct, zero-GC replacement.
221
+
204
222
  ### Input State Manager
205
223
 
206
224
  ```javascript
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zakkster/lite-fastbit32",
3
3
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
4
- "version": "1.1.3",
4
+ "version": "1.2.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,