Needle Engine

Changes between version 3.6.14 and 3.7.1-alpha
Files changed (19) hide show
  1. src/include/three/ARButton.js +9 -6
  2. src/engine-components/export/usdz/extensions/behavior/AudioExtension.ts +3 -1
  3. src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +13 -4
  4. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +19 -6
  5. src/engine/debug/debug_console.ts +36 -9
  6. src/engine/debug/debug_overlay.ts +19 -20
  7. src/engine/engine_context.ts +10 -4
  8. src/engine/engine_element_extras.ts +5 -0
  9. src/engine/engine_element_loading.ts +1 -1
  10. src/engine/engine_element_overlay.ts +45 -26
  11. src/engine/engine_element.ts +35 -2
  12. src/engine/engine_input.ts +6 -1
  13. src/engine/extensions/NEEDLE_progressive.ts +20 -8
  14. src/engine-components/Renderer.ts +4 -2
  15. src/engine-components/ui/Text.ts +21 -6
  16. src/engine-components/export/usdz/ThreeUSDZExporter.ts +5 -10
  17. src/engine-components/export/usdz/USDZExporter.ts +23 -5
  18. src/engine-components/export/usdz/extensions/USDZText.ts +3 -1
  19. src/engine-components/webxr/WebXR.ts +20 -3
src/include/three/ARButton.js CHANGED
@@ -50,11 +50,13 @@
50
50
  // Workaround: seems WebXR Viewer has a non-standard behaviour when it comes to DOM Overlay and Canvas;
51
51
  // HTMLElements that are inside the Canvas element are not visible in the DOM Overlay.
52
52
  const isWebXRViewer = /WebXRViewer\//i.test( navigator.userAgent );
