M3D Viewer: 3D side panel and viewport integration for STL series (visibility, active/lock wireframe, color)
1. Overview
Provide a 3D sidebar panel that allows users to modify the presentation state of the Three.js (M3D) viewport, using the same Segmentation Table components used elsewhere in the Sonador Viewer. From the panel a user can:
- Toggle the display of a model on/off.
- Toggle a model between active (regular rendering) and locked (wireframe rendering).
- Change a model's color via the color picker dialog.
STL series only. GLB series load a complete scene comprised of multiple objects; introspection/interaction with GLB scenes is out of scope (see §9). If the series is a GLB series, the side-panel should not be visible.
Design elaborated from the discussion on imaging-development-env#95 (notes of 2026-07-11).
2. Background and current state
-
extensions/viewer3d/src/components/panels/M3DViewerPanel.jsxis currently a stub: it attachesattachCoreSegmentationTableEventsand renders a bareSegmentationTabletitled "Models" with no data source, no header content, and no action callbacks. This issue replaces the stub with a functional panel. -
extensions/viewer3d/src/ohifComponents/OHIFDicomM3DViewport.jsacquires models through the reference-counted M3D geometry cache (m3dCache/m3dCacheService.js, cache-first + single-flight over the Cornerstone3D geometry cache) and hydrates per-viewport Three.js instances (m3dCache/hydrateM3DInstance.js). For STL, each hydrated instance is a freshMeshover the sharedBufferGeometrywith its ownMeshStandardMaterial— per-viewport color/wireframe changes are already safe by construction. - Per-instance display color is resolved from series metadata by
OHIFDicomM3DViewport.getInstanceColor()(RecommendedDisplayCIELabValue→ hex viadcmjs.data.Colors.dicomlab2RGB), available without fetching the encapsulated document. - The wiki pages 3D Model Viewer (Three.js / M3D) and VTK.js <-> Cornerstone3D Integration document the cache layer and the volume/MPR patterns this feature mirrors; the Cornerstone3D Integration Reference documents the conceptual model.
3. Functional requirements
-
FR-1 — Model list. For an STL series, the side panel lists every model (DICOM instance) in the series using the
@ohif/ui-nextSegmentationTablecomponents. -
FR-2 — Header. The M3D/STL series title is displayed in the Segmentation Header (
SonadorSegmentationHeader,extensions/cornerstone/src/components/SonadorSegmentationHeader.jsx), following the layout used bySegmentationEditorPanel.jsx. -
FR-3 — Row identity. Each row displays: number taken from the instance's Instance Number
(0020,0013); title/description loaded from the DICOM Content Description(0070,0081)(fallback: Content Label(0070,0080), thenModel <n>); color dot taken from the model color (§2,getInstanceColor; fallbackdefaultGeometryColor). -
FR-4 — Visibility toggle. Per-model show/hide via the row's eye toggle, plus the whole-series toggle on the table header. Hidden models set
mesh.visible = falsein every M3D viewport displaying the series. -
FR-5 — Active/lock toggle. A model is either active (regular material rendering) or locked (rendered in wireframe:
material.wireframe = true). Lock is toggled from the row actions menu. -
FR-6 — Color change. Row action opens the color picker dialog (
callColorPickerDialogfrom@ohif/extension-cornerstonecextUtils, as used byeditSegmentColorinextensions/vtk/src/commandsModule.js); the chosen color is applied to the model material in every M3D viewport. - FR-7 — Action visibility. Only Change Color and Lock/Unlock appear in the row actions menu. Rename and Delete must not be visible, and there is no Add Segment row. Requires the component refactor in §6.
- FR-8 — Multi-viewport consistency. All M3D viewports displaying the same series render the same presentation state (visibility, lock/wireframe, color). When more than one viewport is visible, only the first initializes the segmentation state (labels, colors); subsequent viewports consume the existing state.
-
FR-9 — STL gating. The interactive features are enabled only when the display set's model type is STL (
MIMETYPE_STLfromextensions/viewer3d/src/sopClassHandlers/OHIFDicom3DSopClassHandler.js). For GLB series the panel renders the series header only, with no rows or actions.
4. Architectural requirements
These follow the architectural notes on imaging-development-env#95, expanded against the source:
-
AR-1 — State of record: Cornerstone3D segmentation metadata. Model presentation state is stored in the Cornerstone3D segmentation state (
segmentation.statefrom@cornerstonejs/tools) as a segmentation whose segments correspond to the STL instances. State replication between the panel and viewports is driven by SonadorSegmentationServiceevents (extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts) — the same service the Segmentation Editor and volume viewer use. -
AR-2 — No viewport or representation registration. The Three.js viewer is not part of Cornerstone3D's rendering system. M3D viewports must not register a Cornerstone3D viewport ID and must not call
addSegmentationRepresentations. (The existing_viewportIdGUID inOHIFDicomM3DViewportis a cache-refcount identity only and stays out of Cornerstone3D state.) -
AR-3 — Same loading patterns as volume/MPR. Segmentation metadata loading follows the patterns in the volume viewport and MPR views: register segmentation state with
c3dSegmentations.state.addSegmentations([...])during load (compareCornerstone3DLabelmapBaseView.loadSegImageVolume,extensions/vtk/src/components/Cornerstone3DLabelmapBaseView.js); publish the segmentation ID on the display set (displaySet.segmentationIdviaDisplaySetApi) so panels bootstrap throughattachCoreSegmentationTableEvents/attachSegmentationAddTableEvents(extensions/viewer3d-volume/src/utils/cornerstone3dSegmentations.js) exactly asSegmentationEditorPanel.jsxandVolumeViewerPanel.jsxdo. -
AR-4 — First-viewport initialization. Before registering, check
c3dSegmentations.state.getSegmentation(segmentationId); if present, skip registration and consume existing state (FR-8). -
AR-5 — Event-driven viewport updates. The M3D viewport subscribes to
SegmentationServiceevents and updates local Three.js display state by pulling current state from the Cornerstone3D metadata cache (c3dSegmentations.state.getSegmentation,segmentLocking.isSegmentIndexLocked,segmentIndex.getActiveSegmentIndex) — events signal that something changed; the metadata is the source of truth for what the state is. This mirrors howSegmentationEditorLayout.js(extensions/seg-editor) consumesSEGMENT_ACTIVE/SEGMENT_LOCK/SEGMENT_COLOR_MODIFIEDevents.
5. State model
5.1 Segmentation registration (new: extensions/viewer3d/src/m3dCache/m3dSegmentationState.js)
A helper module alongside the existing m3dCache files:
-
getM3DSegmentationId(seriesInstanceUID)→m3dseg:<SeriesInstanceUID>— parallel to them3d:<SOPInstanceUID>scheme inm3dGeometryId.js. -
registerM3DSegmentation({ displaySet, models })— called byOHIFDicomM3DViewportonce all STL models are acquired (end offetchModel). Guards per AR-4, then:-
c3dSegmentations.state.addSegmentations([{ segmentationId, representation: { type: SegmentationRepresentations.Labelmap, data: {} }, config: { label: <SeriesDescription>, segments } }])— Labelmap type is nominal; no representation is ever attached to a viewport (AR-2). -
segmentskeyed by InstanceNumber:{ segmentIndex, label: <ContentDescription>, active: true, locked: false, visible: true, color: <model hex color>, sopInstanceUID, geometryId }. Instance metadata is read viaseries.getInstanceByUID(sop).getData().metadata(same access pattern asgetInstanceColor).sopInstanceUID/geometryIdprovide the segmentIndex <-> Three.js model mapping. - Publishes
displaySet.segmentationIdthroughDisplaySetApi.Instance.displaySetService.addDisplaySets([...])(AR-3).
-
-
releaseM3DSegmentation(seriesInstanceUID)— removes the segmentation from Cornerstone3D state when the last M3D viewport for the series unmounts. Reference counting piggybacks onm3dCacheService.getReferenceCount()for the series' geometry IDs, so segmentation lifetime matches cached-geometry lifetime.
5.2 Viewport-independent state fields
| State | Storage | Read API | Notes |
|---|---|---|---|
| Active segment | Cornerstone3D | segmentIndex.getActiveSegmentIndex(segId) |
Already viewport-free |
| Locked | Cornerstone3D | segmentLocking.isSegmentIndexLocked(segId, idx) |
Already viewport-free; drives wireframe |
| Label | Segmentation segments[idx].label
|
state.getSegmentation(segId) |
Set at registration from Content Description |
| Visibility |
Segment metadata segments[idx].visible
|
state.getSegmentation(segId) |
Per-viewport config.visibility API is unavailable without registered viewports (AR-2) |
| Color |
Segment metadata segments[idx].color
|
state.getSegmentation(segId) |
Per-viewport config.color API is unavailable without registered viewports (AR-2) |
Mutations to the metadata-backed fields go through segmentationService.addOrUpdateSegmentation({ segmentationId, segments }) so SEGMENTATION_MODIFIED fires and every subscriber re-pulls state (AR-5). Lock and active use the service's viewport-free methods (toggleSegmentLocked, setActiveSegment) which emit SEGMENT_LOCK / SEGMENT_ACTIVE.
5.3 Table-data mapping in viewport-free mode
c3dSeg2SegmentationTableData() and syncTableSegRepData() (extensions/viewer3d-volume/src/utils/cornerstone3dSegmentations.js) currently throw when getViewportIdsWithSegmentation() returns no viewports, and read color/visibility from the per-viewport config APIs. Add a source: 'viewport' | 'metadata' option (default 'viewport', preserving current behavior for all existing panels):
-
metadatamode skips the active-viewports check and readscolorandvisiblefrom the segment metadata fields (§5.2);activeandlockedcontinue to use the viewport-free APIs they already use. -
attachCoreSegmentationTableEventsaccepts and forwards the option so the M3D panel'sSEGMENTATION_MODIFIEDhandler regenerates table data in metadata mode.
6. Component refactor: action items render only when handlers are defined
platform/ui-next/src/components/DataRow/DataRow.tsx currently renders Rename, Delete, and Lock menu items unconditionally (only Copy and Color are conditional on their handlers), and platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx always passes defined wrapper functions for onRename/onDelete even when the table-context callbacks (onSegmentEdit, onSegmentDelete) were never provided. Required changes:
-
DataRow.tsx— makeonRename,onDelete,onToggleLockedoptional inDataRowProps, and render eachDropdownMenuItem(Rename / Duplicate / Delete / Change Color / Lock-Unlock) only when its handler is defined — the pattern already used foronCopyandonColor. When no action handlers are defined at all, render the placeholder spacer (<div className="h-6 w-6" />) instead of the dropdown trigger, matching the existingdisableEditingbranch. -
SegmentationSegments.tsx— forwardundefinedrather than an always-defined wrapper when the corresponding context callback is absent, e.g.onRename={onSegmentEdit ? () => onSegmentEdit(segmentationId, segmentIndex) : undefined}(same treatment foronDelete,onToggleLocked,onColor,onToggleVisibility,onSelect). -
Compatibility. These components were ported from OHIF v3 in the current development branch; keep the change additive (optional props, presence checks) so panels that pass every handler —
SegmentationEditorPanel.jsx,VolumeViewerPanel.jsx,SonadorMprSegmentationPanel.jsx— render exactly as today.
7. Implementation plan by file
platform/ui-next
-
components/DataRow/DataRow.tsx— conditional action items (§6). -
components/SegmentationTable/SegmentationSegments.tsx— conditional handler forwarding (§6).
extensions/viewer3d
-
src/m3dCache/m3dSegmentationState.js(new) — id scheme,registerM3DSegmentation,releaseM3DSegmentation(§5.1); export fromm3dCache/index.js. -
src/ohifComponents/OHIFDicomM3DViewport.js—- After STL
fetchModelcompletes:registerM3DSegmentation(guarded per AR-4); publishdisplaySet.segmentationId. - Subscribe to
segmentationServiceSEGMENTATION_MODIFIED,SEGMENT_LOCK,SEGMENT_ACTIVE; on each event pull segmentation metadata and reconcile every model:mesh.visiblefromvisible,material.wireframefrom locked,material.colorfromcolor(mapping segmentIndex -> model viasopInstanceUID/geometryId). Unsubscribe incomponentWillUnmountand on display-set change, following the subscribe/unsubscribe discipline inSegmentationEditorLayout.js. -
releaseM3DSegmentationalongsidereleaseAcquiredModels(). - Needs
servicesManager(orsegmentationService) passed down — follow howConnectedOHIFDicomM3DViewportwires props today.
- After STL
-
src/threejs/M3DModelView.js— extend theapiobject handed toonCreatedwith per-model presentation setters:setModelVisibility(geometryId, visible),setModelWireframe(geometryId, wireframe),setModelColor(geometryId, hexColor), plusgetModelPresentation(geometryId). STL instances own theirMeshStandardMaterial(hydrateM3DInstance), so material mutations are per-viewport-safe; a shared-state reconcile from segmentation metadata keeps viewports consistent (FR-8). -
src/components/panels/M3DViewerPanel.jsx— replace the stub (§8).
extensions/viewer3d-volume
-
src/utils/cornerstone3dSegmentations.js—source: 'metadata'mode (§5.3).
8. Panel specification (M3DViewerPanel.jsx)
Follow the structure of SegmentationEditorPanel.jsx (state refs, attachCoreSegmentationTableEvents + attachSegmentationAddTableEvents with { source: 'metadata' }, portal container, SonadorSegmentationPanelTheme.module.scss):
-
SegmentationTablewithtitle={t('Models')},mode='expanded',data={m3dSegmentations}. -
SegmentationTable.ExpandedcontainingSonadorSegmentationHeader(series title; no Rename dropdown item — pass nodropdownMenuContentor an empty one) andSegmentationTable.Segments. NoSegmentationTable.AddSegmentRow, noSegmentationTable.Config(labelmap fill/outline styles do not apply to meshes). - Callbacks supplied:
onToggleSegmentationRepresentationVisibility(all models),onToggleSegmentVisibility,onToggleSegmentLock->segmentationService.toggleSegmentLocked,onSegmentClick->segmentationService.setActiveSegment,onSegmentColorClick->callColorPickerDialogseeded from segment metadata color, writing back viaaddOrUpdateSegmentation. Callbacks not supplied (and therefore not rendered, per §6):onSegmentEdit,onSegmentDelete,onSegmentAdd,onSegmentCopy. - STL gating per FR-9, keyed off the display set's model type.
9. Out of scope
- GLB series: scene introspection, per-node presentation control, and any GLB entries in the panel beyond the series header.
- Undo/redo of presentation changes; persistence of presentation state beyond the session.
- Writing presentation state back to DICOM.
- OpenCascade/CAD interactions with the retained source bytes.
10. Acceptance criteria
- Opening an STL series shows the Models panel with one row per instance: number = Instance Number, title = Content Description, color dot = model color.
- Eye toggle hides/shows the model in the 3D viewport; header toggle hides/shows all models.
- Lock action renders the model in wireframe; unlock restores regular rendering.
- Change Color opens the color picker and applies the color to the model.
- Row actions menu contains only Change Color and Lock/Unlock; Rename/Delete/Add Segment are absent. Panels that pass full handler sets are visually unchanged.
- With two viewports on the same STL series, presentation changes made in the panel appear in both; the second viewport does not re-initialize labels/colors.
-
No Cornerstone3D viewport IDs or segmentation representations are registered by M3D viewports (verify via
c3dSegmentations.state.getViewportIdsWithSegmentationreturning empty for the M3D segmentation). - GLB series: panel shows the series header only; no interactive rows.
-
Unmounting the last M3D viewport for a series removes its segmentation from Cornerstone3D state; no leaked subscriptions (see the cleanup discipline in
OHIFDicomM3DViewport.releaseAcquiredModels/componentWillUnmount).
11. References
- Discussion/origin: imaging-development-env#95
- Related: #92, oak-tree/medical-imaging/ohif-viewers!58
- Wiki: 3D Model Viewer (Three.js / M3D), VTK.js <-> Cornerstone3D Integration, Cornerstone3D Integration Reference
- Cornerstone3D documentation: Segmentation concepts, segmentation state API, segment locking, active segment index
- DICOM: Encapsulated STL IOD Content Identification Macro — Instance Number
(0020,0013), Content Label(0070,0080), Content Description(0070,0081);RecommendedDisplayCIELabValue(0062,000D)