second commit

This commit is contained in:
Alex E. Tafoya
2026-09-17 16:15:02 -05:00
parent 87fe7927c4
commit 7e3801c7c6
10 changed files with 585 additions and 151 deletions
+64 -10
View File
@@ -18,6 +18,7 @@ enum ToolKind: String, CaseIterable {
case highlight
case obfuscate
case counter
case dropper
case crop
var title: String {
@@ -32,6 +33,7 @@ enum ToolKind: String, CaseIterable {
case .highlight: return "Highlight"
case .obfuscate: return "Obfuscate"
case .counter: return "Step counter"
case .dropper: return "Colour picker"
case .crop: return "Crop"
}
}
@@ -39,15 +41,16 @@ enum ToolKind: String, CaseIterable {
var symbolName: String {
switch self {
case .select: return "cursorarrow"
case .rectangle: return "rectangle"
case .rectangle: return "square"
case .ellipse: return "circle"
case .line: return "line.diagonal"
case .arrow: return "arrow.up.right"
case .freehand: return "scribble"
case .text: return "textformat"
case .arrow: return "arrow.up.forward"
case .freehand: return "scribble.variable"
case .text: return "character.textbox"
case .highlight: return "highlighter"
case .obfuscate: return "square.grid.3x3.fill"
case .counter: return "1.circle"
case .counter: return "number.circle"
case .dropper: return "eyedropper"
case .crop: return "crop"
}
}
@@ -64,6 +67,7 @@ enum ToolKind: String, CaseIterable {
case .highlight: return "h"
case .obfuscate: return "o"
case .counter: return "c"
case .dropper: return "i"
case .crop: return "k"
}
}
@@ -118,6 +122,8 @@ struct StyleCapabilities: OptionSet, Hashable {
class Element {
var frame: CGRect
var style: ElementStyle
/// Where the drag that created this element started; nil once the drag has finished.
var creationAnchor: CGPoint?
init(frame: CGRect, style: ElementStyle) {
self.frame = frame
@@ -217,13 +223,61 @@ final class EllipseElement: Element {
override func copyElement() -> Element { EllipseElement(frame: frame, style: style) }
}
/// A straight line; `frame` stores start in origin and end in origin + size, so the size may be negative.
/// A straight line between two explicit points.
///
/// The end points are stored separately rather than being derived from `frame`: CGRect
/// reports `width` and `height` as absolute values, so a rect built with a negative size
/// cannot describe a line that runs right-to-left or bottom-to-top. `frame` is kept in
/// sync as the bounding box so selection and hit testing behave like every other element.
class LineElement: Element {
var start: CGPoint { frame.origin }
var end: CGPoint { CGPoint(x: frame.origin.x + frame.width, y: frame.origin.y + frame.height) }
private(set) var start: CGPoint
private(set) var end: CGPoint
init(start: CGPoint, end: CGPoint, style: ElementStyle) {
self.start = start
self.end = end
super.init(frame: LineElement.bounds(start, end), style: style)
}
static func bounds(_ a: CGPoint, _ b: CGPoint) -> CGRect {
CGRect(x: min(a.x, b.x), y: min(a.y, b.y),
width: abs(b.x - a.x), height: abs(b.y - a.y))
}
/// Moves the free end while the line is being drawn.
func setEnd(_ point: CGPoint) {
end = point
frame = LineElement.bounds(start, end)
}
var length: CGFloat { hypot(end.x - start.x, end.y - start.y) }
override var capabilities: StyleCapabilities { [.lineColor, .lineThickness, .shadow] }
override func move(by delta: CGVector) {
start = CGPoint(x: start.x + delta.dx, y: start.y + delta.dy)
end = CGPoint(x: end.x + delta.dx, y: end.y + delta.dy)
frame = LineElement.bounds(start, end)
}
/// Maps both end points into the new bounding box, preserving the line's direction.
override func resize(to rect: CGRect) {
let old = frame
let target = CGRect(x: min(rect.minX, rect.maxX), y: min(rect.minY, rect.maxY),
width: abs(rect.width), height: abs(rect.height))
func map(_ point: CGPoint) -> CGPoint {
let fractionX = old.width > 0 ? (point.x - old.minX) / old.width : 0
let fractionY = old.height > 0 ? (point.y - old.minY) / old.height : 0
return CGPoint(x: target.minX + fractionX * target.width,
y: target.minY + fractionY * target.height)
}
let newStart = map(start)
let newEnd = map(end)
start = newStart
end = newEnd
frame = LineElement.bounds(start, end)
}
override func draw(in context: CGContext, baseImage: CGImage?, imageSize: CGSize) {
context.saveGState()
applyShadow(context)
@@ -249,7 +303,7 @@ class LineElement: Element {
return hypot(point.x - (a.x + t * dx), point.y - (a.y + t * dy))
}
override func copyElement() -> Element { LineElement(frame: frame, style: style) }
override func copyElement() -> Element { LineElement(start: start, end: end, style: style) }
}
final class ArrowElement: LineElement {
@@ -282,7 +336,7 @@ final class ArrowElement: LineElement {
context.restoreGState()
}
override func copyElement() -> Element { ArrowElement(frame: frame, style: style) }
override func copyElement() -> Element { ArrowElement(start: start, end: end, style: style) }
}
final class FreehandElement: Element {