Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3be102f
remove debugging toast
MichaelBuessemeyer Nov 20, 2025
b207e4a
only enforce blocked ui when incorporating backend actions when live …
MichaelBuessemeyer Nov 20, 2025
8bfb8e7
add changelog entry
MichaelBuessemeyer Nov 20, 2025
47f4c05
use improved guardAsBlocking logic and correct comments
MichaelBuessemeyer Nov 20, 2025
22cef47
remove debug logging
MichaelBuessemeyer Nov 20, 2025
6c3e297
support applying active mapping changes for agglomerate mappings
MichaelBuessemeyer Nov 20, 2025
6065eeb
fix initial proofread action upon while othersMayEdit is active
MichaelBuessemeyer Nov 21, 2025
681663c
WIP: guard agglomerate skeleton import in live collab with mutex
MichaelBuessemeyer Nov 24, 2025
53edc48
WIP support syncing agglomerate skeleton in non skeleton merge action
MichaelBuessemeyer Nov 24, 2025
23d5993
Merge branch 'master' of github.com:scalableminds/webknossos into liv…
MichaelBuessemeyer Nov 24, 2025
95d5b53
WIP update agglomerate skeletons upon proofreading action
MichaelBuessemeyer Nov 24, 2025
ad8e15d
improve typing in tests
MichaelBuessemeyer Nov 26, 2025
03bd79d
fix some tests
MichaelBuessemeyer Nov 27, 2025
8d44d9a
move proofreading saga
MichaelBuessemeyer Nov 27, 2025
089fe2f
update snapshots
MichaelBuessemeyer Nov 27, 2025
c7bc2c4
WIP add agglomerate skeleton syncing & tests for it
MichaelBuessemeyer Nov 27, 2025
74992cc
WIP: fix duplicate agglomerate helper meshes
MichaelBuessemeyer Nov 28, 2025
916ac22
some more logging to detect race conditions; and don't reload meshes …
MichaelBuessemeyer Dec 1, 2025
72be4ec
WIP: improve skeleton diffing during agglomerate skeleton syncing
MichaelBuessemeyer Dec 1, 2025
fe981fd
fix syncing of agglomerate skeletons
MichaelBuessemeyer Dec 2, 2025
d55191b
use position from segments list (if no segment index exists) for proo…
MichaelBuessemeyer Dec 2, 2025
ef04517
WIP implement agglomerate skeleton inplace reloading for split actions
MichaelBuessemeyer Dec 2, 2025
06bb51a
fix agglomerate skeleton generation for tests
MichaelBuessemeyer Dec 3, 2025
4cc955c
Fix unnecessary delete&create edges actions & update test snapshots
MichaelBuessemeyer Dec 3, 2025
f0c8440
remove outdated & commented-out code
MichaelBuessemeyer Dec 3, 2025
9e34019
fix proofreading tests checking that missing mapping info is loaded d…
MichaelBuessemeyer Dec 4, 2025
224f226
min test cleanup
MichaelBuessemeyer Dec 4, 2025
83fb056
update snapshots
MichaelBuessemeyer Dec 4, 2025
778ba13
remove unused import
MichaelBuessemeyer Dec 4, 2025
de4c596
WIP add agglomerate meshes tests
MichaelBuessemeyer Dec 10, 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
4 changes: 2 additions & 2 deletions frontend/javascripts/admin/api/mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type MeshLodInfo = {
transform: [Vector4, Vector4, Vector4]; // 4x3 matrix
};

type MeshSegmentInfo = {
export type MeshSegmentInfo = {
meshFormat: "draco";
lods: Array<MeshLodInfo>;
chunkScale: Vector3;
Expand Down Expand Up @@ -71,7 +71,7 @@ type MeshChunkDataRequest = {
segmentId: number | null; // Only relevant for neuroglancer precomputed meshes
};

type MeshChunkDataRequestList = {
export type MeshChunkDataRequestList = {
meshFileName: string;
requests: MeshChunkDataRequest[];
};
Expand Down
58 changes: 33 additions & 25 deletions frontend/javascripts/libs/diffable_map.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import _ from "lodash";

const defaultItemsPerBatch = 1000;
let idCounter = 0;
const idSymbol = Symbol("id");
Expand Down Expand Up @@ -417,9 +419,11 @@ class DiffableMap<K extends number, V> implements NotEnumerableByObject {
? "null"
: v === undefined
? "undefined"
: typeof v === "object"
? JSON.stringify(v)
: String(v);
: v instanceof DiffableMap
? v.toString()
: typeof v === "object"
? JSON.stringify(v)
: String(v);
return `${k} => ${vStr}`;
})
.join(", ");
Expand Down Expand Up @@ -468,11 +472,15 @@ function shallowCopy<K extends number, V>(template: DiffableMap<K, V>): Diffable
export function diffDiffableMaps<K extends number, V>(
mapA: DiffableMap<K, V>,
mapB: DiffableMap<K, V>,
useDeepEqualityCheck: boolean = false,
): {
changed: Array<K>;
onlyA: Array<K>;
onlyB: Array<K>;
} {
const areDifferent = (valueA: V | undefined, valueB: V | undefined) =>
useDeepEqualityCheck ? !_.isEqual(valueA, valueB) : valueA !== valueB;

// For the edge case that one of the maps is empty, we will consider them dependent, anyway
const areDiffsDependent = mapA.getId() === mapB.getId() || mapA.size() === 0 || mapB.size() === 0;
let idx = 0;
Expand Down Expand Up @@ -506,7 +514,7 @@ export function diffDiffableMaps<K extends number, V>(

for (const key of setA.values()) {
if (setB.has(key)) {
if (currentMapA.get(key) !== currentMapB.get(key)) {
if (areDifferent(currentMapA.get(key), currentMapB.get(key))) {
changed.push(key);
} else {
// The key exists in both chunks, do not emit this key.
Expand All @@ -530,32 +538,32 @@ export function diffDiffableMaps<K extends number, V>(
idx++;
}

if (!areDiffsDependent) {
// Since, the DiffableMaps don't share the same structure, we might have
// aggregated false-positives, meaning onlyA and onlyB can include the same
// keys, which might or might not belong to the changed set.
// Construct a set for fast lookup
const setA = new Set(onlyA);
// Intersection of onlyA and onlyB:
const missingChangedIds = onlyB.filter((id) => setA.has(id));
const missingChangedIdSet = new Set(missingChangedIds);
const newOnlyA = onlyA.filter((id) => !missingChangedIdSet.has(id));
const newOnlyB = onlyB.filter((id) => !missingChangedIdSet.has(id));
// Ensure that these elements are not equal before adding them to "changed"
const newChanged = changed.concat(
missingChangedIds.filter((id) => mapA.getOrThrow(id) !== mapB.getOrThrow(id)),
);
if (areDiffsDependent) {
return {
changed: newChanged,
onlyA: newOnlyA,
onlyB: newOnlyB,
changed,
onlyA,
onlyB,
};
}

// Since, the DiffableMaps don't share the same structure, we might have
// aggregated false-positives, meaning onlyA and onlyB can include the same
// keys, which might or might not belong to the changed set.
// Construct a set for fast lookup
const setA = new Set(onlyA);
// Intersection of onlyA and onlyB:
const missingChangedIds = onlyB.filter((id) => setA.has(id));
const missingChangedIdSet = new Set(missingChangedIds);
const newOnlyA = onlyA.filter((id) => !missingChangedIdSet.has(id));
const newOnlyB = onlyB.filter((id) => !missingChangedIdSet.has(id));
// Ensure that these elements are not equal before adding them to "changed"
const newChanged = changed.concat(
missingChangedIds.filter((id) => areDifferent(mapA.getOrThrow(id), mapB.getOrThrow(id))),
);
return {
changed,
onlyA,
onlyB,
changed: newChanged,
onlyA: newOnlyA,
onlyB: newOnlyB,
};
}

Expand Down
40 changes: 40 additions & 0 deletions frontend/javascripts/libs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,46 @@ export function diffArrays<T>(
};
}

export function diffNumberArrays(a: number[], b: number[]) {
// Create sorted copies to avoid mutating inputs
const A = [...a].sort((x, y) => x - y);
const B = [...b].sort((x, y) => x - y);

const both: number[] = [];
const onlyA: number[] = [];
const onlyB: number[] = [];

let indexA = 0;
let indexB = 0;

while (indexA < A.length && indexB < B.length) {
if (A[indexA] === B[indexB]) {
both.push(A[indexA]);
indexA++;
indexB++;
} else if (A[indexA] < B[indexB]) {
onlyA.push(A[indexA]);
indexA++;
} else {
onlyB.push(B[indexB]);
indexB++;
}
}

// Remaining elements
while (indexA < A.length) {
onlyA.push(A[indexA]);
indexA++;
}

while (indexB < B.length) {
onlyB.push(B[indexB]);
indexB++;
}

return { both, onlyA, onlyB };
}

export function diffMaps<K, V>(
stateA: Map<K, V>,
stateB: Map<K, V>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ describe("Annotation API (E2E)", () => {
],
},
];
const createTreesUpdateActions = Array.from(diffTrees(tracingId, new DiffableMap(), trees));
const createTreesUpdateActions = Array.from(
diffTrees(tracingId, new DiffableMap(), trees, false),
);
const updateTreeGroupsUpdateAction = UpdateActions.updateTreeGroups(treeGroups, tracingId);
const [saveQueue] = addVersionNumbers(
createSaveQueueFromUpdateActions(
Expand All @@ -266,7 +268,9 @@ describe("Annotation API (E2E)", () => {
const createdExplorational = await api.createExplorational(datasetId, "skeleton", false, null);
const { tracingId } = createdExplorational.annotationLayers[0];
let trees = createTreeMapFromTreeArray(generateDummyTrees(5, 6));
const createTreesUpdateActions = Array.from(diffTrees(tracingId, new DiffableMap(), trees));
const createTreesUpdateActions = Array.from(
diffTrees(tracingId, new DiffableMap(), trees, false),
);
const metadata = [
{
key: "city",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const sampleColorLayer: APIColorLayer = {
};

export const sampleHdf5AgglomerateName = "sampleHdf5Mapping";
export const sampleMappingFileName = "sampleMappingFile";
// this is a uint32 segmentation layer
const sampleSegmentationLayer: APISegmentationLayer = {
name: "segmentation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ export class AgglomerateMapping {
return this.versions[version];
}

// Returns a copy of the current adjacency list. It is a deep clone to avoid direct manipulation from outside.
// AdjacencyList is currently unversioned.
getAdjacencyList(): Map<number, Set<number>> {
return _.cloneDeep(this.adjacencyList);
}

private resetVersionCounter(initialVersion: number) {
/*
* Reset the most recent version to be stored as version `initialVersion`.
Expand Down
Loading
Loading