Skip to content

Commit 42e8038

Browse files
dakersedghi
andauthored
fix(ArrowAnnotate): use svg marker to draw the arrow (#1732)
Co-authored-by: sedghi <[email protected]>
1 parent 2665775 commit 42e8038

File tree

10 files changed

+155
-37
lines changed

10 files changed

+155
-37
lines changed

common/reviews/api/tools.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2121,6 +2121,8 @@ function drawPolyline(svgDrawingHelper: SVGDrawingHelper, annotationUID: string,
21212121
lineWidth?: number;
21222122
lineDash?: string;
21232123
closePath?: boolean;
2124+
markerStartId?: string;
2125+
markerEndId?: string;
21242126
}): void;
21252127

21262128
// @public (undocumented)

packages/tools/examples/stackAnnotationTools/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,9 @@ async function run() {
240240
toolGroup.addTool(BidirectionalTool.toolName);
241241
toolGroup.addTool(AngleTool.toolName);
242242
toolGroup.addTool(CobbAngleTool.toolName);
243-
toolGroup.addTool(ArrowAnnotateTool.toolName);
243+
toolGroup.addTool(ArrowAnnotateTool.toolName, {
244+
arrowHeadStyle: 'standard',
245+
});
244246
toolGroup.addTool(PlanarFreehandROITool.toolName);
245247
toolGroup.addTool(EraserTool.toolName);
246248
toolGroup.addTool(KeyImageTool.toolName);

packages/tools/src/drawingSvg/drawArrow.ts

Lines changed: 101 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import type { Types } from '@cornerstonejs/core';
22
import type { SVGDrawingHelper } from '../types';
33
import drawLine from './drawLine';
44

5+
const svgns = 'http://www.w3.org/2000/svg';
6+
7+
/**
8+
* Draws an arrow annotation using SVG elements. The arrow can be drawn in two ways:
9+
* 1. Using a marker element (via markerEndId) - better for consistent arrowheads.
10+
* 2. Using two additional lines for the arrowhead - the older "legacy" method.
11+
*/
512
export default function drawArrow(
613
svgDrawingHelper: SVGDrawingHelper,
714
annotationUID: string,
@@ -10,28 +17,95 @@ export default function drawArrow(
1017
end: Types.Point2,
1118
options = {}
1219
): void {
13-
// if length is NaN return
1420
if (isNaN(start[0]) || isNaN(start[1]) || isNaN(end[0]) || isNaN(end[1])) {
1521
return;
1622
}
1723

18-
const { color, width, lineWidth, lineDash } = Object.assign(
19-
{
20-
color: 'rgb(0, 255, 0)',
21-
width: '2',
22-
lineWidth: undefined,
23-
lineDash: undefined,
24-
},
25-
options
26-
);
24+
const {
25+
viaMarker = false,
26+
color = 'rgb(0, 255, 0)',
27+
markerSize = 10,
28+
} = options as {
29+
viaMarker?: boolean;
30+
color?: string;
31+
markerSize?: number;
32+
markerEndId?: string;
33+
};
2734

28-
// The line itself
29-
drawLine(svgDrawingHelper, annotationUID, arrowUID, start, end, {
30-
color,
31-
width,
32-
lineWidth,
33-
lineDash,
34-
});
35+
// If NOT using the marker-based approach, fall back to your two-line "legacy" approach:
36+
if (!viaMarker) {
37+
legacyDrawArrow(
38+
svgDrawingHelper,
39+
annotationUID,
40+
arrowUID,
41+
start,
42+
end,
43+
options as {
44+
color?: string;
45+
width?: number;
46+
lineWidth?: number;
47+
lineDash?: string;
48+
}
49+
);
50+
return;
51+
}
52+
const layerId = svgDrawingHelper.svgLayerElement.id;
53+
const markerBaseId = `arrow-${annotationUID}`;
54+
const markerFullId = `${markerBaseId}-${layerId}`;
55+
56+
const defs = svgDrawingHelper.svgLayerElement.querySelector('defs');
57+
let arrowMarker = defs.querySelector(`#${markerFullId}`);
58+
59+
if (!arrowMarker) {
60+
// Marker doesn't exist for this annotationUID, so create it
61+
arrowMarker = document.createElementNS(svgns, 'marker');
62+
arrowMarker.setAttribute('id', markerFullId);
63+
64+
// Basic marker attributes
65+
arrowMarker.setAttribute('viewBox', '0 0 10 10');
66+
arrowMarker.setAttribute('refX', '8');
67+
arrowMarker.setAttribute('refY', '5');
68+
arrowMarker.setAttribute('markerWidth', `${markerSize}`);
69+
arrowMarker.setAttribute('markerHeight', `${markerSize}`);
70+
arrowMarker.setAttribute('orient', 'auto');
71+
72+
// Create the <path> for the arrowhead shape
73+
const arrowPath = document.createElementNS(svgns, 'path');
74+
arrowPath.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
75+
arrowPath.setAttribute('fill', color);
76+
77+
arrowMarker.appendChild(arrowPath);
78+
defs.appendChild(arrowMarker);
79+
} else {
80+
// Marker already exists for this annotationUID; update color & size
81+
arrowMarker.setAttribute('markerWidth', `${markerSize}`);
82+
arrowMarker.setAttribute('markerHeight', `${markerSize}`);
83+
84+
const arrowPath = arrowMarker.querySelector('path');
85+
if (arrowPath) {
86+
arrowPath.setAttribute('fill', color);
87+
}
88+
}
89+
90+
(options as { markerEndId?: string }).markerEndId = markerFullId;
91+
92+
drawLine(svgDrawingHelper, annotationUID, arrowUID, start, end, options);
93+
}
94+
95+
function legacyDrawArrow(
96+
svgDrawingHelper: SVGDrawingHelper,
97+
annotationUID: string,
98+
arrowUID: string,
99+
start: Types.Point2,
100+
end: Types.Point2,
101+
options = {} as {
102+
color?: string;
103+
width?: number;
104+
lineWidth?: number;
105+
lineDash?: string;
106+
}
107+
): void {
108+
const { color = 'rgb(0, 255, 0)', width = 2, lineWidth, lineDash } = options;
35109

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

131+
// the main line
132+
drawLine(svgDrawingHelper, annotationUID, arrowUID, start, end, {
133+
color,
134+
width,
135+
lineWidth,
136+
lineDash,
137+
});
138+
57139
drawLine(
58140
svgDrawingHelper,
59141
annotationUID,
@@ -64,6 +146,7 @@ export default function drawArrow(
64146
color,
65147
width,
66148
lineWidth,
149+
lineDash,
67150
}
68151
);
69152

@@ -77,6 +160,7 @@ export default function drawArrow(
77160
color,
78161
width,
79162
lineWidth,
163+
lineDash,
80164
}
81165
);
82166
}

packages/tools/src/drawingSvg/drawHeight.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export default function drawHeight(
5757
color,
5858
width,
5959
lineWidth,
60+
lineDash,
6061
}
6162
);
6263

