diff --git a/masque/library.py b/masque/library.py index bb2e3d2..13b4d26 100644 --- a/masque/library.py +++ b/masque/library.py @@ -1037,6 +1037,18 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): def label2name(label: tuple) -> str: # noqa: ARG001 return self.get_name(SINGLE_USE_PREFIX + 'shape') + used_names = set(self.keys()) + + def reserve_target_name(label: tuple) -> str: + base_name = label2name(label) + name = base_name + ii = sum(1 for nn in used_names if nn.startswith(base_name)) if base_name in used_names else 0 + while name in used_names or name == '': + name = base_name + b64suffix(ii) + ii += 1 + used_names.add(name) + return name + shape_counts: MutableMapping[tuple, int] = defaultdict(int) shape_funcs = {} @@ -1053,6 +1065,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): shape_counts[label] += 1 shape_pats = {} + target_names = {} for label, count in shape_counts.items(): if count < threshold: continue @@ -1061,6 +1074,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): shape_pat = Pattern() shape_pat.shapes[label[-1]] += [shape_func()] shape_pats[label] = shape_pat + target_names[label] = reserve_target_name(label) # ## Second pass ## for pat in tuple(self.values()): @@ -1085,10 +1099,10 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): # For repeated shapes, create a `Pattern` holding a normalized shape object, # and add `pat.refs` entries for each occurrence in pat. Also, note down that # we should delete the `pat.shapes` entries for which we made `Ref`s. - shapes_to_remove = [] for label, shape_entries in shape_table.items(): layer = label[-1] - target = label2name(label) + target = target_names[label] + shapes_to_remove = [] for ii, values in shape_entries: offset, scale, rotation, mirror_x = values pat.ref(target=target, offset=offset, scale=scale, @@ -1100,7 +1114,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): del pat.shapes[layer][ii] for ll, pp in shape_pats.items(): - self[label2name(ll)] = pp + self[target_names[ll]] = pp return self diff --git a/masque/test/test_library.py b/masque/test/test_library.py index e58bd10..335b6ec 100644 --- a/masque/test/test_library.py +++ b/masque/test/test_library.py @@ -259,3 +259,43 @@ def test_library_dedup_shapes_does_not_merge_custom_capped_paths() -> None: assert not lib["top"].refs assert len(lib["top"].shapes[(1, 0)]) == 2 + + +def test_library_dedup_handles_multiple_duplicate_groups() -> None: + from ..shapes import Circle + + lib = Library() + pat = Pattern() + pat.shapes[(1, 0)] += [Circle(radius=1, offset=(0, 0)), Circle(radius=1, offset=(10, 0))] + pat.shapes[(2, 0)] += [Path(vertices=[[0, 0], [5, 0]], width=2), Path(vertices=[[10, 0], [15, 0]], width=2)] + lib["top"] = pat + + lib.dedup(exclude_types=(), norm_value=1, threshold=2) + + assert len(lib["top"].refs) == 2 + assert all(len(refs) == 2 for refs in lib["top"].refs.values()) + assert len(lib["top"].shapes[(1, 0)]) == 0 + assert len(lib["top"].shapes[(2, 0)]) == 0 + + +def test_library_dedup_uses_stable_target_names_per_label() -> None: + from ..shapes import Circle + + lib = Library() + + p1 = Pattern() + p1.shapes[(1, 0)] += [Circle(radius=1, offset=(0, 0)), Circle(radius=1, offset=(10, 0))] + lib["p1"] = p1 + + p2 = Pattern() + p2.shapes[(2, 0)] += [Path(vertices=[[0, 0], [5, 0]], width=2), Path(vertices=[[10, 0], [15, 0]], width=2)] + lib["p2"] = p2 + + lib.dedup(exclude_types=(), norm_value=1, threshold=2) + + circle_target = next(iter(lib["p1"].refs)) + path_target = next(iter(lib["p2"].refs)) + + assert circle_target != path_target + assert all(isinstance(shape, Circle) for shapes in lib[circle_target].shapes.values() for shape in shapes) + assert all(isinstance(shape, Path) for shapes in lib[path_target].shapes.values() for shape in shapes)