有3d gcode预览,基本能按要求切片,但是缩放后切片会失败
This commit is contained in:
@@ -2,27 +2,69 @@
|
||||
|
||||
{% 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">System Settings</h1>
|
||||
<h1 class="h2">{{ _('System Settings') }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5>CuraEngine Configurations</h5>
|
||||
<h5>{{ _('CuraEngine Configurations') }}</h5>
|
||||
<hr>
|
||||
<form method="POST" action="{{ url_for('admin.settings') }}">
|
||||
<div class="mb-3">
|
||||
<label for="offset_x" class="form-label">Plater Origin Offset X (mm)</label>
|
||||
<label for="offset_x" class="form-label">{{ _('Plater Origin Offset X (mm)') }}</label>
|
||||
<input type="number" class="form-control" name="offset_x" id="offset_x" value="{{ configs.get('offset_x', '0') }}">
|
||||
<div class="form-text">Adjust the X-axis compilation offset for combined files on the build plate.</div>
|
||||
<div class="form-text">{{ _('Adjust the X-axis compilation offset for combined files on the build plate.') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="offset_y" class="form-label">Plater Origin Offset Y (mm)</label>
|
||||
<label for="offset_y" class="form-label">{{ _('Plater Origin Offset Y (mm)') }}</label>
|
||||
<input type="number" class="form-control" name="offset_y" id="offset_y" value="{{ configs.get('offset_y', '0') }}">
|
||||
<div class="form-text">Adjust the Y-axis compilation offset for combined files on the build plate.</div>
|
||||
<div class="form-text">{{ _('Adjust the Y-axis compilation offset for combined files on the build plate.') }}</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||
<h5 class="mt-4">{{ _('Default Plater Settings') }}</h5>
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="default_infill" class="form-label">{{ _('Default Infill Density (%)') }}</label>
|
||||
<input type="number" class="form-control" name="default_infill" id="default_infill" value="{{ configs.get('default_infill', '20') }}" min="0" max="100">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="default_support" class="form-label">{{ _('Default Support') }}</label>
|
||||
<select class="form-select" name="default_support" id="default_support">
|
||||
<option value="false" {% if configs.get('default_support', 'false') == 'false' %}selected{% endif %}>{{ _('None') }}</option>
|
||||
<option value="buildplate" {% if configs.get('default_support', 'false') == 'buildplate' %}selected{% endif %}>{{ _('Touching Buildplate') }}</option>
|
||||
<option value="true" {% if configs.get('default_support', 'false') == 'true' %}selected{% endif %}>{{ _('Everywhere') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="default_support_pattern" class="form-label">{{ _('Default Support Type') }}</label>
|
||||
<select class="form-select" name="default_support_pattern" id="default_support_pattern">
|
||||
<option value="tree" {% if configs.get('default_support_pattern', 'tree') == 'tree' %}selected{% endif %}>{{ _('Tree') }}</option>
|
||||
<option value="lines" {% if configs.get('default_support_pattern', 'tree') == 'lines' %}selected{% endif %}>{{ _('Lines') }}</option>
|
||||
<option value="grid" {% if configs.get('default_support_pattern', 'tree') == 'grid' %}selected{% endif %}>{{ _('Grid') }}</option>
|
||||
<option value="triangles" {% if configs.get('default_support_pattern', 'tree') == 'triangles' %}selected{% endif %}>{{ _('Triangles') }}</option>
|
||||
<option value="concentric" {% if configs.get('default_support_pattern', 'tree') == 'concentric' %}selected{% endif %}>{{ _('Concentric') }}</option>
|
||||
<option value="zigzag" {% if configs.get('default_support_pattern', 'tree') == 'zigzag' %}selected{% endif %}>{{ _('Zig Zag') }}</option>
|
||||
<option value="cross" {% if configs.get('default_support_pattern', 'tree') == 'cross' %}selected{% endif %}>{{ _('Cross') }}</option>
|
||||
<option value="gyroid" {% if configs.get('default_support_pattern', 'tree') == 'gyroid' %}selected{% endif %}>{{ _('Gyroid') }}</option>
|
||||
<option value="honeycomb" {% if configs.get('default_support_pattern', 'tree') == 'honeycomb' %}selected{% endif %}>{{ _('Honeycomb') }}</option>
|
||||
<option value="octagon" {% if configs.get('default_support_pattern', 'tree') == 'octagon' %}selected{% endif %}>{{ _('Octagon') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="default_quality" class="form-label">{{ _('Default Quality Profile') }}</label>
|
||||
<select class="form-select" name="default_quality" id="default_quality">
|
||||
{% for key, name in presets %}
|
||||
<option value="{{ key }}" {% if configs.get('default_quality', 'base_global_standard.inst.cfg') == key %}selected{% endif %}>{{ _(name) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">{{ _('Save Settings') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
{% 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">User Management</h1>
|
||||
<h1 class="h2">{{ _('User Management') }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Created At</th>
|
||||
<th>Actions</th>
|
||||
<th>{{ _('ID') }}</th>
|
||||
<th>{{ _('Username') }}</th>
|
||||
<th>{{ _('Role') }}</th>
|
||||
<th>{{ _('Created At') }}</th>
|
||||
<th>{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -23,16 +23,18 @@
|
||||
<td>{{ user.username }}</td>
|
||||
<td>
|
||||
{% if user.is_admin %}
|
||||
<span class="badge bg-danger">Admin</span>
|
||||
<span class="badge bg-danger">{{ _('Admin') }}</span>
|
||||
{% elif user.is_guest %}
|
||||
<span class="badge bg-secondary">Guest</span>
|
||||
<span class="badge bg-secondary">{{ _('Guest') }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">User</span>
|
||||
<span class="badge bg-primary">{{ _('User') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" {% if user.id == current_user.id %}disabled{% endif %}>Delete</button>
|
||||
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="POST" class="d-inline" onsubmit="return confirm('{{ _('WARNING: Are you sure you want to permanently delete this user AND ALL their uploaded files and G-codes?') }}');">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" {% if user.id == current_user.id %}disabled{% endif %}>{{ _('Delete') }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -2,21 +2,470 @@
|
||||
|
||||
{% 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">GCode Preview: {{ file.original_filename }}</h1>
|
||||
<a href="{{ url_for('main.files') }}" class="btn btn-secondary btn-sm">Back to Files</a>
|
||||
<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 class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-info text-dark d-flex justify-content-between">
|
||||
<span>File Info</span>
|
||||
<span>Total Lines: {{ line_count }}</span>
|
||||
<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>
|
||||
<div class="card-body">
|
||||
<p class="card-text text-muted mb-1">Below is a text preview of the generated GCode (first 500 lines).</p>
|
||||
<pre class="bg-dark text-light p-3 rounded" style="max-height: 500px; overflow-y: auto; font-size: 13px;"><code>{{ content }}</code></pre>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary">Download Full GCode File</a>
|
||||
|
||||
<!-- 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 %}
|
||||
@@ -1,17 +1,46 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
/* 防止整个大页面滚动 */
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<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-grid-3x3 me-2 text-primary"></i>{{ _('Plater / Build Plate') }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="row" style="height: 70vh;">
|
||||
<div class="row" style="height: calc(100vh - 140px);">
|
||||
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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;">
|
||||
<div class="bg-white rounded shadow-sm p-3 opacity-90 border border-secondary" style="width: 170px; pointer-events: auto;">
|
||||
<h6 class="fs-6 mb-2 text-primary"><i class="bi bi-arrows-angle-expand me-1"></i>{{ _('Scale') }}</h6>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text bg-danger text-white border-danger fw-bold opacity-75" style="width: 32px;">X</span>
|
||||
<input type="number" class="form-control" id="scale-x" value="1.0" step="0.1" onchange="applyScaleInput('x')">
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text bg-success text-white border-success fw-bold opacity-75" style="width: 32px;">Y</span>
|
||||
<input type="number" class="form-control" id="scale-y" value="1.0" step="0.1" onchange="applyScaleInput('y')">
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<span class="input-group-text bg-primary text-white border-primary fw-bold opacity-75" style="width: 32px;">Z</span>
|
||||
<input type="number" class="form-control" id="scale-z" value="1.0" step="0.1" onchange="applyScaleInput('z')">
|
||||
</div>
|
||||
<div class="form-check form-switch small mb-0 mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="scale-uniform" checked>
|
||||
<label class="form-check-label user-select-none" for="scale-uniform">{{ _('Uniform Scale') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="position-absolute top-50 start-0 translate-middle-y ms-3 p-2 bg-white rounded shadow-sm d-flex flex-column gap-2 opacity-75" style="z-index: 10;">
|
||||
<button class="btn btn-primary btn-sm rounded" id="btn-translate" title="{{ _('Translate (W)') }}" onclick="setTransformMode('translate')"><i class="bi bi-arrows-move"></i></button>
|
||||
<button class="btn btn-outline-secondary btn-sm rounded" id="btn-rotate" title="{{ _('Rotate (E)') }}" onclick="setTransformMode('rotate')"><i class="bi bi-arrow-clockwise"></i></button>
|
||||
@@ -30,7 +59,7 @@
|
||||
<i class="bi bi-chevron-bar-contract"></i>
|
||||
</div>
|
||||
<div id="collapseModels" class="collapse show">
|
||||
<div class="list-group list-group-flush" id="model-list" style="max-height: 250px; overflow-y: auto;">
|
||||
<div class="list-group list-group-flush" id="model-list" style="min-height: 160px; max-height: max(250px, 35vh); overflow-y: auto;">
|
||||
{% for model in models %}
|
||||
<button id="add-model-btn-{{ model.id }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-matrix="{{ model.transform_matrix or '' }}" onclick="addModelToPlate(this, {{ model.id }}, '{{ model.url }}', '{{ model.name }}', '{{ model.status }}')">
|
||||
<span class="text-truncate">{{ model.name }}</span>
|
||||
@@ -52,24 +81,29 @@
|
||||
<div class="card-body py-2">
|
||||
<div class="mb-2">
|
||||
<label for="infill-density" class="form-label text-secondary small mb-1">{{ _('Infill Density') }} (%)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="infill-density" value="20" min="0" max="100">
|
||||
<input type="number" class="form-control form-control-sm" id="infill-density" value="{{ default_infill }}" min="0" max="100">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="support-type" class="form-label text-secondary small mb-1">{{ _('Support') }}</label>
|
||||
<select class="form-select form-select-sm" id="support-type">
|
||||
<option value="false">{{ _('None') }}</option>
|
||||
<option value="buildplate">{{ _('Touching Buildplate') }}</option>
|
||||
<option value="true">{{ _('Everywhere') }}</option>
|
||||
<option value="false" {% if default_support == 'false' %}selected{% endif %}>{{ _('None') }}</option>
|
||||
<option value="buildplate" {% if default_support == 'buildplate' %}selected{% endif %}>{{ _('Touching Buildplate') }}</option>
|
||||
<option value="true" {% if default_support == 'true' %}selected{% endif %}>{{ _('Everywhere') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="support-pattern" class="form-label text-secondary small mb-1">{{ _('Support Type') }}</label>
|
||||
<select class="form-select form-select-sm" id="support-pattern" disabled>
|
||||
<option value="lines">{{ _('Lines') }} ({{ _('默认线状') }})</option>
|
||||
<option value="grid">{{ _('Grid') }} ({{ _('网格状') }})</option>
|
||||
<option value="triangles">{{ _('Triangles') }} ({{ _('三角网') }})</option>
|
||||
<option value="zigzag">{{ _('ZigZag') }} ({{ _('之字形') }})</option>
|
||||
<option value="tree">{{ _('Tree') }} ({{ _('树状') }})</option>
|
||||
<select class="form-select form-select-sm" id="support-pattern" {% if default_support == 'false' %}disabled{% endif %}>
|
||||
<option value="tree" {% if default_support_pattern == 'tree' %}selected{% endif %}>{{ _('Tree') }}</option>
|
||||
<option value="lines" {% if default_support_pattern == 'lines' %}selected{% endif %}>{{ _('Lines') }}</option>
|
||||
<option value="grid" {% if default_support_pattern == 'grid' %}selected{% endif %}>{{ _('Grid') }}</option>
|
||||
<option value="triangles" {% if default_support_pattern == 'triangles' %}selected{% endif %}>{{ _('Triangles') }}</option>
|
||||
<option value="concentric" {% if default_support_pattern == 'concentric' %}selected{% endif %}>{{ _('Concentric') }}</option>
|
||||
<option value="zigzag" {% if default_support_pattern == 'zigzag' %}selected{% endif %}>{{ _('Zig Zag') }}</option>
|
||||
<option value="cross" {% if default_support_pattern == 'cross' %}selected{% endif %}>{{ _('Cross') }}</option>
|
||||
<option value="gyroid" {% if default_support_pattern == 'gyroid' %}selected{% endif %}>{{ _('Gyroid') }}</option>
|
||||
<option value="honeycomb" {% if default_support_pattern == 'honeycomb' %}selected{% endif %}>{{ _('Honeycomb') }}</option>
|
||||
<option value="octagon" {% if default_support_pattern == 'octagon' %}selected{% endif %}>{{ _('Octagon') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,6 +174,7 @@ let offsetX = {{ offset_x|default(0) }};
|
||||
let offsetY = {{ offset_y|default(0) }};
|
||||
let loadedModels = [];
|
||||
let activeModel = null;
|
||||
const initialAddId = new URLSearchParams(window.location.search).get('add');
|
||||
|
||||
initPlater();
|
||||
animate();
|
||||
@@ -220,7 +255,8 @@ function initPlater() {
|
||||
|
||||
// Controls
|
||||
orbit = new THREE.OrbitControls(camera, renderer.domElement);
|
||||
orbit.enableDamping = true;
|
||||
orbit.enableDamping = false;
|
||||
orbit.mouseButtons.MIDDLE = THREE.MOUSE.PAN;
|
||||
orbit.target.set(0, 0, 0);
|
||||
|
||||
transformProxy = new THREE.Object3D();
|
||||
@@ -228,6 +264,11 @@ function initPlater() {
|
||||
|
||||
transformControl = new THREE.TransformControls(camera, renderer.domElement);
|
||||
transformControl.setSpace('world');
|
||||
transformControl.addEventListener('change', function () {
|
||||
if (transformControl.getMode() === 'scale' && !document.getElementById('scale-panel').classList.contains('d-none')) {
|
||||
updateScalePanel();
|
||||
}
|
||||
});
|
||||
transformControl.addEventListener('dragging-changed', function (event) {
|
||||
orbit.enabled = !event.value;
|
||||
if (!event.value && activeModel) {
|
||||
@@ -264,6 +305,13 @@ function setTransformMode(mode) {
|
||||
document.getElementById('btn-rotate').className = mode === 'rotate' ? 'btn btn-primary btn-sm rounded' : 'btn btn-outline-secondary btn-sm rounded';
|
||||
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) {
|
||||
document.getElementById('scale-panel').classList.remove('d-none');
|
||||
updateScalePanel();
|
||||
} else {
|
||||
document.getElementById('scale-panel').classList.add('d-none');
|
||||
}
|
||||
} else {
|
||||
layFlatMode = true;
|
||||
document.getElementById('btn-translate').className = 'btn btn-outline-secondary btn-sm rounded';
|
||||
@@ -271,9 +319,47 @@ function setTransformMode(mode) {
|
||||
document.getElementById('btn-scale').className = 'btn btn-outline-secondary btn-sm rounded';
|
||||
document.getElementById('btn-layflat').className = 'btn btn-info btn-sm rounded text-white';
|
||||
transformControl.detach();
|
||||
document.getElementById('scale-panel').classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function applyScaleInput(axis) {
|
||||
if (!activeModel) return;
|
||||
let val = parseFloat(document.getElementById('scale-' + axis).value);
|
||||
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;
|
||||
}
|
||||
|
||||
activeModel.updateMatrixWorld(true);
|
||||
|
||||
// re-attach proxy pivot logic without modifying the actual spatial scale
|
||||
transformProxy.scale.set(1, 1, 1);
|
||||
transformProxy.attach(activeModel);
|
||||
|
||||
updateScalePanel();
|
||||
}
|
||||
|
||||
function removeActiveModel() {
|
||||
if (activeModel) {
|
||||
removeModel(activeModel);
|
||||
@@ -363,7 +449,12 @@ function selectModel(model) {
|
||||
transformProxy.scale.set(1, 1, 1);
|
||||
transformProxy.attach(model);
|
||||
transformControl.attach(transformProxy);
|
||||
if(transformControl.getMode() === 'scale') {
|
||||
document.getElementById('scale-panel').classList.remove('d-none');
|
||||
updateScalePanel();
|
||||
}
|
||||
} else {
|
||||
document.getElementById('scale-panel').classList.add('d-none');
|
||||
transformControl.detach();
|
||||
}
|
||||
}
|
||||
@@ -500,7 +591,9 @@ function mergeAndSlice() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadedModels.length === 1) {
|
||||
let isEdit = (loadedModels.length === 1 && String(loadedModels[0].userData.fileId) === String(initialAddId));
|
||||
|
||||
if (isEdit) {
|
||||
const singleModel = loadedModels[0];
|
||||
if (singleModel.userData.status === 'sliced') {
|
||||
if (!confirm("{{ _('This model has already been sliced. The existing GCode will be overwritten. Continue?') }}")) {
|
||||
@@ -543,21 +636,21 @@ function mergeAndSlice() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ pieces: pieces, quality: quality, infill: infill, support: support, support_pattern: supportPattern })
|
||||
body: JSON.stringify({ pieces: pieces, quality: quality, infill: infill, support: support, support_pattern: supportPattern, is_edit: isEdit })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
window.location.href = "{{ url_for('main.files') }}";
|
||||
} else {
|
||||
alert("Error: " + data.error);
|
||||
alert("{{ _('Error:') }} " + data.error);
|
||||
btn.disabled = false;
|
||||
icon.className = 'bi bi-gear-fill me-2';
|
||||
text.innerText = '{{ _("Merge & Slice") }}';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert("Error: " + String(err));
|
||||
alert("{{ _('Error:') }} " + String(err));
|
||||
btn.disabled = false;
|
||||
icon.className = 'bi bi-gear-fill me-2';
|
||||
text.innerText = '{{ _("Merge & Slice") }}';
|
||||
|
||||
Reference in New Issue
Block a user