fluidcad 0.0.34 → 0.0.36
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 +69 -0
- package/bin/commands/login.js +148 -0
- package/bin/commands/mcp.js +3 -2
- package/bin/commands/pack.js +49 -0
- package/bin/commands/publish.js +231 -0
- package/bin/fluidcad.js +6 -0
- package/bin/lib/api-client.js +48 -0
- package/bin/lib/browser.js +16 -0
- package/bin/lib/config.js +39 -0
- package/bin/lib/model-config.js +61 -0
- package/bin/lib/prompt.js +97 -0
- package/bin/lib/workspace.js +57 -0
- package/lib/dist/common/shape-factory.d.ts +2 -1
- package/lib/dist/common/shape-factory.js +4 -0
- package/lib/dist/common/transformable-primitive.d.ts +6 -5
- package/lib/dist/common/transformable-primitive.js +8 -7
- package/lib/dist/common/vertex.js +0 -1
- package/lib/dist/core/2d/aline.d.ts +4 -3
- package/lib/dist/core/2d/aline.js +3 -2
- package/lib/dist/core/2d/arc.d.ts +3 -2
- package/lib/dist/core/2d/arc.js +4 -3
- package/lib/dist/core/2d/bezier.d.ts +8 -6
- package/lib/dist/core/2d/circle.d.ts +4 -3
- package/lib/dist/core/2d/circle.js +3 -2
- package/lib/dist/core/2d/ellipse.d.ts +5 -4
- package/lib/dist/core/2d/ellipse.js +5 -4
- package/lib/dist/core/2d/hline.d.ts +4 -3
- package/lib/dist/core/2d/hline.js +5 -3
- package/lib/dist/core/2d/line.js +1 -0
- package/lib/dist/core/2d/offset.d.ts +3 -2
- package/lib/dist/core/2d/offset.js +6 -5
- package/lib/dist/core/2d/polygon.d.ts +5 -4
- package/lib/dist/core/2d/polygon.js +10 -9
- package/lib/dist/core/2d/rect.d.ts +4 -3
- package/lib/dist/core/2d/rect.js +10 -9
- package/lib/dist/core/2d/slot.d.ts +14 -6
- package/lib/dist/core/2d/slot.js +19 -8
- package/lib/dist/core/2d/vline.d.ts +4 -3
- package/lib/dist/core/2d/vline.js +5 -3
- package/lib/dist/core/chamfer.d.ts +5 -4
- package/lib/dist/core/chamfer.js +7 -6
- package/lib/dist/core/color.d.ts +3 -2
- package/lib/dist/core/color.js +2 -1
- package/lib/dist/core/cut.d.ts +4 -3
- package/lib/dist/core/cut.js +5 -4
- package/lib/dist/core/cylinder.d.ts +2 -1
- package/lib/dist/core/cylinder.js +2 -1
- package/lib/dist/core/draft.d.ts +3 -2
- package/lib/dist/core/draft.js +3 -2
- package/lib/dist/core/extrude.d.ts +4 -3
- package/lib/dist/core/extrude.js +5 -4
- package/lib/dist/core/fillet.d.ts +5 -4
- package/lib/dist/core/fillet.js +6 -5
- package/lib/dist/core/index.d.ts +1 -0
- package/lib/dist/core/index.js +1 -0
- package/lib/dist/core/interfaces.d.ts +25 -24
- package/lib/dist/core/param.d.ts +74 -0
- package/lib/dist/core/param.js +147 -0
- package/lib/dist/core/repeat.d.ts +2 -1
- package/lib/dist/core/repeat.js +10 -8
- package/lib/dist/core/revolve.d.ts +2 -1
- package/lib/dist/core/revolve.js +3 -2
- package/lib/dist/core/rib.d.ts +3 -2
- package/lib/dist/core/rib.js +6 -2
- package/lib/dist/core/rotate.d.ts +5 -4
- package/lib/dist/core/rotate.js +4 -3
- package/lib/dist/core/shell.d.ts +3 -2
- package/lib/dist/core/shell.js +3 -2
- package/lib/dist/core/sphere.d.ts +3 -2
- package/lib/dist/core/sphere.js +2 -1
- package/lib/dist/core/translate.d.ts +7 -6
- package/lib/dist/core/translate.js +6 -5
- package/lib/dist/features/2d/arc.js +5 -5
- package/lib/dist/features/2d/bezier.js +16 -16
- package/lib/dist/features/2d/circle.js +4 -0
- package/lib/dist/features/2d/ellipse.js +4 -0
- package/lib/dist/features/2d/hline.d.ts +3 -0
- package/lib/dist/features/2d/hline.js +9 -2
- package/lib/dist/features/2d/line.d.ts +3 -0
- package/lib/dist/features/2d/line.js +11 -3
- package/lib/dist/features/2d/sketch.js +5 -1
- package/lib/dist/features/2d/slot.d.ts +5 -0
- package/lib/dist/features/2d/slot.js +52 -7
- package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
- package/lib/dist/features/2d/tarc-to-point.js +3 -0
- package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
- package/lib/dist/features/2d/tarc.js +3 -0
- package/lib/dist/features/2d/vline.d.ts +3 -0
- package/lib/dist/features/2d/vline.js +9 -2
- package/lib/dist/features/copy-circular.d.ts +4 -3
- package/lib/dist/features/copy-circular.js +16 -9
- package/lib/dist/features/copy-circular2d.js +16 -9
- package/lib/dist/features/copy-linear.d.ts +4 -3
- package/lib/dist/features/copy-linear.js +18 -12
- package/lib/dist/features/copy-linear2d.js +18 -12
- package/lib/dist/features/extrude-base.d.ts +4 -3
- package/lib/dist/features/extrude-base.js +10 -3
- package/lib/dist/features/mirror-shape2d.js +2 -2
- package/lib/dist/features/repeat-base.d.ts +13 -0
- package/lib/dist/features/repeat-base.js +21 -0
- package/lib/dist/features/repeat-circular.d.ts +6 -5
- package/lib/dist/features/repeat-circular.js +3 -6
- package/lib/dist/features/repeat-linear.d.ts +7 -7
- package/lib/dist/features/repeat-linear.js +3 -6
- package/lib/dist/index.d.ts +5 -0
- package/lib/dist/index.js +8 -1
- package/lib/dist/io/file-import.d.ts +7 -0
- package/lib/dist/io/file-import.js +30 -10
- package/lib/dist/math/lazy-matrix.d.ts +5 -0
- package/lib/dist/math/lazy-matrix.js +78 -10
- package/lib/dist/oc/boolean-ops.d.ts +2 -2
- package/lib/dist/param-registry.d.ts +34 -0
- package/lib/dist/param-registry.js +60 -0
- package/lib/dist/rendering/mesh-builder.js +2 -1
- package/lib/dist/tests/features/copy-circular.test.js +1 -1
- package/lib/dist/tests/features/copy-linear.test.js +10 -10
- package/lib/dist/tests/features/repeat-user-repro-cache.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-user-repro-cache.test.js +97 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/api/bezier.md +10 -11
- package/llm-docs/api/index.json +1 -1
- package/llm-docs/api/types/arc-points.md +2 -2
- package/llm-docs/api/types/cut.md +10 -10
- package/llm-docs/api/types/extrude.md +10 -10
- package/llm-docs/api/types/loft.md +6 -6
- package/llm-docs/api/types/revolve.md +6 -6
- package/llm-docs/api/types/rib.md +2 -2
- package/llm-docs/api/types/slot.md +2 -2
- package/llm-docs/api/types/sweep.md +10 -10
- package/llm-docs/api/types/transformable.md +14 -14
- package/llm-docs/index.json +12 -12
- package/mcp/dist/client.d.ts +1 -0
- package/mcp/dist/client.js +8 -1
- package/mcp/dist/server.js +14 -1
- package/mcp/dist/tools/engine.d.ts +16 -0
- package/mcp/dist/tools/engine.js +45 -0
- package/package.json +9 -3
- package/server/dist/api.d.ts +37 -0
- package/server/dist/api.js +44 -0
- package/server/dist/code-editor.d.ts +64 -0
- package/server/dist/code-editor.js +520 -2
- package/server/dist/fluidcad-server.d.ts +87 -1
- package/server/dist/fluidcad-server.js +254 -88
- package/server/dist/host/blocked-imports.d.ts +8 -0
- package/server/dist/host/blocked-imports.js +30 -0
- package/server/dist/{vite-manager.d.ts → host/local-scene-host.d.ts} +3 -1
- package/server/dist/{vite-manager.js → host/local-scene-host.js} +6 -26
- package/server/dist/host/scene-host.d.ts +19 -0
- package/server/dist/host/scene-host.js +1 -0
- package/server/dist/index.js +24 -117
- package/server/dist/model-package/capture-params.d.ts +19 -0
- package/server/dist/model-package/capture-params.js +42 -0
- package/server/dist/model-package/pack.d.ts +23 -0
- package/server/dist/model-package/pack.js +230 -0
- package/server/dist/model-package/types.d.ts +79 -0
- package/server/dist/model-package/types.js +17 -0
- package/server/dist/routes/hit-test.d.ts +3 -0
- package/server/dist/routes/hit-test.js +17 -0
- package/server/dist/routes/pack.d.ts +10 -0
- package/server/dist/routes/pack.js +47 -0
- package/server/dist/routes/params.d.ts +3 -0
- package/server/dist/routes/params.js +75 -0
- package/server/dist/routes/sketch-edits.d.ts +3 -0
- package/server/dist/routes/sketch-edits.js +542 -0
- package/server/dist/routes/timeline.d.ts +3 -0
- package/server/dist/routes/timeline.js +49 -0
- package/server/dist/server-core.d.ts +53 -0
- package/server/dist/server-core.js +147 -0
- package/server/dist/ws-protocol.d.ts +101 -2
- package/ui/dist/assets/index-CDJmUpFI.css +2 -0
- package/ui/dist/assets/index-MRqwG9Vh.js +5417 -0
- package/ui/dist/index.html +2 -2
- package/server/dist/routes/actions.d.ts +0 -3
- package/server/dist/routes/actions.js +0 -309
- package/ui/dist/assets/index-BdqrMDRu.js +0 -4946
- package/ui/dist/assets/index-DR7c2Qk9.css +0 -2
package/server/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
1
|
import fs from 'fs';
|
|
3
2
|
import http from 'http';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import express from 'express';
|
|
6
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
5
|
import { FluidCadServer } from "./fluidcad-server.js";
|
|
6
|
+
import { createServerCore } from "./server-core.js";
|
|
8
7
|
import { createPropertiesRouter } from "./routes/properties.js";
|
|
9
|
-
import {
|
|
8
|
+
import { createParamsRouter } from "./routes/params.js";
|
|
9
|
+
import { createHitTestRouter } from "./routes/hit-test.js";
|
|
10
|
+
import { createTimelineRouter } from "./routes/timeline.js";
|
|
11
|
+
import { createSketchEditsRouter } from "./routes/sketch-edits.js";
|
|
10
12
|
import { createExportRouter } from "./routes/export.js";
|
|
11
13
|
import { createScreenshotRouter } from "./routes/screenshot.js";
|
|
12
14
|
import { createPreferencesRouter } from "./routes/preferences.js";
|
|
@@ -15,6 +17,7 @@ import { createSceneRouter } from "./routes/scene.js";
|
|
|
15
17
|
import { createEditorRouter, DirtyBufferState } from "./routes/editor.js";
|
|
16
18
|
import { createRenderRouter } from "./routes/render.js";
|
|
17
19
|
import { createLintRouter } from "./routes/lint.js";
|
|
20
|
+
import { createPackRouter } from "./routes/pack.js";
|
|
18
21
|
import { normalizePath } from "./normalize-path.js";
|
|
19
22
|
import { writeInstanceFile, deleteInstanceFile } from "./instance-file.js";
|
|
20
23
|
import { addInstance, removeInstance } from "./global-registry.js";
|
|
@@ -53,20 +56,32 @@ const fluidCadServer = new FluidCadServer();
|
|
|
53
56
|
const dirtyBufferState = new DirtyBufferState();
|
|
54
57
|
const app = express();
|
|
55
58
|
app.use(express.json({ limit: '50mb' }));
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// HTTP + WebSocket server (set up early so routes can reference its helpers)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
const httpServer = http.createServer(app);
|
|
63
|
+
const core = createServerCore(httpServer);
|
|
64
|
+
const broadcastToUI = core.broadcastToUI;
|
|
65
|
+
const requestScreenshot = core.requestScreenshot;
|
|
66
|
+
const getLastCameraState = core.getLastCameraState;
|
|
56
67
|
app.use('/api', createHealthRouter({
|
|
57
68
|
version: PACKAGE_VERSION,
|
|
58
69
|
workspacePath: WORKSPACE_PATH,
|
|
59
70
|
startedAt: STARTED_AT,
|
|
60
71
|
}));
|
|
61
72
|
app.use('/api', createPropertiesRouter(fluidCadServer));
|
|
62
|
-
app.use('/api',
|
|
73
|
+
app.use('/api', createParamsRouter(fluidCadServer, sendToExtension, broadcastToUI));
|
|
74
|
+
app.use('/api', createHitTestRouter(fluidCadServer));
|
|
75
|
+
app.use('/api', createTimelineRouter(fluidCadServer, sendToExtension, broadcastToUI));
|
|
76
|
+
app.use('/api', createSketchEditsRouter(fluidCadServer, sendToExtension, WORKSPACE_PATH));
|
|
63
77
|
app.use('/api', createExportRouter(fluidCadServer, WORKSPACE_PATH));
|
|
64
78
|
app.use('/api', createScreenshotRouter(requestScreenshot));
|
|
65
79
|
app.use('/api', createPreferencesRouter());
|
|
66
|
-
app.use('/api', createSceneRouter(fluidCadServer,
|
|
80
|
+
app.use('/api', createSceneRouter(fluidCadServer, getLastCameraState));
|
|
67
81
|
app.use('/api', createEditorRouter(dirtyBufferState));
|
|
68
82
|
app.use('/api', createRenderRouter((fileName, code) => runLiveRender(fileName, code)));
|
|
69
83
|
app.use('/api', createLintRouter());
|
|
84
|
+
app.use('/api', createPackRouter(fluidCadServer, WORKSPACE_PATH, PACKAGE_VERSION, getLastCameraState));
|
|
70
85
|
// Static files — serve UI build, with SPA fallback
|
|
71
86
|
app.use(express.static(UI_DIST, {
|
|
72
87
|
setHeaders(res, filePath) {
|
|
@@ -80,121 +95,12 @@ app.get('*splat', (_req, res) => {
|
|
|
80
95
|
res.sendFile(path.join(UI_DIST, 'index.html'));
|
|
81
96
|
});
|
|
82
97
|
// ---------------------------------------------------------------------------
|
|
83
|
-
// HTTP + WebSocket server
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
const httpServer = http.createServer(app);
|
|
86
|
-
const wss = new WebSocketServer({ server: httpServer });
|
|
87
|
-
const uiClients = new Set();
|
|
88
|
-
let lastSceneMessage = null;
|
|
89
|
-
let initCompleteMessage = null;
|
|
90
|
-
let lastCameraState = null;
|
|
91
|
-
function broadcastToUI(msg) {
|
|
92
|
-
const data = JSON.stringify(msg);
|
|
93
|
-
if (msg.type === 'scene-rendered') {
|
|
94
|
-
lastSceneMessage = data;
|
|
95
|
-
}
|
|
96
|
-
if (msg.type === 'init-complete') {
|
|
97
|
-
initCompleteMessage = data;
|
|
98
|
-
}
|
|
99
|
-
for (const client of uiClients) {
|
|
100
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
101
|
-
client.send(data);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
// Screenshot request/response coordination
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
const SCREENSHOT_TIMEOUT_MS = 10_000;
|
|
109
|
-
const pendingScreenshots = new Map();
|
|
110
|
-
function requestScreenshot(options) {
|
|
111
|
-
return new Promise((resolve, reject) => {
|
|
112
|
-
if (uiClients.size === 0) {
|
|
113
|
-
reject(new Error('No UI client connected.'));
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const requestId = crypto.randomUUID();
|
|
117
|
-
const timeout = setTimeout(() => {
|
|
118
|
-
pendingScreenshots.delete(requestId);
|
|
119
|
-
reject(new Error('Screenshot request timed out.'));
|
|
120
|
-
}, SCREENSHOT_TIMEOUT_MS);
|
|
121
|
-
pendingScreenshots.set(requestId, {
|
|
122
|
-
resolve(data) {
|
|
123
|
-
clearTimeout(timeout);
|
|
124
|
-
pendingScreenshots.delete(requestId);
|
|
125
|
-
resolve(data);
|
|
126
|
-
},
|
|
127
|
-
reject(err) {
|
|
128
|
-
clearTimeout(timeout);
|
|
129
|
-
pendingScreenshots.delete(requestId);
|
|
130
|
-
reject(err);
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
broadcastToUI({ type: 'take-screenshot', requestId, options });
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
function handleUIMessage(raw) {
|
|
137
|
-
let msg;
|
|
138
|
-
try {
|
|
139
|
-
msg = JSON.parse(raw);
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (msg.type === 'screenshot-result' && msg.requestId) {
|
|
145
|
-
const pending = pendingScreenshots.get(msg.requestId);
|
|
146
|
-
if (!pending) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (msg.success && msg.data) {
|
|
150
|
-
pending.resolve(Buffer.from(msg.data, 'base64'));
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
pending.reject(new Error(msg.error || 'Screenshot failed.'));
|
|
154
|
-
}
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (msg.type === 'camera-state') {
|
|
158
|
-
// Trust UI structure — we own both ends. Just shape-check arrays.
|
|
159
|
-
if (Array.isArray(msg.position) && msg.position.length === 3 &&
|
|
160
|
-
Array.isArray(msg.target) && msg.target.length === 3 &&
|
|
161
|
-
Array.isArray(msg.up) && msg.up.length === 3) {
|
|
162
|
-
lastCameraState = {
|
|
163
|
-
type: 'camera-state',
|
|
164
|
-
position: msg.position,
|
|
165
|
-
target: msg.target,
|
|
166
|
-
up: msg.up,
|
|
167
|
-
projection: msg.projection === 'perspective' ? 'perspective' : 'orthographic',
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// WebSocket connections
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
wss.on('connection', (ws) => {
|
|
176
|
-
uiClients.add(ws);
|
|
177
|
-
// Replay init-complete and last scene to newly connected UI client
|
|
178
|
-
if (initCompleteMessage) {
|
|
179
|
-
ws.send(initCompleteMessage);
|
|
180
|
-
}
|
|
181
|
-
if (lastSceneMessage) {
|
|
182
|
-
ws.send(lastSceneMessage);
|
|
183
|
-
}
|
|
184
|
-
ws.on('message', (data) => {
|
|
185
|
-
handleUIMessage(String(data));
|
|
186
|
-
});
|
|
187
|
-
ws.on('close', () => {
|
|
188
|
-
uiClients.delete(ws);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
98
|
// IPC message handling — extension host → server
|
|
193
99
|
// ---------------------------------------------------------------------------
|
|
194
100
|
let currentFile = null;
|
|
195
101
|
let renderVersion = 0;
|
|
196
102
|
const lastSceneByFile = new Map();
|
|
197
|
-
function emitSuccess(version, absPath, result, rollbackStop, breakpointHit) {
|
|
103
|
+
function emitSuccess(version, absPath, result, rollbackStop, breakpointHit, params) {
|
|
198
104
|
lastSceneByFile.set(absPath, { result, rollbackStop });
|
|
199
105
|
fluidCadServer.setCompileError(null);
|
|
200
106
|
sendToExtension({
|
|
@@ -209,6 +115,7 @@ function emitSuccess(version, absPath, result, rollbackStop, breakpointHit) {
|
|
|
209
115
|
absPath,
|
|
210
116
|
rollbackStop,
|
|
211
117
|
breakpointHit,
|
|
118
|
+
params,
|
|
212
119
|
});
|
|
213
120
|
broadcastToUI({ type: 'render-version', version, state: 'end', absPath });
|
|
214
121
|
}
|
|
@@ -277,7 +184,7 @@ async function runLiveRender(fileName, code) {
|
|
|
277
184
|
if (!data) {
|
|
278
185
|
return { state: 'no-scene-manager', version: myVersion, durationMs: Date.now() - startedAt };
|
|
279
186
|
}
|
|
280
|
-
emitSuccess(myVersion, data.absPath, data.result, data.rollbackStop, data.breakpointHit);
|
|
187
|
+
emitSuccess(myVersion, data.absPath, data.result, data.rollbackStop, data.breakpointHit, data.params);
|
|
281
188
|
return {
|
|
282
189
|
state: 'rendered',
|
|
283
190
|
version: myVersion,
|
|
@@ -312,7 +219,7 @@ async function handleExtensionMessage(msg) {
|
|
|
312
219
|
return;
|
|
313
220
|
}
|
|
314
221
|
if (data) {
|
|
315
|
-
emitSuccess(myVersion, data.absPath, data.result, data.rollbackStop, data.breakpointHit);
|
|
222
|
+
emitSuccess(myVersion, data.absPath, data.result, data.rollbackStop, data.breakpointHit, data.params);
|
|
316
223
|
}
|
|
317
224
|
}
|
|
318
225
|
catch (err) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ParamDefinition } from '../../../lib/dist/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Render a model once, headlessly, and return its full parameter schema.
|
|
4
|
+
*
|
|
5
|
+
* Param *definitions* (type, default, current value, constraints) only exist
|
|
6
|
+
* after the engine runs the model — the packer/bundler never executes user
|
|
7
|
+
* code, so a static manifest can carry override values at most. `fluidcad
|
|
8
|
+
* publish` calls this to capture the real schema and embeds it in the manifest
|
|
9
|
+
* (`paramDefinitions`), so the hub can build param forms without a live worker.
|
|
10
|
+
*
|
|
11
|
+
* The render doubles as a build gate: a compile/runtime error in the model
|
|
12
|
+
* propagates out of here, failing the publish before any draft is created.
|
|
13
|
+
*
|
|
14
|
+
* Side-effect-free at import time — it constructs its own `FluidCadServer`
|
|
15
|
+
* (which boots OC wasm + a Vite SSR pipeline) and tears the Vite server down
|
|
16
|
+
* before returning so a one-shot CLI process can exit. Import this (or use
|
|
17
|
+
* `fluidcad/server/api`), NOT `fluidcad/server`, which boots the desktop binary.
|
|
18
|
+
*/
|
|
19
|
+
export declare function captureParamDefinitions(entryPath: string, workspacePath: string): Promise<ParamDefinition[]>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { LocalSceneHost } from "../host/local-scene-host.js";
|
|
2
|
+
import { FluidCadServer } from "../fluidcad-server.js";
|
|
3
|
+
/**
|
|
4
|
+
* Render a model once, headlessly, and return its full parameter schema.
|
|
5
|
+
*
|
|
6
|
+
* Param *definitions* (type, default, current value, constraints) only exist
|
|
7
|
+
* after the engine runs the model — the packer/bundler never executes user
|
|
8
|
+
* code, so a static manifest can carry override values at most. `fluidcad
|
|
9
|
+
* publish` calls this to capture the real schema and embeds it in the manifest
|
|
10
|
+
* (`paramDefinitions`), so the hub can build param forms without a live worker.
|
|
11
|
+
*
|
|
12
|
+
* The render doubles as a build gate: a compile/runtime error in the model
|
|
13
|
+
* propagates out of here, failing the publish before any draft is created.
|
|
14
|
+
*
|
|
15
|
+
* Side-effect-free at import time — it constructs its own `FluidCadServer`
|
|
16
|
+
* (which boots OC wasm + a Vite SSR pipeline) and tears the Vite server down
|
|
17
|
+
* before returning so a one-shot CLI process can exit. Import this (or use
|
|
18
|
+
* `fluidcad/server/api`), NOT `fluidcad/server`, which boots the desktop binary.
|
|
19
|
+
*/
|
|
20
|
+
export async function captureParamDefinitions(entryPath, workspacePath) {
|
|
21
|
+
const host = new LocalSceneHost();
|
|
22
|
+
const server = new FluidCadServer(host);
|
|
23
|
+
try {
|
|
24
|
+
await server.init(workspacePath);
|
|
25
|
+
const rendered = await server.processFile(entryPath);
|
|
26
|
+
if (!rendered) {
|
|
27
|
+
throw new Error('The engine did not initialize — is there an init.js at the workspace root? ' +
|
|
28
|
+
'Run `fluidcad init` to scaffold one.');
|
|
29
|
+
}
|
|
30
|
+
return rendered.params ?? [];
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
// LocalSceneHost.init() starts a Vite dev server that keeps the event loop
|
|
34
|
+
// alive; close it so the CLI can exit after a single render.
|
|
35
|
+
try {
|
|
36
|
+
await host.server?.close();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* best-effort teardown */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ParamDefinition } from '../../../lib/dist/index.js';
|
|
2
|
+
import { type ModelPackageCamera, type ModelPackageManifest, type ParamValue } from './types.ts';
|
|
3
|
+
export interface PackInputs {
|
|
4
|
+
entryPath: string;
|
|
5
|
+
workspacePath: string;
|
|
6
|
+
fluidcadVersion: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
paramOverrides?: Record<string, ParamValue>;
|
|
10
|
+
/**
|
|
11
|
+
* Full param schema to embed in the manifest. `fluidcad publish` renders the
|
|
12
|
+
* model once to capture this (see `capture-params.ts`); `fluidcad pack` omits
|
|
13
|
+
* it. Kept as an input (rather than rendering inside `packModel`) so packing
|
|
14
|
+
* stays a pure, engine-free file producer.
|
|
15
|
+
*/
|
|
16
|
+
paramDefinitions?: ParamDefinition[];
|
|
17
|
+
camera?: ModelPackageCamera;
|
|
18
|
+
}
|
|
19
|
+
export interface PackResult {
|
|
20
|
+
manifest: ModelPackageManifest;
|
|
21
|
+
zip: Buffer;
|
|
22
|
+
}
|
|
23
|
+
export declare function packModel(inputs: PackInputs): Promise<PackResult>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { build } from 'esbuild';
|
|
2
|
+
import { readFile, readdir, stat } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { basename, extname, join, relative } from 'path';
|
|
5
|
+
import JSZip from 'jszip';
|
|
6
|
+
import ignoreFactory from 'ignore';
|
|
7
|
+
import { normalizePath } from "../normalize-path.js";
|
|
8
|
+
import { getBlockedNodeModule } from "../host/blocked-imports.js";
|
|
9
|
+
import { ASSETS_PREFIX, BUNDLE_FILENAME, FILES_PREFIX, MANIFEST_FILENAME, } from "./types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Reject Node.js builtins that are off-limits in `.fluid.js` code. Same
|
|
12
|
+
* defence the LocalSceneHost applies at SSR transform time; here it runs
|
|
13
|
+
* at pack time so the produced bundle is verified before it ships.
|
|
14
|
+
*/
|
|
15
|
+
function blockNodeBuiltinsPlugin() {
|
|
16
|
+
return {
|
|
17
|
+
name: 'block-node-builtins',
|
|
18
|
+
setup(b) {
|
|
19
|
+
b.onResolve({ filter: /.*/ }, (args) => {
|
|
20
|
+
const blocked = getBlockedNodeModule(args.path);
|
|
21
|
+
if (!blocked)
|
|
22
|
+
return null;
|
|
23
|
+
return {
|
|
24
|
+
errors: [
|
|
25
|
+
{
|
|
26
|
+
text: `Module "${args.path}" is not allowed in FluidCAD scripts. ` +
|
|
27
|
+
`Access to Node.js "${blocked}" module is restricted for security.`,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Bundle the model into a single ES module via a virtual wrapper. When
|
|
37
|
+
* `init.js` exists it runs FIRST (its side effects set up the engine) and
|
|
38
|
+
* its `default` export is forwarded as the bundle's `default` export so
|
|
39
|
+
* the hub-side loader has a handle on the SceneManager. When there's no
|
|
40
|
+
* init.js, the entry is bundled directly.
|
|
41
|
+
*
|
|
42
|
+
* The bundle is self-contained — every transitively-imported workspace file
|
|
43
|
+
* is inlined (npm deps too). The original file text the hub displays comes
|
|
44
|
+
* from the `files/` tree, not from this bundle.
|
|
45
|
+
*/
|
|
46
|
+
async function bundleModel(entryAbs, initAbs, workspaceAbs) {
|
|
47
|
+
const entryRel = './' + normalizePath(relative(workspaceAbs, entryAbs));
|
|
48
|
+
const initRel = initAbs ? './' + normalizePath(relative(workspaceAbs, initAbs)) : null;
|
|
49
|
+
const wrapperSource = initRel
|
|
50
|
+
? `import sceneManager from ${JSON.stringify(initRel)};\n` +
|
|
51
|
+
`import ${JSON.stringify(entryRel)};\n` +
|
|
52
|
+
`export default sceneManager;\n`
|
|
53
|
+
: `export * from ${JSON.stringify(entryRel)};\n`;
|
|
54
|
+
const result = await build({
|
|
55
|
+
stdin: {
|
|
56
|
+
contents: wrapperSource,
|
|
57
|
+
resolveDir: workspaceAbs,
|
|
58
|
+
sourcefile: '__fluidpkg_entry__.js',
|
|
59
|
+
loader: 'js',
|
|
60
|
+
},
|
|
61
|
+
format: 'esm',
|
|
62
|
+
bundle: true,
|
|
63
|
+
write: false,
|
|
64
|
+
platform: 'node',
|
|
65
|
+
external: ['fluidcad', 'fluidcad/*'],
|
|
66
|
+
plugins: [blockNodeBuiltinsPlugin()],
|
|
67
|
+
logLevel: 'silent',
|
|
68
|
+
});
|
|
69
|
+
if (result.errors.length) {
|
|
70
|
+
throw new Error(result.errors.map((e) => e.text).join('\n'));
|
|
71
|
+
}
|
|
72
|
+
if (!result.outputFiles || result.outputFiles.length === 0) {
|
|
73
|
+
throw new Error(`esbuild produced no output for ${entryAbs}`);
|
|
74
|
+
}
|
|
75
|
+
return result.outputFiles[0].text;
|
|
76
|
+
}
|
|
77
|
+
async function collectImportAssetPaths(workspacePath) {
|
|
78
|
+
// STEP imports are stored as cached `.brep` (+ `.colors.json` sidecar) under
|
|
79
|
+
// `imports/` — the engine reads those at render time, not the original
|
|
80
|
+
// `.step` files. Walk the whole workspace so any `.brep`/`.colors.json` is
|
|
81
|
+
// captured; also include any `.step`/`.stp` originals the user kept around
|
|
82
|
+
// (for display in the hub's file viewer; the engine ignores them).
|
|
83
|
+
const out = [];
|
|
84
|
+
async function walk(dir) {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = await readdir(dir);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (entry === 'node_modules' || entry === '.git' || entry === 'dist')
|
|
94
|
+
continue;
|
|
95
|
+
const full = join(dir, entry);
|
|
96
|
+
let st;
|
|
97
|
+
try {
|
|
98
|
+
st = await stat(full);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (st.isDirectory()) {
|
|
104
|
+
await walk(full);
|
|
105
|
+
}
|
|
106
|
+
else if (st.isFile()) {
|
|
107
|
+
const lower = entry.toLowerCase();
|
|
108
|
+
const ext = extname(lower);
|
|
109
|
+
const isColors = lower.endsWith('.colors.json');
|
|
110
|
+
if (ext === '.step' || ext === '.stp' || ext === '.brep' || isColors) {
|
|
111
|
+
out.push(normalizePath(relative(workspacePath, full)));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await walk(workspacePath);
|
|
117
|
+
return out.sort();
|
|
118
|
+
}
|
|
119
|
+
// Enforced on top of any `.gitignore`: dependency trees, prior pack outputs
|
|
120
|
+
// (the latter would otherwise recurse into the next pack), and `fluidcad.json`
|
|
121
|
+
// (the local hub binding — model id + name — which the hub already owns and
|
|
122
|
+
// should never ship as model source). `node_modules` is also pruned during the
|
|
123
|
+
// walk for speed. Hidden dot-entries are excluded by the walk directly (see
|
|
124
|
+
// below), so VCS metadata (`.git`) and secrets (`.env`) need no pattern here.
|
|
125
|
+
const ALWAYS_EXCLUDE = ['node_modules', '*.fluidpkg', 'fluidcad.json'];
|
|
126
|
+
// `ignore` ships a CJS `module.exports = factory`, but its bundled types use
|
|
127
|
+
// `export default`, which loses the call signature under `module: nodenext`.
|
|
128
|
+
// Pin the factory's real signature; the runtime value is the callable factory.
|
|
129
|
+
const ignore = ignoreFactory;
|
|
130
|
+
/**
|
|
131
|
+
* Pack v2 file selection: every non-ignored file in the workspace, so the hub
|
|
132
|
+
* ships the whole project (README, package.json, configs, sources) — not just
|
|
133
|
+
* the entry's transitive imports.
|
|
134
|
+
*
|
|
135
|
+
* A root `.gitignore` is honored via the mature `ignore` package (same matcher
|
|
136
|
+
* eslint/prettier use). Hidden dot-entries (names starting with `.`) are ALWAYS
|
|
137
|
+
* excluded — `.git`, `.env`, and tool/editor state like `.claude`/`.vscode` are
|
|
138
|
+
* never model content and may hold secrets — regardless of whether they're
|
|
139
|
+
* gitignored. `node_modules` is pruned too; `ALWAYS_EXCLUDE` (prior `.fluidpkg`
|
|
140
|
+
* outputs) is enforced on top of any `.gitignore`. We walk and filter per-file
|
|
141
|
+
* rather than pruning ignored directories so negation rules (`!keep/this`) work.
|
|
142
|
+
*/
|
|
143
|
+
async function collectWorkspaceFiles(workspaceAbs) {
|
|
144
|
+
const gitignorePath = join(workspaceAbs, '.gitignore');
|
|
145
|
+
const hasGitignore = existsSync(gitignorePath);
|
|
146
|
+
const ig = ignore().add(ALWAYS_EXCLUDE);
|
|
147
|
+
if (hasGitignore) {
|
|
148
|
+
ig.add(await readFile(gitignorePath, 'utf8'));
|
|
149
|
+
}
|
|
150
|
+
const out = [];
|
|
151
|
+
async function walk(dir) {
|
|
152
|
+
let entries;
|
|
153
|
+
try {
|
|
154
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const name = entry.name;
|
|
161
|
+
// Skip dependency trees and ALL hidden dot-entries (VCS metadata, secrets,
|
|
162
|
+
// editor/tool state) — never packaged, gitignore or not.
|
|
163
|
+
if (name === 'node_modules' || name.startsWith('.'))
|
|
164
|
+
continue;
|
|
165
|
+
const full = join(dir, name);
|
|
166
|
+
const rel = normalizePath(relative(workspaceAbs, full));
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
await walk(full);
|
|
169
|
+
}
|
|
170
|
+
else if (entry.isFile()) {
|
|
171
|
+
if (ig.ignores(rel))
|
|
172
|
+
continue;
|
|
173
|
+
out.push(rel);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await walk(workspaceAbs);
|
|
178
|
+
return out.sort();
|
|
179
|
+
}
|
|
180
|
+
export async function packModel(inputs) {
|
|
181
|
+
const entryAbs = normalizePath(inputs.entryPath);
|
|
182
|
+
const workspaceAbs = normalizePath(inputs.workspacePath);
|
|
183
|
+
const initPath = join(workspaceAbs, 'init.js');
|
|
184
|
+
const initAbs = existsSync(initPath) ? normalizePath(initPath) : null;
|
|
185
|
+
const bundle = await bundleModel(entryAbs, initAbs, workspaceAbs);
|
|
186
|
+
const assetPaths = await collectImportAssetPaths(workspaceAbs);
|
|
187
|
+
// The full human tree, minus anything already shipped under assets/ (so large
|
|
188
|
+
// brep/STEP bytes aren't duplicated). assets + files together = the package.
|
|
189
|
+
const assetSet = new Set(assetPaths);
|
|
190
|
+
const filePaths = (await collectWorkspaceFiles(workspaceAbs)).filter((p) => !assetSet.has(p));
|
|
191
|
+
const entryRelative = normalizePath(relative(workspaceAbs, entryAbs));
|
|
192
|
+
const defaultName = basename(entryAbs).replace(/\.fluid\.js$/i, '');
|
|
193
|
+
const manifest = {
|
|
194
|
+
schemaVersion: 2,
|
|
195
|
+
name: inputs.name ?? defaultName,
|
|
196
|
+
fluidcadVersion: inputs.fluidcadVersion,
|
|
197
|
+
createdAt: new Date().toISOString(),
|
|
198
|
+
entry: entryRelative,
|
|
199
|
+
hasInit: !!initAbs,
|
|
200
|
+
assets: assetPaths,
|
|
201
|
+
files: filePaths,
|
|
202
|
+
};
|
|
203
|
+
if (inputs.description)
|
|
204
|
+
manifest.description = inputs.description;
|
|
205
|
+
if (inputs.paramOverrides && Object.keys(inputs.paramOverrides).length > 0) {
|
|
206
|
+
manifest.params = inputs.paramOverrides;
|
|
207
|
+
}
|
|
208
|
+
if (inputs.paramDefinitions && inputs.paramDefinitions.length > 0) {
|
|
209
|
+
manifest.paramDefinitions = inputs.paramDefinitions;
|
|
210
|
+
}
|
|
211
|
+
if (inputs.camera)
|
|
212
|
+
manifest.camera = inputs.camera;
|
|
213
|
+
const zip = new JSZip();
|
|
214
|
+
zip.file(MANIFEST_FILENAME, JSON.stringify(manifest, null, 2));
|
|
215
|
+
zip.file(BUNDLE_FILENAME, bundle);
|
|
216
|
+
for (const relPath of assetPaths) {
|
|
217
|
+
const bytes = await readFile(join(workspaceAbs, relPath));
|
|
218
|
+
zip.file(ASSETS_PREFIX + relPath, bytes);
|
|
219
|
+
}
|
|
220
|
+
for (const relPath of filePaths) {
|
|
221
|
+
const bytes = await readFile(join(workspaceAbs, relPath));
|
|
222
|
+
zip.file(FILES_PREFIX + relPath, bytes);
|
|
223
|
+
}
|
|
224
|
+
const buffer = await zip.generateAsync({
|
|
225
|
+
type: 'nodebuffer',
|
|
226
|
+
compression: 'DEFLATE',
|
|
227
|
+
compressionOptions: { level: 6 },
|
|
228
|
+
});
|
|
229
|
+
return { manifest, zip: buffer };
|
|
230
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ParamDefinition } from '../../../lib/dist/index.js';
|
|
2
|
+
export type ParamValue = string | number | boolean | (string | number)[];
|
|
3
|
+
export interface ModelPackageCamera {
|
|
4
|
+
position: [number, number, number];
|
|
5
|
+
target: [number, number, number];
|
|
6
|
+
up: [number, number, number];
|
|
7
|
+
projection: 'orthographic' | 'perspective';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Contents of `manifest.json` inside a `.fluidpkg` archive. The bundle, the
|
|
11
|
+
* optional init.js, and any STEP assets live alongside this manifest as
|
|
12
|
+
* separate entries in the zip — we never base64-embed binaries in JSON.
|
|
13
|
+
*
|
|
14
|
+
* The HubSceneHost reads this manifest first to decide what else to load from
|
|
15
|
+
* the archive (presence of `init.js`, which asset paths to map, the file tree).
|
|
16
|
+
*
|
|
17
|
+
* `schemaVersion: 2` retired the `src/` source tree: the single self-contained
|
|
18
|
+
* `bundle.js` is what the engine executes, and the `files` tree (below) is the
|
|
19
|
+
* full human-readable project the hub displays — so `sources`/`src/` (the old
|
|
20
|
+
* transitive-import subset) is gone.
|
|
21
|
+
*/
|
|
22
|
+
export interface ModelPackageManifest {
|
|
23
|
+
schemaVersion: 2;
|
|
24
|
+
name: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
fluidcadVersion: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
entry: string;
|
|
29
|
+
/**
|
|
30
|
+
* True when the workspace had an `init.js`. Its code is bundled at the top of
|
|
31
|
+
* `bundle.js` so the engine pipeline is set up before the entry runs, and the
|
|
32
|
+
* bundle's `default` export is init's default (the SceneManager). The original
|
|
33
|
+
* `init.js` text is still in the `files` tree for display.
|
|
34
|
+
*/
|
|
35
|
+
hasInit: boolean;
|
|
36
|
+
assets: string[];
|
|
37
|
+
/**
|
|
38
|
+
* Workspace-relative paths of every non-ignored file in the workspace
|
|
39
|
+
* (Pack v2), shipped verbatim under `files/<path>`. The full human tree —
|
|
40
|
+
* README, package.json, configs, the `.fluid.js` sources — that the hub's
|
|
41
|
+
* file viewer lists and serves, separate from the self-contained `bundle.js`
|
|
42
|
+
* the engine executes. Paths already shipped under `assets/` (engine
|
|
43
|
+
* brep/STEP) are NOT repeated here; `files` + `assets` is the whole package.
|
|
44
|
+
*
|
|
45
|
+
* Selection respects a root `.gitignore` (via the `ignore` package) and
|
|
46
|
+
* always excludes `node_modules`, prior `*.fluidpkg` outputs, `fluidcad.json`
|
|
47
|
+
* (the local hub binding), and every hidden dot-entry (`.git`, `.env`,
|
|
48
|
+
* `.claude`, `.vscode`, … — never model content, may hold secrets), whether or
|
|
49
|
+
* not they're gitignored.
|
|
50
|
+
*/
|
|
51
|
+
files: string[];
|
|
52
|
+
params?: Record<string, ParamValue>;
|
|
53
|
+
/**
|
|
54
|
+
* Full parameter schema captured by rendering the model once at pack time
|
|
55
|
+
* (type/default/current value/constraints per `param()` call). Unlike
|
|
56
|
+
* `params` (override VALUES only), this is the complete definition set the
|
|
57
|
+
* hub stores and renders forms from. Populated by `fluidcad publish` (which
|
|
58
|
+
* boots the engine to render); plain `fluidcad pack` leaves it undefined.
|
|
59
|
+
*/
|
|
60
|
+
paramDefinitions?: ParamDefinition[];
|
|
61
|
+
camera?: ModelPackageCamera;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Standard layout inside a `.fluidpkg` zip:
|
|
65
|
+
* manifest.json — ModelPackageManifest as JSON
|
|
66
|
+
* bundle.js — esbuild ES module output: init.js code first (if
|
|
67
|
+
* hasInit), then the entry; bundle's default export
|
|
68
|
+
* is init's default (SceneManager) when present
|
|
69
|
+
* assets/<path> — raw bytes of imported STEP files, paths preserved
|
|
70
|
+
* relative to the workspace root
|
|
71
|
+
* files/<path> — every non-ignored workspace file (Pack v2), verbatim;
|
|
72
|
+
* the full human tree the hub viewer lists and serves.
|
|
73
|
+
* Excludes anything already under assets/ to avoid
|
|
74
|
+
* duplicate bytes.
|
|
75
|
+
*/
|
|
76
|
+
export declare const MANIFEST_FILENAME = "manifest.json";
|
|
77
|
+
export declare const BUNDLE_FILENAME = "bundle.js";
|
|
78
|
+
export declare const ASSETS_PREFIX = "assets/";
|
|
79
|
+
export declare const FILES_PREFIX = "files/";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard layout inside a `.fluidpkg` zip:
|
|
3
|
+
* manifest.json — ModelPackageManifest as JSON
|
|
4
|
+
* bundle.js — esbuild ES module output: init.js code first (if
|
|
5
|
+
* hasInit), then the entry; bundle's default export
|
|
6
|
+
* is init's default (SceneManager) when present
|
|
7
|
+
* assets/<path> — raw bytes of imported STEP files, paths preserved
|
|
8
|
+
* relative to the workspace root
|
|
9
|
+
* files/<path> — every non-ignored workspace file (Pack v2), verbatim;
|
|
10
|
+
* the full human tree the hub viewer lists and serves.
|
|
11
|
+
* Excludes anything already under assets/ to avoid
|
|
12
|
+
* duplicate bytes.
|
|
13
|
+
*/
|
|
14
|
+
export const MANIFEST_FILENAME = 'manifest.json';
|
|
15
|
+
export const BUNDLE_FILENAME = 'bundle.js';
|
|
16
|
+
export const ASSETS_PREFIX = 'assets/';
|
|
17
|
+
export const FILES_PREFIX = 'files/';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export function createHitTestRouter(fluidCadServer) {
|
|
3
|
+
const router = Router();
|
|
4
|
+
router.post('/hit-test', (req, res) => {
|
|
5
|
+
const { shapeId, rayOrigin, rayDir, edgeThreshold } = req.body;
|
|
6
|
+
if (typeof shapeId !== 'string' ||
|
|
7
|
+
!Array.isArray(rayOrigin) || rayOrigin.length !== 3 ||
|
|
8
|
+
!Array.isArray(rayDir) || rayDir.length !== 3 ||
|
|
9
|
+
typeof edgeThreshold !== 'number') {
|
|
10
|
+
res.status(400).json({ error: 'Invalid request body' });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const result = fluidCadServer.hitTest(shapeId, rayOrigin, rayDir, edgeThreshold);
|
|
14
|
+
res.json(result);
|
|
15
|
+
});
|
|
16
|
+
return router;
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import type { FluidCadServer } from '../fluidcad-server.ts';
|
|
3
|
+
import type { CameraStateMessage } from '../ws-protocol.ts';
|
|
4
|
+
/**
|
|
5
|
+
* `POST /api/pack` — produce a `.fluidpkg` (zip) archive of the currently
|
|
6
|
+
* rendered file. Pulls live param overrides and the last-known camera state
|
|
7
|
+
* from the running server so the archive matches what the user is seeing.
|
|
8
|
+
* Returns the binary archive directly (application/zip).
|
|
9
|
+
*/
|
|
10
|
+
export declare function createPackRouter(fluidCadServer: FluidCadServer, workspacePath: string, fluidcadVersion: string, getLastCameraState: () => CameraStateMessage | null): Router;
|