Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
MarkerVertex
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
4 / 4
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 inAngle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 outAngle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 angle
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\SvgToPdf\Path;
6
7/**
8 * One SVG 2 §11.6.2 marker vertex: where a marker sits, plus the two
9 * tangent directions that meet there.
10 *
11 * Both directions are always populated. SVG 2 fills in the missing one
12 * at a path's ends — "for a vertex that starts a subpath the 'in'
13 * direction is the same as the 'out' direction, and for a vertex that
14 * ends a subpath the 'out' direction is the same as the 'in'" — and
15 * {@see MarkerVertices} applies that rule while walking, so there is no
16 * such thing here as a half-defined vertex. That is what lets
17 * `marker-start`, `marker-mid` and `marker-end` share ONE angle rule
18 * (the bisector) instead of each needing its own: on an open path the
19 * two directions coincide at the ends and the bisector degenerates to
20 * the single tangent, while on a CLOSED subpath the start/end point is
21 * a genuine corner and the bisector is what browsers draw there.
22 */
23final class MarkerVertex
24{
25    public function __construct(
26        public readonly float $x,
27        public readonly float $y,
28        public readonly float $inSlopeX,
29        public readonly float $inSlopeY,
30        public readonly float $outSlopeX,
31        public readonly float $outSlopeY,
32    ) {}
33
34    /** The incoming tangent direction, in degrees. */
35    public function inAngle(): float
36    {
37        return rad2deg(atan2($this->inSlopeY, $this->inSlopeX));
38    }
39
40    /** The outgoing tangent direction, in degrees. */
41    public function outAngle(): float
42    {
43        return rad2deg(atan2($this->outSlopeY, $this->outSlopeX));
44    }
45
46    /**
47     * The direction `orient="auto"` points the marker in: the bisector
48     * of the incoming and outgoing tangents (SVG 2 §11.6.3).
49     *
50     * Taken as the angle swept FROM the incoming direction TO the
51     * outgoing one, halved — not as the arithmetic mean of two
52     * `atan2` results, which is discontinuous across the ±180° cut and
53     * would flip a marker end-for-end at a shallow join.
54     *
55     * When that sweep is half a turn or more the two tangents are on
56     * opposite sides, so the marker faces along the OTHER bisector,
57     * the one that splits the side the path actually turns through.
58     * The exactly-180° case is what makes the difference visible: a
59     * path that doubles back on itself (`M50,0 v50 z`) has tangents
60     * 90° and -90°, whose mean is 0° — pointing the marker along a
61     * direction the path never travels. The swept bisector gives 180°,
62     * which is what browsers draw.
63     */
64    public function angle(): float
65    {
66        $in = $this->inAngle();
67        $delta = fmod($this->outAngle() - $in, 360.0);
68        if ($delta < 0.0) {
69            $delta += 360.0;
70        }
71        $bisector = $in + $delta / 2.0;
72        return $delta >= 180.0 ? $bisector + 180.0 : $bisector;
73    }
74}