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
+31 -24
View File
@@ -10,6 +10,7 @@ protocol EditorCanvasDelegate: AnyObject {
func canvasDidChangeSelection(_ canvas: EditorCanvasView)
func canvasDidChangeContent(_ canvas: EditorCanvasView)
func canvasDidRequestToolChange(_ canvas: EditorCanvasView, to tool: ToolKind)
func canvasDidPickColor(_ canvas: EditorCanvasView, color: NSColor)
}
enum ResizeHandle: Int, CaseIterable {
@@ -100,7 +101,6 @@ final class EditorCanvasView: NSView, NSTextViewDelegate {
self.surface = surface
super.init(frame: CGRect(origin: .zero, size: surface.imageSize))
wantsLayer = true
layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
surface.onChange = { [weak self] in
guard let self else { return }
self.needsDisplay = true
@@ -196,6 +196,7 @@ final class EditorCanvasView: NSView, NSTextViewDelegate {
switch tool {
case .select: cursor = .arrow
case .text: cursor = .iBeam
case .dropper: cursor = .crosshair
default: cursor = .crosshair
}
addCursorRect(bounds, cursor: cursor)
@@ -225,6 +226,12 @@ final class EditorCanvasView: NSView, NSTextViewDelegate {
// The tool must stay on .text until editing finishes: switching tools calls
// endTextEditing(), which would discard the element before anything is typed.
beginTextEditing(element)
case .dropper:
// Sample the flattened image so annotation colours can be picked up too.
if let color = ImageExporter.color(at: point, in: surface.render(), scale: surface.scale) {
delegate?.canvasDidPickColor(self, color: color)
}
interaction = .none
case .counter:
let element = CounterElement(center: point, number: surface.nextCounterNumber, style: style)
surface.nextCounterNumber += 1
@@ -233,6 +240,7 @@ final class EditorCanvasView: NSView, NSTextViewDelegate {
interaction = .none
case .rectangle, .ellipse, .line, .arrow, .highlight, .obfuscate:
let element = makeElement(for: tool, at: point)
element.creationAnchor = point
surface.add(element)
surface.selection = element
interaction = .creating(element)
@@ -246,8 +254,8 @@ final class EditorCanvasView: NSView, NSTextViewDelegate {
switch tool {
case .rectangle: return RectangleElement(frame: frame, style: style)
case .ellipse: return EllipseElement(frame: frame, style: style)
case .line: return LineElement(frame: frame, style: style)
case .arrow: return ArrowElement(frame: frame, style: style)
case .line: return LineElement(start: point, end: point, style: style)
case .arrow: return ArrowElement(start: point, end: point, style: style)
case .highlight: return HighlightElement(frame: frame, style: style)
case .obfuscate: return ObfuscateElement(frame: frame, style: style)
default: return RectangleElement(frame: frame, style: style)
@@ -287,6 +295,7 @@ final class EditorCanvasView: NSView, NSTextViewDelegate {
surface.remove(existing)
}
let crop = CropElement(frame: CGRect(origin: point, size: .zero), style: style)
crop.creationAnchor = point
surface.add(crop)
surface.selection = crop
interaction = .creating(crop)
@@ -298,20 +307,23 @@ final class EditorCanvasView: NSView, NSTextViewDelegate {
switch interaction {
case .creating(let element):
var target = point
if constrain {
if element is LineElement {
target = constrainedLineEnd(from: element.frame.origin, to: point)
} else {
let side = max(abs(point.x - element.frame.origin.x), abs(point.y - element.frame.origin.y))
target = CGPoint(x: element.frame.origin.x + side * (point.x < element.frame.origin.x ? -1 : 1),
y: element.frame.origin.y + side * (point.y < element.frame.origin.y ? -1 : 1))
if let line = element as? LineElement {
let target = constrain ? constrainedLineEnd(from: line.start, to: point) : point
line.setEnd(target)
} else {
let anchorPoint = element.creationAnchor ?? element.frame.origin
var target = point
if constrain {
let side = max(abs(point.x - anchorPoint.x), abs(point.y - anchorPoint.y))
target = CGPoint(x: anchorPoint.x + side * (point.x < anchorPoint.x ? -1 : 1),
y: anchorPoint.y + side * (point.y < anchorPoint.y ? -1 : 1))
}
// Build the rect from the two corners so dragging up or left works.
element.frame = CGRect(x: min(anchorPoint.x, target.x),
y: min(anchorPoint.y, target.y),
width: abs(target.x - anchorPoint.x),
height: abs(target.y - anchorPoint.y))
}
element.frame = CGRect(x: element.frame.origin.x,
y: element.frame.origin.y,
width: target.x - element.frame.origin.x,
height: target.y - element.frame.origin.y)
surface.changed()
case .moving(let element, let origin):
element.move(by: CGVector(dx: point.x - origin.x, dy: point.y - origin.y))
@@ -341,18 +353,16 @@ final class EditorCanvasView: NSView, NSTextViewDelegate {
switch interaction {
case .creating(let element):
let rect = element.normalizedFrame
let tooSmall = element is LineElement
? hypot(element.frame.width, element.frame.height) < 4
: (rect.width < 4 || rect.height < 4)
let tooSmall = (element as? LineElement).map { $0.length < 4 }
?? (rect.width < 4 || rect.height < 4)
element.creationAnchor = nil
if tooSmall {
surface.remove(element)
} else if !(element is LineElement) {
element.frame = rect
if element is CropElement { surface.cropElement = element as? CropElement }
}
if !(element is CropElement) {
delegate?.canvasDidRequestToolChange(self, to: .select)
}
// The tool stays active so several shapes can be drawn in a row.
case .moving, .resizing, .drawingFreehand, .none:
break
}
@@ -448,9 +458,6 @@ final class EditorCanvasView: NSView, NSTextViewDelegate {
surface.changed()
needsDisplay = true
if tool == .text {
delegate?.canvasDidRequestToolChange(self, to: .select)
}
}
func textDidEndEditing(_ notification: Notification) {