From: Plom Heller Date: Sat, 8 Aug 2026 19:12:40 +0000 (+0200) Subject: Add designs -f ldraw_sizes to calculate outer bounds of bricks. X-Git-Url: https://plomlompom.com/repos/%22https:/validator.w3.org/static/foo.html?a=commitdiff_plain;h=04a03fc97d73f8b29347a75839e8718c3094cb07;p=bricksplom Add designs -f ldraw_sizes to calculate outer bounds of bricks. --- diff --git a/src/bricksplom/ldraw.py b/src/bricksplom/ldraw.py index 8f29447..ae283d8 100644 --- a/src/bricksplom/ldraw.py +++ b/src/bricksplom/ldraw.py @@ -1,8 +1,9 @@ 'Anything LDRAW.' # standard libs +from dataclasses import dataclass from pathlib import Path -from typing import Any, Callable +from typing import Any, Callable, NamedTuple, Optional # ourselves from bricksplom.constants import CHAR_NEWLINE @@ -23,12 +24,19 @@ LDRAW_STUDS = { LdrawRefsTree = tuple[Path, '_SubRefs'] _SubRefs = tuple[tuple[str, LdrawRefsTree], ...] _N_TOKS_PER_REFLINE = 15 +_N_DIMS = 3 class LdrawPathFailure(Exception): 'For when no proper .dat file found to filename.' +class _XYZ(NamedTuple): + x: float + y: float + z: float + + class LdrawDb: 'Connection to local LDRAW archive.' @@ -40,7 +48,7 @@ class LdrawDb: def path( self, - filename + filename: str ) -> Path: 'Find actual path from filename in part/, p/ etc.' for prefix in ('parts', 'p'): @@ -108,3 +116,94 @@ class LdrawDb: return self._walk_tree(filename, collect=count_studs, result=lambda _, to_sum: sum(to_sum)) + + 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) diff --git a/src/bricksplom/misc.py b/src/bricksplom/misc.py index e24a3c8..19a65b4 100644 --- a/src/bricksplom/misc.py +++ b/src/bricksplom/misc.py @@ -346,6 +346,8 @@ class BrickDesign(Textfiled, WithDb, Lookupable): return super().formatters() | { 'ldraw_tree': ('tree of LDRAW references', ldraw_tree), + 'ldraw_sizes': ('sized via LDRAW', + lambda x: str(x.db.ldraw.sizes(x.ldraw))), 'to_boxes': ('to containing boxes', to_boxes) }