diff --git a/src/core/rendering/ActorRenderer.js b/src/core/rendering/ActorRenderer.js index 71046ca..a7ec5be 100644 --- a/src/core/rendering/ActorRenderer.js +++ b/src/core/rendering/ActorRenderer.js @@ -109,21 +109,9 @@ export class ActorRenderer extends AnimatedRenderer { const actorInfo = ActorInfoInit[actorIndex]; const charState = characters[actorIndex]; - // Build texture lookup - for (const tex of globalTextures) { - if (tex.name) { - this.textures.set(tex.name.toLowerCase(), this.createTexture(tex)); - } - } - - // Merge vehicle textures (if present) - if (vehicleInfo && vehicleTextures) { - for (const tex of vehicleTextures) { - if (tex.name && !this.textures.has(tex.name.toLowerCase())) { - this.textures.set(tex.name.toLowerCase(), this.createTexture(tex)); - } - } - } + // Build texture lookup (vehicle textures don't overwrite global ones) + this.loadTextures(globalTextures); + if (vehicleInfo) this.loadTextures(vehicleTextures, false); this.modelGroup = new THREE.Group(); this.partGroups = []; @@ -321,24 +309,7 @@ export class ActorRenderer extends AnimatedRenderer { for (const mesh of lod.meshes) { const geometry = this.createGeometry(mesh, lod); if (!geometry) continue; - - let material; - const meshTexName = mesh.properties?.textureName?.toLowerCase(); - if (meshTexName && this.textures.has(meshTexName)) { - material = new THREE.MeshLambertMaterial({ - map: this.textures.get(meshTexName), - side: THREE.DoubleSide, - color: 0xffffff - }); - } else { - const meshColor = mesh.properties?.color || { r: 128, g: 128, b: 128 }; - material = new THREE.MeshLambertMaterial({ - color: new THREE.Color(meshColor.r / 255, meshColor.g / 255, meshColor.b / 255), - side: THREE.DoubleSide - }); - } - - this.vehicleGroup.add(new THREE.Mesh(geometry, material)); + this.vehicleGroup.add(new THREE.Mesh(geometry, this.createMeshMaterial(mesh))); } } diff --git a/src/core/rendering/AnimatedRenderer.js b/src/core/rendering/AnimatedRenderer.js index a26e3d1..345a8aa 100644 --- a/src/core/rendering/AnimatedRenderer.js +++ b/src/core/rendering/AnimatedRenderer.js @@ -16,6 +16,7 @@ export class AnimatedRenderer extends BaseRenderer { this.currentAction = null; this.animationCache = new Map(); this.raycaster = new THREE.Raycaster(); + this._queuedClickAnim = null; } // ─── Animation Utilities ───────────────────────────────────────── @@ -138,6 +139,185 @@ export class AnimatedRenderer extends BaseRenderer { return { before, after: keys[idx] || null }; } + // ─── Click Animation ───────────────────────────────────────────── + + /** + * Queue a click animation by name. Subclasses may override to + * accept domain-specific arguments and construct the name. + * @param {string} animName - Animation asset name + */ + queueClickAnimation(animName) { + this._queuedClickAnim = animName; + } + + /** + * Play the queued click animation (one-shot), then resume auto-rotate. + */ + async playQueuedAnimation() { + if (!this._queuedClickAnim || !this.modelGroup) return; + + const animName = this._queuedClickAnim; + this._queuedClickAnim = null; + + try { + const animData = await this.fetchAnimationByName(animName); + if (!animData || !this.modelGroup) return; + + const tracks = this.buildRotationTracks(animData); + if (tracks.length === 0) return; + + this.stopAnimation(); + + const clip = new THREE.AnimationClip('clickAnim', -1, tracks); + this.mixer = new THREE.AnimationMixer(this.modelGroup); + const action = this.mixer.clipAction(clip); + action.setLoop(THREE.LoopOnce); + action.clampWhenFinished = false; + this.currentAction = action; + action.play(); + + this.mixer.addEventListener('finished', () => { + this.stopAnimation(); + this.controls.autoRotate = true; + }); + } catch (e) { + // Animation unavailable — ignore + } + } + + // ─── Raycast Hit Testing ───────────────────────────────────────── + + /** + * Check if any mesh in the model was clicked. + * @returns {boolean} True if any mesh was hit + */ + getClickedMesh(mouseEvent) { + if (!this.modelGroup) return false; + + const rect = this.canvas.getBoundingClientRect(); + const mouse = new THREE.Vector2( + ((mouseEvent.clientX - rect.left) / rect.width) * 2 - 1, + -((mouseEvent.clientY - rect.top) / rect.height) * 2 + 1 + ); + + this.raycaster.setFromCamera(mouse, this.camera); + + const meshes = []; + this.modelGroup.traverse((child) => { + if (child instanceof THREE.Mesh) meshes.push(child); + }); + + return this.raycaster.intersectObjects(meshes).length > 0; + } + + // ─── Simple Animation Tree Utilities ───────────────────────────── + + /** + * Build rotation-only keyframe tracks by finding the deepest animated + * node and evaluating the composed transform chain at each keyframe time. + * Used by plant and building animations (single-group models). + */ + buildRotationTracks(animData) { + const duration = animData.duration; + const timesSet = new Set([0]); + this.collectKeyframeTimes(animData.rootNode, timesSet); + const times = [...timesSet].filter(t => t <= duration).sort((a, b) => a - b); + + const targetNode = this.findAnimatedNode(animData.rootNode); + if (!targetNode) return []; + + const quatValues = []; + const timesSec = []; + + for (const time of times) { + const mat = this.evaluateNodeChain(animData.rootNode, targetNode, time); + const position = new THREE.Vector3(); + const quaternion = new THREE.Quaternion(); + const scale = new THREE.Vector3(); + mat.decompose(position, quaternion, scale); + + timesSec.push(time / 1000); + quatValues.push(quaternion.x, quaternion.y, quaternion.z, quaternion.w); + } + + return [ + new THREE.QuaternionKeyframeTrack('.quaternion', timesSec, quatValues) + ]; + } + + /** + * Find the deepest node in the animation tree that has keyframe data. + */ + findAnimatedNode(node) { + for (const child of node.children) { + const found = this.findAnimatedNode(child); + if (found) return found; + } + const d = node.data; + if (d.translationKeys.length > 0 || d.rotationKeys.length > 0 || d.scaleKeys.length > 0) { + return node; + } + return null; + } + + /** + * Evaluate the composed transform matrix from root down to targetNode. + */ + evaluateNodeChain(node, targetNode, time) { + const path = []; + if (!this.findNodePath(node, targetNode, path)) { + return new THREE.Matrix4(); + } + + let mat = new THREE.Matrix4(); + for (const n of path) { + const local = this.evaluateLocalTransform(n.data, time); + mat.multiply(local); + } + return mat; + } + + /** + * Build the path from current node to target via depth-first search. + */ + findNodePath(current, target, path) { + path.push(current); + if (current === target) return true; + for (const child of current.children) { + if (this.findNodePath(child, target, path)) return true; + } + path.pop(); + return false; + } + + /** + * Evaluate the local transform matrix for an animation node at a given time. + */ + evaluateLocalTransform(data, time) { + let mat = new THREE.Matrix4(); + + if (data.scaleKeys.length > 0) { + const scale = this.interpolateVertex(data.scaleKeys, time, false); + if (scale) mat.scale(scale); + } + + if (data.rotationKeys.length > 0) { + const rotMat = this.evaluateRotation(data.rotationKeys, time); + mat = rotMat.multiply(mat); + } + + if (data.translationKeys.length > 0) { + const vertex = this.interpolateVertex(data.translationKeys, time, true); + if (vertex) { + mat.elements[12] += vertex.x; + mat.elements[13] += vertex.y; + mat.elements[14] += vertex.z; + } + } + + return mat; + } + // ─── Scene Management ──────────────────────────────────────────── clearModel() { diff --git a/src/core/rendering/BaseRenderer.js b/src/core/rendering/BaseRenderer.js index a3398f9..c16823c 100644 --- a/src/core/rendering/BaseRenderer.js +++ b/src/core/rendering/BaseRenderer.js @@ -109,6 +109,48 @@ export class BaseRenderer { return texture; } + /** + * Build the texture lookup map from an array of texture data objects. + * @param {Array} textures - Texture data with name, width, height, palette, pixels + * @param {boolean} overwrite - If false, skip textures already in the map + */ + loadTextures(textures, overwrite = true) { + if (!textures) return; + for (const tex of textures) { + if (!tex.name) continue; + const key = tex.name.toLowerCase(); + if (overwrite || !this.textures.has(key)) { + this.textures.set(key, this.createTexture(tex)); + } + } + } + + /** + * Create a material for a mesh using its texture or color properties. + * @param {object} mesh - Mesh with properties (textureName, color) + * @param {THREE.Color} [fallbackColor] - Color when mesh has no texture or color + */ + createMeshMaterial(mesh, fallbackColor = null) { + const meshTexName = mesh.properties?.textureName?.toLowerCase(); + if (meshTexName && this.textures.has(meshTexName)) { + return new THREE.MeshLambertMaterial({ + map: this.textures.get(meshTexName), + side: THREE.DoubleSide, + color: 0xffffff + }); + } + + const meshColor = mesh.properties?.color; + const color = meshColor + ? new THREE.Color(meshColor.r / 255, meshColor.g / 255, meshColor.b / 255) + : (fallbackColor || new THREE.Color(0.5, 0.5, 0.5)); + + return new THREE.MeshLambertMaterial({ + color, + side: THREE.DoubleSide + }); + } + /** * Create a single geometry from mesh data */ diff --git a/src/core/rendering/BuildingRenderer.js b/src/core/rendering/BuildingRenderer.js index 13d2051..7ce0e20 100644 --- a/src/core/rendering/BuildingRenderer.js +++ b/src/core/rendering/BuildingRenderer.js @@ -9,7 +9,6 @@ import { AnimatedRenderer } from './AnimatedRenderer.js'; export class BuildingRenderer extends AnimatedRenderer { constructor(canvas) { super(canvas); - this._queuedClickAnim = null; this.camera.position.set(2.5, 2.0, 4.0); this.camera.lookAt(0, -0.3, 0); @@ -27,17 +26,13 @@ export class BuildingRenderer extends AnimatedRenderer { if (!rois || rois.length === 0) return; - // Build texture lookup - if (textures) { - for (const tex of textures) { - if (tex.name) { - this.textures.set(tex.name.toLowerCase(), this.createTexture(tex)); - } - } - } + this.loadTextures(textures); this.modelGroup = new THREE.Group(); + const colorEntry = LegoColors['lego white'] || { r: 255, g: 255, b: 255 }; + const fallbackColor = new THREE.Color(colorEntry.r / 255, colorEntry.g / 255, colorEntry.b / 255); + for (const roi of rois) { const lods = roi.lods || []; if (lods.length === 0) continue; @@ -46,32 +41,7 @@ export class BuildingRenderer extends AnimatedRenderer { for (const mesh of lod.meshes) { const geometry = this.createGeometry(mesh, lod); if (!geometry) continue; - - let material; - const meshTexName = mesh.properties?.textureName?.toLowerCase(); - if (meshTexName && this.textures.has(meshTexName)) { - material = new THREE.MeshLambertMaterial({ - map: this.textures.get(meshTexName), - side: THREE.DoubleSide, - color: 0xffffff - }); - } else { - const meshColor = mesh.properties?.color; - if (meshColor) { - material = new THREE.MeshLambertMaterial({ - color: new THREE.Color(meshColor.r / 255, meshColor.g / 255, meshColor.b / 255), - side: THREE.DoubleSide - }); - } else { - const colorEntry = LegoColors['lego white'] || { r: 255, g: 255, b: 255 }; - material = new THREE.MeshLambertMaterial({ - color: new THREE.Color(colorEntry.r / 255, colorEntry.g / 255, colorEntry.b / 255), - side: THREE.DoubleSide - }); - } - } - - this.modelGroup.add(new THREE.Mesh(geometry, material)); + this.modelGroup.add(new THREE.Mesh(geometry, this.createMeshMaterial(mesh, fallbackColor))); } } @@ -80,166 +50,7 @@ export class BuildingRenderer extends AnimatedRenderer { this.renderer.render(this.scene, this.camera); } - /** - * Check if the building mesh was clicked. - * @returns {boolean} True if any mesh was hit - */ - getClickedMesh(mouseEvent) { - if (!this.modelGroup) return false; - - const rect = this.canvas.getBoundingClientRect(); - const mouse = new THREE.Vector2( - ((mouseEvent.clientX - rect.left) / rect.width) * 2 - 1, - -((mouseEvent.clientY - rect.top) / rect.height) * 2 + 1 - ); - - this.raycaster.setFromCamera(mouse, this.camera); - - const meshes = []; - this.modelGroup.traverse((child) => { - if (child instanceof THREE.Mesh) meshes.push(child); - }); - - return this.raycaster.intersectObjects(meshes).length > 0; - } - - // ─── Animation System ──────────────────────────────────────────── - - /** - * Queue a click animation to play. - * @param {number} buildingIndex - Building index (9-14) - * @param {number} move - The building's move value - */ queueClickAnimation(buildingIndex, move) { - this._queuedClickAnim = { buildingIndex, move }; - } - - /** - * Play a queued click animation if available. - */ - async playQueuedAnimation() { - if (!this._queuedClickAnim || !this.modelGroup) return; - - const { buildingIndex, move } = this._queuedClickAnim; - this._queuedClickAnim = null; - - const animName = `BuildingAnim${buildingIndex}_${move}`; - - try { - const animData = await this.fetchAnimationByName(animName); - if (!animData || !this.modelGroup) return; - - const tracks = this.buildBuildingTracks(animData); - if (tracks.length === 0) return; - - this.stopAnimation(); - - const clip = new THREE.AnimationClip('buildingClick', -1, tracks); - this.mixer = new THREE.AnimationMixer(this.modelGroup); - const action = this.mixer.clipAction(clip); - action.setLoop(THREE.LoopOnce); - action.clampWhenFinished = false; - this.currentAction = action; - action.play(); - - this.mixer.addEventListener('finished', () => { - this.stopAnimation(); - this.controls.autoRotate = true; - }); - } catch (e) { - // Animation unavailable — ignore - } - } - - /** - * Build animation tracks for a building. Same approach as PlantRenderer. - */ - buildBuildingTracks(animData) { - const duration = animData.duration; - const timesSet = new Set([0]); - this.collectKeyframeTimes(animData.rootNode, timesSet); - const times = [...timesSet].filter(t => t <= duration).sort((a, b) => a - b); - - const buildingNode = this.findAnimatedNode(animData.rootNode); - if (!buildingNode) return []; - - const quatValues = []; - const timesSec = []; - - for (const time of times) { - const mat = this.evaluateNodeChain(animData.rootNode, buildingNode, time); - const position = new THREE.Vector3(); - const quaternion = new THREE.Quaternion(); - const scale = new THREE.Vector3(); - mat.decompose(position, quaternion, scale); - - timesSec.push(time / 1000); - quatValues.push(quaternion.x, quaternion.y, quaternion.z, quaternion.w); - } - - return [ - new THREE.QuaternionKeyframeTrack('.quaternion', timesSec, quatValues) - ]; - } - - findAnimatedNode(node) { - for (const child of node.children) { - const found = this.findAnimatedNode(child); - if (found) return found; - } - const d = node.data; - if (d.translationKeys.length > 0 || d.rotationKeys.length > 0 || d.scaleKeys.length > 0) { - return node; - } - return null; - } - - evaluateNodeChain(node, targetNode, time) { - const path = []; - if (!this.findPath(node, targetNode, path)) { - return new THREE.Matrix4(); - } - - let mat = new THREE.Matrix4(); - for (const n of path) { - const local = this.evaluateLocalTransform(n.data, time); - mat.multiply(local); - } - return mat; - } - - findPath(current, target, path) { - path.push(current); - if (current === target) return true; - for (const child of current.children) { - if (this.findPath(child, target, path)) return true; - } - path.pop(); - return false; - } - - evaluateLocalTransform(data, time) { - let mat = new THREE.Matrix4(); - - if (data.scaleKeys.length > 0) { - const scale = this.interpolateVertex(data.scaleKeys, time, false); - if (scale) mat.scale(scale); - } - - if (data.rotationKeys.length > 0) { - const rotMat = this.evaluateRotation(data.rotationKeys, time); - mat = rotMat.multiply(mat); - } - - if (data.translationKeys.length > 0) { - const vertex = this.interpolateVertex(data.translationKeys, time, true); - if (vertex) { - mat.elements[12] += vertex.x; - mat.elements[13] += vertex.y; - mat.elements[14] += vertex.z; - } - } - - return mat; + super.queueClickAnimation(`BuildingAnim${buildingIndex}_${move}`); } } diff --git a/src/core/rendering/PlantRenderer.js b/src/core/rendering/PlantRenderer.js index ff5e84a..7ca289b 100644 --- a/src/core/rendering/PlantRenderer.js +++ b/src/core/rendering/PlantRenderer.js @@ -19,7 +19,6 @@ const VARIANT_SCALE = [1.6, 1.8, 1.6, 2.0]; export class PlantRenderer extends AnimatedRenderer { constructor(canvas) { super(canvas); - this._queuedClickAnim = null; this.camera.position.set(1.5, 1.2, 2.5); this.camera.lookAt(0, 0.2, 0); @@ -40,56 +39,25 @@ export class PlantRenderer extends AnimatedRenderer { const lodName = PlantLodNames[variant]?.[color]; if (!lodName) return; - // Find the part data (case-insensitive) const partData = partsMap.get(lodName.toLowerCase()); if (!partData) return; - // Build texture lookup - if (textures) { - for (const tex of textures) { - if (tex.name) { - this.textures.set(tex.name.toLowerCase(), this.createTexture(tex)); - } - } - } + this.loadTextures(textures); this.modelGroup = new THREE.Group(); const lods = partData.lods || []; if (lods.length === 0) return; + const colorName = PLANT_COLOR_MAP[color] || 'lego green'; + const colorEntry = LegoColors[colorName] || LegoColors['lego green']; + const fallbackColor = new THREE.Color(colorEntry.r / 255, colorEntry.g / 255, colorEntry.b / 255); + const lod = lods[lods.length - 1]; // Highest quality for (const mesh of lod.meshes) { const geometry = this.createGeometry(mesh, lod); if (!geometry) continue; - - let material; - const meshTexName = mesh.properties?.textureName?.toLowerCase(); - if (meshTexName && this.textures.has(meshTexName)) { - material = new THREE.MeshLambertMaterial({ - map: this.textures.get(meshTexName), - side: THREE.DoubleSide, - color: 0xffffff - }); - } else { - const meshColor = mesh.properties?.color; - if (meshColor) { - material = new THREE.MeshLambertMaterial({ - color: new THREE.Color(meshColor.r / 255, meshColor.g / 255, meshColor.b / 255), - side: THREE.DoubleSide - }); - } else { - // Fallback to plant color - const colorName = PLANT_COLOR_MAP[color] || 'lego green'; - const colorEntry = LegoColors[colorName] || LegoColors['lego green']; - material = new THREE.MeshLambertMaterial({ - color: new THREE.Color(colorEntry.r / 255, colorEntry.g / 255, colorEntry.b / 255), - side: THREE.DoubleSide - }); - } - } - - this.modelGroup.add(new THREE.Mesh(geometry, material)); + this.modelGroup.add(new THREE.Mesh(geometry, this.createMeshMaterial(mesh, fallbackColor))); } this.centerAndScaleModel(VARIANT_SCALE[variant] ?? 2.0); @@ -97,182 +65,8 @@ export class PlantRenderer extends AnimatedRenderer { this.renderer.render(this.scene, this.camera); } - /** - * Check if the plant mesh was clicked. - * @returns {boolean} True if any mesh was hit - */ - getClickedMesh(mouseEvent) { - if (!this.modelGroup) return false; - - const rect = this.canvas.getBoundingClientRect(); - const mouse = new THREE.Vector2( - ((mouseEvent.clientX - rect.left) / rect.width) * 2 - 1, - -((mouseEvent.clientY - rect.top) / rect.height) * 2 + 1 - ); - - this.raycaster.setFromCamera(mouse, this.camera); - - const meshes = []; - this.modelGroup.traverse((child) => { - if (child instanceof THREE.Mesh) meshes.push(child); - }); - - return this.raycaster.intersectObjects(meshes).length > 0; - } - - // ─── Animation System ──────────────────────────────────────────── - - /** - * Queue a click animation to play. - * @param {number} variant - Plant variant (0-3) - * @param {number} move - The plant's move value - */ queueClickAnimation(variant, move) { - this._queuedClickAnim = { variant, move }; - } - - /** - * Play a queued click animation if available. - * Called after model reload or directly for non-visual changes. - */ - async playQueuedAnimation() { - if (!this._queuedClickAnim || !this.modelGroup) return; - - const { variant, move } = this._queuedClickAnim; - this._queuedClickAnim = null; - const suffix = VARIANT_ANIM_SUFFIX[variant]; - const animName = `PlantAnim${suffix}${move}`; - - try { - const animData = await this.fetchAnimationByName(animName); - if (!animData || !this.modelGroup) return; - - const tracks = this.buildPlantTracks(animData); - if (tracks.length === 0) return; - - this.stopAnimation(); - - const clip = new THREE.AnimationClip('plantClick', -1, tracks); - this.mixer = new THREE.AnimationMixer(this.modelGroup); - const action = this.mixer.clipAction(clip); - action.setLoop(THREE.LoopOnce); - action.clampWhenFinished = false; - this.currentAction = action; - action.play(); - - this.mixer.addEventListener('finished', () => { - this.stopAnimation(); - this.controls.autoRotate = true; - }); - } catch (e) { - // Animation unavailable — ignore - } - } - - /** - * Build animation tracks for a plant. Maps animation tree nodes - * to the model group (the entire plant is a single group). - */ - buildPlantTracks(animData) { - const duration = animData.duration; - const timesSet = new Set([0]); - this.collectKeyframeTimes(animData.rootNode, timesSet); - const times = [...timesSet].filter(t => t <= duration).sort((a, b) => a - b); - - // Find the deepest non-root node that has animation data — - // map it to our modelGroup - const plantNode = this.findPlantNode(animData.rootNode); - if (!plantNode) return []; - - const quatValues = []; - const timesSec = []; - - for (const time of times) { - const mat = this.evaluateNodeChain(animData.rootNode, plantNode, time); - const position = new THREE.Vector3(); - const quaternion = new THREE.Quaternion(); - const scale = new THREE.Vector3(); - mat.decompose(position, quaternion, scale); - - timesSec.push(time / 1000); - quatValues.push(quaternion.x, quaternion.y, quaternion.z, quaternion.w); - } - - // Only emit rotation tracks — position tracks would override the - // centering applied by centerAndScaleModel() since the animation - // uses the game's world-space coordinates. - return [ - new THREE.QuaternionKeyframeTrack('.quaternion', timesSec, quatValues) - ]; - } - - /** - * Find the first leaf/deepest node with animation data in the tree. - */ - findPlantNode(node) { - // Depth-first: prefer children - for (const child of node.children) { - const found = this.findPlantNode(child); - if (found) return found; - } - // If this node has actual keyframe data, use it - const d = node.data; - if (d.translationKeys.length > 0 || d.rotationKeys.length > 0 || d.scaleKeys.length > 0) { - return node; - } - return null; - } - - /** - * Evaluate the composed matrix from root down to targetNode at a given time. - */ - evaluateNodeChain(node, targetNode, time) { - const path = []; - if (!this.findPath(node, targetNode, path)) { - return new THREE.Matrix4(); - } - - let mat = new THREE.Matrix4(); - for (const n of path) { - const local = this.evaluateLocalTransform(n.data, time); - mat.multiply(local); - } - return mat; - } - - findPath(current, target, path) { - path.push(current); - if (current === target) return true; - for (const child of current.children) { - if (this.findPath(child, target, path)) return true; - } - path.pop(); - return false; - } - - evaluateLocalTransform(data, time) { - let mat = new THREE.Matrix4(); - - if (data.scaleKeys.length > 0) { - const scale = this.interpolateVertex(data.scaleKeys, time, false); - if (scale) mat.scale(scale); - } - - if (data.rotationKeys.length > 0) { - const rotMat = this.evaluateRotation(data.rotationKeys, time); - mat = rotMat.multiply(mat); - } - - if (data.translationKeys.length > 0) { - const vertex = this.interpolateVertex(data.translationKeys, time, true); - if (vertex) { - mat.elements[12] += vertex.x; - mat.elements[13] += vertex.y; - mat.elements[14] += vertex.z; - } - } - - return mat; + super.queueClickAnimation(`PlantAnim${suffix}${move}`); } } diff --git a/src/core/rendering/VehiclePartRenderer.js b/src/core/rendering/VehiclePartRenderer.js index 39283d9..31fbb9b 100644 --- a/src/core/rendering/VehiclePartRenderer.js +++ b/src/core/rendering/VehiclePartRenderer.js @@ -42,12 +42,7 @@ export class VehiclePartRenderer extends BaseRenderer { this.colorableMeshes = []; this.partsMap = partsMap; - // Build texture lookup map (case-insensitive) - for (const tex of textureList) { - if (tex.name) { - this.textures.set(tex.name.toLowerCase(), this.createTexture(tex)); - } - } + this.loadTextures(textureList); const legoColor = LegoColors[colorName] || LegoColors['lego red']; const threeLegoColor = new THREE.Color(legoColor.r / 255, legoColor.g / 255, legoColor.b / 255);