Skip to content

Commit

Permalink
fix(ArrowAnnotate): use svg marker to draw the arrow (#1732)
Browse files Browse the repository at this point in the history
Co-authored-by: sedghi <[email protected]>
  • Loading branch information
daker and sedghi authored Jan 22, 2025
1 parent 2665775 commit 42e8038
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 37 deletions.
2 changes: 2 additions & 0 deletions common/reviews/api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2121,6 +2121,8 @@ function drawPolyline(svgDrawingHelper: SVGDrawingHelper, annotationUID: string,
lineWidth?: number;
lineDash?: string;
closePath?: boolean;
markerStartId?: string;
markerEndId?: string;
}): void;

// @public (undocumented)
Expand Down
4 changes: 3 additions & 1 deletion packages/tools/examples/stackAnnotationTools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ async function run() {
toolGroup.addTool(BidirectionalTool.toolName);
toolGroup.addTool(AngleTool.toolName);
toolGroup.addTool(CobbAngleTool.toolName);
toolGroup.addTool(ArrowAnnotateTool.toolName);
toolGroup.addTool(ArrowAnnotateTool.toolName, {
arrowHeadStyle: 'standard',
});
toolGroup.addTool(PlanarFreehandROITool.toolName);
toolGroup.addTool(EraserTool.toolName);
toolGroup.addTool(KeyImageTool.toolName);
Expand Down
118 changes: 101 additions & 17 deletions packages/tools/src/drawingSvg/drawArrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import type { Types } from '@cornerstonejs/core';
import type { SVGDrawingHelper } from '../types';
import drawLine from './drawLine';

const svgns = 'http://www.w3.org/2000/svg';

/**
* Draws an arrow annotation using SVG elements. The arrow can be drawn in two ways:
* 1. Using a marker element (via markerEndId) - better for consistent arrowheads.
* 2. Using two additional lines for the arrowhead - the older "legacy" method.
*/
export default function drawArrow(
svgDrawingHelper: SVGDrawingHelper,
annotationUID: string,
Expand All @@ -10,28 +17,95 @@ export default function drawArrow(
end: Types.Point2,
options = {}
): void {
// if length is NaN return
if (isNaN(start[0]) || isNaN(start[1]) || isNaN(end[0]) || isNaN(end[1])) {
return;
}

const { color, width, lineWidth, lineDash } = Object.assign(
{
color: 'rgb(0, 255, 0)',
width: '2',
lineWidth: undefined,
lineDash: undefined,
},
options
);
const {
viaMarker = false,
color = 'rgb(0, 255, 0)',
markerSize = 10,
} = options as {
viaMarker?: boolean;
color?: string;
markerSize?: number;
markerEndId?: string;
};

// The line itself
drawLine(svgDrawingHelper, annotationUID, arrowUID, start, end, {
color,
width,
lineWidth,
lineDash,
});
// If NOT using the marker-based approach, fall back to your two-line "legacy" approach:
if (!viaMarker) {
legacyDrawArrow(
svgDrawingHelper,
annotationUID,
arrowUID,
start,
end,
options as {
color?: string;
width?: number;
lineWidth?: number;
lineDash?: string;
}
);
return;
}
const layerId = svgDrawingHelper.svgLayerElement.id;
const markerBaseId = `arrow-${annotationUID}`;
const markerFullId = `${markerBaseId}-${layerId}`;

const defs = svgDrawingHelper.svgLayerElement.querySelector('defs');
let arrowMarker = defs.querySelector(`#${markerFullId}`);

if (!arrowMarker) {
// Marker doesn't exist for this annotationUID, so create it
arrowMarker = document.createElementNS(svgns, 'marker');
arrowMarker.setAttribute('id', markerFullId);

// Basic marker attributes
arrowMarker.setAttribute('viewBox', '0 0 10 10');
arrowMarker.setAttribute('refX', '8');
arrowMarker.setAttribute('refY', '5');
arrowMarker.setAttribute('markerWidth', `${markerSize}`);
arrowMarker.setAttribute('markerHeight', `${markerSize}`);
arrowMarker.setAttribute('orient', 'auto');

// Create the <path> for the arrowhead shape
const arrowPath = document.createElementNS(svgns, 'path');
arrowPath.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
arrowPath.setAttribute('fill', color);

arrowMarker.appendChild(arrowPath);
defs.appendChild(arrowMarker);
} else {
// Marker already exists for this annotationUID; update color & size
arrowMarker.setAttribute('markerWidth', `${markerSize}`);
arrowMarker.setAttribute('markerHeight', `${markerSize}`);

const arrowPath = arrowMarker.querySelector('path');
if (arrowPath) {
arrowPath.setAttribute('fill', color);
}
}

(options as { markerEndId?: string }).markerEndId = markerFullId;

drawLine(svgDrawingHelper, annotationUID, arrowUID, start, end, options);
}

function legacyDrawArrow(
svgDrawingHelper: SVGDrawingHelper,
annotationUID: string,
arrowUID: string,
start: Types.Point2,
end: Types.Point2,
options = {} as {
color?: string;
width?: number;
lineWidth?: number;
lineDash?: string;
}
): void {
const { color = 'rgb(0, 255, 0)', width = 2, lineWidth, lineDash } = options;

// Drawing the head arrow with two lines
// Variables to be used when creating the arrow
Expand All @@ -54,6 +128,14 @@ export default function drawArrow(
end: end,
};

// the main line
drawLine(svgDrawingHelper, annotationUID, arrowUID, start, end, {
color,
width,
lineWidth,
lineDash,
});

drawLine(
svgDrawingHelper,
annotationUID,
Expand All @@ -64,6 +146,7 @@ export default function drawArrow(
color,
width,
lineWidth,
lineDash,
}
);

Expand All @@ -77,6 +160,7 @@ export default function drawArrow(
color,
width,
lineWidth,
lineDash,
}
);
}
3 changes: 3 additions & 0 deletions packages/tools/src/drawingSvg/drawHeight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default function drawHeight(
color,
width,
lineWidth,
lineDash,
}
);