@@ -71,6 +72,7 @@ export default function drawHeight(
7172
color,
7273
width,
7374
lineWidth,
75+
lineDash,
7476
}
7577
);
7678

@@ -85,6 +87,7 @@ export default function drawHeight(
8587
color,
8688
width,
8789
lineWidth,
90+
lineDash,
8891
}
8992
);
9093
}

packages/tools/src/drawingSvg/drawLine.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,32 @@ export default function drawLine(
1919
return;
2020
}
2121

22-
const { color, width, lineWidth, lineDash, shadow } = Object.assign(
23-
{
24-
color: 'rgb(0, 255, 0)',
25-
width: '2',
26-
lineWidth: undefined,
27-
lineDash: undefined,
28-
shadow: undefined,
29-
},
30-
options
31-
);
22+
const {
23+
color = 'rgb(0, 255, 0)',
24+
width = 10,
25+
lineWidth,
26+
lineDash,
27+
markerStartId = null,
28+
markerEndId = null,
29+
shadow = false,
30+
} = options as {
31+
color?: string;
32+
width?: string;
33+
lineWidth?: string;
34+
lineDash?: string;
35+
markerStartId?: string;
36+
markerEndId?: string;
37+
shadow?: boolean;
38+
};
3239

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

3643
const svgns = 'http://www.w3.org/2000/svg';
3744
const svgNodeHash = _getHash(annotationUID, 'line', lineUID);
3845
const existingLine = svgDrawingHelper.getSvgNode(svgNodeHash);
39-
const dropShadowStyle = shadow
40-
? `filter:url(#shadow-${svgDrawingHelper.svgLayerElement.id});`
41-
: '';
46+
const layerId = svgDrawingHelper.svgLayerElement.id;
47+
const dropShadowStyle = shadow ? `filter:url(#shadow-${layerId});` : '';
4248

