-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathloaders.js
More file actions
339 lines (281 loc) · 9.45 KB
/
loaders.js
File metadata and controls
339 lines (281 loc) · 9.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
/**
* Loaders Module
* Handles loading of 3D models and G-code files
*/
import * as THREE from 'three';
import { GCodeLoaderExtended } from '../libs/GCodeLoaderExtended.js';
import { loadSlicingSettings } from './state.js';
// File extensions
export const MODEL_EXTENSIONS = ['stl', 'obj', '3mf', 'amf', 'ply', 'gltf', 'glb', 'dae'];
export const GCODE_EXTENSIONS = ['gcode', 'gco', 'nc'];
// Fallback build plate dimensions (mm) for known printers when Polyslice is unavailable.
const PRINTER_BUILD_PLATES = {
Ender3: { width: 220, length: 220, height: 250 },
Ender3V2: { width: 220, length: 220, height: 250 },
Ender3Pro: { width: 220, length: 220, height: 250 },
PrusaI3MK3S: { width: 250, length: 210, height: 210 },
AnycubicI3Mega: { width: 210, length: 210, height: 205 },
UltimakerS5: { width: 330, length: 240, height: 300 },
BambuLabP1P: { width: 256, length: 256, height: 256 }
};
const DEFAULT_BUILD_PLATE_WIDTH = 220;
const DEFAULT_BUILD_PLATE_LENGTH = 220;
const DEFAULT_BUILD_PLATE_HEIGHT = 250;
/**
* Get the build plate dimensions for the currently selected printer.
* Tries to use window.Polyslice.Printer when available; falls back to a
* lookup table and then to the Ender3 defaults (220 × 220 × 250 mm).
*
* @returns {{ width: number, length: number, height: number }}
*/
export function getBuildPlateDimensions() {
try {
const savedSettings = loadSlicingSettings();
const printerName = (savedSettings && savedSettings.printer) ? savedSettings.printer : 'Ender3';
if (window.Polyslice?.Printer) {
const printer = new window.Polyslice.Printer(printerName);
return { width: printer.getSizeX(), length: printer.getSizeY(), height: printer.getSizeZ() };
}
const preset = PRINTER_BUILD_PLATES[printerName];
if (preset) {
return preset;
}
} catch (error) {
console.warn('Could not determine build plate dimensions:', error);
}
return { width: DEFAULT_BUILD_PLATE_WIDTH, length: DEFAULT_BUILD_PLATE_LENGTH, height: DEFAULT_BUILD_PLATE_HEIGHT };
}
/**
* Position a loaded mesh on the build plate by:
* 1. Placing its bottom face at Z = 0 (lay flat on the build plate).
* 2. Centering it in XY at (buildPlateWidth / 2, buildPlateLength / 2).
*
* This matches the positioning applied by the Polyslice slicer so that the
* model's visual location in the viewport corresponds to its printed location
* in the generated G-code.
*/
export function positionMeshOnBuildPlate(object) {
const { width: buildPlateWidth, length: buildPlateLength } = getBuildPlateDimensions();
const box = new THREE.Box3().setFromObject(object);
if (box.isEmpty()) {
console.warn('positionMeshOnBuildPlate: bounding box is empty, skipping positioning');
return;
}
const centerX = (box.min.x + box.max.x) / 2;
const centerY = (box.min.y + box.max.y) / 2;
const minZ = box.min.z;
object.position.x += (buildPlateWidth / 2) - centerX;
object.position.y += (buildPlateLength / 2) - centerY;
object.position.z += -minZ;
}
/**
* Handle file upload event.
*/
export function handleFileUpload(event, loadModelCallback, loadGCodeCallback) {
const file = event.target.files[0];
if (!file) {
return;
}
const extension = file.name.split('.').pop().toLowerCase();
if (MODEL_EXTENSIONS.includes(extension)) {
loadModelCallback(file);
} else if (GCODE_EXTENSIONS.includes(extension)) {
const reader = new FileReader();
reader.onload = function (e) {
const content = e.target.result;
loadGCodeCallback(content, file.name);
};
reader.readAsText(file);
} else {
console.warn(`Unsupported file format: ${extension}`);
}
// Clear the input value to allow re-uploading the same file
event.target.value = '';
}
/**
* Load and visualize a 3D model file using the Polyslice loader.
*/
export function loadModel(file, scene, callbacks) {
const { updateDownloadVisibility, hideSlicingGUI, displayMesh, clearGCodeData, clearMeshData } = callbacks;
const url = URL.createObjectURL(file);
const extension = file.name.split('.').pop().toLowerCase();
// Clear G-code content when loading a model
clearGCodeData();
updateDownloadVisibility(false);
// Hide slicing GUI while loading
hideSlicingGUI();
// Remove previous mesh and G-code objects
clearMeshData(scene);
// Hide G-code specific sliders
document.getElementById('layer-slider-container').classList.remove('visible');
document.getElementById('move-slider-container').classList.remove('visible');
// Create material for loaded models
const normalMaterial = new THREE.MeshNormalMaterial();
// Use format-specific loader method
let loadPromise;
switch (extension) {
case 'stl':
loadPromise = window.PolysliceLoader.loadSTL(url, normalMaterial);
break;
case 'obj':
loadPromise = window.PolysliceLoader.loadOBJ(url, normalMaterial);
break;
case 'ply':
loadPromise = window.PolysliceLoader.loadPLY(url, normalMaterial);
break;
case '3mf':
loadPromise = window.PolysliceLoader.load3MF(url);
break;
case 'amf':
loadPromise = window.PolysliceLoader.loadAMF(url);
break;
case 'gltf':
case 'glb':
loadPromise = window.PolysliceLoader.loadGLTF(url);
break;
case 'dae':
loadPromise = window.PolysliceLoader.loadCollada(url);
break;
default:
console.error(`Unsupported file format: ${extension}`);
URL.revokeObjectURL(url);
return;
}
loadPromise
.then((result) => {
// Handle single mesh or array of meshes
let object;
if (Array.isArray(result)) {
object = new THREE.Group();
result.forEach((mesh) => {
if (['3mf', 'amf', 'gltf', 'glb', 'dae'].includes(extension)) {
mesh.material = normalMaterial;
}
object.add(mesh);
});
} else {
object = result;
if (['3mf', 'amf', 'gltf', 'glb', 'dae'].includes(extension)) {
object.traverse((child) => {
if (child.isMesh) {
child.material = normalMaterial;
}
});
}
}
displayMesh(object, file.name);
URL.revokeObjectURL(url);
})
.catch((error) => {
console.error('Error loading model:', error);
URL.revokeObjectURL(url);
});
}
/**
* Display a loaded mesh in the scene.
*/
export function displayMesh(object, filename, scene, callbacks) {
const { centerCamera, hideForkMeBanner, hideGCodeLegends, createSlicingGUI, updateMeshInfo } = callbacks;
// Center the model on the build plate and lay it flat at Z = 0 so its
// position matches the location it will occupy in the sliced G-code.
positionMeshOnBuildPlate(object);
scene.add(object);
// Update info panel
updateMeshInfo(filename, object);
// Center camera
centerCamera(object);
// Hide fork me banner
hideForkMeBanner();
// Hide G-code legends
hideGCodeLegends();
// Show slicing GUI
createSlicingGUI();
return object;
}
/**
* Load and visualize G-code content.
*/
export function loadGCode(content, filename, scene, callbacks) {
const {
updateDownloadVisibility,
hideSlicingGUI,
hideForkMeBanner,
showGCodeLegends,
centerCamera,
setupLayerSlider,
setupMoveSlider,
updateInfo,
applyThickLines,
applyTranslucent,
clearMeshData,
clearGCodeData
} = callbacks;
// Store the G-code content
updateDownloadVisibility(true, content, filename);
// Remove previous objects
clearGCodeData(scene);
clearMeshData(scene);
// Hide slicing GUI
hideSlicingGUI();
// Hide fork me banner
hideForkMeBanner();
// Show G-code legends
showGCodeLegends();
// Parse G-code
const loader = new GCodeLoaderExtended();
loader.splitLayer = true;
const gcodeObject = loader.parse(content);
// Add to scene
scene.add(gcodeObject);
// Setup layers and sliders
setupLayerSlider(gcodeObject);
setupMoveSlider(gcodeObject);
// Update info panel
updateInfo(filename, gcodeObject);
// Center camera
centerCamera(gcodeObject);
// Apply settings
const thickLinesCheckbox = document.getElementById('thick-lines-checkbox');
if (thickLinesCheckbox && thickLinesCheckbox.checked) {
applyThickLines(gcodeObject, true);
}
const translucentLinesCheckbox = document.getElementById('translucent-lines-checkbox');
if (translucentLinesCheckbox && translucentLinesCheckbox.checked) {
applyTranslucent(gcodeObject, true);
}
return gcodeObject;
}
/**
* Update mesh info panel.
*/
export function updateMeshInfo(filename, object) {
document.getElementById('filename').textContent = filename;
let totalTriangles = 0;
let totalVertices = 0;
let meshCount = 0;
object.traverse((child) => {
if (child.isMesh) {
meshCount++;
if (child.geometry) {
const geometry = child.geometry;
if (geometry.index) {
totalTriangles += geometry.index.count / 3;
} else if (geometry.attributes.position) {
totalTriangles += geometry.attributes.position.count / 3;
}
if (geometry.attributes.position) {
totalVertices += geometry.attributes.position.count;
}
}
}
});
const statsLines = [
`Meshes: ${meshCount}`,
`Triangles: ${Math.floor(totalTriangles).toLocaleString()}`,
`Vertices: ${totalVertices.toLocaleString()}`
];
document.getElementById('stats').textContent = statsLines.join('\n');
// Reset info position
document.getElementById('info').style.bottom = '20px';
document.getElementById('info').style.left = '20px';
}