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.
@@ -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
- if (activeSnaps.length === 0 && (!debugConfig?.enabled || !allCandidates?.length)) {
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
- activeSnaps.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 }))),
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 {};