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,451 @@
|
|
|
1
|
+
const TOKEN = window.MATERIALSDB_TOKEN;
|
|
2
|
+
const $ = (id) => document.getElementById(id);
|
|
3
|
+
const selected = new Set();
|
|
4
|
+
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) =>
|
|
5
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch]));
|
|
6
|
+
|
|
7
|
+
const USAGE_LABELS = {
|
|
8
|
+
en: { wall: "Wall", roof: "Roof", floor: "Floor", door: "Door" },
|
|
9
|
+
fr: { wall: "Mur", roof: "Toit", floor: "Plancher", door: "Porte" },
|
|
10
|
+
de: { wall: "Wand", roof: "Dach", floor: "Boden", door: "T\u00fcr" },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const isEmbed = new URLSearchParams(location.search).has("embed");
|
|
14
|
+
let allMaterials = [];
|
|
15
|
+
let lang = "en";
|
|
16
|
+
if (isEmbed) {
|
|
17
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
18
|
+
document.querySelector(".top-tabs")?.remove();
|
|
19
|
+
const sp = document.getElementById("settings-panel");
|
|
20
|
+
if (sp) sp.remove();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
let sortKey = "company";
|
|
24
|
+
let sortAsc = true;
|
|
25
|
+
const facetSelections = { company: new Set(), category: new Set(), type: new Set(), usage: new Set() };
|
|
26
|
+
const detailCache = new Map();
|
|
27
|
+
const expanded = new Set();
|
|
28
|
+
const layerSelections = new Map(); // materialId -> Set(sourceLayerGuid); absent = whole material
|
|
29
|
+
let lastSelectedId = null;
|
|
30
|
+
if (isEmbed) {
|
|
31
|
+
const _buildItems = () => {
|
|
32
|
+
const items = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
for (const id of selected) {
|
|
35
|
+
seen.add(id);
|
|
36
|
+
const layerSet = layerSelections.get(id);
|
|
37
|
+
if (layerSet && layerSet.size) {
|
|
38
|
+
for (const lguid of layerSet) {
|
|
39
|
+
const det = detailCache.get(id);
|
|
40
|
+
const layerDetail = det?.layers?.find((l) => l.id === lguid);
|
|
41
|
+
const thick = layerDetail ? Number(layerDetail.thick) : 0;
|
|
42
|
+
items.push({ material_id: id, thickness_m: thick ? thick / 1000 : 0.2 });
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
items.push({ material_id: id });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const [id, layerSet] of layerSelections) {
|
|
49
|
+
if (seen.has(id) || !layerSet.size) continue;
|
|
50
|
+
for (const lguid of layerSet) {
|
|
51
|
+
const det = detailCache.get(id);
|
|
52
|
+
const layerDetail = det?.layers?.find((l) => l.id === lguid);
|
|
53
|
+
const thick = layerDetail ? Number(layerDetail.thick) : 0;
|
|
54
|
+
items.push({ material_id: id, thickness_m: thick ? thick / 1000 : 0.2 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return items;
|
|
58
|
+
};
|
|
59
|
+
const _notifyParent = () => {
|
|
60
|
+
try { parent.postMessage({ type: "picker-selection", items: _buildItems() }, "*"); } catch {}
|
|
61
|
+
};
|
|
62
|
+
const _origAdd = selected.add.bind(selected); selected.add = (v) => { const r = _origAdd(v); _notifyParent(); return r; };
|
|
63
|
+
const _origDelete = selected.delete.bind(selected); selected.delete = (v) => { const r = _origDelete(v); _notifyParent(); return r; };
|
|
64
|
+
const _origClear = selected.clear.bind(selected); selected.clear = () => { const r = _origClear(); _notifyParent(); return r; };
|
|
65
|
+
const _origLSet = layerSelections.set.bind(layerSelections); layerSelections.set = (k, v) => { const r = _origLSet(k, v); _notifyParent(); return r; };
|
|
66
|
+
const _origLDel = layerSelections.delete.bind(layerSelections); layerSelections.delete = (k) => { const r = _origLDel(k); _notifyParent(); return r; };
|
|
67
|
+
const _origLClear = layerSelections.clear.bind(layerSelections); layerSelections.clear = () => { const r = _origLClear(); _notifyParent(); return r; };
|
|
68
|
+
}
|
|
69
|
+
const COLUMNS = [
|
|
70
|
+
{ key: "display_name", label: "name" },
|
|
71
|
+
{ key: "company", label: "company", facet: true },
|
|
72
|
+
{ key: "category", label: "category", facet: true },
|
|
73
|
+
{ key: "type", label: "type", facet: true },
|
|
74
|
+
{ key: "lambda", label: "\u03bb W/mK" },
|
|
75
|
+
{ key: "thick", label: "thick mm" },
|
|
76
|
+
{ key: "usage", label: "usage", facet: true },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
async function api(path, options = {}) {
|
|
80
|
+
const response = await fetch(path, {
|
|
81
|
+
...options,
|
|
82
|
+
headers: { "Content-Type": "application/json", "X-MaterialsDB-Token": TOKEN },
|
|
83
|
+
});
|
|
84
|
+
const type = response.headers.get("Content-Type") || "";
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const message = type.includes("json") ? (await response.json()).error : response.statusText;
|
|
87
|
+
throw new Error(message);
|
|
88
|
+
}
|
|
89
|
+
return type.includes("json") ? response.json() : response.blob();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function fmt(range, digits = 3) {
|
|
93
|
+
if (range === null || range === undefined) return "";
|
|
94
|
+
return Array.isArray(range)
|
|
95
|
+
? `${Number(range[0]).toFixed(digits)} - ${Number(range[1]).toFixed(digits)}`
|
|
96
|
+
: Number(range).toFixed(digits);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function usageWords(m) {
|
|
100
|
+
const labels = USAGE_LABELS[lang] || USAGE_LABELS.en;
|
|
101
|
+
return Object.entries(m.usage).filter(([, v]) => v).map(([k]) => labels[k] || k);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function getDetail(id) {
|
|
105
|
+
if (!detailCache.has(id)) detailCache.set(id, await api(`/api/materials/${id}`));
|
|
106
|
+
return detailCache.get(id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function loadMaterials() {
|
|
110
|
+
try {
|
|
111
|
+
const cfg = await api("/api/config");
|
|
112
|
+
lang = cfg.lang || lang;
|
|
113
|
+
const langEl = document.getElementById("lang");
|
|
114
|
+
if (langEl) langEl.value = lang;
|
|
115
|
+
const countryEl = document.getElementById("country");
|
|
116
|
+
if (countryEl && cfg.country) countryEl.value = cfg.country;
|
|
117
|
+
document.documentElement.lang = lang;
|
|
118
|
+
} catch {}
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
params.set("lang", lang);
|
|
121
|
+
const { materials } = await api(`/api/materials?${params}`);
|
|
122
|
+
allMaterials = materials;
|
|
123
|
+
renderHeader();
|
|
124
|
+
applyModel();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function facetValues(key) {
|
|
128
|
+
if (key === "usage") return Object.keys(USAGE_LABELS.en);
|
|
129
|
+
return [...new Set(allMaterials.map((m) => String(m[key] ?? "")))].sort();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function numericComparator(key, dir) {
|
|
133
|
+
const numeric = (m) => m[key === "lambda" ? "lambda_min" : "thick_min"];
|
|
134
|
+
return (a, b) => {
|
|
135
|
+
const va = numeric(a);
|
|
136
|
+
const vb = numeric(b);
|
|
137
|
+
const aNull = va === null || va === undefined;
|
|
138
|
+
const bNull = vb === null || vb === undefined;
|
|
139
|
+
if (aNull || bNull) return aNull && bNull ? 0 : (aNull ? 1 : -1); // null metrics stay last
|
|
140
|
+
return dir * (va - vb);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderHeader() {
|
|
145
|
+
const headerRow = $("header-row");
|
|
146
|
+
headerRow.innerHTML = `<th></th>` + COLUMNS.map((col) => {
|
|
147
|
+
const arrow = sortKey === col.key ? (sortAsc ? " \u2191" : " \u2193") : "";
|
|
148
|
+
const chevron = col.facet ? ` <span class="chevron" data-facet="${col.key}">\u25be</span>` : "";
|
|
149
|
+
return `<th data-sort="${col.key}" style="cursor:pointer">${esc(col.label)}${arrow}${chevron}</th>`;
|
|
150
|
+
}).join("");
|
|
151
|
+
headerRow.querySelectorAll("th[data-sort]").forEach((th) => {
|
|
152
|
+
th.addEventListener("click", (event) => {
|
|
153
|
+
if (event.target.classList.contains("chevron")) return;
|
|
154
|
+
const key = th.dataset.sort;
|
|
155
|
+
const numericCol = key === "lambda" || key === "thick";
|
|
156
|
+
if (sortKey === key && numericCol) {
|
|
157
|
+
sortAsc = !sortAsc;
|
|
158
|
+
allMaterials.sort(numericComparator(key, sortAsc ? 1 : -1)); // sign factor: nulls stay last
|
|
159
|
+
} else if (sortKey === key) { sortAsc = !sortAsc; allMaterials.reverse(); }
|
|
160
|
+
else {
|
|
161
|
+
sortKey = key;
|
|
162
|
+
sortAsc = true;
|
|
163
|
+
allMaterials.sort(numericCol ? numericComparator(key, 1)
|
|
164
|
+
: (a, b) => String(a[key] ?? "").localeCompare(String(b[key] ?? "")));
|
|
165
|
+
}
|
|
166
|
+
renderHeader();
|
|
167
|
+
applyModel();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
headerRow.querySelectorAll(".chevron").forEach((el) => {
|
|
171
|
+
el.addEventListener("click", (event) => {
|
|
172
|
+
event.stopPropagation();
|
|
173
|
+
toggleFacetDropdown(el.dataset.facet, el);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function matchesFacets(m) {
|
|
179
|
+
const usageActive = (k) => m.usage[k];
|
|
180
|
+
for (const [facet, chosen] of Object.entries(facetSelections)) {
|
|
181
|
+
if (!chosen.size) continue;
|
|
182
|
+
if (facet === "usage") {
|
|
183
|
+
if (![...chosen].some((k) => usageActive(k))) return false;
|
|
184
|
+
} else if (!chosen.has(String(m[facet] ?? ""))) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let renderGeneration = 0;
|
|
192
|
+
|
|
193
|
+
async function applyModel() {
|
|
194
|
+
const generation = ++renderGeneration;
|
|
195
|
+
const needle = $("text").value.trim().toLowerCase();
|
|
196
|
+
const rowsEl = $("rows");
|
|
197
|
+
const matching = [];
|
|
198
|
+
for (const m of allMaterials) {
|
|
199
|
+
if (!matchesFacets(m)) continue;
|
|
200
|
+
if (needle && !(m.display_name.toLowerCase().includes(needle) ||
|
|
201
|
+
m.company.toLowerCase().includes(needle) || m.category.toLowerCase().includes(needle))) continue;
|
|
202
|
+
matching.push(m);
|
|
203
|
+
}
|
|
204
|
+
let details;
|
|
205
|
+
try {
|
|
206
|
+
details = new Map(await Promise.all([...expanded].map(async (id) => [id, await getDetail(id)])));
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (generation !== renderGeneration) return;
|
|
209
|
+
setStatus(`detail load failed: ${err.message}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (generation !== renderGeneration) return; // a newer render superseded this one
|
|
213
|
+
rowsEl.innerHTML = "";
|
|
214
|
+
let visible = 0;
|
|
215
|
+
for (const m of matching) {
|
|
216
|
+
visible += 1;
|
|
217
|
+
const tr = document.createElement("tr");
|
|
218
|
+
tr.dataset.id = m.id;
|
|
219
|
+
const usage = usageWords(m).map(esc).join(" ");
|
|
220
|
+
const pickCell = m.type === "simple"
|
|
221
|
+
? `<td><span class="expander" data-id="${esc(m.id)}" style="cursor:pointer">${expanded.has(m.id) ? "\u25be" : "\u25b8"}</span>` +
|
|
222
|
+
`<input type="checkbox" title="checked = pick whole material (all layers); untick to choose layers below"></td>`
|
|
223
|
+
: `<td><input type="checkbox" title="pick whole material"></td>`;
|
|
224
|
+
tr.innerHTML = pickCell +
|
|
225
|
+
`<td>${esc(m.display_name)}</td><td>${esc(m.company)}</td><td>${esc(m.category)}</td><td>${esc(m.type)}</td>` +
|
|
226
|
+
`<td>${esc(fmt([m.lambda_min, m.lambda_max]))}</td><td>${esc(fmt([m.thick_min, m.thick_max], 0))}</td>` +
|
|
227
|
+
`<td>${usage}</td>`;
|
|
228
|
+
const [checkbox] = tr.getElementsByTagName("input");
|
|
229
|
+
checkbox.onchange = () => {
|
|
230
|
+
if (checkbox.checked) layerSelections.delete(m.id); // parent checked = all layers
|
|
231
|
+
checkbox.checked ? selected.add(m.id) : selected.delete(m.id);
|
|
232
|
+
document.querySelectorAll(`tr.layerrow[data-parent="${m.id}"] input[data-layer]`)
|
|
233
|
+
.forEach((el) => { el.checked = false; });
|
|
234
|
+
};
|
|
235
|
+
const expander = tr.querySelector(".expander");
|
|
236
|
+
if (expander) expander.addEventListener("click", (event) => {
|
|
237
|
+
event.stopPropagation();
|
|
238
|
+
toggleExpand(tr, m);
|
|
239
|
+
});
|
|
240
|
+
tr.addEventListener("click", (event) => {
|
|
241
|
+
if (event.target.tagName === "INPUT") return;
|
|
242
|
+
document.querySelectorAll("tr.selected").forEach((el) => el.classList.remove("selected"));
|
|
243
|
+
tr.classList.add("selected");
|
|
244
|
+
showDetail(m.id);
|
|
245
|
+
});
|
|
246
|
+
rowsEl.appendChild(tr);
|
|
247
|
+
if (expanded.has(m.id) && details.has(m.id)) {
|
|
248
|
+
tr.insertAdjacentHTML("afterend", layerRowsHtml(m, details.get(m.id)));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!visible) rowsEl.innerHTML = `<tr><td colspan="8" style="color:#888">no materials match</td></tr>`;
|
|
252
|
+
rowsEl.querySelectorAll("input[data-layer]").forEach((input) => bindLayerCheckbox(input));
|
|
253
|
+
setStatus(`${visible} materials`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function toggleExpand(tr, m) {
|
|
257
|
+
if (expanded.has(m.id)) { expanded.delete(m.id); }
|
|
258
|
+
else { expanded.add(m.id); }
|
|
259
|
+
applyModel(); // rerender includes child rows below
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function layerRowsHtml(m, detail) {
|
|
263
|
+
return detail.layers.map((layer) => {
|
|
264
|
+
const chosen = (layerSelections.get(m.id) || new Set()).has(layer.id);
|
|
265
|
+
return `<tr class="layerrow" data-parent="${esc(m.id)}">` +
|
|
266
|
+
`<td><input type="checkbox" data-layer="${esc(layer.id)}" data-material="${esc(m.id)}"` +
|
|
267
|
+
` title="pick only this layer"${chosen ? " checked" : ""}></td>` +
|
|
268
|
+
`<td colspan="7">\u251c ${esc(String(layer.id).slice(0, 8))}\u2026 · ${fmt(layer.thick, 0)} mm · \u03bb ${esc(fmt(layer.lambda_value))}</td></tr>`;
|
|
269
|
+
}).join("");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function bindLayerCheckbox(input) {
|
|
273
|
+
input.addEventListener("change", () => {
|
|
274
|
+
const materialId = input.dataset.material;
|
|
275
|
+
const set = layerSelections.get(materialId) || new Set();
|
|
276
|
+
input.checked ? set.add(input.dataset.layer) : set.delete(input.dataset.layer);
|
|
277
|
+
layerSelections.set(materialId, set);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function toggleFacetDropdown(facetKey, anchor) {
|
|
282
|
+
const existing = document.querySelector(".facet");
|
|
283
|
+
if (existing) { existing.remove(); return; }
|
|
284
|
+
const box = document.createElement("div");
|
|
285
|
+
box.className = "facet";
|
|
286
|
+
const values = facetValues(facetKey).map((value) => {
|
|
287
|
+
const count = allMaterials.filter((m) =>
|
|
288
|
+
facetKey === "usage" ? m.usage[value] : String(m[facetKey] ?? "") === value).length;
|
|
289
|
+
const checked = facetSelections[facetKey].has(value);
|
|
290
|
+
const label = facetKey === "usage" ? ((USAGE_LABELS[lang] || USAGE_LABELS.en)[value] || value) : value;
|
|
291
|
+
return `<label><input type="checkbox" data-value="${esc(value)}"${checked ? " checked" : ""}> ${esc(label)} (${count})</label>`;
|
|
292
|
+
}).join("");
|
|
293
|
+
box.innerHTML = `<div class="label">${esc(facetKey)}</div>${values}` +
|
|
294
|
+
`<div style="margin-top:.3rem"><button class="mock-button" data-clear>clear</button></div>`;
|
|
295
|
+
anchor.parentElement.appendChild(box);
|
|
296
|
+
box.addEventListener("change", (event) => {
|
|
297
|
+
const input = event.target;
|
|
298
|
+
if (input.dataset.value === undefined) return;
|
|
299
|
+
input.checked ? facetSelections[facetKey].add(input.dataset.value)
|
|
300
|
+
: facetSelections[facetKey].delete(input.dataset.value);
|
|
301
|
+
applyModel();
|
|
302
|
+
});
|
|
303
|
+
box.querySelector("[data-clear]").onclick = () => { facetSelections[facetKey].clear(); box.remove(); applyModel(); };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
document.addEventListener("click", (event) => {
|
|
307
|
+
if (!event.target.closest(".facet") && !event.target.classList.contains("chevron")) {
|
|
308
|
+
document.querySelector(".facet")?.remove();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
async function showDetail(id) {
|
|
313
|
+
const m = await api(`/api/materials/${id}`);
|
|
314
|
+
lastSelectedId = id;
|
|
315
|
+
$("preview").style.display = "inline-block";
|
|
316
|
+
const nameLines = Object.entries(m.names)
|
|
317
|
+
.map(([code, value]) => `${esc(code || "(no lang)")}: ${esc(value)}`).join("<br>");
|
|
318
|
+
const description = Object.entries(m.descriptions)
|
|
319
|
+
.map(([code, value]) => `${esc(code || "(no lang)")}: ${esc(value)}`).join("<br>");
|
|
320
|
+
const groups = [
|
|
321
|
+
["names", nameLines], ["descriptions", description],
|
|
322
|
+
["company", esc(`${m.company} (${m.company_id})`)], ["category", esc(m.category)], ["type", esc(m.type)],
|
|
323
|
+
["\u03bb W/mK", fmt([m.lambda_min, m.lambda_max])], ["thickness mm", fmt([m.thick_min, m.thick_max], 0)],
|
|
324
|
+
["U-value", fmt(m.u_value_without)], ["consref", esc(m.consref)], ["design usage", esc(m.designusage)],
|
|
325
|
+
["id", esc(m.id)],
|
|
326
|
+
];
|
|
327
|
+
$("detail").innerHTML = groups.filter(([, v]) => v !== null && v !== undefined && v !== "")
|
|
328
|
+
.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`).join("");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function openDrawer(detail) {
|
|
332
|
+
const m = detail;
|
|
333
|
+
const drawer = $("drawer");
|
|
334
|
+
drawer.style.display = "block";
|
|
335
|
+
let inner;
|
|
336
|
+
if (m.type === "simple" && detail.layers.length) {
|
|
337
|
+
const total = detail.layers.reduce((sum, l) => sum + (l.thick || 0), 0) || 1;
|
|
338
|
+
const bars = detail.layers.map((layer) => {
|
|
339
|
+
const flex = (layer.thick || 0) / total;
|
|
340
|
+
const chosen = (layerSelections.get(m.id) || new Set()).has(layer.id);
|
|
341
|
+
return `<div style="flex:${flex};background:#eef;display:flex;align-items:center;justify-content:center;font-size:.75rem">` +
|
|
342
|
+
`${esc(String(layer.id).slice(0, 8))}\u2026<br>${fmt(layer.thick, 0)} mm<br>` +
|
|
343
|
+
`<input type="checkbox" data-layer="${esc(layer.id)}" data-material="${esc(m.id)}"${chosen ? " checked" : ""}></div>`;
|
|
344
|
+
}).join("");
|
|
345
|
+
inner = `<b>STACK PREVIEW \u2014 ${esc(m.display_name)}</b>` +
|
|
346
|
+
`<div style="float:right"><button id="close-drawer">close</button></div>` +
|
|
347
|
+
`<div class="stackbar">${bars}</div>` +
|
|
348
|
+
`<label style="display:inline"><input type="checkbox" id="all-layers"> pick whole material (all layers)</label>`;
|
|
349
|
+
} else if (m.type === "btk") {
|
|
350
|
+
inner = `<b>VARIATIONS \u2014 ${esc(m.display_name)}</b>` +
|
|
351
|
+
`<div style="float:right"><button id="close-drawer">close</button></div><ul>` +
|
|
352
|
+
detail.variations.map((v) => `<li>${fmt(v.thick, 0)} mm \u00b7 U ${esc(fmt(v.u_value_without))}</li>`).join("") + `</ul>`;
|
|
353
|
+
} else {
|
|
354
|
+
inner = `<b>CONSTRUCTION</b>` +
|
|
355
|
+
`<div style="float:right"><button id="close-drawer">close</button></div>` +
|
|
356
|
+
`<div>consref: ${esc(m.consref || "")} \u00b7 designusage: ${esc(m.designusage || "")}</div>` +
|
|
357
|
+
`<div style="color:#777">assembly composition lives outside this schema record</div>`;
|
|
358
|
+
}
|
|
359
|
+
drawer.innerHTML = inner;
|
|
360
|
+
drawer.querySelector("#close-drawer")?.addEventListener("click", () => (drawer.style.display = "none"));
|
|
361
|
+
drawer.querySelector("#all-layers")?.addEventListener("change", (event) => {
|
|
362
|
+
if (event.target.checked) { layerSelections.delete(m.id); applyModel(); }
|
|
363
|
+
});
|
|
364
|
+
drawer.querySelectorAll("input[data-layer]").forEach((input) => bindLayerCheckbox(input));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function pickIds(action) {
|
|
368
|
+
const items = PickerCore.collectItems(selected, layerSelections);
|
|
369
|
+
if (!items.length) return setStatus("select at least one material or layer");
|
|
370
|
+
if (action === "export") {
|
|
371
|
+
const blob = await api("/api/export", { method: "POST", body: JSON.stringify({ items }) });
|
|
372
|
+
const url = URL.createObjectURL(blob);
|
|
373
|
+
const link = document.createElement("a");
|
|
374
|
+
link.href = url; link.download = "materialsdb_export.ifc"; link.click();
|
|
375
|
+
URL.revokeObjectURL(url);
|
|
376
|
+
} else if (action === "pick") {
|
|
377
|
+
const result = await api("/api/pick", { method: "POST",
|
|
378
|
+
body: JSON.stringify({ items, replace: $("replace").checked }) });
|
|
379
|
+
setStatus(`appended ${result.added}, missing ${result.missing.length}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function openSession() {
|
|
384
|
+
const path = prompt("path to existing .ifc to append into:");
|
|
385
|
+
if (!path) return;
|
|
386
|
+
const result = await api("/api/session/open", { method: "POST", body: JSON.stringify({ path }) });
|
|
387
|
+
setStatus(`session open: ${result.path}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function saveSession() {
|
|
391
|
+
const path = prompt("save as (blank = original path):");
|
|
392
|
+
const result = await api("/api/session/save", { method: "POST", body: path ? JSON.stringify({ path }) : "{}" });
|
|
393
|
+
setStatus(`saved ${result.saved}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function sendToBonsai() {
|
|
397
|
+
const items = PickerCore.collectItems(selected, layerSelections);
|
|
398
|
+
if (!items.length) return setStatus("select at least one material or layer");
|
|
399
|
+
const client_id = bonsaiTarget.current();
|
|
400
|
+
if (!client_id) return setStatus("no Bonsai listener connected");
|
|
401
|
+
const result = await api("/api/listener/send", {
|
|
402
|
+
method: "POST",
|
|
403
|
+
body: JSON.stringify({ client_id, action: "add_materials", items }),
|
|
404
|
+
});
|
|
405
|
+
PickerCore.flash(setStatus, () => $("status").textContent, `sent to ${bonsaiTarget.label() || "?"}: ${result.summary}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function setStatus(text) { $("status").textContent = text; return text; }
|
|
409
|
+
|
|
410
|
+
$("export").onclick = () => pickIds("export").catch((err) => setStatus(err.message));
|
|
411
|
+
$("pick").onclick = () => pickIds("pick").catch((err) => setStatus(err.message));
|
|
412
|
+
$("open").onclick = () => openSession().catch((err) => setStatus(err.message));
|
|
413
|
+
$("save").onclick = () => saveSession().catch((err) => setStatus(err.message));
|
|
414
|
+
$("refresh").onclick = async () => {
|
|
415
|
+
const report = await api("/api/refresh", { method: "POST", body: "{}" });
|
|
416
|
+
setStatus(`cache refreshed: ${report.existing} unchanged, ${report.updated.length} updated`);
|
|
417
|
+
};
|
|
418
|
+
$("send-bonsai").onclick = () => sendToBonsai().catch((err) => setStatus(err.message));
|
|
419
|
+
const bonsaiTarget = PickerCore.startTargetPoll(api, $("bonsai-target"), $("send-bonsai"));
|
|
420
|
+
|
|
421
|
+
let debounceTimer;
|
|
422
|
+
$("text").addEventListener("input", () => {
|
|
423
|
+
clearTimeout(debounceTimer);
|
|
424
|
+
debounceTimer = setTimeout(applyModel, 250);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
$("lang").addEventListener("change", async () => {
|
|
428
|
+
lang = $("lang").value;
|
|
429
|
+
await api("/api/config", { method: "POST", body: JSON.stringify({ lang }) });
|
|
430
|
+
detailCache.clear();
|
|
431
|
+
await loadMaterials();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
$("country").addEventListener("change", async () => {
|
|
435
|
+
await api("/api/config", { method: "POST", body: JSON.stringify({ country: $("country").value }) });
|
|
436
|
+
detailCache.clear();
|
|
437
|
+
await loadMaterials();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
$("preview").onclick = async () => {
|
|
441
|
+
if (!lastSelectedId) return setStatus("select a material first");
|
|
442
|
+
openDrawer(await getDetail(lastSelectedId));
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
document.getElementById("settings-tab").addEventListener("click", (e) => {
|
|
446
|
+
e.preventDefault();
|
|
447
|
+
const p = document.getElementById("settings-panel");
|
|
448
|
+
p.style.display = p.style.display === "none" ? "block" : "none";
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
loadMaterials();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>materialsdb constructions</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; margin: 1rem; }
|
|
8
|
+
.top-tabs { display:flex; gap:.5rem; margin-bottom:.75rem; border-bottom:2px solid #ccc; }
|
|
9
|
+
.top-tabs a { padding:.4rem .9rem; text-decoration:none; color:#333; border-bottom:2px solid transparent; margin-bottom:-2px; }
|
|
10
|
+
.top-tabs a.active { border-bottom-color:#06c; font-weight:bold; color:#06c; }
|
|
11
|
+
table { border-collapse: collapse; margin-top: .5rem; }
|
|
12
|
+
th, td { border: 1px solid #ccc; padding: .25rem .5rem; text-align: left; }
|
|
13
|
+
input, select { margin-right: .5rem; }
|
|
14
|
+
button { margin: .5rem .25rem 0 0; }
|
|
15
|
+
dl dt { font-weight: bold; margin-top: .4rem; }
|
|
16
|
+
dl dd { margin: 0 0 .3rem 0; }
|
|
17
|
+
#status { color: #06c; min-height: 1.2em; }
|
|
18
|
+
.stackbar { display: flex; height: 46px; border: 1px solid #999; margin: .3rem 0; }
|
|
19
|
+
.stackbar-seg { display: flex; align-items: center; justify-content: center;
|
|
20
|
+
font-size: .72rem; text-align: center; border-right: 1px solid #fff; overflow: hidden; }
|
|
21
|
+
.rs-chip { font-size: .75rem; background: #eef; padding: .1rem .5rem; border-radius: 4px; }
|
|
22
|
+
tr.rsrow td { background: #f4f4f8; font-style: italic; }
|
|
23
|
+
.warn { color: #b60; font-weight: bold; cursor: help; }
|
|
24
|
+
.label { font-size: .7rem; text-transform: uppercase; color: #666; margin-top: .4rem; }
|
|
25
|
+
.section-toggle { cursor: pointer; user-select: none; font-size: .75rem; font-weight: bold;
|
|
26
|
+
text-transform: uppercase; letter-spacing: .03em; color: #666; margin: .45rem 0 .25rem; }
|
|
27
|
+
.section-toggle:hover { color: #06c; }
|
|
28
|
+
.chev { display: inline-block; width: 1em; }
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<nav class="top-tabs"><a href="/">Materials</a><a href="/constructions.html" class="active">Constructions</a><a href="#" id="settings-tab">Settings</a></nav>
|
|
33
|
+
<div id="settings-panel" style="display:none;padding:.5rem;border:1px solid #ccc;margin-bottom:.5rem">
|
|
34
|
+
<label>Language <select id="lang"><option value="en">en</option><option value="fr">fr</option><option value="de">de</option></select></label>
|
|
35
|
+
<label>Country <select id="country"><option value="CH">CH</option><option value="FR">FR</option></select></label>
|
|
36
|
+
</div>
|
|
37
|
+
<div style="display:flex;gap:1rem;margin:1rem;height:calc(100vh - 2rem)">
|
|
38
|
+
<div id="saved" style="width:15rem;border-right:1px solid #ccc;padding-right:.6rem;overflow:auto">
|
|
39
|
+
<h3 class="section-toggle" data-section="saved">Saved <span class="chev">▾</span></h3>
|
|
40
|
+
<div id="section-saved">
|
|
41
|
+
<ul id="saved-list" style="list-style:none;padding:0"></ul>
|
|
42
|
+
<button id="new">new</button><button id="delete">delete</button>
|
|
43
|
+
</div>
|
|
44
|
+
<hr>
|
|
45
|
+
<h4 class="section-toggle" data-section="mdb">Materialsdb (read-only) <span class="chev">▾</span></h4>
|
|
46
|
+
<div id="section-mdb">
|
|
47
|
+
<input id="mdb-search" placeholder="search…" style="width:95%">
|
|
48
|
+
<select id="mdb-company" style="width:95%;margin-top:.25rem"><option value="">all companies</option></select>
|
|
49
|
+
<ul id="mdb-list" style="list-style:none;padding:0;font-size:.85rem"></ul>
|
|
50
|
+
</div>
|
|
51
|
+
<hr>
|
|
52
|
+
<h4 class="section-toggle" data-section="incoming">Incoming from model <span id="incoming-count" style="color:#888;font-size:.8rem"></span> <span class="chev">▾</span></h4>
|
|
53
|
+
<div id="section-incoming">
|
|
54
|
+
<ul id="incoming-list" style="list-style:none;padding:0;font-size:.85rem"></ul>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div id="editor" style="flex:2;display:flex;flex-direction:column">
|
|
58
|
+
<div id="readonly-ui" style="display:none"></div>
|
|
59
|
+
<div id="composer-ui" style="display:flex;flex-direction:column;flex:1">
|
|
60
|
+
<div>
|
|
61
|
+
<input id="name" placeholder="construction name">
|
|
62
|
+
<select id="design-usage">
|
|
63
|
+
<option value="">generic</option>
|
|
64
|
+
<option value="consDesignForWall">wall</option>
|
|
65
|
+
<option value="consDesignForRoof">roof</option>
|
|
66
|
+
<option value="consDesignForFloor">floor</option>
|
|
67
|
+
</select>
|
|
68
|
+
<button id="add-layer">add layer</button>
|
|
69
|
+
</div>
|
|
70
|
+
<div id="preview" style="margin-top:.5rem"></div>
|
|
71
|
+
<table><thead><tr>
|
|
72
|
+
<th>#</th><th>material</th><th>thickness mm</th><th>lambda</th><th>r m2K/W</th><th></th>
|
|
73
|
+
</tr></thead><tbody id="layers"></tbody></table>
|
|
74
|
+
<div style="margin-top:.35rem">
|
|
75
|
+
<button data-move="up">↑ up</button><button data-move="down">↓ down</button>
|
|
76
|
+
<button id="reverse">↕ reverse</button>
|
|
77
|
+
<button data-action="remove">remove selected</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div id="ucard" style="width:16rem;border-left:1px solid #ccc;padding-left:.6rem">
|
|
82
|
+
<h3>U-value</h3>
|
|
83
|
+
<select id="preset"><option>ISO6946</option><option>SIA180</option></select>
|
|
84
|
+
<div id="u-display" style="font-size:1.6rem">-</div>
|
|
85
|
+
<div id="formula-line"
|
|
86
|
+
title="U [W/m²K]: thermal transmittance of the construction Rsi [m²K/W]: interior surface resistance (from the selected preset) dᵢ [m]: thickness of layer i λᵢ [W/(m·K)]: thermal conductivity of layer i Rse [m²K/W]: exterior surface resistance (from the selected preset) Layers without a lambda value are excluded and listed as warnings."
|
|
87
|
+
style="font-size:.72rem;color:#555;cursor:help;margin-top:.2rem">
|
|
88
|
+
U = 1/(Rsi + Σ dᵢ/λᵢ + Rse)
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="label" style="margin-top:.3rem">LAYERS</div>
|
|
92
|
+
<dl id="contributions"></dl>
|
|
93
|
+
<div id="warnings" style="color:#b60"></div>
|
|
94
|
+
<hr>
|
|
95
|
+
<button id="save">save</button>
|
|
96
|
+
<button id="export-ifc">export .ifc</button>
|
|
97
|
+
<button id="append-session">append to session</button>
|
|
98
|
+
<button id="send-bonsai" disabled>send to:</button>
|
|
99
|
+
<select id="bonsai-target" style="display:none"></select>
|
|
100
|
+
<p id="status"></p>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<script>window.MATERIALSDB_TOKEN = "__TOKEN__";</script>
|
|
104
|
+
<script src="/picker-core.js"></script>
|
|
105
|
+
<script src="/app-constructions.js"></script>
|
|
106
|
+
</body>
|
|
107
|
+
</html>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>materialsdb picker</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; margin: 1rem; display: flex; flex-wrap: wrap; gap: 1rem; }
|
|
8
|
+
.top-tabs { display:flex; gap:.5rem; width:100%; margin-bottom:.5rem; border-bottom:2px solid #ccc; }
|
|
9
|
+
.top-tabs a { padding:.4rem .9rem; text-decoration:none; color:#333; border-bottom:2px solid transparent; margin-bottom:-2px; }
|
|
10
|
+
.top-tabs a.active { border-bottom-color:#06c; font-weight:bold; color:#06c; }
|
|
11
|
+
#left { flex: 3; } #right { flex: 2; }
|
|
12
|
+
table { border-collapse: collapse; width: 100%; }
|
|
13
|
+
th, td { border: 1px solid #ccc; padding: .25rem .5rem; text-align: left; cursor: pointer; }
|
|
14
|
+
tr.selected { background: #eef; }
|
|
15
|
+
aside { position: sticky; top: 1rem; }
|
|
16
|
+
dl dt { font-weight: bold; margin-top: .4rem; }
|
|
17
|
+
label { display: block; margin-top: .5rem; }
|
|
18
|
+
input, select { margin-right: .5rem; }
|
|
19
|
+
button { margin: .5rem .25rem 0 0; }
|
|
20
|
+
#status { color: #06c; min-height: 1.2em; }
|
|
21
|
+
.facet { position:absolute; background:#fff; border:1px solid #999; padding:.35rem; z-index:5; max-height:14rem; overflow:auto; }
|
|
22
|
+
thead th { position: relative; }
|
|
23
|
+
tr.layerrow td { padding-left: 2rem; color: #555; background: #fafcff; }
|
|
24
|
+
#drawer { border-top: 2px solid #99c; padding: .5rem; background: #fdfdff; }
|
|
25
|
+
.stackbar { display: flex; height: 44px; border: 1px solid #999; margin: .3rem 0; }
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<nav class="top-tabs"><a href="/" class="active">Materials</a><a href="/constructions.html">Constructions</a><a href="#" id="settings-tab">Settings</a></nav>
|
|
30
|
+
<div id="settings-panel" style="display:none;width:100%;padding:.5rem;border:1px solid #ccc;margin-bottom:.5rem">
|
|
31
|
+
<label>Language <select id="lang"><option value="en">en</option><option value="fr">fr</option><option value="de">de</option></select></label>
|
|
32
|
+
<label>Country <select id="country"><option value="CH">CH</option><option value="FR">FR</option></select></label>
|
|
33
|
+
</div>
|
|
34
|
+
<div id="left">
|
|
35
|
+
<div>
|
|
36
|
+
<input id="text" placeholder="live search…" style="min-width:16rem">
|
|
37
|
+
<button id="export">export .ifc</button>
|
|
38
|
+
<button id="open">open session…</button>
|
|
39
|
+
<button id="pick">append selected</button>
|
|
40
|
+
<label style="display:inline">replace <input type="checkbox" id="replace"></label>
|
|
41
|
+
<button id="save">save session</button>
|
|
42
|
+
<button id="refresh">refresh cache</button>
|
|
43
|
+
<button id="send-bonsai" disabled>send to:</button>
|
|
44
|
+
<select id="bonsai-target" style="display:none"></select>
|
|
45
|
+
</div>
|
|
46
|
+
<p id="status"></p>
|
|
47
|
+
<table><thead><tr id="header-row"></tr></thead>
|
|
48
|
+
<tbody id="rows"></tbody></table>
|
|
49
|
+
</div>
|
|
50
|
+
<aside id="right"><h3>detail <button id="preview" style="display:none;float:right">preview stack</button></h3><dl id="detail"></dl></aside>
|
|
51
|
+
<div id="drawer" style="display:none;flex-basis:100%"></div>
|
|
52
|
+
<script>window.MATERIALSDB_TOKEN = "__TOKEN__";</script>
|
|
53
|
+
<script src="/picker-core.js"></script>
|
|
54
|
+
<script src="/app.js"></script>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|