@galfus/engine 0.0.0-stage → 0.31.1-alpha

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.
Files changed (80) hide show
  1. package/README.md +190 -2
  2. package/package.json +59 -4
  3. package/src/anchors/cache.ts +101 -0
  4. package/src/anchors/component.ts +123 -0
  5. package/src/anchors/hierarchy.ts +192 -0
  6. package/src/anchors/index.ts +8 -0
  7. package/src/anchors/solver.ts +247 -0
  8. package/src/anchors/strategy.ts +142 -0
  9. package/src/animation/blending.ts +163 -0
  10. package/src/animation/clip.ts +132 -0
  11. package/src/animation/coordinator.ts +568 -0
  12. package/src/animation/index.ts +7 -0
  13. package/src/animation/native-targets.ts +37 -0
  14. package/src/animation/player.ts +287 -0
  15. package/src/audio/index.ts +4 -0
  16. package/src/audio/manager.ts +371 -0
  17. package/src/collision/components.ts +211 -0
  18. package/src/collision/index.ts +16 -0
  19. package/src/collision/query2d.ts +167 -0
  20. package/src/collision/query3d.ts +170 -0
  21. package/src/ecs/component.ts +130 -0
  22. package/src/ecs/entity.ts +75 -0
  23. package/src/ecs/index.ts +9 -0
  24. package/src/ecs/intent.ts +162 -0
  25. package/src/ecs/metadata.ts +44 -0
  26. package/src/ecs/system.ts +224 -0
  27. package/src/ecs/world.ts +291 -0
  28. package/src/index.ts +19 -0
  29. package/src/interaction/focus.ts +327 -0
  30. package/src/interaction/index.ts +9 -0
  31. package/src/interaction/listener.ts +58 -0
  32. package/src/interaction/snapshot2d.ts +210 -0
  33. package/src/interaction/snapshot3d.ts +214 -0
  34. package/src/interaction/system.ts +544 -0
  35. package/src/interaction/types.ts +27 -0
  36. package/src/math/common.ts +67 -0
  37. package/src/math/index.ts +17 -0
  38. package/src/math/mat2d.ts +320 -0
  39. package/src/math/mat4.ts +576 -0
  40. package/src/math/quat.ts +438 -0
  41. package/src/math/vec2.ts +161 -0
  42. package/src/math/vec3.ts +242 -0
  43. package/src/math/vec4.ts +171 -0
  44. package/src/multiverse/coordinator.ts +540 -0
  45. package/src/multiverse/index.ts +13 -0
  46. package/src/multiverse/presentation-graph.ts +306 -0
  47. package/src/multiverse/presentation-links.ts +134 -0
  48. package/src/multiverse/realm.ts +49 -0
  49. package/src/platform/index.ts +2 -0
  50. package/src/platform/platform.ts +149 -0
  51. package/src/platform/window.ts +181 -0
  52. package/src/realm2d/components.ts +254 -0
  53. package/src/realm2d/index.ts +12 -0
  54. package/src/realm2d/presets/index.ts +36 -0
  55. package/src/realm2d/realm.ts +522 -0
  56. package/src/realm2d/sync.ts +284 -0
  57. package/src/realm2d/utils/index.ts +32 -0
  58. package/src/realm3d/components.ts +341 -0
  59. package/src/realm3d/geometry.ts +224 -0
  60. package/src/realm3d/index.ts +12 -0
  61. package/src/realm3d/presets/index.ts +67 -0
  62. package/src/realm3d/realm.ts +588 -0
  63. package/src/realm3d/resource-ids.ts +62 -0
  64. package/src/realm3d/skinning.ts +128 -0
  65. package/src/realm3d/sync.ts +442 -0
  66. package/src/realm3d/utils/index.ts +26 -0
  67. package/src/resources/index.ts +4 -0
  68. package/src/resources/manager.ts +92 -0
  69. package/src/runtime.ts +588 -0
  70. package/src/spatial/dirty_registry.ts +217 -0
  71. package/src/spatial/flags.ts +14 -0
  72. package/src/spatial/index.ts +6 -0
  73. package/src/spatial/metrics.ts +124 -0
  74. package/src/text/editor.ts +224 -0
  75. package/src/text/encoding.ts +38 -0
  76. package/src/text/index.ts +13 -0
  77. package/src/text/input.ts +160 -0
  78. package/src/text/keyboard.ts +50 -0
  79. package/src/text/svg-decorations.ts +147 -0
  80. package/src/types.ts +85 -0
