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);
|
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
|
// Build a map of all objects by ID
|
||||||
const objectMap = new Map();
|
const objectMap = new Map();
|
||||||
const objectElements = xmlDoc.getElementsByTagName('object');
|
const objectElements = xmlDoc.getElementsByTagName('object');
|
||||||
|
|
@ -283,7 +280,7 @@ class ModelViewer {
|
||||||
const componentElements = obj.getElementsByTagName('component');
|
const componentElements = obj.getElementsByTagName('component');
|
||||||
|
|
||||||
if (componentElements.length > 0) {
|
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`);
|
console.log(`Object ${objectId} has ${componentElements.length} components`);
|
||||||
for (let j = 0; j < componentElements.length; j++) {
|
for (let j = 0; j < componentElements.length; j++) {
|
||||||
const comp = componentElements[j];
|
const comp = componentElements[j];
|
||||||
|
|
@ -293,7 +290,8 @@ class ModelViewer {
|
||||||
if (compObjectId && objectMap.has(compObjectId)) {
|
if (compObjectId && objectMap.has(compObjectId)) {
|
||||||
objectsToRender.push({
|
objectsToRender.push({
|
||||||
object: objectMap.get(compObjectId),
|
object: objectMap.get(compObjectId),
|
||||||
transform: compTransform
|
transform: compTransform,
|
||||||
|
componentName: `Component ${j + 1}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -301,18 +299,23 @@ class ModelViewer {
|
||||||
// Regular mesh object
|
// Regular mesh object
|
||||||
objectsToRender.push({
|
objectsToRender.push({
|
||||||
object: obj,
|
object: obj,
|
||||||
transform: transform
|
transform: transform,
|
||||||
|
componentName: `Model ${i + 1}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No build section, render all objects with meshes
|
// No build section, render each object separately
|
||||||
console.log('No build section found, rendering all objects with meshes');
|
console.log('No build section found, rendering all objects with meshes separately');
|
||||||
for (let i = 0; i < objectElements.length; i++) {
|
for (let i = 0; i < objectElements.length; i++) {
|
||||||
const obj = objectElements[i];
|
const obj = objectElements[i];
|
||||||
if (obj.getElementsByTagName('mesh').length > 0) {
|
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) {
|
if (objectsToRender.length === 0) {
|
||||||
console.log('No objects in main model, loading separate object files...');
|
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.)
|
// Load all separate object files (like object_10.model, etc.)
|
||||||
const objectFiles = modelFiles.filter(f =>
|
const objectFiles = modelFiles.filter(f =>
|
||||||
f.path.includes('/Objects/') && f.path.endsWith('.model')
|
f.path.includes('/Objects/') && f.path.endsWith('.model')
|
||||||
|
|
@ -347,10 +333,6 @@ class ModelViewer {
|
||||||
|
|
||||||
console.log(`Found ${objectFiles.length} separate object files`);
|
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++) {
|
for (let idx = 0; idx < objectFiles.length; idx++) {
|
||||||
const objFile = objectFiles[idx];
|
const objFile = objectFiles[idx];
|
||||||
try {
|
try {
|
||||||
|
|
@ -361,18 +343,10 @@ class ModelViewer {
|
||||||
for (let i = 0; i < objElements.length; i++) {
|
for (let i = 0; i < objElements.length; i++) {
|
||||||
const obj = objElements[i];
|
const obj = objElements[i];
|
||||||
if (obj.getElementsByTagName('mesh').length > 0) {
|
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({
|
objectsToRender.push({
|
||||||
object: obj,
|
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');
|
throw new Error('No objects to render in 3MF file');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each object to render
|
// Store all components for later navigation
|
||||||
for (const { object: obj, transform } of objectsToRender) {
|
this.threeMFComponents = objectsToRender;
|
||||||
// Get the mesh for this object
|
this.threeMFComponentIndex = 0;
|
||||||
const meshElements = obj.getElementsByTagName('mesh');
|
this.threeMFZip = zip;
|
||||||
if (meshElements.length === 0) continue;
|
this.threeMFParser = parser;
|
||||||
|
|
||||||
const mesh = meshElements[0];
|
// Render the first component
|
||||||
|
this.render3MFComponent(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();
|
|
||||||
resolve();
|
resolve();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading 3MF:', 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() {
|
async loadJSZip() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
|
|
@ -687,12 +710,67 @@ async function loadViewerFile(index) {
|
||||||
|
|
||||||
// Load the model
|
// Load the model
|
||||||
await viewer.loadModel(modelUrl, file.type);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading file:', error);
|
console.error('Error loading file:', error);
|
||||||
showNotification('Failed to load file: ' + error.message, '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
|
// Cycle through files
|
||||||
async function cycleViewerFile(direction) {
|
async function cycleViewerFile(direction) {
|
||||||
const newIndex = currentViewerState.currentFileIndex + direction;
|
const newIndex = currentViewerState.currentFileIndex + direction;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue