@wonderlandengine/ar-provider-zappar 1.1.0 → 1.2.0
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.
- package/dist/src/face-tracking-mode-zappar.js +3 -3
- package/dist/src/hit-test-location-zappar.d.ts +47 -0
- package/dist/src/hit-test-location-zappar.js +155 -0
- package/dist/src/image-tracking-mode-zappar.d.ts +4 -0
- package/dist/src/image-tracking-mode-zappar.js +5 -1
- package/dist/src/world-tracking-mode-zappar.d.ts +7 -1
- package/dist/src/world-tracking-mode-zappar.js +50 -0
- package/dist/src/zappar-provider.d.ts +80 -0
- package/dist/src/zappar-provider.js +121 -10
- package/package.json +3 -3
- package/scripts/postinstall.mjs +119 -37
|
@@ -123,7 +123,8 @@ export class FaceTracking_Zappar extends TrackingMode {
|
|
|
123
123
|
const mirrorPoses = pipeline.cameraFrameUserFacing();
|
|
124
124
|
const viewNear = this._view?.near;
|
|
125
125
|
const viewFar = this._view?.far;
|
|
126
|
-
const
|
|
126
|
+
const [cameraDataWidth, cameraDataHeight] = pipeline.cameraDataSize();
|
|
127
|
+
const projectionMatrix = Zappar.projectionMatrixFromCameraModelAndSize(pipeline.cameraModel(), cameraDataWidth, cameraDataHeight, this.component.engine.canvas.width, this.component.engine.canvas.height, typeof viewNear === 'number' ? viewNear : undefined, typeof viewFar === 'number' ? viewFar : undefined);
|
|
127
128
|
if (this._view) {
|
|
128
129
|
this._setProjectionMatrixWithEngineRemap(projectionMatrix);
|
|
129
130
|
}
|
|
@@ -305,8 +306,7 @@ export class FaceTracking_Zappar extends TrackingMode {
|
|
|
305
306
|
}
|
|
306
307
|
return;
|
|
307
308
|
}
|
|
308
|
-
if (typeof engineAny.wasm?._wl_view_component_remapProjectionMatrix ===
|
|
309
|
-
'function') {
|
|
309
|
+
if (typeof engineAny.wasm?._wl_view_component_remapProjectionMatrix === 'function') {
|
|
310
310
|
const ndcDepthIsZeroToOne = false;
|
|
311
311
|
engineAny.wasm._wl_view_component_remapProjectionMatrix(view._id, engineAny.isReverseZEnabled, ndcDepthIsZeroToOne);
|
|
312
312
|
if (debugWindow.__WLE_ZAPPAR_DEBUG__) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hit-test reticle backed by Zappar plane anchors from `WorldTracker`
|
|
3
|
+
* (requires `@zappar/zappar >= 4.x`).
|
|
4
|
+
*
|
|
5
|
+
* Each frame the component casts a ray from the centre of `camera` into the
|
|
6
|
+
* set of planes detected by {@link ZapparProvider}'s `WorldTracker` and moves
|
|
7
|
+
* the owning object to the closest intersection point. A {@link MeshComponent}
|
|
8
|
+
* on the same object is shown while a valid hit is found and hidden otherwise.
|
|
9
|
+
*
|
|
10
|
+
* This is the Zappar equivalent of:
|
|
11
|
+
* - `hit-test-location-root` (WebXR Device API hit-test)
|
|
12
|
+
* - `hit-test-location-xr8` (8th Wall SLAM + XY-plane intersection)
|
|
13
|
+
*
|
|
14
|
+
* **Setup**
|
|
15
|
+
* 1. Attach this component to the reticle object (which should have a
|
|
16
|
+
* {@link MeshComponent} for visual feedback).
|
|
17
|
+
* 2. Set the `camera` property to the scene object that carries
|
|
18
|
+
* {@link ARSLAMCamera}.
|
|
19
|
+
* 3. The `SpawnMeshOnReticle` component works alongside this component –
|
|
20
|
+
* tapping the screen (or selecting on WebXR) will spawn content at this
|
|
21
|
+
* object's position.
|
|
22
|
+
*
|
|
23
|
+
* **Coordinate spaces**
|
|
24
|
+
* Zappar plane poses are expressed in Zappar world space. The WLE camera
|
|
25
|
+
* object's transform is driven directly from the Zappar camera pose matrix
|
|
26
|
+
* (via `WorldTracking_Zappar`), so `camera.getPositionWorld()` and
|
|
27
|
+
* `camera.getForwardWorld()` are already in the same coordinate space as the
|
|
28
|
+
* plane poses. No additional transform is required.
|
|
29
|
+
*/
|
|
30
|
+
import { Component, Object as WLEObject } from '@wonderlandengine/api';
|
|
31
|
+
export declare class HitTestLocationZappar extends Component {
|
|
32
|
+
static TypeName: string;
|
|
33
|
+
/** The scene object that carries the {@link ARSLAMCamera} component. */
|
|
34
|
+
camera: WLEObject;
|
|
35
|
+
private _provider;
|
|
36
|
+
private _mesh;
|
|
37
|
+
private readonly _camPos;
|
|
38
|
+
private readonly _camFwd;
|
|
39
|
+
private readonly _planeNormal;
|
|
40
|
+
private readonly _planeOrigin;
|
|
41
|
+
private readonly _toOrigin;
|
|
42
|
+
private readonly _hitPoint;
|
|
43
|
+
start(): void;
|
|
44
|
+
update(): void;
|
|
45
|
+
private readonly _onSessionStart;
|
|
46
|
+
private readonly _onSessionEnd;
|
|
47
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hit-test reticle backed by Zappar plane anchors from `WorldTracker`
|
|
3
|
+
* (requires `@zappar/zappar >= 4.x`).
|
|
4
|
+
*
|
|
5
|
+
* Each frame the component casts a ray from the centre of `camera` into the
|
|
6
|
+
* set of planes detected by {@link ZapparProvider}'s `WorldTracker` and moves
|
|
7
|
+
* the owning object to the closest intersection point. A {@link MeshComponent}
|
|
8
|
+
* on the same object is shown while a valid hit is found and hidden otherwise.
|
|
9
|
+
*
|
|
10
|
+
* This is the Zappar equivalent of:
|
|
11
|
+
* - `hit-test-location-root` (WebXR Device API hit-test)
|
|
12
|
+
* - `hit-test-location-xr8` (8th Wall SLAM + XY-plane intersection)
|
|
13
|
+
*
|
|
14
|
+
* **Setup**
|
|
15
|
+
* 1. Attach this component to the reticle object (which should have a
|
|
16
|
+
* {@link MeshComponent} for visual feedback).
|
|
17
|
+
* 2. Set the `camera` property to the scene object that carries
|
|
18
|
+
* {@link ARSLAMCamera}.
|
|
19
|
+
* 3. The `SpawnMeshOnReticle` component works alongside this component –
|
|
20
|
+
* tapping the screen (or selecting on WebXR) will spawn content at this
|
|
21
|
+
* object's position.
|
|
22
|
+
*
|
|
23
|
+
* **Coordinate spaces**
|
|
24
|
+
* Zappar plane poses are expressed in Zappar world space. The WLE camera
|
|
25
|
+
* object's transform is driven directly from the Zappar camera pose matrix
|
|
26
|
+
* (via `WorldTracking_Zappar`), so `camera.getPositionWorld()` and
|
|
27
|
+
* `camera.getForwardWorld()` are already in the same coordinate space as the
|
|
28
|
+
* plane poses. No additional transform is required.
|
|
29
|
+
*/
|
|
30
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
31
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
32
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
33
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
34
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
35
|
+
};
|
|
36
|
+
import { Component, MeshComponent } from '@wonderlandengine/api';
|
|
37
|
+
import { property } from '@wonderlandengine/api/decorators.js';
|
|
38
|
+
import { vec3 } from 'gl-matrix';
|
|
39
|
+
import { ARSession } from '@wonderlandengine/ar-tracking';
|
|
40
|
+
import { ZapparProvider } from './zappar-provider.js';
|
|
41
|
+
export class HitTestLocationZappar extends Component {
|
|
42
|
+
static TypeName = 'hit-test-location-zappar';
|
|
43
|
+
/** The scene object that carries the {@link ARSLAMCamera} component. */
|
|
44
|
+
camera;
|
|
45
|
+
_provider = null;
|
|
46
|
+
_mesh = null;
|
|
47
|
+
// Pre-allocated scratch buffers to avoid GC pressure in the update loop.
|
|
48
|
+
_camPos = new Float32Array(3);
|
|
49
|
+
_camFwd = new Float32Array(3);
|
|
50
|
+
_planeNormal = vec3.create();
|
|
51
|
+
_planeOrigin = vec3.create();
|
|
52
|
+
_toOrigin = vec3.create();
|
|
53
|
+
_hitPoint = vec3.create();
|
|
54
|
+
start() {
|
|
55
|
+
const arSession = ARSession.getSessionForEngine(this.engine);
|
|
56
|
+
arSession.onSessionStart.add(this._onSessionStart);
|
|
57
|
+
arSession.onSessionEnd.add(this._onSessionEnd);
|
|
58
|
+
this._mesh = this.object.getComponent(MeshComponent) ?? null;
|
|
59
|
+
if (this._mesh)
|
|
60
|
+
this._mesh.active = false;
|
|
61
|
+
// Deactivate update loop until a valid Zappar session starts.
|
|
62
|
+
this.active = false;
|
|
63
|
+
}
|
|
64
|
+
update() {
|
|
65
|
+
const provider = this._provider;
|
|
66
|
+
if (!provider)
|
|
67
|
+
return;
|
|
68
|
+
if (!this.camera) {
|
|
69
|
+
console.warn(`[${this.type}] 'camera' property is not set.`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const worldTracker = provider.worldTracker;
|
|
73
|
+
if (!worldTracker || worldTracker.planes.size === 0) {
|
|
74
|
+
if (this._mesh)
|
|
75
|
+
this._mesh.active = false;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const cameraPose = provider.slamCameraPoseMatrix;
|
|
79
|
+
if (!cameraPose) {
|
|
80
|
+
if (this._mesh)
|
|
81
|
+
this._mesh.active = false;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// -------------------------------------------------------------------
|
|
85
|
+
// Ray definition in WLE / Zappar world space.
|
|
86
|
+
// The camera's world transform has already been set from the Zappar
|
|
87
|
+
// camera pose so these values are directly comparable to plane poses.
|
|
88
|
+
// -------------------------------------------------------------------
|
|
89
|
+
this.camera.getPositionWorld(this._camPos);
|
|
90
|
+
this.camera.getForwardWorld(this._camFwd);
|
|
91
|
+
let closestT = Infinity;
|
|
92
|
+
let hitFound = false;
|
|
93
|
+
for (const plane of worldTracker.planes.values()) {
|
|
94
|
+
// plane.pose() returns a column-major 4×4 matrix in Zappar world space.
|
|
95
|
+
const planeMat = plane.pose(cameraPose);
|
|
96
|
+
// Column 1 = local Y-axis = surface normal for horizontal planes.
|
|
97
|
+
this._planeNormal[0] = planeMat[4];
|
|
98
|
+
this._planeNormal[1] = planeMat[5];
|
|
99
|
+
this._planeNormal[2] = planeMat[6];
|
|
100
|
+
// Column 3 = translation = a point on the plane.
|
|
101
|
+
this._planeOrigin[0] = planeMat[12];
|
|
102
|
+
this._planeOrigin[1] = planeMat[13];
|
|
103
|
+
this._planeOrigin[2] = planeMat[14];
|
|
104
|
+
// Ray–plane intersection: t = dot(planeOrigin - camPos, N) / dot(D, N)
|
|
105
|
+
const denom = vec3.dot(this._camFwd, this._planeNormal);
|
|
106
|
+
// Skip planes that are nearly parallel to the ray.
|
|
107
|
+
if (Math.abs(denom) < 1e-6)
|
|
108
|
+
continue;
|
|
109
|
+
vec3.sub(this._toOrigin, this._planeOrigin, this._camPos);
|
|
110
|
+
const t = vec3.dot(this._toOrigin, this._planeNormal) / denom;
|
|
111
|
+
// Only accept intersections in front of the camera and closer than
|
|
112
|
+
// the best so far.
|
|
113
|
+
if (t > 1e-3 && t < closestT) {
|
|
114
|
+
closestT = t;
|
|
115
|
+
vec3.scaleAndAdd(this._hitPoint, this._camPos, this._camFwd, t);
|
|
116
|
+
hitFound = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (hitFound) {
|
|
120
|
+
this.object.setPositionWorld(this._hitPoint);
|
|
121
|
+
if (this._mesh)
|
|
122
|
+
this._mesh.active = true;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
if (this._mesh)
|
|
126
|
+
this._mesh.active = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
_onSessionStart = (provider) => {
|
|
130
|
+
if (!(provider instanceof ZapparProvider))
|
|
131
|
+
return;
|
|
132
|
+
this._provider = provider;
|
|
133
|
+
const wt = provider.enableWorldTracker();
|
|
134
|
+
if (!wt) {
|
|
135
|
+
// @zappar/zappar < 4.x or WorldTracker unavailable – degrade
|
|
136
|
+
// gracefully rather than throwing.
|
|
137
|
+
console.warn(`[${this.type}] Zappar WorldTracker (plane anchors) is not` +
|
|
138
|
+
' available in the installed SDK version. Upgrade' +
|
|
139
|
+
' @zappar/zappar to >= 4.x to enable plane-based hit tests.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this.active = true;
|
|
143
|
+
};
|
|
144
|
+
_onSessionEnd = (provider) => {
|
|
145
|
+
if (provider !== this._provider)
|
|
146
|
+
return;
|
|
147
|
+
this._provider = null;
|
|
148
|
+
this.active = false;
|
|
149
|
+
if (this._mesh)
|
|
150
|
+
this._mesh.active = false;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
__decorate([
|
|
154
|
+
property.object()
|
|
155
|
+
], HitTestLocationZappar.prototype, "camera", void 0);
|
|
@@ -20,6 +20,10 @@ export declare class ImageTracking_Zappar extends TrackingMode implements ImageT
|
|
|
20
20
|
readonly onImageFound: Emitter<[event: ImageTrackedEvent]>;
|
|
21
21
|
readonly onImageUpdate: Emitter<[event: ImageTrackedEvent]>;
|
|
22
22
|
readonly onImageLost: Emitter<[event: ImageTrackedEvent]>;
|
|
23
|
+
registerTarget(source: string | ArrayBuffer, options: {
|
|
24
|
+
name: string;
|
|
25
|
+
physicalWidthInMeters?: number;
|
|
26
|
+
}): Promise<void>;
|
|
23
27
|
init(): void;
|
|
24
28
|
startSession(): void;
|
|
25
29
|
endSession(): void;
|
|
@@ -21,6 +21,9 @@ export class ImageTracking_Zappar extends TrackingMode {
|
|
|
21
21
|
onImageFound = new Emitter();
|
|
22
22
|
onImageUpdate = new Emitter();
|
|
23
23
|
onImageLost = new Emitter();
|
|
24
|
+
async registerTarget(source, options) {
|
|
25
|
+
await this.provider.registerImageTarget(source, options);
|
|
26
|
+
}
|
|
24
27
|
init() {
|
|
25
28
|
this._view = this.component.object.getComponent('view') ?? undefined;
|
|
26
29
|
const provider = this.provider;
|
|
@@ -50,7 +53,8 @@ export class ImageTracking_Zappar extends TrackingMode {
|
|
|
50
53
|
const pipeline = provider.getPipeline();
|
|
51
54
|
const viewNear = this._view?.near;
|
|
52
55
|
const viewFar = this._view?.far;
|
|
53
|
-
const
|
|
56
|
+
const [cameraDataWidth, cameraDataHeight] = pipeline.cameraDataSize();
|
|
57
|
+
const projectionMatrix = Zappar.projectionMatrixFromCameraModelAndSize(pipeline.cameraModel(), cameraDataWidth, cameraDataHeight, this.component.engine.canvas.width, this.component.engine.canvas.height, typeof viewNear === 'number' ? viewNear : undefined, typeof viewFar === 'number' ? viewFar : undefined);
|
|
54
58
|
if (this._view) {
|
|
55
59
|
this._setProjectionMatrixWithEngineRemap(projectionMatrix);
|
|
56
60
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference path="../../src/types/global.d.ts" />
|
|
2
2
|
import { Component } from '@wonderlandengine/api';
|
|
3
|
-
import { TrackingMode } from '@wonderlandengine/ar-tracking';
|
|
3
|
+
import { HitTestResult, TrackingMode } from '@wonderlandengine/ar-tracking';
|
|
4
4
|
import { ZapparProvider } from './zappar-provider.js';
|
|
5
5
|
/**
|
|
6
6
|
* SLAM tracking implementation backed by Zappar.
|
|
@@ -14,4 +14,10 @@ export declare class WorldTracking_Zappar extends TrackingMode {
|
|
|
14
14
|
getCameraTransformWorld(): ArrayLike<number> | null;
|
|
15
15
|
getCameraProjectionMatrix(out: Float32Array): boolean;
|
|
16
16
|
update(): void;
|
|
17
|
+
setupHitTest(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Cast a ray from the camera through the screen centre and return the closest
|
|
20
|
+
* intersection with any of the Zappar-detected plane anchors.
|
|
21
|
+
*/
|
|
22
|
+
getHitTestResult(): HitTestResult | null;
|
|
17
23
|
}
|
|
@@ -45,4 +45,54 @@ export class WorldTracking_Zappar extends TrackingMode {
|
|
|
45
45
|
// Wonderland stores transforms as dual quaternions (8 floats): [rotation quat, translation dual part].
|
|
46
46
|
quat2.fromMat4(this._cameraTransform, pose);
|
|
47
47
|
}
|
|
48
|
+
async setupHitTest() {
|
|
49
|
+
const provider = this.provider;
|
|
50
|
+
const tracker = provider.enableWorldTracker();
|
|
51
|
+
if (!tracker) {
|
|
52
|
+
console.warn('[WorldTracking_Zappar] World tracker (plane detection) is not available. ' +
|
|
53
|
+
'Ensure @zappar/zappar >= 4.x is installed.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Cast a ray from the camera through the screen centre and return the closest
|
|
58
|
+
* intersection with any of the Zappar-detected plane anchors.
|
|
59
|
+
*/
|
|
60
|
+
getHitTestResult() {
|
|
61
|
+
const provider = this.provider;
|
|
62
|
+
const worldTracker = provider.worldTracker;
|
|
63
|
+
if (!worldTracker)
|
|
64
|
+
return null;
|
|
65
|
+
const cameraPose = provider.slamCameraPoseMatrix;
|
|
66
|
+
if (!cameraPose)
|
|
67
|
+
return null;
|
|
68
|
+
// Camera position and forward vector in world space.
|
|
69
|
+
const cam = this.component.object;
|
|
70
|
+
const pos = cam.getPositionWorld(new Array(3));
|
|
71
|
+
const fwd = cam.getForwardWorld(new Array(3));
|
|
72
|
+
let closestT = Infinity;
|
|
73
|
+
let hit = null;
|
|
74
|
+
for (const plane of worldTracker.planes.values()) {
|
|
75
|
+
// pose() returns a column-major 4×4 matrix.
|
|
76
|
+
// Column 1 (elements [4,5,6]) = plane normal; column 3 (elements [12,13,14]) = plane origin.
|
|
77
|
+
const m = plane.pose(cameraPose);
|
|
78
|
+
const nx = m[4], ny = m[5], nz = m[6];
|
|
79
|
+
const ox = m[12], oy = m[13], oz = m[14];
|
|
80
|
+
const denom = fwd[0] * nx + fwd[1] * ny + fwd[2] * nz;
|
|
81
|
+
// Skip planes nearly parallel to the ray.
|
|
82
|
+
if (Math.abs(denom) < 1e-6)
|
|
83
|
+
continue;
|
|
84
|
+
const t = ((ox - pos[0]) * nx + (oy - pos[1]) * ny + (oz - pos[2]) * nz) / denom;
|
|
85
|
+
if (t <= 0 || t >= closestT)
|
|
86
|
+
continue;
|
|
87
|
+
closestT = t;
|
|
88
|
+
hit = {
|
|
89
|
+
position: {
|
|
90
|
+
x: pos[0] + fwd[0] * t,
|
|
91
|
+
y: pos[1] + fwd[1] * t,
|
|
92
|
+
z: pos[2] + fwd[2] * t,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return hit;
|
|
97
|
+
}
|
|
48
98
|
}
|
|
@@ -6,6 +6,33 @@ import { ARProvider, ARSession, ITrackingMode, ImageScanningEvent, TrackingType
|
|
|
6
6
|
import { loadZappar } from './zappar-module.js';
|
|
7
7
|
import type { FaceMesh, FaceTracker, ImageTracker, Pipeline } from '@zappar/zappar';
|
|
8
8
|
type ZapparImageTargetType = 'flat' | 'cylindrical' | 'conical';
|
|
9
|
+
/** A single detected horizontal plane produced by {@link ZapparWorldTracker}. */
|
|
10
|
+
export interface ZapparPlaneAnchor {
|
|
11
|
+
/** Pose matrix of this plane in world space. */
|
|
12
|
+
pose(cameraPose: Float32Array): Float32Array;
|
|
13
|
+
/** Ordered 2-D boundary vertices on the plane surface. */
|
|
14
|
+
readonly polygon: ReadonlyArray<readonly [number, number]>;
|
|
15
|
+
/** Increments each time `polygon` is updated. */
|
|
16
|
+
readonly polygonVersion: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Minimal interface for the Zappar `WorldTracker` class available in
|
|
20
|
+
* `@zappar/zappar >= 4.x`. Only the API surface used by this package is
|
|
21
|
+
* declared here.
|
|
22
|
+
*/
|
|
23
|
+
export interface ZapparWorldTracker {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
horizontalPlaneDetectionEnabled: boolean;
|
|
26
|
+
verticalPlaneDetectionEnabled: boolean;
|
|
27
|
+
/** Map of all currently detected plane anchors keyed by their ID. */
|
|
28
|
+
readonly planes: ReadonlyMap<string, ZapparPlaneAnchor>;
|
|
29
|
+
readonly worldAnchor: {
|
|
30
|
+
pose(cameraPose: Float32Array): Float32Array;
|
|
31
|
+
};
|
|
32
|
+
readonly groundAnchor: {
|
|
33
|
+
pose(cameraPose: Float32Array): Float32Array;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
9
36
|
export interface ZapparImageTargetOptions {
|
|
10
37
|
name: string;
|
|
11
38
|
type?: ZapparImageTargetType;
|
|
@@ -41,9 +68,19 @@ export declare class ZapparProvider extends ARProvider {
|
|
|
41
68
|
private _cameraSourceUserFacing;
|
|
42
69
|
private _imageTargetDescriptors;
|
|
43
70
|
private readonly _imageTargetsChanged;
|
|
71
|
+
private _worldTracker;
|
|
72
|
+
/** Set to `true` when a component requests world-tracker before the session starts. */
|
|
73
|
+
private _worldTrackerRequested;
|
|
44
74
|
private cameraStarted;
|
|
45
75
|
private hasInitializedAnchor;
|
|
46
76
|
private _anchorWarmupFramesRemaining;
|
|
77
|
+
/**
|
|
78
|
+
* When `true`, {@link updateTracking} keeps calling
|
|
79
|
+
* `setAnchorPoseFromCameraOffset` every frame so the world anchor
|
|
80
|
+
* continuously follows the camera centre. Set via
|
|
81
|
+
* {@link startUserPlacement}; cleared by {@link placeInstantAnchor}.
|
|
82
|
+
*/
|
|
83
|
+
private _userPlacementMode;
|
|
47
84
|
private preRenderRegistered;
|
|
48
85
|
private _slamStateValid;
|
|
49
86
|
private readonly _slamProjectionMatrix;
|
|
@@ -104,6 +141,29 @@ export declare class ZapparProvider extends ARProvider {
|
|
|
104
141
|
getImageScanningEvent(): ImageScanningEvent;
|
|
105
142
|
getImageTargetDescriptors(): ReadonlyArray<ZapparImageTargetDescriptor>;
|
|
106
143
|
getImageTargetDescriptor(index: number): ZapparImageTargetDescriptor | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* Access the active {@link ZapparWorldTracker} instance, or `null` when
|
|
146
|
+
* plane tracking has not been requested or is unavailable.
|
|
147
|
+
*
|
|
148
|
+
* The instance is only non-null once {@link enableWorldTracker} has been
|
|
149
|
+
* called **and** an AR session is running.
|
|
150
|
+
*/
|
|
151
|
+
get worldTracker(): ZapparWorldTracker | null;
|
|
152
|
+
/**
|
|
153
|
+
* Opt in to Zappar plane tracking via the `WorldTracker` API introduced in
|
|
154
|
+
* `@zappar/zappar >= 4.x`.
|
|
155
|
+
*
|
|
156
|
+
* Call this once (e.g. from a component's `start()` hook) before or after
|
|
157
|
+
* a session starts. If the installed SDK does not expose `WorldTracker`
|
|
158
|
+
* (i.e. < 4.x) a warning is logged and the method returns `null`.
|
|
159
|
+
*
|
|
160
|
+
* The tracker is disabled again when the session ends and re-enabled on
|
|
161
|
+
* the next `startSession()` call.
|
|
162
|
+
*
|
|
163
|
+
* @returns The {@link ZapparWorldTracker} instance, or `null` on failure.
|
|
164
|
+
*/
|
|
165
|
+
enableWorldTracker(): ZapparWorldTracker | null;
|
|
166
|
+
private _createWorldTracker;
|
|
107
167
|
private ensureCameraRunning;
|
|
108
168
|
private onVisibilityChange;
|
|
109
169
|
private onPreRender;
|
|
@@ -114,6 +174,26 @@ export declare class ZapparProvider extends ARProvider {
|
|
|
114
174
|
/** Latest anchor pose matrix (valid only if {@link hasSlamTrackingState} is true). */
|
|
115
175
|
get slamAnchorPoseMatrix(): Readonly<mat4> | null;
|
|
116
176
|
get hasSlamTrackingState(): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* `true` while the instant world anchor is being continuously repositioned
|
|
179
|
+
* in front of the camera (either during warmup or during user-placement
|
|
180
|
+
* mode). `false` once the anchor has been locked.
|
|
181
|
+
*/
|
|
182
|
+
get isPlacingInstantAnchor(): boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Enter user-placement mode: the instant world anchor tracks 5 m in front
|
|
185
|
+
* of the camera every frame until {@link placeInstantAnchor} is called.
|
|
186
|
+
*
|
|
187
|
+
* Typically called by a placement-UI component immediately after session
|
|
188
|
+
* start so the user can choose where to lock the world origin.
|
|
189
|
+
*/
|
|
190
|
+
startUserPlacement(): void;
|
|
191
|
+
/**
|
|
192
|
+
* Lock the instant world anchor at its current position and exit placement
|
|
193
|
+
* mode. After this call {@link isPlacingInstantAnchor} returns `false` and
|
|
194
|
+
* the anchor no longer follows the camera.
|
|
195
|
+
*/
|
|
196
|
+
placeInstantAnchor(): void;
|
|
117
197
|
get slamFrameNumber(): number;
|
|
118
198
|
private bindVideoTextureForSkyMaterial;
|
|
119
199
|
updateTracking(): void;
|
|
@@ -28,9 +28,19 @@ export class ZapparProvider extends ARProvider {
|
|
|
28
28
|
_cameraSourceUserFacing = null;
|
|
29
29
|
_imageTargetDescriptors = [];
|
|
30
30
|
_imageTargetsChanged = new Emitter();
|
|
31
|
+
_worldTracker = null;
|
|
32
|
+
/** Set to `true` when a component requests world-tracker before the session starts. */
|
|
33
|
+
_worldTrackerRequested = false;
|
|
31
34
|
cameraStarted = false;
|
|
32
35
|
hasInitializedAnchor = false;
|
|
33
36
|
_anchorWarmupFramesRemaining = 0;
|
|
37
|
+
/**
|
|
38
|
+
* When `true`, {@link updateTracking} keeps calling
|
|
39
|
+
* `setAnchorPoseFromCameraOffset` every frame so the world anchor
|
|
40
|
+
* continuously follows the camera centre. Set via
|
|
41
|
+
* {@link startUserPlacement}; cleared by {@link placeInstantAnchor}.
|
|
42
|
+
*/
|
|
43
|
+
_userPlacementMode = false;
|
|
34
44
|
preRenderRegistered = false;
|
|
35
45
|
_slamStateValid = false;
|
|
36
46
|
_slamProjectionMatrix = new Float32Array(16);
|
|
@@ -113,6 +123,7 @@ export class ZapparProvider extends ARProvider {
|
|
|
113
123
|
this.ensurePipeline();
|
|
114
124
|
this.ensureCameraSourcePreference();
|
|
115
125
|
await this.ensureCameraRunning();
|
|
126
|
+
this.ensurePreRenderRegistered();
|
|
116
127
|
this.startZapparDebugLogging();
|
|
117
128
|
if (this._faceTracker) {
|
|
118
129
|
this._faceTracker.enabled = true;
|
|
@@ -126,6 +137,15 @@ export class ZapparProvider extends ARProvider {
|
|
|
126
137
|
if (this.instantTracker) {
|
|
127
138
|
this.instantTracker.enabled = true;
|
|
128
139
|
}
|
|
140
|
+
// Create the WorldTracker if it was requested before the session started.
|
|
141
|
+
if (this._worldTrackerRequested && !this._worldTracker) {
|
|
142
|
+
this._createWorldTracker();
|
|
143
|
+
this._worldTrackerRequested = false;
|
|
144
|
+
}
|
|
145
|
+
else if (this._worldTracker) {
|
|
146
|
+
// Re-enable a pre-existing WorldTracker from a previous session.
|
|
147
|
+
this._worldTracker.enabled = true;
|
|
148
|
+
}
|
|
129
149
|
}
|
|
130
150
|
ensurePreRenderRegistered() {
|
|
131
151
|
if (!this.preRenderRegistered) {
|
|
@@ -205,7 +225,7 @@ export class ZapparProvider extends ARProvider {
|
|
|
205
225
|
if (debugWorker) {
|
|
206
226
|
console.log('[ZapparProvider] Creating CV worker:', workerUrl);
|
|
207
227
|
}
|
|
208
|
-
const worker = new Worker(workerUrl);
|
|
228
|
+
const worker = new Worker(workerUrl, { type: 'module' });
|
|
209
229
|
// Keep a reference for debugging / inspection.
|
|
210
230
|
ZapparProvider._cvWorker = worker;
|
|
211
231
|
if (typeof window !== 'undefined') {
|
|
@@ -284,7 +304,6 @@ export class ZapparProvider extends ARProvider {
|
|
|
284
304
|
}
|
|
285
305
|
ensurePipeline() {
|
|
286
306
|
if (this.pipeline) {
|
|
287
|
-
this.ensurePreRenderRegistered();
|
|
288
307
|
return;
|
|
289
308
|
}
|
|
290
309
|
if (!this._zappar) {
|
|
@@ -312,7 +331,6 @@ export class ZapparProvider extends ARProvider {
|
|
|
312
331
|
}
|
|
313
332
|
this.ensureCameraSourcePreference();
|
|
314
333
|
this.instantTracker = new Zappar.InstantWorldTracker(pipeline);
|
|
315
|
-
this.ensurePreRenderRegistered();
|
|
316
334
|
}
|
|
317
335
|
ensureCameraSourcePreference() {
|
|
318
336
|
if (!this.pipeline || !this._zappar) {
|
|
@@ -400,6 +418,12 @@ export class ZapparProvider extends ARProvider {
|
|
|
400
418
|
if (!options.name) {
|
|
401
419
|
throw new Error('Image target registration requires a name.');
|
|
402
420
|
}
|
|
421
|
+
// Deduplicate: if a descriptor with the same name is already registered,
|
|
422
|
+
// skip loading to avoid double-loading the same .zpt file when multiple
|
|
423
|
+
// scene components share the same imageId target.
|
|
424
|
+
if (this._imageTargetDescriptors.some((d) => d.name === options.name)) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
403
427
|
// Allow registering targets before an AR session starts.
|
|
404
428
|
// This is useful so image targets are known for ImageScanningEvent
|
|
405
429
|
// and so apps can prefetch/prepare targets without requesting camera access.
|
|
@@ -479,6 +503,60 @@ export class ZapparProvider extends ARProvider {
|
|
|
479
503
|
getImageTargetDescriptor(index) {
|
|
480
504
|
return this._imageTargetDescriptors[index];
|
|
481
505
|
}
|
|
506
|
+
// -----------------------------------------------------------------------
|
|
507
|
+
// World tracking / plane anchors (Zappar SDK >= 4.x)
|
|
508
|
+
// -----------------------------------------------------------------------
|
|
509
|
+
/**
|
|
510
|
+
* Access the active {@link ZapparWorldTracker} instance, or `null` when
|
|
511
|
+
* plane tracking has not been requested or is unavailable.
|
|
512
|
+
*
|
|
513
|
+
* The instance is only non-null once {@link enableWorldTracker} has been
|
|
514
|
+
* called **and** an AR session is running.
|
|
515
|
+
*/
|
|
516
|
+
get worldTracker() {
|
|
517
|
+
return this._worldTracker;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Opt in to Zappar plane tracking via the `WorldTracker` API introduced in
|
|
521
|
+
* `@zappar/zappar >= 4.x`.
|
|
522
|
+
*
|
|
523
|
+
* Call this once (e.g. from a component's `start()` hook) before or after
|
|
524
|
+
* a session starts. If the installed SDK does not expose `WorldTracker`
|
|
525
|
+
* (i.e. < 4.x) a warning is logged and the method returns `null`.
|
|
526
|
+
*
|
|
527
|
+
* The tracker is disabled again when the session ends and re-enabled on
|
|
528
|
+
* the next `startSession()` call.
|
|
529
|
+
*
|
|
530
|
+
* @returns The {@link ZapparWorldTracker} instance, or `null` on failure.
|
|
531
|
+
*/
|
|
532
|
+
enableWorldTracker() {
|
|
533
|
+
if (this._worldTracker) {
|
|
534
|
+
this._worldTracker.enabled = true;
|
|
535
|
+
return this._worldTracker;
|
|
536
|
+
}
|
|
537
|
+
if (this.pipeline && this._zappar) {
|
|
538
|
+
return this._createWorldTracker();
|
|
539
|
+
}
|
|
540
|
+
// Session not yet started – remember the request.
|
|
541
|
+
this._worldTrackerRequested = true;
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
_createWorldTracker() {
|
|
545
|
+
if (!this.pipeline || !this._zappar)
|
|
546
|
+
return null;
|
|
547
|
+
// WorldTracker is only available in @zappar/zappar >= 4.x.
|
|
548
|
+
const ZapparAny = this._zappar;
|
|
549
|
+
if (typeof ZapparAny['WorldTracker'] !== 'function') {
|
|
550
|
+
console.warn('[ZapparProvider] WorldTracker not available in the installed' +
|
|
551
|
+
' version of @zappar/zappar. Upgrade to >= 4.x to enable plane tracking.');
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
const TrackerCtor = ZapparAny['WorldTracker'];
|
|
555
|
+
this._worldTracker = new TrackerCtor(this.pipeline);
|
|
556
|
+
this._worldTracker.enabled = true;
|
|
557
|
+
this._worldTracker.horizontalPlaneDetectionEnabled = true;
|
|
558
|
+
return this._worldTracker;
|
|
559
|
+
}
|
|
482
560
|
async ensureCameraRunning() {
|
|
483
561
|
if (!this.cameraSource || this.cameraStarted)
|
|
484
562
|
return;
|
|
@@ -572,6 +650,33 @@ export class ZapparProvider extends ARProvider {
|
|
|
572
650
|
get hasSlamTrackingState() {
|
|
573
651
|
return this._slamStateValid;
|
|
574
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* `true` while the instant world anchor is being continuously repositioned
|
|
655
|
+
* in front of the camera (either during warmup or during user-placement
|
|
656
|
+
* mode). `false` once the anchor has been locked.
|
|
657
|
+
*/
|
|
658
|
+
get isPlacingInstantAnchor() {
|
|
659
|
+
return this._userPlacementMode || this._anchorWarmupFramesRemaining > 0;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Enter user-placement mode: the instant world anchor tracks 5 m in front
|
|
663
|
+
* of the camera every frame until {@link placeInstantAnchor} is called.
|
|
664
|
+
*
|
|
665
|
+
* Typically called by a placement-UI component immediately after session
|
|
666
|
+
* start so the user can choose where to lock the world origin.
|
|
667
|
+
*/
|
|
668
|
+
startUserPlacement() {
|
|
669
|
+
this._userPlacementMode = true;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Lock the instant world anchor at its current position and exit placement
|
|
673
|
+
* mode. After this call {@link isPlacingInstantAnchor} returns `false` and
|
|
674
|
+
* the anchor no longer follows the camera.
|
|
675
|
+
*/
|
|
676
|
+
placeInstantAnchor() {
|
|
677
|
+
this._userPlacementMode = false;
|
|
678
|
+
this._anchorWarmupFramesRemaining = 0;
|
|
679
|
+
}
|
|
575
680
|
get slamFrameNumber() {
|
|
576
681
|
return this._slamFrameNumber;
|
|
577
682
|
}
|
|
@@ -660,13 +765,14 @@ export class ZapparProvider extends ARProvider {
|
|
|
660
765
|
// Let Zappar continuously refine a stable surface point briefly, then lock.
|
|
661
766
|
// If we lock immediately at startup, we can end up with effectively 3DoF behavior.
|
|
662
767
|
// If we *never* lock, the origin follows the camera and the camera appears frozen.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
this.
|
|
667
|
-
|
|
668
|
-
else if (!this.hasInitializedAnchor) {
|
|
768
|
+
// _userPlacementMode keeps the anchor updating beyond the warmup window until the
|
|
769
|
+
// user explicitly confirms placement via placeInstantAnchor().
|
|
770
|
+
if (this._anchorWarmupFramesRemaining > 0 ||
|
|
771
|
+
this._userPlacementMode ||
|
|
772
|
+
!this.hasInitializedAnchor) {
|
|
669
773
|
this.instantTracker.setAnchorPoseFromCameraOffset(0, 0, -5);
|
|
774
|
+
if (this._anchorWarmupFramesRemaining > 0)
|
|
775
|
+
this._anchorWarmupFramesRemaining--;
|
|
670
776
|
this.hasInitializedAnchor = true;
|
|
671
777
|
}
|
|
672
778
|
// Use the active view's near/far when available; incorrect near/far can make tracking feel
|
|
@@ -676,7 +782,8 @@ export class ZapparProvider extends ARProvider {
|
|
|
676
782
|
return;
|
|
677
783
|
const zNear = activeView.near;
|
|
678
784
|
const zFar = activeView.far;
|
|
679
|
-
const
|
|
785
|
+
const [cameraDataWidth, cameraDataHeight] = this.pipeline.cameraDataSize();
|
|
786
|
+
const projectionMatrix = Zappar.projectionMatrixFromCameraModelAndSize(this.pipeline.cameraModel(), cameraDataWidth, cameraDataHeight, this.engine.canvas.width, this.engine.canvas.height, zNear, zFar);
|
|
680
787
|
let origin;
|
|
681
788
|
let cameraPoseMatrix;
|
|
682
789
|
// Match zappar-threejs `CameraPoseMode` behavior.
|
|
@@ -829,6 +936,9 @@ export class ZapparProvider extends ARProvider {
|
|
|
829
936
|
if (this.instantTracker) {
|
|
830
937
|
this.instantTracker.enabled = false;
|
|
831
938
|
}
|
|
939
|
+
if (this._worldTracker) {
|
|
940
|
+
this._worldTracker.enabled = false;
|
|
941
|
+
}
|
|
832
942
|
if (this._xrSession) {
|
|
833
943
|
try {
|
|
834
944
|
await this._xrSession.end();
|
|
@@ -840,6 +950,7 @@ export class ZapparProvider extends ARProvider {
|
|
|
840
950
|
}
|
|
841
951
|
this.hasInitializedAnchor = false;
|
|
842
952
|
this._anchorWarmupFramesRemaining = 0;
|
|
953
|
+
this._userPlacementMode = false;
|
|
843
954
|
this._slamStateValid = false;
|
|
844
955
|
this.onSessionEnd.notify(this);
|
|
845
956
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wonderlandengine/ar-provider-zappar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Wonderland Engine AR tracking provider based on Zappar",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@wonderlandengine/api": "^1.0.0",
|
|
40
|
-
"@wonderlandengine/ar-tracking": "^1.
|
|
40
|
+
"@wonderlandengine/ar-tracking": "^1.2.0",
|
|
41
41
|
"@wonderlandengine/components": "^1.0.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@zappar/zappar": "^
|
|
44
|
+
"@zappar/zappar": "^4.3.0",
|
|
45
45
|
"gl-matrix": "^3.4.3"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {createRequire} from 'node:module';
|
|
4
4
|
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import {execSync} from 'node:child_process';
|
|
5
6
|
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -108,51 +109,111 @@ function main() {
|
|
|
108
109
|
const projectRoot = resolveProjectRoot();
|
|
109
110
|
const outDir = path.resolve(projectRoot, 'static', 'zappar-cv');
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
// -------------------------------------------------------------------------
|
|
113
|
+
// Detect package layout.
|
|
114
|
+
// @zappar/zappar < 4.x ships a pre-bundled UMD worker in `umd/`.
|
|
115
|
+
// @zappar/zappar >= 4.x ships only unbundled ESM; the worker must be bundled
|
|
116
|
+
// from `@zappar/zappar-cv/lib/worker.js` using esbuild.
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
const isLegacyLayout = fs.existsSync(umdDir);
|
|
119
|
+
|
|
120
|
+
if (isLegacyLayout) {
|
|
121
|
+
// ---- Legacy (< 4.x) path ----
|
|
122
|
+
const workerSource =
|
|
123
|
+
findFirstFile(umdDir, (name) => name === 'zappar.worker.js') ??
|
|
124
|
+
findFirstFile(umdDir, (name) => name === 'zappar-cv.worker.js') ??
|
|
125
|
+
findFirstFile(zapparRoot, (name) => name === 'zappar.worker.js') ??
|
|
126
|
+
findFirstFile(zapparRoot, (name) => name === 'zappar-cv.worker.js') ??
|
|
127
|
+
null;
|
|
128
|
+
|
|
129
|
+
if (!workerSource) {
|
|
130
|
+
warn('Could not locate Zappar worker under', umdDir);
|
|
131
|
+
warn('Zappar root:', zapparRoot);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
const wasmCandidates = listFiles(umdDir)
|
|
136
|
+
.filter((e) => e.isFile() && e.name.endsWith('.wasm'))
|
|
137
|
+
.map((e) => path.join(umdDir, e.name));
|
|
127
138
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
let wasmSource = wasmCandidates.length === 1 ? wasmCandidates[0] : null;
|
|
140
|
+
wasmSource = wasmSource ?? findWasmFromWorker(workerSource, umdDir);
|
|
141
|
+
wasmSource = wasmSource ?? (wasmCandidates.length > 0 ? wasmCandidates[0] : null);
|
|
131
142
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
if (!wasmSource) {
|
|
144
|
+
warn('Could not locate Zappar wasm under', umdDir);
|
|
145
|
+
warn('Worker found at:', workerSource);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
137
148
|
|
|
138
|
-
|
|
149
|
+
fs.mkdirSync(outDir, {recursive: true});
|
|
139
150
|
|
|
140
|
-
|
|
141
|
-
|
|
151
|
+
const workerDest = path.join(outDir, 'zappar-cv.worker.js');
|
|
152
|
+
copyOrThrow(workerSource, workerDest);
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
const wasmBasename = path.basename(wasmSource);
|
|
155
|
+
const wasmDest = path.join(outDir, wasmBasename);
|
|
156
|
+
copyOrThrow(wasmSource, wasmDest);
|
|
157
|
+
safeLinkOrCopy(wasmDest, path.join(outDir, 'zappar-cv.wasm'));
|
|
158
|
+
// Also copy wasm to static root so the bundled worker can find it at /zappar-cv.wasm.
|
|
159
|
+
copyOrThrow(wasmSource, path.join(path.dirname(outDir), 'zappar-cv.wasm'));
|
|
146
160
|
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
for (const entry of listFiles(umdDir)) {
|
|
162
|
+
if (!entry.isFile()) continue;
|
|
163
|
+
if (!entry.name.endsWith('.zbin')) continue;
|
|
164
|
+
copyOrThrow(path.join(umdDir, entry.name), path.join(outDir, entry.name));
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// ---- Modern (>= 4.x) path ----
|
|
168
|
+
// Bundle the worker entry from @zappar/zappar-cv/lib/worker.js with esbuild.
|
|
169
|
+
const workerEntry = path.join(zapparCvLibDir, 'worker.js');
|
|
170
|
+
if (!fs.existsSync(workerEntry)) {
|
|
171
|
+
warn('Could not find worker entry at', workerEntry);
|
|
172
|
+
warn('Skipping worker bundling.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
149
175
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
fs.mkdirSync(outDir, {recursive: true});
|
|
177
|
+
|
|
178
|
+
const workerDest = path.join(outDir, 'zappar-cv.worker.js');
|
|
179
|
+
|
|
180
|
+
// Try esbuild; it may be installed locally in the consumer project or globally.
|
|
181
|
+
const esbuildPaths = [
|
|
182
|
+
// consumer project's own node_modules (most reliable)
|
|
183
|
+
path.resolve(projectRoot, 'node_modules', '.bin', 'esbuild'),
|
|
184
|
+
path.resolve(projectRoot, 'node_modules', '.bin', 'esbuild.cmd'),
|
|
185
|
+
// provider package's own node_modules (if esbuild is a devDep there)
|
|
186
|
+
path.resolve(__dirname, '..', 'node_modules', '.bin', 'esbuild'),
|
|
187
|
+
path.resolve(__dirname, '..', 'node_modules', '.bin', 'esbuild.cmd'),
|
|
188
|
+
];
|
|
189
|
+
const esbuildBin = esbuildPaths.find((p) => fs.existsSync(p));
|
|
190
|
+
|
|
191
|
+
if (!esbuildBin) {
|
|
192
|
+
warn('esbuild not found — cannot bundle worker for @zappar/zappar >= 4.x.');
|
|
193
|
+
warn('Add esbuild as a devDependency in your project and re-run npm install.');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
log('Bundling Zappar CV worker with esbuild…');
|
|
198
|
+
try {
|
|
199
|
+
execSync(
|
|
200
|
+
`"${esbuildBin}" "${workerEntry}" --bundle --format=esm --platform=browser --outfile="${workerDest}"`,
|
|
201
|
+
{stdio: 'inherit'}
|
|
202
|
+
);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
warn('esbuild failed to bundle the Zappar CV worker:', e.message ?? e);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Copy the wasm file next to the worker so relative URL resolution works.
|
|
209
|
+
const wasmSrc = path.join(zapparCvLibDir, 'zappar-cv.wasm');
|
|
210
|
+
if (!fs.existsSync(wasmSrc)) {
|
|
211
|
+
warn('Could not find zappar-cv.wasm at', wasmSrc);
|
|
212
|
+
} else {
|
|
213
|
+
copyOrThrow(wasmSrc, path.join(outDir, 'zappar-cv.wasm'));
|
|
214
|
+
// Also copy wasm to static root so the bundled worker can find it at /zappar-cv.wasm.
|
|
215
|
+
copyOrThrow(wasmSrc, path.join(path.dirname(outDir), 'zappar-cv.wasm'));
|
|
216
|
+
}
|
|
156
217
|
}
|
|
157
218
|
|
|
158
219
|
// Required for face tracking defaults (FaceTracker.loadDefaultModel / FaceMesh.loadDefault*).
|
|
@@ -174,6 +235,27 @@ function main() {
|
|
|
174
235
|
}
|
|
175
236
|
|
|
176
237
|
log('Copied Zappar CV assets into', outDir);
|
|
238
|
+
|
|
239
|
+
// -------------------------------------------------------------------------
|
|
240
|
+
// Patch @zappar/zappar-cv/lib/profile.js: the file guards on
|
|
241
|
+
// `typeof window !== "undefined"` but then accesses `window.location`
|
|
242
|
+
// unconditionally. In the WLE editor's bundler context `window` is shimmed
|
|
243
|
+
// but `window.location` is undefined, crashing the component parse pass.
|
|
244
|
+
// Replace the bare access with optional chaining so it safely returns
|
|
245
|
+
// undefined rather than throwing.
|
|
246
|
+
// -------------------------------------------------------------------------
|
|
247
|
+
const profilePath = path.join(zapparCvRoot, 'lib', 'profile.js');
|
|
248
|
+
if (fs.existsSync(profilePath)) {
|
|
249
|
+
let profileSrc = fs.readFileSync(profilePath, 'utf8');
|
|
250
|
+
const patched = profileSrc.replaceAll(
|
|
251
|
+
'window.location.href',
|
|
252
|
+
'window.location?.href?'
|
|
253
|
+
);
|
|
254
|
+
if (patched !== profileSrc) {
|
|
255
|
+
fs.writeFileSync(profilePath, patched, 'utf8');
|
|
256
|
+
log('Patched @zappar/zappar-cv/lib/profile.js (window.location guard)');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
177
259
|
}
|
|
178
260
|
|
|
179
261
|
try {
|