-
Notifications
You must be signed in to change notification settings - Fork 62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Widget component factory for tools #420
Changes from all commits
c483e97
e3ab0aa
ba3193b
7f98ebf
88c8cf8
44efe88
688b696
9b030e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool'; | ||
import { Maybe } from '@/src/types'; | ||
import { AnnotationTool } from '@/src/types/annotation-tool'; | ||
import { vtkAnnotationToolWidget } from '@/src/vtk/ToolWidgetUtils/utils'; | ||
import vtkAbstractWidgetFactory from '@kitware/vtk.js/Widgets/Core/AbstractWidgetFactory'; | ||
import vtkWidgetState from '@kitware/vtk.js/Widgets/Core/WidgetState'; | ||
import { VNode } from 'vue'; | ||
|
||
export interface WidgetComponentMeta< | ||
ToolID extends string, | ||
ToolStore extends AnnotationToolStore<ToolID>, | ||
WidgetState extends vtkWidgetState, | ||
WidgetFactory extends vtkAbstractWidgetFactory, | ||
ViewWidget extends vtkAnnotationToolWidget, | ||
SyncedState, | ||
InitState | ||
> { | ||
/** | ||
* The name of the component. | ||
*/ | ||
name: string; | ||
|
||
/** | ||
* The associated tool store. | ||
*/ | ||
useToolStore: () => ToolStore; | ||
|
||
/** | ||
* Construct a new standalone widget state. | ||
* Used by the placing widget component. | ||
*/ | ||
createStandaloneState: () => WidgetState; | ||
|
||
/** | ||
* Construct a new store-backed state. | ||
*/ | ||
createStoreBackedState: (id: ToolID, store: ToolStore) => WidgetState; | ||
|
||
/** | ||
* Construct a widget factory. | ||
*/ | ||
createWidgetFactory: (widgetState: WidgetState) => WidgetFactory; | ||
|
||
/** | ||
* A composable that syncs the widget factory's state to a reactive object. | ||
*/ | ||
useSyncedState: (widgetFactory: WidgetFactory) => SyncedState; | ||
|
||
/** | ||
* Resets the placing widget. | ||
*/ | ||
resetPlacingWidget: (widget: ViewWidget) => void; | ||
|
||
/** | ||
* Widget has been placed, so construct the init state object. | ||
* | ||
* The init state object is used to construct the actual store tool. | ||
*/ | ||
constructInitState: (state: SyncedState) => InitState; | ||
|
||
/** | ||
* An optional render function | ||
* | ||
* @param viewId the view ID | ||
* @param syncedState the synced state from the widget | ||
* @param labelProps the label props associated with the tool | ||
* @param tool an optional tool. Not available for the placing widget. | ||
*/ | ||
render?: ( | ||
viewId: string, | ||
syncedState: SyncedState, | ||
labelProps: Maybe<ToolStore['labels'][string]>, | ||
tool?: AnnotationTool<ToolID> | ||
) => VNode; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { LPSAxisDir } from '@/src/types/lps'; | ||
import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; | ||
import { computed, defineComponent, h, toRefs } from 'vue'; | ||
import type { Component, ComputedRef, PropType } from 'vue'; | ||
import AnnotationContextMenu from '@/src/components/tools/AnnotationContextMenu.vue'; | ||
import { useToolStore as useToolMetaStore } from '@/src/store/tools'; | ||
import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool'; | ||
import { Tools } from '@/src/store/tools/types'; | ||
import { useCurrentImage } from '@/src/composables/useCurrentImage'; | ||
import { getLPSAxisFromDir } from '@/src/utils/lps'; | ||
import { useCurrentFrameOfReference } from '@/src/composables/useCurrentFrameOfReference'; | ||
import { | ||
useContextMenu, | ||
useCurrentTools, | ||
} from '@/src/composables/annotationTool'; | ||
import { Maybe } from '@/src/types'; | ||
import { ContextMenuEvent } from '@/src/types/annotation-tool'; | ||
|
||
export interface AnnotationToolComponentMeta< | ||
ToolID extends string, | ||
ToolStore extends AnnotationToolStore<ToolID> | ||
> { | ||
type: Tools; | ||
name: string; | ||
useToolStore: () => ToolStore; | ||
Widget2DComponent: Component; | ||
PlacingWidget2DComponent: Component; | ||
} | ||
|
||
export function createAnnotationToolComponent< | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Vue component factories?? Cool! (like React =) For my edifcation, why a factory instead of passing everything as props to some generic compoenent? The other naive approach would have been to just start shoving all the logic into Composables. Probably would have ended up with one big There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After trying a component factory, I think we should use a big composable/some smaller composables. Like you said, we can have per-tool overrides, and I think constraining tools to an arbitrary interface is counter-productive. Plus, the use of render functions is not great. I'll update accordingly and let you know. |
||
ToolID extends string, | ||
ToolStore extends AnnotationToolStore<ToolID> | ||
>(meta: AnnotationToolComponentMeta<ToolID, ToolStore>) { | ||
return defineComponent({ | ||
name: meta.name, | ||
props: { | ||
viewId: { | ||
type: String, | ||
required: true, | ||
}, | ||
currentSlice: { | ||
type: Number, | ||
required: true, | ||
}, | ||
viewDirection: { | ||
type: String as PropType<LPSAxisDir>, | ||
required: true, | ||
}, | ||
widgetManager: { | ||
type: Object as PropType<vtkWidgetManager>, | ||
required: true, | ||
}, | ||
}, | ||
setup(props) { | ||
type LabelProps = ToolStore['labels'][string]; | ||
|
||
const { viewId, widgetManager, viewDirection, currentSlice } = | ||
toRefs(props); | ||
const toolMetaStore = useToolMetaStore(); | ||
const toolStore = meta.useToolStore(); | ||
const activeLabel = computed(() => toolStore.activeLabel); | ||
|
||
const { currentImageID } = useCurrentImage(); | ||
const isToolActive = computed( | ||
() => toolMetaStore.currentTool === meta.type | ||
); | ||
const viewAxis = computed(() => getLPSAxisFromDir(viewDirection.value)); | ||
|
||
const currentFrameOfReference = useCurrentFrameOfReference( | ||
viewDirection, | ||
currentSlice | ||
); | ||
|
||
const onToolPlaced = (initState: object) => { | ||
if (!currentImageID.value) return; | ||
toolStore.addTool({ | ||
imageID: currentImageID.value, | ||
frameOfReference: currentFrameOfReference.value, | ||
slice: currentSlice.value, | ||
label: activeLabel.value, | ||
...initState, | ||
}); | ||
}; | ||
|
||
// --- right-click menu --- // | ||
|
||
const { contextMenu, openContextMenu } = useContextMenu(); | ||
|
||
// --- template data --- // | ||
|
||
const currentTools = useCurrentTools(toolStore, viewAxis); | ||
|
||
const activeLabelProps: ComputedRef<Maybe<LabelProps>> = computed(() => { | ||
return activeLabel.value | ||
? (toolStore.labels[activeLabel.value] as LabelProps) | ||
: null; | ||
}); | ||
|
||
const render = () => | ||
h('div', { class: 'overlay-no-events' }, [ | ||
h('svg', { class: 'overlay-no-events' }, [ | ||
isToolActive.value | ||
? h(meta.PlacingWidget2DComponent, { | ||
currentSlice: currentSlice.value, | ||
viewDirection: viewDirection.value, | ||
widgetManager: widgetManager.value, | ||
viewId: viewId.value, | ||
labelProps: activeLabelProps.value, | ||
onPlaced: onToolPlaced, | ||
}) | ||
: null, | ||
...currentTools.value.map((tool) => | ||
h(meta.Widget2DComponent, { | ||
key: tool.id, | ||
toolId: tool.id, | ||
currentSlice: currentSlice.value, | ||
viewId: viewId.value, | ||
viewDirection: viewDirection.value, | ||
widgetManager: widgetManager.value, | ||
onContextmenu: (event: ContextMenuEvent) => | ||
openContextMenu(tool.id, event), | ||
}) | ||
), | ||
]), | ||
h(AnnotationContextMenu<ToolID>, { ref: contextMenu, toolStore }), | ||
]); | ||
|
||
return render; | ||
}, | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { WidgetComponentMeta } from '@/src/components/tools/common'; | ||
import { onVTKEvent } from '@/src/composables/onVTKEvent'; | ||
import { useCurrentImage } from '@/src/composables/useCurrentImage'; | ||
import { useViewWidget } from '@/src/composables/useViewWidget'; | ||
import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool'; | ||
import { Maybe } from '@/src/types'; | ||
import { LPSAxisDir } from '@/src/types/lps'; | ||
import { updatePlaneManipulatorFor2DView } from '@/src/utils/manipulators'; | ||
import { vtkAnnotationToolWidget } from '@/src/vtk/ToolWidgetUtils/utils'; | ||
import vtkAbstractWidgetFactory from '@kitware/vtk.js/Widgets/Core/AbstractWidgetFactory'; | ||
import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; | ||
import vtkWidgetState from '@kitware/vtk.js/Widgets/Core/WidgetState'; | ||
import vtkPlaneManipulator from '@kitware/vtk.js/Widgets/Manipulators/PlaneManipulator'; | ||
import { | ||
PropType, | ||
Ref, | ||
defineComponent, | ||
onMounted, | ||
onUnmounted, | ||
toRefs, | ||
watch, | ||
watchEffect, | ||
} from 'vue'; | ||
|
||
export function createPlacingWidget2DComponent< | ||
ToolID extends string, | ||
ToolStore extends AnnotationToolStore<ToolID>, | ||
WidgetState extends vtkWidgetState, | ||
WidgetFactory extends vtkAbstractWidgetFactory, | ||
ViewWidget extends vtkAnnotationToolWidget, | ||
SyncedState, | ||
InitState | ||
>( | ||
meta: WidgetComponentMeta< | ||
ToolID, | ||
ToolStore, | ||
WidgetState, | ||
WidgetFactory, | ||
ViewWidget, | ||
SyncedState, | ||
InitState | ||
> | ||
) { | ||
type LabelProps = ToolStore['labels'][string]; | ||
return defineComponent({ | ||
name: meta.name, | ||
emits: ['placed'], | ||
props: { | ||
widgetManager: { | ||
type: Object as PropType<vtkWidgetManager>, | ||
required: true, | ||
}, | ||
viewId: { | ||
type: String, | ||
required: true, | ||
}, | ||
viewDirection: { | ||
type: String as PropType<LPSAxisDir>, | ||
required: true, | ||
}, | ||
currentSlice: { | ||
type: Number, | ||
required: true, | ||
}, | ||
labelProps: { | ||
type: Object as PropType<LabelProps>, | ||
required: false, | ||
}, | ||
}, | ||
setup(props, { emit }) { | ||
const { viewId, labelProps, widgetManager, viewDirection, currentSlice } = | ||
toRefs(props); | ||
|
||
const { currentImageID, currentImageMetadata } = useCurrentImage(); | ||
|
||
const widgetState = meta.createStandaloneState(); | ||
const widgetFactory = meta.createWidgetFactory(widgetState); | ||
|
||
const syncedState = meta.useSyncedState(widgetFactory); | ||
const widget = useViewWidget<ViewWidget>(widgetFactory, widgetManager); | ||
|
||
onMounted(() => { | ||
meta.resetPlacingWidget(widget.value!); | ||
}); | ||
|
||
onUnmounted(() => { | ||
widgetFactory.delete(); | ||
}); | ||
|
||
// --- reset on slice/image changes --- // | ||
|
||
watch([currentSlice, currentImageID, widget], () => { | ||
if (widget.value) { | ||
meta.resetPlacingWidget(widget.value); | ||
} | ||
}); | ||
|
||
// --- placed event --- // | ||
|
||
onVTKEvent( | ||
widget as Ref<Maybe<vtkAnnotationToolWidget>>, | ||
'onPlacedEvent', | ||
() => { | ||
const initState = meta.constructInitState(syncedState); | ||
emit('placed', initState); | ||
meta.resetPlacingWidget(widget.value!); | ||
} | ||
); | ||
|
||
// --- manipulator --- // | ||
|
||
const manipulator = vtkPlaneManipulator.newInstance(); | ||
|
||
onMounted(() => { | ||
widget.value?.setManipulator(manipulator); | ||
}); | ||
|
||
watchEffect(() => { | ||
updatePlaneManipulatorFor2DView( | ||
manipulator, | ||
viewDirection.value, | ||
currentSlice.value, | ||
currentImageMetadata.value | ||
); | ||
}); | ||
|
||
// --- visibility --- // | ||
|
||
onMounted(() => { | ||
if (!widget.value) { | ||
return; | ||
} | ||
// hide handle visibility, but not picking visibility | ||
widget.value.setHandleVisibility(false); | ||
widgetManager.value.renderWidgets(); | ||
}); | ||
|
||
// --- // | ||
|
||
return () => meta.render?.(viewId.value, syncedState, labelProps.value); | ||
}, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed because PlacingWidget does not pull from a store and we want a normalized representation of state for Placing and NonPlacing components? We do pass the
tool
to store backed NonPlacingComponents, so passing duplicate info to NonPlacing components. Maybe toss ID into SyncedState, and only NonPlacingCompoents get access, then can droptool
prop.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are certainly passing some duplicate info, but the synced state captures some ephemeral widget state that the store might not capture. For example, the polygon tool currently stores a
movePoint
. I consider that to be ephemeral state that doesn't need to be stored, only propagated from the widget itself.