feat(toolbar): new Toolbar to enable reactive state synchronization (#3983)

This commit is contained in:
Alireza authored and GitHub committed 2024-03-27 16:01:32 -04:00
1 parent 79d5c36bda
commit 566b25a544
155 files changed
+6311 -6577

No files matched your search

+3 -49
View File
@@ -3,16 +3,12 @@ import toolbarButtons from './toolbarButtons';
import { id } from './id';
import initToolGroups from './initToolGroups';
import moreTools from './moreTools';
import moreToolsMpr from './moreToolsMpr';
import i18n from 'i18next';
// Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it.
const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG'];
const DEFAULT_TOOL_GROUP_ID = 'default';
const MPR_TOOL_GROUP_ID = 'mpr';
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
@@ -82,50 +78,8 @@ function modeFactory() {
'@ohif/extension-test.customizationModule.custom-context-menu',
]);
let unsubscribe;
toolbarService.setDefaultTool({
groupId: 'WindowLevel',
itemId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
const activateTool = () => {
toolbarService.recordInteraction(toolbarService.getDefaultTool());
// We don't need to reset the active tool whenever a viewport is getting
// added to the toolGroup.
unsubscribe();
};
// Since we only have one viewport for the basic cs3d mode and it has
// only one hanging protocol, we can just use the first viewport
({ unsubscribe } = toolGroupService.subscribe(
toolGroupService.EVENTS.VIEWPORT_ADDED,
activateTool
));
toolbarService.init(extensionManager);
toolbarService.addButtons([...toolbarButtons, ...moreTools, ...moreToolsMpr]);
toolbarService.createButtonSection(DEFAULT_TOOL_GROUP_ID, [
'MeasurementTools',
'Zoom',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'MPR',
'MoreTools',
]);
toolbarService.createButtonSection(MPR_TOOL_GROUP_ID, [
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'WindowLevel',
@@ -134,7 +88,7 @@ function modeFactory() {
'Layout',
'MPR',
'Crosshairs',
'MoreToolsMpr',
'MoreTools',
]);
},
onModeExit: ({ servicesManager }) => {
+155 -200
View File
@@ -1,228 +1,183 @@
import type { RunCommand } from '@ohif/core/types';
import { EVENTS } from '@cornerstonejs/core';
import { ToolbarService } from '@ohif/core';
import { ToolbarService, ViewportGridService } from '@ohif/core';
import { setToolActiveToolbar } from './toolbarButtons';
const { createButton } = ToolbarService;
const ReferenceLinesCommands: RunCommand = [
const ReferenceLinesListeners: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ReferenceLines',
},
context: 'CORNERSTONE',
},
];
const moreTools = [
{
id: 'MoreTools',
type: 'ohif.splitButton',
uiType: 'ohif.splitButton',
props: {
isRadio: true, // ?
groupId: 'MoreTools',
primary: ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
},
],
'Reset'
),
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
primary: createButton({
id: 'Reset',
icon: 'tool-reset',
tooltip: 'Reset View',
label: 'Reset',
commands: 'resetViewport',
evaluate: 'evaluate.action',
}),
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Tools',
},
items: [
ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
createButton({
id: 'Reset',
icon: 'tool-reset',
label: 'Reset View',
tooltip: 'Reset View',
commands: 'resetViewport',
evaluate: 'evaluate.action',
}),
createButton({
id: 'rotate-right',
icon: 'tool-rotate-right',
label: 'Rotate Right',
tooltip: 'Rotate +90',
commands: 'rotateViewportCW',
evaluate: 'evaluate.action',
}),
createButton({
id: 'flipHorizontal',
icon: 'tool-flip-horizontal',
label: 'Flip Horizontal',
tooltip: 'Flip Horizontally',
commands: 'flipViewportHorizontal',
evaluate: 'evaluate.viewportProperties.toggle',
}),
createButton({
id: 'ImageSliceSync',
icon: 'link',
label: 'Image Slice Sync',
tooltip: 'Enable position synchronization on stack viewports',
commands: {
commandName: 'toggleSynchronizer',
commandOptions: {
type: 'imageSlice',
},
],
'Reset'
),
ToolbarService._createActionButton(
'rotate-right',
'tool-rotate-right',
'Rotate Right',
[
{
commandName: 'rotateViewportCW',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Rotate +90'
),
ToolbarService._createActionButton(
'flip-horizontal',
'tool-flip-horizontal',
'Flip Horizontally',
[
{
commandName: 'flipViewportHorizontal',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Flip Horizontally'
),
ToolbarService._createToggleButton(
'ImageSliceSync',
'link',
'Image Slice Sync',
[
{
},
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleImageSliceSync',
commandOptions: { toggledState: true },
},
],
'Enable position synchronization on stack viewports',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleImageSliceSync',
commandOptions: { toggledState: true },
},
},
evaluate: 'evaluate.cornerstone.synchronizer',
}),
createButton({
id: 'ReferenceLines',
icon: 'tool-referenceLines',
label: 'Reference Lines',
tooltip: 'Show Reference Lines',
commands: {
commandName: 'setToolEnabled',
commandOptions: {
toolName: 'ReferenceLines',
toggle: true,
},
}
),
ToolbarService._createToggleButton(
'ReferenceLines',
'tool-referenceLines', // change this with the new icon
'Reference Lines',
ReferenceLinesCommands,
'Show Reference Lines',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
},
listeners: {
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
},
evaluate: 'evaluate.cornerstoneTool.toggle',
}),
createButton({
id: 'ImageOverlay',
icon: 'toggle-dicom-overlay',
label: 'Image Overlay',
tooltip: 'Toggle Image Overlay',
commands: {
commandName: 'setToolEnabled',
commandOptions: {
toolName: 'ImageOverlayViewer',
toggle: true,
},
}
),
ToolbarService._createToolButton(
'StackScroll',
'tool-stack-scroll',
'Stack Scroll',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'StackScroll',
},
context: 'CORNERSTONE',
},
],
'Stack Scroll'
),
ToolbarService._createActionButton(
'invert',
'tool-invert',
'Invert',
[
{
commandName: 'invertViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Invert Colors'
),
ToolbarService._createToolButton(
'Probe',
'tool-probe',
'Probe',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'DragProbe',
},
context: 'CORNERSTONE',
},
],
'Probe'
),
ToolbarService._createToggleButton(
'cine',
'tool-cine',
'Cine',
[
{
commandName: 'toggleCine',
context: 'CORNERSTONE',
},
],
'Cine'
),
ToolbarService._createToolButton(
'Angle',
'tool-angle',
'Angle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Angle',
},
context: 'CORNERSTONE',
},
],
'Angle'
),
ToolbarService._createToolButton(
'Magnify',
'tool-magnify',
'Magnify',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Magnify',
},
context: 'CORNERSTONE',
},
],
'Magnify'
),
ToolbarService._createToolButton(
'Rectangle',
'tool-rectangle',
'Rectangle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'RectangleROI',
},
context: 'CORNERSTONE',
},
],
'Rectangle'
),
ToolbarService._createActionButton(
'TagBrowser',
'list-bullets',
'Dicom Tag Browser',
[
{
commandName: 'openDICOMTagViewer',
commandOptions: {},
context: 'DEFAULT',
},
],
'Dicom Tag Browser'
),
},
evaluate: 'evaluate.cornerstoneTool.toggle',
}),
createButton({
id: 'StackScroll',
icon: 'tool-stack-scroll',
label: 'Stack Scroll',
tooltip: 'Stack Scroll',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'invert',
icon: 'tool-invert',
label: 'Invert',
tooltip: 'Invert Colors',
commands: 'invertViewport',
evaluate: 'evaluate.viewportProperties.toggle',
}),
createButton({
id: 'Probe',
icon: 'tool-probe',
label: 'Probe',
tooltip: 'Probe',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'Cine',
icon: 'tool-cine',
label: 'Cine',
tooltip: 'Cine',
commands: 'toggleCine',
evaluate: 'evaluate.cine',
}),
createButton({
id: 'Angle',
icon: 'tool-angle',
label: 'Angle',
tooltip: 'Angle',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'Magnify',
icon: 'tool-magnify',
label: 'Magnify',
tooltip: 'Magnify',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'RectangleROI',
icon: 'tool-rectangle',
label: 'Rectangle',
tooltip: 'Rectangle',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'CalibrationLine',
icon: 'tool-calibration',
label: 'Calibration',
tooltip: 'Calibration Line',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'TagBrowser',
icon: 'list-bullets',
label: 'Dicom Tag Browser',
tooltip: 'Dicom Tag Browser',
commands: 'openDICOMTagViewer',
}),
],
},
},
-142
View File
@@ -1,142 +0,0 @@
import { ToolbarService } from '@ohif/core';
const moreToolsMpr = [
{
id: 'MoreToolsMpr',
type: 'ohif.splitButton',
props: {
isRadio: true, // ?
groupId: 'MoreTools',
primary: ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
},
],
'Reset'
),
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Tools',
},
items: [
ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
},
],
'Reset'
),
ToolbarService._createToolButton(
'StackScroll',
'tool-stack-scroll',
'Stack Scroll',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'StackScroll',
},
context: 'CORNERSTONE',
},
],
'Stack Scroll'
),
ToolbarService._createActionButton(
'invert',
'tool-invert',
'Invert',
[
{
commandName: 'invertViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Invert Colors'
),
ToolbarService._createToolButton(
'Probe',
'tool-probe',
'Probe',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'DragProbe',
},
context: 'CORNERSTONE',
},
],
'Probe'
),
ToolbarService._createToggleButton(
'cine',
'tool-cine',
'Cine',
[
{
commandName: 'toggleCine',
context: 'CORNERSTONE',
},
],
'Cine'
),
ToolbarService._createToolButton(
'Angle',
'tool-angle',
'Angle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Angle',
},
context: 'CORNERSTONE',
},
],
'Angle'
),
ToolbarService._createToolButton(
'Rectangle',
'tool-rectangle',
'Rectangle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'RectangleROI',
},
context: 'CORNERSTONE',
},
],
'Rectangle'
),
ToolbarService._createActionButton(
'TagBrowser',
'list-bullets',
'Dicom Tag Browser',
[
{
commandName: 'openDICOMTagViewer',
commandOptions: {},
context: 'DEFAULT',
},
],
'Dicom Tag Browser'
),
],
},
},
];
export default moreToolsMpr;
+92 -290
View File
@@ -1,14 +1,14 @@
// TODO: torn, can either bake this here; or have to create a whole new button type
// Only ways that you can pass in a custom React component for render :l
import {
// ExpandableToolbarButton,
// ListMenu,
WindowLevelMenuItem,
} from '@ohif/ui';
import { ToolbarService, defaults } from '@ohif/core';
import { defaults, ToolbarService } from '@ohif/core';
import type { Button } from '@ohif/core/types';
const { windowLevelPresets } = defaults;
const { createButton } = ToolbarService;
/**
*
@@ -21,7 +21,6 @@ function _createWwwcPreset(preset, title, subtitle) {
id: preset.toString(),
title,
subtitle,
type: 'action',
commands: [
{
commandName: 'setWindowLevel',
@@ -34,212 +33,106 @@ function _createWwwcPreset(preset, title, subtitle) {
};
}
export const setToolActiveToolbar = {
commandName: 'setToolActiveToolbar',
commandOptions: {
toolGroupIds: ['default', 'mpr', 'SRToolGroup'],
},
};
const toolbarButtons: Button[] = [
// Measurement
{
id: 'MeasurementTools',
type: 'ohif.splitButton',
uiType: 'ohif.splitButton',
props: {
groupId: 'MeasurementTools',
isRadio: true, // ?
// Switch?
primary: ToolbarService._createToolButton(
'Length',
'tool-length',
'Length',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Length',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SRLength',
toolGroupId: 'SRToolGroup',
},
// we can use the setToolActive command for this from Cornerstone commandsModule
context: 'CORNERSTONE',
},
],
'Length'
),
// group evaluate to determine which item should move to the top
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
primary: createButton({
id: 'Length',
icon: 'tool-length',
label: 'Length',
tooltip: 'Length Tool',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Measure Tools',
},
items: [
ToolbarService._createToolButton(
'Length',
'tool-length',
'Length',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Length',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SRLength',
toolGroupId: 'SRToolGroup',
},
// we can use the setToolActive command for this from Cornerstone commandsModule
context: 'CORNERSTONE',
},
],
'Length Tool'
),
ToolbarService._createToolButton(
'Bidirectional',
'tool-bidirectional',
'Bidirectional',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Bidirectional',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SRBidirectional',
toolGroupId: 'SRToolGroup',
},
context: 'CORNERSTONE',
},
],
'Bidirectional Tool'
),
ToolbarService._createToolButton(
'ArrowAnnotate',
'tool-annotate',
'Annotation',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ArrowAnnotate',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SRArrowAnnotate',
toolGroupId: 'SRToolGroup',
},
context: 'CORNERSTONE',
},
],
'Arrow Annotate'
),
ToolbarService._createToolButton(
'EllipticalROI',
'tool-elipse',
'Ellipse',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'EllipticalROI',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SREllipticalROI',
toolGroupId: 'SRToolGroup',
},
context: 'CORNERSTONE',
},
],
'Ellipse Tool'
),
ToolbarService._createToolButton(
'CircleROI',
'tool-circle',
'Circle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'CircleROI',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SRCircleROI',
toolGroupId: 'SRToolGroup',
},
context: 'CORNERSTONE',
},
],
'Circle Tool'
),
createButton({
id: 'Length',
icon: 'tool-length',
label: 'Length',
tooltip: 'Length Tool',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'Bidirectional',
icon: 'tool-bidirectional',
label: 'Bidirectional',
tooltip: 'Bidirectional Tool',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'ArrowAnnotate',
icon: 'tool-annotate',
label: 'Annotation',
tooltip: 'Arrow Annotate',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'EllipticalROI',
icon: 'tool-ellipse',
label: 'Ellipse',
tooltip: 'Ellipse ROI',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'CircleROI',
icon: 'tool-circle',
label: 'Circle',
tooltip: 'Circle Tool',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
],
},
},
// Zoom..
{
id: 'Zoom',
type: 'ohif.radioGroup',
uiType: 'ohif.radioGroup',
props: {
type: 'tool',
icon: 'tool-zoom',
label: 'Zoom',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Zoom',
},
context: 'CORNERSTONE',
},
],
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
// Window Level + Presets...
{
id: 'WindowLevel',
type: 'ohif.splitButton',
uiType: 'ohif.splitButton',
props: {
groupId: 'WindowLevel',
primary: ToolbarService._createToolButton(
'WindowLevel',
'tool-window-level',
'Window Level',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
'Window Level'
),
primary: createButton({
id: 'WindowLevel',
icon: 'tool-window-level',
label: 'Window Level',
tooltip: 'Window Level',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
secondary: {
icon: 'chevron-down',
label: 'W/L Manual',
isActive: true,
tooltip: 'W/L Presets',
},
isAction: true, // ?
renderer: WindowLevelMenuItem,
items: [
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
@@ -253,137 +146,47 @@ const toolbarButtons: Button[] = [
// Pan...
{
id: 'Pan',
type: 'ohif.radioGroup',
uiType: 'ohif.radioGroup',
props: {
type: 'tool',
icon: 'tool-move',
label: 'Pan',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Pan',
},
context: 'CORNERSTONE',
},
],
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'Capture',
type: 'ohif.action',
uiType: 'ohif.radioGroup',
props: {
icon: 'tool-capture',
label: 'Capture',
type: 'action',
commands: [
{
commandName: 'showDownloadViewportModal',
commandOptions: {},
context: 'CORNERSTONE',
},
],
evaluate: 'evaluate.action',
},
},
{
id: 'Layout',
type: 'ohif.splitButton',
uiType: 'ohif.layoutSelector',
props: {
groupId: 'LayoutTools',
isRadio: false,
primary: {
id: 'Layout',
type: 'action',
uiType: 'ohif.layoutSelector',
icon: 'tool-layout',
label: 'Grid Layout',
props: {
rows: 4,
columns: 4,
commands: [
{
commandName: 'setLayout',
commandOptions: {},
context: 'CORNERSTONE',
},
],
},
},
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'Hanging Protocols',
},
items: [
rows: 3,
columns: 3,
evaluate: 'evaluate.action',
commands: [
{
id: '2x2',
type: 'action',
label: '2x2',
commands: [
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid',
stageId: '2x2',
},
context: 'DEFAULT',
},
],
},
{
id: '3x1',
type: 'action',
label: '3x1',
commands: [
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid',
stageId: '3x1',
},
context: 'DEFAULT',
},
],
},
{
id: '2x1',
type: 'action',
label: '2x1',
commands: [
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid',
stageId: '2x1',
},
context: 'DEFAULT',
},
],
},
{
id: '1x1',
type: 'action',
label: '1x1',
commands: [
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid',
stageId: '1x1',
},
context: 'DEFAULT',
},
],
commandName: 'setViewportGridLayout',
},
],
},
},
{
id: 'MPR',
type: 'ohif.action',
uiType: 'ohif.radioGroup',
props: {
type: 'toggle',
icon: 'icon-mpr',
label: 'MPR',
commands: [
@@ -394,24 +197,23 @@ const toolbarButtons: Button[] = [
},
},
],
evaluate: 'evaluate.mpr',
},
},
{
id: 'Crosshairs',
type: 'ohif.radioGroup',
uiType: 'ohif.radioGroup',
props: {
type: 'tool',
icon: 'tool-crosshair',
label: 'Crosshairs',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolGroupId: 'mpr',
toolName: 'Crosshairs',
},
commands: {
commandName: 'setToolActiveToolbar',
commandOptions: {
toolGroupIds: ['mpr'],
},
],
},
evaluate: 'evaluate.cornerstoneTool',
},
},
];