@modern-ant/model-diagram 0.2.5

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/THIRD_PARTY_NOTICES.md +32 -0
  4. package/dist/events/emitter.d.ts +8 -0
  5. package/dist/events/emitter.d.ts.map +1 -0
  6. package/dist/events/emitter.js +30 -0
  7. package/dist/events/emitter.js.map +1 -0
  8. package/dist/events/types.d.ts +94 -0
  9. package/dist/events/types.d.ts.map +1 -0
  10. package/dist/events/types.js +2 -0
  11. package/dist/events/types.js.map +1 -0
  12. package/dist/index.d.ts +8 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +4 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/jointjs/adapter.d.ts +75 -0
  17. package/dist/jointjs/adapter.d.ts.map +1 -0
  18. package/dist/jointjs/adapter.js +905 -0
  19. package/dist/jointjs/adapter.js.map +1 -0
  20. package/dist/layout/elk-layout.d.ts +15 -0
  21. package/dist/layout/elk-layout.d.ts.map +1 -0
  22. package/dist/layout/elk-layout.js +112 -0
  23. package/dist/layout/elk-layout.js.map +1 -0
  24. package/dist/model/types.d.ts +45 -0
  25. package/dist/model/types.d.ts.map +1 -0
  26. package/dist/model/types.js +2 -0
  27. package/dist/model/types.js.map +1 -0
  28. package/dist/model/validation.d.ts +8 -0
  29. package/dist/model/validation.d.ts.map +1 -0
  30. package/dist/model/validation.js +95 -0
  31. package/dist/model/validation.js.map +1 -0
  32. package/dist/rendering/diagram.d.ts +96 -0
  33. package/dist/rendering/diagram.d.ts.map +1 -0
  34. package/dist/rendering/diagram.js +733 -0
  35. package/dist/rendering/diagram.js.map +1 -0
  36. package/dist/rendering/geometry.d.ts +20 -0
  37. package/dist/rendering/geometry.d.ts.map +1 -0
  38. package/dist/rendering/geometry.js +42 -0
  39. package/dist/rendering/geometry.js.map +1 -0
  40. package/dist/rendering/store.d.ts +30 -0
  41. package/dist/rendering/store.d.ts.map +1 -0
  42. package/dist/rendering/store.js +231 -0
  43. package/dist/rendering/store.js.map +1 -0
  44. package/dist/routing/relationship-style.d.ts +15 -0
  45. package/dist/routing/relationship-style.d.ts.map +1 -0
  46. package/dist/routing/relationship-style.js +37 -0
  47. package/dist/routing/relationship-style.js.map +1 -0
  48. package/package.json +58 -0
