feat(back-end and front-end): calculator improvements
This commit is contained in:
@@ -193,17 +193,22 @@
|
||||
</p>
|
||||
<p class="item-meta">
|
||||
Qta: {{ item.quantity }} | Materiale:
|
||||
{{ item.materialCode || "-" }} | Colore:
|
||||
{{ getItemMaterialLabel(item) }} | Colore:
|
||||
<span
|
||||
class="color-swatch"
|
||||
*ngIf="isHexColor(item.colorCode)"
|
||||
[style.background-color]="item.colorCode"
|
||||
*ngIf="getItemColorHex(item) as colorHex"
|
||||
[style.background-color]="colorHex"
|
||||
></span>
|
||||
<span>{{ item.colorCode || "-" }}</span>
|
||||
<span>
|
||||
{{ getItemColorLabel(item) }}
|
||||
<ng-container *ngIf="getItemColorCodeSuffix(item) as colorCode">
|
||||
({{ colorCode }})
|
||||
</ng-container>
|
||||
</span>
|
||||
| Nozzle: {{ item.nozzleDiameterMm ?? "-" }} mm | Layer:
|
||||
{{ item.layerHeightMm ?? "-" }} mm | Infill:
|
||||
{{ item.infillPercent ?? "-" }}% | Supporti:
|
||||
{{ item.supportsEnabled ? "Sì" : "No" }}
|
||||
{{ formatSupports(item.supportsEnabled) }}
|
||||
| Riga:
|
||||
{{ item.lineTotalChf | currency: "CHF" : "symbol" : "1.2-2" }}
|
||||
</p>
|
||||
@@ -283,10 +288,11 @@
|
||||
<div class="file-color-row" *ngFor="let item of selectedOrder.items">
|
||||
<span class="filename">{{ item.originalFilename }}</span>
|
||||
<span class="file-color">
|
||||
{{ item.materialCode || "-" }} | {{ item.nozzleDiameterMm ?? "-" }} mm
|
||||
{{ getItemMaterialLabel(item) }} | Colore:
|
||||
{{ getItemColorLabel(item) }} | {{ item.nozzleDiameterMm ?? "-" }} mm
|
||||
| {{ item.layerHeightMm ?? "-" }} mm |
|
||||
{{ item.infillPercent ?? "-" }}% | {{ item.infillPattern || "-" }} |
|
||||
{{ item.supportsEnabled ? "Supporti ON" : "Supporti OFF" }}
|
||||
{{ formatSupportsState(item.supportsEnabled) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component, inject, OnInit } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
AdminOrder,
|
||||
AdminOrderItem,
|
||||
AdminOrdersService,
|
||||
} from '../services/admin-orders.service';
|
||||
import { CopyOnClickDirective } from '../../../shared/directives/copy-on-click.directive';
|
||||
@@ -273,6 +274,68 @@ export class AdminDashboardComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
getItemMaterialLabel(item: AdminOrderItem): string {
|
||||
const variantName = (item.filamentVariantDisplayName || '').trim();
|
||||
const materialCode = (item.materialCode || '').trim();
|
||||
if (!variantName) {
|
||||
return materialCode || '-';
|
||||
}
|
||||
if (!materialCode) {
|
||||
return variantName;
|
||||
}
|
||||
const normalizedVariant = variantName.toLowerCase();
|
||||
const normalizedCode = materialCode.toLowerCase();
|
||||
return normalizedVariant.includes(normalizedCode)
|
||||
? variantName
|
||||
: `${variantName} (${materialCode})`;
|
||||
}
|
||||
|
||||
getItemColorLabel(item: AdminOrderItem): string {
|
||||
const colorName = (item.filamentColorName || '').trim();
|
||||
const colorCode = (item.colorCode || '').trim();
|
||||
return colorName || colorCode || '-';
|
||||
}
|
||||
|
||||
getItemColorHex(item: AdminOrderItem): string | null {
|
||||
const variantHex = (item.filamentColorHex || '').trim();
|
||||
if (this.isHexColor(variantHex)) {
|
||||
return variantHex;
|
||||
}
|
||||
const code = (item.colorCode || '').trim();
|
||||
if (this.isHexColor(code)) {
|
||||
return code;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getItemColorCodeSuffix(item: AdminOrderItem): string | null {
|
||||
const colorHex = this.getItemColorHex(item);
|
||||
if (!colorHex) {
|
||||
return null;
|
||||
}
|
||||
return colorHex === this.getItemColorLabel(item) ? null : colorHex;
|
||||
}
|
||||
|
||||
formatSupports(value?: boolean): string {
|
||||
if (value === true) {
|
||||
return 'Sì';
|
||||
}
|
||||
if (value === false) {
|
||||
return 'No';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
formatSupportsState(value?: boolean): string {
|
||||
if (value === true) {
|
||||
return 'Supporti ON';
|
||||
}
|
||||
if (value === false) {
|
||||
return 'Supporti OFF';
|
||||
}
|
||||
return 'Supporti -';
|
||||
}
|
||||
|
||||
isSelected(orderId: string): boolean {
|
||||
return this.selectedOrder?.id === orderId;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ export interface AdminOrderItem {
|
||||
originalFilename: string;
|
||||
materialCode: string;
|
||||
colorCode: string;
|
||||
filamentVariantId?: number;
|
||||
filamentVariantDisplayName?: string;
|
||||
filamentColorName?: string;
|
||||
filamentColorHex?: string;
|
||||
quality?: string;
|
||||
nozzleDiameterMm?: number;
|
||||
layerHeightMm?: number;
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<div class="result-grid">
|
||||
<app-summary-card
|
||||
class="item full-width"
|
||||
[label]="'CALC.COST' | translate"
|
||||
[label]="'CHECKOUT.SUBTOTAL' | translate"
|
||||
[large]="true"
|
||||
[highlight]="true"
|
||||
>
|
||||
{{ totals().price | currency: result().currency }}
|
||||
{{ costBreakdown().subtotal | currency: result().currency }}
|
||||
</app-summary-card>
|
||||
|
||||
<app-summary-card [label]="'CALC.TIME' | translate">
|
||||
@@ -22,18 +22,6 @@
|
||||
</div>
|
||||
|
||||
<div class="setup-note">
|
||||
<small>{{
|
||||
"CALC.SETUP_NOTE"
|
||||
| translate
|
||||
: { cost: (result().setupCost | currency: result().currency) }
|
||||
}}</small
|
||||
><br />
|
||||
@if ((result().cadTotal || 0) > 0) {
|
||||
<small class="shipping-note" style="color: #666">
|
||||
Servizio CAD: {{ result().cadTotal | currency: result().currency }}
|
||||
</small>
|
||||
<br />
|
||||
}
|
||||
<small class="shipping-note" style="color: #666">{{
|
||||
"CALC.SHIPPING_NOTE" | translate
|
||||
}}</small>
|
||||
@@ -63,7 +51,7 @@
|
||||
<span class="file-details">
|
||||
{{ item.unitTime / 3600 | number: "1.1-1" }}h |
|
||||
{{ item.unitWeight | number: "1.0-0" }}g |
|
||||
<span class="material-chip">{{ item.material || "N/D" }}</span>
|
||||
{{ item.material || "N/D" }}
|
||||
@if (getItemDifferenceLabel(item.fileName, item.material)) {
|
||||
|
|
||||
<small class="item-settings-diff">
|
||||
@@ -108,6 +96,25 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="cost-breakdown">
|
||||
<div class="cost-row">
|
||||
<span>Costo di Avvio</span>
|
||||
<span>{{ costBreakdown().baseSetup | currency: result().currency }}</span>
|
||||
</div>
|
||||
@if (costBreakdown().nozzleChange > 0) {
|
||||
<div class="cost-row">
|
||||
<span>Cambio Ugello</span>
|
||||
<span>{{
|
||||
costBreakdown().nozzleChange | currency: result().currency
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="cost-total">
|
||||
<span>Totale</span>
|
||||
<span>{{ costBreakdown().total | currency: result().currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div class="actions-left">
|
||||
<app-button variant="secondary" (click)="consult.emit()">
|
||||
|
||||
@@ -55,19 +55,6 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.material-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #d9d4bd;
|
||||
background: #fbf7e9;
|
||||
color: #6d5b1d;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.item-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -218,3 +205,35 @@
|
||||
color: #6f5b1a;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.cost-breakdown {
|
||||
margin-top: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.cost-row,
|
||||
.cost-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.cost-row {
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.cost-total {
|
||||
margin-top: var(--space-3);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 2px solid var(--color-border);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { QuoteResultComponent } from './quote-result.component';
|
||||
import { QuoteResult } from '../../services/quote-estimator.service';
|
||||
@@ -38,7 +39,11 @@ describe('QuoteResultComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [QuoteResultComponent, TranslateModule.forRoot()],
|
||||
imports: [
|
||||
QuoteResultComponent,
|
||||
TranslateModule.forRoot(),
|
||||
HttpClientTestingModule,
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(QuoteResultComponent);
|
||||
|
||||
@@ -134,17 +134,37 @@ export class QuoteResultComponent implements OnDestroy {
|
||||
this.items().some((item) => item.quantity > this.directOrderLimit),
|
||||
);
|
||||
|
||||
totals = computed(() => {
|
||||
costBreakdown = computed(() => {
|
||||
const currentItems = this.items();
|
||||
const setup = this.result().setupCost;
|
||||
const cad = this.result().cadTotal || 0;
|
||||
|
||||
let price = setup + cad;
|
||||
let subtotal = cad;
|
||||
currentItems.forEach((item) => {
|
||||
subtotal += item.unitPrice * item.quantity;
|
||||
});
|
||||
|
||||
const nozzleChange = Math.max(0, this.result().nozzleChangeCost || 0);
|
||||
const baseSetupRaw =
|
||||
this.result().baseSetupCost != null
|
||||
? this.result().baseSetupCost
|
||||
: this.result().setupCost - nozzleChange;
|
||||
const baseSetup = Math.max(0, baseSetupRaw || 0);
|
||||
const total = subtotal + baseSetup + nozzleChange;
|
||||
|
||||
return {
|
||||
subtotal: Math.round(subtotal * 100) / 100,
|
||||
baseSetup: Math.round(baseSetup * 100) / 100,
|
||||
nozzleChange: Math.round(nozzleChange * 100) / 100,
|
||||
total: Math.round(total * 100) / 100,
|
||||
};
|
||||
});
|
||||
|
||||
totals = computed(() => {
|
||||
const currentItems = this.items();
|
||||
let time = 0;
|
||||
let weight = 0;
|
||||
|
||||
currentItems.forEach((i) => {
|
||||
price += i.unitPrice * i.quantity;
|
||||
time += i.unitTime * i.quantity;
|
||||
weight += i.unitWeight * i.quantity;
|
||||
});
|
||||
@@ -153,7 +173,7 @@ export class QuoteResultComponent implements OnDestroy {
|
||||
const minutes = Math.ceil((time % 3600) / 60);
|
||||
|
||||
return {
|
||||
price: Math.round(price * 100) / 100,
|
||||
price: this.costBreakdown().total,
|
||||
hours,
|
||||
minutes,
|
||||
weight: Math.ceil(weight),
|
||||
|
||||
@@ -107,7 +107,27 @@
|
||||
>.
|
||||
</p>
|
||||
|
||||
@if (mode() === "advanced") {
|
||||
@if (mode() === "easy") {
|
||||
<div class="easy-global-controls">
|
||||
<label class="easy-global-field">
|
||||
<span>{{ "CALC.MATERIAL" | translate }}</span>
|
||||
<select formControlName="material">
|
||||
@for (mat of materials(); track mat.value) {
|
||||
<option [value]="mat.value">{{ mat.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="easy-global-field">
|
||||
<span>{{ "CALC.QUALITY" | translate }}</span>
|
||||
<select formControlName="quality">
|
||||
@for (quality of qualities(); track quality.value) {
|
||||
<option [value]="quality.value">{{ quality.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="sync-settings">
|
||||
<label class="sync-settings-toggle">
|
||||
<input
|
||||
|
||||
@@ -194,6 +194,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
.easy-global-controls {
|
||||
margin-top: var(--space-4);
|
||||
margin-bottom: var(--space-1);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-3);
|
||||
|
||||
@media (min-width: 640px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.easy-global-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
|
||||
span {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
select {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.55rem 0.75rem;
|
||||
background: var(--color-bg-card);
|
||||
font-size: 0.96rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-brand);
|
||||
box-shadow: 0 0 0 2px rgba(250, 207, 10, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sync-settings {
|
||||
margin-top: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
|
||||
@@ -51,6 +51,8 @@ export interface QuoteItem {
|
||||
export interface QuoteResult {
|
||||
sessionId?: string;
|
||||
items: QuoteItem[];
|
||||
baseSetupCost?: number;
|
||||
nozzleChangeCost?: number;
|
||||
setupCost: number;
|
||||
globalMachineCost: number;
|
||||
cadHours?: number;
|
||||
@@ -382,9 +384,11 @@ export class QuoteEstimatorService {
|
||||
);
|
||||
|
||||
const grandTotal = Number(sessionData?.grandTotalChf);
|
||||
const effectiveSetupCost =
|
||||
Number(sessionData?.setupCostChf ?? session?.setupCostChf ?? 0);
|
||||
const fallbackTotal =
|
||||
Number(sessionData?.itemsTotalChf || 0) +
|
||||
Number(session?.setupCostChf || 0) +
|
||||
effectiveSetupCost +
|
||||
Number(sessionData?.shippingCostChf || 0);
|
||||
|
||||
return {
|
||||
@@ -411,7 +415,11 @@ export class QuoteEstimatorService {
|
||||
? Number(item.nozzleDiameterMm)
|
||||
: undefined,
|
||||
})),
|
||||
setupCost: Number(session?.setupCostChf || 0),
|
||||
baseSetupCost: Number(
|
||||
sessionData?.baseSetupCostChf ?? session?.setupCostChf ?? 0,
|
||||
),
|
||||
nozzleChangeCost: Number(sessionData?.nozzleChangeCostChf ?? 0),
|
||||
setupCost: effectiveSetupCost,
|
||||
globalMachineCost: Number(sessionData?.globalMachineCostChf || 0),
|
||||
cadHours: Number(session?.cadHours || 0),
|
||||
cadTotal: Number(sessionData?.cadTotalChf || 0),
|
||||
|
||||
@@ -249,11 +249,13 @@
|
||||
{{ "CHECKOUT.MATERIAL" | translate }}:
|
||||
{{ itemMaterial(item) }}
|
||||
</span>
|
||||
<span
|
||||
*ngIf="item.colorCode"
|
||||
class="color-dot"
|
||||
[style.background-color]="item.colorCode"
|
||||
></span>
|
||||
<span class="item-color" *ngIf="itemColorLabel(item) !== '-'">
|
||||
<span
|
||||
class="color-dot"
|
||||
[style.background-color]="itemColorSwatch(item)"
|
||||
></span>
|
||||
<span class="color-name">{{ itemColorLabel(item) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="item-specs-sub">
|
||||
{{ item.printTimeSeconds / 3600 | number: "1.1-1" }}h |
|
||||
@@ -328,7 +330,14 @@
|
||||
</div>
|
||||
<div class="total-row">
|
||||
<span>{{ "CHECKOUT.SETUP_FEE" | translate }}</span>
|
||||
<span>{{ session.session.setupCostChf | currency: "CHF" }}</span>
|
||||
<span>{{
|
||||
(session.baseSetupCostChf ?? session.session.setupCostChf)
|
||||
| currency: "CHF"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="total-row" *ngIf="(session.nozzleChangeCostChf || 0) > 0">
|
||||
<span>Cambio Ugello</span>
|
||||
<span>{{ session.nozzleChangeCostChf | currency: "CHF" }}</span>
|
||||
</div>
|
||||
<div class="total-row">
|
||||
<span>{{ "CHECKOUT.SHIPPING" | translate }}</span>
|
||||
|
||||
@@ -230,12 +230,24 @@ app-toggle-selector.user-type-selector-compact {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
.item-color {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '../../shared/components/app-toggle-selector/app-toggle-selector.component';
|
||||
import { LanguageService } from '../../core/services/language.service';
|
||||
import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component';
|
||||
import { getColorHex } from '../../core/constants/colors.const';
|
||||
|
||||
@Component({
|
||||
selector: 'app-checkout',
|
||||
@@ -55,6 +56,8 @@ export class CheckoutComponent implements OnInit {
|
||||
selectedPreviewFile = signal<File | null>(null);
|
||||
selectedPreviewName = signal('');
|
||||
selectedPreviewColor = signal('#c9ced6');
|
||||
private variantHexById = new Map<number, string>();
|
||||
private variantHexByColorName = new Map<string, string>();
|
||||
|
||||
userTypeOptions: ToggleOption[] = [
|
||||
{ label: 'CONTACT.TYPE_PRIVATE', value: 'PRIVATE' },
|
||||
@@ -128,6 +131,8 @@ export class CheckoutComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadMaterialColorPalette();
|
||||
|
||||
this.route.queryParams.subscribe((params) => {
|
||||
this.sessionId = params['session'];
|
||||
if (!this.sessionId) {
|
||||
@@ -212,8 +217,40 @@ export class CheckoutComponent implements OnInit {
|
||||
}
|
||||
|
||||
previewColor(item: any): string {
|
||||
return this.itemColorSwatch(item);
|
||||
}
|
||||
|
||||
itemColorLabel(item: any): string {
|
||||
const raw = String(item?.colorCode ?? '').trim();
|
||||
return raw || '#c9ced6';
|
||||
return raw || '-';
|
||||
}
|
||||
|
||||
itemColorSwatch(item: any): string {
|
||||
const variantId = Number(item?.filamentVariantId);
|
||||
if (Number.isFinite(variantId) && this.variantHexById.has(variantId)) {
|
||||
return this.variantHexById.get(variantId)!;
|
||||
}
|
||||
|
||||
const raw = String(item?.colorCode ?? '').trim();
|
||||
if (!raw) {
|
||||
return '#c9ced6';
|
||||
}
|
||||
|
||||
if (this.isHexColor(raw)) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
const byName = this.variantHexByColorName.get(raw.toLowerCase());
|
||||
if (byName) {
|
||||
return byName;
|
||||
}
|
||||
|
||||
const fallback = getColorHex(raw);
|
||||
if (fallback && fallback !== '#facf0a') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return '#c9ced6';
|
||||
}
|
||||
|
||||
isPreviewLoading(item: any): boolean {
|
||||
@@ -250,6 +287,41 @@ export class CheckoutComponent implements OnInit {
|
||||
this.selectedPreviewColor.set('#c9ced6');
|
||||
}
|
||||
|
||||
private loadMaterialColorPalette(): void {
|
||||
this.quoteService.getOptions().subscribe({
|
||||
next: (options) => {
|
||||
this.variantHexById.clear();
|
||||
this.variantHexByColorName.clear();
|
||||
|
||||
for (const material of options?.materials || []) {
|
||||
for (const variant of material?.variants || []) {
|
||||
const variantId = Number(variant?.id);
|
||||
const colorHex = String(variant?.hexColor || '').trim();
|
||||
const colorName = String(variant?.colorName || '').trim();
|
||||
|
||||
if (Number.isFinite(variantId) && colorHex) {
|
||||
this.variantHexById.set(variantId, colorHex);
|
||||
}
|
||||
if (colorName && colorHex) {
|
||||
this.variantHexByColorName.set(colorName.toLowerCase(), colorHex);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.variantHexById.clear();
|
||||
this.variantHexByColorName.clear();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private isHexColor(value?: string): boolean {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
private loadStlPreviews(session: any): void {
|
||||
if (
|
||||
!this.sessionId ||
|
||||
|
||||
Reference in New Issue
Block a user