From 2e9b63fe9c754feedf7d88f1ef00d015c6fcf9da Mon Sep 17 00:00:00 2001 From: tianma-if Date: Thu, 10 Sep 2026 23:49:24 +0800 Subject: [PATCH] fix(diagram): keep large notes readable instead of fitting to a stamp Compact diagrams still open at 100%. If fitting everything would shrink below reading size, stay at 100% and start from the origin. Also drop the zoom-percent tooltip. --- .../src/components/DiagramEditorPane.test.mjs | 9 ++- apps/web/src/components/DiagramEditorPane.tsx | 20 +++---- apps/web/src/components/DiagramToolbar.tsx | 59 +++++++++---------- apps/web/src/lib/diagram-layout.test.mjs | 8 +-- .../src/diagram-flowchart-style.test.mjs | 11 ++-- .../shared/src/diagram-flowchart-style.ts | 7 +-- packages/shared/src/diagram-layout.ts | 1 - packages/shared/src/i18n/en-US.ts | 1 - packages/shared/src/i18n/zh-CN.ts | 1 - 9 files changed, 52 insertions(+), 65 deletions(-) diff --git a/apps/web/src/components/DiagramEditorPane.test.mjs b/apps/web/src/components/DiagramEditorPane.test.mjs index 2a465fef..b035c7cf 100644 --- a/apps/web/src/components/DiagramEditorPane.test.mjs +++ b/apps/web/src/components/DiagramEditorPane.test.mjs @@ -204,7 +204,10 @@ describe("diagram editor canvas surface", () => { expect(source).toContain("diagramNodeBounds(graph)"); expect(source).toContain("flowchartFitsReadableViewport(bounds, size, padding, minScale, policy.maxScale)"); expect(source).toContain("readDiagramContent(graph, document)"); + expect(source).toContain("readDiagramContent(graphRef.current, document)"); expect(source).toContain("diagramReaderFocusNode(document)"); + expect(source).toContain("zoomDiagram(graph, 1, true)"); + expect(source).not.toContain('document.kind === "flowchart" ? 1 : minScale'); expect(source).toContain("scroller.positionPoint({ x: box.x + box.width / 2, y: box.y }, \"50%\", 48)"); expect(source).toContain('policy.anchor === "leftmost"'); expect(source).toContain("scroller.positionPoint({ x: origin.x, y: origin.y }, 40, 48)"); @@ -212,7 +215,7 @@ describe("diagram editor canvas surface", () => { expect(source).toContain("scroller.zoomToRect(bounds, options)"); expect(source).toContain("getDiagramLayoutViewport(document.kind)"); expect(source).toContain("fitDiagramContent(graph, document, containerRef.current);"); - expect(source).toContain("fitDiagramContent(graph, document, containerRef.current, 40, layout.viewport, { readable: true })"); + expect(source).toContain("fitDiagramContent(graph, document, containerRef.current, 40, layout.viewport)"); }); test("labels auto layout directly instead of relying on an ambiguous icon", () => { @@ -226,14 +229,14 @@ describe("diagram editor canvas surface", () => { expect(toolbarSource).not.toContain("onFit"); expect(toolbarSource).not.toContain('t("diagram.fit")'); expect(source).not.toContain("onFit={() =>"); - expect(source).toContain("fitDiagramContent(graph, document, containerRef.current, 40, layout.viewport, { readable: true });"); + expect(source).toContain("fitDiagramContent(graph, document, containerRef.current, 40, layout.viewport);"); }); test("lets the zoom percent be typed instead of only resetting to 100%", () => { expect(toolbarSource).toContain("onZoomTo"); expect(toolbarSource).toContain("parseDiagramZoomPercent"); expect(toolbarSource).toContain('t("diagram.zoomPercent")'); - expect(toolbarSource).toContain('t("diagram.zoomPercentHint")'); + expect(toolbarSource).not.toContain('t("diagram.zoomPercentHint")'); expect(toolbarSource).toContain("onPointerDown={(event) => event.stopPropagation()}"); expect(toolbarSource).not.toContain("onResetZoom"); expect(source).toContain("onZoomTo={(percent) =>"); diff --git a/apps/web/src/components/DiagramEditorPane.tsx b/apps/web/src/components/DiagramEditorPane.tsx index ce7c7de0..d16f3c34 100644 --- a/apps/web/src/components/DiagramEditorPane.tsx +++ b/apps/web/src/components/DiagramEditorPane.tsx @@ -1153,11 +1153,10 @@ const diagramViewportSize = (graph: Graph, container: HTMLElement | null) => { const readDiagramContent = (graph: Graph, document: DiagramDocument) => { const policy = getDiagramLayoutViewport(document.kind); - const minScale = policy.minScale ?? 1; const focus = diagramReaderFocusNode(document); const cell = focus ? graph.getCellById(focus.id) : null; ensureDiagramPaperContainsNodes(graph); - zoomDiagram(graph, document.kind === "flowchart" ? 1 : minScale, true); + zoomDiagram(graph, 1, true); ensureDiagramPaperContainsNodes(graph); if (!cell?.isNode()) { centerDiagramContent(graph); @@ -1188,19 +1187,16 @@ const fitDiagramContent = ( container: HTMLElement | null, padding = 32, viewport?: DiagramLayoutViewport, - options: { readable?: boolean } = {}, ) => { const policy = viewport ?? getDiagramLayoutViewport(document.kind); const bounds = diagramNodeBounds(graph); if (!bounds) return; ensureDiagramPaperContainsNodes(graph); - if (options.readable) { - const size = diagramViewportSize(graph, container); - const minScale = policy.minScale ?? 1; - if (size && !flowchartFitsReadableViewport(bounds, size, padding, minScale, policy.maxScale)) { - readDiagramContent(graph, document); - return; - } + const size = diagramViewportSize(graph, container); + const minScale = policy.minScale ?? 1; + if (size && !flowchartFitsReadableViewport(bounds, size, padding, minScale, policy.maxScale)) { + readDiagramContent(graph, document); + return; } // 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. @@ -1695,7 +1691,7 @@ export const DiagramEditorPane = ({ if (graphRef.current !== graph) return false; if (!diagramCanvasIsReady(canvasSurfaceRef.current)) return false; ensureDiagramPaperContainsNodes(graph); - fitDiagramContent(graph, document, containerRef.current, 32, undefined, { readable: true }); + fitDiagramContent(graph, document, containerRef.current, 32); return true; }; settleLoadedViewport(); @@ -2467,7 +2463,7 @@ export const DiagramEditorPane = ({ if (document.kind === "mind-map") applyMindMapHierarchy(graph, themeRef.current, appearanceRef.current, structureRef.current); graph.stopBatch("layout"); ensureDiagramPaperContainsNodes(graph); - fitDiagramContent(graph, document, containerRef.current, 40, layout.viewport, { readable: true }); + fitDiagramContent(graph, document, containerRef.current, 40, layout.viewport); if (changed) { setDirty(savedSnapshotRef.current !== diagramEditorSnapshot( titleRef.current, diff --git a/apps/web/src/components/DiagramToolbar.tsx b/apps/web/src/components/DiagramToolbar.tsx index 6588d87f..1aa58734 100644 --- a/apps/web/src/components/DiagramToolbar.tsx +++ b/apps/web/src/components/DiagramToolbar.tsx @@ -220,38 +220,33 @@ const DiagramZoomPercentField = ({ }; return ( - - - - - {t("diagram.zoomPercentHint")} - + ); }; diff --git a/apps/web/src/lib/diagram-layout.test.mjs b/apps/web/src/lib/diagram-layout.test.mjs index 46158667..ed42d81e 100644 --- a/apps/web/src/lib/diagram-layout.test.mjs +++ b/apps/web/src/lib/diagram-layout.test.mjs @@ -314,23 +314,17 @@ describe("diagram auto layout", () => { expect(Math.hypot(loopCenter.x - b3Center.x, loopCenter.y - b3Center.y)).toBeLessThan(420); }); - test("keeps flowchart viewports at reading size instead of shrinking the whole stack", () => { + test("caps auto-layout zoom at 100% and refuses to shrink below reading size", () => { expect(getDiagramLayoutViewport("flowchart")).toEqual({ anchor: "center", maxScale: 1, minScale: DIAGRAM_READABLE_MIN_SCALE, }); - }); - - test("keeps mind-map viewports at reading size instead of shrinking the whole tree", () => { expect(getDiagramLayoutViewport("mind-map")).toEqual({ anchor: "root", maxScale: 1, minScale: DIAGRAM_READABLE_MIN_SCALE, }); - }); - - test("keeps architecture viewports at reading size instead of shrinking the whole graph", () => { expect(getDiagramLayoutViewport("architecture")).toEqual({ anchor: "leftmost", maxScale: 1, diff --git a/packages/shared/src/diagram-flowchart-style.test.mjs b/packages/shared/src/diagram-flowchart-style.test.mjs index 7e124d0a..79aa4e68 100644 --- a/packages/shared/src/diagram-flowchart-style.test.mjs +++ b/packages/shared/src/diagram-flowchart-style.test.mjs @@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"; import { flowchartNodePresentation } from "./diagram-node-presentation.ts"; import { DIAGRAM_READABLE_MIN_SCALE, - FLOWCHART_READABLE_MIN_SCALE, FLOWCHART_SELECTABLE_THEMES, FLOWCHART_SURFACES, FLOWCHART_THEME_GROUPS, @@ -115,13 +114,17 @@ describe("flowchart edge geometry", () => { describe("flowchart readable viewport", () => { const viewport = { width: 960, height: 720 }; - test("keeps compact flows inside the canvas", () => { + test("keeps compact flows inside the canvas at full size", () => { expect(flowchartFitsReadableViewport({ width: 180, height: 280 }, viewport)).toBe(true); + expect(flowchartFitsReadableViewport({ width: 176, height: 240 }, viewport)).toBe(true); + expect(flowchartFitsReadableViewport({ width: 460, height: 220 }, viewport)).toBe(true); + expect(flowchartFitsReadableViewport({ width: 738, height: 330 }, viewport)).toBe(true); }); - test("refuses to shrink a tall flow below reading size", () => { + test("refuses to shrink a tall map into a postage stamp", () => { expect(flowchartFitsReadableViewport({ width: 220, height: 1680 }, viewport)).toBe(false); - expect(FLOWCHART_READABLE_MIN_SCALE).toBe(DIAGRAM_READABLE_MIN_SCALE); expect(DIAGRAM_READABLE_MIN_SCALE).toBe(0.85); }); }); + + diff --git a/packages/shared/src/diagram-flowchart-style.ts b/packages/shared/src/diagram-flowchart-style.ts index 524bd5f4..295eea0a 100644 --- a/packages/shared/src/diagram-flowchart-style.ts +++ b/packages/shared/src/diagram-flowchart-style.ts @@ -41,16 +41,15 @@ export const flowchartEdgeIsStraight = (source: FlowchartBox, target: FlowchartB export const FLOWCHART_LAYOUT_SPACING = { rank: 56, node: 36 }; -// Auto layout and opening stay at reading size. Shrinking the whole graph into -// the viewport makes large notes unreadable; show the start and let the canvas scroll. +// Auto-fit may shrink the whole graph down to this scale. Below it, keep 100% +// and start from the origin — never compress a large note into a postage stamp. export const DIAGRAM_READABLE_MIN_SCALE = 0.85; -export const FLOWCHART_READABLE_MIN_SCALE = DIAGRAM_READABLE_MIN_SCALE; export const flowchartFitsReadableViewport = ( bounds: { width: number; height: number }, viewport: { width: number; height: number }, padding = 32, - minScale = FLOWCHART_READABLE_MIN_SCALE, + minScale = DIAGRAM_READABLE_MIN_SCALE, maxScale = 1, ) => { const availableWidth = Math.max(viewport.width - padding * 2, 1); diff --git a/packages/shared/src/diagram-layout.ts b/packages/shared/src/diagram-layout.ts index efab1732..3b1d0947 100644 --- a/packages/shared/src/diagram-layout.ts +++ b/packages/shared/src/diagram-layout.ts @@ -5,7 +5,6 @@ export { DIAGRAM_READABLE_MIN_SCALE, FLOWCHART_EDGE_ROUTER, FLOWCHART_LAYOUT_SPACING, - FLOWCHART_READABLE_MIN_SCALE, flowchartEdgeIsStraight, flowchartEdgePorts, flowchartFitsReadableViewport, diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index d26704d3..5cb865fa 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -355,7 +355,6 @@ export const enUS = { zoomIn: "Zoom in", zoomOut: "Zoom out", zoomPercent: "Zoom percent", - zoomPercentHint: "Type 30%–250%, press Enter", listCounts: "{{nodes}} nodes · {{edges}} links", resetZoom: "Reset to 100% and center", readFlow: "Read from start", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 11be2523..07c28404 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -355,7 +355,6 @@ export const zhCN = { zoomIn: "放大", zoomOut: "缩小", zoomPercent: "缩放百分比", - zoomPercentHint: "输入 30%–250%,回车确认", listCounts: "{{nodes}} 节点 · {{edges}} 连线", resetZoom: "恢复 100% 并居中", readFlow: "从起点阅读",