cyclecad 1.1.2 → 1.3.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/.github/scripts/cad-diff.js +590 -0
- package/.github/workflows/cad-diff.yml +117 -0
- package/KILLER-README.md +377 -0
- package/app/index.html +88 -30
- package/app/js/ai-copilot.js +53 -18
- package/app/js/brep-engine.js +661 -0
- package/app/js/multiplayer.js +465 -0
- package/app/js/parts-library.js +778 -0
- package/app/js/step-viewer.js +584 -0
- package/app/js/text-to-brep.js +585 -0
- package/docs/ARCHITECTURE.html +1429 -0
- package/package.json +1 -1
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brep-engine.js — Real B-rep kernel for cycleCAD using OpenCascade.js
|
|
3
|
+
*
|
|
4
|
+
* Provides true solid modeling operations:
|
|
5
|
+
* - Primitive creation (box, cylinder, sphere, cone)
|
|
6
|
+
* - Boolean operations (cut, fuse, intersect)
|
|
7
|
+
* - Fillet and chamfer on real edges
|
|
8
|
+
* - Shape → Three.js BufferGeometry conversion
|
|
9
|
+
*
|
|
10
|
+
* Uses opencascade.js v2.0.0-beta (modular WASM build)
|
|
11
|
+
*
|
|
12
|
+
* MIT License — cycleCAD (c) 2026
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// STATE
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
let oc = null; // OpenCascade.js instance
|
|
20
|
+
let _loading = false; // Prevent double-init
|
|
21
|
+
let _ready = false; // True when WASM is loaded
|
|
22
|
+
let _currentShape = null; // Active TopoDS_Shape (the "document")
|
|
23
|
+
const _shapeStack = []; // Undo stack of shapes
|
|
24
|
+
|
|
25
|
+
// CDN for opencascade.js 2.0.0-beta
|
|
26
|
+
const OCC_CDN = 'https://unpkg.com/opencascade.js@2.0.0-beta.533428a/dist/';
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// INITIALIZATION
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load and initialize the OpenCascade.js WASM kernel.
|
|
34
|
+
* Returns a promise that resolves when ready.
|
|
35
|
+
* Subsequent calls return immediately if already loaded.
|
|
36
|
+
*/
|
|
37
|
+
export async function initBRep() {
|
|
38
|
+
if (_ready && oc) return oc;
|
|
39
|
+
if (_loading) {
|
|
40
|
+
// Wait for in-progress load
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const check = setInterval(() => {
|
|
43
|
+
if (_ready && oc) { clearInterval(check); resolve(oc); }
|
|
44
|
+
}, 200);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_loading = true;
|
|
49
|
+
console.log('[BRep] Loading OpenCascade.js WASM kernel...');
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Dynamic import from CDN
|
|
53
|
+
const module = await import(/* webpackIgnore: true */ OCC_CDN + 'opencascade.full.js');
|
|
54
|
+
const factory = module.default || module;
|
|
55
|
+
|
|
56
|
+
// Initialize WASM — factory returns a promise
|
|
57
|
+
oc = await factory({
|
|
58
|
+
locateFile: (file) => OCC_CDN + file,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
_ready = true;
|
|
62
|
+
_loading = false;
|
|
63
|
+
console.log('[BRep] OpenCascade.js ready ✓');
|
|
64
|
+
return oc;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('[BRep] Failed to load OpenCascade.js:', err);
|
|
67
|
+
_loading = false;
|
|
68
|
+
|
|
69
|
+
// Fallback: try loading via script tag
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
console.log('[BRep] Trying script tag fallback...');
|
|
72
|
+
const savedModule = window.Module;
|
|
73
|
+
const script = document.createElement('script');
|
|
74
|
+
script.src = OCC_CDN + 'opencascade.full.js';
|
|
75
|
+
script.onload = async () => {
|
|
76
|
+
try {
|
|
77
|
+
const occFactory = window.Module;
|
|
78
|
+
window.Module = savedModule; // restore
|
|
79
|
+
oc = await new occFactory({
|
|
80
|
+
locateFile: (file) => OCC_CDN + file,
|
|
81
|
+
});
|
|
82
|
+
_ready = true;
|
|
83
|
+
_loading = false;
|
|
84
|
+
console.log('[BRep] OpenCascade.js ready (script fallback) ✓');
|
|
85
|
+
resolve(oc);
|
|
86
|
+
} catch (e2) {
|
|
87
|
+
_loading = false;
|
|
88
|
+
reject(e2);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
script.onerror = () => { _loading = false; reject(new Error('Script load failed')); };
|
|
92
|
+
document.head.appendChild(script);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Check if the B-rep kernel is ready */
|
|
98
|
+
export function isReady() { return _ready && oc !== null; }
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// HELPER: gp_Pnt constructor
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
function pnt(x, y, z) {
|
|
105
|
+
return new oc.gp_Pnt_3(x, y, z);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ax2(origin, dir) {
|
|
109
|
+
return new oc.gp_Ax2_3(
|
|
110
|
+
pnt(origin[0], origin[1], origin[2]),
|
|
111
|
+
new oc.gp_Dir_4(dir[0], dir[1], dir[2])
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function progress() {
|
|
116
|
+
return new oc.Message_ProgressRange_1();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// PRIMITIVES
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a B-rep box centered at origin
|
|
125
|
+
* @param {number} width - X dimension in mm
|
|
126
|
+
* @param {number} height - Y dimension in mm
|
|
127
|
+
* @param {number} depth - Z dimension in mm
|
|
128
|
+
* @returns {TopoDS_Shape}
|
|
129
|
+
*/
|
|
130
|
+
export function makeBox(width, height, depth) {
|
|
131
|
+
const w = width, h = height, d = depth;
|
|
132
|
+
// Create box at (-w/2, -h/2, -d/2) so it's centered
|
|
133
|
+
const builder = new oc.BRepPrimAPI_MakeBox_3(
|
|
134
|
+
pnt(-w / 2, -h / 2, -d / 2), w, h, d
|
|
135
|
+
);
|
|
136
|
+
const shape = builder.Shape();
|
|
137
|
+
builder.delete();
|
|
138
|
+
return shape;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a B-rep cylinder centered at origin, axis along Y
|
|
143
|
+
* @param {number} radius - Radius in mm
|
|
144
|
+
* @param {number} height - Height in mm
|
|
145
|
+
* @returns {TopoDS_Shape}
|
|
146
|
+
*/
|
|
147
|
+
export function makeCylinder(radius, height) {
|
|
148
|
+
const axis = ax2([0, -height / 2, 0], [0, 1, 0]);
|
|
149
|
+
const builder = new oc.BRepPrimAPI_MakeCylinder_2(axis, radius, height);
|
|
150
|
+
const shape = builder.Shape();
|
|
151
|
+
builder.delete();
|
|
152
|
+
return shape;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a B-rep sphere centered at origin
|
|
157
|
+
* @param {number} radius - Radius in mm
|
|
158
|
+
* @returns {TopoDS_Shape}
|
|
159
|
+
*/
|
|
160
|
+
export function makeSphere(radius) {
|
|
161
|
+
const builder = new oc.BRepPrimAPI_MakeSphere_1(radius);
|
|
162
|
+
const shape = builder.Shape();
|
|
163
|
+
builder.delete();
|
|
164
|
+
return shape;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create a B-rep cone centered at origin, axis along Y
|
|
169
|
+
* @param {number} bottomRadius
|
|
170
|
+
* @param {number} topRadius
|
|
171
|
+
* @param {number} height
|
|
172
|
+
* @returns {TopoDS_Shape}
|
|
173
|
+
*/
|
|
174
|
+
export function makeCone(bottomRadius, topRadius, height) {
|
|
175
|
+
const axis = ax2([0, -height / 2, 0], [0, 1, 0]);
|
|
176
|
+
const builder = new oc.BRepPrimAPI_MakeCone_2(axis, bottomRadius, topRadius, height);
|
|
177
|
+
const shape = builder.Shape();
|
|
178
|
+
builder.delete();
|
|
179
|
+
return shape;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// BOOLEAN OPERATIONS
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Boolean cut: result = base - tool
|
|
188
|
+
* @param {TopoDS_Shape} base - Shape to cut from
|
|
189
|
+
* @param {TopoDS_Shape} tool - Shape to subtract
|
|
190
|
+
* @returns {TopoDS_Shape}
|
|
191
|
+
*/
|
|
192
|
+
export function booleanCut(base, tool) {
|
|
193
|
+
const cutter = new oc.BRepAlgoAPI_Cut_3(base, tool, progress());
|
|
194
|
+
cutter.Build(progress());
|
|
195
|
+
if (!cutter.IsDone()) {
|
|
196
|
+
console.warn('[BRep] Boolean cut failed');
|
|
197
|
+
cutter.delete();
|
|
198
|
+
return base;
|
|
199
|
+
}
|
|
200
|
+
const result = cutter.Shape();
|
|
201
|
+
cutter.delete();
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Boolean fuse (union): result = base + tool
|
|
207
|
+
* @param {TopoDS_Shape} base
|
|
208
|
+
* @param {TopoDS_Shape} tool
|
|
209
|
+
* @returns {TopoDS_Shape}
|
|
210
|
+
*/
|
|
211
|
+
export function booleanFuse(base, tool) {
|
|
212
|
+
const fuser = new oc.BRepAlgoAPI_Fuse_3(base, tool, progress());
|
|
213
|
+
fuser.Build(progress());
|
|
214
|
+
if (!fuser.IsDone()) {
|
|
215
|
+
console.warn('[BRep] Boolean fuse failed');
|
|
216
|
+
fuser.delete();
|
|
217
|
+
return base;
|
|
218
|
+
}
|
|
219
|
+
const result = fuser.Shape();
|
|
220
|
+
fuser.delete();
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Boolean intersect: result = base ∩ tool
|
|
226
|
+
* @param {TopoDS_Shape} base
|
|
227
|
+
* @param {TopoDS_Shape} tool
|
|
228
|
+
* @returns {TopoDS_Shape}
|
|
229
|
+
*/
|
|
230
|
+
export function booleanIntersect(base, tool) {
|
|
231
|
+
const common = new oc.BRepAlgoAPI_Common_3(base, tool, progress());
|
|
232
|
+
common.Build(progress());
|
|
233
|
+
if (!common.IsDone()) {
|
|
234
|
+
console.warn('[BRep] Boolean intersect failed');
|
|
235
|
+
common.delete();
|
|
236
|
+
return base;
|
|
237
|
+
}
|
|
238
|
+
const result = common.Shape();
|
|
239
|
+
common.delete();
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// FILLET & CHAMFER
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Apply fillet (round) to ALL edges of a shape
|
|
249
|
+
* @param {TopoDS_Shape} shape - Input shape
|
|
250
|
+
* @param {number} radius - Fillet radius in mm
|
|
251
|
+
* @returns {TopoDS_Shape}
|
|
252
|
+
*/
|
|
253
|
+
export function filletAll(shape, radius) {
|
|
254
|
+
const fillet = new oc.BRepFilletAPI_MakeFillet(shape, oc.ChFi3d_Rational);
|
|
255
|
+
const explorer = new oc.TopExp_Explorer_2(shape, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE);
|
|
256
|
+
|
|
257
|
+
let edgeCount = 0;
|
|
258
|
+
while (explorer.More()) {
|
|
259
|
+
const edge = oc.TopoDS.Edge_1(explorer.Current());
|
|
260
|
+
fillet.Add_2(radius, edge);
|
|
261
|
+
edgeCount++;
|
|
262
|
+
explorer.Next();
|
|
263
|
+
}
|
|
264
|
+
explorer.delete();
|
|
265
|
+
|
|
266
|
+
if (edgeCount === 0) {
|
|
267
|
+
console.warn('[BRep] No edges found for fillet');
|
|
268
|
+
fillet.delete();
|
|
269
|
+
return shape;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const result = fillet.Shape();
|
|
274
|
+
console.log(`[BRep] Fillet applied to ${edgeCount} edges (r=${radius}mm)`);
|
|
275
|
+
fillet.delete();
|
|
276
|
+
return result;
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.warn('[BRep] Fillet failed (radius may be too large):', e.message);
|
|
279
|
+
fillet.delete();
|
|
280
|
+
return shape;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Apply chamfer to ALL edges of a shape
|
|
286
|
+
* @param {TopoDS_Shape} shape - Input shape
|
|
287
|
+
* @param {number} distance - Chamfer distance in mm
|
|
288
|
+
* @returns {TopoDS_Shape}
|
|
289
|
+
*/
|
|
290
|
+
export function chamferAll(shape, distance) {
|
|
291
|
+
const chamfer = new oc.BRepFilletAPI_MakeChamfer(shape);
|
|
292
|
+
const explorer = new oc.TopExp_Explorer_2(shape, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE);
|
|
293
|
+
|
|
294
|
+
let edgeCount = 0;
|
|
295
|
+
while (explorer.More()) {
|
|
296
|
+
const edge = oc.TopoDS.Edge_1(explorer.Current());
|
|
297
|
+
chamfer.Add_2(distance, edge);
|
|
298
|
+
edgeCount++;
|
|
299
|
+
explorer.Next();
|
|
300
|
+
}
|
|
301
|
+
explorer.delete();
|
|
302
|
+
|
|
303
|
+
if (edgeCount === 0) {
|
|
304
|
+
chamfer.delete();
|
|
305
|
+
return shape;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const result = chamfer.Shape();
|
|
310
|
+
console.log(`[BRep] Chamfer applied to ${edgeCount} edges (d=${distance}mm)`);
|
|
311
|
+
chamfer.delete();
|
|
312
|
+
return result;
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.warn('[BRep] Chamfer failed:', e.message);
|
|
315
|
+
chamfer.delete();
|
|
316
|
+
return shape;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ============================================================================
|
|
321
|
+
// TRANSFORM
|
|
322
|
+
// ============================================================================
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Translate a shape by (dx, dy, dz)
|
|
326
|
+
* @param {TopoDS_Shape} shape
|
|
327
|
+
* @param {number} dx
|
|
328
|
+
* @param {number} dy
|
|
329
|
+
* @param {number} dz
|
|
330
|
+
* @returns {TopoDS_Shape}
|
|
331
|
+
*/
|
|
332
|
+
export function translate(shape, dx, dy, dz) {
|
|
333
|
+
const trsf = new oc.gp_Trsf_1();
|
|
334
|
+
trsf.SetTranslation_1(new oc.gp_Vec_4(dx, dy, dz));
|
|
335
|
+
const transform = new oc.BRepBuilderAPI_Transform_2(shape, trsf, true);
|
|
336
|
+
const result = transform.Shape();
|
|
337
|
+
transform.delete();
|
|
338
|
+
trsf.delete();
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// SHAPE → THREE.JS CONVERSION
|
|
344
|
+
// ============================================================================
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Convert a TopoDS_Shape to Three.js BufferGeometry
|
|
348
|
+
* Tessellates the B-rep surface and extracts vertex/normal/index arrays
|
|
349
|
+
*
|
|
350
|
+
* @param {TopoDS_Shape} shape - OCC shape to convert
|
|
351
|
+
* @param {number} deflection - Mesh quality (smaller = finer). Default 0.5mm
|
|
352
|
+
* @returns {THREE.BufferGeometry}
|
|
353
|
+
*/
|
|
354
|
+
export function shapeToGeometry(shape, deflection = 0.5) {
|
|
355
|
+
// Tessellate the shape
|
|
356
|
+
new oc.BRepMesh_IncrementalMesh_2(shape, deflection, false, deflection * 5, false);
|
|
357
|
+
|
|
358
|
+
const positions = [];
|
|
359
|
+
const normals = [];
|
|
360
|
+
const indices = [];
|
|
361
|
+
let vertexOffset = 0;
|
|
362
|
+
|
|
363
|
+
// Iterate over all faces
|
|
364
|
+
const faceExplorer = new oc.TopExp_Explorer_2(
|
|
365
|
+
shape,
|
|
366
|
+
oc.TopAbs_ShapeEnum.TopAbs_FACE,
|
|
367
|
+
oc.TopAbs_ShapeEnum.TopAbs_SHAPE
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
while (faceExplorer.More()) {
|
|
371
|
+
const face = oc.TopoDS.Face_1(faceExplorer.Current());
|
|
372
|
+
const location = new oc.TopLoc_Location_1();
|
|
373
|
+
const triangulation = oc.BRep_Tool.Triangulation(face, location);
|
|
374
|
+
|
|
375
|
+
if (!triangulation.IsNull()) {
|
|
376
|
+
const tri = triangulation.get();
|
|
377
|
+
const nbNodes = tri.NbNodes();
|
|
378
|
+
const nbTriangles = tri.NbTriangles();
|
|
379
|
+
|
|
380
|
+
// Get the transformation from the face location
|
|
381
|
+
const trsf = location.Transformation();
|
|
382
|
+
|
|
383
|
+
// Extract vertices
|
|
384
|
+
for (let i = 1; i <= nbNodes; i++) {
|
|
385
|
+
const node = tri.Node(i);
|
|
386
|
+
const transformed = node.Transformed(trsf);
|
|
387
|
+
positions.push(transformed.X(), transformed.Y(), transformed.Z());
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Compute face normal orientation
|
|
391
|
+
const orientation = face.Orientation_1();
|
|
392
|
+
const reversed = (orientation === oc.TopAbs_Orientation.TopAbs_REVERSED);
|
|
393
|
+
|
|
394
|
+
// Extract triangles
|
|
395
|
+
for (let i = 1; i <= nbTriangles; i++) {
|
|
396
|
+
const triangle = tri.Triangle(i);
|
|
397
|
+
let n1 = triangle.Value(1);
|
|
398
|
+
let n2 = triangle.Value(2);
|
|
399
|
+
let n3 = triangle.Value(3);
|
|
400
|
+
|
|
401
|
+
// Flip winding if face is reversed
|
|
402
|
+
if (reversed) { [n2, n3] = [n3, n2]; }
|
|
403
|
+
|
|
404
|
+
indices.push(
|
|
405
|
+
vertexOffset + n1 - 1,
|
|
406
|
+
vertexOffset + n2 - 1,
|
|
407
|
+
vertexOffset + n3 - 1
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
vertexOffset += nbNodes;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
location.delete();
|
|
415
|
+
faceExplorer.Next();
|
|
416
|
+
}
|
|
417
|
+
faceExplorer.delete();
|
|
418
|
+
|
|
419
|
+
// Build Three.js BufferGeometry
|
|
420
|
+
const geometry = new THREE.BufferGeometry();
|
|
421
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
422
|
+
geometry.setIndex(indices);
|
|
423
|
+
geometry.computeVertexNormals();
|
|
424
|
+
|
|
425
|
+
return geometry;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Convert a TopoDS_Shape to a full Three.js Mesh with material
|
|
430
|
+
*
|
|
431
|
+
* @param {TopoDS_Shape} shape - OCC shape
|
|
432
|
+
* @param {object} options - { color, metalness, roughness, opacity, wireframe }
|
|
433
|
+
* @param {number} deflection - Mesh quality
|
|
434
|
+
* @returns {{ mesh: THREE.Mesh, wireframe: THREE.LineSegments, shape: TopoDS_Shape }}
|
|
435
|
+
*/
|
|
436
|
+
export function shapeToMesh(shape, options = {}, deflection = 0.5) {
|
|
437
|
+
const {
|
|
438
|
+
color = 0x4488cc,
|
|
439
|
+
metalness = 0.3,
|
|
440
|
+
roughness = 0.6,
|
|
441
|
+
opacity = 1.0,
|
|
442
|
+
wireframe = false,
|
|
443
|
+
} = options;
|
|
444
|
+
|
|
445
|
+
const geometry = shapeToGeometry(shape, deflection);
|
|
446
|
+
|
|
447
|
+
const material = new THREE.MeshStandardMaterial({
|
|
448
|
+
color,
|
|
449
|
+
metalness,
|
|
450
|
+
roughness,
|
|
451
|
+
transparent: opacity < 1,
|
|
452
|
+
opacity,
|
|
453
|
+
side: THREE.DoubleSide,
|
|
454
|
+
wireframe,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
458
|
+
mesh.castShadow = true;
|
|
459
|
+
mesh.receiveShadow = true;
|
|
460
|
+
|
|
461
|
+
// Wireframe edges overlay
|
|
462
|
+
const edges = new THREE.EdgesGeometry(geometry, 15);
|
|
463
|
+
const lineMat = new THREE.LineBasicMaterial({ color: 0x000000, opacity: 0.3, transparent: true });
|
|
464
|
+
const wireframeLines = new THREE.LineSegments(edges, lineMat);
|
|
465
|
+
|
|
466
|
+
return { mesh, wireframe: wireframeLines, shape };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// HIGH-LEVEL COPILOT API
|
|
471
|
+
// ============================================================================
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Execute a full copilot command sequence using real B-rep operations.
|
|
475
|
+
* Manages the current shape state and converts to Three.js.
|
|
476
|
+
*
|
|
477
|
+
* @param {Array} commands - Array of { type, params } objects
|
|
478
|
+
* @returns {Promise<{ mesh, wireframe, shape, description }>}
|
|
479
|
+
*/
|
|
480
|
+
export async function executeCommands(commands) {
|
|
481
|
+
if (!_ready) {
|
|
482
|
+
await initBRep();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const SCALE = 0.1; // mm to scene units
|
|
486
|
+
let description = '';
|
|
487
|
+
|
|
488
|
+
for (const cmd of commands) {
|
|
489
|
+
const { type, params = {} } = cmd;
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
switch (type) {
|
|
493
|
+
case 'box': {
|
|
494
|
+
const w = params.width || 100;
|
|
495
|
+
const h = params.height || 100;
|
|
496
|
+
const d = params.depth || 100;
|
|
497
|
+
_pushUndo();
|
|
498
|
+
_currentShape = makeBox(w, h, d);
|
|
499
|
+
description += `Box ${w}×${h}×${d}mm`;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
case 'cylinder': {
|
|
504
|
+
const r = params.radius || 25;
|
|
505
|
+
const h = params.height || 50;
|
|
506
|
+
_pushUndo();
|
|
507
|
+
_currentShape = makeCylinder(r, h);
|
|
508
|
+
description += `Cylinder r${r} h${h}mm`;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
case 'sphere': {
|
|
513
|
+
const r = params.radius || 25;
|
|
514
|
+
_pushUndo();
|
|
515
|
+
_currentShape = makeSphere(r);
|
|
516
|
+
description += `Sphere r${r}mm`;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
case 'cone': {
|
|
521
|
+
const br = params.bottomRadius || params.radius || 25;
|
|
522
|
+
const tr = params.topRadius || 0;
|
|
523
|
+
const h = params.height || 50;
|
|
524
|
+
_pushUndo();
|
|
525
|
+
_currentShape = makeCone(br, tr, h);
|
|
526
|
+
description += `Cone r${br}/${tr} h${h}mm`;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
case 'hole': {
|
|
531
|
+
if (!_currentShape) {
|
|
532
|
+
console.warn('[BRep] No shape to cut hole in — create a shape first');
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
const r = params.radius || 5;
|
|
536
|
+
const depth = params.depth || params.height || 200;
|
|
537
|
+
const count = params.count || 1;
|
|
538
|
+
|
|
539
|
+
_pushUndo();
|
|
540
|
+
|
|
541
|
+
for (let i = 0; i < count; i++) {
|
|
542
|
+
// Position holes at corners for count=4, or center for count=1
|
|
543
|
+
let dx = 0, dz = 0;
|
|
544
|
+
if (count === 4) {
|
|
545
|
+
const spread = (params.spread || 30);
|
|
546
|
+
const corners = [[-spread, -spread], [spread, -spread], [spread, spread], [-spread, spread]];
|
|
547
|
+
[dx, dz] = corners[i % 4];
|
|
548
|
+
} else if (count > 1) {
|
|
549
|
+
const angle = (i / count) * Math.PI * 2;
|
|
550
|
+
const spread = (params.spread || 20);
|
|
551
|
+
dx = Math.cos(angle) * spread;
|
|
552
|
+
dz = Math.sin(angle) * spread;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Create cylinder tool at position, axis along Y
|
|
556
|
+
let tool = makeCylinder(r, depth);
|
|
557
|
+
if (dx !== 0 || dz !== 0) {
|
|
558
|
+
tool = translate(tool, dx, 0, dz);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
_currentShape = booleanCut(_currentShape, tool);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
description += ` + ${count} hole${count > 1 ? 's' : ''} (r${r}mm)`;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
case 'fillet': {
|
|
569
|
+
if (!_currentShape) break;
|
|
570
|
+
const r = params.radius || 5;
|
|
571
|
+
_pushUndo();
|
|
572
|
+
_currentShape = filletAll(_currentShape, r);
|
|
573
|
+
description += ` + fillet r${r}mm`;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
case 'chamfer': {
|
|
578
|
+
if (!_currentShape) break;
|
|
579
|
+
const d = params.distance || 3;
|
|
580
|
+
_pushUndo();
|
|
581
|
+
_currentShape = chamferAll(_currentShape, d);
|
|
582
|
+
description += ` + chamfer ${d}mm`;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
case 'fuse': {
|
|
587
|
+
if (!_currentShape || !params._toolShape) break;
|
|
588
|
+
_pushUndo();
|
|
589
|
+
_currentShape = booleanFuse(_currentShape, params._toolShape);
|
|
590
|
+
description += ' + fuse';
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
default:
|
|
595
|
+
console.warn(`[BRep] Unknown command type: ${type}`);
|
|
596
|
+
}
|
|
597
|
+
} catch (err) {
|
|
598
|
+
console.error(`[BRep] Command "${type}" failed:`, err);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Convert final shape to Three.js
|
|
603
|
+
if (!_currentShape) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const result = shapeToMesh(_currentShape, { color: 0x4488cc }, 0.5);
|
|
608
|
+
// Scale mesh to scene units
|
|
609
|
+
result.mesh.scale.set(SCALE, SCALE, SCALE);
|
|
610
|
+
result.wireframe.scale.set(SCALE, SCALE, SCALE);
|
|
611
|
+
result.description = description;
|
|
612
|
+
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ============================================================================
|
|
617
|
+
// UNDO SUPPORT
|
|
618
|
+
// ============================================================================
|
|
619
|
+
|
|
620
|
+
function _pushUndo() {
|
|
621
|
+
if (_currentShape) {
|
|
622
|
+
_shapeStack.push(_currentShape);
|
|
623
|
+
if (_shapeStack.length > 20) _shapeStack.shift(); // limit memory
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function undo() {
|
|
628
|
+
if (_shapeStack.length > 0) {
|
|
629
|
+
_currentShape = _shapeStack.pop();
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function getCurrentShape() { return _currentShape; }
|
|
636
|
+
export function clearShape() { _currentShape = null; _shapeStack.length = 0; }
|
|
637
|
+
|
|
638
|
+
// ============================================================================
|
|
639
|
+
// REGISTER ON WINDOW FOR COPILOT ACCESS
|
|
640
|
+
// ============================================================================
|
|
641
|
+
|
|
642
|
+
window.brepEngine = {
|
|
643
|
+
initBRep,
|
|
644
|
+
isReady,
|
|
645
|
+
makeBox,
|
|
646
|
+
makeCylinder,
|
|
647
|
+
makeSphere,
|
|
648
|
+
makeCone,
|
|
649
|
+
booleanCut,
|
|
650
|
+
booleanFuse,
|
|
651
|
+
booleanIntersect,
|
|
652
|
+
filletAll,
|
|
653
|
+
chamferAll,
|
|
654
|
+
translate,
|
|
655
|
+
shapeToGeometry,
|
|
656
|
+
shapeToMesh,
|
|
657
|
+
executeCommands,
|
|
658
|
+
undo,
|
|
659
|
+
getCurrentShape,
|
|
660
|
+
clearShape,
|
|
661
|
+
};
|