@@ -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
|
-
|
53
|
+
const overlayElement = options.domOverlay.root;
|
54
|
+
if (isWebXRViewer)
|
55
|
+
{
|
54
56
|
if(options.domOverlay?.root) {
|
55
|
-
|
56
|
-
originalDomOverlayParent
|
57
|
-
|
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(
|
98
|
+
originalDomOverlayParent.appendChild(overlayElement);
|
96
99
|
|
97
100
|
if (ARButtonControlsDomOverlay)
|
98
|
-
|
101
|
+
overlayElement.style.display = 'none';
|
99
102
|
|
100
103
|
currentSession = null;
|
101
104
|
|
@@ -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
|
|
@@ -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
|
-
|
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) {
|
@@ -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,
|
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,
|
249
|
+
select.push(ActionBuilder.fadeAction(v, fadeDuration, false));
|
236
250
|
}
|
237
251
|
for (const v of myVariants) {
|
238
|
-
start.push(ActionBuilder.fadeAction(v,
|
239
|
-
select.push(ActionBuilder.fadeAction(v,
|
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
|
}
|
@@ -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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
|
@@ -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.
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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;
|
@@ -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(
|
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(
|
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)
|
@@ -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))
|
@@ -100,7 +100,7 @@
|
|
100
100
|
}
|
101
101
|
|
102
102
|
onLoadingUpdate(progress: LoadingProgressArgs | ProgressEvent | number, message?: string) {
|
103
|
-
if (!this._loadingElement?.
|
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);
|
@@ -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.
|
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.
|
100
|
-
|
101
|
-
|
102
|
-
if (
|
103
|
-
|
104
|
-
|
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.
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
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
|
-
|
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
|
}
|
@@ -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
|
-
|
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);
|
@@ -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
|
-
|
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 } } = {};
|
@@ -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
|
|
@@ -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) {
|
@@ -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
|
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
|
-
|
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
|
556
|
-
"
|
557
|
-
]
|
570
|
+
const unsupportedStyleNames = [
|
571
|
+
"medium", "mediumitalic", "black", "blackitalic", "thin", "thinitalic", "extrabold", "light", "lightitalic"
|
572
|
+
]
|
@@ -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
|
-
|
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
|
}
|
@@ -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 =
|
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 }))
|
@@ -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
|
-
|
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)
|
@@ -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
|
-
|
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.
|
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);
|