@dcl/ecs 7.18.2-21450088960.commit-3c16004 → 7.18.2-21453292414.commit-1da934f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ export type { AudioSourceComponentDefinitionExtended } from './extended/AudioSou
3
3
  export type { AudioStreamComponentDefinitionExtended } from './extended/AudioStream';
4
4
  export type { MeshRendererComponentDefinitionExtended } from './extended/MeshRenderer';
5
5
  export type { MeshColliderComponentDefinitionExtended } from './extended/MeshCollider';
6
- export type { TextureHelper, MaterialComponentDefinitionExtended } from './extended/Material';
6
+ export type { TextureHelper, MaterialComponentDefinitionExtended, FlatTexture, ReadonlyFlatMaterial, ReadonlyFlatTexture, FlatMaterial } from './extended/Material';
7
7
  export type { TweenHelper, TweenComponentDefinitionExtended } from './extended/Tween';
8
8
  export type { CameraTransitionHelper, VirtualCameraComponentDefinitionExtended } from './extended/VirtualCamera';
9
9
  export type { TransformComponentExtended, TransformTypeWithOptionals } from './manual/Transform';
@@ -1,2 +1,3 @@
1
1
  export * from './coordinates';
2
2
  export * from './tree';
3
+ export { createTimers, Timers, TimerId, TimerCallback } from './timers';
@@ -1,2 +1,3 @@
1
1
  export * from './coordinates';
2
2
  export * from './tree';
