@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.
@@ -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.visible = !this.highlight.visible;
738
- this.edges.visible = !this.edges.visible;
739
- this.pivot.visible = !this.pivot.visible;
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
- if (this.forbiddenSweeps.includes(sweep)) {
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(sweep, {
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(sweep);
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
- return poseCamera?.sweep || null;
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 && sweeps.includes(sweep)) {
3454
- sweepID = sweep;
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
- const attachmentID = await sdk.Tag.registerAttachment({
3520
- type: 'media',
3521
- data: {
3522
- url: comment.externalLink,
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
- const billboardAttachmentID = await sdk.Tag.registerAttachment({
3528
- type: 'billboard',
3529
- data: {
3530
- title: object.title,
3531
- description: tagDescription,
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
- await sdk.Tag.attach(tagID, billboardAttachmentID);
3535
- this.tagsAttachments[tagID] = billboardAttachmentID;
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.currentSweep.next(currentSweep.sid);
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
- const collection = this.sweepCollection;
3999
- if (!collection || !collection[sid])
4000
- return null;
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(model3d, showIconPlan = true, showIconDollhouse = true, showIconFloors = true, showQuickStart = true, showFullscreen = true, showGuidedTour = true, showHighlightReel = true) {
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 [poi] = object.pois.items;
6191
- if (poi && zone) {
6192
- if (zone.sweepIDs && zone.sweepIDs.includes(poi.matterportSweepID)) {
6193
- if (this.poiService.poiIsVirtual(poi) && zone.layer && zone.layer.name === "FLOOR") {
6194
- // we include in Floor zone only
6195
- filteredObjects.push(object);
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
- if (!this.poiService.poiIsVirtual(poi)) {
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
- const uuid = this.matterportService.getSweepUUIDForSid(this.currentSweep);
7170
- console.log('uuid', uuid);
7171
- const sweepToMatch = uuid ?? this.currentSweep;
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
- filetrSubscription;
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.filetrSubscription.unsubscribe();
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 (!this.isMuseumUser) {
7920
- this.navSubscription = this.zoneChangeService.zoneChange.subscribe((zone) => {
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 (!this.isMuseumUser) {
8567
- this.zoneChangeService.zoneChange
8568
- .pipe(takeUntil(this.destroy$))
8569
- .subscribe((zone) => {
8570
- if (!zone || !this.navigatorService.currentSpaceID) {
8571
- this.currentSpaceID = this.navigatorService.currentSpaceID;
8572
- this.equipmentsTags.next(null);
8573
- this.equipmentsUpdated.next({ space: [], zone: null });
8574
- this.zoneIDFilter = null;
8575
- this.currentEquipments = {
8576
- space: [],
8577
- zonesMap: new Map(),
8578
- };
8579
- return;
8580
- }
8581
- if (zone.id !== this.zoneIDFilter) {
8582
- this.zoneIDFilter = zone.id;
8583
- this.currentZone = zone;
8584
- if (this.currentSpaceID === this.navigatorService.currentSpaceID) {
8585
- // same space, only zone update
8586
- // console.log(
8587
- // "going to update equips for zone (same space)",
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
- this.updateObservable
8617
- .pipe(takeUntil(this.destroy$))
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
- const values = comment.dimensions;
9636
- const numberPoints = values.length + 1;
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 = Object.keys(scans).length;
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 (!this.stop) {
12693
- await this.sdk.Sweep.moveTo(scans[index].uuid, { rotation: { x: 0, y: 0 }, transition: this.sdk.Camera.TransitionType.INSTANT, transitionTime: 0 });
12694
- const img = await this.sdk.Renderer.takeEquirectangular({ width: 2048, height: 1024 }, { mattertags: false, sweeps: true });
12695
- /**Upload on S3 are asynchronous, in order to no slow down the process*/
12696
- uploadBase64ImageWithRetry(img, scans[index].uuid, `visits/${this.modelID}/sweeps/`, 'sweep', 5).then((r) => {
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
- else {
12709
- console.log('Abandoning import because it was cancelled');
12710
- resolve(false);
12711
- this.removeFrame();
12712
- break;
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/`);