home · contact · privacy
Add sizes x, y, z to design attributes, make fillable by ldraw calculations. master
authorPlom Heller <plom@plomlompom.com>
Tue, 11 Aug 2026 15:37:15 +0000 (17:37 +0200)
committerPlom Heller <plom@plomlompom.com>
Tue, 11 Aug 2026 15:37:15 +0000 (17:37 +0200)
src/bricksplom/constants.py
src/bricksplom/ldraw.py
src/bricksplom/misc.py

index 434708f991446fc0beccb167398d794a077a2504..a8cf1923add63a0e54c0816ed2819780dbb196d8 100644 (file)
@@ -1,2 +1,3 @@
 'Independent constants.'
 CHAR_NEWLINE = '\n'
+NUMBER_UNSET = -1
index 1a6fcfc92d365f2251dcc092f96912e40d76e913..4fa56bc67b9d16c650b941c59306e2031960b5ee 100644 (file)
@@ -5,7 +5,7 @@ from dataclasses import dataclass
 from pathlib import Path
 from typing import Any, Callable, NamedTuple, Optional, Self
 # ourselves
-from bricksplom.constants import CHAR_NEWLINE
+from bricksplom.constants import CHAR_NEWLINE, NUMBER_UNSET
 
 LDRAW_STUDS = {
     'stud.dat',
@@ -31,18 +31,19 @@ class LdrawPathFailure(Exception):
     'For when no proper .dat file found to filename.'
 
 
-class _XYZ(NamedTuple):
-    x: float
-    y: float
-    z: float
+class XYZ(NamedTuple):
+    'Record of 3D-coordinate/vector.'
+    x: float = NUMBER_UNSET
+    y: float = NUMBER_UNSET
+    z: float = NUMBER_UNSET
 
 
 def _vectors_at(
         toks: tuple[str, ...],
         indices: tuple[int, ...]
-        ) -> tuple[_XYZ, ...]:
+        ) -> tuple[XYZ, ...]:
     as_floats = tuple(float(tok) for tok in toks)
