fix: viewer 3MF Bambu/generic, auto-liaison gcode.3mf->3mf, import gcode.3mf NC picker
Deploy via Portainer / deploy (push) Successful in 0s
Deploy via Portainer / deploy (push) Successful in 0s
This commit is contained in:
+131
-18
@@ -277,6 +277,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<!-- JSZip pour parser les 3MF Bambu Studio -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
@@ -360,14 +362,8 @@ function loadFromUrl(url, ext) {
|
||||
errDiv.style.display = 'none';
|
||||
if (modelMesh) { scene.remove(modelMesh); modelMesh = null; }
|
||||
|
||||
const mat = makeMat();
|
||||
const onError = (e) => {
|
||||
loader.style.display = 'none';
|
||||
errDiv.style.display = '';
|
||||
errMsg.textContent = 'Erreur chargement : ' + (e.message || e);
|
||||
};
|
||||
|
||||
if (ext === 'stl') {
|
||||
const mat = makeMat();
|
||||
new STLLoader().load(url, geo => {
|
||||
geo.computeVertexNormals();
|
||||
modelMesh = new THREE.Mesh(geo, mat);
|
||||
@@ -375,24 +371,141 @@ function loadFromUrl(url, ext) {
|
||||
scene.add(modelMesh);
|
||||
loader.style.display = 'none';
|
||||
fitCamera();
|
||||
}, undefined, onError);
|
||||
} else if (ext === '3mf') {
|
||||
new ThreeMFLoader().load(url, obj => {
|
||||
obj.traverse(child => {
|
||||
if (child.isMesh) child.material = mat.clone();
|
||||
});
|
||||
modelMesh = obj;
|
||||
scene.add(modelMesh);
|
||||
}, undefined, (e) => {
|
||||
loader.style.display = 'none';
|
||||
fitCamera();
|
||||
}, undefined, onError);
|
||||
errDiv.style.display = '';
|
||||
errMsg.textContent = 'Erreur STL : ' + (e.message || e);
|
||||
});
|
||||
|
||||
} else if (ext === '3mf') {
|
||||
// Parser Bambu custom : lit directement les 3D/Objects/*.model
|
||||
loadBambu3MF(url);
|
||||
|
||||
} else {
|
||||
loader.style.display = 'none';
|
||||
errDiv.style.display = '';
|
||||
errMsg.textContent = `Format .${ext} non supporté dans le viewer (STL et 3MF uniquement).`;
|
||||
errMsg.textContent = `Format .${ext} non supporté (STL et 3MF uniquement).`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBambu3MF(url) {
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const buffer = await resp.arrayBuffer();
|
||||
|
||||
const JSZip = window.JSZip;
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
const allPositions = [];
|
||||
const tmpVec = new THREE.Vector3();
|
||||
|
||||
// ── Stratégie A : fichiers externes 3D/Objects/*.model (projet Bambu) ──
|
||||
const objectFiles = Object.keys(zip.files)
|
||||
.filter(n => n.startsWith('3D/Objects/') && n.endsWith('.model'));
|
||||
|
||||
if (objectFiles.length) {
|
||||
// Lire les transforms depuis 3dmodel.model
|
||||
const transforms = {};
|
||||
const mainFile = zip.files['3D/3dmodel.model'];
|
||||
if (mainFile) {
|
||||
const mainXml = await mainFile.async('string');
|
||||
const doc = new DOMParser().parseFromString(mainXml, 'text/xml');
|
||||
doc.querySelectorAll('component').forEach(c => {
|
||||
const path = (c.getAttribute('p:path') || '').replace(/^\//, '');
|
||||
const objId = c.getAttribute('objectid');
|
||||
const tStr = c.getAttribute('transform');
|
||||
if (path && objId) transforms[path + '#' + objId] = tStr || null;
|
||||
});
|
||||
}
|
||||
|
||||
for (const objFile of objectFiles) {
|
||||
const xmlStr = await zip.files[objFile].async('string');
|
||||
const doc = new DOMParser().parseFromString(xmlStr, 'text/xml');
|
||||
for (const objEl of doc.querySelectorAll('object')) {
|
||||
const meshEl = objEl.querySelector('mesh');
|
||||
if (!meshEl) continue;
|
||||
const objId = objEl.getAttribute('id');
|
||||
const tStr = transforms[objFile + '#' + objId];
|
||||
let matrix = null;
|
||||
if (tStr) {
|
||||
const v = tStr.trim().split(/\s+/).map(Number);
|
||||
if (v.length === 12) {
|
||||
matrix = new THREE.Matrix4();
|
||||
matrix.set(v[0],v[3],v[6],v[9], v[1],v[4],v[7],v[10], v[2],v[5],v[8],v[11], 0,0,0,1);
|
||||
}
|
||||
}
|
||||
parseMeshEl(meshEl, matrix, tmpVec, allPositions);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stratégie B : géométrie inline dans 3dmodel.model (generic / standard 3MF) ──
|
||||
} else {
|
||||
const mainFile = zip.files['3D/3dmodel.model'];
|
||||
if (!mainFile) throw new Error('Aucun fichier 3D trouvé dans le 3MF');
|
||||
const mainXml = await mainFile.async('string');
|
||||
const doc = new DOMParser().parseFromString(mainXml, 'text/xml');
|
||||
|
||||
// Construire la map id→transform depuis <build><item>
|
||||
const buildTransforms = {};
|
||||
doc.querySelectorAll('build item').forEach(item => {
|
||||
const objId = item.getAttribute('objectid');
|
||||
const tStr = item.getAttribute('transform');
|
||||
buildTransforms[objId] = tStr || null;
|
||||
});
|
||||
|
||||
for (const objEl of doc.querySelectorAll('resources object')) {
|
||||
const meshEl = objEl.querySelector('mesh');
|
||||
if (!meshEl) continue;
|
||||
const objId = objEl.getAttribute('id');
|
||||
const tStr = buildTransforms[objId];
|
||||
let matrix = null;
|
||||
if (tStr) {
|
||||
const v = tStr.trim().split(/\s+/).map(Number);
|
||||
if (v.length === 12) {
|
||||
matrix = new THREE.Matrix4();
|
||||
matrix.set(v[0],v[3],v[6],v[9], v[1],v[4],v[7],v[10], v[2],v[5],v[8],v[11], 0,0,0,1);
|
||||
}
|
||||
}
|
||||
parseMeshEl(meshEl, matrix, tmpVec, allPositions);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allPositions.length) throw new Error('Aucune géométrie dans le 3MF');
|
||||
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(allPositions, 3));
|
||||
geo.computeVertexNormals();
|
||||
modelMesh = new THREE.Mesh(geo, makeMat());
|
||||
modelMesh.rotation.x = -Math.PI / 2;
|
||||
scene.add(modelMesh);
|
||||
loader.style.display = 'none';
|
||||
fitCamera();
|
||||
|
||||
} catch (e) {
|
||||
loader.style.display = 'none';
|
||||
errDiv.style.display = '';
|
||||
errMsg.textContent = 'Erreur 3MF : ' + (e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
function parseMeshEl(meshEl, matrix, tmpVec, allPositions) {
|
||||
const verts = Array.from(meshEl.querySelectorAll('vertices vertex'))
|
||||
.map(v => [+v.getAttribute('x'), +v.getAttribute('y'), +v.getAttribute('z')]);
|
||||
for (const tri of meshEl.querySelectorAll('triangles triangle')) {
|
||||
for (const attr of ['v1', 'v2', 'v3']) {
|
||||
const [x, y, z] = verts[+tri.getAttribute(attr)];
|
||||
if (matrix) {
|
||||
tmpVec.set(x, y, z).applyMatrix4(matrix);
|
||||
allPositions.push(tmpVec.x, tmpVec.y, tmpVec.z);
|
||||
} else {
|
||||
allPositions.push(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showUploadZone(ext) {
|
||||
uploadZone.style.display = '';
|
||||
canvasWrap.style.display = 'none';
|
||||
|
||||
Reference in New Issue
Block a user