Skip to content

Commit db0bfc1

Browse files
committed
feat(screenshot): add freehand drawing tool with multi-point path support
- Add freehand tool to AnnotationTool enum - Extend AnnotationShape with optional points array for multi-point paths - Implement freehand rendering with smooth line joins and round caps - Update AnnotationView to collect points during mouse drag for freehand tool - Add freehand button to toolbar with pencil.tip icon - Support continuous drawing by appending points during mouseDragged - Validate freehand shapes require at least 2 points before committing - Add localization support: Freehand/画笔/畫筆/ペン - Update toolbar width calculation for 4 tools (rect/circle/arrow/freehand)
1 parent 9553e76 commit db0bfc1

3 files changed

Lines changed: 106 additions & 22 deletions

File tree

DockMasterPro/Plugins/ScreenshotPlugin/ScreenshotAnnotationModel.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ enum AnnotationTool: Equatable {
77
case rectangle
88
case arrow // 箭头工具
99
case circle // 圆形/椭圆工具
10+
case freehand // 画笔工具(自由绘制)
1011
}
1112

1213
// MARK: - Annotation Shape
@@ -17,6 +18,7 @@ struct AnnotationShape {
1718
var endPoint: NSPoint
1819
var color: NSColor
1920
var lineWidth: CGFloat
21+
var points: [NSPoint]? // 用于画笔工具的多点路径
2022

2123
// Shared rendering — draws into whatever NSGraphicsContext is current
2224
static func render(_ shape: AnnotationShape) {
@@ -104,6 +106,27 @@ struct AnnotationShape {
104106
shape.color.setFill()
105107
path.stroke()
106108
path.fill()
109+
110+
case .freehand:
111+
// 绘制自由画笔:连接所有点的平滑曲线
112+
guard let points = shape.points, points.count >= 2 else { return }
113+
114+
let path = NSBezierPath()
115+
116+
// 移动到第一个点
117+
path.move(to: points[0])
118+
119+
// 连接所有点
120+
for i in 1..<points.count {
121+
path.line(to: points[i])
122+
}
123+
124+
// 设置样式并绘制
125+
path.lineWidth = shape.lineWidth
126+
path.lineCapStyle = .round // 圆角端点,让线条更平滑
127+
path.lineJoinStyle = .round // 圆角连接,让转角更平滑
128+
shape.color.setStroke()
129+
path.stroke()
107130
}
108131
}
109132
}

DockMasterPro/Plugins/ScreenshotPlugin/ScreenshotAnnotationView.swift

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,53 @@ final class ScreenshotAnnotationView: NSView {
6969
onDrawingStarted?()
7070

7171
let pt = localPoint(from: event)
72-
activeShape = AnnotationShape(
73-
tool: tool,
74-
startPoint: pt,
75-
endPoint: pt,
76-
color: session.currentColor,
77-
lineWidth: session.currentLineWidth
78-
)
72+
73+
// 画笔工具需要初始化点数组
74+
if tool == .freehand {
75+
activeShape = AnnotationShape(
76+
tool: tool,
77+
startPoint: pt,
78+
endPoint: pt,
79+
color: session.currentColor,
80+
lineWidth: session.currentLineWidth,
81+
points: [pt] // 初始化点数组
82+
)
83+
} else {
84+
activeShape = AnnotationShape(
85+
tool: tool,
86+
startPoint: pt,
87+
endPoint: pt,
88+
color: session.currentColor,
89+
lineWidth: session.currentLineWidth,
90+
points: nil
91+
)
92+
}
7993
needsDisplay = true
8094
}
8195

