diff --git a/CHANGELOG.md b/CHANGELOG.md index c69dfe9c18..789b3d5e77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/) - Clarify sonar user token question [#3445](https://github.com/MaibornWolff/codecharta/pull/3445) - Changed the `--user` flag to `--user-token` in SonarImporter [#3445](https://github.com/MaibornWolff/codecharta/pull/3445) - Changed the interactive dialog of `modify` to prompt user for single action to perform [#3448](https://github.com/MaibornWolff/codecharta/pull/3448) +- Selected buildings now keep their label until they are unselected [#3465](https://github.com/MaibornWolff/codecharta/pull/3465) ### Fixed 🐞 diff --git a/visualization/app/codeCharta/ui/codeMap/codeMap.mouseEvent.service.spec.ts b/visualization/app/codeCharta/ui/codeMap/codeMap.mouseEvent.service.spec.ts index 09e36058bf..e53b1a154d 100644 --- a/visualization/app/codeCharta/ui/codeMap/codeMap.mouseEvent.service.spec.ts +++ b/visualization/app/codeCharta/ui/codeMap/codeMap.mouseEvent.service.spec.ts @@ -5,7 +5,6 @@ import { ThreeSceneService } from "./threeViewer/threeSceneService" import { ThreeRendererService } from "./threeViewer/threeRenderer.service" import { ViewCubeMouseEventsService } from "../viewCube/viewCube.mouseEvents.service" import { CodeMapBuilding } from "./rendering/codeMapBuilding" -import { CODE_MAP_BUILDING, CONSTANT_HIGHLIGHT, TEST_FILE_WITH_PATHS, TEST_NODES } from "../../util/dataMocks" import { BlacklistItem, CcState, CodeMapNode, Node } from "../../codeCharta.model" import { NodeDecorator } from "../../util/nodeDecorator" import { klona } from "klona" @@ -19,6 +18,7 @@ import { setRightClickedNodeData } from "../../state/store/appStatus/rightClicke import { State, Store } from "@ngrx/store" import { MockStore, provideMockStore } from "@ngrx/store/testing" import { defaultState } from "../../state/store/state.manager" +import { CODE_MAP_BUILDING, CODE_MAP_BUILDING_TS_NODE, CONSTANT_HIGHLIGHT, TEST_FILE_WITH_PATHS, TEST_NODES } from "../../util/dataMocks" jest.mock("../../state/selectors/accumulatedData/idToNode.selector", () => ({ idToNodeSelector: jest.fn() @@ -521,6 +521,9 @@ describe("codeMapMouseEventService", () => { it("should call selectBuilding when no building is selected", () => { threeSceneService.getSelectedBuilding = jest.fn() + threeSceneService.getLabelForHoveredNode = jest.fn() + threeSceneService.animateLabel = jest.fn() + codeMapMouseEventService["intersectedBuilding"] = codeMapBuilding codeMapMouseEventService.onDocumentMouseUp(event) @@ -530,6 +533,9 @@ describe("codeMapMouseEventService", () => { it("should call selectBuilding when a new building is selected", () => { threeSceneService.getSelectedBuilding = jest.fn().mockReturnValue(new CodeMapBuilding(200, null, null, null)) + threeSceneService.getLabelForHoveredNode = jest.fn() + threeSceneService.animateLabel = jest.fn() + codeMapMouseEventService["intersectedBuilding"] = codeMapBuilding codeMapMouseEventService.onDocumentMouseUp(event) @@ -548,6 +554,9 @@ describe("codeMapMouseEventService", () => { }) it("should not call clearSelection, when the mouse has moved less or exact 3 pixels but a building is currently being clicked upon", () => { + threeSceneService.getLabelForHoveredNode = jest.fn() + threeSceneService.animateLabel = jest.fn() + codeMapMouseEventService.onDocumentMouseMove(event) codeMapMouseEventService.onDocumentMouseDown(event) codeMapMouseEventService.onDocumentMouseMove({ clientX: 10, clientY: 17 } as MouseEvent) @@ -817,7 +826,7 @@ describe("codeMapMouseEventService", () => { threeSceneService.getLabelForHoveredNode = jest.fn() codeMapLabelService.addLeafLabel = jest.fn() - codeMapMouseEventService["drawTemporaryLabelFor"](codeMapBuilding, null) + codeMapMouseEventService["drawTemporaryLabelFor"](codeMapBuilding) const nodeHeight = codeMapBuilding.node.height + Math.abs(codeMapBuilding.node.heightDelta ?? 0) expect(threeSceneService.getLabelForHoveredNode).toHaveBeenCalled() @@ -829,10 +838,76 @@ describe("codeMapMouseEventService", () => { threeSceneService.getLabelForHoveredNode = jest.fn() codeMapLabelService.addLeafLabel = jest.fn() - codeMapMouseEventService["drawTemporaryLabelFor"](codeMapBuilding, null) + codeMapMouseEventService["drawTemporaryLabelFor"](codeMapBuilding) expect(threeSceneService.getLabelForHoveredNode).toHaveBeenCalled() expect(codeMapLabelService.addLeafLabel).toHaveBeenCalledWith(codeMapBuilding.node, 0, true) }) }) + + describe("labelForSelectedBuilding", () => { + it("should create a label when selecting a building", () => { + threeSceneService.getLabelForHoveredNode = jest.fn() + threeSceneService.animateLabel = jest.fn() + codeMapLabelService.addLeafLabel = jest.fn() + + codeMapMouseEventService["drawLabelForSelected"](codeMapBuilding) + + expect(threeSceneService.getLabelForHoveredNode).toHaveBeenCalled() + expect(codeMapLabelService.addLeafLabel).toHaveBeenCalledWith(codeMapBuilding.node, 0, true) + expect(codeMapMouseEventService["temporaryLabelForSelectedBuilding"]).toEqual(codeMapBuilding.node) + }) + + it("should remove the label when a previously selected building is unselected", () => { + threeSceneService.getLabelForHoveredNode = jest.fn() + threeSceneService.animateLabel = jest.fn() + codeMapLabelService.clearTemporaryLabel = jest.fn() + codeMapMouseEventService["drawLabelForSelected"](codeMapBuilding) + + codeMapMouseEventService["clearLabelForSelected"]() + + expect(codeMapLabelService.clearTemporaryLabel).toHaveBeenCalledWith(codeMapBuilding.node) + expect(codeMapMouseEventService["temporaryLabelForSelectedBuilding"]).toBeNull() + }) + + it("should remove the old and create the new label when selected building is changed", () => { + const oldSelection = codeMapBuilding + const newSelection = CODE_MAP_BUILDING_TS_NODE + + threeSceneService.getLabelForHoveredNode = jest.fn() + threeSceneService.animateLabel = jest.fn() + codeMapLabelService.clearTemporaryLabel = jest.fn() + codeMapLabelService.addLeafLabel = jest.fn() + + codeMapMouseEventService["drawLabelForSelected"](codeMapBuilding) + + codeMapMouseEventService["intersectedBuilding"] = newSelection + codeMapMouseEventService["onLeftClick"]() + + expect(codeMapMouseEventService["temporaryLabelForSelectedBuilding"]).not.toEqual(oldSelection.node) + expect(codeMapMouseEventService["temporaryLabelForSelectedBuilding"]).toEqual(newSelection.node) + expect(codeMapLabelService.clearTemporaryLabel).toHaveBeenCalledWith(codeMapBuilding.node) + expect(codeMapLabelService.addLeafLabel).toHaveBeenCalledWith(oldSelection.node, 0, true) + expect(codeMapLabelService.addLeafLabel).toHaveBeenCalledWith(newSelection.node, 0, true) + expect(codeMapLabelService.addLeafLabel).toHaveBeenCalledTimes(2) + }) + + it("should keep the label when clicking on the already selected building", () => { + threeSceneService.getLabelForHoveredNode = jest.fn() + threeSceneService.animateLabel = jest.fn() + codeMapMouseEventService["clearTemporaryLabel"] = jest.fn() + codeMapMouseEventService["drawLabelForSelected"] = jest.fn() + + codeMapMouseEventService["drawLabelForSelected"](codeMapBuilding) + const referenceLabel = codeMapMouseEventService["temporaryLabelForSelectedBuilding"] + + codeMapMouseEventService["intersectedBuilding"] = codeMapBuilding + codeMapMouseEventService["onLeftClick"]() + + expect(codeMapMouseEventService["drawLabelForSelected"]).toHaveBeenCalledWith(codeMapBuilding) + expect(codeMapMouseEventService["drawLabelForSelected"]).toHaveBeenCalledTimes(2) + expect(codeMapMouseEventService["clearTemporaryLabel"]).not.toHaveBeenCalled() + expect(codeMapMouseEventService["temporaryLabelForSelectedBuilding"]).toEqual(referenceLabel) + }) + }) }) diff --git a/visualization/app/codeCharta/ui/codeMap/codeMap.mouseEvent.service.ts b/visualization/app/codeCharta/ui/codeMap/codeMap.mouseEvent.service.ts index 76a9f38d8e..25b76a16ad 100755 --- a/visualization/app/codeCharta/ui/codeMap/codeMap.mouseEvent.service.ts +++ b/visualization/app/codeCharta/ui/codeMap/codeMap.mouseEvent.service.ts @@ -51,6 +51,7 @@ export class CodeMapMouseEventService implements OnDestroy { private isMoving = false private raycaster = new Raycaster() private temporaryLabelForBuilding = null + private temporaryLabelForSelectedBuilding = null private subscriptions = [ this.store .select(visibleFileStatesSelector) @@ -216,17 +217,14 @@ export class CodeMapMouseEventService implements OnDestroy { const to = this.intersectedBuilding if (from?.id !== to?.id) { - if (this.temporaryLabelForBuilding !== null) { - this.codeMapLabelService.clearTemporaryLabel(this.temporaryLabelForBuilding) - this.temporaryLabelForBuilding = null - } + this.clearTemporaryLabel() this.threeSceneService.resetLabel() this.unhoverBuilding() if (to && !this.isGrabbingOrMoving()) { if (to.node.isLeaf) { const labelForBuilding = - this.threeSceneService.getLabelForHoveredNode(to, labels) ?? this.drawTemporaryLabelFor(to, labels) + this.threeSceneService.getLabelForHoveredNode(to, labels) ?? this.drawTemporaryLabelFor(to) this.threeSceneService.animateLabel(labelForBuilding, this.raycaster, labels) } this.hoverBuilding(to) @@ -236,11 +234,11 @@ export class CodeMapMouseEventService implements OnDestroy { } } - private drawTemporaryLabelFor(codeMapBuilding: CodeMapBuilding, labels: Object3D[]) { + private drawTemporaryLabelFor(codeMapBuilding: CodeMapBuilding) { const enforceLabel = true this.codeMapLabelService.addLeafLabel(codeMapBuilding.node, 0, enforceLabel) - labels = this.threeSceneService.labels?.children + const labels = this.threeSceneService.labels?.children const labelForBuilding = this.threeSceneService.getLabelForHoveredNode(codeMapBuilding, labels) this.temporaryLabelForBuilding = codeMapBuilding.node @@ -355,14 +353,42 @@ export class CodeMapMouseEventService implements OnDestroy { if (!this.hasMouseMovedMoreThanThreePixels(this.mouseOnLastClick)) { if (this.intersectedBuilding) { this.threeSceneService.selectBuilding(this.intersectedBuilding) + this.drawLabelForSelected(this.intersectedBuilding) } else { this.threeSceneService.clearSelection() + this.clearLabelForSelected() } this.threeSceneService.clearConstantHighlight() } this.threeRendererService.render() } + private drawLabelForSelected(codeMapBuilding: CodeMapBuilding) { + this.clearTemporaryLabel() + if (this.temporaryLabelForSelectedBuilding !== null) { + this.codeMapLabelService.clearTemporaryLabel(this.temporaryLabelForSelectedBuilding) + } + if (!codeMapBuilding.node.isLeaf) { + return + } + + this.codeMapLabelService.addLeafLabel(codeMapBuilding.node, 0, true) + + const labels = this.threeSceneService.labels?.children + const labelForBuilding = this.threeSceneService.getLabelForHoveredNode(codeMapBuilding, labels) + this.threeSceneService.animateLabel(labelForBuilding, this.raycaster, labels) + + this.temporaryLabelForSelectedBuilding = codeMapBuilding.node + return labelForBuilding + } + + private clearLabelForSelected() { + if (this.temporaryLabelForSelectedBuilding !== null) { + this.codeMapLabelService.clearTemporaryLabel(this.temporaryLabelForSelectedBuilding) + this.temporaryLabelForSelectedBuilding = null + } + } + private hasMouseMovedMoreThanThreePixels({ x, y }: Coordinates) { return ( Math.abs(this.mouse.x - x) > this.THRESHOLD_FOR_MOUSE_MOVEMENT_TRACKING ||