Files
AIO_3D_Print_Web_Platform/app/templates/gcode_preview.html

471 lines
24 KiB
HTML

{% 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-eye text-primary me-2"></i>{{ _('GCode Preview') }}: {{ file.original_filename }}</h1>
<div>
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary btn-sm rounded shadow-sm"><i class="bi bi-download"></i> {{ _('Download GCode') }}</a>
<a href="{{ url_for('main.files') }}" class="btn btn-outline-secondary btn-sm rounded ms-2 shadow-sm"><i class="bi bi-arrow-left"></i> {{ _('Back') }}</a>
</div>
</div>
<div id="loading-overlay" class="text-center py-5 my-5">
<div class="spinner-border text-primary shadow-sm" role="status" style="width: 3rem; height: 3rem;"></div>
<h4 class="mt-4 text-secondary">{{ _('Loading and Parsing GCode Data...') }}</h4>
</div>
<div class="row d-none" id="preview-container" style="height: 75vh;">
<!-- 3D Canvas Area -->
<div class="col-md-11 position-relative h-100 p-0 border rounded border-secondary shadow-sm" style="background: #111;">
<div id="canvas-container" class="w-100 h-100 d-block overflow-hidden"></div>
<!-- Legend Overlay -->
<div class="position-absolute top-0 start-0 m-3 p-2 rounded shadow bg-dark bg-opacity-75 border border-secondary" style="color: #eee; font-size: 0.85rem; pointer-events: auto; z-index: 10;">
<div class="mb-1 legend-item user-select-none" data-type="WALL-OUTER" 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: #eb8b38;"></span>{{ _('Outer Wall') }}</div>
<div class="mb-1 legend-item user-select-none" data-type="WALL-INNER" 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: #4080cf;"></span>{{ _('Inner Wall') }}</div>
<div class="mb-1 legend-item user-select-none" data-type="FILL" 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: #ccc04b;"></span>{{ _('Infill') }}</div>
<div class="mb-1 legend-item user-select-none" data-type="SKIN" 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: #9e60b3;"></span>{{ _('Skin/TopBottom') }}</div>
<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>
<!-- Bottom Slider (Intra-Layer Progress) -->
<div class="position-absolute bottom-0 start-0 w-100 p-3" style="background: linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.8) 100%); z-index: 10;">
<div class="d-flex align-items-center gap-3">
<span class="text-white fw-medium text-nowrap user-select-none"><i class="bi bi-play-circle me-1"></i>{{ _('Layer Progress:') }}</span>
<input type="range" class="form-range flex-grow-1" id="progress-slider" min="0" max="100" value="100" step="0.1">
</div>
</div>
</div>
<!-- Right Sidebar (Layer Slider) -->
<div class="col-md-1 h-100 d-flex flex-column align-items-center justify-content-center bg-white border rounded shadow-sm position-relative">
<label class="form-label mb-3 fw-bold text-center text-primary mt-3">{{ _('Layer') }}<br>
<span id="layer-display" class="badge bg-primary fs-6 mt-1 shadow-sm px-3 rounded-pill">0</span>
</label>
<div class="flex-grow-1 w-100 d-flex justify-content-center pb-4 py-2">
<input type="range" class="form-range h-100" id="layer-slider" min="0" max="0" value="0" style="writing-mode: bt-lr; -webkit-appearance: slider-vertical; cursor: ns-resize;">
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/three.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/OrbitControls.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const COLORS = {
'WALL-OUTER': new THREE.Color(0xeb8b38),
'WALL-INNER': new THREE.Color(0x4080cf),
'FILL': new THREE.Color(0xccc04b),
'SKIN': new THREE.Color(0x9e60b3),
'SUPPORT': new THREE.Color(0x57b357),
'SKIRT': new THREE.Color(0x00ffff),
'SUPPORT-INTERFACE': new THREE.Color(0x2b6b2b),
'TRAVEL': new THREE.Color(0x405060),
'DEFAULT': new THREE.Color(0xaaaaaa),
};
// Inject printer machine dimensions via Jinja
const bedWidth = {{ machine_width | default(220) }};
const bedDepth = {{ machine_depth | default(220) }};
const bedHeight = {{ machine_height | default(250) }};
const offsetX = {{ offset_x | default(0.0) }};
const offsetY = {{ offset_y | default(0.0) }};
// Type indices for shader visibility filtering
const TYPE_INDEX = {
'TRAVEL': 0, 'WALL-OUTER': 1, 'WALL-INNER': 2,
'FILL': 3, 'SKIN': 4, 'SUPPORT': 5, 'DEFAULT': 6,
'SKIRT': 7, 'SUPPORT-INTERFACE': 8
};
let layers = [];
let scene, camera, renderer, controls;
let group = new THREE.Group();
const layerSlider = document.getElementById('layer-slider');
const layerDisplay = document.getElementById('layer-display');
const progressSlider = document.getElementById('progress-slider');
// Shader material for high-speed dynamic feature visibility
const gcodeMat = new THREE.ShaderMaterial({
uniforms: {
uShowOuter: { value: 1.0 },
uShowInner: { value: 1.0 },
uShowInfill: { value: 1.0 },
uShowSkin: { value: 1.0 },
uShowSupport: { value: 1.0 },
uShowSkirt: { value: 1.0 },
uShowSupportInterface: { value: 1.0 },
uShowTravel: { value: 1.0 },
uShowDefault: { value: 1.0 }
},
vertexShader: `
attribute float pType;
varying vec3 vColor;
varying float vType;
void main() {
vColor = color;
vType = pType;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vType;
uniform float uShowOuter;
uniform float uShowInner;
uniform float uShowInfill;
uniform float uShowSkin;
uniform float uShowSupport;
uniform float uShowSkirt;
uniform float uShowSupportInterface;
uniform float uShowTravel;
uniform float uShowDefault;
void main() {
float show = 1.0;
int t = int(vType + 0.5);
if (t == 0) show = uShowTravel;
else if (t == 1) show = uShowOuter;
else if (t == 2) show = uShowInner;
else if (t == 3) show = uShowInfill;
else if (t == 4) show = uShowSkin;
else if (t == 5) show = uShowSupport;
else if (t == 7) show = uShowSkirt;
else if (t == 8) show = uShowSupportInterface;
else show = uShowDefault;
if (show < 0.5) discard;
gl_FragColor = vec4(vColor, 1.0);
}
`,
vertexColors: true,
side: THREE.DoubleSide,
linewidth: 1
});
// Binding the Legend Buttons
const uniformMap = {
'WALL-OUTER': 'uShowOuter',
'WALL-INNER': 'uShowInner',
'FILL': 'uShowInfill',
'SKIN': 'uShowSkin',
'SUPPORT': 'uShowSupport',
'SKIRT': 'uShowSkirt',
'SUPPORT-INTERFACE': 'uShowSupportInterface',
'TRAVEL': 'uShowTravel'
};
document.querySelectorAll('.legend-item').forEach(el => {
el.addEventListener('click', function() {
const t = this.dataset.type;
const uniformName = uniformMap[t];
if (uniformName) {
const currentVal = gcodeMat.uniforms[uniformName].value;
const newVal = currentVal > 0.5 ? 0.0 : 1.0;
gcodeMat.uniforms[uniformName].value = newVal;
this.style.opacity = newVal > 0.5 ? "1.0" : "0.4";
}
});
});
function init3D() {
const container = document.getElementById('canvas-container');
scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
scene.add(group);
camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 1, 5000);
camera.up.set(0, 0, 1);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = false;
controls.mouseButtons.MIDDLE = THREE.MOUSE.PAN;
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
const container = document.getElementById('canvas-container');
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
async function loadGCode() {
try {
const url = '{{ url_for("main.download_gcode", file_id=file.id) }}';
const response = await fetch(url);
if (!response.ok) throw new Error("GCode Request Failed");
const gcodeText = await response.text();
document.getElementById('loading-overlay').classList.add('d-none');
document.getElementById('preview-container').classList.remove('d-none');
init3D();
parseGCode(gcodeText);
// Add grid matching printer size
setupMachineEnvironment();
animate();
// Init controls
layerSlider.max = Math.max(0, layers.length - 1);
layerSlider.value = Math.max(0, layers.length - 1);
updateUI();
layerSlider.addEventListener('input', updateUI);
progressSlider.addEventListener('input', updateUI);
} catch(e) {
console.error("Error Loading GCode", e);
document.getElementById('loading-overlay').innerHTML = `
<div class="text-danger my-5 py-5">
<i class="bi bi-exclamation-triangle display-1"></i>
<h3 class="mt-3">{{ _('Failed to load GCode preview.') }}</h3>
<p class="text-muted">${e.toString()}</p>
</div>`;
}
}
function parseGCode(text) {
const lines = text.split('\n');
let current = { x: 0, y: 0, z: 0, e: 0 };
let currentTypeStr = 'DEFAULT';
let currentExtrudePoints = [];
let currentExtrudeColors = [];
let currentExtrudeTypes = [];
let currentTravelPoints = [];
let currentTravelColors = [];
let currentTravelTypes = [];
function flushLayer() {
if (currentExtrudePoints.length === 0 && currentTravelPoints.length === 0) return;
let layerGroup = new THREE.Group();
if (currentExtrudePoints.length > 0) {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(currentExtrudePoints, 3));
geo.setAttribute('color', new THREE.Float32BufferAttribute(currentExtrudeColors, 3));
geo.setAttribute('pType', new THREE.Float32BufferAttribute(currentExtrudeTypes, 1));
const mesh = new THREE.Mesh(geo, gcodeMat);
mesh.userData.isExtrude = true;
layerGroup.add(mesh);
currentExtrudePoints = []; currentExtrudeColors = []; currentExtrudeTypes = [];
}
if (currentTravelPoints.length > 0) {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(currentTravelPoints, 3));
geo.setAttribute('color', new THREE.Float32BufferAttribute(currentTravelColors, 3));
geo.setAttribute('pType', new THREE.Float32BufferAttribute(currentTravelTypes, 1));
const lineSeg = new THREE.LineSegments(geo, gcodeMat);
lineSeg.userData.isTravel = true;
layerGroup.add(lineSeg);
currentTravelPoints = []; currentTravelColors = []; currentTravelTypes = [];
}
layers.push(layerGroup);
group.add(layerGroup);
}
for (let i = 0; i < lines.length; i++) {
let chunk = lines[i].trim().toUpperCase();
if (!chunk) continue;
if (chunk.startsWith(';LAYER:')) {
flushLayer();
} else if (chunk.startsWith(';TYPE:')) {
currentTypeStr = chunk.substring(6).trim();
} else if (chunk.startsWith('G0') || chunk.startsWith('G1')) {
let next = { x: current.x, y: current.y, z: current.z, e: current.e };
let parts = chunk.split(/\s+/);
let hasMove = false;
for (let p of parts) {
if (p.startsWith('X')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.x = v; hasMove = true; } }
if (p.startsWith('Y')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.y = v; hasMove = true; } }
if (p.startsWith('Z')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.z = v; hasMove = true; } }
if (p.startsWith('E')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.e = v; } }
}
if (hasMove && !isNaN(next.x) && !isNaN(next.y) && !isNaN(next.z)) {
let isExtrude = (next.e > current.e);
// Cura uses G0 for travel generally
if (chunk.startsWith('G0') && !chunk.includes('E')) isExtrude = false;
let activeType = isExtrude ? currentTypeStr : 'TRAVEL';
let col = COLORS[activeType] || COLORS['DEFAULT'];
let tIdx = TYPE_INDEX[activeType] !== undefined ? TYPE_INDEX[activeType] : TYPE_INDEX['DEFAULT'];
if (isExtrude) {
let dx = next.x - current.x;
let dy = next.y - current.y;
let dist = Math.sqrt(dx*dx + dy*dy);
if (dist > 0.0001) {
let hw = 0.4 / 2.0; // 0.4mm wire width
let hh = 0.2 / 2.0; // 0.2mm layer height roughly
let nx = -(dy / dist) * hw;
let ny = (dx / dist) * hw;
let p1x = current.x + nx, p1y = current.y + ny; // current-left
let p2x = current.x - nx, p2y = current.y - ny; // current-right
let p3x = next.x + nx, p3y = next.y + ny; // next-left
let p4x = next.x - nx, p4y = next.y - ny; // next-right
// Top face
currentExtrudePoints.push(
p1x, p1y, current.z + hh,
p3x, p3y, next.z + hh,
p2x, p2y, current.z + hh,
p3x, p3y, next.z + hh,
p4x, p4y, next.z + hh,
p2x, p2y, current.z + hh
);
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r, col.g, col.b); currentExtrudeTypes.push(tIdx); }
// Bottom face
currentExtrudePoints.push(
p1x, p1y, current.z - hh,
p2x, p2y, current.z - hh,
p3x, p3y, next.z - hh,
p2x, p2y, current.z - hh,
p4x, p4y, next.z - hh,
p3x, p3y, next.z - hh
);
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.4, col.g*0.4, col.b*0.4); currentExtrudeTypes.push(tIdx); }
// Left face
currentExtrudePoints.push(
p1x, p1y, current.z - hh,
p3x, p3y, next.z - hh,
p1x, p1y, current.z + hh,
p3x, p3y, next.z - hh,
p3x, p3y, next.z + hh,
p1x, p1y, current.z + hh
);
// Fake lighting based on normal side
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.6, col.g*0.6, col.b*0.6); currentExtrudeTypes.push(tIdx); }
// Right face
currentExtrudePoints.push(
p2x, p2y, current.z - hh,
p2x, p2y, current.z + hh,
p4x, p4y, next.z - hh,
p2x, p2y, current.z + hh,
p4x, p4y, next.z + hh,
p4x, p4y, next.z - hh
);
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.8, col.g*0.8, col.b*0.8); currentExtrudeTypes.push(tIdx); }
}
} else {
currentTravelPoints.push(current.x, current.y, current.z);
currentTravelPoints.push(next.x, next.y, next.z);
currentTravelColors.push(col.r, col.g, col.b, col.r, col.g, col.b);
currentTravelTypes.push(tIdx, tIdx);
}
current.x = next.x; current.y = next.y; current.z = next.z; current.e = next.e;
}
} else if (chunk.startsWith('G92')) {
let parts = chunk.split(/\s+/);
for (let p of parts) {
if (p.startsWith('E')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.e = v; }
if (p.startsWith('X')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.x = v; }
if (p.startsWith('Y')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.y = v; }
if (p.startsWith('Z')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.z = v; }
}
}
}
flushLayer();
}
function setupMachineEnvironment() {
if (layers.length === 0) return;
let bbox = new THREE.Box3();
for (let layerGrp of layers) {
layerGrp.children.forEach(child => {
child.geometry.computeBoundingBox();
bbox.union(child.geometry.boundingBox);
});
}
// The GCode coordinates for the actual print bed are from (0,0) to (W,H).
// The GCode trajectory is ALREADY offset by plater.html during slicing.
// We just need to place the grid exactly in the center of the bed: (W/2, H/2).
let gridOffsetX = (bedWidth / 2);
let gridOffsetY = (bedDepth / 2);
// Add Grid
const gridDivisions = Math.ceil(Math.max(bedWidth, bedDepth) / 10);
const gridHelper = new THREE.GridHelper(Math.max(bedWidth, bedDepth), gridDivisions, 0x444444, 0x242424);
gridHelper.rotation.x = Math.PI / 2;
gridHelper.position.set(gridOffsetX, gridOffsetY, 0);
scene.add(gridHelper);
// Add Printer Volume Outline
const boxGeo = new THREE.BoxGeometry(bedWidth, bedDepth, bedHeight);
const edges = new THREE.EdgesGeometry(boxGeo);
const boxOutline = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x444444 }));
boxOutline.position.set(gridOffsetX, gridOffsetY, bedHeight/2);
scene.add(boxOutline);
// Align Camera to target the center of the bed grid
controls.target.set(gridOffsetX, gridOffsetY, 0);
camera.position.set(gridOffsetX, gridOffsetY - (bedDepth * 1.5), bedHeight * 0.8);
}
function updateUI() {
if (layers.length === 0) return;
let activeIdx = parseInt(layerSlider.value);
let intraProg = parseFloat(progressSlider.value);
layerDisplay.innerText = activeIdx + " / " + (layers.length - 1);
for (let i = 0; i < layers.length; i++) {
let layerGrp = layers[i];
if (i < activeIdx) {
layerGrp.visible = true;
layerGrp.children.forEach(child => child.geometry.setDrawRange(0, Infinity));
} else if (i === activeIdx) {
layerGrp.visible = true;
layerGrp.children.forEach(child => {
let totalVertices = child.geometry.attributes.position.count;
let elementsPerUnit = child.userData.isTravel ? 2 : 24;
let totalUnits = totalVertices / elementsPerUnit;
let drawCount = Math.floor(totalUnits * (intraProg / 100)) * elementsPerUnit;
child.geometry.setDrawRange(0, drawCount);
});
} else {
layerGrp.visible = false;
}
}
}
loadGCode();
});
</script>
{% endblock %}