I have this code in TypeScript, WebGL and I use Reacts, but it does not work to add text on my box on the back, when I add it it colors the box and when I write it, it doesn’t do exactly what I want. a little more precision, I put the text on the box with a menu.
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
interface TextStyle {
color: string;
size: number;
isBold: boolean;
isItalic: boolean;
isUnderline: boolean;
font?: string;
}
interface Box3DProps {
color: string;
style?: TextStyle;
frontImage?: File;
frontText?: string;
frontTextStyle?: TextStyle;
backText?: string;
backTextStyle?: TextStyle;
imageScale?: number;
isPaused: boolean;
onCapture?: (rectoImage: string, versoImage: string) => void;
}
export const Box3D = ({ color, style, frontImage, frontText, frontTextStyle, backText, backTextStyle, imageScale, isPaused, onCapture }: Box3DProps) => {
const mountRef = useRef<HTMLDivElement>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
const modelRef = useRef<THREE.Group | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const controlsRef = useRef<OrbitControls | null>(null);
const isRotatingRef = useRef<boolean>(!isPaused);
const frontMeshRef = useRef<THREE.Mesh | null>(null);
const backMeshRef = useRef<THREE.Mesh | null>(null);
const sideMeshesRef = useRef<THREE.Mesh[]>([]);
const frontTextureRef = useRef<THREE.Texture | null>(null);
const backTextureRef = useRef<THREE.Texture | null>(null);
const modelLoadedRef = useRef<boolean>(false);
const [frontImageDimensions, setFrontImageDimensions] = useState<{width: number, height: number} | null>(null);
console.log("Props received:", { color, frontText, backText, frontImage, frontTextStyle, backTextStyle });
useEffect(() => {
isRotatingRef.current = !isPaused;
}, [isPaused]);
const createTextCanvas = (text?: string, style?: TextStyle): HTMLCanvasElement | null => {
if (!text) return null;
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 1024;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
let fontStyle = '';
if (style?.isBold) fontStyle += 'bold ';
if (style?.isItalic) fontStyle += 'italic ';
const fontSize = style?.size || 24;
const fontFamily = style?.font || 'Arial, sans-serif';
ctx.font = `${fontStyle}${fontSize}px ${fontFamily}`;
ctx.fillStyle = style?.color || '#000000';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const lines = text.split('\n');
const lineHeight = fontSize * 1.2;
const startY = canvas.height / 2 - (lines.length - 1) * lineHeight / 2;
lines.forEach((line, index) => {
ctx.fillText(line, canvas.width / 2, startY + index * lineHeight);
});
if (style?.isUnderline) {
lines.forEach((line, index) => {
const textMetrics = ctx.measureText(line);
const textWidth = textMetrics.width;
const underlineY = startY + index * lineHeight + (fontSize * 0.2);
ctx.beginPath();
ctx.moveTo((canvas.width - textWidth) / 2, underlineY);
ctx.lineTo((canvas.width + textWidth) / 2, underlineY);
ctx.strokeStyle = style.color;
ctx.lineWidth = Math.max(1, fontSize * 0.05);
ctx.stroke();
});
}
return canvas;
};
const updateModelColors = () => {
if (!modelRef.current || !color) return;
console.log("Updating model color to:", color);
const baseMaterial = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.2,
metalness: 0.3
});
if (frontMeshRef.current) {
const frontMaterial = baseMaterial.clone();
if (frontTextureRef.current) {
frontMaterial.map = frontTextureRef.current;
}
frontMeshRef.current.material = frontMaterial;
}
if (backMeshRef.current) {
const backMaterial = baseMaterial.clone();
backMaterial.side = THREE.DoubleSide;
if (backTextureRef.current) {
backMaterial.map = backTextureRef.current;
}
backMeshRef.current.material = backMaterial;
}
sideMeshesRef.current.forEach(mesh => {
mesh.material = baseMaterial.clone();
});
captureView();
};
const createImageBoundaryCanvas = (originalCanvas: HTMLCanvasElement, boundaryRatio = 0.8): HTMLCanvasElement => {
const canvas = document.createElement('canvas');
canvas.width = originalCanvas.width;
canvas.height = originalCanvas.height;
const ctx = canvas.getContext('2d');
if (!ctx) return originalCanvas;
ctx.drawImage(originalCanvas, 0, 0);
const boundaryWidth = canvas.width * boundaryRatio;
const boundaryHeight = canvas.height * boundaryRatio;
const x = (canvas.width - boundaryWidth) / 2;
const y = (canvas.height - boundaryHeight) / 2;
ctx.strokeStyle = 'rgba(100, 100, 100, 0.5)';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, boundaryWidth, boundaryHeight);
return canvas;
};
// Apply front image
const applyFrontImage = () => {
if (!frontImage || !frontMeshRef.current) return;
console.log("Applying front image...");
const fileReader = new FileReader();
fileReader.onload = (e) => {
if (!e.target?.result) return;
console.log("Front image loaded successfully");
// Charger l'image dans un élément HTML pour récupérer ses dimensions
const img = new Image();
img.onload = () => {
console.log("Image dimensions:", img.width, "x", img.height);
// Création d'un canvas pour garantir la bonne orientation
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Vérifier si l'image est en mode portrait
const isPortrait = img.height > img.width;
// Taille du canvas (ajusté selon l'orientation)
if (isPortrait) {
canvas.width = 1024;
canvas.height = 1024;
} else {
canvas.width = 1024;
canvas.height = 1024;
}
// Rotation de l'image si nécessaire
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save(); // Sauvegarde le contexte pour éviter d'affecter d'autres dessins
if (isPortrait) {
// Si l'image est en portrait, on la pivote pour qu'elle reste verticale
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(Math.PI / -2); // Rotation de 90° dans le sens horaire
ctx.drawImage(img, -canvas.height / 2, -canvas.width / 2, canvas.height, canvas.width);
} else {
// Si l'image est en paysage, on la dessine normalement
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
ctx.restore(); // Restaure le contexte
// Convertir le canvas en texture
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
texture.colorSpace = THREE.SRGBColorSpace;
// Appliquer la texture à la face
if (frontMeshRef.current) {
frontTextureRef.current = texture;
const material = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.2,
metalness: 0.3,
map: texture
});
frontMeshRef.current.material = material;
captureView();
}
};
img.src = e.target.result as string;
};
fileReader.readAsDataURL(frontImage);
};
const applyFrontText = () => {
if (!frontText || !frontMeshRef.current) return;
console.log("Applying front text:", frontText);
const canvas = createTextCanvas(frontText, frontTextStyle);
if (canvas) {
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
const material = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.2,
metalness: 0.3,
map: texture
});
frontMeshRef.current.material = material;
frontTextureRef.current = texture;
captureView();
}
};
const applyBackText = () => {
if (!backText || !backMeshRef.current) return;
console.log("Applying back text:", backText);
const canvas = createTextCanvas(backText, backTextStyle);
if (canvas) {
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
texture.repeat.set(-1, 1);
texture.offset.set(1, 0);
backTextureRef.current = texture;
const material = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.2,
metalness: 0.3,
map: texture,
side: THREE.DoubleSide
});
backMeshRef.current.material = material;
captureView();
}
};
useEffect(() => {
const mount = mountRef.current;
if (!mount) return;
console.log("Initializing 3D scene...");
const scene = new THREE.Scene();
sceneRef.current = scene;
scene.background = new THREE.Color(0x87a2a3);
const renderer = new THREE.WebGLRenderer({
antialias: true,
preserveDrawingBuffer: true
});
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
renderer.setSize(846, 520);
renderer.setClearColor(0x87a2a3, 1.0);
renderer.shadowMap.enabled = true;
rendererRef.current = renderer;
mount.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(65, 846 / 520, 1, 1000);
camera.position.z = 8;
cameraRef.current = camera;
const controls = new OrbitControls(camera, renderer.domElement);
controlsRef.current = controls;
controls.enableDamping = true;
controls.dampingFactor = 0.05;
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(2, 3, 5);
scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 1);
directionalLight2.position.set(1, 2, 4);
scene.add(directionalLight2);
const directionalLight3 = new THREE.DirectionalLight(0xffffff, 1);
directionalLight3.position.set(0, -10, 3);
scene.add(directionalLight3);
const backLight = new THREE.DirectionalLight(0xffffff, 1);
backLight.position.set(-2, 0, -5);
scene.add(backLight);
const pointLight = new THREE.PointLight(0xffffff, 15, 4);
pointLight.position.set(1, 2, 1);
scene.add(pointLight);
pointLight.distance = 30;
const pointLight2 = new THREE.PointLight(0xffffff, 15, 4);
pointLight2.position.set(-1, -2, -1);
scene.add(pointLight2);
pointLight2.distance = 30;
const backPointLight = new THREE.PointLight(0xffffff, 15, 4);
backPointLight.position.set(0, 0, -5);
scene.add(backPointLight);
backPointLight.distance = 30;
const loader = new GLTFLoader();
console.log("Loading 3D model...");
loader.load('/2FINAL.glb',
(gltf) => {
console.log("Model loaded successfully:", gltf);
const model = gltf.scene;
model.scale.set(0.5, 0.5, 0.5);
model.rotation.x = 0 * Math.PI / 180;
model.rotation.y = 90 * Math.PI / 180;
model.rotation.z = 0 * Math.PI / 180;
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
console.log("Mesh found:", child.name, "position:", child.position);
if (child.position.z > 0 ||
child.name.toLowerCase().includes("front") ||
child.name.toLowerCase().includes("recto")) {
child.name = "front";
frontMeshRef.current = child;
console.log("Identified front mesh:", child.name);
}
else if (child.position.z < 0 ||
child.name.toLowerCase().includes("back") ||
child.name.toLowerCase().includes("verso")) {
child.name = "back";
backMeshRef.current = child;
console.log("Identified back mesh:", child.name);
}
else {
child.name = "side";
sideMeshesRef.current.push(child);
}
}
});
if (!frontMeshRef.current || !backMeshRef.current) {
console.log("Identifying faces by mesh order...");
const meshes: THREE.Mesh[] = [];
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
meshes.push(child);
}
});
if (meshes.length >= 2) {
if (!frontMeshRef.current) {
frontMeshRef.current = meshes[0];
frontMeshRef.current.name = "front";
}
if (!backMeshRef.current) {
backMeshRef.current = meshes[1];
backMeshRef.current.name = "back";
}
for (let i = 2; i < meshes.length; i++) {
if (!sideMeshesRef.current.includes(meshes[i])) {
sideMeshesRef.current.push(meshes[i]);
}
}
console.log("Identified by mesh order: Front and Back meshes set");
}
}
const group = new THREE.Group();
group.add(model);
group.position.set(0, 0, 3);
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
model.position.sub(center);
scene.add(group);
modelRef.current = group;
modelLoadedRef.current = true;
console.log("Model added to scene");
updateModelColors();
if (frontText) applyFrontText();
if (backText) applyBackText();
if (frontImage) applyFrontImage();
const animate = () => {
requestAnimationFrame(animate);
if (isRotatingRef.current && modelRef.current) {
modelRef.current.rotation.y += 0.006;
}
controls.update();
renderer.render(scene, camera);
};
animate();
captureView();
},
(progress) => {
console.log("Loading progress:", (progress.loaded / progress.total) * 100, "%");
},
(error) => {
console.error("Error loading model:", error);
}
);
const handleMouseDown = (e: MouseEvent) => {
if (e.target === renderer.domElement) {
isRotatingRef.current = false;
}
};
const handleMouseUp = (e: MouseEvent) => {
if (e.target === renderer.domElement) {
isRotatingRef.current = !isPaused;
}
};
mount.addEventListener("mousedown", handleMouseDown);
mount.addEventListener("mouseup", handleMouseUp);
const handleResize = () => {
if (!camera || !renderer || !mount) return;
camera.aspect = 846 / 520;
camera.updateProjectionMatrix();
renderer.setSize(846, 520);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
mount.removeEventListener("mousedown", handleMouseDown);
mount.removeEventListener("mouseup", handleMouseUp);
if (mount.contains(renderer.domElement)) {
mount.removeChild(renderer.domElement);
}
scene.clear();
};
}, []);
useEffect(() => {
if (modelLoadedRef.current) {
updateModelColors();
}
}, [color]);
useEffect(() => {
if (modelLoadedRef.current) {
applyFrontImage();
}
}, [frontImage, imageScale]);
useEffect(() => {
if (modelLoadedRef.current) {
applyFrontText();
}
}, [frontText, frontTextStyle]);
useEffect(() => {
if (modelLoadedRef.current) {
applyBackText();
}
}, [backText, backTextStyle]);
const captureView = () => {
if (!rendererRef.current || !sceneRef.current || !cameraRef.current || !modelRef.current) return;
const currentRotation = modelRef.current.rotation.y;
modelRef.current.rotation.y = -90 * Math.PI/180;
rendererRef.current.render(sceneRef.current, cameraRef.current);
const rectoImage = rendererRef.current.domElement.toDataURL('image/png');
modelRef.current.rotation.y = 90 * Math.PI/180;
rendererRef.current.render(sceneRef.current, cameraRef.current);
const versoImage = rendererRef.current.domElement.toDataURL('image/png');
modelRef.current.rotation.y = currentRotation;
rendererRef.current.render(sceneRef.current, cameraRef.current);
if (onCapture) {
onCapture(rectoImage, versoImage);
}
};
return (
<div className="flex justify-center items-center w-full">
<div
ref={mountRef}
className="h-[520px] w-[846px] rounded-lg overflow-hidden shadow-lg"
/>
</div>
);
};