@smarterplan/ngx-smarterplan-core 1.4.11 → 1.4.13
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/services/matterport-import.service.mjs +2 -57
- package/esm2022/lib/services/matterport-navigation.service.mjs +131 -1
- package/esm2022/lib/services/matterport-tag-copy.service.mjs +865 -0
- package/esm2022/lib/services/matterport-tag.service.mjs +175 -54
- package/esm2022/lib/services/matterport.service.mjs +267 -39
- package/esm2022/lib/services/models/equipment.service.mjs +16 -1
- package/esm2022/lib/services/models/navigation.service.mjs +4 -1
- package/esm2022/lib/services/models/poi.service.mjs +86 -8
- package/esm2022/lib/services/models/ticket.service.mjs +3 -2
- package/esm2022/lib/services/s3.service.mjs +12 -1
- package/esm2022/lib/services/utils/matterport-transform.util.mjs +39 -0
- package/esm2022/lib/services/viewer.service.mjs +8 -1
- package/esm2022/lib/types.service.mjs +9 -1
- package/esm2022/public-api.mjs +4 -1
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs +1590 -156
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs.map +1 -1
- package/lib/services/matterport-navigation.service.d.ts +25 -0
- package/lib/services/matterport-tag-copy.service.d.ts +129 -0
- package/lib/services/matterport-tag.service.d.ts +61 -7
- package/lib/services/matterport.service.d.ts +81 -1
- package/lib/services/models/equipment.service.d.ts +8 -0
- package/lib/services/models/poi.service.d.ts +14 -0
- package/lib/services/s3.service.d.ts +8 -0
- package/lib/services/utils/matterport-transform.util.d.ts +21 -0
- package/lib/types.service.d.ts +18 -0
- package/package.json +1 -1
- package/public-api.d.ts +2 -0
|
@@ -9,8 +9,8 @@ import * as i1$1 from '@angular/router';
|
|
|
9
9
|
import { RouterModule } from '@angular/router';
|
|
10
10
|
import { Matrix4, Vector3, Color, Object3D, Euler, PerspectiveCamera, CylinderBufferGeometry, BoxGeometry, MeshBasicMaterial, NormalBlending, BackSide, Mesh, EdgesGeometry, AdditiveBlending, ShaderMaterial, AnimationMixer, VectorKeyframeTrack, InterpolateSmooth, AnimationClip, LoopOnce, Group, PlaneBufferGeometry, CanvasTexture, Texture, LinearFilter } from 'three';
|
|
11
11
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
12
|
+
import { Auth, API, graphqlOperation, Storage, Cache } from 'aws-amplify';
|
|
12
13
|
import { DateTime } from 'luxon';
|
|
13
|
-
import { Auth, Storage, API, Cache } from 'aws-amplify';
|
|
14
14
|
import * as i11 from '@angular/common';
|
|
15
15
|
import { CommonModule } from '@angular/common';
|
|
16
16
|
import * as FileSaver from 'file-saver';
|
|
@@ -426,6 +426,14 @@ var SpModule;
|
|
|
426
426
|
SpModule["HOTEL"] = "HOTEL";
|
|
427
427
|
SpModule["IMMO"] = "IMMO";
|
|
428
428
|
})(SpModule || (SpModule = {}));
|
|
429
|
+
var MatterportViewType;
|
|
430
|
+
(function (MatterportViewType) {
|
|
431
|
+
MatterportViewType["BASE"] = "viewtype.base";
|
|
432
|
+
MatterportViewType["DEFURNISH"] = "viewtype.defurnish";
|
|
433
|
+
MatterportViewType["LAYERED_BASE"] = "viewtype.layeredbase";
|
|
434
|
+
MatterportViewType["OTHER"] = "viewtype.other";
|
|
435
|
+
MatterportViewType["USER"] = "viewtype.user";
|
|
436
|
+
})(MatterportViewType || (MatterportViewType = {}));
|
|
429
437
|
|
|
430
438
|
/**
|
|
431
439
|
* The base of all `SceneComponent` created with the sdk.
|
|
@@ -1901,14 +1909,14 @@ class PoiService {
|
|
|
1901
1909
|
});
|
|
1902
1910
|
}
|
|
1903
1911
|
async getPoiByElementId(elementId) {
|
|
1904
|
-
const pois = await this.API.
|
|
1912
|
+
const pois = await this.API.PoisByElement(elementId);
|
|
1905
1913
|
if (pois && pois.items.length > 0) {
|
|
1906
1914
|
return pois.items[0];
|
|
1907
1915
|
}
|
|
1908
1916
|
return null;
|
|
1909
1917
|
}
|
|
1910
1918
|
async getPoisByType(type) {
|
|
1911
|
-
return this.API.
|
|
1919
|
+
return this.API.PoisByType(type).then((pois) => pois.items);
|
|
1912
1920
|
}
|
|
1913
1921
|
async createPOIForSpaceID(poi) {
|
|
1914
1922
|
removeNullKeysFromObject(poi);
|
|
@@ -1930,23 +1938,100 @@ class PoiService {
|
|
|
1930
1938
|
// partitionKey: "",
|
|
1931
1939
|
}, 'AWSKinesis');
|
|
1932
1940
|
}
|
|
1933
|
-
return this.API.
|
|
1941
|
+
return this.API.CreatePOI(poi);
|
|
1934
1942
|
}
|
|
1935
1943
|
poiIsVirtual(poi) {
|
|
1936
1944
|
const { coordinate } = poi;
|
|
1937
1945
|
return !coordinate;
|
|
1938
1946
|
}
|
|
1939
1947
|
async updatePoi(poi) {
|
|
1940
|
-
return this.API.
|
|
1948
|
+
return this.API.UpdatePOI(poi);
|
|
1941
1949
|
}
|
|
1942
1950
|
async deletePoi(poiID) {
|
|
1943
|
-
return this.API.
|
|
1951
|
+
return this.API.DeletePOI({ id: poiID });
|
|
1944
1952
|
}
|
|
1945
1953
|
async getPoisByMatterportSweepByType(sweep, type) {
|
|
1946
|
-
return this.API.
|
|
1947
|
-
.ListPOIs({ matterportSweepID: { eq: sweep } }, 300)
|
|
1954
|
+
return this.API.ListPOIS({ matterportSweepID: { eq: sweep } }, 300)
|
|
1948
1955
|
.then((response) => response.items.filter((poi) => poi.type === type));
|
|
1949
1956
|
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Optimized version: fetches POIs for MANY sweeps in as few GraphQL calls as possible,
|
|
1959
|
+
* using an `or: [{ matterportSweepID: { eq } }, ...]` filter instead of looping one
|
|
1960
|
+
* request per sweep. This is a DynamoDB Scan with a server-side filter (not a GSI
|
|
1961
|
+
* hash-key query), so `or` composition works and collapses N sweep-by-sweep calls
|
|
1962
|
+
* into a handful of paginated calls total.
|
|
1963
|
+
*
|
|
1964
|
+
* NOTE: `in` is NOT supported on this schema's ModelStringInput (older Amplify codegen),
|
|
1965
|
+
* hence the `or` composition instead — functionally equivalent for this use case.
|
|
1966
|
+
*
|
|
1967
|
+
* Chunked because DynamoDB/AppSync filter expressions have a practical size ceiling —
|
|
1968
|
+
* splitting keeps each request well within safe limits regardless of space size.
|
|
1969
|
+
*/
|
|
1970
|
+
async getPoisByMatterportSweeps(sweeps) {
|
|
1971
|
+
if (!sweeps || sweeps.length === 0) {
|
|
1972
|
+
return [];
|
|
1973
|
+
}
|
|
1974
|
+
const queryStatement = `
|
|
1975
|
+
query ListPOISLight($filter: ModelPOIFilterInput, $limit: Int, $nextToken: String) {
|
|
1976
|
+
listPOIS(filter: $filter, limit: $limit, nextToken: $nextToken) {
|
|
1977
|
+
items {
|
|
1978
|
+
id
|
|
1979
|
+
coordinate
|
|
1980
|
+
specific
|
|
1981
|
+
private
|
|
1982
|
+
global
|
|
1983
|
+
type
|
|
1984
|
+
elementID
|
|
1985
|
+
level
|
|
1986
|
+
role
|
|
1987
|
+
category
|
|
1988
|
+
domainID
|
|
1989
|
+
metadata
|
|
1990
|
+
tagIcon
|
|
1991
|
+
tagDescription
|
|
1992
|
+
tagImageID
|
|
1993
|
+
matterportSweepID
|
|
1994
|
+
createdAt
|
|
1995
|
+
updatedAt
|
|
1996
|
+
}
|
|
1997
|
+
nextToken
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
`;
|
|
2001
|
+
const CHUNK_SIZE = 50; // safe margin for an "or" filter expression's practical size
|
|
2002
|
+
const chunks = [];
|
|
2003
|
+
for (let i = 0; i < sweeps.length; i += CHUNK_SIZE) {
|
|
2004
|
+
chunks.push(sweeps.slice(i, i + CHUNK_SIZE));
|
|
2005
|
+
}
|
|
2006
|
+
let allPois = [];
|
|
2007
|
+
for (const chunk of chunks) {
|
|
2008
|
+
let nextToken = null;
|
|
2009
|
+
do {
|
|
2010
|
+
const variables = {
|
|
2011
|
+
filter: {
|
|
2012
|
+
or: chunk.map((sweep) => ({ matterportSweepID: { eq: sweep } }))
|
|
2013
|
+
},
|
|
2014
|
+
limit: 1000,
|
|
2015
|
+
nextToken
|
|
2016
|
+
};
|
|
2017
|
+
try {
|
|
2018
|
+
const response = (await API.graphql(graphqlOperation(queryStatement, variables)));
|
|
2019
|
+
const resData = response.data.listPOIS;
|
|
2020
|
+
if (resData && resData.items.length > 0) {
|
|
2021
|
+
allPois = [...allPois, ...resData.items];
|
|
2022
|
+
}
|
|
2023
|
+
nextToken = resData?.nextToken ?? null;
|
|
2024
|
+
}
|
|
2025
|
+
catch (error) {
|
|
2026
|
+
// A failed page shouldn't abort the whole batch — log and stop paging this chunk only,
|
|
2027
|
+
// the rest of the copy process continues with whatever was already collected.
|
|
2028
|
+
console.error(`Error while fetching POIs for a sweep chunk:`, error);
|
|
2029
|
+
nextToken = null;
|
|
2030
|
+
}
|
|
2031
|
+
} while (nextToken);
|
|
2032
|
+
}
|
|
2033
|
+
return allPois;
|
|
2034
|
+
}
|
|
1950
2035
|
unsubscribe() {
|
|
1951
2036
|
this.destroy$.next(true);
|
|
1952
2037
|
this.destroy$.complete();
|
|
@@ -2367,6 +2452,9 @@ class MatterportNavigationService {
|
|
|
2367
2452
|
else {
|
|
2368
2453
|
console.warn('No matterport floor found to move to');
|
|
2369
2454
|
}
|
|
2455
|
+
console.log("action_go_to_floor.floorMatterport", floorMatterport);
|
|
2456
|
+
console.log("action_go_to_floor.floorName", floorName);
|
|
2457
|
+
console.log("action_go_to_floor.matterportFloorSequence", matterportFloorSequence);
|
|
2370
2458
|
}
|
|
2371
2459
|
/**
|
|
2372
2460
|
* Navigate to a sweep. The caller can pass either a sid or a uuid;
|
|
@@ -2397,6 +2485,9 @@ class MatterportNavigationService {
|
|
|
2397
2485
|
await sdk.Camera.setRotation(rotation, { speed: 100 });
|
|
2398
2486
|
}
|
|
2399
2487
|
}, 1000);
|
|
2488
|
+
console.log("action_go_to_sweep.sweep", sweep);
|
|
2489
|
+
console.log("action_go_to_sweep.rotation", rotation);
|
|
2490
|
+
console.log("action_go_to_sweep.resolvedSid", resolvedSid);
|
|
2400
2491
|
}
|
|
2401
2492
|
/**
|
|
2402
2493
|
* Disable forbidden sweeps. Resolves each sweep ID to sid before calling SDK.
|
|
@@ -2416,6 +2507,130 @@ class MatterportNavigationService {
|
|
|
2416
2507
|
}));
|
|
2417
2508
|
console.log('removed sweeps:', removed);
|
|
2418
2509
|
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Move to a position
|
|
2512
|
+
* @param sdk
|
|
2513
|
+
* @param position
|
|
2514
|
+
* @param rotation
|
|
2515
|
+
* @param options
|
|
2516
|
+
*/
|
|
2517
|
+
async move_to_position(sdk, position, rotation, options) {
|
|
2518
|
+
// Validate SDK
|
|
2519
|
+
if (!sdk) {
|
|
2520
|
+
throw new Error('Matterport SDK instance is required');
|
|
2521
|
+
}
|
|
2522
|
+
// Merge options with defaults
|
|
2523
|
+
const { transition = 'FLY', durationMs = 800, anglesInDegrees = true, offset = { x: 0, y: 0, z: 0 } } = options || {};
|
|
2524
|
+
const degToRad = (d) => d * Math.PI / 180;
|
|
2525
|
+
// Prepare rotation values (default to 0 when not provided)
|
|
2526
|
+
const yaw = rotation?.yaw ?? 0;
|
|
2527
|
+
const pitch = rotation?.pitch ?? 0;
|
|
2528
|
+
const roll = rotation?.roll ?? 0;
|
|
2529
|
+
// Convert to radians if angles are provided in degrees
|
|
2530
|
+
const rotYaw = anglesInDegrees ? degToRad(yaw) : yaw;
|
|
2531
|
+
const rotPitch = anglesInDegrees ? degToRad(pitch) : pitch;
|
|
2532
|
+
const rotRoll = anglesInDegrees ? degToRad(roll) : roll;
|
|
2533
|
+
// Preserve previous transition flag and mark as in-transition
|
|
2534
|
+
const prevInTransition = this.inTransitionMode;
|
|
2535
|
+
this.inTransitionMode = true;
|
|
2536
|
+
try {
|
|
2537
|
+
// Attempt to use Camera.lookAt when available
|
|
2538
|
+
if (sdk?.Camera?.lookAt) {
|
|
2539
|
+
const lookAtOpts = {};
|
|
2540
|
+
// Prefer SDK constants when available, otherwise use fallback string
|
|
2541
|
+
if (sdk.Camera.TransitionType && sdk.Camera.TransitionType[transition]) {
|
|
2542
|
+
lookAtOpts.transition = sdk.Camera.TransitionType[transition];
|
|
2543
|
+
}
|
|
2544
|
+
else {
|
|
2545
|
+
lookAtOpts.transition = transition;
|
|
2546
|
+
}
|
|
2547
|
+
lookAtOpts.duration = durationMs;
|
|
2548
|
+
if (offset)
|
|
2549
|
+
lookAtOpts.offset = offset;
|
|
2550
|
+
try {
|
|
2551
|
+
await sdk.Camera.lookAt(position, lookAtOpts);
|
|
2552
|
+
}
|
|
2553
|
+
catch (err) {
|
|
2554
|
+
// If lookAt fails, continue to fallbacks below
|
|
2555
|
+
if (isDevMode())
|
|
2556
|
+
console.debug('Camera.lookAt failed, falling back to setState', err);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
// If lookAt is not present or failed, try setState for position
|
|
2560
|
+
if (!sdk?.Camera?.lookAt && sdk?.Camera?.setState) {
|
|
2561
|
+
const statePos = {
|
|
2562
|
+
position: { x: position.x, y: position.y, z: position.z }
|
|
2563
|
+
};
|
|
2564
|
+
try {
|
|
2565
|
+
await sdk.Camera.setState(statePos);
|
|
2566
|
+
}
|
|
2567
|
+
catch (err) {
|
|
2568
|
+
if (isDevMode())
|
|
2569
|
+
console.debug('Camera.setState(position) failed', err);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
// Apply rotation if provided
|
|
2573
|
+
if (rotation) {
|
|
2574
|
+
// Prefer setRotation when available
|
|
2575
|
+
if (sdk?.Camera?.setRotation) {
|
|
2576
|
+
// Try Vector2 signature { x: yaw, y: pitch } first
|
|
2577
|
+
try {
|
|
2578
|
+
await sdk.Camera.setRotation({ x: rotYaw, y: rotPitch }, { speed: Math.max(10, Math.floor(durationMs / 10)) });
|
|
2579
|
+
}
|
|
2580
|
+
catch (err1) {
|
|
2581
|
+
// If that fails, try Vector3 signature { x: pitch, y: yaw, z: roll }
|
|
2582
|
+
try {
|
|
2583
|
+
await sdk.Camera.setRotation({ x: rotPitch, y: rotYaw, z: rotRoll }, { speed: Math.max(10, Math.floor(durationMs / 10)) });
|
|
2584
|
+
}
|
|
2585
|
+
catch (err2) {
|
|
2586
|
+
if (isDevMode())
|
|
2587
|
+
console.debug('Camera.setRotation failed with multiple signatures', err1, err2);
|
|
2588
|
+
// Fallback to setState with rotation if available
|
|
2589
|
+
if (sdk?.Camera?.setState) {
|
|
2590
|
+
try {
|
|
2591
|
+
await sdk.Camera.setState({
|
|
2592
|
+
rotation: { x: rotPitch, y: rotYaw, z: rotRoll },
|
|
2593
|
+
position: { x: position.x, y: position.y, z: position.z }
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
catch (err3) {
|
|
2597
|
+
if (isDevMode())
|
|
2598
|
+
console.debug('Fallback Camera.setState(rotation) failed', err3);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
else if (sdk?.Camera?.setState) {
|
|
2605
|
+
// If setRotation is not available, use setState to apply rotation
|
|
2606
|
+
try {
|
|
2607
|
+
await sdk.Camera.setState({
|
|
2608
|
+
rotation: { x: rotPitch, y: rotYaw, z: rotRoll },
|
|
2609
|
+
position: { x: position.x, y: position.y, z: position.z }
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
catch (err) {
|
|
2613
|
+
if (isDevMode())
|
|
2614
|
+
console.debug('Camera.setState(rotation) failed', err);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
else {
|
|
2618
|
+
if (isDevMode())
|
|
2619
|
+
console.warn('No method available to apply rotation (setRotation/setState not found)');
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
// Wait a bit to allow transition animations to complete
|
|
2623
|
+
await wait(durationMs + 120);
|
|
2624
|
+
}
|
|
2625
|
+
catch (err) {
|
|
2626
|
+
// Re-throw after ensuring flags are restored in finally
|
|
2627
|
+
throw err;
|
|
2628
|
+
}
|
|
2629
|
+
finally {
|
|
2630
|
+
// Restore previous transition flag
|
|
2631
|
+
this.inTransitionMode = prevInTransition;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2419
2634
|
/**
|
|
2420
2635
|
* Returns the current sweep as a **uuid** (for database matching).
|
|
2421
2636
|
* The SDK's poseCamera.sweep is a sid; we translate it.
|
|
@@ -3200,6 +3415,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
3200
3415
|
|
|
3201
3416
|
class MatterportTagService {
|
|
3202
3417
|
navigationService;
|
|
3418
|
+
/** Name of the common layer every Smarterplan tag is created in. */
|
|
3419
|
+
static SHARED_TAG_LAYER_NAME = 'smarterplan-shared-tags';
|
|
3203
3420
|
dictionnaryTags = new Map();
|
|
3204
3421
|
tagAddQueue = Promise.resolve();
|
|
3205
3422
|
mattertagToFollow = null;
|
|
@@ -3210,6 +3427,22 @@ class MatterportTagService {
|
|
|
3210
3427
|
tagService = null;
|
|
3211
3428
|
tagsCurrentOpacity = new Map();
|
|
3212
3429
|
tagsCurrentEnabled = new Map();
|
|
3430
|
+
/**
|
|
3431
|
+
* Single layer holding every Smarterplan tag, added to every view of the space so a
|
|
3432
|
+
* tag stays in place — same sid, same content — when the user switches between the
|
|
3433
|
+
* furnished and the Defurnished view. Tags added to the global sdk.Tag (or to a
|
|
3434
|
+
* view's own layer) belong to that view only, which is what made them pop in and out
|
|
3435
|
+
* on every view change.
|
|
3436
|
+
*
|
|
3437
|
+
* Deliberately NOT created with `{ common: true }`: in SDK 26.7.5 a common layer is
|
|
3438
|
+
* pushed into `getOrderedModelViews()` at creation time, and that call defaults to
|
|
3439
|
+
* `includeDefurnish: false`, so the Defurnished view never receives it — while
|
|
3440
|
+
* `View.addLayer()` refuses to repair it ("Layer is common. No need to add."). A
|
|
3441
|
+
* plain layer we attach to each view ourselves reaches every view, Defurnished
|
|
3442
|
+
* included. See attachSharedTagLayerToViews().
|
|
3443
|
+
*/
|
|
3444
|
+
sharedTagLayer = null;
|
|
3445
|
+
sharedTagLayerPromise = null;
|
|
3213
3446
|
constructor(navigationService) {
|
|
3214
3447
|
this.navigationService = navigationService;
|
|
3215
3448
|
}
|
|
@@ -3222,6 +3455,7 @@ class MatterportTagService {
|
|
|
3222
3455
|
return;
|
|
3223
3456
|
try {
|
|
3224
3457
|
this.onGoToTag.next(sid);
|
|
3458
|
+
// `open` only exists on the global Tag module, never on a layer-scoped one.
|
|
3225
3459
|
await sdk.Tag.open(sid);
|
|
3226
3460
|
}
|
|
3227
3461
|
catch (e) {
|
|
@@ -3229,6 +3463,102 @@ class MatterportTagService {
|
|
|
3229
3463
|
console.warn('Cannot open tag', sid, e);
|
|
3230
3464
|
}
|
|
3231
3465
|
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Returns the SDK Tag module to use for an operation addressing an existing tag by ID
|
|
3468
|
+
* (icon, opacity, position, attachments, removal...): always the global sdk.Tag.
|
|
3469
|
+
*
|
|
3470
|
+
* A layer-scoped Tag module only exposes the *replayable* mutations, meant to build
|
|
3471
|
+
* the layer's own content; applying them to an already-created tag silently does
|
|
3472
|
+
* nothing — which is what left every pin white instead of carrying its icon. Only
|
|
3473
|
+
* `add` has to go through the layer, see getTagCreationModule().
|
|
3474
|
+
*/
|
|
3475
|
+
getTagModule(sdk, tagId) {
|
|
3476
|
+
return sdk?.Tag;
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Returns the Tag module every new tag must be created through: the shared layer
|
|
3480
|
+
* when the View API is available, the global sdk.Tag otherwise.
|
|
3481
|
+
*/
|
|
3482
|
+
async getTagCreationModule(sdk) {
|
|
3483
|
+
const layer = await this.ensureSharedTagLayer(sdk);
|
|
3484
|
+
return layer?.Tag || sdk?.Tag;
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* Creates (once per SDK session) and returns the shared tag layer.
|
|
3488
|
+
* Resolves to null when the View API is unavailable, so callers transparently fall
|
|
3489
|
+
* back to the global sdk.Tag.
|
|
3490
|
+
*/
|
|
3491
|
+
async ensureSharedTagLayer(sdk) {
|
|
3492
|
+
if (this.sharedTagLayer)
|
|
3493
|
+
return this.sharedTagLayer;
|
|
3494
|
+
if (!sdk?.View?.createLayer)
|
|
3495
|
+
return null;
|
|
3496
|
+
if (!this.sharedTagLayerPromise) {
|
|
3497
|
+
this.sharedTagLayerPromise = Promise.resolve(sdk.View.createLayer(MatterportTagService.SHARED_TAG_LAYER_NAME))
|
|
3498
|
+
.then(async (layer) => {
|
|
3499
|
+
this.sharedTagLayer = layer;
|
|
3500
|
+
// A freshly created layer can come back toggled off, which hides its tags.
|
|
3501
|
+
if (layer && layer.toggled === false) {
|
|
3502
|
+
await layer.toggle(true).catch(() => { });
|
|
3503
|
+
}
|
|
3504
|
+
return layer;
|
|
3505
|
+
})
|
|
3506
|
+
.catch((e) => {
|
|
3507
|
+
// Allow a later retry (e.g. the SDK was not fully ready yet).
|
|
3508
|
+
this.sharedTagLayerPromise = null;
|
|
3509
|
+
if (isDevMode())
|
|
3510
|
+
console.warn('[MatterportTagService] Could not create the shared tag layer', e);
|
|
3511
|
+
return null;
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
return this.sharedTagLayerPromise;
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* Makes sure the shared layer is part of every view of the space, Defurnished
|
|
3518
|
+
* included. This is what actually shares the tags across views, and the reason the
|
|
3519
|
+
* layer must not be a common one: the SDK only performs the attachment for
|
|
3520
|
+
* non-common layers. Idempotent, so it can run on every view collection update.
|
|
3521
|
+
*/
|
|
3522
|
+
async attachSharedTagLayerToViews(sdk, views) {
|
|
3523
|
+
const layer = await this.ensureSharedTagLayer(sdk);
|
|
3524
|
+
if (!layer)
|
|
3525
|
+
return;
|
|
3526
|
+
for (const view of views) {
|
|
3527
|
+
if (!view?.addLayer || view.hasLayer?.(layer))
|
|
3528
|
+
continue;
|
|
3529
|
+
try {
|
|
3530
|
+
await view.addLayer(layer);
|
|
3531
|
+
}
|
|
3532
|
+
catch (e) {
|
|
3533
|
+
if (isDevMode())
|
|
3534
|
+
console.warn('[MatterportTagService] Cannot add the shared tag layer to view', view?.id, e);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3539
|
+
* Adds a tag through the shared layer so it exists in every view. Falls back
|
|
3540
|
+
* to the global sdk.Tag when the View API is unavailable; that call is raced against
|
|
3541
|
+
* a timeout as a safety net, since it never resolves while a Defurnished view is
|
|
3542
|
+
* active.
|
|
3543
|
+
*/
|
|
3544
|
+
async addTagToSdk(sdk, descriptor) {
|
|
3545
|
+
const sharedTagModule = await this.getTagCreationModule(sdk);
|
|
3546
|
+
try {
|
|
3547
|
+
const sidList = (await Promise.race([
|
|
3548
|
+
sharedTagModule.add(descriptor),
|
|
3549
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Tag.add timed out')), 4000)),
|
|
3550
|
+
]));
|
|
3551
|
+
if (sidList?.length)
|
|
3552
|
+
return sidList[0];
|
|
3553
|
+
}
|
|
3554
|
+
catch (error) {
|
|
3555
|
+
if (error?.message?.includes('already in use'))
|
|
3556
|
+
throw error;
|
|
3557
|
+
if (isDevMode())
|
|
3558
|
+
console.warn('[MatterportTagService] Tag.add failed/timed out', error?.message);
|
|
3559
|
+
}
|
|
3560
|
+
return null;
|
|
3561
|
+
}
|
|
3232
3562
|
/** Opens the most recently created tag in the viewer. */
|
|
3233
3563
|
async goToLastTag(sdk) {
|
|
3234
3564
|
if (!sdk || this.mattertagIDs.length === 0)
|
|
@@ -3243,30 +3573,40 @@ class MatterportTagService {
|
|
|
3243
3573
|
async addMattertagToViewer(sdk, mattertagData) {
|
|
3244
3574
|
if (!sdk)
|
|
3245
3575
|
return null;
|
|
3246
|
-
this.tagAddQueue = this.tagAddQueue
|
|
3247
|
-
|
|
3248
|
-
const
|
|
3576
|
+
this.tagAddQueue = this.tagAddQueue
|
|
3577
|
+
.then(async () => {
|
|
3578
|
+
const descriptor = mattertagData.getMattertagDescriptor()[0];
|
|
3579
|
+
const existingId = descriptor?.id || null;
|
|
3249
3580
|
try {
|
|
3250
|
-
const
|
|
3251
|
-
if (
|
|
3252
|
-
|
|
3253
|
-
this.mattertagIDs.
|
|
3254
|
-
|
|
3255
|
-
|
|
3581
|
+
const sid = await this.addTagToSdk(sdk, descriptor);
|
|
3582
|
+
if (sid) {
|
|
3583
|
+
mattertagData.setMatterportSid(sid);
|
|
3584
|
+
if (!this.mattertagIDs.includes(sid)) {
|
|
3585
|
+
this.mattertagIDs.push(sid);
|
|
3586
|
+
}
|
|
3587
|
+
this.dictionnaryTags.set(sid, mattertagData);
|
|
3588
|
+
return sid;
|
|
3256
3589
|
}
|
|
3590
|
+
if (isDevMode())
|
|
3591
|
+
console.warn('[MatterportTagService] Tag.add returned no sid', descriptor);
|
|
3257
3592
|
}
|
|
3258
3593
|
catch (error) {
|
|
3259
3594
|
if (error?.message?.includes('already in use') && existingId) {
|
|
3260
3595
|
if (!this.dictionnaryTags.has(existingId)) {
|
|
3261
3596
|
this.mattertagIDs.push(existingId);
|
|
3262
|
-
this.dictionnaryTags.set(existingId, mattertagData);
|
|
3263
3597
|
}
|
|
3598
|
+
this.dictionnaryTags.set(existingId, mattertagData);
|
|
3264
3599
|
return existingId;
|
|
3265
3600
|
}
|
|
3266
3601
|
if (isDevMode())
|
|
3267
|
-
console.warn('
|
|
3602
|
+
console.warn('[MatterportTagService] addMattertagToViewer failed', error);
|
|
3268
3603
|
}
|
|
3269
3604
|
return null;
|
|
3605
|
+
})
|
|
3606
|
+
.catch((e) => {
|
|
3607
|
+
if (isDevMode())
|
|
3608
|
+
console.warn('[MatterportTagService] tagAddQueue rejected', e);
|
|
3609
|
+
return null;
|
|
3270
3610
|
});
|
|
3271
3611
|
return this.tagAddQueue;
|
|
3272
3612
|
}
|
|
@@ -3285,7 +3625,8 @@ class MatterportTagService {
|
|
|
3285
3625
|
}
|
|
3286
3626
|
this.tagAddQueue = this.tagAddQueue.then(async () => {
|
|
3287
3627
|
try {
|
|
3288
|
-
const
|
|
3628
|
+
const tagModule = await this.getTagCreationModule(sdk);
|
|
3629
|
+
const [createdSid] = await tagModule.add(...descriptor);
|
|
3289
3630
|
this.mattertagIDs.push(createdSid);
|
|
3290
3631
|
this.dictionnaryTags.set(createdSid, mattertagData);
|
|
3291
3632
|
await wait(100);
|
|
@@ -3332,36 +3673,6 @@ class MatterportTagService {
|
|
|
3332
3673
|
tagPos.z >= min.z - margin && tagPos.z <= max.z + margin);
|
|
3333
3674
|
});
|
|
3334
3675
|
}
|
|
3335
|
-
/**
|
|
3336
|
-
* Computes the final context-aware opacity for a single tag.
|
|
3337
|
-
* @param currentFloorSequence Authoritative current floor sequence from sdk.Floor.current
|
|
3338
|
-
* (null = single-floor model or unknown).
|
|
3339
|
-
* @param currentRooms Array of active rooms the camera is in.
|
|
3340
|
-
*/
|
|
3341
|
-
/*computeSingleTagContextOpacity(
|
|
3342
|
-
mattertagData: MattertagData,
|
|
3343
|
-
poseCamera: any,
|
|
3344
|
-
sweepCollection: any,
|
|
3345
|
-
currentFloorSequence: number | null,
|
|
3346
|
-
currentRooms: any[]
|
|
3347
|
-
): number {
|
|
3348
|
-
const mode = this.navigationService.currentCameraMode;
|
|
3349
|
-
|
|
3350
|
-
// 1. Check Floor
|
|
3351
|
-
const tagFloorId = this.getFloorIdForTag(mattertagData, sweepCollection);
|
|
3352
|
-
const currentFloorId = currentFloorSequence?.toString();
|
|
3353
|
-
if (tagFloorId && currentFloorId && tagFloorId !== currentFloorId) {
|
|
3354
|
-
return 0;
|
|
3355
|
-
}
|
|
3356
|
-
|
|
3357
|
-
// 2. Si Dollhouse -> Pleine opacité
|
|
3358
|
-
//if (mode !== CameraMode.INSIDE) return 1;
|
|
3359
|
-
|
|
3360
|
-
// 3. Si Inside -> Check pièce + Distance progressive
|
|
3361
|
-
//if (!this.isTagInRooms(mattertagData, currentRooms)) return 0;
|
|
3362
|
-
|
|
3363
|
-
return 1;
|
|
3364
|
-
}*/
|
|
3365
3676
|
/** Returns the ID of the most recently added tag. */
|
|
3366
3677
|
getLastTag() {
|
|
3367
3678
|
if (this.mattertagIDs.length === 0)
|
|
@@ -3411,10 +3722,11 @@ class MatterportTagService {
|
|
|
3411
3722
|
}
|
|
3412
3723
|
// Apply changes
|
|
3413
3724
|
// Tooltip and click management
|
|
3725
|
+
const tagModule = this.getTagModule(sdk, tagId);
|
|
3414
3726
|
const lastEnabled = this.tagsCurrentEnabled.get(tagId);
|
|
3415
3727
|
if (lastEnabled !== shouldBeEnabled) {
|
|
3416
3728
|
this.tagsCurrentEnabled.set(tagId, shouldBeEnabled);
|
|
3417
|
-
promises.push(
|
|
3729
|
+
promises.push(tagModule.allowAction(tagId, {
|
|
3418
3730
|
opening: shouldBeEnabled,
|
|
3419
3731
|
navigating: shouldBeEnabled,
|
|
3420
3732
|
docking: false
|
|
@@ -3424,16 +3736,26 @@ class MatterportTagService {
|
|
|
3424
3736
|
const lastOpacity = this.tagsCurrentOpacity.get(tagId) ?? -1;
|
|
3425
3737
|
if (Math.abs(lastOpacity - targetOpacity) > 0.05) {
|
|
3426
3738
|
this.tagsCurrentOpacity.set(tagId, targetOpacity);
|
|
3427
|
-
promises.push(
|
|
3739
|
+
promises.push(tagModule.editOpacity(tagId, targetOpacity).catch(() => { }));
|
|
3428
3740
|
}
|
|
3429
3741
|
}
|
|
3430
3742
|
await Promise.all(promises);
|
|
3431
3743
|
}
|
|
3744
|
+
/**
|
|
3745
|
+
* Drops the memoized opacity/enabled state so the next visibility pass re-applies it
|
|
3746
|
+
* to every tag. Needed after a view switch (furnished <-> defurnished): the SDK
|
|
3747
|
+
* re-renders the shared layer's tags and the cached values would otherwise make
|
|
3748
|
+
* updateTagsVisibilityForContext() skip them, leaving tags stuck invisible.
|
|
3749
|
+
*/
|
|
3750
|
+
resetVisibilityCache() {
|
|
3751
|
+
this.tagsCurrentOpacity.clear();
|
|
3752
|
+
this.tagsCurrentEnabled.clear();
|
|
3753
|
+
}
|
|
3432
3754
|
/** Removes a specific tag from the viewer and local dictionaries. */
|
|
3433
3755
|
async deleteMattertagFromId(sdk, mattertagID) {
|
|
3434
3756
|
try {
|
|
3435
3757
|
if (sdk)
|
|
3436
|
-
await sdk.
|
|
3758
|
+
await this.getTagModule(sdk, mattertagID).remove(mattertagID);
|
|
3437
3759
|
this.dictionnaryTags.delete(mattertagID);
|
|
3438
3760
|
const index = this.mattertagIDs.indexOf(mattertagID);
|
|
3439
3761
|
if (index > -1)
|
|
@@ -3456,7 +3778,7 @@ class MatterportTagService {
|
|
|
3456
3778
|
const sidsToRemove = [...this.mattertagIDs];
|
|
3457
3779
|
for (const sid of sidsToRemove) {
|
|
3458
3780
|
try {
|
|
3459
|
-
await sdk.
|
|
3781
|
+
await this.getTagModule(sdk, sid).remove(sid);
|
|
3460
3782
|
}
|
|
3461
3783
|
catch (e) { }
|
|
3462
3784
|
}
|
|
@@ -3467,7 +3789,7 @@ class MatterportTagService {
|
|
|
3467
3789
|
* Creates and configures a Mattertag from a POI, handling coordinates, normals, and sweep ID.
|
|
3468
3790
|
* Skips creation if a tag for the object already exists.
|
|
3469
3791
|
*/
|
|
3470
|
-
async createMattertagFromPOI(sdk, tagType, object, poi,
|
|
3792
|
+
async createMattertagFromPOI(sdk, tagType, object, poi, getTagFromElementId, setTagIconAndOpacity, injectHtmlInTag) {
|
|
3471
3793
|
const { tag } = getTagFromElementId(object.id);
|
|
3472
3794
|
if (tag)
|
|
3473
3795
|
return;
|
|
@@ -3496,6 +3818,12 @@ class MatterportTagService {
|
|
|
3496
3818
|
}
|
|
3497
3819
|
mattertagData.setPoi(poi);
|
|
3498
3820
|
const createdTagID = await this.addMattertagToViewer(sdk, mattertagData);
|
|
3821
|
+
console.log('---------------');
|
|
3822
|
+
console.log('element id ' + mattertagData.elementID);
|
|
3823
|
+
console.log('position: ', mattertagData.getPosition());
|
|
3824
|
+
console.log('rotation: ', mattertagData.getRotation());
|
|
3825
|
+
console.log('sweep id: ', mattertagData.getSweepID());
|
|
3826
|
+
console.log('---------------');
|
|
3499
3827
|
if (createdTagID && sdk) {
|
|
3500
3828
|
await wait(100);
|
|
3501
3829
|
await setTagIconAndOpacity(createdTagID, mattertagData);
|
|
@@ -3523,10 +3851,11 @@ class MatterportTagService {
|
|
|
3523
3851
|
return;
|
|
3524
3852
|
try {
|
|
3525
3853
|
const iconName = `custom-icon-${mattertagID}`;
|
|
3854
|
+
const tagModule = this.getTagModule(sdk, mattertagID);
|
|
3526
3855
|
await sdk.Asset.registerTexture(iconName, iconPath);
|
|
3527
|
-
await
|
|
3856
|
+
await tagModule.editIcon(mattertagID, iconName);
|
|
3528
3857
|
if (applyOpacity) {
|
|
3529
|
-
await
|
|
3858
|
+
await tagModule.editOpacity(mattertagID, opacity);
|
|
3530
3859
|
}
|
|
3531
3860
|
}
|
|
3532
3861
|
catch (error) {
|
|
@@ -3590,16 +3919,17 @@ class MatterportTagService {
|
|
|
3590
3919
|
if (html && html.trim().length > 0 && sdk) {
|
|
3591
3920
|
const scriptTag = this.tagService.getScriptForTag(object, tagType);
|
|
3592
3921
|
html += `${scriptTag}`;
|
|
3922
|
+
const tagModule = this.getTagModule(sdk, tagID);
|
|
3593
3923
|
const [sandboxId, messenger] = await sdk.Tag.registerSandbox(html);
|
|
3594
3924
|
const attachmentID = this.tagsAttachments[tagID];
|
|
3595
3925
|
if (attachmentID) {
|
|
3596
3926
|
try {
|
|
3597
|
-
await
|
|
3927
|
+
await tagModule.detach(tagID, attachmentID);
|
|
3598
3928
|
}
|
|
3599
3929
|
catch (e) { }
|
|
3600
3930
|
}
|
|
3601
3931
|
this.tagsAttachments[tagID] = sandboxId;
|
|
3602
|
-
await
|
|
3932
|
+
await tagModule.attach(tagID, sandboxId);
|
|
3603
3933
|
// Attach the "View informations" handler in tooltip
|
|
3604
3934
|
messenger.on(TagAction.DETAIL_CLICK, this.tagService.onActionDetailClick.bind(this.tagService));
|
|
3605
3935
|
messenger.on(TagAction.TICKET_CLICK, this.tagService.onActionDetailClick.bind(this.tagService));
|
|
@@ -3612,11 +3942,12 @@ class MatterportTagService {
|
|
|
3612
3942
|
else if (sdk) {
|
|
3613
3943
|
const { comment, tagDescription } = this.tagService.getBillboardMediaToEmbed(object);
|
|
3614
3944
|
if (comment) {
|
|
3945
|
+
const tagModule = this.getTagModule(sdk, tagID);
|
|
3615
3946
|
// 1) Register the media attachment by passing the URL string(s).
|
|
3616
3947
|
// registerAttachment returns an array of attachment IDs.
|
|
3617
3948
|
const [attachmentID] = await sdk.Tag.registerAttachment(comment.externalLink);
|
|
3618
3949
|
// 2) Attach the registered media to the tag.
|
|
3619
|
-
await
|
|
3950
|
+
await tagModule.attach(tagID, attachmentID);
|
|
3620
3951
|
// 3) Determine media type for the billboard (PHOTO or VIDEO).
|
|
3621
3952
|
// Use a simple heuristic: YouTube/Vimeo -> VIDEO, otherwise PHOTO.
|
|
3622
3953
|
// The SDK exposes MediaType on the Tag namespace.
|
|
@@ -3624,7 +3955,7 @@ class MatterportTagService {
|
|
|
3624
3955
|
const mediaType = isVideo ? sdk.Mattertag.MediaType.VIDEO : sdk.Mattertag.MediaType.PHOTO;
|
|
3625
3956
|
// 4) Update the billboard text and media using editBillboard.
|
|
3626
3957
|
// editBillboard accepts Partial<Tag.EditableProperties> such as label, description, media.
|
|
3627
|
-
await
|
|
3958
|
+
await tagModule.editBillboard(tagID, {
|
|
3628
3959
|
label: object.title || 'Media',
|
|
3629
3960
|
description: tagDescription || '',
|
|
3630
3961
|
media: {
|
|
@@ -3650,9 +3981,14 @@ class MatterportTagService {
|
|
|
3650
3981
|
/** Clears all local tag data and resets the addition queue. */
|
|
3651
3982
|
clear() {
|
|
3652
3983
|
this.dictionnaryTags.clear();
|
|
3984
|
+
this.tagsCurrentOpacity.clear();
|
|
3985
|
+
this.tagsCurrentEnabled.clear();
|
|
3653
3986
|
this.tagAddQueue = Promise.resolve();
|
|
3654
3987
|
this.mattertagToFollow = null;
|
|
3655
3988
|
this.mattertagIDs = [];
|
|
3989
|
+
// A new space means a new SDK session, hence a new shared layer to create.
|
|
3990
|
+
this.sharedTagLayer = null;
|
|
3991
|
+
this.sharedTagLayerPromise = null;
|
|
3656
3992
|
}
|
|
3657
3993
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportTagService, deps: [{ token: MatterportNavigationService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3658
3994
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportTagService, providedIn: 'root' });
|
|
@@ -3768,6 +4104,15 @@ class MatterportService {
|
|
|
3768
4104
|
this.navigationService.forbiddenSweeps = value;
|
|
3769
4105
|
}
|
|
3770
4106
|
tagAction = new Subject();
|
|
4107
|
+
/** Views (furnished, defurnished...) exposed by the SDK for the current space. */
|
|
4108
|
+
viewsByType = new Map();
|
|
4109
|
+
currentView = null;
|
|
4110
|
+
viewSubscriptions = [];
|
|
4111
|
+
/** Emits the active view every time the user switches between furnished/defurnished. */
|
|
4112
|
+
onViewChanged = new Subject();
|
|
4113
|
+
/** Delay left to the SDK to rebuild the layer's tags after a view switch. */
|
|
4114
|
+
static VIEW_SWITCH_REFRESH_DELAY_MS = 800;
|
|
4115
|
+
viewSwitchRestoreTimer = null;
|
|
3771
4116
|
get mattertagToFollow() {
|
|
3772
4117
|
return this.matterportTagService.mattertagToFollow;
|
|
3773
4118
|
}
|
|
@@ -3923,7 +4268,9 @@ class MatterportService {
|
|
|
3923
4268
|
default:
|
|
3924
4269
|
break;
|
|
3925
4270
|
}
|
|
3926
|
-
this.sdk.Asset.registerTexture('icon-position', this.config.my_config.icon_position)
|
|
4271
|
+
this.sdk.Asset.registerTexture('icon-position', this.config.my_config.icon_position)
|
|
4272
|
+
.catch((e) => { if (isDevMode())
|
|
4273
|
+
console.warn('icon-position texture registration failed', e); });
|
|
3927
4274
|
// Current Room (used for getting bounding box and eventually lazy load mattertag or object3D inside current Room)
|
|
3928
4275
|
this.sdk.Room.current.subscribe((currentRoom) => {
|
|
3929
4276
|
const rooms = Array.isArray(currentRoom) ? currentRoom : (currentRoom ? [currentRoom] : []);
|
|
@@ -4021,8 +4368,9 @@ class MatterportService {
|
|
|
4021
4368
|
if (!this.sdk || sid === this.mattertagToFollow)
|
|
4022
4369
|
return; // ignore when we are in positioning mode
|
|
4023
4370
|
try {
|
|
4024
|
-
|
|
4025
|
-
|
|
4371
|
+
const tagModule = this.matterportTagService.getTagModule(this.sdk, sid);
|
|
4372
|
+
if (tagModule?.allowAction) {
|
|
4373
|
+
tagModule.allowAction(sid, { opening: true, navigating: true, docking: false });
|
|
4026
4374
|
}
|
|
4027
4375
|
}
|
|
4028
4376
|
catch (e) {
|
|
@@ -4034,7 +4382,11 @@ class MatterportService {
|
|
|
4034
4382
|
};
|
|
4035
4383
|
// Wait for the application to reach PLAYING phase
|
|
4036
4384
|
if (this.sdk.App && this.sdk.App.state) {
|
|
4037
|
-
this.sdk.App.state.waitUntil((s) => s.phase === this.sdk.App.Phase.PLAYING).then(() => {
|
|
4385
|
+
this.sdk.App.state.waitUntil((s) => s.phase === this.sdk.App.Phase.PLAYING).then(async () => {
|
|
4386
|
+
// Create the common layer holding every tag before any tag is added, so that
|
|
4387
|
+
// tags are shared between the furnished and the defurnished views.
|
|
4388
|
+
await this.matterportTagService.ensureSharedTagLayer(this.sdk);
|
|
4389
|
+
this.initViews();
|
|
4038
4390
|
this.sdk.Camera.pose.subscribe((pose) => {
|
|
4039
4391
|
this.poseCamera = pose;
|
|
4040
4392
|
this.matterportTagService.updateTagsVisibilityForContext(this.sdk, this.sweepCollection, this.navigationService.currentCameraMode, this.mattertagToFollow, this.navigationService.currentFloorSequence, this.navigationService.currentRooms);
|
|
@@ -4174,7 +4526,7 @@ class MatterportService {
|
|
|
4174
4526
|
*/
|
|
4175
4527
|
enable_following_tag(mattertag) {
|
|
4176
4528
|
const OFFSET = 0.0001; // quasi-invisible mais suffit à éviter l'interception
|
|
4177
|
-
this.sdk.
|
|
4529
|
+
this.matterportTagService.getTagModule(this.sdk, mattertag).editPosition(mattertag, {
|
|
4178
4530
|
anchorPosition: {
|
|
4179
4531
|
x: this.poseMatterport.position.x + OFFSET,
|
|
4180
4532
|
y: this.poseMatterport.position.y,
|
|
@@ -4201,28 +4553,42 @@ class MatterportService {
|
|
|
4201
4553
|
//
|
|
4202
4554
|
/** Creates a temporary Mattertag that follows the user's cursor for positioning. */
|
|
4203
4555
|
async addCursorMattertag(mattertagData) {
|
|
4556
|
+
if (!this.poseMatterport && this.poseCamera) {
|
|
4557
|
+
this.poseMatterport = {
|
|
4558
|
+
position: { ...this.poseCamera.position },
|
|
4559
|
+
normal: { x: 0, y: -0.15, z: 0 },
|
|
4560
|
+
time: new Date().getTime()
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4204
4563
|
if (!this.poseMatterport)
|
|
4205
4564
|
return;
|
|
4206
4565
|
this.mattertagToFollow = await this.addMattertagToViewer(mattertagData);
|
|
4566
|
+
if (!this.mattertagToFollow) {
|
|
4567
|
+
if (isDevMode())
|
|
4568
|
+
console.warn('addCursorMattertag: tag creation failed, mattertagToFollow is null');
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4207
4571
|
await wait(50);
|
|
4208
|
-
this.
|
|
4209
|
-
this.sdk.Tag.editOpacity(this.mattertagToFollow, 1);
|
|
4210
|
-
this.sdk.Tag.editPosition(this.mattertagToFollow, {
|
|
4211
|
-
anchorPosition: { ...this.poseMatterport.position },
|
|
4212
|
-
stemVector: {
|
|
4213
|
-
x: this.poseMatterport.normal.x * 0.3,
|
|
4214
|
-
y: this.poseMatterport.normal.y * 0.3,
|
|
4215
|
-
z: this.poseMatterport.normal.z * 0.3,
|
|
4216
|
-
}
|
|
4217
|
-
});
|
|
4572
|
+
const tagModule = this.matterportTagService.getTagModule(this.sdk, this.mattertagToFollow);
|
|
4218
4573
|
try {
|
|
4219
|
-
await
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4574
|
+
await tagModule.editIcon(this.mattertagToFollow, 'icon-position');
|
|
4575
|
+
await tagModule.editOpacity(this.mattertagToFollow, 1);
|
|
4576
|
+
await tagModule.editPosition(this.mattertagToFollow, {
|
|
4577
|
+
anchorPosition: { ...this.poseMatterport.position },
|
|
4578
|
+
stemVector: {
|
|
4579
|
+
x: (this.poseMatterport.normal?.x ?? 0) * 0.3,
|
|
4580
|
+
y: (this.poseMatterport.normal?.y ?? -0.15) * 0.3,
|
|
4581
|
+
z: (this.poseMatterport.normal?.z ?? 0) * 0.3,
|
|
4582
|
+
}
|
|
4583
|
+
});
|
|
4584
|
+
await tagModule.allowAction(this.mattertagToFollow, {
|
|
4585
|
+
opening: true, navigating: false, docking: false
|
|
4223
4586
|
});
|
|
4224
4587
|
}
|
|
4225
|
-
catch (e) {
|
|
4588
|
+
catch (e) {
|
|
4589
|
+
if (isDevMode())
|
|
4590
|
+
console.warn('addCursorMattertag: error configuring cursor tag', e);
|
|
4591
|
+
}
|
|
4226
4592
|
}
|
|
4227
4593
|
get tagAddQueue() {
|
|
4228
4594
|
return this.matterportTagService.tagAddQueue;
|
|
@@ -4251,7 +4617,7 @@ class MatterportService {
|
|
|
4251
4617
|
/** Applies a pre-registered icon to a Mattertag. */
|
|
4252
4618
|
async setRegistredIconForTag(mattertagID, iconName) {
|
|
4253
4619
|
try {
|
|
4254
|
-
this.sdk.
|
|
4620
|
+
this.matterportTagService.getTagModule(this.sdk, mattertagID).editIcon(mattertagID, iconName);
|
|
4255
4621
|
}
|
|
4256
4622
|
catch (e) {
|
|
4257
4623
|
if (isDevMode())
|
|
@@ -4269,7 +4635,7 @@ class MatterportService {
|
|
|
4269
4635
|
else {
|
|
4270
4636
|
iconName = `${iconName}_unavailable`;
|
|
4271
4637
|
}
|
|
4272
|
-
this.
|
|
4638
|
+
await this.editTagIcon(mattertagID, iconName);
|
|
4273
4639
|
return;
|
|
4274
4640
|
}
|
|
4275
4641
|
const stringTagType = poiTypeToString(mattertagData.getType());
|
|
@@ -4301,11 +4667,19 @@ class MatterportService {
|
|
|
4301
4667
|
opacity = tagIcon.opacity;
|
|
4302
4668
|
}
|
|
4303
4669
|
}
|
|
4670
|
+
await this.editTagIcon(mattertagID, iconName);
|
|
4671
|
+
}
|
|
4672
|
+
/**
|
|
4673
|
+
* Applies an icon to a tag. The call is awaited and its rejection reported: a
|
|
4674
|
+
* silently dropped promise here leaves the pin with its default white look.
|
|
4675
|
+
*/
|
|
4676
|
+
async editTagIcon(mattertagID, iconName) {
|
|
4304
4677
|
try {
|
|
4305
|
-
this.sdk.
|
|
4678
|
+
await this.matterportTagService.getTagModule(this.sdk, mattertagID).editIcon(mattertagID, iconName);
|
|
4306
4679
|
}
|
|
4307
4680
|
catch (error) {
|
|
4308
|
-
|
|
4681
|
+
if (isDevMode())
|
|
4682
|
+
console.warn('[MatterportService] editIcon failed for', mattertagID, iconName, error);
|
|
4309
4683
|
}
|
|
4310
4684
|
}
|
|
4311
4685
|
/** Navigates the camera to the most recently created tag. */
|
|
@@ -4326,18 +4700,60 @@ class MatterportService {
|
|
|
4326
4700
|
await this.sdk.Camera.lookAt(position, {
|
|
4327
4701
|
transition: this.sdk.Camera.TransitionType.FLY
|
|
4328
4702
|
});
|
|
4329
|
-
await this.sdk.Tag.open(mattertagID);
|
|
4330
|
-
}
|
|
4331
|
-
else {
|
|
4332
|
-
// Fallback for cases where we don't have local data yet
|
|
4333
|
-
await this.sdk.Tag.open(mattertagID);
|
|
4334
4703
|
}
|
|
4704
|
+
// `open` only exists on the global Tag module, never on a layer-scoped one.
|
|
4705
|
+
await this.sdk.Tag.open(mattertagID);
|
|
4335
4706
|
}
|
|
4336
4707
|
catch (error) {
|
|
4337
4708
|
if (isDevMode())
|
|
4338
4709
|
console.warn('Cannot navigate to tag', error);
|
|
4339
4710
|
}
|
|
4340
4711
|
}
|
|
4712
|
+
/**
|
|
4713
|
+
* Move the viewer camera to a specific 3D position and apply a rotation.
|
|
4714
|
+
* Delegates the heavy lifting to MatterportNavigationService.move_to_position.
|
|
4715
|
+
* - position: { x, y, z }
|
|
4716
|
+
* - rotation: { yaw, pitch?, roll? } in degrees by default
|
|
4717
|
+
* - options: optional transition and conversion settings (see navigation service)
|
|
4718
|
+
*/
|
|
4719
|
+
async goToPosition(position, rotation = null, options) {
|
|
4720
|
+
// Ensure SDK is initialized
|
|
4721
|
+
if (!this.sdk) {
|
|
4722
|
+
throw new Error('Matterport SDK is not initialized');
|
|
4723
|
+
}
|
|
4724
|
+
// Prevent concurrent navigation if already in a sweep/mode transition
|
|
4725
|
+
if (this.inTransitionMode || this.inTransitionSweep) {
|
|
4726
|
+
if (isDevMode())
|
|
4727
|
+
console.debug('goToPosition aborted: viewer is in transition');
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4730
|
+
try {
|
|
4731
|
+
// Mark transition state on navigation service (move_to_position will also manage flags)
|
|
4732
|
+
this.inTransitionMode = true;
|
|
4733
|
+
// Delegate to navigation service which contains the robust move_to_position implementation
|
|
4734
|
+
await this.navigationService.move_to_position(this.sdk, position, rotation, options);
|
|
4735
|
+
// Update local cached camera info
|
|
4736
|
+
this.lastCameraPosition = { ...position };
|
|
4737
|
+
// Emit a simplified current camera pose for subscribers
|
|
4738
|
+
this.currentCameraPose.next({
|
|
4739
|
+
position: { ...position },
|
|
4740
|
+
rotation: rotation ? { ...rotation } : null,
|
|
4741
|
+
sweep: this.poseCamera?.sweep ?? null
|
|
4742
|
+
});
|
|
4743
|
+
if (isDevMode()) {
|
|
4744
|
+
console.debug('goToPosition completed', { position, rotation, options });
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
catch (err) {
|
|
4748
|
+
if (isDevMode())
|
|
4749
|
+
console.error('goToPosition error', err);
|
|
4750
|
+
throw err;
|
|
4751
|
+
}
|
|
4752
|
+
finally {
|
|
4753
|
+
// Ensure transition flag is cleared (navigation service may also clear it)
|
|
4754
|
+
this.inTransitionMode = false;
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4341
4757
|
/**
|
|
4342
4758
|
* Computes the context-aware opacity for a single tag at the moment it is rendered.
|
|
4343
4759
|
* - Cursor tag: always use provided defaultOpacity.
|
|
@@ -4370,6 +4786,9 @@ class MatterportService {
|
|
|
4370
4786
|
.logo-link,
|
|
4371
4787
|
.footer-logo,
|
|
4372
4788
|
.bottom-ui,
|
|
4789
|
+
img.logo-image,
|
|
4790
|
+
img[class^="logo-image"],
|
|
4791
|
+
img[class*=" logo-image"],
|
|
4373
4792
|
[data-testid="showcase-footer"] {
|
|
4374
4793
|
opacity: 0 !important;
|
|
4375
4794
|
visibility: hidden !important;
|
|
@@ -4396,7 +4815,7 @@ class MatterportService {
|
|
|
4396
4815
|
*/
|
|
4397
4816
|
async updateMatterTagContentForTagID(mattertagID, object = null, tagType = null) {
|
|
4398
4817
|
if (this.sdk) {
|
|
4399
|
-
await this.sdk.
|
|
4818
|
+
await this.matterportTagService.getTagModule(this.sdk, mattertagID).editBillboard(mattertagID, this.dictionnaryTags.get(mattertagID).getData());
|
|
4400
4819
|
}
|
|
4401
4820
|
// Mise à jour de l'icône immédiate
|
|
4402
4821
|
await this.matterportTagService.updateMatterTagContentForTagID(this.sdk, mattertagID, object, tagType, this.setTagIconAndOpacity.bind(this),
|
|
@@ -4439,7 +4858,7 @@ class MatterportService {
|
|
|
4439
4858
|
* @returns
|
|
4440
4859
|
*/
|
|
4441
4860
|
async createMattertagFromPOI(tagType, object, poi) {
|
|
4442
|
-
return this.matterportTagService.createMattertagFromPOI(this.sdk, tagType, object, poi, this.
|
|
4861
|
+
return this.matterportTagService.createMattertagFromPOI(this.sdk, tagType, object, poi, this.getTagFromElementId.bind(this), this.setTagIconAndOpacity.bind(this), this.injectHtmlInTag.bind(this));
|
|
4443
4862
|
}
|
|
4444
4863
|
/**
|
|
4445
4864
|
* Inject custom HTML as Mattertag content
|
|
@@ -4504,6 +4923,10 @@ class MatterportService {
|
|
|
4504
4923
|
const mattertagData = new MattertagData(poiType);
|
|
4505
4924
|
mattertagData.setSweepID(this.navigationService.resolveSweepUuid(this.poseCamera.sweep));
|
|
4506
4925
|
mattertagData.setRotation(this.poseCamera.rotation);
|
|
4926
|
+
const initialPosition = this.poseMatterport?.position ?? this.poseCamera.position;
|
|
4927
|
+
const initialNormal = this.poseMatterport?.normal ?? { x: 0, y: -0.15, z: 0 };
|
|
4928
|
+
mattertagData.setPosition({ ...initialPosition });
|
|
4929
|
+
mattertagData.setNormal({ ...initialNormal });
|
|
4507
4930
|
this.setInteractionMode(ViewerInteractions.ADDING);
|
|
4508
4931
|
await this.addCursorMattertag(mattertagData);
|
|
4509
4932
|
}
|
|
@@ -4621,8 +5044,14 @@ class MatterportService {
|
|
|
4621
5044
|
}
|
|
4622
5045
|
}
|
|
4623
5046
|
async updateMatterTagPosInSdkViewer(mattertagID, object, poiType, poi = null) {
|
|
4624
|
-
|
|
4625
|
-
|
|
5047
|
+
if (!poi || !poi.coordinate) {
|
|
5048
|
+
console.warn('updateMatterTagPosInSdkViewer: poi or poi.coordinate is missing', poi);
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
5051
|
+
const IndexToUpdate = object.pois?.items ? object.pois.items.findIndex((u) => u?.id === poi.id) : -1;
|
|
5052
|
+
if (IndexToUpdate !== -1 && object.pois?.items) {
|
|
5053
|
+
object.pois.items[IndexToUpdate] = poi;
|
|
5054
|
+
}
|
|
4626
5055
|
const { x, y, z } = JSON.parse(poi.coordinate);
|
|
4627
5056
|
const newPosition = {
|
|
4628
5057
|
anchorPosition: {
|
|
@@ -4637,8 +5066,7 @@ class MatterportService {
|
|
|
4637
5066
|
z: 0,
|
|
4638
5067
|
},
|
|
4639
5068
|
};
|
|
4640
|
-
|
|
4641
|
-
this.sdk.Tag.editPosition(mattertagID, newPosition);
|
|
5069
|
+
this.matterportTagService.getTagModule(this.sdk, mattertagID).editPosition(mattertagID, newPosition);
|
|
4642
5070
|
this.dictionnaryTags
|
|
4643
5071
|
.get(mattertagID)
|
|
4644
5072
|
.setPosition(newPosition.anchorPosition);
|
|
@@ -4648,8 +5076,7 @@ class MatterportService {
|
|
|
4648
5076
|
continue;
|
|
4649
5077
|
}
|
|
4650
5078
|
if (currentTag.elementID === object.id && tagId !== mattertagID) {
|
|
4651
|
-
this.
|
|
4652
|
-
this.sdk.Tag.remove(tagId);
|
|
5079
|
+
await this.matterportTagService.deleteMattertagFromId(this.sdk, tagId);
|
|
4653
5080
|
break;
|
|
4654
5081
|
}
|
|
4655
5082
|
}
|
|
@@ -4691,6 +5118,140 @@ class MatterportService {
|
|
|
4691
5118
|
return null;
|
|
4692
5119
|
}
|
|
4693
5120
|
//
|
|
5121
|
+
// ---------- Views (furnished / defurnished) ----------
|
|
5122
|
+
//
|
|
5123
|
+
/**
|
|
5124
|
+
* Subscribes to the SDK View API to keep track of the views available for the space
|
|
5125
|
+
* and of the active one. Tags live in a shared common layer, so a view change only
|
|
5126
|
+
* requires re-applying their visibility, never re-creating them.
|
|
5127
|
+
*/
|
|
5128
|
+
initViews() {
|
|
5129
|
+
this.clearViewSubscriptions();
|
|
5130
|
+
this.viewsByType.clear();
|
|
5131
|
+
this.currentView = null;
|
|
5132
|
+
if (!this.sdk?.View?.views) {
|
|
5133
|
+
if (isDevMode())
|
|
5134
|
+
console.warn('[MatterportService] View API unavailable on this SDK version');
|
|
5135
|
+
return;
|
|
5136
|
+
}
|
|
5137
|
+
this.viewSubscriptions.push(this.sdk.View.views.subscribe({
|
|
5138
|
+
onCollectionUpdated: (collection) => {
|
|
5139
|
+
// SDK callbacks run outside Angular: re-enter the zone so the views made
|
|
5140
|
+
// available here (defurnish button...) are picked up by change detection.
|
|
5141
|
+
this.ngZone.run(() => {
|
|
5142
|
+
this.viewsByType.clear();
|
|
5143
|
+
for (const view of Object.values(collection)) {
|
|
5144
|
+
this.viewsByType.set(view.type, view);
|
|
5145
|
+
}
|
|
5146
|
+
});
|
|
5147
|
+
// Views are discovered progressively: make sure each one carries the shared
|
|
5148
|
+
// tag layer, otherwise its tags would not exist in that view.
|
|
5149
|
+
this.matterportTagService.attachSharedTagLayerToViews(this.sdk, Object.values(collection));
|
|
5150
|
+
},
|
|
5151
|
+
}));
|
|
5152
|
+
this.viewSubscriptions.push(this.sdk.View.current.subscribe((view) => {
|
|
5153
|
+
const changed = this.currentView?.id !== view?.id;
|
|
5154
|
+
this.currentView = view;
|
|
5155
|
+
if (!changed)
|
|
5156
|
+
return;
|
|
5157
|
+
this.restoreTagsForActiveView(view);
|
|
5158
|
+
this.ngZone.run(() => this.onViewChanged.next(view));
|
|
5159
|
+
}));
|
|
5160
|
+
}
|
|
5161
|
+
/**
|
|
5162
|
+
* Forces the opacity/enabled state to be pushed again to every tag. The SDK
|
|
5163
|
+
* re-renders the shared layer's tags on a view swap, and the memoized values would
|
|
5164
|
+
* otherwise make updateTagsVisibilityForContext() skip them, leaving tags invisible.
|
|
5165
|
+
*/
|
|
5166
|
+
reapplyTagsVisibility() {
|
|
5167
|
+
this.matterportTagService.resetVisibilityCache();
|
|
5168
|
+
this.refreshTagsVisibility();
|
|
5169
|
+
}
|
|
5170
|
+
/**
|
|
5171
|
+
* Puts the tags back in shape in the view that just became active.
|
|
5172
|
+
*
|
|
5173
|
+
* Activating a view re-materializes the shared layer's tags from the descriptors
|
|
5174
|
+
* recorded when they were added, so everything applied to a tag *afterwards* through
|
|
5175
|
+
* the global Tag module is dropped along the way: the custom icon (the pin falls back
|
|
5176
|
+
* to the descriptor's white) and the sandbox carrying the hover content. They have to
|
|
5177
|
+
* be applied again on the freshly rebuilt tags.
|
|
5178
|
+
*
|
|
5179
|
+
* Visibility is refreshed straight away so the tags show up without waiting, then
|
|
5180
|
+
* content is re-applied once the transition is over — re-applying it too early would
|
|
5181
|
+
* target tags the SDK has not rebuilt yet.
|
|
5182
|
+
*/
|
|
5183
|
+
restoreTagsForActiveView(view) {
|
|
5184
|
+
// A view only materializes its layers once activated: (re)attach the shared one.
|
|
5185
|
+
this.matterportTagService.attachSharedTagLayerToViews(this.sdk, [view])
|
|
5186
|
+
.then(() => this.reapplyTagsVisibility());
|
|
5187
|
+
this.reapplyTagsVisibility();
|
|
5188
|
+
clearTimeout(this.viewSwitchRestoreTimer);
|
|
5189
|
+
this.viewSwitchRestoreTimer = setTimeout(() => {
|
|
5190
|
+
this.refreshTagsContent()
|
|
5191
|
+
.catch((e) => {
|
|
5192
|
+
if (isDevMode())
|
|
5193
|
+
console.warn('[MatterportService] Cannot restore tags for view', view?.id, e);
|
|
5194
|
+
})
|
|
5195
|
+
.then(() => this.reapplyTagsVisibility());
|
|
5196
|
+
}, MatterportService.VIEW_SWITCH_REFRESH_DELAY_MS);
|
|
5197
|
+
}
|
|
5198
|
+
/**
|
|
5199
|
+
* Re-applies the icon and the injected HTML of every tag we own. Used after a view
|
|
5200
|
+
* switch, see restoreTagsForActiveView().
|
|
5201
|
+
*/
|
|
5202
|
+
async refreshTagsContent() {
|
|
5203
|
+
if (!this.sdk)
|
|
5204
|
+
return;
|
|
5205
|
+
// Snapshot: the dictionary can be mutated while the awaits below are pending.
|
|
5206
|
+
for (const [sid, mattertagData] of [...this.dictionnaryTags]) {
|
|
5207
|
+
if (sid === this.mattertagToFollow)
|
|
5208
|
+
continue;
|
|
5209
|
+
await this.setTagIconAndOpacity(sid, mattertagData);
|
|
5210
|
+
this.injectHtmlInTag(mattertagData.getType(), mattertagData.getObject(), sid)
|
|
5211
|
+
.catch((err) => {
|
|
5212
|
+
if (isDevMode())
|
|
5213
|
+
console.warn('[MatterportService] HTML re-injection failed for', sid, err);
|
|
5214
|
+
});
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
clearViewSubscriptions() {
|
|
5218
|
+
clearTimeout(this.viewSwitchRestoreTimer);
|
|
5219
|
+
this.viewSwitchRestoreTimer = null;
|
|
5220
|
+
for (const subscription of this.viewSubscriptions) {
|
|
5221
|
+
try {
|
|
5222
|
+
subscription.cancel();
|
|
5223
|
+
}
|
|
5224
|
+
catch (e) {
|
|
5225
|
+
if (isDevMode())
|
|
5226
|
+
console.warn('[MatterportService] Cannot cancel view subscription', e);
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
this.viewSubscriptions = [];
|
|
5230
|
+
}
|
|
5231
|
+
/** True when the space provides a defurnished view. */
|
|
5232
|
+
get hasDefurnishView() {
|
|
5233
|
+
return this.viewsByType.has(MatterportViewType.DEFURNISH);
|
|
5234
|
+
}
|
|
5235
|
+
/** True when the defurnished view is the active one. */
|
|
5236
|
+
get isDefurnishedView() {
|
|
5237
|
+
return this.currentView?.type === MatterportViewType.DEFURNISH;
|
|
5238
|
+
}
|
|
5239
|
+
/**
|
|
5240
|
+
* Switches between the defurnished view and the furnished one.
|
|
5241
|
+
* @param returnToStart Move the camera back to the view's start location.
|
|
5242
|
+
*/
|
|
5243
|
+
async toggleDefurnishView(returnToStart = false) {
|
|
5244
|
+
const target = this.isDefurnishedView
|
|
5245
|
+
? this.viewsByType.get(MatterportViewType.LAYERED_BASE) ?? this.viewsByType.get(MatterportViewType.BASE)
|
|
5246
|
+
: this.viewsByType.get(MatterportViewType.DEFURNISH);
|
|
5247
|
+
if (!target) {
|
|
5248
|
+
if (isDevMode())
|
|
5249
|
+
console.warn('[MatterportService] No target view to switch to');
|
|
5250
|
+
return;
|
|
5251
|
+
}
|
|
5252
|
+
await target.setActive(returnToStart);
|
|
5253
|
+
}
|
|
5254
|
+
//
|
|
4694
5255
|
// ---------- Viewer related (switch views, go to) ----------
|
|
4695
5256
|
//
|
|
4696
5257
|
async action_toolbox_floorplan() {
|
|
@@ -4742,6 +5303,9 @@ class MatterportService {
|
|
|
4742
5303
|
*/
|
|
4743
5304
|
async clearAll() {
|
|
4744
5305
|
console.log('removing viewer');
|
|
5306
|
+
this.clearViewSubscriptions();
|
|
5307
|
+
this.viewsByType.clear();
|
|
5308
|
+
this.currentView = null;
|
|
4745
5309
|
await this.action_delete_all_mattertags();
|
|
4746
5310
|
this.floors = null;
|
|
4747
5311
|
this.sweeps = null;
|
|
@@ -4989,6 +5553,13 @@ class ViewerService {
|
|
|
4989
5553
|
await this.actionAddObjectFromPOI(object, poi);
|
|
4990
5554
|
}
|
|
4991
5555
|
}));
|
|
5556
|
+
// Newly created tags have no opacity/allowAction applied yet (only set by
|
|
5557
|
+
// updateTagsVisibilityForContext, which runs on Mode/Sweep/Floor/Room/Camera
|
|
5558
|
+
// events). If this batch loaded after those fired, the tags stay in whatever
|
|
5559
|
+
// default state the SDK gave them until something else nudges a refresh.
|
|
5560
|
+
if (this.checkMode('Matterport')) {
|
|
5561
|
+
this.matterportService.refreshTagsVisibility();
|
|
5562
|
+
}
|
|
4992
5563
|
}
|
|
4993
5564
|
async actionAddObjectFromPOI(object, poi) {
|
|
4994
5565
|
if (this.checkMode('Matterport')) {
|
|
@@ -5418,6 +5989,17 @@ const uploadJsonToS3 = async (path, json, filename) => {
|
|
|
5418
5989
|
return null;
|
|
5419
5990
|
});
|
|
5420
5991
|
};
|
|
5992
|
+
/**
|
|
5993
|
+
* Physically duplicates an S3 object to a new key. Used when copying attachments
|
|
5994
|
+
* between features/tickets/etc. so the copy is fully independent: deleting the
|
|
5995
|
+
* original file later must not affect the copy.
|
|
5996
|
+
* IMPORTANT: does NOT swallow errors — callers need the real error (e.g. NoSuchKey)
|
|
5997
|
+
* to distinguish an orphaned DB reference from a genuine copy failure.
|
|
5998
|
+
*/
|
|
5999
|
+
const copyFileInS3 = async (sourceKey, destKey) => {
|
|
6000
|
+
const result = await Storage.copy({ key: sourceKey, level: "public" }, { key: destKey, level: "public" });
|
|
6001
|
+
return result.key;
|
|
6002
|
+
};
|
|
5421
6003
|
|
|
5422
6004
|
function getDistanceBetweenTwoPoints(startPoint, endPoint) {
|
|
5423
6005
|
const dx = endPoint.x - startPoint.x;
|
|
@@ -6759,13 +7341,16 @@ class NavigationService {
|
|
|
6759
7341
|
}
|
|
6760
7342
|
async getAllowedSweepsIDsForUserInSpace(spaceID) {
|
|
6761
7343
|
const missionsForSpace = this.userService.currentMissions.filter((mis) => mis.spaceID === spaceID);
|
|
7344
|
+
console.log(missionsForSpace);
|
|
6762
7345
|
let navIDs = [];
|
|
6763
7346
|
if (this.userService.userHasAccessToAllZonesInSpace(spaceID)) {
|
|
7347
|
+
console.log(1);
|
|
6764
7348
|
return this.getSweepsIDsForSpace(spaceID);
|
|
6765
7349
|
}
|
|
6766
7350
|
await Promise.all(missionsForSpace.map(async (mission) => {
|
|
6767
7351
|
navIDs = [...navIDs, ...mission.zone.sweepIDs];
|
|
6768
7352
|
}));
|
|
7353
|
+
console.log('final navids = ', navIDs);
|
|
6769
7354
|
return navIDs;
|
|
6770
7355
|
}
|
|
6771
7356
|
async getForbiddenNavigationIDsForUserInSpace(spaceID) {
|
|
@@ -7933,7 +8518,8 @@ class TicketsService extends BaseObjectService {
|
|
|
7933
8518
|
// partitionKey: "",
|
|
7934
8519
|
}, 'AWSKinesis');
|
|
7935
8520
|
}
|
|
7936
|
-
|
|
8521
|
+
const { id, createdAt, updatedAt, __typename, children, domain, ownerMission, equipments, prestataire, space, pois, ...graphqlInput } = input;
|
|
8522
|
+
return this.API.CreateTicket(input);
|
|
7937
8523
|
}
|
|
7938
8524
|
async updateTicket(input) {
|
|
7939
8525
|
return this.API.__proto__.UpdateTicket(input);
|
|
@@ -8315,6 +8901,21 @@ class EquipmentService extends BaseObjectService {
|
|
|
8315
8901
|
return [];
|
|
8316
8902
|
});
|
|
8317
8903
|
}
|
|
8904
|
+
/**
|
|
8905
|
+
* Creates a Note attached to an equipment from an S3 key that ALREADY exists
|
|
8906
|
+
* (as opposed to uploadDoc/uploadImage, which upload a fresh browser File).
|
|
8907
|
+
* Used when duplicating equipment attachments during a cross-visit copy: the
|
|
8908
|
+
* S3 object itself has already been physically copied by the caller beforehand,
|
|
8909
|
+
* so this only needs to record the Note pointing at that new key.
|
|
8910
|
+
*/
|
|
8911
|
+
async createNoteWithExistingAttachment(equipmentID, attachmentKey, type, ownerMissionID) {
|
|
8912
|
+
return this.API.__proto__.CreateNote({
|
|
8913
|
+
attachment: attachmentKey,
|
|
8914
|
+
type,
|
|
8915
|
+
equipmentID,
|
|
8916
|
+
ownerMissionID,
|
|
8917
|
+
});
|
|
8918
|
+
}
|
|
8318
8919
|
async uploadImage(equipment, imageFile) {
|
|
8319
8920
|
return uploadFileToS3(this.prefix.image, imageFile, equipment.id).then(async (url) => {
|
|
8320
8921
|
if (url) {
|
|
@@ -12692,33 +13293,26 @@ class MatterportImportService {
|
|
|
12692
13293
|
iframe.addEventListener('load', async () => {
|
|
12693
13294
|
try {
|
|
12694
13295
|
this.sdk = await showcaseWindow.MP_SDK.connect(iframe, this.sdkKey, '');
|
|
12695
|
-
// Subscribe to Floor data
|
|
12696
13296
|
this.sdk.Floor.data.subscribe({
|
|
12697
13297
|
onCollectionUpdated: function upd(collection) {
|
|
12698
|
-
// console.log("Floors", collection);
|
|
12699
13298
|
this.floors = collection;
|
|
12700
13299
|
}.bind(this),
|
|
12701
13300
|
});
|
|
12702
|
-
// Subscribe to current Floor
|
|
12703
13301
|
this.sdk.Floor.current.subscribe(function upd(currentFloor) {
|
|
12704
13302
|
this.currentFloor = currentFloor;
|
|
12705
13303
|
}.bind(this));
|
|
12706
|
-
// Subscribe to Sweep data
|
|
12707
13304
|
this.sdk.Sweep.data.subscribe({
|
|
12708
13305
|
onCollectionUpdated: function upd(collection) {
|
|
12709
13306
|
console.log('the entire up-to-date collection', collection);
|
|
12710
13307
|
this.sweeps = collection;
|
|
12711
13308
|
}.bind(this),
|
|
12712
13309
|
});
|
|
12713
|
-
// remove labels
|
|
12714
13310
|
await this.sdk.Settings.update('labels', false);
|
|
12715
13311
|
let timeoutExpired = false;
|
|
12716
|
-
// start timeout
|
|
12717
13312
|
setTimeout(() => (timeoutExpired = true), 7000);
|
|
12718
13313
|
await waitUntil(() => (this.sweeps !== undefined && this.floors !== undefined) ||
|
|
12719
13314
|
timeoutExpired === true);
|
|
12720
13315
|
if (timeoutExpired) {
|
|
12721
|
-
// we are likely in matterport error case (that we cannot catch!)
|
|
12722
13316
|
console.log('timeout intitialising Matterport');
|
|
12723
13317
|
return rej(new Error('error init Matterport'));
|
|
12724
13318
|
}
|
|
@@ -12738,7 +13332,6 @@ class MatterportImportService {
|
|
|
12738
13332
|
}
|
|
12739
13333
|
let appIsLoaded = false;
|
|
12740
13334
|
this.sdk.App.state.subscribe(function (appState) {
|
|
12741
|
-
// app state has changed
|
|
12742
13335
|
console.log('The current phase: ', appState.phase);
|
|
12743
13336
|
if (appState.phase === 'appphase.playing') {
|
|
12744
13337
|
console.log('The app is playing');
|
|
@@ -12747,7 +13340,6 @@ class MatterportImportService {
|
|
|
12747
13340
|
});
|
|
12748
13341
|
await waitUntil(() => appIsLoaded === true);
|
|
12749
13342
|
if (reloading) {
|
|
12750
|
-
// perform only 360 import since we are reloading
|
|
12751
13343
|
await this.import360images(overrideExisting);
|
|
12752
13344
|
return Promise.resolve(true);
|
|
12753
13345
|
}
|
|
@@ -12759,10 +13351,8 @@ class MatterportImportService {
|
|
|
12759
13351
|
return Promise.reject(new Error('missing data (no main Building Zone)'));
|
|
12760
13352
|
}
|
|
12761
13353
|
console.log('importing data from matterport');
|
|
12762
|
-
// save sweeps data as json
|
|
12763
13354
|
const json = JSON.stringify(Object.values(this.sweeps));
|
|
12764
13355
|
await uploadJsonToS3(`visits/${this.modelID}/inventory/`, json, 'data.json');
|
|
12765
|
-
// check layer of building type
|
|
12766
13356
|
const layerBuilding = await this.getLayerByName('BUILDING');
|
|
12767
13357
|
const createdDefaultZone = await this.createZone(node.metadata, layerBuilding.id, surface, mainZone.id, Object.keys(this.sweeps), node);
|
|
12768
13358
|
if (!createdDefaultZone) {
|
|
@@ -12771,7 +13361,6 @@ class MatterportImportService {
|
|
|
12771
13361
|
if (this.floors) {
|
|
12772
13362
|
floorZones = await this.importFloorData(createdDefaultZone.id, node);
|
|
12773
13363
|
}
|
|
12774
|
-
// console.log(floorZones);
|
|
12775
13364
|
await Promise.all(Object.keys(this.sweeps).map(async (key) => {
|
|
12776
13365
|
const sweepInfo = this.sweeps[key];
|
|
12777
13366
|
const navigationInput = {
|
|
@@ -12811,7 +13400,6 @@ class MatterportImportService {
|
|
|
12811
13400
|
const newLocal = await this.zoneService.create(zoneInput);
|
|
12812
13401
|
return newLocal;
|
|
12813
13402
|
}
|
|
12814
|
-
// --- Main import function (complete) ---
|
|
12815
13403
|
async import360images(overrideExisting = true) {
|
|
12816
13404
|
console.log('Importing 360 images');
|
|
12817
13405
|
this.importingImages.next(true);
|
|
@@ -12829,10 +13417,8 @@ class MatterportImportService {
|
|
|
12829
13417
|
}
|
|
12830
13418
|
const sweep = scans[index];
|
|
12831
13419
|
console.log('Moving to sweep:', sweep);
|
|
12832
|
-
// Prefer legacy .id first, then uuid, then sid as last resort
|
|
12833
13420
|
const moveId = sweep.id || sweep.uuid || sweep.sid;
|
|
12834
13421
|
try {
|
|
12835
|
-
// Move to sweep (use the best available id)
|
|
12836
13422
|
await this.sdk.Sweep.moveTo(moveId, {
|
|
12837
13423
|
rotation: { x: 0, y: 0 },
|
|
12838
13424
|
transition: this.sdk.Camera && this.sdk.Camera.TransitionType
|
|
@@ -12840,7 +13426,6 @@ class MatterportImportService {
|
|
|
12840
13426
|
: undefined,
|
|
12841
13427
|
transitionTime: 0,
|
|
12842
13428
|
});
|
|
12843
|
-
// Capture with automatic retry (ensures renderer readiness each attempt)
|
|
12844
13429
|
const img = await this.captureEquirectangularWithRetry({ width: 2048, height: 1024 }, { mattertags: false, sweeps: true }, {
|
|
12845
13430
|
maxAttempts: 3,
|
|
12846
13431
|
initialBackoffMs: 500,
|
|
@@ -12848,10 +13433,7 @@ class MatterportImportService {
|
|
|
12848
13433
|
sweepIdForReadyCheck: moveId,
|
|
12849
13434
|
waitForSweepTimeoutMs: 12000
|
|
12850
13435
|
});
|
|
12851
|
-
|
|
12852
|
-
uploadBase64ImageWithRetry(img,
|
|
12853
|
-
// prefer using uuid for storage name if available, else fallback to moveId
|
|
12854
|
-
sweep.uuid || moveId, `visits/${this.modelID}/sweeps/`, 'sweep', 5).then(() => {
|
|
13436
|
+
uploadBase64ImageWithRetry(img, sweep.uuid || moveId, `visits/${this.modelID}/sweeps/`, 'sweep', 5).then(() => {
|
|
12855
13437
|
this.sweepProcessedCount.next(index);
|
|
12856
13438
|
if (index === nmbScans - 1) {
|
|
12857
13439
|
console.log('Import 360 done');
|
|
@@ -12864,38 +13446,29 @@ class MatterportImportService {
|
|
|
12864
13446
|
});
|
|
12865
13447
|
}
|
|
12866
13448
|
catch (err) {
|
|
12867
|
-
// capture failed after retries — log and continue to next sweep
|
|
12868
13449
|
console.error(`Failed to capture sweep ${moveId} after retries:`, err);
|
|
12869
13450
|
this.sweepProcessedCount.next(index);
|
|
12870
|
-
// continue to next sweep (change to `throw err` if you want to abort on first failure)
|
|
12871
13451
|
continue;
|
|
12872
13452
|
}
|
|
12873
13453
|
}
|
|
12874
|
-
// If loop finishes without hitting the final resolve in upload promise:
|
|
12875
13454
|
resolve(true);
|
|
12876
13455
|
this.removeFrame();
|
|
12877
13456
|
});
|
|
12878
13457
|
}
|
|
12879
|
-
// --- capture with retry and backoff ---
|
|
12880
|
-
// options: renderer size; renderOptions: renderer flags; cfg: retry config
|
|
12881
13458
|
async captureEquirectangularWithRetry(options, renderOptions, { maxAttempts = 3, initialBackoffMs = 500, perAttemptTimeoutMs = 8000, sweepIdForReadyCheck = null, waitForSweepTimeoutMs = 12000 } = {}) {
|
|
12882
13459
|
let attempt = 0;
|
|
12883
13460
|
let lastError = null;
|
|
12884
13461
|
while (attempt < maxAttempts) {
|
|
12885
13462
|
attempt += 1;
|
|
12886
13463
|
try {
|
|
12887
|
-
// Ensure sweep is ready before each attempt if requested
|
|
12888
13464
|
if (sweepIdForReadyCheck) {
|
|
12889
13465
|
await this.waitForSweepReady(sweepIdForReadyCheck, waitForSweepTimeoutMs);
|
|
12890
|
-
// small safety delay to let renderer finalize
|
|
12891
13466
|
await this.sleep(120);
|
|
12892
13467
|
}
|
|
12893
|
-
// Race the takeEquirectangular call against a per-attempt timeout
|
|
12894
13468
|
const img = await Promise.race([
|
|
12895
13469
|
this.sdk.Renderer.takeEquirectangular(options, renderOptions),
|
|
12896
13470
|
this.timeoutPromise(perAttemptTimeoutMs, `takeEquirectangular timed out after ${perAttemptTimeoutMs}ms`)
|
|
12897
13471
|
]);
|
|
12898
|
-
// success
|
|
12899
13472
|
return img;
|
|
12900
13473
|
}
|
|
12901
13474
|
catch (err) {
|
|
@@ -12904,37 +13477,27 @@ class MatterportImportService {
|
|
|
12904
13477
|
if (attempt >= maxAttempts) {
|
|
12905
13478
|
throw lastError;
|
|
12906
13479
|
}
|
|
12907
|
-
// exponential backoff before next attempt
|
|
12908
13480
|
const backoff = initialBackoffMs * Math.pow(2, attempt - 1);
|
|
12909
|
-
// add small jitter to reduce thundering herd
|
|
12910
13481
|
const jitter = Math.floor(Math.random() * 100);
|
|
12911
13482
|
await this.sleep(backoff + jitter);
|
|
12912
|
-
// loop to next attempt
|
|
12913
13483
|
}
|
|
12914
13484
|
}
|
|
12915
13485
|
throw lastError || new Error('captureEquirectangularWithRetry failed without error');
|
|
12916
13486
|
}
|
|
12917
|
-
// --- Wait until the SDK reports the target sweep as current ---
|
|
12918
|
-
// Tries: sdk.Sweep.current.waitUntil -> sdk.Sweep.current.subscribe -> polling getCurrent/value
|
|
12919
|
-
// This function matches targetId against multiple fields and prioritizes .id matching.
|
|
12920
13487
|
waitForSweepReady(targetId, timeoutMs = 10000) {
|
|
12921
13488
|
const sdk = this.sdk;
|
|
12922
|
-
// helper to match multiple id fields, prioritizing id first
|
|
12923
13489
|
const matchesTarget = (data) => {
|
|
12924
13490
|
if (!data)
|
|
12925
13491
|
return false;
|
|
12926
|
-
// check .id first (legacy), then uuid, sid, uid
|
|
12927
13492
|
const fields = [data.id, data.uuid, data.sid, data.uid];
|
|
12928
13493
|
return fields.some(f => !!f && String(f) === String(targetId));
|
|
12929
13494
|
};
|
|
12930
|
-
// 1) prefer waitUntil if available on the observable
|
|
12931
13495
|
if (sdk.Sweep && sdk.Sweep.current && typeof sdk.Sweep.current.waitUntil === 'function') {
|
|
12932
13496
|
return Promise.race([
|
|
12933
13497
|
sdk.Sweep.current.waitUntil((data) => matchesTarget(data)),
|
|
12934
13498
|
this.timeoutPromise(timeoutMs, `Timeout waiting for sweep ${targetId}`)
|
|
12935
13499
|
]);
|
|
12936
13500
|
}
|
|
12937
|
-
// 2) subscribe if available
|
|
12938
13501
|
if (sdk.Sweep && sdk.Sweep.current && typeof sdk.Sweep.current.subscribe === 'function') {
|
|
12939
13502
|
return new Promise((resolve, reject) => {
|
|
12940
13503
|
const timer = setTimeout(() => {
|
|
@@ -12944,7 +13507,6 @@ class MatterportImportService {
|
|
|
12944
13507
|
}, timeoutMs);
|
|
12945
13508
|
let off = null;
|
|
12946
13509
|
try {
|
|
12947
|
-
// subscribe may return an unsubscribe function or an object with unsubscribe()
|
|
12948
13510
|
const subResult = sdk.Sweep.current.subscribe((data) => {
|
|
12949
13511
|
if (matchesTarget(data)) {
|
|
12950
13512
|
clearTimeout(timer);
|
|
@@ -12958,7 +13520,6 @@ class MatterportImportService {
|
|
|
12958
13520
|
resolve();
|
|
12959
13521
|
}
|
|
12960
13522
|
});
|
|
12961
|
-
// if subscribe returned an unsubscribe function, keep it as off
|
|
12962
13523
|
if (typeof subResult === 'function')
|
|
12963
13524
|
off = subResult;
|
|
12964
13525
|
else if (subResult && typeof subResult.unsubscribe === 'function')
|
|
@@ -12970,7 +13531,6 @@ class MatterportImportService {
|
|
|
12970
13531
|
}
|
|
12971
13532
|
});
|
|
12972
13533
|
}
|
|
12973
|
-
// 3) polling fallback: try getCurrent(), sdk.Sweep.current() function, or .value, else poll renderer readiness
|
|
12974
13534
|
return new Promise(async (resolve, reject) => {
|
|
12975
13535
|
const pollInterval = 200;
|
|
12976
13536
|
const timer = setTimeout(() => {
|
|
@@ -12984,11 +13544,9 @@ class MatterportImportService {
|
|
|
12984
13544
|
current = await sdk.Sweep.getCurrent();
|
|
12985
13545
|
}
|
|
12986
13546
|
else if (sdk.Sweep && typeof sdk.Sweep.current === 'function') {
|
|
12987
|
-
// some builds expose a function
|
|
12988
13547
|
current = await sdk.Sweep.current();
|
|
12989
13548
|
}
|
|
12990
13549
|
else if (sdk.Sweep && sdk.Sweep.current && sdk.Sweep.current.value) {
|
|
12991
|
-
// some observables expose a .value
|
|
12992
13550
|
current = sdk.Sweep.current.value;
|
|
12993
13551
|
}
|
|
12994
13552
|
if (matchesTarget(current)) {
|
|
@@ -13001,12 +13559,10 @@ class MatterportImportService {
|
|
|
13001
13559
|
// ignore and retry until timeout
|
|
13002
13560
|
}
|
|
13003
13561
|
};
|
|
13004
|
-
// initial immediate check
|
|
13005
13562
|
await checkNow();
|
|
13006
13563
|
const interval = setInterval(checkNow, pollInterval);
|
|
13007
13564
|
});
|
|
13008
13565
|
}
|
|
13009
|
-
// --- small helpers ---
|
|
13010
13566
|
sleep(ms) {
|
|
13011
13567
|
return new Promise(res => setTimeout(res, ms));
|
|
13012
13568
|
}
|
|
@@ -13027,7 +13583,6 @@ class MatterportImportService {
|
|
|
13027
13583
|
console.log('Floor are not loaded yet');
|
|
13028
13584
|
return;
|
|
13029
13585
|
}
|
|
13030
|
-
// look up for sequence number (the safest method)
|
|
13031
13586
|
let floorMatterport = Object.values(this.floors).find((floor) => floor.sequence === matterportFloorSequence);
|
|
13032
13587
|
if (!floorMatterport) {
|
|
13033
13588
|
floorMatterport = Object.values(this.floors).find((floor) => floorName.includes(floor.name) && floor.name != '');
|
|
@@ -13035,13 +13590,11 @@ class MatterportImportService {
|
|
|
13035
13590
|
if (!floorMatterport) {
|
|
13036
13591
|
floorMatterport = Object.values(this.floors).find((floor) => floorName.includes(floor.id));
|
|
13037
13592
|
}
|
|
13038
|
-
// console.log(floorMatterport)
|
|
13039
13593
|
if (floorMatterport) {
|
|
13040
13594
|
let retry = true;
|
|
13041
13595
|
while (retry) {
|
|
13042
13596
|
try {
|
|
13043
13597
|
const floorIndex = await this.sdk.Floor.moveTo(floorMatterport.sequence);
|
|
13044
|
-
// console.log("moved to floorIndex", floorIndex);
|
|
13045
13598
|
retry = false;
|
|
13046
13599
|
}
|
|
13047
13600
|
catch (error) {
|
|
@@ -13060,13 +13613,10 @@ class MatterportImportService {
|
|
|
13060
13613
|
const nmbFloors = Object.keys(this.floors).length;
|
|
13061
13614
|
console.log('Total number of floors', nmbFloors);
|
|
13062
13615
|
for (let index = 0; index < nmbFloors; index += 1) {
|
|
13063
|
-
// eslint-disable-next-line no-await-in-loop
|
|
13064
13616
|
console.log('Importing floor', index);
|
|
13065
13617
|
await this.sdk.Floor.moveTo(index).then(async () => {
|
|
13066
13618
|
await wait(700);
|
|
13067
|
-
// console.log("about to save the floor", index);
|
|
13068
13619
|
const zone = await this.saveFloor(defautZoneID, node);
|
|
13069
|
-
// console.log("received floor zone", zone);
|
|
13070
13620
|
zones.push(zone);
|
|
13071
13621
|
});
|
|
13072
13622
|
}
|
|
@@ -13084,14 +13634,12 @@ class MatterportImportService {
|
|
|
13084
13634
|
console.log('sweeps for floor', sweepsForFloor.length);
|
|
13085
13635
|
const sweepIDs = sweepsForFloor.map((sweep) => sweep.uuid);
|
|
13086
13636
|
const zone = await this.createZone(`Étage ${name} - ${node.metadata}`, layerFloor.id, null, defautZoneID, sweepIDs, node, JSON.stringify({ matterportFloorSequence: this.currentFloor.sequence }));
|
|
13087
|
-
// TODO link to scans
|
|
13088
13637
|
const base64 = await this.sdk.Renderer.takeScreenShot({
|
|
13089
13638
|
width: 4096,
|
|
13090
13639
|
height: 2048,
|
|
13091
13640
|
});
|
|
13092
13641
|
const spaceIDshort = this.spaceID.slice(0, 8);
|
|
13093
13642
|
await uploadBase64Image(base64, null, `visits/${this.modelID}/plans/`, `plan_floor_${this.currentFloor.id}_${spaceIDshort}`);
|
|
13094
|
-
// create Plan object
|
|
13095
13643
|
const plan = {
|
|
13096
13644
|
name: `plan_floor_${this.currentFloor.id}`,
|
|
13097
13645
|
annexe: `visits/${this.modelID}/plans/plan_floor_${this.currentFloor.id}_${spaceIDshort}.jpeg`,
|
|
@@ -13102,7 +13650,6 @@ class MatterportImportService {
|
|
|
13102
13650
|
isImportedMatterport: true,
|
|
13103
13651
|
};
|
|
13104
13652
|
await this.planService.createPlan(plan);
|
|
13105
|
-
// console.log("floor saved");
|
|
13106
13653
|
return Promise.resolve(zone);
|
|
13107
13654
|
}
|
|
13108
13655
|
abandon() {
|
|
@@ -13124,7 +13671,6 @@ class MatterportImportService {
|
|
|
13124
13671
|
if (layers.length > 0) {
|
|
13125
13672
|
return layers[0];
|
|
13126
13673
|
}
|
|
13127
|
-
// create then
|
|
13128
13674
|
return this.layerService.createLayerForOrganisation(name, currentOrgId);
|
|
13129
13675
|
}
|
|
13130
13676
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportImportService, deps: [{ token: MATTERPORT_SDK_KEY }, { token: NavigationService }, { token: ZoneService }, { token: ViewerService }, { token: LayerService }, { token: BaseUserService }, { token: PlanService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
@@ -13140,6 +13686,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
13140
13686
|
args: [MATTERPORT_SDK_KEY]
|
|
13141
13687
|
}] }, { type: NavigationService }, { type: ZoneService }, { type: ViewerService }, { type: LayerService }, { type: BaseUserService }, { type: PlanService }] });
|
|
13142
13688
|
|
|
13689
|
+
// matterport-transform.util.ts
|
|
13690
|
+
/**
|
|
13691
|
+
* Computes the rigid transform (Y-axis rotation + translation) between two
|
|
13692
|
+
* coordinate frames, from 2 pairs of points representing the same physical spot.
|
|
13693
|
+
*/
|
|
13694
|
+
function computeRigidTransform(sourceA, targetA, sourceB, targetB) {
|
|
13695
|
+
const vSource = { x: sourceB.x - sourceA.x, z: sourceB.z - sourceA.z };
|
|
13696
|
+
const vTarget = { x: targetB.x - targetA.x, z: targetB.z - targetA.z };
|
|
13697
|
+
const angleSource = Math.atan2(vSource.z, vSource.x);
|
|
13698
|
+
const angleTarget = Math.atan2(vTarget.z, vTarget.x);
|
|
13699
|
+
return {
|
|
13700
|
+
rotationYRad: angleTarget - angleSource,
|
|
13701
|
+
refSource: sourceA,
|
|
13702
|
+
refTarget: targetA
|
|
13703
|
+
};
|
|
13704
|
+
}
|
|
13705
|
+
/** Applies the rigid transform to a 3D position (x, y, z). */
|
|
13706
|
+
function applyRigidTransformToPoint(p, t) {
|
|
13707
|
+
const dx = p.x - t.refSource.x;
|
|
13708
|
+
const dz = p.z - t.refSource.z;
|
|
13709
|
+
const cos = Math.cos(t.rotationYRad);
|
|
13710
|
+
const sin = Math.sin(t.rotationYRad);
|
|
13711
|
+
return {
|
|
13712
|
+
x: t.refTarget.x + (dx * cos - dz * sin),
|
|
13713
|
+
y: t.refTarget.y + (p.y - t.refSource.y), // vertical axis is not rotated
|
|
13714
|
+
z: t.refTarget.z + (dx * sin + dz * cos)
|
|
13715
|
+
};
|
|
13716
|
+
}
|
|
13717
|
+
/** Applies the Y rotation (in degrees) to an orientation angle (a pose's yRotation). */
|
|
13718
|
+
function applyRigidTransformToYRotationDeg(yRotationDeg, t) {
|
|
13719
|
+
const deltaDeg = t.rotationYRad * (180 / Math.PI);
|
|
13720
|
+
return yRotationDeg + deltaDeg;
|
|
13721
|
+
}
|
|
13722
|
+
/** Euclidean distance between two points, useful to validate landmark quality. */
|
|
13723
|
+
function distance(a, b) {
|
|
13724
|
+
const dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
|
|
13725
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
13726
|
+
}
|
|
13727
|
+
|
|
13143
13728
|
/* eslint-disable class-methods-use-this */
|
|
13144
13729
|
class Object3DService {
|
|
13145
13730
|
userService;
|
|
@@ -13506,6 +14091,855 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
13506
14091
|
args: ['currentAPIService']
|
|
13507
14092
|
}] }, { type: BaseUserService }, { type: NavigatorService }, { type: ZoneChangeService }, { type: FilterService }] });
|
|
13508
14093
|
|
|
14094
|
+
/** Safely extracts a readable message from an unknown catch value, including Amplify's
|
|
14095
|
+
* GraphQL error shape ({ data, errors: [{ errorType, message }] }). */
|
|
14096
|
+
function toErrorMessage(err) {
|
|
14097
|
+
if (err && typeof err === 'object') {
|
|
14098
|
+
const anyErr = err;
|
|
14099
|
+
if (Array.isArray(anyErr.errors) && anyErr.errors.length > 0) {
|
|
14100
|
+
return anyErr.errors
|
|
14101
|
+
.map((e) => {
|
|
14102
|
+
const title = e.errorType ? `${e.errorType}: ` : '';
|
|
14103
|
+
return `${title}${e.message || 'Unknown error'}`;
|
|
14104
|
+
})
|
|
14105
|
+
.join(' | ');
|
|
14106
|
+
}
|
|
14107
|
+
if (err instanceof Error) {
|
|
14108
|
+
return err.message;
|
|
14109
|
+
}
|
|
14110
|
+
}
|
|
14111
|
+
if (typeof err === 'string') {
|
|
14112
|
+
return err;
|
|
14113
|
+
}
|
|
14114
|
+
try {
|
|
14115
|
+
return JSON.stringify(err);
|
|
14116
|
+
}
|
|
14117
|
+
catch {
|
|
14118
|
+
return String(err);
|
|
14119
|
+
}
|
|
14120
|
+
}
|
|
14121
|
+
/** Thrown when the referenced S3 source file doesn't exist (orphaned DB reference —
|
|
14122
|
+
* e.g. the file was deleted or renamed on S3 without updating the database).
|
|
14123
|
+
* Kept distinct from other copy failures so the calling loop can log a clear,
|
|
14124
|
+
* specific message and skip the whole item instead of a generic error. */
|
|
14125
|
+
class MissingS3FileError extends Error {
|
|
14126
|
+
sourceKey;
|
|
14127
|
+
contextLabel;
|
|
14128
|
+
constructor(sourceKey, contextLabel) {
|
|
14129
|
+
super(`Source file missing on S3: "${sourceKey}" (${contextLabel})`);
|
|
14130
|
+
this.sourceKey = sourceKey;
|
|
14131
|
+
this.contextLabel = contextLabel;
|
|
14132
|
+
this.name = 'MissingS3FileError';
|
|
14133
|
+
}
|
|
14134
|
+
}
|
|
14135
|
+
class MatterportTagCopyService {
|
|
14136
|
+
navigationService;
|
|
14137
|
+
userService;
|
|
14138
|
+
poiService;
|
|
14139
|
+
ticketService;
|
|
14140
|
+
equipmentService;
|
|
14141
|
+
object3DService;
|
|
14142
|
+
featureService;
|
|
14143
|
+
measurementService;
|
|
14144
|
+
commentService;
|
|
14145
|
+
translate;
|
|
14146
|
+
// Emits progress updates during copyTagsFromVisit(); null when idle
|
|
14147
|
+
copyProgress$ = new BehaviorSubject(null);
|
|
14148
|
+
// Emits the accumulated log lines during copyTagsFromVisit(); [] when idle
|
|
14149
|
+
copyLog$ = new BehaviorSubject([]);
|
|
14150
|
+
spModule;
|
|
14151
|
+
log = [];
|
|
14152
|
+
errorSeq = 0;
|
|
14153
|
+
constructor(navigationService, userService, poiService, ticketService, equipmentService, object3DService, featureService, measurementService, commentService, translate) {
|
|
14154
|
+
this.navigationService = navigationService;
|
|
14155
|
+
this.userService = userService;
|
|
14156
|
+
this.poiService = poiService;
|
|
14157
|
+
this.ticketService = ticketService;
|
|
14158
|
+
this.equipmentService = equipmentService;
|
|
14159
|
+
this.object3DService = object3DService;
|
|
14160
|
+
this.featureService = featureService;
|
|
14161
|
+
this.measurementService = measurementService;
|
|
14162
|
+
this.commentService = commentService;
|
|
14163
|
+
this.translate = translate;
|
|
14164
|
+
this.spModule = userService.getSpModule();
|
|
14165
|
+
}
|
|
14166
|
+
//
|
|
14167
|
+
// ---------- Public API ----------
|
|
14168
|
+
//
|
|
14169
|
+
async copyTagsFromVisit(sourceSpaceId, targetSpaceId, landmarkSourceA, landmarkTargetA, landmarkSourceB, landmarkTargetB) {
|
|
14170
|
+
let errorCount = 0;
|
|
14171
|
+
this.resetLog();
|
|
14172
|
+
try {
|
|
14173
|
+
console.log(`Copying Tags from source ${sourceSpaceId} to target ${targetSpaceId}`);
|
|
14174
|
+
this.setStepProgress('collecting', 'collecting', 0);
|
|
14175
|
+
this.copyProgress$.next({
|
|
14176
|
+
phase: 'collecting',
|
|
14177
|
+
processedInPhase: 0, totalInPhase: 0,
|
|
14178
|
+
processedOverall: 0, totalOverall: 0,
|
|
14179
|
+
percent: 0, errorCount: 0
|
|
14180
|
+
});
|
|
14181
|
+
const targetMission = this.userService.currentMission(targetSpaceId);
|
|
14182
|
+
const targetMissionId = targetMission ? targetMission.id : null;
|
|
14183
|
+
const transform = computeRigidTransform(landmarkSourceA, landmarkTargetA, landmarkSourceB, landmarkTargetB);
|
|
14184
|
+
console.log('Rigid transform computed:', {
|
|
14185
|
+
rotationYDeg: transform.rotationYRad * (180 / Math.PI),
|
|
14186
|
+
refSource: transform.refSource,
|
|
14187
|
+
refTarget: transform.refTarget
|
|
14188
|
+
});
|
|
14189
|
+
const sourceSweepIds = await this.navigationService.getAllowedSweepsIDsForUserInSpace(sourceSpaceId);
|
|
14190
|
+
const targetSweepIds = await this.navigationService.getAllowedSweepsIDsForUserInSpace(targetSpaceId);
|
|
14191
|
+
this.setStepProgress('collecting', 'collecting', 30);
|
|
14192
|
+
const [sourceNavPositions, targetNavPositions] = await Promise.all([
|
|
14193
|
+
this.buildNavigationPositionMap(sourceSpaceId, sourceSweepIds),
|
|
14194
|
+
this.buildNavigationPositionMap(targetSpaceId, targetSweepIds)
|
|
14195
|
+
]);
|
|
14196
|
+
const elementIds = {};
|
|
14197
|
+
let sourceTickets = [];
|
|
14198
|
+
let sourceEquipments = [];
|
|
14199
|
+
let targetTickets = [];
|
|
14200
|
+
let targetEquipments = [];
|
|
14201
|
+
let sourceMeasurements = [];
|
|
14202
|
+
let targetMeasurements = [];
|
|
14203
|
+
if (this.spModule == SpModule.IMMO) {
|
|
14204
|
+
sourceTickets = await this.ticketService.getTicketsBySpace(sourceSpaceId);
|
|
14205
|
+
sourceEquipments = await this.equipmentService.getEquipmentsForSpace(sourceSpaceId);
|
|
14206
|
+
targetTickets = await this.ticketService.getTicketsBySpace(targetSpaceId);
|
|
14207
|
+
targetEquipments = await this.equipmentService.getEquipmentsForSpace(targetSpaceId);
|
|
14208
|
+
}
|
|
14209
|
+
this.setStepProgress('collecting', 'collecting', 55);
|
|
14210
|
+
const sourceObjects3D = await this.object3DService.getObjects3DBySpace(sourceSpaceId);
|
|
14211
|
+
const targetObjects3D = await this.object3DService.getObjects3DBySpace(targetSpaceId);
|
|
14212
|
+
const sourceFeatures = await this.featureService.getFeaturesBySpaceWithoutFiltering(sourceSpaceId);
|
|
14213
|
+
const targetFeatures = await this.featureService.getFeaturesBySpaceWithoutFiltering(targetSpaceId);
|
|
14214
|
+
this.setStepProgress('collecting', 'collecting', 75);
|
|
14215
|
+
if (this.spModule == SpModule.IMMO) {
|
|
14216
|
+
sourceMeasurements = await this.measurementService.getMeasurementsForSpaceForUser(sourceSpaceId);
|
|
14217
|
+
targetMeasurements = await this.measurementService.getMeasurementsForSpaceForUser(targetSpaceId);
|
|
14218
|
+
}
|
|
14219
|
+
this.setStepProgress('collecting', 'collecting', 90);
|
|
14220
|
+
const sourcePoisAll = await this.poiService.getPoisByMatterportSweeps(sourceSweepIds);
|
|
14221
|
+
const targetPoisAll = await this.poiService.getPoisByMatterportSweeps(targetSweepIds);
|
|
14222
|
+
const handledPoiTypes = new Set([
|
|
14223
|
+
PoiType.TICKET, PoiType.EQUIPMENT, PoiType.DATA, PoiType.DESK,
|
|
14224
|
+
PoiType.OBJECT3D, PoiType.MEASURE
|
|
14225
|
+
]);
|
|
14226
|
+
const sourcePois = sourcePoisAll.filter(p => !handledPoiTypes.has(p.type));
|
|
14227
|
+
const targetPois = targetPoisAll.filter(p => !handledPoiTypes.has(p.type));
|
|
14228
|
+
this.setStepProgress('collecting', 'collecting', 100);
|
|
14229
|
+
const totalOverall = sourceTickets.length + sourceEquipments.length + sourcePois.length
|
|
14230
|
+
+ sourceObjects3D.length + sourceFeatures.length + sourceMeasurements.length;
|
|
14231
|
+
let processedOverall = 0;
|
|
14232
|
+
const emitProgress = (phase, processedInPhase, totalInPhase) => {
|
|
14233
|
+
this.copyProgress$.next({
|
|
14234
|
+
phase,
|
|
14235
|
+
processedInPhase,
|
|
14236
|
+
totalInPhase,
|
|
14237
|
+
processedOverall,
|
|
14238
|
+
totalOverall,
|
|
14239
|
+
percent: totalOverall > 0 ? Math.round((processedOverall / totalOverall) * 100) : 0,
|
|
14240
|
+
errorCount
|
|
14241
|
+
});
|
|
14242
|
+
};
|
|
14243
|
+
if (this.spModule == SpModule.IMMO) {
|
|
14244
|
+
// TICKETS
|
|
14245
|
+
for (let i = 0; i < sourceTickets.length; i++) {
|
|
14246
|
+
const sourceObj = sourceTickets[i];
|
|
14247
|
+
try {
|
|
14248
|
+
const existsInTarget = this.matchesExisting(sourceObj, targetTickets);
|
|
14249
|
+
if (!existsInTarget) {
|
|
14250
|
+
const rawSource = sourceObj;
|
|
14251
|
+
const ticketInput = {
|
|
14252
|
+
title: rawSource.title,
|
|
14253
|
+
friendlyLabel: rawSource.friendlyLabel,
|
|
14254
|
+
status: rawSource.status,
|
|
14255
|
+
type: rawSource.type,
|
|
14256
|
+
priority: rawSource.priority,
|
|
14257
|
+
description: rawSource.description,
|
|
14258
|
+
level: rawSource.level,
|
|
14259
|
+
metadata: rawSource.metadata || null,
|
|
14260
|
+
read: rawSource.read || null,
|
|
14261
|
+
projectID: rawSource.projectID || null,
|
|
14262
|
+
problemCode: rawSource.problemCode || null,
|
|
14263
|
+
archivedForMissionID: rawSource.archivedForMissionID || null,
|
|
14264
|
+
spaceID: targetSpaceId,
|
|
14265
|
+
parentID: null,
|
|
14266
|
+
domainID: null,
|
|
14267
|
+
ownerMissionID: targetMissionId
|
|
14268
|
+
};
|
|
14269
|
+
const createdObj = await this.ticketService.createTicket(ticketInput);
|
|
14270
|
+
if (createdObj && createdObj.id) {
|
|
14271
|
+
elementIds[sourceObj.id] = createdObj.id;
|
|
14272
|
+
}
|
|
14273
|
+
if (sourceObj.pois.items && sourceObj.pois.items.length > 0) {
|
|
14274
|
+
const sourcePoi = sourceObj.pois.items[0];
|
|
14275
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14276
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
14277
|
+
const poiInput = {
|
|
14278
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
14279
|
+
type: sourcePoi.type,
|
|
14280
|
+
elementID: createdObj.id,
|
|
14281
|
+
metadata: sourcePoi.metadata,
|
|
14282
|
+
matterportSweepID: targetSweepId
|
|
14283
|
+
};
|
|
14284
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
14285
|
+
}
|
|
14286
|
+
}
|
|
14287
|
+
}
|
|
14288
|
+
catch (itemError) {
|
|
14289
|
+
console.error(`Error copying ticket "${sourceObj?.title}" (id: ${sourceObj?.id}):`, itemError);
|
|
14290
|
+
errorCount++;
|
|
14291
|
+
this.logCopyItemError(itemError, 'errorTicket', sourceObj?.title || sourceObj?.id);
|
|
14292
|
+
}
|
|
14293
|
+
processedOverall++;
|
|
14294
|
+
emitProgress('tickets', i + 1, sourceTickets.length);
|
|
14295
|
+
const pct = sourceTickets.length > 0 ? Math.round(((i + 1) / sourceTickets.length) * 100) : 100;
|
|
14296
|
+
this.setStepProgress('tickets', 'tickets', pct);
|
|
14297
|
+
}
|
|
14298
|
+
// EQUIPMENTS (title, POI, PHOTO and NOTES/documents all copied — files physically
|
|
14299
|
+
// duplicated in S3 first, so the copy stays independent from the source)
|
|
14300
|
+
for (let i = 0; i < sourceEquipments.length; i++) {
|
|
14301
|
+
const sourceObj = sourceEquipments[i];
|
|
14302
|
+
try {
|
|
14303
|
+
const existsInTarget = this.matchesExisting(sourceObj, targetEquipments);
|
|
14304
|
+
if (!existsInTarget) {
|
|
14305
|
+
const rawSource = sourceObj;
|
|
14306
|
+
let copiedPhotoKey = null;
|
|
14307
|
+
if (rawSource.photo) {
|
|
14308
|
+
copiedPhotoKey = await this.copyS3KeyIfPresent(rawSource.photo, `equipment photo "${rawSource.title}"`);
|
|
14309
|
+
}
|
|
14310
|
+
const equipmentInput = {
|
|
14311
|
+
title: rawSource.title,
|
|
14312
|
+
status: rawSource.status,
|
|
14313
|
+
description: rawSource.description,
|
|
14314
|
+
archivedForMissionID: rawSource.archivedForMissionID || null,
|
|
14315
|
+
spaceID: targetSpaceId,
|
|
14316
|
+
domainID: null,
|
|
14317
|
+
photo: copiedPhotoKey,
|
|
14318
|
+
};
|
|
14319
|
+
const createdObj = await this.equipmentService.createEquipment(equipmentInput, null, null);
|
|
14320
|
+
if (createdObj && createdObj.id) {
|
|
14321
|
+
elementIds[sourceObj.id] = createdObj.id;
|
|
14322
|
+
}
|
|
14323
|
+
// Copy notes (documents, technical/financial/other/contractual/image notes)
|
|
14324
|
+
if (createdObj && rawSource.notes?.items && rawSource.notes.items.length > 0) {
|
|
14325
|
+
for (const sourceNote of rawSource.notes.items) {
|
|
14326
|
+
try {
|
|
14327
|
+
if (!sourceNote.attachment) {
|
|
14328
|
+
continue; // nothing to duplicate for this note
|
|
14329
|
+
}
|
|
14330
|
+
const copiedKey = await this.copyS3KeyIfPresent(sourceNote.attachment, `equipment note for "${sourceObj?.title}"`);
|
|
14331
|
+
await this.equipmentService.createNoteWithExistingAttachment(createdObj.id, copiedKey, sourceNote.type, targetMissionId);
|
|
14332
|
+
}
|
|
14333
|
+
catch (noteError) {
|
|
14334
|
+
console.error(`Error copying note for equipment "${sourceObj?.title}":`, noteError);
|
|
14335
|
+
errorCount++;
|
|
14336
|
+
this.logCopyItemError(noteError, 'errorEquipmentNote', sourceObj?.title || sourceObj?.id);
|
|
14337
|
+
}
|
|
14338
|
+
}
|
|
14339
|
+
}
|
|
14340
|
+
if (sourceObj.pois.items && sourceObj.pois.items.length > 0) {
|
|
14341
|
+
const sourcePoi = sourceObj.pois.items[0];
|
|
14342
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14343
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
14344
|
+
const poiInput = {
|
|
14345
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
14346
|
+
type: sourcePoi.type,
|
|
14347
|
+
elementID: createdObj.id,
|
|
14348
|
+
metadata: sourcePoi.metadata,
|
|
14349
|
+
matterportSweepID: targetSweepId
|
|
14350
|
+
};
|
|
14351
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
14352
|
+
}
|
|
14353
|
+
}
|
|
14354
|
+
}
|
|
14355
|
+
catch (itemError) {
|
|
14356
|
+
console.error(`Error copying equipment "${sourceObj?.title}" (id: ${sourceObj?.id}):`, itemError);
|
|
14357
|
+
errorCount++;
|
|
14358
|
+
this.logCopyItemError(itemError, 'errorEquipment', sourceObj?.title || sourceObj?.id);
|
|
14359
|
+
}
|
|
14360
|
+
processedOverall++;
|
|
14361
|
+
emitProgress('equipments', i + 1, sourceEquipments.length);
|
|
14362
|
+
const pct = sourceEquipments.length > 0 ? Math.round(((i + 1) / sourceEquipments.length) * 100) : 100;
|
|
14363
|
+
this.setStepProgress('equipments', 'equipments', pct);
|
|
14364
|
+
}
|
|
14365
|
+
// Second pass: remap subequipmentsIDs on newly created equipments to point to
|
|
14366
|
+
// their copied counterparts. IDs with no copied counterpart (source equipment
|
|
14367
|
+
// wasn't copied — e.g. it already existed in target, or was skipped) are dropped
|
|
14368
|
+
// rather than left dangling or pointing back at the source space's equipment.
|
|
14369
|
+
for (const sourceObj of sourceEquipments) {
|
|
14370
|
+
const rawSource = sourceObj;
|
|
14371
|
+
const newId = elementIds[sourceObj.id];
|
|
14372
|
+
if (newId && rawSource.subequipmentsIDs && rawSource.subequipmentsIDs.length > 0) {
|
|
14373
|
+
const remapped = rawSource.subequipmentsIDs
|
|
14374
|
+
.map((subId) => elementIds[subId])
|
|
14375
|
+
.filter((id) => !!id);
|
|
14376
|
+
if (remapped.length > 0) {
|
|
14377
|
+
try {
|
|
14378
|
+
await this.equipmentService.updateEquipment({ id: newId, subequipmentsIDs: remapped });
|
|
14379
|
+
}
|
|
14380
|
+
catch (subEquipError) {
|
|
14381
|
+
console.error(`Error remapping sub-equipments for "${sourceObj?.title}":`, subEquipError);
|
|
14382
|
+
errorCount++;
|
|
14383
|
+
this.logCopyItemError(subEquipError, 'errorEquipmentSubEquip', sourceObj?.title || sourceObj?.id);
|
|
14384
|
+
}
|
|
14385
|
+
}
|
|
14386
|
+
}
|
|
14387
|
+
}
|
|
14388
|
+
}
|
|
14389
|
+
// POIS (standalone — types already handled above are excluded)
|
|
14390
|
+
for (let i = 0; i < sourcePois.length; i++) {
|
|
14391
|
+
const sourcePoi = sourcePois[i];
|
|
14392
|
+
try {
|
|
14393
|
+
const existsInTarget = targetPois.some(tp => tp.coordinate === sourcePoi.coordinate && tp.tagDescription === sourcePoi.tagDescription);
|
|
14394
|
+
if (!existsInTarget) {
|
|
14395
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14396
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
14397
|
+
let elementID = sourcePoi.elementID;
|
|
14398
|
+
if (elementID && elementIds[elementID]) {
|
|
14399
|
+
elementID = elementIds[elementID];
|
|
14400
|
+
}
|
|
14401
|
+
const copiedTagIcon = await this.duplicateTagIcon(sourcePoi.tagIcon);
|
|
14402
|
+
const copiedTagImageID = await this.duplicateTagImageID(sourcePoi.tagImageID);
|
|
14403
|
+
const newPoi = {
|
|
14404
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
14405
|
+
type: sourcePoi.type,
|
|
14406
|
+
elementID: elementID,
|
|
14407
|
+
tagDescription: sourcePoi.tagDescription,
|
|
14408
|
+
tagIcon: copiedTagIcon,
|
|
14409
|
+
tagImageID: copiedTagImageID,
|
|
14410
|
+
virtual: sourcePoi.virtual,
|
|
14411
|
+
xRotation: sourcePoi.xRotation,
|
|
14412
|
+
yRotation: sourcePoi.yRotation,
|
|
14413
|
+
metadata: sourcePoi.metadata,
|
|
14414
|
+
matterportSweepID: targetSweepId
|
|
14415
|
+
};
|
|
14416
|
+
await this.poiService.createPOIForSpaceID(newPoi);
|
|
14417
|
+
}
|
|
14418
|
+
}
|
|
14419
|
+
catch (itemError) {
|
|
14420
|
+
console.error(`Error copying POI (sweep: ${sourcePoi?.matterportSweepID}):`, itemError);
|
|
14421
|
+
errorCount++;
|
|
14422
|
+
this.logCopyItemError(itemError, 'errorPoi', sourcePoi?.matterportSweepID);
|
|
14423
|
+
}
|
|
14424
|
+
processedOverall++;
|
|
14425
|
+
emitProgress('pois', i + 1, sourcePois.length);
|
|
14426
|
+
const pct = sourcePois.length > 0 ? Math.round(((i + 1) / sourcePois.length) * 100) : 100;
|
|
14427
|
+
this.setStepProgress('pois', 'pois', pct);
|
|
14428
|
+
}
|
|
14429
|
+
// OBJECTS 3D (the 3D model file itself — `path` — is physically duplicated in S3)
|
|
14430
|
+
for (let i = 0; i < sourceObjects3D.length; i++) {
|
|
14431
|
+
const sourceObj = sourceObjects3D[i];
|
|
14432
|
+
try {
|
|
14433
|
+
const existsInTarget = targetObjects3D.some(targetObj => targetObj.name === sourceObj.name);
|
|
14434
|
+
if (!existsInTarget) {
|
|
14435
|
+
const rawSource = sourceObj;
|
|
14436
|
+
let copiedPath = rawSource.path;
|
|
14437
|
+
if (rawSource.path) {
|
|
14438
|
+
copiedPath = await this.copyS3KeyIfPresent(rawSource.path, `3D object "${rawSource.name}"`);
|
|
14439
|
+
}
|
|
14440
|
+
/**
|
|
14441
|
+
* Strict parser to ensure [float, float, float] format for GraphQL.
|
|
14442
|
+
* Forces numbers, handles potential DynamoDB object structures,
|
|
14443
|
+
* and ensures we never send [0,0,0] for scale.
|
|
14444
|
+
*/
|
|
14445
|
+
const toFloatArray = (val, defaultVal = [0, 0, 0]) => {
|
|
14446
|
+
let array = [];
|
|
14447
|
+
if (Array.isArray(val)) {
|
|
14448
|
+
// If DB format [ {N: "1"}, ... ]
|
|
14449
|
+
if (val.length > 0 && typeof val[0] === 'object' && 'N' in val[0]) {
|
|
14450
|
+
array = val.map(item => parseFloat(item.N));
|
|
14451
|
+
}
|
|
14452
|
+
// If already standard array [1, 1, 1]
|
|
14453
|
+
else {
|
|
14454
|
+
array = val.map(item => parseFloat(item));
|
|
14455
|
+
}
|
|
14456
|
+
}
|
|
14457
|
+
// If object {x, y, z}
|
|
14458
|
+
else if (val && typeof val === 'object') {
|
|
14459
|
+
array = [parseFloat(val.x), parseFloat(val.y), parseFloat(val.z)];
|
|
14460
|
+
}
|
|
14461
|
+
else {
|
|
14462
|
+
return defaultVal;
|
|
14463
|
+
}
|
|
14464
|
+
// Ensure we return valid numbers
|
|
14465
|
+
return array.map(n => isNaN(n) ? 0 : n);
|
|
14466
|
+
};
|
|
14467
|
+
const object3DInput = {
|
|
14468
|
+
path: copiedPath,
|
|
14469
|
+
name: rawSource.name,
|
|
14470
|
+
description: rawSource.description || "",
|
|
14471
|
+
viewFrustum: !!rawSource.viewFrustum,
|
|
14472
|
+
spaceID: targetSpaceId,
|
|
14473
|
+
position: toFloatArray(rawSource.position, [0, 0, 0]),
|
|
14474
|
+
rotation: toFloatArray(rawSource.rotation, [0, 0, 0]),
|
|
14475
|
+
// Enforce scale to be at least 1.0 if it's invalid
|
|
14476
|
+
scale: toFloatArray(rawSource.scale, [1, 1, 1])
|
|
14477
|
+
};
|
|
14478
|
+
// Debugging: Inspect the exact object sent to the mutation
|
|
14479
|
+
console.log(`[DEBUG] Sending Object3D "${rawSource.name}" to API:`, JSON.stringify(object3DInput));
|
|
14480
|
+
const createdObj = await this.object3DService.create(object3DInput);
|
|
14481
|
+
if (createdObj && createdObj.id) {
|
|
14482
|
+
elementIds[sourceObj.id] = createdObj.id;
|
|
14483
|
+
}
|
|
14484
|
+
if (sourceObj.pois?.items && sourceObj.pois.items.length > 0) {
|
|
14485
|
+
const sourcePoi = sourceObj.pois.items[0];
|
|
14486
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14487
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
14488
|
+
const poiInput = {
|
|
14489
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
14490
|
+
type: sourcePoi.type,
|
|
14491
|
+
elementID: createdObj.id,
|
|
14492
|
+
metadata: sourcePoi.metadata,
|
|
14493
|
+
matterportSweepID: targetSweepId
|
|
14494
|
+
};
|
|
14495
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
14496
|
+
}
|
|
14497
|
+
}
|
|
14498
|
+
}
|
|
14499
|
+
catch (itemError) {
|
|
14500
|
+
console.error(`Error copying object3D "${sourceObj?.name}":`, itemError);
|
|
14501
|
+
errorCount++;
|
|
14502
|
+
this.logCopyItemError(itemError, 'errorObject3d', sourceObj?.name || sourceObj?.id);
|
|
14503
|
+
}
|
|
14504
|
+
processedOverall++;
|
|
14505
|
+
emitProgress('objects3d', i + 1, sourceObjects3D.length);
|
|
14506
|
+
const pct = sourceObjects3D.length > 0 ? Math.round(((i + 1) / sourceObjects3D.length) * 100) : 100;
|
|
14507
|
+
this.setStepProgress('objects3d', 'objects3d', pct);
|
|
14508
|
+
}
|
|
14509
|
+
// FEATURES (title, POI, and attached comments — images/videos/docs/links — all copied,
|
|
14510
|
+
// each file physically duplicated in S3 first)
|
|
14511
|
+
for (let i = 0; i < sourceFeatures.length; i++) {
|
|
14512
|
+
const sourceObj = sourceFeatures[i];
|
|
14513
|
+
try {
|
|
14514
|
+
const existsInTarget = this.matchesExisting(sourceObj, targetFeatures);
|
|
14515
|
+
if (!existsInTarget) {
|
|
14516
|
+
const rawSource = sourceObj;
|
|
14517
|
+
const featureInput = {
|
|
14518
|
+
title: rawSource.title,
|
|
14519
|
+
shortDescription: rawSource.shortDescription,
|
|
14520
|
+
level: rawSource.level,
|
|
14521
|
+
type: rawSource.type,
|
|
14522
|
+
archivedForMissionID: rawSource.archivedForMissionID || null,
|
|
14523
|
+
stripeProductID: rawSource.stripeProductID || null,
|
|
14524
|
+
stripePriceID: rawSource.stripePriceID || null,
|
|
14525
|
+
bookingPriceMetadata: rawSource.bookingPriceMetadata || null,
|
|
14526
|
+
locale: rawSource.locale || null,
|
|
14527
|
+
hashtags: rawSource.hashtags || null,
|
|
14528
|
+
description: rawSource.description || null,
|
|
14529
|
+
domainID: null,
|
|
14530
|
+
spaceID: targetSpaceId,
|
|
14531
|
+
ownerMissionID: targetMissionId
|
|
14532
|
+
};
|
|
14533
|
+
const createdObj = await this.featureService.createFeature(featureInput);
|
|
14534
|
+
if (createdObj && createdObj.id) {
|
|
14535
|
+
elementIds[sourceObj.id] = createdObj.id;
|
|
14536
|
+
}
|
|
14537
|
+
if (createdObj && sourceObj.comments?.items && sourceObj.comments.items.length > 0) {
|
|
14538
|
+
for (const sourceComment of sourceObj.comments.items) {
|
|
14539
|
+
try {
|
|
14540
|
+
const rawComment = sourceComment;
|
|
14541
|
+
let copiedAnnexe = null;
|
|
14542
|
+
if (rawComment.annexe) {
|
|
14543
|
+
copiedAnnexe = await this.copyS3KeyIfPresent(rawComment.annexe, `feature attachment "${rawComment.title}"`);
|
|
14544
|
+
}
|
|
14545
|
+
const commentInput = {
|
|
14546
|
+
title: rawComment.title,
|
|
14547
|
+
type: rawComment.type,
|
|
14548
|
+
annexe: copiedAnnexe,
|
|
14549
|
+
externalLink: rawComment.externalLink || null,
|
|
14550
|
+
dimensions: rawComment.dimensions || null,
|
|
14551
|
+
index: rawComment.index,
|
|
14552
|
+
shownInTag: rawComment.shownInTag,
|
|
14553
|
+
isCoverImage: rawComment.isCoverImage,
|
|
14554
|
+
featureID: createdObj.id,
|
|
14555
|
+
spaceID: targetSpaceId,
|
|
14556
|
+
ownerID: targetMissionId
|
|
14557
|
+
};
|
|
14558
|
+
await this.commentService.createComment(commentInput);
|
|
14559
|
+
}
|
|
14560
|
+
catch (attachmentError) {
|
|
14561
|
+
console.error(`Error copying attachment "${sourceComment?.title}" for feature "${sourceObj?.title}":`, attachmentError);
|
|
14562
|
+
errorCount++;
|
|
14563
|
+
this.logCopyItemError(attachmentError, 'errorFeatureAttachment', sourceComment?.title || sourceComment?.id);
|
|
14564
|
+
}
|
|
14565
|
+
}
|
|
14566
|
+
}
|
|
14567
|
+
if (sourceObj.pois?.items && sourceObj.pois.items.length > 0) {
|
|
14568
|
+
const sourcePoi = sourceObj.pois.items[0];
|
|
14569
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14570
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
14571
|
+
const poiInput = {
|
|
14572
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
14573
|
+
type: sourcePoi.type,
|
|
14574
|
+
elementID: createdObj.id,
|
|
14575
|
+
metadata: sourcePoi.metadata,
|
|
14576
|
+
matterportSweepID: targetSweepId
|
|
14577
|
+
};
|
|
14578
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
14579
|
+
}
|
|
14580
|
+
}
|
|
14581
|
+
}
|
|
14582
|
+
catch (itemError) {
|
|
14583
|
+
console.error(`Error copying feature "${sourceObj?.title}" (id: ${sourceObj?.id}):`, itemError);
|
|
14584
|
+
errorCount++;
|
|
14585
|
+
this.logCopyItemError(itemError, 'errorFeature', sourceObj?.title || sourceObj?.id);
|
|
14586
|
+
}
|
|
14587
|
+
processedOverall++;
|
|
14588
|
+
emitProgress('features', i + 1, sourceFeatures.length);
|
|
14589
|
+
const pct = sourceFeatures.length > 0 ? Math.round(((i + 1) / sourceFeatures.length) * 100) : 100;
|
|
14590
|
+
this.setStepProgress('features', 'features', pct);
|
|
14591
|
+
}
|
|
14592
|
+
// MEASUREMENTS (IMMO only — a Comment + a positional-only POI, no rotation;
|
|
14593
|
+
// the screenshot file is physically duplicated in S3 first)
|
|
14594
|
+
if (this.spModule == SpModule.IMMO) {
|
|
14595
|
+
for (let i = 0; i < sourceMeasurements.length; i++) {
|
|
14596
|
+
const sourceMeasurement = sourceMeasurements[i];
|
|
14597
|
+
try {
|
|
14598
|
+
const existsInTarget = targetMeasurements.some(tm => tm.comment.title === sourceMeasurement.comment.title);
|
|
14599
|
+
if (!existsInTarget) {
|
|
14600
|
+
const rawComment = sourceMeasurement.comment;
|
|
14601
|
+
let copiedAnnexe = null;
|
|
14602
|
+
if (rawComment.annexe) {
|
|
14603
|
+
copiedAnnexe = await this.copyS3KeyIfPresent(rawComment.annexe, `measurement screenshot "${rawComment.title}"`);
|
|
14604
|
+
}
|
|
14605
|
+
const commentInput = {
|
|
14606
|
+
title: rawComment.title,
|
|
14607
|
+
type: CommentType.MEASURE,
|
|
14608
|
+
dimensions: rawComment.dimensions || null,
|
|
14609
|
+
annexe: copiedAnnexe,
|
|
14610
|
+
spaceID: targetSpaceId,
|
|
14611
|
+
};
|
|
14612
|
+
const createdComment = await this.commentService.createComment(commentInput);
|
|
14613
|
+
if (createdComment && sourceMeasurement.poi) {
|
|
14614
|
+
const targetSweepId = this.findClosestTargetSweepId(sourceMeasurement.poi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14615
|
+
const adjustedPosition = this.getAdjustedPositionOnly(sourceMeasurement.poi.coordinate, transform);
|
|
14616
|
+
const poiInput = {
|
|
14617
|
+
coordinate: JSON.stringify(adjustedPosition),
|
|
14618
|
+
type: PoiType.MEASURE,
|
|
14619
|
+
elementID: createdComment.id,
|
|
14620
|
+
metadata: sourceMeasurement.poi.metadata,
|
|
14621
|
+
matterportSweepID: targetSweepId
|
|
14622
|
+
};
|
|
14623
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
14624
|
+
}
|
|
14625
|
+
}
|
|
14626
|
+
}
|
|
14627
|
+
catch (itemError) {
|
|
14628
|
+
console.error(`Error copying measurement "${sourceMeasurement?.comment?.title}":`, itemError);
|
|
14629
|
+
errorCount++;
|
|
14630
|
+
this.logCopyItemError(itemError, 'errorMeasurement', sourceMeasurement?.comment?.title || sourceMeasurement?.id);
|
|
14631
|
+
}
|
|
14632
|
+
processedOverall++;
|
|
14633
|
+
emitProgress('measurements', i + 1, sourceMeasurements.length);
|
|
14634
|
+
const pct = sourceMeasurements.length > 0 ? Math.round(((i + 1) / sourceMeasurements.length) * 100) : 100;
|
|
14635
|
+
this.setStepProgress('measurements', 'measurements', pct);
|
|
14636
|
+
}
|
|
14637
|
+
}
|
|
14638
|
+
this.copyProgress$.next({
|
|
14639
|
+
phase: 'done',
|
|
14640
|
+
processedInPhase: 0, totalInPhase: 0,
|
|
14641
|
+
processedOverall, totalOverall,
|
|
14642
|
+
percent: 100, errorCount
|
|
14643
|
+
});
|
|
14644
|
+
this.setStepLog('done', errorCount > 0
|
|
14645
|
+
? this.translate.instant('matterportCopy.doneWithErrors', { count: errorCount })
|
|
14646
|
+
: this.translate.instant('matterportCopy.done'));
|
|
14647
|
+
return true;
|
|
14648
|
+
}
|
|
14649
|
+
catch (error) {
|
|
14650
|
+
console.error('Error copying POIs:', error);
|
|
14651
|
+
this.appendErrorLog(this.translate.instant('matterportCopy.errorGeneral', { error: toErrorMessage(error) }));
|
|
14652
|
+
this.copyProgress$.next(null);
|
|
14653
|
+
return false;
|
|
14654
|
+
}
|
|
14655
|
+
}
|
|
14656
|
+
//
|
|
14657
|
+
// ---------- Progress / log helpers ----------
|
|
14658
|
+
//
|
|
14659
|
+
/** Adds or updates a step line (identified by stepId) with new text, then republishes the log. */
|
|
14660
|
+
setStepLog(stepId, text) {
|
|
14661
|
+
const existingIndex = this.log.findIndex(e => e.type === 'step' && e.id === stepId);
|
|
14662
|
+
const entry = { id: stepId, type: 'step', text };
|
|
14663
|
+
if (existingIndex >= 0) {
|
|
14664
|
+
this.log[existingIndex] = entry;
|
|
14665
|
+
}
|
|
14666
|
+
else {
|
|
14667
|
+
this.log.push(entry);
|
|
14668
|
+
}
|
|
14669
|
+
this.copyLog$.next([...this.log]);
|
|
14670
|
+
}
|
|
14671
|
+
/** Appends a new, permanent log line (never overwritten), then republishes the log. */
|
|
14672
|
+
appendErrorLog(text) {
|
|
14673
|
+
this.errorSeq++;
|
|
14674
|
+
this.log.push({ id: `error-${this.errorSeq}`, type: 'error', text });
|
|
14675
|
+
this.copyLog$.next([...this.log]);
|
|
14676
|
+
}
|
|
14677
|
+
/** Resets the log for a new run. */
|
|
14678
|
+
resetLog() {
|
|
14679
|
+
this.log = [];
|
|
14680
|
+
this.errorSeq = 0;
|
|
14681
|
+
this.copyLog$.next([]);
|
|
14682
|
+
}
|
|
14683
|
+
/** Builds and sets a step's log line as "<translated label>: <percent>%", no step numbering. */
|
|
14684
|
+
setStepProgress(stepId, translationKey, percent) {
|
|
14685
|
+
const label = this.translate.instant(`matterportCopy.${translationKey}`);
|
|
14686
|
+
this.setStepLog(stepId, `${label}: ${percent}%`);
|
|
14687
|
+
}
|
|
14688
|
+
/** Logs a translated, parameterized error line for a single failed item. */
|
|
14689
|
+
logItemError(translationKey, params) {
|
|
14690
|
+
const text = this.translate.instant(`matterportCopy.${translationKey}`, params);
|
|
14691
|
+
this.appendErrorLog(text);
|
|
14692
|
+
}
|
|
14693
|
+
/** Logs an item-level error, using a specific "missing file / orphaned reference"
|
|
14694
|
+
* translation key when the cause is a MissingS3FileError, and the generic per-type
|
|
14695
|
+
* key otherwise. Keeps the copy log readable: a missing file is not a bug to
|
|
14696
|
+
* investigate the same way a network/permission error is. */
|
|
14697
|
+
logCopyItemError(error, genericKey, itemLabel) {
|
|
14698
|
+
if (error instanceof MissingS3FileError) {
|
|
14699
|
+
this.logItemError('errorMissingS3FileSkipped', {
|
|
14700
|
+
name: itemLabel,
|
|
14701
|
+
file: error.sourceKey
|
|
14702
|
+
});
|
|
14703
|
+
}
|
|
14704
|
+
else {
|
|
14705
|
+
this.logItemError(genericKey, { name: itemLabel, error: toErrorMessage(error) });
|
|
14706
|
+
}
|
|
14707
|
+
}
|
|
14708
|
+
//
|
|
14709
|
+
// ---------- Duplicate detection ----------
|
|
14710
|
+
//
|
|
14711
|
+
/**
|
|
14712
|
+
* Heuristic duplicate-detection: compares title AND description together instead of
|
|
14713
|
+
* title alone, to reduce false positives when several source items happen to share
|
|
14714
|
+
* the same title. Not a perfect guarantee, but meaningfully more robust.
|
|
14715
|
+
*/
|
|
14716
|
+
matchesExisting(source, targetList) {
|
|
14717
|
+
return targetList.some(t => t.title === source.title && (t.description || null) === (source.description || null));
|
|
14718
|
+
}
|
|
14719
|
+
//
|
|
14720
|
+
// ---------- Sweep positions / rigid transform ----------
|
|
14721
|
+
//
|
|
14722
|
+
/**
|
|
14723
|
+
* Fetches the position of every sweep in a space and returns it as a Map for O(1) lookup.
|
|
14724
|
+
* Uses allSettled so a single failing sweep lookup doesn't abort the whole map —
|
|
14725
|
+
* that sweep is simply absent from the resulting map (and skipped later).
|
|
14726
|
+
*/
|
|
14727
|
+
async buildNavigationPositionMap(spaceId, sweepIds) {
|
|
14728
|
+
const map = new Map();
|
|
14729
|
+
const results = await Promise.allSettled(sweepIds.map(async (sid) => {
|
|
14730
|
+
const navs = await this.navigationService.getNavigationsForMatterportIDForSpace(sid, spaceId);
|
|
14731
|
+
if (navs && navs.length > 0 && navs[0].position) {
|
|
14732
|
+
return { sid, position: JSON.parse(navs[0].position) };
|
|
14733
|
+
}
|
|
14734
|
+
return null;
|
|
14735
|
+
}));
|
|
14736
|
+
for (const result of results) {
|
|
14737
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
14738
|
+
map.set(result.value.sid, result.value.position);
|
|
14739
|
+
}
|
|
14740
|
+
else if (result.status === 'rejected') {
|
|
14741
|
+
console.warn('Failed to fetch a sweep position, skipping it:', result.reason);
|
|
14742
|
+
}
|
|
14743
|
+
}
|
|
14744
|
+
return map;
|
|
14745
|
+
}
|
|
14746
|
+
/** Applies the rigid transform to a POI coordinate (position + orientation). */
|
|
14747
|
+
getAdjustedCoordinates(coordinate, transform) {
|
|
14748
|
+
if (coordinate === "null") {
|
|
14749
|
+
return null;
|
|
14750
|
+
}
|
|
14751
|
+
if (!coordinate || typeof coordinate !== 'string') {
|
|
14752
|
+
return coordinate;
|
|
14753
|
+
}
|
|
14754
|
+
const parsed = JSON.parse(coordinate);
|
|
14755
|
+
const rotatedPos = applyRigidTransformToPoint({ x: parsed.x, y: parsed.y, z: parsed.z }, transform);
|
|
14756
|
+
return {
|
|
14757
|
+
x: rotatedPos.x,
|
|
14758
|
+
y: rotatedPos.y,
|
|
14759
|
+
z: rotatedPos.z,
|
|
14760
|
+
xRotation: parsed.xRotation,
|
|
14761
|
+
yRotation: applyRigidTransformToYRotationDeg(parsed.yRotation, transform)
|
|
14762
|
+
};
|
|
14763
|
+
}
|
|
14764
|
+
/**
|
|
14765
|
+
* Applies the rigid transform's translation+rotation to a plain {x,y,z} position,
|
|
14766
|
+
* WITHOUT adding xRotation/yRotation keys. Used for measurements, whose stored
|
|
14767
|
+
* coordinate is a bare position (no orientation), unlike other POI types.
|
|
14768
|
+
*/
|
|
14769
|
+
getAdjustedPositionOnly(coordinate, transform) {
|
|
14770
|
+
if (!coordinate || typeof coordinate !== 'string' || coordinate === 'null') {
|
|
14771
|
+
return coordinate;
|
|
14772
|
+
}
|
|
14773
|
+
const parsed = JSON.parse(coordinate);
|
|
14774
|
+
return applyRigidTransformToPoint({ x: parsed.x, y: parsed.y, z: parsed.z }, transform);
|
|
14775
|
+
}
|
|
14776
|
+
/**
|
|
14777
|
+
* Finds the closest target sweep, comparing positions in the SAME coordinate frame.
|
|
14778
|
+
* Pure in-memory lookup — no network call, thanks to the pre-built position maps.
|
|
14779
|
+
*/
|
|
14780
|
+
findClosestTargetSweepId(sourceSweepID, sourceNavPositions, targetNavPositions, transform) {
|
|
14781
|
+
if (!sourceSweepID)
|
|
14782
|
+
return null;
|
|
14783
|
+
const sourcePos = sourceNavPositions.get(sourceSweepID);
|
|
14784
|
+
if (!sourcePos)
|
|
14785
|
+
return null;
|
|
14786
|
+
const sourcePosInTargetFrame = applyRigidTransformToPoint(sourcePos, transform);
|
|
14787
|
+
let closestSweepID = null;
|
|
14788
|
+
let minDistance = Infinity;
|
|
14789
|
+
for (const [targetSweepId, targetPos] of targetNavPositions.entries()) {
|
|
14790
|
+
const dx = targetPos.x - sourcePosInTargetFrame.x;
|
|
14791
|
+
const dy = targetPos.y - sourcePosInTargetFrame.y;
|
|
14792
|
+
const dz = targetPos.z - sourcePosInTargetFrame.z;
|
|
14793
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
14794
|
+
if (dist < minDistance) {
|
|
14795
|
+
minDistance = dist;
|
|
14796
|
+
closestSweepID = targetSweepId;
|
|
14797
|
+
}
|
|
14798
|
+
}
|
|
14799
|
+
return closestSweepID;
|
|
14800
|
+
}
|
|
14801
|
+
//
|
|
14802
|
+
// ---------- S3 file duplication ----------
|
|
14803
|
+
//
|
|
14804
|
+
/** Generates a short random id, used to give each copied S3 file a fresh, collision-safe key. */
|
|
14805
|
+
generateShortId() {
|
|
14806
|
+
return Math.random().toString(36).slice(2, 10);
|
|
14807
|
+
}
|
|
14808
|
+
/**
|
|
14809
|
+
* Builds a new S3 key for a copied attachment and sanitizes the filename
|
|
14810
|
+
* to prevent AWS Amplify SignatureDoesNotMatch errors.
|
|
14811
|
+
*/
|
|
14812
|
+
buildCopiedAttachmentKey(sourceAnnexe) {
|
|
14813
|
+
const lastSlash = sourceAnnexe.lastIndexOf('/');
|
|
14814
|
+
const folder = sourceAnnexe.substring(0, lastSlash + 1);
|
|
14815
|
+
let filename = sourceAnnexe.substring(lastSlash + 1);
|
|
14816
|
+
// Strip a leading "<8 hex chars>_" id prefix if present
|
|
14817
|
+
const prefixMatch = filename.match(/^[0-9a-fA-F-]{8}_(.+)$/);
|
|
14818
|
+
if (prefixMatch) {
|
|
14819
|
+
filename = prefixMatch[1];
|
|
14820
|
+
}
|
|
14821
|
+
// SANITIZATION: Replace spaces, parentheses, and accents with underscores
|
|
14822
|
+
// Crucial to prevent Amplify's Storage.copy from failing the signature calculation
|
|
14823
|
+
filename = filename.replace(/[^a-zA-Z0-9.\-_]/g, '_');
|
|
14824
|
+
const newPrefix = `${this.generateShortId()}_`;
|
|
14825
|
+
return `${folder}${newPrefix}${filename}`;
|
|
14826
|
+
}
|
|
14827
|
+
/**
|
|
14828
|
+
* Duplicates an S3 object if a key is present.
|
|
14829
|
+
* - If the source file is genuinely missing from S3 (NoSuchKey / orphaned DB reference),
|
|
14830
|
+
* throws a MissingS3FileError so the caller can log a clear, specific message and
|
|
14831
|
+
* skip the whole item entirely, rather than creating a record with a broken file link.
|
|
14832
|
+
* - Any other kind of failure (permissions, network, etc.) still throws a generic Error.
|
|
14833
|
+
*/
|
|
14834
|
+
async copyS3KeyIfPresent(key, contextLabel) {
|
|
14835
|
+
if (!key) {
|
|
14836
|
+
return null;
|
|
14837
|
+
}
|
|
14838
|
+
const sourceKey = this.extractS3KeyFromValue(key);
|
|
14839
|
+
const destKey = this.buildCopiedAttachmentKey(sourceKey);
|
|
14840
|
+
// Log the source key to ensure it matches the real S3 object path
|
|
14841
|
+
console.log(`[DEBUG] Attempting S3 copy from source: "${sourceKey}" to dest: "${destKey}"`);
|
|
14842
|
+
try {
|
|
14843
|
+
const copied = await copyFileInS3(sourceKey, destKey);
|
|
14844
|
+
if (!copied) {
|
|
14845
|
+
throw new MissingS3FileError(sourceKey, contextLabel);
|
|
14846
|
+
}
|
|
14847
|
+
return copied;
|
|
14848
|
+
}
|
|
14849
|
+
catch (error) {
|
|
14850
|
+
const isMissingFile = error instanceof MissingS3FileError
|
|
14851
|
+
|| error?.code === 'NoSuchKey'
|
|
14852
|
+
|| /NoSuchKey|does not exist/i.test(toErrorMessage(error));
|
|
14853
|
+
if (isMissingFile) {
|
|
14854
|
+
console.warn(`[MatterportTagCopyService] Orphaned S3 reference, skipping item: "${sourceKey}" (${contextLabel})`);
|
|
14855
|
+
throw new MissingS3FileError(sourceKey, contextLabel);
|
|
14856
|
+
}
|
|
14857
|
+
throw error;
|
|
14858
|
+
}
|
|
14859
|
+
}
|
|
14860
|
+
/**
|
|
14861
|
+
* Duplicates the file referenced by a POI's tagIcon (a JSON string shaped like
|
|
14862
|
+
* { src, opacity }, per form-feature.component.ts). If the JSON can't be parsed or
|
|
14863
|
+
* has no `src`, the value is returned unchanged rather than risk corrupting it.
|
|
14864
|
+
*/
|
|
14865
|
+
async duplicateTagIcon(tagIconRaw) {
|
|
14866
|
+
if (!tagIconRaw) {
|
|
14867
|
+
return tagIconRaw;
|
|
14868
|
+
}
|
|
14869
|
+
try {
|
|
14870
|
+
const parsed = JSON.parse(tagIconRaw);
|
|
14871
|
+
if (parsed && parsed.src) {
|
|
14872
|
+
parsed.src = await this.copyS3KeyIfPresent(parsed.src, 'tag icon');
|
|
14873
|
+
}
|
|
14874
|
+
return JSON.stringify(parsed);
|
|
14875
|
+
}
|
|
14876
|
+
catch {
|
|
14877
|
+
// Not JSON, or unexpected shape — leave untouched rather than guess.
|
|
14878
|
+
return tagIconRaw;
|
|
14879
|
+
}
|
|
14880
|
+
}
|
|
14881
|
+
/**
|
|
14882
|
+
* Duplicates tagImageID if it looks like a reference to a file (contains a path
|
|
14883
|
+
* separator, whether raw key or pre-signed URL). UNCONFIRMED ASSUMPTION: this
|
|
14884
|
+
* field's exact semantics weren't verified — if it's actually an opaque reference
|
|
14885
|
+
* to another DB record rather than a file, this heuristic may incorrectly attempt
|
|
14886
|
+
* a copy. Confirm the real structure to tighten this if it causes issues.
|
|
14887
|
+
*/
|
|
14888
|
+
async duplicateTagImageID(tagImageID) {
|
|
14889
|
+
if (!tagImageID || !tagImageID.includes('/')) {
|
|
14890
|
+
return tagImageID;
|
|
14891
|
+
}
|
|
14892
|
+
return this.copyS3KeyIfPresent(tagImageID, 'tag image');
|
|
14893
|
+
}
|
|
14894
|
+
/**
|
|
14895
|
+
* Extracts and normalizes the raw S3 key from a stored value (which may be a full URL
|
|
14896
|
+
* or already a raw key).
|
|
14897
|
+
* IMPORTANT: this must return the RAW (decoded, non URL-encoded) key. Amplify's
|
|
14898
|
+
* Storage.copy() already builds and encodes the 'x-amz-copy-source' header internally
|
|
14899
|
+
* from the key we give it. Pre-encoding it here caused a double-encoding bug (e.g. a
|
|
14900
|
+
* space became "%2520" instead of "%20"), so Storage.copy() looked for a key that never
|
|
14901
|
+
* existed on S3, resulting in a NoSuchKey error.
|
|
14902
|
+
*/
|
|
14903
|
+
extractS3KeyFromValue(value) {
|
|
14904
|
+
if (!value) {
|
|
14905
|
+
return '';
|
|
14906
|
+
}
|
|
14907
|
+
let extracted = value;
|
|
14908
|
+
// 1. Extract path from URL and drop query params
|
|
14909
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
14910
|
+
try {
|
|
14911
|
+
const url = new URL(value);
|
|
14912
|
+
extracted = decodeURIComponent(url.pathname);
|
|
14913
|
+
}
|
|
14914
|
+
catch {
|
|
14915
|
+
extracted = value.split('?')[0];
|
|
14916
|
+
}
|
|
14917
|
+
}
|
|
14918
|
+
else {
|
|
14919
|
+
extracted = value.split('?')[0];
|
|
14920
|
+
}
|
|
14921
|
+
// 2. Remove leading slash
|
|
14922
|
+
if (extracted.startsWith('/')) {
|
|
14923
|
+
extracted = extracted.substring(1);
|
|
14924
|
+
}
|
|
14925
|
+
// 3. Remove 'public/' prefix
|
|
14926
|
+
// Amplify prepends this automatically; we must strip it from the input
|
|
14927
|
+
// to avoid the 'public/public/' double-prefix issue.
|
|
14928
|
+
if (extracted.startsWith('public/')) {
|
|
14929
|
+
extracted = extracted.substring('public/'.length);
|
|
14930
|
+
}
|
|
14931
|
+
// Return the raw key as-is — Storage.copy() takes care of properly encoding it
|
|
14932
|
+
// for the CopySource header. Do NOT re-encode it here.
|
|
14933
|
+
return extracted;
|
|
14934
|
+
}
|
|
14935
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportTagCopyService, deps: [{ token: NavigationService }, { token: BaseUserService }, { token: PoiService }, { token: TicketsService }, { token: EquipmentService }, { token: Object3DService }, { token: FeatureService }, { token: MeasurementService }, { token: CommentService }, { token: i1.TranslateService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
14936
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportTagCopyService, providedIn: 'root' });
|
|
14937
|
+
}
|
|
14938
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportTagCopyService, decorators: [{
|
|
14939
|
+
type: Injectable,
|
|
14940
|
+
args: [{ providedIn: 'root' }]
|
|
14941
|
+
}], ctorParameters: () => [{ type: NavigationService }, { type: BaseUserService }, { type: PoiService }, { type: TicketsService }, { type: EquipmentService }, { type: Object3DService }, { type: FeatureService }, { type: MeasurementService }, { type: CommentService }, { type: i1.TranslateService }] });
|
|
14942
|
+
|
|
13509
14943
|
/* eslint-disable class-methods-use-this */
|
|
13510
14944
|
class OrganisationService {
|
|
13511
14945
|
smarterPlanID = "6e28201f-4679-4c29-9d2d-8a325f57cee9";
|
|
@@ -13713,5 +15147,5 @@ function floatValidator() {
|
|
|
13713
15147
|
* Generated bundle index. Do not edit.
|
|
13714
15148
|
*/
|
|
13715
15149
|
|
|
13716
|
-
export { AffectationService, AvatarComponent, BaseLateralTabService, BaseTagService, BaseUserService, BaseVisibilityService, CameraMode, CaptureService, CaptureViewer, CommentService, CommentType, Config, ContentService, CsvExportComponent, DomainService, DomainType, DurationToStringPipe, EmailStatus, EquipmentService, EventService, EventStatus, EventType, FeatureService, FeatureType, FilterService, HashtagFromIdPipe, HashtagService, InterventionService, InventoryStatus, LayerService, LevelStatus, LoaderComponent, Locale, LocaleService, MATTERPORT_SDK_KEY, MatterportImportService, MatterportService, MattertagActionMode, MattertagData, MeasurementService, MenuBarComponent, MissionService, NavigationService, NavigatorService, NgxSmarterplanCoreModule, NgxSmarterplanCoreService, NodeService, Object3DService, OperationService, OrganisationService, PaymentStatus, PlanService, PoiService, PoiType, ProfileEntity, ProfileService, ProfileStatus, PropertyService, PropertyType, RoleStatus, SafeUrlPipe, SearchBarComponent, SearchObjectType, SearchService, SpModule, SpaceService, SpaceStatus, StatusEquipment, SupportModalComponent, SupportService, TagAction, TemplateService, TicketPriority, TicketStatus, TicketType, TicketsService, TimeDateToLocalStringPipe, TypeNote, UsernameFromIdPipe, ValidatorsService, ViewerInteractions, ViewerService, VisitService, ZoneChangeService, ZoneService, arraysContainSameElements, checkElementById, convertTimestampToLocalZone, dateHasExpired, dateTimeToLocalString, deleteFromS3, downloadBlob, downloadEquipmentDocument, downloadFile, downloadFileAsObject, durationToString, emailValidator, enumToArray, filterUniqueArrayByID, floatValidator, getBufferForFileFromS3, getCoefficientsForImage, getCurrentLang, getDistanceBetweenTwoPoints, getHighestLevelForMissions, getHighestRoleForMissions, getLevelsBelow, getLocaleLong, getLocaleShort, getMetaForImage, getRolesBelowForManager, getSignedFile, getSignedImageUrlForEquipment, getSignedImageUrlForProfile, getSignedImageUrlForSpace, getSpaceIDFromUrl, getVisitUrl, isEmptyObject, listFilesInFolder, mean, noEmptyValidator, numberToDateString, openDocument, openModalForVisitSwitch, poiTypeToString, pointToString, removeAllFilesFromFolderS3, removeNullKeysFromObject, showScanPointsOnPlanInDiv, shuffleArray, sortAlphabeticallyOnName, stringLocaleToEnum, stringToLowercaseNoSpaces, styleButton, textValidator, translateDatePeriod, uploadBase64Image, uploadBase64ImageWithRetry, uploadFileToS3, uploadJsonToS3, validEmail, wait, waitUntil };
|
|
15150
|
+
export { AffectationService, AvatarComponent, BaseLateralTabService, BaseTagService, BaseUserService, BaseVisibilityService, CameraMode, CaptureService, CaptureViewer, CommentService, CommentType, Config, ContentService, CsvExportComponent, DomainService, DomainType, DurationToStringPipe, EmailStatus, EquipmentService, EventService, EventStatus, EventType, FeatureService, FeatureType, FilterService, HashtagFromIdPipe, HashtagService, InterventionService, InventoryStatus, LayerService, LevelStatus, LoaderComponent, Locale, LocaleService, MATTERPORT_SDK_KEY, MatterportImportService, MatterportService, MatterportTagCopyService, MatterportViewType, MattertagActionMode, MattertagData, MeasurementService, MenuBarComponent, MissionService, NavigationService, NavigatorService, NgxSmarterplanCoreModule, NgxSmarterplanCoreService, NodeService, Object3DService, OperationService, OrganisationService, PaymentStatus, PlanService, PoiService, PoiType, ProfileEntity, ProfileService, ProfileStatus, PropertyService, PropertyType, RoleStatus, SafeUrlPipe, SearchBarComponent, SearchObjectType, SearchService, SpModule, SpaceService, SpaceStatus, StatusEquipment, SupportModalComponent, SupportService, TagAction, TemplateService, TicketPriority, TicketStatus, TicketType, TicketsService, TimeDateToLocalStringPipe, TypeNote, UsernameFromIdPipe, ValidatorsService, ViewerInteractions, ViewerService, VisitService, ZoneChangeService, ZoneService, applyRigidTransformToPoint, applyRigidTransformToYRotationDeg, arraysContainSameElements, checkElementById, computeRigidTransform, convertTimestampToLocalZone, copyFileInS3, dateHasExpired, dateTimeToLocalString, deleteFromS3, distance, downloadBlob, downloadEquipmentDocument, downloadFile, downloadFileAsObject, durationToString, emailValidator, enumToArray, filterUniqueArrayByID, floatValidator, getBufferForFileFromS3, getCoefficientsForImage, getCurrentLang, getDistanceBetweenTwoPoints, getHighestLevelForMissions, getHighestRoleForMissions, getLevelsBelow, getLocaleLong, getLocaleShort, getMetaForImage, getRolesBelowForManager, getSignedFile, getSignedImageUrlForEquipment, getSignedImageUrlForProfile, getSignedImageUrlForSpace, getSpaceIDFromUrl, getVisitUrl, isEmptyObject, listFilesInFolder, mean, noEmptyValidator, numberToDateString, openDocument, openModalForVisitSwitch, poiTypeToString, pointToString, removeAllFilesFromFolderS3, removeNullKeysFromObject, showScanPointsOnPlanInDiv, shuffleArray, sortAlphabeticallyOnName, stringLocaleToEnum, stringToLowercaseNoSpaces, styleButton, textValidator, translateDatePeriod, uploadBase64Image, uploadBase64ImageWithRetry, uploadFileToS3, uploadJsonToS3, validEmail, wait, waitUntil };
|
|
13717
15151
|
//# sourceMappingURL=smarterplan-ngx-smarterplan-core.mjs.map
|