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.
This commit is contained in:
1 parent
767f4638cf
commit
2e9b63fe9c
9 files changed
+52
-65
No files matched your search
@@ -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) =>");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -220,38 +220,33 @@ const DiagramZoomPercentField = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip open={focused || undefined}>
|
||||
<TooltipTrigger asChild>
|
||||
<label
|
||||
className="inline-flex h-8 w-16 cursor-text items-center justify-center gap-0 rounded-md text-slate-600 hover:bg-slate-50 hover:text-slate-900 focus-within:bg-slate-50 focus-within:text-slate-900"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
aria-label={t("diagram.zoomPercent")}
|
||||
autoComplete="off"
|
||||
className="h-8 w-7 bg-transparent pr-0 text-right text-xs tabular-nums outline-none"
|
||||
inputMode="numeric"
|
||||
spellCheck={false}
|
||||
value={draft}
|
||||
onBlur={() => {
|
||||
if (commitOnBlurRef.current) commit();
|
||||
else commitOnBlurRef.current = true;
|
||||
setFocused(false);
|
||||
}}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onFocus={(event) => {
|
||||
setFocused(true);
|
||||
event.currentTarget.select();
|
||||
}}
|
||||
onKeyDown={keepCanvasShortcutsOut}
|
||||
onKeyUp={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
/>
|
||||
<span aria-hidden="true" className="text-xs">%</span>
|
||||
</label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("diagram.zoomPercentHint")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<label
|
||||
className="inline-flex h-8 w-16 cursor-text items-center justify-center gap-0 rounded-md text-slate-600 hover:bg-slate-50 hover:text-slate-900 focus-within:bg-slate-50 focus-within:text-slate-900"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
aria-label={t("diagram.zoomPercent")}
|
||||
autoComplete="off"
|
||||
className="h-8 w-7 bg-transparent pr-0 text-right text-xs tabular-nums outline-none"
|
||||
inputMode="numeric"
|
||||
spellCheck={false}
|
||||
value={draft}
|
||||
onBlur={() => {
|
||||
if (commitOnBlurRef.current) commit();
|
||||
else commitOnBlurRef.current = true;
|
||||
setFocused(false);
|
||||
}}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onFocus={(event) => {
|
||||
setFocused(true);
|
||||
event.currentTarget.select();
|
||||
}}
|
||||
onKeyDown={keepCanvasShortcutsOut}
|
||||
onKeyUp={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
/>
|
||||
<span aria-hidden="true" className="text-xs">%</span>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,7 +5,6 @@ export {
|
||||
DIAGRAM_READABLE_MIN_SCALE,
|
||||
FLOWCHART_EDGE_ROUTER,
|
||||
FLOWCHART_LAYOUT_SPACING,
|
||||
FLOWCHART_READABLE_MIN_SCALE,
|
||||
flowchartEdgeIsStraight,
|
||||
flowchartEdgePorts,
|
||||
flowchartFitsReadableViewport,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -355,7 +355,6 @@ export const zhCN = {
|
||||
zoomIn: "放大",
|
||||
zoomOut: "缩小",
|
||||
zoomPercent: "缩放百分比",
|
||||
zoomPercentHint: "输入 30%–250%,回车确认",
|
||||
listCounts: "{{nodes}} 节点 · {{edges}} 连线",
|
||||
resetZoom: "恢复 100% 并居中",
|
||||
readFlow: "从起点阅读",
|
||||
|
||||
Reference in new issue
Block a user