@schlenkr/ragents 0.1.0 → 0.1.2

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 (108) hide show
  1. package/README.md +10 -4
  2. package/apps/server/src/plugin-support/language-server/contract.ts +6 -1
  3. package/apps/server/src/plugin-support/language-server/tools.ts +29 -4
  4. package/apps/server/src/plugin-support/run-scripts.ts +4 -1
  5. package/apps/server/src/plugin-support/start-entries-contract.ts +4 -3
  6. package/apps/server/src/plugin-support/workspace-sandbox-host.ts +2 -0
  7. package/apps/server/src/profile-target.ts +25 -0
  8. package/apps/server/src/web-build.ts +82 -0
  9. package/apps/server/tsconfig.json +38 -0
  10. package/apps/web/column.html +12 -0
  11. package/apps/web/index.html +12 -0
  12. package/apps/web/src/App.tsx +0 -10
  13. package/apps/web/src/PluginChat.tsx +8 -8
  14. package/apps/web/src/PluginRegistry.tsx +0 -12
  15. package/apps/web/src/StartSurface.tsx +2 -2
  16. package/apps/web/src/ThemeSettings.tsx +1 -1
  17. package/apps/web/src/column/ColumnApp.tsx +83 -7
  18. package/apps/web/src/column/clipboard.ts +64 -0
  19. package/apps/web/src/column/host-contract.ts +10 -0
  20. package/apps/web/src/column.tsx +2 -0
  21. package/apps/web/src/panel/LoginForm.tsx +9 -3
  22. package/apps/web/src/panel/OverviewPage.tsx +187 -62
  23. package/apps/web/src/panel/PanelDialogs.tsx +115 -0
  24. package/apps/web/src/panel/PanelPage.tsx +1 -1
  25. package/apps/web/src/panel/SettingsPage.tsx +75 -79
  26. package/apps/web/src/panel/contract.ts +17 -9
  27. package/apps/web/src/panel/target-state.ts +27 -2
  28. package/apps/web/src/ui/theme.css +4 -0
  29. package/apps/web/tsconfig.json +41 -0
  30. package/apps/web/vite.config.ts +140 -0
  31. package/package.json +6 -2
  32. package/packages/ragents/src/plugin-host.ts +5 -2
  33. package/packages/ragents/src/plugin-types.ts +3 -2
  34. package/packages/workspace-executor/src/context.ts +4 -0
  35. package/packages/workspace-executor/src/executor.ts +9 -2
  36. package/packages/workspace-executor/src/index.ts +2 -0
  37. package/packages/workspace-executor/src/language-server/adapters/fsharp.ts +1 -0
  38. package/packages/workspace-executor/src/language-server/adapters/roslyn.ts +1 -0
  39. package/packages/workspace-executor/src/language-server/adapters/typescript.ts +34 -6
  40. package/packages/workspace-executor/src/language-server/contract.ts +7 -3
  41. package/packages/workspace-executor/src/language-server/host.ts +275 -138
  42. package/plugins/ragents.actor-programs/contract.ts +1 -1
  43. package/plugins/ragents.actor-programs/prompt.hbs +4 -4
  44. package/plugins/ragents.actor-programs/server/app-project.ts +0 -2
  45. package/plugins/ragents.actor-programs/server/runtime.ts +1 -1
  46. package/plugins/ragents.actor-programs/web/AppsPanel.tsx +3 -5
  47. package/plugins/ragents.actor-programs/web/EmbeddedApps.tsx +7 -39
  48. package/plugins/ragents.actor-programs/web/api.ts +1 -10
  49. package/plugins/ragents.lsp-roslyn/web/language-server-plugin.tsx +73 -44
  50. package/plugins/ragents.orchestration/canvas.hbs +28 -44
  51. package/plugins/ragents.orchestration/contract.ts +17 -287
  52. package/plugins/ragents.orchestration/server/canvas-tool.ts +18 -123
  53. package/plugins/ragents.orchestration/tiled-layout.ts +12 -4
  54. package/plugins/ragents.orchestration/web/ActorCanvasControls.tsx +23 -47
  55. package/plugins/ragents.orchestration/web/ActorChatPreview.tsx +3 -9
  56. package/plugins/ragents.orchestration/web/ActorShortcut.tsx +3 -5
  57. package/plugins/ragents.orchestration/web/ActorTile.tsx +3 -2
  58. package/plugins/ragents.orchestration/web/CanvasTiles.tsx +3 -3
  59. package/plugins/ragents.orchestration/web/Center.tsx +28 -463
  60. package/plugins/ragents.orchestration/web/MaterialBody.tsx +6 -87
  61. package/plugins/ragents.orchestration/web/TiledCanvas.tsx +2 -3
  62. package/plugins/ragents.orchestration/web/canvas-view-settings.ts +7 -13
  63. package/plugins/ragents.orchestration/web/column/RunColumn.tsx +44 -19
  64. package/plugins/ragents.orchestration/web/column/column-state.ts +13 -4
  65. package/plugins/ragents.orchestration/web/constants.ts +0 -2
  66. package/plugins/ragents.orchestration/web/index.tsx +0 -6
  67. package/plugins/ragents.orchestration/web/tile-docking.ts +7 -1
  68. package/plugins/ragents.orchestration/web/tiled-view-settings.ts +7 -9
  69. package/plugins/ragents.overseer/server/coordinator-prompt.ts +1 -1
  70. package/plugins/ragents.profile-distribution/server/web.ts +3 -27
  71. package/plugins/ragents.reference/examples.ts +1 -2
  72. package/plugins/ragents.reference/run-scripts/balcony-wizard/RUN.md +1 -1
  73. package/plugins/ragents.reference/run-scripts/balcony-wizard/actors/balcony-app/package.json +1 -1
  74. package/plugins/ragents.reference/run-scripts/balcony-wizard/src/server.ts +1 -1
  75. package/plugins/ragents.reference/run-scripts/balcony-wizard/tests/program.test.ts +2 -2
  76. package/plugins/ragents.reference/run-scripts/conversation-circle/RUN.md +3 -4
  77. package/plugins/ragents.reference/run-scripts/conversation-circle/src/server.ts +9 -15
  78. package/plugins/ragents.reference/run-scripts/conversation-circle/tests/program.test.ts +5 -3
  79. package/plugins/ragents.reference/run-scripts/learning-afternoon/package.json +1 -1
  80. package/plugins/ragents.reference/run-scripts/learning-afternoon/src/server.ts +14 -4
  81. package/plugins/ragents.reference/run-scripts/word-game/src/server.ts +1 -1
  82. package/plugins/ragents.reference/skills/210-moderator-handover/SKILL.md +1 -1
  83. package/plugins/ragents.reference/walkthroughs.ts +14 -14
  84. package/plugins/ragents.workspace/client/workspace-client.ts +4 -1
  85. package/scripts/agent/agent-cli.ts +9 -24
  86. package/scripts/package/build-package.ts +18 -5
  87. package/scripts/package/publish-package.ts +24 -10
  88. package/scripts/package/ragents.mjs +3 -2
  89. package/scripts/remote/connect.ts +16 -5
  90. package/scripts/remote/start.ts +33 -15
  91. package/scripts/workspace-client/run-workspace-client.ts +3 -1
  92. package/apps/web/src/MaterialSettings.tsx +0 -40
  93. package/apps/web/src/material-settings.ts +0 -104
  94. package/plugins/ragents.orchestration/web/ActorNet.tsx +0 -607
  95. package/plugins/ragents.orchestration/web/CardSizeSettings.tsx +0 -42
  96. package/plugins/ragents.orchestration/web/ScriptActorCard.tsx +0 -56
  97. package/plugins/ragents.orchestration/web/ZoomSettings.tsx +0 -38
  98. package/plugins/ragents.orchestration/web/canvas-layout-storage.ts +0 -39
  99. package/plugins/ragents.orchestration/web/canvas-layout.ts +0 -326
  100. package/plugins/ragents.orchestration/web/canvas-scene.ts +0 -311
  101. package/plugins/ragents.orchestration/web/card-size-settings.ts +0 -36
  102. package/plugins/ragents.orchestration/web/keyboard-navigation.ts +0 -55
  103. package/plugins/ragents.orchestration/web/material-layout.ts +0 -14
  104. package/plugins/ragents.orchestration/web/useCanvasMeasurements.ts +0 -78
  105. package/plugins/ragents.orchestration/web/zoom-settings.ts +0 -28
  106. package/plugins/ragents.reference/skills/120-canvas-conversation-circle/SKILL.md +0 -12
  107. package/plugins/ragents.reference/skills/130-canvas-pipeline-with-approval/SKILL.md +0 -12
  108. package/plugins/ragents.reference/skills/140-canvas-team-tree-and-topic-grid/SKILL.md +0 -12
