Skip to content

ECG Viewe || Smart Paint || Flatfoot Analysis#5913

Open
onkar76 wants to merge 7 commits intoOHIF:release/3.12from
onkar76:feature/ecg-viewr-3.12
Open

ECG Viewe || Smart Paint || Flatfoot Analysis#5913
onkar76 wants to merge 7 commits intoOHIF:release/3.12from
onkar76:feature/ecg-viewr-3.12

Conversation

@onkar76
Copy link
Copy Markdown

@onkar76 onkar76 commented Mar 21, 2026

Context

Changes & Results

Testing

ECG Viewer — Analyze heart ECG strips, measure QT intervals, calculate heart rate and cardiac axis
Smart Paint — Paint/highlight regions on any medical image, measure painted area
Flatfoot Analysis — Measure foot arch angles on X-rays to detect flat foot or high arch

Checklist

PR

  • [] My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • [] My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • [] The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • [] OS:
  • [] Node version:
  • [] Browser:

Greptile Summary

This PR adds three new clinical analysis extensions (ECG Viewer, Smart Paint ROI, and Flatfoot Analysis) to the OHIF viewer, along with a new ECG mode, a Clinical Hub aggregator route, and shortcut buttons on the worklist. It also modifies the longitudinal and basic modes to enable segmentation editing by default, and adds an embedded-iframe overlay capability to ViewerLayout.

Key issues found:

  • P0 runtime crash: A stray true literal is appended to _activatePanelTriggersSubscriptions in modes/longitudinal/src/index.ts. When the mode exits and iterates over subscriptions to call .unsubscribe(), it will throw a TypeError: true.unsubscribe is not a function.
  • P1 silent regression in longitudinal mode: The new onModeEnter overrides any base-mode onModeEnter from basicModeInstance without delegating to it, potentially skipping essential toolbar and tool-group initialization.
  • P1 architectural violation: All three extension panels (PanelEcgViewer, PanelSmartPaint, PanelFlatfoot) import SegmentLabelPanel via a relative path from platform/app/src/components. Extensions must not depend on the application layer; the shared component should live in @ohif/ui or a dedicated shared package.
  • P1 breaking default changes in basic mode: rightPanelClosed is flipped from true to false and enableSegmentationEdit from false to true, silently changing UX defaults for all users of the basic viewer.
  • P1 missing version in pluginConfig.json: The new @custom/extension-ecg-tools entry lacks the "version" field required by every other plugin entry; extension-smart-paint and extension-flatfoot are not registered at all.
  • P2 dead code: An unused mods variable in modes/ecg/src/index.tsx:isValidMode.
  • P2 style: EMBED_VIEWER_EVENT constant is placed between import statements in ViewerLayout/index.tsx.

Confidence Score: 2/5

  • Not safe to merge: a P0 runtime crash on mode exit and several P1 architectural and behavioral regressions need to be resolved first.
  • The stray true in the subscriptions array is a guaranteed TypeError on every longitudinal mode exit. The silent default changes to the basic mode (open right panel + segmentation edit enabled) are visible UX regressions for existing users. The extension–app import boundary violation will create long-term maintainability problems. These three issues together bring the score to 2.
  • modes/longitudinal/src/index.ts (P0 crash), modes/basic/src/index.tsx (breaking defaults), extensions/ecg-tools/src/panels/PanelEcgViewer.tsx + extensions/smart-paint/src/panels/PanelSmartPaint.tsx + extensions/flatfoot/src/panels/PanelFlatfoot.tsx (architectural violation), platform/app/pluginConfig.json (missing version)

Important Files Changed

