Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/context/roiReducer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
ResizeStrategy,
RoiMode,
Size,
UpdateRoiAngleOptions,
} from '../index.js';
import type { CommittedRoiProperties } from '../types/CommittedRoi.js';
import type { Roi, XCornerPosition, YCornerPosition } from '../types/Roi.js';
Expand All @@ -27,6 +28,7 @@ import {
createRoiFromCommittedRoi,
} from '../utilities/rois.js';

import { updateAngle } from './updaters/angle.ts';
import { cancelAction } from './updaters/cancelAction.js';
import { endAction } from './updaters/endAction.js';
import { updateBasePanZoom } from './updaters/initialPanZoom.js';
Expand Down Expand Up @@ -163,6 +165,12 @@ export interface ZoomIntoROIPayload {
options: ZoomIntoROIOptions;
}

export interface UpdateAnglePayload {
id: string;
angle: number;
options?: UpdateRoiAngleOptions;
}

export interface StartDrawPayload {
event: PointerEvent | ReactPointerEvent;
containerBoundingRect: DOMRect;
Expand Down Expand Up @@ -255,6 +263,7 @@ export type RoiReducerAction =
payload: ZoomPayload;
}
| { type: 'ZOOM_INTO_ROI'; payload: ZoomIntoROIPayload }
| { type: 'UPDATE_ROI_ANGLE'; payload: UpdateAnglePayload }
| {
type: 'RESET_ZOOM';
}
Expand Down Expand Up @@ -464,6 +473,9 @@ export function roiReducer(
case 'ZOOM_INTO_ROI':
zoomIntoROI(draft, action.payload);
break;
case 'UPDATE_ROI_ANGLE':
updateAngle(draft, action.payload);
break;
case 'RESET_ZOOM':
resetZoomAction(draft);
break;
Expand Down
51 changes: 51 additions & 0 deletions src/context/updaters/angle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Draft } from 'immer';

import { assert } from '../../utilities/assert.ts';
import { changeBoxRotationCenter } from '../../utilities/box.ts';
import { createRoiFromCommittedRoi } from '../../utilities/rois.ts';
import { normalizeAngle } from '../../utilities/rotate.ts';
import type { ReactRoiState, UpdateAnglePayload } from '../roiReducer.tsx';

import { cancelAction } from './cancelAction.ts';
import { updateCommittedRoiPosition } from './roi.ts';

