forgecad 0.9.4 → 0.9.5

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 (82) hide show
  1. package/dist/assets/{AdminPage-jwoEgwE_.js → AdminPage-uTtcSXtn.js} +1 -1
  2. package/dist/assets/{BlogPage-Ck7g3ue2.js → BlogPage-DYJMjWx3.js} +1 -1
  3. package/dist/assets/{DocsPage-9WaRC14b.js → DocsPage-C58f0K5v.js} +1 -6
  4. package/dist/assets/{EditorApp-Dja2jMmW.js → EditorApp-DNH1TEz1.js} +282 -62
  5. package/dist/assets/{EmbedViewer-37_PfMwv.js → EmbedViewer-CMXWA2LX.js} +2 -2
  6. package/dist/assets/{LandingPageProofDriven-CO8WL0CY.js → LandingPageProofDriven-CAu2OZFn.js} +1 -1
  7. package/dist/assets/{PricingPage-DADKGuOa.js → PricingPage-BIgW7m3X.js} +1 -1
  8. package/dist/assets/{SettingsPage-DKKI4W49.js → SettingsPage-N1l1tMXO.js} +1 -1
  9. package/dist/assets/{app-CwI02pTA.js → app-CFy7g5WP.js} +74 -12
  10. package/dist/assets/cli/{render-Kw5hLEcL.js → render-BrVVdj_T.js} +453 -41
  11. package/dist/assets/{evalWorker-D6ub3kfS.js → evalWorker-c_SB9gg3.js} +2057 -446
  12. package/dist/assets/{manifold-lru0jwVw.js → manifold-CRoBhJKH.js} +2 -2
  13. package/dist/assets/{manifold-CwDdMKyc.js → manifold-Cjk7WhRs.js} +1 -1
  14. package/dist/assets/{manifold-DTvmxSDf.js → manifold-Dp6pvFr6.js} +1 -1
  15. package/dist/assets/{renderSceneState-tvtNKNRi.js → renderSceneState-3DfsSASX.js} +1 -1
  16. package/dist/assets/{reportWorker-DeqktDGt.js → reportWorker-BLkuIoS8.js} +2052 -443
  17. package/dist/assets/{sectionPlaneMath-C8N0w8o3.js → sectionPlaneMath-CykEnkvQ.js} +2258 -518
  18. package/dist/cli/render.html +1 -1
  19. package/dist/docs/index.html +2 -2
  20. package/dist/docs-raw/AI/usage.md +0 -1
  21. package/dist/docs-raw/API/core/concepts.md +11 -1
  22. package/dist/docs-raw/CLI.md +64 -13
  23. package/dist/docs-raw/generated/assembly.md +8 -3
  24. package/dist/docs-raw/generated/concepts.md +44 -41
  25. package/dist/docs-raw/generated/core.md +97 -47
  26. package/dist/docs-raw/generated/curves.md +6 -580
  27. package/dist/docs-raw/generated/lib.md +40 -3
  28. package/dist/docs-raw/generated/output.md +6 -1
  29. package/dist/docs-raw/generated/sdf.md +50 -4
  30. package/dist/docs-raw/generated/viewport.md +1 -9
  31. package/dist/docs-raw/guides/inspection-bundles.md +31 -6
  32. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -0
  33. package/dist/docs-raw/skills/forgecad-image-replicator.md +3 -1
  34. package/dist/docs-raw/skills/forgecad-make-a-model.md +48 -4
  35. package/dist/docs-raw/skills/forgecad-render-inspect.md +3 -1
  36. package/dist/docs-raw/skills/forgecad-visual-spec.md +2 -0
  37. package/dist/docs-raw/skills/forgecad.md +2 -1
  38. package/dist/docs-raw/skills/index.md +0 -1
  39. package/dist/index.html +1 -1
  40. package/dist/sitemap.xml +6 -6
  41. package/dist-cli/blender/render.py +43 -8
  42. package/dist-cli/forgecad.js +4941 -1758
  43. package/dist-cli/forgecad.js.map +1 -1
  44. package/dist-skill/CONTEXT.md +255 -656
  45. package/dist-skill/SKILL-dev.md +2 -1
  46. package/dist-skill/SKILL.md +2 -1
  47. package/dist-skill/docs/API/core/concepts.md +11 -1
  48. package/dist-skill/docs/CLI.md +64 -13
  49. package/dist-skill/docs/generated/assembly.md +8 -3
  50. package/dist-skill/docs/generated/core.md +97 -47
  51. package/dist-skill/docs/generated/curves.md +6 -580
  52. package/dist-skill/docs/generated/lib.md +40 -3
  53. package/dist-skill/docs/generated/output.md +6 -1
  54. package/dist-skill/docs/generated/sdf.md +50 -4
  55. package/dist-skill/docs/generated/viewport.md +1 -9
  56. package/dist-skill/docs/guides/inspection-bundles.md +31 -6
  57. package/dist-skill/docs-dev/API/core/concepts.md +11 -1
  58. package/dist-skill/docs-dev/CLI.md +64 -13
  59. package/dist-skill/docs-dev/generated/assembly.md +8 -3
  60. package/dist-skill/docs-dev/generated/core.md +97 -47
  61. package/dist-skill/docs-dev/generated/curves.md +6 -580
  62. package/dist-skill/docs-dev/generated/lib.md +40 -3
  63. package/dist-skill/docs-dev/generated/output.md +6 -1
  64. package/dist-skill/docs-dev/generated/sdf.md +50 -4
  65. package/dist-skill/docs-dev/generated/viewport.md +1 -9
  66. package/dist-skill/docs-dev/guides/inspection-bundles.md +31 -6
  67. package/dist-skill/library/README.md +0 -1
  68. package/dist-skill/library/forgecad-blockout-model/SKILL.md +1 -0
  69. package/dist-skill/library/forgecad-image-replicator/SKILL.md +3 -1
  70. package/dist-skill/library/forgecad-make-a-model/SKILL.md +48 -4
  71. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  72. package/dist-skill/library/forgecad-visual-spec/SKILL.md +2 -0
  73. package/examples/api/drive-wheel-regions.forge.js +43 -0
  74. package/examples/api/sdf-circular-array-knurling.forge.js +19 -0
  75. package/examples/api/sdf-pattern2d-ceramic-ripple-set.forge.js +83 -0
  76. package/examples/api/sdf-pattern2d-grip-tread.forge.js +72 -0
  77. package/examples/api/sdf-pattern2d-orbital-jewelry.forge.js +62 -0
  78. package/examples/api/sdf-surface-basket-weave.forge.js +67 -0
  79. package/examples/api/sector-gear-body.forge.js +34 -0
  80. package/package.json +1 -1
  81. package/dist/docs-raw/skills/forgecad-api-dogfood.md +0 -130
  82. package/dist-skill/library/forgecad-api-dogfood/SKILL.md +0 -125
