diff --git a/e2e/tests/minimap/MiniMapPageObject.ts b/e2e/tests/minimap/MiniMapPageObject.ts new file mode 100644 index 00000000..ef7935c1 --- /dev/null +++ b/e2e/tests/minimap/MiniMapPageObject.ts @@ -0,0 +1,158 @@ +import { Page } from "@playwright/test"; +import type { GraphPageObject } from "../../page-objects/GraphPageObject"; + +export interface MiniMapLayerOptions { + width?: number; + height?: number; + classNames?: string[]; + cameraBorderSize?: number; + cameraBorderColor?: string; + location?: + | "topLeft" + | "topRight" + | "bottomLeft" + | "bottomRight" + | { top?: string; left?: string; bottom?: string; right?: string }; +} + +export interface MinimapBounds { + x: number; + y: number; + width: number; + height: number; +} + +export interface MinimapPositionRelativeToRoot { + fromLeft: number; + fromTop: number; + fromRight: number; + fromBottom: number; +} + +/** + * Component Object Model for the MiniMap layer. + * Provides helpers to add, interact with and query the minimap canvas element. + */ +export class MiniMapPageObject { + constructor( + private readonly page: Page, + private readonly graphPO: GraphPageObject + ) {} + + /** + * Adds MiniMapLayer to the running graph instance. + * Waits for the initial render to complete. + */ + async addLayer(options: MiniMapLayerOptions = {}): Promise { + await this.page.evaluate((opts) => { + const { MiniMapLayer } = (window as any).GraphModule; + (window as any).minimapLayer = window.graph.addLayer(MiniMapLayer, opts); + }, options as Record); + + await this.graphPO.waitForFrames(5); + } + + /** + * Returns true if the minimap canvas element is present in the DOM. + */ + async exists(): Promise { + return this.page.evaluate(() => Boolean(document.querySelector(".graph-minimap"))); + } + + /** + * Returns the minimap canvas bounding rect in viewport coordinates. + */ + async getCanvasBounds(): Promise { + return this.page.evaluate(() => { + const canvas = document.querySelector(".graph-minimap") as HTMLCanvasElement; + if (!canvas) throw new Error("Minimap canvas not found"); + const rect = canvas.getBoundingClientRect(); + return { x: rect.left, y: rect.top, width: rect.width, height: rect.height }; + }); + } + + /** + * Returns the minimap canvas position relative to the #root element. + * Useful for verifying location props (topLeft, topRight, etc.). + */ + async getPositionRelativeToRoot(): Promise { + return this.page.evaluate(() => { + const canvas = document.querySelector(".graph-minimap") as HTMLCanvasElement; + const root = document.getElementById("root"); + if (!canvas || !root) throw new Error("Canvas or root not found"); + + const canvasRect = canvas.getBoundingClientRect(); + const rootRect = root.getBoundingClientRect(); + + return { + fromLeft: canvasRect.left - rootRect.left, + fromTop: canvasRect.top - rootRect.top, + fromRight: rootRect.right - canvasRect.right, + fromBottom: rootRect.bottom - canvasRect.bottom, + }; + }); + } + + /** + * Returns the rendered CSS size of the minimap canvas (in layout pixels). + */ + async getCanvasSize(): Promise<{ width: number; height: number }> { + return this.page.evaluate(() => { + const canvas = document.querySelector(".graph-minimap") as HTMLCanvasElement; + if (!canvas) throw new Error("Minimap canvas not found"); + const rect = canvas.getBoundingClientRect(); + return { width: Math.round(rect.width), height: Math.round(rect.height) }; + }); + } + + /** + * Clicks at a relative position on the minimap canvas. + * @param relativeX - 0 = left edge, 1 = right edge + * @param relativeY - 0 = top edge, 1 = bottom edge + */ + async clickAt(relativeX: number, relativeY: number): Promise { + const bounds = await this.getCanvasBounds(); + await this.page.mouse.click( + bounds.x + bounds.width * relativeX, + bounds.y + bounds.height * relativeY + ); + await this.graphPO.waitForFrames(3); + } + + /** + * Performs a drag gesture within the minimap canvas. + * Both start and end coordinates are relative (0–1). + */ + async dragFrom( + fromRelX: number, + fromRelY: number, + toRelX: number, + toRelY: number + ): Promise { + const bounds = await this.getCanvasBounds(); + const fromX = bounds.x + bounds.width * fromRelX; + const fromY = bounds.y + bounds.height * fromRelY; + const toX = bounds.x + bounds.width * toRelX; + const toY = bounds.y + bounds.height * toRelY; + + await this.page.mouse.move(fromX, fromY); + await this.page.mouse.down(); + await this.graphPO.waitForFrames(2); + + await this.page.mouse.move(toX, toY, { steps: 10 }); + await this.graphPO.waitForFrames(3); + + await this.page.mouse.up(); + await this.graphPO.waitForFrames(3); + } + + /** + * Checks whether the minimap canvas element has the given CSS class. + */ + async hasClass(className: string): Promise { + return this.page.evaluate((cls) => { + const canvas = document.querySelector(".graph-minimap"); + return canvas ? canvas.classList.contains(cls) : false; + }, className); + } +} diff --git a/e2e/tests/minimap/minimap-graph-changes.spec.ts b/e2e/tests/minimap/minimap-graph-changes.spec.ts new file mode 100644 index 00000000..38f95982 --- /dev/null +++ b/e2e/tests/minimap/minimap-graph-changes.spec.ts @@ -0,0 +1,167 @@ +import { test, expect } from "@playwright/test"; +import { GraphPageObject } from "../../page-objects/GraphPageObject"; +import { MiniMapPageObject } from "./MiniMapPageObject"; + +const INITIAL_BLOCKS = [ + { + id: "block-1", + is: "Block" as const, + x: 0, + y: 0, + width: 200, + height: 100, + name: "Block 1", + anchors: [], + selected: false, + }, + { + id: "block-2", + is: "Block" as const, + x: 600, + y: 600, + width: 200, + height: 100, + name: "Block 2", + anchors: [], + selected: false, + }, +]; + +/** + * A block placed far outside the initial bounding box. + * Adding it causes the usable rect (and therefore minimap scale) to change, + * which in turn changes the world coordinate that any given minimap pixel + * corresponds to. + */ +const FAR_BLOCK = { + id: "block-far", + is: "Block" as const, + x: 5000, + y: 5000, + width: 200, + height: 100, + name: "Far Block", + anchors: [], + selected: false, +}; + +test.describe("MiniMap – graph changes reflection", () => { + let graphPO: GraphPageObject; + let minimapPO: MiniMapPageObject; + + test.beforeEach(async ({ page }) => { + graphPO = new GraphPageObject(page); + minimapPO = new MiniMapPageObject(page, graphPO); + + await graphPO.initialize({ blocks: INITIAL_BLOCKS, connections: [] }); + await minimapPO.addLayer({ location: "topLeft" }); + }); + + test("adding a block far away should update minimap coordinate mapping", async () => { + const camera = graphPO.getCamera(); + + // Click at minimap center with initial blocks; record resulting camera position + await minimapPO.clickAt(0.5, 0.5); + const stateWithInitialBlocks = await camera.getState(); + + // Add a block far outside the current bounding box – + // this expands the usable rect and forces the minimap to rescale + await graphPO.setEntities({ + blocks: [...INITIAL_BLOCKS, FAR_BLOCK], + connections: [], + }); + await graphPO.waitForFrames(5); + + // Click at the same relative position on the now-rescaled minimap + await minimapPO.clickAt(0.5, 0.5); + const stateAfterFarBlock = await camera.getState(); + + // The minimap center now corresponds to a different world position because + // the coordinate mapping changed — camera must have moved to a different place + expect(Math.abs(stateAfterFarBlock.x - stateWithInitialBlocks.x)).toBeGreaterThan(10); + expect(Math.abs(stateAfterFarBlock.y - stateWithInitialBlocks.y)).toBeGreaterThan(10); + }); + + test("removing all blocks should not crash minimap and interaction should still work", async () => { + const camera = graphPO.getCamera(); + + // Remove all blocks + await graphPO.setEntities({ blocks: [], connections: [] }); + await graphPO.waitForFrames(5); + + // Minimap element must still be in the DOM + expect(await minimapPO.exists()).toBe(true); + + // Clicking minimap should not throw and should change camera position + const stateBefore = await camera.getState(); + await minimapPO.clickAt(0.5, 0.5); + const stateAfter = await camera.getState(); + + // Camera may or may not move (empty graph), but no error should occur + // We just verify the page didn't crash (test would fail with an exception otherwise) + expect(typeof stateAfter.x).toBe("number"); + expect(typeof stateAfter.y).toBe("number"); + // With no blocks the usable rect has a default extent so camera should move + expect(stateAfter.x).not.toBeNaN(); + expect(stateAfter.y).not.toBeNaN(); + // Confirm state changed (minimap still routes clicks to camera.move) + const moved = stateAfter.x !== stateBefore.x || stateAfter.y !== stateBefore.y; + expect(moved).toBe(true); + }); + + test("camera pan should not break minimap click interaction", async () => { + const camera = graphPO.getCamera(); + + // Pan the camera significantly + await camera.emulatePan(200, 150); + + // After pan the minimap's camera-frame border moved, but the coordinate + // mapping (scale / relativeX / relativeY) is unchanged. + // Clicking the minimap must still move the camera. + const stateBefore = await camera.getState(); + await minimapPO.clickAt(0.2, 0.2); + const stateAfter = await camera.getState(); + + expect(stateAfter.x).not.toBe(stateBefore.x); + expect(stateAfter.y).not.toBe(stateBefore.y); + }); + + test("camera zoom should not break minimap click interaction", async () => { + const camera = graphPO.getCamera(); + + // Zoom in + await camera.emulateZoom(-300); + + const stateBefore = await camera.getState(); + await minimapPO.clickAt(0.8, 0.8); + const stateAfter = await camera.getState(); + + expect(stateAfter.x).not.toBe(stateBefore.x); + expect(stateAfter.y).not.toBe(stateBefore.y); + }); + + test("moving a block should update minimap coordinate mapping", async () => { + const camera = graphPO.getCamera(); + + // Baseline: click center of minimap + await minimapPO.clickAt(0.5, 0.5); + const stateBaseline = await camera.getState(); + + // Move block-2 far away, forcing usable rect to expand + await graphPO.setEntities({ + blocks: [ + INITIAL_BLOCKS[0], + { ...INITIAL_BLOCKS[1], x: 4000, y: 4000 }, + ], + connections: [], + }); + await graphPO.waitForFrames(5); + + // Click minimap center again – scale changed, so camera ends up elsewhere + await minimapPO.clickAt(0.5, 0.5); + const stateAfterMove = await camera.getState(); + + expect(Math.abs(stateAfterMove.x - stateBaseline.x)).toBeGreaterThan(10); + expect(Math.abs(stateAfterMove.y - stateBaseline.y)).toBeGreaterThan(10); + }); +}); diff --git a/e2e/tests/minimap/minimap-mouse.spec.ts b/e2e/tests/minimap/minimap-mouse.spec.ts new file mode 100644 index 00000000..37990a96 --- /dev/null +++ b/e2e/tests/minimap/minimap-mouse.spec.ts @@ -0,0 +1,125 @@ +import { test, expect } from "@playwright/test"; +import { GraphPageObject } from "../../page-objects/GraphPageObject"; +import { MiniMapPageObject } from "./MiniMapPageObject"; + +/** + * Two blocks spread far apart so the minimap has a meaningful coordinate + * system and clicking different corners produces noticeably different camera + * positions. + */ +const BLOCKS = [ + { + id: "block-1", + is: "Block" as const, + x: 0, + y: 0, + width: 200, + height: 100, + name: "Block 1", + anchors: [], + selected: false, + }, + { + id: "block-2", + is: "Block" as const, + x: 600, + y: 600, + width: 200, + height: 100, + name: "Block 2", + anchors: [], + selected: false, + }, +]; + +test.describe("MiniMap – mouse interactions", () => { + let graphPO: GraphPageObject; + let minimapPO: MiniMapPageObject; + + test.beforeEach(async ({ page }) => { + graphPO = new GraphPageObject(page); + minimapPO = new MiniMapPageObject(page, graphPO); + + await graphPO.initialize({ blocks: BLOCKS, connections: [] }); + await minimapPO.addLayer({ location: "topLeft" }); + }); + + test("should render minimap canvas in the DOM after adding layer", async () => { + expect(await minimapPO.exists()).toBe(true); + }); + + test("clicking on minimap should move camera to corresponding world area", async () => { + const camera = graphPO.getCamera(); + const stateBefore = await camera.getState(); + + // Click on the bottom-right area of the minimap (far from current camera center) + await minimapPO.clickAt(0.85, 0.85); + + const stateAfter = await camera.getState(); + + // Camera position must have changed + expect(stateAfter.x).not.toBe(stateBefore.x); + expect(stateAfter.y).not.toBe(stateBefore.y); + }); + + test("clicking at different minimap positions should move camera to different world positions", async () => { + const camera = graphPO.getCamera(); + + // Click top-left of minimap → camera centers near the top-left world area + await minimapPO.clickAt(0.1, 0.1); + const stateTopLeft = await camera.getState(); + + // Click bottom-right of minimap → camera centers near the bottom-right world area + await minimapPO.clickAt(0.9, 0.9); + const stateBottomRight = await camera.getState(); + + // Camera positions must differ between the two clicks + expect(Math.abs(stateBottomRight.x - stateTopLeft.x)).toBeGreaterThan(10); + expect(Math.abs(stateBottomRight.y - stateTopLeft.y)).toBeGreaterThan(10); + }); + + test("clicking on minimap should not move camera when clicking same position twice", async () => { + const camera = graphPO.getCamera(); + + // First click establishes a camera position + await minimapPO.clickAt(0.5, 0.5); + const stateAfterFirst = await camera.getState(); + + // Second click at the same position – camera should end up at the same place + await minimapPO.clickAt(0.5, 0.5); + const stateAfterSecond = await camera.getState(); + + expect(Math.abs(stateAfterSecond.x - stateAfterFirst.x)).toBeLessThan(5); + expect(Math.abs(stateAfterSecond.y - stateAfterFirst.y)).toBeLessThan(5); + }); + + test("dragging on minimap should move camera continuously", async () => { + const camera = graphPO.getCamera(); + const stateBefore = await camera.getState(); + + // Drag from center toward top-left within the minimap + await minimapPO.dragFrom(0.5, 0.5, 0.1, 0.1); + + const stateAfter = await camera.getState(); + + // Camera must have moved during the drag + expect(stateAfter.x).not.toBe(stateBefore.x); + expect(stateAfter.y).not.toBe(stateBefore.y); + }); + + test("drag in opposite directions should produce different camera positions", async () => { + const camera = graphPO.getCamera(); + + // Drag toward top-left + await minimapPO.dragFrom(0.5, 0.5, 0.1, 0.1); + const stateTopLeft = await camera.getState(); + + // Drag back toward bottom-right from same starting point + await minimapPO.dragFrom(0.5, 0.5, 0.9, 0.9); + const stateBottomRight = await camera.getState(); + + // The resulting camera positions must differ + expect(Math.abs(stateBottomRight.x - stateTopLeft.x)).toBeGreaterThan(10); + expect(Math.abs(stateBottomRight.y - stateTopLeft.y)).toBeGreaterThan(10); + }); +}); diff --git a/e2e/tests/minimap/minimap-styling.spec.ts b/e2e/tests/minimap/minimap-styling.spec.ts new file mode 100644 index 00000000..4bca4c64 --- /dev/null +++ b/e2e/tests/minimap/minimap-styling.spec.ts @@ -0,0 +1,112 @@ +import { test, expect } from "@playwright/test"; +import { GraphPageObject } from "../../page-objects/GraphPageObject"; +import { MiniMapPageObject } from "./MiniMapPageObject"; + +/** Tolerance in pixels when comparing element positions. */ +const POSITION_TOLERANCE = 5; + +const BLOCKS = [ + { + id: "block-1", + is: "Block" as const, + x: 100, + y: 100, + width: 200, + height: 100, + name: "Block 1", + anchors: [], + selected: false, + }, +]; + +test.describe("MiniMap – styling", () => { + let graphPO: GraphPageObject; + let minimapPO: MiniMapPageObject; + + test.beforeEach(async ({ page }) => { + graphPO = new GraphPageObject(page); + minimapPO = new MiniMapPageObject(page, graphPO); + await graphPO.initialize({ blocks: BLOCKS, connections: [] }); + }); + + // ─── CSS class ─────────────────────────────────────────────────────────── + + test("minimap canvas should have graph-minimap CSS class", async () => { + await minimapPO.addLayer(); + expect(await minimapPO.hasClass("graph-minimap")).toBe(true); + }); + + test("additional classNames prop should be added to the minimap element", async () => { + await minimapPO.addLayer({ classNames: ["my-custom-class"] }); + expect(await minimapPO.hasClass("my-custom-class")).toBe(true); + }); + + // ─── Size ───────────────────────────────────────────────────────────────── + + test("default size should be 200×200 pixels", async () => { + await minimapPO.addLayer(); + const size = await minimapPO.getCanvasSize(); + // Default width and height are 200px; the border adds a few pixels to + // the bounding rect so we allow a small tolerance. + expect(size.width).toBeGreaterThanOrEqual(200); + expect(size.width).toBeLessThanOrEqual(210); + expect(size.height).toBeGreaterThanOrEqual(200); + expect(size.height).toBeLessThanOrEqual(210); + }); + + test("custom width and height props should be applied", async () => { + await minimapPO.addLayer({ width: 120, height: 80 }); + const size = await minimapPO.getCanvasSize(); + expect(size.width).toBeGreaterThanOrEqual(120); + expect(size.width).toBeLessThanOrEqual(130); + expect(size.height).toBeGreaterThanOrEqual(80); + expect(size.height).toBeLessThanOrEqual(90); + }); + + // ─── Location ───────────────────────────────────────────────────────────── + + test("default location (no prop) should place minimap at top-left corner", async () => { + await minimapPO.addLayer(); // no location prop → defaults to "topLeft" + const pos = await minimapPO.getPositionRelativeToRoot(); + expect(Math.abs(pos.fromLeft)).toBeLessThanOrEqual(POSITION_TOLERANCE); + expect(Math.abs(pos.fromTop)).toBeLessThanOrEqual(POSITION_TOLERANCE); + }); + + test("topLeft location should place minimap at the top-left corner", async () => { + await minimapPO.addLayer({ location: "topLeft" }); + const pos = await minimapPO.getPositionRelativeToRoot(); + expect(Math.abs(pos.fromLeft)).toBeLessThanOrEqual(POSITION_TOLERANCE); + expect(Math.abs(pos.fromTop)).toBeLessThanOrEqual(POSITION_TOLERANCE); + }); + + test("topRight location should place minimap at the top-right corner", async () => { + await minimapPO.addLayer({ location: "topRight" }); + const pos = await minimapPO.getPositionRelativeToRoot(); + expect(Math.abs(pos.fromRight)).toBeLessThanOrEqual(POSITION_TOLERANCE); + expect(Math.abs(pos.fromTop)).toBeLessThanOrEqual(POSITION_TOLERANCE); + }); + + test("bottomLeft location should place minimap at the bottom-left corner", async () => { + await minimapPO.addLayer({ location: "bottomLeft" }); + const pos = await minimapPO.getPositionRelativeToRoot(); + expect(Math.abs(pos.fromLeft)).toBeLessThanOrEqual(POSITION_TOLERANCE); + expect(Math.abs(pos.fromBottom)).toBeLessThanOrEqual(POSITION_TOLERANCE); + }); + + test("bottomRight location should place minimap at the bottom-right corner", async () => { + await minimapPO.addLayer({ location: "bottomRight" }); + const pos = await minimapPO.getPositionRelativeToRoot(); + expect(Math.abs(pos.fromRight)).toBeLessThanOrEqual(POSITION_TOLERANCE); + expect(Math.abs(pos.fromBottom)).toBeLessThanOrEqual(POSITION_TOLERANCE); + }); + + test("custom location object should place minimap at the specified position", async () => { + // Inset 20px from the right and 30px from the bottom + await minimapPO.addLayer({ + location: { right: "20px", bottom: "30px", top: "unset", left: "unset" }, + }); + const pos = await minimapPO.getPositionRelativeToRoot(); + expect(Math.abs(pos.fromRight - 20)).toBeLessThanOrEqual(POSITION_TOLERANCE); + expect(Math.abs(pos.fromBottom - 30)).toBeLessThanOrEqual(POSITION_TOLERANCE); + }); +}); diff --git a/src/plugins/minimap/layer.ts b/src/plugins/minimap/layer.ts index ba8f5857..03866fec 100644 --- a/src/plugins/minimap/layer.ts +++ b/src/plugins/minimap/layer.ts @@ -1,6 +1,6 @@ import { TGraphLayerContext } from "../../components/canvas/layers/graphLayer/GraphLayer"; import { Layer, LayerContext, LayerProps } from "../../services/Layer"; -import { computeCssVariable, noop } from "../../utils/functions"; +import { computeCssVariable } from "../../utils/functions"; export type TMiniMapLocation = | "topLeft" @@ -23,7 +23,7 @@ export type MiniMapLayerContext = LayerContext & { ctx: CanvasRenderingContext2D; }; -export class MiniMapLayer extends Layer { +export class MiniMapLayer extends Layer { public declare context: Omit; private minimapWidth: number; @@ -33,11 +33,9 @@ export class MiniMapLayer extends Layer private scale: number; private cameraBorderSize: number; private cameraBorderColor: string; - private unSubscribeUsableRectLoaded: typeof noop; constructor(props: MiniMapLayerProps) { - const classNames = Array.isArray(props.classNames) ? props.classNames : []; - classNames.push("graph-minimap"); + const classNames = [...(Array.isArray(props.classNames) ? props.classNames : []), "graph-minimap"]; super({ canvas: { @@ -48,16 +46,74 @@ export class MiniMapLayer extends Layer ...props, }); - this.minimapWidth = this.props.width || 200; - this.minimapHeight = this.props.height || 200; - this.cameraBorderSize = this.props.cameraBorderSize || 2; - this.cameraBorderColor = this.props.cameraBorderColor || "rgba(255, 119, 0, 0.9)"; + this.minimapWidth = this.props.width ?? 200; + this.minimapHeight = this.props.height ?? 200; + this.cameraBorderSize = this.props.cameraBorderSize ?? 2; + this.cameraBorderColor = this.props.cameraBorderColor ?? "rgba(255, 119, 0, 0.9)"; this.relativeX = 0; this.relativeY = 0; this.scale = 1; } protected afterInit(): void { + this.injectPositionStyle(); + + // Fires immediately with the current value — initialises scale/relativeX/Y before the first render. + // Also fires on every subsequent usableRect change (blocks moved/resized/added/removed). + this.onSignal(this.props.graph.hitTest.$usableRect, () => { + this.calculateViewPortCoords(); + this.performRender(); + }); + + this.onGraphEvent("camera-change", () => this.performRender()); + this.onGraphEvent("colors-changed", () => this.performRender()); + + // block-change recalculates coords: blocks may change size/position + // without the usableRect bounding box itself changing. + this.onGraphEvent("block-change", () => { + this.calculateViewPortCoords(); + this.performRender(); + }); + + if (this.canvas) { + this.onCanvasEvent("mousedown", this.handleMouseDownEvent); + } + + super.afterInit(); + } + + protected updateCanvasSize(): void { + const dpr = this.getDRP(); + this.canvas.width = this.minimapWidth * dpr; + this.canvas.height = this.minimapHeight * dpr; + } + + protected willRender(): void { + if (this.firstRender) { + this.canvas.style.width = `${this.minimapWidth}px`; + this.canvas.style.height = `${this.minimapHeight}px`; + } + } + + protected render(): void { + if (!this.context?.ctx) return; + + const usableRect = this.props.graph.api.getUsableRect(); + if (usableRect.width === 0 && usableRect.height === 0) { + this.resetTransform(); + return; + } + + this.resetTransform(); + this.context.ctx.scale(this.scale, this.scale); + this.context.ctx.translate(-this.relativeX, -this.relativeY); + + this.renderUsableRectBelow(); + this.renderBlocks(); + this.drawCameraBorderFrame(); + } + + private injectPositionStyle(): void { const minimapPosition = this.getPositionOfMiniMap(this.props.location); const style = document.createElement("style"); style.innerHTML = ` @@ -66,44 +122,15 @@ export class MiniMapLayer extends Layer left: ${minimapPosition.left}; bottom: ${minimapPosition.bottom}; right: ${minimapPosition.right}; - width: ${this.props.width || 200}px; - height: ${this.props.height || 200}px; + width: ${this.minimapWidth}px; + height: ${this.minimapHeight}px; border: 2px solid var(--g-color-private-cool-grey-1000-solid); background: lightgrey; }`; - this.root.appendChild(style); - - // Set up event subscriptions here if usableRect is already loaded - const usableRect = this.props.graph.api.getUsableRect(); - if (!(usableRect.height === 0 && usableRect.width === 0 && usableRect.x === 0 && usableRect.y === 0)) { - this.calculateViewPortCoords(); - this.rerenderMapContent(); - - // Register event listeners with the graphOn wrapper method for automatic cleanup when unmounted - this.onGraphEvent("camera-change", this.rerenderMapContent); - this.onGraphEvent("colors-changed", this.rerenderMapContent); - this.onGraphEvent("block-change", this.onBlockUpdated); - - // Use canvasOn wrapper method for DOM event listeners to ensure proper cleanup - if (this.canvas) { - this.onCanvasEvent("mousedown", this.handleMouseDownEvent); - } - } - this.onSignal(this.props.graph.hitTest.$usableRect, () => { - this.onBlockUpdated(); - this.calculateViewPortCoords(); - this.rerenderMapContent(); - }); - - super.afterInit(); } - protected updateCanvasSize(): void { - this.rerenderMapContent(); - } - - private calculateViewPortCoords() { + private calculateViewPortCoords(): void { const usableRect = this.props.graph.api.getUsableRect(); const xPos = usableRect.x - this.context.constants.system.USABLE_RECT_GAP; @@ -126,7 +153,7 @@ export class MiniMapLayer extends Layer } // eslint-disable-next-line complexity - private drawCameraBorderFrame() { + private drawCameraBorderFrame(): void { const cameraState = this.props.camera.getCameraState(); const relativeXRight = this.relativeX + this.minimapWidth / this.scale; @@ -219,72 +246,7 @@ export class MiniMapLayer extends Layer return position; } - protected willRender(): void { - if (this.firstRender) { - const canvas = this.getCanvas(); - const dpr = this.getDRP(); - - canvas.width = this.minimapWidth * dpr; - canvas.height = this.minimapHeight * dpr; - - canvas.style.width = `${this.minimapWidth}px`; - canvas.style.height = `${this.minimapHeight}px`; - - this.setContext({ - canvas, - ctx: canvas.getContext("2d"), - camera: this.props.camera, - constants: this.props.graph.graphConstants, - colors: this.props.graph.graphColors, - }); - } - } - - protected didRender(): void { - if (this.firstRender) { - this.unSubscribeUsableRectLoaded = this.props.graph.hitTest.onUsableRectUpdate((usableRect) => { - if (usableRect.height === 0 && usableRect.width === 0 && usableRect.x === 0 && usableRect.y === 0) return; - - this.calculateViewPortCoords(); - this.rerenderMapContent(); - - // If the layer is already attached, set up event subscriptions here - if (this.root) { - // Register event listeners with the graphOn wrapper method for automatic cleanup when unmounted - this.onGraphEvent("camera-change", this.rerenderMapContent); - this.onGraphEvent("colors-changed", this.rerenderMapContent); - this.onGraphEvent("block-change", this.onBlockUpdated); - - // Use canvasOn wrapper method for DOM event listeners to ensure proper cleanup - if (this.canvas) { - this.onCanvasEvent("mousedown", this.handleMouseDownEvent); - } - } - - this.unSubscribeUsableRectLoaded?.(); - }); - } - } - - private onBlockUpdated = () => { - this.calculateViewPortCoords(); - this.rerenderMapContent(); - }; - - private rerenderMapContent = () => { - if (!this.context?.ctx) return; - - this.resetTransform(); - - this.context.ctx.scale(this.scale, this.scale); - this.context.ctx.translate(-this.relativeX, -this.relativeY); - - this.renderUsableRectBelow(); - this.renderBlocks(); - this.drawCameraBorderFrame(); - }; - - private renderUsableRectBelow() { + private renderUsableRectBelow(): void { const usableRect = this.props.graph.api.getUsableRect(); this.context.ctx.fillStyle = computeCssVariable(this.context.colors.canvas.layerBackground); @@ -296,7 +258,7 @@ export class MiniMapLayer extends Layer this.context.ctx.fillRect(xPos, yPos, width, height); } - private renderBlocks() { + private renderBlocks(): void { const blocks = this.props.graph.rootStore.blocksList.$blocks.value; blocks.forEach((block) => { @@ -306,7 +268,7 @@ export class MiniMapLayer extends Layer }); } - private onCameraDrag(event: MouseEvent) { + private onCameraDrag(event: MouseEvent): void { const cameraState = this.props.camera.getCameraState(); const x = -(this.relativeX + event.offsetX / this.scale) + cameraState.relativeWidth / 2; @@ -318,13 +280,13 @@ export class MiniMapLayer extends Layer this.context.camera.move(dx, dy); } - private handleMouseDownEvent = (rootEvent: MouseEvent) => { + private handleMouseDownEvent = (rootEvent: MouseEvent): void => { rootEvent.stopPropagation(); this.onCameraDrag(rootEvent); this.context.graph.dragService.startDrag( { onUpdate: (event: MouseEvent) => this.onCameraDrag(event) }, - { stopOnMouseLeave: true } + { stopOnMouseLeave: true, autopanning: false, cursor: "move" } ); }; }