The primary interface. Generated from the live registry — signatures and docs come from the code itself.
assembly_fasteners(assembly_body: 'dict[str, Any]', parts: 'list[str]', default_length: 'float' = 8.0) -> 'dict[str, Any]'
Toolbox, agent-first (SW-map P6): populate every unmated mech.bolt port with a correctly sized ISO 4762 bolt instance + mate, then run the standard assembly validation as the proof. Sizing comes from the port's spec (thread "M3", optional length); specless ports are reported, never guessed. Returns the additions, skips, the updated assembly body, and the post-populate validation.
assembly_interference(assembly_body: 'dict[str, Any]', models: 'dict[str, str]') -> 'dict[str, Any]'
EXACT interference check: build each instanced part's real geometry
(`models`: part id -> model text), place per the assembly transforms,
boolean-intersect every AABB-overlapping pair. Nonzero common volume =
collision, measured in mm3.
assembly_validate(assembly_body: 'dict[str, Any]', parts: 'list[str]') -> 'dict[str, Any]'
Validate an assembly body (instances + mates) against its parts' interfaces: port-type compatibility and positional coincidence — the cross-domain co-design check (ADR-0008).
board_annotate(board: 'str', schematic: 'str', overwrite_conflicts: 'bool' = False) -> 'dict[str, Any]'
Forward annotation (the ECO write path): push the schematic's netlist onto board pads, matched by ref + pin number. Mismatches are reported, never guessed; existing conflicting nets are kept unless overwrite_conflicts. Returns the annotated board text + sync report + board_parity result. For multi-board systems pass the merged system schematic — refs_missing_on_board then just means 'lives on another board'.
board_autoroute(board: 'str', net: 'str', grid: 'float' = 0.25, width: 'float' = 0.25, clearance: 'float' = 0.2, layers: 'list[str] | None' = None) -> 'dict[str, Any]'
Autorouting assist v1: deterministic grid maze router (Lee) for one net — clearance-aware obstacle grid, through-vias on layer change, honest refusal when no path exists. The routed copper is ordinary tracks/vias; DRC + connectivity gate it like hand routing.
board_back_annotate(schematic: 'str', board: 'str') -> 'dict[str, Any]'
Reverse ECO: board value edits flow back into the schematic source (matched by ref); board-only refs are reported, never invented.
board_connectivity(board: 'str') -> 'dict[str, Any]'
Copper connectivity check: every net's pads must be joined by actual touching copper (tracks/vias), and no copper component may bridge two nets. Geometric — catches mislabeled tracks as the shorts they are.
board_drc(board: 'str', rulepack: 'str | None' = None) -> 'dict[str, Any]'
Design-rule check: clearance, track width, annular ring, drill sizes, hole spacing, edge clearance — against a rule pack (default: conservative 2-layer prototype profile). Rule packs are canonical text and shareable.
board_export_fab(board: 'str', outdir: 'str') -> 'dict[str, Any]'
Validate and write the full fabrication package: Gerber X2 layers, Excellon drill, pick-and-place CSV, manifest.
board_gencad(board: 'str') -> 'dict[str, Any]'
GenCAD 1.4 export — the test/assembly-machine format ($BOARD, $PADS/$PADSTACKS, $SHAPES, $COMPONENTS, $DEVICES, $SIGNALS, $ROUTES). Section grammar mirrors kicad-cli's GenCAD export; units INCH per the installed-base convention.
board_import(path: 'str') -> 'dict[str, Any]'
Import an existing KiCad board (.kicad_pcb) into a gitcad board. Pure Python — no kernel needed. The report lists every approximation and drop (zones, arcs, complex outlines); nothing is lost silently.
board_import_altium(path: 'str') -> 'dict[str, Any]'
Import an Altium .PcbDoc saved in ASCII form (components/pads/ tracks/vias/nets, drops reported). Binary OLE PcbDocs are refused with the working migration path (KiCad's Altium importer -> .kicad_pcb -> board_import).
board_ipc2581(board: 'str', origination: 'str' = '1970-01-01T00:00:00') -> 'dict[str, Any]'
IPC-2581C export — the modern fab/assembly exchange XML (stackup, profile, packages, components, logical nets, per-layer features, drills). Structure conformance-benchmarked element-for-element against kicad-cli's own IPC-2581 on the same board. Deterministic: pass origination only when a customer needs a real date.
board_ipcd356(board: 'str') -> 'dict[str, Any]'
IPC-D-356 electrical test netlist (flying probe / bed-of-nails): every netted pad and via as fixed-column records, metric units. Structure follows the published layout; not yet conformance-run on a physical tester.
board_length_match(board: 'str', pairs: 'list[list]', tol_mm: 'float' = 1.0) -> 'dict[str, Any]'
Matched-pair length check (USB, LVDS, clocks): each [netA, netB] pair's routed lengths must agree within tol_mm; unrouted members and mismatches are named violations.
board_odb(board: 'str') -> 'dict[str, Any]'
ODB++ export — the CAM-exchange directory tree (matrix, per-layer features, components, per-span drills + tools, profile, cadnet). Structure copied from kicad-cli's own ODB++ export and census-checked against it on the real benchmark board. Returns {relpath: content}; write with export_odb or any file sink.
board_pad_position(board: 'str', pad: 'str') -> 'dict[str, Any]'
Resolve "REF.pad_name" to absolute board coordinates + side/through/net — no more mental pad arithmetic (dogfood friction #1).
board_route(board: 'str', net: 'str', points: 'list[dict[str, Any]]', width: 'float' = 0.4) -> 'dict[str, Any]'
Route a net through waypoints ({"pad": "REF.name"} or {"x","y"}, optional "layer") — auto-vias on layer changes, wrong-net pads refused, SMD side enforced. Returns the updated board text.
board_stats(board: 'str') -> 'dict[str, Any]'
Board statistics report: area, component counts by side, SMD/PTH pads, routed copper length, vias, zones vs keepouts, drill-size histogram — kicad-cli's stats as data.
board_teardrops(board: 'str', length_ratio: 'float' = 1.0, width_ratio: 'float' = 0.9) -> 'dict[str, Any]'
Generate teardrop copper (same-net wedges) where thin tracks meet wider via/through-pad barrels — drill-breakout insurance. Idempotent; the result is ordinary zones gated by DRC + connectivity like any copper edit. Returns the updated board text.
board_to_model(board: 'str') -> 'dict[str, Any]'
The board as a 3D mech model (outline x thickness, mounting holes cut) — ready for assemblies, interference, STEP, and the viewer.
board_validate(board: 'str') -> 'dict[str, Any]'
Fab-readiness checks on a board document. Machine-readable.
design_merge(base: 'str', ours: 'str', theirs: 'str') -> 'dict[str, Any]'
Semantic 3-way merge of a design document (ADR-0016): features by stable id for models, components by ref + connectivity by PIN for schematics. Parallel edits to different units merge cleanly (a net rename merges — pins move together); the same unit changed differently returns structured conflicts with both candidates, never text markers. Also the git merge driver: gitcad-merge %O %A %B with .gitattributes '*.gitcad.json merge=gitcad'.
design_review(repo: 'str', base: 'str', head: 'str' = 'HEAD') -> 'dict[str, Any]'
Review the design changes between two git refs: per-file semantic diff (features/components/volume/interface-semver), the CHECK DELTA (violations introduced / fixed / pre-existing — ERC, envelopes, board validation, DRC), and before/after SVG renders. gate_ok fails on any INTRODUCED violation; pre-existing reds don't block (not this PR's fault). The markdown field is ready to post as a PR comment.
feature_add(model: 'str', op: 'str', params: 'dict[str, Any] | None' = None, inputs: 'list[str] | None' = None, validate: 'bool' = True) -> 'dict[str, Any]'
Append an intent-level feature and (by default) BUILD the tree it just
extended: a feature the kernel will refuse is refused HERE, with the
kernel's structured reason — never a success now and a refusal three
tools later (#12). A refused feature is NOT added; the returned model is
unchanged. `validate=false skips the build (batching many adds on a
slow model); the deferred truth then arrives at the next
model_measure/model_validate/model_export.
model` is document text, or the PATH of a file-backed model
(model_new(path=...)): file-backed models are PERSISTED on every
successful add — the project viewer live-reloads, so the user watches
each feature land (#7) — and the result carries the viewer URL (#6).
Returns the new model text and the stable feature id (never an ordinal
index).
footprint_generate(kind: 'str', params: 'dict | None' = None) -> 'dict[str, Any]'
Parametric footprint generators (KiCad-map P5) — wizards, agent-first: chip(size='0603'), soic(n=8, pitch=1.27), qfn(n=16, pitch=0.5, ep=2.1), header(n=6, rows=2). Returns the footprint (pads + courtyard) ready for a Board component or footprint_to_part registry publishing.
get_started(cwd: 'str' = '.') -> 'dict[str, Any]'
CALL THIS FIRST at the start of a session. Onboards the user: scans the working directory for existing gitcad designs and returns the recommended next steps — initialize a project if there is none, and open the interactive GUI (viewer_open) so the user can see the 3D model, drawings, checks, and review tabs. Returns a ready-to-build starter model too.
github_report(repo: 'str', title: 'str', body: 'str', kind: 'str' = 'issue', labels: 'list[str] | None' = None, confirm: 'bool' = False) -> 'dict[str, Any]'
File a GitHub issue (kind='issue') or pull request (kind='pr')
against `repo (owner/name). CONSENT-GATED by default: without
confirm=true it only PREVIEWS what would be filed and files nothing —
show the preview to the user and re-invoke with confirm=true ONLY after
they agree. Uses the gh` CLI (must be installed and authenticated); a PR
also needs a pushed branch.
model_configurations(model: 'str', set: 'dict[str, dict[str, Any]] | None' = None, delete: 'list[str] | None' = None) -> 'dict[str, Any]'
Configurations / design tables (SW-map P2): one document IS a
product family. Each configuration is a named override set of
existing parameters ('M3x10': {'L': 10}); the whole table re-resolves
per variant so dependent expressions follow. Build any variant with
model ops' config argument. `model` is document text or a
file-backed model's path (persisted on update). Returns the model text
plus each configuration's fully resolved parameter table — the design
table, as data.
model_drawing(model: 'str', path: 'str', title: 'str' = 'part', sheet: 'str' = 'A3', details: 'list[dict[str, Any]] | None' = None, bom: 'bool' = True, settings: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
Build the model and emit a dimensioned 2D drawing (SVG or PDF by file
extension) of the final feature — front/top/right/iso, third angle. By
default the sheet carries a BOM table (derived from the model's union tree)
with numbered part balloons on the front view; `bom=False suppresses it
(e.g. a one-piece sheet-metal solid).
sheet is an ISO 5457 name ("A4".."A0"), an ANSI Y14.1 name
("ANSI A".."ANSI E"), or "auto"/"auto:ansi" to pick from the series.
settings overrides title-block fields (owner, drawing_number,
revision, date, author, approved_by, units, sheet_no, sheet_count) and
may carry source_path to derive author/date/revision from the git
log of the part file — absent data renders blank, never invented.
model` is document text or a file-backed model's path.
model_entities(model: 'str', feature_id: 'str', kind: 'str' = 'edge', select: 'str | None' = None) -> 'dict[str, Any]'
Stable entity ids + descriptors for a feature's topology (ADR-0003).
`select filters with the query DSL (e.g. "plane,zmax" = the back face;
"cylinder"; "line,zmin") instead of manual centroid filtering. model`
is document text or a file-backed model's path.
model_export(model: 'str', path: 'str', fmt: 'str' = 'step') -> 'dict[str, Any]'
Build the model and export the final feature's shape to STEP or STL —
the mechanical manufacturing deliverable. `model` is document text or
a file-backed model's path.
model_import(path: 'str', fmt: 'str' = 'auto', assets_dir: 'str' = '.') -> 'dict[str, Any]'
Import existing mechanical work into a gitcad model. STEP (planar solids) imports exactly on the forge kernel. Returns the model text plus an honest report of what was imported, approximated, and dropped.
model_mass(model: 'str', density_g_cm3: 'float' = 1.0) -> 'dict[str, Any]'
Physical mass properties of the model's final body: volume (mm^3),
mass (g) at the given density (g/cm^3), center of mass, and the
unit-density inertia tensor about the COM. The engineering numbers a
drawing title block or a motion study starts from. `model` is document
text or a file-backed model's path.
model_measure(model: 'str') -> 'dict[str, Any]'
Build against the best available kernel and return mass properties per
feature — the deterministic oracle an agent verifies against. `model`
is document text or a file-backed model's path.
model_new(path: 'str' = '') -> 'dict[str, Any]'
Create an empty model. RECOMMENDED: pass `path (e.g. 'body.model')
so the document is created ON DISK in the project — text is source
(ADR-0004), and a document that lives only in this conversation is
invisible to the user, undiffable, and lost if the session dies (#7).
File-backed models auto-start the project viewer and return its URL
(#6; opt out with GITCAD_NO_BROWSER=1), and every later
feature_add(model='<path>', ...) persists, so the user WATCHES
features land. Without path` the model exists only in the returned
string (scratch work).
model_parameters(model: 'str', set: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
Named parameters + equations (SW-map P1): read or update the model's
parameter table. Values are numbers or '=expr' strings ('=W/2 + wall';
trig in degrees, pi available). Feature params reference them the same
way — a part becomes a function of its parameters. Re-valuing a
parameter changes geometry (an ADR-0006 breaking change, gated like
any other) but never re-identifies features. `model` is document
text or a file-backed model's path (persisted on update). Returns the
model text and the fully resolved table.
model_recognize(model: 'str') -> 'dict[str, Any]'
Convert dead imported geometry back into a parameterized model — WITH PROOF. Recognizes plate-with-holes shapes; the returned model rebuilds to exactly the input geometry (symmetric-difference residual ~0) with real, editable dimensions. Unrecognizable shapes are reported honestly.
model_validate(model: 'str') -> 'dict[str, Any]'
Build and run geometric validity checks per feature (watertight,
self-intersection, ...). `geometry_verified: false means only the null
backend was available — structure was checked, geometry was NOT.
model` is document text or a file-backed model's path.
part_check_release(old_part: 'str', new_part: 'str') -> 'dict[str, Any]'
Interface-semver release gate (ADR-0009): given old and new part.json texts, classify the interface change and verify the version bump suffices. The check that stops shipping a breaking change as a patch.
pcba_verify(part: 'str', root: 'str') -> 'dict[str, Any]'
Enter a PCBA's electrical workflow as one gate: ERC + electrical envelopes per referenced schematic, board validation + DRC + copper connectivity, and schematic<->board parity — the Fusion-360 duality: a .pcba is mechanical from the outside (envelope, mounting ports, 3D body) and this suite is what 'inside' means.
project_release(sources: 'list[str]', outdir: 'str', version: 'str') -> 'dict[str, Any]'
Project-Releaser-as-code: run EVERY check (validate/ERC/parity/DRC/fab) across the given model/board/schematic documents; only on all-green write the full artifact set + sha256 manifest. Red checks = no release.
requirements_verify(requirements: 'str', root: 'str') -> 'dict[str, Any]'
Requirements as code: run a canonical requirements document (named limits bound to machine checks — mass_max_g, volume_max_mm3, bbox_max_mm, erc_clean, envelope_clean, rail_utilization_max, drc_clean, interference_clear [assembly-wide pairwise fit within a clash budget — mech models AND populated PCBAs]) against the design tree at root. Every requirement reports measured-vs-limit; one without a check shows as 'unchecked' — visible debt, never silent green. markdown field = the executing traceability matrix.
schematic_annotate(schematic: 'str') -> 'dict[str, Any]'
Deterministic reference numbering (KiCad-map P4): placeholder refs (R?, U?) get the lowest free number per prefix in reading order (top-to-bottom, left-to-right); existing numbers never move; nets referencing placeholders refuse (ambiguous — annotate before connecting). Returns the annotated schematic + the rename map.
schematic_author(name: 'str', ops: 'list[list]') -> 'dict[str, Any]'
Author a DRAWN schematic sheet — symbols placed, wires routed, labels and power flags set — and get back the netlist derived from the drawing (same engine as the KiCad importer), ERC, sheet parity, and a KiCad-style SVG. ops is a sequence of: ["place", ref, kind, x, y, {value, rot, footprint, pin_types, left, right, n}] (kinds: resistor|capacitor|led|diode|ic|header) ["connect", refA, pinA, refB, pinB, [via points...]] ["wire", [[x, y], ...]] ["junction", x, y] ["label", net, x, y] ["power", net, x, y] ["global_label", net, x, y] (design-wide across all sheets) ["hier_label", net, x, y] (child-side sheet-pin attachment) ["sheet", name, x, y, w, h, {"ops": [...child ops...], "pins": {pin: [x, y]}, "ref_map": {childRef: instanceRef}, "child": childname}] Hierarchies flatten through the same merge as KiCad import; reuse one child by placing two sheets with the same ops and distinct ref_maps.
schematic_board_parity(schematic: 'str', board: 'str') -> 'dict[str, Any]'
Schematic <-> board consistency (the ECO check): missing components, missing/extra connections, net mismatches — in both directions.
schematic_envelope(schematic: 'str') -> 'dict[str, Any]'
The hardware type system, electrical v1 (ADR-0015): net voltages derived from rail names + net_specs, checked against each pin's v_abs_max/v_op_min, and rail current draw vs. source capacity — overvoltage caught at design time, not bring-up. Coverage is reported (pins_with_specs) so green never masquerades as verified. Includes the per-rail power budget.
schematic_erc(schematic: 'str') -> 'dict[str, Any]'
Electrical rule check on a schematic document: pin-type conflicts, undriven inputs, unpowered power pins, unconnected pins, degenerate nets. 'The schematic compiles' as a machine-decidable statement.
schematic_export_kicad(schematic: 'str') -> 'dict[str, Any]'
Export a KiCad-format netlist (kicadsexpr) — author in gitcad, lay out in pcbnew or anything else that reads KiCad netlists.
schematic_import(path: 'str') -> 'dict[str, Any]'
Import a KiCad schematic (.kicad_sch) into a gitcad schematic. The netlist is derived the way KiCad derives it — geometrically, from wire-pin connectivity, junctions, labels and power symbols — and the report's wire_end_hit_pct self-checks the symbol transforms. Pure Python — no kernel needed.
schematic_import_eagle(path: 'str') -> 'dict[str, Any]'
Import an Eagle .sch (XML): parts + explicit netlist, honest report (netlist-only — Eagle pin names stand in for pad numbers until device mappings are resolved).
schematic_pdf(schematic: 'str', out: 'str') -> 'dict[str, Any]'
Plot a drawn schematic (imported or sheet-authored graphics) to a
vector PDF at `out` — the print/archive projection. The SVG remains
the review-loop rendering; this is the same drawing on paper.
schematic_render(schematic: 'str', path: 'str') -> 'dict[str, Any]'
Render the schematic DIAGRAM (SVG) — symbols, net lanes, junctions: the human review surface before layout. Auto-layout; manual placement honored via component attrs["at"].
schematic_sim(schematic: 'str', checks: 'list[dict] | None' = None) -> 'dict[str, Any]'
Simulation as tests: export the schematic to SPICE (rails become ideal sources by the same name contract the envelope checker uses; unmodeled parts reported, never dropped) and — when ngspice is installed and checks given — run an operating-point analysis asserting node voltages: checks=[{"node": "OUT", "min": 3.2, "max": 3.4}].
schematic_system_erc(schematics: 'list[str]') -> 'dict[str, Any]'
Merge multiple board schematics (canonical gitcad text) into one system schematic — nets union by NAME (the cross-connector contract) — and run ERC on the whole circuit. This is how multi-board designs are checked: per-sheet ERC flags interface signals as single-pin; system ERC sees the real nets.
semantic_diff(old: 'str', new: 'str') -> 'dict[str, Any]'
Meaning-level diff between two revisions of the same document text: features added/removed/changed by stable id, volume delta, board deltas, or interface-semver classification for parts. The PR review surface.
sheetmetal_author(spec: 'dict[str, Any]') -> 'dict[str, Any]'
Sheet metal (SW-map P3) — the mech Gerber. `spec` declares the
part: {name, width, height, thickness, k_factor, bend_radius,
flanges: [{edge: n|e|s|w, length, angle, direction, radius, holes:
[{u, v, diameter}], children: [...same, edge: "end"]}], base_holes}.
Returns the canonical text, DFM validation (hole-to-bend, setback,
radius rules), the exact K-factor flat pattern, the shop DXF (layers
CUT/BEND_UP/BEND_DOWN/HOLES), the bend table, and the folded solid
as an ordinary model document (viewer/STEP/interference all apply;
sharp-corner bends — the flat pattern is the manufacturing truth).
sketch_solve(points: 'dict[str, list[float]]', constraints: 'list[list]', profile: 'list[str] | None' = None) -> 'dict[str, Any]'
Solve a 2D constraint sketch (ADR-0013) and return exact coordinates — authoring-time only; the solved profile is what goes in the document. points: {name: [x, y]} rough positions (they pin the solution branch). constraints: [[kind, ...args]] with kinds fix(p,x,y), coincident(p,q), horizontal(p,q), vertical(p,q), distance(p,q,d), angle(p,q,deg), parallel(p,q,r,s), perpendicular(p,q,r,s), equal_length(p,q,r,s). profile: optional point order to emit a closed Profile params dict.
update_apply(confirm: 'bool' = False) -> 'dict[str, Any]'
Update gitcad to the latest PyPI release (`pip install -U gitcad).
CONSENT-GATED: without confirm=true` it only reports what it would do.
Restart the MCP server afterwards for the new version to load.
update_check() -> 'dict[str, Any]'
Check PyPI for a newer gitcad release. Read-only: returns the installed version, the latest on PyPI, and whether an update is available.
viewer_ask(question: 'str', options: 'list[str] | None' = None, timeout_s: 'float' = 600.0, path: 'str' = '') -> 'dict[str, Any]'
Ask the human watching the viewer a question and BLOCK until they
answer in the page (or the timeout passes). Use it when a design decision
needs their input — a dimension, a material, which variant to pursue.
`options renders as buttons; without options the page offers free
text. answered: false` means nobody answered: that is a real answer
("nobody was watching") — take the conservative path and say you did,
never treat silence as consent.
viewer_close(url: 'str' = '', path: 'str' = '') -> 'dict[str, Any]'
Deprecated alias of viewer_stop (same explicit-user-request-only contract). Empty arguments stop every viewer this session opened.
viewer_list() -> 'dict[str, Any]'
List project viewers: every project this session opened plus the current directory's, each probed for liveness (they are detached processes, so they may well be running from an earlier session).
viewer_note(text: 'str', instance: 'str' = '', kind: 'str' = 'status', path: 'str' = '') -> 'dict[str, Any]'
Narrate one step onto the live rail of the open viewer, so the human
watching sees the work WHILE it happens. `instance names the assembly
member being worked on — the page highlights it and the camera follows.
kind: status (default), done (clears the working highlight), or
problem (rendered red). Use it around every meaningful edit: say what you
are about to do, mark done when you finish. Targets the most recently
opened viewer; path` aims at another watched design.
viewer_open(path: 'str' = '.', design: 'str' = '', review_base: 'str' = '', port: 'int' = 0, open_browser: 'bool' = True) -> 'dict[str, Any]'
Open (or rejoin) THE project viewer — one persistent local web GUI for
the WHOLE project. Call it EARLY, once per project, and leave it running.
The page opens on the project's TOP ASSEMBLY by default; every individual
part is a sub-interface INSIDE the same page (the parts menu, or the
`#part=<file> deep link returned here) — never open a second viewer or
another port to "look at" a part. path is the project directory or any
design file in it; design text serves an unsaved document instead.
The viewer is a DETACHED process: it survives the end of this MCP session,
live-reloads as files change on disk, and re-invoking viewer_open later
(any session) finds and reuses it — same URL. It also launches the user's
browser (open_browser=false` to skip). ALWAYS relay the returned URL to
the user. NEVER stop the viewer because your task is done — it is the
user's window, not yours; only an explicit user request stops it, via
viewer_stop.
viewer_stop(path: 'str' = '.', url: 'str' = '') -> 'dict[str, Any]'
Stop a project's detached viewer — ONLY when the user explicitly asks.
Finishing your work is never a reason: the viewer is the user's window on
the project and is designed to stay up between sessions. `path: the
project (default: the current directory); url` targets one opened this
session by its URL. Returns how many were stopped; viewer_open afterwards
starts fresh.
visualize(design: 'str', explode: 'float' = 0.0, three: 'bool' = False, width: 'int' = 1400, height: 'int' = 900) -> 'dict[str, Any]'
Render a design document to an inline PNG that displays directly in the
chat. `design is document text (model, board, schematic, pcba) or the
PATH of a design file — an ASSEMBLY must be given as a path, because it
resolves its instances from the .part files sitting next to it. 3D
kinds render an isometric view; explode spreads an assembly along its
axes; three` forces the 3D board view. Rasterization uses a local
Chrome/Edge — without one this returns an actionable error rather than a
silent downgrade.