Expand All @@ -71,6 +72,7 @@ export default function drawHeight(
color,
width,
lineWidth,
lineDash,
}
);

Expand All @@ -85,6 +87,7 @@ export default function drawHeight(
color,
width,
lineWidth,
lineDash,
}
);
}
34 changes: 21 additions & 13 deletions packages/tools/src/drawingSvg/drawLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,32 @@ export default function drawLine(
return;
}

const { color, width, lineWidth, lineDash, shadow } = Object.assign(
{
color: 'rgb(0, 255, 0)',
width: '2',
lineWidth: undefined,
lineDash: undefined,
shadow: undefined,
},
options
);
const {
color = 'rgb(0, 255, 0)',
width = 10,
lineWidth,
lineDash,
markerStartId = null,
markerEndId = null,
shadow = false,
} = options as {
color?: string;
width?: string;
lineWidth?: string;
lineDash?: string;
markerStartId?: string;
markerEndId?: string;
shadow?: boolean;
};

// for supporting both lineWidth and width options
const strokeWidth = lineWidth || width;

const svgns = 'http://www.w3.org/2000/svg';
const svgNodeHash = _getHash(annotationUID, 'line', lineUID);
const existingLine = svgDrawingHelper.getSvgNode(svgNodeHash);
const dropShadowStyle = shadow
? `filter:url(#shadow-${svgDrawingHelper.svgLayerElement.id});`
: '';
const layerId = svgDrawingHelper.svgLayerElement.id;
const dropShadowStyle = shadow ? `filter:url(#shadow-${layerId});` : '';

