@aura3d/scene 1.4.0 → 1.4.1

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.
@@ -1,177 +0,0 @@
1
- import { ValidationError } from "@aura3d/core";
2
- import { DirectionalLight } from "./DirectionalLight.js";
3
- import { OrthographicCamera } from "./OrthographicCamera.js";
4
- import { PerspectiveCamera } from "./PerspectiveCamera.js";
5
- import { PointLight } from "./PointLight.js";
6
- import { Renderable } from "./Renderable.js";
7
- import { Scene } from "./Scene.js";
8
- import type { SerializedSceneMetadata } from "./SceneMetadata.js";
9
- import { SceneNode } from "./SceneNode.js";
10
- import { SpotLight } from "./SpotLight.js";
11
-
12
- export interface SerializedSceneNode {
13
- id: string;
14
- name: string;
15
- visible: boolean;
16
- layerMask: number;
17
- kind: "node" | "perspectiveCamera" | "orthographicCamera" | "directionalLight" | "pointLight" | "spotLight";
18
- transform: {
19
- position: [number, number, number];
20
- rotation: [number, number, number, number];
21
- scale: [number, number, number];
22
- };
23
- renderable?: {
24
- geometry: string;
25
- material: string;
26
- layerMask: number;
27
- castShadow: boolean;
28
- receiveShadow: boolean;
29
- morphWeights?: readonly number[];
30
- instanceTransforms?: readonly number[];
31
- instanceColors?: readonly number[];
32
- };
33
- camera?: {
34
- fovYRadians?: number;
35
- aspect?: number;
36
- left?: number;
37
- right?: number;
38
- bottom?: number;
39
- top?: number;
40
- near: number;
41
- far: number;
42
- zoom?: number;
43
- resizeMode?: "fit-vertical" | "fit-horizontal" | "preserve-frustum";
44
- viewport: { x: number; y: number; width: number; height: number };
45
- };
46
- light?: {
47
- color: [number, number, number];
48
- intensity: number;
49
- castsShadow: boolean;
50
- range?: number;
51
- angle?: number;
52
- penumbra?: number;
53
- };
54
- children: SerializedSceneNode[];
55
- }
56
-
57
- export interface SerializedScene {
58
- version: 1;
59
- metadata?: SerializedSceneMetadata;
60
- root: SerializedSceneNode;
61
- }
62
-
63
- export function serializeScene(scene: Scene): SerializedScene {
64
- return {
65
- version: 1,
66
- ...(scene.metadata.isEmpty ? {} : { metadata: scene.metadata.toJSON() }),
67
- root: serializeNode(scene.root)
68
- };
69
- }
70
-
71
- export function deserializeScene(serialized: SerializedScene): Scene {
72
- if (serialized.version !== 1) throw new ValidationError("SCENE_VERSION", `Unsupported scene version: ${serialized.version}`);
73
- const scene = new Scene();
74
- if (serialized.metadata) scene.metadata.replace(serialized.metadata);
75
- for (const child of serialized.root.children) {
76
- const node = deserializeNode(child);
77
- scene.root.addChild(node);
78
- scene.registerSubtree(node);
79
- }
80
- return scene;
81
- }
82
-
83
- function serializeNode(node: SceneNode): SerializedSceneNode {
84
- const renderable = node.renderable;
85
- return {
86
- id: node.id,
87
- name: node.name,
88
- visible: node.visible,
89
- layerMask: node.layerMask,
90
- kind: node instanceof PerspectiveCamera ? "perspectiveCamera" : node instanceof OrthographicCamera ? "orthographicCamera" : node instanceof DirectionalLight ? "directionalLight" : node instanceof PointLight ? "pointLight" : node instanceof SpotLight ? "spotLight" : "node",
91
- transform: {
92
- position: [...node.transform.position],
93
- rotation: [...node.transform.rotation],
94
- scale: [...node.transform.scale]
95
- },
96
- renderable: renderable ? {
97
- geometry: renderable.geometry,
98
- material: renderable.material,
99
- layerMask: renderable.layerMask,
100
- castShadow: renderable.castShadow,
101
- receiveShadow: renderable.receiveShadow,
102
- ...(renderable.morphWeights.length > 0 ? { morphWeights: [...renderable.morphWeights] } : {}),
103
- ...(renderable.instanceTransforms ? { instanceTransforms: [...renderable.instanceTransforms] } : {}),
104
- ...(renderable.instanceColors ? { instanceColors: [...renderable.instanceColors] } : {})
105
- } : undefined,
106
- camera: serializeCamera(node),
107
- light: serializeLight(node),
108
- children: node.children.map(serializeNode)
109
- };
110
- }
111
-
112
- function deserializeNode(data: SerializedSceneNode): SceneNode {
113
- const node =
114
- data.kind === "perspectiveCamera" ? new PerspectiveCamera({ id: data.id, name: data.name, fovYRadians: data.camera?.fovYRadians, aspect: data.camera?.aspect, near: data.camera?.near, far: data.camera?.far }) :
115
- data.kind === "orthographicCamera" ? new OrthographicCamera({ id: data.id, name: data.name, left: data.camera?.left, right: data.camera?.right, bottom: data.camera?.bottom, top: data.camera?.top, near: data.camera?.near, far: data.camera?.far, zoom: data.camera?.zoom, resizeMode: data.camera?.resizeMode }) :
116
- data.kind === "directionalLight" ? new DirectionalLight(data.name, data.id) :
117
- data.kind === "pointLight" ? new PointLight(data.name, data.id) :
118
- data.kind === "spotLight" ? new SpotLight(data.name, data.id) :
119
- new SceneNode({ id: data.id, name: data.name });
120
- node.visible = data.visible;
121
- node.layerMask = data.layerMask;
122
- node.transform.setPosition(...data.transform.position);
123
- node.transform.setRotation(...data.transform.rotation);
124
- node.transform.setScale(...data.transform.scale);
125
- if (data.camera && (node instanceof PerspectiveCamera || node instanceof OrthographicCamera)) node.setViewport(data.camera.viewport);
126
- if (data.light && (node instanceof DirectionalLight || node instanceof PointLight || node instanceof SpotLight)) {
127
- node.color = [...data.light!.color];
128
- node.intensity = data.light!.intensity;
129
- node.castsShadow = data.light!.castsShadow;
130
- }
131
- if (data.light?.range !== undefined && (node instanceof PointLight || node instanceof SpotLight)) node.range = data.light.range;
132
- if (data.light?.angle !== undefined && node instanceof SpotLight) node.angle = data.light.angle;
133
- if (data.light?.penumbra !== undefined && node instanceof SpotLight) node.penumbra = data.light.penumbra;
134
- if (data.renderable) node.renderable = new Renderable(data.renderable);
135
- for (const child of data.children) node.addChild(deserializeNode(child));
136
- return node;
137
- }
138
-
139
- function serializeCamera(node: SceneNode): SerializedSceneNode["camera"] {
140
- if (node instanceof PerspectiveCamera) {
141
- return {
142
- fovYRadians: node.fovYRadians,
143
- aspect: node.aspect,
144
- near: node.near,
145
- far: node.far,
146
- viewport: { ...node.viewport }
147
- };
148
- }
149
- if (node instanceof OrthographicCamera) {
150
- return {
151
- left: node.left,
152
- right: node.right,
153
- bottom: node.bottom,
154
- top: node.top,
155
- near: node.near,
156
- far: node.far,
157
- zoom: node.zoom,
158
- resizeMode: node.resizeMode,
159
- viewport: { ...node.viewport }
160
- };
161
- }
162
- return undefined;
163
- }
164
-
165
- function serializeLight(node: SceneNode): SerializedSceneNode["light"] {
166
- if (node instanceof DirectionalLight || node instanceof PointLight || node instanceof SpotLight) {
167
- const color = Array.isArray(node.color) ? [...node.color] as [number, number, number] : [node.color.x, node.color.y, node.color.z] as [number, number, number];
168
- return {
169
- color,
170
- intensity: node.intensity,
171
- castsShadow: node.castsShadow,
172
- ...(node instanceof PointLight || node instanceof SpotLight ? { range: node.range } : {}),
173
- ...(node instanceof SpotLight ? { angle: node.angle, penumbra: node.penumbra } : {})
174
- };
175
- }
176
- return undefined;
177
- }
package/src/SpotLight.ts DELETED
@@ -1,39 +0,0 @@
1
- import { ValidationError } from "@aura3d/core";
2
- import { Light } from "./Light.js";
3
-
4
- export class SpotLight extends Light {
5
- private currentAngle = Math.PI / 4;
6
- private currentPenumbra = 0;
7
- private currentRange = 10;
8
-
9
- constructor(name = "SpotLight", id?: string) {
10
- super("spot", name, id);
11
- }
12
-
13
- get angle(): number {
14
- return this.currentAngle;
15
- }
16
-
17
- set angle(value: number) {
18
- if (!(value > 0 && value < Math.PI / 2)) throw new ValidationError("SPOT_LIGHT_ANGLE", "Spot light angle must be within (0, PI / 2).");
19
- this.currentAngle = value;
20
- }
21
-
22
- get penumbra(): number {
23
- return this.currentPenumbra;
24
- }
25
-
26
- set penumbra(value: number) {
27
- if (!(value >= 0 && value <= 1)) throw new ValidationError("SPOT_LIGHT_PENUMBRA", "Spot light penumbra must be within [0, 1].");
28
- this.currentPenumbra = value;
29
- }
30
-
31
- get range(): number {
32
- return this.currentRange;
33
- }
34
-
35
- set range(value: number) {
36
- if (!(value > 0) || !Number.isFinite(value)) throw new ValidationError("SPOT_LIGHT_RANGE", "Spot light range must be positive.");
37
- this.currentRange = value;
38
- }
39
- }
@@ -1,116 +0,0 @@
1
- import {
2
- cloneMat4,
3
- composeMat4,
4
- decomposeMat4,
5
- identityMat4,
6
- invertMat4,
7
- multiplyMat4,
8
- normalizeQuat,
9
- type Mat4,
10
- type Quat,
11
- type Vec3
12
- } from "./MathTypes.js";
13
-
14
- export class TransformNode {
15
- private dirty = true;
16
- private readonly dirtyListeners = new Set<() => void>();
17
- matrixAutoUpdate = true;
18
- readonly position: Vec3 = [0, 0, 0];
19
- readonly rotation: Quat = [0, 0, 0, 1];
20
- readonly scale: Vec3 = [1, 1, 1];
21
- localMatrix: Mat4 = identityMat4();
22
- worldMatrix: Mat4 = identityMat4();
23
- inverseWorldMatrix: Mat4 = identityMat4();
24
-
25
- setPosition(x: number, y: number, z: number): this {
26
- this.position[0] = x;
27
- this.position[1] = y;
28
- this.position[2] = z;
29
- return this.markDirty();
30
- }
31
-
32
- setRotation(x: number, y: number, z: number, w: number): this {
33
- const normalized = normalizeQuat([x, y, z, w]);
34
- this.rotation[0] = normalized[0];
35
- this.rotation[1] = normalized[1];
36
- this.rotation[2] = normalized[2];
37
- this.rotation[3] = normalized[3];
38
- return this.markDirty();
39
- }
40
-
41
- setScale(x: number, y: number, z: number): this {
42
- this.scale[0] = x;
43
- this.scale[1] = y;
44
- this.scale[2] = z;
45
- return this.markDirty();
46
- }
47
-
48
- setFromLocalMatrix(matrix: Mat4): this {
49
- const decomposed = decomposeMat4(matrix);
50
- this.position[0] = decomposed.position[0];
51
- this.position[1] = decomposed.position[1];
52
- this.position[2] = decomposed.position[2];
53
- this.rotation[0] = decomposed.rotation[0];
54
- this.rotation[1] = decomposed.rotation[1];
55
- this.rotation[2] = decomposed.rotation[2];
56
- this.rotation[3] = decomposed.rotation[3];
57
- this.scale[0] = decomposed.scale[0];
58
- this.scale[1] = decomposed.scale[1];
59
- this.scale[2] = decomposed.scale[2];
60
- return this.markDirty();
61
- }
62
-
63
- setLocalMatrix(matrix: Mat4, options: { readonly decompose?: boolean; readonly matrixAutoUpdate?: boolean } = {}): this {
64
- if (!matrix.every(Number.isFinite)) throw new Error("Transform local matrix must contain finite values.");
65
- this.localMatrix = cloneMat4(matrix);
66
- this.matrixAutoUpdate = options.matrixAutoUpdate ?? false;
67
- if (options.decompose ?? true) {
68
- const decomposed = decomposeMat4(matrix);
69
- this.position[0] = decomposed.position[0];
70
- this.position[1] = decomposed.position[1];
71
- this.position[2] = decomposed.position[2];
72
- this.rotation[0] = decomposed.rotation[0];
73
- this.rotation[1] = decomposed.rotation[1];
74
- this.rotation[2] = decomposed.rotation[2];
75
- this.rotation[3] = decomposed.rotation[3];
76
- this.scale[0] = decomposed.scale[0];
77
- this.scale[1] = decomposed.scale[1];
78
- this.scale[2] = decomposed.scale[2];
79
- }
80
- return this.markDirty();
81
- }
82
-
83
- setMatrixAutoUpdate(enabled: boolean): this {
84
- this.matrixAutoUpdate = enabled;
85
- return this.markDirty();
86
- }
87
-
88
- markDirty(): this {
89
- if (!this.dirty) {
90
- this.dirty = true;
91
- for (const listener of [...this.dirtyListeners]) listener();
92
- }
93
- return this;
94
- }
95
-
96
- onDirty(listener: () => void): () => void {
97
- this.dirtyListeners.add(listener);
98
- return () => this.dirtyListeners.delete(listener);
99
- }
100
-
101
- isDirty(): boolean {
102
- return this.dirty;
103
- }
104
-
105
- updateWorld(parentWorld?: Mat4, force = false): boolean {
106
- const shouldUpdate = force || this.dirty;
107
- if (!shouldUpdate) return false;
108
- if (this.matrixAutoUpdate) {
109
- this.localMatrix = composeMat4(this.position, this.rotation, this.scale);
110
- }
111
- this.worldMatrix = parentWorld ? multiplyMat4(parentWorld, this.localMatrix) : cloneMat4(this.localMatrix);
112
- this.inverseWorldMatrix = invertMat4(this.worldMatrix);
113
- this.dirty = false;
114
- return true;
115
- }
116
- }
package/src/index.ts DELETED
@@ -1,20 +0,0 @@
1
- export * from "./MathTypes.js";
2
- export * from "./TransformNode.js";
3
- export * from "./Bounds.js";
4
- export * from "./SceneNode.js";
5
- export * from "./Object3D.js";
6
- export * from "./Hierarchy.js";
7
- export * from "./Camera.js";
8
- export * from "./PerspectiveCamera.js";
9
- export * from "./OrthographicCamera.js";
10
- export * from "./Light.js";
11
- export * from "./DirectionalLight.js";
12
- export * from "./PointLight.js";
13
- export * from "./SpotLight.js";
14
- export * from "./Layers.js";
15
- export * from "./Lights.js";
16
- export * from "./Renderable.js";
17
- export * from "./SceneQuery.js";
18
- export * from "./SceneMetadata.js";
19
- export * from "./Scene.js";
20
- export * from "./SceneSerializer.js";
@@ -1,117 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
- import {
4
- Bounds3,
5
- PerspectiveCamera,
6
- Renderable,
7
- Scene,
8
- SceneNode,
9
- deserializeScene,
10
- quatFromEuler,
11
- serializeScene
12
- } from "../src/index.js";
13
-
14
- test("scene hierarchy propagates nested transforms deterministically", () => {
15
- const scene = new Scene();
16
- const parent = scene.createNode("parent");
17
- const child = scene.createNode("child");
18
- parent.transform.setPosition(2, 0, 0);
19
- child.transform.setPosition(0, 3, 0);
20
- scene.root.addChild(parent);
21
- parent.addChild(child);
22
-
23
- scene.updateWorldTransforms();
24
-
25
- assert.equal(child.transform.worldMatrix[12], 2);
26
- assert.equal(child.transform.worldMatrix[13], 3);
27
- assert.equal(child.transform.worldMatrix[14], 0);
28
- });
29
-
30
- test("scene hierarchy rejects self-parenting and cycles", () => {
31
- const parent = new SceneNode({ name: "parent" });
32
- const child = new SceneNode({ name: "child" });
33
- parent.addChild(child);
34
-
35
- assert.throws(() => parent.addChild(parent), /cannot be parented to itself/);
36
- assert.throws(() => child.addChild(parent), /cycle rejected/);
37
- });
38
-
39
- test("scene traversal snapshots children when nodes are removed during traversal", () => {
40
- const scene = new Scene();
41
- const a = scene.createNode("a");
42
- const b = scene.createNode("b");
43
- scene.root.addChild(a);
44
- scene.root.addChild(b);
45
- const visited: string[] = [];
46
-
47
- scene.traverse((node) => {
48
- visited.push(node.name);
49
- if (node === a) scene.root.removeChild(b);
50
- });
51
-
52
- assert.deepEqual(visited, ["root", "a", "b"]);
53
- });
54
-
55
- test("bounds transform handles negative scale", () => {
56
- const node = new SceneNode();
57
- node.transform.setPosition(5, 0, 0).setScale(-2, 3, 1);
58
- node.updateWorldTransform();
59
-
60
- const world = Bounds3.fromCenterSize([0, 0, 0], [2, 2, 2]).transform(node.transform.worldMatrix);
61
-
62
- assert.deepEqual(world.min, [3, -3, -1]);
63
- assert.deepEqual(world.max, [7, 3, 1]);
64
- });
65
-
66
- test("perspective camera produces view projection and frustum planes", () => {
67
- const camera = new PerspectiveCamera({ fovYRadians: Math.PI / 2, aspect: 1, near: 1, far: 100 });
68
- camera.transform.setPosition(0, 0, 10);
69
- camera.updateCameraMatrices();
70
-
71
- assert.equal(camera.frustumPlanes.length, 6);
72
- assert.equal(camera.viewMatrix[14], -10);
73
- assert.ok(Math.abs(camera.projectionMatrix[0] - 1) < 1e-8);
74
- assert.throws(() => new PerspectiveCamera({ fovYRadians: 0 }).computeProjectionMatrix(), /fov/);
75
- });
76
-
77
- test("directional camera-space contracts are independent from controls", () => {
78
- const camera = new PerspectiveCamera();
79
- camera.transform.setRotation(...quatFromEuler(0, Math.PI / 4, 0));
80
- camera.updateCameraMatrices();
81
-
82
- assert.equal(camera.viewport.width, 1);
83
- assert.equal(camera.viewport.height, 1);
84
- assert.throws(() => camera.setViewport({ x: 0, y: 0, width: 0, height: 1 }), /positive/);
85
- });
86
-
87
- test("scene collects renderables, cameras, and lights without mutation", () => {
88
- const scene = new Scene();
89
- const node = scene.createNode("mesh");
90
- const camera = scene.createPerspectiveCamera();
91
- const light = scene.createLight("directional");
92
- scene.root.addChild(node);
93
- scene.root.addChild(camera);
94
- scene.root.addChild(light);
95
- scene.addRenderable(node, new Renderable({ geometry: "geometry:cube", material: "material:unlit" }));
96
-
97
- const before = node.transform.worldMatrix.slice();
98
- const renderables = scene.collectRenderables();
99
-
100
- assert.equal(renderables.length, 1);
101
- assert.equal(scene.collectCameras().length, 1);
102
- assert.equal(scene.collectLights().length, 1);
103
- assert.deepEqual(node.transform.worldMatrix, before);
104
- });
105
-
106
- test("scene serialization roundtrips a simple hierarchy", () => {
107
- const scene = new Scene();
108
- const parent = scene.createNode("parent");
109
- const child = scene.createNode("child");
110
- parent.addChild(child);
111
- scene.root.addChild(parent);
112
-
113
- const restored = deserializeScene(serializeScene(scene));
114
-
115
- assert.deepEqual(restored.findByName("child").map((node) => node.name), ["child"]);
116
- assert.equal(restored.root.children[0].children[0].name, "child");
117
- });
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "declaration": true,
5
- "outDir": "dist",
6
- "emitDeclarationOnly": true
7
- },
8
- "include": ["src/**/*.ts"]
9
- }