feat(registry): AssetResolver + diffuser supplier curves (Jäger / Aerostrip / PIK / PRK)
Two related changes bundled together because the diffuser curve files
only make sense once the registry namespace they live in exists.
src/registry — new asset-metadata resolver:
- AssetResolver with synchronous resolve(namespace, id) + lazy cache,
async refresh() for future remote pulls.
- FileBackend (per-id or single-file layouts, case-insensitive) and a
stub HttpBackend (disabled unless EVOLV_ASSET_REMOTE=1).
- Namespaces: curves, menu, monsterSamples, monsterSpecs, units. Menu
namespace re-keys by inner softwareType + filename so editors that
pass either string resolve to the same tree.
- README explains how to add a namespace.
- AssetCategoryManager (datasets/assetData/index.js) becomes a thin
facade over the resolver so existing consumers don't move.
- 246/246 tests pass — including the 39-test registry suite.
datasets/assetData — file moves + new diffuser data:
- modelData/*.json deleted; curves/*.json is the canonical home.
- New diffuser.json menu tree with GVA, Jäger, Aquaconsult/Entec,
PIK/PRK suppliers.
- gva-elastox-r.json migrated from the inline _loadSpecs hardcode,
re-tagged coverageBasis="bottom-coverage-pct" (the legacy 2.4
elements/m² was a prior mis-conversion; we can't recover the
original % so it's a single-point curve under key "0").
- jaeger-jetflex-td-65-2-g-epdm-1000.json — extracted from the Jäger
EPDM-1000mm SSOTE/DWP chart on the data sheet (vector-PDF read).
SSOTE 8.20→6.40 %/m, DWP 25→48 mbar across Q 2-12 Nm³/h. Single
coverage (vendor doesn't state test conditions).
- aerostrip-phoenix.json — 4-coverage SOTE family at 4.75 m water
depth (DD 5/10/15/20 %, flux 10-70 Nm³/h·m²) from the Entec/de
Winter 2023-11-22 dataset; DWP curve from the 21 % @ 4.05 m chart.
- pik300.json / prk300.json — 5-coverage SOTE + SSOTR (DD 5-25 %)
with split DWP per model variant, water depth ≈ 4.0 m inferred from
the SOTE↔SSOTR ratio in the source spreadsheet.
src/configs/diffuser.json:
- New asset.{model, assetTagNumber} block so the editor's selected
model id survives validation.
- diffuser.density description corrected to "Bottom coverage [%]";
default 2.4 → 15 (typical fine-bubble install).
src/configs/{rotatingMachine,valve}.json: small alignment edits that
came with the registry phase.
src/menu/asset.js + src/menu/aquonSamples.js: rewritten as facades
over assetResolver, keeping the editor-side cascade behaviour intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,41 +1,30 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
'use strict';
|
||||
|
||||
// AquonSamplesMenu is now a thin facade over assetResolver.
|
||||
// Backed by namespaces `monsterSamples` (sample codes, indexed by code)
|
||||
// and `monsterSpecs` (sampling defaults + per-sample overrides).
|
||||
|
||||
const { assetResolver } = require('../registry');
|
||||
|
||||
class AquonSamplesMenu {
|
||||
constructor(relPath = '../../datasets/assetData') {
|
||||
this.baseDir = path.resolve(__dirname, relPath);
|
||||
this.samplePath = path.resolve(this.baseDir, 'monsterSamples.json');
|
||||
this.specPath = path.resolve(this.baseDir, 'specs/monster/index.json');
|
||||
this.cache = new Map();
|
||||
}
|
||||
// relPath retained for signature compatibility with the previous on-disk
|
||||
// implementation; unused — the registry owns file locations.
|
||||
constructor(/* relPath */) {}
|
||||
|
||||
_loadJSON(filePath, cacheKey) {
|
||||
if (this.cache.has(cacheKey)) {
|
||||
return this.cache.get(cacheKey);
|
||||
getAllMenuData() {
|
||||
const samples = assetResolver
|
||||
.list('monsterSamples')
|
||||
.map((id) => assetResolver.resolve('monsterSamples', id))
|
||||
.filter(Boolean);
|
||||
const specs = assetResolver.resolve('monsterSpecs', 'all') || { defaults: {}, bySample: {} };
|
||||
return {
|
||||
samples,
|
||||
specs: {
|
||||
defaults: specs.defaults || {},
|
||||
bySample: specs.bySample || {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Aquon dataset not found: ${filePath}`);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
this.cache.set(cacheKey, parsed);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
getAllMenuData() {
|
||||
const samples = this._loadJSON(this.samplePath, 'samples');
|
||||
const specs = this._loadJSON(this.specPath, 'specs');
|
||||
|
||||
return {
|
||||
samples: samples.samples || [],
|
||||
specs: {
|
||||
defaults: specs.defaults || {},
|
||||
bySample: specs.bySample || {}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AquonSamplesMenu;
|
||||
|
||||
@@ -624,46 +624,40 @@ class AssetMenu {
|
||||
}
|
||||
|
||||
getSaveInjectionCode(nodeName) {
|
||||
// After the AssetResolver cutover, only model + unit + tagCode are stored
|
||||
// on the node. supplier / assetType / category were denormalized copies of
|
||||
// registry data and are derived at runtime via
|
||||
// assetResolver.resolveAssetMetadata(softwareType, model).
|
||||
//
|
||||
// We still READ the supplier/type DOM elements for validation (the user
|
||||
// must have walked the cascade to pick a model), but we explicitly CLEAR
|
||||
// them from the persisted node — so a saved flow only contains the
|
||||
// identifier surface.
|
||||
return `
|
||||
// Asset save handler for ${nodeName}
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.saveEditor = function(node) {
|
||||
console.log('Saving asset properties for ${nodeName}');
|
||||
const menuAsset = window.EVOLV.nodes.${nodeName}.menuData.asset || {};
|
||||
const categories = menuAsset.categories || {};
|
||||
const defaultCategory = menuAsset.defaultCategory || Object.keys(categories)[0] || null;
|
||||
const resolveCategoryKey = () => {
|
||||
if (node.softwareType && categories[node.softwareType]) {
|
||||
return node.softwareType;
|
||||
}
|
||||
if (node.category && categories[node.category]) {
|
||||
return node.category;
|
||||
}
|
||||
return defaultCategory || '';
|
||||
};
|
||||
|
||||
node.category = resolveCategoryKey();
|
||||
|
||||
const fields = ['supplier', 'assetType', 'model', 'unit', 'assetTagNumber'];
|
||||
const errors = [];
|
||||
|
||||
fields.forEach((field) => {
|
||||
const el = document.getElementById(\`node-input-\${field}\`);
|
||||
node[field] = el ? el.value : '';
|
||||
});
|
||||
const modelEl = document.getElementById('node-input-model');
|
||||
const unitEl = document.getElementById('node-input-unit');
|
||||
const tagEl = document.getElementById('node-input-assetTagNumber');
|
||||
|
||||
if (node.assetType && !node.unit) {
|
||||
errors.push('Unit must be set when a type is specified.');
|
||||
}
|
||||
if (!node.unit) {
|
||||
errors.push('Unit is required.');
|
||||
}
|
||||
node.model = modelEl ? modelEl.value : '';
|
||||
node.unit = unitEl ? unitEl.value : '';
|
||||
node.assetTagNumber = tagEl ? tagEl.value : '';
|
||||
|
||||
// Identity surface only — registry derives the rest.
|
||||
delete node.supplier;
|
||||
delete node.category;
|
||||
delete node.assetType;
|
||||
|
||||
if (!node.model) errors.push('Model is required.');
|
||||
if (!node.unit) errors.push('Unit is required.');
|
||||
|
||||
errors.forEach((msg) => RED.notify(msg, 'error'));
|
||||
|
||||
const saved = fields.reduce((acc, field) => {
|
||||
acc[field] = node[field];
|
||||
return acc;
|
||||
}, {});
|
||||
const saved = { model: node.model, unit: node.unit, assetTagNumber: node.assetTagNumber };
|
||||
if (node.modelMetadata && typeof node.modelMetadata.id !== 'undefined') {
|
||||
saved.modelId = node.modelMetadata.id;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user