dev #29

Merged
JoeKung merged 30 commits from dev into main 2026-03-09 09:58:45 +01:00
7 changed files with 160 additions and 62 deletions
Showing only changes of commit aa6322e928 - Show all commits

View File

@@ -284,8 +284,8 @@
<span class="filename">{{ item.originalFilename }}</span> <span class="filename">{{ item.originalFilename }}</span>
<span class="file-color"> <span class="file-color">
{{ item.materialCode || "-" }} | {{ item.nozzleDiameterMm ?? "-" }} mm {{ item.materialCode || "-" }} | {{ item.nozzleDiameterMm ?? "-" }} mm
| {{ item.layerHeightMm ?? "-" }} mm | {{ item.infillPercent ?? "-" }}% | {{ item.layerHeightMm ?? "-" }} mm |
| {{ item.infillPattern || "-" }} | {{ item.infillPercent ?? "-" }}% | {{ item.infillPattern || "-" }} |
{{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }} {{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }}
</span> </span>
</div> </div>

View File

@@ -149,7 +149,9 @@
{{ item.layerHeightMm ?? "-" }} mm | {{ item.layerHeightMm ?? "-" }} mm |
{{ item.infillPercent ?? "-" }}% | {{ item.infillPercent ?? "-" }}% |
{{ item.infillPattern || "-" }} | {{ item.infillPattern || "-" }} |
{{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }} {{
item.supportsEnabled ? "Supporti ON" : "Supporti OFF"
}}
</td> </td>
<td>{{ item.status }}</td> <td>{{ item.status }}</td>
<td>{{ item.unitPriceChf | currency: "CHF" }}</td> <td>{{ item.unitPriceChf | currency: "CHF" }}</td>

View File

