dev #8

Closed
JoeKung wants to merge 72 commits from dev into int
7 changed files with 139 additions and 68 deletions
Showing only changes of commit a85c57032d - Show all commits

View File

@@ -3,6 +3,7 @@ package com.printcalculator.controller;
import com.printcalculator.entity.PrinterMachine; import com.printcalculator.entity.PrinterMachine;
import com.printcalculator.entity.QuoteLineItem; import com.printcalculator.entity.QuoteLineItem;
import com.printcalculator.entity.QuoteSession; import com.printcalculator.entity.QuoteSession;
import com.printcalculator.model.ModelDimensions;
import com.printcalculator.model.PrintStats; import com.printcalculator.model.PrintStats;
import com.printcalculator.model.QuoteResult; import com.printcalculator.model.QuoteResult;
import com.printcalculator.repository.PrinterMachineRepository; import com.printcalculator.repository.PrinterMachineRepository;
@@ -28,6 +29,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.Optional;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource; import org.springframework.core.io.UrlResource;
@@ -183,6 +185,8 @@ public class QuoteSessionController {
null, // machine overrides null, // machine overrides
processOverrides processOverrides
); );
Optional<ModelDimensions> modelDimensions = slicerService.inspectModelDimensions(persistentPath.toFile());
// 4. Calculate Quote // 4. Calculate Quote
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filamentProfile); QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filamentProfile);
@@ -206,14 +210,16 @@ public class QuoteSessionController {
breakdown.put("setup_fee", 0); breakdown.put("setup_fee", 0);
item.setPricingBreakdown(breakdown); item.setPricingBreakdown(breakdown);
// Dimensions // Dimensions for shipping/package checks are computed server-side from the uploaded model.
// Cannot get bb from GCodeParser yet? item.setBoundingBoxXMm(modelDimensions
// If GCodeParser doesn't return size, we might defaults or 0. .map(dim -> BigDecimal.valueOf(dim.xMm()))
// Stats has filament used. .orElseGet(() -> settings.getBoundingBoxX() != null ? BigDecimal.valueOf(settings.getBoundingBoxX()) : BigDecimal.ZERO));
// Let's set dummy for now or upgrade parser later. item.setBoundingBoxYMm(modelDimensions
item.setBoundingBoxXMm(settings.getBoundingBoxX() != null ? BigDecimal.valueOf(settings.getBoundingBoxX()) : BigDecimal.ZERO); .map(dim -> BigDecimal.valueOf(dim.yMm()))
item.setBoundingBoxYMm(settings.getBoundingBoxY() != null ? BigDecimal.valueOf(settings.getBoundingBoxY()) : BigDecimal.ZERO); .orElseGet(() -> settings.getBoundingBoxY() != null ? BigDecimal.valueOf(settings.getBoundingBoxY()) : BigDecimal.ZERO));
item.setBoundingBoxZMm(settings.getBoundingBoxZ() != null ? BigDecimal.valueOf(settings.getBoundingBoxZ()) : 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.setCreatedAt(OffsetDateTime.now());
item.setUpdatedAt(OffsetDateTime.now()); item.setUpdatedAt(OffsetDateTime.now());
@@ -371,7 +377,16 @@ public class QuoteSessionController {
break; 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); BigDecimal grandTotal = itemsTotal.add(setupFee).add(shippingCostChf);

View File

@@ -176,7 +176,15 @@ public class OrderService {
break; 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); 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.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.printcalculator.model.ModelDimensions;
import com.printcalculator.model.PrintStats; import com.printcalculator.model.PrintStats;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -15,13 +16,19 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service @Service
public class SlicerService { public class SlicerService {
private static final Logger logger = Logger.getLogger(SlicerService.class.getName()); 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 String slicerPath;
private final ProfileManager profileManager; 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) { private void deleteRecursively(Path path) {
if (path == null || !Files.exists(path)) { if (path == null || !Files.exists(path)) {
return; return;

View File

@@ -10,15 +10,11 @@ import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl
import { ColorSelectorComponent } from '../../../../shared/components/color-selector/color-selector.component'; import { ColorSelectorComponent } from '../../../../shared/components/color-selector/color-selector.component';
import { QuoteRequest, QuoteEstimatorService, OptionsResponse, SimpleOption, MaterialOption, VariantOption } from '../../services/quote-estimator.service'; import { QuoteRequest, QuoteEstimatorService, OptionsResponse, SimpleOption, MaterialOption, VariantOption } from '../../services/quote-estimator.service';
import { getColorHex } from '../../../../core/constants/colors.const'; 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 { interface FormItem {
file: File; file: File;
quantity: number; quantity: number;
color: string; color: string;
dimensions?: {x: number, y: number, z: number};
} }
@Component({ @Component({
@@ -74,35 +70,6 @@ export class UploadFormComponent implements OnInit {
return name.endsWith('.stl'); 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() { constructor() {
this.form = this.fb.group({ this.form = this.fb.group({
itemsTouched: [false], // Hack to track touched state for custom items list 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 MAX_SIZE = 200 * 1024 * 1024; // 200MB
const validItems: FormItem[] = []; const validItems: FormItem[] = [];
let hasError = false; let hasError = false;
@@ -198,13 +165,8 @@ export class UploadFormComponent implements OnInit {
if (file.size > MAX_SIZE) { if (file.size > MAX_SIZE) {
hasError = true; hasError = true;
} else { } else {
let dimensions = undefined;
if (file.name.toLowerCase().endsWith('.stl')) {
const dims = await this.getStlDimensions(file);
if (dims) dimensions = dims;
}
// Default color is Black // 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[] = []; const validItems: FormItem[] = [];
for (const file of files) { 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 // 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) { if (validItems.length > 0) {

View File

@@ -5,7 +5,7 @@ import { map, catchError, tap } from 'rxjs/operators';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
export interface QuoteRequest { 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; material: string;
quality: string; quality: string;
notes?: string; notes?: string;
@@ -273,9 +273,6 @@ export class QuoteEstimatorService {
quality: easyPreset ? easyPreset.quality : request.quality, quality: easyPreset ? easyPreset.quality : request.quality,
supportsEnabled: request.supportEnabled, supportsEnabled: request.supportEnabled,
color: item.color || '#FFFFFF', color: item.color || '#FFFFFF',
boundingBoxX: item.dimensions?.x,
boundingBoxY: item.dimensions?.y,
boundingBoxZ: item.dimensions?.z,
layerHeight: easyPreset ? easyPreset.layerHeight : request.layerHeight, layerHeight: easyPreset ? easyPreset.layerHeight : request.layerHeight,
infillDensity: easyPreset ? easyPreset.infillDensity : request.infillDensity, infillDensity: easyPreset ? easyPreset.infillDensity : request.infillDensity,
infillPattern: easyPreset ? easyPreset.infillPattern : request.infillPattern, infillPattern: easyPreset ? easyPreset.infillPattern : request.infillPattern,

View File

@@ -32,6 +32,12 @@
<h3>{{ 'CHECKOUT.BILLING_ADDR' | translate }}</h3> <h3>{{ 'CHECKOUT.BILLING_ADDR' | translate }}</h3>
</div> </div>
<div formGroupName="billingAddress"> <div formGroupName="billingAddress">
<!-- User Type Selector -->
<app-toggle-selector class="mb-4 user-type-selector-compact"
[options]="userTypeOptions"
[selectedValue]="checkoutForm.get('customerType')?.value"
(selectionChange)="setCustomerType($event)">
</app-toggle-selector>
<!-- Private Person Fields --> <!-- Private Person Fields -->
<div *ngIf="!isCompany" class="form-row"> <div *ngIf="!isCompany" class="form-row">
@@ -44,13 +50,6 @@
<app-input formControlName="companyName" [label]="'CHECKOUT.COMPANY_NAME' | translate" [required]="true" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input> <app-input formControlName="companyName" [label]="'CHECKOUT.COMPANY_NAME' | translate" [required]="true" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input>
<app-input formControlName="referencePerson" [label]="'CONTACT.REF_PERSON' | translate" [required]="true" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input> <app-input formControlName="referencePerson" [label]="'CONTACT.REF_PERSON' | translate" [required]="true" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input>
</div> </div>
<!-- User Type Selector -->
<app-toggle-selector
[options]="userTypeOptions"
[selectedValue]="checkoutForm.get('customerType')?.value"
(selectionChange)="setCustomerType($event)">
</app-toggle-selector>
<app-input formControlName="addressLine1" [label]="'CHECKOUT.ADDRESS_1' | translate" [required]="true"></app-input> <app-input formControlName="addressLine1" [label]="'CHECKOUT.ADDRESS_1' | translate" [required]="true"></app-input>
<app-input formControlName="addressLine2" [label]="'CHECKOUT.ADDRESS_2' | translate"></app-input> <app-input formControlName="addressLine2" [label]="'CHECKOUT.ADDRESS_2' | translate"></app-input>

View File

@@ -65,6 +65,11 @@
} }
/* User Type Selector CSS has been extracted to app-toggle-selector component */ /* 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 { .company-fields {
display: flex; display: flex;