From: Plom Heller Date: Tue, 4 Aug 2026 16:45:01 +0000 (+0200) Subject: Add -f ldraw_tree to visualize LDRAW file dependencies. X-Git-Url: https://plomlompom.com/repos/%22https:/validator.w3.org/index.html?a=commitdiff_plain;h=d93be2230ef0e7cbe26eae1978df805eb8002020;p=bricksplom Add -f ldraw_tree to visualize LDRAW file dependencies. --- diff --git a/src/bricksplom/ldraw.py b/src/bricksplom/ldraw.py index 04572d9..bef23b8 100644 --- a/src/bricksplom/ldraw.py +++ b/src/bricksplom/ldraw.py @@ -18,6 +18,10 @@ LDRAW_STUDS = { } +LdrawRefsTree = tuple[Path, '_SubRefs'] +_SubRefs = dict[str, LdrawRefsTree] + + class LdrawDb: 'Connection to local LDRAW archive.' @@ -31,8 +35,29 @@ class LdrawDb: self, filename ) -> Path: - 'Build {._root}/part/{filename}.' - return self._root.joinpath('parts', filename) + 'Find actual path from filename in part/, p/ etc.' + for prefix in ('parts', 'p'): + path = self._root.joinpath(prefix, filename) + if path.exists(): + break + if not path.exists(): + raise Exception(f'invalid ldraw reference: {filename}') + return path + + def tree( + self, + filename: str + ) -> LdrawRefsTree: + 'Produce LdrawRefsTree for filename.' + path = self.path(filename) + refs: _SubRefs = {} + for line in path.read_text(encoding='utf8').split(CHAR_NEWLINE): + line = line.lstrip() + if not line.startswith('1'): + continue + ref = line.split(maxsplit=14)[-1].replace('\\', '/') + refs[ref] = self.tree(ref) + return path, refs def count_studs( self, @@ -40,10 +65,6 @@ class LdrawDb: ) -> int: 'On item at {._root}/p(art)/{filename} count studs.' path = self.path(filename) - if not path.exists(): - path = self._root.joinpath('p', filename) - if not path.exists(): - raise Exception(f'invalid ldraw reference: {filename}') studs_counted = 0 for line in path.read_text(encoding='utf8').split(CHAR_NEWLINE): line = line.lstrip() diff --git a/src/bricksplom/misc.py b/src/bricksplom/misc.py index 76d8417..90aa8ec 100644 --- a/src/bricksplom/misc.py +++ b/src/bricksplom/misc.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Callable, Optional, Self # ourselves from bricksplom.constants import CHAR_NEWLINE -from bricksplom.ldraw import LdrawDb +from bricksplom.ldraw import LdrawDb, LdrawRefsTree CHAR_SEP_TOKEN = ' ' CHAR_COMMENT = '#' @@ -104,14 +104,14 @@ class Textfiled(ABC): class WithDb: - 'Add db:Optional[BricksDB] field to __init__, setting ._db.' + 'Add db:Optional[BricksDB] field to __init__, setting .db.' def __init__( self, db: Optional['BricksDb'] = None, **kwargs ) -> None: - self._db = db + self.db = db super().__init__(**kwargs) @@ -271,7 +271,7 @@ class BrickDesignData: ldraw: str = '' -class BrickDesign(Textfiled, Lookupable): +class BrickDesign(Textfiled, WithDb, Lookupable): 'Shape and texture configurations with descriptions and equalities.' _id_indent = 6 alternate_to: Optional[Self] = None @@ -279,11 +279,13 @@ class BrickDesign(Textfiled, Lookupable): def __init__( self, id_: str, - attrs: Optional[BrickDesignData] = None + attrs: Optional[BrickDesignData] = None, + **kwargs ) -> None: self.id_ = id_ self.direct_attrs = attrs self.alternate_ids: set[str] = set() + super().__init__(**kwargs) def __getattribute__(self, key: str): if key in BrickDesignData.__annotations__: @@ -303,6 +305,34 @@ class BrickDesign(Textfiled, Lookupable): 'Own .id_ plus (sorted) .alternate_ids.' return tuple([self.id_] + sorted(list(self.alternate_ids))) + @classmethod + def formatters( + cls + ) -> dict[str, tuple[str, Callable[[Self], str]]]: + def ldraw_tree( + item: Self + ) -> str: + def walk( + lines: list[str], + filename: str, + refstree: LdrawRefsTree, + depth: int = 0 + ) -> None: + indent = depth * 4 * ' ' + path, subrefs = refstree + lines += [f'{indent}"{filename}": {path}'] + for refname, subrefstree in subrefs.items(): + walk(lines, refname, subrefstree, depth+1) + if not item.ldraw: + return 'no LDRAW file associated' + lines: list[str] = [] + assert item.db is not None + walk(lines, item.ldraw, item.db.ldraw.tree(item.ldraw)) + return CHAR_NEWLINE.join(lines) + + return super().formatters() | { + 'ldraw_tree': ('tree of LDRAW references', ldraw_tree)} + @classmethod def matchers( cls @@ -348,6 +378,7 @@ class BrickDesign(Textfiled, Lookupable): def from_textfile( cls, path: tuple[str, str], + db: Optional['BricksDb'] = None, **_ ) -> dict[str, Self]: collected = {} @@ -360,7 +391,7 @@ class BrickDesign(Textfiled, Lookupable): alt_id = body[1:] alts[alt_id] = alts.get(alt_id, set()) alts[alt_id].add(design_id) - collected[design_id] = cls(design_id) + collected[design_id] = cls(design_id, db=db) else: assert SEP_DESIGN_DESC in body metadata, desc = body.split(SEP_DESIGN_DESC, maxsplit=1) @@ -375,7 +406,7 @@ class BrickDesign(Textfiled, Lookupable): setattr(attrs, a_key, int(a_val_str)) else: setattr(attrs, a_key, a_val_str) - collected[design_id] = cls(design_id, attrs) + collected[design_id] = cls(design_id, attrs, db=db) for id_, alternate_ids in alts.items(): collected[id_].alternate_ids = alternate_ids for alt_id in alternate_ids: @@ -450,9 +481,9 @@ class Brick(Textfiled, WithDb, Lookupable): def __str__( self ) -> str: - assert self._db - design = self._db.designs[self.design_id] - color = str(self._db.colors[self.color_id]).strip() + assert self.db + design = self.db.designs[self.design_id] + color = str(self.db.colors[self.color_id]).strip() comment = f' # {self.comment}' if self.comment else '' return (f'{self.id_indented()} ' f'{BrickDesign.indent_id(self.design_id)} ' @@ -567,14 +598,14 @@ class BrickSet(Textfiled, WithDb, Lookupable): self ) -> str: def format_line(count, brick_id, comment) -> str: - assert self._db - brick = self._db.bricks[brick_id] + assert self.db + brick = self.db.bricks[brick_id] design_id = brick.design_id tail_comment = f' # {comment}' if comment else '' - color = str(self._db.colors[self._db.bricks[brick_id].color_id] + color = str(self.db.colors[self.db.bricks[brick_id].color_id] ).lstrip().split(CHAR_SEP_TOKEN, maxsplit=1)[1] box: Optional[Box] = None - for i_box in self._db.boxes.values(): + for i_box in self.db.boxes.values(): for idx_in_box in [ idx for idx, d_to_ls in enumerate(i_box.designs_to_listings) @@ -584,7 +615,7 @@ class BrickSet(Textfiled, WithDb, Lookupable): if box: break if not box: - for i_box in self._db.boxes.values(): + for i_box in self.db.boxes.values(): for idx_in_box in [ idx for idx, t in enumerate(i_box.designs_to_listings) @@ -598,7 +629,7 @@ class BrickSet(Textfiled, WithDb, Lookupable): return ( f'{count:>2}× {Brick.indent_id(brick_id)}:' f'{BrickDesign.indent_id(design_id)} {box_listing} {color} ' - + self._db.designs[design_id].description + tail_comment) + + self.db.designs[design_id].description + tail_comment) return f'{self}{CHAR_NEWLINE}{self._format_paginated(format_line)}' @@ -614,12 +645,12 @@ class Box(WithDb, Lookupable): **kwargs ) -> None: super().__init__(**kwargs) - assert self._db + assert self.db self.id_ = id_ self._set = bricks_set designed_listings: list[tuple[BrickDesign, list[BrickListing]]] = [] for listing in self.brick_listings_flat(): - design = self._db.designs[self._db.bricks[listing[1]].design_id] + design = self.db.designs[self.db.bricks[listing[1]].design_id] design = design.alternate_to or design if not (designed_listings and designed_listings[-1][0] == design): designed_listings += [(design, []), ] @@ -644,13 +675,13 @@ class Box(WithDb, Lookupable): def show( self, ) -> str: - assert self._db + assert self.db lines = [] for design, listings in self.designs_to_listings: lines += [ f'=== {" / ".join(design.all_ids)}: {design.description} ==='] for count, brick_id, comment in listings: - color = self._db.colors[self._db.bricks[brick_id].color_id] + color = self.db.colors[self.db.bricks[brick_id].color_id] lines += [f'{count:>2}× {Brick.indent_id(brick_id)} / {color} ' f'# {comment}'] return CHAR_NEWLINE.join(lines) @@ -681,13 +712,13 @@ class BricksDb: kwargs = {'db': self} if issubclass(cls, WithDb) else {} setattr(self, name, cls.from_textfile((path_tables, f'{name}.txt'), **kwargs)) - self._ldraw = LdrawDb(Path(path_ldraw)) + self.ldraw = LdrawDb(Path(path_ldraw)) self._ldraw_modes = ldraw_modes.split(CHAR_SEP_LDRAW_MODES) if LDRAW_MODE_FILL_PATHS in self._ldraw_modes: for design in [design for design in self.designs.values() if not (design.alternate_to or design.ldraw)]: filename = f'{design.id_}.dat' - if self._ldraw.path(filename).exists(): + if self.ldraw.path(filename).exists(): assert design.direct_attrs is not None design.direct_attrs.ldraw = filename self._check_consistencies() @@ -696,7 +727,7 @@ class BricksDb: if d.n_studs != 1]: assert design.direct_attrs is not None design.direct_attrs.n_studs\ - = self._ldraw.count_studs(design.ldraw) + = self.ldraw.count_studs(design.ldraw) @property def boxes( @@ -758,7 +789,7 @@ class BricksDb: for design in self._designs_ldrawed(): # hunt for dangling Design.ldraw references if LDRAW_MODE_VERIFY_PATHS in self._ldraw_modes: - if not self._ldraw.path(design.ldraw).exists(): + if not self.ldraw.path(design.ldraw).exists(): fails += ['unresolved ldraw reference for design of ID: ' f'{design.id_} ({design.ldraw})'] continue @@ -766,7 +797,7 @@ class BricksDb: if LDRAW_MODE_VERIFY_N_STUDS in self._ldraw_modes: if design.n_studs <= -1: continue - studs_counted = self._ldraw.count_studs(design.ldraw) + studs_counted = self.ldraw.count_studs(design.ldraw) if design.n_studs != studs_counted: fails += ['n_studs count differs between record and ldraw ' f'calculation for design of ID: {design.id_} '