diff --git a/frontend/assets/icons/viewer/align.svg.tsx b/frontend/assets/icons/viewer/align.svg.tsx
new file mode 100644
index 00000000000..a06f9c8d96b
--- /dev/null
+++ b/frontend/assets/icons/viewer/align.svg.tsx
@@ -0,0 +1,28 @@
+/**
+ * Copyright (C) 2023 3D Repo Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+type IProps = {
+ className?: any;
+};
+
+export default ({ className }: IProps) => (
+);
diff --git a/frontend/assets/icons/viewer/clip_selection.svg.tsx b/frontend/assets/icons/viewer/clip_selection.svg.tsx
new file mode 100644
index 00000000000..fb7f7757be4
--- /dev/null
+++ b/frontend/assets/icons/viewer/clip_selection.svg.tsx
@@ -0,0 +1,28 @@
+/**
+ * Copyright (C) 2023 3D Repo Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+type IProps = {
+ className?: any;
+};
+
+export default ({ className }: IProps) => (
+);
\ No newline at end of file
diff --git a/frontend/assets/icons/viewer/delete.svg.tsx b/frontend/assets/icons/viewer/delete.svg.tsx
new file mode 100644
index 00000000000..7f54cc54edf
--- /dev/null
+++ b/frontend/assets/icons/viewer/delete.svg.tsx
@@ -0,0 +1,27 @@
+/**
+ * Copyright (C) 2023 3D Repo Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+type IProps = {
+ className?: any;
+};
+
+export default ({ className }: IProps) => (
+
+);
\ No newline at end of file
diff --git a/frontend/assets/icons/viewer/flip_plane.svg.tsx b/frontend/assets/icons/viewer/flip_plane.svg.tsx
new file mode 100644
index 00000000000..b75cbd3297f
--- /dev/null
+++ b/frontend/assets/icons/viewer/flip_plane.svg.tsx
@@ -0,0 +1,36 @@
+/**
+ * Copyright (C) 2023 3D Repo Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+type IProps = {
+ className?: any;
+};
+
+export default ({ className }: IProps) => (
+);
\ No newline at end of file
diff --git a/frontend/assets/icons/viewer/gizmo_rotate.svg.tsx b/frontend/assets/icons/viewer/gizmo_rotate.svg.tsx
new file mode 100644
index 00000000000..ebce1d76be6
--- /dev/null
+++ b/frontend/assets/icons/viewer/gizmo_rotate.svg.tsx
@@ -0,0 +1,29 @@
+/**
+ * Copyright (C) 2023 3D Repo Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+type IProps = {
+ className?: any;
+};
+
+export default ({ className }: IProps) => (
+
+);
\ No newline at end of file
diff --git a/frontend/assets/icons/viewer/gizmo_scale.svg.tsx b/frontend/assets/icons/viewer/gizmo_scale.svg.tsx
new file mode 100644
index 00000000000..2f52cff49e8
--- /dev/null
+++ b/frontend/assets/icons/viewer/gizmo_scale.svg.tsx
@@ -0,0 +1,29 @@
+/**
+ * Copyright (C) 2023 3D Repo Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+type IProps = {
+ className?: any;
+};
+
+export default ({ className }: IProps) => (
+
+);
diff --git a/frontend/assets/icons/viewer/gizmo_translate.svg.tsx b/frontend/assets/icons/viewer/gizmo_translate.svg.tsx
new file mode 100644
index 00000000000..669f3226aa6
--- /dev/null
+++ b/frontend/assets/icons/viewer/gizmo_translate.svg.tsx
@@ -0,0 +1,29 @@
+/**
+ * Copyright (C) 2023 3D Repo Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+type IProps = {
+ className?: any;
+};
+
+export default ({ className }: IProps) => (
+
+);
diff --git a/frontend/assets/icons/viewer/viewerIconContainer.styles.ts b/frontend/assets/icons/viewer/viewerIconContainer.styles.ts
index f994159bd36..6fad5055310 100644
--- a/frontend/assets/icons/viewer/viewerIconContainer.styles.ts
+++ b/frontend/assets/icons/viewer/viewerIconContainer.styles.ts
@@ -29,7 +29,7 @@ const selectedStyles = css`
const disabledStyles = css`
.primary, .highlight {
- color: ${({ theme }) => theme.palette.base.lightest};
+ color: ${({ theme }) => theme.palette.base.main};
}
`;
diff --git a/frontend/src/globals/unity-util.ts b/frontend/src/globals/unity-util.ts
index 5778e620958..dc1f7849ee8 100644
--- a/frontend/src/globals/unity-util.ts
+++ b/frontend/src/globals/unity-util.ts
@@ -483,9 +483,9 @@ export class UnityUtil {
}
/** @hidden */
- public static clipUpdated(nPlanes) {
- if (UnityUtil.viewer && UnityUtil.viewer.numClipPlanesUpdated) {
- UnityUtil.viewer.numClipPlanesUpdated(nPlanes);
+ public static clipUpdated() {
+ if (UnityUtil.viewer && UnityUtil.viewer.clipUpdated) {
+ UnityUtil.viewer.clipUpdated();
}
}
@@ -639,6 +639,7 @@ export class UnityUtil {
* Tells the viewer the maximum amount of memory it can expect to be able
* to allocate for its heap. 0 means the maximum amount that the browser
* can handle, determined by hueristics in this method.
+ * @category Streaming
*/
public static setUnityMemory(maxMemoryInMb: number) {
let memory = Number(maxMemoryInMb);
@@ -664,7 +665,7 @@ export class UnityUtil {
}
/**
- * Move the pivot point to eh centre of the objects provided
+ * Move the pivot point to the centre of the objects provided
* @category Navigations
* @param meshIDs - array of json objects each recording { model: , meshID: [array of mesh IDs] }
*/
@@ -829,6 +830,19 @@ export class UnityUtil {
UnityUtil.toUnity('DiffToolRenderTransAsDefault', undefined, undefined);
}
+ // The following methods are concerned with the clip tool. There are three
+ // types of method.
+ // - The start/stop methods turn the tool on and off and change the mode.
+ // - The clipTool.. methods should be hooked up the equivalent buttons on
+ // the toolbar.
+ // - The set/scale/enable methods adjust the visuals and behaviour for fine
+ // tuning; they are not intended to be exposed in the UI, and are not saved.
+
+ // Some of the toolbar buttons will be stateful, such as clipToolRealign.
+ // Use the `clipUpdated` callback to detect when the planes have been placed
+ // in this mode (do not use clipBroadcast - that is only called after
+ // setting the planes from the frontend).
+
/**
* Start clip editing in single plane mode
* @category Clipping Plane
@@ -861,6 +875,117 @@ export class UnityUtil {
UnityUtil.toUnity('StopClipEdit', undefined, undefined);
}
+ /**
+ * Move the clipping planes to the current selection (highlighted objects).
+ * In single plane mode this moves along the normal so that the selected
+ * object(s) are fully visible.
+ * In six plane mode, the planes move to encompass the selection exactly,
+ * retaining their orientation.
+ * @category Clipping Plane
+ */
+ public static clipToolClipToSelection() {
+ UnityUtil.toUnity('ClipToolClipToSelection', UnityUtil.LoadingState.VIEWER_READY, undefined);
+ }
+
+ /**
+ * Puts the clip tool in Realign mode. In this mode the plane (or front of
+ * the box in six plane mode) will snap to the position and orientation of
+ * the first surface to be clicked. If the background is clicked, the planes
+ * or plane will reset to the scene bounding box.
+ * @category Clipping Plane
+ */
+ public static clipToolRealign() {
+ UnityUtil.toUnity('ClipToolRealign', UnityUtil.LoadingState.VIEWER_READY, undefined);
+ }
+
+ /**
+ * Flip the clip plane or box
+ * @category Clipping Plane
+ */
+ public static clipToolFlip() {
+ UnityUtil.toUnity('ClipToolFlip', UnityUtil.LoadingState.VIEWER_READY, undefined);
+ }
+
+ /**
+ * Hide all the clip planes, and reset them to the scene bounding box
+ * @category Clipping Plane
+ */
+ public static clipToolDelete() {
+ UnityUtil.toUnity('ClipToolDelete', UnityUtil.LoadingState.VIEWER_READY, undefined);
+ }
+
+ /**
+ * Puts the Clip Planes Gizmo into Translate mode.
+ * @category Clipping Plane
+ */
+ public static clipToolTranslate() {
+ UnityUtil.toUnity('ClipToolTranslate', UnityUtil.LoadingState.VIEWER_READY, undefined);
+ }
+
+ /**
+ * Puts the Clip Planes Gizmo in Rotate mode.
+ * @category Clipping Plane
+ */
+ public static clipToolRotate() {
+ UnityUtil.toUnity('ClipToolRotate', UnityUtil.LoadingState.VIEWER_READY, undefined);
+ }
+
+ /**
+ * Puts the Clip Planes Gizmo in Scale mode.
+ * @category Clipping Plane
+ */
+ public static clipToolScale() {
+ UnityUtil.toUnity('ClipToolScale', UnityUtil.LoadingState.VIEWER_READY, undefined);
+ }
+
+ /**
+ * Enables the inbuilt Clip Tool Toolbar. The toolbar is always disabled by
+ * default. This may need to be called on embedded viewers that don't have
+ * the full frontend UX.
+ * @category Clipping Plane
+ */
+ public static enableClipToolbar() {
+ UnityUtil.toUnity('EnableClipToolToolbar', UnityUtil.LoadingState.VIEWER_READY, undefined);
+ }
+
+ /**
+ * Disables the inbuilt Clip Tool Toolbar, if it was previously enabled via
+ * enableClipToolbar.
+ * @category Clipping Plane
+ */
+ public static disableClipToolbar() {
+ UnityUtil.toUnity('DisableClipToolToolbar', UnityUtil.LoadingState.VIEWER_READY, undefined);
+ }
+
+ /**
+ * Increase or decrease the Gizmo Size on screen by the amount provided (in percent)
+ * @category Clipping Plane
+ */
+ public static scaleClipGizmo(percent: number) {
+ UnityUtil.toUnity('ScaleClipGizmo', UnityUtil.LoadingState.VIEWER_READY, percent);
+ }
+
+ /**
+ * Set the coefficient linking the change in Clip Box Size to proportion
+ * of the screen covered by the cursor when scaling in all three axes.
+ * @category Clipping Plane
+ */
+ public static setClipScaleSpeed(speed: number) {
+ UnityUtil.toUnity('SetClipScaleSpeed', UnityUtil.LoadingState.VIEWER_READY, speed);
+ }
+
+ /**
+ * Sets the coefficient that defines how much the clip box should be
+ * oversized when using the clip to selection function. The box is
+ * oversized to prevent the clip border being immediately visible.
+ * Setting this to 1 sets it to the default. Above 1 makes it larger
+ * and below 1 makes it smaller. 0 disables the feature.
+ * @category Clipping Plane
+ */
+ public static setClipSelectionBoxScalar(scalar: number) {
+ UnityUtil.toUnity('SetClipSelectionBoxScalar', UnityUtil.LoadingState.VIEWER_READY, scalar);
+ }
+
/**
* Disable the Measuring tool.
* @category Measuring tool
@@ -931,6 +1056,7 @@ export class UnityUtil {
/**
* Remove a particular measurement.
* @param uuid - The measurement id of the measurement to be removed
+ * @category Measuring tool
*/
public static clearMeasureToolMeasurement(uuid) {
UnityUtil.toUnity('ClearMeasureToolMeasurement', undefined, uuid);
@@ -939,6 +1065,7 @@ export class UnityUtil {
/**
* Set color of a particular measurement.
* @param uuid - The measurement id of the measurement that will change color
+ * @category Measuring tool
*/
public static setMeasureToolMeasurementColor(uuid, color) {
UnityUtil.toUnity('SetMeasureToolMeasurementColor', undefined, JSON.stringify({ uuid, color }));
@@ -947,6 +1074,7 @@ export class UnityUtil {
/**
* Set color of a particular measurement.
* @param uuid - The measurement id of the measurement that will change name
+ * @category Measuring tool
*/
public static setMeasureToolMeasurementName(uuid, name) {
UnityUtil.toUnity('SetMeasureToolMeasurementName', undefined, JSON.stringify({ uuid, name }));
@@ -961,41 +1089,75 @@ export class UnityUtil {
}
/**
- * Disnable measure display mode to xyz.
+ * Disable measure display mode to xyz.
* @category Measuring tool
*/
public static disableMeasureToolXYZDisplay() {
UnityUtil.toUnity('DisableMeasureToolXYZDisplay');
}
+ /**
+ * Add a specific measurment to the scene.
+ * @category Measuring tool
+ */
public static addMeasurement(measurement) {
UnityUtil.toUnity('AddMeasurement', UnityUtil.LoadingState.MODEL_LOADING, JSON.stringify(measurement));
}
+ /**
+ * Newly created measurements do not have labels.
+ * @category Measuring tool
+ */
public static hideNewMeasurementsLabels() {
UnityUtil.toUnity('HideNewMeasurementsLabels');
}
+ /**
+ * Newly created measurements have labels visible
+ * @category Measuring tool
+ */
public static showNewMeasurementsLabels() {
UnityUtil.toUnity('ShowNewMeasurementsLabels');
}
+ /**
+ * Select a measurement
+ * @category Measuring tool
+ */
public static selectMeasurement(id: string) {
UnityUtil.toUnity('SelectMeasurement', UnityUtil.LoadingState.MODEL_LOADING, id);
}
+ /**
+ * Deselect a measurement
+ * @category Measuring tool
+ */
public static deselectMeasurement(id: string) {
UnityUtil.toUnity('DeselectMeasurement', UnityUtil.LoadingState.MODEL_LOADING, id);
}
+ /**
+ * Sets the priority of a model namespace (making it higher or lower than
+ * the others). This can be used for example to ensure a model always loads
+ * fully, at the expense of others.
+ * @category Streaming
+ */
public static setStreamingModelPriority(modelNamespace: string, priority: number) {
UnityUtil.toUnity('SetStreamingModelPriority', UnityUtil.LoadingState.VIEWER_READY, JSON.stringify({ modelNamespace, priority }));
}
+ /**
+ * Adjusts the correction factor applied to mesh size estimates to account for Unity overheads.
+ * @category Streaming
+ */
public static setStreamingMeshFactor(factor: number) {
UnityUtil.toUnity('SetStreamingMeshFactor', UnityUtil.LoadingState.VIEWER_READY, Number(factor));
}
+ /**
+ * Adjusts how the visibilty of a Supermesh affects its priority when deciding whether or not to load it.
+ * @category Streaming
+ */
public static setStreamingFovWeight(weight: number) {
UnityUtil.toUnity('SetStreamingFovWeight', UnityUtil.LoadingState.VIEWER_READY, Number(weight));
}
@@ -1174,10 +1336,18 @@ export class UnityUtil {
UnityUtil.toUnity('DropTicketPin', UnityUtil.LoadingState.MODEL_LOADING, JSON.stringify(params));
}
+ /**
+ * Select a Pin by Id
+ * @category Pins
+ */
public static selectPin(id: string) {
UnityUtil.toUnity('SelectPin', UnityUtil.LoadingState.MODEL_LOADING, id);
}
+ /**
+ * Deselect a selected Pin by Id
+ * @category Pins
+ */
public static deselectPin(id: string) {
UnityUtil.toUnity('DeselectPin', UnityUtil.LoadingState.MODEL_LOADING, id);
}
@@ -1363,10 +1533,18 @@ export class UnityUtil {
});
}
+ /**
+ * Enables the Pause Rendering feature of the viewer - when the camera is not moving, the main scene will not be redrawn.
+ * @category Configurations
+ */
public static pauseRendering() {
UnityUtil.toUnity('PauseRendering', UnityUtil.LoadingState.VIEWER_READY, undefined);
}
+ /**
+ * Disables the Pause Rendering feature of the viewer - the entire scene will render every frame regardless of camera behaviour.
+ * @category Configurations
+ */
public static resumeRendering() {
UnityUtil.toUnity('ResumeRendering', UnityUtil.LoadingState.VIEWER_READY, undefined);
}
@@ -1759,6 +1937,7 @@ export class UnityUtil {
/**
* Use orthographic view
+ * @category Configurations
*/
public static useOrthographicProjection() {
UnityUtil.toUnity('UseOrthographicProjection', UnityUtil.LoadingState.MODEL_LOADING, undefined);
@@ -1766,6 +1945,7 @@ export class UnityUtil {
/**
* Use perspective view
+ * @category Configurations
*/
public static usePerspectiveProjection() {
UnityUtil.toUnity('UsePerspectiveProjection', UnityUtil.LoadingState.MODEL_LOADING, undefined);
@@ -1822,6 +2002,7 @@ export class UnityUtil {
/**
* Show progress bar while model is loading
+ * @category Configurations
*/
public static showProgressBar() {
UnityUtil.toUnity('ShowProgressBar', UnityUtil.LoadingState.VIEWER_READY, undefined);
@@ -1829,6 +2010,7 @@ export class UnityUtil {
/**
* Hide progress bar while model is loading
+ * @category Configurations
*/
public static hideProgressBar() {
UnityUtil.toUnity('HideProgressBar', UnityUtil.LoadingState.VIEWER_READY, undefined);
@@ -1927,12 +2109,22 @@ export class UnityUtil {
* @param account - name of teamspace
* @param model - name of model
*/
- public static updateClippingPlanes(clipPlane: object, requireBroadcast: boolean, account?: string, model?: string) {
+ public static updateClippingPlanes(clipPlane: any, requireBroadcast: boolean, account?: string, model?: string) {
const param: any = {};
param.clip = clipPlane;
if (account && model) {
param.nameSpace = `${account}.${model}`;
}
+ if (!clipPlane?.length) {
+ UnityUtil.viewer.stopClipEdit();
+ UnityUtil.viewer.setClipMode(null);
+ }
+ if (clipPlane?.length === 1) {
+ UnityUtil.viewer.setClipMode('SINGLE');
+ }
+ if (clipPlane?.length === 6) {
+ UnityUtil.viewer.setClipMode('BOX');
+ }
param.requiresBroadcast = requireBroadcast;
UnityUtil.toUnity('UpdateClip', UnityUtil.LoadingState.MODEL_LOADED, JSON.stringify(param));
}
@@ -1944,6 +2136,7 @@ export class UnityUtil {
*/
public static disableClippingPlanes() {
UnityUtil.toUnity('DisableClip', undefined, undefined);
+ UnityUtil.viewer.stopClipEdit();
}
/**
@@ -1954,6 +2147,10 @@ export class UnityUtil {
UnityUtil.toUnity('ZoomToHighlightedMeshes', UnityUtil.LoadingState.MODEL_LOADING, undefined);
}
+ /**
+ * Zoom to a set of objects specified by their Ids
+ * @category Configurations
+ */
public static zoomToObjects(meshEntries: object[]) {
UnityUtil.toUnity('ZoomToObjects', UnityUtil.LoadingState.MODEL_LOADED, JSON.stringify(meshEntries));
}
@@ -2059,13 +2256,17 @@ export class UnityUtil {
}
/** How many non-trivial jobs the viewer can complete per frame when the
- * camera isnt moving */
+ * camera isnt moving
+ * @category Configurations
+ * */
public static setJobsPerFrameStatic(numJobs: number) {
UnityUtil.toUnity('SetNumStaticJobs', UnityUtil.LoadingState.VIEWER_READY, numJobs);
}
/** How many non-trivial jobs the viewer can complete per frame when the
- * camera is moving */
+ * camera is moving
+ * @category Configurations
+ * */
public static setJobsPerFrameDynamic(numJobs: number) {
UnityUtil.toUnity('SetNumDynamicJobs', UnityUtil.LoadingState.VIEWER_READY, numJobs);
}
@@ -2111,6 +2312,7 @@ export class UnityUtil {
/**
* Creates an IndexedDbCache object which provides access to cached web requests using IndexedDb.
* @param unityInstance
+ * @category Configurations
*/
public static createIndexedDbCache(gameObjectName: string) {
this.unityInstance.repoDbCache = new IndexedDbCache(this.unityInstance, gameObjectName, this.unityDomain); // IndexedDbCache expects to find the worker at in [unityDomain]/unity/indexeddbworker.js
diff --git a/frontend/src/v4/constants/viewer.ts b/frontend/src/v4/constants/viewer.ts
index 3cd38f268a4..aeda4d1c500 100644
--- a/frontend/src/v4/constants/viewer.ts
+++ b/frontend/src/v4/constants/viewer.ts
@@ -25,6 +25,12 @@ export const VIEWER_CLIP_MODES = {
BOX: 'BOX'
} as const;
+export const VIEWER_GIZMO_MODES = {
+ TRANSLATE: 'TRANSLATE',
+ ROTATE: 'ROTATE',
+ SCALE: 'SCALE'
+} as const;
+
export const VIEWER_MAP_SOURCES = {
OSM: 'OSM',
HERE: 'HERE',
@@ -35,10 +41,13 @@ export const VIEWER_MAP_SOURCES = {
export const VIEWER_EVENTS = {
ADD_PIN: 'VIEWER_ADD_PIN',
- BACKGROUND_SELECTED: 'VIEWER_BACKGROUND_SELECTED',
+ ALL_MEASUREMENTS_REMOVED: 'ALL_MEASUREMENTS_REMOVED',
BACKGROUND_SELECTED_PIN_MODE: 'BACKGROUND_SELECTED_PIN_MODE',
+ BACKGROUND_SELECTED: 'VIEWER_BACKGROUND_SELECTED',
BBOX_READY: 'BBOX_READY',
+ CAMERA_PROJECTION_SET: 'CAMERA_PROJECTION_SET',
CLEAR_CLIPPING_PLANES: 'VIEWER_CLEAR_CLIPPING_PLANES',
+ CLEAR_HIGHLIGHT_OBJECTS: 'VIEWER_CLEAR_HIGHLIGHT_OBJECTS',
CLICK_PIN: 'VIEWER_CLICK_PIN',
CLIPPING_PLANE_BROADCAST: 'VIEWER_CLIPPING_PLANE_BROADCAST',
CLIPPING_PLANE_READY: 'VIEWER_CLIPPING_PLANE_READY',
@@ -48,17 +57,23 @@ export const VIEWER_EVENTS = {
GET_SCREENSHOT: 'VIEWER_GET_SCREENSHOT',
GO_HOME: 'VIEWER_GO_HOME',
HIGHLIGHT_OBJECTS: 'VIEWER_HIGHLIGHT_OBJECTS',
- UNHIGHLIGHT_OBJECTS: 'VIEWER_UNHIGHLIGHT_OBJECTS',
- CLEAR_HIGHLIGHT_OBJECTS: 'VIEWER_CLEAR_HIGHLIGHT_OBJECTS',
INITIALISE: 'VIEWER_EVENT_INITIALISE',
- LOADED: 'VIEWER_EVENT_LOADED',
LOAD_MODEL: 'VIEWER_LOAD_MODEL',
+ LOADED: 'VIEWER_EVENT_LOADED',
LOGO_CLICK: 'VIEWER_LOGO_CLICK',
MEASURE_MODE_CLICK_POINT: 'VIEWER_MEASURE_MODE_CLICK_POINT',
+ MEASUREMENT_CREATED: 'MEASUREMENT_CREATED',
+ MEASUREMENT_MODE_CHANGED: 'MEASUREMENT_MODE_CHANGED',
+ MEASUREMENT_REMOVED: 'MEASUREMENT_REMOVED',
+ MODEL_LOADED: 'MODEL_LOADED',
+ MODEL_LOADING_CANCEL: 'MODEL_LOADING_CANCEL',
+ MODEL_LOADING_PROGRESS: 'MODEL_LOADING_PROGRESS',
+ MODEL_LOADING_START: 'MODEL_LOADING_START',
MOVE_PIN: 'VIEWER_MOVE_PIN',
MOVE_POINT: 'VIEWER_MOVE_POINT',
- OBJECT_SELECTED: 'VIEWER_OBJECT_SELECTED',
MULTI_OBJECTS_SELECTED: 'VIEWER_MULTI_OBJECTS_SELECTED',
+ NAV_MODE_CHANGED: 'NAV_MODE_CHANGED',
+ OBJECT_SELECTED: 'VIEWER_OBJECT_SELECTED',
PICK_POINT: 'VIEWER_PICK_POINT',
REGISTER_MOUSE_MOVE_CALLBACK: 'VIEWER_REGISTER_MOUSE_MOVE_CALLBACK',
REGISTER_VIEWPOINT_CALLBACK: 'VIEWER_REGISTER_VIEWPOINT_CALLBACK',
@@ -70,26 +85,19 @@ export const VIEWER_EVENTS = {
START_LOADING: 'VIEWING_START_LOADING',
SWITCH_FULLSCREEN: 'VIEWER_SWITCH_FULLSCREEN',
SWITCH_OBJECT_VISIBILITY: 'VIEWER_SWITCH_OBJECT_VISIBILITY',
+ TOGGLE_PANEL: 'VIEWER_TOGGLE_PANEL',
+ UNHIGHLIGHT_OBJECTS: 'VIEWER_UNHIGHLIGHT_OBJECTS',
UNITY_ERROR: 'VIEWER_EVENT_UNITY_ERROR',
UNITY_READY: 'VIEWER_EVENT_UNITY_READY',
+ UPDATE_CLIP: 'VIEWER_UPDATE_CLIP',
+ UPDATE_CLIP_EDIT: 'VIEWER_SET_CLIP_EDIT',
+ UPDATE_CLIP_MODE: 'VIEWER_UPDATE_CLIP_MODE',
UPDATE_CLIPPING_PLANES: 'VIEWER_UPDATE_CLIPPING_PLANE',
- VR_READY: 'VIEWER_EVENT_VR_READY',
- NAV_MODE_CHANGED: 'NAV_MODE_CHANGED',
- UPDATE_NUM_CLIP: 'VIEWER_UPDATE_NUM_CLIP',
- TOGGLE_PANEL: 'VIEWER_TOGGLE_PANEL',
- VIEWER_INIT: 'VIEWER_INIT',
- VIEWER_INIT_PROGRESS: 'VIEWER_INIT_PROGRESS',
VIEWER_INIT_FAILED: 'VIEWER_INIT_FAILED',
+ VIEWER_INIT_PROGRESS: 'VIEWER_INIT_PROGRESS',
VIEWER_INIT_SUCCESS: 'VIEWER_INIT_SUCCESS',
- MODEL_LOADING_START: 'MODEL_LOADING_START',
- MODEL_LOADING_PROGRESS: 'MODEL_LOADING_PROGRESS',
- MODEL_LOADING_CANCEL: 'MODEL_LOADING_CANCEL',
- MODEL_LOADED: 'MODEL_LOADED',
- MEASUREMENT_REMOVED: 'MEASUREMENT_REMOVED',
- ALL_MEASUREMENTS_REMOVED: 'ALL_MEASUREMENTS_REMOVED',
- MEASUREMENT_CREATED: 'MEASUREMENT_CREATED',
- MEASUREMENT_MODE_CHANGED: 'MEASUREMENT_MODE_CHANGED',
- CAMERA_PROJECTION_SET: 'CAMERA_PROJECTION_SET'
+ VIEWER_INIT: 'VIEWER_INIT',
+ VR_READY: 'VIEWER_EVENT_VR_READY',
};
export const VIEWER_ERRORS = {
diff --git a/frontend/src/v4/modules/viewerGui/viewerGui.redux.ts b/frontend/src/v4/modules/viewerGui/viewerGui.redux.ts
index 20b9d063da2..10cec46628a 100644
--- a/frontend/src/v4/modules/viewerGui/viewerGui.redux.ts
+++ b/frontend/src/v4/modules/viewerGui/viewerGui.redux.ts
@@ -17,7 +17,8 @@
import { cloneDeep } from 'lodash';
import { createActions, createReducer } from 'reduxsauce';
-import { INITIAL_HELICOPTER_SPEED, VIEWER_NAV_MODES } from '../../constants/viewer';
+import { ClipMode, GizmoMode } from '@/v5/ui/routes/viewer/toolbar/toolbar.types';
+import { INITIAL_HELICOPTER_SPEED, VIEWER_GIZMO_MODES, VIEWER_NAV_MODES } from '../../constants/viewer';
import { getViewerLeftPanels, VIEWER_DRAGGABLE_PANELS, VIEWER_RIGHT_PANELS } from '../../constants/viewerGui';
export const { Types: ViewerGuiTypes, Creators: ViewerGuiActions } = createActions({
@@ -44,11 +45,11 @@ export const { Types: ViewerGuiTypes, Creators: ViewerGuiActions } = createActio
decreaseHelicopterSpeed: ['teamspace', 'modelId'],
setIsFocusMode: ['isFocusMode'],
setClippingMode: ['mode'],
- setClippingModeSuccess: ['mode'],
+ setClipModeSuccess: ['mode'],
setClipEdit: ['isClipEdit'],
setClipEditSuccess: ['isClipEdit'],
- setClipNumber: ['clipNumber'],
- updateClipState: ['clipNumber'],
+ setGizmoMode: ['mode'],
+ setGizmoModeSuccess: ['mode'],
setIsPinDropMode: ['mode'],
setIsPinDropModeSuccess: ['isPinDropMode'],
setPinData: ['pinData'],
@@ -71,11 +72,11 @@ export interface IViewerGuiState {
coordViewActive: boolean;
isModelLoaded: boolean;
navigationMode: string;
- clippingMode: string;
+ clippingMode: ClipMode;
+ gizmoMode: GizmoMode;
helicopterSpeed: number;
isFocusMode: boolean;
isClipEdit: boolean;
- clipNumber: number;
isPinDropMode: boolean;
pinData: any;
}
@@ -89,10 +90,10 @@ export const INITIAL_STATE: IViewerGuiState = {
coordViewActive: false,
navigationMode: VIEWER_NAV_MODES.TURNTABLE,
clippingMode: null,
+ gizmoMode: VIEWER_GIZMO_MODES.TRANSLATE,
helicopterSpeed: INITIAL_HELICOPTER_SPEED,
isFocusMode: false,
isClipEdit: false,
- clipNumber: 0,
isPinDropMode: false,
pinData: null,
};
@@ -149,8 +150,12 @@ const setNavigationModeSuccess = (state = INITIAL_STATE, { mode }) => {
return { ...state, navigationMode: mode };
};
-const setClippingModeSuccess = (state = INITIAL_STATE, { mode }) => {
- return { ...state, clippingMode: mode, isClipEdit: true };
+const setClipModeSuccess = (state = INITIAL_STATE, { mode }) => {
+ return { ...state, clippingMode: mode };
+};
+
+const setGizmoModeSuccess = (state = INITIAL_STATE, { mode }) => {
+ return { ...state, gizmoMode: mode };
};
const setHelicopterSpeed = (state = INITIAL_STATE, { speed }) => {
@@ -169,10 +174,6 @@ const setClipEditSuccess = (state = INITIAL_STATE, { isClipEdit }) => {
return { ...state, isClipEdit };
};
-const setClipNumber = (state = INITIAL_STATE, { clipNumber }) => {
- return { ...state, clipNumber };
-};
-
const setIsPinDropModeSuccess = (state = INITIAL_STATE, { isPinDropMode }) => {
return { ...state, isPinDropMode };
};
@@ -197,11 +198,11 @@ export const reducer = createReducer(INITIAL_STATE, {
[ViewerGuiTypes.SET_IS_MODEL_LOADED] : setIsModelLoaded,
[ViewerGuiTypes.SET_NAVIGATION_MODE_SUCCESS] : setNavigationModeSuccess,
[ViewerGuiTypes.SET_PROJECTION_MODE_SUCCESS] : setProjectionModeSuccess,
- [ViewerGuiTypes.SET_CLIPPING_MODE_SUCCESS] : setClippingModeSuccess,
+ [ViewerGuiTypes.SET_CLIP_MODE_SUCCESS] : setClipModeSuccess,
+ [ViewerGuiTypes.SET_GIZMO_MODE_SUCCESS] : setGizmoModeSuccess,
[ViewerGuiTypes.SET_HELICOPTER_SPEED] : setHelicopterSpeed,
[ViewerGuiTypes.SET_IS_FOCUS_MODE] : setIsFocusMode,
[ViewerGuiTypes.SET_CLIP_EDIT_SUCCESS] : setClipEditSuccess,
- [ViewerGuiTypes.SET_CLIP_NUMBER] : setClipNumber,
[ViewerGuiTypes.SET_COORD_VIEW_SUCCESS] : setCoordViewSuccess,
[ViewerGuiTypes.SET_IS_PIN_DROP_MODE_SUCCESS]: setIsPinDropModeSuccess,
[ViewerGuiTypes.SET_PIN_DATA]: setPinData,
diff --git a/frontend/src/v4/modules/viewerGui/viewerGui.sagas.ts b/frontend/src/v4/modules/viewerGui/viewerGui.sagas.ts
index d73823471f7..0ff85ae438d 100644
--- a/frontend/src/v4/modules/viewerGui/viewerGui.sagas.ts
+++ b/frontend/src/v4/modules/viewerGui/viewerGui.sagas.ts
@@ -17,13 +17,11 @@
import { formatMessage } from '@/v5/services/intl';
import { DialogsActions } from '@/v5/store/dialogs/dialogs.redux';
-import { goBack, push } from 'connected-react-router';
-import { matchPath } from 'react-router';
+import { goBack } from 'connected-react-router';
import { all, put, select, take, takeLatest } from 'redux-saga/effects';
import { TicketsCardActions } from '@/v5/store/tickets/card/ticketsCard.redux';
-import { ROUTES } from '../../constants/routes';
-import { INITIAL_HELICOPTER_SPEED, VIEWER_CLIP_MODES, VIEWER_EVENTS } from '../../constants/viewer';
+import { INITIAL_HELICOPTER_SPEED, VIEWER_GIZMO_MODES, VIEWER_EVENTS, VIEWER_CLIP_MODES } from '../../constants/viewer';
import * as API from '../../services/api';
import { MultiSelect } from '../../services/viewer/multiSelect';
import { Viewer } from '../../services/viewer/viewer';
@@ -45,13 +43,12 @@ import { SequencesActions } from '../sequences';
import { StarredActions } from '../starred';
import { dispatch } from '../store';
import { TreeActions } from '../tree';
-import { selectInitialView, selectViewpointsDomain, selectViewpointsList, ViewpointsActions, ViewpointsTypes } from '../viewpoints';
+import { selectInitialView, selectViewpointsDomain, ViewpointsActions, ViewpointsTypes } from '../viewpoints';
import { ViewerGuiActions, ViewerGuiTypes } from './viewerGui.redux';
import {
- selectClipNumber,
+ selectClippingMode,
selectHelicopterSpeed,
selectIsClipEdit,
- selectIsMetadataVisible
} from './viewerGui.selectors';
function* fetchData({ teamspace, model }) {
@@ -199,24 +196,6 @@ function* stopListenOnClickPin() {
}
}
-function* updateClipState({clipNumber}) {
- try {
- const isClipEdit = yield select(selectIsClipEdit);
- const currentClipNumber = yield select(selectClipNumber);
-
- if (currentClipNumber !== clipNumber) {
- yield put(ViewerGuiActions.setClipNumber(clipNumber));
-
- if (clipNumber === 0 && isClipEdit) {
- yield put(ViewerGuiActions.setClipEdit(false));
- yield put(ViewerGuiActions.setClippingMode(null));
- }
- }
- } catch (error) {
- yield put(DialogActions.showErrorDialog('update', 'clip state', error));
- }
-}
-
function* goToHomeView() {
try {
const defaultView = yield select(selectDefaultView);
@@ -307,28 +286,52 @@ function* decreaseHelicopterSpeed({ teamspace, modelId }) {
}
}
-function* setClippingMode({mode}) {
+function* setClippingMode({ mode }) {
try {
- if (mode) {
- const isSingle = mode === VIEWER_CLIP_MODES.SINGLE;
- yield Viewer.startClip(isSingle);
+ const currentClipMode = yield select(selectClippingMode);
+ if (currentClipMode !== mode) {
+ yield put(ViewerGuiActions.setClipModeSuccess(mode));
+ if (!mode) {
+ yield Viewer.clipToolDelete();
+ yield put(ViewerGuiActions.setClipEdit(false));
+ }
}
- yield put(ViewerGuiActions.setClippingModeSuccess(mode));
} catch (error) {
- yield put(DialogActions.showErrorDialog('set', 'clipping mode', error));
+ yield put(DialogActions.showErrorDialog('set', 'clip mode', error));
}
}
-function* setClipEdit({isClipEdit}) {
+function* setGizmoMode({ mode }) {
try {
- if (isClipEdit) {
- yield Viewer.startClipEdit();
- } else {
- yield Viewer.stopClipEdit();
+ switch (mode) {
+ case VIEWER_GIZMO_MODES.ROTATE:
+ Viewer.clipToolRotate()
+ break;
+ case VIEWER_GIZMO_MODES.SCALE:
+ Viewer.clipToolScale()
+ break
+ default:
+ Viewer.clipToolTranslate();
+ break;
+ }
+ yield put(ViewerGuiActions.setGizmoModeSuccess(mode));
+ } catch (error) {
+ yield put(DialogActions.showErrorDialog('set', 'gizmo mode', error));
+ }
+}
+
+function* setClipEdit({ isClipEdit }) {
+ try {
+ const currentClipEdit = yield select(selectIsClipEdit);
+ if (currentClipEdit !== isClipEdit) {
+ const clippingMode = yield select(selectClippingMode);
+ yield all([
+ isClipEdit ? Viewer.startClip(clippingMode === VIEWER_CLIP_MODES.SINGLE) : Viewer.stopClipEdit(),
+ put(ViewerGuiActions.setClipEditSuccess(isClipEdit)),
+ ])
}
- yield put(ViewerGuiActions.setClipEditSuccess(isClipEdit));
} catch (error) {
- yield put(DialogActions.showErrorDialog('toggle', 'clip edit', error));
+ yield put(DialogActions.showErrorDialog('set', 'clip edit', error));
}
}
@@ -418,7 +421,7 @@ export default function* ViewerGuiSaga() {
yield takeLatest(ViewerGuiTypes.DECREASE_HELICOPTER_SPEED, decreaseHelicopterSpeed);
yield takeLatest(ViewerGuiTypes.GO_TO_HOME_VIEW, goToHomeView);
yield takeLatest(ViewerGuiTypes.SET_CLIPPING_MODE, setClippingMode);
- yield takeLatest(ViewerGuiTypes.UPDATE_CLIP_STATE, updateClipState);
+ yield takeLatest(ViewerGuiTypes.SET_GIZMO_MODE, setGizmoMode);
yield takeLatest(ViewerGuiTypes.SET_CLIP_EDIT, setClipEdit);
yield takeLatest(ViewerGuiTypes.CLEAR_HIGHLIGHTS, clearHighlights);
yield takeLatest(ViewerGuiTypes.SET_CAMERA, setCamera);
diff --git a/frontend/src/v4/modules/viewerGui/viewerGui.selectors.ts b/frontend/src/v4/modules/viewerGui/viewerGui.selectors.ts
index 98eea55cdd0..cae58a3eb8b 100644
--- a/frontend/src/v4/modules/viewerGui/viewerGui.selectors.ts
+++ b/frontend/src/v4/modules/viewerGui/viewerGui.selectors.ts
@@ -81,12 +81,12 @@ export const selectClippingMode = createSelector(
selectViewerGuiDomain, (state) => state.clippingMode
);
-export const selectIsClipEdit = createSelector(
- selectViewerGuiDomain, (state) => state.isClipEdit
+export const selectGizmoMode = createSelector(
+ selectViewerGuiDomain, (state) => state.gizmoMode
);
-export const selectClipNumber = createSelector(
- selectViewerGuiDomain, (state) => state.clipNumber
+export const selectIsClipEdit = createSelector(
+ selectViewerGuiDomain, (state) => state.isClipEdit
);
export const selectColorOverrides = createSelector(
diff --git a/frontend/src/v4/modules/viewpoints/viewpoints.sagas.ts b/frontend/src/v4/modules/viewpoints/viewpoints.sagas.ts
index 296edb1960e..54392f01ad7 100644
--- a/frontend/src/v4/modules/viewpoints/viewpoints.sagas.ts
+++ b/frontend/src/v4/modules/viewpoints/viewpoints.sagas.ts
@@ -182,6 +182,7 @@ export function* showViewpoint({teamspace, modelId, view, ignoreCamera}) {
yield put(TreeActions.setHiddenGeometryVisible(viewpoint.hideIfc === false));
yield Viewer.updateClippingPlanes(clippingPlanes, teamspace, modelId);
+ yield put(ViewerGuiActions.setClipEdit(false));
yield waitForTreeToBeReady();
yield put(ViewpointsActions.fetchViewpointGroups(teamspace, modelId, view));
diff --git a/frontend/src/v4/routes/viewerGui/components/viewerPanel/viewerPanel.styles.tsx b/frontend/src/v4/routes/viewerGui/components/viewerPanel/viewerPanel.styles.tsx
index 83af6c701d5..02530197e2d 100644
--- a/frontend/src/v4/routes/viewerGui/components/viewerPanel/viewerPanel.styles.tsx
+++ b/frontend/src/v4/routes/viewerGui/components/viewerPanel/viewerPanel.styles.tsx
@@ -57,10 +57,6 @@ export const Panel = styled(PanelComponent)`
${({ isPending }) => isPending ? css`
min-height: 120px !important;
` : ''};
-
- &:last-child {
- margin-bottom: 0;
- }
`;
export const TitleContainer = styled.div`
@@ -133,6 +129,7 @@ export const ViewerPanelFooter = styled(Grid).attrs({
flex: none;
min-height: 65px;
font-size: 14px;
+ z-index: 1;
`;
export const ViewerPanelButton = styled(SubmitButton)`
diff --git a/frontend/src/v4/services/viewer/viewer.tsx b/frontend/src/v4/services/viewer/viewer.tsx
index 4b940adbc4b..583180c1d48 100644
--- a/frontend/src/v4/services/viewer/viewer.tsx
+++ b/frontend/src/v4/services/viewer/viewer.tsx
@@ -1223,9 +1223,8 @@ export class ViewerService {
this.emit(VIEWER_EVENTS.CLIPPING_PLANE_BROADCAST, clip);
}
- public numClipPlanesUpdated(nPlanes) {
- this.numClips = nPlanes;
- this.emit(VIEWER_EVENTS.UPDATE_NUM_CLIP, nPlanes);
+ public clipUpdated() {
+ this.emit(VIEWER_EVENTS.UPDATE_CLIP);
}
public updateClippingPlanes( clipPlanes, account, model ) {
@@ -1238,6 +1237,14 @@ export class ViewerService {
} else {
this.startBoxClip();
}
+ this.emit(VIEWER_EVENTS.UPDATE_CLIP_EDIT, true);
+ }
+
+ public setClipMode(mode) {
+ if (!mode) {
+ this.clipToolDelete()
+ }
+ this.emit(VIEWER_EVENTS.UPDATE_CLIP_MODE, mode);
}
public startBoxClip() {
@@ -1250,10 +1257,41 @@ export class ViewerService {
public startClipEdit() {
UnityUtil.startClipEdit();
+ this.emit(VIEWER_EVENTS.UPDATE_CLIP_EDIT, true);
}
public stopClipEdit() {
UnityUtil.stopClipEdit();
+ this.emit(VIEWER_EVENTS.UPDATE_CLIP_EDIT, false);
+ }
+
+ public clipToolFlip() {
+ UnityUtil.clipToolFlip();
+ }
+
+ public clipToolRealign() {
+ UnityUtil.clipToolRealign();
+ }
+
+ public clipToolClipToSelection() {
+ UnityUtil.clipToolClipToSelection();
+ }
+
+ public clipToolDelete() {
+ UnityUtil.clipToolDelete();
+ this.emit(VIEWER_EVENTS.UPDATE_CLIP_MODE, null);
+ }
+
+ public clipToolRotate() {
+ UnityUtil.clipToolRotate();
+ }
+
+ public clipToolScale() {
+ UnityUtil.clipToolScale();
+ }
+
+ public clipToolTranslate() {
+ UnityUtil.clipToolTranslate();
}
public setNavigationOn() {
diff --git a/frontend/src/v5/helpers/viewpoint.helpers.ts b/frontend/src/v5/helpers/viewpoint.helpers.ts
index 86097f3dbf9..7c54ad925cf 100644
--- a/frontend/src/v5/helpers/viewpoint.helpers.ts
+++ b/frontend/src/v5/helpers/viewpoint.helpers.ts
@@ -213,6 +213,7 @@ export const goToView = async (view: Viewpoint) => {
isEmpty(view?.clippingPlanes)) {
return;
}
+
dispatch(ViewpointsActions.showViewpoint(null, null, viewpointV5ToV4(view)));
};
diff --git a/frontend/src/v5/services/actionsDispatchers.ts b/frontend/src/v5/services/actionsDispatchers.ts
index 4820282496d..bdd360696fb 100644
--- a/frontend/src/v5/services/actionsDispatchers.ts
+++ b/frontend/src/v5/services/actionsDispatchers.ts
@@ -38,7 +38,7 @@ import { ViewerActions, ViewerActionsCreators } from '@/v5/store/viewer/viewer.r
import { ViewerGuiActions } from '@/v4/modules/viewerGui';
import { Action } from 'redux';
-import { ClipMode, MeasureMode, NavigationMode, ProjectionMode } from '../ui/routes/viewer/toolbar/toolbar.types';
+import { ClipMode, GizmoMode, MeasureMode, NavigationMode, ProjectionMode } from '../ui/routes/viewer/toolbar/toolbar.types';
interface IBimActionCreators {
setIsActive: (active: boolean) => Action;
@@ -79,9 +79,9 @@ interface IViewerGuiActionCreators {
decreaseHelicopterSpeed: (teamspace: string, containerOrFederation: string) => Action;
resetHelicopterSpeed: (teamspace: string, containerOrFederation: string) => Action;
setClippingMode: (mode: ClipMode) => Action;
+ setGizmoMode: (mode: GizmoMode) => Action;
setCoordView: (visible: boolean) => Action;
setPanelVisibility: (panelName: string, visible: boolean) => Action;
- updateClipState: (clipNumber: number) => Action;
setClipEdit: (isClipEdit: boolean) => Action;
clearColorOverrides: () => Action;
clearTransformations: () => Action;
diff --git a/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/clipButtons.component.tsx b/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/clipButtons.component.tsx
index 3fa05480d22..d1829d1091a 100644
--- a/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/clipButtons.component.tsx
+++ b/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/clipButtons.component.tsx
@@ -23,14 +23,15 @@ import { useEffect, useState } from 'react';
import { ViewerGuiHooksSelectors } from '@/v5/services/selectorsHooks';
import { ViewerGuiActionsDispatchers } from '@/v5/services/actionsDispatchers';
import { Viewer } from '@/v4/services/viewer/viewer';
-import { VIEWER_EVENTS } from '@/v4/constants/viewer';
+import { VIEWER_CLIP_MODES, VIEWER_EVENTS } from '@/v4/constants/viewer';
import { ButtonOptionsContainer, FloatingButtonsContainer, FloatingButton } from './multioptionIcons.styles';
import { ClipMode } from '../../toolbar.types';
import { ToolbarButton } from '../toolbarButton.component';
-const clipTooltipText = formatMessage({ id: 'viewer.toolbar.icon.clip', defaultMessage: 'Clip' });
-const startBoxClipTooltipText = formatMessage({ id: 'viewer.toolbar.icon.clip.startBox', defaultMessage: 'Start Box Clip' });
-const startSingleClipTooltipText = formatMessage({ id: 'viewer.toolbar.icon.clip.startSingle', defaultMessage: 'Start Single Clip' });
+const clipTooltipText = formatMessage({ id: 'viewer.toolbar.icon.sectioning', defaultMessage: 'Sectioning' });
+const startBoxClipTooltipText = formatMessage({ id: 'viewer.toolbar.icon.sectioning.boxSection', defaultMessage: 'Section By Box' });
+const startSingleClipTooltipText = formatMessage({ id: 'viewer.toolbar.icon.sectioning.planeSection', defaultMessage: 'Section By Plane' });
+
export const ClipButtons = () => {
const [expanded, setExpanded] = useState(false);
const clipMode: ClipMode = ViewerGuiHooksSelectors.selectClippingMode();
@@ -39,11 +40,16 @@ export const ClipButtons = () => {
const setMode = (mode: ClipMode) => {
setExpanded(false);
ViewerGuiActionsDispatchers.setClippingMode(mode);
+ ViewerGuiActionsDispatchers.setClipEdit(true);
};
useEffect(() => {
- Viewer.on(VIEWER_EVENTS.UPDATE_NUM_CLIP, ViewerGuiActionsDispatchers.updateClipState);
- return () => Viewer.off(VIEWER_EVENTS.UPDATE_NUM_CLIP, ViewerGuiActionsDispatchers.updateClipState);
+ Viewer.on(VIEWER_EVENTS.UPDATE_CLIP_EDIT, ViewerGuiActionsDispatchers.setClipEdit);
+ Viewer.on(VIEWER_EVENTS.UPDATE_CLIP_MODE, (mode: ClipMode) => ViewerGuiActionsDispatchers.setClippingMode(mode));
+ return () => {
+ Viewer.off(VIEWER_EVENTS.UPDATE_CLIP_EDIT, ViewerGuiActionsDispatchers.setClipEdit);
+ Viewer.off(VIEWER_EVENTS.UPDATE_CLIP_MODE, () => ViewerGuiActionsDispatchers.setClippingMode(null));
+ };
}, []);
if (clipMode === null) {
@@ -52,8 +58,8 @@ export const ClipButtons = () => {
{expanded && (
- setMode('BOX')} title={startBoxClipTooltipText} />
- setMode('SINGLE')} title={startSingleClipTooltipText} />
+ setMode(VIEWER_CLIP_MODES.BOX)} title={startBoxClipTooltipText} />
+ setMode(VIEWER_CLIP_MODES.SINGLE)} title={startSingleClipTooltipText} />
)}
setExpanded(!expanded)} title={!expanded ? clipTooltipText : ''} />
diff --git a/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/gizmoModeButtons.component.tsx b/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/gizmoModeButtons.component.tsx
new file mode 100644
index 00000000000..5b872ec68bf
--- /dev/null
+++ b/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/gizmoModeButtons.component.tsx
@@ -0,0 +1,73 @@
+/**
+ * Copyright (C) 2023 3D Repo Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import GizmoRotateIcon from '@assets/icons/viewer/gizmo_rotate.svg';
+import GizmoScaleIcon from '@assets/icons/viewer/gizmo_scale.svg';
+import GizmoTranslateIcon from '@assets/icons/viewer/gizmo_translate.svg';
+import { ClickAwayListener } from '@mui/material';
+import { formatMessage } from '@/v5/services/intl';
+import { useState } from 'react';
+import { VIEWER_CLIP_MODES, VIEWER_GIZMO_MODES } from '@/v4/constants/viewer';
+import { ButtonOptionsContainer, FloatingButtonsContainer, FloatingButton } from './multioptionIcons.styles';
+import { GizmoMode } from '../../toolbar.types';
+import { ToolbarButton } from '../toolbarButton.component';
+import { ViewerGuiHooksSelectors } from '@/v5/services/selectorsHooks';
+import { ViewerGuiActionsDispatchers } from '@/v5/services/actionsDispatchers';
+
+const GIZMO_OPTIONS = {
+ [VIEWER_GIZMO_MODES.TRANSLATE]: {
+ title: formatMessage({ id: 'viewer.toolbar.icon.gizmoModes.move', defaultMessage: 'Move' }),
+ Icon: GizmoTranslateIcon,
+ },
+ [VIEWER_GIZMO_MODES.SCALE]: {
+ title: formatMessage({ id: 'viewer.toolbar.icon.gizmoModes.resize', defaultMessage: 'Resize' }),
+ Icon: GizmoScaleIcon,
+ },
+ [VIEWER_GIZMO_MODES.ROTATE]: {
+ title: formatMessage({ id: 'viewer.toolbar.icon.gizmoModes.rotate', defaultMessage: 'Rotate' }),
+ Icon: GizmoRotateIcon,
+ },
+};
+
+export const GizmoModeButtons = ({ disabled, ...props }) => {
+ const [expanded, setExpanded] = useState(false);
+ const isBoxClippingMode = ViewerGuiHooksSelectors.selectClippingMode() === VIEWER_CLIP_MODES.BOX;
+ const gizmoMode = ViewerGuiHooksSelectors.selectGizmoMode();
+
+ const setMode = (mode: GizmoMode) => {
+ if (disabled) return;
+ setExpanded(false);
+ ViewerGuiActionsDispatchers.setGizmoMode(mode);
+ };
+
+ const FloatingGizmoButton = ({ mode }) => setMode(mode)} />;
+
+ return (
+ setExpanded(false)}>
+
+ {expanded && (
+
+ {gizmoMode !== VIEWER_GIZMO_MODES.TRANSLATE && }
+ {gizmoMode !== VIEWER_GIZMO_MODES.SCALE && isBoxClippingMode && }
+ {gizmoMode !== VIEWER_GIZMO_MODES.ROTATE && }
+
+ )}
+ setExpanded(!expanded)} title={!expanded ? GIZMO_OPTIONS[gizmoMode].title : ''} disabled={disabled} {...props} />
+
+
+ );
+};
diff --git a/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/multioptionIcons.styles.ts b/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/multioptionIcons.styles.ts
index 5a0ff43bb72..a963664fc7d 100644
--- a/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/multioptionIcons.styles.ts
+++ b/frontend/src/v5/ui/routes/viewer/toolbar/buttons/buttonOptionsContainer/multioptionIcons.styles.ts
@@ -15,11 +15,11 @@
* along with this program. If not, see .
*/
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
import { Container } from '../toolbarButton.styles';
import { ToolbarButton } from '../toolbarButton.component';
-export const ButtonOptionsContainer = styled.div`
+export const ButtonOptionsContainer = styled.div<{ disabled?: boolean }>`
position: relative;
& > ${/* sc-selector */Container}::after {
@@ -34,7 +34,9 @@ export const ButtonOptionsContainer = styled.div`
}
&:hover > ${/* sc-selector */Container}::after {
- border-color: ${({ theme }) => theme.palette.primary.contrast};
+ ${({ disabled, theme }) => !disabled && css`
+ border-color: ${theme.palette.primary.contrast};
+ `}
}
:is(&, &:hover) > ${/* sc-selector */Container}::after {
diff --git a/frontend/src/v5/ui/routes/viewer/toolbar/buttons/toolbarButton.styles.ts b/frontend/src/v5/ui/routes/viewer/toolbar/buttons/toolbarButton.styles.ts
index c3eba479359..6d254c008a8 100644
--- a/frontend/src/v5/ui/routes/viewer/toolbar/buttons/toolbarButton.styles.ts
+++ b/frontend/src/v5/ui/routes/viewer/toolbar/buttons/toolbarButton.styles.ts
@@ -36,7 +36,6 @@ export const Container = styled(ViewerIconContainer)<{ disabled?: boolean, hidde
${({ disabled }) => disabled && css`
cursor: default;
- pointer-events: none;
`}
`;
diff --git a/frontend/src/v5/ui/routes/viewer/toolbar/selectionToolbar/selectionToolbar.component.tsx b/frontend/src/v5/ui/routes/viewer/toolbar/selectionToolbar/selectionToolbar.component.tsx
index 2b3cc18f653..f5b287847ed 100644
--- a/frontend/src/v5/ui/routes/viewer/toolbar/selectionToolbar/selectionToolbar.component.tsx
+++ b/frontend/src/v5/ui/routes/viewer/toolbar/selectionToolbar/selectionToolbar.component.tsx
@@ -14,6 +14,10 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
+import FlipPlaneIcon from '@assets/icons/viewer/flip_plane.svg';
+import AlignIcon from '@assets/icons/viewer/align.svg';
+import ClipSelectionIcon from '@assets/icons/viewer/clip_selection.svg';
+import CancelIcon from '@assets/icons/viewer/delete.svg';
import ClearOverridesIcon from '@assets/icons/viewer/clear_overrides.svg';
import EyeHideIcon from '@assets/icons/viewer/eye_hide.svg';
import EyeShowIcon from '@assets/icons/viewer/eye_show.svg';
@@ -26,15 +30,70 @@ import { isEmpty } from 'lodash';
import { FormattedMessage } from 'react-intl';
import { Section, Container, ClearButton, ClearIcon } from './selectionToolbar.styles';
import { ToolbarButton } from '../buttons/toolbarButton.component';
+import { VIEWER_CLIP_MODES, VIEWER_EVENTS } from '@/v4/constants/viewer';
+import { GizmoModeButtons } from '../buttons/buttonOptionsContainer/gizmoModeButtons.component';
+import { Viewer } from '@/v4/services/viewer/viewer';
+import { useEffect, useState } from 'react';
export const SectionToolbar = () => {
+ const [alignActive, setAlignActive] = useState(false);
+
const hasOverrides = !isEmpty(ViewerGuiHooksSelectors.selectColorOverrides());
const hasHighlightedObjects = !!TreeHooksSelectors.selectFullySelectedNodesIds().length;
const hasHiddenObjects = TreeHooksSelectors.selectModelHasHiddenNodes();
+ const clippingMode = ViewerGuiHooksSelectors.selectClippingMode();
+ const clippingSectionOpen = ViewerGuiHooksSelectors.selectIsClipEdit();
+ const isBoxClippingMode = clippingMode === VIEWER_CLIP_MODES.BOX;
const hasTransformations = !isEmpty(ViewerGuiHooksSelectors.selectTransformations());
+ const onClickAlign = () => {
+ Viewer.clipToolRealign();
+ setAlignActive(true);
+ };
+
+ useEffect(() => {
+ if (alignActive) {
+ Viewer.on(VIEWER_EVENTS.UPDATE_CLIP, () => setAlignActive(false));
+ Viewer.on(VIEWER_EVENTS.BACKGROUND_SELECTED, () => setAlignActive(false));
+ }
+ return () => {
+ Viewer.off(VIEWER_EVENTS.UPDATE_CLIP, () => setAlignActive(false));
+ Viewer.off(VIEWER_EVENTS.BACKGROUND_SELECTED, () => setAlignActive(false));
+ };
+ }, [alignActive]);
+
+
return (
+
+
+
+
+
+ ViewerGuiActionsDispatchers.setClippingMode(null)}
+ title={formatMessage({ id: 'viewer.toolbar.icon.deleteClip', defaultMessage: 'Delete' })}
+ />
+ .
*/
-import { VIEWER_CLIP_MODES, VIEWER_MEASURING_MODE, VIEWER_NAV_MODES, VIEWER_PROJECTION_MODES } from '@/v4/constants/viewer';
+import { VIEWER_CLIP_MODES, VIEWER_GIZMO_MODES, VIEWER_MEASURING_MODE, VIEWER_NAV_MODES, VIEWER_PROJECTION_MODES } from '@/v4/constants/viewer';
type ValuesOf = T[keyof T];
export type ProjectionMode = ValuesOf;
export type NavigationMode = ValuesOf;
-export type ClipMode = ValuesOf;
+export type ClipMode = ValuesOf | null;
+export type GizmoMode = ValuesOf;
export type MeasureMode = ValuesOf;
diff --git a/frontend/src/v5/ui/v4Adapter/overrides/leftPanel.overrides.ts b/frontend/src/v5/ui/v4Adapter/overrides/leftPanel.overrides.ts
index b976fb6ed87..02bde5d4fe5 100644
--- a/frontend/src/v5/ui/v4Adapter/overrides/leftPanel.overrides.ts
+++ b/frontend/src/v5/ui/v4Adapter/overrides/leftPanel.overrides.ts
@@ -182,6 +182,7 @@ export default css`
color: ${({ theme }) => theme.palette.base.light} !important;
box-sizing: border-box;
border-color: ${({ theme }) => theme.palette.base.lightest};
+ z-index: 0;
${StyledIconButton} {
height: 26px;
diff --git a/frontend/src/v5/ui/v4Adapter/overrides/panelsMenu.overrides.ts b/frontend/src/v5/ui/v4Adapter/overrides/panelsMenu.overrides.ts
index 4f6dcba9a28..6e2fa84db60 100644
--- a/frontend/src/v5/ui/v4Adapter/overrides/panelsMenu.overrides.ts
+++ b/frontend/src/v5/ui/v4Adapter/overrides/panelsMenu.overrides.ts
@@ -60,9 +60,6 @@ export default css`
${RightPanels} {
margin-top: 8px;
box-sizing: border-box;
- @media (min-width: 1520px) {
- height: calc(100% - 38px);
- }
${InputContainer} .react-autosuggest__container input {
height: 50px;
diff --git a/frontend/unity/default/unity/Build/unity.data.unityweb b/frontend/unity/default/unity/Build/unity.data.unityweb
index 4791e50d078..37492b8ba7a 100644
Binary files a/frontend/unity/default/unity/Build/unity.data.unityweb and b/frontend/unity/default/unity/Build/unity.data.unityweb differ
diff --git a/frontend/unity/default/unity/Build/unity.framework.js.unityweb b/frontend/unity/default/unity/Build/unity.framework.js.unityweb
index 61e22d9da96..92d0f0a96d8 100644
Binary files a/frontend/unity/default/unity/Build/unity.framework.js.unityweb and b/frontend/unity/default/unity/Build/unity.framework.js.unityweb differ
diff --git a/frontend/unity/default/unity/Build/unity.loader.js b/frontend/unity/default/unity/Build/unity.loader.js
index 0adec7ff350..b92259ff492 100644
--- a/frontend/unity/default/unity/Build/unity.loader.js
+++ b/frontend/unity/default/unity/Build/unity.loader.js
@@ -1 +1 @@
-function createUnityInstance(t,r,l){function d(e,t){if(!d.aborted&&r.showBanner)return"error"==t&&(d.aborted=!0),r.showBanner(e,t);switch(t){case"error":console.error(e);break;case"warning":console.warn(e);break;default:console.log(e)}}function n(e){var t=e.reason||e.error,r=t?t.toString():e.message||e.reason||"",n=t&&t.stack?t.stack.toString():"";(r+="\n"+(n=n.startsWith(r)?n.substring(r.length):n).trim())&&b.stackTraceRegExp&&b.stackTraceRegExp.test(r)&&h(r,e.filename||t&&(t.fileName||t.sourceURL)||"",e.lineno||t&&(t.lineNumber||t.line)||0)}function e(e,t,r){var n=e[t];void 0!==n&&n||(console.warn('Config option "'+t+'" is missing or empty. Falling back to default value: "'+r+'". Consider updating your WebGL template to include the missing config option.'),e[t]=r)}l=l||function(){};var o,b={canvas:t,webglContextAttributes:{preserveDrawingBuffer:!1,powerPreference:2},wasmFileSize:33826535,streamingAssetsUrl:"StreamingAssets",downloadProgress:{},deinitializers:[],intervals:{},setInterval:function(e,t){e=window.setInterval(e,t);return this.intervals[e]=!0,e},clearInterval:function(e){delete this.intervals[e],window.clearInterval(e)},preRun:[],postRun:[],print:function(e){console.log(e)},printErr:function(e){console.error(e),"string"==typeof e&&-1!=e.indexOf("wasm streaming compile failed")&&(-1!=e.toLowerCase().indexOf("mime")?d('HTTP Response Header "Content-Type" configured incorrectly on the server for file '+b.codeUrl+' , should be "application/wasm". Startup time performance will suffer.',"warning"):d('WebAssembly streaming compilation failed! This can happen for example if "Content-Encoding" HTTP header is incorrectly enabled on the server for file '+b.codeUrl+", but the file is not pre-compressed on disk (or vice versa). Check the Network tab in browser Devtools to debug server header configuration.","warning"))},locateFile:function(e){return e},disabledCanvasEvents:["contextmenu","dragstart"]};for(o in e(r,"companyName","Unity"),e(r,"productName","WebGL Player"),e(r,"productVersion","1.0"),r)b[o]=r[o];b.streamingAssetsUrl=new URL(b.streamingAssetsUrl,document.URL).href;var i=b.disabledCanvasEvents.slice();function a(e){e.preventDefault()}i.forEach(function(e){t.addEventListener(e,a)}),window.addEventListener("error",n),window.addEventListener("unhandledrejection",n);var s="",f="";function u(e){document.webkitCurrentFullScreenElement===t?t.style.width&&(s=t.style.width,f=t.style.height,t.style.width="100%",t.style.height="100%"):s&&(t.style.width=s,t.style.height=f,f=s="")}document.addEventListener("webkitfullscreenchange",u),b.deinitializers.push(function(){for(var e in b.disableAccessToMediaDevices(),i.forEach(function(e){t.removeEventListener(e,a)}),window.removeEventListener("error",n),window.removeEventListener("unhandledrejection",n),document.removeEventListener("webkitfullscreenchange",u),b.intervals)window.clearInterval(e);b.intervals={}}),b.QuitCleanup=function(){for(var e=0;e>2],usedWASMHeapSize:b.HEAPU32[t>>2],totalJSHeapSize:b.HEAPF64[r>>3],usedJSHeapSize:b.HEAPF64[n>>3],pageLoadTime:b.HEAPU32[o>>2],pageLoadTimeToFrame1:b.HEAPU32[i>>2],fps:b.HEAPF64[a>>3],movingAverageFps:b.HEAPF64[s>>3],assetLoadTime:b.HEAPU32[l>>2],webAssemblyStartupTime:b.HEAPU32[d>>2]-(b.webAssemblyTimeStart||0),codeDownloadTime:b.HEAPU32[f>>2],gameStartupTime:b.HEAPU32[u>>2],numJankedFrames:b.HEAPU32[u+4>>2]}}};function h(e,t,r){-1==e.indexOf("fullscreen error")&&(b.startupErrorHandler?b.startupErrorHandler(e,t,r):b.errorHandler&&b.errorHandler(e,t,r)||(console.log("Invoking error handler due to\n"+e),"function"==typeof dump&&dump("Invoking error handler due to\n"+e),h.didShowErrorMessage||(-1!=(e="An error occurred running the Unity content on this page. See your browser JavaScript console for more info. The error was:\n"+e).indexOf("DISABLE_EXCEPTION_CATCHING")?e="An exception has occurred, but exception handling has been disabled in this build. If you are the developer of this content, enable exceptions in your project WebGL player settings to be able to catch the exception or see the stack trace.":-1!=e.indexOf("Cannot enlarge memory arrays")?e="Out of memory. If you are the developer of this content, try allocating more memory to your WebGL build in the WebGL player settings.":-1==e.indexOf("Invalid array buffer length")&&-1==e.indexOf("Invalid typed array length")&&-1==e.indexOf("out of memory")&&-1==e.indexOf("could not allocate memory")||(e="The browser could not allocate enough memory for the WebGL content. If you are the developer of this content, try allocating less memory to your WebGL build in the WebGL player settings."),alert(e),h.didShowErrorMessage=!0)))}function m(e,t){if("symbolsUrl"!=e){var r=b.downloadProgress[e],n=(r=r||(b.downloadProgress[e]={started:!1,finished:!1,lengthComputable:!1,total:0,loaded:0}),"object"!=typeof t||"progress"!=t.type&&"load"!=t.type||(r.started||(r.started=!0,r.lengthComputable=t.lengthComputable),r.total=t.total,r.loaded=t.loaded,"load"==t.type&&(r.finished=!0)),0),o=0,i=0,a=0,s=0;for(e in b.downloadProgress){if(!(r=b.downloadProgress[e]).started)return;i++,r.lengthComputable?(n+=r.loaded,o+=r.total,a++):r.finished||s++}l(.9*(i?(i-s-(o?a*(o-n)/o:0))/i:0))}}b.SystemInfo=function(){var e,t,r,n,o=navigator.userAgent+" ",i=[["Firefox","Firefox"],["OPR","Opera"],["Edg","Edge"],["SamsungBrowser","Samsung Browser"],["Trident","Internet Explorer"],["MSIE","Internet Explorer"],["Chrome","Chrome"],["CriOS","Chrome on iOS Safari"],["FxiOS","Firefox on iOS Safari"],["Safari","Safari"]];function a(e,t,r){return(e=RegExp(e,"i").exec(t))&&e[r]}for(var s=0;s>>6:(r<65536?t[o++]=224|r>>>12:(t[o++]=240|r>>>18,t[o++]=128|r>>>12&63),t[o++]=128|r>>>6&63),t[o++]=128|63&r);return t},r.buf2binstring=function(e){return f(e,e.length)},r.binstring2buf=function(e){for(var t=new l.Buf8(e.length),r=0,n=t.length;r>10&1023,i[a++]=56320|1023&r)}return f(i,a)},r.utf8border=function(e,t){for(var r=(t=(t=t||e.length)>e.length?e.length:t)-1;0<=r&&128==(192&e[r]);)r--;return!(r<0)&&0!==r&&r+d[e[r]]>t?r:t}},"zlib/inflate.js":function(e,t,r){"use strict";var B=e("../utils/common"),L=e("./adler32"),O=e("./crc32"),R=e("./inffast"),I=e("./inftrees"),z=0,F=-2,H=1,n=852,o=592;function N(e){return(e>>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function i(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new B.Buf16(320),this.work=new B.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=H,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new B.Buf32(n),t.distcode=t.distdyn=new B.Buf32(o),t.sane=1,t.back=-1,z):F}function s(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):F}function l(e,t){var r,n;return!e||!e.state||(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=e.wsize?(B.arraySet(e.window,t,r-e.wsize,e.wsize,0),e.wnext=0,e.whave=e.wsize):(n<(o=e.wsize-e.wnext)&&(o=n),B.arraySet(e.window,t,r-n,o,e.wnext),(n-=o)?(B.arraySet(e.window,t,r-n,n,0),e.wnext=n,e.whave=e.wsize):(e.wnext+=o,e.wnext===e.wsize&&(e.wnext=0),e.whave>>8&255,r.check=O(r.check,C,2,0),f=d=0,r.mode=2;else if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&d)<<8)+(d>>8))%31)e.msg="incorrect header check",r.mode=30;else if(8!=(15&d))e.msg="unknown compression method",r.mode=30;else{if(f-=4,_=8+(15&(d>>>=4)),0===r.wbits)r.wbits=_;else if(_>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<<_,e.adler=r.check=1,r.mode=512&d?10:12,f=d=0}}break;case 2:for(;f<16;){if(0===s)break e;s--,d+=n[i++]<>8&1),512&r.flags&&(C[0]=255&d,C[1]=d>>>8&255,r.check=O(r.check,C,2,0)),f=d=0,r.mode=3;case 3:for(;f<32;){if(0===s)break e;s--,d+=n[i++]<>>8&255,C[2]=d>>>16&255,C[3]=d>>>24&255,r.check=O(r.check,C,4,0)),f=d=0,r.mode=4;case 4:for(;f<16;){if(0===s)break e;s--,d+=n[i++]<>8),512&r.flags&&(C[0]=255&d,C[1]=d>>>8&255,r.check=O(r.check,C,2,0)),f=d=0,r.mode=5;case 5:if(1024&r.flags){for(;f<16;){if(0===s)break e;s--,d+=n[i++]<>>8&255,r.check=O(r.check,C,2,0)),f=d=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&((h=s<(h=r.length)?s:h)&&(r.head&&(_=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),B.arraySet(r.head.extra,n,i,h,_)),512&r.flags&&(r.check=O(r.check,n,h,i)),s-=h,i+=h,r.length-=h),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===s)break e;for(h=0;_=n[i+h++],r.head&&_&&r.length<65536&&(r.head.name+=String.fromCharCode(_)),_&&h>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;f<32;){if(0===s)break e;s--,d+=n[i++]<>>=7&f,f-=7&f,r.mode=27;else{for(;f<3;){if(0===s)break e;s--,d+=n[i++]<>>=1)){case 0:r.mode=14;break;case 1:var A,A=P=void 0,P=r;if(D){for(Z=new B.Buf32(512),M=new B.Buf32(32),A=0;A<144;)P.lens[A++]=8;for(;A<256;)P.lens[A++]=9;for(;A<280;)P.lens[A++]=7;for(;A<288;)P.lens[A++]=8;for(I(1,P.lens,0,288,Z,0,P.work,{bits:9}),A=0;A<32;)P.lens[A++]=5;I(2,P.lens,0,32,M,0,P.work,{bits:5}),D=!1}if(P.lencode=Z,P.lenbits=9,P.distcode=M,P.distbits=5,r.mode=20,6!==t)break;d>>>=2,f-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}d>>>=2,f-=2}break;case 14:for(d>>>=7&f,f-=7&f;f<32;){if(0===s)break e;s--,d+=n[i++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&d,f=d=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(h=r.length){if(0===(h=l<(h=s>>=5,f-=5,r.ndist=1+(31&d),d>>>=5,f-=5,r.ncode=4+(15&d),d>>>=4,f-=4,286>>=3,f-=3}for(;r.have<19;)r.lens[T[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=I(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,w=65535&U,!((g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>>=g,f-=g,r.lens[r.have++]=w;else{if(16===w){for(E=g+2;f>>=g,f-=g,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}_=r.lens[r.have-1],h=3+(3&d),d>>>=2,f-=2}else if(17===w){for(E=g+3;f>>=g)),d>>>=3,f=f-g-3}else{for(E=g+7;f>>=g)),d>>>=7,f=f-g-7}if(r.have+h>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;h--;)r.lens[r.have++]=_}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=I(1,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=I(2,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=s&&258<=l){e.next_out=a,e.avail_out=l,e.next_in=i,e.avail_in=s,r.hold=d,r.bits=f,R(e,c),a=e.next_out,o=e.output,l=e.avail_out,i=e.next_in,n=e.input,s=e.avail_in,d=r.hold,f=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;p=(U=r.lencode[d&(1<>>16&255,w=65535&U,!((g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>v)])>>>16&255,w=65535&U,!(v+(g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>>=v,f-=v,r.back+=v}if(d>>>=g,f-=g,r.back+=g,r.length=w,0===p){r.mode=26;break}if(32&p){r.back=-1,r.mode=12;break}if(64&p){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&p,r.mode=22;case 22:if(r.extra){for(E=r.extra;f>>=r.extra,f-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;p=(U=r.distcode[d&(1<>>16&255,w=65535&U,!((g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>v)])>>>16&255,w=65535&U,!(v+(g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>>=v,f-=v,r.back+=v}if(d>>>=g,f-=g,r.back+=g,64&p){e.msg="invalid distance code",r.mode=30;break}r.offset=w,r.extra=15&p,r.mode=24;case 24:if(r.extra){for(E=r.extra;f>>=r.extra,f-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===l)break e;if(r.offset>(h=c-l)){if((h=r.offset-h)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}m=h>r.wnext?(h-=r.wnext,r.wsize-h):r.wnext-h,h>r.length&&(h=r.length),b=r.window}else b=o,m=a-r.offset,h=r.length;for(l-=h=l>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:r>>>1;e[t]=r}return e}();t.exports=function(e,t,r,n){var o=s,i=n+r;e^=-1;for(var a=n;a>>8^o[255&(e^t[a])];return-1^e}},"zlib/inffast.js":function(e,t,r){"use strict";t.exports=function(e,t){var r,n,o,i,a,s,l=e.state,d=e.next_in,f=e.input,u=d+(e.avail_in-5),c=e.next_out,h=e.output,m=c-(t-e.avail_out),b=c+(e.avail_out-257),g=l.dmax,p=l.wsize,w=l.whave,v=l.wnext,k=l.window,y=l.hold,_=l.bits,x=l.lencode,S=l.distcode,E=(1<>>=n=r>>>24,_-=n,0==(n=r>>>16&255))h[c++]=65535&r;else{if(!(16&n)){if(0==(64&n)){r=x[(65535&r)+(y&(1<>>=n,_-=n),_<15&&(y+=f[d++]<<_,_+=8,y+=f[d++]<<_,_+=8),r=S[y&U];;){if(y>>>=n=r>>>24,_-=n,!(16&(n=r>>>16&255))){if(0==(64&n)){r=S[(65535&r)+(y&(1<>>=n,_-=n,(n=c-m)>3)<<3))-1,e.next_in=d-=o,e.next_out=c,e.avail_in=dh?(b=L[O+a[v]],T[A+a[v]]):(b=96,0),l=1<<(m=w-S),k=d=1<>S)+(d-=l)]=m<<24|b<<16|g|0,0!==d;);for(l=1<>=1;if(C=0!==l?(C&l-1)+l:0,v++,0==--P[w]){if(w===y)break;w=t[r+a[v]]}if(_e.length||31!=e[0]||139!=e[1])return!1;var n=e[3];if(4&n){if(t+2>e.length)return!1;if((t+=2+e[t]+(e[t+1]<<8))>e.length)return!1}if(8&n){for(;te.length)return!1;t++}return 16&n&&String.fromCharCode.apply(null,e.subarray(t,t+r.length+1))==r+"\0"}},br:{hasUnityMarker:function(e){var t="UnityWeb Compressed Content (brotli)";if(!e.length)return!1;var r=1&e[0]?14&e[0]?4:7:1,n=e[0]&(1<>3);if(commentOffset=1+r+2+1+2+(o<<3)+7>>3,17==n||commentOffset>e.length)return!1;for(var i=n+(6+(o<<4)+(t.length-1<<6)<>>=8)if(e[a]!=(255&i))return!1;return String.fromCharCode.apply(null,e.subarray(commentOffset,commentOffset+t.length))==t}}};function p(t){m(t);var e=b.fetchWithProgress,r=b[t],n=/file:\/\//.exec(r)?"same-origin":void 0;return e(b[t],{method:"GET",companyName:b.companyName,productName:b.productName,productVersion:b.productVersion,control:"no-store",mode:n,onProgress:function(e){m(t,e)}}).then(function(e){return a=e.parsedBody,s=b[t],new Promise(function(e,t){try{for(var r in g){var n,o,i;if(g[r].hasUnityMarker(a))return s&&console.log('You can reduce startup time if you configure your web server to add "Content-Encoding: '+r+'" response header when serving "'+s+'" file.'),(n=g[r]).worker||(o=URL.createObjectURL(new Blob(["this.require = ",n.require.toString(),"; this.decompress = ",n.decompress.toString(),"; this.onmessage = ",function(e){e={id:e.data.id,decompressed:this.decompress(e.data.compressed)};postMessage(e,e.decompressed?[e.decompressed.buffer]:[])}.toString(),"; postMessage({ ready: true });"],{type:"application/javascript"})),n.worker=new Worker(o),n.worker.onmessage=function(e){e.data.ready?URL.revokeObjectURL(o):(this.callbacks[e.data.id](e.data.decompressed),delete this.callbacks[e.data.id])},n.worker.callbacks={},n.worker.nextCallbackId=0),i=n.worker.nextCallbackId++,n.worker.callbacks[i]=e,void n.worker.postMessage({id:i,compressed:a},[a.buffer])}e(a)}catch(e){t(e)}});var a,s}).catch(function(e){var t="Failed to download file "+r;"file:"==location.protocol?d(t+". Loading web pages via a file:// URL without a web server is not supported by this browser. Please use a local development web server to host Unity content, or use the Unity Build and Run option.","error"):console.error(t)})}function w(){var t=performance.now(),m=(Promise.all([p("frameworkUrl").then(function(e){var s=URL.createObjectURL(new Blob([e],{type:"application/javascript"}));return new Promise(function(i,e){var a=document.createElement("script");a.src=s,a.onload=function(){if("undefined"==typeof unityFramework||!unityFramework){var e,t=[["br","br"],["gz","gzip"]];for(e in t){var r,n=t[e];if(b.frameworkUrl.endsWith("."+n[0]))return r="Unable to parse "+b.frameworkUrl+"!","file:"==location.protocol?void d(r+" Loading pre-compressed (brotli or gzip) content via a file:// URL without a web server is not supported by this browser. Please use a local development web server to host compressed Unity content, or use the Unity Build and Run option.","error"):(r+=' This can happen if build compression was enabled but web server hosting the content was misconfigured to not serve the file with HTTP Response Header "Content-Encoding: '+n[1]+'" present. Check browser Console and Devtools Network tab to debug.',"br"==n[0]&&"http:"==location.protocol&&(n=-1!=["localhost","127.0.0.1"].indexOf(location.hostname)?"":"Migrate your server to use HTTPS.",r=/Firefox/.test(navigator.userAgent)?"Unable to parse "+b.frameworkUrl+'! If using custom web server, verify that web server is sending .br files with HTTP Response Header "Content-Encoding: br". Brotli compression may not be supported in Firefox over HTTP connections. '+n+' See https://bugzilla.mozilla.org/show_bug.cgi?id=1670675 for more information.':"Unable to parse "+b.frameworkUrl+'! If using custom web server, verify that web server is sending .br files with HTTP Response Header "Content-Encoding: br". Brotli compression may not be supported over HTTP connections. Migrate your server to use HTTPS.'),void d(r,"error"))}d("Unable to parse "+b.frameworkUrl+"! The file is corrupt, or compression was misconfigured? (check Content-Encoding HTTP Response Header on web server)","error")}var o=unityFramework;unityFramework=null,a.onload=null,URL.revokeObjectURL(s),i(o)},a.onerror=function(e){d("Unable to load file "+b.frameworkUrl+"! Check that the file exists on the remote server. (also check browser Console and Devtools Network tab to debug)","error")},document.body.appendChild(a),b.deinitializers.push(function(){document.body.removeChild(a)})})}),p("codeUrl")]).then(function(e){b.wasmBinary=e[1],e[0](b),b.codeDownloadTimeEnd=performance.now()-t}),performance.now()),e=p("dataUrl");b.preRun.push(function(){b.addRunDependency("dataUrl"),e.then(function(t){var e=new TextDecoder("utf-8"),r=0;function n(){var e=(t[r]|t[r+1]<<8|t[r+2]<<16|t[r+3]<<24)>>>0;return r+=4,e}function o(e){if(g.gzip.hasUnityMarker(t))throw e+'. Failed to parse binary data file, because it is still gzip-compressed and should have been uncompressed by the browser. Web server has likely provided gzip-compressed data without specifying the HTTP Response Header "Content-Encoding: gzip" with it to instruct the browser to decompress it. Please verify your web server hosting configuration.';if(g.br.hasUnityMarker(t))throw e+'. Failed to parse binary data file, because it is still brotli-compressed and should have been uncompressed by the browser. Web server has likely provided brotli-compressed data without specifying the HTTP Response Header "Content-Encoding: br" with it to instruct the browser to decompress it. Please verify your web server hosting configuration.';throw e}var i="UnityWebData1.0\0",a=e.decode(t.subarray(0,i.length)),s=(a!=i&&o('Unknown data format (id="'+a+'")'),r+=i.length,n());for(r+s>t.length&&o("Invalid binary data file header! (pos="+r+", headerSize="+s+", file length="+t.length+")");rt.length&&o("Invalid binary data file size! (offset="+l+", size="+d+", file length="+t.length+")"),n()),u=(r+f>t.length&&o("Invalid binary data file path name! (pos="+r+", length="+f+", file length="+t.length+")"),e.decode(t.subarray(r,r+f)));r+=f;for(var c=0,h=u.indexOf("/",c)+1;0>2],usedWASMHeapSize:b.HEAPU32[t>>2],totalJSHeapSize:b.HEAPF64[r>>3],usedJSHeapSize:b.HEAPF64[n>>3],pageLoadTime:b.HEAPU32[o>>2],pageLoadTimeToFrame1:b.HEAPU32[i>>2],fps:b.HEAPF64[a>>3],movingAverageFps:b.HEAPF64[s>>3],assetLoadTime:b.HEAPU32[l>>2],webAssemblyStartupTime:b.HEAPU32[d>>2]-(b.webAssemblyTimeStart||0),codeDownloadTime:b.HEAPU32[f>>2],gameStartupTime:b.HEAPU32[u>>2],numJankedFrames:b.HEAPU32[u+4>>2]}}};function h(e,t,r){-1==e.indexOf("fullscreen error")&&(b.startupErrorHandler?b.startupErrorHandler(e,t,r):b.errorHandler&&b.errorHandler(e,t,r)||(console.log("Invoking error handler due to\n"+e),"function"==typeof dump&&dump("Invoking error handler due to\n"+e),h.didShowErrorMessage||(-1!=(e="An error occurred running the Unity content on this page. See your browser JavaScript console for more info. The error was:\n"+e).indexOf("DISABLE_EXCEPTION_CATCHING")?e="An exception has occurred, but exception handling has been disabled in this build. If you are the developer of this content, enable exceptions in your project WebGL player settings to be able to catch the exception or see the stack trace.":-1!=e.indexOf("Cannot enlarge memory arrays")?e="Out of memory. If you are the developer of this content, try allocating more memory to your WebGL build in the WebGL player settings.":-1==e.indexOf("Invalid array buffer length")&&-1==e.indexOf("Invalid typed array length")&&-1==e.indexOf("out of memory")&&-1==e.indexOf("could not allocate memory")||(e="The browser could not allocate enough memory for the WebGL content. If you are the developer of this content, try allocating less memory to your WebGL build in the WebGL player settings."),alert(e),h.didShowErrorMessage=!0)))}function m(e,t){if("symbolsUrl"!=e){var r=b.downloadProgress[e],n=(r=r||(b.downloadProgress[e]={started:!1,finished:!1,lengthComputable:!1,total:0,loaded:0}),"object"!=typeof t||"progress"!=t.type&&"load"!=t.type||(r.started||(r.started=!0,r.lengthComputable=t.lengthComputable),r.total=t.total,r.loaded=t.loaded,"load"==t.type&&(r.finished=!0)),0),o=0,i=0,a=0,s=0;for(e in b.downloadProgress){if(!(r=b.downloadProgress[e]).started)return;i++,r.lengthComputable?(n+=r.loaded,o+=r.total,a++):r.finished||s++}l(.9*(i?(i-s-(o?a*(o-n)/o:0))/i:0))}}b.SystemInfo=function(){var e,t,r,n,o=navigator.userAgent+" ",i=[["Firefox","Firefox"],["OPR","Opera"],["Edg","Edge"],["SamsungBrowser","Samsung Browser"],["Trident","Internet Explorer"],["MSIE","Internet Explorer"],["Chrome","Chrome"],["CriOS","Chrome on iOS Safari"],["FxiOS","Firefox on iOS Safari"],["Safari","Safari"]];function a(e,t,r){return(e=RegExp(e,"i").exec(t))&&e[r]}for(var s=0;s>>6:(r<65536?t[o++]=224|r>>>12:(t[o++]=240|r>>>18,t[o++]=128|r>>>12&63),t[o++]=128|r>>>6&63),t[o++]=128|63&r);return t},r.buf2binstring=function(e){return f(e,e.length)},r.binstring2buf=function(e){for(var t=new l.Buf8(e.length),r=0,n=t.length;r>10&1023,i[a++]=56320|1023&r)}return f(i,a)},r.utf8border=function(e,t){for(var r=(t=(t=t||e.length)>e.length?e.length:t)-1;0<=r&&128==(192&e[r]);)r--;return!(r<0)&&0!==r&&r+d[e[r]]>t?r:t}},"zlib/inflate.js":function(e,t,r){"use strict";var B=e("../utils/common"),L=e("./adler32"),O=e("./crc32"),R=e("./inffast"),I=e("./inftrees"),z=0,F=-2,H=1,n=852,o=592;function N(e){return(e>>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function i(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new B.Buf16(320),this.work=new B.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=H,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new B.Buf32(n),t.distcode=t.distdyn=new B.Buf32(o),t.sane=1,t.back=-1,z):F}function s(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):F}function l(e,t){var r,n;return!e||!e.state||(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=e.wsize?(B.arraySet(e.window,t,r-e.wsize,e.wsize,0),e.wnext=0,e.whave=e.wsize):(n<(o=e.wsize-e.wnext)&&(o=n),B.arraySet(e.window,t,r-n,o,e.wnext),(n-=o)?(B.arraySet(e.window,t,r-n,n,0),e.wnext=n,e.whave=e.wsize):(e.wnext+=o,e.wnext===e.wsize&&(e.wnext=0),e.whave>>8&255,r.check=O(r.check,C,2,0),f=d=0,r.mode=2;else if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&d)<<8)+(d>>8))%31)e.msg="incorrect header check",r.mode=30;else if(8!=(15&d))e.msg="unknown compression method",r.mode=30;else{if(f-=4,_=8+(15&(d>>>=4)),0===r.wbits)r.wbits=_;else if(_>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<<_,e.adler=r.check=1,r.mode=512&d?10:12,f=d=0}}break;case 2:for(;f<16;){if(0===s)break e;s--,d+=n[i++]<>8&1),512&r.flags&&(C[0]=255&d,C[1]=d>>>8&255,r.check=O(r.check,C,2,0)),f=d=0,r.mode=3;case 3:for(;f<32;){if(0===s)break e;s--,d+=n[i++]<>>8&255,C[2]=d>>>16&255,C[3]=d>>>24&255,r.check=O(r.check,C,4,0)),f=d=0,r.mode=4;case 4:for(;f<16;){if(0===s)break e;s--,d+=n[i++]<>8),512&r.flags&&(C[0]=255&d,C[1]=d>>>8&255,r.check=O(r.check,C,2,0)),f=d=0,r.mode=5;case 5:if(1024&r.flags){for(;f<16;){if(0===s)break e;s--,d+=n[i++]<>>8&255,r.check=O(r.check,C,2,0)),f=d=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&((h=s<(h=r.length)?s:h)&&(r.head&&(_=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),B.arraySet(r.head.extra,n,i,h,_)),512&r.flags&&(r.check=O(r.check,n,h,i)),s-=h,i+=h,r.length-=h),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===s)break e;for(h=0;_=n[i+h++],r.head&&_&&r.length<65536&&(r.head.name+=String.fromCharCode(_)),_&&h>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;f<32;){if(0===s)break e;s--,d+=n[i++]<>>=7&f,f-=7&f,r.mode=27;else{for(;f<3;){if(0===s)break e;s--,d+=n[i++]<>>=1)){case 0:r.mode=14;break;case 1:var A,A=P=void 0,P=r;if(D){for(Z=new B.Buf32(512),M=new B.Buf32(32),A=0;A<144;)P.lens[A++]=8;for(;A<256;)P.lens[A++]=9;for(;A<280;)P.lens[A++]=7;for(;A<288;)P.lens[A++]=8;for(I(1,P.lens,0,288,Z,0,P.work,{bits:9}),A=0;A<32;)P.lens[A++]=5;I(2,P.lens,0,32,M,0,P.work,{bits:5}),D=!1}if(P.lencode=Z,P.lenbits=9,P.distcode=M,P.distbits=5,r.mode=20,6!==t)break;d>>>=2,f-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}d>>>=2,f-=2}break;case 14:for(d>>>=7&f,f-=7&f;f<32;){if(0===s)break e;s--,d+=n[i++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&d,f=d=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(h=r.length){if(0===(h=l<(h=s>>=5,f-=5,r.ndist=1+(31&d),d>>>=5,f-=5,r.ncode=4+(15&d),d>>>=4,f-=4,286>>=3,f-=3}for(;r.have<19;)r.lens[T[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=I(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,w=65535&U,!((g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>>=g,f-=g,r.lens[r.have++]=w;else{if(16===w){for(E=g+2;f>>=g,f-=g,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}_=r.lens[r.have-1],h=3+(3&d),d>>>=2,f-=2}else if(17===w){for(E=g+3;f>>=g)),d>>>=3,f=f-g-3}else{for(E=g+7;f>>=g)),d>>>=7,f=f-g-7}if(r.have+h>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;h--;)r.lens[r.have++]=_}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=I(1,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=I(2,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=s&&258<=l){e.next_out=a,e.avail_out=l,e.next_in=i,e.avail_in=s,r.hold=d,r.bits=f,R(e,c),a=e.next_out,o=e.output,l=e.avail_out,i=e.next_in,n=e.input,s=e.avail_in,d=r.hold,f=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;p=(U=r.lencode[d&(1<>>16&255,w=65535&U,!((g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>v)])>>>16&255,w=65535&U,!(v+(g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>>=v,f-=v,r.back+=v}if(d>>>=g,f-=g,r.back+=g,r.length=w,0===p){r.mode=26;break}if(32&p){r.back=-1,r.mode=12;break}if(64&p){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&p,r.mode=22;case 22:if(r.extra){for(E=r.extra;f>>=r.extra,f-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;p=(U=r.distcode[d&(1<>>16&255,w=65535&U,!((g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>v)])>>>16&255,w=65535&U,!(v+(g=U>>>24)<=f);){if(0===s)break e;s--,d+=n[i++]<>>=v,f-=v,r.back+=v}if(d>>>=g,f-=g,r.back+=g,64&p){e.msg="invalid distance code",r.mode=30;break}r.offset=w,r.extra=15&p,r.mode=24;case 24:if(r.extra){for(E=r.extra;f>>=r.extra,f-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===l)break e;if(r.offset>(h=c-l)){if((h=r.offset-h)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}m=h>r.wnext?(h-=r.wnext,r.wsize-h):r.wnext-h,h>r.length&&(h=r.length),b=r.window}else b=o,m=a-r.offset,h=r.length;for(l-=h=l>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:r>>>1;e[t]=r}return e}();t.exports=function(e,t,r,n){var o=s,i=n+r;e^=-1;for(var a=n;a>>8^o[255&(e^t[a])];return-1^e}},"zlib/inffast.js":function(e,t,r){"use strict";t.exports=function(e,t){var r,n,o,i,a,s,l=e.state,d=e.next_in,f=e.input,u=d+(e.avail_in-5),c=e.next_out,h=e.output,m=c-(t-e.avail_out),b=c+(e.avail_out-257),g=l.dmax,p=l.wsize,w=l.whave,v=l.wnext,k=l.window,y=l.hold,_=l.bits,x=l.lencode,S=l.distcode,E=(1<>>=n=r>>>24,_-=n,0==(n=r>>>16&255))h[c++]=65535&r;else{if(!(16&n)){if(0==(64&n)){r=x[(65535&r)+(y&(1<>>=n,_-=n),_<15&&(y+=f[d++]<<_,_+=8,y+=f[d++]<<_,_+=8),r=S[y&U];;){if(y>>>=n=r>>>24,_-=n,!(16&(n=r>>>16&255))){if(0==(64&n)){r=S[(65535&r)+(y&(1<>>=n,_-=n,(n=c-m)>3)<<3))-1,e.next_in=d-=o,e.next_out=c,e.avail_in=dh?(b=L[O+a[v]],T[A+a[v]]):(b=96,0),l=1<<(m=w-S),k=d=1<>S)+(d-=l)]=m<<24|b<<16|g|0,0!==d;);for(l=1<>=1;if(C=0!==l?(C&l-1)+l:0,v++,0==--P[w]){if(w===y)break;w=t[r+a[v]]}if(_e.length||31!=e[0]||139!=e[1])return!1;var n=e[3];if(4&n){if(t+2>e.length)return!1;if((t+=2+e[t]+(e[t+1]<<8))>e.length)return!1}if(8&n){for(;te.length)return!1;t++}return 16&n&&String.fromCharCode.apply(null,e.subarray(t,t+r.length+1))==r+"\0"}},br:{hasUnityMarker:function(e){var t="UnityWeb Compressed Content (brotli)";if(!e.length)return!1;var r=1&e[0]?14&e[0]?4:7:1,n=e[0]&(1<>3);if(commentOffset=1+r+2+1+2+(o<<3)+7>>3,17==n||commentOffset>e.length)return!1;for(var i=n+(6+(o<<4)+(t.length-1<<6)<>>=8)if(e[a]!=(255&i))return!1;return String.fromCharCode.apply(null,e.subarray(commentOffset,commentOffset+t.length))==t}}};function p(t){m(t);var e=b.fetchWithProgress,r=b[t],n=/file:\/\//.exec(r)?"same-origin":void 0;return e(b[t],{method:"GET",companyName:b.companyName,productName:b.productName,productVersion:b.productVersion,control:"no-store",mode:n,onProgress:function(e){m(t,e)}}).then(function(e){return a=e.parsedBody,s=b[t],new Promise(function(e,t){try{for(var r in g){var n,o,i;if(g[r].hasUnityMarker(a))return s&&console.log('You can reduce startup time if you configure your web server to add "Content-Encoding: '+r+'" response header when serving "'+s+'" file.'),(n=g[r]).worker||(o=URL.createObjectURL(new Blob(["this.require = ",n.require.toString(),"; this.decompress = ",n.decompress.toString(),"; this.onmessage = ",function(e){e={id:e.data.id,decompressed:this.decompress(e.data.compressed)};postMessage(e,e.decompressed?[e.decompressed.buffer]:[])}.toString(),"; postMessage({ ready: true });"],{type:"application/javascript"})),n.worker=new Worker(o),n.worker.onmessage=function(e){e.data.ready?URL.revokeObjectURL(o):(this.callbacks[e.data.id](e.data.decompressed),delete this.callbacks[e.data.id])},n.worker.callbacks={},n.worker.nextCallbackId=0),i=n.worker.nextCallbackId++,n.worker.callbacks[i]=e,void n.worker.postMessage({id:i,compressed:a},[a.buffer])}e(a)}catch(e){t(e)}});var a,s}).catch(function(e){var t="Failed to download file "+r;"file:"==location.protocol?d(t+". Loading web pages via a file:// URL without a web server is not supported by this browser. Please use a local development web server to host Unity content, or use the Unity Build and Run option.","error"):console.error(t)})}function w(){var t=performance.now(),m=(Promise.all([p("frameworkUrl").then(function(e){var s=URL.createObjectURL(new Blob([e],{type:"application/javascript"}));return new Promise(function(i,e){var a=document.createElement("script");a.src=s,a.onload=function(){if("undefined"==typeof unityFramework||!unityFramework){var e,t=[["br","br"],["gz","gzip"]];for(e in t){var r,n=t[e];if(b.frameworkUrl.endsWith("."+n[0]))return r="Unable to parse "+b.frameworkUrl+"!","file:"==location.protocol?void d(r+" Loading pre-compressed (brotli or gzip) content via a file:// URL without a web server is not supported by this browser. Please use a local development web server to host compressed Unity content, or use the Unity Build and Run option.","error"):(r+=' This can happen if build compression was enabled but web server hosting the content was misconfigured to not serve the file with HTTP Response Header "Content-Encoding: '+n[1]+'" present. Check browser Console and Devtools Network tab to debug.',"br"==n[0]&&"http:"==location.protocol&&(n=-1!=["localhost","127.0.0.1"].indexOf(location.hostname)?"":"Migrate your server to use HTTPS.",r=/Firefox/.test(navigator.userAgent)?"Unable to parse "+b.frameworkUrl+'! If using custom web server, verify that web server is sending .br files with HTTP Response Header "Content-Encoding: br". Brotli compression may not be supported in Firefox over HTTP connections. '+n+' See https://bugzilla.mozilla.org/show_bug.cgi?id=1670675 for more information.':"Unable to parse "+b.frameworkUrl+'! If using custom web server, verify that web server is sending .br files with HTTP Response Header "Content-Encoding: br". Brotli compression may not be supported over HTTP connections. Migrate your server to use HTTPS.'),void d(r,"error"))}d("Unable to parse "+b.frameworkUrl+"! The file is corrupt, or compression was misconfigured? (check Content-Encoding HTTP Response Header on web server)","error")}var o=unityFramework;unityFramework=null,a.onload=null,URL.revokeObjectURL(s),i(o)},a.onerror=function(e){d("Unable to load file "+b.frameworkUrl+"! Check that the file exists on the remote server. (also check browser Console and Devtools Network tab to debug)","error")},document.body.appendChild(a),b.deinitializers.push(function(){document.body.removeChild(a)})})}),p("codeUrl")]).then(function(e){b.wasmBinary=e[1],e[0](b),b.codeDownloadTimeEnd=performance.now()-t}),performance.now()),e=p("dataUrl");b.preRun.push(function(){b.addRunDependency("dataUrl"),e.then(function(t){var e=new TextDecoder("utf-8"),r=0;function n(){var e=(t[r]|t[r+1]<<8|t[r+2]<<16|t[r+3]<<24)>>>0;return r+=4,e}function o(e){if(g.gzip.hasUnityMarker(t))throw e+'. Failed to parse binary data file, because it is still gzip-compressed and should have been uncompressed by the browser. Web server has likely provided gzip-compressed data without specifying the HTTP Response Header "Content-Encoding: gzip" with it to instruct the browser to decompress it. Please verify your web server hosting configuration.';if(g.br.hasUnityMarker(t))throw e+'. Failed to parse binary data file, because it is still brotli-compressed and should have been uncompressed by the browser. Web server has likely provided brotli-compressed data without specifying the HTTP Response Header "Content-Encoding: br" with it to instruct the browser to decompress it. Please verify your web server hosting configuration.';throw e}var i="UnityWebData1.0\0",a=e.decode(t.subarray(0,i.length)),s=(a!=i&&o('Unknown data format (id="'+a+'")'),r+=i.length,n());for(r+s>t.length&&o("Invalid binary data file header! (pos="+r+", headerSize="+s+", file length="+t.length+")");rt.length&&o("Invalid binary data file size! (offset="+l+", size="+d+", file length="+t.length+")"),n()),u=(r+f>t.length&&o("Invalid binary data file path name! (pos="+r+", length="+f+", file length="+t.length+")"),e.decode(t.subarray(r,r+f)));r+=f;for(var c=0,h=u.indexOf("/",c)+1;0