StlViewer.vue 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. <template>
  2. <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" />
  3. </template>
  4. <script setup lang="ts">
  5. import { ref, onMounted, onUnmounted, watch, computed } from "vue";
  6. import * as THREE from "three";
  7. import { STLLoader } from "three/addons/loaders/STLLoader.js";
  8. import { OrbitControls } from "three/addons/controls/OrbitControls.js";
  9. import { getHexColor } from "@/lib/colors";
  10. interface Props {
  11. file: File;
  12. color?: string; // Color name or hex
  13. }
  14. const props = defineProps<Props>();
  15. const container = ref<HTMLElement | null>(null);
  16. let animationId = 0;
  17. let renderer: THREE.WebGLRenderer | null = null;
  18. let mesh: THREE.Mesh | null = null;
  19. const isLight = (hex: string) => {
  20. const color = hex.startsWith("#") ? hex : getHexColor(hex);
  21. const rgb = parseInt(color.slice(1, 7), 16);
  22. const r = (rgb >> 16) & 0xff;
  23. const g = (rgb >> 8) & 0xff;
  24. const b = (rgb >> 0) & 0xff;
  25. const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
  26. return luma > 150;
  27. };
  28. const backgroundColor = computed(() => {
  29. if (!props.color) return "transparent";
  30. return isLight(props.color) ? "#0f172a" : "#f8fafc";
  31. });
  32. watch(() => props.color, (newColor) => {
  33. if (mesh && mesh.material instanceof THREE.MeshPhongMaterial) {
  34. const hex = getHexColor(newColor);
  35. mesh.material.color.set(hex);
  36. }
  37. });
  38. onMounted(() => {
  39. if (!container.value) return;
  40. const width = container.value.clientWidth || 80;
  41. const height = container.value.clientHeight || 80;
  42. const scene = new THREE.Scene();
  43. const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
  44. camera.position.set(0, 0, 150);
  45. renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  46. renderer.setSize(width, height);
  47. renderer.setClearColor(0x000000, 0);
  48. container.value.appendChild(renderer.domElement);
  49. const controls = new OrbitControls(camera, renderer.domElement);
  50. controls.enableDamping = true;
  51. controls.autoRotate = true;
  52. controls.autoRotateSpeed = 3.0;
  53. const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.2);
  54. hemiLight.position.set(0, 200, 0);
  55. scene.add(hemiLight);
  56. const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
  57. dirLight.position.set(100, 200, 100);
  58. scene.add(dirLight);
  59. const loader = new STLLoader();
  60. const objectUrl = URL.createObjectURL(props.file);
  61. loader.load(objectUrl, (geometry) => {
  62. geometry.center();
  63. geometry.computeVertexNormals();
  64. const hex = getHexColor(props.color);
  65. const material = new THREE.MeshPhongMaterial({
  66. color: hex,
  67. specular: 0x222222,
  68. shininess: 60
  69. });
  70. mesh = new THREE.Mesh(geometry, material);
  71. geometry.computeBoundingSphere();
  72. const radius = geometry.boundingSphere?.radius || 50;
  73. const scale = 50 / radius;
  74. mesh.scale.set(scale, scale, scale);
  75. scene.add(mesh);
  76. URL.revokeObjectURL(objectUrl);
  77. });
  78. const animate = () => {
  79. animationId = requestAnimationFrame(animate);
  80. controls.update();
  81. if (renderer) renderer.render(scene, camera);
  82. };
  83. animate();
  84. const handleResize = () => {
  85. if (!container.value || !renderer) return;
  86. const w = container.value.clientWidth;
  87. const h = container.value.clientHeight;
  88. camera.aspect = w / h;
  89. camera.updateProjectionMatrix();
  90. renderer.setSize(w, h);
  91. };
  92. window.addEventListener("resize", handleResize);
  93. onUnmounted(() => {
  94. cancelAnimationFrame(animationId);
  95. window.removeEventListener("resize", handleResize);
  96. if (renderer && renderer.domElement.parentNode) {
  97. renderer.domElement.parentNode.removeChild(renderer.domElement);
  98. }
  99. renderer?.dispose();
  100. });
  101. });
  102. </script>