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 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 07:21:05 +02:00
co-authored by Claude Opus 4.6
parent 0fce3e6d28
commit 519706dd41
2 changed files with 24 additions and 17 deletions
+4 -2
View File
@@ -28,9 +28,11 @@ enum AnnotationRenderer {
static func render(annotations: [AnnotationItem], onto image: NSImage) -> NSImage { static func render(annotations: [AnnotationItem], onto image: NSImage) -> NSImage {
let size = image.size let size = image.size
let result = NSImage(size: 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 { if let context = NSGraphicsContext.current?.cgContext {
for item in annotations { for item in annotations {
+20 -15
View File
@@ -1,6 +1,10 @@
import AppKit import AppKit
import SwiftUI import SwiftUI
private final class FlippedContainerView: NSView {
override var isFlipped: Bool { true }
}
final class EditorWindowController: NSWindowController, NSWindowDelegate { final class EditorWindowController: NSWindowController, NSWindowDelegate {
private let annotationDoc = AnnotationDocument() private let annotationDoc = AnnotationDocument()
@@ -19,7 +23,7 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate {
let maxHeight = screenFrame.height * 0.8 let maxHeight = screenFrame.height * 0.8
let scale = min(maxWidth / image.size.width, maxHeight / image.size.height, 1.0) let scale = min(maxWidth / image.size.width, maxHeight / image.size.height, 1.0)
let windowWidth = max(image.size.width * scale, 600) 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( let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: windowWidth, height: windowHeight), contentRect: NSRect(x: 0, y: 0, width: windowWidth, height: windowHeight),
@@ -40,7 +44,6 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate {
canvasView.onDocumentChanged = { [weak self] in canvasView.onDocumentChanged = { [weak self] in
self?.canvasView.needsDisplay = true self?.canvasView.needsDisplay = true
} }
canvasView.autoresizingMask = [.width, .height]
let toolbarHosting = NSHostingView(rootView: EditorToolbarView( let toolbarHosting = NSHostingView(rootView: EditorToolbarView(
document: annotationDoc, document: annotationDoc,
@@ -48,23 +51,25 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate {
onCopyToClipboard: { [weak self] in self?.copyToClipboard() }, onCopyToClipboard: { [weak self] in self?.copyToClipboard() },
onCancel: { [weak self] in self?.cancel() } onCancel: { [weak self] in self?.cancel() }
)) ))
toolbarHosting.translatesAutoresizingMaskIntoConstraints = true toolbarHosting.translatesAutoresizingMaskIntoConstraints = false
canvasView.translatesAutoresizingMaskIntoConstraints = false
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]
let container = FlippedContainerView()
container.addSubview(toolbarHosting) container.addSubview(toolbarHosting)
container.addSubview(canvasView) container.addSubview(canvasView)
window.contentView = container 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) @available(*, unavailable)