@adatechnology/game-shapes 0.1.0 → 0.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/dist/index.js CHANGED
@@ -15,9 +15,11 @@ import {
15
15
  computeVertexNormals,
16
16
  cone,
17
17
  cylinder,
18
+ defaultShapeGenerators,
18
19
  difference,
19
20
  extrude,
20
21
  generate,
22
+ generatorsOver,
21
23
  getGenerator,
22
24
  hash64,
23
25
  hashValue,
@@ -50,7 +52,20 @@ import {
50
52
  transformPoint,
51
53
  unregisterGenerator,
52
54
  validateShape
53
- } from "./chunk-R4RY23RH.js";
55
+ } from "./chunk-IFXQIDSA.js";
56
+
57
+ // src/instance.ts
58
+ import { applyPatches, mergeById, removeIds } from "@adatechnology/game-contracts";
59
+ function createShapes(options = {}) {
60
+ const entryOf = ([id, generator]) => ({ id, generator });
61
+ const entries = mergeById([], (options.packs ?? []).flatMap((pack) => Object.entries(pack).map(entryOf)));
62
+ const patches = {};
63
+ for (const [id, generator] of Object.entries(options.patch ?? {})) patches[id] = { generator };
64
+ applyPatches(entries, patches);
65
+ removeIds(entries, options.remove);
66
+ const generators = generatorsOver(new Map([...entries].map(([id, entry]) => [id, entry.generator])));
67
+ return { ...generators, generate: (schema, generateOptions = {}) => generate(schema, { ...generateOptions, generators: { ...generators.record(), ...generateOptions.generators }, isolatedGenerators: true }) };
68
+ }
54
69
 
55
70
  // src/cache.ts
