@zombie-mermaid/svg-renderer 4.2.0 → 4.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +42 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +687 -652
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/edge-jog-straightening-1388.test.ts +57 -0
- package/src/layout-engine/from-elk.ts +2 -0
- package/src/layout-engine/jog-straightening.ts +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zombie-mermaid/svg-renderer",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "SVG rendering primitives and the ELK-backed layout engine behind zombie-mermaid's SVG output.",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"elkjs": "^0.11.0",
|
|
39
39
|
"entities": "^7.0.1",
|
|
40
|
-
"@zombie-mermaid/core": "4.2.
|
|
41
|
-
"@zombie-mermaid/mermaid-parser": "4.2.
|
|
40
|
+
"@zombie-mermaid/core": "4.2.2",
|
|
41
|
+
"@zombie-mermaid/mermaid-parser": "4.2.2"
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1388: parallel edges between differently sized, centered nodes kinked by a
|
|
3
|
+
* fraction of a pixel near the target and tilted the arrowhead.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
|
|
7
|
+
import { layoutGraphSync } from '@zombie-mermaid/svg-renderer'
|
|
8
|
+
import { straightenTinyJogs } from '../layout-engine/jog-straightening.ts'
|
|
9
|
+
|
|
10
|
+
describe('straightenTinyJogs', () => {
|
|
11
|
+
it('collapses a sub-pixel vertical jog into one straight run', () => {
|
|
12
|
+
const out = straightenTinyJogs([
|
|
13
|
+
{ x: 62.48, y: 76.9 },
|
|
14
|
+
{ x: 62.48, y: 112.9 },
|
|
15
|
+
{ x: 63.11, y: 112.9 },
|
|
16
|
+
{ x: 63.11, y: 124.9 },
|
|
17
|
+
])
|
|
18
|
+
expect(out).toHaveLength(2)
|
|
19
|
+
expect(out[0]!.x).toBeCloseTo(out[1]!.x, 6)
|
|
20
|
+
expect(out[0]!.y).toBe(76.9)
|
|
21
|
+
expect(out[1]!.y).toBe(124.9)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('collapses a sub-pixel horizontal jog', () => {
|
|
25
|
+
const out = straightenTinyJogs([
|
|
26
|
+
{ x: 0, y: 10 },
|
|
27
|
+
{ x: 20, y: 10 },
|
|
28
|
+
{ x: 20, y: 10.6 },
|
|
29
|
+
{ x: 40, y: 10.6 },
|
|
30
|
+
])
|
|
31
|
+
expect(out).toHaveLength(2)
|
|
32
|
+
expect(out[0]!.y).toBeCloseTo(out[1]!.y, 6)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keeps a real bend', () => {
|
|
36
|
+
const pts = [
|
|
37
|
+
{ x: 0, y: 0 },
|
|
38
|
+
{ x: 0, y: 20 },
|
|
39
|
+
{ x: 30, y: 20 },
|
|
40
|
+
{ x: 30, y: 40 },
|
|
41
|
+
]
|
|
42
|
+
expect(straightenTinyJogs(pts)).toEqual(pts)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('parallel edges between different-width nodes (#1388)', () => {
|
|
47
|
+
it('routes both edges as straight vertical lines', () => {
|
|
48
|
+
const g = layoutGraphSync(
|
|
49
|
+
parseMermaid('graph TD\n Top <--> Bot\n Top <-.- Bot'),
|
|
50
|
+
)
|
|
51
|
+
expect(g.edges).toHaveLength(2)
|
|
52
|
+
for (const e of g.edges) {
|
|
53
|
+
const xs = new Set(e.points.map((p) => p.x.toFixed(2)))
|
|
54
|
+
expect(xs.size, JSON.stringify(e.points)).toBe(1)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -22,6 +22,7 @@ import { clipEdgeToShape } from '../shape-clipping.ts'
|
|
|
22
22
|
import { DEFAULTS } from './constants.ts'
|
|
23
23
|
import { alignLayerNodes } from './layer-alignment.ts'
|
|
24
24
|
import { bundleEdgePaths } from './edge-bundling.ts'
|
|
25
|
+
import { straightenTinyJogs } from './jog-straightening.ts'
|
|
25
26
|
import { edgesReversedForLayout } from './to-elk.ts'
|
|
26
27
|
import {
|
|
27
28
|
extractEdgePoints,
|
|
@@ -222,6 +223,7 @@ export function elkToPositioned(
|
|
|
222
223
|
// ELK treats all nodes as rectangles, so we need to clip edge endpoints
|
|
223
224
|
// to the actual shape boundaries (e.g., diamond vertices).
|
|
224
225
|
for (const edge of edges) {
|
|
226
|
+
edge.points = straightenTinyJogs(edge.points)
|
|
225
227
|
const sourceNode = nodeMap.get(edge.source)
|
|
226
228
|
const targetNode = nodeMap.get(edge.target)
|
|
227
229
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Straighten sub-pixel jogs in orthogonal edge routes.
|
|
3
|
+
*
|
|
4
|
+
* ELK spreads parallel edges across each node's side relative to that node's
|
|
5
|
+
* own width, so two edges between differently sized, centered nodes leave and
|
|
6
|
+
* arrive at slightly different offsets. The route then runs straight, steps
|
|
7
|
+
* sideways by a fraction of a pixel, and runs straight again — a visible kink
|
|
8
|
+
* that also tilts the arrowhead off the edge's axis (#1388).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Point } from '@zombie-mermaid/core'
|
|
12
|
+
|
|
13
|
+
/** Largest sideways step, in px, treated as an artifact rather than a bend. */
|
|
14
|
+
const MAX_JOG = 1.5
|
|
15
|
+
|
|
16
|
+
/** Slack for deciding two coordinates are "the same" run. */
|
|
17
|
+
const EPS = 0.01
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Collapse `run, tiny step, run` into one straight run along the shared axis.
|
|
21
|
+
* Pattern (vertical form): A-B vertical, B-C horizontal with |dx| <= MAX_JOG,
|
|
22
|
+
* C-D vertical. Returns a new array; the input is not mutated.
|
|
23
|
+
*/
|
|
24
|
+
export function straightenTinyJogs(points: Point[]): Point[] {
|
|
25
|
+
const out = points.map((p) => ({ ...p }))
|
|
26
|
+
let i = 0
|
|
27
|
+
while (i + 3 < out.length) {
|
|
28
|
+
const [a, b, c, d] = [out[i]!, out[i + 1]!, out[i + 2]!, out[i + 3]!]
|
|
29
|
+
const vertical =
|
|
30
|
+
Math.abs(a.x - b.x) < EPS &&
|
|
31
|
+
Math.abs(c.x - d.x) < EPS &&
|
|
32
|
+
Math.abs(b.y - c.y) < EPS &&
|
|
33
|
+
Math.abs(b.x - c.x) <= MAX_JOG &&
|
|
34
|
+
Math.abs(b.x - c.x) >= EPS
|
|
35
|
+
const horizontal =
|
|
36
|
+
Math.abs(a.y - b.y) < EPS &&
|
|
37
|
+
Math.abs(c.y - d.y) < EPS &&
|
|
38
|
+
Math.abs(b.x - c.x) < EPS &&
|
|
39
|
+
Math.abs(b.y - c.y) <= MAX_JOG &&
|
|
40
|
+
Math.abs(b.y - c.y) >= EPS
|
|
41
|
+
if (vertical) {
|
|
42
|
+
const x = (a.x + d.x) / 2
|
|
43
|
+
out.splice(i, 4, { x, y: a.y }, { x, y: d.y })
|
|
44
|
+
} else if (horizontal) {
|
|
45
|
+
const y = (a.y + d.y) / 2
|
|
46
|
+
out.splice(i, 4, { x: a.x, y }, { x: d.x, y })
|
|
47
|
+
} else {
|
|
48
|
+
i++
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|