Filename Overview
modes/longitudinal/src/index.ts Adds segmentation tool groups and panel triggers to the longitudinal mode's onModeEnter; contains a critical P0 bug (literal true appended to the subscriptions array) and a P1 concern around the base onModeEnter not being delegated to.
extensions/ecg-tools/src/panels/PanelEcgViewer.tsx New 932-line ECG measurement panel with canvas-based annotation; functionally complete but imports SegmentLabelPanel directly from platform/app, violating OHIF's extension–app boundary.
extensions/smart-paint/src/tools/SmartPaintTool.ts New brush painting utility (mask, undo/redo, contour tracing); logic is sound but renderMaskOverlay creates a throwaway canvas element on every call for color parsing, causing unnecessary GC pressure.
extensions/flatfoot/src/panels/PanelFlatfoot.tsx New 989-line foot arch measurement panel with canvas-based annotation tools; imports SegmentLabelPanel from platform/app (same architectural violation as PanelEcgViewer), otherwise self-contained.
modes/ecg/src/index.tsx New ECG mode definition; contains a dead mods variable in isValidMode that is computed but never used.
modes/basic/src/index.tsx Changes rightPanelClosed from true→false and enableSegmentationEdit from false→true in the basic viewer layout, silently altering default UX for all users of the basic mode.
extensions/default/src/ViewerLayout/index.tsx Adds an embedded iframe overlay triggered via a custom DOM event (ohif:embedViewer); logic is correct but the EMBED_VIEWER_EVENT constant is placed between import statements rather than after them.
platform/app/pluginConfig.json Registers @custom/extension-ecg-tools but omits the required "version" field present on every other entry; extension-smart-paint and extension-flatfoot are not registered here at all.
platform/app/src/routes/ViewerHub/ViewerHubRoute.tsx New Clinical Hub route that composes ECG, Smart Paint and Flatfoot panels under a shared segmentation sidebar; straightforward and well-structured.
platform/i18n/src/index.js Monkey-patches LanguageDetector.prototype.detect to inject a missing isWhitelisted shim for newer i18next; fragile approach that could silently fail across library upgrades.

Sequence Diagram

sequenceDiagram
    participant WL as WorkList
    participant Router as App Router
    participant Hub as ViewerHubRoute
    participant ECG as PanelEcgViewer
    participant SP as PanelSmartPaint
    participant FF as PanelFlatfoot
    participant VL as ViewerLayout
    participant DOM as window CustomEvent

    WL->>Router: navigate /viewer-hub or /ecg-viewer
    Router->>Hub: render ViewerHubRoute
    Hub->>ECG: React.lazy load PanelEcgViewer
    Hub->>SP: React.lazy load PanelSmartPaint
    Hub->>FF: React.lazy load PanelFlatfoot
    note over ECG,FF: All three panels import SegmentLabelPanel from platform/app (arch violation)
    DOM->>VL: CustomEvent ohif:embedViewer with url
    VL->>VL: setEmbeddedViewerUrl(url)
    note over VL: Replace ResizablePanelGroup with iframe
    VL->>DOM: closeEmbedded dispatches ohif:embedViewer url null
Loading

Last reviewed commit: "Merge branch 'releas..."

Greptile also left 8 inline comments on this PR.

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

