@needle-tools/engine 4.8.7-next.3c34e8b → 4.8.7-next.830076f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/{needle-engine.bundle-BthqMTPI.min.js → needle-engine.bundle-DW7OUhMH.min.js} +129 -129
  2. package/dist/{needle-engine.bundle-Fj5SzBaF.js → needle-engine.bundle-DlMlXTv_.js} +5700 -5657
  3. package/dist/{needle-engine.bundle-BgFqEj5W.umd.cjs → needle-engine.bundle-XMeUns-Z.umd.cjs} +131 -131
  4. package/dist/needle-engine.js +2 -2
  5. package/dist/needle-engine.min.js +1 -1
  6. package/dist/needle-engine.umd.cjs +1 -1
  7. package/lib/engine/engine_camera.d.ts +8 -1
  8. package/lib/engine/engine_camera.js +25 -0
  9. package/lib/engine/engine_camera.js.map +1 -1
  10. package/lib/engine/engine_context.d.ts +9 -0
  11. package/lib/engine/engine_context.js +15 -0
  12. package/lib/engine/engine_context.js.map +1 -1
  13. package/lib/engine/webcomponents/needle-engine.attributes.d.ts +1 -0
  14. package/lib/engine/webcomponents/needle-engine.js +11 -6
  15. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  16. package/lib/engine-components/export/usdz/Extension.d.ts +1 -1
  17. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js +1 -1
  18. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js.map +1 -1
  19. package/lib/engine-components/export/usdz/USDZExporter.d.ts +7 -0
  20. package/lib/engine-components/export/usdz/USDZExporter.js +8 -1
  21. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  22. package/lib/engine-components/webxr/WebXRImageTracking.d.ts +3 -1
  23. package/lib/engine-components/webxr/WebXRImageTracking.js +115 -81
  24. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/engine/engine_camera.ts +40 -1
  27. package/src/engine/engine_context.ts +21 -1
  28. package/src/engine/webcomponents/needle-engine.attributes.ts +2 -0
  29. package/src/engine/webcomponents/needle-engine.ts +13 -6
  30. package/src/engine-components/export/usdz/Extension.ts +1 -1
  31. package/src/engine-components/export/usdz/ThreeUSDZExporter.ts +1 -1
  32. package/src/engine-components/export/usdz/USDZExporter.ts +21 -12
  33. package/src/engine-components/webxr/WebXRImageTracking.ts +136 -90
@@ -1,11 +1,11 @@
1
- import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
1
+ import { ImageBitmapLoader, Matrix4, Object3D, Quaternion, Vector3 } from "three";
2
2
  import { Object3DEventMap } from "three";
3
3
 
4
4
  import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
5
5
  import { AssetReference } from "../../engine/engine_addressables.js";
6
6
  import { serializable } from "../../engine/engine_serialization.js";
7
7
  import { IGameObject } from "../../engine/engine_types.js";
8
- import { CircularBuffer, DeviceUtilities, getParam } from "../../engine/engine_utils.js";
8
+ import { CircularBuffer, delay, DeviceUtilities, getParam } from "../../engine/engine_utils.js";
9
9
  import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/xr/api.js";
10
10
  import { IUSDExporterExtension } from "../../engine-components/export/usdz/Extension.js";
11
11
  import { imageToCanvas, USDObject, USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
@@ -148,91 +148,122 @@ export class WebXRImageTrackingModel {
148
148
 
149
149
  class ImageTrackingExtension implements IUSDExporterExtension {
150
150
 
151
+
152
+
153
+ readonly isImageTrackingExtension = true;
151
154
  get extensionName() { return "image-tracking"; }
152
155
 
153
- private filename: string;
154
- private widthInMeters: number;
155
- private imageData: Uint8Array;
156
156
 
157
- constructor(filename: string, imageData: Uint8Array, widthInMeters: number) {
158
- this.filename = filename;
159
- this.imageData = imageData;
160
- this.widthInMeters = widthInMeters;
157
+ constructor(private readonly exporter: USDZExporter, private readonly component: WebXRImageTracking) {
158
+ if (debug) console.log(this);
159
+ this.exporter.anchoringType = "image";
161
160
  }
162
161
 
163
- private getImageTrackingComponents() {
164
- return GameObject.findObjectsOfType(WebXRImageTracking, Context.Current).filter(c => c.trackedImages && c.trackedImages.length > 0 && c.activeAndEnabled);
162
+ // set during export
163
+ private shouldExport: boolean = true;
164
+
165
+ private filename: string = "marker.png";
166
+ private imageModel: WebXRImageTrackingModel | null = null;
167
+
168
+ onBeforeBuildDocument(_context: USDZExporterContext) {
169
+
170
+ // check if this extension is the first image tracking extension in the list
171
+ // since iOS can only track one image at a time we only allow one image tracking extension to be active
172
+ // we have to determine this at the earlierst export callback
173
+ // all subsequent export callbacks should then check is shouldExport is set to true
174
+ // this should only be the case for exactly one extension
175
+ const index = this.exporter.extensions
176
+ .filter(e => {
177
+ const ext = (e as ImageTrackingExtension);
178
+ return ext.isImageTrackingExtension && ext.component.activeAndEnabled && ext.component.trackedImages?.length > 0;
179
+ })
180
+ .indexOf(this);
181
+ this.shouldExport = index === 0;
182
+ if (!this.shouldExport) return;
183
+
184
+ // Warn if more than one tracked image is used for USDZ; that's not supported at the moment.
185
+ if (this.component.trackedImages?.length > 1) {
186
+ if (debug || isDevEnvironment()) {
187
+ showBalloonWarning("USDZ: Only one tracked image is supported.");
188
+ console.warn("USDZ: Only one tracked image is supported. Will choose the first one in the trackedImages list");
189
+ }
190
+ }
165
191
  }
166
192
 
167
193
  onAfterHierarchy(_context: USDZExporterContext, writer: USDWriter) {
194
+ if (!this.shouldExport) return;
195
+
168
196
  const iOSVersion = DeviceUtilities.getiOSVersion();
169
197
  const majorVersion = iOSVersion ? parseInt(iOSVersion.split(".")[0]) : 18;
170
198
  const workaroundForFB16119331 = majorVersion >= 18;
171
199
  const multiplier = workaroundForFB16119331 ? 1 : 100;
172
200
  writer.beginBlock(`def Preliminary_ReferenceImage "AnchoringReferenceImage"`);
173
201
  writer.appendLine(`uniform asset image = @image_tracking/` + this.filename + `@`);
174
- writer.appendLine(`uniform double physicalWidth = ` + (this.widthInMeters * multiplier).toFixed(8));
202
+ writer.appendLine(`uniform double physicalWidth = ` + (this.imageModel!.widthInMeters * multiplier).toFixed(8));
175
203
  writer.closeBlock();
204
+
176
205
  }
177
206
 
178
- onBeforeBuildDocument(_context: USDZExporterContext) {
179
- const imageTracking = this.getImageTrackingComponents()[0];
207
+ async onAfterSerialize(context: USDZExporterContext) {
208
+ if (!this.shouldExport) return;
180
209
 
181
- // Warn if more than one tracked image is used for USDZ; that's not supported at the moment.
182
- if (imageTracking && imageTracking.trackedImages?.length > 1) {
183
- if (isDevEnvironment()) showBalloonWarning("USDZ: Only one tracked image is supported.");
184
- console.warn("USDZ: Only one tracked image is supported.");
185
- }
186
- }
210
+ const imageModel = this.imageModel;
211
+ const img = _imageElements.get(imageModel!.image!)!;
187
212
 
188
- onAfterSerialize(context: USDZExporterContext) {
189
- context.files['image_tracking/' + this.filename] = this.imageData;
213
+ const canvas = await imageToCanvas(img);
214
+ const blob = await canvas.convertToBlob({ type: 'image/png' });
215
+ const arrayBuffer = await blob.arrayBuffer();
216
+ context.files['image_tracking/' + this.filename] = new Uint8Array(arrayBuffer);
190
217
  }
191
218
 
192
219
  onExportObject(object: Object3D<Object3DEventMap>, model: USDObject, _context: USDZExporterContext) {
193
- const imageTracking = this.getImageTrackingComponents()[0];
194
- if (!imageTracking || !imageTracking.trackedImages?.length) return;
195
-
196
- const exporter = GameObject.findObjectOfType(USDZExporter);
197
- if (!exporter) return;
198
-
199
- for (const trackedImage of imageTracking.trackedImages) {
200
- if (trackedImage.object?.asset === object) {
201
- const { scale, target } = exporter.getARScaleAndTarget();
202
-
203
- // We have to reset the image tracking object's position and rotation, because QuickLook applies them.
204
- // On Android WebXR they're replaced by the tracked data
205
- let parent = object;
206
- const relativeMatrix = new Matrix4();
207
- if (object !== target) {
208
- while (parent.parent && parent.parent !== target) {
209
- parent = parent.parent;
210
- relativeMatrix.premultiply(parent.matrix);
211
- }
220
+ if (!this.shouldExport) return;
221
+
222
+ const imageTracking = this.component;
223
+ if (!imageTracking || !imageTracking.trackedImages?.length || !imageTracking.activeAndEnabled) return;
224
+
225
+ // we only care about the first image
226
+ const trackedImage = imageTracking.trackedImages[0];
227
+
228
+ if (trackedImage.object?.asset === object) {
229
+ this.imageModel = trackedImage;
230
+
231
+ const { scale, target } = this.exporter.getARScaleAndTarget();
232
+
233
+ // We have to reset the image tracking object's position and rotation, because QuickLook applies them.
234
+ // On Android WebXR they're replaced by the tracked data
235
+ let parent = object;
236
+
237
+ const relativeMatrix = new Matrix4();
238
+ if (object !== target) {
239
+ while (parent && parent.parent && parent.parent !== target) {
240
+ parent = parent.parent;
241
+ relativeMatrix.premultiply(parent.matrix);
212
242
  }
213
- const mat = relativeMatrix
214
- .clone()
215
- .invert()
216
- // apply session root scale again after undoing the world transformation
217
- model.setMatrix(mat.scale(new Vector3(scale, scale, scale)));
218
-
219
- // Unfortunately looks like Apple's docs are incomplete:
220
- // https://developer.apple.com/documentation/realitykit/preliminary_anchoringapi#Nest-and-Layer-Anchorable-Prims
221
- // In practice, it seems that nesting is not allowed – no image tracking will be applied to nested objects.
222
- // Thus, we can't have separate transforms for "regularly placing content" and "placing content with an image marker".
223
- // model.extraSchemas.push("Preliminary_AnchoringAPI");
224
- // model.addEventListener("serialize", (_writer: USDWriter, _context: USDZExporterContext) => {
225
- // writer.appendLine( `token preliminary:anchoring:type = "image"` );
226
- // writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
227
- // });
228
-
229
- // We can only apply this to the first tracked image, more are not supported by QuickLook.
230
- break;
231
243
  }
244
+ const mat = relativeMatrix
245
+ .clone()
246
+ .invert()
247
+ // apply session root scale again after undoing the world transformation
248
+ model.setMatrix(mat.scale(new Vector3(scale, scale, scale)));
249
+
250
+
251
+ // Unfortunately looks like Apple's docs are incomplete:
252
+ // https://developer.apple.com/documentation/realitykit/preliminary_anchoringapi#Nest-and-Layer-Anchorable-Prims
253
+ // In practice, it seems that nesting is not allowed – no image tracking will be applied to nested objects.
254
+ // Thus, we can't have separate transforms for "regularly placing content" and "placing content with an image marker".
255
+ // model.extraSchemas.push("Preliminary_AnchoringAPI");
256
+ // model.addEventListener("serialize", (_writer: USDWriter, _context: USDZExporterContext) => {
257
+ // writer.appendLine( `token preliminary:anchoring:type = "image"` );
258
+ // writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
259
+ // });
260
+
261
+ // We can only apply this to the first tracked image, more are not supported by QuickLook.
232
262
  }
233
263
  }
234
264
  }
235
265
 
266
+
236
267
  /**
237
268
  * @category XR
238
269
  * @group Components
@@ -247,7 +278,6 @@ export class WebXRImageTracking extends Behaviour {
247
278
  smooth: boolean = true;
248
279
 
249
280
  private readonly trackedImageIndexMap: Map<number, WebXRImageTrackingModel> = new Map();
250
- private static _imageElements: Map<string, ImageBitmap | null> = new Map();
251
281
 
252
282
  /** @returns true if image tracking is supported on this device. This may return false at runtime if the user's browser did not enable webxr incubations */
253
283
  get supported() { return this._supported; }
@@ -259,38 +289,24 @@ export class WebXRImageTracking extends Behaviour {
259
289
  if (!this.trackedImages) return;
260
290
  for (const trackedImage of this.trackedImages) {
261
291
  if (trackedImage.image) {
262
- if (WebXRImageTracking._imageElements.has(trackedImage.image)) {
263
- // already loaded
264
- }
265
- else {
266
- const url = trackedImage.image;
267
- WebXRImageTracking._imageElements.set(url, null);
268
- const imageElement = document.createElement("img") as HTMLImageElement;
269
- imageElement.src = url;
270
- imageElement.addEventListener("load", async () => {
271
- const img = await createImageBitmap(imageElement);
272
- WebXRImageTracking._imageElements.set(url, img);
273
-
274
- // read back Uint8Array to use in USDZ -
275
- // TODO better would be to do that once we actually need it
276
- const canvas = await imageToCanvas(img);
277
- if (canvas) {
278
- const blob = await canvas.convertToBlob({ type: 'image/png' });
279
- const arrayBuffer = await blob.arrayBuffer();
280
-
281
- const exporter = GameObject.findObjectOfType(USDZExporter);
282
- if (exporter && this.trackedImages) {
283
- exporter.extensions.push(
284
- new ImageTrackingExtension("marker.png", new Uint8Array(arrayBuffer), this.trackedImages[0].widthInMeters)
285
- );
286
- exporter.anchoringType = "image";
287
- }
288
- }
289
- });
290
- }
292
+ loadImage(trackedImage.image);
291
293
  }
292
294
  }
293
295
  }
296
+ onEnable() {
297
+ USDZExporter.beforeExport.addEventListener(this.onBeforeUSDZExport);
298
+ }
299
+ onDisable(): void {
300
+ USDZExporter.beforeExport.removeEventListener(this.onBeforeUSDZExport);
301
+ }
302
+
303
+ private onBeforeUSDZExport = (args: { exporter: USDZExporter }) => {
304
+ if (this.activeAndEnabled && this.trackedImages?.length) {
305
+ args.exporter.extensions.push(new ImageTrackingExtension(args.exporter, this));
306
+ }
307
+ }
308
+
309
+
294
310
 
295
311
  onBeforeXR(_mode: XRSessionMode, args: XRSessionInit & { trackedImages: Array<any> }): void {
296
312
  // console.log("onXRRequested", args, this.trackedImages)
@@ -302,7 +318,7 @@ export class WebXRImageTracking extends Behaviour {
302
318
  args.trackedImages = [];
303
319
  for (const trackedImage of this.trackedImages) {
304
320
  if (trackedImage.image?.length && trackedImage.widthInMeters > 0) {
305
- const bitmap = WebXRImageTracking._imageElements.get(trackedImage.image);
321
+ const bitmap = _imageElements.get(trackedImage.image);
306
322
  if (bitmap) {
307
323
  this.trackedImageIndexMap.set(args.trackedImages.length, trackedImage);
308
324
  args.trackedImages.push({
@@ -519,4 +535,34 @@ export class WebXRImageTracking extends Behaviour {
519
535
  }
520
536
  }
521
537
  }
538
+ }
539
+
540
+
541
+
542
+
543
+ const _imageElements: Map<string, ImageBitmap | null> = new Map();
544
+ const _imageLoadingPromises: Map<string, Promise<boolean>> = new Map();
545
+
546
+ async function loadImage(url: string) {
547
+ if (_imageElements.has(url)) {
548
+ if (_imageLoadingPromises.has(url)) return _imageLoadingPromises.get(url);
549
+ return Promise.resolve(true);
550
+ }
551
+ const promise = new Promise<boolean>(res => {
552
+ _imageElements.set(url, null);
553
+ const imageElement = document.createElement("img") as HTMLImageElement;
554
+ imageElement.src = url;
555
+ imageElement.addEventListener("load", async () => {
556
+ const img = await createImageBitmap(imageElement);
557
+ _imageElements.set(url, img);
558
+ res(true);
559
+ });
560
+ });
561
+
562
+ _imageLoadingPromises.set(url, promise);
563
+ promise.finally(() => {
564
+ _imageLoadingPromises.delete(url);
565
+ });
566
+
567
+ return promise;
522
568
  }