diff --git a/src/lib/components/STLViewer.svelte b/src/lib/components/STLViewer.svelte index b1b1bad..9c3c748 100644 --- a/src/lib/components/STLViewer.svelte +++ b/src/lib/components/STLViewer.svelte @@ -3,6 +3,7 @@ import * as THREE from "three"; import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js"; import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js"; + import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; interface Props { @@ -95,6 +96,8 @@ if (extension === "obj") { loadOBJ(); + } else if (extension === "gltf" || extension === "glb") { + loadGLTF(); } else { loadSTL(); } @@ -161,6 +164,56 @@ ); } + function loadGLTF() { + const loader = new GLTFLoader(); + + loader.load( + modelPath, + (gltf) => { + const model = gltf.scene; + + // Center the object + const box = new THREE.Box3().setFromObject(model); + const center = box.getCenter(new THREE.Vector3()); + model.position.sub(center); + + // Scale to fit + const size = box.getSize(new THREE.Vector3()); + const maxDim = Math.max(size.x, size.y, size.z); + const scale = 50 / maxDim; + model.scale.set(scale, scale, scale); + + // glTF models may have their own materials, apply default if missing + model.traverse((child) => { + if (child instanceof THREE.Mesh) { + if ( + !child.material || + (child.material as THREE.Material).type === + "MeshBasicMaterial" + ) { + child.material = new THREE.MeshPhongMaterial({ + color: 0x3b82f6, + specular: 0x111111, + shininess: 50, + }); + } + } + }); + + scene.add(model); + + // Position camera + const distance = maxDim * scale * 2.5; + camera.position.set(distance, distance, distance); + controls.update(); + }, + undefined, + (err) => { + console.error("Error loading glTF:", err); + }, + ); + } + function addGeometryToScene(geometry: THREE.BufferGeometry) { // Center the geometry geometry.computeBoundingBox(); diff --git a/src/lib/components/prints/EditPrintModal.svelte b/src/lib/components/prints/EditPrintModal.svelte index 060403f..6fbbb5c 100644 --- a/src/lib/components/prints/EditPrintModal.svelte +++ b/src/lib/components/prints/EditPrintModal.svelte @@ -101,8 +101,12 @@ stlFile = input.files[0]; uploadStatus = stlFile.name; uploadProgress = 0; + removeModel = false; // Reset remove flag if selecting new file } } + + // Track if user wants to remove the model + let removeModel = $state(false); @@ -121,6 +125,11 @@ } } + // Handle model removal + if (removeModel) { + formData.set("remove_model", "true"); + } + // Convert hours + minutes to total minutes const hours = Number(formData.get("duration_hours") || 0); const mins = Number(formData.get("duration_mins") || 0); @@ -222,22 +231,53 @@ 3D Model {print.stl_file ? "" : "(Optional)"} - {#if print.stl_file && browser && !stlFile} + {#if print.stl_file && browser && !stlFile && !removeModel} -
- {#await import("$lib/components/STLViewer.svelte") then { default: STLViewer }} - - {/await} +
+
+ {#await import("$lib/components/STLViewer.svelte") then { default: STLViewer }} + + {/await} +
+ +

Click below to replace with a new model

+ {:else if removeModel && print.stl_file} + +
+ +

+ Model will be removed when you save +

+ +
{/if} @@ -280,7 +320,7 @@
diff --git a/src/lib/components/prints/LogPrintModal.svelte b/src/lib/components/prints/LogPrintModal.svelte index 9f71b92..7274dc5 100644 --- a/src/lib/components/prints/LogPrintModal.svelte +++ b/src/lib/components/prints/LogPrintModal.svelte @@ -239,7 +239,7 @@ diff --git a/src/routes/api/upload-stl/+server.ts b/src/routes/api/upload-stl/+server.ts index 7047419..23c523a 100644 --- a/src/routes/api/upload-stl/+server.ts +++ b/src/routes/api/upload-stl/+server.ts @@ -5,7 +5,7 @@ import { existsSync } from 'fs'; import path from 'path'; const UPLOAD_DIR = 'static/uploads/models'; -const ALLOWED_EXTENSIONS = ['.stl', '.obj']; +const ALLOWED_EXTENSIONS = ['.stl', '.obj', '.gltf', '.glb']; export const POST: RequestHandler = async ({ request, locals }) => { if (!locals.user) { @@ -24,7 +24,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { const extension = '.' + fileName.split('.').pop(); if (!ALLOWED_EXTENSIONS.includes(extension)) { - throw error(400, 'Only STL and OBJ files are allowed'); + throw error(400, 'Only STL, OBJ, GLTF, and GLB files are allowed'); } // Create upload directory if it doesn't exist diff --git a/src/routes/prints/+page.server.ts b/src/routes/prints/+page.server.ts index 9e7504d..0f1c951 100644 --- a/src/routes/prints/+page.server.ts +++ b/src/routes/prints/+page.server.ts @@ -5,6 +5,9 @@ import { User } from '$lib/models/User'; import { connectDB } from '$lib/server/db'; import type { PageServerLoad, Actions } from './$types'; import { fail, redirect } from '@sveltejs/kit'; +import { unlink } from 'fs/promises'; +import { existsSync } from 'fs'; +import path from 'path'; export const load: PageServerLoad = async ({ locals }) => { if (!locals.user) throw redirect(303, '/login'); @@ -142,6 +145,7 @@ export const actions: Actions = { const printer_id = formData.get('printer_id'); const spool_id = formData.get('spool_id'); const stl_file = formData.get('stl_file'); + const remove_model = formData.get('remove_model'); if (!id || !name) { return fail(400, { missing: true }); @@ -218,9 +222,31 @@ export const actions: Actions = { if (spool_id) { updateData.spool_id = spool_id; } - // Update STL file if provided + // Handle STL file: update if new one provided, or remove if requested if (stl_file) { + // If replacing an existing model, delete the old file + if (printJob.stl_file) { + const oldFilePath = path.join('static', printJob.stl_file); + if (existsSync(oldFilePath)) { + try { + await unlink(oldFilePath); + } catch (e) { + console.error('Failed to delete old model file:', e); + } + } + } updateData.stl_file = stl_file; + } else if (remove_model === 'true' && printJob.stl_file) { + // Delete the file from disk + const filePath = path.join('static', printJob.stl_file); + if (existsSync(filePath)) { + try { + await unlink(filePath); + } catch (e) { + console.error('Failed to delete model file:', e); + } + } + updateData.stl_file = null; } await PrintJob.findOneAndUpdate( @@ -246,6 +272,21 @@ export const actions: Actions = { await connectDB(); try { + // Find the print first to get the model file path + const printJob = await PrintJob.findOne({ _id: id, user_id: locals.user.id }); + + if (printJob?.stl_file) { + // Delete the model file from disk + const filePath = path.join('static', printJob.stl_file); + if (existsSync(filePath)) { + try { + await unlink(filePath); + } catch (e) { + console.error('Failed to delete model file:', e); + } + } + } + await PrintJob.findOneAndDelete({ _id: id, user_id: locals.user.id }); return { success: true }; } catch (error) {