feat(data overlay): add foreground background control plus orientation change in the UI and add RTDOSE support (#5007)
This commit is contained in:
1 parent
c1fced0573
commit
cd4e6408bc
105 files changed
+3117
-788
No files matched your search
@@ -53,7 +53,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.9.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.11.6",
|
||||
"@emotion/serialize": "^1.1.3",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
|
||||
@@ -230,7 +230,7 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
// Update stored position presentation
|
||||
commandsManager.run('updateStoredPositionPresentation', {
|
||||
viewportId: viewport.viewportId,
|
||||
displaySetInstanceUID: referencedDisplaySetInstanceUID,
|
||||
displaySetInstanceUIDs: [referencedDisplaySetInstanceUID],
|
||||
referencedImageId: measurement.referencedImageId,
|
||||
options: {
|
||||
...measurement.metadata,
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.11.6",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21"
|
||||
|
||||
@@ -321,7 +321,7 @@ class MeasurementService extends PubSubService {
|
||||
this.mappings[source.uid] = [mapping];
|
||||
}
|
||||
|
||||
log.info(`New measurement mapping added to source '${this._getSourceToString(source)}'.`);
|
||||
// log.info(`New measurement mapping added to source '${this._getSourceToString(source)}'.`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -239,7 +239,10 @@ export default class ToolbarService extends PubSubService {
|
||||
const { disabled, disabledText, className, isActive } = evaluationResults.get(button.id);
|
||||
return { ...props, disabled, disabledText, className, isActive };
|
||||
} else {
|
||||
const evaluated = props.evaluate?.({ ...refreshProps, button });
|
||||
const evaluated =
|
||||
typeof props.evaluate === 'function'
|
||||
? props.evaluate({ ...refreshProps, button })
|
||||
: undefined;
|
||||
const updatedProps = {
|
||||
...props,
|
||||
...evaluated,
|
||||
|
||||
@@ -13,6 +13,9 @@ export type DisplaySet = {
|
||||
Modality?: string;
|
||||
imageIds?: string[];
|
||||
images?: unknown[];
|
||||
label?: string;
|
||||
/** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */
|
||||
isOverlayDisplaySet?: boolean;
|
||||
|
||||
// Details about how to display:
|
||||
/** A URL that can be used to display the thumbnail. Typically a data url */
|
||||
|
||||
@@ -19,11 +19,14 @@ const combineFrameInstance = (frame, instance) => {
|
||||
ImageType,
|
||||
} = instance;
|
||||
|
||||
if (NumberOfFrames < 2) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
instance.ImageType = dicomSplit(ImageType);
|
||||
const frameNumber = Number.parseInt(frame || 1);
|
||||
|
||||
if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) {
|
||||
const frameNumber = Number.parseInt(frame || 1);
|
||||
|
||||
if (PerFrameFunctionalGroupsSequence && SharedFunctionalGroupsSequence) {
|
||||
// this is to fix NM multiframe datasets with position and orientation
|
||||
// information inside DetectorInformationSequence
|
||||
if (!instance.ImageOrientationPatient && instance.DetectorInformationSequence) {
|
||||
@@ -96,9 +99,52 @@ const combineFrameInstance = (frame, instance) => {
|
||||
configurable: true,
|
||||
});
|
||||
return newInstance;
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// For RTDOSE datasets
|
||||
if (instance.GridFrameOffsetVector) {
|
||||
if (!instance._parentInstance) {
|
||||
Object.defineProperty(instance, '_parentInstance', {
|
||||
value: { ...instance },
|
||||
});
|
||||
}
|
||||
|
||||
const sharedInstance = createCombinedValue(
|
||||
instance._parentInstance,
|
||||
SharedFunctionalGroupsSequence?.[0],
|
||||
'_shared'
|
||||
);
|
||||
|
||||
const newInstance = createCombinedValue(
|
||||
sharedInstance,
|
||||
PerFrameFunctionalGroupsSequence?.[frameNumber - 1],
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const origin = newInstance.ImagePositionPatient?.map(Number);
|
||||
const orientation = newInstance.ImageOrientationPatient?.map(Number);
|
||||
const offset = Number(instance.GridFrameOffsetVector[frameNumber - 1]);
|
||||
|
||||
if (origin && orientation && !Number.isNaN(offset)) {
|
||||
const row = vec3.fromValues(orientation[0], orientation[1], orientation[2]);
|
||||
const col = vec3.fromValues(orientation[3], orientation[4], orientation[5]);
|
||||
const normal = vec3.cross(vec3.create(), row, col);
|
||||
|
||||
const position = vec3.scaleAndAdd(vec3.create(), vec3.fromValues(...origin), normal, offset);
|
||||
newInstance.ImagePositionPatient = [position[0], position[1], position[2]];
|
||||
}
|
||||
|
||||
Object.defineProperty(newInstance, 'frameNumber', {
|
||||
value: frameNumber,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR', 'SR', 'RTSTRUCT', 'RTDOSE', 'RTPLAN', 'RTRECORD', 'REG']);
|
||||
const LOW_PRIORITY_MODALITIES = Object.freeze([
|
||||
'SEG',
|
||||
'KO',
|
||||
'PR',
|
||||
'SR',
|
||||
'RTSTRUCT',
|
||||
'RTDOSE',
|
||||
'RTPLAN',
|
||||
'RTRECORD',
|
||||
'REG',
|
||||
]);
|
||||
|
||||
export default function isLowPriorityModality(Modality) {
|
||||
return LOW_PRIORITY_MODALITIES.includes(Modality);
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
id: add-viewport-icon
|
||||
title: How to add a custom icon to the viewport corners
|
||||
summary: Learn how to add a custom icon or dropdown to one of the viewport corners in OHIF.
|
||||
---
|
||||
|
||||
# How to add a custom icon to the viewport corners
|
||||
|
||||
## Question
|
||||
How can I add a custom icon or dropdown to one of the viewport corners in OHIF?
|
||||
|
||||
## Answer
|
||||
OHIF provides a customizable viewport action menu system that allows you to add icons, buttons, or dropdowns to any of the four corners of a viewport (topLeft, topRight, bottomLeft, bottomRight). This is done through the `customizationService` and the viewport action corners API.
|
||||
|
||||
Here's a complete example that shows how to add a mode switch dropdown to the top-left corner of the viewport:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
Button,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
// This is a complete, self-contained component that shows a dropdown menu
|
||||
// for switching modes when clicked
|
||||
function getModeSwitchMenu({ viewportId, element, location }) {
|
||||
const ModeSwitchMenu = () => {
|
||||
const { servicesManager } = useSystem();
|
||||
const { router } = servicesManager.services;
|
||||
const { viewportActionCornersService } = servicesManager.services;
|
||||
|
||||
const handleModeSwitch = (mode) => {
|
||||
const currentStudyInstanceUID = router.query.StudyInstanceUIDs;
|
||||
// Navigate to the selected mode with the current study
|
||||
router.navigate(`/${mode}?StudyInstanceUIDs=${currentStudyInstanceUID}`);
|
||||
};
|
||||
|
||||
// Get proper alignment based on the location
|
||||
let align = 'center';
|
||||
let side = 'bottom';
|
||||
|
||||
if (location !== undefined) {
|
||||
const positioning = viewportActionCornersService.getAlignAndSide(location);
|
||||
align = positioning.align;
|
||||
side = positioning.side;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-highlight"
|
||||
>
|
||||
<Icons.Tool />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[160px]"
|
||||
align={align}
|
||||
side={side}
|
||||
sideOffset={5}
|
||||
>
|
||||
<DropdownMenuLabel className="-ml-1">Mode</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => handleModeSwitch('longitudinal')}>
|
||||
Longitudinal
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleModeSwitch('segmentation')}>
|
||||
Segmentation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleModeSwitch('tmtv')}>
|
||||
TMTV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleModeSwitch('microscopy')}>
|
||||
Microscopy
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return <ModeSwitchMenu />;
|
||||
}
|
||||
|
||||
// In your mode or extension, add this to the customizations
|
||||
// This example shows how to add it in the onModeEnter lifecycle hook
|
||||
function onModeEnter({ servicesManager }) {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
// Add the mode switch icon to the top-left corner of the viewport
|
||||
customizationService.setCustomizations({
|
||||
'viewportActionMenu.topLeft': {
|
||||
// Use $push to add to existing items or $set to replace all items
|
||||
$push: [
|
||||
{
|
||||
id: 'modeSwitch',
|
||||
enabled: true,
|
||||
component: getModeSwitchMenu,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
1. **Location-based customization**: The viewport is divided into four corners identified by:
|
||||
- `viewportActionMenu.topLeft`
|
||||
- `viewportActionMenu.topRight`
|
||||
- `viewportActionMenu.bottomLeft`
|
||||
- `viewportActionMenu.bottomRight`
|
||||
|
||||
2. **Component structure**:
|
||||
- `id` - A unique identifier for your component
|
||||
- `enabled` - Boolean to control if the component should be displayed
|
||||
- `component` - A function that returns a React component to render
|
||||
|
||||
3. **Component positioning**: The component's position within a corner is determined by its order in the array. Components are rendered in the order they appear.
|
||||
|
||||
4. **Dropdown positioning**: Use the `viewportActionCornersService.getAlignAndSide()` method to get the correct alignment for your dropdown menu based on its location.
|
||||
|
||||
## Adding Multiple Components
|
||||
|
||||
If you want to add multiple components to the same corner or different corners, you can do it in a single customization:
|
||||
|
||||
```js
|
||||
customizationService.setCustomizations({
|
||||
'viewportActionMenu.topLeft': {
|
||||
$push: [
|
||||
{
|
||||
id: 'modeSwitch',
|
||||
enabled: true,
|
||||
component: getModeSwitchMenu,
|
||||
},
|
||||
],
|
||||
},
|
||||
'viewportActionMenu.topRight': {
|
||||
$push: [
|
||||
{
|
||||
id: 'anotherComponent',
|
||||
enabled: true,
|
||||
component: getAnotherComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Replacing Existing Components
|
||||
|
||||
If you want to replace all components in a corner instead of adding to them, use `$set` instead of `$push`:
|
||||
|
||||
```js
|
||||
customizationService.setCustomizations({
|
||||
'viewportActionMenu.topLeft': {
|
||||
$set: [
|
||||
{
|
||||
id: 'modeSwitch',
|
||||
enabled: true,
|
||||
component: getModeSwitchMenu,
|
||||
},
|
||||
// This will be the only components in the top-left corner
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Remember that this customization will affect all viewports in the active mode. If you need different behavior for different viewports, you should check the `viewportId` parameter in your component's logic.
|
||||
@@ -6,6 +6,7 @@ summary: Technical explanations and solutions for OHIF Viewer implementation cha
|
||||
|
||||
# Technical FAQ
|
||||
|
||||
* [How to add a custom icon to the viewport corners](./add-viewport-icon.md)
|
||||
|
||||
|
||||
## Viewer opens but does not show any thumbnails
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: 3.10 -> 3.11 beta
|
||||
---
|
||||
|
||||
# Migration Guide
|
||||
|
||||
This guide provides information about migrating from OHIF version 3.10 to version 3.11.
|
||||
|
||||
## General
|
||||
|
||||
`viewportActionMenu.segmentationOverlay` is renamed to `viewportActionMenu.dataOverlay`
|
||||
as it handles now both segmentation and data overlay.
|
||||
|
||||
## Viewport Action Menu Customization
|
||||
|
||||
The structure for defining viewport action menu customizations has changed. See the [Viewport Action Menu](./viewport-action-menu.md) migration guide for details.
|
||||
|
||||
|
||||
## updateStoredPositionPresentation
|
||||
|
||||
now uses displaySetInstanceUIDs instead of displaySetInstanceUID.
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_label: Viewport Action Menu
|
||||
summary: Migration guide for OHIF 3.11's viewport action menu customization changes, including the transition from individual item configurations to location-based arrays and the removal of index priorities.
|
||||
---
|
||||
|
||||
# Viewport Action Menu Customization
|
||||
|
||||
In OHIF 3.11, we've redesigned how viewport action menu customizations are defined to make them more intuitive and organized by location.
|
||||
|
||||
## Changes
|
||||
|
||||
Previously, viewport action menu customizations were defined with individual item configurations that specified location and priority:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
'viewportActionMenu.orientationMenu': {
|
||||
enabled: true,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
indexPriority: 1,
|
||||
},
|
||||
'viewportActionMenu.dataOverlay': {
|
||||
enabled: true,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
indexPriority: 2,
|
||||
},
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Now, viewport action menu customizations are organized by location, with each location having its own customization ID:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
'viewportActionMenu.topLeft': [
|
||||
{
|
||||
id: 'orientationMenu',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'dataOverlay',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'windowLevelActionMenu',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
'viewportActionMenu.topRight': [],
|
||||
'viewportActionMenu.bottomLeft': [],
|
||||
'viewportActionMenu.bottomRight': [],
|
||||
};
|
||||
```
|
||||
|
||||
## Migration Steps
|
||||
|
||||
1. Reorganize your viewport action menu customizations by using location-based customization IDs (`viewportActionMenu.topLeft`, `viewportActionMenu.topRight`, etc.).
|
||||
2. For each component, move it into the appropriate location array.
|
||||
3. Replace the component key with an `id` property that doesn't include the prefix (just use `orientationMenu` instead of `viewportActionMenu.orientationMenu`).
|
||||
4. Remove the `location` property (since it's now implied by the customization ID).
|
||||
5. Remove the `indexPriority` property (order in the array now determines display order).
|
||||
6. For each component, provide a `component` function that returns the component instance.
|
||||
|
||||
## Component Rendering
|
||||
|
||||
Component rendering logic is now included directly in the item configuration via a `component` function:
|
||||
|
||||
```ts
|
||||
const createOrientationMenu = ({ viewportId, element, location }) => {
|
||||
return getViewportOrientationMenu({
|
||||
viewportId,
|
||||
element,
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
const createDataOverlay = ({ viewportId, element, displaySets, location }) => {
|
||||
return getViewportDataOverlaySettingsMenu({
|
||||
viewportId,
|
||||
element,
|
||||
displaySets,
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
'viewportActionMenu.topLeft': [
|
||||
{
|
||||
id: 'orientationMenu',
|
||||
enabled: true,
|
||||
component: createOrientationMenu,
|
||||
},
|
||||
{
|
||||
id: 'dataOverlay',
|
||||
enabled: true,
|
||||
component: createDataOverlay,
|
||||
},
|
||||
// other components...
|
||||
],
|
||||
// other locations...
|
||||
};
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: 3.9 -> 3.10 beta
|
||||
sidebar_label: 3.9 -> 3.10
|
||||
title: Migration Guide from 3.9 to 3.10 beta
|
||||
summary: Migration guide for upgrading from OHIF 3.9 to 3.10 beta, covering general changes, customization service improvements, UI component upgrades, command handling, hotkey updates, routing changes, and testing strategies.
|
||||
---
|
||||
|
||||
@@ -937,8 +937,8 @@ window.config = {
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'viewportActionMenu.segmentationOverlay',
|
||||
description: 'Configures the display and location of the segmentation overlay in the viewport.',
|
||||
id: 'viewportActionMenu.dataOverlay',
|
||||
description: 'Configures the display and location of the data overlay in the viewport.',
|
||||
image: segmentationOverlay,
|
||||
default: null,
|
||||
configuration: `
|
||||
@@ -946,7 +946,7 @@ window.config = {
|
||||
// rest of window config
|
||||
customizationService: [
|
||||
{
|
||||
'viewportActionMenu.segmentationOverlay': {
|
||||
'viewportActionMenu.dataOverlay': {
|
||||
$merge: {
|
||||
enabled: true,
|
||||
location: 1, // Set the location of the overlay in the viewport.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Button } from '../../components/Button/Button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -57,7 +57,7 @@ import { cn } from '../../lib/utils';
|
||||
* @property {() => void} onColor - Callback when color change is requested
|
||||
*/
|
||||
interface DataRowProps {
|
||||
number: number;
|
||||
number: number | null;
|
||||
disableEditing: boolean;
|
||||
description: string;
|
||||
details?: { primary: string[]; secondary: string[] };
|
||||
@@ -208,15 +208,18 @@ export const DataRow: React.FC<DataRowProps> = ({
|
||||
<div className="bg-primary/20 pointer-events-none absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"></div>
|
||||
|
||||
{/* Number Box */}
|
||||
<div
|
||||
className={`flex h-7 max-h-7 w-7 flex-shrink-0 items-center justify-center rounded-l border-r border-black text-base ${
|
||||
isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{number}
|
||||
</div>
|
||||
{number !== null && (
|
||||
<div
|
||||
className={`flex h-7 max-h-7 w-7 flex-shrink-0 items-center justify-center rounded-l border-r border-black text-base ${
|
||||
isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{number}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Color Circle (Optional) */}
|
||||
{/* add some space if there is not segment index */}
|
||||
{number === null && <div className="ml-1 h-7"></div>}
|
||||
{colorHex && (
|
||||
<div className="flex h-7 w-5 items-center justify-center">
|
||||
<span
|
||||
|
||||
@@ -12,6 +12,7 @@ interface DoubleSliderProps {
|
||||
defaultValue?: [number, number];
|
||||
onValueChange?: (value: [number, number]) => void;
|
||||
showNumberInputs?: boolean;
|
||||
lockMode?: boolean;
|
||||
}
|
||||
|
||||
const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
@@ -24,10 +25,19 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
step = 1,
|
||||
defaultValue = [min, max],
|
||||
showNumberInputs = false,
|
||||
lockMode = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [value, setValue] = React.useState<[number, number]>(defaultValue);
|
||||
const trackRef = React.useRef<HTMLDivElement>(null);
|
||||
const dragStateRef = React.useRef<{
|
||||
startX: number;
|
||||
startValue: [number, number];
|
||||
rangeWidth: number;
|
||||
trackLeft: number;
|
||||
trackWidth: number;
|
||||
} | null>(null);
|
||||
|
||||
const prevDefaultValueRef = React.useRef<[number, number] | null>(null);
|
||||
|
||||
@@ -50,8 +60,172 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
return Math.round(num * inverse) / inverse;
|
||||
};
|
||||
|
||||
const handleRangePointerDown = (event: React.PointerEvent) => {
|
||||
if (!lockMode) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const rect = trackRef.current!.getBoundingClientRect();
|
||||
dragStateRef.current = {
|
||||
startX: event.clientX,
|
||||
startValue: [...value] as [number, number],
|
||||
rangeWidth: value[1] - value[0],
|
||||
trackLeft: rect.left,
|
||||
trackWidth: rect.width,
|
||||
};
|
||||
window.addEventListener('pointermove', handleRangePointerMove);
|
||||
window.addEventListener('pointerup', handleRangePointerUp, { once: true });
|
||||
};
|
||||
|
||||
const handleRangePointerMove = (event: PointerEvent) => {
|
||||
const state = dragStateRef.current;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const { startX, startValue, rangeWidth, trackWidth } = state;
|
||||
const dxPx = event.clientX - startX;
|
||||
const pct = dxPx / trackWidth;
|
||||
const delta = pct * (max - min);
|
||||
|
||||
let newLeft = startValue[0] + delta;
|
||||
let newRight = newLeft + rangeWidth;
|
||||
|
||||
// clamp to bounds
|
||||
if (newLeft < min) {
|
||||
newLeft = min;
|
||||
newRight = min + rangeWidth;
|
||||
} else if (newRight > max) {
|
||||
newRight = max;
|
||||
newLeft = max - rangeWidth;
|
||||
}
|
||||
|
||||
const clampedLeft = roundToStep(newLeft);
|
||||
const clampedRight = roundToStep(newRight);
|
||||
setValue([clampedLeft, clampedRight]);
|
||||
onValueChange?.([clampedLeft, clampedRight]);
|
||||
};
|
||||
|
||||
const handleRangePointerUp = () => {
|
||||
dragStateRef.current = null;
|
||||
window.removeEventListener('pointermove', handleRangePointerMove);
|
||||
};
|
||||
|
||||
const handleSliderChange = React.useCallback(
|
||||
(newValue: number[]) => {
|
||||
// Default behavior (no lock mode)
|
||||
if (!lockMode) {
|
||||
const clampedValue: [number, number] = [
|
||||
roundToStep(Math.max(min, Math.min(newValue[0], max))),
|
||||
roundToStep(Math.min(max, Math.max(newValue[1], min))),
|
||||
];
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock mode behavior
|
||||
// First, determine if this is a symmetric expansion/contraction or a range shift
|
||||
// by checking which thumb(s) moved
|
||||
const isLeftThumbMoved = newValue[0] !== value[0];
|
||||
const isRightThumbMoved = newValue[1] !== value[1];
|
||||
|
||||
// Case 1: Left thumb moved, right thumb needs to move symmetrically (opposite)
|
||||
if (isLeftThumbMoved && !isRightThumbMoved) {
|
||||
const delta = newValue[0] - value[0];
|
||||
const centerPoint = (value[0] + value[1]) / 2;
|
||||
|
||||
// Symmetric movement: move right thumb in opposite direction
|
||||
const newRight = value[1] - delta;
|
||||
|
||||
// Ensure values stay within bounds
|
||||
const clampedLeft = roundToStep(Math.max(min, Math.min(newValue[0], max)));
|
||||
const clampedRight = roundToStep(Math.min(max, Math.max(newRight, min)));
|
||||
|
||||
// Special case: If right would go out of bounds, adjust left too to maintain center
|
||||
if (newRight !== clampedRight) {
|
||||
// Calculate new delta given the clamped right value
|
||||
const adjustedDelta = value[1] - clampedRight;
|
||||
// Apply same delta to left but in opposite direction
|
||||
const adjustedLeft = value[0] + adjustedDelta;
|
||||
const finalClampedLeft = roundToStep(Math.max(min, Math.min(adjustedLeft, max)));
|
||||
|
||||
const finalValues: [number, number] = [finalClampedLeft, clampedRight];
|
||||
setValue(finalValues);
|
||||
onValueChange?.(finalValues);
|
||||
return;
|
||||
}
|
||||
|
||||
const finalValues: [number, number] = [clampedLeft, clampedRight];
|
||||
setValue(finalValues);
|
||||
onValueChange?.(finalValues);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: Right thumb moved, left thumb needs to move symmetrically (opposite)
|
||||
if (!isLeftThumbMoved && isRightThumbMoved) {
|
||||
const delta = newValue[1] - value[1];
|
||||
const centerPoint = (value[0] + value[1]) / 2;
|
||||
|
||||
// Symmetric movement: move left thumb in opposite direction
|
||||
const newLeft = value[0] - delta;
|
||||
|
||||
// Ensure values stay within bounds
|
||||
const clampedRight = roundToStep(Math.min(max, Math.max(newValue[1], min)));
|
||||
const clampedLeft = roundToStep(Math.max(min, Math.min(newLeft, max)));
|
||||
|
||||
// Special case: If left would go out of bounds, adjust right too to maintain center
|
||||
if (newLeft !== clampedLeft) {
|
||||
// Calculate new delta given the clamped left value
|
||||
const adjustedDelta = value[0] - clampedLeft;
|
||||
// Apply same delta to right but in opposite direction
|
||||
const adjustedRight = value[1] + adjustedDelta;
|
||||
const finalClampedRight = roundToStep(Math.min(max, Math.max(adjustedRight, min)));
|
||||
|
||||
const finalValues: [number, number] = [clampedLeft, finalClampedRight];
|
||||
setValue(finalValues);
|
||||
onValueChange?.(finalValues);
|
||||
return;
|
||||
}
|
||||
|
||||
const finalValues: [number, number] = [clampedLeft, clampedRight];
|
||||
setValue(finalValues);
|
||||
onValueChange?.(finalValues);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 3: Both thumbs moved (range shift) - maintain the same distance between thumbs
|
||||
if (isLeftThumbMoved && isRightThumbMoved) {
|
||||
const rangeWidth = value[1] - value[0];
|
||||
|
||||
// Calculate how much the left thumb moved
|
||||
const leftDelta = newValue[0] - value[0];
|
||||
|
||||
// New values maintaining the same range width
|
||||
let newLeft = newValue[0];
|
||||
let newRight = newLeft + rangeWidth;
|
||||
|
||||
// Handle bounds checking
|
||||
if (newRight > max) {
|
||||
newRight = max;
|
||||
newLeft = newRight - rangeWidth;
|
||||
}
|
||||
|
||||
if (newLeft < min) {
|
||||
newLeft = min;
|
||||
newRight = newLeft + rangeWidth;
|
||||
}
|
||||
|
||||
const clampedValues: [number, number] = [
|
||||
roundToStep(Math.max(min, Math.min(newLeft, max))),
|
||||
roundToStep(Math.min(max, Math.max(newRight, min))),
|
||||
];
|
||||
|
||||
setValue(clampedValues);
|
||||
onValueChange?.(clampedValues);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to default behavior
|
||||
const clampedValue: [number, number] = [
|
||||
roundToStep(Math.max(min, Math.min(newValue[0], max))),
|
||||
roundToStep(Math.min(max, Math.max(newValue[1], min))),
|
||||
@@ -59,25 +233,86 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
},
|
||||
[min, max, onValueChange, step]
|
||||
[min, max, onValueChange, step, value, lockMode]
|
||||
);
|
||||
|
||||
const handleInputChange = React.useCallback(
|
||||
(index: 0 | 1, inputValue: string) => {
|
||||
const newValue = parseFloat(inputValue);
|
||||
if (!isNaN(newValue)) {
|
||||
const clampedValue: [number, number] = [...value];
|
||||
clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max));
|
||||
if (index === 0 && clampedValue[0] > clampedValue[1]) {
|
||||
clampedValue[1] = clampedValue[0];
|
||||
} else if (index === 1 && clampedValue[1] < clampedValue[0]) {
|
||||
clampedValue[0] = clampedValue[1];
|
||||
// Default behavior (no lock mode)
|
||||
if (!lockMode) {
|
||||
const clampedValue: [number, number] = [...value];
|
||||
clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max));
|
||||
if (index === 0 && clampedValue[0] > clampedValue[1]) {
|
||||
clampedValue[1] = clampedValue[0];
|
||||
} else if (index === 1 && clampedValue[1] < clampedValue[0]) {
|
||||
clampedValue[0] = clampedValue[1];
|
||||
}
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock mode behavior
|
||||
const centerPoint = (value[0] + value[1]) / 2;
|
||||
const rangeWidth = value[1] - value[0];
|
||||
const clampedValue: [number, number] = [...value];
|
||||
|
||||
// Calculate new value with constraints
|
||||
const boundedNewValue = roundToStep(Math.min(Math.max(newValue, min), max));
|
||||
|
||||
if (index === 0) {
|
||||
// Left thumb changed
|
||||
const delta = boundedNewValue - value[0];
|
||||
|
||||
// Symmetric change: move right thumb in opposite direction
|
||||
const newRight = value[1] - delta;
|
||||
const boundedNewRight = roundToStep(Math.min(Math.max(newRight, min), max));
|
||||
|
||||
// If right would go out of bounds, adjust left too
|
||||
if (newRight !== boundedNewRight) {
|
||||
// Adjust to maintain the same width but respect bounds
|
||||
if (boundedNewRight === max) {
|
||||
clampedValue[0] = roundToStep(Math.max(max - rangeWidth, min));
|
||||
clampedValue[1] = max;
|
||||
} else if (boundedNewRight === min) {
|
||||
clampedValue[0] = min;
|
||||
clampedValue[1] = roundToStep(Math.min(min + rangeWidth, max));
|
||||
}
|
||||
} else {
|
||||
clampedValue[0] = boundedNewValue;
|
||||
clampedValue[1] = boundedNewRight;
|
||||
}
|
||||
} else {
|
||||
// Right thumb changed
|
||||
const delta = boundedNewValue - value[1];
|
||||
|
||||
// Symmetric change: move left thumb in opposite direction
|
||||
const newLeft = value[0] - delta;
|
||||
const boundedNewLeft = roundToStep(Math.min(Math.max(newLeft, min), max));
|
||||
|
||||
// If left would go out of bounds, adjust right too
|
||||
if (newLeft !== boundedNewLeft) {
|
||||
// Adjust to maintain the same width but respect bounds
|
||||
if (boundedNewLeft === min) {
|
||||
clampedValue[0] = min;
|
||||
clampedValue[1] = roundToStep(Math.min(min + rangeWidth, max));
|
||||
} else if (boundedNewLeft === max) {
|
||||
clampedValue[0] = roundToStep(Math.max(max - rangeWidth, min));
|
||||
clampedValue[1] = max;
|
||||
}
|
||||
} else {
|
||||
clampedValue[0] = boundedNewLeft;
|
||||
clampedValue[1] = boundedNewValue;
|
||||
}
|
||||
}
|
||||
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
}
|
||||
},
|
||||
[value, min, max, onValueChange, step]
|
||||
[value, min, max, onValueChange, step, lockMode]
|
||||
);
|
||||
|
||||
const formatValue = (val: number) => {
|
||||
@@ -109,8 +344,14 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
value={value}
|
||||
onValueChange={handleSliderChange}
|
||||
>
|
||||
<SliderPrimitive.Track className="bg-primary/30 relative h-1 w-full grow overflow-hidden rounded-full">
|
||||
<SliderPrimitive.Range className="bg-primary absolute h-full" />
|
||||
<SliderPrimitive.Track
|
||||
ref={trackRef}
|
||||
className="bg-primary/30 relative h-1 w-full grow overflow-hidden rounded-full"
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
className={cn('bg-primary absolute h-full', lockMode && 'cursor-grab')}
|
||||
onPointerDown={handleRangePointerDown}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
|
||||
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
|
||||
|
||||
@@ -75,6 +75,10 @@ import IconTransferring from './Sources/IconTransferring';
|
||||
import Alert from './Sources/Alert';
|
||||
import AlertOutline from './Sources/AlertOutline';
|
||||
import Clipboard from './Sources/Clipboard';
|
||||
import OrientationSwitch from './Sources/OrientationSwitch';
|
||||
import LayerBackground from './Sources/LayerBackground';
|
||||
import LayerForeground from './Sources/LayerForeground';
|
||||
import LayerSegmentation from './Sources/LayerSegmentation';
|
||||
import {
|
||||
Tool3DRotate,
|
||||
ToolAngle,
|
||||
@@ -378,6 +382,10 @@ export const Icons = {
|
||||
/>
|
||||
),
|
||||
// Icons
|
||||
LayerBackground,
|
||||
LayerForeground,
|
||||
LayerSegmentation,
|
||||
OrientationSwitch,
|
||||
Clipboard,
|
||||
ActionNewDialog,
|
||||
GroupLayers,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const LayerBackground = (props: IconProps) => (
|
||||
<svg
|
||||
width="29"
|
||||
height="28"
|
||||
viewBox="0 0 29 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M20.0801 8.92383C22.013 8.92383 23.5799 10.4909 23.5801 12.4238V19.0801C23.5801 21.0131 22.0131 22.5801 20.0801 22.5801H13.4238C11.4908 22.5801 9.92383 21.0131 9.92383 19.0801V16.6562H14.6562C16.3131 16.6562 17.6562 15.3131 17.6562 13.6562V8.92383H20.0801Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x="5.5"
|
||||
y="4.5"
|
||||
width="11.6565"
|
||||
height="11.6565"
|
||||
rx="2.5"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LayerBackground;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const LayerForeground = (props: IconProps) => (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x="10.3999"
|
||||
y="9.3999"
|
||||
width="12.6001"
|
||||
height="12.6001"
|
||||
rx="3"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<rect
|
||||
x="5.5"
|
||||
y="4.5"
|
||||
width="11.6001"
|
||||
height="11.6001"
|
||||
rx="2.5"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LayerForeground;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const LayerSegmentation = (props: IconProps) => (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7 9.16547L9.16546 7.00002M7 12.4512L12.4999 6.95129M7 15.7369L15.7369 6.99999M7.18447 18.8381L18.8381 7.1845M8.77085 20.5375L20.4043 8.90397M11.794 20.8L20.8 11.794M15.0797 20.8L20.8 15.0797M18.4323 20.7333L20.7332 18.4324M10 20.8H17.8C19.4569 20.8 20.8 19.4569 20.8 17.8V10C20.8 8.34315 19.4569 7.00001 17.8 7.00001H10C8.34315 7.00001 7 8.34316 7 10V17.8C7 19.4569 8.34315 20.8 10 20.8Z"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LayerSegmentation;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const OrientationSwitch = (props: IconProps) => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8.07778 16.4182L9.96563 18.8288L7.30469 20.4557"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M20.0917 14C20.3502 14.1669 20.5669 14.3909 20.7252 14.6547C20.8835 14.9186 20.9792 15.2152 21.0048 15.5218C21.0048 17.3123 17.4909 18.7938 13.0151 18.946"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.96782 18.828C6.23931 18.4354 3.5 17.1038 3.5 15.5218C3.52464 15.2179 3.61841 14.9236 3.77416 14.6614C3.9299 14.3992 4.1435 14.1761 4.39865 14.0091"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.02539 14.1079C8.52393 14.1079 8.2002 13.8159 8.2002 13.3525C8.2002 13.2256 8.23193 13.0479 8.30811 12.8511L10.9678 5.62109C11.1899 5.01172 11.5581 4.73242 12.1548 4.73242C12.7578 4.73242 13.126 4.99902 13.3545 5.61475L16.0205 12.8511C16.0967 13.0605 16.1284 13.2065 16.1284 13.3525C16.1284 13.7969 15.7856 14.1079 15.3096 14.1079C14.8652 14.1079 14.6177 13.9048 14.4717 13.416L13.8687 11.6641H10.4473L9.84424 13.4033C9.69189 13.8984 9.45068 14.1079 9.02539 14.1079ZM10.8345 10.3691H13.4688L12.1675 6.45898H12.123L10.8345 10.3691Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default OrientationSwitch;
|
||||
@@ -29,6 +29,7 @@ interface NumericMetaContextValue {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
lockMode?: boolean;
|
||||
}
|
||||
|
||||
const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
|
||||
@@ -47,6 +48,7 @@ interface NumericMetaContainerProps {
|
||||
max?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
lockMode?: boolean; // Add lock mode prop for doubleRange
|
||||
}
|
||||
|
||||
function NumericMetaContainer({
|
||||
@@ -60,6 +62,7 @@ function NumericMetaContainer({
|
||||
max = 100,
|
||||
step = 1,
|
||||
className,
|
||||
lockMode = false,
|
||||
children,
|
||||
}: PropsWithChildren<NumericMetaContainerProps>) {
|
||||
// Calculate default values based on min and max
|
||||
@@ -115,6 +118,7 @@ function NumericMetaContainer({
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
lockMode,
|
||||
}}
|
||||
>
|
||||
<div className={cn('flex flex-col', className)}>{children}</div>
|
||||
@@ -236,7 +240,7 @@ function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
|
||||
throw new Error('DoubleRange must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, doubleValue, setDoubleValue, min, max, step } = ctx;
|
||||
const { mode, doubleValue, setDoubleValue, min, max, step, lockMode } = ctx;
|
||||
|
||||
const handleSliderChange = useCallback(
|
||||
(values: [number, number]) => {
|
||||
@@ -258,6 +262,7 @@ function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
|
||||
defaultValue={doubleValue}
|
||||
onValueChange={handleSliderChange}
|
||||
showNumberInputs={showNumberInputs}
|
||||
lockMode={lockMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
|
||||
onSegmentEdit,
|
||||
onSegmentDelete,
|
||||
data,
|
||||
showSegmentIndex = true,
|
||||
} = useSegmentationTableContext('SegmentationSegments');
|
||||
|
||||
// Try to get segmentation data from expanded context first, then fall back to table context
|
||||
@@ -76,7 +77,7 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
|
||||
const DataRowComponent = (
|
||||
<DataRow
|
||||
key={segmentIndex}
|
||||
number={segmentIndex}
|
||||
number={showSegmentIndex ? segmentIndex : null}
|
||||
title={label}
|
||||
// details={displayText}
|
||||
description={displayText}
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ export interface SegmentationTableContextType {
|
||||
activeSegmentation?: Segmentation;
|
||||
disableEditing: boolean;
|
||||
showAddSegment?: boolean;
|
||||
showSegmentIndex?: boolean;
|
||||
renderInactiveSegmentations?: boolean;
|
||||
|
||||
// Function handlers
|
||||
|
||||
@@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-input text-foreground ring-offset-background placeholder:text-muted-foreground focus:ring-ring [&>span]:line-clamp-1 hover:bg-primary/10 flex h-7 w-full items-center justify-between whitespace-nowrap rounded border bg-transparent px-2 py-2 text-base shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'border-input text-foreground ring-offset-background placeholder:text-muted-foreground focus:ring-ring hover:bg-primary/10 flex h-7 w-full min-w-0 items-center justify-between rounded border bg-transparent px-2 py-2 text-base shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -64,4 +64,4 @@ ViewportActionCorners.propTypes = {
|
||||
cornerComponents: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { ViewportActionCorners };
|
||||
export { ViewportActionCorners };
|
||||
@@ -124,6 +124,7 @@ interface ViewportGridApi {
|
||||
getActiveViewportOptionByKey: (key: string) => any;
|
||||
setViewportGridSizeChanged: (props: any) => void;
|
||||
publishViewportsReady: () => void;
|
||||
getDisplaySetsUIDsForViewport: (viewportId: string) => string[];
|
||||
}
|
||||
|
||||
// Update the context type
|
||||
@@ -539,6 +540,7 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider
|
||||
setViewportGridSizeChanged: props => service.setViewportGridSizeChanged(props),
|
||||
publishViewportsReady: () => service.publishViewportsReady(),
|
||||
getLayoutOptionsFromState: state => service.getLayoutOptionsFromState(state),
|
||||
getDisplaySetsUIDsForViewport: viewportId => service.getDisplaySetsUIDsForViewport(viewportId),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ViewportActionCornersLocations } from '../components/Viewport/ViewportActionCorners';
|
||||
|
||||
export interface ViewportActionCornersComponentInfo {
|
||||
id: string;
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
export interface ViewportActionCornersProps {
|
||||
cornerComponents: Record<ViewportActionCornersLocations, ViewportActionCornersComponentInfo[]>;
|
||||
visibleItemsPerCorner?: number;
|
||||
isActiveViewport: boolean;
|
||||
}
|
||||
Reference in new issue
Block a user