+
+ def sizes(
+ self,
+ filename: str
+ ) -> _XYZ:
+ 'For object at filename, calculate LDU sizes over all three axes.'
+
+ @dataclass
+ class _Transformation:
+ move: _XYZ
+ rotscal_x: _XYZ
+ rotscal_y: _XYZ
+ rotscal_z: _XYZ
+
+ def apply_to(
+ self,
+ vector: _XYZ
+ ) -> _XYZ:
+ 'Rotate, scale, translate vector.'
+ xyz = (
+ sum(rotscal[jdx] * vector[jdx] for jdx in range(_N_DIMS))
+ + self.move[idx]
+ for idx, rotscal in enumerate((self.rotscal_x,
+ self.rotscal_y,
+ self.rotscal_z)))
+ return _XYZ(*xyz)
+
+ @dataclass
+ class _MovingPoint:
+ start: _XYZ
+ transformations: list[_Transformation]
+
+ def vectors_at(
+ toks: tuple[str, ...],
+ indices: tuple[int, ...]
+ ) -> tuple[_XYZ, ...]:
+ as_floats = tuple(float(tok) for tok in toks)
+ return tuple(_XYZ(*as_floats[idx * _N_DIMS:(idx + 1) * _N_DIMS])
+ for idx in indices)
+
+ def walk_collect(
+ walk: Callable[[], list[_MovingPoint]],
+ _: str,
+ toks: list[str]
+ ) -> list[_MovingPoint]:
+ vectors = vectors_at(tuple(toks[2:]), (0, 1, 2, 3))
+ moving_points = walk()
+ for moving_point in moving_points:
+ moving_point.transformations += [_Transformation(*vectors)]
+ return moving_points
+
+ def walk_result(
+ path: Path,
+ collected: list[_MovingPoint]
+ ) -> list[_MovingPoint]:
+ collectables = {'2': (0, 1),
+ '3': (0, 1, 2),
+ '4': (0, 1, 2, 3),
+ '5': (1, 3)}
+ for line in path.read_text(encoding='utf8').split(CHAR_NEWLINE):
+ toks = line.lstrip().split(maxsplit=1)
+ if toks and toks[0] in collectables:
+ collected += [
+ _MovingPoint(xyz, [])
+ for xyz in vectors_at(tuple(toks[1].split()[1:]),
+ collectables[toks[0]])
+ ]
+ return collected
+
+ def outer_bounds(
+ moving_points: list[_MovingPoint]
+ ) -> dict[str, tuple[float, float]]:
+ extremes: dict[str, list[Optional[float]]]\
+ = {axis: [None, None] for axis in ('x', 'y', 'z')}
+ for moving_point in moving_points:
+ position = moving_point.start
+ for transformation in moving_point.transformations:
+ position = transformation.apply_to(position)
+ for axis, min_max in extremes.items():
+ val = getattr(position, axis)
+ for idx, cmp in enumerate((lambda a, b: a > b,
+ lambda a, b: a < b)):
+ if min_max[idx] is None or cmp(min_max[idx], val):
+ min_max[idx] = val
+ return {axis: (min_max[0] or 0.0, min_max[1] or 0.0)
+ for axis, min_max in extremes.items()}
+
+ moving_points = self._walk_tree(filename, walk_collect, walk_result)
+ sizes = {axis: min_max[1] - min_max[0]
+ for axis, min_max in outer_bounds(moving_points).items()}
+ return _XYZ(**sizes)