Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e10a85b
init commit
ChaoStraxcorp Aug 17, 2025
8b6f045
wip
ChaoStraxcorp Aug 18, 2025
b25fd75
The main panel is ready
ChaoStraxcorp Aug 23, 2025
a6d5384
Make the coordinate populates in the text box
ChaoStraxcorp Aug 23, 2025
3d022b4
Add an annotation
ChaoStraxcorp Aug 23, 2025
1255c7f
The annotation work in axial
ChaoStraxcorp Aug 23, 2025
ca85260
Make annotation unique
ChaoStraxcorp Aug 23, 2025
7db7d46
The annotations appear in all three views
ChaoStraxcorp Aug 23, 2025
17e5dbf
Remove the red bounding box around the annotation
ChaoStraxcorp Aug 23, 2025
8f40c63
The annotation is located on the correct spot in sagittal and coronal…
ChaoStraxcorp Aug 24, 2025
dd0a491
Make the right click coordinate to be the centre of the circle.
ChaoStraxcorp Aug 24, 2025
a5af686
Fix the annotation in the correct slice in sagittal and coronal views
ChaoStraxcorp Aug 24, 2025
63b7d7d
Only keep the used tools
ChaoStraxcorp Aug 24, 2025
3080ea1
Add a new viewer layout
ChaoStraxcorp Aug 24, 2025
280eb82
Remove the open health image foundation logo
ChaoStraxcorp Aug 24, 2025
c526fb4
Add logos
ChaoStraxcorp Aug 24, 2025
a4e68c4
Make the logo a component
ChaoStraxcorp Aug 24, 2025
1e21497
Add a dummy results
ChaoStraxcorp Aug 24, 2025
08ee399
All tools works
ChaoStraxcorp Aug 24, 2025
19e3afd
The laylout now looks like what we need
ChaoStraxcorp Aug 24, 2025
63a2da0
Everything is working
ChaoStraxcorp Aug 24, 2025
73a331a
Make the annotaiton work on sagittal and coronal view
ChaoStraxcorp Aug 25, 2025
d5fa544
Refactor the talas annotation
ChaoStraxcorp Sep 1, 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
66 changes: 66 additions & 0 deletions extensions/autometrics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Autometrics Extension

This extension adds an "Autometrics" panel to the OHIF viewer with a group of buttons for automated measurement tools.

## Features

The Autometrics panel includes the following button groups:

### Angular Measurements
- **M1M2** - Automated M1M2 measurements
- **TMT-DOR** - Automated TMT-DOR measurements
- **TMT-LAT** - Automated TMT-LAT measurements
- **CP** - Automated CP measurements
- **HA** - Automated HA measurements

### Foot Ankle Offset
- **TALAS** - Automated TALAS measurements

## Installation

The extension is automatically included in the OHIF viewer configuration and will appear in the right panel of the Basic Viewer mode.

## Usage

1. Open the Basic Viewer mode
2. The Autometrics panel will appear in the right sidebar
3. Click on any of the measurement buttons to trigger automated measurements
4. Currently, the buttons log to the console - you can extend the functionality by modifying the `handleButtonClick` function in `AutometricsPanel.tsx`

## Customization

To add custom functionality to the buttons, modify the `handleButtonClick` function in `src/Panels/AutometricsPanel.tsx`:

```typescript
const handleButtonClick = (buttonName) => {
switch (buttonName) {
case 'M1M2':
// Add M1M2 measurement logic
break;
case 'TMT-DOR':
// Add TMT-DOR measurement logic
break;
case 'TMT-LAT':
// Add TMT-LAT measurement logic
break;
case 'CP':
// Add CP measurement logic
break;
case 'HA':
// Add HA measurement logic
break;
case 'TALAS':
// Add TALAS measurement logic
break;
}
};
```

## Development

The extension is built using React and follows the OHIF extension pattern. The main components are:

- `AutometricsPanel.tsx` - The main panel component
- `getPanelModule.tsx` - Panel module registration
- `index.ts` - Extension entry point
- `Icons/FootIcon.tsx` - Custom foot skeleton icon for the panel
16 changes: 16 additions & 0 deletions extensions/autometrics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@ohif/extension-autometrics",
"version": "3.0.0",
"description": "Autometrics panel extension for OHIF",
"main": "src/index.ts",
"scripts": {
"build": "echo 'No build step required'",
"test": "echo 'No tests specified'"
},
"dependencies": {
"@ohif/ui-next": "^3.0.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
}
67 changes: 67 additions & 0 deletions extensions/autometrics/src/Components/CoordinateGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { Button } from '@ohif/ui-next';

interface CoordinateGroupProps {
title: string;
groupKey: string;
coordinates: { x: string; y: string; z: string };
isSelected: boolean;
onSelect: (groupKey: string) => void;
}

const CoordinateGroup: React.FC<CoordinateGroupProps> = ({
title,
groupKey,
coordinates,
isSelected,
onSelect,
}) => {
return (
<div className="space-y-2">
<div className="border-b border-gray-600 pb-1 text-sm font-medium text-gray-300">{title}</div>
<div className="space-y-2 pl-2">
<div className="flex items-center space-x-2">
<label className="w-4 text-xs text-gray-400">X:</label>
<input
type="text"
readOnly
className="flex-1 rounded border border-gray-600 bg-gray-700 px-2 py-1 text-sm text-gray-300"
placeholder="0.00"
value={coordinates.x}
/>
</div>
<div className="flex items-center space-x-2">
<label className="w-4 text-xs text-gray-400">Y:</label>
<input
type="text"
readOnly
className="flex-1 rounded border border-gray-600 bg-gray-700 px-2 py-1 text-sm text-gray-300"
placeholder="0.00"
value={coordinates.y}
/>
</div>
<div className="flex items-center space-x-2">
<label className="w-4 text-xs text-gray-400">Z:</label>
<input
type="text"
readOnly
className="flex-1 rounded border border-gray-600 bg-gray-700 px-2 py-1 text-sm text-gray-300"
placeholder="0.00"
value={coordinates.z}
/>
</div>
<Button
variant={isSelected ? 'default' : 'secondary'}
size="sm"
onClick={() => onSelect(groupKey)}
className="w-full text-xs"
>
Select
</Button>
</div>
</div>
);
};

