Add 3MF multi-component viewer with navigation controls
This commit is contained in:
parent
6be41cb4d3
commit
88f133bf6c
1 changed files with 191 additions and 113 deletions
|
|
@ -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 = `
|
||||
<button class="btn btn-sm btn-secondary" onclick="cycleViewerComponent(-1)" title="Previous component">
|
||||
<i class="fas fa-chevron-left"></i> Prev
|
||||
</button>
|
||||
<span id="componentCounter" style="color: var(--text-secondary); font-size: 0.9rem;"></span>
|
||||
<button class="btn btn-sm btn-secondary" onclick="cycleViewerComponent(1)" title="Next component">
|
||||
Next <i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue