produzione 1 #9
@@ -13,3 +13,7 @@ pricing.machine-cost-per-hour=${MACHINE_COST_PER_HOUR:2.0}
|
||||
pricing.energy-cost-per-kwh=${ENERGY_COST_PER_KWH:0.30}
|
||||
pricing.printer-power-watts=${PRINTER_POWER_WATTS:150.0}
|
||||
pricing.markup-percent=${MARKUP_PERCENT:20.0}
|
||||
|
||||
# File Upload Limits
|
||||
spring.servlet.multipart.max-file-size=200MB
|
||||
spring.servlet.multipart.max-request-size=200MB
|
||||
|
||||
@@ -22,21 +22,16 @@ import { QuoteEstimatorService, QuoteRequest, QuoteResult } from './services/quo
|
||||
<!-- Left Column: Input -->
|
||||
<div class="col-input">
|
||||
<app-card>
|
||||
<div class="tabs-wrapper">
|
||||
<div class="sub-tabs">
|
||||
<span
|
||||
class="mode-switch"
|
||||
<div class="mode-selector">
|
||||
<div class="mode-option"
|
||||
[class.active]="mode() === 'easy'"
|
||||
(click)="mode.set('easy')">
|
||||
{{ 'CALC.MODE_EASY' | translate }}
|
||||
</span>
|
||||
<span class="divider">/</span>
|
||||
<span
|
||||
class="mode-switch"
|
||||
</div>
|
||||
<div class="mode-option"
|
||||
[class.active]="mode() === 'advanced'"
|
||||
(click)="mode.set('advanced')">
|
||||
{{ 'CALC.MODE_ADVANCED' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +49,13 @@ import { QuoteEstimatorService, QuoteRequest, QuoteResult } from './services/quo
|
||||
<app-alert type="error">Si è verificato un errore durante il calcolo del preventivo.</app-alert>
|
||||
}
|
||||
|
||||
@if (result()) {
|
||||
@if (loading()) {
|
||||
<app-card class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Analisi geometria e slicing in corso...</p>
|
||||
<small class="text-muted">Potrebbe richiedere qualche secondo.</small>
|
||||
</app-card>
|
||||
} @else if (result()) {
|
||||
<app-quote-result [result]="result()!"></app-quote-result>
|
||||
} @else {
|
||||
<app-card>
|
||||
@@ -82,21 +83,61 @@ import { QuoteEstimatorService, QuoteRequest, QuoteResult } from './services/quo
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
/* Mode Selector (Segmented Control style) */
|
||||
.mode-selector {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-neutral-100);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 4px;
|
||||
margin-bottom: var(--space-6);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: var(--space-2);
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sub-tabs { font-size: 0.875rem; color: var(--color-text-muted); }
|
||||
.mode-switch { cursor: pointer; &:hover { color: var(--color-text); } }
|
||||
.mode-switch.active { font-weight: 700; color: var(--color-brand); }
|
||||
.divider { margin: 0 var(--space-2); }
|
||||
.mode-option {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
|
||||
&:hover { color: var(--color-text); }
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-brand);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.benefits { padding-left: var(--space-4); color: var(--color-text-muted); line-height: 2; }
|
||||
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: var(--space-8);
|
||||
color: var(--color-text-muted);
|
||||
|
||||
.spinner {
|
||||
border: 3px solid rgba(0, 0, 0, 0.1);
|
||||
border-left-color: var(--color-brand);
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class CalculatorPageComponent {
|
||||
|
||||
@@ -24,7 +24,7 @@ import { QuoteResult } from '../../services/quote-estimator.service';
|
||||
</app-summary-card>
|
||||
|
||||
<app-summary-card [label]="'CALC.TIME' | translate">
|
||||
{{ result().printTimeHours }}h
|
||||
{{ result().printTimeHours }}h {{ result().printTimeMinutes }}m
|
||||
</app-summary-card>
|
||||
|
||||
<app-summary-card [label]="'CALC.MATERIAL' | translate">
|
||||
|
||||
@@ -101,12 +101,25 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
|
||||
></app-input>
|
||||
}
|
||||
|
||||
@if (loading()) {
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill"></div>
|
||||
</div>
|
||||
<p class="progress-text">Uploading & Analyzing...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="actions">
|
||||
<app-button
|
||||
type="submit"
|
||||
[disabled]="form.invalid || loading()"
|
||||
[fullWidth]="true">
|
||||
{{ loading() ? '...' : ('CALC.CALCULATE' | translate) }}
|
||||
@if (loading()) {
|
||||
Slicing in progress...
|
||||
} @else {
|
||||
{{ 'CALC.CALCULATE' | translate }}
|
||||
}
|
||||
</app-button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -172,6 +185,36 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-container {
|
||||
margin-top: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
background: var(--color-neutral-100);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--space-2);
|
||||
position: relative;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--color-brand);
|
||||
width: 0%;
|
||||
animation: progress 2s ease-in-out infinite;
|
||||
}
|
||||
.progress-text { font-size: 0.875rem; color: var(--color-text-muted); }
|
||||
|
||||
@keyframes progress {
|
||||
0% { width: 0%; transform: translateX(-100%); }
|
||||
50% { width: 100%; transform: translateX(0); }
|
||||
100% { width: 100%; transform: translateX(100%); }
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class UploadFormComponent {
|
||||
@@ -230,13 +273,27 @@ export class UploadFormComponent {
|
||||
}
|
||||
|
||||
onFilesDropped(newFiles: File[]) {
|
||||
this.files.update(current => [...current, ...newFiles]);
|
||||
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
|
||||
const validFiles: File[] = [];
|
||||
let hasError = false;
|
||||
|
||||
for (const file of newFiles) {
|
||||
if (file.size > MAX_SIZE) {
|
||||
hasError = true;
|
||||
} else {
|
||||
validFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
alert("Alcuni file superano il limite di 200MB e non sono stati aggiunti.");
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
this.files.update(current => [...current, ...validFiles]);
|
||||
this.form.patchValue({ files: this.files() });
|
||||
this.form.get('files')?.markAsTouched();
|
||||
|
||||
// Select the last added file by default if none selected
|
||||
if (newFiles.length > 0) {
|
||||
this.selectedFile.set(newFiles[newFiles.length - 1]);
|
||||
this.selectedFile.set(validFiles[validFiles.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface QuoteResult {
|
||||
price: number;
|
||||
currency: string;
|
||||
printTimeHours: number;
|
||||
printTimeMinutes: number;
|
||||
materialUsageGrams: number;
|
||||
setupCost: number;
|
||||
}
|
||||
@@ -104,10 +105,14 @@ export class QuoteEstimatorService {
|
||||
// Total time usually parallel if we have multiple printers, but let's sum for now
|
||||
totalTime = totalTime * request.quantity;
|
||||
|
||||
const totalHours = Math.floor(totalTime / 3600);
|
||||
const totalMinutes = Math.ceil((totalTime % 3600) / 60);
|
||||
|
||||
return {
|
||||
price: Math.round(totalPrice * 100) / 100,
|
||||
price: Math.round(totalPrice * 100) / 100, // Keep 2 decimals
|
||||
currency: 'CHF',
|
||||
printTimeHours: Math.ceil(totalTime / 3600), // Ceil hours
|
||||
printTimeHours: totalHours,
|
||||
printTimeMinutes: totalMinutes,
|
||||
materialUsageGrams: Math.ceil(totalWeight),
|
||||
setupCost
|
||||
};
|
||||
|
||||
@@ -18,6 +18,11 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
<span>Loading 3D Model...</span>
|
||||
</div>
|
||||
}
|
||||
@if (file && !loading) {
|
||||
<div class="dims-overlay">
|
||||
{{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
@@ -53,6 +58,18 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.dims-overlay {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0,0,0,0.6);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
pointer-events: none;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@@ -125,6 +142,8 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
resizeObserver.observe(this.rendererContainer.nativeElement);
|
||||
}
|
||||
|
||||
dimensions = { x: 0, y: 0, z: 0 };
|
||||
|
||||
private loadFile(file: File) {
|
||||
this.loading = true;
|
||||
const reader = new FileReader();
|
||||
@@ -150,22 +169,33 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
geometry.computeBoundingBox();
|
||||
geometry.center();
|
||||
|
||||
// Get Dimensions
|
||||
const boundingBox = geometry.boundingBox!;
|
||||
const size = new THREE.Vector3();
|
||||
boundingBox.getSize(size);
|
||||
|
||||
this.dimensions = {
|
||||
x: Math.round(size.x * 10) / 10,
|
||||
y: Math.round(size.y * 10) / 10,
|
||||
z: Math.round(size.z * 10) / 10
|
||||
};
|
||||
|
||||
// Rotate to stand upright (usually necessary for STLs)
|
||||
this.currentMesh.rotation.x = -Math.PI / 2;
|
||||
|
||||
this.scene.add(this.currentMesh);
|
||||
|
||||
// Adjust camera to fit object
|
||||
const boundingBox = geometry.boundingBox!;
|
||||
const size = new THREE.Vector3();
|
||||
boundingBox.getSize(size);
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const fov = this.camera.fov * (Math.PI / 180);
|
||||
let cameraZ = Math.abs(maxDim / 2 * Math.tan(fov * 2)); // Basic fit
|
||||
cameraZ *= 2.5; // Zoom out a bit
|
||||
|
||||
// Calculate distance towards camera (z-axis)
|
||||
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
|
||||
cameraZ *= 1.5; // Tighter zoom (reduced from 2.5)
|
||||
|
||||
this.camera.position.z = cameraZ;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.controls.update();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading STL:', err);
|
||||
|
||||
Reference in New Issue
Block a user