|
|
@@ -1,20 +1,70 @@
|
|
|
<template>
|
|
|
- <div ref="container" class="w-full h-full cursor-move" title="Drag to rotate" />
|
|
|
+ <div ref="container" class="w-full h-full cursor-move rounded-xl overflow-hidden transition-colors duration-500" :style="{ backgroundColor: backgroundColor }" title="Drag to rotate" />
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, onMounted, onUnmounted } from "vue";
|
|
|
+import { ref, onMounted, onUnmounted, watch, computed } from "vue";
|
|
|
import * as THREE from "three";
|
|
|
import { STLLoader } from "three/addons/loaders/STLLoader.js";
|
|
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
|
|
|
|
interface Props {
|
|
|
file: File;
|
|
|
+ color?: string; // Color name or hex
|
|
|
}
|
|
|
|
|
|
const props = defineProps<Props>();
|
|
|
const container = ref<HTMLElement | null>(null);
|
|
|
let animationId = 0;
|
|
|
+let renderer: THREE.WebGLRenderer | null = null;
|
|
|
+let mesh: THREE.Mesh | null = null;
|
|
|
+
|
|
|
+// Standard color map for common names
|
|
|
+const COLOR_MAP: Record<string, string> = {
|
|
|
+ "white": "#ffffff",
|
|
|
+ "black": "#1a1a1a",
|
|
|
+ "grey": "#808080",
|
|
|
+ "gray": "#808080",
|
|
|
+ "red": "#ef4444",
|
|
|
+ "blue": "#3b82f6",
|
|
|
+ "green": "#22c55e",
|
|
|
+ "yellow": "#eab308",
|
|
|
+ "orange": "#f97316",
|
|
|
+ "purple": "#a855f7",
|
|
|
+ "pink": "#ec4899",
|
|
|
+ "silver": "#c0c0c0",
|
|
|
+ "gold": "#ffd700",
|
|
|
+ "transparent": "#f3f4f6", // fallback for visual
|
|
|
+ "natural": "#f5f5dc",
|
|
|
+};
|
|
|
+
|
|
|
+const getHexColor = (colorName?: string) => {
|
|
|
+ if (!colorName) return "#808080";
|
|
|
+ if (colorName.startsWith("#")) return colorName;
|
|
|
+ return COLOR_MAP[colorName.toLowerCase()] || "#808080";
|
|
|
+};
|
|
|
+
|
|
|
+const isLight = (hex: string) => {
|
|
|
+ const color = hex.startsWith("#") ? hex : getHexColor(hex);
|
|
|
+ const rgb = parseInt(color.slice(1), 16);
|
|
|
+ const r = (rgb >> 16) & 0xff;
|
|
|
+ const g = (rgb >> 8) & 0xff;
|
|
|
+ const b = (rgb >> 0) & 0xff;
|
|
|
+ const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
|
+ return luma > 150; // Threshold for "light"
|
|
|
+};
|
|
|
+
|
|
|
+const backgroundColor = computed(() => {
|
|
|
+ if (!props.color) return "transparent";
|
|
|
+ return isLight(props.color) ? "#0f172a" : "#f8fafc"; // Slate-900 or Slate-50
|
|
|
+});
|
|
|
+
|
|
|
+watch(() => props.color, (newColor) => {
|
|
|
+ if (mesh && mesh.material instanceof THREE.MeshPhongMaterial) {
|
|
|
+ const hex = getHexColor(newColor);
|
|
|
+ mesh.material.color.set(hex);
|
|
|
+ }
|
|
|
+});
|
|
|
|
|
|
onMounted(() => {
|
|
|
if (!container.value) return;
|
|
|
@@ -27,9 +77,9 @@ onMounted(() => {
|
|
|
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
|
|
|
camera.position.set(0, 0, 150);
|
|
|
|
|
|
- const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
|
+ renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
|
renderer.setSize(width, height);
|
|
|
- renderer.setClearColor(0x000000, 0); // transparent
|
|
|
+ renderer.setClearColor(0x000000, 0); // Always transparent, handled by CSS
|
|
|
container.value.appendChild(renderer.domElement);
|
|
|
|
|
|
const controls = new OrbitControls(camera, renderer.domElement);
|
|
|
@@ -38,7 +88,7 @@ onMounted(() => {
|
|
|
controls.autoRotateSpeed = 3.0;
|
|
|
|
|
|
// Add lights
|
|
|
- const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1);
|
|
|
+ const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.2);
|
|
|
hemiLight.position.set(0, 200, 0);
|
|
|
scene.add(hemiLight);
|
|
|
|
|
|
@@ -53,13 +103,14 @@ onMounted(() => {
|
|
|
geometry.center();
|
|
|
geometry.computeVertexNormals();
|
|
|
|
|
|
+ const hex = getHexColor(props.color);
|
|
|
const material = new THREE.MeshPhongMaterial({
|
|
|
- color: 0x3b82f6,
|
|
|
- specular: 0x111111,
|
|
|
- shininess: 100
|
|
|
+ color: hex,
|
|
|
+ specular: 0x222222,
|
|
|
+ shininess: 60
|
|
|
});
|
|
|
|
|
|
- const mesh = new THREE.Mesh(geometry, material);
|
|
|
+ mesh = new THREE.Mesh(geometry, material);
|
|
|
|
|
|
geometry.computeBoundingSphere();
|
|
|
const radius = geometry.boundingSphere?.radius || 50;
|
|
|
@@ -73,12 +124,12 @@ onMounted(() => {
|
|
|
const animate = () => {
|
|
|
animationId = requestAnimationFrame(animate);
|
|
|
controls.update();
|
|
|
- renderer.render(scene, camera);
|
|
|
+ if (renderer) renderer.render(scene, camera);
|
|
|
};
|
|
|
animate();
|
|
|
|
|
|
const handleResize = () => {
|
|
|
- if (!container.value) return;
|
|
|
+ if (!container.value || !renderer) return;
|
|
|
const w = container.value.clientWidth;
|
|
|
const h = container.value.clientHeight;
|
|
|
camera.aspect = w / h;
|
|
|
@@ -91,10 +142,10 @@ onMounted(() => {
|
|
|
onUnmounted(() => {
|
|
|
cancelAnimationFrame(animationId);
|
|
|
window.removeEventListener("resize", handleResize);
|
|
|
- if (renderer.domElement.parentNode) {
|
|
|
+ if (renderer && renderer.domElement.parentNode) {
|
|
|
renderer.domElement.parentNode.removeChild(renderer.domElement);
|
|
|
}
|
|
|
- renderer.dispose();
|
|
|
+ renderer?.dispose();
|
|
|
});
|
|
|
});
|
|
|
</script>
|