diff --git a/Extensions/3D/JsExtension.js b/Extensions/3D/JsExtension.js index a1600760912c..77c3ee2b8ff7 100644 --- a/Extensions/3D/JsExtension.js +++ b/Extensions/3D/JsExtension.js @@ -271,7 +271,8 @@ module.exports = { .setIncludeFile('Extensions/3D/A_RuntimeObject3D.js') .addIncludeFile('Extensions/3D/A_RuntimeObject3DRenderer.js') .addIncludeFile('Extensions/3D/Model3DRuntimeObject.js') - .addIncludeFile('Extensions/3D/Model3DRuntimeObject3DRenderer.js'); + .addIncludeFile('Extensions/3D/Model3DRuntimeObject3DRenderer.js') + .addIncludeFile('Extensions/3D/Model3DRuntimeObjectMeshParts.js'); // Properties expressions/conditions/actions: @@ -842,6 +843,254 @@ module.exports = { .addParameter('object', _('3D model'), 'Model3DObject', false) .addParameter('number', _('Crossfade duration (in seconds)'), '', false) .setFunctionName('setCrossfadeDuration'); + + // Mesh Parts Actions and Conditions + + object + .addScopedAction( + 'SetMeshVisible', + _('Show/hide a mesh part'), + _('Show or hide a specific mesh part of the 3D model.'), + _('Set mesh _PARAM1_ visibility of _PARAM0_ to _PARAM2_'), + _('Mesh parts'), + 'res/conditions/3d_box.svg', + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .addParameter('yesorno', _('Visible'), '', false) + .markAsAdvanced() + .setFunctionName('setMeshVisible'); + + object + .addScopedCondition( + 'IsMeshVisible', + _('Mesh part is visible'), + _('Check if a mesh part is visible.'), + _('Mesh _PARAM1_ of _PARAM0_ is visible'), + _('Mesh parts'), + 'res/conditions/3d_box.svg', + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .markAsAdvanced() + .setFunctionName('isMeshVisible'); + + object + .addScopedCondition( + 'HasMesh', + _('Has mesh part'), + _('Check if the model has a mesh part with the given name.'), + _('_PARAM0_ has mesh _PARAM1_'), + _('Mesh parts'), + 'res/conditions/3d_box.svg', + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .markAsAdvanced() + .setFunctionName('hasMesh'); + + object + .addScopedAction( + 'SetMeshPosition', + _('Change mesh part position'), + _('Change the position of a mesh part (relative to the model).'), + _('Set mesh _PARAM1_ position of _PARAM0_ to X: _PARAM2_, Y: _PARAM3_, Z: _PARAM4_'), + _('Mesh parts'), + 'res/conditions/3d_box.svg', + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .addParameter('number', _('X position'), '', false) + .addParameter('number', _('Y position'), '', false) + .addParameter('number', _('Z position'), '', false) + .markAsAdvanced() + .setFunctionName('setMeshPosition'); + + object + .addExpression( + 'MeshPartPositionX', + _('Mesh part X position'), + _('Return the X position of a mesh part.'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .setFunctionName('getMeshPositionX'); + + object + .addExpression( + 'MeshPartPositionY', + _('Mesh part Y position'), + _('Return the Y position of a mesh part.'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .setFunctionName('getMeshPositionY'); + + object + .addExpression( + 'MeshPartPositionZ', + _('Mesh part Z position'), + _('Return the Z position of a mesh part.'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .setFunctionName('getMeshPositionZ'); + + object + .addScopedAction( + 'SetMeshRotation', + _('Change mesh part rotation'), + _('Change the rotation of a mesh part (relative to the model).'), + _('Set mesh _PARAM1_ rotation of _PARAM0_ to X: _PARAM2_°, Y: _PARAM3_°, Z: _PARAM4_°'), + _('Mesh parts'), + 'res/conditions/3d_box.svg', + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .addParameter('number', _('Rotation X (degrees)'), '', false) + .addParameter('number', _('Rotation Y (degrees)'), '', false) + .addParameter('number', _('Rotation Z (degrees)'), '', false) + .markAsAdvanced() + .setFunctionName('setMeshRotation'); + + object + .addExpression( + 'MeshPartRotationX', + _('Mesh part X rotation'), + _('Return the X rotation of a mesh part (in degrees).'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .setFunctionName('getMeshRotationX'); + + object + .addExpression( + 'MeshPartRotationY', + _('Mesh part Y rotation'), + _('Return the Y rotation of a mesh part (in degrees).'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .setFunctionName('getMeshRotationY'); + + object + .addExpression( + 'MeshPartRotationZ', + _('Mesh part Z rotation'), + _('Return the Z rotation of a mesh part (in degrees).'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .setFunctionName('getMeshRotationZ'); + + object + .addScopedAction( + 'SetMeshScale', + _('Change mesh part scale'), + _('Change the scale of a mesh part.'), + _('Set mesh _PARAM1_ scale of _PARAM0_ to X: _PARAM2_, Y: _PARAM3_, Z: _PARAM4_'), + _('Mesh parts'), + 'res/conditions/3d_box.svg', + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .addParameter('number', _('Scale X'), '', false) + .addParameter('number', _('Scale Y'), '', false) + .addParameter('number', _('Scale Z'), '', false) + .markAsAdvanced() + .setFunctionName('setMeshScale'); + + object + .addExpression( + 'MeshPartScaleX', + _('Mesh part X scale'), + _('Return the X scale of a mesh part.'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .setFunctionName('getMeshScaleX'); + + object + .addExpression( + 'MeshPartScaleY', + _('Mesh part Y scale'), + _('Return the Y scale of a mesh part.'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .setFunctionName('getMeshScaleY'); + + object + .addExpression( + 'MeshPartScaleZ', + _('Mesh part Z scale'), + _('Return the Z scale of a mesh part.'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .setFunctionName('getMeshScaleZ'); + + object + .addScopedAction( + 'RemoveMesh', + _('Remove mesh part'), + _('Remove a mesh part from the 3D model.'), + _('Remove mesh _PARAM1_ from _PARAM0_'), + _('Mesh parts'), + 'res/conditions/3d_box.svg', + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('string', _('Mesh name'), '', false) + .markAsAdvanced() + .setFunctionName('removeMesh'); + + object + .addExpression( + 'MeshesCount', + _('Mesh parts count'), + _('Return the number of mesh parts in the model.'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .setFunctionName('getMeshesCount'); + + object + .addStrExpression( + 'MeshNameAt', + _('Mesh part name at index'), + _('Return the name of the mesh part at the given index.'), + _('Mesh parts'), + 'res/conditions/3d_box.svg' + ) + .addParameter('object', _('3D model'), 'Model3DObject', false) + .addParameter('number', _('Index'), '', false) + .setFunctionName('getMeshNameAt'); } const Cube3DObject = new gd.ObjectJsImplementation(); diff --git a/Extensions/3D/Model3DRuntimeObject.ts b/Extensions/3D/Model3DRuntimeObject.ts index 5c6aba48f9d1..15d53795ad66 100644 --- a/Extensions/3D/Model3DRuntimeObject.ts +++ b/Extensions/3D/Model3DRuntimeObject.ts @@ -75,6 +75,7 @@ namespace gdjs { implements gdjs.Animatable { _renderer: gdjs.Model3DRuntimeObjectRenderer; + _meshParts: gdjs.Model3DRuntimeObjectMeshParts; _modelResourceName: string; _materialType: gdjs.Model3DRuntimeObject.MaterialType = @@ -131,6 +132,7 @@ namespace gdjs { this, instanceContainer ); + this._meshParts = new gdjs.Model3DRuntimeObjectMeshParts(); this._materialType = this._convertMaterialType( objectData.content.materialType ); @@ -502,6 +504,192 @@ namespace gdjs { const originPoint = this._renderer.getOriginPoint(); return this.getZ() - this.getDepth() * originPoint[2]; } + + // Mesh Parts Management + + /** + * Get the mesh parts manager for this object. + * @returns The mesh parts manager + */ + getMeshParts(): gdjs.Model3DRuntimeObjectMeshParts { + return this._meshParts; + } + + /** + * Get the number of meshes in the model. + * @returns Number of meshes + */ + getMeshesCount(): number { + return this._meshParts.getMeshesCount(); + } + + /** + * Get mesh name at a specific index. + * @param index The index + * @returns The mesh name + */ + getMeshNameAt(index: number): string { + return this._meshParts.getMeshNameAt(index); + } + + /** + * Check if a mesh exists. + * @param name The mesh name + * @returns True if the mesh exists + */ + hasMesh(name: string): boolean { + return this._meshParts.hasMesh(name); + } + + /** + * Set mesh visibility. + * @param name The mesh name + * @param visible Whether the mesh should be visible + */ + setMeshVisible(name: string, visible: boolean): void { + this._meshParts.setMeshVisible(name, visible); + } + + /** + * Check if a mesh is visible. + * @param name The mesh name + * @returns True if visible + */ + isMeshVisible(name: string): boolean { + return this._meshParts.isMeshVisible(name); + } + + /** + * Set mesh position. + * @param name The mesh name + * @param x X position + * @param y Y position + * @param z Z position + */ + setMeshPosition(name: string, x: number, y: number, z: number): void { + this._meshParts.setMeshPosition(name, x, y, z); + } + + /** + * Get mesh X position. + * @param name The mesh name + * @returns X position + */ + getMeshPositionX(name: string): number { + return this._meshParts.getMeshPositionX(name); + } + + /** + * Get mesh Y position. + * @param name The mesh name + * @returns Y position + */ + getMeshPositionY(name: string): number { + return this._meshParts.getMeshPositionY(name); + } + + /** + * Get mesh Z position. + * @param name The mesh name + * @returns Z position + */ + getMeshPositionZ(name: string): number { + return this._meshParts.getMeshPositionZ(name); + } + + /** + * Set mesh rotation. + * @param name The mesh name + * @param rotationX Rotation around X axis in degrees + * @param rotationY Rotation around Y axis in degrees + * @param rotationZ Rotation around Z axis in degrees + */ + setMeshRotation( + name: string, + rotationX: number, + rotationY: number, + rotationZ: number + ): void { + this._meshParts.setMeshRotation(name, rotationX, rotationY, rotationZ); + } + + /** + * Get mesh X rotation. + * @param name The mesh name + * @returns X rotation in degrees + */ + getMeshRotationX(name: string): number { + return this._meshParts.getMeshRotationX(name); + } + + /** + * Get mesh Y rotation. + * @param name The mesh name + * @returns Y rotation in degrees + */ + getMeshRotationY(name: string): number { + return this._meshParts.getMeshRotationY(name); + } + + /** + * Get mesh Z rotation. + * @param name The mesh name + * @returns Z rotation in degrees + */ + getMeshRotationZ(name: string): number { + return this._meshParts.getMeshRotationZ(name); + } + + /** + * Set mesh scale. + * @param name The mesh name + * @param scaleX Scale on X axis + * @param scaleY Scale on Y axis + * @param scaleZ Scale on Z axis + */ + setMeshScale( + name: string, + scaleX: number, + scaleY: number, + scaleZ: number + ): void { + this._meshParts.setMeshScale(name, scaleX, scaleY, scaleZ); + } + + /** + * Get mesh X scale. + * @param name The mesh name + * @returns X scale + */ + getMeshScaleX(name: string): number { + return this._meshParts.getMeshScaleX(name); + } + + /** + * Get mesh Y scale. + * @param name The mesh name + * @returns Y scale + */ + getMeshScaleY(name: string): number { + return this._meshParts.getMeshScaleY(name); + } + + /** + * Get mesh Z scale. + * @param name The mesh name + * @returns Z scale + */ + getMeshScaleZ(name: string): number { + return this._meshParts.getMeshScaleZ(name); + } + + /** + * Remove a mesh from the model. + * @param name The mesh name + */ + removeMesh(name: string): void { + this._meshParts.removeMesh(name); + } } /** @category Objects > 3D Model */ diff --git a/Extensions/3D/Model3DRuntimeObject3DRenderer.ts b/Extensions/3D/Model3DRuntimeObject3DRenderer.ts index 1b08ce28fcd4..defe82f8914e 100644 --- a/Extensions/3D/Model3DRuntimeObject3DRenderer.ts +++ b/Extensions/3D/Model3DRuntimeObject3DRenderer.ts @@ -144,7 +144,7 @@ namespace gdjs { * When the object change of size, rotation or position, * the transformation is done on the parent of `threeObject`. * - * This function doesn't mutate anything outside of `threeObject`. + * This function mutates `threeObject` and stores normalization scale in mesh parts. */ stretchModelIntoUnitaryCube( threeObject: THREE.Object3D, @@ -206,6 +206,10 @@ namespace gdjs { threeObject.updateMatrix(); threeObject.applyMatrix4(scaleMatrix); + // Store the normalization scale for mesh parts positioning + // Note: Y scale is negated in the matrix but we store the absolute value + this._model3DRuntimeObject._meshParts.setNormalizationScale(scaleX, scaleY, scaleZ); + return boundingBox; } @@ -321,6 +325,9 @@ namespace gdjs { this.updatePosition(); this._updateShadow(); + // Build mesh parts map after the model is loaded + this._model3DRuntimeObject._meshParts.buildMeshesMap(root, this._model3DRuntimeObject); + // Start the current animation on the new 3D object. this._animationMixer = new THREE.AnimationMixer(root); const isAnimationPaused = this._model3DRuntimeObject.isAnimationPaused(); @@ -467,6 +474,14 @@ namespace gdjs { ); return clip ? clip.duration : 0; } + + /** + * Get the THREE.Object3D for direct access to meshes. + * @returns The THREE.Object3D + */ + getThreeObject(): THREE.Object3D { + return this._threeObject; + } } /** @category Renderers > 3D Model */ diff --git a/Extensions/3D/Model3DRuntimeObjectMeshParts.ts b/Extensions/3D/Model3DRuntimeObjectMeshParts.ts new file mode 100644 index 000000000000..658564b86093 --- /dev/null +++ b/Extensions/3D/Model3DRuntimeObjectMeshParts.ts @@ -0,0 +1,498 @@ +namespace gdjs { + /** + * Manages individual mesh parts of a 3D model. + * Allows control over visibility, position, rotation, and scale of child meshes. + * @category Objects > 3D Model + */ + export class Model3DRuntimeObjectMeshParts { + private _meshesMap: Record; + private _meshesNames: string[]; + private _runtimeObject: gdjs.Model3DRuntimeObject | null; + private _originalMeshPositions: Record; + private _originalMeshRotations: Record; + private _originalMeshScales: Record; + private _normalizationScale: { x: number; y: number; z: number } | null; + + constructor() { + this._meshesMap = {}; + this._meshesNames = []; + this._runtimeObject = null; + this._originalMeshPositions = {}; + this._originalMeshRotations = {}; + this._originalMeshScales = {}; + this._normalizationScale = null; + } + + /** + * Build the meshes map from a THREE.Object3D. + * This traverses the object hierarchy and indexes all named meshes. + * @param threeObject The root THREE.Object3D to traverse + * @param runtimeObject The runtime object that owns this mesh parts manager + */ + buildMeshesMap(threeObject: THREE.Object3D, runtimeObject?: gdjs.Model3DRuntimeObject): void { + this._runtimeObject = runtimeObject || null; + this._meshesMap = {}; + this._meshesNames = []; + this._originalMeshPositions = {}; + this._originalMeshRotations = {}; + this._originalMeshScales = {}; + + if (!threeObject) { + return; + } + + // Traverse the object hierarchy to find all meshes and groups + threeObject.traverse((child: THREE.Object3D) => { + // Only add objects that have a name + if (child.name && child.name !== '') { + // Skip if already indexed (avoid duplicates) + if (child.name in this._meshesMap) { + return; + } + + // Only index Mesh and Group nodes, skip bones, lights, cameras, and other helper nodes + const isMesh = child instanceof THREE.Mesh; + const isGroup = child instanceof THREE.Group; + + if (isMesh || isGroup) { + this._meshesMap[child.name] = child; + this._meshesNames.push(child.name); + // Store the original local transformations of the mesh before any user modifications + this._originalMeshPositions[child.name] = child.position.clone(); + this._originalMeshRotations[child.name] = child.rotation.clone(); + this._originalMeshScales[child.name] = child.scale.clone(); + } + } + }); + } + + /** + * Set the normalization scale used when the model was stretched into a unit cube. + * This scale is needed to correctly convert between object space and mesh local space. + * @param scaleX The X normalization scale (1 / modelWidth) + * @param scaleY The Y normalization scale (1 / modelHeight) + * @param scaleZ The Z normalization scale (1 / modelDepth) + */ + setNormalizationScale(scaleX: number, scaleY: number, scaleZ: number): void { + this._normalizationScale = { x: scaleX, y: scaleY, z: scaleZ }; + } + + /** + * Get all mesh names. + * @returns Array of mesh names + */ + getMeshesNames(): string[] { + return this._meshesNames; + } + + /** + * Get the total number of meshes. + * @returns Number of meshes + */ + getMeshesCount(): number { + return this._meshesNames.length; + } + + /** + * Get mesh name at a specific index. + * @param index The index of the mesh + * @returns The mesh name or empty string if index is out of bounds + */ + getMeshNameAt(index: number): string { + if (index < 0 || index >= this._meshesNames.length) { + return ''; + } + return this._meshesNames[index]; + } + + /** + * Check if a mesh with the given name exists. + * @param name The mesh name + * @returns True if the mesh exists + */ + hasMesh(name: string): boolean { + return name in this._meshesMap; + } + + /** + * Set the visibility of a mesh. + * @param name The mesh name + * @param visible Whether the mesh should be visible + */ + setMeshVisible(name: string, visible: boolean): void { + if (this.hasMesh(name)) { + this._meshesMap[name].visible = visible; + } + } + + /** + * Get the visibility of a mesh. + * @param name The mesh name + * @returns True if visible, false otherwise + */ + isMeshVisible(name: string): boolean { + return this.hasMesh(name) ? this._meshesMap[name].visible : false; + } + + /** + * Set the position of a mesh. + * The position values are in the same coordinate system as the object dimensions. + * They are automatically scaled to match the model's internal coordinate system. + * @param name The mesh name + * @param x X position + * @param y Y position + * @param z Z position + */ + setMeshPosition(name: string, x: number, y: number, z: number): void { + if (!this.hasMesh(name) || !this._runtimeObject || !this._normalizationScale) { + return; + } + + const originalPos = this._originalMeshPositions[name]; + if (!originalPos) { + return; + } + + // Convert from object space to mesh local space. + // The transformation chain is: meshLocal -> normalized (1x1x1) -> objectDimensions + // So: objectSpace = meshLocal * normalizationScale * objectDimensions + // Therefore: meshLocal = objectSpace / (normalizationScale * objectDimensions) + const objectWidth = this._runtimeObject.getWidth(); + const objectHeight = this._runtimeObject.getHeight(); + const objectDepth = this._runtimeObject.getDepth(); + + const normalizedX = objectWidth !== 0 && this._normalizationScale.x !== 0 + ? x / (this._normalizationScale.x * objectWidth) + : 0; + // Y axis is flipped in the renderer, so we need to negate it + const normalizedY = objectHeight !== 0 && this._normalizationScale.y !== 0 + ? -y / (this._normalizationScale.y * objectHeight) + : 0; + const normalizedZ = objectDepth !== 0 && this._normalizationScale.z !== 0 + ? z / (this._normalizationScale.z * objectDepth) + : 0; + + // Add to the original local position to preserve the mesh's position in the model hierarchy + this._meshesMap[name].position.set( + originalPos.x + normalizedX, + originalPos.y + normalizedY, + originalPos.z + normalizedZ + ); + } + + /** + * Get the X position of a mesh. + * The returned value is in the same coordinate system as the object dimensions. + * @param name The mesh name + * @returns X position or 0 if mesh doesn't exist + */ + getMeshPositionX(name: string): number { + if (!this.hasMesh(name) || !this._runtimeObject || !this._normalizationScale) { + return 0; + } + + const originalPos = this._originalMeshPositions[name]; + if (!originalPos) { + return 0; + } + + const currentPos = this._meshesMap[name].position; + const objectWidth = this._runtimeObject.getWidth(); + + // Subtract the original position and convert back to object space + // The transformation is: objectSpace = meshLocal * normalizationScale * objectDimensions + const normalizedOffset = currentPos.x - originalPos.x; + return normalizedOffset * this._normalizationScale.x * objectWidth; + } + + /** + * Get the Y position of a mesh. + * The returned value is in the same coordinate system as the object dimensions. + * @param name The mesh name + * @returns Y position or 0 if mesh doesn't exist + */ + getMeshPositionY(name: string): number { + if (!this.hasMesh(name) || !this._runtimeObject || !this._normalizationScale) { + return 0; + } + + const originalPos = this._originalMeshPositions[name]; + if (!originalPos) { + return 0; + } + + const currentPos = this._meshesMap[name].position; + const objectHeight = this._runtimeObject.getHeight(); + + // Subtract the original position and convert back to object space + // Y axis is flipped in the renderer, so we need to negate it + // The transformation is: objectSpace = meshLocal * normalizationScale * objectDimensions + const normalizedOffset = currentPos.y - originalPos.y; + return -normalizedOffset * this._normalizationScale.y * objectHeight; + } + + /** + * Get the Z position of a mesh. + * The returned value is in the same coordinate system as the object dimensions. + * @param name The mesh name + * @returns Z position or 0 if mesh doesn't exist + */ + getMeshPositionZ(name: string): number { + if (!this.hasMesh(name) || !this._runtimeObject || !this._normalizationScale) { + return 0; + } + + const originalPos = this._originalMeshPositions[name]; + if (!originalPos) { + return 0; + } + + const currentPos = this._meshesMap[name].position; + const objectDepth = this._runtimeObject.getDepth(); + + // Subtract the original position and convert back to object space + // The transformation is: objectSpace = meshLocal * normalizationScale * objectDimensions + const normalizedOffset = currentPos.z - originalPos.z; + return normalizedOffset * this._normalizationScale.z * objectDepth; + } + + /** + * Set the rotation of a mesh (in degrees). + * The rotation values are offsets added to the mesh's original rotation in the model. + * @param name The mesh name + * @param rotationX Rotation offset around X axis in degrees + * @param rotationY Rotation offset around Y axis in degrees + * @param rotationZ Rotation offset around Z axis in degrees + */ + setMeshRotation( + name: string, + rotationX: number, + rotationY: number, + rotationZ: number + ): void { + if (!this.hasMesh(name)) { + return; + } + + const originalRot = this._originalMeshRotations[name]; + if (!originalRot) { + return; + } + + // Set absolute rotation by adding user offset to original rotation + this._meshesMap[name].rotation.set( + originalRot.x + gdjs.toRad(rotationX), + originalRot.y + gdjs.toRad(rotationY), + originalRot.z + gdjs.toRad(rotationZ), + originalRot.order + ); + } + + /** + * Get the X rotation of a mesh (in degrees). + * The returned value is the offset from the mesh's original rotation. + * @param name The mesh name + * @returns X rotation offset in degrees or 0 if mesh doesn't exist + */ + getMeshRotationX(name: string): number { + if (!this.hasMesh(name)) { + return 0; + } + + const originalRot = this._originalMeshRotations[name]; + if (!originalRot) { + return 0; + } + + const currentRot = this._meshesMap[name].rotation; + return gdjs.toDegrees(currentRot.x - originalRot.x); + } + + /** + * Get the Y rotation of a mesh (in degrees). + * The returned value is the offset from the mesh's original rotation. + * @param name The mesh name + * @returns Y rotation offset in degrees or 0 if mesh doesn't exist + */ + getMeshRotationY(name: string): number { + if (!this.hasMesh(name)) { + return 0; + } + + const originalRot = this._originalMeshRotations[name]; + if (!originalRot) { + return 0; + } + + const currentRot = this._meshesMap[name].rotation; + return gdjs.toDegrees(currentRot.y - originalRot.y); + } + + /** + * Get the Z rotation of a mesh (in degrees). + * The returned value is the offset from the mesh's original rotation. + * @param name The mesh name + * @returns Z rotation offset in degrees or 0 if mesh doesn't exist + */ + getMeshRotationZ(name: string): number { + if (!this.hasMesh(name)) { + return 0; + } + + const originalRot = this._originalMeshRotations[name]; + if (!originalRot) { + return 0; + } + + const currentRot = this._meshesMap[name].rotation; + return gdjs.toDegrees(currentRot.z - originalRot.z); + } + + /** + * Set the scale of a mesh. + * The scale values are relative to the mesh's original scale in the model. + * Note: If the mesh has zero scale on any axis in the original model, + * the result will remain zero regardless of the input value. + * @param name The mesh name + * @param scaleX Scale on X axis + * @param scaleY Scale on Y axis + * @param scaleZ Scale on Z axis + */ + setMeshScale( + name: string, + scaleX: number, + scaleY: number, + scaleZ: number + ): void { + if (!this.hasMesh(name)) { + return; + } + + const originalScale = this._originalMeshScales[name]; + if (!originalScale) { + return; + } + + // Multiply the user-specified scale with the original scale + this._meshesMap[name].scale.set( + originalScale.x * scaleX, + originalScale.y * scaleY, + originalScale.z * scaleZ + ); + } + + /** + * Get the X scale of a mesh. + * The returned value is relative to the mesh's original scale. + * @param name The mesh name + * @returns X scale or 1 if mesh doesn't exist + */ + getMeshScaleX(name: string): number { + if (!this.hasMesh(name)) { + return 1; + } + + const originalScale = this._originalMeshScales[name]; + if (!originalScale || originalScale.x === 0) { + return 1; + } + + return this._meshesMap[name].scale.x / originalScale.x; + } + + /** + * Get the Y scale of a mesh. + * The returned value is relative to the mesh's original scale. + * @param name The mesh name + * @returns Y scale or 1 if mesh doesn't exist + */ + getMeshScaleY(name: string): number { + if (!this.hasMesh(name)) { + return 1; + } + + const originalScale = this._originalMeshScales[name]; + if (!originalScale || originalScale.y === 0) { + return 1; + } + + return this._meshesMap[name].scale.y / originalScale.y; + } + + /** + * Get the Z scale of a mesh. + * The returned value is relative to the mesh's original scale. + * @param name The mesh name + * @returns Z scale or 1 if mesh doesn't exist + */ + getMeshScaleZ(name: string): number { + if (!this.hasMesh(name)) { + return 1; + } + + const originalScale = this._originalMeshScales[name]; + if (!originalScale || originalScale.z === 0) { + return 1; + } + + return this._meshesMap[name].scale.z / originalScale.z; + } + + /** + * Remove a mesh from the scene. + * This also removes all descendant meshes from the registry. + * @param name The mesh name + */ + removeMesh(name: string): void { + if (this.hasMesh(name)) { + const mesh = this._meshesMap[name]; + + // Collect all descendant mesh names that need to be removed from the registry + const descendantNames: string[] = []; + mesh.traverse((child: THREE.Object3D) => { + if (child !== mesh && child.name && child.name in this._meshesMap) { + descendantNames.push(child.name); + } + }); + + // Remove from parent (this detaches the entire subtree from the scene) + if (mesh.parent) { + mesh.parent.remove(mesh); + } + + // Clean up the mesh itself + delete this._meshesMap[name]; + delete this._originalMeshPositions[name]; + delete this._originalMeshRotations[name]; + delete this._originalMeshScales[name]; + + // Clean up all descendants + for (const descendantName of descendantNames) { + delete this._meshesMap[descendantName]; + delete this._originalMeshPositions[descendantName]; + delete this._originalMeshRotations[descendantName]; + delete this._originalMeshScales[descendantName]; + } + + // Update the names array to exclude the removed mesh and its descendants + const removedNames = new Set([name, ...descendantNames]); + this._meshesNames = this._meshesNames.filter( + (meshName) => !removedNames.has(meshName) + ); + } + } + + /** + * Clear all mesh references. + */ + clear(): void { + this._meshesMap = {}; + this._meshesNames = []; + this._runtimeObject = null; + this._originalMeshPositions = {}; + this._originalMeshRotations = {}; + this._originalMeshScales = {}; + this._normalizationScale = null; + } + } +}