@aura3d/scene 1.0.6

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 (109) hide show
  1. package/README.md +17 -0
  2. package/dist/Bounds.d.ts +18 -0
  3. package/dist/Bounds.d.ts.map +1 -0
  4. package/dist/Bounds.js +55 -0
  5. package/dist/Bounds.js.map +1 -0
  6. package/dist/Camera.d.ts +22 -0
  7. package/dist/Camera.d.ts.map +1 -0
  8. package/dist/Camera.js +44 -0
  9. package/dist/Camera.js.map +1 -0
  10. package/dist/DirectionalLight.d.ts +7 -0
  11. package/dist/DirectionalLight.d.ts.map +1 -0
  12. package/dist/DirectionalLight.js +12 -0
  13. package/dist/DirectionalLight.js.map +1 -0
  14. package/dist/Hierarchy.d.ts +6 -0
  15. package/dist/Hierarchy.d.ts.map +1 -0
  16. package/dist/Hierarchy.js +19 -0
  17. package/dist/Hierarchy.js.map +1 -0
  18. package/dist/Layers.d.ts +8 -0
  19. package/dist/Layers.d.ts.map +1 -0
  20. package/dist/Layers.js +21 -0
  21. package/dist/Layers.js.map +1 -0
  22. package/dist/Light.d.ts +14 -0
  23. package/dist/Light.d.ts.map +1 -0
  24. package/dist/Light.js +23 -0
  25. package/dist/Light.js.map +1 -0
  26. package/dist/Lights.d.ts +6 -0
  27. package/dist/Lights.d.ts.map +1 -0
  28. package/dist/Lights.js +5 -0
  29. package/dist/Lights.js.map +1 -0
  30. package/dist/MathTypes.d.ts +52 -0
  31. package/dist/MathTypes.d.ts.map +1 -0
  32. package/dist/MathTypes.js +119 -0
  33. package/dist/MathTypes.js.map +1 -0
  34. package/dist/Object3D.d.ts +48 -0
  35. package/dist/Object3D.d.ts.map +1 -0
  36. package/dist/Object3D.js +122 -0
  37. package/dist/Object3D.js.map +1 -0
  38. package/dist/OrthographicCamera.d.ts +29 -0
  39. package/dist/OrthographicCamera.d.ts.map +1 -0
  40. package/dist/OrthographicCamera.js +59 -0
  41. package/dist/OrthographicCamera.js.map +1 -0
  42. package/dist/PerspectiveCamera.d.ts +20 -0
  43. package/dist/PerspectiveCamera.d.ts.map +1 -0
  44. package/dist/PerspectiveCamera.js +42 -0
  45. package/dist/PerspectiveCamera.js.map +1 -0
  46. package/dist/PointLight.d.ts +10 -0
  47. package/dist/PointLight.d.ts.map +1 -0
  48. package/dist/PointLight.js +22 -0
  49. package/dist/PointLight.js.map +1 -0
  50. package/dist/Renderable.d.ts +30 -0
  51. package/dist/Renderable.d.ts.map +1 -0
  52. package/dist/Renderable.js +54 -0
  53. package/dist/Renderable.js.map +1 -0
  54. package/dist/Scene.d.ts +45 -0
  55. package/dist/Scene.d.ts.map +1 -0
  56. package/dist/Scene.js +117 -0
  57. package/dist/Scene.js.map +1 -0
  58. package/dist/SceneMetadata.d.ts +79 -0
  59. package/dist/SceneMetadata.d.ts.map +1 -0
  60. package/dist/SceneMetadata.js +130 -0
  61. package/dist/SceneMetadata.js.map +1 -0
  62. package/dist/SceneNode.d.ts +42 -0
  63. package/dist/SceneNode.d.ts.map +1 -0
  64. package/dist/SceneNode.js +126 -0
  65. package/dist/SceneNode.js.map +1 -0
  66. package/dist/SceneQuery.d.ts +16 -0
  67. package/dist/SceneQuery.d.ts.map +1 -0
  68. package/dist/SceneQuery.js +37 -0
  69. package/dist/SceneQuery.js.map +1 -0
  70. package/dist/SceneSerializer.d.ts +59 -0
  71. package/dist/SceneSerializer.d.ts.map +1 -0
  72. package/dist/SceneSerializer.js +127 -0
  73. package/dist/SceneSerializer.js.map +1 -0
  74. package/dist/SpotLight.d.ts +14 -0
  75. package/dist/SpotLight.d.ts.map +1 -0
  76. package/dist/SpotLight.js +35 -0
  77. package/dist/SpotLight.js.map +1 -0
  78. package/dist/TransformNode.d.ts +26 -0
  79. package/dist/TransformNode.d.ts.map +1 -0
  80. package/dist/TransformNode.js +98 -0
  81. package/dist/TransformNode.js.map +1 -0
  82. package/dist/index.d.ts +21 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +21 -0
  85. package/dist/index.js.map +1 -0
  86. package/package.json +18 -0
  87. package/src/Bounds.ts +68 -0
  88. package/src/Camera.ts +56 -0
  89. package/src/DirectionalLight.ts +13 -0
  90. package/src/Hierarchy.ts +20 -0
  91. package/src/Layers.ts +24 -0
  92. package/src/Light.ts +27 -0
  93. package/src/Lights.ts +5 -0
  94. package/src/MathTypes.ts +157 -0
  95. package/src/Object3D.ts +149 -0
  96. package/src/OrthographicCamera.ts +73 -0
  97. package/src/PerspectiveCamera.ts +54 -0
  98. package/src/PointLight.ts +25 -0
  99. package/src/Renderable.ts +71 -0
  100. package/src/Scene.ts +134 -0
  101. package/src/SceneMetadata.ts +225 -0
  102. package/src/SceneNode.ts +139 -0
  103. package/src/SceneQuery.ts +39 -0
  104. package/src/SceneSerializer.ts +177 -0
  105. package/src/SpotLight.ts +39 -0
  106. package/src/TransformNode.ts +116 -0
  107. package/src/index.ts +20 -0
  108. package/tests/scene.test.ts +117 -0
  109. package/tsconfig.json +9 -0
