fluidcad 0.0.5 → 0.0.7

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.
@@ -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,2 @@
1
+ import { Router } from 'express';
2
+ export declare function createScreenshotRouter(requestScreenshot: (options: Record<string, unknown>) => Promise<Buffer>): Router;
@@ -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,111 @@
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
+ const IMPORT_PATTERN = /\b(?:import|export)\s[\s\S]*?from\s+['"]([^'"]+)['"]|\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
28
+ function scanForBlockedImports(code) {
29
+ let match;
30
+ IMPORT_PATTERN.lastIndex = 0;
31
+ while ((match = IMPORT_PATTERN.exec(code)) !== null) {
32
+ const specifier = match[1] || match[2];
33
+ const blocked = getBlockedNodeModule(specifier);
34
+ if (blocked) {
35
+ return specifier;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ export class ViteManager {
41
+ server;
42
+ rootPath = '';
43
+ buffers = new Map();
44
+ async init(rootPath) {
45
+ this.rootPath = rootPath;
46
+ const that = this;
47
+ this.server = await createServer({
48
+ root: rootPath,
49
+ server: {
50
+ watch: {
51
+ ignoreInitial: true,
52
+ ignored: ['**/node_modules/**']
53
+ }
54
+ },
55
+ optimizeDeps: {
56
+ noDiscovery: true,
57
+ include: []
58
+ },
59
+ plugins: [
60
+ {
61
+ name: 'virtual-module',
62
+ resolveId(id, importer) {
63
+ if (id.startsWith('virtual:')) {
64
+ return id;
65
+ }
66
+ // Resolve relative imports from virtual modules against the real file path
67
+ if (importer && importer.startsWith('virtual:live-render:') && !isAbsolute(id)) {
68
+ const realImporter = importer.replace('virtual:live-render:', '');
69
+ return resolve(dirname(realImporter), id);
70
+ }
71
+ },
72
+ transform(code, id) {
73
+ if (id.startsWith(rootPath) || id.startsWith('virtual:live-render')) {
74
+ const blocked = scanForBlockedImports(code);
75
+ if (blocked) {
76
+ const moduleName = getBlockedNodeModule(blocked);
77
+ throw new Error(`Module "${blocked}" is not allowed in FluidCAD scripts. ` +
78
+ `Access to Node.js "${moduleName}" module is restricted for security.`);
79
+ }
80
+ }
81
+ },
82
+ load(id) {
83
+ if (id.startsWith('virtual:live-render')) {
84
+ let mod = this.getModuleInfo(id);
85
+ if (mod) {
86
+ that.server.moduleGraph.invalidateModule(that.server.moduleGraph.getModuleById(id));
87
+ }
88
+ return that.buffers.get(id) || '';
89
+ }
90
+ else if (that.buffers.has(`virtual:live-render:${id}`)) {
91
+ return that.buffers.get(`virtual:live-render:${id}`);
92
+ }
93
+ }
94
+ }
95
+ ]
96
+ });
97
+ }
98
+ setBuffer(id, code) {
99
+ this.buffers.set(id, code);
100
+ }
101
+ async loadModule(filePath) {
102
+ return this.server.ssrLoadModule(filePath);
103
+ }
104
+ invalidateModule() {
105
+ for (const [id, mod] of this.server.moduleGraph.idToModuleMap) {
106
+ if (id.startsWith(this.rootPath) || id.startsWith('virtual:live-render')) {
107
+ this.server.moduleGraph.invalidateModule(mod);
108
+ }
109
+ }
110
+ }
111
+ }
@@ -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;
@@ -0,0 +1,4 @@
1
+ // ---------------------------------------------------------------------------
2
+ // IPC: Extension → Server messages
3
+ // ---------------------------------------------------------------------------
4
+ export {};