npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bobfrankston/wallplater

v1.0.11

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:

wallplater                                     # no arguments -> usage
wallplater ../KitchenBack.json                 # manifold engine -> KitchenBack.3mf
wallplater ../KitchenBack.json -describe       # explain the config, write nothing
wallplater ../KitchenBack.json -scad           # OpenSCAD engine (same result)
node index.js ../KitchenBack.json              # same, without a global install

| Argument | Meaning | |---|---| | <config.json> | The config to build. Exactly one is required. | | -describe | Print what the config resolves to and write nothing (see below). | | -scad | Use the OpenSCAD engine instead of the built-in manifold engine. | | -help | Print the usage message. |

  • Outputs are written next to the config file, named after the config's name.
  • Flags take a single or double dash and are case-insensitive; unknown flags, a missing config, and more than one config are all rejected with the usage.

Objects and filaments

One object, one part per element: the plate, and one part per label, assembled into a single object named after the config — the structure Bambu Studio's own Assemble command produces. A three-gang plate with a legend, a per-gang breaker, a panel breaker and a note loads as one object with five separately-selectable parts:

KitchenBack plate                   filament 1
KitchenBack g1 legend Disposal      filament 2
KitchenBack g1 breaker A32          filament 2
KitchenBack breaker A36             filament 2
KitchenBack note Note: A18 Behind   filament 3

Names are g<n> <kind> <text> so an element is findable in the slicer's object list. The filament number is written per part via Metadata/model_settings.config (Bambu/Orca extruder), so the plate slices in colour 1 and the text in colour 2 with no manual assignment — but it is only a default: because each element is its own part, any of them can be remapped to any filament in the slicer without regenerating. Defaults are plate → 1, all labels → 2, the note → note.extruder (3). The colours in the config are suggestions — they paint the embedded preview; what actually prints is whatever filament is loaded in that slot.

Label parts sit flush in the plate's front-face pockets (printed front-face-down), so they look hidden in the viewport until coloured. In a single colour they still read as labelDepth (0.6 mm) engraving.

Parts, not separate objects, because Bambu Studio runs a toolpath conflict check between top-level objects and a label inside a pocket of the plate trips it ("Conflicts of gcode paths have been found at layer 2 ... breaker A8 <-> plate"). Parts of one object are exempt, exactly as if the objects had been assembled by hand. (Changed 2026-09-10; earlier files load as separate objects — select them all and Assemble to get the same result.)

Bambu Studio says "The 3mf file has invalid config, load geometry data only" on load. That is a Bambu bug affecting any geometry-only 3MF with no Bambu project config (their own sample files included) — the object, part names and filament assignments still load. This generator deliberately does not write a project_settings.config, which would bake in a printer/filament profile.

Also a Bambu quirk: an object assigned filament 2 shows as filament 1 until the project actually has a second filament — add one in the Filament panel and the labels flip to 2 on their own. (Observed 2026-09-10.)

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.)

-describe

-describe resolves the config (defaults merge, unit conversion, engine choice) and prints it in words instead of building anything — the plate's overall size, each gang's openings and screw holes, every accent label with its size, alignment and position, and the objects and files the run would write. Under -scad it also lists the labels that engine will silently drop.

$ wallplater ../buttons_demo.json -describe
buttons_demo — 2-gang plate, from Y:\dev\...uttons_demo.json
  engine manifold, output 3mf, printReady true (flipped front-face-down for printing)
  plate  115.89mm (4.563in) wide x 114.30mm (4.500in) high x 4.00mm (0.157in) deep
  ...
  gangs (left to right, gang pitch 46.04mm (1.812in)):
    1. decora at x -23.02mm — Decora opening 33.34mm (1.313in) x 66.67mm (2.625in), corner r 0.50mm
       2 screws on the Decora line, 96.84mm (3.813in) apart
       legend "LIGHTS"
    2. button at x +23.02mm — 3 button holes d 12.70mm (0.500in), pip pitch 21.59mm (0.850in)
  ...
  accent text (5 placements in the "labels" object):
    "LIGHTS"  5.00mm  center-aligned at (-23.02, +39.00)mm
  ...
  objects: buttons_demo plate (filament 1), buttons_demo labels (filament 2)
  would write into Y:\dev\...\wallplate: buttons_demo.3mf
  nothing written (-describe)

The label positions come from the same accentPlaces() the manifold engine builds from, so the description cannot drift from the model.

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: false to 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. switch is the text; size/inset come from defaults.json, so a config usually sets only breaker.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 -scad engine 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 implies type:"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 to dims.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 gang legend on top, and print in the accent colour. Sizing/placement: buttonLegendSize (3 mm), buttonLegendGap (1 mm, below each hole).
  • Pip spacing is dims.buttonPitch (0.85in centre-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 -scad engine 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).

Margin note

