New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@annotorious/annotorious

Package Overview
Dependencies
Maintainers
1
Versions
127
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@annotorious/annotorious - npm Package Compare versions

Comparing version
3.7.22
to
3.8.0
+26
dist/annotation/editors/rectangle/rotationUtils.d.ts
import { RectangleGeometry } from '../../../model';
/**
* Rotates a point around a center by the given angle (in rad).
*/
export declare const rotatePoint: (point: [number, number], center: [number, number], angle: number) => [number, number];
/**
* Gets the four corner points of a rotated rectangle in world space.
*/
export declare const getRotatedCorners: (x: number, y: number, w: number, h: number, rot?: number) => [[number, number], [number, number], [number, number], [number, number]];
/**
* Calculates the position of the rotation handle.
*/
export declare const getRotationHandlePosition: (geom: RectangleGeometry, offset: number) => [number, number];
/**
* Transforms a movement delta from world coords to the rectangle's
* local (non-rotated) coordinate system.
*/
export declare const transformDeltaToLocalCoords: (deltaX: number, deltaY: number, rot: number) => [number, number];
/**
* Calculates the rotation angle between a point and the origin, relative to a center.
*/
export declare const angleFromPoints: (point1: [number, number], point2: [number, number], center: [number, number]) => number;
/**
* Snaps an angle to the nearest 45-degree increment
*/
export declare const snapAngle: (angle: number, inc?: number) => number;
import type { Bounds, RectangleGeometry } from '../../../model';
/**
* Rotates a point around a center by the given angle (in rad).
*/
export const rotatePoint = (
point: [number, number],
center: [number, number],
angle: number
): [number, number] => {
const [px, py] = point;
const [cx, cy] = center;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const dx = px - cx;
const dy = py - cy;
return [
cx + dx * cos - dy * sin,
cy + dx * sin + dy * cos
];
}
/**
* Gets the four corner points of a rotated rectangle in world space.
*/
export const getRotatedCorners = (
x: number,
y: number,
w: number,
h: number,
rot: number = 0
): [[number, number], [number, number], [number, number], [number, number]] => {
const corners: [number, number][] = [
[x, y],
[x + w, y],
[x + w, y + h],
[x, y + h]
];
const center: [number, number] = [x + w / 2, y + h / 2];
return corners.map(corner =>
rotatePoint(corner, center, rot)) as [[number, number], [number, number], [number, number], [number, number]];
}
/**
* Calculates the position of the rotation handle.
*/
export const getRotationHandlePosition = (
geom: RectangleGeometry, offset: number
): [number, number] => {
const { x , y, w, h, rot = 0 } = geom;
const center: [number, number] = [x + w / 2, y + h / 2];
let topCenter: [number, number] = [x + w / 2, y - offset];
return rotatePoint(topCenter, center, rot);
}
/**
* Transforms a movement delta from world coords to the rectangle's
* local (non-rotated) coordinate system.
*/
export const transformDeltaToLocalCoords = (
deltaX: number,
deltaY: number,
rot: number
): [number, number] => {
const cos = Math.cos(rot);
const sin = Math.sin(rot);
return [
deltaX * cos + deltaY * sin,
-deltaX * sin + deltaY * cos
];
}
/**
* Calculates the rotation angle between a point and the origin, relative to a center.
*/
export const angleFromPoints = (
point1: [number, number],
point2: [number, number],
center: [number, number]
): number => {
const dx1 = point1[0] - center[0];
const dy1 = point1[1] - center[1];
const angle1 = Math.atan2(dy1, dx1);
const dx2 = point2[0] - center[0];
const dy2 = point2[1] - center[1];
const angle2 = Math.atan2(dy2, dx2);
return angle2 - angle1;
}
/**
* Snaps an angle to the nearest 45-degree increment
*/
export const snapAngle = (angle: number, inc = 10): number => {
const step = (inc * Math.PI) / 180;
return Math.round(angle / step) * step;
}
+1
-1

@@ -1,1 +0,1 @@

