react-board-drawing-hook 1.0.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1505 @@
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { io } from 'socket.io-client';
3
+
4
+ // src/hooks/useBoardDrawing.ts
5
+
6
+ // src/types/board.ts
7
+ var CANVAS_WIDTH = 1600;
8
+ var CANVAS_HEIGHT = 900;
9
+ function clampObjectToCanvas(obj) {
10
+ const result = { ...obj };
11
+ result.width = Math.max(result.width, 10);
12
+ result.height = Math.max(result.height, 10);
13
+ result.x = Math.max(0, Math.min(CANVAS_WIDTH - result.width, result.x));
14
+ result.y = Math.max(0, Math.min(CANVAS_HEIGHT - result.height, result.y));
15
+ if (obj.type === "line" && obj.x2 !== void 0 && obj.y2 !== void 0) {
16
+ result.x2 = Math.max(0, Math.min(CANVAS_WIDTH, obj.x2));
17
+ result.y2 = Math.max(0, Math.min(CANVAS_HEIGHT, obj.y2));
18
+ }
19
+ if (obj.type === "pencil" && obj.points) {
20
+ result.points = obj.points.map((point) => ({
21
+ x: Math.max(0, Math.min(CANVAS_WIDTH, point.x)),
22
+ y: Math.max(0, Math.min(CANVAS_HEIGHT, point.y))
23
+ }));
24
+ }
25
+ return result;
26
+ }
27
+ function canEditObject(obj, userId) {
28
+ return obj.locked_by === null || obj.locked_by === userId;
29
+ }
30
+ function generateObjectId() {
31
+ return `obj_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
32
+ }
33
+ function rotatePoint(x, y, cx, cy, angle) {
34
+ const rad = angle * Math.PI / 180;
35
+ const cos = Math.cos(rad);
36
+ const sin = Math.sin(rad);
37
+ const dx = x - cx;
38
+ const dy = y - cy;
39
+ return {
40
+ x: cx + (dx * cos - dy * sin),
41
+ y: cy + (dx * sin + dy * cos)
42
+ };
43
+ }
44
+ function resizeImageWithAspectRatio(obj, handle, deltaX, deltaY, original) {
45
+ const result = { ...obj };
46
+ const aspectRatio = original.aspectRatio || original.width / original.height;
47
+ switch (handle) {
48
+ case "se":
49
+ result.width = Math.max(10, original.width + deltaX);
50
+ result.height = result.width / aspectRatio;
51
+ break;
52
+ case "sw":
53
+ result.width = Math.max(10, original.width - deltaX);
54
+ result.height = result.width / aspectRatio;
55
+ result.x = original.x + original.width - result.width;
56
+ break;
57
+ case "ne":
58
+ result.width = Math.max(10, original.width + deltaX);
59
+ result.height = result.width / aspectRatio;
60
+ result.y = original.y + original.height - result.height;
61
+ break;
62
+ case "nw":
63
+ result.width = Math.max(10, original.width - deltaX);
64
+ result.height = result.width / aspectRatio;
65
+ result.x = original.x + original.width - result.width;
66
+ result.y = original.y + original.height - result.height;
67
+ break;
68
+ case "e":
69
+ result.width = Math.max(10, original.width + deltaX);
70
+ result.height = result.width / aspectRatio;
71
+ break;
72
+ case "w":
73
+ result.width = Math.max(10, original.width - deltaX);
74
+ result.height = result.width / aspectRatio;
75
+ result.x = original.x + original.width - result.width;
76
+ break;
77
+ case "s":
78
+ result.height = Math.max(10, original.height + deltaY);
79
+ result.width = result.height * aspectRatio;
80
+ break;
81
+ case "n":
82
+ result.height = Math.max(10, original.height - deltaY);
83
+ result.width = result.height * aspectRatio;
84
+ result.y = original.y + original.height - result.height;
85
+ break;
86
+ }
87
+ return clampObjectToCanvas(result);
88
+ }
89
+ function resizeObjectFree(obj, handle, deltaX, deltaY, original) {
90
+ const result = { ...obj };
91
+ const originalLeft = original.x;
92
+ const originalTop = original.y;
93
+ const originalRight = original.x + original.width;
94
+ const originalBottom = original.y + original.height;
95
+ let newLeft = originalLeft;
96
+ let newTop = originalTop;
97
+ let newRight = originalRight;
98
+ let newBottom = originalBottom;
99
+ switch (handle) {
100
+ case "nw":
101
+ newLeft = originalLeft + deltaX;
102
+ newTop = originalTop + deltaY;
103
+ break;
104
+ case "n":
105
+ newTop = originalTop + deltaY;
106
+ break;
107
+ case "ne":
108
+ newRight = originalRight + deltaX;
109
+ newTop = originalTop + deltaY;
110
+ break;
111
+ case "e":
112
+ newRight = originalRight + deltaX;
113
+ break;
114
+ case "se":
115
+ newRight = originalRight + deltaX;
116
+ newBottom = originalBottom + deltaY;
117
+ break;
118
+ case "s":
119
+ newBottom = originalBottom + deltaY;
120
+ break;
121
+ case "sw":
122
+ newLeft = originalLeft + deltaX;
123
+ newBottom = originalBottom + deltaY;
124
+ break;
125
+ case "w":
126
+ newLeft = originalLeft + deltaX;
127
+ break;
128
+ }
129
+ const finalLeft = Math.min(newLeft, newRight);
130
+ const finalRight = Math.max(newLeft, newRight);
131
+ const finalTop = Math.min(newTop, newBottom);
132
+ const finalBottom = Math.max(newTop, newBottom);
133
+ const finalWidth = finalRight - finalLeft;
134
+ const finalHeight = finalBottom - finalTop;
135
+ const minSize = 10;
136
+ if (finalWidth < minSize) {
137
+ const center = (finalLeft + finalRight) / 2;
138
+ result.x = center - minSize / 2;
139
+ result.width = minSize;
140
+ } else {
141
+ result.x = finalLeft;
142
+ result.width = finalWidth;
143
+ }
144
+ if (finalHeight < minSize) {
145
+ const center = (finalTop + finalBottom) / 2;
146
+ result.y = center - minSize / 2;
147
+ result.height = minSize;
148
+ } else {
149
+ result.y = finalTop;
150
+ result.height = finalHeight;
151
+ }
152
+ return clampObjectToCanvas(result);
153
+ }
154
+ function resizeLineObject(obj, handle, deltaX, deltaY, original) {
155
+ const result = { ...obj };
156
+ const x1 = original.x;
157
+ const y1 = original.y;
158
+ const x2 = original.x2 ?? original.x + original.width;
159
+ const y2 = original.y2 ?? original.y + original.height;
160
+ let newX1 = x1;
161
+ let newY1 = y1;
162
+ let newX2 = x2;
163
+ let newY2 = y2;
164
+ switch (handle) {
165
+ case "nw":
166
+ case "w":
167
+ case "sw":
168
+ newX1 = x1 + deltaX;
169
+ newY1 = y1 + deltaY;
170
+ break;
171
+ case "ne":
172
+ case "e":
173
+ case "se":
174
+ newX2 = x2 + deltaX;
175
+ newY2 = y2 + deltaY;
176
+ break;
177
+ case "n":
178
+ newY1 = y1 + deltaY;
179
+ break;
180
+ case "s":
181
+ newY2 = y2 + deltaY;
182
+ break;
183
+ }
184
+ newX1 = Math.max(0, Math.min(CANVAS_WIDTH, newX1));
185
+ newY1 = Math.max(0, Math.min(CANVAS_HEIGHT, newY1));
186
+ newX2 = Math.max(0, Math.min(CANVAS_WIDTH, newX2));
187
+ newY2 = Math.max(0, Math.min(CANVAS_HEIGHT, newY2));
188
+ result.x = newX1;
189
+ result.y = newY1;
190
+ result.x2 = newX2;
191
+ result.y2 = newY2;
192
+ result.width = Math.abs(newX2 - newX1);
193
+ result.height = Math.abs(newY2 - newY1);
194
+ return clampObjectToCanvas(result);
195
+ }
196
+ function getObjectBoundingBox(obj) {
197
+ let minX = obj.x;
198
+ let minY = obj.y;
199
+ let maxX = obj.x + obj.width;
200
+ let maxY = obj.y + obj.height;
201
+ if (obj.type === "line" && obj.x2 !== void 0 && obj.y2 !== void 0) {
202
+ minX = Math.min(obj.x, obj.x2);
203
+ minY = Math.min(obj.y, obj.y2);
204
+ maxX = Math.max(obj.x, obj.x2);
205
+ maxY = Math.max(obj.y, obj.y2);
206
+ } else if (obj.type === "pencil" && obj.points && obj.points.length > 0) {
207
+ minX = Math.min(...obj.points.map((p) => p.x));
208
+ minY = Math.min(...obj.points.map((p) => p.y));
209
+ maxX = Math.max(...obj.points.map((p) => p.x));
210
+ maxY = Math.max(...obj.points.map((p) => p.y));
211
+ }
212
+ return { minX, minY, maxX, maxY };
213
+ }
214
+ function isPointInRotatedRectangle(x, y, obj) {
215
+ if (obj.rotation === 0) {
216
+ return x >= obj.x && x <= obj.x + obj.width && y >= obj.y && y <= obj.y + obj.height;
217
+ }
218
+ const centerX = obj.x + obj.width / 2;
219
+ const centerY = obj.y + obj.height / 2;
220
+ const rad = -obj.rotation * Math.PI / 180;
221
+ const cos = Math.cos(rad);
222
+ const sin = Math.sin(rad);
223
+ const dx = x - centerX;
224
+ const dy = y - centerY;
225
+ const rotatedX = dx * cos - dy * sin;
226
+ const rotatedY = dx * sin + dy * cos;
227
+ return Math.abs(rotatedX) <= obj.width / 2 && Math.abs(rotatedY) <= obj.height / 2;
228
+ }
229
+ function distanceToLine(px, py, x1, y1, x2, y2) {
230
+ const dx = x2 - x1;
231
+ const dy = y2 - y1;
232
+ const lengthSq = dx * dx + dy * dy;
233
+ if (lengthSq === 0) {
234
+ return Math.hypot(px - x1, py - y1);
235
+ }
236
+ let t = ((px - x1) * dx + (py - y1) * dy) / lengthSq;
237
+ t = Math.max(0, Math.min(1, t));
238
+ const projX = x1 + t * dx;
239
+ const projY = y1 + t * dy;
240
+ return Math.hypot(px - projX, py - projY);
241
+ }
242
+ function isPointInObject(x, y, obj, userId) {
243
+ if (!canEditObject(obj, userId))
244
+ return false;
245
+ if (obj.type === "line" && obj.x2 !== void 0 && obj.y2 !== void 0) {
246
+ const distance = distanceToLine(x, y, obj.x, obj.y, obj.x2, obj.y2);
247
+ return distance < (obj.strokeWidth || 2) + 5;
248
+ }
249
+ if (obj.type === "pencil" && obj.points) {
250
+ for (let i = 0; i < obj.points.length - 1; i++) {
251
+ const distance = distanceToLine(
252
+ x,
253
+ y,
254
+ obj.points[i].x,
255
+ obj.points[i].y,
256
+ obj.points[i + 1].x,
257
+ obj.points[i + 1].y
258
+ );
259
+ if (distance < (obj.strokeWidth || 2) + 5)
260
+ return true;
261
+ }
262
+ return false;
263
+ }
264
+ return isPointInRotatedRectangle(x, y, obj);
265
+ }
266
+
267
+ // src/hooks/useBoardDrawing.ts
268
+ var useBoardDrawing = ({
269
+ boardId,
270
+ userId,
271
+ userName,
272
+ canvasRef,
273
+ webSocketService,
274
+ onObjectLocked,
275
+ onObjectUnlocked,
276
+ onUserJoined,
277
+ onUserLeft,
278
+ config = {}
279
+ }) => {
280
+ const {
281
+ enableKeyboardShortcuts = true,
282
+ enableGlobalListeners = true,
283
+ maxImageSize = 400,
284
+ defaultColors = {}
285
+ } = config;
286
+ const {
287
+ stroke: defaultStroke = "#000000",
288
+ fill: defaultFill = "#4F46E5",
289
+ lineWidth: defaultLineWidth = 3,
290
+ fontSize: defaultFontSize = 24
291
+ } = defaultColors;
292
+ const [tool, setTool] = useState("select");
293
+ const [color, setColor] = useState(defaultStroke);
294
+ const [fillColor, setFillColor] = useState(defaultFill);
295
+ const [lineWidth, setLineWidth] = useState(defaultLineWidth);
296
+ const [fontSize, setFontSize] = useState(defaultFontSize);
297
+ const [isDrawing, setIsDrawing] = useState(false);
298
+ const [selectedObjectId, setSelectedObjectId] = useState(null);
299
+ const [interactionMode, setInteractionMode] = useState("none");
300
+ const [activeResizeHandle, setActiveResizeHandle] = useState(null);
301
+ const [objects, setObjects] = useState([]);
302
+ const [usersOnline, setUsersOnline] = useState([]);
303
+ const [isConnected, setIsConnected] = useState(false);
304
+ const [editingTextId, setEditingTextId] = useState(null);
305
+ const drawingStartRef = useRef(null);
306
+ const currentPointsRef = useRef([]);
307
+ const tempObjectRef = useRef(null);
308
+ const originalObjectStateRef = useRef(null);
309
+ const rotationStartRef = useRef(0);
310
+ const imageCacheRef = useRef(/* @__PURE__ */ new Map());
311
+ const getCanvasCoordinates = useCallback(
312
+ (clientX, clientY) => {
313
+ if (!canvasRef.current)
314
+ return { x: 0, y: 0 };
315
+ const canvas = canvasRef.current;
316
+ const rect = canvas.getBoundingClientRect();
317
+ const scaleX = canvas.width / rect.width;
318
+ const scaleY = canvas.height / rect.height;
319
+ return {
320
+ x: (clientX - rect.left) * scaleX,
321
+ y: (clientY - rect.top) * scaleY
322
+ };
323
+ },
324
+ [canvasRef]
325
+ );
326
+ const getObjectCenter = useCallback((obj) => {
327
+ return {
328
+ x: obj.x + obj.width / 2,
329
+ y: obj.y + obj.height / 2
330
+ };
331
+ }, []);
332
+ const findObjectAtPoint = useCallback(
333
+ (x, y) => {
334
+ for (let i = objects.length - 1; i >= 0; i--) {
335
+ const obj = objects[i];
336
+ if (isPointInObject(x, y, obj, userId)) {
337
+ return obj;
338
+ }
339
+ }
340
+ return null;
341
+ },
342
+ [objects, userId]
343
+ );
344
+ const findHandleAtPoint = useCallback(
345
+ (x, y, obj) => {
346
+ if (obj.type === "pencil")
347
+ return null;
348
+ const centerX = obj.x + obj.width / 2;
349
+ const centerY = obj.y + obj.height / 2;
350
+ const rad = -obj.rotation * Math.PI / 180;
351
+ const cos = Math.cos(rad);
352
+ const sin = Math.sin(rad);
353
+ const dx = x - centerX;
354
+ const dy = y - centerY;
355
+ const localX = centerX + (dx * cos - dy * sin);
356
+ const localY = centerY + (dx * sin + dy * cos);
357
+ const rotationHandle = {
358
+ x: centerX,
359
+ y: centerY - obj.height / 2 - 35
360
+ };
361
+ const dxRot = localX - rotationHandle.x;
362
+ const dyRot = localY - rotationHandle.y;
363
+ if (Math.hypot(dxRot, dyRot) <= 12) {
364
+ return "rotate";
365
+ }
366
+ const handles = {
367
+ nw: { x: obj.x, y: obj.y },
368
+ n: { x: obj.x + obj.width / 2, y: obj.y },
369
+ ne: { x: obj.x + obj.width, y: obj.y },
370
+ e: { x: obj.x + obj.width, y: obj.y + obj.height / 2 },
371
+ se: { x: obj.x + obj.width, y: obj.y + obj.height },
372
+ s: { x: obj.x + obj.width / 2, y: obj.y + obj.height },
373
+ sw: { x: obj.x, y: obj.y + obj.height },
374
+ w: { x: obj.x, y: obj.y + obj.height / 2 }
375
+ };
376
+ for (const [handle, point] of Object.entries(handles)) {
377
+ const hDx = localX - point.x;
378
+ const hDy = localY - point.y;
379
+ if (Math.hypot(hDx, hDy) <= 8) {
380
+ return handle;
381
+ }
382
+ }
383
+ return null;
384
+ },
385
+ []
386
+ );
387
+ useEffect(() => {
388
+ const handlers = {
389
+ onConnectionChange: setIsConnected,
390
+ onObjectLocked: (objectId, lockedBy, lockedByName) => {
391
+ setObjects(
392
+ (prev) => prev.map(
393
+ (obj) => obj.id === objectId ? { ...obj, locked_by: lockedBy, locked_by_name: lockedByName } : obj
394
+ )
395
+ );
396
+ onObjectLocked?.(objectId, lockedBy, lockedByName);
397
+ },
398
+ onObjectUnlocked: (objectId) => {
399
+ setObjects(
400
+ (prev) => prev.map(
401
+ (obj) => obj.id === objectId ? { ...obj, locked_by: null, locked_by_name: null } : obj
402
+ )
403
+ );
404
+ onObjectUnlocked?.(objectId);
405
+ },
406
+ onObjectCreated: (object) => {
407
+ setObjects((prev) => {
408
+ if (prev.some((o) => o.id === object.id))
409
+ return prev;
410
+ return [...prev, object];
411
+ });
412
+ },
413
+ onObjectUpdated: (object) => {
414
+ setObjects(
415
+ (prev) => prev.map((obj) => obj.id === object.id ? object : obj)
416
+ );
417
+ },
418
+ onObjectDeleted: (objectId) => {
419
+ setObjects((prev) => prev.filter((obj) => obj.id !== objectId));
420
+ if (selectedObjectId === objectId) {
421
+ setSelectedObjectId(null);
422
+ setInteractionMode("none");
423
+ }
424
+ },
425
+ onUserJoined: (joinedUserId, joinedUserName) => {
426
+ setUsersOnline((prev) => [
427
+ ...prev.filter((u) => u.id !== joinedUserId),
428
+ { id: joinedUserId, name: joinedUserName }
429
+ ]);
430
+ onUserJoined?.(joinedUserId, joinedUserName);
431
+ },
432
+ onUserLeft: (leftUserId, leftUserName) => {
433
+ setUsersOnline((prev) => prev.filter((u) => u.id !== leftUserId));
434
+ onUserLeft?.(leftUserId, leftUserName);
435
+ },
436
+ onError: (error) => {
437
+ console.error("[BoardDrawing]", error);
438
+ }
439
+ };
440
+ webSocketService.setEventHandlers(handlers);
441
+ webSocketService.connect(boardId, userId, userName);
442
+ return () => {
443
+ webSocketService.disconnect();
444
+ };
445
+ }, [
446
+ boardId,
447
+ userId,
448
+ userName,
449
+ webSocketService,
450
+ selectedObjectId,
451
+ onObjectLocked,
452
+ onObjectUnlocked,
453
+ onUserJoined,
454
+ onUserLeft
455
+ ]);
456
+ const drawObject = useCallback(
457
+ (ctx, obj, isSelected) => {
458
+ ctx.save();
459
+ if (obj.rotation !== 0) {
460
+ const centerX = obj.x + obj.width / 2;
461
+ const centerY = obj.y + obj.height / 2;
462
+ ctx.translate(centerX, centerY);
463
+ ctx.rotate(obj.rotation * Math.PI / 180);
464
+ ctx.translate(-centerX, -centerY);
465
+ }
466
+ if (isSelected) {
467
+ ctx.strokeStyle = "#3B82F6";
468
+ ctx.lineWidth = 2;
469
+ ctx.setLineDash([5, 5]);
470
+ ctx.strokeRect(obj.x - 5, obj.y - 5, obj.width + 10, obj.height + 10);
471
+ ctx.setLineDash([]);
472
+ }
473
+ ctx.strokeStyle = obj.strokeColor || obj.color || defaultStroke;
474
+ ctx.fillStyle = obj.fillColor || "transparent";
475
+ ctx.lineWidth = obj.strokeWidth || defaultLineWidth;
476
+ switch (obj.type) {
477
+ case "rectangle":
478
+ if (obj.fillColor)
479
+ ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
480
+ ctx.strokeRect(obj.x, obj.y, obj.width, obj.height);
481
+ break;
482
+ case "circle":
483
+ ctx.beginPath();
484
+ ctx.ellipse(
485
+ obj.x + obj.width / 2,
486
+ obj.y + obj.height / 2,
487
+ obj.width / 2,
488
+ obj.height / 2,
489
+ 0,
490
+ 0,
491
+ Math.PI * 2
492
+ );
493
+ if (obj.fillColor)
494
+ ctx.fill();
495
+ ctx.stroke();
496
+ break;
497
+ case "line":
498
+ if (obj.x2 !== void 0 && obj.y2 !== void 0) {
499
+ ctx.beginPath();
500
+ ctx.moveTo(obj.x, obj.y);
501
+ ctx.lineTo(obj.x2, obj.y2);
502
+ ctx.stroke();
503
+ }
504
+ break;
505
+ case "pencil":
506
+ if (obj.points && obj.points.length > 0) {
507
+ ctx.beginPath();
508
+ ctx.moveTo(obj.points[0].x, obj.points[0].y);
509
+ for (let i = 1; i < obj.points.length; i++) {
510
+ ctx.lineTo(obj.points[i].x, obj.points[i].y);
511
+ }
512
+ ctx.stroke();
513
+ }
514
+ break;
515
+ case "text":
516
+ if (obj.content) {
517
+ ctx.fillStyle = obj.color || defaultStroke;
518
+ ctx.font = `${obj.fontSize || defaultFontSize}px Arial, sans-serif`;
519
+ ctx.textBaseline = "top";
520
+ ctx.fillText(obj.content, obj.x, obj.y);
521
+ }
522
+ break;
523
+ case "image":
524
+ if (obj.imageUrl) {
525
+ const img = imageCacheRef.current.get(obj.imageUrl);
526
+ if (img?.complete) {
527
+ ctx.drawImage(img, obj.x, obj.y, obj.width, obj.height);
528
+ }
529
+ }
530
+ break;
531
+ }
532
+ ctx.restore();
533
+ if (obj.locked_by && obj.locked_by !== userId) {
534
+ ctx.save();
535
+ ctx.font = "12px Arial, sans-serif";
536
+ ctx.fillStyle = "rgba(249, 115, 22, 0.9)";
537
+ const text = `\u{1F512} ${obj.locked_by_name || "Locked"}`;
538
+ const metrics = ctx.measureText(text);
539
+ const padding = 8;
540
+ const boxWidth = metrics.width + padding * 2;
541
+ const boxHeight = 24;
542
+ const centerX = obj.x + obj.width / 2;
543
+ ctx.roundRect(
544
+ centerX - boxWidth / 2,
545
+ obj.y - boxHeight - 5,
546
+ boxWidth,
547
+ boxHeight,
548
+ 4
549
+ );
550
+ ctx.fill();
551
+ ctx.fillStyle = "white";
552
+ ctx.font = "12px Arial, sans-serif";
553
+ ctx.textAlign = "center";
554
+ ctx.textBaseline = "middle";
555
+ ctx.fillText(text, centerX, obj.y - boxHeight / 2 - 5);
556
+ ctx.restore();
557
+ }
558
+ },
559
+ [defaultStroke, defaultLineWidth, defaultFontSize, userId]
560
+ );
561
+ const drawResizeHandles = useCallback(
562
+ (ctx, obj) => {
563
+ if (obj.type === "pencil")
564
+ return;
565
+ ctx.save();
566
+ if (obj.rotation !== 0) {
567
+ const centerX = obj.x + obj.width / 2;
568
+ const centerY = obj.y + obj.height / 2;
569
+ ctx.translate(centerX, centerY);
570
+ ctx.rotate(obj.rotation * Math.PI / 180);
571
+ ctx.translate(-centerX, -centerY);
572
+ }
573
+ ctx.fillStyle = "#3B82F6";
574
+ ctx.strokeStyle = "#FFFFFF";
575
+ ctx.lineWidth = 2;
576
+ const handles = [
577
+ { x: obj.x - 4, y: obj.y - 4 },
578
+ { x: obj.x + obj.width / 2 - 4, y: obj.y - 4 },
579
+ { x: obj.x + obj.width - 4, y: obj.y - 4 },
580
+ { x: obj.x + obj.width - 4, y: obj.y + obj.height / 2 - 4 },
581
+ { x: obj.x + obj.width - 4, y: obj.y + obj.height - 4 },
582
+ { x: obj.x + obj.width / 2 - 4, y: obj.y + obj.height - 4 },
583
+ { x: obj.x - 4, y: obj.y + obj.height - 4 },
584
+ { x: obj.x - 4, y: obj.y + obj.height / 2 - 4 }
585
+ ];
586
+ handles.forEach((handle) => {
587
+ ctx.beginPath();
588
+ ctx.rect(handle.x, handle.y, 8, 8);
589
+ ctx.fill();
590
+ ctx.stroke();
591
+ });
592
+ ctx.restore();
593
+ },
594
+ []
595
+ );
596
+ const drawRotationHandle = useCallback(
597
+ (ctx, obj) => {
598
+ if (obj.type === "pencil")
599
+ return;
600
+ const centerX = obj.x + obj.width / 2;
601
+ ctx.save();
602
+ if (obj.rotation !== 0) {
603
+ const centerY = obj.y + obj.height / 2;
604
+ ctx.translate(centerX, centerY);
605
+ ctx.rotate(obj.rotation * Math.PI / 180);
606
+ ctx.translate(-centerX, -centerY);
607
+ }
608
+ const handleX = centerX;
609
+ const handleY = obj.y - 35;
610
+ ctx.strokeStyle = "#3B82F6";
611
+ ctx.lineWidth = 1;
612
+ ctx.setLineDash([2, 2]);
613
+ ctx.beginPath();
614
+ ctx.moveTo(centerX, obj.y);
615
+ ctx.lineTo(handleX, handleY);
616
+ ctx.stroke();
617
+ ctx.setLineDash([]);
618
+ ctx.fillStyle = "#3B82F6";
619
+ ctx.strokeStyle = "#FFFFFF";
620
+ ctx.lineWidth = 2;
621
+ ctx.beginPath();
622
+ ctx.arc(handleX, handleY, 8, 0, Math.PI * 2);
623
+ ctx.fill();
624
+ ctx.stroke();
625
+ ctx.fillStyle = "#FFFFFF";
626
+ ctx.font = "12px Arial";
627
+ ctx.textAlign = "center";
628
+ ctx.textBaseline = "middle";
629
+ ctx.fillText("\u21BB", handleX, handleY);
630
+ ctx.restore();
631
+ },
632
+ []
633
+ );
634
+ const redrawCanvas = useCallback(() => {
635
+ const canvas = canvasRef.current;
636
+ if (!canvas)
637
+ return;
638
+ const ctx = canvas.getContext("2d");
639
+ if (!ctx)
640
+ return;
641
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
642
+ objects.forEach((obj) => {
643
+ if (obj.id === editingTextId)
644
+ return;
645
+ const isSelected = obj.id === selectedObjectId;
646
+ drawObject(ctx, obj, isSelected);
647
+ });
648
+ if (tempObjectRef.current) {
649
+ drawObject(ctx, tempObjectRef.current, false);
650
+ }
651
+ if (selectedObjectId) {
652
+ const obj = objects.find((o) => o.id === selectedObjectId);
653
+ if (obj && canEditObject(obj, userId) && obj.type !== "pencil") {
654
+ drawResizeHandles(ctx, obj);
655
+ drawRotationHandle(ctx, obj);
656
+ }
657
+ }
658
+ }, [
659
+ canvasRef,
660
+ objects,
661
+ selectedObjectId,
662
+ editingTextId,
663
+ userId,
664
+ drawObject,
665
+ drawResizeHandles,
666
+ drawRotationHandle
667
+ ]);
668
+ useEffect(() => {
669
+ redrawCanvas();
670
+ }, [redrawCanvas]);
671
+ const handleMouseDown = useCallback(
672
+ (e) => {
673
+ e.preventDefault();
674
+ const { x, y } = getCanvasCoordinates(e.clientX, e.clientY);
675
+ if (tool === "select") {
676
+ if (selectedObjectId) {
677
+ const selectedObj = objects.find((o) => o.id === selectedObjectId);
678
+ if (selectedObj) {
679
+ const handle = findHandleAtPoint(x, y, selectedObj);
680
+ if (handle) {
681
+ if (handle === "rotate") {
682
+ setInteractionMode("rotate");
683
+ setActiveResizeHandle("rotate");
684
+ const center = getObjectCenter(selectedObj);
685
+ rotationStartRef.current = Math.atan2(y - center.y, x - center.x) * 180 / Math.PI - selectedObj.rotation;
686
+ } else {
687
+ setInteractionMode("resize");
688
+ setActiveResizeHandle(handle);
689
+ }
690
+ originalObjectStateRef.current = {
691
+ obj: { ...selectedObj },
692
+ centerX: selectedObj.x + selectedObj.width / 2,
693
+ centerY: selectedObj.y + selectedObj.height / 2
694
+ };
695
+ webSocketService.lockObject(selectedObjectId);
696
+ drawingStartRef.current = { x, y };
697
+ setIsDrawing(true);
698
+ return;
699
+ }
700
+ }
701
+ }
702
+ const object = findObjectAtPoint(x, y);
703
+ if (object) {
704
+ if (selectedObjectId && selectedObjectId !== object.id) {
705
+ webSocketService.unlockObject(selectedObjectId);
706
+ }
707
+ setSelectedObjectId(object.id);
708
+ webSocketService.lockObject(object.id);
709
+ setInteractionMode("move");
710
+ originalObjectStateRef.current = {
711
+ obj: { ...object },
712
+ centerX: object.x + object.width / 2,
713
+ centerY: object.y + object.height / 2
714
+ };
715
+ drawingStartRef.current = { x, y };
716
+ setIsDrawing(true);
717
+ } else {
718
+ if (selectedObjectId) {
719
+ webSocketService.unlockObject(selectedObjectId);
720
+ }
721
+ setSelectedObjectId(null);
722
+ setInteractionMode("none");
723
+ }
724
+ } else if (tool === "eraser") {
725
+ const object = findObjectAtPoint(x, y);
726
+ if (object) {
727
+ webSocketService.deleteObject(object.id);
728
+ }
729
+ } else {
730
+ setInteractionMode("draw");
731
+ drawingStartRef.current = { x, y };
732
+ currentPointsRef.current = [{ x, y }];
733
+ setIsDrawing(true);
734
+ if (tool === "pencil") {
735
+ tempObjectRef.current = {
736
+ id: `temp_${Date.now()}`,
737
+ type: "pencil",
738
+ x,
739
+ y,
740
+ width: 0,
741
+ height: 0,
742
+ rotation: 0,
743
+ points: [{ x, y }],
744
+ strokeColor: color,
745
+ strokeWidth: lineWidth,
746
+ locked_by: null,
747
+ locked_by_name: null
748
+ };
749
+ }
750
+ }
751
+ },
752
+ [
753
+ tool,
754
+ color,
755
+ lineWidth,
756
+ userId,
757
+ selectedObjectId,
758
+ objects,
759
+ getCanvasCoordinates,
760
+ findObjectAtPoint,
761
+ findHandleAtPoint,
762
+ getObjectCenter,
763
+ webSocketService
764
+ ]
765
+ );
766
+ const handleMouseMove = useCallback(
767
+ (e) => {
768
+ const { x, y } = getCanvasCoordinates(e.clientX, e.clientY);
769
+ if (!isDrawing && tool === "select" && canvasRef.current) {
770
+ let cursor = "default";
771
+ if (selectedObjectId) {
772
+ const obj = objects.find((o) => o.id === selectedObjectId);
773
+ if (obj && obj.type !== "pencil") {
774
+ const handle = findHandleAtPoint(x, y, obj);
775
+ if (handle) {
776
+ switch (handle) {
777
+ case "nw":
778
+ case "se":
779
+ cursor = "nwse-resize";
780
+ break;
781
+ case "ne":
782
+ case "sw":
783
+ cursor = "nesw-resize";
784
+ break;
785
+ case "n":
786
+ case "s":
787
+ cursor = "ns-resize";
788
+ break;
789
+ case "e":
790
+ case "w":
791
+ cursor = "ew-resize";
792
+ break;
793
+ case "rotate":
794
+ cursor = "grab";
795
+ break;
796
+ }
797
+ }
798
+ }
799
+ }
800
+ if (cursor === "default") {
801
+ const object = findObjectAtPoint(x, y);
802
+ if (object)
803
+ cursor = "move";
804
+ }
805
+ canvasRef.current.style.cursor = cursor;
806
+ }
807
+ if (!isDrawing)
808
+ return;
809
+ if (interactionMode === "move" && selectedObjectId && originalObjectStateRef.current) {
810
+ const deltaX = x - (drawingStartRef.current?.x || 0);
811
+ const deltaY = y - (drawingStartRef.current?.y || 0);
812
+ setObjects(
813
+ (prev) => prev.map((obj) => {
814
+ if (obj.id === selectedObjectId) {
815
+ const original = originalObjectStateRef.current.obj;
816
+ let updated = {
817
+ ...obj,
818
+ x: original.x + deltaX,
819
+ y: original.y + deltaY
820
+ };
821
+ if (obj.type === "line" && obj.x2 !== void 0 && obj.y2 !== void 0) {
822
+ updated.x2 = original.x2 + deltaX;
823
+ updated.y2 = original.y2 + deltaY;
824
+ }
825
+ if (obj.type === "pencil" && obj.points) {
826
+ updated.points = original.points.map((p) => ({
827
+ x: p.x + deltaX,
828
+ y: p.y + deltaY
829
+ }));
830
+ }
831
+ return clampObjectToCanvas(updated);
832
+ }
833
+ return obj;
834
+ })
835
+ );
836
+ } else if (interactionMode === "rotate" && selectedObjectId && originalObjectStateRef.current) {
837
+ const center = originalObjectStateRef.current;
838
+ const currentAngle = Math.atan2(y - center.centerY, x - center.centerX) * 180 / Math.PI;
839
+ const newRotation = currentAngle - rotationStartRef.current;
840
+ setObjects(
841
+ (prev) => prev.map(
842
+ (obj) => obj.id === selectedObjectId ? { ...obj, rotation: newRotation } : obj
843
+ )
844
+ );
845
+ } else if (interactionMode === "resize" && selectedObjectId && activeResizeHandle && originalObjectStateRef.current) {
846
+ const globalDeltaX = x - (drawingStartRef.current?.x || 0);
847
+ const globalDeltaY = y - (drawingStartRef.current?.y || 0);
848
+ const obj = objects.find((o) => o.id === selectedObjectId);
849
+ if (!obj)
850
+ return;
851
+ const original = originalObjectStateRef.current.obj;
852
+ const rad = -original.rotation * Math.PI / 180;
853
+ const cos = Math.cos(rad);
854
+ const sin = Math.sin(rad);
855
+ const localDeltaX = globalDeltaX * cos - globalDeltaY * sin;
856
+ const localDeltaY = globalDeltaX * sin + globalDeltaY * cos;
857
+ let updated = obj;
858
+ if (obj.type === "line") {
859
+ updated = resizeLineObject(
860
+ obj,
861
+ activeResizeHandle,
862
+ localDeltaX,
863
+ localDeltaY,
864
+ original
865
+ );
866
+ } else if (obj.type === "image") {
867
+ updated = resizeImageWithAspectRatio(
868
+ obj,
869
+ activeResizeHandle,
870
+ localDeltaX,
871
+ localDeltaY,
872
+ original
873
+ );
874
+ } else {
875
+ updated = resizeObjectFree(
876
+ obj,
877
+ activeResizeHandle,
878
+ localDeltaX,
879
+ localDeltaY,
880
+ original
881
+ );
882
+ }
883
+ setObjects(
884
+ (prev) => prev.map((o) => o.id === selectedObjectId ? updated : o)
885
+ );
886
+ } else if (interactionMode === "draw" && drawingStartRef.current) {
887
+ currentPointsRef.current.push({ x, y });
888
+ if (tool === "line") {
889
+ tempObjectRef.current = {
890
+ id: `temp_${Date.now()}`,
891
+ type: "line",
892
+ x: drawingStartRef.current.x,
893
+ y: drawingStartRef.current.y,
894
+ width: Math.abs(x - drawingStartRef.current.x),
895
+ height: Math.abs(y - drawingStartRef.current.y),
896
+ rotation: 0,
897
+ strokeColor: color,
898
+ strokeWidth: lineWidth,
899
+ x2: x,
900
+ y2: y,
901
+ locked_by: null,
902
+ locked_by_name: null
903
+ };
904
+ } else if (tool === "rectangle") {
905
+ tempObjectRef.current = {
906
+ id: `temp_${Date.now()}`,
907
+ type: "rectangle",
908
+ x: Math.min(drawingStartRef.current.x, x),
909
+ y: Math.min(drawingStartRef.current.y, y),
910
+ width: Math.abs(x - drawingStartRef.current.x),
911
+ height: Math.abs(y - drawingStartRef.current.y),
912
+ rotation: 0,
913
+ strokeColor: color,
914
+ fillColor,
915
+ strokeWidth: lineWidth,
916
+ locked_by: null,
917
+ locked_by_name: null
918
+ };
919
+ } else if (tool === "circle") {
920
+ const radius = Math.hypot(
921
+ x - drawingStartRef.current.x,
922
+ y - drawingStartRef.current.y
923
+ );
924
+ tempObjectRef.current = {
925
+ id: `temp_${Date.now()}`,
926
+ type: "circle",
927
+ x: drawingStartRef.current.x - radius,
928
+ y: drawingStartRef.current.y - radius,
929
+ width: radius * 2,
930
+ height: radius * 2,
931
+ rotation: 0,
932
+ strokeColor: color,
933
+ fillColor,
934
+ strokeWidth: lineWidth,
935
+ locked_by: null,
936
+ locked_by_name: null
937
+ };
938
+ } else if (tool === "pencil" && tempObjectRef.current) {
939
+ tempObjectRef.current = {
940
+ ...tempObjectRef.current,
941
+ points: [...currentPointsRef.current]
942
+ };
943
+ }
944
+ redrawCanvas();
945
+ }
946
+ },
947
+ [
948
+ isDrawing,
949
+ tool,
950
+ color,
951
+ fillColor,
952
+ lineWidth,
953
+ canvasRef,
954
+ selectedObjectId,
955
+ interactionMode,
956
+ activeResizeHandle,
957
+ objects,
958
+ getCanvasCoordinates,
959
+ findObjectAtPoint,
960
+ findHandleAtPoint,
961
+ redrawCanvas,
962
+ webSocketService
963
+ ]
964
+ );
965
+ const handleMouseUp = useCallback(
966
+ (e) => {
967
+ if (!isDrawing)
968
+ return;
969
+ if (interactionMode === "draw" && drawingStartRef.current) {
970
+ const { x, y } = getCanvasCoordinates(e.clientX, e.clientY);
971
+ const startX = drawingStartRef.current.x;
972
+ const startY = drawingStartRef.current.y;
973
+ const newId = generateObjectId();
974
+ let newObject = null;
975
+ switch (tool) {
976
+ case "line":
977
+ newObject = clampObjectToCanvas({
978
+ id: newId,
979
+ type: "line",
980
+ x: startX,
981
+ y: startY,
982
+ width: Math.abs(x - startX),
983
+ height: Math.abs(y - startY),
984
+ rotation: 0,
985
+ strokeColor: color,
986
+ strokeWidth: lineWidth,
987
+ x2: x,
988
+ y2: y,
989
+ locked_by: null,
990
+ locked_by_name: null
991
+ });
992
+ break;
993
+ case "rectangle":
994
+ newObject = clampObjectToCanvas({
995
+ id: newId,
996
+ type: "rectangle",
997
+ x: Math.min(startX, x),
998
+ y: Math.min(startY, y),
999
+ width: Math.abs(x - startX),
1000
+ height: Math.abs(y - startY),
1001
+ rotation: 0,
1002
+ strokeColor: color,
1003
+ fillColor,
1004
+ strokeWidth: lineWidth,
1005
+ locked_by: null,
1006
+ locked_by_name: null
1007
+ });
1008
+ break;
1009
+ case "circle":
1010
+ const radius = Math.hypot(x - startX, y - startY);
1011
+ newObject = clampObjectToCanvas({
1012
+ id: newId,
1013
+ type: "circle",
1014
+ x: startX - radius,
1015
+ y: startY - radius,
1016
+ width: radius * 2,
1017
+ height: radius * 2,
1018
+ rotation: 0,
1019
+ strokeColor: color,
1020
+ fillColor,
1021
+ strokeWidth: lineWidth,
1022
+ locked_by: null,
1023
+ locked_by_name: null
1024
+ });
1025
+ break;
1026
+ case "pencil":
1027
+ if (currentPointsRef.current.length > 0) {
1028
+ const points = [...currentPointsRef.current];
1029
+ const minX = Math.min(...points.map((p) => p.x));
1030
+ const minY = Math.min(...points.map((p) => p.y));
1031
+ const maxX = Math.max(...points.map((p) => p.x));
1032
+ const maxY = Math.max(...points.map((p) => p.y));
1033
+ newObject = clampObjectToCanvas({
1034
+ id: newId,
1035
+ type: "pencil",
1036
+ x: minX,
1037
+ y: minY,
1038
+ width: Math.max(maxX - minX, 1),
1039
+ height: Math.max(maxY - minY, 1),
1040
+ rotation: 0,
1041
+ points,
1042
+ strokeColor: color,
1043
+ strokeWidth: lineWidth,
1044
+ locked_by: null,
1045
+ locked_by_name: null
1046
+ });
1047
+ }
1048
+ break;
1049
+ case "text":
1050
+ newObject = clampObjectToCanvas({
1051
+ id: newId,
1052
+ type: "text",
1053
+ x: startX,
1054
+ y: startY,
1055
+ width: 200,
1056
+ height: 50,
1057
+ rotation: 0,
1058
+ content: "Double click to edit",
1059
+ fontSize,
1060
+ color,
1061
+ locked_by: null,
1062
+ locked_by_name: null
1063
+ });
1064
+ break;
1065
+ }
1066
+ if (newObject) {
1067
+ setObjects((prev) => [...prev, newObject]);
1068
+ webSocketService.createObject(newObject);
1069
+ }
1070
+ }
1071
+ setIsDrawing(false);
1072
+ setInteractionMode("none");
1073
+ setActiveResizeHandle(null);
1074
+ drawingStartRef.current = null;
1075
+ currentPointsRef.current = [];
1076
+ tempObjectRef.current = null;
1077
+ originalObjectStateRef.current = null;
1078
+ rotationStartRef.current = 0;
1079
+ },
1080
+ [
1081
+ isDrawing,
1082
+ interactionMode,
1083
+ tool,
1084
+ color,
1085
+ fillColor,
1086
+ lineWidth,
1087
+ fontSize,
1088
+ getCanvasCoordinates,
1089
+ webSocketService
1090
+ ]
1091
+ );
1092
+ const handleDoubleClick = useCallback(
1093
+ (e) => {
1094
+ if (tool !== "select")
1095
+ return;
1096
+ const { x, y } = getCanvasCoordinates(e.clientX, e.clientY);
1097
+ const object = findObjectAtPoint(x, y);
1098
+ if (object?.type === "text" && canEditObject(object, userId)) {
1099
+ setEditingTextId(object.id);
1100
+ webSocketService.lockObject(object.id);
1101
+ }
1102
+ },
1103
+ [tool, userId, getCanvasCoordinates, findObjectAtPoint, webSocketService]
1104
+ );
1105
+ useEffect(() => {
1106
+ if (!enableKeyboardShortcuts)
1107
+ return;
1108
+ const handleKeyDown = (e) => {
1109
+ if (editingTextId) {
1110
+ if (e.key === "Escape") {
1111
+ setEditingTextId(null);
1112
+ webSocketService.unlockObject(editingTextId);
1113
+ }
1114
+ return;
1115
+ }
1116
+ if (e.key === "Delete" || e.key === "Backspace") {
1117
+ e.preventDefault();
1118
+ if (selectedObjectId) {
1119
+ webSocketService.deleteObject(selectedObjectId);
1120
+ }
1121
+ }
1122
+ if (e.key === "Escape" && selectedObjectId) {
1123
+ webSocketService.unlockObject(selectedObjectId);
1124
+ setSelectedObjectId(null);
1125
+ setInteractionMode("none");
1126
+ }
1127
+ };
1128
+ window.addEventListener("keydown", handleKeyDown);
1129
+ return () => window.removeEventListener("keydown", handleKeyDown);
1130
+ }, [
1131
+ enableKeyboardShortcuts,
1132
+ editingTextId,
1133
+ selectedObjectId,
1134
+ webSocketService
1135
+ ]);
1136
+ useEffect(() => {
1137
+ if (!enableGlobalListeners)
1138
+ return;
1139
+ const handleBeforeUnload = () => {
1140
+ if (selectedObjectId) {
1141
+ webSocketService.unlockObject(selectedObjectId);
1142
+ }
1143
+ };
1144
+ window.addEventListener("beforeunload", handleBeforeUnload);
1145
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
1146
+ }, [enableGlobalListeners, selectedObjectId, webSocketService]);
1147
+ const deleteSelectedObject = useCallback(() => {
1148
+ if (selectedObjectId) {
1149
+ webSocketService.deleteObject(selectedObjectId);
1150
+ }
1151
+ }, [selectedObjectId, webSocketService]);
1152
+ const addImage = useCallback(
1153
+ (imageUrl) => {
1154
+ const img = new Image();
1155
+ img.crossOrigin = "anonymous";
1156
+ img.onload = () => {
1157
+ let width = img.width;
1158
+ let height = img.height;
1159
+ if (width > maxImageSize || height > maxImageSize) {
1160
+ if (width > height) {
1161
+ width = maxImageSize;
1162
+ height = maxImageSize / img.width * img.height;
1163
+ } else {
1164
+ height = maxImageSize;
1165
+ width = maxImageSize / img.height * img.width;
1166
+ }
1167
+ }
1168
+ const newObject = clampObjectToCanvas({
1169
+ id: generateObjectId(),
1170
+ type: "image",
1171
+ x: CANVAS_WIDTH / 2 - width / 2,
1172
+ y: CANVAS_HEIGHT / 2 - height / 2,
1173
+ width,
1174
+ height,
1175
+ rotation: 0,
1176
+ imageUrl,
1177
+ aspectRatio: img.width / img.height,
1178
+ locked_by: null,
1179
+ locked_by_name: null
1180
+ });
1181
+ imageCacheRef.current.set(imageUrl, img);
1182
+ setObjects((prev) => [...prev, newObject]);
1183
+ webSocketService.createObject(newObject);
1184
+ };
1185
+ img.src = imageUrl;
1186
+ },
1187
+ [maxImageSize, webSocketService]
1188
+ );
1189
+ const updateObjectContent = useCallback(
1190
+ (objectId, content) => {
1191
+ setObjects(
1192
+ (prev) => prev.map((obj) => {
1193
+ if (obj.id === objectId) {
1194
+ const updated = { ...obj, content };
1195
+ webSocketService.updateObject(updated);
1196
+ return updated;
1197
+ }
1198
+ return obj;
1199
+ })
1200
+ );
1201
+ },
1202
+ [webSocketService]
1203
+ );
1204
+ const clearCanvas = useCallback(() => {
1205
+ objects.forEach((obj) => {
1206
+ webSocketService.deleteObject(obj.id);
1207
+ });
1208
+ setObjects([]);
1209
+ setSelectedObjectId(null);
1210
+ }, [objects, webSocketService]);
1211
+ const exportAsJSON = useCallback(() => {
1212
+ return JSON.stringify(objects, null, 2);
1213
+ }, [objects]);
1214
+ const importFromJSON = useCallback(
1215
+ (json) => {
1216
+ try {
1217
+ const imported = JSON.parse(json);
1218
+ imported.forEach((obj) => {
1219
+ webSocketService.createObject(obj);
1220
+ });
1221
+ setObjects((prev) => [...prev, ...imported]);
1222
+ } catch (error) {
1223
+ console.error("Failed to import objects:", error);
1224
+ }
1225
+ },
1226
+ [webSocketService]
1227
+ );
1228
+ return {
1229
+ // State
1230
+ tool,
1231
+ color,
1232
+ fillColor,
1233
+ lineWidth,
1234
+ fontSize,
1235
+ isDrawing,
1236
+ selectedObjectId,
1237
+ interactionMode,
1238
+ objects,
1239
+ usersOnline,
1240
+ isConnected,
1241
+ editingTextId,
1242
+ // Setters
1243
+ setTool,
1244
+ setColor,
1245
+ setFillColor,
1246
+ setLineWidth,
1247
+ setFontSize,
1248
+ setSelectedObjectId,
1249
+ setEditingTextId,
1250
+ // Handlers
1251
+ handleMouseDown,
1252
+ handleMouseMove,
1253
+ handleMouseUp,
1254
+ handleDoubleClick,
1255
+ // Methods
1256
+ deleteSelectedObject,
1257
+ addImage,
1258
+ updateObjectContent,
1259
+ clearCanvas,
1260
+ exportAsJSON,
1261
+ importFromJSON,
1262
+ // Utils
1263
+ redrawCanvas
1264
+ };
1265
+ };
1266
+ if (typeof CanvasRenderingContext2D !== "undefined") {
1267
+ CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
1268
+ if (w < 2 * r)
1269
+ r = w / 2;
1270
+ if (h < 2 * r)
1271
+ r = h / 2;
1272
+ this.moveTo(x + r, y);
1273
+ this.lineTo(x + w - r, y);
1274
+ this.quadraticCurveTo(x + w, y, x + w, y + r);
1275
+ this.lineTo(x + w, y + h - r);
1276
+ this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
1277
+ this.lineTo(x + r, y + h);
1278
+ this.quadraticCurveTo(x, y + h, x, y + h - r);
1279
+ this.lineTo(x, y + r);
1280
+ this.quadraticCurveTo(x, y, x + r, y);
1281
+ return this;
1282
+ };
1283
+ }
1284
+
1285
+ // src/types/websocket.ts
1286
+ var BaseWebSocketAdapter = class {
1287
+ constructor() {
1288
+ this.handlers = {};
1289
+ this.connected = false;
1290
+ }
1291
+ isConnected() {
1292
+ return this.connected;
1293
+ }
1294
+ setEventHandlers(handlers) {
1295
+ this.handlers = handlers;
1296
+ }
1297
+ handleError(error, context) {
1298
+ console.error("[BoardWebSocket]", error, context);
1299
+ this.handlers.onError?.(error, context);
1300
+ }
1301
+ };
1302
+
1303
+ // src/adapters/native-websocket-adapter.ts
1304
+ var NativeWebSocketAdapter = class extends BaseWebSocketAdapter {
1305
+ constructor(url) {
1306
+ super();
1307
+ this.ws = null;
1308
+ this.reconnectAttempts = 0;
1309
+ this.maxReconnectAttempts = 5;
1310
+ this.reconnectTimeout = 1e3;
1311
+ this.url = url;
1312
+ }
1313
+ connect(boardId, userId, userName) {
1314
+ try {
1315
+ const wsUrl = new URL(this.url);
1316
+ wsUrl.searchParams.append("boardId", String(boardId));
1317
+ wsUrl.searchParams.append("userId", String(userId));
1318
+ wsUrl.searchParams.append("userName", userName);
1319
+ this.ws = new WebSocket(wsUrl.toString());
1320
+ this.ws.onopen = () => {
1321
+ this.connected = true;
1322
+ this.reconnectAttempts = 0;
1323
+ this.handlers.onConnectionChange?.(true);
1324
+ };
1325
+ this.ws.onclose = () => {
1326
+ this.connected = false;
1327
+ this.handlers.onConnectionChange?.(false);
1328
+ this.attemptReconnect(boardId, userId, userName);
1329
+ };
1330
+ this.ws.onerror = () => {
1331
+ this.handleError(new Error("WebSocket error"));
1332
+ };
1333
+ this.ws.onmessage = (event) => {
1334
+ try {
1335
+ const message = JSON.parse(event.data);
1336
+ this.handleMessage(message);
1337
+ } catch (error) {
1338
+ this.handleError(error);
1339
+ }
1340
+ };
1341
+ } catch (error) {
1342
+ this.handleError(error);
1343
+ }
1344
+ }
1345
+ attemptReconnect(boardId, userId, userName) {
1346
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
1347
+ this.handleError(new Error("Max reconnection attempts reached"));
1348
+ return;
1349
+ }
1350
+ this.reconnectAttempts++;
1351
+ setTimeout(() => {
1352
+ this.connect(boardId, userId, userName);
1353
+ }, this.reconnectTimeout * this.reconnectAttempts);
1354
+ }
1355
+ handleMessage(message) {
1356
+ switch (message.type) {
1357
+ case "OBJECT_LOCKED":
1358
+ this.handlers.onObjectLocked?.(
1359
+ message.payload.objectId,
1360
+ message.payload.userId,
1361
+ message.payload.userName
1362
+ );
1363
+ break;
1364
+ case "OBJECT_UNLOCKED":
1365
+ this.handlers.onObjectUnlocked?.(message.payload.objectId);
1366
+ break;
1367
+ case "OBJECT_CREATED":
1368
+ this.handlers.onObjectCreated?.(message.payload);
1369
+ break;
1370
+ case "OBJECT_UPDATED":
1371
+ this.handlers.onObjectUpdated?.(message.payload);
1372
+ break;
1373
+ case "OBJECT_DELETED":
1374
+ this.handlers.onObjectDeleted?.(message.payload.objectId);
1375
+ break;
1376
+ case "USER_JOINED":
1377
+ this.handlers.onUserJoined?.(
1378
+ message.payload.userId,
1379
+ message.payload.userName
1380
+ );
1381
+ break;
1382
+ case "USER_LEFT":
1383
+ this.handlers.onUserLeft?.(
1384
+ message.payload.userId,
1385
+ message.payload.userName
1386
+ );
1387
+ break;
1388
+ }
1389
+ }
1390
+ disconnect() {
1391
+ if (this.ws) {
1392
+ this.ws.close();
1393
+ this.ws = null;
1394
+ this.connected = false;
1395
+ }
1396
+ }
1397
+ send(message) {
1398
+ if (this.ws?.readyState === WebSocket.OPEN) {
1399
+ this.ws.send(JSON.stringify(message));
1400
+ }
1401
+ }
1402
+ createObject(object) {
1403
+ this.send({ type: "OBJECT_CREATE", payload: object });
1404
+ }
1405
+ updateObject(object) {
1406
+ this.send({ type: "OBJECT_UPDATE", payload: object });
1407
+ }
1408
+ deleteObject(objectId) {
1409
+ this.send({ type: "OBJECT_DELETE", payload: { objectId } });
1410
+ }
1411
+ lockObject(objectId) {
1412
+ this.send({ type: "OBJECT_LOCK", payload: { objectId } });
1413
+ }
1414
+ unlockObject(objectId) {
1415
+ this.send({ type: "OBJECT_UNLOCK", payload: { objectId } });
1416
+ }
1417
+ };
1418
+ var SocketIOBoardAdapter = class extends BaseWebSocketAdapter {
1419
+ constructor(config) {
1420
+ super();
1421
+ this.socket = null;
1422
+ this.config = {
1423
+ reconnection: true,
1424
+ reconnectionAttempts: 5,
1425
+ reconnectionDelay: 1e3,
1426
+ transports: ["websocket"],
1427
+ ...config
1428
+ };
1429
+ }
1430
+ connect(boardId, userId, userName) {
1431
+ try {
1432
+ this.socket = io(this.config.url, {
1433
+ ...this.config,
1434
+ query: { boardId, userId, userName }
1435
+ });
1436
+ this.socket.on("connect", () => {
1437
+ this.connected = true;
1438
+ this.handlers.onConnectionChange?.(true);
1439
+ });
1440
+ this.socket.on("disconnect", () => {
1441
+ this.connected = false;
1442
+ this.handlers.onConnectionChange?.(false);
1443
+ });
1444
+ this.socket.on("error", (error) => {
1445
+ this.handleError(new Error(error.message || "Socket.IO error"));
1446
+ });
1447
+ this.socket.on("object:locked", (data) => {
1448
+ this.handlers.onObjectLocked?.(
1449
+ data.objectId,
1450
+ data.userId,
1451
+ data.userName
1452
+ );
1453
+ });
1454
+ this.socket.on("object:unlocked", (objectId) => {
1455
+ this.handlers.onObjectUnlocked?.(objectId);
1456
+ });
1457
+ this.socket.on("object:created", (object) => {
1458
+ this.handlers.onObjectCreated?.(object);
1459
+ });
1460
+ this.socket.on("object:updated", (object) => {
1461
+ this.handlers.onObjectUpdated?.(object);
1462
+ });
1463
+ this.socket.on("object:deleted", (objectId) => {
1464
+ this.handlers.onObjectDeleted?.(objectId);
1465
+ });
1466
+ this.socket.on("user:joined", ({ userId: userId2, userName: userName2 }) => {
1467
+ this.handlers.onUserJoined?.(userId2, userName2);
1468
+ });
1469
+ this.socket.on("user:left", ({ userId: userId2, userName: userName2 }) => {
1470
+ this.handlers.onUserLeft?.(userId2, userName2);
1471
+ });
1472
+ } catch (error) {
1473
+ this.handleError(error);
1474
+ }
1475
+ }
1476
+ disconnect() {
1477
+ if (this.socket) {
1478
+ this.socket.disconnect();
1479
+ this.socket = null;
1480
+ this.connected = false;
1481
+ }
1482
+ }
1483
+ createObject(object) {
1484
+ this.socket?.emit("object:create", object);
1485
+ }
1486
+ updateObject(object) {
1487
+ this.socket?.emit("object:update", object);
1488
+ }
1489
+ deleteObject(objectId) {
1490
+ this.socket?.emit("object:delete", objectId);
1491
+ }
1492
+ lockObject(objectId) {
1493
+ this.socket?.emit("object:lock", objectId);
1494
+ }
1495
+ unlockObject(objectId) {
1496
+ this.socket?.emit("object:unlock", objectId);
1497
+ }
1498
+ };
1499
+
1500
+ // src/index.ts
1501
+ var VERSION = "1.0.0";
1502
+
1503
+ export { BaseWebSocketAdapter, CANVAS_HEIGHT, CANVAS_WIDTH, NativeWebSocketAdapter, SocketIOBoardAdapter, VERSION, canEditObject, clampObjectToCanvas, distanceToLine, generateObjectId, getObjectBoundingBox, isPointInObject, isPointInRotatedRectangle, resizeImageWithAspectRatio, resizeLineObject, resizeObjectFree, rotatePoint, useBoardDrawing };
1504
+ //# sourceMappingURL=out.js.map
1505
+ //# sourceMappingURL=index.mjs.map