@bobfrankston/wallplater 1.0.2 → 1.0.4

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.
@@ -4,18 +4,14 @@
4
4
  * one STL per colour part, and reads STLs back into the common Mesh shape.
5
5
  * Note: rocker/header labels are manifold-engine only and are not emitted here.
6
6
  */
7
-
8
7
  import { readFileSync } from "fs";
9
8
  import { execFileSync } from "child_process";
10
9
  import { basename } from "path";
11
- import { type Resolved, gangType } from "./config.ts";
12
- import type { Mesh } from "./threemf.ts";
13
-
14
- function num(n: number): string {
10
+ import { gangType } from "./config.js";
11
+ function num(n) {
15
12
  return Number.isInteger(n) ? String(n) : n.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
16
13
  }
17
-
18
- export function generateScad(cfg: Resolved): string {
14
+ export function generateScad(cfg) {
19
15
  const d = cfg.dims;
20
16
  const gangs = "[" + cfg.gangs.map((g) => `"${gangType(g)}"`).join(", ") + "]";
21
17
  const legend = "[" + cfg.gangs.map((g) => `"${g.legend ?? ""}"`).join(", ") + "]";
@@ -60,7 +56,6 @@ PRINT_READY = ${cfg.printReady};
60
56
  `;
61
57
  return params + SCAD_BODY;
62
58
  }
63
-
64
59
  const SCAD_BODY = `
65
60
  $fn = 96;
66
61
  CSK_DEPTH = (SCREW_HEAD_D - SCREW_CLEAR_D) / 2 / tan(CSK_ANGLE / 2);
@@ -145,24 +140,25 @@ if (MODE == "preview") {
145
140
  else if (MODE == "legend") oriented() legend_solids(LABEL_DEPTH);
146
141
  else if (MODE == "breaker") oriented() breaker_solid(LABEL_DEPTH);
147
142
  `;
148
-
149
- export function runOpenscad(exe: string, scadPath: string, outPath: string, mode: string): void {
150
- const fmt: string = outPath.endsWith(".3mf") ? "3mf" : outPath.endsWith(".png") ? "png" : "binstl";
151
- execFileSync(exe, [`-D`, `MODE="${mode}"`, "--export-format", fmt, "-o", outPath, scadPath],
152
- { stdio: ["ignore", "ignore", "inherit"] });
143
+ export function runOpenscad(exe, scadPath, outPath, mode) {
144
+ const fmt = outPath.endsWith(".3mf") ? "3mf" : outPath.endsWith(".png") ? "png" : "binstl";
145
+ execFileSync(exe, [`-D`, `MODE="${mode}"`, "--export-format", fmt, "-o", outPath, scadPath], { stdio: ["ignore", "ignore", "inherit"] });
153
146
  console.log(" wrote", basename(outPath));
154
147
  }
155
-
156
- export function readStl(path: string): Mesh {
157
- const buf: Buffer = readFileSync(path);
158
- const n: number = buf.readUInt32LE(80);
159
- const map = new Map<string, number>();
160
- const verts: number[][] = [];
161
- const tris: number[][] = [];
162
- const vid = (x: number, y: number, z: number): number => {
148
+ export function readStl(path) {
149
+ const buf = readFileSync(path);
150
+ const n = buf.readUInt32LE(80);
151
+ const map = new Map();
152
+ const verts = [];
153
+ const tris = [];
154
+ const vid = (x, y, z) => {
163
155
  const k = `${x.toFixed(4)},${y.toFixed(4)},${z.toFixed(4)}`;
164
156
  let i = map.get(k);
165
- if (i === undefined) { i = verts.length; verts.push([x, y, z]); map.set(k, i); }
157
+ if (i === undefined) {
158
+ i = verts.length;
159
+ verts.push([x, y, z]);
160
+ map.set(k, i);
161
+ }
166
162
  return i;
167
163
  };
168
164
  let o = 84;
@@ -170,8 +166,10 @@ export function readStl(path: string): Mesh {
170
166
  const a = vid(buf.readFloatLE(o + 12), buf.readFloatLE(o + 16), buf.readFloatLE(o + 20));
171
167
  const b = vid(buf.readFloatLE(o + 24), buf.readFloatLE(o + 28), buf.readFloatLE(o + 32));
172
168
  const c = vid(buf.readFloatLE(o + 36), buf.readFloatLE(o + 40), buf.readFloatLE(o + 44));
173
- if (a !== b && b !== c && a !== c) tris.push([a, b, c]);
169
+ if (a !== b && b !== c && a !== c)
170
+ tris.push([a, b, c]);
174
171
  o += 50;
175
172
  }
176
173
  return { verts, tris };
177
174
  }
175
+ //# sourceMappingURL=scad.js.map
package/text.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import opentype from "opentype.js";
2
+ export type Contours = [number, number][][];
3
+ export type HAlign = "left" | "center" | "right";
4
+ export declare function loadFont(path: string): opentype.Font;
5
+ /**
6
+ * Build aligned, model-space (y-up) filled contours for `text`.
7
+ * halign positions the bounding box horizontally relative to the origin;
8
+ * vertically it is always centred. Returns [] for empty text.
9
+ */
10
+ export declare function textToContours(font: opentype.Font, text: string, sizeMm: number, halign: HAlign): Contours;
11
+ //# sourceMappingURL=text.d.ts.map
@@ -7,25 +7,18 @@
7
7
  * CrossSection with a NonZero fill rule, which resolves the holes — so no
8
8
  * separate triangulation step is needed.
9
9
  */
10
-
11
10
  import opentype from "opentype.js";
12
-
13
- export type Contours = [number, number][][];
14
- export type HAlign = "left" | "center" | "right";
15
-
16
- const CURVE_SEGS = 12; // polyline segments per bezier
17
-
11
+ const CURVE_SEGS = 12; // polyline segments per bezier
18
12
  let cachedPath = "";
19
- let cachedFont: opentype.Font | null = null;
20
-
21
- export function loadFont(path: string): opentype.Font {
22
- if (cachedFont && cachedPath === path) return cachedFont;
13
+ let cachedFont = null;
14
+ export function loadFont(path) {
15
+ if (cachedFont && cachedPath === path)
16
+ return cachedFont;
23
17
  cachedFont = opentype.loadSync(path);
24
18
  cachedPath = path;
25
19
  return cachedFont;
26
20
  }
27
-
28
- function quad(p0: [number, number], c: [number, number], p1: [number, number], out: [number, number][]): void {
21
+ function quad(p0, c, p1, out) {
29
22
  for (let i = 1; i <= CURVE_SEGS; i++) {
30
23
  const t = i / CURVE_SEGS, u = 1 - t;
31
24
  out.push([
@@ -34,8 +27,7 @@ function quad(p0: [number, number], c: [number, number], p1: [number, number], o
34
27
  ]);
35
28
  }
36
29
  }
37
-
38
- function cubic(p0: [number, number], c1: [number, number], c2: [number, number], p1: [number, number], out: [number, number][]): void {
30
+ function cubic(p0, c1, c2, p1, out) {
39
31
  for (let i = 1; i <= CURVE_SEGS; i++) {
40
32
  const t = i / CURVE_SEGS, u = 1 - t;
41
33
  out.push([
@@ -44,54 +36,78 @@ function cubic(p0: [number, number], c1: [number, number], c2: [number, number],
44
36
  ]);
45
37
  }
46
38
  }
47
-
48
39
  /**
49
40
  * Build aligned, model-space (y-up) filled contours for `text`.
50
41
  * halign positions the bounding box horizontally relative to the origin;
51
42
  * vertically it is always centred. Returns [] for empty text.
52
43
  */
53
- export function textToContours(font: opentype.Font, text: string, sizeMm: number, halign: HAlign): Contours {
54
- if (text === "") return [];
55
- const path = font.getPath(text, 0, 0, sizeMm); // opentype y points DOWN
56
- const contours: Contours = [];
57
- let cur: [number, number][] = [];
58
- let px = 0, py = 0; // current point (opentype space)
59
- const flip = (x: number, y: number): [number, number] => [x, -y]; // -> model y-up
60
-
44
+ export function textToContours(font, text, sizeMm, halign) {
45
+ if (text === "")
46
+ return [];
47
+ const path = font.getPath(text, 0, 0, sizeMm); // opentype y points DOWN
48
+ const contours = [];
49
+ let cur = [];
50
+ let px = 0, py = 0; // current point (opentype space)
51
+ const flip = (x, y) => [x, -y]; // -> model y-up
61
52
  for (const cmd of path.commands) {
62
53
  if (cmd.type === "M") {
63
- if (cur.length) contours.push(cur);
54
+ if (cur.length)
55
+ contours.push(cur);
64
56
  cur = [flip(cmd.x, cmd.y)];
65
- px = cmd.x; py = cmd.y;
66
- } else if (cmd.type === "L") {
57
+ px = cmd.x;
58
+ py = cmd.y;
59
+ }
60
+ else if (cmd.type === "L") {
67
61
  cur.push(flip(cmd.x, cmd.y));
68
- px = cmd.x; py = cmd.y;
69
- } else if (cmd.type === "Q") {
70
- const pts: [number, number][] = [];
62
+ px = cmd.x;
63
+ py = cmd.y;
64
+ }
65
+ else if (cmd.type === "Q") {
66
+ const pts = [];
71
67
  quad([px, py], [cmd.x1, cmd.y1], [cmd.x, cmd.y], pts);
72
- for (const p of pts) cur.push(flip(p[0], p[1]));
73
- px = cmd.x; py = cmd.y;
74
- } else if (cmd.type === "C") {
75
- const pts: [number, number][] = [];
68
+ for (const p of pts)
69
+ cur.push(flip(p[0], p[1]));
70
+ px = cmd.x;
71
+ py = cmd.y;
72
+ }
73
+ else if (cmd.type === "C") {
74
+ const pts = [];
76
75
  cubic([px, py], [cmd.x1, cmd.y1], [cmd.x2, cmd.y2], [cmd.x, cmd.y], pts);
77
- for (const p of pts) cur.push(flip(p[0], p[1]));
78
- px = cmd.x; py = cmd.y;
79
- } else if (cmd.type === "Z") {
80
- if (cur.length) contours.push(cur);
76
+ for (const p of pts)
77
+ cur.push(flip(p[0], p[1]));
78
+ px = cmd.x;
79
+ py = cmd.y;
80
+ }
81
+ else if (cmd.type === "Z") {
82
+ if (cur.length)
83
+ contours.push(cur);
81
84
  cur = [];
82
85
  }
83
86
  }
84
- if (cur.length) contours.push(cur);
85
- if (contours.length === 0) return [];
86
-
87
+ if (cur.length)
88
+ contours.push(cur);
89
+ if (contours.length === 0)
90
+ return [];
87
91
  // bounding box over all (already flipped) points
88
92
  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
89
- for (const c of contours) for (const [x, y] of c) {
90
- if (x < minX) minX = x; if (x > maxX) maxX = x;
91
- if (y < minY) minY = y; if (y > maxY) maxY = y;
92
- }
93
+ for (const c of contours)
94
+ for (const [x, y] of c) {
95
+ if (x < minX)
96
+ minX = x;
97
+ if (x > maxX)
98
+ maxX = x;
99
+ if (y < minY)
100
+ minY = y;
101
+ if (y > maxY)
102
+ maxY = y;
103
+ }
93
104
  const dx = halign === "center" ? -(minX + maxX) / 2 : halign === "right" ? -maxX : -minX;
94
105
  const dy = -(minY + maxY) / 2;
95
- for (const c of contours) for (const p of c) { p[0] += dx; p[1] += dy; }
106
+ for (const c of contours)
107
+ for (const p of c) {
108
+ p[0] += dx;
109
+ p[1] += dy;
110
+ }
96
111
  return contours;
97
112
  }
113
+ //# sourceMappingURL=text.js.map
package/threemf.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export interface Mesh {
2
+ verts: number[][];
3
+ tris: number[][];
4
+ }
5
+ export interface Object3mf {
6
+ name: string;
7
+ mesh: Mesh;
8
+ }
9
+ export declare function build3mf(objects: Object3mf[]): Buffer;
10
+ export declare function writeBinaryStl(mesh: Mesh): Buffer;
11
+ //# sourceMappingURL=threemf.d.ts.map
@@ -5,22 +5,23 @@
5
5
  * individually colourable objects), packed into the OPC zip by hand.
6
6
  * - writeBinaryStl(): a single binary STL (used for the manifold STL path).
7
7
  */
8
-
9
8
  import { deflateRawSync } from "zlib";
10
-
11
- export interface Mesh { verts: number[][]; tris: number[][]; }
12
-
9
+ function xmlEscape(s) {
10
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11
+ }
13
12
  // ---------- multi-object 3MF (core spec, no slicer settings) ----------
14
- export function build3mf(meshes: Mesh[]): Buffer {
15
- // separate top-level objects (each individually colourable in Bambu), all
16
- // at the same transform so the labels stay seated in the plate's pockets.
13
+ export function build3mf(objects) {
14
+ // separate, named top-level objects (each individually selectable/colourable
15
+ // in Bambu), all at the same transform so the labels stay seated in the
16
+ // plate's pockets.
17
17
  let objs = "";
18
18
  let items = "";
19
- meshes.forEach((m, i) => {
19
+ objects.forEach((o, i) => {
20
+ const m = o.mesh;
20
21
  const id = i + 1;
21
22
  const vs = m.verts.map((v) => `<vertex x="${v[0]}" y="${v[1]}" z="${v[2]}"/>`).join("");
22
23
  const ts = m.tris.map((t) => `<triangle v1="${t[0]}" v2="${t[1]}" v3="${t[2]}"/>`).join("");
23
- objs += ` <object id="${id}" type="model"><mesh><vertices>${vs}</vertices><triangles>${ts}</triangles></mesh></object>\n`;
24
+ objs += ` <object id="${id}" type="model" name="${xmlEscape(o.name)}"><mesh><vertices>${vs}</vertices><triangles>${ts}</triangles></mesh></object>\n`;
24
25
  items += ` <item objectid="${id}" transform="1 0 0 0 1 0 0 0 1 128 128 0"/>\n`;
25
26
  });
26
27
  const model = `<?xml version="1.0" encoding="UTF-8"?>
@@ -48,9 +49,8 @@ ${items} </build>
48
49
  { name: "3D/3dmodel.model", data: Buffer.from(model) },
49
50
  ]);
50
51
  }
51
-
52
52
  // ---------- binary STL (single object) ----------
53
- export function writeBinaryStl(mesh: Mesh): Buffer {
53
+ export function writeBinaryStl(mesh) {
54
54
  const buf = Buffer.alloc(84 + mesh.tris.length * 50);
55
55
  buf.writeUInt32LE(mesh.tris.length, 80);
56
56
  let o = 84;
@@ -60,58 +60,86 @@ export function writeBinaryStl(mesh: Mesh): Buffer {
60
60
  const vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
61
61
  let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
62
62
  const len = Math.hypot(nx, ny, nz) || 1;
63
- nx /= len; ny /= len; nz /= len;
64
- buf.writeFloatLE(nx, o); buf.writeFloatLE(ny, o + 4); buf.writeFloatLE(nz, o + 8);
65
- buf.writeFloatLE(a[0], o + 12); buf.writeFloatLE(a[1], o + 16); buf.writeFloatLE(a[2], o + 20);
66
- buf.writeFloatLE(b[0], o + 24); buf.writeFloatLE(b[1], o + 28); buf.writeFloatLE(b[2], o + 32);
67
- buf.writeFloatLE(c[0], o + 36); buf.writeFloatLE(c[1], o + 40); buf.writeFloatLE(c[2], o + 44);
63
+ nx /= len;
64
+ ny /= len;
65
+ nz /= len;
66
+ buf.writeFloatLE(nx, o);
67
+ buf.writeFloatLE(ny, o + 4);
68
+ buf.writeFloatLE(nz, o + 8);
69
+ buf.writeFloatLE(a[0], o + 12);
70
+ buf.writeFloatLE(a[1], o + 16);
71
+ buf.writeFloatLE(a[2], o + 20);
72
+ buf.writeFloatLE(b[0], o + 24);
73
+ buf.writeFloatLE(b[1], o + 28);
74
+ buf.writeFloatLE(b[2], o + 32);
75
+ buf.writeFloatLE(c[0], o + 36);
76
+ buf.writeFloatLE(c[1], o + 40);
77
+ buf.writeFloatLE(c[2], o + 44);
68
78
  o += 50;
69
79
  }
70
80
  return buf;
71
81
  }
72
-
73
- const CRC_TABLE: number[] = (() => {
74
- const t: number[] = [];
82
+ const CRC_TABLE = (() => {
83
+ const t = [];
75
84
  for (let n = 0; n < 256; n++) {
76
85
  let c = n;
77
- for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
86
+ for (let k = 0; k < 8; k++)
87
+ c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
78
88
  t[n] = c >>> 0;
79
89
  }
80
90
  return t;
81
91
  })();
82
- function crc32(buf: Buffer): number {
92
+ function crc32(buf) {
83
93
  let c = 0xffffffff;
84
- for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
94
+ for (let i = 0; i < buf.length; i++)
95
+ c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
85
96
  return (c ^ 0xffffffff) >>> 0;
86
97
  }
87
-
88
- function zip(files: { name: string; data: Buffer }[]): Buffer {
89
- const parts: Buffer[] = [];
90
- const central: Buffer[] = [];
98
+ function zip(files) {
99
+ const parts = [];
100
+ const central = [];
91
101
  let offset = 0;
92
102
  for (const f of files) {
93
103
  const name = Buffer.from(f.name);
94
104
  const comp = deflateRawSync(f.data);
95
105
  const crc = crc32(f.data);
96
106
  const lh = Buffer.alloc(30);
97
- lh.writeUInt32LE(0x04034b50, 0); lh.writeUInt16LE(20, 4); lh.writeUInt16LE(0, 6);
98
- lh.writeUInt16LE(8, 8); lh.writeUInt32LE(0, 10); lh.writeUInt32LE(crc, 14);
99
- lh.writeUInt32LE(comp.length, 18); lh.writeUInt32LE(f.data.length, 22);
100
- lh.writeUInt16LE(name.length, 26); lh.writeUInt16LE(0, 28);
107
+ lh.writeUInt32LE(0x04034b50, 0);
108
+ lh.writeUInt16LE(20, 4);
109
+ lh.writeUInt16LE(0, 6);
110
+ lh.writeUInt16LE(8, 8);
111
+ lh.writeUInt32LE(0, 10);
112
+ lh.writeUInt32LE(crc, 14);
113
+ lh.writeUInt32LE(comp.length, 18);
114
+ lh.writeUInt32LE(f.data.length, 22);
115
+ lh.writeUInt16LE(name.length, 26);
116
+ lh.writeUInt16LE(0, 28);
101
117
  parts.push(lh, name, comp);
102
118
  const ch = Buffer.alloc(46);
103
- ch.writeUInt32LE(0x02014b50, 0); ch.writeUInt16LE(20, 4); ch.writeUInt16LE(20, 6);
104
- ch.writeUInt16LE(0, 8); ch.writeUInt16LE(8, 10); ch.writeUInt32LE(0, 12);
105
- ch.writeUInt32LE(crc, 16); ch.writeUInt32LE(comp.length, 20); ch.writeUInt32LE(f.data.length, 24);
106
- ch.writeUInt16LE(name.length, 28); ch.writeUInt32LE(0, 30); ch.writeUInt32LE(0, 34);
107
- ch.writeUInt32LE(0, 38); ch.writeUInt32LE(offset, 42);
119
+ ch.writeUInt32LE(0x02014b50, 0);
120
+ ch.writeUInt16LE(20, 4);
121
+ ch.writeUInt16LE(20, 6);
122
+ ch.writeUInt16LE(0, 8);
123
+ ch.writeUInt16LE(8, 10);
124
+ ch.writeUInt32LE(0, 12);
125
+ ch.writeUInt32LE(crc, 16);
126
+ ch.writeUInt32LE(comp.length, 20);
127
+ ch.writeUInt32LE(f.data.length, 24);
128
+ ch.writeUInt16LE(name.length, 28);
129
+ ch.writeUInt32LE(0, 30);
130
+ ch.writeUInt32LE(0, 34);
131
+ ch.writeUInt32LE(0, 38);
132
+ ch.writeUInt32LE(offset, 42);
108
133
  central.push(ch, name);
109
134
  offset += lh.length + name.length + comp.length;
110
135
  }
111
136
  const cd = Buffer.concat(central);
112
137
  const eocd = Buffer.alloc(22);
113
- eocd.writeUInt32LE(0x06054b50, 0); eocd.writeUInt16LE(files.length, 8);
114
- eocd.writeUInt16LE(files.length, 10); eocd.writeUInt32LE(cd.length, 12);
138
+ eocd.writeUInt32LE(0x06054b50, 0);
139
+ eocd.writeUInt16LE(files.length, 8);
140
+ eocd.writeUInt16LE(files.length, 10);
141
+ eocd.writeUInt32LE(cd.length, 12);
115
142
  eocd.writeUInt32LE(offset, 16);
116
143
  return Buffer.concat([...parts, cd, eocd]);
117
144
  }
145
+ //# sourceMappingURL=threemf.js.map
package/config.ts DELETED
@@ -1,171 +0,0 @@
1
- /*
2
- * config.ts — config schema, defaults, and loading.
3
- *
4
- * A specific config (e.g. KitchenBack.json) is deep-merged over defaults.json
5
- * (in this directory), so a specific file need only carry what differs — often
6
- * just `name`, `gangs`, and a `breaker.switch`.
7
- *
8
- * Lengths are written CSS-style with a unit suffix: "0.375in", "5mm", "-42mm".
9
- * A bare number is treated as millimetres. Angles are plain numbers (degrees).
10
- */
11
-
12
- import { readFileSync } from "fs";
13
- import { join } from "path";
14
-
15
- export const IN = 25.4;
16
-
17
- export type GangType = "blank" | "decora" | "duplex";
18
- export type Engine = "manifold" | "scad";
19
-
20
- /** A length: CSS-style unit string ("0.375in" / "5mm") or a bare number (mm). */
21
- export type Len = string | number;
22
-
23
- /** Parse a length to millimetres. */
24
- export function mm(v: Len): number {
25
- if (typeof v === "number") return v;
26
- const m = /^\s*(-?[\d.]+)\s*(mm|in)?\s*$/i.exec(v);
27
- if (!m) throw new Error(`invalid length "${v}" (use e.g. "0.375in" or "5mm")`);
28
- const n = parseFloat(m[1]);
29
- return m[2] && m[2].toLowerCase() === "in" ? n * IN : n;
30
- }
31
-
32
- export interface RockerCfg {
33
- left?: string; // label in the left margin, beside this rocker
34
- right?: string; // label in the right margin, beside this rocker
35
- }
36
-
37
- export interface GangCfg {
38
- type?: GangType; // "blank" | "decora" | "duplex"
39
- opening?: boolean; // legacy: true->decora, false->blank
40
- legend?: string; // small label under this gang
41
- filler?: boolean; // blank gang backed by a filler/insert plate ->
42
- // cover screws sit on the Decora line, not the box-ear line
43
- screws?: boolean; // default true; set false to omit this gang's screw holes
44
- rockers?: RockerCfg[]; // top -> bottom, for stacked multi-rocker devices (e.g. Leviton 1755
45
- // triple rocker). Labels sit beside each rocker; the opening is unchanged.
46
- header?: string; // text centred above this gang's opening, e.g. "ON / OFF" / "OFF / ON"
47
- }
48
-
49
- export interface BreakerCfg {
50
- switch?: string; // breaker/circuit label in the lower-right corner, e.g. "A8"
51
- size?: Len; // text size
52
- inset?: Len; // inset from the right and bottom edges
53
- }
54
-
55
- export interface DimsCfg {
56
- gangPitch?: Len; oneGangWidth?: Len; height?: Len; depth?: Len;
57
- faceT?: Len; rimW?: Len; cornerR?: Len;
58
- openW?: Len; openH?: Len; openR?: Len;
59
- duplexW?: Len; duplexH?: Len; duplexSpacing?: Len;
60
- screwSpacing?: Len; blankScrewSpacing?: Len;
61
- screwClearD?: Len; screwHeadD?: Len; cskAngle?: number;
62
- }
63
-
64
- export interface Config {
65
- name?: string;
66
- gangs: GangCfg[]; // left -> right; length = number of gangs
67
- breaker?: BreakerCfg;
68
- legendSize?: Len; legendY?: Len; // per-gang legend (below the opening)
69
- rockerSize?: Len; rockerGap?: Len; // per-rocker side labels
70
- headerSize?: Len; headerGap?: Len; // per-gang on/off header
71
- bevel?: Len; labelDepth?: Len;
72
- blankScrews?: boolean; // blank gangs get a screw pair on the Decora line
73
- font?: string; // OpenSCAD fontconfig name (scad engine)
74
- fontFile?: string; // path to a .ttf/.otf (manifold engine)
75
- plateColor?: string;
76
- accentColor?: string;
77
- printReady?: boolean;
78
- dims?: DimsCfg;
79
- output?: "scad" | "stl" | "3mf";
80
- engine?: Engine; // "manifold" (direct) | "scad" (OpenSCAD)
81
- openscad?: string;
82
- }
83
-
84
- /** Resolved breaker: lengths reduced to millimetres. */
85
- export interface Breaker { switch: string; size: number; inset: number; }
86
-
87
- /** Resolved dimensions, all in millimetres (cskAngle in degrees). */
88
- export interface Dims {
89
- gangPitch: number; oneGangWidth: number; height: number; depth: number;
90
- faceT: number; rimW: number; cornerR: number;
91
- openW: number; openH: number; openR: number;
92
- duplexW: number; duplexH: number; duplexSpacing: number;
93
- screwSpacing: number; blankScrewSpacing: number;
94
- screwClearD: number; screwHeadD: number; cskAngle: number;
95
- }
96
-
97
- /** Fully resolved config: every length reduced to millimetres. */
98
- export interface Resolved {
99
- name: string;
100
- gangs: GangCfg[];
101
- breaker: Breaker;
102
- legendSize: number; legendY: number;
103
- rockerSize: number; rockerGap: number;
104
- headerSize: number; headerGap: number;
105
- bevel: number; labelDepth: number;
106
- blankScrews: boolean;
107
- font: string; fontFile: string;
108
- plateColor: string; accentColor: string;
109
- printReady: boolean;
110
- dims: Dims;
111
- output: "scad" | "stl" | "3mf";
112
- engine: Engine;
113
- openscad: string;
114
- }
115
-
116
- // JSON.parse returns dynamic shape; merged then validated below.
117
- function readJson(path: string): any {
118
- return JSON.parse(readFileSync(path, "utf8"));
119
- }
120
-
121
- /** Deep-merge plain objects; arrays and scalars from `over` replace `base`. */
122
- function deepMerge(base: any, over: any): any {
123
- if (over === null || typeof over !== "object" || Array.isArray(over)) return over;
124
- const isObj = (v: any): boolean => v !== null && typeof v === "object" && !Array.isArray(v);
125
- const out: any = { ...base };
126
- for (const k of Object.keys(over))
127
- out[k] = isObj(base?.[k]) && isObj(over[k]) ? deepMerge(base[k], over[k]) : over[k];
128
- return out;
129
- }
130
-
131
- export function loadConfig(path: string): Resolved {
132
- const defaults = readJson(join(import.meta.dirname, "defaults.json"));
133
- const raw = readJson(path) as Config & { code?: unknown };
134
- if (raw.code !== undefined) // renamed 2026-06-08; don't silently ignore old configs
135
- console.warn(" ! config field 'code' is now 'breaker': { switch }. The 'code' value is ignored.");
136
- // every default lives in defaults.json; the merge guarantees presence
137
- const c = deepMerge(defaults, raw) as Config;
138
- if (!c.gangs || c.gangs.length === 0) throw new Error("config needs a non-empty 'gangs' array");
139
-
140
- const d = c.dims;
141
- const dims: Dims = {
142
- gangPitch: mm(d.gangPitch), oneGangWidth: mm(d.oneGangWidth), height: mm(d.height), depth: mm(d.depth),
143
- faceT: mm(d.faceT), rimW: mm(d.rimW), cornerR: mm(d.cornerR),
144
- openW: mm(d.openW), openH: mm(d.openH), openR: mm(d.openR),
145
- duplexW: mm(d.duplexW), duplexH: mm(d.duplexH), duplexSpacing: mm(d.duplexSpacing),
146
- screwSpacing: mm(d.screwSpacing), blankScrewSpacing: mm(d.blankScrewSpacing),
147
- screwClearD: mm(d.screwClearD), screwHeadD: mm(d.screwHeadD), cskAngle: d.cskAngle,
148
- };
149
- const b = c.breaker;
150
- return {
151
- name: c.name,
152
- gangs: c.gangs,
153
- breaker: { switch: b.switch ?? "", size: mm(b.size), inset: mm(b.inset) },
154
- legendSize: mm(c.legendSize), legendY: mm(c.legendY),
155
- rockerSize: mm(c.rockerSize), rockerGap: mm(c.rockerGap),
156
- headerSize: mm(c.headerSize), headerGap: mm(c.headerGap),
157
- bevel: mm(c.bevel), labelDepth: mm(c.labelDepth),
158
- blankScrews: c.blankScrews,
159
- font: c.font, fontFile: c.fontFile,
160
- plateColor: c.plateColor, accentColor: c.accentColor,
161
- printReady: c.printReady,
162
- dims,
163
- output: c.output, engine: c.engine, openscad: c.openscad,
164
- };
165
- }
166
-
167
- export function gangType(g: GangCfg): GangType {
168
- if (g.type) return g.type;
169
- if (g.opening === true) return "decora";
170
- return "blank";
171
- }