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:
@@ -91,9 +91,9 @@ final class CaptureModel: ObservableObject {
|
|||||||
/// Drag selection rect in IMAGE point space (nil = none)
|
/// Drag selection rect in IMAGE point space (nil = none)
|
||||||
@Published var selection: CGRect?
|
@Published var selection: CGRect?
|
||||||
/// Freehand drawing strokes in IMAGE point space
|
/// Freehand drawing strokes in IMAGE point space
|
||||||
@Published var strokes: [[CGPoint]] = []
|
@Published var strokes: [Stroke] = []
|
||||||
/// Redo stack of removed strokes
|
/// Redo stack of removed strokes
|
||||||
@Published var redoStack: [[CGPoint]] = []
|
@Published var redoStack: [Stroke] = []
|
||||||
/// Whether the pen is active (drag draws instead of selecting)
|
/// Whether the pen is active (drag draws instead of selecting)
|
||||||
@Published var isDrawing = false
|
@Published var isDrawing = false
|
||||||
/// Current drawing color
|
/// Current drawing color
|
||||||
@@ -101,6 +101,12 @@ final class CaptureModel: ObservableObject {
|
|||||||
/// Stroke thickness in points (image space)
|
/// Stroke thickness in points (image space)
|
||||||
@Published var strokeWidth: CGFloat = 3.0
|
@Published var strokeWidth: CGFloat = 3.0
|
||||||
|
|
||||||
|
struct Stroke {
|
||||||
|
let points: [CGPoint]
|
||||||
|
let width: CGFloat
|
||||||
|
let color: NSColor
|
||||||
|
}
|
||||||
|
|
||||||
enum CaptureMode {
|
enum CaptureMode {
|
||||||
case screenshot
|
case screenshot
|
||||||
case text
|
case text
|
||||||
@@ -143,15 +149,15 @@ final class CaptureModel: ObservableObject {
|
|||||||
let ctx = NSGraphicsContext.current!.cgContext
|
let ctx = NSGraphicsContext.current!.cgContext
|
||||||
// CGImage draws with a flipped Y in NSGraphicsContext
|
// CGImage draws with a flipped Y in NSGraphicsContext
|
||||||
ctx.draw(image, in: NSRect(origin: .zero, size: size))
|
ctx.draw(image, in: NSRect(origin: .zero, size: size))
|
||||||
drawColor.setStroke()
|
|
||||||
ctx.setLineWidth(strokeWidth)
|
|
||||||
ctx.setLineCap(.round)
|
ctx.setLineCap(.round)
|
||||||
ctx.setLineJoin(.round)
|
ctx.setLineJoin(.round)
|
||||||
for stroke in strokes {
|
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.beginPath()
|
||||||
ctx.move(to: stroke[0])
|
ctx.move(to: stroke.points[0])
|
||||||
for point in stroke.dropFirst() {
|
for point in stroke.points.dropFirst() {
|
||||||
ctx.addLine(to: point)
|
ctx.addLine(to: point)
|
||||||
}
|
}
|
||||||
ctx.strokePath()
|
ctx.strokePath()
|
||||||
@@ -162,7 +168,7 @@ final class CaptureModel: ObservableObject {
|
|||||||
|
|
||||||
func addStroke(_ points: [CGPoint]) {
|
func addStroke(_ points: [CGPoint]) {
|
||||||
guard points.count > 1 else { return }
|
guard points.count > 1 else { return }
|
||||||
strokes.append(points)
|
strokes.append(Stroke(points: points, width: strokeWidth, color: drawColor))
|
||||||
redoStack = []
|
redoStack = []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +393,16 @@ struct ResultView: View {
|
|||||||
.disabled(!model.canRedo)
|
.disabled(!model.canRedo)
|
||||||
.keyboardShortcut("z", modifiers: [.command, .shift])
|
.keyboardShortcut("z", modifiers: [.command, .shift])
|
||||||
.help("Redo stroke (⌘⇧Z)")
|
.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(.horizontal, 14)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
@@ -490,15 +506,15 @@ struct ScreenshotPane: View {
|
|||||||
|
|
||||||
// Completed strokes
|
// Completed strokes
|
||||||
Canvas { context, _ in
|
Canvas { context, _ in
|
||||||
var path = Path()
|
|
||||||
for stroke in model.strokes {
|
for stroke in model.strokes {
|
||||||
guard stroke.count > 1 else { continue }
|
guard stroke.points.count > 1 else { continue }
|
||||||
path.move(to: CGPoint(x: origin.x + stroke[0].x * scale, y: origin.y + stroke[0].y * scale))
|
var path = Path()
|
||||||
for point in stroke.dropFirst() {
|
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))
|
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)
|
.allowsHitTesting(false)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
|
|||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>Icon</string>
|
<string>Icon</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.4.1</string>
|
<string>1.4.2</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>41</string>
|
<string>42</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
|
|||||||
Reference in New Issue
Block a user