@smarterplan/ngx-smarterplan-core 1.4.21 → 1.4.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -24
- package/esm2022/lib/components/csv-export/csv-export.component.mjs +3 -3
- package/esm2022/lib/components/loader/loader.component.mjs +3 -3
- package/esm2022/lib/components/menu-bar/avatar/avatar.component.mjs +1 -1
- package/esm2022/lib/components/menu-bar/menu-bar.component.mjs +3 -3
- package/esm2022/lib/components/menu-bar/navigation-bar/navigation-bar.component.mjs +3 -3
- package/esm2022/lib/components/modal-switch-visit/modal-switch-visit.component.mjs +3 -3
- package/esm2022/lib/components/search-bar/search-bar.component.mjs +3 -3
- package/esm2022/lib/components/splat-viewer/splat-viewer.component.mjs +361 -0
- package/esm2022/lib/components/support-modal/support-modal.component.mjs +3 -3
- package/esm2022/lib/config.mjs +1 -1
- package/esm2022/lib/helpers.service.mjs +1 -1
- package/esm2022/lib/matterport-extensions/nest-thermostat/CanvasImage.mjs +1 -1
- package/esm2022/lib/matterport-extensions/nest-thermostat/CanvasRenderer.mjs +1 -1
- package/esm2022/lib/matterport-extensions/nest-thermostat/NestThermostat.mjs +1 -1
- package/esm2022/lib/matterport-extensions/nest-thermostat/PlaneRenderer.mjs +1 -1
- package/esm2022/lib/matterport-extensions/scene-component/SceneComponent.mjs +1 -1
- package/esm2022/lib/matterport-extensions/security-camera/SecurityCamera.mjs +22 -22
- package/esm2022/lib/matterport-extensions/tv-player/TvPlayer.mjs +1 -1
- package/esm2022/lib/matterport-extensions/view-frustum-mesh/ViewFrustumMesh.mjs +22 -22
- package/esm2022/lib/mattertagData.mjs +1 -1
- package/esm2022/lib/ngx-smarterplan-core.module.mjs +9 -4
- package/esm2022/lib/ngx-smarterplan-core.service.mjs +1 -1
- package/esm2022/lib/pipes/duration-to-string.pipe.mjs +1 -1
- package/esm2022/lib/pipes/format-date-number-to-digits.pipe.mjs +1 -1
- package/esm2022/lib/pipes/hashtag-from-id.pipe.mjs +1 -1
- package/esm2022/lib/pipes/safe-url.pipe.mjs +1 -1
- package/esm2022/lib/pipes/time-date-to-local-string.pipe.mjs +1 -1
- package/esm2022/lib/pipes/username-from-id.pipe.mjs +1 -1
- package/esm2022/lib/services/amplify-cache.service.mjs +1 -1
- package/esm2022/lib/services/base-tab.service.mjs +1 -1
- package/esm2022/lib/services/baseVisibility.service.mjs +1 -1
- package/esm2022/lib/services/content.service.mjs +1 -1
- package/esm2022/lib/services/filter.service.mjs +1 -1
- package/esm2022/lib/services/intervention.service.mjs +1 -1
- package/esm2022/lib/services/locale.service.mjs +1 -1
- package/esm2022/lib/services/matterport-import.service.mjs +1 -1
- package/esm2022/lib/services/matterport-measurement.service.mjs +1 -1
- package/esm2022/lib/services/matterport-navigation.service.mjs +1 -1
- package/esm2022/lib/services/matterport-pointer.service.mjs +6 -6
- package/esm2022/lib/services/matterport-tag-copy.service.mjs +1 -1
- package/esm2022/lib/services/matterport-tag.service.mjs +12 -7
- package/esm2022/lib/services/matterport.service.mjs +17 -17
- package/esm2022/lib/services/models/affectation.service.mjs +1 -1
- package/esm2022/lib/services/models/base-object.service.mjs +1 -1
- package/esm2022/lib/services/models/capture.service.mjs +1 -1
- package/esm2022/lib/services/models/comment.service.mjs +1 -1
- package/esm2022/lib/services/models/domain.service.mjs +1 -1
- package/esm2022/lib/services/models/equipment.service.mjs +1 -1
- package/esm2022/lib/services/models/event.service.mjs +1 -1
- package/esm2022/lib/services/models/feature.service.mjs +1 -1
- package/esm2022/lib/services/models/hashtag.service.mjs +1 -1
- package/esm2022/lib/services/models/layer.service.mjs +1 -1
- package/esm2022/lib/services/models/measurement.service.mjs +1 -1
- package/esm2022/lib/services/models/mission.service.mjs +1 -1
- package/esm2022/lib/services/models/navigation.service.mjs +1 -1
- package/esm2022/lib/services/models/node.service.mjs +1 -1
- package/esm2022/lib/services/models/object3D.service.mjs +1 -1
- package/esm2022/lib/services/models/operation.service.mjs +1 -1
- package/esm2022/lib/services/models/organisation.service.mjs +1 -1
- package/esm2022/lib/services/models/plan.service.mjs +1 -1
- package/esm2022/lib/services/models/poi.service.mjs +27 -27
- package/esm2022/lib/services/models/profile.service.mjs +1 -1
- package/esm2022/lib/services/models/property.service.mjs +1 -1
- package/esm2022/lib/services/models/space.service.mjs +1 -1
- package/esm2022/lib/services/models/template.service.mjs +1 -1
- package/esm2022/lib/services/models/ticket.service.mjs +1 -1
- package/esm2022/lib/services/models/visit.service.mjs +1 -1
- package/esm2022/lib/services/models/zone.service.mjs +1 -1
- package/esm2022/lib/services/navigator.service.mjs +1 -1
- package/esm2022/lib/services/obj-to-glb.mjs +649 -0
- package/esm2022/lib/services/s3.service.mjs +43 -3
- package/esm2022/lib/services/search.service.mjs +1 -1
- package/esm2022/lib/services/support.service.mjs +1 -1
- package/esm2022/lib/services/sweep-locator.service.mjs +1 -1
- package/esm2022/lib/services/tag.service.mjs +16 -16
- package/esm2022/lib/services/threejs-import.service.mjs +6 -4
- package/esm2022/lib/services/threejs.service.mjs +867 -95
- package/esm2022/lib/services/user.service.mjs +1 -1
- package/esm2022/lib/services/utils/matterport-transform.util.mjs +1 -1
- package/esm2022/lib/services/validators.service.mjs +1 -1
- package/esm2022/lib/services/viewer.service.mjs +189 -7
- package/esm2022/lib/services/zone-drawer.service.mjs +1 -1
- package/esm2022/lib/services/zoneChange.service.mjs +1 -1
- package/esm2022/lib/tokens/matterport-sdk-key.token.mjs +1 -1
- package/esm2022/lib/validators/email.directive.mjs +1 -1
- package/esm2022/lib/validators/no-empty.directive.mjs +1 -1
- package/esm2022/lib/validators/number.directive.mjs +1 -1
- package/esm2022/lib/validators/text.directive.mjs +1 -1
- package/esm2022/public-api.mjs +3 -1
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs +2239 -233
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs.map +1 -1
- package/lib/components/splat-viewer/splat-viewer.component.d.ts +98 -0
- package/lib/ngx-smarterplan-core.module.d.ts +7 -6
- package/lib/services/matterport-tag.service.d.ts +5 -5
- package/lib/services/obj-to-glb.d.ts +66 -0
- package/lib/services/s3.service.d.ts +7 -0
- package/lib/services/threejs.service.d.ts +336 -16
- package/lib/services/viewer.service.d.ts +110 -8
- package/package.json +1 -1
- package/public-api.d.ts +2 -0
- package/src/assets/viewers/splat/chunk-BYLI4YBI.js +17037 -0
- package/src/assets/viewers/splat/chunk-LDV6YPMG.js +1 -0
- package/src/assets/viewers/splat/chunk-WZ5Z4TU3.js +105719 -0
- package/src/assets/viewers/splat/convert-4DRLH77X.js +1 -0
- package/src/assets/viewers/splat/convert.html +11 -0
- package/src/assets/viewers/splat/converter-SLBB5MHG.js +1 -0
- package/src/assets/viewers/splat/index.html +90 -0
- package/src/assets/viewers/splat/spz-DEYRea_v-QE6YSGIB.js +5292 -0
- package/src/assets/viewers/splat/viewer-W4X3FWQJ.js +2704 -0
- package/src/assets/viewers/splat/viewer.css +1096 -0
- package/src/assets/viewers/splat/webp.wasm +0 -0
- package/src/assets/viewers/splat/worker.mjs +1383 -0
- package/src/assets/vs-app.json +67 -67
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, Input, Component, InjectionToken, Inject, EventEmitter, isDevMode, Output, Pipe, ViewChild, NgModule } from '@angular/core';
|
|
2
|
+
import { Injectable, Input, Component, InjectionToken, Inject, EventEmitter, isDevMode, Output, Pipe, ViewChild, HostListener, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@ngx-translate/core';
|
|
4
4
|
import { TranslateModule } from '@ngx-translate/core';
|
|
5
5
|
import { Subject, BehaviorSubject, takeUntil, Observable } from 'rxjs';
|
|
@@ -90,11 +90,11 @@ class CsvExportComponent {
|
|
|
90
90
|
link.remove();
|
|
91
91
|
}
|
|
92
92
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CsvExportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
93
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: CsvExportComponent, selector: "lib-csv-export", inputs: { headers: "headers", rowsInput: "rowsInput", getRows: "getRows", title: "title", buttonLabel: "buttonLabel" }, ngImport: i0, template: "<span [style.cursor]=\"'pointer'\" (click)=\"onClickDownload()\" class=\"download-csv d-flex\">\
|
|
93
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: CsvExportComponent, selector: "lib-csv-export", inputs: { headers: "headers", rowsInput: "rowsInput", getRows: "getRows", title: "title", buttonLabel: "buttonLabel" }, ngImport: i0, template: "<span [style.cursor]=\"'pointer'\" (click)=\"onClickDownload()\" class=\"download-csv d-flex\">\n <span class=\"iconify\" data-icon=\"mdi:file-download-outline\"></span>\n {{ buttonLabel | translate}}\n</span>\n", styles: [".download-csv{color:var(--smarterplan-primary)}\n"], dependencies: [{ kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
|
|
94
94
|
}
|
|
95
95
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CsvExportComponent, decorators: [{
|
|
96
96
|
type: Component,
|
|
97
|
-
args: [{ selector: 'lib-csv-export', template: "<span [style.cursor]=\"'pointer'\" (click)=\"onClickDownload()\" class=\"download-csv d-flex\">\
|
|
97
|
+
args: [{ selector: 'lib-csv-export', template: "<span [style.cursor]=\"'pointer'\" (click)=\"onClickDownload()\" class=\"download-csv d-flex\">\n <span class=\"iconify\" data-icon=\"mdi:file-download-outline\"></span>\n {{ buttonLabel | translate}}\n</span>\n", styles: [".download-csv{color:var(--smarterplan-primary)}\n"] }]
|
|
98
98
|
}], propDecorators: { headers: [{
|
|
99
99
|
type: Input
|
|
100
100
|
}], rowsInput: [{
|
|
@@ -821,30 +821,30 @@ class SecurityCamera extends SceneComponent {
|
|
|
821
821
|
}
|
|
822
822
|
}
|
|
823
823
|
function vertexShader$1() {
|
|
824
|
-
return `
|
|
825
|
-
uniform mat4 lightMatrix;
|
|
826
|
-
|
|
827
|
-
varying vec4 vLightCoords;
|
|
828
|
-
|
|
829
|
-
void main() {
|
|
830
|
-
// create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
|
|
831
|
-
vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
|
|
832
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
833
|
-
}
|
|
824
|
+
return `
|
|
825
|
+
uniform mat4 lightMatrix;
|
|
826
|
+
|
|
827
|
+
varying vec4 vLightCoords;
|
|
828
|
+
|
|
829
|
+
void main() {
|
|
830
|
+
// create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
|
|
831
|
+
vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
|
|
832
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
833
|
+
}
|
|
834
834
|
`;
|
|
835
835
|
}
|
|
836
836
|
function fragmentShader$1() {
|
|
837
|
-
return `
|
|
838
|
-
uniform vec3 color;
|
|
839
|
-
|
|
840
|
-
varying vec4 vLightCoords;
|
|
841
|
-
|
|
842
|
-
void main() {
|
|
843
|
-
vec2 lightUV = vLightCoords.xy / vLightCoords.w;
|
|
844
|
-
float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
|
|
845
|
-
|
|
846
|
-
gl_FragColor = vec4(color, min(inView, 0.2));
|
|
847
|
-
}
|
|
837
|
+
return `
|
|
838
|
+
uniform vec3 color;
|
|
839
|
+
|
|
840
|
+
varying vec4 vLightCoords;
|
|
841
|
+
|
|
842
|
+
void main() {
|
|
843
|
+
vec2 lightUV = vLightCoords.xy / vLightCoords.w;
|
|
844
|
+
float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
|
|
845
|
+
|
|
846
|
+
gl_FragColor = vec4(color, min(inView, 0.2));
|
|
847
|
+
}
|
|
848
848
|
`;
|
|
849
849
|
}
|
|
850
850
|
function updateHighlightUniforms$1(projector, uniforms) {
|
|
@@ -1037,30 +1037,30 @@ class ViewFrustumMesh extends SceneComponent {
|
|
|
1037
1037
|
}
|
|
1038
1038
|
}
|
|
1039
1039
|
function vertexShader() {
|
|
1040
|
-
return `
|
|
1041
|
-
uniform mat4 lightMatrix;
|
|
1042
|
-
|
|
1043
|
-
varying vec4 vLightCoords;
|
|
1044
|
-
|
|
1045
|
-
void main() {
|
|
1046
|
-
// create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
|
|
1047
|
-
vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
|
|
1048
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
1049
|
-
}
|
|
1040
|
+
return `
|
|
1041
|
+
uniform mat4 lightMatrix;
|
|
1042
|
+
|
|
1043
|
+
varying vec4 vLightCoords;
|
|
1044
|
+
|
|
1045
|
+
void main() {
|
|
1046
|
+
// create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
|
|
1047
|
+
vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
|
|
1048
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
1049
|
+
}
|
|
1050
1050
|
`;
|
|
1051
1051
|
}
|
|
1052
1052
|
function fragmentShader() {
|
|
1053
|
-
return `
|
|
1054
|
-
uniform vec3 color;
|
|
1055
|
-
|
|
1056
|
-
varying vec4 vLightCoords;
|
|
1057
|
-
|
|
1058
|
-
void main() {
|
|
1059
|
-
vec2 lightUV = vLightCoords.xy / vLightCoords.w;
|
|
1060
|
-
float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
|
|
1061
|
-
|
|
1062
|
-
gl_FragColor = vec4(color, min(inView, 0.2));
|
|
1063
|
-
}
|
|
1053
|
+
return `
|
|
1054
|
+
uniform vec3 color;
|
|
1055
|
+
|
|
1056
|
+
varying vec4 vLightCoords;
|
|
1057
|
+
|
|
1058
|
+
void main() {
|
|
1059
|
+
vec2 lightUV = vLightCoords.xy / vLightCoords.w;
|
|
1060
|
+
float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
|
|
1061
|
+
|
|
1062
|
+
gl_FragColor = vec4(color, min(inView, 0.2));
|
|
1063
|
+
}
|
|
1064
1064
|
`;
|
|
1065
1065
|
}
|
|
1066
1066
|
function updateHighlightUniforms(projector, uniforms) {
|
|
@@ -1095,22 +1095,62 @@ const uploadFileToS3 = async (path, file, objectId) => {
|
|
|
1095
1095
|
return result.key;
|
|
1096
1096
|
});
|
|
1097
1097
|
};
|
|
1098
|
+
/**
|
|
1099
|
+
* Fetches fresh credentials, and says in the console how long the ones held had left: when an
|
|
1100
|
+
* upload is refused, this is what tells why.
|
|
1101
|
+
*/
|
|
1102
|
+
const renewCredentials = async () => {
|
|
1103
|
+
try {
|
|
1104
|
+
const held = await Auth.currentCredentials();
|
|
1105
|
+
const left = held?.expiration ? new Date(held.expiration).getTime() - Date.now() : 0;
|
|
1106
|
+
console.info(`[upload] credentials held expire in ${Math.round(left / 60000)} min, fetching new ones`);
|
|
1107
|
+
// What Amplify itself does on signing in. Nobody signs in here through a federated provider,
|
|
1108
|
+
// whose stored sign-in this would also drop.
|
|
1109
|
+
await Auth.Credentials.clear();
|
|
1110
|
+
await Auth.currentCredentials();
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
// The upload will say what is wrong, if anything is.
|
|
1114
|
+
console.warn("[upload] could not renew the credentials:", error);
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
const isExpiredTokenError = (error) => /ExpiredToken|token has expired/i.test(`${error?.name ?? ""} ${error?.Code ?? ""} ${error?.message ?? error ?? ""}`);
|
|
1098
1118
|
/**
|
|
1099
1119
|
* Uploads a file, reporting progress as a 0-1 fraction.
|
|
1100
1120
|
*
|
|
1101
1121
|
* uploadFileToS3 above swallows progress, which is fine for a photo and unusable for a 3D model:
|
|
1102
1122
|
* these run to hundreds of megabytes, and an upload with no feedback is indistinguishable from a
|
|
1103
1123
|
* frozen page. Returns the stored key, which is what gets saved on the record.
|
|
1124
|
+
*
|
|
1125
|
+
* A large file is sent in parts, each signed when it is sent. Left to itself, Storage.put signs
|
|
1126
|
+
* them all with whatever credentials Amplify last handed the S3 provider, for any call - credentials
|
|
1127
|
+
* that last an hour at most, and an upload of a few gigabytes takes longer: every part after they
|
|
1128
|
+
* expire is refused with "The provided token has expired", and fetching new ones does not help, the
|
|
1129
|
+
* upload never asks for them. Passing no credentials at all makes it ask Amplify for them part by
|
|
1130
|
+
* part, and Amplify renews them as they age. Should they run out anyway, it starts once more.
|
|
1104
1131
|
*/
|
|
1105
1132
|
const uploadFileToS3WithProgress = async (path, file, onProgress) => {
|
|
1106
|
-
|
|
1133
|
+
const put = () => Storage.put(path + file.name, file, {
|
|
1107
1134
|
contentType: file.type || "application/octet-stream",
|
|
1135
|
+
// Present and empty, so that it replaces the provider's own; see above.
|
|
1136
|
+
credentials: undefined,
|
|
1108
1137
|
progressCallback(progress) {
|
|
1109
1138
|
if (onProgress && progress.total > 0) {
|
|
1110
1139
|
onProgress(progress.loaded / progress.total);
|
|
1111
1140
|
}
|
|
1112
1141
|
},
|
|
1113
1142
|
}).then((result) => result.key);
|
|
1143
|
+
try {
|
|
1144
|
+
return await put();
|
|
1145
|
+
}
|
|
1146
|
+
catch (error) {
|
|
1147
|
+
if (!isExpiredTokenError(error)) {
|
|
1148
|
+
throw error;
|
|
1149
|
+
}
|
|
1150
|
+
console.warn("[upload] refused, the token had expired; starting again with new credentials", error);
|
|
1151
|
+
await renewCredentials();
|
|
1152
|
+
return put();
|
|
1153
|
+
}
|
|
1114
1154
|
};
|
|
1115
1155
|
const getSignedImageUrlForEquipment = async (equip) => {
|
|
1116
1156
|
if (!equip.photo) {
|
|
@@ -2223,32 +2263,32 @@ class PoiService {
|
|
|
2223
2263
|
if (!sweeps || sweeps.length === 0) {
|
|
2224
2264
|
return [];
|
|
2225
2265
|
}
|
|
2226
|
-
const queryStatement = `
|
|
2227
|
-
query ListPOISLight($filter: ModelPOIFilterInput, $limit: Int, $nextToken: String) {
|
|
2228
|
-
listPOIS(filter: $filter, limit: $limit, nextToken: $nextToken) {
|
|
2229
|
-
items {
|
|
2230
|
-
id
|
|
2231
|
-
coordinate
|
|
2232
|
-
specific
|
|
2233
|
-
private
|
|
2234
|
-
global
|
|
2235
|
-
type
|
|
2236
|
-
elementID
|
|
2237
|
-
level
|
|
2238
|
-
role
|
|
2239
|
-
category
|
|
2240
|
-
domainID
|
|
2241
|
-
metadata
|
|
2242
|
-
tagIcon
|
|
2243
|
-
tagDescription
|
|
2244
|
-
tagImageID
|
|
2245
|
-
matterportSweepID
|
|
2246
|
-
createdAt
|
|
2247
|
-
updatedAt
|
|
2248
|
-
}
|
|
2249
|
-
nextToken
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2266
|
+
const queryStatement = `
|
|
2267
|
+
query ListPOISLight($filter: ModelPOIFilterInput, $limit: Int, $nextToken: String) {
|
|
2268
|
+
listPOIS(filter: $filter, limit: $limit, nextToken: $nextToken) {
|
|
2269
|
+
items {
|
|
2270
|
+
id
|
|
2271
|
+
coordinate
|
|
2272
|
+
specific
|
|
2273
|
+
private
|
|
2274
|
+
global
|
|
2275
|
+
type
|
|
2276
|
+
elementID
|
|
2277
|
+
level
|
|
2278
|
+
role
|
|
2279
|
+
category
|
|
2280
|
+
domainID
|
|
2281
|
+
metadata
|
|
2282
|
+
tagIcon
|
|
2283
|
+
tagDescription
|
|
2284
|
+
tagImageID
|
|
2285
|
+
matterportSweepID
|
|
2286
|
+
createdAt
|
|
2287
|
+
updatedAt
|
|
2288
|
+
}
|
|
2289
|
+
nextToken
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2252
2292
|
`;
|
|
2253
2293
|
const CHUNK_SIZE = 50; // safe margin for an "or" filter expression's practical size
|
|
2254
2294
|
const chunks = [];
|
|
@@ -3116,11 +3156,11 @@ class MatterportPointerService {
|
|
|
3116
3156
|
}
|
|
3117
3157
|
updateCursorDisplay(poseMatterport) {
|
|
3118
3158
|
if (this.textDisplayCursorPositionPanel && poseMatterport) {
|
|
3119
|
-
this.textDisplayCursorPositionPanel.innerHTML = `position:
|
|
3120
|
-
${pointToString(poseMatterport.position)}\n
|
|
3121
|
-
normal:
|
|
3122
|
-
${pointToString(poseMatterport.normal)}\n
|
|
3123
|
-
floorId:
|
|
3159
|
+
this.textDisplayCursorPositionPanel.innerHTML = `position:
|
|
3160
|
+
${pointToString(poseMatterport.position)}\n
|
|
3161
|
+
normal:
|
|
3162
|
+
${pointToString(poseMatterport.normal)}\n
|
|
3163
|
+
floorId:
|
|
3124
3164
|
${poseMatterport.floorId}`;
|
|
3125
3165
|
if (this.getCursorPositionButton) {
|
|
3126
3166
|
this.getCursorPositionButton.style.display = 'none';
|
|
@@ -4648,11 +4688,11 @@ class MatterportTagService {
|
|
|
4648
4688
|
* floor and room context. Must be called after each sweep, floor or mode change.
|
|
4649
4689
|
*
|
|
4650
4690
|
* Rules:
|
|
4651
|
-
* -
|
|
4652
|
-
*
|
|
4653
|
-
*
|
|
4654
|
-
*
|
|
4655
|
-
* -
|
|
4691
|
+
* - A tag shows when its sweep is on the floor being displayed, in every mode. Tags on
|
|
4692
|
+
* other floors are hidden (opacity 0) and cannot be opened.
|
|
4693
|
+
* - While all floors are displayed - the usual state of DOLLHOUSE and FLOORPLAN - every
|
|
4694
|
+
* tag shows.
|
|
4695
|
+
* - A tag with no sweep belongs to no floor and always shows.
|
|
4656
4696
|
*
|
|
4657
4697
|
* @param sdk Matterport SDK instance.
|
|
4658
4698
|
* @param poseCamera Current camera pose.
|
|
@@ -4664,7 +4704,12 @@ class MatterportTagService {
|
|
|
4664
4704
|
if (!sdk || this.dictionnaryTags.size === 0)
|
|
4665
4705
|
return;
|
|
4666
4706
|
const isInteriorMode = currentCameraMode === CameraMode.INSIDE;
|
|
4667
|
-
|
|
4707
|
+
// The SDK reports sequence -1 while all floors are displayed, which is the dollhouse's and the
|
|
4708
|
+
// floor plan's normal state. That is "no floor in particular", not a floor that no tag is on:
|
|
4709
|
+
// read as a floor id, it matched nothing and hid every tag that has a sweep.
|
|
4710
|
+
const currentFloorId = currentFloorSequence !== null && currentFloorSequence >= 0
|
|
4711
|
+
? currentFloorSequence.toString()
|
|
4712
|
+
: null;
|
|
4668
4713
|
const promises = [];
|
|
4669
4714
|
for (const [tagId, mattertagData] of this.dictionnaryTags) {
|
|
4670
4715
|
if (tagId === mattertagToFollow)
|
|
@@ -5819,22 +5864,22 @@ class MatterportService {
|
|
|
5819
5864
|
);
|
|
5820
5865
|
}*/
|
|
5821
5866
|
addCustomCss() {
|
|
5822
|
-
const css = `
|
|
5823
|
-
.footer-ui,
|
|
5824
|
-
.showcase-footer,
|
|
5825
|
-
.mp-nova-btn-group,
|
|
5826
|
-
.footer-divider,
|
|
5827
|
-
.logo-link,
|
|
5828
|
-
.footer-logo,
|
|
5829
|
-
.bottom-ui,
|
|
5830
|
-
img.logo-image,
|
|
5831
|
-
img[class^="logo-image"],
|
|
5832
|
-
img[class*=" logo-image"],
|
|
5833
|
-
[data-testid="showcase-footer"] {
|
|
5834
|
-
opacity: 0 !important;
|
|
5835
|
-
visibility: hidden !important;
|
|
5836
|
-
pointer-events: none !important;
|
|
5837
|
-
}
|
|
5867
|
+
const css = `
|
|
5868
|
+
.footer-ui,
|
|
5869
|
+
.showcase-footer,
|
|
5870
|
+
.mp-nova-btn-group,
|
|
5871
|
+
.footer-divider,
|
|
5872
|
+
.logo-link,
|
|
5873
|
+
.footer-logo,
|
|
5874
|
+
.bottom-ui,
|
|
5875
|
+
img.logo-image,
|
|
5876
|
+
img[class^="logo-image"],
|
|
5877
|
+
img[class*=" logo-image"],
|
|
5878
|
+
[data-testid="showcase-footer"] {
|
|
5879
|
+
opacity: 0 !important;
|
|
5880
|
+
visibility: hidden !important;
|
|
5881
|
+
pointer-events: none !important;
|
|
5882
|
+
}
|
|
5838
5883
|
`;
|
|
5839
5884
|
// 1st method: Via Scene SDK
|
|
5840
5885
|
if (this.sdk?.Scene?.addStyle) {
|
|
@@ -6824,6 +6869,15 @@ function solveReducedAlignment(pairs, up) {
|
|
|
6824
6869
|
scale,
|
|
6825
6870
|
};
|
|
6826
6871
|
}
|
|
6872
|
+
/**
|
|
6873
|
+
* The 3D viewers' background where there is nothing to draw, unless the app sets its own as
|
|
6874
|
+
* --smarterplan-viewer-background: the SmarterPlan app's light blue, from the POC.
|
|
6875
|
+
*/
|
|
6876
|
+
const VIEWER_BACKGROUND = '#e0f7ff';
|
|
6877
|
+
/** The 3D viewers' background as the app sets it, where the element is. */
|
|
6878
|
+
function viewerBackground(element) {
|
|
6879
|
+
return getComputedStyle(element).getPropertyValue('--smarterplan-viewer-background').trim() || VIEWER_BACKGROUND;
|
|
6880
|
+
}
|
|
6827
6881
|
class ThreeJsService {
|
|
6828
6882
|
ngZone;
|
|
6829
6883
|
scene;
|
|
@@ -6842,6 +6896,17 @@ class ThreeJsService {
|
|
|
6842
6896
|
objectClicked$ = new Subject();
|
|
6843
6897
|
/** Emits when the user clicks a tag, mirroring Matterport's tag click. */
|
|
6844
6898
|
tagClicked$ = new Subject();
|
|
6899
|
+
/**
|
|
6900
|
+
* Emits the tag the camera is sent to (goToTag): for a viewer drawing the model elsewhere, the
|
|
6901
|
+
* splat viewer, to point it out.
|
|
6902
|
+
*/
|
|
6903
|
+
tagFocused$ = new Subject();
|
|
6904
|
+
/**
|
|
6905
|
+
* Emits when the tags change - added, moved, removed, filtered, calibrated differently - at most
|
|
6906
|
+
* once per turn of the event loop, however many changed in it.
|
|
6907
|
+
*/
|
|
6908
|
+
tagsChanged$ = new Subject();
|
|
6909
|
+
tagsChangeQueued = false;
|
|
6845
6910
|
/** Emits a tag's label while the pointer is over it, and null when it leaves. */
|
|
6846
6911
|
tagHovered$ = new Subject();
|
|
6847
6912
|
// Tags live under the model rather than the scene, so the centering and scaling applied when the
|
|
@@ -6851,6 +6916,22 @@ class ThreeJsService {
|
|
|
6851
6916
|
canvasResizeObserver;
|
|
6852
6917
|
/** Aborts the in-flight model download. Parsing cannot be interrupted, only not started. */
|
|
6853
6918
|
loadAbortController;
|
|
6919
|
+
/**
|
|
6920
|
+
* Which load is the wanted one. Every load takes the next number as it starts, and anything that
|
|
6921
|
+
* makes it unwanted - another load, the scene being rebuilt, the viewer closing - moves the
|
|
6922
|
+
* number on.
|
|
6923
|
+
*
|
|
6924
|
+
* Aborting the download is not enough to stop a load. A model read from the cache has no
|
|
6925
|
+
* download to abort, and one that has finished downloading still has seconds of work ahead of it
|
|
6926
|
+
* before it reaches the screen. A load that finds the number has moved on drops what it has.
|
|
6927
|
+
*/
|
|
6928
|
+
loadGeneration = 0;
|
|
6929
|
+
/** Whether the wanted load is still on its way, so abandoning it can say that it stopped. */
|
|
6930
|
+
loadInProgress = false;
|
|
6931
|
+
/** Whether a scene is up: started by init() and not yet taken down by destroy(). */
|
|
6932
|
+
live = false;
|
|
6933
|
+
/** Takes off each listener init() put on the canvas. */
|
|
6934
|
+
canvasListeners = [];
|
|
6854
6935
|
// The loaded base model, kept so tag placement can raycast against its geometry.
|
|
6855
6936
|
modelRoot;
|
|
6856
6937
|
/** True while the next click on the model places a tag instead of orbiting. */
|
|
@@ -6872,6 +6953,12 @@ class ThreeJsService {
|
|
|
6872
6953
|
* placed in this one are told from those that came from elsewhere.
|
|
6873
6954
|
*/
|
|
6874
6955
|
currentCaptureID = null;
|
|
6956
|
+
/**
|
|
6957
|
+
* Whether the visit on screen is the only one the site has. A tag records the visit it was placed
|
|
6958
|
+
* in, but some do not - placed before that was recorded, or in the development model. With
|
|
6959
|
+
* another visit around, such a tag is taken to come from it. With none, it can only be this one's.
|
|
6960
|
+
*/
|
|
6961
|
+
soleVisit = false;
|
|
6875
6962
|
/**
|
|
6876
6963
|
* The model's surface as a grid: every cell some part of the surface passes through.
|
|
6877
6964
|
*
|
|
@@ -6916,6 +7003,17 @@ class ThreeJsService {
|
|
|
6916
7003
|
modelUp = 'y';
|
|
6917
7004
|
/** While measuring: how many points and each segment's length. Null when it is abandoned. */
|
|
6918
7005
|
measurementProgress$ = new Subject();
|
|
7006
|
+
/**
|
|
7007
|
+
* True when a measurement starts taking clicks, false when it stops - finished or abandoned. For a
|
|
7008
|
+
* viewer drawing the model elsewhere, which takes the clicks itself; see addMeasurePointAtModelPoint.
|
|
7009
|
+
*/
|
|
7010
|
+
measuringChanged$ = new Subject();
|
|
7011
|
+
/**
|
|
7012
|
+
* A measurement - under way or finished - was taken off the model. For a viewer drawing it
|
|
7013
|
+
* elsewhere, the splat viewer: a finished one stays drawn while its form is open, which
|
|
7014
|
+
* measurementProgress$ does not say the end of.
|
|
7015
|
+
*/
|
|
7016
|
+
measurementCleared$ = new Subject();
|
|
6919
7017
|
/**
|
|
6920
7018
|
* Layer carrying the model alone, so the depth pick can render it without the tags.
|
|
6921
7019
|
*
|
|
@@ -6923,8 +7021,18 @@ class ThreeJsService {
|
|
|
6923
7021
|
* their parent passed, so marking the model's own meshes is enough - no re-parenting.
|
|
6924
7022
|
*/
|
|
6925
7023
|
static PICK_LAYER = 1;
|
|
7024
|
+
/** How large a tag is drawn, in CSS pixels. About what Matterport draws its own at. */
|
|
7025
|
+
static TAG_SCREEN_PX = 28;
|
|
6926
7026
|
/** The app's highlight colour, so a measurement reads as the active thing on screen. */
|
|
6927
7027
|
static MEASURE_COLOR = '#FFB103';
|
|
7028
|
+
/** Green for the points of a calibration that are settled, as opposed to the one in hand. */
|
|
7029
|
+
static CALIBRATION_DONE_COLOR = '#1E9E5A';
|
|
7030
|
+
/** Size on screen of the numbered pin left where a tag was matched: a little under a tag's. */
|
|
7031
|
+
static CALIBRATION_PIN_PX = 24;
|
|
7032
|
+
/** How long a mark just dropped takes to settle to its size, in milliseconds. */
|
|
7033
|
+
static CONFIRM_PULSE_MS = 350;
|
|
7034
|
+
/** How many matched tags a calibration takes. Three settle everything there is to settle. */
|
|
7035
|
+
static CALIBRATION_PAIRS = 3;
|
|
6928
7036
|
/** Cache Storage bucket holding downloaded models, and how many to keep. */
|
|
6929
7037
|
static MODEL_CACHE = 'smarterplan-3d-models';
|
|
6930
7038
|
static MODEL_CACHE_SIZE = 3;
|
|
@@ -6939,12 +7047,36 @@ class ThreeJsService {
|
|
|
6939
7047
|
/** Translucent marker shown under the cursor while placing. */
|
|
6940
7048
|
placementPreview;
|
|
6941
7049
|
aligning = false;
|
|
7050
|
+
/**
|
|
7051
|
+
* True from the moment the calibration panel opens, before the walkthrough is armed: the tags
|
|
7052
|
+
* from elsewhere are all on show, as they are while calibrating, so that what there is to
|
|
7053
|
+
* calibrate with can be seen while reading what calibrating involves.
|
|
7054
|
+
*/
|
|
7055
|
+
revealingCalibrationTags = false;
|
|
6942
7056
|
alignmentPairs = [];
|
|
6943
7057
|
alignmentSource = null;
|
|
6944
7058
|
/** The tag `alignmentSource` was read from, kept so the same tag is not matched twice. */
|
|
6945
7059
|
alignmentSourceTagId;
|
|
7060
|
+
/**
|
|
7061
|
+
* Everything drawn for the calibration under way: a numbered pin where each tag was matched, a
|
|
7062
|
+
* line from the pin to its tag, and a ring round the tag being matched.
|
|
7063
|
+
*
|
|
7064
|
+
* A sibling of the tag group rather than a child of it. The points clicked are where the tags are
|
|
7065
|
+
* to end up, in the frame the calibration maps onto (see pickAlignmentTarget) - not in the tags'
|
|
7066
|
+
* own frame, which is the one about to change.
|
|
7067
|
+
*/
|
|
7068
|
+
calibrationGroup = new THREE.Group();
|
|
7069
|
+
/** Marks still settling after being dropped, advanced by the render loop. */
|
|
7070
|
+
pulses = [];
|
|
6946
7071
|
/** Drives the alignment walkthrough's UI: how many pairs are done and what it wants next. */
|
|
6947
7072
|
alignmentProgress$ = new Subject();
|
|
7073
|
+
/**
|
|
7074
|
+
* Emits when a click during calibration did nothing, with what it was expected to land on.
|
|
7075
|
+
*
|
|
7076
|
+
* A click that changes nothing looks the same as a click that was not received. This lets the
|
|
7077
|
+
* panel point at what it is waiting for instead.
|
|
7078
|
+
*/
|
|
7079
|
+
alignmentClickIgnored$ = new Subject();
|
|
6948
7080
|
/** In-flight camera move started by goToTag, advanced by the render loop. */
|
|
6949
7081
|
cameraTween = null;
|
|
6950
7082
|
/** Emits true while a heavy OBJ file is being parsed (in a worker, when available), false once done. */
|
|
@@ -7056,15 +7188,30 @@ class ThreeJsService {
|
|
|
7056
7188
|
// by then, so the model being abandoned would be handed to the scene built below.
|
|
7057
7189
|
// - The tag group, which is a field and keeps its sprites. They would be re-parented under the
|
|
7058
7190
|
// new model and show the previous visit's tags on it.
|
|
7191
|
+
// Usually destroy() ran before this and there is nothing left to stop. When it did not, the
|
|
7192
|
+
// scene before would carry on beside the one started below: its render loop drawing every
|
|
7193
|
+
// frame as well, its model still on the graphics card, its listeners answering every click a
|
|
7194
|
+
// second time.
|
|
7195
|
+
if (this.live) {
|
|
7196
|
+
this.destroy();
|
|
7197
|
+
}
|
|
7059
7198
|
this.discardCurrentLoad();
|
|
7060
7199
|
this.clearTags();
|
|
7061
7200
|
this.measuring = false;
|
|
7062
7201
|
this.clearMeasurement();
|
|
7202
|
+
// A calibration left half-done belongs to the scene it was started in: its points mean nothing
|
|
7203
|
+
// in the next one, and left armed it would take that scene's first clicks for itself.
|
|
7204
|
+
this.aligning = false;
|
|
7205
|
+
this.revealingCalibrationTags = false;
|
|
7206
|
+
this.soleVisit = false;
|
|
7207
|
+
this.alignmentPairs = [];
|
|
7208
|
+
this.clearAlignmentSource();
|
|
7209
|
+
this.clearCalibrationMarks();
|
|
7210
|
+
this.pulses = [];
|
|
7063
7211
|
this.cameraTween = null;
|
|
7064
|
-
this.modelRoot = undefined;
|
|
7065
7212
|
// 1. Setup Scene
|
|
7066
7213
|
this.scene = new THREE.Scene();
|
|
7067
|
-
this.scene.background = new THREE.Color(
|
|
7214
|
+
this.scene.background = new THREE.Color(viewerBackground(canvas));
|
|
7068
7215
|
// 2. Setup Camera
|
|
7069
7216
|
this.camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 10000);
|
|
7070
7217
|
this.camera.position.set(0, 5, 10); // will be overridden later if passed
|
|
@@ -7109,7 +7256,14 @@ class ThreeJsService {
|
|
|
7109
7256
|
// A light only contributes while it is part of the scene graph, and this one hangs off the camera.
|
|
7110
7257
|
this.scene.add(this.camera);
|
|
7111
7258
|
// 5. Setup Controls (from POC)
|
|
7112
|
-
|
|
7259
|
+
//
|
|
7260
|
+
// Created outside Angular's zone, as is the pointer-move listener further down. The controls
|
|
7261
|
+
// listen to every pointer move and wheel tick, and inside the zone each of those events had
|
|
7262
|
+
// Angular re-check the whole page - dozens of times a second while the camera is dragged,
|
|
7263
|
+
// competing with the frames being drawn - for a change that only ever concerns the canvas.
|
|
7264
|
+
this.ngZone.runOutsideAngular(() => {
|
|
7265
|
+
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
|
7266
|
+
});
|
|
7113
7267
|
this.controls.rotateSpeed = 1.0;
|
|
7114
7268
|
this.controls.dampingFactor = 0.1;
|
|
7115
7269
|
this.controls.enableDamping = true;
|
|
@@ -7133,9 +7287,18 @@ class ThreeJsService {
|
|
|
7133
7287
|
}, 250);
|
|
7134
7288
|
}
|
|
7135
7289
|
});
|
|
7136
|
-
// Interaction Event Listeners on canvas
|
|
7137
|
-
canvas
|
|
7138
|
-
|
|
7290
|
+
// Interaction Event Listeners on canvas. Kept so that destroy() can take them off again: the
|
|
7291
|
+
// same canvas is used again when one 3D model is swapped for another, and a second set of
|
|
7292
|
+
// listeners would answer every click twice.
|
|
7293
|
+
const listen = (type, handler) => {
|
|
7294
|
+
canvas.addEventListener(type, handler, false);
|
|
7295
|
+
this.canvasListeners.push(() => canvas.removeEventListener(type, handler, false));
|
|
7296
|
+
};
|
|
7297
|
+
// A click changes what the page shows - a panel opens, a point is recorded - so Angular has to
|
|
7298
|
+
// hear of it, wherever init() itself was called from.
|
|
7299
|
+
listen('click', (event) => this.ngZone.run(() => this.onMouseClick(event)));
|
|
7300
|
+
// Moving the pointer does not: it moves the ghost and the tooltip, both drawn here.
|
|
7301
|
+
this.ngZone.runOutsideAngular(() => listen('mousemove', (event) => this.onMouseMove(event)));
|
|
7139
7302
|
// Escape backs out of placement and calibration. On the window rather than the canvas, which
|
|
7140
7303
|
// would need focus the user has no reason to have given it - they are aiming with the mouse.
|
|
7141
7304
|
// Removed first because this service is a singleton: init() runs again for every canvas, and
|
|
@@ -7148,6 +7311,7 @@ class ThreeJsService {
|
|
|
7148
7311
|
this.pickCamera = undefined;
|
|
7149
7312
|
window.addEventListener('keydown', this.onKeyDown);
|
|
7150
7313
|
// 6. Start the render loop
|
|
7314
|
+
this.live = true;
|
|
7151
7315
|
this.ngZone.runOutsideAngular(() => {
|
|
7152
7316
|
this.animate();
|
|
7153
7317
|
});
|
|
@@ -7184,33 +7348,25 @@ class ThreeJsService {
|
|
|
7184
7348
|
}
|
|
7185
7349
|
// Lets gltfpack/gltf-transform output using EXT_meshopt_compression load without extra setup.
|
|
7186
7350
|
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
7187
|
-
this.
|
|
7351
|
+
const wanted = this.beginLoad();
|
|
7188
7352
|
this.modelLoadProgress$.next(0);
|
|
7189
7353
|
// Order matters: normals must exist before anisotropy, and the Phong conversion has to run
|
|
7190
7354
|
// before side/flatShading are touched so those settings land on the materials we keep.
|
|
7191
7355
|
const onLoaded = (gltf) => {
|
|
7356
|
+
if (!wanted()) {
|
|
7357
|
+
// Built for a request that has since been replaced or abandoned. It was never on screen;
|
|
7358
|
+
// what it holds is freed here because nothing else ever will.
|
|
7359
|
+
this.disposeModel(gltf.scene);
|
|
7360
|
+
return;
|
|
7361
|
+
}
|
|
7192
7362
|
this.convertToPhongMaterials(gltf.scene);
|
|
7193
7363
|
this.forceDoubleSidedMaterials(gltf.scene);
|
|
7194
7364
|
this.ensureVertexNormals(gltf.scene);
|
|
7195
7365
|
this.tuneTextureSampling(gltf.scene);
|
|
7196
7366
|
this.placeLoadedObjectInScene(gltf.scene, cameraPosition, rotation, up);
|
|
7197
|
-
this.
|
|
7198
|
-
};
|
|
7199
|
-
const onError = (error) => {
|
|
7200
|
-
// Cancelling is a decision, not a failure: clear the overlay without an error in the console.
|
|
7201
|
-
if (error?.name === 'AbortError') {
|
|
7202
|
-
// Only a cancel of the load that is still current came from the user; anything else is this
|
|
7203
|
-
// load having been superseded or discarded.
|
|
7204
|
-
if (this.loadAbortController?.signal.aborted) {
|
|
7205
|
-
this.modelLoadCancelled$.next();
|
|
7206
|
-
}
|
|
7207
|
-
}
|
|
7208
|
-
else {
|
|
7209
|
-
console.error('Error loading GLTF model:', error);
|
|
7210
|
-
}
|
|
7211
|
-
this.modelLoading$.next(false);
|
|
7212
|
-
this.modelLoadProgress$.next(null);
|
|
7367
|
+
this.endLoad();
|
|
7213
7368
|
};
|
|
7369
|
+
const onError = (error) => this.loadFailed(wanted, error, 'GLTF model');
|
|
7214
7370
|
// GLTFLoader decodes embedded images with createImageBitmap when the browser offers it, and on
|
|
7215
7371
|
// large texture atlases that path returns visibly corrupt data - scattered white blocks across
|
|
7216
7372
|
// the model. The exact same bytes decoded through an HTMLImageElement, which is what the .obj
|
|
@@ -7228,7 +7384,8 @@ class ThreeJsService {
|
|
|
7228
7384
|
.then((buffer) => {
|
|
7229
7385
|
// parse() cannot be interrupted, so a cancel that lands during the paint yield has to be
|
|
7230
7386
|
// caught here - the last moment before the main thread is committed for several seconds.
|
|
7231
|
-
|
|
7387
|
+
// The same goes for a load nobody is waiting for any more.
|
|
7388
|
+
if (!wanted() || this.loadAbortController?.signal.aborted) {
|
|
7232
7389
|
throw new DOMException('Model load cancelled', 'AbortError');
|
|
7233
7390
|
}
|
|
7234
7391
|
const globalScope = globalThis;
|
|
@@ -7243,6 +7400,28 @@ class ThreeJsService {
|
|
|
7243
7400
|
})
|
|
7244
7401
|
.catch(onError);
|
|
7245
7402
|
}
|
|
7403
|
+
/** Ends a load that failed, unless another load has replaced it in the meantime. */
|
|
7404
|
+
loadFailed(wanted, error, what) {
|
|
7405
|
+
if (!wanted()) {
|
|
7406
|
+
// Whatever replaced this load owns the loading indicator now; ending it from here would
|
|
7407
|
+
// switch the indicator off while that one is still on its way.
|
|
7408
|
+
return;
|
|
7409
|
+
}
|
|
7410
|
+
this.loadInProgress = false;
|
|
7411
|
+
// Cancelling is a decision, not a failure: clear the overlay without an error in the console.
|
|
7412
|
+
if (error?.name === 'AbortError') {
|
|
7413
|
+
// Only a cancel of the load that is still current came from the user; anything else is this
|
|
7414
|
+
// load having been superseded or discarded.
|
|
7415
|
+
if (this.loadAbortController?.signal.aborted) {
|
|
7416
|
+
this.modelLoadCancelled$.next();
|
|
7417
|
+
}
|
|
7418
|
+
}
|
|
7419
|
+
else {
|
|
7420
|
+
console.error(`Error loading ${what}:`, error);
|
|
7421
|
+
}
|
|
7422
|
+
this.modelLoading$.next(false);
|
|
7423
|
+
this.modelLoadProgress$.next(null);
|
|
7424
|
+
}
|
|
7246
7425
|
/**
|
|
7247
7426
|
* Stops the model download in progress, if any.
|
|
7248
7427
|
*
|
|
@@ -7261,8 +7440,30 @@ class ThreeJsService {
|
|
|
7261
7440
|
* scene is not something the person asked for, so it must not surface as "loading cancelled".
|
|
7262
7441
|
*/
|
|
7263
7442
|
discardCurrentLoad() {
|
|
7443
|
+
this.loadGeneration += 1;
|
|
7264
7444
|
this.loadAbortController?.abort();
|
|
7265
7445
|
this.loadAbortController = undefined;
|
|
7446
|
+
if (this.loadInProgress) {
|
|
7447
|
+
this.loadInProgress = false;
|
|
7448
|
+
this.modelLoading$.next(false);
|
|
7449
|
+
this.modelLoadProgress$.next(null);
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
/**
|
|
7453
|
+
* Starts a load, replacing whichever was under way, and returns the question it has to keep
|
|
7454
|
+
* asking: is it still the one that is wanted?
|
|
7455
|
+
*/
|
|
7456
|
+
beginLoad() {
|
|
7457
|
+
this.loadGeneration += 1;
|
|
7458
|
+
const generation = this.loadGeneration;
|
|
7459
|
+
this.loadInProgress = true;
|
|
7460
|
+
this.modelLoading$.next(true);
|
|
7461
|
+
return () => generation === this.loadGeneration;
|
|
7462
|
+
}
|
|
7463
|
+
/** Marks the wanted load as over, whichever way it ended. */
|
|
7464
|
+
endLoad() {
|
|
7465
|
+
this.loadInProgress = false;
|
|
7466
|
+
this.modelLoading$.next(false);
|
|
7266
7467
|
}
|
|
7267
7468
|
/**
|
|
7268
7469
|
* Resolves once the browser has painted at least one frame.
|
|
@@ -7293,6 +7494,11 @@ class ThreeJsService {
|
|
|
7293
7494
|
this.loadAbortController = new AbortController();
|
|
7294
7495
|
const { signal } = this.loadAbortController;
|
|
7295
7496
|
const cached = await this.readModelFromCache(cacheKey);
|
|
7497
|
+
if (signal.aborted) {
|
|
7498
|
+
// Replaced or abandoned while the cache was being read. With nothing being downloaded, this
|
|
7499
|
+
// is the only place a load served from the cache can find that out.
|
|
7500
|
+
throw new DOMException('Model load cancelled', 'AbortError');
|
|
7501
|
+
}
|
|
7296
7502
|
if (cached) {
|
|
7297
7503
|
this.modelLoadProgress$.next(1);
|
|
7298
7504
|
return cached;
|
|
@@ -7633,12 +7839,25 @@ class ThreeJsService {
|
|
|
7633
7839
|
const basePath = mtlUrl.substring(0, mtlUrl.lastIndexOf('/') + 1);
|
|
7634
7840
|
mtlLoader.setPath(basePath);
|
|
7635
7841
|
const mtlFileName = mtlUrl.substring(mtlUrl.lastIndexOf('/') + 1);
|
|
7636
|
-
|
|
7842
|
+
// These loaders cannot be interrupted, so a load that is no longer wanted runs to its end and
|
|
7843
|
+
// is dropped there - see loadGeneration.
|
|
7844
|
+
const wanted = this.beginLoad();
|
|
7845
|
+
const finish = () => {
|
|
7846
|
+
if (wanted()) {
|
|
7847
|
+
this.endLoad();
|
|
7848
|
+
}
|
|
7849
|
+
};
|
|
7637
7850
|
mtlLoader.load(mtlFileName, (materials) => {
|
|
7851
|
+
if (!wanted()) {
|
|
7852
|
+
return;
|
|
7853
|
+
}
|
|
7638
7854
|
materials.preload();
|
|
7639
7855
|
if (this.objParserWorker) {
|
|
7640
7856
|
this.requestObjParseInWorker(objUrl)
|
|
7641
7857
|
.then((meshes) => {
|
|
7858
|
+
if (!wanted()) {
|
|
7859
|
+
return;
|
|
7860
|
+
}
|
|
7642
7861
|
const group = new THREE.Group();
|
|
7643
7862
|
meshes.forEach((meshData) => {
|
|
7644
7863
|
group.add(this.meshFromParsedData(meshData, materials));
|
|
@@ -7648,25 +7867,42 @@ class ThreeJsService {
|
|
|
7648
7867
|
.catch((error) => {
|
|
7649
7868
|
console.error('Error parsing OBJ model in worker:', error);
|
|
7650
7869
|
})
|
|
7651
|
-
.finally(
|
|
7870
|
+
.finally(finish);
|
|
7652
7871
|
return;
|
|
7653
7872
|
}
|
|
7654
7873
|
const objLoader = new OBJLoader();
|
|
7655
7874
|
objLoader.setMaterials(materials);
|
|
7656
7875
|
objLoader.load(objUrl, (object) => {
|
|
7876
|
+
if (!wanted()) {
|
|
7877
|
+
this.disposeModel(object);
|
|
7878
|
+
return;
|
|
7879
|
+
}
|
|
7657
7880
|
this.placeLoadedObjectInScene(object, cameraPosition, rotation);
|
|
7658
|
-
|
|
7881
|
+
finish();
|
|
7659
7882
|
}, undefined, (error) => {
|
|
7660
7883
|
console.error('Error loading OBJ model:', error);
|
|
7661
|
-
|
|
7884
|
+
finish();
|
|
7662
7885
|
});
|
|
7663
7886
|
}, undefined, (error) => {
|
|
7664
7887
|
console.error('Error loading MTL file:', error);
|
|
7665
|
-
|
|
7888
|
+
finish();
|
|
7666
7889
|
});
|
|
7667
7890
|
}
|
|
7668
7891
|
/** Centers, scales and adds a loaded object to the scene, then frames the camera on it. */
|
|
7669
7892
|
placeLoadedObjectInScene(object, cameraPosition, rotation, up) {
|
|
7893
|
+
// A scene shows one model. Whatever was there is taken out and freed first, or the two would be
|
|
7894
|
+
// drawn through each other, every frame, for as long as the viewer stays open.
|
|
7895
|
+
if (this.modelRoot) {
|
|
7896
|
+
// Points being clicked for a calibration or a measurement were clicked on the model that is
|
|
7897
|
+
// about to go; they say nothing about the one arriving.
|
|
7898
|
+
if (this.aligning) {
|
|
7899
|
+
this.cancelTagAlignment();
|
|
7900
|
+
}
|
|
7901
|
+
if (this.measuring) {
|
|
7902
|
+
this.cancelMeasuring();
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
this.removeModel();
|
|
7670
7906
|
const box = new THREE.Box3().setFromObject(object);
|
|
7671
7907
|
const center = box.getCenter(new THREE.Vector3());
|
|
7672
7908
|
const size = box.getSize(new THREE.Vector3());
|
|
@@ -7703,8 +7939,9 @@ class ThreeJsService {
|
|
|
7703
7939
|
group.add(centered);
|
|
7704
7940
|
group.scale.setScalar(scale);
|
|
7705
7941
|
this.scene.add(group);
|
|
7706
|
-
|
|
7707
|
-
|
|
7942
|
+
this.scene.updateMatrixWorld(true);
|
|
7943
|
+
// Markers registered before the model finished loading were sized against the old scale.
|
|
7944
|
+
this.refreshScreenSizes();
|
|
7708
7945
|
this.adjustClippingPlanes((size.length() / 2) * scale);
|
|
7709
7946
|
if (cameraPosition) {
|
|
7710
7947
|
this.camera.position.set(cameraPosition.x, cameraPosition.y, cameraPosition.z);
|
|
@@ -7720,16 +7957,25 @@ class ThreeJsService {
|
|
|
7720
7957
|
* not clear enough to act on.
|
|
7721
7958
|
*
|
|
7722
7959
|
* Models arrive in two conventions: Y up (glTF, and what this viewer assumes) and Z up (most CAD
|
|
7723
|
-
* and scanning exports). A Z-up model loads lying on its side.
|
|
7724
|
-
* axis is the vertical one
|
|
7960
|
+
* and scanning exports). A Z-up model loads lying on its side. Three independent cues say which
|
|
7961
|
+
* axis is the vertical one:
|
|
7725
7962
|
*
|
|
7726
7963
|
* - Surface area. Floors and ceilings all face along the vertical, while walls are shared out
|
|
7727
7964
|
* between the two horizontal axes, so the vertical usually collects the most area.
|
|
7728
7965
|
* - Extent. A building is normally wider than it is tall, so the vertical is its shortest side.
|
|
7966
|
+
* - Openness. A capture made from outside has ground and roofs but nothing underneath them, so
|
|
7967
|
+
* along the vertical nearly all of its surface faces one way; walls face both ways evenly.
|
|
7968
|
+
*
|
|
7969
|
+
* Each is wrong often enough on its own - a long corridor has more wall than floor, a tower is
|
|
7970
|
+
* taller than it is wide - so a cue only counts when its answer is clear-cut, and the model is
|
|
7971
|
+
* turned when the clear answers agree. It used to take area and extent agreeing with each other,
|
|
7972
|
+
* which failed on exactly the models that need this most: in a whole building or a city block
|
|
7973
|
+
* there is as much wall as floor, the area cue comes out a three-way tie, and a model 130 m wide
|
|
7974
|
+
* and 34 m tall with 86% of its surface facing one way was left lying on its side.
|
|
7729
7975
|
*
|
|
7730
|
-
*
|
|
7731
|
-
*
|
|
7732
|
-
*
|
|
7976
|
+
* Turning a model that was fine is worse than leaving one that was not: when the clear answers
|
|
7977
|
+
* contradict each other, or there is none, nothing is turned. A model that already has Y as its
|
|
7978
|
+
* vertical is never touched.
|
|
7733
7979
|
*/
|
|
7734
7980
|
detectModelUp(object, size) {
|
|
7735
7981
|
const axes = ['x', 'y', 'z'];
|
|
@@ -7744,6 +7990,7 @@ class ThreeJsService {
|
|
|
7744
7990
|
}
|
|
7745
7991
|
});
|
|
7746
7992
|
if (triangleCount === 0) {
|
|
7993
|
+
// Nothing with faces to weigh: the convention.
|
|
7747
7994
|
return 'y';
|
|
7748
7995
|
}
|
|
7749
7996
|
// A sample is plenty to see where the area lies, and keeps this cheap on models with millions
|
|
@@ -7774,16 +8021,45 @@ class ThreeJsService {
|
|
|
7774
8021
|
}
|
|
7775
8022
|
});
|
|
7776
8023
|
const total = { x: area.x[0] + area.x[1], y: area.y[0] + area.y[1], z: area.z[0] + area.z[1] };
|
|
7777
|
-
const
|
|
7778
|
-
|
|
7779
|
-
|
|
8024
|
+
const extent = { x: size.x, y: size.y, z: size.z };
|
|
8025
|
+
// How one-sided the surface is along each axis: 0 when it faces both ways evenly, 1 when it
|
|
8026
|
+
// all faces the same way.
|
|
8027
|
+
const lopsided = { x: 0, y: 0, z: 0 };
|
|
8028
|
+
axes.forEach((axis) => {
|
|
8029
|
+
lopsided[axis] = total[axis] > 0 ? Math.abs(area[axis][0] - area[axis][1]) / total[axis] : 0;
|
|
8030
|
+
});
|
|
8031
|
+
/** The axes ordered by a measure, largest first. */
|
|
8032
|
+
const ranked = (measure) => [...axes].sort((first, second) => measure[second] - measure[first]);
|
|
8033
|
+
const [mostArea, nextArea] = ranked(total);
|
|
8034
|
+
const [, middleExtent, shortest] = ranked(extent);
|
|
8035
|
+
const [mostOpen, nextOpen] = ranked(lopsided);
|
|
8036
|
+
// What each cue says, when it says it clearly; null when it has no clear answer.
|
|
8037
|
+
const areaSays = total[mostArea] >= 1.25 * total[nextArea] ? mostArea : null;
|
|
8038
|
+
const extentSays = extent[middleExtent] >= 1.5 * extent[shortest] ? shortest : null;
|
|
8039
|
+
const opennessSays = lopsided[mostOpen] >= 0.5
|
|
8040
|
+
&& lopsided[mostOpen] >= 2 * lopsided[nextOpen]
|
|
8041
|
+
// Not on the strength of a few stray triangles: the surface along that axis has to matter.
|
|
8042
|
+
&& total[mostOpen] >= 0.25 * total[mostArea]
|
|
8043
|
+
? mostOpen
|
|
8044
|
+
: null;
|
|
8045
|
+
const clear = [areaSays, extentSays, opennessSays].filter((axis) => axis !== null);
|
|
8046
|
+
let vertical = 'y';
|
|
8047
|
+
if (clear.length > 0) {
|
|
8048
|
+
// Clear answers that contradict each other are no answer.
|
|
8049
|
+
vertical = clear.every((axis) => axis === clear[0]) ? clear[0] : 'y';
|
|
8050
|
+
}
|
|
8051
|
+
else if (mostArea === shortest && total[mostArea] >= 1.25 * total.y) {
|
|
8052
|
+
// Nothing clear-cut, but area and extent lean the same way and away from Y.
|
|
8053
|
+
vertical = mostArea;
|
|
8054
|
+
}
|
|
8055
|
+
if (vertical === 'y') {
|
|
7780
8056
|
return 'y';
|
|
7781
8057
|
}
|
|
7782
8058
|
// Which way along that axis is up. Roofs and ground make an outdoor model face mostly upward,
|
|
7783
8059
|
// which settles it; indoors floors and ceilings balance out, and the convention - positive is
|
|
7784
8060
|
// up - is the better guess.
|
|
7785
|
-
const [positive, negative] = area[
|
|
7786
|
-
const up = `${negative > 0.65 * (positive + negative) ? '-' : ''}${
|
|
8061
|
+
const [positive, negative] = area[vertical];
|
|
8062
|
+
const up = `${negative > 0.65 * (positive + negative) ? '-' : ''}${vertical}`;
|
|
7787
8063
|
console.info(`ThreeJsService: this model's vertical axis looks like ${up}, not y - standing it upright.`);
|
|
7788
8064
|
return up;
|
|
7789
8065
|
}
|
|
@@ -7833,6 +8109,7 @@ class ThreeJsService {
|
|
|
7833
8109
|
this.camera.updateProjectionMatrix();
|
|
7834
8110
|
this.renderer.setSize(width, height);
|
|
7835
8111
|
this.updateMeasureLineResolution();
|
|
8112
|
+
this.refreshScreenSizes();
|
|
7836
8113
|
});
|
|
7837
8114
|
this.canvasResizeObserver.observe(container);
|
|
7838
8115
|
}
|
|
@@ -7843,6 +8120,34 @@ class ThreeJsService {
|
|
|
7843
8120
|
* only has to supply the matching spot on the model. No new records are written; nothing is
|
|
7844
8121
|
* applied until solveAlignmentFromPairs() succeeds.
|
|
7845
8122
|
*/
|
|
8123
|
+
/**
|
|
8124
|
+
* Shows every tag that came from elsewhere, whether or not it lands on the model, without arming
|
|
8125
|
+
* anything: clicks still do what they always do.
|
|
8126
|
+
*
|
|
8127
|
+
* Calibrating shows them too, and ending a calibration - saved or abandoned - puts them back
|
|
8128
|
+
* under the usual rule, so this only ever has to be switched on.
|
|
8129
|
+
*/
|
|
8130
|
+
revealCalibrationTags(revealed = true) {
|
|
8131
|
+
this.revealingCalibrationTags = revealed;
|
|
8132
|
+
this.refreshTagVisibility();
|
|
8133
|
+
// Another viewer showing the tags shows these as well now; see showsCalibrationTags.
|
|
8134
|
+
this.announceTagsChanged();
|
|
8135
|
+
}
|
|
8136
|
+
/**
|
|
8137
|
+
* Whether the tags a calibration works with are on show - while one is under way, or its panel
|
|
8138
|
+
* open - including those that do not land on the model. For another viewer showing the tags,
|
|
8139
|
+
* which otherwise leaves out the ones from elsewhere until the model is calibrated.
|
|
8140
|
+
*/
|
|
8141
|
+
showsCalibrationTags() {
|
|
8142
|
+
return this.aligning || this.revealingCalibrationTags;
|
|
8143
|
+
}
|
|
8144
|
+
/** Says a tag was clicked in a viewer drawing the model elsewhere, as a click on it here would. */
|
|
8145
|
+
clickTag(elementId) {
|
|
8146
|
+
const sprite = this.tagSprites.get(elementId);
|
|
8147
|
+
if (sprite) {
|
|
8148
|
+
this.tagClicked$.next({ elementId, poiType: sprite.userData.poiType ?? null });
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
7846
8151
|
startTagAlignment() {
|
|
7847
8152
|
// One pointer mode at a time, the same rule startMeasuring() applies the other way round.
|
|
7848
8153
|
if (this.measuring) {
|
|
@@ -7850,27 +8155,28 @@ class ThreeJsService {
|
|
|
7850
8155
|
}
|
|
7851
8156
|
this.aligning = true;
|
|
7852
8157
|
this.alignmentPairs = [];
|
|
7853
|
-
this.
|
|
8158
|
+
this.clearAlignmentSource();
|
|
7854
8159
|
// While calibrating, the tags from elsewhere are all shown - including the ones off the model,
|
|
7855
8160
|
// which are exactly the ones a calibration is for.
|
|
7856
8161
|
this.refreshTagVisibility();
|
|
7857
8162
|
if (this.renderer) {
|
|
7858
8163
|
this.renderer.domElement.style.cursor = 'crosshair';
|
|
7859
8164
|
}
|
|
7860
|
-
this.
|
|
8165
|
+
this.alignmentChanged();
|
|
8166
|
+
this.announceTagsChanged();
|
|
7861
8167
|
}
|
|
7862
8168
|
cancelTagAlignment() {
|
|
7863
8169
|
const wasAligning = this.aligning;
|
|
7864
8170
|
this.aligning = false;
|
|
8171
|
+
this.revealingCalibrationTags = false;
|
|
7865
8172
|
this.alignmentPairs = [];
|
|
7866
|
-
this.
|
|
7867
|
-
this.pendingPreviewEvent = null;
|
|
7868
|
-
this.removePlacementPreview();
|
|
8173
|
+
this.clearAlignmentSource();
|
|
7869
8174
|
this.refreshTagVisibility();
|
|
7870
8175
|
if (this.renderer) {
|
|
7871
8176
|
this.renderer.domElement.style.cursor = 'default';
|
|
7872
8177
|
}
|
|
7873
|
-
this.
|
|
8178
|
+
this.alignmentChanged();
|
|
8179
|
+
this.announceTagsChanged();
|
|
7874
8180
|
if (wasAligning) {
|
|
7875
8181
|
this.alignmentCancelled$.next();
|
|
7876
8182
|
}
|
|
@@ -7918,11 +8224,10 @@ class ThreeJsService {
|
|
|
7918
8224
|
// Out of calibrating mode first: applying the result re-decides which tags show, and from here
|
|
7919
8225
|
// on that is the ones on the model again, not all of them.
|
|
7920
8226
|
this.aligning = false;
|
|
8227
|
+
this.revealingCalibrationTags = false;
|
|
7921
8228
|
this.applyAlignmentKeepingOwnTags(alignment);
|
|
7922
|
-
this.pendingPreviewEvent = null;
|
|
7923
|
-
this.removePlacementPreview();
|
|
7924
8229
|
this.renderer.domElement.style.cursor = 'default';
|
|
7925
|
-
this.
|
|
8230
|
+
this.alignmentChanged();
|
|
7926
8231
|
return alignment;
|
|
7927
8232
|
}
|
|
7928
8233
|
/**
|
|
@@ -7981,7 +8286,8 @@ class ThreeJsService {
|
|
|
7981
8286
|
*/
|
|
7982
8287
|
selectAlignmentTag(elementId) {
|
|
7983
8288
|
const sprite = this.tagSprites.get(elementId);
|
|
7984
|
-
if (!this.aligning || this.alignmentSource || !
|
|
8289
|
+
if (!this.aligning || this.alignmentSource || !this.canMatchAnotherTag()
|
|
8290
|
+
|| !sprite || !this.calibrationCandidates().includes(sprite)) {
|
|
7985
8291
|
return false;
|
|
7986
8292
|
}
|
|
7987
8293
|
this.setAlignmentSource(sprite);
|
|
@@ -7993,18 +8299,28 @@ class ThreeJsService {
|
|
|
7993
8299
|
const stored = sprite.position;
|
|
7994
8300
|
this.alignmentSource = { x: stored.x, y: stored.y, z: stored.z };
|
|
7995
8301
|
this.alignmentSourceTagId = sprite.name;
|
|
7996
|
-
this.
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8302
|
+
this.alignmentChanged('tag');
|
|
8303
|
+
}
|
|
8304
|
+
clearAlignmentSource() {
|
|
8305
|
+
this.alignmentSource = null;
|
|
8306
|
+
this.alignmentSourceTagId = undefined;
|
|
8307
|
+
}
|
|
8308
|
+
/** Whether the calibration still has room for a pair and a tag to make it from. */
|
|
8309
|
+
canMatchAnotherTag() {
|
|
8310
|
+
return this.alignmentPairs.length < ThreeJsService.CALIBRATION_PAIRS
|
|
8311
|
+
&& this.calibrationCandidates().length > 0;
|
|
8001
8312
|
}
|
|
8002
8313
|
/** Handles a click while the alignment walkthrough is running. */
|
|
8003
8314
|
handleAlignmentClick(event) {
|
|
8004
8315
|
if (!this.alignmentSource) {
|
|
8005
8316
|
// First half of a pair: which tag are we talking about?
|
|
8317
|
+
if (!this.canMatchAnotherTag()) {
|
|
8318
|
+
// Nothing left to match: what remains is to save, or to take a pair back.
|
|
8319
|
+
return;
|
|
8320
|
+
}
|
|
8006
8321
|
const hit = this.raycaster.intersectObjects(this.calibrationCandidates(), false)[0];
|
|
8007
8322
|
if (!hit) {
|
|
8323
|
+
this.alignmentClickIgnored$.next('tag');
|
|
8008
8324
|
return;
|
|
8009
8325
|
}
|
|
8010
8326
|
this.setAlignmentSource(hit.object);
|
|
@@ -8013,18 +8329,220 @@ class ThreeJsService {
|
|
|
8013
8329
|
// Second half: where that tag actually belongs on the model.
|
|
8014
8330
|
const target = this.pickAlignmentTarget(event);
|
|
8015
8331
|
if (!target) {
|
|
8332
|
+
this.alignmentClickIgnored$.next('point');
|
|
8016
8333
|
return;
|
|
8017
8334
|
}
|
|
8018
8335
|
this.alignmentPairs.push({ tagId: this.alignmentSourceTagId, source: this.alignmentSource, target });
|
|
8019
|
-
this.
|
|
8020
|
-
|
|
8336
|
+
this.clearAlignmentSource();
|
|
8337
|
+
this.alignmentChanged('point');
|
|
8338
|
+
}
|
|
8339
|
+
/**
|
|
8340
|
+
* Records where the tag being matched really is, from a spot another viewer picked on the model -
|
|
8341
|
+
* the splat viewer - in the model's own coordinates: the frame pickAlignmentTarget measures a
|
|
8342
|
+
* click in, as placeTagAtModelPoint takes it. False when no tag is waiting for its spot.
|
|
8343
|
+
*/
|
|
8344
|
+
matchAlignmentTagAtModelPoint(point) {
|
|
8345
|
+
if (!this.aligning || !this.alignmentSource) {
|
|
8346
|
+
return false;
|
|
8347
|
+
}
|
|
8348
|
+
this.alignmentPairs.push({
|
|
8349
|
+
tagId: this.alignmentSourceTagId,
|
|
8350
|
+
source: this.alignmentSource,
|
|
8351
|
+
target: { x: point.x, y: point.y, z: point.z },
|
|
8352
|
+
});
|
|
8353
|
+
this.clearAlignmentSource();
|
|
8354
|
+
this.alignmentChanged('point');
|
|
8355
|
+
return true;
|
|
8356
|
+
}
|
|
8357
|
+
/**
|
|
8358
|
+
* Takes back the last thing done in the calibration under way: the choice of tag while its place
|
|
8359
|
+
* on the model is being asked for, the last matched pair otherwise.
|
|
8360
|
+
*
|
|
8361
|
+
* One step at a time, so a click made by mistake costs that click and not the whole calibration.
|
|
8362
|
+
* Returns false when there is nothing to take back.
|
|
8363
|
+
*/
|
|
8364
|
+
undoAlignmentStep() {
|
|
8365
|
+
if (!this.aligning) {
|
|
8366
|
+
return false;
|
|
8367
|
+
}
|
|
8368
|
+
if (this.alignmentSource) {
|
|
8369
|
+
this.clearAlignmentSource();
|
|
8370
|
+
}
|
|
8371
|
+
else if (this.alignmentPairs.length > 0) {
|
|
8372
|
+
this.alignmentPairs.pop();
|
|
8373
|
+
}
|
|
8374
|
+
else {
|
|
8375
|
+
return false;
|
|
8376
|
+
}
|
|
8377
|
+
this.alignmentChanged();
|
|
8378
|
+
return true;
|
|
8379
|
+
}
|
|
8380
|
+
/**
|
|
8381
|
+
* Removes one matched pair, whichever it is, putting its tag back on offer.
|
|
8382
|
+
*
|
|
8383
|
+
* For the point noticed to be wrong only after others were matched: taking steps back one by one
|
|
8384
|
+
* to reach it would throw away the good ones made since.
|
|
8385
|
+
*/
|
|
8386
|
+
removeAlignmentPair(index) {
|
|
8387
|
+
if (!this.aligning || !Number.isInteger(index) || index < 0 || index >= this.alignmentPairs.length) {
|
|
8388
|
+
return false;
|
|
8389
|
+
}
|
|
8390
|
+
this.alignmentPairs.splice(index, 1);
|
|
8391
|
+
this.alignmentChanged();
|
|
8392
|
+
return true;
|
|
8393
|
+
}
|
|
8394
|
+
/** The tags matched so far in the calibration under way, in the order they are numbered in. */
|
|
8395
|
+
getAlignmentPairs() {
|
|
8396
|
+
return this.alignmentPairs.map((pair) => {
|
|
8397
|
+
const sprite = this.tagSprites.get(pair.tagId);
|
|
8398
|
+
return {
|
|
8399
|
+
tagId: pair.tagId,
|
|
8400
|
+
label: sprite?.userData.label ?? null,
|
|
8401
|
+
poiType: sprite?.userData.poiType ?? null,
|
|
8402
|
+
target: { ...pair.target },
|
|
8403
|
+
};
|
|
8404
|
+
});
|
|
8405
|
+
}
|
|
8406
|
+
/**
|
|
8407
|
+
* Brings the scene and the panel in line with the calibration's state, after anything changed it.
|
|
8408
|
+
*
|
|
8409
|
+
* `dropped` names the mark the change put down, if it put one down, so that it can be shown
|
|
8410
|
+
* arriving: that movement is what tells someone their click was taken.
|
|
8411
|
+
*/
|
|
8412
|
+
alignmentChanged(dropped) {
|
|
8413
|
+
// The ghost previews the pin about to be dropped, number included, so it is never carried over
|
|
8414
|
+
// from one state to the next; the next pointer move draws the right one.
|
|
8021
8415
|
this.pendingPreviewEvent = null;
|
|
8022
8416
|
this.removePlacementPreview();
|
|
8417
|
+
if (this.aligning && this.renderer) {
|
|
8418
|
+
this.renderer.domElement.style.cursor = 'crosshair';
|
|
8419
|
+
}
|
|
8420
|
+
this.redrawCalibrationMarks(dropped);
|
|
8421
|
+
let awaiting = null;
|
|
8422
|
+
if (this.aligning) {
|
|
8423
|
+
if (this.alignmentSource) {
|
|
8424
|
+
awaiting = 'point';
|
|
8425
|
+
}
|
|
8426
|
+
else if (this.canMatchAnotherTag()) {
|
|
8427
|
+
// Otherwise done at three, or sooner when there are no more tags to match: fewer pairs
|
|
8428
|
+
// still calibrate, only less completely.
|
|
8429
|
+
awaiting = 'tag';
|
|
8430
|
+
}
|
|
8431
|
+
}
|
|
8023
8432
|
this.alignmentProgress$.next({
|
|
8024
8433
|
pairs: this.alignmentPairs.length,
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8434
|
+
awaiting,
|
|
8435
|
+
tagId: awaiting === 'point' ? this.alignmentSourceTagId : undefined,
|
|
8436
|
+
matched: this.getAlignmentPairs(),
|
|
8437
|
+
});
|
|
8438
|
+
}
|
|
8439
|
+
/**
|
|
8440
|
+
* Draws the calibration as it stands: what has been matched, and what is being matched.
|
|
8441
|
+
*
|
|
8442
|
+
* Redrawn whole on every change rather than patched. There are at most three pairs, and undoing
|
|
8443
|
+
* or removing one renumbers the others.
|
|
8444
|
+
*/
|
|
8445
|
+
redrawCalibrationMarks(dropped) {
|
|
8446
|
+
this.clearCalibrationMarks();
|
|
8447
|
+
if (!this.aligning) {
|
|
8448
|
+
return;
|
|
8449
|
+
}
|
|
8450
|
+
const frame = this.tagGroup.parent ?? this.scene;
|
|
8451
|
+
if (this.calibrationGroup.parent !== frame) {
|
|
8452
|
+
frame.add(this.calibrationGroup);
|
|
8453
|
+
}
|
|
8454
|
+
this.tagGroup.updateMatrix();
|
|
8455
|
+
// Where a tag is drawn at the moment, in the frame the marks are drawn in.
|
|
8456
|
+
const drawnAt = (stored) => new THREE.Vector3(stored.x, stored.y, stored.z).applyMatrix4(this.tagGroup.matrix);
|
|
8457
|
+
this.alignmentPairs.forEach((pair, index) => {
|
|
8458
|
+
const target = new THREE.Vector3(pair.target.x, pair.target.y, pair.target.z);
|
|
8459
|
+
// From where the tag is to where it was said to belong: the correction, made visible.
|
|
8460
|
+
const link = this.createMeasureLine(drawnAt(pair.source), target, ThreeJsService.CALIBRATION_DONE_COLOR, 2);
|
|
8461
|
+
link.material.opacity = 0.75;
|
|
8462
|
+
// The same green as the pin it leads to; see createCalibrationMark.
|
|
8463
|
+
link.material.color.convertSRGBToLinear();
|
|
8464
|
+
link.material.toneMapped = false;
|
|
8465
|
+
// Behind the tags: it sets off from the middle of one, and should come out from under its
|
|
8466
|
+
// icon rather than run across it.
|
|
8467
|
+
link.renderOrder = 997;
|
|
8468
|
+
this.calibrationGroup.add(link);
|
|
8469
|
+
const pin = this.createCalibrationMark(this.createNumberedTexture(index + 1, ThreeJsService.CALIBRATION_DONE_COLOR), 1001);
|
|
8470
|
+
pin.position.copy(target);
|
|
8471
|
+
this.calibrationGroup.add(pin);
|
|
8472
|
+
this.setScreenSize(pin, ThreeJsService.CALIBRATION_PIN_PX);
|
|
8473
|
+
if (dropped === 'point' && index === this.alignmentPairs.length - 1) {
|
|
8474
|
+
this.pulse(pin);
|
|
8475
|
+
}
|
|
8476
|
+
// Its tag is spoken for; it stays in view to show what the pin refers to, but stands back.
|
|
8477
|
+
const matched = this.tagSprites.get(pair.tagId);
|
|
8478
|
+
if (matched) {
|
|
8479
|
+
matched.material.opacity = 0.4;
|
|
8480
|
+
}
|
|
8481
|
+
});
|
|
8482
|
+
if (this.alignmentSource) {
|
|
8483
|
+
const ring = this.createCalibrationMark(this.createRingTexture(ThreeJsService.MEASURE_COLOR), 998);
|
|
8484
|
+
ring.position.copy(drawnAt(this.alignmentSource));
|
|
8485
|
+
this.calibrationGroup.add(ring);
|
|
8486
|
+
this.setScreenSize(ring, ThreeJsService.TAG_SCREEN_PX * 1.6);
|
|
8487
|
+
if (dropped === 'tag') {
|
|
8488
|
+
this.pulse(ring);
|
|
8489
|
+
}
|
|
8490
|
+
}
|
|
8491
|
+
}
|
|
8492
|
+
/** Removes everything drawn for a calibration, and gives the tags it dimmed their look back. */
|
|
8493
|
+
clearCalibrationMarks() {
|
|
8494
|
+
[...this.calibrationGroup.children].forEach((child) => {
|
|
8495
|
+
this.calibrationGroup.remove(child);
|
|
8496
|
+
child.geometry?.dispose?.();
|
|
8497
|
+
child.material?.map?.dispose?.();
|
|
8498
|
+
child.material?.dispose?.();
|
|
8499
|
+
});
|
|
8500
|
+
this.tagSprites.forEach((sprite) => {
|
|
8501
|
+
sprite.material.opacity = 1;
|
|
8502
|
+
});
|
|
8503
|
+
}
|
|
8504
|
+
/**
|
|
8505
|
+
* A calibration mark: a fixed size on screen and showing through the model, as the tags do, and
|
|
8506
|
+
* drawn in the colours it was painted in.
|
|
8507
|
+
*
|
|
8508
|
+
* The renderer's tone mapping and output conversion are there for the model's lighting. Put
|
|
8509
|
+
* through them a mark comes out pale - the amber nearer to cream - and a mark that has to be
|
|
8510
|
+
* picked out against a photograph of a building is the last thing that should be.
|
|
8511
|
+
*/
|
|
8512
|
+
createCalibrationMark(map, renderOrder) {
|
|
8513
|
+
// Says the canvas holds sRGB, which it does, so the output conversion gives it back unchanged.
|
|
8514
|
+
if ('colorSpace' in map) {
|
|
8515
|
+
map.colorSpace = 'srgb'; // THREE.SRGBColorSpace, r152+
|
|
8516
|
+
}
|
|
8517
|
+
else {
|
|
8518
|
+
map.encoding = 3001; // THREE.sRGBEncoding
|
|
8519
|
+
}
|
|
8520
|
+
const mark = new THREE.Sprite(new THREE.SpriteMaterial({
|
|
8521
|
+
map, depthTest: false, transparent: true, sizeAttenuation: false, toneMapped: false,
|
|
8522
|
+
}));
|
|
8523
|
+
mark.renderOrder = renderOrder;
|
|
8524
|
+
return mark;
|
|
8525
|
+
}
|
|
8526
|
+
/** Shows a mark arriving: it starts oversized and settles. */
|
|
8527
|
+
pulse(sprite) {
|
|
8528
|
+
this.pulses.push({ sprite, startedAt: performance.now() });
|
|
8529
|
+
}
|
|
8530
|
+
stepPulses() {
|
|
8531
|
+
if (this.pulses.length === 0) {
|
|
8532
|
+
return;
|
|
8533
|
+
}
|
|
8534
|
+
const now = performance.now();
|
|
8535
|
+
this.pulses = this.pulses.filter(({ sprite, startedAt }) => {
|
|
8536
|
+
const size = sprite.userData.screenSize;
|
|
8537
|
+
if (!sprite.parent || !size) {
|
|
8538
|
+
// Redrawn or cleared in the meantime.
|
|
8539
|
+
return false;
|
|
8540
|
+
}
|
|
8541
|
+
const progress = Math.min(1, (now - startedAt) / ThreeJsService.CONFIRM_PULSE_MS);
|
|
8542
|
+
this.setScreenSize(sprite, size[0], size[1]);
|
|
8543
|
+
// Eased out, so most of the settling happens while the eye is still on the click.
|
|
8544
|
+
sprite.scale.multiplyScalar(1 + 0.9 * (1 - progress) ** 2);
|
|
8545
|
+
return progress < 1;
|
|
8028
8546
|
});
|
|
8029
8547
|
}
|
|
8030
8548
|
/**
|
|
@@ -8049,6 +8567,7 @@ class ThreeJsService {
|
|
|
8049
8567
|
if (this.renderer) {
|
|
8050
8568
|
this.renderer.domElement.style.cursor = 'crosshair';
|
|
8051
8569
|
}
|
|
8570
|
+
this.measuringChanged$.next(true);
|
|
8052
8571
|
this.measurementProgress$.next({ points: 0, distances: [] });
|
|
8053
8572
|
}
|
|
8054
8573
|
/**
|
|
@@ -8069,8 +8588,10 @@ class ThreeJsService {
|
|
|
8069
8588
|
this.renderer.domElement.style.cursor = 'default';
|
|
8070
8589
|
}
|
|
8071
8590
|
if (wasMeasuring) {
|
|
8591
|
+
this.measuringChanged$.next(false);
|
|
8072
8592
|
this.measurementProgress$.next(null);
|
|
8073
8593
|
}
|
|
8594
|
+
this.measurementCleared$.next();
|
|
8074
8595
|
}
|
|
8075
8596
|
/**
|
|
8076
8597
|
* Ends the measurement and returns it, or null when there are not yet two points to measure
|
|
@@ -8079,11 +8600,12 @@ class ThreeJsService {
|
|
|
8079
8600
|
* The drawing stays on the model so it is still visible while the person names the measurement;
|
|
8080
8601
|
* cancelMeasuring() takes it away afterwards.
|
|
8081
8602
|
*/
|
|
8082
|
-
finishMeasuring() {
|
|
8603
|
+
finishMeasuring(screenshot) {
|
|
8083
8604
|
if (!this.measuring || this.measurePoints.length < 2) {
|
|
8084
8605
|
return null;
|
|
8085
8606
|
}
|
|
8086
8607
|
this.measuring = false;
|
|
8608
|
+
this.measuringChanged$.next(false);
|
|
8087
8609
|
this.pendingPreviewEvent = null;
|
|
8088
8610
|
this.removePlacementPreview();
|
|
8089
8611
|
this.updateMeasureRubberBand(null);
|
|
@@ -8091,10 +8613,32 @@ class ThreeJsService {
|
|
|
8091
8613
|
this.lastMeasurement = {
|
|
8092
8614
|
points: this.measurePoints.map((point) => ({ x: point.x, y: point.y, z: point.z })),
|
|
8093
8615
|
distances: this.measureDistances(),
|
|
8094
|
-
|
|
8616
|
+
// From the viewer the model is drawn in, when it is not this scene: the splat viewer.
|
|
8617
|
+
screenshot: screenshot ?? this.captureMeasurementScreenshot(),
|
|
8095
8618
|
};
|
|
8096
8619
|
return this.lastMeasurement;
|
|
8097
8620
|
}
|
|
8621
|
+
/**
|
|
8622
|
+
* Adds a point to the measurement under way from a spot another viewer picked on the model - the
|
|
8623
|
+
* splat viewer - in the model's own coordinates. Into the frame the measurement is kept in, the
|
|
8624
|
+
* tags', through the calibration, as placeTagAtModelPoint does. False when not measuring.
|
|
8625
|
+
*/
|
|
8626
|
+
addMeasurePointAtModelPoint(point) {
|
|
8627
|
+
if (!this.measuring) {
|
|
8628
|
+
return false;
|
|
8629
|
+
}
|
|
8630
|
+
this.tagGroup.updateMatrix();
|
|
8631
|
+
this.addMeasurePoint(new THREE.Vector3(point.x, point.y, point.z).applyMatrix4(this.tagGroup.matrix.clone().invert()));
|
|
8632
|
+
return true;
|
|
8633
|
+
}
|
|
8634
|
+
/** The measurement's points, under way or finished, in the model's own coordinates: for a viewer drawing it elsewhere. */
|
|
8635
|
+
getMeasurePointsInModelFrame() {
|
|
8636
|
+
this.tagGroup.updateMatrix();
|
|
8637
|
+
return this.measurePoints.map((point) => {
|
|
8638
|
+
const { x, y, z } = point.clone().applyMatrix4(this.tagGroup.matrix);
|
|
8639
|
+
return { x, y, z };
|
|
8640
|
+
});
|
|
8641
|
+
}
|
|
8098
8642
|
isMeasuring() {
|
|
8099
8643
|
return this.measuring;
|
|
8100
8644
|
}
|
|
@@ -8123,10 +8667,10 @@ class ThreeJsService {
|
|
|
8123
8667
|
// anything sized in metres would cover the very thing being measured.
|
|
8124
8668
|
sizeAttenuation: false,
|
|
8125
8669
|
}));
|
|
8126
|
-
marker.scale.set(0.022, 0.022, 1);
|
|
8127
8670
|
marker.position.copy(point);
|
|
8128
8671
|
marker.renderOrder = 1001;
|
|
8129
8672
|
this.measureGroup.add(marker);
|
|
8673
|
+
this.setScreenSize(marker, 12);
|
|
8130
8674
|
if (previous) {
|
|
8131
8675
|
this.measureGroup.add(this.createMeasureLine(previous, point));
|
|
8132
8676
|
const distance = Math.round(previous.distanceTo(point) * 100) / 100;
|
|
@@ -8136,10 +8680,10 @@ class ThreeJsService {
|
|
|
8136
8680
|
transparent: true,
|
|
8137
8681
|
sizeAttenuation: false,
|
|
8138
8682
|
}));
|
|
8139
|
-
label.scale.set(0.15, 0.05, 1);
|
|
8140
8683
|
label.position.copy(previous).add(point).multiplyScalar(0.5);
|
|
8141
8684
|
label.renderOrder = 1002;
|
|
8142
8685
|
this.measureGroup.add(label);
|
|
8686
|
+
this.setScreenSize(label, 84, 28);
|
|
8143
8687
|
}
|
|
8144
8688
|
this.measurementProgress$.next({
|
|
8145
8689
|
points: this.measurePoints.length,
|
|
@@ -8152,12 +8696,12 @@ class ThreeJsService {
|
|
|
8152
8696
|
* WebGL's own lines are one pixel wide whatever is asked for, which all but disappears over a
|
|
8153
8697
|
* photographic texture. This kind is drawn as screen-facing geometry, so the width holds.
|
|
8154
8698
|
*/
|
|
8155
|
-
createMeasureLine(from, to) {
|
|
8699
|
+
createMeasureLine(from, to, color = ThreeJsService.MEASURE_COLOR, width = 3) {
|
|
8156
8700
|
const geometry = new LineGeometry();
|
|
8157
8701
|
geometry.setPositions([from.x, from.y, from.z, to.x, to.y, to.z]);
|
|
8158
8702
|
const material = new LineMaterial({
|
|
8159
|
-
color: new THREE.Color(
|
|
8160
|
-
linewidth:
|
|
8703
|
+
color: new THREE.Color(color).getHex(),
|
|
8704
|
+
linewidth: width,
|
|
8161
8705
|
depthTest: false,
|
|
8162
8706
|
transparent: true,
|
|
8163
8707
|
});
|
|
@@ -8173,7 +8717,7 @@ class ThreeJsService {
|
|
|
8173
8717
|
return;
|
|
8174
8718
|
}
|
|
8175
8719
|
const size = this.renderer.getSize(new THREE.Vector2());
|
|
8176
|
-
this.measureGroup.children.forEach((child) => {
|
|
8720
|
+
[...this.measureGroup.children, ...this.calibrationGroup.children].forEach((child) => {
|
|
8177
8721
|
if (child.material?.resolution) {
|
|
8178
8722
|
child.material.resolution.set(size.x, size.y);
|
|
8179
8723
|
}
|
|
@@ -8259,6 +8803,12 @@ class ThreeJsService {
|
|
|
8259
8803
|
});
|
|
8260
8804
|
return canvas.toDataURL('image/jpeg', 0.85);
|
|
8261
8805
|
}
|
|
8806
|
+
/**
|
|
8807
|
+
* True when tag placement is armed, false when it ends - placed or abandoned. For a viewer drawing
|
|
8808
|
+
* the model instead of this scene, the splat viewer, which picks the spot itself; see
|
|
8809
|
+
* placeTagAtModelPoint.
|
|
8810
|
+
*/
|
|
8811
|
+
placementChanged$ = new Subject();
|
|
8262
8812
|
/** Arms tag placement: the next click on the model drops a tag instead of being ignored. */
|
|
8263
8813
|
startTagPlacement(poiType = null) {
|
|
8264
8814
|
this.placingTag = true;
|
|
@@ -8267,9 +8817,40 @@ class ThreeJsService {
|
|
|
8267
8817
|
if (this.renderer) {
|
|
8268
8818
|
this.renderer.domElement.style.cursor = 'crosshair';
|
|
8269
8819
|
}
|
|
8820
|
+
this.placementChanged$.next(true);
|
|
8821
|
+
}
|
|
8822
|
+
/**
|
|
8823
|
+
* Places the armed tag at a spot another viewer picked on the model - the splat viewer - given
|
|
8824
|
+
* in the model's own coordinates, as a click on the model in this scene would. Into the frame tag
|
|
8825
|
+
* coordinates are stored in, through the calibration, as pickPlacementOnModel does.
|
|
8826
|
+
*/
|
|
8827
|
+
placeTagAtModelPoint(point, normal = null) {
|
|
8828
|
+
if (!this.placingTag) {
|
|
8829
|
+
return null;
|
|
8830
|
+
}
|
|
8831
|
+
this.tagGroup.updateMatrix();
|
|
8832
|
+
const position = new THREE.Vector3(point.x, point.y, point.z).applyMatrix4(this.tagGroup.matrix.clone().invert());
|
|
8833
|
+
// Turned only: the calibration's scale changes no direction.
|
|
8834
|
+
const facing = normal
|
|
8835
|
+
? new THREE.Vector3(normal.x, normal.y, normal.z).applyQuaternion(this.tagGroup.quaternion.clone().invert()).normalize()
|
|
8836
|
+
: new THREE.Vector3(0, 1, 0);
|
|
8837
|
+
const placement = {
|
|
8838
|
+
position: { x: position.x, y: position.y, z: position.z },
|
|
8839
|
+
normal: { x: facing.x, y: facing.y, z: facing.z },
|
|
8840
|
+
poiType: this.placementPoiType,
|
|
8841
|
+
};
|
|
8842
|
+
this.pendingTagPlacement = placement;
|
|
8843
|
+
this.placingTag = false;
|
|
8844
|
+
this.removePlacementPreview();
|
|
8845
|
+
this.placementChanged$.next(false);
|
|
8846
|
+
this.tagPlaced$.next(placement);
|
|
8847
|
+
return placement;
|
|
8270
8848
|
}
|
|
8271
8849
|
/** Leaves placement mode without placing anything. */
|
|
8272
8850
|
cancelTagPlacement() {
|
|
8851
|
+
if (this.placingTag) {
|
|
8852
|
+
this.placementChanged$.next(false);
|
|
8853
|
+
}
|
|
8273
8854
|
this.placingTag = false;
|
|
8274
8855
|
this.pendingTagPlacement = null;
|
|
8275
8856
|
this.pendingPreviewEvent = null;
|
|
@@ -8294,15 +8875,20 @@ class ThreeJsService {
|
|
|
8294
8875
|
}
|
|
8295
8876
|
this.renderer.domElement.style.cursor = 'crosshair';
|
|
8296
8877
|
if (!this.placementPreview) {
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8878
|
+
// While calibrating, the ghost is the pin the click will leave, number and all: what is
|
|
8879
|
+
// under the pointer is what stays there.
|
|
8880
|
+
this.placementPreview = this.aligning
|
|
8881
|
+
? this.createCalibrationMark(this.createNumberedTexture(this.alignmentPairs.length + 1, ThreeJsService.CALIBRATION_DONE_COLOR), 999)
|
|
8882
|
+
: new THREE.Sprite(new THREE.SpriteMaterial({
|
|
8883
|
+
map: this.createTagTexture('#9E9E9E'),
|
|
8884
|
+
depthTest: false,
|
|
8885
|
+
transparent: true,
|
|
8886
|
+
sizeAttenuation: false,
|
|
8887
|
+
}));
|
|
8303
8888
|
this.placementPreview.renderOrder = 999;
|
|
8889
|
+
this.placementPreview.material.opacity = 0.6;
|
|
8304
8890
|
this.tagGroup.add(this.placementPreview);
|
|
8305
|
-
this.
|
|
8891
|
+
this.setScreenSize(this.placementPreview, this.aligning ? ThreeJsService.CALIBRATION_PIN_PX : ThreeJsService.TAG_SCREEN_PX);
|
|
8306
8892
|
}
|
|
8307
8893
|
this.placementPreview.position.copy(position);
|
|
8308
8894
|
this.updateMeasureRubberBand(position);
|
|
@@ -8409,6 +8995,16 @@ class ThreeJsService {
|
|
|
8409
8995
|
setVisibleTagTypes(types) {
|
|
8410
8996
|
this.visibleTagTypes = types;
|
|
8411
8997
|
this.tagSprites.forEach((sprite) => this.applyTagVisibility(sprite));
|
|
8998
|
+
this.announceTagsChanged();
|
|
8999
|
+
}
|
|
9000
|
+
/** Says whether the visit on screen is the site's only one; see soleVisit. */
|
|
9001
|
+
setSoleVisit(sole) {
|
|
9002
|
+
this.soleVisit = sole;
|
|
9003
|
+
this.tagSprites.forEach((sprite) => {
|
|
9004
|
+
this.parentTag(sprite);
|
|
9005
|
+
this.applyTagVisibility(sprite);
|
|
9006
|
+
});
|
|
9007
|
+
this.announceTagsChanged();
|
|
8412
9008
|
}
|
|
8413
9009
|
/** Says which capture is on screen, so the tags placed in it can be told from the rest. */
|
|
8414
9010
|
setCurrentCapture(captureID) {
|
|
@@ -8417,6 +9013,7 @@ class ThreeJsService {
|
|
|
8417
9013
|
this.parentTag(sprite);
|
|
8418
9014
|
this.applyTagVisibility(sprite);
|
|
8419
9015
|
});
|
|
9016
|
+
this.announceTagsChanged();
|
|
8420
9017
|
}
|
|
8421
9018
|
/** Every tag registered in the scene, shown or not. */
|
|
8422
9019
|
getTags() {
|
|
@@ -8492,8 +9089,8 @@ class ThreeJsService {
|
|
|
8492
9089
|
}
|
|
8493
9090
|
/** Whether the tag was placed in the visit on screen. */
|
|
8494
9091
|
isOwnedByVisit(sprite) {
|
|
8495
|
-
|
|
8496
|
-
|
|
9092
|
+
const origin = sprite.userData.originCaptureID;
|
|
9093
|
+
return origin ? origin === this.currentCaptureID : this.soleVisit;
|
|
8497
9094
|
}
|
|
8498
9095
|
/**
|
|
8499
9096
|
* A tag shows when its type is not filtered out and it belongs on this model.
|
|
@@ -8522,8 +9119,9 @@ class ThreeJsService {
|
|
|
8522
9119
|
if (sprite.userData.frame === 'model') {
|
|
8523
9120
|
return false;
|
|
8524
9121
|
}
|
|
8525
|
-
// Calibrating is matching tags to where they belong, so they all have to be on offer
|
|
8526
|
-
|
|
9122
|
+
// Calibrating is matching tags to where they belong, so they all have to be on offer - from
|
|
9123
|
+
// the moment the panel opens, not only once the first click is being waited for.
|
|
9124
|
+
if (this.aligning || this.revealingCalibrationTags) {
|
|
8527
9125
|
return true;
|
|
8528
9126
|
}
|
|
8529
9127
|
return this.isOnModel(sprite);
|
|
@@ -8661,6 +9259,9 @@ class ThreeJsService {
|
|
|
8661
9259
|
*/
|
|
8662
9260
|
goToTag(elementId) {
|
|
8663
9261
|
const sprite = this.tagSprites.get(elementId);
|
|
9262
|
+
if (sprite) {
|
|
9263
|
+
this.tagFocused$.next(elementId);
|
|
9264
|
+
}
|
|
8664
9265
|
if (!sprite || !this.camera) {
|
|
8665
9266
|
return;
|
|
8666
9267
|
}
|
|
@@ -8782,10 +9383,11 @@ class ThreeJsService {
|
|
|
8782
9383
|
this.tagGroup.rotation.set(rotation?.x ?? 0, rotation?.y ?? 0, rotation?.z ?? 0);
|
|
8783
9384
|
this.tagGroup.scale.setScalar(scale ?? 1);
|
|
8784
9385
|
this.tagGroup.updateMatrix();
|
|
8785
|
-
//
|
|
8786
|
-
// is which tags land on the model.
|
|
8787
|
-
this.
|
|
9386
|
+
// Marker sizes are worked back from the scale they inherit, which this call just changed - and
|
|
9387
|
+
// so is which tags land on the model.
|
|
9388
|
+
this.refreshScreenSizes();
|
|
8788
9389
|
this.refreshTagVisibility();
|
|
9390
|
+
this.announceTagsChanged();
|
|
8789
9391
|
}
|
|
8790
9392
|
/**
|
|
8791
9393
|
* Adds a tag, or moves/restyles the one already registered for this element.
|
|
@@ -8802,22 +9404,29 @@ class ThreeJsService {
|
|
|
8802
9404
|
existing.userData.label = tag.label ?? existing.userData.label;
|
|
8803
9405
|
existing.userData.originCaptureID = tag.originCaptureID ?? existing.userData.originCaptureID;
|
|
8804
9406
|
existing.userData.frame = tag.frame ?? existing.userData.frame;
|
|
9407
|
+
existing.userData.iconUrl = tag.iconUrl ?? existing.userData.iconUrl;
|
|
9408
|
+
existing.userData.normal = tag.normal ?? existing.userData.normal;
|
|
8805
9409
|
this.parentTag(existing);
|
|
8806
9410
|
this.applyTagVisibility(existing);
|
|
9411
|
+
this.announceTagsChanged();
|
|
8807
9412
|
return;
|
|
8808
9413
|
}
|
|
8809
9414
|
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
|
|
8810
9415
|
map: this.createTagTexture(tag.color),
|
|
8811
9416
|
// Tags read as UI rather than geometry, so they stay visible through the model instead of
|
|
8812
|
-
// being occluded by whatever wall happens to be between them and the camera
|
|
9417
|
+
// being occluded by whatever wall happens to be between them and the camera - and, for
|
|
9418
|
+
// the same reason, keep one size on screen rather than a size in the model.
|
|
8813
9419
|
depthTest: false,
|
|
8814
9420
|
transparent: true,
|
|
9421
|
+
sizeAttenuation: false,
|
|
8815
9422
|
}));
|
|
8816
9423
|
sprite.name = tag.id;
|
|
8817
9424
|
sprite.userData.poiType = tag.poiType ?? null;
|
|
8818
9425
|
sprite.userData.label = tag.label ?? null;
|
|
8819
9426
|
sprite.userData.originCaptureID = tag.originCaptureID ?? null;
|
|
8820
9427
|
sprite.userData.frame = tag.frame ?? null;
|
|
9428
|
+
sprite.userData.iconUrl = tag.iconUrl ?? null;
|
|
9429
|
+
sprite.userData.normal = tag.normal ?? null;
|
|
8821
9430
|
sprite.position.set(tag.position.x, tag.position.y, tag.position.z);
|
|
8822
9431
|
sprite.renderOrder = 999;
|
|
8823
9432
|
this.parentTag(sprite);
|
|
@@ -8827,6 +9436,52 @@ class ThreeJsService {
|
|
|
8827
9436
|
if (tag.iconUrl) {
|
|
8828
9437
|
this.applyTagIcon(sprite, tag.iconUrl);
|
|
8829
9438
|
}
|
|
9439
|
+
this.announceTagsChanged();
|
|
9440
|
+
}
|
|
9441
|
+
/**
|
|
9442
|
+
* The tags another viewer should show, and the calibration they are drawn under.
|
|
9443
|
+
*
|
|
9444
|
+
* The same ones this scene would offer, short of the test against the model's surface, which needs
|
|
9445
|
+
* the model here: the type filter applies, and a tag placed in another model before it was
|
|
9446
|
+
* calibrated is left out, its coordinates meaning nothing in this one.
|
|
9447
|
+
*/
|
|
9448
|
+
getTagSnapshot() {
|
|
9449
|
+
const tags = [];
|
|
9450
|
+
this.tagSprites.forEach((sprite) => {
|
|
9451
|
+
const owned = this.isOwnedByVisit(sprite);
|
|
9452
|
+
const typeAllowed = this.visibleTagTypes === null || this.visibleTagTypes.includes(sprite.userData.poiType);
|
|
9453
|
+
if (!typeAllowed || (sprite.userData.frame === 'model' && !owned)) {
|
|
9454
|
+
return;
|
|
9455
|
+
}
|
|
9456
|
+
tags.push({
|
|
9457
|
+
id: sprite.name,
|
|
9458
|
+
position: { x: sprite.position.x, y: sprite.position.y, z: sprite.position.z },
|
|
9459
|
+
label: sprite.userData.label ?? null,
|
|
9460
|
+
poiType: sprite.userData.poiType ?? null,
|
|
9461
|
+
iconUrl: sprite.userData.iconUrl ?? null,
|
|
9462
|
+
inModelFrame: sprite.userData.frame === 'model' && owned,
|
|
9463
|
+
normal: sprite.userData.normal ?? null,
|
|
9464
|
+
});
|
|
9465
|
+
});
|
|
9466
|
+
const group = this.tagGroup;
|
|
9467
|
+
return {
|
|
9468
|
+
tags,
|
|
9469
|
+
alignment: {
|
|
9470
|
+
translation: { x: group.position.x, y: group.position.y, z: group.position.z },
|
|
9471
|
+
rotation: { x: group.rotation.x, y: group.rotation.y, z: group.rotation.z },
|
|
9472
|
+
scale: group.scale.x,
|
|
9473
|
+
},
|
|
9474
|
+
};
|
|
9475
|
+
}
|
|
9476
|
+
announceTagsChanged() {
|
|
9477
|
+
if (this.tagsChangeQueued) {
|
|
9478
|
+
return;
|
|
9479
|
+
}
|
|
9480
|
+
this.tagsChangeQueued = true;
|
|
9481
|
+
queueMicrotask(() => {
|
|
9482
|
+
this.tagsChangeQueued = false;
|
|
9483
|
+
this.tagsChanged$.next();
|
|
9484
|
+
});
|
|
8830
9485
|
}
|
|
8831
9486
|
/** Removes the tag registered for an element, if there is one. */
|
|
8832
9487
|
removeTag(elementId) {
|
|
@@ -8838,6 +9493,7 @@ class ThreeJsService {
|
|
|
8838
9493
|
sprite.material.map?.dispose();
|
|
8839
9494
|
sprite.material.dispose();
|
|
8840
9495
|
this.tagSprites.delete(elementId);
|
|
9496
|
+
this.announceTagsChanged();
|
|
8841
9497
|
}
|
|
8842
9498
|
/** Removes every tag from the scene. */
|
|
8843
9499
|
clearTags() {
|
|
@@ -8867,18 +9523,78 @@ class ThreeJsService {
|
|
|
8867
9523
|
context.stroke();
|
|
8868
9524
|
return new THREE.CanvasTexture(canvas);
|
|
8869
9525
|
}
|
|
9526
|
+
/** The default marker with a number on it: the nth point of a calibration. */
|
|
9527
|
+
createNumberedTexture(number, color) {
|
|
9528
|
+
const texture = this.createTagTexture(color);
|
|
9529
|
+
const canvas = texture.image;
|
|
9530
|
+
const context = canvas.getContext('2d');
|
|
9531
|
+
context.fillStyle = '#FFFFFF';
|
|
9532
|
+
context.font = 'bold 76px sans-serif';
|
|
9533
|
+
context.textAlign = 'center';
|
|
9534
|
+
context.textBaseline = 'middle';
|
|
9535
|
+
context.fillText(String(number), canvas.width / 2, canvas.height / 2 + 5);
|
|
9536
|
+
return texture;
|
|
9537
|
+
}
|
|
9538
|
+
/** A ring with nothing in the middle, to go round a tag and leave it showing. */
|
|
9539
|
+
createRingTexture(color) {
|
|
9540
|
+
const size = 128;
|
|
9541
|
+
const canvas = document.createElement('canvas');
|
|
9542
|
+
canvas.width = size;
|
|
9543
|
+
canvas.height = size;
|
|
9544
|
+
const context = canvas.getContext('2d');
|
|
9545
|
+
// White under the colour, a little wider, for the same reason the tags have a white edge.
|
|
9546
|
+
context.beginPath();
|
|
9547
|
+
context.arc(size / 2, size / 2, size / 2 - 10, 0, Math.PI * 2);
|
|
9548
|
+
context.lineWidth = 16;
|
|
9549
|
+
context.strokeStyle = '#FFFFFF';
|
|
9550
|
+
context.stroke();
|
|
9551
|
+
context.lineWidth = 10;
|
|
9552
|
+
context.strokeStyle = color;
|
|
9553
|
+
context.stroke();
|
|
9554
|
+
return new THREE.CanvasTexture(canvas);
|
|
9555
|
+
}
|
|
8870
9556
|
/**
|
|
8871
|
-
*
|
|
9557
|
+
* Gives a tag its size: the same number of pixels on screen whatever the model.
|
|
8872
9558
|
*
|
|
8873
|
-
*
|
|
8874
|
-
* a
|
|
8875
|
-
*
|
|
9559
|
+
* It used to be a fixed fraction of the model instead. That reads fine on a room, but on a model
|
|
9560
|
+
* the size of a city block it is a tag several metres across, which fills the view as soon as
|
|
9561
|
+
* the camera moves in to look at anything.
|
|
8876
9562
|
*/
|
|
8877
9563
|
scaleTagForModel(sprite) {
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
9564
|
+
this.setScreenSize(sprite, ThreeJsService.TAG_SCREEN_PX);
|
|
9565
|
+
}
|
|
9566
|
+
/**
|
|
9567
|
+
* Fixes a sprite's size on screen, in CSS pixels.
|
|
9568
|
+
*
|
|
9569
|
+
* The sprite must be drawn without size attenuation, which makes its size a proportion of the
|
|
9570
|
+
* viewport instead of something that shrinks with distance. Its scale is then worked back from
|
|
9571
|
+
* the pixels wanted, the viewport's height and the scale it inherits from its parents - the
|
|
9572
|
+
* model's normalisation and, for tags, the calibration - so none of those show in the result.
|
|
9573
|
+
* The size asked for is kept on the sprite, so it can be worked out again when any of them changes.
|
|
9574
|
+
*/
|
|
9575
|
+
setScreenSize(sprite, widthPx, heightPx = widthPx) {
|
|
9576
|
+
sprite.userData.screenSize = [widthPx, heightPx];
|
|
9577
|
+
const inherited = (sprite.parent ?? this.tagGroup).getWorldScale(new THREE.Vector3()).x || 1;
|
|
9578
|
+
const perPixel = this.worldUnitsPerPixel() / inherited;
|
|
9579
|
+
sprite.scale.set(widthPx * perPixel, heightPx * perPixel, 1);
|
|
9580
|
+
}
|
|
9581
|
+
/** What one screen pixel is worth in the scale of a sprite drawn without size attenuation. */
|
|
9582
|
+
worldUnitsPerPixel() {
|
|
9583
|
+
const height = this.renderer?.getSize?.(new THREE.Vector2()).y || 800;
|
|
9584
|
+
const fieldOfView = THREE.MathUtils.degToRad(this.camera?.fov ?? 75);
|
|
9585
|
+
return (2 * Math.tan(fieldOfView / 2)) / height;
|
|
9586
|
+
}
|
|
9587
|
+
/** Re-applies every marker's on-screen size: the viewport, the model or the calibration changed. */
|
|
9588
|
+
refreshScreenSizes() {
|
|
9589
|
+
const resize = (object) => {
|
|
9590
|
+
if (object?.userData?.screenSize) {
|
|
9591
|
+
this.setScreenSize(object, object.userData.screenSize[0], object.userData.screenSize[1]);
|
|
9592
|
+
}
|
|
9593
|
+
};
|
|
9594
|
+
this.tagSprites.forEach(resize);
|
|
9595
|
+
this.measureGroup.children.forEach(resize);
|
|
9596
|
+
this.calibrationGroup.children.forEach(resize);
|
|
9597
|
+
resize(this.placementPreview);
|
|
8882
9598
|
}
|
|
8883
9599
|
/**
|
|
8884
9600
|
* Adds secondary interactive objects (e.g., POIs, sensors) to the scene.
|
|
@@ -8992,6 +9708,7 @@ class ThreeJsService {
|
|
|
8992
9708
|
this.placingTag = false;
|
|
8993
9709
|
this.removePlacementPreview();
|
|
8994
9710
|
this.renderer.domElement.style.cursor = 'default';
|
|
9711
|
+
this.placementChanged$.next(false);
|
|
8995
9712
|
this.tagPlaced$.next(placement);
|
|
8996
9713
|
}
|
|
8997
9714
|
return;
|
|
@@ -9019,12 +9736,19 @@ class ThreeJsService {
|
|
|
9019
9736
|
}
|
|
9020
9737
|
}
|
|
9021
9738
|
/**
|
|
9022
|
-
* Escape cancels whatever the pointer is currently committed to.
|
|
9739
|
+
* Escape cancels whatever the pointer is currently committed to. While calibrating, the undo keys
|
|
9740
|
+
* take back one step of it instead of all of it.
|
|
9023
9741
|
*
|
|
9024
9742
|
* Emits through the same subjects the explicit Cancel buttons use, so the UI driving them follows
|
|
9025
9743
|
* along instead of being left showing a mode that is no longer armed.
|
|
9026
9744
|
*/
|
|
9027
9745
|
onKeyDown = (event) => {
|
|
9746
|
+
if (this.aligning && this.isUndoKey(event)) {
|
|
9747
|
+
if (this.undoAlignmentStep()) {
|
|
9748
|
+
event.preventDefault();
|
|
9749
|
+
}
|
|
9750
|
+
return;
|
|
9751
|
+
}
|
|
9028
9752
|
if (event.key !== 'Escape') {
|
|
9029
9753
|
return;
|
|
9030
9754
|
}
|
|
@@ -9041,9 +9765,22 @@ class ThreeJsService {
|
|
|
9041
9765
|
this.cancelTagAlignment();
|
|
9042
9766
|
}
|
|
9043
9767
|
};
|
|
9768
|
+
/**
|
|
9769
|
+
* Ctrl+Z (Cmd+Z on a Mac) or Backspace, unless typed into a field - where they mean something
|
|
9770
|
+
* else, to whatever the field is.
|
|
9771
|
+
*/
|
|
9772
|
+
isUndoKey(event) {
|
|
9773
|
+
const target = event.target;
|
|
9774
|
+
if (target?.isContentEditable || ['INPUT', 'TEXTAREA'].includes(target?.tagName ?? '')) {
|
|
9775
|
+
return false;
|
|
9776
|
+
}
|
|
9777
|
+
return event.key === 'Backspace'
|
|
9778
|
+
|| ((event.ctrlKey || event.metaKey) && !event.shiftKey && event.key.toLowerCase() === 'z');
|
|
9779
|
+
}
|
|
9044
9780
|
animate = () => {
|
|
9045
9781
|
this.animationFrameId = requestAnimationFrame(this.animate);
|
|
9046
9782
|
this.stepCameraTween();
|
|
9783
|
+
this.stepPulses();
|
|
9047
9784
|
this.controls.update();
|
|
9048
9785
|
// One pick per frame, against the pointer's latest position - see onMouseMove.
|
|
9049
9786
|
if (this.pendingPreviewEvent) {
|
|
@@ -9053,11 +9790,84 @@ class ThreeJsService {
|
|
|
9053
9790
|
}
|
|
9054
9791
|
this.renderer.render(this.scene, this.camera);
|
|
9055
9792
|
};
|
|
9056
|
-
|
|
9793
|
+
/**
|
|
9794
|
+
* Stops drawing the scene, or starts again. For while another viewer covers it - the splat viewer
|
|
9795
|
+
* - where it would otherwise go on drawing, unseen, every frame, on the graphics card the other
|
|
9796
|
+
* viewer needs. The scene keeps its tags and calibration all the same.
|
|
9797
|
+
*/
|
|
9798
|
+
setRenderingPaused(paused) {
|
|
9799
|
+
if (paused) {
|
|
9800
|
+
this.stopRendering();
|
|
9801
|
+
}
|
|
9802
|
+
else if (this.live && !this.animationFrameId) {
|
|
9803
|
+
this.ngZone.runOutsideAngular(() => this.animate());
|
|
9804
|
+
}
|
|
9805
|
+
}
|
|
9806
|
+
/** Stops the render loop, if one is running. */
|
|
9807
|
+
stopRendering() {
|
|
9057
9808
|
if (this.animationFrameId) {
|
|
9058
9809
|
cancelAnimationFrame(this.animationFrameId);
|
|
9810
|
+
this.animationFrameId = 0;
|
|
9811
|
+
}
|
|
9812
|
+
}
|
|
9813
|
+
/**
|
|
9814
|
+
* Takes the model out of the scene and frees what it holds on the graphics card.
|
|
9815
|
+
*
|
|
9816
|
+
* The tags are kept. They hang from the model's group so as to share its frame, and are moved out
|
|
9817
|
+
* of it first: they belong to the visit, not to the file that was drawn under them.
|
|
9818
|
+
*/
|
|
9819
|
+
removeModel() {
|
|
9820
|
+
const group = this.modelGroup;
|
|
9821
|
+
const model = this.modelRoot;
|
|
9822
|
+
this.modelGroup = undefined;
|
|
9823
|
+
this.modelRoot = undefined;
|
|
9824
|
+
this.surfaceCells = null;
|
|
9825
|
+
this.modelWorldRadius = 0;
|
|
9826
|
+
[this.tagGroup, this.modelTagGroup, this.calibrationGroup].forEach((kept) => kept.parent?.remove(kept));
|
|
9827
|
+
group?.parent?.remove(group);
|
|
9828
|
+
if (model) {
|
|
9829
|
+
this.disposeModel(model);
|
|
9059
9830
|
}
|
|
9831
|
+
}
|
|
9832
|
+
/**
|
|
9833
|
+
* Frees a model's geometry, materials and textures.
|
|
9834
|
+
*
|
|
9835
|
+
* Dropping the last reference to a model does not do this. What it uploaded stays on the graphics
|
|
9836
|
+
* card until the page is closed - hundreds of megabytes for a captured building - and on a
|
|
9837
|
+
* machine whose graphics memory is shared with the rest of the system, everything slows down
|
|
9838
|
+
* long before anything fails.
|
|
9839
|
+
*/
|
|
9840
|
+
disposeModel(model) {
|
|
9841
|
+
const freed = new Set();
|
|
9842
|
+
const free = (resource) => {
|
|
9843
|
+
if (resource && typeof resource.dispose === 'function' && !freed.has(resource)) {
|
|
9844
|
+
freed.add(resource);
|
|
9845
|
+
resource.dispose();
|
|
9846
|
+
}
|
|
9847
|
+
};
|
|
9848
|
+
model.traverse((child) => {
|
|
9849
|
+
free(child.geometry);
|
|
9850
|
+
const materials = Array.isArray(child.material) ? child.material : [child.material];
|
|
9851
|
+
materials.forEach((material) => {
|
|
9852
|
+
if (!material) {
|
|
9853
|
+
return;
|
|
9854
|
+
}
|
|
9855
|
+
// Whichever slots this kind of material has: map, normalMap, emissiveMap...
|
|
9856
|
+
Object.values(material).forEach((value) => {
|
|
9857
|
+
if (value?.isTexture) {
|
|
9858
|
+
free(value);
|
|
9859
|
+
}
|
|
9860
|
+
});
|
|
9861
|
+
free(material);
|
|
9862
|
+
});
|
|
9863
|
+
});
|
|
9864
|
+
}
|
|
9865
|
+
destroy() {
|
|
9866
|
+
this.live = false;
|
|
9867
|
+
this.stopRendering();
|
|
9060
9868
|
window.removeEventListener('keydown', this.onKeyDown);
|
|
9869
|
+
this.canvasListeners.forEach((remove) => remove());
|
|
9870
|
+
this.canvasListeners = [];
|
|
9061
9871
|
if (this.canvasResizeObserver) {
|
|
9062
9872
|
this.canvasResizeObserver.disconnect();
|
|
9063
9873
|
this.canvasResizeObserver = undefined;
|
|
@@ -9066,6 +9876,13 @@ class ThreeJsService {
|
|
|
9066
9876
|
this.clearTags();
|
|
9067
9877
|
this.measuring = false;
|
|
9068
9878
|
this.clearMeasurement();
|
|
9879
|
+
this.clearCalibrationMarks();
|
|
9880
|
+
this.pulses = [];
|
|
9881
|
+
// While the renderer is still there to hand the memory back through.
|
|
9882
|
+
this.removeModel();
|
|
9883
|
+
// They were in the scene that is going. Kept, the next scene would go on testing every pointer
|
|
9884
|
+
// move against objects nobody can see, and would skip loading them again as "already there".
|
|
9885
|
+
this.clickableObjects = [];
|
|
9069
9886
|
if (this.renderer) {
|
|
9070
9887
|
this.renderer.dispose();
|
|
9071
9888
|
}
|
|
@@ -9551,6 +10368,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
9551
10368
|
/* eslint-disable camelcase */
|
|
9552
10369
|
/* eslint-disable no-underscore-dangle */
|
|
9553
10370
|
/* eslint-disable class-methods-use-this */
|
|
10371
|
+
/**
|
|
10372
|
+
* What a Gaussian splat file is named with, when it is a .ply: the content is what tells it from
|
|
10373
|
+
* another .ply, and the key is all a visit stores (see isGaussianSplatPly).
|
|
10374
|
+
*/
|
|
10375
|
+
const GAUSSIAN_SPLAT_PLY_SUFFIX = '.splat.ply';
|
|
10376
|
+
/**
|
|
10377
|
+
* Whether a 3D model file holds Gaussian splats, which the Three.js viewer cannot draw: .spz,
|
|
10378
|
+
* .splat, .ksplat, SuperSplat's .sog, a .ply named with GAUSSIAN_SPLAT_PLY_SUFFIX, or the
|
|
10379
|
+
* lod-meta.json of a Streamed SOG - SuperSplat's levels of detail, in pieces beside it.
|
|
10380
|
+
*/
|
|
10381
|
+
function isGaussianSplatFile(name) {
|
|
10382
|
+
return /\.(spz|splat|ksplat|sog|splat\.ply)($|\?)/i.test(name ?? '') || isStreamedSplatFile(name);
|
|
10383
|
+
}
|
|
10384
|
+
/**
|
|
10385
|
+
* Whether a .ply holds Gaussian splats: what splat tools export carries, for each splat, its colour
|
|
10386
|
+
* as spherical harmonics (f_dc_0...) and its size (scale_0...). Only the header is read.
|
|
10387
|
+
*/
|
|
10388
|
+
async function isGaussianSplatPly(file) {
|
|
10389
|
+
const start = await file.slice(0, 64 * 1024).text();
|
|
10390
|
+
const end = start.indexOf('end_header');
|
|
10391
|
+
if (!start.startsWith('ply') || end < 0) {
|
|
10392
|
+
return false;
|
|
10393
|
+
}
|
|
10394
|
+
const properties = start.slice(0, end).split(/\r?\n/)
|
|
10395
|
+
.map((line) => line.trim().split(/\s+/))
|
|
10396
|
+
.filter((words) => words[0] === 'property')
|
|
10397
|
+
.map((words) => words[words.length - 1]);
|
|
10398
|
+
return properties.includes('f_dc_0') && properties.includes('scale_0');
|
|
10399
|
+
}
|
|
10400
|
+
/** Whether a 3D model file is the lod-meta.json of a Streamed SOG; see isGaussianSplatFile. */
|
|
10401
|
+
function isStreamedSplatFile(name) {
|
|
10402
|
+
return /(^|\/)lod-meta\.json($|\?)/i.test(name ?? '');
|
|
10403
|
+
}
|
|
9554
10404
|
class ViewerService {
|
|
9555
10405
|
sdkKey;
|
|
9556
10406
|
config;
|
|
@@ -9827,15 +10677,35 @@ class ViewerService {
|
|
|
9827
10677
|
if (![position?.x, position?.y, position?.z].every((value) => typeof value === 'number')) {
|
|
9828
10678
|
return;
|
|
9829
10679
|
}
|
|
10680
|
+
// A tag is its position; the icon is how it is dressed. One that cannot be fetched - a
|
|
10681
|
+
// signature that expired, a file gone from the bucket - must not cost the tag itself.
|
|
10682
|
+
let iconUrl;
|
|
10683
|
+
try {
|
|
10684
|
+
iconUrl = await this.resolveTagIconUrl(poi);
|
|
10685
|
+
}
|
|
10686
|
+
catch (error) {
|
|
10687
|
+
console.warn(`ViewerService: no icon for the tag of element ${object.id}, using the default one.`, error);
|
|
10688
|
+
iconUrl = this.defaultTagIconUrl(poi.type);
|
|
10689
|
+
}
|
|
9830
10690
|
const origin = getThreeJsPoiOrigin(poi);
|
|
10691
|
+
// Matterport's stem, kept with the POI by both viewers that place tags.
|
|
10692
|
+
let normal = null;
|
|
10693
|
+
try {
|
|
10694
|
+
const stored = poi.metadata ? JSON.parse(poi.metadata)?.normal : null;
|
|
10695
|
+
normal = [stored?.x, stored?.y, stored?.z].every((value) => typeof value === 'number') ? stored : null;
|
|
10696
|
+
}
|
|
10697
|
+
catch {
|
|
10698
|
+
normal = null;
|
|
10699
|
+
}
|
|
9831
10700
|
this.threeJsService.upsertTag({
|
|
9832
10701
|
id: object.id,
|
|
9833
10702
|
position,
|
|
9834
10703
|
poiType: poi.type,
|
|
9835
10704
|
label: object.name ?? object.title ?? null,
|
|
9836
|
-
iconUrl
|
|
10705
|
+
iconUrl,
|
|
9837
10706
|
originCaptureID: origin?.captureID ?? null,
|
|
9838
10707
|
frame: origin?.frame ?? null,
|
|
10708
|
+
normal,
|
|
9839
10709
|
});
|
|
9840
10710
|
}
|
|
9841
10711
|
/**
|
|
@@ -9953,6 +10823,40 @@ class ViewerService {
|
|
|
9953
10823
|
this.threeJsService.goToTag(elementId);
|
|
9954
10824
|
}
|
|
9955
10825
|
}
|
|
10826
|
+
/** The tag the Three.js camera is sent to, for the splat viewer to point out; see goToThreeJsTag. */
|
|
10827
|
+
get threeJsTagFocused$() {
|
|
10828
|
+
return this.threeJsService.tagFocused$;
|
|
10829
|
+
}
|
|
10830
|
+
/** A tag clicked in the splat viewer, said as threeJsTagClicked$ says one clicked in the scene. */
|
|
10831
|
+
clickThreeJsTag(elementId) {
|
|
10832
|
+
if (this.checkMode('threejs')) {
|
|
10833
|
+
this.threeJsService.clickTag(elementId);
|
|
10834
|
+
}
|
|
10835
|
+
}
|
|
10836
|
+
/** Whether the visit's 3D model has a calibration, from its capture's metadata. */
|
|
10837
|
+
isThreeJsCalibrated() {
|
|
10838
|
+
return this.threeJsCalibrated;
|
|
10839
|
+
}
|
|
10840
|
+
/** Whether the tags a calibration works with are on show; see ThreeJsService.showsCalibrationTags. */
|
|
10841
|
+
isThreeJsShowingCalibrationTags() {
|
|
10842
|
+
return this.checkMode('threejs') && this.threeJsService.showsCalibrationTags();
|
|
10843
|
+
}
|
|
10844
|
+
/** Whether a calibration is under way in the Three.js scene. */
|
|
10845
|
+
isThreeJsAligningTags() {
|
|
10846
|
+
return this.checkMode('threejs') && this.threeJsService.isAligningTags();
|
|
10847
|
+
}
|
|
10848
|
+
/** Whether a measurement is taking clicks in the Three.js scene. */
|
|
10849
|
+
isThreeJsMeasuring() {
|
|
10850
|
+
return this.checkMode('threejs') && this.threeJsService.isMeasuring();
|
|
10851
|
+
}
|
|
10852
|
+
/** True when a measurement starts taking clicks, false when it stops; see ThreeJsService.measuringChanged$. */
|
|
10853
|
+
get threeJsMeasuringChanged$() {
|
|
10854
|
+
return this.threeJsService.measuringChanged$;
|
|
10855
|
+
}
|
|
10856
|
+
/** Says a click in the splat viewer was of no use to the calibration, as one in the scene would. */
|
|
10857
|
+
ignoreThreeJsAlignmentClick(expected) {
|
|
10858
|
+
this.threeJsService.alignmentClickIgnored$.next(expected);
|
|
10859
|
+
}
|
|
9956
10860
|
/** Shows only tags of the given POI types; null clears the filter. */
|
|
9957
10861
|
setVisibleThreeJsTagTypes(types) {
|
|
9958
10862
|
if (this.checkMode('threejs')) {
|
|
@@ -9975,6 +10879,24 @@ class ViewerService {
|
|
|
9975
10879
|
this.threeJsService.cancelTagAlignment();
|
|
9976
10880
|
}
|
|
9977
10881
|
}
|
|
10882
|
+
/**
|
|
10883
|
+
* Says whether the 3D model on screen is the site's only visit. Then there is nowhere else for a
|
|
10884
|
+
* tag to have come from, and the tags that do not record where they were placed are its own.
|
|
10885
|
+
*/
|
|
10886
|
+
setThreeJsSoleVisit(sole) {
|
|
10887
|
+
if (this.checkMode('threejs')) {
|
|
10888
|
+
this.threeJsService.setSoleVisit(sole);
|
|
10889
|
+
}
|
|
10890
|
+
}
|
|
10891
|
+
/**
|
|
10892
|
+
* Shows the tags a calibration would work with, including those hidden for not landing on the
|
|
10893
|
+
* model, before anything is armed. Cancelling or saving the calibration hides them again.
|
|
10894
|
+
*/
|
|
10895
|
+
revealThreeJsCalibrationTags() {
|
|
10896
|
+
if (this.checkMode('threejs')) {
|
|
10897
|
+
this.threeJsService.revealCalibrationTags(true);
|
|
10898
|
+
}
|
|
10899
|
+
}
|
|
9978
10900
|
get threeJsAlignmentProgress$() {
|
|
9979
10901
|
return this.threeJsService.alignmentProgress$;
|
|
9980
10902
|
}
|
|
@@ -10149,17 +11071,35 @@ class ViewerService {
|
|
|
10149
11071
|
get threeJsMeasurementProgress$() {
|
|
10150
11072
|
return this.threeJsService.measurementProgress$;
|
|
10151
11073
|
}
|
|
10152
|
-
/**
|
|
10153
|
-
|
|
11074
|
+
/**
|
|
11075
|
+
* Ends the Three.js measurement in progress, or returns null when it has fewer than two points.
|
|
11076
|
+
* `screenshot` is the picture of it, when the model is drawn by another viewer than the scene.
|
|
11077
|
+
*/
|
|
11078
|
+
finishThreeJsMeasurement(screenshot) {
|
|
10154
11079
|
if (!this.checkMode('threejs')) {
|
|
10155
11080
|
return null;
|
|
10156
11081
|
}
|
|
10157
|
-
const measurement = this.threeJsService.finishMeasuring();
|
|
11082
|
+
const measurement = this.threeJsService.finishMeasuring(screenshot);
|
|
10158
11083
|
if (measurement) {
|
|
10159
11084
|
this.toolboxStates.mesure.stat = false;
|
|
10160
11085
|
}
|
|
10161
11086
|
return measurement;
|
|
10162
11087
|
}
|
|
11088
|
+
/** A measurement was taken off the model, finished or not; see ThreeJsService.measurementCleared$. */
|
|
11089
|
+
get threeJsMeasurementCleared$() {
|
|
11090
|
+
return this.threeJsService.measurementCleared$;
|
|
11091
|
+
}
|
|
11092
|
+
/**
|
|
11093
|
+
* Adds a point to the measurement under way, picked on the model in another viewer - the splat
|
|
11094
|
+
* viewer - in the model's own coordinates. False when no measurement is under way.
|
|
11095
|
+
*/
|
|
11096
|
+
addThreeJsMeasurePointAtModelPoint(point) {
|
|
11097
|
+
return this.checkMode('threejs') && this.threeJsService.addMeasurePointAtModelPoint(point);
|
|
11098
|
+
}
|
|
11099
|
+
/** The points of the measurement on the model, in the model's own coordinates, for another viewer to draw. */
|
|
11100
|
+
getThreeJsMeasurePointsInModelFrame() {
|
|
11101
|
+
return this.checkMode('threejs') ? this.threeJsService.getMeasurePointsInModelFrame() : [];
|
|
11102
|
+
}
|
|
10163
11103
|
/** The measurement last finished in the Three.js viewer, for the form that saves it. */
|
|
10164
11104
|
getThreeJsLastMeasurement() {
|
|
10165
11105
|
return this.checkMode('threejs') ? this.threeJsService.getLastMeasurement() : null;
|
|
@@ -10221,6 +11161,24 @@ class ViewerService {
|
|
|
10221
11161
|
selectThreeJsAlignmentTag(elementId) {
|
|
10222
11162
|
return this.checkMode('threejs') && this.threeJsService.selectAlignmentTag(elementId);
|
|
10223
11163
|
}
|
|
11164
|
+
/**
|
|
11165
|
+
* Gives the tag being matched in the calibration under way its real place, picked on the model in
|
|
11166
|
+
* another viewer - the splat viewer - in the model's own coordinates. False when no tag was waiting.
|
|
11167
|
+
*/
|
|
11168
|
+
matchThreeJsAlignmentTagAtModelPoint(point) {
|
|
11169
|
+
return this.checkMode('threejs') && this.threeJsService.matchAlignmentTagAtModelPoint(point);
|
|
11170
|
+
}
|
|
11171
|
+
/**
|
|
11172
|
+
* Takes back the last step of the calibration under way: the tag just chosen, or else the last
|
|
11173
|
+
* pair matched. False when there was nothing to take back.
|
|
11174
|
+
*/
|
|
11175
|
+
undoThreeJsAlignmentStep() {
|
|
11176
|
+
return this.checkMode('threejs') && this.threeJsService.undoAlignmentStep();
|
|
11177
|
+
}
|
|
11178
|
+
/** Removes one matched pair of the calibration under way, by its place in the list. */
|
|
11179
|
+
removeThreeJsAlignmentPair(index) {
|
|
11180
|
+
return this.checkMode('threejs') && this.threeJsService.removeAlignmentPair(index);
|
|
11181
|
+
}
|
|
10224
11182
|
/** How far the visible tags sit from the 3D model, in model radii. Null without both. */
|
|
10225
11183
|
getThreeJsTagDistanceFromModel() {
|
|
10226
11184
|
return this.checkMode('threejs') ? this.threeJsService.getTagDistanceFromModel() : null;
|
|
@@ -10295,6 +11253,11 @@ class ViewerService {
|
|
|
10295
11253
|
position: placement.position,
|
|
10296
11254
|
poiType: updatedPoi?.type ?? poi.type,
|
|
10297
11255
|
iconUrl: updatedPoi?.tagIcon ? await getSignedFile(updatedPoi.tagIcon) : null,
|
|
11256
|
+
normal: placement.normal,
|
|
11257
|
+
// As it is stored (see buildThreeJsPoiMetadata): placed in this model, and in its own
|
|
11258
|
+
// coordinates while it is not calibrated - so that it shows as the model's own straight away.
|
|
11259
|
+
...(this.currentCaptureID ? { originCaptureID: this.currentCaptureID } : {}),
|
|
11260
|
+
...(this.currentCaptureID && !this.threeJsCalibrated ? { frame: 'model' } : {}),
|
|
10298
11261
|
});
|
|
10299
11262
|
this.threeJsService.cancelTagPlacement();
|
|
10300
11263
|
return updatedPoi;
|
|
@@ -10309,7 +11272,7 @@ class ViewerService {
|
|
|
10309
11272
|
*
|
|
10310
11273
|
* Returns null when there is no pending placement, which is the normal case in Matterport mode.
|
|
10311
11274
|
*/
|
|
10312
|
-
async createPoiForThreeJsPlacement(element, poiType) {
|
|
11275
|
+
async createPoiForThreeJsPlacement(element, poiType, extra = {}) {
|
|
10313
11276
|
if (!this.checkMode('threejs')) {
|
|
10314
11277
|
return null;
|
|
10315
11278
|
}
|
|
@@ -10321,6 +11284,8 @@ class ViewerService {
|
|
|
10321
11284
|
// sweep - the floor plan most visibly, which only draws tags whose sweep belongs to it.
|
|
10322
11285
|
const matterportSweepID = await this.findNearestSweepForThreeJs(placement.position);
|
|
10323
11286
|
const poi = await this.poiService.createPOIForSpaceID({
|
|
11287
|
+
// What a form asked for besides the place - the museum's, say - under what the placement says.
|
|
11288
|
+
...extra,
|
|
10324
11289
|
coordinate: JSON.stringify(placement.position),
|
|
10325
11290
|
type: poiType,
|
|
10326
11291
|
elementID: element.id,
|
|
@@ -10342,6 +11307,35 @@ class ViewerService {
|
|
|
10342
11307
|
get threeJsAlignmentCancelled$() {
|
|
10343
11308
|
return this.threeJsService.alignmentCancelled$;
|
|
10344
11309
|
}
|
|
11310
|
+
/** Emits when the Three.js tags change; see getThreeJsTagSnapshot. */
|
|
11311
|
+
get threeJsTagsChanged$() {
|
|
11312
|
+
return this.threeJsService.tagsChanged$;
|
|
11313
|
+
}
|
|
11314
|
+
/** The tags, and their calibration, as a viewer drawing the scene elsewhere needs them. */
|
|
11315
|
+
getThreeJsTagSnapshot() {
|
|
11316
|
+
return this.threeJsService.getTagSnapshot();
|
|
11317
|
+
}
|
|
11318
|
+
/** Stops drawing the Three.js scene while another viewer covers it, or starts again. */
|
|
11319
|
+
setThreeJsRenderingPaused(paused) {
|
|
11320
|
+
this.threeJsService.setRenderingPaused(paused);
|
|
11321
|
+
}
|
|
11322
|
+
/** Emits when a click during calibration landed on nothing it could use: a tag, or the model. */
|
|
11323
|
+
get threeJsAlignmentClickIgnored$() {
|
|
11324
|
+
return this.threeJsService.alignmentClickIgnored$;
|
|
11325
|
+
}
|
|
11326
|
+
/** Emits when Three.js tag placement is armed (true) and when it ends (false). */
|
|
11327
|
+
get threeJsPlacementChanged$() {
|
|
11328
|
+
return this.threeJsService.placementChanged$;
|
|
11329
|
+
}
|
|
11330
|
+
/**
|
|
11331
|
+
* Completes an armed Three.js tag placement at a spot the splat viewer picked, in the model's own
|
|
11332
|
+
* coordinates: the creation form then opens as for a click in the Three.js scene.
|
|
11333
|
+
*/
|
|
11334
|
+
placeThreeJsTagAtModelPoint(point, normal = null) {
|
|
11335
|
+
if (this.checkMode('threejs')) {
|
|
11336
|
+
this.threeJsService.placeTagAtModelPoint(point, normal);
|
|
11337
|
+
}
|
|
11338
|
+
}
|
|
10345
11339
|
/** Leaves Three.js tag placement mode without placing anything. */
|
|
10346
11340
|
cancelThreeJsTagPlacement() {
|
|
10347
11341
|
if (this.checkMode('threejs')) {
|
|
@@ -10492,7 +11486,12 @@ class ViewerService {
|
|
|
10492
11486
|
}
|
|
10493
11487
|
}
|
|
10494
11488
|
async initMatterportSdk(spaceID, model3D, tagService, module = SpModule.IMMO) {
|
|
10495
|
-
|
|
11489
|
+
// A Three.js visit is announced as loaded by initThreeJs, and when one visit is swapped for
|
|
11490
|
+
// another that has already happened by the time this runs. Clearing the flag here left the
|
|
11491
|
+
// rest of the page believing no viewer was up - "show in 3D" greyed out on every panel.
|
|
11492
|
+
if (!this.checkMode('threejs')) {
|
|
11493
|
+
this.isLoaded = false;
|
|
11494
|
+
}
|
|
10496
11495
|
this.matterportService.currentSpaceID = spaceID;
|
|
10497
11496
|
if (this.matterportService.sdk) {
|
|
10498
11497
|
await this.clearAll();
|
|
@@ -10592,11 +11591,11 @@ class ModalSwitchVisitComponent {
|
|
|
10592
11591
|
});
|
|
10593
11592
|
}
|
|
10594
11593
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ModalSwitchVisitComponent, deps: [{ token: i1$2.NgbActiveModal }, { token: i1$1.Router }, { token: ViewerService }], target: i0.ɵɵFactoryTarget.Component });
|
|
10595
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ModalSwitchVisitComponent, selector: "lib-modal-switch-visit", inputs: { content: "content", model3D: "model3D", spaceID: "spaceID", dataToMove: "dataToMove" }, ngImport: i0, template: "<div class=\"modal-container\">\
|
|
11594
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ModalSwitchVisitComponent, selector: "lib-modal-switch-visit", inputs: { content: "content", model3D: "model3D", spaceID: "spaceID", dataToMove: "dataToMove" }, ngImport: i0, template: "<div class=\"modal-container\">\n <p>{{content}}</p>\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"switchVisit()\">{{'Yes, switch visit' | translate }}</button>\n <button class=\"btn btn-outline-primary rounded-pill\"(click)=\"activeModal.close()\">{{'No, stay here' | translate }}</button>\n</div>\n", styles: [".modal-container{margin:10px}button{margin-right:8px}\n"], dependencies: [{ kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
|
|
10596
11595
|
}
|
|
10597
11596
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ModalSwitchVisitComponent, decorators: [{
|
|
10598
11597
|
type: Component,
|
|
10599
|
-
args: [{ selector: 'lib-modal-switch-visit', template: "<div class=\"modal-container\">\
|
|
11598
|
+
args: [{ selector: 'lib-modal-switch-visit', template: "<div class=\"modal-container\">\n <p>{{content}}</p>\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"switchVisit()\">{{'Yes, switch visit' | translate }}</button>\n <button class=\"btn btn-outline-primary rounded-pill\"(click)=\"activeModal.close()\">{{'No, stay here' | translate }}</button>\n</div>\n", styles: [".modal-container{margin:10px}button{margin-right:8px}\n"] }]
|
|
10600
11599
|
}], ctorParameters: () => [{ type: i1$2.NgbActiveModal }, { type: i1$1.Router }, { type: ViewerService }], propDecorators: { content: [{
|
|
10601
11600
|
type: Input
|
|
10602
11601
|
}], model3D: [{
|
|
@@ -15315,11 +16314,11 @@ class SearchBarComponent {
|
|
|
15315
16314
|
this.searchEvent.emit(results);
|
|
15316
16315
|
}
|
|
15317
16316
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SearchBarComponent, deps: [{ token: SearchService }], target: i0.ɵɵFactoryTarget.Component });
|
|
15318
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SearchBarComponent, selector: "lib-search-bar", inputs: { searchable: "searchable", objectType: "objectType" }, outputs: { searchEvent: "searchEvent" }, ngImport: i0, template: "<div class=\"col-md-8 searchbar\">\
|
|
16317
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SearchBarComponent, selector: "lib-search-bar", inputs: { searchable: "searchable", objectType: "objectType" }, outputs: { searchEvent: "searchEvent" }, ngImport: i0, template: "<div class=\"col-md-8 searchbar\">\n <div class=\"row depend\">\n <div class=\"col-md-5\">\n <input id=\"searchbar\" type=\"text\" class=\"form-control\" placeholder=\"keyword\" />\n </div>\n <div class=\"col-md-5\">\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"onSubmit()\">{{ 'Search' |\n translate}}</button>\n </div>\n </div>\n</div>", styles: [".searchbar{margin-bottom:10px}\n"], dependencies: [{ kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
|
|
15319
16318
|
}
|
|
15320
16319
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SearchBarComponent, decorators: [{
|
|
15321
16320
|
type: Component,
|
|
15322
|
-
args: [{ selector: 'lib-search-bar', template: "<div class=\"col-md-8 searchbar\">\
|
|
16321
|
+
args: [{ selector: 'lib-search-bar', template: "<div class=\"col-md-8 searchbar\">\n <div class=\"row depend\">\n <div class=\"col-md-5\">\n <input id=\"searchbar\" type=\"text\" class=\"form-control\" placeholder=\"keyword\" />\n </div>\n <div class=\"col-md-5\">\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"onSubmit()\">{{ 'Search' |\n translate}}</button>\n </div>\n </div>\n</div>", styles: [".searchbar{margin-bottom:10px}\n"] }]
|
|
15323
16322
|
}], ctorParameters: () => [{ type: SearchService }], propDecorators: { searchable: [{
|
|
15324
16323
|
type: Input
|
|
15325
16324
|
}], objectType: [{
|
|
@@ -15767,11 +16766,11 @@ class LoaderComponent {
|
|
|
15767
16766
|
document.documentElement.style.setProperty('--color', this.color);
|
|
15768
16767
|
}
|
|
15769
16768
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15770
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LoaderComponent, selector: "lib-loader", inputs: { useLogo: "useLogo", color: "color" }, ngImport: i0, template: "<div class=\"sk-fading-circle\">\
|
|
16769
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LoaderComponent, selector: "lib-loader", inputs: { useLogo: "useLogo", color: "color" }, ngImport: i0, template: "<div class=\"sk-fading-circle\">\n <img *ngIf=\"useLogo\" src=\"../../../../assets/icons/smarterplan.svg\">\n <div class=\"sk-circle1 sk-circle\"></div>\n <div class=\"sk-circle2 sk-circle\"></div>\n <div class=\"sk-circle3 sk-circle\"></div>\n <div class=\"sk-circle4 sk-circle\"></div>\n <div class=\"sk-circle5 sk-circle\"></div>\n <div class=\"sk-circle6 sk-circle\"></div>\n <div class=\"sk-circle7 sk-circle\"></div>\n <div class=\"sk-circle8 sk-circle\"></div>\n <div class=\"sk-circle9 sk-circle\"></div>\n <div class=\"sk-circle10 sk-circle\"></div>\n <div class=\"sk-circle11 sk-circle\"></div>\n <div class=\"sk-circle12 sk-circle\"></div>\n</div>\n", styles: [".sk-fading-circle{width:88px;height:88px;position:relative}.sk-fading-circle img{width:2rem;position:absolute;left:calc(50% - 16px);top:calc(50% - 16px)}.sk-fading-circle .sk-circle{width:100%;height:100%;position:absolute;left:0;top:0}.sk-fading-circle .sk-circle:before{content:\"\";display:block;margin:0 auto;width:15%;height:15%;background-color:var(--color);border-radius:100%;-webkit-animation:sk-circleFadeDelay 1.2s infinite ease-in-out both;animation:sk-circleFadeDelay 1.2s infinite ease-in-out both}.sk-fading-circle .sk-circle2{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);transform:rotate(30deg)}.sk-fading-circle .sk-circle3{-webkit-transform:rotate(60deg);-ms-transform:rotate(60deg);transform:rotate(60deg)}.sk-fading-circle .sk-circle4{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.sk-fading-circle .sk-circle5{-webkit-transform:rotate(120deg);-ms-transform:rotate(120deg);transform:rotate(120deg)}.sk-fading-circle .sk-circle6{-webkit-transform:rotate(150deg);-ms-transform:rotate(150deg);transform:rotate(150deg)}.sk-fading-circle .sk-circle7{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.sk-fading-circle .sk-circle8{-webkit-transform:rotate(210deg);-ms-transform:rotate(210deg);transform:rotate(210deg)}.sk-fading-circle .sk-circle9{-webkit-transform:rotate(240deg);-ms-transform:rotate(240deg);transform:rotate(240deg)}.sk-fading-circle .sk-circle10{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.sk-fading-circle .sk-circle11{-webkit-transform:rotate(300deg);-ms-transform:rotate(300deg);transform:rotate(300deg)}.sk-fading-circle .sk-circle12{-webkit-transform:rotate(330deg);-ms-transform:rotate(330deg);transform:rotate(330deg)}.sk-fading-circle .sk-circle2:before{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.sk-fading-circle .sk-circle3:before{-webkit-animation-delay:-1s;animation-delay:-1s}.sk-fading-circle .sk-circle4:before{-webkit-animation-delay:-.9s;animation-delay:-.9s}.sk-fading-circle .sk-circle5:before{-webkit-animation-delay:-.8s;animation-delay:-.8s}.sk-fading-circle .sk-circle6:before{-webkit-animation-delay:-.7s;animation-delay:-.7s}.sk-fading-circle .sk-circle7:before{-webkit-animation-delay:-.6s;animation-delay:-.6s}.sk-fading-circle .sk-circle8:before{-webkit-animation-delay:-.5s;animation-delay:-.5s}.sk-fading-circle .sk-circle9:before{-webkit-animation-delay:-.4s;animation-delay:-.4s}.sk-fading-circle .sk-circle10:before{-webkit-animation-delay:-.3s;animation-delay:-.3s}.sk-fading-circle .sk-circle11:before{-webkit-animation-delay:-.2s;animation-delay:-.2s}.sk-fading-circle .sk-circle12:before{-webkit-animation-delay:-.1s;animation-delay:-.1s}@-webkit-keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}@keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}\n"], dependencies: [{ kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
15771
16770
|
}
|
|
15772
16771
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoaderComponent, decorators: [{
|
|
15773
16772
|
type: Component,
|
|
15774
|
-
args: [{ selector: 'lib-loader', template: "<div class=\"sk-fading-circle\">\
|
|
16773
|
+
args: [{ selector: 'lib-loader', template: "<div class=\"sk-fading-circle\">\n <img *ngIf=\"useLogo\" src=\"../../../../assets/icons/smarterplan.svg\">\n <div class=\"sk-circle1 sk-circle\"></div>\n <div class=\"sk-circle2 sk-circle\"></div>\n <div class=\"sk-circle3 sk-circle\"></div>\n <div class=\"sk-circle4 sk-circle\"></div>\n <div class=\"sk-circle5 sk-circle\"></div>\n <div class=\"sk-circle6 sk-circle\"></div>\n <div class=\"sk-circle7 sk-circle\"></div>\n <div class=\"sk-circle8 sk-circle\"></div>\n <div class=\"sk-circle9 sk-circle\"></div>\n <div class=\"sk-circle10 sk-circle\"></div>\n <div class=\"sk-circle11 sk-circle\"></div>\n <div class=\"sk-circle12 sk-circle\"></div>\n</div>\n", styles: [".sk-fading-circle{width:88px;height:88px;position:relative}.sk-fading-circle img{width:2rem;position:absolute;left:calc(50% - 16px);top:calc(50% - 16px)}.sk-fading-circle .sk-circle{width:100%;height:100%;position:absolute;left:0;top:0}.sk-fading-circle .sk-circle:before{content:\"\";display:block;margin:0 auto;width:15%;height:15%;background-color:var(--color);border-radius:100%;-webkit-animation:sk-circleFadeDelay 1.2s infinite ease-in-out both;animation:sk-circleFadeDelay 1.2s infinite ease-in-out both}.sk-fading-circle .sk-circle2{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);transform:rotate(30deg)}.sk-fading-circle .sk-circle3{-webkit-transform:rotate(60deg);-ms-transform:rotate(60deg);transform:rotate(60deg)}.sk-fading-circle .sk-circle4{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.sk-fading-circle .sk-circle5{-webkit-transform:rotate(120deg);-ms-transform:rotate(120deg);transform:rotate(120deg)}.sk-fading-circle .sk-circle6{-webkit-transform:rotate(150deg);-ms-transform:rotate(150deg);transform:rotate(150deg)}.sk-fading-circle .sk-circle7{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.sk-fading-circle .sk-circle8{-webkit-transform:rotate(210deg);-ms-transform:rotate(210deg);transform:rotate(210deg)}.sk-fading-circle .sk-circle9{-webkit-transform:rotate(240deg);-ms-transform:rotate(240deg);transform:rotate(240deg)}.sk-fading-circle .sk-circle10{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.sk-fading-circle .sk-circle11{-webkit-transform:rotate(300deg);-ms-transform:rotate(300deg);transform:rotate(300deg)}.sk-fading-circle .sk-circle12{-webkit-transform:rotate(330deg);-ms-transform:rotate(330deg);transform:rotate(330deg)}.sk-fading-circle .sk-circle2:before{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.sk-fading-circle .sk-circle3:before{-webkit-animation-delay:-1s;animation-delay:-1s}.sk-fading-circle .sk-circle4:before{-webkit-animation-delay:-.9s;animation-delay:-.9s}.sk-fading-circle .sk-circle5:before{-webkit-animation-delay:-.8s;animation-delay:-.8s}.sk-fading-circle .sk-circle6:before{-webkit-animation-delay:-.7s;animation-delay:-.7s}.sk-fading-circle .sk-circle7:before{-webkit-animation-delay:-.6s;animation-delay:-.6s}.sk-fading-circle .sk-circle8:before{-webkit-animation-delay:-.5s;animation-delay:-.5s}.sk-fading-circle .sk-circle9:before{-webkit-animation-delay:-.4s;animation-delay:-.4s}.sk-fading-circle .sk-circle10:before{-webkit-animation-delay:-.3s;animation-delay:-.3s}.sk-fading-circle .sk-circle11:before{-webkit-animation-delay:-.2s;animation-delay:-.2s}.sk-fading-circle .sk-circle12:before{-webkit-animation-delay:-.1s;animation-delay:-.1s}@-webkit-keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}@keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}\n"] }]
|
|
15775
16774
|
}], ctorParameters: () => [], propDecorators: { useLogo: [{
|
|
15776
16775
|
type: Input
|
|
15777
16776
|
}], color: [{
|
|
@@ -15829,11 +16828,11 @@ class SupportModalComponent {
|
|
|
15829
16828
|
}
|
|
15830
16829
|
}
|
|
15831
16830
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SupportModalComponent, deps: [{ token: SupportService }, { token: i1$2.NgbActiveModal }, { token: i3.UntypedFormBuilder }, { token: BaseUserService }, { token: ValidatorsService }], target: i0.ɵɵFactoryTarget.Component });
|
|
15832
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SupportModalComponent, selector: "app-support-modal", ngImport: i0, template: "<div class=\"modal-header\">\
|
|
16831
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SupportModalComponent, selector: "app-support-modal", ngImport: i0, template: "<div class=\"modal-header\">\n <h5 class=\"modal-title\" translate=\"Help\"></h5>\n</div>\n<div class=\"modal-body\">\n <div class=\"support-loader-container\">\n <lib-loader *ngIf=\"loading\"></lib-loader>\n </div>\n <form (ngSubmit)=\"onSubmit()\" [formGroup]=\"supportForm\" style=\"margin-top:1rem;\" *ngIf=\"!loading && status==='new'\">\n <div>\n <label>{{'Title' | translate}} *</label>\n <div>\n <input type=\"text\" class=\"form-control\" formControlName=\"title\" data-testid=\"new-support-title\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['title'])}\"\n maxlength=\"255\">\n </div>\n <div class=\"invalid-feedback\">\n <span data-testid=\"new-support-title-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['title'])\"\n *ngIf=\" validatorsService.isControlError(supportForm.controls['title'])\"></span>\n </div>\n </div>\n <div *ngIf=\"!currentUser\">\n <label>{{'Email' | translate}} *</label>\n <div>\n <input type=\"email\" class=\"form-control\" formControlName=\"email\" data-testid=\"new-support-email\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['email'])}\"\n maxlength=\"255\">\n </div>\n <div class=\"invalid-feedback\">\n <span data-testid=\"new-support-email-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['email'])\"\n *ngIf=\" validatorsService.isControlError(supportForm.controls['email'])\"></span>\n </div>\n </div>\n <div>\n <label>{{'Description' | translate}} *</label>\n <div>\n <textarea formControlName=\"description\" class=\"form-control\"\n data-testid=\"new-support-description\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['description'])}\"></textarea>\n </div>\n <div class=\"invalid-feedback\" >\n <span data-testid=\"new-support-description-invalid\" [translate]=\" validatorsService.getError(supportForm.controls['description'])\"\n *ngIf=\"validatorsService.isControlError(supportForm.controls['description'])\"></span>\n </div>\n </div>\n </form>\n\n <div *ngIf=\"status ==='send'\" translate=\"Your request has been registered\" data-testid=\"new-support-send-info\">\n </div>\n <div *ngIf=\"status ==='error'\" translate=\"An error has occurred, please renew your request\"\n data-testid=\"new-support-error-info\">\n\n </div>\n</div>\n<div class=\"modal-footer\">\n <button type=\"button\" class=\"btn rounded-pill btn-museum\" data-dismiss=\"modal\" translate=\"Close\" (click)=\"onClose()\"\n data-testid=\"new-support-close-btn\"></button>\n <button *ngIf=\"!loading && status==='new'\" type=\"button\" class=\"btn rounded-pill btn-museum\" translate=\"Submit\"\n (click)=\"onSubmit()\" [disabled]=\"!supportForm.valid\" data-testid=\"new-support-submit-btn\"></button>\n</div>", styles: [".support-loader-container{width:88px;margin:auto}.invalid-feedback{height:16px}\n"], dependencies: [{ kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i11.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "component", type: LoaderComponent, selector: "lib-loader", inputs: ["useLogo", "color"] }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
|
|
15833
16832
|
}
|
|
15834
16833
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SupportModalComponent, decorators: [{
|
|
15835
16834
|
type: Component,
|
|
15836
|
-
args: [{ selector: 'app-support-modal', template: "<div class=\"modal-header\">\
|
|
16835
|
+
args: [{ selector: 'app-support-modal', template: "<div class=\"modal-header\">\n <h5 class=\"modal-title\" translate=\"Help\"></h5>\n</div>\n<div class=\"modal-body\">\n <div class=\"support-loader-container\">\n <lib-loader *ngIf=\"loading\"></lib-loader>\n </div>\n <form (ngSubmit)=\"onSubmit()\" [formGroup]=\"supportForm\" style=\"margin-top:1rem;\" *ngIf=\"!loading && status==='new'\">\n <div>\n <label>{{'Title' | translate}} *</label>\n <div>\n <input type=\"text\" class=\"form-control\" formControlName=\"title\" data-testid=\"new-support-title\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['title'])}\"\n maxlength=\"255\">\n </div>\n <div class=\"invalid-feedback\">\n <span data-testid=\"new-support-title-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['title'])\"\n *ngIf=\" validatorsService.isControlError(supportForm.controls['title'])\"></span>\n </div>\n </div>\n <div *ngIf=\"!currentUser\">\n <label>{{'Email' | translate}} *</label>\n <div>\n <input type=\"email\" class=\"form-control\" formControlName=\"email\" data-testid=\"new-support-email\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['email'])}\"\n maxlength=\"255\">\n </div>\n <div class=\"invalid-feedback\">\n <span data-testid=\"new-support-email-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['email'])\"\n *ngIf=\" validatorsService.isControlError(supportForm.controls['email'])\"></span>\n </div>\n </div>\n <div>\n <label>{{'Description' | translate}} *</label>\n <div>\n <textarea formControlName=\"description\" class=\"form-control\"\n data-testid=\"new-support-description\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['description'])}\"></textarea>\n </div>\n <div class=\"invalid-feedback\" >\n <span data-testid=\"new-support-description-invalid\" [translate]=\" validatorsService.getError(supportForm.controls['description'])\"\n *ngIf=\"validatorsService.isControlError(supportForm.controls['description'])\"></span>\n </div>\n </div>\n </form>\n\n <div *ngIf=\"status ==='send'\" translate=\"Your request has been registered\" data-testid=\"new-support-send-info\">\n </div>\n <div *ngIf=\"status ==='error'\" translate=\"An error has occurred, please renew your request\"\n data-testid=\"new-support-error-info\">\n\n </div>\n</div>\n<div class=\"modal-footer\">\n <button type=\"button\" class=\"btn rounded-pill btn-museum\" data-dismiss=\"modal\" translate=\"Close\" (click)=\"onClose()\"\n data-testid=\"new-support-close-btn\"></button>\n <button *ngIf=\"!loading && status==='new'\" type=\"button\" class=\"btn rounded-pill btn-museum\" translate=\"Submit\"\n (click)=\"onSubmit()\" [disabled]=\"!supportForm.valid\" data-testid=\"new-support-submit-btn\"></button>\n</div>", styles: [".support-loader-container{width:88px;margin:auto}.invalid-feedback{height:16px}\n"] }]
|
|
15837
16836
|
}], ctorParameters: () => [{ type: SupportService }, { type: i1$2.NgbActiveModal }, { type: i3.UntypedFormBuilder }, { type: BaseUserService }, { type: ValidatorsService }] });
|
|
15838
16837
|
|
|
15839
16838
|
class LocaleService {
|
|
@@ -17273,11 +18272,11 @@ class NavigationBarComponent {
|
|
|
17273
18272
|
this.baseVisibilityService.isChangePositionVisible.next(false);
|
|
17274
18273
|
}
|
|
17275
18274
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationBarComponent, deps: [{ token: 'currentAwsKinesisAnalyticsService' }, { token: i1$1.Router }, { token: SpaceService }, { token: BaseUserService }, { token: ZoneService }, { token: VisitService }, { token: ViewerService }, { token: NavigatorService }, { token: ZoneChangeService }, { token: BaseVisibilityService }], target: i0.ɵɵFactoryTarget.Component });
|
|
17276
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: NavigationBarComponent, selector: "lib-navigation-bar", viewQueries: [{ propertyName: "navbarSelectMenu", first: true, predicate: ["navbarSelectMenu"], descendants: true }], ngImport: i0, template: "<nav class=\"navbar navbar-expand-md navbar-dark navbar-inverse second-nav\">\
|
|
18275
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: NavigationBarComponent, selector: "lib-navigation-bar", viewQueries: [{ propertyName: "navbarSelectMenu", first: true, predicate: ["navbarSelectMenu"], descendants: true }], ngImport: i0, template: "<nav class=\"navbar navbar-expand-md navbar-dark navbar-inverse second-nav\">\n <div class=\"d-flex w-100\">\n <div #navbarSelectMenu class=\"w-100\" id=\"nav-item-container-blue\">\n <div class=\"d-flex flex-column flex-md-row p-1 mt-2\">\n <select class=\"form-control flex-fill flex-lg-fill-unset location select-menu\"\n [(ngModel)]=\"selectedLocation\" (ngModelChange)=\"onLocationClick()\">\n <option [ngValue]=\"null\" selected>{{ 'Location' | translate }}</option>\n <option *ngFor=\"let loc of locations; index as index\" [ngValue]=\"loc\"> {{ loc.name }}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" (change)=\"onVisitClick($event)\"\n *ngIf=\"visitsPerSpace && visitsPerSpace.length > 1\">\n <option value=\"default\" selected disabled hidden>{{'Visit' | translate}}</option>\n <option *ngFor=\"let visit of visitsPerSpace; index as index\" value=\"{{index}}\">{{ visit.name}}\n </option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset floor select-menu\" [(ngModel)]=\"selectedFloor\"\n (ngModelChange)=\"onFloorClick()\">\n <option [ngValue]=\"null\" selected>{{'Floor' | translate}}</option>\n <option *ngFor=\"let floor of floorsPerSpace\" [ngValue]=\"floor\">{{ floor.name}}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu zone\" [(ngModel)]=\"selectedZone\"\n (ngModelChange)=\"onZoneClick()\">\n <option [ngValue]=\"null\" selected>{{'Zone' | translate}}</option>\n <option *ngFor=\"let zone of zonesPerFloor\" [ngValue]=\"zone\"><span\n *ngIf=\"zone.code_ref\">{{zone.code_ref}}_</span>{{zone.name}}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" [(ngModel)]=\"selectedChildZone\"\n *ngIf=\"childrenZones && childrenZones.length>0\" (ngModelChange)=\"onChildZoneClick()\">\n <option [ngValue]=\"null\" selected disabled>{{'Zone' | translate}}</option>\n <option *ngFor=\"let zone of childrenZones\" [ngValue]=\"zone\">{{ zone.name}}</option>\n </select>\n <button (click)=\"goTo()\"\n class=\"btn btn-outline-primary rounded-pill mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d d-none d-lg-block\"\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\n </button>\n </div>\n <div class=\"row align-items-center justify-content-center ml-0 p-1\" *ngIf=\"showLocation()\">\n <p class=\"fw-bold lh-base align-middle text-center\">\n {{ showLocation() }}\n </p>\n </div>\n <div class=\"row align-items-center justify-content-center ml-0 p-1 d-block d-lg-none\">\n <button (click)=\"goTo()\"\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d\"\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\n </button>\n </div>\n </div>\n </div>\n</nav>", styles: [":host{position:sticky;top:0;height:123px}.navbar{z-index:200;padding:.4rem 1rem}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0}@media (min-width: 992px){.flex-lg-fill-unset{flex:unset!important}}.form-control{width:100%;min-width:80px;max-width:18%}.location{width:auto;max-width:35%}.floor,.zone{width:auto;max-width:30%}button{color:#fff;padding:5px 10px;font-size:1.1em}.navbar-toggler{margin-right:1em}.select-menu{max-width:inherit}.btn-3d .icon-go-to-3d{width:30px;height:30px;background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0A<style type=\"text/css\">%0A%09.st0{fill:%23565656;}%0A<\\/style>%0A<g>%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0A%09%09/>%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0A</g>%0A</svg>%0A');border:none;vertical-align:middle;fill:var(--smarterplan-primary)}.btn-3d:hover:enabled .icon-go-to-3d{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0A<style type=\"text/css\">%0A%09.st0{fill:%23FFFFFF;}%0A<\\/style>%0A<g>%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0A%09%09/>%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0A</g>%0A</svg>%0A')}.btn-3d[disabled]:hover .icon-go-to-3d{fill:var(--smarterplan-primary)}.outer_circle{background-color:#ebebeb;border-radius:50%;width:32px;height:32px;padding:1px}@media (max-width: 767px){.navbar{padding:.2rem .5rem}.form-control,.location,.floor,.zone{max-width:100%;margin-bottom:15px}input,select{margin-right:0;margin-bottom:15px}}\n"], dependencies: [{ kind: "directive", type: i3.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i11.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
|
|
17277
18276
|
}
|
|
17278
18277
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationBarComponent, decorators: [{
|
|
17279
18278
|
type: Component,
|
|
17280
|
-
args: [{ selector: 'lib-navigation-bar', template: "<nav class=\"navbar navbar-expand-md navbar-dark navbar-inverse second-nav\">\
|
|
18279
|
+
args: [{ selector: 'lib-navigation-bar', template: "<nav class=\"navbar navbar-expand-md navbar-dark navbar-inverse second-nav\">\n <div class=\"d-flex w-100\">\n <div #navbarSelectMenu class=\"w-100\" id=\"nav-item-container-blue\">\n <div class=\"d-flex flex-column flex-md-row p-1 mt-2\">\n <select class=\"form-control flex-fill flex-lg-fill-unset location select-menu\"\n [(ngModel)]=\"selectedLocation\" (ngModelChange)=\"onLocationClick()\">\n <option [ngValue]=\"null\" selected>{{ 'Location' | translate }}</option>\n <option *ngFor=\"let loc of locations; index as index\" [ngValue]=\"loc\"> {{ loc.name }}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" (change)=\"onVisitClick($event)\"\n *ngIf=\"visitsPerSpace && visitsPerSpace.length > 1\">\n <option value=\"default\" selected disabled hidden>{{'Visit' | translate}}</option>\n <option *ngFor=\"let visit of visitsPerSpace; index as index\" value=\"{{index}}\">{{ visit.name}}\n </option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset floor select-menu\" [(ngModel)]=\"selectedFloor\"\n (ngModelChange)=\"onFloorClick()\">\n <option [ngValue]=\"null\" selected>{{'Floor' | translate}}</option>\n <option *ngFor=\"let floor of floorsPerSpace\" [ngValue]=\"floor\">{{ floor.name}}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu zone\" [(ngModel)]=\"selectedZone\"\n (ngModelChange)=\"onZoneClick()\">\n <option [ngValue]=\"null\" selected>{{'Zone' | translate}}</option>\n <option *ngFor=\"let zone of zonesPerFloor\" [ngValue]=\"zone\"><span\n *ngIf=\"zone.code_ref\">{{zone.code_ref}}_</span>{{zone.name}}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" [(ngModel)]=\"selectedChildZone\"\n *ngIf=\"childrenZones && childrenZones.length>0\" (ngModelChange)=\"onChildZoneClick()\">\n <option [ngValue]=\"null\" selected disabled>{{'Zone' | translate}}</option>\n <option *ngFor=\"let zone of childrenZones\" [ngValue]=\"zone\">{{ zone.name}}</option>\n </select>\n <button (click)=\"goTo()\"\n class=\"btn btn-outline-primary rounded-pill mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d d-none d-lg-block\"\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\n </button>\n </div>\n <div class=\"row align-items-center justify-content-center ml-0 p-1\" *ngIf=\"showLocation()\">\n <p class=\"fw-bold lh-base align-middle text-center\">\n {{ showLocation() }}\n </p>\n </div>\n <div class=\"row align-items-center justify-content-center ml-0 p-1 d-block d-lg-none\">\n <button (click)=\"goTo()\"\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d\"\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\n </button>\n </div>\n </div>\n </div>\n</nav>", styles: [":host{position:sticky;top:0;height:123px}.navbar{z-index:200;padding:.4rem 1rem}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0}@media (min-width: 992px){.flex-lg-fill-unset{flex:unset!important}}.form-control{width:100%;min-width:80px;max-width:18%}.location{width:auto;max-width:35%}.floor,.zone{width:auto;max-width:30%}button{color:#fff;padding:5px 10px;font-size:1.1em}.navbar-toggler{margin-right:1em}.select-menu{max-width:inherit}.btn-3d .icon-go-to-3d{width:30px;height:30px;background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0A<style type=\"text/css\">%0A%09.st0{fill:%23565656;}%0A<\\/style>%0A<g>%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0A%09%09/>%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0A</g>%0A</svg>%0A');border:none;vertical-align:middle;fill:var(--smarterplan-primary)}.btn-3d:hover:enabled .icon-go-to-3d{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0A<style type=\"text/css\">%0A%09.st0{fill:%23FFFFFF;}%0A<\\/style>%0A<g>%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0A%09%09/>%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0A</g>%0A</svg>%0A')}.btn-3d[disabled]:hover .icon-go-to-3d{fill:var(--smarterplan-primary)}.outer_circle{background-color:#ebebeb;border-radius:50%;width:32px;height:32px;padding:1px}@media (max-width: 767px){.navbar{padding:.2rem .5rem}.form-control,.location,.floor,.zone{max-width:100%;margin-bottom:15px}input,select{margin-right:0;margin-bottom:15px}}\n"] }]
|
|
17281
18280
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
17282
18281
|
type: Inject,
|
|
17283
18282
|
args: ['currentAwsKinesisAnalyticsService']
|
|
@@ -17546,15 +18545,367 @@ class MenuBarComponent {
|
|
|
17546
18545
|
}
|
|
17547
18546
|
}
|
|
17548
18547
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MenuBarComponent, deps: [{ token: i1$1.Router }, { token: LocaleService }, { token: BaseUserService }, { token: NavigatorService }, { token: SupportService }, { token: BaseVisibilityService }, { token: i1$2.NgbCalendar }, { token: i1$2.NgbDateParserFormatter }, { token: FilterService }, { token: AmplifyCacheService }], target: i0.ɵɵFactoryTarget.Component });
|
|
17549
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: MenuBarComponent, selector: "lib-menu-bar", inputs: { cacheName: "cacheName" }, ngImport: i0, template: "<div id=\"menu-bar\">\r\n <div *ngIf=\"!isVisitor\">\r\n <nav class=\"navbar navbar-expand-md navbar-light p-0\">\r\n <div class=\"container-fluid\" *ngIf=\"isVisible\">\r\n <div class=\"navbar-brand order-md-0\" [routerLink]=\"['/dashboard']\">\r\n <img alt=\"SmarterPlan\" src=\"../assets/images/logos/logo-immo.png\">\r\n </div>\r\n <div class=\"ms-auto nav-item dropdown profile_pic-container order-md-2\">\r\n <div class=\"profile_pic dropdown-toggle\" data-testid=\"dropdown-menu-button\" data-bs-toggle=\"dropdown\">\r\n <lib-avatar></lib-avatar>\r\n </div>\r\n <div class=\"dropdown-menu dropdown-menu-right\">\r\n <p *ngIf=\"user && mission\" class=\"p-2\">\r\n <span class=\"username\">{{ user.displayName }}</span><br />\r\n <span class=\"user-role\">{{ mission.role }}</span><br />\r\n <span class=\"org\">{{ mission.organisation ? mission.organisation.name : \"No organisation\" |\r\n translate}}</span>\r\n </p>\r\n <div class=\"dropdown-divider\"></div>\r\n <!-- <a class=\"dropdown-item\" routerLink=\"/debug\" *ngIf=\"isManager\">Debug</a> -->\r\n <a class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</a>\r\n <a class=\"dropdown-item\" routerLink=\"/dashboard\">{{'Data management' | translate}}</a>\r\n <a class=\"dropdown-item\" data-testid=\"user-management-btn\" routerLink=\"/admin\" *ngIf=\"isManager\">\r\n {{'Users management' | translate}}</a>\r\n <a class=\"dropdown-item\" routerLink=\"/profile\">\r\n {{'Profile' | translate}}</a>\r\n <div class=\"dropdown-divider\"></div>\r\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\r\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n <button class=\"navbar-toggler order-md-1\" (click)=\"navbarCollapse.toggle()\" type=\"button\"\r\n [attr.aria-expanded]=\"!isCollapsed\" aria-controls=\"navbar-collapse\">\r\n <span class=\"navbar-toggler-icon\"></span>\r\n </button>\r\n <div class=\"navbar-collapse order-md-1\" #navbarCollapse=\"ngbCollapse\" [(ngbCollapse)]=\"isCollapsed\"\r\n [animation]=\"false\">\r\n <form\r\n class=\"form-inline m-0 mt-2 form-container d-flex justify-content-start align-items-start align-items-md-center flex-column flex-md-row flex-nowrap filters-menu-block\">\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\r\n <input class=\"form-control search-button m-1\" placeholder=\"keyword\" id=\"mainSearchBar\" type=\"text\">\r\n <button class=\"btn btn-outline-primary rounded-pill m-1 w-fit-content h-fit-content align-self-md-center\"\r\n (click)=\"onSubmit()\">{{'Search' |\r\n translate}}</button>\r\n </div>\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\r\n <div class=\"input-group date-selector m-1\">\r\n <input #dpFromDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpFromDate\"\r\n [value]=\"fromDate | formatDateNumberToDigits\"\r\n (input)=\"fromDate = validateInput(fromDate, dpFromDate.value)\">\r\n <div class=\"input-group-append\">\r\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\r\n type=\"button\"></button>\r\n </div>\r\n </div>\r\n <input name=\"datepicker\" class=\"form-control visually-hidden\" ngbDatepicker #datepicker=\"ngbDatepicker\"\r\n [autoClose]=\"'outside'\" (dateSelect)=\"onDateSelection($event)\" [displayMonths]=\"2\" [dayTemplate]=\"t\"\r\n outsideDays=\"hidden\" [startDate]=\"fromDate || calendar.getToday()\" [container]=\"'body'\" tabindex=\"-1\">\r\n <ng-template #t let-date let-focused=\"focused\">\r\n <span class=\"custom-day\" [class.focused]=\"focused\" [class.range]=\"isRange(date)\"\r\n [class.faded]=\"isHovered(date) || isInside(date)\" (mouseenter)=\"hoveredDate = date\"\r\n (mouseleave)=\"hoveredDate = null\">\r\n {{ date.day }}\r\n </span>\r\n </ng-template>\r\n <span class=\"ms-1 p-2\">{{'to' | translate}}</span>\r\n <div class=\"input-group date-selector m-1\">\r\n <input #dpToDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpToDate\"\r\n [value]=\"toDate | formatDateNumberToDigits\" (input)=\"toDate = validateInput(toDate, dpToDate.value)\">\r\n <div class=\"input-group-append\">\r\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\r\n type=\"button\"></button>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-1 justify-content-evenly\">\r\n <button\r\n class=\"btn btn-primary rounded-pill ms-1 w-25 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n type=\"button\" (click)=\"onApply()\" *ngIf=\"!isDateFilterApplied || dateChanged\">\r\n {{'Apply' | translate}}\r\n </button>\r\n <span\r\n class=\"appliedText ms-1 py-2 align-self-md-center pb-md-0 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n *ngIf=\"isDateFilterApplied && !dateChanged\">\r\n <span class=\"iconify\" data-icon=\"ion:checkmark\" data-width=\"15\" data-height=\"15\"></span>\r\n {{\"Applied\" | translate}}\r\n </span>\r\n <button\r\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n type=\"button\" (click)=\"onClearDates()\">{{'Clear'\r\n |\r\n translate}}</button>\r\n <div\r\n class=\"change-position-button align-self-end pb-2 align-self-md-center mx-2 pb-md-0 flex-nowrap w-25 w-md-fit-content h-md-fit-content\">\r\n <input type=\"checkbox\" class=\"btn-check\" id=\"btn-check-outlined\" autocomplete=\"off\">\r\n <label class=\"link-primary cursor-pointer text-decoration-underline\" (click)=\"onChangePositionClick()\"\r\n for=\"btn-check-outlined\">{{\r\n (isChangePositionVisible ? \"Close\" : \"Move to\") | translate }}</label>\r\n </div>\r\n </div>\r\n </form>\r\n </div>\r\n </div>\r\n </nav>\r\n <lib-navigation-bar *ngIf=\"isVisible && isChangePositionVisible\"></lib-navigation-bar>\r\n </div>\r\n <div *ngIf=\"isVisitor && isVisible\">\r\n <div class=\"visitor-div\">\r\n <div class=\"logo-container\" data-testid=\"dropdown-menu-button\">\r\n <img class=\"img-logo\" alt=\"SmarterPlanLogo\" src=\"../assets/images/logos/logo-reserve.png\">\r\n </div>\r\n <div class=\"avatar-container-menu\">\r\n <div class=\"profile_pic dropdown-toggle cursor-pointer\" data-bs-toggle=\"dropdown\">\r\n <lib-avatar></lib-avatar>\r\n </div>\r\n <div class=\"dropdown-menu dropdown-menu-right\">\r\n <button class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</button>\r\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\r\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: ["#menu-bar{position:sticky;top:0;z-index:200}.navbar{z-index:200}.navbar-brand{padding-top:5px;padding-bottom:5px;cursor:pointer}.navbar-brand img{height:70px}.profile_pic{border-radius:50%;height:45px;width:45px;margin:8px;border:.2px solid rgb(150,150,150)}.navbar-nav .profile-pic-container .dropdown-menu{position:absolute}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}.profile_pic.dropdown-toggle:after{display:none}.navbar-light{border-bottom:1px solid var(--smarterplan-primary);background-color:#fff}.navbar form{width:100%;margin:0;padding:0}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0;width:auto}button{padding:5px 10px}button:disabled,button[disabled]{color:#c04f4f}.navbar-toggler{margin-right:1em}.icon{height:40px;width:40px;color:var(--smarterplan-primary)}.search-button-container{width:auto;align-items:stretch;justify-items:start}.search-button-container .search-button{width:auto;border-radius:1rem!important}.visitor-div{background-color:var(--smarterplan-primary);height:60px;display:flex;justify-content:space-between}.profile_pic-container{margin-right:1em}.logo-container,.img-logo{height:100%}.change-position-button{margin-left:2rem}.change-position-button label,.change-position-button .btn-check:checked,.change-position-button .btn-check:focus,.change-position-button .btn-outline-primary,.change-position-button .btn{box-shadow:none!important;outline:0}.change-position-button label{border-radius:16px}.change-position-button .btn-check:checked+.btn{background-color:var(--smarterplan-primary);color:#fff!important}.filters-menu-block{width:auto}@media (min-width: 1200px){.navbar-brand img{height:100px}.profile_pic{height:60px;width:60px;margin:15px}}@media (min-width: 992px) and (max-width: 1199.98px){.navbar-brand img{height:80px}.profile_pic{height:50px;width:50px;margin:12px}}@media (min-width: 768px) and (max-width: 991.98px){.navbar-brand img{height:60px}.profile_pic{height:40px;width:40px;margin:10px}}@media (max-width: 767.98px){.navbar-brand img{height:50px}.profile_pic{height:30px;width:30px;margin:8px}.search-button-container,.filters-menu-block{width:100%}}.form-container{margin:.5rem 0}.navbar-collapse.show{display:flex;padding-bottom:.5rem}.hidden{width:0;margin:0;border:none;padding:0}.custom-day{text-align:center;padding:.185rem .25rem;display:inline-block;height:2rem;width:2rem}.custom-day.focused{background-color:#e6e6e6}.custom-day.range,.custom-day:hover{background-color:var(--smarterplan-primary-lighter);color:#fff}.custom-day.faded{background-color:var(--smarterplan-primary-transparent)}.calendar{height:100%}.appliedText,.appliedText span{color:var(--smarterplan-primary)}@media (max-width: 767px){.form-inline{flex-direction:column}}.hidden{opacity:0}.date-selector{flex-wrap:nowrap}.date-selector input{min-width:80px;margin:0}@media (min-width: 768px){.w-md-fit-content{width:fit-content!important}.h-md-content{height:fit-content!important}}\n"], dependencies: [{ kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1$2.NgbCollapse, selector: "[ngbCollapse]", inputs: ["animation", "ngbCollapse", "horizontal"], outputs: ["ngbCollapseChange", "shown", "hidden"], exportAs: ["ngbCollapse"] }, { kind: "directive", type: i1$2.NgbInputDatepicker, selector: "input[ngbDatepicker]", inputs: ["autoClose", "contentTemplate", "datepickerClass", "dayTemplate", "dayTemplateData", "displayMonths", "firstDayOfWeek", "footerTemplate", "markDisabled", "minDate", "maxDate", "navigation", "outsideDays", "placement", "popperOptions", "restoreFocus", "showWeekNumbers", "startDate", "container", "positionTarget", "weekdays", "disabled"], outputs: ["dateSelect", "navigate", "closed"], exportAs: ["ngbDatepicker"] }, { kind: "component", type: AvatarComponent, selector: "lib-avatar", inputs: ["size", "userID"] }, { kind: "component", type: NavigationBarComponent, selector: "lib-navigation-bar" }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }, { kind: "pipe", type: FormatDateNumberToDigitsPipe, name: "formatDateNumberToDigits" }] });
|
|
18548
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: MenuBarComponent, selector: "lib-menu-bar", inputs: { cacheName: "cacheName" }, ngImport: i0, template: "<div id=\"menu-bar\">\n <div *ngIf=\"!isVisitor\">\n <nav class=\"navbar navbar-expand-md navbar-light p-0\">\n <div class=\"container-fluid\" *ngIf=\"isVisible\">\n <div class=\"navbar-brand order-md-0\" [routerLink]=\"['/dashboard']\">\n <img alt=\"SmarterPlan\" src=\"../assets/images/logos/logo-immo.png\">\n </div>\n <div class=\"ms-auto nav-item dropdown profile_pic-container order-md-2\">\n <div class=\"profile_pic dropdown-toggle\" data-testid=\"dropdown-menu-button\" data-bs-toggle=\"dropdown\">\n <lib-avatar></lib-avatar>\n </div>\n <div class=\"dropdown-menu dropdown-menu-right\">\n <p *ngIf=\"user && mission\" class=\"p-2\">\n <span class=\"username\">{{ user.displayName }}</span><br />\n <span class=\"user-role\">{{ mission.role }}</span><br />\n <span class=\"org\">{{ mission.organisation ? mission.organisation.name : \"No organisation\" |\n translate}}</span>\n </p>\n <div class=\"dropdown-divider\"></div>\n <!-- <a class=\"dropdown-item\" routerLink=\"/debug\" *ngIf=\"isManager\">Debug</a> -->\n <a class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</a>\n <a class=\"dropdown-item\" routerLink=\"/dashboard\">{{'Data management' | translate}}</a>\n <a class=\"dropdown-item\" data-testid=\"user-management-btn\" routerLink=\"/admin\" *ngIf=\"isManager\">\n {{'Users management' | translate}}</a>\n <a class=\"dropdown-item\" routerLink=\"/profile\">\n {{'Profile' | translate}}</a>\n <div class=\"dropdown-divider\"></div>\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\n translate}}</button>\n </div>\n </div>\n <button class=\"navbar-toggler order-md-1\" (click)=\"navbarCollapse.toggle()\" type=\"button\"\n [attr.aria-expanded]=\"!isCollapsed\" aria-controls=\"navbar-collapse\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n <div class=\"navbar-collapse order-md-1\" #navbarCollapse=\"ngbCollapse\" [(ngbCollapse)]=\"isCollapsed\"\n [animation]=\"false\">\n <form\n class=\"form-inline m-0 mt-2 form-container d-flex justify-content-start align-items-start align-items-md-center flex-column flex-md-row flex-nowrap filters-menu-block\">\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\n <input class=\"form-control search-button m-1\" placeholder=\"keyword\" id=\"mainSearchBar\" type=\"text\">\n <button class=\"btn btn-outline-primary rounded-pill m-1 w-fit-content h-fit-content align-self-md-center\"\n (click)=\"onSubmit()\">{{'Search' |\n translate}}</button>\n </div>\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\n <div class=\"input-group date-selector m-1\">\n <input #dpFromDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpFromDate\"\n [value]=\"fromDate | formatDateNumberToDigits\"\n (input)=\"fromDate = validateInput(fromDate, dpFromDate.value)\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\n type=\"button\"></button>\n </div>\n </div>\n <input name=\"datepicker\" class=\"form-control visually-hidden\" ngbDatepicker #datepicker=\"ngbDatepicker\"\n [autoClose]=\"'outside'\" (dateSelect)=\"onDateSelection($event)\" [displayMonths]=\"2\" [dayTemplate]=\"t\"\n outsideDays=\"hidden\" [startDate]=\"fromDate || calendar.getToday()\" [container]=\"'body'\" tabindex=\"-1\">\n <ng-template #t let-date let-focused=\"focused\">\n <span class=\"custom-day\" [class.focused]=\"focused\" [class.range]=\"isRange(date)\"\n [class.faded]=\"isHovered(date) || isInside(date)\" (mouseenter)=\"hoveredDate = date\"\n (mouseleave)=\"hoveredDate = null\">\n {{ date.day }}\n </span>\n </ng-template>\n <span class=\"ms-1 p-2\">{{'to' | translate}}</span>\n <div class=\"input-group date-selector m-1\">\n <input #dpToDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpToDate\"\n [value]=\"toDate | formatDateNumberToDigits\" (input)=\"toDate = validateInput(toDate, dpToDate.value)\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\n type=\"button\"></button>\n </div>\n </div>\n </div>\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-1 justify-content-evenly\">\n <button\n class=\"btn btn-primary rounded-pill ms-1 w-25 w-md-fit-content h-md-fit-content align-self-md-center\"\n type=\"button\" (click)=\"onApply()\" *ngIf=\"!isDateFilterApplied || dateChanged\">\n {{'Apply' | translate}}\n </button>\n <span\n class=\"appliedText ms-1 py-2 align-self-md-center pb-md-0 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\n *ngIf=\"isDateFilterApplied && !dateChanged\">\n <span class=\"iconify\" data-icon=\"ion:checkmark\" data-width=\"15\" data-height=\"15\"></span>\n {{\"Applied\" | translate}}\n </span>\n <button\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\n type=\"button\" (click)=\"onClearDates()\">{{'Clear'\n |\n translate}}</button>\n <div\n class=\"change-position-button align-self-end pb-2 align-self-md-center mx-2 pb-md-0 flex-nowrap w-25 w-md-fit-content h-md-fit-content\">\n <input type=\"checkbox\" class=\"btn-check\" id=\"btn-check-outlined\" autocomplete=\"off\">\n <label class=\"link-primary cursor-pointer text-decoration-underline\" (click)=\"onChangePositionClick()\"\n for=\"btn-check-outlined\">{{\n (isChangePositionVisible ? \"Close\" : \"Move to\") | translate }}</label>\n </div>\n </div>\n </form>\n </div>\n </div>\n </nav>\n <lib-navigation-bar *ngIf=\"isVisible && isChangePositionVisible\"></lib-navigation-bar>\n </div>\n <div *ngIf=\"isVisitor && isVisible\">\n <div class=\"visitor-div\">\n <div class=\"logo-container\" data-testid=\"dropdown-menu-button\">\n <img class=\"img-logo\" alt=\"SmarterPlanLogo\" src=\"../assets/images/logos/logo-reserve.png\">\n </div>\n <div class=\"avatar-container-menu\">\n <div class=\"profile_pic dropdown-toggle cursor-pointer\" data-bs-toggle=\"dropdown\">\n <lib-avatar></lib-avatar>\n </div>\n <div class=\"dropdown-menu dropdown-menu-right\">\n <button class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</button>\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\n translate}}</button>\n </div>\n </div>\n </div>\n </div>\n</div>", styles: ["#menu-bar{position:sticky;top:0;z-index:200}.navbar{z-index:200}.navbar-brand{padding-top:5px;padding-bottom:5px;cursor:pointer}.navbar-brand img{height:70px}.profile_pic{border-radius:50%;height:45px;width:45px;margin:8px;border:.2px solid rgb(150,150,150)}.navbar-nav .profile-pic-container .dropdown-menu{position:absolute}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}.profile_pic.dropdown-toggle:after{display:none}.navbar-light{border-bottom:1px solid var(--smarterplan-primary);background-color:#fff}.navbar form{width:100%;margin:0;padding:0}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0;width:auto}button{padding:5px 10px}button:disabled,button[disabled]{color:#c04f4f}.navbar-toggler{margin-right:1em}.icon{height:40px;width:40px;color:var(--smarterplan-primary)}.search-button-container{width:auto;align-items:stretch;justify-items:start}.search-button-container .search-button{width:auto;border-radius:1rem!important}.visitor-div{background-color:var(--smarterplan-primary);height:60px;display:flex;justify-content:space-between}.profile_pic-container{margin-right:1em}.logo-container,.img-logo{height:100%}.change-position-button{margin-left:2rem}.change-position-button label,.change-position-button .btn-check:checked,.change-position-button .btn-check:focus,.change-position-button .btn-outline-primary,.change-position-button .btn{box-shadow:none!important;outline:0}.change-position-button label{border-radius:16px}.change-position-button .btn-check:checked+.btn{background-color:var(--smarterplan-primary);color:#fff!important}.filters-menu-block{width:auto}@media (min-width: 1200px){.navbar-brand img{height:100px}.profile_pic{height:60px;width:60px;margin:15px}}@media (min-width: 992px) and (max-width: 1199.98px){.navbar-brand img{height:80px}.profile_pic{height:50px;width:50px;margin:12px}}@media (min-width: 768px) and (max-width: 991.98px){.navbar-brand img{height:60px}.profile_pic{height:40px;width:40px;margin:10px}}@media (max-width: 767.98px){.navbar-brand img{height:50px}.profile_pic{height:30px;width:30px;margin:8px}.search-button-container,.filters-menu-block{width:100%}}.form-container{margin:.5rem 0}.navbar-collapse.show{display:flex;padding-bottom:.5rem}.hidden{width:0;margin:0;border:none;padding:0}.custom-day{text-align:center;padding:.185rem .25rem;display:inline-block;height:2rem;width:2rem}.custom-day.focused{background-color:#e6e6e6}.custom-day.range,.custom-day:hover{background-color:var(--smarterplan-primary-lighter);color:#fff}.custom-day.faded{background-color:var(--smarterplan-primary-transparent)}.calendar{height:100%}.appliedText,.appliedText span{color:var(--smarterplan-primary)}@media (max-width: 767px){.form-inline{flex-direction:column}}.hidden{opacity:0}.date-selector{flex-wrap:nowrap}.date-selector input{min-width:80px;margin:0}@media (min-width: 768px){.w-md-fit-content{width:fit-content!important}.h-md-content{height:fit-content!important}}\n"], dependencies: [{ kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1$2.NgbCollapse, selector: "[ngbCollapse]", inputs: ["animation", "ngbCollapse", "horizontal"], outputs: ["ngbCollapseChange", "shown", "hidden"], exportAs: ["ngbCollapse"] }, { kind: "directive", type: i1$2.NgbInputDatepicker, selector: "input[ngbDatepicker]", inputs: ["autoClose", "contentTemplate", "datepickerClass", "dayTemplate", "dayTemplateData", "displayMonths", "firstDayOfWeek", "footerTemplate", "markDisabled", "minDate", "maxDate", "navigation", "outsideDays", "placement", "popperOptions", "restoreFocus", "showWeekNumbers", "startDate", "container", "positionTarget", "weekdays", "disabled"], outputs: ["dateSelect", "navigate", "closed"], exportAs: ["ngbDatepicker"] }, { kind: "component", type: AvatarComponent, selector: "lib-avatar", inputs: ["size", "userID"] }, { kind: "component", type: NavigationBarComponent, selector: "lib-navigation-bar" }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }, { kind: "pipe", type: FormatDateNumberToDigitsPipe, name: "formatDateNumberToDigits" }] });
|
|
17550
18549
|
}
|
|
17551
18550
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MenuBarComponent, decorators: [{
|
|
17552
18551
|
type: Component,
|
|
17553
|
-
args: [{ selector: 'lib-menu-bar', template: "<div id=\"menu-bar\">\r\n <div *ngIf=\"!isVisitor\">\r\n <nav class=\"navbar navbar-expand-md navbar-light p-0\">\r\n <div class=\"container-fluid\" *ngIf=\"isVisible\">\r\n <div class=\"navbar-brand order-md-0\" [routerLink]=\"['/dashboard']\">\r\n <img alt=\"SmarterPlan\" src=\"../assets/images/logos/logo-immo.png\">\r\n </div>\r\n <div class=\"ms-auto nav-item dropdown profile_pic-container order-md-2\">\r\n <div class=\"profile_pic dropdown-toggle\" data-testid=\"dropdown-menu-button\" data-bs-toggle=\"dropdown\">\r\n <lib-avatar></lib-avatar>\r\n </div>\r\n <div class=\"dropdown-menu dropdown-menu-right\">\r\n <p *ngIf=\"user && mission\" class=\"p-2\">\r\n <span class=\"username\">{{ user.displayName }}</span><br />\r\n <span class=\"user-role\">{{ mission.role }}</span><br />\r\n <span class=\"org\">{{ mission.organisation ? mission.organisation.name : \"No organisation\" |\r\n translate}}</span>\r\n </p>\r\n <div class=\"dropdown-divider\"></div>\r\n <!-- <a class=\"dropdown-item\" routerLink=\"/debug\" *ngIf=\"isManager\">Debug</a> -->\r\n <a class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</a>\r\n <a class=\"dropdown-item\" routerLink=\"/dashboard\">{{'Data management' | translate}}</a>\r\n <a class=\"dropdown-item\" data-testid=\"user-management-btn\" routerLink=\"/admin\" *ngIf=\"isManager\">\r\n {{'Users management' | translate}}</a>\r\n <a class=\"dropdown-item\" routerLink=\"/profile\">\r\n {{'Profile' | translate}}</a>\r\n <div class=\"dropdown-divider\"></div>\r\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\r\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n <button class=\"navbar-toggler order-md-1\" (click)=\"navbarCollapse.toggle()\" type=\"button\"\r\n [attr.aria-expanded]=\"!isCollapsed\" aria-controls=\"navbar-collapse\">\r\n <span class=\"navbar-toggler-icon\"></span>\r\n </button>\r\n <div class=\"navbar-collapse order-md-1\" #navbarCollapse=\"ngbCollapse\" [(ngbCollapse)]=\"isCollapsed\"\r\n [animation]=\"false\">\r\n <form\r\n class=\"form-inline m-0 mt-2 form-container d-flex justify-content-start align-items-start align-items-md-center flex-column flex-md-row flex-nowrap filters-menu-block\">\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\r\n <input class=\"form-control search-button m-1\" placeholder=\"keyword\" id=\"mainSearchBar\" type=\"text\">\r\n <button class=\"btn btn-outline-primary rounded-pill m-1 w-fit-content h-fit-content align-self-md-center\"\r\n (click)=\"onSubmit()\">{{'Search' |\r\n translate}}</button>\r\n </div>\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\r\n <div class=\"input-group date-selector m-1\">\r\n <input #dpFromDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpFromDate\"\r\n [value]=\"fromDate | formatDateNumberToDigits\"\r\n (input)=\"fromDate = validateInput(fromDate, dpFromDate.value)\">\r\n <div class=\"input-group-append\">\r\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\r\n type=\"button\"></button>\r\n </div>\r\n </div>\r\n <input name=\"datepicker\" class=\"form-control visually-hidden\" ngbDatepicker #datepicker=\"ngbDatepicker\"\r\n [autoClose]=\"'outside'\" (dateSelect)=\"onDateSelection($event)\" [displayMonths]=\"2\" [dayTemplate]=\"t\"\r\n outsideDays=\"hidden\" [startDate]=\"fromDate || calendar.getToday()\" [container]=\"'body'\" tabindex=\"-1\">\r\n <ng-template #t let-date let-focused=\"focused\">\r\n <span class=\"custom-day\" [class.focused]=\"focused\" [class.range]=\"isRange(date)\"\r\n [class.faded]=\"isHovered(date) || isInside(date)\" (mouseenter)=\"hoveredDate = date\"\r\n (mouseleave)=\"hoveredDate = null\">\r\n {{ date.day }}\r\n </span>\r\n </ng-template>\r\n <span class=\"ms-1 p-2\">{{'to' | translate}}</span>\r\n <div class=\"input-group date-selector m-1\">\r\n <input #dpToDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpToDate\"\r\n [value]=\"toDate | formatDateNumberToDigits\" (input)=\"toDate = validateInput(toDate, dpToDate.value)\">\r\n <div class=\"input-group-append\">\r\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\r\n type=\"button\"></button>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-1 justify-content-evenly\">\r\n <button\r\n class=\"btn btn-primary rounded-pill ms-1 w-25 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n type=\"button\" (click)=\"onApply()\" *ngIf=\"!isDateFilterApplied || dateChanged\">\r\n {{'Apply' | translate}}\r\n </button>\r\n <span\r\n class=\"appliedText ms-1 py-2 align-self-md-center pb-md-0 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n *ngIf=\"isDateFilterApplied && !dateChanged\">\r\n <span class=\"iconify\" data-icon=\"ion:checkmark\" data-width=\"15\" data-height=\"15\"></span>\r\n {{\"Applied\" | translate}}\r\n </span>\r\n <button\r\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n type=\"button\" (click)=\"onClearDates()\">{{'Clear'\r\n |\r\n translate}}</button>\r\n <div\r\n class=\"change-position-button align-self-end pb-2 align-self-md-center mx-2 pb-md-0 flex-nowrap w-25 w-md-fit-content h-md-fit-content\">\r\n <input type=\"checkbox\" class=\"btn-check\" id=\"btn-check-outlined\" autocomplete=\"off\">\r\n <label class=\"link-primary cursor-pointer text-decoration-underline\" (click)=\"onChangePositionClick()\"\r\n for=\"btn-check-outlined\">{{\r\n (isChangePositionVisible ? \"Close\" : \"Move to\") | translate }}</label>\r\n </div>\r\n </div>\r\n </form>\r\n </div>\r\n </div>\r\n </nav>\r\n <lib-navigation-bar *ngIf=\"isVisible && isChangePositionVisible\"></lib-navigation-bar>\r\n </div>\r\n <div *ngIf=\"isVisitor && isVisible\">\r\n <div class=\"visitor-div\">\r\n <div class=\"logo-container\" data-testid=\"dropdown-menu-button\">\r\n <img class=\"img-logo\" alt=\"SmarterPlanLogo\" src=\"../assets/images/logos/logo-reserve.png\">\r\n </div>\r\n <div class=\"avatar-container-menu\">\r\n <div class=\"profile_pic dropdown-toggle cursor-pointer\" data-bs-toggle=\"dropdown\">\r\n <lib-avatar></lib-avatar>\r\n </div>\r\n <div class=\"dropdown-menu dropdown-menu-right\">\r\n <button class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</button>\r\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\r\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: ["#menu-bar{position:sticky;top:0;z-index:200}.navbar{z-index:200}.navbar-brand{padding-top:5px;padding-bottom:5px;cursor:pointer}.navbar-brand img{height:70px}.profile_pic{border-radius:50%;height:45px;width:45px;margin:8px;border:.2px solid rgb(150,150,150)}.navbar-nav .profile-pic-container .dropdown-menu{position:absolute}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}.profile_pic.dropdown-toggle:after{display:none}.navbar-light{border-bottom:1px solid var(--smarterplan-primary);background-color:#fff}.navbar form{width:100%;margin:0;padding:0}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0;width:auto}button{padding:5px 10px}button:disabled,button[disabled]{color:#c04f4f}.navbar-toggler{margin-right:1em}.icon{height:40px;width:40px;color:var(--smarterplan-primary)}.search-button-container{width:auto;align-items:stretch;justify-items:start}.search-button-container .search-button{width:auto;border-radius:1rem!important}.visitor-div{background-color:var(--smarterplan-primary);height:60px;display:flex;justify-content:space-between}.profile_pic-container{margin-right:1em}.logo-container,.img-logo{height:100%}.change-position-button{margin-left:2rem}.change-position-button label,.change-position-button .btn-check:checked,.change-position-button .btn-check:focus,.change-position-button .btn-outline-primary,.change-position-button .btn{box-shadow:none!important;outline:0}.change-position-button label{border-radius:16px}.change-position-button .btn-check:checked+.btn{background-color:var(--smarterplan-primary);color:#fff!important}.filters-menu-block{width:auto}@media (min-width: 1200px){.navbar-brand img{height:100px}.profile_pic{height:60px;width:60px;margin:15px}}@media (min-width: 992px) and (max-width: 1199.98px){.navbar-brand img{height:80px}.profile_pic{height:50px;width:50px;margin:12px}}@media (min-width: 768px) and (max-width: 991.98px){.navbar-brand img{height:60px}.profile_pic{height:40px;width:40px;margin:10px}}@media (max-width: 767.98px){.navbar-brand img{height:50px}.profile_pic{height:30px;width:30px;margin:8px}.search-button-container,.filters-menu-block{width:100%}}.form-container{margin:.5rem 0}.navbar-collapse.show{display:flex;padding-bottom:.5rem}.hidden{width:0;margin:0;border:none;padding:0}.custom-day{text-align:center;padding:.185rem .25rem;display:inline-block;height:2rem;width:2rem}.custom-day.focused{background-color:#e6e6e6}.custom-day.range,.custom-day:hover{background-color:var(--smarterplan-primary-lighter);color:#fff}.custom-day.faded{background-color:var(--smarterplan-primary-transparent)}.calendar{height:100%}.appliedText,.appliedText span{color:var(--smarterplan-primary)}@media (max-width: 767px){.form-inline{flex-direction:column}}.hidden{opacity:0}.date-selector{flex-wrap:nowrap}.date-selector input{min-width:80px;margin:0}@media (min-width: 768px){.w-md-fit-content{width:fit-content!important}.h-md-content{height:fit-content!important}}\n"] }]
|
|
18552
|
+
args: [{ selector: 'lib-menu-bar', template: "<div id=\"menu-bar\">\n <div *ngIf=\"!isVisitor\">\n <nav class=\"navbar navbar-expand-md navbar-light p-0\">\n <div class=\"container-fluid\" *ngIf=\"isVisible\">\n <div class=\"navbar-brand order-md-0\" [routerLink]=\"['/dashboard']\">\n <img alt=\"SmarterPlan\" src=\"../assets/images/logos/logo-immo.png\">\n </div>\n <div class=\"ms-auto nav-item dropdown profile_pic-container order-md-2\">\n <div class=\"profile_pic dropdown-toggle\" data-testid=\"dropdown-menu-button\" data-bs-toggle=\"dropdown\">\n <lib-avatar></lib-avatar>\n </div>\n <div class=\"dropdown-menu dropdown-menu-right\">\n <p *ngIf=\"user && mission\" class=\"p-2\">\n <span class=\"username\">{{ user.displayName }}</span><br />\n <span class=\"user-role\">{{ mission.role }}</span><br />\n <span class=\"org\">{{ mission.organisation ? mission.organisation.name : \"No organisation\" |\n translate}}</span>\n </p>\n <div class=\"dropdown-divider\"></div>\n <!-- <a class=\"dropdown-item\" routerLink=\"/debug\" *ngIf=\"isManager\">Debug</a> -->\n <a class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</a>\n <a class=\"dropdown-item\" routerLink=\"/dashboard\">{{'Data management' | translate}}</a>\n <a class=\"dropdown-item\" data-testid=\"user-management-btn\" routerLink=\"/admin\" *ngIf=\"isManager\">\n {{'Users management' | translate}}</a>\n <a class=\"dropdown-item\" routerLink=\"/profile\">\n {{'Profile' | translate}}</a>\n <div class=\"dropdown-divider\"></div>\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\n translate}}</button>\n </div>\n </div>\n <button class=\"navbar-toggler order-md-1\" (click)=\"navbarCollapse.toggle()\" type=\"button\"\n [attr.aria-expanded]=\"!isCollapsed\" aria-controls=\"navbar-collapse\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n <div class=\"navbar-collapse order-md-1\" #navbarCollapse=\"ngbCollapse\" [(ngbCollapse)]=\"isCollapsed\"\n [animation]=\"false\">\n <form\n class=\"form-inline m-0 mt-2 form-container d-flex justify-content-start align-items-start align-items-md-center flex-column flex-md-row flex-nowrap filters-menu-block\">\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\n <input class=\"form-control search-button m-1\" placeholder=\"keyword\" id=\"mainSearchBar\" type=\"text\">\n <button class=\"btn btn-outline-primary rounded-pill m-1 w-fit-content h-fit-content align-self-md-center\"\n (click)=\"onSubmit()\">{{'Search' |\n translate}}</button>\n </div>\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\n <div class=\"input-group date-selector m-1\">\n <input #dpFromDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpFromDate\"\n [value]=\"fromDate | formatDateNumberToDigits\"\n (input)=\"fromDate = validateInput(fromDate, dpFromDate.value)\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\n type=\"button\"></button>\n </div>\n </div>\n <input name=\"datepicker\" class=\"form-control visually-hidden\" ngbDatepicker #datepicker=\"ngbDatepicker\"\n [autoClose]=\"'outside'\" (dateSelect)=\"onDateSelection($event)\" [displayMonths]=\"2\" [dayTemplate]=\"t\"\n outsideDays=\"hidden\" [startDate]=\"fromDate || calendar.getToday()\" [container]=\"'body'\" tabindex=\"-1\">\n <ng-template #t let-date let-focused=\"focused\">\n <span class=\"custom-day\" [class.focused]=\"focused\" [class.range]=\"isRange(date)\"\n [class.faded]=\"isHovered(date) || isInside(date)\" (mouseenter)=\"hoveredDate = date\"\n (mouseleave)=\"hoveredDate = null\">\n {{ date.day }}\n </span>\n </ng-template>\n <span class=\"ms-1 p-2\">{{'to' | translate}}</span>\n <div class=\"input-group date-selector m-1\">\n <input #dpToDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpToDate\"\n [value]=\"toDate | formatDateNumberToDigits\" (input)=\"toDate = validateInput(toDate, dpToDate.value)\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\n type=\"button\"></button>\n </div>\n </div>\n </div>\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-1 justify-content-evenly\">\n <button\n class=\"btn btn-primary rounded-pill ms-1 w-25 w-md-fit-content h-md-fit-content align-self-md-center\"\n type=\"button\" (click)=\"onApply()\" *ngIf=\"!isDateFilterApplied || dateChanged\">\n {{'Apply' | translate}}\n </button>\n <span\n class=\"appliedText ms-1 py-2 align-self-md-center pb-md-0 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\n *ngIf=\"isDateFilterApplied && !dateChanged\">\n <span class=\"iconify\" data-icon=\"ion:checkmark\" data-width=\"15\" data-height=\"15\"></span>\n {{\"Applied\" | translate}}\n </span>\n <button\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\n type=\"button\" (click)=\"onClearDates()\">{{'Clear'\n |\n translate}}</button>\n <div\n class=\"change-position-button align-self-end pb-2 align-self-md-center mx-2 pb-md-0 flex-nowrap w-25 w-md-fit-content h-md-fit-content\">\n <input type=\"checkbox\" class=\"btn-check\" id=\"btn-check-outlined\" autocomplete=\"off\">\n <label class=\"link-primary cursor-pointer text-decoration-underline\" (click)=\"onChangePositionClick()\"\n for=\"btn-check-outlined\">{{\n (isChangePositionVisible ? \"Close\" : \"Move to\") | translate }}</label>\n </div>\n </div>\n </form>\n </div>\n </div>\n </nav>\n <lib-navigation-bar *ngIf=\"isVisible && isChangePositionVisible\"></lib-navigation-bar>\n </div>\n <div *ngIf=\"isVisitor && isVisible\">\n <div class=\"visitor-div\">\n <div class=\"logo-container\" data-testid=\"dropdown-menu-button\">\n <img class=\"img-logo\" alt=\"SmarterPlanLogo\" src=\"../assets/images/logos/logo-reserve.png\">\n </div>\n <div class=\"avatar-container-menu\">\n <div class=\"profile_pic dropdown-toggle cursor-pointer\" data-bs-toggle=\"dropdown\">\n <lib-avatar></lib-avatar>\n </div>\n <div class=\"dropdown-menu dropdown-menu-right\">\n <button class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</button>\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\n translate}}</button>\n </div>\n </div>\n </div>\n </div>\n</div>", styles: ["#menu-bar{position:sticky;top:0;z-index:200}.navbar{z-index:200}.navbar-brand{padding-top:5px;padding-bottom:5px;cursor:pointer}.navbar-brand img{height:70px}.profile_pic{border-radius:50%;height:45px;width:45px;margin:8px;border:.2px solid rgb(150,150,150)}.navbar-nav .profile-pic-container .dropdown-menu{position:absolute}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}.profile_pic.dropdown-toggle:after{display:none}.navbar-light{border-bottom:1px solid var(--smarterplan-primary);background-color:#fff}.navbar form{width:100%;margin:0;padding:0}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0;width:auto}button{padding:5px 10px}button:disabled,button[disabled]{color:#c04f4f}.navbar-toggler{margin-right:1em}.icon{height:40px;width:40px;color:var(--smarterplan-primary)}.search-button-container{width:auto;align-items:stretch;justify-items:start}.search-button-container .search-button{width:auto;border-radius:1rem!important}.visitor-div{background-color:var(--smarterplan-primary);height:60px;display:flex;justify-content:space-between}.profile_pic-container{margin-right:1em}.logo-container,.img-logo{height:100%}.change-position-button{margin-left:2rem}.change-position-button label,.change-position-button .btn-check:checked,.change-position-button .btn-check:focus,.change-position-button .btn-outline-primary,.change-position-button .btn{box-shadow:none!important;outline:0}.change-position-button label{border-radius:16px}.change-position-button .btn-check:checked+.btn{background-color:var(--smarterplan-primary);color:#fff!important}.filters-menu-block{width:auto}@media (min-width: 1200px){.navbar-brand img{height:100px}.profile_pic{height:60px;width:60px;margin:15px}}@media (min-width: 992px) and (max-width: 1199.98px){.navbar-brand img{height:80px}.profile_pic{height:50px;width:50px;margin:12px}}@media (min-width: 768px) and (max-width: 991.98px){.navbar-brand img{height:60px}.profile_pic{height:40px;width:40px;margin:10px}}@media (max-width: 767.98px){.navbar-brand img{height:50px}.profile_pic{height:30px;width:30px;margin:8px}.search-button-container,.filters-menu-block{width:100%}}.form-container{margin:.5rem 0}.navbar-collapse.show{display:flex;padding-bottom:.5rem}.hidden{width:0;margin:0;border:none;padding:0}.custom-day{text-align:center;padding:.185rem .25rem;display:inline-block;height:2rem;width:2rem}.custom-day.focused{background-color:#e6e6e6}.custom-day.range,.custom-day:hover{background-color:var(--smarterplan-primary-lighter);color:#fff}.custom-day.faded{background-color:var(--smarterplan-primary-transparent)}.calendar{height:100%}.appliedText,.appliedText span{color:var(--smarterplan-primary)}@media (max-width: 767px){.form-inline{flex-direction:column}}.hidden{opacity:0}.date-selector{flex-wrap:nowrap}.date-selector input{min-width:80px;margin:0}@media (min-width: 768px){.w-md-fit-content{width:fit-content!important}.h-md-content{height:fit-content!important}}\n"] }]
|
|
17554
18553
|
}], ctorParameters: () => [{ type: i1$1.Router }, { type: LocaleService }, { type: BaseUserService }, { type: NavigatorService }, { type: SupportService }, { type: BaseVisibilityService }, { type: i1$2.NgbCalendar }, { type: i1$2.NgbDateParserFormatter }, { type: FilterService }, { type: AmplifyCacheService }], propDecorators: { cacheName: [{
|
|
17555
18554
|
type: Input
|
|
17556
18555
|
}] } });
|
|
17557
18556
|
|
|
18557
|
+
/**
|
|
18558
|
+
* Gaussian splats, drawn by SuperSplat's viewer (PlayCanvas) in an iframe, as Matterport's showcase
|
|
18559
|
+
* is: the page built from projects/splat-viewer, which each app serves from assets/viewers/splat.
|
|
18560
|
+
*
|
|
18561
|
+
* The visit's Three.js scene stays underneath, unseen, holding the tags, their calibration and the
|
|
18562
|
+
* measurement, as it does for a model it draws itself; this hands the viewer what the scene holds,
|
|
18563
|
+
* and turns what is clicked in it into what a click in the scene does - through ViewerService, so
|
|
18564
|
+
* that whatever drives the scene (the add buttons, the calibration panel, the measuring tool, the
|
|
18565
|
+
* tag lists) drives this as well. Tags clicked come out of ViewerService.threeJsTagClicked$, as
|
|
18566
|
+
* clicks in the scene do.
|
|
18567
|
+
*
|
|
18568
|
+
* What it says of loading is for the app's own loading overlay; see the outputs.
|
|
18569
|
+
*/
|
|
18570
|
+
class SplatViewerComponent {
|
|
18571
|
+
viewerService;
|
|
18572
|
+
host;
|
|
18573
|
+
sanitizer;
|
|
18574
|
+
translate;
|
|
18575
|
+
/** The model: the S3 key - or assets/ path - of a splat file, or of a Streamed SOG's lod-meta.json. */
|
|
18576
|
+
modelKey = null;
|
|
18577
|
+
/** Which of the model's axes is up, from its capture's placement; null for the viewer's own idea. */
|
|
18578
|
+
up = null;
|
|
18579
|
+
/** How far loading has got, from 0 to 1, or null when the size is not known. 0 when it starts. */
|
|
18580
|
+
loadProgress = new EventEmitter();
|
|
18581
|
+
loaded = new EventEmitter();
|
|
18582
|
+
loadFailed = new EventEmitter();
|
|
18583
|
+
/** Said whenever the mode changes, the viewer's own buttons included. */
|
|
18584
|
+
modeChange = new EventEmitter();
|
|
18585
|
+
frame;
|
|
18586
|
+
/**
|
|
18587
|
+
* The viewer's page, made once, in ngOnInit: a new object, even for the same address, would make
|
|
18588
|
+
* Angular set the iframe's src again, which loads the viewer afresh with nothing in it.
|
|
18589
|
+
*/
|
|
18590
|
+
page;
|
|
18591
|
+
/** Whether the page has said it is ready; it says so again each time it starts over. */
|
|
18592
|
+
ready = false;
|
|
18593
|
+
/** The model handed to the page, so the same one is not downloaded twice. */
|
|
18594
|
+
loadedKey = null;
|
|
18595
|
+
/** Set by cancelLoad: the page, starting over empty, waits to be asked again (reload). */
|
|
18596
|
+
cancelled = false;
|
|
18597
|
+
/** Whether the page was last asked to take a click for a calibration. */
|
|
18598
|
+
pickingForCalibration = false;
|
|
18599
|
+
/** The calibration under way, as ViewerService last said. */
|
|
18600
|
+
alignment = null;
|
|
18601
|
+
/** Each segment's length of the measurement on the model, as ViewerService last said. */
|
|
18602
|
+
measureDistances = [];
|
|
18603
|
+
/** The last lod-meta.json handed to the page, as a link of this page's own. */
|
|
18604
|
+
streamedMeta = null;
|
|
18605
|
+
/** Pictures asked of the page, by the number they were asked with, until it answers. */
|
|
18606
|
+
snapshots = new Map();
|
|
18607
|
+
snapshotCount = 0;
|
|
18608
|
+
destroyed$ = new Subject();
|
|
18609
|
+
constructor(viewerService, host, sanitizer, translate) {
|
|
18610
|
+
this.viewerService = viewerService;
|
|
18611
|
+
this.host = host;
|
|
18612
|
+
this.sanitizer = sanitizer;
|
|
18613
|
+
this.translate = translate;
|
|
18614
|
+
}
|
|
18615
|
+
ngOnInit() {
|
|
18616
|
+
// In the app's language, which the viewer's controls speak, and in its colours, as this element's
|
|
18617
|
+
// styles take them from the app's - by now in the page, where they reach it.
|
|
18618
|
+
const style = getComputedStyle(this.host.nativeElement);
|
|
18619
|
+
const query = new URLSearchParams({
|
|
18620
|
+
lang: this.translate.currentLang || 'en',
|
|
18621
|
+
accent: style.color,
|
|
18622
|
+
background: style.backgroundColor,
|
|
18623
|
+
});
|
|
18624
|
+
this.page = this.sanitizer.bypassSecurityTrustResourceUrl(`assets/viewers/splat/index.html?${query}`);
|
|
18625
|
+
// The Three.js scene underneath has nothing to draw while this covers it, and the splat viewer
|
|
18626
|
+
// needs all the graphics card there is.
|
|
18627
|
+
this.viewerService.setThreeJsRenderingPaused(true);
|
|
18628
|
+
const on = (source, next) => source.pipe(takeUntil(this.destroyed$)).subscribe(next);
|
|
18629
|
+
on(this.viewerService.threeJsTagsChanged$, () => this.postTags());
|
|
18630
|
+
// Placing a tag starts from the add buttons, the same in every viewer; the page picks the spot
|
|
18631
|
+
// and says where ('picked').
|
|
18632
|
+
on(this.viewerService.threeJsPlacementChanged$, (placing) => this.post({ type: 'placing', on: placing }));
|
|
18633
|
+
on(this.viewerService.threeJsAlignmentProgress$, (progress) => {
|
|
18634
|
+
this.alignment = progress;
|
|
18635
|
+
this.postCalibration();
|
|
18636
|
+
});
|
|
18637
|
+
on(this.viewerService.threeJsAlignmentCancelled$, () => {
|
|
18638
|
+
this.alignment = null;
|
|
18639
|
+
this.postCalibration();
|
|
18640
|
+
});
|
|
18641
|
+
on(this.viewerService.threeJsMeasuringChanged$, (measuring) => this.post({ type: 'measuring', on: measuring }));
|
|
18642
|
+
on(this.viewerService.threeJsMeasurementProgress$, (progress) => {
|
|
18643
|
+
this.measureDistances = progress?.distances ?? [];
|
|
18644
|
+
this.postMeasurement(!!progress);
|
|
18645
|
+
});
|
|
18646
|
+
// A finished measurement stays drawn while its form is open; this is when it goes.
|
|
18647
|
+
on(this.viewerService.threeJsMeasurementCleared$, () => {
|
|
18648
|
+
this.measureDistances = [];
|
|
18649
|
+
this.postMeasurement(false);
|
|
18650
|
+
});
|
|
18651
|
+
// The camera sent to a tag, from a list: the page points it out.
|
|
18652
|
+
on(this.viewerService.threeJsTagFocused$, (id) => this.post({ type: 'goToTag', id }));
|
|
18653
|
+
}
|
|
18654
|
+
ngOnChanges(changes) {
|
|
18655
|
+
if (changes['modelKey'] && this.ready && !this.cancelled) {
|
|
18656
|
+
void this.load();
|
|
18657
|
+
}
|
|
18658
|
+
}
|
|
18659
|
+
ngOnDestroy() {
|
|
18660
|
+
this.destroyed$.next();
|
|
18661
|
+
this.destroyed$.complete();
|
|
18662
|
+
this.viewerService.setThreeJsRenderingPaused(false);
|
|
18663
|
+
if (this.streamedMeta) {
|
|
18664
|
+
URL.revokeObjectURL(this.streamedMeta);
|
|
18665
|
+
}
|
|
18666
|
+
this.snapshots.forEach((resolve) => resolve(null));
|
|
18667
|
+
this.snapshots.clear();
|
|
18668
|
+
}
|
|
18669
|
+
/** Stops the model loading; nothing is loaded again until reload(). */
|
|
18670
|
+
cancelLoad() {
|
|
18671
|
+
this.cancelled = true;
|
|
18672
|
+
this.loadedKey = null;
|
|
18673
|
+
this.post({ type: 'cancel-load' });
|
|
18674
|
+
}
|
|
18675
|
+
/** Loads the model again, after cancelLoad. */
|
|
18676
|
+
reload() {
|
|
18677
|
+
this.cancelled = false;
|
|
18678
|
+
this.loadedKey = null;
|
|
18679
|
+
if (this.ready) {
|
|
18680
|
+
void this.load();
|
|
18681
|
+
}
|
|
18682
|
+
}
|
|
18683
|
+
setMode(mode) {
|
|
18684
|
+
this.post({ type: 'mode', mode });
|
|
18685
|
+
}
|
|
18686
|
+
/**
|
|
18687
|
+
* The scene as it is on screen, with the measurement drawn on it and no tags, as a JPEG data URL;
|
|
18688
|
+
* null when there is no scene to take, or the page does not answer within ten seconds.
|
|
18689
|
+
*/
|
|
18690
|
+
snapshot() {
|
|
18691
|
+
const id = ++this.snapshotCount;
|
|
18692
|
+
return new Promise((resolve) => {
|
|
18693
|
+
this.snapshots.set(id, resolve);
|
|
18694
|
+
this.post({ type: 'snapshot', id });
|
|
18695
|
+
setTimeout(() => {
|
|
18696
|
+
if (this.snapshots.delete(id)) {
|
|
18697
|
+
resolve(null);
|
|
18698
|
+
}
|
|
18699
|
+
}, 10000);
|
|
18700
|
+
});
|
|
18701
|
+
}
|
|
18702
|
+
/** What the page says: that it is ready, how loading goes, what was clicked in it. */
|
|
18703
|
+
onMessage(event) {
|
|
18704
|
+
// Only this component's page, served from this same origin.
|
|
18705
|
+
if (event.source !== this.frame?.nativeElement.contentWindow || event.origin !== window.location.origin) {
|
|
18706
|
+
return;
|
|
18707
|
+
}
|
|
18708
|
+
const message = event.data;
|
|
18709
|
+
const aligning = this.viewerService.isThreeJsAligningTags();
|
|
18710
|
+
switch (message?.type) {
|
|
18711
|
+
case 'ready':
|
|
18712
|
+
// A new page, with nothing loaded or armed in it: the first, or the viewer starting over
|
|
18713
|
+
// for the next model or after a cancelled load.
|
|
18714
|
+
this.ready = true;
|
|
18715
|
+
this.loadedKey = null;
|
|
18716
|
+
this.pickingForCalibration = false;
|
|
18717
|
+
if (!this.cancelled) {
|
|
18718
|
+
void this.load();
|
|
18719
|
+
}
|
|
18720
|
+
this.postTags();
|
|
18721
|
+
this.postCalibration();
|
|
18722
|
+
this.post({ type: 'measuring', on: this.viewerService.isThreeJsMeasuring() });
|
|
18723
|
+
this.postMeasurement(this.viewerService.getThreeJsMeasurePointsInModelFrame().length > 0);
|
|
18724
|
+
break;
|
|
18725
|
+
case 'progress':
|
|
18726
|
+
this.loadProgress.emit(message.fraction ?? null);
|
|
18727
|
+
break;
|
|
18728
|
+
case 'loaded':
|
|
18729
|
+
this.loaded.emit();
|
|
18730
|
+
break;
|
|
18731
|
+
case 'error':
|
|
18732
|
+
console.error('Splat viewer:', message.message);
|
|
18733
|
+
this.loadFailed.emit(String(message.message ?? ''));
|
|
18734
|
+
break;
|
|
18735
|
+
case 'mode':
|
|
18736
|
+
this.modeChange.emit(message.mode);
|
|
18737
|
+
break;
|
|
18738
|
+
case 'picked':
|
|
18739
|
+
if (aligning && this.alignment?.awaiting === 'point') {
|
|
18740
|
+
// Where the tag being matched really is, as a click on the model in the scene would say.
|
|
18741
|
+
if (!this.viewerService.matchThreeJsAlignmentTagAtModelPoint(message.position)) {
|
|
18742
|
+
this.viewerService.ignoreThreeJsAlignmentClick('point');
|
|
18743
|
+
}
|
|
18744
|
+
break;
|
|
18745
|
+
}
|
|
18746
|
+
// As a click on the model in the scene: the app opens the creation form from threeJsTagPlaced$.
|
|
18747
|
+
this.viewerService.placeThreeJsTagAtModelPoint(message.position, message.normal ?? null);
|
|
18748
|
+
break;
|
|
18749
|
+
case 'placing-cancelled':
|
|
18750
|
+
// Escape, as in the scene: it ends a measurement or a calibration as it ends placing a tag.
|
|
18751
|
+
if (this.viewerService.isThreeJsMeasuring()) {
|
|
18752
|
+
this.viewerService.action_toolbox_cancel_mesure();
|
|
18753
|
+
}
|
|
18754
|
+
else if (aligning) {
|
|
18755
|
+
this.viewerService.cancelThreeJsTagAlignment();
|
|
18756
|
+
}
|
|
18757
|
+
else {
|
|
18758
|
+
this.viewerService.cancelThreeJsTagPlacement();
|
|
18759
|
+
}
|
|
18760
|
+
break;
|
|
18761
|
+
case 'calibration-undo':
|
|
18762
|
+
// Ctrl+Z or Backspace in the page, as in the scene.
|
|
18763
|
+
this.viewerService.undoThreeJsAlignmentStep();
|
|
18764
|
+
break;
|
|
18765
|
+
case 'measure-point':
|
|
18766
|
+
this.viewerService.addThreeJsMeasurePointAtModelPoint(message.position);
|
|
18767
|
+
break;
|
|
18768
|
+
case 'tagClick':
|
|
18769
|
+
if (aligning) {
|
|
18770
|
+
// Calibrating: a tag clicked is the one to match, as in the scene, and opens nothing.
|
|
18771
|
+
if (this.alignment?.awaiting !== 'tag' || !this.viewerService.selectThreeJsAlignmentTag(message.id)) {
|
|
18772
|
+
this.viewerService.ignoreThreeJsAlignmentClick('tag');
|
|
18773
|
+
}
|
|
18774
|
+
break;
|
|
18775
|
+
}
|
|
18776
|
+
this.viewerService.clickThreeJsTag(message.id);
|
|
18777
|
+
break;
|
|
18778
|
+
case 'snapshot':
|
|
18779
|
+
this.snapshots.get(message.id)?.(message.image ?? null);
|
|
18780
|
+
this.snapshots.delete(message.id);
|
|
18781
|
+
break;
|
|
18782
|
+
case 'sign': {
|
|
18783
|
+
// A piece of the Streamed SOG open, by its name beside lod-meta.json.
|
|
18784
|
+
const key = this.loadedKey;
|
|
18785
|
+
const folder = key ? key.slice(0, key.lastIndexOf('/') + 1) : '';
|
|
18786
|
+
const file = String(message.file ?? '');
|
|
18787
|
+
const allowed = key && isStreamedSplatFile(key) && /^[\w.-]+$/.test(file);
|
|
18788
|
+
(allowed ? getSignedFile(folder + file) : Promise.resolve(null)).then((url) => {
|
|
18789
|
+
this.post({ type: 'signed', id: message.id, url });
|
|
18790
|
+
});
|
|
18791
|
+
break;
|
|
18792
|
+
}
|
|
18793
|
+
default:
|
|
18794
|
+
break;
|
|
18795
|
+
}
|
|
18796
|
+
}
|
|
18797
|
+
async load() {
|
|
18798
|
+
const key = this.modelKey;
|
|
18799
|
+
if (!key || key === this.loadedKey) {
|
|
18800
|
+
return;
|
|
18801
|
+
}
|
|
18802
|
+
this.loadedKey = key;
|
|
18803
|
+
this.loadProgress.emit(0);
|
|
18804
|
+
let url = key.startsWith('assets/') ? key : await getSignedFile(key);
|
|
18805
|
+
if (url && isStreamedSplatFile(key)) {
|
|
18806
|
+
url = await this.streamedEntry(url);
|
|
18807
|
+
}
|
|
18808
|
+
if (this.loadedKey !== key) {
|
|
18809
|
+
// Another model was asked for, or the load cancelled, while the link was being signed.
|
|
18810
|
+
return;
|
|
18811
|
+
}
|
|
18812
|
+
this.post({ type: 'load', url, fileName: key.split('/').pop(), up: this.up });
|
|
18813
|
+
}
|
|
18814
|
+
/**
|
|
18815
|
+
* The lod-meta.json of a Streamed SOG, with its pieces named so that the page asks for each of
|
|
18816
|
+
* them when it needs it ('sign').
|
|
18817
|
+
*
|
|
18818
|
+
* The pieces sit beside lod-meta.json in S3 and are named relative to it, which a signed link -
|
|
18819
|
+
* its signature in the query - does not survive. And they are fetched while the visit lasts, as the
|
|
18820
|
+
* camera reaches them, long after any link signed up front has run out.
|
|
18821
|
+
*/
|
|
18822
|
+
async streamedEntry(signedMeta) {
|
|
18823
|
+
const meta = await (await fetch(signedMeta)).json();
|
|
18824
|
+
meta.filenames = (meta.filenames ?? []).map((name) => `https://splat.invalid/${name}`);
|
|
18825
|
+
if (this.streamedMeta) {
|
|
18826
|
+
URL.revokeObjectURL(this.streamedMeta);
|
|
18827
|
+
}
|
|
18828
|
+
this.streamedMeta = URL.createObjectURL(new Blob([JSON.stringify(meta)], { type: 'application/json' }));
|
|
18829
|
+
return this.streamedMeta;
|
|
18830
|
+
}
|
|
18831
|
+
postTags() {
|
|
18832
|
+
const { tags, alignment } = this.viewerService.getThreeJsTagSnapshot();
|
|
18833
|
+
// Tags placed elsewhere - in Matterport, in another model - are in that place's coordinates,
|
|
18834
|
+
// and only a calibration says where they are in this one. Without it they float off the model
|
|
18835
|
+
// and seem to slide as the camera moves: they are left out until it is calibrated. A
|
|
18836
|
+
// calibration under way, or its panel open, shows them all: they are what it matches.
|
|
18837
|
+
const all = this.viewerService.isThreeJsCalibrated() || this.viewerService.isThreeJsShowingCalibrationTags();
|
|
18838
|
+
const shown = all ? tags : tags.filter((tag) => tag.inModelFrame);
|
|
18839
|
+
this.post({
|
|
18840
|
+
type: 'tags',
|
|
18841
|
+
tags: shown.map(({ id, position, label, iconUrl, inModelFrame, normal }) => ({ id, position, label, iconUrl, inModelFrame, normal })),
|
|
18842
|
+
alignment,
|
|
18843
|
+
});
|
|
18844
|
+
}
|
|
18845
|
+
/**
|
|
18846
|
+
* The calibration under way, for the page to draw - the pins dropped, the tag being matched - and,
|
|
18847
|
+
* while the place of a tag is asked for, to take the click on the model that says where ('picked').
|
|
18848
|
+
*/
|
|
18849
|
+
postCalibration() {
|
|
18850
|
+
const on = this.viewerService.isThreeJsAligningTags() && !!this.alignment;
|
|
18851
|
+
const awaiting = on ? this.alignment.awaiting : null;
|
|
18852
|
+
this.post({
|
|
18853
|
+
type: 'calibration',
|
|
18854
|
+
calibration: on
|
|
18855
|
+
? {
|
|
18856
|
+
selected: awaiting === 'point' ? this.alignment.tagId ?? null : null,
|
|
18857
|
+
pairs: this.alignment.matched.map(({ tagId, target }) => ({ tagId, target })),
|
|
18858
|
+
}
|
|
18859
|
+
: null,
|
|
18860
|
+
});
|
|
18861
|
+
const picking = awaiting === 'point';
|
|
18862
|
+
// Only when it changes: placing a tag uses the same click, and is not to be switched off here.
|
|
18863
|
+
if (picking !== this.pickingForCalibration) {
|
|
18864
|
+
this.pickingForCalibration = picking;
|
|
18865
|
+
this.post({ type: 'placing', on: picking });
|
|
18866
|
+
}
|
|
18867
|
+
}
|
|
18868
|
+
/** The measurement for the page to draw - its points, each segment's length - or nothing. */
|
|
18869
|
+
postMeasurement(drawn) {
|
|
18870
|
+
this.post({
|
|
18871
|
+
type: 'measurement',
|
|
18872
|
+
measurement: drawn
|
|
18873
|
+
? { points: this.viewerService.getThreeJsMeasurePointsInModelFrame(), distances: this.measureDistances }
|
|
18874
|
+
: null,
|
|
18875
|
+
});
|
|
18876
|
+
}
|
|
18877
|
+
post(command) {
|
|
18878
|
+
const page = this.frame?.nativeElement.contentWindow;
|
|
18879
|
+
if (page && this.ready) {
|
|
18880
|
+
page.postMessage(command, window.location.origin);
|
|
18881
|
+
}
|
|
18882
|
+
}
|
|
18883
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SplatViewerComponent, deps: [{ token: ViewerService }, { token: i0.ElementRef }, { token: i1$3.DomSanitizer }, { token: i1.TranslateService }], target: i0.ɵɵFactoryTarget.Component });
|
|
18884
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SplatViewerComponent, selector: "lib-splat-viewer", inputs: { modelKey: "modelKey", up: "up" }, outputs: { loadProgress: "loadProgress", loaded: "loaded", loadFailed: "loadFailed", modeChange: "modeChange" }, host: { listeners: { "window:message": "onMessage($event)" } }, viewQueries: [{ propertyName: "frame", first: true, predicate: ["frame"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: '<iframe #frame [src]="page" title="3D" allow="fullscreen; xr-spatial-tracking" allowfullscreen></iframe>', isInline: true, styles: [":host{display:block;color:var(--smarterplan-primary, rgb(83, 182, 187));background-color:var(--smarterplan-viewer-background, #e0f7ff)}iframe{display:block;width:100%;height:100%;border:0}\n"] });
|
|
18885
|
+
}
|
|
18886
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SplatViewerComponent, decorators: [{
|
|
18887
|
+
type: Component,
|
|
18888
|
+
args: [{ selector: 'lib-splat-viewer', template: '<iframe #frame [src]="page" title="3D" allow="fullscreen; xr-spatial-tracking" allowfullscreen></iframe>', styles: [":host{display:block;color:var(--smarterplan-primary, rgb(83, 182, 187));background-color:var(--smarterplan-viewer-background, #e0f7ff)}iframe{display:block;width:100%;height:100%;border:0}\n"] }]
|
|
18889
|
+
}], ctorParameters: () => [{ type: ViewerService }, { type: i0.ElementRef }, { type: i1$3.DomSanitizer }, { type: i1.TranslateService }], propDecorators: { modelKey: [{
|
|
18890
|
+
type: Input
|
|
18891
|
+
}], up: [{
|
|
18892
|
+
type: Input
|
|
18893
|
+
}], loadProgress: [{
|
|
18894
|
+
type: Output
|
|
18895
|
+
}], loaded: [{
|
|
18896
|
+
type: Output
|
|
18897
|
+
}], loadFailed: [{
|
|
18898
|
+
type: Output
|
|
18899
|
+
}], modeChange: [{
|
|
18900
|
+
type: Output
|
|
18901
|
+
}], frame: [{
|
|
18902
|
+
type: ViewChild,
|
|
18903
|
+
args: ['frame', { static: true }]
|
|
18904
|
+
}], onMessage: [{
|
|
18905
|
+
type: HostListener,
|
|
18906
|
+
args: ['window:message', ['$event']]
|
|
18907
|
+
}] } });
|
|
18908
|
+
|
|
17558
18909
|
class NgxSmarterplanCoreModule {
|
|
17559
18910
|
static forRoot(config) {
|
|
17560
18911
|
return {
|
|
@@ -17581,7 +18932,8 @@ class NgxSmarterplanCoreModule {
|
|
|
17581
18932
|
NavigationBarComponent,
|
|
17582
18933
|
LoaderComponent,
|
|
17583
18934
|
SupportModalComponent,
|
|
17584
|
-
FormatDateNumberToDigitsPipe
|
|
18935
|
+
FormatDateNumberToDigitsPipe,
|
|
18936
|
+
SplatViewerComponent], imports: [FormsModule,
|
|
17585
18937
|
ReactiveFormsModule,
|
|
17586
18938
|
CommonModule,
|
|
17587
18939
|
RouterModule,
|
|
@@ -17595,7 +18947,8 @@ class NgxSmarterplanCoreModule {
|
|
|
17595
18947
|
MenuBarComponent,
|
|
17596
18948
|
LoaderComponent,
|
|
17597
18949
|
SupportModalComponent,
|
|
17598
|
-
AvatarComponent
|
|
18950
|
+
AvatarComponent,
|
|
18951
|
+
SplatViewerComponent] });
|
|
17599
18952
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NgxSmarterplanCoreModule, imports: [FormsModule,
|
|
17600
18953
|
ReactiveFormsModule,
|
|
17601
18954
|
CommonModule,
|
|
@@ -17622,7 +18975,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
17622
18975
|
NavigationBarComponent,
|
|
17623
18976
|
LoaderComponent,
|
|
17624
18977
|
SupportModalComponent,
|
|
17625
|
-
FormatDateNumberToDigitsPipe
|
|
18978
|
+
FormatDateNumberToDigitsPipe,
|
|
18979
|
+
SplatViewerComponent
|
|
17626
18980
|
],
|
|
17627
18981
|
imports: [
|
|
17628
18982
|
FormsModule,
|
|
@@ -17646,6 +19000,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
17646
19000
|
LoaderComponent,
|
|
17647
19001
|
SupportModalComponent,
|
|
17648
19002
|
AvatarComponent,
|
|
19003
|
+
SplatViewerComponent,
|
|
17649
19004
|
]
|
|
17650
19005
|
}]
|
|
17651
19006
|
}] });
|
|
@@ -17705,27 +19060,27 @@ class BaseTagService {
|
|
|
17705
19060
|
throw new Error('updatePoiMetadata is not implemented in the base class.');
|
|
17706
19061
|
}*/
|
|
17707
19062
|
async prepareEquipmentHtml(equip) {
|
|
17708
|
-
throw new Error(`prepareEquipmentHtml is not implemented in the base class.
|
|
19063
|
+
throw new Error(`prepareEquipmentHtml is not implemented in the base class.
|
|
17709
19064
|
Please override this method in your extended class.`);
|
|
17710
19065
|
}
|
|
17711
19066
|
async prepareTicketHtml(ticket) {
|
|
17712
|
-
throw new Error(`prepareTicketHtml is not implemented in the base class.
|
|
19067
|
+
throw new Error(`prepareTicketHtml is not implemented in the base class.
|
|
17713
19068
|
Please override this method in your extended class.`);
|
|
17714
19069
|
}
|
|
17715
19070
|
async prepareFeatureHtml(feature) {
|
|
17716
|
-
throw new Error(`prepareFeatureHtml is not implemented in the base class.
|
|
19071
|
+
throw new Error(`prepareFeatureHtml is not implemented in the base class.
|
|
17717
19072
|
Please override this method in your extended class.`);
|
|
17718
19073
|
}
|
|
17719
19074
|
async prepareMeasurementHtml(measure) {
|
|
17720
|
-
throw new Error(`prepareMeasurementHtml is not implemented in the base class.
|
|
19075
|
+
throw new Error(`prepareMeasurementHtml is not implemented in the base class.
|
|
17721
19076
|
Please override this method in your extended class.`);
|
|
17722
19077
|
}
|
|
17723
19078
|
async prepareDeskHtml(feature) {
|
|
17724
|
-
throw new Error(`prepareDeskHtml is not implemented in the base class.
|
|
19079
|
+
throw new Error(`prepareDeskHtml is not implemented in the base class.
|
|
17725
19080
|
Please override this method in your extended class.`);
|
|
17726
19081
|
}
|
|
17727
19082
|
async prepareIndicatorHtml(feature) {
|
|
17728
|
-
throw new Error(`prepareDeskHtml is not implemented in the base class.
|
|
19083
|
+
throw new Error(`prepareDeskHtml is not implemented in the base class.
|
|
17729
19084
|
Please override this method in your extended class.`);
|
|
17730
19085
|
}
|
|
17731
19086
|
async getSignedTagIconSource(tagIconSrc) {
|
|
@@ -17734,15 +19089,15 @@ class BaseTagService {
|
|
|
17734
19089
|
getIconTagImageForFeature(feature, poi) {
|
|
17735
19090
|
// const tagIcon = JSON.parse(poi.tagIcon);
|
|
17736
19091
|
// return tagIcon.src;
|
|
17737
|
-
throw new Error(`getIconTagImageForFeature is not implemented in the base class.
|
|
19092
|
+
throw new Error(`getIconTagImageForFeature is not implemented in the base class.
|
|
17738
19093
|
Please override this method in your extended class.`);
|
|
17739
19094
|
}
|
|
17740
19095
|
getScriptForTag(object, tagType) {
|
|
17741
|
-
throw new Error(`getScriptForTag is not implemented in the base class.
|
|
19096
|
+
throw new Error(`getScriptForTag is not implemented in the base class.
|
|
17742
19097
|
Please override this method in your extended class.`);
|
|
17743
19098
|
}
|
|
17744
19099
|
getAnnexeForCommentTypeInFeature(feature, commentType) {
|
|
17745
|
-
throw new Error(`getAnnexeForCommentTypeInFeature is not implemented in the base class.
|
|
19100
|
+
throw new Error(`getAnnexeForCommentTypeInFeature is not implemented in the base class.
|
|
17746
19101
|
Please override this method in your extended class.`);
|
|
17747
19102
|
}
|
|
17748
19103
|
getBillboardMediaToEmbed(object) {
|
|
@@ -17751,27 +19106,27 @@ class BaseTagService {
|
|
|
17751
19106
|
return { comment: null, tagDescription: '' };
|
|
17752
19107
|
}
|
|
17753
19108
|
onActionDetailClick(url) {
|
|
17754
|
-
throw new Error(`onActionDetailClick is not implemented in the base class.
|
|
19109
|
+
throw new Error(`onActionDetailClick is not implemented in the base class.
|
|
17755
19110
|
Please override this method in your extended class.`);
|
|
17756
19111
|
}
|
|
17757
19112
|
onActionAudioClick(audioCommentID) {
|
|
17758
|
-
throw new Error(`onActionDetailClick is not implemented in the base class.
|
|
19113
|
+
throw new Error(`onActionDetailClick is not implemented in the base class.
|
|
17759
19114
|
Please override this method in your extended class.`);
|
|
17760
19115
|
}
|
|
17761
19116
|
onActionVideoClick(url) {
|
|
17762
|
-
throw new Error(`onActionVideoClick is not implemented in the base class.
|
|
19117
|
+
throw new Error(`onActionVideoClick is not implemented in the base class.
|
|
17763
19118
|
Please override this method in your extended class.`);
|
|
17764
19119
|
}
|
|
17765
19120
|
onActionImageClick(imageCommentID) {
|
|
17766
|
-
throw new Error(`onActionImageClick is not implemented in the base class.
|
|
19121
|
+
throw new Error(`onActionImageClick is not implemented in the base class.
|
|
17767
19122
|
Please override this method in your extended class.`);
|
|
17768
19123
|
}
|
|
17769
19124
|
onActionDocClick(url) {
|
|
17770
|
-
throw new Error(`onActionDocClick is not implemented in the base class.
|
|
19125
|
+
throw new Error(`onActionDocClick is not implemented in the base class.
|
|
17771
19126
|
Please override this method in your extended class.`);
|
|
17772
19127
|
}
|
|
17773
19128
|
onActionYoutubeClick(url) {
|
|
17774
|
-
throw new Error(`onActionYoutubeClick is not implemented in the base class.
|
|
19129
|
+
throw new Error(`onActionYoutubeClick is not implemented in the base class.
|
|
17775
19130
|
Please override this method in your extended class.`);
|
|
17776
19131
|
}
|
|
17777
19132
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BaseTagService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
@@ -18302,7 +19657,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
18302
19657
|
}] }, { type: NavigationService }, { type: ZoneService }, { type: ViewerService }, { type: LayerService }, { type: BaseUserService }, { type: PlanService }] });
|
|
18303
19658
|
|
|
18304
19659
|
/** File extensions the Three.js viewer can load as a base model. */
|
|
18305
|
-
|
|
19660
|
+
// The splat formats open in the splat viewer, which runs in an iframe.
|
|
19661
|
+
const SUPPORTED_MODEL_EXTENSIONS = ['.glb', '.gltf', '.splat.ply', '.spz', '.splat', '.ksplat', '.sog', 'lod-meta.json'];
|
|
18306
19662
|
class ThreeJsImportService {
|
|
18307
19663
|
navigationService;
|
|
18308
19664
|
zoneService;
|
|
@@ -18332,11 +19688,12 @@ class ThreeJsImportService {
|
|
|
18332
19688
|
*/
|
|
18333
19689
|
validateModelKey(key) {
|
|
18334
19690
|
if (!key || !key.trim()) {
|
|
18335
|
-
return 'A 3D model file
|
|
19691
|
+
return 'A 3D model file is required.';
|
|
18336
19692
|
}
|
|
18337
19693
|
const trimmed = key.trim();
|
|
18338
19694
|
if (!SUPPORTED_MODEL_EXTENSIONS.some((extension) => trimmed.toLowerCase().endsWith(extension))) {
|
|
18339
|
-
|
|
19695
|
+
const listed = SUPPORTED_MODEL_EXTENSIONS.slice(0, -1).join(', ');
|
|
19696
|
+
return `The 3D model must be a ${listed} or ${SUPPORTED_MODEL_EXTENSIONS[SUPPORTED_MODEL_EXTENSIONS.length - 1]} file.`;
|
|
18340
19697
|
}
|
|
18341
19698
|
// A full https:// URL is a signed link that expires; the stored value has to be the key itself
|
|
18342
19699
|
// so a fresh URL can be signed on every visit.
|
|
@@ -18442,6 +19799,655 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
18442
19799
|
}]
|
|
18443
19800
|
}], ctorParameters: () => [{ type: NavigationService }, { type: ZoneService }, { type: LayerService }, { type: BaseUserService }, { type: PlanService }] });
|
|
18444
19801
|
|
|
19802
|
+
/**
|
|
19803
|
+
* Converts an .obj model, with its .mtl and textures, into the .glb the Three.js viewer loads - in
|
|
19804
|
+
* the browser, as the model is added, with nothing to install.
|
|
19805
|
+
*
|
|
19806
|
+
* It does what obj2gltf did in converte-modelo.sh, and the reason is the same. An .obj lists every
|
|
19807
|
+
* corner of every triangle; loaded as it is, a captured model's 2.6 million distinct vertices become
|
|
19808
|
+
* 12.6 million, one per corner. Here each position and texture coordinate pair becomes one vertex,
|
|
19809
|
+
* shared by every triangle that uses it, so what reaches the graphics card is the 2.6 million.
|
|
19810
|
+
*
|
|
19811
|
+
* Textures go in as they are, their bytes untouched: a JPEG re-encoded would lose quality and gain
|
|
19812
|
+
* nothing. Normals are only written when the .obj has them; without, the viewer works them out as
|
|
19813
|
+
* it loads the model. The axes are left as the .obj has them - the viewer finds which way is up.
|
|
19814
|
+
*
|
|
19815
|
+
* The .obj is read as a stream, so a file of hundreds of megabytes is never held whole.
|
|
19816
|
+
*/
|
|
19817
|
+
/** Whether a set of files holds an .obj model to convert. */
|
|
19818
|
+
function hasObjModel(files) {
|
|
19819
|
+
return files.some((file) => /\.obj$/i.test(file.name));
|
|
19820
|
+
}
|
|
19821
|
+
/**
|
|
19822
|
+
* Converts the .obj among `files` into a .glb, using the .mtl and the images found with it.
|
|
19823
|
+
*/
|
|
19824
|
+
async function convertObjToGlb(files, options = {}) {
|
|
19825
|
+
const objs = files.filter((file) => /\.obj$/i.test(file.name));
|
|
19826
|
+
if (objs.length !== 1) {
|
|
19827
|
+
throw new Error(objs.length === 0 ? 'No .obj file was given.' : 'Give one .obj file at a time.');
|
|
19828
|
+
}
|
|
19829
|
+
const obj = objs[0];
|
|
19830
|
+
const byName = new Map(files.map((file) => [baseName(file.name).toLowerCase(), file]));
|
|
19831
|
+
// The .obj's own reading is most of the time; the rest is shared out over what follows.
|
|
19832
|
+
const parsed = await readObj(obj.stream(), obj.size, {
|
|
19833
|
+
...options,
|
|
19834
|
+
onProgress: (fraction) => options.onProgress?.(fraction * 0.85),
|
|
19835
|
+
});
|
|
19836
|
+
if (parsed.triangleCount === 0) {
|
|
19837
|
+
throw new Error(`${obj.name} has no faces.`);
|
|
19838
|
+
}
|
|
19839
|
+
// Materials: from the .mtl files the .obj names, or failing that any .mtl given with it.
|
|
19840
|
+
const named = parsed.materialLibraries.map((library) => byName.get(baseName(library).toLowerCase())).filter(Boolean);
|
|
19841
|
+
const libraries = named.length > 0 ? named : files.filter((file) => /\.mtl$/i.test(file.name));
|
|
19842
|
+
const materials = new Map();
|
|
19843
|
+
for (const library of libraries) {
|
|
19844
|
+
parseMtl(await library.text()).forEach((material) => materials.set(material.name, material));
|
|
19845
|
+
}
|
|
19846
|
+
await pause(options.signal);
|
|
19847
|
+
const breaks = new Breaks(options.signal, options.onProgress);
|
|
19848
|
+
const geometry = await indexGeometry(parsed, breaks);
|
|
19849
|
+
options.onProgress?.(0.95);
|
|
19850
|
+
await pause(options.signal);
|
|
19851
|
+
const missing = new Set();
|
|
19852
|
+
const glb = await writeGlb(parsed, geometry, materials, byName, missing, obj.name, breaks);
|
|
19853
|
+
options.onProgress?.(1);
|
|
19854
|
+
return {
|
|
19855
|
+
file: new File([glb], obj.name.replace(/\.obj$/i, '') + '.glb', { type: 'model/gltf-binary' }),
|
|
19856
|
+
vertices: geometry.vertexCount,
|
|
19857
|
+
triangles: parsed.triangleCount,
|
|
19858
|
+
materials: parsed.groups.length,
|
|
19859
|
+
missingTextures: [...missing],
|
|
19860
|
+
};
|
|
19861
|
+
}
|
|
19862
|
+
/** The file name without its folders, which is how an .mtl or a texture is looked up. */
|
|
19863
|
+
function baseName(path) {
|
|
19864
|
+
return path.trim().replace(/^"|"$/g, '').split(/[\\/]/).pop() ?? path;
|
|
19865
|
+
}
|
|
19866
|
+
/** Lets the page draw and answer for a moment, and stops here if the conversion was cancelled. */
|
|
19867
|
+
async function pause(signal) {
|
|
19868
|
+
if (signal?.aborted) {
|
|
19869
|
+
throw new DOMException('Model conversion cancelled', 'AbortError');
|
|
19870
|
+
}
|
|
19871
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
19872
|
+
}
|
|
19873
|
+
/**
|
|
19874
|
+
* Stops a long loop now and then so the page can draw and answer clicks. Checked only every few
|
|
19875
|
+
* thousand turns of the loop: waiting on a promise every turn would cost more than the loop.
|
|
19876
|
+
*/
|
|
19877
|
+
class Breaks {
|
|
19878
|
+
signal;
|
|
19879
|
+
onProgress;
|
|
19880
|
+
last = performance.now();
|
|
19881
|
+
constructor(signal, onProgress) {
|
|
19882
|
+
this.signal = signal;
|
|
19883
|
+
this.onProgress = onProgress;
|
|
19884
|
+
}
|
|
19885
|
+
/** Whether this turn of the loop should stop for a moment. Synchronous: it is asked every turn. */
|
|
19886
|
+
due(turn) {
|
|
19887
|
+
return (turn & 0x3fff) === 0 && performance.now() - this.last > 40;
|
|
19888
|
+
}
|
|
19889
|
+
/** Stops for a moment, reporting how far the conversion has got. */
|
|
19890
|
+
async take(fraction) {
|
|
19891
|
+
this.onProgress?.(fraction);
|
|
19892
|
+
await pause(this.signal);
|
|
19893
|
+
this.last = performance.now();
|
|
19894
|
+
}
|
|
19895
|
+
}
|
|
19896
|
+
/** A growable typed array: an .obj does not say up front how many of anything it holds. */
|
|
19897
|
+
class Growable {
|
|
19898
|
+
data;
|
|
19899
|
+
length = 0;
|
|
19900
|
+
constructor(data) {
|
|
19901
|
+
this.data = data;
|
|
19902
|
+
}
|
|
19903
|
+
push(value) {
|
|
19904
|
+
if (this.length === this.data.length) {
|
|
19905
|
+
const bigger = new this.data.constructor(this.data.length * 2);
|
|
19906
|
+
bigger.set(this.data);
|
|
19907
|
+
this.data = bigger;
|
|
19908
|
+
}
|
|
19909
|
+
this.data[this.length] = value;
|
|
19910
|
+
this.length += 1;
|
|
19911
|
+
}
|
|
19912
|
+
view() {
|
|
19913
|
+
return this.data.subarray(0, this.length);
|
|
19914
|
+
}
|
|
19915
|
+
}
|
|
19916
|
+
/**
|
|
19917
|
+
* Reads an .obj from a stream of its bytes: positions, texture coordinates, normals, and triangles
|
|
19918
|
+
* grouped by material. Polygons are split into triangles; negative indices, counting back from the
|
|
19919
|
+
* end, are resolved; anything else (objects, groups, smoothing) is skipped.
|
|
19920
|
+
*/
|
|
19921
|
+
async function readObj(stream, size, options = {}) {
|
|
19922
|
+
const parser = new ObjParser();
|
|
19923
|
+
const reader = stream.getReader();
|
|
19924
|
+
let read = 0;
|
|
19925
|
+
let lastBreak = performance.now();
|
|
19926
|
+
try {
|
|
19927
|
+
for (;;) {
|
|
19928
|
+
if (options.signal?.aborted) {
|
|
19929
|
+
throw new DOMException('Model conversion cancelled', 'AbortError');
|
|
19930
|
+
}
|
|
19931
|
+
const { done, value } = await reader.read();
|
|
19932
|
+
if (done) {
|
|
19933
|
+
break;
|
|
19934
|
+
}
|
|
19935
|
+
parser.push(value);
|
|
19936
|
+
read += value.length;
|
|
19937
|
+
if (performance.now() - lastBreak > 40) {
|
|
19938
|
+
options.onProgress?.(size > 0 ? Math.min(read / size, 1) : 0);
|
|
19939
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
19940
|
+
lastBreak = performance.now();
|
|
19941
|
+
}
|
|
19942
|
+
}
|
|
19943
|
+
}
|
|
19944
|
+
finally {
|
|
19945
|
+
reader.cancel().catch(() => undefined);
|
|
19946
|
+
}
|
|
19947
|
+
parser.end();
|
|
19948
|
+
options.onProgress?.(1);
|
|
19949
|
+
return parser.result();
|
|
19950
|
+
}
|
|
19951
|
+
/**
|
|
19952
|
+
* Line by line, straight from the bytes. Building a string for each of the millions of lines of a
|
|
19953
|
+
* captured model would be most of the work; this reads the numbers off the bytes instead.
|
|
19954
|
+
*/
|
|
19955
|
+
class ObjParser {
|
|
19956
|
+
positions = new Growable(new Float64Array(1 << 16));
|
|
19957
|
+
uvs = new Growable(new Float64Array(1 << 16));
|
|
19958
|
+
normals = new Growable(new Float64Array(1 << 16));
|
|
19959
|
+
groups = [];
|
|
19960
|
+
groupByMaterial = new Map();
|
|
19961
|
+
current = null;
|
|
19962
|
+
materialLibraries = [];
|
|
19963
|
+
triangleCount = 0;
|
|
19964
|
+
/** The bytes of the line being read, kept across chunks. */
|
|
19965
|
+
line = new Uint8Array(256);
|
|
19966
|
+
lineLength = 0;
|
|
19967
|
+
// The corners of the face being read, as position/texture/normal indices.
|
|
19968
|
+
face = new Int32Array(3 * 64);
|
|
19969
|
+
push(bytes) {
|
|
19970
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
19971
|
+
const byte = bytes[index];
|
|
19972
|
+
if (byte === 10) {
|
|
19973
|
+
this.readLine();
|
|
19974
|
+
this.lineLength = 0;
|
|
19975
|
+
}
|
|
19976
|
+
else {
|
|
19977
|
+
if (this.lineLength === this.line.length) {
|
|
19978
|
+
const longer = new Uint8Array(this.line.length * 2);
|
|
19979
|
+
longer.set(this.line);
|
|
19980
|
+
this.line = longer;
|
|
19981
|
+
}
|
|
19982
|
+
this.line[this.lineLength] = byte;
|
|
19983
|
+
this.lineLength += 1;
|
|
19984
|
+
}
|
|
19985
|
+
}
|
|
19986
|
+
}
|
|
19987
|
+
end() {
|
|
19988
|
+
this.readLine();
|
|
19989
|
+
this.lineLength = 0;
|
|
19990
|
+
}
|
|
19991
|
+
result() {
|
|
19992
|
+
return {
|
|
19993
|
+
positions: this.positions.view(),
|
|
19994
|
+
uvs: this.uvs.view(),
|
|
19995
|
+
normals: this.normals.view(),
|
|
19996
|
+
groups: this.groups.filter((group) => group.corners.length > 0),
|
|
19997
|
+
materialLibraries: this.materialLibraries,
|
|
19998
|
+
triangleCount: this.triangleCount,
|
|
19999
|
+
};
|
|
20000
|
+
}
|
|
20001
|
+
readLine() {
|
|
20002
|
+
const line = this.line;
|
|
20003
|
+
const length = this.lineLength;
|
|
20004
|
+
let at = 0;
|
|
20005
|
+
while (at < length && (line[at] === 32 || line[at] === 9)) {
|
|
20006
|
+
at += 1;
|
|
20007
|
+
}
|
|
20008
|
+
if (at + 1 >= length) {
|
|
20009
|
+
return;
|
|
20010
|
+
}
|
|
20011
|
+
const first = line[at];
|
|
20012
|
+
const second = line[at + 1];
|
|
20013
|
+
if (first === 118 && (second === 32 || second === 9)) {
|
|
20014
|
+
// v x y z
|
|
20015
|
+
this.readNumbers(at + 2, 3, this.positions);
|
|
20016
|
+
}
|
|
20017
|
+
else if (first === 118 && second === 116) {
|
|
20018
|
+
// vt u v
|
|
20019
|
+
this.readNumbers(at + 3, 2, this.uvs);
|
|
20020
|
+
}
|
|
20021
|
+
else if (first === 118 && second === 110) {
|
|
20022
|
+
// vn x y z
|
|
20023
|
+
this.readNumbers(at + 3, 3, this.normals);
|
|
20024
|
+
}
|
|
20025
|
+
else if (first === 102 && (second === 32 || second === 9)) {
|
|
20026
|
+
this.readFace(at + 2);
|
|
20027
|
+
}
|
|
20028
|
+
else if (this.startsWith(at, 'usemtl')) {
|
|
20029
|
+
this.useMaterial(this.restOfLine(at + 6));
|
|
20030
|
+
}
|
|
20031
|
+
else if (this.startsWith(at, 'mtllib')) {
|
|
20032
|
+
this.materialLibraries.push(...this.restOfLine(at + 6).split(/\s+(?=\S+\.mtl\b)/i).filter(Boolean));
|
|
20033
|
+
}
|
|
20034
|
+
}
|
|
20035
|
+
startsWith(at, word) {
|
|
20036
|
+
for (let index = 0; index < word.length; index += 1) {
|
|
20037
|
+
if (this.line[at + index] !== word.charCodeAt(index)) {
|
|
20038
|
+
return false;
|
|
20039
|
+
}
|
|
20040
|
+
}
|
|
20041
|
+
const after = this.line[at + word.length];
|
|
20042
|
+
return after === 32 || after === 9;
|
|
20043
|
+
}
|
|
20044
|
+
restOfLine(at) {
|
|
20045
|
+
let end = this.lineLength;
|
|
20046
|
+
while (end > at && (this.line[end - 1] === 13 || this.line[end - 1] === 32 || this.line[end - 1] === 9)) {
|
|
20047
|
+
end -= 1;
|
|
20048
|
+
}
|
|
20049
|
+
return new TextDecoder().decode(this.line.subarray(at, end)).trim();
|
|
20050
|
+
}
|
|
20051
|
+
useMaterial(name) {
|
|
20052
|
+
let group = this.groupByMaterial.get(name);
|
|
20053
|
+
if (!group) {
|
|
20054
|
+
group = { material: name, corners: new Growable(new Int32Array(1 << 12)) };
|
|
20055
|
+
this.groupByMaterial.set(name, group);
|
|
20056
|
+
this.groups.push(group);
|
|
20057
|
+
}
|
|
20058
|
+
this.current = group;
|
|
20059
|
+
}
|
|
20060
|
+
/** Reads up to `count` numbers from `at` onwards into `target`, padding a short line with 0. */
|
|
20061
|
+
readNumbers(at, count, target) {
|
|
20062
|
+
let read = 0;
|
|
20063
|
+
let position = at;
|
|
20064
|
+
while (read < count) {
|
|
20065
|
+
const [value, next] = this.readNumber(position);
|
|
20066
|
+
if (next === position) {
|
|
20067
|
+
break;
|
|
20068
|
+
}
|
|
20069
|
+
target.push(value);
|
|
20070
|
+
read += 1;
|
|
20071
|
+
position = next;
|
|
20072
|
+
}
|
|
20073
|
+
for (; read < count; read += 1) {
|
|
20074
|
+
target.push(0);
|
|
20075
|
+
}
|
|
20076
|
+
}
|
|
20077
|
+
/** Reads one number starting at or after `at`; returns it and where it ended. */
|
|
20078
|
+
readNumber(at) {
|
|
20079
|
+
const line = this.line;
|
|
20080
|
+
const length = this.lineLength;
|
|
20081
|
+
let index = at;
|
|
20082
|
+
while (index < length && (line[index] === 32 || line[index] === 9 || line[index] === 13)) {
|
|
20083
|
+
index += 1;
|
|
20084
|
+
}
|
|
20085
|
+
const start = index;
|
|
20086
|
+
let negative = false;
|
|
20087
|
+
if (line[index] === 45 || line[index] === 43) {
|
|
20088
|
+
negative = line[index] === 45;
|
|
20089
|
+
index += 1;
|
|
20090
|
+
}
|
|
20091
|
+
let whole = 0;
|
|
20092
|
+
let fraction = 0;
|
|
20093
|
+
let scale = 1;
|
|
20094
|
+
let digits = 0;
|
|
20095
|
+
while (index < length && line[index] >= 48 && line[index] <= 57) {
|
|
20096
|
+
whole = whole * 10 + (line[index] - 48);
|
|
20097
|
+
index += 1;
|
|
20098
|
+
digits += 1;
|
|
20099
|
+
}
|
|
20100
|
+
if (line[index] === 46) {
|
|
20101
|
+
index += 1;
|
|
20102
|
+
while (index < length && line[index] >= 48 && line[index] <= 57) {
|
|
20103
|
+
fraction = fraction * 10 + (line[index] - 48);
|
|
20104
|
+
scale *= 10;
|
|
20105
|
+
index += 1;
|
|
20106
|
+
digits += 1;
|
|
20107
|
+
}
|
|
20108
|
+
}
|
|
20109
|
+
if (digits === 0) {
|
|
20110
|
+
return [0, at];
|
|
20111
|
+
}
|
|
20112
|
+
let value = whole + fraction / scale;
|
|
20113
|
+
if (line[index] === 101 || line[index] === 69) {
|
|
20114
|
+
index += 1;
|
|
20115
|
+
let negativeExponent = false;
|
|
20116
|
+
if (line[index] === 45 || line[index] === 43) {
|
|
20117
|
+
negativeExponent = line[index] === 45;
|
|
20118
|
+
index += 1;
|
|
20119
|
+
}
|
|
20120
|
+
let exponent = 0;
|
|
20121
|
+
while (index < length && line[index] >= 48 && line[index] <= 57) {
|
|
20122
|
+
exponent = exponent * 10 + (line[index] - 48);
|
|
20123
|
+
index += 1;
|
|
20124
|
+
}
|
|
20125
|
+
value *= 10 ** (negativeExponent ? -exponent : exponent);
|
|
20126
|
+
}
|
|
20127
|
+
return [negative ? -value : value, index === start ? at : index];
|
|
20128
|
+
}
|
|
20129
|
+
/** f v/vt/vn ... - any of the three forms, any number of corners, split into a fan. */
|
|
20130
|
+
readFace(at) {
|
|
20131
|
+
const line = this.line;
|
|
20132
|
+
const length = this.lineLength;
|
|
20133
|
+
let corners = 0;
|
|
20134
|
+
let index = at;
|
|
20135
|
+
while (index < length) {
|
|
20136
|
+
while (index < length && (line[index] === 32 || line[index] === 9 || line[index] === 13)) {
|
|
20137
|
+
index += 1;
|
|
20138
|
+
}
|
|
20139
|
+
if (index >= length) {
|
|
20140
|
+
break;
|
|
20141
|
+
}
|
|
20142
|
+
// One corner: up to three integers separated by '/', any of the last two possibly empty.
|
|
20143
|
+
const values = [0, 0, 0];
|
|
20144
|
+
for (let slot = 0; slot < 3; slot += 1) {
|
|
20145
|
+
let negative = false;
|
|
20146
|
+
if (line[index] === 45) {
|
|
20147
|
+
negative = true;
|
|
20148
|
+
index += 1;
|
|
20149
|
+
}
|
|
20150
|
+
let value = 0;
|
|
20151
|
+
let digits = 0;
|
|
20152
|
+
while (index < length && line[index] >= 48 && line[index] <= 57) {
|
|
20153
|
+
value = value * 10 + (line[index] - 48);
|
|
20154
|
+
index += 1;
|
|
20155
|
+
digits += 1;
|
|
20156
|
+
}
|
|
20157
|
+
values[slot] = digits === 0 ? 0 : negative ? -value : value;
|
|
20158
|
+
if (line[index] !== 47) {
|
|
20159
|
+
break;
|
|
20160
|
+
}
|
|
20161
|
+
index += 1;
|
|
20162
|
+
}
|
|
20163
|
+
if (corners < 64 && values[0] !== 0) {
|
|
20164
|
+
// To zero-based indices, counting a negative one back from what has been read so far; -1
|
|
20165
|
+
// means "none" for the texture and normal.
|
|
20166
|
+
this.face[corners * 3] = this.resolve(values[0], this.positions.length / 3);
|
|
20167
|
+
this.face[corners * 3 + 1] = values[1] === 0 ? -1 : this.resolve(values[1], this.uvs.length / 2);
|
|
20168
|
+
this.face[corners * 3 + 2] = values[2] === 0 ? -1 : this.resolve(values[2], this.normals.length / 3);
|
|
20169
|
+
corners += 1;
|
|
20170
|
+
}
|
|
20171
|
+
// Skip to the next separator, past anything unexpected in this token.
|
|
20172
|
+
while (index < length && line[index] !== 32 && line[index] !== 9) {
|
|
20173
|
+
index += 1;
|
|
20174
|
+
}
|
|
20175
|
+
}
|
|
20176
|
+
if (corners < 3) {
|
|
20177
|
+
return;
|
|
20178
|
+
}
|
|
20179
|
+
if (!this.current) {
|
|
20180
|
+
this.useMaterial(null);
|
|
20181
|
+
}
|
|
20182
|
+
const target = this.current.corners;
|
|
20183
|
+
for (let corner = 1; corner + 1 < corners; corner += 1) {
|
|
20184
|
+
for (const which of [0, corner, corner + 1]) {
|
|
20185
|
+
target.push(this.face[which * 3]);
|
|
20186
|
+
target.push(this.face[which * 3 + 1]);
|
|
20187
|
+
target.push(this.face[which * 3 + 2]);
|
|
20188
|
+
}
|
|
20189
|
+
this.triangleCount += 1;
|
|
20190
|
+
}
|
|
20191
|
+
}
|
|
20192
|
+
resolve(index, count) {
|
|
20193
|
+
return index > 0 ? index - 1 : count + index;
|
|
20194
|
+
}
|
|
20195
|
+
}
|
|
20196
|
+
/**
|
|
20197
|
+
* Turns corners into shared vertices: one vertex per distinct position, texture and normal triple.
|
|
20198
|
+
*
|
|
20199
|
+
* Corners are grouped by position first, and the few corners that share a position are compared
|
|
20200
|
+
* with one another. That needs no table of every triple ever seen - which, over millions of
|
|
20201
|
+
* corners, would cost far more memory than the model itself.
|
|
20202
|
+
*/
|
|
20203
|
+
async function indexGeometry(parsed, breaks) {
|
|
20204
|
+
const positionCount = parsed.positions.length / 3;
|
|
20205
|
+
const groups = parsed.groups.map((group) => group.corners.view());
|
|
20206
|
+
const cornerCount = groups.reduce((sum, corners) => sum + corners.length / 3, 0);
|
|
20207
|
+
// Every corner, numbered across all groups, listed under its position.
|
|
20208
|
+
const perPosition = new Int32Array(positionCount + 1);
|
|
20209
|
+
groups.forEach((corners) => {
|
|
20210
|
+
for (let at = 0; at < corners.length; at += 3) {
|
|
20211
|
+
perPosition[corners[at] + 1] += 1;
|
|
20212
|
+
}
|
|
20213
|
+
});
|
|
20214
|
+
for (let position = 0; position < positionCount; position += 1) {
|
|
20215
|
+
perPosition[position + 1] += perPosition[position];
|
|
20216
|
+
}
|
|
20217
|
+
const filled = perPosition.slice(0, positionCount);
|
|
20218
|
+
const byPosition = new Int32Array(cornerCount);
|
|
20219
|
+
const cornerStart = [];
|
|
20220
|
+
let numbered = 0;
|
|
20221
|
+
groups.forEach((corners) => {
|
|
20222
|
+
cornerStart.push(numbered);
|
|
20223
|
+
for (let at = 0; at < corners.length; at += 3) {
|
|
20224
|
+
byPosition[filled[corners[at]]] = numbered;
|
|
20225
|
+
filled[corners[at]] += 1;
|
|
20226
|
+
numbered += 1;
|
|
20227
|
+
}
|
|
20228
|
+
});
|
|
20229
|
+
// Which group each corner is in, so its triple can be read back where it already is rather than
|
|
20230
|
+
// from a copy of every corner.
|
|
20231
|
+
const groupOf = new Uint16Array(cornerCount);
|
|
20232
|
+
groups.forEach((corners, group) => groupOf.fill(group, cornerStart[group], cornerStart[group] + corners.length / 3));
|
|
20233
|
+
const vertexOfCorner = new Uint32Array(cornerCount);
|
|
20234
|
+
const sources = new Growable(new Int32Array(Math.max(16, positionCount * 3 * 1.3) | 0));
|
|
20235
|
+
let vertexCount = 0;
|
|
20236
|
+
for (let position = 0; position < positionCount; position += 1) {
|
|
20237
|
+
if (breaks.due(position)) {
|
|
20238
|
+
await breaks.take(0.85 + 0.07 * (position / positionCount));
|
|
20239
|
+
}
|
|
20240
|
+
const first = vertexCount;
|
|
20241
|
+
for (let at = perPosition[position]; at < perPosition[position + 1]; at += 1) {
|
|
20242
|
+
const corner = byPosition[at];
|
|
20243
|
+
const group = groupOf[corner];
|
|
20244
|
+
const triple = (corner - cornerStart[group]) * 3;
|
|
20245
|
+
const uv = groups[group][triple + 1];
|
|
20246
|
+
const normal = groups[group][triple + 2];
|
|
20247
|
+
let found = -1;
|
|
20248
|
+
for (let vertex = first; vertex < vertexCount; vertex += 1) {
|
|
20249
|
+
if (sources.data[vertex * 3 + 1] === uv && sources.data[vertex * 3 + 2] === normal) {
|
|
20250
|
+
found = vertex;
|
|
20251
|
+
break;
|
|
20252
|
+
}
|
|
20253
|
+
}
|
|
20254
|
+
if (found < 0) {
|
|
20255
|
+
found = vertexCount;
|
|
20256
|
+
sources.push(position);
|
|
20257
|
+
sources.push(uv);
|
|
20258
|
+
sources.push(normal);
|
|
20259
|
+
vertexCount += 1;
|
|
20260
|
+
}
|
|
20261
|
+
vertexOfCorner[corner] = found;
|
|
20262
|
+
}
|
|
20263
|
+
}
|
|
20264
|
+
return {
|
|
20265
|
+
vertexCount,
|
|
20266
|
+
sources: sources.view(),
|
|
20267
|
+
indices: groups.map((corners, group) => vertexOfCorner.slice(cornerStart[group], cornerStart[group] + corners.length / 3)),
|
|
20268
|
+
};
|
|
20269
|
+
}
|
|
20270
|
+
/** The parts of an .mtl that matter here: the colour, the opacity and the colour texture. */
|
|
20271
|
+
function parseMtl(text) {
|
|
20272
|
+
const materials = [];
|
|
20273
|
+
let current = null;
|
|
20274
|
+
text.split(/\r?\n/).forEach((raw) => {
|
|
20275
|
+
const line = raw.trim();
|
|
20276
|
+
const [keyword, ...rest] = line.split(/\s+/);
|
|
20277
|
+
if (keyword === 'newmtl') {
|
|
20278
|
+
current = { name: rest.join(' '), colour: [1, 1, 1], opacity: 1, texture: null };
|
|
20279
|
+
materials.push(current);
|
|
20280
|
+
}
|
|
20281
|
+
else if (!current) {
|
|
20282
|
+
return;
|
|
20283
|
+
}
|
|
20284
|
+
else if (keyword === 'Kd' && rest.length >= 3) {
|
|
20285
|
+
current.colour = [Number(rest[0]), Number(rest[1]), Number(rest[2])];
|
|
20286
|
+
}
|
|
20287
|
+
else if (keyword === 'd' && rest.length >= 1) {
|
|
20288
|
+
current.opacity = Number(rest[0]);
|
|
20289
|
+
}
|
|
20290
|
+
else if (keyword === 'map_Kd' && rest.length >= 1) {
|
|
20291
|
+
// Options (-s 1 1 1, -o ..., -bm 0.5) come before the file name, which is what is left.
|
|
20292
|
+
current.texture = rest[rest.length - 1];
|
|
20293
|
+
}
|
|
20294
|
+
});
|
|
20295
|
+
return materials;
|
|
20296
|
+
}
|
|
20297
|
+
/**
|
|
20298
|
+
* Writes the .glb. The binary part is assembled from the arrays and the texture files as they are,
|
|
20299
|
+
* into a Blob - so the textures are never read into memory, nor the whole file built in one buffer.
|
|
20300
|
+
*/
|
|
20301
|
+
async function writeGlb(parsed, geometry, materials, files, missing, name, breaks) {
|
|
20302
|
+
const { vertexCount, sources } = geometry;
|
|
20303
|
+
const hasUv = parsed.uvs.length > 0 && hasAny(sources, 1);
|
|
20304
|
+
const hasNormals = parsed.normals.length > 0 && hasAny(sources, 2);
|
|
20305
|
+
const positions = new Float32Array(vertexCount * 3);
|
|
20306
|
+
const min = [Infinity, Infinity, Infinity];
|
|
20307
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
20308
|
+
const uvs = hasUv ? new Float32Array(vertexCount * 2) : null;
|
|
20309
|
+
const normals = hasNormals ? new Float32Array(vertexCount * 3) : null;
|
|
20310
|
+
for (let vertex = 0; vertex < vertexCount; vertex += 1) {
|
|
20311
|
+
if (breaks.due(vertex)) {
|
|
20312
|
+
await breaks.take(0.95 + 0.04 * (vertex / vertexCount));
|
|
20313
|
+
}
|
|
20314
|
+
const position = sources[vertex * 3];
|
|
20315
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
20316
|
+
const value = parsed.positions[position * 3 + axis];
|
|
20317
|
+
positions[vertex * 3 + axis] = value;
|
|
20318
|
+
min[axis] = Math.min(min[axis], Math.fround(value));
|
|
20319
|
+
max[axis] = Math.max(max[axis], Math.fround(value));
|
|
20320
|
+
}
|
|
20321
|
+
if (uvs) {
|
|
20322
|
+
const uv = sources[vertex * 3 + 1];
|
|
20323
|
+
// An .obj counts v up from the bottom of the image, glTF down from the top.
|
|
20324
|
+
uvs[vertex * 2] = uv >= 0 ? parsed.uvs[uv * 2] : 0;
|
|
20325
|
+
uvs[vertex * 2 + 1] = uv >= 0 ? 1 - parsed.uvs[uv * 2 + 1] : 0;
|
|
20326
|
+
}
|
|
20327
|
+
if (normals) {
|
|
20328
|
+
const normal = sources[vertex * 3 + 2];
|
|
20329
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
20330
|
+
normals[vertex * 3 + axis] = normal >= 0 ? parsed.normals[normal * 3 + axis] : 0;
|
|
20331
|
+
}
|
|
20332
|
+
}
|
|
20333
|
+
}
|
|
20334
|
+
const parts = [];
|
|
20335
|
+
const bufferViews = [];
|
|
20336
|
+
let byteLength = 0;
|
|
20337
|
+
const addView = (part, size, target) => {
|
|
20338
|
+
const padding = (4 - (byteLength % 4)) % 4;
|
|
20339
|
+
if (padding) {
|
|
20340
|
+
parts.push(new Uint8Array(padding));
|
|
20341
|
+
byteLength += padding;
|
|
20342
|
+
}
|
|
20343
|
+
parts.push(part);
|
|
20344
|
+
bufferViews.push({ buffer: 0, byteOffset: byteLength, byteLength: size, ...(target ? { target } : {}) });
|
|
20345
|
+
byteLength += size;
|
|
20346
|
+
return bufferViews.length - 1;
|
|
20347
|
+
};
|
|
20348
|
+
const accessors = [];
|
|
20349
|
+
const addAccessor = (data, type, count, extra = {}, target) => {
|
|
20350
|
+
const view = addView(data, data.byteLength, target);
|
|
20351
|
+
accessors.push({ bufferView: view, componentType: data instanceof Float32Array ? 5126 : 5125, count, type, ...extra });
|
|
20352
|
+
return accessors.length - 1;
|
|
20353
|
+
};
|
|
20354
|
+
const attributes = {
|
|
20355
|
+
POSITION: addAccessor(positions, 'VEC3', vertexCount, { min, max }, 34962),
|
|
20356
|
+
};
|
|
20357
|
+
if (normals) {
|
|
20358
|
+
attributes['NORMAL'] = addAccessor(normals, 'VEC3', vertexCount, {}, 34962);
|
|
20359
|
+
}
|
|
20360
|
+
if (uvs) {
|
|
20361
|
+
attributes['TEXCOORD_0'] = addAccessor(uvs, 'VEC2', vertexCount, {}, 34962);
|
|
20362
|
+
}
|
|
20363
|
+
// Materials, each texture file stored once however many materials use it.
|
|
20364
|
+
const images = [];
|
|
20365
|
+
const textures = [];
|
|
20366
|
+
const textureOf = new Map();
|
|
20367
|
+
const textureIndex = (fileName) => {
|
|
20368
|
+
const key = baseName(fileName).toLowerCase();
|
|
20369
|
+
if (textureOf.has(key)) {
|
|
20370
|
+
return textureOf.get(key);
|
|
20371
|
+
}
|
|
20372
|
+
const file = files.get(key);
|
|
20373
|
+
if (!file) {
|
|
20374
|
+
missing.add(baseName(fileName));
|
|
20375
|
+
return null;
|
|
20376
|
+
}
|
|
20377
|
+
const mimeType = /\.png$/i.test(file.name) ? 'image/png' : 'image/jpeg';
|
|
20378
|
+
images.push({ bufferView: addView(file, file.size), mimeType, name: baseName(file.name) });
|
|
20379
|
+
textures.push({ source: images.length - 1, sampler: 0 });
|
|
20380
|
+
textureOf.set(key, textures.length - 1);
|
|
20381
|
+
return textures.length - 1;
|
|
20382
|
+
};
|
|
20383
|
+
const gltfMaterials = [];
|
|
20384
|
+
const primitives = parsed.groups.map((group, index) => {
|
|
20385
|
+
const material = group.material !== null ? materials.get(group.material) : undefined;
|
|
20386
|
+
const texture = material?.texture ? textureIndex(material.texture) : null;
|
|
20387
|
+
gltfMaterials.push({
|
|
20388
|
+
name: group.material ?? 'default',
|
|
20389
|
+
pbrMetallicRoughness: {
|
|
20390
|
+
...(texture !== null ? { baseColorTexture: { index: texture } } : {}),
|
|
20391
|
+
baseColorFactor: texture !== null ? [1, 1, 1, material?.opacity ?? 1] : [...(material?.colour ?? [0.8, 0.8, 0.8]), material?.opacity ?? 1],
|
|
20392
|
+
metallicFactor: 0,
|
|
20393
|
+
roughnessFactor: 1,
|
|
20394
|
+
},
|
|
20395
|
+
...(material && material.opacity < 1 ? { alphaMode: 'BLEND' } : {}),
|
|
20396
|
+
});
|
|
20397
|
+
const triangles = geometry.indices[index];
|
|
20398
|
+
return {
|
|
20399
|
+
attributes,
|
|
20400
|
+
indices: addAccessor(triangles, 'SCALAR', triangles.length, {}, 34963),
|
|
20401
|
+
material: gltfMaterials.length - 1,
|
|
20402
|
+
};
|
|
20403
|
+
});
|
|
20404
|
+
const json = {
|
|
20405
|
+
asset: { version: '2.0', generator: 'SmarterPlan obj-to-glb' },
|
|
20406
|
+
scene: 0,
|
|
20407
|
+
scenes: [{ nodes: [0] }],
|
|
20408
|
+
nodes: [{ mesh: 0, name: name.replace(/\.obj$/i, '') }],
|
|
20409
|
+
meshes: [{ primitives }],
|
|
20410
|
+
materials: gltfMaterials,
|
|
20411
|
+
...(textures.length ? {
|
|
20412
|
+
textures,
|
|
20413
|
+
images,
|
|
20414
|
+
// Linear filtering with mipmaps, repeating: what a captured model's atlas expects.
|
|
20415
|
+
samplers: [{ magFilter: 9729, minFilter: 9987, wrapS: 10497, wrapT: 10497 }],
|
|
20416
|
+
} : {}),
|
|
20417
|
+
accessors,
|
|
20418
|
+
bufferViews,
|
|
20419
|
+
buffers: [{ byteLength }],
|
|
20420
|
+
};
|
|
20421
|
+
let jsonBytes = new TextEncoder().encode(JSON.stringify(json));
|
|
20422
|
+
const jsonPadding = (4 - (jsonBytes.length % 4)) % 4;
|
|
20423
|
+
if (jsonPadding) {
|
|
20424
|
+
const padded = new Uint8Array(jsonBytes.length + jsonPadding).fill(32);
|
|
20425
|
+
padded.set(jsonBytes);
|
|
20426
|
+
jsonBytes = padded;
|
|
20427
|
+
}
|
|
20428
|
+
const binPadding = (4 - (byteLength % 4)) % 4;
|
|
20429
|
+
const binLength = byteLength + binPadding;
|
|
20430
|
+
const header = new DataView(new ArrayBuffer(12 + 8));
|
|
20431
|
+
header.setUint32(0, 0x46546c67, true); // "glTF"
|
|
20432
|
+
header.setUint32(4, 2, true);
|
|
20433
|
+
header.setUint32(8, 12 + 8 + jsonBytes.length + 8 + binLength, true);
|
|
20434
|
+
header.setUint32(12, jsonBytes.length, true);
|
|
20435
|
+
header.setUint32(16, 0x4e4f534a, true); // "JSON"
|
|
20436
|
+
const binHeader = new DataView(new ArrayBuffer(8));
|
|
20437
|
+
binHeader.setUint32(0, binLength, true);
|
|
20438
|
+
binHeader.setUint32(4, 0x004e4942, true); // "BIN"
|
|
20439
|
+
return new Blob([header, jsonBytes, binHeader, ...parts, new Uint8Array(binPadding)]);
|
|
20440
|
+
}
|
|
20441
|
+
/** Whether any vertex uses the given slot of its source triple (texture or normal). */
|
|
20442
|
+
function hasAny(sources, slot) {
|
|
20443
|
+
for (let at = slot; at < sources.length; at += 3) {
|
|
20444
|
+
if (sources[at] >= 0) {
|
|
20445
|
+
return true;
|
|
20446
|
+
}
|
|
20447
|
+
}
|
|
20448
|
+
return false;
|
|
20449
|
+
}
|
|
20450
|
+
|
|
18445
20451
|
// matterport-transform.util.ts
|
|
18446
20452
|
/**
|
|
18447
20453
|
* Computes the rigid transform (Y-axis rotation + translation) between two
|
|
@@ -21416,5 +23422,5 @@ function floatValidator() {
|
|
|
21416
23422
|
* Generated bundle index. Do not edit.
|
|
21417
23423
|
*/
|
|
21418
23424
|
|
|
21419
|
-
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, SweepLocatorService, THREEJS_MODEL_UP_OPTIONS, TagAction, TemplateService, ThreeJsImportService, ThreeJsService, TicketPriority, TicketStatus, TicketType, TicketsService, TimeDateToLocalStringPipe, TypeNote, UsernameFromIdPipe, ValidatorsService, ViewerInteractions, ViewerService, VisitService, ZoneChangeService, ZoneService, applyRigidTransformToDirection, applyRigidTransformToPoint, applyRigidTransformToYRotationDeg, arraysContainSameElements, checkElementById, computeRigidTransform, convertTimestampToLocalZone, copyFileInS3, copyFileInS3ViaDownload, 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, getThreeJsPoiOrigin, getVisitUrl, isEmptyObject, listFilesInFolder, mean, noEmptyValidator, numberToDateString, openDocument, openModalForVisitSwitch, parseThreeJsPlacement, poiTypeToString, pointToString, removeAllFilesFromFolderS3, removeNullKeysFromObject, showScanPointsOnPlanInDiv, shuffleArray, solveThreeJsAlignment, sortAlphabeticallyOnName, stringLocaleToEnum, stringToLowercaseNoSpaces, styleButton, textValidator, translateDatePeriod, uploadBase64Image, uploadBase64ImageWithRetry, uploadFileToS3, uploadFileToS3WithProgress, uploadJsonToS3, validEmail, wait, waitUntil };
|
|
23425
|
+
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, GAUSSIAN_SPLAT_PLY_SUFFIX, 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, SplatViewerComponent, StatusEquipment, SupportModalComponent, SupportService, SweepLocatorService, THREEJS_MODEL_UP_OPTIONS, TagAction, TemplateService, ThreeJsImportService, ThreeJsService, TicketPriority, TicketStatus, TicketType, TicketsService, TimeDateToLocalStringPipe, TypeNote, UsernameFromIdPipe, VIEWER_BACKGROUND, ValidatorsService, ViewerInteractions, ViewerService, VisitService, ZoneChangeService, ZoneService, applyRigidTransformToDirection, applyRigidTransformToPoint, applyRigidTransformToYRotationDeg, arraysContainSameElements, checkElementById, computeRigidTransform, convertObjToGlb, convertTimestampToLocalZone, copyFileInS3, copyFileInS3ViaDownload, 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, getThreeJsPoiOrigin, getVisitUrl, hasObjModel, isEmptyObject, isGaussianSplatFile, isGaussianSplatPly, isStreamedSplatFile, listFilesInFolder, mean, noEmptyValidator, numberToDateString, openDocument, openModalForVisitSwitch, parseThreeJsPlacement, poiTypeToString, pointToString, readObj, removeAllFilesFromFolderS3, removeNullKeysFromObject, showScanPointsOnPlanInDiv, shuffleArray, solveThreeJsAlignment, sortAlphabeticallyOnName, stringLocaleToEnum, stringToLowercaseNoSpaces, styleButton, textValidator, translateDatePeriod, uploadBase64Image, uploadBase64ImageWithRetry, uploadFileToS3, uploadFileToS3WithProgress, uploadJsonToS3, validEmail, wait, waitUntil };
|
|
21420
23426
|
//# sourceMappingURL=smarterplan-ngx-smarterplan-core.mjs.map
|