cornerstone 등 OPT 기능을 구현하기 위한 여러 graphics layer를 구현한다.
Install
npm install @lunit/insight-viewer
{
"scripts": {
"app:build": "zeroconfig-webapp-scripts build app --static-file-packages @lunit/insight-viewer",
"app:start": "zeroconfig-webapp-scripts start app --static-file-packages @lunit/insight-viewer"
}
}
Package 내부에 public/cornerstoneWADOImageLoaderCodecs.min.js
파일과 public/cornerstoneWADOImageLoaderWebWorker.min.js
파일을 가지고 있고, 이를 App 빌드에 적용하기 위해서는 위와 같이 --static-file-packages
옵션을 추가해줘야 한다.
API
Basic Types
import { CornerstoneEventData } from 'cornerstone-core';
export type CornerstoneRenderData = Required<Pick<CornerstoneEventData, 'canvasContext' | 'element' | 'enabledElement' | 'image' | 'renderTimeInMs' | 'viewport'>>;
export type Point = [number, number];
export interface ContourInfo {
confidenceLevel: number;
}
export interface Contour extends ContourInfo {
id: number;
polygon: Point[];
}
import { Image } from 'cornerstone-core';
import { Observable } from 'rxjs';
export interface CornerstoneImage {
readonly image: Observable<Image | null>;
readonly progress: Observable<number>;
destroy: () => void;
}
export interface CornerstoneBulkImage extends CornerstoneImage {
length: () => number;
readonly index: Observable<number>;
next: (delta?: number) => void;
prev: (delta?: number) => void;
goto: (index: number) => void;
getIndex: () => number;
}
export interface ProgressEventDetail {
url: string;
imageId: string;
loaded: number;
total: number;
percentComplete: number;
}
export function getProgressEventDetail(event: Event): ProgressEventDetail | undefined {
const detail: object | undefined = event['detail'];
if (
detail
&& typeof detail['url'] === 'string'
&& typeof detail['imageId'] === 'string'
&& typeof detail['loaded'] === 'number'
&& typeof detail['total'] === 'number'
&& typeof detail['percentComplete'] === 'number'
) {
return detail as ProgressEventDetail;
}
return undefined;
}
Images
new CornerstoneSingleImage(imageId: string, options: { unload: () => void })
new CornerstoneSeriesImage(imageIds: string[], options: { unload: () => void })
Components
<InsightViewer width={number}
height={number}
image={CornerstoneImage}
resetTime={number}
pan={boolean | HTMLElement | null}
adjust={boolean | HTMLElement | null}
zoom={boolean | HTMLElement | null}
invert={boolean}
flip={boolean}
updateCornerstoneRenderData={(renderData: CornerstoneRenderData) => void} />
interface InsightViewerContainerProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
<InsightViewerContainer {...divProps}
width={number}
height={number} />
<MachineHeatmapViewer width={number}
height={number}
posMap={number[][]}
threshold={number}
cornerstoneRenderData={CornerstoneRenderData | null}/>
<UserContourViewer width={number}
height={number}
contours={Contour[]}
focusedContour={Contour | null}
cornerstoneRenderData={CornerstoneRenderData | null}
canvasStrokeLineWidth?={number}
canvasStrokeStyle?={string}
canvasFillStyle?={string}
canvasFontStyle?={string}
canvasFocusedStrokeLineWidth?={number}
canvasFocusedStrokeStyle?={string}
canvasFocusedFillStyle?={string}
canvasFocusedFontStyle?={string} />
<UserContourDrawer width={number}
height={number}
contours={Contour[]}
draw={boolean | HTMLElement | null}
onFocus={(contour: Contour | null) => void}
onAdd={(polygon: Point[], event: MouseEvent) => void}
onRemove={(contour: Contour) => void}
cornerstoneRenderData={CornerstoneRenderData | null}
canvasStokeLineWidght?={number}
canvasStokeStyle?={string}
canvasFillStyle?={string}/>
<ProgressViewer width={number}
height={number}
inProgress?={boolean}
image?={CornerstoneImage | null | undefined} />
<ProgressCollector>
{children}
</ProgressCollector>
function useProgrssViewersActivity(): boolean
function useContainerStyleOfProgressViewersInactivity(inactivityStyle: CSSProperties): CSSProperties
Hooks
useBulkImageScroll({
image: CornerstoneBulkImage,
element: HTMLElement | null,
enabled?: boolean,
})
useImageProgress(image: CornerstoneImage | null | undefined): number | null
useInsightViewerSync(): {
cornerstoneRenderData: CornerstoneRenderData | null,
updateCornerstoneRenderData: (renderData: CornerstoneRenderData) => void,
}
useUserContour(): {
contours: Contour[],
focusedContour: Contour | null,
addContour: (polygon: Point[], confidenceLevel: number) => Contour | null,
focusContour: (contour: Contour | null) => void,
updateContour: (contour: Contour, patch: Partial<Contour>) => void,
removeContour: (contour: Contour) => void,
removeAllContours: () => void,
}
useViewportMirroring(...destinations: (InsightViewer | RefObject<InsightViewer>)[]): {
updateMasterRenderData: (renderData: CornerstoneRenderData) => void,
}
WADO Image Loader
installWADOImageLoader()
unloadWADOImage(imageId: string | string[] | null)
Sample Codes
Storybook
Stories
__stories__/InsightViewer.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
installWADOImageLoader,
unloadWADOImage,
useInsightViewerSync,
} from '@lunit/insight-viewer';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
function Sample() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const {
updateCornerstoneRenderData,
} = useInsightViewerSync();
return (
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan'}
adjust={control === 'adjust'}
zoom={wheel === 'zoom'}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<InsightViewer>', () => <Sample/>);
__stories__/MachineHeatmapViewer.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
MachineHeatmapViewer,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
import data from './posMap.sample.json';
const {engine_result: {engine_result: {pos_map: posMap}}} = data;
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
function Sample() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const [interactionElement, setInteractionElement] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
return (
<InsightViewerContainer ref={setInteractionElement} width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan' && interactionElement}
adjust={control === 'adjust' && interactionElement}
zoom={wheel === 'zoom' && interactionElement}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{/*
engineResult.posMap을 그리는 Layer
현재 Heatmap Spec (number[][])에 맞춰서 개발되었기 때문에
Spec이 다르다면 새로운 Viewer를 만들어야 한다
*/}
<MachineHeatmapViewer width={width}
height={height}
posMap={posMap}
threshold={0.1}
cornerstoneRenderData={cornerstoneRenderData}/>
{
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
<UserContourViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
<UserContourDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<MachineHeatmapViewer>', () => <Sample/>);
__stories__/ProgressCollector.stories.tsx
import {
CornerstoneImage,
CornerstoneSeriesImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressCollector,
ProgressViewer,
unloadWADOImage,
useContainerStyleOfProgressViewersInactivity,
useInsightViewerSync,
useProgressViewersActivity,
} from '@lunit/insight-viewer';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { CSSProperties } from 'react';
import { useController, withTestController } from './decorators/withTestController';
import series from './series.json';
installWADOImageLoader();
const resetTime: number = Date.now();
const image1: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
const image2: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000020.dcm`, {unload: unloadWADOImage});
const image3: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000030.dcm`, {unload: unloadWADOImage});
const image4: CornerstoneImage = new CornerstoneSeriesImage(series.map(p => `wadouri:samples/series/${p}`), {unload: unloadWADOImage});
function Sample() {
return (
<ProgressCollector>
<Container/>
</ProgressCollector>
);
}
function Container() {
const progressActivity: boolean = useProgressViewersActivity();
const containerDisabledStyle: CSSProperties = useContainerStyleOfProgressViewersInactivity({pointerEvents: 'none'});
return (
<div style={containerDisabledStyle}>
<div style={{display: 'flex'}}>
<Component image={image1}/>
<Component image={image2}/>
<Component image={image3}/>
<Component image={image4}/>
</div>
<p>
{
progressActivity
? '<ProgressViewer>가 하나 이상 작동 중입니다!!!'
: '동작중인 <ProgressViewer>가 없습니다!!!'
}
</p>
</div>
);
}
function Component({image}: {image: CornerstoneImage}) {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const {updateCornerstoneRenderData} = useInsightViewerSync();
return (
<InsightViewerContainer width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan'}
adjust={control === 'adjust'}
zoom={wheel === 'zoom'}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{/* 이 <ProgressViewer>가 <ProgressCollector>에 수집된다 */}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [300, 200, 500],
height: [400, 300, 600],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<ProgressCollector>', () => <Sample/>);
__stories__/SeriesImage.stories.tsx
import {
CornerstoneBulkImage,
CornerstoneSeriesImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useBulkImageScroll,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { withSeriesImageController } from './decorators/withSeriesImageController';
import { useController, withTestController } from './decorators/withTestController';
import series from './series.json';
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneBulkImage = new CornerstoneSeriesImage(series.map(p => `wadouri:samples/series/${p}`), {unload: unloadWADOImage});
function Component() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const [interactionElement, setInteractionElement] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
useBulkImageScroll({
image,
element: interactionElement,
enabled: wheel === 'scroll',
});
return (
<InsightViewerContainer ref={setInteractionElement} width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan' && interactionElement}
adjust={control === 'adjust' && interactionElement}
zoom={wheel === 'zoom' && interactionElement}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
<UserContourViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
<UserContourDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['scroll', ['none', 'zoom', 'scroll']],
flip: false,
invert: false,
}))
.addDecorator(withSeriesImageController(image))
.add('Series Image', () => <Component/>);
__stories__/UserContourViewer.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
function Sample() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const [interactionElement, setInteractionElement] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
return (
<InsightViewerContainer ref={setInteractionElement} width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan' && interactionElement}
adjust={control === 'adjust' && interactionElement}
zoom={wheel === 'zoom' && interactionElement}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{
// 사용자가 그린 Annotation을 보여준다
// contours가 있는 경우에만 출력
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
<UserContourViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
// Annotation을 그리고, 지우게 해준다
// control === 'pen' 인 경우에만 출력
contours &&
cornerstoneRenderData &&
control === 'pen' &&
<UserContourDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserContourViewer>', () => <Sample/>);
__stories__/UserContourViewerCanvasStyle.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourCanvasDrawer,
UserContourCanvasViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
function Sample() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const [interactionElement, setInteractionElement] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
return (
<InsightViewerContainer ref={setInteractionElement} width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan' && interactionElement}
adjust={control === 'adjust' && interactionElement}
zoom={wheel === 'zoom' && interactionElement}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
// Canvas Style을 변경할 수 있다
<UserContourCanvasViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}
canvasStrokeLineWidth={10}
canvasStrokeStyle="blue"
canvasFillStyle="rgba(0, 0, 255, 0.3)"
canvasFontStyle="normal normal 600 40px proximanova"
canvasFocusedStrokeLineWidth={20}
canvasFocusedStrokeStyle="red"
canvasFocusedFillStyle="rgba(255, 0, 0, 0.3)"
canvasFocusedFontStyle="normal normal 600 50px proximanova"/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
// Canvas Style을 변경할 수 있다
<UserContourCanvasDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}
canvasStokeLineWidth={8}
canvasStokeStyle="purple"
canvasFillStyle="rgba(255, 255, 255, 0.4)"/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserContourCanvasViewer canvasStokeStyle="{}">', () => <Sample/>);
__stories__/UserContourViewerStyle.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import styled from 'styled-components';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
const Viewer = styled(UserContourViewer)`
> polygon {
stroke-width: 10px;
stroke: blue;
fill: rgba(0, 0, 255, 0.3);
&[data-focused] {
stroke-width: 20px;
stroke: red;
fill: rgba(255, 0, 0, 0.3);
}
}
> text {
fill: blue;
&[data-focused] {
fill: red;
}
}
`;
const Drawer = styled(UserContourDrawer)`
> polyline {
stroke: purple;
stroke-width: 8px;
fill: rgba(255, 255, 255, 0.4);
}
`;
function Sample() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const [interactionElement, setInteractionElement] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
return (
<InsightViewerContainer ref={setInteractionElement} width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan' && interactionElement}
adjust={control === 'adjust' && interactionElement}
zoom={wheel === 'zoom' && interactionElement}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
// Canvas Style을 변경할 수 있다
<Viewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
// Canvas Style을 변경할 수 있다
<Drawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserContourViewer className="">', () => <Sample/>);
__stories__/useResizeObserver.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
unloadWADOImage,
useInsightViewerSync,
} from '@lunit/insight-viewer';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React from 'react';
import useResizeObserver from 'use-resize-observer';
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
function Sample() {
const {
updateCornerstoneRenderData,
} = useInsightViewerSync();
const [resizeRef, width, height] = useResizeObserver();
console.log('useResizeObserver.stories.tsx..Sample()', width, height);
return (
<div ref={resizeRef} style={{width: '50vw', height: '80vh'}}>
<InsightViewerContainer width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={false}
flip={false}
pan
adjust={false}
zoom={false}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
</InsightViewerContainer>
</div>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withOptTheme)
.add('useResizeObserver', () => <Sample/>);
__stories__/useViewportMirroring.stories.tsx
import {
CornerstoneBulkImage,
CornerstoneImage,
CornerstoneSeriesImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
unloadWADOImage,
useBulkImageScroll,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
useViewportMirroring,
} from '@lunit/insight-viewer';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { RefObject, useRef, useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
import series from './series.json';
installWADOImageLoader();
const resetTime: number = Date.now();
const image1: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
const image2: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000020.dcm`, {unload: unloadWADOImage});
const image3: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000030.dcm`, {unload: unloadWADOImage});
const image4: CornerstoneBulkImage = new CornerstoneSeriesImage(series.map(p => `wadouri:samples/series/${p}`), {unload: unloadWADOImage});
function Sample() {
const {
width,
height,
invert,
flip,
} = useController();
const viewer2: RefObject<InsightViewer> = useRef(null);
const viewer3: RefObject<InsightViewer> = useRef(null);
const viewer4: RefObject<InsightViewer> = useRef(null);
const {updateMasterRenderData} = useViewportMirroring(viewer2, viewer3, viewer4);
const [interactionElement3, setInteractionElement3] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
const [interactionElement4, setInteractionElement4] = useState<HTMLElement | null>(null);
useBulkImageScroll({
image: image4,
element: interactionElement4,
enabled: true,
});
return (
<div style={{display: 'flex'}}>
<InsightViewerContainer width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan
adjust={false}
zoom
resetTime={resetTime}
image={image1}
updateCornerstoneRenderData={updateMasterRenderData}/>
</InsightViewerContainer>
<InsightViewerContainer width={width} height={height}>
<InsightViewer ref={viewer2}
width={width}
height={height}
invert={invert}
flip={flip}
pan={false}
adjust
zoom={false}
resetTime={resetTime}
image={image2}
updateCornerstoneRenderData={renderData => console.log('#2', renderData)}/>
</InsightViewerContainer>
<InsightViewerContainer ref={setInteractionElement3} width={width} height={height}>
<InsightViewer ref={viewer3}
width={width}
height={height}
invert={invert}
flip={flip}
pan={false}
adjust={false}
zoom={false}
resetTime={resetTime}
image={image3}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
<UserContourViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
<UserContourDrawer width={width}
height={height}
contours={contours}
draw={interactionElement3}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
</InsightViewerContainer>
<InsightViewerContainer ref={setInteractionElement4} width={width} height={height}>
<InsightViewer ref={viewer4}
width={width}
height={height}
invert={invert}
flip={flip}
pan={false}
adjust={false}
zoom={false}
resetTime={resetTime}
image={image4}
updateCornerstoneRenderData={renderData => console.log('#4', renderData)}/>
</InsightViewerContainer>
</div>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [300, 200, 500],
height: [400, 300, 600],
control: ['none', ['none']],
wheel: ['none', ['none']],
flip: false,
invert: false,
}))
.add('useViewportMirroring()', () => <Sample/>);
Tests
Changelog
2.1.1
Added
<UserContourViewer className="">
, <UserContourDrawer className="">
Custom Style
2.1.0
Added
- 기존
<UserContourViewer>
와 <UserContourDrawer>
는 <UserContourCanvasViewer>
와 <UserContourCanvasDrawer>
로 변경 - 신규
<UserContourViewer>
와 <UserContourDrawer>
는 SVG 기반으로 변경 (Viewport와 무관하게 일정한 Style을 유지함)
2.0.3
Fixed
useViewportMirroring()
는 위치에 관련된 일부 속성만을 미러링 (hflip
, vflip
, translation
, scale
)