基础的切片和质量控制
This commit is contained in:
234
app/templates/slice.html
Normal file
234
app/templates/slice.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user