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
41 changes: 41 additions & 0 deletions src/components/RoiList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CSSProperties, JSX, ReactNode, SVGAttributes } from 'react';
import { useMemo } from 'react';

import { useRois } from '../hooks/useRois.js';
import { useRoiState } from '../index.js';
Expand Down Expand Up @@ -52,6 +53,28 @@ export interface RoiListProps<TData = unknown> {
getOverlayOpacity?: GetOverlayOpacity<TData>;
allowRotate?: boolean;
showGrid?: boolean;
/**
* Spacing (in device pixels) between vertical grid lines along the horizontal axis.
* If not provided, grid lines will be spaced by `horizontalGridSize` pixels.
*/
gridSpacingX?: number;
/**
* Spacing (in device pixels) between horizontal grid lines along the vertical axis.
* If not provided, grid lines will be spaced by `verticalGridSize` pixels.
*/
gridSpacingY?: number;
/**
* Number of horizontal grid lines.
* horizontalGridSpacing takes precedence if both are provided.
* @default 2
*/
gridHorizontalLineCount?: number;
/**
* Number of vertical grid lines
* verticalGridSpacing takes precedence if both are provided.
* @default 2
*/
gridVerticalLineCount?: number;
}

export function RoiList<TData = unknown>(props: RoiListProps<TData>) {
Expand All @@ -62,6 +85,10 @@ export function RoiList<TData = unknown>(props: RoiListProps<TData>) {
renderLabel = defaultRenderLabel,
allowRotate = false,
showGrid = false,
gridHorizontalLineCount = 2,
gridVerticalLineCount = 2,
gridSpacingX,
gridSpacingY,
} = props;
const rois = useRois().slice();
const { selectedRoi } = useRoiState();
Expand All @@ -71,6 +98,19 @@ export function RoiList<TData = unknown>(props: RoiListProps<TData>) {
const roi = rois.splice(index, 1)[0];
rois.push(roi);
}
const gridOptions = useMemo(() => {
return {
gridHorizontalLineCount,
gridVerticalLineCount,
gridSpacingX,
gridSpacingY,
};
}, [
gridHorizontalLineCount,
gridVerticalLineCount,
gridSpacingX,
gridSpacingY,
]);

return (
<>
Expand All @@ -85,6 +125,7 @@ export function RoiList<TData = unknown>(props: RoiListProps<TData>) {
allowRotate={allowRotate}
isSelected={roi.id === selectedRoi}
showGrid={showGrid}
gridOptions={gridOptions}
/>
))}
</>
Expand Down
5 changes: 4 additions & 1 deletion src/components/box/BoxSvg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useRoiState } from '../../index.js';
import type { Roi } from '../../types/Roi.js';
import type { Box } from '../../utilities/box.js';
import { getAllCorners } from '../../utilities/corners.js';
import type { GetGridLinesOptions } from '../../utilities/grid.js';
import { getAllEdges, getAllGridLines } from '../../utilities/grid.js';

import { RoiBoxCorner } from './RoiBoxCorner.js';
Expand All @@ -27,6 +28,7 @@ export interface BoxAnnotationProps {
getStyle: GetStyleCallback;
allowRotate: boolean;
showGrid: boolean;
gridOptions: GetGridLinesOptions;
}

