fix(back-end): fix process and new feature
Some checks failed
Build, Test and Deploy / test-backend (push) Failing after 32s
Build, Test and Deploy / build-and-push (push) Has been skipped
Build, Test and Deploy / deploy (push) Has been skipped

This commit is contained in:
2026-02-16 16:09:36 +01:00
parent 579ac3fcb6
commit 875c6ffd2d
12 changed files with 421 additions and 123 deletions

View File

@@ -1,11 +1,14 @@
package com.printcalculator.controller;
import com.printcalculator.entity.PrinterMachine;
import com.printcalculator.exception.ModelTooLargeException;
import com.printcalculator.model.PrintStats;
import com.printcalculator.model.QuoteResult;
import com.printcalculator.model.StlBounds;
import com.printcalculator.repository.PrinterMachineRepository;
import com.printcalculator.service.QuoteCalculator;
import com.printcalculator.service.SlicerService;
import com.printcalculator.service.StlService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -20,6 +23,7 @@ import java.nio.file.Path;
public class QuoteController {
private final SlicerService slicerService;
private final StlService stlService;
private final QuoteCalculator quoteCalculator;
private final PrinterMachineRepository machineRepo;
@@ -27,8 +31,9 @@ public class QuoteController {
private static final String DEFAULT_FILAMENT = "pla_basic";
private static final String DEFAULT_PROCESS = "standard";
public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo) {
public QuoteController(SlicerService slicerService, StlService stlService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo) {
this.slicerService = slicerService;
this.stlService = stlService;
this.quoteCalculator = quoteCalculator;
this.machineRepo = machineRepo;
}
@@ -117,18 +122,36 @@ public class QuoteController {
slicerMachineProfile = "bambu_a1";
}
// Validate model size against machine volume
validateModelSize(tempInput.toFile(), machine);
PrintStats stats = slicerService.slice(tempInput.toFile(), slicerMachineProfile, filament, process, machineOverrides, processOverrides);
// Calculate Quote (Pass machine display name for pricing lookup)
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filament);
return ResponseEntity.ok(result);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.internalServerError().build();
} finally {
Files.deleteIfExists(tempInput);
}
}
private void validateModelSize(java.io.File stlFile, PrinterMachine machine) throws IOException {
StlBounds bounds = stlService.readBounds(stlFile);
double x = bounds.sizeX();
double y = bounds.sizeY();
double z = bounds.sizeZ();
int bx = machine.getBuildVolumeXMm();
int by = machine.getBuildVolumeYMm();
int bz = machine.getBuildVolumeZMm();
double eps = 0.01;
boolean fits = (x <= bx + eps && y <= by + eps && z <= bz + eps)
|| (y <= bx + eps && x <= by + eps && z <= bz + eps);
if (!fits) {
throw new ModelTooLargeException(x, y, z, bx, by, bz);
}
}
}

View File

@@ -3,13 +3,16 @@ package com.printcalculator.controller;
import com.printcalculator.entity.PrinterMachine;
import com.printcalculator.entity.QuoteLineItem;
import com.printcalculator.entity.QuoteSession;
import com.printcalculator.exception.ModelTooLargeException;
import com.printcalculator.model.PrintStats;
import com.printcalculator.model.QuoteResult;
import com.printcalculator.model.StlBounds;
import com.printcalculator.repository.PrinterMachineRepository;
import com.printcalculator.repository.QuoteLineItemRepository;
import com.printcalculator.repository.QuoteSessionRepository;
import com.printcalculator.service.QuoteCalculator;
import com.printcalculator.service.SlicerService;
import com.printcalculator.service.StlService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
@@ -37,6 +40,7 @@ public class QuoteSessionController {
private final QuoteSessionRepository sessionRepo;
private final QuoteLineItemRepository lineItemRepo;
private final SlicerService slicerService;
private final StlService stlService;
private final QuoteCalculator quoteCalculator;
private final PrinterMachineRepository machineRepo;
private final com.printcalculator.repository.PricingPolicyRepository pricingRepo;
@@ -49,6 +53,7 @@ public class QuoteSessionController {
public QuoteSessionController(QuoteSessionRepository sessionRepo,
QuoteLineItemRepository lineItemRepo,
SlicerService slicerService,
StlService stlService,
QuoteCalculator quoteCalculator,
PrinterMachineRepository machineRepo,
com.printcalculator.repository.PricingPolicyRepository pricingRepo,
@@ -56,6 +61,7 @@ public class QuoteSessionController {
this.sessionRepo = sessionRepo;
this.lineItemRepo = lineItemRepo;
this.slicerService = slicerService;
this.stlService = stlService;
this.quoteCalculator = quoteCalculator;
this.machineRepo = machineRepo;
this.pricingRepo = pricingRepo;
@@ -127,8 +133,11 @@ public class QuoteSessionController {
// 1. Pick Machine (default to first active or specific)
PrinterMachine machine = machineRepo.findFirstByIsActiveTrue()
.orElseThrow(() -> new RuntimeException("No active printer found"));
// 2. Validate model size against machine volume
StlBounds bounds = validateModelSize(persistentPath.toFile(), machine);
// 2. Pick Profiles
// 3. Pick Profiles
String machineProfile = machine.getSlicerMachineProfile();
if (machineProfile == null || machineProfile.isBlank()) {
machineProfile = machine.getPrinterDisplayName(); // e.g. "Bambu Lab A1 0.4 nozzle"
@@ -194,7 +203,7 @@ public class QuoteSessionController {
machineOverrides.put("nozzle_diameter", String.valueOf(settings.getNozzleDiameter()));
}
// 3. Slice (Use persistent path)
// 4. Slice (Use persistent path)
PrintStats stats = slicerService.slice(
persistentPath.toFile(),
machineProfile,
@@ -204,10 +213,10 @@ public class QuoteSessionController {
processOverrides
);
// 4. Calculate Quote
// 5. Calculate Quote
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filamentProfile);
// 5. Create Line Item
// 6. Create Line Item
QuoteLineItem item = new QuoteLineItem();
item.setQuoteSession(session);
item.setOriginalFilename(file.getOriginalFilename());
@@ -227,14 +236,10 @@ public class QuoteSessionController {
breakdown.put("setup_fee", result.getSetupCost());
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(BigDecimal.ZERO);
item.setBoundingBoxYMm(BigDecimal.ZERO);
item.setBoundingBoxZMm(BigDecimal.ZERO);
// Dimensions from STL
item.setBoundingBoxXMm(BigDecimal.valueOf(bounds.sizeX()));
item.setBoundingBoxYMm(BigDecimal.valueOf(bounds.sizeY()));
item.setBoundingBoxZMm(BigDecimal.valueOf(bounds.sizeZ()));
item.setCreatedAt(OffsetDateTime.now());
item.setUpdatedAt(OffsetDateTime.now());
@@ -250,6 +255,26 @@ public class QuoteSessionController {
}
}
private StlBounds validateModelSize(java.io.File stlFile, PrinterMachine machine) throws IOException {
StlBounds bounds = stlService.readBounds(stlFile);
double x = bounds.sizeX();
double y = bounds.sizeY();
double z = bounds.sizeZ();
int bx = machine.getBuildVolumeXMm();
int by = machine.getBuildVolumeYMm();
int bz = machine.getBuildVolumeZMm();
double eps = 0.01;
boolean fits = (x <= bx + eps && y <= by + eps && z <= bz + eps)
|| (y <= bx + eps && x <= by + eps && z <= bz + eps);
if (!fits) {
throw new ModelTooLargeException(x, y, z, bx, by, bz);
}
return bounds;
}
private void applyPrintSettings(com.printcalculator.dto.PrintSettingsDto settings) {
if ("BASIC".equalsIgnoreCase(settings.getComplexityMode())) {
// Set defaults based on Quality

View File

@@ -9,6 +9,8 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.util.HashMap;
import java.util.Map;
import java.math.BigDecimal;
import java.math.RoundingMode;
@ControllerAdvice
@Slf4j
@@ -43,4 +45,27 @@ public class GlobalExceptionHandler {
response.put("message", "The uploaded file exceeds the maximum allowed size.");
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(response);
}
@ExceptionHandler(ModelTooLargeException.class)
public ResponseEntity<?> handleModelTooLarge(ModelTooLargeException exc) {
Map<String, String> response = new HashMap<>();
response.put("error", "Model too large");
response.put("code", "MODEL_TOO_LARGE");
response.put("message", String.format(
"Model size %.2fx%.2fx%.2f mm exceeds build volume %dx%dx%d mm.",
exc.getModelX(), exc.getModelY(), exc.getModelZ(),
exc.getBuildX(), exc.getBuildY(), exc.getBuildZ()
));
response.put("model_x_mm", formatMm(exc.getModelX()));
response.put("model_y_mm", formatMm(exc.getModelY()));
response.put("model_z_mm", formatMm(exc.getModelZ()));
response.put("build_x_mm", String.valueOf(exc.getBuildX()));
response.put("build_y_mm", String.valueOf(exc.getBuildY()));
response.put("build_z_mm", String.valueOf(exc.getBuildZ()));
return ResponseEntity.unprocessableEntity().body(response);
}
private String formatMm(double value) {
return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).toPlainString();
}
}

View File

@@ -0,0 +1,45 @@
package com.printcalculator.exception;
public class ModelTooLargeException extends RuntimeException {
private final double modelX;
private final double modelY;
private final double modelZ;
private final int buildX;
private final int buildY;
private final int buildZ;
public ModelTooLargeException(double modelX, double modelY, double modelZ,
int buildX, int buildY, int buildZ) {
super("Model size exceeds build volume");
this.modelX = modelX;
this.modelY = modelY;
this.modelZ = modelZ;
this.buildX = buildX;
this.buildY = buildY;
this.buildZ = buildZ;
}
public double getModelX() {
return modelX;
}
public double getModelY() {
return modelY;
}
public double getModelZ() {
return modelZ;
}
public int getBuildX() {
return buildX;
}
public int getBuildY() {
return buildY;
}
public int getBuildZ() {
return buildZ;
}
}

View File

@@ -0,0 +1,16 @@
package com.printcalculator.model;
public record StlBounds(double minX, double minY, double minZ,
double maxX, double maxY, double maxZ) {
public double sizeX() {
return maxX - minX;
}
public double sizeY() {
return maxY - minY;
}
public double sizeZ() {
return maxZ - minZ;
}
}

View File

@@ -50,17 +50,9 @@ public class SlicerService {
if (machineOverrides != null) machineOverrides.forEach(machineProfile::put);
if (processOverrides != null) processOverrides.forEach(processProfile::put);
// MANTENIAMO L'IDENTITÀ BAMBU LAB A1
// Ma puliamo solo i riferimenti a mesh esterne che causano il crash grafico (Unable to create exclude triangles)
machineProfile.put("printer_model", "Bambu Lab A1");
// Impostiamo aree di esclusione vuote esplicitamente per evitare che lo slicer tenti di caricarle dai suoi interni
machineProfile.putArray("bed_exclude_area");
machineProfile.putArray("head_wrap_detect_zone");
// Rimuoviamo i modelli 3D del piatto che richiedono caricamento di mesh STL/OBJ interne
machineProfile.remove("bed_custom_model");
machineProfile.remove("bed_custom_texture");
// Evitiamo solo asset grafici opzionali che potrebbero richiedere file esterni
if (machineProfile.has("bed_custom_model")) machineProfile.put("bed_custom_model", "");
if (machineProfile.has("bed_custom_texture")) machineProfile.put("bed_custom_texture", "");
machineProfile.remove("thumbnail");
Path baseTempPath = Paths.get("/app/temp");

View File

@@ -0,0 +1,134 @@
package com.printcalculator.service;
import com.printcalculator.model.StlBounds;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@Service
public class StlService {
public StlBounds readBounds(File stlFile) throws IOException {
long size = stlFile.length();
if (size >= 84 && isBinaryStl(stlFile, size)) {
return readBinaryBounds(stlFile);
}
return readAsciiBounds(stlFile);
}
private boolean isBinaryStl(File stlFile, long size) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(stlFile, "r")) {
raf.seek(80);
long triangleCount = readLEUInt32(raf);
long expected = 84L + triangleCount * 50L;
return expected == size;
}
}
private StlBounds readBinaryBounds(File stlFile) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(stlFile, "r")) {
raf.seek(80);
long triangleCount = readLEUInt32(raf);
raf.seek(84);
BoundsAccumulator acc = new BoundsAccumulator();
for (long i = 0; i < triangleCount; i++) {
// skip normal
readLEFloat(raf);
readLEFloat(raf);
readLEFloat(raf);
// 3 vertices
acc.accept(readLEFloat(raf), readLEFloat(raf), readLEFloat(raf));
acc.accept(readLEFloat(raf), readLEFloat(raf), readLEFloat(raf));
acc.accept(readLEFloat(raf), readLEFloat(raf), readLEFloat(raf));
// skip attribute byte count
raf.skipBytes(2);
}
return acc.toBounds();
}
}
private StlBounds readAsciiBounds(File stlFile) throws IOException {
BoundsAccumulator acc = new BoundsAccumulator();
try (BufferedReader reader = Files.newBufferedReader(stlFile.toPath(), StandardCharsets.US_ASCII)) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.startsWith("vertex")) continue;
String[] parts = line.split("\\s+");
if (parts.length < 4) continue;
double x = Double.parseDouble(parts[1]);
double y = Double.parseDouble(parts[2]);
double z = Double.parseDouble(parts[3]);
acc.accept(x, y, z);
}
}
return acc.toBounds();
}
private long readLEUInt32(RandomAccessFile raf) throws IOException {
int b1 = raf.read();
int b2 = raf.read();
int b3 = raf.read();
int b4 = raf.read();
if ((b1 | b2 | b3 | b4) < 0) throw new IOException("Unexpected EOF while reading STL");
return ((long) b1 & 0xFF)
| (((long) b2 & 0xFF) << 8)
| (((long) b3 & 0xFF) << 16)
| (((long) b4 & 0xFF) << 24);
}
private int readLEInt(RandomAccessFile raf) throws IOException {
int b1 = raf.read();
int b2 = raf.read();
int b3 = raf.read();
int b4 = raf.read();
if ((b1 | b2 | b3 | b4) < 0) throw new IOException("Unexpected EOF while reading STL");
return (b1 & 0xFF)
| ((b2 & 0xFF) << 8)
| ((b3 & 0xFF) << 16)
| ((b4 & 0xFF) << 24);
}
private float readLEFloat(RandomAccessFile raf) throws IOException {
return Float.intBitsToFloat(readLEInt(raf));
}
private static class BoundsAccumulator {
private boolean hasPoint = false;
private double minX;
private double minY;
private double minZ;
private double maxX;
private double maxY;
private double maxZ;
void accept(double x, double y, double z) {
if (!hasPoint) {
minX = maxX = x;
minY = maxY = y;
minZ = maxZ = z;
hasPoint = true;
return;
}
if (x < minX) minX = x;
if (y < minY) minY = y;
if (z < minZ) minZ = z;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
if (z > maxZ) maxZ = z;
}
StlBounds toBounds() throws IOException {
if (!hasPoint) {
throw new IOException("STL appears to contain no vertices");
}
return new StlBounds(minX, minY, minZ, maxX, maxY, maxZ);
}
}
}