lcms-pdf-annotation 0.0.0-stage → 1.0.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/README.md CHANGED
@@ -1,3 +1,456 @@
1
- # Temporary Holding Version
1
+ # crystal-draw
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A multi-board drawing and whiteboard React component with PDF import and export functionality.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/crystal-draw)](https://www.npmjs.com/package/crystal-draw)
6
+ [![license](https://img.shields.io/npm/l/crystal-draw)](https://www.npmjs.com/package/crystal-draw)
7
+
8
+ ---
9
+
10
+ ## Features
11
+
12
+ - đŸ–Šī¸ **Drawing tools** — Pen, Highlighter, Eraser, and Text
13
+ - 📐 **Shape tools** — Lines, Arrows, Rectangles, Circles, Triangles, Polygons, 2D/3D Graphs, Cones, Cylinders, Cuboids
14
+ - đŸ—‚ī¸ **Multi-board** — Add, remove, and navigate between multiple boards
15
+ - â†Šī¸ **Undo / Redo** — Full history with keyboard shortcuts (`Ctrl+Z` / `Ctrl+Y` / `Ctrl+Shift+Z`)
16
+ - 📄 **PDF import** — Upload a PDF and each page becomes a board background
17
+ - 📤 **PDF export** — Export all boards as a single PDF file
18
+ - 📱 **Touch support** — Works on touch devices
19
+ - 🎨 **Customisable** — Accepts `className` and `style` props for easy layout integration
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install crystal-draw
27
+ ```
28
+
29
+ > **Peer dependencies** — React 16 or higher is required and must be installed in your project.
30
+
31
+ ---
32
+
33
+ ## Quick Start
34
+
35
+ ```tsx
36
+ import CrystalDraw from 'crystal-draw';
37
+ import 'crystal-draw/style.css';
38
+
39
+ export default function App() {
40
+ return (
41
+ <div style={{ width: '100vw', height: '100vh' }}>
42
+ <CrystalDraw />
43
+ </div>
44
+ );
45
+ }
46
+ ```
47
+
48
+ > **Important:** Always import `crystal-draw/style.css` so that cursor and tool styles are applied correctly.
49
+
50
+ ---
51
+
52
+ ## Component Props
53
+
54
+ | Prop | Type | Default | Description |
55
+ |-------------|-------------------------|-------------|--------------------------------------------------|
56
+ | `className` | `string` | `undefined` | CSS class(es) applied to the root container div |
57
+ | `style` | `React.CSSProperties` | `undefined` | Inline styles applied to the root container div |
58
+
59
+ The component fills its parent container. Give the parent a defined width and height to control the canvas size.
60
+
61
+ ```tsx
62
+ <CrystalDraw
63
+ className="my-whiteboard"
64
+ style={{ border: '1px solid #ccc', borderRadius: '8px' }}
65
+ />
66
+ ```
67
+
68
+ ---
69
+
70
+ ## API — Imperative Controls
71
+
72
+ All state is managed by an internal [Zustand](https://github.com/pmndrs/zustand) store. You can control the whiteboard programmatically using the exported helper functions — ideal for building your own toolbar.
73
+
74
+ ### Tool Selection
75
+
76
+ ```tsx
77
+ import { setTool } from 'crystal-draw';
78
+
79
+ // Switch the active drawing tool
80
+ setTool('pen'); // freehand pen
81
+ setTool('highlighter'); // semi-transparent highlighter
82
+ setTool('eraser'); // erase drawn strokes
83
+ setTool('text'); // click on canvas to place text
84
+ setTool('laser'); // laser pointer (no persistent marks)
85
+ setTool('select'); // selection mode
86
+
87
+ // Shape tools (2D)
88
+ setTool('line');
89
+ setTool('arrowline');
90
+ setTool('rectangle');
91
+ setTool('circle');
92
+ setTool('triangle');
93
+ setTool('pentagon');
94
+ setTool('hexagon');
95
+ setTool('octagon');
96
+ setTool('parabola');
97
+ setTool('twoDgraph');
98
+ setTool('threeDgraph');
99
+
100
+ // Shape tools (3D)
101
+ setTool('cuboid');
102
+ setTool('cone');
103
+ setTool('cylinder');
104
+ ```
105
+
106
+ ### Color & Line Width
107
+
108
+ ```tsx
109
+ import { setColor, setHighlighterColor, setLineWidth } from 'crystal-draw';
110
+
111
+ // Set pen / shape color (any valid CSS color string)
112
+ setColor('#ff0000');
113
+ setColor('rgb(0, 128, 255)');
114
+
115
+ // Set highlighter color (use rgba for transparency)
116
+ setHighlighterColor('rgba(255, 255, 0, 0.5)');
117
+
118
+ // Set line width for a specific tool type
119
+ setLineWidth(2, 'PEN'); // thin pen stroke
120
+ setLineWidth(16, 'HIGHLIGHTER'); // default highlighter width
121
+ setLineWidth(60, 'ERASER'); // large eraser
122
+ ```
123
+
124
+ ### Undo & Redo
125
+
126
+ ```tsx
127
+ import { undo, redo } from 'crystal-draw';
128
+
129
+ undo(); // remove last drawn stroke
130
+ redo(); // re-apply last undone stroke
131
+ ```
132
+
133
+ Keyboard shortcuts are enabled automatically when the canvas is mounted:
134
+ - **Undo:** `Ctrl+Z` (Windows/Linux) or `⌘+Z` (macOS)
135
+ - **Redo:** `Ctrl+Y` or `Ctrl+Shift+Z` (Windows/Linux) / `⌘+Shift+Z` (macOS)
136
+
137
+ ### Board Management
138
+
139
+ ```tsx
140
+ import { addBoard, removeBoard, clearBoard, setCurrentBoardIndex } from 'crystal-draw';
141
+
142
+ addBoard(); // append a new blank board
143
+ removeBoard(2); // remove board at index 2
144
+ clearBoard(0); // erase all strokes on board at index 0
145
+ setCurrentBoardIndex(1); // navigate to board at index 1
146
+ ```
147
+
148
+ ### Board Rotation
149
+
150
+ Rotate the active board (or any specific board by index) in 90° increments. Supports rotation in both clockwise and anti-clockwise directions:
151
+
152
+ ```tsx
153
+ import {
154
+ rotateClockwise,
155
+ rotateAntiClockwise,
156
+ rotateBoard,
157
+ getBoardRotation
158
+ } from 'crystal-draw';
159
+
160
+ // Rotate the current board by 90 degrees
161
+ await rotateClockwise(); // rotates 90° clockwise (e.g. 0° -> 90° -> 180° -> 270° -> 0°)
162
+ await rotateAntiClockwise(); // rotates 90° anti-clockwise
163
+
164
+ // Rotate a specific board by board index
165
+ await rotateClockwise(0); // rotate board 0 clockwise by 90°
166
+ await rotateAntiClockwise(2); // rotate board 2 anti-clockwise by 90°
167
+
168
+ // Or use the generic rotateBoard function
169
+ await rotateBoard('clockwise');
170
+ await rotateBoard('anti-clockwise');
171
+
172
+ // Get the current rotation angle (0, 90, 180, or 270)
173
+ const rotation = getBoardRotation(); // e.g. 90
174
+ ```
175
+
176
+ ### PDF Import
177
+
178
+ ```tsx
179
+ import { uploadPDF } from 'crystal-draw';
180
+
181
+ async function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
182
+ const file = event.target.files?.[0];
183
+ if (file) {
184
+ await uploadPDF(file); // each page becomes a new board background
185
+ }
186
+ }
187
+
188
+ // Usage in JSX
189
+ <input type="file" accept="application/pdf" onChange={handleFileChange} />
190
+ ```
191
+
192
+ If the whiteboard currently has only one empty board, `uploadPDF` replaces it with the PDF pages. Otherwise the pages are appended as additional boards.
193
+
194
+ ### PDF Export
195
+
196
+ ```tsx
197
+ import { exportPDF } from 'crystal-draw';
198
+ import { useRef } from 'react';
199
+
200
+ function App() {
201
+ const containerRef = useRef<HTMLDivElement>(null);
202
+
203
+ async function handleExport() {
204
+ const boardsContainer = document.getElementById('crystal--draw-container') as HTMLDivElement;
205
+ if (!boardsContainer) return;
206
+
207
+ const file = await exportPDF(boardsContainer);
208
+ if (file) {
209
+ const url = URL.createObjectURL(file);
210
+ const a = document.createElement('a');
211
+ a.href = url;
212
+ a.download = 'whiteboard.pdf';
213
+ a.click();
214
+ URL.revokeObjectURL(url);
215
+ }
216
+ }
217
+
218
+ return (
219
+ <>
220
+ <button onClick={handleExport}>Export PDF</button>
221
+ <div style={{ width: '100vw', height: '100vh' }}>
222
+ <CrystalDraw />
223
+ </div>
224
+ </>
225
+ );
226
+ }
227
+ ```
228
+
229
+ > The root element rendered by `<CrystalDraw />` always has the id `crystal--draw-container`.
230
+
231
+ ---
232
+
233
+ ## Advanced — Zustand Store
234
+
235
+ For reactive access to the internal state (e.g. building a live toolbar that reflects the current tool), use the exported `useBoundStore` hook directly:
236
+
237
+ ```tsx
238
+ import { useBoundStore } from 'crystal-draw';
239
+
240
+ function Toolbar() {
241
+ const tool = useBoundStore((state) => state.tool);
242
+ const boards = useBoundStore((state) => state.boards);
243
+
244
+ return (
245
+ <div>
246
+ <p>Active tool: {tool}</p>
247
+ <p>Total boards: {boards.length}</p>
248
+ </div>
249
+ );
250
+ }
251
+ ```
252
+
253
+ ### Store State Shape
254
+
255
+ | Field | Type | Description |
256
+ |---------------------|----------------------------------------------|----------------------------------------------|
257
+ | `tool` | `ToolTypes` | Currently active tool |
258
+ | `color` | `string` | Current pen / shape color |
259
+ | `highlighterColor` | `string` | Current highlighter color |
260
+ | `lineWidths` | `{ PEN: number, HIGHLIGHTER: number, ERASER: number }` | Line widths per tool type |
261
+ | `boards` | `Array<{ id, shapes, page, rotation, zoom }>` | All board data (zoom defaults to 1) |
262
+ | `currentBoardIndex` | `number` | Index of the currently active board |
263
+ | `history` | `historyItem[]` | Undo/redo history stack |
264
+ | `undoIndex` | `number` | Current position in the history stack |
265
+
266
+ ---
267
+
268
+ ### Zoom (Page Scale)
269
+
270
+ Each board has an independent zoom level (default `1.0`, range `0.1–5`).
271
+
272
+ ```tsx
273
+ import {
274
+ zoomIn,
275
+ zoomOut,
276
+ setZoom,
277
+ resetZoom,
278
+ getZoom,
279
+ getBoardZoom,
280
+ } from 'crystal-draw';
281
+
282
+ // Zoom in/out by a step (default step = 0.1). Returns the new zoom level.
283
+ const z1 = zoomIn(); // 1.0 → 1.1
284
+ const z2 = zoomOut(0.25); // 1.1 → 0.85
285
+
286
+ // Set an exact zoom level (clamped to [0.1, 5])
287
+ setZoom(1.5); // 150 %
288
+ setZoom(0.5); // 50 %
289
+
290
+ // Reset zoom to 100 %
291
+ resetZoom();
292
+
293
+ // Read the current zoom level
294
+ const zoom = getZoom(); // e.g. 1.5
295
+ const zoom2 = getBoardZoom(2); // zoom of board at index 2
296
+
297
+ // All functions accept an optional boardIndex (defaults to currentBoardIndex)
298
+ zoomIn(0.1, 0); // zoom board 0 in by 0.1
299
+ zoomOut(0.2, 1); // zoom board 1 out by 0.2
300
+ setZoom(2, 0); // set board 0 to 200 %
301
+ resetZoom(1); // reset board 1
302
+ ```
303
+
304
+ #### Via `ref` (imperative API)
305
+
306
+ ```tsx
307
+ import CrystalDraw, { CrystalDrawRef } from 'crystal-draw';
308
+ import { useRef, useState } from 'react';
309
+
310
+ function App() {
311
+ const boardRef = useRef<CrystalDrawRef>(null);
312
+ const [zoom, setCurrentZoom] = useState(1);
313
+
314
+ return (
315
+ <>
316
+ <button onClick={() => { const z = boardRef.current?.zoomIn(); if (z != null) setCurrentZoom(z); }}>
317
+ Zoom In
318
+ </button>
319
+ <button onClick={() => { const z = boardRef.current?.zoomOut(); if (z != null) setCurrentZoom(z); }}>
320
+ Zoom Out
321
+ </button>
322
+ <button onClick={() => { boardRef.current?.resetZoom(); setCurrentZoom(1); }}>
323
+ Reset
324
+ </button>
325
+ <span>{Math.round(zoom * 100)}%</span>
326
+
327
+ <div style={{ width: '100vw', height: '100vh' }}>
328
+ <CrystalDraw ref={boardRef} onZoomChange={(z) => setCurrentZoom(z)} />
329
+ </div>
330
+ </>
331
+ );
332
+ }
333
+ ```
334
+
335
+ #### Via `getZoomCallbacks` prop
336
+
337
+ ```tsx
338
+ import CrystalDraw, { ZoomCallbacks } from 'crystal-draw';
339
+
340
+ function App() {
341
+ let controls: ZoomCallbacks | null = null;
342
+
343
+ return (
344
+ <>
345
+ <button onClick={() => controls?.zoomIn()}>+</button>
346
+ <button onClick={() => controls?.zoomOut()}>-</button>
347
+ <div style={{ width: '100vw', height: '100vh' }}>
348
+ <CrystalDraw
349
+ onZoomChange={(zoom, boardIndex) =>
350
+ console.log(`Board ${boardIndex} zoom: ${zoom}`)
351
+ }
352
+ getZoomCallbacks={(cb) => { controls = cb; }}
353
+ />
354
+ </div>
355
+ </>
356
+ );
357
+ }
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Full Example — Custom Toolbar
363
+
364
+ ```tsx
365
+ import CrystalDraw, {
366
+ setTool,
367
+ setColor,
368
+ setLineWidth,
369
+ undo,
370
+ redo,
371
+ addBoard,
372
+ clearBoard,
373
+ uploadPDF,
374
+ exportPDF,
375
+ useBoundStore,
376
+ } from 'crystal-draw';
377
+ import 'crystal-draw/style.css';
378
+
379
+ export default function Whiteboard() {
380
+ const tool = useBoundStore((state) => state.tool);
381
+ const boards = useBoundStore((state) => state.boards);
382
+
383
+ async function handleExport() {
384
+ const el = document.getElementById('crystal--draw-container') as HTMLDivElement;
385
+ const file = await exportPDF(el);
386
+ if (file) {
387
+ const a = document.createElement('a');
388
+ a.href = URL.createObjectURL(file);
389
+ a.download = 'drawing.pdf';
390
+ a.click();
391
+ }
392
+ }
393
+
394
+ return (
395
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
396
+ {/* Toolbar */}
397
+ <div style={{ display: 'flex', gap: 8, padding: 8, background: '#1e1e2e' }}>
398
+ <button onClick={() => setTool('pen')} style={{ fontWeight: tool === 'pen' ? 'bold' : 'normal' }}>Pen</button>
399
+ <button onClick={() => setTool('highlighter')} style={{ fontWeight: tool === 'highlighter' ? 'bold' : 'normal' }}>Highlight</button>
400
+ <button onClick={() => setTool('eraser')} style={{ fontWeight: tool === 'eraser' ? 'bold' : 'normal' }}>Eraser</button>
401
+ <button onClick={() => setTool('text')}>Text</button>
402
+ <button onClick={() => setColor('#ff0000')}>Red</button>
403
+ <button onClick={() => setColor('#0000ff')}>Blue</button>
404
+ <button onClick={() => setLineWidth(2, 'PEN')}>Thin</button>
405
+ <button onClick={() => setLineWidth(6, 'PEN')}>Thick</button>
406
+ <button onClick={undo}>Undo</button>
407
+ <button onClick={redo}>Redo</button>
408
+ <button onClick={addBoard}>+ Board</button>
409
+ <button onClick={() => clearBoard(boards.length - 1)}>Clear</button>
410
+ <label style={{ cursor: 'pointer', color: 'white' }}>
411
+ Import PDF
412
+ <input
413
+ type="file"
414
+ accept="application/pdf"
415
+ style={{ display: 'none' }}
416
+ onChange={(e) => e.target.files?.[0] && uploadPDF(e.target.files[0])}
417
+ />
418
+ </label>
419
+ <button onClick={handleExport}>Export PDF</button>
420
+ </div>
421
+
422
+ {/* Canvas */}
423
+ <div style={{ flex: 1 }}>
424
+ <CrystalDraw />
425
+ </div>
426
+ </div>
427
+ );
428
+ }
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Built-in Color Palette
434
+
435
+ The package exports sensible defaults you can reference:
436
+
437
+ **Pen colors:** `#f8f8f8` (default), `#262626` (black), `#ff0000` (red), `#00ff00` (lime), `#0000ff` (blue), `#ffff00` (yellow), and more standard web colors.
438
+
439
+ **Highlighter colors:** Yellow `rgba(225,203,5,0.7)`, Blue `rgba(39,152,177,0.7)`, Purple `rgba(234,151,255,0.7)`, Green `rgba(100,255,75,0.7)`, Orange `rgba(255,172,75,0.7)`.
440
+
441
+ You can use any valid CSS color string with `setColor` / `setHighlighterColor`.
442
+
443
+ ---
444
+
445
+ ## Browser / Environment Requirements
446
+
447
+ - **React** 16+
448
+ - **Modern browser** with Canvas 2D API support
449
+ - **PDF.js** is used internally for PDF import (lazy-loaded, no extra setup needed)
450
+ - **jsPDF** and **html2canvas-pro** are used internally for PDF export (lazy-loaded)
451
+
452
+ ---
453
+
454
+ ## License
455
+
456
+ ISC — © Nakul Soral ([nakulsoral@aesl.in](mailto:nakulsoral@aesl.in))
@@ -0,0 +1,20 @@
1
+ import { LineElement, Point } from "./interface";
2
+ export declare const rotatePoint: (x: number, y: number, cx: number, cy: number, angle: number) => Point;
3
+ export declare function calculateBoundingBox(shape: LineElement, canvas: HTMLCanvasElement): {
4
+ minX: number;
5
+ maxX: number;
6
+ minY: number;
7
+ maxY: number;
8
+ };
9
+ export declare const isShapeSelected: (shape: LineElement, point: Point, canvas: HTMLCanvasElement) => boolean;
10
+ export declare function normalizeCoordinates(start: Point, current: Point): {
11
+ topLeft: {
12
+ x: number;
13
+ y: number;
14
+ };
15
+ dimensions: {
16
+ width: number;
17
+ height: number;
18
+ };
19
+ };
20
+ export declare function getHandleUnderCursor(position: Point, shape: LineElement, canvas: HTMLCanvasElement): "top-left" | "top-right" | "bottom-left" | "bottom-right" | null;
@@ -0,0 +1,148 @@
1
+ const padding = 5;
2
+ export const rotatePoint = (x, y, cx, cy, angle) => {
3
+ return {
4
+ x: (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
5
+ y: (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
6
+ };
7
+ };
8
+ export function calculateBoundingBox(shape, canvas) {
9
+ let minX = Infinity;
10
+ let maxX = -Infinity;
11
+ let minY = Infinity;
12
+ let maxY = -Infinity;
13
+ if (shape.type === 'text' && shape.text) {
14
+ const startPoint = shape.points[0];
15
+ const fontSizematch = shape.font?.match(/(\d+)px/);
16
+ const fontSize = fontSizematch ? parseInt(fontSizematch[1], 10) : 20;
17
+ let widthInPixels = 0;
18
+ let heightInPixels = 0;
19
+ const ctx = canvas.getContext('2d');
20
+ if (ctx) {
21
+ ctx.save();
22
+ if (shape.font)
23
+ ctx.font = shape.font;
24
+ const maxWidth = 400;
25
+ const rawLines = shape.text.split('\n');
26
+ const wrappedLines = [];
27
+ rawLines.forEach((line) => {
28
+ if (ctx.measureText(line).width <= maxWidth) {
29
+ wrappedLines.push(line);
30
+ }
31
+ else {
32
+ const words = line.split(/(\s+)/);
33
+ let currentLine = '';
34
+ words.forEach((word) => {
35
+ const testLine = currentLine + word;
36
+ if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
37
+ wrappedLines.push(currentLine);
38
+ currentLine = word;
39
+ }
40
+ else {
41
+ currentLine = testLine;
42
+ }
43
+ });
44
+ if (currentLine)
45
+ wrappedLines.push(currentLine);
46
+ }
47
+ });
48
+ const maxLineWidth = wrappedLines.length > 0
49
+ ? Math.max(...wrappedLines.map(l => ctx.measureText(l).width))
50
+ : 0;
51
+ widthInPixels = maxLineWidth;
52
+ heightInPixels = wrappedLines.length * (fontSize * 1.2);
53
+ ctx.restore();
54
+ }
55
+ else {
56
+ const lines = shape.text.split('\n');
57
+ const maxLineLength = Math.max(...lines.map(l => l.length));
58
+ widthInPixels = maxLineLength * fontSize * 0.6;
59
+ heightInPixels = lines.length * fontSize * 1.2;
60
+ }
61
+ return {
62
+ minX: startPoint.x + (4 / canvas.width),
63
+ maxX: startPoint.x + (4 + widthInPixels) / canvas.width,
64
+ minY: startPoint.y + (2 / canvas.height),
65
+ maxY: startPoint.y + (2 + heightInPixels) / canvas.height
66
+ };
67
+ }
68
+ shape.points.forEach((point) => {
69
+ if (point.x < minX)
70
+ minX = point.x;
71
+ if (point.x > maxX)
72
+ maxX = point.x;
73
+ if (point.y < minY)
74
+ minY = point.y;
75
+ if (point.y > maxY)
76
+ maxY = point.y;
77
+ });
78
+ return {
79
+ minX,
80
+ maxX,
81
+ minY,
82
+ maxY
83
+ };
84
+ }
85
+ export const isShapeSelected = (shape, point, canvas) => {
86
+ const boundingBox = calculateBoundingBox(shape, canvas);
87
+ const width = boundingBox.maxX - boundingBox.minX;
88
+ const height = boundingBox.maxY - boundingBox.minY;
89
+ const centerX = (boundingBox.minX + width / 2) * canvas.width;
90
+ const centerY = (boundingBox.minY + height / 2) * canvas.height;
91
+ // Rotate the point in the opposite direction of the shape's rotation
92
+ const rotatedPoint = rotatePoint(point.x * canvas.width, point.y * canvas.height, centerX, centerY, 0);
93
+ return (rotatedPoint.x >= boundingBox.minX * canvas.width - padding &&
94
+ rotatedPoint.x <= boundingBox.maxX * canvas.width + padding &&
95
+ rotatedPoint.y >= boundingBox.minY * canvas.height - padding &&
96
+ rotatedPoint.y <= boundingBox.maxY * canvas.height + padding);
97
+ };
98
+ export function normalizeCoordinates(start, current) {
99
+ return {
100
+ topLeft: {
101
+ x: Math.min(start.x, current.x),
102
+ y: Math.min(start.y, current.y)
103
+ },
104
+ dimensions: {
105
+ width: current.x - start.x,
106
+ height: current.y - start.y
107
+ }
108
+ };
109
+ }
110
+ export function getHandleUnderCursor(position, shape, canvas) {
111
+ position = { x: position.x * canvas.width, y: position.y * canvas.height };
112
+ const startPoint = shape.points[0];
113
+ const endPoint = shape.points[shape.points.length - 1];
114
+ const { dimensions } = normalizeCoordinates(startPoint, endPoint);
115
+ const centerX = (startPoint.x + dimensions.width / 2) * canvas.width;
116
+ const centerY = (startPoint.y + dimensions.height / 2) * canvas.height;
117
+ const handleSize = 8;
118
+ const topLeft = rotatePoint(startPoint.x * canvas.width, startPoint.y * canvas.height, centerX, centerY, 0);
119
+ const topRight = rotatePoint((startPoint.x + dimensions.width) * canvas.width, startPoint.y * canvas.height, centerX, centerY, 0);
120
+ const bottomLeft = rotatePoint(startPoint.x * canvas.width, (startPoint.y + dimensions.height) * canvas.height, centerX, centerY, 0);
121
+ const bottomRight = rotatePoint((startPoint.x + dimensions.width) * canvas.width, (startPoint.y + dimensions.height) * canvas.height, centerX, centerY, 0);
122
+ if (position.x >= topLeft.x - handleSize &&
123
+ position.x <= topLeft.x + handleSize &&
124
+ position.y >= topLeft.y - handleSize &&
125
+ position.y <= topLeft.y + handleSize) {
126
+ return 'top-left';
127
+ }
128
+ if (position.x >= topRight.x - handleSize &&
129
+ position.x <= topRight.x + handleSize &&
130
+ position.y >= topRight.y - handleSize &&
131
+ position.y <= topRight.y + handleSize) {
132
+ return 'top-right';
133
+ }
134
+ if (position.x >= bottomLeft.x - handleSize &&
135
+ position.x <= bottomLeft.x + handleSize &&
136
+ position.y >= bottomLeft.y - handleSize &&
137
+ position.y <= bottomLeft.y + handleSize) {
138
+ return 'bottom-left';
139
+ }
140
+ if (position.x >= bottomRight.x - handleSize &&
141
+ position.x <= bottomRight.x + handleSize &&
142
+ position.y >= bottomRight.y - handleSize &&
143
+ position.y <= bottomRight.y + handleSize) {
144
+ return 'bottom-right';
145
+ }
146
+ return null;
147
+ }
148
+ //# sourceMappingURL=TwoDshapes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TwoDshapes.js","sourceRoot":"","sources":["../src/TwoDshapes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="#000" stroke="#fff"
2
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3
+ <path d="m9 11-6 6v3h9l3-3" />
4
+ <path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" />
5
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="#000" stroke="#fff"
2
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3
+ <path
4
+ d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
5
+ <path d="m15 5 4 4" />
6
+ </svg>