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
30 changes: 19 additions & 11 deletions src/context/RoiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ export interface RoiProviderInitialConfig<TData> {
* @default 'inside_auto'
*/
commitRoiBoundaryStrategy?: BoundaryStrategy;
/**
* Defines how the target should fit within the container at initialization.
* - `none`: The target is not transformed relative to the container. The top-left corner of the target matches the
* top-left corner of the container.
* - `contain`: The target is transformed such as the full target is visible in the container. The center of the
* target matches the center of the container.
* - `cover`: The target is scaled down such that one of the dimensions is fitting the container exactly, or not
* scaled at all if the target is smaller than the container. The center of the target matches the center
* of the container.
* - `center`: The center of the target matches the center of the container, without scaling the target.
* @default 'contain'
*/
resizeStrategy?: ResizeStrategy;
/**
* How should the roi be updated before committing it when created / moved / resized / rotated.
Expand Down Expand Up @@ -120,7 +132,8 @@ function createInitialState<T>(
return {
mode: initialConfig.mode,
action: 'idle',
isInitialized: false,
isContainerInitialized: false,
isTargetInitialized: false,
resizeStrategy: initialConfig.resizeStrategy,
commitRoiBoxStrategy: initialConfig.commitRoiBoxStrategy,
commitRoiBoundaryStrategy: initialConfig.commitRoiBoundaryStrategy,
Expand Down Expand Up @@ -219,7 +232,8 @@ export function RoiProvider<TData>(props: RoiProviderProps<TData>) {
startPanZoom,
targetSize,
containerSize,
isInitialized,
isTargetInitialized,
isContainerInitialized,
action,
} = state;
const roiState: RoiState = useMemo(() => {
Expand All @@ -230,6 +244,7 @@ export function RoiProvider<TData>(props: RoiProviderProps<TData>) {
};
}, [mode, selectedRoi, action]);

const isReady = isTargetInitialized && isContainerInitialized;
const panzoomContextValue: PanZoomContext = useMemo(() => {
return {
targetSize,
Expand All @@ -238,16 +253,9 @@ export function RoiProvider<TData>(props: RoiProviderProps<TData>) {
basePanZoom,
startPanZoom,
// We memoize this here to minimize the number of times we need to render the container
isReady: isInitialized,
isReady,
};
}, [
panZoom,
basePanZoom,
startPanZoom,
isInitialized,
targetSize,
containerSize,
]);
}, [panZoom, basePanZoom, startPanZoom, isReady, targetSize, containerSize]);

return (
<callbacksRefContext.Provider value={callbacksRef}>
Expand Down
29 changes: 21 additions & 8 deletions src/context/roiReducer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,14 @@ export interface ReactRoiState<TData = unknown> {
zoomDomain: ZoomDomain;

/**
* Toggled to true once image and container dimensions are known.
* Toggled to true once the target's size is known
*/
isInitialized: boolean;
isTargetInitialized: boolean;

/**
* Toggled to true once the container's size is known
*/
isContainerInitialized: boolean;
}

