From 4835a716fe26b8d62d67b5658c7912e15267c02d Mon Sep 17 00:00:00 2001 From: Denis Gladkiy Date: Tue, 21 Oct 2025 18:00:46 +0600 Subject: [PATCH 1/2] Drawing toolbar controls switching: return pen, after panning and color selection. Signed-off-by: Denis Gladkiy --- .../src/___tests___/drawing.test.ts | 49 +++++++++++++++++++ .../src/components/DrawingBoard.svelte | 3 ++ packages/presentation/src/drawing.ts | 14 ++++++ .../src/components/DrawingBoardEditor.svelte | 4 +- 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/packages/presentation/src/___tests___/drawing.test.ts b/packages/presentation/src/___tests___/drawing.test.ts index 703cfe5fb96..14ed0a25baf 100644 --- a/packages/presentation/src/___tests___/drawing.test.ts +++ b/packages/presentation/src/___tests___/drawing.test.ts @@ -593,5 +593,54 @@ describe('drawing module tests', () => { ]) }) }) + + describe('tools selection', () => { + it('select color after panning', () => { + const colorsList: ColorsList = [ + ['alpha', new ThemeAwareColor('red', 'yellow')], + ['beta', new ThemeAwareColor('blue', 'blue')] + ] + + const toolChangedSpy = jest.fn() + + const initialPenColor: ColorMetaNameOrHex = 'alpha' + const systemUnderTest = drawing(drawingPlugInPoint, { + colorsList, + getCurrentTheme: () => 'theme-light', + subscribeOnThemeChange: () => {}, + readonly: false, + imageWidth: 400, + imageHeight: 300, + commands: [], + tool: 'pen', + penColor: initialPenColor, + toolChanged: toolChangedSpy + }) + + systemUnderTest.update?.({ + colorsList, + getCurrentTheme: () => 'theme-light', + subscribeOnThemeChange: () => {}, + readonly: false, + tool: 'pan', + toolChanged: toolChangedSpy + }) + + expect(toolChangedSpy).toHaveBeenCalledWith('pan') + toolChangedSpy.mockClear() + + const newColor: ColorMetaNameOrHex = 'beta' + systemUnderTest.update?.({ + colorsList, + getCurrentTheme: () => 'theme-light', + subscribeOnThemeChange: () => {}, + readonly: false, + penColor: newColor, + toolChanged: toolChangedSpy + }) + + expect(toolChangedSpy).toHaveBeenCalledWith('pen') + }) + }) }) }) diff --git a/packages/presentation/src/components/DrawingBoard.svelte b/packages/presentation/src/components/DrawingBoard.svelte index fe64e017048..97d3295fbf5 100644 --- a/packages/presentation/src/components/DrawingBoard.svelte +++ b/packages/presentation/src/components/DrawingBoard.svelte @@ -240,6 +240,9 @@ cmdDeleted: deleteCommand, editorCreated: (editor) => { cmdEditor = editor + }, + toolChanged: (newTool) => { + tool = newTool } }} > diff --git a/packages/presentation/src/drawing.ts b/packages/presentation/src/drawing.ts index f788163f084..4048dc9726a 100644 --- a/packages/presentation/src/drawing.ts +++ b/packages/presentation/src/drawing.ts @@ -67,6 +67,7 @@ export interface DrawingProps { personCursorMoved?: (nodePos: Point) => void panning?: (offset: Point) => void panned?: (offset: Point) => void + toolChanged?: (tool: DrawingTool) => void } export type DrawingTool = 'pen' | 'erase' | 'pan' | 'text' @@ -81,6 +82,7 @@ type PointStatus = 'last-point' | 'intermediate-point' class DrawState { on = false + usedBeforePanningTool: DrawingTool | undefined = undefined tool: DrawingTool = 'pen' penColor: ColorMetaNameOrHex = 'alpha' penWidth = 4 @@ -1024,6 +1026,11 @@ export function drawing ( } if (props.tool !== undefined) { if (draw.tool !== props.tool) { + if (props.tool === 'pan') { + draw.usedBeforePanningTool = draw.tool + } else { + draw.usedBeforePanningTool = props.tool + } draw.tool = props.tool syncToolCursor = true toolChanged = true @@ -1031,6 +1038,10 @@ export function drawing ( } if (props.penColor !== undefined) { if (draw.penColor !== props.penColor) { + if (draw.tool === 'pan' && draw.usedBeforePanningTool != null) { + draw.tool = draw.usedBeforePanningTool + toolChanged = true + } draw.penColor = props.penColor syncLiveTextBox = true syncToolCursor = true @@ -1084,6 +1095,9 @@ export function drawing ( } } + if (toolChanged) { + props.toolChanged?.(draw.tool) + } if (syncToolCursor) { updateToolCursor() } diff --git a/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte b/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte index e2c2cdadb10..6dbce83cc33 100644 --- a/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte +++ b/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte @@ -32,7 +32,6 @@ import { Loading, Component, themeStore } from '@hcengineering/ui' import { onMount, onDestroy } from 'svelte' import { Array as YArray, Map as YMap, Doc as YDoc } from 'yjs' - import { get } from 'svelte/store' export let boardId: string export let document: YDoc @@ -288,6 +287,9 @@ }, personCursorMoved: (nodePos) => { personCursorNodePos = nodePos + }, + toolChanged: (newTool) => { + tool = newTool } }} > From 8d436713a26ebd617631fba476c1bb0272e5c935 Mon Sep 17 00:00:00 2001 From: Denis Gladkiy Date: Tue, 21 Oct 2025 18:51:29 +0600 Subject: [PATCH 2/2] Middle mouse button panning. Signed-off-by: Denis Gladkiy --- .../src/components/DrawingBoard.svelte | 1 + packages/presentation/src/drawing.ts | 63 ++++++++++++++----- .../src/components/DrawingBoardEditor.svelte | 1 + 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/packages/presentation/src/components/DrawingBoard.svelte b/packages/presentation/src/components/DrawingBoard.svelte index 97d3295fbf5..d7cba893a47 100644 --- a/packages/presentation/src/components/DrawingBoard.svelte +++ b/packages/presentation/src/components/DrawingBoard.svelte @@ -230,6 +230,7 @@ penWidth, eraserWidth, fontSize, + enableMiddleMousePanning: false, changingCmdId, cmdAdded: addCommand, cmdChanging: showCommandProps, diff --git a/packages/presentation/src/drawing.ts b/packages/presentation/src/drawing.ts index 4048dc9726a..8573286654d 100644 --- a/packages/presentation/src/drawing.ts +++ b/packages/presentation/src/drawing.ts @@ -57,6 +57,7 @@ export interface DrawingProps { changingCmdId?: CommandUid personCursorPos?: Point personCursorVisible?: boolean + enableMiddleMousePanning?: boolean cmdAdded?: (cmd: DrawingCmd) => void cmdChanging?: (id: CommandUid) => void cmdUnchanged?: (id: CommandUid) => void @@ -375,6 +376,7 @@ export function drawing ( resizeObserver.observe(canvas) let touchId: number | undefined + let isMiddleMousePanning = false function findTouch (touches: TouchList, id: number | undefined = touchId): Touch | undefined { for (let i = 0; i < touches.length; i++) { @@ -400,7 +402,8 @@ export function drawing ( } const touch = e.changedTouches[0] touchId = touch.identifier - drawStart(touchToNodePoint(touch, canvas)) + const forcePan = false + drawStart(touchToNodePoint(touch, canvas), forcePan) } canvas.ontouchmove = (e) => { @@ -409,7 +412,8 @@ export function drawing ( } const touch = findTouch(e.changedTouches) if (touch !== undefined) { - drawContinue(touchToNodePoint(touch, canvas)) + const forcePan = false + drawContinue(touchToNodePoint(touch, canvas), forcePan) } } @@ -419,7 +423,8 @@ export function drawing ( } const touch = findTouch(e.changedTouches) if (touch !== undefined) { - drawEnd(touchToNodePoint(touch, canvas)) + const forcePan = false + drawEnd(touchToNodePoint(touch, canvas), forcePan) } touchId = undefined } @@ -430,12 +435,22 @@ export function drawing ( if (readonly) { return } + const MiddleMouseButton = 1 + if (e.button === MiddleMouseButton && props.enableMiddleMousePanning === true) { + e.preventDefault() + isMiddleMousePanning = true + canvas.setPointerCapture(e.pointerId) + const forcePan = true + drawStart(pointerToNodePoint(e), forcePan) + return + } if (e.button !== 0) { return } e.preventDefault() canvas.setPointerCapture(e.pointerId) - drawStart(pointerToNodePoint(e)) + const forcePan = false + drawStart(pointerToNodePoint(e), forcePan) } canvas.onpointermove = (e) => { @@ -443,7 +458,8 @@ export function drawing ( return } e.preventDefault() - drawContinue(pointerToNodePoint(e)) + const forcePan = isMiddleMousePanning + drawContinue(pointerToNodePoint(e), forcePan) } canvas.onpointerup = (e) => { @@ -452,10 +468,23 @@ export function drawing ( } e.preventDefault() canvas.releasePointerCapture(e.pointerId) - drawEnd(pointerToNodePoint(e)) + const forcePan = isMiddleMousePanning + drawEnd(pointerToNodePoint(e), forcePan) + if (e.button === 1) { + isMiddleMousePanning = false + } } - canvas.onpointercancel = canvas.onpointerup + canvas.onpointercancel = (e) => { + if (readonly) { + return + } + e.preventDefault() + canvas.releasePointerCapture(e.pointerId) + const forcePan = isMiddleMousePanning + drawEnd(pointerToNodePoint(e), forcePan) + isMiddleMousePanning = false + } canvas.onpointerenter = () => { if (!readonly && draw.isDrawingTool()) { @@ -474,20 +503,20 @@ export function drawing ( return makeMouseScaledPoint(scaled.x, scaled.y) } - function drawStart (p: NodePoint): void { + function drawStart (p: NodePoint, forcePan: boolean): void { const scaledPoint = rescaleWithCss(p) draw.on = true draw.points = [] prevPos = scaledPoint - if (draw.isDrawingTool()) { + if (!forcePan && draw.isDrawingTool()) { draw.addPoint(scaledPoint) } } - function drawContinue (p: NodePoint): void { + function drawContinue (p: NodePoint, forcePan: boolean): void { const scaledPoint = rescaleWithCss(p) - if (draw.isDrawingTool()) { + if (!forcePan && draw.isDrawingTool()) { const cursorSize = draw.cursorWidth() const canvasOffsetInParent = offsetInParent(node, canvas) @@ -503,7 +532,7 @@ export function drawing ( } } - if (draw.on && draw.tool === 'pan') { + if (draw.on && (draw.tool === 'pan' || forcePan)) { requestAnimationFrame(() => { draw.offset.x += scaledPoint.x - prevPos.x draw.offset.y += scaledPoint.y - prevPos.y @@ -515,22 +544,22 @@ export function drawing ( }) } - if (draw.on && draw.tool === 'text') { + if (draw.on && draw.tool === 'text' && !forcePan) { prevPos = scaledPoint } - if (props.pointerMoved !== undefined) { + if (props.pointerMoved !== undefined && !forcePan) { props.pointerMoved(draw.mouseToCanvasPoint(scaledPoint)) } } - function drawEnd (p: NodePoint): void { + function drawEnd (p: NodePoint, forcePan: boolean): void { const scaledPoint = rescaleWithCss(p) if (draw.on) { - if (draw.isDrawingTool()) { + if (!forcePan && draw.isDrawingTool()) { draw.drawLine(scaledPoint, 'last-point', props.getCurrentTheme()) storeLineCommand() - } else if (draw.tool === 'pan') { + } else if (draw.tool === 'pan' || forcePan) { props.panned?.(draw.offset) } else if (draw.tool === 'text') { if (liveTextBox !== undefined) { diff --git a/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte b/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte index 6dbce83cc33..331d324fc24 100644 --- a/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte +++ b/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte @@ -261,6 +261,7 @@ eraserWidth, fontSize, personCursorPos: personCursorCanvasPos, + enableMiddleMousePanning: true, changingCmdId, cmdAdded: (commandToAdd) => { commandsProcessor.addCommand(commandToAdd)