@@ -1,56 +0,0 @@
1
- import { useState } from "react";
2
- import { SourceCode } from "@aicontainer/web/SourceCode";
3
- import { CodeXmlIcon } from "lucide-react";
4
- import { Button, Dialog, DialogBody, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@aicontainer/web/ui";
5
- import { materialHeadClass, materialIconClass, materialRuleClass, materialTitleClass } from "./MaterialBody";
6
- import type { RunActor } from "./run-view";
7
-
8
- const stateBarClass: Readonly<Record<string, string>> = {
9
- running: "bg-primary animate-fade-pulse motion-reduce:animate-none",
10
- done: "bg-success",
11
- failed: "bg-destructive",
12
- };
13
-
14
- export function ScriptActorCard({ actor, state, stateLabel, inputCount, onSelect }: {
15
- actor: RunActor;
16
- state: string;
17
- stateLabel: string;
18
- inputCount: number;
19
- onSelect: () => void;
20
- }) {
21
- const [sourceOpen, setSourceOpen] = useState(false);
22
- return <>
23
- <div className={`${materialHeadClass} flex-none`}>
24
- <button className="flex min-w-0 flex-1 cursor-pointer items-center gap-2.5 text-left focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-4"
25
- onClick={onSelect} title={`Details von @${actor.handle} öffnen`} type="button">
26
- <span className={`${materialIconClass} font-mono text-[10px] font-bold leading-none tracking-[.04em]`} aria-label="TypeScript">TS</span>
27
- <span className={materialTitleClass}>@{actor.handle}</span>
28
- </button>
29
- <Button className="flex-none gap-1.5 rounded-[3px] text-[11px]" size="sm" variant="ghost" disabled={actor.source === undefined}
30
- title={actor.source === undefined ? "Kein Quelltext in der Laufansicht vorhanden" : `Skript von @${actor.handle} anzeigen`}
31
- aria-label={`Skript von @${actor.handle} anzeigen`} onClick={() => setSourceOpen(true)}>
32
- <CodeXmlIcon />
33
- Skript
34
- </Button>
35
- </div>
36
- <div className={`relative mx-3 flex items-center justify-between gap-3 border-t py-2 text-[11px] leading-4 text-muted-foreground ${materialRuleClass}`}>
37
- <span className="flex items-center gap-[7px]">
38
- <span aria-hidden="true" className={`h-2.5 w-[5px] ${stateBarClass[state] ?? "bg-muted-foreground"}`} />
39
- {stateLabel}
40
- </span>
41
- <span title="An diesen Actor zugestellte Eingaben">{inputCount} {inputCount === 1 ? "Eingabe" : "Eingaben"}</span>
42
- </div>
43
- {sourceOpen && actor.source !== undefined && <Dialog open onOpenChange={(open) => { if (!open) setSourceOpen(false); }}>
44
- <DialogContent scope="run" size="wide">
45
- <DialogHeader>
46
- <DialogTitle>Skript von @{actor.handle}</DialogTitle>
47
- <DialogDescription>TypeScript</DialogDescription>
48
- </DialogHeader>
49
- <DialogBody className="max-h-[65vh] overflow-auto overscroll-contain" role="region"
50
- aria-label={`TypeScript-Quelltext von @${actor.handle}`} tabIndex={0}>
51
- <SourceCode className="m-0 overflow-visible" content={actor.source} language="typescript" path={`${actor.handle}.ts`} />
52
- </DialogBody>
53
- </DialogContent>
54
- </Dialog>}
55
- </>;
56
- }
@@ -1,38 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { useAccess } from "@aicontainer/web/AccessContext";
3
- import { Button, Input } from "@aicontainer/web/ui";
4
- import { DEFAULT_PINCH_ZOOM_SENSITIVITY, savePinchZoomSensitivity, usePinchZoomSensitivity } from "./zoom-settings";
5
-
6
- export function ZoomSettings() {
7
- const access = useAccess();
8
- const sensitivity = usePinchZoomSensitivity();
9
- const [value, setValue] = useState(String(sensitivity));
10
- const [message, setMessage] = useState<string>();
11
- const [error, setError] = useState<string>();
12
- useEffect(() => { setValue(String(sensitivity)); }, [sensitivity]);
13
- const writable = access.can("settings.write");
14
- const save = (next: number) => {
15
- if (!writable) return;
16
- setError(undefined);
17
- setMessage(undefined);
18
- try {
19
- savePinchZoomSensitivity(next);
20
- setValue(String(next));
21
- setMessage("Zoom-Empfindlichkeit gespeichert.");
22
- } catch (cause) {
23
- setError(cause instanceof Error ? cause.message : String(cause));
24
- }
25
- };
26
- return <form className="grid gap-3" onSubmit={(event) => {
27
- event.preventDefault(); save(Number(value));
28
- }}>
29
- <p className="text-muted-foreground">Empfindlichkeit der Pinch-Geste auf dem Canvas. Faktor 1 entspricht dem bisherigen Verhalten, Faktor 2 ist doppelt so empfindlich. Gilt sofort für alle Runs in diesem Browser.</p>
30
- <div className="flex flex-wrap items-end gap-3">
31
- <label className="grid gap-1.5"><span>Pinch-Zoom-Faktor</span><Input className="w-[130px]" type="number" required step="0.1" min="0.1" max="10"
32
- disabled={!writable} value={value} onChange={(event) => { setValue(event.target.value); setMessage(undefined); }} /></label>
33
- <Button type="submit" disabled={!writable}>Speichern</Button>
34
- <Button disabled={!writable} onClick={() => save(DEFAULT_PINCH_ZOOM_SENSITIVITY)} variant="outline">Zurücksetzen</Button>
35
- </div>
36
- {error ? <p className="text-muted-foreground" role="alert">{error}</p> : message && <p className="text-muted-foreground" role="status">{message}</p>}
37
- </form>;
38
- }
@@ -1,39 +0,0 @@
1
- export interface CanvasCamera { x: number; y: number; zoom: number }
2
- export interface CanvasSize { width: number; height: number }
3
- export interface CanvasElementSizes {
4
- canvasSizes: [string, CanvasSize][];
5
- actorSizes: [string, CanvasSize][];
6
- expandedActors: string[];
7
- collapsedElements: string[];
8
- }
9
-
10
- export const canvasLayoutStorageKey = (runId: string, part: "camera" | "elements") =>
11
- `ragents.orchestration.canvas-layout:${runId}:${part}`;
12
-
13
- const record = (value: unknown): value is Record<string, unknown> =>
14
- value !== null && typeof value === "object" && !Array.isArray(value);
15
- const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
16
- const positive = (value: unknown): value is number => finite(value) && value > 0;
17
-
18
- export function parseCanvasCamera(raw: string | null): CanvasCamera | undefined {
19
- if (raw === null) return undefined;
20
- const value: unknown = JSON.parse(raw);
21
- if (!record(value) || !finite(value.x) || !finite(value.y) || !positive(value.zoom) || value.zoom > 2.5) {
22
- throw new Error("Die gespeicherte Canvas-Kamera ist ungültig.");
23
- }
24
- return { x: value.x, y: value.y, zoom: value.zoom };
25
- }
26
-
27
- export function parseCanvasElementSizes(raw: string | null): CanvasElementSizes {
28
- if (raw === null) return { canvasSizes: [], actorSizes: [], expandedActors: [], collapsedElements: [] };
29
- const value: unknown = JSON.parse(raw);
30
- const sizes = (entries: unknown) => Array.isArray(entries) && entries.every((entry: unknown) =>
31
- Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string"
32
- && record(entry[1]) && positive(entry[1].width) && positive(entry[1].height));
33
- const ids = (entries: unknown) => Array.isArray(entries) && entries.every((entry: unknown) => typeof entry === "string");
34
- if (!record(value) || !sizes(value.canvasSizes) || !sizes(value.actorSizes)
35
- || !ids(value.expandedActors) || !ids(value.collapsedElements)) {
36
- throw new Error("Die gespeicherten Canvas-Größen sind ungültig.");
37
- }
38
- return value as unknown as CanvasElementSizes;
39
- }
@@ -1,326 +0,0 @@
1
- export interface SceneLeaf {
2
- kind: "leaf";
3
- key: string;
4
- width: number;
5
- height: number;
6
- }
7
-
8
- export type GroupLayout =
9
- | { type: "stack"; axis: "h" | "v"; gap: number }
10
- | { type: "wrap"; axis: "h" | "v"; limit: number; gap: number }
11
- | { type: "grid"; columns: number; gap: number }
12
- | { type: "circle"; gap: number }
13
- | { type: "tree"; direction: "right" | "down"; rankGap: number; siblingGap: number };
14
-
15
- /** A frame is a caption above the members, an outline around them, or both. */
16
- export interface SceneFrame {
17
- label?: string;
18
- outline: boolean;
19
- }
20
-
21
- export interface SceneGroup {
22
- kind: "group";
23
- key: string;
24
- layout: GroupLayout;
25
- children: SceneNode[];
26
- frame?: SceneFrame;
27
- }
28
-
29
- export type SceneNode = SceneLeaf | SceneGroup;
30
-
31
- export interface SceneLabel {
32
- fromKey: string;
33
- toKey: string;
34
- width: number;
35
- height: number;
36
- }
37
-
38
- const LABEL_CLEARANCE = 64;
39
-
40
- /** Reserve label space in the smallest group containing both endpoints. */
41
- export const withLabelSpacing = (root: SceneNode, labels: readonly SceneLabel[]): SceneNode => {
42
- const paths = new Map<string, SceneGroup[]>();
43
- const collect = (node: SceneNode, parents: SceneGroup[]) => {
44
- if (node.kind === "leaf") paths.set(node.key, parents);
45
- else node.children.forEach((child) => collect(child, [...parents, node]));
46
- };
47
- collect(root, []);
48
- const spacing = new Map<SceneGroup, { width: number; height: number }>();
49
- for (const label of labels) {
50
- if (label.fromKey === label.toKey) continue;
51
- const from = paths.get(label.fromKey);
52
- const to = paths.get(label.toKey);
53
- if (!from || !to) continue;
54
- let common: SceneGroup | undefined;
55
- for (let i = 0; i < Math.min(from.length, to.length) && from[i] === to[i]; i++) common = from[i];
56
- if (!common) continue;
57
- const previous = spacing.get(common);
58
- spacing.set(common, {
59
- width: Math.max(previous?.width ?? 0, label.width + LABEL_CLEARANCE),
60
- height: Math.max(previous?.height ?? 0, label.height + LABEL_CLEARANCE),
61
- });
62
- }
63
- const update = (node: SceneNode): SceneNode => {
64
- if (node.kind === "leaf") return node;
65
- const room = spacing.get(node);
66
- let layout = node.layout;
67
- if (room) {
68
- switch (layout.type) {
69
- case "stack":
70
- layout = { ...layout, gap: Math.max(layout.gap, layout.axis === "h" ? room.width : room.height) };
71
- break;
72
- case "tree":
73
- layout = { ...layout,
74
- rankGap: Math.max(layout.rankGap, layout.direction === "right" ? room.width : room.height),
75
- siblingGap: Math.max(layout.siblingGap, layout.direction === "right" ? room.height : room.width),
76
- };
77
- break;
78
- default:
79
- layout = { ...layout, gap: Math.max(layout.gap, room.width, room.height) };
80
- }
81
- }
82
- return { ...node, layout, children: node.children.map(update) };
83
- };
84
- return labels.length ? update(root) : root;
85
- };
86
-
87
- /** A positioned box; x and y are the top-left corner in world coordinates. */
88
- export interface PlacedBox {
89
- key: string;
90
- x: number;
91
- y: number;
92
- width: number;
93
- height: number;
94
- }
95
-
96
- export interface PlacedFrame extends PlacedBox {
97
- label?: string;
98
- outline: boolean;
99
- }
100
-
101
- export interface SceneLayout {
102
- boxes: PlacedBox[];
103
- frames: PlacedFrame[];
104
- x: number;
105
- y: number;
106
- width: number;
107
- height: number;
108
- }
109
-
110
- export const FRAME_PADDING = 18;
111
- export const FRAME_LABEL_HEIGHT = 22;
112
-
113
- interface Size {
114
- width: number;
115
- height: number;
116
- }
117
-
118
- interface Offset {
119
- x: number;
120
- y: number;
121
- }
122
-
123
- const extent = (sizes: readonly Size[], offsets: readonly Offset[]): Size => ({
124
- width: Math.max(0, ...sizes.map((size, index) => offsets[index].x + size.width)),
125
- height: Math.max(0, ...sizes.map((size, index) => offsets[index].y + size.height)),
126
- });
127
-
128
- const stackOffsets = (sizes: readonly Size[], axis: "h" | "v", gap: number): Offset[] => {
129
- let cursor = 0;
130
- return sizes.map((size) => {
131
- const offset = axis === "h" ? { x: cursor, y: 0 } : { x: 0, y: cursor };
132
- cursor += (axis === "h" ? size.width : size.height) + gap;
133
- return offset;
134
- });
135
- };
136
-
137
- const wrapOffsets = (sizes: readonly Size[], axis: "h" | "v", limit: number, gap: number): Offset[] => {
138
- const offsets: Offset[] = [];
139
- let main = 0;
140
- let cross = 0;
141
- let lineCross = 0;
142
- let lineCount = 0;
143
- for (const size of sizes) {
144
- const mainSize = axis === "h" ? size.width : size.height;
145
- const crossSize = axis === "h" ? size.height : size.width;
146
- if (lineCount > 0 && main + mainSize > limit) {
147
- cross += lineCross + gap;
148
- main = 0;
149
- lineCross = 0;
150
- lineCount = 0;
151
- }
152
- offsets.push(axis === "h" ? { x: main, y: cross } : { x: cross, y: main });
153
- main += mainSize + gap;
154
- lineCross = Math.max(lineCross, crossSize);
155
- lineCount += 1;
156
- }
157
- return offsets;
158
- };
159
-
160
- const gridOffsets = (sizes: readonly Size[], columns: number, gap: number): Offset[] => {
161
- const cellWidth = Math.max(0, ...sizes.map((size) => size.width));
162
- const cellHeight = Math.max(0, ...sizes.map((size) => size.height));
163
- return sizes.map((size, index) => ({
164
- x: (index % columns) * (cellWidth + gap) + (cellWidth - size.width) / 2,
165
- y: Math.floor(index / columns) * (cellHeight + gap) + (cellHeight - size.height) / 2,
166
- }));
167
- };
168
-
169
- /** Room between ring neighbours for a short line and its label. */
170
- const CIRCLE_LINK_ROOM = 96;
171
-
172
- const circleOffsets = (sizes: readonly Size[], gap: number): Offset[] => {
173
- const count = sizes.length;
174
- if (count <= 1) return sizes.map(() => ({ x: 0, y: 0 }));
175
- const diagonals = sizes.map((size) => Math.hypot(size.width, size.height));
176
- const chord = Math.max(...sizes.map((size, index) => {
177
- const next = sizes[(index + 1) % count];
178
- return Math.max(
179
- (diagonals[index] + diagonals[(index + 1) % count]) / 2 + gap,
180
- (size.width + next.width) / 2 + CIRCLE_LINK_ROOM,
181
- );
182
- }));
183
- const radius = chord / (2 * Math.sin(Math.PI / count));
184
- const centers = sizes.map((_, index) => {
185
- const angle = -Math.PI / 2 + index * 2 * Math.PI / count;
186
- return { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius };
187
- });
188
- const minX = Math.min(...centers.map((center, index) => center.x - sizes[index].width / 2));
189
- const minY = Math.min(...centers.map((center, index) => center.y - sizes[index].height / 2));
190
- return centers.map((center, index) => ({
191
- x: center.x - sizes[index].width / 2 - minX,
192
- y: center.y - sizes[index].height / 2 - minY,
193
- }));
194
- };
195
-
196
- const treeOffsets = (sizes: readonly Size[], direction: "right" | "down", rankGap: number, siblingGap: number): Offset[] => {
197
- if (sizes.length === 0) return [];
198
- const [node, ...subtrees] = sizes;
199
- const branch = direction === "right"
200
- ? stackOffsets(subtrees, "v", siblingGap).map((offset) => ({ x: node.width + rankGap, y: offset.y }))
201
- : stackOffsets(subtrees, "h", siblingGap).map((offset) => ({ x: offset.x, y: node.height + rankGap }));
202
- return [{ x: 0, y: 0 }, ...branch];
203
- };
204
-
205
- const childOffsets = (layout: GroupLayout, sizes: readonly Size[]): Offset[] => {
206
- switch (layout.type) {
207
- case "stack": return stackOffsets(sizes, layout.axis, layout.gap);
208
- case "wrap": return wrapOffsets(sizes, layout.axis, layout.limit, layout.gap);
209
- case "grid": return gridOffsets(sizes, layout.columns, layout.gap);
210
- case "circle": return circleOffsets(sizes, layout.gap);
211
- case "tree": return treeOffsets(sizes, layout.direction, layout.rankGap, layout.siblingGap);
212
- }
213
- };
214
-
215
- const framePadding = (group: SceneGroup): number => (group.frame?.outline ? FRAME_PADDING : 0);
216
-
217
- const frameInset = (group: SceneGroup): Offset => ({
218
- x: framePadding(group),
219
- y: framePadding(group) + (group.frame?.label ? FRAME_LABEL_HEIGHT : 0),
220
- });
221
-
222
- interface Measured {
223
- size: Size;
224
- offsets: Offset[];
225
- }
226
-
227
- const measure = (node: SceneNode, cache: Map<SceneNode, Measured>): Measured => {
228
- const known = cache.get(node);
229
- if (known) return known;
230
- let result: Measured;
231
- if (node.kind === "leaf") {
232
- result = { size: { width: node.width, height: node.height }, offsets: [] };
233
- } else {
234
- const sizes = node.children.map((child) => measure(child, cache).size);
235
- const offsets = childOffsets(node.layout, sizes);
236
- const inner = extent(sizes, offsets);
237
- const inset = frameInset(node);
238
- result = {
239
- size: {
240
- width: inner.width + inset.x + framePadding(node),
241
- height: inner.height + inset.y + framePadding(node),
242
- },
243
- offsets,
244
- };
245
- }
246
- cache.set(node, result);
247
- return result;
248
- };
249
-
250
- const arrange = (node: SceneNode, x: number, y: number, cache: Map<SceneNode, Measured>, out: SceneLayout) => {
251
- const { size, offsets } = measure(node, cache);
252
- if (node.kind === "leaf") {
253
- out.boxes.push({ key: node.key, x, y, width: size.width, height: size.height });
254
- return;
255
- }
256
- if (node.frame) {
257
- out.frames.push({ key: node.key, x, y, width: size.width, height: size.height, label: node.frame.label, outline: node.frame.outline });
258
- }
259
- const inset = frameInset(node);
260
- node.children.forEach((child, index) => {
261
- arrange(child, x + inset.x + offsets[index].x, y + inset.y + offsets[index].y, cache, out);
262
- });
263
- };
264
-
265
- /** Places every leaf and frame of the scene; the root's top-left corner lands at the given origin. */
266
- export const layoutScene = (root: SceneNode, originX = 0, originY = 0): SceneLayout => {
267
- const cache = new Map<SceneNode, Measured>();
268
- const out: SceneLayout = { boxes: [], frames: [], x: originX, y: originY, width: 0, height: 0 };
269
- arrange(root, originX, originY, cache, out);
270
- const { size } = measure(root, cache);
271
- out.width = size.width;
272
- out.height = size.height;
273
- return out;
274
- };
275
-
276
- export type BoundaryShape = "rect" | "circle" | "diamond";
277
-
278
- export interface LineEnd {
279
- box: PlacedBox;
280
- boundary: BoundaryShape;
281
- }
282
-
283
- export interface LineGeometry {
284
- x1: number;
285
- y1: number;
286
- x2: number;
287
- y2: number;
288
- middle: { x: number; y: number };
289
- }
290
-
291
- const LINE_MARGIN = 3;
292
-
293
- const exitDistance = (end: LineEnd, dx: number, dy: number): number => {
294
- const halfWidth = end.box.width / 2;
295
- const halfHeight = end.box.height / 2;
296
- const ax = Math.abs(dx);
297
- const ay = Math.abs(dy);
298
- switch (end.boundary) {
299
- case "rect":
300
- return Math.min(ax > 0 ? halfWidth / ax : Infinity, ay > 0 ? halfHeight / ay : Infinity);
301
- case "circle":
302
- return Math.min(halfWidth, halfHeight);
303
- case "diamond":
304
- return 1 / (ax / halfWidth + ay / halfHeight);
305
- }
306
- };
307
-
308
- /** A straight connector between two boxes, trimmed to their outlines. */
309
- export const lineGeometry = (from: LineEnd, to: LineEnd): LineGeometry => {
310
- const start = { x: from.box.x + from.box.width / 2, y: from.box.y + from.box.height / 2 };
311
- const finish = { x: to.box.x + to.box.width / 2, y: to.box.y + to.box.height / 2 };
312
- const dx = finish.x - start.x;
313
- const dy = finish.y - start.y;
314
- const length = Math.hypot(dx, dy);
315
- if (length < 1) return { x1: start.x, y1: start.y, x2: finish.x, y2: finish.y, middle: start };
316
- const ux = dx / length;
317
- const uy = dy / length;
318
- const fromTrim = exitDistance(from, ux, uy) + LINE_MARGIN;
319
- const toTrim = exitDistance(to, ux, uy) + LINE_MARGIN;
320
- if (fromTrim + toTrim >= length) return { x1: start.x, y1: start.y, x2: finish.x, y2: finish.y, middle: { x: (start.x + finish.x) / 2, y: (start.y + finish.y) / 2 } };
321
- const x1 = start.x + ux * fromTrim;
322
- const y1 = start.y + uy * fromTrim;
323
- const x2 = finish.x - ux * toTrim;
324
- const y2 = finish.y - uy * toTrim;
325
- return { x1, y1, x2, y2, middle: { x: (x1 + x2) / 2, y: (y1 + y2) / 2 } };
326
- };