diff --git a/backend/calculator.py b/backend/calculator.py new file mode 100644 index 0000000..da164cd --- /dev/null +++ b/backend/calculator.py @@ -0,0 +1,63 @@ +import trimesh +import sys + +def calcola_volumi(volume_totale, superficie, wall_line_width=0.4, wall_line_count=3, + layer_height=0.2, infill_percentage=0.15): + # Volume perimetrale stimato = superficie * spessore parete + spessore_parete = wall_line_width * wall_line_count + volume_pareti = superficie * spessore_parete + + # Volume interno (infill) + volume_infill = (volume_totale - volume_pareti) * infill_percentage + volume_effettivo = volume_pareti + max(volume_infill, 0) + + return volume_effettivo, volume_pareti, max(volume_infill, 0) + +def calcola_peso(volume_mm3, densita_g_cm3=1.24): + densita_g_mm3 = densita_g_cm3 / 1000 + return volume_mm3 * densita_g_mm3 + +def calcola_costo(peso_g, prezzo_kg=20.0): + return round((peso_g / 1000) * prezzo_kg, 2) + +def stima_tempo(volume_mm3 ): + velocita_mm3_min = 0.4 *0.2 * 100 *60 # mm/s * mm * 60 s/min + tempo_minuti = volume_mm3 / velocita_mm3_min + return round(tempo_minuti, 1) + +def main(percorso_stl): + try: + mesh = trimesh.load(percorso_stl) + volume_modello = mesh.volume + superficie = mesh.area + + volume_stampa, volume_pareti, volume_infill = calcola_volumi( + volume_totale=volume_modello, + superficie=superficie, + wall_line_width=0.4, + wall_line_count=3, + layer_height=0.2, + infill_percentage=0.15 + ) + + peso = calcola_peso(volume_stampa) + costo = calcola_costo(peso) + tempo = stima_tempo(volume_stampa) + + print(f"Volume STL: {volume_modello:.2f} mm³") + print(f"Superficie esterna: {superficie:.2f} mm²") + print(f"Volume stimato pareti: {volume_pareti:.2f} mm³") + print(f"Volume stimato infill: {volume_infill:.2f} mm³") + print(f"Volume totale da stampare: {volume_stampa:.2f} mm³") + print(f"Peso stimato: {peso:.2f} g") + print(f"Costo stimato: CHF {costo}") + print(f"Tempo stimato: {tempo} min") + + except Exception as e: + print("Errore durante l'elaborazione:", e) + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Uso: python calcolatore_stl.py modello.stl") + else: + main(sys.argv[1]) diff --git a/backend/main.py b/backend/main.py index 9b62b72..6ad2c59 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,16 +1,72 @@ -from fastapi import FastAPI +import io +import trimesh +from fastapi import FastAPI, UploadFile, File, HTTPException from fastapi.middleware.cors import CORSMiddleware app = FastAPI() app.add_middleware( CORSMiddleware, - allow_origins=["*"], # oppure ["http://localhost:4200"] se vuoi essere più restrittivo + allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) -@app.get("/api/hello") -def hello(): - return {"message": "Hello from Python API"} +def calculate_volumes(total_volume_mm3: float, + surface_area_mm2: float, + nozzle_width_mm: float = 0.4, + wall_line_count: int = 3, + layer_height_mm: float = 0.2, + infill_fraction: float = 0.15): + wall_thickness_mm = nozzle_width_mm * wall_line_count + wall_volume_mm3 = surface_area_mm2 * wall_thickness_mm + infill_volume_mm3 = max(total_volume_mm3 - wall_volume_mm3, 0) * infill_fraction + total_print_volume_mm3 = wall_volume_mm3 + infill_volume_mm3 + return total_print_volume_mm3, wall_volume_mm3, infill_volume_mm3 + +def calculate_weight(volume_mm3: float, density_g_cm3: float = 1.24): + density_g_mm3 = density_g_cm3 / 1000.0 + return volume_mm3 * density_g_mm3 + +def calculate_cost(weight_g: float, price_per_kg: float = 20.0): + return round((weight_g / 1000.0) * price_per_kg, 2) + +def estimate_time(volume_mm3: float, + nozzle_width_mm: float = 0.4, + layer_height_mm: float = 0.2, + print_speed_mm_per_s: float = 100.0): + volumetric_speed_mm3_per_min = nozzle_width_mm * layer_height_mm * print_speed_mm_per_s * 60.0 + return round(volume_mm3 / volumetric_speed_mm3_per_min, 1) + +@app.post("/calculate/stl") +async def calculate_from_stl(file: UploadFile = File(...)): + if not file.filename.lower().endswith(".stl"): + raise HTTPException(status_code=400, detail="Please upload an STL file.") + try: + contents = await file.read() + mesh = trimesh.load(io.BytesIO(contents), file_type="stl") + model_volume_mm3 = mesh.volume + model_surface_area_mm2 = mesh.area + + print_volume, wall_volume, infill_volume = calculate_volumes( + total_volume_mm3=model_volume_mm3, + surface_area_mm2=model_surface_area_mm2 + ) + + weight_g = calculate_weight(print_volume) + cost_chf = calculate_cost(weight_g) + time_min = estimate_time(print_volume) + + return { + "stl_volume_mm3": round(model_volume_mm3, 2), + "surface_area_mm2": round(model_surface_area_mm2, 2), + "wall_volume_mm3": round(wall_volume, 2), + "infill_volume_mm3": round(infill_volume, 2), + "print_volume_mm3": round(print_volume, 2), + "weight_g": round(weight_g, 2), + "cost_chf": cost_chf, + "time_min": time_min + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/frontend/angular.json b/frontend/angular.json index dbbad04..9111a06 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -32,6 +32,7 @@ } ], "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", "src/styles.scss" ], "scripts": [] @@ -91,6 +92,7 @@ } ], "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", "src/styles.scss" ], "scripts": [] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cb58fd2..eb81467 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,10 +8,12 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@angular/cdk": "^19.2.16", "@angular/common": "^19.2.0", "@angular/compiler": "^19.2.0", "@angular/core": "^19.2.0", "@angular/forms": "^19.2.0", + "@angular/material": "^19.2.16", "@angular/platform-browser": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0", "@angular/router": "^19.2.0", @@ -493,6 +495,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/@angular/cdk": { + "version": "19.2.16", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.16.tgz", + "integrity": "sha512-67nbWqoiZIBc8nEaCn7GHd02bM5T9qAbJ5w+Zq4V19CL3oCtrCrS4CV3Lsoi5HETSmn4iZcYS/Dph8omCvNkew==", + "license": "MIT", + "dependencies": { + "parse5": "^7.1.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/cli": { "version": "19.2.12", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.12.tgz", @@ -666,6 +683,23 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/material": { + "version": "19.2.16", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.2.16.tgz", + "integrity": "sha512-SSky/3MBOTdCBWOEffmVdnnKaCX6T4r3CqK2TJCLqWsHarPz5jovYIacfOe1RJzXijmDxXK5+VYhS64PNJaa6g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": "19.2.16", + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "@angular/forms": "^19.0.0 || ^20.0.0", + "@angular/platform-browser": "^19.0.0 || ^20.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/platform-browser": { "version": "19.2.11", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.11.tgz", @@ -11226,7 +11260,6 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -11267,7 +11300,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" diff --git a/frontend/package.json b/frontend/package.json index 16cd075..e61efc8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,10 +10,12 @@ }, "private": true, "dependencies": { + "@angular/cdk": "^19.2.16", "@angular/common": "^19.2.0", "@angular/compiler": "^19.2.0", "@angular/core": "^19.2.0", "@angular/forms": "^19.2.0", + "@angular/material": "^19.2.16", "@angular/platform-browser": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0", "@angular/router": "^19.2.0", @@ -34,4 +36,4 @@ "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.7.2" } -} +} \ No newline at end of file diff --git a/frontend/src/app/calculator/calculator.component.html b/frontend/src/app/calculator/calculator.component.html index 316f283..7475473 100644 --- a/frontend/src/app/calculator/calculator.component.html +++ b/frontend/src/app/calculator/calculator.component.html @@ -1,2 +1,37 @@ -

