Needle Engine

Changes between version 3.36.6 and 3.37.0-alpha
Files changed (23) hide show
  1. src/engine-components/export/usdz/utils/animationutils.ts +33 -11
  2. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +21 -10
  3. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +34 -49
  4. src/engine-components/postprocessing/Effects/ColorAdjustments.ts +3 -3
  5. src/engine-components/codegen/components.ts +1 -0
  6. src/engine/engine_context.ts +1 -0
  7. src/engine/engine_element_attributes.ts +1 -0
  8. src/engine/engine_element.ts +46 -19
  9. src/engine/engine_hot_reload.ts +3 -1
  10. src/engine/engine_input.ts +8 -2
  11. src/engine/engine_mainloop_utils.ts +12 -1
  12. src/engine/engine_networking_files.ts +1 -1
  13. src/engine/engine_time_utils.ts +1 -1
  14. src/engine/assets/index.ts +6 -6
  15. src/engine/webcomponents/needle menu/needle-menu.ts +4 -2
  16. src/engine-components/postprocessing/PostProcessingHandler.ts +20 -1
  17. src/engine/codegen/register_types.ts +2 -0
  18. src/engine-components/RigidBody.ts +3 -0
  19. src/engine-components/SpriteRenderer.ts +82 -22
  20. src/engine-components/postprocessing/Effects/Tonemapping.ts +3 -1
  21. src/engine-components/export/usdz/USDZExporter.ts +3 -1
  22. src/engine-components/webxr/WebXRButtons.ts +5 -2
  23. src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts +137 -0
