@wonderlandengine/ar-provider-zappar 1.1.1 → 1.2.1
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 +130 -12
- package/package.json +3 -3
- package/scripts/postinstall.mjs +123 -40
|
@@ -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);
|
|
@@ -127,6 +137,15 @@ export class ZapparProvider extends ARProvider {
|
|
|
127
137
|
if (this.instantTracker) {
|
|
128
138
|
this.instantTracker.enabled = true;
|
|
129
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
|
+
}
|
|
130
149
|
}
|
|
131
150
|
ensurePreRenderRegistered() {
|
|
132
151
|
if (!this.preRenderRegistered) {
|
|
@@ -190,8 +209,8 @@ export class ZapparProvider extends ARProvider {
|
|
|
190
209
|
if (ZapparProvider._cvWorkerConfigured)
|
|
191
210
|
return;
|
|
192
211
|
// Hard-coded paths for local development
|
|
193
|
-
const workerUrl = './zappar-cv
|
|
194
|
-
const wasmUrl = './zappar-cv
|
|
212
|
+
const workerUrl = './zappar-cv.worker.js';
|
|
213
|
+
const wasmUrl = './zappar-cv.wasm';
|
|
195
214
|
const debugWorker = this.isVerboseDebugLoggingEnabled;
|
|
196
215
|
if (debugWorker) {
|
|
197
216
|
console.log('[ZapparProvider] configureCvWorkerIfNeeded()', {
|
|
@@ -206,7 +225,7 @@ export class ZapparProvider extends ARProvider {
|
|
|
206
225
|
if (debugWorker) {
|
|
207
226
|
console.log('[ZapparProvider] Creating CV worker:', workerUrl);
|
|
208
227
|
}
|
|
209
|
-
const worker = new Worker(workerUrl);
|
|
228
|
+
const worker = new Worker(workerUrl, { type: 'module' });
|
|
210
229
|
// Keep a reference for debugging / inspection.
|
|
211
230
|
ZapparProvider._cvWorker = worker;
|
|
212
231
|
if (typeof window !== 'undefined') {
|
|
@@ -265,12 +284,18 @@ export class ZapparProvider extends ARProvider {
|
|
|
265
284
|
}, 5000);
|
|
266
285
|
}
|
|
267
286
|
if (wasmUrl) {
|
|
287
|
+
const resolvedWasmUrl = new URL(wasmUrl, window.location.href).toString();
|
|
268
288
|
if (debugWorker) {
|
|
269
|
-
console.log('[ZapparProvider]
|
|
289
|
+
console.log('[ZapparProvider] Compiling WASM for worker:', resolvedWasmUrl);
|
|
270
290
|
}
|
|
291
|
+
// The worker's instantiateWasm callback expects a pre-compiled
|
|
292
|
+
// WebAssembly.Module (not just a URL). Compile it on the main
|
|
293
|
+
// thread so it can be structured-cloned into the worker.
|
|
294
|
+
const compiledModule = await WebAssembly.compileStreaming(fetch(resolvedWasmUrl));
|
|
271
295
|
worker.postMessage({
|
|
272
296
|
t: 'wasm',
|
|
273
|
-
url:
|
|
297
|
+
url: resolvedWasmUrl,
|
|
298
|
+
module: compiledModule,
|
|
274
299
|
});
|
|
275
300
|
}
|
|
276
301
|
await zapparSetOptions({ worker });
|
|
@@ -399,6 +424,12 @@ export class ZapparProvider extends ARProvider {
|
|
|
399
424
|
if (!options.name) {
|
|
400
425
|
throw new Error('Image target registration requires a name.');
|
|
401
426
|
}
|
|
427
|
+
// Deduplicate: if a descriptor with the same name is already registered,
|
|
428
|
+
// skip loading to avoid double-loading the same .zpt file when multiple
|
|
429
|
+
// scene components share the same imageId target.
|
|
430
|
+
if (this._imageTargetDescriptors.some((d) => d.name === options.name)) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
402
433
|
// Allow registering targets before an AR session starts.
|
|
403
434
|
// This is useful so image targets are known for ImageScanningEvent
|
|
404
435
|
// and so apps can prefetch/prepare targets without requesting camera access.
|
|
@@ -478,6 +509,60 @@ export class ZapparProvider extends ARProvider {
|
|
|
478
509
|
getImageTargetDescriptor(index) {
|
|
479
510
|
return this._imageTargetDescriptors[index];
|
|
480
511
|
}
|
|
512
|
+
// -----------------------------------------------------------------------
|
|
513
|
+
// World tracking / plane anchors (Zappar SDK >= 4.x)
|
|
514
|
+
// -----------------------------------------------------------------------
|
|
515
|
+
/**
|
|
516
|
+
* Access the active {@link ZapparWorldTracker} instance, or `null` when
|
|
517
|
+
* plane tracking has not been requested or is unavailable.
|
|
518
|
+
*
|
|
519
|
+
* The instance is only non-null once {@link enableWorldTracker} has been
|
|
520
|
+
* called **and** an AR session is running.
|
|
521
|
+
*/
|
|
522
|
+
get worldTracker() {
|
|
523
|
+
return this._worldTracker;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Opt in to Zappar plane tracking via the `WorldTracker` API introduced in
|
|
527
|
+
* `@zappar/zappar >= 4.x`.
|
|
528
|
+
*
|
|
529
|
+
* Call this once (e.g. from a component's `start()` hook) before or after
|
|
530
|
+
* a session starts. If the installed SDK does not expose `WorldTracker`
|
|
531
|
+
* (i.e. < 4.x) a warning is logged and the method returns `null`.
|
|
532
|
+
*
|
|
533
|
+
* The tracker is disabled again when the session ends and re-enabled on
|
|
534
|
+
* the next `startSession()` call.
|
|
535
|
+
*
|
|
536
|
+
* @returns The {@link ZapparWorldTracker} instance, or `null` on failure.
|
|
537
|
+
*/
|
|
538
|
+
enableWorldTracker() {
|
|
539
|
+
if (this._worldTracker) {
|
|
540
|
+
this._worldTracker.enabled = true;
|
|
541
|
+
return this._worldTracker;
|
|
542
|
+
}
|
|
543
|
+
if (this.pipeline && this._zappar) {
|
|
544
|
+
return this._createWorldTracker();
|
|
545
|
+
}
|
|
546
|
+
// Session not yet started – remember the request.
|
|
547
|
+
this._worldTrackerRequested = true;
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
_createWorldTracker() {
|
|
551
|
+
if (!this.pipeline || !this._zappar)
|
|
552
|
+
return null;
|
|
553
|
+
// WorldTracker is only available in @zappar/zappar >= 4.x.
|
|
554
|
+
const ZapparAny = this._zappar;
|
|
555
|
+
if (typeof ZapparAny['WorldTracker'] !== 'function') {
|
|
556
|
+
console.warn('[ZapparProvider] WorldTracker not available in the installed' +
|
|
557
|
+
' version of @zappar/zappar. Upgrade to >= 4.x to enable plane tracking.');
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
const TrackerCtor = ZapparAny['WorldTracker'];
|
|
561
|
+
this._worldTracker = new TrackerCtor(this.pipeline);
|
|
562
|
+
this._worldTracker.enabled = true;
|
|
563
|
+
this._worldTracker.horizontalPlaneDetectionEnabled = true;
|
|
564
|
+
return this._worldTracker;
|
|
565
|
+
}
|
|
481
566
|
async ensureCameraRunning() {
|
|
482
567
|
if (!this.cameraSource || this.cameraStarted)
|
|
483
568
|
return;
|
|
@@ -571,6 +656,33 @@ export class ZapparProvider extends ARProvider {
|
|
|
571
656
|
get hasSlamTrackingState() {
|
|
572
657
|
return this._slamStateValid;
|
|
573
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* `true` while the instant world anchor is being continuously repositioned
|
|
661
|
+
* in front of the camera (either during warmup or during user-placement
|
|
662
|
+
* mode). `false` once the anchor has been locked.
|
|
663
|
+
*/
|
|
664
|
+
get isPlacingInstantAnchor() {
|
|
665
|
+
return this._userPlacementMode || this._anchorWarmupFramesRemaining > 0;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Enter user-placement mode: the instant world anchor tracks 5 m in front
|
|
669
|
+
* of the camera every frame until {@link placeInstantAnchor} is called.
|
|
670
|
+
*
|
|
671
|
+
* Typically called by a placement-UI component immediately after session
|
|
672
|
+
* start so the user can choose where to lock the world origin.
|
|
673
|
+
*/
|
|
674
|
+
startUserPlacement() {
|
|
675
|
+
this._userPlacementMode = true;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Lock the instant world anchor at its current position and exit placement
|
|
679
|
+
* mode. After this call {@link isPlacingInstantAnchor} returns `false` and
|
|
680
|
+
* the anchor no longer follows the camera.
|
|
681
|
+
*/
|
|
682
|
+
placeInstantAnchor() {
|
|
683
|
+
this._userPlacementMode = false;
|
|
684
|
+
this._anchorWarmupFramesRemaining = 0;
|
|
685
|
+
}
|
|
574
686
|
get slamFrameNumber() {
|
|
575
687
|
return this._slamFrameNumber;
|
|
576
688
|
}
|
|
@@ -659,13 +771,14 @@ export class ZapparProvider extends ARProvider {
|
|
|
659
771
|
// Let Zappar continuously refine a stable surface point briefly, then lock.
|
|
660
772
|
// If we lock immediately at startup, we can end up with effectively 3DoF behavior.
|
|
661
773
|
// If we *never* lock, the origin follows the camera and the camera appears frozen.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
this.
|
|
666
|
-
|
|
667
|
-
else if (!this.hasInitializedAnchor) {
|
|
774
|
+
// _userPlacementMode keeps the anchor updating beyond the warmup window until the
|
|
775
|
+
// user explicitly confirms placement via placeInstantAnchor().
|
|
776
|
+
if (this._anchorWarmupFramesRemaining > 0 ||
|
|
777
|
+
this._userPlacementMode ||
|
|
778
|
+
!this.hasInitializedAnchor) {
|
|
668
779
|
this.instantTracker.setAnchorPoseFromCameraOffset(0, 0, -5);
|
|
780
|
+
if (this._anchorWarmupFramesRemaining > 0)
|
|
781
|
+
this._anchorWarmupFramesRemaining--;
|
|
669
782
|
this.hasInitializedAnchor = true;
|
|
670
783
|
}
|
|
671
784
|
// Use the active view's near/far when available; incorrect near/far can make tracking feel
|
|
@@ -675,7 +788,8 @@ export class ZapparProvider extends ARProvider {
|
|
|
675
788
|
return;
|
|
676
789
|
const zNear = activeView.near;
|
|
677
790
|
const zFar = activeView.far;
|
|
678
|
-
const
|
|
791
|
+
const [cameraDataWidth, cameraDataHeight] = this.pipeline.cameraDataSize();
|
|
792
|
+
const projectionMatrix = Zappar.projectionMatrixFromCameraModelAndSize(this.pipeline.cameraModel(), cameraDataWidth, cameraDataHeight, this.engine.canvas.width, this.engine.canvas.height, zNear, zFar);
|
|
679
793
|
let origin;
|
|
680
794
|
let cameraPoseMatrix;
|
|
681
795
|
// Match zappar-threejs `CameraPoseMode` behavior.
|
|
@@ -828,6 +942,9 @@ export class ZapparProvider extends ARProvider {
|
|
|
828
942
|
if (this.instantTracker) {
|
|
829
943
|
this.instantTracker.enabled = false;
|
|
830
944
|
}
|
|
945
|
+
if (this._worldTracker) {
|
|
946
|
+
this._worldTracker.enabled = false;
|
|
947
|
+
}
|
|
831
948
|
if (this._xrSession) {
|
|
832
949
|
try {
|
|
833
950
|
await this._xrSession.end();
|
|
@@ -839,6 +956,7 @@ export class ZapparProvider extends ARProvider {
|
|
|
839
956
|
}
|
|
840
957
|
this.hasInitializedAnchor = false;
|
|
841
958
|
this._anchorWarmupFramesRemaining = 0;
|
|
959
|
+
this._userPlacementMode = false;
|
|
842
960
|
this._slamStateValid = false;
|
|
843
961
|
this.onSessionEnd.notify(this);
|
|
844
962
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wonderlandengine/ar-provider-zappar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
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);
|
|
@@ -106,53 +107,114 @@ function main() {
|
|
|
106
107
|
const zapparCvLibDir = path.join(zapparCvRoot, 'lib');
|
|
107
108
|
|
|
108
109
|
const projectRoot = resolveProjectRoot();
|
|
109
|
-
const outDir = path.resolve(projectRoot, 'static'
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
const outDir = path.resolve(projectRoot, 'static');
|
|
111
|
+
// Face/CV model files (.zbin) are fetched by the Zappar runtime from the
|
|
112
|
+
// hardcoded path "zappar-cv/<filename>" relative to the page origin, so
|
|
113
|
+
// they must live in a zappar-cv/ subdirectory even though the worker and
|
|
114
|
+
// wasm sit at the static root.
|
|
115
|
+
const modelsDir = path.resolve(projectRoot, 'static', 'zappar-cv');
|
|
116
|
+
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
// Detect package layout.
|
|
119
|
+
// @zappar/zappar < 4.x ships a pre-bundled UMD worker in `umd/`.
|
|
120
|
+
// @zappar/zappar >= 4.x ships only unbundled ESM; the worker must be bundled
|
|
121
|
+
// from `@zappar/zappar-cv/lib/worker.js` using esbuild.
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
const isLegacyLayout = fs.existsSync(umdDir);
|
|
124
|
+
|
|
125
|
+
if (isLegacyLayout) {
|
|
126
|
+
// ---- Legacy (< 4.x) path ----
|
|
127
|
+
const workerSource =
|
|
128
|
+
findFirstFile(umdDir, (name) => name === 'zappar.worker.js') ??
|
|
129
|
+
findFirstFile(umdDir, (name) => name === 'zappar-cv.worker.js') ??
|
|
130
|
+
findFirstFile(zapparRoot, (name) => name === 'zappar.worker.js') ??
|
|
131
|
+
findFirstFile(zapparRoot, (name) => name === 'zappar-cv.worker.js') ??
|
|
132
|
+
null;
|
|
133
|
+
|
|
134
|
+
if (!workerSource) {
|
|
135
|
+
warn('Could not locate Zappar worker under', umdDir);
|
|
136
|
+
warn('Zappar root:', zapparRoot);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
123
139
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
const wasmCandidates = listFiles(umdDir)
|
|
141
|
+
.filter((e) => e.isFile() && e.name.endsWith('.wasm'))
|
|
142
|
+
.map((e) => path.join(umdDir, e.name));
|
|
127
143
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
let wasmSource = wasmCandidates.length === 1 ? wasmCandidates[0] : null;
|
|
145
|
+
wasmSource = wasmSource ?? findWasmFromWorker(workerSource, umdDir);
|
|
146
|
+
wasmSource = wasmSource ?? (wasmCandidates.length > 0 ? wasmCandidates[0] : null);
|
|
131
147
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
if (!wasmSource) {
|
|
149
|
+
warn('Could not locate Zappar wasm under', umdDir);
|
|
150
|
+
warn('Worker found at:', workerSource);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
137
153
|
|
|
138
|
-
|
|
154
|
+
fs.mkdirSync(outDir, {recursive: true});
|
|
139
155
|
|
|
140
|
-
|
|
141
|
-
|
|
156
|
+
const workerDest = path.join(outDir, 'zappar-cv.worker.js');
|
|
157
|
+
copyOrThrow(workerSource, workerDest);
|
|
142
158
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
const wasmBasename = path.basename(wasmSource);
|
|
160
|
+
const wasmDest = path.join(outDir, wasmBasename);
|
|
161
|
+
copyOrThrow(wasmSource, wasmDest);
|
|
162
|
+
safeLinkOrCopy(wasmDest, path.join(outDir, 'zappar-cv.wasm'));
|
|
146
163
|
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
for (const entry of listFiles(umdDir)) {
|
|
165
|
+
if (!entry.isFile()) continue;
|
|
166
|
+
if (!entry.name.endsWith('.zbin')) continue;
|
|
167
|
+
copyOrThrow(path.join(umdDir, entry.name), path.join(modelsDir, entry.name));
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
// ---- Modern (>= 4.x) path ----
|
|
171
|
+
// Bundle the worker entry from @zappar/zappar-cv/lib/worker.js with esbuild.
|
|
172
|
+
const workerEntry = path.join(zapparCvLibDir, 'worker.js');
|
|
173
|
+
if (!fs.existsSync(workerEntry)) {
|
|
174
|
+
warn('Could not find worker entry at', workerEntry);
|
|
175
|
+
warn('Skipping worker bundling.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
149
178
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
179
|
+
fs.mkdirSync(outDir, {recursive: true});
|
|
180
|
+
|
|
181
|
+
const workerDest = path.join(outDir, 'zappar-cv.worker.js');
|
|
182
|
+
|
|
183
|
+
// Try esbuild; it may be installed locally in the consumer project or globally.
|
|
184
|
+
const esbuildPaths = [
|
|
185
|
+
// consumer project's own node_modules (most reliable)
|
|
186
|
+
path.resolve(projectRoot, 'node_modules', '.bin', 'esbuild'),
|
|
187
|
+
path.resolve(projectRoot, 'node_modules', '.bin', 'esbuild.cmd'),
|
|
188
|
+
// provider package's own node_modules (if esbuild is a devDep there)
|
|
189
|
+
path.resolve(__dirname, '..', 'node_modules', '.bin', 'esbuild'),
|
|
190
|
+
path.resolve(__dirname, '..', 'node_modules', '.bin', 'esbuild.cmd'),
|
|
191
|
+
];
|
|
192
|
+
const esbuildBin = esbuildPaths.find((p) => fs.existsSync(p));
|
|
193
|
+
|
|
194
|
+
if (!esbuildBin) {
|
|
195
|
+
warn('esbuild not found — cannot bundle worker for @zappar/zappar >= 4.x.');
|
|
196
|
+
warn('Add esbuild as a devDependency in your project and re-run npm install.');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
log('Bundling Zappar CV worker with esbuild…');
|
|
201
|
+
try {
|
|
202
|
+
execSync(
|
|
203
|
+
`"${esbuildBin}" "${workerEntry}" --bundle --format=esm --platform=browser --outfile="${workerDest}"`,
|
|
204
|
+
{stdio: 'inherit'}
|
|
205
|
+
);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
warn('esbuild failed to bundle the Zappar CV worker:', e.message ?? e);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Copy the wasm file next to the worker so relative URL resolution works.
|
|
212
|
+
const wasmSrc = path.join(zapparCvLibDir, 'zappar-cv.wasm');
|
|
213
|
+
if (!fs.existsSync(wasmSrc)) {
|
|
214
|
+
warn('Could not find zappar-cv.wasm at', wasmSrc);
|
|
215
|
+
} else {
|
|
216
|
+
copyOrThrow(wasmSrc, path.join(outDir, 'zappar-cv.wasm'));
|
|
217
|
+
}
|
|
156
218
|
}
|
|
157
219
|
|
|
158
220
|
// Required for face tracking defaults (FaceTracker.loadDefaultModel / FaceMesh.loadDefault*).
|
|
@@ -164,7 +226,7 @@ function main() {
|
|
|
164
226
|
|
|
165
227
|
for (const filename of requiredFaceModelFiles) {
|
|
166
228
|
const src = path.join(zapparCvLibDir, filename);
|
|
167
|
-
const dest = path.join(
|
|
229
|
+
const dest = path.join(modelsDir, filename);
|
|
168
230
|
if (!fs.existsSync(src)) {
|
|
169
231
|
warn('Could not locate required Zappar face model:', filename);
|
|
170
232
|
warn('Looked under:', zapparCvLibDir);
|
|
@@ -174,6 +236,27 @@ function main() {
|
|
|
174
236
|
}
|
|
175
237
|
|
|
176
238
|
log('Copied Zappar CV assets into', outDir);
|
|
239
|
+
|
|
240
|
+
// -------------------------------------------------------------------------
|
|
241
|
+
// Patch @zappar/zappar-cv/lib/profile.js: the file guards on
|
|
242
|
+
// `typeof window !== "undefined"` but then accesses `window.location`
|
|
243
|
+
// unconditionally. In the WLE editor's bundler context `window` is shimmed
|
|
244
|
+
// but `window.location` is undefined, crashing the component parse pass.
|
|
245
|
+
// Replace the bare access with optional chaining so it safely returns
|
|
246
|
+
// undefined rather than throwing.
|
|
247
|
+
// -------------------------------------------------------------------------
|
|
248
|
+
const profilePath = path.join(zapparCvRoot, 'lib', 'profile.js');
|
|
249
|
+
if (fs.existsSync(profilePath)) {
|
|
250
|
+
let profileSrc = fs.readFileSync(profilePath, 'utf8');
|
|
251
|
+
const patched = profileSrc.replaceAll(
|
|
252
|
+
'window.location.href',
|
|
253
|
+
'window.location?.href?'
|
|
254
|
+
);
|
|
255
|
+
if (patched !== profileSrc) {
|
|
256
|
+
fs.writeFileSync(profilePath, patched, 'utf8');
|
|
257
|
+
log('Patched @zappar/zappar-cv/lib/profile.js (window.location guard)');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
177
260
|
}
|
|
178
261
|
|
|
179
262
|
try {
|