forgecad 0.9.14 → 0.9.15
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.
- package/LICENSE +6 -4
- package/README.md +8 -4
- package/dist/assets/{AdminPage-eWGs2K6H.js → AdminPage-CDyGUinA.js} +2 -2
- package/dist/assets/{BenchmarkPage-CTrLKfpo.js → BenchmarkPage-DfPMY_-d.js} +4 -15
- package/dist/assets/{BlogPage-5nPesyds.js → BlogPage-kF0fkdJT.js} +2 -2
- package/dist/assets/{DocsPage-C4Y3nbYc.js → DocsPage-B954L3YN.js} +9 -3
- package/dist/assets/EditorApp-Beb-IZ0y.js +14014 -0
- package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-CuDLxKqL.css} +698 -0
- package/dist/assets/{EmbedViewer-C8fB4n5U.js → EmbedViewer-C77B-TrF.js} +3 -3
- package/dist/assets/{LandingPageProofDriven-jSz0LaMM.js → LandingPageProofDriven-Cr6fXMDj.js} +35 -37
- package/dist/assets/LegalPage-BRlScr9A.css +91 -0
- package/dist/assets/LegalPage-Dzklqmmg.js +39 -0
- package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
- package/dist/assets/{PricingPage-B83B90zh.js → PricingPage-zWXkvlwl.js} +19 -19
- package/dist/assets/{SettingsPage-DY889pcu.js → SettingsPage-Bz0of4KQ.js} +2 -2
- package/dist/assets/app-CE3sYcV7.css +3890 -0
- package/dist/assets/{app-bEww1ic4.js → app-D3kDkggg.js} +2293 -946
- package/dist/assets/cli/{render-Cho2uKG_.js → render-DSY3mMQa.js} +337 -7
- package/dist/assets/{constructionHistoryWorker-HYwzJY4m.js → constructionHistoryWorker-gpDo-uH2.js} +927 -243
- package/dist/assets/{evalWorker-CjQwJSE-.js → evalWorker-CU0Ke6DP.js} +7800 -4164
- package/dist/assets/{forgecad_geometry-CH2nvuLA.js → forgecad_geometry-Dgceylq9.js} +43 -1
- package/dist/assets/forgecad_geometry_bg-dD4RNQF1.wasm +0 -0
- package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-COyp8XXA.js} +927 -243
- package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
- package/dist/assets/landing-proof-driven-DiGqdtWa.js +18 -0
- package/dist/assets/{landing-proof-driven-oFYW6mjz.css → landing-proof-driven-ORyigZ6p.css} +13 -7
- package/dist/assets/legalContent-ZfFGMmi4.js +251 -0
- package/dist/assets/{manifold-CG9Fokx-.js → manifold-BRI5prcH.js} +1 -1
- package/dist/assets/{manifold-uRzgk5O8.js → manifold-C-3h2M7p.js} +2 -2
- package/dist/assets/{manifold-rmfAcdwF.js → manifold-DNkrUWpA.js} +1 -1
- package/dist/assets/{reportWorker-4cW_ZpoS.js → reportWorker-CdBz5bNg.js} +7538 -10857
- package/dist/assets/{scalar-sampling-budget-CfDiFvh7.js → scalar-sampling-budget-wJF98aY9.js} +6935 -4331
- package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-B-9VbLIs.js} +32 -1
- package/dist/assets/{solver-DuJAO8S6.js → solver-BZ9LPTHs.js} +1 -1
- package/dist/assets/solver_bg-DAHZJ_rw.wasm +0 -0
- package/dist/assets/{targets-D6PWsv6X.js → targets-B9sGB5nB.js} +1 -1
- package/dist/assets/{vendor-react-Da3A2QmU.js → vendor-react-6j1Kke-Y.js} +6 -5
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/AI/ai-native-cad.md +50 -0
- package/dist/docs-raw/AI/usage.md +3 -12
- package/dist/docs-raw/CLI.md +30 -10
- package/dist/docs-raw/component-model.md +27 -11
- package/dist/docs-raw/generated/assembly.md +301 -212
- package/dist/docs-raw/generated/concepts.md +235 -237
- package/dist/docs-raw/generated/core.md +283 -6
- package/dist/docs-raw/generated/curves.md +274 -361
- package/dist/docs-raw/generated/lib.md +7 -1
- package/dist/docs-raw/generated/output.md +19 -4
- package/dist/docs-raw/generated/runtime-names.md +41 -0
- package/dist/docs-raw/generated/sdf.md +31 -0
- package/dist/docs-raw/generated/sheet-metal.md +9 -0
- package/dist/docs-raw/generated/sketch.md +44 -1
- package/dist/docs-raw/generated/viewport.md +11 -3
- package/dist/docs-raw/guides/coordinate-system.md +20 -16
- package/dist/docs-raw/guides/geometry-conventions.md +2 -2
- package/dist/docs-raw/guides/inspection-bundles.md +2 -1
- package/dist/docs-raw/guides/joint-design.md +24 -0
- package/dist/docs-raw/guides/positioning.md +13 -3
- package/dist/docs-raw/legal/privacy.md +63 -0
- package/dist/docs-raw/legal/software-license.md +55 -0
- package/dist/docs-raw/legal/terms.md +87 -0
- package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +1 -1
- package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
- package/dist/docs-raw/skills/forgecad-component-model.md +11 -2
- package/dist/docs-raw/skills/forgecad-high-level-spec.md +1 -1
- package/dist/docs-raw/skills/forgecad-image-replicator.md +8 -8
- package/dist/docs-raw/skills/forgecad-lld.md +1 -1
- package/dist/docs-raw/skills/forgecad-make-a-model.md +1 -1
- package/dist/docs-raw/skills/forgecad-model-grader.md +2 -2
- package/dist/docs-raw/skills/forgecad-prepare-prompt.md +2 -2
- package/dist/docs-raw/skills/forgecad-project.md +1 -1
- package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +1 -1
- package/dist/docs-raw/skills/forgecad-render-inspect.md +4 -2
- package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
- package/dist/docs-raw/skills/forgecad.md +4 -3
- package/dist/index.html +40 -12
- package/dist/llms.txt +8 -0
- package/dist/site.webmanifest +1 -1
- package/dist/sitemap.xml +49 -13
- package/dist-cli/{check-compiler-U5SOPN7X.js → check-compiler-SDX5QIXI.js} +1 -2
- package/dist-cli/{check-query-propagation-XOKNSSYU.js → check-query-propagation-EAYEFT77.js} +1 -2
- package/dist-cli/{chunk-EXWGNL6K.js → chunk-N4O47JLF.js} +12540 -9046
- package/dist-cli/forgecad.js +1786 -679
- package/dist-cli/{forgecad_geometry-GYVNKPIE.js → forgecad_geometry-QOQIIP53.js} +42 -1
- package/dist-cli/forgecad_geometry_bg.wasm +0 -0
- package/dist-cli/{solver-46FFSK2U.js → solver-OK4HECRH.js} +0 -1
- package/dist-cli/solver_bg.wasm +0 -0
- package/dist-skill/CONTEXT.md +1117 -721
- package/dist-skill/SKILL.md +3 -2
- package/dist-skill/docs/API/core/concepts.md +64 -1
- package/dist-skill/docs/CLI.md +30 -10
- package/dist-skill/docs/generated/assembly.md +277 -229
- package/dist-skill/docs/generated/core.md +283 -6
- package/dist-skill/docs/generated/curves.md +272 -362
- package/dist-skill/docs/generated/lib.md +7 -1
- package/dist-skill/docs/generated/output.md +19 -4
- package/dist-skill/docs/generated/runtime-names.md +41 -0
- package/dist-skill/docs/generated/sdf.md +31 -0
- package/dist-skill/docs/generated/sheet-metal.md +9 -0
- package/dist-skill/docs/generated/sketch.md +44 -2
- package/dist-skill/docs/generated/viewport.md +2 -87
- package/dist-skill/docs/guides/coordinate-system.md +20 -16
- package/dist-skill/docs/guides/geometry-conventions.md +2 -2
- package/dist-skill/docs/guides/inspection-bundles.md +2 -1
- package/dist-skill/docs/guides/joint-design.md +24 -0
- package/dist-skill/docs/guides/positioning.md +13 -3
- package/dist-skill/library/forgecad-component-model/SKILL.md +10 -1
- package/dist-skill/library/forgecad-image-replicator/SKILL.md +6 -6
- package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.py +166 -0
- package/dist-skill/library/forgecad-model-grader/SKILL.md +1 -1
- package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
- package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
- package/examples/api/assembly-kinematics-foundation.forge.js +65 -0
- package/examples/api/assembly-kinematics-four-bar.forge.js +115 -0
- package/examples/api/assembly-kinematics-limb.forge.js +116 -0
- package/examples/api/connector-frame-rig-chain.forge.js +102 -0
- package/examples/api/exact-sheet-shell-assembly.forge.js +0 -2
- package/examples/api/exact-surface-studio.forge.js +6 -8
- package/examples/api/helix-basics.forge.js +6 -6
- package/examples/api/lean-foundations/README.md +12 -0
- package/examples/api/lean-foundations/curve-blend-exact.forge.js +22 -0
- package/examples/api/lean-foundations/curve-fit-interpolation.forge.js +18 -0
- package/examples/api/lean-foundations/curve-helix-canonicalization.forge.js +27 -0
- package/examples/api/lean-foundations/curve-route-canonicalization.forge.js +16 -0
- package/examples/api/lean-foundations/curve-trim-reverse.forge.js +24 -0
- package/examples/api/lean-foundations/exact-curve-arc.forge.js +36 -0
- package/examples/api/mixed-edge-finishes-proof.forge.js +8 -11
- package/examples/api/route3d-elbow.forge.js +68 -0
- package/examples/api/transition-curves.forge.js +44 -15
- package/examples/api/y-blend-corner-showcase.forge.js +0 -2
- package/examples/generative/coral-vase.forge.js +1 -1
- package/examples/nurbs-tube.forge.js +1 -1
- package/package.json +14 -13
- package/dist/assets/EditorApp-lXv53A1m.js +0 -13610
- package/dist/assets/app-CsHnaBWt.css +0 -1789
- package/dist/assets/forgecad_geometry_bg-C5_E9Oa9.wasm +0 -0
- package/dist/assets/solver_bg-CWvv4lnN.wasm +0 -0
- package/dist/docs-raw/API/README.md +0 -16
- package/dist/docs-raw/API/core/concepts.md +0 -118
- package/dist/docs-raw/INDEX.md +0 -138
- package/dist/docs-raw/RELEASING.md +0 -87
- package/dist/docs-raw/agent-native-api.md +0 -27
- package/dist/docs-raw/beta-deployment.md +0 -304
- package/dist/docs-raw/beta-operations.md +0 -325
- package/dist/docs-raw/blueprint-first.md +0 -145
- package/dist/docs-raw/cli-monetization.md +0 -112
- package/dist/docs-raw/coding-best-practices.md +0 -120
- package/dist/docs-raw/coding.md +0 -340
- package/dist/docs-raw/deployment.md +0 -374
- package/dist/docs-raw/guides/skill-maintenance.md +0 -161
- package/dist/docs-raw/guides/surface-members.md +0 -82
- package/dist/docs-raw/harbor-cli.md +0 -854
- package/dist/docs-raw/internals/backend-vocabulary.md +0 -35
- package/dist/docs-raw/internals/compiler.md +0 -307
- package/dist/docs-raw/internals/constraint-solver-quality.md +0 -161
- package/dist/docs-raw/internals/constraint-solver.md +0 -176
- package/dist/docs-raw/internals/shape-from-slices.md +0 -152
- package/dist/docs-raw/internals/sketch-2d-pipeline.md +0 -108
- package/dist/docs-raw/platform/admin.md +0 -45
- package/dist/docs-raw/platform/architecture.md +0 -82
- package/dist/docs-raw/platform/auth.md +0 -139
- package/dist/docs-raw/platform/email.md +0 -67
- package/dist/docs-raw/platform/google-oauth-setup.md +0 -88
- package/dist/docs-raw/platform/observability.md +0 -197
- package/dist/docs-raw/platform/projects.md +0 -111
- package/dist/docs-raw/platform/sharing.md +0 -90
- package/dist/docs-raw/product/README.md +0 -39
- package/dist/docs-raw/product/api-as-product-language.md +0 -13
- package/dist/docs-raw/product/business-model.md +0 -15
- package/dist/docs-raw/product/competitive-positioning.md +0 -17
- package/dist/docs-raw/product/creative-manufacturing.md +0 -15
- package/dist/docs-raw/product/founder-story.md +0 -11
- package/dist/docs-raw/product/manufacturing-workflows.md +0 -15
- package/dist/docs-raw/product/onboarding-first-experience.md +0 -256
- package/dist/docs-raw/product/product-loop.md +0 -17
- package/dist/docs-raw/product/strategic-decisions.md +0 -22
- package/dist/docs-raw/product/user-outreach-email-templates.md +0 -161
- package/dist/docs-raw/product/user-segments.md +0 -15
- package/dist/docs-raw/product/vision.md +0 -26
- package/dist/docs-raw/rl-environments.md +0 -350
- package/dist/docs-raw/runbook.md +0 -611
- package/dist-cli/check-compiler-U5SOPN7X.js.map +0 -1
- package/dist-cli/check-query-propagation-XOKNSSYU.js.map +0 -1
- package/dist-cli/chunk-EXWGNL6K.js.map +0 -1
- package/dist-cli/forgecad.js.map +0 -1
- package/dist-cli/forgecad_geometry-GYVNKPIE.js.map +0 -1
- package/dist-cli/solver-46FFSK2U.js.map +0 -1
- package/dist-skill/SKILL-dev.md +0 -145
- package/dist-skill/docs-dev/API/core/concepts.md +0 -118
- package/dist-skill/docs-dev/CLI.md +0 -677
- package/dist-skill/docs-dev/agent-native-api.md +0 -27
- package/dist-skill/docs-dev/blueprint-first.md +0 -145
- package/dist-skill/docs-dev/coding-best-practices.md +0 -120
- package/dist-skill/docs-dev/coding.md +0 -340
- package/dist-skill/docs-dev/component-model.md +0 -164
- package/dist-skill/docs-dev/generated/assembly.md +0 -794
- package/dist-skill/docs-dev/generated/core.md +0 -2117
- package/dist-skill/docs-dev/generated/curves.md +0 -2583
- package/dist-skill/docs-dev/generated/lib.md +0 -169
- package/dist-skill/docs-dev/generated/output.md +0 -247
- package/dist-skill/docs-dev/generated/sdf.md +0 -446
- package/dist-skill/docs-dev/generated/sheet-metal.md +0 -504
- package/dist-skill/docs-dev/generated/sketch.md +0 -1811
- package/dist-skill/docs-dev/generated/viewport.md +0 -585
- package/dist-skill/docs-dev/generated/wood.md +0 -108
- package/dist-skill/docs-dev/guides/coordinate-system.md +0 -46
- package/dist-skill/docs-dev/guides/geometry-conventions.md +0 -52
- package/dist-skill/docs-dev/guides/inspection-bundles.md +0 -485
- package/dist-skill/docs-dev/guides/joint-design.md +0 -78
- package/dist-skill/docs-dev/guides/modeling-recipes.md +0 -78
- package/dist-skill/docs-dev/guides/positioning.md +0 -161
- package/dist-skill/docs-dev/guides/skill-maintenance.md +0 -161
- package/dist-skill/docs-dev/internals/backend-vocabulary.md +0 -35
- package/dist-skill/docs-dev/internals/compiler.md +0 -307
- package/dist-skill/docs-dev/internals/constraint-solver-quality.md +0 -161
- package/dist-skill/docs-dev/internals/constraint-solver.md +0 -176
- package/dist-skill/docs-dev/internals/sketch-2d-pipeline.md +0 -108
- package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.mjs +0 -289
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
skill-group: dev-solver
|
|
3
|
-
skill-order: 1
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Constraint Solver Internals
|
|
7
|
-
|
|
8
|
-
> For a new team member. This covers the solver architecture, the key design
|
|
9
|
-
> decisions behind it, and the pitfalls we've already hit and solved.
|
|
10
|
-
|
|
11
|
-
## Architecture overview
|
|
12
|
-
|
|
13
|
-
The constraint solver lives in `src/forge/sketch/constraints/`. Each constraint
|
|
14
|
-
type is a file in `defs/` that calls `registerConstraint()` at module load time.
|
|
15
|
-
The central registry (`registry.ts`) drives the solver.
|
|
16
|
-
|
|
17
|
-
### Solver pipeline
|
|
18
|
-
|
|
19
|
-
When the user calls `sk.solve()`, the pipeline is:
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
1. Clone the definition (preserve originals for undo/compare)
|
|
23
|
-
2. Build Maps for fast lookup (points, lines, circles, arcs, shapes)
|
|
24
|
-
3. PRESOLVE — call constraint.presolve() once per constraint
|
|
25
|
-
4. GS WARM-UP — 5 cheap Gauss-Seidel iterations to propagate positions
|
|
26
|
-
5. NEWTON-RAPHSON — up to N iterations (if every constraint has a residual)
|
|
27
|
-
6. GS FALLBACK — if NR wasn't available or didn't converge, run full GS loop
|
|
28
|
-
7. Arc enforcement — scale arc endpoints to consistent radius
|
|
29
|
-
8. Status — compute DOF, detect over/under constraint, find redundancies
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### Constraint definition anatomy
|
|
33
|
-
|
|
34
|
-
Every constraint def file exports a `ConstraintDef` with these methods:
|
|
35
|
-
|
|
36
|
-
| Method | Called by | Purpose |
|
|
37
|
-
|---|---|---|
|
|
38
|
-
| `presolve` | Once, before iterations | One-shot initialisation (pin fixed points, snap angles, enforce winding) |
|
|
39
|
-
| `solve` | Each GS iteration | Move points to satisfy this constraint (returns error magnitude) |
|
|
40
|
-
| `residual` | Each NR iteration | Return equation residual(s) for Gauss-Newton step |
|
|
41
|
-
| `computeDof` | Status computation | Declare which points this constraint "touches" for DOF counting |
|
|
42
|
-
| `displayPosition` | UI rendering | Where to place the constraint label in the viewport |
|
|
43
|
-
|
|
44
|
-
## The three solver phases, and why we need all of them
|
|
45
|
-
|
|
46
|
-
### Presolve
|
|
47
|
-
|
|
48
|
-
Each constraint can optionally run a one-shot setup before any iterations.
|
|
49
|
-
The `fixed` constraint uses this to pin points (`pt.fixed = true`). Angle
|
|
50
|
-
constraints use it to snap endpoints into the correct half-plane so NR doesn't
|
|
51
|
-
start near a degenerate configuration.
|
|
52
|
-
|
|
53
|
-
### GS warm-up (Gauss-Seidel)
|
|
54
|
-
|
|
55
|
-
**Why this exists:** Fresh points often start at degenerate positions (all at
|
|
56
|
-
`(0, 1)`, creating zero-length lines). Newton-Raphson needs a reasonable
|
|
57
|
-
starting point — if the Jacobian is near-singular at the starting position, NR
|
|
58
|
-
can't compute a useful step.
|
|
59
|
-
|
|
60
|
-
The warm-up runs 5 cheap GS iterations: for each constraint in definition
|
|
61
|
-
order, call `solve()` which directly pushes points toward the target. For a
|
|
62
|
-
chain of angle-constrained lines, this propagates positions forward:
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
GS iteration 1:
|
|
66
|
-
absoluteAngle(line1, -90°) → anchors line1.a, moves line1.b downward
|
|
67
|
-
absoluteAngle(line2, 0°) → anchors line2.a = line1.b, moves line2.b right
|
|
68
|
-
absoluteAngle(line3, 90°) → anchors line3.a = line2.b, moves line3.b up
|
|
69
|
-
...
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
After a few passes, all points are near their final positions. NR takes over
|
|
73
|
-
for fine convergence.
|
|
74
|
-
|
|
75
|
-
### Newton-Raphson (NR)
|
|
76
|
-
|
|
77
|
-
The main solver for precision. Uses numerical Jacobian (forward finite
|
|
78
|
-
differences, step = 1e-6) and Gauss-Newton least-squares with Armijo
|
|
79
|
-
backtracking line search. All free point coordinates are variables; all
|
|
80
|
-
constraint residuals form the equation system.
|
|
81
|
-
|
|
82
|
-
NR only runs if **every** constraint defines a `residual()` function.
|
|
83
|
-
Constraints with `equations: 0` (like `fixed` and `ccw`) return `[]` from
|
|
84
|
-
`residual()` — this is fine; they just don't contribute equations.
|
|
85
|
-
|
|
86
|
-
### GS fallback
|
|
87
|
-
|
|
88
|
-
If NR wasn't available (some constraint missing residual) or didn't converge
|
|
89
|
-
below tolerance, a full Gauss-Seidel loop runs for up to N iterations.
|
|
90
|
-
|
|
91
|
-
## Key design pitfalls (and how we handle them)
|
|
92
|
-
|
|
93
|
-
### The "modulo-180°" ambiguity
|
|
94
|
-
|
|
95
|
-
**Problem:** A line from `a` to `b` has angle `θ` from the positive X-axis.
|
|
96
|
-
The same line traversed from `b` to `a` has angle `θ + 180°`. A naïve residual
|
|
97
|
-
like `sin(angle − target) = 0` is satisfied at **both** `θ = target` and
|
|
98
|
-
`θ = target + 180°`, so the solver can converge to the wrong orientation.
|
|
99
|
-
|
|
100
|
-
**Solution:** Use `normalizeAngle(angle − target)` as the residual. This has:
|
|
101
|
-
- A unique zero at `angle = target`
|
|
102
|
-
- Gradient ≈ 1 everywhere (unlike `1 − cos` which has gradient 0 at the target)
|
|
103
|
-
- A discontinuity at `±π`, which `presolve` avoids by snapping points into the
|
|
104
|
-
correct half-plane before NR starts.
|
|
105
|
-
|
|
106
|
-
We tried two other formulations before landing on this:
|
|
107
|
-
|
|
108
|
-
| Residual | Unique zero? | Gradient at target |
|
|
109
|
-
|---|---|---|
|
|
110
|
-
| `sin(angle − t)` | No (also zero at t+π) | 1 (good) |
|
|
111
|
-
| `1 − cos(angle − t)` | Yes | **0** (NR stalls!) |
|
|
112
|
-
| `normalizeAngle(angle − t)` | Yes | 1 (good) |
|
|
113
|
-
|
|
114
|
-
### The "reference-count heuristic" for which point to move
|
|
115
|
-
|
|
116
|
-
**Problem:** When `absoluteAngle.solve()` runs, it needs to decide which
|
|
117
|
-
endpoint to anchor and which to move. If neither point is explicitly `fixed`,
|
|
118
|
-
the wrong choice corrupts shared points. Example: the last line in a chain
|
|
119
|
-
closes back to a triangle vertex — moving the triangle vertex breaks the
|
|
120
|
-
triangle.
|
|
121
|
-
|
|
122
|
-
**Solution:** Count how many lines each point appears in (`pointLineRefs`).
|
|
123
|
-
The point referenced by more lines is more constrained by the rest of the
|
|
124
|
-
system — anchor it, move the other one.
|
|
125
|
-
|
|
126
|
-
```
|
|
127
|
-
Line closing back to triangle:
|
|
128
|
-
a = chain endpoint (2 line refs) → LESS constrained → MOVE this one
|
|
129
|
-
b = triangle vertex (3 line refs) → MORE constrained → ANCHOR this
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### The "unconditional presolve snap"
|
|
133
|
-
|
|
134
|
-
**Problem:** Fresh points often start at `(0, 1)`, creating zero-length lines.
|
|
135
|
-
`angleOfLine(a, b)` returns 0 for a zero-length line (atan2(0,0) = 0). If this
|
|
136
|
-
bogus angle happens to equal the target, presolve skips the line. Then NR starts
|
|
137
|
-
with a degenerate Jacobian (zero-length lines have undefined angular gradients)
|
|
138
|
-
and can't converge.
|
|
139
|
-
|
|
140
|
-
**Solution:** `absoluteAngle.presolve()` unconditionally snaps the free
|
|
141
|
-
endpoint to `(anchor + cos/sin(target) * max(len, 1))`. This ensures every
|
|
142
|
-
line starts with a meaningful direction and nonzero length.
|
|
143
|
-
|
|
144
|
-
### The "discrete orientation ambiguity" (CCW constraint)
|
|
145
|
-
|
|
146
|
-
**Problem:** An equilateral triangle with a fixed vertex and `absoluteAngle` on
|
|
147
|
-
one side has its shape fully determined — except the third vertex can be on
|
|
148
|
-
either side of the first line (mirror image). Both solutions satisfy all
|
|
149
|
-
continuous constraints. Nothing in the equation-based solver prefers one over
|
|
150
|
-
the other.
|
|
151
|
-
|
|
152
|
-
**Solution:** The `ccw` constraint (0 equations, presolve + solve only). It
|
|
153
|
-
computes the signed polygon area and, if negative (clockwise), reflects the
|
|
154
|
-
last non-fixed vertex across the line formed by the first two vertices. Since it
|
|
155
|
-
runs in presolve (before NR) and in solve (during GS), the triangle is always
|
|
156
|
-
pushed to CCW. NR respects this because its steps are small — it won't swing a
|
|
157
|
-
vertex through the opposite side in a single step.
|
|
158
|
-
|
|
159
|
-
Usage:
|
|
160
|
-
```js
|
|
161
|
-
const t = eqilateralTriangle(origin, sk.point(1, 1), sk.point(0, 5));
|
|
162
|
-
sk.ccw(origin, sk.point(1, 1), sk.point(0, 5)); // lock CCW winding
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## Builder rejection protocol
|
|
166
|
-
|
|
167
|
-
When a user calls `sk.absoluteAngle(line, 90)`, the builder doesn't just
|
|
168
|
-
record the constraint — it does a **test solve** (30 iterations) to check
|
|
169
|
-
feasibility. If `maxError > tolerance × 5`, the constraint is rejected and
|
|
170
|
-
stored in `rejectedConstraints[]`. This is why solver convergence from cold
|
|
171
|
-
start matters: a constraint that is mathematically satisfiable can still get
|
|
172
|
-
rejected if the solver can't reach the solution in 30 iterations from the
|
|
173
|
-
initial point positions.
|
|
174
|
-
|
|
175
|
-
The GS warm-up phase was added specifically to address this: it gets points
|
|
176
|
-
close enough that NR can finish the job within the iteration budget.
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# Shape.fromSlices — Design Document
|
|
2
|
-
|
|
3
|
-
## Problem
|
|
4
|
-
|
|
5
|
-
Users want to define 3D shapes by specifying 2D cross-section profiles on
|
|
6
|
-
different planes (like slicing an object at various positions), then
|
|
7
|
-
interpolating between them to create the solid. Think: "here's what the egg
|
|
8
|
-
looks like from the side, here's what it looks like from the top — build me
|
|
9
|
-
the 3D shape."
|
|
10
|
-
|
|
11
|
-
Today's `loft()` only handles parallel XY cross-sections at different Z
|
|
12
|
-
heights. There is no way to combine constraints from multiple viewing
|
|
13
|
-
directions (e.g., XY circle + XZ egg-ellipse = egg).
|
|
14
|
-
|
|
15
|
-
## API
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
Shape.fromSlices(slices: Slice[], options?: FromSlicesOptions): Shape
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### Types
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
type SlicePlane = 'xy' | 'xz' | 'yz' | Vec3;
|
|
25
|
-
|
|
26
|
-
interface Slice {
|
|
27
|
-
on: SlicePlane; // plane normal (string shorthand or arbitrary unit vector)
|
|
28
|
-
at: number; // signed offset along that normal from the origin
|
|
29
|
-
profile: Sketch; // 2D cross-section on that plane
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface FromSlicesOptions {
|
|
33
|
-
edgeLength?: number; // mesh resolution (Manifold only)
|
|
34
|
-
boundsPadding?: number; // SDF bounding box padding (Manifold only)
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Why `Shape.fromSlices()`?
|
|
39
|
-
|
|
40
|
-
1. **No new global** — `Shape` already exists in the runtime. Adding a static
|
|
41
|
-
method adds zero namespace pollution.
|
|
42
|
-
2. **Discoverable** — "I want a Shape, what can I build one from?" leads
|
|
43
|
-
naturally to `Shape.fromSlices`, `Shape.fromPatch`, etc.
|
|
44
|
-
3. **Precedent** — `Points.distance()`, `sdf.sphere()`, `route.fillet()` all
|
|
45
|
-
use the namespace-dot-method pattern. This extends it to the core `Shape`
|
|
46
|
-
class.
|
|
47
|
-
4. **Consistent with broader direction** — new specialized constructors go on
|
|
48
|
-
`Shape` as static methods instead of polluting the flat global namespace.
|
|
49
|
-
|
|
50
|
-
## Algorithm
|
|
51
|
-
|
|
52
|
-
### Core idea: group → interpolate → intersect
|
|
53
|
-
|
|
54
|
-
1. **Group** slices by normal direction. Two slices with the same (or
|
|
55
|
-
antiparallel) normal belong to the same group.
|
|
56
|
-
|
|
57
|
-
2. **Within each group** — build a solid by lofting (ThruSections / level-set)
|
|
58
|
-
between the profiles, placed on their respective parallel planes.
|
|
59
|
-
- Single-slice group: extrude the profile along the normal direction,
|
|
60
|
-
bounded generously (later trimmed by intersection).
|
|
61
|
-
- Multi-slice group: loft between profiles via ThruSections (OCCT) or
|
|
62
|
-
level-set (Manifold).
|
|
63
|
-
|
|
64
|
-
3. **Across groups** — boolean-intersect all group solids. Each group's solid
|
|
65
|
-
represents a "viewing constraint": the final shape must fit within all of
|
|
66
|
-
them simultaneously.
|
|
67
|
-
|
|
68
|
-
### Why intersection?
|
|
69
|
-
|
|
70
|
-
Each group defines "what the object looks like from one direction." Extruding
|
|
71
|
-
a side-view silhouette gives a prism that contains every possible shape
|
|
72
|
-
matching that silhouette. Intersecting all such prisms carves out the shape
|
|
73
|
-
that satisfies all views simultaneously. This is the *visual hull*
|
|
74
|
-
reconstruction principle.
|
|
75
|
-
|
|
76
|
-
### Examples
|
|
77
|
-
|
|
78
|
-
**Egg (two silhouettes):**
|
|
79
|
-
```
|
|
80
|
-
Group 1 (normal=Y, 1 slice): XZ egg-ellipse → extrude along Y → egg prism
|
|
81
|
-
Group 2 (normal=X, 1 slice): YZ egg-ellipse → extrude along X → egg prism
|
|
82
|
-
Intersect → egg solid
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**Vase (loft + envelope):**
|
|
86
|
-
```
|
|
87
|
-
Group 1 (normal=Z, 3 slices): circle@0, squircle@40, circle@80 → loft
|
|
88
|
-
Group 2 (normal=Y, 1 slice): vase silhouette → extrude along Y
|
|
89
|
-
Intersect → vase with cross-section transitions
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**Pure loft (single group):**
|
|
93
|
-
```
|
|
94
|
-
Group 1 (normal=Z, 3 slices): profiles at different Z heights → loft
|
|
95
|
-
No intersection needed (single group) → equivalent to loft()
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Compile plan
|
|
99
|
-
|
|
100
|
-
A new `kind: 'fromSlices'` is added to `ShapeCompilePlan`:
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
{
|
|
104
|
-
kind: 'fromSlices';
|
|
105
|
-
groups: FromSlicesGroupPlan[];
|
|
106
|
-
edgeLength: number;
|
|
107
|
-
boundsPadding: number;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
interface FromSlicesGroupPlan {
|
|
111
|
-
normal: [number, number, number]; // unit normal for this group
|
|
112
|
-
slices: {
|
|
113
|
-
offset: number; // signed distance along normal
|
|
114
|
-
profile: ProfileCompilePlan;
|
|
115
|
-
}[];
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### Lowering strategy
|
|
120
|
-
|
|
121
|
-
**OCCT backend:**
|
|
122
|
-
- Single-slice group → extrude profile, rotate to align with normal, translate
|
|
123
|
-
- Multi-slice group → ThruSections with wires placed on their planes
|
|
124
|
-
(existing lowerLoftPlan pattern, generalized from Z-only to arbitrary normal)
|
|
125
|
-
- Intersection → `BRepAlgoAPI_Common`
|
|
126
|
-
- Result: exact B-rep, exportable to STEP
|
|
127
|
-
|
|
128
|
-
**Manifold backend:**
|
|
129
|
-
- Single-slice group → extrude cross-section, transform
|
|
130
|
-
- Multi-slice group → loft via level-set (existing buildLoftLevelSetInput)
|
|
131
|
-
- Intersection → Manifold boolean intersection
|
|
132
|
-
- Result: mesh
|
|
133
|
-
|
|
134
|
-
### Single-group optimization
|
|
135
|
-
|
|
136
|
-
When all slices share the same normal (pure loft case), skip the intersection
|
|
137
|
-
entirely and lower as a plain loft. This avoids unnecessary boolean overhead
|
|
138
|
-
and produces identical output to `loft()`.
|
|
139
|
-
|
|
140
|
-
## Broader API implications
|
|
141
|
-
|
|
142
|
-
This is the first `Shape` static method. It establishes a pattern for future
|
|
143
|
-
specialized constructors:
|
|
144
|
-
|
|
145
|
-
| Method | Replaces global | Status |
|
|
146
|
-
|--------|----------------|--------|
|
|
147
|
-
| `Shape.fromSlices()` | (new) | This PR |
|
|
148
|
-
| `Shape.fromPatch()` | `surfacePatch()` | Future |
|
|
149
|
-
|
|
150
|
-
Existing globals (`loft`, `sweep`, `box`, etc.) remain unchanged — no
|
|
151
|
-
migration, no breakage. New specialized construction goes through
|
|
152
|
-
`Shape.method()`.
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
skill-group: dev-compiler
|
|
3
|
-
skill-order: 2
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# 2D Sketch Pipeline: Runtime Mesh ↔ ProfileCompilePlan ↔ Export Backend
|
|
7
|
-
|
|
8
|
-
Captured from implementing sketch region selection, planar arrangement detection, and cross-sketch reference geometry (2026-03).
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## The Two-Track Export Model
|
|
13
|
-
|
|
14
|
-
Every 3D shape in ForgeCAD has two representations:
|
|
15
|
-
|
|
16
|
-
1. **Runtime mesh** — a watertight triangulated solid used for preview rendering and mesh-domain operations.
|
|
17
|
-
2. **`ProfileCompilePlan`** — a serializable Forge intent description (discriminated union with `kind: 'rect' | 'circle' | 'polygon' | 'boolean' | ...`) that the current export backend reads to produce STEP geometry.
|
|
18
|
-
|
|
19
|
-
When exporting STEP, the system walks the `ProfileCompilePlan` tree. If a node has no plan (plan is `null`), it falls back to a faceted mesh-to-STEP conversion — useful for interchange, but it loses parametric intent.
|
|
20
|
-
|
|
21
|
-
**Key invariant**: any sketch API that only calls `polygon()` and boolean ops (`add`/`subtract`) automatically stays on the maintained STEP path. No special backend-specific code is needed.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## What `Sketch` Is
|
|
26
|
-
|
|
27
|
-
`Sketch` wraps a Manifold `CrossSection` (2D polygon set). It also carries:
|
|
28
|
-
- An optional `ProfileCompilePlan` for the STEP path.
|
|
29
|
-
- A `Placement3D` for face-mounted sketches (`.onFace()`).
|
|
30
|
-
|
|
31
|
-
`ConstrainedSketchBuilder` → `.solve()` → `ConstraintSketch extends Sketch` holds the constraint definition alongside the solved `CrossSection`. The solved `CrossSection` is built from explicitly declared loops (`addLoop`); geometry outside loops is not included in the area — only in the constraint definition for arrangement detection.
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## New 2D Sketch Surface-Selection APIs (2026-03)
|
|
36
|
-
|
|
37
|
-
### 1. `sketch.regions()` / `sketch.region(seed)`
|
|
38
|
-
|
|
39
|
-
**File**: `src/forge/sketch/regions.ts`
|
|
40
|
-
|
|
41
|
-
Decomposes a Manifold `CrossSection` into its distinct filled areas.
|
|
42
|
-
|
|
43
|
-
- `CrossSection.toPolygons()` returns a flat list of contours. Positive signed area → outer boundary; negative → hole.
|
|
44
|
-
- Holes are nested into their smallest containing outer boundary via `pointInPolygon`.
|
|
45
|
-
- Each outer boundary + its holes is reassembled as a `polygon(outerPts, holePts)` sketch.
|
|
46
|
-
- The `region(seed)` variant picks the one face whose outer boundary contains the seed and no hole does.
|
|
47
|
-
|
|
48
|
-
**STEP export**: uses `polygon()` and potentially `boolean(difference)` for rings — both are handled by the current export backend without extra sketch-specific code.
|
|
49
|
-
|
|
50
|
-
### 2. `constraintSketch.detectArrangement()` / `detectArrangementRegion(seed)`
|
|
51
|
-
|
|
52
|
-
**File**: `src/forge/sketch/arrangement.ts`
|
|
53
|
-
|
|
54
|
-
DCEL-based planar arrangement detection from the raw line segments in a `ConstraintDefinition`.
|
|
55
|
-
|
|
56
|
-
Algorithm:
|
|
57
|
-
1. Extract non-construction line segments from `def.lines`.
|
|
58
|
-
2. Split segments at all pairwise intersections — both X-crossings (`segSegT`) and **T-junctions** (`pointOnSegT`). T-junction support is critical: when a divider endpoint touches a boundary edge interior, only `pointOnSegT` detects it.
|
|
59
|
-
3. Snap nearby nodes and build a clean planar graph.
|
|
60
|
-
4. Build DCEL half-edges. At each node sort outgoing half-edges by polar angle.
|
|
61
|
-
5. `next(u→v)` = outgoing from v immediately preceding `twin(u→v)` in CCW order at v — i.e., `out[(pos−1+n) % n]`.
|
|
62
|
-
6. Traverse all face cycles; keep CCW faces (positive signed area). CW = unbounded outer face, excluded.
|
|
63
|
-
7. Return each face as a `polygon(pts)` sketch.
|
|
64
|
-
|
|
65
|
-
No explicit loops needed from the caller. Works on any set of line constraints.
|
|
66
|
-
|
|
67
|
-
**STEP export**: each face is a `polygon()` — handled by the maintained export path.
|
|
68
|
-
|
|
69
|
-
### 3. `builderB.referenceFrom(sketchA, entityId)` / `referenceAllFrom(sketchA)`
|
|
70
|
-
|
|
71
|
-
**File**: `src/forge/sketch/constraints.ts`
|
|
72
|
-
|
|
73
|
-
Import solved geometry from another `ConstraintSketch` as fixed construction references.
|
|
74
|
-
|
|
75
|
-
- Fixed points (`fixed: true`) and construction lines (`construction: true`) participate in constraint solving but contribute zero area to the resulting CrossSection profile.
|
|
76
|
-
- `referenceFrom(source, id)` looks up the entity by id in the source's `ConstraintDefinition` and creates a fixed copy in the current builder.
|
|
77
|
-
- Enables constraints like `parallel(bBot, refBase)` to lock relationships between separate sketches.
|
|
78
|
-
|
|
79
|
-
**STEP export**: construction elements are ignored by `buildSketchFromDefinition` when assembling loops → no impact on export path.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Backend Agnosticism
|
|
84
|
-
|
|
85
|
-
These APIs are fully backend-agnostic because they operate on:
|
|
86
|
-
- **2D coordinates only** — no 3D kernel calls during region/arrangement detection.
|
|
87
|
-
- **`polygon()`** — the lowest-level sketch primitive, available on every backend.
|
|
88
|
-
- **Boolean ops** — available on every backend (Manifold, OCCT, …).
|
|
89
|
-
|
|
90
|
-
No backend-specific code was added. Adding a new backend (e.g., CGAL, OpenCASCADE directly) automatically inherits all three APIs as long as it handles `polygon` and `boolean` plan kinds.
|
|
91
|
-
|
|
92
|
-
The only Manifold-specific call is `CrossSection.toPolygons()` in `regions.ts`. If a future backend doesn't use `CrossSection`, `sketchRegions` would need an adapter — but the algorithm and public API shape would be unchanged.
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
## Gotchas
|
|
97
|
-
|
|
98
|
-
### Empty CrossSection for loop-less sketches
|
|
99
|
-
|
|
100
|
-
`constrainedSketch().solve()` used to throw "at least one closed loop" when called without `addLoop()`. Users calling `.detectArrangement()` never add loops. Fix: return an empty `CrossSection` via `CrossSection.difference([unit, unit])` (not `new CrossSection([])` — Manifold's `polygons2vec` crashes on empty array).
|
|
101
|
-
|
|
102
|
-
### T-junctions
|
|
103
|
-
|
|
104
|
-
Interior-only intersection detection (`segSegT`) misses T-junctions entirely. A 3×2 grid yields only 4 cells instead of 6 if T-junctions aren't split. Always run `pointOnSegT` for every endpoint of every other segment against every segment.
|
|
105
|
-
|
|
106
|
-
### DCEL `next` pointer direction
|
|
107
|
-
|
|
108
|
-
The formula traces faces to the **LEFT** of each directed half-edge. CW winding = unbounded outer face. Keep only faces with positive signed area (CCW winding = bounded interior face).
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# Admin Dashboard & Audit System
|
|
2
|
-
|
|
3
|
-
The admin dashboard lives at `/admin` and is restricted to users with `role = 'admin'`. The frontend gate is in `src/pages/AdminPage.tsx`; the backend enforces the same role check on `/api/admin/*` routes.
|
|
4
|
-
|
|
5
|
-
## Dashboard features
|
|
6
|
-
|
|
7
|
-
- **System health** -- server status, uptime, version (fetched from `/api/health`).
|
|
8
|
-
- **User statistics** -- total users, total projects, active today.
|
|
9
|
-
- **User list** -- all users with name, email, role, and join date.
|
|
10
|
-
- **Audit log** -- recent activity feed (via `/api/admin/audit`).
|
|
11
|
-
|
|
12
|
-
## Audit log
|
|
13
|
-
|
|
14
|
-
Every significant user action is recorded in the `audit_log` table. The schema (`server/db/schema.ts`):
|
|
15
|
-
|
|
16
|
-
| Column | Type | Description |
|
|
17
|
-
|--------|------|-------------|
|
|
18
|
-
| `id` | uuid | Primary key |
|
|
19
|
-
| `user_id` | uuid | FK to `users` |
|
|
20
|
-
| `project_id` | uuid | FK to `projects` (nullable) |
|
|
21
|
-
| `action` | text | Event type (see below) |
|
|
22
|
-
| `details` | jsonb | Arbitrary metadata |
|
|
23
|
-
| `ip_address` | inet | Client IP |
|
|
24
|
-
| `created_at` | timestamptz | When it happened |
|
|
25
|
-
|
|
26
|
-
### Tracked actions
|
|
27
|
-
|
|
28
|
-
| Category | Actions |
|
|
29
|
-
|----------|---------|
|
|
30
|
-
| Auth | `user.register`, `user.login`, `user.password_reset`, `user.token_replay_detected` |
|
|
31
|
-
| Projects | `project.create`, `project.update`, `project.delete` |
|
|
32
|
-
| Files | `file.save`, `file.delete` |
|
|
33
|
-
| Shares | `share.publish`, `share.unpublish` |
|
|
34
|
-
|
|
35
|
-
Audit inserts are inline in each route handler (`server/routes/auth.ts`, `server/routes/projects.ts`, `server/routes/files.ts`, `server/routes/shares.ts`).
|
|
36
|
-
|
|
37
|
-
## Granting admin access
|
|
38
|
-
|
|
39
|
-
There is no self-service role escalation. Set the role directly in the database:
|
|
40
|
-
|
|
41
|
-
```sql
|
|
42
|
-
UPDATE users SET role = 'admin' WHERE email = 'user@example.com';
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
See [auth.md](auth.md) for the role system and [../runbook.md](../runbook.md) for production database access.
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# System Architecture
|
|
2
|
-
|
|
3
|
-
ForgeCAD is a browser-based parametric CAD application. The production stack runs as Docker containers on Hetzner, managed by Kamal 2.
|
|
4
|
-
|
|
5
|
-
## Stack Overview
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
Internet
|
|
9
|
-
|
|
|
10
|
-
Cloudflare
|
|
11
|
-
|
|
|
12
|
-
kamal-proxy (:80/:443)
|
|
13
|
-
|
|
|
14
|
-
forgecad web container (Fastify / Node.js)
|
|
15
|
-
|--- serves SPA (Vite-built React frontend)
|
|
16
|
-
|--- API routes (/api/*)
|
|
17
|
-
|--- calls forgecad-backend over Docker network
|
|
18
|
-
|
|
|
19
|
-
PostgreSQL accessory
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
**Frontend:** React SPA built with Vite. Three.js (via React Three Fiber) powers the 3D viewport. Monaco Editor provides the code editor. The SPA is served by the same Fastify process that handles API routes.
|
|
23
|
-
|
|
24
|
-
**Server:** Fastify with Drizzle ORM for database access. Serves both the static SPA bundle and the `/api/*` JSON endpoints from a single process.
|
|
25
|
-
|
|
26
|
-
**Database:** PostgreSQL for user accounts, project metadata, and application state.
|
|
27
|
-
|
|
28
|
-
**File storage:** User project files are stored on disk (Docker volume `project-data`), not in the database.
|
|
29
|
-
|
|
30
|
-
## Deployment Topology
|
|
31
|
-
|
|
32
|
-
Production uses Kamal deploy configs, not Docker Compose:
|
|
33
|
-
|
|
34
|
-
| File | Purpose |
|
|
35
|
-
|------|---------|
|
|
36
|
-
| `config/deploy.yml` | Kamal config for the ForgeCAD web app |
|
|
37
|
-
| `config/deploy.backend.yml` | Kamal config for the ForgeCAD backend compute service |
|
|
38
|
-
|
|
39
|
-
Docker Compose remains useful for local/self-hosted setups, but it is not the production control plane.
|
|
40
|
-
|
|
41
|
-
## Authentication
|
|
42
|
-
|
|
43
|
-
JWT-based auth with token rotation:
|
|
44
|
-
|
|
45
|
-
- **Access tokens:** 15-minute expiry
|
|
46
|
-
- **Refresh tokens:** 7-day expiry, rotated on use
|
|
47
|
-
|
|
48
|
-
See [auth.md](auth.md) for details on OAuth providers, token flow, and session management.
|
|
49
|
-
|
|
50
|
-
## URL Routes
|
|
51
|
-
|
|
52
|
-
| Route | Access | Purpose |
|
|
53
|
-
|-------|--------|---------|
|
|
54
|
-
| `/` | Public | Landing page |
|
|
55
|
-
| `/docs`, `/docs/:slug` | Public | API documentation |
|
|
56
|
-
| `/blog`, `/blog/:slug` | Public | Announcements and tutorials |
|
|
57
|
-
| `/app` | Auth-gated | The CAD editor |
|
|
58
|
-
| `/app?gist=<id>` | Public preview | Shared model from GitHub Gist (read-only until login) |
|
|
59
|
-
| `/app?url=<url>` | Public preview | Shared model from external URL |
|
|
60
|
-
| `/app#code/<file>/<compressed>` | Public preview | Shared model with inline code |
|
|
61
|
-
| `/app?embed=1` | Public | Embed-only viewport (no chrome, for iframes) |
|
|
62
|
-
| `/m/:shareId` | Public | Published model preview (code + 3D viewport) |
|
|
63
|
-
| `/m/:shareId?embed=1` | Public | Published model embed (viewport only) |
|
|
64
|
-
| `/admin` | Admin role | User management, system health, audit log |
|
|
65
|
-
| `/auth/callback/*` | Public | OAuth callback handlers |
|
|
66
|
-
|
|
67
|
-
## Security
|
|
68
|
-
|
|
69
|
-
- **Rate limiting:** 100 requests/min per IP; 10 auth attempts per 15 minutes
|
|
70
|
-
- **CORS:** Origin whitelist (no wildcard)
|
|
71
|
-
- **CSP:** Content Security Policy headers on all responses
|
|
72
|
-
- **Path traversal:** Server-side validation on all file access paths
|
|
73
|
-
|
|
74
|
-
## Related Documentation
|
|
75
|
-
|
|
76
|
-
- [auth.md](auth.md) -- Authentication and authorization
|
|
77
|
-
- [projects.md](projects.md) -- Project storage and management
|
|
78
|
-
- [sharing.md](sharing.md) -- Model sharing and embeds
|
|
79
|
-
- [admin.md](admin.md) -- Admin panel
|
|
80
|
-
- [email.md](email.md) -- Transactional email
|
|
81
|
-
- [../deployment.md](../deployment.md) -- Environment variables and deployment setup
|
|
82
|
-
- [../runbook.md](../runbook.md) -- Operational procedures and troubleshooting
|