8296
override func mouseDragged(with event: NSEvent) {
8397
// 如果没有激活形状,将事件传递给下一个响应者
84-
guard activeShape != nil else {
98+
guard var shape = activeShape else {
8599
nextResponder?.mouseDragged(with: event)
86100
return
87101
}
88102

89-
activeShape?.endPoint = localPoint(from: event)
103+
let pt = localPoint(from: event)
104+
105+
// 画笔工具:添加新点到路径
106+
if shape.tool == .freehand {
107+
if shape.points == nil {
108+
shape.points = [shape.startPoint]
109+
}
110+
shape.points?.append(pt)
111+
shape.endPoint = pt // 更新终点
112+
activeShape = shape
113+
} else {
114+
// 其他工具:只更新终点
115+
shape.endPoint = pt
116+
activeShape = shape
117+
}
118+
90119
needsDisplay = true
91120
}
92121

@@ -97,12 +126,34 @@ final class ScreenshotAnnotationView: NSView {
97126
return
98127
}
99128

100-
shape.endPoint = localPoint(from: event)
129+
let pt = localPoint(from: event)
130+
131+
// 画笔工具:添加最后一个点
132+
if shape.tool == .freehand {
133+
if shape.points == nil {
134+
shape.points = [shape.startPoint]
135+
}
136+
shape.points?.append(pt)
137+
shape.endPoint = pt
138+
} else {
139+
shape.endPoint = pt
140+
}
141+
101142
activeShape = nil
102143

103-
let dx = abs(shape.endPoint.x - shape.startPoint.x)
104-
let dy = abs(shape.endPoint.y - shape.startPoint.y)
105-
if dx > 3 || dy > 3 {
144+
// 检查形状是否足够大
145+
let isValid: Bool
146+
if shape.tool == .freehand {
147+
// 画笔工具:至少需要2个点
148+
isValid = (shape.points?.count ?? 0) >= 2
149+
} else {
150+
// 其他工具:检查宽高
151+
let dx = abs(shape.endPoint.x - shape.startPoint.x)
152+
let dy = abs(shape.endPoint.y - shape.startPoint.y)
153+
isValid = dx > 3 || dy > 3
154+
}
155+
156+
if isValid {
106157
session.commit(shape)
107158
onSessionChanged?()
108159
// 通知形状已提交到画布(此时锁定选区)

DockMasterPro/Plugins/ScreenshotPlugin/ScreenshotEditToolbar.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ final class ScreenshotEditToolbar: NSView {
2727
private static let leftPadding: CGFloat = 8
2828
private static let rightPadding: CGFloat = 8
2929

30-
// 工具按钮数量(矩形、圆形、箭头)
31-
private static let toolButtonCount: Int = 3
30+
// 工具按钮数量(矩形、圆形、箭头、画笔
31+
private static let toolButtonCount: Int = 4
3232

3333
// 计算总宽度
3434
static var width: CGFloat {
@@ -126,6 +126,15 @@ final class ScreenshotEditToolbar: NSView {
126126
action: #selector(toolBtnClicked(_:))
127127
)
128128
toolButtons.append((.arrow, arrowBtn))
129+
toolX += bs + gap
130+
131+
let freehandBtn = addBtn(
132+
systemName: "pencil.tip",
133+
tooltip: L("edit.tool.freehand"),
134+
frame: NSRect(x: toolX, y: cy, width: bs, height: bs),
135+
action: #selector(toolBtnClicked(_:))
136+
)
137+
toolButtons.append((.freehand, freehandBtn))
129138

130139
// ── Right group: undo/redo | copy cancel save ─────────────────────
131140
let undoRedoW = bs + gap + bs
@@ -293,13 +302,14 @@ final class ScreenshotEditToolbar: NSView {
293302

294303
static func L(_ key: String) -> String {
295304
let table: [String: [String: String]] = [
296-
"edit.tool.rect": ["en": "Rectangle", "zh-Hans": "矩形", "zh-Hant": "矩形", "ja": "矩形"],
297-
"edit.tool.circle": ["en": "Circle", "zh-Hans": "圆形", "zh-Hant": "圓形", "ja": ""],
298-
"edit.tool.arrow": ["en": "Arrow", "zh-Hans": "箭头", "zh-Hant": "箭頭", "ja": "矢印"],
299-
"edit.undo": ["en": "Undo", "zh-Hans": "撤销", "zh-Hant": "復原", "ja": "元に戻す"],
300-
"edit.redo": ["en": "Redo", "zh-Hans": "重做", "zh-Hant": "重做", "ja": "やり直し"],
301-
"toolbar.copy": ["en": "Copy", "zh-Hans": "复制", "zh-Hant": "複製", "ja": "コピー"],
302-
"toolbar.cancel": ["en": "Cancel", "zh-Hans": "取消", "zh-Hant": "取消", "ja": "キャンセル"],
305+
"edit.tool.rect": ["en": "Rectangle", "zh-Hans": "矩形", "zh-Hant": "矩形", "ja": "矩形"],
306+
"edit.tool.circle": ["en": "Circle", "zh-Hans": "圆形", "zh-Hant": "圓形", "ja": ""],
307+
"edit.tool.arrow": ["en": "Arrow", "zh-Hans": "箭头", "zh-Hant": "箭頭", "ja": "矢印"],
308+
"edit.tool.freehand": ["en": "Freehand", "zh-Hans": "画笔", "zh-Hant": "畫筆", "ja": "ペン"],
309+
"edit.undo": ["en": "Undo", "zh-Hans": "撤销", "zh-Hant": "復原", "ja": "元に戻す"],
310+
"edit.redo": ["en": "Redo", "zh-Hans": "重做", "zh-Hant": "重做", "ja": "やり直し"],
311+
"toolbar.copy": ["en": "Copy", "zh-Hans": "复制", "zh-Hant": "複製", "ja": "コピー"],
312+
"toolbar.cancel": ["en": "Cancel", "zh-Hans": "取消", "zh-Hant": "取消", "ja": "キャンセル"],
303313
"toolbar.save": ["en": "Save", "zh-Hans": "保存", "zh-Hant": "儲存", "ja": "保存"],
304314
]
305315
let pref = Locale.preferredLanguages.first ?? "en"

0 commit comments

Comments
 (0)