src/engine-components/export/usdz/utils/animationutils.ts CHANGED
@@ -32,7 +32,7 @@
32
32
  if (ext.injectImplicitBehaviours) {
33
33
  // We're registering animators with implicit PlayAnimationOnClick (with hacked "start" trigger) here.
34
34
  for (const animator of animators) {
35
- if (!animator || !animator.runtimeAnimatorController) continue;
35
+ if (!animator || !animator.runtimeAnimatorController || !animator.enabled) continue;
36
36
  const activeState = animator.runtimeAnimatorController.activeState;
37
37
 
38
38
  // skip missing data, empty states or motions
@@ -60,7 +60,7 @@
60
60
  }
61
61
  else {
62
62
  for (const animator of animators) {
63
- if (!animator || !animator.runtimeAnimatorController) continue;
63
+ if (!animator || !animator.runtimeAnimatorController || !animator.enabled) continue;
64
64
 
65
65
  if (debug) console.log(animator);
66
66
 
@@ -81,21 +81,43 @@
81
81
 
82
82
  // TODO once PlayAnimationOnClick can use animation components as well,
83
83
  // we can treat them the same as we treat Animators above.
84
- for (const animationComponent of animationComponents) {
85
- if (debug)
86
- console.log(animationComponent);
84
+ if (ext.injectImplicitBehaviours) {
85
+ for (const animationComponent of animationComponents) {
86
+ if (!animationComponent || !animationComponent.clip ||!animationComponent.enabled) continue;
87
+ if (!animationComponent.playAutomatically) continue;
87
88
 
88
- const clips: AnimationClip[] = [];
89
+ // Create a PlayAnimationOnClick component that will play the animation on start
90
+ // This is a bit hacky right now (we're using the internal "start" trigger)
91
+ const newComponent = new PlayAnimationOnClick();
92
+ newComponent.animation = animationComponent;
93
+ newComponent.stateName = animationComponent.clip.name;
94
+ newComponent.trigger = "start";
95
+ newComponent.name = "PlayAnimationOnClick_implicitAtStart_" + newComponent.stateName;
96
+ const go = new Object3D();
97
+ GameObject.addComponent(go, newComponent);
98
+ constructedObjects.push(go);
89
99
 
90
- for (const clip of animationComponent.animations) {
91
- if (!clips.includes(clip))
92
- clips.push(clip);
100
+ // the behaviour can be anywhere in the hierarchy
101
+ root.add(go);
93
102
  }
103
+ }
104
+ else {
105
+ for (const animationComponent of animationComponents) {
106
+ if (debug)
107
+ console.log(animationComponent);
94
108
 
95
- animationClips.push({ root: animationComponent.gameObject, clips: clips });
109
+ const clips: AnimationClip[] = [];
110
+
111
+ for (const clip of animationComponent.animations) {
112
+ if (!clips.includes(clip))
113
+ clips.push(clip);
114
+ }
115
+
116
+ animationClips.push({ root: animationComponent.gameObject, clips: clips });
117
+ }
96
118
  }
97
119
 
98
- if (debug) console.log("USDZ Animation Clips", animationClips);
120
+ if (debug && animationClips?.length > 0) console.log("USDZ Animation Clips without behaviours", animationClips);
99
121
 
100
122
  for (const pair of animationClips) {
101
123
  for (const clip of pair.clips)
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { getParam } from "../../../../../engine/engine_utils.js";
7
7
  import type { State } from "../../../../../engine/extensions/NEEDLE_animator_controller_model.js";
8
8
  import { NEEDLE_progressive } from "../../../../../engine/extensions/NEEDLE_progressive.js";
9
+ import { Animation } from "../../../../Animation.js";
9
10
  import { Animator } from "../../../../Animator.js";
10
11
  import { AudioSource } from "../../../../AudioSource.js";
11
12
  import { Behaviour, GameObject } from "../../../../Component.js";
@@ -155,7 +156,7 @@
155
156
  if (this.target && this.object && this.gameObject) {
156
157
  const moveForward = new BehaviorModel("Move to " + this.target?.name,
157
158
  TriggerBuilder.tapTrigger(this.gameObject),
158
- ActionBuilder.transformAction(this.object, this.target, this.duration, this.relativeMotion ? Space.Relative : Space.Absolute),
159
+ ActionBuilder.transformAction(this.object, this.target, this.duration, this.relativeMotion ? "relative" : "absolute"),
159
160
  );
160
161
  ext.addBehavior(moveForward);
161
162
  }
@@ -582,7 +583,7 @@
582
583
  duration: number = 0.5;
583
584
 
584
585
  @serializable()
585
- motionType: MotionType = MotionType.bounce;
586
+ motionType: MotionType = "bounce";
586
587
 
587
588
  beforeCreateDocument() { }
588
589
 
@@ -661,7 +662,7 @@
661
662
  const playbackTarget = this.target ? this.target.gameObject : this.gameObject;
662
663
  const clipName = AudioExtension.getName(clipUrl);
663
664
  const volume = this.target ? this.target.volume : 1;
664
- const auralMode = this.target && this.target.spatialBlend == 0 ? AuralMode.NonSpatial : AuralMode.Spatial;
665
+ const auralMode = this.target && this.target.spatialBlend == 0 ? "nonSpatial" : "spatial";
665
666
 
666
667
  // This checks if any child is clickable – if yes, the tap trigger is added; if not, we omit it.
667
668
  let anyChildHasGeometry = false;
@@ -673,9 +674,9 @@
673
674
  anyChildHasGeometry = true;
674
675
  if (anyChildHasGeometry)
675
676
  {
676
- let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, PlayAction.Play, volume, auralMode);
677
+ let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, "play", volume, auralMode);
677
678
  // does not seem to work in iOS / QuickLook...
678
- if (this.toggleOnClick) (playAction as ActionModel).multiplePerformOperation = MultiplePerformOperation.Stop;
679
+ if (this.toggleOnClick) (playAction as ActionModel).multiplePerformOperation = "stop";
679
680
  if (this.target && this.target.loop)
680
681
  playAction = ActionBuilder.sequence(playAction).makeLooping();
681
682
  const playClipOnTap = new BehaviorModel("playAudio " + this.name,
@@ -687,7 +688,7 @@
687
688
 
688
689
  // automatically play audio on start too if the referenced AudioSource has playOnAwake enabled
689
690
  if (this.target && this.target.playOnAwake && this.target.enabled) {
690
- let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, PlayAction.Play, volume, auralMode);
691
+ let playAction: IBehaviorElement = ActionBuilder.playAudioAction(playbackTarget, "audio/" + clipName, "play", volume, auralMode);
691
692
  if (this.target.loop)
692
693
  playAction = ActionBuilder.sequence(playAction).makeLooping();
693
694
  const playClipOnStart = new BehaviorModel("playAudioOnStart" + (this.name ? "_" + this.name : ""),
@@ -730,8 +731,9 @@
730
731
  // we want to expose this once we have a nice drawer for "Triggers" (e.g. shows proximity distance)
731
732
  // and once we rename the component to "PlayAnimation" or "PlayAnimationOnTrigger"
732
733
  trigger: "tap" | "start" = "tap"; // "proximity"
734
+ animation?: Animation;
733
735
 
734
- private get target() { return this.animator?.gameObject }
736
+ private get target() { return this.animator?.gameObject || this.animation?.gameObject }
735
737
 
736
738
  start(): void {
737
739
  ensureRaycaster(this.gameObject);
@@ -775,7 +777,7 @@
775
777
  }
776
778
 
777
779
  afterCreateDocument(ext: BehaviorExtension, context: USDZExporterContext) {
778
- if (!this.animationSequence || !this.stateAnimationModel) return;
780
+ if ((this.animationSequence === undefined && this.animationLoopAfterSequence === undefined) || !this.stateAnimationModel) return;
779
781
  if (!this.target) return;
780
782
 
781
783
  const document = context.document;
@@ -842,8 +844,18 @@
842
844
  }
843
845
 
844
846
  createAnimation(ext: AnimationExtension, model: USDObject, _context: USDZExporterContext) {
845
- if (!this.target || !this.animator) return;
847
+ if (!this.target || (!this.animator && !this.animation)) return;
846
848
 
849
+ this.stateAnimationModel = model;
850
+
851
+ if (this.animation && this.animation.clip) {
852
+ this.animationSequence = new Array<RegisteredAnimationInfo>();
853
+ this.animationLoopAfterSequence = new Array<RegisteredAnimationInfo>();
854
+ const anim = ext.registerAnimation(this.target, this.animation.clip);
855
+ if (anim) this.animationLoopAfterSequence.push(anim);
856
+ return;
857
+ }
858
+
847
859
  // If there's a separate state specified to play after this one, we
848
860
  // play it automatically. Theoretically an animator state machine flow could be encoded here.
849
861
 
@@ -918,7 +930,6 @@
918
930
  console.warn("No clips found for state " + this.stateName + " on " + this.animator?.name + ", can't export animation data");
919
931
  return;
920
932
  }
921
- this.stateAnimationModel = model;
922
933
 
923
934
  const addStateToSequence = (state: State, sequence: Array<RegisteredAnimationInfo>) => {
924
935
  if (!this.target) return;
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Object3D } from "three";
2
2
 
3
- import { getParam } from "../../../../../engine/engine_utils.js";
3
+ import { EnumToPrimitiveUnion, getParam } from "../../../../../engine/engine_utils.js";
4
4
  import { makeNameSafeForUSD,USDDocument, USDObject, USDWriter } from "../../ThreeUSDZExporter.js";
5
5
  import { BehaviorExtension } from "./Behaviour.js";
6
6
 
@@ -34,8 +34,6 @@
34
34
  writeTo(_ext: BehaviorExtension, document: USDDocument, writer: USDWriter) {
35
35
  if (!this.trigger || !this.action) return;
36
36
  writer.beginBlock(`def Preliminary_Behavior "${this.id}"`);
37
- writer.appendLine(`rel actions = <${this.action.id}>`);
38
- writer.appendLine(`uniform bool exclusive = ${this.exclusive}`);
39
37
  let triggerString = "";
40
38
  if (Array.isArray(this.trigger)) {
41
39
  triggerString = "[";
@@ -50,6 +48,8 @@
50
48
  triggerString = `<${this.trigger.id}>`;
51
49
 
52
50
  writer.appendLine(`rel triggers = ${triggerString} `);
51
+ writer.appendLine(`rel actions = <${this.action.id}>`);
52
+ writer.appendLine(`uniform bool exclusive = ${this.exclusive}`);
53
53
  writer.appendLine();
54
54
  if (Array.isArray(this.trigger)) {
55
55
  for (const trigger of this.trigger) {
@@ -197,6 +197,7 @@
197
197
 
198
198
  makeLooping() {
199
199
  this.loops = 1;
200
+ this.performCount = 0;
200
201
  return this;
201
202
  }
202
203
 
@@ -217,8 +218,8 @@
217
218
  writer.appendLine();
218
219
 
219
220
  writer.appendLine(`token info:id = "Group"`);
220
- writer.appendLine(`bool loops = ${this.loops} `);
221
- writer.appendLine(`int performCount = ${this.performCount} `);
221
+ writer.appendLine(`bool loops = ${this.loops > 0 ? "true" : "false" } `);
222
+ writer.appendLine(`int performCount = ${Math.max(0, this.performCount)} `);
222
223
  writer.appendLine(`token type = "${this.type}"`);
223
224
  writer.appendLine();
224
225
 
@@ -232,29 +233,22 @@
232
233
  }
233
234
  }
234
235
 
235
- export enum MotionType {
236
- pop = 0,
237
- blink = 1,
238
- bounce = 2,
239
- flip = 3,
240
- float = 4,
241
- jiggle = 5,
242
- pulse = 6,
243
- spin = 7,
244
- };
236
+ /** @internal */
237
+ export type MotionType = "pop" | "blink" | "bounce" | "flip" | "float" | "jiggle" | "pulse" | "spin";
245
238
 
246
- export enum Space {
247
- Relative = "relative",
248
- Absolute = "absolute"
249
- };
239
+ /** @internal */
240
+ export type MotionStyle = "basic";
250
241
 
242
+ /** @internal */
243
+ export type Space = "relative" | "absolute";
244
+
251
245
  // https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar/actions_and_triggers/preliminary_action/multipleperformoperation
252
- export enum MultiplePerformOperation {
253
- Allow = "allow",
254
- Ignore = "ignore",
255
- Stop = "stop",
256
- }
246
+ /** @internal */
247
+ export type MultiplePerformOperation = "allow" | "ignore" | "stop";
257
248
 
249
+ /** @internal */
250
+ export type EaseType = "none" | "in" | "out" | "inout";
251
+
258
252
  export class ActionModel implements IBehaviorElement {
259
253
 
260
254
  private static global_id: number = 0;
@@ -262,12 +256,12 @@
262
256
  id: string;
263
257
  tokenId?: string;
264
258
  affectedObjects?: string | Target;
265
- easeType?: string;;
266
- motionType?: string;
259
+ easeType?: EaseType;;
260
+ motionType?: MotionType;
267
261
  duration?: number;
268
262
  moveDistance?: number;
269
- style?: string;
270
- type?: string;
263
+ style?: MotionStyle;
264
+ type?: Space | string; // combined types of different actions
271
265
  front?: Vec3;
272
266
  up?: Vec3;
273
267
  start?: number;
@@ -277,8 +271,8 @@
277
271
  xFormTarget?: Target | string;
278
272
  audio?: string;
279
273
  gain?: number;
280
- auralMode?: string;
281
- multiplePerformOperation?: string;
274
+ auralMode?: AuralMode;
275
+ multiplePerformOperation?: MultiplePerformOperation;
282
276
 
283
277
  clone(): ActionModel {
284
278
  const copy = new ActionModel();
@@ -389,18 +383,9 @@
389
383
  }
390
384
  }
391
385
 
392
- export enum PlayAction {
393
- Play = "play",
394
- Pause = "pause",
395
- Stop = "stop",
396
- }
386
+ export type PlayAction = "play" | "pause" | "stop";
387
+ export type AuralMode = "spatial" | "nonSpatial" | "ambient";
397
388
 
398
- export enum AuralMode {
399
- Spatial = "spatial",
400
- NonSpatial = "nonSpatial",
401
- Ambient = "ambient",
402
- }
403
-
404
389
  export class ActionBuilder {
405
390
 
406
391
  static sequence(...params: IBehaviorElement[]) {
@@ -420,7 +405,7 @@
420
405
  act.duration = duration;
421
406
 
422
407
  act.style = "basic";
423
- act.motionType = "none";
408
+ act.motionType = undefined;
424
409
  act.moveDistance = 0;
425
410
  act.easeType = "none";
426
411
  return act;
@@ -441,7 +426,7 @@
441
426
  act.animationSpeed = animationSpeed;
442
427
  act.reversed = reversed;
443
428
  act.pingPong = pingPong;
444
- act.multiplePerformOperation = MultiplePerformOperation.Allow;
429
+ act.multiplePerformOperation = "allow";
445
430
  if (reversed) {
446
431
  act.start -= duration;
447
432
  //console.warn("Reversed animation does currently not work. The resulting file will most likely not playback.", act.id, targetObject);
@@ -479,22 +464,22 @@
479
464
  return act;
480
465
  }
481
466
 
482
- static emphasize(targets: Target, duration: number, motionType: MotionType = MotionType.bounce, moveDistance: number = 1, style: string = "basic") {
467
+ static emphasize(targets: Target, duration: number, motionType: MotionType = "bounce", moveDistance: number = 1, style: MotionStyle = "basic") {
483
468
  const act = new ActionModel(targets);
484
469
  act.tokenId = "Emphasize";
485
470
  act.duration = duration;
486
471
  act.style = style ?? "basic";
487
- act.motionType = MotionType[motionType];
472
+ act.motionType = motionType;
488
473
  act.moveDistance = moveDistance;
489
474
  return act;
490
475
  }
491
476
 
492
- static transformAction(targets: Target, transformTarget: Target, duration: number, transformType: Space, easeType: string = "inout") {
477
+ static transformAction(targets: Target, transformTarget: Target, duration: number, transformType: Space, easeType: EaseType = "inout") {
493
478
  const act = new ActionModel(targets);
494
479
  act.tokenId = "Transform";
495
480
  act.duration = duration;
496
481
  act.type = transformType;
497
- act.easeType = easeType;
482
+ act.easeType = duration > 0 ? easeType : "none";
498
483
  if (Array.isArray(transformTarget)) {
499
484
  console.error("Transform target must not be an array", transformTarget);
500
485
  }
@@ -502,14 +487,14 @@
502
487
  return act;
503
488
  }
504
489
 
505
- static playAudioAction(targets: Target, audio: string, type: PlayAction = PlayAction.Play, gain: number = 1, auralMode: AuralMode = AuralMode.Spatial) {
490
+ static playAudioAction(targets: Target, audio: string, type: PlayAction = "play", gain: number = 1, auralMode: AuralMode = "spatial") {
506
491
  const act = new ActionModel(targets);
507
492
  act.tokenId = "Audio";
508
493
  act.type = type;
509
494
  act.audio = audio;
510
495
  act.gain = gain;
511
496
  act.auralMode = auralMode;
512
- act.multiplePerformOperation = MultiplePerformOperation.Allow;
497
+ act.multiplePerformOperation = "allow";
513
498
  return act;
514
499
  }
515
500
 
src/engine-components/postprocessing/Effects/ColorAdjustments.ts CHANGED
@@ -58,7 +58,7 @@
58
58
 
59
59
  unapply() {
60
60
  // reset to the current value in the toneMappingEffect if that exists, else Linear
61
- const currentMode: any = this.toneMappingEffect?.mode.value;
61
+ const currentMode: any = this.toneMappingEffect?.mode?.value;
62
62
  const newMode = this.toneMappingEffect?.getThreeToneMapping(currentMode)
63
63
  this.context.renderer.toneMapping = newMode ?? LinearToneMapping;
64
64
  }
@@ -83,7 +83,7 @@
83
83
 
84
84
  // workaround: find the ToneMapping effect in the scene so we can apply the mode
85
85
  this.toneMappingEffect = GameObject.findObjectOfType(Volume)?.sharedProfile?.components.find(c => c.typeName === "ToneMapping") as ToneMapping | null;
86
- const currentMode: any = this.toneMappingEffect?.mode.value;
86
+ const currentMode: any = this.toneMappingEffect?.mode?.value;
87
87
  const expectedThreeMode = this.toneMappingEffect?.getThreeToneMapping(currentMode);
88
88
 
89
89
  // We need this effect if someone uses ACES or AgX tonemapping;
@@ -99,7 +99,7 @@
99
99
  this.context.renderer.toneMappingExposure = v;
100
100
 
101
101
  // this is a workaround so that we can apply tonemapping options – no access to the ToneMappingEffect instance from the Tonemapping effect right now...
102
- const currentMode = this.toneMappingEffect?.mode.value;
102
+ const currentMode = this.toneMappingEffect?.mode?.value;
103
103
  const threeMode = this.toneMappingEffect?.getThreeToneMapping(currentMode);
104
104
  const mappedMode = this.threeToneMappingToEffectMode(threeMode);
105
105
  if (mappedMode !== undefined)
src/engine-components/codegen/components.ts CHANGED
@@ -118,6 +118,7 @@
118
118
  export { ParticleSubEmitter } from "../ParticleSystemSubEmitter.js";
119
119
  export { ParticleSystem } from "../ParticleSystem.js";
120
120
  export { ParticleSystemRenderer } from "../ParticleSystem.js";
121
+ export { PhysicsExtension } from "../export/usdz/extensions/behavior/PhysicsExtension.js";
121
122
  export { PixelationEffect } from "../postprocessing/Effects/Pixelation.js";
122
123
  export { PlayableDirector } from "../timeline/PlayableDirector.js";
123
124
  export { PlayAnimationOnClick } from "../export/usdz/extensions/behavior/BehaviourComponents.js";
src/engine/engine_context.ts CHANGED
@@ -977,6 +977,7 @@
977
977
  }
978
978
  const file = files[i];
979
979
  if (!file.includes(".glb") && !file.includes(".gltf")) {
980
+ // TODO this may not be true if the URL just forwards to a proper GLB file resource
980
981
  const warning = `Needle Engine: found suspicious src "${file}"`;
981
982
  console.warn(warning);
982
983
  if (isLocalNetwork()) showBalloonWarning(warning);
src/engine/engine_element_attributes.ts CHANGED
@@ -55,6 +55,7 @@
55
55
 
56
56
  /**
57
57
  * Available attributes for the `<needle-engine>` web component
58
+ * @inheritdoc
58
59
  */
59
60
  export type NeedleEngineAttributes =
60
61
  MainAttributes
src/engine/engine_element.ts CHANGED
@@ -39,8 +39,17 @@
39
39
 
40
40
  // https://developers.google.com/web/fundamentals/web-components/customelements
41
41
 
42
- /** <needle-engine> web component. See {@link NeedleEngineAttributes} attributes for supported attributes
43
- * @type {import ("./engine_element_attributes.js").NeedleEngineAttributes}
42
+ /**
43
+ * <needle-engine> web component. See {@link NeedleEngineAttributes} attributes for supported attributes
44
+ * The needle engine web component creates and manages a needle engine context which is responsible for rendering a 3D scene using threejs.
45
+ * The needle engine context is created when the src attribute is set and disposed when the needle engine is removed from the document (you can prevent this by setting the keep-alive attribute to true).
46
+ * The needle engine context is accessible via the context property on the needle engine element (e.g. document.querySelector("needle-engine").context).
47
+ * @link https://engine.needle.tools/docs/reference/needle-engine-attributes
48
+ *
49
+ * @example
50
+ * <needle-engine src="https://example.com/scene.glb"></needle-engine>
51
+ * @example
52
+ * <needle-engine src="https://example.com/scene.glb" camera-controls="false"></needle-engine>
44
53
  */
45
54
  export class NeedleEngineHTMLElement extends HTMLElement implements INeedleEngineComponent {
46
55
 
@@ -72,6 +81,11 @@
72
81
  return true;
73
82
  }
74
83
 
84
+ /**
85
+ * Get the current context for this web component instance. The context is created when the src attribute is set and the loading has finished.
86
+ * The context is disposed when the needle engine is removed from the document (you can prevent this by setting the keep-alive attribute to true).
87
+ * @returns {Promise<Context>} a promise that resolves to the context when the loading has finished
88
+ */
75
89
  public getContext(): Promise<Context> {
76
90
  return new Promise((res, _rej) => {
77
91
  if (this._context && this.loadingFinished) {
@@ -89,15 +103,11 @@
89
103
  });
90
104
  }
91
105
 
106
+ /**
107
+ * Get the context that is created when the src attribute is set and the loading has finished.
108
+ */
92
109
  public get context() { return this._context; }
93
110
 
94
- /**@obsolete use context */
95
- public get Context() { return this._context; }
96
- /**@obsolete use Needle.GameObject */
97
- private gameObject = GameObject;
98
- /**@obsolete use Needle.GameObject */
99
- private GameObject = GameObject;
100
-
101
111
  private _context: Context;
102
112
  private _overlay_ar: AROverlayHandler;
103
113
  private _loadingProgress01: number = 0;
@@ -184,6 +194,9 @@
184
194
  }
185
195
 
186
196
 
197
+ /**
198
+ * @internal
199
+ */
187
200
  async connectedCallback() {
188
201
  if (debug) {
189
202
  console.log("<needle-engine> connected");
@@ -218,6 +231,9 @@
218
231
  }
219
232
  }
220
233
 
234
+ /**
235
+ * @internal
236
+ */
221
237
  disconnectedCallback() {
222
238
  this.removeEventListener("xr-session-started", this.onXRSessionStarted);
223
239
 
@@ -238,6 +254,9 @@
238
254
  }
239
255
  }
240
256
 
257
+ /**
258
+ * @internal
259
+ */
241
260
  attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
242
261
  if (debug) console.log("attributeChangedCallback", name, _oldValue, newValue);
243
262
  switch (name) {
@@ -508,10 +527,16 @@
508
527
  }
509
528
  }
510
529
 
530
+ /**
531
+ * @internal
532
+ */
511
533
  getAROverlayContainer(): HTMLElement {
512
534
  return this._overlay_ar.createOverlayContainer(this);
513
535
  }
514
536
 
537
+ /**
538
+ * @internal
539
+ */
515
540
  getVROverlayContainer(): HTMLElement | null {
516
541
  for (let i = 0; i < this.children.length; i++) {
517
542
  const ch = this.children[i] as HTMLElement;
@@ -521,6 +546,9 @@
521
546
  return null;
522
547
  }
523
548
 
549
+ /**
550
+ * @internal
551
+ */
524
552
  onEnterAR(session: XRSession) {
525
553
  this.onSetupAR();
526
554
  const overlayContainer = this.getAROverlayContainer();
@@ -528,17 +556,26 @@
528
556
  this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
529
557
  }
530
558
 
559
+ /**
560
+ * @internal
561
+ */
531
562
  onExitAR(session: XRSession) {
532
563
  this._overlay_ar.onEnd(this._context!);
533
564
  this.onSetupDesktop();
534
565
  this.dispatchEvent(new CustomEvent("exit-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
535
566
  }
536
567
 
568
+ /**
569
+ * @internal
570
+ */
537
571
  onEnterVR(session: XRSession) {
538
572
  this.onSetupVR();
539
573
  this.dispatchEvent(new CustomEvent("enter-vr", { detail: { session: session, context: this._context } }));
540
574
  }
541
575
 
576
+ /**
577
+ * @internal
578
+ */
542
579
  onExitVR(session: XRSession) {
543
580
  this.onSetupDesktop();
544
581
  this.dispatchEvent(new CustomEvent("exit-vr", { detail: { session: session, context: this._context } }));
@@ -628,16 +665,6 @@
628
665
 
629
666
 
630
667
 
631
-
632
-
633
- function getHashFromString(str: string) {
634
- let hash = 0;
635
- for (let i = 0; i < str.length; i++) {
636
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
637
- }
638
- return hash;
639
- }
640
-
641
668
  function getDisplayName(str: string) {
642
669
  const parts = str.split("/");
643
670
  let name = parts[parts.length - 1];
src/engine/engine_hot_reload.ts CHANGED
@@ -27,11 +27,12 @@
27
27
 
28
28
  const instances: Map<string, object[]> = new Map();
29
29
 
30
- /** true during hot reload, can be used to modify behaviour in onEnable and onDisable */
30
+ /** @internal true during hot reload, can be used to modify behaviour in onEnable and onDisable */
31
31
  export function isHotReloading() {
32
32
  return isApplyingChanges;
33
33
  }
34
34
 
35
+ /** @internal */
35
36
  export function registerHotReloadType(instance: object) {
36
37
  if (isApplyingChanges) return;
37
38
  const type = instance.constructor;
@@ -44,6 +45,7 @@
44
45
  }
45
46
  }
46
47
 
48
+ /** @internal */
47
49
  export function unregisterHotReloadType(instance: object) {
48
50
  if (isApplyingChanges) return;
49
51
  const type = instance.constructor;
src/engine/engine_input.ts CHANGED
@@ -223,9 +223,14 @@
223
223
  * @param callback The callback to call when the event is triggered
224
224
  * @param options The options for adding the event listener
225
225
  */
226
- addEventListener(type: InputEvents | InputEventNames, callback: PointerEventListener, options?: EventListenerOptions): void {
226
+ addEventListener(type: InputEvents | InputEventNames, callback: InputEventListener, options?: EventListenerOptions): void {
227
227
  if (!this._eventListeners[type]) this._eventListeners[type] = [];
228
228
 
229
+ if (!callback || typeof callback !== "function") {
230
+ console.error("Invalid call to addEventListener: callback is required and must be a function!");
231
+ return;
232
+ }
233
+
229
234
  if (!options) options = {};
230
235
  // create a copy of the options object to avoid the original object being modified
231
236
  else options = { ...options };
@@ -248,8 +253,9 @@
248
253
  * @param callback The callback to remove
249
254
  * @param options The options for removing the event listener
250
255
  */
251
- removeEventListener(type: InputEvents | InputEventNames, callback: PointerEventListener, options?: EventListenerOptions): void {
256
+ removeEventListener(type: InputEvents | InputEventNames, callback: InputEventListener, options?: EventListenerOptions): void {
252
257
  if (!this._eventListeners[type]) return;
258
+ if (!callback) return;
253
259
  const listeners = this._eventListeners[type];
254
260
  // if a specific queue is requested the callback should only be removed from that queue
255
261
  if (options?.queue != undefined) {
src/engine/engine_mainloop_utils.ts CHANGED
@@ -17,10 +17,12 @@
17
17
  // so we use this copy buffer
18
18
  const new_scripts_buffer: any[] = [];
19
19
 
20
+ /** @internal */
20
21
  export function hasNewScripts() {
21
22
  return new_scripts_buffer.length > 0;
22
23
  }
23
24
 
25
+ /** @internal */
24
26
  export function processNewScripts(context: IContext) {
25
27
  if (context.new_scripts.length <= 0) return;
26
28
  if (debug)
@@ -162,12 +164,14 @@
162
164
  context.new_scripts_post_setup_callbacks.length = 0;
163
165
  }
164
166
 
167
+ /** @internal */
165
168
  export function processRemoveFromScene(script: IComponent) {
166
169
  if (!script) return;
167
170
  script.__internalDisable(true);
168
171
  removeScriptFromContext(script, script.context);
169
172
  }
170
173
 
174
+ /** @internal */
171
175
  export function processStart(context: IContext, object?: Object3D) {
172
176
  // Call start on scripts
173
177
  for (let i = 0; i < context.new_script_start.length; i++) {
@@ -199,6 +203,7 @@
199
203
  }
200
204
 
201
205
 
206
+ /** @internal */
202
207
  export function addScriptToArrays(script: any, context: IContext) {
203
208
  // TODO: not sure if this is ideal - maybe we should add a map if we have many scripts?
204
209
  const index = context.scripts.indexOf(script);
@@ -216,6 +221,7 @@
216
221
  if (isNeedleXRSessionEventReceiver(script, "immersive-ar")) context.scripts_immersive_ar.push(script);
217
222
  }
218
223
 
224
+ /** @internal */
219
225
  export function removeScriptFromContext(script: any, context: IContext) {
220
226
  removeFromArray(script, context.new_scripts);
221
227
  removeFromArray(script, context.new_script_start);
@@ -237,6 +243,7 @@
237
243
  if (index >= 0) array.splice(index, 1);
238
244
  }
239
245
 
246
+ /** @internal */
240
247
  export function isNeedleXRSessionEventReceiver(script: any, mode: XRSessionMode | null): script is INeedleXRSessionEventReceiver {
241
248
  if (script) {
242
249
  const i = script as Partial<INeedleXRSessionEventReceiver>;
@@ -257,6 +264,7 @@
257
264
  }
258
265
 
259
266
 
267
+ /** @internal */
260
268
  export function updateIsActive(obj?: Object3D) {
261
269
  if (!obj) obj = ContextRegistry.Current.scene;
262
270
  if (!obj) {
@@ -370,6 +378,7 @@
370
378
  // temporyChildArrayBuffer.push(arr);
371
379
  // }
372
380
 
381
+ /** @internal */
373
382
  export function updateActiveInHierarchyWithoutEventCall(go: Object3D) {
374
383
  let activeInHierarchy = true;
375
384
  let current: Object3D | null = go;
@@ -404,6 +413,7 @@
404
413
  const $waitingForPrewarm = Symbol("waitingForPrewarm");
405
414
  const debugPrewarm = getParam("debugprewarm");
406
415
 
416
+ /** @internal */
407
417
  export function registerPrewarmObject(obj: Object3D, context: IContext) {
408
418
  if (!obj) return;
409
419
  // allow objects to be marked as prewarmed in which case we dont need to register them again
@@ -421,7 +431,7 @@
421
431
  let prewarmTarget: WebGLCubeRenderTarget | null = null;
422
432
  let prewarmCamera: CubeCamera | null = null;
423
433
 
424
- // called by the engine to remove scroll or animation hiccup when objects are rendered/compiled for the first time
434
+ /** @internal called by the engine to remove scroll or animation hiccup when objects are rendered/compiled for the first time */
425
435
  export function runPrewarm(context: IContext) {
426
436
  if (!context) return;
427
437
  const list = prewarmList.get(context);
@@ -447,6 +457,7 @@
447
457
  }
448
458
  }
449
459
 
460
+ /** @internal */
450
461
  export function clearPrewarmList(context: IContext) {
451
462
  const list = prewarmList.get(context);
452
463
  if (list) {
src/engine/engine_networking_files.ts CHANGED
@@ -45,7 +45,7 @@
45
45
  export async function addFile(file: File, context: Context, backendUrl?: string): Promise<GLTF | null> {
46
46
 
47
47
  const name = file.name;
48
- if (name.endsWith(".gltf") || name.endsWith(".glb")) {
48
+ if (name.endsWith(".gltf") || name.endsWith(".glb") || file.type === "model/gltf+json" || file.type === "model/gltf-binary") {
49
49
  return new Promise((resolve, _reject) => {
50
50
  const reader = new FileReader()
51
51
  reader.readAsArrayBuffer(file);
src/engine/engine_time_utils.ts CHANGED
@@ -155,7 +155,7 @@
155
155
  this.parentDepth = this.parentScope.parentDepth + 1;
156
156
  }
157
157
  this.scopeLabel = " ".repeat(this.parentDepth * 2) + scope;
158
- this.showLogs = options?.logTimings ?? showProgressLogs !== undefined;
158
+ this.showLogs = options?.logTimings ?? !!showProgressLogs;
159
159
  if (this.showLogs) console.time(this.scopeLabel);
160
160
  this.onProgress = options?.onProgress;
161
161
  }
src/engine/assets/index.ts CHANGED
@@ -3,22 +3,22 @@
3
3
 
4
4
 
5
5
  const logoSvgString = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 160 187.74"><defs><linearGradient id="a" x1="89.64" y1="184.81" x2="90.48" y2="21.85" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#62d399"/><stop offset=".51" stop-color="#acd842"/><stop offset=".9" stop-color="#d7db0a"/></linearGradient><linearGradient id="b" x1="69.68" y1="178.9" x2="68.08" y2="16.77" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0ba398"/><stop offset=".5" stop-color="#4ca352"/><stop offset="1" stop-color="#76a30a"/></linearGradient><linearGradient id="c" x1="36.6" y1="152.17" x2="34.7" y2="84.19" gradientUnits="userSpaceOnUse"><stop offset=".19" stop-color="#36a382"/><stop offset=".54" stop-color="#49a459"/><stop offset="1" stop-color="#76a30b"/></linearGradient><linearGradient id="d" x1="15.82" y1="153.24" x2="18" y2="90.86" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#267880"/><stop offset=".51" stop-color="#457a5c"/><stop offset="1" stop-color="#717516"/></linearGradient><linearGradient id="e" x1="135.08" y1="135.43" x2="148.93" y2="63.47" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0d939"/><stop offset="1" stop-color="#eadb04"/></linearGradient><linearGradient id="f" x1="-4163.25" y1="2285.12" x2="-4160.81" y2="2215.34" gradientTransform="rotate(20 4088.49 13316.712)" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#74af52"/><stop offset=".48" stop-color="#99be32"/><stop offset="1" stop-color="#c0c40a"/></linearGradient><symbol id="g" viewBox="0 0 160 187.74"><path style="fill:url(#a)" d="M79.32 36.98v150.76L95 174.54l6.59-156.31-22.27 18.75z"/><path style="fill:url(#b)" d="M79.32 36.98 57.05 18.23l6.59 156.31 15.68 13.2V36.98z"/><path style="fill:url(#c)" d="m25.19 104.83 8.63 49.04 12.5-14.95-2.46-56.42-18.67 22.33z"/><path style="fill:url(#d)" d="M25.19 104.83 0 90.24l16.97 53.86 16.85 9.77-8.63-49.04z"/><path style="fill:#9c3" d="M43.86 82.5 18.69 67.98 0 90.24l25.18 14.59L43.86 82.5z"/><path style="fill:url(#e)" d="m134.82 78.69-9.97 56.5 15.58-9.04L160 64.1l-25.18 14.59z"/><path style="fill:url(#f)" d="m134.82 78.69-18.68-22.33-2.86 65 11.57 13.83 9.97-56.5z"/><path style="fill:#ffe113" d="m160 64.1-18.69-22.26-25.17 14.52 18.67 22.33L160 64.1z"/><path style="fill:#f3e600" d="M101.59 18.23 79.32 0 57.05 18.23l22.27 18.75 22.27-18.75z"/></symbol></defs><use width="160" height="187.74" xlink:href="#g"/></svg>`;
6
- const logoSvgBlob = new Blob([logoSvgString], { type: "image/svg+xml;charset=utf-8" });
7
- const logoSvgUrl = URL.createObjectURL(logoSvgBlob);
6
+ const logoSvgBlob = btoa(logoSvgString);
7
+ const logoSvgUrl = "data:image/svg+xml;base64," + logoSvgBlob;
8
8
  /** Logo Only */
9
9
  export const needleLogoOnlySVG: string = logoSvgUrl;
10
10
 
11
11
 
12
12
  const madeWithNeedleSvgString = `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'> <svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 1014 282" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m665.95 132.73v44.88l-10.56-8.4c-0.8-0.64-1.2-1.44-1.2-2.4v-32.4c0-6.48-4.12-9.72-12.36-9.72-2.16 0-4.18 0.4-6.06 1.2s-3.54 1.8-4.98 3-2.56 2.5-3.36 3.9-1.2 2.7-1.2 3.9v40.92l-10.68-8.4c-0.72-0.64-1.08-1.44-1.08-2.4v-53.76l10.92 8.52c0.32 0.24 0.56 0.44 0.72 0.6s0.36 0.32 0.6 0.48c0.96-1.2 2.14-2.28 3.54-3.24s2.92-1.76 4.56-2.4 3.34-1.14 5.1-1.5 3.44-0.54 5.04-0.54c1.44 0 2.92 0.04 4.44 0.12s2.84 0.28 3.96 0.6c4.56 1.12 7.8 3.12 9.72 6s2.88 6.56 2.88 11.04z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m732.38 146.05c0 0.88 0.02 1.5 0.06 1.86s-0.02 0.98-0.18 1.86h-7.08c-2.08 0-4.44-0.02-7.08-0.06s-5.36-0.06-8.16-0.06h-22.08c0 2.88 0.56 5.36 1.68 7.44s2.6 3.8 4.44 5.16 3.94 2.36 6.3 3 4.74 0.96 7.14 0.96c3.04 0 5.9-0.76 8.58-2.28s4.94-3.52 6.78-6c0.64 0.56 1.54 1.48 2.7 2.76s2.94 3.2 5.34 5.76c-2.8 3.36-6.22 6.02-10.26 7.98s-8.42 2.94-13.14 2.94-8.92-0.64-12.84-1.92-7.32-3.24-10.2-5.88-5.12-5.98-6.72-10.02-2.4-8.82-2.4-14.34c0-4.96 0.66-9.42 1.98-13.38s3.22-7.32 5.7-10.08 5.44-4.9 8.88-6.42 7.32-2.28 11.64-2.28c5.76 0 10.52 0.88 14.28 2.64s6.72 4.16 8.88 7.2 3.66 6.54 4.5 10.5 1.26 8.18 1.26 12.66zm-29.4-22.8c-2.16 0.16-4.16 0.72-6 1.68s-3.42 2.2-4.74 3.72-2.36 3.28-3.12 5.28-1.14 4.12-1.14 6.36h33.12c0-2-0.22-4.06-0.66-6.18s-1.3-4.02-2.58-5.7-3.1-3.02-5.46-4.02-5.5-1.38-9.42-1.14z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m795.93 146.05c0 0.88 0.02 1.5 0.06 1.86s-0.02 0.98-0.18 1.86h-7.08c-2.08 0-4.44-0.02-7.08-0.06s-5.36-0.06-8.16-0.06h-22.08c0 2.88 0.56 5.36 1.68 7.44s2.6 3.8 4.44 5.16 3.94 2.36 6.3 3 4.74 0.96 7.14 0.96c3.04 0 5.9-0.76 8.58-2.28s4.94-3.52 6.78-6c0.64 0.56 1.54 1.48 2.7 2.76s2.94 3.2 5.34 5.76c-2.8 3.36-6.22 6.02-10.26 7.98s-8.42 2.94-13.14 2.94-8.92-0.64-12.84-1.92-7.32-3.24-10.2-5.88-5.12-5.98-6.72-10.02-2.4-8.82-2.4-14.34c0-4.96 0.66-9.42 1.98-13.38s3.22-7.32 5.7-10.08 5.44-4.9 8.88-6.42 7.32-2.28 11.64-2.28c5.76 0 10.52 0.88 14.28 2.64s6.72 4.16 8.88 7.2 3.66 6.54 4.5 10.5 1.26 8.18 1.26 12.66zm-29.4-22.8c-2.16 0.16-4.16 0.72-6 1.68s-3.42 2.2-4.74 3.72-2.36 3.28-3.12 5.28-1.14 4.12-1.14 6.36h33.12c0-2-0.22-4.06-0.66-6.18s-1.3-4.02-2.58-5.7-3.1-3.02-5.46-4.02-5.5-1.38-9.42-1.14z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m858.57 97.21c0.64 0.48 0.96 1.16 0.96 2.04v74.88c-0.08 1.04-0.12 2.12-0.12 3.24-1.84-1.52-3.56-2.92-5.16-4.2-1.36-1.12-2.66-2.18-3.9-3.18s-2.06-1.66-2.46-1.98c-1.76 2.48-4.26 4.44-7.5 5.88s-7.02 2.16-11.34 2.16c-3.84 0-7.4-0.7-10.68-2.1s-6.14-3.44-8.58-6.12-4.34-5.94-5.7-9.78-2.04-8.16-2.04-12.96c0-4.32 0.78-8.34 2.34-12.06s3.6-6.92 6.12-9.6 5.38-4.78 8.58-6.3 6.48-2.28 9.84-2.28c2.56 0 4.82 0.22 6.78 0.66s3.68 1.06 5.16 1.86 2.78 1.74 3.9 2.82 2.16 2.22 3.12 3.42v-35.04l10.68 8.64zm-27.96 67.92c3.6 0 6.52-0.68 8.76-2.04s3.98-3.06 5.22-5.1 2.1-4.22 2.58-6.54 0.72-4.44 0.72-6.36v-1.2c0-1.12-0.22-2.7-0.66-4.74s-1.28-4.06-2.52-6.06-3-3.7-5.28-5.1-5.22-2.02-8.82-1.86c-3.44 0-6.26 0.74-8.46 2.22s-3.96 3.26-5.28 5.34-2.24 4.2-2.76 6.36-0.78 3.92-0.78 5.28c0 1.84 0.24 3.92 0.72 6.24s1.36 4.48 2.64 6.48 3.04 3.68 5.28 5.04 5.12 2.04 8.64 2.04z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m882.81 97.09c0.64 0.48 0.96 1.12 0.96 1.92l-0.12 41.04v37.08l-10.56-8.4c-0.72-0.64-1.08-1.44-1.08-2.4v-77.88l10.8 8.64z" fill-rule="nonzero"/> </g> <g transform="matrix(1.008 0 0 1.008 -2.239 .61874)"> <path d="m950.36 146.05c0 0.88 0.02 1.5 0.06 1.86s-0.02 0.98-0.18 1.86h-7.08c-2.08 0-4.44-0.02-7.08-0.06s-5.36-0.06-8.16-0.06h-22.08c0 2.88 0.56 5.36 1.68 7.44s2.6 3.8 4.44 5.16 3.94 2.36 6.3 3 4.74 0.96 7.14 0.96c3.04 0 5.9-0.76 8.58-2.28s4.94-3.52 6.78-6c0.64 0.56 1.54 1.48 2.7 2.76s2.94 3.2 5.34 5.76c-2.8 3.36-6.22 6.02-10.26 7.98s-8.42 2.94-13.14 2.94-8.92-0.64-12.84-1.92-7.32-3.24-10.2-5.88-5.12-5.98-6.72-10.02-2.4-8.82-2.4-14.34c0-4.96 0.66-9.42 1.98-13.38s3.22-7.32 5.7-10.08 5.44-4.9 8.88-6.42 7.32-2.28 11.64-2.28c5.76 0 10.52 0.88 14.28 2.64s6.72 4.16 8.88 7.2 3.66 6.54 4.5 10.5 1.26 8.18 1.26 12.66zm-29.4-22.8c-2.16 0.16-4.16 0.72-6 1.68s-3.42 2.2-4.74 3.72-2.36 3.28-3.12 5.28-1.14 4.12-1.14 6.36h33.12c0-2-0.22-4.06-0.66-6.18s-1.3-4.02-2.58-5.7-3.1-3.02-5.46-4.02-5.5-1.38-9.42-1.14z" fill-rule="nonzero"/> </g> <g transform="matrix(1.8559 0 0 .7642 45.348 36.475)"> <g transform="translate(2.7114)"> <path d="m3.935 173.02c-0.331 0-0.497-0.402-0.497-1.207v-51.002c0-0.738 0.138-1.107 0.414-1.107h1.781c0.277 0 0.415 0.335 0.415 1.006v5.935c0 0.336 0.027 0.553 0.083 0.654 0.055 0.101 0.151-0.017 0.289-0.352 0.912-1.744 1.754-3.236 2.527-4.477 0.773-1.24 1.554-2.179 2.341-2.816s1.65-0.956 2.588-0.956c1.685 0 3.011 0.922 3.977 2.766 0.967 1.845 1.602 3.84 1.905 5.986 0.056 0.268 0.139 0.369 0.249 0.302s0.221-0.235 0.331-0.503c0.939-1.811 1.802-3.353 2.589-4.628 0.787-1.274 1.581-2.246 2.382-2.917s1.671-1.006 2.61-1.006c2.016 0 3.569 1.392 4.66 4.175 1.09 2.783 1.636 6.421 1.636 10.915v37.925c0 0.871-0.18 1.307-0.539 1.307h-1.739c-0.138 0-0.249-0.1-0.332-0.301-0.083-0.202-0.124-0.503-0.124-0.906v-36.315c0-3.555-0.338-6.321-1.015-8.3-0.676-1.978-1.76-2.967-3.251-2.967-0.884 0-1.726 0.386-2.527 1.157s-1.519 1.727-2.154 2.867-1.201 2.213-1.699 3.219c-0.248 0.469-0.421 0.905-0.517 1.308-0.097 0.402-0.145 0.972-0.145 1.71v37.221c0 0.871-0.166 1.307-0.497 1.307h-1.74c-0.166 0-0.29-0.1-0.373-0.301-0.083-0.202-0.124-0.503-0.124-0.906v-36.315c0-3.555-0.332-6.321-0.994-8.3-0.663-1.978-1.754-2.967-3.273-2.967-1.242 0-2.375 0.704-3.396 2.112-1.022 1.409-2.223 3.555-3.604 6.439v39.031c0 0.805-0.18 1.207-0.539 1.207h-1.698z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m53.642 166.28c-1.077 2.549-2.237 4.477-3.479 5.785-1.243 1.307-2.61 1.961-4.101 1.961-2.154 0-3.853-1.324-5.095-3.973-1.243-2.649-1.864-6.187-1.864-10.613 0-3.488 0.4-6.489 1.201-9.004s1.988-4.51 3.562-5.985c1.574-1.476 3.521-2.414 5.841-2.817l3.686-0.704c0.221-0.067 0.394-0.218 0.518-0.453 0.124-0.234 0.187-0.587 0.187-1.056v-2.917c0-3.89-0.504-6.975-1.512-9.255s-2.354-3.42-4.039-3.42c-1.298 0-2.472 0.72-3.521 2.162s-2.002 3.572-2.858 6.388c-0.083 0.268-0.159 0.453-0.228 0.554-0.069 0.1-0.172 0.083-0.311-0.051l-1.698-1.71c-0.083-0.134-0.138-0.285-0.166-0.453-0.027-0.167 0.014-0.452 0.125-0.855 0.856-3.353 2.009-6.052 3.459-8.098 1.449-2.045 3.224-3.068 5.322-3.068 1.74 0 3.211 0.687 4.412 2.062s2.112 3.37 2.734 5.986c0.621 2.615 0.932 5.7 0.932 9.255v35.712c0 0.536-0.035 0.888-0.104 1.056s-0.2 0.251-0.393 0.251h-1.533c-0.166 0-0.29-0.117-0.373-0.352-0.083-0.234-0.124-0.553-0.124-0.955l-0.083-5.231c-0.055-0.939-0.221-1.006-0.497-0.202zm0.456-19.314c0-1.14-0.194-1.643-0.58-1.509l-3.107 0.603c-1.436 0.202-2.686 0.638-3.749 1.308-1.063 0.671-1.953 1.543-2.671 2.616s-1.257 2.33-1.616 3.772-0.538 3.102-0.538 4.98c0 3.152 0.455 5.616 1.367 7.393 0.911 1.778 2.14 2.666 3.686 2.666 0.939 0 1.85-0.419 2.734-1.257s1.671-1.895 2.361-3.169c0.663-1.408 1.181-2.85 1.553-4.326 0.373-1.475 0.56-2.883 0.56-4.225v-8.852z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m79.034 173.02c-0.166 0-0.297-0.117-0.394-0.352-0.096-0.234-0.145-0.553-0.145-0.955v-4.628c0-0.536-0.041-0.838-0.124-0.905s-0.207 0.1-0.373 0.503c-0.276 0.67-0.69 1.593-1.242 2.766-0.553 1.174-1.271 2.23-2.154 3.169-0.884 0.939-1.961 1.408-3.231 1.408-1.74 0-3.314-0.989-4.722-2.967-1.409-1.979-2.534-4.963-3.376-8.953-0.843-3.991-1.264-8.937-1.264-14.838 0-5.701 0.415-10.68 1.243-14.939s1.988-7.595 3.479-10.009c1.492-2.415 3.204-3.622 5.137-3.622 1.436 0 2.616 0.57 3.541 1.71 0.926 1.14 1.719 2.381 2.382 3.722 0.249 0.47 0.414 0.637 0.497 0.503s0.125-0.536 0.125-1.207v-23.841c0-0.805 0.151-1.208 0.455-1.208h1.864c0.276 0 0.414 0.369 0.414 1.107v72.128c0 0.537-0.041 0.905-0.124 1.107-0.083 0.201-0.235 0.301-0.455 0.301h-1.533zm-0.621-42.049c-0.939-2.213-1.885-3.94-2.838-5.181s-2.009-1.861-3.169-1.861c-1.463 0-2.768 0.889-3.914 2.666s-2.044 4.376-2.693 7.796-0.973 7.578-0.973 12.474c0 5.097 0.338 9.272 1.015 12.524 0.676 3.253 1.567 5.651 2.672 7.193 1.104 1.543 2.305 2.314 3.603 2.314 1.188 0 2.258-0.704 3.211-2.113 0.952-1.408 1.705-3.118 2.257-5.13s0.829-3.957 0.829-5.835v-24.847z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m89.514 149.38c0 3.42 0.345 6.606 1.035 9.557 0.691 2.951 1.609 5.315 2.755 7.092s2.437 2.666 3.873 2.666c1.519 0 2.837-0.738 3.956-2.213 1.118-1.476 2.064-3.655 2.837-6.539 0.083-0.336 0.166-0.52 0.249-0.554 0.083-0.033 0.179 0.017 0.29 0.151l1.408 1.912c0.221 0.268 0.235 0.67 0.041 1.207-0.69 2.548-1.47 4.661-2.34 6.337-0.87 1.677-1.857 2.935-2.962 3.773-1.104 0.838-2.319 1.257-3.645 1.257-2.043 0-3.838-1.14-5.385-3.42-1.546-2.28-2.761-5.482-3.645-9.607-0.884-4.124-1.325-8.836-1.325-14.134 0-5.901 0.455-10.931 1.367-15.089 0.911-4.158 2.14-7.377 3.686-9.658 1.547-2.28 3.3-3.42 5.261-3.42 1.988 0 3.714 1.073 5.178 3.219 1.463 2.146 2.595 5.231 3.396 9.255s1.201 8.886 1.201 14.587c0 0.469-0.02 0.939-0.062 1.408-0.041 0.469-0.214 0.704-0.517 0.704h-16.362c-0.083 0-0.152 0.151-0.207 0.453-0.056 0.302-0.083 0.654-0.083 1.056zm13.752-6.237c0.304 0 0.497-0.1 0.58-0.302 0.083-0.201 0.124-0.57 0.124-1.106 0-3.219-0.283-6.187-0.849-8.903s-1.367-4.896-2.402-6.539c-1.036-1.643-2.272-2.464-3.708-2.464-1.629 0-2.996 0.955-4.101 2.867-1.104 1.911-1.94 4.342-2.506 7.293s-0.849 6.002-0.849 9.154h13.711z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m148.54 119.7c0.165 0 0.283 0.117 0.352 0.352s0.076 0.52 0.02 0.855l-6.254 50.902c-0.028 0.47-0.104 0.788-0.228 0.956s-0.297 0.251-0.518 0.251h-1.615c-0.442 0-0.718-0.402-0.829-1.207l-5.26-40.138c-0.111-0.604-0.201-0.905-0.27-0.905s-0.131 0.301-0.186 0.905l-5.012 40.138c-0.028 0.47-0.097 0.788-0.207 0.956-0.111 0.168-0.277 0.251-0.497 0.251h-1.74c-0.442 0-0.718-0.402-0.829-1.207l-6.503-50.801c-0.055-0.403-0.048-0.721 0.021-0.956s0.2-0.352 0.393-0.352h1.823c0.166 0 0.297 0.067 0.393 0.201 0.097 0.134 0.159 0.403 0.187 0.805l5.302 41.848c0.083 0.671 0.179 0.989 0.29 0.956 0.11-0.034 0.207-0.386 0.29-1.056l5.219-41.949c0.055-0.268 0.124-0.47 0.207-0.604s0.193-0.201 0.331-0.201h1.533c0.138 0 0.262 0.067 0.373 0.201 0.11 0.134 0.179 0.403 0.207 0.805l5.468 41.848c0.083 0.671 0.179 0.989 0.29 0.956 0.11-0.034 0.207-0.386 0.29-1.056l5.053-41.849c0.055-0.335 0.138-0.57 0.249-0.704 0.11-0.134 0.234-0.201 0.373-0.201h1.284z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m156.49 171.51c0 0.604-0.042 1.006-0.125 1.208-0.082 0.201-0.262 0.301-0.538 0.301h-1.533c-0.221 0-0.366-0.083-0.435-0.251s-0.103-0.486-0.103-0.956v-50.902c0-0.805 0.152-1.207 0.456-1.207h1.822c0.304 0 0.456 0.402 0.456 1.207v50.6zm0.165-63.979c0 1.207-0.207 1.811-0.621 1.811h-1.905c-0.221 0-0.366-0.135-0.435-0.403s-0.104-0.67-0.104-1.207v-7.847c0-1.006 0.18-1.509 0.539-1.509h1.988c0.359 0 0.538 0.47 0.538 1.409v7.746z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m168.3 124.83c-0.221 0-0.331 0.269-0.331 0.805v33.801c0 3.42 0.221 5.667 0.663 6.74 0.441 1.073 1.09 1.609 1.946 1.609h3.024c0.138 0 0.242 0.084 0.311 0.252 0.069 0.167 0.103 0.419 0.103 0.754v2.716c0 0.537-0.138 0.906-0.414 1.107-0.248 0.067-0.614 0.134-1.098 0.201-0.483 0.067-0.959 0.118-1.429 0.151-0.469 0.034-0.828 0.05-1.077 0.05-1.712 0-2.934-0.955-3.665-2.867-0.732-1.911-1.098-5.013-1.098-9.305v-35.108c0-0.604-0.124-0.906-0.373-0.906h-3.521c-0.248 0-0.373-0.268-0.373-0.804v-3.521c0-0.537 0.111-0.805 0.332-0.805h3.686c0.166 0 0.263-0.268 0.29-0.805l0.415-16.095c0-0.805 0.124-1.207 0.372-1.207h1.492c0.303 0 0.455 0.436 0.455 1.307v15.995c0 0.537 0.097 0.805 0.29 0.805h5.468c0.221 0 0.331 0.268 0.331 0.805v3.521c0 0.536-0.124 0.804-0.373 0.804h-5.426z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> <g transform="translate(2.7114)"> <path d="m179.4 173.02c-0.331 0-0.497-0.402-0.497-1.207v-72.329c0-0.738 0.138-1.107 0.414-1.107h1.782c0.276 0 0.414 0.336 0.414 1.006v27.162c0 0.335 0.034 0.536 0.103 0.603s0.159-0.033 0.27-0.302c0.994-1.81 1.898-3.319 2.713-4.526 0.814-1.208 1.629-2.113 2.444-2.717 0.814-0.603 1.691-0.905 2.63-0.905 2.182 0 3.839 1.375 4.971 4.125 1.132 2.749 1.698 6.404 1.698 10.965v37.925c0 0.871-0.166 1.307-0.497 1.307h-1.74c-0.165 0-0.29-0.1-0.373-0.301-0.082-0.202-0.124-0.503-0.124-0.906v-36.315c0-3.555-0.366-6.321-1.097-8.3-0.732-1.978-1.899-2.967-3.501-2.967-0.883 0-1.705 0.318-2.464 0.956-0.76 0.637-1.526 1.576-2.299 2.816-0.773 1.241-1.643 2.834-2.61 4.779v39.031c0 0.805-0.179 1.207-0.538 1.207h-1.699z" fill-rule="nonzero" stroke="#000" stroke-width=".7px"/> </g> </g> <g transform="matrix(.80638 0 0 .80638 452.53 65.421)" fill-rule="nonzero"> <path d="m79.32 36.98v150.76l15.68-13.2 6.59-156.31-22.27 18.75z" fill="url(#f)"/> <path d="m79.32 36.98-22.27-18.75 6.59 156.31 15.68 13.2v-150.76z" fill="url(#e)"/> <path d="m25.19 104.83 8.63 49.04 12.5-14.95-2.46-56.42-18.67 22.33z" fill="url(#d)"/> <path d="m25.19 104.83-25.19-14.59 16.97 53.86 16.85 9.77-8.63-49.04z" fill="url(#c)"/> <path d="M43.86,82.5L18.69,67.98L0,90.24L25.18,104.83L43.86,82.5Z" fill="#9c3"/> <path d="m134.82 78.69-9.97 56.5 15.58-9.04 19.57-62.05-25.18 14.59z" fill="url(#b)"/> <path d="m134.82 78.69-18.68-22.33-2.86 65 11.57 13.83 9.97-56.5z" fill="url(#a)"/> <path d="m160 64.1-18.69-22.26-25.17 14.52 18.67 22.33 25.19-14.59z" fill="#ffe113"/> <path d="M101.59,18.23L79.32,0L57.05,18.23L79.32,36.98L101.59,18.23Z" fill="#f3e600"/> </g> <defs> <linearGradient id="f" x2="1" gradientTransform="matrix(.84 -162.96 162.96 .84 89.64 184.81)" gradientUnits="userSpaceOnUse"><stop stop-color="#62d399" offset="0"/><stop stop-color="#acd842" offset=".51"/><stop stop-color="#d7db0a" offset=".9"/><stop stop-color="#d7db0a" offset="1"/></linearGradient> <linearGradient id="e" x2="1" gradientTransform="matrix(-1.6,-162.13,162.13,-1.6,69.68,178.9)" gradientUnits="userSpaceOnUse"><stop stop-color="#0ba398" offset="0"/><stop stop-color="#4ca352" offset=".5"/><stop stop-color="#76a30a" offset="1"/></linearGradient> <linearGradient id="d" x2="1" gradientTransform="matrix(-1.9,-67.98,67.98,-1.9,36.6,152.17)" gradientUnits="userSpaceOnUse"><stop stop-color="#36a382" offset="0"/><stop stop-color="#36a382" offset=".19"/><stop stop-color="#49a459" offset=".54"/><stop stop-color="#76a30b" offset="1"/></linearGradient> <linearGradient id="c" x2="1" gradientTransform="matrix(2.18,-62.38,62.38,2.18,15.82,153.24)" gradientUnits="userSpaceOnUse"><stop stop-color="#267880" offset="0"/><stop stop-color="#457a5c" offset=".51"/><stop stop-color="#717516" offset="1"/></linearGradient> <linearGradient id="b" x2="1" gradientTransform="matrix(13.85,-71.96,71.96,13.85,135.08,135.43)" gradientUnits="userSpaceOnUse"><stop stop-color="#b0d939" offset="0"/><stop stop-color="#eadb04" offset="1"/></linearGradient> <linearGradient id="a" x2="1" gradientTransform="matrix(26.159 -64.737 64.737 26.159 107.42 128.14)" gradientUnits="userSpaceOnUse"><stop stop-color="#74af52" offset="0"/><stop stop-color="#74af52" offset=".17"/><stop stop-color="#99be32" offset=".48"/><stop stop-color="#c0c40a" offset="1"/></linearGradient> </defs> </svg>`
13
- const madeWithNeedleBlob = new Blob([madeWithNeedleSvgString], { type: "image/svg+xml;charset=utf-8" });
14
- const madeWithNeedleUrl = URL.createObjectURL(madeWithNeedleBlob);
13
+ const madeWithNeedleBlob = btoa(madeWithNeedleSvgString);
14
+ const madeWithNeedleUrl = "data:image/svg+xml;charset=utf-8;base64," + madeWithNeedleBlob;
15
15
  /** Made With Needle Logo */
16
16
  export const madeWithNeedleSVG: string = madeWithNeedleUrl;
17
17
 
18
18
 
19
19
  const needleLogoSvgString = `<svg viewBox="0 0 509 154" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M665.95 132.73v44.88l-10.56-8.4c-.8-.64-1.2-1.44-1.2-2.4v-32.4c0-6.48-4.12-9.72-12.36-9.72-2.16 0-4.18.4-6.06 1.2-1.88.8-3.54 1.8-4.98 3-1.44 1.2-2.56 2.5-3.36 3.9-.8 1.4-1.2 2.7-1.2 3.9v40.92l-10.68-8.4c-.72-.64-1.08-1.44-1.08-2.4v-53.76l10.92 8.52c.32.24.56.44.72.6.16.16.36.32.6.48.96-1.2 2.14-2.28 3.54-3.24 1.4-.96 2.92-1.76 4.56-2.4 1.64-.64 3.34-1.14 5.1-1.5 1.76-.36 3.44-.54 5.04-.54 1.44 0 2.92.04 4.44.12 1.52.08 2.84.28 3.96.6 4.56 1.12 7.8 3.12 9.72 6 1.92 2.88 2.88 6.56 2.88 11.04ZM732.38 146.05c0 .88.02 1.5.06 1.86.04.36-.02.98-.18 1.86h-7.08c-2.08 0-4.44-.02-7.08-.06-2.64-.04-5.36-.06-8.16-.06h-22.08c0 2.88.56 5.36 1.68 7.44 1.12 2.08 2.6 3.8 4.44 5.16 1.84 1.36 3.94 2.36 6.3 3 2.36.64 4.74.96 7.14.96 3.04 0 5.9-.76 8.58-2.28 2.68-1.52 4.94-3.52 6.78-6 .64.56 1.54 1.48 2.7 2.76 1.16 1.28 2.94 3.2 5.34 5.76-2.8 3.36-6.22 6.02-10.26 7.98-4.04 1.96-8.42 2.94-13.14 2.94-4.72 0-8.92-.64-12.84-1.92-3.92-1.28-7.32-3.24-10.2-5.88-2.88-2.64-5.12-5.98-6.72-10.02-1.6-4.04-2.4-8.82-2.4-14.34 0-4.96.66-9.42 1.98-13.38 1.32-3.96 3.22-7.32 5.7-10.08s5.44-4.9 8.88-6.42c3.44-1.52 7.32-2.28 11.64-2.28 5.76 0 10.52.88 14.28 2.64 3.76 1.76 6.72 4.16 8.88 7.2 2.16 3.04 3.66 6.54 4.5 10.5.84 3.96 1.26 8.18 1.26 12.66Zm-29.4-22.8c-2.16.16-4.16.72-6 1.68-1.84.96-3.42 2.2-4.74 3.72-1.32 1.52-2.36 3.28-3.12 5.28-.76 2-1.14 4.12-1.14 6.36h33.12c0-2-.22-4.06-.66-6.18-.44-2.12-1.3-4.02-2.58-5.7-1.28-1.68-3.1-3.02-5.46-4.02-2.36-1-5.5-1.38-9.42-1.14ZM795.93 146.05c0 .88.02 1.5.06 1.86.04.36-.02.98-.18 1.86h-7.08c-2.08 0-4.44-.02-7.08-.06-2.64-.04-5.36-.06-8.16-.06h-22.08c0 2.88.56 5.36 1.68 7.44 1.12 2.08 2.6 3.8 4.44 5.16 1.84 1.36 3.94 2.36 6.3 3 2.36.64 4.74.96 7.14.96 3.04 0 5.9-.76 8.58-2.28 2.68-1.52 4.94-3.52 6.78-6 .64.56 1.54 1.48 2.7 2.76 1.16 1.28 2.94 3.2 5.34 5.76-2.8 3.36-6.22 6.02-10.26 7.98-4.04 1.96-8.42 2.94-13.14 2.94-4.72 0-8.92-.64-12.84-1.92-3.92-1.28-7.32-3.24-10.2-5.88-2.88-2.64-5.12-5.98-6.72-10.02-1.6-4.04-2.4-8.82-2.4-14.34 0-4.96.66-9.42 1.98-13.38 1.32-3.96 3.22-7.32 5.7-10.08s5.44-4.9 8.88-6.42c3.44-1.52 7.32-2.28 11.64-2.28 5.76 0 10.52.88 14.28 2.64 3.76 1.76 6.72 4.16 8.88 7.2 2.16 3.04 3.66 6.54 4.5 10.5.84 3.96 1.26 8.18 1.26 12.66Zm-29.4-22.8c-2.16.16-4.16.72-6 1.68-1.84.96-3.42 2.2-4.74 3.72-1.32 1.52-2.36 3.28-3.12 5.28-.76 2-1.14 4.12-1.14 6.36h33.12c0-2-.22-4.06-.66-6.18-.44-2.12-1.3-4.02-2.58-5.7-1.28-1.68-3.1-3.02-5.46-4.02-2.36-1-5.5-1.38-9.42-1.14ZM858.57 97.21c.64.48.96 1.16.96 2.04v74.88c-.08 1.04-.12 2.12-.12 3.24-1.84-1.52-3.56-2.92-5.16-4.2-1.36-1.12-2.66-2.18-3.9-3.18-1.24-1-2.06-1.66-2.46-1.98-1.76 2.48-4.26 4.44-7.5 5.88-3.24 1.44-7.02 2.16-11.34 2.16-3.84 0-7.4-.7-10.68-2.1-3.28-1.4-6.14-3.44-8.58-6.12-2.44-2.68-4.34-5.94-5.7-9.78-1.36-3.84-2.04-8.16-2.04-12.96 0-4.32.78-8.34 2.34-12.06 1.56-3.72 3.6-6.92 6.12-9.6 2.52-2.68 5.38-4.78 8.58-6.3 3.2-1.52 6.48-2.28 9.84-2.28 2.56 0 4.82.22 6.78.66 1.96.44 3.68 1.06 5.16 1.86s2.78 1.74 3.9 2.82a35.92 35.92 0 0 1 3.12 3.42V88.57l10.68 8.64Zm-27.96 67.92c3.6 0 6.52-.68 8.76-2.04 2.24-1.36 3.98-3.06 5.22-5.1a20.5 20.5 0 0 0 2.58-6.54c.48-2.32.72-4.44.72-6.36v-1.2c0-1.12-.22-2.7-.66-4.74-.44-2.04-1.28-4.06-2.52-6.06s-3-3.7-5.28-5.1c-2.28-1.4-5.22-2.02-8.82-1.86-3.44 0-6.26.74-8.46 2.22-2.2 1.48-3.96 3.26-5.28 5.34-1.32 2.08-2.24 4.2-2.76 6.36-.52 2.16-.78 3.92-.78 5.28 0 1.84.24 3.92.72 6.24.48 2.32 1.36 4.48 2.64 6.48s3.04 3.68 5.28 5.04c2.24 1.36 5.12 2.04 8.64 2.04ZM882.81 97.09c.64.48.96 1.12.96 1.92l-.12 41.04v37.08l-10.56-8.4c-.72-.64-1.08-1.44-1.08-2.4V88.45l10.8 8.64ZM950.36 146.05c0 .88.02 1.5.06 1.86.04.36-.02.98-.18 1.86h-7.08c-2.08 0-4.44-.02-7.08-.06-2.64-.04-5.36-.06-8.16-.06h-22.08c0 2.88.56 5.36 1.68 7.44 1.12 2.08 2.6 3.8 4.44 5.16 1.84 1.36 3.94 2.36 6.3 3 2.36.64 4.74.96 7.14.96 3.04 0 5.9-.76 8.58-2.28 2.68-1.52 4.94-3.52 6.78-6 .64.56 1.54 1.48 2.7 2.76 1.16 1.28 2.94 3.2 5.34 5.76-2.8 3.36-6.22 6.02-10.26 7.98-4.04 1.96-8.42 2.94-13.14 2.94-4.72 0-8.92-.64-12.84-1.92-3.92-1.28-7.32-3.24-10.2-5.88-2.88-2.64-5.12-5.98-6.72-10.02-1.6-4.04-2.4-8.82-2.4-14.34 0-4.96.66-9.42 1.98-13.38 1.32-3.96 3.22-7.32 5.7-10.08s5.44-4.9 8.88-6.42c3.44-1.52 7.32-2.28 11.64-2.28 5.76 0 10.52.88 14.28 2.64 3.76 1.76 6.72 4.16 8.88 7.2 2.16 3.04 3.66 6.54 4.5 10.5.84 3.96 1.26 8.18 1.26 12.66Zm-29.4-22.8c-2.16.16-4.16.72-6 1.68-1.84.96-3.42 2.2-4.74 3.72-1.32 1.52-2.36 3.28-3.12 5.28-.76 2-1.14 4.12-1.14 6.36h33.12c0-2-.22-4.06-.66-6.18-.44-2.12-1.3-4.02-2.58-5.7-1.28-1.68-3.1-3.02-5.46-4.02-2.36-1-5.5-1.38-9.42-1.14Z" style="fill-rule:nonzero" transform="translate(-452.406 -63.709) scale(1.00797)"/><path d="M79.32 36.98v150.76L95 174.54l6.59-156.31-22.27 18.75Z" style="fill:url(#a);fill-rule:nonzero" transform="matrix(.80638 0 0 .80638 2.361 1.094)"/><path d="M79.32 36.98 57.05 18.23l6.59 156.31 15.68 13.2V36.98Z" style="fill:url(#b);fill-rule:nonzero" transform="matrix(.80638 0 0 .80638 2.361 1.094)"/><path d="m25.19 104.83 8.63 49.04 12.5-14.95-2.46-56.42-18.67 22.33Z" style="fill:url(#c);fill-rule:nonzero" transform="matrix(.80638 0 0 .80638 2.361 1.094)"/><path d="M25.19 104.83 0 90.24l16.97 53.86 16.85 9.77-8.63-49.04Z" style="fill:url(#d);fill-rule:nonzero" transform="matrix(.80638 0 0 .80638 2.361 1.094)"/><path d="M43.86 82.5 18.69 67.98 0 90.24l25.18 14.59L43.86 82.5Z" style="fill:#9c3;fill-rule:nonzero" transform="matrix(.80638 0 0 .80638 2.361 1.094)"/><path d="m134.82 78.69-9.97 56.5 15.58-9.04L160 64.1l-25.18 14.59Z" style="fill:url(#e);fill-rule:nonzero" transform="matrix(.80638 0 0 .80638 2.361 1.094)"/><path d="m134.82 78.69-18.68-22.33-2.86 65 11.57 13.83 9.97-56.5Z" style="fill:url(#f);fill-rule:nonzero" transform="matrix(.80638 0 0 .80638 2.361 1.094)"/><path d="m160 64.1-18.69-22.26-25.17 14.52 18.67 22.33L160 64.1Z" style="fill:#ffe113;fill-rule:nonzero" transform="matrix(.80638 0 0 .80638 2.361 1.094)"/><path d="M101.59 18.23 79.32 0 57.05 18.23l22.27 18.75 22.27-18.75Z" style="fill:#f3e600;fill-rule:nonzero" transform="matrix(.80638 0 0 .80638 2.361 1.094)"/><defs><linearGradient id="a" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.84 -162.96 162.96 .84 89.64 184.81)"><stop offset="0" style="stop-color:#62d399;stop-opacity:1"/><stop offset=".51" style="stop-color:#acd842;stop-opacity:1"/><stop offset=".9" style="stop-color:#d7db0a;stop-opacity:1"/><stop offset="1" style="stop-color:#d7db0a;stop-opacity:1"/></linearGradient><linearGradient id="b" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-90.565 123.412 54.953) scale(162.14)"><stop offset="0" style="stop-color:#0ba398;stop-opacity:1"/><stop offset=".5" style="stop-color:#4ca352;stop-opacity:1"/><stop offset="1" style="stop-color:#76a30a;stop-opacity:1"/></linearGradient><linearGradient id="c" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="scale(-68) rotate(88.4 .881 -1.396)"><stop offset="0" style="stop-color:#36a382;stop-opacity:1"/><stop offset=".19" style="stop-color:#36a382;stop-opacity:1"/><stop offset=".54" style="stop-color:#49a459;stop-opacity:1"/><stop offset="1" style="stop-color:#76a30b;stop-opacity:1"/></linearGradient><linearGradient id="d" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-88 87.255 68.431) scale(62.42)"><stop offset="0" style="stop-color:#267880;stop-opacity:1"/><stop offset=".51" style="stop-color:#457a5c;stop-opacity:1"/><stop offset="1" style="stop-color:#717516;stop-opacity:1"/></linearGradient><linearGradient id="e" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-79.1 149.53 -14.065) scale(73.28)"><stop offset="0" style="stop-color:#b0d939;stop-opacity:1"/><stop offset="1" style="stop-color:#eadb04;stop-opacity:1"/></linearGradient><linearGradient id="f" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-67.997 148.705 -15.558) scale(69.8226)"><stop offset="0" style="stop-color:#74af52;stop-opacity:1"/><stop offset=".17" style="stop-color:#74af52;stop-opacity:1"/><stop offset=".48" style="stop-color:#99be32;stop-opacity:1"/><stop offset="1" style="stop-color:#c0c40a;stop-opacity:1"/></linearGradient></defs></svg>`;
20
- const needleLogoBlob = new Blob([needleLogoSvgString], { type: "image/svg+xml;charset=utf-8" });
21
- const needleLogoUrl = URL.createObjectURL(needleLogoBlob);
20
+ const needleLogoBlob = btoa(needleLogoSvgString);
21
+ const needleLogoUrl = "data:image/svg+xml;charset=utf-8;base64," + needleLogoBlob;
22
22
  /** Logo + Needle Typo */
23
23
  export const needleLogoSVG: string = needleLogoUrl
24
24
 
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -407,8 +407,10 @@
407
407
  // https://github.com/google/material-design-icons/issues/1165
408
408
  ensureFonts();
409
409
  // add to document head AND shadow dom to work
410
- loadFont("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&display=block", { loadedCallback: () => { this.handleSizeChange() } });
411
- loadFont("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&display=block", { element: shadow });
410
+ const fontUrl = "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&display=block";
411
+ // const fontUrl = "./include/fonts/MaterialSymbolsOutlined.css"; // for offline support
412
+ loadFont(fontUrl, { loadedCallback: () => { this.handleSizeChange() } });
413
+ loadFont(fontUrl, { element: shadow });
412
414
 
413
415
  const content = template.content.cloneNode(true) as DocumentFragment;
414
416
  shadow?.appendChild(content);
src/engine-components/postprocessing/PostProcessingHandler.ts CHANGED
@@ -160,6 +160,8 @@
160
160
  screenpass.mainScene = scene;
161
161
  composer.addPass(screenpass);
162
162
 
163
+ const automaticEffectsOrdering = true;
164
+ if (automaticEffectsOrdering) {
163
165
  try {
164
166
  this.orderEffects();
165
167
 
@@ -177,6 +179,11 @@
177
179
  effects.length = 0;
178
180
  composer.addPass(ef as Pass);
179
181
  }
182
+ else {
183
+ // seems some effects are not correctly typed, but three can deal with them,
184
+ // so we might need to just pass them through
185
+ // composer.addPass(ef);
186
+ }
180
187
  }
181
188
 
182
189
  // create and apply uber pass
@@ -192,8 +199,20 @@
192
199
  console.error("Error while applying postprocessing effects", e);
193
200
  composer.removeAllPasses();
194
201
  }
202
+ }
203
+ else {
204
+ for (const ef of effectsOrPasses) {
205
+ if (ef instanceof Effect)
206
+ composer.addPass(new EffectPass(cam, ef as Effect));
207
+ else if (ef instanceof Pass)
208
+ composer.addPass(ef as Pass);
209
+ else
210
+ // seems some effects are not correctly typed, but three can deal with them,
211
+ // so we just pass them through
212
+ composer.addPass(ef);
213
+ }
214
+ }
195
215
 
196
-
197
216
  if (debug)
198
217
  console.log("PostProcessing Passes", effectsOrPasses, "->", composer.passes);
199
218
  }
src/engine/codegen/register_types.ts CHANGED
@@ -120,6 +120,7 @@
120
120
  import { ParticleSubEmitter } from "../../engine-components/ParticleSystemSubEmitter.js";
121
121
  import { ParticleSystem } from "../../engine-components/ParticleSystem.js";
122
122
  import { ParticleSystemRenderer } from "../../engine-components/ParticleSystem.js";
123
+ import { PhysicsExtension } from "../../engine-components/export/usdz/extensions/behavior/PhysicsExtension.js";
123
124
  import { PixelationEffect } from "../../engine-components/postprocessing/Effects/Pixelation.js";
124
125
  import { PlayableDirector } from "../../engine-components/timeline/PlayableDirector.js";
125
126
  import { PlayAnimationOnClick } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents.js";
@@ -339,6 +340,7 @@
339
340
  TypeStore.add("ParticleSubEmitter", ParticleSubEmitter);
340
341
  TypeStore.add("ParticleSystem", ParticleSystem);
341
342
  TypeStore.add("ParticleSystemRenderer", ParticleSystemRenderer);
343
+ TypeStore.add("PhysicsExtension", PhysicsExtension);
342
344
  TypeStore.add("PixelationEffect", PixelationEffect);
343
345
  TypeStore.add("PlayableDirector", PlayableDirector);
344
346
  TypeStore.add("PlayAnimationOnClick", PlayAnimationOnClick);
src/engine-components/RigidBody.ts CHANGED
@@ -167,6 +167,9 @@
167
167
  @serializable()
168
168
  useGravity: boolean = true;
169
169
 
170
+ @serializable()
171
+ centerOfMass: Vector3 = new Vector3();
172
+
170
173
  /**
171
174
  * Constraints are used to lock the position or rotation of an object in a specific axis.
172
175
  */
src/engine-components/SpriteRenderer.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  static cache: { [key: string]: BufferGeometry } = {};
16
16
 
17
17
  static getOrCreateGeometry(sprite: Sprite): BufferGeometry {
18
- if (sprite._geometry) return sprite._geometry;
18
+ if (sprite.__cached_geometry) return sprite.__cached_geometry;
19
19
  if (sprite.guid) {
20
20
  if (SpriteUtils.cache[sprite.guid]) {
21
21
  if (debug) console.log("Take cached geometry for sprite", sprite.guid);
@@ -23,7 +23,7 @@
23
23
  }
24
24
  }
25
25
  const geo = new BufferGeometry();
26
- sprite._geometry = geo;
26
+ sprite.__cached_geometry = geo;
27
27
  const vertices = new Float32Array(sprite.triangles.length * 3);
28
28
  const uvs = new Float32Array(sprite.triangles.length * 2);
29
29
  for (let i = 0; i < sprite.triangles.length; i += 1) {
@@ -70,7 +70,11 @@
70
70
  y!: number;
71
71
  }
72
72
 
73
+ /**
74
+ * A sprite is a mesh that represents a 2D image
75
+ */
73
76
  export class Sprite {
77
+
74
78
  @serializable()
75
79
  guid?: string;
76
80
  @serializable(Texture)
@@ -82,8 +86,48 @@
82
86
  @serializeable()
83
87
  vertices!: Array<Vec2>;
84
88
 
85
- _geometry?: BufferGeometry;
86
- _hasLoadedProgressive: boolean = false;
89
+ /** @internal */
90
+ __cached_geometry?: BufferGeometry;
91
+
92
+ /**
93
+ * The mesh that represents the sprite
94
+ */
95
+ get mesh(): Mesh {
96
+ if (!this._mesh) {
97
+ this._mesh = new Mesh(SpriteUtils.getOrCreateGeometry(this), this.material);
98
+ }
99
+ return this._mesh;
100
+ }
101
+ private _mesh: Mesh | undefined;
102
+
103
+ /**
104
+ * The material used to render the sprite
105
+ */
106
+ get material() {
107
+ if (!this._material) {
108
+ if (this.texture) {
109
+ this.texture.colorSpace = SRGBColorSpace;
110
+ if (this.texture.minFilter == NearestFilter && this.texture.magFilter == NearestFilter)
111
+ this.texture.anisotropy = 1;
112
+ this.texture.needsUpdate = true;
113
+ }
114
+ this._material = new MeshBasicMaterial({
115
+ map: this.texture,
116
+ color: 0xffffff,
117
+ side: DoubleSide,
118
+ transparent: true
119
+ });
120
+ }
121
+ return this._material;
122
+ }
123
+ private _material: MeshBasicMaterial | undefined;
124
+
125
+ /**
126
+ * The geometry of the sprite that can be used to create a mesh
127
+ */
128
+ getGeometry() {
129
+ return SpriteUtils.getOrCreateGeometry(this);
130
+ }
87
131
  }
88
132
 
89
133
  const $spriteTexOwner = Symbol("spriteOwner");
@@ -107,20 +151,21 @@
107
151
  const index = this.index;
108
152
  if (index < 0 || index >= this.spriteSheet.sprites.length)
109
153
  return;
110
- const slice = this.spriteSheet.sprites[index];
111
- const tex = slice?.texture;
154
+
155
+ const sprite = this.spriteSheet.sprites[index];
156
+ const tex = sprite?.texture;
112
157
  if (!tex) return;
113
158
  tex.colorSpace = SRGBColorSpace;
114
159
  if (tex.minFilter == NearestFilter && tex.magFilter == NearestFilter)
115
160
  tex.anisotropy = 1;
116
161
  tex.needsUpdate = true;
117
162
 
118
- if (!slice._hasLoadedProgressive) {
119
- slice._hasLoadedProgressive = true;
163
+ if (!sprite["__hasLoadedProgressive"]) {
164
+ sprite["__hasLoadedProgressive"] = true;
120
165
  const previousTexture = tex;
121
166
  NEEDLE_progressive.assignTextureLOD(context, sourceId!, tex, 0).then(res => {
122
167
  if (res instanceof Texture) {
123
- slice.texture = res;
168
+ sprite.texture = res;
124
169
  const shouldUpdateInMaterial = material?.["map"] === previousTexture;
125
170
  if (shouldUpdateInMaterial) {
126
171
  material["map"] = res;
@@ -137,30 +182,42 @@
137
182
  */
138
183
  export class SpriteRenderer extends Behaviour {
139
184
 
185
+ /** @internal The draw mode of the sprite renderer */
140
186
  @serializable()
141
187
  drawMode: SpriteDrawMode = SpriteDrawMode.Simple;
142
188
 
189
+ /** @internal Used when drawMode is set to Tiled */
143
190
  size: Vec2 = { x: 1, y: 1 };
144
191
 
145
192
  @serializable(RGBAColor)
146
193
  color?: RGBAColor;
147
194
 
195
+ /**
196
+ * The material that is used to render the sprite
197
+ */
148
198
  @serializable(Material)
149
199
  sharedMaterial?: Material;
150
200
 
201
+ // additional data
202
+ @serializable()
203
+ transparent: boolean = true;
204
+ @serializable()
205
+ cutoutThreshold: number = 0;
206
+ @serializable()
207
+ castShadows: boolean = false;
208
+
151
209
  @serializable(SpriteData)
152
210
  get sprite(): SpriteData | undefined {
153
211
  return this._spriteSheet;
154
212
  }
213
+ /**
214
+ * Set a new sprite sheetsheet or update the index of the sprite to be rendered in the currently assigned sprite sheet
215
+ */
155
216
  set sprite(value: SpriteData | undefined | number) {
156
217
  if (value === this._spriteSheet) return;
157
218
  if (typeof value === "number") {
158
219
  const index = Math.floor(value);
159
- // if (value === index)
160
220
  this.spriteIndex = index;
161
- // else if (debug) {
162
- // console.log("Spritesheet framedrop", index, value);
163
- // }
164
221
  return;
165
222
  }
166
223
  else {
@@ -169,6 +226,9 @@
169
226
  }
170
227
  }
171
228
 
229
+ /**
230
+ * Set the index of the sprite to be rendered in the currently assigned sprite sheet
231
+ */
172
232
  set spriteIndex(value: number) {
173
233
  if (!this._spriteSheet) return;
174
234
  this._spriteSheet.index = value;
@@ -177,6 +237,9 @@
177
237
  get spriteIndex(): number {
178
238
  return this._spriteSheet?.index ?? 0;
179
239
  }
240
+ /**
241
+ * Get the number of sprites in the currently assigned sprite sheet
242
+ */
180
243
  get spriteFrames(): number {
181
244
  return this._spriteSheet?.spriteSheet?.sprites.length ?? 0;
182
245
  }
@@ -184,14 +247,7 @@
184
247
  private _spriteSheet?: SpriteData;
185
248
  private _currentSprite?: Mesh;
186
249
 
187
- // additional data
188
- @serializable()
189
- transparent: boolean = true;
190
- @serializable()
191
- cutoutThreshold: number = 0;
192
- @serializable()
193
- castShadows: boolean = false;
194
-
250
+ /** @internal */
195
251
  awake(): void {
196
252
  this._currentSprite = undefined;
197
253
  if (debug) {
@@ -199,6 +255,7 @@
199
255
  }
200
256
  }
201
257
 
258
+ /** @internal */
202
259
  start() {
203
260
  if (!this._currentSprite)
204
261
  this.updateSprite();
@@ -206,7 +263,10 @@
206
263
  this.gameObject.add(this._currentSprite);
207
264
  }
208
265
 
209
- private updateSprite() {
266
+ /**
267
+ * Update the sprite. Modified properties will be applied to the sprite mesh. This method is called automatically when the sprite is changed.
268
+ */
269
+ updateSprite() {
210
270
  if (!this.__didAwake) return;
211
271
  if (!this.sprite?.spriteSheet?.sprites) return;
212
272
  const sprite = this.sprite.spriteSheet.sprites[this.spriteIndex];
src/engine-components/postprocessing/Effects/Tonemapping.ts CHANGED
@@ -20,15 +20,17 @@
20
20
  }
21
21
 
22
22
  @serializable(VolumeParameter)
23
- mode!: VolumeParameter;
23
+ mode: VolumeParameter | undefined;
24
24
 
25
25
  get isToneMapping() { return true; }
26
26
 
27
27
  init(): void {
28
+ if (!this.mode) this.mode = new VolumeParameter(NoToneMapping);
28
29
  this.mode.defaultValue = NoToneMapping;
29
30
  }
30
31
 
31
32
  apply() {
33
+ if (!this.mode) this.init();
32
34
  this.mode!.onValueChanged = this._apply.bind(this);
33
35
  this._apply(this.mode!.value)
34
36
  }
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  import { AnimationExtension } from "./extensions/Animation.js"
15
15
  import { AudioExtension } from "./extensions/behavior/AudioExtension.js";
16
16
  import { BehaviorExtension } from "./extensions/behavior/Behaviour.js";
17
+ import { PhysicsExtension } from "./extensions/behavior/PhysicsExtension.js"
17
18
  import { TextExtension } from "./extensions/USDZText.js";
18
19
  import { USDZUIExtension } from "./extensions/USDZUI.js";
19
20
  import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter.js";
@@ -66,7 +67,7 @@
66
67
  * If this setting is off, Animators need to be registered by components – for example from PlayAnimationOnClick.
67
68
  */
68
69
  @serializable()
69
- autoExportAnimations: boolean = false;
70
+ autoExportAnimations: boolean = true;
70
71
 
71
72
  /** Collect all AudioSources automatically on export and emit them as playing at the start.
72
73
  * They will loop according to their settings.
@@ -145,6 +146,7 @@
145
146
  if (this.interactive) {
146
147
  this.extensions.push(new BehaviorExtension());
147
148
  this.extensions.push(new AudioExtension());
149
+ this.extensions.push(new PhysicsExtension());
148
150
  this.extensions.push(new TextExtension());
149
151
  this.extensions.push(new USDZUIExtension());
150
152
  }
src/engine-components/webxr/WebXRButtons.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isDevEnvironment } from "../../engine/debug/index.js";
1
+ import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
2
2
  import { isMozillaXR } from "../../engine/engine_utils.js";
3
3
  import { NeedleXRSession } from "../../engine/engine_xr.js";
4
4
  import { ButtonsFactory } from "../../engine/webcomponents/buttons.js";
@@ -159,7 +159,10 @@
159
159
  button.title = "Click to send this page to the Oculus Browser on your Quest";
160
160
  button.addEventListener("click", () => {
161
161
  const urlParameter = encodeURIComponent(window.location.href);
162
- window.open(baseUrl + urlParameter);
162
+ const url = baseUrl + urlParameter;
163
+ if (window.open(url) == null) {
164
+ showBalloonMessage("This page doesn't allow popups. Please paste " + url + " into your browser.");
165
+ }
163
166
  });
164
167
  this.listenToXRSessionState(button);
165
168
  this.hideElementDuringXRSession(button);
src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts ADDED
@@ -0,0 +1,137 @@
1
+ import { Object3D } from "three";
2
+
3
+ import { BoxCollider, CapsuleCollider, Collider, MeshCollider, SphereCollider } from "../../../../Collider.js";
4
+ import { GameObject } from "../../../../Component.js";
5
+ import { Rigidbody } from "../../../../RigidBody.js";
6
+ import type { IUSDExporterExtension } from "../../Extension.js";
7
+ import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
8
+
9
+
10
+ export class PhysicsExtension implements IUSDExporterExtension {
11
+
12
+ get extensionName(): string {
13
+ return "Physics";
14
+ }
15
+
16
+ onExportObject?(object: Object3D, model : USDObject, _context: USDZExporterContext) {
17
+
18
+ const rigidBodySources = GameObject.getComponents(object, Rigidbody).filter(c => c.enabled);
19
+ // no triggers supported for now; large trigger areas seem to break VisionOS since they're still interactable and block OS UI
20
+ const colliderSources = GameObject.getComponents(object, Collider).filter(c => c.enabled && !c.isTrigger);
21
+ let rigidBody = rigidBodySources.length > 0 ? rigidBodySources[0] : null;
22
+ const colliderSource = colliderSources.length > 0 ? colliderSources[0] : null;
23
+
24
+ // we still need a RigidBody component for the object to participate in physics;
25
+ // with only a Collider component it can only serve as trigger
26
+ // see https://developer.apple.com/documentation/realitykit/collisioncomponent#overview
27
+ let temporaryRigidbody: Rigidbody | undefined = undefined;
28
+ if (colliderSource && !rigidBody) {
29
+ rigidBody = GameObject.addComponent(object, Rigidbody);
30
+ rigidBody.isKinematic = true;
31
+ temporaryRigidbody = rigidBody;
32
+ }
33
+
34
+ if (rigidBody) {
35
+ model.addEventListener('serialize', (writer: USDWriter, _context: USDZExporterContext) => {
36
+ if (!rigidBody) return;
37
+
38
+ writer.appendLine();
39
+ writer.beginBlock(`def RealityKitComponent "RigidBody"`, "{", true );
40
+
41
+ // Gravity is enabled by default
42
+ if (!rigidBody.useGravity){
43
+ writer.appendLine(`bool gravityEnabled = 0`);
44
+ }
45
+ writer.appendLine(`uniform token info:id = "RealityKit.RigidBody"`);
46
+
47
+ if (rigidBody.isKinematic){
48
+ writer.appendLine(`token motionType = "Kinematic"`);
49
+ }
50
+
51
+ writer.beginBlock(`def RealityKitStruct "massFrame"`, "{", true );
52
+ writer.appendLine(`float m_mass = ${rigidBody.mass}`);
53
+ writer.beginBlock(`def RealityKitStruct "m_pose"`, "{", true );
54
+ /* TODO: Apple has a concept of center of mass orientation, not sure what that means here or what an equivalent mapping would be */
55
+ writer.appendLine(`float3 position = (${rigidBody.centerOfMass.x}, ${rigidBody.centerOfMass.y}, ${rigidBody.centerOfMass.z})`);
56
+ writer.closeBlock( "}" );
57
+ writer.closeBlock( "}" );
58
+
59
+ if (colliderSources.length > 0) {
60
+ const colliderSource = colliderSources[0];
61
+ writer.beginBlock(`def RealityKitStruct "material"`, "{", true );
62
+ const mat = colliderSource.sharedMaterial;
63
+ if (mat && mat.dynamicFriction !== undefined)
64
+ writer.appendLine(`double dynamicFriction = ${colliderSource.sharedMaterial?.dynamicFriction}`);
65
+ if (mat && mat.bounciness !== undefined)
66
+ writer.appendLine(`double restitution = ${colliderSource.sharedMaterial?.bounciness}`);
67
+ if (mat && mat.staticFriction !== undefined)
68
+ writer.appendLine(`double staticFriction = ${colliderSource.sharedMaterial?.staticFriction}`);
69
+ writer.closeBlock( "}" );
70
+ }
71
+ writer.closeBlock( "}" );
72
+ });
73
+
74
+ // we can remove the temporary component again here
75
+ if (temporaryRigidbody)
76
+ GameObject.removeComponent(temporaryRigidbody);
77
+ }
78
+
79
+ if (colliderSource) {
80
+ // TODO: Apple only allows one collider, Unity allows many, are many typically used on each object though? What can we do here? combine them? take the first?
81
+ model.addEventListener('serialize', (writer: USDWriter, _context: USDZExporterContext) => {
82
+ writer.beginBlock(`def RealityKitComponent "Collider"`, "{", true );
83
+ // TODO: a group is needed for collisions, not sure what this controls though
84
+ writer.appendLine(`uint group = 1`);
85
+ writer.appendLine(`uniform token info:id = "RealityKit.Collider"`);
86
+
87
+ // TODO: a mask is needed for collisions, this is the max value for int, possibly collision flags but i'm not sure what each bit means, or if we have that exported info from unity
88
+ // This is coming from RealityComposerPro
89
+ writer.appendLine(`uint mask = 4294967295`);
90
+ const isTrigger = colliderSource.isTrigger; // not currently used, see comment above
91
+ const typeName = isTrigger ? "Trigger" : "Default";
92
+ writer.appendLine(`token type = "${typeName}"`);
93
+ writer.beginBlock(`def RealityKitStruct "Shape"`, "{", true );
94
+ if (colliderSource instanceof SphereCollider){
95
+ const sphereCollider = colliderSource as SphereCollider;
96
+ writer.appendLine(`token shapeType = "Sphere"`);
97
+ writer.appendLine(`float radius = ${sphereCollider.radius}`);
98
+ }
99
+ else if (colliderSource instanceof BoxCollider){
100
+ const boxCollider = colliderSource as BoxCollider;
101
+ writer.appendLine(`token shapeType = "Box"`);
102
+ writer.appendLine(`float3 extent = (${boxCollider.size.x}, ${boxCollider.size.y}, ${boxCollider.size.z})`);
103
+ }
104
+ else if (colliderSource instanceof CapsuleCollider){
105
+ const capsuleCollider = colliderSource as CapsuleCollider;
106
+ writer.appendLine(`token shapeType = "Capsule"`);
107
+ writer.appendLine(`float radius = ${capsuleCollider.radius}`);
108
+ writer.appendLine(`float height = ${capsuleCollider.height}`);
109
+ }
110
+ else if (colliderSource instanceof MeshCollider && colliderSource.sharedMesh?.geometry) {
111
+ // get the bounds of the mesh
112
+ const geo = colliderSource.sharedMesh.geometry;
113
+ if (!geo.boundingBox) geo.computeBoundingBox();
114
+ const box = colliderSource.sharedMesh.geometry.boundingBox;
115
+ if (box) {
116
+ writer.appendLine(`token shapeType = "Box"`);
117
+ writer.appendLine(`float3 extent = (${box.max.x - box.min.x}, ${box.max.y - box.min.y}, ${box.max.z - box.min.z})`);
118
+ console.log("[USDZ]: Only Box, Sphere, and Capsule colliders are supported in visionOS/iOS. MeshCollider will be exported as Box", colliderSource);
119
+ }
120
+ }
121
+ else {
122
+ console.warn("[USDZ]: Only Box, Sphere, and Capsule colliders are supported in visionOS/iOS. Ignoring collider:", colliderSource)
123
+ }
124
+
125
+ writer.beginBlock(`def RealityKitStruct "pose"`, "{", true );
126
+ // TODO: this is used for rotating the collider, I think this isn't necessary since all the changes happen in the parent transform from needle. Not positive about this though.
127
+ writer.closeBlock( "}" );
128
+ writer.closeBlock( "}" );
129
+ writer.closeBlock( "}" );
130
+ });
131
+
132
+ if (colliderSources.length > 1) {
133
+ console.log("WARNING: Multiple colliders detected. visionOS / iOS can only support objects with a single collider, only exporting the first collider: ", colliderSource)
134
+ }
135
+ }
136
+ }
137
+ }