235 lines
9.8 KiB
HTML
235 lines
9.8 KiB
HTML
{% extends 'base.html' %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
|
<h1 class="h2"><i class="bi bi-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 %}
|