3
+ export { createTimers } from './timers';
@@ -0,0 +1,85 @@
1
+ import { IEngine } from '../../engine/types';
2
+ export type TimerId = number;
3
+ export type TimerCallback = () => void;
4
+ export type Timers = {
5
+ /**
6
+ * Delays the execution of a function by a given amount of milliseconds.
7
+ *
8
+ * @param callback - The function to execute after the delay
9
+ * @param ms - The delay in milliseconds
10
+ * @returns A TimerId that can be used to cancel the timeout
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const timeoutId = timers.setTimeout(() => {
15
+ * console.log('1 second passed')
16
+ * }, 1000)
17
+ * ```
18
+ */
19
+ setTimeout(callback: TimerCallback, ms: number): TimerId;
20
+ /**
21
+ * Cancels a timeout previously established by setTimeout.
22
+ *
23
+ * @param timerId - The TimerId returned by setTimeout
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const timeoutId = timers.setTimeout(() => {
28
+ * console.log('This will not run')
29
+ * }, 1000)
30
+ *
31
+ * timers.clearTimeout(timeoutId)
32
+ * ```
33
+ */
34
+ clearTimeout(timerId: TimerId): void;
35
+ /**
36
+ * Repeatedly executes a function at specified intervals.
37
+ *
38
+ * @param callback - The function to execute at each interval
39
+ * @param ms - The interval in milliseconds
40
+ * @returns A TimerId that can be used to cancel the interval
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const intervalId = timers.setInterval(() => {
45
+ * console.log('Printing this every 1 second')
46
+ * }, 1000)
47
+ * ```
48
+ */
49
+ setInterval(callback: TimerCallback, ms: number): TimerId;
50
+ /**
51
+ * Cancels an interval previously established by setInterval.
52
+ *
53
+ * @param timerId - The TimerId returned by setInterval
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const intervalId = timers.setInterval(() => {
58
+ * console.log('This will stop')
59
+ * }, 1000)
60
+ *
61
+ * timers.clearInterval(intervalId)
62
+ * ```
63
+ */
64
+ clearInterval(timerId: TimerId): void;
65
+ };
66
+ /**
67
+ * Creates a timer system bound to a specific engine instance.
68
+ *
69
+ * @param targetEngine - The engine instance to bind timers to
70
+ * @returns A Timers object with setTimeout, clearTimeout, setInterval, and clearInterval methods
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * import { Engine } from '@dcl/sdk/ecs'
75
+ * import { createTimers } from '@dcl/sdk/ecs'
76
+ *
77
+ * const engine = Engine()
78
+ * const timers = createTimers(engine)
79
+ *
80
+ * timers.setTimeout(() => console.log('done'), 1000)
81
+ * ```
82
+ *
83
+ * @public
84
+ */
85
+ export declare function createTimers(targetEngine: IEngine): Timers;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Creates a timer system bound to a specific engine instance.
3
+ *
4
+ * @param targetEngine - The engine instance to bind timers to
5
+ * @returns A Timers object with setTimeout, clearTimeout, setInterval, and clearInterval methods
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { Engine } from '@dcl/sdk/ecs'
10
+ * import { createTimers } from '@dcl/sdk/ecs'
11
+ *
12
+ * const engine = Engine()
13
+ * const timers = createTimers(engine)
14
+ *
15
+ * timers.setTimeout(() => console.log('done'), 1000)
16
+ * ```
17
+ *
18
+ * @public
19
+ */
20
+ export function createTimers(targetEngine) {
21
+ const timers = new Map();
22
+ let timerIdCounter = 0;
23
+ function system(dt) {
24
+ for (const [timerId, timerData] of timers) {
25
+ timerData.accumulatedTime += 1000 * dt;
26
+ if (timerData.accumulatedTime < timerData.interval) {
27
+ continue;
28
+ }
29
+ if (timerData.recurrent) {
30
+ // For intervals, subtract full interval periods to handle accumulated time
31
+ const fullIntervals = Math.floor(timerData.accumulatedTime / timerData.interval);
32
+ timerData.accumulatedTime -= fullIntervals * timerData.interval;
33
+ }
34
+ else {
35
+ timers.delete(timerId);
36
+ }
37
+ timerData.callback();
38
+ }
39
+ }
40
+ targetEngine.addSystem(system, Number.MAX_SAFE_INTEGER, '@dcl/ecs/timers');
41
+ return {
42
+ setTimeout(callback, ms) {
43
+ const timerId = timerIdCounter++;
44
+ timers.set(timerId, {
45
+ callback,
46
+ interval: ms,
47
+ recurrent: false,
48
+ accumulatedTime: 0
49
+ });
50
+ return timerId;
51
+ },
52
+ clearTimeout(timerId) {
53
+ timers.delete(timerId);
54
+ },
55
+ setInterval(callback, ms) {
56
+ const timerId = timerIdCounter++;
57
+ timers.set(timerId, {
58
+ callback,
59
+ interval: ms,
60
+ recurrent: true,
61
+ accumulatedTime: 0
62
+ });
63
+ return timerId;
64
+ },
65
+ clearInterval(timerId) {
66
+ timers.delete(timerId);
67
+ }
68
+ };
69
+ }
@@ -1,5 +1,7 @@
1
1
  import { Entity } from '../../engine/entity';
2
2
  import { ComponentDefinition, IEngine } from '../../engine';
3
+ import { Vector3Type } from '../../schemas/custom/Vector3';
4
+ import { QuaternionType } from '../../schemas/custom/Quaternion';
3
5
  /**
4
6
  * Get an iterator of entities that follow a tree structure for a component
5
7
  * @public
@@ -30,3 +32,62 @@ export declare function getComponentEntityTree<T>(engine: Pick<IEngine, 'getEnti
30
32
  * @public
31
33
  */
32
34
  export declare function removeEntityWithChildren(engine: Pick<IEngine, 'getEntitiesWith' | 'defineComponentFromSchema' | 'removeEntity' | 'defineComponent'>, entity: Entity): void;
35
+ /**
36
+ * Get all entities that have the given entity as their parent
37
+ * @public
38
+ * @param engine - the engine running the entities
39
+ * @param parent - the parent entity to find children for
40
+ * @returns An array of entities that have the given parent
41
+ *
42
+ * Example:
43
+ * ```ts
44
+ * const children = getEntitiesWithParent(engine, myEntity)
45
+ * for (const child of children) {
46
+ * // process each child entity
47
+ * }
48
+ * ```
49
+ */
50
+ export declare function getEntitiesWithParent(engine: Pick<IEngine, 'getEntitiesWith' | 'defineComponentFromSchema'>, parent: Entity): Entity[];
51
+ /** @public Engine type for world transform functions */
52
+ export type WorldTransformEngine = Pick<IEngine, 'getEntitiesWith' | 'defineComponentFromSchema' | 'PlayerEntity'>;
53
+ /**
54
+ * Get the world position of an entity, taking into account the full parent hierarchy.
55
+ * This computes the world-space position by accumulating all parent transforms
56
+ * (position, rotation, and scale).
57
+ *
58
+ * When the entity has AvatarAttach and Transform, the renderer updates the Transform
59
+ * with avatar-relative values (including the exact anchor point offset for hand, head, etc.).
60
+ * This function combines the player's transform with those values to compute the world position.
61
+ *
62
+ * @public
63
+ * @param engine - the engine running the entities
64
+ * @param entity - the entity to get the world position for
65
+ * @returns The entity's position in world space. Returns `{x: 0, y: 0, z: 0}` if the entity has no Transform.
66
+ *
67
+ * Example:
68
+ * ```ts
69
+ * const worldPos = getWorldPosition(engine, childEntity)
70
+ * console.log(`World position: ${worldPos.x}, ${worldPos.y}, ${worldPos.z}`)
71
+ * ```
72
+ */
73
+ export declare function getWorldPosition(engine: WorldTransformEngine, entity: Entity): Vector3Type;
74
+ /**
75
+ * Get the world rotation of an entity, taking into account the full parent hierarchy.
76
+ * This computes the world-space rotation by combining all parent rotations.
77
+ *
78
+ * When the entity has AvatarAttach and Transform, the renderer updates the Transform
79
+ * with avatar-relative values (including the exact anchor point rotation for hand, head, etc.).
80
+ * This function combines the player's rotation with those values to compute the world rotation.
81
+ *
82
+ * @public
83
+ * @param engine - the engine running the entities
84
+ * @param entity - the entity to get the world rotation for
85
+ * @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.
86
+ *
87
+ * Example:
88
+ * ```ts
89
+ * const worldRot = getWorldRotation(engine, childEntity)
90
+ * console.log(`World rotation: ${worldRot.x}, ${worldRot.y}, ${worldRot.z}, ${worldRot.w}`)
91
+ * ```
92
+ */
93
+ export declare function getWorldRotation(engine: WorldTransformEngine, entity: Entity): QuaternionType;
@@ -1,4 +1,161 @@
1
1
  import * as components from '../../components';
2
+ /**
3
+ * @internal
4
+ * Add two Vector3 values
5
+ */
6
+ function addVectors(v1, v2) {
7
+ return {
8
+ x: v1.x + v2.x,
9
+ y: v1.y + v2.y,
10
+ z: v1.z + v2.z
11
+ };
12
+ }
13
+ /**
14
+ * @internal
15
+ * Multiply two Vector3 values element-wise (used for scaling)
16
+ */
17
+ function multiplyVectors(v1, v2) {
18
+ return {
19
+ x: v1.x * v2.x,
20
+ y: v1.y * v2.y,
21
+ z: v1.z * v2.z
22
+ };
23
+ }
24
+ /**
25
+ * @internal
26
+ * Multiply two quaternions (combines rotations)
27
+ * Result represents applying q1 first, then q2
28
+ */
29
+ function multiplyQuaternions(q1, q2) {
30
+ return {
31
+ x: q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y,
32
+ y: q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x,
33
+ z: q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w,
34
+ w: q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z
35
+ };
36
+ }
37
+ /**
38
+ * @internal
39
+ * Rotate a vector by a quaternion
40
+ * Uses the formula: v' = q * v * q^(-1), optimized version
41
+ */
42
+ function rotateVectorByQuaternion(v, q) {
43
+ // Extract quaternion components
44
+ const qx = q.x, qy = q.y, qz = q.z, qw = q.w;
45
+ // Calculate cross product terms (q.xyz × v) * 2
46
+ const ix = qw * v.x + qy * v.z - qz * v.y;
47
+ const iy = qw * v.y + qz * v.x - qx * v.z;
48
+ const iz = qw * v.z + qx * v.y - qy * v.x;
49
+ const iw = -qx * v.x - qy * v.y - qz * v.z;
50
+ // Calculate final rotated vector
51
+ return {
52
+ x: ix * qw + iw * -qx + iy * -qz - iz * -qy,
53
+ y: iy * qw + iw * -qy + iz * -qx - ix * -qz,
54
+ z: iz * qw + iw * -qz + ix * -qy - iy * -qx
55
+ };
56
+ }
57
+ /** @internal Identity transform values */
58
+ const IDENTITY_POSITION = { x: 0, y: 0, z: 0 };
59
+ const IDENTITY_ROTATION = { x: 0, y: 0, z: 0, w: 1 };
60
+ const IDENTITY_SCALE = { x: 1, y: 1, z: 1 };
61
+ /**
62
+ * @internal
63
+ * Computes the world transform for an entity with AvatarAttach.
64
+ * If the entity has a Transform, the avatar-relative values (set by the renderer)
65
+ * are combined with the player's transform. Otherwise, returns the player's transform
66
+ * with identity scale.
67
+ */
68
+ function computeAvatarAttachedWorldTransform(playerTransform, entityTransform) {
69
+ if (!entityTransform) {
70
+ return {
71
+ position: { ...playerTransform.position },
72
+ rotation: { ...playerTransform.rotation },
73
+ scale: { ...IDENTITY_SCALE }
74
+ };
75
+ }
76
+ const rotatedPosition = rotateVectorByQuaternion(entityTransform.position, playerTransform.rotation);
77
+ return {
78
+ position: addVectors(playerTransform.position, rotatedPosition),
79
+ rotation: multiplyQuaternions(playerTransform.rotation, entityTransform.rotation),
80
+ scale: entityTransform.scale
81
+ };
82
+ }
83
+ /**
84
+ * @internal
85
+ * Finds the transform of a player by their avatar ID.
86
+ * Returns the local player's transform if avatarId is undefined,
87
+ * or searches for a remote player by matching their address.
88
+ */
89
+ function findPlayerTransform(Transform, PlayerIdentityData, localPlayerEntity, avatarId) {
90
+ // Local player (avatarId undefined)
91
+ if (avatarId === undefined) {
92
+ return Transform.getOrNull(localPlayerEntity);
93
+ }
94
+ // Remote player - find their entity by matching address
95
+ if (!PlayerIdentityData) {
96
+ return null;
97
+ }
98
+ for (const [playerEntity, identityData] of PlayerIdentityData.iterator()) {
99
+ if (identityData.address === avatarId) {
100
+ return Transform.getOrNull(playerEntity);
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * @internal
107
+ * Computes world position, rotation, and scale in a single hierarchy traversal.
108
+ * This is O(n) where n is the depth of the hierarchy.
109
+ *
110
+ * When an entity has AvatarAttach and Transform, the renderer updates the Transform
111
+ * with avatar-relative values (including the exact anchor point offset). This function
112
+ * combines the player's transform with the entity's avatar-relative transform to
113
+ * compute the world-space position.
114
+ *
115
+ * @throws Error if a circular dependency is detected in the entity hierarchy
116
+ */
117
+ function getWorldTransformInternal(Transform, AvatarAttach, PlayerIdentityData, PlayerEntity, entity, visited = new Set()) {
118
+ const transform = Transform.getOrNull(entity);
119
+ const avatarAttach = AvatarAttach?.getOrNull(entity);
120
+ // Handle AvatarAttach: combine player's transform with the entity's avatar-relative transform
121
+ // (which the renderer updates with the exact anchor point offset for hand, head, etc.)
122
+ if (avatarAttach) {
123
+ const playerTransform = findPlayerTransform(Transform, PlayerIdentityData, PlayerEntity, avatarAttach.avatarId);
124
+ if (playerTransform) {
125
+ return computeAvatarAttachedWorldTransform(playerTransform, transform);
126
+ }
127
+ // Player's Transform not available, fall through to normal Transform handling
128
+ }
129
+ if (!transform) {
130
+ return {
131
+ position: { ...IDENTITY_POSITION },
132
+ rotation: { ...IDENTITY_ROTATION },
133
+ scale: { ...IDENTITY_SCALE }
134
+ };
135
+ }
136
+ if (!transform.parent) {
137
+ return {
138
+ position: { ...transform.position },
139
+ rotation: { ...transform.rotation },
140
+ scale: { ...transform.scale }
141
+ };
142
+ }
143
+ visited.add(entity);
144
+ if (visited.has(transform.parent)) {
145
+ throw new Error(`Circular dependency detected in entity hierarchy: entity ${entity} has ancestor ${transform.parent} which creates a cycle`);
146
+ }
147
+ const parentWorld = getWorldTransformInternal(Transform, AvatarAttach, PlayerIdentityData, PlayerEntity, transform.parent, visited);
148
+ const worldScale = multiplyVectors(parentWorld.scale, transform.scale);
149
+ const worldRotation = multiplyQuaternions(parentWorld.rotation, transform.rotation);
150
+ const scaledPosition = multiplyVectors(transform.position, parentWorld.scale);
151
+ const rotatedPosition = rotateVectorByQuaternion(scaledPosition, parentWorld.rotation);
152
+ const worldPosition = addVectors(parentWorld.position, rotatedPosition);
153
+ return {
154
+ position: worldPosition,
155
+ rotation: worldRotation,
156
+ scale: worldScale
157
+ };
158
+ }
2
159
  function* genEntityTree(entity, entities) {
3
160
  // This avoid infinite loop when there is a cyclic parenting
4
161
  if (!entities.has(entity))
@@ -70,3 +227,84 @@ export function removeEntityWithChildren(engine, entity) {
70
227
  engine.removeEntity(ent);
71
228
  }
72
229
  }
230
+ /**
231
+ * Get all entities that have the given entity as their parent
232
+ * @public
233
+ * @param engine - the engine running the entities
234
+ * @param parent - the parent entity to find children for
235
+ * @returns An array of entities that have the given parent
236
+ *
237
+ * Example:
238
+ * ```ts
239
+ * const children = getEntitiesWithParent(engine, myEntity)
240
+ * for (const child of children) {
241
+ * // process each child entity
242
+ * }
243
+ * ```
244
+ */
245
+ export function getEntitiesWithParent(engine, parent) {
246
+ const Transform = components.Transform(engine);
247
+ const entitiesWithParent = [];
248
+ for (const [entity, transform] of engine.getEntitiesWith(Transform)) {
249
+ if (transform.parent === parent) {
250
+ entitiesWithParent.push(entity);
251
+ }
252
+ }
253
+ return entitiesWithParent;
254
+ }
255
+ /**
256
+ * @internal
257
+ * Computes the world transform for an entity using the provided engine.
258
+ * This is a convenience wrapper that initializes the required components.
259
+ */
260
+ function getWorldTransform(engine, entity) {
261
+ const Transform = components.Transform(engine);
262
+ const AvatarAttach = components.AvatarAttach(engine);
263
+ const PlayerIdentityData = components.PlayerIdentityData(engine);
264
+ return getWorldTransformInternal(Transform, AvatarAttach, PlayerIdentityData, engine.PlayerEntity, entity);
265
+ }
266
+ /**
267
+ * Get the world position of an entity, taking into account the full parent hierarchy.
268
+ * This computes the world-space position by accumulating all parent transforms
269
+ * (position, rotation, and scale).
270
+ *
271
+ * When the entity has AvatarAttach and Transform, the renderer updates the Transform
272
+ * with avatar-relative values (including the exact anchor point offset for hand, head, etc.).
273
+ * This function combines the player's transform with those values to compute the world position.
274
+ *
275
+ * @public
276
+ * @param engine - the engine running the entities
277
+ * @param entity - the entity to get the world position for
278
+ * @returns The entity's position in world space. Returns `{x: 0, y: 0, z: 0}` if the entity has no Transform.
279
+ *
280
+ * Example:
281
+ * ```ts
282
+ * const worldPos = getWorldPosition(engine, childEntity)
283
+ * console.log(`World position: ${worldPos.x}, ${worldPos.y}, ${worldPos.z}`)
284
+ * ```
285
+ */
286
+ export function getWorldPosition(engine, entity) {
287
+ return getWorldTransform(engine, entity).position;
288
+ }
289
+ /**
290
+ * Get the world rotation of an entity, taking into account the full parent hierarchy.
291
+ * This computes the world-space rotation by combining all parent rotations.
292
+ *
293
+ * When the entity has AvatarAttach and Transform, the renderer updates the Transform
294
+ * with avatar-relative values (including the exact anchor point rotation for hand, head, etc.).
295
+ * This function combines the player's rotation with those values to compute the world rotation.
296
+ *
297
+ * @public
298
+ * @param engine - the engine running the entities
299
+ * @param entity - the entity to get the world rotation for
300
+ * @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.
301
+ *
302
+ * Example:
303
+ * ```ts
304
+ * const worldRot = getWorldRotation(engine, childEntity)
305
+ * console.log(`World rotation: ${worldRot.x}, ${worldRot.y}, ${worldRot.z}, ${worldRot.w}`)
306
+ * ```
307
+ */
308
+ export function getWorldRotation(engine, entity) {
309
+ return getWorldTransform(engine, entity).rotation;
310
+ }
@@ -10,6 +10,7 @@ import { RaycastSystem } from '../../systems/raycast';
10
10
  import { VideoEventsSystem } from '../../systems/videoEvents';
11
11
  import { TweenSystem } from '../../systems/tween';
12
12
  import { TriggerAreaEventsSystem } from '../../systems/triggerArea';
13
+ import { createTimers, Timers } from '../helpers/timers';
13
14
  /**
14
15
  * @public
15
16
  * The engine is the part of the scene that sits in the middle and manages all of the other parts.
@@ -63,6 +64,12 @@ export { TweenSystem };
63
64
  */
64
65
  export declare const triggerAreaEventsSystem: TriggerAreaEventsSystem;
65
66
  export { TriggerAreaEventsSystem };
67
+ /**
68
+ * @public
69
+ * Timer utilities for delayed and repeated execution.
70
+ */
71
+ export declare const timers: Timers;
72
+ export { Timers, createTimers };
66
73
  /**
67
74
  * @public
68
75
  * Runs an async function
@@ -11,6 +11,7 @@ import { createVideoEventsSystem } from '../../systems/videoEvents';
11
11
  import { createTweenSystem } from '../../systems/tween';
12
12
  import { pointerEventColliderChecker } from '../../systems/pointer-event-collider-checker';
13
13
  import { createTriggerAreaEventsSystem } from '../../systems/triggerArea';
14
+ import { createTimers } from '../helpers/timers';
14
15
  /**
15
16
  * @public
16
17
  * The engine is the part of the scene that sits in the middle and manages all of the other parts.
@@ -58,6 +59,16 @@ export const tweenSystem = createTweenSystem(engine);
58
59
  * Register callback functions for trigger area results.
59
60
  */
60
61
  export const triggerAreaEventsSystem = /* @__PURE__ */ createTriggerAreaEventsSystem(engine);
62
+ /**
63
+ * @public
64
+ * Timer utilities for delayed and repeated execution.
65
+ */
66
+ export const timers = /* @__PURE__ */ createTimers(engine);
67
+ export { createTimers };
68
+ globalThis.setTimeout = globalThis.setTimeout ?? timers.setTimeout;
69
+ globalThis.clearTimeout = globalThis.clearTimeout ?? timers.clearTimeout;
70
+ globalThis.setInterval = globalThis.setInterval ?? timers.setInterval;
71
+ globalThis.clearInterval = globalThis.clearInterval ?? timers.clearInterval;
61
72
  /**
62
73
  * Adds pointer event collider system only in DEV env
63
74
  */