@bobfrankston/wallplater
v1.0.7
Published
JSON-driven wall-plate generator (Decora / duplex / blank). Direct-to-3MF via manifold-3d, with an optional OpenSCAD engine.
Readme
@bobfrankston/wallplater
JSON-driven wall-plate generator (Decora / duplex / blank / button gangs). Reads a small JSON (JSON5) config and emits a multi-object 3MF (plate + labels as separate, individually colourable objects) or per-part STLs.
Engines
| Engine | Flag | Needs OpenSCAD? | Speed | Notes |
|---|---|---|---|---|
| manifold (default) | — | no | ~1 s | Direct geometry via manifold-3d (Clipper2 + robust CSG). Text via opentype.js. |
| scad (legacy) | -scad or "engine":"scad" | yes | ~30 s | Writes a .scad and shells out to OpenSCAD. Reference / fallback. Renders all hole geometry (incl. button pips) but not rocker/header/per-button labels. |
The manifold engine has no system dependency beyond Node and is the path
forward; -scad is kept for parity/verification.
If the -scad engine can't find OpenSCAD (neither the openscad config path nor
openscad on PATH), it offers to install it for you — winget on Windows,
brew --cask on macOS, apt-get on Linux — then re-checks. Decline (or a
non-interactive stdin) exits with a hint to install manually or drop -scad.
Build / install
TypeScript compiled to co-located .js / .d.ts / .map (no src/dist
split). tsc is the global install.
npm install
npm run build # tsc (or: npm run watch for tsc -w)Installed globally (npm i -g @bobfrankston/wallplater) it exposes a
wallplater command. defaults.json is resolved relative to the module, so it
runs correctly from any working directory.
Usage
Requires Node 24+ (ESM). Run the compiled entry:
node index.js ../KitchenBack.json # manifold engine -> KitchenBack.3mf
node index.js ../KitchenBack.json -scad # OpenSCAD engine (same result)
wallplater ../KitchenBack.json # if installed globally- Outputs are written next to the config file, named after the config's
name. - Unknown flags are rejected.
-scad(or--scad) selects the OpenSCAD engine.
The 3MF holds up to three named, individually-selectable objects — <name> plate
(filament 1), <name> labels (all accent text, filament 2), <name> breaker
(filament 2). The filament assignment is baked in via Metadata/model_settings.config
(Bambu/Orca per-object extruder), so the text slices in your second colour
without manual assignment — provided the project has ≥2 filaments. The label
objects sit flush in the plate's front-face pockets (printed front-face-down), so
they look hidden in the viewport until coloured.
The 3MF also embeds a preview thumbnail (Metadata/thumbnail.png, wired via
the OPC thumbnail relationship) so Windows Explorer and slicers show an image of
the plate. It's a pure-JS orthographic render of the front face in the configured
plateColor / accentColor — no extra dependency, no STL/mesh rendering by the
shell. (Without it, the file previews blank: the shell never renders the geometry
itself, it only displays this embedded PNG.)
Config
Each specific config is deep-merged over defaults.json (in this
directory), so a specific file carries only what differs — often just name,
gangs, and a breaker.switch. Every default lives in defaults.json, not in
code.
Configs are parsed with JSON5 (the most lenient reader), so comments
(//, /* */), trailing commas, and unquoted keys are all allowed — plain JSON
still works unchanged.
Lengths are written CSS-style with a unit suffix. Supported units:
| Suffix | Meaning | mm |
|---|---|---|
| in | inch | 25.4 |
| ft | foot | 304.8 |
| cm | centimetre | 10 |
| mm | millimetre | 1 |
e.g. "0.375in", "5mm", "2cm", "0.5ft", "-42mm". A bare number (no
suffix) is treated as millimetres. Angles (e.g. cskAngle) are plain numbers.
// KitchenBack.json — only what differs from defaults.json
{
"name": "KitchenBack",
"gangs": [
{ "type": "decora", "legend": "Disposal" },
{ "type": "blank", "screws": false }, // middle blank, no screw holes
{ "type": "decora" }
]
// add later: "breaker": { "switch": "A8" }
}gangs[].type:"decora"|"duplex"|"blank"|"button".gangs[].screws:falseto omit that gang's screw holes (default true).gangs[].filler: on a blank gang, marks it as backed by a filler/insert plate, so its cover screws sit on the Decora line (wider spacing) instead of the box-ear line.gangs[].legend: single label centred above the gang's opening (positioned clear of the screw holes;legendY/legendSize).gangs[].breaker: per-gang breaker/circuit label, lower-right of the opening and clear of the screws (gangBreakerY/gangBreakerSize).breaker:{ "switch": "A8", "size": "0.375in", "inset": "0.25in" }— the panel-wide breaker label in the lower-right corner.switchis the text;size/insetcome fromdefaults.json, so a config usually sets onlybreaker.switch. Per-gang (gangs[].breaker) and panel-wide (breaker) labels are independent — use either or both.
Stacked rocker labels (Leviton 1755 triple rocker, etc.)
The 1755 is three rockers stacked in one standard Decora opening, so the opening is unchanged — only the labelling differs. Per gang:
{
"type": "decora",
"header": "ON / OFF", // centred above the opening
"rockers": [ // top -> bottom (1-3+ entries)
{ "left": "1", "right": "FAN" }, // a label each side of every rocker
{ "left": "2", "right": "LIGHT" },
{ "left": "3", "right": "HEAT" }
]
}Rockers are spaced evenly down the opening; left labels are right-aligned snug
to the opening, right labels left-aligned. legend / header / rockers /
per-gang breaker all print in the accent colour (the labels object). Relevant defaults:
rockerSize (3.5mm), rockerGap (1.5mm), headerSize (4mm), headerGap
(5mm). Keep side labels short on multi-gang plates — the side margin is ~18 mm
on an end/single gang but only ~6 mm between interior gangs; long labels can
overflow. See ../triple_demo.json for a worked single-gang example.
Rocker/header labels are rendered by the manifold engine only. The
-scadengine ignores them (it warns).
Button gangs (round holes in a pip layout)
A "button" gang punches a set of round button holes (default 0.5 in
diameter) arranged symmetrically like the pips on a die / playing card — odd
counts put a single button in the centre. Set the count with buttons (1–9):
{
"type": "button",
"legend": "FAN", // the legend "on top" (above the buttons)
"buttons": 3, // 1-9; pip layout, single = centre
"buttonD": "0.5in", // optional per-gang diameter (default dims.buttonD)
"buttonLegends": ["HI", "MED", "LO"] // optional small per-button labels
}buttons: number of holes (1–9). Setting it impliestype:"button". The pip pattern is the standard die/domino layout, read top → bottom, left → right:1=centre,2/3=diagonal,4/5=corners (+centre for 5),6/7=two columns (+centre for 7),8/9=full ring/grid. Counts above 9 are an error.buttonD: per-gang hole diameter; defaults todims.buttonD(0.5in).buttonLegends: small labels, one per button in pip reading order (""or a short array skips some). They are in addition to the ganglegendon top, and print in the accent colour. Sizing/placement:buttonLegendSize(3 mm),buttonLegendGap(1 mm, below each hole).- Pip spacing is
dims.buttonPitch(0.85incentre-to-centre). A button gang mounts like a device — it gets a screw pair on the Decora line unless"screws": false.
See ../buttons_demo.json for a worked Decora-plus-buttons example.
Per-button legends are rendered by the manifold engine only (the
-scadengine emits the button holes but skips the small labels, and warns).
See ../decora_wallplate_spec.md for the dimensional reference and print notes
(print front-face-down; PETG/ASA recommended for heat near devices; keep plate
and labels the same material family — see the chat note on ABS+PLA).
Appearance, output & engine
Top-level keys (all have defaults in defaults.json):
| Key | Default | Meaning |
|---|---|---|
| output | "3mf" | "3mf" (multi-object + thumbnail), "stl" (one binary STL per part), or "scad" (write the .scad only, no render — implies the scad engine). |
| engine | "manifold" | "manifold" (direct) or "scad" (OpenSCAD). -scad on the CLI forces scad. |
| plateColor | "#35589f" | Plate (filament 1) colour — used for the embedded preview and the scad-engine preview. |
| accentColor | "#101010" | Label (filament 2) colour, same uses. |
| printReady | true | Flip the model front-face-down (rotate 180° about X) for printing. The preview camera follows the flip, so labels stay upright either way. |
| fontFile | Arial Bold | Path to a .ttf/.otf for the manifold engine's text. |
| font | Liberation Sans Bold | OpenSCAD fontconfig name for the scad engine's text. |
| openscad | Program Files path | OpenSCAD executable; only used by the scad engine (auto-install offered if missing). |
| bevel | 1.2mm | Front-edge chamfer width. |
| labelDepth | 0.6mm | Recess depth of the label pockets / height of the label solids. |
Plate geometry, opening sizes, screw spacing, etc. live under the dims object
(see defaults.json for the full list, all unit-suffixed lengths). Override any
single dim by deep-merge, e.g. "dims": { "depth": "5mm" }.
Files
index.ts— CLI entry / engine dispatch / arg validationconfig.ts— schema, unit parsing, defaults.json merge, loaderdefaults.json— all default values (merged under each specific config)geometry.ts— manifold engine (direct geometry)text.ts— glyph outlines -> filled contours (opentype.js)scad.ts— legacy OpenSCAD engine (.scadgeneration + STL readback)threemf.ts— multi-object 3MF writer, binary STL writer
