feat/calculator-options #26
@@ -4,14 +4,12 @@ import com.printcalculator.dto.AddressDto;
|
||||
import com.printcalculator.dto.AdminOrderStatusUpdateRequest;
|
||||
import com.printcalculator.dto.OrderDto;
|
||||
import com.printcalculator.dto.OrderItemDto;
|
||||
import com.printcalculator.entity.Order;
|
||||
import com.printcalculator.entity.OrderItem;
|
||||
import com.printcalculator.entity.Payment;
|
||||
import com.printcalculator.entity.QuoteSession;
|
||||
import com.printcalculator.entity.*;
|
||||
import com.printcalculator.event.OrderShippedEvent;
|
||||
import com.printcalculator.repository.OrderItemRepository;
|
||||
import com.printcalculator.repository.OrderRepository;
|
||||
import com.printcalculator.repository.PaymentRepository;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||
import com.printcalculator.service.payment.PaymentService;
|
||||
import com.printcalculator.service.payment.QrBillService;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { HttpClient, HttpEventType } from '@angular/common/http';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, catchError, tap } from 'rxjs/operators';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
export interface QuoteRequest {
|
||||
items: {
|
||||
export interface QuoteRequestItem {
|
||||
file: File;
|
||||
quantity: number;
|
||||
material?: string;
|
||||
@@ -17,14 +15,10 @@ export interface QuoteRequest {
|
||||
infillPattern?: string;
|
||||
layerHeight?: number;
|
||||
nozzleDiameter?: number;
|
||||
material?: string;
|
||||
quality?: string;
|
||||
nozzleDiameter?: number;
|
||||
layerHeight?: number;
|
||||
infillDensity?: number;
|
||||
infillPattern?: string;
|
||||
supportEnabled?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface QuoteRequest {
|
||||
items: QuoteRequestItem[];
|
||||
material: string;
|
||||
quality: string;
|
||||
notes?: string;
|
||||
@@ -40,8 +34,8 @@ export interface QuoteItem {
|
||||
id?: string;
|
||||
fileName: string;
|
||||
unitPrice: number;
|
||||
unitTime: number; // seconds
|
||||
unitWeight: number; // grams
|
||||
unitTime: number;
|
||||
unitWeight: number;
|
||||
quantity: number;
|
||||
material?: string;
|
||||
quality?: string;
|
||||
@@ -69,36 +63,12 @@ export interface QuoteResult {
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
interface BackendResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
print_time_seconds: number;
|
||||
material_grams: number;
|
||||
cost: {
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface BackendQuoteResult {
|
||||
totalPrice: number;
|
||||
currency: string;
|
||||
setupCost: number;
|
||||
stats: {
|
||||
printTimeSeconds: number;
|
||||
printTimeFormatted: string;
|
||||
filamentWeightGrams: number;
|
||||
filamentLengthMm: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Options Interfaces
|
||||
export interface MaterialOption {
|
||||
code: string;
|
||||
label: string;
|
||||
variants: VariantOption[];
|
||||
}
|
||||
|
||||
export interface VariantOption {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -109,28 +79,36 @@ export interface VariantOption {
|
||||
stockFilamentGrams: number;
|
||||
isOutOfStock: boolean;
|
||||
}
|
||||
|
||||
export interface QualityOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface InfillOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface NumericOption {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface NozzleLayerHeightOptions {
|
||||
nozzleDiameter: number;
|
||||
layerHeights: NumericOption[];
|
||||
}
|
||||
|
||||
export interface OptionsResponse {
|
||||
materials: MaterialOption[];
|
||||
qualities: QualityOption[];
|
||||
infillPatterns: InfillOption[];
|
||||
layerHeights: NumericOption[];
|
||||
nozzleDiameters: NumericOption[];
|
||||
layerHeightsByNozzle: NozzleLayerHeightOptions[];
|
||||
}
|
||||
|
||||
// UI Option for Select Component
|
||||
export interface SimpleOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
@@ -142,70 +120,23 @@ export interface SimpleOption {
|
||||
export class QuoteEstimatorService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
private buildEasyModePreset(quality: string | undefined): {
|
||||
quality: string;
|
||||
layerHeight: number;
|
||||
infillDensity: number;
|
||||
infillPattern: string;
|
||||
nozzleDiameter: number;
|
||||
} {
|
||||
const normalized = (quality || 'standard').toLowerCase();
|
||||
|
||||
// Legacy alias support.
|
||||
if (normalized === 'high' || normalized === 'extra_fine') {
|
||||
return {
|
||||
quality: 'extra_fine',
|
||||
layerHeight: 0.12,
|
||||
infillDensity: 20,
|
||||
infillPattern: 'grid',
|
||||
nozzleDiameter: 0.4,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized === 'draft') {
|
||||
return {
|
||||
quality: 'extra_fine',
|
||||
layerHeight: 0.24,
|
||||
infillDensity: 12,
|
||||
infillPattern: 'grid',
|
||||
nozzleDiameter: 0.4,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
quality: 'standard',
|
||||
layerHeight: 0.2,
|
||||
infillDensity: 15,
|
||||
infillPattern: 'grid',
|
||||
nozzleDiameter: 0.4,
|
||||
};
|
||||
}
|
||||
private pendingConsultation = signal<{
|
||||
files: File[];
|
||||
message: string;
|
||||
} | null>(null);
|
||||
|
||||
getOptions(): Observable<OptionsResponse> {
|
||||
console.log('QuoteEstimatorService: Requesting options...');
|
||||
const headers: any = {};
|
||||
return this.http
|
||||
.get<OptionsResponse>(`${environment.apiUrl}/api/calculator/options`, {
|
||||
return this.http.get<OptionsResponse>(`${environment.apiUrl}/api/calculator/options`, {
|
||||
headers,
|
||||
})
|
||||
.pipe(
|
||||
tap({
|
||||
next: (res) =>
|
||||
console.log('QuoteEstimatorService: Options loaded', res),
|
||||
error: (err) =>
|
||||
console.error('QuoteEstimatorService: Options failed', err),
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// NEW METHODS for Order Flow
|
||||
|
||||
getQuoteSession(sessionId: string): Observable<any> {
|
||||
const headers: any = {};
|
||||
return this.http.get(
|
||||
`${environment.apiUrl}/api/quote-sessions/${sessionId}`,
|
||||
{ headers },
|
||||
);
|
||||
return this.http.get(`${environment.apiUrl}/api/quote-sessions/${sessionId}`, {
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
updateLineItem(lineItemId: string, changes: any): Observable<any> {
|
||||
@@ -244,13 +175,10 @@ export class QuoteEstimatorService {
|
||||
|
||||
getOrderInvoice(orderId: string): Observable<Blob> {
|
||||
const headers: any = {};
|
||||
return this.http.get(
|
||||
`${environment.apiUrl}/api/orders/${orderId}/invoice`,
|
||||
{
|
||||
return this.http.get(`${environment.apiUrl}/api/orders/${orderId}/invoice`, {
|
||||
headers,
|
||||
responseType: 'blob',
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getOrderConfirmation(orderId: string): Observable<Blob> {
|
||||
@@ -272,87 +200,68 @@ export class QuoteEstimatorService {
|
||||
}
|
||||
|
||||
calculate(request: QuoteRequest): Observable<number | QuoteResult> {
|
||||
console.log('QuoteEstimatorService: Calculating quote...', request);
|
||||
if (request.items.length === 0) {
|
||||
console.warn('QuoteEstimatorService: No items to calculate');
|
||||
return of();
|
||||
if (!request.items || request.items.length === 0) {
|
||||
return of(0);
|
||||
}
|
||||
|
||||
return new Observable((observer) => {
|
||||
// 1. Create Session first
|
||||
return new Observable<number | QuoteResult>((observer) => {
|
||||
const headers: any = {};
|
||||
|
||||
this.http
|
||||
.post<any>(`${environment.apiUrl}/api/quote-sessions`, {}, { headers })
|
||||
.subscribe({
|
||||
next: (sessionRes) => {
|
||||
const sessionId = sessionRes.id;
|
||||
const sessionSetupCost = sessionRes.setupCostChf || 0;
|
||||
const sessionId = String(sessionRes?.id || '');
|
||||
if (!sessionId) {
|
||||
observer.error('Could not initialize quote session');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Upload files to this session
|
||||
const totalItems = request.items.length;
|
||||
const allProgress: number[] = new Array(totalItems).fill(0);
|
||||
const finalResponses: any[] = [];
|
||||
let completedRequests = 0;
|
||||
const uploadProgress = new Array(totalItems).fill(0);
|
||||
const uploadResults: { success: boolean }[] = new Array(totalItems)
|
||||
.fill(null)
|
||||
.map(() => ({ success: false }));
|
||||
let completed = 0;
|
||||
|
||||
const checkCompletion = () => {
|
||||
const emitProgress = () => {
|
||||
const avg = Math.round(
|
||||
allProgress.reduce((a, b) => a + b, 0) / totalItems,
|
||||
uploadProgress.reduce((sum, value) => sum + value, 0) / totalItems,
|
||||
);
|
||||
observer.next(avg);
|
||||
};
|
||||
|
||||
if (completedRequests === totalItems) {
|
||||
finalize(finalResponses, sessionSetupCost, sessionId);
|
||||
const finalize = () => {
|
||||
emitProgress();
|
||||
if (completed !== totalItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasFailure = uploadResults.some((entry) => !entry.success);
|
||||
if (hasFailure) {
|
||||
observer.error('One or more files failed during upload/analysis');
|
||||
return;
|
||||
}
|
||||
|
||||
this.getQuoteSession(sessionId).subscribe({
|
||||
next: (sessionData) => {
|
||||
observer.next(100);
|
||||
const result = this.mapSessionToQuoteResult(sessionData);
|
||||
result.notes = request.notes;
|
||||
observer.next(result);
|
||||
observer.complete();
|
||||
},
|
||||
error: () => {
|
||||
observer.error('Failed to calculate final quote');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
request.items.forEach((item, index) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', item.file);
|
||||
|
||||
const effectiveQuality = item.quality || request.quality;
|
||||
const easyPreset =
|
||||
request.mode === 'easy'
|
||||
? this.buildEasyModePreset(effectiveQuality)
|
||||
: null;
|
||||
|
||||
const settings = {
|
||||
complexityMode:
|
||||
request.mode === 'easy'
|
||||
? 'ADVANCED'
|
||||
: request.mode.toUpperCase(),
|
||||
material: item.material || request.material,
|
||||
filamentVariantId: item.filamentVariantId,
|
||||
quantity: item.quantity,
|
||||
quality: easyPreset ? easyPreset.quality : effectiveQuality,
|
||||
supportsEnabled:
|
||||
item.supportEnabled ?? request.supportEnabled ?? false,
|
||||
quality: easyPreset
|
||||
? easyPreset.quality
|
||||
: item.quality || request.quality,
|
||||
supportsEnabled:
|
||||
easyPreset != null
|
||||
? request.supportEnabled
|
||||
: item.supportEnabled ?? request.supportEnabled,
|
||||
color: item.color || '#FFFFFF',
|
||||
layerHeight: easyPreset
|
||||
? easyPreset.layerHeight
|
||||
: (item.layerHeight ?? request.layerHeight),
|
||||
: item.layerHeight ?? request.layerHeight,
|
||||
infillDensity: easyPreset
|
||||
? easyPreset.infillDensity
|
||||
: (item.infillDensity ?? request.infillDensity),
|
||||
: item.infillDensity ?? request.infillDensity,
|
||||
infillPattern: easyPreset
|
||||
? easyPreset.infillPattern
|
||||
: (item.infillPattern ?? request.infillPattern),
|
||||
: item.infillPattern ?? request.infillPattern,
|
||||
nozzleDiameter: easyPreset
|
||||
? easyPreset.nozzleDiameter
|
||||
: (item.nozzleDiameter ?? request.nozzleDiameter),
|
||||
: item.nozzleDiameter ?? request.nozzleDiameter,
|
||||
};
|
||||
|
||||
const settings = this.buildSettingsPayload(request, item);
|
||||
const settingsBlob = new Blob([JSON.stringify(settings)], {
|
||||
type: 'application/json',
|
||||
});
|
||||
@@ -374,84 +283,46 @@ export class QuoteEstimatorService {
|
||||
event.type === HttpEventType.UploadProgress &&
|
||||
event.total
|
||||
) {
|
||||
allProgress[index] = Math.round(
|
||||
uploadProgress[index] = Math.round(
|
||||
(100 * event.loaded) / event.total,
|
||||
);
|
||||
checkCompletion();
|
||||
} else if (event.type === HttpEventType.Response) {
|
||||
allProgress[index] = 100;
|
||||
finalResponses[index] = {
|
||||
...event.body,
|
||||
success: true,
|
||||
fileName: item.file.name,
|
||||
originalQty: item.quantity,
|
||||
originalItem: item,
|
||||
};
|
||||
completedRequests++;
|
||||
checkCompletion();
|
||||
emitProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === HttpEventType.Response) {
|
||||
uploadProgress[index] = 100;
|
||||
uploadResults[index] = { success: true };
|
||||
completed += 1;
|
||||
finalize();
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Item upload failed', err);
|
||||
finalResponses[index] = {
|
||||
success: false,
|
||||
fileName: item.file.name,
|
||||
};
|
||||
completedRequests++;
|
||||
checkCompletion();
|
||||
error: () => {
|
||||
uploadProgress[index] = 100;
|
||||
uploadResults[index] = { success: false };
|
||||
completed += 1;
|
||||
finalize();
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to create session', err);
|
||||
error: () => {
|
||||
observer.error('Could not initialize quote session');
|
||||
},
|
||||
});
|
||||
|
||||
const finalize = (
|
||||
responses: any[],
|
||||
setupCost: number,
|
||||
sessionId: string,
|
||||
) => {
|
||||
this.http
|
||||
.get<any>(`${environment.apiUrl}/api/quote-sessions/${sessionId}`, {
|
||||
headers,
|
||||
})
|
||||
.subscribe({
|
||||
next: (sessionData) => {
|
||||
observer.next(100);
|
||||
const result = this.mapSessionToQuoteResult(sessionData);
|
||||
result.notes = request.notes;
|
||||
observer.next(result);
|
||||
observer.complete();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to fetch final session calculation', err);
|
||||
observer.error('Failed to calculate final quote');
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Consultation Data Transfer
|
||||
private pendingConsultation = signal<{
|
||||
files: File[];
|
||||
message: string;
|
||||
} | null>(null);
|
||||
|
||||
setPendingConsultation(data: { files: File[]; message: string }) {
|
||||
this.pendingConsultation.set(data);
|
||||
}
|
||||
|
||||
getPendingConsultation() {
|
||||
const data = this.pendingConsultation();
|
||||
this.pendingConsultation.set(null); // Clear after reading
|
||||
this.pendingConsultation.set(null);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Session File Retrieval
|
||||
getLineItemContent(
|
||||
sessionId: string,
|
||||
lineItemId: string,
|
||||
@@ -483,50 +354,141 @@ export class QuoteEstimatorService {
|
||||
}
|
||||
|
||||
mapSessionToQuoteResult(sessionData: any): QuoteResult {
|
||||
const session = sessionData.session;
|
||||
const items = sessionData.items || [];
|
||||
const session = sessionData?.session || {};
|
||||
const items = Array.isArray(sessionData?.items) ? sessionData.items : [];
|
||||
|
||||
const totalTime = items.reduce(
|
||||
(acc: number, item: any) =>
|
||||
acc + (item.printTimeSeconds || 0) * item.quantity,
|
||||
0,
|
||||
);
|
||||
const totalWeight = items.reduce(
|
||||
(acc: number, item: any) =>
|
||||
acc + (item.materialGrams || 0) * item.quantity,
|
||||
acc + Number(item?.printTimeSeconds || 0) * Number(item?.quantity || 1),
|
||||
0,
|
||||
);
|
||||
|
||||
const totalWeight = items.reduce(
|
||||
(acc: number, item: any) =>
|
||||
acc + Number(item?.materialGrams || 0) * Number(item?.quantity || 1),
|
||||
0,
|
||||
);
|
||||
|
||||
const grandTotal = Number(sessionData?.grandTotalChf);
|
||||
const fallbackTotal =
|
||||
Number(sessionData?.itemsTotalChf || 0) +
|
||||
Number(session?.setupCostChf || 0) +
|
||||
Number(sessionData?.shippingCostChf || 0);
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
sessionId: session?.id,
|
||||
items: items.map((item: any) => ({
|
||||
id: item.id,
|
||||
fileName: item.originalFilename,
|
||||
unitPrice: item.unitPriceChf,
|
||||
unitTime: item.printTimeSeconds,
|
||||
unitWeight: item.materialGrams,
|
||||
quantity: item.quantity,
|
||||
material: item.materialCode || session.materialCode,
|
||||
color: item.colorCode,
|
||||
filamentVariantId: item.filamentVariantId,
|
||||
supportEnabled: item.supportsEnabled,
|
||||
infillDensity: item.infillPercent,
|
||||
infillPattern: item.infillPattern,
|
||||
layerHeight: item.layerHeightMm,
|
||||
nozzleDiameter: item.nozzleDiameterMm,
|
||||
id: item?.id,
|
||||
fileName: item?.originalFilename,
|
||||
unitPrice: Number(item?.unitPriceChf || 0),
|
||||
unitTime: Number(item?.printTimeSeconds || 0),
|
||||
unitWeight: Number(item?.materialGrams || 0),
|
||||
quantity: Number(item?.quantity || 1),
|
||||
material: item?.materialCode || session?.materialCode,
|
||||
quality: item?.quality,
|
||||
color: item?.colorCode,
|
||||
filamentVariantId: item?.filamentVariantId,
|
||||
supportEnabled: Boolean(item?.supportsEnabled),
|
||||
infillDensity:
|
||||
item?.infillPercent != null ? Number(item.infillPercent) : undefined,
|
||||
infillPattern: item?.infillPattern,
|
||||
layerHeight:
|
||||
item?.layerHeightMm != null ? Number(item.layerHeightMm) : undefined,
|
||||
nozzleDiameter:
|
||||
item?.nozzleDiameterMm != null
|
||||
? Number(item.nozzleDiameterMm)
|
||||
: undefined,
|
||||
})),
|
||||
setupCost: session.setupCostChf || 0,
|
||||
globalMachineCost: sessionData.globalMachineCostChf || 0,
|
||||
cadHours: session.cadHours || 0,
|
||||
cadTotal: sessionData.cadTotalChf || 0,
|
||||
currency: 'CHF', // Fixed for now
|
||||
totalPrice:
|
||||
(sessionData.itemsTotalChf || 0) +
|
||||
(session.setupCostChf || 0) +
|
||||
(sessionData.shippingCostChf || 0),
|
||||
setupCost: Number(session?.setupCostChf || 0),
|
||||
globalMachineCost: Number(sessionData?.globalMachineCostChf || 0),
|
||||
cadHours: Number(session?.cadHours || 0),
|
||||
cadTotal: Number(sessionData?.cadTotalChf || 0),
|
||||
currency: 'CHF',
|
||||
totalPrice: Number.isFinite(grandTotal) ? grandTotal : fallbackTotal,
|
||||
totalTimeHours: Math.floor(totalTime / 3600),
|
||||
totalTimeMinutes: Math.ceil((totalTime % 3600) / 60),
|
||||
totalWeight: Math.ceil(totalWeight),
|
||||
notes: session.notes,
|
||||
notes: session?.notes,
|
||||
};
|
||||
}
|
||||
|
||||
private buildSettingsPayload(request: QuoteRequest, item: QuoteRequestItem): any {
|
||||
const normalizedQuality = this.normalizeQuality(item.quality || request.quality);
|
||||
const easyPreset =
|
||||
request.mode === 'easy'
|
||||
? this.buildEasyModePreset(normalizedQuality)
|
||||
: null;
|
||||
|
||||
return {
|
||||
complexityMode: request.mode === 'easy' ? 'BASIC' : 'ADVANCED',
|
||||
material: String(item.material || request.material || 'PLA'),
|
||||
color: item.color || '#FFFFFF',
|
||||
filamentVariantId: item.filamentVariantId,
|
||||
quality: easyPreset ? easyPreset.quality : normalizedQuality,
|
||||
supportsEnabled: item.supportEnabled ?? request.supportEnabled ?? false,
|
||||
layerHeight:
|
||||
easyPreset?.layerHeight ?? item.layerHeight ?? request.layerHeight ?? 0.2,
|
||||
infillDensity:
|
||||
easyPreset?.infillDensity ??
|
||||
item.infillDensity ??
|
||||
request.infillDensity ??
|
||||
20,
|
||||
infillPattern:
|
||||
easyPreset?.infillPattern ??
|
||||
item.infillPattern ??
|
||||
request.infillPattern ??
|
||||
'grid',
|
||||
nozzleDiameter:
|
||||
easyPreset?.nozzleDiameter ??
|
||||
item.nozzleDiameter ??
|
||||
request.nozzleDiameter ??
|
||||
0.4,
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeQuality(value: string | undefined): string {
|
||||
const normalized = String(value || 'standard').trim().toLowerCase();
|
||||
if (normalized === 'high' || normalized === 'high_definition') {
|
||||
return 'extra_fine';
|
||||
}
|
||||
return normalized || 'standard';
|
||||
}
|
||||
|
||||
private buildEasyModePreset(quality: string): {
|
||||
quality: string;
|
||||
layerHeight: number;
|
||||
infillDensity: number;
|
||||
infillPattern: string;
|
||||
nozzleDiameter: number;
|
||||
} {
|
||||
const normalized = this.normalizeQuality(quality);
|
||||
|
||||
if (normalized === 'draft') {
|
||||
return {
|
||||
quality: 'draft',
|
||||
layerHeight: 0.28,
|
||||
infillDensity: 15,
|
||||
infillPattern: 'grid',
|
||||
nozzleDiameter: 0.4,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized === 'extra_fine') {
|
||||
return {
|
||||
quality: 'extra_fine',
|
||||
layerHeight: 0.12,
|
||||
infillDensity: 20,
|
||||
infillPattern: 'gyroid',
|
||||
nozzleDiameter: 0.4,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
quality: 'standard',
|
||||
layerHeight: 0.2,
|
||||
infillDensity: 15,
|
||||
infillPattern: 'grid',
|
||||
nozzleDiameter: 0.4,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user