Skip to content

feat(materials): dynamic library registry, picker source tabs, Other category#525

Merged
wass08 merged 5 commits into
mainfrom
feat/ai-materials-library
Jul 21, 2026
Merged

feat(materials): dynamic library registry, picker source tabs, Other category#525
wass08 merged 5 commits into
mainfrom
feat/ai-materials-library

Conversation

@wass08

@wass08 wass08 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Editor-side support for the hosted AI material generation pass (private-editor companion PR).

What's in here

  • Dynamic material library registry (@pascal-app/core): hosts can registerLibraryMaterials / unregisterLibraryMaterials at runtime so DB-backed generated materials resolve as library:<id> refs in the viewer and paint tool, with a version counter + subscription for React integration.
  • Material picker source tabs (@pascal-app/editor): All / Pascal / Mine / Workspace / Community filter row in the catalog material picker, restyled to the underline tab convention used by the other browse surfaces (was pill buttons identical to the category row above it). Workspace tab only shows when workspace materials exist.
  • Create-material entry point: optional onCreateMaterialRequest hook renders a "New material" tile so hosts can route into their authoring flow (the community studio Materials tab).
  • other material category: new fallback category so uncategorized generated materials get their own home instead of polluting a real category; category tabs only render when non-empty.

Testing

  • Verified E2E from the community app on :3005: generated Patina materials register into the picker, source tabs filter correctly, paint tool applies library: refs.
  • bun typecheck green in the consuming monorepo.

🤖 Generated with Claude Code


Note

Medium Risk
Touches core material resolution, editor paint UX, and WebGPU material graph behavior—regressions could affect catalog display, library: refs, or render stability when painting.

Overview
Adds runtime host registration for catalog materials so generated or DB-backed finishes resolve as library:<id> alongside the static catalog, with subscribe/version hooks for React and static IDs winning on collision. Catalog items gain optional source (pascal / mine / workspace / community) and an other category for uncategorized generated materials.

The material picker gains underline source filter tabs, optional New material via onCreateMaterialRequest (wired through MaterialPaintPanel), and re-renders when the dynamic library changes.

The viewer fixes WebGPU crashes when swapping paint-preview materials by always giving MeshStandardNodeMaterial a neutral displacement placeholder and rebuilding the node graph when texture slots clear or cold-load instead of leaving null TextureNode references.

Reviewed by Cursor Bugbot for commit fd087fa. Bugbot is set up for automated code reviews on this repo. Configure here.

wass08 and others added 3 commits July 20, 2026 14:20
…-material entry point

Core gains a runtime material registry (registerLibraryMaterials/
unregisterLibraryMaterials/subscribeLibraryMaterials) so embedders can
feed user/community materials; library: refs to registered materials
resolve in the viewer unchanged. MaterialCatalogItem carries an optional
source (pascal|community|mine|workspace). MaterialPicker gets a source
filter row and an optional onCreateMaterialRequest '+ New material'
tile, threaded through MaterialPaintPanel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wse surfaces

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ials

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(category) => getMaterialsForCategory(category).length > 0,
)
const catalogItems = getMaterialsForCategory(selectedCategory)
const catalogItems = filterBySource(getMaterialsForCategory(selectedCategory), sourceFilter)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Category tabs ignore source filter

Medium Severity

The material picker can show an empty grid and a broken UI state when the selected category or source filter no longer has any materials. This happens because availableCategories doesn't filter by the active sourceFilter, and selectedCategory or sourceFilter aren't reset when dynamic materials unregister, leaving the current selection invalid.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2e419f6. Configure here.

(category) => getMaterialsForCategory(category).length > 0,
)
const catalogItems = getMaterialsForCategory(selectedCategory)
const catalogItems = filterBySource(getMaterialsForCategory(selectedCategory), sourceFilter)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Category sync ignores library updates

Medium Severity

The effect that syncs selectedCategory to the active catalog material only depends on selectedMaterialPreset, not libraryVersion. If that ref is already set before registerLibraryMaterials resolves the entry—or the entry reappears after unregister—getCatalogMaterialById runs too early, the effect never re-runs, and the picker stays on the wrong category so the selected swatch never comes into view.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2e419f6. Configure here.

Reused cached WebGPU materials keep a compiled TextureNode per slot; nulling
the slot for a cold texture load (or a preset without the map) without
needsUpdate leaves the node's per-frame material reference pulling null,
crashing the render pass in TextureNode.update. Cold loads are the norm for
freshly generated library materials, whose maps aren't in the texture cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

There are 6 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 87d8d18. Configure here.

(category) => getMaterialsForCategory(category).length > 0,
)
const catalogItems = getMaterialsForCategory(selectedCategory)
const catalogItems = filterBySource(getMaterialsForCategory(selectedCategory), sourceFilter)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brush survives material unregister

Medium Severity

Unregistering a library material leaves activePaintMaterial pointing at that library: ref. The picker re-renders from the registry subscription, but nothing clears the brush, so further painting can write dangling refs that resolveMaterialRef cannot resolve.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 87d8d18. Configure here.

Comment thread packages/editor/src/components/ui/controls/material-picker.tsx
for (const item of items) {
dynamicLibraryMaterials.set(item.id, item)
}
notifyDynamicLibraryChange()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static id collision ghost tab

Low Severity

registerLibraryMaterials stores dynamic materials even when their IDs collide with static catalog items. While some getters, like getMaterialsForCategory, filter these collisions, getDynamicLibraryMaterials returns the unfiltered list. This can result in a source: 'workspace' material being registered but not displayed in the UI, potentially showing an empty Workspace tab.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 87d8d18. Configure here.

for (const item of items) {
dynamicLibraryMaterials.set(item.id, item)
}
notifyDynamicLibraryChange()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Library refs cached as static

High Severity

registerLibraryMaterials can add or overwrite library: presets at runtime, but only the picker subscribes to version changes. Viewer wall/roof/fence caches still key library: slots by ref string alone, so late registration or same-id preset updates leave painted geometry on stale fallbacks or old textures.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 87d8d18. Configure here.

three's WebGPU shadow pass copies each object's displacementMap onto one
shared per-light shadow material while caching per-mesh shadow node graphs
against that shared override. A mesh whose shadow graph was built with a
displacement TextureNode (painted with a generated material — the only
presets carrying height maps) crashes the render pass with "null (reading
'matrix')" the moment a material without a displacement texture is swapped
onto it, which is exactly what the paint hover preview does. Give every
MeshStandardNodeMaterial a shared 1x1 black displacement texel (zero offset)
when it has no real height map, so the copied slot is never null in either
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wass08
wass08 merged commit 603f5d2 into main Jul 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant