x1, $line->y1, $line->x2, $line->y2, $line, 0, 0.0); if ($line->back !== null) { $segs[] = new Seg($line->x2, $line->y2, $line->x1, $line->y1, $line, 1, 0.0); } } $this->root = $this->build($segs); } /** @param Seg[] $segs @return int child id (node index, or -1 - subsector index) */ private function build(array $segs): int { $best = $this->choose($segs); if ($best === null) { $this->subsectors[] = new Subsector($segs[0]->frontSector, $segs); return -1 - (count($this->subsectors) - 1); } [$px, $py, $pdx, $pdy] = $best; $front = []; $back = []; foreach ($segs as $s) { $a = self::cross($px, $py, $pdx, $pdy, $s->x1, $s->y1); $b = self::cross($px, $py, $pdx, $pdy, $s->x2, $s->y2); $sa = abs($a) < self::EPS ? 0 : ($a > 0 ? 1 : -1); $sb = abs($b) < self::EPS ? 0 : ($b > 0 ? 1 : -1); if ($sa === 0 && $sb === 0) { // on the partition: same direction goes front if (($s->x2 - $s->x1) * $pdx + ($s->y2 - $s->y1) * $pdy > 0) { $front[] = $s; } else { $back[] = $s; } } elseif ($sa >= 0 && $sb >= 0) { $front[] = $s; } elseif ($sa <= 0 && $sb <= 0) { $back[] = $s; } else { // split at the crossing point $t = $a / ($a - $b); $ix = $s->x1 + ($s->x2 - $s->x1) * $t; $iy = $s->y1 + ($s->y2 - $s->y1) * $t; $len = sqrt(($ix - $s->x1) ** 2 + ($iy - $s->y1) ** 2); $first = new Seg($s->x1, $s->y1, $ix, $iy, $s->line, $s->side, $s->offset); $second = new Seg($ix, $iy, $s->x2, $s->y2, $s->line, $s->side, $s->offset + $len); if ($sa > 0) { $front[] = $first; $back[] = $second; } else { $back[] = $first; $front[] = $second; } } } $node = new Node($px, $py, $pdx, $pdy); $index = count($this->nodes); $this->nodes[] = $node; $node->bbox = [self::bbox($front), self::bbox($back)]; $node->children = [$this->build($front), $this->build($back)]; return $index; } /** The best partition (a seg's line), or null when the segs already form a convex leaf. */ private function choose(array $segs): ?array { $n = count($segs); if ($this->convex($segs)) { return null; } $step = max(1, intdiv($n, 48)); $bestCost = PHP_INT_MAX; $best = null; for ($i = 0; $i < $n; $i += $step) { $p = $segs[$i]; $pdx = $p->x2 - $p->x1; $pdy = $p->y2 - $p->y1; $front = $back = $splits = 0; foreach ($segs as $s) { $a = self::cross($p->x1, $p->y1, $pdx, $pdy, $s->x1, $s->y1); $b = self::cross($p->x1, $p->y1, $pdx, $pdy, $s->x2, $s->y2); $sa = abs($a) < self::EPS ? 0 : ($a > 0 ? 1 : -1); $sb = abs($b) < self::EPS ? 0 : ($b > 0 ? 1 : -1); if ($sa === 0 && $sb === 0) { (($s->x2 - $s->x1) * $pdx + ($s->y2 - $s->y1) * $pdy > 0) ? $front++ : $back++; } elseif ($sa >= 0 && $sb >= 0) { $front++; } elseif ($sa <= 0 && $sb <= 0) { $back++; } else { $splits++; } } if ($back === 0 && $splits === 0) { continue; // everything in front: not a partition } $cost = $splits * 6 + abs($front - $back); // prefer axis-aligned partitions a little (fewer odd splits) if (abs($pdx) > self::EPS && abs($pdy) > self::EPS) { $cost += 4; } if ($cost < $bestCost) { $bestCost = $cost; $best = [$p->x1, $p->y1, $pdx, $pdy]; } } if ($best === null) { // sampling missed every partition: try them all foreach ($segs as $p) { $pdx = $p->x2 - $p->x1; $pdy = $p->y2 - $p->y1; foreach ($segs as $s) { if (self::cross($p->x1, $p->y1, $pdx, $pdy, $s->x1, $s->y1) < -self::EPS || self::cross($p->x1, $p->y1, $pdx, $pdy, $s->x2, $s->y2) < -self::EPS) { return [$p->x1, $p->y1, $pdx, $pdy]; } } } } return $best; } /** Every seg in front of every other one's line, and one sector. */ private function convex(array $segs): bool { $sector = $segs[0]->frontSector; foreach ($segs as $p) { if ($p->frontSector !== $sector) { return false; } $pdx = $p->x2 - $p->x1; $pdy = $p->y2 - $p->y1; foreach ($segs as $s) { if (self::cross($p->x1, $p->y1, $pdx, $pdy, $s->x1, $s->y1) < -self::EPS || self::cross($p->x1, $p->y1, $pdx, $pdy, $s->x2, $s->y2) < -self::EPS) { return false; } } } return true; } /** > 0 when (x, y) is on the right (front) of the line through (px, py) along (dx, dy). */ private static function cross(float $px, float $py, float $dx, float $dy, float $x, float $y): float { $len = sqrt($dx * $dx + $dy * $dy) ?: 1.0; return (($x - $px) * $dy - ($y - $py) * $dx) / $len; } /** @param Seg[] $segs @return array{0: float, 1: float, 2: float, 3: float} top, bottom, left, right */ private static function bbox(array $segs): array { $t = $r = -INF; $b = $l = INF; foreach ($segs as $s) { $t = max($t, $s->y1, $s->y2); $b = min($b, $s->y1, $s->y2); $l = min($l, $s->x1, $s->x2); $r = max($r, $s->x1, $s->x2); } return [$t, $b, $l, $r]; } }