From: Plom Heller Date: Tue, 11 Aug 2026 15:37:15 +0000 (+0200) Subject: Add sizes x, y, z to design attributes, make fillable by ldraw calculations. X-Git-Url: https://plomlompom.com/repos/%7B%7Bdb.prefix%7D%7D/booking/static/foo.html?a=commitdiff_plain;p=bricksplom Add sizes x, y, z to design attributes, make fillable by ldraw calculations. --- diff --git a/src/bricksplom/constants.py b/src/bricksplom/constants.py index 434708f..a8cf192 100644 --- a/src/bricksplom/constants.py +++ b/src/bricksplom/constants.py @@ -1,2 +1,3 @@ 'Independent constants.' CHAR_NEWLINE = '\n' +NUMBER_UNSET = -1 diff --git a/src/bricksplom/ldraw.py b/src/bricksplom/ldraw.py index 1a6fcfc..4fa56bc 100644 --- a/src/bricksplom/ldraw.py +++ b/src/bricksplom/ldraw.py @@ -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) diff --git a/src/bricksplom/misc.py b/src/bricksplom/misc.py index 3b1aa5c..221b56c 100644 --- a/src/bricksplom/misc.py +++ b/src/bricksplom/misc.py @@ -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(