feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)
Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
200 files changed
+9589
-2178
No files matched your search
@@ -0,0 +1,189 @@
|
||||
import React, { useEffect, useCallback, useState, useMemo, ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import WindowLevelHistogram from './WindowLevelHistogram';
|
||||
import InputRange from '../InputRange';
|
||||
import InputDoubleRange from '../InputDoubleRange';
|
||||
import {
|
||||
VOI,
|
||||
Colormap,
|
||||
Histogram,
|
||||
voiPropType,
|
||||
histogramPropType,
|
||||
colormapPropType,
|
||||
} from './types';
|
||||
import PanelSection from '../PanelSection';
|
||||
|
||||
const convertVOItoVOIRange = voi => {
|
||||
return {
|
||||
min: voi.windowCenter - voi.windowWidth / 2,
|
||||
max: voi.windowCenter + voi.windowWidth / 2,
|
||||
};
|
||||
};
|
||||
|
||||
const WindowLevel = ({
|
||||
title,
|
||||
step,
|
||||
histogram,
|
||||
voi: voiProp,
|
||||
opacity: opacityProp,
|
||||
showOpacitySlider,
|
||||
colormap,
|
||||
style,
|
||||
fillColor,
|
||||
lineColor,
|
||||
containerClassName,
|
||||
onVOIChange,
|
||||
onOpacityChange,
|
||||
}: {
|
||||
title: string;
|
||||
step: number;
|
||||
histogram: Histogram;
|
||||
voi: VOI;
|
||||
opacity: number;
|
||||
showOpacitySlider: boolean;
|
||||
colormap: Colormap;
|
||||
style: string;
|
||||
fillColor: string;
|
||||
lineColor: string;
|
||||
containerClassName: string;
|
||||
onVOIChange: (voi: VOI) => void;
|
||||
onOpacityChange: (opacity: number) => void;
|
||||
}): ReactElement => {
|
||||
const [opacity, setOpacity] = useState(opacityProp);
|
||||
|
||||
const range = useMemo(
|
||||
() => ({
|
||||
min: histogram.range.min,
|
||||
max: histogram.range.max,
|
||||
}),
|
||||
[histogram]
|
||||
);
|
||||
|
||||
const [voiRange, setVOIRange] = useState(
|
||||
voiProp
|
||||
? convertVOItoVOIRange(voiProp)
|
||||
: {
|
||||
min: range.min,
|
||||
max: range.max,
|
||||
}
|
||||
);
|
||||
|
||||
const handleVOIRangeChange = useCallback(
|
||||
newRange => {
|
||||
if (newRange[0] === voiRange.min && newRange[1] === voiRange.max) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowWidth = newRange[1] - newRange[0];
|
||||
const windowCenter = newRange[0] + windowWidth / 2;
|
||||
|
||||
setVOIRange({
|
||||
min: newRange[0],
|
||||
max: newRange[1],
|
||||
});
|
||||
|
||||
if (onVOIChange) {
|
||||
onVOIChange({ windowWidth, windowCenter });
|
||||
}
|
||||
},
|
||||
[onVOIChange, voiRange]
|
||||
);
|
||||
|
||||
const handleOpacityChange = useCallback(
|
||||
value => {
|
||||
if (onOpacityChange) {
|
||||
onOpacityChange(value);
|
||||
}
|
||||
},
|
||||
[onOpacityChange]
|
||||
);
|
||||
|
||||
useEffect(() => setVOIRange(convertVOItoVOIRange(voiProp)), [voiProp]);
|
||||
|
||||
useEffect(() => setOpacity(opacityProp), [opacityProp]);
|
||||
return (
|
||||
<div className={classnames('maxValue-w-sm p-0.5 text-[0px] text-white', containerClassName)}>
|
||||
<div className="px-2 pt-0 pb-[0.5]">
|
||||
{/* <legend className="pb-1 text-base text-white"> {title}</legend> */}
|
||||
<div className="flex h-4 text-xs">
|
||||
<div className="relative h-fit grow">
|
||||
<span className="absolute left-0 bottom-px leading-3">{range.min}</span>
|
||||
</div>
|
||||
<div className="relative h-fit grow text-right">
|
||||
<span className="absolute right-0 bottom-px leading-3">{range.max}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
<div className="pb-2.5">
|
||||
<div className="h-11 bg-white">
|
||||
<WindowLevelHistogram
|
||||
range={range}
|
||||
voiRange={voiRange}
|
||||
histogram={histogram}
|
||||
colormap={colormap}
|
||||
style={style}
|
||||
fillColor={fillColor}
|
||||
lineColor={lineColor}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InputDoubleRange
|
||||
values={[voiRange.min, voiRange.max]}
|
||||
onChange={handleVOIRangeChange}
|
||||
minValue={range.min}
|
||||
maxValue={range.max}
|
||||
allowNumberEdit={true}
|
||||
showLabel={true}
|
||||
step={step}
|
||||
allowOutOfRange={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{showOpacitySlider && (
|
||||
<div className="flex items-center justify-between text-base">
|
||||
<div className="text-xs text-white">Opacity</div>
|
||||
<InputRange
|
||||
inputClassName="grow"
|
||||
maxValue={1}
|
||||
minValue={0}
|
||||
step={0.1}
|
||||
value={opacity}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
onChange={handleOpacityChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WindowLevel.defaultProps = {
|
||||
step: 1,
|
||||
opacity: 1,
|
||||
showOpacitySlider: false,
|
||||
style: 'polygon',
|
||||
fillColor: '#3f3f3f',
|
||||
lineColor: '#707070',
|
||||
};
|
||||
|
||||
WindowLevel.propTypes = {
|
||||
title: PropTypes.string,
|
||||
step: PropTypes.number,
|
||||
histogram: histogramPropType.isRequired,
|
||||
voi: voiPropType,
|
||||
opacity: PropTypes.number,
|
||||
showOpacitySlider: PropTypes.bool,
|
||||
colormap: colormapPropType,
|
||||
style: PropTypes.oneOf(['polygon', 'bars']),
|
||||
fillColor: PropTypes.string,
|
||||
lineColor: PropTypes.string,
|
||||
containerClassName: PropTypes.string,
|
||||
onVOIChange: PropTypes.func,
|
||||
onOpacityChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default WindowLevel;
|
||||
@@ -0,0 +1,236 @@
|
||||
import React, { useLayoutEffect, useEffect, useRef, ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Range,
|
||||
VOIRange,
|
||||
Histogram,
|
||||
Colormap,
|
||||
voiRangePropType,
|
||||
rangePropType,
|
||||
histogramPropType,
|
||||
colormapPropType,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_COLORMAP = {
|
||||
Name: 'Grayscale',
|
||||
RGBPoints: [0, 0, 0, 0, 1, 1, 1, 1],
|
||||
};
|
||||
|
||||
const clamp = (value, min, max) => Math.min(Math.max(min, value), max);
|
||||
|
||||
const interpolateVec3 = (a, b, t) => {
|
||||
return [a[0] * (1 - t) + b[0] * t, a[1] * (1 - t) + b[1] * t, a[2] * (1 - t) + b[2] * t];
|
||||
};
|
||||
|
||||
const drawBackground = (canvas, range, voiRange, colormap) => {
|
||||
const context = canvas.getContext('2d');
|
||||
const { width, height } = canvas;
|
||||
const windowWidth = voiRange.max - voiRange.min;
|
||||
const rgbPoints = colormap.RGBPoints;
|
||||
const colorsCount = rgbPoints.length / 4;
|
||||
|
||||
const getColorPoint = index => {
|
||||
const offset = 4 * index;
|
||||
|
||||
return index < colorsCount
|
||||
? {
|
||||
index,
|
||||
position: rgbPoints[offset],
|
||||
color: [rgbPoints[offset + 1], rgbPoints[offset + 2], rgbPoints[offset + 3]],
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
||||
let leftColorPoint = undefined;
|
||||
let rightColorPoint = getColorPoint(0);
|
||||
|
||||
const incRawPixelValue = (range.max - range.min) / (width - 1);
|
||||
let rawPixelValue = range.min;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const tVoiRange = (rawPixelValue - voiRange.min) / windowWidth;
|
||||
|
||||
// Find the color in a linear way (O(n) complexity)
|
||||
if (rightColorPoint) {
|
||||
for (let i = rightColorPoint.index; i < colorsCount; i++) {
|
||||
if (tVoiRange <= rightColorPoint.position) {
|
||||
break;
|
||||
}
|
||||
|
||||
leftColorPoint = rightColorPoint;
|
||||
rightColorPoint = getColorPoint(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
let normColor;
|
||||
|
||||
if (!leftColorPoint) {
|
||||
normColor = [...rightColorPoint.color];
|
||||
} else if (!rightColorPoint) {
|
||||
normColor = [...leftColorPoint.color];
|
||||
} else {
|
||||
const tColorRange =
|
||||
(tVoiRange - leftColorPoint.position) /
|
||||
(rightColorPoint.position - leftColorPoint.position);
|
||||
|
||||
normColor = interpolateVec3(leftColorPoint.color, rightColorPoint.color, tColorRange);
|
||||
}
|
||||
|
||||
const color = normColor.map(color => clamp(Math.round(color * 255), 0, 255));
|
||||
|
||||
context.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
|
||||
context.fillRect(x, 0, 1, height);
|
||||
|
||||
rawPixelValue += incRawPixelValue;
|
||||
}
|
||||
};
|
||||
|
||||
/* plots the histogram bins as a polygon that traces the centers of each bin */
|
||||
const drawPolygonHistogram = (canvas, context, histogram, options) => {
|
||||
const xScale = canvas.width / histogram.numBins;
|
||||
const { scale } = options;
|
||||
const maxVal = scale(histogram.maxBinValue);
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(0, canvas.height);
|
||||
context.lineTo(0, canvas.height - (canvas.height * scale(histogram.bins[0])) / maxVal);
|
||||
|
||||
let x = xScale / 2;
|
||||
for (let bin = 0; bin < histogram.numBins; bin++) {
|
||||
context.lineTo(x, canvas.height - (canvas.height * scale(histogram.bins[bin])) / maxVal);
|
||||
x += xScale;
|
||||
}
|
||||
context.lineTo(
|
||||
canvas.width,
|
||||
canvas.height - (canvas.height * scale(histogram.bins[histogram.numBins - 1])) / maxVal
|
||||
);
|
||||
context.lineTo(canvas.width, canvas.height);
|
||||
context.lineTo(0, canvas.height);
|
||||
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
/* plots the histogram bins as a series of n vertical bars (n = number of bins) */
|
||||
const drawBarHistogram = (canvas, context, histogram, options) => {
|
||||
const xScale = canvas.width / histogram.numBins;
|
||||
const { scale } = options;
|
||||
const maxVal = scale(histogram.maxBinValue);
|
||||
|
||||
context.beginPath();
|
||||
|
||||
for (let bin = 0; bin < histogram.numBins; bin++) {
|
||||
context.moveTo(xScale * bin, canvas.height);
|
||||
context.lineTo(
|
||||
xScale * bin,
|
||||
canvas.height - (canvas.height * scale(histogram.bins[bin])) / maxVal
|
||||
);
|
||||
}
|
||||
|
||||
context.closePath();
|
||||
context.strokeStyle = options.fillColor;
|
||||
context.lineWidth = xScale;
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
const drawHistogram = (canvas, histogram, options) => {
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.fillStyle = options.fillColor;
|
||||
context.strokeStyle = options.lineColor;
|
||||
|
||||
if (options.style === 'polygon') {
|
||||
drawPolygonHistogram(canvas, context, histogram, options);
|
||||
} else if (options.style === 'bars') {
|
||||
drawBarHistogram(canvas, context, histogram, options);
|
||||
} else {
|
||||
throw new Error(`Invalid style (${options.style})`);
|
||||
}
|
||||
};
|
||||
|
||||
const WindowLevelHistogram = ({
|
||||
range,
|
||||
voiRange,
|
||||
histogram,
|
||||
colormap,
|
||||
style,
|
||||
fillColor,
|
||||
lineColor,
|
||||
}: {
|
||||
range: Range;
|
||||
voiRange: VOIRange;
|
||||
histogram: Histogram;
|
||||
colormap: Colormap;
|
||||
style: string;
|
||||
fillColor: string;
|
||||
lineColor: string;
|
||||
}): ReactElement => {
|
||||
const containerRef = useRef(null);
|
||||
const canvasRef = useRef(null);
|
||||
const minVOIPercent = ((voiRange.min - range.min) / (range.max - range.min)) * 100;
|
||||
const maxVoiPercent = (1 - (range.max - voiRange.max) / (range.max - range.min)) * 100;
|
||||
const background =
|
||||
colormap.Name !== 'Grayscale'
|
||||
? undefined
|
||||
: `linear-gradient(to right, #000 0%, #000 ${minVOIPercent}%, #fff ${maxVoiPercent}%, #fff 100%)`;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
canvas.width = container.offsetWidth;
|
||||
canvas.height = container.offsetHeight;
|
||||
}, [containerRef, canvasRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext('2d');
|
||||
const fnIdentity = x => x;
|
||||
const options = {
|
||||
style,
|
||||
fillColor,
|
||||
lineColor,
|
||||
scale: fnIdentity,
|
||||
};
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
drawBackground(canvas, range, voiRange, colormap);
|
||||
drawHistogram(canvas, histogram, options);
|
||||
}, [range, voiRange, histogram, colormap, style, fillColor, lineColor, canvasRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-full"
|
||||
style={{ background }}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width="1"
|
||||
height="1"
|
||||
></canvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WindowLevelHistogram.defaultProps = {
|
||||
colormap: DEFAULT_COLORMAP,
|
||||
style: 'polygon',
|
||||
fillColor: '#3f3f3f',
|
||||
lineColor: '#707070',
|
||||
};
|
||||
|
||||
WindowLevelHistogram.propTypes = {
|
||||
range: rangePropType.isRequired,
|
||||
voiRange: voiRangePropType.isRequired,
|
||||
histogram: histogramPropType.isRequired,
|
||||
colormap: colormapPropType,
|
||||
style: PropTypes.oneOf(['polygon', 'bars']),
|
||||
fillColor: PropTypes.string,
|
||||
lineColor: PropTypes.string,
|
||||
};
|
||||
|
||||
export default WindowLevelHistogram;
|
||||
@@ -0,0 +1,103 @@
|
||||
import WindowLevel from '../WindowLevel';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
import '../../InputRange/InputRange.css';
|
||||
|
||||
export const argTypes = {
|
||||
component: WindowLevel,
|
||||
title: 'Components/WindowLevel',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/WindowLevel"
|
||||
component={WindowLevel}
|
||||
/>
|
||||
|
||||
export const WindowLevelTemplate = args => (
|
||||
<div className="w-96">
|
||||
<WindowLevel {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="WindowLevel"
|
||||
componentRelativePath="WindowLevel/WindowLevel.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
WindowLevel is a component that...
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
title: 'Target',
|
||||
voi: {
|
||||
windowWidth: 800,
|
||||
windowCenter: 0,
|
||||
},
|
||||
showOpacitySlider: true,
|
||||
histogram: {
|
||||
numBins: 256,
|
||||
range: {
|
||||
min: -1000,
|
||||
max: 1000,
|
||||
},
|
||||
bins: [
|
||||
469817, 485077, 499920, 513954, 526106, 472824, 551087, 560798, 570524, 576182, 509119,
|
||||
585371, 585123, 586201, 581405, 504649, 569829, 560213, 550658, 535250, 455914, 504388,
|
||||
487305, 467924, 446629, 427088, 355151, 384904, 362374, 342472, 321159, 262968, 282026,
|
||||
261558, 243751, 225659, 184247, 195394, 180728, 166477, 153932, 124837, 132557, 123424,
|
||||
114390, 106514, 98492, 80936, 87261, 81444, 76932, 72578, 60694, 66376, 62465, 59951,
|
||||
57424, 48593, 53942, 52361, 50857, 49578, 42447, 47633, 46587, 45540, 45061, 38899, 44420,
|
||||
43817, 43381, 43384, 42993, 37937, 43329, 43692, 43943, 44504, 39516, 46166, 46882, 48458,
|
||||
49524, 44720, 53620, 55901, 58585, 61007, 56985, 68541, 72795, 78542, 84522, 91109, 86305,
|
||||
106424, 115156, 125756, 136606, 130742, 160773, 176254, 192943, 209036, 197566, 242622,
|
||||
261520, 282054, 301671, 280379, 340427, 361651, 381541, 400844, 366045, 431948, 448730,
|
||||
461965, 474742, 484742, 429398, 495580, 498192, 500972, 495907, 430082, 487111, 478174,
|
||||
466274, 454838, 387260, 429238, 412109, 394391, 377166, 314069, 342320, 323342, 303672,
|
||||
287065, 269839, 222018, 237587, 220910, 207113, 192675, 157585, 168534, 158188, 147129,
|
||||
137956, 112961, 122187, 114196, 108509, 103007, 85884, 94253, 90044, 87221, 84668, 71879,
|
||||
80385, 78623, 77783, 77091, 76156, 66260, 76005, 75463, 74981, 75396, 65342, 75501, 76266,
|
||||
76450, 76053, 67137, 76115, 76326, 76434, 77201, 67050, 76313, 76293, 75723, 75894, 75980,
|
||||
65911, 74524, 74224, 73147, 72685, 62974, 70932, 70591, 69998, 68042, 58874, 66580, 65624,
|
||||
64716, 63501, 54665, 61603, 60323, 59414, 57771, 50209, 56266, 55508, 54012, 53424, 52116,
|
||||
44747, 50440, 49397, 47822, 47640, 40761, 45368, 44812, 44429, 42998, 37046, 41961, 41013,
|
||||
39896, 39609, 34086, 38540, 37511, 36643, 35892, 35723, 30200, 34272, 33283, 32656, 32171,
|
||||
27407, 30907, 30045, 29634, 28707, 24846, 27648, 27349, 26831, 25707, 22627, 25339, 24986,
|
||||
24310, 23723, 23324,
|
||||
],
|
||||
maxBin: 13,
|
||||
maxBinValue: 586201,
|
||||
},
|
||||
colormap: {
|
||||
Name: '2hot',
|
||||
RGBPoints: [
|
||||
-1, 0.0416667, 0, 0, -0.873016, 0.208333, 0, 0, -0.746032, 0.375, 0, 0, -0.619048,
|
||||
0.541667, 0, 0, -0.492063, 0.708333, 0, 0, -0.365079, 0.854137, 0, 0, -0.238095, 0.937488,
|
||||
0.039062, 0, -0.111111, 1, 0.208333, 0, 0.015873, 1, 0.375, 0, 0.142857, 1, 0.541667, 0,
|
||||
0.269841, 1, 0.708333, 0, 0.396825, 1, 0.858805, 0.03125, 0.52381, 1, 0.947392, 0.15625,
|
||||
0.650794, 1, 1, 0.3125, 0.777778, 1, 1, 0.5625, 0.904762, 1, 1, 0.8125, 1, 1, 1, 1,
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
{WindowLevelTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={WindowLevel} />
|
||||
|
||||
## Usage
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="WindowLevel/__stories__/windowLevel.stories.mdx" />
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './WindowLevel';
|
||||
@@ -0,0 +1,64 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export type Range = {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
|
||||
export type VOI = {
|
||||
windowWidth: number;
|
||||
windowCenter: number;
|
||||
};
|
||||
|
||||
export type VOIRange = {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
|
||||
export type Histogram = {
|
||||
bins: number[];
|
||||
numBins: number;
|
||||
maxBin: number;
|
||||
maxBinValue: number;
|
||||
range: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type Colormap = {
|
||||
Name: string;
|
||||
RGBPoints: number[];
|
||||
};
|
||||
|
||||
export const rangePropType = PropTypes.shape({
|
||||
min: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
export const voiRangePropType = PropTypes.shape({
|
||||
min: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
export const voiPropType = PropTypes.shape({
|
||||
windowCenter: PropTypes.number.isRequired,
|
||||
windowWidth: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
export const histogramPropType = PropTypes.shape({
|
||||
bins: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.instanceOf(Int32Array)])
|
||||
.isRequired,
|
||||
numBins: PropTypes.number.isRequired,
|
||||
maxBin: PropTypes.number.isRequired,
|
||||
maxBinValue: PropTypes.number.isRequired,
|
||||
range: PropTypes.shape({
|
||||
min: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
}),
|
||||
});
|
||||
|
||||
export const colormapPropType = PropTypes.shape({
|
||||
Name: PropTypes.string,
|
||||
RGBPoints: PropTypes.arrayOf(PropTypes.number),
|
||||
});
|
||||
Reference in new issue
Block a user