0.2.5 • Published 5 years ago

@lunit/insight-viewer2 v0.2.5

Weekly downloads
1
License
MIT
Repository
github
Last release
5 years ago

cornerstone 등 OPT 기능을 구현하기 위한 여러 graphics layer를 구현한다.

Install

npm install @lunit/insight-viewer2
{
  "scripts": {
    "app:build": "zeroconfig-webapp-scripts build app --static-file-packages @lunit/insight-viewer2",
    "app:start": "zeroconfig-webapp-scripts start app --static-file-packages @lunit/insight-viewer2" 
  }
}

Package 내부에 public/cornerstoneWADOImageLoaderCodecs.min.js 파일과 public/cornerstoneWADOImageLoaderWebWorker.min.js 파일을 가지고 있고, 이를 App 빌드에 적용하기 위해서는 위와 같이 --static-file-packages 옵션을 추가해줘야 한다.

Sample Codes

Storybook

Stories

// cornerstoneWADOImageLoader 초기화 installWADOImageLoader();

// 을 변경하면 Viewport 등 cornerstone-core 관련 속성들이 초기화 된다 const resetTime: number = Date.now();

// unload 옵션은 위에 선언된 installWADOImageLoader()와 함께 동작한다 // CornerstoneImage 객체를 unload 할때 wado image loader의 unload 동작을 하게 된다 const image: CornerstoneImage = new CornerstoneSingleImage(wadouri:samples/series/CT000010.dcm, {unload: unloadWADOImage});

