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',
'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)
@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))
for idx, rotscal in enumerate((self.rotscal_x,
self.rotscal_y,
self.rotscal_z)))
- return _XYZ(*xyz)
+ return XYZ(*xyz)
@classmethod
def from_toks(
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(
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)
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 = '#'
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
@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
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
'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
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(
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)}'
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(