@smarterplan/ngx-smarterplan-core 1.4.9 → 1.4.10
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/esm2022/lib/matterport-extensions/security-camera/SecurityCamera.mjs +10 -4
- package/esm2022/lib/services/filter.service.mjs +19 -16
- package/esm2022/lib/services/matterport-import.service.mjs +179 -13
- package/esm2022/lib/services/matterport-navigation.service.mjs +85 -6
- package/esm2022/lib/services/matterport-tag.service.mjs +32 -17
- package/esm2022/lib/services/matterport.service.mjs +15 -9
- package/esm2022/lib/services/models/comment.service.mjs +6 -3
- package/esm2022/lib/services/models/equipment.service.mjs +51 -59
- package/esm2022/lib/services/models/ticket.service.mjs +33 -33
- package/esm2022/lib/services/navigator.service.mjs +4 -5
- package/esm2022/lib/services/viewer.service.mjs +11 -2
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs +431 -153
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs.map +1 -1
- package/lib/services/matterport-import.service.d.ts +10 -0
- package/lib/services/matterport-navigation.service.d.ts +30 -0
- package/lib/services/matterport.service.d.ts +4 -0
- package/lib/services/models/ticket.service.d.ts +1 -1
- package/lib/services/viewer.service.d.ts +12 -1
- package/package.json +1 -1
|
@@ -734,9 +734,15 @@ class SecurityCamera extends SceneComponent {
|
|
|
734
734
|
this.highlight = new THREE.Mesh(roomMesh.geometry, shader);
|
|
735
735
|
}
|
|
736
736
|
toggleViewFrustum() {
|
|
737
|
-
this.highlight
|
|
738
|
-
|
|
739
|
-
|
|
737
|
+
if (this.highlight) {
|
|
738
|
+
this.highlight.visible = !this.highlight.visible;
|
|
739
|
+
}
|
|
740
|
+
if (this.edges) {
|
|
741
|
+
this.edges.visible = !this.edges.visible;
|
|
742
|
+
}
|
|
743
|
+
if (this.pivot) {
|
|
744
|
+
this.pivot.visible = !this.pivot.visible;
|
|
745
|
+
}
|
|
740
746
|
}
|
|
741
747
|
makeAnimation() {
|
|
742
748
|
const THREE = this.context.three;
|
|
@@ -2227,7 +2233,67 @@ class MatterportNavigationService {
|
|
|
2227
2233
|
currentRooms = [];
|
|
2228
2234
|
/** Sequence number of the floor currently visible in sdk.Floor.current (null = single-floor or unknown). */
|
|
2229
2235
|
currentFloorSequence = null;
|
|
2236
|
+
// ── Bidirectional sid ↔ uuid lookup maps ──
|
|
2237
|
+
// sid = short runtime key used by Matterport SDK (collection keys, Sweep.moveTo, Camera.pose.sweep)
|
|
2238
|
+
// uuid = persistent identifier stored in the database (zone.sweepIDs, poi.matterportSweepID, startSweepID)
|
|
2239
|
+
sidToUuid = new Map();
|
|
2240
|
+
uuidToSid = new Map();
|
|
2230
2241
|
constructor() { }
|
|
2242
|
+
// ── Sweep ID translation helpers ──
|
|
2243
|
+
/**
|
|
2244
|
+
* Builds the bidirectional sid ↔ uuid maps from the sweep collection.
|
|
2245
|
+
* Must be called every time the sweep collection is updated.
|
|
2246
|
+
*/
|
|
2247
|
+
buildSweepIdMaps(collection) {
|
|
2248
|
+
this.sidToUuid.clear();
|
|
2249
|
+
this.uuidToSid.clear();
|
|
2250
|
+
if (!collection)
|
|
2251
|
+
return;
|
|
2252
|
+
for (const sid of Object.keys(collection)) {
|
|
2253
|
+
const sweepData = collection[sid];
|
|
2254
|
+
const uuid = sweepData?.uuid;
|
|
2255
|
+
if (uuid && uuid !== sid) {
|
|
2256
|
+
this.sidToUuid.set(sid, uuid);
|
|
2257
|
+
this.uuidToSid.set(uuid, sid);
|
|
2258
|
+
}
|
|
2259
|
+
// Also map sid→sid and uuid→uuid for pass-through cases
|
|
2260
|
+
this.sidToUuid.set(sid, uuid || sid);
|
|
2261
|
+
if (uuid) {
|
|
2262
|
+
this.uuidToSid.set(uuid, sid);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
if (isDevMode()) {
|
|
2266
|
+
console.debug(`[MatterportNavigation] Built sweep ID maps: ${this.sidToUuid.size} sids, ${this.uuidToSid.size} uuids`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Resolves any sweep identifier (sid or uuid) to the SDK runtime **sid**.
|
|
2271
|
+
* This is what sdk.Sweep.moveTo() and sdk.Sweep.disable() expect.
|
|
2272
|
+
* Falls back to the input if no mapping exists.
|
|
2273
|
+
*/
|
|
2274
|
+
resolveSweepSid(idOrUuid) {
|
|
2275
|
+
if (!idOrUuid)
|
|
2276
|
+
return idOrUuid;
|
|
2277
|
+
// If it's already a known sid (exists as a key in sweepCollection), return it
|
|
2278
|
+
if (this.sidToUuid.has(idOrUuid))
|
|
2279
|
+
return idOrUuid;
|
|
2280
|
+
// Otherwise try to map from uuid → sid
|
|
2281
|
+
return this.uuidToSid.get(idOrUuid) ?? idOrUuid;
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Resolves any sweep identifier (sid or uuid) to the persistent **uuid**.
|
|
2285
|
+
* This is what the database stores (zone.sweepIDs, poi.matterportSweepID).
|
|
2286
|
+
* Falls back to the input if no mapping exists.
|
|
2287
|
+
*/
|
|
2288
|
+
resolveSweepUuid(sidOrUuid) {
|
|
2289
|
+
if (!sidOrUuid)
|
|
2290
|
+
return sidOrUuid;
|
|
2291
|
+
// If it's already a known uuid, return it
|
|
2292
|
+
if (this.uuidToSid.has(sidOrUuid))
|
|
2293
|
+
return sidOrUuid;
|
|
2294
|
+
// Otherwise try to map from sid → uuid
|
|
2295
|
+
return this.sidToUuid.get(sidOrUuid) ?? sidOrUuid;
|
|
2296
|
+
}
|
|
2231
2297
|
async action_toolbox_floorplan(sdk) {
|
|
2232
2298
|
if (this.inTransitionMode || this.inTransitionSweep) {
|
|
2233
2299
|
console.log('viewer is in transition, cannot go floorplan');
|
|
@@ -2295,15 +2361,23 @@ class MatterportNavigationService {
|
|
|
2295
2361
|
console.warn('No matterport floor found to move to');
|
|
2296
2362
|
}
|
|
2297
2363
|
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Navigate to a sweep. The caller can pass either a sid or a uuid;
|
|
2366
|
+
* this method resolves it to the SDK sid before calling Sweep.moveTo().
|
|
2367
|
+
*/
|
|
2298
2368
|
async action_go_to_sweep(sdk, sweep, rotation) {
|
|
2299
|
-
|
|
2369
|
+
const resolvedSid = this.resolveSweepSid(sweep);
|
|
2370
|
+
if (this.forbiddenSweeps.includes(sweep) || this.forbiddenSweeps.includes(resolvedSid)) {
|
|
2300
2371
|
console.log('user is not allowed to go to this sweep');
|
|
2301
2372
|
return;
|
|
2302
2373
|
}
|
|
2374
|
+
if (isDevMode() && resolvedSid !== sweep) {
|
|
2375
|
+
console.debug(`[action_go_to_sweep] Resolved uuid "${sweep}" → sid "${resolvedSid}"`);
|
|
2376
|
+
}
|
|
2303
2377
|
setTimeout(async () => {
|
|
2304
2378
|
try {
|
|
2305
2379
|
this.inTransitionSweep = true;
|
|
2306
|
-
await sdk.Sweep.moveTo(
|
|
2380
|
+
await sdk.Sweep.moveTo(resolvedSid, {
|
|
2307
2381
|
transition: sdk.Camera.TransitionType.INSTANT,
|
|
2308
2382
|
transitionTime: 1500
|
|
2309
2383
|
});
|
|
@@ -2317,12 +2391,16 @@ class MatterportNavigationService {
|
|
|
2317
2391
|
}
|
|
2318
2392
|
}, 1000);
|
|
2319
2393
|
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Disable forbidden sweeps. Resolves each sweep ID to sid before calling SDK.
|
|
2396
|
+
*/
|
|
2320
2397
|
async removeForbiddenSweeps(sdk, forbiddenSweeps) {
|
|
2321
2398
|
this.forbiddenSweeps = [...forbiddenSweeps];
|
|
2322
2399
|
let removed = 0;
|
|
2323
2400
|
await Promise.all(forbiddenSweeps.map(async (sweep) => {
|
|
2401
|
+
const resolvedSid = this.resolveSweepSid(sweep);
|
|
2324
2402
|
try {
|
|
2325
|
-
await sdk.Sweep.disable(
|
|
2403
|
+
await sdk.Sweep.disable(resolvedSid);
|
|
2326
2404
|
removed += 1;
|
|
2327
2405
|
}
|
|
2328
2406
|
catch (error) {
|
|
@@ -2331,8 +2409,15 @@ class MatterportNavigationService {
|
|
|
2331
2409
|
}));
|
|
2332
2410
|
console.log('removed sweeps:', removed);
|
|
2333
2411
|
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Returns the current sweep as a **uuid** (for database matching).
|
|
2414
|
+
* The SDK's poseCamera.sweep is a sid; we translate it.
|
|
2415
|
+
*/
|
|
2334
2416
|
getCurrentSweep(poseCamera) {
|
|
2335
|
-
|
|
2417
|
+
const sid = poseCamera?.sweep || null;
|
|
2418
|
+
if (!sid)
|
|
2419
|
+
return null;
|
|
2420
|
+
return this.resolveSweepUuid(sid);
|
|
2336
2421
|
}
|
|
2337
2422
|
setCameraMode(mode) {
|
|
2338
2423
|
this.inTransitionSweep = false;
|
|
@@ -3450,8 +3535,12 @@ class MatterportTagService {
|
|
|
3450
3535
|
if (mattertagData.elementID === elementID) {
|
|
3451
3536
|
tagID = mattertagID;
|
|
3452
3537
|
const sweep = mattertagData.getSweepID();
|
|
3453
|
-
if (sweep && sweeps
|
|
3454
|
-
|
|
3538
|
+
if (sweep && sweeps) {
|
|
3539
|
+
// sweep is a uuid; sweeps array contains sids – check both formats
|
|
3540
|
+
const resolvedSid = this.navigationService.resolveSweepSid(sweep);
|
|
3541
|
+
if (sweeps.includes(sweep) || sweeps.includes(resolvedSid)) {
|
|
3542
|
+
sweepID = sweep;
|
|
3543
|
+
}
|
|
3455
3544
|
}
|
|
3456
3545
|
}
|
|
3457
3546
|
}
|
|
@@ -3516,23 +3605,34 @@ class MatterportTagService {
|
|
|
3516
3605
|
else if (sdk) {
|
|
3517
3606
|
const { comment, tagDescription } = this.tagService.getBillboardMediaToEmbed(object);
|
|
3518
3607
|
if (comment) {
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
label: object.title || 'Media',
|
|
3524
|
-
},
|
|
3525
|
-
});
|
|
3608
|
+
// 1) Register the media attachment by passing the URL string(s).
|
|
3609
|
+
// registerAttachment returns an array of attachment IDs.
|
|
3610
|
+
const [attachmentID] = await sdk.Tag.registerAttachment(comment.externalLink);
|
|
3611
|
+
// 2) Attach the registered media to the tag.
|
|
3526
3612
|
await sdk.Tag.attach(tagID, attachmentID);
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3613
|
+
// 3) Determine media type for the billboard (PHOTO or VIDEO).
|
|
3614
|
+
// Use a simple heuristic: YouTube/Vimeo -> VIDEO, otherwise PHOTO.
|
|
3615
|
+
// The SDK exposes MediaType on the Tag namespace.
|
|
3616
|
+
const isVideo = /youtube\.com|youtu\.be|vimeo\.com/i.test(comment.externalLink);
|
|
3617
|
+
const mediaType = isVideo ? sdk.Mattertag.MediaType.VIDEO : sdk.Mattertag.MediaType.PHOTO;
|
|
3618
|
+
// 4) Update the billboard text and media using editBillboard.
|
|
3619
|
+
// editBillboard accepts Partial<Tag.EditableProperties> such as label, description, media.
|
|
3620
|
+
await sdk.Tag.editBillboard(tagID, {
|
|
3621
|
+
label: object.title || 'Media',
|
|
3622
|
+
description: tagDescription || '',
|
|
3623
|
+
media: {
|
|
3624
|
+
type: mediaType,
|
|
3625
|
+
src: comment.externalLink,
|
|
3532
3626
|
},
|
|
3533
3627
|
});
|
|
3534
|
-
|
|
3535
|
-
this.tagsAttachments[tagID] =
|
|
3628
|
+
// 5) Keep a local reference
|
|
3629
|
+
this.tagsAttachments[tagID] = {
|
|
3630
|
+
attachmentID,
|
|
3631
|
+
title: object.title || 'Media',
|
|
3632
|
+
description: tagDescription || '',
|
|
3633
|
+
mediaSrc: comment.externalLink,
|
|
3634
|
+
mediaType,
|
|
3635
|
+
};
|
|
3536
3636
|
}
|
|
3537
3637
|
}
|
|
3538
3638
|
}
|
|
@@ -3862,13 +3962,16 @@ class MatterportService {
|
|
|
3862
3962
|
onCollectionUpdated: function subscr(collection) {
|
|
3863
3963
|
this.sweeps = Object.keys(collection);
|
|
3864
3964
|
this.sweepCollection = collection;
|
|
3965
|
+
// Rebuild sid ↔ uuid maps whenever the collection updates
|
|
3966
|
+
this.navigationService.buildSweepIdMaps(collection);
|
|
3865
3967
|
}.bind(this),
|
|
3866
3968
|
});
|
|
3867
|
-
// subscribe to current sweep
|
|
3969
|
+
// subscribe to current sweep – emit uuid (DB format) instead of raw sid
|
|
3868
3970
|
this.sdk.Sweep.current.subscribe(function subscr(currentSweep) {
|
|
3869
3971
|
if (currentSweep.sid === '')
|
|
3870
3972
|
return;
|
|
3871
|
-
this.
|
|
3973
|
+
const uuid = this.navigationService.resolveSweepUuid(currentSweep.sid);
|
|
3974
|
+
this.currentSweep.next(uuid);
|
|
3872
3975
|
}.bind(this));
|
|
3873
3976
|
// Subscribe to Floor data
|
|
3874
3977
|
this.sdk.Floor.data.subscribe({
|
|
@@ -3994,11 +4097,14 @@ class MatterportService {
|
|
|
3994
4097
|
});
|
|
3995
4098
|
});
|
|
3996
4099
|
}
|
|
4100
|
+
/**
|
|
4101
|
+
* @deprecated Use navigationService.resolveSweepUuid() instead.
|
|
4102
|
+
* Kept temporarily for backwards compatibility.
|
|
4103
|
+
*/
|
|
3997
4104
|
getSweepUUIDForSid(sid) {
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
return collection[sid].uuid ?? null;
|
|
4105
|
+
return this.navigationService.resolveSweepUuid(sid) !== sid
|
|
4106
|
+
? this.navigationService.resolveSweepUuid(sid)
|
|
4107
|
+
: null;
|
|
4002
4108
|
}
|
|
4003
4109
|
setLightingOff() {
|
|
4004
4110
|
this.noLightForObjects = true;
|
|
@@ -4374,7 +4480,7 @@ class MatterportService {
|
|
|
4374
4480
|
mattertagData.setPosition(JSON.parse(poi.coordinate));
|
|
4375
4481
|
mattertagData.setPoi(poi); // to keep custom tagIcon and opacity
|
|
4376
4482
|
}
|
|
4377
|
-
mattertagData.setSweepID(this.poseCamera.sweep);
|
|
4483
|
+
mattertagData.setSweepID(this.navigationService.resolveSweepUuid(this.poseCamera.sweep));
|
|
4378
4484
|
mattertagData.setRotation(this.poseCamera.rotation);
|
|
4379
4485
|
mattertagData.setObject(element, poiType);
|
|
4380
4486
|
this.mattertagToFollow = await this.addMattertagToViewer(mattertagData);
|
|
@@ -4387,7 +4493,7 @@ class MatterportService {
|
|
|
4387
4493
|
*/
|
|
4388
4494
|
async addMattertagWhenAdding(poiType) {
|
|
4389
4495
|
const mattertagData = new MattertagData(poiType);
|
|
4390
|
-
mattertagData.setSweepID(this.poseCamera.sweep);
|
|
4496
|
+
mattertagData.setSweepID(this.navigationService.resolveSweepUuid(this.poseCamera.sweep));
|
|
4391
4497
|
mattertagData.setRotation(this.poseCamera.rotation);
|
|
4392
4498
|
this.setInteractionMode(ViewerInteractions.ADDING);
|
|
4393
4499
|
await this.addCursorMattertag(mattertagData);
|
|
@@ -4746,7 +4852,8 @@ class ViewerService {
|
|
|
4746
4852
|
setMode(indexMode) {
|
|
4747
4853
|
this.viewerMode = indexMode;
|
|
4748
4854
|
}
|
|
4749
|
-
setTourUrl(
|
|
4855
|
+
setTourUrl(options) {
|
|
4856
|
+
const { model3d, showIconPlan = true, showIconDollhouse = true, showIconFloors = true, showQuickStart = true, showFullscreen = true, showGuidedTour = true, showHighlightReel = true, showDefurnish = true, showRoomMeasurements = true } = options;
|
|
4750
4857
|
const baseUrl = `/assets/bundle/showcase.html`;
|
|
4751
4858
|
const params = [];
|
|
4752
4859
|
// Modèle
|
|
@@ -4789,6 +4896,14 @@ class ViewerService {
|
|
|
4789
4896
|
if (!showFullscreen) {
|
|
4790
4897
|
params.push(`fs=0`);
|
|
4791
4898
|
}
|
|
4899
|
+
// Defurnish
|
|
4900
|
+
if (showDefurnish) {
|
|
4901
|
+
params.push(`ad=1`);
|
|
4902
|
+
}
|
|
4903
|
+
// Room Measurements
|
|
4904
|
+
if (showRoomMeasurements) {
|
|
4905
|
+
params.push(`measurements=1`);
|
|
4906
|
+
}
|
|
4792
4907
|
this.tourUrl = `${baseUrl}?${params.join("&")}`;
|
|
4793
4908
|
}
|
|
4794
4909
|
async clearAll() {
|
|
@@ -6187,26 +6302,29 @@ class FilterService {
|
|
|
6187
6302
|
zoneID = null, zone = null) {
|
|
6188
6303
|
const filteredObjects = [];
|
|
6189
6304
|
await Promise.all(objects.map(async (object) => {
|
|
6190
|
-
const
|
|
6191
|
-
if (
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6305
|
+
const pois = object.pois;
|
|
6306
|
+
if (pois) {
|
|
6307
|
+
const [poi] = pois.items;
|
|
6308
|
+
if (poi && zone) {
|
|
6309
|
+
if (zone.sweepIDs && zone.sweepIDs.includes(poi.matterportSweepID)) {
|
|
6310
|
+
if (this.poiService.poiIsVirtual(poi) && zone.layer && zone.layer.name === "FLOOR") {
|
|
6311
|
+
// we include in Floor zone only
|
|
6312
|
+
filteredObjects.push(object);
|
|
6313
|
+
}
|
|
6314
|
+
if (!this.poiService.poiIsVirtual(poi)) {
|
|
6315
|
+
filteredObjects.push(object);
|
|
6316
|
+
}
|
|
6196
6317
|
}
|
|
6197
|
-
|
|
6318
|
+
}
|
|
6319
|
+
if (poi && !zone && zoneID) {
|
|
6320
|
+
const zones = await this.zoneService.getZonesForObject(object);
|
|
6321
|
+
if (zones &&
|
|
6322
|
+
zones.some((zone_) => zone_.id === zoneID ||
|
|
6323
|
+
zone_.parentID === zoneID)) {
|
|
6198
6324
|
filteredObjects.push(object);
|
|
6199
6325
|
}
|
|
6200
6326
|
}
|
|
6201
6327
|
}
|
|
6202
|
-
if (poi && !zone && zoneID) {
|
|
6203
|
-
const zones = await this.zoneService.getZonesForObject(object);
|
|
6204
|
-
if (zones &&
|
|
6205
|
-
zones.some((zone_) => zone_.id === zoneID ||
|
|
6206
|
-
zone_.parentID === zoneID)) {
|
|
6207
|
-
filteredObjects.push(object);
|
|
6208
|
-
}
|
|
6209
|
-
}
|
|
6210
6328
|
}));
|
|
6211
6329
|
return filteredObjects;
|
|
6212
6330
|
}
|
|
@@ -7166,10 +7284,9 @@ class NavigatorService {
|
|
|
7166
7284
|
if (!this.currentSweep) {
|
|
7167
7285
|
return;
|
|
7168
7286
|
}
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
let zonesForSweep = this.zonesForUserForSpace?.filter((zone) => zone.sweepIDs && zone.sweepIDs.includes(sweepToMatch));
|
|
7287
|
+
// currentSweep is already a uuid (translated at the source in MatterportService),
|
|
7288
|
+
// so it matches directly against zone.sweepIDs which store uuids from the import.
|
|
7289
|
+
let zonesForSweep = this.zonesForUserForSpace?.filter((zone) => zone.sweepIDs && zone.sweepIDs.includes(this.currentSweep));
|
|
7173
7290
|
const audioZones = this.audioZonesForUserForSpace?.filter((zone) => zone.sweepIDs && zone.sweepIDs.includes(this.currentSweep));
|
|
7174
7291
|
this.audioZonesChange.next(audioZones);
|
|
7175
7292
|
this.currentAudioZones = audioZones;
|
|
@@ -7478,7 +7595,7 @@ class TicketsService extends BaseObjectService {
|
|
|
7478
7595
|
awsKinesisAnalytics; //AWS
|
|
7479
7596
|
isMuseumUser = false;
|
|
7480
7597
|
navSubscription;
|
|
7481
|
-
|
|
7598
|
+
filterSubscription;
|
|
7482
7599
|
floorsPerSpace = null;
|
|
7483
7600
|
selectedFloor = null;
|
|
7484
7601
|
destroy$ = new Subject();
|
|
@@ -7506,7 +7623,7 @@ class TicketsService extends BaseObjectService {
|
|
|
7506
7623
|
else {
|
|
7507
7624
|
if (this.navSubscription) {
|
|
7508
7625
|
this.navSubscription.unsubscribe();
|
|
7509
|
-
this.
|
|
7626
|
+
this.filterSubscription.unsubscribe();
|
|
7510
7627
|
}
|
|
7511
7628
|
}
|
|
7512
7629
|
});
|
|
@@ -7517,6 +7634,9 @@ class TicketsService extends BaseObjectService {
|
|
|
7517
7634
|
});
|
|
7518
7635
|
}
|
|
7519
7636
|
async initTickets() {
|
|
7637
|
+
if (this.userService.getSpModule() === SpModule.MUSEUM) {
|
|
7638
|
+
return;
|
|
7639
|
+
}
|
|
7520
7640
|
let ticketsFiltered = [];
|
|
7521
7641
|
if (this.currentSpaceID) {
|
|
7522
7642
|
this.updating.next(true);
|
|
@@ -7689,11 +7809,8 @@ class TicketsService extends BaseObjectService {
|
|
|
7689
7809
|
if (!ticket.ownerMissionID) {
|
|
7690
7810
|
ticket.ownerMissionID = this.userService.currentMission(ticket.spaceID).id;
|
|
7691
7811
|
}
|
|
7692
|
-
// TODO!!! filter missions by space of ticket
|
|
7693
7812
|
try {
|
|
7694
7813
|
const receivedTicket = await this.createTicket(ticket);
|
|
7695
|
-
// console.log('!!! Response from creating ticket :');
|
|
7696
|
-
// console.log(receivedTicket);
|
|
7697
7814
|
this.addEventToTicket(receivedTicket, {
|
|
7698
7815
|
title: 'Création du ticket',
|
|
7699
7816
|
description: `Ticket créé par -`,
|
|
@@ -7750,7 +7867,6 @@ class TicketsService extends BaseObjectService {
|
|
|
7750
7867
|
}
|
|
7751
7868
|
async updateStatusOfTicket(ticket, status) {
|
|
7752
7869
|
const currentStatus = ticket.status;
|
|
7753
|
-
const oldTicketStatus = ticket.status;
|
|
7754
7870
|
ticket.status = status;
|
|
7755
7871
|
if (currentStatus === status) {
|
|
7756
7872
|
return ticket;
|
|
@@ -7916,32 +8032,33 @@ class TicketsService extends BaseObjectService {
|
|
|
7916
8032
|
}
|
|
7917
8033
|
}
|
|
7918
8034
|
initSubscriptions() {
|
|
7919
|
-
if (
|
|
7920
|
-
|
|
7921
|
-
this.currentSpaceID = this.navigatorService.currentSpaceID;
|
|
7922
|
-
if (!this.currentSpaceID) {
|
|
7923
|
-
this.ticketTags.next(null);
|
|
7924
|
-
this.ticketsUpdated.next({ space: [], zone: null });
|
|
7925
|
-
this.ticketTypeFilter = null;
|
|
7926
|
-
this.zoneIDFilter = null;
|
|
7927
|
-
}
|
|
7928
|
-
else if (zone.id !== this.zoneIDFilter) {
|
|
7929
|
-
this.zoneIDFilter = zone.id;
|
|
7930
|
-
// console.log("going to init tickets from zone");
|
|
7931
|
-
this.initTickets().catch((e) => console.log(e.message));
|
|
7932
|
-
}
|
|
7933
|
-
});
|
|
7934
|
-
this.filetrSubscription = this.filterService.subscribeToDataFilterUpdate((dateRange) => {
|
|
7935
|
-
this.dateFilter = dateRange;
|
|
7936
|
-
this.initTickets().catch((e) => console.log(e.message));
|
|
7937
|
-
});
|
|
7938
|
-
this.dateFilter = this.filterService.currentDateFilter;
|
|
7939
|
-
this.updateDone.subscribe(() => {
|
|
7940
|
-
if (this.currentSpaceID) {
|
|
7941
|
-
this.initTickets().catch((e) => console.log(e.message));
|
|
7942
|
-
}
|
|
7943
|
-
});
|
|
8035
|
+
if (this.userService.getSpModule() === SpModule.MUSEUM) {
|
|
8036
|
+
return;
|
|
7944
8037
|
}
|
|
8038
|
+
this.navSubscription = this.zoneChangeService.zoneChange.subscribe((zone) => {
|
|
8039
|
+
this.currentSpaceID = this.navigatorService.currentSpaceID;
|
|
8040
|
+
if (!this.currentSpaceID) {
|
|
8041
|
+
this.ticketTags.next(null);
|
|
8042
|
+
this.ticketsUpdated.next({ space: [], zone: null });
|
|
8043
|
+
this.ticketTypeFilter = null;
|
|
8044
|
+
this.zoneIDFilter = null;
|
|
8045
|
+
}
|
|
8046
|
+
else if (zone.id !== this.zoneIDFilter) {
|
|
8047
|
+
this.zoneIDFilter = zone.id;
|
|
8048
|
+
// console.log("going to init tickets from zone");
|
|
8049
|
+
this.initTickets().catch((e) => console.log(e.message));
|
|
8050
|
+
}
|
|
8051
|
+
});
|
|
8052
|
+
this.filterSubscription = this.filterService.subscribeToDataFilterUpdate((dateRange) => {
|
|
8053
|
+
this.dateFilter = dateRange;
|
|
8054
|
+
this.initTickets().catch((e) => console.log(e.message));
|
|
8055
|
+
});
|
|
8056
|
+
this.dateFilter = this.filterService.currentDateFilter;
|
|
8057
|
+
this.updateDone.subscribe(() => {
|
|
8058
|
+
if (this.currentSpaceID) {
|
|
8059
|
+
this.initTickets().catch((e) => console.log(e.message));
|
|
8060
|
+
}
|
|
8061
|
+
});
|
|
7945
8062
|
}
|
|
7946
8063
|
unsubscribe() {
|
|
7947
8064
|
this.destroy$.next(true);
|
|
@@ -8335,7 +8452,7 @@ class EquipmentService extends BaseObjectService {
|
|
|
8335
8452
|
});
|
|
8336
8453
|
}
|
|
8337
8454
|
async initEquips() {
|
|
8338
|
-
if (!this.currentSpaceID) {
|
|
8455
|
+
if (!this.currentSpaceID || this.userService.getSpModule() === SpModule.MUSEUM) {
|
|
8339
8456
|
return;
|
|
8340
8457
|
}
|
|
8341
8458
|
this.updating.next(true);
|
|
@@ -8563,64 +8680,56 @@ class EquipmentService extends BaseObjectService {
|
|
|
8563
8680
|
}); // for lateral menu
|
|
8564
8681
|
}
|
|
8565
8682
|
initSubscriptions() {
|
|
8566
|
-
if (
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
}
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
// zone,
|
|
8589
|
-
// );
|
|
8590
|
-
this.updateEquipsForZone();
|
|
8591
|
-
}
|
|
8592
|
-
else {
|
|
8593
|
-
this.currentSpaceID = this.navigatorService.currentSpaceID;
|
|
8594
|
-
// console.log(
|
|
8595
|
-
// "going to init equips for zone for new space",
|
|
8596
|
-
// this.currentSpaceID,
|
|
8597
|
-
// );
|
|
8598
|
-
this.initEquips();
|
|
8599
|
-
}
|
|
8600
|
-
}
|
|
8601
|
-
});
|
|
8602
|
-
this.deleteObservable
|
|
8603
|
-
.pipe(takeUntil(this.destroy$))
|
|
8604
|
-
.subscribe((equipment) => {
|
|
8605
|
-
if (this.currentSpaceID) {
|
|
8606
|
-
this.updateDueToDelete(equipment);
|
|
8607
|
-
}
|
|
8608
|
-
});
|
|
8609
|
-
this.createObservable
|
|
8610
|
-
.pipe(takeUntil(this.destroy$))
|
|
8611
|
-
.subscribe((equipment) => {
|
|
8612
|
-
if (this.currentSpaceID) {
|
|
8613
|
-
this.updateDueToCreate(equipment);
|
|
8683
|
+
if (this.userService.getSpModule() === SpModule.MUSEUM) {
|
|
8684
|
+
return;
|
|
8685
|
+
}
|
|
8686
|
+
this.zoneChangeService.zoneChange
|
|
8687
|
+
.pipe(takeUntil(this.destroy$))
|
|
8688
|
+
.subscribe((zone) => {
|
|
8689
|
+
if (!zone || !this.navigatorService.currentSpaceID) {
|
|
8690
|
+
this.currentSpaceID = this.navigatorService.currentSpaceID;
|
|
8691
|
+
this.equipmentsTags.next(null);
|
|
8692
|
+
this.equipmentsUpdated.next({ space: [], zone: null });
|
|
8693
|
+
this.zoneIDFilter = null;
|
|
8694
|
+
this.currentEquipments = {
|
|
8695
|
+
space: [],
|
|
8696
|
+
zonesMap: new Map(),
|
|
8697
|
+
};
|
|
8698
|
+
return;
|
|
8699
|
+
}
|
|
8700
|
+
if (zone.id !== this.zoneIDFilter) {
|
|
8701
|
+
this.zoneIDFilter = zone.id;
|
|
8702
|
+
this.currentZone = zone;
|
|
8703
|
+
if (this.currentSpaceID === this.navigatorService.currentSpaceID) {
|
|
8704
|
+
this.updateEquipsForZone();
|
|
8614
8705
|
}
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
.subscribe((equipment) => {
|
|
8619
|
-
if (this.currentSpaceID) {
|
|
8620
|
-
this.updateDueToEquipUpdated(equipment);
|
|
8706
|
+
else {
|
|
8707
|
+
this.currentSpaceID = this.navigatorService.currentSpaceID;
|
|
8708
|
+
this.initEquips();
|
|
8621
8709
|
}
|
|
8622
|
-
}
|
|
8623
|
-
}
|
|
8710
|
+
}
|
|
8711
|
+
});
|
|
8712
|
+
this.deleteObservable
|
|
8713
|
+
.pipe(takeUntil(this.destroy$))
|
|
8714
|
+
.subscribe((equipment) => {
|
|
8715
|
+
if (this.currentSpaceID) {
|
|
8716
|
+
this.updateDueToDelete(equipment);
|
|
8717
|
+
}
|
|
8718
|
+
});
|
|
8719
|
+
this.createObservable
|
|
8720
|
+
.pipe(takeUntil(this.destroy$))
|
|
8721
|
+
.subscribe((equipment) => {
|
|
8722
|
+
if (this.currentSpaceID) {
|
|
8723
|
+
this.updateDueToCreate(equipment);
|
|
8724
|
+
}
|
|
8725
|
+
});
|
|
8726
|
+
this.updateObservable
|
|
8727
|
+
.pipe(takeUntil(this.destroy$))
|
|
8728
|
+
.subscribe((equipment) => {
|
|
8729
|
+
if (this.currentSpaceID) {
|
|
8730
|
+
this.updateDueToEquipUpdated(equipment);
|
|
8731
|
+
}
|
|
8732
|
+
});
|
|
8624
8733
|
}
|
|
8625
8734
|
unsubscribe() {
|
|
8626
8735
|
this.destroy$.next(true);
|
|
@@ -9632,8 +9741,11 @@ class CommentService {
|
|
|
9632
9741
|
if (comment.ticketID && !comment.ticket) {
|
|
9633
9742
|
comment.ticket = await this.API.__proto__.GetTicket(comment.ticketID);
|
|
9634
9743
|
}
|
|
9635
|
-
|
|
9636
|
-
|
|
9744
|
+
let values = comment.dimensions;
|
|
9745
|
+
let numberPoints = 0;
|
|
9746
|
+
if (values) {
|
|
9747
|
+
numberPoints = values.length + 1;
|
|
9748
|
+
}
|
|
9637
9749
|
const poi = await this.poiService.getPoiByElementId(commentID);
|
|
9638
9750
|
const measure = {
|
|
9639
9751
|
id: comment.id,
|
|
@@ -12546,7 +12658,7 @@ class MatterportImportService {
|
|
|
12546
12658
|
if (!iframe) {
|
|
12547
12659
|
throw new Error('Cannot create iframe');
|
|
12548
12660
|
}
|
|
12549
|
-
this.viewerService.setTourUrl(modelID);
|
|
12661
|
+
this.viewerService.setTourUrl({ model3d: modelID });
|
|
12550
12662
|
const url = this.viewerService.getTourUrl();
|
|
12551
12663
|
iframe.setAttribute('src', url);
|
|
12552
12664
|
iframe.allow = 'xr-spatial-tracking';
|
|
@@ -12680,20 +12792,47 @@ class MatterportImportService {
|
|
|
12680
12792
|
const newLocal = await this.zoneService.create(zoneInput);
|
|
12681
12793
|
return newLocal;
|
|
12682
12794
|
}
|
|
12795
|
+
// --- Main import function (complete) ---
|
|
12683
12796
|
async import360images(overrideExisting = true) {
|
|
12684
12797
|
console.log('Importing 360 images');
|
|
12685
12798
|
this.importingImages.next(true);
|
|
12686
12799
|
return new Promise(async (resolve) => {
|
|
12687
12800
|
const scans = Object.values(this.sweeps);
|
|
12688
|
-
const nmbScans =
|
|
12801
|
+
const nmbScans = scans.length;
|
|
12689
12802
|
this.totalSweepsCount.next(nmbScans);
|
|
12690
12803
|
const start = overrideExisting ? 0 : await this.getUploadedImageCount(this.modelID);
|
|
12691
12804
|
for (let index = start; index < nmbScans; index += 1) {
|
|
12692
|
-
if (
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12805
|
+
if (this.stop) {
|
|
12806
|
+
console.log('Abandoning import because it was cancelled');
|
|
12807
|
+
resolve(false);
|
|
12808
|
+
this.removeFrame();
|
|
12809
|
+
break;
|
|
12810
|
+
}
|
|
12811
|
+
const sweep = scans[index];
|
|
12812
|
+
console.log('Moving to sweep:', sweep);
|
|
12813
|
+
// Prefer legacy .id first, then uuid, then sid as last resort
|
|
12814
|
+
const moveId = sweep.id || sweep.uuid || sweep.sid;
|
|
12815
|
+
try {
|
|
12816
|
+
// Move to sweep (use the best available id)
|
|
12817
|
+
await this.sdk.Sweep.moveTo(moveId, {
|
|
12818
|
+
rotation: { x: 0, y: 0 },
|
|
12819
|
+
transition: this.sdk.Camera && this.sdk.Camera.TransitionType
|
|
12820
|
+
? this.sdk.Camera.TransitionType.INSTANT
|
|
12821
|
+
: undefined,
|
|
12822
|
+
transitionTime: 0,
|
|
12823
|
+
});
|
|
12824
|
+
// Capture with automatic retry (ensures renderer readiness each attempt)
|
|
12825
|
+
const img = await this.captureEquirectangularWithRetry({ width: 2048, height: 1024 }, { mattertags: false, sweeps: true }, {
|
|
12826
|
+
maxAttempts: 3,
|
|
12827
|
+
initialBackoffMs: 500,
|
|
12828
|
+
perAttemptTimeoutMs: 8000,
|
|
12829
|
+
sweepIdForReadyCheck: moveId,
|
|
12830
|
+
waitForSweepTimeoutMs: 12000
|
|
12831
|
+
});
|
|
12832
|
+
// Upload asynchronously (do not await to keep throughput)
|
|
12833
|
+
uploadBase64ImageWithRetry(img,
|
|
12834
|
+
// prefer using uuid for storage name if available, else fallback to moveId
|
|
12835
|
+
sweep.uuid || moveId, `visits/${this.modelID}/sweeps/`, 'sweep', 5).then(() => {
|
|
12697
12836
|
this.sweepProcessedCount.next(index);
|
|
12698
12837
|
if (index === nmbScans - 1) {
|
|
12699
12838
|
console.log('Import 360 done');
|
|
@@ -12705,17 +12844,156 @@ class MatterportImportService {
|
|
|
12705
12844
|
console.log("Error uploading scan : ", e);
|
|
12706
12845
|
});
|
|
12707
12846
|
}
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
this.
|
|
12712
|
-
|
|
12847
|
+
catch (err) {
|
|
12848
|
+
// capture failed after retries — log and continue to next sweep
|
|
12849
|
+
console.error(`Failed to capture sweep ${moveId} after retries:`, err);
|
|
12850
|
+
this.sweepProcessedCount.next(index);
|
|
12851
|
+
// continue to next sweep (change to `throw err` if you want to abort on first failure)
|
|
12852
|
+
continue;
|
|
12713
12853
|
}
|
|
12714
12854
|
}
|
|
12855
|
+
// If loop finishes without hitting the final resolve in upload promise:
|
|
12715
12856
|
resolve(true);
|
|
12716
12857
|
this.removeFrame();
|
|
12717
12858
|
});
|
|
12718
12859
|
}
|
|
12860
|
+
// --- capture with retry and backoff ---
|
|
12861
|
+
// options: renderer size; renderOptions: renderer flags; cfg: retry config
|
|
12862
|
+
async captureEquirectangularWithRetry(options, renderOptions, { maxAttempts = 3, initialBackoffMs = 500, perAttemptTimeoutMs = 8000, sweepIdForReadyCheck = null, waitForSweepTimeoutMs = 12000 } = {}) {
|
|
12863
|
+
let attempt = 0;
|
|
12864
|
+
let lastError = null;
|
|
12865
|
+
while (attempt < maxAttempts) {
|
|
12866
|
+
attempt += 1;
|
|
12867
|
+
try {
|
|
12868
|
+
// Ensure sweep is ready before each attempt if requested
|
|
12869
|
+
if (sweepIdForReadyCheck) {
|
|
12870
|
+
await this.waitForSweepReady(sweepIdForReadyCheck, waitForSweepTimeoutMs);
|
|
12871
|
+
// small safety delay to let renderer finalize
|
|
12872
|
+
await this.sleep(120);
|
|
12873
|
+
}
|
|
12874
|
+
// Race the takeEquirectangular call against a per-attempt timeout
|
|
12875
|
+
const img = await Promise.race([
|
|
12876
|
+
this.sdk.Renderer.takeEquirectangular(options, renderOptions),
|
|
12877
|
+
this.timeoutPromise(perAttemptTimeoutMs, `takeEquirectangular timed out after ${perAttemptTimeoutMs}ms`)
|
|
12878
|
+
]);
|
|
12879
|
+
// success
|
|
12880
|
+
return img;
|
|
12881
|
+
}
|
|
12882
|
+
catch (err) {
|
|
12883
|
+
lastError = err;
|
|
12884
|
+
console.warn(`takeEquirectangular attempt ${attempt} failed:`, err);
|
|
12885
|
+
if (attempt >= maxAttempts) {
|
|
12886
|
+
throw lastError;
|
|
12887
|
+
}
|
|
12888
|
+
// exponential backoff before next attempt
|
|
12889
|
+
const backoff = initialBackoffMs * Math.pow(2, attempt - 1);
|
|
12890
|
+
// add small jitter to reduce thundering herd
|
|
12891
|
+
const jitter = Math.floor(Math.random() * 100);
|
|
12892
|
+
await this.sleep(backoff + jitter);
|
|
12893
|
+
// loop to next attempt
|
|
12894
|
+
}
|
|
12895
|
+
}
|
|
12896
|
+
throw lastError || new Error('captureEquirectangularWithRetry failed without error');
|
|
12897
|
+
}
|
|
12898
|
+
// --- Wait until the SDK reports the target sweep as current ---
|
|
12899
|
+
// Tries: sdk.Sweep.current.waitUntil -> sdk.Sweep.current.subscribe -> polling getCurrent/value
|
|
12900
|
+
// This function matches targetId against multiple fields and prioritizes .id matching.
|
|
12901
|
+
waitForSweepReady(targetId, timeoutMs = 10000) {
|
|
12902
|
+
const sdk = this.sdk;
|
|
12903
|
+
// helper to match multiple id fields, prioritizing id first
|
|
12904
|
+
const matchesTarget = (data) => {
|
|
12905
|
+
if (!data)
|
|
12906
|
+
return false;
|
|
12907
|
+
// check .id first (legacy), then uuid, sid, uid
|
|
12908
|
+
const fields = [data.id, data.uuid, data.sid, data.uid];
|
|
12909
|
+
return fields.some(f => !!f && String(f) === String(targetId));
|
|
12910
|
+
};
|
|
12911
|
+
// 1) prefer waitUntil if available on the observable
|
|
12912
|
+
if (sdk.Sweep && sdk.Sweep.current && typeof sdk.Sweep.current.waitUntil === 'function') {
|
|
12913
|
+
return Promise.race([
|
|
12914
|
+
sdk.Sweep.current.waitUntil((data) => matchesTarget(data)),
|
|
12915
|
+
this.timeoutPromise(timeoutMs, `Timeout waiting for sweep ${targetId}`)
|
|
12916
|
+
]);
|
|
12917
|
+
}
|
|
12918
|
+
// 2) subscribe if available
|
|
12919
|
+
if (sdk.Sweep && sdk.Sweep.current && typeof sdk.Sweep.current.subscribe === 'function') {
|
|
12920
|
+
return new Promise((resolve, reject) => {
|
|
12921
|
+
const timer = setTimeout(() => {
|
|
12922
|
+
if (off)
|
|
12923
|
+
off();
|
|
12924
|
+
reject(new Error(`Timeout waiting for sweep ${targetId}`));
|
|
12925
|
+
}, timeoutMs);
|
|
12926
|
+
let off = null;
|
|
12927
|
+
try {
|
|
12928
|
+
// subscribe may return an unsubscribe function or an object with unsubscribe()
|
|
12929
|
+
const subResult = sdk.Sweep.current.subscribe((data) => {
|
|
12930
|
+
if (matchesTarget(data)) {
|
|
12931
|
+
clearTimeout(timer);
|
|
12932
|
+
try {
|
|
12933
|
+
if (typeof subResult === 'function')
|
|
12934
|
+
subResult();
|
|
12935
|
+
else if (subResult && typeof subResult.unsubscribe === 'function')
|
|
12936
|
+
subResult.unsubscribe();
|
|
12937
|
+
}
|
|
12938
|
+
catch (e) { /*ignore*/ }
|
|
12939
|
+
resolve();
|
|
12940
|
+
}
|
|
12941
|
+
});
|
|
12942
|
+
// if subscribe returned an unsubscribe function, keep it as off
|
|
12943
|
+
if (typeof subResult === 'function')
|
|
12944
|
+
off = subResult;
|
|
12945
|
+
else if (subResult && typeof subResult.unsubscribe === 'function')
|
|
12946
|
+
off = () => subResult.unsubscribe();
|
|
12947
|
+
}
|
|
12948
|
+
catch (e) {
|
|
12949
|
+
clearTimeout(timer);
|
|
12950
|
+
reject(new Error('sdk.Sweep.current.subscribe not usable'));
|
|
12951
|
+
}
|
|
12952
|
+
});
|
|
12953
|
+
}
|
|
12954
|
+
// 3) polling fallback: try getCurrent(), sdk.Sweep.current() function, or .value, else poll renderer readiness
|
|
12955
|
+
return new Promise(async (resolve, reject) => {
|
|
12956
|
+
const pollInterval = 200;
|
|
12957
|
+
const timer = setTimeout(() => {
|
|
12958
|
+
clearInterval(interval);
|
|
12959
|
+
reject(new Error(`Timeout waiting for sweep ${targetId}`));
|
|
12960
|
+
}, timeoutMs);
|
|
12961
|
+
const checkNow = async () => {
|
|
12962
|
+
try {
|
|
12963
|
+
let current = null;
|
|
12964
|
+
if (sdk.Sweep && typeof sdk.Sweep.getCurrent === 'function') {
|
|
12965
|
+
current = await sdk.Sweep.getCurrent();
|
|
12966
|
+
}
|
|
12967
|
+
else if (sdk.Sweep && typeof sdk.Sweep.current === 'function') {
|
|
12968
|
+
// some builds expose a function
|
|
12969
|
+
current = await sdk.Sweep.current();
|
|
12970
|
+
}
|
|
12971
|
+
else if (sdk.Sweep && sdk.Sweep.current && sdk.Sweep.current.value) {
|
|
12972
|
+
// some observables expose a .value
|
|
12973
|
+
current = sdk.Sweep.current.value;
|
|
12974
|
+
}
|
|
12975
|
+
if (matchesTarget(current)) {
|
|
12976
|
+
clearTimeout(timer);
|
|
12977
|
+
clearInterval(interval);
|
|
12978
|
+
resolve();
|
|
12979
|
+
}
|
|
12980
|
+
}
|
|
12981
|
+
catch (e) {
|
|
12982
|
+
// ignore and retry until timeout
|
|
12983
|
+
}
|
|
12984
|
+
};
|
|
12985
|
+
// initial immediate check
|
|
12986
|
+
await checkNow();
|
|
12987
|
+
const interval = setInterval(checkNow, pollInterval);
|
|
12988
|
+
});
|
|
12989
|
+
}
|
|
12990
|
+
// --- small helpers ---
|
|
12991
|
+
sleep(ms) {
|
|
12992
|
+
return new Promise(res => setTimeout(res, ms));
|
|
12993
|
+
}
|
|
12994
|
+
timeoutPromise(ms, message) {
|
|
12995
|
+
return new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
|
|
12996
|
+
}
|
|
12719
12997
|
async getUploadedImageCount(modelID) {
|
|
12720
12998
|
try {
|
|
12721
12999
|
const images = await listFilesInFolder(`visits/${modelID}/sweeps/`);
|