@bendyline/squisq-editor-react 1.6.1 → 1.6.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.
- package/dist/index.d.ts +87 -41
- package/dist/index.js +8263 -6705
- package/dist/index.js.map +1 -1
- package/dist/styles/index.css +841 -91
- package/package.json +4 -4
- package/src/BlockPropertiesPopover.tsx +23 -7
- package/src/EditorShell.tsx +65 -20
- package/src/MediaBin.tsx +171 -28
- package/src/PreviewControls.tsx +177 -44
- package/src/PreviewPanel.tsx +2 -0
- package/src/TemplateAnnotation.ts +22 -7
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +295 -128
- package/src/ThemeCustomizerPanel.tsx +22 -15
- package/src/Toolbar.tsx +527 -207
- package/src/TransitionPicker.tsx +8 -1
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +45 -3
- package/src/__tests__/buildPreviewDocTransition.test.ts +1 -2
- package/src/__tests__/editorShellProps.test.tsx +268 -1
- package/src/__tests__/headingTransition.test.ts +59 -9
- package/src/__tests__/imageEditorShell.test.tsx +23 -0
- package/src/__tests__/mediaReferences.test.ts +82 -0
- package/src/__tests__/previewControls.test.tsx +94 -1
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +23 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/tiptapBridge.test.ts +47 -0
- package/src/diagram/DiagramWidget.tsx +6 -4
- package/src/headingTransition.ts +96 -21
- package/src/index.ts +2 -1
- package/src/mediaReferences.ts +299 -0
- package/src/scene/Scene.tsx +53 -7
- package/src/scene/SceneBlockWidget.tsx +6 -3
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- package/src/scene/layers/DiagramEdges.tsx +4 -3
- package/src/scene/layers/edgeGeometry.ts +23 -4
- package/src/scene/scene.css +142 -2
- package/src/scene/tools/ConnectTool.ts +113 -24
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +2 -0
- package/src/styles/editor.css +862 -101
- package/src/templateContentPreviewResolver.ts +353 -0
- package/src/tiptapBridge.ts +60 -33
package/src/scene/scene.css
CHANGED
|
@@ -20,6 +20,28 @@
|
|
|
20
20
|
overflow: hidden;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-shell {
|
|
24
|
+
--squisq-surface: #0f172a;
|
|
25
|
+
--squisq-surface-hover: #1e293b;
|
|
26
|
+
--squisq-border: #475569;
|
|
27
|
+
--squisq-text: #f8fafc;
|
|
28
|
+
--squisq-text-muted: #94a3b8;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-root,
|
|
32
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-inline,
|
|
33
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-diagram-inline {
|
|
34
|
+
background: #0f172a;
|
|
35
|
+
border-color: #475569;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--shape,
|
|
39
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--path,
|
|
40
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-layer--text,
|
|
41
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-extras {
|
|
42
|
+
filter: invert(1) hue-rotate(180deg) saturate(0.9);
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
.squisq-scene-viewport {
|
|
24
46
|
display: block;
|
|
25
47
|
width: 100%;
|
|
@@ -111,6 +133,18 @@
|
|
|
111
133
|
pointer-events: none;
|
|
112
134
|
}
|
|
113
135
|
|
|
136
|
+
.squisq-scene-connection-point {
|
|
137
|
+
fill: #ffffff;
|
|
138
|
+
stroke: #6366f1;
|
|
139
|
+
stroke-width: 2;
|
|
140
|
+
vector-effect: non-scaling-stroke;
|
|
141
|
+
pointer-events: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.squisq-scene-connection-point--active {
|
|
145
|
+
fill: #6366f1;
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
/* ── Toolbar ──────────────────────────────────────────────── */
|
|
115
149
|
|
|
116
150
|
.squisq-scene-toolbar {
|
|
@@ -261,6 +295,46 @@
|
|
|
261
295
|
color: #b91c1c;
|
|
262
296
|
}
|
|
263
297
|
|
|
298
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-toolbar,
|
|
299
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-block-tools,
|
|
300
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-block-actions,
|
|
301
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-block-props {
|
|
302
|
+
background: #111827;
|
|
303
|
+
border-color: #475569;
|
|
304
|
+
color: #e5e7eb;
|
|
305
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.32);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool,
|
|
309
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action {
|
|
310
|
+
background: #111827;
|
|
311
|
+
color: #e5e7eb;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool:hover,
|
|
315
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action:hover:not(:disabled) {
|
|
316
|
+
background: #1f2937;
|
|
317
|
+
border-color: #64748b;
|
|
318
|
+
color: #f8fafc;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool--active,
|
|
322
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-tool--active:hover {
|
|
323
|
+
background: #312e81;
|
|
324
|
+
border-color: #818cf8;
|
|
325
|
+
color: #f8fafc;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action--danger {
|
|
329
|
+
color: #fca5a5;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-action--danger:hover:not(:disabled) {
|
|
333
|
+
background: #450a0a;
|
|
334
|
+
border-color: #f87171;
|
|
335
|
+
color: #fecaca;
|
|
336
|
+
}
|
|
337
|
+
|
|
264
338
|
/* Maximized overlay: a flex-filling canvas with the toolbar as a right-hand
|
|
265
339
|
column (full screen has room to reserve it). */
|
|
266
340
|
.squisq-scene-block-max {
|
|
@@ -303,8 +377,8 @@
|
|
|
303
377
|
overflow-y: auto;
|
|
304
378
|
}
|
|
305
379
|
|
|
306
|
-
/* Re-flow the (normally horizontal, right-aligned) toolbar into a
|
|
307
|
-
vertical column whose button groups wrap within the 180px
|
|
380
|
+
/* Re-flow the (normally horizontal, right-aligned) gutter toolbar into a
|
|
381
|
+
top-aligned vertical column whose button groups wrap within the 180px. */
|
|
308
382
|
.squisq-scene-side-toolbar .squisq-scene-block-toolbar {
|
|
309
383
|
flex-direction: column;
|
|
310
384
|
align-items: stretch;
|
|
@@ -326,6 +400,23 @@
|
|
|
326
400
|
right: calc(100% + 6px);
|
|
327
401
|
}
|
|
328
402
|
|
|
403
|
+
/* ── Narrow-width fallback bar ─────────────────────────────────
|
|
404
|
+
When SceneSideToolbar measures that the right gutter can't hold the 180px
|
|
405
|
+
column, it renders the same toolbar as an in-flow horizontal bar above the
|
|
406
|
+
canvas instead. Normal flow (not an overlay) so it always paints and is
|
|
407
|
+
never clipped; it just keeps the toolbar's natural horizontal layout and
|
|
408
|
+
wraps on very narrow widths. */
|
|
409
|
+
.squisq-scene-inline-toolbar {
|
|
410
|
+
display: flex;
|
|
411
|
+
margin-bottom: 8px;
|
|
412
|
+
}
|
|
413
|
+
.squisq-scene-inline-toolbar .squisq-scene-block-toolbar {
|
|
414
|
+
flex-wrap: wrap;
|
|
415
|
+
justify-content: flex-start;
|
|
416
|
+
width: 100%;
|
|
417
|
+
margin: 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
329
420
|
/* ── Maximize button ──────────────────────────────────────── */
|
|
330
421
|
|
|
331
422
|
.squisq-scene-maximize-btn {
|
|
@@ -354,6 +445,19 @@
|
|
|
354
445
|
color: #0f172a;
|
|
355
446
|
}
|
|
356
447
|
|
|
448
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-maximize-btn {
|
|
449
|
+
background: #111827;
|
|
450
|
+
border-color: #475569;
|
|
451
|
+
color: #e5e7eb;
|
|
452
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.32);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-maximize-btn:hover {
|
|
456
|
+
background: #1f2937;
|
|
457
|
+
border-color: #64748b;
|
|
458
|
+
color: #f8fafc;
|
|
459
|
+
}
|
|
460
|
+
|
|
357
461
|
/* ── Diagram node card (used by nodeCard.tsx) ─────────────── */
|
|
358
462
|
|
|
359
463
|
.squisq-scene-node-card {
|
|
@@ -502,6 +606,35 @@
|
|
|
502
606
|
font-weight: 700;
|
|
503
607
|
}
|
|
504
608
|
|
|
609
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-palette {
|
|
610
|
+
background: #111827;
|
|
611
|
+
color: #e5e7eb;
|
|
612
|
+
border-color: #475569;
|
|
613
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.42);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-palette-search,
|
|
617
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-select {
|
|
618
|
+
background: #0f172a;
|
|
619
|
+
color: #e5e7eb;
|
|
620
|
+
border-color: #475569;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-palette-item:hover,
|
|
624
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-clear:hover {
|
|
625
|
+
background: #1f2937;
|
|
626
|
+
border-color: #64748b;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-thumb,
|
|
630
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-thumb-line {
|
|
631
|
+
stroke: #e5e7eb;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-shape-thumb-text {
|
|
635
|
+
fill: #e5e7eb;
|
|
636
|
+
}
|
|
637
|
+
|
|
505
638
|
/* Per-selection style controls, inline in the contextual toolbar (Fill /
|
|
506
639
|
Stroke / Color swatches + connector dropdowns) — styled as a pill that
|
|
507
640
|
matches the tools/actions groups. */
|
|
@@ -566,6 +699,13 @@
|
|
|
566
699
|
background: #f1f5f9;
|
|
567
700
|
}
|
|
568
701
|
|
|
702
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-color,
|
|
703
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-scene-prop-clear {
|
|
704
|
+
background: #0f172a;
|
|
705
|
+
color: #e5e7eb;
|
|
706
|
+
border-color: #475569;
|
|
707
|
+
}
|
|
708
|
+
|
|
569
709
|
/* ── Inline widget host (mirrors squisq-diagram-inline) ──── */
|
|
570
710
|
|
|
571
711
|
.squisq-scene-widget-host {
|
|
@@ -13,14 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
import { createElement, type JSX } from 'react';
|
|
15
15
|
import type { SceneTool, SceneToolContext } from './SceneTool';
|
|
16
|
-
import { nodeIdFromCardLayerId
|
|
16
|
+
import { nodeIdFromCardLayerId } from '../layers/nodeCard';
|
|
17
|
+
import {
|
|
18
|
+
curvedPath,
|
|
19
|
+
edgeEndpoints,
|
|
20
|
+
snapPointToward,
|
|
21
|
+
type EdgeNodeBox,
|
|
22
|
+
} from '../layers/edgeGeometry';
|
|
17
23
|
|
|
18
24
|
interface ConnectState {
|
|
19
25
|
sourceNodeId: string;
|
|
20
|
-
/** Source node
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
26
|
+
/** Source node/card box in viewport units (cached at drag start). */
|
|
27
|
+
sourceBox: EdgeNodeBox;
|
|
28
|
+
/** Preview start/end in viewport units. */
|
|
29
|
+
start: { x: number; y: number };
|
|
30
|
+
end: { x: number; y: number };
|
|
24
31
|
/** Currently-hovered target node id, if any (for highlight + drop). */
|
|
25
32
|
hoveredTargetId: string | null;
|
|
26
33
|
}
|
|
@@ -32,13 +39,73 @@ function pointerFromEvent(e: React.PointerEvent): { sx: number; sy: number } {
|
|
|
32
39
|
return { sx: e.clientX - rect.left, sy: e.clientY - rect.top };
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
function
|
|
36
|
-
ctx: SceneToolContext,
|
|
37
|
-
nodeId: string,
|
|
38
|
-
): { x: number; y: number } | null {
|
|
42
|
+
function nodeBoxFromCard(ctx: SceneToolContext, nodeId: string): EdgeNodeBox | null {
|
|
39
43
|
const card = ctx.hitItems.find((it) => it.id === `node-card-${nodeId}`);
|
|
40
44
|
if (!card) return null;
|
|
41
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
id: nodeId,
|
|
47
|
+
x: card.bounds.x,
|
|
48
|
+
y: card.bounds.y,
|
|
49
|
+
width: card.bounds.width,
|
|
50
|
+
height: card.bounds.height,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nodeBoxes(ctx: SceneToolContext): EdgeNodeBox[] {
|
|
55
|
+
const boxes: EdgeNodeBox[] = [];
|
|
56
|
+
for (const it of ctx.hitItems) {
|
|
57
|
+
if (!it.id.startsWith('node-card-')) continue;
|
|
58
|
+
const nodeId = nodeIdFromCardLayerId(it.id);
|
|
59
|
+
if (!nodeId) continue;
|
|
60
|
+
boxes.push({
|
|
61
|
+
id: nodeId,
|
|
62
|
+
x: it.bounds.x,
|
|
63
|
+
y: it.bounds.y,
|
|
64
|
+
width: it.bounds.width,
|
|
65
|
+
height: it.bounds.height,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return boxes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function connectionPoints(box: EdgeNodeBox): Array<{ x: number; y: number }> {
|
|
72
|
+
const width = box.width ?? 0;
|
|
73
|
+
const height = box.height ?? 0;
|
|
74
|
+
const left = box.x;
|
|
75
|
+
const right = box.x + width;
|
|
76
|
+
const top = box.y;
|
|
77
|
+
const bottom = box.y + height;
|
|
78
|
+
const centerX = box.x + width / 2;
|
|
79
|
+
const centerY = box.y + height / 2;
|
|
80
|
+
return [
|
|
81
|
+
{ x: centerX, y: top },
|
|
82
|
+
{ x: right, y: centerY },
|
|
83
|
+
{ x: centerX, y: bottom },
|
|
84
|
+
{ x: left, y: centerY },
|
|
85
|
+
{ x: left, y: top },
|
|
86
|
+
{ x: right, y: top },
|
|
87
|
+
{ x: right, y: bottom },
|
|
88
|
+
{ x: left, y: bottom },
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function updatePreview(
|
|
93
|
+
sourceBox: EdgeNodeBox,
|
|
94
|
+
current: { x: number; y: number },
|
|
95
|
+
targetBox: EdgeNodeBox | null,
|
|
96
|
+
): { start: { x: number; y: number }; end: { x: number; y: number } } {
|
|
97
|
+
if (targetBox) {
|
|
98
|
+
return (
|
|
99
|
+
edgeEndpoints([sourceBox, targetBox], sourceBox.id, targetBox.id) ?? {
|
|
100
|
+
start: current,
|
|
101
|
+
end: current,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
start: snapPointToward([sourceBox], sourceBox.id, current) ?? current,
|
|
107
|
+
end: current,
|
|
108
|
+
};
|
|
42
109
|
}
|
|
43
110
|
|
|
44
111
|
export const ConnectTool: SceneTool = {
|
|
@@ -46,6 +113,7 @@ export const ConnectTool: SceneTool = {
|
|
|
46
113
|
label: 'Connect',
|
|
47
114
|
cursor: 'crosshair',
|
|
48
115
|
shortcut: 'c',
|
|
116
|
+
hideSelectionHandles: true,
|
|
49
117
|
|
|
50
118
|
onPointerDown(e, ctx) {
|
|
51
119
|
if (e.button !== 0) return;
|
|
@@ -55,12 +123,14 @@ export const ConnectTool: SceneTool = {
|
|
|
55
123
|
if (!hitId) return;
|
|
56
124
|
const nodeId = nodeIdFromCardLayerId(hitId);
|
|
57
125
|
if (!nodeId) return;
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
126
|
+
const sourceBox = nodeBoxFromCard(ctx, nodeId);
|
|
127
|
+
if (!sourceBox) return;
|
|
128
|
+
const preview = updatePreview(sourceBox, v, null);
|
|
60
129
|
state = {
|
|
61
130
|
sourceNodeId: nodeId,
|
|
62
|
-
|
|
63
|
-
|
|
131
|
+
sourceBox,
|
|
132
|
+
start: preview.start,
|
|
133
|
+
end: preview.end,
|
|
64
134
|
hoveredTargetId: null,
|
|
65
135
|
};
|
|
66
136
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
@@ -71,10 +141,14 @@ export const ConnectTool: SceneTool = {
|
|
|
71
141
|
if (!state) return;
|
|
72
142
|
const { sx, sy } = pointerFromEvent(e);
|
|
73
143
|
const v = ctx.screenToViewport(sx, sy);
|
|
74
|
-
state.current = v;
|
|
75
144
|
const hitId = ctx.hit(v);
|
|
76
145
|
const nodeId = hitId ? nodeIdFromCardLayerId(hitId) : null;
|
|
77
|
-
|
|
146
|
+
const targetId = nodeId && nodeId !== state.sourceNodeId ? nodeId : null;
|
|
147
|
+
const targetBox = targetId ? nodeBoxFromCard(ctx, targetId) : null;
|
|
148
|
+
const preview = updatePreview(state.sourceBox, v, targetBox);
|
|
149
|
+
state.start = preview.start;
|
|
150
|
+
state.end = preview.end;
|
|
151
|
+
state.hoveredTargetId = targetBox?.id ?? null;
|
|
78
152
|
},
|
|
79
153
|
|
|
80
154
|
onPointerUp(e, ctx) {
|
|
@@ -87,17 +161,32 @@ export const ConnectTool: SceneTool = {
|
|
|
87
161
|
state = null;
|
|
88
162
|
},
|
|
89
163
|
|
|
90
|
-
renderOverlay(): JSX.Element | null {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
164
|
+
renderOverlay(ctx): JSX.Element | null {
|
|
165
|
+
const points = nodeBoxes(ctx).flatMap((box) =>
|
|
166
|
+
connectionPoints(box).map((point, index) =>
|
|
167
|
+
createElement('circle', {
|
|
168
|
+
key: `${box.id}-${index}`,
|
|
169
|
+
cx: point.x,
|
|
170
|
+
cy: point.y,
|
|
171
|
+
r: 4,
|
|
172
|
+
className:
|
|
173
|
+
state?.hoveredTargetId === box.id
|
|
174
|
+
? 'squisq-scene-connection-point squisq-scene-connection-point--active'
|
|
175
|
+
: 'squisq-scene-connection-point',
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (!state) {
|
|
181
|
+
return points.length ? createElement('g', { key: 'connection-points' }, ...points) : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { start: a, end: b } = state;
|
|
185
|
+
const d = curvedPath(a, b);
|
|
98
186
|
return createElement(
|
|
99
187
|
'g',
|
|
100
188
|
{ key: 'connect-preview' },
|
|
189
|
+
...points,
|
|
101
190
|
createElement('path', { d, className: 'squisq-scene-connect-preview' }),
|
|
102
191
|
// Optional: a dot at the endpoint so the user sees the snap target.
|
|
103
192
|
createElement('circle', {
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
import { createElement, type JSX } from 'react';
|
|
17
17
|
import type { SceneTool, SceneToolContext } from './SceneTool';
|
|
18
18
|
import { shapeIdFromLayerId, isPrimaryShapeLayer } from '../layers/shapeLayers';
|
|
19
|
+
import {
|
|
20
|
+
edgeEndpoints,
|
|
21
|
+
snapPointToward,
|
|
22
|
+
straightPath,
|
|
23
|
+
type EdgeNodeBox,
|
|
24
|
+
} from '../layers/edgeGeometry';
|
|
19
25
|
|
|
20
26
|
/** A grab on the selected connector's endpoint handle. */
|
|
21
27
|
export interface EndpointHit {
|
|
@@ -37,8 +43,9 @@ type DragState =
|
|
|
37
43
|
| {
|
|
38
44
|
mode: 'new';
|
|
39
45
|
sourceId: string;
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
sourceBox: EdgeNodeBox;
|
|
47
|
+
start: { x: number; y: number };
|
|
48
|
+
end: { x: number; y: number };
|
|
42
49
|
hovered: string | null;
|
|
43
50
|
}
|
|
44
51
|
| {
|
|
@@ -46,8 +53,9 @@ type DragState =
|
|
|
46
53
|
connectorId: string;
|
|
47
54
|
end: 'from' | 'to';
|
|
48
55
|
fixedShapeId: string;
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
fixedBox: EdgeNodeBox;
|
|
57
|
+
start: { x: number; y: number };
|
|
58
|
+
endPoint: { x: number; y: number };
|
|
51
59
|
hovered: string | null;
|
|
52
60
|
};
|
|
53
61
|
|
|
@@ -59,10 +67,16 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
59
67
|
return { sx: e.clientX - rect.left, sy: e.clientY - rect.top };
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
function
|
|
70
|
+
function shapeBox(ctx: SceneToolContext, shapeId: string): EdgeNodeBox | null {
|
|
63
71
|
const it = ctx.hitItems.find((h) => h.id === `dshape-${shapeId}`);
|
|
64
72
|
if (!it) return null;
|
|
65
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
id: shapeId,
|
|
75
|
+
x: it.bounds.x,
|
|
76
|
+
y: it.bounds.y,
|
|
77
|
+
width: it.bounds.width,
|
|
78
|
+
height: it.bounds.height,
|
|
79
|
+
};
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
function shapeAt(ctx: SceneToolContext, v: { x: number; y: number }): string | null {
|
|
@@ -71,6 +85,45 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
71
85
|
return shapeIdFromLayerId(hitId);
|
|
72
86
|
}
|
|
73
87
|
|
|
88
|
+
function previewNew(
|
|
89
|
+
sourceBox: EdgeNodeBox,
|
|
90
|
+
current: { x: number; y: number },
|
|
91
|
+
targetBox: EdgeNodeBox | null,
|
|
92
|
+
): { start: { x: number; y: number }; end: { x: number; y: number } } {
|
|
93
|
+
if (targetBox) {
|
|
94
|
+
return (
|
|
95
|
+
edgeEndpoints([sourceBox, targetBox], sourceBox.id, targetBox.id) ?? {
|
|
96
|
+
start: current,
|
|
97
|
+
end: current,
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
start: snapPointToward([sourceBox], sourceBox.id, current) ?? current,
|
|
103
|
+
end: current,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function previewRetarget(
|
|
108
|
+
fixedBox: EdgeNodeBox,
|
|
109
|
+
movedEnd: 'from' | 'to',
|
|
110
|
+
current: { x: number; y: number },
|
|
111
|
+
targetBox: EdgeNodeBox | null,
|
|
112
|
+
): { start: { x: number; y: number }; end: { x: number; y: number } } {
|
|
113
|
+
if (targetBox) {
|
|
114
|
+
const source = movedEnd === 'from' ? targetBox : fixedBox;
|
|
115
|
+
const target = movedEnd === 'from' ? fixedBox : targetBox;
|
|
116
|
+
return (
|
|
117
|
+
edgeEndpoints([source, target], source.id, target.id) ?? {
|
|
118
|
+
start: current,
|
|
119
|
+
end: current,
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const fixed = snapPointToward([fixedBox], fixedBox.id, current) ?? current;
|
|
124
|
+
return movedEnd === 'from' ? { start: current, end: fixed } : { start: fixed, end: current };
|
|
125
|
+
}
|
|
126
|
+
|
|
74
127
|
return {
|
|
75
128
|
id: 'connect',
|
|
76
129
|
label: 'Connect',
|
|
@@ -85,14 +138,17 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
85
138
|
// Endpoint handle of the selected connector → re-target gesture.
|
|
86
139
|
const ep = options.endpointAt?.(v);
|
|
87
140
|
if (ep) {
|
|
88
|
-
const
|
|
141
|
+
const fixedBox = shapeBox(ctx, ep.fixedShapeId);
|
|
142
|
+
if (!fixedBox) return;
|
|
143
|
+
const preview = previewRetarget(fixedBox, ep.end, v, null);
|
|
89
144
|
drag = {
|
|
90
145
|
mode: 'retarget',
|
|
91
146
|
connectorId: ep.connectorId,
|
|
92
147
|
end: ep.end,
|
|
93
148
|
fixedShapeId: ep.fixedShapeId,
|
|
94
|
-
|
|
95
|
-
|
|
149
|
+
fixedBox,
|
|
150
|
+
start: preview.start,
|
|
151
|
+
endPoint: preview.end,
|
|
96
152
|
hovered: null,
|
|
97
153
|
};
|
|
98
154
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
@@ -103,11 +159,15 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
103
159
|
// Otherwise start a new connection from the shape under the pointer.
|
|
104
160
|
const src = shapeAt(ctx, v);
|
|
105
161
|
if (!src) return;
|
|
162
|
+
const sourceBox = shapeBox(ctx, src);
|
|
163
|
+
if (!sourceBox) return;
|
|
164
|
+
const preview = previewNew(sourceBox, v, null);
|
|
106
165
|
drag = {
|
|
107
166
|
mode: 'new',
|
|
108
167
|
sourceId: src,
|
|
109
|
-
|
|
110
|
-
|
|
168
|
+
sourceBox,
|
|
169
|
+
start: preview.start,
|
|
170
|
+
end: preview.end,
|
|
111
171
|
hovered: null,
|
|
112
172
|
};
|
|
113
173
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
@@ -118,10 +178,20 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
118
178
|
if (!drag) return;
|
|
119
179
|
const { sx, sy } = pointer(e);
|
|
120
180
|
const v = ctx.screenToViewport(sx, sy);
|
|
121
|
-
drag.current = v;
|
|
122
181
|
const tgt = shapeAt(ctx, v);
|
|
123
182
|
const exclude = drag.mode === 'new' ? drag.sourceId : drag.fixedShapeId;
|
|
124
|
-
|
|
183
|
+
const targetId = tgt && tgt !== exclude ? tgt : null;
|
|
184
|
+
const targetBox = targetId ? shapeBox(ctx, targetId) : null;
|
|
185
|
+
if (drag.mode === 'new') {
|
|
186
|
+
const preview = previewNew(drag.sourceBox, v, targetBox);
|
|
187
|
+
drag.start = preview.start;
|
|
188
|
+
drag.end = preview.end;
|
|
189
|
+
} else {
|
|
190
|
+
const preview = previewRetarget(drag.fixedBox, drag.end, v, targetBox);
|
|
191
|
+
drag.start = preview.start;
|
|
192
|
+
drag.endPoint = preview.end;
|
|
193
|
+
}
|
|
194
|
+
drag.hovered = targetBox?.id ?? null;
|
|
125
195
|
},
|
|
126
196
|
|
|
127
197
|
onPointerUp(e, ctx) {
|
|
@@ -139,13 +209,13 @@ export function createDrawingConnectTool(options: DrawingConnectOptions = {}): S
|
|
|
139
209
|
|
|
140
210
|
renderOverlay(): JSX.Element | null {
|
|
141
211
|
if (!drag) return null;
|
|
142
|
-
const a = drag.
|
|
143
|
-
const b = drag.
|
|
212
|
+
const a = drag.start;
|
|
213
|
+
const b = drag.mode === 'new' ? drag.end : drag.endPoint;
|
|
144
214
|
return createElement(
|
|
145
215
|
'g',
|
|
146
216
|
{ key: 'drawing-connect-preview' },
|
|
147
217
|
createElement('path', {
|
|
148
|
-
d:
|
|
218
|
+
d: straightPath(a, b),
|
|
149
219
|
className: 'squisq-scene-connect-preview',
|
|
150
220
|
fill: 'none',
|
|
151
221
|
}),
|
|
@@ -55,6 +55,8 @@ export interface SceneTool {
|
|
|
55
55
|
cursor: string;
|
|
56
56
|
/** Optional keyboard accelerator (single-character, lowercase). */
|
|
57
57
|
shortcut?: string;
|
|
58
|
+
/** Hide resize handles on selected layers while this tool is active. */
|
|
59
|
+
hideSelectionHandles?: boolean;
|
|
58
60
|
/**
|
|
59
61
|
* Optional icon — JSX rendered inside a fixed-size toolbar button. Keep
|
|
60
62
|
* it light (a single SVG path is ideal); the toolbar wrapper handles
|