Drawing board rect, ellipse and line shape drawing. (#10142)

* Tools selection merged into a popup.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Popup availability indicator.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Rectangle tool.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Ellipse tool.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Line tool.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Refactoring: code generalization.
Stroke width tool visibility fix.
Panning cursor position fix.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Drawing board toolbar overflow fix.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Refactoring: code generalization.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Formatting.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Svelte check fix.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

---------

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>
This commit is contained in:
Denis 'GeneralGDA' Gladkiy
2025-10-24 18:47:50 +06:00
committed by GitHub
parent 564223c6d7
commit 70a4ea2b93
21 changed files with 501 additions and 112 deletions
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "Zpět",
"Redo": "Znovu",
"ClearCanvas": "Vyčistit plátno",
"PenTool": "Nástroj pero",
"EraserTool": "Nástroj guma",
"PanTool": "Nástroj posun",
"TextTool": "Nástroj text",
"PenTool": "Pero",
"EraserTool": "Guma",
"PanTool": "Posun",
"TextTool": "Text",
"LineTool": "Čára",
"RectangleTool": "Obdélník",
"EllipseTool": "Elipsa",
"PaletteManagementMenu": "Spravovat barevné předvolby"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "Rückgängig",
"Redo": "Wiederholen",
"ClearCanvas": "Leinwand löschen",
"PenTool": "Stift-Werkzeug",
"EraserTool": "Radiergummi-Werkzeug",
"PanTool": "Verschieben-Werkzeug",
"TextTool": "Text-Werkzeug",
"PenTool": "Stift",
"EraserTool": "Radiergummi",
"PanTool": "Verschieben",
"TextTool": "Text",
"LineTool": "Linie",
"RectangleTool": "Rechteck",
"EllipseTool": "Ellipse",
"PaletteManagementMenu": "Farbpresets verwalten"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "Undo",
"Redo": "Redo",
"ClearCanvas": "Clear canvas",
"PenTool": "Pen tool",
"EraserTool": "Eraser tool",
"PanTool": "Pan tool",
"TextTool": "Text tool",
"PenTool": "Pen",
"EraserTool": "Eraser",
"PanTool": "Pan",
"TextTool": "Text",
"LineTool": "Line",
"RectangleTool": "Rectangle",
"EllipseTool": "Ellipse",
"PaletteManagementMenu": "Manage color presets"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "Deshacer",
"Redo": "Rehacer",
"ClearCanvas": "Limpiar lienzo",
"PenTool": "Herramienta lápiz",
"EraserTool": "Herramienta borrador",
"PanTool": "Herramienta mover",
"TextTool": "Herramienta texto",
"PenTool": "Lápiz",
"EraserTool": "Borrador",
"PanTool": "Mover",
"TextTool": "Texto",
"LineTool": "Línea",
"RectangleTool": "Rectángulo",
"EllipseTool": "Elipse",
"PaletteManagementMenu": "Gestionar preajustes de color"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "Annuler",
"Redo": "Rétablir",
"ClearCanvas": "Effacer la toile",
"PenTool": "Outil stylo",
"EraserTool": "Outil gomme",
"PanTool": "Outil déplacement",
"TextTool": "Outil texte",
"PenTool": "Stylo",
"EraserTool": "Gomme",
"PanTool": "Déplacement",
"TextTool": "Texte",
"LineTool": "Ligne",
"RectangleTool": "Rectangle",
"EllipseTool": "Ellipse",
"PaletteManagementMenu": "Gérer les préréglages de couleur"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "Annulla",
"Redo": "Ripristina",
"ClearCanvas": "Cancella la tela",
"PenTool": "Strumento penna",
"EraserTool": "Strumento gomma",
"PanTool": "Strumento sposta",
"TextTool": "Strumento testo",
"PenTool": "Penna",
"EraserTool": "Gomma",
"PanTool": "Sposta",
"TextTool": "Testo",
"LineTool": "Linea",
"RectangleTool": "Rettangolo",
"EllipseTool": "Ellisse",
"PaletteManagementMenu": "Gestisci i preset di colore"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "元に戻す",
"Redo": "やり直し",
"ClearCanvas": "キャンバスをクリア",
"PenTool": "ペンツール",
"EraserTool": "消しゴムツール",
"PanTool": "パンツール",
"TextTool": "テキストツール",
"PenTool": "ペン",
"EraserTool": "消しゴム",
"PanTool": "パン",
"TextTool": "テキスト",
"LineTool": "直線",
"RectangleTool": "長方形",
"EllipseTool": "楕円",
"PaletteManagementMenu": "カラープリセットを管理"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "Desfazer",
"Redo": "Refazer",
"ClearCanvas": "Limpar tela",
"PenTool": "Ferramenta caneta",
"EraserTool": "Ferramenta borracha",
"PanTool": "Ferramenta mover",
"TextTool": "Ferramenta texto",
"PenTool": "Caneta",
"EraserTool": "Borracha",
"PanTool": "Mover",
"TextTool": "Texto",
"LineTool": "Linha",
"RectangleTool": "Retângulo",
"EllipseTool": "Elipse",
"PaletteManagementMenu": "Gerenciar predefinições de cor"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "Отменить",
"Redo": "Повторить",
"ClearCanvas": "Очистить холст",
"PenTool": "Инструмент перо",
"EraserTool": "Инструмент ластик",
"PanTool": "Инструмент перемещения",
"TextTool": "Инструмент текст",
"PenTool": "Перо",
"EraserTool": "Ластик",
"PanTool": "Перемещение",
"TextTool": "Текст",
"LineTool": "Линия",
"RectangleTool": "Прямоугольник",
"EllipseTool": "Эллипс",
"PaletteManagementMenu": "Управление цветовыми пресетами"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "Geri al",
"Redo": "Yinele",
"ClearCanvas": "Tuvali temizle",
"PenTool": "Kalem aracı",
"EraserTool": "Silgi aracı",
"PanTool": "Kaydırma aracı",
"TextTool": "Metin aracı",
"PenTool": "Kalem",
"EraserTool": "Silgi",
"PanTool": "Kaydırma",
"TextTool": "Metin",
"LineTool": "Çizgi",
"RectangleTool": "Dikdörtgen",
"EllipseTool": "Elips",
"PaletteManagementMenu": "Renk önayarlarını yönet"
},
"status": {
+7 -4
View File
@@ -49,10 +49,13 @@
"Undo": "撤销",
"Redo": "重做",
"ClearCanvas": "清除画布",
"PenTool": "画笔工具",
"EraserTool": "橡皮擦工具",
"PanTool": "移动工具",
"TextTool": "文字工具",
"PenTool": "画笔",
"EraserTool": "橡皮擦",
"PanTool": "移动",
"TextTool": "文字",
"LineTool": "直线",
"RectangleTool": "矩形",
"EllipseTool": "椭圆",
"PaletteManagementMenu": "管理颜色预设"
},
"status": {
@@ -26,10 +26,13 @@
eventToHTMLElement,
showPopup
} from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import { ComponentType, createEventDispatcher, onMount } from 'svelte'
import IconEraser from './icons/Eraser.svelte'
import IconMove from './icons/Move.svelte'
import IconText from './icons/Text.svelte'
import IconRectangle from './icons/Rectangle.svelte'
import IconEllipse from './icons/Ellipse.svelte'
import IconLine from './icons/Line.svelte'
import { DrawingTool } from '../drawing'
import presentation from '../plugin'
import { ColorMetaName, ColorMetaNameOrHex } from '../drawingUtils'
@@ -37,6 +40,23 @@
import DrawingBoardColorSelectorIcon from './DrawingBoardColorSelectorIcon.svelte'
import { ColorsList, DrawingBoardColoringSetup } from '../drawingColors'
import { Analytics } from '@hcengineering/analytics'
import type { IntlString } from '@hcengineering/platform'
interface ToolPresentation {
label: IntlString
icon: ComponentType
tool: DrawingTool
}
const tools: ToolPresentation[] = [
{ label: presentation.string.PenTool, icon: IconEdit, tool: 'pen' },
{ label: presentation.string.EraserTool, icon: IconEraser, tool: 'erase' },
{ label: presentation.string.PanTool, icon: IconMove, tool: 'pan' },
{ label: presentation.string.TextTool, icon: IconText, tool: 'text' },
{ label: presentation.string.LineTool, icon: IconLine, tool: 'shape-line' },
{ label: presentation.string.RectangleTool, icon: IconRectangle, tool: 'shape-rectangle' },
{ label: presentation.string.EllipseTool, icon: IconEllipse, tool: 'shape-ellipse' }
]
interface DrawingBoardToolbarEvents {
undo: undefined
@@ -45,6 +65,7 @@
}
const dispatch = createEventDispatcher<DrawingBoardToolbarEvents>()
const maxColors = 8
const minColors = 0
const defaultColor: ColorMetaName = 'alpha'
@@ -58,6 +79,21 @@
}
export let tool: DrawingTool = 'pen'
function evaluateToolPresentation (tool: DrawingTool): ToolPresentation {
const found = tools.find((t) => t.tool === tool)
if (found == null) {
return tools[0]
}
return found
}
let toolPresentation: ToolPresentation = evaluateToolPresentation(tool)
$: {
toolPresentation = evaluateToolPresentation(tool)
}
export let penColor: ColorMetaNameOrHex
export let penWidth: number
export let eraserWidth: number
@@ -150,6 +186,25 @@
localStorage.setItem(storageKey.color, penColor)
}
function showToolSelectionMenu (ev: MouseEvent): void {
const items: Array<Omit<SelectPopupValueType, 'id'> & { id: DrawingTool }> = []
for (const toolPresentation of tools) {
if (toolPresentation.tool === 'pan' && !showPanTool) {
continue
}
items.push({
id: toolPresentation.tool,
label: toolPresentation.label,
icon: toolPresentation.icon
})
}
showPopup(SelectPopup, { value: items }, eventToHTMLElement(ev), (id: DrawingTool | undefined) => {
if (id != null) {
tool = id
}
})
}
onMount(() => {
try {
const savedColors = localStorage.getItem(storageKey.colors)
@@ -210,61 +265,23 @@
}}
/>
<div class="divider buttons-divider" />
<Button kind="icon" showTooltip={{ label: toolPresentation.label }} noFocus on:click={showToolSelectionMenu}>
<div class="tool-button-with-indicator" slot="content">
<svelte:component this={toolPresentation.icon} size="small" />
<div class="tool-indicator" />
</div>
</Button>
<Button
icon={IconDelete}
kind="icon"
showTooltip={{ label: presentation.string.ClearCanvas }}
noFocus
on:click={() => {
tool = 'pen'
dispatch('clear')
}}
/>
<div class="divider buttons-divider" />
<Button
icon={IconEdit}
kind="icon"
showTooltip={{ label: presentation.string.PenTool }}
noFocus
selected={tool === 'pen'}
on:click={() => {
tool = 'pen'
}}
/>
<Button
icon={IconEraser}
kind="icon"
showTooltip={{ label: presentation.string.EraserTool }}
noFocus
selected={tool === 'erase'}
on:click={() => {
tool = 'erase'
}}
/>
{#if showPanTool}
<Button
icon={IconMove}
kind="icon"
showTooltip={{ label: presentation.string.PanTool }}
noFocus
selected={tool === 'pan'}
on:click={() => {
tool = 'pan'
}}
/>
{/if}
<Button
icon={IconText}
kind="icon"
showTooltip={{ label: presentation.string.TextTool }}
noFocus
selected={tool === 'text'}
on:click={() => {
tool = 'text'
}}
/>
<div class="divider buttons-divider" />
{#if tool === 'pen'}
{#if tool !== 'erase' && tool !== 'pan' && tool !== 'text'}
<input
class="widthSelector"
type="range"
@@ -336,7 +353,12 @@
&.inside {
left: 0.5rem;
top: 0.5rem;
right: auto;
bottom: unset;
display: inline-flex;
flex-wrap: wrap;
align-items: center;
max-width: calc(100% - 1rem);
background-color: var(--theme-popup-header);
border-radius: var(--small-BorderRadius);
border: 1px solid var(--theme-popup-divider);
@@ -360,4 +382,23 @@
.widthSelector {
width: 80px;
}
.tool-button-with-indicator {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.tool-indicator {
position: absolute;
bottom: -0.125rem;
right: -0.125rem;
width: 0;
height: 0;
border-style: solid;
border-width: 0 0 0.25rem 0.25rem;
border-color: transparent transparent currentColor transparent;
opacity: 0.7;
}
</style>
@@ -0,0 +1,23 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="8" cy="8" rx="6" ry="4" stroke="currentColor" fill="none" stroke-width="1" />
</svg>
@@ -1,3 +1,18 @@
<!--
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
@@ -0,0 +1,23 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<line x1="2" y1="14" x2="14" y2="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
@@ -1,3 +1,18 @@
<!--
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
@@ -0,0 +1,23 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3h12v10H2V3zm1 1v8h10V4H3z" />
</svg>
@@ -1,3 +1,18 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
+198 -21
View File
@@ -31,7 +31,16 @@ import {
offsetCanvasPoint,
type ColorMetaNameOrHex
} from './drawingUtils'
import { type DrawingCmd, type CommandUid, type DrawTextCmd, type DrawLineCmd, makeCommandUid } from './drawingCommand'
import {
type DrawingCmd,
type CommandUid,
type DrawTextCmd,
type DrawLineCmd,
type DrawRectCmd,
type DrawEllipseCmd,
type DrawStraightLineCmd,
makeCommandUid
} from './drawingCommand'
import { type ColorsList, DrawingBoardColoringSetup, metaColorNameToHex } from './drawingColors'
export interface DrawingData {
@@ -71,7 +80,7 @@ export interface DrawingProps {
toolChanged?: (tool: DrawingTool) => void
}
export type DrawingTool = 'pen' | 'erase' | 'pan' | 'text'
export type DrawingTool = 'pen' | 'erase' | 'pan' | 'text' | 'shape-rectangle' | 'shape-ellipse' | 'shape-line'
const maxTextLength = 500
@@ -129,7 +138,13 @@ class DrawState {
}
isDrawingTool = (): boolean => {
return this.tool === 'pen' || this.tool === 'erase'
return (
this.tool === 'pen' ||
this.tool === 'erase' ||
this.tool === 'shape-rectangle' ||
this.tool === 'shape-ellipse' ||
this.tool === 'shape-line'
)
}
translateCtx = (): void => {
@@ -165,6 +180,12 @@ class DrawState {
drawCommand = (cmd: DrawingCmd, currentTheme: ThemeVariantType): void => {
if (cmd.type === 'text') {
this.drawTextCommand(cmd as DrawTextCmd, currentTheme)
} else if (cmd.type === 'rectangle') {
this.drawRectCommand(cmd as DrawRectCmd, currentTheme)
} else if (cmd.type === 'ellipse') {
this.drawEllipseCommand(cmd as DrawEllipseCmd, currentTheme)
} else if (cmd.type === 'straight-line') {
this.drawStraightLineCommand(cmd as DrawStraightLineCmd, currentTheme)
} else {
this.drawLineCommand(cmd as DrawLineCmd, currentTheme)
}
@@ -206,6 +227,46 @@ class DrawState {
this.ctx.restore()
}
drawRectCommand = (cmd: DrawRectCmd, currentTheme: ThemeVariantType): void => {
this.ctx.save()
this.translateCtx()
this.ctx.beginPath()
this.ctx.strokeStyle = metaColorNameToHex(cmd.penColor, currentTheme, this.colors)
this.ctx.lineWidth = cmd.lineWidth
const width = cmd.end.x - cmd.start.x
const height = cmd.end.y - cmd.start.y
this.ctx.strokeRect(cmd.start.x, cmd.start.y, width, height)
this.ctx.restore()
}
drawEllipseCommand = (cmd: DrawEllipseCmd, currentTheme: ThemeVariantType): void => {
this.ctx.save()
this.translateCtx()
this.ctx.beginPath()
this.ctx.strokeStyle = metaColorNameToHex(cmd.penColor, currentTheme, this.colors)
this.ctx.lineWidth = cmd.lineWidth
const centerX = (cmd.start.x + cmd.end.x) / 2
const centerY = (cmd.start.y + cmd.end.y) / 2
const radiusX = Math.abs(cmd.end.x - cmd.start.x) / 2
const radiusY = Math.abs(cmd.end.y - cmd.start.y) / 2
this.ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2)
this.ctx.stroke()
this.ctx.restore()
}
drawStraightLineCommand = (cmd: DrawStraightLineCmd, currentTheme: ThemeVariantType): void => {
this.ctx.save()
this.translateCtx()
this.ctx.beginPath()
this.ctx.lineCap = 'round'
this.ctx.strokeStyle = metaColorNameToHex(cmd.penColor, currentTheme, this.colors)
this.ctx.lineWidth = cmd.lineWidth
this.ctx.moveTo(cmd.start.x, cmd.start.y)
this.ctx.lineTo(cmd.end.x, cmd.end.y)
this.ctx.stroke()
this.ctx.restore()
}
isPointInText = (p: CanvasPoint, cmd: DrawTextCmd): boolean => {
this.ctx.font = `${cmd.fontSize}px ${cmd.fontFace}`
const lines = cmd.text.split('\n').map((l) => l.trim())
@@ -311,7 +372,7 @@ export function drawing (
draw.offset = props.offset ?? draw.offset
let isOffsetAnimating = false
updateToolCursor()
updateToolCursor(false)
updateCanvasTouchAction()
interface LiveTextBox {
@@ -338,7 +399,7 @@ export function drawing (
replayCommands(currentCommands)
props.subscribeOnThemeChange(() => {
updateToolCursor()
updateToolCursor(false)
replayCommands(currentCommands)
})
@@ -431,14 +492,16 @@ export function drawing (
canvas.ontouchcancel = canvas.ontouchend
const MiddleMouseButton = 1
canvas.onpointerdown = (e) => {
if (readonly) {
return
}
const MiddleMouseButton = 1
if (e.button === MiddleMouseButton && props.enableMiddleMousePanning === true) {
e.preventDefault()
isMiddleMousePanning = true
updateToolCursor(true)
canvas.setPointerCapture(e.pointerId)
const forcePan = true
drawStart(pointerToNodePoint(e), forcePan)
@@ -470,8 +533,9 @@ export function drawing (
canvas.releasePointerCapture(e.pointerId)
const forcePan = isMiddleMousePanning
drawEnd(pointerToNodePoint(e), forcePan)
if (e.button === 1) {
if (e.button === MiddleMouseButton) {
isMiddleMousePanning = false
updateToolCursor(false)
}
}
@@ -484,6 +548,7 @@ export function drawing (
const forcePan = isMiddleMousePanning
drawEnd(pointerToNodePoint(e), forcePan)
isMiddleMousePanning = false
updateToolCursor(false)
}
canvas.onpointerenter = () => {
@@ -516,16 +581,33 @@ export function drawing (
function drawContinue (p: NodePoint, forcePan: boolean): void {
const scaledPoint = rescaleWithCss(p)
if (!forcePan && draw.isDrawingTool()) {
if (draw.isDrawingTool() || forcePan) {
const cursorSize = draw.cursorWidth()
const canvasOffsetInParent = offsetInParent(node, canvas)
const parentRelativeLocation = offsetPoint(scaledPoint, canvasOffsetInParent)
toolCursor.style.left = `${parentRelativeLocation.x - cursorSize / 2}px`
toolCursor.style.top = `${parentRelativeLocation.y - cursorSize / 2}px`
}
if (!forcePan && draw.isDrawingTool()) {
if (draw.on) {
if (Math.hypot(prevPos.x - scaledPoint.x, prevPos.y - scaledPoint.y) >= draw.minLineLength) {
if (draw.tool === 'shape-rectangle') {
requestAnimationFrame(() => {
replayCommands(currentCommands)
drawPreviewRectangle(scaledPoint)
})
} else if (draw.tool === 'shape-ellipse') {
requestAnimationFrame(() => {
replayCommands(currentCommands)
drawPreviewEllipse(scaledPoint)
})
} else if (draw.tool === 'shape-line') {
requestAnimationFrame(() => {
replayCommands(currentCommands)
drawPreviewStraightLine(scaledPoint)
})
} else if (Math.hypot(prevPos.x - scaledPoint.x, prevPos.y - scaledPoint.y) >= draw.minLineLength) {
draw.drawLine(scaledPoint, 'intermediate-point', props.getCurrentTheme())
prevPos = scaledPoint
}
@@ -548,7 +630,7 @@ export function drawing (
prevPos = scaledPoint
}
if (props.pointerMoved !== undefined && !forcePan) {
if (props.pointerMoved !== undefined) {
props.pointerMoved(draw.mouseToCanvasPoint(scaledPoint))
}
}
@@ -557,8 +639,16 @@ export function drawing (
const scaledPoint = rescaleWithCss(p)
if (draw.on) {
if (!forcePan && draw.isDrawingTool()) {
draw.drawLine(scaledPoint, 'last-point', props.getCurrentTheme())
storeLineCommand()
if (draw.tool === 'shape-rectangle') {
storeRectCommand(scaledPoint)
} else if (draw.tool === 'shape-ellipse') {
storeEllipseCommand(scaledPoint)
} else if (draw.tool === 'shape-line') {
storeStraightLineCommand(scaledPoint)
} else {
draw.drawLine(scaledPoint, 'last-point', props.getCurrentTheme())
storeLineCommand()
}
} else if (draw.tool === 'pan' || forcePan) {
props.panned?.(draw.offset)
} else if (draw.tool === 'text') {
@@ -885,24 +975,111 @@ export function drawing (
}
function storeLineCommand (): void {
if (draw.points.length > 0) {
const erasing = draw.tool === 'erase'
const cmd: DrawLineCmd = {
if (draw.points.length === 0) {
return
}
const erasing = draw.tool === 'erase'
const cmd: DrawLineCmd = {
id: makeCommandUid(),
type: 'line',
lineWidth: erasing ? draw.eraserWidth : draw.penWidth,
erasing,
penColor: draw.penColor,
points: draw.points
}
props.cmdAdded?.(cmd)
}
function drawShapePreview (
endPoint: MouseScaledPoint,
drawShape: (start: CanvasPoint, end: CanvasPoint) => void
): void {
if (draw.points.length === 0) {
return
}
const start = draw.points[0]
const end = draw.mouseToCanvasPoint(endPoint)
draw.ctx.save()
draw.translateCtx()
draw.ctx.beginPath()
draw.ctx.strokeStyle = metaColorNameToHex(draw.penColor, props.getCurrentTheme(), colorsSetup)
draw.ctx.lineWidth = draw.penWidth
drawShape(start, end)
draw.ctx.stroke()
draw.ctx.restore()
}
function drawPreviewRectangle (endPoint: MouseScaledPoint): void {
drawShapePreview(endPoint, (start, end) => {
const width = end.x - start.x
const height = end.y - start.y
draw.ctx.strokeRect(start.x, start.y, width, height)
})
}
function drawPreviewEllipse (endPoint: MouseScaledPoint): void {
drawShapePreview(endPoint, (start, end) => {
const centerX = (start.x + end.x) / 2
const centerY = (start.y + end.y) / 2
const radiusX = Math.abs(end.x - start.x) / 2
const radiusY = Math.abs(end.y - start.y) / 2
draw.ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2)
})
}
function drawPreviewStraightLine (endPoint: MouseScaledPoint): void {
drawShapePreview(endPoint, (start, end) => {
draw.ctx.lineCap = 'round'
draw.ctx.moveTo(start.x, start.y)
draw.ctx.lineTo(end.x, end.y)
})
}
function storeShapeCommand (endPoint: MouseScaledPoint, type: 'rectangle' | 'ellipse' | 'straight-line'): void {
if (draw.points.length === 0) {
return
}
const start = draw.points[0]
const end = draw.mouseToCanvasPoint(endPoint)
const minSize = 2
const nonDegenerate = Math.abs(end.x - start.x) > minSize || Math.abs(end.y - start.y) > minSize
if (nonDegenerate) {
const cmd: DrawRectCmd | DrawEllipseCmd | DrawStraightLineCmd = {
id: makeCommandUid(),
type: 'line',
lineWidth: erasing ? draw.eraserWidth : draw.penWidth,
erasing,
type,
lineWidth: draw.penWidth,
penColor: draw.penColor,
points: draw.points
start,
end
}
props.cmdAdded?.(cmd)
}
}
function updateToolCursor (): void {
function storeRectCommand (endPoint: MouseScaledPoint): void {
storeShapeCommand(endPoint, 'rectangle')
}
function storeEllipseCommand (endPoint: MouseScaledPoint): void {
storeShapeCommand(endPoint, 'ellipse')
}
function storeStraightLineCommand (endPoint: MouseScaledPoint): void {
storeShapeCommand(endPoint, 'straight-line')
}
function updateToolCursor (forcePanning: boolean): void {
if (readonly) {
toolCursor.style.visibility = 'hidden'
canvas.style.cursor = props.defaultCursor ?? 'default'
} else if (forcePanning) {
canvas.style.cursor = 'grabbing'
toolCursor.style.visibility = 'hidden'
} else if (draw.isDrawingTool()) {
canvas.style.cursor = 'none'
toolCursor.style.visibility = 'visible'
@@ -1128,7 +1305,7 @@ export function drawing (
props.toolChanged?.(draw.tool)
}
if (syncToolCursor) {
updateToolCursor()
updateToolCursor(false)
}
if (syncPersonCursor !== undefined) {
updatePersonCursor(syncPersonCursor)
+22 -1
View File
@@ -20,7 +20,7 @@ export type CommandUid = string & { readonly __brand: 'CommandUid' }
export interface DrawingCmd {
id: CommandUid
type: 'line' | 'text'
type: 'line' | 'text' | 'rectangle' | 'ellipse' | 'straight-line'
}
export interface DrawTextCmd extends DrawingCmd {
@@ -38,6 +38,27 @@ export interface DrawLineCmd extends DrawingCmd {
points: CanvasPoint[]
}
export interface DrawRectCmd extends DrawingCmd {
lineWidth: number
penColor: ColorMetaNameOrHex
start: CanvasPoint
end: CanvasPoint
}
export interface DrawEllipseCmd extends DrawingCmd {
lineWidth: number
penColor: ColorMetaNameOrHex
start: CanvasPoint
end: CanvasPoint
}
export interface DrawStraightLineCmd extends DrawingCmd {
lineWidth: number
penColor: ColorMetaNameOrHex
start: CanvasPoint
end: CanvasPoint
}
export const makeCommandUid = (): CommandUid => {
return (crypto?.randomUUID?.() ?? generateId()) as CommandUid
}
+3
View File
@@ -155,6 +155,9 @@ export default plugin(presentationId, {
EraserTool: '' as IntlString,
PanTool: '' as IntlString,
TextTool: '' as IntlString,
LineTool: '' as IntlString,
RectangleTool: '' as IntlString,
EllipseTool: '' as IntlString,
PaletteManagementMenu: '' as IntlString
},
extension: {