@tumaet/apollon 4.7.0 → 4.9.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.
Files changed (73) hide show
  1. package/README.md +45 -20
  2. package/dist/assets/style.css +1 -1
  3. package/dist/export.d.ts +73 -0
  4. package/dist/export.js +219 -0
  5. package/dist/exportStyles-Co-WBqTa.js +5 -0
  6. package/dist/external/LICENSE-InterFont +92 -0
  7. package/dist/external/exportFonts-DxO9jrR1.js +19 -0
  8. package/dist/external/exportStyles-Co-WBqTa.js +5 -0
  9. package/dist/external/index.js +22191 -0
  10. package/dist/fontStack-DOtVH2j8.js +5 -0
  11. package/dist/index.d.ts +150 -3
  12. package/dist/index.js +19993 -19219
  13. package/dist/internals.d.ts +65 -1
  14. package/dist/internals.js +14 -12
  15. package/dist/react/apollon-editor.d.ts +56 -3
  16. package/dist/react/components/AlignmentGuides.d.ts +1 -1
  17. package/dist/react/components/CustomControls.d.ts +7 -0
  18. package/dist/react/components/DraggableGhost.d.ts +5 -0
  19. package/dist/react/components/popovers/PopoverManager.d.ts +1 -1
  20. package/dist/react/components/popovers/activityDiagram/ActivitySwimlaneEditPopover.d.ts +2 -0
  21. package/dist/react/components/popovers/activityDiagram/index.d.ts +1 -0
  22. package/dist/react/components/react/ApollonControl.d.ts +18 -0
  23. package/dist/react/components/svgs/nodes/InterfaceLabel.d.ts +26 -0
  24. package/dist/react/components/svgs/nodes/activityDiagram/ActivitySwimlaneSVG.d.ts +9 -0
  25. package/dist/react/components/svgs/nodes/activityDiagram/index.d.ts +1 -0
  26. package/dist/react/components/svgs/nodes/componentDiagram/ComponentInterfaceNodeSVG.d.ts +2 -0
  27. package/dist/react/components/svgs/nodes/deploymentDiagram/DeploymentInterfaceSVG.d.ts +2 -0
  28. package/dist/react/constants.d.ts +28 -0
  29. package/dist/react/edges/labelTypes/EdgeIncludeExtendLabel.d.ts +1 -4
  30. package/dist/react/edges/labelTypes/EdgeMiddleLabels.d.ts +7 -4
  31. package/dist/react/edges/labelTypes/EdgeMultipleLabels.d.ts +2 -3
  32. package/dist/react/edges/labelTypes/messageLayout.d.ts +51 -0
  33. package/dist/react/export/exportErrors.d.ts +10 -0
  34. package/dist/react/export/index.d.ts +13 -0
  35. package/dist/react/export/normalizeExportSvg.d.ts +16 -0
  36. package/dist/react/export/preProcessSvgForPdf.d.ts +20 -0
  37. package/dist/react/export/svgToPdf.d.ts +17 -0
  38. package/dist/react/export/svgToPng.d.ts +38 -0
  39. package/dist/react/exportStyles-yVX7Dykt.js +6 -0
  40. package/dist/react/hooks/index.d.ts +0 -1
  41. package/dist/react/hooks/useRemoteDraggingNodes.d.ts +20 -0
  42. package/dist/react/hooks/useStepPathEdge.d.ts +11 -0
  43. package/dist/react/index.d.ts +1 -0
  44. package/dist/react/nodes/activityDiagram/ActivitySwimlane.d.ts +3 -0
  45. package/dist/react/nodes/activityDiagram/index.d.ts +1 -0
  46. package/dist/react/nodes/types.d.ts +2 -1
  47. package/dist/react/overlay/OverlayLayer.d.ts +8 -0
  48. package/dist/react/overlay/RegionMount.d.ts +10 -0
  49. package/dist/react/overlay/overlayStore.d.ts +20 -0
  50. package/dist/react/overlay/types.d.ts +48 -0
  51. package/dist/react/react.d.ts +1 -0
  52. package/dist/react/react.js +13625 -18669
  53. package/dist/react/store/context.d.ts +3 -0
  54. package/dist/react/store/diagramStore.d.ts +17 -1
  55. package/dist/react/sync/perfCounters.d.ts +6 -0
  56. package/dist/react/sync/ydoc.d.ts +2 -0
  57. package/dist/react/sync/yjsSync.d.ts +3 -2
  58. package/dist/react/types/nodes/NodeProps.d.ts +24 -0
  59. package/dist/react/typings.d.ts +17 -0
  60. package/dist/react/utils/collaboration.d.ts +10 -1
  61. package/dist/react/utils/exportUtils.d.ts +0 -130
  62. package/dist/react/utils/geometry/edgeLabelLayout.d.ts +123 -0
  63. package/dist/react/utils/geometry/interfaceLabelLayout.d.ts +44 -0
  64. package/dist/react/utils/index.d.ts +1 -0
  65. package/dist/react/utils/paletteLayout.d.ts +70 -0
  66. package/dist/react/utils/swimlaneUtils.d.ts +58 -0
  67. package/dist/yjsSync-BCNFF_EI.js +45418 -0
  68. package/package.json +33 -8
  69. package/schema/uml-model-4.schema.json +1 -0
  70. package/dist/exportStyles-Xk-Vm7Ul.js +0 -5
  71. package/dist/react/exportStyles-DZCHk5mK.js +0 -6
  72. package/dist/react/hooks/useMessagePositioning.d.ts +0 -30
  73. package/dist/yjsSync-aPxjWNdZ.js +0 -50939
