home · contact · privacy
Add designs -f ldraw_sizes to calculate outer bounds of bricks. master
authorPlom Heller <plom@plomlompom.com>
Sat, 8 Aug 2026 19:12:40 +0000 (21:12 +0200)
committerPlom Heller <plom@plomlompom.com>
Sat, 8 Aug 2026 19:12:40 +0000 (21:12 +0200)
src/bricksplom/ldraw.py
src/bricksplom/misc.py

index 8f294471cdc743b956f7266ad371a13f47632130..ae283d827d4201be2c8663c54caa4bf52e53930f 100644 (file)
@@ -1,8 +1,9 @@
 'Anything LDRAW.'
 
 # standard libs
 'Anything LDRAW.'
 
 # standard libs
+from dataclasses import dataclass
 from pathlib import Path
 from pathlib import Path
-from typing import Any, Callable
+from typing import Any, Callable, NamedTuple, Optional
 # ourselves
 from bricksplom.constants import CHAR_NEWLINE
 
 # 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
 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 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.'
 
 class LdrawDb:
     'Connection to local LDRAW archive.'
 
@@ -40,7 +48,7 @@ class LdrawDb:
 
     def path(
             self,
 
     def path(
             self,
-            filename
+            filename: str
             ) -> Path:
         'Find actual path from filename in part/, p/ etc.'
         for prefix in ('parts', 'p'):
             ) -> 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))
         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)
index e24a3c8835b80e2b7b52b5e28bfbfa4f74be6044..19a65b499917ec18b43f66296dabb1b7be5cd4e4 100644 (file)
@@ -346,6 +346,8 @@ class BrickDesign(Textfiled, WithDb, Lookupable):
 
         return super().formatters() | {
                 'ldraw_tree': ('tree of LDRAW references', ldraw_tree),
 
         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)
                 }
 
                 'to_boxes': ('to containing boxes', to_boxes)
                 }