function Sample() { // addDecorator(withTestController())의 값을 받는다 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-viewer2', 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('', () => );

<h3>\_\_stories\_\_/MachineHeatmapViewer.stories.tsx</h3>
```tsx
import { MachineHeatmapViewer } from '@lunit/insight-viewer2';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { InsightViewerContainer } from '../components/InsightViewerContainer';
import { ProgressViewer } from '../components/ProgressViewer';
import { UserContourDrawer } from '../components/UserContourDrawer';
import { UserContourViewer } from '../components/UserContourViewer';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { useUserContour } from '../hooks/useUserContour';
import { CornerstoneSingleImage } from '../image/CornerstoneSingleImage';
import { CornerstoneImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
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-viewer2', 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/>);

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 ( ); }

function Container() { // 에서 수집한 정보를 얻을 수 있다 const progressActivity: boolean = useProgressViewersActivity(); // 혹은 가 동작중일때 비활성을 처리할 Style을 만들 수 있다 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 ( <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-viewer2', 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('', () => );

<h3>\_\_stories\_\_/SeriesImage.stories.tsx</h3>
```tsx
import { UserContourDrawer, UserContourViewer, useUserContour } from '@lunit/insight-viewer2';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { InsightViewerContainer } from '../components/InsightViewerContainer';
import { ProgressViewer } from '../components/ProgressViewer';
import { useBulkImageScroll } from '../hooks/useBulkImageScroll';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { CornerstoneSeriesImage } from '../image/CornerstoneSeriesImage';
import { CornerstoneBulkImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
import { withSeriesImageController } from './decorators/withSeriesImageController';
import { useController, withTestController } from './decorators/withTestController';
import series from './series.json';

installWADOImageLoader();

const resetTime: number = Date.now();

// CornerstoneSeriesImage는 여러장의 dcm 이미지를 받는다
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();
  
  // CornerstoneBulkImage를 Wheel과 연결 시킨다
  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-viewer2', 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/>);

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();

// pan, adjust, zoom은 // pan={true}로 설정하면 내부 Element를 사용해서 MouseEvent를 처리하게 되고, // pan={HTMLElement}로 설정하면 해당 Element를 사용해서 MouseEvent를 처리하게 된다. // MouseEvent를 처리하는 Layer가 여러개 중첩될 때, 하위 Layer의 MouseEvent가 막히는 현상을 해결해준다. const interactionElement, setInteractionElement = useState<HTMLElement | null>(null);

// 내부의 Canvas Render를 외부의 다른 Component들에 동기화 시키기 위한 Hook const { cornerstoneRenderData, updateCornerstoneRenderData, } = useInsightViewerSync();

// Annotation (사용자 Contour)를 다루기 위한 Hook const { contours, focusedContour, addContour, removeContour, focusContour, } = useUserContour();

return ( <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 && } { // 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}/> } ); }

storiesOf('insight-viewer2', 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('', () => );

<h3>\_\_stories\_\_/UserContourViewerCanvasStyle.stories.tsx</h3>
```tsx
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { InsightViewerContainer } from '../components/InsightViewerContainer';
import { ProgressViewer } from '../components/ProgressViewer';
import { UserContourDrawer } from '../components/UserContourDrawer';
import { UserContourViewer } from '../components/UserContourViewer';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { useUserContour } from '../hooks/useUserContour';
import { CornerstoneSingleImage } from '../image/CornerstoneSingleImage';
import { CornerstoneImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
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을 변경할 수 있다
        <UserContourViewer 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을 변경할 수 있다
        <UserContourDrawer width={width}
                           height={height}
                           contours={contours}
                           draw={control === 'pen' && interactionElement}
                           onFocus={focusContour}
                           onAdd={contour => addContour(contour, 0)}
                           onRemove={removeContour}
                           cornerstoneRenderData={cornerstoneRenderData}
        
                           canvasStokeLineWidght={8}
                           canvasStokeStyle="purple"
                           canvasFillStyle="rgba(255, 255, 255, 0.4)"/>
      }
      <ProgressViewer image={image}
                      width={width}
                      height={height}/>
    </InsightViewerContainer>
  );
}

storiesOf('insight-viewer2', 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 canvasStokeStyle="{}">', () => <Sample/>);

installWADOImageLoader();

const resetTime: number = Date.now(); const image: CornerstoneImage = new CornerstoneSingleImage(wadouri:samples/series/CT000010.dcm, {unload: unloadWADOImage});

function Sample() { const { updateCornerstoneRenderData, } = useInsightViewerSync();

// 특정 Element의 width, height를 지속적으로 감지한다 // flex 등 layout으로 처리된 Element의 width, height를 useResizeObserver()로 받아서 // 로 넘길 수 있다 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-viewer2', module) .addDecorator(withOptTheme) .add('useResizeObserver', () => );

<h3>\_\_stories\_\_/useViewportMirroring.stories.tsx</h3>
```tsx
import {
  CornerstoneBulkImage,
  useInsightViewerSync,
  UserContourDrawer,
  UserContourViewer,
  useUserContour,
} from '@lunit/insight-viewer2';
import { useBulkImageScroll } from '@lunit/insight-viewer2/hooks/useBulkImageScroll';
import { useViewportMirroring } from '@lunit/insight-viewer2/hooks/useViewportMirroring';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { RefObject, useRef, useState } from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { InsightViewerContainer } from '../components/InsightViewerContainer';
import { CornerstoneSeriesImage } from '../image/CornerstoneSeriesImage';
import { CornerstoneSingleImage } from '../image/CornerstoneSingleImage';
import { CornerstoneImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
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();
  
  // Viewport Mirroring의 대상에게 부여할 RefObject 들을 만든다
  const viewer2: RefObject<InsightViewer> = useRef(null);
  const viewer3: RefObject<InsightViewer> = useRef(null);
  const viewer4: RefObject<InsightViewer> = useRef(null);
  
  // updateMasterRenderData()가 실행되면 viewer2, viewer3, viewer4에 Viewport가 Mirroring 된다
  const {updateMasterRenderData} = useViewportMirroring(viewer2, viewer3, viewer4);
  
  // 3번째 화면을 위한 처리
  const [interactionElement3, setInteractionElement3] = useState<HTMLElement | null>(null);
  
  const {
    cornerstoneRenderData,
    updateCornerstoneRenderData,
  } = useInsightViewerSync();
  
  const {
    contours,
    focusedContour,
    addContour,
    removeContour,
    focusContour,
  } = useUserContour();
  
  // 4번째 화면을 위한 처리
  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-viewer2', 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