tmp
This commit is contained in:
@@ -27,9 +27,17 @@
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top shadow-sm">
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid position-relative d-flex justify-content-between align-items-center">
|
||||
<a class="navbar-brand fw-bold" href="{{ url_for('main.index') }}"><i class="bi bi-printer me-2"></i>AIO 3D Slicer</a>
|
||||
<div class="d-flex text-light align-items-center">
|
||||
|
||||
<div class="d-none d-md-flex mx-auto" style="position: absolute; left: 50%; transform: translateX(-50%);">
|
||||
<div class="btn-group border border-secondary shadow-sm rounded-pill p-1 bg-dark" role="group" style="background-color: #1a1e21 !important;">
|
||||
<a href="{{ url_for('main.files') }}" class="btn btn-sm rounded-pill {% if not request.blueprint == 'printer' %}btn-primary active fw-bold text-white px-4{% else %}btn-transparent text-secondary border-0 px-3{% endif %}" style="transition: all 0.2s;"><i class="bi bi-layers me-1"></i>{{ _('Slicer') }}</a>
|
||||
<a href="{{ url_for('printer.status') }}" class="btn btn-sm rounded-pill {% if request.blueprint == 'printer' %}btn-primary active fw-bold text-white px-4{% else %}btn-transparent text-secondary border-0 px-3{% endif %}" style="transition: all 0.2s;"><i class="bi bi-printer-fill me-1"></i>{{ _('Printer') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex text-light align-items-center ms-auto">
|
||||
<div class="dropdown me-3">
|
||||
<button class="btn btn-sm btn-outline-light dropdown-toggle" type="button" id="langDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-globe me-1"></i>{{ _('Language') }}
|
||||
@@ -57,6 +65,48 @@
|
||||
<div class="row">
|
||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-white sidebar collapse border-end">
|
||||
<div class="sidebar-sticky pt-3 px-2">
|
||||
|
||||
{% if request.blueprint == 'printer' %}
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mb-2 text-muted fw-bold text-uppercase" style="font-size: 0.75rem;">
|
||||
<span><i class="bi bi-list-task me-1"></i>{{ _('General Operations') }}</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column nav-pills gap-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark {% if request.endpoint == 'printer.status' %}active text-white shadow-sm{% endif %}" href="{{ url_for('printer.status') }}">
|
||||
<i class="bi bi-activity me-2"></i>{{ _('Printer Status') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark {% if request.endpoint == 'printer.prepare' %}active text-white shadow-sm{% endif %}" href="{{ url_for('printer.prepare') }}">
|
||||
<i class="bi bi-file-earmark-plus me-2"></i>{{ _('Prepare Print') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item mb-1">
|
||||
<a class="nav-link text-dark {% if request.endpoint == 'printer.control' %}active text-white shadow-sm{% endif %}" href="{{ url_for('printer.control') }}">
|
||||
<i class="bi bi-arrows-move me-2"></i>{{ _('Control') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-2 text-muted fw-bold text-uppercase" style="font-size: 0.75rem;">
|
||||
<span><i class="bi bi-shield-lock me-1"></i>{{ _('Admin / OctoPrint') }}</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column nav-pills gap-1 mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark {% if request.endpoint == 'printer.octo_config' %}active text-white shadow-sm{% endif %}" href="{{ url_for('printer.octo_config') }}">
|
||||
<i class="bi bi-plug me-2"></i>{{ _('System Config') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark {% if request.endpoint == 'printer.octo_embed' %}active text-white shadow-sm{% endif %}" href="{{ url_for('printer.octo_embed') }}">
|
||||
<i class="bi bi-window-sidebar me-2"></i>{{ _('OctoPrint Panel') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<ul class="nav flex-column nav-pills gap-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark {% if request.endpoint == 'main.index' %}active text-white shadow-sm{% endif %}" href="{{ url_for('main.index') }}">
|
||||
@@ -92,6 +142,8 @@
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="mb-1 legend-item user-select-none" data-type="SUPPORT" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #57b357;"></span>{{ _('Support') }}</div>
|
||||
<div class="mb-1 legend-item user-select-none" data-type="SKIRT" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #00ffff;"></span>{{ _('Skirt') }}</div>
|
||||
<div class="mb-1 legend-item user-select-none" data-type="SUPPORT-INTERFACE" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #2b6b2b;"></span>{{ _('Support Interface') }}</div>
|
||||
<div class="mb-1 legend-item user-select-none" data-type="TRAVEL" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #405060;"></span>{{ _('Travel (Move)') }}</div>
|
||||
<div class="mb-1 legend-item user-select-none" data-type="TRAVEL" style="cursor: pointer; transition: opacity 0.2s; opacity: 0.4;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #405060;"></span>{{ _('Travel (Move)') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Slider (Intra-Layer Progress) -->
|
||||
@@ -100,7 +100,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
uShowSupport: { value: 1.0 },
|
||||
uShowSkirt: { value: 1.0 },
|
||||
uShowSupportInterface: { value: 1.0 },
|
||||
uShowTravel: { value: 1.0 },
|
||||
uShowTravel: { value: 0.0 },
|
||||
uShowDefault: { value: 1.0 }
|
||||
},
|
||||
vertexShader: `
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
87
app/templates/printer/control.html
Normal file
87
app/templates/printer/control.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2"><i class="bi bi-arrows-move text-primary me-2"></i>{{ _('Printer Control') }}</h1>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row row-cols-1 row-cols-lg-2 g-4">
|
||||
<!-- Webcam Stream -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-dark text-light fw-bold rounded-top">
|
||||
<i class="bi bi-camera-video me-1"></i>{{ _('Live Webcam') }}
|
||||
</div>
|
||||
<div class="card-body p-0 ratio ratio-16x9">
|
||||
<img src="{{ webcam_url }}" alt="{{ _('Loading webcam stream...') }}" class="w-100 h-100 object-fit-cover">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Motion Control -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light fw-bold text-secondary">
|
||||
<i class="bi bi-dpad me-1"></i>{{ _('Basic Control') }}
|
||||
</div>
|
||||
<div class="card-body text-center d-flex flex-column justify-content-center align-items-center">
|
||||
<!-- Home button -->
|
||||
<button class="btn btn-lg btn-primary rounded-circle mb-4 shadow" style="width: 80px; height: 80px;" onclick="sendCommand('home')" title="{{ _('Home All Axes') }}">
|
||||
<i class="bi bi-house-door fs-2"></i>
|
||||
</button>
|
||||
<div class="text-muted mb-4">{{ _('Home All Axes') }} (G28)</div>
|
||||
|
||||
<!-- Quick macros -->
|
||||
<div class="d-flex gap-3 justify-content-center flex-wrap w-100">
|
||||
<button class="btn btn-outline-danger flex-fill shadow-sm py-3" onclick="sendCommand('pause')" title="{{ _('Pause/Resume Print') }}">
|
||||
<i class="bi bi-pause-circle fs-4 d-block mb-1"></i>{{ _('Pause') }}
|
||||
</button>
|
||||
<button class="btn btn-outline-warning flex-fill shadow-sm py-3" onclick="sendCommand('cancel')" title="{{ _('Cancel Print') }}">
|
||||
<i class="bi bi-stop-circle fs-4 d-block mb-1"></i>{{ _('Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function sendCommand(cmdName) {
|
||||
if ((cmdName === 'cancel' || cmdName === 'home') && !confirm("Are you sure you want to perform this action?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('{{ url_for("printer.api_command") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({command: cmdName})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
flashMessage("success", "Command " + cmdName + " sent.");
|
||||
} else {
|
||||
flashMessage("danger", "Control failed: " + data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
flashMessage("danger", "Network Error: " + err);
|
||||
});
|
||||
}
|
||||
function flashMessage(type, text) {
|
||||
const container = document.querySelector('.toast-container');
|
||||
if(!container) return alert(text);
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center border-0 bg-${type} text-white show`;
|
||||
toast.innerHTML = `<div class="d-flex"><div class="toast-body fw-medium">${text}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 4000);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
42
app/templates/printer/octo_config.html
Normal file
42
app/templates/printer/octo_config.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2"><i class="bi bi-gear-wide-connected text-primary me-2"></i>{{ _('OctoPrint Configuration') }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-light fw-bold text-secondary border-bottom-0">
|
||||
<i class="bi bi-link-45deg me-1"></i>{{ _('Connection Settings') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('printer.octo_config') }}">
|
||||
<div class="mb-3">
|
||||
<label for="octoprint_url" class="form-label fw-bold">{{ _('OctoPrint Base URL') }}</label>
|
||||
<div class="input-group mb-3 shadow-sm">
|
||||
<span class="input-group-text bg-white text-muted" id="url-addon"><i class="bi bi-globe"></i></span>
|
||||
<input type="url" class="form-control" id="octoprint_url" name="octoprint_url" aria-describedby="url-addon" placeholder="e.g. http://octopi.local" value="{{ configs.get('octoprint_url', '') }}" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
{{ _('The local IP address or hostname of your OctoPrint server.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="octoprint_apikey" class="form-label fw-bold">{{ _('API Key / Application Key') }}</label>
|
||||
<div class="input-group shadow-sm">
|
||||
<span class="input-group-text bg-white text-muted" id="key-addon"><i class="bi bi-key"></i></span>
|
||||
<input type="password" class="form-control" id="octoprint_apikey" name="octoprint_apikey" aria-describedby="key-addon" placeholder="{{ _('Paste API Key here') }}" value="{{ configs.get('octoprint_apikey', '') }}">
|
||||
</div>
|
||||
<div class="form-text">
|
||||
{{ _('Can be found in OctoPrint Settings -> Application Keys or API.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary px-4 rounded-pill shadow-sm"><i class="bi bi-save2 me-2"></i>{{ _('Save Connection Settings') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
app/templates/printer/octo_embed.html
Normal file
28
app/templates/printer/octo_embed.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2"><i class="bi bi-window-sidebar text-info me-2"></i>{{ _('OctoPrint Panel (Embedded)') }}</h1>
|
||||
</div>
|
||||
|
||||
{% if embed_url %}
|
||||
<div class="card shadow rounded overflow-hidden" style="height: calc(100vh - 180px); min-height: 500px;">
|
||||
<!-- iFrame wrapper for responsivness -->
|
||||
<div class="w-100 h-100 position-relative">
|
||||
<iframe src="{{ embed_url }}"
|
||||
class="position-absolute border-0 w-100 h-100"
|
||||
style="top: 0; left: 0;"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning shadow-sm border-0 d-flex align-items-center" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill fs-4 text-warning me-3"></i>
|
||||
<div>
|
||||
<strong>{{ _('Configuration Required:') }}</strong>
|
||||
{{ _('The OctoPrint URL is not set. Please go to the ') }} <a href="{{ url_for('printer.octo_config') }}" class="alert-link text-decoration-underline">{{ _('System Configuration') }}</a> {{ _('page to set it up.') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
62
app/templates/printer/prepare.html
Normal file
62
app/templates/printer/prepare.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2"><i class="bi bi-file-earmark-plus text-primary me-2"></i>{{ _('Prepare Print') }}</h1>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light fw-bold text-secondary">
|
||||
<i class="bi bi-card-text me-1"></i>{{ _('Available Files on Printer') }}
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for f in files %}
|
||||
{% if f.type == 'machinecode' %}
|
||||
<div class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3">
|
||||
<div class="me-auto text-truncate" style="max-width: 80%;">
|
||||
<h6 class="mb-1"><i class="bi bi-file-earmark-code text-primary me-2"></i>{{ f.name }}</h6>
|
||||
<small class="text-muted d-block">{{ _('Size:') }} {{ f.size }} bytes, {{ _('Time:') }} {{ f.gcodeAnalysis.estimatedPrintTime if f.gcodeAnalysis else 'Unknown' }}s</small>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-success rounded-pill px-3 shadow-sm" onclick="printFile('{{ f.origin }}', '{{ f.path }}')"><i class="bi bi-play-fill me-1"></i>{{ _('Print Now') }}</button>
|
||||
<!-- <button class="btn btn-sm btn-outline-secondary rounded-pill ms-2" onclick="selectFile('{{ f.origin }}', '{{ f.path }}')">{{ _('Select') }}</button> -->
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="list-group-item text-center py-5 text-muted">
|
||||
<i class="bi bi-inbox display-4 d-block mb-3"></i>
|
||||
<p>{{ _('No printable files found. Go slice some G-Code first!') }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function printFile(origin, path) {
|
||||
if(!confirm("{{ _('Send this file to print immediately?') }}\n\n" + path)) return;
|
||||
|
||||
fetch('{{ url_for("printer.api_print_file") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ origin: origin, path: path })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
alert("{{ _('Print starting! Going to dashboard...') }}");
|
||||
window.location.href = "{{ url_for('printer.status') }}";
|
||||
} else {
|
||||
alert("Error: " + data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => alert("Error: " + err));
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
105
app/templates/printer/status.html
Normal file
105
app/templates/printer/status.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2"><i class="bi bi-activity text-primary me-2"></i>{{ _('Printer Status') }}</h1>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('printer.octo_config') }}" class="alert-link">{{ _('Go to Configuration') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif status %}
|
||||
<div class="row row-cols-1 row-cols-md-2 g-4">
|
||||
<!-- State Card -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light fw-bold text-secondary">
|
||||
<i class="bi bi-info-circle me-1"></i>{{ _('Current State') }}
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<h3 class="display-6 mt-3 text-primary">{{ status.get('state', {}).get('text', 'Unknown') }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temperature Card -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light fw-bold text-secondary">
|
||||
<i class="bi bi-thermometer-half me-1"></i>{{ _('Temperatures') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% set temps = status.get('temperature', {}) %}
|
||||
|
||||
<h5 class="mb-1"><i class="bi bi-fire text-danger me-2"></i>{{ _('Tool/Nozzle') }}</h5>
|
||||
<h4 class="ms-4 mb-4">
|
||||
{{ temps.get('tool0', {}).get('actual', 0) }} °C
|
||||
<small class="text-muted fs-6">/ {{ temps.get('tool0', {}).get('target', 0) }} °C</small>
|
||||
</h4>
|
||||
|
||||
<h5 class="mb-1"><i class="bi bi-square-fill text-warning me-2"></i>{{ _('Bed') }}</h5>
|
||||
<h4 class="ms-4">
|
||||
{{ temps.get('bed', {}).get('actual', 0) }} °C
|
||||
<small class="text-muted fs-6">/ {{ temps.get('bed', {}).get('target', 0) }} °C</small>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if job and job.get('job', {}).get('file', {}).get('name') %}
|
||||
<div class="card shadow-sm mt-4 border-success">
|
||||
<div class="card-header bg-success text-white fw-bold">
|
||||
<i class="bi bi-play-circle me-1"></i>{{ _('Active Print Job') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>{{ job.get('job', {}).get('file', {}).get('name') }}</h5>
|
||||
|
||||
{% set progress = job.get('progress', {}).get('completion', 0) %}
|
||||
{% if progress == None %}{% set progress = 0 %}{% endif %}
|
||||
<div class="progress mt-3 mb-2" style="height: 25px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: {{ progress }}%;" aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100">
|
||||
{{ "%.1f"|format(progress) }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between text-muted small mt-2">
|
||||
<span><strong>{{ _('Print Time:') }}</strong> {{ job.get('progress', {}).get('printTime', 0) }}s</span>
|
||||
<span><strong>{{ _('Time Left:') }}</strong> {{ job.get('progress', {}).get('printTimeLeft', 0) }}s</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 gap-2 d-flex">
|
||||
<button class="btn btn-warning" onclick="sendCmd('pause')"><i class="bi bi-pause-fill me-1"></i>{{ _('Pause/Resume') }}</button>
|
||||
<button class="btn btn-danger" onclick="sendCmd('cancel')"><i class="bi bi-stop-fill me-1"></i>{{ _('Cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function sendCmd(cmd) {
|
||||
if(cmd === 'cancel' && !confirm("{{ _('Are you sure you want to cancel the print?') }}")) return;
|
||||
|
||||
fetch('{{ url_for("printer.api_command") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({command: cmd})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("Error: " + data.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
setTimeout(() => { if (!window.pauseRefresh) window.location.reload(); }, 15000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user