53
- if (isWebXRViewer) {
53
+ const overlayElement = options.domOverlay.root;
54
+ if (isWebXRViewer)
55
+ {
54
56
  if(options.domOverlay?.root) {
55
- const overlayElement = options.domOverlay.root;
56
- originalDomOverlayParent = overlayElement.parentElement;
57
- if (originalDomOverlayParent) {
57
+ originalDomOverlayParent = overlayElement.parentNode;
58
+ if (originalDomOverlayParent)
59
+ {
58
60
  console.log("Reparent DOM Overlay to body", overlayElement, overlayElement.style.display);
59
61
  // mozilla webxr does hide elements on session start
60
62
  // this is only necessary if we generated the overlay element
@@ -90,12 +92,13 @@
90
92
 
91
93
  button.textContent = 'START AR';
92
94
 
95
+ const overlayElement = options.domOverlay.root;
93
96
  // if we reparented the DOM overlay, we're reverting it here
94
97
  if (originalDomOverlayParent)
95
- originalDomOverlayParent.appendChild(options.domOverlay.root);
98
+ originalDomOverlayParent.appendChild(overlayElement);
96
99
 
97
100
  if (ARButtonControlsDomOverlay)
98
- options.domOverlay.root.style.display = 'none';
101
+ overlayElement.style.display = 'none';
99
102
 
100
103
  currentSession = null;
101
104
 
src/engine-components/export/usdz/extensions/behavior/AudioExtension.ts CHANGED
@@ -26,7 +26,10 @@
26
26
  continue;
27
27
 
28
28
  const clipName = audioSource.clip.split("/").pop();
29
+ // TODO: ensure audio clip doesnt start with number
30
+
29
31
 
32
+ // TODO: store clipname in file and use in onAfterSerialize instead of creating it again!
30
33
  if (!this.files.includes(audioSource.clip)) {
31
34
  this.files.push(audioSource.clip);
32
35
  }
@@ -46,7 +49,6 @@
46
49
  }
47
50
 
48
51
  async onAfterSerialize(context: USDZExporterContext) {
49
- console.warn("onAfterSerialize", this);
50
52
  // write the files to the context.
51
53
  for (const file of this.files) {
52
54
 
src/engine-components/export/usdz/extensions/behavior/Behaviour.ts CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  export interface UsdzBehaviour {
8
8
  createBehaviours?(ext: BehaviorExtension, model: USDObject, context: IContext): void;
9
- beforeCreateDocument?(ext: BehaviorExtension, context: IContext): void;
10
- afterCreateDocument?(ext: BehaviorExtension, context: IContext): void;
9
+ beforeCreateDocument?(ext: BehaviorExtension, context: IContext): void | Promise<void>;
10
+ afterCreateDocument?(ext: BehaviorExtension, context: IContext): void | Promise<void>
11
11
  afterSerialize?(ext: BehaviorExtension, context: IContext): void;
12
12
  }
13
13
 
@@ -29,19 +29,28 @@
29
29
 
30
30
 
31
31
  onBeforeBuildDocument(context) {
32
+ const beforeCreateDocumentPromises : Array<Promise<any>> = [];
32
33
  context.root.traverse(e => {
33
34
  GameObject.foreachComponent(e, (comp) => {
34
35
  const c = comp as unknown as UsdzBehaviour;
36
+ // Test if the components has any of the behaviour type methods
35
37
  if (
36
38
  typeof c.createBehaviours === "function" ||
37
39
  typeof c.beforeCreateDocument === "function" ||
38
- typeof c.afterCreateDocument === "function"
40
+ typeof c.afterCreateDocument === "function" ||
41
+ typeof c.afterSerialize === "function"
39
42
  ) {
40
43
  this.behaviourComponents.push(c);
41
- c.beforeCreateDocument?.call(c, this, context);
44
+ // run beforeCreateDocument. We run them in parallel if any of them is async because the order in which this is invoked on the components is not guaranteed anyways
45
+ // (or at least no behaviour component should rely on another to have finished this method)
46
+ const res = c.beforeCreateDocument?.call(c, this, context);
47
+ if(res instanceof Promise) {
48
+ beforeCreateDocumentPromises.push(res);
49
+ }
42
50
  }
43
51
  }, false);
44
52
  });
53
+ return Promise.all(beforeCreateDocumentPromises);
45
54
  }
46
55
 
47
56
  onExportObject(_object, model: USDObject, context) {
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  import { BehaviorExtension, UsdzBehaviour } from "./Behaviour";
13
13
  import { ActionBuilder, ActionModel, AuralMode, BehaviorModel, IBehaviorElement, MotionType, PlayAction, Space, TriggerBuilder } from "./BehavioursBuilder";
14
14
  import { AudioSource } from "../../../../AudioSource";
15
+ import { NEEDLE_progressive } from "../../../../../engine/extensions/NEEDLE_progressive";
15
16
 
16
17
  export class ChangeTransformOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
17
18
 
@@ -150,6 +151,9 @@
150
151
  @serializable(Material)
151
152
  variantMaterial?: Material;
152
153
 
154
+ @serializable()
155
+ fadeDuration : number = 0;
156
+
153
157
  private _objectsWithThisMaterial: Renderer[] = [];
154
158
 
155
159
  awake() {
@@ -176,9 +180,17 @@
176
180
  private static _materialTriggersPerId: { [key: string]: ChangeMaterialOnClick[] } = {}
177
181
 
178
182
 
179
- beforeCreateDocument(_ext: BehaviorExtension, _context) {
183
+ async beforeCreateDocument(_ext: BehaviorExtension, _context) {
180
184
  this.targetModels = [];
181
185
  ChangeMaterialOnClick._materialTriggersPerId = {}
186
+
187
+ // Ensure that the progressive textures have been loaded for all variants and materials
188
+ if (this.materialToSwitch) {
189
+ await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, this.materialToSwitch, 0);
190
+ }
191
+ if(this.variantMaterial) {
192
+ await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, this.variantMaterial, 0);
193
+ }
182
194
  }
183
195
 
184
196
 
@@ -226,17 +238,19 @@
226
238
  const start: ActionModel[] = [];
227
239
  const select: ActionModel[] = [];
228
240
 
241
+ const fadeDuration = Math.max(0, this.fadeDuration);
242
+
229
243
  // the order here matters
230
244
  for (const target of this.targetModels) {
231
- const hideOriginal = ActionBuilder.fadeAction(target, 0, false);
245
+ const hideOriginal = ActionBuilder.fadeAction(target, fadeDuration, false);
232
246
  select.push(hideOriginal);
233
247
  }
234
248
  for (const v of otherVariants) {
235
- select.push(ActionBuilder.fadeAction(v, 0, false));
249
+ select.push(ActionBuilder.fadeAction(v, fadeDuration, false));
236
250
  }
237
251
  for (const v of myVariants) {
238
- start.push(ActionBuilder.fadeAction(v, 0, false));
239
- select.push(ActionBuilder.fadeAction(v, 0, true));
252
+ start.push(ActionBuilder.fadeAction(v, fadeDuration, false));
253
+ select.push(ActionBuilder.fadeAction(v, fadeDuration, true));
240
254
  }
241
255
 
242
256
  ext.addBehavior(new BehaviorModel("Select " + this.selfModel.name,
@@ -436,7 +450,6 @@
436
450
  newAudioSource.spatialBlend = 1;
437
451
  newAudioSource.volume = 1;
438
452
  newAudioSource.loop = false;
439
-
440
453
  this.target = newAudioSource;
441
454
  }
442
455
  }
src/engine/debug/debug_console.ts CHANGED
@@ -20,19 +20,31 @@
20
20
  if (isLocalNetwork())
21
21
  console.log("Add the ?console query parameter to the url to show the debug console (on mobile it will automatically open for local development when your get errors)");
22
22
  const isMobile = isMobileDevice();
23
- if (isMobile) {
23
+ if (isMobile)
24
+ {
24
25
  beginWatchingLogs();
25
26
  createConsole(true);
26
- if (isMobile) {
27
+ if (isMobile)
28
+ {
27
29
  const engineElement = document.querySelector("needle-engine");
30
+ // setTimeout(() => {
31
+ // const el = getConsoleElement();
32
+ // console.log(el);
33
+ // if (el) {
34
+ // const overlay = engineElement["getAROverlayContainer"]?.call(engineElement);
35
+ // overlay.appendChild(el);
36
+ // }
37
+ // }, 1000)
28
38
  engineElement?.addEventListener("enter-ar", () => {
29
39
  if (showConsole || consoleInstance || getErrorCount() > 0) {
30
40
  if (getParam("noerrors")) return;
31
- const overlay = engineElement["getAROverlayContainer"]?.call(engineElement);
32
- const consoleElement = getConsoleElement();
33
- if (consoleElement && overlay) {
34
- overlay.appendChild(consoleElement);
35
- }
41
+ // TODO: this doesnt work with shadow DOM since styles are added to the HEAD by the console script
42
+ // and therefor not applied anymore when the element is moved to the overlay container
43
+ // const overlay = engineElement["getAROverlayContainer"]?.call(engineElement);
44
+ // const consoleElement = getConsoleElement();
45
+ // if (consoleElement && overlay) {
46
+ // overlay.appendChild(consoleElement);
47
+ // }
36
48
  }
37
49
  });
38
50
  engineElement?.addEventListener("exit-ar", () => {
@@ -103,7 +115,6 @@
103
115
  isLoading = true;
104
116
 
105
117
  const script = document.createElement("script");
106
- script.src = "https://unpkg.com/vconsole@latest/dist/vconsole.min.js";
107
118
  script.onload = () => {
108
119
  isLoading = false;
109
120
  isVisible = true;
@@ -115,6 +126,21 @@
115
126
  consoleHtmlElement[$defaultConsoleParent] = consoleHtmlElement.parentElement;
116
127
  consoleHtmlElement.style.position = "absolute";
117
128
  consoleHtmlElement.style.zIndex = Number.MAX_SAFE_INTEGER.toString();
129
+ // const styleSheetList = document.styleSheets;
130
+ // for (let i = 0; i < styleSheetList.length; i++) {
131
+ // const styleSheet = styleSheetList[i];
132
+ // const firstRule = styleSheet.cssRules[0] as CSSStyleRule;
133
+ // if(firstRule && firstRule.selectorText === "#__vconsole") {
134
+ // console.log("found vconsole style sheet");
135
+ // const styleTag = document.createElement("style");
136
+ // styleTag.innerHTML = "#__needleconsole {}";
137
+ // for (let j = 0; j < styleSheet.cssRules.length; j++) {
138
+ // const rule = styleSheet.cssRules[j] as CSSStyleRule;
139
+ // styleTag.innerHTML += rule.cssText;
140
+ // }
141
+ // consoleHtmlElement.appendChild(styleTag);
142
+ // }
143
+ // }
118
144
  }
119
145
  consoleInstance.setSwitchPosition(20, 10);
120
146
  consoleSwitchButton = getConsoleSwitchButton();
@@ -163,12 +189,13 @@
163
189
  }
164
190
  }
165
191
  `;
166
- document.head.appendChild(styles);
192
+ consoleHtmlElement?.prepend(styles);
167
193
  if (startHidden === true)
168
194
  hideDebugConsole();
169
195
  }
170
196
 
171
197
  };
198
+ script.src = "https://unpkg.com/vconsole@latest/dist/vconsole.min.js";
172
199
  document.body.appendChild(script);
173
200
  }
174
201
 
src/engine/debug/debug_overlay.ts CHANGED
@@ -97,7 +97,7 @@
97
97
  }
98
98
  message = newMessage;
99
99
  }
100
- if (message.length <= 0) return;
100
+ if (!message || message.length <= 0) return;
101
101
  showMessage(type, domElement, message);
102
102
  }
103
103
 
@@ -185,26 +185,25 @@
185
185
  const container = document.createElement("div");
186
186
  errorsMap.set(domElement, container);
187
187
  container.setAttribute("data-needle_engine_debug_overlay", "");
188
- container.classList.add(arContainerClassName);
189
- container.classList.add("desktop");
190
188
  container.classList.add("debug-container");
191
- container.style.position = "absolute";
192
- container.style.top = "0";
193
- container.style.right = "5px";
194
- container.style.paddingTop = "0px";
195
- container.style.maxWidth = "70%";
196
- container.style.maxHeight = "calc(100% - 5px)";
197
- container.style.zIndex = "1000";
198
- // container.style.pointerEvents = "none";
199
- container.style.pointerEvents = "scroll";
200
- // container.style["-webkit-overflow-scrolling"] = "touch";
201
- container.style.display = "flex";
202
- container.style.alignItems = "end";
203
- container.style.flexDirection = "column";
204
- container.style.color = "white";
205
- container.style.overflow = "auto";
206
- // container.style.border = "1px solid red";
207
- domElement.appendChild(container);
189
+ container.style.cssText = `
190
+ position: absolute;
191
+ top: 0;
192
+ right: 5px;
193
+ padding-top: 0px;
194
+ max-width: 70%;
195
+ max-height: calc(100% - 5px);
196
+ z-index: 1000;
197
+ pointer-events: scroll;
198
+ display: flex;
199
+ align-items: end;
200
+ flex-direction: column;
201
+ color: white;
202
+ overflow: auto;
203
+ `
204
+ if (domElement.shadowRoot)
205
+ domElement.shadowRoot.appendChild(container);
206
+ else domElement.appendChild(container);
208
207
 
209
208
  const style = document.createElement("style");
210
209
  style.innerHTML = logsContainerStyles;
src/engine/engine_context.ts CHANGED
@@ -140,6 +140,12 @@
140
140
  /** the <needle-engine> HTML element */
141
141
  domElement: HTMLElement;
142
142
 
143
+ appendHTMLElement(element: HTMLElement) {
144
+ if (this.domElement.shadowRoot)
145
+ return this.domElement.shadowRoot.appendChild(element);
146
+ else return this.domElement.appendChild(element);
147
+ }
148
+
143
149
  get resolutionScaleFactor() { return this._resolutionScaleFactor; }
144
150
  /** use to scale the resolution up or down of the renderer. default is 1 */
145
151
  set resolutionScaleFactor(val: number) {
@@ -314,7 +320,7 @@
314
320
  this._disposeCallbacks.push(() => this._intersectionObserver?.disconnect());
315
321
  }
316
322
 
317
- private createRenderer(domElement?: HTMLElement) {
323
+ private createRenderer() {
318
324
  this.renderer?.dispose();
319
325
 
320
326
  const params: WebGLRendererParameters = {
@@ -322,7 +328,7 @@
322
328
  };
323
329
 
324
330
  // get canvas already configured in the Needle Engine Web Component
325
- const canvas = domElement?.shadowRoot?.querySelector("canvas");
331
+ const canvas = this.domElement?.shadowRoot?.querySelector("canvas");
326
332
  if (canvas) params.canvas = canvas;
327
333
 
328
334
  this.renderer = new WebGLRenderer(params);
@@ -716,8 +722,8 @@
716
722
  this._sizeChanged = true;
717
723
 
718
724
  if (this._stats) {
719
- this._stats.showPanel(1);
720
- this.domElement.appendChild(this._stats.dom);
725
+ this._stats.showPanel(0);
726
+ this.domElement.shadowRoot?.appendChild(this._stats.dom);
721
727
  }
722
728
 
723
729
  if (debug)
src/engine/engine_element_extras.ts CHANGED
@@ -4,6 +4,11 @@
4
4
  DO NOT IMPORT ENGINE_ELEMENT FROM HERE
5
5
  */
6
6
 
7
+ /**
8
+ * Call with the name of an attribute that you want to receive change events for
9
+ * This is useful for example if you want to add custom attributes to <needle-engine>
10
+ * Use the addAttributeChangeCallback utility methods to register callback events
11
+ */
7
12
  export async function registerObservableAttribute(name: string) {
8
13
  const { NeedleEngineHTMLElement } = await import("./engine_element");
9
14
  if (!NeedleEngineHTMLElement.observedAttributes.includes(name))
src/engine/engine_element_loading.ts CHANGED
@@ -100,7 +100,7 @@
100
100
  }
101
101
 
102
102
  onLoadingUpdate(progress: LoadingProgressArgs | ProgressEvent | number, message?: string) {
103
- if (!this._loadingElement?.parentElement) {
103
+ if (!this._loadingElement?.parentNode) {
104
104
  return;
105
105
  }
106
106
  // console.log(callback.name, callback.progress.loaded / callback.progress.total, callback.index + "/" + callback.count);
src/engine/engine_element_overlay.ts CHANGED
@@ -30,7 +30,7 @@
30
30
  this.currentSession = session;
31
31
  this.arContainer = overlayContainer;
32
32
 
33
- const arElements = context.domElement.querySelectorAll(`.${arContainerClassName}`);
33
+ const arElements = context.domElement.shadowRoot!.querySelectorAll(`.${arContainerClassName}`);
34
34
  arElements.forEach(el => {
35
35
  if (!el) return;
36
36
  if (el === this.arContainer) return;
@@ -76,7 +76,7 @@
76
76
  // Canvas is not in DOM anymore after AR using Mozilla XR
77
77
  const canvas = _context.renderer.domElement;
78
78
  if (canvas) {
79
- _context.domElement.insertBefore(canvas, _context.domElement.firstChild);
79
+ _context.domElement.shadowRoot?.prepend(canvas);
80
80
  }
81
81
 
82
82
  // Fix visibility
@@ -92,35 +92,45 @@
92
92
  }
93
93
 
94
94
  findOrCreateARContainer(element: HTMLElement): HTMLElement {
95
+ if(debug) console.log("findOrCreateARContainer");
95
96
  // search in the needle-engine element
96
97
  if (element.classList.contains(arContainerClassName)) {
98
+ if(debug) console.log("Found overlay container in needle-engine element");
97
99
  return element;
98
100
  }
99
- if (element.children) {
100
- for (let i = 0; i < element.children.length; i++) {
101
- const ch = element.children[i] as HTMLElement;
102
- if (!ch || !ch.classList) continue;
103
- if (ch.classList.contains(arContainerClassName)) {
104
- return ch;
105
- }
106
- }
101
+ if (element.shadowRoot) {
102
+ const el = element.shadowRoot!.querySelector(`.${arContainerClassName}`);
103
+ if (el) {
104
+ if(debug) console.log("Found overlay container in needle-engine element");
105
+ return el as HTMLElement;
106
+ };
107
107
  }
108
108
 
109
109
  // search in document as well; "ar" element could live outside needle-engine element
110
110
  const arElements = document.getElementsByClassName(arContainerClassName);
111
- if (arElements && arElements.length > 0)
111
+ if (arElements && arElements.length > 0){
112
+ if(debug) console.log("Found overlay container in document");
112
113
  return arElements[0] as HTMLElement;
114
+ }
113
115
 
114
116
  if (debug)
115
117
  console.log("No overlay container found in document - generating new ony");
116
118
  const el = document.createElement("div");
117
119
  el.classList.add(arContainerClassName);
118
- el.style.position = "absolute";
119
- el.style.width = "100%";
120
- el.style.height = "100%";
121
- el.style.display = "flex";
122
- el.style.visibility = "visible";
123
- return element.appendChild(el);
120
+ el.style.cssText = `
121
+ position: fixed;
122
+ top: 0;
123
+ left: 0;
124
+ width: 100%;
125
+ height: 100%;
126
+ display: flex;
127
+ visibility: visible;
128
+ z-index: 9999;
129
+ pointer-events: none;
130
+ // background: rgba(0,0,0,1);
131
+ `;
132
+ if(debug) this.createFallbackCloseARButton(element);
133
+ return this.appendElement(el, element) as HTMLElement;
124
134
  }
125
135
 
126
136
  private onRequestedEndAR() {
@@ -135,24 +145,33 @@
135
145
  }
136
146
 
137
147
  private createFallbackCloseARButton(element: HTMLElement) {
148
+ const quitARSlot = document.createElement("slot");
149
+ quitARSlot.setAttribute("name", "quit-ar");
150
+ this.appendElement(quitARSlot, element);
151
+ if(debug) quitARSlot.addEventListener('click', () => console.log("Clicked fallback close button"));
152
+ quitARSlot.addEventListener('click', this.closeARCallback);
153
+ this._createdAROnlyElements.push(quitARSlot);
154
+ // for mozilla XR reparenting we have to make sure the close button is clickable so we set it on the element directly
155
+ // it's in general perhaps more safe to set it on the element to ensure it's clickable
156
+ quitARSlot.style.pointerEvents = "auto";
157
+
138
158
  var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
159
+ svg.classList.add("quit-ar-button");
139
160
  svg.setAttribute('width', "38px");
140
161
  svg.setAttribute('height', "38px");
141
- svg.style.position = 'absolute';
142
- svg.style.right = '20px';
143
- svg.style.top = '40px';
144
- svg.style.zIndex = '9999';
145
- svg.style.pointerEvents = 'auto';
146
- svg.addEventListener('click', this.closeARCallback);
147
- element.appendChild(svg);
148
- this._createdAROnlyElements.push(svg);
162
+ quitARSlot.appendChild(svg);
149
163
 
150
164
  var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
151
165
  path.setAttribute('d', 'M 12,12 L 28,28 M 28,12 12,28');
152
166
  path.setAttribute('stroke', '#ddd');
153
167
  path.setAttribute('stroke-width', "3px");
154
168
  svg.appendChild(path);
155
- this._createdAROnlyElements.push(path);
169
+ if(debug) console.log("Created fallback close button", svg, element);
156
170
  }
157
171
 
172
+ private appendElement(element: Element, parent: HTMLElement) {
173
+ if(parent.shadowRoot) return parent.shadowRoot.appendChild(element);
174
+ return parent.appendChild(element);
175
+ }
176
+
158
177
  }
src/engine/engine_element.ts CHANGED
@@ -91,9 +91,41 @@
91
91
  constructor() {
92
92
  super();
93
93
  this._overlay_ar = new AROverlayHandler();
94
- this._context = new Context({ domElement: this });
95
94
  // TODO: do we want to rename this event?
96
95
  this.addEventListener("ready", this.onReady);
96
+
97
+ this.attachShadow({mode: 'open'});
98
+ const template = document.createElement('template');
99
+ template.innerHTML =
100
+ `<style>
101
+ :host {
102
+ position: relative;
103
+ display: block;
104
+ width: 600px;
105
+ height: 300px;
106
+ }
107
+ :host canvas {
108
+ position: absolute;
109
+ user-select: none;
110
+ touch-action: none;
111
+ }
112
+ :host slot[name="quit-ar"]:hover {
113
+ cursor: pointer;
114
+ }
115
+ :host .quit-ar-button {
116
+ position: absolute;
117
+ top: 40px;
118
+ right: 20px;
119
+ z-index: 9999;
120
+ }
121
+ </style>
122
+ <canvas></canvas>
123
+ `;
124
+
125
+ if (this.shadowRoot)
126
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
127
+
128
+ this._context = new Context({ domElement: this });
97
129
  this.addEventListener("error", this.onError);
98
130
  }
99
131
 
@@ -137,7 +169,7 @@
137
169
  }
138
170
 
139
171
  attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
140
- // console.log(name, oldValue, newValue);
172
+ if (debug) console.log("attributeChangedCallback", name, _oldValue, newValue);
141
173
  switch (name) {
142
174
  case "src":
143
175
  if (debug) console.warn("src changed to type:", typeof newValue, ", from \"", _oldValue, "\" to \"", newValue, "\"")
@@ -413,6 +445,7 @@
413
445
  this.classList.add(arSessionActiveClassName);
414
446
  this.classList.remove(desktopSessionActiveClassName);
415
447
  const arContainer = this.getAROverlayContainer();
448
+ if(debug) console.warn("onSetupAR:", arContainer)
416
449
  if (arContainer) {
417
450
  arContainer.classList.add(arSessionActiveClassName);
418
451
  arContainer.classList.remove(desktopSessionActiveClassName);
src/engine/engine_input.ts CHANGED
@@ -397,7 +397,12 @@
397
397
  // if(evt.target === this.context.renderer.domElement) return true;
398
398
  // const css = window.getComputedStyle(evt.target as HTMLElement);
399
399
  // if(css.pointerEvents === "all") return false;
400
- return evt.target === this.context.renderer.domElement;
400
+
401
+ // We only check the target elements here since the canvas may be overlapped by other elements
402
+ // in which case we do not want to use the input (e.g. if a HTML element is being triggered)
403
+ if(evt.target === this.context.renderer.domElement) return true;
404
+ if(evt.target === this.context.domElement) return true;
405
+ return false;
401
406
  }
402
407
 
403
408
  private keysPressed: { [key: KeyCode | string]: { pressed: boolean, frame: number, startFrame: number, key: string, code: KeyCode | string } } = {};
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -39,30 +39,38 @@
39
39
  export class NEEDLE_progressive implements GLTFLoaderPlugin {
40
40
 
41
41
  static assignTextureLOD(context: Context, source: SourceIdentifier | undefined, material: Material, level: number = 0) {
42
- if (!material) return;
42
+ if (!material) return Promise.resolve(null);
43
43
 
44
+ const promises: Promise<Texture | null>[] = [];
45
+
44
46
  for (let slot of Object.keys(material)) {
45
47
  const val = material[slot];
46
- if (val?.isTexture)
47
- this.assignTextureLODForSlot(context, source, material, level, slot, val);
48
+ if (val?.isTexture) {
49
+ const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
50
+ promises.push(task);
51
+ }
48
52
  }
49
53
 
50
54
  if (material instanceof RawShaderMaterial) {
51
55
  // iterate uniforms
52
56
  for (let slot of Object.keys(material.uniforms)) {
53
57
  const val = material.uniforms[slot].value;
54
- if (val?.isTexture)
55
- this.assignTextureLODForSlot(context, source, material, level, slot, val);
58
+ if (val?.isTexture) {
59
+ const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
60
+ promises.push(task);
61
+ }
56
62
  }
57
63
  }
64
+
65
+ return promises;
58
66
  }
59
67
 
60
- private static assignTextureLODForSlot(context: Context, source: SourceIdentifier | undefined, material: Material, level: number, slot: string, val: any) {
61
- if (val?.isTexture !== true) return;
68
+ private static assignTextureLODForSlot(context: Context, source: SourceIdentifier | undefined, material: Material, level: number, slot: string, val: any): Promise<Texture | null> {
69
+ if (val?.isTexture !== true) return Promise.resolve(null);
62
70
 
63
71
  if (debug) console.log("-----------\n", "FIND", material.name, slot, val?.name, val?.userData, val, material);
64
72
 
65
- NEEDLE_progressive.getOrLoadTexture(context, source, material, slot, val, level).then(t => {
73
+ return NEEDLE_progressive.getOrLoadTexture(context, source, material, slot, val, level).then(t => {
66
74
  if (t?.isTexture === true) {
67
75
 
68
76
  if (debug) console.log("Assign LOD", material.name, slot, t.name, t["guid"], material, "Prev:", val, "Now:", t, "\n--------------");
@@ -83,7 +91,11 @@
83
91
  }
84
92
  entry.lod0 = t;
85
93
  }
94
+
95
+ return t;
86
96
  }
97
+
98
+ return null;
87
99
  });
88
100
  }
89
101
 
src/engine-components/Renderer.ts CHANGED
@@ -184,7 +184,7 @@
184
184
  this.changed = true;
185
185
  }
186
186
 
187
- private getMaterial(index: number) {
187
+ private getMaterial(index: number) : Material | null {
188
188
  index = this.resolveIndex(index);
189
189
  if (index < 0) return null;
190
190
  const obj = this._targets;
@@ -683,9 +683,11 @@
683
683
  if (debugProgressiveLoading) {
684
684
  console.log("Load material LOD", material.name);
685
685
  }
686
- NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, material);
686
+ return NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, material);
687
687
  }
688
688
  }
689
+
690
+ return Promise.resolve(true);
689
691
  }
690
692
 
691
693
  applySettings(go: THREE.Object3D) {
src/engine-components/ui/Text.ts CHANGED
@@ -448,7 +448,9 @@
448
448
  // - Arial instead of assets/arial
449
449
  // - Arial should stay Arial instead of arial
450
450
  if (!this.font) return;
451
- let familyName = this.getFamilyNameWithCorrectSuffix(this.font, fontStyle);
451
+ let fontName = this.font;
452
+ let familyName = this.getFamilyNameWithCorrectSuffix(fontName, fontStyle);
453
+ if (debug) console.log("Selected font family:" + familyName);
452
454
 
453
455
  // ensure a font family is register under this name
454
456
  let fontFamily = ThreeMeshUI.FontLibrary.getFontFamily(familyName as string);
@@ -502,16 +504,29 @@
502
504
  }
503
505
 
504
506
  private getFamilyNameWithCorrectSuffix(familyName: string, style: FontStyle): string {
505
-
506
507
  // we can only change the style for the family if the name has a suffix (e.g. Arial-Bold)
507
508
  const styleSeparator = familyName.lastIndexOf('-');
508
509
  if (styleSeparator < 0) return familyName;
509
510
 
511
+ // Check if the font name contains a style that we don't support in the enum
512
+ // e.g. -Medium, -Black, -Thin...
513
+ const styleName = familyName.substring(styleSeparator + 1)?.toLowerCase();
514
+ if (unsupportedStyleNames.includes(styleName)) {
515
+ if(debug) console.warn("Unsupported font style: " + styleName);
516
+ return familyName;
517
+ }
518
+
510
519
  // Try find a suffix that matches the style
511
520
  // We assume that if the font name is "Arial-Regular" then the bold version is "Arial-Bold"
512
521
  // and if the font name is "arial-regular" then the bold version is "arial-bold"
513
- let isUpperCase = familyName[0] === familyName[0].toUpperCase();
522
+ const pathSeparatorIndex = familyName.lastIndexOf("/");
523
+ let fontBaseName = familyName;
524
+ if (pathSeparatorIndex >= 0) {
525
+ fontBaseName = fontBaseName.substring(pathSeparatorIndex + 1);
526
+ }
527
+ let isUpperCase = fontBaseName[0] === fontBaseName[0].toUpperCase();
514
528
  const fontNameWithoutSuffix = familyName.substring(0, styleSeparator);
529
+ if (debug) console.log("Select font: ", familyName, FontStyle[style], fontBaseName, isUpperCase, fontNameWithoutSuffix);
515
530
 
516
531
  switch (style) {
517
532
  case FontStyle.Normal:
@@ -552,6 +567,6 @@
552
567
  // const regex = new RegExp('<(?<type>.+?)>(?<text>.+?)<\/.+?>', 'g');
553
568
 
554
569
 
555
- const upperCaseStyleSuffixOptions = [
556
- "Regular", "Bold", "Italic", "BoldItalic", "Medium", "MediumItalic", "SemiBold", "SemiBoldItalic", "Thin", "ThinItalic", "ExtraLight", "ExtraLightItalic", "Light", "LightItalic", "Black", "BlackItalic"
557
- ];
570
+ const unsupportedStyleNames = [
571
+ "medium", "mediumitalic", "black", "blackitalic", "thin", "thinitalic", "extrabold", "light", "lightitalic"
572
+ ]
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -440,11 +440,11 @@
440
440
  const materials = context.materials;
441
441
  const textures = context.textures;
442
442
 
443
- invokeAll( context, 'onBeforeBuildDocument' );
443
+ await invokeAll( context, 'onBeforeBuildDocument' );
444
444
 
445
445
  traverseVisible( scene, context.document, context );
446
446
 
447
- invokeAll( context, 'onAfterBuildDocument' );
447
+ await invokeAll( context, 'onAfterBuildDocument' );
448
448
 
449
449
  parseDocument( context );
450
450
 
@@ -704,15 +704,10 @@
704
704
  if ( typeof ext[ name ] === 'function' ) {
705
705
 
706
706
  const method = ext[ name ];
707
-
708
- const isAsync = method.constructor.name === "AsyncFunction";
709
-
710
- if ( isAsync ) {
711
- await method.call( ext, context, writer );
712
- } else {
713
- method.call( ext, context, writer );
707
+ const res = method.call( ext, context, writer );
708
+ if(res instanceof Promise) {
709
+ await res;
714
710
  }
715
-
716
711
  }
717
712
 
718
713
  }
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  import { BehaviorExtension } from "./extensions/behavior/Behaviour";
17
17
  import { AudioExtension } from "./extensions/behavior/AudioExtension";
18
18
  import { TextExtension } from "./extensions/USDZText";
19
+ import { Renderer } from "../../Renderer"
19
20
 
20
21
  const debug = getParam("debugusdz");
21
22
 
@@ -136,7 +137,7 @@
136
137
  }
137
138
 
138
139
  async exportAsync() {
139
-
140
+
140
141
  let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
141
142
  if (!hasProLicense()) name += "-MadeWithNeedle";
142
143
  name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
@@ -146,7 +147,7 @@
146
147
 
147
148
  // see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look
148
149
  const overlay = this.buildQuicklookOverlay();
149
- if(debug) console.log(overlay);
150
+ if (debug) console.log(overlay);
150
151
  const callToAction = overlay.callToAction ? encodeURIComponent(overlay.callToAction) : "";
151
152
  const checkoutTitle = overlay.checkoutTitle ? encodeURIComponent(overlay.checkoutTitle) : "";
152
153
  const checkoutSubtitle = overlay.checkoutSubtitle ? encodeURIComponent(overlay.checkoutSubtitle) : "";
@@ -167,6 +168,22 @@
167
168
 
168
169
  if (!this.objectToExport) return;
169
170
 
171
+ // trigger progressive textures to be loaded:
172
+ const renderers = GameObject.getComponentsInChildren(this.objectToExport, Renderer);
173
+ const progressiveLoading = new Array<Promise<any>>();
174
+ for (const rend of renderers) {
175
+ for (const mat of rend.sharedMaterials) {
176
+ if (mat) {
177
+ const task = rend.loadProgressiveTextures(mat);
178
+ if (task instanceof Promise)
179
+ progressiveLoading.push(task);
180
+ }
181
+ }
182
+ }
183
+ if (debug) showBalloonMessage("Load textures: " + progressiveLoading.length);
184
+ await Promise.all(progressiveLoading);
185
+ if(debug) showBalloonMessage("Load textures: done");
186
+
170
187
  // make sure we apply the AR scale
171
188
  if (this.webARSessionRoot) {
172
189
  const scene = this.webARSessionRoot.gameObject;
@@ -192,7 +209,7 @@
192
209
 
193
210
  //@ts-ignore
194
211
  exporter.debug = debug;
195
-
212
+
196
213
  // sanitize anchoring types
197
214
  if (this.anchoringType !== "plane" && this.anchoringType !== "none" && this.anchoringType !== "image" && this.anchoringType !== "face")
198
215
  this.anchoringType = "plane";
@@ -226,7 +243,7 @@
226
243
 
227
244
  // see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look
228
245
  const overlay = this.buildQuicklookOverlay();
229
- if(debug) console.log(overlay);
246
+ if (debug) console.log(overlay);
230
247
  const callToAction = overlay.callToAction ? encodeURIComponent(overlay.callToAction) : "";
231
248
  const checkoutTitle = overlay.checkoutTitle ? encodeURIComponent(overlay.checkoutTitle) : "";
232
249
  const checkoutSubtitle = overlay.checkoutSubtitle ? encodeURIComponent(overlay.checkoutSubtitle) : "";
@@ -325,7 +342,7 @@
325
342
  else {
326
343
  this.webxr.createARButton = false;
327
344
  this.webxr.createVRButton = false;
328
- let container = window.document.querySelector(".webxr-buttons");
345
+ let container = this.context.domElement.shadowRoot!.querySelector(".webxr-buttons");
329
346
  if (!container) {
330
347
  container = document.createElement("div");
331
348
  container.classList.add("webxr-buttons");
@@ -338,6 +355,7 @@
338
355
  button.classList.add('webxr-ar-button');
339
356
  button.classList.add('webxr-button');
340
357
  button.classList.add("quicklook-ar-button");
358
+ this._quicklookButton = button;
341
359
  container.appendChild(button);
342
360
  this._quicklookButtonContainer = container;
343
361
  this.dispatchEvent(new CustomEvent("created-button", { detail: button }))
src/engine-components/export/usdz/extensions/USDZText.ts CHANGED
@@ -164,7 +164,9 @@
164
164
 
165
165
  // model.matrix.scale(new Vector3(100, 100, 100));
166
166
  newModel.addEventListener("serialize", (writer: USDWriter, _context: USDZExporterContext) => {
167
- const textObj = TextBuilder.multiLine(text.text, width, height, HorizontalAlignment.center, VerticalAlignment.bottom, TextWrapMode.flowing);
167
+ let txt = text.text;
168
+ txt = txt.replace(/\n/g, "\\n");
169
+ const textObj = TextBuilder.multiLine(txt, width, height, HorizontalAlignment.center, VerticalAlignment.bottom, TextWrapMode.flowing);
168
170
  this.setTextAlignment(textObj, text.alignment);
169
171
  this.setOverflow(textObj, text);
170
172
  if (newModel.material)
src/engine-components/webxr/WebXR.ts CHANGED
@@ -18,6 +18,7 @@
18
18
  import { XRFlag, XRState, XRStateFlag } from "../XRFlag";
19
19
  import { showBalloonWarning } from '../../engine/debug';
20
20
 
21
+ const debugWebXR = getParam("debugwebxr");
21
22
 
22
23
  export async function detectARSupport() {
23
24
  if(isMozillaXR()) return true;
@@ -238,10 +239,26 @@
238
239
  let arButton, vrButton;
239
240
  const buttonsContainer = document.createElement('div');
240
241
  buttonsContainer.classList.add("webxr-buttons");
241
- this.context.domElement.append(buttonsContainer);
242
+ buttonsContainer.style.cssText = `
243
+ position: fixed;
244
+ bottom: 21px;
245
+ left: 50%;
246
+ transform: translate(-50%, 0%);
247
+ z-index: 1000;
248
+
249
+ display: flex;
250
+ flex-direction: row;
251
+ justify-content: center;
252
+ align-items: flex-start;
253
+ gap: 10px;
254
+ `;
255
+ this.context.appendHTMLElement(buttonsContainer);
242
256
 
257
+ const forceButtons = debugWebXR;
258
+ if(debugWebXR) console.log("ARSupported?", arSupported, "VRSupported?", vrSupported);
259
+
243
260
  // AR support
244
- if (this.enableAR && this.createARButton && arSupported)
261
+ if (forceButtons || (this.createARButton && this.enableAR && arSupported))
245
262
  {
246
263
  arButton = WebXR.createARButton(this);
247
264
  this._arButton = arButton;
@@ -249,7 +266,7 @@
249
266
  }
250
267
 
251
268
  // VR support
252
- if (this.createVRButton && this.enableVR && vrSupported) {
269
+ if (forceButtons || (this.createVRButton && this.enableVR && vrSupported)) {
253
270
  vrButton = WebXR.createVRButton(this);
254
271
  this._vrButton = vrButton;
255
272
  buttonsContainer.appendChild(vrButton);