@@ -0,0 +1,905 @@
1
+ import * as joint from "@joint/core";
2
+ import { getRelationshipAppearance } from "../routing/relationship-style.js";
3
+ import { CLASS_HEADER_HEIGHT, NOTE_HEIGHT, NOTE_WIDTH, clientToDiagramPosition, classSize, positionsEqual, } from "../rendering/geometry.js";
4
+ const INTERNAL = { modelDiagramInternal: true };
5
+ const LINE_COLOR = "#334155";
6
+ const SELECTED_COLOR = "#2563eb";
7
+ const WAYPOINT_HIT_RADIUS = 14;
8
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
9
+ const NO_MARKER = {
10
+ type: "path",
11
+ d: "M 0 0",
12
+ fill: "none",
13
+ stroke: "none",
14
+ };
15
+ // JointJS's stock vertex handle is a 12 px circle. Keep that visual size while
16
+ // giving pointer input a forgiving, transparent 28 px target. Pointer movement
17
+ // is handled by the adapter below so mouse, pen, and touch use one code path.
18
+ const WaypointHandle = joint.linkTools.Vertices.VertexHandle.extend({
19
+ tagName: "g",
20
+ events: {},
21
+ documentEvents: {},
22
+ attributes: { cursor: "move" },
23
+ render() {
24
+ const hitTarget = this.el.ownerDocument.createElementNS(SVG_NAMESPACE, "circle");
25
+ hitTarget.classList.add("model-diagram-waypoint-hit");
26
+ hitTarget.setAttribute("r", String(WAYPOINT_HIT_RADIUS));
27
+ hitTarget.setAttribute("fill", "transparent");
28
+ hitTarget.setAttribute("stroke", "transparent");
29
+ hitTarget.setAttribute("pointer-events", "all");
30
+ const marker = this.el.ownerDocument.createElementNS(SVG_NAMESPACE, "circle");
31
+ marker.classList.add("model-diagram-waypoint-marker");
32
+ marker.setAttribute("r", "6");
33
+ marker.setAttribute("fill", "#33334f");
34
+ marker.setAttribute("stroke", "#ffffff");
35
+ marker.setAttribute("stroke-width", "2");
36
+ marker.setAttribute("pointer-events", "none");
37
+ this.el.setAttribute("data-waypoint-index", String(this.options.index));
38
+ this.el.replaceChildren(hitTarget, marker);
39
+ return this;
40
+ },
41
+ });
42
+ function positionFromElement(element) {
43
+ const position = element.position();
44
+ const size = element.size();
45
+ return { x: position.x + size.width / 2, y: position.y + size.height / 2 };
46
+ }
47
+ function metadata(cell) {
48
+ const kind = cell.get("semanticKind");
49
+ const id = cell.get("semanticId");
50
+ return kind && id ? { kind, id } : undefined;
51
+ }
52
+ function formatAttributes(attributes) {
53
+ if (!attributes || attributes.length === 0)
54
+ return "(no attributes)";
55
+ return attributes
56
+ .map((attribute) => {
57
+ const indicator = attribute.required ? "●" : "○";
58
+ const type = attribute.type ? `: ${attribute.type}` : "";
59
+ const multiplicity = attribute.multiplicity
60
+ ? ` [${attribute.multiplicity}]`
61
+ : "";
62
+ return `${indicator} ${attribute.name}${type}${multiplicity}`;
63
+ })
64
+ .join("\n");
65
+ }
66
+ function classMarkup() {
67
+ return [
68
+ { tagName: "rect", selector: "body" },
69
+ { tagName: "rect", selector: "header" },
70
+ { tagName: "text", selector: "stereotype" },
71
+ { tagName: "text", selector: "className" },
72
+ { tagName: "text", selector: "attributes" },
73
+ ];
74
+ }
75
+ function noteMarkup() {
76
+ return [
77
+ { tagName: "path", selector: "body" },
78
+ { tagName: "path", selector: "fold" },
79
+ { tagName: "text", selector: "note" },
80
+ ];
81
+ }
82
+ function closestSegmentIndex(point, points) {
83
+ let bestIndex = 0;
84
+ let bestDistance = Number.POSITIVE_INFINITY;
85
+ for (let index = 0; index < points.length - 1; index += 1) {
86
+ const start = points[index];
87
+ const end = points[index + 1];
88
+ if (!start || !end)
89
+ continue;
90
+ const dx = end.x - start.x;
91
+ const dy = end.y - start.y;
92
+ const lengthSquared = dx * dx + dy * dy;
93
+ const ratio = lengthSquared === 0
94
+ ? 0
95
+ : Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) /
96
+ lengthSquared));
97
+ const projection = { x: start.x + ratio * dx, y: start.y + ratio * dy };
98
+ const distance = Math.hypot(point.x - projection.x, point.y - projection.y);
99
+ if (distance < bestDistance) {
100
+ bestDistance = distance;
101
+ bestIndex = index;
102
+ }
103
+ }
104
+ return bestIndex;
105
+ }
106
+ export class JointJsAdapter {
107
+ container;
108
+ callbacks;
109
+ graph = new joint.dia.Graph();
110
+ surface;
111
+ paper;
112
+ classCells = new Map();
113
+ noteCells = new Map();
114
+ relationshipCells = new Map();
115
+ noteConnectorCells = new Map();
116
+ nativeListeners = [];
117
+ originalContainerStyle;
118
+ hadContainerClass;
119
+ resizeObserver;
120
+ dragState;
121
+ panState;
122
+ waypointDragState;
123
+ selection = null;
124
+ editable;
125
+ scaleValue = 1;
126
+ translation = { x: 0, y: 0 };
127
+ destroyed = false;
128
+ constructor(container, editable, callbacks) {
129
+ this.container = container;
130
+ this.callbacks = callbacks;
131
+ this.editable = editable;
132
+ this.originalContainerStyle = {
133
+ position: container.style.position,
134
+ overflow: container.style.overflow,
135
+ touchAction: container.style.touchAction,
136
+ };
137
+ this.hadContainerClass = container.classList.contains("model-diagram");
138
+ this.prepareContainer();
139
+ this.surface = document.createElement("div");
140
+ this.surface.classList.add("model-diagram-surface");
141
+ container.append(this.surface);
142
+ this.paper = new joint.dia.Paper({
143
+ el: this.surface,
144
+ model: this.graph,
145
+ width: Math.max(container.clientWidth, 1),
146
+ height: Math.max(container.clientHeight, 1),
147
+ gridSize: 10,
148
+ drawGrid: { name: "mesh", args: { color: "#e2e8f0", thickness: 1 } },
149
+ background: { color: "#f8fafc" },
150
+ async: false,
151
+ sorting: joint.dia.Paper.sorting.APPROX,
152
+ interactive: (cellView) => {
153
+ const info = metadata(cellView.model);
154
+ return Boolean(this.editable && (info?.kind === "class" || info?.kind === "note"));
155
+ },
156
+ defaultConnectionPoint: { name: "boundary", args: { offset: 2 } },
157
+ });
158
+ // Paper.render() sets its element to position: relative. The paper element
159
+ // is library-owned and must not participate in sizing the consumer host.
160
+ this.surface.style.position = "absolute";
161
+ this.surface.style.left = "0";
162
+ this.surface.style.top = "0";
163
+ this.bindJointEvents();
164
+ this.bindNativeEvents();
165
+ if (typeof ResizeObserver !== "undefined") {
166
+ this.resizeObserver = new ResizeObserver(() => this.resize());
167
+ this.resizeObserver.observe(container);
168
+ }
169
+ }
170
+ render(store) {
171
+ this.assertAlive();
172
+ const model = store.model;
173
+ const classIds = new Set(model.classes.map(({ id }) => id));
174
+ const relationshipIds = new Set(model.relationships.map(({ id }) => id));
175
+ const noteIds = new Set(model.classes.filter(({ note }) => note).map(({ id }) => id));
176
+ this.removeMissing(this.classCells, classIds);
177
+ this.removeMissing(this.noteCells, noteIds);
178
+ this.removeMissing(this.noteConnectorCells, noteIds);
179
+ this.removeMissing(this.relationshipCells, relationshipIds);
180
+ for (const diagramClass of model.classes) {
181
+ let cell = this.classCells.get(diagramClass.id);
182
+ if (!cell) {
183
+ cell = this.createClassCell(diagramClass.id);
184
+ this.classCells.set(diagramClass.id, cell);
185
+ this.graph.addCell(cell, INTERNAL);
186
+ }
187
+ const size = classSize(diagramClass);
188
+ cell.resize(size.width, size.height, INTERNAL);
189
+ cell.position(store.getClassPosition(diagramClass.id).x - size.width / 2, store.getClassPosition(diagramClass.id).y - size.height / 2, INTERNAL);
190
+ cell.attr({
191
+ body: { width: size.width, height: size.height },
192
+ header: { width: size.width, height: CLASS_HEADER_HEIGHT },
193
+ stereotype: {
194
+ text: diagramClass.stereotype ? `«${diagramClass.stereotype}»` : "",
195
+ x: size.width / 2,
196
+ y: 18,
197
+ },
198
+ className: {
199
+ text: diagramClass.name,
200
+ x: size.width / 2,
201
+ y: diagramClass.stereotype ? 41 : 31,
202
+ },
203
+ attributes: {
204
+ text: formatAttributes(diagramClass.attributes),
205
+ x: 14,
206
+ y: CLASS_HEADER_HEIGHT + 18,
207
+ },
208
+ }, INTERNAL);
209
+ if (diagramClass.note) {
210
+ this.upsertNote(diagramClass.id, diagramClass.note, store);
211
+ }
212
+ }
213
+ for (const relationship of model.relationships) {
214
+ this.upsertRelationship(relationship, store);
215
+ }
216
+ this.applySelectionAppearance();
217
+ }
218
+ syncAllPositions(store) {
219
+ for (const diagramClass of store.model.classes) {
220
+ this.syncClassPosition(diagramClass.id, store);
221
+ if (diagramClass.note)
222
+ this.syncNotePosition(diagramClass.id, store);
223
+ }
224
+ for (const relationship of store.model.relationships) {
225
+ this.syncWaypoints(relationship.id, store);
226
+ }
227
+ }
228
+ syncClassPosition(classId, store) {
229
+ const cell = this.classCells.get(classId);
230
+ if (!cell)
231
+ return;
232
+ const position = store.getClassPosition(classId);
233
+ const size = cell.size();
234
+ cell.position(position.x - size.width / 2, position.y - size.height / 2, INTERNAL);
235
+ }
236
+ syncNotePosition(classId, store) {
237
+ const cell = this.noteCells.get(classId);
238
+ if (!cell)
239
+ return;
240
+ const position = store.getNotePosition(classId);
241
+ cell.position(position.x - NOTE_WIDTH / 2, position.y - NOTE_HEIGHT / 2, INTERNAL);
242
+ }
243
+ syncWaypoints(relationshipId, store) {
244
+ const link = this.relationshipCells.get(relationshipId);
245
+ if (!link)
246
+ return;
247
+ const waypoints = store.getWaypoints(relationshipId);
248
+ link.vertices(waypoints, INTERNAL);
249
+ this.applyRouter(link, waypoints.length > 0);
250
+ }
251
+ syncRelationship(relationshipId, store) {
252
+ this.upsertRelationship(store.getRelationship(relationshipId), store);
253
+ this.applySelectionAppearance();
254
+ }
255
+ setEditable(editable) {
256
+ this.editable = editable;
257
+ this.paper.setInteractivity((cellView) => {
258
+ const info = metadata(cellView.model);
259
+ return Boolean(this.editable && (info?.kind === "class" || info?.kind === "note"));
260
+ });
261
+ if (!editable)
262
+ this.removeTools();
263
+ else
264
+ this.showRelationshipTools();
265
+ }
266
+ setSelection(selection) {
267
+ const previousRelationshipId = this.selectedRelationshipId();
268
+ this.selection = selection;
269
+ this.applySelectionAppearance();
270
+ if (previousRelationshipId !== this.selectedRelationshipId()) {
271
+ this.showRelationshipTools();
272
+ }
273
+ }
274
+ fitToContent(padding = 48) {
275
+ const width = Math.max(this.container.clientWidth, 1);
276
+ const height = Math.max(this.container.clientHeight, 1);
277
+ const bbox = this.paper.getContentBBox();
278
+ if (bbox.width === 0 || bbox.height === 0)
279
+ return;
280
+ const scale = Math.max(0.1, Math.min(2, (width - padding * 2) / bbox.width, (height - padding * 2) / bbox.height));
281
+ this.scaleValue = scale;
282
+ this.translation = {
283
+ x: (width - bbox.width * scale) / 2 - bbox.x * scale,
284
+ y: (height - bbox.height * scale) / 2 - bbox.y * scale,
285
+ };
286
+ this.applyTransform();
287
+ this.showRelationshipTools();
288
+ }
289
+ setZoom(scale, center) {
290
+ const nextScale = Math.max(0.1, Math.min(4, scale));
291
+ const viewportCenter = center ?? {
292
+ x: this.container.clientWidth / 2,
293
+ y: this.container.clientHeight / 2,
294
+ };
295
+ const local = {
296
+ x: (viewportCenter.x - this.translation.x) / this.scaleValue,
297
+ y: (viewportCenter.y - this.translation.y) / this.scaleValue,
298
+ };
299
+ this.scaleValue = nextScale;
300
+ this.translation = {
301
+ x: viewportCenter.x - local.x * nextScale,
302
+ y: viewportCenter.y - local.y * nextScale,
303
+ };
304
+ this.applyTransform();
305
+ this.showRelationshipTools();
306
+ }
307
+ getZoom() {
308
+ return this.scaleValue;
309
+ }
310
+ panBy(dx, dy) {
311
+ this.translation.x += dx;
312
+ this.translation.y += dy;
313
+ this.applyTransform();
314
+ }
315
+ destroy() {
316
+ if (this.destroyed)
317
+ return;
318
+ this.destroyed = true;
319
+ this.resizeObserver?.disconnect();
320
+ for (const removeListener of this.nativeListeners.splice(0))
321
+ removeListener();
322
+ this.paper.remove();
323
+ this.surface.remove();
324
+ this.graph.clear({ silent: true });
325
+ this.classCells.clear();
326
+ this.noteCells.clear();
327
+ this.relationshipCells.clear();
328
+ this.noteConnectorCells.clear();
329
+ this.container.style.position = this.originalContainerStyle.position;
330
+ this.container.style.overflow = this.originalContainerStyle.overflow;
331
+ this.container.style.touchAction = this.originalContainerStyle.touchAction;
332
+ if (!this.hadContainerClass) {
333
+ this.container.classList.remove("model-diagram");
334
+ }
335
+ }
336
+ prepareContainer() {
337
+ if (getComputedStyle(this.container).position === "static") {
338
+ this.container.style.position = "relative";
339
+ }
340
+ this.container.style.overflow = "hidden";
341
+ this.container.style.touchAction = "none";
342
+ this.container.classList.add("model-diagram");
343
+ }
344
+ createClassCell(id) {
345
+ const cell = new joint.dia.Element();
346
+ cell.set({
347
+ type: "model-diagram.Class",
348
+ semanticKind: "class",
349
+ semanticId: id,
350
+ z: 20,
351
+ markup: classMarkup(),
352
+ attrs: {
353
+ body: {
354
+ rx: 7,
355
+ ry: 7,
356
+ fill: "#ffffff",
357
+ stroke: LINE_COLOR,
358
+ strokeWidth: 1.5,
359
+ },
360
+ header: {
361
+ rx: 7,
362
+ ry: 7,
363
+ fill: "#e2e8f0",
364
+ stroke: LINE_COLOR,
365
+ strokeWidth: 1.5,
366
+ },
367
+ stereotype: {
368
+ textAnchor: "middle",
369
+ fontFamily: "Inter, system-ui, sans-serif",
370
+ fontSize: 12,
371
+ fill: "#475569",
372
+ },
373
+ className: {
374
+ textAnchor: "middle",
375
+ fontFamily: "Inter, system-ui, sans-serif",
376
+ fontSize: 15,
377
+ fontWeight: 700,
378
+ fill: "#0f172a",
379
+ },
380
+ attributes: {
381
+ textAnchor: "start",
382
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
383
+ fontSize: 12,
384
+ lineHeight: "1.9em",
385
+ fill: "#1e293b",
386
+ },
387
+ },
388
+ });
389
+ return cell;
390
+ }
391
+ upsertNote(classId, text, store) {
392
+ let note = this.noteCells.get(classId);
393
+ if (!note) {
394
+ const createdNote = new joint.dia.Element();
395
+ createdNote.set({
396
+ type: "model-diagram.Note",
397
+ semanticKind: "note",
398
+ semanticId: classId,
399
+ z: 20,
400
+ markup: noteMarkup(),
401
+ attrs: {
402
+ body: {
403
+ d: `M 0 0 H ${NOTE_WIDTH - 20} L ${NOTE_WIDTH} 20 V ${NOTE_HEIGHT} H 0 Z`,
404
+ fill: "#fef3c7",
405
+ stroke: "#b45309",
406
+ strokeWidth: 1.25,
407
+ },
408
+ fold: {
409
+ d: `M ${NOTE_WIDTH - 20} 0 V 20 H ${NOTE_WIDTH}`,
410
+ fill: "none",
411
+ stroke: "#b45309",
412
+ strokeWidth: 1.25,
413
+ },
414
+ note: {
415
+ x: 12,
416
+ y: 16,
417
+ width: NOTE_WIDTH - 28,
418
+ height: NOTE_HEIGHT - 28,
419
+ textAnchor: "start",
420
+ textVerticalAnchor: "top",
421
+ fontFamily: "Inter, system-ui, sans-serif",
422
+ fontSize: 12,
423
+ lineHeight: "1.35em",
424
+ fill: "#78350f",
425
+ textWrap: { width: NOTE_WIDTH - 28, height: NOTE_HEIGHT - 28 },
426
+ },
427
+ },
428
+ });
429
+ createdNote.resize(NOTE_WIDTH, NOTE_HEIGHT, INTERNAL);
430
+ this.noteCells.set(classId, createdNote);
431
+ this.graph.addCell(createdNote, INTERNAL);
432
+ note = createdNote;
433
+ }
434
+ note.attr("note/text", text, INTERNAL);
435
+ this.syncNotePosition(classId, store);
436
+ let connector = this.noteConnectorCells.get(classId);
437
+ if (!connector) {
438
+ connector = new joint.shapes.standard.Link({
439
+ type: "model-diagram.NoteConnector",
440
+ semanticKind: "note-connector",
441
+ semanticId: classId,
442
+ z: 5,
443
+ attrs: {
444
+ line: {
445
+ stroke: "#b45309",
446
+ strokeWidth: 1,
447
+ strokeDasharray: "4 4",
448
+ sourceMarker: NO_MARKER,
449
+ targetMarker: NO_MARKER,
450
+ },
451
+ },
452
+ });
453
+ this.noteConnectorCells.set(classId, connector);
454
+ this.graph.addCell(connector, INTERNAL);
455
+ }
456
+ const classCell = this.classCells.get(classId);
457
+ if (classCell) {
458
+ connector.source(classCell, INTERNAL);
459
+ connector.target(note, INTERNAL);
460
+ connector.router("normal", {}, INTERNAL);
461
+ connector.connector("straight", {}, INTERNAL);
462
+ }
463
+ }
464
+ upsertRelationship(relationship, store) {
465
+ let link = this.relationshipCells.get(relationship.id);
466
+ if (!link) {
467
+ link = new joint.shapes.standard.Link({
468
+ type: "model-diagram.Relationship",
469
+ semanticKind: "relationship",
470
+ semanticId: relationship.id,
471
+ z: 10,
472
+ });
473
+ this.relationshipCells.set(relationship.id, link);
474
+ this.graph.addCell(link, INTERNAL);
475
+ }
476
+ const source = this.classCells.get(relationship.from);
477
+ const target = this.classCells.get(relationship.to);
478
+ if (!source || !target)
479
+ return;
480
+ link.source(source, INTERNAL);
481
+ link.target(target, INTERNAL);
482
+ const appearance = getRelationshipAppearance(relationship.type);
483
+ link.attr("line", {
484
+ stroke: LINE_COLOR,
485
+ strokeWidth: 1.6,
486
+ strokeDasharray: appearance.strokeDasharray ?? "none",
487
+ sourceMarker: appearance.sourceMarker ?? NO_MARKER,
488
+ targetMarker: appearance.targetMarker ?? NO_MARKER,
489
+ }, INTERNAL);
490
+ const labels = [];
491
+ if (relationship.label?.trim()) {
492
+ labels.push(this.makeRelationshipLabel(relationship.label));
493
+ }
494
+ if (relationship.role) {
495
+ labels.push(this.makeRoleLabel(relationship.role));
496
+ }
497
+ if (relationship.fromMultiplicity) {
498
+ labels.push(this.makeMultiplicityLabel(relationship.fromMultiplicity, 0.12));
499
+ }
500
+ if (relationship.toMultiplicity) {
501
+ labels.push(this.makeMultiplicityLabel(relationship.toMultiplicity, 0.88));
502
+ }
503
+ link.labels(labels, INTERNAL);
504
+ this.syncWaypoints(relationship.id, store);
505
+ }
506
+ makeRelationshipLabel(text) {
507
+ return {
508
+ markup: [
509
+ {
510
+ tagName: "text",
511
+ selector: "text",
512
+ className: "model-diagram-relationship-label",
513
+ },
514
+ ],
515
+ position: { distance: 0.5, offset: 14, args: { keepGradient: false } },
516
+ attrs: {
517
+ text: {
518
+ text,
519
+ fill: "#0f172a",
520
+ fontFamily: "Inter, system-ui, sans-serif",
521
+ fontSize: 12,
522
+ fontWeight: 600,
523
+ textAnchor: "middle",
524
+ textVerticalAnchor: "middle",
525
+ pointerEvents: "none",
526
+ },
527
+ },
528
+ };
529
+ }
530
+ makeRoleLabel(text) {
531
+ return {
532
+ position: { distance: 0.5, offset: -13, args: { keepGradient: false } },
533
+ attrs: {
534
+ rect: {
535
+ fill: "#f8fafc",
536
+ stroke: "#cbd5e1",
537
+ strokeWidth: 0.75,
538
+ rx: 3,
539
+ ry: 3,
540
+ ref: "text",
541
+ refWidth: "120%",
542
+ refHeight: "140%",
543
+ refX: "-10%",
544
+ refY: "-20%",
545
+ },
546
+ text: {
547
+ text,
548
+ fill: "#334155",
549
+ fontFamily: "Inter, system-ui, sans-serif",
550
+ fontSize: 11,
551
+ fontWeight: 600,
552
+ textAnchor: "middle",
553
+ textVerticalAnchor: "middle",
554
+ },
555
+ },
556
+ };
557
+ }
558
+ makeMultiplicityLabel(text, distance) {
559
+ return {
560
+ markup: [
561
+ {
562
+ tagName: "text",
563
+ selector: "text",
564
+ className: "model-diagram-multiplicity-label",
565
+ },
566
+ ],
567
+ position: { distance, offset: 13, args: { keepGradient: false } },
568
+ attrs: {
569
+ text: {
570
+ text,
571
+ fill: "#334155",
572
+ fontFamily: "Inter, system-ui, sans-serif",
573
+ fontSize: 11,
574
+ fontWeight: 500,
575
+ textAnchor: "middle",
576
+ textVerticalAnchor: "middle",
577
+ pointerEvents: "none",
578
+ },
579
+ },
580
+ };
581
+ }
582
+ applyRouter(link, hasWaypoints) {
583
+ if (hasWaypoints) {
584
+ link.router("normal", {}, INTERNAL);
585
+ link.connector("rounded", { radius: 10 }, INTERNAL);
586
+ }
587
+ else {
588
+ link.router("manhattan", { padding: 28, step: 10 }, INTERNAL);
589
+ link.connector("rounded", { radius: 10 }, INTERNAL);
590
+ }
591
+ }
592
+ bindJointEvents() {
593
+ this.paper.on("element:pointerdown", (view, event) => {
594
+ const info = metadata(view.model);
595
+ if (!info || (info.kind !== "class" && info.kind !== "note"))
596
+ return;
597
+ this.select({ kind: info.kind, id: info.id });
598
+ if (!this.editable || event.button !== 0)
599
+ return;
600
+ this.callbacks.onPositionDragStarted(info.kind, info.id);
601
+ this.dragState = {
602
+ cell: view.model,
603
+ kind: info.kind,
604
+ id: info.id,
605
+ start: positionFromElement(view.model),
606
+ };
607
+ });
608
+ this.paper.on("element:pointerup", (view) => {
609
+ if (!this.dragState || this.dragState.cell !== view.model)
610
+ return;
611
+ const completed = this.dragState;
612
+ this.dragState = undefined;
613
+ const position = positionFromElement(completed.cell);
614
+ if (!positionsEqual(completed.start, position)) {
615
+ this.callbacks.onPositionCommitted(completed.kind, completed.id, completed.start, position);
616
+ }
617
+ });
618
+ this.paper.on("link:pointerclick", (view) => {
619
+ const info = metadata(view.model);
620
+ if (info?.kind === "relationship") {
621
+ this.select({ kind: "relationship", id: info.id });
622
+ }
623
+ });
624
+ this.paper.on("blank:pointerclick", () => this.select(null));
625
+ this.paper.on("blank:pointerdown", (event) => {
626
+ if (event.button !== 0)
627
+ return;
628
+ this.panState = {
629
+ x: event.clientX ?? 0,
630
+ y: event.clientY ?? 0,
631
+ tx: this.translation.x,
632
+ ty: this.translation.y,
633
+ };
634
+ });
635
+ this.graph.on("change:position", (cell, _position, options) => {
636
+ if (options.modelDiagramInternal)
637
+ return;
638
+ const info = metadata(cell);
639
+ if (!info || (info.kind !== "class" && info.kind !== "note"))
640
+ return;
641
+ this.callbacks.onPositionPreview(info.kind, info.id, positionFromElement(cell));
642
+ });
643
+ this.graph.on("change:vertices", (link, vertices, options) => {
644
+ if (options.modelDiagramInternal)
645
+ return;
646
+ const info = metadata(link);
647
+ if (info?.kind !== "relationship")
648
+ return;
649
+ this.callbacks.onWaypointsPreview(info.id, vertices.map(({ x, y }) => ({ x, y })));
650
+ });
651
+ }
652
+ bindNativeEvents() {
653
+ const onPointerDown = (event) => {
654
+ if (!this.editable)
655
+ return;
656
+ if (event.button !== 0)
657
+ return;
658
+ const waypoint = this.waypointFromTarget(event.target);
659
+ if (!waypoint)
660
+ return;
661
+ const link = this.relationshipCells.get(waypoint.relationshipId);
662
+ if (!link)
663
+ return;
664
+ event.preventDefault();
665
+ event.stopImmediatePropagation();
666
+ this.panState = undefined;
667
+ this.waypointDragState = {
668
+ pointerId: event.pointerId,
669
+ relationshipId: waypoint.relationshipId,
670
+ index: waypoint.index,
671
+ before: link.vertices().map(({ x, y }) => ({ x, y })),
672
+ };
673
+ this.paper.el.setPointerCapture?.(event.pointerId);
674
+ this.select({
675
+ kind: "relationship-waypoint",
676
+ relationshipId: waypoint.relationshipId,
677
+ index: waypoint.index,
678
+ });
679
+ };
680
+ const onPointerMove = (event) => {
681
+ const waypointDrag = this.waypointDragState;
682
+ if (waypointDrag && event.pointerId === waypointDrag.pointerId) {
683
+ event.preventDefault();
684
+ event.stopImmediatePropagation();
685
+ const link = this.relationshipCells.get(waypointDrag.relationshipId);
686
+ const vertices = link?.vertices().map(({ x, y }) => ({ x, y }));
687
+ const currentPosition = vertices?.[waypointDrag.index];
688
+ if (!link || !vertices || !currentPosition)
689
+ return;
690
+ const bounds = this.surface.getBoundingClientRect();
691
+ const position = clientToDiagramPosition({ x: event.clientX, y: event.clientY }, { x: bounds.left, y: bounds.top }, this.scaleValue, this.translation);
692
+ if (positionsEqual(currentPosition, position))
693
+ return;
694
+ vertices[waypointDrag.index] = position;
695
+ link.vertices(vertices, { modelDiagramWaypointDrag: true });
696
+ return;
697
+ }
698
+ if (!this.panState)
699
+ return;
700
+ this.translation = {
701
+ x: this.panState.tx + event.clientX - this.panState.x,
702
+ y: this.panState.ty + event.clientY - this.panState.y,
703
+ };
704
+ this.applyTransform();
705
+ };
706
+ const finishWaypointDrag = (event) => {
707
+ const waypointDrag = this.waypointDragState;
708
+ if (!waypointDrag || event.pointerId !== waypointDrag.pointerId) {
709
+ return false;
710
+ }
711
+ event.preventDefault();
712
+ event.stopImmediatePropagation();
713
+ this.waypointDragState = undefined;
714
+ if (this.paper.el.hasPointerCapture?.(event.pointerId)) {
715
+ this.paper.el.releasePointerCapture(event.pointerId);
716
+ }
717
+ const after = this.relationshipCells
718
+ .get(waypointDrag.relationshipId)
719
+ ?.vertices()
720
+ .map(({ x, y }) => ({ x, y }));
721
+ const previous = waypointDrag.before[waypointDrag.index];
722
+ const position = after?.[waypointDrag.index];
723
+ if (after &&
724
+ previous &&
725
+ position &&
726
+ after.length === waypointDrag.before.length &&
727
+ !positionsEqual(previous, position)) {
728
+ this.callbacks.onWaypointsCommitted(waypointDrag.relationshipId, waypointDrag.before, after);
729
+ }
730
+ return true;
731
+ };
732
+ const onPointerUp = (event) => {
733
+ if (finishWaypointDrag(event))
734
+ return;
735
+ this.panState = undefined;
736
+ };
737
+ const onWheel = (event) => {
738
+ event.preventDefault();
739
+ const bounds = this.surface.getBoundingClientRect();
740
+ const center = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
741
+ this.setZoom(this.scaleValue * (event.deltaY < 0 ? 1.12 : 1 / 1.12), center);
742
+ };
743
+ const onDoubleClick = (event) => {
744
+ if (!this.editable || !(event.target instanceof Element))
745
+ return;
746
+ const waypoint = this.waypointFromTarget(event.target);
747
+ if (waypoint) {
748
+ event.preventDefault();
749
+ event.stopImmediatePropagation();
750
+ this.select({
751
+ kind: "relationship-waypoint",
752
+ relationshipId: waypoint.relationshipId,
753
+ index: waypoint.index,
754
+ });
755
+ this.callbacks.onDeleteSelectedWaypoint();
756
+ return;
757
+ }
758
+ if (event.target.closest(".joint-tool"))
759
+ return;
760
+ const linkNode = event.target.closest(".joint-link");
761
+ const modelId = linkNode?.getAttribute("model-id");
762
+ const link = modelId ? this.graph.getCell(modelId) : undefined;
763
+ if (!(link instanceof joint.dia.Link))
764
+ return;
765
+ const semanticLink = link;
766
+ const info = metadata(semanticLink);
767
+ if (info?.kind !== "relationship")
768
+ return;
769
+ event.preventDefault();
770
+ const localPoint = this.paper.clientToLocalPoint(event.clientX, event.clientY);
771
+ const point = { x: localPoint.x, y: localPoint.y };
772
+ const source = this.linkEndpointCenter(semanticLink, "source");
773
+ const target = this.linkEndpointCenter(semanticLink, "target");
774
+ const vertices = semanticLink.vertices().map(({ x, y }) => ({ x, y }));
775
+ const index = closestSegmentIndex(point, [source, ...vertices, target]);
776
+ this.callbacks.onWaypointAddRequested(info.id, point, index);
777
+ };
778
+ const onKeyDown = (event) => {
779
+ if (this.editable &&
780
+ (event.key === "Delete" || event.key === "Backspace") &&
781
+ this.selection?.kind === "relationship-waypoint") {
782
+ event.preventDefault();
783
+ this.callbacks.onDeleteSelectedWaypoint();
784
+ }
785
+ };
786
+ this.addNativeListener(this.paper.el, "pointerdown", onPointerDown, true);
787
+ this.addNativeListener(document, "pointermove", onPointerMove);
788
+ this.addNativeListener(document, "pointerup", onPointerUp);
789
+ this.addNativeListener(document, "pointercancel", onPointerUp);
790
+ this.addNativeListener(this.paper.el, "wheel", onWheel, { passive: false });
791
+ this.addNativeListener(this.paper.el, "dblclick", onDoubleClick);
792
+ this.addNativeListener(document, "keydown", onKeyDown);
793
+ }
794
+ addNativeListener(target, type, listener, options) {
795
+ target.addEventListener(type, listener, options);
796
+ this.nativeListeners.push(() => target.removeEventListener(type, listener, options));
797
+ }
798
+ select(selection) {
799
+ this.setSelection(selection);
800
+ this.callbacks.onSelectionChanged(selection);
801
+ }
802
+ showRelationshipTools() {
803
+ this.removeTools();
804
+ if (!this.editable || !this.selection)
805
+ return;
806
+ const relationshipId = this.selection.kind === "relationship"
807
+ ? this.selection.id
808
+ : this.selection.kind === "relationship-waypoint"
809
+ ? this.selection.relationshipId
810
+ : undefined;
811
+ if (!relationshipId)
812
+ return;
813
+ const link = this.relationshipCells.get(relationshipId);
814
+ if (!link)
815
+ return;
816
+ const view = this.paper.requireView(link);
817
+ view.addTools(new joint.dia.ToolsView({
818
+ tools: [
819
+ new joint.linkTools.Vertices({
820
+ handleClass: WaypointHandle,
821
+ scale: 1 / this.scaleValue,
822
+ vertexAdding: false,
823
+ vertexRemoving: false,
824
+ vertexMoving: false,
825
+ redundancyRemoval: false,
826
+ snapRadius: 5,
827
+ }),
828
+ ],
829
+ }));
830
+ }
831
+ removeTools() {
832
+ for (const link of this.relationshipCells.values()) {
833
+ this.paper.findViewByModel(link)?.removeTools();
834
+ }
835
+ }
836
+ selectedRelationshipId() {
837
+ return this.selection?.kind === "relationship"
838
+ ? this.selection.id
839
+ : this.selection?.kind === "relationship-waypoint"
840
+ ? this.selection.relationshipId
841
+ : undefined;
842
+ }
843
+ waypointFromTarget(target) {
844
+ if (!(target instanceof Element))
845
+ return undefined;
846
+ const handle = target.closest(".joint-marker-vertex");
847
+ const tool = handle?.closest(".joint-tool");
848
+ const modelId = tool?.getAttribute("model-id");
849
+ const link = modelId ? this.graph.getCell(modelId) : undefined;
850
+ const info = link ? metadata(link) : undefined;
851
+ const index = Number(handle?.getAttribute("data-waypoint-index"));
852
+ if (!handle ||
853
+ !tool ||
854
+ info?.kind !== "relationship" ||
855
+ !Number.isInteger(index) ||
856
+ index < 0) {
857
+ return undefined;
858
+ }
859
+ return { relationshipId: info.id, index };
860
+ }
861
+ applySelectionAppearance() {
862
+ for (const [id, cell] of this.classCells) {
863
+ cell.attr("body/stroke", this.selection?.kind === "class" && this.selection.id === id ? SELECTED_COLOR : LINE_COLOR, INTERNAL);
864
+ cell.attr("body/strokeWidth", this.selection?.kind === "class" && this.selection.id === id ? 2.5 : 1.5, INTERNAL);
865
+ }
866
+ for (const [id, cell] of this.noteCells) {
867
+ cell.attr("body/stroke", this.selection?.kind === "note" && this.selection.id === id ? SELECTED_COLOR : "#b45309", INTERNAL);
868
+ cell.attr("body/strokeWidth", this.selection?.kind === "note" && this.selection.id === id ? 2.5 : 1.25, INTERNAL);
869
+ }
870
+ for (const [id, link] of this.relationshipCells) {
871
+ const selected = (this.selection?.kind === "relationship" && this.selection.id === id) ||
872
+ (this.selection?.kind === "relationship-waypoint" &&
873
+ this.selection.relationshipId === id);
874
+ link.attr("line/stroke", selected ? SELECTED_COLOR : LINE_COLOR, INTERNAL);
875
+ }
876
+ }
877
+ linkEndpointCenter(link, end) {
878
+ const endpoint = end === "source" ? link.getSourceCell() : link.getTargetCell();
879
+ return endpoint instanceof joint.dia.Element
880
+ ? positionFromElement(endpoint)
881
+ : { x: 0, y: 0 };
882
+ }
883
+ applyTransform() {
884
+ this.paper.scale(this.scaleValue, this.scaleValue, INTERNAL);
885
+ this.paper.translate(this.translation.x, this.translation.y, INTERNAL);
886
+ }
887
+ resize() {
888
+ if (this.destroyed)
889
+ return;
890
+ this.paper.setDimensions(Math.max(this.container.clientWidth, 1), Math.max(this.container.clientHeight, 1));
891
+ }
892
+ removeMissing(cells, wanted) {
893
+ for (const [id, cell] of cells) {
894
+ if (wanted.has(id))
895
+ continue;
896
+ cell.remove(INTERNAL);
897
+ cells.delete(id);
898
+ }
899
+ }
900
+ assertAlive() {
901
+ if (this.destroyed)
902
+ throw new Error("Diagram has been destroyed");
903
+ }
904
+ }
905
+ //# sourceMappingURL=adapter.js.map