This commit is contained in:
2026-05-09 16:36:21 +08:00
parent 5a265b6b1d
commit 65c342219d
9 changed files with 244052 additions and 2 deletions

45
refer/GcodeParser.py Normal file
View File

@@ -0,0 +1,45 @@
import re
def load_gcode_vertices(path):
vertices = []
x = 0
y = 0
z = 0
with open(path, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.strip()
if not line:
continue
if line.startswith("G0") or line.startswith("G1"):
old_x = x
old_y = y
old_z = z
mx = re.search(r"X([-0-9.]+)", line)
my = re.search(r"Y([-0-9.]+)", line)
mz = re.search(r"Z([-0-9.]+)", line)
if mx:
x = float(mx.group(1))
if my:
y = float(my.group(1))
if mz:
z = float(mz.group(1))
vertices.append({
"x1": old_x,
"y1": old_y,
"z1": old_z,
"x2": x,
"y2": y,
"z2": z,
})
return vertices

77
refer/api_handle.py Normal file
View File

@@ -0,0 +1,77 @@
import functools
from flask import Blueprint, request, jsonify
from app.models import ApiKey
from app.utils.octoprint_client import OctoPrintClient
from app.models import SystemConfig
api_bp = Blueprint('api_handle', __name__, url_prefix='/api/v1')
def get_octo_client():
url = SystemConfig.query.filter_by(key='octoprint_url').first()
apikey = SystemConfig.query.filter_by(key='octoprint_apikey').first()
if url and url.value and apikey and apikey.value:
return OctoPrintClient(url.value, apikey.value)
return None
def require_api_key(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
api_key_header = request.headers.get('X-Api-Key')
if not api_key_header:
return jsonify({'error': 'Missing API Key in headers (X-Api-Key)'}), 401
key_record = ApiKey.query.filter_by(key=api_key_header).first()
if not key_record:
return jsonify({'error': 'Invalid API Key'}), 401
return f(*args, **kwargs)
return decorated
@api_bp.route('/status', methods=['GET'])
@require_api_key
def get_status():
client = get_octo_client()
if not client:
return jsonify({'error': 'Printer not configured'}), 503
try:
status_data = client.get_printer_status()
job_data = client.get_job_info()
return jsonify({'status': status_data, 'job': job_data})
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/octoprint_client', methods=['POST'])
@require_api_key
def invoke_octoprint_client():
"""
Expects JSON payload like:
{
"method": "pause_print",
"kwargs": {"action": "pause"}
}
"""
client = get_octo_client()
if not client:
return jsonify({'error': 'Printer not configured'}), 503
data = request.get_json()
if not data or 'method' not in data:
return jsonify({'error': 'Missing method in JSON payload'}), 400
method_name = data['method']
kwargs = data.get('kwargs', {})
args = data.get('args', [])
if not hasattr(client, method_name):
return jsonify({'error': f'Method {method_name} not found on OctoPrintClient'}), 400
func = getattr(client, method_name)
if not callable(func) or method_name.startswith('_'):
return jsonify({'error': f'Method {method_name} is not allowed'}), 403
try:
result = func(*args, **kwargs)
return jsonify({'success': True, 'result': result})
except Exception as e:
return jsonify({'error': str(e)}), 500

549
refer/gcode_preview.html Normal file
View File

@@ -0,0 +1,549 @@
{% 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-2 border-bottom">
<div>
<h1 class="h2 mb-1"><i class="bi bi-eye text-primary me-2"></i>{{ _('GCode Preview') }}: {{ file.original_filename }}</h1>
<div class="text-muted small">
<span class="me-3"><i class="bi bi-clock-history me-1"></i>{{ _('Estimated Time:') }} <span class="fw-bold">{{ time_info }}</span></span>
<span class="me-3"><i class="bi bi-layers me-1"></i>{{ _('First Layer Time:') }} <span class="fw-bold">{{ layer1_time }}</span></span>
<span><i class="bi bi-rulers me-1"></i>{{ _('Filament Used [mm]:') }} <span class="fw-bold">{{ filament_used }}</span></span>
</div>
</div>
<div class="mt-2 mt-md-0">
<a href="{{ url_for('printer.prepare') }}#file-{{ file.id }}" class="btn btn-warning btn-sm rounded shadow-sm fw-bold"><i class="bi bi-printer"></i> {{ _('Go to Print') }}</a>
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary btn-sm rounded shadow-sm ms-2"><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 id="legend-overlay" 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>
<!-- 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() {
// 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
let COLORS = {};
let TYPE_INDEX = {};
let gcodeMat = null;
const SLICER_CONFIGS = {
'Cura': [
{ id: 'TRAVEL', label: '{{ _("Travel (Move)") }}', color: 0x405060, defaultShow: false },
{ id: 'WALL-OUTER', label: '{{ _("Outer Wall") }}', color: 0xeb8b38, defaultShow: true },
{ id: 'WALL-INNER', label: '{{ _("Inner Wall") }}', color: 0x4080cf, defaultShow: true },
{ id: 'FILL', label: '{{ _("Infill") }}', color: 0xccc04b, defaultShow: true },
{ id: 'SKIN', label: '{{ _("Skin/TopBottom") }}', color: 0x9e60b3, defaultShow: true },
{ id: 'SUPPORT', label: '{{ _("Support") }}', color: 0x57b357, defaultShow: true },
{ id: 'SKIRT', label: '{{ _("Skirt") }}', color: 0x00ffff, defaultShow: true },
{ id: 'SUPPORT-INTERFACE', label: '{{ _("Support Interface") }}', color: 0x2b6b2b, defaultShow: true },
{ id: 'DEFAULT', label: '{{ _("Others") }}', color: 0xaaaaaa, defaultShow: true }
],
'Prusa': [
{ id: 'TRAVEL', label: '{{ _("Travel (Move)") }}', color: 0x405060, defaultShow: false },
{ id: 'Custom', label: '{{ _("Custom") }}', color: 0xd0e0ff, defaultShow: true },
{ id: 'Skirt/Brim', label: '{{ _("Skirt/Brim") }}', color: 0x00FFFF, defaultShow: true },
{ id: 'Support material', label: '{{ _("Support material") }}', color: 0x90EE90, defaultShow: true },
{ id: 'Perimeter', label: '{{ _("Perimeter") }}', color: 0xFFFFE0, defaultShow: true },
{ id: 'External perimeter', label: '{{ _("External perimeter") }}', color: 0xFFA500, defaultShow: true },
{ id: 'Solid infill', label: '{{ _("Solid infill") }}', color: 0x800080, defaultShow: true },
{ id: 'Overhang perimeter', label: '{{ _("Overhang perimeter") }}', color: 0x00008B, defaultShow: true },
{ id: 'Internal infill', label: '{{ _("Internal infill") }}', color: 0x8B0000, defaultShow: true },
{ id: 'Bridge infill', label: '{{ _("Bridge infill") }}', color: 0x0000FF, defaultShow: true },
{ id: 'Top solid infill', label: '{{ _("Top solid infill") }}', color: 0xFF0000, defaultShow: true },
{ id: 'Support material interface', label: '{{ _("Support Interface") }}', color: 0x2b6b2b, defaultShow: true },
{ id: 'DEFAULT', label: '{{ _("Others") }}', color: 0xaaaaaa, defaultShow: true }
]
};
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');
function setupSlicerConfig(text) {
let slicerType = 'Cura'; // default
if (text.substring(0, 500).includes('generated by PrusaSlicer')) {
slicerType = 'Prusa';
}
const config = SLICER_CONFIGS[slicerType];
// 1. Build uniforms & shader strings dynamically
let uniformsObj = {};
let fragmentUniformsDecl = '';
let fragmentUniformsLogic = '';
let overlayHTML = '';
config.forEach((c, idx) => {
COLORS[c.id] = new THREE.Color(c.color);
TYPE_INDEX[c.id] = idx;
const uniformName = 'uShow' + idx;
uniformsObj[uniformName] = { value: c.defaultShow ? 1.0 : 0.0 };
fragmentUniformsDecl += `uniform float ${uniformName};\n`;
if (idx === 0) {
fragmentUniformsLogic += `if (t == 0) show = ${uniformName};\n`;
} else {
fragmentUniformsLogic += ` else if (t == ${idx}) show = ${uniformName};\n`;
}
// Build Legend UI
const hexColor = '#' + c.color.toString(16).padStart(6, '0');
const opacityStyle = c.defaultShow ? '1.0' : '0.4';
overlayHTML += `
<div class="mb-1 legend-item user-select-none" data-id="${c.id}" data-uniform="${uniformName}" style="cursor: pointer; transition: opacity 0.2s; opacity: ${opacityStyle};"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: ${hexColor};"></span>${c.label}</div>`;
});
// Add fallback condition
fragmentUniformsLogic += ` else show = 1.0;\n`;
document.getElementById('legend-overlay').innerHTML = overlayHTML;
gcodeMat = new THREE.ShaderMaterial({
uniforms: uniformsObj,
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;
${fragmentUniformsDecl}
void main() {
float show = 1.0;
int t = int(vType + 0.5);
${fragmentUniformsLogic}
if (show < 0.5) discard;
gl_FragColor = vec4(vColor, 1.0);
}
`,
vertexColors: true,
side: THREE.DoubleSide,
linewidth: 1
});
// Legend binding
document.querySelectorAll('.legend-item').forEach(el => {
el.addEventListener('click', function() {
const uniformName = this.dataset.uniform;
if (uniformName && gcodeMat.uniforms[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');
setupSlicerConfig(gcodeText);
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 relativeE = false; // Track M83 (relative) vs M82 (absolute)
// Dynamically compute width and layer height based on gcode info if possible
let extWidth = 0.4;
let layerHeight = 0.2;
let pWidth = extWidth;
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();
if (!chunk) continue;
let upperChunk = chunk.toUpperCase();
if (upperChunk.startsWith('M82')) relativeE = false;
else if (upperChunk.startsWith('M83')) relativeE = true;
if (upperChunk.startsWith(';LAYER:') || upperChunk.startsWith(';LAYER_CHANGE')) {
flushLayer();
} else if (upperChunk.startsWith(';LAYER_HEIGHT:')) {
let lh = parseFloat(chunk.substring(14));
if (!isNaN(lh) && lh > 0) layerHeight = lh;
} else if (upperChunk.startsWith(';HEIGHT:')) {
let lh = parseFloat(chunk.substring(8));
if (!isNaN(lh) && lh > 0) layerHeight = lh;
} else if (upperChunk.startsWith(';WIDTH:')) {
let w = parseFloat(chunk.substring(7));
if (!isNaN(w) && w > 0) pWidth = w;
} else if (upperChunk.startsWith(';TYPE:')) {
currentTypeStr = chunk.substring(6).trim();
} else if (chunk.startsWith(';') && COLORS[chunk.substring(1).trim()] !== undefined) {
currentTypeStr = chunk.substring(1).trim();
} else if (upperChunk.startsWith(';') && chunk.includes(' perimeter')) {
currentTypeStr = chunk.substring(1).trim();
} else if (upperChunk.startsWith(';') && chunk.includes(' infill')) {
// Heuristics for Prusa/Slic3r specific comments like `; Internal infill`
currentTypeStr = chunk.substring(1).trim();
} else if (upperChunk.startsWith(';') && chunk.includes(' material')) {
// Support material
currentTypeStr = chunk.substring(1).trim();
} else if (upperChunk.startsWith(';') && chunk.includes('Skirt/Brim')) {
// Skirt/Brim
currentTypeStr = 'Skirt/Brim';
} else if (upperChunk.startsWith('G0') || upperChunk.startsWith('G1')) {
let next = { x: current.x, y: current.y, z: current.z, e: current.e };
let parts = upperChunk.split(/\s+/);
let hasMove = false;
let hasE = false;
let eVal = 0;
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)) { eVal = v; hasE = true; } }
}
if (hasMove && !isNaN(next.x) && !isNaN(next.y) && !isNaN(next.z)) {
let isExtrude = false;
if (hasE) {
if (relativeE) {
next.e = current.e + eVal;
isExtrude = eVal > 0;
} else {
next.e = eVal;
isExtrude = next.e > current.e;
}
}
// Cura uses G0 for travel generally
if (upperChunk.startsWith('G0') && !upperChunk.includes('E')) isExtrude = false;
let activeType = isExtrude ? currentTypeStr : 'TRAVEL';
let resolvedType = activeType;
if (isExtrude && COLORS[activeType] === undefined) {
resolvedType = 'DEFAULT';
}
let col = COLORS[resolvedType] || COLORS['DEFAULT'];
let tIdx = TYPE_INDEX[resolvedType] !== undefined ? TYPE_INDEX[resolvedType] : 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 = pWidth / 2.0;
let hh = layerHeight / 2.0;
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 {
// Travel lines get slight vertical offset for visibility
let zOff = 0.05;
currentTravelPoints.push(current.x, current.y, current.z + zOff);
currentTravelPoints.push(next.x, next.y, next.z + zOff);
currentTravelColors.push(col.r, col.g, col.b, col.r, col.g, col.b);
currentTravelTypes.push(tIdx, tIdx);
}
// Update E based on parsed G-code execution type
if (hasE) {
if (relativeE) current.e += eVal;
else current.e = eVal;
}
current.x = next.x; current.y = next.y; current.z = next.z;
}
} else if (upperChunk.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 %}

34
refer/gcode_view.qml Normal file
View File

@@ -0,0 +1,34 @@
import QtQuick
import QtQuick3D
Item {
id: root
property real progress: 100
property real maxLayer: 10
View3D {
anchors.fill: parent
environment: SceneEnvironment {
clearColor: "#1a1a1a"
backgroundMode: SceneEnvironment.Color
}
PerspectiveCamera {
id: camera
z: 300
y: -200
eulerRotation.x: 45
}
DirectionalLight { eulerRotation.x: -45 }
// Render layers using repeated line models or a custom geometry
// Since QtQuick3D dynamic geometry from Python is complex, we render primitives for demo
Node { id: gcodeNode }
}
Text {
text: "QtQuick3D GCode Preview\nProgress: " + progress.toFixed(1) + "%"
color: "white"
font.pixelSize: 20
}
}

91
refer/gcode_view2.qml Normal file
View File

@@ -0,0 +1,91 @@
import QtQuick
import QtQuick.Window
import QtQuick3D
Window {
visible: true
visibility: Window.FullScreen
width: 1280
height: 720
color: "black"
View3D {
anchors.fill: parent
environment: SceneEnvironment {
clearColor: "#202020"
backgroundMode: SceneEnvironment.Color
}
PerspectiveCamera {
id: camera
position: Qt.vector3d(0, 100, 300)
eulerRotation.x: -20
}
DirectionalLight {
eulerRotation.x: -45
brightness: 2
}
Node {
id: rootNode
Repeater3D {
model: gcodeData
delegate: Model {
property real dx: modelData.x2 - modelData.x1
property real dy: modelData.y2 - modelData.y1
property real dz: modelData.z2 - modelData.z1
property real length: Math.sqrt(dx*dx + dy*dy + dz*dz)
source: "#Cylinder"
position: Qt.vector3d(
(modelData.x1 + modelData.x2)/2,
(modelData.z1 + modelData.z2)/2,
(modelData.y1 + modelData.y2)/2
)
scale: Qt.vector3d(0.2, length / 100.0, 0.2)
materials: [
DefaultMaterial {
diffuseColor: "#00ff88"
}
]
}
}
}
MouseArea {
anchors.fill: parent
property real lastX
property real lastY
onPressed: {
lastX = mouse.x
lastY = mouse.y
}
onPositionChanged: {
let dx = mouse.x - lastX
let dy = mouse.y - lastY
rootNode.eulerRotation.y += dx * 0.3
rootNode.eulerRotation.x += dy * 0.3
lastX = mouse.x
lastY = mouse.y
}
onWheel: {
camera.position.z += wheel.angleDelta.y * -0.1
}
}
}
}

37
refer/qml.py Normal file
View File

@@ -0,0 +1,37 @@
import sys
from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQml import QQmlApplicationEngine
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
from GcodeParser import load_gcode_vertices
class GcodeData(QObject):
verticesChanged = pyqtSignal()
def __init__(self):
super().__init__()
# self._vertices = load_gcode_vertices("/home/lhye200/.octoprint/uploads/20260414135441_42bff5215c6148b8b5f4d8c4f15d5ddc.gcode")
self._vertices = load_gcode_vertices("test.gcode")
def getVertices(self):
return self._vertices
vertices = pyqtProperty('QVariantList', getVertices, notify=verticesChanged)
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
model = GcodeData()
engine.rootContext().setContextProperty("gcodeData", model.vertices)
engine.load("gcode_view2.qml")
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())

242948
refer/smp4bwithfan.gcode Normal file

File diff suppressed because it is too large Load Diff

268
refer/test.gcode Normal file
View File

@@ -0,0 +1,268 @@
G92 E0 ;Reset Extruder
G1 Z20.0 F3000 ;Move Z Axis up
M104 S235 ;Set final nozzle temp
G1 X-13 Y20 Z3 F5000.0 ;Move to out position
M190 S70 ;Wait for bed temp to stabilize
M109 S235 ;Wait for nozzle temp to stabilize
G1 X-2.1 Y20 Z0.28 F5000.0 ;Move to start position
G1 X-2.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line
G1 X-2.4 Y145.0 Z0.28 F5000.0 ;Move to side a little
G1 X-2.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line
G92 E0 ;Reset Extruder
G1 E-1.0000 F1800 ;Retract a bit
G1 Z2.0 F3000 ;Move Z Axis up
G1 E0.0000 F1800
G21 ; set units to millimeters
G90 ; use absolute coordinates
M83 ; use relative distances for extrusion
M107
;LAYER_CHANGE
;Z:0.2
;HEIGHT:0.2
G1 E-1.2 F1800
G1 Z.6 F9000
G1 X52.817 Y67.413
G1 Z.2
G1 E1.2 F1800
M204 P500
;TYPE:Skirt/Brim
;WIDTH:0.5
G1 X53.827 Y66.755 E.04581
G1 X54.927 Y66.264 E.04578
G1 X56.091 Y65.95 E.04582
G1 X57.5 Y65.814 E.0538
G1 X162.711 Y65.823 E3.99868
G1 X163.87 Y65.945 E.04429
G1 X165.073 Y66.264 E.0473
G1 X165.798 Y66.564 E.02982
G1 X166.842 Y67.165 E.04578
G1 X167.78 Y67.922 E.04581
G1 X168.587 Y68.817 E.0458
G1 X169.436 Y70.202 E.06174
G1 X169.867 Y71.328 E.04582
G1 X170.05 Y72.091 E.02982
G1 X170.177 Y73.289 E.04579
G1 X170.186 Y146.5 E2.78248
G1 X170.05 Y147.909 E.0538
G1 X169.736 Y149.073 E.04582
G1 X169.436 Y149.798 E.02982
G1 X168.587 Y151.183 E.06174
G1 X168.078 Y151.78 E.02982
G1 X167.183 Y152.587 E.0458
G1 X165.798 Y153.436 E.06174
G1 X164.672 Y153.867 E.04582
G1 X163.796 Y154.074 E.03421
G1 X129.811 Y160.705 E1.316
G1 X128.826 Y160.825 E.03771
G1 X127.704 Y160.792 E.04266
G1 X56.467 Y154.112 E2.71933
G1 X54.927 Y153.736 E.06025
G1 X53.827 Y153.245 E.04578
G1 X53.158 Y152.835 E.02982
G1 X52.22 Y152.078 E.04581
G1 X51.413 Y151.183 E.0458
G1 X50.755 Y150.173 E.04581
G1 X50.264 Y149.073 E.04578
G1 X49.95 Y147.909 E.04582
G1 X49.814 Y146.5 E.0538
G1 X49.814 Y73.5 E2.77446
G1 X49.95 Y72.091 E.0538
G1 X50.133 Y71.328 E.02982
G1 X50.564 Y70.202 E.04582
G1 X51.413 Y68.817 E.06174
G1 X51.922 Y68.22 E.02982
G1 X52.772 Y67.453 E.04351
M204 P2500
M204 T2000
G1 X53.077 Y67.793 F9000
M204 P500
G1 F1800
G1 X53.114 Y67.76 E.00188
G1 X54.066 Y67.145 E.04308
G1 X55.102 Y66.686 E.04307
G1 X56.198 Y66.395 E.0431
G1 X57.5 Y66.271 E.04971
G1 X162.675 Y66.278 E3.99732
G1 X163.765 Y66.39 E.04165
G1 X164.554 Y66.575 E.0308
G1 X165.623 Y66.986 E.04353
G1 X166.603 Y67.555 E.04307
G1 X167.73 Y68.517 E.05632
G1 X168.445 Y69.397 E.04309
G1 X169.014 Y70.377 E.04307
G1 X169.314 Y71.102 E.02982
G1 X169.605 Y72.198 E.0431
G1 X169.722 Y73.325 E.04306
G1 X169.729 Y146.5 E2.78111
G1 X169.605 Y147.802 E.04971
G1 X169.422 Y148.565 E.02982
G1 X169.014 Y149.623 E.0431
G1 X168.24 Y150.886 E.0563
G1 X167.73 Y151.483 E.02984
G1 X166.886 Y152.24 E.04309
G1 X165.623 Y153.014 E.0563
G1 X164.565 Y153.422 E.0431
G1 X163.709 Y153.626 E.03344
G1 X129.723 Y160.256 E1.31603
G1 X128.837 Y160.368 E.03394
G1 X127.746 Y160.337 E.04148
G1 X56.51 Y153.657 E2.7193
G1 X55.102 Y153.314 E.05508
G1 X54.066 Y152.855 E.04307
G1 X53.114 Y152.24 E.04308
G1 X52.517 Y151.73 E.02984
G1 X51.76 Y150.886 E.04309
G1 X51.145 Y149.934 E.04308
G1 X50.686 Y148.898 E.04307
G1 X50.395 Y147.802 E.0431
G1 X50.271 Y146.5 E.04971
G1 X50.271 Y73.5 E2.77446
G1 X50.395 Y72.198 E.04971
G1 X50.578 Y71.435 E.02982
G1 X50.986 Y70.377 E.0431
G1 X51.76 Y69.114 E.0563
G1 X52.27 Y68.517 E.02984
G1 X53.033 Y67.833 E.03895
M204 P2500
G1 E-1.2
G1 Z.6 F9000
; printing object temp_edit_f69b6cf4417a43fa81e39d69b3952b58.stl id:0 copy 0
G1 X131.959 Y152.611
G1 Z.2
G1 E1.2 F1800
M204 P500
;TYPE:Support material
G1 X132.246 Y153.441 E.03338
G1 X132.344 Y154.317 E.0335
G1 X132.246 Y155.193 E.0335
G1 X131.959 Y156.022 E.03334
G1 X131.547 Y156.692 E.02989
G1 X131.146 Y157.136 E.02274
G1 X130.723 Y157.499 E.02118
G1 X129.894 Y157.918 E.0353
G1 X128.891 Y158.141 E.03905
G1 X127.954 Y158.118 E.03562
G1 X127.013 Y157.85 E.03719
G1 X126.539 Y157.579 E.02075
G1 X126.056 Y157.138 E.02486
G1 X125.499 Y156.436 E.03406
G1 X125.153 Y155.827 E.02662
G1 X124.929 Y155.168 E.02645
G1 X124.834 Y154.278 E.03402
G1 X124.963 Y153.42 E.03298
G1 X125.287 Y152.609 E.03319
G1 X125.63 Y152.1 E.02333
G1 X131.643 Y152.1 E.22853
G1 X131.923 Y152.553 E.02024
M204 P2500
G1 X131.838 Y155.742 F9000
M204 P500
G1 F1800
G1 X131.838 Y153.52 E.08445
G1 X131.585 Y152.789 E.0294
G1 X131.414 Y152.511 E.0124
G1 X131.33 Y152.511 E.00319
G1 X131.33 Y156.259 E.14245
G1 X130.822 Y156.872 E.03026
G1 X130.822 Y152.511 E.16575
G1 X130.315 Y152.511 E.01927
G1 X130.315 Y157.244 E.17988
G1 X129.807 Y157.501 E.02164
G1 X129.807 Y152.511 E.18965
G1 X129.299 Y152.511 E.01931
G1 X129.299 Y157.644 E.19509
G1 X128.791 Y157.727 E.01956
G1 X128.791 Y152.511 E.19824
G1 X128.283 Y152.511 E.01931
G1 X128.283 Y157.714 E.19775
G1 X127.775 Y157.639 E.01952
G1 X127.775 Y152.511 E.1949
G1 X127.267 Y152.511 E.01931
G1 X127.267 Y157.494 E.18939
G1 X127.173 Y157.468 E.00371
G1 X126.76 Y157.224 E.01823
G1 X126.76 Y152.511 E.17912
G1 X126.252 Y152.511 E.01931
G1 X126.252 Y156.695 E.15902
G1 X125.744 Y156.034 E.03168
G1 X125.744 Y152.306 E.14169
M204 P2500
G1 E-1.2
G1 Z.6 F9000
G1 X140.305 Y87.116
G1 Z.2
G1 E1.2 F1800
M204 P500
;TYPE:Perimeter
;WIDTH:0.499999
G1 X140.124 Y87.275 E.00916
G1 X139.922 Y87.388 E.0088
G1 X139.444 Y87.556 E.01926
G1 X138.76 Y87.587 E.02602
G1 X138.531 Y87.548 E.00883
G1 X138.055 Y87.379 E.0192
G1 X137.502 Y86.973 E.02607
G1 X137.349 Y86.799 E.00881
G1 X137.085 Y86.371 E.01911
G1 X137.01 Y86.175 E.00798
G1 X136.904 Y85.68 E.01924
G1 X136.899 Y85.473 E.00787
G1 X136.967 Y84.955 E.01986
G1 X137.244 Y84.346 E.02543
G1 X137.382 Y84.159 E.00883
G1 X137.771 Y83.79 E.02038
G1 X138.359 Y83.499 E.02493
G1 X138.584 Y83.441 E.00883
G1 X139.121 Y83.397 E.02048
G1 X139.587 Y83.478 E.01798
G1 X140.079 Y83.697 E.02047
G1 X140.271 Y83.827 E.00881
G1 X140.66 Y84.204 E.02059
G1 X140.972 Y84.775 E.02473
G1 X141.04 Y84.996 E.00879
G1 X141.105 Y85.513 E.0198
G1 X140.99 Y86.175 E.02554
G1 X140.903 Y86.39 E.00881
G1 X140.632 Y86.828 E.01958
G1 X140.35 Y87.076 E.01427
M204 P2500
G1 X140.021 Y86.764 F9000
M204 P500
;TYPE:External perimeter
G1 F1800
G1 X139.837 Y86.919 E.00914
G1 X139.316 Y87.117 E.02118
G1 X138.76 Y87.13 E.02114
G1 X138.23 Y86.957 E.02119
G1 X137.789 Y86.617 E.02116
G1 X137.486 Y86.15 E.02116
G1 X137.356 Y85.608 E.02118
G1 X137.414 Y85.055 E.02113
G1 X137.653 Y84.551 E.0212
G1 X138.046 Y84.157 E.02115
G1 X138.548 Y83.916 E.02116
G1 X139.101 Y83.855 E.02114
G1 X139.452 Y83.916 E.01354
G1 X139.908 Y84.123 E.01903
G1 X140.301 Y84.489 E.02041
G1 X140.564 Y84.981 E.0212
G1 X140.647 Y85.531 E.02114
G1 X140.543 Y86.078 E.02116
G1 X140.262 Y86.559 E.02117
G1 X140.066 Y86.725 E.00976
M204 P2500
G1 E-1.2
G1 Z.6 F9000
G1 X162.311 Y71.773
G1 Z.2
G1 E1.2 F1800
M204 P500
G1 X162.659 Y71.282 E.02287
G1 X162.97 Y71.031 E.01519
G1 X163.488 Y70.797 E.0216
G1 X164.024 Y70.726 E.02055
G1 X164.494 Y70.79 E.01803
G1 X164.817 Y70.912 E.01312
G1 X165.285 Y71.228 E.02146
G1 X165.677 Y71.736 E.02439
G1 X165.877 Y72.346 E.0244