python-materialsdb 0.0.2__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- materialsdb/__init__.py +3 -1
- materialsdb/cache.py +5 -6
- materialsdb/classes.py +14 -6
- materialsdb/config.py +3 -6
- materialsdb/construction.py +441 -0
- materialsdb/gui/__init__.py +0 -0
- materialsdb/gui/__main__.py +32 -0
- materialsdb/gui/discovery.py +28 -0
- materialsdb/gui/listener.py +151 -0
- materialsdb/gui/server.py +730 -0
- materialsdb/gui/static/app-constructions.js +639 -0
- materialsdb/gui/static/app.js +451 -0
- materialsdb/gui/static/constructions.html +107 -0
- materialsdb/gui/static/index.html +56 -0
- materialsdb/gui/static/picker-core.js +111 -0
- materialsdb/ifc/material_builder.py +295 -0
- materialsdb/ifc/material_psets.json +594 -580
- materialsdb/ifc/project_library.py +77 -212
- materialsdb/query.py +24 -0
- materialsdb/schema/MaterialsDBIndex100.xsd +60 -0
- materialsdb/schema/materialsdb102.xsd +820 -0
- materialsdb/schema/materialsdb103.xsd +938 -0
- materialsdb/serialiser.py +45 -18
- materialsdb/store.py +293 -0
- materialsdb/summary.py +83 -0
- materialsdb/utils.py +15 -21
- python_materialsdb-0.2.0.dist-info/METADATA +170 -0
- python_materialsdb-0.2.0.dist-info/RECORD +33 -0
- {python_materialsdb-0.0.2.dist-info → python_materialsdb-0.2.0.dist-info}/WHEEL +1 -1
- python_materialsdb-0.2.0.dist-info/entry_points.txt +2 -0
- python_materialsdb-0.0.2.dist-info/METADATA +0 -68
- python_materialsdb-0.0.2.dist-info/RECORD +0 -14
- {python_materialsdb-0.0.2.dist-info → python_materialsdb-0.2.0.dist-info/licenses}/LICENSE.md +0 -0
- {python_materialsdb-0.0.2.dist-info → python_materialsdb-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
const TOKEN = window.MATERIALSDB_TOKEN;
|
|
2
|
+
const $ = (id) => document.getElementById(id);
|
|
3
|
+
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) =>
|
|
4
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch]));
|
|
5
|
+
|
|
6
|
+
let layers = []; // [{material_id, thickness_m, display_name?, lambda_value?, choices_mm?, anyThickness?}]
|
|
7
|
+
let selectedRow = -1;
|
|
8
|
+
let lastResult = null;
|
|
9
|
+
let currentPreset = "ISO6946";
|
|
10
|
+
|
|
11
|
+
async function api(path, options = {}) {
|
|
12
|
+
const response = await fetch(path, {
|
|
13
|
+
...options,
|
|
14
|
+
headers: { "Content-Type": "application/json", "X-MaterialsDB-Token": TOKEN },
|
|
15
|
+
});
|
|
16
|
+
const type = response.headers.get("Content-Type") || "";
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
const message = type.includes("json") ? (await response.json()).error : response.statusText;
|
|
19
|
+
throw new Error(message);
|
|
20
|
+
}
|
|
21
|
+
return type.includes("json") ? response.json() : response.blob();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fmt(range, digits = 3) {
|
|
25
|
+
if (range === null || range === undefined) return "";
|
|
26
|
+
return Array.isArray(range)
|
|
27
|
+
? `${Number(range[0]).toFixed(digits)} - ${Number(range[1]).toFixed(digits)}`
|
|
28
|
+
: Number(range).toFixed(digits);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ---------- thickness cell: free input + manufacturer dropdown + warning ---------- */
|
|
32
|
+
|
|
33
|
+
function thicknessCellHtml(layer, index) {
|
|
34
|
+
const mm = Math.round(layer.thickness_m * 1000 * 1000) / 1000;
|
|
35
|
+
const choices = layer.material_id ? layer.choices_mm || [] : [];
|
|
36
|
+
let control;
|
|
37
|
+
if (choices.length > 1) {
|
|
38
|
+
const options = choices.map((c) =>
|
|
39
|
+
`<option value="${c}"${Math.abs(c - mm) < 1e-6 ? " selected" : ""}>${c} mm</option>`).join("");
|
|
40
|
+
const customSelected = !choices.some((c) => Math.abs(c - mm) < 1e-6);
|
|
41
|
+
control = `<select data-role="choice" data-index="${index}" style="max-width:7rem">
|
|
42
|
+
${customSelected ? `<option value="" selected>custom</option>` : ""}${options}</select>
|
|
43
|
+
<input type="text" inputmode="decimal" data-role="thick" data-index="${index}"
|
|
44
|
+
value="${mm}" style="width:4.5rem" title="thickness in mm">`;
|
|
45
|
+
} else {
|
|
46
|
+
control = `<input type="text" inputmode="decimal" data-role="thick" data-index="${index}"
|
|
47
|
+
value="${mm}" style="width:4.5rem" title="thickness in mm">`;
|
|
48
|
+
}
|
|
49
|
+
const warned = choices.length > 0 && !layer.anyThickness &&
|
|
50
|
+
!choices.some((c) => Math.abs(c - mm) < 1e-6);
|
|
51
|
+
return `${control} mm` +
|
|
52
|
+
(warned ? ` <span class="warn" title="the manufacturer does not offer this thickness">\u26a0</span>` : "");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function bindThicknessControls() {
|
|
56
|
+
$("layers").querySelectorAll("select[data-role=choice]").forEach((select) => {
|
|
57
|
+
select.addEventListener("change", () => {
|
|
58
|
+
const index = Number(select.dataset.index);
|
|
59
|
+
if (select.value !== "") {
|
|
60
|
+
layers[index].thickness_m = Number(select.value) / 1000;
|
|
61
|
+
renderLayers();
|
|
62
|
+
refreshU();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
$("layers").querySelectorAll("input[data-role=thick]").forEach((input) => {
|
|
67
|
+
input.addEventListener("change", () => {
|
|
68
|
+
const index = Number(input.dataset.index);
|
|
69
|
+
const mm = Number(input.value.replace(",", "."));
|
|
70
|
+
if (!mm || mm <= 0) { setStatus("thickness must be > 0"); input.focus(); return; }
|
|
71
|
+
layers[index].thickness_m = mm / 1000;
|
|
72
|
+
renderLayers();
|
|
73
|
+
refreshU();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function fetchLayerChoices(materialId) {
|
|
79
|
+
const detail = await api(`/api/materials/${encodeURIComponent(materialId)}`);
|
|
80
|
+
const raw = (detail.layers || []).map((l) => l.thick).filter((t) => t !== null && t !== undefined);
|
|
81
|
+
const anyThickness = raw.some((t) => Number(t) === 0);
|
|
82
|
+
const choices = [...new Set(raw.filter((t) => Number(t) > 0).map(Number))].sort((a, b) => a - b);
|
|
83
|
+
// category / own color for preview coloring
|
|
84
|
+
const category = detail.category || detail.group || "Others";
|
|
85
|
+
const ownColor = detail.color ?? detail.information?.color ?? null;
|
|
86
|
+
return { choices_mm: choices, anyThickness, category, ownColor, display_name: detail.display_name || detail.names?.[""] || "" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ---------- scaled stack preview + Rsi/Rse boundaries ---------- */
|
|
90
|
+
|
|
91
|
+
function renderPreview() {
|
|
92
|
+
const drawer = $("preview");
|
|
93
|
+
if (!drawer) return;
|
|
94
|
+
const labels = i18nLabels();
|
|
95
|
+
const total = layers.reduce((sum, l) => sum + l.thickness_m, 0);
|
|
96
|
+
let inner = "";
|
|
97
|
+
const rsiKnown = lastResult ? lastResult.rsi : null;
|
|
98
|
+
const rseKnown = lastResult ? lastResult.rse : null;
|
|
99
|
+
inner += layers.map((layer, index) => {
|
|
100
|
+
const flex = total ? (layer.thickness_m / total) * 100 : 0;
|
|
101
|
+
const name = layer.display_name || (layer.material_id ? layer.material_id.slice(0, 8) : (layer.placeholder?.name || "(model material)"));
|
|
102
|
+
const bg = (typeof PickerCore !== "undefined"
|
|
103
|
+
? PickerCore.categoryColorStyle(layer.category || "Others", layer.ownColor)
|
|
104
|
+
: "#eef");
|
|
105
|
+
return `<div class="stackbar-seg" style="flex:${flex};background:${bg};border-right:2px solid #fff" title="${esc(name)} \u2014 ${esc(fmt(layer.thickness_m * 1000, 0))} mm">` +
|
|
106
|
+
`${esc(String(index + 1))}<br>${fmt(layer.thickness_m * 1000, 0)} mm</div>`;
|
|
107
|
+
}).join("");
|
|
108
|
+
drawer.innerHTML =
|
|
109
|
+
`<span class="rs-chip">${esc(labels.exterior)} \u00b7 Rse = ${rseKnown ?? "?"}</span>` +
|
|
110
|
+
`<div class="stackbar">${inner || "<i style='color:#888'>no layers</i>"}</div>` +
|
|
111
|
+
`<span class="rs-chip">${esc(labels.interior)} \u00b7 Rsi = ${rsiKnown ?? "?"}</span>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function i18nLabels() {
|
|
115
|
+
const lang = (document.documentElement.lang || "en").slice(0, 2);
|
|
116
|
+
const dict = {
|
|
117
|
+
en: { interior: "Interior", exterior: "Exterior", formula: "How U is computed",
|
|
118
|
+
warnOffered: "manufacturer does not offer this thickness" },
|
|
119
|
+
fr: { interior: "Int\u00e9rieur", exterior: "Ext\u00e9rieur", formula: "Calcul du U",
|
|
120
|
+
warnOffered: "\u00e9paisseur non propos\u00e9e par le fabricant" },
|
|
121
|
+
de: { interior: "Innen", exterior: "Au\u00dfen", formula: "U-Berechnung",
|
|
122
|
+
warnOffered: "Dicke vom Hersteller nicht angeboten" },
|
|
123
|
+
};
|
|
124
|
+
return dict[lang] || dict.en;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ---------- table rendering ---------- */
|
|
128
|
+
|
|
129
|
+
function renderLayers() {
|
|
130
|
+
const tbody = $("layers");
|
|
131
|
+
const rows = layers.map((layer, index) => {
|
|
132
|
+
const selectedAttr = index === selectedRow ? ' style="background:#eef"' : "";
|
|
133
|
+
const catColor = typeof PickerCore !== "undefined"
|
|
134
|
+
? PickerCore.categoryColorStyle(layer.category || "Others", layer.ownColor)
|
|
135
|
+
: "#fff";
|
|
136
|
+
const isPlaceholder = !layer.material_id && layer.placeholder;
|
|
137
|
+
const nameCell = isPlaceholder
|
|
138
|
+
? `${esc(layer.placeholder.name || "(model material)")} <span style="color:#888;font-size:.8rem">model material</span>`
|
|
139
|
+
: esc(layer.display_name || layer.material_id);
|
|
140
|
+
return `<tr data-index="${index}"${selectedAttr} style="cursor:pointer;border-left:4px solid ${catColor}">` +
|
|
141
|
+
`<td>${index + 1}</td><td data-role="name">${nameCell}</td>` +
|
|
142
|
+
`<td>${thicknessCellHtml(layer, index)}</td>` +
|
|
143
|
+
`<td data-role="lambda">${esc(layer.lambda_value ?? (isPlaceholder ? layer.placeholder.lambda_value ?? "" : ""))}</td><td data-role="r">${esc(fmtR(layer, index))}</td><td></td></tr>`;
|
|
144
|
+
}).join("");
|
|
145
|
+
tbody.innerHTML = rsBoundaryRow("exterior") + rows + rsBoundaryRow("interior");
|
|
146
|
+
bindThicknessControls();
|
|
147
|
+
tbody.querySelectorAll("tr[data-index]").forEach((tr) => {
|
|
148
|
+
tr.addEventListener("click", (event) => {
|
|
149
|
+
if (event.target.tagName === "INPUT" || event.target.tagName === "SELECT") return;
|
|
150
|
+
selectedRow = Number(tr.dataset.index);
|
|
151
|
+
renderLayers();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function rsBoundaryRow(which) {
|
|
157
|
+
const presetName = $("preset").value;
|
|
158
|
+
const values = lastResult ? [lastResult.rsi, lastResult.rse] : null;
|
|
159
|
+
const label = i18nLabels()[which];
|
|
160
|
+
const value = which === "interior"
|
|
161
|
+
? (values ? values[0] : RESISTANCE_DISPLAY[presetName]?.rsi)
|
|
162
|
+
: (values ? values[1] : RESISTANCE_DISPLAY[presetName]?.rse);
|
|
163
|
+
return `<tr class="rsrow"><td colspan="2">${esc(label)}</td>` +
|
|
164
|
+
`<td colspan="3">R${which === "interior" ? "si" : "se"} = ${value ?? "?"} m\u00b2K/W</td><td></td></tr>`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const RESISTANCE_DISPLAY = {
|
|
168
|
+
ISO6946: { rsi: 0.13, rse: 0.04 },
|
|
169
|
+
SIA180: { rsi: 0.13, rse: 0.04 },
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function fmtR(layer, index) {
|
|
173
|
+
if (!lastResult) return "";
|
|
174
|
+
const c = lastResult.contributions.find((x) =>
|
|
175
|
+
x.layer_index != null ? x.layer_index === index : x.material_id === layer.material_id);
|
|
176
|
+
return c ? c.r.toFixed(3) : "";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderContributions() {
|
|
180
|
+
if (!lastResult) { $("u-display").textContent = "-"; $("contributions").innerHTML = ""; $("warnings").textContent = ""; }
|
|
181
|
+
else if (lastResult.u === null) {
|
|
182
|
+
$("u-display").textContent = "?";
|
|
183
|
+
$("warnings").textContent = "layers without lambda or invalid thickness excluded: " + lastResult.missing_lambda_ids.length;
|
|
184
|
+
} else {
|
|
185
|
+
$("u-display").textContent = lastResult.u.toFixed(3) + " W/m2K";
|
|
186
|
+
$("warnings").textContent = lastResult.missing_lambda_ids.length ? `warning: ${lastResult.missing_lambda_ids.length} layer(s) without lambda excluded` : "";
|
|
187
|
+
}
|
|
188
|
+
$("contributions").innerHTML = (lastResult ? lastResult.contributions : []).map((c) =>
|
|
189
|
+
`<dt>${esc(c.name || (c.material_id ? c.material_id.slice(0, 8) : "(model material)"))} \u2014 ${c.d_m.toFixed(3)} m</dt><dd>R = ${c.r.toFixed(3)}</dd>`).join("");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function refreshU() {
|
|
193
|
+
if (!layers.length) { lastResult = null; renderContributions(); renderPreview(); return; }
|
|
194
|
+
const body = {
|
|
195
|
+
construction: {
|
|
196
|
+
name: $("name").value || "draft",
|
|
197
|
+
design_usage: $("design-usage").value || null,
|
|
198
|
+
layers: layers.map((l) => ({
|
|
199
|
+
material_id: l.material_id,
|
|
200
|
+
thickness_m: l.thickness_m,
|
|
201
|
+
...(l.placeholder ? { placeholder: { name: l.placeholder.name, lambda_value: l.placeholder.lambda_value } } : {}),
|
|
202
|
+
})),
|
|
203
|
+
},
|
|
204
|
+
preset: $("preset").value,
|
|
205
|
+
};
|
|
206
|
+
currentPreset = $("preset").value;
|
|
207
|
+
try {
|
|
208
|
+
lastResult = await api("/api/u_value", { method: "POST", body: JSON.stringify(body) });
|
|
209
|
+
lastResult.contributions.forEach((c) => {
|
|
210
|
+
// contributions carry their original layer position; placeholder rows all
|
|
211
|
+
// share material_id null, so index matching is the only safe hydration
|
|
212
|
+
const rows = c.layer_index != null
|
|
213
|
+
? [layers[c.layer_index]].filter(Boolean)
|
|
214
|
+
: layers.filter((l) => l.material_id === c.material_id);
|
|
215
|
+
rows.forEach((row) => {
|
|
216
|
+
row.display_name = c.name || row.display_name;
|
|
217
|
+
row.lambda_value = c.lambda_value;
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
} catch (err) { setStatus(err.message); }
|
|
221
|
+
renderLayers();
|
|
222
|
+
renderContributions();
|
|
223
|
+
renderPreview();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ---------- layer choices after add ---------- */
|
|
227
|
+
|
|
228
|
+
async function attachLayerChoices(layer) {
|
|
229
|
+
try {
|
|
230
|
+
Object.assign(layer, await fetchLayerChoices(layer.material_id));
|
|
231
|
+
} catch { /* choices are optional */ }
|
|
232
|
+
renderLayers();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* ---------- saved + materialsdb lists ---------- */
|
|
236
|
+
|
|
237
|
+
async function initConfig() {
|
|
238
|
+
try {
|
|
239
|
+
const cfg = await api("/api/config");
|
|
240
|
+
const langEl = document.getElementById("lang");
|
|
241
|
+
const countryEl = document.getElementById("country");
|
|
242
|
+
if (langEl && cfg.lang) { langEl.value = cfg.lang; document.documentElement.lang = cfg.lang; }
|
|
243
|
+
if (countryEl && cfg.country) countryEl.value = cfg.country;
|
|
244
|
+
} catch {}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function loadList() {
|
|
248
|
+
const { constructions } = await api("/api/constructions");
|
|
249
|
+
$("saved-list").innerHTML = constructions.map((name) =>
|
|
250
|
+
`<li><a href="#" data-name="${esc(name)}">${esc(name)}</a></li>`).join("") ||
|
|
251
|
+
`<li style="color:#777;font-size:.8rem">none saved yet.<br>Create one with <b>new</b> + <b>add layer</b>, or duplicate a materialsdb construction below.</li>`;
|
|
252
|
+
$("saved-list").querySelectorAll("a").forEach((a) => a.addEventListener("click", async (event) => {
|
|
253
|
+
event.preventDefault();
|
|
254
|
+
const construction = await api(`/api/constructions/${encodeURIComponent(a.dataset.name)}`);
|
|
255
|
+
loadEditable(construction);
|
|
256
|
+
}));
|
|
257
|
+
await loadMaterialsdbConstructions();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let incoming = [];
|
|
261
|
+
|
|
262
|
+
/* --- collapsible panel sections (persisted; a new model push fronts the
|
|
263
|
+
* incoming section by collapsing the other two) --- */
|
|
264
|
+
const SECTION_KEYS = ["saved", "incoming", "mdb"];
|
|
265
|
+
let sectionState = { saved: true, incoming: true, mdb: true };
|
|
266
|
+
try {
|
|
267
|
+
Object.assign(sectionState, JSON.parse(localStorage.getItem("materialsdb.sections") || "{}"));
|
|
268
|
+
} catch {}
|
|
269
|
+
|
|
270
|
+
function applySections() {
|
|
271
|
+
for (const key of SECTION_KEYS) {
|
|
272
|
+
const container = $(`section-${key}`);
|
|
273
|
+
if (container) container.style.display = sectionState[key] ? "" : "none";
|
|
274
|
+
const chev = document.querySelector(`.section-toggle[data-section="${key}"] .chev`);
|
|
275
|
+
if (chev) chev.textContent = sectionState[key] ? "▾" : "▸";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function toggleSection(key) {
|
|
280
|
+
sectionState[key] = !sectionState[key];
|
|
281
|
+
try {
|
|
282
|
+
localStorage.setItem("materialsdb.sections", JSON.stringify(sectionState));
|
|
283
|
+
} catch {}
|
|
284
|
+
applySections();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
document.querySelectorAll(".section-toggle").forEach((header) =>
|
|
288
|
+
header.addEventListener("click", () => toggleSection(header.dataset.section)));
|
|
289
|
+
applySections();
|
|
290
|
+
|
|
291
|
+
async function refreshIncoming() {
|
|
292
|
+
try {
|
|
293
|
+
incoming = (await api("/api/composer/incoming")).incoming;
|
|
294
|
+
} catch {
|
|
295
|
+
incoming = [];
|
|
296
|
+
}
|
|
297
|
+
const grew = lastIncomingLength !== null && incoming.length > lastIncomingLength;
|
|
298
|
+
lastIncomingLength = incoming.length;
|
|
299
|
+
if (grew) {
|
|
300
|
+
sectionState.saved = false;
|
|
301
|
+
sectionState.mdb = false;
|
|
302
|
+
sectionState.incoming = true;
|
|
303
|
+
try {
|
|
304
|
+
localStorage.setItem("materialsdb.sections", JSON.stringify(sectionState));
|
|
305
|
+
} catch {}
|
|
306
|
+
applySections();
|
|
307
|
+
}
|
|
308
|
+
const count = $("incoming-count");
|
|
309
|
+
if (count) count.textContent = incoming.length ? `(${incoming.length})` : "";
|
|
310
|
+
const box = $("incoming-list");
|
|
311
|
+
box.innerHTML = incoming.length
|
|
312
|
+
? incoming.map((construction, index) =>
|
|
313
|
+
`<li><a href="#" data-index="${index}">${esc(construction.name)}</a> ` +
|
|
314
|
+
`<span style="color:#888">· ${construction.layers.length} layer(s)` +
|
|
315
|
+
`${construction.layers.some((l) => l.placeholder) ? " · has model materials" : ""}</span></li>`).join("")
|
|
316
|
+
: `<li style="color:#777;font-size:.8rem">nothing incoming from the model</li>`;
|
|
317
|
+
box.querySelectorAll("a").forEach((a) => a.addEventListener("click", async (event) => {
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
const result = await api("/api/composer/incoming/consume", {
|
|
320
|
+
method: "POST",
|
|
321
|
+
body: JSON.stringify({ index: Number(a.dataset.index) }),
|
|
322
|
+
});
|
|
323
|
+
loadEditable(result.construction);
|
|
324
|
+
setStatus(`editing copy of ${result.construction.name} — save to keep, send to: to push back`);
|
|
325
|
+
refreshIncoming();
|
|
326
|
+
}));
|
|
327
|
+
if (grew) {
|
|
328
|
+
const header = document.querySelector('.section-toggle[data-section="incoming"]');
|
|
329
|
+
if (header && header.scrollIntoView) header.scrollIntoView({ block: "nearest" });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let lastIncomingLength = null;
|
|
334
|
+
setInterval(refreshIncoming, 2000);
|
|
335
|
+
refreshIncoming();
|
|
336
|
+
|
|
337
|
+
let materialsdbConstructions = [];
|
|
338
|
+
|
|
339
|
+
async function loadMaterialsdbConstructions() {
|
|
340
|
+
const { materials } = await api("/api/materials?type=construction");
|
|
341
|
+
materialsdbConstructions = materials;
|
|
342
|
+
|
|
343
|
+
const companySelect = $("mdb-company");
|
|
344
|
+
const companies = [...new Set(materials.map((m) => m.company))].sort();
|
|
345
|
+
companySelect.innerHTML = `<option value="">all companies</option>` +
|
|
346
|
+
companies.map((c) => `<option value="${esc(c)}">${esc(c)}</option>`).join("");
|
|
347
|
+
|
|
348
|
+
const renderMdbList = () => {
|
|
349
|
+
const needle = $("mdb-search").value.trim().toLowerCase();
|
|
350
|
+
const company = companySelect.value;
|
|
351
|
+
const visible = materialsdbConstructions.filter((m) =>
|
|
352
|
+
(!company || m.company === company) &&
|
|
353
|
+
(!needle || m.display_name.toLowerCase().includes(needle)));
|
|
354
|
+
$("mdb-list").innerHTML = visible.map((m) =>
|
|
355
|
+
`<li><a href="#" data-id="${esc(m.id)}">${esc(m.display_name)}</a> ` +
|
|
356
|
+
`<span style="color:#888">· ${esc(m.company)}</span></li>`).join("") ||
|
|
357
|
+
"<li><i>no match</i></li>";
|
|
358
|
+
$("mdb-list").querySelectorAll("a").forEach((a) => a.addEventListener("click", async (event) => {
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
showLegacy(a.dataset.id);
|
|
361
|
+
}));
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
$("mdb-search").addEventListener("input", () => clearTimeout(setTimeout(renderMdbList, 200)) || undefined);
|
|
365
|
+
let searchTimer;
|
|
366
|
+
$("mdb-search").addEventListener("input", () => { clearTimeout(searchTimer); searchTimer = setTimeout(renderMdbList, 200); });
|
|
367
|
+
companySelect.addEventListener("change", renderMdbList);
|
|
368
|
+
renderMdbList();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let legacyState = null; // {payload, variantIndex}
|
|
372
|
+
|
|
373
|
+
async function showLegacy(materialId) {
|
|
374
|
+
const payload = await api(`/api/constructions/legacy/${encodeURIComponent(materialId)}`);
|
|
375
|
+
legacyState = { payload, materialId, variantIndex: 0 };
|
|
376
|
+
$("composer-ui").style.display = "none";
|
|
377
|
+
const view = $("readonly-ui");
|
|
378
|
+
view.style.display = "block";
|
|
379
|
+
const entry = materialsdbConstructions.find((m) => m.id === materialId);
|
|
380
|
+
const chips = payload.variants.map((v, index) =>
|
|
381
|
+
`<button class="variant-chip" data-variant="${index}" style="background:${index === 0 ? "#eef" : ""}">` +
|
|
382
|
+
`variant ${index + 1}${v.layers.length ? ` (${v.layers.length} layers)` : ""}</button>`).join("");
|
|
383
|
+
view.innerHTML =
|
|
384
|
+
`<div><button id="back-composer">← back to composer</button>` +
|
|
385
|
+
` <h3 style="display:inline;margin-left:.5rem">${esc(entry ? entry.display_name : materialId)}` +
|
|
386
|
+
` <span class="warn" title="this content is a non-spec vendor string; display is best-effort">non-spec vendor data</span></h3></div>` +
|
|
387
|
+
`<div style="margin:.3rem 0">consref: <b>${esc(payload.consref) || "-"}</b> · designusage: <b>${esc(payload.designusage) || "-"}</b></div>` +
|
|
388
|
+
`<div id="variant-chips">${chips}</div>` +
|
|
389
|
+
`<div id="variant-body" style="margin-top:.4rem"></div>` +
|
|
390
|
+
`<details style="margin-top:.4rem"><summary>raw vendor string</summary>` +
|
|
391
|
+
`<pre style="white-space:pre-wrap">${esc(payload.raw)}</pre></details>` +
|
|
392
|
+
`<button id="copy-legacy" style="margin-top:.5rem">create editable copy</button>`;
|
|
393
|
+
view.querySelector("#back-composer").onclick = hideLegacy;
|
|
394
|
+
view.querySelector("#variant-chips").addEventListener("click", (event) => {
|
|
395
|
+
if (event.target.dataset.variant === undefined) return;
|
|
396
|
+
legacyState.variantIndex = Number(event.target.dataset.variant);
|
|
397
|
+
view.querySelectorAll(".variant-chip").forEach((chip) =>
|
|
398
|
+
chip.style.background = chip.dataset.variant === String(legacyState.variantIndex) ? "#eef" : "");
|
|
399
|
+
renderLegacyVariant();
|
|
400
|
+
});
|
|
401
|
+
view.querySelector("#copy-legacy").onclick = () => {
|
|
402
|
+
copyLegacyToComposer(materialId, payload);
|
|
403
|
+
hideLegacy();
|
|
404
|
+
};
|
|
405
|
+
renderLegacyVariant();
|
|
406
|
+
setStatus("materialsdb construction loaded (read-only)");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function hideLegacy() {
|
|
410
|
+
$("readonly-ui").style.display = "none";
|
|
411
|
+
$("composer-ui").style.display = "flex";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function renderLegacyVariant() {
|
|
415
|
+
const { payload, variantIndex } = legacyState;
|
|
416
|
+
const entry = materialsdbConstructions.find((m) => m.id === legacyState.materialId);
|
|
417
|
+
const variant = payload.variants[variantIndex];
|
|
418
|
+
const rows = variant.layers.map((l, li) => {
|
|
419
|
+
const flag = l.resolvable ? "" : ' <span class="warn" title="guid not found in the local index">⚠ unresolved</span>';
|
|
420
|
+
return `<tr><td>${li + 1}</td><td>${esc(l.name || l.guid.slice(0, 8))}${flag}</td>` +
|
|
421
|
+
`<td>${fmt(l.thickness_m * 1000, 0)} mm</td><td>${esc(fmt(l.lambda_value))}</td></tr>`;
|
|
422
|
+
}).join("");
|
|
423
|
+
const uLine = variant.u !== null && variant.u !== undefined ? `${variant.u.toFixed(3)} W/m²K` : "n/a";
|
|
424
|
+
$("variant-body").innerHTML =
|
|
425
|
+
`<table><thead><tr><th>#</th><th>material</th><th>thickness</th><th>λ W/mK</th></tr></thead><tbody>${rows}</tbody></table>` +
|
|
426
|
+
`<p style="margin-top:.4rem"><b>U = ${uLine}</b>${variant.unresolved_count ? ` · ${variant.unresolved_count} unresolvable guid(s) excluded from the sum` : ""}</p>` +
|
|
427
|
+
(entry && entry.company ? `<p style="color:#777">producer: ${esc(entry.company)}</p>` : "");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function copyLegacyToComposer(materialId, payload) {
|
|
431
|
+
const variant = payload.variants.find((v) => v.layers.every((l) => l.resolvable)) || payload.variants[0];
|
|
432
|
+
layers = variant.layers
|
|
433
|
+
.filter((l) => l.resolvable)
|
|
434
|
+
.map((l) => ({ material_id: l.guid, thickness_m: l.thickness_m }));
|
|
435
|
+
selectedRow = -1;
|
|
436
|
+
$("name").value = entry_name(materialId) || materialId.slice(0, 8);
|
|
437
|
+
const usageMap = { consDesignForWall: "consDesignForWall", consDesignForRoof: "consDesignForRoof", consDesignForFloor: "consDesignForFloor" };
|
|
438
|
+
$("design-usage").value = usageMap[payload.designusage] || "";
|
|
439
|
+
await Promise.all(layers.map(attachLayerChoices));
|
|
440
|
+
await refreshU();
|
|
441
|
+
setStatus("editable copy created \u2014 adjust and save under your own name");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function entry_name(materialId) {
|
|
445
|
+
const entry = materialsdbConstructions.find((m) => m.id === materialId);
|
|
446
|
+
return entry ? entry.display_name : null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function loadEditable(construction) {
|
|
450
|
+
$("name").value = construction.name;
|
|
451
|
+
$("design-usage").value = construction.design_usage || "";
|
|
452
|
+
layers = construction.layers.map((l) => ({ ...l }));
|
|
453
|
+
selectedRow = -1; lastResult = null;
|
|
454
|
+
layerChoicesInitAll();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function layerChoicesInitAll() {
|
|
458
|
+
await Promise.all(layers.map(async (layer) => {
|
|
459
|
+
if (!layer.material_id) return;
|
|
460
|
+
try {
|
|
461
|
+
Object.assign(layer, await fetchLayerChoices(layer.material_id));
|
|
462
|
+
} catch {
|
|
463
|
+
/* choices optional; id may be absent from the local store */
|
|
464
|
+
}
|
|
465
|
+
}));
|
|
466
|
+
await refreshU();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function setStatus(text) { $("status").textContent = text; return text; }
|
|
470
|
+
|
|
471
|
+
$("new").onclick = () => { layers = []; selectedRow = -1; lastResult = null; $("name").value = ""; renderLayers(); renderContributions(); renderPreview(); };
|
|
472
|
+
$("delete").onclick = async () => {
|
|
473
|
+
const name = $("name").value; if (!name) return setStatus("nothing loaded");
|
|
474
|
+
await api(`/api/constructions/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
475
|
+
setStatus(`deleted ${name}`); loadList(); $("new").onclick();
|
|
476
|
+
};
|
|
477
|
+
$("add-layer").onclick = () => openChooser(addLayerFromChooser);
|
|
478
|
+
|
|
479
|
+
async function addLayerFromChooser(picked) {
|
|
480
|
+
const items = Array.isArray(picked) ? picked : [{ material_id: picked }];
|
|
481
|
+
let added = 0;
|
|
482
|
+
for (const it of items) {
|
|
483
|
+
const thicknessHint = it.thickness_m;
|
|
484
|
+
if (thicknessHint == null) {
|
|
485
|
+
if (layers.some((l) => l.material_id === it.material_id)) {
|
|
486
|
+
// whole-material already present — skip duplicate
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
if (layers.some((l) => l.material_id === it.material_id && Math.abs(l.thickness_m - thicknessHint) < 1e-9)) continue;
|
|
491
|
+
}
|
|
492
|
+
const layer = { material_id: it.material_id, thickness_m: thicknessHint != null ? thicknessHint : 0.2 };
|
|
493
|
+
layers.push(layer);
|
|
494
|
+
selectedRow = layers.length - 1;
|
|
495
|
+
added++;
|
|
496
|
+
renderLayers();
|
|
497
|
+
await attachLayerChoices(layer);
|
|
498
|
+
if (thicknessHint == null && !layer.anyThickness && layer.choices_mm && layer.choices_mm.length) {
|
|
499
|
+
layer.thickness_m = layer.choices_mm[0] / 1000;
|
|
500
|
+
}
|
|
501
|
+
renderLayers();
|
|
502
|
+
}
|
|
503
|
+
if (added) await refreshU();
|
|
504
|
+
else if (items.length) setStatus("selected materials already in construction");
|
|
505
|
+
}
|
|
506
|
+
document.querySelector("[data-move=up]").onclick = () => {
|
|
507
|
+
if (selectedRow > 0) { [layers[selectedRow - 1], layers[selectedRow]] = [layers[selectedRow], layers[selectedRow - 1]]; selectedRow -= 1; renderLayers(); refreshU(); }
|
|
508
|
+
};
|
|
509
|
+
document.querySelector("[data-move=down]").onclick = () => {
|
|
510
|
+
if (selectedRow > -1 && selectedRow < layers.length - 1) { [layers[selectedRow + 1], layers[selectedRow]] = [layers[selectedRow], layers[selectedRow + 1]]; selectedRow += 1; renderLayers(); refreshU(); }
|
|
511
|
+
};
|
|
512
|
+
document.querySelector("[data-action=remove]").onclick = () => {
|
|
513
|
+
if (selectedRow > -1) { layers.splice(selectedRow, 1); selectedRow = -1; renderLayers(); refreshU(); }
|
|
514
|
+
};
|
|
515
|
+
$("reverse").onclick = () => {
|
|
516
|
+
if (!layers.length) return;
|
|
517
|
+
layers.reverse();
|
|
518
|
+
if (selectedRow !== -1) selectedRow = layers.length - 1 - selectedRow;
|
|
519
|
+
renderLayers(); refreshU();
|
|
520
|
+
};
|
|
521
|
+
document.getElementById("settings-tab").addEventListener("click", (e) => {
|
|
522
|
+
e.preventDefault();
|
|
523
|
+
const p = document.getElementById("settings-panel");
|
|
524
|
+
p.style.display = p.style.display === "none" ? "block" : "none";
|
|
525
|
+
});
|
|
526
|
+
$("preset").onchange = () => { refreshU(); };
|
|
527
|
+
$("design-usage").onchange = refreshU;
|
|
528
|
+
$("save").onclick = async () => {
|
|
529
|
+
const name = $("name").value.trim(); if (!name) return setStatus("name required");
|
|
530
|
+
if (!layers.length) return setStatus("add at least one layer");
|
|
531
|
+
await api(`/api/constructions/${encodeURIComponent(name)}`, { method: "POST",
|
|
532
|
+
body: JSON.stringify({ name, design_usage: $("design-usage").value || null,
|
|
533
|
+
layers: layers.map((l) => ({
|
|
534
|
+
material_id: l.material_id,
|
|
535
|
+
thickness_m: l.thickness_m,
|
|
536
|
+
...(l.placeholder ? { placeholder: { name: l.placeholder.name, lambda_value: l.placeholder.lambda_value } } : {}),
|
|
537
|
+
})) }) });
|
|
538
|
+
setStatus(`saved ${name} (overwrites same-name construction)`); loadList();
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const bonsaiTarget = PickerCore.startTargetPoll(api, $("bonsai-target"), $("send-bonsai"));
|
|
542
|
+
|
|
543
|
+
$("send-bonsai").onclick = async () => {
|
|
544
|
+
if (!layers.length) return setStatus("add at least one layer");
|
|
545
|
+
if (!$("name").value.trim()) return setStatus("name the construction before sending");
|
|
546
|
+
const client_id = bonsaiTarget.current();
|
|
547
|
+
if (!client_id) return setStatus("no Bonsai listener connected");
|
|
548
|
+
const result = await api("/api/listener/send", {
|
|
549
|
+
method: "POST",
|
|
550
|
+
body: JSON.stringify({
|
|
551
|
+
client_id,
|
|
552
|
+
action: "add_construction",
|
|
553
|
+
construction: {
|
|
554
|
+
name: $("name").value.trim(),
|
|
555
|
+
design_usage: $("design-usage").value || null,
|
|
556
|
+
layers: layers.map((l) => ({
|
|
557
|
+
material_id: l.material_id,
|
|
558
|
+
thickness_m: l.thickness_m,
|
|
559
|
+
...(l.placeholder ? { placeholder: { name: l.placeholder.name, lambda_value: l.placeholder.lambda_value } } : {}),
|
|
560
|
+
})),
|
|
561
|
+
},
|
|
562
|
+
}),
|
|
563
|
+
});
|
|
564
|
+
PickerCore.flash(setStatus, () => $("status").textContent, `sent to ${bonsaiTarget.label() || "?"}: ${result.summary}`);
|
|
565
|
+
};
|
|
566
|
+
$("export-ifc").onclick = async () => {
|
|
567
|
+
try {
|
|
568
|
+
const name = $("name").value.trim(); if (!name || !layers.length) return setStatus("nothing to export");
|
|
569
|
+
const blob = await api("/api/export-construction", { method: "POST",
|
|
570
|
+
body: JSON.stringify({ construction: { name, design_usage: $("design-usage").value || null,
|
|
571
|
+
layers: layers.map((l) => ({
|
|
572
|
+
material_id: l.material_id,
|
|
573
|
+
thickness_m: l.thickness_m,
|
|
574
|
+
...(l.placeholder ? { placeholder: { name: l.placeholder.name, lambda_value: l.placeholder.lambda_value } } : {}),
|
|
575
|
+
})) } }) });
|
|
576
|
+
const url = URL.createObjectURL(blob);
|
|
577
|
+
const link = document.createElement("a"); link.href = url; link.download = `${name}.ifc`; link.click();
|
|
578
|
+
URL.revokeObjectURL(url);
|
|
579
|
+
} catch (err) {
|
|
580
|
+
setStatus(err.message);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
$("append-session").onclick = async () => {
|
|
584
|
+
try {
|
|
585
|
+
if (!$("name").value || !layers.length) return setStatus("nothing to append");
|
|
586
|
+
const result = await api("/api/append-construction", { method: "POST",
|
|
587
|
+
body: JSON.stringify({ construction: { name: $("name").value,
|
|
588
|
+
design_usage: $("design-usage").value || null,
|
|
589
|
+
layers: layers.map((l) => ({
|
|
590
|
+
material_id: l.material_id,
|
|
591
|
+
thickness_m: l.thickness_m,
|
|
592
|
+
...(l.placeholder ? { placeholder: { name: l.placeholder.name, lambda_value: l.placeholder.lambda_value } } : {}),
|
|
593
|
+
})) } }),
|
|
594
|
+
});
|
|
595
|
+
setStatus(`appended layer set (${result.layer_count} layers)`);
|
|
596
|
+
} catch (err) {
|
|
597
|
+
setStatus(err.message);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
initConfig().then(() => loadList().then(() => { renderLayers(); renderContributions(); renderPreview(); }));
|
|
601
|
+
document.getElementById("lang")?.addEventListener("change", async () => {
|
|
602
|
+
await api("/api/config", { method: "POST", body: JSON.stringify({ lang: $("lang").value }) });
|
|
603
|
+
document.documentElement.lang = $("lang").value;
|
|
604
|
+
});
|
|
605
|
+
document.getElementById("country")?.addEventListener("change", async () => {
|
|
606
|
+
await api("/api/config", { method: "POST", body: JSON.stringify({ country: $("country").value }) });
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
/* --- chooser: embedded full picker via iframe --- */
|
|
610
|
+
function openChooser(onPick) {
|
|
611
|
+
const overlay = document.createElement("div");
|
|
612
|
+
overlay.id = "chooser";
|
|
613
|
+
overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;z-index:1000";
|
|
614
|
+
overlay.innerHTML = `<div style="background:#fff;width:85%;height:85%;display:flex;flex-direction:column;border-radius:6px;overflow:hidden">` +
|
|
615
|
+
`<div style="display:flex;justify-content:space-between;align-items:center;padding:.4rem .6rem;border-bottom:1px solid #ccc"><b>Select materials</b>` +
|
|
616
|
+
`<div><button id="chooser-add" disabled>Add selected (0)</button><button id="chooser-close" style="margin-left:.4rem">close</button></div></div>` +
|
|
617
|
+
`<iframe src="/?embed=1" style="flex:1;border:none;width:100%"></iframe></div>`;
|
|
618
|
+
document.body.appendChild(overlay);
|
|
619
|
+
const close = () => { window.removeEventListener("message", onMessage); overlay.remove(); };
|
|
620
|
+
overlay.querySelector("#chooser-close").onclick = close;
|
|
621
|
+
overlay.addEventListener("click", (event) => { if (event.target === overlay) close(); });
|
|
622
|
+
|
|
623
|
+
let pending = [];
|
|
624
|
+
function onMessage(event) {
|
|
625
|
+
if (event.data && event.data.type === "picker-selection") {
|
|
626
|
+
pending = event.data.items || [];
|
|
627
|
+
const btn = overlay.querySelector("#chooser-add");
|
|
628
|
+
btn.textContent = `Add selected (${pending.length})`;
|
|
629
|
+
btn.disabled = pending.length === 0;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
window.addEventListener("message", onMessage);
|
|
633
|
+
overlay.querySelector("#chooser-add").onclick = () => {
|
|
634
|
+
if (!pending.length) return;
|
|
635
|
+
const copy = [...pending];
|
|
636
|
+
close();
|
|
637
|
+
onPick(copy);
|
|
638
|
+
};
|
|
639
|
+
}
|