feat: add 3D preview component and integrate it into the designer page
All checks were successful
Deploy Production / deploy (push) Successful in 2m2s
All checks were successful
Deploy Production / deploy (push) Successful in 2m2s
This commit is contained in:
174
app/components/designer/ThreeDPreview.vue
Normal file
174
app/components/designer/ThreeDPreview.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelPath: string;
|
||||
canvasTexture?: HTMLCanvasElement | null;
|
||||
}>();
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
let scene: THREE.Scene;
|
||||
let camera: THREE.PerspectiveCamera;
|
||||
let renderer: THREE.WebGLRenderer;
|
||||
let controls: OrbitControls;
|
||||
let model: THREE.Object3D | null = null;
|
||||
let jerseyMaterial: THREE.MeshStandardMaterial | null = null;
|
||||
let animationFrameId: number;
|
||||
|
||||
const setupScene = () => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
// Scene
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x1e293b);
|
||||
|
||||
// Camera
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
45,
|
||||
containerRef.value.clientWidth / containerRef.value.clientHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
camera.position.set(0, 1.5, 3);
|
||||
|
||||
// Renderer
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.shadowMap.enabled = true;
|
||||
containerRef.value.appendChild(renderer.domElement);
|
||||
|
||||
// Controls
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.autoRotate = true;
|
||||
controls.autoRotateSpeed = 2;
|
||||
|
||||
// Lights
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
directionalLight.position.set(5, 5, 5);
|
||||
directionalLight.castShadow = true;
|
||||
scene.add(directionalLight);
|
||||
|
||||
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
directionalLight2.position.set(-5, 5, -5);
|
||||
scene.add(directionalLight2);
|
||||
|
||||
// Load model
|
||||
loadModel();
|
||||
|
||||
// Animation loop
|
||||
const animate = () => {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
animate();
|
||||
|
||||
// Handle resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
camera.aspect = containerRef.value.clientWidth / containerRef.value.clientHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight);
|
||||
};
|
||||
|
||||
const loadModel = () => {
|
||||
const loader = new GLTFLoader();
|
||||
|
||||
loader.load(
|
||||
props.modelPath,
|
||||
(gltf) => {
|
||||
model = gltf.scene;
|
||||
|
||||
// Find the mesh that should receive the texture
|
||||
model.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const mesh = child as THREE.Mesh;
|
||||
|
||||
// Create a material for the jersey surface
|
||||
jerseyMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0xffffff,
|
||||
roughness: 0.7,
|
||||
metalness: 0.1,
|
||||
});
|
||||
|
||||
mesh.material = jerseyMaterial;
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
// Apply initial texture if provided
|
||||
if (props.canvasTexture) {
|
||||
updateTexture(props.canvasTexture);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scene.add(model);
|
||||
|
||||
// Center and scale model
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
model.position.sub(center);
|
||||
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const scale = 2 / maxDim;
|
||||
model.scale.multiplyScalar(scale);
|
||||
},
|
||||
undefined,
|
||||
(error) => {
|
||||
console.error('Error loading model:', error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const updateTexture = (canvas: HTMLCanvasElement) => {
|
||||
if (!jerseyMaterial) return;
|
||||
|
||||
// Create texture from canvas
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.needsUpdate = true;
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
// Apply texture to material
|
||||
jerseyMaterial.map = texture;
|
||||
jerseyMaterial.needsUpdate = true;
|
||||
};
|
||||
|
||||
// Watch for texture updates
|
||||
watch(() => props.canvasTexture, (newCanvas) => {
|
||||
if (newCanvas) {
|
||||
updateTexture(newCanvas);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setupScene();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
renderer?.dispose();
|
||||
controls?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="w-full h-full rounded-lg overflow-hidden"></div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user