UNPKG

@dcl/ecs

Version:
311 lines (310 loc) 11.8 kB
import * as components from '../../components'; /** * @internal * Add two Vector3 values */ function addVectors(v1, v2) { return { x: v1.x + v2.x, y: v1.y + v2.y, z: v1.z + v2.z }; } /** * @internal * Multiply two Vector3 values element-wise (used for scaling) */ function multiplyVectors(v1, v2) { return { x: v1.x * v2.x, y: v1.y * v2.y, z: v1.z * v2.z }; } /** * @internal * Multiply two quaternions (combines rotations) * Result represents applying q1 first, then q2 */ function multiplyQuaternions(q1, q2) { return { x: q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, y: q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, z: q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, w: q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z }; } /** * @internal * Rotate a vector by a quaternion * Uses the formula: v' = q * v * q^(-1), optimized version */ function rotateVectorByQuaternion(v, q) { // Extract quaternion components const qx = q.x, qy = q.y, qz = q.z, qw = q.w; // Calculate cross product terms (q.xyz × v) * 2 const ix = qw * v.x + qy * v.z - qz * v.y; const iy = qw * v.y + qz * v.x - qx * v.z; const iz = qw * v.z + qx * v.y - qy * v.x; const iw = -qx * v.x - qy * v.y - qz * v.z; // Calculate final rotated vector return { x: ix * qw + iw * -qx + iy * -qz - iz * -qy, y: iy * qw + iw * -qy + iz * -qx - ix * -qz, z: iz * qw + iw * -qz + ix * -qy - iy * -qx }; } /** @internal Identity transform values */ const IDENTITY_POSITION = { x: 0, y: 0, z: 0 }; const IDENTITY_ROTATION = { x: 0, y: 0, z: 0, w: 1 }; const IDENTITY_SCALE = { x: 1, y: 1, z: 1 }; /** * @internal * Computes the world transform for an entity with AvatarAttach. * If the entity has a Transform, the avatar-relative values (set by the renderer) * are combined with the player's transform. Otherwise, returns the player's transform * with identity scale. */ function computeAvatarAttachedWorldTransform(playerTransform, entityTransform) { if (!entityTransform) { return { position: { ...playerTransform.position }, rotation: { ...playerTransform.rotation }, scale: { ...IDENTITY_SCALE } }; } const rotatedPosition = rotateVectorByQuaternion(entityTransform.position, playerTransform.rotation); return { position: addVectors(playerTransform.position, rotatedPosition), rotation: multiplyQuaternions(playerTransform.rotation, entityTransform.rotation), scale: entityTransform.scale }; } /** * @internal * Finds the transform of a player by their avatar ID. * Returns the local player's transform if avatarId is undefined, * or searches for a remote player by matching their address. */ function findPlayerTransform(Transform, PlayerIdentityData, localPlayerEntity, avatarId) { // Local player (avatarId undefined) if (avatarId === undefined) { return Transform.getOrNull(localPlayerEntity); } // Remote player - find their entity by matching address if (!PlayerIdentityData) { return null; } for (const [playerEntity, identityData] of PlayerIdentityData.iterator()) { if (identityData.address === avatarId) { return Transform.getOrNull(playerEntity); } } return null; } /** * @internal * Computes world position, rotation, and scale in a single hierarchy traversal. * This is O(n) where n is the depth of the hierarchy. * * When an entity has AvatarAttach and Transform, the renderer updates the Transform * with avatar-relative values (including the exact anchor point offset). This function * combines the player's transform with the entity's avatar-relative transform to * compute the world-space position. * * @throws Error if a circular dependency is detected in the entity hierarchy */ function getWorldTransformInternal(Transform, AvatarAttach, PlayerIdentityData, PlayerEntity, entity, visited = new Set()) { const transform = Transform.getOrNull(entity); const avatarAttach = AvatarAttach?.getOrNull(entity); // Handle AvatarAttach: combine player's transform with the entity's avatar-relative transform // (which the renderer updates with the exact anchor point offset for hand, head, etc.) if (avatarAttach) { const playerTransform = findPlayerTransform(Transform, PlayerIdentityData, PlayerEntity, avatarAttach.avatarId); if (playerTransform) { return computeAvatarAttachedWorldTransform(playerTransform, transform); } // Player's Transform not available, fall through to normal Transform handling } if (!transform) { return { position: { ...IDENTITY_POSITION }, rotation: { ...IDENTITY_ROTATION }, scale: { ...IDENTITY_SCALE } }; } if (!transform.parent) { return { position: { ...transform.position }, rotation: { ...transform.rotation }, scale: { ...transform.scale } }; } visited.add(entity); if (visited.has(transform.parent)) { throw new Error(`Circular dependency detected in entity hierarchy: entity ${entity} has ancestor ${transform.parent} which creates a cycle`); } const parentWorld = getWorldTransformInternal(Transform, AvatarAttach, PlayerIdentityData, PlayerEntity, transform.parent, visited); const worldScale = multiplyVectors(parentWorld.scale, transform.scale); const worldRotation = multiplyQuaternions(parentWorld.rotation, transform.rotation); const scaledPosition = multiplyVectors(transform.position, parentWorld.scale); const rotatedPosition = rotateVectorByQuaternion(scaledPosition, parentWorld.rotation); const worldPosition = addVectors(parentWorld.position, rotatedPosition); return { position: worldPosition, rotation: worldRotation, scale: worldScale }; } function* genEntityTree(entity, entities) { // This avoid infinite loop when there is a cyclic parenting if (!entities.has(entity)) return; entities.delete(entity); for (const [_entity, value] of entities) { if (value.parent === entity) { yield* genEntityTree(_entity, entities); } } yield entity; } /** * Get an iterator of entities that follow a tree structure for a component * @public * @param engine - the engine running the entities * @param entity - the root entity of the tree * @param component - the parenting component to filter by * @returns An iterator of an array as [entity, entity2, ...] * * Example: * ```ts * const TreeComponent = engine.defineComponent('custom::TreeComponent', { * label: Schemas.String, * parent: Schemas.Entity * }) * * for (const entity of getComponentEntityTree(engine, entity, TreeComponent)) { * // entity in the tree * } * ``` */ export function getComponentEntityTree(engine, entity, component) { const entities = new Map(engine.getEntitiesWith(component)); return genEntityTree(entity, entities); } // I swear by all the gods that this is being tested on test/sdk/network/sync-engines.spec.ts /* istanbul ignore next */ function removeNetworkEntityChildrens(engine, parent) { const NetworkParent = components.NetworkParent(engine); const NetworkEntity = components.NetworkEntity(engine); // Remove parent engine.removeEntity(parent); // Remove childs const network = NetworkEntity.getOrNull(parent); if (network) { for (const [entity, parent] of engine.getEntitiesWith(NetworkParent)) { if (parent.entityId === network.entityId && parent.networkId === network.networkId) { removeNetworkEntityChildrens(engine, entity); } } } return; } /** * Remove all components of each entity in the tree made with Transform parenting * @param engine - the engine running the entities * @param firstEntity - the root entity of the tree * @public */ export function removeEntityWithChildren(engine, entity) { const Transform = components.Transform(engine); const NetworkEntity = components.NetworkEntity(engine); /* istanbul ignore if */ if (NetworkEntity.has(entity)) { return removeNetworkEntityChildrens(engine, entity); } for (const ent of getComponentEntityTree(engine, entity, Transform)) { engine.removeEntity(ent); } } /** * Get all entities that have the given entity as their parent * @public * @param engine - the engine running the entities * @param parent - the parent entity to find children for * @returns An array of entities that have the given parent * * Example: * ```ts * const children = getEntitiesWithParent(engine, myEntity) * for (const child of children) { * // process each child entity * } * ``` */ export function getEntitiesWithParent(engine, parent) { const Transform = components.Transform(engine); const entitiesWithParent = []; for (const [entity, transform] of engine.getEntitiesWith(Transform)) { if (transform.parent === parent) { entitiesWithParent.push(entity); } } return entitiesWithParent; } /** * @internal * Computes the world transform for an entity using the provided engine. * This is a convenience wrapper that initializes the required components. */ function getWorldTransform(engine, entity) { const Transform = components.Transform(engine); const AvatarAttach = components.AvatarAttach(engine); const PlayerIdentityData = components.PlayerIdentityData(engine); return getWorldTransformInternal(Transform, AvatarAttach, PlayerIdentityData, engine.PlayerEntity, entity); } /** * Get the world position of an entity, taking into account the full parent hierarchy. * This computes the world-space position by accumulating all parent transforms * (position, rotation, and scale). * * When the entity has AvatarAttach and Transform, the renderer updates the Transform * with avatar-relative values (including the exact anchor point offset for hand, head, etc.). * This function combines the player's transform with those values to compute the world position. * * @public * @param engine - the engine running the entities * @param entity - the entity to get the world position for * @returns The entity's position in world space. Returns `{x: 0, y: 0, z: 0}` if the entity has no Transform. * * Example: * ```ts * const worldPos = getWorldPosition(engine, childEntity) * console.log(`World position: ${worldPos.x}, ${worldPos.y}, ${worldPos.z}`) * ``` */ export function getWorldPosition(engine, entity) { return getWorldTransform(engine, entity).position; } /** * Get the world rotation of an entity, taking into account the full parent hierarchy. * This computes the world-space rotation by combining all parent rotations. * * When the entity has AvatarAttach and Transform, the renderer updates the Transform * with avatar-relative values (including the exact anchor point rotation for hand, head, etc.). * This function combines the player's rotation with those values to compute the world rotation. * * @public * @param engine - the engine running the entities * @param entity - the entity to get the world rotation for * @returns The entity's rotation in world space as a quaternion. Returns identity quaternion `{x: 0, y: 0, z: 0, w: 1}` if the entity has no Transform. * * Example: * ```ts * const worldRot = getWorldRotation(engine, childEntity) * console.log(`World rotation: ${worldRot.x}, ${worldRot.y}, ${worldRot.z}, ${worldRot.w}`) * ``` */ export function getWorldRotation(engine, entity) { return getWorldTransform(engine, entity).rotation; }