This commit is contained in:
2026-04-13 16:32:30 +08:00
parent dad17dbadd
commit 1de35f21d7
14 changed files with 1081 additions and 63 deletions

View File

@@ -16,7 +16,7 @@
<!-- 3D Area -->
<div class="col-md-9 h-100 position-relative">
<div id="plater-container" class="w-100 h-100 rounded shadow-sm border border-secondary" style="overflow: hidden; background: #f8f9fa;"></div>
<div id="plater-container" class="w-100 h-100 rounded shadow-sm border border-secondary" style="overflow: hidden; background: #f8f9fa; position: relative;"></div>
<!-- Parameterized Scale Input Box -->
<div id="scale-panel" class="position-absolute top-50 start-0 translate-middle-y ms-5 ps-4 d-none" style="z-index: 10; pointer-events: none;">
@@ -174,6 +174,13 @@ let offsetX = {{ offset_x|default(0) }};
let offsetY = {{ offset_y|default(0) }};
let loadedModels = [];
let activeModel = null;
let selectedModels = [];
let selectionBoxDiv = document.createElement('div');
selectionBoxDiv.id = 'selection-box';
selectionBoxDiv.style.cssText = 'position: absolute; border: 1px dashed #007bff; background: rgba(0, 123, 255, 0.1); pointer-events: none; display: none; z-index: 100;';
document.getElementById('plater-container').appendChild(selectionBoxDiv);
let dragStartPoint = null;
let isDraggingBox = false;
const initialAddId = new URLSearchParams(window.location.search).get('add');
initPlater();
@@ -271,12 +278,24 @@ function initPlater() {
});
transformControl.addEventListener('dragging-changed', function (event) {
orbit.enabled = !event.value;
if (!event.value && activeModel) {
scene.attach(activeModel);
transformProxy.position.copy(activeModel.getWorldPosition(new THREE.Vector3()));
if (!event.value && selectedModels.length > 0) {
let center = new THREE.Vector3();
selectedModels.forEach(m => {
scene.attach(m);
m.geometry.computeBoundingBox();
let box = m.geometry.boundingBox.clone();
box.applyMatrix4(m.matrixWorld);
center.add(box.getCenter(new THREE.Vector3()));
});
center.divideScalar(selectedModels.length);
transformProxy.position.copy(center);
transformProxy.rotation.set(0, 0, 0);
transformProxy.scale.set(1, 1, 1);
transformProxy.attach(activeModel);
selectedModels.forEach(m => {
transformProxy.attach(m);
});
}
});
scene.add(transformControl);
@@ -306,7 +325,7 @@ function setTransformMode(mode) {
document.getElementById('btn-scale').className = mode === 'scale' ? 'btn btn-primary btn-sm rounded' : 'btn btn-outline-secondary btn-sm rounded';
document.getElementById('btn-layflat').className = 'btn btn-outline-info btn-sm rounded';
if (mode === 'scale' && activeModel) {
if (mode === 'scale' && selectedModels && selectedModels.length > 0) {
document.getElementById('scale-panel').classList.remove('d-none');
updateScalePanel();
} else {
@@ -324,47 +343,56 @@ function setTransformMode(mode) {
}
function updateScalePanel() {
if (!activeModel) return;
const v = activeModel.getWorldScale(new THREE.Vector3());
document.getElementById('scale-x').value = v.x.toFixed(3);
document.getElementById('scale-y').value = v.y.toFixed(3);
document.getElementById('scale-z').value = v.z.toFixed(3);
if (selectedModels.length === 0) return;
// Check if scales match
let firstScale = selectedModels[0].getWorldScale(new THREE.Vector3());
let allXMatch = true, allYMatch = true, allZMatch = true;
for (let i = 1; i < selectedModels.length; i++) {
let v = selectedModels[i].getWorldScale(new THREE.Vector3());
if (Math.abs(v.x - firstScale.x) > 0.001) allXMatch = false;
if (Math.abs(v.y - firstScale.y) > 0.001) allYMatch = false;
if (Math.abs(v.z - firstScale.z) > 0.001) allZMatch = false;
}
document.getElementById('scale-x').value = allXMatch ? firstScale.x.toFixed(3) : '';
document.getElementById('scale-y').value = allYMatch ? firstScale.y.toFixed(3) : '';
document.getElementById('scale-z').value = allZMatch ? firstScale.z.toFixed(3) : '';
}
function applyScaleInput(axis) {
if (!activeModel) return;
let val = parseFloat(document.getElementById('scale-' + axis).value);
if (selectedModels.length === 0) return;
let valStr = document.getElementById('scale-' + axis).value;
if (valStr === '') return;
let val = parseFloat(valStr);
if (isNaN(val) || val <= 0.001) val = 1.0;
scene.attach(activeModel); // temporarily detach to operate purely on local=world scale
const isUniform = document.getElementById('scale-uniform').checked;
if (isUniform) {
// Find previous scale
const prev = activeModel.scale[axis];
const ratio = val / prev;
activeModel.scale.x *= ratio;
activeModel.scale.y *= ratio;
activeModel.scale.z *= ratio;
} else {
activeModel.scale[axis] = val;
}
selectedModels.forEach(m => {
scene.attach(m);
if (isUniform) {
const prev = m.scale[axis];
const ratio = val / prev;
m.scale.x *= ratio;
m.scale.y *= ratio;
m.scale.z *= ratio;
} else {
m.scale[axis] = val;
}
m.updateMatrixWorld(true);
});
activeModel.updateMatrixWorld(true);
// re-attach proxy pivot logic without modifying the actual spatial scale
transformProxy.scale.set(1, 1, 1);
transformProxy.attach(activeModel);
selectedModels.forEach(m => {
transformProxy.attach(m);
});
updateScalePanel();
}
function removeActiveModel() {
if (activeModel) {
removeModel(activeModel);
}
}
function onKeyDown(event) {
switch (event.key.toLowerCase()) {
@@ -386,6 +414,15 @@ function onPointerDown(event) {
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
if (event.ctrlKey || event.metaKey) {
dragStartPoint = { x: event.clientX, y: event.clientY };
isDraggingBox = false;
orbit.enabled = false;
document.addEventListener('pointermove', onPointerMoveBox);
document.addEventListener('pointerup', onPointerUpBox);
}
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(loadedModels, true);
@@ -424,30 +461,146 @@ function onPointerDown(event) {
// Exit lay flat mode and reset to translate
setTransformMode('translate');
selectModel(obj);
selectModels([obj]);
}
}
return;
}
if (intersects.length > 0) {
selectModel(intersects[0].object);
if (event.ctrlKey || event.metaKey) {
toggleModelSelection(intersects[0].object);
} else {
selectModels([intersects[0].object]);
}
} else {
selectModel(null);
if (!event.ctrlKey && !event.metaKey) {
selectModels([]);
}
}
}
function selectModel(model) {
if (activeModel && activeModel !== model) {
scene.attach(activeModel);
function onPointerMoveBox(event) {
if (!dragStartPoint) return;
const dx = Math.abs(event.clientX - dragStartPoint.x);
const dy = Math.abs(event.clientY - dragStartPoint.y);
if (dx > 5 || dy > 5) {
isDraggingBox = true;
const container = document.getElementById('plater-container');
const rect = container.getBoundingClientRect();
const startX = dragStartPoint.x - rect.left;
const startY = dragStartPoint.y - rect.top;
const currentX = event.clientX - rect.left;
const currentY = event.clientY - rect.top;
selectionBoxDiv.style.display = 'block';
selectionBoxDiv.style.left = Math.min(currentX, startX) + 'px';
selectionBoxDiv.style.top = Math.min(currentY, startY) + 'px';
selectionBoxDiv.style.width = Math.abs(currentX - startX) + 'px';
selectionBoxDiv.style.height = Math.abs(currentY - startY) + 'px';
}
activeModel = model;
if (model) {
scene.attach(model);
transformProxy.position.copy(model.getWorldPosition(new THREE.Vector3()));
}
function onPointerUpBox(event) {
document.removeEventListener('pointermove', onPointerMoveBox);
document.removeEventListener('pointerup', onPointerUpBox);
orbit.enabled = true;
if (isDraggingBox) {
selectionBoxDiv.style.display = 'none';
const rect = renderer.domElement.getBoundingClientRect();
const minX = Math.min(dragStartPoint.x, event.clientX) - rect.left;
const maxX = Math.max(dragStartPoint.x, event.clientX) - rect.left;
const minY = Math.min(dragStartPoint.y, event.clientY) - rect.top;
const maxY = Math.max(dragStartPoint.y, event.clientY) - rect.top;
let newSelection = [...selectedModels];
loadedModels.forEach(m => {
m.geometry.computeBoundingBox();
let box = m.geometry.boundingBox.clone();
box.applyMatrix4(m.matrixWorld);
// Project 8 corners
const corners = [
new THREE.Vector3(box.min.x, box.min.y, box.min.z),
new THREE.Vector3(box.max.x, box.min.y, box.min.z),
new THREE.Vector3(box.min.x, box.max.y, box.min.z),
new THREE.Vector3(box.max.x, box.max.y, box.min.z),
new THREE.Vector3(box.min.x, box.min.y, box.max.z),
new THREE.Vector3(box.max.x, box.min.y, box.max.z),
new THREE.Vector3(box.min.x, box.max.y, box.max.z),
new THREE.Vector3(box.max.x, box.max.y, box.max.z)
];
let inside = false;
corners.forEach(v => {
v.project(camera);
let sx = (v.x * .5 + .5) * rect.width;
let sy = (v.y * -.5 + .5) * rect.height;
if (sx >= minX && sx <= maxX && sy >= minY && sy <= maxY) {
inside = true;
}
});
if (inside && !newSelection.includes(m)) {
newSelection.push(m);
}
});
selectModels(newSelection);
} else if (dragStartPoint && !isDraggingBox) {
// Just a ctrl+click that missed logic is handled by raycaster above, but we have to ensure no double toggle
}
dragStartPoint = null;
isDraggingBox = false;
}
function toggleModelSelection(model) {
let newSel = [...selectedModels];
if (newSel.includes(model)) {
newSel = newSel.filter(m => m !== model);
} else {
newSel.push(model);
}
selectModels(newSel);
}
function selectModels(models) {
selectedModels.forEach(m => {
scene.attach(m);
m.material.color.setHex(0xcccccc);
});
selectedModels = models;
activeModel = selectedModels.length > 0 ? selectedModels[selectedModels.length - 1] : null;
if (selectedModels.length > 0) {
// compute joint center
let center = new THREE.Vector3();
let count = 0;
selectedModels.forEach(m => {
m.material.color.setHex(0x0d6efd);
scene.attach(m); // ensure in world
m.geometry.computeBoundingBox();
let box = m.geometry.boundingBox.clone();
box.applyMatrix4(m.matrixWorld);
center.add(box.getCenter(new THREE.Vector3()));
count++;
});
center.divideScalar(count);
transformProxy.position.copy(center);
transformProxy.rotation.set(0, 0, 0);
transformProxy.scale.set(1, 1, 1);
transformProxy.attach(model);
selectedModels.forEach(m => {
transformProxy.attach(m);
});
transformControl.attach(transformProxy);
if(transformControl.getMode() === 'scale') {
document.getElementById('scale-panel').classList.remove('d-none');
@@ -459,14 +612,32 @@ function selectModel(model) {
}
}
function removeModel(model) {
if (activeModel === model) {
transformControl.detach();
scene.attach(model);
activeModel = null;
// Keep a backward compatible selectModel definition for single cases
function selectModel(model) {
if (model) {
selectModels([model]);
} else {
selectModels([]);
}
}
function removeModel(model) {
if (selectedModels.includes(model)) {
selectedModels = selectedModels.filter(m => m !== model);
if (selectedModels.length === 0) transformControl.detach();
}
scene.attach(model);
scene.remove(model);
loadedModels = loadedModels.filter(m => m !== model);
activeModel = selectedModels.length > 0 ? selectedModels[0] : null;
}
function removeActiveModel() {
if (selectedModels.length > 0) {
[...selectedModels].forEach(m => removeModel(m));
selectModels([]);
}
}
function clearPlate() {
@@ -476,12 +647,29 @@ function clearPlate() {
scene.remove(m);
});
loadedModels = [];
selectedModels = [];
activeModel = null;
}
function addModelToPlate(btnElement, fileId, url, name, status) {
let matrixData = btnElement ? btnElement.getAttribute('data-matrix') : null;
if (matrixData) {
try {
let data = JSON.parse(matrixData);
if (data.settings) {
if (data.settings.infill) document.getElementById('infill-density').value = data.settings.infill;
if (data.settings.support) {
let supportSelect = document.getElementById('support-type');
supportSelect.value = data.settings.support;
supportSelect.dispatchEvent(new Event('change'));
}
if (data.settings.support_pattern) document.getElementById('support-pattern').value = data.settings.support_pattern;
if (data.settings.quality) document.getElementById('quality').value = data.settings.quality;
}
} catch (e) {}
}
if (matrixData && matrixData.includes('"is_composite"')) {
try {
let comp = JSON.parse(matrixData);
@@ -538,7 +726,7 @@ function loadSTL(fileId, url, name, status, matrixData, callback) {
const minZ = geometry.boundingBox.min.z;
geometry.translate(-center.x, -center.y, -minZ);
const material = new THREE.MeshPhongMaterial({ color: 0x0d6efd, specular: 0x111111, shininess: 200 });
const material = new THREE.MeshPhongMaterial({ color: 0xcccccc, specular: 0x111111, shininess: 200 });
const mesh = new THREE.Mesh(geometry, material);
mesh.userData = {
fileId: fileId,
@@ -547,9 +735,15 @@ function loadSTL(fileId, url, name, status, matrixData, callback) {
geomTrans: new THREE.Matrix4().makeTranslation(-center.x, -center.y, -minZ)
};
if (matrixData && matrixData.trim() !== '' && matrixData !== 'None' && !matrixData.includes('"is_composite"')) {
if (matrixData && matrixData.trim() !== '' && matrixData !== 'None') {
try {
let mArray = JSON.parse(matrixData);
// Skip if it actually is a composite (handled by addModelToPlate)
if (mArray && mArray.is_composite === true) return;
if (mArray && !Array.isArray(mArray) && mArray.matrix) {
mArray = mArray.matrix;
}
let savedMatrix = new THREE.Matrix4().fromArray(mArray);
savedMatrix.decompose(mesh.position, mesh.quaternion, mesh.scale);
} catch (e) {
@@ -622,7 +816,7 @@ function animate() {
}
function mergeAndSlice() {
selectModel(null); // Detach any active model to bake transformProxy world coordinates into its local matrix properties
selectModels([]); // Detach any active model to bake transformProxy world coordinates into its local matrix properties
if (loadedModels.length === 0) {
alert("{{ _('Please add at least one model to the build plate.') }}");
@@ -728,8 +922,13 @@ document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('add-model-btn-' + addId);
if (btn) {
let matrixData = btn.getAttribute('data-matrix');
if (matrixData && matrixData.includes('"is_composite"')) {
window.isCompositeEdit = true;
if (matrixData) {
try {
let d = JSON.parse(matrixData);
if (d && d.is_composite === true) {
window.isCompositeEdit = true;
}
} catch(e) {}
}
btn.click();
}