@aura3d/scene 1.3.3 → 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,54 +0,0 @@
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
- }
package/src/PointLight.ts DELETED
@@ -1,25 +0,0 @@
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
- }
package/src/Renderable.ts DELETED
@@ -1,71 +0,0 @@
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 DELETED
@@ -1,134 +0,0 @@
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
- }
@@ -1,225 +0,0 @@
1
- import { ValidationError } from "@aura3d/core";
2
-
3
- export type SceneMetadataSeed = string | number;
4
- export type SceneMetadataPlanKind =
5
- | "animation"
6
- | "camera"
7
- | "environment"
8
- | "interaction"
9
- | "lighting"
10
- | "physics"
11
- | "postprocess"
12
- | "rendering";
13
- export type SceneMetadataCapabilityStatus = "supported" | "unsupported" | "approximation" | "route-local";
14
-
15
- export interface SceneAssetProvenance {
16
- readonly id: string;
17
- readonly uri?: string;
18
- readonly source?: string;
19
- readonly loader?: string;
20
- readonly version?: string;
21
- readonly digest?: string;
22
- readonly license?: string;
23
- readonly nodeIds?: readonly string[];
24
- readonly materialIds?: readonly string[];
25
- readonly notes?: string;
26
- }
27
-
28
- export interface SceneMaterialAssignment {
29
- readonly nodeId: string;
30
- readonly materialId: string;
31
- readonly slot?: string | number;
32
- readonly sourceMaterialId?: string;
33
- readonly fallback?: boolean;
34
- readonly unsupportedFeatures?: readonly string[];
35
- }
36
-
37
- export interface ScenePlanMetadata {
38
- readonly id: string;
39
- readonly kind: SceneMetadataPlanKind;
40
- readonly description: string;
41
- readonly owner?: string;
42
- readonly nodeIds?: readonly string[];
43
- readonly deterministicSeed?: SceneMetadataSeed;
44
- readonly unsupportedFeatures?: readonly string[];
45
- readonly evidence?: readonly string[];
46
- }
47
-
48
- export interface SceneCapabilityDisclosure {
49
- readonly feature: string;
50
- readonly status: SceneMetadataCapabilityStatus;
51
- readonly reason?: string;
52
- readonly owner?: string;
53
- readonly evidence?: readonly string[];
54
- }
55
-
56
- export interface SceneRevisionMetadata {
57
- readonly id?: string;
58
- readonly parentRevisionId?: string;
59
- readonly summary: string;
60
- readonly author?: string;
61
- readonly timestamp?: string;
62
- readonly deterministicSeed?: SceneMetadataSeed;
63
- }
64
-
65
- export type SceneRevisionRecord = Required<Pick<SceneRevisionMetadata, "id" | "summary">> & SceneRevisionMetadata;
66
-
67
- export interface SceneMetadataOptions {
68
- readonly deterministicSeed?: SceneMetadataSeed;
69
- readonly assets?: readonly SceneAssetProvenance[];
70
- readonly materialAssignments?: readonly SceneMaterialAssignment[];
71
- readonly plans?: readonly ScenePlanMetadata[];
72
- readonly capabilities?: readonly SceneCapabilityDisclosure[];
73
- readonly revisions?: readonly SceneRevisionMetadata[];
74
- }
75
-
76
- export type SerializedSceneMetadata = SceneMetadataOptions;
77
-
78
- export class SceneMetadata {
79
- deterministicSeed?: SceneMetadataSeed;
80
- readonly assets: SceneAssetProvenance[] = [];
81
- readonly materialAssignments: SceneMaterialAssignment[] = [];
82
- readonly plans: ScenePlanMetadata[] = [];
83
- readonly capabilities: SceneCapabilityDisclosure[] = [];
84
- readonly revisions: SceneRevisionRecord[] = [];
85
-
86
- constructor(options: SceneMetadataOptions = {}) {
87
- this.replace(options);
88
- }
89
-
90
- replace(options: SceneMetadataOptions | SceneMetadata): this {
91
- this.deterministicSeed = options.deterministicSeed;
92
- this.assets.length = 0;
93
- this.materialAssignments.length = 0;
94
- this.plans.length = 0;
95
- this.capabilities.length = 0;
96
- this.revisions.length = 0;
97
- for (const asset of options.assets ?? []) this.registerAsset(asset);
98
- for (const assignment of options.materialAssignments ?? []) this.assignMaterial(assignment);
99
- for (const plan of options.plans ?? []) this.addPlan(plan);
100
- for (const capability of options.capabilities ?? []) this.discloseCapability(capability);
101
- for (const revision of options.revisions ?? []) this.recordRevision(revision);
102
- return this;
103
- }
104
-
105
- clone(): SceneMetadata {
106
- return new SceneMetadata(this.toJSON());
107
- }
108
-
109
- get isEmpty(): boolean {
110
- return this.deterministicSeed === undefined
111
- && this.assets.length === 0
112
- && this.materialAssignments.length === 0
113
- && this.plans.length === 0
114
- && this.capabilities.length === 0
115
- && this.revisions.length === 0;
116
- }
117
-
118
- registerAsset(asset: SceneAssetProvenance): this {
119
- requireNonEmpty(asset.id, "asset id");
120
- upsertByKey(this.assets, cloneAsset(asset), (candidate) => candidate.id === asset.id);
121
- return this;
122
- }
123
-
124
- assignMaterial(assignment: SceneMaterialAssignment): this {
125
- requireNonEmpty(assignment.nodeId, "material assignment nodeId");
126
- requireNonEmpty(assignment.materialId, "material assignment materialId");
127
- const cloned = cloneMaterialAssignment(assignment);
128
- upsertByKey(
129
- this.materialAssignments,
130
- cloned,
131
- (candidate) => candidate.nodeId === cloned.nodeId && candidate.slot === cloned.slot
132
- );
133
- return this;
134
- }
135
-
136
- addPlan(plan: ScenePlanMetadata): this {
137
- requireNonEmpty(plan.id, "plan id");
138
- requireNonEmpty(plan.description, "plan description");
139
- upsertByKey(this.plans, clonePlan(plan), (candidate) => candidate.id === plan.id);
140
- return this;
141
- }
142
-
143
- discloseCapability(disclosure: SceneCapabilityDisclosure): this {
144
- requireNonEmpty(disclosure.feature, "capability feature");
145
- upsertByKey(this.capabilities, cloneCapability(disclosure), (candidate) => candidate.feature === disclosure.feature);
146
- return this;
147
- }
148
-
149
- discloseUnsupportedFeature(
150
- feature: string,
151
- reason: string,
152
- options: Omit<SceneCapabilityDisclosure, "feature" | "reason" | "status"> = {}
153
- ): this {
154
- return this.discloseCapability({ ...options, feature, reason, status: "unsupported" });
155
- }
156
-
157
- recordRevision(revision: SceneRevisionMetadata): this {
158
- requireNonEmpty(revision.summary, "revision summary");
159
- const id = revision.id ?? `revision-${this.revisions.length + 1}`;
160
- upsertByKey(this.revisions, cloneRevision({ ...revision, id }), (candidate) => candidate.id === id);
161
- return this;
162
- }
163
-
164
- unsupportedFeatures(): readonly SceneCapabilityDisclosure[] {
165
- return this.capabilities.filter((capability) => capability.status === "unsupported");
166
- }
167
-
168
- toJSON(): SerializedSceneMetadata {
169
- return {
170
- ...(this.deterministicSeed !== undefined ? { deterministicSeed: this.deterministicSeed } : {}),
171
- ...(this.assets.length > 0 ? { assets: this.assets.map(cloneAsset) } : {}),
172
- ...(this.materialAssignments.length > 0 ? { materialAssignments: this.materialAssignments.map(cloneMaterialAssignment) } : {}),
173
- ...(this.plans.length > 0 ? { plans: this.plans.map(clonePlan) } : {}),
174
- ...(this.capabilities.length > 0 ? { capabilities: this.capabilities.map(cloneCapability) } : {}),
175
- ...(this.revisions.length > 0 ? { revisions: this.revisions.map(cloneRevision) } : {})
176
- };
177
- }
178
- }
179
-
180
- function requireNonEmpty(value: string | undefined, label: string): void {
181
- if (!value || value.trim().length === 0) {
182
- throw new ValidationError("SCENE_METADATA", `Scene metadata ${label} must be a non-empty string.`);
183
- }
184
- }
185
-
186
- function upsertByKey<T>(items: T[], value: T, matches: (candidate: T) => boolean): void {
187
- const index = items.findIndex(matches);
188
- if (index >= 0) items[index] = value;
189
- else items.push(value);
190
- }
191
-
192
- function cloneAsset(asset: SceneAssetProvenance): SceneAssetProvenance {
193
- return {
194
- ...asset,
195
- ...(asset.nodeIds ? { nodeIds: [...asset.nodeIds] } : {}),
196
- ...(asset.materialIds ? { materialIds: [...asset.materialIds] } : {})
197
- };
198
- }
199
-
200
- function cloneMaterialAssignment(assignment: SceneMaterialAssignment): SceneMaterialAssignment {
201
- return {
202
- ...assignment,
203
- ...(assignment.unsupportedFeatures ? { unsupportedFeatures: [...assignment.unsupportedFeatures] } : {})
204
- };
205
- }
206
-
207
- function clonePlan(plan: ScenePlanMetadata): ScenePlanMetadata {
208
- return {
209
- ...plan,
210
- ...(plan.nodeIds ? { nodeIds: [...plan.nodeIds] } : {}),
211
- ...(plan.unsupportedFeatures ? { unsupportedFeatures: [...plan.unsupportedFeatures] } : {}),
212
- ...(plan.evidence ? { evidence: [...plan.evidence] } : {})
213
- };
214
- }
215
-
216
- function cloneCapability(capability: SceneCapabilityDisclosure): SceneCapabilityDisclosure {
217
- return {
218
- ...capability,
219
- ...(capability.evidence ? { evidence: [...capability.evidence] } : {})
220
- };
221
- }
222
-
223
- function cloneRevision(revision: SceneRevisionMetadata): SceneRevisionRecord {
224
- return { ...revision, id: revision.id ?? "revision-1" };
225
- }
package/src/SceneNode.ts DELETED
@@ -1,139 +0,0 @@
1
- import { ValidationError } from "@aura3d/core";
2
- import { Bounds3 } from "./Bounds.js";
3
- import { cloneMat4, invertMat4, multiplyMat4 } from "./MathTypes.js";
4
- import type { Renderable } from "./Renderable.js";
5
- import { TransformNode } from "./TransformNode.js";
6
-
7
- let nextSceneNodeId = 1;
8
-
9
- export interface SceneNodeOptions {
10
- id?: string;
11
- name?: string;
12
- }
13
-
14
- export interface AddChildOptions {
15
- preserveWorldTransform?: boolean;
16
- }
17
-
18
- export interface DisposeNodeOptions {
19
- recursive?: boolean;
20
- }
21
-
22
- export class SceneNode {
23
- readonly id: string;
24
- name: string;
25
- visible = true;
26
- layerMask = 1;
27
- renderOrder = 0;
28
- disposed = false;
29
- userData: Record<string, unknown> = {};
30
- renderable?: Renderable;
31
- readonly transform = new TransformNode();
32
- localBounds: Bounds3 | null = null;
33
- worldBounds = new Bounds3();
34
- readonly children: SceneNode[] = [];
35
- parent: SceneNode | null = null;
36
-
37
- constructor(options: SceneNodeOptions = {}) {
38
- this.id = options.id ?? `node-${nextSceneNodeId++}`;
39
- this.name = options.name ?? this.id;
40
- this.transform.onDirty(() => this.markWorldDirty());
41
- }
42
-
43
- addChild(child: SceneNode, options: AddChildOptions = {}): this {
44
- this.assertAlive();
45
- child.assertAlive();
46
- assertCanParent(this, child);
47
- if (child.parent === this) return this;
48
- const preservedWorld = options.preserveWorldTransform ? cloneMat4(child.transform.worldMatrix) : undefined;
49
- if (preservedWorld) this.updateWorldTransform();
50
- child.parent?.removeChild(child);
51
- this.children.push(child);
52
- child.parent = this;
53
- if (preservedWorld) child.transform.setFromLocalMatrix(multiplyMat4(invertMat4(this.transform.worldMatrix), preservedWorld));
54
- child.markWorldDirty();
55
- return this;
56
- }
57
-
58
- removeChild(child: SceneNode): boolean {
59
- this.assertAlive();
60
- const index = this.children.indexOf(child);
61
- if (index < 0) return false;
62
- this.children.splice(index, 1);
63
- child.parent = null;
64
- child.markWorldDirty();
65
- return true;
66
- }
67
-
68
- removeFromParent(): boolean {
69
- return this.parent?.removeChild(this) ?? false;
70
- }
71
-
72
- isAncestorOf(candidate: SceneNode): boolean {
73
- let current = candidate.parent;
74
- while (current) {
75
- if (current === this) return true;
76
- current = current.parent;
77
- }
78
- return false;
79
- }
80
-
81
- traverse(visitor: (node: SceneNode) => void): void {
82
- visitor(this);
83
- for (const child of [...this.children]) child.traverse(visitor);
84
- }
85
-
86
- updateWorldTransform(force = false): void {
87
- const changed = this.transform.updateWorld(this.parent?.transform.worldMatrix, force);
88
- for (const child of this.children) child.updateWorldTransform(force || changed);
89
- }
90
-
91
- updateWorldBounds(): Bounds3 {
92
- let bounds = this.localBounds ? this.localBounds.transform(this.transform.worldMatrix) : new Bounds3();
93
- for (const child of this.children) bounds = bounds.union(child.updateWorldBounds());
94
- this.worldBounds = bounds;
95
- return this.worldBounds;
96
- }
97
-
98
- setLocalBounds(bounds: Bounds3 | null): this {
99
- this.assertAlive();
100
- this.localBounds = bounds?.clone() ?? null;
101
- this.worldBounds = this.localBounds ? this.localBounds.transform(this.transform.worldMatrix) : new Bounds3();
102
- return this;
103
- }
104
-
105
- dispose(options: DisposeNodeOptions = {}): void {
106
- if (this.disposed) return;
107
- const recursive = options.recursive ?? true;
108
- const children = [...this.children];
109
- if (recursive) {
110
- for (const child of children) child.dispose(options);
111
- } else {
112
- for (const child of children) child.parent = null;
113
- }
114
- this.children.length = 0;
115
- this.parent?.removeChild(this);
116
- this.parent = null;
117
- this.renderable = undefined;
118
- this.localBounds = null;
119
- this.userData = {};
120
- this.visible = false;
121
- this.disposed = true;
122
- }
123
-
124
- markWorldDirty(): void {
125
- for (const child of this.children) {
126
- child.transform.markDirty();
127
- child.markWorldDirty();
128
- }
129
- }
130
-
131
- protected assertAlive(): void {
132
- if (this.disposed) throw new ValidationError("DISPOSED_SCENE_NODE", `Scene node ${this.name} is disposed.`);
133
- }
134
- }
135
-
136
- export function assertCanParent(parent: SceneNode, child: SceneNode): void {
137
- if (parent === child) throw new ValidationError("SCENE_SELF_PARENT", "A scene node cannot be parented to itself.");
138
- if (child.isAncestorOf(parent)) throw new ValidationError("SCENE_HIERARCHY_CYCLE", "Scene hierarchy cycle rejected.");
139
- }
package/src/SceneQuery.ts DELETED
@@ -1,39 +0,0 @@
1
- import { Bounds3 } from "./Bounds.js";
2
- import { Camera } from "./Camera.js";
3
- import { Light } from "./Light.js";
4
- import { Renderable } from "./Renderable.js";
5
- import { SceneNode } from "./SceneNode.js";
6
-
7
- export interface SceneQueryOptions {
8
- name?: string | RegExp;
9
- visible?: boolean;
10
- layerMask?: number;
11
- type?: "camera" | "light" | "node";
12
- bounds?: Bounds3;
13
- }
14
-
15
- export function queryScene(root: SceneNode, options: SceneQueryOptions): SceneNode[] {
16
- const result: SceneNode[] = [];
17
- root.traverse((node) => {
18
- if (options.visible !== undefined && node.visible !== options.visible) return;
19
- if (options.layerMask !== undefined && (node.layerMask & options.layerMask) === 0) return;
20
- if (options.name !== undefined) {
21
- const matches = typeof options.name === "string" ? node.name === options.name : options.name.test(node.name);
22
- if (!matches) return;
23
- }
24
- if (options.type === "camera" && !(node instanceof Camera)) return;
25
- if (options.type === "light" && !(node instanceof Light)) return;
26
- if (options.bounds && !node.worldBounds.intersects(options.bounds)) return;
27
- result.push(node);
28
- });
29
- return result;
30
- }
31
-
32
- export function collectRenderables(root: SceneNode): { node: SceneNode; renderable: Renderable }[] {
33
- const result: { node: SceneNode; renderable: Renderable }[] = [];
34
- root.traverse((node) => {
35
- const maybe = node.renderable;
36
- if (maybe && node.visible) result.push({ node, renderable: maybe });
37
- });
38
- return result;
39
- }