flowsvg 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -1
- package/PATTERNS.md +71 -4
- package/README.md +148 -438
- package/README.zh-CN.md +148 -438
- package/dist/adapters/react.d.ts +55 -0
- package/dist/adapters/react.js +138 -0
- package/dist/adapters/vue.d.ts +158 -0
- package/dist/adapters/vue.js +177 -0
- package/dist/adapters/web-component.d.ts +111 -0
- package/dist/adapters/web-component.js +425 -0
- package/dist/{player-controls.d.ts → browser/controls.d.ts} +7 -3
- package/dist/{player-controls.js → browser/controls.js} +23 -31
- package/dist/browser/export-controls.d.ts +32 -0
- package/dist/browser/export-controls.js +151 -0
- package/dist/browser/player.d.ts +77 -0
- package/dist/browser/player.js +412 -0
- package/dist/browser/select-control.d.ts +33 -0
- package/dist/browser/select-control.js +171 -0
- package/dist/browser.d.ts +5 -69
- package/dist/browser.js +3 -353
- package/dist/core/color.d.ts +3 -0
- package/dist/core/color.js +18 -0
- package/dist/core/diagram.d.ts +40 -0
- package/dist/core/diagram.js +481 -0
- package/dist/core/edge-geometry.d.ts +45 -0
- package/dist/core/edge-geometry.js +609 -0
- package/dist/{core.d.ts → core/model.d.ts} +37 -61
- package/dist/core/model.js +1 -0
- package/dist/core/node-content-measurement.d.ts +34 -0
- package/dist/core/node-content-measurement.js +66 -0
- package/dist/{node-content-paint.d.ts → core/node-content-paint.d.ts} +2 -2
- package/dist/{node-content-paint.js → core/node-content-paint.js} +11 -20
- package/dist/{node-content.d.ts → core/node-content.d.ts} +18 -1
- package/dist/{node-content.js → core/node-content.js} +30 -6
- package/dist/core/svg-number.d.ts +5 -0
- package/dist/core/svg-number.js +7 -0
- package/dist/{theme.d.ts → core/theme.d.ts} +6 -0
- package/dist/core/theme.js +17 -0
- package/dist/{timeline.d.ts → core/timeline.d.ts} +11 -5
- package/dist/{timeline.js → core/timeline.js} +30 -22
- package/dist/core/validation.d.ts +11 -0
- package/dist/core/validation.js +399 -0
- package/dist/export/browser.d.ts +9 -0
- package/dist/export/browser.js +172 -0
- package/dist/export/captions.d.ts +18 -0
- package/dist/export/captions.js +44 -0
- package/dist/export/plan.d.ts +25 -0
- package/dist/export/plan.js +94 -0
- package/dist/export/types.d.ts +64 -0
- package/dist/export/types.js +9 -0
- package/dist/export/worker-source.d.ts +1 -0
- package/dist/export/worker-source.js +1 -0
- package/dist/export.d.ts +3 -0
- package/dist/export.js +2 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +3 -1
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +72 -0
- package/dist/node/gif.d.ts +16 -0
- package/dist/node/gif.js +184 -0
- package/dist/node/html.d.ts +19 -0
- package/dist/node/html.js +61 -0
- package/dist/node/output.d.ts +2 -0
- package/dist/node/output.js +22 -0
- package/dist/node.d.ts +6 -0
- package/dist/node.js +3 -0
- package/dist/portable/LICENSE.esbuild.txt +21 -0
- package/dist/portable/LICENSE.flowsvg.txt +21 -0
- package/dist/portable/LICENSE.gifenc.txt +21 -0
- package/dist/portable/export.js +5 -0
- package/dist/portable/manifest.json +36 -0
- package/dist/portable/player.js +2 -0
- package/dist/portable/styles.css +227 -0
- package/dist/react.d.ts +3 -48
- package/dist/react.js +2 -126
- package/dist/styles.css +194 -14
- package/dist/vue.d.ts +3 -132
- package/dist/vue.js +2 -161
- package/dist/web-component.d.ts +3 -103
- package/dist/web-component.js +2 -396
- package/docs/images/entity-graph.png +0 -0
- package/docs/images/facts-timeline.png +0 -0
- package/docs/images/facts-timeline.zh-CN.png +0 -0
- package/docs/images/flowsvg-wordmark.gif +0 -0
- package/docs/images/node-content.png +0 -0
- package/docs/images/numbered-list.png +0 -0
- package/docs/images/numbered-list.zh-CN.png +0 -0
- package/docs/images/quick-start.png +0 -0
- package/docs/images/record-tags.png +0 -0
- package/package.json +38 -6
- package/dist/core.js +0 -1149
- package/dist/theme.js +0 -6
- /package/dist/{player-accessibility.d.ts → browser/accessibility.d.ts} +0 -0
- /package/dist/{player-accessibility.js → browser/accessibility.js} +0 -0
- /package/dist/{player-lifecycle.d.ts → browser/lifecycle.d.ts} +0 -0
- /package/dist/{player-lifecycle.js → browser/lifecycle.js} +0 -0
- /package/dist/{svg-dom.d.ts → browser/svg-dom.d.ts} +0 -0
- /package/dist/{svg-dom.js → browser/svg-dom.js} +0 -0
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* The browser consumes these results; this module does not read the DOM, network, system theme or browser clock.
|
|
4
|
-
*/
|
|
5
|
-
import { type NodeContent, type NodeContentLayout } from "./node-content.js";
|
|
6
|
-
export type { NodeContent, ContentBlock, ContentListItem, ContentListStyle, ContentTag, ContentTagStyle, ContentTagBackground, ContentGraphBlock, ContentGraphNode, ContentGraphEdge, ContentGraphLayout, ContentText, ContentTextSpan, ContentSpanStyle, ContentSpan, ContentTextStyle, ContentAlignment, ContentSafeArea, ContentLine, NodeContentLayout } from "./node-content.js";
|
|
1
|
+
/** Shared serializable document and evaluated-frame types; no runtime or platform dependencies. */
|
|
2
|
+
import type { NodeContent, NodeContentLayout } from "./node-content.js";
|
|
7
3
|
/** Supported shapes; a group also supplies the origin and visibility constraints for its children. */
|
|
8
4
|
export type NodeKind = "rect" | "roundedRect" | "ellipse" | "cylinder" | "group";
|
|
9
|
-
/** Route families generated from node geometry; orthogonal routes avoid obstacles, and curved routes use cubic Bézier controls. */
|
|
5
|
+
/** Route families generated from node geometry; orthogonal routes avoid obstacles with rounded bends, and curved routes use cubic Bézier controls. */
|
|
10
6
|
export type EdgeRoute = "straight" | "orthogonal" | "curved";
|
|
11
7
|
/** Cardinal node sides used for explicit edge attachments. */
|
|
12
8
|
export type PortDirection = "top" | "right" | "bottom" | "left";
|
|
@@ -18,6 +14,14 @@ export type ThemeName = "light" | "dark";
|
|
|
18
14
|
export interface Viewport {
|
|
19
15
|
width: number;
|
|
20
16
|
height: number;
|
|
17
|
+
/**
|
|
18
|
+
* Fit full-view height to current painted geometry plus finite nonnegative bottom padding, in diagram units.
|
|
19
|
+
* Round height outward to whole units. Keep origin and authored width; empty frames use authored height. Explicit focus keeps authored viewport semantics.
|
|
20
|
+
* GIF export uses authored fixed dimensions for every frame to preserve diagram scale.
|
|
21
|
+
*/
|
|
22
|
+
fitHeight?: {
|
|
23
|
+
padding: number;
|
|
24
|
+
};
|
|
21
25
|
}
|
|
22
26
|
/** Initial base style; colors accept hexadecimal RGB/RGBA only, and dashed is used for group borders. */
|
|
23
27
|
export interface NodeStyle {
|
|
@@ -62,7 +66,7 @@ export interface DiagramEdge {
|
|
|
62
66
|
route?: EdgeRoute;
|
|
63
67
|
/** Optional cardinal side for the source attachment; omitted values use geometry-aware automatic routing. */
|
|
64
68
|
fromPort?: PortDirection;
|
|
65
|
-
/** Optional
|
|
69
|
+
/** Optional target side, otherwise selected automatically. Same-side curved and orthogonal inputs receive separate positions, including hidden inputs. */
|
|
66
70
|
toPort?: PortDirection;
|
|
67
71
|
visible?: boolean;
|
|
68
72
|
style?: EdgeStyle;
|
|
@@ -87,15 +91,26 @@ export interface Scene {
|
|
|
87
91
|
label: string;
|
|
88
92
|
beats: Beat[];
|
|
89
93
|
}
|
|
94
|
+
/** Immutable effective beat interval in global milliseconds; caption includes inherited text. */
|
|
95
|
+
export interface DiagramBeat {
|
|
96
|
+
readonly sceneId: string;
|
|
97
|
+
readonly beatId: string;
|
|
98
|
+
/** Zero-based index within the scene. */
|
|
99
|
+
readonly beatIndex: number;
|
|
100
|
+
readonly startMs: number;
|
|
101
|
+
readonly endMs: number;
|
|
102
|
+
readonly caption?: string;
|
|
103
|
+
}
|
|
90
104
|
/**
|
|
91
105
|
* Persistent actions take effect from the beat start and inherit into later beats; durationMs must be positive.
|
|
92
106
|
* transitionMs defaults to min(250, durationMs) within [0, durationMs]; omitted captions inherit the previous text.
|
|
93
|
-
*
|
|
107
|
+
* One flow moves at a reference 180 diagram units/second with a 400 ms minimum, then uses a 500 ms post-arrival interval.
|
|
108
|
+
* Single-flow duration replaces authored timing; multiple flows share authored full-beat progress. All flows expire without inheritance.
|
|
94
109
|
*/
|
|
95
110
|
export interface Beat {
|
|
96
|
-
/**
|
|
111
|
+
/** Authored beat length in milliseconds for no-flow/multi-flow beats; one flow uses movement plus 500 ms. */
|
|
97
112
|
durationMs: number;
|
|
98
|
-
/** Opening transition in milliseconds; defaults to min(250, durationMs), and zero applies changes immediately. */
|
|
113
|
+
/** Opening transition in milliseconds; defaults to min(250, durationMs), clamps to effective beat length, and zero applies changes immediately. */
|
|
99
114
|
transitionMs?: number;
|
|
100
115
|
/** Replaces the inherited caption at the beat start; an empty string clears it. */
|
|
101
116
|
caption?: string;
|
|
@@ -128,7 +143,11 @@ export interface EdgeVisualState {
|
|
|
128
143
|
}
|
|
129
144
|
/**
|
|
130
145
|
* Declarative beat action. Visibility, explicit highlighting, base updates and focus persist into later beats.
|
|
131
|
-
* Flow markers and their temporary
|
|
146
|
+
* Flow markers and their temporary highlights expire at the beat end without changing persistent layers.
|
|
147
|
+
* Flows highlight their edge and, by default, both endpoints; highlightParticipants: false excludes the endpoints.
|
|
148
|
+
* Single-flow receiver paint fades in for 200 ms after arrival, holds for 100 ms, then automatic node/edge paint fades out for 200 ms.
|
|
149
|
+
* Logical participant flags and markers last for the whole beat; explicit highlights retain authored timing.
|
|
150
|
+
* Reduced motion skips paint interpolation. Multiple flows retain synchronized endpoint paint.
|
|
132
151
|
* focus accepts a node/group ID or null for the full view; duplicate assignments within one beat are rejected.
|
|
133
152
|
* A beat permits at most one flow per edge, regardless of direction; other visual layers can coexist.
|
|
134
153
|
*/
|
|
@@ -152,6 +171,8 @@ export type Action = {
|
|
|
152
171
|
direction: FlowDirection;
|
|
153
172
|
/** Plain short message for this transfer; provided text uses a horizontal capsule instead of a dot. */
|
|
154
173
|
text?: string;
|
|
174
|
+
/** Defaults to true. False disables temporary endpoint highlights; explicit highlights and the flow edge are unchanged. */
|
|
175
|
+
highlightParticipants?: boolean;
|
|
155
176
|
};
|
|
156
177
|
/** Validation path and reason, for example scenes[0].beats[1].actions[0].target. */
|
|
157
178
|
export interface ValidationIssue {
|
|
@@ -160,13 +181,13 @@ export interface ValidationIssue {
|
|
|
160
181
|
}
|
|
161
182
|
/**
|
|
162
183
|
* Reproducible visual options: theme overrides the document theme, with light as the final default.
|
|
163
|
-
* reducedMotion completes transitions immediately and hides flow markers while retaining flow
|
|
184
|
+
* reducedMotion completes transitions immediately and hides flow markers while retaining the flow's configured highlights.
|
|
164
185
|
* Beat durations and timeline positions do not change.
|
|
165
186
|
*/
|
|
166
187
|
export interface EvaluateOptions {
|
|
167
188
|
/** Override the document theme; the final fallback is light. */
|
|
168
189
|
theme?: ThemeName;
|
|
169
|
-
/** Apply target values immediately and hide flow markers; retain
|
|
190
|
+
/** Apply target values immediately and hide flow markers; retain configured flow highlights, beat durations and selection. */
|
|
170
191
|
reducedMotion?: boolean;
|
|
171
192
|
}
|
|
172
193
|
/** Deterministically laid-out line; x/y specify its centered anchor and text baseline in SVG document coordinates. */
|
|
@@ -187,6 +208,7 @@ export interface EvaluatedNode extends DiagramNode {
|
|
|
187
208
|
visible: boolean;
|
|
188
209
|
/** Effective opacity in [0, 1], including visibility transitions and ancestor opacity. */
|
|
189
210
|
opacity: number;
|
|
211
|
+
/** Logical highlight state; automatic single-flow paint can still be pending or interpolating. */
|
|
190
212
|
highlighted: boolean;
|
|
191
213
|
labelLines: string[];
|
|
192
214
|
labelBoxes: TextLine[];
|
|
@@ -207,7 +229,7 @@ export interface EvaluatedEdge extends DiagramEdge {
|
|
|
207
229
|
/** Deterministic transfer state derived from the current beat and edge path. */
|
|
208
230
|
export interface EdgeFlowMarker {
|
|
209
231
|
direction: FlowDirection;
|
|
210
|
-
/** Linear completion
|
|
232
|
+
/** Linear movement completion in [0, 1]; a single flow holds at 1 until its beat ends. */
|
|
211
233
|
progress: number;
|
|
212
234
|
point: DiagramPoint;
|
|
213
235
|
/** Unmodified beat-local message; SVG uses deterministic code-point widths and keeps the capsule horizontal. */
|
|
@@ -258,49 +280,3 @@ export interface DiagramFrameState {
|
|
|
258
280
|
caption?: string;
|
|
259
281
|
warnings: LayoutWarning[];
|
|
260
282
|
}
|
|
261
|
-
/** Compilation error for invalid documents; issues contains all field problems found by validation. */
|
|
262
|
-
export declare class DiagramValidationError extends Error {
|
|
263
|
-
readonly issues: readonly ValidationIssue[];
|
|
264
|
-
constructor(issues: ValidationIssue[]);
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Validate document structure, values and references; an empty list means validation passed.
|
|
268
|
-
* Accumulate issues across inspectable branches without mutating input or producing a partial Diagram.
|
|
269
|
-
*/
|
|
270
|
-
export declare function validateDocument(input: unknown): ValidationIssue[];
|
|
271
|
-
/**
|
|
272
|
-
* Validate and copy a document, then compile its timeline; later input mutations cannot affect evaluation.
|
|
273
|
-
* @throws {DiagramValidationError} If document structure, values or references are invalid.
|
|
274
|
-
*/
|
|
275
|
-
export declare function createDiagram(input: DiagramDocument): Diagram;
|
|
276
|
-
/** One-shot static SVG export; use createDiagram to reuse compilation across repeated evaluations or exports. */
|
|
277
|
-
export declare function renderSvg(document: DiagramDocument, timeMs?: number, options?: EvaluateOptions): string;
|
|
278
|
-
/** Compiled diagram independent of the DOM, network and clock; each query evaluates from its time and options. */
|
|
279
|
-
export interface Diagram {
|
|
280
|
-
/** Global timeline duration in milliseconds; zero for static diagrams. */
|
|
281
|
-
readonly totalDuration: number;
|
|
282
|
-
/**
|
|
283
|
-
* Evaluate a frame; finite times clamp to [0, totalDuration], and non-finite times become zero.
|
|
284
|
-
* Beat intervals include their start and exclude their end; totalDuration returns the completed final beat.
|
|
285
|
-
*/
|
|
286
|
-
evaluateAt(timeMs: number, options?: EvaluateOptions): DiagramFrameState;
|
|
287
|
-
/** Serialize the same frame as self-contained static SVG with escaped text and resolved colors, without controls. */
|
|
288
|
-
renderSvg(timeMs?: number, options?: EvaluateOptions): string;
|
|
289
|
-
/**
|
|
290
|
-
* Hit-test document coordinates in paint order: ordinary nodes, then edges, then group backgrounds.
|
|
291
|
-
* Objects within each layer are searched in reverse document order.
|
|
292
|
-
* Node hits include their visible label area. Hidden/zero-opacity objects are excluded.
|
|
293
|
-
* Return null for no hit or non-finite coordinates.
|
|
294
|
-
*/
|
|
295
|
-
hitTest(timeMs: number, point: DiagramPoint, options?: EvaluateOptions): HitObject | null;
|
|
296
|
-
}
|
|
297
|
-
interface Typography {
|
|
298
|
-
fontSize: number;
|
|
299
|
-
lineHeight: number;
|
|
300
|
-
charWidth: number;
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Estimate wrapping with fixed code-point widths, preserving explicit empty lines and preferring spaces as breaks.
|
|
304
|
-
* No font/DOM measurement is used; this handles width only, while frame evaluation reports height overflow.
|
|
305
|
-
*/
|
|
306
|
-
export declare function wrapText(label: string, maxWidth: number, typography?: Typography): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Author-time content measurement shares deterministic core layout and never reads browser font metrics. */
|
|
2
|
+
import { type NodeContent } from "./node-content.js";
|
|
3
|
+
/** Inputs use diagram units and the same content styles as fixed-size nodes. */
|
|
4
|
+
export interface NodeContentMeasurementInput {
|
|
5
|
+
kind: "rect" | "roundedRect" | "cylinder";
|
|
6
|
+
/** Positive finite node width in diagram units. */
|
|
7
|
+
width: number;
|
|
8
|
+
/** One content version or a nonempty collection; caller-owned objects and arrays are never mutated. */
|
|
9
|
+
content: NodeContent | readonly NodeContent[];
|
|
10
|
+
}
|
|
11
|
+
/** Unresolved overflow for a zero-based content version and its source path. */
|
|
12
|
+
export interface NodeContentMeasurementDiagnostic {
|
|
13
|
+
/** A single content object uses index zero. */
|
|
14
|
+
versionIndex: number;
|
|
15
|
+
/** Normalized source path, such as content[1].blocks[0]. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Height refers to overflow inside a fixed-height graph block. */
|
|
18
|
+
dimension: "width" | "height";
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
/** A fixed node height in diagram units; diagnostics can still require content or width changes. */
|
|
22
|
+
export interface NodeContentMeasurement {
|
|
23
|
+
/** Positive finite recommended node height, including complete line boxes and shape clearance. */
|
|
24
|
+
height: number;
|
|
25
|
+
diagnostics: NodeContentMeasurementDiagnostic[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Measure content before creating a document, using deterministic SDK text estimates rather than browser fonts.
|
|
29
|
+
* Multiple versions share their largest required height. Diagnostics retain every unresolved block overflow.
|
|
30
|
+
* The caller writes the result into explicit node dimensions; input objects and arrays remain unchanged.
|
|
31
|
+
* Finite source values can overflow height arithmetic; validation issues identify every affected content version.
|
|
32
|
+
* @throws {DiagramValidationError} If input is invalid or any required node height is non-finite.
|
|
33
|
+
*/
|
|
34
|
+
export declare function measureNodeContent(input: NodeContentMeasurementInput): NodeContentMeasurement;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Author-time content measurement shares deterministic core layout and never reads browser font metrics. */
|
|
2
|
+
import { layoutNodeContentWithMetrics, supportsNodeContent, validateNodeContent } from "./node-content.js";
|
|
3
|
+
import { DiagramValidationError } from "./validation.js";
|
|
4
|
+
import { resolveThemePalette } from "./theme.js";
|
|
5
|
+
/**
|
|
6
|
+
* Measure content before creating a document, using deterministic SDK text estimates rather than browser fonts.
|
|
7
|
+
* Multiple versions share their largest required height. Diagnostics retain every unresolved block overflow.
|
|
8
|
+
* The caller writes the result into explicit node dimensions; input objects and arrays remain unchanged.
|
|
9
|
+
* Finite source values can overflow height arithmetic; validation issues identify every affected content version.
|
|
10
|
+
* @throws {DiagramValidationError} If input is invalid or any required node height is non-finite.
|
|
11
|
+
*/
|
|
12
|
+
export function measureNodeContent(input) {
|
|
13
|
+
const issues = validateMeasurementInput(input);
|
|
14
|
+
if (issues.length > 0)
|
|
15
|
+
throw new DiagramValidationError(issues);
|
|
16
|
+
const versions = isContentVersions(input.content) ? input.content : [input.content];
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
const heights = versions.map((content, versionIndex) => {
|
|
19
|
+
// Saturate shape clearance once. Any nonempty cylinder needs more height than its capped end reserves.
|
|
20
|
+
const { contentHeight, layout, overflows } = layoutNodeContentWithMetrics(content, { x: 0, y: 0, width: input.width, height: Number.MAX_VALUE }, resolveThemePalette(undefined), input.kind);
|
|
21
|
+
// Append separately so overflowing block counts cannot exceed the engine's function-argument limit.
|
|
22
|
+
for (const { blockIndex, dimension } of overflows) {
|
|
23
|
+
diagnostics.push({ versionIndex, path: `content[${versionIndex}].blocks[${blockIndex}]`, dimension,
|
|
24
|
+
message: dimension === "width" ? "content exceeds the available width; increasing node height cannot fix this overflow"
|
|
25
|
+
: "graph content exceeds its fixed block height; increasing node height cannot fix this overflow"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const height = contentHeight + layout.safeArea.y * 2;
|
|
29
|
+
// Individually finite source values can still overflow when line heights, gaps and insets are combined.
|
|
30
|
+
if (!Number.isFinite(height)) {
|
|
31
|
+
issues.push({ path: `content[${versionIndex}]`, message: "required node height must be finite" });
|
|
32
|
+
}
|
|
33
|
+
return height;
|
|
34
|
+
});
|
|
35
|
+
if (issues.length > 0)
|
|
36
|
+
throw new DiagramValidationError(issues);
|
|
37
|
+
return { height: heights.reduce((largest, height) => Math.max(largest, height), 1), diagnostics };
|
|
38
|
+
}
|
|
39
|
+
function isContentVersions(value) {
|
|
40
|
+
return Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
/** Reuse document content validation before layout so malformed JavaScript inputs receive field errors. */
|
|
43
|
+
function validateMeasurementInput(input) {
|
|
44
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
45
|
+
return [{ path: "$", message: "measurement input must be an object" }];
|
|
46
|
+
}
|
|
47
|
+
const issues = [];
|
|
48
|
+
for (const field of Object.keys(input)) {
|
|
49
|
+
if (!["kind", "width", "content"].includes(field))
|
|
50
|
+
issues.push({ path: field, message: "unknown field" });
|
|
51
|
+
}
|
|
52
|
+
if (!("kind" in input) || !supportsNodeContent(input.kind)) {
|
|
53
|
+
issues.push({ path: "kind", message: "must be rect, roundedRect or cylinder" });
|
|
54
|
+
}
|
|
55
|
+
if (!("width" in input) || typeof input.width !== "number" || !Number.isFinite(input.width) || input.width <= 0) {
|
|
56
|
+
issues.push({ path: "width", message: "must be a finite number greater than zero" });
|
|
57
|
+
}
|
|
58
|
+
const content = "content" in input ? input.content : undefined;
|
|
59
|
+
const versions = Array.isArray(content) ? content : [content];
|
|
60
|
+
if (versions.length === 0)
|
|
61
|
+
issues.push({ path: "content", message: "must contain at least one content version" });
|
|
62
|
+
for (let index = 0; index < versions.length; index += 1) {
|
|
63
|
+
validateNodeContent(versions[index], `content[${index}]`, issues);
|
|
64
|
+
}
|
|
65
|
+
return issues;
|
|
66
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { NodeContentLayout } from "./node-content.js";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Retain theme-specific semantic ink and tag fills; preserve geometry and explicit author colors.
|
|
8
|
+
* Check default ink against the evaluated fill because authored base fills can cross low-contrast transition colors.
|
|
9
9
|
*/
|
|
10
10
|
export declare function paintContentHighlight(layout: NodeContentLayout, amount: number, fill: string): NodeContentLayout;
|
|
@@ -1,42 +1,33 @@
|
|
|
1
|
-
import { mixColor } from "./timeline.js";
|
|
2
|
-
const HIGHLIGHT_CONTENT_COLORS = {
|
|
3
|
-
title: "#172033", body: "#172033", marker: "#172033", auxiliary: "#354158", tag: "#164b9b", status: "#16466b"
|
|
4
|
-
};
|
|
5
|
-
const HIGHLIGHT_TAG_FILL = "#eaf1ff";
|
|
6
1
|
const MIN_CONTENT_CONTRAST = 4.5;
|
|
7
2
|
const CONTRAST_DARK_INK = "#000000";
|
|
8
3
|
const CONTRAST_LIGHT_INK = "#ffffff";
|
|
9
4
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Retain theme-specific semantic ink and tag fills; preserve geometry and explicit author colors.
|
|
6
|
+
* Check default ink against the evaluated fill because authored base fills can cross low-contrast transition colors.
|
|
12
7
|
*/
|
|
13
8
|
export function paintContentHighlight(layout, amount, fill) {
|
|
14
9
|
if (amount === 0)
|
|
15
10
|
return layout;
|
|
16
|
-
const tags = layout.tags.map((tag) => ({ ...tag, fill: tag.explicitFill ? tag.fill : mixColor(tag.fill, HIGHLIGHT_TAG_FILL, amount) }));
|
|
17
11
|
return {
|
|
18
|
-
...layout,
|
|
12
|
+
...layout,
|
|
19
13
|
...(layout.graphs ? { graphs: layout.graphs.map((graph) => ({ ...graph,
|
|
20
|
-
nodes: graph.nodes.map((node) => ({ ...node, fill: readableDefaultInk(node.fill,
|
|
21
|
-
edges: graph.edges.map((edge) => ({ ...edge, color: readableDefaultInk(edge.color,
|
|
14
|
+
nodes: graph.nodes.map((node) => ({ ...node, fill: readableDefaultInk(node.fill, fill) })),
|
|
15
|
+
edges: graph.edges.map((edge) => ({ ...edge, color: readableDefaultInk(edge.color, fill) }))
|
|
22
16
|
})) } : {}),
|
|
23
17
|
lines: layout.lines.map((line) => {
|
|
24
|
-
const tag = line.role === "tag" ? tags.find((candidate) => candidate.blockIndex === line.blockIndex
|
|
18
|
+
const tag = line.role === "tag" ? layout.tags.find((candidate) => candidate.blockIndex === line.blockIndex
|
|
25
19
|
&& candidate.itemIndex === line.itemIndex && candidate.tagIndex === line.tagIndex) : undefined;
|
|
26
20
|
const background = tag?.fill ?? fill;
|
|
27
|
-
const paint = (color) => readableDefaultInk(color,
|
|
21
|
+
const paint = (color) => readableDefaultInk(color, background);
|
|
28
22
|
const spans = line.spans.map((span) => ({ ...span, color: span.explicitColor ? span.color : paint(span.color) }));
|
|
29
23
|
return { ...line, spans, color: spans[0]?.color ?? paint(line.color) };
|
|
30
24
|
})
|
|
31
25
|
};
|
|
32
26
|
}
|
|
33
|
-
/**
|
|
34
|
-
function readableDefaultInk(source,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (contrastRatio(candidate, background) >= MIN_CONTENT_CONTRAST)
|
|
38
|
-
return candidate;
|
|
39
|
-
}
|
|
27
|
+
/** Keep semantic ink when readable; otherwise select opaque dark/light ink without changing author colors. */
|
|
28
|
+
function readableDefaultInk(source, background) {
|
|
29
|
+
if (contrastRatio(source, background) >= MIN_CONTENT_CONTRAST)
|
|
30
|
+
return source;
|
|
40
31
|
return contrastRatio(CONTRAST_DARK_INK, background) >= contrastRatio(CONTRAST_LIGHT_INK, background)
|
|
41
32
|
? CONTRAST_DARK_INK : CONTRAST_LIGHT_INK;
|
|
42
33
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Deterministic node-content model and layout. The core supplies current absolute node geometry and theme colors.
|
|
3
3
|
* This module does not measure browser fonts or mutate authored dimensions; SVG consumes its evaluated lines and tag backgrounds.
|
|
4
4
|
*/
|
|
5
|
-
import type { NodeKind, ValidationIssue } from "./
|
|
5
|
+
import type { NodeKind, ValidationIssue } from "./model.js";
|
|
6
6
|
import type { ThemePalette } from "./theme.js";
|
|
7
7
|
/** Horizontal placement within the current content safe area. */
|
|
8
8
|
export type ContentAlignment = "left" | "center" | "right";
|
|
@@ -27,6 +27,8 @@ export type ContentText = string | ContentTextSpan[];
|
|
|
27
27
|
/** Controlled tag typography and optional background; tags remain left-aligned within the item. */
|
|
28
28
|
export interface ContentTagStyle extends ContentSpanStyle {
|
|
29
29
|
fill?: string;
|
|
30
|
+
/** Rounded corners by default; pill uses half the final tag height without changing text layout. */
|
|
31
|
+
shape?: "rounded" | "pill";
|
|
30
32
|
}
|
|
31
33
|
/** A decorative text tag, without an independent identity or interaction. */
|
|
32
34
|
export interface ContentTag {
|
|
@@ -159,6 +161,8 @@ export interface ContentLine {
|
|
|
159
161
|
export interface ContentTagBackground extends ContentSafeArea {
|
|
160
162
|
fill: string;
|
|
161
163
|
explicitFill: boolean;
|
|
164
|
+
/** Omitted shape uses the default rounded corners; pill follows the final aligned height. */
|
|
165
|
+
shape?: ContentTagStyle["shape"];
|
|
162
166
|
blockIndex: number;
|
|
163
167
|
itemIndex: number;
|
|
164
168
|
tagIndex: number;
|
|
@@ -176,6 +180,19 @@ export interface NodeContentLayout {
|
|
|
176
180
|
export declare function supportsNodeContent(kind: unknown): boolean;
|
|
177
181
|
/** Lay out current content from the top left, without DOM font measurement or authored-size changes. */
|
|
178
182
|
export declare function layoutNodeContent(content: NodeContent, bounds: ContentSafeArea, palette: ThemePalette, kind: NodeKind): NodeContentLayout;
|
|
183
|
+
/**
|
|
184
|
+
* Internal layout shared by fixed-size evaluation and author-time measurement.
|
|
185
|
+
* contentHeight includes complete line boxes, padded tags, graph block heights and all authored gaps.
|
|
186
|
+
* overflows retain only width limits and graph-internal height limits; outer node height remains adjustable.
|
|
187
|
+
*/
|
|
188
|
+
export declare function layoutNodeContentWithMetrics(content: NodeContent, bounds: ContentSafeArea, palette: ThemePalette, kind: NodeKind): {
|
|
189
|
+
layout: NodeContentLayout;
|
|
190
|
+
contentHeight: number;
|
|
191
|
+
overflows: {
|
|
192
|
+
blockIndex: number;
|
|
193
|
+
dimension: "width" | "height";
|
|
194
|
+
}[];
|
|
195
|
+
};
|
|
179
196
|
/** Validate nested content separately from shape and label-mode checks at the document boundary. */
|
|
180
197
|
export declare function validateNodeContent(value: unknown, path: string, issues: ValidationIssue[]): void;
|
|
181
198
|
/** Validate inline source values before evaluation; unknown fields keep their exact source position. */
|
|
@@ -43,26 +43,44 @@ function contentSafeArea(bounds, padding, kind) {
|
|
|
43
43
|
}
|
|
44
44
|
/** Lay out current content from the top left, without DOM font measurement or authored-size changes. */
|
|
45
45
|
export function layoutNodeContent(content, bounds, palette, kind) {
|
|
46
|
+
return layoutNodeContentWithMetrics(content, bounds, palette, kind).layout;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Internal layout shared by fixed-size evaluation and author-time measurement.
|
|
50
|
+
* contentHeight includes complete line boxes, padded tags, graph block heights and all authored gaps.
|
|
51
|
+
* overflows retain only width limits and graph-internal height limits; outer node height remains adjustable.
|
|
52
|
+
*/
|
|
53
|
+
export function layoutNodeContentWithMetrics(content, bounds, palette, kind) {
|
|
46
54
|
const safeArea = contentSafeArea(bounds, content.style?.padding ?? DEFAULT_PADDING, kind);
|
|
47
55
|
let cursorY = safeArea.y;
|
|
48
56
|
let widthOverflow = false;
|
|
49
57
|
let heightOverflow = false;
|
|
50
58
|
const tags = [];
|
|
51
59
|
const graphs = [];
|
|
60
|
+
const overflows = [];
|
|
52
61
|
const lines = content.blocks.flatMap((block, blockIndex) => {
|
|
53
62
|
const result = block.type === "graph"
|
|
54
63
|
? layoutContentGraph(block, safeArea, cursorY, palette, blockIndex, graphs)
|
|
55
64
|
: block.type === "list"
|
|
56
65
|
? layoutContentList(block, safeArea, cursorY, palette, blockIndex)
|
|
57
66
|
: layoutTextPart(block.text, block.type === "title" ? "title" : block.role ?? "body", block.style, safeArea, cursorY, palette, blockIndex);
|
|
58
|
-
widthOverflow
|
|
67
|
+
const blockWidthOverflow = result.widthOverflow || result.lines.some((line) => contentLineExceedsWidth(line, safeArea));
|
|
68
|
+
if (blockWidthOverflow)
|
|
69
|
+
overflows.push({ blockIndex, dimension: "width" });
|
|
70
|
+
if (result.heightOverflow)
|
|
71
|
+
overflows.push({ blockIndex, dimension: "height" });
|
|
72
|
+
widthOverflow ||= blockWidthOverflow;
|
|
59
73
|
heightOverflow ||= result.heightOverflow ?? false;
|
|
60
74
|
tags.push(...result.tags);
|
|
61
75
|
cursorY = result.bottom + (blockIndex < content.blocks.length - 1 ? content.style?.blockGap ?? DEFAULT_BLOCK_GAP : 0);
|
|
62
76
|
return result.lines;
|
|
63
77
|
});
|
|
64
|
-
return {
|
|
65
|
-
|
|
78
|
+
return { contentHeight: cursorY - safeArea.y, overflows, layout: {
|
|
79
|
+
safeArea, lines, tags, ...(graphs.length ? { graphs } : {}),
|
|
80
|
+
widthOverflow,
|
|
81
|
+
// Authored positions change accumulation rounding; use the same geometric tolerance as width checks.
|
|
82
|
+
heightOverflow: heightOverflow || cursorY > safeArea.y + safeArea.height + OVERFLOW_TOLERANCE
|
|
83
|
+
} };
|
|
66
84
|
}
|
|
67
85
|
/** Graph labels reuse text metrics; authored graph geometry is clipped instead of resized. */
|
|
68
86
|
function layoutContentGraph(block, area, top, palette, blockIndex, graphs) {
|
|
@@ -82,7 +100,9 @@ function layoutContentGraph(block, area, top, palette, blockIndex, graphs) {
|
|
|
82
100
|
graphs.push({ blockIndex, nodes, edges });
|
|
83
101
|
return { lines: parts.flatMap((part) => part.lines), tags: [],
|
|
84
102
|
bottom: top + block.height,
|
|
85
|
-
|
|
103
|
+
// Recovering a label top from its absolute baseline can round across the block boundary.
|
|
104
|
+
heightOverflow: parts.some((part) => part.lines.some((line) => line.y - line.fontSize < top - OVERFLOW_TOLERANCE)
|
|
105
|
+
|| part.bottom > top + block.height)
|
|
86
106
|
|| block.nodes.some((node) => node.y - GRAPH_NODE_RADIUS < 0 || node.y + GRAPH_NODE_RADIUS > block.height),
|
|
87
107
|
widthOverflow: parts.some((part) => part.widthOverflow) || block.nodes.some((node) => node.x < GRAPH_NODE_RADIUS
|
|
88
108
|
|| node.x + GRAPH_NODE_RADIUS > area.width) };
|
|
@@ -215,7 +235,8 @@ function layoutContentTag(tag, area, top, palette, blockIndex, itemIndex, tagInd
|
|
|
215
235
|
return { ...result, widthOverflow: result.widthOverflow || area.width < TAG_PADDING_X * 2,
|
|
216
236
|
lines: result.lines.map((line) => ({ ...line, tagIndex })), bottom: result.bottom + TAG_PADDING_Y,
|
|
217
237
|
tags: [{ x: area.x, y: top, width: area.width, height: result.bottom - top + TAG_PADDING_Y,
|
|
218
|
-
fill: tag.style?.fill ?? palette.tagFill, explicitFill: tag.style?.fill !== undefined,
|
|
238
|
+
fill: tag.style?.fill ?? palette.tagFill, explicitFill: tag.style?.fill !== undefined,
|
|
239
|
+
...(tag.style?.shape === undefined ? {} : { shape: tag.style.shape }), blockIndex, itemIndex, tagIndex }] };
|
|
219
240
|
}
|
|
220
241
|
/** Baseline shifts preserve line spacing and expand tag backgrounds instead of moving the row boundary. */
|
|
221
242
|
function shiftTagText(part, offset) {
|
|
@@ -473,10 +494,13 @@ function validateContentTags(value, path, issues) {
|
|
|
473
494
|
validateContentTextStyle(tag.style, `${tagPath}.style`, issues, true);
|
|
474
495
|
continue;
|
|
475
496
|
}
|
|
476
|
-
const { fill, ...textStyle } = tag.style;
|
|
497
|
+
const { fill, shape, ...textStyle } = tag.style;
|
|
477
498
|
validateContentTextStyle(textStyle, `${tagPath}.style`, issues, true);
|
|
478
499
|
if (fill !== undefined)
|
|
479
500
|
validateColor(fill, `${tagPath}.style.fill`, issues);
|
|
501
|
+
if (shape !== undefined && shape !== "rounded" && shape !== "pill") {
|
|
502
|
+
issues.push({ path: `${tagPath}.style.shape`, message: "must be rounded or pill" });
|
|
503
|
+
}
|
|
480
504
|
}
|
|
481
505
|
}
|
|
482
506
|
/** Validate inline source values before evaluation; unknown fields keep their exact source position. */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SVG number serialization. Geometry and SVG serialization use this
|
|
3
|
+
* deterministic format so moving either implementation cannot change output.
|
|
4
|
+
*/
|
|
5
|
+
export function formatNumber(value) {
|
|
6
|
+
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(3)));
|
|
7
|
+
}
|
|
@@ -9,7 +9,13 @@ export interface ThemePalette {
|
|
|
9
9
|
tagFill: string;
|
|
10
10
|
accentText: string;
|
|
11
11
|
edge: string;
|
|
12
|
+
/** Border and flow-marker emphasis; node interiors use highlightFill instead. */
|
|
12
13
|
highlight: string;
|
|
14
|
+
highlightFill: string;
|
|
15
|
+
highlightText: string;
|
|
13
16
|
}
|
|
17
|
+
export declare const DEFAULT_NODE_STROKE_WIDTH = 1.2;
|
|
18
|
+
export declare const NODE_CORNER_RADIUS = 8;
|
|
19
|
+
export declare const DIAGRAM_FONT_FAMILY = "system-ui, -apple-system, \"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", sans-serif";
|
|
14
20
|
/** Select a palette without consulting browser preferences. */
|
|
15
21
|
export declare function resolveThemePalette(theme: "light" | "dark" | undefined): ThemePalette;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const LIGHT_PALETTE = {
|
|
2
|
+
background: "#ffffff", fill: "#ffffff", stroke: "#cfd8e3", text: "#1d2939", mutedText: "#5a6473",
|
|
3
|
+
tagText: "#1d4fa8", tagFill: "#eaf1ff", accentText: "#1d4fa8", edge: "#8892a4",
|
|
4
|
+
highlight: "#2a63c8", highlightFill: "#eaf1ff", highlightText: "#ffffff"
|
|
5
|
+
};
|
|
6
|
+
const DARK_PALETTE = {
|
|
7
|
+
background: "#101828", fill: "#1d2939", stroke: "#475467", text: "#f2f4f7", mutedText: "#b8c2d1",
|
|
8
|
+
tagText: "#caddff", tagFill: "#253b5c", accentText: "#9bbfff", edge: "#98a2b3",
|
|
9
|
+
highlight: "#8ab4f8", highlightFill: "#233b60", highlightText: "#101828"
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_NODE_STROKE_WIDTH = 1.2;
|
|
12
|
+
export const NODE_CORNER_RADIUS = 8;
|
|
13
|
+
export const DIAGRAM_FONT_FAMILY = 'system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif';
|
|
14
|
+
/** Select a palette without consulting browser preferences. */
|
|
15
|
+
export function resolveThemePalette(theme) {
|
|
16
|
+
return theme === "dark" ? DARK_PALETTE : LIGHT_PALETTE;
|
|
17
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Deterministic global timeline: compile actions/snapshots and evaluate base styles, visibility, highlights and focus.
|
|
3
3
|
* Spatial layout and SVG output belong to the core; theme defaults are supplied only during evaluation.
|
|
4
4
|
*/
|
|
5
|
-
import type { DiagramDocument, EvaluateOptions, FlowDirection, NodeVisualState, EdgeVisualState } from "./
|
|
5
|
+
import type { DiagramBeat, DiagramDocument, EvaluateOptions, FlowDirection, NodeVisualState, EdgeVisualState } from "./model.js";
|
|
6
6
|
type VisualValues = NodeVisualState & EdgeVisualState;
|
|
7
7
|
/** Evaluation-time theme defaults; unspecified snapshot colors remain unresolved so compilation does not fix a theme. */
|
|
8
8
|
export interface TimelinePalette {
|
|
@@ -10,11 +10,12 @@ export interface TimelinePalette {
|
|
|
10
10
|
stroke: string;
|
|
11
11
|
edge: string;
|
|
12
12
|
}
|
|
13
|
-
/** One transfer
|
|
13
|
+
/** One beat-local transfer; single-flow movement may finish before its beat, and no flow persists in snapshots. */
|
|
14
14
|
export interface CompiledFlow {
|
|
15
15
|
target: string;
|
|
16
16
|
direction: FlowDirection;
|
|
17
17
|
text?: string;
|
|
18
|
+
highlightParticipants: boolean;
|
|
18
19
|
}
|
|
19
20
|
/** Object transition result; highlight is the overlay weight, while highlighted is the target logical state. */
|
|
20
21
|
export interface TimelineObject {
|
|
@@ -30,6 +31,12 @@ export interface TimelineFrame {
|
|
|
30
31
|
sceneId: string | null;
|
|
31
32
|
beatId: string | null;
|
|
32
33
|
beatProgress: number;
|
|
34
|
+
/** Linear movement completion, clamped at arrival while the beat retains temporary highlights. */
|
|
35
|
+
flowProgress: number;
|
|
36
|
+
/** Single-flow receiver paint after arrival; independent of logical highlights and opening transitions. */
|
|
37
|
+
flowArrivalProgress: number;
|
|
38
|
+
/** Single-flow automatic paint removal during the last 200 ms; logical flags and marker lifetime are unchanged. */
|
|
39
|
+
flowDepartureProgress: number;
|
|
33
40
|
transitionProgress: number;
|
|
34
41
|
focusBefore: string | null;
|
|
35
42
|
focusAfter: string | null;
|
|
@@ -44,15 +51,14 @@ export interface TimelineFrame {
|
|
|
44
51
|
*/
|
|
45
52
|
export declare class CompiledTimeline {
|
|
46
53
|
readonly totalDuration: number;
|
|
54
|
+
readonly schedule: readonly DiagramBeat[];
|
|
47
55
|
private readonly beats;
|
|
48
56
|
private readonly initial;
|
|
49
57
|
private readonly title?;
|
|
50
|
-
constructor(document: DiagramDocument);
|
|
58
|
+
constructor(document: DiagramDocument, movementDurations?: ReadonlyMap<string, number>);
|
|
51
59
|
/** Clamp finite times globally; reduced motion skips interpolation but preserves beat selection and total duration. */
|
|
52
60
|
evaluateAt(timeMs: number, options: EvaluateOptions, palette: TimelinePalette): TimelineFrame;
|
|
53
61
|
/** Select the next beat at endMs, retaining the last beat at total duration; static documents return undefined. */
|
|
54
62
|
private findBeat;
|
|
55
63
|
}
|
|
56
|
-
/** Interpolate validated hexadecimal RGB/RGBA channels; preserve endpoint strings and normalize intermediate colors. */
|
|
57
|
-
export declare function mixColor(source: string, target: string, progress: number): string;
|
|
58
64
|
export {};
|