@neuctra/notlix-mcp 1.0.0 → 2.0.1

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.
@@ -0,0 +1,451 @@
1
+ // Copied from neuctra-notlix-client/lib/workspace/diagrams.js by scripts/sync.mjs. Don't edit here.
2
+
3
+ // The diagram model. A diagram is JSON: nodes are boxes on an open canvas, and
4
+ // edges name the nodes they connect, so a connector follows when a node moves.
5
+
6
+ import { resizeFreely } from "./presentations.js";
7
+ import { shapePath } from "./slideShapes.js";
8
+
9
+ export const UNTITLED_DIAGRAM = "Untitled diagram";
10
+ export const GRID_SIZE = 10;
11
+ export const MIN_ZOOM = 0.2;
12
+ export const MAX_ZOOM = 3;
13
+
14
+ const STUB = 20;
15
+ const EXPORT_PADDING = 40;
16
+
17
+ const newId = () => crypto.randomUUID().slice(0, 8);
18
+
19
+ export const snap = (value) => Math.round(value / GRID_SIZE) * GRID_SIZE;
20
+
21
+ // ---- Node shapes ------------------------------------------------------------
22
+
23
+ function terminator(w, h) {
24
+ const r = Math.min(w, h) / 2;
25
+ return `M${r} 0H${w - r}A${r} ${r} 0 0 1 ${w - r} ${h}H${r}A${r} ${r} 0 0 1 ${r} 0Z`;
26
+ }
27
+
28
+ function cylinder(w, h) {
29
+ const ry = Math.min(h * 0.14, 16);
30
+ const rx = w / 2;
31
+ return `M0 ${ry}A${rx} ${ry} 0 0 1 ${w} ${ry}V${h - ry}A${rx} ${ry} 0 0 1 0 ${h - ry}ZM0 ${ry}A${rx} ${ry} 0 0 0 ${w} ${ry}`;
32
+ }
33
+
34
+ function documentShape(w, h) {
35
+ return `M0 0H${w}V${h * 0.84}C${w * 0.75} ${h * 0.68} ${w * 0.5} ${h * 1.05} 0 ${h * 0.86}Z`;
36
+ }
37
+
38
+ function note(w, h) {
39
+ const fold = Math.min(w, h) * 0.22;
40
+ return `M0 0H${w - fold}L${w} ${fold}V${h}H0ZM${w - fold} 0V${fold}H${w}`;
41
+ }
42
+
43
+ function subroutine(w, h) {
44
+ const inset = Math.min(w * 0.1, 14);
45
+ return `M0 0H${w}V${h}H0ZM${inset} 0V${h}M${w - inset} 0V${h}`;
46
+ }
47
+
48
+ function cloud(w, h) {
49
+ const x = (value) => (value * w) / 100;
50
+ const y = (value) => (value * h) / 100;
51
+ return `M${x(25)} ${y(85)}C${x(5)} ${y(85)} ${x(0)} ${y(55)} ${x(18)} ${y(50)}C${x(12)} ${y(25)} ${x(40)} ${y(12)} ${x(50)} ${y(30)}C${x(62)} ${y(5)} ${x(92)} ${y(20)} ${x(84)} ${y(45)}C${x(104)} ${y(50)} ${x(98)} ${y(85)} ${x(76)} ${y(85)}Z`;
52
+ }
53
+
54
+ // Sizes are even multiples of the grid, so centered shapes line up exactly.
55
+ const wide = { w: 140, h: 80 };
56
+
57
+ export const DIAGRAM_SHAPES = [
58
+ { id: "rect", name: "Process", group: "Flowchart", ...wide },
59
+ { id: "terminator", name: "Start or end", group: "Flowchart", w: 140, h: 60, draw: terminator },
60
+ { id: "diamond", name: "Decision", group: "Flowchart", w: 160, h: 100 },
61
+ { id: "parallelogram", name: "Input or output", group: "Flowchart", ...wide },
62
+ { id: "documentShape", name: "Document", group: "Flowchart", ...wide, draw: documentShape },
63
+ { id: "subroutine", name: "Subprocess", group: "Flowchart", ...wide, draw: subroutine },
64
+ { id: "hexagon", name: "Preparation", group: "Flowchart", ...wide },
65
+ { id: "roundRect", name: "Rounded box", group: "General", ...wide },
66
+ { id: "ellipse", name: "Ellipse", group: "General", w: 120, h: 120 },
67
+ { id: "triangle", name: "Triangle", group: "General", w: 120, h: 100 },
68
+ {
69
+ id: "note",
70
+ name: "Sticky note",
71
+ group: "General",
72
+ w: 140,
73
+ h: 120,
74
+ draw: note,
75
+ fields: { fill: "#fef3c7", stroke: "#b08900", textColor: "#3f3000" },
76
+ },
77
+ {
78
+ id: "text",
79
+ name: "Text",
80
+ group: "General",
81
+ w: 120,
82
+ h: 40,
83
+ draw: (w, h) => shapePath("rect", w, h),
84
+ fields: { fill: "none", strokeWidth: 0 },
85
+ },
86
+ { id: "cylinder", name: "Database", group: "Systems", w: 120, h: 100, draw: cylinder },
87
+ { id: "cloud", name: "Cloud", group: "Systems", w: 160, h: 100, draw: cloud },
88
+ {
89
+ id: "container",
90
+ name: "Group box",
91
+ group: "Systems",
92
+ w: 360,
93
+ h: 240,
94
+ draw: (w, h) => shapePath("rect", w, h),
95
+ fields: { fill: "none", dash: "dash", valign: "top" },
96
+ },
97
+ ];
98
+
99
+ export const DIAGRAM_SHAPE_GROUPS = ["Flowchart", "General", "Systems"];
100
+
101
+ const shapeById = (shapeId) =>
102
+ DIAGRAM_SHAPES.find((shape) => shape.id === shapeId) || DIAGRAM_SHAPES[0];
103
+
104
+ export const diagramShapePath = (shapeId, w, h) => {
105
+ const shape = shapeById(shapeId);
106
+ return shape.draw ? shape.draw(w, h) : shapePath(shape.id, w, h);
107
+ };
108
+
109
+ export function createNode(shapeId, centerX, centerY, fields = {}) {
110
+ const shape = shapeById(shapeId);
111
+ const { w, h } = { w: shape.w, h: shape.h, ...fields };
112
+
113
+ return {
114
+ id: newId(),
115
+ shape: shape.id,
116
+ x: snap(centerX - w / 2),
117
+ y: snap(centerY - h / 2),
118
+ w,
119
+ h,
120
+ text: "",
121
+ fill: null,
122
+ stroke: null,
123
+ textColor: null,
124
+ strokeWidth: 2,
125
+ dash: "solid",
126
+ fontSize: 14,
127
+ bold: false,
128
+ valign: "middle",
129
+ ...shape.fields,
130
+ ...fields,
131
+ };
132
+ }
133
+
134
+ export function createEdge(from, to, fields = {}) {
135
+ return {
136
+ id: newId(),
137
+ from,
138
+ to,
139
+ fromSide: null,
140
+ toSide: null,
141
+ label: "",
142
+ style: "elbow",
143
+ stroke: null,
144
+ strokeWidth: 2,
145
+ dash: "solid",
146
+ startArrow: false,
147
+ endArrow: true,
148
+ ...fields,
149
+ };
150
+ }
151
+
152
+ /** Records can come from anywhere, so a diagram always has both lists. */
153
+ export function normalizeDiagram({ nodes, edges }) {
154
+ return {
155
+ nodes: Array.isArray(nodes) ? nodes : [],
156
+ edges: Array.isArray(edges) ? edges : [],
157
+ };
158
+ }
159
+
160
+ /** Plain text for card search. */
161
+ export function diagramText({ nodes, edges }) {
162
+ return [...nodes.map((node) => node.text), ...edges.map((edge) => edge.label)]
163
+ .filter(Boolean)
164
+ .join(" ");
165
+ }
166
+
167
+ // ---- Connector routing ------------------------------------------------------
168
+
169
+ const SIDES = {
170
+ top: { x: 0, y: -1 },
171
+ right: { x: 1, y: 0 },
172
+ bottom: { x: 0, y: 1 },
173
+ left: { x: -1, y: 0 },
174
+ };
175
+
176
+ export const SIDE_NAMES = Object.keys(SIDES);
177
+
178
+ const centerOf = ({ x, y, w, h }) => ({ x: x + w / 2, y: y + h / 2 });
179
+
180
+ export function anchorOf(node, side) {
181
+ const center = centerOf(node);
182
+ return {
183
+ x: center.x + (SIDES[side].x * node.w) / 2,
184
+ y: center.y + (SIDES[side].y * node.h) / 2,
185
+ };
186
+ }
187
+
188
+ /** The side of `node` that faces `target`. */
189
+ function facingSide(node, target) {
190
+ const center = centerOf(node);
191
+ const dx = target.x - center.x;
192
+ const dy = target.y - center.y;
193
+
194
+ if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left";
195
+ return dy >= 0 ? "bottom" : "top";
196
+ }
197
+
198
+ function polylineMiddle(points) {
199
+ const lengths = points.slice(1).map((point, index) =>
200
+ Math.hypot(point.x - points[index].x, point.y - points[index].y),
201
+ );
202
+ let remaining = lengths.reduce((total, length) => total + length, 0) / 2;
203
+
204
+ for (let index = 0; index < lengths.length; index += 1) {
205
+ if (remaining <= lengths[index]) {
206
+ const ratio = lengths[index] ? remaining / lengths[index] : 0;
207
+ return {
208
+ x: points[index].x + (points[index + 1].x - points[index].x) * ratio,
209
+ y: points[index].y + (points[index + 1].y - points[index].y) * ratio,
210
+ };
211
+ }
212
+ remaining -= lengths[index];
213
+ }
214
+
215
+ return points[0];
216
+ }
217
+
218
+ function elbowPoints(start, startDir, end, endDir) {
219
+ const a = { x: start.x + startDir.x * STUB, y: start.y + startDir.y * STUB };
220
+ const b = { x: end.x + endDir.x * STUB, y: end.y + endDir.y * STUB };
221
+ const startsAcross = startDir.x !== 0;
222
+ const endsAcross = endDir.x !== 0;
223
+
224
+ let middle;
225
+ if (startsAcross && endsAcross) {
226
+ const x = (a.x + b.x) / 2;
227
+ middle = [{ x, y: a.y }, { x, y: b.y }];
228
+ } else if (!startsAcross && !endsAcross) {
229
+ const y = (a.y + b.y) / 2;
230
+ middle = [{ x: a.x, y }, { x: b.x, y }];
231
+ } else {
232
+ middle = [startsAcross ? { x: b.x, y: a.y } : { x: a.x, y: b.y }];
233
+ }
234
+
235
+ return [start, a, ...middle, b, end];
236
+ }
237
+
238
+ /** Draws a connector between two points, leaving each in the given direction. */
239
+ export function routeBetween(start, startDir, end, endDir, style) {
240
+ if (style === "curve") {
241
+ const pull = Math.max(40, Math.hypot(end.x - start.x, end.y - start.y) / 3);
242
+ const c1 = { x: start.x + startDir.x * pull, y: start.y + startDir.y * pull };
243
+ const c2 = { x: end.x + endDir.x * pull, y: end.y + endDir.y * pull };
244
+
245
+ return {
246
+ d: `M${start.x} ${start.y}C${c1.x} ${c1.y} ${c2.x} ${c2.y} ${end.x} ${end.y}`,
247
+ start,
248
+ end,
249
+ beforeStart: c1,
250
+ beforeEnd: c2,
251
+ middle: {
252
+ x: (start.x + 3 * c1.x + 3 * c2.x + end.x) / 8,
253
+ y: (start.y + 3 * c1.y + 3 * c2.y + end.y) / 8,
254
+ },
255
+ };
256
+ }
257
+
258
+ const points = style === "straight" ? [start, end] : elbowPoints(start, startDir, end, endDir);
259
+
260
+ return {
261
+ d: `M${points.map((point) => `${point.x} ${point.y}`).join("L")}`,
262
+ start,
263
+ end,
264
+ beforeStart: points[1],
265
+ beforeEnd: points[points.length - 2],
266
+ middle: polylineMiddle(points),
267
+ };
268
+ }
269
+
270
+ const oppositeOf = (dir) => ({ x: -dir.x, y: -dir.y });
271
+
272
+ /** `pointer` replaces one end while it's being dragged. */
273
+ export function routeEdge(edge, nodes, pointer = null) {
274
+ const fromNode = nodes.find((node) => node.id === edge.from);
275
+ const toNode = nodes.find((node) => node.id === edge.to);
276
+
277
+ const fromTarget = pointer?.end === "to" ? pointer : centerOf(toNode || pointer);
278
+ const toTarget = pointer?.end === "from" ? pointer : centerOf(fromNode || pointer);
279
+
280
+ const fromSide = edge.fromSide || (fromNode && facingSide(fromNode, fromTarget));
281
+ const toSide = edge.toSide || (toNode && facingSide(toNode, toTarget));
282
+
283
+ const startDir = fromSide ? SIDES[fromSide] : null;
284
+ const endDir = toSide ? SIDES[toSide] : null;
285
+
286
+ if (pointer?.end === "to") {
287
+ const start = anchorOf(fromNode, fromSide);
288
+ return routeBetween(start, startDir, pointer, oppositeOf(startDir), edge.style);
289
+ }
290
+ if (pointer?.end === "from") {
291
+ const end = anchorOf(toNode, toSide);
292
+ return routeBetween(pointer, oppositeOf(endDir), end, endDir, edge.style);
293
+ }
294
+
295
+ return routeBetween(anchorOf(fromNode, fromSide), startDir, anchorOf(toNode, toSide), endDir, edge.style);
296
+ }
297
+
298
+ /** Edges whose node was removed can't be drawn. */
299
+ export const drawableEdges = ({ nodes, edges }) => {
300
+ const ids = new Set(nodes.map((node) => node.id));
301
+ return edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to));
302
+ };
303
+
304
+ // ---- Canvas helpers ---------------------------------------------------------
305
+
306
+ export const nodeAt = (nodes, point) =>
307
+ [...nodes]
308
+ .reverse()
309
+ .find(
310
+ ({ x, y, w, h }) =>
311
+ point.x >= x && point.x <= x + w && point.y >= y && point.y <= y + h,
312
+ );
313
+
314
+ export function nodesInRect(nodes, rect) {
315
+ return nodes.filter(
316
+ ({ x, y, w, h }) =>
317
+ x < rect.x + rect.w && x + w > rect.x && y < rect.y + rect.h && y + h > rect.y,
318
+ );
319
+ }
320
+
321
+ export function boundsOf(nodes) {
322
+ if (nodes.length === 0) return { x: 0, y: 0, w: 800, h: 500 };
323
+
324
+ const left = Math.min(...nodes.map((node) => node.x));
325
+ const top = Math.min(...nodes.map((node) => node.y));
326
+ const right = Math.max(...nodes.map((node) => node.x + node.w));
327
+ const bottom = Math.max(...nodes.map((node) => node.y + node.h));
328
+
329
+ return {
330
+ x: left - EXPORT_PADDING,
331
+ y: top - EXPORT_PADDING,
332
+ w: right - left + EXPORT_PADDING * 2,
333
+ h: bottom - top + EXPORT_PADDING * 2,
334
+ };
335
+ }
336
+
337
+ export function resizeNode(box, handle, dx, dy, keepRatio) {
338
+ const next = resizeFreely(box, handle, dx, dy, keepRatio);
339
+ const left = snap(next.x);
340
+ const top = snap(next.y);
341
+
342
+ return {
343
+ x: left,
344
+ y: top,
345
+ w: snap(next.x + next.w) - left,
346
+ h: snap(next.y + next.h) - top,
347
+ };
348
+ }
349
+
350
+ /** Copies nodes and the edges between them, offset so the copy is visible. */
351
+ export function cloneNodes(sourceNodes, sourceEdges, offset) {
352
+ const ids = new Map(sourceNodes.map((node) => [node.id, newId()]));
353
+
354
+ return {
355
+ nodes: sourceNodes.map((node) => ({
356
+ ...node,
357
+ id: ids.get(node.id),
358
+ x: node.x + offset,
359
+ y: node.y + offset,
360
+ })),
361
+ edges: sourceEdges
362
+ .filter((edge) => ids.has(edge.from) && ids.has(edge.to))
363
+ .map((edge) => ({
364
+ ...edge,
365
+ id: newId(),
366
+ from: ids.get(edge.from),
367
+ to: ids.get(edge.to),
368
+ })),
369
+ };
370
+ }
371
+
372
+ // ---- Templates --------------------------------------------------------------
373
+
374
+ function flowchart() {
375
+ const start = createNode("terminator", 400, 60, { text: "Start" });
376
+ const step = createNode("rect", 400, 170, { text: "Do the work" });
377
+ const check = createNode("diamond", 400, 300, { text: "Is it OK?" });
378
+ const fix = createNode("rect", 640, 300, { text: "Fix the problem" });
379
+ const end = createNode("terminator", 400, 430, { text: "End" });
380
+
381
+ return {
382
+ nodes: [start, step, check, fix, end],
383
+ edges: [
384
+ createEdge(start.id, step.id),
385
+ createEdge(step.id, check.id),
386
+ createEdge(check.id, end.id, { label: "Yes" }),
387
+ createEdge(check.id, fix.id, { label: "No" }),
388
+ createEdge(fix.id, step.id, { fromSide: "top", toSide: "right" }),
389
+ ],
390
+ };
391
+ }
392
+
393
+ function orgChart() {
394
+ const lead = createNode("roundRect", 400, 60, { text: "Lead" });
395
+ const teams = ["Product", "Engineering", "Sales"].map((text, index) =>
396
+ createNode("roundRect", 180 + index * 220, 200, { text }),
397
+ );
398
+ const people = createNode("roundRect", 400, 340, { text: "Team member" });
399
+
400
+ return {
401
+ nodes: [lead, ...teams, people],
402
+ edges: [
403
+ ...teams.map((team) => createEdge(lead.id, team.id, { endArrow: false })),
404
+ createEdge(teams[1].id, people.id, { endArrow: false }),
405
+ ],
406
+ };
407
+ }
408
+
409
+ function mindMap() {
410
+ const topic = createNode("ellipse", 400, 240, { text: "Main idea", w: 160, h: 100, bold: true });
411
+ const spots = [
412
+ [150, 100, "First idea"],
413
+ [650, 100, "Second idea"],
414
+ [150, 380, "Third idea"],
415
+ [650, 380, "Fourth idea"],
416
+ ];
417
+ const ideas = spots.map(([x, y, text]) => createNode("roundRect", x, y, { text }));
418
+
419
+ return {
420
+ nodes: [topic, ...ideas],
421
+ edges: ideas.map((idea) =>
422
+ createEdge(topic.id, idea.id, { style: "curve", endArrow: false }),
423
+ ),
424
+ };
425
+ }
426
+
427
+ function systemMap() {
428
+ const users = createNode("ellipse", 80, 180, { text: "Users", w: 100, h: 100 });
429
+ const app = createNode("roundRect", 300, 180, { text: "Web app" });
430
+ const api = createNode("roundRect", 520, 180, { text: "API" });
431
+ const data = createNode("cylinder", 740, 180, { text: "Database" });
432
+ const service = createNode("cloud", 520, 340, { text: "Third-party service" });
433
+
434
+ return {
435
+ nodes: [users, app, api, data, service],
436
+ edges: [
437
+ createEdge(users.id, app.id),
438
+ createEdge(app.id, api.id),
439
+ createEdge(api.id, data.id),
440
+ createEdge(api.id, service.id, { dash: "dash" }),
441
+ ],
442
+ };
443
+ }
444
+
445
+ export const DIAGRAM_TEMPLATES = [
446
+ { id: "blank", name: "Blank", description: "An empty canvas.", group: "Basics", build: () => ({ nodes: [], edges: [] }) },
447
+ { id: "flowchart", name: "Flowchart", description: "Start, a step, a decision and an end.", group: "Basics", build: flowchart },
448
+ { id: "org", name: "Org chart", description: "A lead, teams and people.", group: "Basics", build: orgChart },
449
+ { id: "mind", name: "Mind map", description: "One idea in the middle, four around it.", group: "Basics", build: mindMap },
450
+ { id: "system", name: "System map", description: "Users, an app, an API and a database.", group: "Basics", build: systemMap },
451
+ ];