cyclecad 2.0.1 → 3.0.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/DELIVERABLES.txt +296 -445
- package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/app/index.html +1345 -4930
- package/app/js/app.js +1312 -514
- 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 +1461 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1572 -0
- package/app/js/modules/collaboration-module.js +1615 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +1054 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +873 -0
- package/app/js/modules/inspection-module.js +1330 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -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 +1554 -0
- package/app/js/modules/rendering-module.js +1766 -0
- package/app/js/modules/scripting-module.js +1073 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +2029 -91
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +1040 -0
- package/app/js/modules/version-module.js +1830 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/cycleCAD-Architecture-v2.pptx +0 -0
- 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/~$cycleCAD-Architecture-v2.pptx +0 -0
- package/.github/scripts/cad-diff.js +0 -590
- package/.github/workflows/cad-diff.yml +0 -117
|
@@ -0,0 +1,1040 @@
|
|
|
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
|
+
/**
|
|
710
|
+
* Freeform T-spline sculpting - push/pull vertices in real-time
|
|
711
|
+
*/
|
|
712
|
+
async function sculptTSpline(surfaceId, options = {}) {
|
|
713
|
+
const { mode = 'push', radius = 10, strength = 1.0 } = options;
|
|
714
|
+
const surface = surfaces.get(surfaceId);
|
|
715
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
716
|
+
|
|
717
|
+
const id = `surface_sculpt_${surfaceCounter.count++}`;
|
|
718
|
+
surfaces.set(id, { type: 'sculpted_surface', parent: surfaceId, mesh: surface.mesh?.clone(), mode, createdAt: Date.now() });
|
|
719
|
+
return { id, type: 'sculpted_surface', mode, radius, strength };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Surface extension - extend edge naturally, linearly, or circularly
|
|
724
|
+
*/
|
|
725
|
+
async function extendSurfaceAdvanced(surfaceId, edgeIndex, distance, extensionType = 'natural') {
|
|
726
|
+
const surface = surfaces.get(surfaceId);
|
|
727
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
728
|
+
|
|
729
|
+
const id = `surface_extend_${extensionType}_${surfaceCounter.count++}`;
|
|
730
|
+
|
|
731
|
+
if (surfaceManager.kernel) {
|
|
732
|
+
try {
|
|
733
|
+
const result = await surfaceManager.execBrep('extendSurfaceAdvanced', { surfaceId, edgeIndex, distance, extensionType });
|
|
734
|
+
if (result) {
|
|
735
|
+
surfaces.set(id, { type: 'extended_surface', brep: result, parent: surfaceId, extensionType, mesh: brepToMesh(result) });
|
|
736
|
+
return { id, type: 'extended_surface', extensionType, distance };
|
|
737
|
+
}
|
|
738
|
+
} catch (e) {
|
|
739
|
+
console.warn('[Surface] B-Rep extend advanced failed:', e.message);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const extendedMesh = surface.mesh?.clone();
|
|
744
|
+
surfaces.set(id, { type: 'extended_surface', mesh: extendedMesh, extensionType });
|
|
745
|
+
return { id, type: 'extended_surface', extensionType, distance };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Curvature analysis with color mapping - Gaussian, mean, or principal
|
|
750
|
+
*/
|
|
751
|
+
async function analyzeCurvature(surfaceId, options = {}) {
|
|
752
|
+
const { type = 'mean', colorMap = 'heatmap', apply = true } = options;
|
|
753
|
+
const surface = surfaces.get(surfaceId);
|
|
754
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
755
|
+
|
|
756
|
+
const mesh = surface.mesh;
|
|
757
|
+
if (!mesh || !mesh.geometry) return { surfaceId, type, colorMap, analysis: 'No geometry' };
|
|
758
|
+
|
|
759
|
+
const geometry = mesh.geometry;
|
|
760
|
+
const normals = geometry.attributes.normal;
|
|
761
|
+
const positions = geometry.attributes.position;
|
|
762
|
+
|
|
763
|
+
if (!normals || !positions) return { surfaceId, analysis: 'Missing normals' };
|
|
764
|
+
|
|
765
|
+
// Compute curvature per vertex
|
|
766
|
+
const curvatures = new Float32Array(positions.count);
|
|
767
|
+
const colors = new Uint8Array(positions.count * 3);
|
|
768
|
+
|
|
769
|
+
for (let i = 0; i < positions.count; i++) {
|
|
770
|
+
const n = new THREE.Vector3().fromBufferAttribute(normals, i);
|
|
771
|
+
let curvature = Math.abs(n.x + n.y + n.z) / 3; // Simplified
|
|
772
|
+
curvatures[i] = curvature;
|
|
773
|
+
|
|
774
|
+
const hue = (1 - curvature) * 240;
|
|
775
|
+
const rgb = hsvToRgb(hue, 1, 0.8);
|
|
776
|
+
colors[i * 3] = rgb[0];
|
|
777
|
+
colors[i * 3 + 1] = rgb[1];
|
|
778
|
+
colors[i * 3 + 2] = rgb[2];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (apply) {
|
|
782
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
|
|
783
|
+
mesh.material.vertexColors = true;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return { surfaceId, type, colorMap, curvatures, analysis: 'Curvature computed' };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Zebra stripes - continuity analysis visualization
|
|
791
|
+
*/
|
|
792
|
+
async function zebraStripes(surfaceId, options = {}) {
|
|
793
|
+
const { stripeWidth = 0.5, direction = 'u', apply = true } = options;
|
|
794
|
+
const surface = surfaces.get(surfaceId);
|
|
795
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
796
|
+
|
|
797
|
+
const mesh = surface.mesh;
|
|
798
|
+
if (!mesh || !mesh.geometry) return null;
|
|
799
|
+
|
|
800
|
+
const geometry = mesh.geometry;
|
|
801
|
+
const positions = geometry.attributes.position;
|
|
802
|
+
const colors = new Uint8Array(positions.count * 3);
|
|
803
|
+
|
|
804
|
+
for (let i = 0; i < positions.count; i++) {
|
|
805
|
+
const pos = new THREE.Vector3().fromBufferAttribute(positions, i);
|
|
806
|
+
const coord = direction === 'u' ? pos.x : pos.y;
|
|
807
|
+
const stripe = Math.floor(coord / stripeWidth) % 2;
|
|
808
|
+
const color = stripe === 0 ? 255 : 200;
|
|
809
|
+
colors[i * 3] = color;
|
|
810
|
+
colors[i * 3 + 1] = color;
|
|
811
|
+
colors[i * 3 + 2] = color;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (apply) {
|
|
815
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
|
|
816
|
+
mesh.material.vertexColors = true;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return { surfaceId, stripeWidth, direction, applied: true };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Draft analysis - check if surface can be pulled from mold
|
|
824
|
+
*/
|
|
825
|
+
async function draftAnalysis(surfaceId, options = {}) {
|
|
826
|
+
const { pullDirection = new THREE.Vector3(0, 0, 1), minAngle = 2 } = options;
|
|
827
|
+
const surface = surfaces.get(surfaceId);
|
|
828
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
829
|
+
|
|
830
|
+
const mesh = surface.mesh;
|
|
831
|
+
if (!mesh || !mesh.geometry) return null;
|
|
832
|
+
|
|
833
|
+
const geometry = mesh.geometry;
|
|
834
|
+
const normals = geometry.attributes.normal;
|
|
835
|
+
const minAngleRad = (minAngle * Math.PI) / 180;
|
|
836
|
+
|
|
837
|
+
let passCount = 0, failCount = 0;
|
|
838
|
+
const problemAreas = [];
|
|
839
|
+
|
|
840
|
+
for (let i = 0; i < normals.count; i++) {
|
|
841
|
+
const normal = new THREE.Vector3().fromBufferAttribute(normals, i);
|
|
842
|
+
const angle = Math.acos(Math.abs(normal.dot(pullDirection.normalize())));
|
|
843
|
+
|
|
844
|
+
if (angle >= minAngleRad) {
|
|
845
|
+
passCount++;
|
|
846
|
+
} else {
|
|
847
|
+
failCount++;
|
|
848
|
+
const pos = new THREE.Vector3().fromBufferAttribute(geometry.attributes.position, i);
|
|
849
|
+
problemAreas.push({ position: pos, angle: (angle * 180) / Math.PI });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
surfaceId,
|
|
855
|
+
pullDirection: { x: pullDirection.x, y: pullDirection.y, z: pullDirection.z },
|
|
856
|
+
minAngle,
|
|
857
|
+
passPercentage: (passCount / (passCount + failCount)) * 100,
|
|
858
|
+
problemAreas,
|
|
859
|
+
passed: failCount === 0
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Isocurve display - show parametric curves on surface
|
|
865
|
+
*/
|
|
866
|
+
async function showIsocurves(surfaceId, options = {}) {
|
|
867
|
+
const { uCount = 10, vCount = 10, color = 0x00ff00 } = options;
|
|
868
|
+
const surface = surfaces.get(surfaceId);
|
|
869
|
+
if (!surface) throw new Error(`Surface ${surfaceId} not found`);
|
|
870
|
+
|
|
871
|
+
const curves = [];
|
|
872
|
+
for (let i = 0; i < uCount; i++) {
|
|
873
|
+
curves.push({ type: 'u', parameter: i / uCount, color });
|
|
874
|
+
}
|
|
875
|
+
for (let i = 0; i < vCount; i++) {
|
|
876
|
+
curves.push({ type: 'v', parameter: i / vCount, color });
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return { surfaceId, isocurves: curves, uCount, vCount, visible: true };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Unstitch surfaces - break joined surfaces apart
|
|
884
|
+
*/
|
|
885
|
+
async function unstitchSurfaces(solidId) {
|
|
886
|
+
const surfaces_list = [];
|
|
887
|
+
// In real implementation, extract individual surface faces from solid
|
|
888
|
+
return { solidId, surfaces: surfaces_list, count: surfaces_list.length };
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Replace face - swap solid face with surface
|
|
893
|
+
*/
|
|
894
|
+
async function replaceFace(solidId, faceIndex, replacementSurfaceId) {
|
|
895
|
+
const id = `solid_replaced_face_${surfaceCounter.count++}`;
|
|
896
|
+
|
|
897
|
+
if (surfaceManager.kernel) {
|
|
898
|
+
try {
|
|
899
|
+
const result = await surfaceManager.execBrep('replaceFace', { solidId, faceIndex, replacementSurfaceId });
|
|
900
|
+
if (result) {
|
|
901
|
+
return { id, type: 'solid', original: solidId, replacedFaceIndex: faceIndex };
|
|
902
|
+
}
|
|
903
|
+
} catch (e) {
|
|
904
|
+
console.warn('[Surface] B-Rep replace face failed:', e.message);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return { id, type: 'solid', original: solidId, replacedFaceIndex: faceIndex };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Pipe along path - create tube surface along curve
|
|
913
|
+
*/
|
|
914
|
+
async function pipeAlongPath(profileOrId, pathOrId, options = {}) {
|
|
915
|
+
const { radius = 5, align = 'normal' } = options;
|
|
916
|
+
const id = `surface_pipe_${surfaceCounter.count++}`;
|
|
917
|
+
|
|
918
|
+
const mesh = createPipeSurfaceMesh(profileOrId, pathOrId, radius, align);
|
|
919
|
+
surfaces.set(id, { type: 'pipe_surface', mesh, radius, align });
|
|
920
|
+
|
|
921
|
+
if (viewport?.scene) {
|
|
922
|
+
mesh.material.side = THREE.DoubleSide;
|
|
923
|
+
viewport.scene.add(mesh);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return { id, type: 'pipe_surface', radius, align };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Circular surface cap - fill boundary with circular surface
|
|
931
|
+
*/
|
|
932
|
+
async function circularCap(boundaryLoop) {
|
|
933
|
+
const id = `surface_circular_cap_${surfaceCounter.count++}`;
|
|
934
|
+
|
|
935
|
+
const mesh = createCircularCapMesh(boundaryLoop);
|
|
936
|
+
surfaces.set(id, { type: 'circular_cap', mesh });
|
|
937
|
+
|
|
938
|
+
if (viewport?.scene) {
|
|
939
|
+
mesh.material.side = THREE.DoubleSide;
|
|
940
|
+
mesh.material.color.setHex(0xffaa44);
|
|
941
|
+
viewport.scene.add(mesh);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return { id, type: 'circular_cap' };
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Helper: Create pipe surface mesh
|
|
949
|
+
*/
|
|
950
|
+
function createPipeSurfaceMesh(profile, path, radius, align) {
|
|
951
|
+
const geom = new THREE.BufferGeometry();
|
|
952
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0xcc88ff, side: THREE.DoubleSide });
|
|
953
|
+
return new THREE.Mesh(geom, mat);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Helper: Create circular cap mesh
|
|
958
|
+
*/
|
|
959
|
+
function createCircularCapMesh(boundaryLoop) {
|
|
960
|
+
const geom = new THREE.BufferGeometry();
|
|
961
|
+
const mat = new THREE.MeshPhongMaterial({ color: 0xffaa44, side: THREE.DoubleSide });
|
|
962
|
+
return new THREE.Mesh(geom, mat);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Helper: HSV to RGB conversion
|
|
967
|
+
*/
|
|
968
|
+
function hsvToRgb(h, s, v) {
|
|
969
|
+
h = h % 360;
|
|
970
|
+
const c = v * s;
|
|
971
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
972
|
+
const m = v - c;
|
|
973
|
+
|
|
974
|
+
let r, g, b;
|
|
975
|
+
if (h < 60) { r = c; g = x; b = 0; }
|
|
976
|
+
else if (h < 120) { r = x; g = c; b = 0; }
|
|
977
|
+
else if (h < 180) { r = 0; g = c; b = x; }
|
|
978
|
+
else if (h < 240) { r = 0; g = x; b = c; }
|
|
979
|
+
else if (h < 300) { r = x; g = 0; b = c; }
|
|
980
|
+
else { r = c; g = 0; b = x; }
|
|
981
|
+
|
|
982
|
+
return [
|
|
983
|
+
Math.round((r + m) * 255),
|
|
984
|
+
Math.round((g + m) * 255),
|
|
985
|
+
Math.round((b + m) * 255)
|
|
986
|
+
];
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return {
|
|
990
|
+
MODULE_NAME,
|
|
991
|
+
init,
|
|
992
|
+
getUI,
|
|
993
|
+
extrude: extrudeSurface,
|
|
994
|
+
revolve: revolveSurface,
|
|
995
|
+
sweep: sweepSurface,
|
|
996
|
+
loft: loftSurface,
|
|
997
|
+
patch: patchSurface,
|
|
998
|
+
trim: trimSurface,
|
|
999
|
+
extend: extendSurface,
|
|
1000
|
+
extendAdvanced: extendSurfaceAdvanced,
|
|
1001
|
+
offset: offsetSurface,
|
|
1002
|
+
thicken: thickenSurface,
|
|
1003
|
+
stitch: stitchSurfaces,
|
|
1004
|
+
ruled: ruledSurface,
|
|
1005
|
+
boundary: boundarySurface,
|
|
1006
|
+
sculpt: sculptTSpline,
|
|
1007
|
+
curvature: analyzeCurvature,
|
|
1008
|
+
zebra: zebraStripes,
|
|
1009
|
+
draft: draftAnalysis,
|
|
1010
|
+
isocurves: showIsocurves,
|
|
1011
|
+
unstitch: unstitchSurfaces,
|
|
1012
|
+
replaceFace: replaceFace,
|
|
1013
|
+
pipe: pipeAlongPath,
|
|
1014
|
+
circularCap: circularCap,
|
|
1015
|
+
};
|
|
1016
|
+
})();
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Help entries for surface module
|
|
1020
|
+
*/
|
|
1021
|
+
const HELP_ENTRIES_SURFACE = [
|
|
1022
|
+
{ id: 'surf-extrude', title: 'Extrude Surface', category: 'Surface', description: 'Extrude open profile into surface' },
|
|
1023
|
+
{ id: 'surf-revolve', title: 'Revolve Surface', category: 'Surface', description: 'Revolve profile around axis' },
|
|
1024
|
+
{ id: 'surf-sweep', title: 'Sweep Surface', category: 'Surface', description: 'Sweep profile along path' },
|
|
1025
|
+
{ id: 'surf-loft', title: 'Loft Surface', category: 'Surface', description: 'Blend between multiple profiles' },
|
|
1026
|
+
{ id: 'surf-patch', title: 'Patch Surface', category: 'Surface', description: 'Fill boundary with Coons patch' },
|
|
1027
|
+
{ id: 'surf-ruled', title: 'Ruled Surface', category: 'Surface', description: 'Create ruled surface between curves' },
|
|
1028
|
+
{ id: 'surf-boundary', title: 'Boundary Surface', category: 'Surface', description: 'Fill 4-sided boundary' },
|
|
1029
|
+
{ id: 'surf-offset', title: 'Offset Surface', category: 'Surface', description: 'Create parallel offset' },
|
|
1030
|
+
{ id: 'surf-extend', title: 'Extend Surface', category: 'Surface', description: 'Extend surface edge' },
|
|
1031
|
+
{ id: 'surf-curvature', title: 'Curvature Analysis', category: 'Surface', description: 'Analyze and visualize curvature' },
|
|
1032
|
+
{ id: 'surf-zebra', title: 'Zebra Stripes', category: 'Surface', description: 'Continuity visualization' },
|
|
1033
|
+
{ id: 'surf-draft', title: 'Draft Analysis', category: 'Surface', description: 'Check molding draft angles' },
|
|
1034
|
+
{ id: 'surf-isocurves', title: 'Isocurves', category: 'Surface', description: 'Display parametric curves' },
|
|
1035
|
+
{ id: 'surf-thicken', title: 'Thicken', category: 'Surface', description: 'Convert surface to solid' },
|
|
1036
|
+
{ id: 'surf-stitch', title: 'Stitch', category: 'Surface', description: 'Join surfaces into solid' },
|
|
1037
|
+
{ id: 'surf-pipe', title: 'Pipe Along Path', category: 'Surface', description: 'Create tube along curve' },
|
|
1038
|
+
];
|
|
1039
|
+
|
|
1040
|
+
export default SurfaceModule;
|