@@ -0,0 +1,157 @@
1
+ import { Frustum, Matrix4, Quaternion, Vector3 } from "@aura3d/math";
2
+
3
+ export type Vec3 = [number, number, number];
4
+ export type Quat = [number, number, number, number];
5
+ export type Mat4 = [
6
+ number, number, number, number,
7
+ number, number, number, number,
8
+ number, number, number, number,
9
+ number, number, number, number
10
+ ];
11
+ export type PlaneTuple = [number, number, number, number];
12
+
13
+ export const EPSILON = 1e-8;
14
+
15
+ export function vec3(x = 0, y = 0, z = 0): Vec3 {
16
+ return [x, y, z];
17
+ }
18
+
19
+ export function quat(x = 0, y = 0, z = 0, w = 1): Quat {
20
+ return fromMathQuat(new Quaternion(x, y, z, w).normalize());
21
+ }
22
+
23
+ export function identityMat4(): Mat4 {
24
+ return fromMathMat4(Matrix4.identity());
25
+ }
26
+
27
+ export function cloneMat4(value: Mat4): Mat4 {
28
+ return [...value] as Mat4;
29
+ }
30
+
31
+ export function addVec3(a: Vec3, b: Vec3): Vec3 {
32
+ return toMathVec3(a).add(toMathVec3(b)).toArray();
33
+ }
34
+
35
+ export function subVec3(a: Vec3, b: Vec3): Vec3 {
36
+ return toMathVec3(a).subtract(toMathVec3(b)).toArray();
37
+ }
38
+
39
+ export function scaleVec3(a: Vec3, scalar: number): Vec3 {
40
+ return toMathVec3(a).multiplyScalar(scalar).toArray();
41
+ }
42
+
43
+ export function lengthVec3(a: Vec3): number {
44
+ return toMathVec3(a).length();
45
+ }
46
+
47
+ export function normalizeVec3(a: Vec3): Vec3 {
48
+ return toMathVec3(a).normalize().toArray();
49
+ }
50
+
51
+ export function normalizeQuat(value: Quat): Quat {
52
+ return fromMathQuat(toMathQuat(value).normalize());
53
+ }
54
+
55
+ export function quatFromEuler(x: number, y: number, z: number): Quat {
56
+ const sx = Math.sin(x / 2);
57
+ const cx = Math.cos(x / 2);
58
+ const sy = Math.sin(y / 2);
59
+ const cy = Math.cos(y / 2);
60
+ const sz = Math.sin(z / 2);
61
+ const cz = Math.cos(z / 2);
62
+ return quat(
63
+ sx * cy * cz + cx * sy * sz,
64
+ cx * sy * cz - sx * cy * sz,
65
+ cx * cy * sz + sx * sy * cz,
66
+ cx * cy * cz - sx * sy * sz
67
+ );
68
+ }
69
+
70
+ export function multiplyMat4(a: Mat4, b: Mat4): Mat4 {
71
+ return fromMathMat4(toMathMat4(a).multiply(toMathMat4(b)));
72
+ }
73
+
74
+ export function composeMat4(position: Vec3, rotation: Quat, scale: Vec3): Mat4 {
75
+ return fromMathMat4(Matrix4.compose(toMathVec3(position), toMathQuat(rotation), toMathVec3(scale)));
76
+ }
77
+
78
+ export function transformPoint(matrix: Mat4, point: Vec3): Vec3 {
79
+ return toMathMat4(matrix).transformPoint(toMathVec3(point)).toArray();
80
+ }
81
+
82
+ export function invertMat4(matrix: Mat4): Mat4 {
83
+ return fromMathMat4(toMathMat4(matrix).inverse());
84
+ }
85
+
86
+ export function perspectiveMat4(fovYRadians: number, aspect: number, near: number, far: number): Mat4 {
87
+ return fromMathMat4(Matrix4.perspective(fovYRadians, aspect, near, far));
88
+ }
89
+
90
+ export function orthographicMat4(left: number, right: number, bottom: number, top: number, near: number, far: number): Mat4 {
91
+ return fromMathMat4(Matrix4.orthographic(left, right, bottom, top, near, far));
92
+ }
93
+
94
+ export function extractFrustumPlanes(viewProjection: Mat4): PlaneTuple[] {
95
+ return Frustum.fromMatrix(toMathMat4(viewProjection)).planes.map((plane) => [
96
+ plane.normal.x,
97
+ plane.normal.y,
98
+ plane.normal.z,
99
+ plane.constant
100
+ ]);
101
+ }
102
+
103
+ export function toMathVec3(value: Vec3): Vector3 {
104
+ return new Vector3(value[0], value[1], value[2]);
105
+ }
106
+
107
+ export function toMathQuat(value: Quat): Quaternion {
108
+ return new Quaternion(value[0], value[1], value[2], value[3]);
109
+ }
110
+
111
+ export function toMathMat4(value: Mat4): Matrix4 {
112
+ return new Matrix4(value);
113
+ }
114
+
115
+ export function fromMathMat4(value: Matrix4): Mat4 {
116
+ return [...value.elements] as Mat4;
117
+ }
118
+
119
+ export function fromMathQuat(value: Quaternion): Quat {
120
+ return [value.x, value.y, value.z, value.w];
121
+ }
122
+
123
+ export function decomposeMat4(matrix: Mat4): { position: Vec3; rotation: Quat; scale: Vec3 } {
124
+ const position: Vec3 = [matrix[12], matrix[13], matrix[14]];
125
+ const sx = Math.hypot(matrix[0], matrix[1], matrix[2]);
126
+ const sy = Math.hypot(matrix[4], matrix[5], matrix[6]);
127
+ const sz = Math.hypot(matrix[8], matrix[9], matrix[10]);
128
+ if (sx <= EPSILON || sy <= EPSILON || sz <= EPSILON) throw new Error("Cannot decompose transform matrix with zero scale.");
129
+
130
+ const m00 = matrix[0] / sx;
131
+ const m01 = matrix[4] / sy;
132
+ const m02 = matrix[8] / sz;
133
+ const m10 = matrix[1] / sx;
134
+ const m11 = matrix[5] / sy;
135
+ const m12 = matrix[9] / sz;
136
+ const m20 = matrix[2] / sx;
137
+ const m21 = matrix[6] / sy;
138
+ const m22 = matrix[10] / sz;
139
+ const trace = m00 + m11 + m22;
140
+ let rotation: Quat;
141
+
142
+ if (trace > 0) {
143
+ const s = Math.sqrt(trace + 1) * 2;
144
+ rotation = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, 0.25 * s];
145
+ } else if (m00 > m11 && m00 > m22) {
146
+ const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
147
+ rotation = [0.25 * s, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s];
148
+ } else if (m11 > m22) {
149
+ const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
150
+ rotation = [(m01 + m10) / s, 0.25 * s, (m12 + m21) / s, (m02 - m20) / s];
151
+ } else {
152
+ const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
153
+ rotation = [(m02 + m20) / s, (m12 + m21) / s, 0.25 * s, (m10 - m01) / s];
154
+ }
155
+
156
+ return { position, rotation: normalizeQuat(rotation), scale: [sx, sy, sz] };
157
+ }
@@ -0,0 +1,149 @@
1
+ import { SceneNode, type SceneNodeOptions } from "./SceneNode.js";
2
+ import { Renderable, type RenderableDescriptor } from "./Renderable.js";
3
+ import type { Mat4, Quat, Vec3 } from "./MathTypes.js";
4
+
5
+ export interface Object3DOptions extends SceneNodeOptions {
6
+ readonly visible?: boolean;
7
+ readonly layerMask?: number;
8
+ readonly renderOrder?: number;
9
+ readonly userData?: Record<string, unknown>;
10
+ }
11
+
12
+ export class Object3D extends SceneNode {
13
+ readonly isObject3D = true;
14
+
15
+ constructor(options: Object3DOptions = {}) {
16
+ super(options);
17
+ applyObject3DOptions(this, options);
18
+ }
19
+
20
+ get position(): Vec3 {
21
+ return this.transform.position;
22
+ }
23
+
24
+ get quaternion(): Quat {
25
+ return this.transform.rotation;
26
+ }
27
+
28
+ get scale(): Vec3 {
29
+ return this.transform.scale;
30
+ }
31
+
32
+ get matrix(): Mat4 {
33
+ return this.transform.localMatrix;
34
+ }
35
+
36
+ get matrixWorld(): Mat4 {
37
+ return this.transform.worldMatrix;
38
+ }
39
+
40
+ get matrixAutoUpdate(): boolean {
41
+ return this.transform.matrixAutoUpdate;
42
+ }
43
+
44
+ set matrixAutoUpdate(enabled: boolean) {
45
+ this.transform.setMatrixAutoUpdate(enabled);
46
+ }
47
+
48
+ setLocalMatrix(matrix: Mat4, options: { readonly decompose?: boolean; readonly matrixAutoUpdate?: boolean } = {}): this {
49
+ this.transform.setLocalMatrix(matrix, options);
50
+ return this;
51
+ }
52
+ }
53
+
54
+ export class Group extends Object3D {
55
+ readonly isGroup = true;
56
+ }
57
+
58
+ export interface MeshOptions extends Object3DOptions {
59
+ readonly renderable?: Renderable | RenderableDescriptor;
60
+ }
61
+
62
+ export class Mesh extends Object3D {
63
+ readonly isMesh = true;
64
+
65
+ constructor(options: MeshOptions = {}) {
66
+ super(options);
67
+ if (options.renderable) this.renderable = toRenderable(options.renderable);
68
+ }
69
+
70
+ setRenderable(renderable: Renderable | RenderableDescriptor): this {
71
+ this.renderable = toRenderable(renderable);
72
+ return this;
73
+ }
74
+ }
75
+
76
+ export class SkinnedMesh extends Mesh {
77
+ readonly isSkinnedMesh = true;
78
+
79
+ get skinning(): Renderable["skinning"] | undefined {
80
+ return this.renderable?.skinning;
81
+ }
82
+
83
+ setSkinning(skinning: NonNullable<Renderable["skinning"]>): this {
84
+ if (!this.renderable) throw new Error("SkinnedMesh requires a renderable before skinning can be assigned.");
85
+ this.renderable.skinning = {
86
+ jointCount: skinning.jointCount,
87
+ matrices: new Float32Array(skinning.matrices)
88
+ };
89
+ return this;
90
+ }
91
+ }
92
+
93
+ export class InstancedMesh extends Mesh {
94
+ readonly isInstancedMesh = true;
95
+
96
+ get instanceTransforms(): Float32Array | undefined {
97
+ return this.renderable?.instanceTransforms;
98
+ }
99
+
100
+ get instanceColors(): Float32Array | undefined {
101
+ return this.renderable?.instanceColors;
102
+ }
103
+
104
+ setInstanceTransforms(transforms: Float32Array | readonly number[]): this {
105
+ if (!this.renderable) throw new Error("InstancedMesh requires a renderable before instance transforms can be assigned.");
106
+ this.renderable.instanceTransforms = new Renderable({
107
+ geometry: this.renderable.geometry,
108
+ material: this.renderable.material,
109
+ layerMask: this.renderable.layerMask,
110
+ castShadow: this.renderable.castShadow,
111
+ receiveShadow: this.renderable.receiveShadow,
112
+ skinning: this.renderable.skinning,
113
+ morphWeights: this.renderable.morphWeights,
114
+ instanceTransforms: transforms,
115
+ instanceColors: this.renderable.instanceColors
116
+ }).instanceTransforms;
117
+ return this;
118
+ }
119
+
120
+ setInstanceColors(colors: Float32Array | readonly number[]): this {
121
+ if (!this.renderable) throw new Error("InstancedMesh requires a renderable before instance colors can be assigned.");
122
+ this.renderable.instanceColors = new Renderable({
123
+ geometry: this.renderable.geometry,
124
+ material: this.renderable.material,
125
+ layerMask: this.renderable.layerMask,
126
+ castShadow: this.renderable.castShadow,
127
+ receiveShadow: this.renderable.receiveShadow,
128
+ skinning: this.renderable.skinning,
129
+ morphWeights: this.renderable.morphWeights,
130
+ instanceTransforms: this.renderable.instanceTransforms,
131
+ instanceColors: colors
132
+ }).instanceColors;
133
+ return this;
134
+ }
135
+ }
136
+
137
+ function applyObject3DOptions(node: SceneNode, options: Object3DOptions): void {
138
+ if (options.visible !== undefined) node.visible = options.visible;
139
+ if (options.layerMask !== undefined) node.layerMask = options.layerMask;
140
+ if (options.renderOrder !== undefined) {
141
+ if (!Number.isFinite(options.renderOrder)) throw new Error("Object3D renderOrder must be finite.");
142
+ node.renderOrder = options.renderOrder;
143
+ }
144
+ if (options.userData) node.userData = { ...options.userData };
145
+ }
146
+
147
+ function toRenderable(renderable: Renderable | RenderableDescriptor): Renderable {
148
+ return renderable instanceof Renderable ? renderable : new Renderable(renderable);
149
+ }
@@ -0,0 +1,73 @@
1
+ import { ValidationError } from "@aura3d/core";
2
+ import { orthographicMat4, type Mat4 } from "./MathTypes.js";
3
+ import { Camera } from "./Camera.js";
4
+
5
+ export interface OrthographicCameraOptions {
6
+ id?: string;
7
+ name?: string;
8
+ left?: number;
9
+ right?: number;
10
+ bottom?: number;
11
+ top?: number;
12
+ near?: number;
13
+ far?: number;
14
+ zoom?: number;
15
+ resizeMode?: OrthographicResizeMode;
16
+ }
17
+
18
+ export type OrthographicResizeMode = "fit-vertical" | "fit-horizontal" | "preserve-frustum";
19
+
20
+ export class OrthographicCamera extends Camera {
21
+ left: number;
22
+ right: number;
23
+ bottom: number;
24
+ top: number;
25
+ near: number;
26
+ far: number;
27
+ zoom: number;
28
+ resizeMode: OrthographicResizeMode;
29
+
30
+ constructor(options: OrthographicCameraOptions = {}) {
31
+ super(options.name ?? "OrthographicCamera", options.id);
32
+ this.left = options.left ?? -1;
33
+ this.right = options.right ?? 1;
34
+ this.bottom = options.bottom ?? -1;
35
+ this.top = options.top ?? 1;
36
+ this.near = options.near ?? 0.1;
37
+ this.far = options.far ?? 1000;
38
+ this.zoom = options.zoom ?? 1;
39
+ this.resizeMode = options.resizeMode ?? "fit-vertical";
40
+ this.projectionMatrix = this.computeProjectionMatrix();
41
+ }
42
+
43
+ computeProjectionMatrix(): Mat4 {
44
+ if (!(this.zoom > 0)) throw new ValidationError("CAMERA_ZOOM", "Orthographic zoom must be positive.");
45
+ return orthographicMat4(this.left / this.zoom, this.right / this.zoom, this.bottom / this.zoom, this.top / this.zoom, this.near, this.far);
46
+ }
47
+
48
+ resize(width: number, height: number): void {
49
+ if (width <= 0 || height <= 0) throw new ValidationError("CAMERA_RESIZE", "Orthographic resize dimensions must be positive.");
50
+ const aspect = width / height;
51
+ const centerX = (this.left + this.right) / 2;
52
+ const centerY = (this.bottom + this.top) / 2;
53
+ const halfWidth = (this.right - this.left) / 2;
54
+ const halfHeight = (this.top - this.bottom) / 2;
55
+ if (this.resizeMode === "fit-horizontal") {
56
+ const resizedHalfHeight = halfWidth / aspect;
57
+ this.bottom = centerY - resizedHalfHeight;
58
+ this.top = centerY + resizedHalfHeight;
59
+ } else if (this.resizeMode === "preserve-frustum") {
60
+ const resizedHalfWidth = Math.max(halfWidth, halfHeight * aspect);
61
+ const resizedHalfHeight = Math.max(halfHeight, halfWidth / aspect);
62
+ this.left = centerX - resizedHalfWidth;
63
+ this.right = centerX + resizedHalfWidth;
64
+ this.bottom = centerY - resizedHalfHeight;
65
+ this.top = centerY + resizedHalfHeight;
66
+ } else {
67
+ const resizedHalfWidth = halfHeight * aspect;
68
+ this.left = centerX - resizedHalfWidth;
69
+ this.right = centerX + resizedHalfWidth;
70
+ }
71
+ this.updateCameraMatrices();
72
+ }
73
+ }
@@ -0,0 +1,54 @@
1
+ import { ValidationError } from "@aura3d/core";
2
+ import { perspectiveMat4, type Mat4 } from "./MathTypes.js";
3
+ import { Camera } from "./Camera.js";
4
+
5
+ export interface PerspectiveCameraOptions {
6
+ id?: string;
7
+ name?: string;
8
+ fovYRadians?: number;
9
+ aspect?: number;
10
+ near?: number;
11
+ far?: number;
12
+ }
13
+
14
+ export class PerspectiveCamera extends Camera {
15
+ fovYRadians: number;
16
+ aspect: number;
17
+ near: number;
18
+ far: number;
19
+
20
+ constructor(options: PerspectiveCameraOptions = {}) {
21
+ super(options.name ?? "PerspectiveCamera", options.id);
22
+ this.fovYRadians = options.fovYRadians ?? Math.PI / 3;
23
+ this.aspect = options.aspect ?? 1;
24
+ this.near = options.near ?? 0.1;
25
+ this.far = options.far ?? 1000;
26
+ this.projectionMatrix = this.computeProjectionMatrix();
27
+ }
28
+
29
+ computeProjectionMatrix(): Mat4 {
30
+ validatePerspectiveProjection(this.fovYRadians, this.aspect, this.near, this.far);
31
+ return perspectiveMat4(this.fovYRadians, this.aspect, this.near, this.far);
32
+ }
33
+
34
+ resize(width: number, height: number): void {
35
+ if (width <= 0 || height <= 0) throw new ValidationError("CAMERA_RESIZE", "Perspective resize dimensions must be positive.");
36
+ this.aspect = width / height;
37
+ this.updateCameraMatrices();
38
+ }
39
+ }
40
+
41
+ function validatePerspectiveProjection(fovYRadians: number, aspect: number, near: number, far: number): void {
42
+ if (!Number.isFinite(fovYRadians) || fovYRadians <= 0 || fovYRadians >= Math.PI) {
43
+ throw new ValidationError("CAMERA_FOV", "Perspective fovYRadians must be finite and greater than 0 and less than PI.");
44
+ }
45
+ if (!Number.isFinite(aspect) || aspect <= 0) {
46
+ throw new ValidationError("CAMERA_ASPECT", "Perspective aspect must be finite and positive.");
47
+ }
48
+ if (!Number.isFinite(near) || near <= 0) {
49
+ throw new ValidationError("CAMERA_NEAR", "Perspective near plane must be finite and positive.");
50
+ }
51
+ if (!Number.isFinite(far) || far <= near) {
52
+ throw new ValidationError("CAMERA_FAR", "Perspective far plane must be finite and greater than near.");
53
+ }
54
+ }
@@ -0,0 +1,25 @@
1
+ import { ValidationError } from "@aura3d/core";
2
+ import { Bounds3 } from "./Bounds.js";
3
+ import { Light } from "./Light.js";
4
+
5
+ export class PointLight extends Light {
6
+ private currentRange = 10;
7
+
8
+ constructor(name = "PointLight", id?: string) {
9
+ super("point", name, id);
10
+ }
11
+
12
+ get range(): number {
13
+ return this.currentRange;
14
+ }
15
+
16
+ set range(value: number) {
17
+ if (!(value > 0) || !Number.isFinite(value)) throw new ValidationError("POINT_LIGHT_RANGE", "Point light range must be positive.");
18
+ this.currentRange = value;
19
+ }
20
+
21
+ getWorldBounds(): Bounds3 {
22
+ const p = this.transform.worldMatrix;
23
+ return Bounds3.fromCenterSize([p[12], p[13], p[14]], [this.range * 2, this.range * 2, this.range * 2]);
24
+ }
25
+ }
@@ -0,0 +1,71 @@
1
+ import { ValidationError } from "@aura3d/core";
2
+
3
+ export interface RenderableDescriptor {
4
+ geometry: string;
5
+ material: string;
6
+ layerMask?: number;
7
+ castShadow?: boolean;
8
+ receiveShadow?: boolean;
9
+ skinning?: {
10
+ readonly jointCount: number;
11
+ readonly matrices: Float32Array;
12
+ };
13
+ morphWeights?: readonly number[];
14
+ instanceTransforms?: Float32Array | readonly number[];
15
+ instanceColors?: Float32Array | readonly number[];
16
+ }
17
+
18
+ export class Renderable {
19
+ readonly geometry: string;
20
+ readonly material: string;
21
+ layerMask: number;
22
+ castShadow: boolean;
23
+ receiveShadow: boolean;
24
+ skinning?: {
25
+ readonly jointCount: number;
26
+ readonly matrices: Float32Array;
27
+ };
28
+ morphWeights: number[];
29
+ instanceTransforms?: Float32Array;
30
+ instanceColors?: Float32Array;
31
+
32
+ constructor(descriptor: RenderableDescriptor) {
33
+ if (!descriptor.geometry) throw new ValidationError("RENDERABLE_GEOMETRY", "Renderable geometry handle is required.");
34
+ if (!descriptor.material) throw new ValidationError("RENDERABLE_MATERIAL", "Renderable material handle is required.");
35
+ this.geometry = descriptor.geometry;
36
+ this.material = descriptor.material;
37
+ this.layerMask = descriptor.layerMask ?? 1;
38
+ this.castShadow = descriptor.castShadow ?? true;
39
+ this.receiveShadow = descriptor.receiveShadow ?? true;
40
+ if (descriptor.skinning !== undefined) {
41
+ if (!Number.isInteger(descriptor.skinning.jointCount) || descriptor.skinning.jointCount <= 0 || descriptor.skinning.jointCount > 96) {
42
+ throw new ValidationError("RENDERABLE_SKINNING", "Renderable skinning jointCount must be an integer in [1, 96].");
43
+ }
44
+ if (descriptor.skinning.matrices.length !== descriptor.skinning.jointCount * 16 || !Array.from(descriptor.skinning.matrices).every(Number.isFinite)) {
45
+ throw new ValidationError("RENDERABLE_SKINNING", "Renderable skinning matrices must contain one finite mat4 per joint.");
46
+ }
47
+ this.skinning = {
48
+ jointCount: descriptor.skinning.jointCount,
49
+ matrices: new Float32Array(descriptor.skinning.matrices)
50
+ };
51
+ }
52
+ this.morphWeights = [...(descriptor.morphWeights ?? [])];
53
+ if (descriptor.instanceTransforms !== undefined) {
54
+ const values = Array.from(descriptor.instanceTransforms);
55
+ if (values.length === 0 || values.length % 16 !== 0 || !values.every(Number.isFinite)) {
56
+ throw new ValidationError("RENDERABLE_INSTANCES", "Renderable instanceTransforms must contain one or more finite mat4 values.");
57
+ }
58
+ this.instanceTransforms = new Float32Array(values);
59
+ }
60
+ if (descriptor.instanceColors !== undefined) {
61
+ const values = Array.from(descriptor.instanceColors);
62
+ if (values.length === 0 || values.length % 4 !== 0 || !values.every(Number.isFinite)) {
63
+ throw new ValidationError("RENDERABLE_INSTANCE_COLORS", "Renderable instanceColors must contain one or more finite vec4 values.");
64
+ }
65
+ if (this.instanceTransforms && values.length / 4 !== this.instanceTransforms.length / 16) {
66
+ throw new ValidationError("RENDERABLE_INSTANCE_COLORS", "Renderable instanceColors count must match instanceTransforms count.");
67
+ }
68
+ this.instanceColors = new Float32Array(values);
69
+ }
70
+ }
71
+ }
package/src/Scene.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { ValidationError } from "@aura3d/core";
2
+ import { Camera } from "./Camera.js";
3
+ import { DirectionalLight } from "./DirectionalLight.js";
4
+ import { Light } from "./Light.js";
5
+ import { Group, InstancedMesh, Mesh, Object3D, SkinnedMesh, type MeshOptions, type Object3DOptions } from "./Object3D.js";
6
+ import { OrthographicCamera } from "./OrthographicCamera.js";
7
+ import { PerspectiveCamera } from "./PerspectiveCamera.js";
8
+ import { PointLight } from "./PointLight.js";
9
+ import { Renderable } from "./Renderable.js";
10
+ import { collectRenderables, queryScene, type SceneQueryOptions } from "./SceneQuery.js";
11
+ import { SceneMetadata } from "./SceneMetadata.js";
12
+ import { SceneNode } from "./SceneNode.js";
13
+ import { SpotLight } from "./SpotLight.js";
14
+
15
+ export class Scene {
16
+ readonly root = new SceneNode({ id: "root", name: "root" });
17
+ readonly metadata = new SceneMetadata();
18
+ private readonly nodesById = new Map<string, SceneNode>([[this.root.id, this.root]]);
19
+
20
+ createNode(name?: string): SceneNode {
21
+ return this.register(new SceneNode({ name }));
22
+ }
23
+
24
+ createObject3D(options: Object3DOptions = {}): Object3D {
25
+ return this.register(new Object3D(options));
26
+ }
27
+
28
+ createGroup(options: Object3DOptions = {}): Group {
29
+ return this.register(new Group(options));
30
+ }
31
+
32
+ createMesh(options: MeshOptions = {}): Mesh {
33
+ return this.register(new Mesh(options));
34
+ }
35
+
36
+ createSkinnedMesh(options: MeshOptions = {}): SkinnedMesh {
37
+ return this.register(new SkinnedMesh(options));
38
+ }
39
+
40
+ createInstancedMesh(options: MeshOptions = {}): InstancedMesh {
41
+ return this.register(new InstancedMesh(options));
42
+ }
43
+
44
+ createPerspectiveCamera(options: ConstructorParameters<typeof PerspectiveCamera>[0] = {}): PerspectiveCamera {
45
+ return this.register(new PerspectiveCamera(options));
46
+ }
47
+
48
+ createOrthographicCamera(options: ConstructorParameters<typeof OrthographicCamera>[0] = {}): OrthographicCamera {
49
+ return this.register(new OrthographicCamera(options));
50
+ }
51
+
52
+ createLight(kind: "directional" | "point" | "spot", name?: string): DirectionalLight | PointLight | SpotLight {
53
+ const light = kind === "directional" ? new DirectionalLight(name) : kind === "point" ? new PointLight(name) : new SpotLight(name);
54
+ return this.register(light);
55
+ }
56
+
57
+ addRenderable(node: SceneNode, renderable: Renderable): void {
58
+ node.renderable = renderable;
59
+ }
60
+
61
+ getNodeById(id: string): SceneNode | undefined {
62
+ return this.nodesById.get(id);
63
+ }
64
+
65
+ findByName(name: string): SceneNode[] {
66
+ return this.query({ name });
67
+ }
68
+
69
+ query(options: SceneQueryOptions): SceneNode[] {
70
+ if (options.bounds) this.updateWorldBounds();
71
+ return queryScene(this.root, options);
72
+ }
73
+
74
+ updateWorldTransforms(): void {
75
+ this.root.updateWorldTransform();
76
+ }
77
+
78
+ updateWorldBounds(): void {
79
+ this.updateWorldTransforms();
80
+ this.root.updateWorldBounds();
81
+ }
82
+
83
+ update(): void {
84
+ this.updateWorldBounds();
85
+ }
86
+
87
+ traverse(visitor: (node: SceneNode) => void): void {
88
+ this.root.traverse(visitor);
89
+ }
90
+
91
+ collectRenderables(): { node: SceneNode; renderable: Renderable }[] {
92
+ return collectRenderables(this.root);
93
+ }
94
+
95
+ collectCameras(): Camera[] {
96
+ return this.query({ type: "camera" }).filter((node): node is Camera => node instanceof Camera);
97
+ }
98
+
99
+ collectLights(): Light[] {
100
+ return this.query({ type: "light" }).filter((node): node is Light => node instanceof Light);
101
+ }
102
+
103
+ removeNode(node: SceneNode): boolean {
104
+ if (node === this.root) return false;
105
+ const descendants: SceneNode[] = [];
106
+ node.traverse((descendant) => descendants.push(descendant));
107
+ const removed = node.removeFromParent();
108
+ for (const descendant of descendants) this.nodesById.delete(descendant.id);
109
+ return removed;
110
+ }
111
+
112
+ dispose(): void {
113
+ const descendants: SceneNode[] = [];
114
+ for (const child of [...this.root.children]) child.traverse((descendant) => descendants.push(descendant));
115
+ for (const child of [...this.root.children]) child.dispose();
116
+ for (const descendant of descendants) this.nodesById.delete(descendant.id);
117
+ this.root.children.length = 0;
118
+ }
119
+
120
+ registerSubtree(node: SceneNode): void {
121
+ node.traverse((descendant) => {
122
+ if (this.nodesById.has(descendant.id)) {
123
+ throw new ValidationError("DUPLICATE_SCENE_NODE", `Duplicate scene node id: ${descendant.id}`);
124
+ }
125
+ this.nodesById.set(descendant.id, descendant);
126
+ });
127
+ }
128
+
129
+ private register<T extends SceneNode>(node: T): T {
130
+ if (this.nodesById.has(node.id)) throw new ValidationError("DUPLICATE_SCENE_NODE", `Duplicate scene node id: ${node.id}`);
131
+ this.nodesById.set(node.id, node);
132
+ return node;
133
+ }
134
+ }