Skip to content
This repository was archived by the owner on Apr 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 8 additions & 68 deletions packages/pkg.module.board/Board.tsx
Original file line number Diff line number Diff line change
@@ -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<ToolType>('pen');

const stageRef = useRef<Konva.Stage>(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<WheelEvent>) => {
setStagePosition(e.currentTarget.position());
handleWheel(e);
};

return (
<div className="flex h-full w-full flex-col">
<div className="relative flex-1 overflow-hidden">
<ZoomMenu zoomIn={handleZoomIn} zoomOut={handleZoomOut} resetZoom={handleResetZoom} />
<Stage
width={boardWidth}
height={boardHeight}
ref={stageRef}
className="bg-gray-0"
onWheel={handleOnWheel}
onDragEnd={(e) => {
setStagePosition(e.currentTarget.position());
}}
draggable
>
<BackgroundLayer />
<CanvasLayer boardElements={boardElements} selectedTool={selectedTool} />
</Stage>
</div>
</div>
);
};
import { Canvas } from './Canvas';
import { StageProvider } from './providers';

export const Board = () => (
<StageProvider>
<Canvas />
</StageProvider>
);
52 changes: 52 additions & 0 deletions packages/pkg.module.board/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex h-full w-full flex-col">
<div className="relative flex-1 overflow-hidden">
<ZoomMenu zoomIn={handleZoomIn} zoomOut={handleZoomOut} resetZoom={handleResetZoom} />
<Navbar />
<SelectedElementToolbar />
<Stage
width={boardWidth}
height={boardHeight}
ref={stageRef}
className="bg-gray-0"
onWheel={handleOnWheel}
onDragEnd={handleDragEnd}
draggable={selectedTool === 'hand'}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
<BackgroundLayer />
<CanvasLayer />
</Stage>
</div>
</div>
);
};
223 changes: 51 additions & 172 deletions packages/pkg.module.board/CanvasLayer.tsx
Original file line number Diff line number Diff line change
@@ -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<CanvasLayerProps> = ({ boardElements, selectedTool }) => {
// Для инструмента "ручка" будем сохранять текущую линию до завершения рисования
const [newLine, setNewLine] = useState<number[]>([]);
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 (
<Layer onMouseDown={handleMouseDown} onMousemove={handleMouseMove} onMouseup={handleMouseUp}>
{/* Отрисовка сохранённых элементов доски */}
{boardElements.map((el) => {
switch (el.type) {
case 'line':
return (
<Line
key={el.id}
points={el.points!}
stroke={el.stroke}
strokeWidth={el.strokeWidth}
opacity={el.opacity}
lineCap="round"
lineJoin="round"
draggable
onDragEnd={() => {
// Можно обновлять положение линии, если требуется
// updateElement(el.id, { ... });
}}
/>
);
case 'rectangle':
return (
<Rect
key={el.id}
x={el.x}
y={el.y}
width={el.width}
height={el.height}
stroke={el.stroke}
fill={el.fill}
draggable
onDragEnd={(e) => {
updateElement(el.id, { x: e.target.x(), y: e.target.y() });
}}
/>
);
case 'circle':
return (
<Circle
key={el.id}
x={el.x}
y={el.y}
radius={el.radius}
stroke={el.stroke}
fill={el.fill}
draggable
onDragEnd={(e) => {
updateElement(el.id, { x: e.target.x(), y: e.target.y() });
}}
/>
);
case 'text':
return (
<Text
key={el.id}
x={el.x}
y={el.y}
text={el.text}
fontSize={el.fontSize}
fontFamily={el.fontFamily}
fill={el.fill}
draggable
onDragEnd={(e) => {
updateElement(el.id, { x: e.target.x(), y: e.target.y() });
}}
/>
);
case 'sticker':
return (
<React.Fragment key={el.id}>
<Rect
x={el.x}
y={el.y}
width={el.width}
height={el.height}
fill={el.backgroundColor}
stroke="black"
draggable
onDragEnd={(e) => {
updateElement(el.id, { x: e.target.x(), y: e.target.y() });
}}
/>
<Text
x={el.x! + 10}
y={el.y! + 10}
text={el.text}
fontSize={el.fontSize}
fontFamily={el.fontFamily}
fill={el.textColor}
draggable
/>
</React.Fragment>
);
case 'image':
return <BoardImage key={el.id} el={el} updateElement={updateElement} />;
default:
return null;
}
})}

{/* Отрисовка временной линии при рисовании */}
{isDrawing && selectedTool === 'pen' && (
<Line points={newLine} stroke="#000000" strokeWidth={2} lineCap="round" lineJoin="round" />
<Layer ref={layerRef}>
{boardElements.map((element) =>
element.type === 'line' ? <LineShape key={element.id} element={element} /> : null,
)}
{selectedElementId && selectedTool === 'select' && (
<Transformer
ref={transformerRef}
rotateEnabled={false}
anchorCornerRadius={8}
anchorStroke="#070707"
borderStroke="#070707"
borderDash={[5, 5]}
padding={8}
enabledAnchors={['top-left', 'top-right', 'bottom-left', 'bottom-right']}
onTransform={onChangeTransformerPosition}
onDragMove={onChangeTransformerPosition}
/>
)}
</Layer>
);
};

export default CanvasLayer;
});

// Вспомогательный компонент для отрисовки изображений
interface BoardImageProps {
el: BoardElement;
updateElement: (id: string, updates: Partial<BoardElement>) => void;
}

const BoardImage: React.FC<BoardImageProps> = ({ el, updateElement }) => {
const [image] = useImage(el.src!);
return (
<Image
image={image}
x={el.x}
y={el.y}
width={el.width}
height={el.height}
draggable
onDragEnd={(e) => {
updateElement(el.id, { x: e.target.x(), y: e.target.y() });
}}
/>
);
};
CanvasLayer.displayName = 'CanvasLayer';
Loading