orcasvn-react-diagrams 0.2.0 → 0.2.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 (133) hide show
  1. package/README.md +22 -1
  2. package/ai/api-contract.json +57 -5
  3. package/ai/invariants.json +5 -3
  4. package/ai/manifest.json +1 -1
  5. package/dist/cjs/examples.js +11775 -0
  6. package/dist/cjs/index.js +3889 -1112
  7. package/dist/cjs/types/api/createDiagramEditor.d.ts +7 -2
  8. package/dist/cjs/types/api/types.d.ts +178 -0
  9. package/dist/cjs/types/displaybox/demos/DeletionEventsDemoTab.d.ts +3 -0
  10. package/dist/cjs/types/displaybox/demos/ShapeHoverControlsDemoTab.d.ts +3 -0
  11. package/dist/cjs/types/displaybox/demos/TextLayoutDemoTab.d.ts +3 -0
  12. package/dist/cjs/types/displaybox/demos/deletionEventsDemo.d.ts +2 -0
  13. package/dist/cjs/types/displaybox/demos/rotatedCreationDemo.d.ts +2 -0
  14. package/dist/cjs/types/displaybox/demos/roundedRectRadiusDemo.d.ts +2 -0
  15. package/dist/cjs/types/displaybox/demos/shapeBorderMovementDemo.d.ts +2 -0
  16. package/dist/cjs/types/displaybox/demos/shapeHoverControlsDemo.d.ts +10 -0
  17. package/dist/cjs/types/displaybox/demos/textDemo.d.ts +4 -0
  18. package/dist/cjs/types/displaybox/useDemoEditor.d.ts +5 -2
  19. package/dist/cjs/types/engine/AutoLayoutService.d.ts +24 -0
  20. package/dist/cjs/types/engine/DiagramEngine.d.ts +32 -14
  21. package/dist/cjs/types/engine/EngineCommands.d.ts +4 -1
  22. package/dist/cjs/types/engine/LinkRoutingService.d.ts +35 -0
  23. package/dist/cjs/types/engine/MutationPipeline.d.ts +23 -0
  24. package/dist/cjs/types/engine/TextLayoutService.d.ts +40 -0
  25. package/dist/cjs/types/examples/index.d.ts +2 -0
  26. package/dist/cjs/types/measure/textStyleDefaults.d.ts +9 -0
  27. package/dist/cjs/types/models/DiagramModel.d.ts +1 -0
  28. package/dist/cjs/types/models/ElementModel.d.ts +1 -0
  29. package/dist/cjs/types/models/PortModel.d.ts +3 -0
  30. package/dist/cjs/types/models/TextModel.d.ts +8 -0
  31. package/dist/cjs/types/renderer/RenderTypes.d.ts +34 -1
  32. package/dist/cjs/types/renderer/konva/KonvaHitTester.d.ts +1 -1
  33. package/dist/cjs/types/renderer/konva/KonvaInteraction.d.ts +53 -3
  34. package/dist/cjs/types/renderer/konva/KonvaNodeFactory.d.ts +18 -1
  35. package/dist/cjs/types/renderer/konva/KonvaRenderer.d.ts +49 -2
  36. package/dist/cjs/types/shapes/BuiltInShapes.d.ts +107 -0
  37. package/dist/cjs/types/shapes/__tests__/BuiltInShapes.test.d.ts +1 -0
  38. package/dist/cjs/types/shapes/index.d.ts +1 -0
  39. package/dist/cjs/types/utils/__tests__/borderGeometry.test.d.ts +1 -0
  40. package/dist/cjs/types/utils/borderGeometry.d.ts +6 -0
  41. package/dist/cjs/types/utils/geometry.d.ts +22 -0
  42. package/dist/esm/examples.js +11767 -0
  43. package/dist/esm/examples.js.map +1 -0
  44. package/dist/esm/index.js +3890 -1113
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/types/api/createDiagramEditor.d.ts +7 -2
  47. package/dist/esm/types/api/types.d.ts +178 -0
  48. package/dist/esm/types/displaybox/demos/DeletionEventsDemoTab.d.ts +3 -0
  49. package/dist/esm/types/displaybox/demos/ShapeHoverControlsDemoTab.d.ts +3 -0
  50. package/dist/esm/types/displaybox/demos/TextLayoutDemoTab.d.ts +3 -0
  51. package/dist/esm/types/displaybox/demos/deletionEventsDemo.d.ts +2 -0
  52. package/dist/esm/types/displaybox/demos/rotatedCreationDemo.d.ts +2 -0
  53. package/dist/esm/types/displaybox/demos/roundedRectRadiusDemo.d.ts +2 -0
  54. package/dist/esm/types/displaybox/demos/shapeBorderMovementDemo.d.ts +2 -0
  55. package/dist/esm/types/displaybox/demos/shapeHoverControlsDemo.d.ts +10 -0
  56. package/dist/esm/types/displaybox/demos/textDemo.d.ts +4 -0
  57. package/dist/esm/types/displaybox/useDemoEditor.d.ts +5 -2
  58. package/dist/esm/types/engine/AutoLayoutService.d.ts +24 -0
  59. package/dist/esm/types/engine/DiagramEngine.d.ts +32 -14
  60. package/dist/esm/types/engine/EngineCommands.d.ts +4 -1
  61. package/dist/esm/types/engine/LinkRoutingService.d.ts +35 -0
  62. package/dist/esm/types/engine/MutationPipeline.d.ts +23 -0
  63. package/dist/esm/types/engine/TextLayoutService.d.ts +40 -0
  64. package/dist/esm/types/examples/index.d.ts +2 -0
  65. package/dist/esm/types/measure/textStyleDefaults.d.ts +9 -0
  66. package/dist/esm/types/models/DiagramModel.d.ts +1 -0
  67. package/dist/esm/types/models/ElementModel.d.ts +1 -0
  68. package/dist/esm/types/models/PortModel.d.ts +3 -0
  69. package/dist/esm/types/models/TextModel.d.ts +8 -0
  70. package/dist/esm/types/renderer/RenderTypes.d.ts +34 -1
  71. package/dist/esm/types/renderer/konva/KonvaHitTester.d.ts +1 -1
  72. package/dist/esm/types/renderer/konva/KonvaInteraction.d.ts +53 -3
  73. package/dist/esm/types/renderer/konva/KonvaNodeFactory.d.ts +18 -1
  74. package/dist/esm/types/renderer/konva/KonvaRenderer.d.ts +49 -2
  75. package/dist/esm/types/shapes/BuiltInShapes.d.ts +107 -0
  76. package/dist/esm/types/shapes/__tests__/BuiltInShapes.test.d.ts +1 -0
  77. package/dist/esm/types/shapes/index.d.ts +1 -0
  78. package/dist/esm/types/utils/__tests__/borderGeometry.test.d.ts +1 -0
  79. package/dist/esm/types/utils/borderGeometry.d.ts +6 -0
  80. package/dist/esm/types/utils/geometry.d.ts +22 -0
  81. package/dist/examples.d.ts +532 -0
  82. package/dist/index.d.ts +233 -2
  83. package/docs/API_CONTRACT.md +59 -3
  84. package/docs/ARCHITECTURE.md +1 -0
  85. package/docs/CAPABILITIES.md +3 -1
  86. package/docs/COMMANDS_EVENTS.md +5 -0
  87. package/docs/DOCUMENTATION_WORKFLOW.md +6 -8
  88. package/docs/INTEGRATION_PLAYBOOK.md +2 -0
  89. package/docs/PORTING_CHECKLIST.md +1 -0
  90. package/docs/STATE_INVARIANTS.md +4 -0
  91. package/package.json +20 -10
  92. package/src/displaybox/demos/AutoLayoutDemoTab.tsx +501 -0
  93. package/src/displaybox/demos/DeletionEventsDemoTab.tsx +147 -0
  94. package/src/displaybox/demos/EngineEventsDemoTab.tsx +151 -0
  95. package/src/displaybox/demos/EventHandlersDemoTab.tsx +110 -0
  96. package/src/displaybox/demos/ExternalDragDropDemoTab.tsx +261 -0
  97. package/src/displaybox/demos/LinkCancelDemoTab.tsx +238 -0
  98. package/src/displaybox/demos/ObstacleRoutingDemoTab.tsx +30 -0
  99. package/src/displaybox/demos/ShapeHoverControlsDemoTab.tsx +558 -0
  100. package/src/displaybox/demos/SimpleDemo.tsx +73 -0
  101. package/src/displaybox/demos/SvgPathDemoTab.tsx +327 -0
  102. package/src/displaybox/demos/TextLayoutDemoTab.tsx +386 -0
  103. package/src/displaybox/demos/autoLayoutDemo.ts +111 -0
  104. package/src/displaybox/demos/basicDemo.ts +131 -0
  105. package/src/displaybox/demos/childConstraintsDemo.ts +65 -0
  106. package/src/displaybox/demos/customDemo.ts +59 -0
  107. package/src/displaybox/demos/deletionEventsDemo.ts +91 -0
  108. package/src/displaybox/demos/engineEventsDemo.ts +64 -0
  109. package/src/displaybox/demos/eventHandlersDemo.ts +41 -0
  110. package/src/displaybox/demos/externalDragDropDemo.ts +28 -0
  111. package/src/displaybox/demos/gridOverlayDemo.ts +50 -0
  112. package/src/displaybox/demos/index.tsx +217 -0
  113. package/src/displaybox/demos/linkBendHandlesDemo.ts +143 -0
  114. package/src/displaybox/demos/linkCancelDemo.ts +56 -0
  115. package/src/displaybox/demos/linkPortCreationDemo.ts +46 -0
  116. package/src/displaybox/demos/multiLevelTreeDemo.ts +120 -0
  117. package/src/displaybox/demos/multipleElementsDemo.ts +62 -0
  118. package/src/displaybox/demos/nestedDemo.ts +78 -0
  119. package/src/displaybox/demos/obstacleRoutingDemo.ts +176 -0
  120. package/src/displaybox/demos/portBorderDemo.ts +98 -0
  121. package/src/displaybox/demos/portConstraintsDemo.ts +175 -0
  122. package/src/displaybox/demos/rotatedCreationDemo.ts +185 -0
  123. package/src/displaybox/demos/roundedRectRadiusDemo.ts +93 -0
  124. package/src/displaybox/demos/routingDemo.ts +57 -0
  125. package/src/displaybox/demos/selectionDemo.ts +49 -0
  126. package/src/displaybox/demos/shapeBorderMovementDemo.ts +126 -0
  127. package/src/displaybox/demos/shapeGalleryDemo.ts +73 -0
  128. package/src/displaybox/demos/shapeHoverControlsDemo.ts +172 -0
  129. package/src/displaybox/demos/shared.ts +161 -0
  130. package/src/displaybox/demos/svgPathDemo.ts +71 -0
  131. package/src/displaybox/demos/textDemo.ts +62 -0
  132. package/src/displaybox/types.ts +66 -0
  133. package/src/examples/index.ts +21 -0
