react-svg-canvas 0.0.1 → 0.1.0

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 (51) hide show
  1. package/LICENSE +8 -8
  2. package/README.md +610 -2
  3. package/lib/geometry/bounds.d.ts +44 -0
  4. package/lib/geometry/bounds.js +157 -0
  5. package/lib/geometry/index.d.ts +6 -0
  6. package/lib/geometry/index.js +7 -0
  7. package/lib/geometry/math.d.ts +44 -0
  8. package/lib/geometry/math.js +80 -0
  9. package/lib/geometry/transforms.d.ts +52 -0
  10. package/lib/geometry/transforms.js +122 -0
  11. package/lib/hooks/index.d.ts +5 -0
  12. package/lib/hooks/index.js +6 -0
  13. package/lib/hooks/useDraggable.d.ts +32 -0
  14. package/lib/hooks/useDraggable.js +162 -0
  15. package/lib/hooks/useResizable.d.ts +33 -0
  16. package/lib/hooks/useResizable.js +78 -0
  17. package/lib/index.d.ts +9 -0
  18. package/lib/index.js +16 -0
  19. package/lib/queries/index.d.ts +4 -0
  20. package/lib/queries/index.js +5 -0
  21. package/lib/queries/spatial.d.ts +51 -0
  22. package/lib/queries/spatial.js +107 -0
  23. package/lib/selection/ResizeHandle.d.ts +18 -0
  24. package/lib/selection/ResizeHandle.js +14 -0
  25. package/lib/selection/SelectionBox.d.ts +21 -0
  26. package/lib/selection/SelectionBox.js +21 -0
  27. package/lib/selection/index.d.ts +6 -0
  28. package/lib/selection/index.js +7 -0
  29. package/lib/selection/useSelection.d.ts +43 -0
  30. package/lib/selection/useSelection.js +101 -0
  31. package/lib/snapping/SnapDebugOverlay.d.ts +17 -0
  32. package/lib/snapping/SnapDebugOverlay.js +105 -0
  33. package/lib/snapping/SnapGuides.d.ts +20 -0
  34. package/lib/snapping/SnapGuides.js +184 -0
  35. package/lib/snapping/index.d.ts +12 -0
  36. package/lib/snapping/index.js +17 -0
  37. package/lib/snapping/rotation-utils.d.ts +67 -0
  38. package/lib/snapping/rotation-utils.js +204 -0
  39. package/lib/snapping/snap-engine.d.ts +40 -0
  40. package/lib/snapping/snap-engine.js +592 -0
  41. package/lib/snapping/snap-targets.d.ts +40 -0
  42. package/lib/snapping/snap-targets.js +272 -0
  43. package/lib/snapping/types.d.ts +178 -0
  44. package/lib/snapping/types.js +37 -0
  45. package/lib/snapping/useSnapping.d.ts +52 -0
  46. package/lib/snapping/useSnapping.js +121 -0
  47. package/lib/svgcanvas.d.ts +27 -2
  48. package/lib/svgcanvas.js +189 -116
  49. package/lib/types.d.ts +67 -0
  50. package/lib/types.js +14 -0
  51. package/package.json +45 -11
package/LICENSE CHANGED
@@ -1,18 +1,18 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Szilard Hajba <szilu@symbion.hu>
3
+ Copyright (c) 2024 Szilárd Hajba
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy of
6
- this software and associated documentation files (the 'Software'), to deal in
7
- the Software without restriction, including without limitation the rights to
8
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
- of the Software, and to permit persons to whom the Software is furnished to do
10
- so, subject to the following conditions:
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
11
 
12
12
  The above copyright notice and this permission notice shall be included in all
13
13
  copies or substantial portions of the Software.
14
14
 
15
- THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
16
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
17
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
package/README.md CHANGED
@@ -1,4 +1,612 @@
1
- React SVG Canvas
2
- ================
1
+ # react-svg-canvas
3
2
 
3
+ A React library for building interactive SVG canvas applications with pan, zoom, selection, drag-and-drop, resize, and Figma-style snapping.
4
4
 
