diff --git a/src/shader/ShaderPackageRegistry.cpp b/src/shader/ShaderPackageRegistry.cpp index 2517755..2a91ff2 100644 --- a/src/shader/ShaderPackageRegistry.cpp +++ b/src/shader/ShaderPackageRegistry.cpp @@ -77,6 +77,7 @@ bool ShaderPackageRegistry::Scan( return false; } + std::map customUiTagOwners; for (const auto& entry : std::filesystem::directory_iterator(shaderRoot)) { if (!entry.is_directory()) @@ -100,6 +101,21 @@ bool ShaderPackageRegistry::Scan( continue; } + if (shaderPackage.ui.enabled) + { + const auto tagOwnerIt = customUiTagOwners.find(shaderPackage.ui.customElementTag); + if (tagOwnerIt != customUiTagOwners.end()) + { + packageStatuses.push_back(BuildUnavailableStatus( + manifestPath, + shaderPackage, + "Duplicate shader UI custom element tag '" + shaderPackage.ui.customElementTag + + "' already used by shader '" + tagOwnerIt->second + "'.")); + continue; + } + customUiTagOwners[shaderPackage.ui.customElementTag] = shaderPackage.id; + } + packageOrder.push_back(shaderPackage.id); packageStatuses.push_back(BuildAvailableStatus(shaderPackage)); packagesById[shaderPackage.id] = shaderPackage; diff --git a/tests/ShaderPackageRegistryTests.cpp b/tests/ShaderPackageRegistryTests.cpp index b90210b..dcba966 100644 --- a/tests/ShaderPackageRegistryTests.cpp +++ b/tests/ShaderPackageRegistryTests.cpp @@ -138,6 +138,38 @@ void TestInvalidUiDefinition() std::filesystem::remove_all(root); } +void TestDuplicateCustomUiTags() +{ + const std::filesystem::path root = MakeTestRoot(); + WriteFile(root / "a" / "ui" / "controls.js", "customElements.define('shared-controls', class extends HTMLElement {});\n"); + WriteFile(root / "b" / "ui" / "controls.js", "customElements.define('shared-controls', class extends HTMLElement {});\n"); + WriteShaderPackage(root, "a", R"({ + "id": "shader-a", + "name": "Shader A", + "ui": { "type": "webComponent", "entry": "ui/controls.js", "tag": "shared-controls" }, + "parameters": [] + })"); + WriteShaderPackage(root, "b", R"({ + "id": "shader-b", + "name": "Shader B", + "ui": { "type": "webComponent", "entry": "ui/controls.js", "tag": "shared-controls" }, + "parameters": [] + })"); + + ShaderPackageRegistry registry(4); + std::map packages; + std::vector order; + std::vector statuses; + std::string error; + Expect(registry.Scan(root, packages, order, statuses, error), "duplicate custom UI tags do not fail the whole scan"); + Expect(packages.size() == 1, "only one duplicate custom UI tag owner remains available"); + Expect(std::any_of(statuses.begin(), statuses.end(), [](const ShaderPackageStatus& status) { + return !status.available && status.error.find("Duplicate shader UI custom element tag") != std::string::npos; + }), "duplicate custom UI tag is surfaced as an unavailable shader"); + + std::filesystem::remove_all(root); +} + void TestExplicitPassManifest() { const std::filesystem::path root = MakeTestRoot(); @@ -335,6 +367,7 @@ int main() TestValidManifest(); TestExplicitPassManifest(); TestInvalidUiDefinition(); + TestDuplicateCustomUiTags(); TestMissingFontAsset(); TestInvalidManifest(); TestInvalidTemporalSettings(); diff --git a/ui/src/components/ShaderCustomPanel.jsx b/ui/src/components/ShaderCustomPanel.jsx index 6218ce7..20d3394 100644 --- a/ui/src/components/ShaderCustomPanel.jsx +++ b/ui/src/components/ShaderCustomPanel.jsx @@ -140,7 +140,11 @@ export function ShaderCustomPanel({ layer, ui, onParameterChange, onResetParamet return; } if (element.parentNode !== node) { - node.replaceChildren(element); + try { + node.replaceChildren(element); + } catch (error) { + setLoadError(error instanceof Error ? error.message : "Custom controls failed to mount."); + } } }} >