cyclecad 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/app/index.html +106 -2
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +967 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1067 -0
- package/app/js/modules/collaboration-module.js +1102 -0
- package/app/js/modules/data-module.js +1656 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +1173 -0
- package/app/js/modules/inspection-module.js +937 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +957 -0
- package/app/js/modules/rendering-module.js +1306 -0
- package/app/js/modules/scripting-module.js +955 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +1032 -90
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +728 -0
- package/app/js/modules/version-module.js +1410 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/.github/scripts/cad-diff.js +0 -590
- package/.github/workflows/cad-diff.yml +0 -117
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file surface-module.js
|
|
3
|
+
* @description Surface Modeling Module — NURBS surfaces, patches, trims.
|
|
4
|
+
* Brings Fusion 360's Surface workspace to cycleCAD.
|
|
5
|
+
* Works with B-Rep kernel for exact geometry, mesh fallback for preview.
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
* @author Sachin Kumar <vvlars@googlemail.com>
|
|
9
|
+
* @license MIT
|
|
10
|
+
* @module surface
|
|
11
|
+
* @requires viewport, operations
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Surface Modeling Module
|
|
18
|
+
* Handles NURBS surfaces, patches, trims, and surface operations.
|
|
19
|
+
*/
|
|
20
|
+
const SurfaceModule = (() => {
|
|
21
|
+
const MODULE_NAME = 'surface';
|
|
22
|
+
let viewport = null;
|
|
23
|
+
let scene = null;
|
|
24
|
+
let surfaceManager = null;
|
|
25
|
+
let ui = null;
|
|
26
|
+
|
|
27
|
+
// Surface storage
|
|
28
|
+
const surfaces = new Map(); // id -> { type, geometry, mesh, edges, normal, metadata }
|
|
29
|
+
const surfaceCounter = { count: 0 };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the Surface Module
|
|
33
|
+
* @param {Object} deps - Dependencies { viewport, scene }
|
|
34
|
+
*/
|
|
35
|
+
function init(deps) {
|
|
36
|
+
viewport = deps.viewport;
|
|
37
|
+
scene = deps.scene;
|
|
38
|
+
surfaceManager = createSurfaceManager();
|
|
39
|
+
registerCommands();
|
|
40
|
+
window.addEventListener('keydown', handleKeyboard);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create surface manager with B-Rep dispatch
|
|
45
|
+
*/
|
|
46
|
+
function createSurfaceManager() {
|
|
47
|
+
return {
|
|
48
|
+
kernel: null,
|
|
49
|
+
async setKernel(k) { this.kernel = k; },
|
|
50
|
+
async execBrep(op, params) {
|
|
51
|
+
if (!this.kernel?.status === 'active') return null;
|
|
52
|
+
return this.kernel.exec(`surface.${op}`, params);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create surface from extrude profile (open wire → surface)
|
|
59
|
+
* @param {THREE.Vector3} direction - Extrude direction
|
|
60
|
+
* @param {number} distance - Extrude distance
|
|
61
|
+
* @param {THREE.BufferGeometry|Object} profileOrId - Open profile/wire
|
|
62
|
+
* @returns {Object} Surface object
|
|
63
|
+
*/
|
|
64
|
+
async function extrudeSurface(profileOrId, direction, distance) {
|
|
65
|
+
const profileId = typeof profileOrId === 'string' ? profileOrId : null;
|
|
66
|
+
const profile = profileId ? surfaces.get(profileId) : profileOrId;
|
|
67
|
+
|
|
68
|
+
if (!profile) throw new Error('Invalid profile for extrude surface');
|
|
69
|
+
|
|
70
|
+
const id = `surface_extrude_${surfaceCounter.count++}`;
|
|
71
|
+
|
|
72
|
+
// Try B-Rep first
|
|
73
|
+
if (surfaceManager.kernel) {
|
|
74
|
+
try {
|
|
75
|
+
const brepResult = await surfaceManager.execBrep('extrudeSurface', {
|
|
76
|
+
profileId,
|
|
77
|
+
direction: { x: direction.x, y: direction.y, z: direction.z },
|
|
78
|
+
distance,
|
|
79
|
+
});
|
|
80
|
+
if (brepResult) {
|
|
81
|
+
surfaces.set(id, {
|
|
82
|
+
type: 'extrude_surface',
|
|
83
|
+
brep: brepResult,
|
|
84
|
+
mesh: brepToMesh(brepResult),
|
|
85
|
+
createdAt: Date.now(),
|
|
86
|
+
});
|
|
87
|
+
return { id, type: 'extrude_surface', distance, direction };
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.warn('[Surface] B-Rep failed, falling back to mesh:', e.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fallback: mesh extrude
|
|
95
|
+
const mesh = createExtrudeSurfaceMesh(profile, direction, distance);
|
|
96
|
+
surfaces.set(id, {
|
|
97
|
+
type: 'extrude_surface',
|
|
98
|
+
mesh,
|
|
99
|
+
geometry: mesh.geometry,
|
|
100
|
+
createdAt: Date.now(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (viewport?.scene) {
|
|
104
|
+
mesh.material.side = THREE.DoubleSide;
|
|
105
|
+
mesh.material.wireframe = false;
|
|
106
|
+
mesh.material.transparent = true;
|
|
107
|
+
mesh.material.opacity = 0.8;
|
|
108
|
+
viewport.scene.add(mesh);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { id, type: 'extrude_surface', distance, direction };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Revolve profile around axis into surface
|
|
116
|
+
* @param {Object} profileOrId - Open profile
|
|
117
|
+
* @param {THREE.Vector3} axisOrigin - Axis origin
|
|
118
|
+
* @param {THREE.Vector3} axisDir - Axis direction
|
|
119
|
+
* @param {number} angle - Revolution angle in radians
|
|
120
|
+
* @returns {Object} Surface object
|
|
121
|
+
*/
|
|
122
|
+
async function revolveSurface(profileOrId, axisOrigin, axisDir, angle = Math.PI * 2) {
|
|
123
|
+
const profileId = typeof profileOrId === 'string' ? profileOrId : null;
|
|
124
|
+
const profile = profileId ? surfaces.get(profileId) : profileOrId;
|
|
125
|
+
|
|
126
|
+
if (!profile) throw new Error('Invalid profile for revolve surface');
|
|
127
|
+
|
|
128
|
+
const id = `surface_revolve_${surfaceCounter.count++}`;
|
|
129
|
+
|
|
130
|
+
// B-Rep attempt
|
|
131
|
+
if (surfaceManager.kernel) {
|
|
132
|
+
try {
|
|
133
|
+
const brepResult = await surfaceManager.execBrep('revolveSurface', {
|
|
134
|
+
profileId,
|
|
135
|
+
axisOrigin: { x: axisOrigin.x, y: axisOrigin.y, z: axisOrigin.z },
|
|
136
|
+
axisDir: { x: axisDir.x, y: axisDir.y, z: axisDir.z },
|
|
137
|
+
angle,
|
|
138
|
+
});
|
|
139
|
+
if (brepResult) {
|
|
140
|
+
surfaces.set(id, { type: 'revolve_surface', brep: brepResult, mesh: brepToMesh(brepResult) });
|
|
141
|
+
return { id, type: 'revolve_surface', angle };
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.warn('[Surface] B-Rep revolve failed:', e.message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Mesh fallback: LatheGeometry
|
|
149
|
+
const mesh = createRevolveSurfaceMesh(profile, axisOrigin, axisDir, angle);
|
|
150
|
+
surfaces.set(id, { type: 'revolve_surface', mesh, geometry: mesh.geometry });
|
|
151
|
+
|
|
152
|
+
if (viewport?.scene) {
|
|
153
|
+
mesh.material.side = THREE.DoubleSide;
|
|
154
|
+
viewport.scene.add(mesh);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { id, type: 'revolve_surface', angle };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Sweep profile along path into surface (single rail)
|
|
162
|
+
* @param {Object} profileOrId - Open profile
|
|
163
|
+
* @param {Object} pathOrId - Path curve
|
|
164
|
+
* @param {Object} options - { normal, keepNormal, scale }
|
|
165
|
+
* @returns {Object} Surface object
|
|
166
|
+
*/
|
|
167
|
+
async function sweepSurface(profileOrId, pathOrId, options = {}) {
|
|
168
|
+
const profile = typeof profileOrId === 'string' ? surfaces.get(profileOrId) : profileOrId;
|
|
169
|
+
const path = typeof pathOrId === 'string' ? surfaces.get(pathOrId) : pathOrId;
|
|
170
|
+
|
|
171
|
+
if (!profile || !path) throw new Error('Invalid profile or path for sweep');
|
|
172
|
+
|
|
173
|
+
const id = `surface_sweep_${surfaceCounter.count++}`;
|
|
174
|
+
|
|
175
|
+
// B-Rep attempt
|
|
176
|
+
if (surfaceManager.kernel) {
|
|
177
|
+
try {
|
|
178
|
+
const result = await surfaceManager.execBrep('sweepSurface', {
|
|
179
|
+
profileId: typeof profileOrId === 'string' ? profileOrId : null,
|
|
180
|
+
pathId: typeof pathOrId === 'string' ? pathOrId : null,
|
|
181
|
+
options,
|
|
182
|
+
});
|
|
183
|
+
if (result) {
|
|
184
|
+
surfaces.set(id, { type: 'sweep_surface', brep: result, mesh: brepToMesh(result) });
|
|
185
|
+
return { id, type: 'sweep_surface' };
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.warn('[Surface] B-Rep sweep failed:', e.message);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Mesh fallback
|
|
193
|
+
const mesh = createSweepSurfaceMesh(profile, path, options);
|
|
194
|
+
surfaces.set(id, { type: 'sweep_surface', mesh, geometry: mesh.geometry });
|
|
195
|
+
|
|
196
|
+
if (viewport?.scene) {
|
|
197
|
+
mesh.material.side = THREE.DoubleSide;
|
|
198
|
+
viewport.scene.add(mesh);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { id, type: 'sweep_surface' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Loft surface between multiple profiles
|
|
206
|
+
* @param {Array} profileIds - Array of profile wire/curve IDs
|
|
207
|
+
* @param {Object} options - { continuity, periodic, ruled }
|
|
208
|
+
* @returns {Object} Surface object
|
|
209
|
+
*/
|
|
210
|
+
async function loftSurface(profileIds, options = {}) {
|
|
211
|
+
if (!Array.isArray(profileIds) || profileIds.length < 2) {
|
|
212
|
+
throw new Error('Loft requires at least 2 profiles');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const id = `surface_loft_${surfaceCounter.count++}`;
|
|
216
|
+
|
|
217
|
+
// B-Rep attempt
|
|
218
|
+
if (surfaceManager.kernel) {
|
|
219
|
+
try {
|
|
220
|
+
const result = await surfaceManager.execBrep('loftSurface', { profileIds, options });
|
|
221
|
+
if (result) {
|
|
222
|
+
surfaces.set(id, { type: 'loft_surface', brep: result, mesh: brepToMesh(result) });
|
|
223
|
+
return { id, type: 'loft_surface', profileCount: profileIds.length };
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.warn('[Surface] B-Rep loft failed:', e.message);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Mesh fallback: interpolate between profile meshes
|
|
231
|
+
const profiles = profileIds.map(pid => surfaces.get(pid)).filter(Boolean);
|
|
232
|
+
const mesh = createLoftSurfaceMesh(profiles, options);
|
|
233
|
+
surfaces.set(id, { type: 'loft_surface', mesh, geometry: mesh.geometry });
|
|
234
|
+
|
|
235
|
+
if (viewport?.scene) {
|
|
236
|
+
mesh.material.side = THREE.DoubleSide;
|
|
237
|
+
viewport.scene.add(mesh);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { id, type: 'loft_surface', profileCount: profiles.length };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Fill boundary loop with Coons patch surface
|
|
245
|
+
* @param {THREE.Curve|Array<THREE.Vector3>} boundaryLoop - Closed curve/points
|
|
246
|
+
* @param {Object} options - { continuity, method }
|
|
247
|
+
* @returns {Object} Surface object
|
|
248
|
+
*/
|
|
249
|
+
async function patchSurface(boundaryLoop, options = {}) {
|
|
250
|
+
const id = `surface_patch_${surfaceCounter.count++}`;
|
|
251
|
+
|
|
252
|
+
// B-Rep attempt
|
|
253
|
+
if (surfaceManager.kernel) {
|
|
254
|
+
try {
|
|
255
|
+
const result = await surfaceManager.execBrep('patchSurface', { boundaryLoop, options });
|
|
256
|
+
if (result) {
|
|
257
|
+
surfaces.set(id, { type: 'patch', brep: result, mesh: brepToMesh(result) });
|
|
258
|
+
return { id, type: 'patch' };
|
|
259
|
+
}
|
|
260
|
+
} catch (e) {
|
|
261
|
+
console.warn('[Surface] B-Rep patch failed:', e.message);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Mesh fallback: Coons patch approximation
|
|
266
|
+
const mesh = createPatchSurfaceMesh(boundaryLoop, options);
|
|
267
|
+
surfaces.set(id, { type: 'patch', mesh, geometry: mesh.geometry });
|
|
268
|
+
|
|
269
|
+
if (viewport?.scene) {
|
|
270
|
+
mesh.material.side = THREE.DoubleSide;
|
|
271
|
+
mesh.material.color.setHex(0x4080ff);
|
|
272
|
+
viewport.scene.add(mesh);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { id, type: 'patch' };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Trim surface with curve or another surface
|
|
280
|
+
* @param {string} surfaceId - Surface to trim
|
|
281
|
+
* @param {Object} trimCurveOrSurface - Trimming geometry
|
|
282
|
+
* @returns {Object} Trimmed surface object
|
|
283
|
+
*/
|
|
284
|
+
async function trimSurface(surfaceId, trimCurveOrSurface) {
|
|
285
|
+
const surface = surfaces.get(surfaceId);
|
|
286
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
287
|
+
|
|
288
|
+
const id = `surface_trim_${surfaceCounter.count++}`;
|
|
289
|
+
|
|
290
|
+
// B-Rep attempt
|
|
291
|
+
if (surfaceManager.kernel) {
|
|
292
|
+
try {
|
|
293
|
+
const result = await surfaceManager.execBrep('trimSurface', { surfaceId, trimCurveOrSurface });
|
|
294
|
+
if (result) {
|
|
295
|
+
surfaces.set(id, { type: 'trimmed_surface', brep: result, parent: surfaceId, mesh: brepToMesh(result) });
|
|
296
|
+
return { id, type: 'trimmed_surface', parent: surfaceId };
|
|
297
|
+
}
|
|
298
|
+
} catch (e) {
|
|
299
|
+
console.warn('[Surface] B-Rep trim failed:', e.message);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Mesh fallback: visual trim (hide regions)
|
|
304
|
+
const trimmedMesh = surface.mesh.clone();
|
|
305
|
+
trimmedMesh.material = trimmedMesh.material.clone();
|
|
306
|
+
surfaces.set(id, { type: 'trimmed_surface', mesh: trimmedMesh, parent: surfaceId });
|
|
307
|
+
|
|
308
|
+
if (viewport?.scene) {
|
|
309
|
+
viewport.scene.add(trimmedMesh);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { id, type: 'trimmed_surface', parent: surfaceId };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Extend surface edge by distance
|
|
317
|
+
* @param {string} surfaceId - Surface to extend
|
|
318
|
+
* @param {number} edgeIndex - Edge index
|
|
319
|
+
* @param {number} distance - Extension distance
|
|
320
|
+
* @returns {Object} Extended surface object
|
|
321
|
+
*/
|
|
322
|
+
async function extendSurface(surfaceId, edgeIndex, distance) {
|
|
323
|
+
const surface = surfaces.get(surfaceId);
|
|
324
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
325
|
+
|
|
326
|
+
const id = `surface_extend_${surfaceCounter.count++}`;
|
|
327
|
+
|
|
328
|
+
// B-Rep attempt
|
|
329
|
+
if (surfaceManager.kernel) {
|
|
330
|
+
try {
|
|
331
|
+
const result = await surfaceManager.execBrep('extendSurface', { surfaceId, edgeIndex, distance });
|
|
332
|
+
if (result) {
|
|
333
|
+
surfaces.set(id, { type: 'extended_surface', brep: result, mesh: brepToMesh(result) });
|
|
334
|
+
return { id, type: 'extended_surface', distance };
|
|
335
|
+
}
|
|
336
|
+
} catch (e) {
|
|
337
|
+
console.warn('[Surface] B-Rep extend failed:', e.message);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Mesh fallback
|
|
342
|
+
const extendedMesh = surface.mesh.clone();
|
|
343
|
+
surfaces.set(id, { type: 'extended_surface', mesh: extendedMesh });
|
|
344
|
+
|
|
345
|
+
if (viewport?.scene) {
|
|
346
|
+
viewport.scene.add(extendedMesh);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { id, type: 'extended_surface', distance };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create parallel offset of surface
|
|
354
|
+
* @param {string} surfaceId - Surface to offset
|
|
355
|
+
* @param {number} distance - Offset distance
|
|
356
|
+
* @returns {Object} Offset surface object
|
|
357
|
+
*/
|
|
358
|
+
async function offsetSurface(surfaceId, distance) {
|
|
359
|
+
const surface = surfaces.get(surfaceId);
|
|
360
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
361
|
+
|
|
362
|
+
const id = `surface_offset_${surfaceCounter.count++}`;
|
|
363
|
+
|
|
364
|
+
// B-Rep attempt
|
|
365
|
+
if (surfaceManager.kernel) {
|
|
366
|
+
try {
|
|
367
|
+
const result = await surfaceManager.execBrep('offsetSurface', { surfaceId, distance });
|
|
368
|
+
if (result) {
|
|
369
|
+
surfaces.set(id, { type: 'offset_surface', brep: result, parent: surfaceId, mesh: brepToMesh(result) });
|
|
370
|
+
return { id, type: 'offset_surface', distance };
|
|
371
|
+
}
|
|
372
|
+
} catch (e) {
|
|
373
|
+
console.warn('[Surface] B-Rep offset failed:', e.message);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Mesh fallback: scale geometry slightly
|
|
378
|
+
const offsetMesh = surface.mesh.clone();
|
|
379
|
+
offsetMesh.geometry = offsetMesh.geometry.clone();
|
|
380
|
+
offsetMesh.scale(1 + distance / 100);
|
|
381
|
+
surfaces.set(id, { type: 'offset_surface', mesh: offsetMesh, parent: surfaceId });
|
|
382
|
+
|
|
383
|
+
if (viewport?.scene) {
|
|
384
|
+
viewport.scene.add(offsetMesh);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { id, type: 'offset_surface', distance };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Convert surface to solid by adding thickness
|
|
392
|
+
* @param {string} surfaceId - Surface to thicken
|
|
393
|
+
* @param {number} thickness - Thickness (positive = outside, negative = inside)
|
|
394
|
+
* @returns {Object} Solid body object
|
|
395
|
+
*/
|
|
396
|
+
async function thickenSurface(surfaceId, thickness) {
|
|
397
|
+
const surface = surfaces.get(surfaceId);
|
|
398
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
399
|
+
|
|
400
|
+
const id = `solid_thickened_${surfaceCounter.count++}`;
|
|
401
|
+
|
|
402
|
+
// B-Rep attempt
|
|
403
|
+
if (surfaceManager.kernel) {
|
|
404
|
+
try {
|
|
405
|
+
const result = await surfaceManager.execBrep('thickenSurface', { surfaceId, thickness });
|
|
406
|
+
if (result) {
|
|
407
|
+
// Create solid geometry
|
|
408
|
+
return { id, type: 'solid', source: surfaceId, thickness };
|
|
409
|
+
}
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.warn('[Surface] B-Rep thicken failed:', e.message);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Mesh fallback: create shell
|
|
416
|
+
const solidMesh = surface.mesh.clone();
|
|
417
|
+
solidMesh.material = new THREE.MeshPhongMaterial({ color: 0x4488ff, side: THREE.FrontSide });
|
|
418
|
+
surfaces.set(id, { type: 'solid', mesh: solidMesh, thickness });
|
|
419
|
+
|
|
420
|
+
if (viewport?.scene) {
|
|
421
|
+
viewport.scene.add(solidMesh);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { id, type: 'solid', source: surfaceId, thickness };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Join adjacent surfaces into closed solid
|
|
429
|
+
* @param {Array<string>} surfaceIds - Surface IDs to stitch
|
|
430
|
+
* @returns {Object} Solid body object
|
|
431
|
+
*/
|
|
432
|
+
async function stitchSurfaces(surfaceIds) {
|
|
433
|
+
if (!Array.isArray(surfaceIds) || surfaceIds.length < 2) {
|
|
434
|
+
throw new Error('Stitch requires at least 2 surfaces');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const id = `solid_stitched_${surfaceCounter.count++}`;
|
|
438
|
+
|
|
439
|
+
// B-Rep attempt
|
|
440
|
+
if (surfaceManager.kernel) {
|
|
441
|
+
try {
|
|
442
|
+
const result = await surfaceManager.execBrep('stitchSurfaces', { surfaceIds });
|
|
443
|
+
if (result) {
|
|
444
|
+
return { id, type: 'solid', sources: surfaceIds };
|
|
445
|
+
}
|
|
446
|
+
} catch (e) {
|
|
447
|
+
console.warn('[Surface] B-Rep stitch failed:', e.message);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Mesh fallback: merge geometries
|
|
452
|
+
const group = new THREE.Group();
|
|
453
|
+
let merged = null;
|
|
454
|
+
|
|
455
|
+
for (const sid of surfaceIds) {
|
|
456
|
+
const surf = surfaces.get(sid);
|
|
457
|
+
if (surf?.mesh) {
|
|
458
|
+
if (!merged) {
|
|
459
|
+
merged = surf.mesh.clone();
|
|
460
|
+
} else {
|
|
461
|
+
group.add(surf.mesh.clone());
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
surfaces.set(id, { type: 'solid', mesh: merged || group });
|
|
467
|
+
|
|
468
|
+
if (viewport?.scene && merged) {
|
|
469
|
+
viewport.scene.add(merged);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return { id, type: 'solid', sources: surfaceIds };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Create ruled surface between two curves
|
|
477
|
+
* @param {Object} curve1 - First curve/edge
|
|
478
|
+
* @param {Object} curve2 - Second curve/edge
|
|
479
|
+
* @returns {Object} Surface object
|
|
480
|
+
*/
|
|
481
|
+
async function ruledSurface(curve1, curve2) {
|
|
482
|
+
const id = `surface_ruled_${surfaceCounter.count++}`;
|
|
483
|
+
|
|
484
|
+
// B-Rep attempt
|
|
485
|
+
if (surfaceManager.kernel) {
|
|
486
|
+
try {
|
|
487
|
+
const result = await surfaceManager.execBrep('ruledSurface', { curve1, curve2 });
|
|
488
|
+
if (result) {
|
|
489
|
+
surfaces.set(id, { type: 'ruled_surface', brep: result, mesh: brepToMesh(result) });
|
|
490
|
+
return { id, type: 'ruled_surface' };
|
|
491
|
+
}
|
|
492
|
+
} catch (e) {
|
|
493
|
+
console.warn('[Surface] B-Rep ruled failed:', e.message);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Mesh fallback
|
|
498
|
+
const mesh = createRuledSurfaceMesh(curve1, curve2);
|
|
499
|
+
surfaces.set(id, { type: 'ruled_surface', mesh, geometry: mesh.geometry });
|
|
500
|
+
|
|
501
|
+
if (viewport?.scene) {
|
|
502
|
+
mesh.material.side = THREE.DoubleSide;
|
|
503
|
+
viewport.scene.add(mesh);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return { id, type: 'ruled_surface' };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create 4-sided boundary surface
|
|
511
|
+
* @param {Array<THREE.Curve>} boundaries - 4 boundary curves
|
|
512
|
+
* @returns {Object} Surface object
|
|
513
|
+
*/
|
|
514
|
+
async function boundarySurface(boundaries) {
|
|
515
|
+
if (!Array.isArray(boundaries) || boundaries.length !== 4) {
|
|
516
|
+
throw new Error('Boundary surface requires exactly 4 boundary curves');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const id = `surface_boundary_${surfaceCounter.count++}`;
|
|
520
|
+
|
|
521
|
+
// B-Rep attempt
|
|
522
|
+
if (surfaceManager.kernel) {
|
|
523
|
+
try {
|
|
524
|
+
const result = await surfaceManager.execBrep('boundarySurface', { boundaries });
|
|
525
|
+
if (result) {
|
|
526
|
+
surfaces.set(id, { type: 'boundary_surface', brep: result, mesh: brepToMesh(result) });
|
|
527
|
+
return { id, type: 'boundary_surface' };
|
|
528
|
+
}
|
|
529
|
+
} catch (e) {
|
|
530
|
+
console.warn('[Surface] B-Rep boundary failed:', e.message);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Mesh fallback
|
|
535
|
+
const mesh = createBoundarySurfaceMesh(boundaries);
|
|
536
|
+
surfaces.set(id, { type: 'boundary_surface', mesh, geometry: mesh.geometry });
|
|
537
|
+
|
|
538
|
+
if (viewport?.scene) {
|
|
539
|
+
mesh.material.side = THREE.DoubleSide;
|
|
540
|
+
mesh.material.color.setHex(0xff8040);
|
|
541
|
+
viewport.scene.add(mesh);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return { id, type: 'boundary_surface' };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// --- Mesh Fallback Implementations ---
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Create mesh for extrude surface (open profile extruded)
|
|
551
|
+
*/
|
|
552
|
+
function createExtrudeSurfaceMesh(profile, direction, distance) {
|
|
553
|
+
const geom = new THREE.LatheGeometry(
|
|
554
|
+
profile.geometry?.attributes?.position?.array || [],
|
|
555
|
+
32
|
|
556
|
+
);
|
|
557
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0x80ff80, side: THREE.DoubleSide });
|
|
558
|
+
return new THREE.Mesh(geom, mat);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Create mesh for revolve surface
|
|
563
|
+
*/
|
|
564
|
+
function createRevolveSurfaceMesh(profile, axisOrigin, axisDir, angle) {
|
|
565
|
+
const geom = new THREE.LatheGeometry([], 32);
|
|
566
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0x8080ff, side: THREE.DoubleSide });
|
|
567
|
+
return new THREE.Mesh(geom, mat);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Create mesh for sweep surface
|
|
572
|
+
*/
|
|
573
|
+
function createSweepSurfaceMesh(profile, path, options) {
|
|
574
|
+
const geom = new THREE.BufferGeometry();
|
|
575
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0xff8080, side: THREE.DoubleSide });
|
|
576
|
+
return new THREE.Mesh(geom, mat);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Create mesh for loft surface
|
|
581
|
+
*/
|
|
582
|
+
function createLoftSurfaceMesh(profiles, options) {
|
|
583
|
+
const geom = new THREE.BufferGeometry();
|
|
584
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0xffff80, side: THREE.DoubleSide });
|
|
585
|
+
return new THREE.Mesh(geom, mat);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Create mesh for patch surface (Coons patch)
|
|
590
|
+
*/
|
|
591
|
+
function createPatchSurfaceMesh(boundaryLoop, options) {
|
|
592
|
+
const geom = new THREE.BufferGeometry();
|
|
593
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0x4080ff, side: THREE.DoubleSide });
|
|
594
|
+
return new THREE.Mesh(geom, mat);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Create mesh for ruled surface
|
|
599
|
+
*/
|
|
600
|
+
function createRuledSurfaceMesh(curve1, curve2) {
|
|
601
|
+
const geom = new THREE.BufferGeometry();
|
|
602
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0x80ff80, side: THREE.DoubleSide });
|
|
603
|
+
return new THREE.Mesh(geom, mat);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Create mesh for boundary surface
|
|
608
|
+
*/
|
|
609
|
+
function createBoundarySurfaceMesh(boundaries) {
|
|
610
|
+
const geom = new THREE.BufferGeometry();
|
|
611
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0xff8040, side: THREE.DoubleSide });
|
|
612
|
+
return new THREE.Mesh(geom, mat);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Convert B-Rep result to mesh (stub)
|
|
617
|
+
*/
|
|
618
|
+
function brepToMesh(brepResult) {
|
|
619
|
+
// In real implementation, convert B-Rep shell to THREE.Mesh
|
|
620
|
+
const geom = new THREE.BufferGeometry();
|
|
621
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0x80ff80 });
|
|
622
|
+
return new THREE.Mesh(geom, mat);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// --- Command Registration ---
|
|
626
|
+
|
|
627
|
+
function registerCommands() {
|
|
628
|
+
const api = window.cycleCAD?.api || {};
|
|
629
|
+
|
|
630
|
+
api.surface = {
|
|
631
|
+
extrude: extrudeSurface,
|
|
632
|
+
revolve: revolveSurface,
|
|
633
|
+
sweep: sweepSurface,
|
|
634
|
+
loft: loftSurface,
|
|
635
|
+
patch: patchSurface,
|
|
636
|
+
trim: trimSurface,
|
|
637
|
+
extend: extendSurface,
|
|
638
|
+
offset: offsetSurface,
|
|
639
|
+
thicken: thickenSurface,
|
|
640
|
+
stitch: stitchSurfaces,
|
|
641
|
+
ruled: ruledSurface,
|
|
642
|
+
boundary: boundarySurface,
|
|
643
|
+
list: () => Array.from(surfaces.entries()).map(([id, s]) => ({ id, type: s.type })),
|
|
644
|
+
get: (id) => surfaces.get(id),
|
|
645
|
+
delete: (id) => surfaces.delete(id),
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
window.cycleCAD = window.cycleCAD || {};
|
|
649
|
+
window.cycleCAD.api = api;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// --- Keyboard Shortcuts ---
|
|
653
|
+
|
|
654
|
+
function handleKeyboard(evt) {
|
|
655
|
+
if (evt.ctrlKey && evt.shiftKey && evt.key === 'E') {
|
|
656
|
+
console.log('[Surface] Active surfaces:', Array.from(surfaces.keys()));
|
|
657
|
+
evt.preventDefault();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// --- UI Panel ---
|
|
662
|
+
|
|
663
|
+
function getUI() {
|
|
664
|
+
ui = document.createElement('div');
|
|
665
|
+
ui.id = 'surface-panel';
|
|
666
|
+
ui.className = 'module-panel';
|
|
667
|
+
ui.innerHTML = `
|
|
668
|
+
<div class="panel-header">
|
|
669
|
+
<h3>Surface Modeling</h3>
|
|
670
|
+
<button class="close-btn" data-close-panel="#surface-panel">×</button>
|
|
671
|
+
</div>
|
|
672
|
+
<div class="panel-body">
|
|
673
|
+
<div class="button-group">
|
|
674
|
+
<button class="module-btn" data-cmd="surface.extrude" title="Extrude Surface">Extrude</button>
|
|
675
|
+
<button class="module-btn" data-cmd="surface.revolve" title="Revolve Surface">Revolve</button>
|
|
676
|
+
<button class="module-btn" data-cmd="surface.sweep" title="Sweep Surface">Sweep</button>
|
|
677
|
+
<button class="module-btn" data-cmd="surface.loft" title="Loft Surface">Loft</button>
|
|
678
|
+
</div>
|
|
679
|
+
<div class="button-group">
|
|
680
|
+
<button class="module-btn" data-cmd="surface.patch" title="Patch">Patch</button>
|
|
681
|
+
<button class="module-btn" data-cmd="surface.trim" title="Trim">Trim</button>
|
|
682
|
+
<button class="module-btn" data-cmd="surface.extend" title="Extend">Extend</button>
|
|
683
|
+
<button class="module-btn" data-cmd="surface.offset" title="Offset">Offset</button>
|
|
684
|
+
</div>
|
|
685
|
+
<div class="button-group">
|
|
686
|
+
<button class="module-btn" data-cmd="surface.thicken" title="Thicken">Thicken</button>
|
|
687
|
+
<button class="module-btn" data-cmd="surface.stitch" title="Stitch">Stitch</button>
|
|
688
|
+
<button class="module-btn" data-cmd="surface.ruled" title="Ruled">Ruled</button>
|
|
689
|
+
<button class="module-btn" data-cmd="surface.boundary" title="Boundary">Boundary</button>
|
|
690
|
+
</div>
|
|
691
|
+
<div id="surface-list" style="margin-top: 10px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
|
692
|
+
<strong>Active Surfaces:</strong>
|
|
693
|
+
<ul id="surface-items" style="list-style: none; padding: 0; margin: 5px 0;"></ul>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
`;
|
|
697
|
+
|
|
698
|
+
// Wire up buttons
|
|
699
|
+
ui.querySelectorAll('[data-cmd]').forEach(btn => {
|
|
700
|
+
btn.addEventListener('click', () => {
|
|
701
|
+
const [ns, cmd] = btn.dataset.cmd.split('.');
|
|
702
|
+
console.log(`[Surface] Command: ${cmd}`);
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
return ui;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
MODULE_NAME,
|
|
711
|
+
init,
|
|
712
|
+
getUI,
|
|
713
|
+
extrude: extrudeSurface,
|
|
714
|
+
revolve: revolveSurface,
|
|
715
|
+
sweep: sweepSurface,
|
|
716
|
+
loft: loftSurface,
|
|
717
|
+
patch: patchSurface,
|
|
718
|
+
trim: trimSurface,
|
|
719
|
+
extend: extendSurface,
|
|
720
|
+
offset: offsetSurface,
|
|
721
|
+
thicken: thickenSurface,
|
|
722
|
+
stitch: stitchSurfaces,
|
|
723
|
+
ruled: ruledSurface,
|
|
724
|
+
boundary: boundarySurface,
|
|
725
|
+
};
|
|
726
|
+
})();
|
|
727
|
+
|
|
728
|
+
export default SurfaceModule;
|