From 74f8f3e8ba84944da4be56552405987d3c79c105 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 6 Oct 2025 21:11:57 +0200 Subject: [PATCH 01/37] wip --- .../combinations/volume_handlers.ts | 43 ++++++++++++++----- .../model/accessors/disabled_tool_accessor.ts | 14 +++--- .../model/reducers/skeletontracing_reducer.ts | 9 ++-- .../viewer/model/sagas/volume/helpers.ts | 2 + .../viewer/model/sagas/volumetracing_saga.tsx | 8 +++- .../viewer/model/volumetracing/volumelayer.ts | 19 ++------ 6 files changed, 58 insertions(+), 37 deletions(-) diff --git a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts index e0f75fefc41..3c7634990c7 100644 --- a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts @@ -3,8 +3,8 @@ import memoizeOne from "memoize-one"; import type { AdditionalCoordinate } from "types/api_types"; import type { OrthoView, Point2, Vector3 } from "viewer/constants"; import { ContourModeEnum } from "viewer/constants"; -import { getVisibleSegmentationLayer } from "viewer/model/accessors/dataset_accessor"; -import { globalToLayerTransformedPosition } from "viewer/model/accessors/dataset_layer_transformation_accessor"; +import { getLayerByName, getVisibleSegmentationLayer } from "viewer/model/accessors/dataset_accessor"; +import { getTransformsForLayer, globalToLayerTransformedPosition } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { calculateGlobalPos } from "viewer/model/accessors/view_mode_accessor"; import { updateUserSettingAction } from "viewer/model/actions/settings_actions"; import { @@ -17,34 +17,57 @@ import { startEditingAction, updateSegmentAction, } from "viewer/model/actions/volumetracing_actions"; +import { invertTransform, transformPointUnscaled } from "viewer/model/helpers/transformation_helpers"; import { Model, Store, api } from "viewer/singletons"; +import type { WebknossosState } from "viewer/store"; export function handleDrawStart(pos: Point2, plane: OrthoView) { const state = Store.getState(); const globalPosRounded = calculateGlobalPos(state, pos).rounded; + const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); + Store.dispatch(setContourTracingModeAction(ContourModeEnum.DRAW)); - Store.dispatch(startEditingAction(globalPosRounded, plane)); - Store.dispatch(addToLayerAction(globalPosRounded)); + Store.dispatch(startEditingAction(untransformedPos, plane)); + Store.dispatch(addToLayerAction(untransformedPos)); } + +function getUntransformedSegmentationPosition(state: WebknossosState, globalPosRounded: Vector3) { + const { nativelyRenderedLayerName } = state.datasetConfiguration; + const maybeLayer = Model.getVisibleSegmentationLayer(); + if (maybeLayer == null) { throw new Error("Segmentation layer does not exist"); } + + const layer = getLayerByName(state.dataset, maybeLayer.name); + const segmentationTransforms = getTransformsForLayer(state.dataset, layer, nativelyRenderedLayerName); + const untransformedPos = transformPointUnscaled(invertTransform(segmentationTransforms))(globalPosRounded); + return untransformedPos; +} + export function handleEraseStart(pos: Point2, plane: OrthoView) { + const state = Store.getState(); + const globalPosRounded = calculateGlobalPos(state, pos).rounded; + const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); + Store.dispatch(setContourTracingModeAction(ContourModeEnum.DELETE)); - Store.dispatch(startEditingAction(calculateGlobalPos(Store.getState(), pos).rounded, plane)); + Store.dispatch(startEditingAction(untransformedPos, plane)); } export function handleMoveForDrawOrErase(pos: Point2) { const state = Store.getState(); - Store.dispatch(addToLayerAction(calculateGlobalPos(state, pos).rounded)); + const globalPosRounded = calculateGlobalPos(state, pos).rounded; + const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); + Store.dispatch(addToLayerAction(untransformedPos)); } export function handleEndForDrawOrErase() { Store.dispatch(finishEditingAction()); Store.dispatch(resetContourAction()); } export function handlePickCell(pos: Point2) { - const storeState = Store.getState(); - const globalPosRounded = calculateGlobalPos(storeState, pos).rounded; + const state = Store.getState(); + const globalPosRounded = calculateGlobalPos(state, pos).rounded; + const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); return handlePickCellFromGlobalPosition( - globalPosRounded, - storeState.flycam.additionalCoordinates || [], + untransformedPos, + state.flycam.additionalCoordinates || [], ); } diff --git a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts index 23b8876e906..1dedcccabbe 100644 --- a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts @@ -333,11 +333,12 @@ function getDisabledVolumeInfo(state: WebknossosState) { const labeledMag = getRenderableMagForSegmentationTracing(state, segmentationTracingLayer)?.mag; const isSegmentationTracingVisibleForMag = labeledMag != null; const visibleSegmentationLayer = getVisibleSegmentationLayer(state); - const isSegmentationTracingTransformed = - segmentationTracingLayer != null && - getTransformsPerLayer(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName)[ - segmentationTracingLayer.tracingId - ] !== IdentityTransform; + const isSegmentationTracingTransformed = false; + // todop: set to true if not a 90 deg rotation + // segmentationTracingLayer != null && + // getTransformsPerLayer(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName)[ + // segmentationTracingLayer.tracingId + // ] !== IdentityTransform; const isSegmentationTracingVisible = segmentationTracingLayer != null && visibleSegmentationLayer != null && @@ -397,7 +398,8 @@ const _getDisabledInfoForTools = ( const { annotation } = state; const hasSkeleton = annotation.skeleton != null; const isFlycamRotated = isRotated(state.flycam); - const geometriesTransformed = areGeometriesTransformed(state); + // todop: check for 90 deg rotations + const geometriesTransformed = false; // areGeometriesTransformed(state); const areaMeasurementToolInfo = getAreaMeasurementToolInfo(isFlycamRotated); const skeletonToolInfo = getSkeletonToolInfo( hasSkeleton, diff --git a/frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts b/frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts index 87e62abf978..03a55155f36 100644 --- a/frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts @@ -686,10 +686,11 @@ function SkeletonTracingReducer( switch (action.type) { case "CREATE_NODE": { - if (areGeometriesTransformed(state)) { - // Don't create nodes if the skeleton layer is rendered with transforms. - return state; - } + // todop + // if (areGeometriesTransformed(state)) { + // // Don't create nodes if the skeleton layer is rendered with transforms. + // return state; + // } const { position, rotation, viewport, mag, treeId, timestamp, additionalCoordinates } = action; diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index e57f256860a..87688d23d13 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -201,6 +201,8 @@ export function* labelWithVoxelBuffer2D( min: topLeft3DCoord, max: bottomRight3DCoord, }); + console.log("topLeft3DCoord", topLeft3DCoord) + console.log("bottomRight3DCoord", bottomRight3DCoord) for (const boundingBoxChunk of outerBoundingBox.chunkIntoBuckets()) { const { min, max } = boundingBoxChunk; const bucketZoomedAddress = zoomedPositionToZoomedAddress( diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index 9640de10137..655b5808ca6 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -89,6 +89,7 @@ import { ensureWkInitialized } from "./ready_sagas"; import { floodFill } from "./volume/floodfill_saga"; import { type BooleanBox, createVolumeLayer, labelWithVoxelBuffer2D } from "./volume/helpers"; import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_saga"; +import Dimensions from "../dimensions"; const OVERWRITE_EMPTY_WARNING_KEY = "OVERWRITE-EMPTY-WARNING"; @@ -235,11 +236,14 @@ export function* editVolumeLayerAsync(): Saga { ), ); const { zoomStep: labeledZoomStep, mag: labeledMag } = maybeLabeledMagWithZoomStep; + + const w = Dimensions.thirdDimensionForPlane(startEditingAction.planeId) const currentLayer = yield* call( createVolumeLayer, volumeTracing, - startEditingAction.planeId, + window.hardPlaneId ?? startEditingAction.planeId, labeledMag, + startEditingAction.position[w] ); const initialViewport = yield* select((state) => state.viewModeData.plane.activeViewport); @@ -286,7 +290,7 @@ export function* editVolumeLayerAsync(): Saga { if (isTraceTool(activeTool) || (isBrushTool(activeTool) && isDrawing)) { // Close the polygon. When brushing, this causes an auto-fill which is why // it's only performed when drawing (not when erasing). - currentLayer.addContour(addToLayerAction.position); + currentLayer.updateArea(addToLayerAction.position); } if (isBrushTool(activeTool)) { diff --git a/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts b/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts index fc550300a05..b151f0a167e 100644 --- a/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts +++ b/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts @@ -174,10 +174,6 @@ class VolumeLayer { this.thirdDimensionValue = Math.floor(thirdDimensionValue / this.activeMag[thirdDim]); } - addContour(globalPos: Vector3): void { - this.updateArea(globalPos); - } - updateArea(globalPos: Vector3): void { const pos = scaleGlobalPositionWithMagnification(globalPos, this.activeMag); let [maxCoord, minCoord] = [this.maxCoord, this.minCoord]; @@ -342,7 +338,10 @@ class VolumeLayer { } createVoxelBuffer2D(minCoord2d: Vector2, width: number, height: number, fillValue: number = 0) { - const map = this._createMap(width, height, fillValue); + const map = new Uint8Array(width * height); + if (fillValue !== 0) { + map.fill(fillValue); + } return new VoxelBuffer2D( map, @@ -354,16 +353,6 @@ class VolumeLayer { ); } - _createMap(width: number, height: number, fillValue: number = 0): Uint8Array { - const map = new Uint8Array(width * height); - - if (fillValue !== 0) { - map.fill(fillValue); - } - - return map; - } - getRectangleBetweenCircles( centre1: Vector2, centre2: Vector2, From 5ee1c8f165827ed594e56ac8e4f105e64160e1c9 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 27 Oct 2025 14:58:35 +0100 Subject: [PATCH 02/37] clean up --- frontend/javascripts/libs/drawing.ts | 65 +++---------------- frontend/javascripts/viewer/constants.ts | 10 +-- .../model/accessors/dataset_accessor.ts | 4 +- .../viewer/model/volumetracing/volumelayer.ts | 28 ++++---- 4 files changed, 29 insertions(+), 78 deletions(-) diff --git a/frontend/javascripts/libs/drawing.ts b/frontend/javascripts/libs/drawing.ts index 05358d87f70..f58bd60254a 100644 --- a/frontend/javascripts/libs/drawing.ts +++ b/frontend/javascripts/libs/drawing.ts @@ -1,17 +1,10 @@ -import type { Vector3 } from "viewer/constants"; type RangeItem = [number, number, number, boolean | null, boolean, boolean]; + // This is a class with static methods and constants dealing with drawing -// lines and filling polygons -// Macros -// Constants -const SMOOTH_LENGTH = 4; -const SMOOTH_ALPHA = 0.2; +// lines and filling polygons. class Drawing { - alpha: number = SMOOTH_ALPHA; - smoothLength: number = SMOOTH_LENGTH; - - drawHorizontalLine2d( + private drawHorizontalLine2d( y: number, x1: number, x2: number, @@ -91,7 +84,7 @@ class Drawing { } } - addNextLine( + private addNextLine( newY: number, isNext: boolean, downwards: boolean, @@ -135,7 +128,7 @@ class Drawing { } } - paintBorder( + private paintBorder( x1: number, y1: number, x2: number, @@ -212,19 +205,15 @@ class Drawing { while (ranges.length) { const r = ranges.pop(); - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. + if (r == null) { + throw new Error("Array is exptected to be not empty."); + } let minX = r[0]; - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. let maxX = r[1]; - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. y = r[2]; - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. const down = r[3] === true; - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. const up = r[3] === false; - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. const extendLeft = r[4]; - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. const extendRight = r[5]; if (extendLeft) { @@ -250,19 +239,15 @@ class Drawing { maxX++; } } else { - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. r[0]--; - // @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'. r[1]++; } if (y < height) { - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'RangeItem | undefined' is not as... Remove this comment to see the full error message this.addNextLine(y + 1, !up, true, minX, maxX, r, ranges, test, paint); } if (y > 0) { - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'RangeItem | undefined' is not as... Remove this comment to see the full error message this.addNextLine(y - 1, !down, false, minX, maxX, r, ranges, test, paint); } } @@ -288,40 +273,6 @@ class Drawing { } } } - - // Source : http://twistedoakstudios.com/blog/Post3138_mouse-path-smoothing-for-jack-lumber - smoothLine(points: Array, callback: (arg0: Vector3) => void): Array { - const smoothLength = this.smoothLength || SMOOTH_LENGTH; - const a = this.alpha || SMOOTH_ALPHA; - - if (points.length > 2 + smoothLength) { - for (let i = 0; i < smoothLength; i++) { - const j = points.length - i - 2; - const p0 = points[j]; - const p1 = points[j + 1]; - const p = [0, 0, 0]; - - for (let k = 0; k < 3; k++) { - p[k] = p0[k] * (1 - a) + p1[k] * a; - } - - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'number[]' is not assignable to p... Remove this comment to see the full error message - callback(p); - // @ts-expect-error ts-migrate(2322) FIXME: Type 'number[]' is not assignable to type 'Vector3... Remove this comment to see the full error message - points[j] = p; - } - } - - return points; - } - - setSmoothLength(v: number): void { - this.smoothLength = v; - } - - setAlpha(v: number): void { - this.alpha = v; - } } export default new Drawing(); diff --git a/frontend/javascripts/viewer/constants.ts b/frontend/javascripts/viewer/constants.ts index f47dd8316da..b0fff22c2d1 100644 --- a/frontend/javascripts/viewer/constants.ts +++ b/frontend/javascripts/viewer/constants.ts @@ -46,11 +46,11 @@ export type Rect = { height: number; }; export const AnnotationContentTypes = ["skeleton", "volume", "hybrid"]; -export const Vector2Indicies = [0, 1] as const; -export const Vector3Indicies = [0, 1, 2] as const; -export const Vector4Indicies = [0, 1, 2, 3] as const; -export const Vector5Indicies = [0, 1, 2, 3, 4] as const; -export const Vector6Indicies = [0, 1, 2, 3, 4, 5] as const; +export const Vector2Indices = [0, 1] as const; +export const Vector3Indices = [0, 1, 2] as const; +export const Vector4Indices = [0, 1, 2, 3] as const; +export const Vector5Indices = [0, 1, 2, 3, 4] as const; +export const Vector6Indices = [0, 1, 2, 3, 4, 5] as const; export enum OrthoViews { PLANE_XY = "PLANE_XY", PLANE_YZ = "PLANE_YZ", diff --git a/frontend/javascripts/viewer/model/accessors/dataset_accessor.ts b/frontend/javascripts/viewer/model/accessors/dataset_accessor.ts index 8469e526f3c..52d6d36eca7 100644 --- a/frontend/javascripts/viewer/model/accessors/dataset_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/dataset_accessor.ts @@ -17,7 +17,7 @@ import type { } from "types/api_types"; import type { DataLayer } from "types/schemas/datasource.types"; import { LongUnitToShortUnitMap, type Vector3, type ViewMode } from "viewer/constants"; -import constants, { ViewModeValues, Vector3Indicies, MappingStatusEnum } from "viewer/constants"; +import constants, { ViewModeValues, Vector3Indices, MappingStatusEnum } from "viewer/constants"; import type { ActiveMappingInfo, BoundingBoxObject, @@ -244,7 +244,7 @@ export function getDatasetBoundingBox(dataset: APIDataset): BoundingBox { for (const dataLayer of layers) { const layerBox = getLayerBoundingBox(dataset, dataLayer.name); - for (const i of Vector3Indicies) { + for (const i of Vector3Indices) { min[i] = Math.min(min[i], layerBox.min[i]); max[i] = Math.max(max[i], layerBox.max[i]); } diff --git a/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts b/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts index b151f0a167e..0076995e33d 100644 --- a/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts +++ b/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts @@ -4,7 +4,7 @@ import Toast from "libs/toast"; import _ from "lodash"; import messages from "messages"; import type { OrthoView, Vector2, Vector3 } from "viewer/constants"; -import Constants, { OrthoViews, Vector3Indicies, Vector2Indicies } from "viewer/constants"; +import Constants, { OrthoViews, Vector3Indices, Vector2Indices } from "viewer/constants"; import type { AnnotationTool } from "viewer/model/accessors/tool_accessor"; import { isBrushTool } from "viewer/model/accessors/tool_accessor"; import { getVolumeTracingById } from "viewer/model/accessors/volumetracing_accessor"; @@ -25,12 +25,12 @@ import Store from "viewer/store"; should be applied. */ export class VoxelBuffer2D { - map: Uint8Array; - width: number; - height: number; - minCoord2d: Vector2; - get3DCoordinate: (arg0: Vector2) => Vector3; - getFast3DCoordinate: (arg0: number, arg1: number, arg2: Vector3 | Float32Array) => void; + readonly map: Uint8Array; + readonly width: number; + readonly height: number; + readonly minCoord2d: Vector2; + readonly get3DCoordinate: (arg0: Vector2) => Vector3; + readonly getFast3DCoordinate: (arg0: number, arg1: number, arg2: Vector3 | Float32Array) => void; static empty(): VoxelBuffer2D { return new VoxelBuffer2D( @@ -149,15 +149,15 @@ class VolumeLayer { Therefore, members of this class are in the mag space of `activeMag`. */ - volumeTracingId: string; - plane: OrthoView; - thirdDimensionValue: number; + readonly volumeTracingId: string; + readonly plane: OrthoView; + readonly thirdDimensionValue: number; // Stored in global (but mag-dependent) coordinates: minCoord: Vector3 | null | undefined; maxCoord: Vector3 | null | undefined; - activeMag: Vector3; + readonly activeMag: Vector3; constructor( volumeTracingId: string, @@ -183,7 +183,7 @@ class VolumeLayer { minCoord = _.clone(pos); } - for (const i of Vector3Indicies) { + for (const i of Vector3Indices) { minCoord[i] = Math.min(minCoord[i], Math.floor(pos[i]) - 2); maxCoord[i] = Math.max(maxCoord[i], Math.ceil(pos[i]) + 2); } @@ -320,7 +320,7 @@ class VolumeLayer { vector2Norm(vector: Vector2): number { let norm = 0; - for (const i of Vector2Indicies) { + for (const i of Vector2Indices) { norm += Math.pow(vector[i], 2); } @@ -330,7 +330,7 @@ class VolumeLayer { vector2DistanceWithScale(pos1: Vector2, pos2: Vector2, scale: Vector2): number { let distance = 0; - for (const i of Vector2Indicies) { + for (const i of Vector2Indices) { distance += Math.pow((pos2[i] - pos1[i]) / scale[i], 2); } From 52da095d8ad2fe12e9e62a0b52e1d97d375a8601 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 27 Oct 2025 15:06:00 +0100 Subject: [PATCH 03/37] rename volume layer to section labeler --- .../volumetracing/volumetracing_saga.spec.ts | 28 +++++++++---------- .../viewer/model/sagas/volume/helpers.ts | 8 +++--- .../quick_select_heuristic_saga.ts | 14 +++++++--- .../sagas/volume/volume_interpolation_saga.ts | 10 +++---- .../viewer/model/sagas/volumetracing_saga.tsx | 28 +++++++++---------- .../viewer/model/volumetracing/volumelayer.ts | 4 +-- 6 files changed, 49 insertions(+), 43 deletions(-) diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts index a85a713940e..da2d279e623 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts @@ -12,12 +12,12 @@ import * as VolumeTracingActions from "viewer/model/actions/volumetracing_action import { expectValueDeepEqual, execCall } from "test/helpers/sagaHelpers"; import type { ActiveMappingInfo } from "viewer/store"; import { askUserForLockingActiveMapping } from "viewer/model/sagas/saga_helpers"; -import { editVolumeLayerAsync, finishLayer } from "viewer/model/sagas/volumetracing_saga"; +import { editVolumeLayerAsync, finishSectionLabeler } from "viewer/model/sagas/volumetracing_saga"; import { requestBucketModificationInVolumeTracing, ensureMaybeActiveMappingIsLocked, } from "viewer/model/sagas/saga_helpers"; -import VolumeLayer from "viewer/model/volumetracing/volumelayer"; +import SectionLabeler from "viewer/model/volumetracing/volumelayer"; import { serverVolumeToClientVolumeTracing } from "viewer/model/reducers/volumetracing_reducer"; import { Model, Store } from "viewer/singletons"; import { hasRootSagaCrashed } from "viewer/model/sagas/root_saga"; @@ -158,13 +158,13 @@ describe("VolumeTracingSaga", () => { ); saga.next(); // advance from the put action - const volumeLayer = new VolumeLayer( + const sectionLabeler = new SectionLabeler( volumeTracing.tracingId, OrthoViews.PLANE_XY, 10, [1, 1, 1], ); - saga.next(volumeLayer); + saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); saga.next("action_channel"); saga.next(addToLayerActionFn([1, 2, 3])); @@ -173,8 +173,8 @@ describe("VolumeTracingSaga", () => { saga.next(OrthoViews.PLANE_XY); saga.next(addToLayerActionFn([3, 4, 5])); saga.next(OrthoViews.PLANE_XY); - expect(volumeLayer.minCoord).toEqual([-1, 0, 1]); - expect(volumeLayer.maxCoord).toEqual([5, 6, 7]); + expect(sectionLabeler.minCoord).toEqual([-1, 0, 1]); + expect(sectionLabeler.maxCoord).toEqual([5, 6, 7]); }); it("should finish a volume layer (saga test)", () => { @@ -213,13 +213,13 @@ describe("VolumeTracingSaga", () => { ); saga.next(); // advance from the put action - const volumeLayer = new VolumeLayer( + const sectionLabeler = new SectionLabeler( volumeTracing.tracingId, OrthoViews.PLANE_XY, 10, [1, 1, 1], ); - saga.next(volumeLayer); + saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); saga.next("action_channel"); saga.next(addToLayerActionFn([1, 2, 3])); @@ -232,8 +232,8 @@ describe("VolumeTracingSaga", () => { expect, saga.next(finishEditingAction), call( - finishLayer, - volumeLayer, + finishSectionLabeler, + sectionLabeler, AnnotationTool.TRACE, ContourModeEnum.DRAW, OverwriteModeEnum.OVERWRITE_ALL, @@ -282,13 +282,13 @@ describe("VolumeTracingSaga", () => { ); saga.next(); // advance from the put action - const volumeLayer = new VolumeLayer( + const sectionLabeler = new SectionLabeler( volumeTracing.tracingId, OrthoViews.PLANE_XY, 10, [1, 1, 1], ); - saga.next(volumeLayer); + saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); saga.next("action_channel"); saga.next(addToLayerActionFn([1, 2, 3])); @@ -301,8 +301,8 @@ describe("VolumeTracingSaga", () => { expect, saga.next(finishEditingAction), call( - finishLayer, - volumeLayer, + finishSectionLabeler, + sectionLabeler, AnnotationTool.TRACE, ContourModeEnum.DELETE, OverwriteModeEnum.OVERWRITE_ALL, diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index 87688d23d13..b48628cd3c6 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -24,7 +24,7 @@ import { getHalfViewportExtentsInVx } from "viewer/model/sagas/saga_selectors"; import sampleVoxelMapToMagnification, { applyVoxelMap, } from "viewer/model/volumetracing/volume_annotation_sampling"; -import VolumeLayer, { type VoxelBuffer2D } from "viewer/model/volumetracing/volumelayer"; +import SectionLabeler, { type VoxelBuffer2D } from "viewer/model/volumetracing/volumelayer"; import { Model } from "viewer/singletons"; import type { BoundingBoxObject, VolumeTracing } from "viewer/store"; @@ -283,13 +283,13 @@ export function* labelWithVoxelBuffer2D( } } -export function* createVolumeLayer( +export function* createSectionLabeler( volumeTracing: VolumeTracing, planeId: OrthoView, labeledMags: Vector3, thirdDimValue?: number, -): Saga { +): Saga { const position = yield* select((state) => getFlooredPosition(state.flycam)); thirdDimValue = thirdDimValue ?? position[Dimensions.thirdDimensionForPlane(planeId)]; - return new VolumeLayer(volumeTracing.tracingId, planeId, thirdDimValue, labeledMags); + return new SectionLabeler(volumeTracing.tracingId, planeId, thirdDimValue, labeledMags); } diff --git a/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts b/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts index ad6095d7b64..397d2ddc6e6 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts @@ -60,7 +60,7 @@ import type { VolumeTracing, WebknossosState, } from "viewer/store"; -import { createVolumeLayer, labelWithVoxelBuffer2D } from "../helpers"; +import { createSectionLabeler, labelWithVoxelBuffer2D } from "../helpers"; import { copyNdArray } from "../volume_interpolation_saga"; const TOAST_KEY = "QUICKSELECT_PREVIEW_MESSAGE"; @@ -512,10 +512,16 @@ export function* finalizeQuickSelectForSlice( skipFinishAnnotationStroke: boolean = false, ) { quickSelectGeometry.setCoordinates([0, 0, 0], [0, 0, 0]); - const volumeLayer = yield* call(createVolumeLayer, volumeTracing, activeViewport, labeledMag, w); + const sectionLabeler = yield* call( + createSectionLabeler, + volumeTracing, + activeViewport, + labeledMag, + w, + ); const sizeUVWInMag = mask.shape; - const voxelBuffer2D = volumeLayer.createVoxelBuffer2D( - V2.floor(volumeLayer.globalCoordToMag2DFloat(boundingBoxMag1.min)), + const voxelBuffer2D = sectionLabeler.createVoxelBuffer2D( + V2.floor(sectionLabeler.globalCoordToMag2DFloat(boundingBoxMag1.min)), sizeUVWInMag[0], sizeUVWInMag[1], ); diff --git a/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts b/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts index 596c257c5fa..4d6a79bda4c 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts @@ -37,7 +37,7 @@ import type { VoxelBuffer2D } from "viewer/model/volumetracing/volumelayer"; import { Model, api } from "viewer/singletons"; import type { WebknossosState } from "viewer/store"; import { requestBucketModificationInVolumeTracing } from "../saga_helpers"; -import { createVolumeLayer, getBoundingBoxForViewport, labelWithVoxelBuffer2D } from "./helpers"; +import { createSectionLabeler, getBoundingBoxForViewport, labelWithVoxelBuffer2D } from "./helpers"; /* * This saga is capable of doing segment interpolation between two slices. @@ -367,16 +367,16 @@ export default function* maybeInterpolateSegmentationLayer(): Saga { targetOffsetW < adaptedInterpolationRange[1]; targetOffsetW++ ) { - const interpolationLayer = yield* call( - createVolumeLayer, + const sectionLabeler = yield* call( + createSectionLabeler, volumeTracing, activeViewport, labeledMag, relevantBoxMag1.min[thirdDim] + labeledMag[thirdDim] * targetOffsetW, ); - interpolationVoxelBuffers[targetOffsetW] = interpolationLayer.createVoxelBuffer2D( + interpolationVoxelBuffers[targetOffsetW] = sectionLabeler.createVoxelBuffer2D( V2.floor( - interpolationLayer.globalCoordToMag2DFloat( + sectionLabeler.globalCoordToMag2DFloat( V3.add(relevantBoxMag1.min, transpose([0, 0, targetOffsetW])), ), ), diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index 655b5808ca6..257b0d560a6 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -80,14 +80,14 @@ import { updateSegmentVisibilityVolumeAction, updateSegmentVolumeAction, } from "viewer/model/sagas/volume/update_actions"; -import type VolumeLayer from "viewer/model/volumetracing/volumelayer"; +import type SectionLabeler from "viewer/model/volumetracing/volumelayer"; import { Model, api } from "viewer/singletons"; import type { SegmentMap, VolumeTracing } from "viewer/store"; import { pushSaveQueueTransaction } from "../actions/save_actions"; import { diffBoundingBoxes, diffGroups } from "../helpers/diff_helpers"; import { ensureWkInitialized } from "./ready_sagas"; import { floodFill } from "./volume/floodfill_saga"; -import { type BooleanBox, createVolumeLayer, labelWithVoxelBuffer2D } from "./volume/helpers"; +import { type BooleanBox, createSectionLabeler, labelWithVoxelBuffer2D } from "./volume/helpers"; import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_saga"; import Dimensions from "../dimensions"; @@ -237,20 +237,20 @@ export function* editVolumeLayerAsync(): Saga { ); const { zoomStep: labeledZoomStep, mag: labeledMag } = maybeLabeledMagWithZoomStep; - const w = Dimensions.thirdDimensionForPlane(startEditingAction.planeId) - const currentLayer = yield* call( - createVolumeLayer, + const w = Dimensions.thirdDimensionForPlane(startEditingAction.planeId); + const currentSectionLabeler = yield* call( + createSectionLabeler, volumeTracing, window.hardPlaneId ?? startEditingAction.planeId, labeledMag, - startEditingAction.position[w] + startEditingAction.position[w], ); const initialViewport = yield* select((state) => state.viewModeData.plane.activeViewport); if (isBrushTool(activeTool)) { yield* call( labelWithVoxelBuffer2D, - currentLayer.getCircleVoxelBuffer2D(startEditingAction.position), + currentSectionLabeler.getCircleVoxelBuffer2D(startEditingAction.position), contourTracingMode, overwriteMode, labeledZoomStep, @@ -290,11 +290,11 @@ export function* editVolumeLayerAsync(): Saga { if (isTraceTool(activeTool) || (isBrushTool(activeTool) && isDrawing)) { // Close the polygon. When brushing, this causes an auto-fill which is why // it's only performed when drawing (not when erasing). - currentLayer.updateArea(addToLayerAction.position); + currentSectionLabeler.updateArea(addToLayerAction.position); } if (isBrushTool(activeTool)) { - const rectangleVoxelBuffer2D = currentLayer.getRectangleVoxelBuffer2D( + const rectangleVoxelBuffer2D = currentSectionLabeler.getRectangleVoxelBuffer2D( lastPosition, addToLayerAction.position, ); @@ -313,7 +313,7 @@ export function* editVolumeLayerAsync(): Saga { yield* call( labelWithVoxelBuffer2D, - currentLayer.getCircleVoxelBuffer2D(addToLayerAction.position), + currentSectionLabeler.getCircleVoxelBuffer2D(addToLayerAction.position), contourTracingMode, overwriteMode, labeledZoomStep, @@ -326,8 +326,8 @@ export function* editVolumeLayerAsync(): Saga { } yield* call( - finishLayer, - currentLayer, + finishSectionLabeler, + currentSectionLabeler, activeTool, contourTracingMode, overwriteMode, @@ -363,8 +363,8 @@ export function* editVolumeLayerAsync(): Saga { } } -export function* finishLayer( - layer: VolumeLayer, +export function* finishSectionLabeler( + layer: SectionLabeler, activeTool: AnnotationTool, contourTracingMode: ContourMode, overwriteMode: OverwriteMode, diff --git a/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts b/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts index 0076995e33d..6627eb7cbc7 100644 --- a/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts +++ b/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts @@ -142,7 +142,7 @@ export class VoxelNeighborQueue2D extends VoxelNeighborQueue3D { } } -class VolumeLayer { +class SectionLabeler { /* From the outside, the VolumeLayer accepts only global positions. Internally, these are converted to the actual used mags (activeMag). @@ -580,4 +580,4 @@ export function getFast3DCoordinateHelper( } } } -export default VolumeLayer; +export default SectionLabeler; From 5bfa7809f62fc887758e72466dc83dfbbb18ef8f Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 27 Oct 2025 15:06:49 +0100 Subject: [PATCH 04/37] rename section labeling module --- .../test/sagas/volumetracing/volumetracing_saga.spec.ts | 2 +- .../javascripts/viewer/model/bucket_data_handling/data_cube.ts | 2 +- frontend/javascripts/viewer/model/sagas/volume/helpers.ts | 2 +- .../viewer/model/sagas/volume/volume_interpolation_saga.ts | 2 +- frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx | 2 +- .../model/volumetracing/{volumelayer.ts => section_labeling.ts} | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename frontend/javascripts/viewer/model/volumetracing/{volumelayer.ts => section_labeling.ts} (100%) diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts index da2d279e623..652a19daa2d 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts @@ -17,7 +17,7 @@ import { requestBucketModificationInVolumeTracing, ensureMaybeActiveMappingIsLocked, } from "viewer/model/sagas/saga_helpers"; -import SectionLabeler from "viewer/model/volumetracing/volumelayer"; +import SectionLabeler from "viewer/model/volumetracing/section_labeling"; import { serverVolumeToClientVolumeTracing } from "viewer/model/reducers/volumetracing_reducer"; import { Model, Store } from "viewer/singletons"; import { hasRootSagaCrashed } from "viewer/model/sagas/root_saga"; diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts b/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts index 55eeef09795..45cedf8d627 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts @@ -34,7 +34,7 @@ import type { DimensionMap } from "viewer/model/dimensions"; import Dimensions from "viewer/model/dimensions"; import { listenToStoreProperty } from "viewer/model/helpers/listener_helpers"; import { globalPositionToBucketPosition } from "viewer/model/helpers/position_converter"; -import { VoxelNeighborQueue2D, VoxelNeighborQueue3D } from "viewer/model/volumetracing/volumelayer"; +import { VoxelNeighborQueue2D, VoxelNeighborQueue3D } from "viewer/model/volumetracing/section_labeling"; import type { Mapping } from "viewer/store"; import Store from "viewer/store"; import type { MagInfo } from "../helpers/mag_info"; diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index b48628cd3c6..78facac8d68 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -24,7 +24,7 @@ import { getHalfViewportExtentsInVx } from "viewer/model/sagas/saga_selectors"; import sampleVoxelMapToMagnification, { applyVoxelMap, } from "viewer/model/volumetracing/volume_annotation_sampling"; -import SectionLabeler, { type VoxelBuffer2D } from "viewer/model/volumetracing/volumelayer"; +import SectionLabeler, { type VoxelBuffer2D } from "viewer/model/volumetracing/section_labeling"; import { Model } from "viewer/singletons"; import type { BoundingBoxObject, VolumeTracing } from "viewer/store"; diff --git a/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts b/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts index 4d6a79bda4c..df24cb424ef 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts @@ -33,7 +33,7 @@ import { import Dimensions from "viewer/model/dimensions"; import type { Saga } from "viewer/model/sagas/effect-generators"; import { select } from "viewer/model/sagas/effect-generators"; -import type { VoxelBuffer2D } from "viewer/model/volumetracing/volumelayer"; +import type { VoxelBuffer2D } from "viewer/model/volumetracing/section_labeling"; import { Model, api } from "viewer/singletons"; import type { WebknossosState } from "viewer/store"; import { requestBucketModificationInVolumeTracing } from "../saga_helpers"; diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index 257b0d560a6..3e0061ef135 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -80,7 +80,7 @@ import { updateSegmentVisibilityVolumeAction, updateSegmentVolumeAction, } from "viewer/model/sagas/volume/update_actions"; -import type SectionLabeler from "viewer/model/volumetracing/volumelayer"; +import type SectionLabeler from "viewer/model/volumetracing/section_labeling"; import { Model, api } from "viewer/singletons"; import type { SegmentMap, VolumeTracing } from "viewer/store"; import { pushSaveQueueTransaction } from "../actions/save_actions"; diff --git a/frontend/javascripts/viewer/model/volumetracing/volumelayer.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts similarity index 100% rename from frontend/javascripts/viewer/model/volumetracing/volumelayer.ts rename to frontend/javascripts/viewer/model/volumetracing/section_labeling.ts From adc5b631ce704347cd8684e7456c42e03000d6fb Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 27 Oct 2025 15:07:09 +0100 Subject: [PATCH 05/37] update comment --- .../javascripts/viewer/model/volumetracing/section_labeling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 6627eb7cbc7..acbd07f9369 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -144,7 +144,7 @@ export class VoxelNeighborQueue2D extends VoxelNeighborQueue3D { class SectionLabeler { /* - From the outside, the VolumeLayer accepts only global positions. Internally, + From the outside, the SectionLabeler accepts only global positions. Internally, these are converted to the actual used mags (activeMag). Therefore, members of this class are in the mag space of `activeMag`. From 1cce98f1c182a2984a4da093063e356989099a31 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 27 Oct 2025 16:19:41 +0100 Subject: [PATCH 06/37] more private methods --- .../model/volumetracing/section_labeling.ts | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index acbd07f9369..b8cd16ee1f4 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -192,7 +192,7 @@ class SectionLabeler { this.maxCoord = maxCoord; } - getArea(): number { + private getArea(): number { const [maxCoord, minCoord] = [this.maxCoord, this.minCoord]; if (maxCoord == null || minCoord == null) { @@ -203,15 +203,6 @@ class SectionLabeler { return difference[0] * difference[1] * difference[2]; } - getLabeledBoundingBox(): BoundingBox | null { - if (this.minCoord == null || this.maxCoord == null) { - return null; - } - const min = zoomedPositionToGlobalPosition(this.minCoord, this.activeMag); - const max = zoomedPositionToGlobalPosition(this.maxCoord, this.activeMag); - return new BoundingBox({ min, max }); - } - getContourList(useGlobalCoords: boolean = false) { const globalContourList = getVolumeTracingById( Store.getState().annotation, @@ -303,7 +294,7 @@ class SectionLabeler { return buffer2D; } - vector2PerpendicularVector(pos1: Vector2, pos2: Vector2): Vector2 { + private vector2PerpendicularVector(pos1: Vector2, pos2: Vector2): Vector2 { const dx = pos2[0] - pos1[0]; if (dx === 0) { @@ -317,7 +308,7 @@ class SectionLabeler { } } - vector2Norm(vector: Vector2): number { + private vector2Norm(vector: Vector2): number { let norm = 0; for (const i of Vector2Indices) { @@ -327,7 +318,7 @@ class SectionLabeler { return Math.sqrt(norm); } - vector2DistanceWithScale(pos1: Vector2, pos2: Vector2, scale: Vector2): number { + private vector2DistanceWithScale(pos1: Vector2, pos2: Vector2, scale: Vector2): number { let distance = 0; for (const i of Vector2Indices) { @@ -353,7 +344,7 @@ class SectionLabeler { ); } - getRectangleBetweenCircles( + private getRectangleBetweenCircles( centre1: Vector2, centre2: Vector2, radius: number, @@ -479,7 +470,7 @@ class SectionLabeler { return buffer2D; } - drawOutlineVoxels(setMap: (arg0: number, arg1: number) => void): void { + private drawOutlineVoxels(setMap: (arg0: number, arg1: number) => void): void { const contourList = this.getContourList(); let p1; let p2; @@ -491,7 +482,7 @@ class SectionLabeler { } } - fillOutsideArea(map: Uint8Array, width: number, height: number): void { + private fillOutsideArea(map: Uint8Array, width: number, height: number): void { const setMap = (x: number, y: number) => { map[x * height + y] = 0; }; @@ -502,17 +493,17 @@ class SectionLabeler { Drawing.fillArea(0, 0, width, height, false, isEmpty, setMap); } - get2DCoordinate(coord3d: Vector3): Vector2 { + private get2DCoordinate(coord3d: Vector3): Vector2 { // Throw out 'thirdCoordinate' which is equal anyways const transposed = Dimensions.transDim(coord3d, this.plane); return [transposed[0], transposed[1]]; } - get3DCoordinate(coord2d: Vector2): Vector3 { + private get3DCoordinate(coord2d: Vector2): Vector3 { return Dimensions.transDim([coord2d[0], coord2d[1], this.thirdDimensionValue], this.plane); } - getFast3DCoordinateFunction(): ( + private getFast3DCoordinateFunction(): ( coordX: number, coordY: number, out: Vector3 | Float32Array, @@ -521,6 +512,8 @@ class SectionLabeler { } getUnzoomedCentroid(): Vector3 { + /* The return value is in global coordinate system */ + // Formula: // https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon let sumArea = 0; From 756fe62294ad7692e5880c2770a0c887011d9e9a Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 27 Oct 2025 20:49:42 +0100 Subject: [PATCH 07/37] refactor fast/non-fast 3d coordinate functions in section labeling --- .../viewer/model/sagas/volume/helpers.ts | 15 +++-- .../model/volumetracing/section_labeling.ts | 58 ++++++------------- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index 78facac8d68..21207668f08 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -188,8 +188,15 @@ export function* labelWithVoxelBuffer2D( const magInfo = yield* call(getMagInfo, segmentationLayer.mags); const labeledMag = magInfo.getMagByIndexOrThrow(labeledZoomStep); - const get3DCoordinateFromLocal2D = ([x, y]: Vector2) => - voxelBuffer.get3DCoordinate([x + voxelBuffer.minCoord2d[0], y + voxelBuffer.minCoord2d[1]]); + const get3DCoordinateFromLocal2D = ([x, y]: Vector2) => { + const outVar: Vector3 = [0, 0, 0]; + voxelBuffer.getFast3DCoordinate( + x + voxelBuffer.minCoord2d[0], + y + voxelBuffer.minCoord2d[1], + outVar, + ); + return outVar; + }; const topLeft3DCoord = get3DCoordinateFromLocal2D([0, 0]); const bottomRight3DCoord = get3DCoordinateFromLocal2D([voxelBuffer.width, voxelBuffer.height]); @@ -201,8 +208,8 @@ export function* labelWithVoxelBuffer2D( min: topLeft3DCoord, max: bottomRight3DCoord, }); - console.log("topLeft3DCoord", topLeft3DCoord) - console.log("bottomRight3DCoord", bottomRight3DCoord) + console.log("topLeft3DCoord", topLeft3DCoord); + console.log("bottomRight3DCoord", bottomRight3DCoord); for (const boundingBoxChunk of outerBoundingBox.chunkIntoBuckets()) { const { min, max } = boundingBoxChunk; const bucketZoomedAddress = zoomedPositionToZoomedAddress( diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index b8cd16ee1f4..c9af95fd0f8 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -8,7 +8,6 @@ import Constants, { OrthoViews, Vector3Indices, Vector2Indices } from "viewer/co import type { AnnotationTool } from "viewer/model/accessors/tool_accessor"; import { isBrushTool } from "viewer/model/accessors/tool_accessor"; import { getVolumeTracingById } from "viewer/model/accessors/volumetracing_accessor"; -import BoundingBox from "viewer/model/bucket_data_handling/bounding_box"; import Dimensions from "viewer/model/dimensions"; import { scaleGlobalPositionWithMagnification, @@ -29,18 +28,10 @@ export class VoxelBuffer2D { readonly width: number; readonly height: number; readonly minCoord2d: Vector2; - readonly get3DCoordinate: (arg0: Vector2) => Vector3; readonly getFast3DCoordinate: (arg0: number, arg1: number, arg2: Vector3 | Float32Array) => void; static empty(): VoxelBuffer2D { - return new VoxelBuffer2D( - new Uint8Array(0), - 0, - 0, - [0, 0], - () => [0, 0, 0], - () => {}, - ); + return new VoxelBuffer2D(new Uint8Array(0), 0, 0, [0, 0], () => {}); } constructor( @@ -48,14 +39,12 @@ export class VoxelBuffer2D { width: number, height: number, minCoord2d: Vector2, - get3DCoordinate: (arg0: Vector2) => Vector3, getFast3DCoordinate: (arg0: number, arg1: number, arg2: Vector3 | Float32Array) => void, ) { this.map = map; this.width = width; this.height = height; this.minCoord2d = minCoord2d; - this.get3DCoordinate = get3DCoordinate; this.getFast3DCoordinate = getFast3DCoordinate; if (!V2.equals(this.minCoord2d, V2.floor(this.minCoord2d))) { @@ -159,6 +148,8 @@ class SectionLabeler { readonly activeMag: Vector3; + fast3DCoordinateFunction: (coordX: number, coordY: number, out: Vector3 | Float32Array) => void; + constructor( volumeTracingId: string, plane: OrthoView, @@ -172,9 +163,14 @@ class SectionLabeler { this.activeMag = activeMag; const thirdDim = Dimensions.thirdDimensionForPlane(this.plane); this.thirdDimensionValue = Math.floor(thirdDimensionValue / this.activeMag[thirdDim]); + + this.fast3DCoordinateFunction = getFast3DCoordinateHelper(this.plane, this.thirdDimensionValue); } updateArea(globalPos: Vector3): void { + /* + * Adapts minCoord and maxCoord to the given position if necessary. + */ const pos = scaleGlobalPositionWithMagnification(globalPos, this.activeMag); let [maxCoord, minCoord] = [this.maxCoord, this.minCoord]; @@ -334,14 +330,7 @@ class SectionLabeler { map.fill(fillValue); } - return new VoxelBuffer2D( - map, - width, - height, - minCoord2d, - this.get3DCoordinate.bind(this), - this.getFast3DCoordinateFunction(), - ); + return new VoxelBuffer2D(map, width, height, minCoord2d, this.fast3DCoordinateFunction); } private getRectangleBetweenCircles( @@ -494,28 +483,18 @@ class SectionLabeler { } private get2DCoordinate(coord3d: Vector3): Vector2 { - // Throw out 'thirdCoordinate' which is equal anyways + // Throw out 'thirdCoordinate' which is always the same, anyway. const transposed = Dimensions.transDim(coord3d, this.plane); return [transposed[0], transposed[1]]; } - private get3DCoordinate(coord2d: Vector2): Vector3 { - return Dimensions.transDim([coord2d[0], coord2d[1], this.thirdDimensionValue], this.plane); - } - - private getFast3DCoordinateFunction(): ( - coordX: number, - coordY: number, - out: Vector3 | Float32Array, - ) => void { - return getFast3DCoordinateHelper(this.plane, this.thirdDimensionValue); - } - getUnzoomedCentroid(): Vector3 { - /* The return value is in global coordinate system */ + /* Returns the centroid (in the global coordinate system). + * + * Formula: + * https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon + */ - // Formula: - // https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon let sumArea = 0; let sumCx = 0; let sumCy = 0; @@ -536,13 +515,14 @@ class SectionLabeler { const cx = sumCx / 6 / area; const cy = sumCy / 6 / area; - const zoomedPosition = this.get3DCoordinate([cx, cy]); - const pos = zoomedPositionToGlobalPosition(zoomedPosition, this.activeMag); + const outZoomedPosition: Vector3 = [0, 0, 0]; + this.fast3DCoordinateFunction(cx, cy, outZoomedPosition); + const pos = zoomedPositionToGlobalPosition(outZoomedPosition, this.activeMag); return pos; } } -export function getFast3DCoordinateHelper( +function getFast3DCoordinateHelper( plane: OrthoView, thirdDimensionValue: number, ): (coordX: number, coordY: number, out: Vector3 | Float32Array) => void { From dc237b95c767d6cfea6530134e7baabab5a67d96 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 27 Oct 2025 21:10:05 +0100 Subject: [PATCH 08/37] move get3DCoordinateFromLocal2D into voxel buffer and add getTopLeft3DCoord and getBottomRight3DCoord methods --- .../model/helpers/transformation_helpers.ts | 2 +- .../viewer/model/sagas/volume/helpers.ts | 14 ++------------ .../model/volumetracing/section_labeling.ts | 18 ++++++++++++++++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/frontend/javascripts/viewer/model/helpers/transformation_helpers.ts b/frontend/javascripts/viewer/model/helpers/transformation_helpers.ts index 01b43c1210a..f7c8ba56723 100644 --- a/frontend/javascripts/viewer/model/helpers/transformation_helpers.ts +++ b/frontend/javascripts/viewer/model/helpers/transformation_helpers.ts @@ -167,6 +167,6 @@ export const transformPointUnscaled = (transforms: Transform) => { const matrix = M4x4.transpose(transforms.affineMatrix); return (pos: Vector3) => M4x4.transformVectorsAffine(matrix, [pos])[0]; } else { + return (pos: Vector3) => transforms.scaledTps.transformUnscaled(pos[0], pos[1], pos[2]); } - return (pos: Vector3) => transforms.scaledTps.transformUnscaled(pos[0], pos[1], pos[2]); }; diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index 21207668f08..8927485e3c5 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -188,18 +188,8 @@ export function* labelWithVoxelBuffer2D( const magInfo = yield* call(getMagInfo, segmentationLayer.mags); const labeledMag = magInfo.getMagByIndexOrThrow(labeledZoomStep); - const get3DCoordinateFromLocal2D = ([x, y]: Vector2) => { - const outVar: Vector3 = [0, 0, 0]; - voxelBuffer.getFast3DCoordinate( - x + voxelBuffer.minCoord2d[0], - y + voxelBuffer.minCoord2d[1], - outVar, - ); - return outVar; - }; - - const topLeft3DCoord = get3DCoordinateFromLocal2D([0, 0]); - const bottomRight3DCoord = get3DCoordinateFromLocal2D([voxelBuffer.width, voxelBuffer.height]); + const topLeft3DCoord = voxelBuffer.getTopLeft3DCoord(); + const bottomRight3DCoord = voxelBuffer.getBottomRight3DCoord(); // Since the bottomRight3DCoord is exclusive for the described bounding box, // the third dimension has to be increased by one (otherwise, the volume of the bounding // box would be empty) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index c9af95fd0f8..93be3566902 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -1,5 +1,5 @@ import Drawing from "libs/drawing"; -import { V2, V3 } from "libs/mjs"; +import { Matrix4x4, V2, V3 } from "libs/mjs"; import Toast from "libs/toast"; import _ from "lodash"; import messages from "messages"; @@ -16,6 +16,11 @@ import { } from "viewer/model/helpers/position_converter"; import { getBaseVoxelFactorsInUnit } from "viewer/model/scaleinfo"; import Store from "viewer/store"; +import { + invertTransform, + Transform, + transformPointUnscaled, +} from "../helpers/transformation_helpers"; /* A VoxelBuffer2D instance holds a two dimensional slice @@ -52,6 +57,15 @@ export class VoxelBuffer2D { } } + getTopLeft3DCoord = () => this.get3DCoordinateFromLocal2D([0, 0]); + getBottomRight3DCoord = () => this.get3DCoordinateFromLocal2D([this.width, this.height]); + + private get3DCoordinateFromLocal2D = ([x, y]: Vector2) => { + const outVar: Vector3 = [0, 0, 0]; + this.getFast3DCoordinate(x + this.minCoord2d[0], y + this.minCoord2d[1], outVar); + return outVar; + }; + linearizeIndex(x: number, y: number): number { return x * this.height + y; } @@ -199,7 +213,7 @@ class SectionLabeler { return difference[0] * difference[1] * difference[2]; } - getContourList(useGlobalCoords: boolean = false) { + private getContourList(useGlobalCoords: boolean = false) { const globalContourList = getVolumeTracingById( Store.getState().annotation, this.volumeTracingId, From 98fbefe8f6de52f137926c8229277a5897d88c88 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 27 Oct 2025 21:10:22 +0100 Subject: [PATCH 09/37] implement wip for TransformedSectionLabeler --- .../model/volumetracing/section_labeling.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 93be3566902..2c09ada71ff 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -536,6 +536,83 @@ class SectionLabeler { } } +export class TransformedSectionLabeler { + private readonly base: SectionLabeler; + private readonly transform: Transform; + applyTransform: (pos: Vector3) => Vector3; + applyInverseTransform: (pos: Vector3) => Vector3; + + constructor( + volumeTracingId: string, + plane: OrthoView, + thirdDimensionValue: number, + activeMag: Vector3, + transform: Transform, + ) { + this.assertOrthogonalTransform(transform); + this.transform = transform; + this.base = new SectionLabeler(volumeTracingId, plane, thirdDimensionValue, activeMag); + + this.applyTransform = transformPointUnscaled(this.transform); + this.applyInverseTransform = transformPointUnscaled(invertTransform(this.transform)); + } + + // --- Core coordinate helpers --- + + // private applyTransformList(list: Vector3[]): Vector3[] { + // return list.map((v) => this.applyTransform(v)); + // } + + private applyInverseTransformList(list: Vector3[]): Vector3[] { + return list.map((v) => this.applyInverseTransform(v)); + } + + private assertOrthogonalTransform(_m: Transform): void { + // todop + // Quick check for orthogonal ±1 rotation/flip matrices + // const shouldBeIdentity = Transform.multiply(m, Transform.transpose(m)); + // const isOrtho = Transform.isCloseToIdentity(shouldBeIdentity); + // const hasValidEntries = m.flat().every((x) => Math.abs(x) === 0 || Math.abs(x) === 1); + // if (!isOrtho || !hasValidEntries) { + // throw new Error("Transformation matrix must be an orthogonal ±1 rotation/flip/scale matrix"); + // } + } + + // --- Delegated methods with coordinate adaptation --- + + updateArea(globalPos: Vector3): void { + this.base.updateArea(this.applyTransform(globalPos)); + } + + isEmpty(): boolean { + return this.base.isEmpty(); + } + + getFillingVoxelBuffer2D(mode: AnnotationTool): VoxelBuffer2D { + // Buffer orientation could be left unchanged unless 2D plane transforms are needed. + return this.base.getFillingVoxelBuffer2D(mode); + } + + getRectangleVoxelBuffer2D( + lastUnzoomedPosition: Vector3, + unzoomedPosition: Vector3, + ): VoxelBuffer2D | null { + const p1 = this.applyTransform(lastUnzoomedPosition); + const p2 = this.applyTransform(unzoomedPosition); + return this.base.getRectangleVoxelBuffer2D(p1, p2); + } + + getCircleVoxelBuffer2D(position: Vector3): VoxelBuffer2D { + const p = this.applyTransform(position); + return this.base.getCircleVoxelBuffer2D(p); + } + + getUnzoomedCentroid(): Vector3 { + const centroid = this.base.getUnzoomedCentroid(); + return this.applyInverseTransform(centroid); + } +} + function getFast3DCoordinateHelper( plane: OrthoView, thirdDimensionValue: number, From 3369292ef6927025496fa5e014901eebbd975577 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 28 Oct 2025 16:05:01 +0100 Subject: [PATCH 10/37] rename addToLayer --- .../reducers/volumetracing_reducer.spec.ts | 24 ++++++++--------- .../volumetracing/bucket_eviction_helper.ts | 6 ++--- .../volumetracing/volumetracing_saga.spec.ts | 12 ++++----- .../volumetracing_saga_integration_1.spec.ts | 14 +++++----- .../volumetracing_saga_integration_2.spec.ts | 26 +++++++++---------- .../combinations/volume_handlers.ts | 6 ++--- .../model/actions/volumetracing_actions.ts | 8 +++--- .../model/helpers/action_logger_middleware.ts | 2 +- .../model/reducers/volumetracing_reducer.ts | 6 ++--- .../reducers/volumetracing_reducer_helpers.ts | 2 +- .../viewer/model/sagas/volume/helpers.ts | 6 ++++- .../viewer/model/sagas/volumetracing_saga.tsx | 20 +++++++------- 12 files changed, 68 insertions(+), 64 deletions(-) diff --git a/frontend/javascripts/test/reducers/volumetracing_reducer.spec.ts b/frontend/javascripts/test/reducers/volumetracing_reducer.spec.ts index 2bbe6a3b246..d64abf198a6 100644 --- a/frontend/javascripts/test/reducers/volumetracing_reducer.spec.ts +++ b/frontend/javascripts/test/reducers/volumetracing_reducer.spec.ts @@ -250,7 +250,7 @@ describe("VolumeTracing", () => { [1, 2, 3], [9, 3, 2], ] as Vector3[]; - const addToLayerActionFn = VolumeTracingActions.addToLayerAction; + const addToContourListActionFn = VolumeTracingActions.addToContourListAction; const alteredState = update(initialState, { annotation: { isUpdatingCurrentlyAllowed: { @@ -260,9 +260,9 @@ describe("VolumeTracing", () => { }); // Try to add positions to the contourList - let newState = VolumeTracingReducer(alteredState, addToLayerActionFn(contourList[0])); - newState = VolumeTracingReducer(newState, addToLayerActionFn(contourList[1])); - newState = VolumeTracingReducer(newState, addToLayerActionFn(contourList[2])); + let newState = VolumeTracingReducer(alteredState, addToContourListActionFn(contourList[0])); + newState = VolumeTracingReducer(newState, addToContourListActionFn(contourList[1])); + newState = VolumeTracingReducer(newState, addToContourListActionFn(contourList[2])); expect(newState).toBe(alteredState); }); @@ -272,13 +272,13 @@ describe("VolumeTracing", () => { [1, 2, 3], [9, 3, 2], ] as Vector3[]; - const addToLayerActionFn = VolumeTracingActions.addToLayerAction; + const addToContourListActionFn = VolumeTracingActions.addToContourListAction; const resetContourAction = VolumeTracingActions.resetContourAction(); // Add positions to the contourList - let newState = VolumeTracingReducer(initialState, addToLayerActionFn(contourList[0])); - newState = VolumeTracingReducer(newState, addToLayerActionFn(contourList[1])); - newState = VolumeTracingReducer(newState, addToLayerActionFn(contourList[2])); + let newState = VolumeTracingReducer(initialState, addToContourListActionFn(contourList[0])); + newState = VolumeTracingReducer(newState, addToContourListActionFn(contourList[1])); + newState = VolumeTracingReducer(newState, addToContourListActionFn(contourList[2])); // And reset the list newState = VolumeTracingReducer(newState, resetContourAction); @@ -294,11 +294,11 @@ const prepareContourListTest = (state: WebknossosState) => { [1, 2, 3], [9, 3, 2], ] as Vector3[]; - const addToLayerActionFn = VolumeTracingActions.addToLayerAction; + const addToContourListActionFn = VolumeTracingActions.addToContourListAction; - let newState = VolumeTracingReducer(state, addToLayerActionFn(contourList[0])); - newState = VolumeTracingReducer(newState, addToLayerActionFn(contourList[1])); - newState = VolumeTracingReducer(newState, addToLayerActionFn(contourList[2])); + let newState = VolumeTracingReducer(state, addToContourListActionFn(contourList[0])); + newState = VolumeTracingReducer(newState, addToContourListActionFn(contourList[1])); + newState = VolumeTracingReducer(newState, addToContourListActionFn(contourList[2])); return { newState, diff --git a/frontend/javascripts/test/sagas/volumetracing/bucket_eviction_helper.ts b/frontend/javascripts/test/sagas/volumetracing/bucket_eviction_helper.ts index b0d83cdefdf..f4c5dcad3c6 100644 --- a/frontend/javascripts/test/sagas/volumetracing/bucket_eviction_helper.ts +++ b/frontend/javascripts/test/sagas/volumetracing/bucket_eviction_helper.ts @@ -8,7 +8,7 @@ import { setToolAction } from "viewer/model/actions/ui_actions"; import { setPositionAction } from "viewer/model/actions/flycam_actions"; import { setActiveCellAction, - addToLayerAction, + addToContourListAction, startEditingAction, finishEditingAction, } from "viewer/model/actions/volumetracing_actions"; @@ -60,7 +60,7 @@ export async function testLabelingManyBuckets( for (const paintPosition of paintPositions1) { Store.dispatch(setPositionAction(paintPosition)); Store.dispatch(startEditingAction(paintPosition, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintPosition)); + Store.dispatch(addToContourListAction(paintPosition)); Store.dispatch(finishEditingAction()); } @@ -73,7 +73,7 @@ export async function testLabelingManyBuckets( for (const paintPosition of paintPositions2) { Store.dispatch(setPositionAction(paintPosition)); Store.dispatch(startEditingAction(paintPosition, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintPosition)); + Store.dispatch(addToContourListAction(paintPosition)); Store.dispatch(finishEditingAction()); } diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts index 652a19daa2d..a77c54b7945 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts @@ -39,7 +39,7 @@ const ensureMaybeMappingIsLockedReturnValueDummy = { isMappingLockedIfNeeded: tr const ACTIVE_CELL_ID = 5; const setActiveCellAction = VolumeTracingActions.setActiveCellAction(ACTIVE_CELL_ID); const startEditingAction = VolumeTracingActions.startEditingAction([0, 0, 0], OrthoViews.PLANE_XY); -const addToLayerActionFn = VolumeTracingActions.addToLayerAction; +const addToContourListActionFn = VolumeTracingActions.addToContourListAction; const finishEditingAction = VolumeTracingActions.finishEditingAction(); describe("VolumeTracingSaga", () => { @@ -167,11 +167,11 @@ describe("VolumeTracingSaga", () => { saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); saga.next("action_channel"); - saga.next(addToLayerActionFn([1, 2, 3])); + saga.next(addToContourListActionFn([1, 2, 3])); saga.next(OrthoViews.PLANE_XY); - saga.next(addToLayerActionFn([2, 3, 4])); + saga.next(addToContourListActionFn([2, 3, 4])); saga.next(OrthoViews.PLANE_XY); - saga.next(addToLayerActionFn([3, 4, 5])); + saga.next(addToContourListActionFn([3, 4, 5])); saga.next(OrthoViews.PLANE_XY); expect(sectionLabeler.minCoord).toEqual([-1, 0, 1]); expect(sectionLabeler.maxCoord).toEqual([5, 6, 7]); @@ -222,7 +222,7 @@ describe("VolumeTracingSaga", () => { saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); saga.next("action_channel"); - saga.next(addToLayerActionFn([1, 2, 3])); + saga.next(addToContourListActionFn([1, 2, 3])); saga.next(OrthoViews.PLANE_XY); // Validate that finishLayer was called const wroteVoxelsBox = { @@ -291,7 +291,7 @@ describe("VolumeTracingSaga", () => { saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); saga.next("action_channel"); - saga.next(addToLayerActionFn([1, 2, 3])); + saga.next(addToContourListActionFn([1, 2, 3])); saga.next(OrthoViews.PLANE_XY); const wroteVoxelsBox = { value: false, diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts index 802e53e6977..7ec79fef629 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts @@ -21,7 +21,7 @@ import { setSegmentGroupsAction, updateSegmentAction, setActiveCellAction, - addToLayerAction, + addToContourListAction, startEditingAction, finishEditingAction, setContourTracingModeAction, @@ -69,12 +69,12 @@ describe("Volume Tracing", () => { // Brush with ${newCellId} Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); // Brush with ${newCellId + 1} Store.dispatch(setActiveCellAction(newCellId + 1)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); expect( @@ -129,7 +129,7 @@ describe("Volume Tracing", () => { Store.dispatch(setToolAction(AnnotationTool.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); await api.tracing.save(); @@ -183,7 +183,7 @@ describe("Volume Tracing", () => { Store.dispatch(setPositionAction([0, 0, 0])); Store.dispatch(setToolAction(AnnotationTool.ERASE_BRUSH)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); await api.tracing.save(); @@ -233,7 +233,7 @@ describe("Volume Tracing", () => { Store.dispatch(setPositionAction([0, 0, 0])); Store.dispatch(setToolAction(AnnotationTool.ERASE_BRUSH)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); if (loadBeforeUndo) { @@ -279,7 +279,7 @@ describe("Volume Tracing", () => { Store.dispatch(setPositionAction([0, 0, 0])); Store.dispatch(setToolAction(AnnotationTool.ERASE_BRUSH)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); for (let zoomStep = 0; zoomStep <= 5; zoomStep++) { diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_2.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_2.spec.ts index b6f51a7f746..d83101e5e3b 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_2.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_2.spec.ts @@ -17,7 +17,7 @@ import Store from "viewer/store"; import { V3 } from "libs/mjs"; import { setActiveCellAction, - addToLayerAction, + addToContourListAction, dispatchFloodfillAsync, startEditingAction, finishEditingAction, @@ -62,7 +62,7 @@ describe("Volume Tracing", () => { Store.dispatch(setToolAction(AnnotationTool.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); const volumeTracingLayerName = api.data.getVolumeTracingLayerIds()[0]; @@ -152,7 +152,7 @@ describe("Volume Tracing", () => { Store.dispatch(setToolAction(AnnotationTool.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); const volumeTracingLayerName = api.data.getVolumeTracingLayerIds()[0]; @@ -316,7 +316,7 @@ describe("Volume Tracing", () => { Store.dispatch(setToolAction(AnnotationTool.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); for (let zoomStep = 0; zoomStep <= 5; zoomStep++) { @@ -389,7 +389,7 @@ describe("Volume Tracing", () => { Store.dispatch(setToolAction(AnnotationTool.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); expect(await api.data.getDataValue(volumeTracingLayerName, paintCenter)).toBe(newCellId); @@ -464,12 +464,12 @@ describe("Volume Tracing", () => { // Brush with ${newCellId} Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); // Brush with ${newCellId + 1} Store.dispatch(setActiveCellAction(newCellId + 1)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); if (assertBeforeUndo) { @@ -525,18 +525,18 @@ describe("Volume Tracing", () => { // Brush with ${newCellId} Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); // Brush with ${newCellId + 1} Store.dispatch(setActiveCellAction(newCellId + 1)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); // Erase everything Store.dispatch(setContourTracingModeAction(ContourModeEnum.DELETE)); Store.dispatch(setToolAction(AnnotationTool.ERASE_BRUSH)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); // Undo erasure await dispatchUndoAsync(Store.dispatch); @@ -603,15 +603,15 @@ describe("Volume Tracing", () => { Store.dispatch(setToolAction(AnnotationTool.BRUSH)); Store.dispatch(setActiveCellAction(newCellId)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); Store.dispatch(setActiveCellAction(newCellId + 1)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); Store.dispatch(setActiveCellAction(newCellId + 2)); Store.dispatch(startEditingAction(paintCenter, OrthoViews.PLANE_XY)); - Store.dispatch(addToLayerAction(paintCenter)); + Store.dispatch(addToContourListAction(paintCenter)); Store.dispatch(finishEditingAction()); await dispatchUndoAsync(Store.dispatch); diff --git a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts index 3c7634990c7..dacb3f9bc2b 100644 --- a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts @@ -8,7 +8,7 @@ import { getTransformsForLayer, globalToLayerTransformedPosition } from "viewer/ import { calculateGlobalPos } from "viewer/model/accessors/view_mode_accessor"; import { updateUserSettingAction } from "viewer/model/actions/settings_actions"; import { - addToLayerAction, + addToContourListAction, finishEditingAction, floodFillAction, resetContourAction, @@ -28,7 +28,7 @@ export function handleDrawStart(pos: Point2, plane: OrthoView) { Store.dispatch(setContourTracingModeAction(ContourModeEnum.DRAW)); Store.dispatch(startEditingAction(untransformedPos, plane)); - Store.dispatch(addToLayerAction(untransformedPos)); + Store.dispatch(addToContourListAction(untransformedPos)); } function getUntransformedSegmentationPosition(state: WebknossosState, globalPosRounded: Vector3) { @@ -54,7 +54,7 @@ export function handleMoveForDrawOrErase(pos: Point2) { const state = Store.getState(); const globalPosRounded = calculateGlobalPos(state, pos).rounded; const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); - Store.dispatch(addToLayerAction(untransformedPos)); + Store.dispatch(addToContourListAction(untransformedPos)); } export function handleEndForDrawOrErase() { Store.dispatch(finishEditingAction()); diff --git a/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts b/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts index c6ad8c7c1df..f2b2673adff 100644 --- a/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts +++ b/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts @@ -14,7 +14,7 @@ export type InitializeVolumeTracingAction = ReturnType; export type CreateCellAction = ReturnType; type StartEditingAction = ReturnType; -type AddToLayerAction = ReturnType; +type AddToContourListAction = ReturnType; export type FloodFillAction = ReturnType; export type PerformMinCutAction = ReturnType; type FinishEditingAction = ReturnType; @@ -83,7 +83,7 @@ export type VolumeTracingAction = | InitializeVolumeTracingAction | CreateCellAction | StartEditingAction - | AddToLayerAction + | AddToContourListAction | FloodFillAction | PerformMinCutAction | FinishEditingAction @@ -181,9 +181,9 @@ export const startEditingAction = (position: Vector3, planeId: OrthoView) => planeId, }) as const; -export const addToLayerAction = (position: Vector3) => +export const addToContourListAction = (position: Vector3) => ({ - type: "ADD_TO_LAYER", + type: "ADD_TO_CONTOUR_LIST", position, }) as const; diff --git a/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts b/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts index 129ea49a6c3..2f2bd5890b6 100644 --- a/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts +++ b/frontend/javascripts/viewer/model/helpers/action_logger_middleware.ts @@ -11,7 +11,7 @@ let lastActionName: string | null = null; let lastActionCount: number = 0; const actionBlacklist = [ - "ADD_TO_LAYER", + "ADD_TO_CONTOUR_LIST", "MOVE_FLYCAM", "MOVE_FLYCAM_ABSOLUTE", "MOVE_FLYCAM_ORTHO", diff --git a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts index 40c0b26dbd1..7e358014895 100644 --- a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts @@ -29,7 +29,7 @@ import { } from "viewer/model/reducers/reducer_helpers"; import { type VolumeTracingReducerAction, - addToLayerReducer, + addToContourListReducer, createCellReducer, getSegmentUpdateInfo, hideBrushReducer, @@ -432,8 +432,8 @@ function VolumeTracingReducer( return updateDirectionReducer(state, volumeTracing, action.centroid); } - case "ADD_TO_LAYER": { - return addToLayerReducer(state, volumeTracing, action.position); + case "ADD_TO_CONTOUR_LIST": { + return addToContourListReducer(state, volumeTracing, action.position); } case "RESET_CONTOUR": { diff --git a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer_helpers.ts b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer_helpers.ts index 257fcfdddcd..31f5a462d36 100644 --- a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer_helpers.ts +++ b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer_helpers.ts @@ -128,7 +128,7 @@ export function updateDirectionReducer( .slice(0, MAXIMUM_LABEL_ACTIONS_COUNT), }); } -export function addToLayerReducer( +export function addToContourListReducer( state: WebknossosState, volumeTracing: VolumeTracing, position: Vector3, diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index 8927485e3c5..4fbf57b7365 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -10,7 +10,11 @@ import Constants, { type Vector2, type Vector3, } from "viewer/constants"; -import { getDatasetBoundingBox, getMagInfo } from "viewer/model/accessors/dataset_accessor"; +import { + getDatasetBoundingBox, + getLayerByName, + getMagInfo, +} from "viewer/model/accessors/dataset_accessor"; import { getFlooredPosition } from "viewer/model/accessors/flycam_accessor"; import { enforceActiveVolumeTracing } from "viewer/model/accessors/volumetracing_accessor"; import BoundingBox from "viewer/model/bucket_data_handling/bounding_box"; diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index 3e0061ef135..a02a26f3360 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -260,17 +260,17 @@ export function* editVolumeLayerAsync(): Saga { } let lastPosition = startEditingAction.position; - const channel = yield* actionChannel(["ADD_TO_LAYER", "FINISH_EDITING"]); + const channel = yield* actionChannel(["ADD_TO_CONTOUR_LIST", "FINISH_EDITING"]); while (true) { const currentAction = yield* take(channel); - const { addToLayerAction, finishEditingAction } = { - addToLayerAction: currentAction.type === "ADD_TO_LAYER" ? currentAction : null, + const { addToContourListAction, finishEditingAction } = { + addToContourListAction: currentAction.type === "ADD_TO_CONTOUR_LIST" ? currentAction : null, finishEditingAction: currentAction.type === "FINISH_EDITING" ? currentAction : null, }; if (finishEditingAction) break; - if (!addToLayerAction || addToLayerAction.type !== "ADD_TO_LAYER") { + if (!addToContourListAction || addToContourListAction.type !== "ADD_TO_CONTOUR_LIST") { throw new Error("Unexpected action. Satisfy typescript."); } @@ -281,7 +281,7 @@ export function* editVolumeLayerAsync(): Saga { continue; } - if (V3.equals(lastPosition, addToLayerAction.position)) { + if (V3.equals(lastPosition, addToContourListAction.position)) { // The voxel position did not change since the last action (the mouse moved // within a voxel). There is no need to do anything. continue; @@ -290,13 +290,13 @@ export function* editVolumeLayerAsync(): Saga { if (isTraceTool(activeTool) || (isBrushTool(activeTool) && isDrawing)) { // Close the polygon. When brushing, this causes an auto-fill which is why // it's only performed when drawing (not when erasing). - currentSectionLabeler.updateArea(addToLayerAction.position); + currentSectionLabeler.updateArea(addToContourListAction.position); } if (isBrushTool(activeTool)) { const rectangleVoxelBuffer2D = currentSectionLabeler.getRectangleVoxelBuffer2D( lastPosition, - addToLayerAction.position, + addToContourListAction.position, ); if (rectangleVoxelBuffer2D) { @@ -313,7 +313,7 @@ export function* editVolumeLayerAsync(): Saga { yield* call( labelWithVoxelBuffer2D, - currentSectionLabeler.getCircleVoxelBuffer2D(addToLayerAction.position), + currentSectionLabeler.getCircleVoxelBuffer2D(addToContourListAction.position), contourTracingMode, overwriteMode, labeledZoomStep, @@ -322,7 +322,7 @@ export function* editVolumeLayerAsync(): Saga { ); } - lastPosition = addToLayerAction.position; + lastPosition = addToContourListAction.position; } yield* call( @@ -677,7 +677,7 @@ function* maintainContourGeometry(): Saga { const { contour } = SceneController; while (true) { - yield* take(["ADD_TO_LAYER", "RESET_CONTOUR"]); + yield* take(["ADD_TO_CONTOUR_LIST", "RESET_CONTOUR"]); const isTraceToolActive = yield* select((state) => isTraceTool(state.uiInformation.activeTool)); const volumeTracing = yield* select(getActiveSegmentationTracing); From 0680b2dda71c06c659cf531e400de1300e68390e Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 28 Oct 2025 16:16:23 +0100 Subject: [PATCH 11/37] adapt plane to rotation --- .../viewer/model/sagas/volume/helpers.ts | 30 +++++-- .../quick_select_heuristic_saga.ts | 2 +- .../sagas/volume/volume_interpolation_saga.ts | 2 +- .../viewer/model/sagas/volumetracing_saga.tsx | 8 +- .../model/volumetracing/section_labeling.ts | 78 +++++++++++++++++-- 5 files changed, 98 insertions(+), 22 deletions(-) diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index 4fbf57b7365..8c43131dc67 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -28,9 +28,13 @@ import { getHalfViewportExtentsInVx } from "viewer/model/sagas/saga_selectors"; import sampleVoxelMapToMagnification, { applyVoxelMap, } from "viewer/model/volumetracing/volume_annotation_sampling"; -import SectionLabeler, { type VoxelBuffer2D } from "viewer/model/volumetracing/section_labeling"; -import { Model } from "viewer/singletons"; +import SectionLabeler, { + TransformedSectionLabeler, + type VoxelBuffer2D, +} from "viewer/model/volumetracing/section_labeling"; +import { Model, Store } from "viewer/singletons"; import type { BoundingBoxObject, VolumeTracing } from "viewer/store"; +import { getTransformsForLayer } from "viewer/model/accessors/dataset_layer_transformation_accessor"; function* pairwise(arr: Array): Generator<[T, T], any, any> { for (let i = 0; i < arr.length - 1; i++) { @@ -284,13 +288,23 @@ export function* labelWithVoxelBuffer2D( } } -export function* createSectionLabeler( +export function createSectionLabeler( volumeTracing: VolumeTracing, planeId: OrthoView, labeledMags: Vector3, - thirdDimValue?: number, -): Saga { - const position = yield* select((state) => getFlooredPosition(state.flycam)); - thirdDimValue = thirdDimValue ?? position[Dimensions.thirdDimensionForPlane(planeId)]; - return new SectionLabeler(volumeTracing.tracingId, planeId, thirdDimValue, labeledMags); + getThirdDimValue: (thirdDim: number) => number, +): SectionLabeler | TransformedSectionLabeler { + const state = Store.getState(); + const { dataset } = state; + const { nativelyRenderedLayerName } = state.datasetConfiguration; + const layer = getLayerByName(dataset, volumeTracing.tracingId); + const segmentationTransforms = getTransformsForLayer(dataset, layer, nativelyRenderedLayerName); + + return new TransformedSectionLabeler( + volumeTracing.tracingId, + planeId, + getThirdDimValue, + labeledMags, + segmentationTransforms, + ); } diff --git a/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts b/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts index 397d2ddc6e6..c8b003cb226 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts @@ -517,7 +517,7 @@ export function* finalizeQuickSelectForSlice( volumeTracing, activeViewport, labeledMag, - w, + () => w, // todop ); const sizeUVWInMag = mask.shape; const voxelBuffer2D = sectionLabeler.createVoxelBuffer2D( diff --git a/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts b/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts index df24cb424ef..cb7a83b019c 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/volume_interpolation_saga.ts @@ -372,7 +372,7 @@ export default function* maybeInterpolateSegmentationLayer(): Saga { volumeTracing, activeViewport, labeledMag, - relevantBoxMag1.min[thirdDim] + labeledMag[thirdDim] * targetOffsetW, + (thirdDim) => relevantBoxMag1.min[thirdDim] + labeledMag[thirdDim] * targetOffsetW, ); interpolationVoxelBuffers[targetOffsetW] = sectionLabeler.createVoxelBuffer2D( V2.floor( diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index a02a26f3360..c5b8c047d8f 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -90,6 +90,7 @@ import { floodFill } from "./volume/floodfill_saga"; import { type BooleanBox, createSectionLabeler, labelWithVoxelBuffer2D } from "./volume/helpers"; import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_saga"; import Dimensions from "../dimensions"; +import { TransformedSectionLabeler } from "viewer/model/volumetracing/section_labeling"; const OVERWRITE_EMPTY_WARNING_KEY = "OVERWRITE-EMPTY-WARNING"; @@ -237,13 +238,12 @@ export function* editVolumeLayerAsync(): Saga { ); const { zoomStep: labeledZoomStep, mag: labeledMag } = maybeLabeledMagWithZoomStep; - const w = Dimensions.thirdDimensionForPlane(startEditingAction.planeId); const currentSectionLabeler = yield* call( createSectionLabeler, volumeTracing, - window.hardPlaneId ?? startEditingAction.planeId, + startEditingAction.planeId, labeledMag, - startEditingAction.position[w], + (thirdDim) => startEditingAction.position[thirdDim], ); const initialViewport = yield* select((state) => state.viewModeData.plane.activeViewport); @@ -364,7 +364,7 @@ export function* editVolumeLayerAsync(): Saga { } export function* finishSectionLabeler( - layer: SectionLabeler, + layer: SectionLabeler | TransformedSectionLabeler, activeTool: AnnotationTool, contourTracingMode: ContourMode, overwriteMode: OverwriteMode, diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 2c09ada71ff..0e6ef534efe 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -1,10 +1,17 @@ import Drawing from "libs/drawing"; -import { Matrix4x4, V2, V3 } from "libs/mjs"; +import { V2, V3 } from "libs/mjs"; import Toast from "libs/toast"; import _ from "lodash"; import messages from "messages"; +import * as THREE from "three"; +import { type Euler, Matrix3, Vector3 as ThreeVector3 } from "three"; import type { OrthoView, Vector2, Vector3 } from "viewer/constants"; -import Constants, { OrthoViews, Vector3Indices, Vector2Indices } from "viewer/constants"; +import Constants, { + OrthoViews, + Vector3Indices, + Vector2Indices, + OrthoBaseRotations, +} from "viewer/constants"; import type { AnnotationTool } from "viewer/model/accessors/tool_accessor"; import { isBrushTool } from "viewer/model/accessors/tool_accessor"; import { getVolumeTracingById } from "viewer/model/accessors/volumetracing_accessor"; @@ -17,8 +24,8 @@ import { import { getBaseVoxelFactorsInUnit } from "viewer/model/scaleinfo"; import Store from "viewer/store"; import { + type Transform, invertTransform, - Transform, transformPointUnscaled, } from "../helpers/transformation_helpers"; @@ -536,22 +543,69 @@ class SectionLabeler { } } +function eulerToNormal(e: Euler): ThreeVector3 { + const m = new Matrix3().setFromMatrix4(new THREE.Matrix4().makeRotationFromEuler(e)); + const n = new ThreeVector3(0, 0, 1); + n.applyMatrix3(m).normalize(); + return n; +} + +function mapTransformedPlane(originalPlane: OrthoView, transform: Transform): OrthoView { + const originalNormal = eulerToNormal(OrthoBaseRotations[originalPlane]); + const transformedNormal = originalNormal + .clone() + .applyMatrix4(new THREE.Matrix4(...transform.affineMatrix)) + .normalize(); + + const canonical: Record = { + [OrthoViews.PLANE_XY]: new ThreeVector3(0, 0, 1), + [OrthoViews.PLANE_YZ]: new ThreeVector3(1, 0, 0), + [OrthoViews.PLANE_XZ]: new ThreeVector3(0, 1, 0), + [OrthoViews.TDView]: new ThreeVector3(1, 1, 1).normalize(), + }; + let bestView = OrthoViews.PLANE_XY; + let bestDot = Number.NEGATIVE_INFINITY; + + for (const [view, normal] of Object.entries(canonical)) { + const dot = Math.abs(transformedNormal.dot(normal as ThreeVector3)); + if (dot > bestDot) { + bestDot = dot; + bestView = view as OrthoView; + } + } + + return bestView; +} + export class TransformedSectionLabeler { private readonly base: SectionLabeler; private readonly transform: Transform; applyTransform: (pos: Vector3) => Vector3; applyInverseTransform: (pos: Vector3) => Vector3; + mappedPlane: OrthoView; constructor( volumeTracingId: string, plane: OrthoView, - thirdDimensionValue: number, + getThirdDimValue: (thirdDim: number) => number, activeMag: Vector3, transform: Transform, ) { this.assertOrthogonalTransform(transform); this.transform = transform; - this.base = new SectionLabeler(volumeTracingId, plane, thirdDimensionValue, activeMag); + this.mappedPlane = mapTransformedPlane(plane, transform); + + const thirdDimensionValue = getThirdDimValue( + Dimensions.thirdDimensionForPlane(this.mappedPlane), + ); + + // the base SectionLabeler operates in the *transformed* plane + this.base = new SectionLabeler( + volumeTracingId, + this.mappedPlane, + thirdDimensionValue, + activeMag, + ); this.applyTransform = transformPointUnscaled(this.transform); this.applyInverseTransform = transformPointUnscaled(invertTransform(this.transform)); @@ -563,9 +617,9 @@ export class TransformedSectionLabeler { // return list.map((v) => this.applyTransform(v)); // } - private applyInverseTransformList(list: Vector3[]): Vector3[] { - return list.map((v) => this.applyInverseTransform(v)); - } + // private applyInverseTransformList(list: Vector3[]): Vector3[] { + // return list.map((v) => this.applyInverseTransform(v)); + // } private assertOrthogonalTransform(_m: Transform): void { // todop @@ -580,6 +634,10 @@ export class TransformedSectionLabeler { // --- Delegated methods with coordinate adaptation --- + createVoxelBuffer2D(minCoord2d: Vector2, width: number, height: number, fillValue: number = 0) { + return this.base.createVoxelBuffer2D(minCoord2d, width, height, fillValue); + } + updateArea(globalPos: Vector3): void { this.base.updateArea(this.applyTransform(globalPos)); } @@ -602,6 +660,10 @@ export class TransformedSectionLabeler { return this.base.getRectangleVoxelBuffer2D(p1, p2); } + globalCoordToMag2DFloat(position: Vector3): Vector2 { + return this.base.globalCoordToMag2DFloat(position); + } + getCircleVoxelBuffer2D(position: Vector3): VoxelBuffer2D { const p = this.applyTransform(position); return this.base.getCircleVoxelBuffer2D(p); From 104c0d75a442af5b96476c7372d242844ad922d9 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 28 Oct 2025 17:15:50 +0100 Subject: [PATCH 12/37] fix annotation contour in transformed layer --- .../viewer/controller/scene_controller.ts | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index b92be58850d..3dff65a6d55 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -88,29 +88,29 @@ const getVisibleSegmentationLayerNames = reuseInstanceOnEquality((storeState: We ); class SceneController { - skeletons: Record = {}; - isPlaneVisible: OrthoViewMap; - clippingDistanceInUnit: number; - datasetBoundingBox!: Cube; - userBoundingBoxGroup!: Group; - layerBoundingBoxGroup!: Group; - userBoundingBoxes!: Array; - layerBoundingBoxes!: { [layerName: string]: Cube }; - annotationToolsGeometryGroup!: Group; - highlightedBBoxId: number | null | undefined; - taskCubeByTracingId: Record = {}; - contour!: ContourGeometry; - quickSelectGeometry!: QuickSelectGeometry; - lineMeasurementGeometry!: LineMeasurementGeometry; - areaMeasurementGeometry!: ContourGeometry; - planes!: OrthoViewWithoutTDMap; - rootNode!: Group; - renderer!: WebGLRenderer; - scene!: Scene; - rootGroup!: Group; - segmentMeshController: SegmentMeshController; - storePropertyUnsubscribers: Array<() => void>; - splitBoundaryMesh: Mesh | null = null; + public skeletons: Record = {}; + private isPlaneVisible: OrthoViewMap; + private clippingDistanceInUnit: number; + private datasetBoundingBox!: Cube; + private userBoundingBoxGroup!: Group; + private layerBoundingBoxGroup!: Group; + private userBoundingBoxes!: Array; + private layerBoundingBoxes!: { [layerName: string]: Cube }; + private annotationToolsGeometryGroup!: Group; + private highlightedBBoxId: number | null | undefined; + private taskCubeByTracingId: Record = {}; + public contour!: ContourGeometry; + public quickSelectGeometry!: QuickSelectGeometry; + public lineMeasurementGeometry!: LineMeasurementGeometry; + public areaMeasurementGeometry!: ContourGeometry; + private planes!: OrthoViewWithoutTDMap; + private rootNode!: Group; + public renderer!: WebGLRenderer; + public scene!: Scene; + public rootGroup!: Group; + public segmentMeshController: SegmentMeshController; + private storePropertyUnsubscribers: Array<() => void>; + private splitBoundaryMesh: Mesh | null = null; // Created as instance properties to avoid creating objects in each update call. private rotatedPositionOffsetVector = new ThreeVector3(); @@ -567,7 +567,13 @@ class SceneController { } } - updateUserBoundingBoxesAndMeshesAccordingToTransforms(): void { + updateGeometriesToTransforms(): void { + /* + * The following geometries are updated in accordance to the current transforms: + * - user bounding boxes + * - meshes + * - annotation specific geometries (e.g., the contour) + */ const state = Store.getState(); const tracingStoringUserBBoxes = getSomeTracing(state.annotation); const transformForBBoxes = @@ -597,6 +603,8 @@ class SceneController { transformForMeshes, this.segmentMeshController.meshesLayerLODRootGroup, ); + + this.applyTransformToGroup(transformForMeshes, this.annotationToolsGeometryGroup); } updateMeshesAccordingToLayerVisibility(): void { @@ -768,7 +776,7 @@ class SceneController { (storeState) => storeState.datasetConfiguration.nativelyRenderedLayerName, () => { this.updateLayerBoundingBoxes(); - this.updateUserBoundingBoxesAndMeshesAccordingToTransforms(); + this.updateGeometriesToTransforms(); }, ), listenToStoreProperty(getVisibleSegmentationLayerNames, () => From b573f9310ee183e6f800cd5b2ee00e13352b9ef7 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 28 Oct 2025 17:18:27 +0100 Subject: [PATCH 13/37] format --- .../combinations/volume_handlers.ts | 41 +++++++++++++------ .../viewer/controller/scene_controller.ts | 2 +- .../model/accessors/disabled_tool_accessor.ts | 12 +++--- .../model/bucket_data_handling/data_cube.ts | 5 ++- .../viewer/model/sagas/volume/helpers.ts | 13 +++--- .../viewer/model/sagas/volumetracing_saga.tsx | 3 +- 6 files changed, 46 insertions(+), 30 deletions(-) diff --git a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts index dacb3f9bc2b..216718bb08a 100644 --- a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts @@ -3,8 +3,14 @@ import memoizeOne from "memoize-one"; import type { AdditionalCoordinate } from "types/api_types"; import type { OrthoView, Point2, Vector3 } from "viewer/constants"; import { ContourModeEnum } from "viewer/constants"; -import { getLayerByName, getVisibleSegmentationLayer } from "viewer/model/accessors/dataset_accessor"; -import { getTransformsForLayer, globalToLayerTransformedPosition } from "viewer/model/accessors/dataset_layer_transformation_accessor"; +import { + getLayerByName, + getVisibleSegmentationLayer, +} from "viewer/model/accessors/dataset_accessor"; +import { + getTransformsForLayer, + globalToLayerTransformedPosition, +} from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { calculateGlobalPos } from "viewer/model/accessors/view_mode_accessor"; import { updateUserSettingAction } from "viewer/model/actions/settings_actions"; import { @@ -17,7 +23,10 @@ import { startEditingAction, updateSegmentAction, } from "viewer/model/actions/volumetracing_actions"; -import { invertTransform, transformPointUnscaled } from "viewer/model/helpers/transformation_helpers"; +import { + invertTransform, + transformPointUnscaled, +} from "viewer/model/helpers/transformation_helpers"; import { Model, Store, api } from "viewer/singletons"; import type { WebknossosState } from "viewer/store"; @@ -32,14 +41,22 @@ export function handleDrawStart(pos: Point2, plane: OrthoView) { } function getUntransformedSegmentationPosition(state: WebknossosState, globalPosRounded: Vector3) { - const { nativelyRenderedLayerName } = state.datasetConfiguration; - const maybeLayer = Model.getVisibleSegmentationLayer(); - if (maybeLayer == null) { throw new Error("Segmentation layer does not exist"); } - - const layer = getLayerByName(state.dataset, maybeLayer.name); - const segmentationTransforms = getTransformsForLayer(state.dataset, layer, nativelyRenderedLayerName); - const untransformedPos = transformPointUnscaled(invertTransform(segmentationTransforms))(globalPosRounded); - return untransformedPos; + const { nativelyRenderedLayerName } = state.datasetConfiguration; + const maybeLayer = Model.getVisibleSegmentationLayer(); + if (maybeLayer == null) { + throw new Error("Segmentation layer does not exist"); + } + + const layer = getLayerByName(state.dataset, maybeLayer.name); + const segmentationTransforms = getTransformsForLayer( + state.dataset, + layer, + nativelyRenderedLayerName, + ); + const untransformedPos = transformPointUnscaled(invertTransform(segmentationTransforms))( + globalPosRounded, + ); + return untransformedPos; } export function handleEraseStart(pos: Point2, plane: OrthoView) { @@ -61,7 +78,7 @@ export function handleEndForDrawOrErase() { Store.dispatch(resetContourAction()); } export function handlePickCell(pos: Point2) { - const state = Store.getState(); + const state = Store.getState(); const globalPosRounded = calculateGlobalPos(state, pos).rounded; const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index 3dff65a6d55..158c4abf850 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -432,7 +432,7 @@ class SceneController { if (this.splitBoundaryMesh != null) { this.splitBoundaryMesh.visible = id === OrthoViews.TDView; } - this.annotationToolsGeometryGroup.visible = id !== OrthoViews.TDView; + this.annotationToolsGeometryGroup.visible = true; // todop id !== OrthoViews.TDView; this.lineMeasurementGeometry.updateForCam(id); const originalPosition = getPosition(flycam); diff --git a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts index 1dedcccabbe..e5e0252b778 100644 --- a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts @@ -5,13 +5,11 @@ import { } from "admin/organization/pricing_plan_utils"; import memoizeOne from "memoize-one"; import type { APIOrganization, APIUser } from "types/api_types"; -import { IdentityTransform } from "viewer/constants"; import { getVisibleSegmentationLayer } from "viewer/model/accessors/dataset_accessor"; import { isMagRestrictionViolated, isRotated } from "viewer/model/accessors/flycam_accessor"; import type { WebknossosState } from "viewer/store"; import { reuseInstanceOnEquality } from "./accessor_helpers"; -import { getTransformsPerLayer } from "./dataset_layer_transformation_accessor"; -import { areGeometriesTransformed, isSkeletonLayerVisible } from "./skeletontracing_accessor"; +import { isSkeletonLayerVisible } from "./skeletontracing_accessor"; import { type AgglomerateState, @@ -335,10 +333,10 @@ function getDisabledVolumeInfo(state: WebknossosState) { const visibleSegmentationLayer = getVisibleSegmentationLayer(state); const isSegmentationTracingTransformed = false; // todop: set to true if not a 90 deg rotation - // segmentationTracingLayer != null && - // getTransformsPerLayer(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName)[ - // segmentationTracingLayer.tracingId - // ] !== IdentityTransform; + // segmentationTracingLayer != null && + // getTransformsPerLayer(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName)[ + // segmentationTracingLayer.tracingId + // ] !== IdentityTransform; const isSegmentationTracingVisible = segmentationTracingLayer != null && visibleSegmentationLayer != null && diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts b/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts index 45cedf8d627..fbb630016b7 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/data_cube.ts @@ -34,7 +34,10 @@ import type { DimensionMap } from "viewer/model/dimensions"; import Dimensions from "viewer/model/dimensions"; import { listenToStoreProperty } from "viewer/model/helpers/listener_helpers"; import { globalPositionToBucketPosition } from "viewer/model/helpers/position_converter"; -import { VoxelNeighborQueue2D, VoxelNeighborQueue3D } from "viewer/model/volumetracing/section_labeling"; +import { + VoxelNeighborQueue2D, + VoxelNeighborQueue3D, +} from "viewer/model/volumetracing/section_labeling"; import type { Mapping } from "viewer/store"; import Store from "viewer/store"; import type { MagInfo } from "../helpers/mag_info"; diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index 8c43131dc67..d72b471b192 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -7,7 +7,6 @@ import Constants, { type OrthoView, type OverwriteMode, OverwriteModeEnum, - type Vector2, type Vector3, } from "viewer/constants"; import { @@ -15,7 +14,7 @@ import { getLayerByName, getMagInfo, } from "viewer/model/accessors/dataset_accessor"; -import { getFlooredPosition } from "viewer/model/accessors/flycam_accessor"; +import { getTransformsForLayer } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { enforceActiveVolumeTracing } from "viewer/model/accessors/volumetracing_accessor"; import BoundingBox from "viewer/model/bucket_data_handling/bounding_box"; import type DataCube from "viewer/model/bucket_data_handling/data_cube"; @@ -25,16 +24,16 @@ import { zoomedPositionToZoomedAddress } from "viewer/model/helpers/position_con import type { Saga } from "viewer/model/sagas/effect-generators"; import { select } from "viewer/model/sagas/effect-generators"; import { getHalfViewportExtentsInVx } from "viewer/model/sagas/saga_selectors"; -import sampleVoxelMapToMagnification, { - applyVoxelMap, -} from "viewer/model/volumetracing/volume_annotation_sampling"; -import SectionLabeler, { +import type SectionLabeler from "viewer/model/volumetracing/section_labeling"; +import { TransformedSectionLabeler, type VoxelBuffer2D, } from "viewer/model/volumetracing/section_labeling"; +import sampleVoxelMapToMagnification, { + applyVoxelMap, +} from "viewer/model/volumetracing/volume_annotation_sampling"; import { Model, Store } from "viewer/singletons"; import type { BoundingBoxObject, VolumeTracing } from "viewer/store"; -import { getTransformsForLayer } from "viewer/model/accessors/dataset_layer_transformation_accessor"; function* pairwise(arr: Array): Generator<[T, T], any, any> { for (let i = 0; i < arr.length - 1; i++) { diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index c5b8c047d8f..fa4a6a6b490 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -81,6 +81,7 @@ import { updateSegmentVolumeAction, } from "viewer/model/sagas/volume/update_actions"; import type SectionLabeler from "viewer/model/volumetracing/section_labeling"; +import type { TransformedSectionLabeler } from "viewer/model/volumetracing/section_labeling"; import { Model, api } from "viewer/singletons"; import type { SegmentMap, VolumeTracing } from "viewer/store"; import { pushSaveQueueTransaction } from "../actions/save_actions"; @@ -89,8 +90,6 @@ import { ensureWkInitialized } from "./ready_sagas"; import { floodFill } from "./volume/floodfill_saga"; import { type BooleanBox, createSectionLabeler, labelWithVoxelBuffer2D } from "./volume/helpers"; import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_saga"; -import Dimensions from "../dimensions"; -import { TransformedSectionLabeler } from "viewer/model/volumetracing/section_labeling"; const OVERWRITE_EMPTY_WARNING_KEY = "OVERWRITE-EMPTY-WARNING"; From ae6d11be2564af7f679021411d01618e4222a70a Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 28 Oct 2025 17:31:48 +0100 Subject: [PATCH 14/37] use adapted plane when applying voxel buffer --- .../volumetracing/volumetracing_saga.spec.ts | 2 -- .../controller/combinations/volume_handlers.ts | 3 +++ .../model/actions/volumetracing_actions.ts | 2 +- .../viewer/model/sagas/volumetracing_saga.tsx | 18 ++++++++---------- .../model/volumetracing/section_labeling.ts | 12 ++++++++++-- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts index a77c54b7945..5b91e6fec19 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts @@ -238,7 +238,6 @@ describe("VolumeTracingSaga", () => { ContourModeEnum.DRAW, OverwriteModeEnum.OVERWRITE_ALL, 0, - OrthoViews.PLANE_XY, wroteVoxelsBox, ), ); @@ -307,7 +306,6 @@ describe("VolumeTracingSaga", () => { ContourModeEnum.DELETE, OverwriteModeEnum.OVERWRITE_ALL, 0, - OrthoViews.PLANE_XY, wroteVoxelsBox, ), ); diff --git a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts index 216718bb08a..f8ac04bb754 100644 --- a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts @@ -41,6 +41,9 @@ export function handleDrawStart(pos: Point2, plane: OrthoView) { } function getUntransformedSegmentationPosition(state: WebknossosState, globalPosRounded: Vector3) { + /* + * Converts the given position from world space to layer space. + */ const { nativelyRenderedLayerName } = state.datasetConfiguration; const maybeLayer = Model.getVisibleSegmentationLayer(); if (maybeLayer == null) { diff --git a/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts b/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts index f2b2673adff..b7fc5333df8 100644 --- a/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts +++ b/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts @@ -177,7 +177,7 @@ export const createCellAction = (activeCellId: number, largestSegmentId: number) export const startEditingAction = (position: Vector3, planeId: OrthoView) => ({ type: "START_EDITING", - position, + position, // in layer space planeId, }) as const; diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index fa4a6a6b490..de2a62c0fa3 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -253,7 +253,7 @@ export function* editVolumeLayerAsync(): Saga { contourTracingMode, overwriteMode, labeledZoomStep, - initialViewport, + currentSectionLabeler.getPlane(), wroteVoxelsBox, ); } @@ -305,7 +305,7 @@ export function* editVolumeLayerAsync(): Saga { contourTracingMode, overwriteMode, labeledZoomStep, - activeViewport, + currentSectionLabeler.getPlane(), wroteVoxelsBox, ); } @@ -316,7 +316,7 @@ export function* editVolumeLayerAsync(): Saga { contourTracingMode, overwriteMode, labeledZoomStep, - activeViewport, + currentSectionLabeler.getPlane(), wroteVoxelsBox, ); } @@ -331,7 +331,6 @@ export function* editVolumeLayerAsync(): Saga { contourTracingMode, overwriteMode, labeledZoomStep, - initialViewport, wroteVoxelsBox, ); // Update the position of the current segment to the last position of the most recent annotation stroke. @@ -363,31 +362,30 @@ export function* editVolumeLayerAsync(): Saga { } export function* finishSectionLabeler( - layer: SectionLabeler | TransformedSectionLabeler, + sectionLabeler: SectionLabeler | TransformedSectionLabeler, activeTool: AnnotationTool, contourTracingMode: ContourMode, overwriteMode: OverwriteMode, labeledZoomStep: number, - activeViewport: OrthoView, wroteVoxelsBox: BooleanBox, ): Saga { - if (layer == null || layer.isEmpty()) { + if (sectionLabeler == null || sectionLabeler.isEmpty()) { return; } if (isVolumeDrawingTool(activeTool)) { yield* call( labelWithVoxelBuffer2D, - layer.getFillingVoxelBuffer2D(activeTool), + sectionLabeler.getFillingVoxelBuffer2D(activeTool), contourTracingMode, overwriteMode, labeledZoomStep, - activeViewport, + sectionLabeler.getPlane(), wroteVoxelsBox, ); } - yield* put(registerLabelPointAction(layer.getUnzoomedCentroid())); + yield* put(registerLabelPointAction(sectionLabeler.getUnzoomedCentroid())); } export function* ensureToolIsAllowedInMag(): Saga { diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 0e6ef534efe..2bea44a3ab6 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -541,6 +541,10 @@ class SectionLabeler { const pos = zoomedPositionToGlobalPosition(outZoomedPosition, this.activeMag); return pos; } + + getPlane(): OrthoView { + return this.plane; + } } function eulerToNormal(e: Euler): ThreeVector3 { @@ -665,14 +669,18 @@ export class TransformedSectionLabeler { } getCircleVoxelBuffer2D(position: Vector3): VoxelBuffer2D { - const p = this.applyTransform(position); - return this.base.getCircleVoxelBuffer2D(p); + // const p = this.applyTransform(position); + return this.base.getCircleVoxelBuffer2D(position); } getUnzoomedCentroid(): Vector3 { const centroid = this.base.getUnzoomedCentroid(); return this.applyInverseTransform(centroid); } + + getPlane(): OrthoView { + return this.mappedPlane; + } } function getFast3DCoordinateHelper( From ec73778b05501fd7326640623cc9d7569e999a55 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 3 Nov 2025 11:05:48 +0100 Subject: [PATCH 15/37] refactor; hardcode transformed plane; fix brush scaling; add debug output for layer pos to status bar --- .../dataset/dataset_rotation_form_item.tsx | 27 +++- .../combinations/volume_handlers.ts | 5 +- .../dataset_layer_transformation_accessor.ts | 20 ++- .../model/bucket_data_handling/bucket.ts | 1 + .../viewer/model/sagas/volume/helpers.ts | 14 +- .../model/volumetracing/section_labeling.ts | 149 ++++++++++++------ .../javascripts/viewer/view/statusbar.tsx | 8 +- 7 files changed, 150 insertions(+), 74 deletions(-) diff --git a/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx b/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx index bbc1e778347..38706896dc3 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx @@ -3,7 +3,7 @@ import { Col, Form, type FormInstance, InputNumber, Row, Slider, Tooltip, Typogr import FormItem from "antd/es/form/FormItem"; import Checkbox, { type CheckboxChangeEvent } from "antd/lib/checkbox/Checkbox"; import { useCallback, useEffect, useMemo } from "react"; -import type { APIDataLayer } from "types/api_types"; +import type { AffineTransformation, APIDataLayer } from "types/api_types"; import { AXIS_TO_TRANSFORM_INDEX, EXPECTED_TRANSFORMATION_LENGTH, @@ -64,13 +64,7 @@ export const AxisRotationFormItem: React.FC = ({ y: RotationAndMirroringSettings; z: RotationAndMirroringSettings; } = form.getFieldValue(["datasetRotation"]); - const transformations = [ - fromCenterToOrigin(datasetBoundingBox), - getRotationMatrixAroundAxis("x", rotationValues["x"]), - getRotationMatrixAroundAxis("y", rotationValues["y"]), - getRotationMatrixAroundAxis("z", rotationValues["z"]), - fromOriginToCenter(datasetBoundingBox), - ]; + const transformations = getRotationalTransformation(datasetBoundingBox, rotationValues); const dataLayersWithUpdatedTransforms = dataLayers.map((layer) => { return { ...layer, @@ -227,3 +221,20 @@ export const AxisRotationSettingForDataset: React.FC ); }; + +export function getRotationalTransformation( + datasetBoundingBox: BoundingBox, + rotationValues: { + x: RotationAndMirroringSettings; + y: RotationAndMirroringSettings; + z: RotationAndMirroringSettings; + }, +): AffineTransformation[] { + return [ + fromCenterToOrigin(datasetBoundingBox), + getRotationMatrixAroundAxis("x", rotationValues["x"]), + getRotationMatrixAroundAxis("y", rotationValues["y"]), + getRotationMatrixAroundAxis("z", rotationValues["z"]), + fromOriginToCenter(datasetBoundingBox), + ]; +} diff --git a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts index f8ac04bb754..a4b13a877a3 100644 --- a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts @@ -40,7 +40,10 @@ export function handleDrawStart(pos: Point2, plane: OrthoView) { Store.dispatch(addToContourListAction(untransformedPos)); } -function getUntransformedSegmentationPosition(state: WebknossosState, globalPosRounded: Vector3) { +export function getUntransformedSegmentationPosition( + state: WebknossosState, + globalPosRounded: Vector3, +) { /* * Converts the given position from world space to layer space. */ diff --git a/frontend/javascripts/viewer/model/accessors/dataset_layer_transformation_accessor.ts b/frontend/javascripts/viewer/model/accessors/dataset_layer_transformation_accessor.ts index 5096909cdda..f4f9ab0fc5a 100644 --- a/frontend/javascripts/viewer/model/accessors/dataset_layer_transformation_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/dataset_layer_transformation_accessor.ts @@ -178,6 +178,20 @@ function _getOriginalTransformsForLayerOrNull( return null; } + return combineCoordinateTransformations( + coordinateTransformations, + dataset.dataSource.scale.factor, + ); +} + +export const getOriginalTransformsForLayerOrNull = memoizeWithTwoKeys( + _getOriginalTransformsForLayerOrNull, +); + +export function combineCoordinateTransformations( + coordinateTransformations: CoordinateTransformation[], + scaleFactor: Vector3, +): Transform { const transforms = coordinateTransformations.map((coordTransformation) => { const { type } = coordTransformation; if (type === "affine") { @@ -186,7 +200,7 @@ function _getOriginalTransformsForLayerOrNull( } else if (type === "thin_plate_spline") { const { source, target } = coordTransformation.correspondences; - return createThinPlateSplineTransform(source, target, dataset.dataSource.scale.factor); + return createThinPlateSplineTransform(source, target, scaleFactor); } console.error( @@ -197,10 +211,6 @@ function _getOriginalTransformsForLayerOrNull( return transforms.reduce(chainTransforms, IdentityTransform); } -export const getOriginalTransformsForLayerOrNull = memoizeWithTwoKeys( - _getOriginalTransformsForLayerOrNull, -); - export function isLayerWithoutTransformationConfigSupport(layer: APIDataLayer | APISkeletonLayer) { return ( layer.category === "skeleton" || diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts b/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts index 5f9eb805c61..b60bc36382a 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts @@ -567,6 +567,7 @@ export class DataBucket { if (shouldOverwrite || (!shouldOverwrite && currentSegmentId === overwritableValue)) { data[voxelAddress] = segmentId; + // console.log("out coord", out); wroteVoxels = true; } } diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index d72b471b192..fd78499cd5d 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -205,8 +205,10 @@ export function* labelWithVoxelBuffer2D( min: topLeft3DCoord, max: bottomRight3DCoord, }); + console.log("topLeft3DCoord", topLeft3DCoord); console.log("bottomRight3DCoord", bottomRight3DCoord); + for (const boundingBoxChunk of outerBoundingBox.chunkIntoBuckets()) { const { min, max } = boundingBoxChunk; const bucketZoomedAddress = zoomedPositionToZoomedAddress( @@ -222,19 +224,15 @@ export function* labelWithVoxelBuffer2D( const labelMapOfBucket = new Uint8Array(Constants.BUCKET_WIDTH ** 2); currentLabeledVoxelMap.set(bucketZoomedAddress, labelMapOfBucket); + // voxelBuffer.print(); + // globalA (first dim) and globalB (second dim) are global coordinates // which can be used to index into the 2D slice of the VoxelBuffer2D (when subtracting the minCoord2d) // and the LabeledVoxelMap for (let globalA = min[dimensionIndices[0]]; globalA < max[dimensionIndices[0]]; globalA++) { for (let globalB = min[dimensionIndices[1]]; globalB < max[dimensionIndices[1]]; globalB++) { - if ( - voxelBuffer.map[ - voxelBuffer.linearizeIndex( - globalA - voxelBuffer.minCoord2d[0], - globalB - voxelBuffer.minCoord2d[1], - ) - ] - ) { + if (voxelBuffer.getValueFromGlobal(globalA, globalB)) { + // console.log("setting 1 at", globalA, globalB); labelMapOfBucket[ (globalA % Constants.BUCKET_WIDTH) * Constants.BUCKET_WIDTH + (globalB % Constants.BUCKET_WIDTH) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 2bea44a3ab6..4ed275e124e 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -73,7 +73,7 @@ export class VoxelBuffer2D { return outVar; }; - linearizeIndex(x: number, y: number): number { + private linearizeIndex(x: number, y: number): number { return x * this.height + y; } @@ -81,9 +81,31 @@ export class VoxelBuffer2D { this.map[this.linearizeIndex(x, y)] = value; } + getValue(x: number, y: number): number { + return this.map[this.linearizeIndex(x, y)]; + } + + getValueFromGlobal(globalX: number, globalY: number): number { + return this.map[ + this.linearizeIndex(globalX - this.minCoord2d[0], globalY - this.minCoord2d[1]) + ]; + } + isEmpty(): boolean { return this.width === 0 || this.height === 0; } + + print(): void { + const lines = []; + for (let y = 0; y < this.width; y++) { + const line = []; + for (let x = 0; x < this.width; x++) { + line.push(this.getValue(x, y)); + } + lines.push(line); + } + console.log("VoxelBuffer content:", lines.join("\n")); + } } export class VoxelNeighborQueue3D { queue: Array; @@ -159,33 +181,31 @@ class SectionLabeler { Therefore, members of this class are in the mag space of `activeMag`. */ - readonly volumeTracingId: string; - readonly plane: OrthoView; readonly thirdDimensionValue: number; // Stored in global (but mag-dependent) coordinates: minCoord: Vector3 | null | undefined; maxCoord: Vector3 | null | undefined; - readonly activeMag: Vector3; - fast3DCoordinateFunction: (coordX: number, coordY: number, out: Vector3 | Float32Array) => void; constructor( - volumeTracingId: string, - plane: OrthoView, + public readonly volumeTracingId: string, + public readonly plane: OrthoView, thirdDimensionValue: number, - activeMag: Vector3, + public readonly activeMag: Vector3, + public readonly isFlipped: boolean, ) { - this.volumeTracingId = volumeTracingId; - this.plane = plane; this.maxCoord = null; this.minCoord = null; - this.activeMag = activeMag; const thirdDim = Dimensions.thirdDimensionForPlane(this.plane); this.thirdDimensionValue = Math.floor(thirdDimensionValue / this.activeMag[thirdDim]); - this.fast3DCoordinateFunction = getFast3DCoordinateHelper(this.plane, this.thirdDimensionValue); + this.fast3DCoordinateFunction = getFast3DCoordinateFn( + this.plane, + this.thirdDimensionValue, + isFlipped, + ); } updateArea(globalPos: Vector3): void { @@ -438,7 +458,7 @@ class SectionLabeler { ); } - getCircleVoxelBuffer2D(position: Vector3): VoxelBuffer2D { + getCircleVoxelBuffer2D(position: Vector3, scale?: Vector2): VoxelBuffer2D { const state = Store.getState(); const { brushSize } = state.userConfiguration; const dimIndices = Dimensions.getIndices(this.plane); @@ -460,9 +480,29 @@ class SectionLabeler { ]; const buffer2D = this.createVoxelBuffer2D(minCoord2d, width, height); // Use the baseVoxelFactors to scale the circle, otherwise it'll become an ellipse - const [scaleX, scaleY] = this.get2DCoordinate( + let [scaleX, scaleY] = this.get2DCoordinate( getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale), ); + // if (window.wscale) { + // scaleX = window.wscale[0]; + // scaleY = window.wscale[1]; + // } + if (this.isFlipped) { + [scaleX, scaleY] = [scaleY, scaleX]; + } + if (scale) { + [scaleX, scaleY] = scale; + } + if (window.wscale) { + // Original scale is 1, 1, 0.39 + // XY -> XZ -> 1, 1 + // XZ -> XY -> 1, 0.39285714285714285 + // YZ -> YZ _> 1, 0.39285714285714285 + [scaleX, scaleY] = window.wscale; + } + + console.log("this.plane", this.plane); + console.log(`scaleX=${scaleX}, scaleY=${scaleY}`); const setMap = (x: number, y: number) => { buffer2D.setValue(x, y, 1); @@ -503,9 +543,9 @@ class SectionLabeler { Drawing.fillArea(0, 0, width, height, false, isEmpty, setMap); } - private get2DCoordinate(coord3d: Vector3): Vector2 { + public get2DCoordinate(coord3d: Vector3, plane?: OrthoView): Vector2 { // Throw out 'thirdCoordinate' which is always the same, anyway. - const transposed = Dimensions.transDim(coord3d, this.plane); + const transposed = Dimensions.transDim(coord3d, plane ?? this.plane); return [transposed[0], transposed[1]]; } @@ -554,7 +594,21 @@ function eulerToNormal(e: Euler): ThreeVector3 { return n; } -function mapTransformedPlane(originalPlane: OrthoView, transform: Transform): OrthoView { +export function mapTransformedPlane( + originalPlane: OrthoView, + transform: Transform, +): [OrthoView, boolean] { + if (originalPlane === "PLANE_XY") { + return ["PLANE_XZ", false]; + } + if (originalPlane === "PLANE_XZ") { + return ["PLANE_XY", false]; + } + if (originalPlane === "PLANE_YZ") { + return ["PLANE_YZ", true]; + } + throw new Error("Unexpected input plane"); + const originalNormal = eulerToNormal(OrthoBaseRotations[originalPlane]); const transformedNormal = originalNormal .clone() @@ -583,21 +637,20 @@ function mapTransformedPlane(originalPlane: OrthoView, transform: Transform): Or export class TransformedSectionLabeler { private readonly base: SectionLabeler; - private readonly transform: Transform; applyTransform: (pos: Vector3) => Vector3; applyInverseTransform: (pos: Vector3) => Vector3; - mappedPlane: OrthoView; + readonly mappedPlane: OrthoView; + private readonly isFlipped: boolean; constructor( volumeTracingId: string, - plane: OrthoView, + private readonly originalPlane: OrthoView, getThirdDimValue: (thirdDim: number) => number, activeMag: Vector3, - transform: Transform, + private readonly transform: Transform, ) { this.assertOrthogonalTransform(transform); - this.transform = transform; - this.mappedPlane = mapTransformedPlane(plane, transform); + [this.mappedPlane, this.isFlipped] = mapTransformedPlane(originalPlane, transform); const thirdDimensionValue = getThirdDimValue( Dimensions.thirdDimensionForPlane(this.mappedPlane), @@ -609,6 +662,7 @@ export class TransformedSectionLabeler { this.mappedPlane, thirdDimensionValue, activeMag, + this.isFlipped, ); this.applyTransform = transformPointUnscaled(this.transform); @@ -670,7 +724,16 @@ export class TransformedSectionLabeler { getCircleVoxelBuffer2D(position: Vector3): VoxelBuffer2D { // const p = this.applyTransform(position); - return this.base.getCircleVoxelBuffer2D(position); + + let scale = this.base.get2DCoordinate( + getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale), + this.originalPlane, + ); + if (this.isFlipped) { + scale = [scale[1], scale[0]]; + } + + return this.base.getCircleVoxelBuffer2D(position, scale); } getUnzoomedCentroid(): Vector3 { @@ -683,35 +746,19 @@ export class TransformedSectionLabeler { } } -function getFast3DCoordinateHelper( +function getFast3DCoordinateFn( plane: OrthoView, thirdDimensionValue: number, + _isFlipped: boolean, ): (coordX: number, coordY: number, out: Vector3 | Float32Array) => void { - switch (plane) { - case OrthoViews.PLANE_XY: - return (coordX, coordY, out) => { - out[0] = coordX; - out[1] = coordY; - out[2] = thirdDimensionValue; - }; - - case OrthoViews.PLANE_YZ: - return (coordX, coordY, out) => { - out[0] = thirdDimensionValue; - out[1] = coordY; - out[2] = coordX; - }; - - case OrthoViews.PLANE_XZ: - return (coordX, coordY, out) => { - out[0] = coordX; - out[1] = thirdDimensionValue; - out[2] = coordY; - }; - - default: { - throw new Error("Unknown plane id"); - } - } + let [u, v, w] = Dimensions.getIndices(plane); + // if (isFlipped) { + // [u, v] = [v, u]; + // } + return (coordX, coordY, out) => { + out[u] = coordX; + out[v] = coordY; + out[w] = thirdDimensionValue; + }; } export default SectionLabeler; diff --git a/frontend/javascripts/viewer/view/statusbar.tsx b/frontend/javascripts/viewer/view/statusbar.tsx index a0c6d95b00d..adb2e7b319f 100644 --- a/frontend/javascripts/viewer/view/statusbar.tsx +++ b/frontend/javascripts/viewer/view/statusbar.tsx @@ -42,6 +42,7 @@ import { getGlobalDataConnectionInfo } from "viewer/model/data_connection_info"; import { Store } from "viewer/singletons"; import { NumberInputPopoverSetting } from "viewer/view/components/setting_input_views"; import { CommandPalette } from "./components/command_palette"; +import { getUntransformedSegmentationPosition } from "viewer/controller/combinations/volume_handlers"; const lineColor = "rgba(255, 255, 255, 0.67)"; const moreIconStyle = { @@ -573,6 +574,11 @@ function SegmentAndMousePosition() { const isPlaneMode = useWkSelector((state) => getIsPlaneMode(state)); const globalMousePositionRounded = useWkSelector(getGlobalMousePosition); + const untransformedPos = + globalMousePositionRounded != null + ? getUntransformedSegmentationPosition(Store.getState(), globalMousePositionRounded) + : [0, 0, 0]; + return ( <> {isPlaneMode ? : null} @@ -582,7 +588,7 @@ function SegmentAndMousePosition() { {globalMousePositionRounded ? getPosString(globalMousePositionRounded, additionalCoordinates) : "-,-,-"} - ] + ] Layer Pos [{untransformedPos.join(", ")}] ) : null} From 4851335848acf8418d8a558bba250e95bbcdad0d Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Nov 2025 16:18:21 +0100 Subject: [PATCH 16/37] add and fix tests --- frontend/javascripts/test/global_mocks.ts | 3 + .../model/transformed_section_labeler.spec.ts | 172 +++++++++++++++++ .../model/volumetracing/section_labeling.ts | 177 +++++++++++------- 3 files changed, 285 insertions(+), 67 deletions(-) create mode 100644 frontend/javascripts/test/model/transformed_section_labeler.spec.ts diff --git a/frontend/javascripts/test/global_mocks.ts b/frontend/javascripts/test/global_mocks.ts index dfabfbf1ed7..c4311be106a 100644 --- a/frontend/javascripts/test/global_mocks.ts +++ b/frontend/javascripts/test/global_mocks.ts @@ -140,6 +140,9 @@ vi.mock("antd", () => { Form: { Item: {}, }, + Typography: { + Text: {}, + }, }; }); diff --git a/frontend/javascripts/test/model/transformed_section_labeler.spec.ts b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts new file mode 100644 index 00000000000..8c09e522137 --- /dev/null +++ b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts @@ -0,0 +1,172 @@ +import { getRotationalTransformation } from "dashboard/dataset/dataset_rotation_form_item"; +import { IdentityTransform, OrthoView } from "viewer/constants"; +import { combineCoordinateTransformations } from "viewer/model/accessors/dataset_layer_transformation_accessor"; +import BoundingBox from "viewer/model/bucket_data_handling/bounding_box"; +import { Transform } from "viewer/model/helpers/transformation_helpers"; +import { mapTransformedPlane as originalMapTransformedPlane } from "viewer/model/volumetracing/section_labeling"; +import { describe, expect, it } from "vitest"; + +const mapTransformedPlane = (plane: OrthoView, transform: Transform) => { + const [transformedPlane, isSwapped, adaptFn] = originalMapTransformedPlane(plane, transform); + const adaptedScale = adaptFn([0, 1, 2]); + return [transformedPlane, isSwapped, adaptedScale]; +}; + +describe("TransformedSectionLabeler", () => { + it("Identity transform should result in identity mapping of plane", async () => { + expect(mapTransformedPlane("PLANE_XY", IdentityTransform)).toEqual(["PLANE_XY", false, [0, 1]]); + expect(mapTransformedPlane("PLANE_YZ", IdentityTransform)).toEqual(["PLANE_YZ", false, [2, 1]]); + expect(mapTransformedPlane("PLANE_XZ", IdentityTransform)).toEqual(["PLANE_XZ", false, [0, 2]]); + }); + + it("Rotation by 90deg around X should be handled correctly", async () => { + const rotationalTransform = combineCoordinateTransformations( + getRotationalTransformation(new BoundingBox({ min: [5, 10, 15], max: [105, 115, 120] }), { + x: { rotationInDegrees: 90, isMirrored: false }, + y: { rotationInDegrees: 0, isMirrored: false }, + z: { rotationInDegrees: 0, isMirrored: false }, + }), + [1, 2, 3], + ); + + expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual([ + "PLANE_XZ", + false, + [0, 1], + ]); + expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ + "PLANE_YZ", + true, + [1, 2], + ]); + expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ + "PLANE_XY", + false, + [0, 2], + ]); + }); + + it("[L4] Rotation by 90deg around X should be handled correctly", async () => { + const coordinateTransformations = [ + { + matrix: [ + [1, 0, 0, -1529], + [0, 1, 0, -1478], + [0, 0, 1, -1476], + [0, 0, 0, 1], + ], + type: "affine" as const, + }, + { + matrix: [ + [1, 0, 0, 0], + [0, 0, -1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + ], + type: "affine" as const, + }, + { + matrix: [ + [1, 0, 0, 1529], + [0, 1, 0, 1478], + [0, 0, 1, 1476], + [0, 0, 0, 1], + ], + type: "affine" as const, + }, + ]; + + const rotationalTransform = combineCoordinateTransformations( + coordinateTransformations, + [11, 19, 28], + ); + + expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual([ + "PLANE_XZ", + false, + [0, 1], + ]); + expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ + "PLANE_YZ", + true, + [1, 2], + ]); + expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ + "PLANE_XY", + false, + [0, 2], + ]); + }); + + it("[L4] Rotation by 90deg around Z should be handled correctly", async () => { + const coordinateTransformations = [ + { + type: "affine" as const, + matrix: [ + [1, 0, 0, -1529], + [0, 1, 0, -1478], + [0, 0, 1, -1476], + [0, 0, 0, 1], + ], + }, + { + type: "affine" as const, + matrix: [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ], + }, + { + type: "affine" as const, + matrix: [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ], + }, + { + type: "affine" as const, + matrix: [ + [0, -1, 0, 0], + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ], + }, + { + type: "affine" as const, + matrix: [ + [1, 0, 0, 1529], + [0, 1, 0, 1478], + [0, 0, 1, 1476], + [0, 0, 0, 1], + ], + }, + ]; + + const rotationalTransform = combineCoordinateTransformations( + coordinateTransformations, + [11, 19, 28], + ); + + expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual([ + "PLANE_XY", + true, + [1, 0], + ]); + expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ + "PLANE_XZ", + false, + [1, 2], + ]); + expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ + "PLANE_YZ", + false, + [2, 0], + ]); + }); +}); diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 4ed275e124e..e0780b64c5b 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -4,14 +4,8 @@ import Toast from "libs/toast"; import _ from "lodash"; import messages from "messages"; import * as THREE from "three"; -import { type Euler, Matrix3, Vector3 as ThreeVector3 } from "three"; import type { OrthoView, Vector2, Vector3 } from "viewer/constants"; -import Constants, { - OrthoViews, - Vector3Indices, - Vector2Indices, - OrthoBaseRotations, -} from "viewer/constants"; +import Constants, { OrthoViews, Vector3Indices, Vector2Indices } from "viewer/constants"; import type { AnnotationTool } from "viewer/model/accessors/tool_accessor"; import { isBrushTool } from "viewer/model/accessors/tool_accessor"; import { getVolumeTracingById } from "viewer/model/accessors/volumetracing_accessor"; @@ -28,6 +22,7 @@ import { invertTransform, transformPointUnscaled, } from "../helpers/transformation_helpers"; +import { invertAndTranspose } from "../accessors/dataset_layer_transformation_accessor"; /* A VoxelBuffer2D instance holds a two dimensional slice @@ -194,7 +189,7 @@ class SectionLabeler { public readonly plane: OrthoView, thirdDimensionValue: number, public readonly activeMag: Vector3, - public readonly isFlipped: boolean, + public readonly isSwapped: boolean, ) { this.maxCoord = null; this.minCoord = null; @@ -204,7 +199,7 @@ class SectionLabeler { this.fast3DCoordinateFunction = getFast3DCoordinateFn( this.plane, this.thirdDimensionValue, - isFlipped, + isSwapped, ); } @@ -483,24 +478,12 @@ class SectionLabeler { let [scaleX, scaleY] = this.get2DCoordinate( getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale), ); - // if (window.wscale) { - // scaleX = window.wscale[0]; - // scaleY = window.wscale[1]; + // if (this.isSwapped) { + // [scaleX, scaleY] = [scaleY, scaleX]; // } - if (this.isFlipped) { - [scaleX, scaleY] = [scaleY, scaleX]; - } if (scale) { [scaleX, scaleY] = scale; } - if (window.wscale) { - // Original scale is 1, 1, 0.39 - // XY -> XZ -> 1, 1 - // XZ -> XY -> 1, 0.39285714285714285 - // YZ -> YZ _> 1, 0.39285714285714285 - [scaleX, scaleY] = window.wscale; - } - console.log("this.plane", this.plane); console.log(`scaleX=${scaleX}, scaleY=${scaleY}`); @@ -547,6 +530,7 @@ class SectionLabeler { // Throw out 'thirdCoordinate' which is always the same, anyway. const transposed = Dimensions.transDim(coord3d, plane ?? this.plane); return [transposed[0], transposed[1]]; + // return this.isSwapped ? [transposed[1], transposed[0]] : [transposed[0], transposed[1]]; } getUnzoomedCentroid(): Vector3 { @@ -587,52 +571,108 @@ class SectionLabeler { } } -function eulerToNormal(e: Euler): ThreeVector3 { - const m = new Matrix3().setFromMatrix4(new THREE.Matrix4().makeRotationFromEuler(e)); - const n = new ThreeVector3(0, 0, 1); - n.applyMatrix3(m).normalize(); - return n; -} - export function mapTransformedPlane( originalPlane: OrthoView, transform: Transform, -): [OrthoView, boolean] { - if (originalPlane === "PLANE_XY") { - return ["PLANE_XZ", false]; - } - if (originalPlane === "PLANE_XZ") { - return ["PLANE_XY", false]; - } - if (originalPlane === "PLANE_YZ") { - return ["PLANE_YZ", true]; - } - throw new Error("Unexpected input plane"); +): [OrthoView, boolean /* swapped */, (scale: Vector3) => Vector2 /* scaleAdaptFn */] { + const canonicalBases: Record< + OrthoView, + { u: THREE.Vector3; v: THREE.Vector3; n: THREE.Vector3 } + > = { + [OrthoViews.PLANE_XY]: { + u: new THREE.Vector3(1, 0, 0), + v: new THREE.Vector3(0, 1, 0), + n: new THREE.Vector3(0, 0, 1), + }, + [OrthoViews.PLANE_YZ]: { + u: new THREE.Vector3(0, 1, 0), + v: new THREE.Vector3(0, 0, 1), + n: new THREE.Vector3(1, 0, 0), + }, + [OrthoViews.PLANE_XZ]: { + u: new THREE.Vector3(1, 0, 0), + v: new THREE.Vector3(0, 0, 1), + n: new THREE.Vector3(0, -1, 0), + }, + }; + + const basis = canonicalBases[originalPlane]; + + const m = new THREE.Matrix4( + // @ts-ignore + ...invertAndTranspose(transform.affineMatrix), + ); - const originalNormal = eulerToNormal(OrthoBaseRotations[originalPlane]); - const transformedNormal = originalNormal - .clone() - .applyMatrix4(new THREE.Matrix4(...transform.affineMatrix)) - .normalize(); - - const canonical: Record = { - [OrthoViews.PLANE_XY]: new ThreeVector3(0, 0, 1), - [OrthoViews.PLANE_YZ]: new ThreeVector3(1, 0, 0), - [OrthoViews.PLANE_XZ]: new ThreeVector3(0, 1, 0), - [OrthoViews.TDView]: new ThreeVector3(1, 1, 1).normalize(), + console.log("base", basis); + + // transform each basis vector + const u2 = basis.u.clone().applyMatrix4(m).normalize(); + const v2 = basis.v.clone().applyMatrix4(m).normalize(); + const n2 = basis.n.clone().applyMatrix4(m).normalize(); + + console.log("transformed base", { u2, v2, n2 }); + + // find which canonical plane the transformed normal aligns with + const canonicalNormals: Record = { + [OrthoViews.PLANE_XY]: new THREE.Vector3(0, 0, 1), + [OrthoViews.PLANE_YZ]: new THREE.Vector3(1, 0, 0), + [OrthoViews.PLANE_XZ]: new THREE.Vector3(0, 1, 0), }; - let bestView = OrthoViews.PLANE_XY; + + let bestView: OrthoView = OrthoViews.PLANE_XY; let bestDot = Number.NEGATIVE_INFINITY; - for (const [view, normal] of Object.entries(canonical)) { - const dot = Math.abs(transformedNormal.dot(normal as ThreeVector3)); + for (const [view, normal] of Object.entries(canonicalNormals)) { + const dot = Math.abs(n2.dot(normal as THREE.Vector3)); if (dot > bestDot) { bestDot = dot; bestView = view as OrthoView; } } - return bestView; + // determine if u/v got swapped within the plane + // (we can check orientation: n2 should ≈ u2 × v2) + // const cross1 = basis.u.clone().cross(basis.v).normalize(); + // console.log("cross1", cross1); + // console.log("cross1.dot(basis.n)", cross1.dot(basis.n)); + + // const cross2 = u2.clone().cross(v2).normalize(); + // console.log("cross2", cross1); + // console.log("cross2.dot(n2)", cross2.dot(n2)); + // const swapped = Math.sign(cross1.dot(basis.n)) !== Math.sign(cross2.dot(n2)); + // const swapped = cross1.dot(cross2) < 0; + + // console.log("basis.u", basis.u); + // console.log("u2", u2); + const swapped = basis.u.dot(u2) === 0; + // console.log("v2.clone().normalize()", v2.clone().length()); + + const scaleAdaptFn = (scale: Vector3): Vector2 => { + const transposed = Dimensions.transDim(scale, originalPlane); + if (swapped) { + return [transposed[1], transposed[0]]; + } else { + return [transposed[0], transposed[1]]; + } + }; + + return [bestView, bestView === originalPlane && swapped, scaleAdaptFn]; +} + +export function mapTransformedPlane2( + originalPlane: OrthoView, + _transform: Transform, +): [OrthoView, boolean] { + if (originalPlane === "PLANE_XY") { + return ["PLANE_XY", true]; + } + if (originalPlane === "PLANE_XZ") { + return ["PLANE_YZ", true]; + } + if (originalPlane === "PLANE_YZ") { + return ["PLANE_XZ", true]; + } + throw new Error("Unexpected input plane"); } export class TransformedSectionLabeler { @@ -640,7 +680,8 @@ export class TransformedSectionLabeler { applyTransform: (pos: Vector3) => Vector3; applyInverseTransform: (pos: Vector3) => Vector3; readonly mappedPlane: OrthoView; - private readonly isFlipped: boolean; + private readonly isSwapped: boolean; + private scaleAdaptFn: (scale: Vector3) => Vector2; constructor( volumeTracingId: string, @@ -650,7 +691,10 @@ export class TransformedSectionLabeler { private readonly transform: Transform, ) { this.assertOrthogonalTransform(transform); - [this.mappedPlane, this.isFlipped] = mapTransformedPlane(originalPlane, transform); + [this.mappedPlane, this.isSwapped, this.scaleAdaptFn] = mapTransformedPlane( + originalPlane, + transform, + ); const thirdDimensionValue = getThirdDimValue( Dimensions.thirdDimensionForPlane(this.mappedPlane), @@ -662,7 +706,7 @@ export class TransformedSectionLabeler { this.mappedPlane, thirdDimensionValue, activeMag, - this.isFlipped, + this.isSwapped, ); this.applyTransform = transformPointUnscaled(this.transform); @@ -723,15 +767,14 @@ export class TransformedSectionLabeler { } getCircleVoxelBuffer2D(position: Vector3): VoxelBuffer2D { - // const p = this.applyTransform(position); + console.log( + "global scale:", + getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale), + ); - let scale = this.base.get2DCoordinate( + const scale = this.scaleAdaptFn( getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale), - this.originalPlane, ); - if (this.isFlipped) { - scale = [scale[1], scale[0]]; - } return this.base.getCircleVoxelBuffer2D(position, scale); } @@ -749,10 +792,10 @@ export class TransformedSectionLabeler { function getFast3DCoordinateFn( plane: OrthoView, thirdDimensionValue: number, - _isFlipped: boolean, + _isSwapped: boolean, ): (coordX: number, coordY: number, out: Vector3 | Float32Array) => void { let [u, v, w] = Dimensions.getIndices(plane); - // if (isFlipped) { + // if (_isSwapped) { // [u, v] = [v, u]; // } return (coordX, coordY, out) => { From 4efe5940ec864b2d048d807e63af4e4c8146c284 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 11:40:55 +0100 Subject: [PATCH 17/37] clean up --- .../model/transformed_section_labeler.spec.ts | 81 ++++++++++++++++++- .../model/volumetracing/section_labeling.ts | 17 ++-- .../javascripts/viewer/view/statusbar.tsx | 3 +- 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/frontend/javascripts/test/model/transformed_section_labeler.spec.ts b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts index 8c09e522137..c4c9ed0d259 100644 --- a/frontend/javascripts/test/model/transformed_section_labeler.spec.ts +++ b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts @@ -1,8 +1,9 @@ import { getRotationalTransformation } from "dashboard/dataset/dataset_rotation_form_item"; -import { IdentityTransform, OrthoView } from "viewer/constants"; +import type { CoordinateTransformation } from "types/api_types"; +import { IdentityTransform, type OrthoView } from "viewer/constants"; import { combineCoordinateTransformations } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import BoundingBox from "viewer/model/bucket_data_handling/bounding_box"; -import { Transform } from "viewer/model/helpers/transformation_helpers"; +import type { Transform } from "viewer/model/helpers/transformation_helpers"; import { mapTransformedPlane as originalMapTransformedPlane } from "viewer/model/volumetracing/section_labeling"; import { describe, expect, it } from "vitest"; @@ -75,7 +76,7 @@ describe("TransformedSectionLabeler", () => { ], type: "affine" as const, }, - ]; + ] as CoordinateTransformation[]; const rotationalTransform = combineCoordinateTransformations( coordinateTransformations, @@ -146,7 +147,7 @@ describe("TransformedSectionLabeler", () => { [0, 0, 0, 1], ], }, - ]; + ] as CoordinateTransformation[]; const rotationalTransform = combineCoordinateTransformations( coordinateTransformations, @@ -169,4 +170,76 @@ describe("TransformedSectionLabeler", () => { [2, 0], ]); }); + + // Does not work yet + it.skip("[L4] Rotation by 90deg around all axes should be handled correctly", async () => { + const coordinateTransformations = [ + { + type: "affine", + matrix: [ + [1, 0, 0, -1529], + [0, 1, 0, -1478], + [0, 0, 1, -1476], + [0, 0, 0, 1], + ], + }, + { + type: "affine", + matrix: [ + [1, 0, 0, 0], + [0, 0, -1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + ], + }, + { + type: "affine", + matrix: [ + [0, 0, 1, 0], + [0, 1, 0, 0], + [-1, 0, 0, 0], + [0, 0, 0, 1], + ], + }, + { + type: "affine", + matrix: [ + [0, -1, 0, 0], + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ], + }, + { + type: "affine", + matrix: [ + [1, 0, 0, 1529], + [0, 1, 0, 1478], + [0, 0, 1, 1476], + [0, 0, 0, 1], + ], + }, + ] as CoordinateTransformation[]; + + const rotationalTransform = combineCoordinateTransformations( + coordinateTransformations, + [11, 19, 28], + ); + + expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual([ + "PLANE_YZ", + false, + [0, 1], + ]); + // expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ + // "PLANE_XY", + // false, + // [2, 1], + // ]); + // expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ + // "PLANE_XZ", + // true, + // [2, 0], + // ]); + }); }); diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index e0780b64c5b..00e32efbfd6 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -574,7 +574,7 @@ class SectionLabeler { export function mapTransformedPlane( originalPlane: OrthoView, transform: Transform, -): [OrthoView, boolean /* swapped */, (scale: Vector3) => Vector2 /* scaleAdaptFn */] { +): [OrthoView, boolean /* swapped */, (scale: Vector3) => Vector2 /* adaptScaleFn */] { const canonicalBases: Record< OrthoView, { u: THREE.Vector3; v: THREE.Vector3; n: THREE.Vector3 } @@ -642,12 +642,12 @@ export function mapTransformedPlane( // const swapped = Math.sign(cross1.dot(basis.n)) !== Math.sign(cross2.dot(n2)); // const swapped = cross1.dot(cross2) < 0; - // console.log("basis.u", basis.u); - // console.log("u2", u2); + console.log("basis.u", basis.u); + console.log("u2", u2); const swapped = basis.u.dot(u2) === 0; // console.log("v2.clone().normalize()", v2.clone().length()); - const scaleAdaptFn = (scale: Vector3): Vector2 => { + const adaptScaleFn = (scale: Vector3): Vector2 => { const transposed = Dimensions.transDim(scale, originalPlane); if (swapped) { return [transposed[1], transposed[0]]; @@ -656,7 +656,8 @@ export function mapTransformedPlane( } }; - return [bestView, bestView === originalPlane && swapped, scaleAdaptFn]; + // bestView === originalPlane is probably incorrect + return [bestView, bestView === originalPlane && swapped, adaptScaleFn]; } export function mapTransformedPlane2( @@ -681,7 +682,7 @@ export class TransformedSectionLabeler { applyInverseTransform: (pos: Vector3) => Vector3; readonly mappedPlane: OrthoView; private readonly isSwapped: boolean; - private scaleAdaptFn: (scale: Vector3) => Vector2; + private adaptScaleFn: (scale: Vector3) => Vector2; constructor( volumeTracingId: string, @@ -691,7 +692,7 @@ export class TransformedSectionLabeler { private readonly transform: Transform, ) { this.assertOrthogonalTransform(transform); - [this.mappedPlane, this.isSwapped, this.scaleAdaptFn] = mapTransformedPlane( + [this.mappedPlane, this.isSwapped, this.adaptScaleFn] = mapTransformedPlane( originalPlane, transform, ); @@ -772,7 +773,7 @@ export class TransformedSectionLabeler { getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale), ); - const scale = this.scaleAdaptFn( + let scale = this.adaptScaleFn( getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale), ); diff --git a/frontend/javascripts/viewer/view/statusbar.tsx b/frontend/javascripts/viewer/view/statusbar.tsx index adb2e7b319f..c6c89f98ba3 100644 --- a/frontend/javascripts/viewer/view/statusbar.tsx +++ b/frontend/javascripts/viewer/view/statusbar.tsx @@ -572,10 +572,11 @@ function SegmentAndMousePosition() { // For the sake of performance, it is isolated as a single component. const additionalCoordinates = useWkSelector((state) => state.flycam.additionalCoordinates); const isPlaneMode = useWkSelector((state) => getIsPlaneMode(state)); + const visibleSegmentationLayer = useWkSelector((state) => getVisibleSegmentationLayer(state)); const globalMousePositionRounded = useWkSelector(getGlobalMousePosition); const untransformedPos = - globalMousePositionRounded != null + globalMousePositionRounded != null && visibleSegmentationLayer != null ? getUntransformedSegmentationPosition(Store.getState(), globalMousePositionRounded) : [0, 0, 0]; From b37dc7f3c74985085adc7829aada3f98624d9b0c Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:13:38 +0100 Subject: [PATCH 18/37] clean up --- .../model/volumetracing/section_labeling.ts | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 00e32efbfd6..ab48b40477f 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -575,10 +575,10 @@ export function mapTransformedPlane( originalPlane: OrthoView, transform: Transform, ): [OrthoView, boolean /* swapped */, (scale: Vector3) => Vector2 /* adaptScaleFn */] { - const canonicalBases: Record< - OrthoView, - { u: THREE.Vector3; v: THREE.Vector3; n: THREE.Vector3 } - > = { + if (originalPlane === "TDView") { + throw new Error("Unexpected 3D view"); + } + const canonicalBases = { [OrthoViews.PLANE_XY]: { u: new THREE.Vector3(1, 0, 0), v: new THREE.Vector3(0, 1, 0), @@ -603,17 +603,12 @@ export function mapTransformedPlane( ...invertAndTranspose(transform.affineMatrix), ); - console.log("base", basis); - // transform each basis vector const u2 = basis.u.clone().applyMatrix4(m).normalize(); - const v2 = basis.v.clone().applyMatrix4(m).normalize(); const n2 = basis.n.clone().applyMatrix4(m).normalize(); - console.log("transformed base", { u2, v2, n2 }); - // find which canonical plane the transformed normal aligns with - const canonicalNormals: Record = { + const canonicalNormals = { [OrthoViews.PLANE_XY]: new THREE.Vector3(0, 0, 1), [OrthoViews.PLANE_YZ]: new THREE.Vector3(1, 0, 0), [OrthoViews.PLANE_XZ]: new THREE.Vector3(0, 1, 0), @@ -630,22 +625,7 @@ export function mapTransformedPlane( } } - // determine if u/v got swapped within the plane - // (we can check orientation: n2 should ≈ u2 × v2) - // const cross1 = basis.u.clone().cross(basis.v).normalize(); - // console.log("cross1", cross1); - // console.log("cross1.dot(basis.n)", cross1.dot(basis.n)); - - // const cross2 = u2.clone().cross(v2).normalize(); - // console.log("cross2", cross1); - // console.log("cross2.dot(n2)", cross2.dot(n2)); - // const swapped = Math.sign(cross1.dot(basis.n)) !== Math.sign(cross2.dot(n2)); - // const swapped = cross1.dot(cross2) < 0; - - console.log("basis.u", basis.u); - console.log("u2", u2); const swapped = basis.u.dot(u2) === 0; - // console.log("v2.clone().normalize()", v2.clone().length()); const adaptScaleFn = (scale: Vector3): Vector2 => { const transposed = Dimensions.transDim(scale, originalPlane); From 9d78d139f9e72319e273bbc4b44e9f90ef3e04ca Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:17:15 +0100 Subject: [PATCH 19/37] fix tests --- .../test/sagas/volumetracing/volumetracing_saga.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts index 5b91e6fec19..273b855cc8d 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts @@ -163,6 +163,7 @@ describe("VolumeTracingSaga", () => { OrthoViews.PLANE_XY, 10, [1, 1, 1], + false, ); saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); @@ -218,6 +219,7 @@ describe("VolumeTracingSaga", () => { OrthoViews.PLANE_XY, 10, [1, 1, 1], + false, ); saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); @@ -286,6 +288,7 @@ describe("VolumeTracingSaga", () => { OrthoViews.PLANE_XY, 10, [1, 1, 1], + false, ); saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); From 346ae66c5cc8c0b2dd5357e68b62638dc2e2bc84 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:27:02 +0100 Subject: [PATCH 20/37] disable annotation for rotated layers again --- .../viewer/controller/scene_controller.ts | 2 +- .../model/accessors/disabled_tool_accessor.ts | 16 ++++++++-------- .../model/reducers/skeletontracing_reducer.ts | 9 ++++----- .../quick_select/quick_select_heuristic_saga.ts | 2 +- .../model/volumetracing/section_labeling.ts | 12 ------------ 5 files changed, 14 insertions(+), 27 deletions(-) diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index 158c4abf850..3dff65a6d55 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -432,7 +432,7 @@ class SceneController { if (this.splitBoundaryMesh != null) { this.splitBoundaryMesh.visible = id === OrthoViews.TDView; } - this.annotationToolsGeometryGroup.visible = true; // todop id !== OrthoViews.TDView; + this.annotationToolsGeometryGroup.visible = id !== OrthoViews.TDView; this.lineMeasurementGeometry.updateForCam(id); const originalPosition = getPosition(flycam); diff --git a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts index e5e0252b778..92318781f78 100644 --- a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts @@ -19,6 +19,8 @@ import { isVolumeAnnotationDisallowedForZoom, } from "viewer/model/accessors/volumetracing_accessor"; import { AnnotationTool, type AnnotationToolId } from "./tool_accessor"; +import { getTransformsPerLayer } from "./dataset_layer_transformation_accessor"; +import { IdentityTransform } from "viewer/constants"; export type DisabledInfo = { isDisabled: boolean; @@ -331,12 +333,11 @@ function getDisabledVolumeInfo(state: WebknossosState) { const labeledMag = getRenderableMagForSegmentationTracing(state, segmentationTracingLayer)?.mag; const isSegmentationTracingVisibleForMag = labeledMag != null; const visibleSegmentationLayer = getVisibleSegmentationLayer(state); - const isSegmentationTracingTransformed = false; - // todop: set to true if not a 90 deg rotation - // segmentationTracingLayer != null && - // getTransformsPerLayer(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName)[ - // segmentationTracingLayer.tracingId - // ] !== IdentityTransform; + const isSegmentationTracingTransformed = + segmentationTracingLayer != null && + getTransformsPerLayer(state.dataset, state.datasetConfiguration.nativelyRenderedLayerName)[ + segmentationTracingLayer.tracingId + ] !== IdentityTransform; const isSegmentationTracingVisible = segmentationTracingLayer != null && visibleSegmentationLayer != null && @@ -396,8 +397,7 @@ const _getDisabledInfoForTools = ( const { annotation } = state; const hasSkeleton = annotation.skeleton != null; const isFlycamRotated = isRotated(state.flycam); - // todop: check for 90 deg rotations - const geometriesTransformed = false; // areGeometriesTransformed(state); + const geometriesTransformed = areGeometriesTransformed(state); const areaMeasurementToolInfo = getAreaMeasurementToolInfo(isFlycamRotated); const skeletonToolInfo = getSkeletonToolInfo( hasSkeleton, diff --git a/frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts b/frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts index 03a55155f36..87e62abf978 100644 --- a/frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts @@ -686,11 +686,10 @@ function SkeletonTracingReducer( switch (action.type) { case "CREATE_NODE": { - // todop - // if (areGeometriesTransformed(state)) { - // // Don't create nodes if the skeleton layer is rendered with transforms. - // return state; - // } + if (areGeometriesTransformed(state)) { + // Don't create nodes if the skeleton layer is rendered with transforms. + return state; + } const { position, rotation, viewport, mag, treeId, timestamp, additionalCoordinates } = action; diff --git a/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts b/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts index c8b003cb226..3fb606df5e7 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/quick_select/quick_select_heuristic_saga.ts @@ -517,7 +517,7 @@ export function* finalizeQuickSelectForSlice( volumeTracing, activeViewport, labeledMag, - () => w, // todop + () => w, ); const sizeUVWInMag = mask.shape; const voxelBuffer2D = sectionLabeler.createVoxelBuffer2D( diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index ab48b40477f..8079117e8ac 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -671,7 +671,6 @@ export class TransformedSectionLabeler { activeMag: Vector3, private readonly transform: Transform, ) { - this.assertOrthogonalTransform(transform); [this.mappedPlane, this.isSwapped, this.adaptScaleFn] = mapTransformedPlane( originalPlane, transform, @@ -704,17 +703,6 @@ export class TransformedSectionLabeler { // return list.map((v) => this.applyInverseTransform(v)); // } - private assertOrthogonalTransform(_m: Transform): void { - // todop - // Quick check for orthogonal ±1 rotation/flip matrices - // const shouldBeIdentity = Transform.multiply(m, Transform.transpose(m)); - // const isOrtho = Transform.isCloseToIdentity(shouldBeIdentity); - // const hasValidEntries = m.flat().every((x) => Math.abs(x) === 0 || Math.abs(x) === 1); - // if (!isOrtho || !hasValidEntries) { - // throw new Error("Transformation matrix must be an orthogonal ±1 rotation/flip/scale matrix"); - // } - } - // --- Delegated methods with coordinate adaptation --- createVoxelBuffer2D(minCoord2d: Vector2, width: number, height: number, fillValue: number = 0) { From 42431fa7ef6f6d6293174d8e7884b07d60c9c6cc Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:27:56 +0100 Subject: [PATCH 21/37] more clean up --- .../model/volumetracing/section_labeling.ts | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 8079117e8ac..76fe4c0adc2 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -478,14 +478,10 @@ class SectionLabeler { let [scaleX, scaleY] = this.get2DCoordinate( getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale), ); - // if (this.isSwapped) { - // [scaleX, scaleY] = [scaleY, scaleX]; - // } + if (scale) { [scaleX, scaleY] = scale; } - console.log("this.plane", this.plane); - console.log(`scaleX=${scaleX}, scaleY=${scaleY}`); const setMap = (x: number, y: number) => { buffer2D.setValue(x, y, 1); @@ -666,7 +662,7 @@ export class TransformedSectionLabeler { constructor( volumeTracingId: string, - private readonly originalPlane: OrthoView, + originalPlane: OrthoView, getThirdDimValue: (thirdDim: number) => number, activeMag: Vector3, private readonly transform: Transform, @@ -693,16 +689,6 @@ export class TransformedSectionLabeler { this.applyInverseTransform = transformPointUnscaled(invertTransform(this.transform)); } - // --- Core coordinate helpers --- - - // private applyTransformList(list: Vector3[]): Vector3[] { - // return list.map((v) => this.applyTransform(v)); - // } - - // private applyInverseTransformList(list: Vector3[]): Vector3[] { - // return list.map((v) => this.applyInverseTransform(v)); - // } - // --- Delegated methods with coordinate adaptation --- createVoxelBuffer2D(minCoord2d: Vector2, width: number, height: number, fillValue: number = 0) { @@ -736,11 +722,6 @@ export class TransformedSectionLabeler { } getCircleVoxelBuffer2D(position: Vector3): VoxelBuffer2D { - console.log( - "global scale:", - getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale), - ); - let scale = this.adaptScaleFn( getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale), ); @@ -764,9 +745,6 @@ function getFast3DCoordinateFn( _isSwapped: boolean, ): (coordX: number, coordY: number, out: Vector3 | Float32Array) => void { let [u, v, w] = Dimensions.getIndices(plane); - // if (_isSwapped) { - // [u, v] = [v, u]; - // } return (coordX, coordY, out) => { out[u] = coordX; out[v] = coordY; From e958267cfd9036dd0a5c53c3028733e931947662 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:28:35 +0100 Subject: [PATCH 22/37] add missing import --- .../viewer/model/accessors/disabled_tool_accessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts index 92318781f78..a6b12022b69 100644 --- a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts @@ -9,7 +9,7 @@ import { getVisibleSegmentationLayer } from "viewer/model/accessors/dataset_acce import { isMagRestrictionViolated, isRotated } from "viewer/model/accessors/flycam_accessor"; import type { WebknossosState } from "viewer/store"; import { reuseInstanceOnEquality } from "./accessor_helpers"; -import { isSkeletonLayerVisible } from "./skeletontracing_accessor"; +import { areGeometriesTransformed, isSkeletonLayerVisible } from "./skeletontracing_accessor"; import { type AgglomerateState, From b92cd534e6f8945439a7ddba793815265ed7f956 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:28:53 +0100 Subject: [PATCH 23/37] format --- .../dashboard/dataset/dataset_rotation_form_item.tsx | 2 +- .../viewer/model/accessors/disabled_tool_accessor.ts | 4 ++-- .../javascripts/viewer/model/sagas/volumetracing_saga.tsx | 2 +- .../viewer/model/volumetracing/section_labeling.ts | 2 +- frontend/javascripts/viewer/view/statusbar.tsx | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx b/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx index 38706896dc3..56a5fc20a2d 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx @@ -3,7 +3,7 @@ import { Col, Form, type FormInstance, InputNumber, Row, Slider, Tooltip, Typogr import FormItem from "antd/es/form/FormItem"; import Checkbox, { type CheckboxChangeEvent } from "antd/lib/checkbox/Checkbox"; import { useCallback, useEffect, useMemo } from "react"; -import type { AffineTransformation, APIDataLayer } from "types/api_types"; +import type { APIDataLayer, AffineTransformation } from "types/api_types"; import { AXIS_TO_TRANSFORM_INDEX, EXPECTED_TRANSFORMATION_LENGTH, diff --git a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts index a6b12022b69..3a2e548a34e 100644 --- a/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/disabled_tool_accessor.ts @@ -11,6 +11,7 @@ import type { WebknossosState } from "viewer/store"; import { reuseInstanceOnEquality } from "./accessor_helpers"; import { areGeometriesTransformed, isSkeletonLayerVisible } from "./skeletontracing_accessor"; +import { IdentityTransform } from "viewer/constants"; import { type AgglomerateState, getActiveSegmentationTracing, @@ -18,9 +19,8 @@ import { hasAgglomerateMapping, isVolumeAnnotationDisallowedForZoom, } from "viewer/model/accessors/volumetracing_accessor"; -import { AnnotationTool, type AnnotationToolId } from "./tool_accessor"; import { getTransformsPerLayer } from "./dataset_layer_transformation_accessor"; -import { IdentityTransform } from "viewer/constants"; +import { AnnotationTool, type AnnotationToolId } from "./tool_accessor"; export type DisabledInfo = { isDisabled: boolean; diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index de2a62c0fa3..a8a6350609d 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -2,7 +2,7 @@ import { diffDiffableMaps } from "libs/diffable_map"; import { V3 } from "libs/mjs"; import Toast from "libs/toast"; import memoizeOne from "memoize-one"; -import type { ContourMode, OrthoView, OverwriteMode } from "viewer/constants"; +import type { ContourMode, OverwriteMode } from "viewer/constants"; import { ContourModeEnum, OrthoViews, OverwriteModeEnum } from "viewer/constants"; import getSceneController from "viewer/controller/scene_controller_provider"; import { CONTOUR_COLOR_DELETE, CONTOUR_COLOR_NORMAL } from "viewer/geometries/helper_geometries"; diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 76fe4c0adc2..1eaef847fd1 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -17,12 +17,12 @@ import { } from "viewer/model/helpers/position_converter"; import { getBaseVoxelFactorsInUnit } from "viewer/model/scaleinfo"; import Store from "viewer/store"; +import { invertAndTranspose } from "../accessors/dataset_layer_transformation_accessor"; import { type Transform, invertTransform, transformPointUnscaled, } from "../helpers/transformation_helpers"; -import { invertAndTranspose } from "../accessors/dataset_layer_transformation_accessor"; /* A VoxelBuffer2D instance holds a two dimensional slice diff --git a/frontend/javascripts/viewer/view/statusbar.tsx b/frontend/javascripts/viewer/view/statusbar.tsx index c6c89f98ba3..aa35b743087 100644 --- a/frontend/javascripts/viewer/view/statusbar.tsx +++ b/frontend/javascripts/viewer/view/statusbar.tsx @@ -16,6 +16,7 @@ import { type ActionDescriptor, getToolControllerForAnnotationTool, } from "viewer/controller/combinations/tool_controls"; +import { getUntransformedSegmentationPosition } from "viewer/controller/combinations/volume_handlers"; import { getMappingInfoOrNull, getVisibleSegmentationLayer, @@ -42,7 +43,6 @@ import { getGlobalDataConnectionInfo } from "viewer/model/data_connection_info"; import { Store } from "viewer/singletons"; import { NumberInputPopoverSetting } from "viewer/view/components/setting_input_views"; import { CommandPalette } from "./components/command_palette"; -import { getUntransformedSegmentationPosition } from "viewer/controller/combinations/volume_handlers"; const lineColor = "rgba(255, 255, 255, 0.67)"; const moreIconStyle = { From e816c6f4ab6c253208064eea26a7446831287270 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:31:06 +0100 Subject: [PATCH 24/37] remove layer pos from status bar again --- frontend/javascripts/viewer/view/statusbar.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/javascripts/viewer/view/statusbar.tsx b/frontend/javascripts/viewer/view/statusbar.tsx index aa35b743087..610ec8f43be 100644 --- a/frontend/javascripts/viewer/view/statusbar.tsx +++ b/frontend/javascripts/viewer/view/statusbar.tsx @@ -572,14 +572,8 @@ function SegmentAndMousePosition() { // For the sake of performance, it is isolated as a single component. const additionalCoordinates = useWkSelector((state) => state.flycam.additionalCoordinates); const isPlaneMode = useWkSelector((state) => getIsPlaneMode(state)); - const visibleSegmentationLayer = useWkSelector((state) => getVisibleSegmentationLayer(state)); const globalMousePositionRounded = useWkSelector(getGlobalMousePosition); - const untransformedPos = - globalMousePositionRounded != null && visibleSegmentationLayer != null - ? getUntransformedSegmentationPosition(Store.getState(), globalMousePositionRounded) - : [0, 0, 0]; - return ( <> {isPlaneMode ? : null} @@ -589,7 +583,7 @@ function SegmentAndMousePosition() { {globalMousePositionRounded ? getPosString(globalMousePositionRounded, additionalCoordinates) : "-,-,-"} - ] Layer Pos [{untransformedPos.join(", ")}] + ] ) : null} From 520ca702b18333c6360ad507f1b92d1778e4e4cd Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:31:14 +0100 Subject: [PATCH 25/37] remove more console.logs --- .../javascripts/viewer/model/bucket_data_handling/bucket.ts | 1 - frontend/javascripts/viewer/model/sagas/volume/helpers.ts | 6 ------ 2 files changed, 7 deletions(-) diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts b/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts index b60bc36382a..5f9eb805c61 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/bucket.ts @@ -567,7 +567,6 @@ export class DataBucket { if (shouldOverwrite || (!shouldOverwrite && currentSegmentId === overwritableValue)) { data[voxelAddress] = segmentId; - // console.log("out coord", out); wroteVoxels = true; } } diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index fd78499cd5d..1658f6596f8 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -206,9 +206,6 @@ export function* labelWithVoxelBuffer2D( max: bottomRight3DCoord, }); - console.log("topLeft3DCoord", topLeft3DCoord); - console.log("bottomRight3DCoord", bottomRight3DCoord); - for (const boundingBoxChunk of outerBoundingBox.chunkIntoBuckets()) { const { min, max } = boundingBoxChunk; const bucketZoomedAddress = zoomedPositionToZoomedAddress( @@ -224,15 +221,12 @@ export function* labelWithVoxelBuffer2D( const labelMapOfBucket = new Uint8Array(Constants.BUCKET_WIDTH ** 2); currentLabeledVoxelMap.set(bucketZoomedAddress, labelMapOfBucket); - // voxelBuffer.print(); - // globalA (first dim) and globalB (second dim) are global coordinates // which can be used to index into the 2D slice of the VoxelBuffer2D (when subtracting the minCoord2d) // and the LabeledVoxelMap for (let globalA = min[dimensionIndices[0]]; globalA < max[dimensionIndices[0]]; globalA++) { for (let globalB = min[dimensionIndices[1]]; globalB < max[dimensionIndices[1]]; globalB++) { if (voxelBuffer.getValueFromGlobal(globalA, globalB)) { - // console.log("setting 1 at", globalA, globalB); labelMapOfBucket[ (globalA % Constants.BUCKET_WIDTH) * Constants.BUCKET_WIDTH + (globalB % Constants.BUCKET_WIDTH) From c624d9b823afe35dd45fa995a80e5f7696efa15e Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:31:47 +0100 Subject: [PATCH 26/37] remove import --- frontend/javascripts/viewer/view/statusbar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/javascripts/viewer/view/statusbar.tsx b/frontend/javascripts/viewer/view/statusbar.tsx index 610ec8f43be..a0c6d95b00d 100644 --- a/frontend/javascripts/viewer/view/statusbar.tsx +++ b/frontend/javascripts/viewer/view/statusbar.tsx @@ -16,7 +16,6 @@ import { type ActionDescriptor, getToolControllerForAnnotationTool, } from "viewer/controller/combinations/tool_controls"; -import { getUntransformedSegmentationPosition } from "viewer/controller/combinations/volume_handlers"; import { getMappingInfoOrNull, getVisibleSegmentationLayer, From 227bc6171543508dd2ceec8300071ab3afaec850 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:55:37 +0100 Subject: [PATCH 27/37] clean up --- .../viewer/model/volumetracing/section_labeling.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 1eaef847fd1..f8b62ad578d 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -522,11 +522,10 @@ class SectionLabeler { Drawing.fillArea(0, 0, width, height, false, isEmpty, setMap); } - public get2DCoordinate(coord3d: Vector3, plane?: OrthoView): Vector2 { + public get2DCoordinate(coord3d: Vector3): Vector2 { // Throw out 'thirdCoordinate' which is always the same, anyway. - const transposed = Dimensions.transDim(coord3d, plane ?? this.plane); + const transposed = Dimensions.transDim(coord3d, this.plane); return [transposed[0], transposed[1]]; - // return this.isSwapped ? [transposed[1], transposed[0]] : [transposed[0], transposed[1]]; } getUnzoomedCentroid(): Vector3 { From 204d929d319f9817eb63af41137e65e6668a5518 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Nov 2025 14:57:45 +0100 Subject: [PATCH 28/37] refactor --- .../model/volumetracing/section_labeling.ts | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index f8b62ad578d..4808435df2b 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -566,6 +566,29 @@ class SectionLabeler { } } +const CANONICAL_BASES = { + [OrthoViews.PLANE_XY]: { + u: new THREE.Vector3(1, 0, 0), + v: new THREE.Vector3(0, 1, 0), + n: new THREE.Vector3(0, 0, 1), + }, + [OrthoViews.PLANE_YZ]: { + u: new THREE.Vector3(0, 1, 0), + v: new THREE.Vector3(0, 0, 1), + n: new THREE.Vector3(1, 0, 0), + }, + [OrthoViews.PLANE_XZ]: { + u: new THREE.Vector3(1, 0, 0), + v: new THREE.Vector3(0, 0, 1), + n: new THREE.Vector3(0, -1, 0), + }, +}; +const CANONICAL_NORMALS = { + [OrthoViews.PLANE_XY]: new THREE.Vector3(0, 0, 1), + [OrthoViews.PLANE_YZ]: new THREE.Vector3(1, 0, 0), + [OrthoViews.PLANE_XZ]: new THREE.Vector3(0, 1, 0), +}; + export function mapTransformedPlane( originalPlane: OrthoView, transform: Transform, @@ -573,25 +596,8 @@ export function mapTransformedPlane( if (originalPlane === "TDView") { throw new Error("Unexpected 3D view"); } - const canonicalBases = { - [OrthoViews.PLANE_XY]: { - u: new THREE.Vector3(1, 0, 0), - v: new THREE.Vector3(0, 1, 0), - n: new THREE.Vector3(0, 0, 1), - }, - [OrthoViews.PLANE_YZ]: { - u: new THREE.Vector3(0, 1, 0), - v: new THREE.Vector3(0, 0, 1), - n: new THREE.Vector3(1, 0, 0), - }, - [OrthoViews.PLANE_XZ]: { - u: new THREE.Vector3(1, 0, 0), - v: new THREE.Vector3(0, 0, 1), - n: new THREE.Vector3(0, -1, 0), - }, - }; - const basis = canonicalBases[originalPlane]; + const basis = CANONICAL_BASES[originalPlane]; const m = new THREE.Matrix4( // @ts-ignore @@ -603,16 +609,11 @@ export function mapTransformedPlane( const n2 = basis.n.clone().applyMatrix4(m).normalize(); // find which canonical plane the transformed normal aligns with - const canonicalNormals = { - [OrthoViews.PLANE_XY]: new THREE.Vector3(0, 0, 1), - [OrthoViews.PLANE_YZ]: new THREE.Vector3(1, 0, 0), - [OrthoViews.PLANE_XZ]: new THREE.Vector3(0, 1, 0), - }; let bestView: OrthoView = OrthoViews.PLANE_XY; let bestDot = Number.NEGATIVE_INFINITY; - for (const [view, normal] of Object.entries(canonicalNormals)) { + for (const [view, normal] of Object.entries(CANONICAL_NORMALS)) { const dot = Math.abs(n2.dot(normal as THREE.Vector3)); if (dot > bestDot) { bestDot = dot; @@ -631,7 +632,6 @@ export function mapTransformedPlane( } }; - // bestView === originalPlane is probably incorrect return [bestView, bestView === originalPlane && swapped, adaptScaleFn]; } @@ -688,8 +688,6 @@ export class TransformedSectionLabeler { this.applyInverseTransform = transformPointUnscaled(invertTransform(this.transform)); } - // --- Delegated methods with coordinate adaptation --- - createVoxelBuffer2D(minCoord2d: Vector2, width: number, height: number, fillValue: number = 0) { return this.base.createVoxelBuffer2D(minCoord2d, width, height, fillValue); } @@ -703,7 +701,6 @@ export class TransformedSectionLabeler { } getFillingVoxelBuffer2D(mode: AnnotationTool): VoxelBuffer2D { - // Buffer orientation could be left unchanged unless 2D plane transforms are needed. return this.base.getFillingVoxelBuffer2D(mode); } From 668d77bd6720c4d3db9b452c622b26494f1da228 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 7 Nov 2025 10:18:00 +0100 Subject: [PATCH 29/37] fix tests --- .../volumetracing/volumetracing_saga.spec.ts | 23 +++++++++++++++---- .../viewer/model/sagas/volume/helpers.ts | 13 ++++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts index 273b855cc8d..1dda32941e8 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts @@ -1,3 +1,5 @@ +import update from "immutability-helper"; + import { it, expect, describe, beforeEach, afterEach } from "vitest"; import { setupWebknossosForTesting, type WebknossosTestContext } from "test/helpers/apiHelpers"; import { take, put, call } from "redux-saga/effects"; @@ -22,6 +24,8 @@ import { serverVolumeToClientVolumeTracing } from "viewer/model/reducers/volumet import { Model, Store } from "viewer/singletons"; import { hasRootSagaCrashed } from "viewer/model/sagas/root_saga"; import { tracing as serverVolumeTracing } from "test/fixtures/volumetracing_server_objects"; +import { sampleTracingLayer } from "test/fixtures/dataset_server_object"; +import { initialState as defaultVolumeState } from "test/fixtures/volumetracing_object"; const volumeTracing = serverVolumeToClientVolumeTracing(serverVolumeTracing, null, null); @@ -42,6 +46,14 @@ const startEditingAction = VolumeTracingActions.startEditingAction([0, 0, 0], Or const addToContourListActionFn = VolumeTracingActions.addToContourListAction; const finishEditingAction = VolumeTracingActions.finishEditingAction(); +const mockedDataset = update(defaultVolumeState.dataset, { + dataSource: { + dataLayers: { + $set: [sampleTracingLayer], + }, + }, +}); + describe("VolumeTracingSaga", () => { describe("With Saga Middleware", () => { beforeEach(async (context) => { @@ -114,12 +126,13 @@ describe("VolumeTracingSaga", () => { ), ), ); - const startEditingSaga = execCall(expect, saga.next()); - startEditingSaga.next(); + const createSectionLabelerSaga = execCall(expect, saga.next()); + createSectionLabelerSaga.next(); // kick off saga + createSectionLabelerSaga.next(mockedDataset); - // Pass position - const layer = startEditingSaga.next([1, 1, 1]).value; - expect(layer.plane).toBe(OrthoViews.PLANE_XY); + // Pass datasource config + const labeller = createSectionLabelerSaga.next({ nativelyRenderedLayerName: undefined }).value; + expect(labeller.getPlane()).toBe(OrthoViews.PLANE_XY); }); it("should add values to volume layer (saga test)", () => { diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index 1658f6596f8..9564d9245ae 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -32,7 +32,7 @@ import { import sampleVoxelMapToMagnification, { applyVoxelMap, } from "viewer/model/volumetracing/volume_annotation_sampling"; -import { Model, Store } from "viewer/singletons"; +import { Model } from "viewer/singletons"; import type { BoundingBoxObject, VolumeTracing } from "viewer/store"; function* pairwise(arr: Array): Generator<[T, T], any, any> { @@ -279,15 +279,16 @@ export function* labelWithVoxelBuffer2D( } } -export function createSectionLabeler( +export function* createSectionLabeler( volumeTracing: VolumeTracing, planeId: OrthoView, labeledMags: Vector3, getThirdDimValue: (thirdDim: number) => number, -): SectionLabeler | TransformedSectionLabeler { - const state = Store.getState(); - const { dataset } = state; - const { nativelyRenderedLayerName } = state.datasetConfiguration; +): Saga { + const dataset = yield* select((state) => state.dataset); + const datasetConfiguration = yield* select((state) => state.datasetConfiguration); + + const { nativelyRenderedLayerName } = datasetConfiguration; const layer = getLayerByName(dataset, volumeTracing.tracingId); const segmentationTransforms = getTransformsForLayer(dataset, layer, nativelyRenderedLayerName); From 60992beb58c87c18f99cba7d2b54cef9ef212c6f Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 10 Nov 2025 14:53:28 +0100 Subject: [PATCH 30/37] incorporate some feedback --- .../model/volumetracing/section_labeling.ts | 40 +++++-------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 4808435df2b..37ed61981fa 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -196,11 +196,7 @@ class SectionLabeler { const thirdDim = Dimensions.thirdDimensionForPlane(this.plane); this.thirdDimensionValue = Math.floor(thirdDimensionValue / this.activeMag[thirdDim]); - this.fast3DCoordinateFunction = getFast3DCoordinateFn( - this.plane, - this.thirdDimensionValue, - isSwapped, - ); + this.fast3DCoordinateFunction = getFast3DCoordinateFn(this.plane, this.thirdDimensionValue); } updateArea(globalPos: Vector3): void { @@ -475,13 +471,8 @@ class SectionLabeler { ]; const buffer2D = this.createVoxelBuffer2D(minCoord2d, width, height); // Use the baseVoxelFactors to scale the circle, otherwise it'll become an ellipse - let [scaleX, scaleY] = this.get2DCoordinate( - getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale), - ); - - if (scale) { - [scaleX, scaleY] = scale; - } + const [scaleX, scaleY] = + scale ?? this.get2DCoordinate(getBaseVoxelFactorsInUnit(state.dataset.dataSource.scale)); const setMap = (x: number, y: number) => { buffer2D.setValue(x, y, 1); @@ -589,6 +580,10 @@ const CANONICAL_NORMALS = { [OrthoViews.PLANE_XZ]: new THREE.Vector3(0, 1, 0), }; +function isAlmostZero(num: number, threshold: number = 0.01) { + return Math.abs(num) < threshold; +} + export function mapTransformedPlane( originalPlane: OrthoView, transform: Transform, @@ -604,7 +599,7 @@ export function mapTransformedPlane( ...invertAndTranspose(transform.affineMatrix), ); - // transform each basis vector + // transform basis vectors const u2 = basis.u.clone().applyMatrix4(m).normalize(); const n2 = basis.n.clone().applyMatrix4(m).normalize(); @@ -621,7 +616,7 @@ export function mapTransformedPlane( } } - const swapped = basis.u.dot(u2) === 0; + const swapped = isAlmostZero(basis.u.dot(u2)); const adaptScaleFn = (scale: Vector3): Vector2 => { const transposed = Dimensions.transDim(scale, originalPlane); @@ -635,22 +630,6 @@ export function mapTransformedPlane( return [bestView, bestView === originalPlane && swapped, adaptScaleFn]; } -export function mapTransformedPlane2( - originalPlane: OrthoView, - _transform: Transform, -): [OrthoView, boolean] { - if (originalPlane === "PLANE_XY") { - return ["PLANE_XY", true]; - } - if (originalPlane === "PLANE_XZ") { - return ["PLANE_YZ", true]; - } - if (originalPlane === "PLANE_YZ") { - return ["PLANE_XZ", true]; - } - throw new Error("Unexpected input plane"); -} - export class TransformedSectionLabeler { private readonly base: SectionLabeler; applyTransform: (pos: Vector3) => Vector3; @@ -738,7 +717,6 @@ export class TransformedSectionLabeler { function getFast3DCoordinateFn( plane: OrthoView, thirdDimensionValue: number, - _isSwapped: boolean, ): (coordX: number, coordY: number, out: Vector3 | Float32Array) => void { let [u, v, w] = Dimensions.getIndices(plane); return (coordX, coordY, out) => { From 0cbd82ec4a010d7e1dac9ba38dc807e269e5795c Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 10 Nov 2025 15:08:33 +0100 Subject: [PATCH 31/37] fix layer vs world space --- .../combinations/volume_handlers.ts | 56 +++++++++---------- frontend/javascripts/viewer/store.ts | 2 +- .../segments_tab/segments_view.tsx | 4 +- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts index a4b13a877a3..7d04fb7a005 100644 --- a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts @@ -11,7 +11,10 @@ import { getTransformsForLayer, globalToLayerTransformedPosition, } from "viewer/model/accessors/dataset_layer_transformation_accessor"; -import { calculateGlobalPos } from "viewer/model/accessors/view_mode_accessor"; +import { + calculateGlobalPos, + PositionWithRounding, +} from "viewer/model/accessors/view_mode_accessor"; import { updateUserSettingAction } from "viewer/model/actions/settings_actions"; import { addToContourListAction, @@ -33,11 +36,11 @@ import type { WebknossosState } from "viewer/store"; export function handleDrawStart(pos: Point2, plane: OrthoView) { const state = Store.getState(); const globalPosRounded = calculateGlobalPos(state, pos).rounded; - const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); + const layerPos = getUntransformedSegmentationPosition(state, globalPosRounded); Store.dispatch(setContourTracingModeAction(ContourModeEnum.DRAW)); - Store.dispatch(startEditingAction(untransformedPos, plane)); - Store.dispatch(addToContourListAction(untransformedPos)); + Store.dispatch(startEditingAction(layerPos, plane)); + Store.dispatch(addToContourListAction(layerPos)); } export function getUntransformedSegmentationPosition( @@ -59,25 +62,25 @@ export function getUntransformedSegmentationPosition( layer, nativelyRenderedLayerName, ); - const untransformedPos = transformPointUnscaled(invertTransform(segmentationTransforms))( + const layerPos = transformPointUnscaled(invertTransform(segmentationTransforms))( globalPosRounded, ); - return untransformedPos; + return layerPos; } export function handleEraseStart(pos: Point2, plane: OrthoView) { const state = Store.getState(); const globalPosRounded = calculateGlobalPos(state, pos).rounded; - const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); + const layerPos = getUntransformedSegmentationPosition(state, globalPosRounded); Store.dispatch(setContourTracingModeAction(ContourModeEnum.DELETE)); - Store.dispatch(startEditingAction(untransformedPos, plane)); + Store.dispatch(startEditingAction(layerPos, plane)); } export function handleMoveForDrawOrErase(pos: Point2) { const state = Store.getState(); const globalPosRounded = calculateGlobalPos(state, pos).rounded; - const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); - Store.dispatch(addToContourListAction(untransformedPos)); + const layerPos = getUntransformedSegmentationPosition(state, globalPosRounded); + Store.dispatch(addToContourListAction(layerPos)); } export function handleEndForDrawOrErase() { Store.dispatch(finishEditingAction()); @@ -85,13 +88,9 @@ export function handleEndForDrawOrErase() { } export function handlePickCell(pos: Point2) { const state = Store.getState(); - const globalPosRounded = calculateGlobalPos(state, pos).rounded; - const untransformedPos = getUntransformedSegmentationPosition(state, globalPosRounded); + const globalPos = calculateGlobalPos(state, pos); - return handlePickCellFromGlobalPosition( - untransformedPos, - state.flycam.additionalCoordinates || [], - ); + return handlePickCellFromGlobalPosition(globalPos, state.flycam.additionalCoordinates || []); } const _getSegmentIdForPosition = (mapped: boolean) => (globalPos: Vector3) => { @@ -217,27 +216,22 @@ export async function getSegmentIdForPositionAsync(globalPos: Vector3) { renderedZoomStepForCameraPosition, ); } -function handlePickCellFromGlobalPosition( - globalPos: Vector3, +export function handlePickCellFromGlobalPosition( + globalPos: PositionWithRounding, additionalCoordinates: AdditionalCoordinate[], ) { - const visibleSegmentationLayer = getVisibleSegmentationLayer(Store.getState()); - if (visibleSegmentationLayer == null) { - return; - } - const posInLayerSpace = globalToLayerTransformedPosition( - globalPos, - visibleSegmentationLayer.name, - "segmentation", - Store.getState(), - ); - - const segmentId = getSegmentIdForPosition(globalPos); + const segmentId = getSegmentIdForPosition(globalPos.rounded); if (segmentId === 0) { return; } - Store.dispatch(setActiveCellAction(segmentId, posInLayerSpace, additionalCoordinates)); + const visibleSegmentationLayer = getVisibleSegmentationLayer(Store.getState()); + if (visibleSegmentationLayer == null) { + return; + } + const state = Store.getState(); + const layerPos = getUntransformedSegmentationPosition(state, globalPos.floating); + Store.dispatch(setActiveCellAction(segmentId, layerPos, additionalCoordinates)); Store.dispatch( updateSegmentAction( diff --git a/frontend/javascripts/viewer/store.ts b/frontend/javascripts/viewer/store.ts index ea461507149..0735d340186 100644 --- a/frontend/javascripts/viewer/store.ts +++ b/frontend/javascripts/viewer/store.ts @@ -175,7 +175,7 @@ export type SkeletonTracing = TracingBase & { export type Segment = { readonly id: number; readonly name: string | null | undefined; - readonly somePosition: Vector3 | undefined; + readonly somePosition: Vector3 | undefined; // in layer space readonly someAdditionalCoordinates: AdditionalCoordinate[] | undefined | null; readonly creationTime: number | null | undefined; readonly color: Vector3 | null; diff --git a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segments_view.tsx index 70b122a0676..fe5878c84c8 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segments_view.tsx @@ -255,10 +255,10 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ setActiveCell( segmentId: number, - somePosition?: Vector3, + somePositionInLayerSpace?: Vector3, someAdditionalCoordinates?: AdditionalCoordinate[] | null, ) { - dispatch(setActiveCellAction(segmentId, somePosition, someAdditionalCoordinates)); + dispatch(setActiveCellAction(segmentId, somePositionInLayerSpace, someAdditionalCoordinates)); }, setCurrentMeshFile(layerName: string, fileName: string) { From ee102fedb8c0e930d8c4eb05bc1e0a294e6aec43 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 10 Nov 2025 15:37:09 +0100 Subject: [PATCH 32/37] misc --- frontend/javascripts/libs/drawing.ts | 2 +- .../model/transformed_section_labeler.spec.ts | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/javascripts/libs/drawing.ts b/frontend/javascripts/libs/drawing.ts index f58bd60254a..e0fa737fa16 100644 --- a/frontend/javascripts/libs/drawing.ts +++ b/frontend/javascripts/libs/drawing.ts @@ -206,7 +206,7 @@ class Drawing { while (ranges.length) { const r = ranges.pop(); if (r == null) { - throw new Error("Array is exptected to be not empty."); + throw new Error("Array is expected to be not empty."); } let minX = r[0]; let maxX = r[1]; diff --git a/frontend/javascripts/test/model/transformed_section_labeler.spec.ts b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts index c4c9ed0d259..440959d0a35 100644 --- a/frontend/javascripts/test/model/transformed_section_labeler.spec.ts +++ b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts @@ -171,7 +171,7 @@ describe("TransformedSectionLabeler", () => { ]); }); - // Does not work yet + // Todo #8965. Does not work yet it.skip("[L4] Rotation by 90deg around all axes should be handled correctly", async () => { const coordinateTransformations = [ { @@ -231,15 +231,15 @@ describe("TransformedSectionLabeler", () => { false, [0, 1], ]); - // expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ - // "PLANE_XY", - // false, - // [2, 1], - // ]); - // expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ - // "PLANE_XZ", - // true, - // [2, 0], - // ]); + expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ + "PLANE_XY", + false, + [2, 1], + ]); + expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ + "PLANE_XZ", + true, + [2, 0], + ]); }); }); From 0fb28a6dc567cfccfa00bcd5272741180bf7b314 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 10 Nov 2025 15:48:12 +0100 Subject: [PATCH 33/37] format --- .../viewer/controller/combinations/volume_handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts index 7d04fb7a005..bf453f76b70 100644 --- a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts @@ -12,8 +12,8 @@ import { globalToLayerTransformedPosition, } from "viewer/model/accessors/dataset_layer_transformation_accessor"; import { + type PositionWithRounding, calculateGlobalPos, - PositionWithRounding, } from "viewer/model/accessors/view_mode_accessor"; import { updateUserSettingAction } from "viewer/model/actions/settings_actions"; import { From 5f3b01fad4c98c9347148af5b1458b0237097303 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 9 Dec 2025 18:23:01 +0100 Subject: [PATCH 34/37] remove unused is swap property --- .../model/transformed_section_labeler.spec.ts | 82 ++++--------------- .../volumetracing/volumetracing_saga.spec.ts | 3 - .../model/volumetracing/section_labeling.ts | 12 +-- 3 files changed, 20 insertions(+), 77 deletions(-) diff --git a/frontend/javascripts/test/model/transformed_section_labeler.spec.ts b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts index 440959d0a35..1f5ec174b34 100644 --- a/frontend/javascripts/test/model/transformed_section_labeler.spec.ts +++ b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts @@ -8,16 +8,16 @@ import { mapTransformedPlane as originalMapTransformedPlane } from "viewer/model import { describe, expect, it } from "vitest"; const mapTransformedPlane = (plane: OrthoView, transform: Transform) => { - const [transformedPlane, isSwapped, adaptFn] = originalMapTransformedPlane(plane, transform); + const [transformedPlane, adaptFn] = originalMapTransformedPlane(plane, transform); const adaptedScale = adaptFn([0, 1, 2]); - return [transformedPlane, isSwapped, adaptedScale]; + return [transformedPlane, adaptedScale]; }; describe("TransformedSectionLabeler", () => { it("Identity transform should result in identity mapping of plane", async () => { - expect(mapTransformedPlane("PLANE_XY", IdentityTransform)).toEqual(["PLANE_XY", false, [0, 1]]); - expect(mapTransformedPlane("PLANE_YZ", IdentityTransform)).toEqual(["PLANE_YZ", false, [2, 1]]); - expect(mapTransformedPlane("PLANE_XZ", IdentityTransform)).toEqual(["PLANE_XZ", false, [0, 2]]); + expect(mapTransformedPlane("PLANE_XY", IdentityTransform)).toEqual(["PLANE_XY", [0, 1]]); + expect(mapTransformedPlane("PLANE_YZ", IdentityTransform)).toEqual(["PLANE_YZ", [2, 1]]); + expect(mapTransformedPlane("PLANE_XZ", IdentityTransform)).toEqual(["PLANE_XZ", [0, 2]]); }); it("Rotation by 90deg around X should be handled correctly", async () => { @@ -30,21 +30,9 @@ describe("TransformedSectionLabeler", () => { [1, 2, 3], ); - expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual([ - "PLANE_XZ", - false, - [0, 1], - ]); - expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ - "PLANE_YZ", - true, - [1, 2], - ]); - expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ - "PLANE_XY", - false, - [0, 2], - ]); + expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual(["PLANE_XZ", [0, 1]]); + expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual(["PLANE_YZ", [1, 2]]); + expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual(["PLANE_XY", [0, 2]]); }); it("[L4] Rotation by 90deg around X should be handled correctly", async () => { @@ -83,21 +71,9 @@ describe("TransformedSectionLabeler", () => { [11, 19, 28], ); - expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual([ - "PLANE_XZ", - false, - [0, 1], - ]); - expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ - "PLANE_YZ", - true, - [1, 2], - ]); - expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ - "PLANE_XY", - false, - [0, 2], - ]); + expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual(["PLANE_XZ", [0, 1]]); + expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual(["PLANE_YZ", [1, 2]]); + expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual(["PLANE_XY", [0, 2]]); }); it("[L4] Rotation by 90deg around Z should be handled correctly", async () => { @@ -154,21 +130,9 @@ describe("TransformedSectionLabeler", () => { [11, 19, 28], ); - expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual([ - "PLANE_XY", - true, - [1, 0], - ]); - expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ - "PLANE_XZ", - false, - [1, 2], - ]); - expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ - "PLANE_YZ", - false, - [2, 0], - ]); + expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual(["PLANE_XY", [1, 0]]); + expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual(["PLANE_XZ", [1, 2]]); + expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual(["PLANE_YZ", [2, 0]]); }); // Todo #8965. Does not work yet @@ -226,20 +190,8 @@ describe("TransformedSectionLabeler", () => { [11, 19, 28], ); - expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual([ - "PLANE_YZ", - false, - [0, 1], - ]); - expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual([ - "PLANE_XY", - false, - [2, 1], - ]); - expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual([ - "PLANE_XZ", - true, - [2, 0], - ]); + expect(mapTransformedPlane("PLANE_XY", rotationalTransform)).toEqual(["PLANE_YZ", [0, 1]]); + expect(mapTransformedPlane("PLANE_YZ", rotationalTransform)).toEqual(["PLANE_XY", [2, 1]]); + expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual(["PLANE_XZ", [2, 0]]); }); }); diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts index 3fc5dcc4fad..edb7c6d6c16 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts @@ -176,7 +176,6 @@ describe("VolumeTracingSaga", () => { OrthoViews.PLANE_XY, 10, [1, 1, 1], - false, ); saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); @@ -232,7 +231,6 @@ describe("VolumeTracingSaga", () => { OrthoViews.PLANE_XY, 10, [1, 1, 1], - false, ); saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); @@ -299,7 +297,6 @@ describe("VolumeTracingSaga", () => { OrthoViews.PLANE_XY, 10, [1, 1, 1], - false, ); saga.next(sectionLabeler); saga.next(OrthoViews.PLANE_XY); diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index 37ed61981fa..c595bbd38a2 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -189,7 +189,6 @@ class SectionLabeler { public readonly plane: OrthoView, thirdDimensionValue: number, public readonly activeMag: Vector3, - public readonly isSwapped: boolean, ) { this.maxCoord = null; this.minCoord = null; @@ -587,7 +586,7 @@ function isAlmostZero(num: number, threshold: number = 0.01) { export function mapTransformedPlane( originalPlane: OrthoView, transform: Transform, -): [OrthoView, boolean /* swapped */, (scale: Vector3) => Vector2 /* adaptScaleFn */] { +): [OrthoView, (scale: Vector3) => Vector2 /* adaptScaleFn */] { if (originalPlane === "TDView") { throw new Error("Unexpected 3D view"); } @@ -627,7 +626,7 @@ export function mapTransformedPlane( } }; - return [bestView, bestView === originalPlane && swapped, adaptScaleFn]; + return [bestView, adaptScaleFn]; } export class TransformedSectionLabeler { @@ -635,7 +634,6 @@ export class TransformedSectionLabeler { applyTransform: (pos: Vector3) => Vector3; applyInverseTransform: (pos: Vector3) => Vector3; readonly mappedPlane: OrthoView; - private readonly isSwapped: boolean; private adaptScaleFn: (scale: Vector3) => Vector2; constructor( @@ -645,10 +643,7 @@ export class TransformedSectionLabeler { activeMag: Vector3, private readonly transform: Transform, ) { - [this.mappedPlane, this.isSwapped, this.adaptScaleFn] = mapTransformedPlane( - originalPlane, - transform, - ); + [this.mappedPlane, this.adaptScaleFn] = mapTransformedPlane(originalPlane, transform); const thirdDimensionValue = getThirdDimValue( Dimensions.thirdDimensionForPlane(this.mappedPlane), @@ -660,7 +655,6 @@ export class TransformedSectionLabeler { this.mappedPlane, thirdDimensionValue, activeMag, - this.isSwapped, ); this.applyTransform = transformPointUnscaled(this.transform); From baa06b8688573f405e875ed6db36bac0daf44b88 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 10 Dec 2025 11:24:39 +0100 Subject: [PATCH 35/37] improve global vs layer space in naming --- .../volumetracing/volumetracing_saga.spec.ts | 8 +++--- .../controller/combinations/tool_controls.ts | 4 +-- .../combinations/volume_handlers.ts | 15 +++++++---- .../model/actions/volumetracing_actions.ts | 20 +++++++++------ .../model/reducers/volumetracing_reducer.ts | 2 +- .../reducers/volumetracing_reducer_helpers.ts | 4 +-- .../model/sagas/volume/floodfill_saga.tsx | 2 +- .../viewer/model/sagas/volume/helpers.ts | 3 ++- .../viewer/model/sagas/volumetracing_saga.tsx | 18 ++++++------- .../model/volumetracing/section_labeling.ts | 25 ++++++++++++++----- frontend/javascripts/viewer/store.ts | 2 +- .../javascripts/viewer/view/context_menu.tsx | 3 ++- 12 files changed, 65 insertions(+), 41 deletions(-) diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts index edb7c6d6c16..b23eb4a1ca7 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts @@ -119,7 +119,7 @@ describe("VolumeTracingSaga", () => { VolumeTracingActions.updateSegmentAction( ACTIVE_CELL_ID, { - somePosition: startEditingAction.position, + somePosition: startEditingAction.positionInLayerSpace, someAdditionalCoordinates: [], }, volumeTracing.tracingId, @@ -162,7 +162,7 @@ describe("VolumeTracingSaga", () => { VolumeTracingActions.updateSegmentAction( ACTIVE_CELL_ID, { - somePosition: startEditingAction.position, + somePosition: startEditingAction.positionInLayerSpace, someAdditionalCoordinates: [], }, volumeTracing.tracingId, @@ -217,7 +217,7 @@ describe("VolumeTracingSaga", () => { VolumeTracingActions.updateSegmentAction( ACTIVE_CELL_ID, { - somePosition: startEditingAction.position, + somePosition: startEditingAction.positionInLayerSpace, someAdditionalCoordinates: [], }, volumeTracing.tracingId, @@ -283,7 +283,7 @@ describe("VolumeTracingSaga", () => { VolumeTracingActions.updateSegmentAction( ACTIVE_CELL_ID, { - somePosition: startEditingAction.position, + somePosition: startEditingAction.positionInLayerSpace, someAdditionalCoordinates: [], }, volumeTracing.tracingId, diff --git a/frontend/javascripts/viewer/controller/combinations/tool_controls.ts b/frontend/javascripts/viewer/controller/combinations/tool_controls.ts index 09467ce8d82..93de7be615a 100644 --- a/frontend/javascripts/viewer/controller/combinations/tool_controls.ts +++ b/frontend/javascripts/viewer/controller/combinations/tool_controls.ts @@ -553,7 +553,7 @@ export class EraseToolController { const isControlOrMetaPressed = event.ctrlKey || event.metaKey; if (event.shiftKey) { if (isControlOrMetaPressed) { - VolumeHandlers.handleFloodFill(pos, plane); + VolumeHandlers.handleFloodFill(Store.getState(), pos, plane); } else { VolumeHandlers.handlePickCell(pos); } @@ -641,7 +641,7 @@ export class FillCellToolController { if (shouldPickCell) { VolumeHandlers.handlePickCell(pos); } else { - VolumeHandlers.handleFloodFill(pos, plane); + VolumeHandlers.handleFloodFill(Store.getState(), pos, plane); } }, }; diff --git a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts index bf453f76b70..b4a072d3dee 100644 --- a/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts +++ b/frontend/javascripts/viewer/controller/combinations/volume_handlers.ts @@ -245,12 +245,17 @@ export function handlePickCellFromGlobalPosition( ), ); } -export function handleFloodFill(pos: Point2, plane: OrthoView) { - const globalPosRounded = calculateGlobalPos(Store.getState(), pos).rounded; - handleFloodFillFromGlobalPosition(globalPosRounded, plane); +export function handleFloodFill(state: WebknossosState, screenPos: Point2, plane: OrthoView) { + const globalPosRounded = calculateGlobalPos(Store.getState(), screenPos).rounded; + handleFloodFillFromGlobalPosition(state, globalPosRounded, plane); } -export function handleFloodFillFromGlobalPosition(globalPos: Vector3, plane: OrthoView) { - Store.dispatch(floodFillAction(globalPos, plane)); +export function handleFloodFillFromGlobalPosition( + state: WebknossosState, + globalPos: Vector3, + plane: OrthoView, +) { + const positionInLayerSpace = getUntransformedSegmentationPosition(state, globalPos); + Store.dispatch(floodFillAction(positionInLayerSpace, plane)); } const MAX_BRUSH_CHANGE_VALUE = 5; const BRUSH_CHANGING_CONSTANT = 0.02; diff --git a/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts b/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts index b7fc5333df8..0200e0560ae 100644 --- a/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts +++ b/frontend/javascripts/viewer/model/actions/volumetracing_actions.ts @@ -174,23 +174,27 @@ export const createCellAction = (activeCellId: number, largestSegmentId: number) } as const; }; -export const startEditingAction = (position: Vector3, planeId: OrthoView) => +export const startEditingAction = (positionInLayerSpace: Vector3, planeId: OrthoView) => ({ type: "START_EDITING", - position, // in layer space + positionInLayerSpace, // in layer space planeId, }) as const; -export const addToContourListAction = (position: Vector3) => +export const addToContourListAction = (positionInLayerSpace: Vector3) => ({ type: "ADD_TO_CONTOUR_LIST", - position, + positionInLayerSpace, }) as const; -export const floodFillAction = (position: Vector3, planeId: OrthoView, callback?: () => void) => +export const floodFillAction = ( + positionInLayerSpace: Vector3, + planeId: OrthoView, + callback?: () => void, +) => ({ type: "FLOOD_FILL", - position, + positionInLayerSpace, planeId, callback, }) as const; @@ -413,11 +417,11 @@ export const setLargestSegmentIdAction = (segmentId: number) => export const dispatchFloodfillAsync = async ( dispatch: Dispatch, - position: Vector3, + positionInLayerSpace: Vector3, planeId: OrthoView, ): Promise => { const readyDeferred = new Deferred(); - const action = floodFillAction(position, planeId, () => readyDeferred.resolve(null)); + const action = floodFillAction(positionInLayerSpace, planeId, () => readyDeferred.resolve(null)); dispatch(action); await readyDeferred.promise(); }; diff --git a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts index 7e358014895..48d8b7a6d71 100644 --- a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts @@ -433,7 +433,7 @@ function VolumeTracingReducer( } case "ADD_TO_CONTOUR_LIST": { - return addToContourListReducer(state, volumeTracing, action.position); + return addToContourListReducer(state, volumeTracing, action.positionInLayerSpace); } case "RESET_CONTOUR": { diff --git a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer_helpers.ts b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer_helpers.ts index 31f5a462d36..ef04458dc9d 100644 --- a/frontend/javascripts/viewer/model/reducers/volumetracing_reducer_helpers.ts +++ b/frontend/javascripts/viewer/model/reducers/volumetracing_reducer_helpers.ts @@ -131,7 +131,7 @@ export function updateDirectionReducer( export function addToContourListReducer( state: WebknossosState, volumeTracing: VolumeTracing, - position: Vector3, + positionInLayerSpace: Vector3, ) { const { isUpdatingCurrentlyAllowed } = state.annotation; @@ -143,7 +143,7 @@ export function addToContourListReducer( } return updateVolumeTracing(state, volumeTracing.tracingId, { - contourList: [...volumeTracing.contourList, position], + contourList: [...volumeTracing.contourList, positionInLayerSpace], }); } export function resetContourReducer(state: WebknossosState, volumeTracing: VolumeTracing) { diff --git a/frontend/javascripts/viewer/model/sagas/volume/floodfill_saga.tsx b/frontend/javascripts/viewer/model/sagas/volume/floodfill_saga.tsx index bd59e477e2f..30167f97ba2 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/floodfill_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volume/floodfill_saga.tsx @@ -155,7 +155,7 @@ function* handleFloodFill(floodFillAction: FloodFillAction): Saga { return; } - const { position: positionFloat, planeId } = floodFillAction; + const { positionInLayerSpace: positionFloat, planeId } = floodFillAction; const volumeTracing = yield* select(enforceActiveVolumeTracing); if (volumeTracing.hasEditableMapping) { const message = "Volume modification is not allowed when an editable mapping is active."; diff --git a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts index 9564d9245ae..19dd6055366 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/helpers.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/helpers.ts @@ -221,7 +221,8 @@ export function* labelWithVoxelBuffer2D( const labelMapOfBucket = new Uint8Array(Constants.BUCKET_WIDTH ** 2); currentLabeledVoxelMap.set(bucketZoomedAddress, labelMapOfBucket); - // globalA (first dim) and globalB (second dim) are global coordinates + // globalA (first dim) and globalB (second dim) are global coordinates in layer-space. + // They are "global" in the sense that they are not bucket-local coordinates. // which can be used to index into the 2D slice of the VoxelBuffer2D (when subtracting the minCoord2d) // and the LabeledVoxelMap for (let globalA = min[dimensionIndices[0]]; globalA < max[dimensionIndices[0]]; globalA++) { diff --git a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx index c3e9be85dde..ea0961bdda3 100644 --- a/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx @@ -232,7 +232,7 @@ export function* editVolumeLayerAsync(): Saga { updateSegmentAction( activeCellId, { - somePosition: startEditingAction.position, + somePosition: startEditingAction.positionInLayerSpace, someAdditionalCoordinates: additionalCoordinates || undefined, }, volumeTracing.tracingId, @@ -245,14 +245,14 @@ export function* editVolumeLayerAsync(): Saga { volumeTracing, startEditingAction.planeId, labeledMag, - (thirdDim) => startEditingAction.position[thirdDim], + (thirdDim) => startEditingAction.positionInLayerSpace[thirdDim], ); const initialViewport = yield* select((state) => state.viewModeData.plane.activeViewport); if (isBrushTool(activeTool)) { yield* call( labelWithVoxelBuffer2D, - currentSectionLabeler.getCircleVoxelBuffer2D(startEditingAction.position), + currentSectionLabeler.getCircleVoxelBuffer2D(startEditingAction.positionInLayerSpace), contourTracingMode, overwriteMode, labeledZoomStep, @@ -261,7 +261,7 @@ export function* editVolumeLayerAsync(): Saga { ); } - let lastPosition = startEditingAction.position; + let lastPosition = startEditingAction.positionInLayerSpace; const channel = yield* actionChannel(["ADD_TO_CONTOUR_LIST", "FINISH_EDITING"]); while (true) { @@ -283,7 +283,7 @@ export function* editVolumeLayerAsync(): Saga { continue; } - if (V3.equals(lastPosition, addToContourListAction.position)) { + if (V3.equals(lastPosition, addToContourListAction.positionInLayerSpace)) { // The voxel position did not change since the last action (the mouse moved // within a voxel). There is no need to do anything. continue; @@ -292,13 +292,13 @@ export function* editVolumeLayerAsync(): Saga { if (isTraceTool(activeTool) || (isBrushTool(activeTool) && isDrawing)) { // Close the polygon. When brushing, this causes an auto-fill which is why // it's only performed when drawing (not when erasing). - currentSectionLabeler.updateArea(addToContourListAction.position); + currentSectionLabeler.updateArea(addToContourListAction.positionInLayerSpace); } if (isBrushTool(activeTool)) { const rectangleVoxelBuffer2D = currentSectionLabeler.getRectangleVoxelBuffer2D( lastPosition, - addToContourListAction.position, + addToContourListAction.positionInLayerSpace, ); if (rectangleVoxelBuffer2D) { @@ -315,7 +315,7 @@ export function* editVolumeLayerAsync(): Saga { yield* call( labelWithVoxelBuffer2D, - currentSectionLabeler.getCircleVoxelBuffer2D(addToContourListAction.position), + currentSectionLabeler.getCircleVoxelBuffer2D(addToContourListAction.positionInLayerSpace), contourTracingMode, overwriteMode, labeledZoomStep, @@ -324,7 +324,7 @@ export function* editVolumeLayerAsync(): Saga { ); } - lastPosition = addToContourListAction.position; + lastPosition = addToContourListAction.positionInLayerSpace; } yield* call( diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index c595bbd38a2..da1e7478fcc 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -59,6 +59,9 @@ export class VoxelBuffer2D { } } + /* + * These methods return the coordinates in mag-1 layer space. + */ getTopLeft3DCoord = () => this.get3DCoordinateFromLocal2D([0, 0]); getBottomRight3DCoord = () => this.get3DCoordinateFromLocal2D([this.width, this.height]); @@ -103,6 +106,9 @@ export class VoxelBuffer2D { } } export class VoxelNeighborQueue3D { + /* + * The positions are in layer space. + */ queue: Array; constructor(initialPosition: Vector3) { @@ -171,14 +177,15 @@ export class VoxelNeighborQueue2D extends VoxelNeighborQueue3D { class SectionLabeler { /* - From the outside, the SectionLabeler accepts only global positions. Internally, - these are converted to the actual used mags (activeMag). + From the outside, the SectionLabeler accepts only global (mag 1 and not bucket-local) + positions in layer space. Internally, these are converted to the actual used + mags (activeMag). Therefore, members of this class are in the mag space of `activeMag`. */ readonly thirdDimensionValue: number; - // Stored in global (but mag-dependent) coordinates: + // Stored in global (but mag-dependent) coordinates in layer space: minCoord: Vector3 | null | undefined; maxCoord: Vector3 | null | undefined; @@ -230,13 +237,18 @@ class SectionLabeler { return difference[0] * difference[1] * difference[2]; } - private getContourList(useGlobalCoords: boolean = false) { + private getContourList(useActiveMag: boolean = false) { + /* + * Returns layer-space coordinates in mag 1 if useActiveMag is false. + * Otherwise, return layer-space coordinates in `activeMag`. + */ + const globalContourList = getVolumeTracingById( Store.getState().annotation, this.volumeTracingId, ).contourList; - if (useGlobalCoords) { + if (useActiveMag) { return globalContourList; } @@ -513,13 +525,14 @@ class SectionLabeler { } public get2DCoordinate(coord3d: Vector3): Vector2 { + // coord3d is in layer space. // Throw out 'thirdCoordinate' which is always the same, anyway. const transposed = Dimensions.transDim(coord3d, this.plane); return [transposed[0], transposed[1]]; } getUnzoomedCentroid(): Vector3 { - /* Returns the centroid (in the global coordinate system). + /* Returns the centroid (in layer space). * * Formula: * https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon diff --git a/frontend/javascripts/viewer/store.ts b/frontend/javascripts/viewer/store.ts index 0735d340186..c670ca93665 100644 --- a/frontend/javascripts/viewer/store.ts +++ b/frontend/javascripts/viewer/store.ts @@ -207,7 +207,7 @@ export type VolumeTracing = TracingBase & { // lastLabelActions[0] is the most recent one readonly lastLabelActions: Array; readonly contourTracingMode: ContourMode; - // Stores points of the currently drawn region in global coordinates + // Stores points of the currently drawn region in layer-space coordinates. readonly contourList: Array; readonly fallbackLayer?: string; readonly mappingName?: string | null | undefined; diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index 0c598f67bd8..1055707d8fd 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -1457,7 +1457,8 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] allowUpdate && !disabledVolumeInfo.FILL_CELL.isDisabled ? { key: "fill-cell", - onClick: () => handleFloodFillFromGlobalPosition(globalPosition, viewport), + onClick: () => + handleFloodFillFromGlobalPosition(Store.getState(), globalPosition, viewport), label: "Fill Segment (flood-fill region)", } : null, From c1b69071bf71c0c6c40ae14545e4b6fd875e53ac Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 10 Dec 2025 12:44:42 +0100 Subject: [PATCH 36/37] extend comment --- frontend/javascripts/viewer/constants.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/viewer/constants.ts b/frontend/javascripts/viewer/constants.ts index ed6359b69ea..2b982af4b88 100644 --- a/frontend/javascripts/viewer/constants.ts +++ b/frontend/javascripts/viewer/constants.ts @@ -283,9 +283,10 @@ export const Unicode = { MultiplicationSymbol: "×", }; // A LabeledVoxelsMap maps from a bucket address -// to a 2D slice of labeled voxels. These labeled voxels -// are stored in a Uint8Array in a binary way (which cell -// id the voxels should be changed to is not encoded). +// to a 2D slice of labeled voxels within a bucket. +// These labeled voxels are stored in a Uint8Array in a binary way (which +// segment id the voxels should be changed to is not encoded). +// The array should have BUCKET_WIDTH**2 entries. export type LabeledVoxelsMap = Map; // LabelMasksByBucketAndW is similar to LabeledVoxelsMap with the difference From be6c6e95acfe49ddc3af1a2c6f11f3cc4b1fd5f7 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 10 Dec 2025 15:25:07 +0100 Subject: [PATCH 37/37] add todo comments --- .../model/transformed_section_labeler.spec.ts | 4 ++++ .../viewer/model/sagas/volume/proofread_saga.ts | 2 +- .../model/volumetracing/section_labeling.ts | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/test/model/transformed_section_labeler.spec.ts b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts index 1f5ec174b34..86a8d6714fa 100644 --- a/frontend/javascripts/test/model/transformed_section_labeler.spec.ts +++ b/frontend/javascripts/test/model/transformed_section_labeler.spec.ts @@ -76,6 +76,10 @@ describe("TransformedSectionLabeler", () => { expect(mapTransformedPlane("PLANE_XZ", rotationalTransform)).toEqual(["PLANE_XY", [0, 2]]); }); + it.skip("[L4] Rotation by 90deg around Y should be handled correctly", async () => { + // TODO: Implement in a follow-up. + }); + it("[L4] Rotation by 90deg around Z should be handled correctly", async () => { const coordinateTransformations = [ { diff --git a/frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts b/frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts index 07e14a4c63d..c8a64ccdce5 100644 --- a/frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts @@ -1077,7 +1077,7 @@ function* handleProofreadMergeOrMinCut(action: Action) { ); const annotationVersion = yield* select((state) => state.annotation.version); - // Now that the changes are saved, we can split the mapping locally (because it requires + // Now that the changes are saved, we can split the local mapping (because it requires // communication with the back-end). const splitMapping = yield* splitAgglomerateInMapping( activeMapping, diff --git a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts index da1e7478fcc..1207d4c626b 100644 --- a/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts +++ b/frontend/javascripts/viewer/model/volumetracing/section_labeling.ts @@ -616,7 +616,6 @@ export function mapTransformedPlane( const n2 = basis.n.clone().applyMatrix4(m).normalize(); // find which canonical plane the transformed normal aligns with - let bestView: OrthoView = OrthoViews.PLANE_XY; let bestDot = Number.NEGATIVE_INFINITY; @@ -628,6 +627,11 @@ export function mapTransformedPlane( } } + // TODO: Sometimes the u and v coordinates need to be swapped. + // However, the detection for this doesn't fully work yet. + // See transformed_section_labeler.spec.ts for tests. + // The code was already added during a refactoring (#9023) + // and needs to be fixed and finished as a follow-up. const swapped = isAlmostZero(basis.u.dot(u2)); const adaptScaleFn = (scale: Vector3): Vector2 => { @@ -643,6 +647,16 @@ export function mapTransformedPlane( } export class TransformedSectionLabeler { + /* + * This class is a wrapper around SectionLabeler + * and should enable labelling a transformed dataset + * by mapping the annotated plane to another one. + * + * TODO: The class does not fully work yet. + * See transformed_section_labeler.spec.ts for tests. + * It was already added during a refactoring (#9023) + * and needs to be fixed and finished as a follow-up. + */ private readonly base: SectionLabeler; applyTransform: (pos: Vector3) => Vector3; applyInverseTransform: (pos: Vector3) => Vector3; @@ -708,6 +722,7 @@ export class TransformedSectionLabeler { getBaseVoxelFactorsInUnit(Store.getState().dataset.dataSource.scale), ); + // todo: does this need a transformation? return this.base.getCircleVoxelBuffer2D(position, scale); }