56
71
  function createShapeCache(options = {}) {
@@ -139,11 +154,11 @@ function createShapePool(options = {}) {
139
154
  };
140
155
  for (let i = 0; i < size; i++) slots.push(spawnSlot());
141
156
  const run = (schema, generateOptions) => {
142
- if (slots.length === 0 || generateOptions.generators) {
157
+ if (slots.length === 0 || generateOptions.generators || options.isolatedGenerators) {
143
158
  inThread++;
144
159
  try {
145
160
  const generators = { ...options.generators ?? {}, ...generateOptions.generators ?? {} };
146
- return Promise.resolve(generate(schema, { ...generateOptions, ...Object.keys(generators).length ? { generators } : {} }));
161
+ return Promise.resolve(generate(schema, { ...generateOptions, ...Object.keys(generators).length ? { generators } : {}, ...options.isolatedGenerators ? { isolatedGenerators: true } : {} }));
147
162
  } catch (error) {
148
163
  return Promise.reject(error);
149
164
  }
@@ -219,7 +234,9 @@ export {
219
234
  cone,
220
235
  createShapeCache,
221
236
  createShapePool,
237
+ createShapes,
222
238
  cylinder,
239
+ defaultShapeGenerators,
223
240
  difference,
224
241
  extrude,
225
242
  generate,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cache.ts","../src/pool.ts"],"sourcesContent":["import { byteSizeOf } from './merge'\nimport type { ShapeBuffers } from './types'\n\nexport interface ShapeCacheOptions {\n /** Most results kept (default 256). */\n readonly maxEntries?: number\n /** Most bytes of arrays kept (default 64 MiB). A result larger than this alone is not kept. */\n readonly maxBytes?: number\n}\n\nexport interface ShapeCacheStats {\n readonly entries: number\n readonly bytes: number\n readonly hits: number\n readonly misses: number\n readonly evictions: number\n}\n\n/** An LRU of results by `shapeKey`. What it returns is shared: read-only (copy with `cloneBuffers` before deforming). */\nexport interface ShapeCache {\n get(key: string): ShapeBuffers | undefined\n set(key: string, buffers: ShapeBuffers): void\n has(key: string): boolean\n delete(key: string): boolean\n clear(): void\n stats(): ShapeCacheStats\n}\n\n/** An LRU of `ShapeBuffers` by key, bounded by entries and by bytes of the arrays. Pure memoisation: a hit gives the bytes a miss would. */\nexport function createShapeCache(options: ShapeCacheOptions = {}): ShapeCache {\n const maxEntries = options.maxEntries ?? 256\n const maxBytes = options.maxBytes ?? 64 * 1024 * 1024\n const entries = new Map<string, { buffers: ShapeBuffers; bytes: number }>()\n let bytes = 0\n let hits = 0\n let misses = 0\n let evictions = 0\n const drop = (key: string): boolean => {\n const entry = entries.get(key)\n if (!entry) return false\n entries.delete(key)\n bytes -= entry.bytes\n return true\n }\n return {\n get(key) {\n const entry = entries.get(key)\n if (!entry) {\n misses++\n return undefined\n }\n hits++\n // most recently used goes last\n entries.delete(key)\n entries.set(key, entry)\n return entry.buffers\n },\n set(key, buffers) {\n drop(key)\n const size = byteSizeOf(buffers)\n if (size > maxBytes || maxEntries <= 0) return\n entries.set(key, { buffers, bytes: size })\n bytes += size\n for (const oldest of entries.keys()) {\n if (entries.size <= maxEntries && bytes <= maxBytes) break\n drop(oldest)\n evictions++\n }\n },\n has: (key) => entries.has(key),\n delete: drop,\n clear() {\n entries.clear()\n bytes = 0\n },\n stats: () => ({ entries: entries.size, bytes, hits, misses, evictions }),\n }\n}\n","import type { ShapeCache } from './cache'\nimport { generate, shapeKey } from './generate'\nimport type { GenerateOptions, ShapeBuffers, ShapeGenerator, ShapeSchema } from './types'\nimport type { ShapeJob, ShapeReply } from './worker'\n\n/** Anything with `postMessage` / `onmessage` / `terminate`: a browser `Worker`, a `worker_threads` wrapper, an in-thread fake. */\nexport interface WorkerLike {\n onmessage: ((event: { data: unknown }) => void) | null\n onerror?: ((event: unknown) => void) | null\n postMessage(message: unknown, transfer?: ArrayBuffer[]): void\n terminate(): unknown\n}\n\nexport interface ShapePoolOptions {\n /** Workers (default 0: generate in the calling thread). */\n readonly size?: number\n /** Makes one worker that runs `installShapeWorker`. Without it the pool generates in the calling thread. */\n readonly spawn?: () => WorkerLike\n /** Results by `shapeKey`: a hit resolves at once and never reaches a worker. */\n readonly cache?: ShapeCache\n /** Generators of the `custom` nodes for the jobs made in the calling thread (the workers register their own). */\n readonly generators?: Readonly<Record<string, ShapeGenerator>>\n}\n\nexport interface ShapeRequest {\n readonly schema: ShapeSchema\n readonly options?: GenerateOptions\n}\n\nexport interface ShapePoolStats {\n readonly workers: number\n readonly jobs: number\n readonly cacheHits: number\n readonly shared: number\n readonly inThread: number\n readonly busy: number\n}\n\nexport interface ShapePool {\n /** `generate(schema, options)` off the main thread. Same key, same promise while in flight; the result is shared (read-only). */\n generate(schema: ShapeSchema, options?: GenerateOptions): Promise<ShapeBuffers>\n /** A list spread over the workers, resolved in input order. */\n generateMany(list: readonly (ShapeSchema | ShapeRequest)[]): Promise<ShapeBuffers[]>\n stats(): ShapePoolStats\n /** Stops the workers; pending jobs reject. */\n terminate(): void\n}\n\ninterface Slot {\n worker: WorkerLike\n pending: Map<number, { resolve: (b: ShapeBuffers) => void; reject: (e: Error) => void }>\n}\n\nconst isRequest = (value: ShapeSchema | ShapeRequest): value is ShapeRequest => (value as ShapeRequest).schema !== undefined && (value as ShapeSchema).root === undefined\n\n/**\n * A pool of shape workers. The schema is cheap (plain data) and is built on the main thread; the buffers are generated in a worker and\n * transferred back. With `size: 0` or no `spawn` it generates in the calling thread, with the same cache and in-flight sharing.\n */\nexport function createShapePool(options: ShapePoolOptions = {}): ShapePool {\n const size = options.spawn ? Math.max(0, options.size ?? 0) : 0\n const cache = options.cache\n const slots: Slot[] = []\n const inFlight = new Map<string, Promise<ShapeBuffers>>()\n let nextId = 1\n let jobs = 0\n let cacheHits = 0\n let shared = 0\n let inThread = 0\n let terminated = false\n\n const failAll = (slot: Slot, error: Error): void => {\n for (const job of slot.pending.values()) job.reject(error)\n slot.pending.clear()\n }\n const spawnSlot = (): Slot => {\n const slot: Slot = { worker: (options.spawn as () => WorkerLike)(), pending: new Map() }\n slot.worker.onmessage = (event) => {\n const reply = event.data as ShapeReply\n const job = slot.pending.get(reply.id)\n if (!job) return\n slot.pending.delete(reply.id)\n if (reply.ok) job.resolve(reply.buffers)\n else job.reject(new Error(reply.error))\n }\n slot.worker.onerror = (event) => {\n // a worker that crashed takes its jobs with it; a fresh one takes its place\n failAll(slot, new Error(`game-shapes worker failed: ${(event as { message?: string })?.message ?? String(event)}`))\n slot.worker.terminate()\n const index = slots.indexOf(slot)\n if (index >= 0 && !terminated) slots[index] = spawnSlot()\n }\n return slot\n }\n for (let i = 0; i < size; i++) slots.push(spawnSlot())\n\n const run = (schema: ShapeSchema, generateOptions: GenerateOptions): Promise<ShapeBuffers> => {\n // custom generators passed per call are functions: they cannot reach a worker, so that job runs here\n if (slots.length === 0 || generateOptions.generators) {\n inThread++\n try {\n const generators = { ...(options.generators ?? {}), ...(generateOptions.generators ?? {}) }\n return Promise.resolve(generate(schema, { ...generateOptions, ...(Object.keys(generators).length ? { generators } : {}) }))\n } catch (error) {\n return Promise.reject(error)\n }\n }\n let slot = slots[0] as Slot\n for (const s of slots) if (s.pending.size < slot.pending.size) slot = s\n const id = nextId++\n const job: ShapeJob = { id, schema, options: generateOptions }\n return new Promise<ShapeBuffers>((resolve, reject) => {\n slot.pending.set(id, { resolve, reject })\n slot.worker.postMessage(job)\n })\n }\n\n const pool: ShapePool = {\n generate(schema, generateOptions = {}) {\n if (terminated) return Promise.reject(new Error('createShapePool: the pool was terminated'))\n jobs++\n const key = shapeKey(schema, generateOptions)\n const hit = cache?.get(key)\n if (hit) {\n cacheHits++\n return Promise.resolve(hit)\n }\n const flying = inFlight.get(key)\n if (flying) {\n shared++\n return flying\n }\n const promise = run(schema, generateOptions).then(\n (buffers) => {\n inFlight.delete(key)\n cache?.set(key, buffers)\n return buffers\n },\n (error: unknown) => {\n inFlight.delete(key)\n throw error\n },\n )\n inFlight.set(key, promise)\n return promise\n },\n generateMany(list) {\n return Promise.all(list.map((item) => (isRequest(item) ? pool.generate(item.schema, item.options) : pool.generate(item))))\n },\n stats: () => ({ workers: slots.length, jobs, cacheHits, shared, inThread, busy: slots.reduce((n, s) => n + s.pending.size, 0) }),\n terminate() {\n terminated = true\n for (const slot of slots) {\n failAll(slot, new Error('createShapePool: the pool was terminated'))\n slot.worker.terminate()\n }\n slots.length = 0\n },\n }\n return pool\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BO,SAAS,iBAAiB,UAA6B,CAAC,GAAe;AAC5E,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,WAAW,QAAQ,YAAY,KAAK,OAAO;AACjD,QAAM,UAAU,oBAAI,IAAsD;AAC1E,MAAI,QAAQ;AACZ,MAAI,OAAO;AACX,MAAI,SAAS;AACb,MAAI,YAAY;AAChB,QAAM,OAAO,CAAC,QAAyB;AACrC,UAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,QAAI,CAAC,MAAO,QAAO;AACnB,YAAQ,OAAO,GAAG;AAClB,aAAS,MAAM;AACf,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,IAAI,KAAK;AACP,YAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,UAAI,CAAC,OAAO;AACV;AACA,eAAO;AAAA,MACT;AACA;AAEA,cAAQ,OAAO,GAAG;AAClB,cAAQ,IAAI,KAAK,KAAK;AACtB,aAAO,MAAM;AAAA,IACf;AAAA,IACA,IAAI,KAAK,SAAS;AAChB,WAAK,GAAG;AACR,YAAM,OAAO,WAAW,OAAO;AAC/B,UAAI,OAAO,YAAY,cAAc,EAAG;AACxC,cAAQ,IAAI,KAAK,EAAE,SAAS,OAAO,KAAK,CAAC;AACzC,eAAS;AACT,iBAAW,UAAU,QAAQ,KAAK,GAAG;AACnC,YAAI,QAAQ,QAAQ,cAAc,SAAS,SAAU;AACrD,aAAK,MAAM;AACX;AAAA,MACF;AAAA,IACF;AAAA,IACA,KAAK,CAAC,QAAQ,QAAQ,IAAI,GAAG;AAAA,IAC7B,QAAQ;AAAA,IACR,QAAQ;AACN,cAAQ,MAAM;AACd,cAAQ;AAAA,IACV;AAAA,IACA,OAAO,OAAO,EAAE,SAAS,QAAQ,MAAM,OAAO,MAAM,QAAQ,UAAU;AAAA,EACxE;AACF;;;ACxBA,IAAM,YAAY,CAAC,UAA8D,MAAuB,WAAW,UAAc,MAAsB,SAAS;AAMzJ,SAAS,gBAAgB,UAA4B,CAAC,GAAc;AACzE,QAAM,OAAO,QAAQ,QAAQ,KAAK,IAAI,GAAG,QAAQ,QAAQ,CAAC,IAAI;AAC9D,QAAM,QAAQ,QAAQ;AACtB,QAAM,QAAgB,CAAC;AACvB,QAAM,WAAW,oBAAI,IAAmC;AACxD,MAAI,SAAS;AACb,MAAI,OAAO;AACX,MAAI,YAAY;AAChB,MAAI,SAAS;AACb,MAAI,WAAW;AACf,MAAI,aAAa;AAEjB,QAAM,UAAU,CAAC,MAAY,UAAuB;AAClD,eAAW,OAAO,KAAK,QAAQ,OAAO,EAAG,KAAI,OAAO,KAAK;AACzD,SAAK,QAAQ,MAAM;AAAA,EACrB;AACA,QAAM,YAAY,MAAY;AAC5B,UAAM,OAAa,EAAE,QAAS,QAAQ,MAA2B,GAAG,SAAS,oBAAI,IAAI,EAAE;AACvF,SAAK,OAAO,YAAY,CAAC,UAAU;AACjC,YAAM,QAAQ,MAAM;AACpB,YAAM,MAAM,KAAK,QAAQ,IAAI,MAAM,EAAE;AACrC,UAAI,CAAC,IAAK;AACV,WAAK,QAAQ,OAAO,MAAM,EAAE;AAC5B,UAAI,MAAM,GAAI,KAAI,QAAQ,MAAM,OAAO;AAAA,UAClC,KAAI,OAAO,IAAI,MAAM,MAAM,KAAK,CAAC;AAAA,IACxC;AACA,SAAK,OAAO,UAAU,CAAC,UAAU;AAE/B,cAAQ,MAAM,IAAI,MAAM,8BAA+B,OAAgC,WAAW,OAAO,KAAK,CAAC,EAAE,CAAC;AAClH,WAAK,OAAO,UAAU;AACtB,YAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,UAAI,SAAS,KAAK,CAAC,WAAY,OAAM,KAAK,IAAI,UAAU;AAAA,IAC1D;AACA,WAAO;AAAA,EACT;AACA,WAAS,IAAI,GAAG,IAAI,MAAM,IAAK,OAAM,KAAK,UAAU,CAAC;AAErD,QAAM,MAAM,CAAC,QAAqB,oBAA4D;AAE5F,QAAI,MAAM,WAAW,KAAK,gBAAgB,YAAY;AACpD;AACA,UAAI;AACF,cAAM,aAAa,EAAE,GAAI,QAAQ,cAAc,CAAC,GAAI,GAAI,gBAAgB,cAAc,CAAC,EAAG;AAC1F,eAAO,QAAQ,QAAQ,SAAS,QAAQ,EAAE,GAAG,iBAAiB,GAAI,OAAO,KAAK,UAAU,EAAE,SAAS,EAAE,WAAW,IAAI,CAAC,EAAG,CAAC,CAAC;AAAA,MAC5H,SAAS,OAAO;AACd,eAAO,QAAQ,OAAO,KAAK;AAAA,MAC7B;AAAA,IACF;AACA,QAAI,OAAO,MAAM,CAAC;AAClB,eAAW,KAAK,MAAO,KAAI,EAAE,QAAQ,OAAO,KAAK,QAAQ,KAAM,QAAO;AACtE,UAAM,KAAK;AACX,UAAM,MAAgB,EAAE,IAAI,QAAQ,SAAS,gBAAgB;AAC7D,WAAO,IAAI,QAAsB,CAAC,SAAS,WAAW;AACpD,WAAK,QAAQ,IAAI,IAAI,EAAE,SAAS,OAAO,CAAC;AACxC,WAAK,OAAO,YAAY,GAAG;AAAA,IAC7B,CAAC;AAAA,EACH;AAEA,QAAM,OAAkB;AAAA,IACtB,SAAS,QAAQ,kBAAkB,CAAC,GAAG;AACrC,UAAI,WAAY,QAAO,QAAQ,OAAO,IAAI,MAAM,0CAA0C,CAAC;AAC3F;AACA,YAAM,MAAM,SAAS,QAAQ,eAAe;AAC5C,YAAM,MAAM,OAAO,IAAI,GAAG;AAC1B,UAAI,KAAK;AACP;AACA,eAAO,QAAQ,QAAQ,GAAG;AAAA,MAC5B;AACA,YAAM,SAAS,SAAS,IAAI,GAAG;AAC/B,UAAI,QAAQ;AACV;AACA,eAAO;AAAA,MACT;AACA,YAAM,UAAU,IAAI,QAAQ,eAAe,EAAE;AAAA,QAC3C,CAAC,YAAY;AACX,mBAAS,OAAO,GAAG;AACnB,iBAAO,IAAI,KAAK,OAAO;AACvB,iBAAO;AAAA,QACT;AAAA,QACA,CAAC,UAAmB;AAClB,mBAAS,OAAO,GAAG;AACnB,gBAAM;AAAA,QACR;AAAA,MACF;AACA,eAAS,IAAI,KAAK,OAAO;AACzB,aAAO;AAAA,IACT;AAAA,IACA,aAAa,MAAM;AACjB,aAAO,QAAQ,IAAI,KAAK,IAAI,CAAC,SAAU,UAAU,IAAI,IAAI,KAAK,SAAS,KAAK,QAAQ,KAAK,OAAO,IAAI,KAAK,SAAS,IAAI,CAAE,CAAC;AAAA,IAC3H;AAAA,IACA,OAAO,OAAO,EAAE,SAAS,MAAM,QAAQ,MAAM,WAAW,QAAQ,UAAU,MAAM,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,MAAM,CAAC,EAAE;AAAA,IAC9H,YAAY;AACV,mBAAa;AACb,iBAAW,QAAQ,OAAO;AACxB,gBAAQ,MAAM,IAAI,MAAM,0CAA0C,CAAC;AACnE,aAAK,OAAO,UAAU;AAAA,MACxB;AACA,YAAM,SAAS;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/instance.ts","../src/cache.ts","../src/pool.ts"],"sourcesContent":["import { applyPatches, mergeById, removeIds } from '@adatechnology/game-contracts'\n\nimport { generate } from './generate'\nimport { generatorsOver } from './registry'\nimport type { ShapeGenerators } from './registry'\nimport type { GenerateOptions, ShapeBuffers, ShapeGenerator, ShapeSchema } from './types'\n\nexport interface ShapesOptions {\n /** Generators by name, added to the set; later packs replace earlier ones with the same name. */\n readonly packs?: readonly Readonly<Record<string, ShapeGenerator>>[]\n /** A generator replaced by name (a function is replaced as a whole). */\n readonly patch?: Readonly<Record<string, ShapeGenerator>>\n /** Names taken out. */\n readonly remove?: readonly string[]\n}\n\nexport interface Shapes extends ShapeGenerators {\n /** `generate` that looks custom nodes up in this set only (a name only the global registry has throws). */\n generate(schema: ShapeSchema, options?: Omit<GenerateOptions, 'isolatedGenerators'>): ShapeBuffers\n}\n\ntype Entry = { readonly id: string; readonly generator: ShapeGenerator }\n\n/** A set of shape generators of its own. Nothing is shared with any other set or with the global registry. */\nexport function createShapes(options: ShapesOptions = {}): Shapes {\n const entryOf = ([id, generator]: [string, ShapeGenerator]): Entry => ({ id, generator })\n const entries = mergeById<Entry>([], (options.packs ?? []).flatMap((pack) => Object.entries(pack).map(entryOf)))\n const patches: Record<string, Partial<Entry>> = {}\n for (const [id, generator] of Object.entries(options.patch ?? {})) patches[id] = { generator }\n applyPatches(entries, patches)\n removeIds(entries, options.remove)\n const generators = generatorsOver(new Map([...entries].map(([id, entry]) => [id, entry.generator])))\n return { ...generators, generate: (schema, generateOptions = {}) => generate(schema, { ...generateOptions, generators: { ...generators.record(), ...generateOptions.generators }, isolatedGenerators: true }) }\n}\n","import { byteSizeOf } from './merge'\nimport type { ShapeBuffers } from './types'\n\nexport interface ShapeCacheOptions {\n /** Most results kept (default 256). */\n readonly maxEntries?: number\n /** Most bytes of arrays kept (default 64 MiB). A result larger than this alone is not kept. */\n readonly maxBytes?: number\n}\n\nexport interface ShapeCacheStats {\n readonly entries: number\n readonly bytes: number\n readonly hits: number\n readonly misses: number\n readonly evictions: number\n}\n\n/** An LRU of results by `shapeKey`. What it returns is shared: read-only (copy with `cloneBuffers` before deforming). */\nexport interface ShapeCache {\n get(key: string): ShapeBuffers | undefined\n set(key: string, buffers: ShapeBuffers): void\n has(key: string): boolean\n delete(key: string): boolean\n clear(): void\n stats(): ShapeCacheStats\n}\n\n/** An LRU of `ShapeBuffers` by key, bounded by entries and by bytes of the arrays. Pure memoisation: a hit gives the bytes a miss would. */\nexport function createShapeCache(options: ShapeCacheOptions = {}): ShapeCache {\n const maxEntries = options.maxEntries ?? 256\n const maxBytes = options.maxBytes ?? 64 * 1024 * 1024\n const entries = new Map<string, { buffers: ShapeBuffers; bytes: number }>()\n let bytes = 0\n let hits = 0\n let misses = 0\n let evictions = 0\n const drop = (key: string): boolean => {\n const entry = entries.get(key)\n if (!entry) return false\n entries.delete(key)\n bytes -= entry.bytes\n return true\n }\n return {\n get(key) {\n const entry = entries.get(key)\n if (!entry) {\n misses++\n return undefined\n }\n hits++\n // most recently used goes last\n entries.delete(key)\n entries.set(key, entry)\n return entry.buffers\n },\n set(key, buffers) {\n drop(key)\n const size = byteSizeOf(buffers)\n if (size > maxBytes || maxEntries <= 0) return\n entries.set(key, { buffers, bytes: size })\n bytes += size\n for (const oldest of entries.keys()) {\n if (entries.size <= maxEntries && bytes <= maxBytes) break\n drop(oldest)\n evictions++\n }\n },\n has: (key) => entries.has(key),\n delete: drop,\n clear() {\n entries.clear()\n bytes = 0\n },\n stats: () => ({ entries: entries.size, bytes, hits, misses, evictions }),\n }\n}\n","import type { ShapeCache } from './cache'\nimport { generate, shapeKey } from './generate'\nimport type { GenerateOptions, ShapeBuffers, ShapeGenerator, ShapeSchema } from './types'\nimport type { ShapeJob, ShapeReply } from './worker'\n\n/** Anything with `postMessage` / `onmessage` / `terminate`: a browser `Worker`, a `worker_threads` wrapper, an in-thread fake. */\nexport interface WorkerLike {\n onmessage: ((event: { data: unknown }) => void) | null\n onerror?: ((event: unknown) => void) | null\n postMessage(message: unknown, transfer?: ArrayBuffer[]): void\n terminate(): unknown\n}\n\nexport interface ShapePoolOptions {\n /** Workers (default 0: generate in the calling thread). */\n readonly size?: number\n /** Makes one worker that runs `installShapeWorker`. Without it the pool generates in the calling thread. */\n readonly spawn?: () => WorkerLike\n /** Results by `shapeKey`: a hit resolves at once and never reaches a worker. */\n readonly cache?: ShapeCache\n /** Generators of the `custom` nodes for the jobs made in the calling thread (the workers register their own). */\n readonly generators?: Readonly<Record<string, ShapeGenerator>>\n /** Look custom nodes up in `generators` only (for example `shapes.record()` of `createShapes`), never in the global registry; the jobs then run in the calling thread, since a worker cannot receive functions. */\n readonly isolatedGenerators?: boolean\n}\n\nexport interface ShapeRequest {\n readonly schema: ShapeSchema\n readonly options?: GenerateOptions\n}\n\nexport interface ShapePoolStats {\n readonly workers: number\n readonly jobs: number\n readonly cacheHits: number\n readonly shared: number\n readonly inThread: number\n readonly busy: number\n}\n\nexport interface ShapePool {\n /** `generate(schema, options)` off the main thread. Same key, same promise while in flight; the result is shared (read-only). */\n generate(schema: ShapeSchema, options?: GenerateOptions): Promise<ShapeBuffers>\n /** A list spread over the workers, resolved in input order. */\n generateMany(list: readonly (ShapeSchema | ShapeRequest)[]): Promise<ShapeBuffers[]>\n stats(): ShapePoolStats\n /** Stops the workers; pending jobs reject. */\n terminate(): void\n}\n\ninterface Slot {\n worker: WorkerLike\n pending: Map<number, { resolve: (b: ShapeBuffers) => void; reject: (e: Error) => void }>\n}\n\nconst isRequest = (value: ShapeSchema | ShapeRequest): value is ShapeRequest => (value as ShapeRequest).schema !== undefined && (value as ShapeSchema).root === undefined\n\n/**\n * A pool of shape workers. The schema is cheap (plain data) and is built on the main thread; the buffers are generated in a worker and\n * transferred back. With `size: 0` or no `spawn` it generates in the calling thread, with the same cache and in-flight sharing.\n */\nexport function createShapePool(options: ShapePoolOptions = {}): ShapePool {\n const size = options.spawn ? Math.max(0, options.size ?? 0) : 0\n const cache = options.cache\n const slots: Slot[] = []\n const inFlight = new Map<string, Promise<ShapeBuffers>>()\n let nextId = 1\n let jobs = 0\n let cacheHits = 0\n let shared = 0\n let inThread = 0\n let terminated = false\n\n const failAll = (slot: Slot, error: Error): void => {\n for (const job of slot.pending.values()) job.reject(error)\n slot.pending.clear()\n }\n const spawnSlot = (): Slot => {\n const slot: Slot = { worker: (options.spawn as () => WorkerLike)(), pending: new Map() }\n slot.worker.onmessage = (event) => {\n const reply = event.data as ShapeReply\n const job = slot.pending.get(reply.id)\n if (!job) return\n slot.pending.delete(reply.id)\n if (reply.ok) job.resolve(reply.buffers)\n else job.reject(new Error(reply.error))\n }\n slot.worker.onerror = (event) => {\n // a worker that crashed takes its jobs with it; a fresh one takes its place\n failAll(slot, new Error(`game-shapes worker failed: ${(event as { message?: string })?.message ?? String(event)}`))\n slot.worker.terminate()\n const index = slots.indexOf(slot)\n if (index >= 0 && !terminated) slots[index] = spawnSlot()\n }\n return slot\n }\n for (let i = 0; i < size; i++) slots.push(spawnSlot())\n\n const run = (schema: ShapeSchema, generateOptions: GenerateOptions): Promise<ShapeBuffers> => {\n // custom generators passed per call are functions: they cannot reach a worker, so that job runs here\n if (slots.length === 0 || generateOptions.generators || options.isolatedGenerators) {\n inThread++\n try {\n const generators = { ...(options.generators ?? {}), ...(generateOptions.generators ?? {}) }\n return Promise.resolve(generate(schema, { ...generateOptions, ...(Object.keys(generators).length ? { generators } : {}), ...(options.isolatedGenerators ? { isolatedGenerators: true } : {}) }))\n } catch (error) {\n return Promise.reject(error)\n }\n }\n let slot = slots[0] as Slot\n for (const s of slots) if (s.pending.size < slot.pending.size) slot = s\n const id = nextId++\n const job: ShapeJob = { id, schema, options: generateOptions }\n return new Promise<ShapeBuffers>((resolve, reject) => {\n slot.pending.set(id, { resolve, reject })\n slot.worker.postMessage(job)\n })\n }\n\n const pool: ShapePool = {\n generate(schema, generateOptions = {}) {\n if (terminated) return Promise.reject(new Error('createShapePool: the pool was terminated'))\n jobs++\n const key = shapeKey(schema, generateOptions)\n const hit = cache?.get(key)\n if (hit) {\n cacheHits++\n return Promise.resolve(hit)\n }\n const flying = inFlight.get(key)\n if (flying) {\n shared++\n return flying\n }\n const promise = run(schema, generateOptions).then(\n (buffers) => {\n inFlight.delete(key)\n cache?.set(key, buffers)\n return buffers\n },\n (error: unknown) => {\n inFlight.delete(key)\n throw error\n },\n )\n inFlight.set(key, promise)\n return promise\n },\n generateMany(list) {\n return Promise.all(list.map((item) => (isRequest(item) ? pool.generate(item.schema, item.options) : pool.generate(item))))\n },\n stats: () => ({ workers: slots.length, jobs, cacheHits, shared, inThread, busy: slots.reduce((n, s) => n + s.pending.size, 0) }),\n terminate() {\n terminated = true\n for (const slot of slots) {\n failAll(slot, new Error('createShapePool: the pool was terminated'))\n slot.worker.terminate()\n }\n slots.length = 0\n },\n }\n return pool\n}\n"],"mappings":"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","names":[]}
@@ -341,6 +341,8 @@ interface GenerateOptions {
341
341
  readonly levels?: readonly number[];
342
342
  /** Generators besides the registered ones. */
343
343
  readonly generators?: Readonly<Record<string, ShapeGenerator>>;
344
+ /** Look custom nodes up only in `generators`, never in the global registry (what a `createShapes` set does). */
345
+ readonly isolatedGenerators?: boolean;
344
346
  /** Validate the schema first (default true). */
345
347
  readonly validate?: boolean;
346
348
  /**
package/dist/worker.d.ts CHANGED
@@ -1 +1 @@
1
- export { x as ShapeJob, J as ShapeReply, O as ShapeWorkerOptions, Y as WorkerScopeLike, Z as installShapeWorker } from './worker-W5A14FiP.js';
1
+ export { x as ShapeJob, J as ShapeReply, O as ShapeWorkerOptions, Y as WorkerScopeLike, Z as installShapeWorker } from './worker-BZ7plgyI.js';
package/dist/worker.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  installShapeWorker
3
- } from "./chunk-R4RY23RH.js";
3
+ } from "./chunk-IFXQIDSA.js";
4
4
  export {
5
5
  installShapeWorker
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/game-shapes",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "description": "Renderer-agnostic 3D shape generator: a JSON shape schema (primitives, extrusions, lathes, cheap booleans, transforms, groups, named parts, materials by id, LOD rules, instancing hints) in, typed-array buffers per material out. Pure, deterministic by seed, worker friendly and cacheable; the generators are bit-exact ports of three.js r160.",
6
6
  "keywords": [
@@ -43,6 +43,9 @@
43
43
  "dist",
44
44
  "schema"
45
45
  ],
46
+ "dependencies": {
47
+ "@adatechnology/game-contracts": "^0.22.0"
48
+ },
46
49
  "devDependencies": {
47
50
  "@types/node": "^22.10.7",
48
51
  "@types/three": "^0.160.0",