From a85c57032dd481f595c91c76ef6aee3961d4a4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Fri, 27 Feb 2026 10:43:06 +0100 Subject: [PATCH] fix(front-end): calculator improvements --- .../controller/QuoteSessionController.java | 33 +++++-- .../printcalculator/service/OrderService.java | 10 ++- .../service/SlicerService.java | 90 +++++++++++++++++++ .../upload-form/upload-form.component.ts | 51 +---------- .../services/quote-estimator.service.ts | 5 +- .../features/checkout/checkout.component.html | 13 ++- .../features/checkout/checkout.component.scss | 5 ++ 7 files changed, 139 insertions(+), 68 deletions(-) diff --git a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java index eccf104..47ea7a8 100644 --- a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java +++ b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java @@ -3,6 +3,7 @@ package com.printcalculator.controller; import com.printcalculator.entity.PrinterMachine; import com.printcalculator.entity.QuoteLineItem; import com.printcalculator.entity.QuoteSession; +import com.printcalculator.model.ModelDimensions; import com.printcalculator.model.PrintStats; import com.printcalculator.model.QuoteResult; import com.printcalculator.repository.PrinterMachineRepository; @@ -28,6 +29,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.Optional; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; @@ -183,6 +185,8 @@ public class QuoteSessionController { null, // machine overrides processOverrides ); + + Optional modelDimensions = slicerService.inspectModelDimensions(persistentPath.toFile()); // 4. Calculate Quote QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filamentProfile); @@ -206,14 +210,16 @@ public class QuoteSessionController { breakdown.put("setup_fee", 0); item.setPricingBreakdown(breakdown); - // Dimensions - // Cannot get bb from GCodeParser yet? - // If GCodeParser doesn't return size, we might defaults or 0. - // Stats has filament used. - // Let's set dummy for now or upgrade parser later. - item.setBoundingBoxXMm(settings.getBoundingBoxX() != null ? BigDecimal.valueOf(settings.getBoundingBoxX()) : BigDecimal.ZERO); - item.setBoundingBoxYMm(settings.getBoundingBoxY() != null ? BigDecimal.valueOf(settings.getBoundingBoxY()) : BigDecimal.ZERO); - item.setBoundingBoxZMm(settings.getBoundingBoxZ() != null ? BigDecimal.valueOf(settings.getBoundingBoxZ()) : BigDecimal.ZERO); + // Dimensions for shipping/package checks are computed server-side from the uploaded model. + item.setBoundingBoxXMm(modelDimensions + .map(dim -> BigDecimal.valueOf(dim.xMm())) + .orElseGet(() -> settings.getBoundingBoxX() != null ? BigDecimal.valueOf(settings.getBoundingBoxX()) : BigDecimal.ZERO)); + item.setBoundingBoxYMm(modelDimensions + .map(dim -> BigDecimal.valueOf(dim.yMm())) + .orElseGet(() -> settings.getBoundingBoxY() != null ? BigDecimal.valueOf(settings.getBoundingBoxY()) : BigDecimal.ZERO)); + item.setBoundingBoxZMm(modelDimensions + .map(dim -> BigDecimal.valueOf(dim.zMm())) + .orElseGet(() -> settings.getBoundingBoxZ() != null ? BigDecimal.valueOf(settings.getBoundingBoxZ()) : BigDecimal.ZERO)); item.setCreatedAt(OffsetDateTime.now()); item.setUpdatedAt(OffsetDateTime.now()); @@ -371,7 +377,16 @@ public class QuoteSessionController { break; } } - BigDecimal shippingCostChf = exceedsBaseSize ? BigDecimal.valueOf(4.00) : BigDecimal.valueOf(2.00); + int totalQuantity = items.stream() + .mapToInt(i -> i.getQuantity() != null ? i.getQuantity() : 1) + .sum(); + + BigDecimal shippingCostChf; + if (exceedsBaseSize) { + shippingCostChf = totalQuantity > 5 ? BigDecimal.valueOf(9.00) : BigDecimal.valueOf(4.00); + } else { + shippingCostChf = BigDecimal.valueOf(2.00); + } BigDecimal grandTotal = itemsTotal.add(setupFee).add(shippingCostChf); diff --git a/backend/src/main/java/com/printcalculator/service/OrderService.java b/backend/src/main/java/com/printcalculator/service/OrderService.java index 9b3f683..e757a86 100644 --- a/backend/src/main/java/com/printcalculator/service/OrderService.java +++ b/backend/src/main/java/com/printcalculator/service/OrderService.java @@ -176,7 +176,15 @@ public class OrderService { break; } } - order.setShippingCostChf(exceedsBaseSize ? BigDecimal.valueOf(4.00) : BigDecimal.valueOf(2.00)); + int totalQuantity = quoteItems.stream() + .mapToInt(i -> i.getQuantity() != null ? i.getQuantity() : 1) + .sum(); + + if (exceedsBaseSize) { + order.setShippingCostChf(totalQuantity > 5 ? BigDecimal.valueOf(9.00) : BigDecimal.valueOf(4.00)); + } else { + order.setShippingCostChf(BigDecimal.valueOf(2.00)); + } order = orderRepo.save(order); diff --git a/backend/src/main/java/com/printcalculator/service/SlicerService.java b/backend/src/main/java/com/printcalculator/service/SlicerService.java index 117f965..f9ef570 100644 --- a/backend/src/main/java/com/printcalculator/service/SlicerService.java +++ b/backend/src/main/java/com/printcalculator/service/SlicerService.java @@ -2,6 +2,7 @@ package com.printcalculator.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.printcalculator.model.ModelDimensions; import com.printcalculator.model.PrintStats; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -15,13 +16,19 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Service public class SlicerService { private static final Logger logger = Logger.getLogger(SlicerService.class.getName()); + private static final Pattern SIZE_X_PATTERN = Pattern.compile("(?m)^\\s*size_x\\s*=\\s*([-+]?\\d+(?:\\.\\d+)?)\\s*$"); + private static final Pattern SIZE_Y_PATTERN = Pattern.compile("(?m)^\\s*size_y\\s*=\\s*([-+]?\\d+(?:\\.\\d+)?)\\s*$"); + private static final Pattern SIZE_Z_PATTERN = Pattern.compile("(?m)^\\s*size_z\\s*=\\s*([-+]?\\d+(?:\\.\\d+)?)\\s*$"); private final String slicerPath; private final ProfileManager profileManager; @@ -149,6 +156,89 @@ public class SlicerService { } } + public Optional inspectModelDimensions(File inputModel) { + Path tempDir = null; + try { + tempDir = Files.createTempDirectory("slicer_info_"); + Path infoLogPath = tempDir.resolve("orcaslicer-info.log"); + + List command = new ArrayList<>(); + command.add(slicerPath); + command.add("--info"); + command.add(inputModel.getAbsolutePath()); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.directory(tempDir.toFile()); + pb.redirectErrorStream(true); + pb.redirectOutput(infoLogPath.toFile()); + + Process process = pb.start(); + boolean finished = process.waitFor(2, TimeUnit.MINUTES); + if (!finished) { + process.destroyForcibly(); + logger.warning("Model info extraction timed out for " + inputModel.getName()); + return Optional.empty(); + } + + String output = Files.exists(infoLogPath) + ? Files.readString(infoLogPath, StandardCharsets.UTF_8) + : ""; + + if (process.exitValue() != 0) { + logger.warning("OrcaSlicer --info failed (exit " + process.exitValue() + ") for " + + inputModel.getName() + ": " + output); + return Optional.empty(); + } + + Optional parsed = parseModelDimensionsFromInfoOutput(output); + if (parsed.isEmpty()) { + logger.warning("Could not parse size_x/size_y/size_z from OrcaSlicer --info output for " + + inputModel.getName() + ": " + output); + } + return parsed; + } catch (Exception e) { + logger.warning("Failed to inspect model dimensions for " + inputModel.getName() + ": " + e.getMessage()); + return Optional.empty(); + } finally { + if (tempDir != null) { + deleteRecursively(tempDir); + } + } + } + + static Optional parseModelDimensionsFromInfoOutput(String output) { + if (output == null || output.isBlank()) { + return Optional.empty(); + } + + Double x = extractDouble(SIZE_X_PATTERN, output); + Double y = extractDouble(SIZE_Y_PATTERN, output); + Double z = extractDouble(SIZE_Z_PATTERN, output); + + if (x == null || y == null || z == null) { + return Optional.empty(); + } + + if (x <= 0 || y <= 0 || z <= 0) { + return Optional.empty(); + } + + return Optional.of(new ModelDimensions(x, y, z)); + } + + private static Double extractDouble(Pattern pattern, String text) { + Matcher matcher = pattern.matcher(text); + if (!matcher.find()) { + return null; + } + + try { + return Double.parseDouble(matcher.group(1)); + } catch (NumberFormatException ignored) { + return null; + } + } + private void deleteRecursively(Path path) { if (path == null || !Files.exists(path)) { return; diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index ed01dd0..e48d97b 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -10,15 +10,11 @@ import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl import { ColorSelectorComponent } from '../../../../shared/components/color-selector/color-selector.component'; import { QuoteRequest, QuoteEstimatorService, OptionsResponse, SimpleOption, MaterialOption, VariantOption } from '../../services/quote-estimator.service'; import { getColorHex } from '../../../../core/constants/colors.const'; -import * as THREE from 'three'; -// @ts-ignore -import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; interface FormItem { file: File; quantity: number; color: string; - dimensions?: {x: number, y: number, z: number}; } @Component({ @@ -74,35 +70,6 @@ export class UploadFormComponent implements OnInit { return name.endsWith('.stl'); } - private async getStlDimensions(file: File): Promise<{x: number, y: number, z: number} | null> { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const loader = new STLLoader(); - const geometry = loader.parse(e.target?.result as ArrayBuffer); - geometry.computeBoundingBox(); - if (geometry.boundingBox) { - const size = new THREE.Vector3(); - geometry.boundingBox.getSize(size); - resolve({ - x: Math.round(size.x * 10) / 10, - y: Math.round(size.y * 10) / 10, - z: Math.round(size.z * 10) / 10 - }); - return; - } - resolve(null); - } catch (err) { - console.error("Error parsing STL for dimensions:", err); - resolve(null); - } - }; - reader.onerror = () => resolve(null); - reader.readAsArrayBuffer(file); - }); - } - constructor() { this.form = this.fb.group({ itemsTouched: [false], // Hack to track touched state for custom items list @@ -189,7 +156,7 @@ export class UploadFormComponent implements OnInit { } } - async onFilesDropped(newFiles: File[]) { + onFilesDropped(newFiles: File[]) { const MAX_SIZE = 200 * 1024 * 1024; // 200MB const validItems: FormItem[] = []; let hasError = false; @@ -198,13 +165,8 @@ export class UploadFormComponent implements OnInit { if (file.size > MAX_SIZE) { hasError = true; } else { - let dimensions = undefined; - if (file.name.toLowerCase().endsWith('.stl')) { - const dims = await this.getStlDimensions(file); - if (dims) dimensions = dims; - } // Default color is Black - validItems.push({ file, quantity: 1, color: 'Black', dimensions }); + validItems.push({ file, quantity: 1, color: 'Black' }); } } @@ -296,16 +258,11 @@ export class UploadFormComponent implements OnInit { }); } - async setFiles(files: File[]) { + setFiles(files: File[]) { const validItems: FormItem[] = []; for (const file of files) { - let dimensions = undefined; - if (file.name.toLowerCase().endsWith('.stl')) { - const dims = await this.getStlDimensions(file); - if (dims) dimensions = dims; - } // Default color is Black or derive from somewhere if possible, but here we just init - validItems.push({ file, quantity: 1, color: 'Black', dimensions }); + validItems.push({ file, quantity: 1, color: 'Black' }); } if (validItems.length > 0) { diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index caeeaeb..975c035 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -5,7 +5,7 @@ import { map, catchError, tap } from 'rxjs/operators'; import { environment } from '../../../../environments/environment'; export interface QuoteRequest { - items: { file: File, quantity: number, color?: string, dimensions?: {x: number, y: number, z: number} }[]; + items: { file: File, quantity: number, color?: string }[]; material: string; quality: string; notes?: string; @@ -273,9 +273,6 @@ export class QuoteEstimatorService { quality: easyPreset ? easyPreset.quality : request.quality, supportsEnabled: request.supportEnabled, color: item.color || '#FFFFFF', - boundingBoxX: item.dimensions?.x, - boundingBoxY: item.dimensions?.y, - boundingBoxZ: item.dimensions?.z, layerHeight: easyPreset ? easyPreset.layerHeight : request.layerHeight, infillDensity: easyPreset ? easyPreset.infillDensity : request.infillDensity, infillPattern: easyPreset ? easyPreset.infillPattern : request.infillPattern, diff --git a/frontend/src/app/features/checkout/checkout.component.html b/frontend/src/app/features/checkout/checkout.component.html index 25a98dc..9cfa854 100644 --- a/frontend/src/app/features/checkout/checkout.component.html +++ b/frontend/src/app/features/checkout/checkout.component.html @@ -32,6 +32,12 @@

{{ 'CHECKOUT.BILLING_ADDR' | translate }}

+ + +
@@ -44,13 +50,6 @@
- - - - diff --git a/frontend/src/app/features/checkout/checkout.component.scss b/frontend/src/app/features/checkout/checkout.component.scss index 2926f15..271151b 100644 --- a/frontend/src/app/features/checkout/checkout.component.scss +++ b/frontend/src/app/features/checkout/checkout.component.scss @@ -65,6 +65,11 @@ } /* User Type Selector CSS has been extracted to app-toggle-selector component */ +app-toggle-selector.user-type-selector-compact { + display: block; + width: 100%; + max-width: 400px; +} .company-fields { display: flex;