This is the worked template for adding new modules, using the window + door apertures as the example. Follow the same six touch-points for any future module (sliding door, double door, garage header, etc.).
Per the Iconic CAD Protocol, one library-entry schema compiles three ways:
library/modules/<id>/schema.py ─┬── scripts/gen_specs.py ──▶ web/assets/lib/specs.json
(authored schema) │── scripts/gen_wall_instances.py ──▶ wall_instances.yaml
│── (node) scripts/export_members.mjs ──▶ web/assets/lib/members.json
│ (enumerateMembers() — the single framing-lumber source, Decision 6)
│── (freecadcmd, reads members.json) ──▶ web/assets/lib/<id>__<dir>.brp (4: N/S/E/W)
│── (freecadcmd, reads members.json) ──▶ web/assets/lib/volumes.json
│── (freecadcmd, reads members.json) ──▶ cad_library/<id>.FCStd (the part)
└── (headless Chromium) ──▶ web/thumbs/<id>.png
▲
└─── python build_lib.py regenerates ALL of the above
(members.json is baked FIRST, ahead of the freecadcmd steps)
web UI (web/js/) ──Export──▶ layout.json ──compile_from_json.py──▶ House.FCStd
place + snap │ (Python + FreeCAD, ./compile.sh,
3D + BOM │ loads cad_library/*.FCStd)
├──web/js/fcstd.js──▶ house.FCStd
│ (browser, no terminal; loads the .brp +
│ specs.json browser assets above)
└──export_ifc.py──▶ house.ifc
(Python, requires ifcopenshell)
The library entry is the authored source of truth. python build_lib.py is the
single command that validates entries first, then regenerates every derived
artifact from them — the compatibility wall_instances.yaml, the Python
compiler's cad_library/*.FCStd, and the committed browser-export assets
(web/assets/lib/*.brp, volumes.json, specs.json) and web/thumbs/*.png.
The web UI lets a user place parts and exports positions; the JSON compiler (or
the in-browser fcstd.js) reassembles the parts into a house. (The old SVG/icon
compiler is retired — see the legacy branch.)
Module ids carry no orientation or material suffix. The entry directory,
meta.yamlid, andSCHEMA["document_name"]use the module id everywhere downstream — thewall_instances.yamlcompatibility id, thespecs.jsonkey, the<id>__<dir>.brpfilename, theweb/thumbs/<id>.pngname, thecad_library/<id>.FCStdpart, and theidinweb/js/constants.js. Keep it the same string in all of them (e.g.window_4x8_2x6_36x48, notwindow_4x8_2x6_36x48_south).build_libenforces this — every artifact is derived from the entry id.
A door is a window taken to the floor. Same king/jack/header/cripple logic;
a window just adds a sill and lower cripples. So windows and doors are one
parametric object — a family: aperture_wall_panel driven by an aperture
block — not two code paths. They are 48"-wide wall panels and snap exactly like
a plain wall. Exact framing dims were measured from OSE source CAD; see
aperture_framing_reference.md.
Add a module entry with schema.py, compiler.py, meta.yaml, and
expect.yaml. The SCHEMA parameters carry the shared wall parameters plus an
aperture block:
SCHEMA = {
"schema_name": "ApertureWallPanel",
"units": "in",
"document_name": "window_4x8_2x6_36x48",
"family": "aperture_wall_panel",
"parameters": (
("nominal_width_ft", 4.0),
("nominal_height_ft", 8.0),
("stud_lumber_nominal", "2x6"),
("stud_spacing_oc_in", 16),
("osb_thickness_in", 0.4375),
("exterior_face", "south"),
("reference_house_orientation", "faces_south"),
("aperture", (
("type", "window"),
("rough_opening_width_in", 36),
("rough_opening_height_in", 48),
("sill_height_in", 24),
("header_lumber_nominal", "2x8"),
("header_plies", 2),
)),
),
}osb_thickness_in: 0 makes it an interior (2x4, no-OSB) panel.
wall_instances.yaml is generated from these entries by
scripts/gen_wall_instances.py for older code paths that still read the YAML.
build_aperture_panel() reads the aperture block and emits conventional
platform framing (kings, jacks, header at the RO top, cripples above, plus a
sill + lower cripples for a window). main() dispatches to it whenever an
instance has an aperture key. Port markers are identical to walls, so the
JSON compiler treats apertures as ordinary panels — no compiler changes needed.
You don't call this directly — build_lib (step 3b) invokes the same
build_wall / build_aperture_panel functions to produce both the
cad_library/*.FCStd part and the per-direction browser .brp solids. To
build only the cad_library/ parts for the CLI compiler, ./generate.sh still
runs generate_wall_library.py on its own.
Verify a panel's framing by measuring its solids (W × D × H, origin) in
freecadcmd — that's how the reference dims were checked.
- Add the module to
APERTURE_MODULES(exterior) orINT_APERTURE_MODULES(interior) inweb/js/constants.jswith anaperturemetadata object (inches). These flow intoALL_MODULES, so snapping/ports/save/load work for free. drawAperturePlan()inweb/js/render2d.jsrenders the conventional top-down floor-plan symbol — glazing double-line + jamb ticks for a window; opening gap- leaf + swing arc for a door (hinged to the interior side).
- The library palette tile uses an inline-SVG plan thumbnail (no icon files); pick it, then press R to rotate.
This is the step that's easy to forget — and forgetting it is exactly what leaves the in-browser FreeCAD export stale. After adding the module entry, regenerate every derived artifact with one command:
python build_lib.pyIt produces (all idempotent, from the entries):
| Artifact | Consumer | Needs |
|---|---|---|
wall_instances.yaml |
compatibility path for older generators / compilers | plain Python |
web/assets/lib/specs.json |
fcstd.js framing params |
plain Python |
web/assets/lib/members.json |
seh_lib/wall_builder.py's framing-lumber solids |
node |
web/assets/lib/<id>__<dir>.brp (×4) |
fcstd.js per-direction solids |
freecadcmd |
web/assets/lib/volumes.json |
reference / sanity-check | freecadcmd |
cad_library/<id>.FCStd |
compile_from_json.py (CLI) |
freecadcmd |
web/thumbs/<id>.png |
library palette | headless Chromium |
The .brp are baked per direction the same way compile_from_json.prepare_shape
poses each wall (rotate about Z, drop the bounding box to the origin), so
fcstd.js's Locations-injection reproduces the Python compiler exactly. Without
this step a new module works in the CLI compiler and the 2D/3D preview but is
missing or wrong in the browser .FCStd export.
The editor can save a placed arrangement of existing validated wall modules as a
custom assembly zip. The zip contains one library entry folder named by the
entry id, with schema.py, compiler.py, meta.yaml, expect.yaml,
<id>.json, and README.txt. The browser loads only the canonical
<id>.json view; it never executes the Python files.
Custom assemblies are layer: assembly entries and default to status: wip.
The editor also ships built-in VCS assemblies from vcs-library exported canonical views in web/data/builtin_library/.
Validate a saved zip by unpacking it into a library root and running:
python -m libtools validate-code --root <library-root> <id>This path saves arrangements only. New module geometry, new framing families, or
new baked browser-export solids still go through the normal library entry plus
python build_lib.py bake path described above.
Useful flags:
python build_lib.py --verify # rebuild to a temp dir, diff vs committed; clobbers nothing
python build_lib.py --no-thumbs # skip the Chromium thumbnail bake (needs network for the three.js CDN)Commit the entry change together with the regenerated compatibility YAML,
specs.json, .brp, volumes.json, and thumbs. CI asserts the YAML and
specs.json stay in sync with the entries.
(FreeCAD version differences can make the .brp non-byte-identical even when the
geometry is correct — --verify compares geometry, not bytes.)
buildAperture3D() in web/js/render3d.js mirrors the generator framing in
three.js so the live preview shows the real opening (OSB cut out around the RO).
buildWall3D() dispatches to it for any module with an aperture.
Add a module_specs.<id> entry (studs, plates, osb_sheets,
lumber_type, nail/screw counts). Stud counts for apertures are full-stud-
equivalent estimates (kings + jacks + cripples + header stock); note this in
the entry's notes.
Do not edit wall_instances.yaml directly. It is regenerated from
library/modules/<id>/schema.py:
python scripts/gen_wall_instances.pyThe fast CI lane runs:
python scripts/gen_wall_instances.py --verify--verify regenerates the YAML to a temp file and byte-compares it against the
committed copy, exiting nonzero on drift.
-
library/modules/<id>/entry added (id has no orientation/material suffix — same string used everywhere downstream) -
web/js/constants.js: module entry inAPERTURE_MODULES/INT_APERTURE_MODULESwith the sameid -
web/js/render2d.js: plan silhouette indrawAperturePlan() -
web/js/render3d.js: 3D framing inbuildAperture3D() -
web/pricing.json: BOM spec -
python scripts/gen_wall_instances.py— regenerates compatibilitywall_instances.yamlfrom the entries -
python build_lib.py— regeneratesspecs.json,members.json, the 4.brp,volumes.json,cad_library/*.FCStd, the compatibility YAML, and the thumbnail. Don't skip this — it's what keeps the browser export (and, since the single-enumerator refactor, the Python compiler's framing geometry too) from going stale. Commit all of it with the entry change. -
compile_from_json.pyreassembles it (run./compile.sh test_layout.json) -
python build_lib.py --verifyclean, andnode tests/parity.mjspasses -
node tests/enumerate_parity.mjsandnode tests/members_export.mjspass -
python scripts/gen_wall_instances.py --verifyclean - dims sourced from real CAD / code, not guessed — record them in aperture_framing_reference.md