Make stroke thickness per-stroke; restore Clear button

Each drawn stroke now stores its own width and color, so changing the
thickness slider no longer retroactively resizes existing strokes. The
Clear button is back in the draw toolbar.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
This commit is contained in:
2026-08-23 14:00:00 +08:00
parent 1961d0dc9c
commit 8b27263962
2 changed files with 31 additions and 15 deletions
+29 -13
View File
@@ -91,9 +91,9 @@ final class CaptureModel: ObservableObject {
/// Drag selection rect in IMAGE point space (nil = none)
@Published var selection: CGRect?
/// Freehand drawing strokes in IMAGE point space
@Published var strokes: [[CGPoint]] = []
@Published var strokes: [Stroke] = []
/// Redo stack of removed strokes
@Published var redoStack: [[CGPoint]] = []
@Published var redoStack: [Stroke] = []
/// Whether the pen is active (drag draws instead of selecting)
@Published var isDrawing = false
/// Current drawing color
@@ -101,6 +101,12 @@ final class CaptureModel: ObservableObject {
/// Stroke thickness in points (image space)
@Published var strokeWidth: CGFloat = 3.0
struct Stroke {
let points: [CGPoint]
let width: CGFloat
let color: NSColor
}
enum CaptureMode {
case screenshot
case text
@@ -143,15 +149,15 @@ final class CaptureModel: ObservableObject {
let ctx = NSGraphicsContext.current!.cgContext
// CGImage draws with a flipped Y in NSGraphicsContext
ctx.draw(image, in: NSRect(origin: .zero, size: size))
drawColor.setStroke()
ctx.setLineWidth(strokeWidth)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
for stroke in strokes {
guard stroke.count > 1 else { continue }
guard stroke.points.count > 1 else { continue }
stroke.color.setStroke()
ctx.setLineWidth(stroke.width)
ctx.beginPath()
ctx.move(to: stroke[0])
for point in stroke.dropFirst() {
ctx.move(to: stroke.points[0])
for point in stroke.points.dropFirst() {
ctx.addLine(to: point)
}
ctx.strokePath()
@@ -162,7 +168,7 @@ final class CaptureModel: ObservableObject {
func addStroke(_ points: [CGPoint]) {
guard points.count > 1 else { return }
strokes.append(points)
strokes.append(Stroke(points: points, width: strokeWidth, color: drawColor))
redoStack = []
}
@@ -387,6 +393,16 @@ struct ResultView: View {
.disabled(!model.canRedo)
.keyboardShortcut("z", modifiers: [.command, .shift])
.help("Redo stroke (⌘⇧Z)")
Button {
model.clearAnnotations()
} label: {
Label("Clear", systemImage: "eraser")
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(model.strokes.isEmpty)
.help("Remove all drawn annotations")
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
@@ -490,15 +506,15 @@ struct ScreenshotPane: View {
// Completed strokes
Canvas { context, _ in
var path = Path()
for stroke in model.strokes {
guard stroke.count > 1 else { continue }
path.move(to: CGPoint(x: origin.x + stroke[0].x * scale, y: origin.y + stroke[0].y * scale))
for point in stroke.dropFirst() {
guard stroke.points.count > 1 else { continue }
var path = Path()
path.move(to: CGPoint(x: origin.x + stroke.points[0].x * scale, y: origin.y + stroke.points[0].y * scale))
for point in stroke.points.dropFirst() {
path.addLine(to: CGPoint(x: origin.x + point.x * scale, y: origin.y + point.y * scale))
}
context.stroke(path, with: .color(Color(nsColor: stroke.color)), lineWidth: stroke.width)
}
context.stroke(path, with: .color(Color(nsColor: model.drawColor)), lineWidth: model.strokeWidth)
}
.allowsHitTesting(false)