@aethervtt/shard-procgen 0.0.1

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.
@@ -0,0 +1,553 @@
1
+ import {
2
+ AssetStore,
3
+ assetServer,
4
+ defineAssetType,
5
+ findAssetType,
6
+ sha256Hex
7
+ } from "@aethervtt/shard-assets";
8
+ import {
9
+ defineResource,
10
+ isPlainObject,
11
+ ShardError
12
+ } from "@aethervtt/shard-core";
13
+ import { loadNoiseKernel } from "@aethervtt/shard-noise";
14
+ import { LogResource } from "@aethervtt/shard-runtime";
15
+ import {
16
+ codeHashOf,
17
+ guidOf,
18
+ identityOf,
19
+ proceduralPath,
20
+ requestOf,
21
+ requireGenerator
22
+ } from "./generator.js";
23
+ import { executeJob } from "./job.js";
24
+ import { checkFragment } from "./outputs.js";
25
+ import { decodeRecord, encodeRecord, recordBytes } from "./record.js";
26
+ const encoder = new TextEncoder();
27
+ const GeneratorBindings = defineResource(
28
+ "procgen/Generators",
29
+ {
30
+ description: "Generator assets (a generator and default params) by guid.",
31
+ init: () => new AssetStore("Generator")
32
+ }
33
+ );
34
+ const GeneratorAssetType = defineAssetType("Generator", {
35
+ store: GeneratorBindings,
36
+ load: (artifact) => artifact.json
37
+ });
38
+ const GeneratedData = defineResource("procgen/Data", {
39
+ description: "Values of generators whose output is untyped data.",
40
+ init: () => new AssetStore("Data")
41
+ });
42
+ const DataAssetType = defineAssetType("Data", {
43
+ store: GeneratedData,
44
+ load: (artifact) => artifact.json ?? null
45
+ });
46
+ let host = {};
47
+ function configureProcgenHost(options) {
48
+ host = { ...host, ...options };
49
+ }
50
+ function procgenHost() {
51
+ return host;
52
+ }
53
+ async function warmGeneratorWorkers() {
54
+ const { workers } = host;
55
+ const workerModule = await host.workerModule;
56
+ if (!workers || workers.size === 0 || !workerModule) return;
57
+ const jobs = [];
58
+ for (let i = 0; i < workers.size; i++) jobs.push(workers.run(workerModule, "warmUp", []));
59
+ await Promise.all(jobs);
60
+ }
61
+ const SLICE_MS = 0.25;
62
+ const waiting = [];
63
+ let sliceStart = -1;
64
+ let pumping = false;
65
+ let stepStart = -1;
66
+ let mainThreadMs = 0;
67
+ const nextTask = typeof setImmediate === "function" ? (run) => void setImmediate(run) : (run) => {
68
+ const channel = new MessageChannel();
69
+ channel.port1.onmessage = () => run();
70
+ channel.port2.postMessage(0);
71
+ };
72
+ function pump() {
73
+ if (stepStart >= 0) {
74
+ mainThreadMs += performance.now() - stepStart;
75
+ stepStart = -1;
76
+ }
77
+ if (waiting.length === 0) {
78
+ sliceStart = -1;
79
+ pumping = false;
80
+ return;
81
+ }
82
+ if (sliceStart < 0) sliceStart = performance.now();
83
+ else if (performance.now() - sliceStart > SLICE_MS) {
84
+ sliceStart = -1;
85
+ nextTask(pump);
86
+ return;
87
+ }
88
+ stepStart = performance.now();
89
+ waiting.shift()();
90
+ queueMicrotask(pump);
91
+ }
92
+ function procgenMainThreadMs() {
93
+ return mainThreadMs;
94
+ }
95
+ function mainThreadTurn() {
96
+ return new Promise((resolve) => {
97
+ waiting.push(resolve);
98
+ if (!pumping) {
99
+ pumping = true;
100
+ nextTask(pump);
101
+ }
102
+ });
103
+ }
104
+ let running = 0;
105
+ const queued = [];
106
+ async function runJob(job) {
107
+ const pool = host.workers;
108
+ const workerModule = await host.workerModule;
109
+ if (pool && pool.size > 0 && workerModule && job.deps.every((d) => d.artifact)) {
110
+ const kernel = await loadNoiseKernel();
111
+ if (running >= pool.size * 2) await new Promise((resolve) => queued.push(resolve));
112
+ running++;
113
+ try {
114
+ await mainThreadTurn();
115
+ const posted = {
116
+ ...job,
117
+ // The worker bundle can be older than this thread's component schemas.
118
+ checkComponents: false,
119
+ deps: job.deps.map(({ object: _o, ...d }) => d),
120
+ noise: kernel.module,
121
+ noiseSimd: kernel.simd
122
+ };
123
+ const { packed } = await pool.run(workerModule, "runGeneratorJob", [
124
+ posted
125
+ ]);
126
+ const record = decodeRecord(packed);
127
+ if (!record) {
128
+ throw new ShardError(
129
+ "procgen/generator-failed",
130
+ `${job.generator}: a worker sent back no record`
131
+ );
132
+ }
133
+ const gen = requireGenerator(job.generator);
134
+ if (gen.output === "entities") {
135
+ await mainThreadTurn();
136
+ for (const a of record.assets) if (a.type === "Prefab") checkFragment(gen, a);
137
+ }
138
+ return {
139
+ assets: record.assets,
140
+ children: record.children,
141
+ warnings: record.warnings,
142
+ ms: record.ms,
143
+ packed
144
+ };
145
+ } finally {
146
+ running--;
147
+ queued.shift()?.();
148
+ }
149
+ }
150
+ await Promise.resolve();
151
+ return executeJob(job);
152
+ }
153
+ function artifactOf(asset) {
154
+ const out = {};
155
+ if (asset.bytes) out.bytes = asset.bytes;
156
+ if (asset.json !== void 0) out.json = asset.json;
157
+ return out;
158
+ }
159
+ function collectHandles(fields, value, out) {
160
+ if (!isPlainObject(value)) return;
161
+ for (const [key, field] of Object.entries(fields)) visitHandle(field, value[key], out);
162
+ }
163
+ function visitHandle(field, value, out) {
164
+ if (value === null || value === void 0) return;
165
+ if (field.kind === "handle" && isPlainObject(value)) {
166
+ out.push(value);
167
+ } else if (field.kind === "struct" && field.fields) {
168
+ collectHandles(field.fields, value, out);
169
+ } else if (field.kind === "list" && field.item && Array.isArray(value)) {
170
+ for (const v of value) visitHandle(field.item, v, out);
171
+ }
172
+ }
173
+ function paramHandles(gen, params) {
174
+ const out = [];
175
+ collectHandles(gen.params.fields, params, out);
176
+ return out;
177
+ }
178
+ class ProcgenRuntime {
179
+ world;
180
+ server;
181
+ options;
182
+ stats = { runs: 0, memoryHits: 0, diskHits: 0, failures: 0, mainMs: 0 };
183
+ memory = /* @__PURE__ */ new Map();
184
+ memoryBytes = 0;
185
+ stored = [];
186
+ storedBytes = 0;
187
+ inflight = /* @__PURE__ */ new Map();
188
+ live = /* @__PURE__ */ new Map();
189
+ requests = /* @__PURE__ */ new Map();
190
+ /** Imported dependencies' artifacts and hashes, by guid (valid while the version matches). */
191
+ artifacts = /* @__PURE__ */ new Map();
192
+ pendingDeps = /* @__PURE__ */ new Set();
193
+ constructor(world, options = {}) {
194
+ this.world = world;
195
+ this.server = assetServer(world);
196
+ this.options = { cacheDir: ".shard/cache", cacheSize: 256 * 1024 * 1024, ...options };
197
+ this.server.onEvent((e) => {
198
+ if (e.kind !== "modified") return;
199
+ for (const live of this.live.values()) {
200
+ if (live.deps.has(e.guid)) {
201
+ this.pendingDeps.add(e.guid);
202
+ queueMicrotask(() => this.regenerateForDeps());
203
+ return;
204
+ }
205
+ }
206
+ });
207
+ }
208
+ configure(options) {
209
+ this.options = { ...this.options, ...options };
210
+ return this;
211
+ }
212
+ /** The output entry of a request (label '' or a sub-asset like 'LOD1'), registered lazily. */
213
+ entryFor(request, label = "") {
214
+ const gen = requireGenerator(request.generator);
215
+ const base = guidOf(request);
216
+ if (!this.requests.has(base)) this.requests.set(base, request);
217
+ const guid = label === "" ? base : `${base}/${label}`;
218
+ const existing = this.server.entry(guid);
219
+ if (existing) return existing;
220
+ const path = label === "" ? proceduralPath(request) : `${proceduralPath(request)}#${label}`;
221
+ const type = label === "" ? gen.outputType : "Mesh";
222
+ return this.server.virtual(guid, path, type, () => this.produceLogged(guid), {
223
+ lazy: true,
224
+ collectable: true
225
+ });
226
+ }
227
+ /** Generates (or finds cached) an output and resolves with its ref once it's loaded. */
228
+ async generate(gen, params = {}, seed = 0) {
229
+ const request = requestOf(gen, params, seed);
230
+ const entry = this.entryFor(request);
231
+ await this.server.load(entry.guid);
232
+ return { type: gen.outputType, guid: entry.guid, path: entry.path };
233
+ }
234
+ /** What the last make of an output did: cache result, key, and time. */
235
+ lastResult(guid) {
236
+ const live = this.live.get(guid);
237
+ return live && { hit: live.hit, key: live.key, ms: live.ms };
238
+ }
239
+ /** The cached record behind a loaded output, if it's still in memory. */
240
+ recordOf(guid) {
241
+ const key = this.live.get(guid)?.key;
242
+ return key ? this.memory.get(key) : void 0;
243
+ }
244
+ /** `produce`, with failures logged (a GeneratorInstance or scene waiting on it shows why). */
245
+ async produceLogged(guid) {
246
+ try {
247
+ return await this.produce(guid);
248
+ } catch (err) {
249
+ this.world.tryResource(LogResource)?.error(err);
250
+ throw err;
251
+ }
252
+ }
253
+ /** Makes the object of `gen:<identity>[/label]`: the virtual asset's `create`. */
254
+ async produce(guid) {
255
+ const slash = guid.indexOf("/");
256
+ const base = slash === -1 ? guid : guid.slice(0, slash);
257
+ const label = slash === -1 ? "" : guid.slice(slash + 1);
258
+ const entry = this.server.entry(guid);
259
+ const request = this.requests.get(base) ?? this.requestOfPath(entry.path);
260
+ await mainThreadTurn();
261
+ const { record, hit, deps, codeHash } = await this.recordFor(request);
262
+ await mainThreadTurn();
263
+ const start = performance.now();
264
+ const asset = record.assets.find((a) => a.label === label);
265
+ if (!asset) {
266
+ throw new ShardError("assets/not-found", `${request.generator} made no "${label}"`, {
267
+ path: entry.path,
268
+ hint: `Its outputs: ${record.assets.map((a) => a.label || "(main)").join(", ")}.`
269
+ });
270
+ }
271
+ const type = findAssetType(asset.type);
272
+ if (!type) {
273
+ throw new ShardError("assets/unknown-type", `No asset type "${asset.type}" is registered`, {
274
+ path: entry.path,
275
+ hint: "Add the plugin that defines it (render/forward for meshes, textures)."
276
+ });
277
+ }
278
+ const self = label === "" ? guid : base;
279
+ const object = await type.load(artifactOf(asset), {
280
+ guid,
281
+ path: entry.path,
282
+ resolve: (p) => p.startsWith("#") ? this.server.resolve(`${self}/${p.slice(1)}`) : this.server.resolve(p)
283
+ });
284
+ this.live.set(guid, { request, label, key: record.key, codeHash, deps, hit, ms: record.ms });
285
+ if (label === "") {
286
+ for (const a of record.assets) if (a.label !== "") this.entryFor(request, a.label);
287
+ for (const child of record.children) {
288
+ try {
289
+ this.entryFor(child);
290
+ } catch (err) {
291
+ this.world.tryResource(LogResource)?.error(err);
292
+ }
293
+ }
294
+ const log = this.world.tryResource(LogResource);
295
+ for (const w of record.warnings)
296
+ log?.warn(`${request.generator}: ${w.message}`, { code: "procgen/warning", path: w.path });
297
+ }
298
+ this.stats.mainMs += performance.now() - start;
299
+ return object;
300
+ }
301
+ requestOfPath(path) {
302
+ const hash = path.indexOf("#");
303
+ const spec = (hash === -1 ? path : path.slice(0, hash)).slice("procedural:".length);
304
+ return parseRef(spec);
305
+ }
306
+ /** The dependencies of a request: loaded, with their artifacts (for workers) and hashes. */
307
+ async dependencies(gen, request) {
308
+ const deps = [];
309
+ const hashes = [];
310
+ const versions = /* @__PURE__ */ new Map();
311
+ for (const ref of paramHandles(gen, request.params)) {
312
+ const entry = this.server.entry(ref);
313
+ if (!entry) {
314
+ throw new ShardError(
315
+ "procgen/dependency-failed",
316
+ `${gen.name} needs ${ref.path ?? ref.guid}, which isn't in the catalog`,
317
+ { hint: "Check the path, or run `shard import` for new files." }
318
+ );
319
+ }
320
+ await this.server.load(entry.guid);
321
+ versions.set(entry.guid, entry.version);
322
+ const type = findAssetType(entry.type);
323
+ const object = type ? this.world.initResource(type.store).byGuid(entry.guid) : void 0;
324
+ const dep = {
325
+ guid: ref.guid ?? entry.guid,
326
+ path: ref.path ?? entry.path,
327
+ type: entry.type,
328
+ object
329
+ };
330
+ if (entry.source !== void 0) {
331
+ let cached = this.artifacts.get(entry.guid);
332
+ if (cached?.version !== entry.version) {
333
+ cached = {
334
+ version: entry.version,
335
+ artifact: await this.server.artifact(entry.guid),
336
+ hash: JSON.stringify(this.server.info(entry.guid).artifact ?? entry.guid)
337
+ };
338
+ this.artifacts.set(entry.guid, cached);
339
+ }
340
+ dep.artifact = cached.artifact;
341
+ hashes.push(cached.hash);
342
+ } else {
343
+ const live = this.live.get(entry.guid);
344
+ const record = live?.key ? this.memory.get(live.key) : void 0;
345
+ const asset = record?.assets.find((a) => a.label === live.label);
346
+ if (asset) dep.artifact = artifactOf(asset);
347
+ hashes.push(live?.key ? `${live.key}#${live.label}` : `${entry.guid}@${entry.version}`);
348
+ }
349
+ deps.push(dep);
350
+ }
351
+ return { deps, hashes, versions };
352
+ }
353
+ async recordFor(request) {
354
+ const gen = requireGenerator(request.generator);
355
+ const { deps, hashes, versions } = await this.dependencies(gen, request);
356
+ const codeHash = codeHashOf(gen);
357
+ const key = await sha256Hex(
358
+ encoder.encode(
359
+ JSON.stringify([gen.name, gen.version, codeHash, request.params, request.seed, hashes])
360
+ )
361
+ );
362
+ const cached = this.memory.get(key);
363
+ if (cached) {
364
+ this.memory.delete(key);
365
+ this.memory.set(key, cached);
366
+ this.stats.memoryHits++;
367
+ return { record: cached, hit: "memory", deps: versions, codeHash };
368
+ }
369
+ const running2 = this.inflight.get(key);
370
+ if (running2) return { record: await running2.promise, hit: "shared", deps: versions, codeHash };
371
+ let hit = "run";
372
+ const promise = (async () => {
373
+ const stored = await this.readStored(key);
374
+ if (stored) {
375
+ hit = "disk";
376
+ this.stats.diskHits++;
377
+ return stored;
378
+ }
379
+ let result;
380
+ try {
381
+ result = await runJob({ ...request, deps, chain: [] });
382
+ } catch (err) {
383
+ this.stats.failures++;
384
+ throw err;
385
+ }
386
+ this.stats.runs++;
387
+ const record = {
388
+ key,
389
+ assets: result.assets,
390
+ children: result.children,
391
+ warnings: result.warnings,
392
+ ms: result.ms,
393
+ bytes: recordBytes(result.assets)
394
+ };
395
+ void this.writeStored(record, result.packed).catch(
396
+ (err) => this.world.tryResource(LogResource)?.error(err)
397
+ );
398
+ return record;
399
+ })();
400
+ this.inflight.set(key, { promise, generator: gen.name, started: performance.now() });
401
+ try {
402
+ const record = await promise;
403
+ this.remember(record);
404
+ return { record, hit, deps: versions, codeHash };
405
+ } finally {
406
+ this.inflight.delete(key);
407
+ }
408
+ }
409
+ remember(record) {
410
+ if (this.memory.has(record.key)) return;
411
+ this.memory.set(record.key, record);
412
+ this.memoryBytes += record.bytes;
413
+ for (const [key, r] of this.memory) {
414
+ if (this.memoryBytes <= this.options.cacheSize || key === record.key) break;
415
+ this.memory.delete(key);
416
+ this.memoryBytes -= r.bytes;
417
+ }
418
+ }
419
+ storedPath(key) {
420
+ return `${this.options.cacheDir}/generated/${key.slice(0, 2)}/${key}`;
421
+ }
422
+ async readStored(key) {
423
+ const fs = this.options.fs;
424
+ try {
425
+ let record;
426
+ if (fs && await fs.exists(this.storedPath(key)))
427
+ record = decodeRecord(await fs.readBytes(this.storedPath(key)));
428
+ else {
429
+ const bytes = await this.options.storage?.read(`procgen/${key}`);
430
+ record = bytes ? decodeRecord(bytes) : void 0;
431
+ }
432
+ if (record) record.key = key;
433
+ return record;
434
+ } catch {
435
+ return void 0;
436
+ }
437
+ }
438
+ async writeStored(record, packed) {
439
+ const fs = this.options.fs;
440
+ if (fs?.writable) {
441
+ await fs.writeBytes(this.storedPath(record.key), packed ?? encodeRecord(record));
442
+ return;
443
+ }
444
+ const storage = this.options.storage;
445
+ if (!storage) return;
446
+ await storage.write(`procgen/${record.key}`, packed ?? encodeRecord(record));
447
+ this.stored.push({ key: record.key, bytes: record.bytes });
448
+ this.storedBytes += record.bytes;
449
+ while (this.storedBytes > this.options.cacheSize && this.stored.length > 1) {
450
+ const old = this.stored.shift();
451
+ this.storedBytes -= old.bytes;
452
+ await storage.delete(`procgen/${old.key}`);
453
+ }
454
+ }
455
+ // --- hot reload ------------------------------------------------------------------------------
456
+ regenerate(guid) {
457
+ const entry = this.server.entry(guid);
458
+ if (entry?.state !== "loaded") return;
459
+ this.server.updateVirtual(guid, entry.path, entry.type, () => this.produceLogged(guid), {
460
+ collectable: true
461
+ });
462
+ }
463
+ regenerateForDeps() {
464
+ const changed = this.pendingDeps;
465
+ if (changed.size === 0) return;
466
+ this.pendingDeps = /* @__PURE__ */ new Set();
467
+ for (const [guid, live] of this.live) {
468
+ if ([...live.deps.keys()].some((g) => changed.has(g))) this.regenerate(guid);
469
+ }
470
+ }
471
+ /**
472
+ * Regenerates loaded outputs whose generator's code hash changed (or whose generator was
473
+ * redefined) and those whose dependencies reloaded. They swap in place when ready; returns their
474
+ * paths.
475
+ */
476
+ regenerateStale() {
477
+ const out = [];
478
+ for (const [guid, live] of this.live) {
479
+ const entry = this.server.entry(guid);
480
+ if (entry?.state !== "loaded") continue;
481
+ let stale;
482
+ try {
483
+ stale = codeHashOf(requireGenerator(live.request.generator)) !== live.codeHash;
484
+ } catch {
485
+ continue;
486
+ }
487
+ for (const [dep, version] of live.deps) {
488
+ if (this.server.entry(dep)?.version !== version) stale = true;
489
+ }
490
+ if (!stale) continue;
491
+ this.regenerate(guid);
492
+ out.push(entry.path);
493
+ }
494
+ return out.sort();
495
+ }
496
+ /** Jobs running now, for procgen.describe. */
497
+ inFlight() {
498
+ const now = performance.now();
499
+ return [...this.inflight].map(([key, j]) => ({
500
+ generator: j.generator,
501
+ key,
502
+ ms: Math.round(now - j.started)
503
+ }));
504
+ }
505
+ cacheStats() {
506
+ return {
507
+ records: this.memory.size,
508
+ bytes: this.memoryBytes,
509
+ limit: this.options.cacheSize,
510
+ live: this.live.size
511
+ };
512
+ }
513
+ }
514
+ let parseRef = () => {
515
+ throw new ShardError("procgen/unknown-generator", "procedural refs are not set up");
516
+ };
517
+ function setRefParser(parse) {
518
+ parseRef = parse;
519
+ return parse;
520
+ }
521
+ const ProcgenResource = defineResource("procgen/Runtime", {
522
+ description: "Generator outputs of this world: caches, in-flight jobs, stats."
523
+ });
524
+ function procgen(world) {
525
+ let runtime = world.tryResource(ProcgenResource);
526
+ if (!runtime) {
527
+ runtime = new ProcgenRuntime(world);
528
+ world.insertResource(ProcgenResource, runtime);
529
+ }
530
+ return runtime;
531
+ }
532
+ function generate(world, gen, params = {}, seed = 0) {
533
+ return procgen(world).generate(gen, params, seed);
534
+ }
535
+ export {
536
+ DataAssetType,
537
+ GeneratedData,
538
+ GeneratorAssetType,
539
+ GeneratorBindings,
540
+ ProcgenResource,
541
+ ProcgenRuntime,
542
+ configureProcgenHost,
543
+ generate,
544
+ identityOf,
545
+ mainThreadTurn,
546
+ paramHandles,
547
+ procgen,
548
+ procgenHost,
549
+ procgenMainThreadMs,
550
+ runJob,
551
+ setRefParser,
552
+ warmGeneratorWorkers
553
+ };
@@ -0,0 +1,6 @@
1
+ import type { JsonSchema } from '@aethervtt/shard-core';
2
+ /**
3
+ * `.shard/schemas/gen.schema.json`: generator files, with `params` checked against the schema of
4
+ * the generator the file names (one branch per generator).
5
+ */
6
+ export declare function genFileSchema(): JsonSchema;
package/dist/schema.js ADDED
@@ -0,0 +1,35 @@
1
+ import { allGenerators } from "./generator.js";
2
+ function genFileSchema() {
3
+ const gens = allGenerators();
4
+ const base = {
5
+ $schema: "http://json-schema.org/draft-07/schema#",
6
+ title: "Shard generator file (*.gen.json)",
7
+ description: "A generator, a seed, and params. Imports as the generator output (a mesh, texture, data value, or prefab-shaped entities).",
8
+ type: "object",
9
+ required: ["generator"],
10
+ properties: {
11
+ $schema: { type: "string" },
12
+ generator: {
13
+ type: "string",
14
+ description: "The generator, by name.",
15
+ ...gens.length ? { enum: gens.map((g) => g.name) } : {}
16
+ },
17
+ seed: { type: "integer", minimum: 0, maximum: 4294967295, default: 0 },
18
+ params: { type: "object" }
19
+ },
20
+ additionalProperties: false
21
+ };
22
+ if (gens.length === 0) return base;
23
+ return {
24
+ ...base,
25
+ oneOf: gens.map((g) => ({
26
+ properties: {
27
+ generator: { const: g.name },
28
+ params: { ...g.params.jsonSchema(), description: g.description ?? `${g.name} params.` }
29
+ }
30
+ }))
31
+ };
32
+ }
33
+ export {
34
+ genFileSchema
35
+ };
@@ -0,0 +1,7 @@
1
+ import type { PreviewImage } from '@aethervtt/shard-protocol';
2
+ /** Writes `text` at (x, y) in white on a dark box, each glyph pixel `scale` pixels wide. */
3
+ export declare function drawLabel(image: PreviewImage, text: string, x: number, y: number, scale?: number): void;
4
+ /** Lays images out in a grid (`columns` wide), each in a `cell`×`cell` slot, centred. */
5
+ export declare function contactSheet(images: readonly PreviewImage[], labels: readonly string[], cell: number, columns?: number): PreviewImage;
6
+ /** Parses `3`, `1-9`, or `1,4,9` into seeds. */
7
+ export declare function parseSeeds(spec: string | readonly number[]): number[];
package/dist/sheet.js ADDED
@@ -0,0 +1,85 @@
1
+ const GLYPHS = {
2
+ "0": [".###.", "#...#", "#..##", "#.#.#", "##..#", "#...#", ".###."],
3
+ "1": ["..#..", ".##..", "..#..", "..#..", "..#..", "..#..", ".###."],
4
+ "2": [".###.", "#...#", "....#", "...#.", "..#..", ".#...", "#####"],
5
+ "3": ["####.", "....#", "....#", ".###.", "....#", "....#", "####."],
6
+ "4": ["...#.", "..##.", ".#.#.", "#..#.", "#####", "...#.", "...#."],
7
+ "5": ["#####", "#....", "####.", "....#", "....#", "#...#", ".###."],
8
+ "6": ["..##.", ".#...", "#....", "####.", "#...#", "#...#", ".###."],
9
+ "7": ["#####", "....#", "...#.", "..#..", ".#...", ".#...", ".#..."],
10
+ "8": [".###.", "#...#", "#...#", ".###.", "#...#", "#...#", ".###."],
11
+ "9": [".###.", "#...#", "#...#", ".####", "....#", "...#.", ".##.."],
12
+ s: [".....", ".....", ".####", "#....", ".###.", "....#", "####."],
13
+ e: [".....", ".....", ".###.", "#...#", "#####", "#....", ".###."],
14
+ d: ["....#", "....#", ".##.#", "#..##", "#...#", "#...#", ".####"],
15
+ "-": [".....", ".....", ".....", "#####", ".....", ".....", "....."],
16
+ ".": [".....", ".....", ".....", ".....", ".....", ".##..", ".##.."],
17
+ " ": [".....", ".....", ".....", ".....", ".....", ".....", "....."]
18
+ };
19
+ function drawLabel(image, text, x, y, scale = 2) {
20
+ const glyphW = 6 * scale;
21
+ const w = text.length * glyphW + 2 * scale;
22
+ const h = 9 * scale;
23
+ const set = (px, py, v, a) => {
24
+ if (px < 0 || py < 0 || px >= image.width || py >= image.height) return;
25
+ const i = (py * image.width + px) * 4;
26
+ for (let c = 0; c < 3; c++) image.data[i + c] = Math.round(image.data[i + c] * (1 - a) + v * a);
27
+ image.data[i + 3] = 255;
28
+ };
29
+ for (let py = 0; py < h; py++) for (let px = 0; px < w; px++) set(x + px, y + py, 0, 0.6);
30
+ for (let k = 0; k < text.length; k++) {
31
+ const glyph = GLYPHS[text[k]] ?? GLYPHS[" "];
32
+ for (let row = 0; row < 7; row++) {
33
+ for (let col = 0; col < 5; col++) {
34
+ if (glyph[row][col] !== "#") continue;
35
+ for (let sy = 0; sy < scale; sy++)
36
+ for (let sx = 0; sx < scale; sx++)
37
+ set(x + scale + k * glyphW + col * scale + sx, y + scale + row * scale + sy, 255, 1);
38
+ }
39
+ }
40
+ }
41
+ }
42
+ function contactSheet(images, labels, cell, columns = Math.ceil(Math.sqrt(images.length))) {
43
+ const rows = Math.ceil(images.length / columns);
44
+ const gap = 2;
45
+ const width = columns * cell + (columns + 1) * gap;
46
+ const height = rows * cell + (rows + 1) * gap;
47
+ const data = new Uint8Array(width * height * 4);
48
+ for (let i = 0; i < data.length; i += 4) {
49
+ data[i] = 24;
50
+ data[i + 1] = 26;
51
+ data[i + 2] = 30;
52
+ data[i + 3] = 255;
53
+ }
54
+ const sheet = { width, height, data };
55
+ images.forEach((img, n) => {
56
+ const cx = gap + n % columns * (cell + gap);
57
+ const cy = gap + Math.floor(n / columns) * (cell + gap);
58
+ const ox = cx + (cell - img.width >> 1);
59
+ const oy = cy + (cell - img.height >> 1);
60
+ for (let y = 0; y < img.height; y++) {
61
+ const row = img.data.subarray(y * img.width * 4, (y + 1) * img.width * 4);
62
+ data.set(row, ((oy + y) * width + ox) * 4);
63
+ }
64
+ const label = labels[n];
65
+ if (label) drawLabel(sheet, label, cx + 4, cy + 4, cell >= 192 ? 2 : 1);
66
+ });
67
+ return sheet;
68
+ }
69
+ function parseSeeds(spec) {
70
+ if (Array.isArray(spec)) return spec.map((n) => n >>> 0);
71
+ const out = [];
72
+ for (const part of String(spec).split(",")) {
73
+ const m = /^\s*(\d+)\s*(?:-\s*(\d+))?\s*$/.exec(part);
74
+ if (!m) continue;
75
+ const a = Number(m[1]);
76
+ const b = m[2] === void 0 ? a : Number(m[2]);
77
+ for (let s = Math.min(a, b); s <= Math.max(a, b) && out.length < 64; s++) out.push(s);
78
+ }
79
+ return out;
80
+ }
81
+ export {
82
+ contactSheet,
83
+ drawLabel,
84
+ parseSeeds
85
+ };
@@ -0,0 +1,10 @@
1
+ import { type GenJob } from './job.js';
2
+ /**
3
+ * Runs one generator job with the clock and Math.random guarded for all of it. The result is one
4
+ * packed record (`encodeRecord`), so its bytes move back to the caller instead of being copied.
5
+ */
6
+ export declare function runGeneratorJob(job: GenJob): Promise<{
7
+ packed: Uint8Array;
8
+ }>;
9
+ /** Nothing: loading this module (and the project's code) is the point. */
10
+ export declare function warmUp(): true;