classes.c51_quadtree_splitter

  1import random
  2from typing import NamedTuple
  3
  4
  5Coordinates = tuple[float, float, float, float]
  6
  7
  8class KQuadtreeNode(NamedTuple):
  9    """A single quadtree node.
 10
 11    Attributes:
 12            coords: Rectangle as (x, y, width, height).
 13            depth: Depth level where this node appears.
 14            nodeIndex: Stable breadth-first index used for lineage.
 15    """
 16
 17    coords: Coordinates
 18    depth: int
 19    nodeIndex: int
 20
 21
 22class KQuadtreeSplitter:
 23    """Reusable quadtree splitter with Fibonacci-biased subdivision."""
 24
 25    Node = KQuadtreeNode
 26
 27    def __init__(
 28        self,
 29        splitBias: float = 0.65,
 30        splitDecay: float = 0.55,
 31        minSize: float = 36,
 32        seed: int | None = None,
 33    ):
 34        self.splitBias = splitBias
 35        self.splitDecay = splitDecay
 36        self.minSize = minSize
 37        self.seed = seed
 38
 39    def _fib(self, n: int) -> int:
 40        a, b = 1, 1
 41        for _ in range(n):
 42            a, b = b, a + b
 43        return a
 44
 45    def _fibRatio(self, step: int) -> float:
 46        # Consecutive Fibonacci division ratio, converging toward ~0.618.
 47        return self._fib(step + 2) / self._fib(step + 3)
 48
 49    def _split4Fib(
 50        self,
 51        coords: Coordinates,
 52        depth: int,
 53        focusPoint: tuple[float, float],
 54    ) -> list[Coordinates]:
 55        x, y, w, h = coords
 56
 57        rxBase = self._fibRatio(depth)
 58        ryBase = self._fibRatio(depth + 1)
 59
 60        fx, fy = focusPoint
 61        cx = x + w * 0.5
 62        cy = y + h * 0.5
 63
 64        # Keep Fibonacci ratio and flip direction toward one focus point.
 65        rx = 1 - rxBase if fx >= cx else rxBase
 66        ry = 1 - ryBase if fy >= cy else ryBase
 67
 68        wLeft = w * rx
 69        wRight = w - wLeft
 70        hBottom = h * ry
 71        hTop = h - hBottom
 72
 73        return [
 74            (x, y, wLeft, hBottom),
 75            (x + wLeft, y, wRight, hBottom),
 76            (x, y + hBottom, wLeft, hTop),
 77            (x + wLeft, y + hBottom, wRight, hTop),
 78        ]
 79
 80    def buildNodes(
 81        self,
 82        coords: Coordinates,
 83        maxDepth: int,
 84    ) -> list[KQuadtreeNode]:
 85        """Build quadtree nodes and return them as named tuples."""
 86        rng = random.Random(self.seed)
 87
 88        x, y, w, h = coords
 89        focusPoint = (x + w * rng.random(), y + h * rng.random())
 90
 91        level: list[tuple[Coordinates, int]] = [(coords, 0)]
 92        nodes: list[KQuadtreeNode] = []
 93
 94        for depth in range(maxDepth + 1):
 95            for nodeCoords, nodeIndex in level:
 96                nodes.append(
 97                    KQuadtreeNode(
 98                        coords=nodeCoords,
 99                        depth=depth,
100                        nodeIndex=nodeIndex,
101                    )
102                )
103
104            if depth < maxDepth:
105                nextLevel: list[tuple[Coordinates, int]] = []
106                for c, nodeIndex in level:
107                    _, _, cw, ch = c
108                    forceFirstSplit = maxDepth > 0 and depth == 0 and nodeIndex == 0
109                    splitOk = cw > self.minSize and ch > self.minSize
110                    splitProb = self.splitBias * (self.splitDecay**depth)
111                    splitGate = rng.random() < splitProb
112                    canSplit = forceFirstSplit or (splitOk and splitGate)
113
114                    if canSplit:
115                        children = self._split4Fib(
116                            c,
117                            depth=depth,
118                            focusPoint=focusPoint,
119                        )
120                        for childIndex, child in enumerate(children):
121                            nextLevel.append((child, nodeIndex * 4 + childIndex + 1))
122                    else:
123                        # Keep some nodes un-split for in-between visual density.
124                        nextLevel.append((c, nodeIndex))
125
126                level = nextLevel
127
128        return nodes
Coordinates = tuple[float, float, float, float]
class KQuadtreeNode(typing.NamedTuple):
 9class KQuadtreeNode(NamedTuple):
