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 ( +