-    return tuple(_XYZ(*as_floats[idx * _N_DIMS:(idx + 1) * _N_DIMS])
+    return tuple(XYZ(*as_floats[idx * _N_DIMS:(idx + 1) * _N_DIMS])
                  for idx in indices)
 
 
@@ -61,15 +62,15 @@ def _instructions_for(
 
 @dataclass
 class _Transformation:
-    move: _XYZ
-    rotscal_x: _XYZ
-    rotscal_y: _XYZ
-    rotscal_z: _XYZ
+    move: XYZ
+    rotscal_x: XYZ
+    rotscal_y: XYZ
+    rotscal_z: XYZ
 
     def apply_to(
             self,
-            vector: _XYZ
-            ) -> _XYZ:
+            vector: XYZ
+            ) -> XYZ:
         'Rotate, scale, translate vector.'
         xyz = (
             sum(rotscal[jdx] * vector[jdx] for jdx in range(_N_DIMS))
@@ -77,7 +78,7 @@ class _Transformation:
             for idx, rotscal in enumerate((self.rotscal_x,
                                            self.rotscal_y,
                                            self.rotscal_z)))
-        return _XYZ(*xyz)
+        return XYZ(*xyz)
 
     @classmethod
     def from_toks(
@@ -169,12 +170,12 @@ class LdrawDb:
     def sizes(
             self,
             filename: str
-            ) -> _XYZ:
+            ) -> XYZ:
         'For object at filename, calculate LDU sizes over all three axes.'
 
         @dataclass
         class _MovingPoint:
-            start: _XYZ
+            start: XYZ
             transformations: list[_Transformation]
 
         def walk_collect(
@@ -225,4 +226,4 @@ class LdrawDb:
         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)
+        return XYZ(**sizes)
index 3b1aa5cc81be1d9242adcb3fa2be572d7ae69d5d..221b56c42b86789bd6885f8b8d0b1ad5c38cfce0 100644 (file)
@@ -6,8 +6,8 @@ from dataclasses import dataclass, field as dc_field
 from pathlib import Path
 from typing import Any, Callable, Optional, Self
 # ourselves
-from bricksplom.constants import CHAR_NEWLINE
-from bricksplom.ldraw import LdrawDb, LdrawPathFailure, LdrawRefsTree
+from bricksplom.constants import CHAR_NEWLINE, NUMBER_UNSET
+from bricksplom.ldraw import LdrawDb, LdrawPathFailure, LdrawRefsTree, XYZ
 
 CHAR_SEP_TOKEN = ' '
 CHAR_COMMENT = '#'
@@ -34,17 +34,22 @@ TOK_SORT_ID = 'id'
 
 LDRAW_MODE_FILL_N_STUDS = 'fill_n_studs'
 LDRAW_MODE_FILL_PATHS = 'fill_paths'
+LDRAW_MODE_FILL_SIZES = 'fill_sizes'
 LDRAW_MODE_VERIFY_N_STUDS = 'verify_n_studs'
 LDRAW_MODE_VERIFY_PATHS = 'verify_paths'
 LDRAW_MODES = {LDRAW_MODE_FILL_N_STUDS,
                LDRAW_MODE_FILL_PATHS,
+               LDRAW_MODE_FILL_SIZES,
                LDRAW_MODE_VERIFY_N_STUDS,
                LDRAW_MODE_VERIFY_PATHS}
 
 ATTR_DESCS: dict[str, str] = {
     'n_studs': 'number of studs',
     'description': 'guesstimate what this be',
-    'ldraw': 'path of associated LDRAW file'
+    'ldraw': 'path of associated LDRAW file',
+    'x': 'size over x-axis',
+    'y': 'size over y-axis',
+    'z': 'size over z-axis',
 }
 
 BrickListing = tuple[int, str, str]  # count, ID, comment
@@ -265,9 +270,12 @@ class BrickColor(Textfiled, Lookupable):
 @dataclass
 class BrickDesignData:
     'Lookupables for BrickDesign besides .id.'
-    n_studs: int = -1
+    n_studs: int = NUMBER_UNSET
     description: str = '?'
     ldraw: str = ''
+    x: float = NUMBER_UNSET
+    y: float = NUMBER_UNSET
+    z: float = NUMBER_UNSET
     _secured: set = dc_field(default_factory=set)
 
     @classmethod
@@ -313,6 +321,10 @@ class BrickDesignData:
             if attributables[a_key] is int:
                 assert a_val_str.isdigit(), a_val_str
                 setattr(attrs, a_key, int(a_val_str))
+            elif attributables[a_key] is float:
+                assert [c for c in a_val_str if c.isdigit() or c == '.']
+                assert len([c for c in a_val_str if c == '.']) in {0, 1}
+                setattr(attrs, a_key, float(a_val_str))
             else:
                 setattr(attrs, a_key, a_val_str)
         return attrs
@@ -352,6 +364,13 @@ class BrickDesign(Textfiled, WithDb, Lookupable):
         'Own .id_ plus (sorted) .alternate_ids.'
         return tuple([self.id_] + sorted(list(self.alternate_ids)))
 
+    def at_default(
+            self,
+            attr_name: str
+            ) -> bool:
+        'Compare current attribute value to BrickDesignData default.'
+        return getattr(self, attr_name) == getattr(BrickDesignData, attr_name)
+
     @classmethod
     def formatters(
             cls
@@ -413,7 +432,7 @@ class BrickDesign(Textfiled, WithDb, Lookupable):
         return super().matchers() | {
             f'{attr_name}{CHAR_ATTR_EQ}': attr_matcher(attr_name)
             for attr_name, attr_type in BrickDesignData.sortables().items()
-            if attr_type is int}
+            if attr_type in {int, float}}
 
     @classmethod
     def sorters(
@@ -472,10 +491,8 @@ class BrickDesign(Textfiled, WithDb, Lookupable):
             return f'{raw}{CHAR_DESIGN_ALT}{self.alternate_to.id_}'
         attrs = []
         for attr_key in [k for k in BrickDesignData.sortables()
-                         if k != 'description']:
-            attr_val = getattr(self, attr_key)
-            if attr_val != getattr(BrickDesignData, attr_key):
-                attrs += [f'{attr_key}{CHAR_ATTR_EQ}{attr_val}']
+                         if k != 'description' and not self.at_default(k)]:
+            attrs += [f'{attr_key}{CHAR_ATTR_EQ}{getattr(self, attr_key)}']
         attrs += [f'{SEP_DESIGN_DESC}{self.description}']
         return f'{raw}{CHAR_SEP_TOKEN.join(attrs)}'
 
@@ -792,12 +809,18 @@ class BricksDb:
                     assert design.direct_attrs is not None
                     design.direct_attrs.ldraw = filename
         self._check_consistencies()
-        if LDRAW_MODE_FILL_N_STUDS in self._ldraw_modes:
-            for design in [d for d in self._designs_ldrawed()
-                           if d.n_studs != 1]:
+        for design in self._designs_ldrawed():
+            if LDRAW_MODE_FILL_N_STUDS in self._ldraw_modes\
+                    and design.at_default('n_studs'):
                 assert design.direct_attrs is not None
                 design.direct_attrs.n_studs\
                     = self.ldraw.count_studs(design.ldraw)
+            if LDRAW_MODE_FILL_SIZES in self._ldraw_modes:
+                xyz_calc = XYZ()
+                for k in [k for k in ('x', 'y', 'z') if design.at_default(k)]:
+                    if set(xyz_calc) == {NUMBER_UNSET}:
+                        xyz_calc = self.ldraw.sizes(design.ldraw)
+                    setattr(design.direct_attrs, k, getattr(xyz_calc, k))
 
     @property
     def boxes(