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,11 +362,44 @@ 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;
|
||||||
|
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
|
// Get the mesh for this object
|
||||||
const meshElements = obj.getElementsByTagName('mesh');
|
const meshElements = obj.getElementsByTagName('mesh');
|
||||||
if (meshElements.length === 0) continue;
|
if (meshElements.length === 0) {
|
||||||
|
this.model = group;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const mesh = meshElements[0];
|
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) {
|
if (vertices.length > 0 && indices.length > 0) {
|
||||||
const geometry = new THREE.BufferGeometry();
|
const geometry = new THREE.BufferGeometry();
|
||||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
||||||
|
|
@ -455,19 +462,35 @@ class ModelViewer {
|
||||||
|
|
||||||
group.add(meshObj);
|
group.add(meshObj);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add group to scene
|
// Add group to scene
|
||||||
this.model = group;
|
this.model = group;
|
||||||
this.scene.add(group);
|
this.scene.add(group);
|
||||||
|
|
||||||
this.centerAndScaleModel();
|
this.centerAndScaleModel();
|
||||||
resolve();
|
|
||||||
} catch (error) {
|
// Store the component name for UI display
|
||||||
console.error('Error loading 3MF:', error);
|
this.currentComponent3MFName = componentName;
|
||||||
reject(error);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
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() {
|
||||||
|
|
@ -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