fix: preserve annotations while resizing selection
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"test": "node --test tests/*.test.ts",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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 saveRemoteIcon from '../assets/icons/save-remote.svg?raw'
|
||||||
import ftpIcon from '../assets/icons/ftp.svg?raw'
|
import ftpIcon from '../assets/icons/ftp.svg?raw'
|
||||||
import uploadIcon from '../assets/icons/upload.svg?raw'
|
import uploadIcon from '../assets/icons/upload.svg?raw'
|
||||||
|
import { preserveAnnotationScreenPositions } from '../utils/annotationGeometry'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
width: number
|
width: number
|
||||||
@@ -539,12 +540,16 @@ function resizeSelection(p: Point) {
|
|||||||
right = clamp(right, 0, props.width)
|
right = clamp(right, 0, props.width)
|
||||||
top = clamp(top, 0, props.height)
|
top = clamp(top, 0, props.height)
|
||||||
bottom = clamp(bottom, 0, props.height)
|
bottom = clamp(bottom, 0, props.height)
|
||||||
rect.value = {
|
const nextRect = {
|
||||||
x: Math.min(left, right),
|
x: Math.min(left, right),
|
||||||
y: Math.min(top, bottom),
|
y: Math.min(top, bottom),
|
||||||
w: Math.abs(right - left),
|
w: Math.abs(right - left),
|
||||||
h: Math.abs(bottom - top),
|
h: Math.abs(bottom - top),
|
||||||
}
|
}
|
||||||
|
if (rect.value) {
|
||||||
|
preserveAnnotationScreenPositions(annotations.value, rect.value, nextRect)
|
||||||
|
}
|
||||||
|
rect.value = nextRect
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTool(tool: Tool) {
|
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)));
|
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
|
// Translate a selection rectangle expressed in view-local coordinates into
|
||||||
// the global top-left screen coordinate system used by
|
// the global top-left screen coordinate system used by
|
||||||
// `/usr/sbin/screencapture -R`.
|
// `/usr/sbin/screencapture -R`.
|
||||||
@@ -992,7 +1010,9 @@ static id nativeOverlayKeyMonitor = nil;
|
|||||||
right = MAX(0, MIN(right, self.bounds.size.width));
|
right = MAX(0, MIN(right, self.bounds.size.width));
|
||||||
top = MAX(0, MIN(top, self.bounds.size.height));
|
top = MAX(0, MIN(top, self.bounds.size.height));
|
||||||
bottom = MAX(0, MIN(bottom, 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) {
|
} else if (_draggingTextAnnotation && _selectedTextAnnotationIndex >= 0 && _selectedTextAnnotationIndex < (NSInteger)_annotations.count) {
|
||||||
NSMutableDictionary *item = (NSMutableDictionary *)_annotations[(NSUInteger)_selectedTextAnnotationIndex];
|
NSMutableDictionary *item = (NSMutableDictionary *)_annotations[(NSUInteger)_selectedTextAnnotationIndex];
|
||||||
NSMutableArray *points = (NSMutableArray *)item[@"points"];
|
NSMutableArray *points = (NSMutableArray *)item[@"points"];
|
||||||
|
|||||||
Reference in New Issue
Block a user