export default CoordinateGroup;

29 changes: 29 additions & 0 deletions extensions/autometrics/src/Components/LogoSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';

const LogoSection: React.FC = () => {
return (
<div className="mt-6 flex items-center justify-center border-t border-gray-600 pt-4">
<div className="flex w-full flex-col items-center space-y-3">
{/* CurveBeam Logo */}
<div className="w-full">
<img
src="/Curvebeam-Logo.png"
alt="CurveBeam Logo"
className="h-auto w-full object-contain"
/>
</div>

{/* Autometrics Logo */}
<div className="w-full">
<img
src="/Autometrics.png"
alt="Autometrics"
className="h-auto w-full object-contain"
/>
</div>
</div>
</div>
);
};

export default LogoSection;
38 changes: 38 additions & 0 deletions extensions/autometrics/src/Components/PopupModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

interface PopupModalProps {
isVisible: boolean;
onClose: () => void;
imageSrc: string;
imageAlt: string;
}

const PopupModal: React.FC<PopupModalProps> = ({ isVisible, onClose, imageSrc, imageAlt }) => {
if (!isVisible) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="relative max-h-[90vh] max-w-[90vw] overflow-auto rounded-lg bg-white p-4 shadow-xl">
{/* Close Button */}
<button
onClick={onClose}
className="absolute right-2 top-2 z-10 rounded-full bg-gray-800 p-2 text-white hover:bg-gray-600"
>
</button>

{/* Image */}
<img
src={imageSrc}
alt={imageAlt}
className="h-auto w-full object-contain"
onLoad={() => console.log('Image loaded successfully')}
onError={e => console.error('Image failed to load:', e)}
/>
</div>
</div>
);
};

export default PopupModal;

116 changes: 116 additions & 0 deletions extensions/autometrics/src/Components/SidePanelWithServices.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useEffect, useState, useCallback } from 'react';
import { SidePanel } from '@ohif/ui-next';
import { Types } from '@ohif/core';

export type SidePanelWithServicesProps = {
servicesManager: AppTypes.ServicesManager;
side: 'left' | 'right';
className?: string;
activeTabIndex: number;
tabs?: any;
expandedWidth?: number;
onClose: () => void;
onOpen: () => void;
isExpanded: boolean;
collapsedWidth?: number;
expandedInsideBorderSize?: number;
collapsedInsideBorderSize?: number;
collapsedOutsideBorderSize?: number;
};

const SidePanelWithServices = ({
servicesManager,
side,
activeTabIndex: activeTabIndexProp,
isExpanded,
tabs: tabsProp,
onOpen,
onClose,
...props
}: SidePanelWithServicesProps) => {
const panelService = servicesManager?.services?.panelService;

// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
const [sidePanelExpanded, setSidePanelExpanded] = useState(isExpanded);
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
const [closedManually, setClosedManually] = useState(false);
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));

const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => {
setActiveTabIndex(activeTabIndex);
}, []);

const handleOpen = useCallback(() => {
setSidePanelExpanded(true);
onOpen?.();
}, [onOpen]);

const handleClose = useCallback(() => {
setSidePanelExpanded(false);
setClosedManually(true);
onClose?.();
}, [onClose]);

useEffect(() => {
setSidePanelExpanded(isExpanded);
}, [isExpanded]);

/** update the active tab index from outside */
useEffect(() => {
setActiveTabIndex(activeTabIndexProp ?? 0);
}, [activeTabIndexProp]);

useEffect(() => {
const { unsubscribe } = panelService.subscribe(
panelService.EVENTS.PANELS_CHANGED,
panelChangedEvent => {
if (panelChangedEvent.position !== side) {
return;
}

setTabs(panelService.getPanels(side));
}
);

return () => {
unsubscribe();
};
}, [panelService, side]);

useEffect(() => {
const activatePanelSubscription = panelService.subscribe(
panelService.EVENTS.ACTIVATE_PANEL,
(activatePanelEvent: Types.ActivatePanelEvent) => {
if (sidePanelExpanded || activatePanelEvent.forceActive) {
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
if (tabIndex !== -1) {
if (!closedManually) {
setSidePanelExpanded(true);
}
setActiveTabIndex(tabIndex);
}
}
}
);

return () => {
activatePanelSubscription.unsubscribe();
};
}, [tabs, sidePanelExpanded, panelService, closedManually]);

return (
<SidePanel
{...props}
side={side}
tabs={tabs}
activeTabIndex={activeTabIndex}
isExpanded={sidePanelExpanded}
onOpen={handleOpen}
onClose={handleClose}
onActiveTabIndexChange={handleActiveTabIndexChange}
/>
);
};

export default SidePanelWithServices;
25 changes: 25 additions & 0 deletions extensions/autometrics/src/Components/TalasHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { Button } from '@ohif/ui-next';

interface TalasHeaderProps {
onBack: () => void;
}

const TalasHeader: React.FC<TalasHeaderProps> = ({ onBack }) => {
return (
<div className="mb-4 flex items-center space-x-3">
<Button
variant="outline"
size="sm"
onClick={onBack}
className="text-sm"
>
Back
</Button>
<div className="text-lg font-semibold text-white">TALAS</div>
</div>
);
};

export default TalasHeader;

Loading
Loading