Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/components/PaintControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@
</v-item-group>
</v-row>
<template v-if="mode === PaintMode.CirclePaint || mode === PaintMode.Erase">
<v-row no-gutters align="center" class="mb-2">
<span class="mr-2">Sync Views</span>
<v-switch
v-model="crossPlaneSync"
color="primary"
density="compact"
hide-details
class="ml-3"
></v-switch>
</v-row>
<v-row no-gutters>Size (pixels)</v-row>
<v-row no-gutters align="center">
<v-slider
Expand Down Expand Up @@ -135,7 +145,8 @@ import { useImageStatsStore } from '@/src/store/image-stats';

const paintStore = usePaintToolStore();
const imageStatsStore = useImageStatsStore();
const { brushSize, activeMode, thresholdRange } = storeToRefs(paintStore);
const { brushSize, activeMode, thresholdRange, crossPlaneSync } =
storeToRefs(paintStore);
const { currentImageID } = useCurrentImage();

const currentImageStats = computed(() => {
Expand Down
8 changes: 6 additions & 2 deletions src/components/tools/paint/PaintWidget2D.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,17 @@ export default defineComponent({

onVTKEvent(widget, 'onStartInteractionEvent', () => {
// StartInteraction cannot occur if origin is null.
const indexPoint = worldPointToIndex(widgetState.getBrush().getOrigin()!);
const origin = widgetState.getBrush().getOrigin()!;
const indexPoint = worldPointToIndex(origin);
paintStore.startStroke(indexPoint, viewAxisIndex.value);
paintStore.updatePaintPosition(origin, viewId.value);
});

onVTKEvent(widget, 'onInteractionEvent', () => {
const indexPoint = worldPointToIndex(widgetState.getBrush().getOrigin()!);
const origin = widgetState.getBrush().getOrigin()!;
const indexPoint = worldPointToIndex(origin);
paintStore.placeStrokePoint(indexPoint, viewAxisIndex.value);
paintStore.updatePaintPosition(origin, viewId.value);
});

onVTKEvent(widget, 'onEndInteractionEvent', () => {
Expand Down
1 change: 1 addition & 0 deletions src/io/state-file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export async function serialize() {
activeSegmentGroupID: null,
activeSegment: null,
brushSize: 8,
crossPlaneSync: false,
},
crop: {},
current: Tools.WindowLevel,
Expand Down
1 change: 1 addition & 0 deletions src/io/state-file/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ const Paint = z.object({
activeSegmentGroupID: z.string().nullable(),
activeSegment: z.number().nullish(),
brushSize: z.number(),
crossPlaneSync: z.boolean().default(false),
labelmapOpacity: z.number().optional(), // labelmapOpacity now ignored. Opacity per segment group via layerColoring store.
});

Expand Down
75 changes: 72 additions & 3 deletions src/store/tools/paint.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import type { Vector2 } from '@kitware/vtk.js/types';
import type { Vector2, Vector3 } from '@kitware/vtk.js/types';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import type { Manifest, StateFile } from '@/src/io/state-file/schema';
import type { Maybe } from '@/src/types';
import { useImageStatsStore } from '@/src/store/image-stats';
import { computed, ref } from 'vue';
import { computed, ref, unref, watch } from 'vue';
import { watchImmediate } from '@vueuse/core';
import { vec3 } from 'gl-matrix';
import { defineStore } from 'pinia';
import { PaintMode } from '@/src/core/tools/paint';
import { getLPSAxisFromDir } from '@/src/utils/lps';
import { Tools } from './types';
import { useSegmentGroupStore } from '../segmentGroups';
import useViewSliceStore from '../view-configs/slicing';
import { useViewStore } from '../views';
import { useViewCameraStore } from '../view-configs/camera';

const DEFAULT_BRUSH_SIZE = 4;
const DEFAULT_THRESHOLD_RANGE: Vector2 = [
Expand All @@ -27,9 +31,16 @@ export const usePaintToolStore = defineStore('paint', () => {
const strokePoints = ref<vec3[]>([]);
const isActive = ref(false);
const thresholdRange = ref<Vector2>([...DEFAULT_THRESHOLD_RANGE]);
const crossPlaneSync = ref(false);
const paintPosition = ref<Vector3>([0, 0, 0]);
const activePaintViewID = ref<Maybe<string>>(null);

const { currentImageID, currentImageData } = useCurrentImage();
const { currentImageID, currentImageData, currentImageMetadata } =
useCurrentImage();
const imageStatsStore = useImageStatsStore();
const viewSliceStore = useViewSliceStore();
const viewStore = useViewStore();
const viewCameraStore = useViewCameraStore();

function getWidgetFactory(this: _This) {
return this.$paint.factory;
Expand All @@ -42,6 +53,16 @@ export const usePaintToolStore = defineStore('paint', () => {
return segmentGroupStore.dataIndex[activeSegmentGroupID.value] ?? null;
});

const currentViewIDs = computed(() => {
const imageID = unref(currentImageID);
if (imageID) {
return viewStore.viewIDs.filter(
(viewID) => !!viewSliceStore.getConfig(viewID, imageID)
);
}
return [];
});

// --- actions --- //

/**
Expand Down Expand Up @@ -284,12 +305,56 @@ export const usePaintToolStore = defineStore('paint', () => {
thresholdRange.value = range;
}

function setCrossPlaneSync(enabled: boolean) {
crossPlaneSync.value = enabled;
}

watch(paintPosition, (worldPosition) => {
if (!crossPlaneSync.value || !isActive.value) return;

const imageID = unref(currentImageID);
const metadata = unref(currentImageMetadata);
if (!imageID || !metadata?.lpsOrientation || !metadata?.worldToIndex)
return;

const { lpsOrientation, worldToIndex } = metadata;
const indexPos = vec3.create();
vec3.transformMat4(indexPos, worldPosition, worldToIndex);

currentViewIDs.value.forEach((viewID) => {
const sliceConfig = viewSliceStore.getConfig(viewID, imageID);
if (!sliceConfig) return;

// Update slice position
const axis = getLPSAxisFromDir(sliceConfig.axisDirection);
const index = lpsOrientation[axis];
const slice = Math.round(indexPos[index]);
if (slice !== sliceConfig.slice) {
viewSliceStore.updateConfig(viewID, imageID, { slice });
}

// Center camera on paint position (skip active view)
if (activePaintViewID.value && viewID === activePaintViewID.value) {
return;
}
viewCameraStore.updateConfig(viewID, imageID, {
focalPoint: worldPosition,
});
});
});

function updatePaintPosition(worldPosition: Vector3, activeViewID?: string) {
paintPosition.value = worldPosition;
activePaintViewID.value = activeViewID;
}

function serialize(state: StateFile) {
const { paint } = state.manifest.tools;

paint.activeSegmentGroupID = activeSegmentGroupID.value ?? null;
paint.brushSize = brushSize.value;
paint.activeSegment = activeSegment.value;
paint.crossPlaneSync = crossPlaneSync.value;
}

function deserialize(
Expand All @@ -307,6 +372,7 @@ export const usePaintToolStore = defineStore('paint', () => {
setActiveSegmentGroup(activeSegmentGroupID.value);
setActiveSegment.call(this, paint.activeSegment);
}
setCrossPlaneSync(paint.crossPlaneSync ?? false);
}

return {
Expand All @@ -317,6 +383,7 @@ export const usePaintToolStore = defineStore('paint', () => {
strokePoints,
isActive,
thresholdRange,
crossPlaneSync,

getWidgetFactory,

Expand All @@ -329,6 +396,8 @@ export const usePaintToolStore = defineStore('paint', () => {
setBrushSize,
setSliceAxis,
setThresholdRange,
setCrossPlaneSync,
updatePaintPosition,
startStroke,
placeStrokePoint,
endStroke,
Expand Down
Loading