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,11 +362,44 @@ class ModelViewer {
|
|||
throw new Error('No objects to render in 3MF file');
|
||||
}
|
||||
|
||||
// Process each object to render
|
||||
for (const { object: obj, transform } of objectsToRender) {
|
||||
// Store all components for later navigation
|
||||
this.threeMFComponents = objectsToRender;
|
||||
this.threeMFComponentIndex = 0;
|
||||
this.threeMFZip = zip;
|
||||
this.threeMFParser = parser;
|
||||
|
||||
// Render the first component
|
||||
this.render3MFComponent(0);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error('Error loading 3MF:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) continue;
|
||||
if (meshElements.length === 0) {
|
||||
this.model = group;
|
||||
return;
|
||||
}
|
||||
|
||||
const mesh = meshElements[0];
|
||||
|
||||
|
|
@ -420,7 +427,7 @@ class ModelViewer {
|
|||
);
|
||||
}
|
||||
|
||||
// Create geometry for this object
|
||||
// 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));
|
||||
|
|
@ -455,19 +462,35 @@ class ModelViewer {
|
|||
|
||||
group.add(meshObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Add group to scene
|
||||
this.model = group;
|
||||
this.scene.add(group);
|
||||
|
||||
this.centerAndScaleModel();
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error('Error loading 3MF:', error);
|
||||
reject(error);
|
||||
|
||||
// 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() {
|
||||
|
|
@ -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