nucleation 0.10.4 → 0.10.8

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,167 @@
1
+ /** Typings for the JS design veneer (`nucleation/design`), the 1:1 mirror
2
+ * of `bindings/python/nucleation/design.py` over the generated wasm core. */
3
+
4
+ export type Vec3 = [number, number, number] | number[];
5
+
6
+ export interface GateObj {
7
+ anchor: Vec3;
8
+ step: Vec3;
9
+ name?: string | null;
10
+ }
11
+ /** A bus gate; unnamed gates auto-name `g0`, `g1`, ... in order. */
12
+ export function Gate(anchor: Vec3, step: Vec3, name?: string | null): GateObj;
13
+
14
+ export interface StyleOpts {
15
+ busBlock?: string | null;
16
+ transparentBlock?: string | null;
17
+ /** wire-format spellings pass through untouched */
18
+ bus_block?: string;
19
+ transparent_block?: string;
20
+ }
21
+ /** Per-bus routing style; omitted fields fall back to router defaults. */
22
+ export function Style(opts?: StyleOpts): Record<string, string>;
23
+
24
+ export declare class CheckReport {
25
+ constructor(raw: any);
26
+ raw: any;
27
+ readonly clean: boolean;
28
+ readonly drc: any[];
29
+ readonly lvs: Record<string, any>;
30
+ readonly rules: any[];
31
+ readonly buses: Record<string, any>;
32
+ readonly skew: Record<string, any>;
33
+ get(key: string): any;
34
+ }
35
+
36
+ export declare class DesignCheckError extends Error {
37
+ report: CheckReport;
38
+ }
39
+
40
+ export declare class Bus {
41
+ design: Design;
42
+ name: string;
43
+ gateNames: string[];
44
+ readonly state: string;
45
+ readonly skew: { per_bit_rt: number[]; skew_rt: number; max_rt: number };
46
+ moveGate(gate: number | string, anchor: Vec3): { state: string; rerouted_segments: number };
47
+ addGate(anchor: Vec3, step: Vec3, name?: string | null): string;
48
+ rule(rule?: Record<string, any>): void;
49
+ rip(): void;
50
+ }
51
+
52
+ export declare class Executor {
53
+ cell: any; // core CellExecutor
54
+ static forSchematic(schematic: any, settle?: number): Executor;
55
+ set(name: string, value: number | boolean | string): void;
56
+ get(name: string): number | boolean;
57
+ setInput(name: string, value: number | boolean | string): void;
58
+ readOutput(name: string): any; // core Value
59
+ settle(budget?: number): boolean;
60
+ reset(): void;
61
+ }
62
+
63
+ export declare class Flat {
64
+ constructor(raw: any);
65
+ raw: any; // core Schematic
66
+ executor(settle?: number): Executor;
67
+ toBytes(format?: string): Uint8Array;
68
+ save(path: string): Promise<void>; // Node only
69
+ }
70
+
71
+ export interface PortDecl {
72
+ anchor: Vec3;
73
+ step: Vec3;
74
+ width: number;
75
+ ty?: "uint" | "bool" | string;
76
+ }
77
+
78
+ export declare class Design {
79
+ readonly raw: any; // core Design
80
+ static create(name: string): Design;
81
+ static forSchematic(name: string, schematic: any): Design;
82
+ static fromNucm(data: Uint8Array | number[]): Design;
83
+ static fromLitematic(data: Uint8Array | number[]): Design;
84
+ static loadNucm(path: string): Promise<Design>; // Node only
85
+ static importLitematic(path: string): Promise<Design>; // Node only
86
+
87
+ declareInput(name: string, port: PortDecl): void;
88
+ declareOutput(name: string, port: PortDecl): void;
89
+
90
+ routeBus(name: string, opts: {
91
+ driver: string; sinks: string | string[];
92
+ gates?: GateObj[]; style?: StyleOpts | Record<string, string> | string | null;
93
+ }): Bus;
94
+ routeBusOr(name: string, opts: {
95
+ drivers: string | string[]; sinks: string | string[];
96
+ gates?: GateObj[]; style?: StyleOpts | Record<string, string> | string | null;
97
+ }): Bus;
98
+ addGate(bus: string, gate: string, anchor: Vec3, step: Vec3): string;
99
+ moveGate(bus: string, gate: string, anchor: Vec3): { state: string; rerouted_segments: number };
100
+ setBusRule(bus: string, rule?: Record<string, any>): void;
101
+ busState(name: string): string;
102
+ busSkew(name: string): { per_bit_rt: number[]; skew_rt: number; max_rt: number };
103
+ rip(name: string): void;
104
+
105
+ addCell(name: string, schematic: any): string;
106
+ place(name: string, cell: string, at: Vec3, rot?: number): void;
107
+ moveInstance(name: string, at: Vec3, rot?: number): {
108
+ rerouted: string[]; failed: Record<string, string>;
109
+ };
110
+
111
+ /** Executor hardware (levers/lamps, hand-drivable) vs a routable dust
112
+ * input. Reversible: `"executor"` restores the shipped blocks exactly. */
113
+ setPortMode(instance: string, port: string, mode: "bus" | "executor"): PortModeReport;
114
+ promotePort(instance: string, port: string): PortModeReport;
115
+ portModes(): Array<{ name: string; mode: "bus" | "executor"; patch: PortPatch }>;
116
+ planPortPromotion(instance: string, port: string): PortPatch;
117
+
118
+ setBlock(x: number, y: number, z: number, block: string): void;
119
+
120
+ check(opts?: { strict?: boolean }): CheckReport;
121
+ flatten(): Flat;
122
+ bake(budget?: number): Flat;
123
+
124
+ toNucmB64(): string;
125
+ toLitematicB64(): string;
126
+ toBytes(pathOrSuffix: string): Uint8Array;
127
+ save(path: string): Promise<void>; // Node only
128
+ }
129
+
130
+ /** What Bus mode lays down, and the executor hardware it replaces. */
131
+ export interface PortPatch {
132
+ wires: Vec3[];
133
+ hardware: Vec3[];
134
+ step: Vec3;
135
+ removed: number;
136
+ added: number;
137
+ /** True when the port's hardware was a horizontal ROW and a form adapter
138
+ * was grown to reach the vertical 2y-pitch bus stack. */
139
+ pivoted: boolean;
140
+ note: string;
141
+ }
142
+
143
+ /** Outcome of a port-mode switch. `changed` is in WORLD coordinates. */
144
+ export interface PortModeReport {
145
+ port: string;
146
+ mode: "bus" | "executor";
147
+ note: string;
148
+ changed: Array<{ at: Vec3; from: string | null; to: string | null }>;
149
+ /** Buses ripped because their endpoint stopped existing. */
150
+ removed_buses: string[];
151
+ moves: { rerouted: string[]; failed: Record<string, string> };
152
+ patch: PortPatch;
153
+ }
154
+
155
+ export interface VeneerSurface {
156
+ Design: typeof Design;
157
+ Executor: typeof Executor;
158
+ Flat: typeof Flat;
159
+ Bus: typeof Bus;
160
+ CheckReport: typeof CheckReport;
161
+ DesignCheckError: typeof DesignCheckError;
162
+ Gate: typeof Gate;
163
+ Style: typeof Style;
164
+ }
165
+
166
+ /** Bind the veneer to a loaded wasm core module; memoized on the core. */
167
+ export function veneer(core: any): VeneerSurface;
@@ -0,0 +1,480 @@
1
+ /** Idiomatic veneer over the generated `Design` / `CellExecutor` core.
2
+ *
3
+ * Thin by contract: every method here only marshals arrays, object
4
+ * literals and options into the exact JSON strings and positional splats
5
+ * the generated bridge expects, and parses the JSON it writes back. No
6
+ * routing/design logic lives on this side. This module mirrors
7
+ * `bindings/python/nucleation/design.py` 1:1 (same names in camelCase,
8
+ * same shapes; object literals where Python takes dataclasses/kwargs).
9
+ *
10
+ * Import as `nucleation/design`, passing the loaded core module once:
11
+ *
12
+ * import * as core from "nucleation"; // or the runtime engine URL
13
+ * import { veneer } from "nucleation/design";
14
+ * const { Design, Gate, Style, Executor } = veneer(core);
15
+ *
16
+ * `veneer(core)` exists because the wasm core is loaded at RUNTIME by URL
17
+ * in the browser apps; there is no static import this module could take.
18
+ */
19
+
20
+ // --------------------------------------------------------------------------
21
+ // small value objects (plain objects accepted anywhere these are)
22
+ // --------------------------------------------------------------------------
23
+
24
+ /** A bus gate: `Gate([x, y, z], [sx, sy, sz], name?)`.
25
+ * Unnamed gates are auto-named `g0`, `g1`, ... in declaration order. */
26
+ export function Gate(anchor, step, name = null) {
27
+ return { anchor: [...anchor], step: [...step], name };
28
+ }
29
+
30
+ /** Per-bus routing style; omitted fields fall back to router defaults. */
31
+ export function Style({ busBlock = null, transparentBlock = null, ...rest } = {}) {
32
+ // Python spells the wire keys snake_case; accept both spellings.
33
+ const s = { ...rest };
34
+ if (busBlock != null) s.bus_block = busBlock;
35
+ if (transparentBlock != null) s.transparent_block = transparentBlock;
36
+ return s;
37
+ }
38
+
39
+ function styleJson(style) {
40
+ if (style == null) return "{}";
41
+ if (typeof style === "string") return style; // already wire-format JSON
42
+ if (typeof style === "object") return JSON.stringify(style);
43
+ throw new TypeError("style must be a Style/object, JSON string, null or undefined");
44
+ }
45
+
46
+ /** -> [wire JSON, [gate names]] with `g<i>` auto-naming. */
47
+ function gatesJson(gates) {
48
+ const items = (gates ?? []).map((g, i) => {
49
+ if (typeof g !== "object" || !g.anchor || !g.step) {
50
+ throw new TypeError("gates must be Gate(...) results or {anchor, step, name?} objects");
51
+ }
52
+ return { name: g.name ?? `g${i}`, anchor: [...g.anchor], step: [...g.step] };
53
+ });
54
+ return [JSON.stringify(items), items.map((g) => g.name)];
55
+ }
56
+
57
+ function namesJson(names) {
58
+ return JSON.stringify(typeof names === "string" ? [names] : [...names]);
59
+ }
60
+
61
+ function unwrap(schematic) {
62
+ return schematic instanceof FlatBase ? schematic.raw : schematic;
63
+ }
64
+
65
+ function b64ToBytes(b64) {
66
+ if (typeof Buffer !== "undefined") return Buffer.from(b64, "base64");
67
+ const bin = atob(b64);
68
+ const out = new Uint8Array(bin.length);
69
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
70
+ return out;
71
+ }
72
+
73
+ // --------------------------------------------------------------------------
74
+ // reports
75
+ // --------------------------------------------------------------------------
76
+
77
+ /** Parsed `d.check()`: `.clean` plus the DRC/LVS/rule sections. */
78
+ export class CheckReport {
79
+ constructor(raw) {
80
+ this.raw = raw;
81
+ }
82
+ get clean() { return this.raw.clean ?? false; }
83
+ get drc() { return this.raw.drc ?? []; }
84
+ get lvs() { return this.raw.lvs ?? {}; }
85
+ get rules() { return this.raw.rules ?? []; }
86
+ get buses() { return this.raw.buses ?? {}; }
87
+ get skew() { return this.raw.skew ?? this.raw.buses ?? {}; }
88
+ /** `report.get("drc")` — the Python `report["drc"]` spelling. */
89
+ get(key) { return this.raw[key]; }
90
+ toString() {
91
+ return `CheckReport(clean=${this.clean}, drc=${this.drc.length}, rules=${this.rules.length})`;
92
+ }
93
+ }
94
+
95
+ /** Thrown by `d.check({ strict: true })` when the report is not clean. */
96
+ export class DesignCheckError extends Error {
97
+ constructor(report) {
98
+ super(`design check failed: ${JSON.stringify(report.raw, null, 2)}`);
99
+ this.name = "DesignCheckError";
100
+ this.report = report;
101
+ }
102
+ }
103
+
104
+ // --------------------------------------------------------------------------
105
+ // executor sugar
106
+ // --------------------------------------------------------------------------
107
+
108
+ function toValue(core, v) {
109
+ if (v instanceof core.Value) return v;
110
+ if (typeof v === "boolean") return core.Value.fromBool(v);
111
+ if (typeof v === "number") {
112
+ if (Number.isInteger(v)) {
113
+ return v >= 0 ? core.Value.fromU32(v) : core.Value.fromI32(v);
114
+ }
115
+ return core.Value.fromF32(v);
116
+ }
117
+ if (typeof v === "string") return core.Value.fromString(v);
118
+ throw new TypeError(`cannot convert ${v} to a port Value`);
119
+ }
120
+
121
+ function fromValue(value) {
122
+ for (const conv of ["asU32", "asI32", "asBool", "asF32"]) {
123
+ try {
124
+ return value[conv]();
125
+ } catch {
126
+ /* next */
127
+ }
128
+ }
129
+ return value;
130
+ }
131
+
132
+ /** `const ex = baked.executor(); ex.set("a", 0x55); ex.settle(); ex.get("a_out")`.
133
+ *
134
+ * `set`/`get` convert plain JS values to/from typed port `Value`s (the JS
135
+ * spelling of Python's `ex["a"] = 0x55; ex["a_out"]`); the explicit
136
+ * `setInput` / `readOutput` / `settle` methods stay available, and
137
+ * everything else is reachable on `.cell` (the core `CellExecutor`).
138
+ */
139
+ class ExecutorBase {
140
+ constructor(core, cell) {
141
+ this._core = core;
142
+ this.cell = cell;
143
+ }
144
+ set(name, value) { this.cell.setInput(name, toValue(this._core, value)); }
145
+ get(name) { return fromValue(this.cell.readOutput(name)); }
146
+ setInput(name, value) { this.cell.setInput(name, toValue(this._core, value)); }
147
+ readOutput(name) { return this.cell.readOutput(name); }
148
+ settle(budget = 4000) { return this.cell.settle(budget); }
149
+ reset() { return this.cell.reset(); }
150
+ }
151
+
152
+ /** A flattened/baked artifact: the core `Schematic` plus sugar.
153
+ *
154
+ * `.executor()` builds a settled typed executor over the embedded
155
+ * contract; `.toBytes(format)` serializes it (`"schem"`, `"litematic"`,
156
+ * `"snapshot"`, `"mcstructure"`); `.save(path)` writes it in Node;
157
+ * `.raw` is the core `Schematic` for APIs that want it.
158
+ */
159
+ class FlatBase {
160
+ constructor(core, raw) {
161
+ this._core = core;
162
+ this.raw = raw;
163
+ }
164
+ executor(settle = 4000) {
165
+ return surfaces.get(this._core).Executor.forSchematic(this.raw, settle);
166
+ }
167
+ /** Artifact bytes; `format` from the path suffix in `.save()`. */
168
+ toBytes(format = "schem") {
169
+ const b64 = {
170
+ schem: () => this.raw.toSchematicB64(),
171
+ schematic: () => this.raw.toSchematicB64(),
172
+ litematic: () => this.raw.toLitematicB64(),
173
+ snapshot: () => this.raw.toSnapshotB64(),
174
+ nuc: () => this.raw.toSnapshotB64(),
175
+ mcstructure: () => this.raw.toMcstructureB64(),
176
+ }[format];
177
+ if (!b64) throw new Error(`unknown artifact format ${JSON.stringify(format)}`);
178
+ return b64ToBytes(b64());
179
+ }
180
+ /** Node-only path save (browsers: use `.toBytes()` + a download). */
181
+ async save(path) {
182
+ const fs = await import("node:fs");
183
+ fs.writeFileSync(path, this.toBytes(String(path).split(".").pop()));
184
+ }
185
+ }
186
+
187
+ // --------------------------------------------------------------------------
188
+ // bus handle
189
+ // --------------------------------------------------------------------------
190
+
191
+ /** Anything the core writes back from a call that used to be void — a route
192
+ * report, say. `null` when the core wrote nothing, so a caller can treat
193
+ * "this build does not report it" and "nothing to report" the same way. */
194
+ function optionalReport(raw) {
195
+ if (typeof raw !== "string" || raw.trim() === "") return null;
196
+ try {
197
+ return JSON.parse(raw);
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+
203
+ /** Handle returned by `routeBus`: live `.state`, drag and rip. */
204
+ export class Bus {
205
+ constructor(design, name, gateNames, report = null) {
206
+ this.design = design;
207
+ this.name = name;
208
+ this.gateNames = [...gateNames];
209
+ /** What realizing the bus did beyond routing it — e.g. `promotions`, the
210
+ * executor-only ports it converted so the bus could land. `null` on cores
211
+ * that do not report it. */
212
+ this.report = report;
213
+ }
214
+ /** Ports the router promoted to make this bus land, as
215
+ * `[{port, from, to, note}]` (best effort: `[]` when unreported). */
216
+ get promotions() {
217
+ const p = this.report?.promotions ?? this.report?.promoted ?? [];
218
+ return Array.isArray(p) ? p : [];
219
+ }
220
+ get state() { return this.design.busState(this.name); }
221
+ get skew() { return this.design.busSkew(this.name); }
222
+ _gate(gate) { return typeof gate === "number" ? this.gateNames[gate] : gate; }
223
+ /** Drag a gate (by index or name) to `[x, y, z]`; -> reroute report. */
224
+ moveGate(gate, anchor) { return this.design.moveGate(this.name, this._gate(gate), anchor); }
225
+ addGate(anchor, step, name = null) {
226
+ name = name ?? `g${this.gateNames.length}`;
227
+ const state = this.design.addGate(this.name, name, anchor, step);
228
+ this.gateNames.push(name);
229
+ return state;
230
+ }
231
+ rule(rule = {}) { this.design.setBusRule(this.name, rule); }
232
+ rip() { this.design.rip(this.name); }
233
+ toString() { return `Bus(${JSON.stringify(this.name)}, ${this.state})`; }
234
+ }
235
+
236
+ // --------------------------------------------------------------------------
237
+ // the design document
238
+ // --------------------------------------------------------------------------
239
+
240
+ /** Options-object/array veneer over the generated wire-format `Design`.
241
+ *
242
+ * Anything not wrapped here (`setBlock`, `toNucmB64`, ...) is reachable
243
+ * on `.raw` (the core object) unchanged.
244
+ */
245
+ class DesignBase {
246
+ constructor(core, raw) {
247
+ this._core = core;
248
+ this._d = raw;
249
+ }
250
+ get raw() { return this._d; }
251
+
252
+ // -- ports ---------------------------------------------------------------
253
+
254
+ declareInput(name, { anchor, step, width, ty = "uint" }) {
255
+ const [ax, ay, az] = anchor, [sx, sy, sz] = step;
256
+ this._d.declareInput(name, ax, ay, az, sx, sy, sz, width, ty);
257
+ }
258
+ declareOutput(name, { anchor, step, width, ty = "uint" }) {
259
+ const [ax, ay, az] = anchor, [sx, sy, sz] = step;
260
+ this._d.declareOutput(name, ax, ay, az, sx, sy, sz, width, ty);
261
+ }
262
+
263
+ // -- buses ---------------------------------------------------------------
264
+
265
+ routeBus(name, { driver, sinks, gates = [], style = null }) {
266
+ const [gj, gateNames] = gatesJson(gates);
267
+ const raw = this._d.routeBus(name, driver, namesJson(sinks), gj, styleJson(style));
268
+ return new Bus(this, name, gateNames, optionalReport(raw));
269
+ }
270
+ routeBusOr(name, { drivers, sinks, gates = [], style = null }) {
271
+ const [gj, gateNames] = gatesJson(gates);
272
+ const raw = this._d.routeBusOr(name, namesJson(drivers), namesJson(sinks), gj, styleJson(style));
273
+ return new Bus(this, name, gateNames, optionalReport(raw));
274
+ }
275
+ /** Route a bus whose driver and sinks are DIFFERENT WIDTHS, with the
276
+ * alignment stated rather than guessed.
277
+ *
278
+ * `align`: `"lsb"` (default — bit 0 to bit 0, so the magnitude survives),
279
+ * `"msb"` (top bit to top bit, i.e. a shift up by the width difference), or
280
+ * `"shift"` with `shift` bits (positive moves toward the MSB).
281
+ * `truncate` permits DROPPING source bits that fall outside the destination;
282
+ * without it a lossy connection is refused, because throwing away a word's
283
+ * high bits is not the router's decision to make. Destination bits nothing
284
+ * drives read 0 with no hardware at all.
285
+ */
286
+ routeBusAdapted(name, { driver, sinks, gates = [], style = null,
287
+ align = "lsb", shift = 0, truncate = false }) {
288
+ const [gj, gateNames] = gatesJson(gates);
289
+ const code = { lsb: 0, msb: 1, shift: 2 }[String(align).toLowerCase()];
290
+ if (code === undefined) {
291
+ throw new TypeError(`align must be "lsb", "msb" or "shift" (got ${JSON.stringify(align)})`);
292
+ }
293
+ const raw = this._d.routeBusAdapted(
294
+ name, driver, [...sinks].join(","), gj, styleJson(style), code, shift | 0, !!truncate);
295
+ return new Bus(this, name, gateNames, optionalReport(raw));
296
+ }
297
+ /** How a bus's bits line up: `{driver, width, sinks: [{port, width, bits}]}`
298
+ * shaped by the core. `null` on cores that do not report it. */
299
+ busWidthMap(name) {
300
+ if (typeof this._d.busWidthMap !== "function") return null;
301
+ try {
302
+ return JSON.parse(this._d.busWidthMap(name));
303
+ } catch {
304
+ return null;
305
+ }
306
+ }
307
+ addGate(bus, gate, anchor, step) {
308
+ const [x, y, z] = anchor, [sx, sy, sz] = step;
309
+ return this._d.addGate(bus, gate, x, y, z, sx, sy, sz);
310
+ }
311
+ moveGate(bus, gate, anchor) {
312
+ const [x, y, z] = anchor;
313
+ return JSON.parse(this._d.moveGate(bus, gate, x, y, z));
314
+ }
315
+ setBusRule(bus, rule = {}) { this._d.setBusRule(bus, JSON.stringify(rule)); }
316
+ busState(name) { return this._d.busState(name); }
317
+ busSkew(name) { return JSON.parse(this._d.busSkew(name)); }
318
+ rip(name) { this._d.rip(name); }
319
+ /** Re-realize a ripped/failed bus from its stored declaration; -> state. */
320
+ reroute(name) { return this._d.reroute(name); }
321
+ /** Delete a bus outright — fragment AND declaration, freeing the name. */
322
+ removeBus(name) { this._d.removeBus(name); }
323
+
324
+ // -- cells / instances ---------------------------------------------------
325
+
326
+ addCell(name, schematic) { return this._d.addCell(name, unwrap(schematic)); }
327
+ place(name, cell, at, rot = 0) {
328
+ const [x, y, z] = at;
329
+ this._d.place(name, cell, x, y, z, rot);
330
+ }
331
+ moveInstance(name, at, rot = 0) {
332
+ const [x, y, z] = at;
333
+ return JSON.parse(this._d.moveInstance(name, x, y, z, rot));
334
+ }
335
+ /** Remove an instance. Buses that terminated on one of its ports are
336
+ * DELETED (they lost an endpoint) and named in the report; buses that
337
+ * merely crossed its space are ripped and co-rerouted.
338
+ * -> `{removed_buses, rerouted, failed}`. */
339
+ removeInstance(name) { return JSON.parse(this._d.removeInstance(name)); }
340
+
341
+ // -- instance ports (derived routing endpoints) ---------------------------
342
+
343
+ /** Every endpoint the placed instances expose, as
344
+ * `{name: "u0.sum", instance, port, role, ty, width, hardware, wires,
345
+ * step, routable, blocked}`.
346
+ *
347
+ * `name` is exactly what `routeBus` accepts. `role` is the CELL-facing
348
+ * direction: `"output"` drives a bus, `"input"` receives one. A cell
349
+ * contract names EXECUTOR hardware (levers/buttons in, lamps out) while a
350
+ * bus lands on dust, so `wires` carries the derived dust connection cells
351
+ * — and a port with no dust to tap (a bare lever input) reports
352
+ * `routable: false` with the reason in `blocked`. */
353
+ instancePorts() { return JSON.parse(this._d.instancePorts()); }
354
+
355
+ /** Switch a port between executor hardware and a routable dust input —
356
+ * `mode` is `"bus"` or `"executor"`.
357
+ *
358
+ * This is the composability switch. A community cell's inputs are LEVERS,
359
+ * and nothing in redstone drives a lever, so `add.sum -> bcd.bin` needs
360
+ * `bin` in `"bus"` mode first. The conversion is a reversible per-instance
361
+ * patch: `"executor"` restores the shipped blocks byte-exactly.
362
+ *
363
+ * Returns `{port, mode, note, changed: [{at, from, to}], removed_buses,
364
+ * moves, patch}`. `note` is a ready-made toast; `changed` is in WORLD
365
+ * coordinates. A bus that terminated on the port is RIPPED and named in
366
+ * `removed_buses` — its endpoint physically stopped existing. */
367
+ setPortMode(instance, port, mode) {
368
+ return JSON.parse(this._d.setPortMode(instance, port, mode));
369
+ }
370
+
371
+ /** `setPortMode(instance, port, "bus")`. */
372
+ promotePort(instance, port) { return this.setPortMode(instance, port, "bus"); }
373
+
374
+ /** Ports whose mode has been switched: `[{name, mode, patch}]`. Anything
375
+ * absent is in `"executor"` mode. */
376
+ portModes() { return JSON.parse(this._d.portModes()); }
377
+
378
+ /** What promoting a port WOULD do, without doing it: `{wires, hardware,
379
+ * step, removed, added, pivoted, note}`. Throws with the reason when the
380
+ * port cannot be promoted (a ceiling lever, say). */
381
+ planPortPromotion(instance, port) {
382
+ return JSON.parse(this._d.planPortPromotion(instance, port));
383
+ }
384
+
385
+ /** Resolve one endpoint name to the geometry a bus would use:
386
+ * `{name, anchor, step, width, direction, connectable}`. `direction` is
387
+ * DESIGN-facing, so `"input"` drives buses. Throws with the reason when
388
+ * the name is unknown or the port cannot terminate a bus. */
389
+ resolvePort(name) { return JSON.parse(this._d.resolvePort(name)); }
390
+
391
+ // -- loose layer (explicit forward; more via `.raw`) ---------------------
392
+
393
+ setBlock(x, y, z, block) { this._d.setBlock(x, y, z, block); }
394
+
395
+ // -- lifecycle -----------------------------------------------------------
396
+
397
+ check({ strict = false } = {}) {
398
+ const report = new CheckReport(JSON.parse(this._d.check()));
399
+ if (strict && !report.clean) throw new DesignCheckError(report);
400
+ return report;
401
+ }
402
+ flatten() { return new (surfaces.get(this._core).Flat)(this._d.flatten()); }
403
+ /** The flattened stack composited into ONE region — what an interchange
404
+ * artifact wants. `flatten()` keeps `inst:*`/`bus:*` regions (the
405
+ * renderer needs them); a single-region schematic cannot lose a
406
+ * named-layer cell to the loose layer's bounding box. */
407
+ flattenComposite() {
408
+ return new (surfaces.get(this._core).Flat)(this._d.flattenComposite());
409
+ }
410
+ bake(budget = 4000) { return new (surfaces.get(this._core).Flat)(this._d.bake(budget)); }
411
+
412
+ // -- persistence ---------------------------------------------------------
413
+
414
+ toNucmB64() { return this._d.toNucmB64(); }
415
+ toLitematicB64() { return this._d.toLitematicB64(); }
416
+ toSchemB64() { return this._d.toSchemB64(); }
417
+ /** Document/artifact bytes by suffix: `.nucm` -> project tier,
418
+ * `.litematic` -> layered interchange, anything else -> the flattened
419
+ * artifact, composited (the browser-side spelling of Python's
420
+ * `d.save(path)`). */
421
+ toBytes(pathOrSuffix) {
422
+ const suffix = String(pathOrSuffix).split(".").pop();
423
+ if (suffix === "nucm") return b64ToBytes(this._d.toNucmB64());
424
+ if (suffix === "litematic") return b64ToBytes(this._d.toLitematicB64());
425
+ if (suffix === "schem" || suffix === "schematic") return b64ToBytes(this._d.toSchemB64());
426
+ return this.flattenComposite().toBytes(suffix);
427
+ }
428
+ /** Node-only path save, tier dispatched by suffix like Python's. */
429
+ async save(path) {
430
+ const fs = await import("node:fs");
431
+ fs.writeFileSync(path, this.toBytes(path));
432
+ }
433
+ }
434
+
435
+ // --------------------------------------------------------------------------
436
+ // binding to a loaded core
437
+ // --------------------------------------------------------------------------
438
+
439
+ const surfaces = new WeakMap(); // module namespaces are frozen; memoize here
440
+
441
+ /** Bind the veneer to a loaded wasm core module; returns the surface. */
442
+ export function veneer(core) {
443
+ const memo = surfaces.get(core);
444
+ if (memo) return memo;
445
+
446
+ class Executor extends ExecutorBase {
447
+ constructor(cell) { super(core, cell); }
448
+ static forSchematic(schematic, settle = 4000) {
449
+ const ex = new Executor(core.CellExecutor.forSchematic(unwrap(schematic)));
450
+ if (settle) ex.cell.settle(settle);
451
+ return ex;
452
+ }
453
+ }
454
+ class Flat extends FlatBase {
455
+ constructor(raw) { super(core, raw); }
456
+ }
457
+ class Design extends DesignBase {
458
+ constructor(raw) { super(core, raw); }
459
+ static create(name) { return new Design(core.Design.create(name)); }
460
+ static forSchematic(name, schematic) {
461
+ return new Design(core.Design.forSchematic(name, unwrap(schematic)));
462
+ }
463
+ static fromNucm(data) { return new Design(core.Design.fromNucm(Array.from(data))); }
464
+ static fromLitematic(data) { return new Design(core.Design.fromLitematic(Array.from(data))); }
465
+ static async loadNucm(path) {
466
+ const fs = await import("node:fs");
467
+ return Design.fromNucm(fs.readFileSync(String(path)));
468
+ }
469
+ static async importLitematic(path) {
470
+ const fs = await import("node:fs");
471
+ return Design.fromLitematic(fs.readFileSync(String(path)));
472
+ }
473
+ }
474
+
475
+ const surface = {
476
+ Design, Executor, Flat, Bus, CheckReport, DesignCheckError, Gate, Style,
477
+ };
478
+ surfaces.set(core, surface);
479
+ return surface;
480
+ }