feat(back-end): add ClamAV service remember to add env and compose.deploy on server
This commit is contained in:
@@ -67,6 +67,16 @@ public class OrderItem {
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
private void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = OffsetDateTime.now();
|
||||
}
|
||||
if (quantity == null) {
|
||||
quantity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="checkout-page">
|
||||
<h2 class="section-title">Checkout</h2>
|
||||
<h2 class="section-title">{{ 'CHECKOUT.TITLE' | translate }}</h2>
|
||||
|
||||
<div class="checkout-layout">
|
||||
|
||||
@@ -15,23 +15,12 @@
|
||||
<!-- Contact Info Card -->
|
||||
<div class="form-card">
|
||||
<div class="card-header">
|
||||
<h3>Contact Information</h3>
|
||||
<h3>{{ 'CHECKOUT.CONTACT_INFO' | translate }}</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
|
||||
<!-- User Type Selector -->
|
||||
<div class="user-type-selector">
|
||||
<div class="type-option" [class.selected]="!isCompany" (click)="setCustomerType(false)">
|
||||
Private
|
||||
</div>
|
||||
<div class="type-option" [class.selected]="isCompany" (click)="setCustomerType(true)">
|
||||
Company
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<app-input formControlName="email" type="email" label="Email" [required]="true" [error]="checkoutForm.get('email')?.hasError('email') ? 'Invalid email' : null"></app-input>
|
||||
<app-input formControlName="phone" type="tel" label="Phone" [required]="true"></app-input>
|
||||
<app-input formControlName="email" type="email" [label]="'CHECKOUT.EMAIL' | translate" [required]="true" [error]="checkoutForm.get('email')?.hasError('email') ? 'Invalid email' : null"></app-input>
|
||||
<app-input formControlName="phone" type="tel" [label]="'CHECKOUT.PHONE' | translate" [required]="true"></app-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,24 +28,37 @@
|
||||
<!-- Billing Address Card -->
|
||||
<div class="form-card">
|
||||
<div class="card-header">
|
||||
<h3>Billing Address</h3>
|
||||
<h3>{{ 'CHECKOUT.BILLING_ADDR' | translate }}</h3>
|
||||
</div>
|
||||
<div class="card-content" formGroupName="billingAddress">
|
||||
<div class="form-row">
|
||||
<app-input formControlName="firstName" label="First Name" [required]="true"></app-input>
|
||||
<app-input formControlName="lastName" label="Last Name" [required]="true"></app-input>
|
||||
<app-input formControlName="firstName" [label]="'CHECKOUT.FIRST_NAME' | translate" [required]="true"></app-input>
|
||||
<app-input formControlName="lastName" [label]="'CHECKOUT.LAST_NAME' | translate" [required]="true"></app-input>
|
||||
</div>
|
||||
|
||||
<!-- Company Name (Conditional) -->
|
||||
<app-input *ngIf="isCompany" formControlName="companyName" label="Company Name" [required]="true"></app-input>
|
||||
|
||||
<app-input formControlName="addressLine1" label="Address Line 1" [required]="true"></app-input>
|
||||
<app-input formControlName="addressLine2" label="Address Line 2 (Optional)"></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>
|
||||
|
||||
<div class="form-row three-cols">
|
||||
<app-input formControlName="zip" label="ZIP Code" [required]="true"></app-input>
|
||||
<app-input formControlName="city" label="City" class="city-field" [required]="true"></app-input>
|
||||
<app-input formControlName="countryCode" label="Country" [disabled]="true" [required]="true"></app-input>
|
||||
<app-input formControlName="zip" [label]="'CHECKOUT.ZIP' | translate" [required]="true"></app-input>
|
||||
<app-input formControlName="city" [label]="'CHECKOUT.CITY' | translate" class="city-field" [required]="true"></app-input>
|
||||
<app-input formControlName="countryCode" [label]="'CHECKOUT.COUNTRY' | translate" [disabled]="true" [required]="true"></app-input>
|
||||
</div>
|
||||
|
||||
<!-- User Type Selector -->
|
||||
<div class="user-type-selector">
|
||||
<div class="type-option" [class.selected]="!isCompany" (click)="setCustomerType(false)">
|
||||
{{ 'CONTACT.TYPE_PRIVATE' | translate }}
|
||||
</div>
|
||||
<div class="type-option" [class.selected]="isCompany" (click)="setCustomerType(true)">
|
||||
{{ 'CONTACT.TYPE_COMPANY' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Company Fields (Indented with left border) -->
|
||||
<div *ngIf="isCompany" class="company-fields">
|
||||
<app-input formControlName="companyName" [label]="('CONTACT.COMPANY_NAME' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,36 +68,39 @@
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" formControlName="shippingSameAsBilling">
|
||||
<span class="checkmark"></span>
|
||||
Shipping address same as billing
|
||||
{{ 'CHECKOUT.SHIPPING_SAME' | translate }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Address Card (Conditional) -->
|
||||
<div class="form-card" *ngIf="!checkoutForm.get('shippingSameAsBilling')?.value">
|
||||
<div class="card-header">
|
||||
<h3>Shipping Address</h3>
|
||||
<h3>{{ 'CHECKOUT.SHIPPING_ADDR' | translate }}</h3>
|
||||
</div>
|
||||
<div class="card-content" formGroupName="shippingAddress">
|
||||
<div class="form-row">
|
||||
<app-input formControlName="firstName" label="First Name"></app-input>
|
||||
<app-input formControlName="lastName" label="Last Name"></app-input>
|
||||
<app-input formControlName="firstName" [label]="'CHECKOUT.FIRST_NAME' | translate"></app-input>
|
||||
<app-input formControlName="lastName" [label]="'CHECKOUT.LAST_NAME' | translate"></app-input>
|
||||
</div>
|
||||
|
||||
<app-input formControlName="companyName" label="Company (Optional)"></app-input>
|
||||
<app-input formControlName="addressLine1" label="Address Line 1"></app-input>
|
||||
<app-input formControlName="zip" label="ZIP Code"></app-input>
|
||||
<div *ngIf="isCompany" class="company-fields">
|
||||
<app-input formControlName="companyName" [label]="('CHECKOUT.COMPANY_NAME' | translate) + ' (Optional)'"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' (Optional)'"></app-input>
|
||||
</div>
|
||||
|
||||
<app-input formControlName="addressLine1" [label]="'CHECKOUT.ADDRESS_1' | translate"></app-input>
|
||||
|
||||
<div class="form-row three-cols">
|
||||
<app-input formControlName="zip" label="ZIP Code"></app-input>
|
||||
<app-input formControlName="city" label="City" class="city-field"></app-input>
|
||||
<app-input formControlName="countryCode" label="Country" [disabled]="true"></app-input>
|
||||
<app-input formControlName="zip" [label]="'CHECKOUT.ZIP' | translate"></app-input>
|
||||
<app-input formControlName="city" [label]="'CHECKOUT.CITY' | translate" class="city-field"></app-input>
|
||||
<app-input formControlName="countryCode" [label]="'CHECKOUT.COUNTRY' | translate" [disabled]="true"></app-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<app-button type="submit" [disabled]="checkoutForm.invalid || isSubmitting()" [fullWidth]="true">
|
||||
{{ isSubmitting() ? 'Processing...' : 'Place Order' }}
|
||||
{{ isSubmitting() ? ('CHECKOUT.PROCESSING' | translate) : ('CHECKOUT.PLACE_ORDER' | translate) }}
|
||||
</app-button>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +111,7 @@
|
||||
<div class="checkout-summary-section">
|
||||
<div class="form-card sticky-card">
|
||||
<div class="card-header">
|
||||
<h3>Order Summary</h3>
|
||||
<h3>{{ 'CHECKOUT.SUMMARY_TITLE' | translate }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
@@ -115,7 +120,7 @@
|
||||
<div class="item-details">
|
||||
<span class="item-name">{{ item.originalFilename }}</span>
|
||||
<div class="item-specs">
|
||||
<span>Qty: {{ item.quantity }}</span>
|
||||
<span>{{ 'CHECKOUT.QTY' | translate }}: {{ item.quantity }}</span>
|
||||
<span *ngIf="item.colorCode" class="color-dot" [style.background-color]="item.colorCode"></span>
|
||||
</div>
|
||||
<div class="item-specs-sub">
|
||||
@@ -130,17 +135,17 @@
|
||||
|
||||
<div class="summary-totals" *ngIf="quoteSession() as session">
|
||||
<div class="total-row">
|
||||
<span>Subtotal</span>
|
||||
<span>{{ (session.totalPrice - session.setupCostChf) | currency:'CHF' }}</span>
|
||||
<span>{{ 'CHECKOUT.SUBTOTAL' | translate }}</span>
|
||||
<span>{{ session.itemsTotalChf | currency:'CHF' }}</span>
|
||||
</div>
|
||||
<div class="total-row">
|
||||
<span>Setup Fee</span>
|
||||
<span>{{ session.setupCostChf | currency:'CHF' }}</span>
|
||||
<span>{{ 'CHECKOUT.SETUP_FEE' | translate }}</span>
|
||||
<span>{{ session.session.setupCostChf | currency:'CHF' }}</span>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="total-row grand-total">
|
||||
<span>Total</span>
|
||||
<span>{{ session.totalPrice | currency:'CHF' }}</span>
|
||||
<span>{{ 'CHECKOUT.TOTAL' | translate }}</span>
|
||||
<span>{{ session.grandTotalChf | currency:'CHF' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,15 +72,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* User Type Selector Styles */
|
||||
/* User Type Selector Styles - Matched with Contact Form */
|
||||
.user-type-selector {
|
||||
display: flex;
|
||||
background-color: var(--color-bg-subtle);
|
||||
background-color: var(--color-neutral-100);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 4px;
|
||||
margin-bottom: var(--space-4);
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.type-option {
|
||||
@@ -105,6 +106,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.company-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
padding-left: var(--space-4);
|
||||
border-left: 2px solid var(--color-border);
|
||||
margin-top: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.shipping-option {
|
||||
margin: var(--space-6) 0;
|
||||
}
|
||||
@@ -239,9 +250,8 @@
|
||||
}
|
||||
|
||||
.summary-totals {
|
||||
background: var(--color-bg-subtle);
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
padding-top: var(--space-4);
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-top: var(--space-4);
|
||||
|
||||
.total-row {
|
||||
@@ -249,21 +259,18 @@
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
|
||||
&.grand-total {
|
||||
color: var(--color-heading);
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
margin-top: var(--space-3);
|
||||
padding-top: var(--space-3);
|
||||
margin-top: var(--space-4);
|
||||
padding-top: var(--space-4);
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: none; // Handled by border-top in grand-total
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service';
|
||||
import { AppInputComponent } from '../../shared/components/app-input/app-input.component';
|
||||
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
||||
@@ -12,6 +13,7 @@ import { AppButtonComponent } from '../../shared/components/app-button/app-butto
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
AppInputComponent,
|
||||
AppButtonComponent
|
||||
],
|
||||
@@ -43,6 +45,7 @@ export class CheckoutComponent implements OnInit {
|
||||
firstName: ['', Validators.required],
|
||||
lastName: ['', Validators.required],
|
||||
companyName: [''],
|
||||
referencePerson: [''],
|
||||
addressLine1: ['', Validators.required],
|
||||
addressLine2: [''],
|
||||
zip: ['', Validators.required],
|
||||
@@ -54,6 +57,7 @@ export class CheckoutComponent implements OnInit {
|
||||
firstName: [''],
|
||||
lastName: [''],
|
||||
companyName: [''],
|
||||
referencePerson: [''],
|
||||
addressLine1: [''],
|
||||
addressLine2: [''],
|
||||
zip: [''],
|
||||
@@ -74,13 +78,17 @@ export class CheckoutComponent implements OnInit {
|
||||
// Update validators based on type
|
||||
const billingGroup = this.checkoutForm.get('billingAddress') as FormGroup;
|
||||
const companyControl = billingGroup.get('companyName');
|
||||
const referenceControl = billingGroup.get('referencePerson');
|
||||
|
||||
if (isCompany) {
|
||||
companyControl?.setValidators([Validators.required]);
|
||||
referenceControl?.setValidators([Validators.required]);
|
||||
} else {
|
||||
companyControl?.clearValidators();
|
||||
referenceControl?.clearValidators();
|
||||
}
|
||||
companyControl?.updateValueAndValidity();
|
||||
referenceControl?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -147,6 +155,7 @@ export class CheckoutComponent implements OnInit {
|
||||
firstName: formVal.billingAddress.firstName,
|
||||
lastName: formVal.billingAddress.lastName,
|
||||
companyName: formVal.billingAddress.companyName,
|
||||
contactPerson: formVal.billingAddress.referencePerson,
|
||||
addressLine1: formVal.billingAddress.addressLine1,
|
||||
addressLine2: formVal.billingAddress.addressLine2,
|
||||
zip: formVal.billingAddress.zip,
|
||||
@@ -157,6 +166,7 @@ export class CheckoutComponent implements OnInit {
|
||||
firstName: formVal.shippingAddress.firstName,
|
||||
lastName: formVal.shippingAddress.lastName,
|
||||
companyName: formVal.shippingAddress.companyName,
|
||||
contactPerson: formVal.shippingAddress.referencePerson,
|
||||
addressLine1: formVal.shippingAddress.addressLine1,
|
||||
addressLine2: formVal.shippingAddress.addressLine2,
|
||||
zip: formVal.shippingAddress.zip,
|
||||
|
||||
@@ -25,6 +25,7 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
private controls!: OrbitControls;
|
||||
private animationId: number | null = null;
|
||||
private currentMesh: THREE.Mesh | null = null;
|
||||
private autoRotate = true;
|
||||
|
||||
loading = false;
|
||||
|
||||
@@ -38,14 +39,14 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
}
|
||||
|
||||
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);
|
||||
this.applyColorStyle(this.color);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.animationId) cancelAnimationFrame(this.animationId);
|
||||
this.clearCurrentMesh();
|
||||
if (this.controls) this.controls.dispose();
|
||||
if (this.renderer) this.renderer.dispose();
|
||||
}
|
||||
|
||||
@@ -54,28 +55,51 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
const height = this.rendererContainer.nativeElement.clientHeight;
|
||||
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0xf7f6f2); // Neutral-50
|
||||
this.scene.background = new THREE.Color(0xf4f8fc);
|
||||
|
||||
// Lights
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.75);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
directionalLight.position.set(1, 1, 1);
|
||||
this.scene.add(directionalLight);
|
||||
const hemiLight = new THREE.HemisphereLight(0xf8fbff, 0xc8d3df, 0.95);
|
||||
hemiLight.position.set(0, 30, 0);
|
||||
this.scene.add(hemiLight);
|
||||
|
||||
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.35);
|
||||
directionalLight1.position.set(6, 8, 6);
|
||||
this.scene.add(directionalLight1);
|
||||
|
||||
const directionalLight2 = new THREE.DirectionalLight(0xe8f0ff, 0.85);
|
||||
directionalLight2.position.set(-7, 4, -5);
|
||||
this.scene.add(directionalLight2);
|
||||
|
||||
const directionalLight3 = new THREE.DirectionalLight(0xffffff, 0.55);
|
||||
directionalLight3.position.set(0, 5, -9);
|
||||
this.scene.add(directionalLight3);
|
||||
|
||||
// Camera
|
||||
this.camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000);
|
||||
this.camera.position.z = 100;
|
||||
|
||||
// Renderer
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
this.renderer.toneMappingExposure = 1.2;
|
||||
this.renderer.setSize(width, height);
|
||||
this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
|
||||
|
||||
// Controls
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.06;
|
||||
this.controls.enablePan = false;
|
||||
this.controls.minDistance = 10;
|
||||
this.controls.maxDistance = 600;
|
||||
this.controls.addEventListener('start', () => {
|
||||
this.autoRotate = false;
|
||||
});
|
||||
|
||||
this.animate();
|
||||
|
||||
@@ -95,24 +119,27 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
private loadFile(file: File) {
|
||||
this.loading = true;
|
||||
this.autoRotate = true;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const loader = new STLLoader();
|
||||
const geometry = loader.parse(event.target?.result as ArrayBuffer);
|
||||
|
||||
if (this.currentMesh) {
|
||||
this.scene.remove(this.currentMesh);
|
||||
this.currentMesh.geometry.dispose();
|
||||
}
|
||||
this.clearCurrentMesh();
|
||||
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: this.color,
|
||||
specular: 0x111111,
|
||||
shininess: 200
|
||||
roughness: 0.42,
|
||||
metalness: 0.05,
|
||||
emissive: 0x000000,
|
||||
emissiveIntensity: 0
|
||||
});
|
||||
|
||||
this.currentMesh = new THREE.Mesh(geometry, material);
|
||||
this.applyColorStyle(this.color);
|
||||
|
||||
// Center geometry
|
||||
geometry.computeBoundingBox();
|
||||
@@ -140,9 +167,10 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
// Calculate distance towards camera (z-axis)
|
||||
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
|
||||
cameraZ *= 1.5; // Tighter zoom (reduced from 2.5)
|
||||
cameraZ *= 1.72;
|
||||
|
||||
this.camera.position.z = cameraZ;
|
||||
this.camera.position.set(cameraZ * 0.65, cameraZ * 0.95, cameraZ * 1.1);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.controls.update();
|
||||
|
||||
@@ -157,9 +185,63 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
private animate() {
|
||||
this.animationId = requestAnimationFrame(() => this.animate());
|
||||
|
||||
if (this.currentMesh && this.autoRotate) {
|
||||
this.currentMesh.rotation.z += 0.0025;
|
||||
}
|
||||
|
||||
if (this.controls) this.controls.update();
|
||||
if (this.renderer && this.scene && this.camera) {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
|
||||
private clearCurrentMesh() {
|
||||
if (!this.currentMesh) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.remove(this.currentMesh);
|
||||
this.currentMesh.geometry.dispose();
|
||||
|
||||
const meshMaterial = this.currentMesh.material;
|
||||
if (Array.isArray(meshMaterial)) {
|
||||
meshMaterial.forEach((m) => m.dispose());
|
||||
} else {
|
||||
meshMaterial.dispose();
|
||||
}
|
||||
|
||||
this.currentMesh = null;
|
||||
}
|
||||
|
||||
private applyColorStyle(color: string) {
|
||||
if (!this.currentMesh) {
|
||||
return;
|
||||
}
|
||||
|
||||
const darkColor = this.isDarkColor(color);
|
||||
const meshMaterial = this.currentMesh.material;
|
||||
|
||||
if (meshMaterial instanceof THREE.MeshStandardMaterial) {
|
||||
meshMaterial.color.set(color);
|
||||
if (darkColor) {
|
||||
meshMaterial.emissive.set(0x2a2f36);
|
||||
meshMaterial.emissiveIntensity = 0.28;
|
||||
meshMaterial.roughness = 0.5;
|
||||
meshMaterial.metalness = 0.03;
|
||||
} else {
|
||||
meshMaterial.emissive.set(0x000000);
|
||||
meshMaterial.emissiveIntensity = 0;
|
||||
meshMaterial.roughness = 0.42;
|
||||
meshMaterial.metalness = 0.05;
|
||||
}
|
||||
meshMaterial.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
private isDarkColor(color: string): boolean {
|
||||
const c = new THREE.Color(color);
|
||||
const luminance = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
|
||||
return luminance < 0.22;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,5 +148,29 @@
|
||||
"SUCCESS_TITLE": "Message Sent Successfully",
|
||||
"SUCCESS_DESC": "Thank you for contacting us. We have received your message and will send you a recap email shortly.",
|
||||
"SEND_ANOTHER": "Send Another Message"
|
||||
},
|
||||
"CHECKOUT": {
|
||||
"TITLE": "Checkout",
|
||||
"CONTACT_INFO": "Contact Information",
|
||||
"BILLING_ADDR": "Billing Address",
|
||||
"SHIPPING_ADDR": "Shipping Address",
|
||||
"FIRST_NAME": "First Name",
|
||||
"LAST_NAME": "Last Name",
|
||||
"EMAIL": "Email",
|
||||
"PHONE": "Phone",
|
||||
"COMPANY_NAME": "Company Name",
|
||||
"ADDRESS_1": "Address Line 1",
|
||||
"ADDRESS_2": "Address Line 2 (Optional)",
|
||||
"ZIP": "ZIP Code",
|
||||
"CITY": "City",
|
||||
"COUNTRY": "Country",
|
||||
"SHIPPING_SAME": "Shipping address same as billing",
|
||||
"PLACE_ORDER": "Place Order",
|
||||
"PROCESSING": "Processing...",
|
||||
"SUMMARY_TITLE": "Order Summary",
|
||||
"SUBTOTAL": "Subtotal",
|
||||
"SETUP_FEE": "Setup Fee",
|
||||
"TOTAL": "Total",
|
||||
"QTY": "Qty"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,5 +127,29 @@
|
||||
"SUCCESS_TITLE": "Messaggio Inviato con Successo",
|
||||
"SUCCESS_DESC": "Grazie per averci contattato. Abbiamo ricevuto il tuo messaggio e ti invieremo una email di riepilogo a breve.",
|
||||
"SEND_ANOTHER": "Invia un altro messaggio"
|
||||
},
|
||||
"CHECKOUT": {
|
||||
"TITLE": "Checkout",
|
||||
"CONTACT_INFO": "Informazioni di Contatto",
|
||||
"BILLING_ADDR": "Indirizzo di Fatturazione",
|
||||
"SHIPPING_ADDR": "Indirizzo di Spedizione",
|
||||
"FIRST_NAME": "Nome",
|
||||
"LAST_NAME": "Cognome",
|
||||
"EMAIL": "Email",
|
||||
"PHONE": "Telefono",
|
||||
"COMPANY_NAME": "Nome Azienda",
|
||||
"ADDRESS_1": "Indirizzo (Via e numero)",
|
||||
"ADDRESS_2": "Informazioni aggiuntive (opzionale)",
|
||||
"ZIP": "CAP",
|
||||
"CITY": "Città",
|
||||
"COUNTRY": "Paese",
|
||||
"SHIPPING_SAME": "L'indirizzo di spedizione è lo stesso di quello di fatturazione",
|
||||
"PLACE_ORDER": "Invia Ordine",
|
||||
"PROCESSING": "Elaborazione...",
|
||||
"SUMMARY_TITLE": "Riepilogo Ordine",
|
||||
"SUBTOTAL": "Subtotale",
|
||||
"SETUP_FEE": "Costo di Avvio",
|
||||
"TOTAL": "Totale",
|
||||
"QTY": "Qtà"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user