5
+ ## Features
6
+
7
+ - **Pan & Zoom** - Middle mouse/touch panning, mouse wheel/pinch-to-zoom
8
+ - **Touch Support** - Full touch event handling for mobile devices
9
+ - **Selection System** - Multi-select, rectangle selection, selection bounds
10
+ - **Drag & Drop** - Smooth dragging with window-level event handling
11
+ - **Resize Handles** - 8-point resize with min/max constraints
12
+ - **Snapping** - Figma-style snapping to edges, centers, grid, and matching sizes
13
+ - **Geometry Utilities** - Bounds operations, transforms, coordinate conversion
14
+ - **Spatial Queries** - Hit testing, rectangle selection, culling
15
+ - **TypeScript** - Full type definitions included
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install react-svg-canvas
21
+ # or
22
+ pnpm add react-svg-canvas
23
+ # or
24
+ yarn add react-svg-canvas
25
+ ```
26
+
27
+ **Peer Dependencies:** React 18+
28
+
29
+ ## Quick Start
30
+
31
+ ```tsx
32
+ import { SvgCanvas, useSvgCanvas } from 'react-svg-canvas'
33
+
34
+ function MyCanvas() {
35
+ return (
36
+ <SvgCanvas
37
+ className="my-canvas"
38
+ style={{ width: '100%', height: '100%' }}
39
+ >
40
+ <rect x={100} y={100} width={200} height={150} fill="#3b82f6" />
41
+ <circle cx={400} cy={200} r={50} fill="#ef4444" />
42
+ </SvgCanvas>
43
+ )
44
+ }
45
+ ```
46
+
47
+ ## API Reference
48
+
49
+ ### SvgCanvas
50
+
51
+ The main canvas component that provides pan, zoom, and coordinate transformation.
52
+
53
+ ```tsx
54
+ import { SvgCanvas, SvgCanvasHandle } from 'react-svg-canvas'
55
+
56
+ function App() {
57
+ const canvasRef = useRef<SvgCanvasHandle>(null)
58
+
59
+ return (
60
+ <SvgCanvas
61
+ ref={canvasRef}
62
+ className="canvas"
63
+ style={{ width: '100vw', height: '100vh' }}
64
+ fixed={<MyToolbar />} // Renders in screen space (not transformed)
65
+ onToolStart={(e) => console.log('Tool start:', e.x, e.y)}
66
+ onToolMove={(e) => console.log('Tool move:', e.x, e.y)}
67
+ onToolEnd={() => console.log('Tool end')}
68
+ onContextReady={(ctx) => console.log('Scale:', ctx.scale)}
69
+ >
70
+ {/* Children render in canvas space (transformed) */}
71
+ <MyShapes />
72
+ </SvgCanvas>
73
+ )
74
+ }
75
+ ```
76
+
77
+ #### Props
78
+
79
+ | Prop | Type | Description |
80
+ |------|------|-------------|
81
+ | `className` | `string` | CSS class for the SVG element |
82
+ | `style` | `CSSProperties` | Inline styles for the SVG element |
83
+ | `children` | `ReactNode` | Content rendered in canvas space (pan/zoom applied) |
84
+ | `fixed` | `ReactNode` | Content rendered in screen space (UI overlays) |
85
+ | `onToolStart` | `(e: ToolEvent) => void` | Called on left mouse/touch start |
86
+ | `onToolMove` | `(e: ToolEvent) => void` | Called during drag |
87
+ | `onToolEnd` | `() => void` | Called on mouse/touch end |
88
+ | `onContextReady` | `(ctx: SvgCanvasContext) => void` | Called when context changes (zoom, pan) |
89
+
90
+ #### Imperative Handle
91
+
92
+ ```tsx
93
+ const canvasRef = useRef<SvgCanvasHandle>(null)
94
+
95
+ // Center viewport on a point
96
+ canvasRef.current?.centerOn(100, 100, 1.5) // x, y, optional zoom
97
+
98
+ // Fit a rectangle in view
99
+ canvasRef.current?.centerOnRect(0, 0, 500, 400, 50) // x, y, w, h, padding
100
+
101
+ // Get/set transform matrix
102
+ const matrix = canvasRef.current?.getMatrix()
103
+ canvasRef.current?.setMatrix([1, 0, 0, 1, 0, 0])
104
+ ```
105
+
106
+ #### Interaction Controls
107
+
108
+ | Input | Action |
109
+ |-------|--------|
110
+ | Left mouse | Tool events (onToolStart/Move/End) |
111
+ | Middle mouse | Pan canvas |
112
+ | Mouse wheel | Zoom in/out |
113
+ | Single touch | Tool events or pan |
114
+ | Two-finger pinch | Zoom |
115
+
116
+ ---
117
+
118
+ ### useSvgCanvas
119
+
120
+ Hook to access canvas context from child components.
121
+
122
+ ```tsx
123
+ function MyShape() {
124
+ const { svg, matrix, scale, translateTo, translateFrom } = useSvgCanvas()
125
+
126
+ // Convert screen coords to canvas coords
127
+ const [canvasX, canvasY] = translateTo(screenX, screenY)
128
+
129
+ // Convert canvas coords to screen coords
130
+ const [screenX, screenY] = translateFrom(canvasX, canvasY)
131
+
132
+ return <rect x={100} y={100} width={100 / scale} height={100 / scale} />
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ### Selection System
139
+
140
+ #### useSelection
141
+
142
+ Manages selection state for canvas objects.
143
+
144
+ ```tsx
145
+ import { useSelection, SpatialObject } from 'react-svg-canvas'
146
+
147
+ interface MyObject extends SpatialObject {
148
+ id: string
149
+ bounds: Bounds
150
+ color: string
151
+ }
152
+
153
+ function Canvas({ objects }: { objects: MyObject[] }) {
154
+ const {
155
+ selectedIds,
156
+ selectedObjects,
157
+ selectionCount,
158
+ selectionBounds,
159
+ hasSelection,
160
+ select,
161
+ selectMultiple,
162
+ deselect,
163
+ toggle,
164
+ clear,
165
+ selectAll,
166
+ selectInRect,
167
+ setSelection,
168
+ isSelected
169
+ } = useSelection({ objects, onChange: (ids) => console.log('Selection:', ids) })
170
+
171
+ return (
172
+ <SvgCanvas>
173
+ {objects.map(obj => (
174
+ <rect
175
+ key={obj.id}
176
+ {...obj.bounds}
177
+ fill={isSelected(obj.id) ? 'blue' : obj.color}
178
+ onClick={(e) => select(obj.id, e.shiftKey)}
179
+ />
180
+ ))}
181
+ {selectionBounds && (
182
+ <SelectionBox bounds={selectionBounds} onResizeStart={handleResize} />
183
+ )}
184
+ </SvgCanvas>
185
+ )
186
+ }
187
+ ```
188
+
189
+ #### SelectionBox
190
+
191
+ Renders a selection rectangle with resize handles.
192
+
193
+ ```tsx
194
+ import { SelectionBox } from 'react-svg-canvas'
195
+
196
+ <SelectionBox
197
+ bounds={{ x: 100, y: 100, width: 200, height: 150 }}
198
+ rotation={45}
199
+ stroke="#0066ff"
200
+ strokeDasharray="4,4"
201
+ showHandles={true}
202
+ handleSize={8}
203
+ onResizeStart={(handle, e) => console.log('Resize:', handle)}
204
+ />
205
+ ```
206
+
207
+ ---
208
+
209
+ ### Interaction Hooks
210
+
211
+ #### useDraggable
212
+
213
+ Provides smooth drag interaction with window-level events.
214
+
215
+ ```tsx
216
+ import { useDraggable, svgTransformCoordinates } from 'react-svg-canvas'
217
+
218
+ function DraggableRect({ x, y, onMove }) {
219
+ const { isDragging, dragProps } = useDraggable({
220
+ onDragStart: (e) => console.log('Start:', e.x, e.y),
221
+ onDragMove: (e) => onMove(e.deltaX, e.deltaY),
222
+ onDragEnd: (e) => console.log('End'),
223
+ transformCoordinates: svgTransformCoordinates // For SVG coordinate space
224
+ })
225
+
226
+ return (
227
+ <rect
228
+ x={x} y={y}
229
+ width={100} height={80}
230
+ fill={isDragging ? 'orange' : 'blue'}
231
+ style={{ cursor: 'move' }}
232
+ {...dragProps}
233
+ />
234
+ )
235
+ }
236
+ ```
237
+
238
+ #### useResizable
239
+
240
+ Provides resize interaction for selected objects.
241
+
242
+ ```tsx
243
+ import { useResizable } from 'react-svg-canvas'
244
+
245
+ function ResizableRect({ bounds, onResize }) {
246
+ const { isResizing, activeHandle, handleResizeStart } = useResizable({
247
+ bounds,
248
+ minWidth: 50,
249
+ minHeight: 50,
250
+ onResize: (e) => onResize(e.bounds),
251
+ onResizeEnd: (e) => console.log('Final bounds:', e.bounds)
252
+ })
253
+
254
+ return (
255
+ <SelectionBox
256
+ bounds={bounds}
257
+ onResizeStart={handleResizeStart}
258
+ />
259
+ )
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ### Snapping System
266
+
267
+ Figma-style snapping with visual guide lines.
268
+
269
+ #### useSnapping
270
+
271
+ ```tsx
272
+ import { useSnapping, SnapGuides, DEFAULT_SNAP_CONFIG } from 'react-svg-canvas'
273
+
274
+ function Canvas({ objects }) {
275
+ const { svg, translateFrom } = useSvgCanvas()
276
+ const viewBounds = { x: 0, y: 0, width: 1000, height: 800 }
277
+
278
+ const { snapDrag, snapResize, activeSnaps, allCandidates, clearSnaps } = useSnapping({
279
+ objects,
280
+ config: DEFAULT_SNAP_CONFIG,
281
+ viewBounds
282
+ })
283
+
284
+ function handleDrag(objectId, bounds, delta, grabPoint) {
285
+ const result = snapDrag({
286
+ bounds: { ...bounds, rotation: 0 },
287
+ objectId,
288
+ delta,
289
+ grabPoint
290
+ })
291
+ // result.position contains snapped coordinates
292
+ // result.activeSnaps contains active snap info
293
+ }
294
+
295
+ return (
296
+ <SvgCanvas
297
+ fixed={
298
+ <SnapGuides
299
+ activeSnaps={activeSnaps}
300
+ config={DEFAULT_SNAP_CONFIG.guides}
301
+ viewBounds={viewBounds}
302
+ transformPoint={translateFrom}
303
+ />
304
+ }
305
+ >
306
+ {/* Your objects */}
307
+ </SvgCanvas>
308
+ )
309
+ }
310
+ ```
311
+
312
+ #### useGrabPoint
313
+
314
+ Helper hook for calculating the normalized grab point when dragging objects.
315
+
316
+ ```tsx
317
+ import { useGrabPoint } from 'react-svg-canvas'
318
+
319
+ function MyDraggable({ bounds }) {
320
+ const { setGrabPoint, getGrabPoint } = useGrabPoint()
321
+
322
+ function handleDragStart(mousePos) {
323
+ setGrabPoint(mousePos, bounds)
324
+ }
325
+
326
+ function handleDrag(delta) {
327
+ const grabPoint = getGrabPoint() // Returns { x: 0-1, y: 0-1 }
328
+ // Use with snapDrag...
329
+ }
330
+ }
331
+ ```
332
+
333
+ #### Snap Configuration
334
+
335
+ ```tsx
336
+ const config: SnapConfiguration = {
337
+ enabled: true,
338
+ snapToGrid: true,
339
+ snapToObjects: true,
340
+ snapToSizes: true, // Snap to matching widths/heights
341
+ gridSize: 10,
342
+ snapThreshold: 8, // Pixels within which snapping activates
343
+ weights: {
344
+ distance: 10, // How much distance affects snap priority
345
+ direction: 3, // Movement direction influence
346
+ velocity: 2, // Faster movement = less sticky
347
+ grabProximity: 5, // Snaps near grab point prioritized
348
+ hierarchy: 4, // Parent/sibling preference
349
+ edgePriority: 1.2,
350
+ centerPriority: 1.0,
351
+ gridPriority: 0.8,
352
+ sizePriority: 0.9
353
+ },
354
+ guides: {
355
+ color: '#ff3366',
356
+ strokeWidth: 1,
357
+ showDistanceIndicators: true
358
+ },
359
+ debug: {
360
+ enabled: false,
361
+ showTopN: 5,
362
+ showScores: true,
363
+ showScoreBreakdown: false
364
+ }
365
+ }
366
+ ```
367
+
368
+ ---
369
+
370
+ ### Geometry Utilities
371
+
372
+ #### Bounds Operations
373
+
374
+ ```tsx
375
+ import {
376
+ getBoundsCenter,
377
+ expandBounds,
378
+ unionBounds,
379
+ unionAllBounds,
380
+ boundsIntersect,
381
+ boundsContains,
382
+ pointInBounds,
383
+ boundsFromPoints,
384
+ getHandlePositions,
385
+ resizeBounds
386
+ } from 'react-svg-canvas'
387
+
388
+ // Get center point
389
+ const center = getBoundsCenter({ x: 0, y: 0, width: 100, height: 100 })
390
+ // { x: 50, y: 50 }
391
+
392
+ // Expand bounds by margin
393
+ const expanded = expandBounds(bounds, 10)
394
+
395
+ // Union of two bounds
396
+ const combined = unionBounds(boundsA, boundsB)
397
+
398
+ // Check intersection
399
+ if (boundsIntersect(selection, object.bounds)) {
400
+ // Object is selected
401
+ }
402
+
403
+ // Create bounds from drag rectangle
404
+ const selectionRect = boundsFromPoints(startPoint, endPoint)
405
+
406
+ // Resize bounds from handle drag
407
+ const newBounds = resizeBounds(originalBounds, 'se', deltaX, deltaY, minW, minH)
408
+ ```
409
+
410
+ #### Transforms
411
+
412
+ ```tsx
413
+ import {
414
+ transformPoint,
415
+ invertTransform,
416
+ composeTransforms,
417
+ matrixToTransform,
418
+ transformToMatrix,
419
+ getAbsolutePosition
420
+ } from 'react-svg-canvas'
421
+
422
+ // Apply transform to point
423
+ const worldPoint = transformPoint({ x: 10, y: 10 }, transform)
424
+
425
+ // Convert SVG matrix to transform object
426
+ const transform = matrixToTransform([1, 0, 0, 1, 100, 50])
427
+
428
+ // Get absolute position walking up hierarchy
429
+ const absPos = getAbsolutePosition(item, (item) => itemsById[item.parentId])
430
+ ```
431
+
432
+ #### Math Utilities
433
+
434
+ ```tsx
435
+ import {
436
+ rotatePoint,
437
+ scalePoint,
438
+ distance,
439
+ snapToGrid,
440
+ snapPointToGrid,
441
+ lerp,
442
+ clamp,
443
+ normalizeAngle,
444
+ degToRad,
445
+ radToDeg
446
+ } from 'react-svg-canvas'
447
+
448
+ // Rotate point around center
449
+ const rotated = rotatePoint({ x: 100, y: 0 }, { x: 0, y: 0 }, 90)
450
+
451
+ // Snap to grid
452
+ const snapped = snapPointToGrid({ x: 123, y: 456 }, 10)
453
+ // { x: 120, y: 460 }
454
+ ```
455
+
456
+ ---
457
+
458
+ ### Spatial Queries
459
+
460
+ ```tsx
461
+ import {
462
+ getObjectsAtPoint,
463
+ getTopmostAtPoint,
464
+ getObjectsIntersectingRect,
465
+ getObjectsContainedInRect,
466
+ getSelectionBounds,
467
+ getObjectsInView,
468
+ findNearestObject,
469
+ getObjectsInRadius
470
+ } from 'react-svg-canvas'
471
+
472
+ // Hit testing
473
+ const clicked = getTopmostAtPoint(objects, { x: mouseX, y: mouseY })
474
+
475
+ // Rectangle selection
476
+ const selected = getObjectsIntersectingRect(objects, selectionRect)
477
+
478
+ // Viewport culling (render only visible objects)
479
+ const visible = getObjectsInView(objects, viewBounds)
480
+
481
+ // Find nearest object
482
+ const nearest = findNearestObject(objects, cursorPos, maxDistance)
483
+ ```
484
+
485
+ ---
486
+
487
+ ## Types
488
+
489
+ ```tsx
490
+ interface Point {
491
+ x: number
492
+ y: number
493
+ }
494
+
495
+ interface Bounds {
496
+ x: number
497
+ y: number
498
+ width: number
499
+ height: number
500
+ }
501
+
502
+ interface Transform {
503
+ x: number
504
+ y: number
505
+ rotation: number
506
+ scaleX: number
507
+ scaleY: number
508
+ }
509
+
510
+ interface SpatialObject {
511
+ id: string
512
+ bounds: Bounds
513
+ }
514
+
515
+ interface ToolEvent {
516
+ startX: number
517
+ startY: number
518
+ x: number
519
+ y: number
520
+ }
521
+
522
+ type ResizeHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'
523
+ ```
524
+
525
+ ---
526
+
527
+ ## Example: Complete Editor
528
+
529
+ ```tsx
530
+ import {
531
+ SvgCanvas,
532
+ SvgCanvasHandle,
533
+ useSelection,
534
+ useDraggable,
535
+ useSnapping,
536
+ SelectionBox,
537
+ SnapGuides,
538
+ DEFAULT_SNAP_CONFIG
539
+ } from 'react-svg-canvas'
540
+
541
+ function Editor() {
542
+ const canvasRef = useRef<SvgCanvasHandle>(null)
543
+ const [objects, setObjects] = useState<MyObject[]>(initialObjects)
544
+
545
+ const selection = useSelection({
546
+ objects,
547
+ onChange: (ids) => console.log('Selected:', ids)
548
+ })
549
+
550
+ const snapping = useSnapping({
551
+ objects,
552
+ config: DEFAULT_SNAP_CONFIG,
553
+ viewBounds: { x: 0, y: 0, width: 1920, height: 1080 }
554
+ })
555
+
556
+ return (
557
+ <SvgCanvas
558
+ ref={canvasRef}
559
+ style={{ width: '100%', height: '100vh' }}
560
+ onToolStart={(e) => {
561
+ const hit = getTopmostAtPoint(objects, e)
562
+ if (hit) selection.select(hit.id, false)
563
+ else selection.clear()
564
+ }}
565
+ fixed={
566
+ <SnapGuides
567
+ activeSnaps={snapping.activeSnaps}
568
+ config={DEFAULT_SNAP_CONFIG.guides}
569
+ viewBounds={viewBounds}
570
+ />
571
+ }
572
+ >
573
+ {objects.map(obj => (
574
+ <DraggableShape
575
+ key={obj.id}
576
+ object={obj}
577
+ isSelected={selection.isSelected(obj.id)}
578
+ onMove={(delta) => {
579
+ const result = snapping.snapDrag({
580
+ bounds: obj.bounds,
581
+ objectId: obj.id,
582
+ delta,
583
+ grabPoint: { x: 0.5, y: 0.5 }
584
+ })
585
+ updateObject(obj.id, result.position)
586
+ }}
587
+ />
588
+ ))}
589
+
590
+ {selection.selectionBounds && (
591
+ <SelectionBox
592
+ bounds={selection.selectionBounds}
593
+ onResizeStart={handleResize}
594
+ />
595
+ )}
596
+ </SvgCanvas>
597
+ )
598
+ }
599
+ ```
600
+
601
+ ## Browser Support
602
+
603
+ - Modern browsers with ES2021 support
604
+ - Touch devices (iOS Safari, Android Chrome)
605
+
606
+ ## License
607
+
608
+ MIT License - see [LICENSE](./LICENSE) for details.
609
+
610
+ ## Author
611
+
612
+ Szilard Hajba <szilard@cloudillo.org>
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Bounds operations for SVG canvas
3
+ */
4
+ import type { Point, Bounds, HandlePosition, ResizeHandle } from '../types';
5
+ /**
6
+ * Get center of bounds
7
+ */
8
+ export declare function getBoundsCenter(bounds: Bounds): Point;
9
+ /**
10
+ * Expand bounds by margin
11
+ */
12
+ export declare function expandBounds(bounds: Bounds, margin: number): Bounds;
13
+ /**
14
+ * Union of two bounds
15
+ */
16
+ export declare function unionBounds(a: Bounds, b: Bounds): Bounds;
17
+ /**
18
+ * Union of multiple bounds
19
+ */
20
+ export declare function unionAllBounds(boundsArray: Bounds[]): Bounds | null;
21
+ /**
22
+ * Check if two bounds intersect (AABB collision)
23
+ */
24
+ export declare function boundsIntersect(a: Bounds, b: Bounds): boolean;
25
+ /**
26
+ * Check if point is inside bounds
27
+ */
28
+ export declare function pointInBounds(point: Point, bounds: Bounds): boolean;
29
+ /**
30
+ * Check if bounds A is completely inside bounds B
31
+ */
32
+ export declare function boundsContains(outer: Bounds, inner: Bounds): boolean;
33
+ /**
34
+ * Create bounds from two points (handles negative width/height)
35
+ */
36
+ export declare function boundsFromPoints(p1: Point, p2: Point): Bounds;
37
+ /**
38
+ * Get the 8 resize handle positions for a bounds
39
+ */
40
+ export declare function getHandlePositions(bounds: Bounds): HandlePosition[];
41
+ /**
42
+ * Calculate new bounds after resizing from a handle
43
+ */
44
+ export declare function resizeBounds(originalBounds: Bounds, handle: ResizeHandle, deltaX: number, deltaY: number, minWidth?: number, minHeight?: number): Bounds;