export type UpdateRoiPayload = Partial<CommittedRoiProperties> & {
Expand Down Expand Up @@ -220,7 +225,7 @@ export type RoiReducerAction =
payload: CreateRoiPayload;
}
| {
type: 'SET_SIZE';
type: 'SET_TARGET_SIZE';
payload: Size;
}
| {
Expand Down Expand Up @@ -280,23 +285,31 @@ export function roiReducer(
case 'SET_MODE':
draft.mode = action.payload;
break;
case 'SET_SIZE': {
case 'SET_TARGET_SIZE': {
// Ignore if size is 0
if (action.payload.width === 0 || action.payload.height === 0) return;
if (action.payload.width === 0 || action.payload.height === 0) {
return;
}

draft.targetSize = action.payload;
sanitizeRois(draft);

if (draft.isContainerInitialized && !draft.isTargetInitialized) {
updateBasePanZoom(draft);
resetZoomAction(draft);
}
draft.isTargetInitialized = true;
break;
}
case 'SET_CONTAINER_SIZE': {
draft.containerSize = action.payload;
if (draft.targetSize && !draft.isInitialized) {
draft.isInitialized = true;
if (draft.isTargetInitialized && !draft.isContainerInitialized) {
updateBasePanZoom(draft);
resetZoomAction(draft);
} else {
} else if (draft.isContainerInitialized && draft.isTargetInitialized) {
updateBasePanZoom(draft);
}
draft.isContainerInitialized = true;
break;
}
case 'REMOVE_ROI': {
Expand Down
8 changes: 4 additions & 4 deletions src/context/updaters/initialPanZoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export const initialSize: Size = {
height: 1,
};

/**
* Updates the base transform which places the target relative to the container.
* This should only be called once both the target and container sizes are known.
*/
export function updateBasePanZoom(draft: Draft<ReactRoiState>) {
if (!draft.isInitialized) {
return;
}

switch (draft.resizeStrategy) {
case 'contain':
updateBasePanZoomContain(draft);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useTargetRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
const targetRef = useRef<T>(null);
const roiDispatch = useRoiDispatch();

// TODO: https://github.com/zakodium-oss/react-roi/issues/164

Check warning on line 10 in src/hooks/useTargetRef.ts

View workflow job for this annotation

GitHub Actions / nodejs / lint-eslint

Unexpected 'todo' comment: 'TODO:...'
// @ts-expect-error This can be ignored for now
useResizeObserver(targetRef, (data) => {
roiDispatch({
type: 'SET_SIZE',
type: 'SET_TARGET_SIZE',
payload: {
width: data.contentRect.width,
height: data.contentRect.height,
Expand Down
143 changes: 143 additions & 0 deletions stories/8-edge-cases/initial-config-zoom.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import type { Meta } from '@storybook/react-vite';
import type { PropsWithChildren } from 'react';

import type { ResizeStrategy } from '../../src/index.ts';
import {
RoiContainer,
RoiList,
RoiProvider,
TargetImage,
getTargetImageStyle,
useTargetRef,
} from '../../src/index.ts';
import { useResetOnChange } from '../utils/useResetOnChange.ts';

export default {
title: 'Edge cases > zoom',
decorators: (Story) => {
return (
<AppLayout>
<Story />
</AppLayout>
);
},
args: {
resizeStrategy: 'contain',
},
argTypes: {
resizeStrategy: {
options: ['contain', 'cover', 'center', 'none'],
control: {
type: 'select',
},
},
},
} as Meta;

function AppLayout(props: PropsWithChildren) {
return (
<div
style={{
height: '75vh',
// background: '#f5f5f5',
border: 'solid 1px black',
}}
>
{props.children}
</div>
);
}

export function NoConfigWithTargetImage(props: {
resizeStrategy: ResizeStrategy;
}) {
const keyId = useResetOnChange([props.resizeStrategy]);
return (
<RoiProvider
key={keyId}
initialConfig={{ resizeStrategy: props.resizeStrategy }}
>
<RoiContainer
target={<TargetImage src="/barbara.jpg" />}
style={{ height: '100%' }}
>
<RoiList />
</RoiContainer>
</RoiProvider>
);
}

export function InitialConfigZoomWithTargetImage(props: {
resizeStrategy: ResizeStrategy;
}) {
const keyId = useResetOnChange([props.resizeStrategy]);
return (
<RoiProvider
key={keyId}
initialConfig={{
zoom: { initial: { origin: 'center', scale: 0.8 } },
resizeStrategy: props.resizeStrategy,
}}
>
<RoiContainer
target={<TargetImage src="/barbara.jpg" />}
style={{ height: '100%' }}
>
<RoiList />
</RoiContainer>
</RoiProvider>
);
}

// when we use image with ref from useTargetRef it's broken
export function NoConfigWithoutTargetImage(props: {
resizeStrategy: ResizeStrategy;
}) {
const keyId = useResetOnChange([props.resizeStrategy]);
return (
<RoiProvider
key={keyId}
initialConfig={{ resizeStrategy: props.resizeStrategy }}
>
<Container />
</RoiProvider>
);
}

// when we use image with ref from useTargetRef it's broken
export function InitialConfigZoomWithoutTargetImage(props: {
resizeStrategy: ResizeStrategy;
}) {
const keyId = useResetOnChange([props.resizeStrategy]);
return (
<RoiProvider
key={keyId}
initialConfig={{
zoom: { initial: { origin: 'center', scale: 0.8 } },
resizeStrategy: props.resizeStrategy,
}}
>
<Container />
</RoiProvider>
);
}

function Container() {
const ref = useTargetRef<HTMLImageElement>();

return (
<RoiContainer
target={
<img
ref={ref}
src="/barbara.jpg"
alt=""
style={getTargetImageStyle()}
/>
}
style={{ height: '100%' }}
>
<RoiList />
</RoiContainer>
);
}
Loading