@@ -14,6 +14,7 @@ import from "..constants.tools" { TOOL_SHORTCUTS }
1414import from .canvas .CanvasRenderer { CanvasRenderer }
1515import from .canvas .TextInput { TextInput }
1616import from .canvas .ContextMenu { ContextMenu }
17+ import from .canvas .collaborator_cursors { CollaboratorCursors }
1718import from .layout .ShortcutHelp { ShortcutHelp }
1819import from .layout .ConfirmDialog { ConfirmDialog }
1920
@@ -29,14 +30,31 @@ def:pub Canvas(props: dict) -> JsxElement {
2930 currentLineStyle = props.currentLineStyle or " solid" ;
3031 currentCurveStyle = props.currentCurveStyle or " curved" ;
3132 currentFillColor = props.currentFillColor or " transparent" ;
33+ roomState = props.roomState or {};
34+ onSessionEnded = props.onSessionEnded;
35+ endSessionRef = props.endSessionRef;
36+ updateUsernameRef = props.updateUsernameRef;
37+ onParticipantsChange = props.onParticipantsChange;
3238
3339 canvasHook = useCanvas();
3440 drawingState = useDrawingState();
3541 elementsHook = useElements();
3642 selectionHook = useSelection();
3743 viewportHook = useViewport();
3844 textInputHook = useTextInput();
39- useWebSocket(elementsHook, viewportHook, canvasHook.canvasRef, canvasHook);
45+ wsHook = useWebSocket(elementsHook, viewportHook, canvasHook.canvasRef, canvasHook, roomState, onSessionEnded);
46+ wsHookRef = useRef(wsHook);
47+ wsHookRef.current = wsHook;
48+ # Expose sendEndSession to parent via ref
49+ if endSessionRef and wsHook.sendEndSessionRef {
50+ endSessionRef.current = wsHook.sendEndSessionRef.current;
51+ }
52+ if updateUsernameRef and wsHook.sendUpdateUsernameRef {
53+ updateUsernameRef.current = wsHook.sendUpdateUsernameRef.current;
54+ }
55+ hasBroadcastMountedRef = useRef(False );
56+ lastCursorBroadcastRef = useRef(0 );
57+ lastDrawBroadcastRef = useRef(0 );
4058
4159 canvasRef = canvasHook.canvasRef;
4260 containerRef = canvasHook.containerRef;
@@ -74,6 +92,28 @@ def:pub Canvas(props: dict) -> JsxElement {
7492 editingShapeId = textInputHook.textInput.editingShapeId;
7593 }
7694
95+ can with [elementsHook .elements ] entry {
96+ if not hasBroadcastMountedRef.current {
97+ hasBroadcastMountedRef.current = True ;
98+ } elif wsHookRef.current and wsHookRef.current.isApplyingRemoteRef and wsHookRef.current.isApplyingRemoteRef.current {
99+ wsHookRef.current.isApplyingRemoteRef.current = False ;
100+ } elif wsHookRef.current and wsHookRef.current.sendRoomBroadcastRef and wsHookRef.current.sendRoomBroadcastRef.current {
101+ wsHookRef.current.sendRoomBroadcastRef.current(elementsHook.elements);
102+ }
103+ }
104+
105+ can with [wsHook .participants , wsHook .hostSessionId ] entry {
106+ if onParticipantsChange {
107+ # Merge hostSessionId as __hostSid meta key so parent can read it
108+ merged = Object.assign({}, wsHook.participants);
109+ if wsHook.hostSessionId {
110+ merged[" __hostSid" ] = wsHook.hostSessionId;
111+ }
112+ onParticipantsChange(merged);
113+ }
114+ return lambda -> None {};
115+ }
116+
77117 can with [elementsHook .elements , canvasHook .canvasSize , viewportHook .zoom , viewportHook .panOffset , selectionHook .selectedElement , selectionHook .selectedElements , selectionHook .isBoxSelecting , selectionHook .boxStart , selectionHook .boxEnd , drawingState .isDrawing , textInputHook .textInput ] entry {
78118 canvas = canvasRef.current;
79119 if canvas {
@@ -409,6 +449,15 @@ def:pub Canvas(props: dict) -> JsxElement {
409449 });
410450 };
411451 }
452+ if props.elementsForShareRef {
453+ props.elementsForShareRef.current = elementsHook.elements;
454+ }
455+ if props.setElementsRef {
456+ props.setElementsRef.current = lambda elements: any -> None {
457+ elementsHook.setElements(elements);
458+ selectionHook.clearSelection();
459+ };
460+ }
412461 }
413462
414463 def handleCanvasClick (e : dict ) -> None {
@@ -1159,6 +1208,17 @@ def:pub Canvas(props: dict) -> JsxElement {
11591208 def handleMouseMove (e : dict ) -> None {
11601209 pos = getMousePosition(e, canvasRef.current, viewportHook.panOffset, viewportHook.zoom);
11611210
1211+ # Throttled cursor broadcast (50ms) — only when in a room
1212+ cur_now = Date.now();
1213+ if cur_now - lastCursorBroadcastRef.current > 50 {
1214+ lastCursorBroadcastRef.current = cur_now;
1215+ cur_ws_hook = wsHookRef.current;
1216+ if cur_ws_hook and cur_ws_hook.sendCursorBroadcastRef and cur_ws_hook.sendCursorBroadcastRef.current {
1217+ cur_stored_name = localStorage.getItem(" jasketch_username" ) or " Anonymous" ;
1218+ cur_ws_hook.sendCursorBroadcastRef.current(pos.x, pos.y, cur_stored_name);
1219+ }
1220+ }
1221+
11621222 if viewportHook.isPanning {
11631223 viewportHook.handlePan(e.movementX, e.movementY);
11641224 return ;
@@ -1642,6 +1702,38 @@ def:pub Canvas(props: dict) -> JsxElement {
16421702 drawPreview(previewCtx, currentTool, drawingState.startPoint, previewEnd, currentColor, currentStrokeWidth);
16431703 previewCtx.restore();
16441704 }
1705+
1706+ # Throttled live drawing broadcast (80ms) — sends in-progress element as __draft__
1707+ if drawingState.isDrawing {
1708+ draw_now = Date.now();
1709+ if draw_now - lastDrawBroadcastRef.current > 80 {
1710+ lastDrawBroadcastRef.current = draw_now;
1711+ preview_ws_hook = wsHookRef.current;
1712+ if preview_ws_hook and preview_ws_hook.sendRoomPreviewRef and preview_ws_hook.sendRoomPreviewRef.current {
1713+ live_draft_el = None ;
1714+ if currentTool == " freehand" and drawingState.currentPath and drawingState.currentPath.length > 0 {
1715+ live_draft_el = {" type" : " freehand" , " id" : " __draft__" , " points" : drawingState.currentPath, " color" : currentColor, " strokeWidth" : currentStrokeWidth, " lineStyle" : " solid" , " opacity" : currentOpacity};
1716+ } elif currentTool == " rectangle" and drawingState.startPoint {
1717+ ldraft_rw = pos.x - drawingState.startPoint.x;
1718+ ldraft_rh = pos.y - drawingState.startPoint.y;
1719+ live_draft_el = {" type" : " rectangle" , " id" : " __draft__" , " x" : drawingState.startPoint.x, " y" : drawingState.startPoint.y, " width" : ldraft_rw, " height" : ldraft_rh, " color" : currentColor, " strokeWidth" : currentStrokeWidth, " fillColor" : currentFillColor, " lineStyle" : currentLineStyle, " opacity" : currentOpacity};
1720+ } elif currentTool == " circle" and drawingState.startPoint {
1721+ ldraft_cw = pos.x - drawingState.startPoint.x;
1722+ ldraft_ch = pos.y - drawingState.startPoint.y;
1723+ live_draft_el = {" type" : " circle" , " id" : " __draft__" , " x" : drawingState.startPoint.x, " y" : drawingState.startPoint.y, " width" : ldraft_cw, " height" : ldraft_ch, " color" : currentColor, " strokeWidth" : currentStrokeWidth, " fillColor" : currentFillColor, " lineStyle" : currentLineStyle, " opacity" : currentOpacity};
1724+ } elif currentTool == " diamond" and drawingState.startPoint {
1725+ ldraft_dw = pos.x - drawingState.startPoint.x;
1726+ ldraft_dh = pos.y - drawingState.startPoint.y;
1727+ live_draft_el = {" type" : " diamond" , " id" : " __draft__" , " x" : drawingState.startPoint.x, " y" : drawingState.startPoint.y, " width" : ldraft_dw, " height" : ldraft_dh, " color" : currentColor, " strokeWidth" : currentStrokeWidth, " fillColor" : currentFillColor, " lineStyle" : currentLineStyle, " opacity" : currentOpacity};
1728+ } elif (currentTool == " line" or currentTool == " arrow" ) and drawingState.startPoint {
1729+ live_draft_el = {" type" : currentTool, " id" : " __draft__" , " x1" : drawingState.startPoint.x, " y1" : drawingState.startPoint.y, " x2" : pos.x, " y2" : pos.y, " color" : currentColor, " strokeWidth" : currentStrokeWidth, " lineStyle" : currentLineStyle, " curveStyle" : currentCurveStyle, " opacity" : currentOpacity};
1730+ }
1731+ if live_draft_el {
1732+ preview_ws_hook.sendRoomPreviewRef.current(elementsHook.elements.concat([live_draft_el]));
1733+ }
1734+ }
1735+ }
1736+ }
16451737 }
16461738
16471739 def handleMouseUp (e : dict ) -> None {
@@ -1857,6 +1949,13 @@ def:pub Canvas(props: dict) -> JsxElement {
18571949 onMouseLeave = { handleMouseLeave}
18581950 />
18591951
1952+ <CollaboratorCursors
1953+ cursor_states = { wsHook.cursorStates or {}}
1954+ zoom = { viewportHook.zoom}
1955+ pan_x = { viewportHook.panOffset.x}
1956+ pan_y = { viewportHook.panOffset.y}
1957+ />
1958+
18601959 <TextInput
18611960 textInput = { textInputHook.textInput}
18621961 textInputRef = { textInputRef}
0 commit comments