A small note along the top or bottom edge — the circuit behind the plate, an install date — as its own object on its own filament (default 3), so it can print in a third colour:

"note": "Note: A18 Behind"                  // shorthand: just the text

"note": {                                   // or the full form
  "text": "Note: A18 Behind",                // "\n" stacks it over two lines
  "size": "4mm",                            // the margin band is narrow — see the fit check
  "position": "bottom",                     // "bottom" (default) | "top"
  "align": "left",                          // "left" (default) | "center" | "right"
  "inset": "3mm",                           // from the plate edge, and from the aligned side
  "x": "-60mm", "y": "-54mm",               // optional: exact placement, overriding the above
  "color": "#b00020",                       // preview suggestion, not the printed colour
  "extruder": 3,                            // filament slot; set 2 on a two-colour printer
  "fontFile": "C:/Windows/Fonts/ariali.ttf" // its own face; "" = the plate's fontFile
}

Yes, the note can have its own font — note.fontFile takes any .ttf/.otf independent of the plate's fontFile (manifold engine; the scad engine skips the note entirely and warns).

The margin is tight, so -describe measures the fit from the real glyph outlines and reports it:

  note: bottom margin, left-aligned, inset 4.00mm
        font C:/Windows/Fonts/arialbd.ttf
        clear band in that margin: 1.96mm tall, centred 53.37mm from the plate centre (a note up to ~1.96mm clears the screw heads)
        box 13.01mm x 7.39mm, x -76.96..-63.95, y -53.09..-45.71
        clears the screw heads by 13.95mm, the bevel by 1.20mm

The clear band is the gap between the countersink heads (screwSpacing/2 + screwHeadD/2 out from the centre) and the inner edge of the front chamfer — 1.96 mm on a standard plate with the default 2.8 mm bevel. The box/clears lines are the real thing: the note's glyphs measured in place, against every screw head (as circles) and against the plate outline including the corner radius. A clearance under 0.25 mm is flagged (tight), a negative one OVERLAPS — check this line before printing rather than trusting the size alone.

Sizing, for reference: the default 4 mm gives 2.93 mm-tall glyphs (0.58 mm stems in Arial Bold — comfortably over one 0.4 mm nozzle line); 3 mm gives 0.43 mm stems, about the thinnest that prints cleanly. Neither fits the 1.96 mm band on the screw columns, so a note on a standard plate needs an explicit y that pulls it inboard of the chamfer, and it must sit left or right of the screw columns (the heads reach 3.97 mm either side of each gang centreline). The sample above is a two-line note at the default size with "y": "-49.4mm", "inset": "4mm".

Two lines: put a \n in text ("A18\nBehind") and it stacks, each line aligned on its own by align, the block centred on the note's position. Line spacing is the font's own ascender-to-descender height (~1.15x size).

Note that stacking needs vertical room the margin does not have: two 5 mm lines are ~9.2 mm tall against a 1.96 mm band, so the default edge placement runs off the plate. Give a two-line note an explicit y that pulls it inboard — on a standard plate "y": "-49.4mm" clears the chamfer by 1.2 mm at the default size, and inset should be at least a millimetre more than bevel — and keep it left or right of the screw columns, where nothing else competes for the height. -describe measures the stacked block, so its box/clears lines stay honest.

inset is measured from the plate edge at the note's own height, not from where the straight edge would be — otherwise a left- or right-aligned note in the corner region hangs over the rounded corner. On a wide plate that makes no difference; in the corner it shifts the note inwards by a few mm.

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. | | note | "" | A note in the top or bottom margin, in its own colour/filament — see below. | | 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 | 2.8mm | Front-edge 45° chamfer: how far it runs in from the edge, and how deep. May run deeper than the face (it must stay under dims.depth, and bevel - faceT under dims.rimW). | | 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" }.

Plate profile. The front edge is a 45° chamfer taking most of the depth (bevel 2.8mm of dims.depth 4mm, leaving a 1.2 mm vertical back edge), following the original Wally-customizer plate's profile (4 of 6 mm there — "dims": { "depth": "6mm" }, "bevel": "4mm" reproduces it). The chamfer follows the plan corners (dims.cornerR, 5 mm). On the back, the cavity wall is drafted 45° from the rim (dims.rimW, 3 mm at the back) down to the floor, as the original's is. The Decora opening corner radius dims.openR is 0.5mm — effectively square, so devices seat; a larger radius curves the corners inward and some Decora devices no longer fit.

Files

  • index.ts — CLI entry / usage / engine dispatch / arg validation
  • config.ts — schema, unit parsing, defaults.json merge, loader
  • parts.ts — object names/filenames, default filament per group, scad-engine part list
  • describe.ts — -describe report (reads geometry.ts / parts.ts, writes nothing)
  • defaults.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 (.scad generation + STL readback)
  • threemf.ts — multi-object 3MF writer, binary STL writer