基础的切片和质量控制

This commit is contained in:
2026-04-10 13:58:18 +08:00
commit 975f06eb46
3302 changed files with 650758 additions and 0 deletions

234
app/templates/slice.html Normal file
View File

@@ -0,0 +1,234 @@
{% 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-cloud-arrow-up me-2 text-primary"></i>{{ _('Upload & Slice STL') }}</h1>
</div>
<div class="row">
<div class="col-md-6 mb-4 mb-md-0">
<div class="card shadow-sm h-100">
<div class="card-body p-4">
<form id="upload-form" method="POST" enctype="multipart/form-data">
<div class="mb-4">
<label class="form-label fw-bold text-secondary">{{ _('Select STL File') }}</label>
<div id="drop-zone" class="border rounded p-4 text-center position-relative" style="border: 2px dashed #0d6efd !important; cursor: pointer; transition: all 0.3s ease; background-color: #f8f9fa;">
<i class="bi bi-cloud-arrow-up display-4 text-primary mb-2"></i>
<p class="mt-2 text-secondary fw-bold mb-0" id="drop-text">{{ _('Drag & Drop STL file here or Click to Select') }}</p>
<input class="form-control position-absolute w-100 h-100 top-0 start-0 opacity-0" type="file" id="file" name="file" accept=".stl" style="cursor: pointer;" required>
</div>
</div>
<div id="progress-container" class="mb-4 d-none">
<div class="d-flex justify-content-between mb-1">
<span class="text-secondary fw-bold small" id="progress-text">{{ _('Uploading...') }}</span>
<span class="text-primary fw-bold small" id="progress-percent">0%</span>
</div>
<div class="progress rounded-pill" style="height: 10px;">
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<!-- Here you can add slice configurations -->
<div class="mb-4">
<label for="quality" class="form-label fw-bold text-secondary">{{ _('Quality Profile') }}</label>
<select class="form-select bg-light" id="quality" name="quality">
{% for key, name in presets %}
<option value="{{ key }}" {% if key == last_quality %}selected{% endif %}>{{ _(name) }}</option>
{% endfor %}
</select>
</div>
<button type="submit" id="submit-btn" class="btn btn-success fw-bold px-4 py-2 w-100 shadow-sm"><i class="bi bi-gear-fill me-2" id="submit-icon"></i><span id="submit-text">{{ _('Upload & Slice') }}</span></button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm h-100">
<div class="card-body p-0 position-relative">
<div id="stl_viewer_container" style="height: 400px; width: 100%; border-radius: 0.375rem; overflow: hidden; background: #f8f9fa;">
<!-- STL Viewer Integration Point -->
<div id="viewer_placeholder" class="text-muted text-center position-absolute top-50 start-50 translate-middle">
<i class="bi bi-box display-1 text-secondary opacity-50 mb-3 d-block"></i>
<h5>{{ _('3D Preview Area') }}</h5><small>{{ _('Upload a file to display') }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Three.js + STLLoader + OrbitControls -->
<script src="{{ url_for('static', filename='js/three.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/OrbitControls.js') }}"></script>
<script src="{{ url_for('static', filename='js/STLLoader.js') }}"></script>
<script>
const fileInput = document.getElementById('file');
const dropZone = document.getElementById('drop-zone');
const dropText = document.getElementById('drop-text');
const uploadForm = document.getElementById('upload-form');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressPercent = document.getElementById('progress-percent');
const submitBtn = document.getElementById('submit-btn');
const submitIcon = document.getElementById('submit-icon');
const submitText = document.getElementById('submit-text');
function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.style.backgroundColor = '#e9ecef';
dropZone.style.borderColor = '#0b5ed7';
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.style.backgroundColor = '#f8f9fa';
dropZone.style.borderColor = '#0d6efd';
}, false);
});
dropZone.addEventListener('drop', e => {
if(e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
fileInput.dispatchEvent(new Event('change'));
}
});
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if(!file) return;
dropText.innerText = file.name;
document.getElementById('viewer_placeholder').style.display = 'none';
const reader = new FileReader();
reader.onload = function(event) {
initViewer(event.target.result);
};
reader.readAsArrayBuffer(file);
});
uploadForm.addEventListener('submit', function(e) {
e.preventDefault();
const file = fileInput.files[0];
if(!file) return;
const formData = new FormData(uploadForm);
progressContainer.classList.remove('d-none');
submitBtn.disabled = true;
submitIcon.className = 'spinner-border spinner-border-sm me-2';
submitText.innerText = '{{ _("Uploading...") }}';
const xhr = new XMLHttpRequest();
xhr.open('POST', window.location.href, true);
xhr.upload.onprogress = function(e) {
if(e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percent + '%';
progressBar.setAttribute('aria-valuenow', percent);
progressPercent.innerText = percent + '%';
}
};
xhr.onload = function() {
if(xhr.status >= 200 && xhr.status < 300) {
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.remove('progress-bar-striped');
submitText.innerText = '{{ _("Slicing queued!") }}';
window.location.href = "{{ url_for('main.files') }}";
} else {
alert('Error: ' + xhr.statusText);
resetUploadState();
}
};
xhr.onerror = function() {
alert('Upload failed');
resetUploadState();
};
xhr.send(formData);
});
function resetUploadState() {
progressContainer.classList.add('d-none');
submitBtn.disabled = false;
submitIcon.className = 'bi bi-gear-fill me-2';
submitText.innerText = '{{ _("Upload & Slice") }}';
progressBar.style.width = '0%';
progressPercent.innerText = '0%';
}
let scene, camera, renderer, controls;
function initViewer(data) {
const container = document.getElementById('stl_viewer_container');
// Clear previous if any
container.innerHTML = '';
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xf8f9fa );
// Setup camera
camera = new THREE.PerspectiveCamera( 45, container.clientWidth / container.clientHeight, 1, 1000 );
camera.position.set( 0, -150, 150 );
// Setup renderer
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( container.clientWidth, container.clientHeight );
container.appendChild( renderer.domElement );
// Add lighting
scene.add( new THREE.AmbientLight( 0x777777 ) );
const directionalLight = new THREE.DirectionalLight( 0xffffff, 1 );
directionalLight.position.set( 1, 1, 2 );
scene.add( directionalLight );
// Load STL
const loader = new THREE.STLLoader();
const geometry = loader.parse( data );
geometry.computeBoundingBox();
const center = geometry.boundingBox.getCenter(new THREE.Vector3());
geometry.center();
const material = new THREE.MeshPhongMaterial( { color: 0x0d6efd, specular: 0x111111, shininess: 200 } );
const mesh = new THREE.Mesh( geometry, material );
// Optional: scale model to fit view automatically
const boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());
const radius = boundingSphere.radius;
camera.position.set(0, -radius * 2, radius * 2);
scene.add( mesh );
// Add controls
controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.target.set(0,0,0);
controls.update();
animate();
}
function animate() {
requestAnimationFrame( animate );
controls.update();
renderer.render( scene, camera );
}
// Handle window resize dynamically inside container context
window.addEventListener('resize', function() {
if(camera && renderer) {
const container = document.getElementById('stl_viewer_container');
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize( container.clientWidth, container.clientHeight );
}
});
</script>
{% endblock %}