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