10    """A single quadtree node.
11
12    Attributes:
13            coords: Rectangle as (x, y, width, height).
14            depth: Depth level where this node appears.
15            nodeIndex: Stable breadth-first index used for lineage.
16    """
17
18    coords: Coordinates
19    depth: int
20    nodeIndex: int

A single quadtree node.

Attributes:
  • coords: Rectangle as (x, y, width, height).
  • depth: Depth level where this node appears.
  • nodeIndex: Stable breadth-first index used for lineage.
KQuadtreeNode( coords: tuple[float, float, float, float], depth: int, nodeIndex: int)

Create new instance of KQuadtreeNode(coords, depth, nodeIndex)

coords: tuple[float, float, float, float]

Alias for field number 0

depth: int

Alias for field number 1

nodeIndex: int

Alias for field number 2

class KQuadtreeSplitter:
 23class KQuadtreeSplitter:
 24    """Reusable quadtree splitter with Fibonacci-biased subdivision."""
 25
 26    Node = KQuadtreeNode
 27
 28    def __init__(
 29        self,
 30        splitBias: float = 0.65,
 31        splitDecay: float = 0.55,
 32        minSize: float = 36,
 33        seed: int | None = None,
 34    ):
 35        self.splitBias = splitBias
 36        self.splitDecay = splitDecay
 37        self.minSize = minSize
 38        self.seed = seed
 39
 40    def _fib(self, n: int) -> int:
 41        a, b = 1, 1
 42        for _ in range(n):
 43            a, b = b, a + b
 44        return a
 45
 46    def _fibRatio(self, step: int) -> float:
 47        # Consecutive Fibonacci division ratio, converging toward ~0.618.
 48        return self._fib(step + 2) / self._fib(step + 3)
 49
 50    def _split4Fib(
 51        self,
 52        coords: Coordinates,
 53        depth: int,
 54        focusPoint: tuple[float, float],
 55    ) -> list[Coordinates]:
 56        x, y, w, h = coords
 57
 58        rxBase = self._fibRatio(depth)
 59        ryBase = self._fibRatio(depth + 1)
 60
 61        fx, fy = focusPoint
 62        cx = x + w * 0.5
 63        cy = y + h * 0.5
 64
 65        # Keep Fibonacci ratio and flip direction toward one focus point.
 66        rx = 1 - rxBase if fx >= cx else rxBase
 67        ry = 1 - ryBase if fy >= cy else ryBase
 68
 69        wLeft = w * rx
 70        wRight = w - wLeft
 71        hBottom = h * ry
 72        hTop = h - hBottom
 73
 74        return [
 75            (x, y, wLeft, hBottom),
 76            (x + wLeft, y, wRight, hBottom),
 77            (x, y + hBottom, wLeft, hTop),
 78            (x + wLeft, y + hBottom, wRight, hTop),
 79        ]
 80
 81    def buildNodes(
 82        self,
 83        coords: Coordinates,
 84        maxDepth: int,
 85    ) -> list[KQuadtreeNode]:
 86        """Build quadtree nodes and return them as named tuples."""
 87        rng = random.Random(self.seed)
 88
 89        x, y, w, h = coords
 90        focusPoint = (x + w * rng.random(), y + h * rng.random())
 91
 92        level: list[tuple[Coordinates, int]] = [(coords, 0)]
 93        nodes: list[KQuadtreeNode] = []
 94
 95        for depth in range(maxDepth + 1):
 96            for nodeCoords, nodeIndex in level:
 97                nodes.append(
 98                    KQuadtreeNode(
 99                        coords=nodeCoords,
100                        depth=depth,
101                        nodeIndex=nodeIndex,
102                    )
103                )
104
105            if depth < maxDepth:
106                nextLevel: list[tuple[Coordinates, int]] = []
107                for c, nodeIndex in level:
108                    _, _, cw, ch = c
109                    forceFirstSplit = maxDepth > 0 and depth == 0 and nodeIndex == 0
110                    splitOk = cw > self.minSize and ch > self.minSize
111                    splitProb = self.splitBias * (self.splitDecay**depth)
112                    splitGate = rng.random() < splitProb
113                    canSplit = forceFirstSplit or (splitOk and splitGate)
114
115                    if canSplit:
116                        children = self._split4Fib(
117                            c,
118                            depth=depth,
119                            focusPoint=focusPoint,
120                        )
121                        for childIndex, child in enumerate(children):
122                            nextLevel.append((child, nodeIndex * 4 + childIndex + 1))
123                    else:
124                        # Keep some nodes un-split for in-between visual density.
125                        nextLevel.append((c, nodeIndex))
126
127                level = nextLevel
128
129        return nodes

