From cec578dc844cb10bc3ad17fe813d8050177b7436 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Tue, 15 Sep 2026 15:18:43 -0400 Subject: [PATCH 01/15] refactor(processing): rename the segmentation result instruction --- backend-contract/README.md | 13 ++++++ .../fixtures/negative/wrong-length-color.json | 2 +- ... intent.import-segmentation.embedded.json} | 2 +- ...nt.import-segmentation.with-segments.json} | 2 +- .../generated/job-results.schema.json | 2 +- backend-contract/generated/openapi.json | 6 +-- .../generated/result-intent.schema.json | 2 +- backend-contract/package.json | 2 +- .../processing/__tests__/wire.spec.ts | 26 +++++------ backend-contract/processing/openapi.ts | 2 +- backend-contract/processing/wire.ts | 8 ++-- .../__tests__/applyResults.segments.spec.ts | 2 +- src/processing/__tests__/applyResults.spec.ts | 46 +++++++++---------- src/processing/applyResults.ts | 4 +- .../engine/__tests__/resultFiles.spec.ts | 2 +- .../engine/__tests__/resultToIntent.spec.ts | 2 +- src/processing/engine/__tests__/wire.spec.ts | 4 +- 17 files changed, 70 insertions(+), 57 deletions(-) rename backend-contract/fixtures/wire/{intent.add-segment-group.embedded.json => intent.import-segmentation.embedded.json} (88%) rename backend-contract/fixtures/wire/{intent.add-segment-group.with-segments.json => intent.import-segmentation.with-segments.json} (95%) diff --git a/backend-contract/README.md b/backend-contract/README.md index e6fc24fec..de7f7b7a2 100644 --- a/backend-contract/README.md +++ b/backend-contract/README.md @@ -113,6 +113,19 @@ Two versions on separate clocks: the task-spec `specVersion`. These version the wire vocabulary for additive compatibility negotiation. +### Result instruction rollout + +Contract artifact 0.3.0 uses intent vocabulary 3 and names segmentation import +`import-segmentation`. Deploy the updated producer and VolView client together. +An older client treats the unfamiliar instruction as an ordinary result and +will not apply its segmentation automatically. Update Girder's pinned VolView +package when releasing the paired change. + +This vocabulary change does not change task-spec versions or saved-session +schemas. Girder projects stored job outputs into current instructions when +results are requested; stored output references and mask provenance keep their +identities. + ## Regenerating ``` diff --git a/backend-contract/fixtures/negative/wrong-length-color.json b/backend-contract/fixtures/negative/wrong-length-color.json index f5aa4fee1..cc483faa6 100644 --- a/backend-contract/fixtures/negative/wrong-length-color.json +++ b/backend-contract/fixtures/negative/wrong-length-color.json @@ -1,6 +1,6 @@ { "id": "6600000000000000000000e1", - "intent": "add-segment-group", + "intent": "import-segmentation", "url": "/api/v1/file/6600000000000000000000e1/proxiable/otsu.nii.gz", "name": "otsu.nii.gz", "segments": [ diff --git a/backend-contract/fixtures/wire/intent.add-segment-group.embedded.json b/backend-contract/fixtures/wire/intent.import-segmentation.embedded.json similarity index 88% rename from backend-contract/fixtures/wire/intent.add-segment-group.embedded.json rename to backend-contract/fixtures/wire/intent.import-segmentation.embedded.json index 5c6f74ba9..a0a1da4c5 100644 --- a/backend-contract/fixtures/wire/intent.add-segment-group.embedded.json +++ b/backend-contract/fixtures/wire/intent.import-segmentation.embedded.json @@ -1,6 +1,6 @@ { "id": "6600000000000000000000e2", - "intent": "add-segment-group", + "intent": "import-segmentation", "url": "/api/v1/file/6600000000000000000000e2/proxiable/threshold.seg.nrrd", "name": "threshold.seg.nrrd", "source": { diff --git a/backend-contract/fixtures/wire/intent.add-segment-group.with-segments.json b/backend-contract/fixtures/wire/intent.import-segmentation.with-segments.json similarity index 95% rename from backend-contract/fixtures/wire/intent.add-segment-group.with-segments.json rename to backend-contract/fixtures/wire/intent.import-segmentation.with-segments.json index fa0f30949..87cb530d3 100644 --- a/backend-contract/fixtures/wire/intent.add-segment-group.with-segments.json +++ b/backend-contract/fixtures/wire/intent.import-segmentation.with-segments.json @@ -1,6 +1,6 @@ { "id": "6600000000000000000000e1", - "intent": "add-segment-group", + "intent": "import-segmentation", "url": "/api/v1/file/6600000000000000000000e1/proxiable/otsu.nii.gz", "name": "otsu.nii.gz", "segments": [ diff --git a/backend-contract/generated/job-results.schema.json b/backend-contract/generated/job-results.schema.json index 79b8f1c1f..7b651e668 100644 --- a/backend-contract/generated/job-results.schema.json +++ b/backend-contract/generated/job-results.schema.json @@ -114,7 +114,7 @@ "properties": { "intent": { "type": "string", - "const": "add-segment-group" + "const": "import-segmentation" }, "id": { "type": "string", diff --git a/backend-contract/generated/openapi.json b/backend-contract/generated/openapi.json index ff8e661b1..847ae9b41 100644 --- a/backend-contract/generated/openapi.json +++ b/backend-contract/generated/openapi.json @@ -3,8 +3,8 @@ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "VolView neutral backend contract", - "version": "0.2.0", - "description": "DRAFT 0.x — shapes may change until a second backend passes the conformance kit (the pinned 1.0 criterion). The neutral REST surface the VolView client calls to run processing tasks against a backend. A conforming server-side BACKEND implements these endpoints and the referenced wire schemas — no VolView client change is needed to bring a new backend online. Everything here is neutral: no backend routes, ids, status enums, or URL shapes leak. The artifact version is the draft artifact version, distinct from the shape versions: the result-intent vocabulary is at version 2 (INTENT_VOCABULARY_VERSION); the task-spec shape at version 1 (specVersion)." + "version": "0.3.0", + "description": "DRAFT 0.x — shapes may change until a second backend passes the conformance kit (the pinned 1.0 criterion). The neutral REST surface the VolView client calls to run processing tasks against a backend. A conforming server-side BACKEND implements these endpoints and the referenced wire schemas — no VolView client change is needed to bring a new backend online. Everything here is neutral: no backend routes, ids, status enums, or URL shapes leak. The artifact version is the draft artifact version, distinct from the shape versions: the result-intent vocabulary is at version 3 (INTENT_VOCABULARY_VERSION); the task-spec shape at version 1 (specVersion)." }, "servers": [ { @@ -1203,7 +1203,7 @@ "properties": { "intent": { "type": "string", - "const": "add-segment-group" + "const": "import-segmentation" }, "id": { "type": "string", diff --git a/backend-contract/generated/result-intent.schema.json b/backend-contract/generated/result-intent.schema.json index e0b27e58b..9ba4f9ec8 100644 --- a/backend-contract/generated/result-intent.schema.json +++ b/backend-contract/generated/result-intent.schema.json @@ -102,7 +102,7 @@ "properties": { "intent": { "type": "string", - "const": "add-segment-group" + "const": "import-segmentation" }, "id": { "type": "string", diff --git a/backend-contract/package.json b/backend-contract/package.json index 1afe62c13..f1a07f920 100644 --- a/backend-contract/package.json +++ b/backend-contract/package.json @@ -1,5 +1,5 @@ { "name": "@volview/backend-contract", - "version": "0.2.0", + "version": "0.3.0", "private": true } diff --git a/backend-contract/processing/__tests__/wire.spec.ts b/backend-contract/processing/__tests__/wire.spec.ts index 603c1bed6..a472b54ef 100644 --- a/backend-contract/processing/__tests__/wire.spec.ts +++ b/backend-contract/processing/__tests__/wire.spec.ts @@ -194,12 +194,12 @@ describe('neutral job status fixtures', () => { // --------------------------------------------------------------------------- describe('result intent fixtures', () => { - it('exports vocabulary version 2 and the exactly-four state intents', () => { - expect(INTENT_VOCABULARY_VERSION).toBe(2); + it('exports vocabulary version 3 and the exactly-four state intents', () => { + expect(INTENT_VOCABULARY_VERSION).toBe(3); expect([...RESULT_INTENTS]).toEqual([ 'add-base-image', 'add-layer', - 'add-segment-group', + 'import-segmentation', 'add-annotations', ]); expect(wire).not.toHaveProperty('intent.download'); @@ -208,8 +208,8 @@ describe('result intent fixtures', () => { it.each([ 'intent.add-base-image', 'intent.add-layer', - 'intent.add-segment-group.with-segments', - 'intent.add-segment-group.embedded', + 'intent.import-segmentation.with-segments', + 'intent.import-segmentation.embedded', 'intent.add-annotations', 'intent.unknown', ])('validates %s', (name) => { @@ -253,11 +253,11 @@ describe('result intent fixtures', () => { ).toBe(false); }); - it('parses add-segment-group WITH segments and a source provenance tag', () => { + it('parses import-segmentation WITH segments and a source provenance tag', () => { const parsed = resultIntentSchema.parse( - wire['intent.add-segment-group.with-segments'] + wire['intent.import-segmentation.with-segments'] ) as Record; - expect(parsed.intent).toBe('add-segment-group'); + expect(parsed.intent).toBe('import-segmentation'); expect(Array.isArray(parsed.segments)).toBe(true); expect(parsed.source).toEqual({ providerId: 'analysis-provider', @@ -266,18 +266,18 @@ describe('result intent fixtures', () => { }); }); - it('parses add-segment-group WITHOUT segments (embedded metadata) but with source', () => { + it('parses import-segmentation WITHOUT segments (embedded metadata) but with source', () => { const parsed = resultIntentSchema.parse( - wire['intent.add-segment-group.embedded'] + wire['intent.import-segmentation.embedded'] ) as Record; - expect(parsed.intent).toBe('add-segment-group'); + expect(parsed.intent).toBe('import-segmentation'); expect(parsed.segments).toBeUndefined(); expect(parsed.source).toMatchObject({ outputId: 'outputLabelmap' }); }); it('rejects a segment-group source without provider identity', () => { const value = structuredClone( - wire['intent.add-segment-group.with-segments'] + wire['intent.import-segmentation.with-segments'] ) as { source: { providerId?: string } }; delete value.source.providerId; expect(knownResultIntentSchema.safeParse(value).success).toBe(false); @@ -373,7 +373,7 @@ describe('result intent fixtures', () => { expect(knownResultIntentSchema.safeParse(short).success).toBe(false); expect(resultIntentSchema.safeParse(short).success).toBe(true); - const good = wire['intent.add-segment-group.with-segments'] as { + const good = wire['intent.import-segmentation.with-segments'] as { segments: { color: number[] }[]; }; const long = structuredClone(good); diff --git a/backend-contract/processing/openapi.ts b/backend-contract/processing/openapi.ts index 8a62458d6..e9bda206d 100644 --- a/backend-contract/processing/openapi.ts +++ b/backend-contract/processing/openapi.ts @@ -474,7 +474,7 @@ export const buildOpenApiDocument = (): Record => ({ // VERSION / specVersion) below. It is deliberately literal, not derived from // the shape-version constants — the artifact and the shapes version on // separate clocks. - version: '0.2.0', + version: '0.3.0', description: 'DRAFT 0.x — shapes may change until a second backend passes the ' + 'conformance kit (the pinned 1.0 criterion). ' + diff --git a/backend-contract/processing/wire.ts b/backend-contract/processing/wire.ts index 27c2d41ba..a834c60bf 100644 --- a/backend-contract/processing/wire.ts +++ b/backend-contract/processing/wire.ts @@ -21,7 +21,7 @@ import { pathSegmentIdSchema } from './ids'; // Bump when the intent vocabulary's shape changes so producers and the applier // can negotiate compatibility. Adding an intent is a compatible bump: an older // client demotes the unknown intent through the fail-open branch above. -export const INTENT_VOCABULARY_VERSION = 2; +export const INTENT_VOCABULARY_VERSION = 3; // --------------------------------------------------------------------------- // Input value: what the client sends at submit @@ -158,7 +158,7 @@ export type NeutralJobStatus = z.infer; export const RESULT_INTENTS = [ 'add-base-image', 'add-layer', - 'add-segment-group', + 'import-segmentation', 'add-annotations', ] as const; export type ResultIntentName = (typeof RESULT_INTENTS)[number]; @@ -212,13 +212,13 @@ const addLayer = z .object({ intent: z.literal('add-layer'), ...resultListItemSchema.shape }) .passthrough(); -// `add-segment-group` carries OPTIONAL `segments` (the bare-labelmap + +// `import-segmentation` carries OPTIONAL `segments` (the bare-labelmap + // labels-sidecar case; a `seg.nrrd` with embedded metadata carries none — the // client uses `segments` when present, else the file's own metadata) and an // optional `source` provenance tag (the idempotency key). const addSegmentGroup = z .object({ - intent: z.literal('add-segment-group'), + intent: z.literal('import-segmentation'), ...resultListItemSchema.shape, segments: z.array(segmentDescriptorSchema).optional(), source: resultSourceSchema.optional(), diff --git a/src/processing/__tests__/applyResults.segments.spec.ts b/src/processing/__tests__/applyResults.segments.spec.ts index 2ff22019d..9cfc7bac1 100644 --- a/src/processing/__tests__/applyResults.segments.spec.ts +++ b/src/processing/__tests__/applyResults.segments.spec.ts @@ -33,7 +33,7 @@ const existingMask = (imageId: string, name: string) => { const importResult = (segments?: SegmentDescriptor[]) => applyIntent( { - intent: 'add-segment-group', + intent: 'import-segmentation', id: 'result', name: 'output.nrrd', url: 'https://example/output.nrrd', diff --git a/src/processing/__tests__/applyResults.spec.ts b/src/processing/__tests__/applyResults.spec.ts index 8131daf6d..2220231da 100644 --- a/src/processing/__tests__/applyResults.spec.ts +++ b/src/processing/__tests__/applyResults.spec.ts @@ -131,7 +131,7 @@ describe('applyIntent', () => { { value: 2, name: 'tumor', color: rgba(0, 255, 0, 255), visible: false }, ]; await apply( - { intent: 'add-segment-group', ...file, segments }, + { intent: 'import-segmentation', ...file, segments }, context('parent') ); expect(deps.segmentWriter.convertImageToLabelmap).toHaveBeenCalledWith( @@ -143,9 +143,9 @@ describe('applyIntent', () => { expect(deps.openVolumeUrls).not.toHaveBeenCalled(); }); - it('add-segment-group removes the temporarily imported child dataset', async () => { + it('import-segmentation removes the temporarily imported child dataset', async () => { const outcome = await apply( - { intent: 'add-segment-group', ...file }, + { intent: 'import-segmentation', ...file }, context('parent') ); expect(outcome.status).toBe('applied'); @@ -155,12 +155,12 @@ describe('applyIntent', () => { ); }); - it('add-segment-group removes the imported child even when conversion fails', async () => { + it('import-segmentation removes the imported child even when conversion fails', async () => { deps.segmentWriter.convertImageToLabelmap.mockRejectedValue( new Error('bounds do not intersect') ); const outcome = await apply( - { intent: 'add-segment-group', ...file }, + { intent: 'import-segmentation', ...file }, context('parent') ); expect(outcome.status).toBe('failed'); @@ -176,8 +176,8 @@ describe('applyIntent', () => { expect(deps.removeDataset).not.toHaveBeenCalled(); }); - it('add-segment-group with no segments still converts (embedded metadata)', async () => { - await apply({ intent: 'add-segment-group', ...file }, context('parent')); + it('import-segmentation with no segments still converts (embedded metadata)', async () => { + await apply({ intent: 'import-segmentation', ...file }, context('parent')); expect(deps.segmentWriter.convertImageToLabelmap).toHaveBeenCalledWith( 'child-selection', 'parent', @@ -193,7 +193,7 @@ describe('applyIntent', () => { outputId: 'outputLabelmap', }; await apply( - { intent: 'add-segment-group', ...file, source }, + { intent: 'import-segmentation', ...file, source }, context('parent') ); expect(deps.segmentWriter.convertImageToLabelmap).toHaveBeenCalledWith( @@ -213,7 +213,7 @@ describe('applyIntent', () => { deps.segmentWriter.resultSourcesInScene.mockReturnValue([source]); const outcome = await apply( - { intent: 'add-segment-group', ...file, source }, + { intent: 'import-segmentation', ...file, source }, context('parent') ); @@ -229,7 +229,7 @@ describe('applyIntent', () => { deps.segmentWriter.resultSourcesInScene.mockReturnValue([inScene]); const outcome = await apply( - { intent: 'add-segment-group', ...file, source }, + { intent: 'import-segmentation', ...file, source }, context('parent') ); @@ -258,7 +258,7 @@ describe('applyIntent', () => { deps.segmentWriter.resultSourcesInScene.mockReturnValue([undefined]); const outcome = await apply( - { intent: 'add-segment-group', ...file }, + { intent: 'import-segmentation', ...file }, context('parent') ); @@ -266,8 +266,8 @@ describe('applyIntent', () => { expect(deps.segmentWriter.convertImageToLabelmap).toHaveBeenCalledTimes(1); }); - it('add-segment-group with no originating dataset falls back to opening', async () => { - await apply({ intent: 'add-segment-group', ...file }, context(undefined)); + it('import-segmentation with no originating dataset falls back to opening', async () => { + await apply({ intent: 'import-segmentation', ...file }, context(undefined)); expect(deps.segmentWriter.convertImageToLabelmap).not.toHaveBeenCalled(); expect(deps.openVolumeUrls).toHaveBeenCalledWith({ urls: [file.url], @@ -275,10 +275,10 @@ describe('applyIntent', () => { }); }); - it('add-segment-group reports an explicit failure when the result fails to load (#7)', async () => { + it('import-segmentation reports an explicit failure when the result fails to load (#7)', async () => { deps.importVolume.mockResolvedValue(null); const applied = await apply( - { intent: 'add-segment-group', ...file }, + { intent: 'import-segmentation', ...file }, context('parent') ); expect(deps.segmentWriter.convertImageToLabelmap).not.toHaveBeenCalled(); @@ -330,7 +330,7 @@ describe('autoLoadProcessingResults', () => { result({ id: 'b', intent: 'add-layer' }), result({ id: 'c', - intent: 'add-segment-group', + intent: 'import-segmentation', source: { providerId: 'p1', jobId: 'j1', outputId: 'seg' }, segments: [{ value: 1, name: 'liver', color: rgba(1, 2, 3, 4) }], }), @@ -369,7 +369,7 @@ describe('autoLoadProcessingResults', () => { it('opens a parentless segment-group result as an ordinary dataset', async () => { await autoLoad( - [result({ intent: 'add-segment-group' })], + [result({ intent: 'import-segmentation' })], context(undefined) ); expect(deps.segmentWriter.convertImageToLabelmap).not.toHaveBeenCalled(); @@ -386,8 +386,8 @@ describe('autoLoadProcessingResults', () => { .mockResolvedValueOnce([importedComponent({ 1: 'segment-g2' })]); const application = await autoLoad( [ - result({ id: 'a', intent: 'add-segment-group' }), - result({ id: 'b', intent: 'add-segment-group' }), + result({ id: 'a', intent: 'import-segmentation' }), + result({ id: 'b', intent: 'import-segmentation' }), ], context('parent') ); @@ -422,12 +422,12 @@ describe('autoLoadProcessingResults', () => { [ result({ id: 'restored', - intent: 'add-segment-group', + intent: 'import-segmentation', source: restoredSource, }), result({ id: 'new', - intent: 'add-segment-group', + intent: 'import-segmentation', source: newSource, }), ], @@ -448,7 +448,7 @@ describe('autoLoadProcessingResults', () => { describe('autoLoadProcessingResults — labelmap auto-apply', () => { const segResult = (overrides: Partial = {}) => - result({ id: 'seg', intent: 'add-segment-group', ...overrides }); + result({ id: 'seg', intent: 'import-segmentation', ...overrides }); it('auto-applies an importable labelmap', async () => { deps.segmentWriter.convertImageToLabelmap.mockResolvedValue([ @@ -483,7 +483,7 @@ describe('autoLoadProcessingResults — born-persistent (no confirm gate)', () = importedComponent({ 1: 'segment-1' }), ]); await autoLoad( - [result({ id: 'seg', intent: 'add-segment-group', source })], + [result({ id: 'seg', intent: 'import-segmentation', source })], context('parent') ); expect(deps.segmentWriter.convertImageToLabelmap).toHaveBeenCalledWith( diff --git a/src/processing/applyResults.ts b/src/processing/applyResults.ts index cbb2f3b50..de65ec68f 100644 --- a/src/processing/applyResults.ts +++ b/src/processing/applyResults.ts @@ -41,7 +41,7 @@ type ResultFile = { url: string; name: string }; type SegmentationIntent = Extract< KnownResultIntent, - { intent: 'add-segment-group' } + { intent: 'import-segmentation' } >; type AnnotationsIntent = Extract< KnownResultIntent, @@ -448,7 +448,7 @@ export async function applyIntent( } return { status: 'applied' }; } - case 'add-segment-group': { + case 'import-segmentation': { // Session-restored groups retain their result source. Treat that // durable provenance as an application receipt so retrying Load is // idempotent instead of creating a duplicate group. diff --git a/src/processing/engine/__tests__/resultFiles.spec.ts b/src/processing/engine/__tests__/resultFiles.spec.ts index 838f992aa..a15bdd2e5 100644 --- a/src/processing/engine/__tests__/resultFiles.spec.ts +++ b/src/processing/engine/__tests__/resultFiles.spec.ts @@ -24,7 +24,7 @@ describe('sceneApplicableResults', () => { it('drops a result whose intent name is known but shape is invalid', () => { const malformed = result({ - intent: 'add-segment-group', + intent: 'import-segmentation', segments: [{ value: 0, name: 'bad', color: [0, 0, 0, 255] }], }); expect(sceneApplicableResults([malformed])).toEqual([]); diff --git a/src/processing/engine/__tests__/resultToIntent.spec.ts b/src/processing/engine/__tests__/resultToIntent.spec.ts index 8e7d21729..d0a063003 100644 --- a/src/processing/engine/__tests__/resultToIntent.spec.ts +++ b/src/processing/engine/__tests__/resultToIntent.spec.ts @@ -19,7 +19,7 @@ describe('resultToIntent', () => { [ 'malformed', result({ - intent: 'add-segment-group', + intent: 'import-segmentation', segments: [{ value: 0, name: 'bad', color: [0, 0, 0, 255] }], }), ], diff --git a/src/processing/engine/__tests__/wire.spec.ts b/src/processing/engine/__tests__/wire.spec.ts index 3d6f27c02..041d679fa 100644 --- a/src/processing/engine/__tests__/wire.spec.ts +++ b/src/processing/engine/__tests__/wire.spec.ts @@ -160,7 +160,7 @@ describe('parseResults', () => { id: 'r1', name: 'seg.nrrd', url: 'https://example/seg.nrrd', - intent: 'add-segment-group', + intent: 'import-segmentation', segments: [{ value: 1, name: 'liver', color: [255, 0, 0, 255] }], extra: 'keep-me', }, @@ -176,7 +176,7 @@ describe('parseResults', () => { id: 'r1', name: 'seg.nrrd', url: 'https://example/seg.nrrd', - intent: 'add-segment-group', + intent: 'import-segmentation', segments: [{ value: 0, name: 'bg', color: [300, -5, 0, 255] }], }, ]; From 8f1f0edc61296a7966f9ac639f29aa8ca81ac065 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Tue, 15 Sep 2026 15:34:49 -0400 Subject: [PATCH 02/15] feat(segmentation): choose labelmap bit depth from label count Composed labelmaps use unsigned 8-bit voxels for up to 255 labels and unsigned 16-bit voxels for 256 through 65535. An imported 16-bit labelmap splits into byte masks in one sweep. Fractional, NaN and infinite voxel values are excluded during import. --- src/processing/components/JobsModule.vue | 7 +- .../components/__tests__/JobsModule.spec.ts | 2 +- src/segmentation/README.md | 11 ++ .../__tests__/boundedSegmentMasks.spec.ts | 7 +- .../__tests__/segmentMaskComposite.spec.ts | 70 +++++++--- .../components/SaveSegmentationDialog.vue | 24 +++- .../io/__tests__/labelmap.spec.ts | 86 ++++++++++++ src/segmentation/io/composition.ts | 79 ++++------- src/segmentation/io/import.ts | 99 +++++--------- src/segmentation/io/labelmap.ts | 65 +++++++++ src/segmentation/masks/labelValue.ts | 26 ---- src/segmentation/masks/overlap.ts | 2 +- src/segmentation/masks/voxelAccess.ts | 11 +- src/segmentation/store.ts | 6 +- tests/specs/adaptive-labelmap.e2e.ts | 124 ++++++++++++++++++ 15 files changed, 424 insertions(+), 195 deletions(-) create mode 100644 src/segmentation/io/__tests__/labelmap.spec.ts create mode 100644 src/segmentation/io/labelmap.ts create mode 100644 tests/specs/adaptive-labelmap.e2e.ts diff --git a/src/processing/components/JobsModule.vue b/src/processing/components/JobsModule.vue index 9b7ee50be..2057ce567 100644 --- a/src/processing/components/JobsModule.vue +++ b/src/processing/components/JobsModule.vue @@ -124,7 +124,7 @@ diff --git a/src/segmentation/components/__tests__/SegmentList.spec.ts b/src/segmentation/components/__tests__/SegmentList.spec.ts index 38331c690..10eac1c89 100644 --- a/src/segmentation/components/__tests__/SegmentList.spec.ts +++ b/src/segmentation/components/__tests__/SegmentList.spec.ts @@ -495,7 +495,7 @@ describe('flat segment list row actions', () => { await nextTick(); expect(lockTooltip(wrapper, segment.id)).toMatch(/^Unlock\b/); - expect(lockTooltip(wrapper, segment.id)).toMatch(/takes its voxels/i); + expect(lockTooltip(wrapper, segment.id)).toMatch(/replaces its voxels/i); }); it('deletes one type, with the masks it had, by id', async () => { From be2a1f55adfb50f2f10d3731072163ad0dd1a305 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Fri, 25 Sep 2026 12:44:42 -0400 Subject: [PATCH 15/15] fix(settings): switch theme with theme.change --- src/components/Settings.vue | 2 +- src/plugins/vuetify.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Settings.vue b/src/components/Settings.vue index 94ca96577..5d81bdf4c 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -77,7 +77,7 @@ export default defineComponent({ const dark = ref(theme.global.name.value === DarkTheme); watch(dark, (isDark) => { - theme.global.name.value = isDark ? DarkTheme : LightTheme; + theme.change(isDark ? DarkTheme : LightTheme); store.value = theme.global.name.value; }); diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js index 3cfa1a0e0..ff3db2278 100644 --- a/src/plugins/vuetify.js +++ b/src/plugins/vuetify.js @@ -48,6 +48,6 @@ const theme = useLocalStorage(ThemeStorageKey, DefaultTheme); if (theme.value !== DarkTheme && theme.value !== LightTheme) { theme.value = DefaultTheme; } -vuetify.theme.global.name.value = theme.value; +vuetify.theme.change(theme.value); export default vuetify;