PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 All 164 releases
woocommerce-pos / vendor_prefixed / chillerlan / php-qrcode / src / Detector / FinderPatternFinder.php

FinderPatternFinder.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.20, at vendor_prefixed/chillerlan/php-qrcode/src/Detector/FinderPatternFinder.php

645 lines 27.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class FinderPatternFinder
5 *
6 * @created 17.01.2021
7 * @author ZXing Authors
8 * @author Smiley <[email protected]>
9 * @copyright 2021 Smiley
10 * @license Apache-2.0
11 *
12 * @phan-file-suppress PhanTypePossiblyInvalidDimOffset
13 */
14 namespace WCPOS\Vendor\chillerlan\QRCode\Detector;
15
16 use WCPOS\Vendor\chillerlan\QRCode\Decoder\BitMatrix;
17 use function abs, count, intdiv, usort;
18 use const PHP_FLOAT_MAX;
19 /**
20 * This class attempts to find finder patterns in a QR Code. Finder patterns are the square
21 * markers at three corners of a QR Code.
22 *
23 * This class is thread-safe but not reentrant. Each thread must allocate its own object.
24 *
25 * @author Sean Owen
26 */
27 final class FinderPatternFinder
28 {
29 private const MIN_SKIP = 2;
30 private const MAX_MODULES = 177;
31 // 1 pixel/module times 3 modules/center
32 private const CENTER_QUORUM = 2;
33 // support up to version 10 for mobile clients
34 private BitMatrix $matrix;
35 /** @var \chillerlan\QRCode\Detector\FinderPattern[] */
36 private array $possibleCenters;
37 private bool $hasSkipped = \false;
38 /**
39 * Creates a finder that will search the image for three finder patterns.
40 *
41 * @param BitMatrix $matrix image to search
42 */
43 public function __construct(BitMatrix $matrix)
44 {
45 $this->matrix = $matrix;
46 $this->possibleCenters = [];
47 }
48 /**
49 * @return \chillerlan\QRCode\Detector\FinderPattern[]
50 */
51 public function find() : array
52 {
53 $dimension = $this->matrix->getSize();
54 // We are looking for black/white/black/white/black modules in
55 // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far
56 // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the
57 // image, and then account for the center being 3 modules in size. This gives the smallest
58 // number of pixels the center could be, so skip this often.
59 $iSkip = intdiv(3 * $dimension, 4 * self::MAX_MODULES);
60 if ($iSkip < self::MIN_SKIP) {
61 $iSkip = self::MIN_SKIP;
62 }
63 $done = \false;
64 for ($i = $iSkip - 1; $i < $dimension && !$done; $i += $iSkip) {
65 // Get a row of black/white values
66 $stateCount = $this->getCrossCheckStateCount();
67 $currentState = 0;
68 for ($j = 0; $j < $dimension; $j++) {
69 // Black pixel
70 if ($this->matrix->check($j, $i)) {
71 // Counting white pixels
72 if (($currentState & 1) === 1) {
73 $currentState++;
74 }
75 $stateCount[$currentState]++;
76 } else {
77 // Counting black pixels
78 if (($currentState & 1) === 0) {
79 // A winner?
80 if ($currentState === 4) {
81 // Yes
82 if ($this->foundPatternCross($stateCount)) {
83 $confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
84 if ($confirmed) {
85 // Start examining every other line. Checking each line turned out to be too
86 // expensive and didn't improve performance.
87 $iSkip = 3;
88 if ($this->hasSkipped) {
89 $done = $this->haveMultiplyConfirmedCenters();
90 } else {
91 $rowSkip = $this->findRowSkip();
92 if ($rowSkip > $stateCount[2]) {
93 // Skip rows between row of lower confirmed center
94 // and top of presumed third confirmed center
95 // but back up a bit to get a full chance of detecting
96 // it, entire width of center of finder pattern
97 // Skip by rowSkip, but back off by $stateCount[2] (size of last center
98 // of pattern we saw) to be conservative, and also back off by iSkip which
99 // is about to be re-added
100 $i += $rowSkip - $stateCount[2] - $iSkip;
101 $j = $dimension - 1;
102 }
103 }
104 } else {
105 $stateCount = $this->doShiftCounts2($stateCount);
106 $currentState = 3;
107 continue;
108 }
109 // Clear state to start looking again
110 $currentState = 0;
111 $stateCount = $this->getCrossCheckStateCount();
112 } else {
113 $stateCount = $this->doShiftCounts2($stateCount);
114 $currentState = 3;
115 }
116 } else {
117 $stateCount[++$currentState]++;
118 }
119 } else {
120 $stateCount[$currentState]++;
121 }
122 }
123 }
124 if ($this->foundPatternCross($stateCount)) {
125 $confirmed = $this->handlePossibleCenter($stateCount, $i, $dimension);
126 if ($confirmed) {
127 $iSkip = $stateCount[0];
128 if ($this->hasSkipped) {
129 // Found a third one
130 $done = $this->haveMultiplyConfirmedCenters();
131 }
132 }
133 }
134 }
135 return $this->orderBestPatterns($this->selectBestPatterns());
136 }
137 /**
138 * @return int[]
139 */
140 private function getCrossCheckStateCount() : array
141 {
142 return [0, 0, 0, 0, 0];
143 }
144 /**
145 * @param int[] $stateCount
146 *
147 * @return int[]
148 */
149 private function doShiftCounts2(array $stateCount) : array
150 {
151 $stateCount[0] = $stateCount[2];
152 $stateCount[1] = $stateCount[3];
153 $stateCount[2] = $stateCount[4];
154 $stateCount[3] = 1;
155 $stateCount[4] = 0;
156 return $stateCount;
157 }
158 /**
159 * Given a count of black/white/black/white/black pixels just seen and an end position,
160 * figures the location of the center of this run.
161 *
162 * @param int[] $stateCount
163 */
164 private function centerFromEnd(array $stateCount, int $end) : float
165 {
166 return (float) ($end - $stateCount[4] - $stateCount[3] - $stateCount[2] / 2);
167 }
168 /**
169 * @param int[] $stateCount
170 */
171 private function foundPatternCross(array $stateCount) : bool
172 {
173 // Allow less than 50% variance from 1-1-3-1-1 proportions
174 return $this->foundPatternVariance($stateCount, 2.0);
175 }
176 /**
177 * @param int[] $stateCount
178 */
179 private function foundPatternDiagonal(array $stateCount) : bool
180 {
181 // Allow less than 75% variance from 1-1-3-1-1 proportions
182 return $this->foundPatternVariance($stateCount, 1.333);
183 }
184 /**
185 * @param int[] $stateCount count of black/white/black/white/black pixels just read
186 *
187 * @return bool true if the proportions of the counts is close enough to the 1/1/3/1/1 ratios
188 * used by finder patterns to be considered a match
189 */
190 private function foundPatternVariance(array $stateCount, float $variance) : bool
191 {
192 $totalModuleSize = 0;
193 for ($i = 0; $i < 5; $i++) {
194 $count = $stateCount[$i];
195 if ($count === 0) {
196 return \false;
197 }
198 $totalModuleSize += $count;
199 }
200 if ($totalModuleSize < 7) {
201 return \false;
202 }
203 $moduleSize = $totalModuleSize / 7.0;
204 $maxVariance = $moduleSize / $variance;
205 return abs($moduleSize - $stateCount[0]) < $maxVariance && abs($moduleSize - $stateCount[1]) < $maxVariance && abs(3.0 * $moduleSize - $stateCount[2]) < 3 * $maxVariance && abs($moduleSize - $stateCount[3]) < $maxVariance && abs($moduleSize - $stateCount[4]) < $maxVariance;
206 }
207 /**
208 * After a vertical and horizontal scan finds a potential finder pattern, this method
209 * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible
210 * finder pattern to see if the same proportion is detected.
211 *
212 * @param int $centerI row where a finder pattern was detected
213 * @param int $centerJ center of the section that appears to cross a finder pattern
214 *
215 * @return bool true if proportions are withing expected limits
216 */
217 private function crossCheckDiagonal(int $centerI, int $centerJ) : bool
218 {
219 $stateCount = $this->getCrossCheckStateCount();
220 // Start counting up, left from center finding black center mass
221 $i = 0;
222 while ($centerI >= $i && $centerJ >= $i && $this->matrix->check($centerJ - $i, $centerI - $i)) {
223 $stateCount[2]++;
224 $i++;
225 }
226 if ($stateCount[2] === 0) {
227 return \false;
228 }
229 // Continue up, left finding white space
230 while ($centerI >= $i && $centerJ >= $i && !$this->matrix->check($centerJ - $i, $centerI - $i)) {
231 $stateCount[1]++;
232 $i++;
233 }
234 if ($stateCount[1] === 0) {
235 return \false;
236 }
237 // Continue up, left finding black border
238 while ($centerI >= $i && $centerJ >= $i && $this->matrix->check($centerJ - $i, $centerI - $i)) {
239 $stateCount[0]++;
240 $i++;
241 }
242 if ($stateCount[0] === 0) {
243 return \false;
244 }
245 $dimension = $this->matrix->getSize();
246 // Now also count down, right from center
247 $i = 1;
248 // phpcs:ignore
249 while ($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->matrix->check($centerJ + $i, $centerI + $i)) {
250 $stateCount[2]++;
251 $i++;
252 }
253 // phpcs:ignore
254 while ($centerI + $i < $dimension && $centerJ + $i < $dimension && !$this->matrix->check($centerJ + $i, $centerI + $i)) {
255 $stateCount[3]++;
256 $i++;
257 }
258 if ($stateCount[3] === 0) {
259 return \false;
260 }
261 // phpcs:ignore
262 while ($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->matrix->check($centerJ + $i, $centerI + $i)) {
263 $stateCount[4]++;
264 $i++;
265 }
266 if ($stateCount[4] === 0) {
267 return \false;
268 }
269 return $this->foundPatternDiagonal($stateCount);
270 }
271 /**
272 * After a horizontal scan finds a potential finder pattern, this method
273 * "cross-checks" by scanning down vertically through the center of the possible
274 * finder pattern to see if the same proportion is detected.
275 *
276 * @param int $startI row where a finder pattern was detected
277 * @param int $centerJ center of the section that appears to cross a finder pattern
278 * @param int $maxCount maximum reasonable number of modules that should be
279 * observed in any reading state, based on the results of the horizontal scan
280 * @param int $originalStateCountTotal
281 *
282 * @return float|null vertical center of finder pattern, or null if not found
283 * @noinspection DuplicatedCode
284 */
285 private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal) : ?float
286 {
287 $maxI = $this->matrix->getSize();
288 $stateCount = $this->getCrossCheckStateCount();
289 // Start counting up from center
290 $i = $startI;
291 while ($i >= 0 && $this->matrix->check($centerJ, $i)) {
292 $stateCount[2]++;
293 $i--;
294 }
295 if ($i < 0) {
296 return null;
297 }
298 while ($i >= 0 && !$this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount) {
299 $stateCount[1]++;
300 $i--;
301 }
302 // If already too many modules in this state or ran off the edge:
303 if ($i < 0 || $stateCount[1] > $maxCount) {
304 return null;
305 }
306 while ($i >= 0 && $this->matrix->check($centerJ, $i) && $stateCount[0] <= $maxCount) {
307 $stateCount[0]++;
308 $i--;
309 }
310 if ($stateCount[0] > $maxCount) {
311 return null;
312 }
313 // Now also count down from center
314 $i = $startI + 1;
315 while ($i < $maxI && $this->matrix->check($centerJ, $i)) {
316 $stateCount[2]++;
317 $i++;
318 }
319 if ($i === $maxI) {
320 return null;
321 }
322 while ($i < $maxI && !$this->matrix->check($centerJ, $i) && $stateCount[3] < $maxCount) {
323 $stateCount[3]++;
324 $i++;
325 }
326 if ($i === $maxI || $stateCount[3] >= $maxCount) {
327 return null;
328 }
329 while ($i < $maxI && $this->matrix->check($centerJ, $i) && $stateCount[4] < $maxCount) {
330 $stateCount[4]++;
331 $i++;
332 }
333 if ($stateCount[4] >= $maxCount) {
334 return null;
335 }
336 // If we found a finder-pattern-like section, but its size is more than 40% different from
337 // the original, assume it's a false positive
338 $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
339 if (5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal) {
340 return null;
341 }
342 if (!$this->foundPatternCross($stateCount)) {
343 return null;
344 }
345 return $this->centerFromEnd($stateCount, $i);
346 }
347 /**
348 * Like #crossCheckVertical(int, int, int, int), and in fact is basically identical,
349 * except it reads horizontally instead of vertically. This is used to cross-cross
350 * check a vertical cross-check and locate the real center of the alignment pattern.
351 * @noinspection DuplicatedCode
352 */
353 private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal) : ?float
354 {
355 $maxJ = $this->matrix->getSize();
356 $stateCount = $this->getCrossCheckStateCount();
357 $j = $startJ;
358 while ($j >= 0 && $this->matrix->check($j, $centerI)) {
359 $stateCount[2]++;
360 $j--;
361 }
362 if ($j < 0) {
363 return null;
364 }
365 while ($j >= 0 && !$this->matrix->check($j, $centerI) && $stateCount[1] <= $maxCount) {
366 $stateCount[1]++;
367 $j--;
368 }
369 if ($j < 0 || $stateCount[1] > $maxCount) {
370 return null;
371 }
372 while ($j >= 0 && $this->matrix->check($j, $centerI) && $stateCount[0] <= $maxCount) {
373 $stateCount[0]++;
374 $j--;
375 }
376 if ($stateCount[0] > $maxCount) {
377 return null;
378 }
379 $j = $startJ + 1;
380 while ($j < $maxJ && $this->matrix->check($j, $centerI)) {
381 $stateCount[2]++;
382 $j++;
383 }
384 if ($j === $maxJ) {
385 return null;
386 }
387 while ($j < $maxJ && !$this->matrix->check($j, $centerI) && $stateCount[3] < $maxCount) {
388 $stateCount[3]++;
389 $j++;
390 }
391 if ($j === $maxJ || $stateCount[3] >= $maxCount) {
392 return null;
393 }
394 while ($j < $maxJ && $this->matrix->check($j, $centerI) && $stateCount[4] < $maxCount) {
395 $stateCount[4]++;
396 $j++;
397 }
398 if ($stateCount[4] >= $maxCount) {
399 return null;
400 }
401 // If we found a finder-pattern-like section, but its size is significantly different from
402 // the original, assume it's a false positive
403 $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
404 if (5 * abs($stateCountTotal - $originalStateCountTotal) >= $originalStateCountTotal) {
405 return null;
406 }
407 if (!$this->foundPatternCross($stateCount)) {
408 return null;
409 }
410 return $this->centerFromEnd($stateCount, $j);
411 }
412 /**
413 * This is called when a horizontal scan finds a possible alignment pattern. It will
414 * cross-check with a vertical scan, and if successful, will, ah, cross-cross-check
415 * with another horizontal scan. This is needed primarily to locate the real horizontal
416 * center of the pattern in cases of extreme skew.
417 * And then we cross-cross-cross check with another diagonal scan.
418 *
419 * If that succeeds the finder pattern location is added to a list that tracks
420 * the number of times each location has been nearly-matched as a finder pattern.
421 * Each additional find is more evidence that the location is in fact a finder
422 * pattern center
423 *
424 * @param int[] $stateCount reading state module counts from horizontal scan
425 * @param int $i row where finder pattern may be found
426 * @param int $j end of possible finder pattern in row
427 *
428 * @return bool if a finder pattern candidate was found this time
429 */
430 private function handlePossibleCenter(array $stateCount, int $i, int $j) : bool
431 {
432 $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
433 $centerJ = $this->centerFromEnd($stateCount, $j);
434 $centerI = $this->crossCheckVertical($i, (int) $centerJ, $stateCount[2], $stateCountTotal);
435 if ($centerI !== null) {
436 // Re-cross check
437 $centerJ = $this->crossCheckHorizontal((int) $centerJ, (int) $centerI, $stateCount[2], $stateCountTotal);
438 if ($centerJ !== null && $this->crossCheckDiagonal((int) $centerI, (int) $centerJ)) {
439 $estimatedModuleSize = $stateCountTotal / 7.0;
440 $found = \false;
441 // cautious (was in for fool in which $this->possibleCenters is updated)
442 $count = count($this->possibleCenters);
443 for ($index = 0; $index < $count; $index++) {
444 $center = $this->possibleCenters[$index];
445 // Look for about the same center and module size:
446 if ($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)) {
447 $this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
448 $found = \true;
449 break;
450 }
451 }
452 if (!$found) {
453 $point = new FinderPattern($centerJ, $centerI, $estimatedModuleSize);
454 $this->possibleCenters[] = $point;
455 }
456 return \true;
457 }
458 }
459 return \false;
460 }
461 /**
462 * @return int number of rows we could safely skip during scanning, based on the first
463 * two finder patterns that have been located. In some cases their position will
464 * allow us to infer that the third pattern must lie below a certain point farther
465 * down in the image.
466 */
467 private function findRowSkip() : int
468 {
469 $max = count($this->possibleCenters);
470 if ($max <= 1) {
471 return 0;
472 }
473 $firstConfirmedCenter = null;
474 foreach ($this->possibleCenters as $center) {
475 if ($center->getCount() >= self::CENTER_QUORUM) {
476 if ($firstConfirmedCenter === null) {
477 $firstConfirmedCenter = $center;
478 } else {
479 // We have two confirmed centers
480 // How far down can we skip before resuming looking for the next
481 // pattern? In the worst case, only the difference between the
482 // difference in the x / y coordinates of the two centers.
483 // This is the case where you find top left last.
484 $this->hasSkipped = \true;
485 return (int) ((abs($firstConfirmedCenter->getX() - $center->getX()) - abs($firstConfirmedCenter->getY() - $center->getY())) / 2);
486 }
487 }
488 }
489 return 0;
490 }
491 /**
492 * @return bool true if we have found at least 3 finder patterns that have been detected
493 * at least #CENTER_QUORUM times each, and, the estimated module size of the
494 * candidates is "pretty similar"
495 */
496 private function haveMultiplyConfirmedCenters() : bool
497 {
498 $confirmedCount = 0;
499 $totalModuleSize = 0.0;
500 $max = count($this->possibleCenters);
501 foreach ($this->possibleCenters as $pattern) {
502 if ($pattern->getCount() >= self::CENTER_QUORUM) {
503 $confirmedCount++;
504 $totalModuleSize += $pattern->getEstimatedModuleSize();
505 }
506 }
507 if ($confirmedCount < 3) {
508 return \false;
509 }
510 // OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive"
511 // and that we need to keep looking. We detect this by asking if the estimated module sizes
512 // vary too much. We arbitrarily say that when the total deviation from average exceeds
513 // 5% of the total module size estimates, it's too much.
514 $average = $totalModuleSize / (float) $max;
515 $totalDeviation = 0.0;
516 foreach ($this->possibleCenters as $pattern) {
517 $totalDeviation += abs($pattern->getEstimatedModuleSize() - $average);
518 }
519 return $totalDeviation <= 0.05 * $totalModuleSize;
520 }
521 /**
522 * @return \chillerlan\QRCode\Detector\FinderPattern[] the 3 best FinderPatterns from our list of candidates. The "best" are
523 * those that have been detected at least #CENTER_QUORUM times, and whose module
524 * size differs from the average among those patterns the least
525 * @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if 3 such finder patterns do not exist
526 */
527 private function selectBestPatterns() : array
528 {
529 $startSize = count($this->possibleCenters);
530 if ($startSize < 3) {
531 throw new QRCodeDetectorException('could not find enough finder patterns');
532 }
533 usort($this->possibleCenters, fn(FinderPattern $a, FinderPattern $b) => $a->getEstimatedModuleSize() <=> $b->getEstimatedModuleSize());
534 $distortion = PHP_FLOAT_MAX;
535 $bestPatterns = [];
536 for ($i = 0; $i < $startSize - 2; $i++) {
537 $fpi = $this->possibleCenters[$i];
538 $minModuleSize = $fpi->getEstimatedModuleSize();
539 for ($j = $i + 1; $j < $startSize - 1; $j++) {
540 $fpj = $this->possibleCenters[$j];
541 $squares0 = $fpi->getSquaredDistance($fpj);
542 for ($k = $j + 1; $k < $startSize; $k++) {
543 $fpk = $this->possibleCenters[$k];
544 $maxModuleSize = $fpk->getEstimatedModuleSize();
545 // module size is not similar
546 if ($maxModuleSize > $minModuleSize * 1.4) {
547 continue;
548 }
549 $a = $squares0;
550 $b = $fpj->getSquaredDistance($fpk);
551 $c = $fpi->getSquaredDistance($fpk);
552 // sorts ascending - inlined
553 if ($a < $b) {
554 if ($b > $c) {
555 if ($a < $c) {
556 $temp = $b;
557 $b = $c;
558 $c = $temp;
559 } else {
560 $temp = $a;
561 $a = $c;
562 $c = $b;
563 $b = $temp;
564 }
565 }
566 } else {
567 if ($b < $c) {
568 if ($a < $c) {
569 $temp = $a;
570 $a = $b;
571 $b = $temp;
572 } else {
573 $temp = $a;
574 $a = $b;
575 $b = $c;
576 $c = $temp;
577 }
578 } else {
579 $temp = $a;
580 $a = $c;
581 $c = $temp;
582 }
583 }
584 // a^2 + b^2 = c^2 (Pythagorean theorem), and a = b (isosceles triangle).
585 // Since any right triangle satisfies the formula c^2 - b^2 - a^2 = 0,
586 // we need to check both two equal sides separately.
587 // The value of |c^2 - 2 * b^2| + |c^2 - 2 * a^2| increases as dissimilarity
588 // from isosceles right triangle.
589 $d = abs($c - 2 * $b) + abs($c - 2 * $a);
590 if ($d < $distortion) {
591 $distortion = $d;
592 $bestPatterns = [$fpi, $fpj, $fpk];
593 }
594 }
595 }
596 }
597 if ($distortion === PHP_FLOAT_MAX) {
598 throw new QRCodeDetectorException('finder patterns may be too distorted');
599 }
600 return $bestPatterns;
601 }
602 /**
603 * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC
604 * and BC is less than AC, and the angle between BC and BA is less than 180 degrees.
605 *
606 * @param \chillerlan\QRCode\Detector\FinderPattern[] $patterns array of three FinderPattern to order
607 *
608 * @return \chillerlan\QRCode\Detector\FinderPattern[]
609 */
610 private function orderBestPatterns(array $patterns) : array
611 {
612 // Find distances between pattern centers
613 $zeroOneDistance = $patterns[0]->getDistance($patterns[1]);
614 $oneTwoDistance = $patterns[1]->getDistance($patterns[2]);
615 $zeroTwoDistance = $patterns[0]->getDistance($patterns[2]);
616 // Assume one closest to other two is B; A and C will just be guesses at first
617 if ($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance) {
618 [$pointB, $pointA, $pointC] = $patterns;
619 } elseif ($zeroTwoDistance >= $oneTwoDistance && $zeroTwoDistance >= $zeroOneDistance) {
620 [$pointA, $pointB, $pointC] = $patterns;
621 } else {
622 [$pointA, $pointC, $pointB] = $patterns;
623 }
624 // Use cross product to figure out whether A and C are correct or flipped.
625 // This asks whether BC x BA has a positive z component, which is the arrangement
626 // we want for A, B, C. If it's negative, then we've got it flipped around and
627 // should swap A and C.
628 if ($this->crossProductZ($pointA, $pointB, $pointC) < 0.0) {
629 $temp = $pointA;
630 $pointA = $pointC;
631 $pointC = $temp;
632 }
633 return [$pointA, $pointB, $pointC];
634 }
635 /**
636 * Returns the z component of the cross product between vectors BC and BA.
637 */
638 private function crossProductZ(FinderPattern $pointA, FinderPattern $pointB, FinderPattern $pointC) : float
639 {
640 $bX = $pointB->getX();
641 $bY = $pointB->getY();
642 return ($pointC->getX() - $bX) * ($pointA->getY() - $bY) - ($pointC->getY() - $bY) * ($pointA->getX() - $bX);
643 }
644 }
645