Context
Phase 1 of the dispatch refactor (spike branch spike/vis-asset-dispatch, closing mat#220 / mat-vis #285 / mat-vis #298) made Vis a thin delegate to mat-vis's client.asset(...). Phase 2 is the symmetric step on the browse side: surface mat-vis's search() / Match unification (mat-vis #359) on Vis so users can answer "what catalog appearances match this material?" without manually building queries.
Today
Vis.discover(*, category=None, roughness=None, metallic=None, limit=5, auto_set=False) exists but:
- The caller has to pass scalar/category filters explicitly — even when the calling Vis ALREADY has them set (
vis.roughness, vis.metallic, owning Material's category).
auto_set=True mutates the Vis in place; no immutable variant.
- Returns
list[dict] today; on mat-vis dev (#359) search() returns list[Match] — Phase 2 should pin the Match contract.
- No fuzzy name matching (
search(query=...)) — but probably out of scope until upstream adds it.
Proposed shape
```python
class Vis:
def candidates(
self,
*,
# All optional — defaults derive from this Vis's own state
category: str | None = None, # else from owning Material's category (if set)
roughness: float | None = None, # else self.roughness
metalness: float | None = None, # else self.metallic
roughness_range: tuple[float, float] | None = None,
metalness_range: tuple[float, float] | None = None,
source: str | None = None,
tier: str = "1k",
limit: int = 10,
) -> list["Match"]:
"""Find catalog appearances matching this material's properties.
Auto-populates the search query from this Vis's own PBR scalars
when filters aren't supplied. Returns Match handles ready to
feed to ``client.asset(m)`` or assign back via ``vis.with_match(m)``.
\"\"\"
from mat_vis_client import search
return search(
category=category, # caller-passed wins; else add owning-Material category lookup
roughness=roughness if roughness is not None else self.roughness,
metalness=metalness if metalness is not None else self.metallic,
roughness_range=roughness_range,
metalness_range=metalness_range,
source=source,
tier=tier,
limit=limit,
)
def with_match(self, match: \"Match\") -> Vis:
\"\"\"Return a new Vis with this Match's identity (source, id, tier).
Immutable companion to ``set_identity`` / ``override``.\"\"\"
return self.override(
source=match.source,
material_id=match.id,
tier=match.tiers[0] if match.tiers else self.tier,
)
```
Migration of existing discover()
Two paths:
A. Add candidates alongside; deprecate discover. discover was added before mat-vis #359; it pre-dates Match. candidates is the cleaner name (returns the candidate set; doesn't imply mutation). Mark discover deprecated in 3.x, remove in 4.x.
B. Evolve discover in place. Accept new auto-query semantics; keep the name. Less churn for existing callers; loses the chance to introduce with_match symmetry.
Recommendation: A. The semantics shift (auto-query, Match return, no mutation) is observable enough to warrant the new name.
Acceptance
Out of scope
- Name-fuzzy search (
search(query=\"steel\")) — needs upstream support; file separately if/when needed
- Cross-tier discovery (rank by which tier is staged) — a search ergonomics issue
- Rendering candidates as thumbnails — covered by mat-vis #362 (VisAsset.thumb)
Related
- spike PR
spike/vis-asset-dispatch (Phase 1 — render dispatch refactor)
- mat-vis #359 (Match unification — the substrate Phase 2 builds on)
- mat-vis #367 (
_scalars_for name resolution — orthogonal asymmetry, not blocking Phase 2)
- ADR-0002 Principle 3 (thin delegation sugar)
Context
Phase 1 of the dispatch refactor (spike branch
spike/vis-asset-dispatch, closing mat#220 / mat-vis #285 / mat-vis #298) madeVisa thin delegate to mat-vis'sclient.asset(...). Phase 2 is the symmetric step on the browse side: surface mat-vis'ssearch()/ Match unification (mat-vis #359) onVisso users can answer "what catalog appearances match this material?" without manually building queries.Today
Vis.discover(*, category=None, roughness=None, metallic=None, limit=5, auto_set=False)exists but:vis.roughness,vis.metallic, owning Material's category).auto_set=Truemutates the Vis in place; no immutable variant.list[dict]today; on mat-vis dev (#359)search()returnslist[Match]— Phase 2 should pin the Match contract.search(query=...)) — but probably out of scope until upstream adds it.Proposed shape
```python
class Vis:
def candidates(
self,
*,
# All optional — defaults derive from this Vis's own state
category: str | None = None, # else from owning Material's category (if set)
roughness: float | None = None, # else self.roughness
metalness: float | None = None, # else self.metallic
roughness_range: tuple[float, float] | None = None,
metalness_range: tuple[float, float] | None = None,
source: str | None = None,
tier: str = "1k",
limit: int = 10,
) -> list["Match"]:
"""Find catalog appearances matching this material's properties.
```
Migration of existing
discover()Two paths:
A. Add
candidatesalongside; deprecatediscover.discoverwas added before mat-vis #359; it pre-dates Match.candidatesis the cleaner name (returns the candidate set; doesn't imply mutation). Markdiscoverdeprecated in 3.x, remove in 4.x.B. Evolve
discoverin place. Accept new auto-query semantics; keep the name. Less churn for existing callers; loses the chance to introducewith_matchsymmetry.Recommendation: A. The semantics shift (auto-query, Match return, no mutation) is observable enough to warrant the new name.
Acceptance
vis.candidates()with no args returns matches scored against this Vis's own scalars (no manual roughness=, metalness=)list[Match]post mat-vis #359; falls back gracefully on older client versionsvis.with_match(match)returns a new Vis with the match's identity; original Vis unchangedvis.candidates()does NOT trigger texture HTTP fetches (search is index-only)Material.vis, look up Material's category for the search filter (today'sdiscoverdoesn't auto-derive category either)```python
steel = pymat["Stainless Steel 304"]
for m in steel.vis.candidates():
print(m) # uses Match.str — name + scalars + tiers
picked = steel.with_vis(steel.vis.with_match(steel.vis.candidates()[0]))
picked.vis.to_threejs() # works end-to-end
```
Out of scope
search(query=\"steel\")) — needs upstream support; file separately if/when neededRelated
spike/vis-asset-dispatch(Phase 1 — render dispatch refactor)_scalars_forname resolution — orthogonal asymmetry, not blocking Phase 2)