fix: preserve annotations while resizing selection
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"test": "node --test tests/*.test.ts",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
bb7ffb87329c9ad4990374471d4ce9a4
|
||||
e2d56b98c3c8ae5968db0bbb461b5615
|
||||
@@ -0,0 +1,28 @@
|
||||
export interface AnnotationPoint {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface PointCollection {
|
||||
points: AnnotationPoint[]
|
||||
}
|
||||
|
||||
// Annotations are stored relative to the selection origin. Resizing from the
|
||||
// top or left moves that origin, so offset the local points in the opposite
|
||||
// direction to keep them attached to the same screen pixels.
|
||||
export function preserveAnnotationScreenPositions(
|
||||
annotations: PointCollection[],
|
||||
previousOrigin: AnnotationPoint,
|
||||
nextOrigin: AnnotationPoint
|
||||
) {
|
||||
const dx = previousOrigin.x - nextOrigin.x
|
||||
const dy = previousOrigin.y - nextOrigin.y
|
||||
if (dx === 0 && dy === 0) return
|
||||
|
||||
for (const annotation of annotations) {
|
||||
for (const point of annotation.points) {
|
||||
point.x += dx
|
||||
point.y += dy
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import saveIcon from '../assets/icons/save-local.svg?raw'
|
||||
import saveRemoteIcon from '../assets/icons/save-remote.svg?raw'
|
||||
import ftpIcon from '../assets/icons/ftp.svg?raw'
|
||||
import uploadIcon from '../assets/icons/upload.svg?raw'
|
||||
import { preserveAnnotationScreenPositions } from '../utils/annotationGeometry'
|
||||
|
||||
interface Props {
|
||||
width: number
|
||||
@@ -539,12 +540,16 @@ function resizeSelection(p: Point) {
|
||||
right = clamp(right, 0, props.width)
|
||||
top = clamp(top, 0, props.height)
|
||||
bottom = clamp(bottom, 0, props.height)
|
||||
rect.value = {
|
||||
const nextRect = {
|
||||
x: Math.min(left, right),
|
||||
y: Math.min(top, bottom),
|
||||
w: Math.abs(right - left),
|
||||
h: Math.abs(bottom - top),
|
||||
}
|
||||
if (rect.value) {
|
||||
preserveAnnotationScreenPositions(annotations.value, rect.value, nextRect)
|
||||
}
|
||||
rect.value = nextRect
|
||||
}
|
||||
|
||||
function selectTool(tool: Tool) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { preserveAnnotationScreenPositions } from '../src/utils/annotationGeometry.ts'
|
||||
|
||||
test('keeps annotation points at the same screen position when the origin moves', () => {
|
||||
const annotations = [
|
||||
{ points: [{ x: 30, y: 40 }, { x: 80, y: 90 }] },
|
||||
{ points: [{ x: 12, y: 18 }] },
|
||||
]
|
||||
|
||||
preserveAnnotationScreenPositions(
|
||||
annotations,
|
||||
{ x: 100, y: 200 },
|
||||
{ x: 125, y: 230 }
|
||||
)
|
||||
|
||||
assert.deepEqual(annotations, [
|
||||
{ points: [{ x: 5, y: 10 }, { x: 55, y: 60 }] },
|
||||
{ points: [{ x: -13, y: -12 }] },
|
||||
])
|
||||
assert.deepEqual(
|
||||
annotations[0].points.map((point) => ({
|
||||
x: 125 + point.x,
|
||||
y: 230 + point.y,
|
||||
})),
|
||||
[{ x: 130, y: 240 }, { x: 180, y: 290 }]
|
||||
)
|
||||
})
|
||||
|
||||
test('does not move annotations when only the right or bottom edge changes', () => {
|
||||
const annotations = [{ points: [{ x: 30, y: 40 }] }]
|
||||
|
||||
preserveAnnotationScreenPositions(
|
||||
annotations,
|
||||
{ x: 100, y: 200 },
|
||||
{ x: 100, y: 200 }
|
||||
)
|
||||
|
||||
assert.deepEqual(annotations, [{ points: [{ x: 30, y: 40 }] }])
|
||||
})
|
||||
|
||||
test('preserves screen positions across repeated drags and an edge crossover', () => {
|
||||
const annotations = [{ points: [{ x: 50, y: 60 }] }]
|
||||
|
||||
preserveAnnotationScreenPositions(
|
||||
annotations,
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 140, y: 130 }
|
||||
)
|
||||
preserveAnnotationScreenPositions(
|
||||
annotations,
|
||||
{ x: 140, y: 130 },
|
||||
{ x: 300, y: 150 }
|
||||
)
|
||||
|
||||
assert.deepEqual(annotations, [{ points: [{ x: -150, y: 10 }] }])
|
||||
assert.deepEqual(
|
||||
{
|
||||
x: 300 + annotations[0].points[0].x,
|
||||
y: 150 + annotations[0].points[0].y,
|
||||
},
|
||||
{ x: 150, y: 160 }
|
||||
)
|
||||
})
|
||||
@@ -838,6 +838,24 @@ static id nativeOverlayKeyMonitor = nil;
|
||||
MAX(0, MIN(p.y - _selection.origin.y, _selection.size.height)));
|
||||
}
|
||||
|
||||
// Annotations use selection-local coordinates. When a top or left resize
|
||||
// moves the selection origin, offset every local point in the opposite
|
||||
// direction so the annotation remains attached to the same screen pixels.
|
||||
- (void)preserveAnnotationScreenPositionsFromOrigin:(NSPoint)previousOrigin toOrigin:(NSPoint)nextOrigin {
|
||||
CGFloat dx = previousOrigin.x - nextOrigin.x;
|
||||
CGFloat dy = previousOrigin.y - nextOrigin.y;
|
||||
if (fabs(dx) <= 0.001 && fabs(dy) <= 0.001) {
|
||||
return;
|
||||
}
|
||||
for (NSDictionary *annotation in _annotations) {
|
||||
NSMutableArray *points = (NSMutableArray *)annotation[@"points"];
|
||||
for (NSUInteger i = 0; i < points.count; i++) {
|
||||
NSPoint point = [points[i] pointValue];
|
||||
points[i] = [NSValue valueWithPoint:NSMakePoint(point.x + dx, point.y + dy)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Translate a selection rectangle expressed in view-local coordinates into
|
||||
// the global top-left screen coordinate system used by
|
||||
// `/usr/sbin/screencapture -R`.
|
||||
@@ -992,7 +1010,9 @@ static id nativeOverlayKeyMonitor = nil;
|
||||
right = MAX(0, MIN(right, self.bounds.size.width));
|
||||
top = MAX(0, MIN(top, self.bounds.size.height));
|
||||
bottom = MAX(0, MIN(bottom, self.bounds.size.height));
|
||||
_selection = NSMakeRect(MIN(left, right), MIN(top, bottom), fabs(right - left), fabs(bottom - top));
|
||||
NSRect nextSelection = NSMakeRect(MIN(left, right), MIN(top, bottom), fabs(right - left), fabs(bottom - top));
|
||||
[self preserveAnnotationScreenPositionsFromOrigin:_selection.origin toOrigin:nextSelection.origin];
|
||||
_selection = nextSelection;
|
||||
} else if (_draggingTextAnnotation && _selectedTextAnnotationIndex >= 0 && _selectedTextAnnotationIndex < (NSInteger)_annotations.count) {
|
||||
NSMutableDictionary *item = (NSMutableDictionary *)_annotations[(NSUInteger)_selectedTextAnnotationIndex];
|
||||
NSMutableArray *points = (NSMutableArray *)item[@"points"];
|
||||
|
||||
Reference in New Issue
Block a user