export function updateAngle(
draft: Draft<ReactRoiState>,
payload: UpdateAnglePayload,
) {
const {
id,
angle,
options = {
commit: true,
boundaryStrategy: 'none',
xRotationCenter: 'left',
yRotationCenter: 'top',
},
} = payload;
if (!id) return;
const index = draft.rois.findIndex((roi) => roi.id === id);

assert(index !== -1, 'ROI not found');
if (options.commit) {
const committedRoi = draft.committedRois[index];
assert(committedRoi, 'Committed ROI not found');
draft.rois[index] = createRoiFromCommittedRoi(committedRoi);
const roi = draft.rois[index];
roi.box = changeBoxRotationCenter(roi.box, options);
roi.box.angle = normalizeAngle(angle);
try {
updateCommittedRoiPosition(draft, committedRoi, draft.rois[index], {
boundaryStrategy: options.boundaryStrategy ?? 'none',
boxStrategy: 'exact',
});
} catch {
cancelAction(draft, { noUnselection: false }, id);
}
} else {
const roi = draft.rois[index];
roi.box = changeBoxRotationCenter(roi.box, options);
roi.box.angle = normalizeAngle(angle);
roi.action = { type: 'external' };
}
}
19 changes: 18 additions & 1 deletion src/hooks/useActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import type {
ZoomIntoROIOptions,
} from '../context/roiReducer.js';
import { zoomAction } from '../context/updaters/zoom.js';
import type { CommittedRoi, CommittedRoiProperties } from '../index.js';
import type {
CommittedRoi,
CommittedRoiProperties,
UpdateRoiAngleOptions,
} from '../index.js';
import type { UpdateRoiOptions } from '../types/actions.ts';
import type { RoiMode } from '../types/utils.js';
import type { Point } from '../utilities/point.js';
Expand Down Expand Up @@ -75,6 +79,19 @@ export function useActions<TData = unknown>() {
callbacksRef.current.onZoom(newState.panZoom);
}
},
/**
* Update the angle of the ROI, using a custom rotation center.
*/
updateRoiAngle: (
roiId: string,
angle: number,
options?: UpdateRoiAngleOptions,
) => {
roiDispatch({
type: 'UPDATE_ROI_ANGLE',
payload: { id: roiId, angle, options },
});
},
createRoi: (roi: CommittedRoiProperties<TData>) => {
roiDispatch({
type: 'CREATE_ROI',
Expand Down
8 changes: 8 additions & 0 deletions src/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ export interface UpdateRoiOptionsWithCommit {
commit: true;
boundaryStrategy?: UpdateRoiOptionsBoundaryStrategy;
}

export type XRotationCenter = 'left' | 'right' | 'center';
export type YRotationCenter = 'top' | 'bottom' | 'center';

export type UpdateRoiAngleOptions = UpdateRoiOptions & {
xRotationCenter: XRotationCenter;
yRotationCenter: YRotationCenter;
};
4 changes: 3 additions & 1 deletion src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ export interface PanZoom {
translation: [number, number];
}

export type RotationOrigin = 'top-left' | 'center';

export interface PanZoomWithOrigin extends PanZoom {
origin: 'top-left' | 'center';
origin: RotationOrigin;
}

export type RoiMode = 'select' | 'draw' | 'hybrid' | 'rotate_selected';
Expand Down
10 changes: 7 additions & 3 deletions src/utilities/box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
XCornerPosition,
YCornerPosition,
} from '../types/Roi.js';
import type { XRotationCenter, YRotationCenter } from '../types/actions.ts';
import type { CommittedBox } from '../types/box.js';
import type { PanZoom } from '../types/utils.js';

Expand Down Expand Up @@ -31,9 +32,6 @@ export interface Box {
height: number;
}

export type XRotationCenter = 'left' | 'right' | 'center';
export type YRotationCenter = 'top' | 'bottom' | 'center';

/**
* Represents the coordinates and angle of a box
* Units are pixels in the client's coordinates system and relative to the container's position.
Expand Down Expand Up @@ -319,6 +317,12 @@ export function changeBoxRotationCenter(
box: BoxWithRotationCenter,
newCenter: CenterOrigin,
): BoxWithRotationCenter {
if (
box.xRotationCenter === newCenter.xRotationCenter &&
box.yRotationCenter === newCenter.yRotationCenter
) {
return box;
}
const oldRotationCenter = getRotationCenter(box);
const altPoint = getRotationCenter({ ...box, ...newCenter });
const newCenterPoint = rotatePoint(altPoint, oldRotationCenter, box.angle);
Expand Down
98 changes: 97 additions & 1 deletion stories/1-actions/update.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
CommittedRoiProperties,
UpdateData,
UpdateRoiOptionsBoundaryStrategy,
XRotationCenter,
YRotationCenter,
} from '../../src/index.ts';
import {
RoiContainer,
Expand All @@ -23,7 +25,7 @@ export default {
title: 'Actions',
} as Meta;

export function UpdatePosition() {
export function UpdateRoiPosition() {
function UpdateXYPositionButton() {
const { selectedRoi } = useRoiState();
const { updateRoi } = useActions();
Expand Down Expand Up @@ -68,6 +70,100 @@ export function UpdatePosition() {
);
}

export function UpdateRoiAngle() {
function UpdateXYPositionButton() {
const [angle, setAngle] = useState('15');
const [xRotationCenter, setXRotationCenter] =
useState<XRotationCenter>('center');
const [yRotationCenter, setYRotationCenter] =
useState<YRotationCenter>('center');
const { selectedRoi } = useRoiState();
const { updateRoiAngle } = useActions();

function updateAngle(commit: boolean, angle: number) {
if (selectedRoi) {
updateRoiAngle(selectedRoi, (angle * Math.PI) / 180, {
xRotationCenter,
yRotationCenter,
commit,
boundaryStrategy: 'none',
});
}
}

return (
<div
style={{ display: 'grid', gridTemplateColumns: 'auto auto', gap: 5 }}
>
<label htmlFor="angle">Angle (degrees)</label>
<input
id="angle"
type="number"
onChange={(event) => setAngle(event.target.value)}
value={angle}
/>
<label htmlFor="xRotationCenter">Center X</label>
<select
id="xRotationCenter"
name="xRotationCenter"
onChange={(event) =>
setXRotationCenter(event.target.value as XRotationCenter)
}
>
<option value="center">Center</option>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
<label htmlFor="yRotationCenter">Center Y</label>
<select
id="yRotationCenter"
name="yRotationCenter"
onChange={(event) =>
setYRotationCenter(event.target.value as YRotationCenter)
}
>
<option value="center">Center</option>
<option value="top">Top</option>
<option value="bottom">Bottom</option>
</select>

<button type="button" onClick={() => updateAngle(true, Number(angle))}>
Set angle and commit
</button>
<input
type="range"
min={-180}
max={180}
step={1}
value={angle}
onChange={(event) => {
const angle = event.target.valueAsNumber;
setAngle(angle.toString());
updateAngle(false, angle);
}}
onPointerUp={() => {
updateAngle(true, Number(angle));
}}
/>
</div>
);
}

return (
<RoiProvider initialConfig={{ rois: getInitialRois(320, 320) }}>
<Layout>
<UpdateXYPositionButton />
<RoiContainer
target={<TargetImage id="story-image" src="/barbara.jpg" />}
>
<RoiList />
</RoiContainer>
<CommittedRoisButton />
</Layout>
</RoiProvider>
);
}

const baseRoi: CommittedRoiProperties<BoundaryStrategy> = {
id: 'editable',
x: 50,
Expand Down
19 changes: 10 additions & 9 deletions stories/utils/CommittedRoisButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ export function CommittedRoisButton(props: {
}
const newImage = image.clone();
for (const roi of rois) {
newImage.drawRectangle({
strokeColor: [255, 255, 255],
origin: {
column: Math.round(roi.x),
row: Math.round(roi.y),
const points = roi.getRectanglePoints();
newImage.drawPolygon(
points.map((point) => ({
row: point.y,
column: point.x,
})),
{
strokeColor: [255, 255, 255],
out: newImage,
},
width: Math.round(roi.width),
height: Math.round(roi.height),
out: newImage,
});
);
}

if (ref.current) {
Expand Down
Loading