export function BoxSvg({
Expand All @@ -38,6 +40,7 @@ export function BoxSvg({
box,
allowRotate,
showGrid,
gridOptions,
}: BoxAnnotationProps) {
const isAltKeyDown = useIsKeyDown('Alt');
const roiDispatch = useRoiDispatch();
Expand Down Expand Up @@ -123,7 +126,7 @@ export function BoxSvg({
/>
{isSelected &&
showGrid &&
getAllGridLines(box).map((gridLine, idx) => (
getAllGridLines(box, gridOptions).map((gridLine, idx) => (
<RoiBoxGridLine
// eslint-disable-next-line react/no-array-index-key
key={idx}
Expand Down
4 changes: 4 additions & 0 deletions src/components/box/RoiBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
} from '../../index.js';
import type { Roi } from '../../types/Roi.js';
import { applyTransformToBox } from '../../utilities/box.js';
import type { GetGridLinesOptions } from '../../utilities/grid.ts';
import { computeTotalPanZoom } from '../../utilities/panZoom.js';
import { LabelBox } from '../label/LabelBox.js';

Expand All @@ -25,6 +26,7 @@ interface RoiBoxProps {
allowRotate: boolean;
getOverlayOpacity: GetOverlayOpacity;
showGrid: boolean;
gridOptions: GetGridLinesOptions;
}

function RoiBoxInternal(props: RoiBoxProps): JSX.Element {
Expand All @@ -37,6 +39,7 @@ function RoiBoxInternal(props: RoiBoxProps): JSX.Element {
getOverlayOpacity,
allowRotate,
showGrid,
gridOptions,
} = props;

const panzoom = usePanZoom();
Expand Down Expand Up @@ -87,6 +90,7 @@ function RoiBoxInternal(props: RoiBoxProps): JSX.Element {
getStyle={getStyle}
allowRotate={allowRotate}
showGrid={showGrid}
gridOptions={gridOptions}
/>
</div>
<LabelBox roi={roi} label={label} panZoom={totalPanzoom} />
Expand Down
40 changes: 32 additions & 8 deletions src/utilities/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,47 @@ export function getAllEdges(roi: Box, angle: number): EdgeData[] {

export type GridLineData = Pick<EdgeData, 'x1' | 'x2' | 'y1' | 'y2'>;

export function getAllGridLines(roi: Box): GridLineData[] {
const gridSize = 3;
export interface GetGridLinesOptions {
gridSpacingX?: number;
gridSpacingY?: number;
gridHorizontalLineCount: number;
gridVerticalLineCount: number;
}

export function getAllGridLines(
roi: Box,
options: GetGridLinesOptions,
): GridLineData[] {
const {
gridHorizontalLineCount,
gridVerticalLineCount,
gridSpacingY,
gridSpacingX,
} = options;

const gridSizeY = gridSpacingY
? Math.max(2, Math.floor(roi.height / gridSpacingY))
: gridHorizontalLineCount + 1;
const gridSizeX = gridSpacingX
? Math.max(2, Math.floor(roi.width / gridSpacingX))
: gridVerticalLineCount + 1;

// const gridSize = 3;
const lines = [];
for (let i = 1; i < gridSize; i++) {
for (let i = 1; i < gridSizeX; i++) {
lines.push({
x1: roi.x + (roi.width * i) / gridSize,
x1: roi.x + (roi.width * i) / gridSizeX,
y1: roi.y,
x2: roi.x + (roi.width * i) / gridSize,
x2: roi.x + (roi.width * i) / gridSizeX,
y2: roi.y + roi.height,
});
}
for (let j = 1; j < gridSize; j++) {
for (let j = 1; j < gridSizeY; j++) {
lines.push({
x1: roi.x,
y1: roi.y + (roi.height * j) / gridSize,
y1: roi.y + (roi.height * j) / gridSizeY,
x2: roi.x + roi.width,
y2: roi.y + (roi.height * j) / gridSize,
y2: roi.y + (roi.height * j) / gridSizeY,
});
}
return lines;
Expand Down
6 changes: 3 additions & 3 deletions stories/hooks/useActions/zoom-external.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function UpdateZoom({
justifyContent: 'center',
}}
type="button"
onClick={() => onClick(1.2)}
onClick={() => onClick(2)}
>
+
</button>
Expand All @@ -134,7 +134,7 @@ export function UpdateZoom({
justifyContent: 'center',
}}
type="button"
onClick={() => onClick(0.8)}
onClick={() => onClick(0.5)}
>
-
</button>
Expand Down Expand Up @@ -167,7 +167,7 @@ export function UpdateZoom({
}}
target={<TargetImage id="story-image" src="/barbara.jpg" />}
>
<RoiList />
<RoiList showGrid />
</RoiContainer>
</Layout>
<CommittedRoisButton />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Meta } from '@storybook/react-vite';
import type { CSSProperties, ReactElement, SVGAttributes } from 'react';
import { useState } from 'react';

import type { RoiListProps } from '../../src/index.ts';
import {
RoiContainer,
RoiList,
Expand All @@ -16,7 +18,12 @@ import { getInitialRois } from '../utils/initialRois.ts';
export default {
title: 'ROI custom styles',
args: {
showGrid: false,
allowRotate: false,
gridHorizontalLineCount: 2,
gridVerticalLineCount: 2,
gridSpacingX: 0,
gridSpacingY: 0,
},
} as Meta;

Expand All @@ -32,11 +39,29 @@ const initialRois = getInitialRois<CustomColorData>(320, 320, {
backgroundColor: 'green',
});

interface StoryProps {
allowRotate: boolean;
type StoryProps = Pick<
RoiListProps,
| 'allowRotate'
| 'showGrid'
| 'gridHorizontalLineCount'
| 'gridVerticalLineCount'
| 'gridSpacingX'
| 'gridSpacingY'
>;

export function ControlGrid(props: StoryProps) {
return (
<Layout>
<RoiProvider initialConfig={{ rois: initialRois }}>
<RoiContainer target={<TargetImage src="/barbara.jpg" />}>
<RoiList<CustomColorData> {...props} />
</RoiContainer>
</RoiProvider>
</Layout>
);
}

export function WithShadowAroundSelectedRoi({ allowRotate }: StoryProps) {
export function WithShadowAroundSelectedRoi(props: StoryProps) {
return (
<Layout>
<RoiProvider initialConfig={{ rois: initialRois, zoom: { min: 0.1 } }}>
Expand All @@ -45,7 +70,7 @@ export function WithShadowAroundSelectedRoi({ allowRotate }: StoryProps) {
target={<TargetImage src="/barbara.jpg" />}
>
<RoiList<CustomColorData>
allowRotate={allowRotate}
{...props}
getOverlayOpacity={(roi, { isSelected }) =>
isSelected && (roi.box.width > 0 || roi.box.height > 0) ? 0.6 : 0
}
Expand All @@ -62,14 +87,14 @@ export function WithShadowAroundSelectedRoi({ allowRotate }: StoryProps) {
);
}

export function OverrideDefaultStyle({ allowRotate }: StoryProps) {
export function OverrideDefaultStyle(props: StoryProps) {
return (
<Layout>
<RoiProvider initialConfig={{ rois: getInitialRois(320, 320) }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<RoiContainer target={<TargetImage src="/barbara.jpg" />}>
<RoiList<CustomColorData>
allowRotate={allowRotate}
{...props}
getStyle={(roi, { isSelected }) => {
const patternId = `stripe-pattern-${roi.id}`;
return {
Expand Down Expand Up @@ -105,7 +130,7 @@ export function OverrideDefaultStyle({ allowRotate }: StoryProps) {
);
}

export function WithIndividualStyles({ allowRotate }: StoryProps) {
export function WithIndividualStyles(props: StoryProps) {
function UpdateStyleButton() {
const { selectedRoi } = useRoiState();
const { updateRoi } = useActions<CustomColorData>();
Expand Down Expand Up @@ -135,7 +160,7 @@ export function WithIndividualStyles({ allowRotate }: StoryProps) {

<RoiContainer target={<TargetImage src="/barbara.jpg" />}>
<RoiList<CustomColorData>
allowRotate={allowRotate}
{...props}
getStyle={(roi, { isSelected }) => {
const { data } = roi;
assert(data);
Expand Down Expand Up @@ -235,14 +260,37 @@ function StripePattern(props: {
);
}

export function ShowGrid({ allowRotate }: StoryProps) {
export function StyleNewRoi(props: StoryProps) {
const [selectedColor, setSelectedColor] = useState('blue');
return (
<Layout>
<RoiProvider initialConfig={{ rois: initialRois }}>
<RoiContainer target={<TargetImage src="/barbara.jpg" />}>
<RoiList<CustomColorData> allowRotate={allowRotate} showGrid />
<RoiProvider>
<Layout>
<select
value={selectedColor}
onChange={(event) => setSelectedColor(event.target.value)}
>
<option value="blue">Blue</option>
<option value="red">Red</option>
<option value="green">Green</option>
</select>
<RoiContainer<string>
getNewRoiData={() => {
return selectedColor;
}}
target={<TargetImage id="story-image" src="/barbara.jpg" />}
>
<RoiList<string>
{...props}
getStyle={(roi) => {
return {
rectAttributes: {
fill: roi.data,
},
};
}}
/>
</RoiContainer>
</RoiProvider>
</Layout>
</Layout>
</RoiProvider>
);
}
Loading
Loading