@@ -5,7 +5,10 @@ import { useCallback, useRef, useState, useEffect, useMemo } from 'react';
55import { Plus , Trash2 , Copy , Eraser , CheckSquare } from 'lucide-react' ;
66import FreeTextBlock from './FreeTextBlock' ;
77import { FreeCanvasOverlays } from './FreeCanvasOverlays' ;
8- import type { FreeCanvasData , FreeCanvasBlock } from '@/types/models' ;
8+ import { InkToolbar } from './canvas/InkToolbar' ;
9+ import { InkLayer } from './canvas/InkLayer' ;
10+ import { useInkDrawing , type InkTool } from '../lib/canvas/useInkDrawing' ;
11+ import type { FreeCanvasData , FreeCanvasBlock , InkStroke , InkPoint } from '@/types/models' ;
912
1013interface FreeCanvasProps {
1114 content : FreeCanvasData | null ;
@@ -44,6 +47,44 @@ export default function FreeCanvas({ content, onChange }: FreeCanvasProps) {
4447 const [ selectedBlockIds , setSelectedBlockIds ] = useState < Set < string > > ( new Set ( ) ) ;
4548 const [ paletteOpen , setPaletteOpen ] = useState ( false ) ;
4649
50+ // 阶段三:墨迹工具状态 / ink tool state
51+ const [ inkTool , setInkTool ] = useState < InkTool > ( 'select' ) ;
52+ const [ inkColor , setInkColor ] = useState ( '#1e293b' ) ;
53+ const [ inkWidth , setInkWidth ] = useState ( 4 ) ;
54+ const innerCanvasRef = useRef < HTMLDivElement > ( null ) ;
55+
56+ // 阶段三:墨迹笔画数据与绘制 / ink strokes data & drawing
57+ const strokes = data . strokes ?? [ ] ;
58+
59+ const getCanvasPoint = useCallback ( ( clientX : number , clientY : number ) : InkPoint => {
60+ const rect = innerCanvasRef . current ?. getBoundingClientRect ( ) ;
61+ if ( ! rect ) return { x : 0 , y : 0 } ;
62+ return { x : clientX - rect . left , y : clientY - rect . top } ;
63+ } , [ ] ) ;
64+
65+ const handleCommitStroke = useCallback ( ( stroke : InkStroke ) => {
66+ const current = dataRef . current ;
67+ emitChange ( { ...current , strokes : [ ...( current . strokes ?? [ ] ) , stroke ] } ) ;
68+ } , [ emitChange ] ) ;
69+
70+ const handleErase = useCallback ( ( predicate : ( s : InkStroke ) => boolean ) => {
71+ const current = dataRef . current ;
72+ const prev = current . strokes ?? [ ] ;
73+ const remaining = prev . filter ( ( s ) => ! predicate ( s ) ) ;
74+ if ( remaining . length !== prev . length ) {
75+ emitChange ( { ...current , strokes : remaining } ) ;
76+ }
77+ } , [ emitChange ] ) ;
78+
79+ const { currentStroke, handlePointerDown, handlePointerMove, handlePointerUp } = useInkDrawing ( {
80+ tool : inkTool ,
81+ color : inkColor ,
82+ width : inkWidth ,
83+ getCanvasPoint,
84+ onCommitStroke : handleCommitStroke ,
85+ onErase : handleErase ,
86+ } ) ;
87+
4788 // 框选状态
4889 const [ selectionBox , setSelectionBox ] = useState < { x1 : number ; y1 : number ; x2 : number ; y2 : number } | null > ( null ) ;
4990 const isSelectingRef = useRef ( false ) ;
@@ -125,6 +166,7 @@ export default function FreeCanvas({ content, onChange }: FreeCanvasProps) {
125166
126167 // 双击空白区域添加文本块
127168 const handleCanvasDoubleClick = ( e : React . MouseEvent ) => {
169+ if ( inkTool !== 'select' ) return ; // 绘制工具下不新建文本块
128170 const scrollContainer = scrollContainerRef . current ;
129171 if ( ! scrollContainer ) return ;
130172
@@ -489,6 +531,7 @@ export default function FreeCanvas({ content, onChange }: FreeCanvasProps) {
489531 onContextMenu = { ( e ) => e . preventDefault ( ) }
490532 >
491533 < div
534+ ref = { innerCanvasRef }
492535 style = { {
493536 position : 'relative' ,
494537 width : data . canvasWidth ,
@@ -511,6 +554,26 @@ export default function FreeCanvas({ content, onChange }: FreeCanvasProps) {
511554 />
512555 ) ) }
513556
557+ { /* 阶段三:墨迹渲染层 / ink rendering layer */ }
558+ < InkLayer
559+ strokes = { strokes }
560+ currentStroke = { currentStroke }
561+ width = { data . canvasWidth }
562+ height = { data . canvasHeight }
563+ />
564+
565+ { /* 阶段三:绘制覆盖层(绘制工具激活时捕获指针事件) */ }
566+ { inkTool !== 'select' && (
567+ < div
568+ className = "absolute inset-0"
569+ style = { { zIndex : 8 , cursor : inkTool === 'eraser' ? 'cell' : 'crosshair' , touchAction : 'none' } }
570+ onPointerDown = { handlePointerDown }
571+ onPointerMove = { handlePointerMove }
572+ onPointerUp = { handlePointerUp }
573+ onPointerLeave = { handlePointerUp }
574+ />
575+ ) }
576+
514577 { /* 框选矩形 */ }
515578 { selectionBox && (
516579 < div
@@ -534,6 +597,16 @@ export default function FreeCanvas({ content, onChange }: FreeCanvasProps) {
534597 actions = { actions }
535598 onClosePalette = { ( ) => setPaletteOpen ( false ) }
536599 />
600+
601+ { /* 阶段三:墨迹工具栏 / ink toolbar */ }
602+ < InkToolbar
603+ tool = { inkTool }
604+ color = { inkColor }
605+ width = { inkWidth }
606+ onToolChange = { setInkTool }
607+ onColorChange = { setInkColor }
608+ onWidthChange = { setInkWidth }
609+ />
537610 </ div >
538611 ) ;
539612}
0 commit comments