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
|
@@ -1,107 +1,206 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
-
import {
|
|
4
|
+
import { LocalSceneHost } from "./host/local-scene-host.js";
|
|
5
5
|
import { normalizePath } from "./normalize-path.js";
|
|
6
6
|
import { BreakpointHit } from '../../lib/dist/common/breakpoint-hit.js';
|
|
7
|
+
import { createParamRegistry, getParamRegistry } from '../../lib/dist/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* `sessionId` is the per-renderer state key. In desktop mode it equals the
|
|
10
|
+
* file path being edited (so per-file state survives switching files). In
|
|
11
|
+
* hub mode it's a WebSocket connection UUID (so concurrent viewers stay
|
|
12
|
+
* isolated). Map keys called `sessionId` accept either flavour.
|
|
13
|
+
*/
|
|
7
14
|
export class FluidCadServer {
|
|
8
|
-
|
|
15
|
+
host;
|
|
9
16
|
sceneManager;
|
|
17
|
+
// Per-session render output, scene cache, and param overrides. Desktop's
|
|
18
|
+
// sessionId is the normalized filePath; hub mode's sessionId is the WS
|
|
19
|
+
// connection UUID. Maps must be cleared via `destroySession` on hub-side
|
|
20
|
+
// disconnect to avoid leaks.
|
|
10
21
|
previousScenes = new Map();
|
|
11
22
|
renderingCache = new Map();
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// Avoids redundant OCC work when multiple producers see the same write.
|
|
23
|
+
// Records the last successful render per session as `{ paramsHash, data }`.
|
|
24
|
+
// Any subsequent render request short-circuits when the new params hash to
|
|
25
|
+
// the same value — avoids redundant OCC work when desktop producers see the
|
|
26
|
+
// same code+params, or hub clients re-emit the same param mutation.
|
|
17
27
|
lastRendered = new Map();
|
|
28
|
+
paramOverrides = new Map();
|
|
29
|
+
// What file each session is rendering. For desktop, sessionId === filePath
|
|
30
|
+
// (set lazily on first processFile call). For hub, set explicitly via
|
|
31
|
+
// createSession with the bundle's manifest entry.
|
|
32
|
+
sessionFiles = new Map();
|
|
33
|
+
// Serializes OCC calls across all sessions. OCC isn't thread-safe and we
|
|
34
|
+
// share one engine instance per host process; concurrent param edits from
|
|
35
|
+
// multiple hub clients have to queue. Promise-chain pattern: each render
|
|
36
|
+
// awaits the previous one's settlement before starting.
|
|
37
|
+
renderMutex = Promise.resolve();
|
|
18
38
|
currentFileName = '';
|
|
19
39
|
currentFilePath = '';
|
|
20
40
|
lastRollbackStop = -1;
|
|
21
41
|
compileError = null;
|
|
42
|
+
constructor(host = new LocalSceneHost()) {
|
|
43
|
+
this.host = host;
|
|
44
|
+
}
|
|
45
|
+
getCurrentCode() {
|
|
46
|
+
if (!this.currentFileName)
|
|
47
|
+
return null;
|
|
48
|
+
return this.host.getBuffer(this.currentFileName);
|
|
49
|
+
}
|
|
22
50
|
async init(workspacePath) {
|
|
23
|
-
await this.
|
|
51
|
+
await this.host.init(workspacePath);
|
|
24
52
|
const initFilePath = normalizePath(join(workspacePath, 'init.js'));
|
|
25
53
|
if (existsSync(initFilePath)) {
|
|
26
|
-
const { default: _sceneManager } = await this.
|
|
54
|
+
const { default: _sceneManager } = await this.host.loadModule(initFilePath);
|
|
27
55
|
this.sceneManager = await _sceneManager;
|
|
28
56
|
}
|
|
29
57
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Capture an already-initialized SceneManager. Used by the hub-mode entry
|
|
60
|
+
* after running the packed bundle once to materialize the engine globals.
|
|
61
|
+
*/
|
|
62
|
+
setSceneManager(manager) {
|
|
63
|
+
this.sceneManager = manager;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Run `fn` with exclusive access to the OCC engine. The mutex is process-
|
|
67
|
+
* wide: in hub mode concurrent client sessions land here too. Order is
|
|
68
|
+
* first-come, first-served via Promise chain.
|
|
69
|
+
*/
|
|
70
|
+
async serialized(fn) {
|
|
71
|
+
const prev = this.renderMutex;
|
|
72
|
+
let release = () => { };
|
|
73
|
+
const next = new Promise((resolve) => { release = resolve; });
|
|
74
|
+
this.renderMutex = next;
|
|
75
|
+
try {
|
|
76
|
+
await prev;
|
|
77
|
+
return await fn();
|
|
33
78
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.currentFileName = normalizedFileName;
|
|
37
|
-
this.currentFilePath = filePath;
|
|
38
|
-
if (!ignoreCache) {
|
|
39
|
-
const fromCache = this.renderingCache.get(normalizedFileName);
|
|
40
|
-
if (fromCache) {
|
|
41
|
-
this.lastRollbackStop = fromCache.length - 1;
|
|
42
|
-
this.compileError = null;
|
|
43
|
-
return {
|
|
44
|
-
absPath: normalizedFileName,
|
|
45
|
-
result: fromCache,
|
|
46
|
-
rollbackStop: fromCache.length - 1,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
79
|
+
finally {
|
|
80
|
+
release();
|
|
49
81
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Session lifecycle (hub mode)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
createSession(sessionId, entryFilePath) {
|
|
87
|
+
this.sessionFiles.set(sessionId, normalizePath(entryFilePath));
|
|
88
|
+
}
|
|
89
|
+
destroySession(sessionId) {
|
|
90
|
+
this.previousScenes.delete(sessionId);
|
|
91
|
+
this.renderingCache.delete(sessionId);
|
|
92
|
+
this.lastRendered.delete(sessionId);
|
|
93
|
+
this.paramOverrides.delete(sessionId);
|
|
94
|
+
this.sessionFiles.delete(sessionId);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Re-render the session's entry, ignoring caches. Hub clients call this
|
|
98
|
+
* after editing a param. Returns the fresh render or null if no manager.
|
|
99
|
+
*/
|
|
100
|
+
async recomputeForSession(sessionId) {
|
|
101
|
+
const filePath = this.sessionFiles.get(sessionId);
|
|
102
|
+
if (!filePath)
|
|
103
|
+
return null;
|
|
104
|
+
this.renderingCache.delete(sessionId);
|
|
105
|
+
this.lastRendered.delete(sessionId);
|
|
106
|
+
return this.processFileInternal(sessionId, filePath, true);
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Render — internal core used by both desktop and hub entry points
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
async processFileInternal(sessionId, filePath, ignoreCache) {
|
|
112
|
+
return this.serialized(async () => {
|
|
113
|
+
if (!this.sceneManager) {
|
|
114
|
+
return null;
|
|
57
115
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
116
|
+
const normalizedFileName = filePath.replace('virtual:live-render:', '');
|
|
117
|
+
this.currentFileName = normalizedFileName;
|
|
118
|
+
this.currentFilePath = filePath;
|
|
119
|
+
if (!ignoreCache) {
|
|
120
|
+
const fromCache = this.renderingCache.get(sessionId);
|
|
121
|
+
if (fromCache) {
|
|
122
|
+
this.lastRollbackStop = fromCache.length - 1;
|
|
123
|
+
this.compileError = null;
|
|
124
|
+
return {
|
|
125
|
+
absPath: normalizedFileName,
|
|
126
|
+
result: fromCache,
|
|
127
|
+
rollbackStop: fromCache.length - 1,
|
|
128
|
+
};
|
|
64
129
|
}
|
|
65
130
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
131
|
+
try {
|
|
132
|
+
let scene = this.sceneManager.startScene();
|
|
133
|
+
this.sceneManager.setCurrentFile(normalizedFileName);
|
|
134
|
+
this.host.invalidateModule();
|
|
135
|
+
const registry = createParamRegistry();
|
|
136
|
+
const overrides = this.paramOverrides.get(sessionId);
|
|
137
|
+
if (overrides) {
|
|
138
|
+
registry.setOverrides(overrides);
|
|
139
|
+
}
|
|
140
|
+
let breakpointHit = false;
|
|
141
|
+
try {
|
|
142
|
+
await this.host.loadModule(filePath);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
if (e instanceof BreakpointHit) {
|
|
146
|
+
breakpointHit = true;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const params = getParamRegistry().getDefinitions();
|
|
153
|
+
if (this.previousScenes.has(sessionId)) {
|
|
154
|
+
const previousScene = this.previousScenes.get(sessionId);
|
|
155
|
+
scene = this.sceneManager.compare(previousScene, scene);
|
|
76
156
|
}
|
|
157
|
+
this.previousScenes.set(sessionId, scene);
|
|
158
|
+
this.sceneManager.renderScene(scene);
|
|
159
|
+
const result = scene.getRenderedObjects();
|
|
160
|
+
for (const obj of result) {
|
|
161
|
+
if (obj.sourceLocation) {
|
|
162
|
+
obj.sourceLocation.filePath = obj.sourceLocation.filePath.replace('virtual:live-render:', '');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!filePath.startsWith('virtual:live-render')) {
|
|
166
|
+
this.renderingCache.set(sessionId, result);
|
|
167
|
+
}
|
|
168
|
+
this.lastRollbackStop = result.length - 1;
|
|
169
|
+
this.compileError = null;
|
|
170
|
+
return {
|
|
171
|
+
absPath: normalizedFileName,
|
|
172
|
+
result,
|
|
173
|
+
rollbackStop: result.length - 1,
|
|
174
|
+
breakpointHit,
|
|
175
|
+
params,
|
|
176
|
+
};
|
|
77
177
|
}
|
|
78
|
-
|
|
79
|
-
this.
|
|
178
|
+
catch (error) {
|
|
179
|
+
this.host.invalidateModule();
|
|
180
|
+
console.log('Error processing file:', error);
|
|
181
|
+
throw error;
|
|
80
182
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.viteManager.invalidateModule();
|
|
92
|
-
console.log('Error processing file:', error);
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Desktop API — sessionId is implicit (filePath)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
async processFile(filePath, ignoreCache = false) {
|
|
189
|
+
filePath = normalizePath(filePath);
|
|
190
|
+
const sessionId = filePath.replace('virtual:live-render:', '');
|
|
191
|
+
this.sessionFiles.set(sessionId, sessionId);
|
|
192
|
+
return this.processFileInternal(sessionId, filePath, ignoreCache);
|
|
95
193
|
}
|
|
96
194
|
async updateLiveCode(fileName, code) {
|
|
97
195
|
fileName = normalizePath(fileName);
|
|
98
|
-
// Dedup against the last successful render
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
196
|
+
// Dedup against the last successful render. Multiple producers (editor
|
|
197
|
+
// live-update, save-triggered process-file, watcher, MCP /api/render)
|
|
198
|
+
// commonly hand us identical content; without this short-circuit each
|
|
199
|
+
// would trigger a redundant OCC pass. paramsHash mixes code content with
|
|
200
|
+
// current param overrides so a param change invalidates the cache.
|
|
201
|
+
const paramsHash = this.computeParamsHash(fileName, code);
|
|
103
202
|
const cached = this.lastRendered.get(fileName);
|
|
104
|
-
if (cached && cached.
|
|
203
|
+
if (cached && cached.paramsHash === paramsHash) {
|
|
105
204
|
this.compileError = null;
|
|
106
205
|
this.currentFileName = fileName;
|
|
107
206
|
this.currentFilePath = `virtual:live-render:${fileName}`;
|
|
@@ -109,11 +208,12 @@ export class FluidCadServer {
|
|
|
109
208
|
return cached.data;
|
|
110
209
|
}
|
|
111
210
|
const id = `virtual:live-render:${fileName}`;
|
|
112
|
-
this.
|
|
211
|
+
this.host.setBuffer(id, code);
|
|
113
212
|
this.renderingCache.delete(fileName);
|
|
114
|
-
|
|
213
|
+
this.sessionFiles.set(fileName, fileName);
|
|
214
|
+
const result = await this.processFileInternal(fileName, id, true);
|
|
115
215
|
if (result) {
|
|
116
|
-
this.lastRendered.set(fileName, {
|
|
216
|
+
this.lastRendered.set(fileName, { paramsHash, data: result });
|
|
117
217
|
}
|
|
118
218
|
return result;
|
|
119
219
|
}
|
|
@@ -124,10 +224,10 @@ export class FluidCadServer {
|
|
|
124
224
|
if (!this.currentFilePath) {
|
|
125
225
|
return null;
|
|
126
226
|
}
|
|
127
|
-
this.
|
|
128
|
-
this.renderingCache.delete(
|
|
129
|
-
this.lastRendered.delete(
|
|
130
|
-
return this.
|
|
227
|
+
const sessionId = this.currentFileName;
|
|
228
|
+
this.renderingCache.delete(sessionId);
|
|
229
|
+
this.lastRendered.delete(sessionId);
|
|
230
|
+
return this.processFileInternal(sessionId, this.currentFilePath, true);
|
|
131
231
|
}
|
|
132
232
|
async rollback(fileName, index) {
|
|
133
233
|
if (!this.sceneManager) {
|
|
@@ -195,6 +295,36 @@ export class FluidCadServer {
|
|
|
195
295
|
}
|
|
196
296
|
return this.sceneManager.exportShapes(scene, shapeIds, options);
|
|
197
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Export every solid of a hub session's latest render. The session-keyed twin
|
|
300
|
+
* of `exportShapes` (which reads the desktop `currentFileName`): hub mode keys
|
|
301
|
+
* each render's scene by `sessionId`, so exporting/downloading from a hub
|
|
302
|
+
* session must look it up the same way — exactly why `hitTestForSession`
|
|
303
|
+
* exists. Gathers all solids itself ("download the whole model"); returns null
|
|
304
|
+
* when the session has no rendered scene or it holds no solids (the caller maps
|
|
305
|
+
* that to a "nothing to export" response).
|
|
306
|
+
*/
|
|
307
|
+
exportShapesForSession(sessionId, options) {
|
|
308
|
+
if (!this.sceneManager) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const scene = this.previousScenes.get(sessionId);
|
|
312
|
+
if (!scene) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const shapeIds = [];
|
|
316
|
+
for (const obj of scene.getAllSceneObjects()) {
|
|
317
|
+
for (const shape of obj.getAddedShapes()) {
|
|
318
|
+
if (shape.isSolid()) {
|
|
319
|
+
shapeIds.push(shape.id);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (shapeIds.length === 0) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
return this.sceneManager.exportShapes(scene, shapeIds, options);
|
|
327
|
+
}
|
|
198
328
|
hitTest(shapeId, rayOrigin, rayDir, edgeThreshold) {
|
|
199
329
|
if (!this.sceneManager) {
|
|
200
330
|
return null;
|
|
@@ -205,12 +335,41 @@ export class FluidCadServer {
|
|
|
205
335
|
}
|
|
206
336
|
return this.sceneManager.hitTest(scene, shapeId, rayOrigin, rayDir, edgeThreshold);
|
|
207
337
|
}
|
|
338
|
+
hitTestForSession(sessionId, shapeId, rayOrigin, rayDir, edgeThreshold) {
|
|
339
|
+
if (!this.sceneManager) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const scene = this.previousScenes.get(sessionId);
|
|
343
|
+
if (!scene) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
return this.sceneManager.hitTest(scene, shapeId, rayOrigin, rayDir, edgeThreshold);
|
|
347
|
+
}
|
|
208
348
|
setCompileError(err) {
|
|
209
349
|
this.compileError = err;
|
|
210
350
|
}
|
|
211
351
|
getCompileError() {
|
|
212
352
|
return this.compileError;
|
|
213
353
|
}
|
|
354
|
+
setParam(sessionId, label, value) {
|
|
355
|
+
sessionId = normalizePath(sessionId);
|
|
356
|
+
if (!this.paramOverrides.has(sessionId)) {
|
|
357
|
+
this.paramOverrides.set(sessionId, new Map());
|
|
358
|
+
}
|
|
359
|
+
this.paramOverrides.get(sessionId).set(label, value);
|
|
360
|
+
this.lastRendered.delete(sessionId);
|
|
361
|
+
}
|
|
362
|
+
resetParams(sessionId) {
|
|
363
|
+
sessionId = normalizePath(sessionId);
|
|
364
|
+
this.paramOverrides.delete(sessionId);
|
|
365
|
+
this.lastRendered.delete(sessionId);
|
|
366
|
+
}
|
|
367
|
+
getParamOverrides(sessionId) {
|
|
368
|
+
const map = this.paramOverrides.get(normalizePath(sessionId));
|
|
369
|
+
if (!map)
|
|
370
|
+
return {};
|
|
371
|
+
return Object.fromEntries(map);
|
|
372
|
+
}
|
|
214
373
|
getCurrentFileName() {
|
|
215
374
|
return this.currentFileName;
|
|
216
375
|
}
|
|
@@ -279,15 +438,22 @@ export class FluidCadServer {
|
|
|
279
438
|
}
|
|
280
439
|
return { shapes };
|
|
281
440
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
441
|
+
/**
|
|
442
|
+
* Compose a stable cache key over the rendering inputs: the source bytes
|
|
443
|
+
* being rendered plus the param overrides currently in effect for the
|
|
444
|
+
* session. Param changes flip the hash so cached entries don't shadow a
|
|
445
|
+
* recompute, even when the code text is byte-identical.
|
|
446
|
+
*/
|
|
447
|
+
computeParamsHash(sessionId, codeOrBundle) {
|
|
448
|
+
const overrides = this.paramOverrides.get(sessionId);
|
|
449
|
+
const sortedEntries = overrides ? [...overrides.entries()].sort(([a], [b]) => a.localeCompare(b)) : [];
|
|
450
|
+
const normalized = codeOrBundle.replace(/\r\n/g, '\n');
|
|
451
|
+
return createHash('sha1')
|
|
452
|
+
.update(normalized)
|
|
453
|
+
.update('\0')
|
|
454
|
+
.update(JSON.stringify(sortedEntries))
|
|
455
|
+
.digest('hex');
|
|
456
|
+
}
|
|
291
457
|
}
|
|
292
458
|
const MAX_PARAM_DEPTH = 6;
|
|
293
459
|
function sanitizeParams(value, depth = 0) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node built-in modules that user `.fluid.js` code must not import. Same set
|
|
3
|
+
* enforced by LocalSceneHost (at SSR transform time) and the model packer
|
|
4
|
+
* (at bundle time) so a packed model can't smuggle in capabilities a live
|
|
5
|
+
* workspace would have rejected.
|
|
6
|
+
*/
|
|
7
|
+
export declare const BLOCKED_NODE_MODULES: Set<string>;
|
|
8
|
+
export declare function getBlockedNodeModule(id: string): string | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node built-in modules that user `.fluid.js` code must not import. Same set
|
|
3
|
+
* enforced by LocalSceneHost (at SSR transform time) and the model packer
|
|
4
|
+
* (at bundle time) so a packed model can't smuggle in capabilities a live
|
|
5
|
+
* workspace would have rejected.
|
|
6
|
+
*/
|
|
7
|
+
export const BLOCKED_NODE_MODULES = new Set([
|
|
8
|
+
'fs',
|
|
9
|
+
'child_process',
|
|
10
|
+
'net',
|
|
11
|
+
'dgram',
|
|
12
|
+
'tls',
|
|
13
|
+
'http',
|
|
14
|
+
'https',
|
|
15
|
+
'http2',
|
|
16
|
+
'os',
|
|
17
|
+
'worker_threads',
|
|
18
|
+
'vm',
|
|
19
|
+
'cluster',
|
|
20
|
+
'dns',
|
|
21
|
+
'module',
|
|
22
|
+
]);
|
|
23
|
+
export function getBlockedNodeModule(id) {
|
|
24
|
+
let name = id;
|
|
25
|
+
if (name.startsWith('node:')) {
|
|
26
|
+
name = name.slice(5);
|
|
27
|
+
}
|
|
28
|
+
const baseName = name.split('/')[0];
|
|
29
|
+
return BLOCKED_NODE_MODULES.has(baseName) ? baseName : null;
|
|
30
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type ViteDevServer } from 'vite';
|
|
2
|
-
|
|
2
|
+
import type { SceneHost } from './scene-host.ts';
|
|
3
|
+
export declare class LocalSceneHost implements SceneHost {
|
|
3
4
|
server: ViteDevServer;
|
|
4
5
|
private rootPath;
|
|
5
6
|
private buffers;
|
|
6
7
|
init(rootPath: string): Promise<void>;
|
|
7
8
|
setBuffer(id: string, code: string): void;
|
|
9
|
+
getBuffer(fileName: string): string | null;
|
|
8
10
|
loadModule(filePath: string): Promise<Record<string, any>>;
|
|
9
11
|
invalidateModule(): void;
|
|
10
12
|
}
|
|
@@ -1,30 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'vite';
|
|
2
2
|
import { dirname, resolve, isAbsolute } from 'path';
|
|
3
|
-
import { normalizePath } from "
|
|
4
|
-
|
|
5
|
-
'fs',
|
|
6
|
-
'child_process',
|
|
7
|
-
'net',
|
|
8
|
-
'dgram',
|
|
9
|
-
'tls',
|
|
10
|
-
'http',
|
|
11
|
-
'https',
|
|
12
|
-
'http2',
|
|
13
|
-
'os',
|
|
14
|
-
'worker_threads',
|
|
15
|
-
'vm',
|
|
16
|
-
'cluster',
|
|
17
|
-
'dns',
|
|
18
|
-
'module',
|
|
19
|
-
]);
|
|
20
|
-
function getBlockedNodeModule(id) {
|
|
21
|
-
let name = id;
|
|
22
|
-
if (name.startsWith('node:')) {
|
|
23
|
-
name = name.slice(5);
|
|
24
|
-
}
|
|
25
|
-
const baseName = name.split('/')[0];
|
|
26
|
-
return BLOCKED_NODE_MODULES.has(baseName) ? baseName : null;
|
|
27
|
-
}
|
|
3
|
+
import { normalizePath } from "../normalize-path.js";
|
|
4
|
+
import { getBlockedNodeModule } from "./blocked-imports.js";
|
|
28
5
|
const IMPORT_PATTERN = /\b(?:import|export)\s[\s\S]*?from\s+['"]([^'"]+)['"]|\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
29
6
|
function scanForBlockedImports(code) {
|
|
30
7
|
let match;
|
|
@@ -38,7 +15,7 @@ function scanForBlockedImports(code) {
|
|
|
38
15
|
}
|
|
39
16
|
return null;
|
|
40
17
|
}
|
|
41
|
-
export class
|
|
18
|
+
export class LocalSceneHost {
|
|
42
19
|
server;
|
|
43
20
|
rootPath = '';
|
|
44
21
|
buffers = new Map();
|
|
@@ -102,6 +79,9 @@ export class ViteManager {
|
|
|
102
79
|
setBuffer(id, code) {
|
|
103
80
|
this.buffers.set(id, code);
|
|
104
81
|
}
|
|
82
|
+
getBuffer(fileName) {
|
|
83
|
+
return this.buffers.get(`virtual:live-render:${fileName}`) ?? null;
|
|
84
|
+
}
|
|
105
85
|
async loadModule(filePath) {
|
|
106
86
|
const mod = await this.server.ssrLoadModule(filePath);
|
|
107
87
|
for (const value of Object.values(mod)) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-of-truth + execution seam for a workspace's scene code.
|
|
3
|
+
*
|
|
4
|
+
* `FluidCadServer` owns the engine pipeline (param registry, scene cache,
|
|
5
|
+
* render orchestration) and does not care where `.fluid.js` source comes
|
|
6
|
+
* from or how it gets turned into a runnable module. That responsibility
|
|
7
|
+
* lives behind this interface.
|
|
8
|
+
*
|
|
9
|
+
* Implementations:
|
|
10
|
+
* - LocalSceneHost: Vite SSR over the workspace directory (desktop).
|
|
11
|
+
* - HubSceneHost (Phase 2): in-memory consumer of a packed model bundle.
|
|
12
|
+
*/
|
|
13
|
+
export interface SceneHost {
|
|
14
|
+
init(workspacePath: string): Promise<void>;
|
|
15
|
+
loadModule(filePath: string): Promise<Record<string, any>>;
|
|
16
|
+
setBuffer(id: string, code: string): void;
|
|
17
|
+
getBuffer(fileName: string): string | null;
|
|
18
|
+
invalidateModule(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|