@onkar76 onkar76 changed the title Feature/ecg viewr 3.12 ECG Viewe || Smart Paint || Flatfoot Analysis Mar 21, 2026
...panelService.addActivatePanelTriggers(
cornerstone.labelMapSegmentationPanel,
[
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Stray true corrupts subscriptions array

The literal true appended to _activatePanelTriggersSubscriptions will cause a runtime error when the mode exits and iterates over the subscriptions to call .unsubscribe() on each entry. true is not an object and has no .unsubscribe() method.

Suggested change
{
true

should be removed entirely:

          ...panelService.addActivatePanelTriggers(
            tracked.measurements,
            [
              {
                sourcePubSubService: measurementService,
                sourceEvents: [
                  measurementService.EVENTS.MEASUREMENT_ADDED,
                  measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
                ],
              },
            ],
            true
          ),
        ];

@@ -63,6 +66,69 @@ export const modeInstance = {
longitudinalRoute
],
extensions: extensionDependencies,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 New onModeEnter silently drops base-mode initialization

The new onModeEnter added to modeInstance overrides any onModeEnter that exists on basicModeInstance (if the base object contains one) without delegating to it. Any initialization logic defined there — toolbar registration, tool-group setup, panel triggers — will be silently skipped for the longitudinal mode.

If basicModeInstance does not have an onModeEnter, this is harmless; but it should be made explicit. If it does, call it first:

onModeEnter(args) {
  // delegate to base implementation if present
  basicModeInstance.onModeEnter?.(args);

  const { servicesManager, extensionManager, commandsManager, panelService, segmentationService } = args;
  // ... rest of the new logic
},

Comment on lines +38 to +40
{
path: 'dicomlocal',
layoutTemplate: () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused variable mods

mods is computed but never read. The function always returns { valid: true } regardless of the modality split, so the variable is dead code.

Suggested change
{
path: 'dicomlocal',
layoutTemplate: () => {
isValidMode({ modalities }) {
// ECG mode is valid for ECG modality and all non-DICOM image uploads
return { valid: true, description: 'ECG & Clinical Tools' };
},

Comment on lines +68 to +70
},
{
"packageName": "@custom/extension-ecg-tools"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing version field for @custom/extension-ecg-tools

Every other entry in this file includes a "version" field. The plugin resolution at build time may silently fail or pick an unexpected version without it. The three other custom extensions added in this PR (extension-smart-paint, extension-flatfoot) do not appear here at all either, which may mean they rely on a different registration path — but for any entry that is present, the version should be specified.

Suggested change
},
{
"packageName": "@custom/extension-ecg-tools"
{
"packageName": "@custom/extension-ecg-tools",
"version": "1.0.0"
}

rrVariance,
type EcgCalibration,
} from '../utils/ecgCalculations';
import SegmentLabelPanel, { type Segment } from '../../../../platform/app/src/components/SegmentLabelPanel';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Extension imports from platform/app — architectural violation

PanelEcgViewer (and similarly PanelSmartPaint at extensions/smart-paint/src/panels/PanelSmartPaint.tsx:9 and PanelFlatfoot at extensions/flatfoot/src/panels/PanelFlatfoot.tsx:21) reaches into platform/app/src/components/SegmentLabelPanel via a relative path that crosses the extension–app boundary.

In the OHIF architecture, extensions must not import from the application platform layer. Doing so:

  • Creates a hard circular-ish dependency (extensions are supposed to be consumed by the app, not depend on it).
  • Makes the extensions impossible to use in any context other than this specific app build.
  • Breaks tree-shaking and may produce duplicate module instances.

SegmentLabelPanel (or the Segment type it exports) should be moved to a shared @ohif/ui or @ohif/core package, or at minimum into a shared extensions utility package, so extensions can import it without crossing the app boundary.

@@ -1,4 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react';

const EMBED_VIEWER_EVENT = 'ohif:embedViewer';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 const declaration placed between import statements

EMBED_VIEWER_EVENT is declared between two import lines. While technically valid in ES modules (the const is in module scope and imports are hoisted), it is a non-standard pattern that confuses linters, formatters, and reviewers. Move it below all imports.

Suggested change
const EMBED_VIEWER_EVENT = 'ohif:embedViewer';

(Remove this line from between the imports and place it after the last import statement.)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 285 to +289
props: {
leftPanels: [ohif.thumbnailList],
leftPanelResizable: true,
rightPanels: [cornerstone.segmentation, cornerstone.measurements],
rightPanelClosed: true,
rightPanels: [cornerstone.labelMapSegmentationPanel, cornerstone.measurements],
rightPanelClosed: false,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Behaviour-breaking default changes to basic mode

Two silent behavioural changes are introduced here:

  1. rightPanelClosed: truefalse — the right panel will now open by default in the basic viewer for all users.
  2. enableSegmentationEdit: falsetrue — segmentation editing tools are now enabled by default.

Both are user-visible regressions for anyone using the basic viewer who does not expect segmentation or an open right panel. If these changes are intentional (e.g. they were requested as part of this feature), please add a comment explaining the rationale and update the relevant documentation/changelog entry.

Comment on lines +129 to +138
) {
const { mask, width, height } = state;
const imageData = ctx.createImageData(width, height);
const data = imageData.data;

// Parse color
const tmp = document.createElement('canvas');
tmp.width = tmp.height = 1;
const tmpCtx = tmp.getContext('2d')!;
tmpCtx.fillStyle = color;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Temporary canvas created on every render overlay call

A new <canvas> element is created and immediately thrown away on every call to renderMaskOverlay solely to parse the color string into RGB components. This is called during interactive painting (potentially dozens of times per second) and will create significant GC pressure.

Cache the parsed color values, or use a CSS-color parsing utility:

// Simple inline parser for rgba(r,g,b,a) and hex strings
function parseColor(color: string): [number, number, number] {
  const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (m) return [+m[1], +m[2], +m[3]];
  // hex
  const hex = color.replace('#', '');
  return [
    parseInt(hex.slice(0, 2), 16),
    parseInt(hex.slice(2, 4), 16),
    parseInt(hex.slice(4, 6), 16),
  ];
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant