diff --git a/apps/web/src/components/DiagramEditorPane.test.mjs b/apps/web/src/components/DiagramEditorPane.test.mjs index 6e115576..dc856eb9 100644 --- a/apps/web/src/components/DiagramEditorPane.test.mjs +++ b/apps/web/src/components/DiagramEditorPane.test.mjs @@ -171,7 +171,8 @@ describe("diagram editor canvas surface", () => { expect(toolbarSource).toContain("showTheme = true"); expect(source).toContain("maxScale: policy.maxScale"); expect(source).not.toContain("minScale: policy.minScale"); - expect(source).toContain("graph.centerContent()"); + expect(source).toContain("centerDiagramContent(graph)"); + expect(source).toContain("scroller.centerContent()"); expect(source).not.toContain("desiredLeft - contentLeft"); expect(source).not.toContain("visibleNodes.length !== graph.getNodes().length"); expect(source).toContain("bindDiagramScrollerFit(graph)"); @@ -179,9 +180,9 @@ describe("diagram editor canvas surface", () => { expect(source).toContain("diagramNodeBounds(graph)"); expect(source).toContain("graph.scale().sx < policy.minScale"); expect(source).toContain("readFlowchart(graph, document, container)"); - expect(source).toContain("scroller?.container.clientHeight"); - expect(source).toContain("graph.centerPoint(box.x + box.width / 2, box.y + viewportHeight / 2 - 48)"); - expect(source).toContain("graph.zoomToRect(bounds, { padding, maxScale: policy.maxScale })"); + expect(source).toContain("scroller.positionPoint({ x: box.x + box.width / 2, y: box.y }, \"50%\", 48)"); + expect(source).toContain("fitDiagramRect(graph, bounds, { padding, maxScale: policy.maxScale })"); + expect(source).toContain("scroller.zoomToRect(bounds, options)"); expect(source).toContain("getDiagramLayoutViewport(document.kind)"); expect(source).toContain("fitDiagramContent(graph, document, containerRef.current);"); }); diff --git a/apps/web/src/components/DiagramEditorPane.tsx b/apps/web/src/components/DiagramEditorPane.tsx index d8d8c8a5..95343f18 100644 --- a/apps/web/src/components/DiagramEditorPane.tsx +++ b/apps/web/src/components/DiagramEditorPane.tsx @@ -645,13 +645,43 @@ const ensureDiagramPaperContainsNodes = (graph: Graph) => { if (!bounds) return; graph.transform.fitToContent({ allowNewOrigin: "any", - padding: 32, + padding: 48, gridWidth: 1, gridHeight: 1, contentArea: bounds, }); }; +const zoomDiagram = (graph: Graph, factor: number, absolute = false) => { + const scroller = getDiagramScroller(graph); + if (scroller) { + if (absolute) scroller.zoomTo(factor); + else scroller.zoom(factor); + return; + } + if (absolute) graph.zoomTo(factor); + else graph.zoom(factor); +}; + +const fitDiagramRect = ( + graph: Graph, + bounds: { x: number; y: number; width: number; height: number }, + options: { padding: number; maxScale: number }, +) => { + const scroller = getDiagramScroller(graph); + if (scroller) scroller.zoomToRect(bounds, options); + else graph.zoomToRect(bounds, options); +}; + +const centerDiagramContent = (graph: Graph) => { + const scroller = getDiagramScroller(graph); + if (scroller) { + scroller.centerContent(); + return; + } + graph.centerContent(); +}; + const diagramClientToLocalPoint = (graph: Graph, point: { x: number; y: number }) => { const anchorNode = graph.getNodes().find((node) => node.isVisible()); const anchorView = anchorNode ? graph.findViewByCell(anchorNode) : null; @@ -1103,14 +1133,17 @@ const readFlowchart = (graph: Graph, document: DiagramDocument, container: HTMLE const start = document.nodes.find((node) => !incoming.has(node.id)) ?? document.nodes[0]; const cell = graph.getCellById(start.id); if (!cell?.isNode()) return; - graph.zoomTo(1); + ensureDiagramPaperContainsNodes(graph); + zoomDiagram(graph, 1, true); + ensureDiagramPaperContainsNodes(graph); const box = cell.getBBox(); const scroller = getDiagramScroller(graph); - const viewportHeight = scroller?.container.clientHeight - || container?.clientHeight - || graph.container.clientHeight - || 640; - graph.centerPoint(box.x + box.width / 2, box.y + viewportHeight / 2 - 48); + if (scroller) { + scroller.positionPoint({ x: box.x + box.width / 2, y: box.y }, "50%", 48); + } else { + graph.centerPoint(box.x + box.width / 2, box.y); + } + ensureDiagramPaperContainsNodes(graph); }; const fitDiagramContent = ( @@ -1123,9 +1156,11 @@ const fitDiagramContent = ( const policy = viewport ?? getDiagramLayoutViewport(document.kind); const bounds = diagramNodeBounds(graph); if (!bounds) return; + ensureDiagramPaperContainsNodes(graph); // Fit every node, including mind-map branches left of the root. Zooming to a // visible subset or to edge paths lets Scroller shrink the paper and clip. - graph.zoomToRect(bounds, { padding, maxScale: policy.maxScale }); + fitDiagramRect(graph, bounds, { padding, maxScale: policy.maxScale }); + ensureDiagramPaperContainsNodes(graph); if ( document.kind === "flowchart" && policy.minScale != null @@ -1929,8 +1964,10 @@ export const DiagramEditorPane = ({ }); graph.bindKey("1", (event) => { event.preventDefault(); - graph.zoomTo(1); - graph.centerContent(); + ensureDiagramPaperContainsNodes(graph); + zoomDiagram(graph, 1, true); + ensureDiagramPaperContainsNodes(graph); + centerDiagramContent(graph); }); graph.bindKey("esc", (event) => { if (!flowQuickCreateRef.current) return; @@ -2821,12 +2858,26 @@ export const DiagramEditorPane = ({ onResetZoom={() => { const graph = graphRef.current; if (!graph) return; - graph.zoomTo(1); + ensureDiagramPaperContainsNodes(graph); + zoomDiagram(graph, 1, true); + ensureDiagramPaperContainsNodes(graph); const bounds = diagramNodeBounds(graph); - if (bounds) graph.centerPoint(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + const scroller = getDiagramScroller(graph); + if (bounds && scroller) scroller.centerPoint(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + else if (bounds) graph.centerPoint(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + }} + onZoomIn={() => { + const graph = graphRef.current; + if (!graph) return; + zoomDiagram(graph, 0.1); + ensureDiagramPaperContainsNodes(graph); + }} + onZoomOut={() => { + const graph = graphRef.current; + if (!graph) return; + zoomDiagram(graph, -0.1); + ensureDiagramPaperContainsNodes(graph); }} - onZoomIn={() => graphRef.current?.zoom(0.1)} - onZoomOut={() => graphRef.current?.zoom(-0.1)} readOnly={readOnly} selectionEditor={( <>