forgecad 0.10.0 → 0.10.2

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.
Files changed (75) hide show
  1. package/dist/assets/{AdminPage-DwYHz72L.js → AdminPage-CHY6ZN-p.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-a9_f-1US.js → BenchmarkPage-BcRT5iGN.js} +1 -1
  3. package/dist/assets/{BlogPage-DodHpvmf.js → BlogPage-BssBbnb-.js} +1 -1
  4. package/dist/assets/{DocsPage-B5LePEuj.js → DocsPage-DsvdiRNK.js} +33 -2
  5. package/dist/assets/{EditorApp-QXsAISLR.js → EditorApp-Bfd3jbtC.js} +185 -44
  6. package/dist/assets/{EmbedViewer-DdEHGUMU.js → EmbedViewer-D5t8WamV.js} +3 -3
  7. package/dist/assets/{LandingPageProofDriven-yhhOodbf.js → LandingPageProofDriven-DbN7o-Be.js} +1 -1
  8. package/dist/assets/{LegalPage-5RbKRGYK.js → LegalPage-DNGrrY0p.js} +1 -1
  9. package/dist/assets/{PricingPage-E3Rma7aV.js → PricingPage-Nczr3pRz.js} +1 -1
  10. package/dist/assets/{SettingsPage-BJZcM97j.js → SettingsPage-DZlyu4d4.js} +1 -1
  11. package/dist/assets/{app-DSYrDg0V.js → app-C9ct2hRD.js} +1752 -474
  12. package/dist/assets/{app-CE3sYcV7.css → app-CjsbDlb7.css} +143 -0
  13. package/dist/assets/{scalar-sampling-budget-o90NSNmF.js → backendInit-ymjonyQp.js} +85756 -78750
  14. package/dist/assets/cli/{render-ZMHR9HkV.js → render-B_0lQwKU.js} +71 -193
  15. package/dist/assets/{constructionHistoryWorker-AwMMWSxg.js → constructionHistoryWorker-CZ42Dksy.js} +8058 -1225
  16. package/dist/assets/{evalWorker-DbNs7Dkp.js → evalWorker-C2pm8LHP.js} +23037 -15821
  17. package/dist/assets/{forgecad_geometry-Dgceylq9.js → forgecad_geometry-BlMtqluF.js} +120 -1
  18. package/dist/assets/{forgecad_geometry_bg-dD4RNQF1.wasm → forgecad_geometry_bg-BllP_WiL.wasm} +0 -0
  19. package/dist/assets/{inspectWorker-CZsCFtQT.js → inspectWorker-D5T5VbfK.js} +31375 -32603
  20. package/dist/assets/{jointPose-DO6mnXn_.js → jointPose-4r8ed8_5.js} +1 -1
  21. package/dist/assets/{manifold-BU-tJwQh.js → manifold-5PP1eGLN.js} +1 -1
  22. package/dist/assets/{manifold-fy2MV7K1.js → manifold-C4r6B-XY.js} +2 -2
  23. package/dist/assets/{manifold-BGlQBBH9.js → manifold-DjBkyIc8.js} +1 -1
  24. package/dist/assets/{reportWorker-DO6hcQbh.js → reportWorker-CwenM7wB.js} +46620 -44936
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +2 -2
  27. package/dist/docs-raw/CLI.md +43 -16
  28. package/dist/docs-raw/generated/assembly.md +71 -6
  29. package/dist/docs-raw/generated/concepts.md +17 -3
  30. package/dist/docs-raw/generated/core.md +10 -3
  31. package/dist/docs-raw/generated/output.md +14 -43
  32. package/dist/docs-raw/generated/runtime-names.md +4 -4
  33. package/dist/docs-raw/generated/sdf.md +2 -2
  34. package/dist/docs-raw/guides/simready-quickstart.md +173 -0
  35. package/dist/docs-raw/simulation-workflow.md +273 -0
  36. package/dist/index.html +2 -2
  37. package/dist/sitemap.xml +25 -13
  38. package/dist-cli/{check-compiler-JTVBITCR.js → check-compiler-SP7FAL7R.js} +1 -1
  39. package/dist-cli/{check-query-propagation-3FFLSMVN.js → check-query-propagation-BRLSHP22.js} +1 -1
  40. package/dist-cli/{chunk-OAN5T4XD.js → chunk-RQQ42YCP.js} +51209 -43456
  41. package/dist-cli/forgecad.js +5783 -1691
  42. package/dist-cli/{forgecad_geometry-QOQIIP53.js → forgecad_geometry-7TVSNVUB.js} +119 -0
  43. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  44. package/dist-skill/CONTEXT.md +107 -68
  45. package/dist-skill/docs/API/core/concepts.md +2 -2
  46. package/dist-skill/docs/CLI.md +43 -16
  47. package/dist-skill/docs/generated/assembly.md +67 -6
  48. package/dist-skill/docs/generated/core.md +10 -3
  49. package/dist-skill/docs/generated/output.md +14 -43
  50. package/dist-skill/docs/generated/runtime-names.md +4 -4
  51. package/dist-skill/docs/generated/sdf.md +2 -2
  52. package/examples/api/gyroid-voronoi-blend.forge.js +1 -1
  53. package/examples/api/organic-noise-sculpture.forge.js +1 -1
  54. package/examples/api/sdf-circular-array-knurling.forge.js +1 -1
  55. package/examples/api/{sdf-custom-raymarch.forge.js → sdf-custom-field-mesh-preview.forge.js} +3 -4
  56. package/examples/api/sdf-materialize-tree.forge.js +2 -2
  57. package/examples/api/sdf-plain-return.forge.js +3 -2
  58. package/examples/api/sdf-shapes.forge.js +2 -2
  59. package/examples/api/sdf-surface-basket-weave.forge.js +2 -2
  60. package/examples/generative/twisted-lattice-tower.forge.js +1 -1
  61. package/examples/generative/voronoi-lampshade.forge.js +1 -1
  62. package/examples/robotics/README.md +46 -0
  63. package/examples/robotics/scout-cam-rover-simready/README.md +119 -0
  64. package/examples/robotics/scout-cam-rover-simready/lib/dims.js +140 -0
  65. package/examples/robotics/scout-cam-rover-simready/main.forge.js +343 -0
  66. package/examples/robotics/scout-cam-rover-simready/parts/body.forge.js +304 -0
  67. package/examples/robotics/scout-cam-rover-simready/parts/chassis.forge.js +320 -0
  68. package/examples/robotics/scout-cam-rover-simready/parts/hardware.forge.js +21 -0
  69. package/examples/robotics/scout-cam-rover-simready/parts/turret.forge.js +70 -0
  70. package/examples/robotics/scout-cam-rover-simready/parts/wheel.forge.js +116 -0
  71. package/examples/robotics/simready-asset-crate.forge.js +79 -0
  72. package/examples/robotics/simready-diff-drive-rover.forge.js +141 -0
  73. package/examples/robotics/simready-parallel-gripper.forge.js +102 -0
  74. package/package.json +2 -2
  75. package/dist/assets/manifold-CzYf_iub.js +0 -3023