@@ -5,15 +5,18 @@ import { PopoverStore } from './popoverStore';
5
5
  import { AssessmentSelectionStore } from './assessmentSelectionStore';
6
6
  import { AlignmentGuidesStore } from './alignmentGuidesStore';
7
7
  import { EdgeGeometryStore } from './edgeGeometryStore';
8
+ import { OverlayStore } from '../overlay/overlayStore';
8
9
  export declare const DiagramStoreContext: import('react').Context<StoreApi<DiagramStore> | null>;
9
10
  export declare const MetadataStoreContext: import('react').Context<StoreApi<MetadataStore> | null>;
10
11
  export declare const PopoverStoreContext: import('react').Context<StoreApi<PopoverStore> | null>;
11
12
  export declare const AssessmentSelectionStoreContext: import('react').Context<StoreApi<AssessmentSelectionStore> | null>;
12
13
  export declare const AlignmentGuidesStoreContext: import('react').Context<StoreApi<AlignmentGuidesStore> | null>;
13
14
  export declare const EdgeGeometryStoreContext: import('react').Context<StoreApi<EdgeGeometryStore> | null>;
15
+ export declare const OverlayStoreContext: import('react').Context<StoreApi<OverlayStore> | null>;
14
16
  export declare const useDiagramStore: <T>(selector: (state: DiagramStore) => T) => T;
15
17
  export declare const useMetadataStore: <T>(selector: (state: MetadataStore) => T) => T;
16
18
  export declare const usePopoverStore: <T>(selector: (state: PopoverStore) => T) => T;
17
19
  export declare const useAssessmentSelectionStore: <T>(selector: (state: AssessmentSelectionStore) => T) => T;
18
20
  export declare const useAlignmentGuidesStore: <T>(selector: (state: AlignmentGuidesStore) => T) => T;
19
21
  export declare const useEdgeGeometryStore: <T>(selector: (state: EdgeGeometryStore) => T) => T;
22
+ export declare const useOverlayStore: <T>(selector: (state: OverlayStore) => T) => T;
@@ -1,6 +1,6 @@
1
1
  import { StoreApi, UseBoundStore } from 'zustand';
2
2
  import { Node, Edge, OnNodesChange, OnEdgesChange } from '@xyflow/react';
3
- import { Assessment, InteractiveElements } from '../typings';
3
+ import { Assessment, DraggingNode, InteractiveElements } from '../typings';
4
4
  import * as Y from "yjs";