Calculator Component

-

{{ message }}

+ + 3D Print Cost Calculator + + + + + + + +

{{ error }}

+ +
+

Volume STL: {{ results.stl_volume_mm3 }} mm³

+

Superficie: {{ results.surface_area_mm2 }} mm²

+

Volume pareti: {{ results.wall_volume_mm3 }} mm³

+

Volume infill: {{ results.infill_volume_mm3 }} mm³

+

Volume stampa: {{ results.print_volume_mm3 }} mm³

+

Peso stimato: {{ results.weight_g }} g

+

Costo stimato: CHF {{ results.cost_chf }}

+

Tempo stimato: {{ results.time_min }} min

+
+
diff --git a/frontend/src/app/calculator/calculator.component.ts b/frontend/src/app/calculator/calculator.component.ts index 596576b..cd00238 100644 --- a/frontend/src/app/calculator/calculator.component.ts +++ b/frontend/src/app/calculator/calculator.component.ts @@ -1,20 +1,62 @@ -import { Component, OnInit } from '@angular/core'; +// calculator.component.ts +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @Component({ selector: 'app-calculator', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatCardModule, + MatFormFieldModule, + MatButtonModule, + MatProgressSpinnerModule + ], templateUrl: './calculator.component.html', - styleUrls: ['./calculator.component.css'] + styleUrls: ['./calculator.component.scss'] }) -export class CalculatorComponent implements OnInit { - message = ''; +export class CalculatorComponent { + file: File | null = null; + results: any = null; + error = ''; + loading = false; constructor(private http: HttpClient) {} - ngOnInit(): void { - this.http.get<{ message: string }>('http://localhost:8000/api/hello') - .subscribe(response => { - this.message = response.message; + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + this.file = input.files[0]; + this.results = null; + this.error = ''; + } + } + + uploadAndCalculate(): void { + if (!this.file) { + this.error = 'Seleziona un file STL prima di procedere.'; + return; + } + const formData = new FormData(); + formData.append('file', this.file); + this.loading = true; + this.http.post('http://localhost:8000/calculate/stl', formData) + .subscribe({ + next: res => { + this.results = res; + this.loading = false; + }, + error: err => { + this.error = err.error?.detail || err.message; + this.loading = false; + } }); } } diff --git a/frontend/src/index.html b/frontend/src/index.html index 3af61ec..d4f82fb 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -6,6 +6,8 @@ + + diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 90d4ee0..7e7239a 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1 +1,4 @@ /* You can add global styles to this file, and also import other style files */ + +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }