fix(back-end): fix process and new feature
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user