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,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STEP Viewer Module for cycleCAD
|
|
3
|
+
* Drag-and-drop STEP/STP file import with interactive 3D viewer
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Drag-drop and file picker import
|
|
7
|
+
* - occt-import-js for <50MB, server converter for larger files
|
|
8
|
+
* - Instant Three.js rendering with part coloring
|
|
9
|
+
* - Part selection, visibility toggle, BOM export
|
|
10
|
+
* - Exploded view slider
|
|
11
|
+
* - Share & embed code generation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const PALETTE = [0x4488CC, 0xCC4444, 0x44AA44, 0xCCAA44, 0x8844CC, 0x44CCAA,
|
|
15
|
+
0xCC6644, 0x4466CC, 0xAA44CC, 0x44CC66, 0xCC4488, 0x88CC44];
|
|
16
|
+
|
|
17
|
+
const OCCT_WASM_URL = 'https://cdn.jsdelivr.net/npm/occt-import-js@0.0.23/dist/occt-import-js.wasm';
|
|
18
|
+
const OCCT_CDN_BASE = 'https://cdn.jsdelivr.net/npm/occt-import-js@0.0.23/dist/';
|
|
19
|
+
const SIZE_THRESHOLD = 50 * 1024 * 1024; // 50MB threshold for browser vs server parsing
|
|
20
|
+
|
|
21
|
+
let stepViewerState = {
|
|
22
|
+
scene: null,
|
|
23
|
+
camera: null,
|
|
24
|
+
renderer: null,
|
|
25
|
+
allParts: [],
|
|
26
|
+
meshMap: new Map(), // mesh → part data
|
|
27
|
+
selectedMesh: null,
|
|
28
|
+
explodeAmount: 0,
|
|
29
|
+
originalPositions: new Map(),
|
|
30
|
+
centerOfMass: new THREE.Vector3(),
|
|
31
|
+
container: null,
|
|
32
|
+
partListPanel: null,
|
|
33
|
+
dropOverlay: null,
|
|
34
|
+
converterUrl: localStorage.getItem('ev_converter_url') || 'http://localhost:8787'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export async function initStepViewer(container) {
|
|
38
|
+
stepViewerState.container = container;
|
|
39
|
+
|
|
40
|
+
// Setup drop overlay
|
|
41
|
+
setupDropOverlay(container);
|
|
42
|
+
|
|
43
|
+
// Setup file input
|
|
44
|
+
setupFileInput();
|
|
45
|
+
|
|
46
|
+
// Setup part list panel
|
|
47
|
+
setupPartListPanel();
|
|
48
|
+
|
|
49
|
+
// Setup Three.js scene (reuse global or create new)
|
|
50
|
+
if (window._scene && window._camera && window._renderer) {
|
|
51
|
+
stepViewerState.scene = window._scene;
|
|
52
|
+
stepViewerState.camera = window._camera;
|
|
53
|
+
stepViewerState.renderer = window._renderer;
|
|
54
|
+
} else {
|
|
55
|
+
setupScene(container);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Setup drag-drop
|
|
59
|
+
setupDragDrop(container);
|
|
60
|
+
|
|
61
|
+
// Setup part selection click handler
|
|
62
|
+
setupPartSelection();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function setupDropOverlay(container) {
|
|
66
|
+
const overlay = document.createElement('div');
|
|
67
|
+
overlay.id = 'step-drop-overlay';
|
|
68
|
+
overlay.style.cssText = `
|
|
69
|
+
position: fixed;
|
|
70
|
+
top: 0;
|
|
71
|
+
left: 0;
|
|
72
|
+
width: 100%;
|
|
73
|
+
height: 100%;
|
|
74
|
+
background: rgba(0, 0, 0, 0.7);
|
|
75
|
+
border: 3px dashed #0284C7;
|
|
76
|
+
display: none;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
align-items: center;
|
|
80
|
+
z-index: 9999;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
font-family: Calibri, sans-serif;
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
overlay.innerHTML = `
|
|
86
|
+
<div style="text-align: center; pointer-events: auto;">
|
|
87
|
+
<div style="font-size: 48px; margin-bottom: 20px;">📦</div>
|
|
88
|
+
<h2 style="color: #0284C7; margin: 0 0 10px 0; font-size: 24px;">Drop STEP file here</h2>
|
|
89
|
+
<p style="color: #aaa; margin: 0; font-size: 14px;">.step, .stp up to 500MB</p>
|
|
90
|
+
</div>
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
document.body.appendChild(overlay);
|
|
94
|
+
stepViewerState.dropOverlay = overlay;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setupFileInput() {
|
|
98
|
+
let input = document.getElementById('step-file-input');
|
|
99
|
+
if (!input) {
|
|
100
|
+
input = document.createElement('input');
|
|
101
|
+
input.id = 'step-file-input';
|
|
102
|
+
input.type = 'file';
|
|
103
|
+
input.accept = '.step,.stp,.STEP,.STP';
|
|
104
|
+
input.style.display = 'none';
|
|
105
|
+
document.body.appendChild(input);
|
|
106
|
+
|
|
107
|
+
input.addEventListener('change', async (e) => {
|
|
108
|
+
if (e.target.files[0]) {
|
|
109
|
+
await importStepFile(e.target.files[0]);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function setupDragDrop(container) {
|
|
116
|
+
const overlay = stepViewerState.dropOverlay;
|
|
117
|
+
|
|
118
|
+
container.addEventListener('dragover', (e) => {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
if (e.dataTransfer.types.includes('Files')) {
|
|
122
|
+
overlay.style.display = 'flex';
|
|
123
|
+
overlay.style.pointerEvents = 'auto';
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
container.addEventListener('dragleave', (e) => {
|
|
128
|
+
if (e.target === container) {
|
|
129
|
+
overlay.style.display = 'none';
|
|
130
|
+
overlay.style.pointerEvents = 'none';
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
container.addEventListener('drop', async (e) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
overlay.style.display = 'none';
|
|
138
|
+
overlay.style.pointerEvents = 'none';
|
|
139
|
+
|
|
140
|
+
const files = e.dataTransfer.files;
|
|
141
|
+
for (let file of files) {
|
|
142
|
+
if (file.name.match(/\.(step|stp)$/i)) {
|
|
143
|
+
await importStepFile(file);
|
|
144
|
+
break; // Only load first STEP file
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function setupPartListPanel() {
|
|
151
|
+
let panel = document.getElementById('step-part-list-panel');
|
|
152
|
+
if (panel) {
|
|
153
|
+
panel.remove();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
panel = document.createElement('div');
|
|
157
|
+
panel.id = 'step-part-list-panel';
|
|
158
|
+
panel.style.cssText = `
|
|
159
|
+
position: fixed;
|
|
160
|
+
left: 0;
|
|
161
|
+
top: 0;
|
|
162
|
+
width: 320px;
|
|
163
|
+
height: 100vh;
|
|
164
|
+
background: #1e1e1e;
|
|
165
|
+
border-right: 1px solid #333;
|
|
166
|
+
color: #ddd;
|
|
167
|
+
font-family: Calibri, sans-serif;
|
|
168
|
+
font-size: 13px;
|
|
169
|
+
z-index: 100;
|
|
170
|
+
display: none;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
box-shadow: 2px 0 8px rgba(0,0,0,0.5);
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
panel.innerHTML = `
|
|
177
|
+
<div style="padding: 12px; border-bottom: 1px solid #333; font-weight: bold; display: flex; justify-content: space-between; align-items: center;">
|
|
178
|
+
<span>Parts</span>
|
|
179
|
+
<button id="step-part-list-close" style="background: none; border: none; color: #aaa; cursor: pointer; font-size: 18px; padding: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;">×</button>
|
|
180
|
+
</div>
|
|
181
|
+
<div id="step-part-list-content" style="overflow-y: auto; flex: 1; padding: 8px;">
|
|
182
|
+
<div style="color: #666; padding: 12px; text-align: center;">No model loaded</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div style="border-top: 1px solid #333; padding: 8px; display: flex; gap: 8px; flex-direction: column;">
|
|
185
|
+
<button id="step-export-bom" style="padding: 8px; background: #0284C7; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold;">📊 Export BOM</button>
|
|
186
|
+
<button id="step-share-code" style="padding: 8px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold;">🔗 Share / Embed</button>
|
|
187
|
+
</div>
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
document.body.appendChild(panel);
|
|
191
|
+
stepViewerState.partListPanel = panel;
|
|
192
|
+
|
|
193
|
+
document.getElementById('step-part-list-close').addEventListener('click', () => {
|
|
194
|
+
panel.style.display = 'none';
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
document.getElementById('step-export-bom').addEventListener('click', exportBOM);
|
|
198
|
+
document.getElementById('step-share-code').addEventListener('click', () => {
|
|
199
|
+
const code = getEmbedCode();
|
|
200
|
+
const dialog = prompt('Embed code (copy to share on web):\n\n', code);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function setupScene(container) {
|
|
205
|
+
const width = container.clientWidth;
|
|
206
|
+
const height = container.clientHeight;
|
|
207
|
+
|
|
208
|
+
stepViewerState.camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100000);
|
|
209
|
+
stepViewerState.camera.position.set(500, 400, 500);
|
|
210
|
+
|
|
211
|
+
stepViewerState.scene = new THREE.Scene();
|
|
212
|
+
stepViewerState.scene.background = new THREE.Color(0x222222);
|
|
213
|
+
|
|
214
|
+
// Lights
|
|
215
|
+
const ambLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
216
|
+
stepViewerState.scene.add(ambLight);
|
|
217
|
+
|
|
218
|
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
219
|
+
dirLight.position.set(300, 400, 300);
|
|
220
|
+
dirLight.castShadow = true;
|
|
221
|
+
dirLight.shadow.camera.far = 2000;
|
|
222
|
+
stepViewerState.scene.add(dirLight);
|
|
223
|
+
|
|
224
|
+
stepViewerState.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
|
225
|
+
stepViewerState.renderer.setSize(width, height);
|
|
226
|
+
stepViewerState.renderer.shadowMap.enabled = true;
|
|
227
|
+
container.appendChild(stepViewerState.renderer.domElement);
|
|
228
|
+
|
|
229
|
+
// Grid
|
|
230
|
+
const gridHelper = new THREE.GridHelper(1000, 20, 0x444444, 0x222222);
|
|
231
|
+
stepViewerState.scene.add(gridHelper);
|
|
232
|
+
|
|
233
|
+
// Animate loop
|
|
234
|
+
function animate() {
|
|
235
|
+
requestAnimationFrame(animate);
|
|
236
|
+
stepViewerState.renderer.render(stepViewerState.scene, stepViewerState.camera);
|
|
237
|
+
}
|
|
238
|
+
animate();
|
|
239
|
+
|
|
240
|
+
// Handle resize
|
|
241
|
+
window.addEventListener('resize', () => {
|
|
242
|
+
const w = container.clientWidth;
|
|
243
|
+
const h = container.clientHeight;
|
|
244
|
+
stepViewerState.camera.aspect = w / h;
|
|
245
|
+
stepViewerState.camera.updateProjectionMatrix();
|
|
246
|
+
stepViewerState.renderer.setSize(w, h);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function setupPartSelection() {
|
|
251
|
+
const canvas = stepViewerState.renderer.domElement;
|
|
252
|
+
const raycaster = new THREE.Raycaster();
|
|
253
|
+
const mouse = new THREE.Vector2();
|
|
254
|
+
|
|
255
|
+
canvas.addEventListener('click', (e) => {
|
|
256
|
+
const rect = canvas.getBoundingClientRect();
|
|
257
|
+
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
258
|
+
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
259
|
+
|
|
260
|
+
raycaster.setFromCamera(mouse, stepViewerState.camera);
|
|
261
|
+
const intersects = raycaster.intersectObjects(stepViewerState.scene.children, true);
|
|
262
|
+
|
|
263
|
+
// Deselect previous
|
|
264
|
+
if (stepViewerState.selectedMesh) {
|
|
265
|
+
const prevPart = stepViewerState.meshMap.get(stepViewerState.selectedMesh);
|
|
266
|
+
if (prevPart) {
|
|
267
|
+
stepViewerState.selectedMesh.material.emissive.setHex(0x000000);
|
|
268
|
+
updatePartListHighlight(null);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Select new
|
|
273
|
+
for (let hit of intersects) {
|
|
274
|
+
const part = stepViewerState.meshMap.get(hit.object);
|
|
275
|
+
if (part) {
|
|
276
|
+
stepViewerState.selectedMesh = hit.object;
|
|
277
|
+
hit.object.material.emissive.setHex(0x444444);
|
|
278
|
+
updatePartListHighlight(part.index);
|
|
279
|
+
console.log(`[STEP] Selected: ${part.name}`);
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function importStepFile(file) {
|
|
287
|
+
console.log(`[STEP] Importing ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)...`);
|
|
288
|
+
|
|
289
|
+
const statusDiv = showImportStatus('Parsing STEP file...');
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
let meshes = [];
|
|
293
|
+
|
|
294
|
+
// Choose parsing method
|
|
295
|
+
if (file.size < SIZE_THRESHOLD) {
|
|
296
|
+
console.log('[STEP] Using browser WASM (occt-import-js)');
|
|
297
|
+
meshes = await parseViaOCCT(file);
|
|
298
|
+
} else {
|
|
299
|
+
console.log('[STEP] Using server converter (file >50MB)');
|
|
300
|
+
meshes = await parseViaServer(file);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
statusDiv.textContent = `Building 3D scene (${meshes.length} parts)...`;
|
|
304
|
+
buildScene(meshes, file.name);
|
|
305
|
+
|
|
306
|
+
statusDiv.textContent = `✓ Loaded ${meshes.length} parts from ${file.name}`;
|
|
307
|
+
setTimeout(() => statusDiv.remove(), 3000);
|
|
308
|
+
|
|
309
|
+
// Show part list
|
|
310
|
+
stepViewerState.partListPanel.style.display = 'flex';
|
|
311
|
+
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error('[STEP] Import failed:', err);
|
|
314
|
+
statusDiv.textContent = `✗ Error: ${err.message}`;
|
|
315
|
+
statusDiv.style.color = '#ff6666';
|
|
316
|
+
setTimeout(() => statusDiv.remove(), 5000);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function parseViaOCCT(file) {
|
|
321
|
+
const occtImportJs = await import(OCCT_CDN_BASE + 'occt-import-js.js');
|
|
322
|
+
const occt = await occtImportJs.default({
|
|
323
|
+
locateFile: (filename) => {
|
|
324
|
+
if (filename.endsWith('.wasm')) return OCCT_WASM_URL;
|
|
325
|
+
return OCCT_CDN_BASE + filename;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
330
|
+
const result = occt.ReadStepFile(buffer, null);
|
|
331
|
+
|
|
332
|
+
if (!result || !result.meshes || result.meshes.length === 0) {
|
|
333
|
+
throw new Error('No meshes extracted from STEP file');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return result.meshes.map((mesh, i) => ({
|
|
337
|
+
name: mesh.name || `Part_${i}`,
|
|
338
|
+
index: i,
|
|
339
|
+
position: mesh.attributes.position.array,
|
|
340
|
+
normal: mesh.attributes.normal?.array,
|
|
341
|
+
index: mesh.attributes.index?.array,
|
|
342
|
+
color: mesh.color || PALETTE[i % PALETTE.length]
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function parseViaServer(file) {
|
|
347
|
+
const formData = new FormData();
|
|
348
|
+
formData.append('file', file);
|
|
349
|
+
|
|
350
|
+
const response = await fetch(`${stepViewerState.converterUrl}/convert/metadata`, {
|
|
351
|
+
method: 'POST',
|
|
352
|
+
body: formData
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
throw new Error(`Server error: ${response.statusText}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const data = await response.json();
|
|
360
|
+
return data.meshes || [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function buildScene(meshes, filename) {
|
|
364
|
+
// Clear previous
|
|
365
|
+
stepViewerState.allParts = [];
|
|
366
|
+
stepViewerState.meshMap.clear();
|
|
367
|
+
stepViewerState.originalPositions.clear();
|
|
368
|
+
|
|
369
|
+
let group = new THREE.Group();
|
|
370
|
+
let bbox = new THREE.Box3();
|
|
371
|
+
let centerOfMass = new THREE.Vector3();
|
|
372
|
+
let totalVolume = 0;
|
|
373
|
+
|
|
374
|
+
meshes.forEach((meshData, idx) => {
|
|
375
|
+
const geometry = new THREE.BufferGeometry();
|
|
376
|
+
|
|
377
|
+
const positions = new Float32Array(meshData.position);
|
|
378
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
379
|
+
|
|
380
|
+
if (meshData.normal) {
|
|
381
|
+
const normals = new Float32Array(meshData.normal);
|
|
382
|
+
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
|
|
383
|
+
} else {
|
|
384
|
+
geometry.computeVertexNormals();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (meshData.index) {
|
|
388
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(meshData.index), 1));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
geometry.computeBoundingBox();
|
|
392
|
+
bbox.expandByObject(new THREE.Object3D());
|
|
393
|
+
|
|
394
|
+
const color = new THREE.Color(meshData.color);
|
|
395
|
+
const material = new THREE.MeshPhongMaterial({
|
|
396
|
+
color: color,
|
|
397
|
+
emissive: 0x000000,
|
|
398
|
+
shininess: 100
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
402
|
+
mesh.castShadow = true;
|
|
403
|
+
mesh.receiveShadow = true;
|
|
404
|
+
|
|
405
|
+
group.add(mesh);
|
|
406
|
+
|
|
407
|
+
const partData = {
|
|
408
|
+
name: meshData.name,
|
|
409
|
+
index: idx,
|
|
410
|
+
mesh: mesh,
|
|
411
|
+
visible: true,
|
|
412
|
+
originalPosition: mesh.position.clone()
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
stepViewerState.meshMap.set(mesh, partData);
|
|
416
|
+
stepViewerState.allParts.push(partData);
|
|
417
|
+
stepViewerState.originalPositions.set(mesh, mesh.position.clone());
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
stepViewerState.scene.add(group);
|
|
421
|
+
|
|
422
|
+
// Fit camera
|
|
423
|
+
const size = new THREE.Vector3();
|
|
424
|
+
bbox.getSize(size);
|
|
425
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
426
|
+
const fov = stepViewerState.camera.fov * (Math.PI / 180);
|
|
427
|
+
const cameraDistance = maxDim / (2 * Math.tan(fov / 2)) * 1.5;
|
|
428
|
+
|
|
429
|
+
stepViewerState.camera.position.set(cameraDistance, cameraDistance * 0.6, cameraDistance);
|
|
430
|
+
stepViewerState.camera.lookAt(group.position);
|
|
431
|
+
|
|
432
|
+
// Update part list
|
|
433
|
+
updatePartList();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function updatePartList() {
|
|
437
|
+
const content = document.getElementById('step-part-list-content');
|
|
438
|
+
if (!content) return;
|
|
439
|
+
|
|
440
|
+
const html = stepViewerState.allParts.map((part, i) => `
|
|
441
|
+
<div class="step-part-item" data-index="${i}" style="
|
|
442
|
+
padding: 8px;
|
|
443
|
+
margin: 4px 0;
|
|
444
|
+
background: #2a2a2a;
|
|
445
|
+
border-radius: 4px;
|
|
446
|
+
cursor: pointer;
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: center;
|
|
449
|
+
gap: 8px;
|
|
450
|
+
border-left: 3px solid #${part.mesh.material.color.getHexString()};
|
|
451
|
+
user-select: none;
|
|
452
|
+
">
|
|
453
|
+
<div style="flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12px;">
|
|
454
|
+
${part.name}
|
|
455
|
+
</div>
|
|
456
|
+
<button class="step-toggle-visibility" data-index="${i}" style="
|
|
457
|
+
background: none;
|
|
458
|
+
border: none;
|
|
459
|
+
color: ${part.visible ? '#44aa44' : '#666'};
|
|
460
|
+
cursor: pointer;
|
|
461
|
+
font-size: 14px;
|
|
462
|
+
padding: 0;
|
|
463
|
+
width: 20px;
|
|
464
|
+
height: 20px;
|
|
465
|
+
">${part.visible ? '👁' : '🚫'}</button>
|
|
466
|
+
</div>
|
|
467
|
+
`).join('');
|
|
468
|
+
|
|
469
|
+
content.innerHTML = html || '<div style="color: #666; text-align: center; padding: 12px;">No parts</div>';
|
|
470
|
+
|
|
471
|
+
// Wire up event handlers
|
|
472
|
+
content.querySelectorAll('.step-part-item').forEach(el => {
|
|
473
|
+
el.addEventListener('click', (e) => {
|
|
474
|
+
if (!e.target.classList.contains('step-toggle-visibility')) {
|
|
475
|
+
const idx = parseInt(el.dataset.index);
|
|
476
|
+
const part = stepViewerState.allParts[idx];
|
|
477
|
+
part.mesh.material.emissive.setHex(0x444444);
|
|
478
|
+
stepViewerState.selectedMesh = part.mesh;
|
|
479
|
+
updatePartListHighlight(idx);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
content.querySelectorAll('.step-toggle-visibility').forEach(btn => {
|
|
485
|
+
btn.addEventListener('click', (e) => {
|
|
486
|
+
e.stopPropagation();
|
|
487
|
+
const idx = parseInt(btn.dataset.index);
|
|
488
|
+
const part = stepViewerState.allParts[idx];
|
|
489
|
+
part.visible = !part.visible;
|
|
490
|
+
part.mesh.visible = part.visible;
|
|
491
|
+
btn.textContent = part.visible ? '👁' : '🚫';
|
|
492
|
+
btn.style.color = part.visible ? '#44aa44' : '#666';
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function updatePartListHighlight(idx) {
|
|
498
|
+
document.querySelectorAll('.step-part-item').forEach(el => {
|
|
499
|
+
el.style.background = idx !== null && parseInt(el.dataset.index) === idx ? '#0284C7' : '#2a2a2a';
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function explodeView(amount) {
|
|
504
|
+
stepViewerState.explodeAmount = Math.max(0, Math.min(1, amount));
|
|
505
|
+
|
|
506
|
+
stepViewerState.allParts.forEach(part => {
|
|
507
|
+
const origPos = stepViewerState.originalPositions.get(part.mesh);
|
|
508
|
+
const direction = origPos.clone().normalize();
|
|
509
|
+
const distance = 200 * stepViewerState.explodeAmount;
|
|
510
|
+
|
|
511
|
+
part.mesh.position.copy(origPos).addScaledVector(direction, distance);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function exportBOM() {
|
|
516
|
+
if (stepViewerState.allParts.length === 0) {
|
|
517
|
+
alert('No parts loaded');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const csv = ['Part Number,Name,Material\n'];
|
|
522
|
+
stepViewerState.allParts.forEach((part, i) => {
|
|
523
|
+
csv.push(`${i + 1},"${part.name}",\n`);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const blob = new Blob([csv.join('')], { type: 'text/csv' });
|
|
527
|
+
const url = URL.createObjectURL(blob);
|
|
528
|
+
const a = document.createElement('a');
|
|
529
|
+
a.href = url;
|
|
530
|
+
a.download = `bom_${Date.now()}.csv`;
|
|
531
|
+
a.click();
|
|
532
|
+
URL.revokeObjectURL(url);
|
|
533
|
+
|
|
534
|
+
console.log('[STEP] BOM exported');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function getEmbedCode() {
|
|
538
|
+
const url = window.location.origin + window.location.pathname +
|
|
539
|
+
`?step_viewer=true&model=${btoa('imported_' + Date.now())}`;
|
|
540
|
+
|
|
541
|
+
return `<iframe
|
|
542
|
+
src="${url}"
|
|
543
|
+
width="1200" height="800"
|
|
544
|
+
style="border: none; border-radius: 8px;"
|
|
545
|
+
allow="fullscreen"
|
|
546
|
+
></iframe>`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function showImportStatus(message) {
|
|
550
|
+
let div = document.getElementById('step-import-status');
|
|
551
|
+
if (!div) {
|
|
552
|
+
div = document.createElement('div');
|
|
553
|
+
div.id = 'step-import-status';
|
|
554
|
+
document.body.appendChild(div);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
div.textContent = message;
|
|
558
|
+
div.style.cssText = `
|
|
559
|
+
position: fixed;
|
|
560
|
+
bottom: 20px;
|
|
561
|
+
right: 20px;
|
|
562
|
+
background: #0284C7;
|
|
563
|
+
color: white;
|
|
564
|
+
padding: 12px 20px;
|
|
565
|
+
border-radius: 6px;
|
|
566
|
+
font-family: Calibri, sans-serif;
|
|
567
|
+
font-size: 13px;
|
|
568
|
+
z-index: 1000;
|
|
569
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
570
|
+
`;
|
|
571
|
+
|
|
572
|
+
return div;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Register on window
|
|
576
|
+
window.stepViewer = {
|
|
577
|
+
initStepViewer,
|
|
578
|
+
importStepFile,
|
|
579
|
+
explodeView,
|
|
580
|
+
exportBOM,
|
|
581
|
+
getEmbedCode
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
console.log('[STEP Viewer] Module loaded. Use window.stepViewer.initStepViewer(container)');
|