|
17 | 17 | * The algorithm implements the SVG 2 specification's arc-to-Bézier conversion, |
18 | 18 | * decomposing the arc into multiple segments for accurate curve approximation. |
19 | 19 | */ |
20 | | -final class SvgArcConverter |
| 20 | +final readonly class SvgArcConverter |
21 | 21 | { |
| 22 | + public function __construct( |
| 23 | + private \LibreSign\XObjectTemplate\Pdf\Svg\SvgArcMath $math = new \LibreSign\XObjectTemplate\Pdf\Svg\SvgArcMath(), |
| 24 | + ) { |
| 25 | + } |
| 26 | + |
22 | 27 | /** |
23 | 28 | * Convert SVG arc command parameters to cubic Bézier curves. |
24 | 29 | * |
@@ -70,195 +75,18 @@ public function arcToBezierCurves( |
70 | 75 | $sweep, |
71 | 76 | ); |
72 | 77 |
|
73 | | - // Step 1: Normalize radii |
74 | | - $params = $this->normalizeArcRadii($params); |
| 78 | + $params = $this->math->normalizeArcRadii($params); |
75 | 79 |
|
76 | | - // Step 2: Calculate center point |
77 | | - [$centerX, $centerY] = $this->calculateArcCenter($params); |
| 80 | + [$centerX, $centerY] = $this->math->calculateArcCenter($params); |
78 | 81 |
|
79 | | - // Step 3: Calculate angles and deltas |
80 | | - [$startAngle, $deltaAngle] = $this->calculateArcAngles($params, $centerX, $centerY); |
| 82 | + [$startAngle, $deltaAngle] = $this->math->calculateArcAngles($params); |
81 | 83 |
|
82 | | - // Step 4: Generate cubic Bézier curves |
83 | | - return $this->generateArcCurves( |
| 84 | + return $this->math->generateArcCurves( |
84 | 85 | $params, |
85 | 86 | $centerX, |
86 | 87 | $centerY, |
87 | 88 | $startAngle, |
88 | 89 | $deltaAngle, |
89 | 90 | ); |
90 | 91 | } |
91 | | - |
92 | | - /** |
93 | | - * Normalize arc radii to satisfy SVG constraints. |
94 | | - * |
95 | | - * If the given radii are too small, they are scaled up to ensure the arc |
96 | | - * can connect the start and end points. |
97 | | - */ |
98 | | - private function normalizeArcRadii(ArcParams $params): ArcParams |
99 | | - { |
100 | | - $deltaX2 = ($params->fromX - $params->toX) / 2.0; |
101 | | - $deltaY2 = ($params->fromY - $params->toY) / 2.0; |
102 | | - $primeX = $params->cosTh * $deltaX2 + $params->sinTh * $deltaY2; |
103 | | - $primeY = -$params->sinTh * $deltaX2 + $params->cosTh * $deltaY2; |
104 | | - |
105 | | - $radiusX2 = $params->radiusX * $params->radiusX; |
106 | | - $radiusY2 = $params->radiusY * $params->radiusY; |
107 | | - $primeX2 = $primeX * $primeX; |
108 | | - $primeY2 = $primeY * $primeY; |
109 | | - $scale = $primeX2 / $radiusX2 + $primeY2 / $radiusY2; |
110 | | - if ($scale <= 1.0) { |
111 | | - return $params; |
112 | | - } |
113 | | - |
114 | | - $scaleFactor = sqrt($scale); |
115 | | - |
116 | | - return $params->withRadii($params->radiusX * $scaleFactor, $params->radiusY * $scaleFactor); |
117 | | - } |
118 | | - |
119 | | - /** |
120 | | - * Calculate the center point of the arc. |
121 | | - * |
122 | | - * @return array{0:float,1:float} [$cx, $cy] |
123 | | - */ |
124 | | - private function calculateArcCenter(ArcParams $params): array |
125 | | - { |
126 | | - [$primeX, $primeY] = $this->calculatePrimeCoordinates($params); |
127 | | - |
128 | | - $radiusX2 = $params->radiusX * $params->radiusX; |
129 | | - $radiusY2 = $params->radiusY * $params->radiusY; |
130 | | - $primeX2 = $primeX * $primeX; |
131 | | - $primeY2 = $primeY * $primeY; |
132 | | - $numerator = max(0.0, $radiusX2 * $radiusY2 - $radiusX2 * $primeY2 - $radiusY2 * $primeX2); |
133 | | - $denominator = $radiusX2 * $primeY2 + $radiusY2 * $primeX2; |
134 | | - $denominatorBucket = floor($denominator * 1e10); |
135 | | - $squareRoot = $denominatorBucket > 0 ? sqrt($numerator / $denominator) : 0.0; |
136 | | - if ($params->largeArc === $params->sweep) { |
137 | | - $squareRoot = -$squareRoot; |
138 | | - } |
139 | | - |
140 | | - $centerX1 = $squareRoot * $params->radiusX * $primeY / $params->radiusY; |
141 | | - $centerY1 = -$squareRoot * $params->radiusY * $primeX / $params->radiusX; |
142 | | - |
143 | | - $midX = ($params->fromX + $params->toX) / 2.0; |
144 | | - $midY = ($params->fromY + $params->toY) / 2.0; |
145 | | - $centerX = $params->cosTh * $centerX1 - $params->sinTh * $centerY1 + $midX; |
146 | | - $centerY = $params->sinTh * $centerX1 + $params->cosTh * $centerY1 + $midY; |
147 | | - |
148 | | - return [$centerX, $centerY]; |
149 | | - } |
150 | | - |
151 | | - /** |
152 | | - * Calculate start angle and total angle delta for the arc. |
153 | | - * |
154 | | - * @return array{0:float,1:float} [$startAngle, $dAngle] |
155 | | - */ |
156 | | - private function calculateArcAngles(ArcParams $params, float $centerX, float $centerY): array |
157 | | - { |
158 | | - [$primeX, $primeY] = $this->calculatePrimeCoordinates($params); |
159 | | - |
160 | | - $vectorUX = $primeX / $params->radiusX; |
161 | | - $vectorUY = $primeY / $params->radiusY; |
162 | | - |
163 | | - $startAngle = atan2($vectorUY, $vectorUX); |
164 | | - |
165 | | - // In this formulation, V is always the opposite of U, so acos(cosDA) |
166 | | - // collapses to π for stable magnitudes and π/2 for near-zero magnitudes. |
167 | | - $normSquared = $vectorUX * $vectorUX + $vectorUY * $vectorUY; |
168 | | - $magnitudeBucket = floor($normSquared * 1e10); |
169 | | - $deltaAngle = $magnitudeBucket > 0 ? M_PI : M_PI / 2.0; |
170 | | - |
171 | | - if ($params->sweep === 0) { |
172 | | - $deltaAngle -= 2.0 * M_PI; |
173 | | - } |
174 | | - |
175 | | - return [$startAngle, $deltaAngle]; |
176 | | - } |
177 | | - |
178 | | - /** |
179 | | - * Calculate transformed midpoint delta coordinates in the rotated arc space. |
180 | | - * |
181 | | - * @return array{0:float,1:float} [$px, $py] |
182 | | - */ |
183 | | - private function calculatePrimeCoordinates(ArcParams $params): array |
184 | | - { |
185 | | - $deltaX2 = ($params->fromX - $params->toX) / 2.0; |
186 | | - $deltaY2 = ($params->fromY - $params->toY) / 2.0; |
187 | | - $primeX = $params->cosTh * $deltaX2 + $params->sinTh * $deltaY2; |
188 | | - $primeY = -$params->sinTh * $deltaX2 + $params->cosTh * $deltaY2; |
189 | | - |
190 | | - return [$primeX, $primeY]; |
191 | | - } |
192 | | - |
193 | | - /** |
194 | | - * Generate cubic Bézier curve segments for the arc. |
195 | | - * |
196 | | - * Splits the arc into multiple segments and computes the Bézier control |
197 | | - * points for each segment to approximate the circular/elliptical arc. |
198 | | - * |
199 | | - * @param ArcParams $params Arc geometry and endpoint parameters |
200 | | - * @param float $centerX Center X coordinate |
201 | | - * @param float $centerY Center Y coordinate |
202 | | - * @param float $startAngle Starting angle in radians |
203 | | - * @param float $deltaAngle Total angle delta in radians |
204 | | - * @return array<int, array<int, float>> Array of Bézier curve control points |
205 | | - */ |
206 | | - private function generateArcCurves( |
207 | | - ArcParams $params, |
208 | | - float $centerX, |
209 | | - float $centerY, |
210 | | - float $startAngle, |
211 | | - float $deltaAngle, |
212 | | - ): array { |
213 | | - $segments = max(1, intval(ceil(abs($deltaAngle) / (M_PI / 2.0)))); |
214 | | - $angleStep = $deltaAngle / $segments; |
215 | | - $tanHalfAngleStep = tan($angleStep / 2.0); |
216 | | - $alpha = abs($angleStep) > 1e-10 |
217 | | - ? sin($angleStep) * (sqrt(4.0 + 3.0 * $tanHalfAngleStep * $tanHalfAngleStep) - 1.0) / 3.0 |
218 | | - : 0.0; |
219 | | - |
220 | | - $curves = []; |
221 | | - $angle1 = $startAngle; |
222 | | - $cos1 = cos($angle1); |
223 | | - $sin1 = sin($angle1); |
224 | | - $endX1 = $centerX + $params->cosTh * $params->radiusX * $cos1 - $params->sinTh * $params->radiusY * $sin1; |
225 | | - $endY1 = $centerY + $params->sinTh * $params->radiusX * $cos1 + $params->cosTh * $params->radiusY * $sin1; |
226 | | - |
227 | | - foreach (range(0, $segments - 1) as $i) { |
228 | | - $angle2 = $angle1 + $angleStep; |
229 | | - $cos2 = cos($angle2); |
230 | | - $sin2 = sin($angle2); |
231 | | - |
232 | | - $endX2 = $centerX + $params->cosTh * $params->radiusX * $cos2 - $params->sinTh * $params->radiusY * $sin2; |
233 | | - $endY2 = $centerY + $params->sinTh * $params->radiusX * $cos2 + $params->cosTh * $params->radiusY * $sin2; |
234 | | - |
235 | | - // For the last segment, ensure the endpoint is exactly the target point |
236 | | - if ($i === $segments - 1) { |
237 | | - $endX2 = $params->toX; |
238 | | - $endY2 = $params->toY; |
239 | | - } |
240 | | - |
241 | | - $tangentXD1 = -$params->cosTh * $params->radiusX * $sin1 - $params->sinTh * $params->radiusY * $cos1; |
242 | | - $tangentYD1 = -$params->sinTh * $params->radiusX * $sin1 + $params->cosTh * $params->radiusY * $cos1; |
243 | | - $tangentXD2 = -$params->cosTh * $params->radiusX * $sin2 - $params->sinTh * $params->radiusY * $cos2; |
244 | | - $tangentYD2 = -$params->sinTh * $params->radiusX * $sin2 + $params->cosTh * $params->radiusY * $cos2; |
245 | | - |
246 | | - $curves[] = [ |
247 | | - $endX1 + $alpha * $tangentXD1, |
248 | | - $endY1 + $alpha * $tangentYD1, |
249 | | - $endX2 - $alpha * $tangentXD2, |
250 | | - $endY2 - $alpha * $tangentYD2, |
251 | | - $endX2, |
252 | | - $endY2, |
253 | | - ]; |
254 | | - |
255 | | - $angle1 = $angle2; |
256 | | - $cos1 = $cos2; |
257 | | - $sin1 = $sin2; |
258 | | - $endX1 = $endX2; |
259 | | - $endY1 = $endY2; |
260 | | - } |
261 | | - |
262 | | - return $curves; |
263 | | - } |
264 | 92 | } |
0 commit comments