first try for calculate from stl
This commit is contained in:
63
backend/calculator.py
Normal file
63
backend/calculator.py
Normal 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])
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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": []
|
||||||
|
|||||||
36
frontend/package-lock.json
generated
36
frontend/package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user