produzione 1 #9
41
frontend/src/app/core/constants/colors.const.ts
Normal file
41
frontend/src/app/core/constants/colors.const.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export interface ColorOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
hex: string;
|
||||||
|
outOfStock?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColorCategory {
|
||||||
|
name: string; // 'Glossy' | 'Matte'
|
||||||
|
colors: ColorOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PRODUCT_COLORS: ColorCategory[] = [
|
||||||
|
{
|
||||||
|
name: 'Lucidi', // Glossy
|
||||||
|
colors: [
|
||||||
|
{ label: 'Black', value: 'Black', hex: '#1a1a1a' }, // Not pure black for visibility
|
||||||
|
{ label: 'White', value: 'White', hex: '#f5f5f5' },
|
||||||
|
{ label: 'Red', value: 'Red', hex: '#d32f2f', outOfStock: true },
|
||||||
|
{ label: 'Blue', value: 'Blue', hex: '#1976d2' },
|
||||||
|
{ label: 'Green', value: 'Green', hex: '#388e3c' },
|
||||||
|
{ label: 'Yellow', value: 'Yellow', hex: '#fbc02d' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Opachi', // Matte
|
||||||
|
colors: [
|
||||||
|
{ label: 'Matte Black', value: 'Matte Black', hex: '#2c2c2c' }, // Lighter charcoal for matte
|
||||||
|
{ label: 'Matte White', value: 'Matte White', hex: '#e0e0e0' },
|
||||||
|
{ label: 'Matte Gray', value: 'Matte Gray', hex: '#757575' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getColorHex(value: string): string {
|
||||||
|
for (const cat of PRODUCT_COLORS) {
|
||||||
|
const found = cat.colors.find(c => c.value === value);
|
||||||
|
if (found) return found.hex;
|
||||||
|
}
|
||||||
|
return '#facf0a'; // Default Brand Color if not found
|
||||||
|
}
|
||||||
@@ -72,11 +72,14 @@ export class CalculatorPageComponent {
|
|||||||
|
|
||||||
details += `- File:\n`;
|
details += `- File:\n`;
|
||||||
req.items.forEach(item => {
|
req.items.forEach(item => {
|
||||||
details += ` * ${item.file.name} (Qtà: ${item.quantity})\n`;
|
details += ` * ${item.file.name} (Qtà: ${item.quantity}`;
|
||||||
|
if (item.color) {
|
||||||
|
details += `, Colore: ${item.color}`;
|
||||||
|
}
|
||||||
|
details += `)\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.mode === 'advanced') {
|
if (req.mode === 'advanced') {
|
||||||
if (req.color) details += `- Colore: ${req.color}\n`;
|
|
||||||
if (req.infillDensity) details += `- Infill: ${req.infillDensity}%\n`;
|
if (req.infillDensity) details += `- Infill: ${req.infillDensity}%\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
@if (selectedFile()) {
|
@if (selectedFile()) {
|
||||||
<div class="viewer-wrapper">
|
<div class="viewer-wrapper">
|
||||||
<app-stl-viewer [file]="selectedFile()"></app-stl-viewer>
|
<app-stl-viewer
|
||||||
|
[file]="selectedFile()"
|
||||||
|
[color]="getSelectedFileColor()">
|
||||||
|
</app-stl-viewer>
|
||||||
<!-- Close button removed as requested -->
|
<!-- Close button removed as requested -->
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -29,8 +32,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<div class="card-controls">
|
||||||
<div class="qty-group">
|
<div class="qty-group">
|
||||||
<label>Qtà</label>
|
<label>QTÀ</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -40,6 +44,15 @@
|
|||||||
(click)="$event.stopPropagation()">
|
(click)="$event.stopPropagation()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="color-group">
|
||||||
|
<label>COLORE</label>
|
||||||
|
<app-color-selector
|
||||||
|
[selectedColor]="item.color"
|
||||||
|
(colorSelected)="updateItemColor(i, $event)">
|
||||||
|
</app-color-selector>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn-remove" (click)="removeItem(i); $event.stopPropagation()" title="Remove file">
|
<button type="button" class="btn-remove" (click)="removeItem(i); $event.stopPropagation()" title="Remove file">
|
||||||
X
|
X
|
||||||
</button>
|
</button>
|
||||||
@@ -81,12 +94,6 @@
|
|||||||
|
|
||||||
@if (mode() === 'advanced') {
|
@if (mode() === 'advanced') {
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<app-select
|
|
||||||
formControlName="color"
|
|
||||||
[label]="'CALC.COLOR' | translate"
|
|
||||||
[options]="colors"
|
|
||||||
></app-select>
|
|
||||||
|
|
||||||
<app-select
|
<app-select
|
||||||
formControlName="infillPattern"
|
formControlName="infillPattern"
|
||||||
[label]="'CALC.PATTERN' | translate"
|
[label]="'CALC.PATTERN' | translate"
|
||||||
|
|||||||
@@ -16,18 +16,18 @@
|
|||||||
/* Grid Layout for Files */
|
/* Grid Layout for Files */
|
||||||
.items-grid {
|
.items-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr 1fr; /* Force 2 columns on mobile */
|
||||||
gap: var(--space-3);
|
gap: var(--space-2); /* Tighten gap for mobile */
|
||||||
margin-top: var(--space-4);
|
margin-top: var(--space-4);
|
||||||
margin-bottom: var(--space-4);
|
margin-bottom: var(--space-4);
|
||||||
|
|
||||||
@media(min-width: 640px) {
|
@media(min-width: 640px) {
|
||||||
grid-template-columns: 1fr 1fr;
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-card {
|
.file-card {
|
||||||
padding: var(--space-3);
|
padding: var(--space-2); /* Reduced from space-3 */
|
||||||
background: var(--color-neutral-100);
|
background: var(--color-neutral-100);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
@@ -35,7 +35,9 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-2);
|
gap: 4px; /* Reduced gap */
|
||||||
|
position: relative; /* For absolute positioning of remove btn */
|
||||||
|
min-width: 0; /* Allow flex item to shrink below content size if needed */
|
||||||
|
|
||||||
&:hover { border-color: var(--color-neutral-300); }
|
&:hover { border-color: var(--color-neutral-300); }
|
||||||
&.active {
|
&.active {
|
||||||
@@ -47,11 +49,13 @@
|
|||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding-right: 25px; /* Adjusted */
|
||||||
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name {
|
.file-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.85rem;
|
font-size: 0.8rem; /* Smaller font */
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -61,34 +65,64 @@
|
|||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qty-group {
|
.card-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-end; /* Align bottom of input and color circle */
|
||||||
gap: var(--space-2);
|
gap: 16px; /* Space between Qty and Color */
|
||||||
label { font-size: 0.75rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-group, .color-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Stack label and input */
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-group {
|
||||||
|
align-items: flex-start; /* Align label left */
|
||||||
|
/* margin-right removed */
|
||||||
|
|
||||||
|
/* Override margin in selector for this context */
|
||||||
|
::ng-deep .color-selector-container {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.qty-input {
|
.qty-input {
|
||||||
width: 40px;
|
width: 36px; /* Slightly smaller */
|
||||||
padding: 2px 4px;
|
padding: 1px 2px;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
background: white;
|
background: white;
|
||||||
|
height: 24px; /* Explicit height to match color circle somewhat */
|
||||||
&:focus { outline: none; border-color: var(--color-brand); }
|
&:focus { outline: none; border-color: var(--color-brand); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-remove {
|
.btn-remove {
|
||||||
width: 24px;
|
position: absolute;
|
||||||
height: 24px;
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid transparent; // var(--color-border);
|
border: none;
|
||||||
background: transparent; // white;
|
background: transparent;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -96,12 +130,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-danger-100);
|
background: var(--color-danger-100);
|
||||||
color: var(--color-danger-500);
|
color: var(--color-danger-500);
|
||||||
border-color: var(--color-danger-200);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,20 @@ import { AppSelectComponent } from '../../../../shared/components/app-select/app
|
|||||||
import { AppDropzoneComponent } from '../../../../shared/components/app-dropzone/app-dropzone.component';
|
import { AppDropzoneComponent } from '../../../../shared/components/app-dropzone/app-dropzone.component';
|
||||||
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
|
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
|
||||||
import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl-viewer.component';
|
import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl-viewer.component';
|
||||||
|
import { ColorSelectorComponent } from '../../../../shared/components/color-selector/color-selector.component';
|
||||||
import { QuoteRequest } from '../../services/quote-estimator.service';
|
import { QuoteRequest } from '../../services/quote-estimator.service';
|
||||||
|
import { getColorHex } from '../../../../core/constants/colors.const';
|
||||||
|
|
||||||
interface FormItem {
|
interface FormItem {
|
||||||
file: File;
|
file: File;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-upload-form',
|
selector: 'app-upload-form',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppInputComponent, AppSelectComponent, AppDropzoneComponent, AppButtonComponent, StlViewerComponent],
|
imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppInputComponent, AppSelectComponent, AppDropzoneComponent, AppButtonComponent, StlViewerComponent, ColorSelectorComponent],
|
||||||
templateUrl: './upload-form.component.html',
|
templateUrl: './upload-form.component.html',
|
||||||
styleUrl: './upload-form.component.scss'
|
styleUrl: './upload-form.component.scss'
|
||||||
})
|
})
|
||||||
@@ -44,15 +47,6 @@ export class UploadFormComponent {
|
|||||||
{ label: 'Alta definizione', value: 'High' }
|
{ label: 'Alta definizione', value: 'High' }
|
||||||
];
|
];
|
||||||
|
|
||||||
colors = [
|
|
||||||
{ label: 'Black', value: 'Black' },
|
|
||||||
{ label: 'White', value: 'White' },
|
|
||||||
{ label: 'Gray', value: 'Gray' },
|
|
||||||
{ label: 'Red', value: 'Red' },
|
|
||||||
{ label: 'Blue', value: 'Blue' },
|
|
||||||
{ label: 'Green', value: 'Green' },
|
|
||||||
{ label: 'Yellow', value: 'Yellow' }
|
|
||||||
];
|
|
||||||
infillPatterns = [
|
infillPatterns = [
|
||||||
{ label: 'Grid', value: 'grid' },
|
{ label: 'Grid', value: 'grid' },
|
||||||
{ label: 'Gyroid', value: 'gyroid' },
|
{ label: 'Gyroid', value: 'gyroid' },
|
||||||
@@ -69,7 +63,7 @@ export class UploadFormComponent {
|
|||||||
quality: ['Standard', Validators.required],
|
quality: ['Standard', Validators.required],
|
||||||
notes: [''],
|
notes: [''],
|
||||||
// Advanced fields
|
// Advanced fields
|
||||||
color: ['Black'],
|
// Color removed from global form
|
||||||
infillDensity: [20, [Validators.min(0), Validators.max(100)]],
|
infillDensity: [20, [Validators.min(0), Validators.max(100)]],
|
||||||
infillPattern: ['grid'],
|
infillPattern: ['grid'],
|
||||||
supportEnabled: [false]
|
supportEnabled: [false]
|
||||||
@@ -85,7 +79,8 @@ export class UploadFormComponent {
|
|||||||
if (file.size > MAX_SIZE) {
|
if (file.size > MAX_SIZE) {
|
||||||
hasError = true;
|
hasError = true;
|
||||||
} else {
|
} else {
|
||||||
validItems.push({ file, quantity: 1 });
|
// Default color is Black
|
||||||
|
validItems.push({ file, quantity: 1, color: 'Black' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +124,18 @@ export class UploadFormComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to get color of currently selected file
|
||||||
|
getSelectedFileColor(): string {
|
||||||
|
const file = this.selectedFile();
|
||||||
|
if (!file) return '#facf0a'; // Default
|
||||||
|
|
||||||
|
const item = this.items().find(i => i.file === file);
|
||||||
|
if (item) {
|
||||||
|
return getColorHex(item.color);
|
||||||
|
}
|
||||||
|
return '#facf0a';
|
||||||
|
}
|
||||||
|
|
||||||
updateItemQuantity(index: number, event: Event) {
|
updateItemQuantity(index: number, event: Event) {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
let val = parseInt(input.value, 10);
|
let val = parseInt(input.value, 10);
|
||||||
@@ -141,6 +148,14 @@ export class UploadFormComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateItemColor(index: number, newColor: string) {
|
||||||
|
this.items.update(current => {
|
||||||
|
const updated = [...current];
|
||||||
|
updated[index] = { ...updated[index], color: newColor };
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
removeItem(index: number) {
|
removeItem(index: number) {
|
||||||
this.items.update(current => {
|
this.items.update(current => {
|
||||||
const updated = [...current];
|
const updated = [...current];
|
||||||
@@ -155,7 +170,7 @@ export class UploadFormComponent {
|
|||||||
onSubmit() {
|
onSubmit() {
|
||||||
if (this.form.valid && this.items().length > 0) {
|
if (this.form.valid && this.items().length > 0) {
|
||||||
this.submitRequest.emit({
|
this.submitRequest.emit({
|
||||||
items: this.items(), // Pass the items array
|
items: this.items(), // Pass the items array including colors
|
||||||
...this.form.value,
|
...this.form.value,
|
||||||
mode: this.mode()
|
mode: this.mode()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { map, catchError } from 'rxjs/operators';
|
|||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
export interface QuoteRequest {
|
export interface QuoteRequest {
|
||||||
items: { file: File, quantity: number }[];
|
items: { file: File, quantity: number, color?: string }[];
|
||||||
material: string;
|
material: string;
|
||||||
quality: string;
|
quality: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
color?: string;
|
// color removed from global scope
|
||||||
infillDensity?: number;
|
infillDensity?: number;
|
||||||
infillPattern?: string;
|
infillPattern?: string;
|
||||||
supportEnabled?: boolean;
|
supportEnabled?: boolean;
|
||||||
@@ -69,8 +69,11 @@ export class QuoteEstimatorService {
|
|||||||
formData.append('machine', 'bambu_a1');
|
formData.append('machine', 'bambu_a1');
|
||||||
formData.append('filament', this.mapMaterial(request.material));
|
formData.append('filament', this.mapMaterial(request.material));
|
||||||
formData.append('quality', this.mapQuality(request.quality));
|
formData.append('quality', this.mapQuality(request.quality));
|
||||||
|
|
||||||
|
// Send color for both modes if present, defaulting to Black
|
||||||
|
formData.append('material_color', item.color || 'Black');
|
||||||
|
|
||||||
if (request.mode === 'advanced') {
|
if (request.mode === 'advanced') {
|
||||||
if (request.color) formData.append('material_color', request.color);
|
|
||||||
if (request.infillDensity) formData.append('infill_density', request.infillDensity.toString());
|
if (request.infillDensity) formData.append('infill_density', request.infillDensity.toString());
|
||||||
if (request.infillPattern) formData.append('infill_pattern', request.infillPattern);
|
if (request.infillPattern) formData.append('infill_pattern', request.infillPattern);
|
||||||
if (request.supportEnabled) formData.append('support_enabled', 'true');
|
if (request.supportEnabled) formData.append('support_enabled', 'true');
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<div class="color-selector-container">
|
||||||
|
@if (isOpen()) {
|
||||||
|
<div class="backdrop" (click)="close()"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="color-circle trigger"
|
||||||
|
[style.background-color]="getCurrentHex()"
|
||||||
|
[title]="selectedColor()"
|
||||||
|
(click)="toggleOpen()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isOpen()) {
|
||||||
|
<div class="color-popup">
|
||||||
|
@for (category of categories; track category.name) {
|
||||||
|
<div class="category">
|
||||||
|
<div class="category-name">{{ category.name }}</div>
|
||||||
|
<div class="colors-grid">
|
||||||
|
@for (color of category.colors; track color.value) {
|
||||||
|
<div
|
||||||
|
class="color-item"
|
||||||
|
(click)="selectColor(color)"
|
||||||
|
[class.disabled]="color.outOfStock">
|
||||||
|
|
||||||
|
<div class="selection-ring"
|
||||||
|
[class.active]="selectedColor() === color.value"
|
||||||
|
[class.out-of-stock]="color.outOfStock">
|
||||||
|
<div class="color-circle small" [style.background-color]="color.hex"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="color-name">{{ color.label }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
.color-selector-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
// margin-left: 10px; // Handled by parent now
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 999;
|
||||||
|
background: transparent;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-circle {
|
||||||
|
width: 20px; /* Reduced from 24px */
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||||
|
|
||||||
|
&.trigger:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-popup {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px); // Explicit gap
|
||||||
|
left: -2px; // Align left edge with slight offset
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px; /* Slightly tighter radius */
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 1000;
|
||||||
|
width: 230px; /* Increased size */
|
||||||
|
|
||||||
|
// Little triangle arrow
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
left: 8px; // Align arrow to left side near trigger
|
||||||
|
border-width: 0 6px 6px 6px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent transparent white transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colors-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr); /* 3 columns for better alignment */
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover .selection-ring {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
&:hover .selection-ring { transform: none; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-ring {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 2px; /* Space for ring */
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: var(--color-brand, #facf0a);
|
||||||
|
box-shadow: 0 0 0 1px var(--color-brand, #facf0a);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.out-of-stock::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: #cc0000;
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-name {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: #444;
|
||||||
|
line-height: 1.1;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Component, input, output, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { PRODUCT_COLORS, getColorHex, ColorCategory, ColorOption } from '../../../core/constants/colors.const';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-color-selector',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, TranslateModule],
|
||||||
|
templateUrl: './color-selector.component.html',
|
||||||
|
styleUrl: './color-selector.component.scss'
|
||||||
|
})
|
||||||
|
export class ColorSelectorComponent {
|
||||||
|
selectedColor = input<string>('Black');
|
||||||
|
colorSelected = output<string>();
|
||||||
|
|
||||||
|
isOpen = signal(false);
|
||||||
|
|
||||||
|
categories: ColorCategory[] = PRODUCT_COLORS;
|
||||||
|
|
||||||
|
toggleOpen() {
|
||||||
|
this.isOpen.update(v => !v);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectColor(color: ColorOption) {
|
||||||
|
if (color.outOfStock) return;
|
||||||
|
|
||||||
|
this.colorSelected.emit(color.value);
|
||||||
|
this.isOpen.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to find hex for the current selected value
|
||||||
|
getCurrentHex(): string {
|
||||||
|
return getColorHex(this.selectedColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.isOpen.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|||||||
})
|
})
|
||||||
export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||||
@Input() file: File | null = null;
|
@Input() file: File | null = null;
|
||||||
|
@Input() color: string = '#facf0a'; // Default Brand Color
|
||||||
|
|
||||||
@ViewChild('rendererContainer', { static: true }) rendererContainer!: ElementRef;
|
@ViewChild('rendererContainer', { static: true }) rendererContainer!: ElementRef;
|
||||||
|
|
||||||
private scene!: THREE.Scene;
|
private scene!: THREE.Scene;
|
||||||
@@ -34,6 +36,12 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
if (changes['file'] && this.file) {
|
if (changes['file'] && this.file) {
|
||||||
this.loadFile(this.file);
|
this.loadFile(this.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changes['color'] && this.currentMesh && !changes['file']) {
|
||||||
|
// Update existing mesh color if only color changed
|
||||||
|
const mat = this.currentMesh.material as THREE.MeshPhongMaterial;
|
||||||
|
mat.color.set(this.color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@@ -99,7 +107,7 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const material = new THREE.MeshPhongMaterial({
|
const material = new THREE.MeshPhongMaterial({
|
||||||
color: 0xFACF0A, // Brand color
|
color: this.color,
|
||||||
specular: 0x111111,
|
specular: 0x111111,
|
||||||
shininess: 200
|
shininess: 200
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user