Needle Engine

Changes between version 3.6.3 and 3.6.4
Files changed (9) hide show
  1. plugins/vite/copyfiles.js +2 -0
  2. plugins/vite/transform-codegen.js +10 -1
  3. src/engine-components/AnimationUtils.ts +70 -5
  4. src/engine/api.ts +12 -11
  5. src/engine-components/Component.ts +2 -1
  6. src/engine/engine_create_objects.ts +18 -5
  7. src/engine/engine_license.ts +4 -6
  8. src/engine/engine_scenetools.ts +9 -72
  9. src/engine-components/Gizmos.ts +1 -1
plugins/vite/copyfiles.js CHANGED
@@ -36,6 +36,7 @@
36
36
  const needleConfig = tryLoadProjectConfig();
37
37
  if (needleConfig) {
38
38
  assetsDirName = needleConfig.assetsDirectory;
39
+ while(assetsDirName.startsWith('/')) assetsDirName = assetsDirName.substring(1);
39
40
  }
40
41
 
41
42
  if (copyIncludesFromEngine !== false) {
@@ -84,6 +85,7 @@
84
85
  const targetDir = resolve(outDir, 'assets');
85
86
  copyRecursiveSync(assetsDir, targetDir);
86
87
  }
88
+ else console.log(`WARN: No assets directory found. Skipping copy of ${assetsDirName} resolved to ${assetsDir}`)
87
89
  // copy include dir
88
90
  const includeDir = resolve(baseDir, 'include');
