fluidcad 0.0.4 → 0.0.6
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 +16 -0
- package/package.json +2 -2
- package/server/dist/fluidcad-server.d.ts +32 -0
- package/server/dist/fluidcad-server.js +150 -0
- package/server/dist/index.d.ts +1 -0
- package/server/dist/index.js +290 -0
- package/server/dist/routes/actions.d.ts +3 -0
- package/server/dist/routes/actions.js +100 -0
- package/server/dist/routes/export.d.ts +3 -0
- package/server/dist/routes/export.js +55 -0
- package/server/dist/routes/properties.d.ts +3 -0
- package/server/dist/routes/properties.js +46 -0
- package/server/dist/routes/screenshot.d.ts +2 -0
- package/server/dist/routes/screenshot.js +76 -0
- package/server/dist/vite-manager.d.ts +10 -0
- package/server/dist/vite-manager.js +93 -0
- package/server/dist/ws-protocol.d.ts +138 -0
- package/server/dist/ws-protocol.js +4 -0
package/README.md
CHANGED
|
@@ -6,7 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center"><strong>Write CAD models in JavaScript. See the result in real time.</strong></p>
|
|
8
8
|
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://fluidcad.io/docs/getting-started">Getting Started</a> ·
|
|
11
|
+
<a href="https://fluidcad.io/docs/tutorials/">Tutorials</a> ·
|
|
12
|
+
<a href="https://fluidcad.io/docs/guides">Guides</a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
9
15
|
> FluidCAD is under active development. APIs and features may change as the project evolves.
|
|
16
|
+
>
|
|
17
|
+
> I'm not accepting pull requests just yet -- I'm still finalizing the design and putting together a roadmap. Once I hit **v0.1.0**, I'd love to have contributions from the community. Stay tuned!
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Under the Hood
|
|
22
|
+
|
|
23
|
+
FluidCAD is built on [OpenCascade](https://dev.opencascade.org/), a full B-Rep (boundary representation) modeling kernel, through the [opencascade.js](https://ocjs.org/) WebAssembly binding. This means precise, production-grade geometry -- exact edges, fillets, and booleans -- not mesh approximations.
|
|
24
|
+
|
|
25
|
+
A huge thanks to the [opencascade.js](https://ocjs.org/) team for making this possible.
|
|
10
26
|
|
|
11
27
|
---
|
|
12
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluidcad",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Parametric CAD modeling library using javascript",
|
|
5
5
|
"author": "Marwan Aouida <contact@marwan.dev>",
|
|
6
6
|
"homepage": "https://fluidcad.io",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"./server": "./server/dist/index.js"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"clean": "rm -rf lib/dist server/dist ui/dist",
|
|
42
|
+
"clean": "rm -rf lib/dist server/dist ui/dist lib/tsconfig.tsbuildinfo server/tsconfig.tsbuildinfo",
|
|
43
43
|
"build:lib": "tsc -p lib/tsconfig.json",
|
|
44
44
|
"build:server": "tsc -p server/tsconfig.json",
|
|
45
45
|
"build:ui": "vite build --config ui/vite.config.ts",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type SceneRenderedData = {
|
|
2
|
+
absPath: string;
|
|
3
|
+
result: any[];
|
|
4
|
+
rollbackStop: number;
|
|
5
|
+
};
|
|
6
|
+
export declare class FluidCadServer {
|
|
7
|
+
private viteManager;
|
|
8
|
+
private sceneManager;
|
|
9
|
+
private previousScenes;
|
|
10
|
+
private renderingCache;
|
|
11
|
+
private currentFileName;
|
|
12
|
+
init(workspacePath: string): Promise<void>;
|
|
13
|
+
processFile(filePath: string, ignoreCache?: boolean): Promise<SceneRenderedData | null>;
|
|
14
|
+
updateLiveCode(fileName: string, code: string): Promise<SceneRenderedData | null>;
|
|
15
|
+
rollbackFromUI(index: number): Promise<SceneRenderedData | null>;
|
|
16
|
+
rollback(fileName: string, index: number): Promise<SceneRenderedData | null>;
|
|
17
|
+
importFile(workspacePath: string, fileName: string, data: string): Promise<void>;
|
|
18
|
+
getShapeProperties(shapeId: string): any;
|
|
19
|
+
getFaceProperties(shapeId: string, faceIndex: number): any;
|
|
20
|
+
getEdgeProperties(shapeId: string, edgeIndex: number): any;
|
|
21
|
+
exportShapes(shapeIds: string[], options: {
|
|
22
|
+
format: 'step' | 'stl';
|
|
23
|
+
includeColors?: boolean;
|
|
24
|
+
resolution?: string;
|
|
25
|
+
customLinearDeflection?: number;
|
|
26
|
+
customAngularDeflectionDeg?: number;
|
|
27
|
+
}): {
|
|
28
|
+
data: string | Uint8Array;
|
|
29
|
+
fileName: string;
|
|
30
|
+
} | null;
|
|
31
|
+
hitTest(shapeId: string, rayOrigin: [number, number, number], rayDir: [number, number, number], edgeThreshold: number): any;
|
|
32
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { ViteManager } from "./vite-manager.js";
|
|
4
|
+
export class FluidCadServer {
|
|
5
|
+
viteManager = new ViteManager();
|
|
6
|
+
sceneManager;
|
|
7
|
+
previousScenes = new Map();
|
|
8
|
+
renderingCache = new Map();
|
|
9
|
+
currentFileName = '';
|
|
10
|
+
async init(workspacePath) {
|
|
11
|
+
await this.viteManager.init(workspacePath);
|
|
12
|
+
const initFilePath = join(workspacePath, 'init.js');
|
|
13
|
+
if (existsSync(initFilePath)) {
|
|
14
|
+
const { default: _sceneManager } = await this.viteManager.loadModule(initFilePath);
|
|
15
|
+
this.sceneManager = await _sceneManager;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async processFile(filePath, ignoreCache = false) {
|
|
19
|
+
if (!this.sceneManager) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const normalizedFileName = filePath.replace('virtual:live-render:', '');
|
|
23
|
+
this.currentFileName = normalizedFileName;
|
|
24
|
+
if (!ignoreCache) {
|
|
25
|
+
const fromCache = this.renderingCache.get(normalizedFileName);
|
|
26
|
+
if (fromCache) {
|
|
27
|
+
return {
|
|
28
|
+
absPath: normalizedFileName,
|
|
29
|
+
result: fromCache,
|
|
30
|
+
rollbackStop: fromCache.length - 1,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
let scene = this.sceneManager.startScene();
|
|
36
|
+
this.sceneManager.setCurrentFile(normalizedFileName);
|
|
37
|
+
this.viteManager.invalidateModule();
|
|
38
|
+
await this.viteManager.loadModule(filePath);
|
|
39
|
+
if (this.previousScenes.has(normalizedFileName)) {
|
|
40
|
+
const previousScene = this.previousScenes.get(normalizedFileName);
|
|
41
|
+
scene = this.sceneManager.compare(previousScene, scene);
|
|
42
|
+
}
|
|
43
|
+
this.previousScenes.set(normalizedFileName, scene);
|
|
44
|
+
this.sceneManager.renderScene(scene);
|
|
45
|
+
const result = scene.getRenderedObjects();
|
|
46
|
+
for (const obj of result) {
|
|
47
|
+
if (obj.sourceLocation) {
|
|
48
|
+
obj.sourceLocation.filePath = obj.sourceLocation.filePath.replace('virtual:live-render:', '');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!filePath.startsWith('virtual:live-render')) {
|
|
52
|
+
this.renderingCache.set(normalizedFileName, result);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
absPath: normalizedFileName,
|
|
56
|
+
result,
|
|
57
|
+
rollbackStop: result.length - 1,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
this.viteManager.invalidateModule();
|
|
62
|
+
console.log('Error processing file:', error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async updateLiveCode(fileName, code) {
|
|
67
|
+
const id = `virtual:live-render:${fileName}`;
|
|
68
|
+
this.viteManager.setBuffer(id, code);
|
|
69
|
+
this.renderingCache.delete(fileName);
|
|
70
|
+
return this.processFile(id, true);
|
|
71
|
+
}
|
|
72
|
+
async rollbackFromUI(index) {
|
|
73
|
+
return this.rollback(this.currentFileName, index);
|
|
74
|
+
}
|
|
75
|
+
async rollback(fileName, index) {
|
|
76
|
+
if (!this.sceneManager) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const scene = this.previousScenes.get(fileName);
|
|
80
|
+
if (!scene) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const totalObjects = scene.getAllSceneObjects().length;
|
|
84
|
+
const rollbackIndex = index >= totalObjects - 1 ? totalObjects - 1 : index;
|
|
85
|
+
this.sceneManager.rollbackScene(scene, rollbackIndex);
|
|
86
|
+
const result = scene.getRenderedObjects();
|
|
87
|
+
return {
|
|
88
|
+
absPath: fileName,
|
|
89
|
+
result,
|
|
90
|
+
rollbackStop: index,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async importFile(workspacePath, fileName, data) {
|
|
94
|
+
if (!this.sceneManager) {
|
|
95
|
+
throw new Error('SceneManager not initialized');
|
|
96
|
+
}
|
|
97
|
+
const binaryData = Buffer.from(data, 'base64');
|
|
98
|
+
await this.sceneManager.importFile(workspacePath, fileName, binaryData);
|
|
99
|
+
}
|
|
100
|
+
getShapeProperties(shapeId) {
|
|
101
|
+
if (!this.sceneManager) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const scene = this.previousScenes.get(this.currentFileName);
|
|
105
|
+
if (!scene) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return this.sceneManager.getShapeProperties(scene, shapeId);
|
|
109
|
+
}
|
|
110
|
+
getFaceProperties(shapeId, faceIndex) {
|
|
111
|
+
if (!this.sceneManager) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const scene = this.previousScenes.get(this.currentFileName);
|
|
115
|
+
if (!scene) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return this.sceneManager.getFaceProperties(scene, shapeId, faceIndex);
|
|
119
|
+
}
|
|
120
|
+
getEdgeProperties(shapeId, edgeIndex) {
|
|
121
|
+
if (!this.sceneManager) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const scene = this.previousScenes.get(this.currentFileName);
|
|
125
|
+
if (!scene) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return this.sceneManager.getEdgeProperties(scene, shapeId, edgeIndex);
|
|
129
|
+
}
|
|
130
|
+
exportShapes(shapeIds, options) {
|
|
131
|
+
if (!this.sceneManager) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const scene = this.previousScenes.get(this.currentFileName);
|
|
135
|
+
if (!scene) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return this.sceneManager.exportShapes(scene, shapeIds, options);
|
|
139
|
+
}
|
|
140
|
+
hitTest(shapeId, rayOrigin, rayDir, edgeThreshold) {
|
|
141
|
+
if (!this.sceneManager) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const scene = this.previousScenes.get(this.currentFileName);
|
|
145
|
+
if (!scene) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return this.sceneManager.hitTest(scene, shapeId, rayOrigin, rayDir, edgeThreshold);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
6
|
+
import { FluidCadServer } from "./fluidcad-server.js";
|
|
7
|
+
import { createPropertiesRouter } from "./routes/properties.js";
|
|
8
|
+
import { createActionsRouter } from "./routes/actions.js";
|
|
9
|
+
import { createExportRouter } from "./routes/export.js";
|
|
10
|
+
import { createScreenshotRouter } from "./routes/screenshot.js";
|
|
11
|
+
const PORT = parseInt(process.env.FLUIDCAD_SERVER_PORT || '3100', 10);
|
|
12
|
+
const WORKSPACE_PATH = process.env.FLUIDCAD_WORKSPACE_PATH || '';
|
|
13
|
+
const UI_DIST = path.resolve(import.meta.dirname, '../../ui/dist');
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// IPC helpers — communication with extension host process
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function sendToExtension(msg) {
|
|
18
|
+
if (process.send) {
|
|
19
|
+
process.send(msg);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Express app
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
const fluidCadServer = new FluidCadServer();
|
|
26
|
+
const app = express();
|
|
27
|
+
app.use(express.json({ limit: '50mb' }));
|
|
28
|
+
app.use('/api', createPropertiesRouter(fluidCadServer));
|
|
29
|
+
app.use('/api', createActionsRouter(fluidCadServer, sendToExtension, broadcastToUI, WORKSPACE_PATH));
|
|
30
|
+
app.use('/api', createExportRouter(fluidCadServer));
|
|
31
|
+
app.use('/api', createScreenshotRouter(requestScreenshot));
|
|
32
|
+
// Static files — serve UI build, with SPA fallback
|
|
33
|
+
app.use(express.static(UI_DIST, {
|
|
34
|
+
setHeaders(res, filePath) {
|
|
35
|
+
if (path.extname(filePath) === '.html') {
|
|
36
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
app.get('*splat', (_req, res) => {
|
|
41
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
42
|
+
res.sendFile(path.join(UI_DIST, 'index.html'));
|
|
43
|
+
});
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// HTTP + WebSocket server
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
const httpServer = http.createServer(app);
|
|
48
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
49
|
+
const uiClients = new Set();
|
|
50
|
+
let lastSceneMessage = null;
|
|
51
|
+
let initCompleteMessage = null;
|
|
52
|
+
function broadcastToUI(msg) {
|
|
53
|
+
const data = JSON.stringify(msg);
|
|
54
|
+
if (msg.type === 'scene-rendered') {
|
|
55
|
+
lastSceneMessage = data;
|
|
56
|
+
}
|
|
57
|
+
if (msg.type === 'init-complete') {
|
|
58
|
+
initCompleteMessage = data;
|
|
59
|
+
}
|
|
60
|
+
for (const client of uiClients) {
|
|
61
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
62
|
+
client.send(data);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Screenshot request/response coordination
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
const SCREENSHOT_TIMEOUT_MS = 10_000;
|
|
70
|
+
const pendingScreenshots = new Map();
|
|
71
|
+
function requestScreenshot(options) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
if (uiClients.size === 0) {
|
|
74
|
+
reject(new Error('No UI client connected.'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const requestId = crypto.randomUUID();
|
|
78
|
+
const timeout = setTimeout(() => {
|
|
79
|
+
pendingScreenshots.delete(requestId);
|
|
80
|
+
reject(new Error('Screenshot request timed out.'));
|
|
81
|
+
}, SCREENSHOT_TIMEOUT_MS);
|
|
82
|
+
pendingScreenshots.set(requestId, {
|
|
83
|
+
resolve(data) {
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
pendingScreenshots.delete(requestId);
|
|
86
|
+
resolve(data);
|
|
87
|
+
},
|
|
88
|
+
reject(err) {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
pendingScreenshots.delete(requestId);
|
|
91
|
+
reject(err);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
broadcastToUI({ type: 'take-screenshot', requestId, options });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function handleUIMessage(raw) {
|
|
98
|
+
let msg;
|
|
99
|
+
try {
|
|
100
|
+
msg = JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (msg.type === 'screenshot-result' && msg.requestId) {
|
|
106
|
+
const pending = pendingScreenshots.get(msg.requestId);
|
|
107
|
+
if (!pending) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (msg.success && msg.data) {
|
|
111
|
+
pending.resolve(Buffer.from(msg.data, 'base64'));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
pending.reject(new Error(msg.error || 'Screenshot failed.'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// WebSocket connections
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
wss.on('connection', (ws) => {
|
|
122
|
+
uiClients.add(ws);
|
|
123
|
+
// Replay init-complete and last scene to newly connected UI client
|
|
124
|
+
if (initCompleteMessage) {
|
|
125
|
+
ws.send(initCompleteMessage);
|
|
126
|
+
}
|
|
127
|
+
if (lastSceneMessage) {
|
|
128
|
+
ws.send(lastSceneMessage);
|
|
129
|
+
}
|
|
130
|
+
ws.on('message', (data) => {
|
|
131
|
+
handleUIMessage(String(data));
|
|
132
|
+
});
|
|
133
|
+
ws.on('close', () => {
|
|
134
|
+
uiClients.delete(ws);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// IPC message handling — extension host → server
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
let currentFile = null;
|
|
141
|
+
let renderVersion = 0;
|
|
142
|
+
async function handleExtensionMessage(msg) {
|
|
143
|
+
try {
|
|
144
|
+
switch (msg.type) {
|
|
145
|
+
case 'process-file': {
|
|
146
|
+
const myVersion = ++renderVersion;
|
|
147
|
+
broadcastToUI({ type: 'processing-file' });
|
|
148
|
+
currentFile = msg.filePath;
|
|
149
|
+
const data = await fluidCadServer.processFile(msg.filePath);
|
|
150
|
+
if (myVersion !== renderVersion) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (data) {
|
|
154
|
+
sendToExtension({
|
|
155
|
+
type: 'scene-rendered',
|
|
156
|
+
absPath: data.absPath,
|
|
157
|
+
result: data.result,
|
|
158
|
+
rollbackStop: data.rollbackStop,
|
|
159
|
+
});
|
|
160
|
+
broadcastToUI({
|
|
161
|
+
type: 'scene-rendered',
|
|
162
|
+
result: data.result,
|
|
163
|
+
absPath: data.absPath,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'live-update': {
|
|
169
|
+
const myVersion = ++renderVersion;
|
|
170
|
+
if (msg.fileName !== currentFile) {
|
|
171
|
+
broadcastToUI({ type: 'processing-file' });
|
|
172
|
+
currentFile = msg.fileName;
|
|
173
|
+
}
|
|
174
|
+
const data = await fluidCadServer.updateLiveCode(msg.fileName, msg.code);
|
|
175
|
+
if (myVersion !== renderVersion) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (data) {
|
|
179
|
+
sendToExtension({
|
|
180
|
+
type: 'scene-rendered',
|
|
181
|
+
absPath: data.absPath,
|
|
182
|
+
result: data.result,
|
|
183
|
+
rollbackStop: data.rollbackStop,
|
|
184
|
+
});
|
|
185
|
+
broadcastToUI({
|
|
186
|
+
type: 'scene-rendered',
|
|
187
|
+
result: data.result,
|
|
188
|
+
absPath: data.absPath,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'rollback': {
|
|
194
|
+
const myVersion = ++renderVersion;
|
|
195
|
+
const data = await fluidCadServer.rollback(msg.fileName, msg.index);
|
|
196
|
+
if (myVersion !== renderVersion) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (data) {
|
|
200
|
+
sendToExtension({
|
|
201
|
+
type: 'scene-rendered',
|
|
202
|
+
absPath: data.absPath,
|
|
203
|
+
result: data.result,
|
|
204
|
+
rollbackStop: data.rollbackStop,
|
|
205
|
+
});
|
|
206
|
+
broadcastToUI({
|
|
207
|
+
type: 'scene-rendered',
|
|
208
|
+
result: data.result,
|
|
209
|
+
absPath: data.absPath,
|
|
210
|
+
rollbackStop: data.rollbackStop,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case 'import-file': {
|
|
216
|
+
try {
|
|
217
|
+
await fluidCadServer.importFile(msg.workspacePath, msg.fileName, msg.data);
|
|
218
|
+
sendToExtension({ type: 'import-complete', success: true });
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
sendToExtension({ type: 'error', message: err.stack || err.message || String(err) });
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'highlight-shape': {
|
|
226
|
+
broadcastToUI({ type: 'highlight-shape', shapeId: msg.shapeId });
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'clear-highlight': {
|
|
230
|
+
broadcastToUI({ type: 'clear-highlight' });
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'show-shape-properties': {
|
|
234
|
+
broadcastToUI({ type: 'show-shape-properties', shapeId: msg.shapeId });
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case 'export-scene': {
|
|
238
|
+
try {
|
|
239
|
+
const result = fluidCadServer.exportShapes(msg.shapeIds, msg.options);
|
|
240
|
+
if (result) {
|
|
241
|
+
const data = typeof result.data === 'string'
|
|
242
|
+
? Buffer.from(result.data, 'utf-8').toString('base64')
|
|
243
|
+
: Buffer.from(result.data).toString('base64');
|
|
244
|
+
sendToExtension({
|
|
245
|
+
type: 'export-complete',
|
|
246
|
+
success: true,
|
|
247
|
+
data,
|
|
248
|
+
fileName: result.fileName,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
sendToExtension({ type: 'export-complete', success: false, error: 'No active scene to export.' });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
sendToExtension({ type: 'export-complete', success: false, error: err.message || String(err) });
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
sendToExtension({
|
|
264
|
+
type: 'error',
|
|
265
|
+
message: err.stack || err.message || String(err),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Listen for IPC messages from extension host
|
|
270
|
+
process.on('message', (msg) => {
|
|
271
|
+
handleExtensionMessage(msg);
|
|
272
|
+
});
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Start
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
httpServer.listen(PORT, () => {
|
|
277
|
+
const url = `http://localhost:${PORT}`;
|
|
278
|
+
console.log(`FluidCAD server listening on ${url}`);
|
|
279
|
+
// Signal ready immediately so extension can show the webview
|
|
280
|
+
sendToExtension({ type: 'ready', port: PORT, url });
|
|
281
|
+
// Initialize FluidCAD server in the background
|
|
282
|
+
fluidCadServer.init(WORKSPACE_PATH).then(() => {
|
|
283
|
+
sendToExtension({ type: 'init-complete', success: true });
|
|
284
|
+
broadcastToUI({ type: 'init-complete', success: true });
|
|
285
|
+
}).catch((err) => {
|
|
286
|
+
const error = err.stack || err.message || String(err);
|
|
287
|
+
sendToExtension({ type: 'init-complete', success: false, error });
|
|
288
|
+
broadcastToUI({ type: 'init-complete', success: false, error });
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import type { FluidCadServer } from '../fluidcad-server.ts';
|
|
3
|
+
export declare function createActionsRouter(fluidCadServer: FluidCadServer, sendToExtension: (msg: any) => void, broadcastToUI: (msg: any) => void, workspacePath: string): Router;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export function createActionsRouter(fluidCadServer, sendToExtension, broadcastToUI, workspacePath) {
|
|
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
|
+
router.post('/insert-point', (req, res) => {
|
|
17
|
+
const { point, sourceLocation } = req.body;
|
|
18
|
+
if (!Array.isArray(point) || point.length !== 2 ||
|
|
19
|
+
!sourceLocation || typeof sourceLocation.line !== 'number' || typeof sourceLocation.column !== 'number') {
|
|
20
|
+
res.status(400).json({ error: 'Invalid request body' });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
sendToExtension({
|
|
24
|
+
type: 'insert-point',
|
|
25
|
+
point: point,
|
|
26
|
+
sourceLocation,
|
|
27
|
+
});
|
|
28
|
+
res.json({ success: true });
|
|
29
|
+
});
|
|
30
|
+
router.post('/remove-point', (req, res) => {
|
|
31
|
+
const { point, sourceLocation } = req.body;
|
|
32
|
+
if (!Array.isArray(point) || point.length !== 2 ||
|
|
33
|
+
!sourceLocation || typeof sourceLocation.line !== 'number' || typeof sourceLocation.column !== 'number') {
|
|
34
|
+
res.status(400).json({ error: 'Invalid request body' });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
sendToExtension({
|
|
38
|
+
type: 'remove-point',
|
|
39
|
+
point: point,
|
|
40
|
+
sourceLocation,
|
|
41
|
+
});
|
|
42
|
+
res.json({ success: true });
|
|
43
|
+
});
|
|
44
|
+
router.post('/rollback', async (req, res) => {
|
|
45
|
+
const { index } = req.body;
|
|
46
|
+
if (typeof index !== 'number' || index < 0) {
|
|
47
|
+
res.status(400).json({ error: 'Invalid index' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const data = await fluidCadServer.rollbackFromUI(index);
|
|
51
|
+
if (!data) {
|
|
52
|
+
res.status(404).json({ error: 'No active scene' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
sendToExtension({
|
|
56
|
+
type: 'scene-rendered',
|
|
57
|
+
absPath: data.absPath,
|
|
58
|
+
result: data.result,
|
|
59
|
+
rollbackStop: data.rollbackStop,
|
|
60
|
+
});
|
|
61
|
+
broadcastToUI({
|
|
62
|
+
type: 'scene-rendered',
|
|
63
|
+
result: data.result,
|
|
64
|
+
absPath: data.absPath,
|
|
65
|
+
rollbackStop: data.rollbackStop,
|
|
66
|
+
});
|
|
67
|
+
res.json({ success: true });
|
|
68
|
+
});
|
|
69
|
+
router.post('/set-pick-points', (req, res) => {
|
|
70
|
+
const { points, sourceLocation } = req.body;
|
|
71
|
+
if (!Array.isArray(points) ||
|
|
72
|
+
!sourceLocation || typeof sourceLocation.line !== 'number' || typeof sourceLocation.column !== 'number') {
|
|
73
|
+
res.status(400).json({ error: 'Invalid request body' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
sendToExtension({
|
|
77
|
+
type: 'set-pick-points',
|
|
78
|
+
points: points,
|
|
79
|
+
sourceLocation,
|
|
80
|
+
});
|
|
81
|
+
res.json({ success: true });
|
|
82
|
+
});
|
|
83
|
+
router.post('/import-file', async (req, res) => {
|
|
84
|
+
const { fileName, data } = req.body;
|
|
85
|
+
if (typeof fileName !== 'string' || typeof data !== 'string') {
|
|
86
|
+
res.status(400).json({ error: 'Invalid request body' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await fluidCadServer.importFile(workspacePath, fileName, data);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
res.status(500).json({ error: err.message || String(err) });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const loadName = fileName.replace(/\.(step|stp)$/i, '');
|
|
97
|
+
res.json({ success: true, fileName: loadName });
|
|
98
|
+
});
|
|
99
|
+
return router;
|
|
100
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export function createExportRouter(fluidCadServer) {
|
|
3
|
+
const router = Router();
|
|
4
|
+
router.post('/export', (req, res) => {
|
|
5
|
+
const { format, shapeIds, includeColors, resolution, customAngularDeflectionDeg, customLinearDeflection } = req.body;
|
|
6
|
+
if (format !== 'step' && format !== 'stl') {
|
|
7
|
+
res.status(400).json({ error: 'Invalid format. Must be "step" or "stl".' });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (!Array.isArray(shapeIds) || shapeIds.length === 0) {
|
|
11
|
+
res.status(400).json({ error: 'shapeIds must be a non-empty array.' });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (format === 'stl') {
|
|
15
|
+
const validResolutions = ['coarse', 'medium', 'fine', 'custom'];
|
|
16
|
+
if (resolution && !validResolutions.includes(resolution)) {
|
|
17
|
+
res.status(400).json({ error: 'Invalid resolution.' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (resolution === 'custom') {
|
|
21
|
+
if (typeof customLinearDeflection !== 'number' || typeof customAngularDeflectionDeg !== 'number') {
|
|
22
|
+
res.status(400).json({ error: 'Custom resolution requires customLinearDeflection and customAngularDeflectionDeg.' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const result = fluidCadServer.exportShapes(shapeIds, {
|
|
29
|
+
format,
|
|
30
|
+
includeColors,
|
|
31
|
+
resolution: resolution || 'medium',
|
|
32
|
+
customLinearDeflection,
|
|
33
|
+
customAngularDeflectionDeg,
|
|
34
|
+
});
|
|
35
|
+
if (!result) {
|
|
36
|
+
res.status(404).json({ error: 'No active scene to export.' });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const ext = format === 'step' ? '.step' : '.stl';
|
|
40
|
+
const mimeType = format === 'step' ? 'application/step' : 'application/sla';
|
|
41
|
+
res.setHeader('Content-Type', mimeType);
|
|
42
|
+
res.setHeader('Content-Disposition', `attachment; filename="export${ext}"`);
|
|
43
|
+
if (typeof result.data === 'string') {
|
|
44
|
+
res.send(Buffer.from(result.data, 'utf-8'));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
res.send(Buffer.from(result.data));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
res.status(500).json({ error: err.message || String(err) });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return router;
|
|
55
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { getMaterials } from '../../../lib/dist/common/materials.js';
|
|
3
|
+
export function createPropertiesRouter(fluidCadServer) {
|
|
4
|
+
const router = Router();
|
|
5
|
+
router.get('/materials', (_req, res) => {
|
|
6
|
+
res.json(getMaterials());
|
|
7
|
+
});
|
|
8
|
+
router.get('/shape-properties', (req, res) => {
|
|
9
|
+
const shapeId = req.query.shapeId || '';
|
|
10
|
+
const props = fluidCadServer.getShapeProperties(shapeId);
|
|
11
|
+
if (!props) {
|
|
12
|
+
res.status(404).json({ error: 'Shape not found' });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
res.json(props);
|
|
16
|
+
});
|
|
17
|
+
router.get('/face-properties', (req, res) => {
|
|
18
|
+
const shapeId = req.query.shapeId || '';
|
|
19
|
+
const faceIndex = parseInt(req.query.faceIndex || '', 10);
|
|
20
|
+
if (!shapeId || isNaN(faceIndex) || faceIndex < 0) {
|
|
21
|
+
res.status(400).json({ error: 'Missing or invalid shapeId / faceIndex' });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const props = fluidCadServer.getFaceProperties(shapeId, faceIndex);
|
|
25
|
+
if (!props) {
|
|
26
|
+
res.status(404).json({ error: 'Face not found' });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
res.json(props);
|
|
30
|
+
});
|
|
31
|
+
router.get('/edge-properties', (req, res) => {
|
|
32
|
+
const shapeId = req.query.shapeId || '';
|
|
33
|
+
const edgeIndex = parseInt(req.query.edgeIndex || '', 10);
|
|
34
|
+
if (!shapeId || isNaN(edgeIndex) || edgeIndex < 0) {
|
|
35
|
+
res.status(400).json({ error: 'Missing or invalid shapeId / edgeIndex' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const props = fluidCadServer.getEdgeProperties(shapeId, edgeIndex);
|
|
39
|
+
if (!props) {
|
|
40
|
+
res.status(404).json({ error: 'Edge not found' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
res.json(props);
|
|
44
|
+
});
|
|
45
|
+
return router;
|
|
46
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export function createScreenshotRouter(requestScreenshot) {
|
|
3
|
+
const router = Router();
|
|
4
|
+
router.post('/screenshot', async (req, res) => {
|
|
5
|
+
const { width, height, showGrid, showAxes, transparent, autoCrop, fitToModel, margin } = req.body;
|
|
6
|
+
const options = {};
|
|
7
|
+
if (width !== undefined) {
|
|
8
|
+
if (typeof width !== 'number' || width < 1 || width > 8192) {
|
|
9
|
+
res.status(400).json({ error: 'width must be a number between 1 and 8192.' });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
options.width = width;
|
|
13
|
+
}
|
|
14
|
+
if (height !== undefined) {
|
|
15
|
+
if (typeof height !== 'number' || height < 1 || height > 8192) {
|
|
16
|
+
res.status(400).json({ error: 'height must be a number between 1 and 8192.' });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
options.height = height;
|
|
20
|
+
}
|
|
21
|
+
if (showGrid !== undefined) {
|
|
22
|
+
if (typeof showGrid !== 'boolean') {
|
|
23
|
+
res.status(400).json({ error: 'showGrid must be a boolean.' });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
options.showGrid = showGrid;
|
|
27
|
+
}
|
|
28
|
+
if (showAxes !== undefined) {
|
|
29
|
+
if (typeof showAxes !== 'boolean') {
|
|
30
|
+
res.status(400).json({ error: 'showAxes must be a boolean.' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
options.showAxes = showAxes;
|
|
34
|
+
}
|
|
35
|
+
if (transparent !== undefined) {
|
|
36
|
+
if (typeof transparent !== 'boolean') {
|
|
37
|
+
res.status(400).json({ error: 'transparent must be a boolean.' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
options.transparent = transparent;
|
|
41
|
+
}
|
|
42
|
+
if (autoCrop !== undefined) {
|
|
43
|
+
if (typeof autoCrop !== 'boolean') {
|
|
44
|
+
res.status(400).json({ error: 'autoCrop must be a boolean.' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
options.autoCrop = autoCrop;
|
|
48
|
+
}
|
|
49
|
+
if (fitToModel !== undefined) {
|
|
50
|
+
if (typeof fitToModel !== 'boolean') {
|
|
51
|
+
res.status(400).json({ error: 'fitToModel must be a boolean.' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
options.fitToModel = fitToModel;
|
|
55
|
+
}
|
|
56
|
+
if (margin !== undefined) {
|
|
57
|
+
if (typeof margin !== 'number' || margin < 0) {
|
|
58
|
+
res.status(400).json({ error: 'margin must be a non-negative number.' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
options.margin = margin;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const png = await requestScreenshot(options);
|
|
65
|
+
res.setHeader('Content-Type', 'image/png');
|
|
66
|
+
res.setHeader('Content-Disposition', 'inline; filename="screenshot.png"');
|
|
67
|
+
res.send(png);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const message = err.message || String(err);
|
|
71
|
+
const status = message.includes('No UI client') || message.includes('timed out') ? 503 : 500;
|
|
72
|
+
res.status(status).json({ error: message });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return router;
|
|
76
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ViteDevServer } from 'vite';
|
|
2
|
+
export declare class ViteManager {
|
|
3
|
+
server: ViteDevServer;
|
|
4
|
+
private rootPath;
|
|
5
|
+
private buffers;
|
|
6
|
+
init(rootPath: string): Promise<void>;
|
|
7
|
+
setBuffer(id: string, code: string): void;
|
|
8
|
+
loadModule(filePath: string): Promise<Record<string, any>>;
|
|
9
|
+
invalidateModule(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createServer } from 'vite';
|
|
2
|
+
import { dirname, resolve, isAbsolute } from 'path';
|
|
3
|
+
const BLOCKED_NODE_MODULES = new Set([
|
|
4
|
+
'fs',
|
|
5
|
+
'child_process',
|
|
6
|
+
'net',
|
|
7
|
+
'dgram',
|
|
8
|
+
'tls',
|
|
9
|
+
'http',
|
|
10
|
+
'https',
|
|
11
|
+
'http2',
|
|
12
|
+
'os',
|
|
13
|
+
'worker_threads',
|
|
14
|
+
'vm',
|
|
15
|
+
'cluster',
|
|
16
|
+
'dns',
|
|
17
|
+
'module',
|
|
18
|
+
]);
|
|
19
|
+
function getBlockedNodeModule(id) {
|
|
20
|
+
let name = id;
|
|
21
|
+
if (name.startsWith('node:')) {
|
|
22
|
+
name = name.slice(5);
|
|
23
|
+
}
|
|
24
|
+
const baseName = name.split('/')[0];
|
|
25
|
+
return BLOCKED_NODE_MODULES.has(baseName) ? baseName : null;
|
|
26
|
+
}
|
|
27
|
+
export class ViteManager {
|
|
28
|
+
server;
|
|
29
|
+
rootPath = '';
|
|
30
|
+
buffers = new Map();
|
|
31
|
+
async init(rootPath) {
|
|
32
|
+
this.rootPath = rootPath;
|
|
33
|
+
const that = this;
|
|
34
|
+
this.server = await createServer({
|
|
35
|
+
root: rootPath,
|
|
36
|
+
server: {
|
|
37
|
+
watch: {
|
|
38
|
+
ignoreInitial: true,
|
|
39
|
+
ignored: ['**/node_modules/**']
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
optimizeDeps: {
|
|
43
|
+
noDiscovery: true,
|
|
44
|
+
include: []
|
|
45
|
+
},
|
|
46
|
+
plugins: [
|
|
47
|
+
{
|
|
48
|
+
name: 'virtual-module',
|
|
49
|
+
resolveId(id, importer) {
|
|
50
|
+
const blockedModule = getBlockedNodeModule(id);
|
|
51
|
+
if (blockedModule) {
|
|
52
|
+
throw new Error(`Module "${id}" is not allowed in FluidCAD scripts. ` +
|
|
53
|
+
`Access to Node.js "${blockedModule}" module is restricted for security.`);
|
|
54
|
+
}
|
|
55
|
+
if (id.startsWith('virtual:')) {
|
|
56
|
+
return id;
|
|
57
|
+
}
|
|
58
|
+
// Resolve relative imports from virtual modules against the real file path
|
|
59
|
+
if (importer && importer.startsWith('virtual:live-render:') && !isAbsolute(id)) {
|
|
60
|
+
const realImporter = importer.replace('virtual:live-render:', '');
|
|
61
|
+
return resolve(dirname(realImporter), id);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
load(id) {
|
|
65
|
+
if (id.startsWith('virtual:live-render')) {
|
|
66
|
+
let mod = this.getModuleInfo(id);
|
|
67
|
+
if (mod) {
|
|
68
|
+
that.server.moduleGraph.invalidateModule(that.server.moduleGraph.getModuleById(id));
|
|
69
|
+
}
|
|
70
|
+
return that.buffers.get(id) || '';
|
|
71
|
+
}
|
|
72
|
+
else if (that.buffers.has(`virtual:live-render:${id}`)) {
|
|
73
|
+
return that.buffers.get(`virtual:live-render:${id}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
setBuffer(id, code) {
|
|
81
|
+
this.buffers.set(id, code);
|
|
82
|
+
}
|
|
83
|
+
async loadModule(filePath) {
|
|
84
|
+
return this.server.ssrLoadModule(filePath);
|
|
85
|
+
}
|
|
86
|
+
invalidateModule() {
|
|
87
|
+
for (const [id, mod] of this.server.moduleGraph.idToModuleMap) {
|
|
88
|
+
if (id.startsWith(this.rootPath) || id.startsWith('virtual:live-render')) {
|
|
89
|
+
this.server.moduleGraph.invalidateModule(mod);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export type ProcessFileMessage = {
|
|
2
|
+
type: 'process-file';
|
|
3
|
+
filePath: string;
|
|
4
|
+
};
|
|
5
|
+
export type LiveUpdateMessage = {
|
|
6
|
+
type: 'live-update';
|
|
7
|
+
fileName: string;
|
|
8
|
+
code: string;
|
|
9
|
+
};
|
|
10
|
+
export type RollbackMessage = {
|
|
11
|
+
type: 'rollback';
|
|
12
|
+
fileName: string;
|
|
13
|
+
index: number;
|
|
14
|
+
};
|
|
15
|
+
export type ImportFileMessage = {
|
|
16
|
+
type: 'import-file';
|
|
17
|
+
workspacePath: string;
|
|
18
|
+
fileName: string;
|
|
19
|
+
data: string;
|
|
20
|
+
};
|
|
21
|
+
export type HighlightShapeMessage = {
|
|
22
|
+
type: 'highlight-shape';
|
|
23
|
+
shapeId: string;
|
|
24
|
+
};
|
|
25
|
+
export type ClearHighlightMessage = {
|
|
26
|
+
type: 'clear-highlight';
|
|
27
|
+
};
|
|
28
|
+
export type ShowShapePropertiesMessage = {
|
|
29
|
+
type: 'show-shape-properties';
|
|
30
|
+
shapeId: string;
|
|
31
|
+
};
|
|
32
|
+
export type ExportSceneMessage = {
|
|
33
|
+
type: 'export-scene';
|
|
34
|
+
shapeIds: string[];
|
|
35
|
+
options: {
|
|
36
|
+
format: 'step' | 'stl';
|
|
37
|
+
includeColors?: boolean;
|
|
38
|
+
resolution?: string;
|
|
39
|
+
customLinearDeflection?: number;
|
|
40
|
+
customAngularDeflectionDeg?: number;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export type ExtensionMessage = ProcessFileMessage | LiveUpdateMessage | RollbackMessage | ImportFileMessage | HighlightShapeMessage | ClearHighlightMessage | ShowShapePropertiesMessage | ExportSceneMessage;
|
|
44
|
+
export type ReadyMessage = {
|
|
45
|
+
type: 'ready';
|
|
46
|
+
port: number;
|
|
47
|
+
url: string;
|
|
48
|
+
};
|
|
49
|
+
export type InitCompleteMessage = {
|
|
50
|
+
type: 'init-complete';
|
|
51
|
+
success: boolean;
|
|
52
|
+
error?: string;
|
|
53
|
+
};
|
|
54
|
+
export type SceneRenderedMessage = {
|
|
55
|
+
type: 'scene-rendered';
|
|
56
|
+
absPath: string;
|
|
57
|
+
result: any[];
|
|
58
|
+
rollbackStop: number;
|
|
59
|
+
};
|
|
60
|
+
export type ErrorMessage = {
|
|
61
|
+
type: 'error';
|
|
62
|
+
message: string;
|
|
63
|
+
};
|
|
64
|
+
export type ImportCompleteMessage = {
|
|
65
|
+
type: 'import-complete';
|
|
66
|
+
success: boolean;
|
|
67
|
+
};
|
|
68
|
+
export type InsertPointMessage = {
|
|
69
|
+
type: 'insert-point';
|
|
70
|
+
point: [number, number];
|
|
71
|
+
sourceLocation: {
|
|
72
|
+
line: number;
|
|
73
|
+
column: number;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export type RemovePointMessage = {
|
|
77
|
+
type: 'remove-point';
|
|
78
|
+
point: [number, number];
|
|
79
|
+
sourceLocation: {
|
|
80
|
+
line: number;
|
|
81
|
+
column: number;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
export type SetPickPointsMessage = {
|
|
85
|
+
type: 'set-pick-points';
|
|
86
|
+
points: [number, number][];
|
|
87
|
+
sourceLocation: {
|
|
88
|
+
line: number;
|
|
89
|
+
column: number;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
export type ExportCompleteMessage = {
|
|
93
|
+
type: 'export-complete';
|
|
94
|
+
success: boolean;
|
|
95
|
+
data?: string;
|
|
96
|
+
fileName?: string;
|
|
97
|
+
error?: string;
|
|
98
|
+
};
|
|
99
|
+
export type ServerToExtensionMessage = ReadyMessage | InitCompleteMessage | SceneRenderedMessage | ErrorMessage | ImportCompleteMessage | InsertPointMessage | RemovePointMessage | SetPickPointsMessage | ExportCompleteMessage;
|
|
100
|
+
export type UISceneRenderedMessage = {
|
|
101
|
+
type: 'scene-rendered';
|
|
102
|
+
result: any[];
|
|
103
|
+
absPath: string;
|
|
104
|
+
rollbackStop?: number;
|
|
105
|
+
};
|
|
106
|
+
export type UIHighlightShapeMessage = {
|
|
107
|
+
type: 'highlight-shape';
|
|
108
|
+
shapeId: string;
|
|
109
|
+
};
|
|
110
|
+
export type UIClearHighlightMessage = {
|
|
111
|
+
type: 'clear-highlight';
|
|
112
|
+
};
|
|
113
|
+
export type UIShowShapePropertiesMessage = {
|
|
114
|
+
type: 'show-shape-properties';
|
|
115
|
+
shapeId: string;
|
|
116
|
+
};
|
|
117
|
+
export type UIInitCompleteMessage = {
|
|
118
|
+
type: 'init-complete';
|
|
119
|
+
success: boolean;
|
|
120
|
+
error?: string;
|
|
121
|
+
};
|
|
122
|
+
export type UIProcessingFileMessage = {
|
|
123
|
+
type: 'processing-file';
|
|
124
|
+
};
|
|
125
|
+
export type UITakeScreenshotMessage = {
|
|
126
|
+
type: 'take-screenshot';
|
|
127
|
+
requestId: string;
|
|
128
|
+
options: {
|
|
129
|
+
width?: number;
|
|
130
|
+
height?: number;
|
|
131
|
+
showGrid?: boolean;
|
|
132
|
+
showAxes?: boolean;
|
|
133
|
+
transparent?: boolean;
|
|
134
|
+
autoCrop?: boolean;
|
|
135
|
+
margin?: number;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
export type ServerToUIMessage = UIInitCompleteMessage | UIProcessingFileMessage | UISceneRenderedMessage | UIHighlightShapeMessage | UIClearHighlightMessage | UIShowShapePropertiesMessage | UITakeScreenshotMessage;
|