Reusable quadtree splitter with Fibonacci-biased subdivision.

KQuadtreeSplitter( splitBias: float = 0.65, splitDecay: float = 0.55, minSize: float = 36, seed: int | None = None)
28    def __init__(
29        self,
30        splitBias: float = 0.65,
31        splitDecay: float = 0.55,
32        minSize: float = 36,
33        seed: int | None = None,
34    ):
35        self.splitBias = splitBias
36        self.splitDecay = splitDecay
37        self.minSize = minSize
38        self.seed = seed
Node = <class 'KQuadtreeNode'>
splitBias
splitDecay
minSize
seed
def buildNodes( self, coords: tuple[float, float, float, float], maxDepth: int) -> list[KQuadtreeNode]:
 81    def buildNodes(
 82        self,
 83        coords: Coordinates,
 84        maxDepth: int,
 85    ) -> list[KQuadtreeNode]:
 86        """Build quadtree nodes and return them as named tuples."""
 87        rng = random.Random(self.seed)
 88
 89        x, y, w, h = coords
 90        focusPoint = (x + w * rng.random(), y + h * rng.random())
 91
 92        level: list[tuple[Coordinates, int]] = [(coords, 0)]
 93        nodes: list[KQuadtreeNode] = []
 94
 95        for depth in range(maxDepth + 1):
 96            for nodeCoords, nodeIndex in level:
 97                nodes.append(
 98                    KQuadtreeNode(
 99                        coords=nodeCoords,
100                        depth=depth,
101                        nodeIndex=nodeIndex,
102                    )
103                )
104
105            if depth < maxDepth:
106                nextLevel: list[tuple[Coordinates, int]] = []
107                for c, nodeIndex in level:
108                    _, _, cw, ch = c
109                    forceFirstSplit = maxDepth > 0 and depth == 0 and nodeIndex == 0
110                    splitOk = cw > self.minSize and ch > self.minSize
111                    splitProb = self.splitBias * (self.splitDecay**depth)
112                    splitGate = rng.random() < splitProb
113                    canSplit = forceFirstSplit or (splitOk and splitGate)
114
115                    if canSplit:
116                        children = self._split4Fib(
117                            c,
118                            depth=depth,
119                            focusPoint=focusPoint,
120                        )
121                        for childIndex, child in enumerate(children):
122                            nextLevel.append((child, nodeIndex * 4 + childIndex + 1))
123                    else:
124                        # Keep some nodes un-split for in-between visual density.
125                        nextLevel.append((c, nodeIndex))
126
127                level = nextLevel
128
129        return nodes

Build quadtree nodes and return them as named tuples.