circle.a9s-handle-buffer.svelte-qtyc7s:focus{outline:none}circle.a9s-handle-buffer.svelte-qtyc7s:focus-visible{stroke:#fffc;stroke-width:3px}.a9s-polygon-midpoint.svelte-12ykj76{cursor:crosshair}.a9s-polygon-midpoint-buffer.svelte-12ykj76{fill:transparent}.a9s-polygon-midpoint-outer.svelte-12ykj76{display:none;fill:transparent;pointer-events:none;stroke:#00000059;stroke-width:1.5px;vector-effect:non-scaling-stroke}.a9s-polygon-midpoint-inner.svelte-12ykj76{fill:#00000040;pointer-events:none;stroke:#fff;stroke-width:1px;vector-effect:non-scaling-stroke}mask.a9s-polygon-editor-mask.svelte-1h2slbm>rect.svelte-1h2slbm{fill:#fff}mask.a9s-polygon-editor-mask.svelte-1h2slbm>circle.svelte-1h2slbm,mask.a9s-polygon-editor-mask.svelte-1h2slbm>polygon.svelte-1h2slbm{fill:#000}mask.a9s-rectangle-editor-mask.svelte-1njczvj>rect.rect-mask-bg.svelte-1njczvj{fill:#fff}mask.a9s-rectangle-editor-mask.svelte-1njczvj>rect.rect-mask-fg.svelte-1njczvj{fill:#000}mask.a9s-multipolygon-editor-mask.svelte-1vxo6dc>rect.svelte-1vxo6dc{fill:#fff}mask.a9s-multipolygon-editor-mask.svelte-1vxo6dc>circle.svelte-1vxo6dc,mask.a9s-multipolygon-editor-mask.svelte-1vxo6dc>path.svelte-1vxo6dc{fill:#000}mask.a9s-rubberband-rectangle-mask.svelte-1a76qe7>rect.rect-mask-bg.svelte-1a76qe7{fill:#fff}mask.a9s-rubberband-rectangle-mask.svelte-1a76qe7>rect.rect-mask-fg.svelte-1a76qe7{fill:#000}mask.a9s-rubberband-polygon-mask.svelte-18wrg3t>rect.svelte-18wrg3t{fill:#fff}mask.a9s-rubberband-polygon-mask.svelte-18wrg3t>polygon.svelte-18wrg3t{fill:#000}circle.a9s-handle.svelte-18wrg3t.svelte-18wrg3t{fill:#fff;pointer-events:none;stroke:#00000059;stroke-width:1px;vector-effect:non-scaling-stroke}path.open.svelte-1w0132l{fill:transparent!important}.a9s-annotationlayer{box-sizing:border-box;height:100%;left:0;outline:none;position:absolute;top:0;touch-action:none;width:100%;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.a9s-annotationlayer.hover{cursor:pointer}.a9s-annotationlayer.hidden{display:none}.a9s-annotationlayer ellipse,.a9s-annotationlayer line,.a9s-annotationlayer path,.a9s-annotationlayer polygon,.a9s-annotationlayer rect{fill:transparent;shape-rendering:geometricPrecision;vector-effect:non-scaling-stroke;-webkit-tap-highlight-color:transparent}.a9s-touch-halo{fill:transparent;pointer-events:none;stroke-width:0;transition:fill .15s}.a9s-touch-halo.touched{fill:#fff6}.a9s-handle-buffer{fill:transparent}.a9s-handle [role=button]{cursor:inherit!important}.a9s-handle-dot{fill:#fff;pointer-events:none;stroke:#00000059;stroke-width:1px;vector-effect:non-scaling-stroke}.a9s-handle-dot.selected{fill:#1a1a1a;stroke:none}.a9s-handle-selected{animation:dash-rotate .35s linear infinite reverse;fill:#ffffff40;stroke:#000000e6;stroke-dasharray:2 2;stroke-width:1px;pointer-events:none;vector-effect:non-scaling-stroke}@keyframes dash-rotate{0%{stroke-dashoffset:0}to{stroke-dashoffset:4}}.a9s-edge-handle{fill:transparent;stroke:transparent;stroke-width:6px}.a9s-shape-handle,.a9s-handle{cursor:move}.a9s-handle.a9s-corner-handle{cursor:crosshair}.a9s-edge-handle-top{cursor:n-resize}.a9s-edge-handle-right{cursor:e-resize}.a9s-edge-handle-bottom{cursor:s-resize}.a9s-edge-handle-left{cursor:w-resize}.a9s-handle.a9s-corner-handle-topleft{cursor:nw-resize}.a9s-handle.a9s-corner-handle-topright{cursor:ne-resize}.a9s-handle.a9s-corner-handle-bottomright{cursor:se-resize}.a9s-handle.a9s-corner-handle-bottomleft{cursor:sw-resize}.a9s-annotationlayer .a9s-outer,div[data-theme=dark] .a9s-annotationlayer .a9s-outer{display:none}.a9s-annotationlayer .a9s-inner,div[data-theme=dark] .a9s-annotationlayer .a9s-inner{fill:#0000001f;stroke:#000;stroke-width:1px}rect.a9s-handle,div[data-theme=dark] rect.a9s-handle{fill:#000;rx:2px}rect.a9s-close-polygon-handle,div[data-theme=dark] rect.a9s-close-polygon-handle{fill:#000;rx:1px}.a9s-annotationlayer .a9s-outer,div[data-theme=light] .a9s-annotationlayer .a9s-outer{display:block;stroke:#00000059;stroke-width:3px}.a9s-annotationlayer .a9s-inner,div[data-theme=light] .a9s-annotationlayer .a9s-inner{fill:#ffffff26;stroke:#fff;stroke-width:1.5px}rect.a9s-handle,div[data-theme=light] rect.a9s-handle{fill:#fff;rx:1px;stroke:#00000073;stroke-width:1px}rect.a9s-close-polygon-handle,div[data-theme=light] rect.a9s-close-polygon-handle{fill:#fff;rx:1px;stroke:#00000073;stroke-width:1px}
circle.a9s-handle-buffer.svelte-qtyc7s:focus{outline:none}circle.a9s-handle-buffer.svelte-qtyc7s:focus-visible{stroke:#fffc;stroke-width:3px}.a9s-polygon-midpoint.svelte-12ykj76{cursor:crosshair}.a9s-polygon-midpoint-buffer.svelte-12ykj76{fill:transparent}.a9s-polygon-midpoint-outer.svelte-12ykj76{display:none;fill:transparent;pointer-events:none;stroke:#00000059;stroke-width:1.5px;vector-effect:non-scaling-stroke}.a9s-polygon-midpoint-inner.svelte-12ykj76{fill:#00000040;pointer-events:none;stroke:#fff;stroke-width:1px;vector-effect:non-scaling-stroke}mask.a9s-polygon-editor-mask.svelte-1h2slbm>rect.svelte-1h2slbm{fill:#fff}mask.a9s-polygon-editor-mask.svelte-1h2slbm>circle.svelte-1h2slbm,mask.a9s-polygon-editor-mask.svelte-1h2slbm>polygon.svelte-1h2slbm{fill:#000}mask.a9s-rectangle-editor-mask.svelte-1bwhzbc rect.rect-mask-bg.svelte-1bwhzbc{fill:#fff}mask.a9s-rectangle-editor-mask.svelte-1bwhzbc polygon.rect-mask-fg.svelte-1bwhzbc{fill:#000}.a9s-rotation-handle-line-bg{stroke:#00000080;stroke-width:1.5px;vector-effect:non-scaling-stroke}.a9s-rotation-handle-line-fg{stroke:#fff;stroke-width:1px;stroke-dasharray:3 1;vector-effect:non-scaling-stroke}mask.a9s-multipolygon-editor-mask.svelte-1vxo6dc>rect.svelte-1vxo6dc{fill:#fff}mask.a9s-multipolygon-editor-mask.svelte-1vxo6dc>circle.svelte-1vxo6dc,mask.a9s-multipolygon-editor-mask.svelte-1vxo6dc>path.svelte-1vxo6dc{fill:#000}mask.a9s-rubberband-rectangle-mask.svelte-1a76qe7>rect.rect-mask-bg.svelte-1a76qe7{fill:#fff}mask.a9s-rubberband-rectangle-mask.svelte-1a76qe7>rect.rect-mask-fg.svelte-1a76qe7{fill:#000}mask.a9s-rubberband-polygon-mask.svelte-18wrg3t>rect.svelte-18wrg3t{fill:#fff}mask.a9s-rubberband-polygon-mask.svelte-18wrg3t>polygon.svelte-18wrg3t{fill:#000}circle.a9s-handle.svelte-18wrg3t.svelte-18wrg3t{fill:#fff;pointer-events:none;stroke:#00000059;stroke-width:1px;vector-effect:non-scaling-stroke}path.open.svelte-1w0132l{fill:transparent!important}.a9s-annotationlayer{box-sizing:border-box;height:100%;left:0;outline:none;position:absolute;top:0;touch-action:none;width:100%;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.a9s-annotationlayer.hover{cursor:pointer}.a9s-annotationlayer.hidden{display:none}.a9s-annotationlayer ellipse,.a9s-annotationlayer line,.a9s-annotationlayer path,.a9s-annotationlayer polygon,.a9s-annotationlayer rect{fill:transparent;shape-rendering:geometricPrecision;vector-effect:non-scaling-stroke;-webkit-tap-highlight-color:transparent}.a9s-touch-halo{fill:transparent;pointer-events:none;stroke-width:0;transition:fill .15s}.a9s-touch-halo.touched{fill:#fff6}.a9s-handle-buffer{fill:transparent}.a9s-handle [role=button]{cursor:inherit!important}.a9s-handle-dot{fill:#fff;pointer-events:none;stroke:#00000059;stroke-width:1px;vector-effect:non-scaling-stroke}.a9s-handle-dot.selected{fill:#1a1a1a;stroke:none}.a9s-handle-selected{animation:dash-rotate .35s linear infinite reverse;fill:#ffffff40;stroke:#000000e6;stroke-dasharray:2 2;stroke-width:1px;pointer-events:none;vector-effect:non-scaling-stroke}@keyframes dash-rotate{0%{stroke-dashoffset:0}to{stroke-dashoffset:4}}.a9s-edge-handle{fill:transparent;stroke:transparent;stroke-width:6px;vector-effect:non-scaling-stroke}.a9s-shape-handle,.a9s-handle{cursor:move}.a9s-handle.a9s-corner-handle{cursor:crosshair}.a9s-edge-handle-top{cursor:n-resize}.a9s-edge-handle-right{cursor:e-resize}.a9s-edge-handle-bottom{cursor:s-resize}.a9s-edge-handle-left{cursor:w-resize}.a9s-handle.a9s-corner-handle-topleft{cursor:nw-resize}.a9s-handle.a9s-corner-handle-topright{cursor:ne-resize}.a9s-handle.a9s-corner-handle-bottomright{cursor:se-resize}.a9s-handle.a9s-corner-handle-bottomleft{cursor:sw-resize}.a9s-annotationlayer .a9s-outer,div[data-theme=dark] .a9s-annotationlayer .a9s-outer{display:none}.a9s-annotationlayer .a9s-inner,div[data-theme=dark] .a9s-annotationlayer .a9s-inner{fill:#0000001f;stroke:#000;stroke-width:1px}rect.a9s-handle,div[data-theme=dark] rect.a9s-handle{fill:#000;rx:2px}rect.a9s-close-polygon-handle,div[data-theme=dark] rect.a9s-close-polygon-handle{fill:#000;rx:1px}.a9s-annotationlayer .a9s-outer,div[data-theme=light] .a9s-annotationlayer .a9s-outer{display:block;stroke:#00000059;stroke-width:3px}.a9s-annotationlayer .a9s-inner,div[data-theme=light] .a9s-annotationlayer .a9s-inner{fill:#ffffff26;stroke:#fff;stroke-width:1.5px}rect.a9s-handle,div[data-theme=light] rect.a9s-handle{fill:#fff;rx:1px;stroke:#00000073;stroke-width:1px}rect.a9s-close-polygon-handle,div[data-theme=light] rect.a9s-close-polygon-handle{fill:#fff;rx:1px;stroke:#00000073;stroke-width:1px}

@@ -10,3 +10,4 @@ import { Bounds, Geometry, Shape } from '../Shape';

h: number;
rot?: number;
bounds: Bounds;
}
{
"name": "@annotorious/annotorious",
"version": "3.7.22",
"version": "3.8.0",
"description": "Add image annotation functionality to any web page with a few lines of JavaScript",

@@ -58,3 +58,3 @@ "author": "Rainer Simon",

"dependencies": {
"@annotorious/core": "3.7.22",
"@annotorious/core": "3.8.0",
"dequal": "^2.0.3",

@@ -61,0 +61,0 @@ "rbush": "^4.0.1",

<script lang="ts">
import { onMount } from 'svelte';
import Handle from '../Handle.svelte';
import { getMaskDimensions } from '../../utils';
import type { Rectangle, Shape } from '../../../model';
import { boundsFromPoints, type Rectangle, type RectangleGeometry, type Shape } from '../../../model';
import type { Transform } from '../../Transform';
import { Editor } from '..';
import {
getRotatedCorners,
getRotationHandlePosition,
transformDeltaToLocalCoords,
angleFromPoints,
snapAngle
} from './rotationUtils';

@@ -15,32 +23,55 @@ /** Props */

let shiftPressed = false;
$: ROTATION_HANDLE_OFFSET = 20 / viewportScale;
$: geom = shape.geometry;
$: rotatedCorners = getRotatedCorners(geom.x, geom.y, geom.w, geom.h, geom.rot);
$: rotationHandlePos = getRotationHandlePosition(geom, ROTATION_HANDLE_OFFSET);
const editor = (rectangle: Shape, handle: string, delta: [number, number]) => {
const initialBounds = rectangle.geometry.bounds;
let { x, y, w, h, rot = 0 } = (rectangle.geometry as RectangleGeometry);
let [x0, y0] = [initialBounds.minX, initialBounds.minY];
let [x1, y1] = [initialBounds.maxX, initialBounds.maxY];
const [dx, dy] = delta;
if (handle === 'SHAPE') {
x0 += dx;
x1 += dx;
y0 += dy;
y1 += dy;
if (handle === 'ROTATION') {
const handlePos = getRotationHandlePosition(rectangle.geometry as RectangleGeometry, ROTATION_HANDLE_OFFSET);
// Handle position after moving by delta
const currentHandleX = handlePos[0] + dx;
const currentHandleY = handlePos[1] + dy;
// Calculate the new rotation angle
const center: [number, number] = [x + w / 2, y + h / 2];
rot += angleFromPoints([handlePos[0], handlePos[1]], [currentHandleX, currentHandleY], center);
// Snap to 10 degrees if SHIFT is held
if (shiftPressed)
rot = snapAngle(rot);
} else if (handle === 'SHAPE') {
// Moving the entire shape - translate it without rotation change
x += dx;
y += dy;
} else {
// Edge or corner handle - resize in local (rotated) coordinate space
let localX0 = 0;
let localY0 = 0;
let localX1 = w;
let localY1 = h;
const [localDx, localDy] = rot !== 0
? transformDeltaToLocalCoords(dx, dy, rot)
: [dx, dy];
switch (handle) {
case 'TOP':
case 'TOP_LEFT':
case 'TOP_RIGHT': {
y0 += dy;
case 'TOP_RIGHT':
localY0 += localDy;
break;
}
case 'BOTTOM':
case 'BOTTOM_LEFT':
case 'BOTTOM_RIGHT': {
y1 += dy;
case 'BOTTOM_RIGHT':
localY1 += localDy;
break;
}
}

@@ -51,20 +82,42 @@

case 'TOP_LEFT':
case 'BOTTOM_LEFT': {
x0 += dx;
case 'BOTTOM_LEFT':
localX0 += localDx;
break;
}
case 'RIGHT':
case 'TOP_RIGHT':
case 'BOTTOM_RIGHT': {
x1 += dx;
case 'BOTTOM_RIGHT':
localX1 += localDx;
break;
}
}
// The center shifts as edges move - calculate new center in local space
const newLocalCx = (localX0 + localX1) / 2;
const newLocalCy = (localY0 + localY1) / 2;
w = Math.abs(localX1 - localX0);
h = Math.abs(localY1 - localY0);
// Rotate the local center offset back to world space
const oldCenter: [number, number] = [
x + (rectangle.geometry as RectangleGeometry).w / 2,
y + (rectangle.geometry as RectangleGeometry).h / 2
];
const localCenterOffset: [number, number] = [
newLocalCx - (rectangle.geometry as RectangleGeometry).w / 2,
newLocalCy - (rectangle.geometry as RectangleGeometry).h / 2
];
const cos = Math.cos(rot);
const sin = Math.sin(rot);
const worldCx = oldCenter[0] + localCenterOffset[0] * cos - localCenterOffset[1] * sin;
const worldCy = oldCenter[1] + localCenterOffset[0] * sin + localCenterOffset[1] * cos;
x = worldCx - w / 2;
y = worldCy - h / 2;
}
const x = Math.min(x0, x1);
const y = Math.min(y0, y1);
const w = Math.abs(x1 - x0);
const h = Math.abs(y1 - y0);
// Calculate new bounds
const bounds = boundsFromPoints(rotatedCorners);

@@ -74,9 +127,4 @@ return {

geometry: {
x, y, w, h,
bounds: {
minX: x,
minY: y,
maxX: x + w,
maxY: y + h
}
x, y, w, h, rot,
bounds
}

@@ -86,3 +134,22 @@ };

$: mask = getMaskDimensions(geom.bounds, 2 / viewportScale);
onMount(() => {
// Track SHIFT key
const onKeyDown = (evt: KeyboardEvent) => {
if (evt.key === 'Shift') shiftPressed = true;
}
const onKeyUp = (evt: KeyboardEvent) => {
if (evt.key === 'Shift') shiftPressed = false;
}
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
});
$: mask = getMaskDimensions(geom.bounds, 5 / viewportScale);

@@ -105,42 +172,78 @@ const maskId = `rect-mask-${Math.random().toString(36).substring(2, 12)}`;

<rect class="rect-mask-bg" x={mask.x} y={mask.y} width={mask.w} height={mask.h} />
<rect class="rect-mask-fg" x={geom.x} y={geom.y} width={geom.w} height={geom.h} />
<polygon
class="rect-mask-fg"
points={rotatedCorners.map(c => `${c[0]},${c[1]}`).join(' ')} />
</mask>
</defs>
<rect
class="a9s-outer"
mask={`url(#${maskId})`}
on:pointerdown={grab('SHAPE')}
x={geom.x} y={geom.y} width={geom.w} height={geom.h} />
<!-- Rotation handle -->
<g>
<line
class="a9s-rotation-handle-line-bg"
x1={rotatedCorners[0][0] + (rotatedCorners[1][0] - rotatedCorners[0][0]) / 2}
y1={rotatedCorners[0][1] + (rotatedCorners[1][1] - rotatedCorners[0][1]) / 2}
x2={rotationHandlePos[0]}
y2={rotationHandlePos[1]}
pointer-events="none" />
<rect
class="a9s-inner a9s-shape-handle"
style={computedStyle}
on:pointerdown={grab('SHAPE')}
x={geom.x} y={geom.y} width={geom.w} height={geom.h} />
<line
class="a9s-rotation-handle-line-fg"
x1={rotatedCorners[0][0] + (rotatedCorners[1][0] - rotatedCorners[0][0]) / 2}
y1={rotatedCorners[0][1] + (rotatedCorners[1][1] - rotatedCorners[0][1]) / 2}
x2={rotationHandlePos[0]}
y2={rotationHandlePos[1]}
pointer-events="none" />
<rect
class="a9s-edge-handle a9s-edge-handle-top"
on:pointerdown={grab('TOP')}
x={geom.x} y={geom.y} height={1} width={geom.w} />
<Handle
class="a9s-rotation-handle"
on:pointerdown={grab('ROTATION')}
x={rotationHandlePos[0]} y={rotationHandlePos[1]}
scale={viewportScale} />
</g>
<rect
<!-- Rectangle shape -->
<g>
<polygon
class="a9s-outer"
mask={`url(#${maskId})`}
on:pointerdown={grab('SHAPE')}
points={rotatedCorners.map(c => `${c[0]},${c[1]}`).join(' ')} />
<polygon
class="a9s-inner a9s-shape-handle"
style={computedStyle}
on:pointerdown={grab('SHAPE')}
points={rotatedCorners.map(c => `${c[0]},${c[1]}`).join(' ')} />
</g>
<!-- Edge handles -->
<line
class="a9s-edge-handle a9s-edge-handle-top"
x1={rotatedCorners[0][0]} y1={rotatedCorners[0][1]}
x2={rotatedCorners[1][0]} y2={rotatedCorners[1][1]}
on:pointerdown={grab('TOP')} />
<line
class="a9s-edge-handle a9s-edge-handle-right"
on:pointerdown={grab('RIGHT')}
x={geom.x + geom.w} y={geom.y} height={geom.h} width={1}/>
x1={rotatedCorners[1][0]} y1={rotatedCorners[1][1]}
x2={rotatedCorners[2][0]} y2={rotatedCorners[2][1]}
on:pointerdown={grab('RIGHT')} />
<rect
class="a9s-edge-handle a9s-edge-handle-bottom"
on:pointerdown={grab('BOTTOM')}
x={geom.x} y={geom.y + geom.h} height={1} width={geom.w} />
<line
class="a9s-edge-handle a9s-edge-handle-bottom"
x1={rotatedCorners[2][0]} y1={rotatedCorners[2][1]}
x2={rotatedCorners[3][0]} y2={rotatedCorners[3][1]}
on:pointerdown={grab('BOTTOM')} />
<rect
class="a9s-edge-handle a9s-edge-handle-left"
on:pointerdown={grab('LEFT')}
x={geom.x} y={geom.y} height={geom.h} width={1} />
<line
class="a9s-edge-handle a9s-edge-handle-left"
x1={rotatedCorners[3][0]} y1={rotatedCorners[3][1]}
x2={rotatedCorners[0][0]} y2={rotatedCorners[0][1]}
on:pointerdown={grab('LEFT')} />
<!-- Corner handles -->
<Handle
class="a9s-corner-handle-topleft"
on:pointerdown={grab('TOP_LEFT')}
x={geom.x} y={geom.y}
x={rotatedCorners[0][0]} y={rotatedCorners[0][1]}
scale={viewportScale} />

@@ -151,3 +254,3 @@

on:pointerdown={grab('TOP_RIGHT')}
x={geom.x + geom.w} y={geom.y}
x={rotatedCorners[1][0]} y={rotatedCorners[1][1]}
scale={viewportScale} />

@@ -158,3 +261,3 @@

on:pointerdown={grab('BOTTOM_RIGHT')}
x={geom.x + geom.w} y={geom.y + geom.h}
x={rotatedCorners[2][0]} y={rotatedCorners[2][1]}
scale={viewportScale} />

@@ -165,3 +268,3 @@

on:pointerdown={grab('BOTTOM_LEFT')}
x={geom.x} y={geom.y + geom.h}
x={rotatedCorners[3][0]} y={rotatedCorners[3][1]}
scale={viewportScale} />

@@ -171,9 +274,22 @@ </Editor>

<style>
mask.a9s-rectangle-editor-mask > rect.rect-mask-bg {
mask.a9s-rectangle-editor-mask rect.rect-mask-bg {
fill: #fff;
}
mask.a9s-rectangle-editor-mask > rect.rect-mask-fg {
mask.a9s-rectangle-editor-mask polygon.rect-mask-fg {
fill: #000;
}
:global(.a9s-rotation-handle-line-bg) {
stroke: rgba(0, 0, 0, 0.5);
stroke-width: 1.5px;
vector-effect: non-scaling-stroke;
}
:global(.a9s-rotation-handle-line-fg) {
stroke: #fff;
stroke-width: 1px;
stroke-dasharray: 3 1;
vector-effect: non-scaling-stroke;
}
</style>

@@ -13,21 +13,28 @@ <script lang="ts">

$: ({ x, y, w, h } = geom as RectangleGeometry);
$: ({ x, y, w, h, rot } = geom as RectangleGeometry);
// Calculate transform for rotation
$: rectTransform = (rot ?? 0) !== 0 ?
`translate(${x + w / 2}, ${y + h / 2}) rotate(${((rot ?? 0) * 180) / Math.PI}) translate(${-(x + w / 2)}, ${-(y + h / 2)})` :
undefined;
</script>
<g class="a9s-annotation" data-id={annotation.id}>
<rect
class="a9s-outer"
style={computedStyle ? 'display:none;' : undefined}
x={x}
y={y}
width={w}
height={h} />
<g transform={rectTransform}>
<rect
class="a9s-outer"
style={computedStyle ? 'display:none;' : undefined}
x={x}
y={y}
width={w}
height={h} />
<rect
class="a9s-inner"
style={computedStyle}
x={x}
y={y}
width={w}
height={h} />
<rect
class="a9s-inner"
style={computedStyle}
x={x}
y={y}
width={w}
height={h} />
</g>
</g>
<script lang="ts" generics="I extends Annotation, E extends unknown">
import { type SvelteComponent, onMount } from 'svelte';
import { onMount } from 'svelte';
import { v4 as uuidv4 } from 'uuid';
import type { Annotation, DrawingStyleExpression, StoreChangeEvent, User } from '@annotorious/core';
import type { Annotation, DrawingStyleExpression, Selection, StoreChangeEvent, User } from '@annotorious/core';
import { isImageAnnotation, ShapeType } from '../model';

@@ -59,3 +59,3 @@ import type { ImageAnnotation, Shape} from '../model';

$: trackSelection($selection.selected);
$: trackSelection(($selection as Selection).selected);

@@ -62,0 +62,0 @@ const trackSelection = (selected: { id: string, editable?: boolean }[]) => {

@@ -109,2 +109,3 @@ <script lang="ts">

},
rot: 0,
x, y, w, h

@@ -111,0 +112,0 @@ }

@@ -96,2 +96,3 @@ /**

stroke-width: 6px;
vector-effect: non-scaling-stroke;
}

@@ -98,0 +99,0 @@

@@ -19,4 +19,6 @@ import type { Bounds, Geometry, Shape } from '../Shape';

rot?: number;
bounds: Bounds;
}

@@ -9,10 +9,36 @@ import { ShapeType } from '../Shape';

intersects: (rect: Rectangle, x: number, y: number): boolean =>
x >= rect.geometry.x &&
x <= rect.geometry.x + rect.geometry.w &&
y >= rect.geometry.y &&
y <= rect.geometry.y + rect.geometry.h
intersects: (rect: Rectangle, x: number, y: number): boolean => {
const geom = rect.geometry;
if (!geom.rot) {
return x >= geom.x &&
x <= geom.x + geom.w &&
y >= geom.y &&
y <= geom.y + geom.h;
} else {
// For rotated rectangles, transform the test point to local coordinates
const centerX = geom.x + geom.w / 2;
const centerY = geom.y + geom.h / 2;
// Translate point relative to center
const dx = x - centerX;
const dy = y - centerY;
// Rotate backwards to get to local (non-rotated) coordinates
const cos = Math.cos(geom.rot);
const sin = Math.sin(geom.rot);
const localX = dx * cos + dy * sin;
const localY = -dx * sin + dy * cos;
// Check if point is within rectangle bounds in local space
return localX >= -geom.w / 2 &&
localX <= geom.w / 2 &&
localY >= -geom.h / 2 &&
localY <= geom.h / 2;
}
}
};
registerShapeUtil(ShapeType.RECTANGLE, RectangleUtil);

@@ -58,2 +58,3 @@ import type { Rectangle, RectangleGeometry } from '../../core';

h,
rot: 0,
bounds: {

@@ -60,0 +61,0 @@ minX: x,

@@ -15,2 +15,4 @@ import { boundsFromPoints, computeSVGPath, multipolygonElementToPath, ShapeType } from '../../core';

PolylineGeometry,
Rectangle,
RectangleGeometry,
Shape

@@ -111,2 +113,64 @@ } from '../../core';

const parseSVGRect = (value: string): Rectangle => {
const doc = parseSVGXML(value);
const rect = doc.nodeName === 'rect' ? doc : Array.from(doc.querySelectorAll('rect'))[0];
if (!rect) throw new Error('Could not parse SVG rect');
const x = parseFloat(rect.getAttribute('x')!);
const y = parseFloat(rect.getAttribute('y')!);
const w = parseFloat(rect.getAttribute('width')!);
const h = parseFloat(rect.getAttribute('height')!);
const transform = rect.getAttribute('transform');
let rot = 0;
if (transform && transform.startsWith('rotate(')) {
const match = transform.match(/rotate\(([^)]+)\)/);
if (match) {
const params = match[1].split(/\s+/).map(parseFloat);
rot = (params[0] * Math.PI) / 180;
}
}
// Compute bounds
const cx = x + w / 2;
const cy = y + h / 2;
const corners = [
[x, y],
[x + w, y],
[x + w, y + h],
[x, y + h]
];
// Rotate corners around center
const rotatedCorners = corners.map(([px, py]) => {
const dx = px - cx;
const dy = py - cy;
const cos = Math.cos(rot);
const sin = Math.sin(rot);
return [
cx + dx * cos - dy * sin,
cy + dx * sin + dy * cos
];
});
const bounds = boundsFromPoints(rotatedCorners as [number, number][]);
return {
type: ShapeType.RECTANGLE,
geometry: {
x,
y,
w,
h,
rot,
bounds
}
};
}
const parseSVGPathToPolygon = (value: string): Polygon | MultiPolygon => {

@@ -156,2 +220,4 @@ const doc = parseSVGXML(value);

return parseSVGLine(value) as unknown as T;
else if (value.includes('<rect '))
return parseSVGRect(value) as unknown as T;
else

@@ -172,2 +238,17 @@ throw 'Unsupported SVG shape: ' + value;

switch (shape.type) {
case ShapeType.RECTANGLE: {
const geom = shape.geometry as RectangleGeometry;
const { x, y, w, h, rot } = geom;
if (!rot) {
value = `<svg><rect x="${x}" y="${y}" width="${w}" height="${h}" /></svg>`;
} else {
const cx = x + w / 2;
const cy = y + h / 2;
const angle = ((rot ?? 0) * 180) / Math.PI;
value = `<svg><rect x="${x}" y="${y}" width="${w}" height="${h}" transform="rotate(${angle} ${cx} ${cy})" /></svg>`;
}
break;
}
case ShapeType.POLYGON: {

@@ -174,0 +255,0 @@ const geom = shape.geometry as PolygonGeometry;

@@ -106,5 +106,7 @@ import { v4 as uuidv4 } from 'uuid';

try {
w3cSelector = selector.type == ShapeType.RECTANGLE ?
serializeFragmentSelector(selector.geometry as RectangleGeometry) :
serializeSVGSelector(selector);
if (selector.type === ShapeType.RECTANGLE && !(selector.geometry as RectangleGeometry).rot) {
w3cSelector = serializeFragmentSelector(selector.geometry as RectangleGeometry);
} else {
w3cSelector = serializeSVGSelector(selector);
}
} catch (error) {

@@ -114,3 +116,3 @@ if (opts.strict)

else
w3cSelector = selector;
w3cSelector = selector;
}

@@ -117,0 +119,0 @@

@@ -86,7 +86,4 @@ import RBush from 'rbush';

// Exact hit test on shape (not needed for rectangles!)
const exactHits = idxHits.filter(target => {
return (target.selector.type === ShapeType.RECTANGLE) ||
intersects(target.selector, x, y, buffer);
});
// Exact hit test on shape
const exactHits = idxHits.filter(({ selector }) => intersects(selector, x, y, buffer));

@@ -93,0 +90,0 @@ // Get smallest shape

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display