@@ -72,7 +72,10 @@ export class CalculatorPageComponent implements OnInit {
Record<string, { differences: string[] }> Record<string, { differences: string[] }>
>({}); >({});
private baselinePrintSettings: TrackedPrintSettings | null = null; private baselinePrintSettings: TrackedPrintSettings | null = null;
private baselineItemSettingsByFileName = new Map<string, TrackedPrintSettings>(); private baselineItemSettingsByFileName = new Map<
string,
TrackedPrintSettings
>();
@ViewChild('uploadForm') uploadForm!: UploadFormComponent; @ViewChild('uploadForm') uploadForm!: UploadFormComponent;
@ViewChild('resultCol') resultCol!: ElementRef; @ViewChild('resultCol') resultCol!: ElementRef;
@@ -528,7 +531,9 @@ export class CalculatorPageComponent implements OnInit {
}); });
} }
private toTrackedSettingsFromRequest(req: QuoteRequest): TrackedPrintSettings { private toTrackedSettingsFromRequest(
req: QuoteRequest,
): TrackedPrintSettings {
return { return {
mode: req.mode, mode: req.mode,
material: this.normalizeString(req.material || 'PLA'), material: this.normalizeString(req.material || 'PLA'),
@@ -590,17 +595,17 @@ export class CalculatorPageComponent implements OnInit {
item: any, item: any,
fallback: TrackedPrintSettings, fallback: TrackedPrintSettings,
): TrackedPrintSettings { ): TrackedPrintSettings {
const layer = this.normalizeNumber(item?.layerHeightMm, fallback.layerHeight, 3); const layer = this.normalizeNumber(
item?.layerHeightMm,
fallback.layerHeight,
3,
);
return { return {
mode: this.mode(), mode: this.mode(),
material: this.normalizeString(item?.materialCode || fallback.material), material: this.normalizeString(item?.materialCode || fallback.material),
quality: this.normalizeString( quality: this.normalizeString(
item?.quality || item?.quality ||
(layer >= 0.24 (layer >= 0.24 ? 'draft' : layer <= 0.12 ? 'extra_fine' : 'standard'),
? 'draft'
: layer <= 0.12
? 'extra_fine'
: 'standard'),
), ),
nozzleDiameter: this.normalizeNumber( nozzleDiameter: this.normalizeNumber(
item?.nozzleDiameterMm, item?.nozzleDiameterMm,
@@ -616,9 +621,7 @@ export class CalculatorPageComponent implements OnInit {
infillPattern: this.normalizeString( infillPattern: this.normalizeString(
item?.infillPattern || fallback.infillPattern, item?.infillPattern || fallback.infillPattern,
), ),
supportEnabled: Boolean( supportEnabled: Boolean(item?.supportsEnabled ?? fallback.supportEnabled),
item?.supportsEnabled ?? fallback.supportEnabled,
),
}; };
} }

View File

@@ -226,7 +226,12 @@
<label> <label>
{{ "CALC.LAYER_HEIGHT" | translate }} {{ "CALC.LAYER_HEIGHT" | translate }}
<select formControlName="layerHeight"> <select formControlName="layerHeight">
@for (l of getLayerHeightOptionsForNozzle(form.get('nozzleDiameter')?.value); track l.value) { @for (
l of getLayerHeightOptionsForNozzle(
form.get("nozzleDiameter")?.value
);
track l.value
) {
<option [value]="l.value">{{ l.label }}</option> <option [value]="l.value">{{ l.label }}</option>
} }
</select> </select>

View File

@@ -197,16 +197,28 @@ export class UploadFormComponent implements OnInit {
this.fullMaterialOptions = options.materials || []; this.fullMaterialOptions = options.materials || [];
this.materials.set( this.materials.set(
(options.materials || []).map((m) => ({ label: m.label, value: m.code })), (options.materials || []).map((m) => ({
label: m.label,
value: m.code,
})),
); );
this.qualities.set( this.qualities.set(
(options.qualities || []).map((q) => ({ label: q.label, value: q.id })), (options.qualities || []).map((q) => ({
label: q.label,
value: q.id,
})),
); );
this.infillPatterns.set( this.infillPatterns.set(
(options.infillPatterns || []).map((p) => ({ label: p.label, value: p.id })), (options.infillPatterns || []).map((p) => ({
label: p.label,
value: p.id,
})),
); );
this.nozzleDiameters.set( this.nozzleDiameters.set(
(options.nozzleDiameters || []).map((n) => ({ label: n.label, value: n.value })), (options.nozzleDiameters || []).map((n) => ({
label: n.label,
value: n.value,
})),
); );
this.allLayerHeights = (options.layerHeights || []).map((l) => ({ this.allLayerHeights = (options.layerHeights || []).map((l) => ({
@@ -282,12 +294,19 @@ export class UploadFormComponent implements OnInit {
return this.items()[index] || null; return this.items()[index] || null;
} }
getVariantsForMaterial(materialCode: string | null | undefined): VariantOption[] { getVariantsForMaterial(
const normalized = String(materialCode || '').trim().toUpperCase(); materialCode: string | null | undefined,
): VariantOption[] {
const normalized = String(materialCode || '')
.trim()
.toUpperCase();
if (!normalized) return []; if (!normalized) return [];
const found = this.fullMaterialOptions.find( const found = this.fullMaterialOptions.find(
(m) => String(m.code || '').trim().toUpperCase() === normalized, (m) =>
String(m.code || '')
.trim()
.toUpperCase() === normalized,
); );
return found?.variants || []; return found?.variants || [];
} }
@@ -462,7 +481,9 @@ export class UploadFormComponent implements OnInit {
const colorName = const colorName =
typeof newSelection === 'string' ? newSelection : newSelection.colorName; typeof newSelection === 'string' ? newSelection : newSelection.colorName;
const filamentVariantId = const filamentVariantId =
typeof newSelection === 'string' ? undefined : newSelection.filamentVariantId; typeof newSelection === 'string'
? undefined
: newSelection.filamentVariantId;
this.items.update((current) => { this.items.update((current) => {
if (index < 0 || index >= current.length) { if (index < 0 || index >= current.length) {
@@ -709,11 +730,17 @@ export class UploadFormComponent implements OnInit {
}); });
} }
if (this.nozzleDiameters().length > 0 && !this.form.get('nozzleDiameter')?.value) { if (
this.nozzleDiameters().length > 0 &&
!this.form.get('nozzleDiameter')?.value
) {
this.form.get('nozzleDiameter')?.setValue(0.4, { emitEvent: false }); this.form.get('nozzleDiameter')?.setValue(0.4, { emitEvent: false });
} }
if (this.infillPatterns().length > 0 && !this.form.get('infillPattern')?.value) { if (
this.infillPatterns().length > 0 &&
!this.form.get('infillPattern')?.value
) {
this.form this.form
.get('infillPattern') .get('infillPattern')
?.setValue(this.infillPatterns()[0].value, { emitEvent: false }); ?.setValue(this.infillPatterns()[0].value, { emitEvent: false });
@@ -726,7 +753,9 @@ export class UploadFormComponent implements OnInit {
); );
if (this.mode() === 'easy') { if (this.mode() === 'easy') {
this.applyEasyPresetFromQuality(String(this.form.get('quality')?.value || 'standard')); this.applyEasyPresetFromQuality(
String(this.form.get('quality')?.value || 'standard'),
);
} }
this.emitPrintSettingsChange(); this.emitPrintSettingsChange();
@@ -817,9 +846,18 @@ export class UploadFormComponent implements OnInit {
return { return {
material, material,
quality, quality,
nozzleDiameter: this.normalizeNumber(this.form.get('nozzleDiameter')?.value, 0.4), nozzleDiameter: this.normalizeNumber(
layerHeight: this.normalizeNumber(this.form.get('layerHeight')?.value, 0.2), this.form.get('nozzleDiameter')?.value,
infillDensity: this.normalizeNumber(this.form.get('infillDensity')?.value, 20), 0.4,
),
layerHeight: this.normalizeNumber(
this.form.get('layerHeight')?.value,
0.2,
),
infillDensity: this.normalizeNumber(
this.form.get('infillDensity')?.value,
20,
),
infillPattern: String(this.form.get('infillPattern')?.value || 'grid'), infillPattern: String(this.form.get('infillPattern')?.value || 'grid'),
supportEnabled: Boolean(this.form.get('supportEnabled')?.value), supportEnabled: Boolean(this.form.get('supportEnabled')?.value),
}; };
@@ -829,7 +867,9 @@ export class UploadFormComponent implements OnInit {
item: FormItem, item: FormItem,
defaults: ReturnType<UploadFormComponent['getCurrentGlobalItemDefaults']>, defaults: ReturnType<UploadFormComponent['getCurrentGlobalItemDefaults']>,
): QuoteRequestItem { ): QuoteRequestItem {
const quality = this.normalizeQualityValue(item.quality || defaults.quality); const quality = this.normalizeQualityValue(
item.quality || defaults.quality,
);
if (this.mode() === 'easy') { if (this.mode() === 'easy') {
const preset = this.easyModePresetForQuality(quality); const preset = this.easyModePresetForQuality(quality);
@@ -856,10 +896,16 @@ export class UploadFormComponent implements OnInit {
color: item.color, color: item.color,
filamentVariantId: item.filamentVariantId, filamentVariantId: item.filamentVariantId,
supportEnabled: item.supportEnabled, supportEnabled: item.supportEnabled,
infillDensity: this.normalizeNumber(item.infillDensity, defaults.infillDensity), infillDensity: this.normalizeNumber(
item.infillDensity,
defaults.infillDensity,
),
infillPattern: item.infillPattern || defaults.infillPattern, infillPattern: item.infillPattern || defaults.infillPattern,
layerHeight: this.normalizeNumber(item.layerHeight, defaults.layerHeight), layerHeight: this.normalizeNumber(item.layerHeight, defaults.layerHeight),
nozzleDiameter: this.normalizeNumber(item.nozzleDiameter, defaults.nozzleDiameter), nozzleDiameter: this.normalizeNumber(
item.nozzleDiameter,
defaults.nozzleDiameter,
),
}; };
} }
@@ -1029,11 +1075,15 @@ export class UploadFormComponent implements OnInit {
return; return;
} }
const currentLayer = this.normalizeNumber(this.form.get('layerHeight')?.value, options[0].value as number); const currentLayer = this.normalizeNumber(
this.form.get('layerHeight')?.value,
options[0].value as number,
);
const allowed = options.some( const allowed = options.some(
(option) => (option) =>
Math.abs(this.normalizeNumber(option.value, currentLayer) - currentLayer) < Math.abs(
0.0001, this.normalizeNumber(option.value, currentLayer) - currentLayer,
) < 0.0001,
); );
if (allowed) { if (allowed) {
@@ -1076,13 +1126,17 @@ export class UploadFormComponent implements OnInit {
this.items().forEach((item) => { this.items().forEach((item) => {
const differences: string[] = []; const differences: string[] = [];
if (this.normalizeText(item.material) !== this.normalizeText(baseline.material)) { if (
this.normalizeText(item.material) !==
this.normalizeText(baseline.material)
) {
differences.push(item.material.toUpperCase()); differences.push(item.material.toUpperCase());
} }
if (this.mode() === 'easy') { if (this.mode() === 'easy') {
if ( if (
this.normalizeText(item.quality) !== this.normalizeText(baseline.quality) this.normalizeText(item.quality) !==
this.normalizeText(baseline.quality)
) { ) {
differences.push(`quality:${item.quality}`); differences.push(`quality:${item.quality}`);
} }
@@ -1173,19 +1227,28 @@ export class UploadFormComponent implements OnInit {
return ( return (
this.normalizeText(a.material) === this.normalizeText(b.material) && this.normalizeText(a.material) === this.normalizeText(b.material) &&
this.normalizeText(a.quality) === this.normalizeText(b.quality) && this.normalizeText(a.quality) === this.normalizeText(b.quality) &&
Math.abs(this.normalizeNumber(a.nozzleDiameter, 0.4) - this.normalizeNumber(b.nozzleDiameter, 0.4)) < Math.abs(
0.0001 && this.normalizeNumber(a.nozzleDiameter, 0.4) -
Math.abs(this.normalizeNumber(a.layerHeight, 0.2) - this.normalizeNumber(b.layerHeight, 0.2)) < this.normalizeNumber(b.nozzleDiameter, 0.4),
0.0001 && ) < 0.0001 &&
Math.abs(this.normalizeNumber(a.infillDensity, 20) - this.normalizeNumber(b.infillDensity, 20)) < Math.abs(
0.0001 && this.normalizeNumber(a.layerHeight, 0.2) -
this.normalizeText(a.infillPattern) === this.normalizeText(b.infillPattern) && this.normalizeNumber(b.layerHeight, 0.2),
) < 0.0001 &&
Math.abs(
this.normalizeNumber(a.infillDensity, 20) -
this.normalizeNumber(b.infillDensity, 20),
) < 0.0001 &&
this.normalizeText(a.infillPattern) ===
this.normalizeText(b.infillPattern) &&
Boolean(a.supportEnabled) === Boolean(b.supportEnabled) Boolean(a.supportEnabled) === Boolean(b.supportEnabled)
); );
} }
private normalizeQualityValue(value: any): string { private normalizeQualityValue(value: any): string {
const normalized = String(value || 'standard').trim().toLowerCase(); const normalized = String(value || 'standard')
.trim()
.toLowerCase();
if (normalized === 'high' || normalized === 'high_definition') { if (normalized === 'high' || normalized === 'high_definition') {
return 'extra_fine'; return 'extra_fine';
} }

View File

@@ -127,16 +127,22 @@ export class QuoteEstimatorService {
getOptions(): Observable<OptionsResponse> { getOptions(): Observable<OptionsResponse> {
const headers: any = {}; const headers: any = {};
return this.http.get<OptionsResponse>(`${environment.apiUrl}/api/calculator/options`, { return this.http.get<OptionsResponse>(
headers, `${environment.apiUrl}/api/calculator/options`,
}); {
headers,
},
);
} }
getQuoteSession(sessionId: string): Observable<any> { getQuoteSession(sessionId: string): Observable<any> {
const headers: any = {}; const headers: any = {};
return this.http.get(`${environment.apiUrl}/api/quote-sessions/${sessionId}`, { return this.http.get(
headers, `${environment.apiUrl}/api/quote-sessions/${sessionId}`,
}); {
headers,
},
);
} }
updateLineItem(lineItemId: string, changes: any): Observable<any> { updateLineItem(lineItemId: string, changes: any): Observable<any> {
@@ -175,10 +181,13 @@ export class QuoteEstimatorService {
getOrderInvoice(orderId: string): Observable<Blob> { getOrderInvoice(orderId: string): Observable<Blob> {
const headers: any = {}; const headers: any = {};
return this.http.get(`${environment.apiUrl}/api/orders/${orderId}/invoice`, { return this.http.get(
headers, `${environment.apiUrl}/api/orders/${orderId}/invoice`,
responseType: 'blob', {
}); headers,
responseType: 'blob',
},
);
} }
getOrderConfirmation(orderId: string): Observable<Blob> { getOrderConfirmation(orderId: string): Observable<Blob> {
@@ -226,7 +235,8 @@ export class QuoteEstimatorService {
const emitProgress = () => { const emitProgress = () => {
const avg = Math.round( const avg = Math.round(
uploadProgress.reduce((sum, value) => sum + value, 0) / totalItems, uploadProgress.reduce((sum, value) => sum + value, 0) /
totalItems,
); );
observer.next(avg); observer.next(avg);
}; };
@@ -239,7 +249,9 @@ export class QuoteEstimatorService {
const hasFailure = uploadResults.some((entry) => !entry.success); const hasFailure = uploadResults.some((entry) => !entry.success);
if (hasFailure) { if (hasFailure) {
observer.error('One or more files failed during upload/analysis'); observer.error(
'One or more files failed during upload/analysis',
);
return; return;
} }
@@ -412,8 +424,13 @@ export class QuoteEstimatorService {
}; };
} }
private buildSettingsPayload(request: QuoteRequest, item: QuoteRequestItem): any { private buildSettingsPayload(
const normalizedQuality = this.normalizeQuality(item.quality || request.quality); request: QuoteRequest,
item: QuoteRequestItem,
): any {
const normalizedQuality = this.normalizeQuality(
item.quality || request.quality,
);
const easyPreset = const easyPreset =
request.mode === 'easy' request.mode === 'easy'
? this.buildEasyModePreset(normalizedQuality) ? this.buildEasyModePreset(normalizedQuality)
@@ -427,7 +444,10 @@ export class QuoteEstimatorService {
quality: easyPreset ? easyPreset.quality : normalizedQuality, quality: easyPreset ? easyPreset.quality : normalizedQuality,
supportsEnabled: item.supportEnabled ?? request.supportEnabled ?? false, supportsEnabled: item.supportEnabled ?? request.supportEnabled ?? false,
layerHeight: layerHeight:
easyPreset?.layerHeight ?? item.layerHeight ?? request.layerHeight ?? 0.2, easyPreset?.layerHeight ??
item.layerHeight ??
request.layerHeight ??
0.2,
infillDensity: infillDensity:
easyPreset?.infillDensity ?? easyPreset?.infillDensity ??
item.infillDensity ?? item.infillDensity ??
@@ -447,7 +467,9 @@ export class QuoteEstimatorService {
} }
private normalizeQuality(value: string | undefined): string { private normalizeQuality(value: string | undefined): string {
const normalized = String(value || 'standard').trim().toLowerCase(); const normalized = String(value || 'standard')
.trim()
.toLowerCase();
if (normalized === 'high' || normalized === 'high_definition') { if (normalized === 'high' || normalized === 'high_definition') {
return 'extra_fine'; return 'extra_fine';
} }

View File

@@ -259,7 +259,10 @@
{{ item.printTimeSeconds / 3600 | number: "1.1-1" }}h | {{ item.printTimeSeconds / 3600 | number: "1.1-1" }}h |
{{ item.materialGrams | number: "1.0-0" }}g {{ item.materialGrams | number: "1.0-0" }}g
</div> </div>
<div class="item-preview" *ngIf="isCadSession() && isStlItem(item)"> <div
class="item-preview"
*ngIf="isCadSession() && isStlItem(item)"
>
<ng-container <ng-container
*ngIf="previewFile(item) as itemPreview; else previewState" *ngIf="previewFile(item) as itemPreview; else previewState"
> >