Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6f6d2a3
fix coloring and spacing of icon in ai list view
MichaelBuessemeyer Apr 2, 2025
f61e1d4
makes bboxes and meshes rotate according to transformation settings
MichaelBuessemeyer Apr 2, 2025
8012ee2
remove unused meshes root group
MichaelBuessemeyer Apr 2, 2025
62f797d
Merge branch 'master' of github.com:scalableminds/webknossos into tra…
MichaelBuessemeyer Apr 3, 2025
d758fe2
add utility function to iterate over each mesh group
MichaelBuessemeyer Apr 3, 2025
2da3fd3
Merge branch 'master' of github.com:scalableminds/webknossos into tra…
MichaelBuessemeyer Apr 10, 2025
bb33e65
fix applying transforms for meshes
MichaelBuessemeyer Apr 11, 2025
b209851
move mesh mag scale into outer group again
MichaelBuessemeyer Apr 11, 2025
568b3dc
Apply PR feedback (function renaming, wording and more)
MichaelBuessemeyer Apr 14, 2025
00cfb87
refactor scene controller a bit to make hierarchy more obvious
MichaelBuessemeyer Apr 14, 2025
d0f2608
get rid of group helper
MichaelBuessemeyer Apr 14, 2025
5fa59b4
transform bboxes according to attached tracing and meshes according t…
MichaelBuessemeyer Apr 14, 2025
5c5b955
Merge branch 'master' of github.com:scalableminds/webknossos into tra…
MichaelBuessemeyer Apr 14, 2025
8a5332f
Merge branch 'master' of github.com:scalableminds/webknossos into tra…
MichaelBuessemeyer Apr 15, 2025
bbff27e
fix frontend tests
MichaelBuessemeyer Apr 15, 2025
7ac737a
fix adding layer manually via advanced dataset settings tab
MichaelBuessemeyer Apr 22, 2025
8ce7b01
make meshes visible dependent on layer visibility
MichaelBuessemeyer Apr 22, 2025
104a05e
add missing mesh lights
MichaelBuessemeyer Apr 22, 2025
26b5704
fix removing meshes
MichaelBuessemeyer Apr 22, 2025
b4e13fe
Merge branch 'master' of github.com:scalableminds/webknossos into tra…
MichaelBuessemeyer Apr 22, 2025
cf00900
use reuseInstanceOnEquality to cache visible segmentation layer names
MichaelBuessemeyer Apr 28, 2025
ae589e3
fix precomputed mesh loading (context of yield calls was missing)
MichaelBuessemeyer Apr 28, 2025
acfd4c0
Merge branch 'master' of github.com:scalableminds/webknossos into tra…
MichaelBuessemeyer Apr 28, 2025
f853033
format frontend
MichaelBuessemeyer Apr 28, 2025
e1c99a5
Merge branch 'master' of github.com:scalableminds/webknossos into tra…
MichaelBuessemeyer Apr 29, 2025
1077954
fix import sorting in frontend
MichaelBuessemeyer Apr 29, 2025
5a6aff4
Merge branch 'master' into transform-meshes-and-bboxes
MichaelBuessemeyer Apr 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion frontend/javascripts/admin/voxelytics/ai_model_list_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,10 @@ const renderActionsForModel = (model: AiModel, onChangeSharedOrganizations: () =
{voxelyticsWorkflowHash != null ? (
/* margin left is needed as organizationSharingButton is a button with a 16 margin */
<Row style={{ marginLeft: 16 }}>
<FileTextOutlined />
<FileTextOutlined
className="icon-margin-right"
style={{ color: "var(--ant-color-primary)" }}
/>
<Link to={`/workflows/${voxelyticsWorkflowHash}`}>Voxelytics Report</Link>
</Row>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ function SimpleLayerForm({
form: FormInstance;
dataset: APIDataset | null | undefined;
}) {
const layerCategorySavedOnServer = dataset?.dataSource.dataLayers[index].category;
const layerCategorySavedOnServer = dataset?.dataSource.dataLayers[index]?.category;
const isStoredAsSegmentationLayer = layerCategorySavedOnServer === "segmentation";
const dataLayers = Form.useWatch(["dataSource", "dataLayers"]);
const category = Form.useWatch(["dataSource", "dataLayers", index, "category"]);
Expand Down
164 changes: 121 additions & 43 deletions frontend/javascripts/oxalis/controller/scene_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,37 @@ import {
getDataLayers,
getDatasetBoundingBox,
getLayerBoundingBox,
getLayerByName,
getLayerNameToIsDisabled,
getSegmentationLayers,
getVisibleSegmentationLayers,
} from "oxalis/model/accessors/dataset_accessor";
import { getTransformsForLayerOrNull } from "oxalis/model/accessors/dataset_layer_transformation_accessor";
import {
getTransformsForLayer,
getTransformsForLayerOrNull,
getTransformsForSkeletonLayer,
} from "oxalis/model/accessors/dataset_layer_transformation_accessor";
import { getActiveMagIndicesForLayers, getPosition } from "oxalis/model/accessors/flycam_accessor";
import { getSkeletonTracing } from "oxalis/model/accessors/skeletontracing_accessor";
import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor";
import { getPlaneScalingFactor } from "oxalis/model/accessors/view_mode_accessor";
import { sceneControllerReadyAction } from "oxalis/model/actions/actions";
import Dimensions from "oxalis/model/dimensions";
import { listenToStoreProperty } from "oxalis/model/helpers/listener_helpers";
import type { Transform } from "oxalis/model/helpers/transformation_helpers";
import { getVoxelPerUnit } from "oxalis/model/scaleinfo";
import { Model } from "oxalis/singletons";
import type { OxalisState, SkeletonTracing, UserBoundingBox } from "oxalis/store";
import Store from "oxalis/store";
import * as THREE from "three";
import type CustomLOD from "./custom_lod";
import SegmentMeshController from "./segment_mesh_controller";

const CUBE_COLOR = 0x999999;
const LAYER_CUBE_COLOR = 0xffff99;

class SceneController {
skeletons: Record<number, Skeleton> = {};
current: number;
isPlaneVisible: OrthoViewMap<boolean>;
planeShift: Vector3;
datasetBoundingBox!: Cube;
Expand All @@ -69,17 +77,18 @@ class SceneController {
lineMeasurementGeometry!: LineMeasurementGeometry;
areaMeasurementGeometry!: ContourGeometry;
planes!: OrthoViewWithoutTDMap<Plane>;
rootNode!: THREE.Object3D;
rootNode!: THREE.Group;
renderer!: THREE.WebGLRenderer;
scene!: THREE.Scene;
rootGroup!: THREE.Object3D;
rootGroup!: THREE.Group;
segmentMeshController: SegmentMeshController;
storePropertyUnsubscribers: Array<() => void>;
// Used to cache visible segmentation layers to only update the meshes visibility when this changes.
visibleSegmentationLayerNames: Array<string> = [];

// This class collects all the meshes displayed in the Skeleton View and updates position and scale of each
// element depending on the provided flycam.
constructor() {
this.current = 0;
this.isPlaneVisible = {
[OrthoViews.PLANE_XY]: true,
[OrthoViews.PLANE_YZ]: true,
Expand All @@ -96,22 +105,25 @@ class SceneController {
this.createMeshes();
this.bindToEvents();
this.scene = new THREE.Scene();
this.highlightedBBoxId = null;
this.rootGroup = new THREE.Group();
this.scene.add(
this.rootGroup.add(
this.rootNode,
this.segmentMeshController.meshesLayerLODRootGroup,
this.segmentMeshController.lightsGroup,
),
);
// Because the voxel coordinates do not have a cube shape but are distorted,
// we need to distort the entire scene to provide an illustration that is
// proportional to the actual size in nm.
// For some reason, all objects have to be put into a group object. Changing
// scene.scale does not have an effect.
this.rootGroup = new THREE.Object3D();
this.rootGroup.add(this.getRootNode());

this.highlightedBBoxId = null;
// The dimension(s) with the highest mag will not be distorted
// The dimension(s) with the highest mag will not be distorted.
this.rootGroup.scale.copy(
new THREE.Vector3(...Store.getState().dataset.dataSource.scale.factor),
);
// Add scene to the group, all Geometries are then added to group
this.scene.add(this.rootGroup);
this.scene.add(this.segmentMeshController.meshesLODRootGroup);
// Add rootGroup to scene, all Geometries are then added to the rootGroup
this.setupDebuggingMethods();
}

Expand Down Expand Up @@ -194,14 +206,10 @@ class SceneController {
}

createMeshes(): void {
this.rootNode = new THREE.Object3D();
this.userBoundingBoxes = [];
this.userBoundingBoxGroup = new THREE.Group();
this.layerBoundingBoxGroup = new THREE.Group();
this.rootNode.add(this.userBoundingBoxGroup);
this.rootNode.add(this.layerBoundingBoxGroup);
this.annotationToolsGeometryGroup = new THREE.Group();
this.rootNode.add(this.annotationToolsGeometryGroup);
this.userBoundingBoxes = [];
const state = Store.getState();
// Cubes
const { min, max } = getDatasetBoundingBox(state.dataset);
Expand All @@ -212,28 +220,11 @@ class SceneController {
showCrossSections: true,
isHighlighted: false,
});
this.datasetBoundingBox.getMeshes().forEach((mesh) => this.rootNode.add(mesh));
const taskBoundingBox = getSomeTracing(state.annotation).boundingBox;
this.buildTaskingBoundingBox(taskBoundingBox);

this.contour = new ContourGeometry();
this.contour.getMeshes().forEach((mesh) => this.annotationToolsGeometryGroup.add(mesh));

this.quickSelectGeometry = new QuickSelectGeometry();
this.annotationToolsGeometryGroup.add(this.quickSelectGeometry.getMeshGroup());

this.lineMeasurementGeometry = new LineMeasurementGeometry();
this.lineMeasurementGeometry
.getMeshes()
.forEach((mesh) => this.annotationToolsGeometryGroup.add(mesh));
this.areaMeasurementGeometry = new ContourGeometry(true);
this.areaMeasurementGeometry
.getMeshes()
.forEach((mesh) => this.annotationToolsGeometryGroup.add(mesh));

if (state.annotation.skeleton != null) {
this.addSkeleton((_state) => getSkeletonTracing(_state.annotation), true);
}

this.planes = {
[OrthoViews.PLANE_XY]: new Plane(OrthoViews.PLANE_XY),
Expand All @@ -244,10 +235,25 @@ class SceneController {
this.planes[OrthoViews.PLANE_YZ].setRotation(new THREE.Euler(Math.PI, (1 / 2) * Math.PI, 0));
this.planes[OrthoViews.PLANE_XZ].setRotation(new THREE.Euler((-1 / 2) * Math.PI, 0, 0));

for (const plane of _.values(this.planes)) {
plane.getMeshes().forEach((mesh: THREE.Object3D) => this.rootNode.add(mesh));
}
const planeMeshes = _.values(this.planes).flatMap((plane) => plane.getMeshes());
this.rootNode = new THREE.Group().add(
this.userBoundingBoxGroup,
this.layerBoundingBoxGroup,
this.annotationToolsGeometryGroup.add(
...this.contour.getMeshes(),
this.quickSelectGeometry.getMeshGroup(),
...this.lineMeasurementGeometry.getMeshes(),
...this.areaMeasurementGeometry.getMeshes(),
),
...this.datasetBoundingBox.getMeshes(),
...planeMeshes,
);

const taskBoundingBox = getSomeTracing(state.annotation).boundingBox;
this.buildTaskingBoundingBox(taskBoundingBox);
if (state.annotation.skeleton != null) {
this.addSkeleton((_state) => getSkeletonTracing(_state.annotation), true);
}
// Hide all objects at first, they will be made visible later if needed
this.stopPlaneMode();
}
Expand Down Expand Up @@ -316,7 +322,7 @@ class SceneController {

this.taskBoundingBox?.updateForCam(id);

this.segmentMeshController.meshesLODRootGroup.visible = id === OrthoViews.TDView;
this.segmentMeshController.meshesLayerLODRootGroup.visible = id === OrthoViews.TDView;
this.annotationToolsGeometryGroup.visible = id !== OrthoViews.TDView;
this.lineMeasurementGeometry.updateForCam(id);

Expand Down Expand Up @@ -434,6 +440,61 @@ class SceneController {
this.rootNode.add(this.userBoundingBoxGroup);
}

private applyTransformToGroup(transform: Transform, group: THREE.Group | CustomLOD) {
if (transform.affineMatrix) {
const matrix = new THREE.Matrix4();
// @ts-ignore
matrix.set(...transform.affineMatrix);
// We need to disable matrixAutoUpdate as otherwise the update to the matrix will be lost.
group.matrixAutoUpdate = false;
group.matrix = matrix;
}
}

updateUserBoundingBoxesAndMeshesAccordingToTransforms(): void {
const state = Store.getState();
const tracingStoringUserBBoxes = getSomeTracing(state.annotation);
const transformForBBoxes =
tracingStoringUserBBoxes.type === "volume"
? getTransformsForLayer(
state.dataset,
getLayerByName(state.dataset, tracingStoringUserBBoxes.tracingId),
state.datasetConfiguration.nativelyRenderedLayerName,
)
: getTransformsForSkeletonLayer(
state.dataset,
state.datasetConfiguration.nativelyRenderedLayerName,
);
this.applyTransformToGroup(transformForBBoxes, this.userBoundingBoxGroup);
const visibleSegmentationLayers = getVisibleSegmentationLayers(state);
if (visibleSegmentationLayers.length === 0) {
return;
}
// Use transforms of active segmentation layer to transform the meshes.
// All meshes not belonging to this layer should be hidden via updateMeshesAccordingToLayerVisibility anyway.
const transformForMeshes = getTransformsForLayer(
state.dataset,
visibleSegmentationLayers[0],
state.datasetConfiguration.nativelyRenderedLayerName,
);
this.applyTransformToGroup(
transformForMeshes,
this.segmentMeshController.meshesLayerLODRootGroup,
);
}

updateMeshesAccordingToLayerVisibility(): void {
const state = Store.getState();
const visibleSegmentationLayers = getVisibleSegmentationLayers(state);
const allSegmentationLayers = getSegmentationLayers(state.dataset);
allSegmentationLayers.forEach((layer) => {
const layerName = layer.name;
const isLayerVisible =
visibleSegmentationLayers.find((layer) => layer.name === layerName) !== undefined;
this.segmentMeshController.setVisibilityOfMeshesOfLayer(layerName, isLayerVisible);
});
}

updateLayerBoundingBoxes(): void {
const state = Store.getState();
const dataset = state.dataset;
Expand Down Expand Up @@ -513,8 +574,8 @@ class SceneController {

this.taskBoundingBox?.setVisibility(false);

if (this.segmentMeshController.meshesLODRootGroup != null) {
this.segmentMeshController.meshesLODRootGroup.visible = false;
if (this.segmentMeshController.meshesLayerLODRootGroup != null) {
this.segmentMeshController.meshesLayerLODRootGroup.visible = false;
}
}

Expand Down Expand Up @@ -563,7 +624,7 @@ class SceneController {
plane.destroy();
}

this.rootNode = new THREE.Object3D();
this.rootNode = new THREE.Group();
}

bindToEvents(): void {
Expand All @@ -590,7 +651,24 @@ class SceneController {
),
listenToStoreProperty(
(storeState) => storeState.datasetConfiguration.nativelyRenderedLayerName,
() => this.updateLayerBoundingBoxes(),
() => {
this.updateLayerBoundingBoxes();
this.updateUserBoundingBoxesAndMeshesAccordingToTransforms();
},
),
listenToStoreProperty(
(storeState) => {
const visibleSegmentationLayerNames = getVisibleSegmentationLayers(storeState).map(
(l) => l.name,
);
if (_.isEqual(this.visibleSegmentationLayerNames, visibleSegmentationLayerNames)) {
return this.visibleSegmentationLayerNames;
} else {
this.visibleSegmentationLayerNames = visibleSegmentationLayerNames;
return visibleSegmentationLayerNames;
}
},
() => this.updateMeshesAccordingToLayerVisibility(),
),
listenToStoreProperty(
(storeState) => getSomeTracing(storeState.annotation).boundingBox,
Expand Down
Loading