5
5
  export type DiagramStoreData = {
6
6
  nodes: Node[];
@@ -18,8 +18,24 @@ export type DiagramStore = {
18
18
  canUndo: boolean;
19
19
  canRedo: boolean;
20
20
  undoManager: Y.UndoManager | null;
21
+ collaborationEnabled: boolean;
21
22
  previewMode: boolean;
22
23
  setDiagramId: (diagramId: string) => void;
24
+ setCollaborationEnabled: (enabled: boolean) => void;
25
+ /**
26
+ * Inject the sink that forwards transient drag/resize frames onto the
27
+ * ephemeral awareness channel (wired by `YjsSync` for every editor). Kept as
28
+ * runtime wiring rather than diagram state so a `reset()` never clears it.
29
+ * The broadcast itself is gated by `collaborationEnabled`, not by this sink;
30
+ * `null` (headless, where no `YjsSync` runs) just leaves it unwired.
31
+ */
32
+ setDraggingNodesPublisher: (publisher: ((draggingNodes: DraggingNode[] | null) => void) | null) => void;
33
+ /**
34
+ * Clear the peers' live-drag overlay once a gesture's settled value is
35
+ * committed. Called from `onNodeDragStop` (after the doc write) and on
36
+ * collaboration teardown; a no-op when nothing is being broadcast.
37
+ */
38
+ endTransientNodeBroadcast: () => void;
23
39
  setNodes: (payload: Node[] | ((nodes: Node[]) => Node[])) => void;
24
40
  setEdges: (payload: Edge[] | ((edges: Edge[]) => Edge[])) => void;
25
41
  setNodesAndEdges: (nodes: Node[], edges: Edge[]) => void;
@@ -0,0 +1,6 @@
1
+ export type PerfCounters = {
2
+ storeNodeWrites: number;
3
+ };
4
+ export declare const perfCounters: PerfCounters;
5
+ export declare const getPerfCounters: () => PerfCounters | undefined;
6
+ export declare const recordStoreNodeWrite: () => void;
@@ -1,7 +1,9 @@
1
1
  import { Assessment } from '../typings';
2
2
  import { Node, Edge } from '@xyflow/react';
3
3
  import * as Y from "yjs";
4
+ export declare const STORE_ORIGIN = "store";
4
5
  export declare const getNodesMap: (ydoc: Y.Doc) => Y.Map<Node>;
5
6
  export declare const getEdgesMap: (ydoc: Y.Doc) => Y.Map<Edge>;
6
7
  export declare const getAssessments: (ydoc: Y.Doc) => Y.Map<Assessment>;
7
8
  export declare const getDiagramMetadata: (ydoc: Y.Doc) => Y.Map<string>;
9
+ export declare const reconcileYMap: <T>(map: Y.Map<T>, nextEntries: Iterable<readonly [string, T]>) => void;
@@ -1,6 +1,6 @@
1
1
  import { DiagramStore } from '../store/diagramStore';
2
2
  import { MetadataStore } from '../store/metadataStore';
3
- import { CollaborationState, CollaborationUser, CollaborationViewport, CollaboratorInfo } from '../typings';
3
+ import { CollaborationState, CollaborationUser, CollaborationViewport, CollaboratorInfo, DraggingNode } from '../typings';
4
4
  import { StoreApi } from 'zustand';
5
5
  import * as Y from "yjs";
6
6
  export declare enum MessageType {
@@ -37,6 +37,7 @@ export declare class YjsSync {
37
37
  setLocalAwarenessViewport: (viewport: CollaborationViewport | null) => void;
38
38
  setLocalAwarenessFollowing: (followingClientId: number | null) => void;
39
39
  setLocalAwarenessSelectedElement: (selectedElementId: string | null) => void;
40
+ setLocalAwarenessDraggingNodes: (draggingNodes: DraggingNode[] | null) => void;
40
41
  setLocalAwarenessState: (state: Partial<CollaborationState>) => void;
41
42
  subscribeToAwarenessChanges: (callback: (states: Map<number, CollaborationState>) => void) => () => void;
42
43
  getAwarenessStates: () => Map<number, CollaborationState>;
@@ -58,5 +59,5 @@ export declare class YjsSync {
58
59
  /**
59
60
  * Convert Base64 string to Uint8Array
60
61
  */
61
- private base64ToUint8;
62
+ static base64ToUint8(base64: string): Uint8Array;
62
63
  }
@@ -8,6 +8,30 @@ export type DefaultNodeProps = {
8
8
  export type ClassNodeElement = {
9
9
  id: string;
10
10
  } & DefaultNodeProps;
11
+ /** A single partition (lane) inside an activity swimlane. */
12
+ export type SwimlaneLane = {
13
+ id: string;
14
+ name: string;
15
+ /**
16
+ * Lane's extent along the swimlane's primary axis, in absolute flow px. The
17
+ * last lane is elastic (fills the remainder), so resizing the swimlane only
18
+ * changes the last lane. Optional: lanes without it divide the space equally,
19
+ * so existing saved swimlanes render as equal lanes until a separator moves.
20
+ */
21
+ size?: number;
22
+ };
23
+ /**
24
+ * Activity-partition (swimlane) container. Lanes are equal divisions along the
25
+ * primary axis (columns when vertical, rows when horizontal). Elements dropped
26
+ * into the swimlane become its children and move with it; which lane an element
27
+ * belongs to is expressed by where the modeller places it, matching the UML
28
+ * activity-partition notation where partition membership is positional rather
29
+ * than stored per element.
30
+ */
31
+ export type ActivitySwimlaneProps = DefaultNodeProps & {
32
+ orientation: "vertical" | "horizontal";
33
+ lanes: SwimlaneLane[];
34
+ };
11
35
  export type ClassNodeProps = {
12
36
  methods: ClassNodeElement[];
13
37
  attributes: ClassNodeElement[];
@@ -24,12 +24,29 @@ export type CollaborationViewport = {
24
24
  y: number;
25
25
  zoom: number;
26
26
  };
27
+ /**
28
+ * One node a peer is actively dragging or resizing. Broadcast over the
29
+ * ephemeral awareness channel — never written to the Yjs document — so peers
30
+ * can render the in-progress gesture live without growing the CRDT or entering
31
+ * anyone's undo history. The settled position/size is committed once on
32
+ * drop/release through the document like any other edit.
33
+ */
34
+ export type DraggingNode = {
35
+ id: string;
36
+ position: {
37
+ x: number;
38
+ y: number;
39
+ };
40
+ width?: number | null;
41
+ height?: number | null;
42
+ };
27
43
  export type CollaborationState = {
28
44
  user?: CollaborationUser;
29
45
  cursor?: CollaborationCursor | null;
30
46
  viewport?: CollaborationViewport | null;
31
47
  followingClientId?: number | null;
32
48
  selectedElementId?: string | null;
49
+ draggingNodes?: DraggingNode[] | null;
33
50
  };
34
51
  export type CollaboratorInfo = {
35
52
  id: string;
@@ -1,4 +1,4 @@
1
- import { CollaborationViewport } from '../typings';
1
+ import { CollaborationViewport, DraggingNode } from '../typings';
2
2
  export declare const randomCollabName: () => string;
3
3
  export declare const collabColorFromName: (name: string) => string;
4
4
  /**
@@ -8,3 +8,12 @@ export declare const collabColorFromName: (name: string) => string;
8
8
  * Returns `null` for anything malformed.
9
9
  */
10
10
  export declare const sanitizeCollaborationViewport: (raw: unknown) => CollaborationViewport | null;
11
+ /**
12
+ * Narrow an untrusted, peer-supplied `draggingNodes` payload before the overlay
13
+ * reader iterates it. A non-array value (or entries missing a string `id` or a
14
+ * finite numeric position) would otherwise throw in the awareness subscription
15
+ * handler and break the live overlay for everyone reading that peer. Returns the
16
+ * well-formed entries only; `null` if the value isn't an array. Per-entry
17
+ * `width`/`height` survive only when finite or explicitly `null`.
18
+ */
19
+ export declare const sanitizeDraggingNodes: (raw: unknown) => DraggingNode[] | null;
@@ -1,5 +1,4 @@
1
1
  import { ReactFlowInstance, Node, Edge, Rect } from '@xyflow/react';
2
- import { Point } from './pathParsing';
3
2
  type SvgExportMode = "web" | "compat";
4
3
  type ExportFilterOptions = {
5
4
  include?: string[];
@@ -8,134 +7,5 @@ type ExportFilterOptions = {
8
7
  };
9
8
  export declare function filterRenderedElements(container: HTMLElement, options?: ExportFilterOptions): void;
10
9
  export declare const getSVG: (container: HTMLElement, clip: Rect, options?: ExportFilterOptions, fontFaceCss?: string) => string;
11
- /**
12
- * Extract all coordinate points from an SVG path string.
13
- * This includes endpoints AND control points for bezier curves,
14
- * which is important because bezier curves are bounded by the
15
- * convex hull of all their control points.
16
- *
17
- * For S (smooth cubic) and T (smooth quadratic) commands, we also
18
- * include the reflected control point which may extend the bounds.
19
- */
20
- declare function extractPathPoints(pathD: string): Point[];
21
- declare function getNodeBoundsFromDOM(container: HTMLElement, reactFlow?: ReactFlowInstance<Node, Edge>): Rect | undefined;
22
- /**
23
- * Calculate bounds for node SVG overflow content.
24
- *
25
- * Some nodes render elements outside their viewBox (e.g., the initial marking
26
- * arrow in Reachability Graphs extends to negative coordinates). These elements
27
- * are visible because the node SVGs use overflow="visible", but they are NOT
28
- * included in reactFlow.getNodesBounds() which only considers node position
29
- * and dimensions.
30
- *
31
- * This function scans node SVGs for <line>, <path>, and <circle> elements
32
- * that extend outside the node's local coordinate system (viewBox), converts
33
- * them to global coordinates, and returns the bounding box of all such content.
34
- */
35
- declare function getNodeOverflowBoundsFromDOM(container: HTMLElement): Rect | undefined;
36
- declare function mergeBounds(a: Rect, b: Rect): Rect;
37
10
  export declare function getRenderedDiagramBounds(reactFlow: ReactFlowInstance<Node, Edge>, container: HTMLElement): Rect;
38
- declare function extractStyles(styleString: string): {
39
- transform: {
40
- x: number;
41
- y: number;
42
- };
43
- width: string | null;
44
- height: string | null;
45
- };
46
- type CSSVariableMap = Readonly<Record<string, string>>;
47
- /**
48
- * Resolve a single CSS variable reference to its final value.
49
- * Handles recursive var() resolution and fallback values.
50
- */
51
- declare function resolveCSSVariable(value: string, cssVarMap?: CSSVariableMap): string;
52
- /**
53
- * Replace CSS variables and currentColor in all attributes of an element tree.
54
- *
55
- * @param node - The DOM node to process
56
- * @param inheritedColor - The inherited 'currentColor' value from parent elements
57
- */
58
- declare function replaceCSSVariables(node: Element | ChildNode, inheritedColor?: string, cssVarMap?: CSSVariableMap): void;
59
- /**
60
- * Convert inline style properties to direct SVG attributes for better compatibility.
61
- * PowerPoint and some other applications don't properly parse CSS in style attributes.
62
- */
63
- declare function convertStyleToAttributes(node: Element | ChildNode): void;
64
- /**
65
- * Ensure all <text> elements have explicit font-size, font-weight, and font-family.
66
- *
67
- * In the browser, text inherits these from CSS (:root font-family, default font-size).
68
- * In exported SVGs opened in non-browser renderers, missing attributes cause text to
69
- * render with the renderer's own defaults (often Times New Roman at an arbitrary size).
70
- *
71
- * This pass runs AFTER convertStyleToAttributes so any font props already extracted
72
- * from inline styles are present as attributes.
73
- */
74
- declare function ensureTextFontDefaults(svg: Element): void;
75
- /**
76
- * Resolve relative `font-size` (`%`, `em`) to px against the inherited size —
77
- * otherwise stereotypes (`font-size="85%"`) balloon over the class title. Walks
78
- * depth-first carrying the resolved px; runs after ensureTextFontDefaults so
79
- * every <text> already has a px size to inherit.
80
- */
81
- declare function resolveRelativeFontSizes(el: Element, inheritedPx?: number): void;
82
- /**
83
- * Flatten cumulative `<tspan dy>` to absolute `y` — Skia collapses sibling
84
- * tspans onto one line, overlapping a stereotype with its class name. Assumes
85
- * the flat `<text><tspan/></text>` shape Apollon emits (no nested tspans).
86
- */
87
- declare function resolveTspanDy(svg: Element): void;
88
- /**
89
- * Resolve `dominant-baseline` to an explicit baseline `y` — non-browser engines
90
- * draw every label at the alphabetic baseline (too high) otherwise. Runs after
91
- * resolveTspanDy so tspan `y` is already absolute.
92
- */
93
- declare function resolveDominantBaseline(svg: Element): void;
94
- /**
95
- * Replace `text-decoration="underline"` on `<text>` elements with manual
96
- * `<line>` siblings so the underline is visible in non-browser renderers.
97
- *
98
- * resvg 2.6.2 has a rendering bug where 3+ `text-decoration="underline"`
99
- * attributes across nested `<svg>` elements cause unrelated paths (particularly
100
- * vertical lines) to disappear. This workaround removes the problematic
101
- * attribute and draws explicit underline lines using `getBBox()` for accurate
102
- * text measurements.
103
- *
104
- * The SVG must be temporarily attached to the DOM for `getBBox()` to work.
105
- */
106
- declare function replaceTextDecorationWithManualUnderline(svg: SVGSVGElement): void;
107
- /**
108
- * Embed `@font-face` CSS (typically the bundled Inter woff2 as base64) into the
109
- * export SVG so the document carries its own font and renders identically when
110
- * opened away from the editor. Inserted first so the face is declared before
111
- * any `<text>` references it. Idempotent: a second call is a no-op.
112
- */
113
- declare function embedFontFaceCss(svg: SVGSVGElement, css: string): void;
114
- /**
115
- * Final safety pass: strip any legacy <marker> references that could sneak in
116
- * from third-party content. Keeps exports clean for PowerPoint/Keynote.
117
- */
118
- declare function removeMarkerElements(svg: Element): void;
119
- /**
120
- * @internal — Exported for unit testing only. Not part of the public API.
121
- */
122
- export declare const __testing: {
123
- readonly embedFontFaceCss: typeof embedFontFaceCss;
124
- readonly filterRenderedElements: typeof filterRenderedElements;
125
- readonly getRenderedDiagramBounds: typeof getRenderedDiagramBounds;
126
- readonly extractPathPoints: typeof extractPathPoints;
127
- readonly extractStyles: typeof extractStyles;
128
- readonly resolveCSSVariable: typeof resolveCSSVariable;
129
- readonly replaceCSSVariables: typeof replaceCSSVariables;
130
- readonly convertStyleToAttributes: typeof convertStyleToAttributes;
131
- readonly ensureTextFontDefaults: typeof ensureTextFontDefaults;
132
- readonly resolveRelativeFontSizes: typeof resolveRelativeFontSizes;
133
- readonly resolveTspanDy: typeof resolveTspanDy;
134
- readonly resolveDominantBaseline: typeof resolveDominantBaseline;
135
- readonly removeMarkerElements: typeof removeMarkerElements;
136
- readonly replaceTextDecorationWithManualUnderline: typeof replaceTextDecorationWithManualUnderline;
137
- readonly mergeBounds: typeof mergeBounds;
138
- readonly getNodeBoundsFromDOM: typeof getNodeBoundsFromDOM;
139
- readonly getNodeOverflowBoundsFromDOM: typeof getNodeOverflowBoundsFromDOM;
140
- };
141
11
  export {};
@@ -0,0 +1,123 @@
1
+ import { IPoint } from '../../edges/Connection';
2
+ /**
3
+ * Pure geometry for edge-label placement, in flow space. No React, no DOM —
4
+ * every output is derived synchronously from the edge's polyline so it is
5
+ * identical interactively and in headless (resvg) export, and unit-testable on
6
+ * its own (mirrors edges/labelTypes/messageLayout.ts, the #645 gold standard).
7
+ *
8
+ * The model follows the cross-tool consensus (yEd / ELK CENTER / mxGraph /
9
+ * JointJS): keep the text horizontal and offset it by a constant gap onto a
10
+ * perpendicular side. The label is hosted on whichever ARM of the (possibly
11
+ * stepped) edge has room — scored against the edge's own arms, nodes, and
12
+ * neighbour edges — defaulting to centered-on-top of the arc-mid segment when
13
+ * that is already clear. Deterministic scoring, not a continuous optimizer.
14
+ */
15
+ export interface Rect {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ }
21
+ export interface MidSegment {
22
+ /** Rounded midpoint of the chosen mid-segment (flow space). */
23
+ point: IPoint;
24
+ /** Orientation of the chosen mid-segment. */
25
+ isHorizontal: boolean;
26
+ /** Index of the chosen segment in the collapsed polyline. */
27
+ segmentIndex: number;
28
+ /** The chosen segment's endpoints in source->target order. Useful to derive
29
+ * the LOCAL flow direction at the middle (e.g. communication arrows) instead
30
+ * of the raw node anchors, which can disagree on a bent edge. */
31
+ start: IPoint;
32
+ end: IPoint;
33
+ }
34
+ export type LabelSide = "above" | "below" | "left" | "right";
35
+ export interface PlacedLabel {
36
+ x: number;
37
+ y: number;
38
+ textAnchor: "start" | "middle" | "end";
39
+ dominantBaseline: "auto" | "middle" | "hanging";
40
+ /** Exposed for unit assertions; not needed for rendering. */
41
+ side: LabelSide;
42
+ }
43
+ /**
44
+ * The mid-SEGMENT midpoint and orientation, computed purely from the polyline.
45
+ *
46
+ * Walks the collapsed polyline accumulating Manhattan segment length (exact
47
+ * because EDGES.STEP_BORDER_RADIUS === 0, so getSmoothStepPath emits no corner
48
+ * arcs and the rendered path equals the polyline). The "middle" is the first
49
+ * segment whose cumulative length reaches half the total; the midpoint is lerped
50
+ * strictly INSIDE that one segment, so it is corner-safe — unlike sampling the
51
+ * rendered path with getPointAtLength(total/2), which can land on a bend or, if
52
+ * line-jump bridges are present, on a bridge arc off the straight line.
53
+ */
54
+ export declare function getMidSegment(renderPoints: IPoint[], fallbackSource: IPoint, fallbackTarget: IPoint): MidSegment;
55
+ /**
56
+ * Rough single-line text width (flow px). Deterministic — a pure character-count
57
+ * estimate, never canvas measurement — so the chosen placement is identical
58
+ * interactively and in headless export, and SSR-safe. Slightly generous so the
59
+ * scoring box errs toward MORE clearance.
60
+ */
61
+ export declare function estimateLabelWidth(text: string, fontSize: number): number;
62
+ /**
63
+ * The text box (flow space) for a label anchored at `point` on the chosen side,
64
+ * sized to the actual label (`w` × `h`). Used to score overlap against the
65
+ * edge's own arms, neighbour edges, and nodes.
66
+ */
67
+ export declare function candidateBox(point: IPoint, side: LabelSide, w: number, h: number): Rect;
68
+ /**
69
+ * The other edges' polylines whose bounding box comes within `radius` of
70
+ * `center`, excluding `selfId`. Bounds the neighbour scan to edges actually near
71
+ * the label so placement stays O(nearby), not O(all edges) per label.
72
+ */
73
+ export declare function collectNeighborPolylines(geometryById: Record<string, IPoint[]>, selfId: string, center: IPoint, radius: number): IPoint[][];
74
+ export interface MiddleLabelInput {
75
+ /** The edge's full rendered polyline (source → target). */
76
+ renderPoints: IPoint[];
77
+ /** The label text — used for a width fallback when measuredWidth is absent. */
78
+ labelText: string;
79
+ /** Render font size (px) of the label. */
80
+ fontSize: number;
81
+ /** Real measured label width (px), from the rendering context. Preferred over
82
+ * the character-count estimate so the scored box equals the rendered ink. */
83
+ measuredWidth?: number;
84
+ /** Node rects to avoid — NOT only source/target: every node the edge routes
85
+ * near, so the label never lands on an unrelated node's body. */
86
+ nodeRects?: Rect[];
87
+ /** Nearby other-edge polylines (see collectNeighborPolylines), collected over
88
+ * the WHOLE edge so they cover any arm the label may land on. */
89
+ neighborGeometry?: IPoint[][];
90
+ }
91
+ /**
92
+ * Places the relationship/stereotype middle label so it overlaps nothing it can
93
+ * avoid. The label is hosted on whichever arm of the edge — and on whichever
94
+ * perpendicular side — has room: each candidate (every segment × its two sides)
95
+ * is scored lexicographically by
96
+ * [ overlaps (own other arms + nodes + neighbour edges),
97
+ * does-not-fit-within-the-segment,
98
+ * distance from the arc-midpoint (keep it central),
99
+ * side preference (on top / right) ].
100
+ * For a simple edge the arc-mid segment wins with zero overlaps, so the label
101
+ * stays centered on top exactly as before; on a zig-zag/stepped edge it moves to
102
+ * a longer arm with clearance instead of crossing the short mid-segment's
103
+ * neighbouring arms. All inputs are static geometry, so the placement is stable
104
+ * across selection and identical in headless export.
105
+ */
106
+ export declare function computeMiddleLabelLayout(input: MiddleLabelInput): PlacedLabel;
107
+ export interface RotatedLabelPlacement {
108
+ x: number;
109
+ y: number;
110
+ /** Degrees; the label is rotated to lie along the connector, flipped to stay
111
+ * upright. */
112
+ rotation: number;
113
+ }
114
+ /**
115
+ * Placement for a label that lies ALONG a straight (possibly diagonal)
116
+ * connector — use-case association names and the <<include>>/<<extend>>
117
+ * stereotype. The text is rotated to the line's slope (flipped to stay upright)
118
+ * and pushed `perpendicularOffset` px off the line on the upper-left normal.
119
+ * Pass 0 to center it on the line (e.g. include/extend, which sits in the gap
120
+ * carved by calculateStraightPath). Centralises the rotation math that was
121
+ * duplicated across the use-case label components.
122
+ */
123
+ export declare function computeUseCaseLabelLayout(source: IPoint, target: IPoint, perpendicularOffset: number): RotatedLabelPlacement;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pure side-selection for the provided/required interface node label. The
3
+ * interface is a small circle that connects only at its four directional
4
+ * handles ("top"/"right"/"bottom"/"left"). The name sits centered below by
5
+ * default (see InterfaceLabel), but must move off any side a connecting edge
6
+ * attaches to — otherwise the label overlaps that edge. Pure and unit-testable
7
+ * in the spirit of edgeLabelLayout.ts; no React, no DOM, no constants.
8
+ */
9
+ /** A side an edge can attach to (the four directional interface handles). */
10
+ export type CardinalSide = "top" | "right" | "bottom" | "left";
11
+ /**
12
+ * Where the label sits relative to the circle: one of the four sides, or — when
13
+ * every side has a connecting edge — a diagonal quadrant (which no cardinal edge
14
+ * passes through).
15
+ */
16
+ export type InterfaceLabelSide = CardinalSide | "top-left" | "top-right" | "bottom-left" | "bottom-right";
17
+ /** Minimal structural view of a React Flow edge (the store's `Edge[]` fits). */
18
+ interface InterfaceEdgeLike {
19
+ source: string;
20
+ target: string;
21
+ sourceHandle?: string | null;
22
+ targetHandle?: string | null;
23
+ }
24
+ /** The sides of `nodeId` that a connecting edge attaches to. */
25
+ export declare function getOccupiedInterfaceSides(edges: ReadonlyArray<InterfaceEdgeLike>, nodeId: string): Set<CardinalSide>;
26
+ /**
27
+ * Picks where the label sits. Prefers a free cardinal side by priority — reads
28
+ * naturally under a small circle (bottom, then top, then a horizontal side).
29
+ * When the assessment badge occupies the top-right corner, "top" and "right"
30
+ * are demoted last so a label can't graze the badge.
31
+ *
32
+ * If every cardinal side has a connecting edge, the label moves into a DIAGONAL
33
+ * quadrant: each cardinal edge leaves along an axis, so a corner label sits in
34
+ * the gap between two of them and crosses none. Prefer a corner away from the
35
+ * top-right badge.
36
+ */
37
+ export declare function pickInterfaceLabelSide(occupied: ReadonlySet<CardinalSide>, opts?: {
38
+ badgeTopRight?: boolean;
39
+ }): InterfaceLabelSide;
40
+ /** Convenience: the interface label side for `nodeId` given the current edges. */
41
+ export declare function computeInterfaceLabelSide(edges: ReadonlyArray<InterfaceEdgeLike>, nodeId: string, opts?: {
42
+ badgeTopRight?: boolean;
43
+ }): InterfaceLabelSide;
44
+ export {};
@@ -10,6 +10,7 @@ export * from './diagramTypeUtils';
10
10
  export * from './storeUtils';
11
11
  export * from './deepPartial';
12
12
  export * from './bpmnConstraints';
13
+ export * from './swimlaneUtils';
13
14
  export { importDiagram } from './versionConverter';
14
15
  export * from './alignmentUtils';
15
16
  export * from './requiredInterfaceUtils';
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Layout math for the floating element palette. The palette is an overlay on
3
+ * the canvas (all viewports — there is no docked variant), so it must lay every
4
+ * element out in a grid that fits the available canvas WITHOUT scrolling.
5
+ *
6
+ * Objective (in priority order):
7
+ * 1. Use the FEWEST columns — a tall, narrow card preserves horizontal canvas
8
+ * space, which is what you actually draw in.
9
+ * 2. Fill the available VERTICAL space — a single column down the side reads
10
+ * like a classic palette and wastes no height.
11
+ * 3. Shrink the cells down to the `COMFORT_MIN_H` LEGIBILITY threshold BEFORE
12
+ * adding a column — but no smaller: below that, spill to another column so
13
+ * the node types stay distinguishable (rather than collapsing to an
14
+ * illegible sliver). Cap at `CELL_MAX_H` so they're never too large. The
15
+ * `CELL_MIN_H` touch floor is the last-resort fallback on a tiny canvas.
16
+ *
17
+ * So: walk column counts low→high and take the first one whose cells, sized to
18
+ * fill the height (capped), are still at least the legibility threshold. One
19
+ * column of legible cells is preferred over two columns of bigger ones; cells
20
+ * shrink toward the legibility threshold first, and only spill to another column
21
+ * when staying legible in fewer columns no longer fits the height.
22
+ *
23
+ * `availH` is the palette's REAL available band (the caller measures the space
24
+ * between the palette's top and the bottom controls), so this math does NOT
25
+ * re-subtract any top/bottom chrome reserve — it fills the band it is given.
26
+ *
27
+ * Cells are rectangular (the dominant elements — class boxes, BPMN tasks — are
28
+ * ~1.6:1 wide); narrower/square elements letterbox centered inside. Sizing the
29
+ * cell, not the SVG, keeps the node components untouched.
30
+ */
31
+ export declare const PALETTE: Readonly<{
32
+ /** Absolute floor (HIG/WCAG touch) — only reached on a tiny canvas where even
33
+ * spilling to more columns can't keep cells legible; never shrink past it. */
34
+ readonly CELL_MIN_H: 44;
35
+ /** Legibility threshold: shrink a column's cells down to here BEFORE adding
36
+ * another column, but spill (rather than go smaller) so the node types stay
37
+ * distinguishable. ~a comfortably legible node-preview cell. */
38
+ readonly COMFORT_MIN_H: 64;
39
+ /** Upper bound so few-element palettes don't get absurdly tall cells;
40
+ * ~a 0.8-scale class-box preview. */
41
+ readonly CELL_MAX_H: 88;
42
+ /** cellW = round(CELL_RATIO * cellH); ~matches the 160×100 class box. */
43
+ readonly CELL_RATIO: 1.6;
44
+ readonly GAP: 8;
45
+ readonly PAD: 6;
46
+ /** Keep the palette horizontally narrow so the canvas keeps its width. */
47
+ readonly MAX_FRAC_W: 0.5;
48
+ /** Letterbox padding around the preview inside a cell. */
49
+ readonly CONTENT_INSET: 6;
50
+ }>;
51
+ export interface PaletteLayout {
52
+ cols: number;
53
+ cellW: number;
54
+ cellH: number;
55
+ /** True only in the rare case all items can't fit even at the floor size. */
56
+ scroll: boolean;
57
+ }
58
+ /**
59
+ * @param itemCount total grid cells (drag elements + the color-description cell)
60
+ * @param availW measured canvas width
61
+ * @param availH measured canvas height
62
+ * @param chromeH height of non-grid palette chrome (view switch / hint), 0 if none
63
+ */
64
+ export declare function computePaletteLayout(itemCount: number, availW: number, availH: number, chromeH: number): PaletteLayout;
65
+ /**
66
+ * Preview scale that fits an element's natural size into a cell's content box.
67
+ * Elements fill their cell (so they read "big"); wide boxes fit width, tall/
68
+ * square nodes fit height and letterbox.
69
+ */
70
+ export declare function previewScaleForCell(naturalWidth: number, naturalHeight: number, cellW: number, cellH: number): number;
@@ -0,0 +1,58 @@
1
+ import { SwimlaneLane } from '../types';
2
+ /** Smallest a lane may be shrunk to (flow px) when dragging a separator. */
3
+ export declare const MIN_LANE_EXTENT = 40;
4
+ /**
5
+ * Pixel `start`/`extent` of every lane along the primary axis.
6
+ *
7
+ * Lane sizes are stored as absolute flow px. The **last** lane is elastic: it
8
+ * always fills the remaining space. That way resizing the whole swimlane only
9
+ * grows/shrinks the last lane — the interior separators stay put — instead of
10
+ * rescaling every lane (which would move every separator). Lanes with no size
11
+ * at all (a freshly dropped swimlane) divide the axis equally.
12
+ */
13
+ export declare const getLaneOffsets: (lanes: {
14
+ size?: number;
15
+ }[], primaryExtent: number) => {
16
+ start: number;
17
+ extent: number;
18
+ }[];
19
+ /**
20
+ * Pin every lane's current rendered extent as its `size`, so the elastic last
21
+ * lane gets a concrete value before an operation (reorder/add/remove) that
22
+ * changes which lane is last.
23
+ */
24
+ export declare const materializeLaneSizes: (lanes: SwimlaneLane[], primaryExtent: number) => SwimlaneLane[];
25
+ /** Index of the lane whose range contains the primary-axis coordinate `pos`. */
26
+ export declare const laneIndexAtOffset: (offsets: {
27
+ start: number;
28
+ extent: number;
29
+ }[], pos: number) => number;
30
+ /**
31
+ * Balanced resize of the divider between lane `index` and `index + 1`: move the
32
+ * shared boundary by `deltaPx`, growing one lane and shrinking the other by the
33
+ * same amount so the swimlane's outer size never changes. Neither lane drops
34
+ * below `MIN_LANE_EXTENT`. Returns lanes with an explicit `size` on every entry.
35
+ *
36
+ * When `snapGridPx > 0` the moved boundary is snapped to that grid, so a lane
37
+ * separator lands on the same grid that nodes and edge bends snap to. The lane
38
+ * offsets are measured from the swimlane's own origin, which React Flow already
39
+ * keeps grid-aligned, so the snapped boundary lines up with the absolute grid.
40
+ */
41
+ export declare const resizeLaneDivider: (lanes: SwimlaneLane[], index: number, deltaPx: number, primaryExtent: number, snapGridPx?: number) => SwimlaneLane[];
42
+ /**
43
+ * Reposition a swimlane child when orientation flips and the swimlane's
44
+ * width/height swap: transpose the child's corner (x,y)->(y,x), then clamp it
45
+ * into the swapped frame by its own size so it can't be stranded off the new
46
+ * (shorter) cross-axis. `newWidth`/`newHeight` are the post-swap dimensions.
47
+ */
48
+ export declare const flipSwimlaneChildPosition: (child: {
49
+ position: {
50
+ x: number;
51
+ y: number;
52
+ };
53
+ width?: number;
54
+ height?: number;
55
+ }, newWidth: number, newHeight: number) => {
56
+ x: number;
57
+ y: number;
58
+ };