namespace gdjs { type CustomObject3DNetworkSyncDataType = { z: float; d: float; rx: float; ry: float; ifz: boolean; ccz: float; }; type CustomObject3DNetworkSyncData = CustomObjectNetworkSyncData & CustomObject3DNetworkSyncDataType; /** * Base class for 3D custom objects. * @category Objects > Custom Object 3D */ export class CustomRuntimeObject3D extends gdjs.CustomRuntimeObject implements gdjs.Base3DHandler { /** * Position on the Z axis. */ private _z: float = 0; private _minZ: float = 0; private _maxZ: float = 0; private _scaleZ: float = 1; private _flippedZ: boolean = false; /** * Euler angle with the `ZYX` order. * * Note that `_rotationZ` is `angle` from `gdjs.RuntimeObject`. */ private _rotationX: float = 0; /** * Euler angle with the `ZYX` order. * * Note that `_rotationZ` is `angle` from `gdjs.RuntimeObject`. */ private _rotationY: float = 0; private _customCenterZ: float = 0; private static _temporaryVector = new THREE.Vector3(); constructor( parent: gdjs.RuntimeInstanceContainer, objectData: gdjs.Object3DData & gdjs.CustomObjectConfiguration, instanceData?: InstanceData ) { super(parent, objectData, instanceData); } protected override _createRender() { const parent = this._runtimeScene; return new gdjs.CustomRuntimeObject3DRenderer( this, this._instanceContainer, parent ); } protected override _reinitializeRenderer(): void { this.getRenderer().reinitialize(this, this.getParent()); } override getRenderer(): gdjs.CustomRuntimeObject3DRenderer { return super.getRenderer() as gdjs.CustomRuntimeObject3DRenderer; } override get3DRendererObject() { // It can't be null because Three.js is always loaded // when a custom 3D object is used. return this.getRenderer().get3DRendererObject()!; } override extraInitializationFromInitialInstance( initialInstanceData: InstanceData ) { super.extraInitializationFromInitialInstance(initialInstanceData); if (initialInstanceData.depth !== undefined) { this.setDepth(initialInstanceData.depth); } this.flipX(!!initialInstanceData.flippedX); this.flipY(!!initialInstanceData.flippedY); this.flipZ(!!initialInstanceData.flippedZ); } getNetworkSyncData( syncOptions: GetNetworkSyncDataOptions ): CustomObject3DNetworkSyncData { return { ...super.getNetworkSyncData(syncOptions), z: this.getZ(), d: this.getDepth(), rx: this.getRotationX(), ry: this.getRotationY(), ifz: this.isFlippedZ(), ccz: this._customCenterZ, }; } updateFromNetworkSyncData( networkSyncData: CustomObject3DNetworkSyncData, options: UpdateFromNetworkSyncDataOptions ): void { super.updateFromNetworkSyncData(networkSyncData, options); if (networkSyncData.z !== undefined) this.setZ(networkSyncData.z); if (networkSyncData.d !== undefined) this.setDepth(networkSyncData.d); if (networkSyncData.rx !== undefined) this.setRotationX(networkSyncData.rx); if (networkSyncData.ry !== undefined) this.setRotationY(networkSyncData.ry); if (networkSyncData.ifz !== undefined) this.flipZ(networkSyncData.ifz); if (networkSyncData.ccz !== undefined) this._customCenterZ = networkSyncData.ccz; } /** * Set the object position on the Z axis. */ setZ(z: float): void { if (z === this._z) return; this._z = z; this.getRenderer().updatePosition(); } /** * Get the object position on the Z axis. */ getZ(): float { return this._z; } /** * Get the Z position of the rendered object. * * For most objects, this will returns the same value as getZ(). But if the * object has an origin that is not the same as the point (0,0,0) of the * object displayed, getDrawableZ will differ. * * @return The Z position of the rendered object. */ getDrawableZ(): float { const minZ = this.getUnscaledMinZ(); const absScaleZ = this.getScaleZ(); if (!this._flippedZ) { return this._z + minZ * absScaleZ; } else { return ( this._z + (-minZ - this.getUnscaledDepth() + 2 * this.getUnscaledCenterZ()) * absScaleZ ); } } private getUnscaledMinZ(): float { if (this._innerArea) { return this._innerArea.min[2]; } else { if (this._isUntransformedHitBoxesDirty) { this._updateUntransformedHitBoxes(); } return this._minZ; } } /** * Return the Z position of the object center, **relative to the object Z * position** (`getDrawableX`). * * Use `getCenterZInScene` to get the position of the center in the scene. * * @return the Z position of the object center, relative to * `getDrawableZ()`. */ getCenterZ(): float { return ( (this.getUnscaledCenterZ() - this.getUnscaledMinZ()) * this.getScaleZ() ); } getCenterZInScene(): float { return this.getDrawableZ() + this.getCenterZ(); } setCenterZInScene(z: float): void { this.setZ(z + this._z - (this.getDrawableZ() + this.getCenterZ())); } /** * Return the bottom Z of the object. * Rotations around X and Y are not taken into account. */ getUnrotatedAABBMinZ(): number { return this.getDrawableZ(); } /** * Return the top Z of the object. * Rotations around X and Y are not taken into account. */ getUnrotatedAABBMaxZ(): number { return this.getDrawableZ() + this.getDepth(); } /** * Set the object rotation on the X axis. * * This is an Euler angle. Objects use the `ZYX` order. */ setRotationX(angle: float): void { this._rotationX = angle; this.getRenderer().updateRotation(); } /** * Set the object rotation on the Y axis. * * This is an Euler angle. Objects use the `ZYX` order. */ setRotationY(angle: float): void { this._rotationY = angle; this.getRenderer().updateRotation(); } /** * Get the object rotation on the X axis. * * This is an Euler angle. Objects use the `ZYX` order. */ getRotationX(): float { return this._rotationX; } /** * Get the object rotation on the Y axis. * * This is an Euler angle. Objects use the `ZYX` order. */ getRotationY(): float { return this._rotationY; } /** * Turn the object around the scene x axis at its center. * @param deltaAngle the rotation angle */ turnAroundX(deltaAngle: float): void { const axisX = gdjs.CustomRuntimeObject3D._temporaryVector; axisX.set(1, 0, 0); const mesh = this.get3DRendererObject(); mesh.rotateOnWorldAxis(axisX, gdjs.toRad(deltaAngle)); this._rotationX = gdjs.toDegrees(mesh.rotation.x); this._rotationY = gdjs.toDegrees(mesh.rotation.y); this.setAngle(gdjs.toDegrees(mesh.rotation.z)); } /** * Turn the object around the scene y axis at its center. * @param deltaAngle the rotation angle */ turnAroundY(deltaAngle: float): void { const axisY = gdjs.CustomRuntimeObject3D._temporaryVector; axisY.set(0, 1, 0); const mesh = this.get3DRendererObject(); mesh.rotateOnWorldAxis(axisY, gdjs.toRad(deltaAngle)); this._rotationX = gdjs.toDegrees(mesh.rotation.x); this._rotationY = gdjs.toDegrees(mesh.rotation.y); this.setAngle(gdjs.toDegrees(mesh.rotation.z)); } /** * Turn the object around the scene z axis at its center. * @param deltaAngle the rotation angle */ turnAroundZ(deltaAngle: float): void { const axisZ = gdjs.CustomRuntimeObject3D._temporaryVector; axisZ.set(0, 0, 1); const mesh = this.get3DRendererObject(); mesh.rotateOnWorldAxis(axisZ, gdjs.toRad(deltaAngle)); this._rotationX = gdjs.toDegrees(mesh.rotation.x); this._rotationY = gdjs.toDegrees(mesh.rotation.y); this.setAngle(gdjs.toDegrees(mesh.rotation.z)); } /** * @return the internal top bound of the object according to its children. */ getInnerAreaMinZ(): number { if (this._innerArea) { return this._innerArea.min[2]; } if (this._isUntransformedHitBoxesDirty) { this._updateUntransformedHitBoxes(); } return this._minZ; } /** * @return the internal bottom bound of the object according to its children. */ getInnerAreaMaxZ(): number { if (this._innerArea) { return this._innerArea.max[2]; } if (this._isUntransformedHitBoxesDirty) { this._updateUntransformedHitBoxes(); } return this._maxZ; } /** * @return the internal width of the object according to its children. */ getUnscaledDepth(): float { if (this._innerArea) { return this._innerArea.max[2] - this._innerArea.min[2]; } if (this._isUntransformedHitBoxesDirty) { this._updateUntransformedHitBoxes(); } return this._maxZ - this._minZ; } getOriginalDepth(): float { return this._instanceContainer._getInitialInnerAreaDepth(); } override _updateUntransformedHitBoxes(): void { super._updateUntransformedHitBoxes(); let minZ = Number.MAX_VALUE; let maxZ = -Number.MAX_VALUE; for (const childInstance of this._instanceContainer.getAdhocListOfAllInstances()) { if (!childInstance.isIncludedInParentCollisionMask()) { continue; } if (!gdjs.Base3DHandler.is3D(childInstance)) { continue; } minZ = Math.min(minZ, childInstance.getUnrotatedAABBMinZ()); maxZ = Math.max(maxZ, childInstance.getUnrotatedAABBMaxZ()); } if (minZ === Number.MAX_VALUE) { // The unscaled size can't be 0 because setWidth and setHeight wouldn't // have any effect. minZ = 0; maxZ = 1; } this._minZ = minZ; this._maxZ = maxZ; } /** * @returns the center Z from the local origin (0;0). */ getUnscaledCenterZ(): float { if (this.hasCustomRotationCenter()) { return this._customCenterZ; } return this.getUnscaledDepth() / 2 + this.getUnscaledMinZ(); } /** * The center of rotation is defined relatively to the origin (the object * position). * This avoids the center to move when children push the bounds. * * When no custom center is defined, it will move * to stay at the center of the children bounds. * * @param x coordinate of the custom center * @param y coordinate of the custom center */ setRotationCenter3D(x: float, y: float, z: float) { this._customCenterZ = z; this.setRotationCenter(x, y); } /** * Get the object size on the Z axis (called "depth"). */ getDepth(): float { return this.getUnscaledDepth() * this.getScaleZ(); } /** * Set the object size on the Z axis (called "depth"). */ setDepth(depth: float): void { const unscaledDepth = this.getUnscaledDepth(); if (unscaledDepth === 0) { return; } const scaleZ = depth / unscaledDepth; if (this._innerArea && this._isInnerAreaFollowingParentSize) { this._innerArea.min[2] *= scaleZ; this._innerArea.max[2] *= scaleZ; } else { this.setScaleZ(scaleZ); } } /** * Change the scale on X, Y and Z axis of the object. * * @param newScale The new scale (must be greater than 0). */ setScale(newScale: number): void { super.setScale(newScale); this.setScaleZ(newScale); } /** * Change the scale on Z axis of the object (changing its height). * * @param newScale The new scale (must be greater than 0). */ setScaleZ(newScale: number): void { if (this._innerArea && this._isInnerAreaFollowingParentSize) { // The scale is always 1; return; } if (newScale < 0) { newScale = 0; } if (newScale === Math.abs(this._scaleZ)) { return; } this._scaleZ = newScale * (this._flippedZ ? -1 : 1); this.getRenderer().updateSize(); } /** * Get the scale of the object (or the geometric average of X, Y and Z scale in case they are different). * * @return the scale of the object (or the geometric average of X, Y and Z scale in case they are different). */ getScale(): number { const scaleX = this.getScaleX(); const scaleY = this.getScaleY(); const scaleZ = this.getScaleZ(); return scaleX === scaleY && scaleX === scaleZ ? scaleX : Math.pow(scaleX * scaleY * scaleZ, 1 / 3); } /** * Get the scale of the object on Z axis. * * @return the scale of the object on Z axis */ getScaleZ(): float { return Math.abs(this._scaleZ); } flipZ(enable: boolean) { if (enable === this._flippedZ) { return; } this._flippedZ = enable; this.getRenderer().updateSize(); } isFlippedZ(): boolean { return this._flippedZ; } } }