89
91
  if (existsSync(includeDir)) {
plugins/vite/transform-codegen.js CHANGED
@@ -29,8 +29,17 @@
29
29
  apply: 'build',
30
30
  transform(src, id) {
31
31
  if (id.endsWith(codegenDirectory + "/gen.js")) {
32
- const assetsDir = builtAssetsDirectory();
32
+ let assetsDir = builtAssetsDirectory();
33
33
  if (assetsDir !== configuredAssetsDirectory) {
34
+ // check if the the assets directory is expected to be relative to the root
35
+ // like for example "/assets"
36
+ // then we want to add the leading dash also to the new path
37
+ // if (configuredAssetsDirectory.startsWith("/")) assetsDir = "/" + assetsDir;
38
+ // // are they now the same? if so well we dont have to do anything
39
+ // if (assetsDir === configuredAssetsDirectory) {
40
+ // return;
41
+ // }
42
+
34
43
  console.log(`[needle-transform-files] - Transform codegen paths \"${configuredAssetsDirectory}\" → \"${assetsDir}\"`)
35
44
  // replace codegen paths
36
45
  src = src.replaceAll(configuredAssetsDirectory, assetsDir);
src/engine-components/AnimationUtils.ts CHANGED
@@ -1,14 +1,79 @@
1
1
  import { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
2
2
  import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry";
3
- import { findAnimations } from "../engine/engine_scenetools";
3
+ import { addNewComponent } from "../engine/engine_components";
4
+ import { Animator } from "./Animator";
5
+ import { Animation } from "./Animation";
6
+ import { GameObject } from "./Component";
7
+ import { PlayableDirector } from "./api";
4
8
 
5
-
6
9
  ContextRegistry.registerCallback(ContextEvent.ContextCreated, args => {
7
10
  const autoplay = args.context.domElement.getAttribute("autoplay");
8
11
  if (autoplay !== undefined && (autoplay === "" || autoplay === "true")) {
9
12
  if (args.files) {
10
- for (const file of args.files)
11
- findAnimations(file.file as GLTF, true);
13
+ for (const file of args.files) {
14
+ const hasAnimation = GameObject.foreachComponent(file.file.scene, comp => {
15
+ if (comp.enabled === false) return undefined;
16
+ if (comp instanceof Animation && comp.playAutomatically || comp instanceof Animator || comp instanceof PlayableDirector && comp.playOnAwake === true) {
17
+ return true;
18
+ }
19
+ else if (comp instanceof Animation) {
20
+ comp.playAutomatically = true;
21
+ return true;
22
+ }
23
+ else if (comp instanceof PlayableDirector) {
24
+ comp.playOnAwake = true;
25
+ return true;
26
+ }
27
+ return undefined;
28
+ }, true);
29
+ if (hasAnimation !== true)
30
+ findAnimations(file.file as GLTF);
31
+ }
12
32
  }
13
33
  }
14
- });
34
+ });
35
+
36
+
37
+
38
+ function findAnimations(gltf: GLTF) {
39
+ if (!gltf || !gltf.animations) return;
40
+
41
+
42
+ for (let i = 0; i < gltf.animations.length; i++) {
43
+ const animation = gltf.animations[i];
44
+ if (!animation.tracks || animation.tracks.length <= 0) continue;
45
+ for (const t in animation.tracks) {
46
+ const track = animation.tracks[t];
47
+ const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
48
+ const obj = gltf.scene.getObjectByName(objectName);
49
+ if (!obj) {
50
+ // console.warn("could not find " + objectName, animation, gltf.scene);
51
+ continue;
52
+ }
53
+ let animationComponent = findAnimationGameObjectInParent(obj);
54
+ if (!animationComponent) {
55
+ animationComponent = addNewComponent(gltf.scene, new Animation());
56
+ }
57
+ const animations = animationComponent.animations = animationComponent.animations || [];
58
+ animation["name_animator"] = animationComponent.name;
59
+ // console.log(objectName, obj, animator.name, animations.length);
60
+ if (animations.indexOf(animation) < 0) {
61
+ animations.push(animation);
62
+ }
63
+ }
64
+ }
65
+ function findAnimationGameObjectInParent(obj) {
66
+ if (!obj) return;
67
+ const components = obj.userData?.components;
68
+ if (components && components.length > 0) {
69
+ for (let i = 0; i < components.length; i++) {
70
+ const component = components[i];
71
+ // console.log(component);
72
+ if (component instanceof Animator || component instanceof Animation) {
73
+ return obj;;
74
+ }
75
+ }
76
+ }
77
+ return findAnimationGameObjectInParent(obj.parent);
78
+ }
79
+ }
src/engine/api.ts CHANGED
@@ -1,12 +1,13 @@
1
1
 
2
- export * from "./extensions"
2
+ export * from "./extensions";
3
3
  export * from "./engine_addressables";
4
- export * from "./engine_application"
5
- export * from "./engine_assetdatabase"
4
+ export * from "./engine_application";
5
+ export * from "./engine_assetdatabase";
6
+ export * from "./engine_create_objects";
6
7
  export * from "./engine_components_internal";
7
8
  export * from "./engine_components";
8
9
  export * from "./engine_components_internal";
9
- export * from "./engine_context_registry"
10
+ export * from "./engine_context_registry";
10
11
  export * from "./engine_context";
11
12
  export * from "./engine_coroutine"
12
13
  export * from "./engine_constants";
@@ -24,12 +25,12 @@
24
25
  export * from "./engine_networking_files";
25
26
  export * from "./engine_networking_instantiate";
26
27
  export * from "./engine_networking_utils";
27
- export * from "./engine_patcher"
28
- export * from "./engine_playerview"
29
- export * from "./engine_physics"
30
- export * from "./engine_physics.types"
31
- export * from "./engine_physics_rapier"
32
- export * from "./engine_scenelighting"
28
+ export * from "./engine_patcher";
29
+ export * from "./engine_playerview";
30
+ export * from "./engine_physics";
31
+ export * from "./engine_physics.types";
32
+ export * from "./engine_physics_rapier";
33
+ export * from "./engine_scenelighting";
33
34
  export * from "./engine_input";
34
35
  export * from "./engine_math";
35
36
  export * from "./js-extensions";
@@ -47,5 +48,5 @@
47
48
  export { TypeStore, registerType } from "./engine_typestore";
48
49
 
49
50
  export { InstancingUtil } from "./engine_instancing";
50
- export { validate, prefix } from "./engine_util_decorator"
51
+ export { validate, prefix } from "./engine_util_decorator";
51
52
  export { hasProLicense, hasIndieLicense } from "./engine_license";
src/engine-components/Component.ts CHANGED
@@ -63,8 +63,9 @@
63
63
 
64
64
  /** Run a callback for all components of the provided type on the provided object and its children (if recursive is true)
65
65
  * @param instance object to run the method on
66
- * @param cb callback to run on each component
66
+ * @param cb callback to run on each component, "return undefined;" to continue and "return <anything>;" to break the loop
67
67
  * @param recursive if true, the method will be run on all children as well
68
+ * @returns the last return value of the callback
68
69
  */
69
70
  public static foreachComponent(instance: Object3D, cb: (comp: Component) => any, recursive: boolean = true): any {
70
71
  return foreachComponent(instance, cb as (comp: IComponent) => any, recursive);
src/engine/engine_create_objects.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { PlaneGeometry, MeshBasicMaterial, DoubleSide, Mesh, Material } from "three"
1
+ import { PlaneGeometry, MeshBasicMaterial, DoubleSide, Mesh, Material, MeshStandardMaterial, BoxGeometry, SphereGeometry } from "three"
2
2
 
3
3
 
4
4
  export enum PrimitiveType {
5
- Quad = 0
5
+ Quad = 0,
6
+ Cube = 1,
7
+ Sphere = 2,
6
8
  }
7
9
 
8
10
  export type ObjectOptions = {
@@ -16,9 +18,20 @@
16
18
  let obj: Mesh;
17
19
  switch (type) {
18
20
  case PrimitiveType.Quad:
19
- const geometry = new PlaneGeometry(1, 1, 1, 1);
20
- const material = opts?.material ?? new MeshBasicMaterial({ color: 0xffffff });
21
- obj = new Mesh(geometry, material);
21
+ const quadGeo = new PlaneGeometry(1, 1, 1, 1);
22
+ const quadMat = opts?.material ?? new MeshBasicMaterial({ color: 0xffffff });
23
+ obj = new Mesh(quadGeo, quadMat);
24
+ break;
25
+ case PrimitiveType.Cube:
26
+ const boxGeo = new BoxGeometry(1, 1, 1);
27
+ const boxMat = opts?.material ?? new MeshStandardMaterial({ color: 0xdddddd });
28
+ obj = new Mesh(boxGeo, boxMat);
29
+ break;
30
+ case PrimitiveType.Sphere:
31
+ const sphereGeo = new SphereGeometry(.5, 16, 16);
32
+ const sphereMat = opts?.material ?? new MeshStandardMaterial({ color: 0xdddddd });
33
+ obj = new Mesh(sphereGeo, sphereMat);
34
+ break;
22
35
  }
23
36
  if (opts?.name)
24
37
  obj.name = opts.name;
src/engine/engine_license.ts CHANGED
@@ -142,18 +142,16 @@
142
142
  background-image:url('${logo}');
143
143
  background-max-size: 40px;
144
144
  margin-bottom: 5px;
145
- margin-top: 2em;
146
- margin-bottom: 2em;
145
+ margin-top: .3em;
146
+ margin-bottom: .5em;
147
147
  padding: .2em;
148
148
  padding-left: 37px;
149
- background-color: rgba(250,160,160,.2);
150
149
  border-radius: .5em;
151
- border: 2px solid rgba(250,160,160,.5);
152
- color: rgba(90,20,20,1);
150
+ border: 2px solid rgba(160,160,160,.5);
153
151
  `;
154
152
  // url must contain https for firefox to make it clickable
155
153
  let licenseText = "Needle Engine — No license active, commercial use is not allowed. Visit https://needle.tools/pricing for more information and licensing options.";
156
- console.log("%c" + licenseText, style);
154
+ console.log("%c " + licenseText, style);
157
155
  }
158
156
 
159
157
  function createLicenseElement() {
src/engine/engine_scenetools.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  import { Context } from "./engine_setup"
2
- import { Animator } from '../engine-components/Animator';
3
- import { Animation } from '../engine-components/Animation';
4
2
  import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
5
3
  // import * as object from "./engine_gltf_builtin_components";
6
4
  import * as loaders from "./engine_loaders"
@@ -11,8 +9,8 @@
11
9
  import { createBuiltinComponents, writeBuiltinComponentData } from "./engine_gltf_builtin_components";
12
10
  import { SerializationContext } from "./engine_serialization_core";
13
11
  import { NEEDLE_components } from "./extensions/NEEDLE_components";
14
- import { addNewComponent, getComponentInChildren } from "./engine_components";
15
12
  import { registerPrewarmObject } from "./engine_mainloop_utils";
13
+ import { Object3D } from "three";
16
14
 
17
15
 
18
16
  export class NeedleGltfLoader implements INeedleGltfLoader {
@@ -24,10 +22,10 @@
24
22
  return writeBuiltinComponentData(comp, context);
25
23
  }
26
24
 
27
- parseSync(context: Context, data : string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined> {
25
+ parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined> {
28
26
  return parseSync(context, data, path, seed);
29
27
  }
30
- loadSync(context: Context, url: string, sourceId:string, seed: number | UIDProvider | null, prog?: ((ProgressEvent: any) => void) | undefined): Promise<GLTF | undefined> {
28
+ loadSync(context: Context, url: string, sourceId: string, seed: number | UIDProvider | null, prog?: ((ProgressEvent: any) => void) | undefined): Promise<GLTF | undefined> {
31
29
  return loadSync(context, url, sourceId, seed, prog);
32
30
  }
33
31
  }
@@ -98,7 +96,7 @@
98
96
  return loader;
99
97
  }
100
98
 
101
- export function parseSync(context: Context, data : string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined> {
99
+ export function parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined> {
102
100
  if (typeof path !== "string") {
103
101
  console.warn("Parse gltf binary without path, this might lead to errors in resolving extensions. Please provide the source path of the gltf/glb file", path, typeof path);
104
102
  }
@@ -127,7 +125,7 @@
127
125
  });
128
126
  }
129
127
 
130
- export function loadSync(context: Context, url: string, sourceId:string, seed: number | UIDProvider | null, prog?: (ProgressEvent) => void): Promise<GLTF | undefined> {
128
+ export function loadSync(context: Context, url: string, sourceId: string, seed: number | UIDProvider | null, prog?: (ProgressEvent) => void): Promise<GLTF | undefined> {
131
129
  // better to create new loaders every time
132
130
  // (maybe we can cache them...)
133
131
  // but due to the async nature and potentially triggering multiple loads at the same time
@@ -160,75 +158,13 @@
160
158
  });
161
159
  }
162
160
 
163
- export function findAnimationsLate(_context: Context, gltf, callbackarray, allowAddingAnimator: boolean = false) {
164
- if (gltf && gltf.animations && gltf.animations.length > 0) {
165
- callbackarray.push(() => {
166
- // console.trace("callback", gltf);
167
- findAnimations(gltf, allowAddingAnimator);
168
- });
169
- }
170
- }
171
161
 
172
- export function findAnimations(gltf: GLTF, allowAddingAnimator: boolean = false) {
173
- console.log(gltf);
174
- if (!gltf || !gltf.animations) return;
175
-
176
- if (!allowAddingAnimator) {
177
- // we only need to search if any animation component is in the scene
178
- // otherwise if we dont add anything there is no reason to search and log anything
179
- if (!getComponentInChildren(gltf.scene, Animation)) return;
180
- }
181
-
182
- for (let i = 0; i < gltf.animations.length; i++) {
183
- const animation = gltf.animations[i];
184
- if (!animation.tracks || animation.tracks.length <= 0) continue;
185
- for (const t in animation.tracks) {
186
- const track = animation.tracks[t];
187
- const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
188
- const obj = gltf.scene.getObjectByName(objectName);
189
- if (!obj) {
190
- // console.warn("could not find " + objectName, animation, gltf.scene);
191
- continue;
192
- }
193
- let animationComponent = findAnimationGameObjectInParent(obj);
194
- if (!animationComponent) {
195
- if (allowAddingAnimator)
196
- animationComponent = addNewComponent(gltf.scene, new Animation());
197
- else {
198
- console.warn("Failed finding animator for", track.name, objectName);
199
- continue;
200
- }
201
- }
202
- const animations = animationComponent.animations = animationComponent.animations || [];
203
- animation["name_animator"] = animationComponent.name;
204
- // console.log(objectName, obj, animator.name, animations.length);
205
- if (animations.indexOf(animation) < 0) {
206
- animations.push(animation);
207
- }
208
- }
209
- }
210
- function findAnimationGameObjectInParent(obj) {
211
- if (!obj) return;
212
- const components = obj.userData?.components;
213
- if (components && components.length > 0) {
214
- for (let i = 0; i < components.length; i++) {
215
- const component = components[i];
216
- // console.log(component);
217
- if (component instanceof Animator || component instanceof Animation) {
218
- return obj;;
219
- }
220
- }
221
- }
222
- return findAnimationGameObjectInParent(obj.parent);
223
- }
224
- }
225
-
226
-
227
162
  // TODO: save references in guid map
228
163
  // const guidMap = {};
229
164
 
230
165
 
231
- export function tryFindObjectByName(name, obj, recursive = true) {
166
+ /** @deprecated */
167
+ export function tryFindObjectByName(name: string, obj: Object3D, recursive = true) {
232
168
  if (obj.userData && obj.userData.name === name) return obj;
233
169
  if (obj.children && obj.children.length > 0) {
234
170
  for (let i = 0; i < obj.children.length; i++) {
@@ -239,7 +175,8 @@
239
175
  }
240
176
  }
241
177
 
242
- export function tryFindScript(globalObjectIdentifier, list = null) {
178
+ /** @deprecated */
179
+ export function tryFindScript(globalObjectIdentifier: string, list = null) {
243
180
  const arr = list ?? Context.Current.scripts;
244
181
  for (const i in arr) {
245
182
  const script = arr[i];
src/engine-components/Gizmos.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  onEnable(): void {
22
22
  if (this.isGizmo && !params.showGizmos) return;
23
23
  if (!this._gizmoObject) {
24
- if (this.objectBounds && this.gameObject["isMesh"] === true) {
24
+ if (this.objectBounds) {
25
25
  this._gizmoObject = new THREE.BoxHelper(this.gameObject, this.color ?? 0xffff00);
26
26
  }
27
27
  else {