forgecad 0.6.3 → 0.8.0
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/README.md +3 -12
- package/dist/assets/{AdminPage-CeqCUUgu.js → AdminPage-D4bocK4E.js} +250 -151
- package/dist/assets/{BlogPage-P_AJP0v9.js → BlogPage-CJEXL_zJ.js} +94 -70
- package/dist/assets/{DocsPage-CKRV2iq2.js → DocsPage-D3A_g8V3.js} +329 -163
- package/dist/assets/{EditorApp-CnC2k4cW.css → EditorApp-BWYUSpUN.css} +590 -136
- package/dist/assets/EditorApp-Cihhqcsq.js +11692 -0
- package/dist/assets/{EmbedViewer-DBlzmQ5i.js → EmbedViewer-kWjKaC_t.js} +2 -4
- package/dist/assets/LandingPageProofDriven-Bg2IUc3l.css +856 -0
- package/dist/assets/LandingPageProofDriven-DXkKlyhI.js +601 -0
- package/dist/assets/PricingPage-BsU5vzEx.js +232 -0
- package/dist/assets/{SettingsPage-BqCh9JcC.js → SettingsPage-PqvpAKIs.js} +129 -5
- package/dist/assets/{evalWorker-Ql-aKwLA.js → evalWorker-C-hzNUMy.js} +8949 -3161
- package/dist/assets/{Viewport-CoB46f5R.js → index-Pz321YAt.js} +38382 -7501
- package/dist/assets/{index-2hfs_ub0.css → index-ay13WNfa.css} +726 -53
- package/dist/assets/{javascript-DCxGoE5Y.js → javascript-DAl8Gmyo.js} +1 -1
- package/dist/assets/{manifold-CqNMHHKO.js → manifold-BcbjWLIo.js} +4 -3
- package/dist/assets/{manifold-Cce9wRFz.js → manifold-DBckbFgx.js} +1 -1
- package/dist/assets/{manifold-D6BeHIOo.js → manifold-O2AAGXyj.js} +1 -1
- package/dist/assets/{reportWorker-sFEFonXf.js → reportWorker-Dxr-5A7w.js} +8760 -3559
- package/dist/assets/{vendor-react-Dt7-aaJH.js → vendor-react-CG3i_wp0.js} +65 -8
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/CLI.md +341 -718
- package/dist/docs-raw/generated/assembly.md +699 -112
- package/dist/docs-raw/generated/concepts.md +1834 -1346
- package/dist/docs-raw/generated/core.md +1012 -1059
- package/dist/docs-raw/generated/curves.md +759 -116
- package/dist/docs-raw/generated/lib.md +43 -748
- package/dist/docs-raw/generated/output.md +139 -245
- package/dist/docs-raw/generated/sdf.md +208 -0
- package/dist/docs-raw/generated/sheet-metal.md +473 -21
- package/dist/docs-raw/generated/sketch.md +1518 -362
- package/dist/docs-raw/generated/viewport.md +368 -299
- package/dist/docs-raw/generated/wood.md +104 -0
- package/dist/index.html +2 -2
- package/dist/landing/proof-ams-adapter.png +0 -0
- package/dist/landing/proof-bolt-and-nut.png +0 -0
- package/dist/landing/proof-fillet-enclosure.png +0 -0
- package/dist/landing/proof-glasses.png +0 -0
- package/dist/landing/proof-gyroid.png +0 -0
- package/dist/sitemap.xml +6 -6
- package/dist-cli/forgecad.js +12321 -5700
- package/dist-cli/forgecad.js.map +1 -0
- package/dist-cli/solver-46FFSK2U.js +363 -0
- package/dist-cli/solver-46FFSK2U.js.map +1 -0
- package/dist-skill/CONTEXT.md +4890 -6302
- package/dist-skill/SKILL-dev.md +22 -66
- package/dist-skill/SKILL.md +20 -59
- package/dist-skill/docs/API/core/concepts.md +37 -92
- package/dist-skill/docs/CLI.md +341 -718
- package/dist-skill/docs/generated/assembly.md +699 -112
- package/dist-skill/docs/generated/core.md +1012 -1059
- package/dist-skill/docs/generated/curves.md +759 -116
- package/dist-skill/docs/generated/lib.md +43 -748
- package/dist-skill/docs/generated/output.md +139 -245
- package/dist-skill/docs/generated/sdf.md +208 -0
- package/dist-skill/docs/generated/sheet-metal.md +473 -21
- package/dist-skill/docs/generated/sketch.md +1518 -362
- package/dist-skill/docs/generated/viewport.md +368 -299
- package/dist-skill/docs/generated/wood.md +104 -0
- package/dist-skill/docs/guides/coordinate-system.md +11 -17
- package/dist-skill/docs/guides/geometry-conventions.md +13 -70
- package/dist-skill/docs/guides/joint-design.md +78 -0
- package/dist-skill/docs/guides/modeling-recipes.md +22 -195
- package/dist-skill/docs/guides/positioning.md +88 -147
- package/dist-skill/docs-dev/API/core/concepts.md +78 -0
- package/dist-skill/docs-dev/CLI.md +488 -0
- package/dist-skill/{docs → docs-dev}/blueprint-first.md +5 -0
- package/dist-skill/{docs → docs-dev}/coding-best-practices.md +6 -8
- package/dist-skill/{docs → docs-dev}/coding.md +2 -4
- package/dist-skill/docs-dev/component-model.md +164 -0
- package/dist-skill/docs-dev/generated/assembly.md +779 -0
- package/dist-skill/docs-dev/generated/core.md +1676 -0
- package/dist-skill/docs-dev/generated/curves.md +855 -0
- package/dist-skill/docs-dev/generated/lib.md +55 -0
- package/dist-skill/docs-dev/generated/output.md +234 -0
- package/dist-skill/docs-dev/generated/sdf.md +208 -0
- package/dist-skill/docs-dev/generated/sheet-metal.md +506 -0
- package/dist-skill/docs-dev/generated/sketch.md +1753 -0
- package/dist-skill/docs-dev/generated/viewport.md +513 -0
- package/dist-skill/docs-dev/generated/wood.md +104 -0
- package/dist-skill/docs-dev/guides/coordinate-system.md +46 -0
- package/dist-skill/docs-dev/guides/geometry-conventions.md +52 -0
- package/dist-skill/docs-dev/guides/joint-design.md +78 -0
- package/dist-skill/docs-dev/guides/modeling-recipes.md +77 -0
- package/dist-skill/docs-dev/guides/positioning.md +151 -0
- package/dist-skill/{docs → docs-dev}/guides/skill-maintenance.md +21 -10
- package/dist-skill/{docs → docs-dev}/internals/compiler.md +5 -6
- package/dist-skill/{docs → docs-dev}/internals/constraint-solver-quality.md +0 -1
- package/dist-skill/{docs → docs-dev}/internals/constraint-solver.md +0 -1
- package/dist-skill/{docs → docs-dev}/internals/sketch-2d-pipeline.md +2 -3
- package/examples/api/attachTo-basics.forge.js +8 -8
- package/examples/api/bill-of-materials.forge.js +9 -9
- package/examples/api/bolt-pattern.forge.js +5 -5
- package/examples/api/boolean-operations.forge.js +5 -5
- package/examples/api/bounding-box-visualizer.forge.js +3 -3
- package/examples/api/clone-duplicate.forge.js +2 -2
- package/examples/api/colors-union-vs-array.forge.js +6 -6
- package/examples/api/connector-assembly.forge.js +8 -6
- package/examples/api/connector-basics.forge.js +7 -7
- package/examples/api/constrained-sketch-mechanical.forge.js +4 -4
- package/examples/api/elbow-test.forge.js +3 -3
- package/examples/api/extrude-options.forge.js +8 -14
- package/examples/api/feature-created-faces.forge.js +6 -10
- package/examples/api/fillet-showcase.forge.js +2 -2
- package/examples/api/folded-service-panel-cover.forge.js +2 -2
- package/examples/api/gears-tier1.forge.js +5 -5
- package/examples/api/group-test.forge.js +3 -3
- package/examples/api/group-vs-union.forge.js +1 -1
- package/examples/api/highlight-debug.forge.js +4 -0
- package/examples/api/js-module-pillars.js +1 -1
- package/examples/api/js-module-scene.js +2 -2
- package/examples/api/mesh-import-slats.forge.js +4 -4
- package/examples/api/patterns.forge.js +3 -3
- package/examples/api/pointAlong-orientation.forge.js +3 -3
- package/examples/api/profile-2020-b-slot6.forge.js +4 -5
- package/examples/api/route-perimeter-flange.forge.js +1 -1
- package/examples/api/sdf-rover-demo.forge.js +10 -10
- package/examples/api/sketch-on-face-demo.forge.js +2 -2
- package/examples/api/sketch-regions.forge.js +4 -4
- package/examples/api/sketch-rounding-strategies.forge.js +1 -1
- package/examples/api/smooth-curve-connections.forge.js +1 -1
- package/examples/api/transition-curves.forge.js +4 -4
- package/examples/api/variable-sweep-pure-sdf-test.forge.js +162 -0
- package/examples/api/variable-sweep-test.forge.js +2 -2
- package/examples/api/wood-joinery.forge.js +60 -0
- package/examples/compiler-corpus/enclosure-shell-cuts.forge.js +3 -3
- package/examples/compiler-corpus/fastener-plate-variants.forge.js +2 -2
- package/examples/constraints/01-fully-constrained-rect.forge.js +2 -2
- package/examples/constraints/02-underconstrained-triangle.forge.js +1 -1
- package/examples/constraints/03-redundant-constraints.forge.js +2 -2
- package/examples/constraints/05-parallel-with-linedistance.forge.js +2 -2
- package/examples/constraints/06-complex-spectrogram.forge.js +1 -1
- package/examples/constraints/07-perpendicular-chain.forge.js +4 -4
- package/examples/constraints/08-symmetric-bracket.forge.js +4 -4
- package/examples/constraints/09-stress-spiral.forge.js +1 -1
- package/examples/constraints/10-stress-honeycomb.forge.js +1 -1
- package/examples/constraints/11-surface-grid.forge.js +2 -2
- package/examples/constraints/12-surface-nested.forge.js +4 -4
- package/examples/constraints/13-surface-complex.forge.js +1 -1
- package/examples/exact-arc-housing.forge.js +12 -0
- package/examples/experiments/drone-arm.forge.js +53 -0
- package/examples/furniture/adjustable-table.forge.js +15 -15
- package/examples/furniture/bathroom.forge.js +26 -26
- package/examples/furniture/chair.forge.js +13 -13
- package/examples/furniture/picture-frame.forge.js +6 -6
- package/examples/furniture/shoe-rack-doors.forge.js +8 -8
- package/examples/furniture/shoe-rack.forge.js +7 -7
- package/examples/furniture/table-lamp.forge.js +8 -8
- package/examples/gcode/lissajous-vase.forge.js +4 -4
- package/examples/gcode/math-surface.forge.js +3 -3
- package/examples/gcode/parametric-vase.forge.js +4 -4
- package/examples/gcode/spiral-tower.forge.js +4 -4
- package/examples/generative/crystal-growth.forge.js +9 -9
- package/examples/generative/frost-spires.forge.js +9 -9
- package/examples/generative/golden-spiral-tower.forge.js +11 -11
- package/examples/generative/molten-forge.forge.js +6 -6
- package/examples/generative/neon-coral.forge.js +7 -7
- package/examples/mechanical/3d-printer.forge.js +37 -37
- package/examples/mechanical/5-finger-robot-hand.forge.js +19 -19
- package/examples/mechanical/airplane-propeller.forge.js +9 -9
- package/examples/mechanical/bolt-and-nut.forge.js +10 -10
- package/examples/mechanical/door-with-hinges.forge.js +7 -7
- package/examples/mechanical/fillet-enclosure.forge.js +15 -11
- package/examples/mechanical/headphone-hanger-v2.forge.js +11 -11
- package/examples/mechanical/robot_hand.forge.js +24 -24
- package/examples/mechanical/robot_hand_2.forge.js +26 -26
- package/examples/nurbs-surface.forge.js +8 -0
- package/examples/nurbs-tube.forge.js +7 -0
- package/examples/products/bottle.forge.js +8 -8
- package/examples/products/chess-set.forge.js +25 -25
- package/examples/products/classical-piano.forge.js +20 -20
- package/examples/products/clock.forge.js +33 -33
- package/examples/products/cup.forge.js +5 -5
- package/examples/products/iphone.forge.js +20 -20
- package/examples/products/laptop.forge.js +24 -24
- package/examples/products/laser-cut-box.forge.js +6 -6
- package/examples/products/laser-cut-tray.forge.js +6 -6
- package/examples/products/liquid-soap-dispenser.forge.js +23 -23
- package/examples/products/origami-fish.forge.js +14 -12
- package/examples/products/spiderman-cake.forge.js +6 -6
- package/examples/shelf/container.forge.js +5 -5
- package/examples/shelf/shelf-unit.forge.js +6 -6
- package/examples/toolbox/bolted-joint.forge.js +7 -7
- package/package.json +9 -4
- package/dist/assets/EditorApp-B-vQvgam.js +0 -9888
- package/dist/assets/LandingPage-C5n9hDXI.js +0 -322
- package/dist/assets/PublishedModelPage-Dt7PCVBj.js +0 -146
- package/dist/assets/__vite-browser-external-CURh0WXD.js +0 -8
- package/dist/assets/deserializeRunResult-BLAFoiE0.js +0 -19365
- package/dist/assets/index-1CYp3zUp.js +0 -1455
- package/dist-skill/docs/API/API.md +0 -1666
- package/dist-skill/docs/API/README.md +0 -37
- package/dist-skill/docs/API/assembly/assembly.md +0 -617
- package/dist-skill/docs/API/core/edge-queries.md +0 -130
- package/dist-skill/docs/API/core/parameters.md +0 -122
- package/dist-skill/docs/API/core/reserved-terms.md +0 -137
- package/dist-skill/docs/API/core/sdf.md +0 -326
- package/dist-skill/docs/API/core/skill-cli.md +0 -194
- package/dist-skill/docs/API/core/skill-guide.md +0 -205
- package/dist-skill/docs/API/core/specs.md +0 -186
- package/dist-skill/docs/API/core/topology.md +0 -372
- package/dist-skill/docs/API/entities.md +0 -268
- package/dist-skill/docs/API/output/bom.md +0 -58
- package/dist-skill/docs/API/output/brep-export.md +0 -87
- package/dist-skill/docs/API/output/dimensions.md +0 -67
- package/dist-skill/docs/API/output/export.md +0 -110
- package/dist-skill/docs/API/output/gcode.md +0 -195
- package/dist-skill/docs/API/runtime/viewport.md +0 -420
- package/dist-skill/docs/API/sheet-metal/sheet-metal.md +0 -185
- package/dist-skill/docs/API/sketch/anchor.md +0 -37
- package/dist-skill/docs/API/sketch/booleans.md +0 -91
- package/dist-skill/docs/API/sketch/core.md +0 -73
- package/dist-skill/docs/API/sketch/extrude.md +0 -62
- package/dist-skill/docs/API/sketch/on-face.md +0 -104
- package/dist-skill/docs/API/sketch/operations.md +0 -78
- package/dist-skill/docs/API/sketch/path.md +0 -75
- package/dist-skill/docs/API/sketch/primitives.md +0 -146
- package/dist-skill/docs/API/sketch/regions.md +0 -80
- package/dist-skill/docs/API/sketch/text.md +0 -108
- package/dist-skill/docs/API/sketch/transforms.md +0 -65
- package/dist-skill/docs/API/toolbox/fasteners.md +0 -129
- package/dist-skill/docs/INDEX.md +0 -94
- package/dist-skill/docs/RELEASING.md +0 -55
- package/dist-skill/docs/cli-monetization.md +0 -111
- package/dist-skill/docs/deployment.md +0 -281
- package/dist-skill/docs/generated/concepts.md +0 -2112
- package/dist-skill/docs/internals/shape-from-slices.md +0 -152
- package/dist-skill/docs/platform/admin.md +0 -45
- package/dist-skill/docs/platform/architecture.md +0 -79
- package/dist-skill/docs/platform/auth.md +0 -110
- package/dist-skill/docs/platform/email.md +0 -67
- package/dist-skill/docs/platform/projects.md +0 -111
- package/dist-skill/docs/platform/sharing.md +0 -90
- package/dist-skill/docs/runbook.md +0 -345
|
@@ -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,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 [../project/runbook.md](../project/runbook.md) for production database access.
|
|
@@ -1,79 +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 Coolify.
|
|
4
|
-
|
|
5
|
-
## Stack Overview
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
Internet
|
|
9
|
-
|
|
|
10
|
-
Coolify Proxy (Traefik, TLS termination)
|
|
11
|
-
|
|
|
12
|
-
forgecad container (Fastify / Node.js)
|
|
13
|
-
|--- serves SPA (Vite-built React frontend)
|
|
14
|
-
|--- API routes (/api/*)
|
|
15
|
-
|
|
|
16
|
-
PostgreSQL
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
**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.
|
|
20
|
-
|
|
21
|
-
**Server:** Fastify with Drizzle ORM for database access. Serves both the static SPA bundle and the `/api/*` JSON endpoints from a single process.
|
|
22
|
-
|
|
23
|
-
**Database:** PostgreSQL for user accounts, project metadata, and application state.
|
|
24
|
-
|
|
25
|
-
**File storage:** User project files are stored on disk (Docker volume `project-data`), not in the database.
|
|
26
|
-
|
|
27
|
-
## Docker Compose Configurations
|
|
28
|
-
|
|
29
|
-
Two compose files support different deployment targets:
|
|
30
|
-
|
|
31
|
-
| File | Services | Use case |
|
|
32
|
-
|------|----------|----------|
|
|
33
|
-
| `docker-compose.yml` | Caddy + ForgeCAD + PostgreSQL + Redis | Full self-hosted deployment with built-in TLS |
|
|
34
|
-
| `docker-compose.coolify.yml` | ForgeCAD + PostgreSQL | Coolify-managed deployment (Traefik handles TLS) |
|
|
35
|
-
|
|
36
|
-
Production uses `docker-compose.coolify.yml`. Coolify auto-deploys on push to the `mainline` branch.
|
|
37
|
-
|
|
38
|
-
## Authentication
|
|
39
|
-
|
|
40
|
-
JWT-based auth with token rotation:
|
|
41
|
-
|
|
42
|
-
- **Access tokens:** 15-minute expiry
|
|
43
|
-
- **Refresh tokens:** 7-day expiry, rotated on use
|
|
44
|
-
|
|
45
|
-
See [auth.md](auth.md) for details on OAuth providers, token flow, and session management.
|
|
46
|
-
|
|
47
|
-
## URL Routes
|
|
48
|
-
|
|
49
|
-
| Route | Access | Purpose |
|
|
50
|
-
|-------|--------|---------|
|
|
51
|
-
| `/` | Public | Landing page |
|
|
52
|
-
| `/docs`, `/docs/:slug` | Public | API documentation |
|
|
53
|
-
| `/blog`, `/blog/:slug` | Public | Announcements and tutorials |
|
|
54
|
-
| `/app` | Auth-gated | The CAD editor |
|
|
55
|
-
| `/app?gist=<id>` | Public preview | Shared model from GitHub Gist (read-only until login) |
|
|
56
|
-
| `/app?url=<url>` | Public preview | Shared model from external URL |
|
|
57
|
-
| `/app#code/<file>/<compressed>` | Public preview | Shared model with inline code |
|
|
58
|
-
| `/app?embed=1` | Public | Embed-only viewport (no chrome, for iframes) |
|
|
59
|
-
| `/m/:shareId` | Public | Published model preview (code + 3D viewport) |
|
|
60
|
-
| `/m/:shareId?embed=1` | Public | Published model embed (viewport only) |
|
|
61
|
-
| `/admin` | Admin role | User management, system health, audit log |
|
|
62
|
-
| `/auth/callback/*` | Public | OAuth callback handlers |
|
|
63
|
-
|
|
64
|
-
## Security
|
|
65
|
-
|
|
66
|
-
- **Rate limiting:** 100 requests/min per IP; 10 auth attempts per 15 minutes
|
|
67
|
-
- **CORS:** Origin whitelist (no wildcard)
|
|
68
|
-
- **CSP:** Content Security Policy headers on all responses
|
|
69
|
-
- **Path traversal:** Server-side validation on all file access paths
|
|
70
|
-
|
|
71
|
-
## Related Documentation
|
|
72
|
-
|
|
73
|
-
- [auth.md](auth.md) -- Authentication and authorization
|
|
74
|
-
- [projects.md](projects.md) -- Project storage and management
|
|
75
|
-
- [sharing.md](sharing.md) -- Model sharing and embeds
|
|
76
|
-
- [admin.md](admin.md) -- Admin panel
|
|
77
|
-
- [email.md](email.md) -- Transactional email
|
|
78
|
-
- [../project/deployment.md](../project/deployment.md) -- Environment variables and deployment setup
|
|
79
|
-
- [../project/runbook.md](../project/runbook.md) -- Operational procedures and troubleshooting
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# Authentication
|
|
2
|
-
|
|
3
|
-
ForgeCAD uses JWT-based authentication with refresh token rotation. Tokens are delivered via `httpOnly` cookies -- never exposed to client-side JavaScript.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Token Architecture
|
|
8
|
-
|
|
9
|
-
- **Access token**: JWT signed with HMAC (HS256). Default lifetime: 15 minutes.
|
|
10
|
-
- **Refresh token**: Opaque token stored in the database. Default lifetime: 7 days.
|
|
11
|
-
- **Rotation**: Each refresh request issues a new refresh token and invalidates the old one. If a previously-used refresh token is replayed, all sessions for that user are revoked (replay detection).
|
|
12
|
-
|
|
13
|
-
Both tokens are set as `httpOnly`, `Secure`, `SameSite=Strict` cookies.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## User Roles
|
|
18
|
-
|
|
19
|
-
| Role | Access |
|
|
20
|
-
|-------|--------|
|
|
21
|
-
| `user` | Standard account, can use `/app` and all editor features |
|
|
22
|
-
| `admin` | Full access including `/admin` dashboard |
|
|
23
|
-
|
|
24
|
-
Middleware:
|
|
25
|
-
|
|
26
|
-
- `requireAuth` -- extracts the user from the JWT access token. Returns 401 if the token is missing or expired.
|
|
27
|
-
- `requireAdmin` -- calls `requireAuth`, then checks `role === 'admin'`. Returns 403 otherwise.
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Registration and Login
|
|
32
|
-
|
|
33
|
-
1. **Register** (`POST /api/auth/register`) -- accepts email and password. Creates the account and sends a verification email. See [email delivery](email.md) for delivery configuration.
|
|
34
|
-
2. **Verify email** (`GET /api/auth/verify-email`) -- confirms the address using the token from the email link.
|
|
35
|
-
3. **Login** (`POST /api/auth/login`) -- validates credentials, sets access and refresh token cookies.
|
|
36
|
-
|
|
37
|
-
Password reset:
|
|
38
|
-
|
|
39
|
-
1. `POST /api/auth/forgot-password` -- sends a reset email with a single-use token (1-hour expiry).
|
|
40
|
-
2. `POST /api/auth/reset-password` -- applies the new password using the reset token.
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## OAuth
|
|
45
|
-
|
|
46
|
-
GitHub and Google are supported as optional OAuth providers. They are enabled by setting the corresponding environment variables (see below). When not configured, the provider is omitted from the login UI.
|
|
47
|
-
|
|
48
|
-
- `GET /api/auth/providers` -- returns the list of configured OAuth providers and their authorization URLs.
|
|
49
|
-
- `POST /api/auth/callback/:provider` -- handles the OAuth redirect, creates or links the account, and sets token cookies.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## API Endpoints
|
|
54
|
-
|
|
55
|
-
| Method | Route | Purpose |
|
|
56
|
-
|--------|-------|---------|
|
|
57
|
-
| `POST` | `/api/auth/register` | Create account |
|
|
58
|
-
| `POST` | `/api/auth/login` | Login (sets cookies) |
|
|
59
|
-
| `POST` | `/api/auth/refresh` | Rotate refresh token, issue new access token |
|
|
60
|
-
| `POST` | `/api/auth/logout` | Clear session cookies, invalidate refresh token |
|
|
61
|
-
| `GET` | `/api/auth/session` | Return current user info (requires valid access token) |
|
|
62
|
-
| `GET` | `/api/auth/providers` | List available OAuth providers |
|
|
63
|
-
| `POST` | `/api/auth/callback/:provider` | OAuth callback |
|
|
64
|
-
| `POST` | `/api/auth/forgot-password` | Request password reset email |
|
|
65
|
-
| `POST` | `/api/auth/reset-password` | Apply password reset |
|
|
66
|
-
| `GET` | `/api/auth/verify-email` | Confirm email address |
|
|
67
|
-
| `POST` | `/api/auth/resend-verification` | Resend verification email |
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## Rate Limiting
|
|
72
|
-
|
|
73
|
-
Auth endpoints are rate-limited to **10 requests per 15 minutes per IP**. This applies to login, registration, password reset, and token refresh. Exceeding the limit returns HTTP 429.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Environment Variables
|
|
78
|
-
|
|
79
|
-
| Variable | Required | Default | Purpose |
|
|
80
|
-
|----------|----------|---------|---------|
|
|
81
|
-
| `FORGE_JWT_SECRET` | Yes | -- | HMAC signing key for JWT access tokens |
|
|
82
|
-
| `FORGE_JWT_ACCESS_EXPIRES` | No | `15m` | Access token lifetime (e.g. `15m`, `1h`) |
|
|
83
|
-
| `FORGE_JWT_REFRESH_EXPIRES` | No | `7d` | Refresh token lifetime |
|
|
84
|
-
| `GITHUB_CLIENT_ID` | No | -- | GitHub OAuth application ID |
|
|
85
|
-
| `GITHUB_CLIENT_SECRET` | No | -- | GitHub OAuth secret |
|
|
86
|
-
| `GOOGLE_CLIENT_ID` | No | -- | Google OAuth client ID |
|
|
87
|
-
| `GOOGLE_CLIENT_SECRET` | No | -- | Google OAuth secret |
|
|
88
|
-
| `FORGE_ALLOWED_ORIGINS` | No | `localhost` | Comma-separated list of allowed CORS origins |
|
|
89
|
-
|
|
90
|
-
See [deployment.md](../project/deployment.md) for the full environment variable reference.
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## Troubleshooting
|
|
95
|
-
|
|
96
|
-
**500 on login** -- `FORGE_JWT_SECRET` is not set. The server cannot sign tokens without it.
|
|
97
|
-
|
|
98
|
-
**401 on authenticated endpoints** -- the access token has expired. The client should call `POST /api/auth/refresh` to obtain a new one. If refresh also returns 401, the session has expired entirely and the user must log in again.
|
|
99
|
-
|
|
100
|
-
**CORS errors** -- `FORGE_ALLOWED_ORIGINS` does not include the frontend's origin. Add the correct domain (e.g. `https://forgecad.io`).
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## Source Files
|
|
105
|
-
|
|
106
|
-
| File | Purpose |
|
|
107
|
-
|------|---------|
|
|
108
|
-
| `server/routes/auth.ts` | All auth route handlers |
|
|
109
|
-
| `server/middleware/auth.ts` | `requireAuth` and `requireAdmin` middleware |
|
|
110
|
-
| `server/db/schema.ts` | User and refresh token table definitions |
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# Email Delivery
|
|
2
|
-
|
|
3
|
-
ForgeCAD uses [Resend](https://resend.com) for transactional email. The free tier covers 3,000 emails/month and 100/day — more than enough for early production.
|
|
4
|
-
|
|
5
|
-
Emails are sent for:
|
|
6
|
-
- Email address verification (on register + resend)
|
|
7
|
-
- Password reset links
|
|
8
|
-
|
|
9
|
-
See [auth.md](auth.md) for the authentication flows that trigger these emails.
|
|
10
|
-
|
|
11
|
-
## Environment Variables
|
|
12
|
-
|
|
13
|
-
| Variable | Required | Default | Notes |
|
|
14
|
-
|---|---|---|---|
|
|
15
|
-
| `RESEND_API_KEY` | Yes (in prod) | — | From Resend dashboard |
|
|
16
|
-
| `FORGE_EMAIL_FROM` | No | `ForgeCAD <noreply@forgecad.io>` | Must match a verified domain |
|
|
17
|
-
| `FORGE_APP_URL` | No | `http://localhost:5173` | Base URL used in email links |
|
|
18
|
-
|
|
19
|
-
Without `RESEND_API_KEY`, emails fall back to `console.log` — fine for local dev.
|
|
20
|
-
|
|
21
|
-
See [../project/deployment.md](../project/deployment.md) for the full environment variable reference.
|
|
22
|
-
|
|
23
|
-
## First-Time Setup
|
|
24
|
-
|
|
25
|
-
### 1. Create a Resend account
|
|
26
|
-
|
|
27
|
-
Sign up at [resend.com](https://resend.com). No credit card required for the free tier.
|
|
28
|
-
|
|
29
|
-
### 2. Verify your sending domain
|
|
30
|
-
|
|
31
|
-
In the Resend dashboard: **Domains** > **Add Domain** > enter `forgecad.io`.
|
|
32
|
-
|
|
33
|
-
Resend will show you DNS records to add (typically a few TXT/MX entries). If your DNS is on **Cloudflare**, Resend can auto-configure the records — just connect your Cloudflare account in the Resend dashboard. It takes a few minutes to propagate.
|
|
34
|
-
|
|
35
|
-
### 3. Create an API key
|
|
36
|
-
|
|
37
|
-
In the Resend dashboard: **API Keys** > **Create API Key**.
|
|
38
|
-
|
|
39
|
-
Use **Send access only** — no need for full access on the server.
|
|
40
|
-
|
|
41
|
-
### 4. Set env vars
|
|
42
|
-
|
|
43
|
-
In Coolify (production) or your local `.env`:
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
RESEND_API_KEY=re_...
|
|
47
|
-
FORGE_EMAIL_FROM=ForgeCAD <noreply@forgecad.io>
|
|
48
|
-
FORGE_APP_URL=https://forgecad.io
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Local Development
|
|
52
|
-
|
|
53
|
-
Leave `RESEND_API_KEY` unset. The `send()` function detects the missing key and logs the email content and token to the console instead of sending. Token links are printed so you can copy them directly into the browser.
|
|
54
|
-
|
|
55
|
-
## Code Layout
|
|
56
|
-
|
|
57
|
-
| File | Purpose |
|
|
58
|
-
|---|---|
|
|
59
|
-
| `server/email/send.ts` | Resend client + `sendVerificationEmail` / `sendPasswordResetEmail` helpers |
|
|
60
|
-
| `server/env.ts` | `resendApiKey`, `emailFrom`, `appUrl` env vars |
|
|
61
|
-
| `server/routes/auth.ts` | Calls into `send.ts` from register, forgot-password, resend-verification routes |
|
|
62
|
-
|
|
63
|
-
## Adding a New Email Type
|
|
64
|
-
|
|
65
|
-
1. Add a new `send*Email(to, ...)` export in `server/email/send.ts`
|
|
66
|
-
2. Call it from the relevant route in `server/routes/auth.ts` (or wherever)
|
|
67
|
-
3. No new infrastructure needed — same Resend client and `send()` helper
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# Projects and File Management
|
|
2
|
-
|
|
3
|
-
Projects are the top-level container for user work in ForgeCAD. Each project groups related scripts, notebooks, and output files under a single name with access controls.
|
|
4
|
-
|
|
5
|
-
## Project Model
|
|
6
|
-
|
|
7
|
-
Each project has:
|
|
8
|
-
|
|
9
|
-
| Field | Type | Description |
|
|
10
|
-
|-------|------|-------------|
|
|
11
|
-
| `id` | UUID | Primary key |
|
|
12
|
-
| `slug` | string | URL-safe identifier, unique per owner |
|
|
13
|
-
| `name` | string | Display name |
|
|
14
|
-
| `visibility` | `private` \| `public` | Controls unauthenticated access |
|
|
15
|
-
| `ownerId` | UUID | Foreign key to the user who created the project |
|
|
16
|
-
|
|
17
|
-
Source: `server/db/schema.ts`
|
|
18
|
-
|
|
19
|
-
## Member Roles
|
|
20
|
-
|
|
21
|
-
Projects support collaborative access through membership:
|
|
22
|
-
|
|
23
|
-
| Role | Capabilities |
|
|
24
|
-
|------|-------------|
|
|
25
|
-
| **owner** | Full control: rename, delete, manage members, read/write files |
|
|
26
|
-
| **editor** | Read and write files (save, delete, mkdir) |
|
|
27
|
-
| **viewer** | Read files, watch for changes |
|
|
28
|
-
|
|
29
|
-
The owner is always the user who created the project. Ownership cannot be transferred through the membership API.
|
|
30
|
-
|
|
31
|
-
## File Storage
|
|
32
|
-
|
|
33
|
-
Project files are stored on disk, not in the database. The storage root is configured by the `FORGE_STORAGE_ROOT` environment variable (default: `/data/projects` in Docker). Each project gets an isolated directory under this root.
|
|
34
|
-
|
|
35
|
-
See [deployment.md](../project/deployment.md) for Docker volume and storage configuration.
|
|
36
|
-
|
|
37
|
-
### Supported File Types
|
|
38
|
-
|
|
39
|
-
| Extension | Purpose |
|
|
40
|
-
|-----------|---------|
|
|
41
|
-
| `.forge.js` | ForgeCAD model scripts |
|
|
42
|
-
| `.forge-notebook.json` | Interactive notebooks |
|
|
43
|
-
| `.js` | Utility modules (imported by scripts) |
|
|
44
|
-
| `.svg` | Vector assets |
|
|
45
|
-
| `.stl`, `.3mf` | Exported mesh files |
|
|
46
|
-
|
|
47
|
-
### Path Traversal Protection
|
|
48
|
-
|
|
49
|
-
All file operations validate resolved paths against the project's storage directory. Any path that escapes the project root (via `..` segments or symlinks) is rejected. This check runs in `server/storage.ts` before any read or write reaches the filesystem.
|
|
50
|
-
|
|
51
|
-
### Storage Quotas
|
|
52
|
-
|
|
53
|
-
Each user has a storage quota tracked via `storage_used_bytes` on the user record.
|
|
54
|
-
|
|
55
|
-
| Limit | Value |
|
|
56
|
-
|-------|-------|
|
|
57
|
-
| Storage per user | 50 MB |
|
|
58
|
-
| Projects per user | 20 |
|
|
59
|
-
|
|
60
|
-
The quota is checked before every file write. Storage counters are adjusted atomically and clamped to zero to prevent negative drift from race conditions or deleted files.
|
|
61
|
-
|
|
62
|
-
Source: `server/quotas.ts`
|
|
63
|
-
|
|
64
|
-
## Real-Time File Watching
|
|
65
|
-
|
|
66
|
-
Clients can subscribe to file changes via Server-Sent Events:
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
GET /api/projects/:id/watch
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
This endpoint streams events whenever files in the project are created, modified, or deleted. The editor uses this to stay in sync when multiple clients have the same project open.
|
|
73
|
-
|
|
74
|
-
## API Endpoints
|
|
75
|
-
|
|
76
|
-
### Projects
|
|
77
|
-
|
|
78
|
-
| Method | Route | Min Role | Purpose |
|
|
79
|
-
|--------|-------|----------|---------|
|
|
80
|
-
| GET | `/api/projects` | -- | List the authenticated user's projects |
|
|
81
|
-
| POST | `/api/projects` | -- | Create a new project |
|
|
82
|
-
| GET | `/api/projects/:id` | viewer | Get project details |
|
|
83
|
-
| PATCH | `/api/projects/:id` | owner | Update project name, slug, or visibility |
|
|
84
|
-
| DELETE | `/api/projects/:id` | owner | Delete project and all its files |
|
|
85
|
-
|
|
86
|
-
### Members
|
|
87
|
-
|
|
88
|
-
| Method | Route | Min Role | Purpose |
|
|
89
|
-
|--------|-------|----------|---------|
|
|
90
|
-
| GET | `/api/projects/:id/members` | viewer | List project members |
|
|
91
|
-
| POST | `/api/projects/:id/members` | owner | Add a member by user ID |
|
|
92
|
-
| PATCH | `/api/projects/:id/members/:userId` | owner | Change a member's role |
|
|
93
|
-
| DELETE | `/api/projects/:id/members/:userId` | owner | Remove a member |
|
|
94
|
-
|
|
95
|
-
### Files
|
|
96
|
-
|
|
97
|
-
| Method | Route | Min Role | Purpose |
|
|
98
|
-
|--------|-------|----------|---------|
|
|
99
|
-
| GET | `/api/projects/:id/watch` | viewer | SSE stream of file changes |
|
|
100
|
-
| POST | `/api/projects/:id/save` | editor | Write file contents to disk |
|
|
101
|
-
| POST | `/api/projects/:id/delete` | editor | Delete a file |
|
|
102
|
-
| POST | `/api/projects/:id/mkdir` | editor | Create a directory |
|
|
103
|
-
| GET | `/api/projects/:id/read-binary` | viewer | Download a binary file (meshes, assets) |
|
|
104
|
-
|
|
105
|
-
Source: `server/routes/projects.ts`, `server/routes/files.ts`
|
|
106
|
-
|
|
107
|
-
## Related
|
|
108
|
-
|
|
109
|
-
- [Architecture](architecture.md) -- system overview and deployment topology
|
|
110
|
-
- [Sharing](sharing.md) -- model publishing and public URLs
|
|
111
|
-
- [Deployment](../project/deployment.md) -- storage volume configuration, Docker setup
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# Model Sharing and Publishing
|
|
2
|
-
|
|
3
|
-
ForgeCAD supports multiple ways to share models. Two categories exist: **server-backed publishing** (persistent URLs, requires auth to publish) and **client-side sharing** (no server needed, data encoded in the URL).
|
|
4
|
-
|
|
5
|
-
See [architecture.md](architecture.md) for the full URL route table.
|
|
6
|
-
|
|
7
|
-
## Server-backed publishing
|
|
8
|
-
|
|
9
|
-
Authenticated users can publish models to get a stable, public URL. Published models are stored in the `shared_files` database table.
|
|
10
|
-
|
|
11
|
-
### Database schema (`shared_files`)
|
|
12
|
-
|
|
13
|
-
| Column | Type | Notes |
|
|
14
|
-
|--------|------|-------|
|
|
15
|
-
| `id` | uuid | Primary key |
|
|
16
|
-
| `share_id` | text | Unique, 10-char nanoid -- the URL identifier |
|
|
17
|
-
| `owner_id` | uuid | FK to `users.id`, cascade delete |
|
|
18
|
-
| `filename` | text | Original filename |
|
|
19
|
-
| `code` | text | Full source code |
|
|
20
|
-
| `title` | text | Optional display title |
|
|
21
|
-
| `created_at` | timestamptz | Auto-set |
|
|
22
|
-
| `updated_at` | timestamptz | Auto-set, updated on re-publish |
|
|
23
|
-
|
|
24
|
-
### Public URLs
|
|
25
|
-
|
|
26
|
-
| URL | Behavior |
|
|
27
|
-
|-----|----------|
|
|
28
|
-
| `/m/:shareId` | Full model preview: source code panel + 3D viewport |
|
|
29
|
-
| `/m/:shareId?embed=1` | Viewport only, no UI chrome -- designed for iframe embeds |
|
|
30
|
-
|
|
31
|
-
No authentication is required to view a published model.
|
|
32
|
-
|
|
33
|
-
### API endpoints
|
|
34
|
-
|
|
35
|
-
| Method | Route | Auth | Purpose |
|
|
36
|
-
|--------|-------|------|---------|
|
|
37
|
-
| GET | `/api/shares/:shareId` | None | Fetch a published model (code, title, author name, timestamps) |
|
|
38
|
-
| GET | `/api/shares` | Required | List the authenticated user's published models |
|
|
39
|
-
| POST | `/api/shares` | Required | Publish a new model or update an existing one |
|
|
40
|
-
| DELETE | `/api/shares/:shareId` | Required (owner or admin) | Unpublish a model |
|
|
41
|
-
|
|
42
|
-
### Publish behavior
|
|
43
|
-
|
|
44
|
-
- If the user already has a published model with the same filename, `POST /api/shares` updates the existing share rather than creating a new one. The `share_id` is preserved so existing links remain valid.
|
|
45
|
-
- Each user may publish up to **100** models. Exceeding this limit returns `403` with `{ error: "Share limit reached" }`.
|
|
46
|
-
- All publish, update, and delete operations are recorded in the `audit_log` table.
|
|
47
|
-
|
|
48
|
-
### Audit trail
|
|
49
|
-
|
|
50
|
-
Every share mutation writes to `audit_log` with one of these actions:
|
|
51
|
-
- `share.publish` -- new model published
|
|
52
|
-
- `share.update` -- existing model code/title updated
|
|
53
|
-
- `share.delete` -- model unpublished
|
|
54
|
-
|
|
55
|
-
## Client-side sharing (no server)
|
|
56
|
-
|
|
57
|
-
These methods encode model data directly into the URL. They work without authentication and without the hosted backend.
|
|
58
|
-
|
|
59
|
-
### Inline code (hash fragment)
|
|
60
|
-
|
|
61
|
-
Format: `/app#code/<filename>/<lz-compressed-code>`
|
|
62
|
-
|
|
63
|
-
The code is compressed with `lz-string` (`compressToEncodedURIComponent`) and placed in the URL hash. Since hash fragments are not sent to the server, this works entirely client-side. Encoding and decoding logic lives in `src/share.ts`.
|
|
64
|
-
|
|
65
|
-
### Multi-file bundles (hash fragment)
|
|
66
|
-
|
|
67
|
-
Format: `/app#bundle/<lz-compressed-packed>`
|
|
68
|
-
|
|
69
|
-
Packs multiple files into a single compressed payload using null-byte separators: `entry\0filename1\0code1\0filename2\0code2...`. Used when a model has import dependencies.
|
|
70
|
-
|
|
71
|
-
### Gist sharing
|
|
72
|
-
|
|
73
|
-
Format: `/app?gist=<github-gist-id>`
|
|
74
|
-
|
|
75
|
-
Fetches code from a public GitHub Gist at load time.
|
|
76
|
-
|
|
77
|
-
### URL sharing
|
|
78
|
-
|
|
79
|
-
Format: `/app?url=<external-url>`
|
|
80
|
-
|
|
81
|
-
Fetches code from any publicly accessible URL at load time.
|
|
82
|
-
|
|
83
|
-
## Source files
|
|
84
|
-
|
|
85
|
-
| File | Role |
|
|
86
|
-
|------|------|
|
|
87
|
-
| `server/routes/shares.ts` | API route handlers for publish/unpublish/list |
|
|
88
|
-
| `server/db/schema.ts` | `sharedFiles` table definition and types |
|
|
89
|
-
| `src/pages/SharedModelPage.tsx` | Frontend page rendering `/m/:shareId` |
|
|
90
|
-
| `src/share.ts` | Client-side encode/decode for hash-based sharing |
|