@@ -10,6 +10,7 @@ Pre-built fasteners, gears, pipes, structural profiles, and utility shapes. Acce
10
10
  ## Contents
11
11
 
12
12
  - [TangentLoop2D](#tangentloop2d)
13
+ - [DriveWheelBuilder](#drivewheelbuilder)
13
14
  - [lib](#lib)
14
15
 
15
16
  ---
@@ -50,6 +51,32 @@ toProfile(): Sketch
50
51
  offsetBand(thickness: number): Sketch
51
52
  ```
52
53
 
54
+ ### `DriveWheelBuilder`
55
+
56
+ #### `addSpurTeethBetween()` — Add an involute spur-tooth window on part of the pitch circle.
57
+
58
+ ```ts
59
+ addSpurTeethBetween(options: DriveWheelSpurTeethRegionOptions): this
60
+ ```
61
+
62
+ #### `addSolidArcBetween()` — Add a constant-radius solid arc region such as a dwell, stop, or pusher.
63
+
64
+ ```ts
65
+ addSolidArcBetween(options: DriveWheelSolidArcRegionOptions): this
66
+ ```
67
+
68
+ #### `addShapeRegion()` — Add a fully custom region shape while preserving region metadata.
69
+
70
+ ```ts
71
+ addShapeRegion(name: string, shape: Shape, options?: DriveWheelShapeRegionOptions): this
72
+ ```
73
+
74
+ #### `build()` — Build the final wheel shape with a bore connector and region metadata.
75
+
76
+ ```ts
77
+ build(): Shape
78
+ ```
79
+
53
80
  ---
54
81
 
55
82
  ## Constants
@@ -58,7 +85,7 @@ offsetBand(thickness: number): Sketch
58
85
 
59
86
  Pre-built parametric parts available in user scripts as `lib.*`.
60
87
 
61
- Every key in this object becomes a method on the `lib` namespace exposed to `.forge.js` scripts. The catalog includes:
88
+ Every key in this object becomes a method or namespace on the `lib` object exposed to `.forge.js` scripts. The catalog includes:
62
89
 
63
90
  **Fasteners:** `bolt`, `nut`, `washer`, `fastenerSet`, `fastenerHole`, `boltHole`, `counterbore`, `hexNut`, `holePattern`
64
91
 
@@ -68,7 +95,9 @@ Every key in this object becomes a method on the `lib` namespace exposed to `.fo
68
95
 
69
96
  **Threads:** `thread`
70
97
 
71
- **Gears:** `spurGear`, `bevelGear`, `faceGear`, `sideGear`, `ringGear`, `rackGear`, `gearPair`, `bevelGearPair`, `faceGearPair`, `sideGearPair`
98
+ **Gears:** `spurGear`, `sectorGear`, `driveWheel`, `bevelGear`, `faceGear`, `sideGear`, `ringGear`, `rackGear`, `gearPair`, `bevelGearPair`, `faceGearPair`, `sideGearPair`
99
+
100
+ **Gear bodies:** `gearBodies.disk`, `gearBodies.diskWithHub`, `gearBodies.spoked`, `gearBodies.fromProfile` plus direct aliases `gearBodyDisk`, `gearBodyDiskWithHub`, `gearBodySpoked`, `gearBodyFromProfile`
72
101
 
73
102
  **Gear ratios (pure math helpers):** `gearRatio`, `rackRatio`, `planetaryRatio`
74
103
 
@@ -76,7 +105,7 @@ Every key in this object becomes a method on the `lib` namespace exposed to `.fo
76
105
 
77
106
  **Utilities:** `explode`
78
107
 
79
- Extend this by adding new entries here and registering the corresponding runner binding in `runner.ts`. Sizes outside the supported ranges will throw at runtime with a descriptive error.
108
+ Sizes outside the supported ranges will throw at runtime with a descriptive error.
80
109
 
81
110
  - `boltHole(diameter: number, depth: number): Shape` — Simple cylindrical through-hole cutter centered on Z=0. Subtract the result from a part to produce a plain cylindrical clearance hole. For ISO metric sizes with fit classes and counterbore/countersink, use {
82
111
  - `fastenerHole(opts: FastenerHoleOptions): Shape` — ISO metric fastener hole cutter with optional counterbore or countersink. **Details** Returns a cutter shape (subtract from a solid to produce the hole). Sizes outside M2–M10 will throw. Extend `METRIC_HOLE_TABLE` in this file to add new sizes. **Example** ```ts const plate = box(60, 40, 8) .subtract(lib.fastenerHole({ size: 'M5', fit: 'normal', depth: 8 }) .translate(15, 10, 4)); ```
@@ -114,3 +143,11 @@ Extend this by adding new entries here and registering the corresponding runner
114
143
  - `rackRatio(module: number, pinionTeeth: number): number` — Coupling ratio between a pinion and a rack. When the pinion rotates by `θ` degrees, the rack slides by `θ × (π × module × teeth / 360)` mm. Equivalently, 1mm of rack travel = `180 / (π × pitchRadius)` degrees of pinion rotation.
115
144
  - `planetaryRatio(sunTeeth: number, ringTeeth: number): number` — Planetary gear reduction ratio when the ring is held fixed. Input: sun. Output: carrier. Ratio: `1 + ringTeeth / sunTeeth`. One turn of the sun produces `1 / ratio` turns of the carrier.
116
145
  - `boltPattern(options: BoltPatternOptions): BoltPattern` — Define a bolt pattern once and cut it from multiple parts. const base = bolts.cut(box(60, 50, 10), 12, { from: -1 }); const cover = bolts.cut(box(60, 50, 3), 5, { from: -1 }); // Same positions in both parts — guaranteed aligned. ```
146
+ - `driveWheel(options?: DriveWheelOptions): DriveWheelBuilder` — Start a composable exceptional gear or drive wheel.
147
+ - `readDriveWheelMeta(shape: Shape): DriveWheelMeta | null` — Read the functional-region metadata attached by `driveWheel().build()`.
148
+ - `sectorGear(options: SectorGearOptions): Shape` — Involute sector gear with teeth on only part of the pitch circle. Specify the full-circle pitch as `teethOnFullCircle`, then choose the active tooth window with `firstTooth` and `toothCount`. The body is separate from the tooth region: pass a `gearBody...` shape for spokes, hubs, and product styling, or omit it for a simple root-radius disk. **Example** ```ts const body = lib.gearBodies.spoked({ outerRadius: 22, rimWidth: 3, hubDiameter: 10, spokeCount: 5, spokeWidth: 2.5, faceWidth: 8, boreDiameter: 5, }); const sector = lib.sectorGear({ module: 1.25, teethOnFullCircle: 36, toothCount: 10, faceWidth: 8, body, }); ```
149
+ - `gearBodies: { ... }` — Gear body preset namespace: disk, diskWithHub, spoked, and fromProfile.
150
+ - `gearBodyDisk(options: GearBodyDiskOptions): Shape` — Solid disk/ring gear body, independent from any tooth geometry.
151
+ - `gearBodyDiskWithHub(options: GearBodyDiskWithHubOptions): Shape` — Disk gear body with a raised center hub.
152
+ - `gearBodySpoked(options: GearBodySpokedOptions): Shape` — Spoked gear body with an outer rim, center hub, and radial spokes.
153
+ - `gearBodyFromProfile(profile: Sketch, options: GearBodyFromProfileOptions): Shape` — Extrude a custom 2D profile into a gear body.
@@ -144,7 +144,12 @@ robotExport(options: RobotExportOptions): CollectedRobotExport
144
144
 
145
145
  `AssemblyPartDef`: `{ name: string, part: AssemblyPart, base: Transform, metadata?: PartMetadata }`
146
146
 
147
- **`PartMetadata`**: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`
147
+ **`PartMetadata`**
148
+
149
+ | Option | Type | Description |
150
+ |--------|------|-------------|
151
+ | `tags?` | `string \| readonly string[]` | Viewport organization tags applied to scene objects produced from this part. |
152
+ | `material?`, `process?`, `tolerance?`, `qty?`, `notes?`, `densityKgM3?`, `massKg?` | | — |
148
153
 
149
154
  **`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`
150
155
 
@@ -191,6 +191,36 @@ intersect(...others: SdfShape[]): SdfShape
191
191
  clipBox(x: number, y: number, z: number): SdfShape
192
192
  ```
193
193
 
194
+ #### `fillWith()` — Keep only the material where this shape overlaps another SDF pattern.
195
+
196
+ ```ts
197
+ fillWith(pattern: SdfShape): SdfShape
198
+ ```
199
+
200
+ #### `fillWithGyroid()` — Keep only the gyroid lattice inside this shape.
201
+
202
+ ```ts
203
+ fillWithGyroid(options: TpmsOptions): SdfShape
204
+ ```
205
+
206
+ #### `fillWithSchwarzP()` — Keep only the Schwarz-P lattice inside this shape.
207
+
208
+ ```ts
209
+ fillWithSchwarzP(options: TpmsOptions): SdfShape
210
+ ```
211
+
212
+ #### `fillWithDiamond()` — Keep only the diamond TPMS lattice inside this shape.
213
+
214
+ ```ts
215
+ fillWithDiamond(options: TpmsOptions): SdfShape
216
+ ```
217
+
218
+ #### `fillWithLidinoid()` — Keep only the lidinoid TPMS lattice inside this shape.
219
+
220
+ ```ts
221
+ fillWithLidinoid(options: TpmsOptions): SdfShape
222
+ ```
223
+
194
224
  #### `smoothUnion()` — Smooth union — blends shapes together with a smooth radius.
195
225
 
196
226
  ```ts
@@ -269,6 +299,14 @@ bend(radius: number): SdfShape
269
299
  repeat(spacing: Vec3, count?: Vec3): SdfShape
270
300
  ```
271
301
 
302
+ #### `circularArray()` — Arrange this SDF in a circular array around the Z axis.
303
+
304
+ The source shape is translated by `offset` in +X before arraying. This uses angular domain folding, so evaluation stays O(1): the source SDF is sampled twice no matter how many copies are requested.
305
+
306
+ ```ts
307
+ circularArray(count: number, offset?: number): SdfShape
308
+ ```
309
+
272
310
  #### `shell()` — Hollow out, keeping only a shell of given thickness.
273
311
 
274
312
  ```ts
@@ -281,8 +319,8 @@ shell(thickness: number): SdfShape
281
319
  // Function displacement
282
320
  shape.displace((x, y, z) => Math.sin(x) * 0.5)
283
321
 
284
- // Pattern displacement (e.g. basketWeave)
285
- shape.displace(sdf.basketWeave({ threads: 16, spacing: 3 }))
322
+ // Pattern displacement from a 3D SDF field
323
+ shape.displace(sdf.knurl({ pitch: 2, depth: 0.3 }))
286
324
  ```
287
325
 
288
326
  ```ts
@@ -295,10 +333,16 @@ Automatically detects the shape's UV parametrization (sphere, cylinder, torus) f
295
333
 
296
334
  UV coordinates are in **surface millimeters** — patterns defined with `spacing: 3` always produce 3mm spacing, regardless of shape size.
297
335
 
336
+ Prefer `sdf.pattern2d()` or built-in surface patterns when the relief should stay on the native shader and meshing path. Callback functions are supported for experimentation, but they are opaque to the typed pattern optimizer.
337
+
298
338
  ```js
299
- // Surface-following basket weave — auto-detects sphere UV
339
+ // Native typed pattern — auto-detects sphere UV
340
+ const p = sdf.pattern2d()
341
+ const ribs = p.stripes({ spacing: 3, width: 0.8, depth: 0.35 })
342
+ .add(p.sineWave({ direction: [0, 1], wavelength: 14, amplitude: 0.08 }))
343
+
300
344
  sdf.sphere(27).shell(3)
301
- .surfaceDisplace(sdf.basketWeave({ spacing: 3, depth: 0.8 }))
345
+ .surfaceDisplace(ribs)
302
346
  .toShape()
303
347
 
304
348
  // Custom 2D pattern via function
@@ -370,9 +414,11 @@ return {
370
414
  - `brick(options?: BrickOptions): SdfShape` — Brick/stone wall pattern — running bond with mortar grooves.
371
415
  - `weave(options?: WeaveOptions): SdfShape` — Grid lattice pattern — two families of infinite slabs crossing at 90°.
372
416
  - `basketWeave(options?: BasketWeaveOptions): SurfacePattern` — Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`.
417
+ - `pattern2d(): Pattern2DBuilder` — Create typed, composable 2D surface patterns for `.surfaceDisplace()`.
373
418
  - `twist(shape: SdfShape, degreesPerUnit: number): SdfShape` — Twist an SDF shape around the Z axis.
374
419
  - `bend(shape: SdfShape, radius: number): SdfShape` — Bend an SDF shape around the Z axis.
375
420
  - `repeat(shape: SdfShape, spacing: Vec3, count?: Vec3): SdfShape` — Repeat an SDF shape in space.
421
+ - `circularArray(shape: SdfShape, count: number, offset?: number): SdfShape` — Arrange an SDF shape in a circular array around the Z axis with O(1) folded-domain evaluation.
376
422
  - `SurfacePattern: typeof SurfacePattern` — A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`.
377
423
  - `fromFunction(fn: SdfFunctionSource, options: SdfFunctionOptions): SdfShape` — Create a custom SDF from one expression; shader-safe expressions raymarch directly.
378
424
  - `Sculpt: { sphere: (radius: number) => SdfShape; box: (x: number, y: number, z: number, options?: SculptBoxOptions) => SdfShape; cylinder: (height: number, radius: number) => SdfShape; disk: (radius: number, thickness?: number) => SdfShape; circle: (radius: number, thickness?: number) => SdfShape; capsule: (height: number, radius: number) => SdfShape; torus: (majorRadius: number, minorRadius: number) => SdfShape; cone: (height: number, radius: number) => SdfShape; tube: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape; curve: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape; path: (points: SculptPointList, options?: SculptTubeOptions) => SdfShape; blend: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape; union: (first?: SculptBlendInput, ...rest: SculptBlendInput[]) => SdfShape; carve: (base: SdfShape, cutters: SculptBlendInput, options?: SculptBlendOptions) => SdfShape; keep: (first?: SculptBlendInput | SculptBlendOptions, optionsOrShape?: SculptBlendInput | SculptBlendOptions, ...rest: (SculptBlendInput | SculptBlendOptions)[]) => SdfShape; polish: (shape: SdfShape, input?: SculptPolishInput) => SdfShape; material: (input?: SculptPolishInput) => ShapeMaterialProps & { color?: string; }; look: (preset?: SculptLookPreset) => SceneOptions; knownMaterials: typeof knownSculptMaterialPresets; }` — Sculpt-like facade: friendly liquid-modeling verbs backed by the same SDF kernel.
@@ -59,7 +59,7 @@ When `lights` is specified, **all** default lights are removed. You must include
59
59
 
60
60
  Setting `camera.position` overrides auto-framing — the viewport will no longer auto-fit the geometry on script reload.
61
61
 
62
- Named render views let scripts check in repeatable cameras next to the model code. The canonical shape is `{ camera: { position, target } }`, and a direct camera shorthand `{ position, target }` is also accepted. Use the canonical shape when you may add view metadata later. Use it from the CLI with `forgecad render 3d model.forge.js --view hero`.
62
+ Named render views let scripts check in repeatable cameras next to the model code. The canonical shape is `{ camera: { position, target } }`, and a direct camera shorthand `{ position, target }` is also accepted. Use the canonical shape when you may add view metadata later. Use it from the CLI with `--view hero` on `forgecad render 3d`, `forgecad render hq`, or `forgecad capture`.
63
63
 
64
64
  Model journeys let scripts check in a compact guided path through named objects. Each journey has ordered `steps`; each step can name a `focus` target by object name/tree path, provide a caption, and optionally provide an explicit camera. In the viewer, journeys are opt-in: they appear as a small Explore control and do not move the camera until the user starts them. Use `forgecad run model.forge.js --journeys` or `--journeys-json` to inspect resolved targets.
65
65
 
@@ -128,7 +128,6 @@ scene(options: SceneOptions): void
128
128
  | `behavior?` | `"opt-in" \| "auto"` | Whether the viewer should offer or auto-open the journey. First slice supports opt-in. |
129
129
  | `steps` | `SceneJourneyStepConfig[]` | Ordered journey spine. Branches can be added later without changing this core contract. |
130
130
  | `valid?` | `boolean` | True unless any journey or step diagnostic has level "error". |
131
- | `diagnostics?` | `SceneJourneyDiagnostic[]` | Whole-journey diagnostics, including unresolved startsAt and step target diagnostics. |
132
131
 
133
132
  **`SceneJourneyStepConfig`**
134
133
 
@@ -141,9 +140,6 @@ scene(options: SceneOptions): void
141
140
  | `camera?` | `SceneViewCameraConfig` | Optional explicit camera for this step. When omitted, the viewer fits `focus`. |
142
141
  | `resolvedFocusId?` | `string \| null` | Resolved object id after script execution, when `focus` matched exactly one object. |
143
142
  | `resolvedFocusPath?` | `string \| null` | Resolved object tree path or name after script execution. |
144
- | `diagnostics?` | `SceneJourneyDiagnostic[]` | Resolution diagnostics for this step. |
145
-
146
- `SceneJourneyDiagnostic`: `{ level: SceneJourneyDiagnosticLevel, message: string, stepId?: string, suggestions?: string[] }`
147
143
 
148
144
  **`SceneLightConfig`**
149
145
 
@@ -219,10 +215,6 @@ viewConfig({
219
215
  viewConfig(options?: ViewConfigOptions): void
220
216
  ```
221
217
 
222
- `ViewConfigOptions`: `{ jointOverlay?: JointOverlayViewConfigOptions }`
223
-
224
- **`JointOverlayViewConfigOptions`**: `enabled?: boolean`, `axisColor?: string`, `axisCoreColor?: string`, `arcColor?: string`, `zeroColor?: string`, `arcVisualLimitDeg?: number`, `axisLengthScale?: number`, `axisLengthMin?: number`, `axisLineRadiusScale?: number`, `axisLineRadiusMin?: number`, `axisLineRadiusMax?: number`, `spokeLineRadiusScale?: number`, `spokeLineRadiusMin?: number`, `spokeLineRadiusMax?: number`, `arcLineRadiusScale?: number`, `arcLineRadiusMin?: number`, `arcLineRadiusMax?: number`, `axisDotRadiusScale?: number`, `axisDotRadiusMin?: number`, `axisArrowRadiusScale?: number`, `axisArrowRadiusMin?: number`, `axisArrowLengthScale?: number`, `axisArrowLengthMin?: number`, `axisArrowOffsetFactor?: number`, `arcRadiusScale?: number`, `arcRadiusMin?: number`, `arcDotRadiusScale?: number`, `arcDotRadiusMin?: number`, `arcArrowRadiusScale?: number`, `arcArrowRadiusMin?: number`, `arcArrowLengthScale?: number`, `arcArrowLengthMin?: number`, `arcArrowOffsetFactor?: number`, `arcStepDeg?: number`, `arcMinSteps?: number`, `arcTubeSegmentsMin?: number`, `arcTubeSegmentsFactor?: number`, `arcTubeRadialSegments?: number`
225
-
226
218
  #### `explodeView()` — Configure how the viewport explode slider offsets returned objects.
227
219
 
228
220
  Offsets are resolved from the returned object tree, not a flat list. In `radial` mode each node follows its parent branch direction, then fans locally from the immediate parent center — nested assemblies peel apart level by level. In fixed-axis or fixed-vector modes, the branch follows that axis/vector but nested descendants fan out perpendicular by default.
@@ -7,8 +7,9 @@ skill-order: 2
7
7
 
8
8
  `forgecad render inspect` writes a deterministic directory bundle for agents,
9
9
  tests, and automation. Use it when a single shaded PNG is too ambiguous and the
10
- consumer needs geometry-aware signals such as depth, normals, part identity,
11
- physical connected components, collisions, local thickness, or cross-sections.
10
+ consumer needs geometry-aware signals such as depth, normals, surface roughness,
11
+ part identity, physical connected components, collisions, local thickness, or
12
+ cross-sections.
12
13
 
13
14
  ## When To Use It
14
15
 
@@ -28,14 +29,16 @@ forgecad render inspect model.forge.js --channels rgb,mask,section
28
29
  forgecad render inspect model.forge.js --channels collisions --focus Bench
29
30
  forgecad render inspect model.forge.js --channels rgb,mask --hide "Bench.Slat0,Bench.Slat1"
30
31
  forgecad render inspect model.forge.js --channels thickness --min-thickness 1.2 --warn-thickness 2.0
32
+ forgecad render inspect channels
31
33
  ```
32
34
 
33
35
  The default output directory is `<script-name>-inspect/` next to the input file.
34
36
  Pass `--force` to replace an existing bundle directory.
35
37
 
36
38
  There are no default channels. Pass `--channels` every time as a
37
- comma-separated subset. Keep bundles targeted to the current question so heavy
38
- analyses do not run unnecessarily.
39
+ comma-separated subset. Run `forgecad render inspect channels` to list the
40
+ supported channels in the installed CLI. Keep bundles targeted to the current
41
+ question so heavy analyses do not run unnecessarily.
39
42
 
40
43
  `--focus` and `--hide` use the same object-name filtering semantics as
41
44
  `forgecad run` and `forgecad render 3d`. A bare `--focus` hides mock objects;
@@ -91,12 +94,13 @@ implemented channel in one bundle:
91
94
 
92
95
  ```bash
93
96
  forgecad render inspect model.forge.js --channels depth,normals
97
+ forgecad render inspect model.forge.js --channels rgb,roughness
94
98
  forgecad render inspect model.forge.js --channels rgb,mask,collisions
95
99
  forgecad render inspect model.forge.js --channels rgb,section,thickness
96
100
  ```
97
101
 
98
- Supported channels are `rgb`, `depth`, `normals`, `mask`, `connectivity`,
99
- `distance`, `collisions`, `thickness`, and `section`.
102
+ Supported channels are `rgb`, `depth`, `normals`, `roughness`, `mask`,
103
+ `connectivity`, `distance`, `collisions`, `thickness`, and `section`.
100
104
 
101
105
  ## Channel Semantics
102
106
 
@@ -123,6 +127,27 @@ normal = normalize((rgb / 255) * 2 - 1)
123
127
 
124
128
  Background pixels are black and should be treated as `null`.
125
129
 
130
+ `roughness` emits a mesh-dihedral surface-quality heatmap. Smooth and gently
131
+ curved triangles render as a faint translucent shadow over black, while
132
+ triangles adjacent to sharp, harsh, boundary, or non-manifold mesh edges render
133
+ in orange or magenta:
134
+
135
+ ```text
136
+ shadow = max adjacent angle < sharpAngleDeg
137
+ orange = sharpAngleDeg <= angle < harshAngleDeg
138
+ magenta = angle >= harshAngleDeg, boundary, or non-manifold
139
+ ```
140
+
141
+ The default thresholds are `smoothAngleDeg=5`, `sharpAngleDeg=30`, and
142
+ `harshAngleDeg=90`. The manifest stores the method, thresholds, palette, object
143
+ list, per-object triangle and edge counts, area percentages by smooth,
144
+ moderate, sharp, and harsh classes, angle percentiles, maximum angle, quality
145
+ score, and warnings. Moderate angles are reported in the manifest but stay in
146
+ the shadow layer by default so intentionally curved surfaces do not light up as
147
+ defects. Use this channel to spot spiky tessellation, accidental faceting,
148
+ jagged boolean residue, and dense sharp-corner regions without losing the
149
+ silhouette of otherwise smooth surfaces.
150
+
126
151
  `mask` emits one object-color image per view. Black is background. Non-black
127
152
  pixels resolve through `manifest.channels.mask.objects`, which includes object
128
153
  index, RGB color, object id, name, group, tree path, and mock flag. Edge pixels
@@ -72,6 +72,7 @@ Use today's date for the directory. Use the user's current ForgeCAD project when
72
72
  - Use at most a handful of top-level `param()` values when comparing rough proportions. Do not parameterize every dimension.
73
73
  - Name uncertainty honestly: `armLengthGuess`, `baseWidthApprox`, `screenVolume`, `clearanceEnvelope`.
74
74
  - Do not add text labels, callout arrows, legends, coordinate labels, or explanatory plaques to the model. Use named return objects and a short written note outside the geometry.
75
+ - Do not cut away a shell, remove covers, or permanently explode parts just to show hidden intent. Even a blockout should represent the object in its normal assembled state unless the user explicitly asks for a presentation view.
75
76
  - Use transparent ghost geometry for:
76
77
  - sweep arcs
77
78
  - keep-out volumes
@@ -17,6 +17,8 @@ The reference image is evidence. It is not the deliverable.
17
17
 
18
18
  The deliverable is a real parametric object that remains believable from front, back, side, top, bottom, and reference camera views. A model that matches one image but falls apart from other angles has failed, even if the comparison board looks close.
19
19
 
20
+ If a reference image is cutaway, sectioned, exploded, partly hidden, or transparent, treat that as evidence about the complete object. Do not make the default ForgeCAD result a permanently cutaway or exploded display unless the user explicitly asked for a teaching/display model. Build the closed artifact first, then use ForgeCAD viewer/inspection tools to recreate explanatory views.
21
+
20
22
  ### Required Companion Skills
21
23
 
22
24
  - Use `forgecad` for API docs, model authoring, and renderer behavior.
@@ -85,7 +87,7 @@ Reference matching is a validation step after the object exists.
85
87
  When multiple images are attached, do not choose one as the target and ignore the rest. Assign each image a camera, evidence list, and confidence level. Optimize one shared geometry against the whole set. If an image is decorative, distorted, or contradictory, state how it was weighted.
86
88
 
87
89
  10. Inspect the final object.
88
- Run `forgecad run`, render the reference comparison boards, render canonical views, and use `forgecad render inspect` with relevant channels. For multi-part, mechanical, internal, or fit-sensitive models, include collisions and section views.
90
+ Run `forgecad run`, render the reference comparison boards, render canonical views, and use `forgecad render inspect` with relevant channels. For multi-part, mechanical, internal, or fit-sensitive models, include collisions and section-channel inspection, but keep the delivered model as the complete closed artifact.
89
91
 
90
92
  ### Renderer Camera Support
91
93
 
@@ -45,12 +45,14 @@ Use today's date for the directory. Use the user's current ForgeCAD project when
45
45
  - When there are multiple versions of the same object, expose the version as a choice parameter and render one selected version at a time
46
46
  - Use clear variable names
47
47
  - Build any implied internal structure as real geometry, even when it will be hidden in the final view
48
+ - Build the complete physical artifact first: closed shells, installed covers, real part positions, and all meaningful internal structure in place
48
49
  - Make final mating geometry physically plausible: parts may touch, clear each other, or be boolean-joined, but should not unintentionally pass through each other
49
50
  - Model the physical artifact, not an educational diagram: no explanatory arrows, floating labels, section labels, legends, or text plaques unless the user explicitly requested a presentation/teaching view
51
+ - Do not make the default returned model a cutaway, sectioned shell, permanently exploded assembly, or hidden-parts teaching view. ForgeCAD gives the user viewer and inspection tools for slicing, exploding, hiding, and looking inside after the real CAD exists.
50
52
  - Return the final geometry (single shape, array, or named objects array)
51
- - Avoid expensive global edge treatment on repeated decorative geometry: do not call `fillet(shape, r)` or `chamfer(shape, r)` on every edge of large unioned/repeated parts unless the render/run loop proves it is fast enough. Prefer simpler primitive profiles, lower segment counts, or targeted edge selectors.
53
+ - Treat `fillet(shape, r)` and `chamfer(shape, r)` as experimental edge treatments: Manifold can produce incorrect results and OCCT can be very slow. Prefer simpler primitive profiles, lower segment counts, targeted edge selectors, and inspection before relying on the result.
52
54
  4. Validate — run `forgecad run <file>` to check for errors. For multi-file projects, always validate `main.forge.js`.
53
- 5. Verify geometry — render the result and run `forgecad render inspect` with the relevant channels for the task (see Render-Verify Loop below). For multi-file projects, render and inspect `main.forge.js`.
55
+ 5. Verify geometry — render the result, run `forgecad run --connectivity` when the model has multiple returned objects or visible attachments, and run `forgecad render inspect` with the relevant channels for the task (see Final Acceptance Gate and Render-Verify Loop below). For multi-file projects, render and inspect `main.forge.js`.
54
56
 
55
57
  ### Manufacturing Process Is Not Assumed
56
58
 
@@ -103,7 +105,9 @@ Build hidden features as actual geometry:
103
105
  - Mechanism clearances, travel envelopes, stops, guides, rails, hinges, gear spaces, and service access
104
106
  - Process-specific features such as bends, tubes, sheet-metal flanges, machined bosses, cast ribs, molded draft, weld tabs, laser-cut slots, or print-oriented ribs where appropriate
105
107
 
106
- When internals are hidden by the final exterior, add a temporary verification view: transparent shell, exploded view, cutaway, underside render, or named ghost objects. Use that view to check fit and collisions, but keep the final returned model faithful to the real closed artifact unless the user asked for a visible cutaway.
108
+ Do not reveal hidden structure by permanently cutting away the production geometry. Keep the returned default model faithful to the real closed artifact, with covers installed and parts in their actual assembled positions.
109
+
110
+ When internals are hidden by the final exterior, verify them with exploration tools instead of changing the artifact: render underside or alternate camera views, use `render inspect` section channels, use viewer-only cut planes or explode controls, temporarily make a shell transparent, or add named ghost objects for fit checks. Those views are diagnostic/presentation modes; they must not replace the real model unless the user explicitly asked for a cutaway teaching model.
107
111
 
108
112
  ### Final Geometry Should Be Physically Plausible
109
113
 
@@ -115,6 +119,46 @@ Temporary collisions during construction are fine when they are part of how the
115
119
 
116
120
  Before delivery on any multi-part, internal, or mechanical model, run the collision inspection, read the collision channel PNGs, and check `manifest.json`. Fix unexpected overlaps. If a collision is intentional, document it in the model naming/comments or isolate that inspection with `--focus` / `--hide` so the remaining collision report proves the final assembly is real.
117
121
 
122
+ ### Final Acceptance Gate
123
+
124
+ Before telling the user the model is done, prove both technical validity and visual plausibility. A model can pass `forgecad run` and still be wrong because a rail, cable, trim line, handle, or fastener is merely hovering over a curved surface. Use this gate for any model with multiple bodies, surface-mounted details, cables/strings, rails/tracks, handles, product skins, visible hardware, or hidden mating geometry.
125
+
126
+ 1. State the intended physical component graph. Decide whether the final artifact should be one connected component, several intentionally separate components, or a selected assembly plus named ghosts. Then run:
127
+
128
+ ```bash
129
+ forgecad run model.forge.js --connectivity
130
+ ```
131
+
132
+ The reported component count must match the design intent. Treat unexpected islands, accidental fusion, or bbox-only "touching" that does not make physical sense as model bugs.
133
+
134
+ 2. Run a collision bundle and read both the manifest and images:
135
+
136
+ ```bash
137
+ forgecad render inspect model.forge.js /tmp/model-inspect --channels rgb,mask,collisions --force --size 700
138
+ jq '.channels.collisions | {collisionCount, collisions, warnings}' /tmp/model-inspect/manifest.json
139
+ ```
140
+
141
+ `collisionCount` should be zero unless an overlap is deliberately manufactured, fused, welded, overmolded, or isolated with `--focus` / `--hide`. Do not ignore the channel PNGs; visually inspect where the findings or warnings appear.
142
+
143
+ 3. Render risk-specific views, not only a hero shot. At minimum, render one hero view plus the orthographic views that expose the likely failure modes:
144
+
145
+ - long products, vehicles, weapons, rails, handles, and tools: side and top
146
+ - sockets, underside joins, stands, brackets, and handles: side plus underside; use section-channel inspection only when hidden geometry must be checked
147
+ - cables, strings, belts, tubes, and hoses: top plus side so endpoints and sag/clearance are visible
148
+ - surface details on curved ProductSkin bodies: grazing side view plus top/iso to confirm they conform or are embedded as intended
149
+
150
+ 4. Do a visual attachment audit. For every detail that should be connected, ask: "Where does this physically enter, seat, wrap, terminate, or fasten?" Check that view directly. Common failures to fix before delivery:
151
+
152
+ - a flat rail or arrow bed sitting on top of a curved shell instead of being recessed, saddled, socketed, or structurally blended into the body
153
+ - strings/cables that pass through space without terminal knots, hooks, holes, posts, ferrules, pulleys, or anchors
154
+ - decorative brass/trim lines floating above the body instead of following a ProductSkin surface or being built as inset/raised strips with believable thickness
155
+ - handles/grips touching only by a tangent or thin face instead of having a neck, bridge, socket, screws, or overmolded landing
156
+ - small hardware or gems that are bbox-connected but visually read as levitating; replace with flush/inset seats or explicit brackets
157
+
158
+ 5. Treat ProductSkin and surface-member limitations honestly. If `render inspect` reports boolean-test warnings because a sampled `Product.skin` loft has boundary edges, distinguish that from real collision findings. You may still deliver if `collisionCount` is clean, the intended connectivity is correct, and the visual attachment audit passes. Mention the residual warning briefly in the final response.
159
+
160
+ 6. Final response must name the evidence: commands run, component count, collision count, and any residual warnings or intentional exceptions. Do not just say "validated."
161
+
118
162
  ### Render-Verify Loop
119
163
 
120
164
  You are building blind unless you render. `forgecad run` only checks that code executes — it cannot tell you a hole is in the wrong place or a part doesn't fit. Render and look at the result.
@@ -236,7 +280,7 @@ Key primitives:
236
280
 
237
281
  - `box(x, y, z)`, `cylinder(h, r, rTop?, segments?)`, `sphere(r)`, `torus(R, r)`
238
282
  - `union()`, `difference()`, `intersection()`
239
- - `.fillet()`, `.chamfer()` for edge treatments
283
+ - `.fillet()`, `.chamfer()` for experimental edge treatments only
240
284
  - `param(name, default, opts)`, `boolParam(name, default)`
241
285
  - Return `[{ name, shape, color }]` for multi-part colored models
242
286
 
@@ -15,6 +15,8 @@ Use `forgecad render inspect` when a shaded viewport render is too ambiguous and
15
15
 
16
16
  This skill owns the inspection workflow: choosing channels, generating the bundle, reading the manifest, visually inspecting the relevant PNGs, and turning the findings into model fixes or a verification report.
17
17
 
18
+ Inspection is not a substitute artifact. Use section, mask, transparency, focus, and hide channels to look inside a real model; do not edit the model into a cutaway or exploded default just to make the inspection easier.
19
+
18
20
  ### Trigger Boundary
19
21
 
20
22
  Use this skill for:
@@ -148,7 +150,7 @@ Manifest fields to check first:
148
150
  - `channels.thickness.objects`: inspect `minThickness`, `p05Thickness`, critical/warning percentages, and unresolved area.
149
151
  - `channels.connectivity.componentCount`: compare to the expected number of physical components.
150
152
  - `channels.distance.maxRootDistance` and per-object `nearestGap`: check suspicious isolation or spacing.
151
- - `channels.section.planes`: look for missing slices, wrong path counts, or empty internal cuts.
153
+ - `channels.section.planes`: look for missing slices, wrong path counts, or empty internal cuts. These are inspection views, not instructions to section the returned production geometry.
152
154
 
153
155
  PNG review order:
154
156
 
@@ -37,6 +37,7 @@ The image should not:
37
37
  - smooth away the mechanism into a fake consumer shell
38
38
  - invent flashy sci-fi styling that hides how it works
39
39
  - pretend to be a CAD drawing, dimensioned blueprint, or engineering diagram
40
+ - turn the artifact into a cutaway, sectioned shell, or exploded teaching view unless the user explicitly asks for that representation
40
41
 
41
42
  ### Default Strategy
42
43
 
@@ -124,6 +125,7 @@ Bias harder toward interfaces, seams, mounted actuators, and subsystem boundarie
124
125
  Use when the user wants assembly logic or modular breakdown.
125
126
 
126
127
  Keep the explosion restrained. Separate only major modules, not every screw.
128
+ This is a visual-spec support image, not a default ForgeCAD modeling instruction; the CAD artifact should still be the complete assembled product.
127
129
 
128
130
  #### Workshop Prototype Realism
129
131
 
@@ -25,6 +25,7 @@ Author or modify ForgeCAD models, sketches, assemblies, and CLI workflows. Prefe
25
25
  8. Validate with `forgecad run <file>` (add `--debug-imports` for import chain issues, and pass `--backend manifold|occt|truck` when the backend matters).
26
26
  9. For `jointsView()` animations, keep wrapped revolute tracks continuous across branch cuts; do not assume the viewport will auto-fix `-180/180` jumps.
27
27
  10. Model the physical artifact, not an educational diagram. Do not add explanatory labels, arrows, legends, or text plaques unless the user explicitly asks for a presentation or teaching view. Product markings are allowed only when they would exist on the real object.
28
+ 11. Build the real closed CAD first. Do not bake cutaways, sectioned shells, permanently exploded layouts, or hidden-parts views into the default model just to show internals. Use viewer-only cut planes, `explodeView`, object hiding, transparency, or `render inspect` section channels after the artifact exists.
28
29
 
29
30
  #### Import and Composition
30
31
 
@@ -92,7 +93,7 @@ Parametric bolts, nuts, washers, standard hardware, gears, pipes, and structural
92
93
  - `docs/permanent/generated/lib.md`
93
94
  - `docs/permanent/generated/wood.md`
94
95
 
95
- #### 9. Runtime Viewport APIs (for cut planes, jointsView, and animation playback)
96
+ #### 9. Runtime Viewport APIs (for cut planes, exploded views, hiding, and animation playback)
96
97
 
97
98
  Viewer-only APIs such as cutPlane, explodeView, jointsView, and animation behavior.
98
99
 
@@ -9,7 +9,6 @@ forgecad skill install
9
9
  | Skill | Installed by | Purpose |
10
10
  | --- | --- | --- |
11
11
  | [forgecad](/docs/skills/forgecad) | `forgecad skill install` | ForgeCAD model authoring, editing, debugging, and execution guidance for .forge.js, SVG-import, assembly, and CLI workflows. Use when building or modifying ForgeCAD geometry, structuring multi-file projects, validating scripts, or using ForgeCAD export/render tooling. |
12
- | [forgecad-api-dogfood](/docs/skills/forgecad-api-dogfood) | `forgecad skill install` | Build a ForgeCAD model while actively hunting for API friction — missing helpers, awkward patterns, bad defaults, verbose boilerplate. Use when asked to dogfood, stress-test the API, or build a model with the goal of improving ForgeCAD. |
13
12
  | [forgecad-blockout-model](/docs/skills/forgecad-blockout-model) | `forgecad skill install` | Create rough high-level ForgeCAD concept models from simple primitives to explore layout, proportions, motion, and part relationships without production detail. Use when asked for a quick model sketch, blockout, spatial mockup, or intuitive low-detail 3D concept. |
14
13
  | [forgecad-component-model](/docs/skills/forgecad-component-model) | `forgecad skill install` | Enforce the ForgeCAD Component Model when building multi-part assemblies. Parts build at origin, connectors position them, data flows down from parent. Use when building or reviewing any multi-file ForgeCAD project. |
15
14
  | [forgecad-high-level-spec](/docs/skills/forgecad-high-level-spec) | `forgecad skill install` | Write a high-level design document (HLD) for a model, mechanism, or assembly before detailed specification or coding. Use when starting a new design, rethinking an existing one, or when the user asks to spec out, plan, or think through a model at a high level. Works backwards from requirements — defines the problem, explores alternatives, records decisions. Produces a right-sized design document for review and iteration. |
package/dist/index.html CHANGED
@@ -55,7 +55,7 @@
55
55
  * { margin: 0; padding: 0; box-sizing: border-box; }
56
56
  html, body, #root { width: 100%; min-height: 100%; background: var(--fc-bg); color: var(--fc-text); font-family: system-ui, -apple-system, sans-serif; }
57
57
  </style>
58
- <script type="module" crossorigin src="/assets/app-CwI02pTA.js"></script>
58
+ <script type="module" crossorigin src="/assets/app-CFy7g5WP.js"></script>
59
59
  <link rel="stylesheet" crossorigin href="/assets/app-CsHnaBWt.css">
60
60
  </head>
61
61
  <body>
package/dist/sitemap.xml CHANGED
@@ -2,37 +2,37 @@
2
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
3
  <url>
4
4
  <loc>https://forgecad.io/</loc>
5
- <lastmod>2026-05-10</lastmod>
5
+ <lastmod>2026-05-16</lastmod>
6
6
  <changefreq>weekly</changefreq>
7
7
  <priority>1.0</priority>
8
8
  </url>
9
9
  <url>
10
10
  <loc>https://forgecad.io/docs</loc>
11
- <lastmod>2026-05-10</lastmod>
11
+ <lastmod>2026-05-16</lastmod>
12
12
  <changefreq>weekly</changefreq>
13
13
  <priority>0.8</priority>
14
14
  </url>
15
15
  <url>
16
16
  <loc>https://forgecad.io/blog</loc>
17
- <lastmod>2026-05-10</lastmod>
17
+ <lastmod>2026-05-16</lastmod>
18
18
  <changefreq>weekly</changefreq>
19
19
  <priority>0.7</priority>
20
20
  </url>
21
21
  <url>
22
22
  <loc>https://forgecad.io/pricing</loc>
23
- <lastmod>2026-05-10</lastmod>
23
+ <lastmod>2026-05-16</lastmod>
24
24
  <changefreq>monthly</changefreq>
25
25
  <priority>0.6</priority>
26
26
  </url>
27
27
  <url>
28
28
  <loc>https://forgecad.io/examples</loc>
29
- <lastmod>2026-05-10</lastmod>
29
+ <lastmod>2026-05-16</lastmod>
30
30
  <changefreq>weekly</changefreq>
31
31
  <priority>0.6</priority>
32
32
  </url>
33
33
  <url>
34
34
  <loc>https://forgecad.io/blog/hello-forgecad-io</loc>
35
- <lastmod>2026-05-10</lastmod>
35
+ <lastmod>2026-05-16</lastmod>
36
36
  <changefreq>monthly</changefreq>
37
37
  <priority>0.5</priority>
38
38
  </url>
@@ -28,7 +28,7 @@ import sys
28
28
  import json
29
29
  import math
30
30
  import os
31
- from mathutils import Vector
31
+ from mathutils import Matrix, Vector
32
32
 
33
33
 
34
34
  # ---------------------------------------------------------------------------
@@ -414,18 +414,26 @@ def setup_camera(config, objects):
414
414
 
415
415
  cam_config = config.get('camera') or {}
416
416
 
417
- # Set FOV
418
- fov = cam_config.get('fov', 45)
419
- cam_data.lens_unit = 'FOV'
420
- cam_data.angle = math.radians(fov)
417
+ projection_mode = cam_config.get('projectionMode') or cam_config.get('type') or 'perspective'
418
+ if projection_mode == 'orthographic':
419
+ cam_data.type = 'ORTHO'
420
+ if cam_config.get('orthoScale'):
421
+ cam_data.ortho_scale = float(cam_config['orthoScale'])
422
+ elif cam_config.get('orthoZoom'):
423
+ cam_data.ortho_scale = 960.0 / float(cam_config['orthoZoom'])
424
+ else:
425
+ cam_data.ortho_scale = 960.0
426
+ else:
427
+ cam_data.type = 'PERSP'
428
+ fov = cam_config.get('fov', 45)
429
+ cam_data.lens_unit = 'FOV'
430
+ cam_data.angle = math.radians(fov)
421
431
 
422
432
  if cam_config.get('position') and cam_config.get('target'):
423
433
  pos = Vector(cam_config['position'])
424
434
  target = Vector(cam_config['target'])
425
435
  cam_obj.location = pos
426
- direction = target - pos
427
- rot_quat = direction.to_track_quat('-Z', 'Y')
428
- cam_obj.rotation_euler = rot_quat.to_euler()
436
+ set_camera_orientation(cam_obj, pos, target, cam_config.get('up'))
429
437
  else:
430
438
  # Auto-frame: compute bounding box of all mesh objects
431
439
  auto_frame_camera(cam_obj, objects)
@@ -433,6 +441,33 @@ def setup_camera(config, objects):
433
441
  return cam_obj
434
442
 
435
443
 
444
+ def set_camera_orientation(cam_obj, pos, target, up_values=None):
445
+ direction = target - pos
446
+ if direction.length <= 1e-8:
447
+ return
448
+
449
+ if up_values:
450
+ forward = direction.normalized()
451
+ up = Vector(up_values)
452
+ if up.length > 1e-8:
453
+ up.normalize()
454
+ right = up.cross(forward)
455
+ if right.length > 1e-8:
456
+ right.normalize()
457
+ true_up = forward.cross(right)
458
+ true_up.normalize()
459
+ rotation = Matrix((
460
+ (right.x, true_up.x, -forward.x),
461
+ (right.y, true_up.y, -forward.y),
462
+ (right.z, true_up.z, -forward.z),
463
+ ))
464
+ cam_obj.rotation_euler = rotation.to_euler()
465
+ return
466
+
467
+ rot_quat = direction.to_track_quat('-Z', 'Y')
468
+ cam_obj.rotation_euler = rot_quat.to_euler()
469
+
470
+
436
471
  def auto_frame_camera(cam_obj, objects):
437
472
  """Position camera to frame all objects nicely — iso view."""
438
473
  if not objects: