@@ -58,7 +58,7 @@
|
|
58
58
|
return null;
|
59
59
|
}
|
60
60
|
|
61
|
-
console.log("INFO: Resolve license for " + license.id + "::" + license.key);
|
61
|
+
console.log("INFO: Resolve license for " + obscure(license.id + "::" + license.key));
|
62
62
|
const url = await fetch(LICENSE_ENDPOINT, { method: "GET" });
|
63
63
|
if (!url.ok) {
|
64
64
|
console.warn("WARN: Failed to fetch license URL from endpoint. " + url.statusText);
|
@@ -84,4 +84,14 @@
|
|
84
84
|
}
|
85
85
|
console.warn("WARN: Received invalid license.");
|
86
86
|
return null;
|
87
|
+
}
|
88
|
+
|
89
|
+
|
90
|
+
/**
|
91
|
+
* @param {string} str
|
92
|
+
*/
|
93
|
+
function obscure(str) {
|
94
|
+
const start = str.substring(0, 3);
|
95
|
+
const end = str.substring(str.length - 3);
|
96
|
+
return start + "***" + end;
|
87
97
|
}
|
@@ -50,10 +50,17 @@
|
|
50
50
|
export { needleFacebookInstantGames } from "./facebook-instant-games.js";
|
51
51
|
|
52
52
|
import { vite_4_4_hack } from "./vite-4.4-hack.js";
|
53
|
+
|
53
54
|
import { needleImportsLogger } from "./imports-logger.js";
|
55
|
+
export { needleImportsLogger } from "./imports-logger.js";
|
56
|
+
|
54
57
|
import { needleBuildInfo } from "./buildinfo.js";
|
58
|
+
export { needleBuildInfo } from "./buildinfo.js";
|
55
59
|
|
60
|
+
import { needleServer } from "./server.js";
|
61
|
+
export { needleServer } from "./server.js";
|
56
62
|
|
63
|
+
|
57
64
|
export * from "./gzip.js";
|
58
65
|
export * from "./config.js";
|
59
66
|
|
@@ -112,6 +119,7 @@
|
|
112
119
|
needleImportsLogger(command, config, userSettings),
|
113
120
|
needleBuildPipeline(command, config, userSettings),
|
114
121
|
needlePWA(command, config, userSettings),
|
122
|
+
needleServer(command, config, userSettings),
|
115
123
|
];
|
116
124
|
array.push(await editorConnection(command, config, userSettings, array));
|
117
125
|
return array;
|
@@ -1,15 +1,15 @@
|
|
1
|
-
import {
|
1
|
+
import { Box3Helper, Object3D, PerspectiveCamera, Ray, Vector2, Vector3 } from "three";
|
2
2
|
import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
3
|
-
import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
|
4
3
|
|
5
|
-
import { setCameraController
|
4
|
+
import { setCameraController } from "../engine/engine_camera.js";
|
6
5
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
6
|
+
import { NEPointerEvent } from "../engine/engine_input.js";
|
7
7
|
import { Mathf } from "../engine/engine_math.js";
|
8
8
|
import { RaycastOptions } from "../engine/engine_physics.js";
|
9
9
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
10
10
|
import { getBoundingBox, getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
|
11
11
|
import type { ICameraController } from "../engine/engine_types.js";
|
12
|
-
import {
|
12
|
+
import { getParam, isMobileDevice } from "../engine/engine_utils.js";
|
13
13
|
import { Camera } from "./Camera.js";
|
14
14
|
import { Behaviour, GameObject } from "./Component.js";
|
15
15
|
import { LookAtConstraint } from "./LookAtConstraint.js";
|
@@ -168,29 +168,41 @@
|
|
168
168
|
*/
|
169
169
|
@serializable()
|
170
170
|
enablePan: boolean = true;
|
171
|
-
/** Assigning a {@link LookAtConstraint} will make the camera look at the constraint source
|
171
|
+
/** Assigning a {@link LookAtConstraint} will make the camera look at the constraint source
|
172
|
+
* @default null
|
173
|
+
*/
|
172
174
|
@serializable(LookAtConstraint)
|
173
175
|
lookAtConstraint: LookAtConstraint | null = null;
|
174
|
-
/** The weight of the first lookAtConstraint source
|
176
|
+
/** The weight of the first lookAtConstraint source
|
177
|
+
* @default 1
|
178
|
+
*/
|
175
179
|
@serializable()
|
176
180
|
lookAtConstraint01: number = 1;
|
177
181
|
|
178
|
-
/** If true user input interrupts the camera from animating to a target
|
182
|
+
/** If true user input interrupts the camera from animating to a target
|
183
|
+
* @default true
|
184
|
+
*/
|
179
185
|
@serializable()
|
180
186
|
allowInterrupt: boolean = true;
|
181
187
|
/** If true the camera will focus on the target when the middle mouse button is clicked */
|
182
188
|
@serializable()
|
183
189
|
middleClickToFocus: boolean = true;
|
184
|
-
/** If true the camera will focus on the target when the left mouse button is double clicked
|
190
|
+
/** If true the camera will focus on the target when the left mouse button is double clicked
|
191
|
+
* @default true
|
192
|
+
*/
|
185
193
|
@serializable()
|
186
194
|
doubleClickToFocus: boolean = true;
|
187
|
-
|
188
|
-
|
195
|
+
/**
|
196
|
+
* When enabled the camera will fit the scene to the camera view when the background is clicked the specified number of times within a short time
|
197
|
+
* @default 2
|
198
|
+
*/
|
199
|
+
@serializable()
|
200
|
+
clickBackgroundToFitScene: number = 2;
|
189
201
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
202
|
+
/**
|
203
|
+
* @internal If true debug information will be logged to the console
|
204
|
+
* @default false
|
205
|
+
*/
|
194
206
|
debugLog: boolean = false;
|
195
207
|
|
196
208
|
/**
|
@@ -200,7 +212,9 @@
|
|
200
212
|
get targetLerpSpeed() { return 5 }
|
201
213
|
set targetLerpSpeed(v) { this.targetLerpDuration = 1 / v; }
|
202
214
|
|
203
|
-
/** The duration in seconds it takes for the camera look ad and position lerp to reach their destination (when set via `setCameraTargetPosition` and `setLookTargetPosition`)
|
215
|
+
/** The duration in seconds it takes for the camera look ad and position lerp to reach their destination (when set via `setCameraTargetPosition` and `setLookTargetPosition`)
|
216
|
+
* @default 1
|
217
|
+
*/
|
204
218
|
@serializable()
|
205
219
|
targetLerpDuration = 1;
|
206
220
|
|
@@ -315,6 +329,7 @@
|
|
315
329
|
// if (this._didStart) {
|
316
330
|
// if (this.autoFit) this.fitCamera()
|
317
331
|
// }
|
332
|
+
this.context.input.addEventListener("pointerup", this._onPointerDown);
|
318
333
|
}
|
319
334
|
|
320
335
|
/** @internal */
|
@@ -328,8 +343,37 @@
|
|
328
343
|
this._controls.removeEventListener("start", this.onControlsChangeStarted);
|
329
344
|
// this._controls.reset();
|
330
345
|
}
|
346
|
+
this.context.input.removeEventListener("pointerup", this._onPointerDown);
|
331
347
|
}
|
332
348
|
|
349
|
+
private _lastTimeClickOnBackground: number = -1;
|
350
|
+
private _clickOnBackgroundCount: number = 0;
|
351
|
+
private _onPointerDown = (evt: NEPointerEvent) => {
|
352
|
+
|
353
|
+
if (this.clickBackgroundToFitScene > 0 && evt.isClick) {
|
354
|
+
|
355
|
+
// it's possible that we didnt raycast in this frame
|
356
|
+
if (!evt.hasRay) {
|
357
|
+
evt.intersections.push(...this.context.physics.raycast());
|
358
|
+
}
|
359
|
+
|
360
|
+
if (evt.intersections.length <= 0) {
|
361
|
+
const dt = this.context.time.time - this._lastTimeClickOnBackground;
|
362
|
+
this._lastTimeClickOnBackground = this.context.time.time;
|
363
|
+
if (this.clickBackgroundToFitScene <= 1 || dt < this.clickBackgroundToFitScene * .15) {
|
364
|
+
this._clickOnBackgroundCount += 1;
|
365
|
+
if (this._clickOnBackgroundCount >= this.clickBackgroundToFitScene - 1)
|
366
|
+
this.fitCamera(this.context.scene.children, undefined, false);
|
367
|
+
}
|
368
|
+
else {
|
369
|
+
this._clickOnBackgroundCount = 0;
|
370
|
+
}
|
371
|
+
}
|
372
|
+
|
373
|
+
if (debug) console.log(this.clickBackgroundToFitScene, evt.intersections.length, this._clickOnBackgroundCount)
|
374
|
+
}
|
375
|
+
};
|
376
|
+
|
333
377
|
private onControlsChangeStarted = () => {
|
334
378
|
if (this._syncedTransform) {
|
335
379
|
this._syncedTransform.requestOwnership();
|
@@ -360,7 +404,6 @@
|
|
360
404
|
}
|
361
405
|
this._controls.enabled = true;
|
362
406
|
|
363
|
-
|
364
407
|
this.__handleSetTargetWhenBecomingActiveTheFirstTime();
|
365
408
|
|
366
409
|
if (this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2) || this.context.input.mouseWheelChanged || (this.context.input.getPointerPressed(0) && this.context.input.getPointerPositionDelta(0)?.length() || 0 > .1)) {
|
@@ -585,7 +585,12 @@
|
|
585
585
|
}
|
586
586
|
|
587
587
|
Progress.report('export-usdz', "Traversing hierarchy");
|
588
|
-
if(scene) traverse( scene, context.document, context, this.keepObject);
|
588
|
+
if (scene) traverse( scene, context.document, context, this.keepObject);
|
589
|
+
|
590
|
+
// Root object should have identity matrix
|
591
|
+
// so that root transformations don't end up in the resulting file.
|
592
|
+
// if (context.document.children?.length > 0)
|
593
|
+
// context.document.children[0]?.matrix.identity(); //.multiply(new Matrix4().makeRotationY(Math.PI));
|
589
594
|
|
590
595
|
Progress.report('export-usdz', "Invoking onAfterBuildDocument");
|
591
596
|
await invokeAll( context, 'onAfterBuildDocument' );
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { NEEDLE_progressive } from "@needle-tools/gltf-progressive";
|
2
|
-
import { Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "three";
|
2
|
+
import { Euler, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "three";
|
3
3
|
|
4
4
|
import { showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
|
5
5
|
import { hasProLicense } from "../../../engine/engine_license.js";
|
@@ -572,24 +572,24 @@
|
|
572
572
|
}
|
573
573
|
|
574
574
|
private static invertForwardMatrix = new Matrix4().makeRotationY(Math.PI);
|
575
|
+
private static invertForwardQuaternion = new Quaternion().setFromEuler(new Euler(0, Math.PI, 0));
|
575
576
|
|
576
577
|
private _rootSessionRootWasAppliedTo: Object3D | null = null;
|
577
578
|
private _rootPositionBeforeExport: Vector3 = new Vector3();
|
578
579
|
private _rootRotationBeforeExport: Quaternion = new Quaternion();
|
579
580
|
private _rootScaleBeforeExport: Vector3 = new Vector3();
|
580
581
|
|
581
|
-
getARScaleAndTarget(): { scale: number, _invertForward: boolean, target: Object3D } {
|
582
|
-
if (!this.objectToExport) return { scale: 1, _invertForward: false, target: this.gameObject };
|
582
|
+
getARScaleAndTarget(): { scale: number, _invertForward: boolean, target: Object3D, sessionRoot: Object3D | null} {
|
583
|
+
if (!this.objectToExport) return { scale: 1, _invertForward: false, target: this.gameObject, sessionRoot: null};
|
583
584
|
|
584
585
|
let sessionRoot = GameObject.getComponentInParent(this.objectToExport, WebARSessionRoot);
|
585
|
-
const hasSessionRootInParentHierarchy = sessionRoot !== null && sessionRoot !== undefined;
|
586
586
|
if(!sessionRoot) sessionRoot = GameObject.getComponentInChildren(this.objectToExport, WebARSessionRoot);
|
587
587
|
|
588
588
|
if (debug) console.log("applyWebARSessionRoot", sessionRoot, sessionRoot?.arScale);
|
589
589
|
|
590
590
|
let arScale = 1;
|
591
591
|
let _invertForward = false;
|
592
|
-
const target =
|
592
|
+
const target = this.objectToExport;
|
593
593
|
|
594
594
|
if (!sessionRoot) {
|
595
595
|
const xr = GameObject.findObjectOfType(WebXR);
|
@@ -601,14 +601,16 @@
|
|
601
601
|
}
|
602
602
|
|
603
603
|
const scale = 1 / arScale;
|
604
|
-
|
605
|
-
|
604
|
+
const result = { scale, _invertForward, target, sessionRoot: sessionRoot?.gameObject ?? null };
|
605
|
+
console.log("getARScaleAndTarget", result);
|
606
|
+
return result;
|
606
607
|
}
|
607
608
|
|
608
609
|
private applyWebARSessionRoot() {
|
609
610
|
if (!this.objectToExport) return;
|
610
611
|
|
611
|
-
const { scale, _invertForward, target } = this.getARScaleAndTarget();
|
612
|
+
const { scale, _invertForward, target, sessionRoot } = this.getARScaleAndTarget();
|
613
|
+
const sessionRootMatrixWorld = sessionRoot?.matrixWorld.clone().invert();
|
612
614
|
|
613
615
|
this._rootSessionRootWasAppliedTo = target;
|
614
616
|
this._rootPositionBeforeExport.copy(target.position);
|
@@ -616,17 +618,14 @@
|
|
616
618
|
this._rootScaleBeforeExport.copy(target.scale);
|
617
619
|
|
618
620
|
target.scale.multiplyScalar(scale);
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
}
|
621
|
+
if (_invertForward)
|
622
|
+
target.quaternion.multiply(USDZExporter.invertForwardQuaternion);
|
623
|
+
|
623
624
|
// udate childs as well
|
625
|
+
target.updateMatrix();
|
624
626
|
target.updateMatrixWorld(true);
|
625
|
-
|
626
|
-
|
627
|
-
// basically the inverted effect of WebARSessionRoot.onApplyPose()
|
628
|
-
|
629
|
-
// TODO why are we not reverting this transformation after the export?
|
627
|
+
if (sessionRoot && sessionRootMatrixWorld)
|
628
|
+
target.matrix.premultiply(sessionRootMatrixWorld);
|
630
629
|
}
|
631
630
|
|
632
631
|
private revertWebARSessionRoot() {
|
@@ -639,11 +638,11 @@
|
|
639
638
|
target.scale.copy(this._rootScaleBeforeExport);
|
640
639
|
|
641
640
|
// udate childs as well
|
641
|
+
target.updateMatrix();
|
642
642
|
target.updateMatrixWorld(true);
|
643
643
|
this._rootSessionRootWasAppliedTo = null;
|
644
644
|
}
|
645
645
|
|
646
|
-
|
647
646
|
private createQuicklookButton() {
|
648
647
|
const buttoncontainer = WebXRButtonFactory.getOrCreate();
|
649
648
|
const button = buttoncontainer.createQuicklookButton();
|
@@ -62,4 +62,10 @@
|
|
62
62
|
* Use to activate a needle engine license
|
63
63
|
*/
|
64
64
|
license?: License;
|
65
|
+
|
66
|
+
/**
|
67
|
+
* When set to `true` a plugin will automatically attempt to open the browser using a network ip address when the local server has started
|
68
|
+
* @default undefined
|
69
|
+
*/
|
70
|
+
openBrowser?: boolean;
|
65
71
|
}
|
@@ -39,7 +39,6 @@
|
|
39
39
|
return quat;
|
40
40
|
}
|
41
41
|
|
42
|
-
private static y180 = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
43
42
|
applyToObject(object: Object3D, t01: number | undefined = undefined) {
|
44
43
|
this.ensureTransformData();
|
45
44
|
// check if position/_position or rotation/_rotation changed more than just a little bit
|
@@ -55,7 +54,6 @@
|
|
55
54
|
object.quaternion.slerp(this._rotation, t01);
|
56
55
|
// InstancingUtil.markDirty(object);
|
57
56
|
}
|
58
|
-
object.quaternion.multiply(WebXRTrackedImage.y180);
|
59
57
|
}
|
60
58
|
|
61
59
|
private static _positionBuffer: CircularBuffer<Vector3> = new CircularBuffer(() => new Vector3(), 20);
|
@@ -186,20 +184,29 @@
|
|
186
184
|
const imageTracking = GameObject.findObjectOfType(WebXRImageTracking);
|
187
185
|
if (!imageTracking || !imageTracking.trackedImages) return;
|
188
186
|
|
187
|
+
|
189
188
|
for (const trackedImage of imageTracking.trackedImages) {
|
190
189
|
if (trackedImage.object?.asset === object) {
|
191
190
|
const exporter = GameObject.findObjectOfType(USDZExporter);
|
192
191
|
if (!exporter) continue;
|
193
192
|
|
194
|
-
const { scale } = exporter.getARScaleAndTarget();
|
193
|
+
const { scale, target } = exporter.getARScaleAndTarget();
|
195
194
|
|
196
195
|
// We have to reset the image tracking object's position and rotation, because QuickLook applies them.
|
197
|
-
// On Android WebXR they're replaced by the tracked data
|
198
|
-
|
196
|
+
// On Android WebXR they're replaced by the tracked data
|
197
|
+
let parent = object;
|
198
|
+
const relativeMatrix = new Matrix4();
|
199
|
+
if (object !== target) {
|
200
|
+
while (parent.parent && parent.parent !== target) {
|
201
|
+
parent = parent.parent;
|
202
|
+
relativeMatrix.premultiply(parent.matrix);
|
203
|
+
}
|
204
|
+
}
|
205
|
+
const mat = relativeMatrix
|
206
|
+
.clone()
|
199
207
|
.invert()
|
200
|
-
|
208
|
+
model.matrix = mat
|
201
209
|
// apply session root scale again after undoing the world transformation
|
202
|
-
// TODO check if we actually do that in WebXR image tracking
|
203
210
|
.scale(new Vector3(scale, scale, scale));
|
204
211
|
|
205
212
|
// Unfortunately looks like Apple's docs are incomplete:
|
@@ -207,10 +214,10 @@
|
|
207
214
|
// In practice, it seems that nesting is not allowed – no image tracking will be applied to nested objects.
|
208
215
|
// Thus, we can't have separate transforms for "regularly placing content" and "placing content with an image marker".
|
209
216
|
// model.extraSchemas.push("Preliminary_AnchoringAPI");
|
210
|
-
model.addEventListener("serialize", (_writer: USDWriter, _context: USDZExporterContext) => {
|
217
|
+
// model.addEventListener("serialize", (_writer: USDWriter, _context: USDZExporterContext) => {
|
211
218
|
// writer.appendLine( `token preliminary:anchoring:type = "image"` );
|
212
219
|
// writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
|
213
|
-
});
|
220
|
+
// });
|
214
221
|
|
215
222
|
// We can only apply this to the first tracked image, more are not supported by QuickLook.
|
216
223
|
break;
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import open from 'open'
|
2
|
+
import { resolveConfig } from 'vite';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Can open the network server in the browser
|
6
|
+
* @param {string} command
|
7
|
+
* @param {import('../types').needleMeta | null} config
|
8
|
+
* @param {import('../types').userSettings} userSettings
|
9
|
+
*/
|
10
|
+
export const needleServer = (command, config, userSettings) => {
|
11
|
+
|
12
|
+
const shouldOpenBrowser = userSettings.openBrowser === true;
|
13
|
+
|
14
|
+
/**
|
15
|
+
* @type {import("vite").UserConfig}
|
16
|
+
*/
|
17
|
+
return {
|
18
|
+
name: 'needle:server',
|
19
|
+
config(config) {
|
20
|
+
// if this plugin is used to open the browser we want to make sure "open" in the vite config is set to false
|
21
|
+
if (shouldOpenBrowser) {
|
22
|
+
console.log("[needle:server] Setting 'open: false' in vite server config because 'openBrowser' is enabled in needlePlugins")
|
23
|
+
return {
|
24
|
+
...config,
|
25
|
+
server: {
|
26
|
+
...config.server,
|
27
|
+
open: false
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
},
|
32
|
+
/**
|
33
|
+
* @param {import("vite").ViteDevServer} server
|
34
|
+
*/
|
35
|
+
configureServer(server) {
|
36
|
+
if (shouldOpenBrowser) {
|
37
|
+
console.log('[needle:server] Waiting for server...')
|
38
|
+
const i = setInterval(() => {
|
39
|
+
// https://github.com/vitejs/vite/blob/e861168f476b8cb278f599a0341076b0511c5264/packages/vite/src/node/preview.ts#L231
|
40
|
+
const resolvedUrls = server.resolvedUrls;
|
41
|
+
if (resolvedUrls) {
|
42
|
+
// stop trying when the urls are resolved
|
43
|
+
clearInterval(i);
|
44
|
+
// check if the network urls are available and attempt to open the first one
|
45
|
+
const networkUrls = resolvedUrls.network;
|
46
|
+
if (Array.isArray(networkUrls) && networkUrls.length > 0) {
|
47
|
+
const networkUrl = networkUrls[0];
|
48
|
+
// perhaps we can just use the vite code here: https://github.com/vitejs/vite/blob/e861168f476b8cb278f599a0341076b0511c5264/packages/vite/src/node/server/openBrowser.ts#L72
|
49
|
+
console.log('[needle:server] Opening network URL: ' + networkUrl)
|
50
|
+
open(networkUrl);
|
51
|
+
}
|
52
|
+
else {
|
53
|
+
const local = resolvedUrls.local;
|
54
|
+
if (local?.length) {
|
55
|
+
console.log('[needle:server] Opening local URL: ' + local)
|
56
|
+
open(local);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
}
|
61
|
+
}, 100)
|
62
|
+
}
|
63
|
+
},
|
64
|
+
}
|
65
|
+
};
|