react-svg-canvas 0.1.2 → 0.1.3
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/lib/snapping/SnapGuides.d.ts +4 -2
- package/lib/snapping/SnapGuides.js +87 -3
- package/lib/snapping/distribution-detection.d.ts +54 -0
- package/lib/snapping/distribution-detection.js +715 -0
- package/lib/snapping/index.d.ts +1 -0
- package/lib/snapping/index.js +2 -0
- package/lib/snapping/snap-engine.d.ts +9 -0
- package/lib/snapping/snap-engine.js +155 -5
- package/lib/snapping/snap-targets.d.ts +9 -5
- package/lib/snapping/snap-targets.js +204 -16
- package/lib/snapping/types.d.ts +45 -1
- package/lib/snapping/types.js +3 -1
- package/lib/snapping/useSnapping.d.ts +3 -1
- package/lib/snapping/useSnapping.js +7 -2
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
import type { Bounds } from '../types';
|
|
6
|
-
import type { ActiveSnap, ScoredCandidate, SnapGuidesConfig, SnapDebugConfig, RotatedBounds } from './types';
|
|
6
|
+
import type { ActiveSnap, ActiveSnapEdge, ScoredCandidate, SnapGuidesConfig, SnapDebugConfig, RotatedBounds } from './types';
|
|
7
7
|
export interface SnapGuidesProps {
|
|
8
8
|
activeSnaps: ActiveSnap[];
|
|
9
9
|
allCandidates?: ScoredCandidate[];
|
|
@@ -11,10 +11,12 @@ export interface SnapGuidesProps {
|
|
|
11
11
|
debugConfig?: SnapDebugConfig;
|
|
12
12
|
viewBounds: Bounds;
|
|
13
13
|
draggedBounds?: RotatedBounds;
|
|
14
|
+
/** Which snap edges are active based on grab point (for visual feedback) */
|
|
15
|
+
activeSnapEdges?: ActiveSnapEdge[];
|
|
14
16
|
/** Transform function to convert canvas coords to screen coords (for fixed layer rendering) */
|
|
15
17
|
transformPoint?: (x: number, y: number) => [number, number];
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
20
|
* Main snap guides component - renders Figma-style red guide lines
|
|
19
21
|
*/
|
|
20
|
-
export declare function SnapGuides({ activeSnaps, allCandidates, config, debugConfig, viewBounds, draggedBounds, transformPoint }: SnapGuidesProps): React.JSX.Element | null;
|
|
22
|
+
export declare function SnapGuides({ activeSnaps, allCandidates, config, debugConfig, viewBounds, draggedBounds, activeSnapEdges, transformPoint }: SnapGuidesProps): React.JSX.Element | null;
|
|
@@ -49,11 +49,55 @@ function SourceBoundingBoxHighlight({ bounds, color, strokeWidth, transformPoint
|
|
|
49
49
|
const height = Math.abs(y2 - y1);
|
|
50
50
|
return (React.createElement("rect", { x: minX, y: minY, width: width, height: height, fill: "none", stroke: color, strokeWidth: strokeWidth, strokeDasharray: "4,3", strokeOpacity: 0.7 }));
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Render active snap edge indicators on the dragged object
|
|
54
|
+
* Shows lines along the edges that are being used for snapping
|
|
55
|
+
*/
|
|
56
|
+
function ActiveSnapEdgeIndicators({ activeSnapEdges, draggedBounds, config, transformPoint }) {
|
|
57
|
+
if (activeSnapEdges.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
const color = config.color;
|
|
60
|
+
const extensionLength = 10; // How far to extend beyond the object bounds
|
|
61
|
+
return (React.createElement("g", { className: "active-snap-edges" }, activeSnapEdges.map((edge, index) => {
|
|
62
|
+
// Calculate line along the full edge
|
|
63
|
+
let x1, y1, x2, y2;
|
|
64
|
+
if (edge.axis === 'x') {
|
|
65
|
+
// Vertical edge (left, right, centerX)
|
|
66
|
+
// Draw a vertical line along the edge, extending beyond bounds
|
|
67
|
+
const edgeX = edge.position;
|
|
68
|
+
x1 = edgeX;
|
|
69
|
+
y1 = draggedBounds.y - extensionLength;
|
|
70
|
+
x2 = edgeX;
|
|
71
|
+
y2 = draggedBounds.y + draggedBounds.height + extensionLength;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Horizontal edge (top, bottom, centerY)
|
|
75
|
+
// Draw a horizontal line along the edge, extending beyond bounds
|
|
76
|
+
const edgeY = edge.position;
|
|
77
|
+
x1 = draggedBounds.x - extensionLength;
|
|
78
|
+
y1 = edgeY;
|
|
79
|
+
x2 = draggedBounds.x + draggedBounds.width + extensionLength;
|
|
80
|
+
y2 = edgeY;
|
|
81
|
+
}
|
|
82
|
+
// Transform to screen coordinates
|
|
83
|
+
const [sx1, sy1] = transformPoint(x1, y1);
|
|
84
|
+
const [sx2, sy2] = transformPoint(x2, y2);
|
|
85
|
+
// Calculate midpoint for the diamond marker
|
|
86
|
+
const midX = (sx1 + sx2) / 2;
|
|
87
|
+
const midY = (sy1 + sy2) / 2;
|
|
88
|
+
return (React.createElement("g", { key: `snap-edge-${edge.edge}-${index}` },
|
|
89
|
+
React.createElement("line", { x1: sx1, y1: sy1, x2: sx2, y2: sy2, stroke: "white", strokeWidth: 5, strokeLinecap: "round" }),
|
|
90
|
+
React.createElement("line", { x1: sx1, y1: sy1, x2: sx2, y2: sy2, stroke: color, strokeWidth: 3, strokeLinecap: "round" }),
|
|
91
|
+
React.createElement(DiamondMarker, { x: midX, y: midY, size: 10, color: "white" }),
|
|
92
|
+
React.createElement(DiamondMarker, { x: midX, y: midY, size: 7, color: color })));
|
|
93
|
+
})));
|
|
94
|
+
}
|
|
52
95
|
/**
|
|
53
96
|
* Main snap guides component - renders Figma-style red guide lines
|
|
54
97
|
*/
|
|
55
|
-
export function SnapGuides({ activeSnaps, allCandidates, config, debugConfig, viewBounds, draggedBounds, transformPoint }) {
|
|
56
|
-
|
|
98
|
+
export function SnapGuides({ activeSnaps, allCandidates, config, debugConfig, viewBounds, draggedBounds, activeSnapEdges, transformPoint }) {
|
|
99
|
+
const hasActiveContent = activeSnaps.length > 0 || (activeSnapEdges && activeSnapEdges.length > 0);
|
|
100
|
+
if (!hasActiveContent && (!debugConfig?.enabled || !allCandidates?.length)) {
|
|
57
101
|
return null;
|
|
58
102
|
}
|
|
59
103
|
// Identity transform if none provided
|
|
@@ -71,7 +115,13 @@ export function SnapGuides({ activeSnaps, allCandidates, config, debugConfig, vi
|
|
|
71
115
|
}, [activeSnaps]);
|
|
72
116
|
return (React.createElement("g", { className: "snap-guides", pointerEvents: "none" },
|
|
73
117
|
sourceObjectsToHighlight.map(([sourceId, bounds]) => (React.createElement(SourceBoundingBoxHighlight, { key: `source-highlight-${sourceId}`, bounds: bounds, color: config.color, strokeWidth: config.strokeWidth, transformPoint: transform }))),
|
|
74
|
-
|
|
118
|
+
activeSnapEdges && draggedBounds && (React.createElement(ActiveSnapEdgeIndicators, { activeSnapEdges: activeSnapEdges, draggedBounds: draggedBounds, config: config, transformPoint: transform })),
|
|
119
|
+
activeSnaps
|
|
120
|
+
.filter(snap => !snap.distribution)
|
|
121
|
+
.map((snap, index) => (React.createElement(SnapGuideLine, { key: `active-${index}-${snap.target.axis}-${snap.target.value}`, snap: snap, config: config, draggedBounds: draggedBounds, isActive: true, transformPoint: transform }))),
|
|
122
|
+
activeSnaps
|
|
123
|
+
.filter(snap => snap.distribution)
|
|
124
|
+
.map((snap, index) => (React.createElement(DistributionGapsRenderer, { key: `dist-${index}`, snap: snap, config: config, transformPoint: transform }))),
|
|
75
125
|
debugConfig?.enabled && allCandidates && (React.createElement(SnapDebugCandidates, { candidates: allCandidates, config: debugConfig, activeSnaps: activeSnaps, transformPoint: transform }))));
|
|
76
126
|
}
|
|
77
127
|
/**
|
|
@@ -81,6 +131,40 @@ function DiamondMarker({ x, y, size = 4, color }) {
|
|
|
81
131
|
const half = size / 2;
|
|
82
132
|
return (React.createElement("polygon", { points: `${x},${y - half} ${x + half},${y} ${x},${y + half} ${x - half},${y}`, fill: color }));
|
|
83
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Distribution gap indicator with arrows and spacing label
|
|
136
|
+
*/
|
|
137
|
+
function DistributionGapIndicator({ gap, color, strokeWidth, transformPoint }) {
|
|
138
|
+
const [x1, y1] = transformPoint(gap.start.x, gap.start.y);
|
|
139
|
+
const [x2, y2] = transformPoint(gap.end.x, gap.end.y);
|
|
140
|
+
const midX = (x1 + x2) / 2;
|
|
141
|
+
const midY = (y1 + y2) / 2;
|
|
142
|
+
const spacing = Math.round(gap.distance);
|
|
143
|
+
// Arrow size
|
|
144
|
+
const arrowSize = 5;
|
|
145
|
+
const isHorizontal = gap.axis === 'x';
|
|
146
|
+
return (React.createElement("g", null,
|
|
147
|
+
React.createElement("line", { x1: x1, y1: y1, x2: x2, y2: y2, stroke: color, strokeWidth: strokeWidth }),
|
|
148
|
+
isHorizontal ? (React.createElement(React.Fragment, null,
|
|
149
|
+
React.createElement("polygon", { points: `${x1},${y1} ${x1 + arrowSize},${y1 - arrowSize / 2} ${x1 + arrowSize},${y1 + arrowSize / 2}`, fill: color }),
|
|
150
|
+
React.createElement("polygon", { points: `${x2},${y2} ${x2 - arrowSize},${y2 - arrowSize / 2} ${x2 - arrowSize},${y2 + arrowSize / 2}`, fill: color }))) : (React.createElement(React.Fragment, null,
|
|
151
|
+
React.createElement("polygon", { points: `${x1},${y1} ${x1 - arrowSize / 2},${y1 + arrowSize} ${x1 + arrowSize / 2},${y1 + arrowSize}`, fill: color }),
|
|
152
|
+
React.createElement("polygon", { points: `${x2},${y2} ${x2 - arrowSize / 2},${y2 - arrowSize} ${x2 + arrowSize / 2},${y2 - arrowSize}`, fill: color }))),
|
|
153
|
+
spacing > 0 && (React.createElement("g", null, isHorizontal ? (React.createElement(React.Fragment, null,
|
|
154
|
+
React.createElement("rect", { x: midX - 15, y: midY - 18, width: 30, height: 14, fill: "white", fillOpacity: 0.95, rx: 2 }),
|
|
155
|
+
React.createElement("text", { x: midX, y: midY - 7, fontSize: 10, fill: color, fontFamily: "system-ui, sans-serif", textAnchor: "middle" }, spacing))) : (React.createElement(React.Fragment, null,
|
|
156
|
+
React.createElement("rect", { x: midX + 5, y: midY - 7, width: 30, height: 14, fill: "white", fillOpacity: 0.95, rx: 2 }),
|
|
157
|
+
React.createElement("text", { x: midX + 20, y: midY + 4, fontSize: 10, fill: color, fontFamily: "system-ui, sans-serif", textAnchor: "middle" }, spacing)))))));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Render all distribution gaps for an active snap
|
|
161
|
+
*/
|
|
162
|
+
function DistributionGapsRenderer({ snap, config, transformPoint }) {
|
|
163
|
+
if (!snap.distribution || snap.distribution.gaps.length === 0) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return (React.createElement("g", { className: "distribution-gaps" }, snap.distribution.gaps.map((gap, index) => (React.createElement(DistributionGapIndicator, { key: `dist-gap-${index}`, gap: gap, color: config.color, strokeWidth: config.strokeWidth, transformPoint: transformPoint })))));
|
|
167
|
+
}
|
|
84
168
|
/**
|
|
85
169
|
* Individual snap guide line
|
|
86
170
|
*/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distribution snap detection for equal-spacing patterns
|
|
3
|
+
* Detects row, column, and staircase arrangements
|
|
4
|
+
*/
|
|
5
|
+
import type { Point, Bounds } from '../types';
|
|
6
|
+
import type { SnapSpatialObject, RotatedBounds, DistributionSnapInfo, ActiveSnap, SnapConfiguration } from './types';
|
|
7
|
+
/**
|
|
8
|
+
* Candidate for distribution snapping
|
|
9
|
+
*/
|
|
10
|
+
export interface DistributionCandidate {
|
|
11
|
+
/** Snapped position for the dragged object */
|
|
12
|
+
position: Point;
|
|
13
|
+
/** Distribution pattern info */
|
|
14
|
+
info: DistributionSnapInfo;
|
|
15
|
+
/** How far the object moved to snap */
|
|
16
|
+
distance: number;
|
|
17
|
+
/** Score for prioritization */
|
|
18
|
+
score: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Object with computed bounds for distribution detection
|
|
22
|
+
*/
|
|
23
|
+
interface ObjectWithBounds {
|
|
24
|
+
id: string;
|
|
25
|
+
bounds: Bounds;
|
|
26
|
+
centerX: number;
|
|
27
|
+
centerY: number;
|
|
28
|
+
left: number;
|
|
29
|
+
right: number;
|
|
30
|
+
top: number;
|
|
31
|
+
bottom: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Detect row distribution pattern (horizontal equal spacing)
|
|
35
|
+
*/
|
|
36
|
+
export declare function detectRowDistribution(dragged: ObjectWithBounds, objects: ObjectWithBounds[], threshold: number, minObjects?: number): DistributionCandidate | null;
|
|
37
|
+
/**
|
|
38
|
+
* Detect column distribution pattern (vertical equal spacing)
|
|
39
|
+
*/
|
|
40
|
+
export declare function detectColumnDistribution(dragged: ObjectWithBounds, objects: ObjectWithBounds[], threshold: number, minObjects?: number): DistributionCandidate | null;
|
|
41
|
+
/**
|
|
42
|
+
* Detect staircase distribution pattern (diagonal equal spacing)
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectStaircaseDistribution(dragged: ObjectWithBounds, objects: ObjectWithBounds[], threshold: number, minObjects?: number): DistributionCandidate | null;
|
|
45
|
+
/**
|
|
46
|
+
* Main distribution detection function
|
|
47
|
+
* Detects all distribution patterns and returns the best candidates
|
|
48
|
+
*/
|
|
49
|
+
export declare function detectDistribution(draggedBounds: RotatedBounds, draggedId: string, objects: SnapSpatialObject[], excludeIds: Set<string>, config: SnapConfiguration): DistributionCandidate[];
|
|
50
|
+
/**
|
|
51
|
+
* Convert distribution candidate to ActiveSnap for rendering
|
|
52
|
+
*/
|
|
53
|
+
export declare function distributionToActiveSnap(candidate: DistributionCandidate, config: SnapConfiguration): ActiveSnap;
|
|
54
|
+
export {};
|