4349
const attributes = {
4450
x1: `${start[0]}`,
@@ -49,6 +55,8 @@ export default function drawLine(
4955
style: dropShadowStyle,
5056
'stroke-width': strokeWidth,
5157
'stroke-dasharray': lineDash,
58+
'marker-start': markerStartId ? `url(#${markerStartId})` : '',
59+
'marker-end': markerEndId ? `url(#${markerEndId})` : '',
5260
};
5361

5462
if (existingLine) {

packages/tools/src/drawingSvg/drawPolyline.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export default function drawPolyline(
2323
lineWidth?: number;
2424
lineDash?: string;
2525
closePath?: boolean;
26+
markerStartId?: string;
27+
markerEndId?: string;
2628
}
2729
): void {
2830
if (points.length < 2) {
@@ -37,6 +39,8 @@ export default function drawPolyline(
3739
lineWidth,
3840
lineDash,
3941
closePath = false,
42+
markerStartId = null,
43+
markerEndId = null,
4044
} = options;
4145

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

7076
if (existingPolyLine) {

packages/tools/src/stateManagement/annotation/config/ToolStyle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class ToolStyle {
4646
textBoxLinkLineWidth: '1',
4747
textBoxLinkLineDash: '2,3',
4848
textBoxShadow: true,
49+
markerSize: '10',
4950
};
5051

5152
this._initializeConfig(defaultConfig);

packages/tools/src/tools/annotation/ArrowAnnotateTool.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ class ArrowAnnotateTool extends AnnotationTool {
7171
changeTextCallback,
7272
preventHandleOutsideImage: false,
7373
arrowFirst: true,
74+
// there are two styles for the arrow head, legacy and standard,
75+
// where legacy uses two separate lines and standard uses a single line
76+
// with a marker at the end.
77+
arrowHeadStyle: 'legacy',
7478
},
7579
}
7680
) {
@@ -723,10 +727,11 @@ class ArrowAnnotateTool extends AnnotationTool {
723727

724728
styleSpecifier.annotationUID = annotationUID;
725729

726-
const { color, lineWidth, lineDash } = this.getAnnotationStyle({
727-
annotation,
728-
styleSpecifier,
729-
});
730+
const { color, lineWidth, lineDash, markerSize } =
731+
this.getAnnotationStyle({
732+
annotation,
733+
styleSpecifier,
734+
});
730735

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

@@ -778,6 +783,8 @@ class ArrowAnnotateTool extends AnnotationTool {
778783
color,
779784
width: lineWidth,
780785
lineDash: lineDash,
786+
viaMarker: this.configuration.arrowHeadStyle !== 'legacy',
787+
markerSize,
781788
}
782789
);
783790
} else {
@@ -791,6 +798,8 @@ class ArrowAnnotateTool extends AnnotationTool {
791798
color,
792799
width: lineWidth,
793800
lineDash: lineDash,
801+
viaMarker: this.configuration.arrowHeadStyle !== 'legacy',
802+
markerSize,
794803
}
795804
);
796805
}

packages/tools/src/tools/base/AnnotationTool.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,9 +418,10 @@ abstract class AnnotationTool extends AnnotationDisplayTool {
418418
const visibility = isAnnotationVisible(annotationUID);
419419
const locked = isAnnotationLocked(annotationUID);
420420

421-
const lineWidth = getStyle('lineWidth') as number;
421+
const lineWidth = getStyle('lineWidth') as string;
422422
const lineDash = getStyle('lineDash') as string;
423423
const color = getStyle('color') as string;
424+
const markerSize = getStyle('markerSize') as string;
424425
const shadow = getStyle('shadow') as boolean;
425426
const textboxStyle = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
426427

@@ -435,6 +436,7 @@ abstract class AnnotationTool extends AnnotationDisplayTool {
435436
fillOpacity: 0,
436437
shadow,
437438
textbox: textboxStyle,
439+
markerSize,
438440
} as AnnotationStyle;
439441
}
440442

packages/tools/src/types/AnnotationStyle.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ type Properties =
1818
| 'fillOpacity'
1919
| 'textbox'
2020
| 'shadow'
21-
| 'visibility';
21+
| 'visibility'
22+
| 'markerSize';
2223

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

0 commit comments

Comments
 (0)