fix(front-end): calculator improvements

This commit is contained in:
2026-02-27 10:43:06 +01:00
parent 219b4e127d
commit a85c57032d
7 changed files with 139 additions and 68 deletions

View File

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

View File

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

View File

@@ -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<ModelDimensions> inspectModelDimensions(File inputModel) {
Path tempDir = null;
try {
tempDir = Files.createTempDirectory("slicer_info_");
Path infoLogPath = tempDir.resolve("orcaslicer-info.log");
List<String> 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<ModelDimensions> 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<ModelDimensions> 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;