mirror of
https://github.com/hcengineering/platform.git
synced 2026-10-01 05:55:09 +02:00
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:
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "元に戻す",
|
||||
"Redo": "やり直し",
|
||||
"ClearCanvas": "キャンバスをクリア",
|
||||
"PenTool": "ペンツール",
|
||||
"EraserTool": "消しゴムツール",
|
||||
"PanTool": "パンツール",
|
||||
"TextTool": "テキストツール",
|
||||
"PenTool": "ペン",
|
||||
"EraserTool": "消しゴム",
|
||||
"PanTool": "パン",
|
||||
"TextTool": "テキスト",
|
||||
"LineTool": "直線",
|
||||
"RectangleTool": "長方形",
|
||||
"EllipseTool": "楕円",
|
||||
"PaletteManagementMenu": "カラープリセットを管理"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Отменить",
|
||||
"Redo": "Повторить",
|
||||
"ClearCanvas": "Очистить холст",
|
||||
"PenTool": "Инструмент перо",
|
||||
"EraserTool": "Инструмент ластик",
|
||||
"PanTool": "Инструмент перемещения",
|
||||
"TextTool": "Инструмент текст",
|
||||
"PenTool": "Перо",
|
||||
"EraserTool": "Ластик",
|
||||
"PanTool": "Перемещение",
|
||||
"TextTool": "Текст",
|
||||
"LineTool": "Линия",
|
||||
"RectangleTool": "Прямоугольник",
|
||||
"EllipseTool": "Эллипс",
|
||||
"PaletteManagementMenu": "Управление цветовыми пресетами"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user