desktop-readability.mjs
132 lines · archify/renderers/shared/desktop-readability.mjs at a406594
1export const DESKTOP_READABILITY_VIEWPORT = Object.freeze({ width: 1440, height: 900 });
2export const DESKTOP_READER_MIN_WIDTH = 960;
3export const DESKTOP_READER_HORIZONTAL_CHROME = 30;
4export const DESKTOP_READER_DIAGRAM_WIDTH = DESKTOP_READER_MIN_WIDTH - DESKTOP_READER_HORIZONTAL_CHROME;
5export const MIN_PROJECTED_NODE_TEXT_PX = 6;
6export const DECLARED_WIDE_READER_CONTRACT = 'declared-wide-v1';
7export const DECLARED_WIDE_READER_RATIO = 1.55;
8export const DECLARED_WIDE_READER_MAX_WIDTH = 1920;
9export const DECLARED_WIDE_REFERENCE_BODY_HORIZONTAL_PX = 64;
10export const DECLARED_WIDE_REFERENCE_DIAGRAM_HORIZONTAL_PX = 30;
12export function projectedNodeTextPx(sourceFontPx, viewBoxWidth, diagramWidth = DESKTOP_READER_DIAGRAM_WIDTH) {
13 if (![sourceFontPx, viewBoxWidth, diagramWidth].every(Number.isFinite) || viewBoxWidth <= 0 || diagramWidth <= 0) {
14 return Number.NaN;
15 }
16 return sourceFontPx * Math.min(1, diagramWidth / viewBoxWidth);
17}
19export function minimumReadableSourceTextPx(
20 viewBoxWidth,
21 diagramWidth = DESKTOP_READER_DIAGRAM_WIDTH,
22 minimumProjectedPx = MIN_PROJECTED_NODE_TEXT_PX,
23) {
24 if (![viewBoxWidth, diagramWidth, minimumProjectedPx].every(Number.isFinite)
25 || viewBoxWidth <= 0
26 || diagramWidth <= 0
27 || minimumProjectedPx <= 0) {
28 return Number.NaN;
29 }
30 return minimumProjectedPx / Math.min(1, diagramWidth / viewBoxWidth);
31}
33// This is deliberately separate from the legacy 930px projection. Architecture
34// boundary convergence depends on that legacy default, while only a recognized
35// v2 wide Reader may use this declared-width proof.
36export function declaredWideReadabilityBudget({
37 viewBoxWidth,
38 viewBoxHeight,
39 minimumSourceTextPx,
40 requestedMinimumTextPx,
41 viewportWidth = DESKTOP_READABILITY_VIEWPORT.width,
42 bodyHorizontalPx = DECLARED_WIDE_REFERENCE_BODY_HORIZONTAL_PX,
43 diagramHorizontalPx = DECLARED_WIDE_REFERENCE_DIAGRAM_HORIZONTAL_PX,
44 minimumReaderWidth = DESKTOP_READER_MIN_WIDTH,
45 maximumReaderWidth = DECLARED_WIDE_READER_MAX_WIDTH,
46} = {}) {
47 const values = [
48 viewBoxWidth, viewBoxHeight, minimumSourceTextPx, requestedMinimumTextPx,
49 viewportWidth, bodyHorizontalPx, diagramHorizontalPx, minimumReaderWidth, maximumReaderWidth,
50 ];
51 if (!values.every(Number.isFinite) || viewBoxWidth <= 0 || viewBoxHeight <= 0
52 || minimumSourceTextPx <= 0 || requestedMinimumTextPx <= 0 || viewportWidth <= 0
53 || bodyHorizontalPx < 0 || diagramHorizontalPx < 0 || minimumReaderWidth <= 0
54 || maximumReaderWidth < minimumReaderWidth || viewBoxWidth / viewBoxHeight < DECLARED_WIDE_READER_RATIO) {
55 return null;
56 }
57 const requestedTargetPx = Math.max(MIN_PROJECTED_NODE_TEXT_PX, requestedMinimumTextPx);
58 const requestedScale = Math.min(1, requestedTargetPx / minimumSourceTextPx);
59 const desiredReaderWidth = Math.max(minimumReaderWidth, viewBoxWidth * requestedScale + diagramHorizontalPx);
60 const viewportCap = Math.max(0, viewportWidth - bodyHorizontalPx);
61 const cap = Math.min(maximumReaderWidth, viewportCap);
62 const actualReaderWidth = Math.min(desiredReaderWidth, cap);
63 const guaranteedSvgWidth = Math.max(0, actualReaderWidth - diagramHorizontalPx);
64 const projectedMinimumTextPx = projectedNodeTextPx(minimumSourceTextPx, viewBoxWidth, guaranteedSvgWidth);
65 const limit = actualReaderWidth < desiredReaderWidth
66 ? (viewportCap <= maximumReaderWidth ? 'viewport-cap' : 'reader-cap')
67 : 'source-size';
68 return {
69 requestedTargetPx,
70 requestedScale,
71 desiredReaderWidth,
72 viewportCap,
73 maximumReaderWidth,
74 actualReaderWidth,
75 guaranteedSvgWidth,
76 projectedMinimumTextPx,
77 hardFloorPx: MIN_PROJECTED_NODE_TEXT_PX,
78 hardFloorMet: projectedMinimumTextPx >= MIN_PROJECTED_NODE_TEXT_PX,
79 requestedTargetMet: projectedMinimumTextPx >= requestedMinimumTextPx,
80 limit,
81 };
82}
84// Vertical chrome that always stacks with the SVG at the 1440x900 desktop
85// viewport, measured from the delivered Viewer with the shortest one-line
86// header and no cards: body padding 12, header 39, diagram padding/border 75.
87// Cards are excluded so the prediction stays a lower bound.
88export const DESKTOP_FIXED_VERTICAL_CHROME_PX = Object.freeze({ body: 12, header: 39, diagram: 75 });
90// A canvas the Reader can neither narrow (viewBox ratio below the wide
91// threshold) nor scroll readably (no automatic Reader fit) renders at the full
92// reader width, so its page height is a function of the viewBox alone. Returns
93// null when the Reader has a way to fit the page; otherwise the certain
94// overflow at 1440x900 before any cards are counted.
95export function predictedFixedWidthOverflow({
96 viewBoxWidth,
97 viewBoxHeight,
98 readerFit,
99 diagramType,
100 viewport = DESKTOP_READABILITY_VIEWPORT,
101 bodyHorizontalPx = DECLARED_WIDE_REFERENCE_BODY_HORIZONTAL_PX,
102 diagramHorizontalPx = DECLARED_WIDE_REFERENCE_DIAGRAM_HORIZONTAL_PX,
103 chrome = DESKTOP_FIXED_VERTICAL_CHROME_PX,
104} = {}) {
105 if (![viewBoxWidth, viewBoxHeight].every(Number.isFinite) || viewBoxWidth <= 0 || viewBoxHeight <= 0) return null;
106 const ratio = viewBoxWidth / viewBoxHeight;
107 if (readerFit === 'intrinsic-height' || readerFit === 'width-first'
108 || (readerFit === 'authored-height' && diagramType === 'architecture')
109 || ratio >= DECLARED_WIDE_READER_RATIO) return null;
110 const svgWidthPx = viewport.width - bodyHorizontalPx - diagramHorizontalPx;
111 const svgHeightPx = Math.round(svgWidthPx * viewBoxHeight / viewBoxWidth);
112 const fixedChromePx = chrome.body + chrome.header + chrome.diagram;
113 const pageHeightPx = svgHeightPx + fixedChromePx;
114 if (pageHeightPx <= viewport.height) return null;
115 return {
116 viewportWidth: viewport.width,
117 viewportHeight: viewport.height,
118 ratio: Math.round(ratio * 100) / 100,
119 wideRatio: DECLARED_WIDE_READER_RATIO,
120 svgWidthPx,
121 svgHeightPx,
122 fixedChromePx,
123 pageHeightPx,
124 overflowPx: pageHeightPx - viewport.height,
125 };
126}
128export function describeFixedWidthOverflow(issue) {
129 const maximumViewBoxHeight = Math.floor(issue.viewBoxWidth / issue.wideRatio);
130 const wideViewBoxWidth = Math.ceil(issue.viewBoxHeight * issue.wideRatio);
131 return `Preserve every node, relationship, and label. This ${issue.viewBoxWidth}x${issue.viewBoxHeight} canvas (ratio ${issue.ratio}) declares no intrinsic-height fit and is below the ${issue.wideRatio} wide ratio, so the desktop Reader can neither narrow it nor accept vertical scroll: it renders ${issue.svgHeightPx}px tall at the full ${issue.svgWidthPx}px width and the page reaches ${issue.pageHeightPx}px before cards against ${issue.viewportHeight}px, a certain visual-check failure. Either compact vertical spacing so meta.viewBox height is at most ${maximumViewBoxHeight} at this width, or spread content sideways so the width is at least ${wideViewBoxWidth} at this height; for architecture and readable-v2 workflow, omitting meta.viewBox lets the renderer size the canvas and declare the fit.`;
132}