From 519706dd41a7165060dba5d655e973696de3277f Mon Sep 17 00:00:00 2001 From: Marco Wende Date: Wed, 22 Jul 2026 07:21:05 +0200 Subject: [PATCH] Fix annotation position shift in editor and export Use flipped container with Auto Layout for correct coordinate mapping. Use lockFocusFlipped(true) in renderer so exported annotations match canvas positions. Co-Authored-By: Claude Opus 4.6 --- Sources/Editor/AnnotationRenderer.swift | 6 ++-- Sources/Editor/EditorWindowController.swift | 35 ++++++++++++--------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Sources/Editor/AnnotationRenderer.swift b/Sources/Editor/AnnotationRenderer.swift index 0839902..977850d 100644 --- a/Sources/Editor/AnnotationRenderer.swift +++ b/Sources/Editor/AnnotationRenderer.swift @@ -28,9 +28,11 @@ enum AnnotationRenderer { static func render(annotations: [AnnotationItem], onto image: NSImage) -> NSImage { let size = image.size let result = NSImage(size: size) - result.lockFocus() + result.lockFocusFlipped(true) - image.draw(in: NSRect(origin: .zero, size: size)) + image.draw(in: NSRect(origin: .zero, size: size), + from: .zero, operation: .sourceOver, fraction: 1, + respectFlipped: true, hints: nil) if let context = NSGraphicsContext.current?.cgContext { for item in annotations { diff --git a/Sources/Editor/EditorWindowController.swift b/Sources/Editor/EditorWindowController.swift index ec99ed1..7f974d9 100644 --- a/Sources/Editor/EditorWindowController.swift +++ b/Sources/Editor/EditorWindowController.swift @@ -1,6 +1,10 @@ import AppKit import SwiftUI +private final class FlippedContainerView: NSView { + override var isFlipped: Bool { true } +} + final class EditorWindowController: NSWindowController, NSWindowDelegate { private let annotationDoc = AnnotationDocument() @@ -19,7 +23,7 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate { let maxHeight = screenFrame.height * 0.8 let scale = min(maxWidth / image.size.width, maxHeight / image.size.height, 1.0) let windowWidth = max(image.size.width * scale, 600) - let windowHeight = max(image.size.height * scale, 400) + 52 // toolbar height + let windowHeight = max(image.size.height * scale, 400) + 52 let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: windowWidth, height: windowHeight), @@ -40,7 +44,6 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate { canvasView.onDocumentChanged = { [weak self] in self?.canvasView.needsDisplay = true } - canvasView.autoresizingMask = [.width, .height] let toolbarHosting = NSHostingView(rootView: EditorToolbarView( document: annotationDoc, @@ -48,23 +51,25 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate { onCopyToClipboard: { [weak self] in self?.copyToClipboard() }, onCancel: { [weak self] in self?.cancel() } )) - toolbarHosting.translatesAutoresizingMaskIntoConstraints = true - - let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) - container.autoresizingMask = [.width, .height] - - toolbarHosting.frame = NSRect(x: 0, y: container.bounds.height - 52, - width: container.bounds.width, height: 52) - toolbarHosting.autoresizingMask = [.width, .minYMargin] - - canvasView.frame = NSRect(x: 0, y: 0, - width: container.bounds.width, - height: container.bounds.height - 52) - canvasView.autoresizingMask = [.width, .height] + toolbarHosting.translatesAutoresizingMaskIntoConstraints = false + canvasView.translatesAutoresizingMaskIntoConstraints = false + let container = FlippedContainerView() container.addSubview(toolbarHosting) container.addSubview(canvasView) window.contentView = container + + NSLayoutConstraint.activate([ + toolbarHosting.topAnchor.constraint(equalTo: container.topAnchor), + toolbarHosting.leadingAnchor.constraint(equalTo: container.leadingAnchor), + toolbarHosting.trailingAnchor.constraint(equalTo: container.trailingAnchor), + toolbarHosting.heightAnchor.constraint(equalToConstant: 52), + + canvasView.topAnchor.constraint(equalTo: toolbarHosting.bottomAnchor), + canvasView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + canvasView.trailingAnchor.constraint(equalTo: container.trailingAnchor), + canvasView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + ]) } @available(*, unavailable)