diff --git a/src/context/roiReducer.tsx b/src/context/roiReducer.tsx index 619df3a..9c4bd6f 100644 --- a/src/context/roiReducer.tsx +++ b/src/context/roiReducer.tsx @@ -12,6 +12,7 @@ import type { } from '../index.js'; import type { CommittedRoiProperties } from '../types/CommittedRoi.js'; import type { Roi, XCornerPosition, YCornerPosition } from '../types/Roi.js'; +import type { UpdateRoiOptions } from '../types/actions.ts'; import { assert, assertUnreachable } from '../utilities/assert.js'; import { changeBoxRotationCenter, @@ -29,6 +30,7 @@ import { cancelAction } from './updaters/cancelAction.js'; import { endAction } from './updaters/endAction.js'; import { updateInitialPanZoom } from './updaters/initialPanZoom.js'; import { pointerMove } from './updaters/pointerMove.js'; +import { updateCommittedRoiPosition } from './updaters/roi.ts'; import { sanitizeRois } from './updaters/sanitizeRois.js'; import { prepareSelectedBoxForAction, @@ -119,16 +121,6 @@ export interface ReactRoiState { zoomDomain: ZoomDomain; } -export interface UpdateRoiOptions { - /** - * Whether the update should be committed immediately. - * In both cases, the ROI will move immediately on screen. - * If set to false, the ROI will enter a mode where it can't be modified through user interactions until committed. - * Setting it to false also prevents having frequent updates to committed ROIs when those can trigger expensive operations. - */ - commit: boolean; -} - export type UpdateRoiPayload = Partial & { id: string; options?: UpdateRoiOptions; @@ -335,7 +327,7 @@ export function roiReducer( case 'UPDATE_ROI': { const { id, - options = { commit: true, boundingStrategy: 'none' }, + options = { commit: true, boundaryStrategy: 'none' }, ...updatedData } = action.payload; if (!id) return; @@ -344,11 +336,21 @@ export function roiReducer( if (options.commit) { const committedRoi = draft.committedRois[index]; assert(committedRoi, 'Committed ROI not found'); - Object.assign< - CommittedRoiProperties, - Partial - >(committedRoi, updatedData); - draft.rois[index] = createRoiFromCommittedRoi(committedRoi); + draft.rois[index] = createRoiFromCommittedRoi({ + ...committedRoi, + ...updatedData, + }); + try { + updateCommittedRoiPosition(draft, committedRoi, draft.rois[index], { + boundaryStrategy: options.boundaryStrategy ?? 'none', + boxStrategy: 'exact', + }); + if (updatedData.data) { + committedRoi.data = updatedData.data; + } + } catch { + cancelAction(draft, { noUnselection: false }, id); + } } else { const roi = draft.rois[index]; const { x, y, width, height, angle, ...otherData } = updatedData; diff --git a/src/context/updaters/cancelAction.ts b/src/context/updaters/cancelAction.ts index 89f60ba..0993154 100644 --- a/src/context/updaters/cancelAction.ts +++ b/src/context/updaters/cancelAction.ts @@ -1,5 +1,6 @@ import type { Draft } from 'immer'; +import type { Roi } from '../../types/Roi.ts'; import { assert } from '../../utilities/assert.js'; import { denormalizeBox } from '../../utilities/box.js'; import type { CancelActionPayload, ReactRoiState } from '../roiReducer.js'; @@ -7,8 +8,20 @@ import type { CancelActionPayload, ReactRoiState } from '../roiReducer.js'; export function cancelAction( draft: Draft, payload: CancelActionPayload, + /** + * If you want to cancel action for a specific ROI, provide its ID here. + * Otherwise, all ROIs with non-idle actions will be cancelled. + */ + roiId?: string, ) { - const rois = draft.rois.filter((roi) => roi.action.type !== 'idle'); + function byRoiId(roi: Roi) { + return roi.id === roiId; + } + function byIdleAction(roi: Roi) { + return roi.action.type !== 'idle'; + } + + const rois = draft.rois.filter(roiId ? byRoiId : byIdleAction); if (rois.length === 0) { return; diff --git a/src/context/updaters/roi.ts b/src/context/updaters/roi.ts index 40d05ac..58d089e 100644 --- a/src/context/updaters/roi.ts +++ b/src/context/updaters/roi.ts @@ -1,6 +1,7 @@ import type { Draft } from 'immer'; import type { + BoundaryStrategy, CommittedBox, CommittedRoiProperties, Size, @@ -14,8 +15,9 @@ import { denormalizeBox, normalizeBox, } from '../../utilities/box.js'; +import { rectanglesIntersect } from '../../utilities/intersection.ts'; import { getMBRBoundaries } from '../../utilities/rotate.js'; -import type { ReactRoiState } from '../roiReducer.js'; +import type { CommitBoxStrategy, ReactRoiState } from '../roiReducer.js'; export function boundBox( committedBox: CommittedBox, @@ -77,15 +79,15 @@ export function boundBox( break; } case 'is_partially_inside': { - const mbr = getMBRBoundaries(committedBox); - // The separating axis theorem says that if any of the projections of 2 convex shapes do not overlap, then the shapes do not overlap. - if ( - mbr.maxX < 0 || - mbr.minX > targetSize.width || - mbr.maxY < 0 || - mbr.minY > targetSize.height - ) { - // Throwing an error here will cancel the operation + const targetRectangle = { + x: 0, + y: 0, + width: targetSize.width, + height: targetSize.height, + angle: 0, + }; + const intersects = rectanglesIntersect(committedBox, targetRectangle); + if (!intersects) { throw new Error('ROI is completely outside of target boundaries'); } break; @@ -114,13 +116,21 @@ export function updateCommittedRoiPosition( draft: ReactRoiState, committedRoi: Draft, roi: Draft, + strategies?: { + boxStrategy?: CommitBoxStrategy; + boundaryStrategy?: BoundaryStrategy; + }, ) { const strategy = getBoundaryStrategyFromAction( roi, - draft.commitRoiBoundaryStrategy, + strategies?.boundaryStrategy ?? draft.commitRoiBoundaryStrategy, ); const normalizedBox = boundBox( - commitBox(normalizeBox(roi.box), roi.action, draft.commitRoiBoxStrategy), + commitBox( + normalizeBox(roi.box), + roi.action, + strategies?.boxStrategy ?? draft.commitRoiBoxStrategy, + ), draft.targetSize, strategy, ); diff --git a/src/hooks/useActions.ts b/src/hooks/useActions.ts index ccea937..5501f24 100644 --- a/src/hooks/useActions.ts +++ b/src/hooks/useActions.ts @@ -4,11 +4,11 @@ import { useMemo } from 'react'; import type { CancelActionPayload, - UpdateRoiOptions, ZoomIntoROIOptions, } from '../context/roiReducer.js'; import { zoomAction } from '../context/updaters/zoom.js'; import type { CommittedRoi, CommittedRoiProperties } from '../index.js'; +import type { UpdateRoiOptions } from '../types/actions.ts'; import type { RoiMode } from '../types/utils.js'; import type { Point } from '../utilities/point.js'; @@ -20,6 +20,7 @@ import { useRoiDispatch } from './useRoiDispatch.js'; export type UpdateData = Partial< Omit, 'id'> >; + export function useActions() { const roiDispatch = useRoiDispatch(); const containerRef = useRoiContainerRef(); diff --git a/src/index.ts b/src/index.ts index d101060..a81c0cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export * from './types/utils.js'; export * from './types/box.js'; export * from './types/CommittedRoi.js'; export * from './types/RoiList.js'; +export * from './types/actions.js'; export * from './hooks/useActions.js'; export * from './hooks/useCommittedRois.js'; diff --git a/src/types/CommittedRoi.ts b/src/types/CommittedRoi.ts index e557f73..70a0be0 100644 --- a/src/types/CommittedRoi.ts +++ b/src/types/CommittedRoi.ts @@ -1,5 +1,4 @@ import { getRectanglePoints } from '../utilities/box.js'; -import type { Point } from '../utilities/point.js'; import type { Roi } from './Roi.js'; import type { CommittedBox } from './box.js'; @@ -29,7 +28,14 @@ export class CommittedRoi this.angle = properties.angle; this.data = properties.data; } - public getRectanglePoints(): Point[] { + + /** + * Get the four corner points of the rectangle representing this ROI. + * The unrotated rectangle is used to determine the order of the points: + * top-left, top-right, bottom-right, bottom-left. + * @return The four corner points of the rectangle. + */ + public getRectanglePoints() { return getRectanglePoints(this); } } diff --git a/src/types/actions.ts b/src/types/actions.ts new file mode 100644 index 0000000..d433d39 --- /dev/null +++ b/src/types/actions.ts @@ -0,0 +1,26 @@ +import type { BoundaryStrategy } from './utils.ts'; + +export type UpdateRoiOptions = + | UpdateRoiOptionsNoCommit + | UpdateRoiOptionsWithCommit; + +export interface UpdateRoiOptionsNoCommit { + /** + * Whether the update should be committed immediately. + * In both cases, the ROI will move immediately on screen. + * If set to false, the ROI will enter a mode where it can't be modified through user interactions until committed. + * Setting it to false also prevents having frequent updates to committed ROIs when those can trigger expensive operations. + */ + commit: false; +} + +// The inside_auto strategy is excluded because it requires to know about the nature of the action (move, resize, etc.). +export type UpdateRoiOptionsBoundaryStrategy = Exclude< + BoundaryStrategy, + 'inside_auto' +>; + +export interface UpdateRoiOptionsWithCommit { + commit: true; + boundaryStrategy?: UpdateRoiOptionsBoundaryStrategy; +} diff --git a/src/utilities/box.ts b/src/utilities/box.ts index 5b8eb3c..f264dc8 100644 --- a/src/utilities/box.ts +++ b/src/utilities/box.ts @@ -387,7 +387,9 @@ export const yAxisCornerToCenter: Record = { center: 'center', }; -export function getRectanglePoints(box: CommittedBox): Point[] { +export function getRectanglePoints( + box: CommittedBox, +): [Point, Point, Point, Point] { const { x, y, width, height, angle } = box; const center: Point = { x, y }; return [ diff --git a/src/utilities/intersection.ts b/src/utilities/intersection.ts new file mode 100644 index 0000000..573c4a9 --- /dev/null +++ b/src/utilities/intersection.ts @@ -0,0 +1,50 @@ +import type { CommittedBox } from '../types/box.ts'; + +import { getRectanglePoints } from './box.ts'; +import type { Point } from './point.ts'; +import { dotProduct, subtract } from './point.ts'; + +// Check if 2 rectangles intersect based on the separating Axis Theorem (SAT) +export function rectanglesIntersect( + rect1: CommittedBox, + rect2: CommittedBox, +): boolean { + const corners1 = getRectanglePoints(rect1); + const corners2 = getRectanglePoints(rect2); + + const axes = [...getAxes(corners1), ...getAxes(corners2)]; + + for (const axis of axes) { + const proj1 = projectOntoAxis(corners1, axis); + const proj2 = projectOntoAxis(corners2, axis); + + if (proj1.max < proj2.min || proj2.max < proj1.min) { + return false; // Gap found, no intersection + } + } + + return true; +} + +function getAxes(corners: Point[]): Point[] { + const axes: Point[] = []; + for (let i = 0; i < corners.length; i++) { + const p1 = corners[i]; + const p2 = corners[(i + 1) % corners.length]; + const edge = subtract(p2, p1); + // Perpendicular axis + axes.push({ x: -edge.y, y: edge.x }); + } + return axes; +} + +function projectOntoAxis( + corners: Point[], + axis: Point, +): { min: number; max: number } { + const projections = corners.map((c) => dotProduct(c, axis)); + return { + min: Math.min(...projections), + max: Math.max(...projections), + }; +} diff --git a/src/utilities/point.ts b/src/utilities/point.ts index cd70799..96ceb86 100644 --- a/src/utilities/point.ts +++ b/src/utilities/point.ts @@ -16,6 +16,13 @@ export function add(pointA: Point, pointB: Point): Point { }; } +export function subtract(pointA: Point, pointB: Point): Point { + return { + x: pointA.x - pointB.x, + y: pointA.y - pointB.y, + }; +} + export function mulScalar(point: Point, scalar: number): Point { return { x: point.x * scalar, @@ -23,6 +30,10 @@ export function mulScalar(point: Point, scalar: number): Point { }; } +export function dotProduct(pointA: Point, pointB: Point): number { + return pointA.x * pointB.x + pointA.y * pointB.y; +} + export function getBoundaries(points: Point[]) { assert(points.length > 1, 'must pass at least 2 points'); let maxX = 0; diff --git a/stories/hooks/useActions/add.stories.tsx b/stories/1-actions/add.stories.tsx similarity index 77% rename from stories/hooks/useActions/add.stories.tsx rename to stories/1-actions/add.stories.tsx index d90a78c..431e0ac 100644 --- a/stories/hooks/useActions/add.stories.tsx +++ b/stories/1-actions/add.stories.tsx @@ -6,13 +6,13 @@ import { RoiProvider, TargetImage, useActions, -} from '../../../src/index.ts'; -import { CommittedRoisButton } from '../../utils/CommittedRoisButton.tsx'; -import { Layout } from '../../utils/Layout.tsx'; -import { getInitialRois } from '../../utils/initialRois.ts'; +} from '../../src/index.ts'; +import { CommittedRoisButton } from '../utils/CommittedRoisButton.tsx'; +import { Layout } from '../utils/Layout.tsx'; +import { getInitialRois } from '../utils/initialRois.ts'; export default { - title: 'hooks/useActions', + title: 'Actions', } as Meta; export function AddROI() { diff --git a/stories/hooks/useActions/cancel.stories.tsx b/stories/1-actions/cancel.stories.tsx similarity index 84% rename from stories/hooks/useActions/cancel.stories.tsx rename to stories/1-actions/cancel.stories.tsx index 9889b0d..71bedb7 100644 --- a/stories/hooks/useActions/cancel.stories.tsx +++ b/stories/1-actions/cancel.stories.tsx @@ -7,12 +7,12 @@ import { RoiProvider, TargetImage, useActions, -} from '../../../src/index.ts'; -import { Layout } from '../../utils/Layout.tsx'; -import { getInitialRois } from '../../utils/initialRois.ts'; +} from '../../src/index.ts'; +import { Layout } from '../utils/Layout.tsx'; +import { getInitialRois } from '../utils/initialRois.ts'; export default { - title: 'hooks/useActions', + title: 'Actions', decorators: [ (Story) => ( diff --git a/stories/hooks/useActions/mode.stories.tsx b/stories/1-actions/mode.stories.tsx similarity index 81% rename from stories/hooks/useActions/mode.stories.tsx rename to stories/1-actions/mode.stories.tsx index 49fb8ef..fd5c87b 100644 --- a/stories/hooks/useActions/mode.stories.tsx +++ b/stories/1-actions/mode.stories.tsx @@ -1,19 +1,19 @@ import type { Meta } from '@storybook/react-vite'; import type { ChangeEvent } from 'react'; -import type { RoiMode } from '../../../src/index.ts'; +import type { RoiMode } from '../../src/index.ts'; import { RoiContainer, RoiList, RoiProvider, TargetImage, useActions, -} from '../../../src/index.ts'; -import { CommittedRoisButton } from '../../utils/CommittedRoisButton.tsx'; -import { Layout } from '../../utils/Layout.tsx'; +} from '../../src/index.ts'; +import { CommittedRoisButton } from '../utils/CommittedRoisButton.tsx'; +import { Layout } from '../utils/Layout.tsx'; export default { - title: 'hooks/useActions', + title: 'Actions', } as Meta; export function ChangeMode() { diff --git a/stories/hooks/useActions/remove.stories.tsx b/stories/1-actions/remove.stories.tsx similarity index 78% rename from stories/hooks/useActions/remove.stories.tsx rename to stories/1-actions/remove.stories.tsx index f3002f1..1a5481e 100644 --- a/stories/hooks/useActions/remove.stories.tsx +++ b/stories/1-actions/remove.stories.tsx @@ -7,13 +7,13 @@ import { TargetImage, useActions, useRoiState, -} from '../../../src/index.ts'; -import { CommittedRoisButton } from '../../utils/CommittedRoisButton.tsx'; -import { Layout } from '../../utils/Layout.tsx'; -import { getInitialRois } from '../../utils/initialRois.ts'; +} from '../../src/index.ts'; +import { CommittedRoisButton } from '../utils/CommittedRoisButton.tsx'; +import { Layout } from '../utils/Layout.tsx'; +import { getInitialRois } from '../utils/initialRois.ts'; export default { - title: 'hooks/useActions', + title: 'Actions', } as Meta; export function RemoveROI() { diff --git a/stories/hooks/useActions/rotation-mode.stories.tsx b/stories/1-actions/rotation-mode.stories.tsx similarity index 91% rename from stories/hooks/useActions/rotation-mode.stories.tsx rename to stories/1-actions/rotation-mode.stories.tsx index 5116a5a..d6b398b 100644 --- a/stories/hooks/useActions/rotation-mode.stories.tsx +++ b/stories/1-actions/rotation-mode.stories.tsx @@ -7,12 +7,12 @@ import { RoiProvider, TargetImage, useActions, -} from '../../../src/index.ts'; -import { Layout } from '../../utils/Layout.tsx'; -import { getInitialRois } from '../../utils/initialRois.ts'; +} from '../../src/index.ts'; +import { Layout } from '../utils/Layout.tsx'; +import { getInitialRois } from '../utils/initialRois.ts'; export default { - title: 'hooks/useActions', + title: 'Actions', decorators: [ (Story) => ( + + + + ); + } + + return ( + + + + } + > + + + + + + ); +} + +const baseRoi: CommittedRoiProperties = { + id: 'editable', + x: 50, + y: 50, + width: 100, + height: 100, + angle: 0, +}; +const initialRois: Array> = [ + { + ...baseRoi, + id: 'non-editable', + data: 'partially_inside', + }, + + baseRoi, +]; + +export function UpdatePositionWithCommitStrategyDebug() { + const [strategy, setStrategy] = + useState('partially_inside'); + function SelectStrategy() { + const { updateRoi } = useActions(); + return ( + + ); + } + return ( + { + actions.updateRoi( + 'non-editable', + { + width: roi.width, + height: roi.height, + x: roi.x, + y: roi.y, + angle: roi.angle, + }, + { + commit: true, + boundaryStrategy: strategy, + }, + ); + }} + > + +

+ Update the black / transparent ROI and see how it affects the orange + ROI. +
+ { + "On commit, the black / transparent ROI's coordinate are passed to the orange ROI via the `updateRoi` action, with the boundary strategy selected below." + } +

+ + + } + style={{ border: '1px solid red' }} + > + + displayRotationHandle + getReadOnly={(roi) => roi.id !== 'editable'} + renderLabel={({ data }) => data} + getStyle={({ isReadOnly }) => { + if (isReadOnly) { + return { + rectAttributes: { + fill: 'orange', + }, + resizeHandlerColor: 'rgba(0,0,0,0.1)', + }; + } + return { + rectAttributes: { + fill: 'rgba(0,0,0,0.2)', + }, + }; + }} + /> + +
+
+ ); +} + +export function UpdateLabel() { + function UpdateLabelButton() { + const { selectedRoi } = useRoiState(); + const { updateRoi } = useActions(); + + function onClick() { + if (selectedRoi) { + updateRoi(selectedRoi, { label: 'Hello, World!' }); + } + } + + return ( + + ); + } + + return ( + + + + }> + + + + + ); +} diff --git a/stories/hooks/useActions/zoom-external.stories.tsx b/stories/1-actions/zoom-external.stories.tsx similarity index 92% rename from stories/hooks/useActions/zoom-external.stories.tsx rename to stories/1-actions/zoom-external.stories.tsx index b3a58ed..4f252af 100644 --- a/stories/hooks/useActions/zoom-external.stories.tsx +++ b/stories/1-actions/zoom-external.stories.tsx @@ -1,6 +1,6 @@ import type { Meta } from '@storybook/react-vite'; -import type { PanZoom, ResizeStrategy } from '../../../src/index.ts'; +import type { PanZoom, ResizeStrategy } from '../../src/index.ts'; import { RoiContainer, RoiList, @@ -8,14 +8,14 @@ import { TargetImage, useActions, useCommittedRois, -} from '../../../src/index.ts'; -import { CommittedRoisButton } from '../../utils/CommittedRoisButton.tsx'; -import { Layout } from '../../utils/Layout.tsx'; -import { getInitialRois } from '../../utils/initialRois.ts'; -import { useResetOnChange } from '../../utils/useResetOnChange.ts'; +} from '../../src/index.ts'; +import { CommittedRoisButton } from '../utils/CommittedRoisButton.tsx'; +import { Layout } from '../utils/Layout.tsx'; +import { getInitialRois } from '../utils/initialRois.ts'; +import { useResetOnChange } from '../utils/useResetOnChange.ts'; export default { - title: 'hooks/useActions', + title: 'Actions', argTypes: { onZoomChange: { action: 'zoom' }, minZoom: { diff --git a/stories/roi-styles/custom_styles.stories.tsx b/stories/2-roi-styles/custom_styles.stories.tsx similarity index 99% rename from stories/roi-styles/custom_styles.stories.tsx rename to stories/2-roi-styles/custom_styles.stories.tsx index bd3945b..413aa2a 100644 --- a/stories/roi-styles/custom_styles.stories.tsx +++ b/stories/2-roi-styles/custom_styles.stories.tsx @@ -16,7 +16,7 @@ import { Layout } from '../utils/Layout.tsx'; import { getInitialRois } from '../utils/initialRois.ts'; export default { - title: 'ROI custom styles', + title: 'Custom styles', args: { showGrid: false, displayRotationHandle: false, diff --git a/stories/panzoom/initial-zoom.stories.tsx b/stories/3-panzoom/initial-zoom.stories.tsx similarity index 100% rename from stories/panzoom/initial-zoom.stories.tsx rename to stories/3-panzoom/initial-zoom.stories.tsx diff --git a/stories/panzoom/lock.stories.tsx b/stories/3-panzoom/lock.stories.tsx similarity index 100% rename from stories/panzoom/lock.stories.tsx rename to stories/3-panzoom/lock.stories.tsx diff --git a/stories/panzoom/no-alt-key.stories.tsx b/stories/3-panzoom/no-alt-key.stories.tsx similarity index 100% rename from stories/panzoom/no-alt-key.stories.tsx rename to stories/3-panzoom/no-alt-key.stories.tsx diff --git a/stories/preferences/bounding-strategy.stories.tsx b/stories/4-preferences/bounding-strategy.stories.tsx similarity index 100% rename from stories/preferences/bounding-strategy.stories.tsx rename to stories/4-preferences/bounding-strategy.stories.tsx diff --git a/stories/preferences/commit-strategy.stories.tsx b/stories/4-preferences/commit-strategy.stories.tsx similarity index 100% rename from stories/preferences/commit-strategy.stories.tsx rename to stories/4-preferences/commit-strategy.stories.tsx diff --git a/stories/preferences/layouts.stories.tsx b/stories/4-preferences/layouts.stories.tsx similarity index 100% rename from stories/preferences/layouts.stories.tsx rename to stories/4-preferences/layouts.stories.tsx diff --git a/stories/lifecycle/lifecycle.stories.tsx b/stories/5-lifecycle/lifecycle.stories.tsx similarity index 95% rename from stories/lifecycle/lifecycle.stories.tsx rename to stories/5-lifecycle/lifecycle.stories.tsx index 0cd9519..23f6f32 100644 --- a/stories/lifecycle/lifecycle.stories.tsx +++ b/stories/5-lifecycle/lifecycle.stories.tsx @@ -194,7 +194,11 @@ export function SyncRoisAfterUpdate() { }; return ( - initialConfig={{ mode: 'hybrid', rois: syncInitialRois }} + initialConfig={{ + mode: 'hybrid', + rois: syncInitialRois, + commitRoiBoundaryStrategy: 'partially_inside', + }} onCommit={updateRois} > @@ -245,7 +249,7 @@ export function SyncRoisDuringUpdate() { height: roi.height, angle: roi.angle, }, - { commit }, + { commit, boundaryStrategy: 'none' }, ); } else { const leftRoi = roisBeforeCommit.find( @@ -261,14 +265,18 @@ export function SyncRoisDuringUpdate() { height: roi.height, angle: roi.angle, }, - { commit }, + { commit, boundaryStrategy: 'none' }, ); } }; return ( - initialConfig={{ mode: 'hybrid', rois: syncInitialRois }} + initialConfig={{ + mode: 'hybrid', + rois: syncInitialRois, + commitRoiBoundaryStrategy: 'partially_inside', + }} onCommit={(param) => { const { roi, actions, actionType } = param; assert(roi.data); diff --git a/stories/full-examples/crop.stories.tsx b/stories/6-full-examples/crop.stories.tsx similarity index 100% rename from stories/full-examples/crop.stories.tsx rename to stories/6-full-examples/crop.stories.tsx diff --git a/stories/selection/initial-selected.stories.tsx b/stories/7-selection/initial-selected.stories.tsx similarity index 100% rename from stories/selection/initial-selected.stories.tsx rename to stories/7-selection/initial-selected.stories.tsx diff --git a/stories/edge-cases/draw.stories.tsx b/stories/8-edge-cases/draw.stories.tsx similarity index 100% rename from stories/edge-cases/draw.stories.tsx rename to stories/8-edge-cases/draw.stories.tsx diff --git a/stories/edge-cases/focus-out.stories.tsx b/stories/8-edge-cases/focus-out.stories.tsx similarity index 100% rename from stories/edge-cases/focus-out.stories.tsx rename to stories/8-edge-cases/focus-out.stories.tsx diff --git a/stories/edge-cases/id.stories.tsx b/stories/8-edge-cases/id.stories.tsx similarity index 100% rename from stories/edge-cases/id.stories.tsx rename to stories/8-edge-cases/id.stories.tsx diff --git a/stories/edge-cases/init-box.stories.tsx b/stories/8-edge-cases/init-box.stories.tsx similarity index 100% rename from stories/edge-cases/init-box.stories.tsx rename to stories/8-edge-cases/init-box.stories.tsx diff --git a/stories/edge-cases/read-only.stories.tsx b/stories/8-edge-cases/read-only.stories.tsx similarity index 100% rename from stories/edge-cases/read-only.stories.tsx rename to stories/8-edge-cases/read-only.stories.tsx diff --git a/stories/edge-cases/readonly.stories.tsx b/stories/8-edge-cases/readonly.stories.tsx similarity index 100% rename from stories/edge-cases/readonly.stories.tsx rename to stories/8-edge-cases/readonly.stories.tsx diff --git a/stories/edge-cases/scroll.stories.tsx b/stories/8-edge-cases/scroll.stories.tsx similarity index 100% rename from stories/edge-cases/scroll.stories.tsx rename to stories/8-edge-cases/scroll.stories.tsx diff --git a/stories/hooks/useActions/update.stories.tsx b/stories/hooks/useActions/update.stories.tsx deleted file mode 100644 index dc2891f..0000000 --- a/stories/hooks/useActions/update.stories.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import type { Meta } from '@storybook/react-vite'; - -import type { UpdateData } from '../../../src/index.ts'; -import { - RoiContainer, - RoiList, - RoiProvider, - TargetImage, - useActions, - useRoiState, -} from '../../../src/index.ts'; -import { CommittedRoisButton } from '../../utils/CommittedRoisButton.tsx'; -import { Layout } from '../../utils/Layout.tsx'; -import { getInitialRois } from '../../utils/initialRois.ts'; - -export default { - title: 'hooks/useActions', -} as Meta; - -export function UpdatePosition() { - function UpdateXYPositionButton() { - const { selectedRoi } = useRoiState(); - const { updateRoi } = useActions(); - - function onClick(type: 'start' | 'top') { - const updated: UpdateData = {}; - if (type === 'start') { - updated.x = 0; - } else { - updated.y = 0; - } - - if (selectedRoi) { - updateRoi(selectedRoi, updated); - } - } - - return ( -
- - -
- ); - } - - return ( - - - - } - > - - - - - - ); -} - -export function UpdateLabel() { - function UpdateLabelButton() { - const { selectedRoi } = useRoiState(); - const { updateRoi } = useActions(); - - function onClick() { - if (selectedRoi) { - updateRoi(selectedRoi, { label: 'Hello, World!' }); - } - } - - return ( - - ); - } - - return ( - - - - }> - - - - - ); -} diff --git a/stories/hooks/useCommittedRois/committed.stories.tsx b/stories/hooks/useCommittedRois/committed.stories.tsx deleted file mode 100644 index 17f54a1..0000000 --- a/stories/hooks/useCommittedRois/committed.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type { Meta } from '@storybook/react-vite'; -import { decode, writeCanvas } from 'image-js'; -import { useEffect, useRef } from 'react'; - -import { - RoiContainer, - RoiList, - RoiProvider, - TargetImage, - useCommittedRois, -} from '../../../src/index.ts'; -import { Layout } from '../../utils/Layout.tsx'; -import { getInitialRois } from '../../utils/initialRois.ts'; - -export default { - title: 'hooks/useCommittedRois', - decorators: [ - (Story) => ( - - - }> - - - - - - ), - ], -} as Meta; - -export function DisplayCommitedRois() { - const ref = useRef(null); - const rois = useCommittedRois(); - - useEffect(() => { - void fetch('/barbara.jpg') - .then((response) => response.arrayBuffer()) - .then((buffer) => { - const image = decode(new DataView(buffer)); - - for (const roi of rois) { - image.drawRectangle({ - strokeColor: [255, 255, 255], - origin: { - column: roi.x, - row: roi.y, - }, - width: roi.width, - height: roi.height, - out: image, - }); - } - - if (ref.current) { - writeCanvas(image, ref.current); - } - }); - }, [rois]); - - return ; -}