diff --git a/client/viewer3d.js b/client/viewer3d.js
index b57b224..27ef62a 100644
--- a/client/viewer3d.js
+++ b/client/viewer3d.js
@@ -239,9 +239,6 @@ class ModelViewer {
this.scene.remove(this.model);
}
- // Create a group to hold all objects
- const group = new THREE.Group();
-
// Build a map of all objects by ID
const objectMap = new Map();
const objectElements = xmlDoc.getElementsByTagName('object');
@@ -283,7 +280,7 @@ class ModelViewer {
const componentElements = obj.getElementsByTagName('component');
if (componentElements.length > 0) {
- // This is a composite object, add all its components
+ // This is a composite object, add all its components as separate items
console.log(`Object ${objectId} has ${componentElements.length} components`);
for (let j = 0; j < componentElements.length; j++) {
const comp = componentElements[j];
@@ -293,7 +290,8 @@ class ModelViewer {
if (compObjectId && objectMap.has(compObjectId)) {
objectsToRender.push({
object: objectMap.get(compObjectId),
- transform: compTransform
+ transform: compTransform,
+ componentName: `Component ${j + 1}`
});
}
}
@@ -301,18 +299,23 @@ class ModelViewer {
// Regular mesh object
objectsToRender.push({
object: obj,
- transform: transform
+ transform: transform,
+ componentName: `Model ${i + 1}`
});
}
}
}
} else {
- // No build section, render all objects with meshes
- console.log('No build section found, rendering all objects with meshes');
+ // No build section, render each object separately
+ console.log('No build section found, rendering all objects with meshes separately');
for (let i = 0; i < objectElements.length; i++) {
const obj = objectElements[i];
if (obj.getElementsByTagName('mesh').length > 0) {
- objectsToRender.push({ object: obj, transform: null });
+ objectsToRender.push({
+ object: obj,
+ transform: null,
+ componentName: `Model ${i + 1}`
+ });
}
}
}
@@ -323,23 +326,6 @@ class ModelViewer {
if (objectsToRender.length === 0) {
console.log('No objects in main model, loading separate object files...');
- // Try to load cut_information.xml for positioning data
- let cutInfo = null;
- const cutInfoFile = files.find(f => f.includes('cut_information.xml'));
- if (cutInfoFile) {
- try {
- const cutInfoEntry = zip.file(cutInfoFile);
- if (cutInfoEntry) {
- const cutInfoContent = await cutInfoEntry.async('text');
- const cutInfoDoc = parser.parseFromString(cutInfoContent, 'text/xml');
- cutInfo = cutInfoDoc;
- console.log('Loaded cut_information.xml for positioning');
- }
- } catch (err) {
- console.error('Error loading cut_information.xml:', err);
- }
- }
-
// Load all separate object files (like object_10.model, etc.)
const objectFiles = modelFiles.filter(f =>
f.path.includes('/Objects/') && f.path.endsWith('.model')
@@ -347,10 +333,6 @@ class ModelViewer {
console.log(`Found ${objectFiles.length} separate object files`);
- // Auto-arrange objects in a grid - all on same Z plane
- const gridSize = Math.ceil(Math.sqrt(objectFiles.length));
- const spacing = 150; // mm spacing between objects
-
for (let idx = 0; idx < objectFiles.length; idx++) {
const objFile = objectFiles[idx];
try {
@@ -361,18 +343,10 @@ class ModelViewer {
for (let i = 0; i < objElements.length; i++) {
const obj = objElements[i];
if (obj.getElementsByTagName('mesh').length > 0) {
- // Calculate grid position - spread only on X axis (horizontal)
- // Keep all parts at Y=0, Z=0 so they stay on the same plane
- const offsetX = (idx - objectFiles.length / 2) * spacing;
-
- // Create a transform matrix for positioning (only X offset)
- // Format: m00 m01 m02 m10 m11 m12 m20 m21 m22 tx ty tz
- // This positions parts in a horizontal line on the build plate
- const transformMatrix = `1 0 0 0 1 0 0 0 1 ${offsetX} 0 0`;
-
objectsToRender.push({
object: obj,
- transform: transformMatrix
+ transform: null,
+ componentName: objFile.path.split('/').pop().replace('.model', '')
});
}
}
@@ -388,80 +362,14 @@ class ModelViewer {
throw new Error('No objects to render in 3MF file');
}
- // Process each object to render
- for (const { object: obj, transform } of objectsToRender) {
- // Get the mesh for this object
- const meshElements = obj.getElementsByTagName('mesh');
- if (meshElements.length === 0) continue;
+ // Store all components for later navigation
+ this.threeMFComponents = objectsToRender;
+ this.threeMFComponentIndex = 0;
+ this.threeMFZip = zip;
+ this.threeMFParser = parser;
- const mesh = meshElements[0];
-
- // Extract vertices for this mesh
- const vertices = [];
- const vertexElements = mesh.getElementsByTagName('vertex');
- for (let i = 0; i < vertexElements.length; i++) {
- const v = vertexElements[i];
- vertices.push(
- parseFloat(v.getAttribute('x')) || 0,
- parseFloat(v.getAttribute('y')) || 0,
- parseFloat(v.getAttribute('z')) || 0
- );
- }
-
- // Extract triangles for this mesh
- const indices = [];
- const triangleElements = mesh.getElementsByTagName('triangle');
- for (let i = 0; i < triangleElements.length; i++) {
- const t = triangleElements[i];
- indices.push(
- parseInt(t.getAttribute('v1')) || 0,
- parseInt(t.getAttribute('v2')) || 0,
- parseInt(t.getAttribute('v3')) || 0
- );
- }
-
- // Create geometry for this object
- if (vertices.length > 0 && indices.length > 0) {
- const geometry = new THREE.BufferGeometry();
- geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
- geometry.setIndex(indices);
- geometry.computeVertexNormals();
-
- // Create material
- const material = new THREE.MeshPhongMaterial({
- color: 0x00ae42,
- shininess: 30,
- flatShading: false
- });
-
- // Create mesh
- const meshObj = new THREE.Mesh(geometry, material);
-
- // Apply transform if present
- if (transform) {
- // Parse transform matrix (format: m00 m01 m02 m10 m11 m12 m20 m21 m22 m30 m31 m32)
- const values = transform.split(' ').map(v => parseFloat(v));
- if (values.length === 12) {
- const matrix = new THREE.Matrix4();
- matrix.set(
- values[0], values[3], values[6], values[9],
- values[1], values[4], values[7], values[10],
- values[2], values[5], values[8], values[11],
- 0, 0, 0, 1
- );
- meshObj.applyMatrix4(matrix);
- }
- }
-
- group.add(meshObj);
- }
- }
-
- // Add group to scene
- this.model = group;
- this.scene.add(group);
-
- this.centerAndScaleModel();
+ // Render the first component
+ this.render3MFComponent(0);
resolve();
} catch (error) {
console.error('Error loading 3MF:', error);
@@ -470,6 +378,121 @@ class ModelViewer {
});
}
+ render3MFComponent(componentIndex) {
+ if (!this.threeMFComponents || componentIndex >= this.threeMFComponents.length) {
+ return;
+ }
+
+ this.threeMFComponentIndex = componentIndex;
+ const { object: obj, transform, componentName } = this.threeMFComponents[componentIndex];
+
+ // Remove previous model group
+ if (this.model) {
+ this.scene.remove(this.model);
+ }
+
+ // Create a new group for this component
+ const group = new THREE.Group();
+
+ // Get the mesh for this object
+ const meshElements = obj.getElementsByTagName('mesh');
+ if (meshElements.length === 0) {
+ this.model = group;
+ return;
+ }
+
+ const mesh = meshElements[0];
+
+ // Extract vertices for this mesh
+ const vertices = [];
+ const vertexElements = mesh.getElementsByTagName('vertex');
+ for (let i = 0; i < vertexElements.length; i++) {
+ const v = vertexElements[i];
+ vertices.push(
+ parseFloat(v.getAttribute('x')) || 0,
+ parseFloat(v.getAttribute('y')) || 0,
+ parseFloat(v.getAttribute('z')) || 0
+ );
+ }
+
+ // Extract triangles for this mesh
+ const indices = [];
+ const triangleElements = mesh.getElementsByTagName('triangle');
+ for (let i = 0; i < triangleElements.length; i++) {
+ const t = triangleElements[i];
+ indices.push(
+ parseInt(t.getAttribute('v1')) || 0,
+ parseInt(t.getAttribute('v2')) || 0,
+ parseInt(t.getAttribute('v3')) || 0
+ );
+ }
+
+ // Create geometry for this component
+ if (vertices.length > 0 && indices.length > 0) {
+ const geometry = new THREE.BufferGeometry();
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
+ geometry.setIndex(indices);
+ geometry.computeVertexNormals();
+
+ // Create material
+ const material = new THREE.MeshPhongMaterial({
+ color: 0x00ae42,
+ shininess: 30,
+ flatShading: false
+ });
+
+ // Create mesh
+ const meshObj = new THREE.Mesh(geometry, material);
+
+ // Apply transform if present
+ if (transform) {
+ // Parse transform matrix (format: m00 m01 m02 m10 m11 m12 m20 m21 m22 m30 m31 m32)
+ const values = transform.split(' ').map(v => parseFloat(v));
+ if (values.length === 12) {
+ const matrix = new THREE.Matrix4();
+ matrix.set(
+ values[0], values[3], values[6], values[9],
+ values[1], values[4], values[7], values[10],
+ values[2], values[5], values[8], values[11],
+ 0, 0, 0, 1
+ );
+ meshObj.applyMatrix4(matrix);
+ }
+ }
+
+ group.add(meshObj);
+ }
+
+ // Add group to scene
+ this.model = group;
+ this.scene.add(group);
+
+ this.centerAndScaleModel();
+
+ // Store the component name for UI display
+ this.currentComponent3MFName = componentName;
+ }
+
+ next3MFComponent() {
+ if (!this.threeMFComponents) return;
+ const nextIndex = (this.threeMFComponentIndex + 1) % this.threeMFComponents.length;
+ this.render3MFComponent(nextIndex);
+ }
+
+ previous3MFComponent() {
+ if (!this.threeMFComponents) return;
+ const prevIndex = (this.threeMFComponentIndex - 1 + this.threeMFComponents.length) % this.threeMFComponents.length;
+ this.render3MFComponent(prevIndex);
+ }
+
+ get3MFComponentCount() {
+ return this.threeMFComponents ? this.threeMFComponents.length : 0;
+ }
+
+ get3MFCurrentComponentIndex() {
+ return this.threeMFComponentIndex;
+ }
+
async loadJSZip() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
@@ -687,12 +710,67 @@ async function loadViewerFile(index) {
// Load the model
await viewer.loadModel(modelUrl, file.type);
+
+ // If it's a 3MF file with multiple components, show component navigation
+ if (file.type === '.3mf' && viewer.get3MFComponentCount && viewer.get3MFComponentCount() > 1) {
+ const componentCount = viewer.get3MFComponentCount();
+ let componentNav = document.getElementById('componentNavigation');
+
+ if (!componentNav) {
+ // Create component navigation area
+ const navContainer = document.querySelector('[id="currentFileName"]').parentElement;
+ componentNav = document.createElement('div');
+ componentNav.id = 'componentNavigation';
+ componentNav.style.cssText = 'margin-top: 0.5rem; display: flex; gap: 0.5rem; align-items: center;';
+ navContainer.appendChild(componentNav);
+ }
+
+ componentNav.innerHTML = `
+
+
+
+ `;
+
+ updateComponentCounter();
+ } else {
+ // Hide component navigation if not a 3MF with components
+ const componentNav = document.getElementById('componentNavigation');
+ if (componentNav) {
+ componentNav.style.display = 'none';
+ }
+ }
} catch (error) {
console.error('Error loading file:', error);
showNotification('Failed to load file: ' + error.message, 'error');
}
}
+// Update the component counter display
+function updateComponentCounter() {
+ const counterEl = document.getElementById('componentCounter');
+ if (counterEl && viewer && viewer.get3MFComponentCount) {
+ const componentCount = viewer.get3MFComponentCount();
+ const currentIndex = viewer.get3MFCurrentComponentIndex();
+ counterEl.textContent = `Component ${currentIndex + 1} / ${componentCount}`;
+ }
+}
+
+// Navigate to next/previous 3MF component
+function cycleViewerComponent(direction) {
+ if (viewer && viewer.next3MFComponent && viewer.previous3MFComponent) {
+ if (direction > 0) {
+ viewer.next3MFComponent();
+ } else {
+ viewer.previous3MFComponent();
+ }
+ updateComponentCounter();
+ }
+}
+
// Cycle through files
async function cycleViewerFile(direction) {
const newIndex = currentViewerState.currentFileIndex + direction;