From ba0b28d61658676deaecdbf91cbcbeeb9079ba39 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 31 Mar 2026 17:39:39 +0200 Subject: [PATCH] Add model instance rendering functionality --- src/assets/model-instance-rendering.ts | 104 +++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/assets/model-instance-rendering.ts diff --git a/src/assets/model-instance-rendering.ts b/src/assets/model-instance-rendering.ts new file mode 100644 index 00000000..15d2b533 --- /dev/null +++ b/src/assets/model-instance-rendering.ts @@ -0,0 +1,104 @@ +import { BoxGeometry, Group, Mesh, MeshBasicMaterial } from "three"; + +import type { Vec3 } from "../core/vector"; + +import { instantiateModelTemplate, type LoadedModelAsset } from "./gltf-model-import"; +import type { ModelInstance } from "./model-instances"; +import type { ProjectAssetRecord } from "./project-assets"; + +const MODEL_PLACEHOLDER_COLOR = 0x89b6ff; +const MODEL_SELECTION_COLOR = 0xf7d2aa; + +interface ModelInstanceBounds { + center: Vec3; + size: Vec3; +} + +function cloneVec3(vector: Vec3): Vec3 { + return { + x: vector.x, + y: vector.y, + z: vector.z + }; +} + +function getLocalModelBounds(asset: ProjectAssetRecord | undefined): ModelInstanceBounds { + if (asset?.kind === "model" && asset.metadata.boundingBox !== null) { + const boundingBox = asset.metadata.boundingBox; + + return { + center: { + x: (boundingBox.min.x + boundingBox.max.x) * 0.5, + y: (boundingBox.min.y + boundingBox.max.y) * 0.5, + z: (boundingBox.min.z + boundingBox.max.z) * 0.5 + }, + size: { + x: Math.max(0.1, Math.abs(boundingBox.max.x - boundingBox.min.x)), + y: Math.max(0.1, Math.abs(boundingBox.max.y - boundingBox.min.y)), + z: Math.max(0.1, Math.abs(boundingBox.max.z - boundingBox.min.z)) + } + }; + } + + return { + center: { + x: 0, + y: 0, + z: 0 + }, + size: { + x: 1, + y: 1, + z: 1 + } + }; +} + +function createWireframeBox(size: Vec3, color: number, opacity: number): Mesh { + return new Mesh( + new BoxGeometry(size.x, size.y, size.z), + new MeshBasicMaterial({ + color, + wireframe: true, + transparent: true, + opacity, + depthWrite: false + }) + ); +} + +export function createModelInstanceRenderGroup( + modelInstance: ModelInstance, + asset: ProjectAssetRecord | undefined, + loadedAsset: LoadedModelAsset | undefined, + selected = false +): Group { + const bounds = getLocalModelBounds(asset); + const group = new Group(); + + group.position.copy(cloneVec3(modelInstance.position)); + group.rotation.set( + (modelInstance.rotationDegrees.x * Math.PI) / 180, + (modelInstance.rotationDegrees.y * Math.PI) / 180, + (modelInstance.rotationDegrees.z * Math.PI) / 180 + ); + group.scale.set(modelInstance.scale.x, modelInstance.scale.y, modelInstance.scale.z); + group.userData.modelInstanceId = modelInstance.id; + group.userData.assetId = modelInstance.assetId; + + if (loadedAsset !== undefined) { + group.add(instantiateModelTemplate(loadedAsset.template)); + } else { + const placeholder = createWireframeBox(bounds.size, MODEL_PLACEHOLDER_COLOR, 0.28); + placeholder.position.set(bounds.center.x, bounds.center.y, bounds.center.z); + group.add(placeholder); + } + + if (selected) { + const selectionShell = createWireframeBox(bounds.size, MODEL_SELECTION_COLOR, 0.8); + selectionShell.position.set(bounds.center.x, bounds.center.y, bounds.center.z); + group.add(selectionShell); + } + + return group; +}