Needle Engine

Changes between version 3.6.13 and 3.7.0-alpha
Files changed (9) hide show
  1. src/include/three/ARButton.js +9 -6
  2. src/engine/debug/debug_console.ts +36 -9
  3. src/engine/debug/debug_overlay.ts +19 -20
  4. src/engine/engine_context.ts +10 -4
  5. src/engine/engine_element_extras.ts +5 -0
  6. src/engine/engine_element_overlay.ts +45 -26
  7. src/engine/engine_element.ts +35 -2
  8. src/engine/engine_input.ts +6 -1
  9. 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/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_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-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);