first try for calculate from stl

This commit is contained in:
2025-05-19 16:35:42 +02:00
parent b8bd3b0398
commit 00e62fc558
9 changed files with 255 additions and 18 deletions

63
backend/calculator.py Normal file
View File

@@ -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])

View File

@@ -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 from fastapi.middleware.cors import CORSMiddleware
app = FastAPI() app = FastAPI()
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], # oppure ["http://localhost:4200"] se vuoi essere più restrittivo allow_origins=["*"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
@app.get("/api/hello") def calculate_volumes(total_volume_mm3: float,
def hello(): surface_area_mm2: float,
return {"message": "Hello from Python API"} 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))

View File

@@ -32,6 +32,7 @@
} }
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": []
@@ -91,6 +92,7 @@
} }
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": []

View File

@@ -8,10 +8,12 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@angular/cdk": "^19.2.16",
"@angular/common": "^19.2.0", "@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0", "@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0", "@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0", "@angular/forms": "^19.2.0",
"@angular/material": "^19.2.16",
"@angular/platform-browser": "^19.2.0", "@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0", "@angular/router": "^19.2.0",
@@ -493,6 +495,21 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/@angular/cli": {
"version": "19.2.12", "version": "19.2.12",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.12.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.12.tgz",
@@ -666,6 +683,23 @@
"rxjs": "^6.5.3 || ^7.4.0" "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": { "node_modules/@angular/platform-browser": {
"version": "19.2.11", "version": "19.2.11",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.11.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.11.tgz",
@@ -11226,7 +11260,6 @@
"version": "7.3.0", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"entities": "^6.0.0" "entities": "^6.0.0"
@@ -11267,7 +11300,6 @@
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
"integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"

View File

@@ -10,10 +10,12 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^19.2.16",
"@angular/common": "^19.2.0", "@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0", "@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0", "@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0", "@angular/forms": "^19.2.0",
"@angular/material": "^19.2.16",
"@angular/platform-browser": "^19.2.0", "@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0", "@angular/router": "^19.2.0",
@@ -34,4 +36,4 @@
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2" "typescript": "~5.7.2"
} }
} }

View File

@@ -1,2 +1,37 @@
<h2>Calculator Component</h2> <mat-card>
<p>{{ message }}</p> <mat-card-title>3D Print Cost Calculator</mat-card-title>
<input
type="file"
accept=".stl"
(change)="onFileSelected($event)"
/>
<button
mat-raised-button
color="primary"
(click)="uploadAndCalculate()"
[disabled]="!file || loading"
>
{{ loading ? 'Calculating...' : 'Calculate' }}
</button>
<mat-progress-spinner
*ngIf="loading"
diameter="30"
mode="indeterminate"
></mat-progress-spinner>
<p *ngIf="error" class="error">{{ error }}</p>
<div *ngIf="results">
<p>Volume STL: {{ results.stl_volume_mm3 }} mm³</p>
<p>Superficie: {{ results.surface_area_mm2 }} mm²</p>
<p>Volume pareti: {{ results.wall_volume_mm3 }} mm³</p>
<p>Volume infill: {{ results.infill_volume_mm3 }} mm³</p>
<p>Volume stampa: {{ results.print_volume_mm3 }} mm³</p>
<p>Peso stimato: {{ results.weight_g }} g</p>
<p>Costo stimato: CHF {{ results.cost_chf }}</p>
<p>Tempo stimato: {{ results.time_min }} min</p>
</div>
</mat-card>

View File

@@ -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 { 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({ @Component({
selector: 'app-calculator', selector: 'app-calculator',
standalone: true,
imports: [
CommonModule,
FormsModule,
MatCardModule,
MatFormFieldModule,
MatButtonModule,
MatProgressSpinnerModule
],
templateUrl: './calculator.component.html', templateUrl: './calculator.component.html',
styleUrls: ['./calculator.component.css'] styleUrls: ['./calculator.component.scss']
}) })
export class CalculatorComponent implements OnInit { export class CalculatorComponent {
message = ''; file: File | null = null;
results: any = null;
error = '';
loading = false;
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
ngOnInit(): void { onFileSelected(event: Event): void {
this.http.get<{ message: string }>('http://localhost:8000/api/hello') const input = event.target as HTMLInputElement;
.subscribe(response => { if (input.files && input.files.length > 0) {
this.message = response.message; 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<any>('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;
}
}); });
} }
} }

View File

@@ -6,6 +6,8 @@
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>

View File

@@ -1 +1,4 @@
/* You can add global styles to this file, and also import other style files */ /* 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; }