@@ -154,6 +154,25 @@ function geometry_create_revolved_regions(regions_json, degrees, segments) {
154
154
  }
155
155
  return ret[0] >>> 0;
156
156
  }
157
+ function geometry_create_sdf_mesh(abi_version, opcodes, arg_a, arg_b, arg_c, constants, output, slot_count, options_json) {
158
+ const ptr0 = passArray8ToWasm0(opcodes, wasm.__wbindgen_malloc);
159
+ const len0 = WASM_VECTOR_LEN;
160
+ const ptr1 = passArray32ToWasm0(arg_a, wasm.__wbindgen_malloc);
161
+ const len1 = WASM_VECTOR_LEN;
162
+ const ptr2 = passArray32ToWasm0(arg_b, wasm.__wbindgen_malloc);
163
+ const len2 = WASM_VECTOR_LEN;
164
+ const ptr3 = passArray32ToWasm0(arg_c, wasm.__wbindgen_malloc);
165
+ const len3 = WASM_VECTOR_LEN;
166
+ const ptr4 = passArrayF64ToWasm0(constants, wasm.__wbindgen_malloc);
167
+ const len4 = WASM_VECTOR_LEN;
168
+ const ptr5 = passStringToWasm0(options_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
169
+ const len5 = WASM_VECTOR_LEN;
170
+ const ret = wasm.geometry_create_sdf_mesh(abi_version, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, output, slot_count, ptr5, len5);
171
+ if (ret[2]) {
172
+ throw takeFromExternrefTable0(ret[1]);
173
+ }
174
+ return ret[0] >>> 0;
175
+ }
157
176
  function geometry_create_side_profile_cut(spec_json) {
158
177
  const ptr0 = passStringToWasm0(spec_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
159
178
  const len0 = WASM_VECTOR_LEN;
@@ -249,6 +268,9 @@ function geometry_delete_nurbs_curve(handle) {
249
268
  function geometry_delete_nurbs_surface(handle) {
250
269
  wasm.geometry_delete_nurbs_surface(handle);
251
270
  }
271
+ function geometry_delete_sdf_mesh(handle) {
272
+ wasm.geometry_delete_sdf_mesh(handle);
273
+ }
252
274
  function geometry_export_step(handle) {
253
275
  let deferred2_0;
254
276
  let deferred2_1;
@@ -509,6 +531,47 @@ function geometry_sample_nurbs_surface(handle, u_samples, v_samples) {
509
531
  wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
510
532
  }
511
533
  }
534
+ function geometry_sdf_mesh_metadata(handle) {
535
+ let deferred2_0;
536
+ let deferred2_1;
537
+ try {
538
+ const ret = wasm.geometry_sdf_mesh_metadata(handle);
539
+ var ptr1 = ret[0];
540
+ var len1 = ret[1];
541
+ if (ret[3]) {
542
+ ptr1 = 0;
543
+ len1 = 0;
544
+ throw takeFromExternrefTable0(ret[2]);
545
+ }
546
+ deferred2_0 = ptr1;
547
+ deferred2_1 = len1;
548
+ return getStringFromWasm0(ptr1, len1);
549
+ } finally {
550
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
551
+ }
552
+ }
553
+ function geometry_sdf_mesh_tri_verts(handle) {
554
+ const ret = wasm.geometry_sdf_mesh_tri_verts(handle);
555
+ if (ret[3]) {
556
+ throw takeFromExternrefTable0(ret[2]);
557
+ }
558
+ var v1 = getArrayU32FromWasm0(ret[0], ret[1]).slice();
559
+ wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
560
+ return v1;
561
+ }
562
+ function geometry_sdf_mesh_vert_properties(handle) {
563
+ const ret = wasm.geometry_sdf_mesh_vert_properties(handle);
564
+ if (ret[3]) {
565
+ throw takeFromExternrefTable0(ret[2]);
566
+ }
567
+ var v1 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
568
+ wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
569
+ return v1;
570
+ }
571
+ function geometry_sdf_mesher_abi_version() {
572
+ const ret = wasm.geometry_sdf_mesher_abi_version();
573
+ return ret >>> 0;
574
+ }
512
575
  function geometry_select_edges(handle, query_json) {
513
576
  let deferred3_0;
514
577
  let deferred3_1;
@@ -621,9 +684,38 @@ function __wbg_get_imports() {
621
684
  "./forgecad_geometry_bg.js": import0
622
685
  };
623
686
  }
687
+ function getArrayF32FromWasm0(ptr, len) {
688
+ ptr = ptr >>> 0;
689
+ return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
690
+ }
691
+ function getArrayU32FromWasm0(ptr, len) {
692
+ ptr = ptr >>> 0;
693
+ return getUint32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
694
+ }
695
+ var cachedFloat32ArrayMemory0 = null;
696
+ function getFloat32ArrayMemory0() {
697
+ if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
698
+ cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
699
+ }
700
+ return cachedFloat32ArrayMemory0;
701
+ }
702
+ var cachedFloat64ArrayMemory0 = null;
703
+ function getFloat64ArrayMemory0() {
704
+ if (cachedFloat64ArrayMemory0 === null || cachedFloat64ArrayMemory0.byteLength === 0) {
705
+ cachedFloat64ArrayMemory0 = new Float64Array(wasm.memory.buffer);
706
+ }
707
+ return cachedFloat64ArrayMemory0;
708
+ }
624
709
  function getStringFromWasm0(ptr, len) {
625
710
  return decodeText(ptr >>> 0, len);
626
711
  }
712
+ var cachedUint32ArrayMemory0 = null;
713
+ function getUint32ArrayMemory0() {
714
+ if (cachedUint32ArrayMemory0 === null || cachedUint32ArrayMemory0.byteLength === 0) {
715
+ cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer);
716
+ }
717
+ return cachedUint32ArrayMemory0;
718
+ }
627
719
  var cachedUint8ArrayMemory0 = null;
628
720
  function getUint8ArrayMemory0() {
629
721
  if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
@@ -631,6 +723,24 @@ function getUint8ArrayMemory0() {
631
723
  }
632
724
  return cachedUint8ArrayMemory0;
633
725
  }
726
+ function passArray32ToWasm0(arg, malloc) {
727
+ const ptr = malloc(arg.length * 4, 4) >>> 0;
728
+ getUint32ArrayMemory0().set(arg, ptr / 4);
729
+ WASM_VECTOR_LEN = arg.length;
730
+ return ptr;
731
+ }
732
+ function passArray8ToWasm0(arg, malloc) {
733
+ const ptr = malloc(arg.length * 1, 1) >>> 0;
734
+ getUint8ArrayMemory0().set(arg, ptr / 1);
735
+ WASM_VECTOR_LEN = arg.length;
736
+ return ptr;
737
+ }
738
+ function passArrayF64ToWasm0(arg, malloc) {
739
+ const ptr = malloc(arg.length * 8, 8) >>> 0;
740
+ getFloat64ArrayMemory0().set(arg, ptr / 8);
741
+ WASM_VECTOR_LEN = arg.length;
742
+ return ptr;
743
+ }
634
744
  function passStringToWasm0(arg, malloc, realloc) {
635
745
  if (realloc === void 0) {
636
746
  const buf = cachedTextEncoder.encode(arg);
@@ -698,6 +808,9 @@ function __wbg_finalize_init(instance, module) {
698
808
  wasmInstance = instance;
699
809
  wasm = instance.exports;
700
810
  wasmModule = module;
811
+ cachedFloat32ArrayMemory0 = null;
812
+ cachedFloat64ArrayMemory0 = null;
813
+ cachedUint32ArrayMemory0 = null;
701
814
  cachedUint8ArrayMemory0 = null;
702
815
  wasm.__wbindgen_start();
703
816
  return wasm;
@@ -791,6 +904,7 @@ export {
791
904
  geometry_create_nurbs_surface_handle,
792
905
  geometry_create_path_swept_regions,
793
906
  geometry_create_revolved_regions,
907
+ geometry_create_sdf_mesh,
794
908
  geometry_create_side_profile_cut,
795
909
  geometry_create_sphere,
796
910
  geometry_create_surface_grid,
@@ -804,6 +918,7 @@ export {
804
918
  geometry_delete,
805
919
  geometry_delete_nurbs_curve,
806
920
  geometry_delete_nurbs_surface,
921
+ geometry_delete_sdf_mesh,
807
922
  geometry_export_step,
808
923
  geometry_export_step_multi,
809
924
  geometry_fillet_edges,
@@ -818,6 +933,10 @@ export {
818
933
  geometry_nurbs_surface_tessellation,
819
934
  geometry_sample_nurbs_curve,
820
935
  geometry_sample_nurbs_surface,
936
+ geometry_sdf_mesh_metadata,
937
+ geometry_sdf_mesh_tri_verts,
938
+ geometry_sdf_mesh_vert_properties,
939
+ geometry_sdf_mesher_abi_version,
821
940
  geometry_select_edges,
822
941
  geometry_topology,
823
942
  geometry_transform,
Binary file
@@ -39,16 +39,16 @@ Author or modify ForgeCAD models, sketches, assemblies, and CLI workflows. Prefe
39
39
  ### Validation and export commands (ask the user to run these)
40
40
 
41
41
  ```bash
42
- forgecad run <file.forge.js> # geometry diagnostics, verify.* results — run after every edit
43
- forgecad run <file.forge.js> --debug-imports # trace import-chain failures
44
- forgecad check print <file.forge.js> --json # collisions, mesh health, walls, overhangs, bed contact
45
- forgecad render 3d <file.forge.js> # shaded PNG render
46
- forgecad render wireframe <file.forge.js> # edges only — internal geometry, edge flow
47
- forgecad render section <file.forge.js> --plane XZ # 2D cross-section (SVG/PNG)
48
- forgecad capture gif <file.forge.js> # animated orbit or joint-playback GIF
49
- forgecad export stl <file.forge.js> # mesh for 3D printing
50
- forgecad export 3mf <file.forge.js> # mesh + metadata for 3D printing
51
- forgecad export step <file.forge.js> # exact B-rep for CAD interchange
42
+ forgecad run <file.forge.js> [file ...] # geometry diagnostics, verify.* results — run after every edit
43
+ forgecad run <file.forge.js> --debug-imports # trace import-chain failures
44
+ forgecad check print <file.forge.js> [file ...] --json # collisions, mesh health, walls, overhangs, bed contact
45
+ forgecad render 3d <file.forge.js> [file ...] # shaded PNG render
46
+ forgecad render wireframe <file.forge.js> [file ...] # edges only — internal geometry, edge flow
47
+ forgecad render section <file.forge.js> [file ...] --plane XZ # 2D cross-section (SVG/PNG)
48
+ forgecad capture gif <file.forge.js> [file ...] # animated orbit or joint-playback GIF
49
+ forgecad export stl <file.forge.js> [file ...] # mesh for 3D printing
50
+ forgecad export 3mf <file.forge.js> [file ...] # mesh + metadata for 3D printing
51
+ forgecad export step <file.forge.js> [file ...] # exact B-rep for CAD interchange
52
52
  ```
53
53
 
54
54
  Add `--param Name=Value` to test a specific parameter value. Pick the export that matches the goal: STL/3MF for printing, STEP for exact CAD interchange.
@@ -89,7 +89,7 @@ A script must return one of three shapes:
89
89
  ];
90
90
  ```
91
91
 
92
- 3. **A metadata object** — a plain object whose renderable values are rendered and whose non-renderable values (numbers, hole tables, builder functions) are silently skipped at render but flow to importers via `require()`.
92
+ 3. **A metadata object** — a plain object whose renderable values are rendered and whose non-renderable values (numbers, hole tables, builder functions) are silently skipped at render but flow to importers via `require()`. Each key becomes a named group, so don't pile independent parts into one array key (`{ parts: [a, b, c] }`) — the integrity gate reads that as a single fragmented part. Give each part its own key (`{ collar12, collar16, plug }`) or use named descriptors (form 2).
93
93
 
94
94
  Return an unsolved `Assembly` directly — ForgeCAD solves it at default joint values for display. Use `assembly.solve(state)` for a specific pose. Never call `.toGroup()` just to make an assembly render; use it only when you need `ShapeGroup` composition or named-child lookup.
95
95
 
@@ -114,7 +114,7 @@ Resolve labels with `.face(name)` or `.face(query)` — see the Shape class docs
114
114
 
115
115
  **No explanatory text inside CAD geometry.** Model the physical artifact; explain the design through names, comments, BOM entries, and docs. Use `text2d()` only when letters are part of the real object (engraving, branding, gauge ticks); use `Viewport.label()` only for temporary review/debug annotation — never to compensate for unclear geometry.
116
116
 
117
- **SDF shapes preview natively** when returned directly; call `.toShape()` only when mesh-backed CAD/export behavior is needed. See [SDF docs](../../generated/sdf.md).
117
+ **SDF shapes preview natively** when returned directly — including plain object/array trees of SDF leaves; call `.toShape()` only when mesh-backed CAD/export behavior is needed. See [SDF docs](../../generated/sdf.md).
118
118
 
119
119
  ---
120
120
 
@@ -143,10 +143,10 @@ Point2D, Points, polygon, polygonVertices, port, Product, ProductPanelBuilder, P
143
143
  ProductSkin, ProductSkinBuilder, ProductStationBuilder, ProductSurfaceBuilder, ProductSurfaceRef, projectToPlane, queueMicrotask, rect
144
144
  Rectangle2D, robotExport, roundedRect, Route3D, scene, Sculpt, sdf, SdfShape
145
145
  selectEdge, selectEdges, self, setActiveBackend, setImmediate, setInterval, setTimeout, Shape
146
- ShapeGroup, sheetMetal, SheetMetalPart, sheetStock, Sketch, sketchToDxf, sketchToSvg, slot
147
- SolvedAssembly, spec, sphere, spline2d, stroke, Surface, SurfaceBody, SurfaceMembers
148
- sweep, text2d, textWidth, torus, toShape, Transform, union, union2d
149
- variableSweep, verify, Viewport, window, Wood
146
+ ShapeGroup, sheetMetal, SheetMetalPart, sheetStock, Sim, Sketch, sketchToDxf, sketchToSvg
147
+ slot, SolvedAssembly, spec, sphere, spline2d, stroke, Surface, SurfaceBody
148
+ SurfaceMembers, sweep, text2d, textWidth, torus, toShape, Transform, union
149
+ union2d, variableSweep, verify, Viewport, window, Wood
150
150
  ```
151
151
 
152
152
  `showLabels` is also a runtime global, but it is not part of the top-level collision check. Avoid reusing it unless you intentionally want a local value with that name.
@@ -263,6 +263,8 @@ The `edges` parameter is flexible:
263
263
 
264
264
  Throws if no edges match the selection, or if `radius` is not a positive finite number.
265
265
 
266
+ Selectorless (all-edges) calls draw from a per-run broad edge-feature budget. Exceeding it throws — except in live preview, which skips the finish with a warning for responsiveness. Explicit edge selectors are never budgeted; `FORGECAD_BROAD_EDGE_FEATURE_BUDGET` / `FORGECAD_ALLOW_BROAD_EDGE_FEATURES=1` raise or lift the budget.
267
+
266
268
  ```ts
267
269
  // Fillet all edges
268
270
  fillet(myShape, 2)
@@ -285,6 +287,8 @@ fillet(base, 5, base.edge('vert-br'))
285
287
 
286
288
  Produces a 45° bevel at the specified `size` (distance from edge). Edge selections compile into backend operations; unsupported selections fail as explicit kernel gaps instead of using TypeScript geometry fallbacks.
287
289
 
290
+ Selectorless (all-edges) calls draw from a per-run broad edge-feature budget. Exceeding it throws — except in live preview, which skips the finish with a warning for responsiveness. Explicit edge selectors are never budgeted; `FORGECAD_BROAD_EDGE_FEATURE_BUDGET` / `FORGECAD_ALLOW_BROAD_EDGE_FEATURES=1` raise or lift the budget.
291
+
288
292
  The `edges` parameter accepts the same options as `fillet()`: inline `EdgeQuery`, pre-selected `EdgeSegment`/`EdgeSegment[]`, a tracked `EdgeRef` from `shape.edge('vert-br')` (exact compiler-owned path), or `undefined` (all sharp edges).
289
293
 
290
294
  ```ts
@@ -303,7 +307,7 @@ chamfer(base, 3, base.edge('vert-br'))
303
307
 
304
308
  Adds a taper angle to the vertical faces of a solid so that it can be extracted from a mold. The neutral plane is the Z position where the draft angle is zero — faces above and below are tapered symmetrically. Typical values for injection molding are 1–5°.
305
309
 
306
- Truck supports vertical-prism solids with Z-axis pull directions. OCCT uses its native draft operation when available. Manifold throws.
310
+ SDF, Manifold, and Truck lower supported vertical-prism solids with Z-axis pull directions to a tapered loft. OCCT uses its native draft operation when available.
307
311
 
308
312
  ```ts
309
313
  // Add 3° draft to a box for injection molding
@@ -1558,18 +1562,21 @@ Namespaced file-format import helpers — the single vocabulary for bringing ext
1558
1562
 
1559
1563
  - `dxfSketch(fileName: string, options?: DxfImportOptions): Sketch` — Parse a DXF file and return closed 2D profile geometry as a Sketch. The result can be extruded directly.
1560
1564
  - `svgSketch(fileName: string, options?: SvgImportOptions): Sketch` — Parse an SVG file and return it as a Sketch with options for region filtering, scaling, and simplification.
1561
- - `mesh(fileName: string, options?: { scale?: number; center?: boolean; object?: string; separateObjects?: boolean; }): Shape | ShapeGroup` — Import an external mesh file (STL, OBJ, 3MF).
1565
+ - `mesh(fileName: string, options?: MeshImportOptions): Shape | ShapeGroup` — Import an external mesh file (STL, OBJ, 3MF).
1562
1566
 
1563
1567
  By default, 3MF build items are flattened into one Shape for compatibility. Use `separateObjects: true` to import 3MF build items/resource objects as a named ShapeGroup whose children are targetable by `forgecad ls`. Use `object` to import one item by the stable ref/name reported by `forgecad run`.
1564
1568
 
1565
1569
  For 3MF sources, `forgecad run` prints a source-structure table with one line per build item: `[3mf:build:NNN:object:N] name type=... verts=... tris=... bbox=[min] → [max]`. Build items are numbered from `001`; files with no build items list resource objects as `3mf:object:N` instead. Per-item bboxes reveal multi-part structure — account for every substantial item before flattening. Pass any listed stable ref or name as `object` to import that item alone.
1566
1570
 
1571
+ Use `sourceFrame: { up: "+Y" }` when the file was authored in a non-Z-up coordinate system. ForgeCAD remains Z-up; the import is rotated so the named source axis becomes ForgeCAD +Z. Supported values: `"+X"`, `"-X"`, `"+Y"`, `"-Y"`, `"+Z"`, `"-Z"`.
1572
+
1567
1573
  ```js
1568
1574
  const all = Import.mesh("./assembly.3mf", { separateObjects: true });
1569
1575
  const pin = all.child("Pin #001");
1570
1576
  const plate = Import.mesh("./assembly.3mf", { object: "3mf:build:001:object:7" });
1577
+ const yUpPart = Import.mesh("./part.obj", { sourceFrame: { up: "+Y" } });
1571
1578
  ```
1572
- - `step(fileName: string): Shape` — Import a STEP file (.step, .stp) as an exact OCCT-backed Shape. Preserves NURBS curves, B-spline surfaces, and exact topology. Requires running with the OCCT backend.
1579
+ - `step(fileName: string, options?: StepImportOptions): Shape` — Import a STEP file (.step, .stp) as an exact OCCT-backed Shape. Preserves NURBS curves, B-spline surfaces, and exact topology. Requires running with the OCCT backend. Use `sourceFrame: { up: "+Y" }` to rotate Y-up source files into ForgeCAD's Z-up world.
1573
1580
 
1574
1581
  ---
1575
1582
 
@@ -3868,7 +3875,7 @@ const arm = bottle.path()
3868
3875
 
3869
3876
  # Assembly API
3870
3877
 
3871
- Assembly-owned links, constraints, connectors, solved poses, and robot export.
3878
+ Assembly-owned links, constraints, connectors, solved poses, and source-level simulation metadata.
3872
3879
 
3873
3880
  ## Contents
3874
3881
 
@@ -3881,6 +3888,59 @@ Assembly-owned links, constraints, connectors, solved poses, and robot export.
3881
3888
 
3882
3889
  ### Assembly & Joints
3883
3890
 
3891
+ #### `Sim.material(name: string, options?: SimMaterialOptions): SimMaterialDef` — Create a named physical material with density and contact coefficients for simulation export and checks.
3892
+
3893
+ `SimMaterialOptions`: `{ densityKgM3?: number, staticFriction?: number, dynamicFriction?: number, restitution?: number }`
3894
+
3895
+ `SimMaterialDef`: `{ kind: "material", name: string }`
3896
+
3897
+ #### `Sim.body(options: SimBodyOptions): SimBodyDef` — Describe one assembly part as a physical body with mass/density, material, collider intent, and optional contact surfaces.
3898
+
3899
+ **`SimBodyOptions`**: `massKg?: number`, `densityKgM3?: number`, `material?: SimMaterialDef`, `collider?: SimColliderDef`, `contacts?: Record<string, SimContactDef>`
3900
+
3901
+ `SimColliderDef`: `{ kind: "collider", mode: SimColliderMode, reason?: string }`
3902
+
3903
+ `SimContactDef`: `{ kind: "wheelSurface" | "gripperSurface", connectorName: string }`
3904
+
3905
+ `SimBodyDef`: `{ kind: "body" }`
3906
+
3907
+ #### `Sim.collider` — Collision-geometry intent constructors for physical parts.
3908
+
3909
+ - `Sim.collider.convexHull(): SimColliderDef` — Use a generated collision mesh for the part. This is the default fast rigid-body collider for irregular parts.
3910
+ - `Sim.collider.boundingBox(): SimColliderDef` — Use the part bounding box as the collision geometry. This is fastest and works well for chassis and simple blocks.
3911
+ - `Sim.collider.visualMesh(): SimColliderDef` — Use the visual mesh as collision geometry. This is exact but usually slower in physics engines.
3912
+ - `Sim.collider.none(reason: string): SimColliderDef` — Disable collision for a part with an explicit reason, such as a sensor-only or decorative object.
3913
+
3914
+ #### `Sim.drive` — Joint-drive intent constructors for passive or powered assembly joints.
3915
+
3916
+ - `Sim.drive.passive(options?: SimPassiveDriveOptions): SimDriveDef` — Mark a joint as passive while preserving damping and friction metadata for simulation export.
3917
+ - `Sim.drive.velocity(options: SimVelocityDriveOptions): SimDriveDef` — Mark a revolute joint as velocity-driven with torque and speed limits. Speed is authored in rpm and exported as deg/s or rad/s as needed.
3918
+
3919
+ `SimPassiveDriveOptions`: `{ damping?: number, friction?: number }`
3920
+
3921
+ `SimVelocityDriveOptions`: `{ maxTorqueNm: number, maxSpeedRpm: number }`
3922
+
3923
+ #### `Sim.contact` — Contact-surface metadata over existing part connectors.
3924
+
3925
+ - `Sim.contact.wheelSurface(connectorName: string): SimContactDef` — Mark a connector as the wheel tread contact surface for offline checks and downstream simulation metadata.
3926
+ - `Sim.contact.gripperSurface(connectorName: string): SimContactDef` — Mark a connector as a gripper pad/contact surface for offline checks and downstream grasp-readiness metadata.
3927
+
3928
+ #### `Sim.profile` — Named validation/export profile constructors.
3929
+
3930
+ - `Sim.profile.robotBodyRunnable(): SimProfileDef` — SimReady-style profile for a robot body that should be runnable in a physics simulator.
3931
+ - `Sim.profile.robotBodyIsaac(): SimProfileDef` — SimReady-style profile for robot bodies targeting Isaac Sim readiness.
3932
+ - `Sim.profile.roboticsAssetPhysx(): SimProfileDef` — SimReady-style profile for robotics assets with PhysX-ready rigid bodies and colliders.
3933
+
3934
+ `SimProfileDef`: `{ kind: "profile", name: SimProfileName }`
3935
+
3936
+ #### `Sim.controller` — Standard controller metadata constructors for simulator package generation.
3937
+
3938
+ - `Sim.controller.diffDrive(options: SimDiffDriveControllerOptions): SimDiffDriveControllerDef` — Describe a differential-drive controller from left/right wheel joints and wheel dimensions.
3939
+
3940
+ **`SimDiffDriveControllerOptions`**: `leftJoints: string[]`, `rightJoints: string[]`, `wheelSeparationMm: number`, `wheelRadiusMm: number`, `topic?: string`, `odomTopic?: string`, `tfTopic?: string`, `frameId?: string`, `odomFrameId?: string`, `maxLinearVelocity?: number`, `maxAngularVelocity?: number`, `linearAcceleration?: number`, `angularAcceleration?: number`
3941
+
3942
+ `SimDiffDriveControllerDef`: `{ kind: "diffDrive" }`
3943
+
3884
3944
  #### `assembly(name?: string): Assembly` — Create an assembly container with named parts, connectors, and kinematic links.
3885
3945
 
3886
3946
  **Use this from iteration 1 for any model with moving parts.** Do not build one static pose and retrofit motion later.
@@ -4014,7 +4074,7 @@ const housing = group(
4014
4074
  assembly.addPart("Base Assembly", housing);
4015
4075
  ```
4016
4076
 
4017
- **`PartOptions`**: `transform?: TransformInput`, `metadata?: PartMetadata`, `mate?: AssemblyPartMateInput | AssemblyPartMateInput[]`, `bindToFrame?: string`
4077
+ **`PartOptions`**: `transform?: TransformInput`, `metadata?: PartMetadata`, `sim?: SimBodyDef`, `mate?: AssemblyPartMateInput | AssemblyPartMateInput[]`, `bindToFrame?: string`
4018
4078
 
4019
4079
  **`PartMetadata`**
4020
4080
 
@@ -4075,7 +4135,9 @@ Frame semantics: `origin` is the pivot/contact point, `axis` the hinge or slide
4075
4135
 
4076
4136
  **Face-to-face:** each connector's axis points outward from its part; mating makes the axes anti-parallel, like a plug meeting a socket (same convention as `matchTo()`).
4077
4137
 
4078
- **Mirrored revolute axes:** revolute values follow the right-hand rule, so a mirrored hinge axis (`[1, 0, 0]` vs `[-1, 0, 0]`) rotates oppositely for the same `+theta`: negate the mirrored side's value and mirror limits as `[min, max] -> [-max, -min]`. Prismatic joints have no handedness flip. Use an explicit per-side sign mapping (or side-neutral link controls) for bilateral mechanisms.
4138
+ **Revolute sign:** a positive joint value follows the right-hand rule about the **child** connector's placed axis. Because face-to-face mating makes the axes anti-parallel, that is the *left*-hand rule about the parent connector's outward axis if `+30` swings the opposite way you expected, you predicted from the parent's axis. `forgecad debug assembly` prints each joint's resolved world axis.
4139
+
4140
+ **Mirrored revolute axes:** because of the right-hand rule, a mirrored hinge axis (`[1, 0, 0]` vs `[-1, 0, 0]`) rotates oppositely for the same `+theta`: negate the mirrored side's value and mirror limits as `[min, max] -> [-max, -min]`. Prismatic joints have no handedness flip. Use an explicit per-side sign mapping (or side-neutral link controls) for bilateral mechanisms.
4079
4141
 
4080
4142
  Joint type defaults to the connector's `kind`. For `start`/`end` connectors, `align` / `parentAlign` / `childAlign` (`'start' | 'middle' | 'end'`) choose which point meets.
4081
4143
 
@@ -4102,7 +4164,7 @@ assembly("Door").addPart("Frame", frame).addPart("Door", door)
4102
4164
  | `align?` | `PortAlign` | Shorthand: set both parentAlign and childAlign at once. |
4103
4165
  | `follows?` | `JointFollowOptions` | Slave this joint to another joint: `value = ratio × source + offset` (e.g. a mirrored jaw with `ratio: -1`). |
4104
4166
 
4105
- Also: `as?: string`, `type?: JointType`, `default?: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`.
4167
+ Also: `as?: string`, `type?: JointType`, `default?: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`, `drive?: SimDriveDef`.
4106
4168
 
4107
4169
  **`JointFollowOptions`**
4108
4170
  - `joint: string` — Name of the source joint that drives this one.
@@ -4162,6 +4224,12 @@ return mech.solve({ theta: 45 });
4162
4224
 
4163
4225
  **Other**
4164
4226
 
4227
+ #### `withSimulation(options: SimAssemblySimulationOptions): Assembly` — Attach the root simulation contract for this assembly.
4228
+
4229
+ Use this after adding physical parts and joints. Robot-body profiles require `rootPart`; asset profiles can describe one-part or multi-part physical assets. URDF/SDF exporters and `forgecad check simready` read this contract directly, so model files no longer need a separate `robotExport(...)` side effect.
4230
+
4231
+ `SimAssemblySimulationOptions`: `{ profile: SimProfileDef, rootPart?: string, controllers?: SimControllerDef[] }`
4232
+
4165
4233
  #### `edgeBetweenFrames(a: string, b: string, options?: AssemblyFrameEdgeOptions): Assembly` — Add a visual skeleton edge between two rig frame origins.
4166
4234
 
4167
4235
  Frame edges follow the solved frame poses produced by `fixedJoint()`, `revoluteJoint()`, and `prismaticJoint()`. They do not add constraints, degrees of freedom, parts, or geometry; use them to make a frame-only rig readable in the Motion/rig inspection overlay.
@@ -4297,7 +4365,7 @@ The sub-assembly must have exactly one root part before it can be merged (collap
4297
4365
  | `connectorRefs?` | `JointConnectorRefs` | Connector refs that define this joint contract. Usually set by `connect()` / `match()`. |
4298
4366
  | `follows?` | `JointFollowOptions` | Slave this joint to another joint: `value = ratio × source + offset`. Use for mechanisms with one physical DOF expressed through several joints — a mirrored gripper jaw (`ratio: -1`), a gear pair, a drive crank turning with its servo. A followed joint stops being an independent control: the Motion view drives it from its source, `solve()` derives its value (a direct state override is ignored with a warning), and limits still clamp the derived value. |
4299
4367
 
4300
- Also: `frame?: TransformInput`, `origin?: Vec3`, `axis?: Vec3`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`.
4368
+ Also: `frame?: TransformInput`, `origin?: Vec3`, `axis?: Vec3`, `min?: number`, `max?: number`, `default?: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`, `drive?: SimDriveDef`.
4301
4369
 
4302
4370
  `JointConnectorRefs`: `{ parent: string, child: string, parentAlign?: PortAlign, childAlign?: PortAlign }`
4303
4371
 
@@ -4369,7 +4437,7 @@ console.log("Collisions", solved.collisionReport());
4369
4437
 
4370
4438
  #### `minClearance(partA: string, partB: string, searchLength?: number): number` — Compute the minimum gap (clearance) between two parts in this solved pose.
4371
4439
 
4372
- Returns `0` if the parts are touching or overlapping. Requires the Manifold backend. `searchLength` bounds the search radius in mm — increase it for widely separated parts.
4440
+ Returns `0` if the parts are touching or overlapping. Manifold-backed parts use the exact Manifold gap query. SDF-backed parts use a mesh-derived sampled gap. `searchLength` bounds the Manifold search radius in mm — increase it for widely separated Manifold parts.
4373
4441
 
4374
4442
  ---
4375
4443
 
@@ -4430,12 +4498,14 @@ bom(tubeLen, "rectangular steel tube", {
4430
4498
  | `notes?` | `string` | Free-form notes |
4431
4499
  | `grain?` | `string` | Wood grain direction, e.g. "long", "cross" |
4432
4500
 
4433
- #### `robotExport(options: RobotExportOptions): CollectedRobotExport` — Declare that this script should export the assembly as a SDF/URDF robot package.
4501
+ #### `robotExport(options: RobotExportOptions): CollectedRobotExport` — Compatibility shim for SDF/URDF robot package metadata.
4434
4502
 
4435
- Call `robotExport()` alongside your assembly definition. `forgecad export sdf` / `forgecad export urdf` pick up the declaration (see the CLI docs for flags) and produce a robot package with:
4503
+ Prefer returning `assembly(...).withSimulation(...)` with `Sim.body(...)`, `Sim.drive.*(...)`, and `Sim.controller.*(...)` metadata. The CLI commands `forgecad export sdf` and `forgecad export urdf` now read that assembly simulation contract directly.
4504
+
4505
+ `robotExport()` remains available for one compatibility window. It converts the legacy descriptor into the same internal simulation model used by source-authored `Sim` metadata and produces a robot package with:
4436
4506
 
4437
4507
  - Mesh-based inertia tensors (full 6-component, not bounding-box approximations)
4438
- - Separate collision meshes (convex hull by default — ~50–80% smaller)
4508
+ - Separate collision meshes
4439
4509
  - Joint limits, effort/velocity/damping/friction metadata from assembly joints
4440
4510
 
4441
4511
  **Collision mesh modes** (set per-link via `links["PartName"].collision`):
@@ -4454,6 +4524,8 @@ Call `robotExport()` alongside your assembly definition. `forgecad export sdf` /
4454
4524
  - `massKg` is preferred; `densityKgM3` is used when mass is unknown.
4455
4525
  - Compatibility coupling metadata, when present, maps only the primary term (largest ratio) to `<mimic>` — SDF/URDF support single-leader mimic only. Dropped terms emit a warning.
4456
4526
 
4527
+ **Legacy example**
4528
+
4457
4529
  ```ts
4458
4530
  robotExport({
4459
4531
  assembly: rover, // assembly() with parts + revolute wheel joints
@@ -4469,6 +4541,13 @@ robotExport({
4469
4541
  });
4470
4542
  ```
4471
4543
 
4544
+ **Preferred CLI usage**
4545
+
4546
+ ```bash
4547
+ forgecad export sdf model.forge.js # SDF package (Gazebo/Ignition)
4548
+ forgecad export urdf model.forge.js # URDF package (ROS/PyBullet/MuJoCo)
4549
+ ```
4550
+
4472
4551
  **`RobotExportOptions`**: `assembly: Assembly`, `modelName?: string`, `state?: JointState`, `static?: boolean`, `selfCollide?: boolean`, `allowAutoDisable?: boolean`, `links?: Record<string, RobotLinkExportOptions>`, `joints?: Record<string, RobotJointExportOptions>`, `plugins?: { diffDrive?: RobotDiffDrivePluginOptions; jointStatePublisher?: RobotJointStatePublisherOptions; }`, `world?: RobotWorldOptions`
4473
4552
 
4474
4553
  `RobotLinkExportOptions`: `{ massKg?: number, densityKgM3?: number, collision?: "visual" | "convex" | "box" | "none" }`
@@ -4483,46 +4562,6 @@ robotExport({
4483
4562
 
4484
4563
  `RobotWorldKeyboardTeleopOptions`: `{ enabled?: boolean, linearStep?: number, angularStep?: number }`
4485
4564
 
4486
- **`CollectedRobotExport`**: `modelName: string`, `assembly: AssemblyDefinition`, `state: JointState`, `static: boolean`, `selfCollide: boolean`, `allowAutoDisable: boolean`, `links: Record<string, RobotLinkExportOptions>`, `joints: Record<string, RobotJointExportOptions>`, `plugins: { diffDrive?: RobotDiffDrivePluginOptions; jointStatePublisher?: RobotJointStatePublisherOptions; }`, `world: RobotWorldOptions | null`
4487
-
4488
- **`AssemblyDefinition`**: `name: string`, `parts: AssemblyPartDef[]`, `joints: AssemblyJointDef[]`, `jointCouplings: AssemblyJointCouplingDef[]`, `kinematics: AssemblyKinematicGraphDef`, `frames: AssemblyFrameDef[]`, `frameJoints: AssemblyFrameJointDef[]`, `frameEdges: AssemblyFrameEdgeDef[]`
4489
-
4490
- **`AssemblyPartDef`**: `name: string`, `part: AssemblyPart`, `base: Transform`, `metadata?: PartMetadata`, `mates: AssemblyPartMateInput[]`, `bindToFrame?: string`
4491
-
4492
- `PartMetadata` — defined in [assembly](/docs/assembly).
4493
-
4494
- `AssemblyPartMateInput` — defined in [assembly](/docs/assembly).
4495
-
4496
- **`AssemblyJointDef`**: `name: string`, `type: JointType`, `parent: string`, `child: string`, `frame: Transform`, `axis: Vec3`, `min?: number`, `max?: number`, `defaultValue: number`, `unit?: string`, `effort?: number`, `velocity?: number`, `damping?: number`, `friction?: number`, `connectorRefs?: JointConnectorRefs`
4497
-
4498
- `JointConnectorRefs` — defined in [assembly](/docs/assembly).
4499
-
4500
- `AssemblyJointCouplingDef`: `{ joint: string, terms: JointCouplingTermRecord[], offset: number }`
4501
-
4502
- `JointCouplingTermRecord`: `{ joint: string, ratio: number }`
4503
-
4504
- **`AssemblyKinematicGraphDef`**: `links: AssemblyLinkDef[]`, `edges: AssemblyEdgeBetweenLinksDef[]`, `angles: AssemblyAngleBetweenLinksDef[]`, `derivedLinks: AssemblyDerivedLinkDef[]`
4505
-
4506
- `AssemblyLinkDef`: `{ name: string, at: Vec3, fixed: boolean, metadata?: Record<string, unknown> }`
4507
-
4508
- **`AssemblyEdgeBetweenLinksDef`**: `name: string`, `a: string`, `b: string`, `length: number | null`, `min?: number`, `max?: number`, `visualOnly: boolean`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
4509
-
4510
- `AssemblyKinematicControlOptions` — defined in [assembly](/docs/assembly).
4511
-
4512
- **`AssemblyAngleBetweenLinksDef`**: `name: string`, `a?: string`, `b: string`, `c: string`, `reference?: AssemblyAngleReferenceDef`, `target?: number`, `min?: number`, `max?: number`, `control?: AssemblyKinematicControlOptions`, `metadata?: Record<string, unknown>`
4513
-
4514
- `AssemblyAngleReferenceDef`: `{ kind: "worldDirection", direction: Vec3 }`
4515
-
4516
- **`AssemblyDerivedLinkDef`**
4517
- - `distance: number` — Signed: positive moves from `fromLink` toward `towardLink`, negative moves away.
4518
- - Also: `name: string`, `fromLink: string`, `towardLink: string`.
4519
-
4520
- `AssemblyFrameDef`: `{ name: string, origin: Vec3, axis: Vec3, up: Vec3, fixed: boolean, metadata?: Record<string, unknown> }`
4521
-
4522
- **`AssemblyFrameJointDef`**: `name: string`, `type: AssemblyFrameJointType`, `parent: string`, `child: string`, `rest: Transform`, `min?: number`, `max?: number`, `defaultValue: number`, `unit?: string`, `control: boolean`, `metadata?: Record<string, unknown>`
4523
-
4524
- `AssemblyFrameEdgeDef`: `{ name: string, a: string, b: string, metadata?: Record<string, unknown> }`
4525
-
4526
4565
  #### `dim()` — Add a dimension annotation between two points, or along an entity.
4527
4566
 
4528
4567
  Overloads:
@@ -23,7 +23,7 @@ A script must return one of three shapes:
23
23
  ];
24
24
  ```
25
25
 
26
- 3. **A metadata object** — a plain object whose renderable values are rendered and whose non-renderable values (numbers, hole tables, builder functions) are silently skipped at render but flow to importers via `require()`.
26
+ 3. **A metadata object** — a plain object whose renderable values are rendered and whose non-renderable values (numbers, hole tables, builder functions) are silently skipped at render but flow to importers via `require()`. Each key becomes a named group, so don't pile independent parts into one array key (`{ parts: [a, b, c] }`) — the integrity gate reads that as a single fragmented part. Give each part its own key (`{ collar12, collar16, plug }`) or use named descriptors (form 2).
27
27
 
28
28
  Return an unsolved `Assembly` directly — ForgeCAD solves it at default joint values for display. Use `assembly.solve(state)` for a specific pose. Never call `.toGroup()` just to make an assembly render; use it only when you need `ShapeGroup` composition or named-child lookup.
29
29
 
@@ -48,4 +48,4 @@ Resolve labels with `.face(name)` or `.face(query)` — see the Shape class docs
48
48
 
49
49
  **No explanatory text inside CAD geometry.** Model the physical artifact; explain the design through names, comments, BOM entries, and docs. Use `text2d()` only when letters are part of the real object (engraving, branding, gauge ticks); use `Viewport.label()` only for temporary review/debug annotation — never to compensate for unclear geometry.
50
50
 
51
- **SDF shapes preview natively** when returned directly; call `.toShape()` only when mesh-backed CAD/export behavior is needed. See [SDF docs](../../generated/sdf.md).
51
+ **SDF shapes preview natively** when returned directly — including plain object/array trees of SDF leaves; call `.toShape()` only when mesh-backed CAD/export behavior is needed. See [SDF docs](../../generated/sdf.md).