feat(web) improvments in calculation page
This commit is contained in:
@@ -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.energy-cost-per-kwh=${ENERGY_COST_PER_KWH:0.30}
|
||||||
pricing.printer-power-watts=${PRINTER_POWER_WATTS:150.0}
|
pricing.printer-power-watts=${PRINTER_POWER_WATTS:150.0}
|
||||||
pricing.markup-percent=${MARKUP_PERCENT:20.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 -->
|
<!-- Left Column: Input -->
|
||||||
<div class="col-input">
|
<div class="col-input">
|
||||||
<app-card>
|
<app-card>
|
||||||
<div class="tabs-wrapper">
|
<div class="mode-selector">
|
||||||
<div class="sub-tabs">
|
<div class="mode-option"
|
||||||
<span
|
|
||||||
class="mode-switch"
|
|
||||||
[class.active]="mode() === 'easy'"
|
[class.active]="mode() === 'easy'"
|
||||||
(click)="mode.set('easy')">
|
(click)="mode.set('easy')">
|
||||||
{{ 'CALC.MODE_EASY' | translate }}
|
{{ 'CALC.MODE_EASY' | translate }}
|
||||||
</span>
|
</div>
|
||||||
<span class="divider">/</span>
|
<div class="mode-option"
|
||||||
<span
|
|
||||||
class="mode-switch"
|
|
||||||
[class.active]="mode() === 'advanced'"
|
[class.active]="mode() === 'advanced'"
|
||||||
(click)="mode.set('advanced')">
|
(click)="mode.set('advanced')">
|
||||||
{{ 'CALC.MODE_ADVANCED' | translate }}
|
{{ 'CALC.MODE_ADVANCED' | translate }}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<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>
|
<app-quote-result [result]="result()!"></app-quote-result>
|
||||||
} @else {
|
} @else {
|
||||||
<app-card>
|
<app-card>
|
||||||
@@ -82,21 +83,61 @@ import { QuoteEstimatorService, QuoteRequest, QuoteResult } from './services/quo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-wrapper {
|
/* Mode Selector (Segmented Control style) */
|
||||||
|
.mode-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
background-color: var(--color-neutral-100);
|
||||||
align-items: center;
|
border-radius: var(--radius-md);
|
||||||
|
padding: 4px;
|
||||||
margin-bottom: var(--space-6);
|
margin-bottom: var(--space-6);
|
||||||
border-bottom: 1px solid var(--color-border);
|
gap: 4px;
|
||||||
padding-bottom: var(--space-2);
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-tabs { font-size: 0.875rem; color: var(--color-text-muted); }
|
.mode-option {
|
||||||
.mode-switch { cursor: pointer; &:hover { color: var(--color-text); } }
|
flex: 1;
|
||||||
.mode-switch.active { font-weight: 700; color: var(--color-brand); }
|
text-align: center;
|
||||||
.divider { margin: 0 var(--space-2); }
|
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; }
|
.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 {
|
export class CalculatorPageComponent {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { QuoteResult } from '../../services/quote-estimator.service';
|
|||||||
</app-summary-card>
|
</app-summary-card>
|
||||||
|
|
||||||
<app-summary-card [label]="'CALC.TIME' | translate">
|
<app-summary-card [label]="'CALC.TIME' | translate">
|
||||||
{{ result().printTimeHours }}h
|
{{ result().printTimeHours }}h {{ result().printTimeMinutes }}m
|
||||||
</app-summary-card>
|
</app-summary-card>
|
||||||
|
|
||||||
<app-summary-card [label]="'CALC.MATERIAL' | translate">
|
<app-summary-card [label]="'CALC.MATERIAL' | translate">
|
||||||
|
|||||||
@@ -101,12 +101,25 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
|
|||||||
></app-input>
|
></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">
|
<div class="actions">
|
||||||
<app-button
|
<app-button
|
||||||
type="submit"
|
type="submit"
|
||||||
[disabled]="form.invalid || loading()"
|
[disabled]="form.invalid || loading()"
|
||||||
[fullWidth]="true">
|
[fullWidth]="true">
|
||||||
{{ loading() ? '...' : ('CALC.CALCULATE' | translate) }}
|
@if (loading()) {
|
||||||
|
Slicing in progress...
|
||||||
|
} @else {
|
||||||
|
{{ 'CALC.CALCULATE' | translate }}
|
||||||
|
}
|
||||||
</app-button>
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -172,6 +185,36 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
|
|||||||
cursor: pointer;
|
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 {
|
export class UploadFormComponent {
|
||||||
@@ -230,13 +273,27 @@ export class UploadFormComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFilesDropped(newFiles: File[]) {
|
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.patchValue({ files: this.files() });
|
||||||
this.form.get('files')?.markAsTouched();
|
this.form.get('files')?.markAsTouched();
|
||||||
|
this.selectedFile.set(validFiles[validFiles.length - 1]);
|
||||||
// Select the last added file by default if none selected
|
|
||||||
if (newFiles.length > 0) {
|
|
||||||
this.selectedFile.set(newFiles[newFiles.length - 1]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface QuoteResult {
|
|||||||
price: number;
|
price: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
printTimeHours: number;
|
printTimeHours: number;
|
||||||
|
printTimeMinutes: number;
|
||||||
materialUsageGrams: number;
|
materialUsageGrams: number;
|
||||||
setupCost: 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
|
// Total time usually parallel if we have multiple printers, but let's sum for now
|
||||||
totalTime = totalTime * request.quantity;
|
totalTime = totalTime * request.quantity;
|
||||||
|
|
||||||
|
const totalHours = Math.floor(totalTime / 3600);
|
||||||
|
const totalMinutes = Math.ceil((totalTime % 3600) / 60);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
price: Math.round(totalPrice * 100) / 100,
|
price: Math.round(totalPrice * 100) / 100, // Keep 2 decimals
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
printTimeHours: Math.ceil(totalTime / 3600), // Ceil hours
|
printTimeHours: totalHours,
|
||||||
|
printTimeMinutes: totalMinutes,
|
||||||
materialUsageGrams: Math.ceil(totalWeight),
|
materialUsageGrams: Math.ceil(totalWeight),
|
||||||
setupCost
|
setupCost
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|||||||
<span>Loading 3D Model...</span>
|
<span>Loading 3D Model...</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@if (file && !loading) {
|
||||||
|
<div class="dims-overlay">
|
||||||
|
{{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: [`
|
styles: [`
|
||||||
@@ -53,6 +58,18 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
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 {
|
export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||||
@@ -125,6 +142,8 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
resizeObserver.observe(this.rendererContainer.nativeElement);
|
resizeObserver.observe(this.rendererContainer.nativeElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dimensions = { x: 0, y: 0, z: 0 };
|
||||||
|
|
||||||
private loadFile(file: File) {
|
private loadFile(file: File) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -150,22 +169,33 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
geometry.computeBoundingBox();
|
geometry.computeBoundingBox();
|
||||||
geometry.center();
|
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)
|
// Rotate to stand upright (usually necessary for STLs)
|
||||||
this.currentMesh.rotation.x = -Math.PI / 2;
|
this.currentMesh.rotation.x = -Math.PI / 2;
|
||||||
|
|
||||||
this.scene.add(this.currentMesh);
|
this.scene.add(this.currentMesh);
|
||||||
|
|
||||||
// Adjust camera to fit object
|
// 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 maxDim = Math.max(size.x, size.y, size.z);
|
||||||
const fov = this.camera.fov * (Math.PI / 180);
|
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.position.z = cameraZ;
|
||||||
this.camera.updateProjectionMatrix();
|
this.camera.updateProjectionMatrix();
|
||||||
|
this.controls.update();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading STL:', err);
|
console.error('Error loading STL:', err);
|
||||||
|
|||||||
Reference in New Issue
Block a user