@@ -0,0 +1,558 @@
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
2
+ import { EllipseMidPoint } from '../../api';
3
+ import type {
4
+ DiagramEditorHandle,
5
+ DiagramState,
6
+ ElementShapeHoverControlInteractionEvent,
7
+ ElementShapeHoverControls,
8
+ Point,
9
+ PortData,
10
+ ShapeControlVisibilityTrigger,
11
+ } from '../../api';
12
+ import DisplayBoxControls from '../DisplayBoxControls';
13
+ import DisplayBoxStage from '../DisplayBoxStage';
14
+ import useDemoControls from '../useDemoControls';
15
+ import useDemoEditor from '../useDemoEditor';
16
+ import useOffsetSequence from '../useOffsetSequence';
17
+ import type { DemoActionHelpers } from '../types';
18
+ import { createId } from '../../utils/ids';
19
+ import { shapeHoverControlsDemoConfig, shapeHoverDemoIds } from './shapeHoverControlsDemo';
20
+
21
+ const EDGE_ICON_PATH = 'M8 1 A7 7 0 1 1 7.999 1 M8 4 L8 12 M4 8 L12 8';
22
+ const MIDPOINT_ICON_PATH = 'M2 8 H14 M8 2 V14 M4 4 L12 12 M12 4 L4 12';
23
+ const VERTEX_ICON_PATH = 'M2 8 H14 M8 2 V14';
24
+ const ELLIPSE_MIDPOINT_ICON_PATH = 'M3 8 H13 M8 3 V13 M3 8 A5 5 0 1 1 13 8 A5 5 0 1 1 3 8';
25
+ const ellipseMidPointOrder: EllipseMidPoint[] = [
26
+ EllipseMidPoint.top,
27
+ EllipseMidPoint.right,
28
+ EllipseMidPoint.bottom,
29
+ EllipseMidPoint.left,
30
+ ];
31
+
32
+ type TriggerMode = 'target-hover' | 'element-hover' | 'both';
33
+ type IndexedHoverControlState = {
34
+ enabled: boolean;
35
+ triggerMode: TriggerMode;
36
+ indicesInput: string;
37
+ allowAllTargets: boolean;
38
+ };
39
+ type EllipseMidPointControlState = {
40
+ enabled: boolean;
41
+ triggerMode: TriggerMode;
42
+ allowAllTargets: boolean;
43
+ selectedMidPoints: Record<EllipseMidPoint, boolean>;
44
+ };
45
+
46
+ const toVisibilityTriggers = (mode: TriggerMode): ShapeControlVisibilityTrigger[] =>
47
+ mode === 'both' ? ['target-hover', 'element-hover'] : [mode];
48
+
49
+ const parseIndices = (input: string): number[] | undefined => {
50
+ const values = input
51
+ .split(',')
52
+ .map((entry) => Number(entry.trim()))
53
+ .filter((entry) => Number.isInteger(entry) && entry >= 0);
54
+ const unique = Array.from(new Set(values));
55
+ return unique.length ? unique : undefined;
56
+ };
57
+
58
+ const resolveSelectedEllipseMidPoints = (
59
+ state: EllipseMidPointControlState,
60
+ ): EllipseMidPoint[] =>
61
+ ellipseMidPointOrder.filter((value) => state.selectedMidPoints[value]);
62
+
63
+ const toEllipseMidPointSelector = (
64
+ selectedMidPoints: EllipseMidPoint[],
65
+ ): EllipseMidPoint | EllipseMidPoint[] | undefined => {
66
+ if (!selectedMidPoints.length) return undefined;
67
+ if (selectedMidPoints.length === 1) return selectedMidPoints[0];
68
+ return selectedMidPoints;
69
+ };
70
+
71
+ const buildShapeHoverControls = (
72
+ edgeState: IndexedHoverControlState,
73
+ midpointState: IndexedHoverControlState,
74
+ vertexState: IndexedHoverControlState,
75
+ ellipseMidPointState: EllipseMidPointControlState,
76
+ ): ElementShapeHoverControls | undefined => {
77
+ const controls: NonNullable<ElementShapeHoverControls['controls']> = [];
78
+ if (edgeState.enabled) {
79
+ controls.push({
80
+ id: 'drawer-edge-add-child',
81
+ targetKind: 'edge' as const,
82
+ targetIndices: parseIndices(edgeState.indicesInput),
83
+ allowAllTargets: edgeState.allowAllTargets,
84
+ visibilityTriggers: toVisibilityTriggers(edgeState.triggerMode),
85
+ tolerance: 12,
86
+ lineStyle: { stroke: '#ff7a00', strokeWidth: 3 },
87
+ icon: {
88
+ svgPath: EDGE_ICON_PATH,
89
+ size: { width: 16, height: 16 },
90
+ style: { fill: '#ffffff', stroke: '#ff7a00', strokeWidth: 2 },
91
+ },
92
+ });
93
+ }
94
+ if (midpointState.enabled) {
95
+ controls.push({
96
+ id: 'midpoint-drag-log',
97
+ targetKind: 'midpoint' as const,
98
+ targetIndices: parseIndices(midpointState.indicesInput),
99
+ allowAllTargets: midpointState.allowAllTargets,
100
+ visibilityTriggers: toVisibilityTriggers(midpointState.triggerMode),
101
+ tolerance: 12,
102
+ icon: {
103
+ svgPath: MIDPOINT_ICON_PATH,
104
+ size: { width: 16, height: 16 },
105
+ style: { fill: '#ffffff', stroke: '#2b6f99', strokeWidth: 1.8 },
106
+ },
107
+ });
108
+ }
109
+ if (vertexState.enabled) {
110
+ controls.push({
111
+ id: 'diamond-vertex-link',
112
+ targetKind: 'vertex' as const,
113
+ targetIndices: parseIndices(vertexState.indicesInput),
114
+ allowAllTargets: vertexState.allowAllTargets,
115
+ visibilityTriggers: toVisibilityTriggers(vertexState.triggerMode),
116
+ tolerance: 14,
117
+ icon: {
118
+ svgPath: VERTEX_ICON_PATH,
119
+ size: { width: 16, height: 16 },
120
+ style: { fill: 'none', stroke: '#6b3fa0', strokeWidth: 2, lineCap: 'round' },
121
+ },
122
+ });
123
+ }
124
+ if (ellipseMidPointState.enabled) {
125
+ controls.push({
126
+ id: 'ellipse-midpoint-log',
127
+ targetKind: 'ellipse-midpoint' as const,
128
+ ellipseMidPoints: toEllipseMidPointSelector(resolveSelectedEllipseMidPoints(ellipseMidPointState)),
129
+ allowAllTargets: ellipseMidPointState.allowAllTargets,
130
+ visibilityTriggers: toVisibilityTriggers(ellipseMidPointState.triggerMode),
131
+ tolerance: 14,
132
+ icon: {
133
+ svgPath: ELLIPSE_MIDPOINT_ICON_PATH,
134
+ size: { width: 16, height: 16 },
135
+ style: { fill: '#ffffff', stroke: '#0f766e', strokeWidth: 1.8 },
136
+ },
137
+ });
138
+ }
139
+ return controls.length ? { controls } : undefined;
140
+ };
141
+
142
+ const resolveElementWorldRect = (state: DiagramState, elementId: string) => {
143
+ const element = state.elements.find((item) => item.id === elementId);
144
+ if (!element) return null;
145
+ return {
146
+ element,
147
+ rect: {
148
+ x: element.position.x,
149
+ y: element.position.y,
150
+ width: element.size.width,
151
+ height: element.size.height,
152
+ },
153
+ };
154
+ };
155
+
156
+ const ensureBorderPort = (
157
+ editor: DiagramEditorHandle,
158
+ state: DiagramState,
159
+ elementId: string,
160
+ worldPoint: Point,
161
+ ): string => {
162
+ const existing = state.ports.find((port) => port.elementId === elementId);
163
+ if (existing) {
164
+ return existing.id;
165
+ }
166
+ const portId = `hover-port-${createId()}`;
167
+ const port: PortData = {
168
+ id: portId,
169
+ elementId,
170
+ position: { x: 0, y: 0 },
171
+ shapeId: 'port-dark',
172
+ moveMode: 'border',
173
+ anchorCenter: true,
174
+ orientToHostBorder: true,
175
+ };
176
+ editor.addPortToElement(elementId, port);
177
+ editor.movePortTo(portId, worldPoint.x, worldPoint.y);
178
+ return portId;
179
+ };
180
+
181
+ const ShapeHoverControlsDemo = () => {
182
+ const demo = shapeHoverControlsDemoConfig;
183
+ const [edgeState, setEdgeState] = useState<IndexedHoverControlState>({
184
+ enabled: true,
185
+ triggerMode: 'target-hover',
186
+ indicesInput: '2',
187
+ allowAllTargets: false,
188
+ });
189
+ const [midpointState, setMidpointState] = useState<IndexedHoverControlState>({
190
+ enabled: true,
191
+ triggerMode: 'target-hover',
192
+ indicesInput: '2',
193
+ allowAllTargets: false,
194
+ });
195
+ const [vertexState, setVertexState] = useState<IndexedHoverControlState>({
196
+ enabled: true,
197
+ triggerMode: 'target-hover',
198
+ indicesInput: '1',
199
+ allowAllTargets: false,
200
+ });
201
+ const [ellipseMidPointState, setEllipseMidPointState] = useState<EllipseMidPointControlState>({
202
+ enabled: true,
203
+ triggerMode: 'target-hover',
204
+ allowAllTargets: false,
205
+ selectedMidPoints: {
206
+ [EllipseMidPoint.top]: true,
207
+ [EllipseMidPoint.right]: false,
208
+ [EllipseMidPoint.bottom]: true,
209
+ [EllipseMidPoint.left]: false,
210
+ },
211
+ });
212
+ const [eventLog, setEventLog] = useState<string[]>([]);
213
+ const [eventCounts, setEventCounts] = useState({ click: 0, dragStart: 0, dragMove: 0, dragEnd: 0 });
214
+ const editorHandleRef = useRef<DiagramEditorHandle | null>(null);
215
+ const nextOffset = useOffsetSequence();
216
+ const actionHelpers: DemoActionHelpers = useMemo(() => ({ nextOffset }), [nextOffset]);
217
+ const initialHoverControls = useMemo(
218
+ () =>
219
+ buildShapeHoverControls(
220
+ { enabled: true, triggerMode: 'target-hover', indicesInput: '2', allowAllTargets: false },
221
+ { enabled: true, triggerMode: 'target-hover', indicesInput: '2', allowAllTargets: false },
222
+ { enabled: true, triggerMode: 'target-hover', indicesInput: '1', allowAllTargets: false },
223
+ {
224
+ enabled: true,
225
+ triggerMode: 'target-hover',
226
+ allowAllTargets: false,
227
+ selectedMidPoints: {
228
+ [EllipseMidPoint.top]: true,
229
+ [EllipseMidPoint.right]: false,
230
+ [EllipseMidPoint.bottom]: true,
231
+ [EllipseMidPoint.left]: false,
232
+ },
233
+ },
234
+ ),
235
+ [],
236
+ );
237
+
238
+ const onControlInteraction = useCallback((event: ElementShapeHoverControlInteractionEvent) => {
239
+ const editor = editorHandleRef.current;
240
+ if (!editor) return;
241
+ const currentState = editor.getState();
242
+ const dragDelta = event.drag ? `${event.drag.delta.x.toFixed(1)},${event.drag.delta.y.toFixed(1)}` : '-';
243
+ const ellipseMidPoint = event.ellipseMidPoint ? ` | ellipseMidPoint=${event.ellipseMidPoint}` : '';
244
+ const entry = `${event.eventType} | ${event.controlId} | ${event.targetKind}[${event.targetIndex}]${ellipseMidPoint} | delta=${dragDelta}`;
245
+ setEventLog((prev) => [entry, ...prev].slice(0, 10));
246
+ setEventCounts((prev) => ({
247
+ click: prev.click + (event.eventType === 'click' ? 1 : 0),
248
+ dragStart: prev.dragStart + (event.eventType === 'drag-start' ? 1 : 0),
249
+ dragMove: prev.dragMove + (event.eventType === 'drag-move' ? 1 : 0),
250
+ dragEnd: prev.dragEnd + (event.eventType === 'drag-end' ? 1 : 0),
251
+ }));
252
+
253
+ if (event.eventType !== 'click') return;
254
+ if (event.controlId === 'drawer-edge-add-child' && event.elementId === shapeHoverDemoIds.drawer && event.edge) {
255
+ const drawer = currentState.elements.find((element) => element.id === shapeHoverDemoIds.drawer);
256
+ if (!drawer) return;
257
+ const childCount = currentState.elements.filter((element) => element.parentId === shapeHoverDemoIds.drawer).length;
258
+ const id = `hover-child-${createId()}`;
259
+ const col = childCount % 3;
260
+ const row = Math.floor(childCount / 3);
261
+ editor.addElement({
262
+ id,
263
+ parentId: shapeHoverDemoIds.drawer,
264
+ position: { x: 24 + col * 116, y: 150 + row * 54 },
265
+ size: { width: 100, height: 44 },
266
+ shapeId: 'default',
267
+ });
268
+ editor.addText({
269
+ id: `hover-child-label-${createId()}`,
270
+ ownerId: id,
271
+ content: `Child ${childCount + 1}`,
272
+ position: { x: 8, y: 12 },
273
+ style: { fontSize: 12, fontFamily: 'sans-serif' },
274
+ });
275
+ editor.setSelection([id]);
276
+ return;
277
+ }
278
+ if (event.controlId !== 'diamond-vertex-link' || event.elementId !== shapeHoverDemoIds.diamond) {
279
+ return;
280
+ }
281
+ const sourcePoint = event.vertex?.position;
282
+ if (!sourcePoint) return;
283
+ const targetInfo = resolveElementWorldRect(currentState, shapeHoverDemoIds.target);
284
+ if (!targetInfo) return;
285
+ const targetPoint = {
286
+ x: targetInfo.rect.x,
287
+ y: targetInfo.rect.y + targetInfo.rect.height / 2,
288
+ };
289
+ const sourcePortId = ensureBorderPort(editor, currentState, shapeHoverDemoIds.diamond, sourcePoint);
290
+ const stateAfterSource = editor.getState();
291
+ const targetPortId = ensureBorderPort(editor, stateAfterSource, shapeHoverDemoIds.target, targetPoint);
292
+ const latestState = editor.getState();
293
+ const alreadyLinked = latestState.links.some(
294
+ (link) => link.sourcePortId === sourcePortId && link.targetPortId === targetPortId,
295
+ );
296
+ if (alreadyLinked) return;
297
+ const linkId = `hover-link-${createId()}`;
298
+ editor.addLink({
299
+ id: linkId,
300
+ sourcePortId,
301
+ targetPortId,
302
+ points: [],
303
+ });
304
+ editor.setSelection([linkId]);
305
+ }, []);
306
+
307
+ const { containerRef, editorRef, diagramState, selection, snapEnabled, setSnapEnabled } = useDemoEditor({
308
+ createState: demo.createState,
309
+ elementShapes: demo.elementShapes,
310
+ portShapes: demo.portShapes,
311
+ elementShapeHoverControls: initialHoverControls,
312
+ onElementShapeHoverControlInteraction: onControlInteraction,
313
+ });
314
+ editorHandleRef.current = editorRef.current;
315
+
316
+ const controls = useDemoControls({
317
+ demo,
318
+ editorRef,
319
+ diagramState,
320
+ selection,
321
+ snapEnabled,
322
+ setSnapEnabled,
323
+ actionHelpers,
324
+ });
325
+
326
+ const runtimeHoverControls = useMemo(
327
+ () => buildShapeHoverControls(edgeState, midpointState, vertexState, ellipseMidPointState),
328
+ [edgeState, midpointState, vertexState, ellipseMidPointState],
329
+ );
330
+
331
+ React.useEffect(() => {
332
+ editorRef.current?.setElementShapeHoverControls(runtimeHoverControls);
333
+ }, [editorRef, runtimeHoverControls]);
334
+
335
+ const drawerChildCount = useMemo(() => {
336
+ if (!diagramState) return 0;
337
+ return diagramState.elements.filter((element) => element.parentId === shapeHoverDemoIds.drawer).length;
338
+ }, [diagramState]);
339
+
340
+ return (
341
+ <section>
342
+ <div style={{ marginBottom: 12 }}>
343
+ <h2 style={{ marginTop: 0, marginBottom: 4 }}>{demo.title}</h2>
344
+ <p style={{ marginTop: 0 }}>{demo.description}</p>
345
+ </div>
346
+ <DisplayBoxControls
347
+ actions={demo.actions}
348
+ snapEnabled={controls.snapEnabled}
349
+ selectedLinkRouting={controls.selectedLinkRouting}
350
+ canToggleLinkRouting={controls.canToggleLinkRouting}
351
+ onReload={controls.handleReload}
352
+ onZoomIn={controls.handleZoomIn}
353
+ onZoomOut={controls.handleZoomOut}
354
+ onResetViewport={controls.handleResetViewport}
355
+ onToggleSnap={controls.handleToggleSnap}
356
+ onManualRender={controls.handleManualRender}
357
+ onToggleLinkRouting={controls.handleToggleLinkRouting}
358
+ onAction={controls.handleAction}
359
+ />
360
+ <div
361
+ style={{
362
+ marginBottom: 12,
363
+ padding: '10px 12px',
364
+ border: '1px solid #d9e1ec',
365
+ borderRadius: 8,
366
+ background: '#fbfdff',
367
+ }}
368
+ >
369
+ <div style={{ fontWeight: 600, marginBottom: 6 }}>How To Verify</div>
370
+ <div style={{ fontSize: 13, color: '#2d3a4d', marginBottom: 8 }}>
371
+ Switch each control between target-hover, element-hover, and both. Use index selectors (0-based) or enable
372
+ allow-all to control eligibility. Click drawer edge control to add children and click diamond vertex control to
373
+ create the link. Use ellipse-midpoint selectors to target top/right/bottom/left on the ellipse and circle hosts.
374
+ Drag any visible control icon to inspect drag-start/move/end payload behavior.
375
+ </div>
376
+ <div style={{ fontSize: 12, color: '#4d5a70', marginBottom: 8 }}>
377
+ Shape validity note: edge/midpoint index controls are available for rectangle/polygon hover geometry.
378
+ ellipse-midpoint controls are available for circle/ellipse only; the rectangle sample remains a safe no-op.
379
+ </div>
380
+ <div style={{ display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))' }}>
381
+ <HoverControlSection
382
+ title="Edge Control"
383
+ state={edgeState}
384
+ onChange={setEdgeState}
385
+ toggleLabel={edgeState.enabled ? 'Disable Edge Control' : 'Enable Edge Control'}
386
+ />
387
+ <HoverControlSection
388
+ title="Midpoint Control"
389
+ state={midpointState}
390
+ onChange={setMidpointState}
391
+ toggleLabel={midpointState.enabled ? 'Disable Midpoint Control' : 'Enable Midpoint Control'}
392
+ />
393
+ <HoverControlSection
394
+ title="Vertex Control"
395
+ state={vertexState}
396
+ onChange={setVertexState}
397
+ toggleLabel={vertexState.enabled ? 'Disable Vertex Control' : 'Enable Vertex Control'}
398
+ />
399
+ <EllipseMidPointControlSection
400
+ title="Ellipse Midpoint Control"
401
+ state={ellipseMidPointState}
402
+ onChange={setEllipseMidPointState}
403
+ toggleLabel={ellipseMidPointState.enabled ? 'Disable Ellipse Midpoint Control' : 'Enable Ellipse Midpoint Control'}
404
+ />
405
+ </div>
406
+ <div style={{ marginTop: 10, fontSize: 13 }}>
407
+ Drawer child count: {drawerChildCount} | Click: {eventCounts.click} | Drag start/move/end:{' '}
408
+ {eventCounts.dragStart}/{eventCounts.dragMove}/{eventCounts.dragEnd}
409
+ </div>
410
+ <div style={{ marginTop: 8, fontSize: 12, color: '#2d3a4d' }}>
411
+ <strong>Latest control events:</strong>
412
+ <pre
413
+ style={{
414
+ marginTop: 6,
415
+ padding: 8,
416
+ maxHeight: 140,
417
+ overflow: 'auto',
418
+ background: '#ffffff',
419
+ border: '1px solid #d9e1ec',
420
+ borderRadius: 6,
421
+ whiteSpace: 'pre-wrap',
422
+ }}
423
+ >
424
+ {eventLog.length ? eventLog.join('\n') : 'No events yet'}
425
+ </pre>
426
+ </div>
427
+ </div>
428
+ <DisplayBoxStage containerRef={containerRef} />
429
+ </section>
430
+ );
431
+ };
432
+
433
+ const HoverControlSection = ({
434
+ title,
435
+ state,
436
+ onChange,
437
+ toggleLabel,
438
+ }: {
439
+ title: string;
440
+ state: IndexedHoverControlState;
441
+ onChange: React.Dispatch<React.SetStateAction<IndexedHoverControlState>>;
442
+ toggleLabel: string;
443
+ }) => (
444
+ <div style={{ border: '1px solid #d9e1ec', borderRadius: 6, padding: 8, background: '#ffffff' }}>
445
+ <div style={{ fontWeight: 600, marginBottom: 6 }}>{title}</div>
446
+ <button
447
+ type="button"
448
+ onClick={() => onChange((prev) => ({ ...prev, enabled: !prev.enabled }))}
449
+ style={{ marginBottom: 6, padding: '6px 10px' }}
450
+ >
451
+ {toggleLabel}
452
+ </button>
453
+ <div style={{ display: 'grid', gap: 6 }}>
454
+ <label style={{ fontSize: 12 }}>
455
+ Trigger mode
456
+ <select
457
+ value={state.triggerMode}
458
+ onChange={(event) => onChange((prev) => ({ ...prev, triggerMode: event.target.value as TriggerMode }))}
459
+ style={{ marginLeft: 8 }}
460
+ >
461
+ <option value="target-hover">target-hover</option>
462
+ <option value="element-hover">element-hover</option>
463
+ <option value="both">both</option>
464
+ </select>
465
+ </label>
466
+ <label style={{ fontSize: 12 }}>
467
+ Target indices (CSV)
468
+ <input
469
+ value={state.indicesInput}
470
+ onChange={(event) => onChange((prev) => ({ ...prev, indicesInput: event.target.value }))}
471
+ placeholder="example: 0,2"
472
+ style={{ marginLeft: 8, width: 120 }}
473
+ />
474
+ </label>
475
+ <label style={{ fontSize: 12 }}>
476
+ <input
477
+ type="checkbox"
478
+ checked={state.allowAllTargets}
479
+ onChange={(event) => onChange((prev) => ({ ...prev, allowAllTargets: event.target.checked }))}
480
+ style={{ marginRight: 6 }}
481
+ />
482
+ allowAllTargets
483
+ </label>
484
+ </div>
485
+ </div>
486
+ );
487
+
488
+ const EllipseMidPointControlSection = ({
489
+ title,
490
+ state,
491
+ onChange,
492
+ toggleLabel,
493
+ }: {
494
+ title: string;
495
+ state: EllipseMidPointControlState;
496
+ onChange: React.Dispatch<React.SetStateAction<EllipseMidPointControlState>>;
497
+ toggleLabel: string;
498
+ }) => (
499
+ <div style={{ border: '1px solid #d9e1ec', borderRadius: 6, padding: 8, background: '#ffffff' }}>
500
+ <div style={{ fontWeight: 600, marginBottom: 6 }}>{title}</div>
501
+ <button
502
+ type="button"
503
+ onClick={() => onChange((prev) => ({ ...prev, enabled: !prev.enabled }))}
504
+ style={{ marginBottom: 6, padding: '6px 10px' }}
505
+ >
506
+ {toggleLabel}
507
+ </button>
508
+ <div style={{ display: 'grid', gap: 6 }}>
509
+ <label style={{ fontSize: 12 }}>
510
+ Trigger mode
511
+ <select
512
+ value={state.triggerMode}
513
+ onChange={(event) => onChange((prev) => ({ ...prev, triggerMode: event.target.value as TriggerMode }))}
514
+ style={{ marginLeft: 8 }}
515
+ >
516
+ <option value="target-hover">target-hover</option>
517
+ <option value="element-hover">element-hover</option>
518
+ <option value="both">both</option>
519
+ </select>
520
+ </label>
521
+ <label style={{ fontSize: 12 }}>
522
+ <input
523
+ type="checkbox"
524
+ checked={state.allowAllTargets}
525
+ onChange={(event) => onChange((prev) => ({ ...prev, allowAllTargets: event.target.checked }))}
526
+ style={{ marginRight: 6 }}
527
+ />
528
+ allowAllTargets
529
+ </label>
530
+ <div style={{ fontSize: 12 }}>
531
+ Enum targets
532
+ </div>
533
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 4 }}>
534
+ {ellipseMidPointOrder.map((midPoint) => (
535
+ <label key={midPoint} style={{ fontSize: 12 }}>
536
+ <input
537
+ type="checkbox"
538
+ checked={state.selectedMidPoints[midPoint]}
539
+ onChange={(event) =>
540
+ onChange((prev) => ({
541
+ ...prev,
542
+ selectedMidPoints: {
543
+ ...prev.selectedMidPoints,
544
+ [midPoint]: event.target.checked,
545
+ },
546
+ }))
547
+ }
548
+ style={{ marginRight: 6 }}
549
+ />
550
+ {midPoint}
551
+ </label>
552
+ ))}
553
+ </div>
554
+ </div>
555
+ </div>
556
+ );
557
+
558
+ export default ShapeHoverControlsDemo;
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import DisplayBoxControls from '../DisplayBoxControls';
3
+ import DisplayBoxStage from '../DisplayBoxStage';
4
+ import useDemoControls from '../useDemoControls';
5
+ import useDemoEditor from '../useDemoEditor';
6
+ import useOffsetSequence from '../useOffsetSequence';
7
+ import type { DemoConfig, DemoActionHelpers } from '../types';
8
+
9
+ type SimpleDemoProps = {
10
+ demo: DemoConfig;
11
+ beforeStage?: React.ReactNode;
12
+ stageHandlers?: {
13
+ onDragOver?: (event: React.DragEvent<HTMLDivElement>) => void;
14
+ onDragLeave?: () => void;
15
+ onDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
16
+ };
17
+ };
18
+
19
+ const SimpleDemo = ({ demo, beforeStage, stageHandlers }: SimpleDemoProps) => {
20
+ const { containerRef, editorRef, diagramState, selection, snapEnabled, setSnapEnabled } = useDemoEditor({
21
+ createState: demo.createState,
22
+ elementShapes: demo.elementShapes,
23
+ portShapes: demo.portShapes,
24
+ router: demo.router,
25
+ snapDefault: demo.snapDefault,
26
+ });
27
+
28
+ const nextOffset = useOffsetSequence();
29
+ const actionHelpers: DemoActionHelpers = React.useMemo(() => ({ nextOffset }), [nextOffset]);
30
+
31
+ const controls = useDemoControls({
32
+ demo,
33
+ editorRef,
34
+ diagramState,
35
+ selection,
36
+ snapEnabled,
37
+ setSnapEnabled,
38
+ actionHelpers,
39
+ });
40
+
41
+ return (
42
+ <section>
43
+ <div style={{ marginBottom: 12 }}>
44
+ <h2 style={{ marginTop: 0, marginBottom: 4 }}>{demo.title}</h2>
45
+ <p style={{ marginTop: 0 }}>{demo.description}</p>
46
+ </div>
47
+ <DisplayBoxControls
48
+ actions={demo.actions}
49
+ snapEnabled={controls.snapEnabled}
50
+ selectedLinkRouting={controls.selectedLinkRouting}
51
+ canToggleLinkRouting={controls.canToggleLinkRouting}
52
+ onReload={controls.handleReload}
53
+ onZoomIn={controls.handleZoomIn}
54
+ onZoomOut={controls.handleZoomOut}
55
+ onResetViewport={controls.handleResetViewport}
56
+ onToggleSnap={controls.handleToggleSnap}
57
+ onManualRender={controls.handleManualRender}
58
+ onToggleLinkRouting={controls.handleToggleLinkRouting}
59
+ onAction={controls.handleAction}
60
+ />
61
+ {beforeStage}
62
+ <DisplayBoxStage
63
+ containerRef={containerRef}
64
+ onDragOver={stageHandlers?.onDragOver}
65
+ onDragLeave={stageHandlers?.onDragLeave}
66
+ onDrop={stageHandlers?.onDrop}
67
+ stageStyle={demo.stageStyle}
68
+ />
69
+ </section>
70
+ );
71
+ };
72
+
73
+ export default SimpleDemo;