const attributes = {
x1: `${start[0]}`,
Expand All @@ -49,6 +55,8 @@ export default function drawLine(
style: dropShadowStyle,
'stroke-width': strokeWidth,
'stroke-dasharray': lineDash,
'marker-start': markerStartId ? `url(#${markerStartId})` : '',
'marker-end': markerEndId ? `url(#${markerEndId})` : '',
};

if (existingLine) {
Expand Down
6 changes: 6 additions & 0 deletions packages/tools/src/drawingSvg/drawPolyline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default function drawPolyline(
lineWidth?: number;
lineDash?: string;
closePath?: boolean;
markerStartId?: string;
markerEndId?: string;
}
): void {
if (points.length < 2) {
Expand All @@ -37,6 +39,8 @@ export default function drawPolyline(
lineWidth,
lineDash,
closePath = false,
markerStartId = null,
markerEndId = null,
} = options;

// for supporting both lineWidth and width options
Expand Down Expand Up @@ -65,6 +69,8 @@ export default function drawPolyline(
'fill-opacity': fillOpacity,
'stroke-width': strokeWidth,
'stroke-dasharray': lineDash,
'marker-start': markerStartId ? `url(#${markerStartId})` : '',
'marker-end': markerEndId ? `url(#${markerEndId})` : '',
};

if (existingPolyLine) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class ToolStyle {
textBoxLinkLineWidth: '1',
textBoxLinkLineDash: '2,3',
textBoxShadow: true,
markerSize: '10',
};

this._initializeConfig(defaultConfig);
Expand Down
17 changes: 13 additions & 4 deletions packages/tools/src/tools/annotation/ArrowAnnotateTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ class ArrowAnnotateTool extends AnnotationTool {
changeTextCallback,
preventHandleOutsideImage: false,
arrowFirst: true,
// there are two styles for the arrow head, legacy and standard,
// where legacy uses two separate lines and standard uses a single line
// with a marker at the end.
arrowHeadStyle: 'legacy',
},
}
) {
Expand Down Expand Up @@ -723,10 +727,11 @@ class ArrowAnnotateTool extends AnnotationTool {

styleSpecifier.annotationUID = annotationUID;

const { color, lineWidth, lineDash } = this.getAnnotationStyle({
annotation,
styleSpecifier,
});
const { color, lineWidth, lineDash, markerSize } =
this.getAnnotationStyle({
annotation,
styleSpecifier,
});

const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));

Expand Down Expand Up @@ -778,6 +783,8 @@ class ArrowAnnotateTool extends AnnotationTool {
color,
width: lineWidth,
lineDash: lineDash,
viaMarker: this.configuration.arrowHeadStyle !== 'legacy',
markerSize,
}
);
} else {
Expand All @@ -791,6 +798,8 @@ class ArrowAnnotateTool extends AnnotationTool {
color,
width: lineWidth,
lineDash: lineDash,
viaMarker: this.configuration.arrowHeadStyle !== 'legacy',
markerSize,
}
);
}
Expand Down
4 changes: 3 additions & 1 deletion packages/tools/src/tools/base/AnnotationTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,10 @@ abstract class AnnotationTool extends AnnotationDisplayTool {
const visibility = isAnnotationVisible(annotationUID);
const locked = isAnnotationLocked(annotationUID);

const lineWidth = getStyle('lineWidth') as number;
const lineWidth = getStyle('lineWidth') as string;
const lineDash = getStyle('lineDash') as string;
const color = getStyle('color') as string;
const markerSize = getStyle('markerSize') as string;
const shadow = getStyle('shadow') as boolean;
const textboxStyle = this.getLinkedTextBoxStyle(styleSpecifier, annotation);

Expand All @@ -435,6 +436,7 @@ abstract class AnnotationTool extends AnnotationDisplayTool {
fillOpacity: 0,
shadow,
textbox: textboxStyle,
markerSize,
} as AnnotationStyle;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/tools/src/types/AnnotationStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ type Properties =
| 'fillOpacity'
| 'textbox'
| 'shadow'
| 'visibility';
| 'visibility'
| 'markerSize';

export type AnnotationStyle = {
[key in `${Properties}${States}${Modes}`]?:
Expand Down

0 comments on commit 42e8038

Please sign in to comment.