@wonderlandengine/ar-provider-zappar 1.0.0 → 1.1.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/README.md +20 -5
- package/dist/src/face-tracking-mode-zappar.d.ts +7 -1
- package/dist/src/face-tracking-mode-zappar.js +129 -50
- package/dist/src/image-tracking-mode-zappar.d.ts +1 -0
- package/dist/src/image-tracking-mode-zappar.js +63 -2
- package/dist/src/world-tracking-mode-zappar.js +2 -0
- package/dist/src/zappar-provider.d.ts +12 -0
- package/dist/src/zappar-provider.js +71 -25
- package/package.json +2 -2
- package/scripts/postinstall.mjs +184 -184
package/README.md
CHANGED
|
@@ -19,6 +19,14 @@ This package depends on `@zappar/zappar`; make sure your bundler is configured
|
|
|
19
19
|
to serve the `zcv.wasm` asset as described in the
|
|
20
20
|
[Zappar installation guide](https://docs.zap.works/universal-ar/javascript/getting-started/installation/).
|
|
21
21
|
|
|
22
|
+
For Wonderland Editor publishing and image-target training workflows, see the
|
|
23
|
+
companion package
|
|
24
|
+
[`@wonderlandengine/zappar-publish-plugin`](https://www.npmjs.com/package/@wonderlandengine/zappar-publish-plugin).
|
|
25
|
+
|
|
26
|
+
> **Note:** The provider currently keeps tracking active but does not draw the
|
|
27
|
+
> Zappar camera feed to the Wonderland canvas. If you require a background
|
|
28
|
+
> video, composite it manually in your application.
|
|
29
|
+
|
|
22
30
|
## Supported Tracking Modes
|
|
23
31
|
|
|
24
32
|
The Zappar provider implements SLAM, face tracking, and image tracking. These
|
|
@@ -26,9 +34,9 @@ map to the standard Wonderland Engine components supplied by
|
|
|
26
34
|
`@wonderlandengine/ar-tracking` (`ARSLAMCamera`, `ARFaceTrackingCamera`, and
|
|
27
35
|
`ARImageTrackingCamera`).
|
|
28
36
|
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
- Face tracking loads Zappar's default model and mesh. Attachment points that
|
|
38
|
+
have no exact landmark are approximated to the closest available Zappar
|
|
39
|
+
landmark.
|
|
32
40
|
|
|
33
41
|
## Recording a Zappar Sequence (Camera + Motion)
|
|
34
42
|
|
|
@@ -65,8 +73,8 @@ URL.revokeObjectURL(a.href);
|
|
|
65
73
|
|
|
66
74
|
Notes:
|
|
67
75
|
|
|
68
|
-
-
|
|
69
|
-
-
|
|
76
|
+
- Sequence recording captures camera + IMU, so it’s best done on a mobile device.
|
|
77
|
+
- You can replay sequences using Zappar’s `SequenceSource` API (preferred over MP4 replay for correct tracking).
|
|
70
78
|
|
|
71
79
|
### Registering Image Targets
|
|
72
80
|
|
|
@@ -93,3 +101,10 @@ await provider.registerImageTarget('/static/targets/poster.zpt', posterTarget);
|
|
|
93
101
|
|
|
94
102
|
Once registered, the provider emits image scanning and tracking events via
|
|
95
103
|
`ARImageTrackingCamera`.
|
|
104
|
+
|
|
105
|
+
## Related Packages
|
|
106
|
+
|
|
107
|
+
- [`@wonderlandengine/ar-tracking`](https://www.npmjs.com/package/@wonderlandengine/ar-tracking)
|
|
108
|
+
Core AR abstractions and camera components.
|
|
109
|
+
- [`@wonderlandengine/zappar-publish-plugin`](https://www.npmjs.com/package/@wonderlandengine/zappar-publish-plugin)
|
|
110
|
+
Wonderland Editor plugin for ZapWorks upload/publish and target training.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Emitter } from '@wonderlandengine/api';
|
|
2
|
-
import { FaceFoundEvent, FaceLoadingEvent, FaceLostEvent, FaceTrackingMode, TrackingMode } from '@wonderlandengine/ar-tracking';
|
|
2
|
+
import { FaceAttachmentPoint, FaceFoundEvent, FaceLoadingEvent, FaceLostEvent, FaceTrackingMode, TrackingMode } from '@wonderlandengine/ar-tracking';
|
|
3
|
+
type AttachmentMapping = Partial<Record<FaceAttachmentPoint, string>>;
|
|
4
|
+
export declare const AttachmentLandmarkKeys: AttachmentMapping;
|
|
5
|
+
export declare function buildFaceLoadingEventFromMesh(maxDetections: number, verticesArray: ArrayLike<number>, indicesArray: ArrayLike<number>, uvsArray: ArrayLike<number>): FaceLoadingEvent;
|
|
3
6
|
export declare class FaceTracking_Zappar extends TrackingMode implements FaceTrackingMode {
|
|
4
7
|
private _zappar;
|
|
5
8
|
private _view?;
|
|
@@ -19,6 +22,7 @@ export declare class FaceTracking_Zappar extends TrackingMode implements FaceTra
|
|
|
19
22
|
private readonly _scratchPosition;
|
|
20
23
|
private readonly _scratchRotation;
|
|
21
24
|
private readonly _scratchScale;
|
|
25
|
+
private readonly _providerRotation;
|
|
22
26
|
private _loadingEvent;
|
|
23
27
|
readonly onFaceScanning: Emitter<[event: FaceLoadingEvent]>;
|
|
24
28
|
readonly onFaceLoading: Emitter<[event: FaceLoadingEvent]>;
|
|
@@ -36,5 +40,7 @@ export declare class FaceTracking_Zappar extends TrackingMode implements FaceTra
|
|
|
36
40
|
private _handleAnchorNotVisible;
|
|
37
41
|
private _buildFaceEvent;
|
|
38
42
|
private _applyCameraPose;
|
|
43
|
+
private _setProjectionMatrixWithEngineRemap;
|
|
39
44
|
private _anchorNumericId;
|
|
40
45
|
}
|
|
46
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Emitter } from '@wonderlandengine/api';
|
|
2
2
|
import { FaceAttachmentPoint, TrackingMode, } from '@wonderlandengine/ar-tracking';
|
|
3
3
|
import { mat4, quat, vec3 } from 'gl-matrix';
|
|
4
|
-
const AttachmentLandmarkKeys = {
|
|
4
|
+
export const AttachmentLandmarkKeys = {
|
|
5
5
|
[FaceAttachmentPoint.Forehead]: 'NOSE_BRIDGE',
|
|
6
6
|
[FaceAttachmentPoint.EyeOuterCornerLeft]: 'EYE_LEFT',
|
|
7
7
|
[FaceAttachmentPoint.EyeOuterCornerRight]: 'EYE_RIGHT',
|
|
@@ -27,6 +27,39 @@ const AttachmentLandmarkKeys = {
|
|
|
27
27
|
[FaceAttachmentPoint.Chin]: 'CHIN',
|
|
28
28
|
};
|
|
29
29
|
const AttachmentList = Object.values(FaceAttachmentPoint);
|
|
30
|
+
const FACE_LOCAL_Y_180 = quat.fromValues(0, 1, 0, 0);
|
|
31
|
+
function convertFaceLocalVectorToProviderSpace(x, y, z) {
|
|
32
|
+
// Zappar face-local basis differs from our provider contract by a 180° yaw.
|
|
33
|
+
return { x: -x, y, z: -z };
|
|
34
|
+
}
|
|
35
|
+
function convertFaceLocalRotationToProviderSpace(rotation) {
|
|
36
|
+
// If local coordinates are rotated by C, then world rotation must become R' = R * C
|
|
37
|
+
// to preserve world-space geometry.
|
|
38
|
+
const out = quat.create();
|
|
39
|
+
quat.multiply(out, rotation, FACE_LOCAL_Y_180);
|
|
40
|
+
quat.normalize(out, out);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
export function buildFaceLoadingEventFromMesh(maxDetections, verticesArray, indicesArray, uvsArray) {
|
|
44
|
+
const indices = [];
|
|
45
|
+
for (let i = 0; i < indicesArray.length; i += 3) {
|
|
46
|
+
indices.push({
|
|
47
|
+
a: indicesArray[i],
|
|
48
|
+
b: indicesArray[i + 2],
|
|
49
|
+
c: indicesArray[i + 1],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const uvs = [];
|
|
53
|
+
for (let i = 0; i < uvsArray.length; i += 2) {
|
|
54
|
+
uvs.push({ u: 1 - uvsArray[i], v: 1 - uvsArray[i + 1] });
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
maxDetections,
|
|
58
|
+
pointsPerDetection: verticesArray.length / 3,
|
|
59
|
+
indices: indices,
|
|
60
|
+
uvs: uvs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
30
63
|
export class FaceTracking_Zappar extends TrackingMode {
|
|
31
64
|
_zappar = null;
|
|
32
65
|
_view;
|
|
@@ -46,6 +79,7 @@ export class FaceTracking_Zappar extends TrackingMode {
|
|
|
46
79
|
_scratchPosition = vec3.create();
|
|
47
80
|
_scratchRotation = quat.create();
|
|
48
81
|
_scratchScale = vec3.create();
|
|
82
|
+
_providerRotation = quat.create();
|
|
49
83
|
_loadingEvent = null;
|
|
50
84
|
onFaceScanning = new Emitter();
|
|
51
85
|
onFaceLoading = new Emitter();
|
|
@@ -54,6 +88,15 @@ export class FaceTracking_Zappar extends TrackingMode {
|
|
|
54
88
|
onFaceLost = new Emitter();
|
|
55
89
|
init() {
|
|
56
90
|
this._view = this.component.object.getComponent('view') ?? undefined;
|
|
91
|
+
const provider = this.provider;
|
|
92
|
+
const faceComponent = this.component;
|
|
93
|
+
if (typeof faceComponent.cameraDirection === 'number') {
|
|
94
|
+
// ARFaceTrackingCamera enum is ['front', 'back']
|
|
95
|
+
provider.setPreferredCameraUserFacing(faceComponent.cameraDirection === 0);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
provider.setPreferredCameraUserFacing(true);
|
|
99
|
+
}
|
|
57
100
|
const input = this.component.object.getComponent('input');
|
|
58
101
|
if (input) {
|
|
59
102
|
input.active = false;
|
|
@@ -77,14 +120,17 @@ export class FaceTracking_Zappar extends TrackingMode {
|
|
|
77
120
|
return;
|
|
78
121
|
const provider = this.provider;
|
|
79
122
|
const pipeline = provider.getPipeline();
|
|
80
|
-
const
|
|
123
|
+
const mirrorPoses = pipeline.cameraFrameUserFacing();
|
|
124
|
+
const viewNear = this._view?.near;
|
|
125
|
+
const viewFar = this._view?.far;
|
|
126
|
+
const projectionMatrix = Zappar.projectionMatrixFromCameraModel(pipeline.cameraModel(), this.component.engine.canvas.width, this.component.engine.canvas.height, typeof viewNear === 'number' ? viewNear : undefined, typeof viewFar === 'number' ? viewFar : undefined);
|
|
81
127
|
if (this._view) {
|
|
82
|
-
this.
|
|
128
|
+
this._setProjectionMatrixWithEngineRemap(projectionMatrix);
|
|
83
129
|
}
|
|
84
130
|
const cameraPose = pipeline.cameraPoseDefault();
|
|
85
131
|
this._applyCameraPose(cameraPose);
|
|
86
132
|
for (const anchor of this._faceTracker.visible) {
|
|
87
|
-
const event = this._buildFaceEvent(anchor);
|
|
133
|
+
const event = this._buildFaceEvent(anchor, mirrorPoses);
|
|
88
134
|
this.onFaceUpdate.notify(event);
|
|
89
135
|
}
|
|
90
136
|
}
|
|
@@ -133,91 +179,66 @@ export class FaceTracking_Zappar extends TrackingMode {
|
|
|
133
179
|
if (!this._faceTracker || !this._faceMesh) {
|
|
134
180
|
return null;
|
|
135
181
|
}
|
|
136
|
-
|
|
137
|
-
const uvsArray = this._faceMesh.uvs;
|
|
138
|
-
const verticesArray = this._faceMesh.vertices;
|
|
139
|
-
const indices = [];
|
|
140
|
-
for (let i = 0; i < indicesArray.length; i += 3) {
|
|
141
|
-
indices.push({
|
|
142
|
-
a: indicesArray[i],
|
|
143
|
-
b: indicesArray[i + 1],
|
|
144
|
-
c: indicesArray[i + 2],
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
const uvs = [];
|
|
148
|
-
for (let i = 0; i < uvsArray.length; i += 2) {
|
|
149
|
-
uvs.push({ u: uvsArray[i], v: uvsArray[i + 1] });
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
maxDetections: this._faceTracker.maxFaces,
|
|
153
|
-
pointsPerDetection: verticesArray.length / 3,
|
|
154
|
-
indices: indices,
|
|
155
|
-
uvs: uvs,
|
|
156
|
-
};
|
|
182
|
+
return buildFaceLoadingEventFromMesh(this._faceTracker.maxFaces, this._faceMesh.vertices, this._faceMesh.indices, this._faceMesh.uvs);
|
|
157
183
|
}
|
|
158
184
|
_handleAnchorVisible = (anchor) => {
|
|
159
185
|
if (!this._resourcesReady) {
|
|
160
186
|
return;
|
|
161
187
|
}
|
|
162
|
-
const
|
|
188
|
+
const provider = this.provider;
|
|
189
|
+
const pipeline = provider.getPipeline();
|
|
190
|
+
const mirrorPoses = pipeline.cameraFrameUserFacing();
|
|
191
|
+
const event = this._buildFaceEvent(anchor, mirrorPoses);
|
|
163
192
|
this.onFaceFound.notify(event);
|
|
164
193
|
};
|
|
165
194
|
_handleAnchorNotVisible = (anchor) => {
|
|
166
195
|
const id = this._anchorNumericId(anchor.id);
|
|
167
196
|
this.onFaceLost.notify({ id });
|
|
168
197
|
};
|
|
169
|
-
_buildFaceEvent(anchor) {
|
|
198
|
+
_buildFaceEvent(anchor, mirrorPoses) {
|
|
170
199
|
const provider = this.provider;
|
|
171
200
|
const pipeline = provider.getPipeline();
|
|
172
201
|
const cameraPose = pipeline.cameraPoseDefault();
|
|
173
|
-
const anchorPose = anchor.pose(cameraPose,
|
|
202
|
+
const anchorPose = anchor.pose(cameraPose, mirrorPoses);
|
|
174
203
|
mat4.copy(this._scratchMatrix, anchorPose);
|
|
175
204
|
mat4.getTranslation(this._scratchPosition, this._scratchMatrix);
|
|
176
205
|
mat4.getRotation(this._scratchRotation, this._scratchMatrix);
|
|
177
206
|
mat4.getScaling(this._scratchScale, this._scratchMatrix);
|
|
207
|
+
quat.copy(this._providerRotation, convertFaceLocalRotationToProviderSpace(this._scratchRotation));
|
|
208
|
+
this._scratchScale[0] = Math.abs(this._scratchScale[0]);
|
|
209
|
+
this._scratchScale[1] = Math.abs(this._scratchScale[1]);
|
|
210
|
+
this._scratchScale[2] = Math.abs(this._scratchScale[2]);
|
|
178
211
|
const scale = (this._scratchScale[0] + this._scratchScale[1] + this._scratchScale[2]) / 3;
|
|
179
212
|
const anchorPosition = {
|
|
180
213
|
x: this._scratchPosition[0],
|
|
181
214
|
y: this._scratchPosition[1],
|
|
182
215
|
z: this._scratchPosition[2],
|
|
183
216
|
};
|
|
184
|
-
this._faceMesh.updateFromFaceAnchor(anchor,
|
|
217
|
+
this._faceMesh.updateFromFaceAnchor(anchor, mirrorPoses);
|
|
185
218
|
const verticesArray = this._faceMesh.vertices;
|
|
186
219
|
const normalsArray = this._faceMesh.normals;
|
|
187
220
|
const vertices = [];
|
|
188
221
|
for (let i = 0; i < verticesArray.length; i += 3) {
|
|
189
|
-
vertices.push(
|
|
190
|
-
x: verticesArray[i],
|
|
191
|
-
y: verticesArray[i + 1],
|
|
192
|
-
z: verticesArray[i + 2],
|
|
193
|
-
});
|
|
222
|
+
vertices.push(convertFaceLocalVectorToProviderSpace(verticesArray[i], verticesArray[i + 1], verticesArray[i + 2]));
|
|
194
223
|
}
|
|
195
224
|
const normals = [];
|
|
196
225
|
for (let i = 0; i < normalsArray.length; i += 3) {
|
|
197
|
-
normals.push(
|
|
198
|
-
x: normalsArray[i],
|
|
199
|
-
y: normalsArray[i + 1],
|
|
200
|
-
z: normalsArray[i + 2],
|
|
201
|
-
});
|
|
226
|
+
normals.push(convertFaceLocalVectorToProviderSpace(normalsArray[i], normalsArray[i + 1], normalsArray[i + 2]));
|
|
202
227
|
}
|
|
203
228
|
const attachmentPoints = {};
|
|
204
229
|
for (const attachment of AttachmentList) {
|
|
205
230
|
const landmark = this._landmarks.get(attachment);
|
|
206
231
|
if (landmark) {
|
|
207
|
-
landmark.updateFromFaceAnchor(anchor,
|
|
232
|
+
landmark.updateFromFaceAnchor(anchor, mirrorPoses);
|
|
208
233
|
mat4.copy(this._scratchMatrix, landmark.pose);
|
|
209
234
|
mat4.getTranslation(this._scratchPosition, this._scratchMatrix);
|
|
210
235
|
attachmentPoints[attachment] = {
|
|
211
|
-
position:
|
|
212
|
-
x: this._scratchPosition[0],
|
|
213
|
-
y: this._scratchPosition[1],
|
|
214
|
-
z: this._scratchPosition[2],
|
|
215
|
-
},
|
|
236
|
+
position: convertFaceLocalVectorToProviderSpace(this._scratchPosition[0], this._scratchPosition[1], this._scratchPosition[2]),
|
|
216
237
|
};
|
|
217
238
|
}
|
|
218
239
|
else {
|
|
219
240
|
attachmentPoints[attachment] = {
|
|
220
|
-
position:
|
|
241
|
+
position: convertFaceLocalVectorToProviderSpace(0, 0, 0),
|
|
221
242
|
};
|
|
222
243
|
}
|
|
223
244
|
}
|
|
@@ -230,10 +251,10 @@ export class FaceTracking_Zappar extends TrackingMode {
|
|
|
230
251
|
transform: {
|
|
231
252
|
position: { ...anchorPosition },
|
|
232
253
|
rotation: {
|
|
233
|
-
x: this.
|
|
234
|
-
y: this.
|
|
235
|
-
z: this.
|
|
236
|
-
w: this.
|
|
254
|
+
x: this._providerRotation[0],
|
|
255
|
+
y: this._providerRotation[1],
|
|
256
|
+
z: this._providerRotation[2],
|
|
257
|
+
w: this._providerRotation[3],
|
|
237
258
|
},
|
|
238
259
|
scale,
|
|
239
260
|
scaledWidth: this._scratchScale[0],
|
|
@@ -250,6 +271,64 @@ export class FaceTracking_Zappar extends TrackingMode {
|
|
|
250
271
|
this.component.object.setPositionWorld(this._cameraPosition);
|
|
251
272
|
this.component.object.setRotationWorld(this._cameraRotation);
|
|
252
273
|
}
|
|
274
|
+
_setProjectionMatrixWithEngineRemap(matrix) {
|
|
275
|
+
const debugWindow = globalThis;
|
|
276
|
+
const view = this._view;
|
|
277
|
+
if (!view) {
|
|
278
|
+
if (debugWindow.__WLE_ZAPPAR_DEBUG__) {
|
|
279
|
+
debugWindow.__WLE_ZAPPAR_LAST_PROJECTION_REMAP__ = {
|
|
280
|
+
mode: 'face',
|
|
281
|
+
viewId: null,
|
|
282
|
+
reverseZ: false,
|
|
283
|
+
status: 'skipped',
|
|
284
|
+
reason: 'no-view',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (typeof view._setProjectionMatrix === 'function') {
|
|
290
|
+
view._setProjectionMatrix(matrix);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
view.projectionMatrix.set(matrix);
|
|
294
|
+
}
|
|
295
|
+
const engineAny = this.component.engine;
|
|
296
|
+
if (typeof view._id !== 'number') {
|
|
297
|
+
if (debugWindow.__WLE_ZAPPAR_DEBUG__) {
|
|
298
|
+
debugWindow.__WLE_ZAPPAR_LAST_PROJECTION_REMAP__ = {
|
|
299
|
+
mode: 'face',
|
|
300
|
+
viewId: null,
|
|
301
|
+
reverseZ: engineAny.isReverseZEnabled,
|
|
302
|
+
status: 'skipped',
|
|
303
|
+
reason: 'missing-view-id',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (typeof engineAny.wasm?._wl_view_component_remapProjectionMatrix ===
|
|
309
|
+
'function') {
|
|
310
|
+
const ndcDepthIsZeroToOne = false;
|
|
311
|
+
engineAny.wasm._wl_view_component_remapProjectionMatrix(view._id, engineAny.isReverseZEnabled, ndcDepthIsZeroToOne);
|
|
312
|
+
if (debugWindow.__WLE_ZAPPAR_DEBUG__) {
|
|
313
|
+
debugWindow.__WLE_ZAPPAR_LAST_PROJECTION_REMAP__ = {
|
|
314
|
+
mode: 'face',
|
|
315
|
+
viewId: view._id,
|
|
316
|
+
reverseZ: engineAny.isReverseZEnabled,
|
|
317
|
+
status: 'applied',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (debugWindow.__WLE_ZAPPAR_DEBUG__) {
|
|
323
|
+
debugWindow.__WLE_ZAPPAR_LAST_PROJECTION_REMAP__ = {
|
|
324
|
+
mode: 'face',
|
|
325
|
+
viewId: view._id,
|
|
326
|
+
reverseZ: engineAny.isReverseZEnabled,
|
|
327
|
+
status: 'skipped',
|
|
328
|
+
reason: 'missing-wasm-remap-function',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
253
332
|
_anchorNumericId(anchorId) {
|
|
254
333
|
let id = this._anchorNumericIds.get(anchorId);
|
|
255
334
|
if (id === undefined) {
|
|
@@ -23,6 +23,8 @@ export class ImageTracking_Zappar extends TrackingMode {
|
|
|
23
23
|
onImageLost = new Emitter();
|
|
24
24
|
init() {
|
|
25
25
|
this._view = this.component.object.getComponent('view') ?? undefined;
|
|
26
|
+
const provider = this.provider;
|
|
27
|
+
provider.setPreferredCameraUserFacing(false);
|
|
26
28
|
const input = this.component.object.getComponent('input');
|
|
27
29
|
if (input) {
|
|
28
30
|
input.active = false;
|
|
@@ -46,9 +48,11 @@ export class ImageTracking_Zappar extends TrackingMode {
|
|
|
46
48
|
return;
|
|
47
49
|
const provider = this.provider;
|
|
48
50
|
const pipeline = provider.getPipeline();
|
|
49
|
-
const
|
|
51
|
+
const viewNear = this._view?.near;
|
|
52
|
+
const viewFar = this._view?.far;
|
|
53
|
+
const projectionMatrix = Zappar.projectionMatrixFromCameraModel(pipeline.cameraModel(), this.component.engine.canvas.width, this.component.engine.canvas.height, typeof viewNear === 'number' ? viewNear : undefined, typeof viewFar === 'number' ? viewFar : undefined);
|
|
50
54
|
if (this._view) {
|
|
51
|
-
this.
|
|
55
|
+
this._setProjectionMatrixWithEngineRemap(projectionMatrix);
|
|
52
56
|
}
|
|
53
57
|
const cameraPose = pipeline.cameraPoseDefault();
|
|
54
58
|
this._applyCameraPose(cameraPose);
|
|
@@ -170,4 +174,61 @@ export class ImageTracking_Zappar extends TrackingMode {
|
|
|
170
174
|
this.component.object.setPositionWorld(this._cameraPosition);
|
|
171
175
|
this.component.object.setRotationWorld(this._cameraRotation);
|
|
172
176
|
}
|
|
177
|
+
_setProjectionMatrixWithEngineRemap(matrix) {
|
|
178
|
+
const debugWindow = globalThis;
|
|
179
|
+
const view = this._view;
|
|
180
|
+
if (!view) {
|
|
181
|
+
if (debugWindow.__WLE_ZAPPAR_DEBUG__) {
|
|
182
|
+
debugWindow.__WLE_ZAPPAR_LAST_PROJECTION_REMAP__ = {
|
|
183
|
+
mode: 'image',
|
|
184
|
+
viewId: null,
|
|
185
|
+
reverseZ: false,
|
|
186
|
+
status: 'skipped',
|
|
187
|
+
reason: 'no-view',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (typeof view._setProjectionMatrix === 'function') {
|
|
193
|
+
view._setProjectionMatrix(matrix);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
view.projectionMatrix.set(matrix);
|
|
197
|
+
}
|
|
198
|
+
const engineAny = this.component.engine;
|
|
199
|
+
if (typeof view._id !== 'number') {
|
|
200
|
+
if (debugWindow.__WLE_ZAPPAR_DEBUG__) {
|
|
201
|
+
debugWindow.__WLE_ZAPPAR_LAST_PROJECTION_REMAP__ = {
|
|
202
|
+
mode: 'image',
|
|
203
|
+
viewId: null,
|
|
204
|
+
reverseZ: engineAny.isReverseZEnabled,
|
|
205
|
+
status: 'skipped',
|
|
206
|
+
reason: 'missing-view-id',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (typeof engineAny.wasm?._wl_view_component_remapProjectionMatrix === 'function') {
|
|
212
|
+
const ndcDepthIsZeroToOne = false;
|
|
213
|
+
engineAny.wasm._wl_view_component_remapProjectionMatrix(view._id, engineAny.isReverseZEnabled, ndcDepthIsZeroToOne);
|
|
214
|
+
if (debugWindow.__WLE_ZAPPAR_DEBUG__) {
|
|
215
|
+
debugWindow.__WLE_ZAPPAR_LAST_PROJECTION_REMAP__ = {
|
|
216
|
+
mode: 'image',
|
|
217
|
+
viewId: view._id,
|
|
218
|
+
reverseZ: engineAny.isReverseZEnabled,
|
|
219
|
+
status: 'applied',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (debugWindow.__WLE_ZAPPAR_DEBUG__) {
|
|
225
|
+
debugWindow.__WLE_ZAPPAR_LAST_PROJECTION_REMAP__ = {
|
|
226
|
+
mode: 'image',
|
|
227
|
+
viewId: view._id,
|
|
228
|
+
reverseZ: engineAny.isReverseZEnabled,
|
|
229
|
+
status: 'skipped',
|
|
230
|
+
reason: 'missing-wasm-remap-function',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
173
234
|
}
|
|
@@ -10,6 +10,8 @@ export class WorldTracking_Zappar extends TrackingMode {
|
|
|
10
10
|
super(provider, component);
|
|
11
11
|
}
|
|
12
12
|
init() {
|
|
13
|
+
const provider = this.provider;
|
|
14
|
+
provider.setPreferredCameraUserFacing(false);
|
|
13
15
|
const input = this.component.object.getComponent('input');
|
|
14
16
|
if (input) {
|
|
15
17
|
// Camera pose will be driven by the AR tracking implementation.
|
|
@@ -37,6 +37,8 @@ export declare class ZapparProvider extends ARProvider {
|
|
|
37
37
|
private _faceMesh;
|
|
38
38
|
private _faceResourcesPromise;
|
|
39
39
|
private _imageTracker;
|
|
40
|
+
private _preferredCameraUserFacing;
|
|
41
|
+
private _cameraSourceUserFacing;
|
|
40
42
|
private _imageTargetDescriptors;
|
|
41
43
|
private readonly _imageTargetsChanged;
|
|
42
44
|
private cameraStarted;
|
|
@@ -66,14 +68,23 @@ export declare class ZapparProvider extends ARProvider {
|
|
|
66
68
|
private readonly _debugLastSampleAnchorPosition;
|
|
67
69
|
private _zapparDebugLogIntervalId;
|
|
68
70
|
private _preRenderErrorLogged;
|
|
71
|
+
private get isVerboseDebugLoggingEnabled();
|
|
69
72
|
static Name: string;
|
|
70
73
|
get name(): string;
|
|
71
74
|
get supportsInstantTracking(): boolean;
|
|
72
75
|
get onImageTargetsChanged(): Emitter<void[]>;
|
|
73
76
|
get xrSession(): XRSession | null;
|
|
77
|
+
/**
|
|
78
|
+
* Hint camera facing preference for the next/active session.
|
|
79
|
+
* - `true`: user/front camera
|
|
80
|
+
* - `false`: rear/back camera
|
|
81
|
+
* - `null`: Zappar default
|
|
82
|
+
*/
|
|
83
|
+
setPreferredCameraUserFacing(userFacing: boolean | null): void;
|
|
74
84
|
static registerTrackingProviderWithARSession(arSession: ARSession): ZapparProvider;
|
|
75
85
|
private constructor();
|
|
76
86
|
startSession(): Promise<void>;
|
|
87
|
+
private ensurePreRenderRegistered;
|
|
77
88
|
private startZapparDebugLogging;
|
|
78
89
|
private stopZapparDebugLogging;
|
|
79
90
|
private ensureZapparLoaded;
|
|
@@ -81,6 +92,7 @@ export declare class ZapparProvider extends ARProvider {
|
|
|
81
92
|
ensureZapparNamespace(): Promise<Awaited<ReturnType<typeof loadZappar>>>;
|
|
82
93
|
private configureCvWorkerIfNeeded;
|
|
83
94
|
private ensurePipeline;
|
|
95
|
+
private ensureCameraSourcePreference;
|
|
84
96
|
getPipeline(): Pipeline;
|
|
85
97
|
ensureFaceResources(): Promise<void>;
|
|
86
98
|
getFaceTracker(): FaceTracker;
|
|
@@ -24,6 +24,8 @@ export class ZapparProvider extends ARProvider {
|
|
|
24
24
|
_faceMesh = null;
|
|
25
25
|
_faceResourcesPromise = null;
|
|
26
26
|
_imageTracker = null;
|
|
27
|
+
_preferredCameraUserFacing = null;
|
|
28
|
+
_cameraSourceUserFacing = null;
|
|
27
29
|
_imageTargetDescriptors = [];
|
|
28
30
|
_imageTargetsChanged = new Emitter();
|
|
29
31
|
cameraStarted = false;
|
|
@@ -53,6 +55,14 @@ export class ZapparProvider extends ARProvider {
|
|
|
53
55
|
_debugLastSampleAnchorPosition = vec3.create();
|
|
54
56
|
_zapparDebugLogIntervalId = null;
|
|
55
57
|
_preRenderErrorLogged = false;
|
|
58
|
+
get isVerboseDebugLoggingEnabled() {
|
|
59
|
+
if (typeof window === 'undefined') {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const debugWindow = window;
|
|
63
|
+
return (debugWindow.__WLE_ZAPPAR_DEBUG__ === true ||
|
|
64
|
+
debugWindow.__ZAPPAR_WORKER_DEBUG__ === true);
|
|
65
|
+
}
|
|
56
66
|
static Name = 'Zappar';
|
|
57
67
|
get name() {
|
|
58
68
|
return ZapparProvider.Name;
|
|
@@ -66,6 +76,15 @@ export class ZapparProvider extends ARProvider {
|
|
|
66
76
|
get xrSession() {
|
|
67
77
|
return this._xrSession;
|
|
68
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Hint camera facing preference for the next/active session.
|
|
81
|
+
* - `true`: user/front camera
|
|
82
|
+
* - `false`: rear/back camera
|
|
83
|
+
* - `null`: Zappar default
|
|
84
|
+
*/
|
|
85
|
+
setPreferredCameraUserFacing(userFacing) {
|
|
86
|
+
this._preferredCameraUserFacing = userFacing;
|
|
87
|
+
}
|
|
69
88
|
static registerTrackingProviderWithARSession(arSession) {
|
|
70
89
|
const provider = new ZapparProvider(arSession.engine);
|
|
71
90
|
arSession.registerTrackingProvider(provider);
|
|
@@ -78,10 +97,9 @@ export class ZapparProvider extends ARProvider {
|
|
|
78
97
|
}
|
|
79
98
|
engine.onXRSessionStart.add((session) => {
|
|
80
99
|
this._xrSession = session;
|
|
81
|
-
this.onSessionStart.notify(this);
|
|
82
100
|
});
|
|
83
101
|
engine.onXRSessionEnd.add(() => {
|
|
84
|
-
this.
|
|
102
|
+
this._xrSession = null;
|
|
85
103
|
});
|
|
86
104
|
}
|
|
87
105
|
async startSession() {
|
|
@@ -93,8 +111,15 @@ export class ZapparProvider extends ARProvider {
|
|
|
93
111
|
await this.ensureZapparLoaded();
|
|
94
112
|
await this._zappar.loadedPromise();
|
|
95
113
|
this.ensurePipeline();
|
|
114
|
+
this.ensureCameraSourcePreference();
|
|
96
115
|
await this.ensureCameraRunning();
|
|
97
116
|
this.startZapparDebugLogging();
|
|
117
|
+
if (this._faceTracker) {
|
|
118
|
+
this._faceTracker.enabled = true;
|
|
119
|
+
}
|
|
120
|
+
if (this._imageTracker) {
|
|
121
|
+
this._imageTracker.enabled = true;
|
|
122
|
+
}
|
|
98
123
|
// For instant tracking providers, we should emit session start here.
|
|
99
124
|
// (Unlike WebXR, there is no XRSessionStart event.)
|
|
100
125
|
this.onSessionStart.notify(this);
|
|
@@ -102,9 +127,18 @@ export class ZapparProvider extends ARProvider {
|
|
|
102
127
|
this.instantTracker.enabled = true;
|
|
103
128
|
}
|
|
104
129
|
}
|
|
130
|
+
ensurePreRenderRegistered() {
|
|
131
|
+
if (!this.preRenderRegistered) {
|
|
132
|
+
this.engine.scene.onPreRender.add(this.onPreRender);
|
|
133
|
+
this.preRenderRegistered = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
105
136
|
startZapparDebugLogging() {
|
|
106
137
|
if (this._zapparDebugLogIntervalId !== null)
|
|
107
138
|
return;
|
|
139
|
+
if (!this.isVerboseDebugLoggingEnabled) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
108
142
|
const canAccessWindow = typeof window !== 'undefined' &&
|
|
109
143
|
typeof window.setInterval ===
|
|
110
144
|
'function';
|
|
@@ -157,9 +191,7 @@ export class ZapparProvider extends ARProvider {
|
|
|
157
191
|
// Hard-coded paths for local development
|
|
158
192
|
const workerUrl = './zappar-cv/zappar-cv.worker.js';
|
|
159
193
|
const wasmUrl = './zappar-cv/zappar-cv.wasm';
|
|
160
|
-
const debugWorker =
|
|
161
|
-
window
|
|
162
|
-
.__ZAPPAR_WORKER_DEBUG__;
|
|
194
|
+
const debugWorker = this.isVerboseDebugLoggingEnabled;
|
|
163
195
|
if (debugWorker) {
|
|
164
196
|
console.log('[ZapparProvider] configureCvWorkerIfNeeded()', {
|
|
165
197
|
workerUrl,
|
|
@@ -188,21 +220,23 @@ export class ZapparProvider extends ARProvider {
|
|
|
188
220
|
const resolvedWasmUrl = typeof window !== 'undefined'
|
|
189
221
|
? new URL(wasmUrl, window.location.href).toString()
|
|
190
222
|
: wasmUrl;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
223
|
+
if (debugWorker) {
|
|
224
|
+
console.log('[ZapparProvider] CV worker created', {
|
|
225
|
+
workerUrl,
|
|
226
|
+
resolvedWorkerUrl,
|
|
227
|
+
wasmUrl,
|
|
228
|
+
resolvedWasmUrl,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
197
231
|
let workerMessagesSeen = 0;
|
|
198
232
|
worker.addEventListener('message', (event) => {
|
|
199
233
|
workerMessagesSeen++;
|
|
200
|
-
if (workerMessagesSeen === 1) {
|
|
234
|
+
if (workerMessagesSeen === 1 && debugWorker) {
|
|
201
235
|
console.log('[ZapparProvider] CV worker first message received', event.data);
|
|
202
236
|
return;
|
|
203
237
|
}
|
|
204
238
|
// Keep a couple more messages for context, but avoid spamming.
|
|
205
|
-
if (workerMessagesSeen <= 5) {
|
|
239
|
+
if (debugWorker && workerMessagesSeen <= 5) {
|
|
206
240
|
console.log('[ZapparProvider] CV worker message', {
|
|
207
241
|
index: workerMessagesSeen,
|
|
208
242
|
data: event.data,
|
|
@@ -249,8 +283,10 @@ export class ZapparProvider extends ARProvider {
|
|
|
249
283
|
}
|
|
250
284
|
}
|
|
251
285
|
ensurePipeline() {
|
|
252
|
-
if (this.pipeline)
|
|
286
|
+
if (this.pipeline) {
|
|
287
|
+
this.ensurePreRenderRegistered();
|
|
253
288
|
return;
|
|
289
|
+
}
|
|
254
290
|
if (!this._zappar) {
|
|
255
291
|
throw new Error('Zappar is not loaded yet. Call startSession() first.');
|
|
256
292
|
}
|
|
@@ -270,15 +306,27 @@ export class ZapparProvider extends ARProvider {
|
|
|
270
306
|
globalThis.ZapparPipeline = pipeline;
|
|
271
307
|
}
|
|
272
308
|
if (typeof window !== 'undefined') {
|
|
273
|
-
|
|
309
|
+
if (this.isVerboseDebugLoggingEnabled) {
|
|
310
|
+
console.log('[ZapparProvider] Using device camera source');
|
|
311
|
+
}
|
|
274
312
|
}
|
|
275
|
-
|
|
276
|
-
this.cameraSource = new Zappar.CameraSource(pipeline, deviceId);
|
|
313
|
+
this.ensureCameraSourcePreference();
|
|
277
314
|
this.instantTracker = new Zappar.InstantWorldTracker(pipeline);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
315
|
+
this.ensurePreRenderRegistered();
|
|
316
|
+
}
|
|
317
|
+
ensureCameraSourcePreference() {
|
|
318
|
+
if (!this.pipeline || !this._zappar) {
|
|
319
|
+
return;
|
|
281
320
|
}
|
|
321
|
+
const desiredUserFacing = this._preferredCameraUserFacing;
|
|
322
|
+
if (this.cameraSource && this._cameraSourceUserFacing === desiredUserFacing) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const deviceId = desiredUserFacing === null
|
|
326
|
+
? this._zappar.cameraDefaultDeviceID()
|
|
327
|
+
: this._zappar.cameraDefaultDeviceID(desiredUserFacing);
|
|
328
|
+
this.cameraSource = new this._zappar.CameraSource(this.pipeline, deviceId);
|
|
329
|
+
this._cameraSourceUserFacing = desiredUserFacing;
|
|
282
330
|
}
|
|
283
331
|
getPipeline() {
|
|
284
332
|
if (!this.pipeline) {
|
|
@@ -477,15 +525,13 @@ export class ZapparProvider extends ARProvider {
|
|
|
477
525
|
if (previousUnpackBuffer)
|
|
478
526
|
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
|
|
479
527
|
try {
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
// - texture upload: cameraFrameUploadGL() is performed separately (cameraTexture.ts)
|
|
528
|
+
// Keep GL update order consistent with the non-GL path:
|
|
529
|
+
// process tracking input, upload camera frame texture, then update frame state.
|
|
483
530
|
this.pipeline.processGL();
|
|
531
|
+
this.pipeline.cameraFrameUploadGL();
|
|
484
532
|
this.pipeline.frameUpdate();
|
|
485
533
|
// Update SLAM state every frame (one loop in the provider).
|
|
486
534
|
this.updateTracking();
|
|
487
|
-
// Upload the latest camera frame to a WebGL texture (for background rendering).
|
|
488
|
-
this.pipeline.cameraFrameUploadGL();
|
|
489
535
|
// Bind the Zappar camera texture for Wonderland's sky material.
|
|
490
536
|
// Wonderland Engine will handle drawing; we only ensure that:
|
|
491
537
|
// - `videoTexture` sampler uniform points to the last texture unit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wonderlandengine/ar-provider-zappar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Wonderland Engine AR tracking provider based on Zappar",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"scripts/**/*.mjs"
|
|
33
33
|
],
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"prettier": "^
|
|
35
|
+
"prettier": "^3.5.3",
|
|
36
36
|
"typescript": "^4.9.5"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import {createRequire} from 'node:module';
|
|
4
|
-
import {fileURLToPath} from 'node:url';
|
|
5
|
-
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
|
-
|
|
10
|
-
const logPrefix = '[ar-provider-zappar:postinstall]';
|
|
11
|
-
|
|
12
|
-
function log(...args) {
|
|
13
|
-
// Keep output minimal but actionable.
|
|
14
|
-
console.log(logPrefix, ...args);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function warn(...args) {
|
|
18
|
-
console.warn(logPrefix, ...args);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function copyOrThrow(src, dest) {
|
|
22
|
-
fs.mkdirSync(path.dirname(dest), {recursive: true});
|
|
23
|
-
fs.copyFileSync(src, dest);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function safeLinkOrCopy(src, dest) {
|
|
27
|
-
fs.mkdirSync(path.dirname(dest), {recursive: true});
|
|
28
|
-
try {
|
|
29
|
-
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
|
30
|
-
fs.linkSync(src, dest);
|
|
31
|
-
} catch {
|
|
32
|
-
try {
|
|
33
|
-
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
|
34
|
-
} catch {
|
|
35
|
-
// ignore
|
|
36
|
-
}
|
|
37
|
-
fs.copyFileSync(src, dest);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function listFiles(dir) {
|
|
42
|
-
try {
|
|
43
|
-
return fs.readdirSync(dir, {withFileTypes: true});
|
|
44
|
-
} catch {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function findFirstFile(dir, predicate) {
|
|
50
|
-
for (const entry of listFiles(dir)) {
|
|
51
|
-
if (!entry.isFile()) continue;
|
|
52
|
-
if (predicate(entry.name)) return path.join(dir, entry.name);
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function findWasmFromWorker(workerPath, umdDir) {
|
|
58
|
-
try {
|
|
59
|
-
const src = fs.readFileSync(workerPath, 'utf8');
|
|
60
|
-
const match = src.match(/["']([^"']+\.wasm)["']/);
|
|
61
|
-
if (!match) return null;
|
|
62
|
-
|
|
63
|
-
const candidate = path.resolve(umdDir, match[1]);
|
|
64
|
-
const relative = path.relative(umdDir, candidate);
|
|
65
|
-
|
|
66
|
-
// Ensure we never resolve outside the umd directory.
|
|
67
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
68
|
-
|
|
69
|
-
return fs.existsSync(candidate) ? candidate : null;
|
|
70
|
-
} catch {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function resolveZapparRoot() {
|
|
76
|
-
const providerRoot = path.resolve(__dirname, '..');
|
|
77
|
-
const zapparPackageJson = require.resolve('@zappar/zappar/package.json', {
|
|
78
|
-
paths: [providerRoot],
|
|
79
|
-
});
|
|
80
|
-
return path.dirname(zapparPackageJson);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function resolveZapparCvRoot() {
|
|
84
|
-
const providerRoot = path.resolve(__dirname, '..');
|
|
85
|
-
const zapparCvPackageJson = require.resolve('@zappar/zappar-cv/package.json', {
|
|
86
|
-
paths: [providerRoot],
|
|
87
|
-
});
|
|
88
|
-
return path.dirname(zapparCvPackageJson);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function resolveProjectRoot() {
|
|
92
|
-
// INIT_CWD is set by npm/yarn/pnpm for lifecycle scripts and points at the
|
|
93
|
-
// consumer project that triggered the install.
|
|
94
|
-
const initCwd = process.env.INIT_CWD;
|
|
95
|
-
if (initCwd && initCwd.trim()) return path.resolve(initCwd);
|
|
96
|
-
|
|
97
|
-
// Fallback (best effort): current working directory.
|
|
98
|
-
return process.cwd();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function main() {
|
|
102
|
-
const zapparRoot = resolveZapparRoot();
|
|
103
|
-
const umdDir = path.join(zapparRoot, 'umd');
|
|
104
|
-
|
|
105
|
-
const zapparCvRoot = resolveZapparCvRoot();
|
|
106
|
-
const zapparCvLibDir = path.join(zapparCvRoot, 'lib');
|
|
107
|
-
|
|
108
|
-
const projectRoot = resolveProjectRoot();
|
|
109
|
-
const outDir = path.resolve(projectRoot, 'static', 'zappar-cv');
|
|
110
|
-
|
|
111
|
-
const workerSource =
|
|
112
|
-
findFirstFile(umdDir, (name) => name === 'zappar.worker.js') ??
|
|
113
|
-
findFirstFile(umdDir, (name) => name === 'zappar-cv.worker.js') ??
|
|
114
|
-
findFirstFile(zapparRoot, (name) => name === 'zappar.worker.js') ??
|
|
115
|
-
findFirstFile(zapparRoot, (name) => name === 'zappar-cv.worker.js') ??
|
|
116
|
-
null;
|
|
117
|
-
|
|
118
|
-
if (!workerSource) {
|
|
119
|
-
warn('Could not locate Zappar worker under', umdDir);
|
|
120
|
-
warn('Zappar root:', zapparRoot);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const wasmCandidates = listFiles(umdDir)
|
|
125
|
-
.filter((e) => e.isFile() && e.name.endsWith('.wasm'))
|
|
126
|
-
.map((e) => path.join(umdDir, e.name));
|
|
127
|
-
|
|
128
|
-
let wasmSource = wasmCandidates.length === 1 ? wasmCandidates[0] : null;
|
|
129
|
-
wasmSource = wasmSource ?? findWasmFromWorker(workerSource, umdDir);
|
|
130
|
-
wasmSource = wasmSource ?? (wasmCandidates.length > 0 ? wasmCandidates[0] : null);
|
|
131
|
-
|
|
132
|
-
if (!wasmSource) {
|
|
133
|
-
warn('Could not locate Zappar wasm under', umdDir);
|
|
134
|
-
warn('Worker found at:', workerSource);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
fs.mkdirSync(outDir, {recursive: true});
|
|
139
|
-
|
|
140
|
-
const workerDest = path.join(outDir, 'zappar-cv.worker.js');
|
|
141
|
-
copyOrThrow(workerSource, workerDest);
|
|
142
|
-
|
|
143
|
-
const wasmBasename = path.basename(wasmSource);
|
|
144
|
-
const wasmDest = path.join(outDir, wasmBasename);
|
|
145
|
-
copyOrThrow(wasmSource, wasmDest);
|
|
146
|
-
|
|
147
|
-
// Create a stable alias that the Wonderland provider code points at.
|
|
148
|
-
safeLinkOrCopy(wasmDest, path.join(outDir, 'zappar-cv.wasm'));
|
|
149
|
-
|
|
150
|
-
// Optional: copy any Zappar binary blobs shipped alongside the wasm.
|
|
151
|
-
// (Harmless if unused; avoids runtime 404s if Zappar expects them.)
|
|
152
|
-
for (const entry of listFiles(umdDir)) {
|
|
153
|
-
if (!entry.isFile()) continue;
|
|
154
|
-
if (!entry.name.endsWith('.zbin')) continue;
|
|
155
|
-
copyOrThrow(path.join(umdDir, entry.name), path.join(outDir, entry.name));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Required for face tracking defaults (FaceTracker.loadDefaultModel / FaceMesh.loadDefault*).
|
|
159
|
-
// These ship in @zappar/zappar-cv/lib but are fetched at runtime unless staged.
|
|
160
|
-
const requiredFaceModelFiles = [
|
|
161
|
-
'face_tracking_model.zbin',
|
|
162
|
-
'face_mesh_face_model.zbin',
|
|
163
|
-
];
|
|
164
|
-
|
|
165
|
-
for (const filename of requiredFaceModelFiles) {
|
|
166
|
-
const src = path.join(zapparCvLibDir, filename);
|
|
167
|
-
const dest = path.join(outDir, filename);
|
|
168
|
-
if (!fs.existsSync(src)) {
|
|
169
|
-
warn('Could not locate required Zappar face model:', filename);
|
|
170
|
-
warn('Looked under:', zapparCvLibDir);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
copyOrThrow(src, dest);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
log('Copied Zappar CV assets into', outDir);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
main();
|
|
181
|
-
} catch (e) {
|
|
182
|
-
// Postinstall should not hard-fail the whole install; consumers can still override.
|
|
183
|
-
warn('Failed to stage Zappar CV assets:', e);
|
|
184
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createRequire} from 'node:module';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const logPrefix = '[ar-provider-zappar:postinstall]';
|
|
11
|
+
|
|
12
|
+
function log(...args) {
|
|
13
|
+
// Keep output minimal but actionable.
|
|
14
|
+
console.log(logPrefix, ...args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function warn(...args) {
|
|
18
|
+
console.warn(logPrefix, ...args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function copyOrThrow(src, dest) {
|
|
22
|
+
fs.mkdirSync(path.dirname(dest), {recursive: true});
|
|
23
|
+
fs.copyFileSync(src, dest);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeLinkOrCopy(src, dest) {
|
|
27
|
+
fs.mkdirSync(path.dirname(dest), {recursive: true});
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
|
30
|
+
fs.linkSync(src, dest);
|
|
31
|
+
} catch {
|
|
32
|
+
try {
|
|
33
|
+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
|
34
|
+
} catch {
|
|
35
|
+
// ignore
|
|
36
|
+
}
|
|
37
|
+
fs.copyFileSync(src, dest);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listFiles(dir) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.readdirSync(dir, {withFileTypes: true});
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findFirstFile(dir, predicate) {
|
|
50
|
+
for (const entry of listFiles(dir)) {
|
|
51
|
+
if (!entry.isFile()) continue;
|
|
52
|
+
if (predicate(entry.name)) return path.join(dir, entry.name);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findWasmFromWorker(workerPath, umdDir) {
|
|
58
|
+
try {
|
|
59
|
+
const src = fs.readFileSync(workerPath, 'utf8');
|
|
60
|
+
const match = src.match(/["']([^"']+\.wasm)["']/);
|
|
61
|
+
if (!match) return null;
|
|
62
|
+
|
|
63
|
+
const candidate = path.resolve(umdDir, match[1]);
|
|
64
|
+
const relative = path.relative(umdDir, candidate);
|
|
65
|
+
|
|
66
|
+
// Ensure we never resolve outside the umd directory.
|
|
67
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
68
|
+
|
|
69
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveZapparRoot() {
|
|
76
|
+
const providerRoot = path.resolve(__dirname, '..');
|
|
77
|
+
const zapparPackageJson = require.resolve('@zappar/zappar/package.json', {
|
|
78
|
+
paths: [providerRoot],
|
|
79
|
+
});
|
|
80
|
+
return path.dirname(zapparPackageJson);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveZapparCvRoot() {
|
|
84
|
+
const providerRoot = path.resolve(__dirname, '..');
|
|
85
|
+
const zapparCvPackageJson = require.resolve('@zappar/zappar-cv/package.json', {
|
|
86
|
+
paths: [providerRoot],
|
|
87
|
+
});
|
|
88
|
+
return path.dirname(zapparCvPackageJson);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveProjectRoot() {
|
|
92
|
+
// INIT_CWD is set by npm/yarn/pnpm for lifecycle scripts and points at the
|
|
93
|
+
// consumer project that triggered the install.
|
|
94
|
+
const initCwd = process.env.INIT_CWD;
|
|
95
|
+
if (initCwd && initCwd.trim()) return path.resolve(initCwd);
|
|
96
|
+
|
|
97
|
+
// Fallback (best effort): current working directory.
|
|
98
|
+
return process.cwd();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function main() {
|
|
102
|
+
const zapparRoot = resolveZapparRoot();
|
|
103
|
+
const umdDir = path.join(zapparRoot, 'umd');
|
|
104
|
+
|
|
105
|
+
const zapparCvRoot = resolveZapparCvRoot();
|
|
106
|
+
const zapparCvLibDir = path.join(zapparCvRoot, 'lib');
|
|
107
|
+
|
|
108
|
+
const projectRoot = resolveProjectRoot();
|
|
109
|
+
const outDir = path.resolve(projectRoot, 'static', 'zappar-cv');
|
|
110
|
+
|
|
111
|
+
const workerSource =
|
|
112
|
+
findFirstFile(umdDir, (name) => name === 'zappar.worker.js') ??
|
|
113
|
+
findFirstFile(umdDir, (name) => name === 'zappar-cv.worker.js') ??
|
|
114
|
+
findFirstFile(zapparRoot, (name) => name === 'zappar.worker.js') ??
|
|
115
|
+
findFirstFile(zapparRoot, (name) => name === 'zappar-cv.worker.js') ??
|
|
116
|
+
null;
|
|
117
|
+
|
|
118
|
+
if (!workerSource) {
|
|
119
|
+
warn('Could not locate Zappar worker under', umdDir);
|
|
120
|
+
warn('Zappar root:', zapparRoot);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const wasmCandidates = listFiles(umdDir)
|
|
125
|
+
.filter((e) => e.isFile() && e.name.endsWith('.wasm'))
|
|
126
|
+
.map((e) => path.join(umdDir, e.name));
|
|
127
|
+
|
|
128
|
+
let wasmSource = wasmCandidates.length === 1 ? wasmCandidates[0] : null;
|
|
129
|
+
wasmSource = wasmSource ?? findWasmFromWorker(workerSource, umdDir);
|
|
130
|
+
wasmSource = wasmSource ?? (wasmCandidates.length > 0 ? wasmCandidates[0] : null);
|
|
131
|
+
|
|
132
|
+
if (!wasmSource) {
|
|
133
|
+
warn('Could not locate Zappar wasm under', umdDir);
|
|
134
|
+
warn('Worker found at:', workerSource);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fs.mkdirSync(outDir, {recursive: true});
|
|
139
|
+
|
|
140
|
+
const workerDest = path.join(outDir, 'zappar-cv.worker.js');
|
|
141
|
+
copyOrThrow(workerSource, workerDest);
|
|
142
|
+
|
|
143
|
+
const wasmBasename = path.basename(wasmSource);
|
|
144
|
+
const wasmDest = path.join(outDir, wasmBasename);
|
|
145
|
+
copyOrThrow(wasmSource, wasmDest);
|
|
146
|
+
|
|
147
|
+
// Create a stable alias that the Wonderland provider code points at.
|
|
148
|
+
safeLinkOrCopy(wasmDest, path.join(outDir, 'zappar-cv.wasm'));
|
|
149
|
+
|
|
150
|
+
// Optional: copy any Zappar binary blobs shipped alongside the wasm.
|
|
151
|
+
// (Harmless if unused; avoids runtime 404s if Zappar expects them.)
|
|
152
|
+
for (const entry of listFiles(umdDir)) {
|
|
153
|
+
if (!entry.isFile()) continue;
|
|
154
|
+
if (!entry.name.endsWith('.zbin')) continue;
|
|
155
|
+
copyOrThrow(path.join(umdDir, entry.name), path.join(outDir, entry.name));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Required for face tracking defaults (FaceTracker.loadDefaultModel / FaceMesh.loadDefault*).
|
|
159
|
+
// These ship in @zappar/zappar-cv/lib but are fetched at runtime unless staged.
|
|
160
|
+
const requiredFaceModelFiles = [
|
|
161
|
+
'face_tracking_model.zbin',
|
|
162
|
+
'face_mesh_face_model.zbin',
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
for (const filename of requiredFaceModelFiles) {
|
|
166
|
+
const src = path.join(zapparCvLibDir, filename);
|
|
167
|
+
const dest = path.join(outDir, filename);
|
|
168
|
+
if (!fs.existsSync(src)) {
|
|
169
|
+
warn('Could not locate required Zappar face model:', filename);
|
|
170
|
+
warn('Looked under:', zapparCvLibDir);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
copyOrThrow(src, dest);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
log('Copied Zappar CV assets into', outDir);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
main();
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Postinstall should not hard-fail the whole install; consumers can still override.
|
|
183
|
+
warn('Failed to stage Zappar CV assets:', e);
|
|
184
|
+
}
|