@kiberon-labs/behave-graph-scene 1.0.0
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.
- package/package.json +34 -0
- package/src/Abstractions/Drivers/DummyScene.ts +79 -0
- package/src/Abstractions/IScene.ts +18 -0
- package/src/GLTFJson.ts +34 -0
- package/src/Nodes/Actions/EaseSceneProperty.ts +162 -0
- package/src/Nodes/Actions/SetSceneProperty.ts +35 -0
- package/src/Nodes/Events/OnSceneNodeClick.ts +66 -0
- package/src/Nodes/Logic/ColorNodes.ts +121 -0
- package/src/Nodes/Logic/EulerNodes.ts +115 -0
- package/src/Nodes/Logic/Mat3Nodes.ts +202 -0
- package/src/Nodes/Logic/Mat4Nodes.ts +257 -0
- package/src/Nodes/Logic/QuatNodes.ts +178 -0
- package/src/Nodes/Logic/Vec2Nodes.ts +111 -0
- package/src/Nodes/Logic/Vec3Nodes.ts +121 -0
- package/src/Nodes/Logic/Vec4Nodes.ts +112 -0
- package/src/Nodes/Logic/VecElements.ts +34 -0
- package/src/Nodes/Queries/GetSceneProperty.ts +35 -0
- package/src/Values/ColorValue.ts +22 -0
- package/src/Values/EulerValue.ts +22 -0
- package/src/Values/Internal/Mat2.ts +214 -0
- package/src/Values/Internal/Mat3.ts +422 -0
- package/src/Values/Internal/Mat4.ts +831 -0
- package/src/Values/Internal/Vec2.ts +97 -0
- package/src/Values/Internal/Vec3.ts +244 -0
- package/src/Values/Internal/Vec4.ts +350 -0
- package/src/Values/Mat3Value.ts +20 -0
- package/src/Values/Mat4Value.ts +20 -0
- package/src/Values/QuatValue.ts +22 -0
- package/src/Values/Vec2Value.ts +14 -0
- package/src/Values/Vec3Value.ts +22 -0
- package/src/Values/Vec4Value.ts +21 -0
- package/src/buildScene.ts +479 -0
- package/src/index.ts +38 -0
- package/src/loadScene.ts +81 -0
- package/src/registerSceneProfile.ts +105 -0
- package/tests/graphs/logic/Color.json +53 -0
- package/tests/graphs/logic/Euler.json +53 -0
- package/tests/graphs/logic/Quaternion.json +56 -0
- package/tests/graphs/logic/Vector2.json +50 -0
- package/tests/graphs/logic/Vector3.json +53 -0
- package/tests/graphs/logic/Vector4.json +56 -0
- package/tests/readSceneGraphs.test.ts +57 -0
- package/tests/registerSceneProfile.test.ts +62 -0
- package/tests/tsconfig.json +11 -0
- package/tests/values/internal/Vec2.test.ts +74 -0
- package/tests/values/internal/Vec3.test.ts +83 -0
- package/tests/values/internal/Vec4.test.ts +91 -0
- package/tsconfig.json +55 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ValueType } from '@kiberon-labs/behave-graph';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Mat4,
|
|
5
|
+
mat4Equals,
|
|
6
|
+
type Mat4JSON,
|
|
7
|
+
mat4Mix,
|
|
8
|
+
mat4Parse
|
|
9
|
+
} from './Internal/Mat4.js';
|
|
10
|
+
|
|
11
|
+
export const Mat4Value: ValueType = {
|
|
12
|
+
name: 'mat4',
|
|
13
|
+
creator: () => new Mat4(),
|
|
14
|
+
deserialize: (value: string | Mat4JSON) =>
|
|
15
|
+
typeof value === 'string' ? mat4Parse(value) : new Mat4(value),
|
|
16
|
+
serialize: (value) => value.elements as Mat4JSON,
|
|
17
|
+
lerp: (start: Mat4, end: Mat4, t: number) => mat4Mix(start, end, t),
|
|
18
|
+
equals: (a: Mat4, b: Mat4) => mat4Equals(a, b),
|
|
19
|
+
clone: (value: Mat4) => value.clone()
|
|
20
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ValueType } from '@kiberon-labs/behave-graph';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
quatSlerp,
|
|
5
|
+
Vec4,
|
|
6
|
+
vec4Equals,
|
|
7
|
+
type Vec4JSON,
|
|
8
|
+
vec4Parse
|
|
9
|
+
} from './Internal/Vec4.js';
|
|
10
|
+
|
|
11
|
+
export const QuatValue: ValueType = {
|
|
12
|
+
name: 'quat',
|
|
13
|
+
creator: () => new Vec4(),
|
|
14
|
+
deserialize: (value: string | Vec4JSON) =>
|
|
15
|
+
typeof value === 'string'
|
|
16
|
+
? vec4Parse(value)
|
|
17
|
+
: new Vec4(value[0], value[1], value[2], value[3]),
|
|
18
|
+
serialize: (value) => [value.x, value.y, value.z, value.w] as Vec4JSON,
|
|
19
|
+
lerp: (start: Vec4, end: Vec4, t: number) => quatSlerp(start, end, t),
|
|
20
|
+
equals: (a: Vec4, b: Vec4) => vec4Equals(a, b),
|
|
21
|
+
clone: (value: Vec4) => value.clone()
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ValueType } from '@kiberon-labs/behave-graph';
|
|
2
|
+
|
|
3
|
+
import { Vec2, type Vec2JSON, vec2Mix, vec2Parse } from './Internal/Vec2.js';
|
|
4
|
+
|
|
5
|
+
export const Vec2Value: ValueType = {
|
|
6
|
+
name: 'vec2',
|
|
7
|
+
creator: () => new Vec2(),
|
|
8
|
+
deserialize: (value: string | Vec2JSON) =>
|
|
9
|
+
typeof value === 'string' ? vec2Parse(value) : new Vec2(value[0], value[1]),
|
|
10
|
+
serialize: (value) => [value.x, value.y] as Vec2JSON,
|
|
11
|
+
lerp: (start: Vec2, end: Vec2, t: number) => vec2Mix(start, end, t),
|
|
12
|
+
equals: (a: Vec2, b: Vec2) => a.x === b.x && a.y === b.y,
|
|
13
|
+
clone: (value: Vec2) => value.clone()
|
|
14
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ValueType } from '@kiberon-labs/behave-graph';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Vec3,
|
|
5
|
+
vec3Equals,
|
|
6
|
+
type Vec3JSON,
|
|
7
|
+
vec3Mix,
|
|
8
|
+
vec3Parse
|
|
9
|
+
} from './Internal/Vec3.js';
|
|
10
|
+
|
|
11
|
+
export const Vec3Value: ValueType = {
|
|
12
|
+
name: 'vec3',
|
|
13
|
+
creator: () => new Vec3(),
|
|
14
|
+
deserialize: (value: string | Vec3JSON) =>
|
|
15
|
+
typeof value === 'string'
|
|
16
|
+
? vec3Parse(value)
|
|
17
|
+
: new Vec3(value[0], value[1], value[2]),
|
|
18
|
+
serialize: (value) => [value.x, value.y, value.z] as Vec3JSON,
|
|
19
|
+
lerp: (start: Vec3, end: Vec3, t: number) => vec3Mix(start, end, t),
|
|
20
|
+
equals: (a: Vec3, b: Vec3) => vec3Equals(a, b),
|
|
21
|
+
clone: (value: Vec3) => value.clone()
|
|
22
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ValueType } from '@kiberon-labs/behave-graph';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Vec4,
|
|
5
|
+
vec4Equals,
|
|
6
|
+
type Vec4JSON,
|
|
7
|
+
vec4Mix,
|
|
8
|
+
vec4Parse
|
|
9
|
+
} from './Internal/Vec4.js';
|
|
10
|
+
export const Vec4Value: ValueType = {
|
|
11
|
+
name: 'vec4',
|
|
12
|
+
creator: () => new Vec4(),
|
|
13
|
+
deserialize: (value: string | Vec4JSON) =>
|
|
14
|
+
typeof value === 'string'
|
|
15
|
+
? vec4Parse(value)
|
|
16
|
+
: new Vec4(value[0], value[1], value[2], value[3]),
|
|
17
|
+
serialize: (value) => [value.x, value.y, value.z, value.w] as Vec4JSON,
|
|
18
|
+
lerp: (start: Vec4, end: Vec4, t: number) => vec4Mix(start, end, t),
|
|
19
|
+
equals: (a: Vec4, b: Vec4) => vec4Equals(a, b),
|
|
20
|
+
clone: (value: Vec4) => value.clone()
|
|
21
|
+
};
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { type Choices, EventEmitter } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import {
|
|
3
|
+
type Event,
|
|
4
|
+
Material,
|
|
5
|
+
MeshBasicMaterial,
|
|
6
|
+
Object3D,
|
|
7
|
+
Quaternion,
|
|
8
|
+
Vector3,
|
|
9
|
+
Vector4
|
|
10
|
+
} from 'three';
|
|
11
|
+
import type { GLTF } from 'three-stdlib';
|
|
12
|
+
|
|
13
|
+
import type { IScene } from './Abstractions/IScene.js';
|
|
14
|
+
import type { GLTFJson } from './GLTFJson.js';
|
|
15
|
+
import { Vec3 } from './Values/Internal/Vec3.js';
|
|
16
|
+
import { Vec4 } from './Values/Internal/Vec4.js';
|
|
17
|
+
|
|
18
|
+
const Resource = {
|
|
19
|
+
nodes: 'nodes',
|
|
20
|
+
materials: 'materials',
|
|
21
|
+
animations: 'animations'
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
type Resource = (typeof Resource)[keyof typeof Resource];
|
|
25
|
+
|
|
26
|
+
function toVec3(value: Vector3): Vec3 {
|
|
27
|
+
return new Vec3(value.x, value.y, value.z);
|
|
28
|
+
}
|
|
29
|
+
function toVec4(value: Vector4 | Quaternion): Vec4 {
|
|
30
|
+
return new Vec4(value.x, value.y, value.z, value.w);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export declare type ObjectMap = {
|
|
34
|
+
nodes: {
|
|
35
|
+
[name: string]: Object3D;
|
|
36
|
+
};
|
|
37
|
+
materials: {
|
|
38
|
+
[name: string]: Material;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const shortPathRegEx = /^\/?(?<resource>[^/]+)\/(?<index>\d+)$/;
|
|
43
|
+
const jsonPathRegEx =
|
|
44
|
+
/^\/?(?<resource>[^/]+)\/(?<index>\d+)\/(?<property>[^/]+)$/;
|
|
45
|
+
|
|
46
|
+
export type Optional<T> = {
|
|
47
|
+
[K in keyof T]: T[K] | undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type Path = {
|
|
51
|
+
resource: Resource;
|
|
52
|
+
index: number;
|
|
53
|
+
property: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function toJsonPathString(
|
|
57
|
+
{ index, property, resource: resourceType }: Optional<Path>,
|
|
58
|
+
short: boolean
|
|
59
|
+
) {
|
|
60
|
+
if (short) {
|
|
61
|
+
if (!resourceType || index === undefined) return;
|
|
62
|
+
return `${resourceType}/${index}`;
|
|
63
|
+
} else {
|
|
64
|
+
if (!resourceType || index === undefined || !property) return;
|
|
65
|
+
return `${resourceType}/${index}/${property}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseJsonPath(jsonPath: string, short = false): Path {
|
|
70
|
+
// hack = for now we see if there are 2 segments to know if its short
|
|
71
|
+
const regex = short ? shortPathRegEx : jsonPathRegEx;
|
|
72
|
+
const matches = regex.exec(jsonPath);
|
|
73
|
+
if (matches === null) throw new Error(`can not parse jsonPath: ${jsonPath}`);
|
|
74
|
+
if (matches.groups === undefined)
|
|
75
|
+
throw new Error(`can not parse jsonPath (no groups): ${jsonPath}`);
|
|
76
|
+
return {
|
|
77
|
+
resource: matches.groups.resource as Resource,
|
|
78
|
+
index: +matches.groups.index!,
|
|
79
|
+
property: matches.groups.property!
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function applyPropertyToModel(
|
|
84
|
+
{ resource, index, property }: Path,
|
|
85
|
+
gltf: GLTF & ObjectMap,
|
|
86
|
+
value: any,
|
|
87
|
+
properties: Properties,
|
|
88
|
+
setActiveAnimations:
|
|
89
|
+
| ((animation: string, active: boolean) => void)
|
|
90
|
+
| undefined
|
|
91
|
+
) {
|
|
92
|
+
const nodeName = getResourceName({ resource, index }, properties);
|
|
93
|
+
if (!nodeName) throw new Error(`could not get node at index ${index}`);
|
|
94
|
+
if (resource === Resource.nodes) {
|
|
95
|
+
const node = gltf.nodes[nodeName] as unknown as Object3D | undefined;
|
|
96
|
+
|
|
97
|
+
if (!node) {
|
|
98
|
+
console.error(`no node at path ${nodeName}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
applyNodeModifier(property, node, value);
|
|
103
|
+
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (resource === Resource.materials) {
|
|
107
|
+
const node = gltf.materials[nodeName] as unknown as Material | undefined;
|
|
108
|
+
|
|
109
|
+
if (!node) {
|
|
110
|
+
console.error(`no node at path ${nodeName}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
applyMaterialModifier(property, node, value);
|
|
115
|
+
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (resource === Resource.animations) {
|
|
120
|
+
if (!setActiveAnimations) {
|
|
121
|
+
console.error(
|
|
122
|
+
'cannot apply animation property without setActiveAnimations'
|
|
123
|
+
);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setActiveAnimations(nodeName, value as boolean);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.error(`unknown resource type ${resource}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const getResourceName = (
|
|
135
|
+
{ resource, index }: Pick<Path, 'resource' | 'index'>,
|
|
136
|
+
properties: Properties
|
|
137
|
+
) => {
|
|
138
|
+
return properties[resource]?.options[index]?.name;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const getPropertyFromModel = (
|
|
142
|
+
{ resource, index, property }: Path,
|
|
143
|
+
gltf: GLTF & ObjectMap,
|
|
144
|
+
properties: Properties
|
|
145
|
+
) => {
|
|
146
|
+
if (resource === Resource.nodes) {
|
|
147
|
+
const nodeName = getResourceName({ resource, index }, properties);
|
|
148
|
+
if (!nodeName) throw new Error(`could not get node at index ${index}`);
|
|
149
|
+
const node = gltf.nodes[nodeName] as unknown as Object3D | undefined;
|
|
150
|
+
|
|
151
|
+
if (!node) {
|
|
152
|
+
console.error(`no node at path ${nodeName}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getPropertyValue(property, node);
|
|
157
|
+
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
function applyNodeModifier(property: string, objectRef: Object3D, value: any) {
|
|
163
|
+
switch (property) {
|
|
164
|
+
case 'visible': {
|
|
165
|
+
objectRef.visible = value as boolean;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'translation': {
|
|
169
|
+
const v = value as Vec3;
|
|
170
|
+
objectRef.position.set(v.x, v.y, v.z);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'scale': {
|
|
174
|
+
const v = value as Vec3;
|
|
175
|
+
console.log(v.x);
|
|
176
|
+
objectRef.scale.set(v.x, v.y, v.z);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'rotation': {
|
|
180
|
+
const v = value as Vec4;
|
|
181
|
+
objectRef.quaternion.set(v.x, v.y, v.z, v.w);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function applyMaterialModifier(
|
|
188
|
+
property: string,
|
|
189
|
+
materialRef: Material,
|
|
190
|
+
value: any
|
|
191
|
+
) {
|
|
192
|
+
switch (property) {
|
|
193
|
+
case 'color': {
|
|
194
|
+
const basic = materialRef as MeshBasicMaterial;
|
|
195
|
+
|
|
196
|
+
if (basic.color) {
|
|
197
|
+
const v = value as Vec3;
|
|
198
|
+
basic.color.setRGB(v.x, v.y, v.z);
|
|
199
|
+
basic.needsUpdate = true;
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getPropertyValue(property: string, objectRef: Object3D<Event>) {
|
|
207
|
+
switch (property) {
|
|
208
|
+
case 'visible': {
|
|
209
|
+
return objectRef.visible;
|
|
210
|
+
}
|
|
211
|
+
case 'translation': {
|
|
212
|
+
return toVec3(objectRef.position);
|
|
213
|
+
}
|
|
214
|
+
case 'scale': {
|
|
215
|
+
return toVec3(objectRef.scale);
|
|
216
|
+
}
|
|
217
|
+
case 'rotation': {
|
|
218
|
+
return toVec4(objectRef.quaternion);
|
|
219
|
+
}
|
|
220
|
+
default:
|
|
221
|
+
throw new Error(`unrecognized property: ${property}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type ResourceOption = {
|
|
226
|
+
name: string;
|
|
227
|
+
index: number;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export type ResourceProperties = {
|
|
231
|
+
options: ResourceOption[];
|
|
232
|
+
properties: string[];
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
type Properties = {
|
|
236
|
+
[key in Resource]?: ResourceProperties;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export type ParsableScene = GLTF &
|
|
240
|
+
ObjectMap & {
|
|
241
|
+
json?: GLTFJson;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export const extractProperties = (gltf: ParsableScene): Properties => {
|
|
245
|
+
const nodeProperties = [
|
|
246
|
+
'visible',
|
|
247
|
+
'translation',
|
|
248
|
+
'scale',
|
|
249
|
+
'rotation',
|
|
250
|
+
'color'
|
|
251
|
+
];
|
|
252
|
+
const animationProperties = ['playing'];
|
|
253
|
+
const materialProperties = ['color'];
|
|
254
|
+
|
|
255
|
+
const gltfJson = gltf.parser.json as GLTFJson;
|
|
256
|
+
|
|
257
|
+
const nodeOptions = gltfJson.nodes?.map(({ name }, index) => ({
|
|
258
|
+
name: name || index.toString(),
|
|
259
|
+
index
|
|
260
|
+
}));
|
|
261
|
+
const materialOptions = gltfJson.materials?.map(({ name }, index) => ({
|
|
262
|
+
name: name || index.toString(),
|
|
263
|
+
index
|
|
264
|
+
}));
|
|
265
|
+
const animationOptions = gltf.animations?.map(({ name }, index) => ({
|
|
266
|
+
name: name || index.toString(),
|
|
267
|
+
index
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
const properties: Properties = {};
|
|
271
|
+
|
|
272
|
+
properties.nodes = { options: nodeOptions, properties: nodeProperties };
|
|
273
|
+
|
|
274
|
+
if (materialOptions) {
|
|
275
|
+
properties.materials = {
|
|
276
|
+
options: materialOptions,
|
|
277
|
+
properties: materialProperties
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (animationOptions) {
|
|
282
|
+
properties.animations = {
|
|
283
|
+
options: animationOptions,
|
|
284
|
+
properties: animationProperties
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return properties;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
function createPropertyChoice(
|
|
292
|
+
resource: string,
|
|
293
|
+
name: string,
|
|
294
|
+
property: string,
|
|
295
|
+
index: number
|
|
296
|
+
): { text: string; value: any } {
|
|
297
|
+
return {
|
|
298
|
+
text: `${resource}/${name}/${property}`,
|
|
299
|
+
value: `${resource}/${index}/${property}`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function generateChoicesForProperty(
|
|
304
|
+
property: ResourceProperties | undefined,
|
|
305
|
+
resource: Resource
|
|
306
|
+
) {
|
|
307
|
+
if (!property) return [];
|
|
308
|
+
const choices: { text: string; value: any }[] = [];
|
|
309
|
+
|
|
310
|
+
property.options.forEach(({ index, name }) => {
|
|
311
|
+
property.properties.forEach((property) => {
|
|
312
|
+
choices.push(createPropertyChoice(resource, name, property, index));
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return choices;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function generateSettableChoices(properties: Properties): Choices {
|
|
320
|
+
const choices: { text: string; value: any }[] = [
|
|
321
|
+
...generateChoicesForProperty(properties.nodes, Resource.nodes),
|
|
322
|
+
...generateChoicesForProperty(properties.materials, Resource.materials),
|
|
323
|
+
...generateChoicesForProperty(properties.animations, Resource.animations)
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
return choices;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function generateRaycastableChoices(properties: Properties): Choices {
|
|
330
|
+
const choices: { text: string; value: any }[] = [];
|
|
331
|
+
|
|
332
|
+
properties.nodes?.options.forEach(({ index, name }) => {
|
|
333
|
+
choices.push({
|
|
334
|
+
text: `nodes/${name}`,
|
|
335
|
+
value: `nodes/${index}`
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return choices;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export type OnClickCallback = (jsonPath: string) => void;
|
|
343
|
+
|
|
344
|
+
export type OnClickListener = {
|
|
345
|
+
path: Path;
|
|
346
|
+
elementName: string;
|
|
347
|
+
callbacks: OnClickCallback[];
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
export type OnClickListeners = {
|
|
351
|
+
[jsonPath: string]: OnClickListener;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const buildScene = ({
|
|
355
|
+
gltf,
|
|
356
|
+
setOnClickListeners,
|
|
357
|
+
setActiveAnimations
|
|
358
|
+
}: {
|
|
359
|
+
gltf: GLTF & ObjectMap;
|
|
360
|
+
setOnClickListeners:
|
|
361
|
+
| ((cb: (existing: OnClickListeners) => OnClickListeners) => void)
|
|
362
|
+
| undefined;
|
|
363
|
+
setActiveAnimations:
|
|
364
|
+
| ((animation: string, active: boolean) => void)
|
|
365
|
+
| undefined;
|
|
366
|
+
}) => {
|
|
367
|
+
const properties = extractProperties(gltf);
|
|
368
|
+
|
|
369
|
+
const onSceneChanged = new EventEmitter<void>();
|
|
370
|
+
|
|
371
|
+
const addOnClickedListener = (
|
|
372
|
+
jsonPath: string,
|
|
373
|
+
callback: (jsonPath: string) => void
|
|
374
|
+
) => {
|
|
375
|
+
if (!setOnClickListeners) return;
|
|
376
|
+
const path = parseJsonPath(jsonPath, true);
|
|
377
|
+
|
|
378
|
+
setOnClickListeners((existing) => {
|
|
379
|
+
const listenersForPath = existing[jsonPath] || {
|
|
380
|
+
path,
|
|
381
|
+
elementName: getResourceName(
|
|
382
|
+
{ resource: path.resource, index: path.index },
|
|
383
|
+
properties
|
|
384
|
+
)!,
|
|
385
|
+
callbacks: []
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const updatedListeners: OnClickListener = {
|
|
389
|
+
...listenersForPath,
|
|
390
|
+
callbacks: [...listenersForPath.callbacks, callback]
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const result: OnClickListeners = {
|
|
394
|
+
...existing,
|
|
395
|
+
[jsonPath]: updatedListeners
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
return result;
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const removeOnClickedListener = (
|
|
403
|
+
jsonPath: string,
|
|
404
|
+
callback: (jsonPath: string) => void
|
|
405
|
+
) => {
|
|
406
|
+
if (!setOnClickListeners) return;
|
|
407
|
+
setOnClickListeners((existing) => {
|
|
408
|
+
const listenersForPath = existing[jsonPath];
|
|
409
|
+
|
|
410
|
+
if (!listenersForPath) return existing;
|
|
411
|
+
|
|
412
|
+
const updatedCallbacks = listenersForPath.callbacks.filter(
|
|
413
|
+
(x) => x !== callback
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
if (updatedCallbacks.length > 0) {
|
|
417
|
+
const updatedListeners = {
|
|
418
|
+
...listenersForPath,
|
|
419
|
+
callback: updatedCallbacks
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
...existing,
|
|
424
|
+
[jsonPath]: updatedListeners
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const result = {
|
|
429
|
+
...existing
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
delete result[jsonPath];
|
|
433
|
+
|
|
434
|
+
return result;
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const getProperty = (jsonPath: string, _valueTypeName: string) => {
|
|
439
|
+
const path = parseJsonPath(jsonPath);
|
|
440
|
+
|
|
441
|
+
return getPropertyFromModel(path, gltf, properties);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const setProperty = (jsonPath: string, valueTypeName: string, value: any) => {
|
|
445
|
+
const path = parseJsonPath(jsonPath);
|
|
446
|
+
|
|
447
|
+
applyPropertyToModel(path, gltf, value, properties, setActiveAnimations);
|
|
448
|
+
|
|
449
|
+
onSceneChanged.emit();
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const settableChoices = generateSettableChoices(properties);
|
|
453
|
+
const raycastableChoices = generateRaycastableChoices(properties);
|
|
454
|
+
|
|
455
|
+
const addOnSceneChangedListener: IScene['addOnSceneChangedListener'] = (
|
|
456
|
+
listener
|
|
457
|
+
) => {
|
|
458
|
+
onSceneChanged.addListener(listener);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const removeOnSceneChangedListener: IScene['removeOnSceneChangedListener'] = (
|
|
462
|
+
listener
|
|
463
|
+
) => {
|
|
464
|
+
onSceneChanged.removeListener(listener);
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const scene: IScene = {
|
|
468
|
+
getProperty,
|
|
469
|
+
setProperty,
|
|
470
|
+
getProperties: () => settableChoices,
|
|
471
|
+
getRaycastableProperties: () => raycastableChoices,
|
|
472
|
+
addOnClickedListener,
|
|
473
|
+
removeOnClickedListener,
|
|
474
|
+
addOnSceneChangedListener,
|
|
475
|
+
removeOnSceneChangedListener
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
return scene;
|
|
479
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// scene profile
|
|
2
|
+
export * from './Abstractions/IScene.js';
|
|
3
|
+
export * from './Abstractions/Drivers/DummyScene.js';
|
|
4
|
+
|
|
5
|
+
export * from './Values/Internal/Mat3.js';
|
|
6
|
+
export * from './Values/Internal/Mat4.js';
|
|
7
|
+
export * from './Values/Internal/Vec2.js';
|
|
8
|
+
export * from './Values/Internal/Vec3.js';
|
|
9
|
+
export * from './Values/Internal/Vec4.js';
|
|
10
|
+
|
|
11
|
+
export * from './Values/ColorValue.js';
|
|
12
|
+
export * from './Values/EulerValue.js';
|
|
13
|
+
export * from './Values/Mat3Value.js';
|
|
14
|
+
export * from './Values/Mat4Value.js';
|
|
15
|
+
export * from './Values/Vec2Value.js';
|
|
16
|
+
export * from './Values/Vec3Value.js';
|
|
17
|
+
export * from './Values/Vec4Value.js';
|
|
18
|
+
export * from './Values/QuatValue.js';
|
|
19
|
+
|
|
20
|
+
export * from './Nodes/Actions/SetSceneProperty.js';
|
|
21
|
+
export * from './Nodes/Actions/EaseSceneProperty.js';
|
|
22
|
+
|
|
23
|
+
export * from './Nodes/Events/OnSceneNodeClick.js';
|
|
24
|
+
|
|
25
|
+
export * as ColorNodes from './Nodes/Logic/ColorNodes.js';
|
|
26
|
+
export * as EulerNodes from './Nodes/Logic/EulerNodes.js';
|
|
27
|
+
export * as Mat3Nodes from './Nodes/Logic/Mat3Nodes.js';
|
|
28
|
+
export * as Mat4Nodes from './Nodes/Logic/Mat4Nodes.js';
|
|
29
|
+
export * as Vec2Nodes from './Nodes/Logic/Vec2Nodes.js';
|
|
30
|
+
export * as Vec3Nodes from './Nodes/Logic/Vec3Nodes.js';
|
|
31
|
+
export * as Vec4Nodes from './Nodes/Logic/Vec4Nodes.js';
|
|
32
|
+
export * as QuatNodes from './Nodes/Logic/QuatNodes.js';
|
|
33
|
+
export * from './Nodes/Logic/VecElements.js';
|
|
34
|
+
|
|
35
|
+
export * from './Nodes/Queries/GetSceneProperty.js';
|
|
36
|
+
|
|
37
|
+
export * from './registerSceneProfile.js';
|
|
38
|
+
export * from './buildScene.js';
|
package/src/loadScene.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Group } from 'three';
|
|
2
|
+
import { DRACOLoader, type GLTF, GLTFLoader } from 'three-stdlib';
|
|
3
|
+
|
|
4
|
+
import type { IScene } from './Abstractions/IScene.js';
|
|
5
|
+
import { buildScene, type ObjectMap } from './buildScene.js';
|
|
6
|
+
|
|
7
|
+
// Taken from react-three-fiber
|
|
8
|
+
// Collects nodes and materials from a THREE.Object3D
|
|
9
|
+
export function buildGraph(object: Group) {
|
|
10
|
+
const data: ObjectMap = { nodes: {}, materials: {} };
|
|
11
|
+
if (object) {
|
|
12
|
+
object.traverse((obj: any) => {
|
|
13
|
+
if (obj.name) data.nodes[obj.name] = obj;
|
|
14
|
+
if (obj.material && !data.materials[obj.material.name])
|
|
15
|
+
data.materials[obj.material.name] = obj.material;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ThreeSceneReturn = {
|
|
22
|
+
scene: IScene;
|
|
23
|
+
gltf: GLTF & ObjectMap;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Loads a gltf, and corresponding IScene from a url
|
|
28
|
+
* @param url
|
|
29
|
+
* @param onProgress invoked on progress of loading the gltf
|
|
30
|
+
* @returns
|
|
31
|
+
*/
|
|
32
|
+
export const loadGltfAndBuildScene = (
|
|
33
|
+
url: string,
|
|
34
|
+
onProgress?: (progress: number) => void
|
|
35
|
+
): Promise<ThreeSceneReturn> => {
|
|
36
|
+
const loader = new GLTFLoader();
|
|
37
|
+
|
|
38
|
+
// Optional: Provide a DRACOLoader instance to decode compressed mesh data
|
|
39
|
+
const dracoLoader = new DRACOLoader();
|
|
40
|
+
dracoLoader.setDecoderPath(
|
|
41
|
+
'https://www.gstatic.com/draco/versioned/decoders/1.4.3/'
|
|
42
|
+
);
|
|
43
|
+
loader.setDRACOLoader(dracoLoader);
|
|
44
|
+
|
|
45
|
+
// Load a glTF resource
|
|
46
|
+
|
|
47
|
+
// eslint-disable-next-line promise/avoid-new
|
|
48
|
+
const result = new Promise<ThreeSceneReturn>((resolve, reject) => {
|
|
49
|
+
loader.load(
|
|
50
|
+
// resource URL
|
|
51
|
+
url,
|
|
52
|
+
// called when the resource is loaded
|
|
53
|
+
function (gltf) {
|
|
54
|
+
Object.assign(gltf, buildGraph(gltf.scene));
|
|
55
|
+
const asObjectMap = gltf as GLTF & ObjectMap;
|
|
56
|
+
|
|
57
|
+
const scene = buildScene({
|
|
58
|
+
gltf: asObjectMap,
|
|
59
|
+
setOnClickListeners: undefined,
|
|
60
|
+
setActiveAnimations: undefined
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
resolve({
|
|
64
|
+
scene,
|
|
65
|
+
gltf: asObjectMap
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
// called while loading is progressing
|
|
69
|
+
function (xhr) {
|
|
70
|
+
const progress = (xhr.loaded / xhr.total) * 100;
|
|
71
|
+
if (onProgress) onProgress(progress);
|
|
72
|
+
},
|
|
73
|
+
// called when loading has errors
|
|
74
|
+
function (error) {
|
|
75
|
+
reject(error);
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
};
|