@@ -0,0 +1,210 @@
1
+ import type { SvgElementRect } from '@galfus/transport-types';
2
+ import {
3
+ type Collider2DComponent,
4
+ type ColliderEntry,
5
+ type ColliderShape2D,
6
+ getColliderEntries
7
+ } from '../collision/components';
8
+ import { containsColliderPoint2D } from '../collision/query2d';
9
+ import { mat2d, type Mat2d, type ReadonlyMat2d, vec2 } from '../math';
10
+ import type {
11
+ Camera2DComponent,
12
+ Model2DComponent,
13
+ Shape2DComponent,
14
+ Sprite2DComponent,
15
+ Svg2DComponent
16
+ } from '../realm2d/components';
17
+ import type { Realm2D } from '../realm2d/realm';
18
+ import type { InteractiveTarget } from './types';
19
+
20
+ interface ShapeSnapshot {
21
+ inverse: Mat2d;
22
+ entry?: ColliderEntry<ColliderShape2D>;
23
+ }
24
+ export interface CameraSnapshot2D {
25
+ id: number;
26
+ slot: number;
27
+ x: number;
28
+ y: number;
29
+ rotation: number;
30
+ scale: number;
31
+ near: number;
32
+ far: number;
33
+ }
34
+ /** Reused storage for the 2D state submitted by one successful dispatch. */
35
+ export class TargetSnapshot2D {
36
+ private readonly shapes: ShapeSnapshot[] = [];
37
+ private count = 0;
38
+ private readonly combined = mat2d.create();
39
+ private readonly previousModel = mat2d.create();
40
+ private hasModel = false;
41
+ private readonly svgInverse = mat2d.create();
42
+ private svgRect?: Readonly<SvgElementRect>;
43
+ private svgWidth = 0;
44
+ private svgHeight = 0;
45
+ private readonly point: [number, number] = [0, 0];
46
+ private readonly svgPosition = vec2.create();
47
+ private readonly svgScale = vec2.create();
48
+ zLayer = 0;
49
+ drawOrder = 0;
50
+ resourceId = 0;
51
+ valid = false;
52
+ exists = false;
53
+
54
+ capture(realm: Realm2D, entityId: number, target: InteractiveTarget): void {
55
+ const wasValid = this.valid;
56
+ this.valid = false;
57
+ this.exists = false;
58
+ this.count = 0;
59
+ const model = realm.world.getComponent<Model2DComponent>(entityId, 'Model2D');
60
+ if (!model) return;
61
+ let sameModel = this.hasModel;
62
+ for (let i = 0; i < 6; i++)
63
+ if (this.previousModel[i] !== model.worldMatrix[i]) sameModel = false;
64
+ mat2d.copy(this.previousModel, model.worldMatrix);
65
+ this.hasModel = true;
66
+ this.zLayer = model.zLayer;
67
+ this.drawOrder =
68
+ realm.world.getComponent<Svg2DComponent>(entityId, 'Svg2D')?.desc.drawOrder ??
69
+ realm.world.getComponent<Sprite2DComponent>(entityId, 'Sprite2D')?.desc.drawOrder ??
70
+ realm.world.getComponent<Shape2DComponent>(entityId, 'Shape2D')?.desc.drawOrder ??
71
+ model.drawOrder;
72
+ this.resourceId = model.modelId;
73
+ if (target.type === 'collider') {
74
+ const component = realm.world.getComponent<Collider2DComponent>(entityId, 'Collider2D');
75
+ if (!component) return;
76
+ for (const entry of getColliderEntries(component)) {
77
+ if (target.colliderId !== undefined && entry.colliderId !== target.colliderId) continue;
78
+ this.exists = true;
79
+ if (!entry.active) continue;
80
+ let saved = this.shapes[this.count];
81
+ if (!saved) {
82
+ saved = { inverse: mat2d.create() };
83
+ this.shapes.push(saved);
84
+ }
85
+ if (!sameModel || saved.entry !== entry) {
86
+ mat2d.multiply(
87
+ this.combined,
88
+ model.worldMatrix,
89
+ entry.matrix as unknown as ReadonlyMat2d
90
+ );
91
+ if (!mat2d.invert(saved.inverse, this.combined)) {
92
+ saved.entry = undefined;
93
+ continue;
94
+ }
95
+ saved.entry = entry;
96
+ }
97
+ this.count++;
98
+ }
99
+ this.valid = this.count > 0;
100
+ return;
101
+ }
102
+ const svg = realm.world.getComponent<Svg2DComponent>(entityId, 'Svg2D');
103
+ if (!svg || svg.svgId !== target.svgId) return;
104
+ const rect = realm.getSvgElementRect(svg.svgId, target.elementId);
105
+ if (!rect || !rect.viewportWidth || !rect.viewportHeight) return;
106
+ const width = svg.desc.dimensions?.[0] ?? rect.viewportWidth;
107
+ const height = svg.desc.dimensions?.[1] ?? rect.viewportHeight;
108
+ if (
109
+ wasValid &&
110
+ sameModel &&
111
+ rect === this.svgRect &&
112
+ width === this.svgWidth &&
113
+ height === this.svgHeight
114
+ ) {
115
+ this.valid = true;
116
+ return;
117
+ }
118
+ // Renderer decomposes the world transform into translation, rotation and signed scale.
119
+ mat2d.getTranslation(this.svgPosition, model.worldMatrix);
120
+ mat2d.getScaling(this.svgScale, model.worldMatrix);
121
+ mat2d.fromRotationTranslationScale(
122
+ this.combined,
123
+ this.svgPosition,
124
+ mat2d.getRotation(model.worldMatrix),
125
+ this.svgScale
126
+ );
127
+ if (!mat2d.invert(this.svgInverse, this.combined)) return;
128
+ this.svgRect = rect;
129
+ this.svgWidth = width;
130
+ this.svgHeight = height;
131
+ this.valid = this.svgWidth > 0 && this.svgHeight > 0;
132
+ }
133
+
134
+ contains(
135
+ worldPoint: [number, number],
136
+ target: InteractiveTarget,
137
+ outUv?: [number, number]
138
+ ): boolean {
139
+ if (!this.valid) return false;
140
+ if (target.type === 'collider') {
141
+ for (let i = 0; i < this.count; i++) {
142
+ const shape = this.shapes[i]!;
143
+ vec2.transformMat2d(this.point, worldPoint, shape.inverse);
144
+ if (shape.entry && containsColliderPoint2D(shape.entry.shape, this.point)) {
145
+ if (outUv && shape.entry.shape.type === 'rectangle') {
146
+ const hx = shape.entry.shape.halfExtents[0];
147
+ const hy = shape.entry.shape.halfExtents[1];
148
+ outUv[0] = Math.max(0, Math.min(1, (this.point[0] + hx) / (2 * hx)));
149
+ outUv[1] = Math.max(0, Math.min(1, (hy - this.point[1]) / (2 * hy)));
150
+ }
151
+ return true;
152
+ }
153
+ }
154
+ return false;
155
+ }
156
+ const rect = this.svgRect!;
157
+ vec2.transformMat2d(this.point, worldPoint, this.svgInverse);
158
+ const x = (this.point[0] / this.svgWidth + 0.5) * rect.viewportWidth;
159
+ const y = (0.5 - this.point[1] / this.svgHeight) * rect.viewportHeight;
160
+ const inside =
161
+ x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
162
+ if (inside && outUv) {
163
+ outUv[0] = Math.max(0, Math.min(1, (x - rect.x) / (rect.width || 1)));
164
+ outUv[1] = Math.max(0, Math.min(1, (y - rect.y) / (rect.height || 1)));
165
+ }
166
+ return inside;
167
+ }
168
+ }
169
+
170
+ export function captureCameras2D(realm: Realm2D, out: CameraSnapshot2D[]): void {
171
+ let count = 0;
172
+ for (const [entityId, camera] of realm.world.components.entries<Camera2DComponent>('Camera2D')) {
173
+ if (camera.desc.active === false) continue;
174
+ const model = realm.world.getComponent<Model2DComponent>(entityId, 'Model2D');
175
+ if (!model) continue;
176
+ let saved = out[count];
177
+ if (!saved)
178
+ out[count] = saved = { id: 0, slot: 0, x: 0, y: 0, rotation: 0, scale: 0, near: 0, far: 0 };
179
+ saved.id = camera.cameraId;
180
+ saved.slot = camera.desc.cameraSlot ?? 0;
181
+ saved.x = model.worldMatrix[4];
182
+ saved.y = model.worldMatrix[5];
183
+ saved.rotation = mat2d.getRotation(model.worldMatrix);
184
+ saved.scale = camera.desc.orthoScale;
185
+ saved.near = camera.desc.near ?? -1000;
186
+ saved.far = camera.desc.far ?? 1000;
187
+ count++;
188
+ }
189
+ out.length = count;
190
+ }
191
+
192
+ /** Maps a physical pixel in a resolved viewport into world coordinates. */
193
+ export function pointerToWorld2D(
194
+ out: [number, number],
195
+ x: number,
196
+ y: number,
197
+ viewport: readonly [number, number, number, number],
198
+ camera: CameraSnapshot2D
199
+ ): boolean {
200
+ const [left, top, width, height] = viewport;
201
+ if (width <= 0 || height <= 0 || x < left || y < top || x >= left + width || y >= top + height)
202
+ return false;
203
+ const localX = (((x - left) / width - 0.5) * camera.scale * width) / height;
204
+ const localY = (0.5 - (y - top) / height) * camera.scale;
205
+ const sin = Math.sin(camera.rotation),
206
+ cos = Math.cos(camera.rotation);
207
+ out[0] = camera.x + localX * cos - localY * sin;
208
+ out[1] = camera.y + localX * sin + localY * cos;
209
+ return true;
210
+ }
@@ -0,0 +1,214 @@
1
+ import {
2
+ type Collider3DComponent,
3
+ type ColliderEntry,
4
+ type ColliderShape3D,
5
+ getColliderEntries
6
+ } from '../collision/components';
7
+ import { intersectCollider3D } from '../collision/query3d';
8
+ import { mat4, type Mat4, quat, type ReadonlyMat4, vec3, type Vec3 } from '../math';
9
+ import type { Camera3DComponent, Model3DComponent } from '../realm3d/components';
10
+ import type { Realm3D } from '../realm3d/realm';
11
+
12
+ interface Shape3D {
13
+ inverse: Mat4;
14
+ entry?: ColliderEntry<ColliderShape3D>;
15
+ }
16
+ export interface CameraSnapshot3D {
17
+ id: number;
18
+ slot: number;
19
+ perspective: boolean;
20
+ fov: number;
21
+ scale: number;
22
+ near: number;
23
+ far: number;
24
+ position: Vec3;
25
+ forward: Vec3;
26
+ right: Vec3;
27
+ up: Vec3;
28
+ }
29
+ const FORWARD: [number, number, number] = [0, 0, -1];
30
+ const UP: [number, number, number] = [0, 1, 0];
31
+ const RIGHT: [number, number, number] = [1, 0, 0];
32
+ const ALT_UP: [number, number, number] = [0, 0, 1];
33
+
34
+ export class TargetSnapshot3D {
35
+ private readonly shapes: Shape3D[] = [];
36
+ private count = 0;
37
+ private readonly combined = mat4.create();
38
+ private readonly previousModel = mat4.create();
39
+ private hasModel = false;
40
+ private readonly localOrigin = vec3.create();
41
+ private readonly localDirection = vec3.create();
42
+ private readonly hit: import('../collision/query3d').LocalRayHit3D = {
43
+ distance: 0,
44
+ normal: [0, 0, 0]
45
+ };
46
+ valid = false;
47
+ exists = false;
48
+
49
+ capture(realm: Realm3D, entityId: number, colliderId?: number): void {
50
+ this.valid = false;
51
+ this.exists = false;
52
+ this.count = 0;
53
+ const model = realm.world.getComponent<Model3DComponent>(entityId, 'Model3D');
54
+ const collider = realm.world.getComponent<Collider3DComponent>(entityId, 'Collider3D');
55
+ if (!model || !collider) return;
56
+ let sameModel = this.hasModel;
57
+ for (let i = 0; i < 16; i++)
58
+ if (this.previousModel[i] !== model.worldMatrix[i]) sameModel = false;
59
+ mat4.copy(this.previousModel, model.worldMatrix);
60
+ this.hasModel = true;
61
+ for (const entry of getColliderEntries(collider)) {
62
+ if (colliderId !== undefined && colliderId !== entry.colliderId) continue;
63
+ this.exists = true;
64
+ if (!entry.active) continue;
65
+ let saved = this.shapes[this.count];
66
+ if (!saved) this.shapes.push((saved = { inverse: mat4.create() }));
67
+ if (!sameModel || saved.entry !== entry) {
68
+ mat4.multiply(this.combined, model.worldMatrix, entry.matrix as unknown as ReadonlyMat4);
69
+ if (!mat4.invert(saved.inverse, this.combined)) {
70
+ saved.entry = undefined;
71
+ continue;
72
+ }
73
+ saved.entry = entry;
74
+ }
75
+ this.count++;
76
+ }
77
+ this.valid = this.count > 0;
78
+ }
79
+
80
+ distance(
81
+ origin: Vec3,
82
+ direction: Vec3,
83
+ maxDistance: number,
84
+ outUv?: [number, number]
85
+ ): number | undefined {
86
+ let closest = Infinity;
87
+ let closestHitUv: [number, number] | undefined;
88
+ for (let i = 0; i < this.count; i++) {
89
+ const item = this.shapes[i]!;
90
+ vec3.transformMat4(this.localOrigin, origin, item.inverse);
91
+ vec3.transformDirectionMat4(this.localDirection, direction, item.inverse);
92
+ if (!item.entry) continue;
93
+ const hit = intersectCollider3D(
94
+ item.entry.shape,
95
+ this.localOrigin,
96
+ this.localDirection,
97
+ maxDistance,
98
+ this.hit
99
+ );
100
+ if (hit && hit.distance < closest) {
101
+ closest = hit.distance;
102
+ closestHitUv = hit.uv ? [hit.uv[0], hit.uv[1]] : undefined;
103
+ }
104
+ }
105
+ if (closest < Infinity) {
106
+ if (outUv && closestHitUv) {
107
+ outUv[0] = closestHitUv[0];
108
+ outUv[1] = closestHitUv[1];
109
+ }
110
+ return closest;
111
+ }
112
+ return undefined;
113
+ }
114
+ }
115
+
116
+ const rotation = quat.create();
117
+ const lookDirection = vec3.create();
118
+ export function captureCameras3D(realm: Realm3D, out: CameraSnapshot3D[]): void {
119
+ let count = 0;
120
+ for (const [entityId, camera] of realm.world.components.entries<Camera3DComponent>('Camera3D')) {
121
+ const model = realm.world.getComponent<Model3DComponent>(entityId, 'Model3D');
122
+ if (!model) continue;
123
+ let saved = out[count];
124
+ if (!saved)
125
+ out[count] = saved = {
126
+ id: 0,
127
+ slot: 0,
128
+ perspective: true,
129
+ fov: 60,
130
+ scale: 10,
131
+ near: 0.1,
132
+ far: 1000,
133
+ position: vec3.create(),
134
+ forward: vec3.create(),
135
+ right: vec3.create(),
136
+ up: vec3.create()
137
+ };
138
+ saved.id = camera.cameraId;
139
+ saved.slot = camera.desc.cameraSlot ?? 0;
140
+ saved.perspective = camera.desc.kind !== 'orthographic';
141
+ saved.fov = camera.desc.fovY ?? 60;
142
+ saved.scale = camera.desc.orthoScale ?? 10;
143
+ saved.near = camera.desc.near ?? 0.1;
144
+ saved.far = camera.desc.far ?? 1000;
145
+ mat4.getTranslation(saved.position, model.worldMatrix);
146
+ mat4.getRotation(rotation, model.worldMatrix);
147
+ vec3.transformQuat(saved.forward, FORWARD, rotation);
148
+ vec3.transformQuat(saved.up, UP, rotation);
149
+ vec3.transformQuat(saved.right, RIGHT, rotation);
150
+ const lookAt = camera.desc.lookAt;
151
+ if (lookAt) {
152
+ vec3.subtract(lookDirection, lookAt.target, saved.position);
153
+ if (vec3.dot(lookDirection, lookDirection) > 1e-8) {
154
+ vec3.normalize(saved.forward, lookDirection);
155
+ vec3.cross(saved.right, saved.forward, lookAt.up ?? UP);
156
+ if (vec3.dot(saved.right, saved.right) <= 1e-8)
157
+ vec3.cross(saved.right, saved.forward, Math.abs(saved.forward[1]) < 0.99 ? UP : ALT_UP);
158
+ vec3.normalize(saved.right, saved.right);
159
+ vec3.cross(saved.up, saved.right, saved.forward);
160
+ vec3.normalize(saved.up, saved.up);
161
+ }
162
+ }
163
+ count++;
164
+ }
165
+ out.length = count;
166
+ }
167
+
168
+ export function pointerToRay3D(
169
+ origin: Vec3,
170
+ direction: Vec3,
171
+ x: number,
172
+ y: number,
173
+ viewport: readonly [number, number, number, number],
174
+ camera: CameraSnapshot3D
175
+ ): number | undefined {
176
+ const [left, top, width, height] = viewport;
177
+ if (
178
+ width <= 0 ||
179
+ height <= 0 ||
180
+ x < left ||
181
+ y < top ||
182
+ x >= left + width ||
183
+ y >= top + height ||
184
+ (camera.perspective && camera.near <= 0) ||
185
+ camera.far <= camera.near
186
+ )
187
+ return;
188
+ const u = (2 * (x - left)) / width - 1;
189
+ const v = 1 - (2 * (y - top)) / height;
190
+ let offsetX: number, offsetY: number;
191
+ if (camera.perspective) {
192
+ const half = Math.tan((camera.fov * Math.PI) / 360);
193
+ offsetX = (u * half * width) / height;
194
+ offsetY = v * half;
195
+ for (let i = 0; i < 3; i++)
196
+ direction[i] = camera.forward[i]! + camera.right[i]! * offsetX + camera.up[i]! * offsetY;
197
+ vec3.normalize(direction, direction);
198
+ const cosine = vec3.dot(direction, camera.forward);
199
+ if (cosine <= 0) return;
200
+ vec3.scaleAndAdd(origin, camera.position, direction, camera.near / cosine);
201
+ return (camera.far - camera.near) / cosine;
202
+ }
203
+ offsetX = (u * camera.scale * width) / height / 2;
204
+ offsetY = (v * camera.scale) / 2;
205
+ for (let i = 0; i < 3; i++) {
206
+ direction[i] = camera.forward[i]!;
207
+ origin[i] =
208
+ camera.position[i]! +
209
+ camera.right[i]! * offsetX +
210
+ camera.up[i]! * offsetY +
211
+ camera.forward[i]! * camera.near;
212
+ }
213
+ return camera.far - camera.near;
214
+ }