diff --git a/packages/pkg.module.board/Board.tsx b/packages/pkg.module.board/Board.tsx index ad60c508..bf769724 100644 --- a/packages/pkg.module.board/Board.tsx +++ b/packages/pkg.module.board/Board.tsx @@ -1,68 +1,8 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// components/Whiteboard.tsx -import React, { useState, useRef, useEffect } from 'react'; -import { Stage } from 'react-konva'; -import Konva from 'konva'; -import CanvasLayer from './CanvasLayer'; -import { ToolType } from './types'; -import { useBoardStore, useUIStore } from './store'; -import { useZoom } from './hooks'; -import { BackgroundLayer } from './components'; -import { ZoomMenu } from './components/ZoomMenu'; - -export const Board: React.FC = () => { - // Выбранный инструмент - const [selectedTool, setSelectedTool] = useState('pen'); - - const stageRef = useRef(null); - - const { boardElements } = useBoardStore(); - const { setStagePosition } = useUIStore(); - - const boardWidth = window.innerWidth; - const boardHeight = window.innerHeight; - - // Пример хоткеев: Escape – переключиться в режим выделения, - // Delete – удалить выделенные элементы (реализовать логику выбора) - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - setSelectedTool('select'); - } - if (e.key === 'Delete') { - // TODO: удалить выбранные элементы (логика выделения реализуется дополнительно) - } - }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, []); - - const { handleWheel, handleZoomIn, handleZoomOut, handleResetZoom } = useZoom(stageRef); - - const handleOnWheel = (e: Konva.KonvaEventObject) => { - setStagePosition(e.currentTarget.position()); - handleWheel(e); - }; - - return ( -
-
- - { - setStagePosition(e.currentTarget.position()); - }} - draggable - > - - - -
-
- ); -}; +import { Canvas } from './Canvas'; +import { StageProvider } from './providers'; + +export const Board = () => ( + + + +); diff --git a/packages/pkg.module.board/Canvas.tsx b/packages/pkg.module.board/Canvas.tsx new file mode 100644 index 00000000..a602063e --- /dev/null +++ b/packages/pkg.module.board/Canvas.tsx @@ -0,0 +1,52 @@ +// components/Whiteboard.tsx +import { Stage } from 'react-konva'; +import { useKeyPress } from 'pkg.utils.client'; +import { useBoardStore } from './store'; +import { useCanvasHandlers, useZoom } from './hooks'; +import { useStage } from './providers'; +import { BackgroundLayer, SelectedElementToolbar, Navbar, ZoomMenu } from './components'; +import { CanvasLayer } from './CanvasLayer'; + +export const Canvas = () => { + const { stageRef } = useStage(); + const { selectedTool, removeElement, selectedElementId, selectElement } = useBoardStore(); + const { handleOnWheel, handleMouseUp, handleMouseDown, handleMouseMove, handleDragEnd } = + useCanvasHandlers(); + + const { handleResetZoom, handleZoomIn, handleZoomOut } = useZoom(stageRef); + + const boardWidth = window.innerWidth; + const boardHeight = window.innerHeight; + + useKeyPress('Backspace', () => { + if (selectedElementId) { + removeElement(selectedElementId); + selectElement(null); + } + }); + + return ( +
+
+ + + + + + + +
+
+ ); +}; diff --git a/packages/pkg.module.board/CanvasLayer.tsx b/packages/pkg.module.board/CanvasLayer.tsx index 267c9468..0e8b5d80 100644 --- a/packages/pkg.module.board/CanvasLayer.tsx +++ b/packages/pkg.module.board/CanvasLayer.tsx @@ -1,185 +1,64 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// components/CanvasLayer.tsx -import React, { useState } from 'react'; -import { Layer, Line, Rect, Text, Image, Circle } from 'react-konva'; -import useImage from 'use-image'; -import { BoardElement, ToolType } from './types'; +import { memo, useEffect } from 'react'; +import { Layer, Transformer } from 'react-konva'; import { useBoardStore } from './store'; +import { LineShape } from './components/Shapes'; +import { useStage } from './providers'; +import { useElementHandlers, useIsStageScaling } from './hooks'; -interface CanvasLayerProps { - boardElements: BoardElement[]; - selectedTool: ToolType; -} +export const CanvasLayer = memo(() => { + const { boardElements, selectedElementId, selectedTool, setSelectToolbarPosition } = + useBoardStore(); -const CanvasLayer: React.FC = ({ boardElements, selectedTool }) => { - // Для инструмента "ручка" будем сохранять текущую линию до завершения рисования - const [newLine, setNewLine] = useState([]); - const [isDrawing, setIsDrawing] = useState(false); - const { addElement, updateElement } = useBoardStore(); + const { layerRef, transformerRef } = useStage(); + const { onChangeTransformerPosition } = useElementHandlers(); + const { isScaling } = useIsStageScaling(); - const handleMouseDown = (e: any) => { - if (selectedTool === 'pen') { - setIsDrawing(true); - const pos = e.target.getStage().getPointerPosition(); - setNewLine([pos.x, pos.y]); + useEffect(() => { + if (transformerRef.current && selectedElementId) { + const selectedNode = layerRef.current?.findOne(`#${selectedElementId}`); + if (selectedNode) { + transformerRef.current.nodes([selectedNode]); + transformerRef.current.getLayer()?.batchDraw(); + const box = transformerRef.current.getClientRect(); + setSelectToolbarPosition({ + x: box.x, + y: box.y, + }); + } else { + transformerRef.current.nodes([]); + } + } else { + transformerRef.current?.nodes([]); } - // TODO: обработка других инструментов (например, создание стикера, текста и пр.) - }; + }, [layerRef, selectedElementId, setSelectToolbarPosition, transformerRef]); - const handleMouseMove = (e: any) => { - if (!isDrawing || selectedTool !== 'pen') return; - const pos = e.target.getStage().getPointerPosition(); - setNewLine((prev) => [...prev, pos.x, pos.y]); - }; - - const handleMouseUp = () => { - if (selectedTool === 'pen' && isDrawing) { - // Добавляем новый элемент линии - const id = new Date().getTime().toString(); - addElement({ - id, - type: 'line', - points: newLine, - stroke: '#000000', // можно добавить выбор цвета - strokeWidth: 2, - opacity: 1, - }); - setNewLine([]); - setIsDrawing(false); + useEffect(() => { + if (!isScaling && selectedElementId) { + onChangeTransformerPosition(); } - }; + }, [isScaling, selectedElementId, onChangeTransformerPosition]); return ( - - {/* Отрисовка сохранённых элементов доски */} - {boardElements.map((el) => { - switch (el.type) { - case 'line': - return ( - { - // Можно обновлять положение линии, если требуется - // updateElement(el.id, { ... }); - }} - /> - ); - case 'rectangle': - return ( - { - updateElement(el.id, { x: e.target.x(), y: e.target.y() }); - }} - /> - ); - case 'circle': - return ( - { - updateElement(el.id, { x: e.target.x(), y: e.target.y() }); - }} - /> - ); - case 'text': - return ( - { - updateElement(el.id, { x: e.target.x(), y: e.target.y() }); - }} - /> - ); - case 'sticker': - return ( - - { - updateElement(el.id, { x: e.target.x(), y: e.target.y() }); - }} - /> - - - ); - case 'image': - return ; - default: - return null; - } - })} - - {/* Отрисовка временной линии при рисовании */} - {isDrawing && selectedTool === 'pen' && ( - + + {boardElements.map((element) => + element.type === 'line' ? : null, + )} + {selectedElementId && selectedTool === 'select' && ( + )} ); -}; - -export default CanvasLayer; +}); -// Вспомогательный компонент для отрисовки изображений -interface BoardImageProps { - el: BoardElement; - updateElement: (id: string, updates: Partial) => void; -} - -const BoardImage: React.FC = ({ el, updateElement }) => { - const [image] = useImage(el.src!); - return ( - { - updateElement(el.id, { x: e.target.x(), y: e.target.y() }); - }} - /> - ); -}; +CanvasLayer.displayName = 'CanvasLayer'; diff --git a/packages/pkg.module.board/components/BackgroundLayer.tsx b/packages/pkg.module.board/components/BackgroundLayer.tsx index c6c2bcd5..9d115974 100644 --- a/packages/pkg.module.board/components/BackgroundLayer.tsx +++ b/packages/pkg.module.board/components/BackgroundLayer.tsx @@ -3,9 +3,9 @@ import React, { useEffect, useMemo } from 'react'; import { Layer, Shape } from 'react-konva'; import { useUIStore } from '../store'; -import { baseDotSize, baseGridStep, minDotSize } from '../const'; +import { gridConfig } from '../utils'; -export const BackgroundLayer = () => { +const BackgroundLayerComponent = () => { const { viewport, setViewport, stagePosition, scale } = useUIStore(); useEffect(() => { @@ -19,6 +19,10 @@ export const BackgroundLayer = () => { }, [setViewport]); const dots = useMemo(() => { + if (!viewport) return null; + + const { baseGridStep, baseDotSize, minDotSize } = gridConfig; + const visibleWidth = viewport.width / scale; const visibleHeight = viewport.height / scale; @@ -45,7 +49,7 @@ export const BackgroundLayer = () => { return ( { - context.fillStyle = '#e8e8e8'; + context.fillStyle = gridConfig.dotFill; for (let x = startX; x <= endX; x += gridStep) { for (let y = startY; y <= endY; y += gridStep) { context.beginPath(); @@ -56,7 +60,9 @@ export const BackgroundLayer = () => { }} /> ); - }, [viewport.width, viewport.height, scale, stagePosition]); + }, [viewport, scale, stagePosition.x, stagePosition.y]); return {dots}; }; + +export const BackgroundLayer = React.memo(BackgroundLayerComponent); diff --git a/packages/pkg.module.board/components/Navbar.tsx b/packages/pkg.module.board/components/Navbar.tsx index d383f0f3..8ae2c021 100644 --- a/packages/pkg.module.board/components/Navbar.tsx +++ b/packages/pkg.module.board/components/Navbar.tsx @@ -3,18 +3,23 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@xipkg import { NavbarAction } from './NavbarAction'; // import { StickerPopupContent } from './StickerPopupContent'; import { navBarElements, NavbarElementT } from '../utils/navBarElements'; +import { useBoardStore } from '../store'; +import { ToolType } from '../types'; // import { StylePopupContent } from './StylePopupContent'; export const Navbar = () => { const [isTooltipOpen] = React.useState(false); + const { setSelectedTool, selectedTool } = useBoardStore(); - const resetStyles = () => {}; + // const resetStyles = () => {}; - // const hanleTool = () => {}; + const handleSelectTool = (toolName: ToolType) => { + setSelectedTool(toolName); + }; return (
-
+
@@ -22,8 +27,7 @@ export const Navbar = () => {
{navBarElements.map((item: NavbarElementT) => { - // const isActive = editor.getCurrentToolId() === item.action; - const isActive = true; + const isActive = item.action === selectedTool; return ( @@ -34,7 +38,7 @@ export const Navbar = () => { className={`pointer-events-auto flex h-6 w-6 items-center justify-center rounded-lg lg:h-8 lg:w-8 ${isActive ? 'bg-brand-0' : 'bg-gray-0'}`} data-isactive={isActive} onClick={() => { - resetStyles(); + handleSelectTool(item.action as ToolType); }} > {item.icon ? item.icon : item.title} @@ -43,11 +47,11 @@ export const Navbar = () => { {/* {editor.getCurrentToolId() === 'sticker' && ( - )} - {editor.getCurrentToolId() === 'draw' && ( + )} */} + {/* {selectedTool === 'pen' && ( )} */} - 1 + {item.title}
diff --git a/packages/pkg.module.board/components/NavbarAction.tsx b/packages/pkg.module.board/components/NavbarAction.tsx index c078845a..1e88b098 100644 --- a/packages/pkg.module.board/components/NavbarAction.tsx +++ b/packages/pkg.module.board/components/NavbarAction.tsx @@ -1,8 +1,6 @@ import { Undo, Redo } from '@xipkg/icons'; export const NavbarAction = () => { - console.log('NavbarAction'); - const canUndo = false; const canRedo = false; return ( diff --git a/packages/pkg.module.board/components/SelectedElementToolbar.tsx b/packages/pkg.module.board/components/SelectedElementToolbar.tsx new file mode 100644 index 00000000..4c4c3300 --- /dev/null +++ b/packages/pkg.module.board/components/SelectedElementToolbar.tsx @@ -0,0 +1,46 @@ +import { Button } from '@xipkg/button'; +import { Trash, Copy, MoreVert } from '@xipkg/icons'; +import { useBoardStore } from '../store'; +import { useIsStageScaling } from '../hooks'; + +export const SelectedElementToolbar = () => { + const { selectToolbarPosition, selectedTool, selectedElementId, removeElement, selectElement } = + useBoardStore(); + + const { isScaling } = useIsStageScaling(); + + if (!selectedElementId || selectedTool !== 'select') { + return null; + } + + return ( +
+ + + +
+ ); +}; diff --git a/packages/pkg.module.board/components/Shapes/LineShape.tsx b/packages/pkg.module.board/components/Shapes/LineShape.tsx new file mode 100644 index 00000000..9fd2fa80 --- /dev/null +++ b/packages/pkg.module.board/components/Shapes/LineShape.tsx @@ -0,0 +1,31 @@ +import { memo } from 'react'; +import { Line } from 'react-konva'; +import { useBoardStore, useUIStore } from '../../store'; +import { BoardElement } from '../../types'; +import { useElementHandlers } from '../../hooks'; + +export const LineShape = memo(({ element }: { element: BoardElement }) => { + const { selectedElementId } = useBoardStore(); + const { handleSelect, handleDragEnd } = useElementHandlers(); + const { scale } = useUIStore(); + + const hitStrokeWidth = Math.max(20, Math.min(40, 20 / scale)); + + return ( + handleSelect(e, element.id)} + draggable={selectedElementId === element.id} + onDragEnd={(e) => handleDragEnd(e, element)} + /> + ); +}); + +LineShape.displayName = 'LineShape'; diff --git a/packages/pkg.module.board/components/Shapes/index.ts b/packages/pkg.module.board/components/Shapes/index.ts new file mode 100644 index 00000000..3d2a6b12 --- /dev/null +++ b/packages/pkg.module.board/components/Shapes/index.ts @@ -0,0 +1 @@ +export { LineShape } from './LineShape'; diff --git a/packages/pkg.module.board/components/ZoomMenu.tsx b/packages/pkg.module.board/components/ZoomMenu.tsx index bae665c5..228071aa 100644 --- a/packages/pkg.module.board/components/ZoomMenu.tsx +++ b/packages/pkg.module.board/components/ZoomMenu.tsx @@ -13,24 +13,23 @@ export const ZoomMenu = ({ zoomIn, zoomOut, resetZoom }: ZoomMenuPropsT) => { return (
-
+