python-materialsdb 0.3.1__tar.gz → 0.4.0__tar.gz

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.
Files changed (65) hide show
  1. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/PKG-INFO +17 -2
  2. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/README.md +16 -1
  3. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/pyproject.toml +1 -1
  4. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/classes.py +1 -1
  5. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/config.py +17 -0
  6. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/listener.py +113 -16
  7. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/server.py +561 -5
  8. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/static/app-constructions.js +8 -19
  9. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/static/app.js +308 -35
  10. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/static/constructions.html +1 -5
  11. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/static/index.html +20 -5
  12. python_materialsdb-0.4.0/src/materialsdb/gui/static/settings.html +74 -0
  13. python_materialsdb-0.4.0/src/materialsdb/gui/static/settings.js +643 -0
  14. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/ifc/material_builder.py +91 -48
  15. python_materialsdb-0.4.0/src/materialsdb/schemes.py +196 -0
  16. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/store.py +101 -0
  17. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/utils.py +5 -0
  18. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/python_materialsdb.egg-info/PKG-INFO +17 -2
  19. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/python_materialsdb.egg-info/SOURCES.txt +8 -0
  20. python_materialsdb-0.4.0/tests/test_bonsai_discovery.py +224 -0
  21. python_materialsdb-0.4.0/tests/test_bonsai_insert.py +1550 -0
  22. python_materialsdb-0.4.0/tests/test_bonsai_safety_net.py +48 -0
  23. python_materialsdb-0.4.0/tests/test_config.py +33 -0
  24. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_construction_frontend.py +1 -0
  25. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_gui_server.py +886 -5
  26. python_materialsdb-0.4.0/tests/test_listener_payload.py +571 -0
  27. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_material_builder.py +142 -0
  28. python_materialsdb-0.4.0/tests/test_model_changes_frontend.py +42 -0
  29. python_materialsdb-0.4.0/tests/test_schemes.py +80 -0
  30. python_materialsdb-0.4.0/tests/test_settings_frontend.py +84 -0
  31. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_store.py +116 -0
  32. python_materialsdb-0.3.1/tests/test_bonsai_discovery.py +0 -90
  33. python_materialsdb-0.3.1/tests/test_bonsai_insert.py +0 -515
  34. python_materialsdb-0.3.1/tests/test_listener_payload.py +0 -262
  35. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/LICENSE.md +0 -0
  36. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/setup.cfg +0 -0
  37. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/__init__.py +0 -0
  38. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/cache.py +0 -0
  39. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/construction.py +0 -0
  40. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/__init__.py +0 -0
  41. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/__main__.py +0 -0
  42. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/discovery.py +0 -0
  43. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/gui/static/picker-core.js +0 -0
  44. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/ifc/__init__.py +0 -0
  45. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/ifc/material_psets.json +0 -0
  46. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/ifc/project_library.py +0 -0
  47. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/query.py +0 -0
  48. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/schema/MaterialsDBIndex100.xsd +0 -0
  49. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/schema/materialsdb102.xsd +0 -0
  50. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/schema/materialsdb103.xsd +0 -0
  51. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/serialiser.py +0 -0
  52. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/materialsdb/summary.py +0 -0
  53. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/python_materialsdb.egg-info/dependency_links.txt +0 -0
  54. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/python_materialsdb.egg-info/entry_points.txt +0 -0
  55. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/python_materialsdb.egg-info/requires.txt +0 -0
  56. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/src/python_materialsdb.egg-info/top_level.txt +0 -0
  57. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_build_bonsai_addon.py +0 -0
  58. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_cache.py +0 -0
  59. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_construction.py +0 -0
  60. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_construction_ifc.py +0 -0
  61. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_picker_frontend.py +0 -0
  62. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_project_library.py +0 -0
  63. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_query.py +0 -0
  64. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_serialiser.py +0 -0
  65. {python_materialsdb-0.3.1 → python_materialsdb-0.4.0}/tests/test_summary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-materialsdb
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: A library to work with materialsdb.org open standard for building materials.
5
5
  Author-email: Cyril Waechter <cyrwae@hotmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -107,6 +107,17 @@ a standalone `.ifc`, or open one of your own `.ifc` files and append the
107
107
  selected materials into it. The same HTTP API powers future BIM software
108
108
  plugins (all mutating calls require a per-launch token).
109
109
 
110
+ # Colour schemes & settings :
111
+ The Settings page (top navigation) holds language and country, the active
112
+ colour scheme and an *always use category colours* switch. Materials with a
113
+ producer colour keep it unless the switch is on; the others take their
114
+ category's colour from the active scheme (built-in Lesosai, or your own
115
+ palettes). The palette editor edits all 17 category colours with colour
116
+ pickers and a live preview, saves palettes by name, renames on save, and
117
+ exports/imports them as JSON. *Apply colours to model* repaints every
118
+ materialsdb material in the open Bonsai model — choosing the target model and
119
+ showing the listener's result — without touching the material data.
120
+
110
121
  # Construction maker :
111
122
  Compose thermal constructions from materialsdb materials and compute their
112
123
  U-value (ISO 6946 / SIA 180 surface resistance presets):
@@ -152,7 +163,11 @@ constructions become typed elements (`IfcWallType`/`IfcSlabType`/
152
163
  visible in the outliner and revertible with Ctrl+Z. Each pushed type
153
164
  also carries its computed U-value as `ThermalTransmittance` in
154
165
  `Pset_WallCommon`/`Pset_SlabCommon`/`Pset_RoofCommon` (per the element
155
- type). Round-trip: select
166
+ type). The picker flags materials whose producer data changed since you
167
+ pushed them, with a changed-field report and opt-in *Update*/*Replace*
168
+ actions (in-place updates keep the entity and never rewrite layer
169
+ thicknesses); *force update used materials* refreshes every materialsdb
170
+ material used by walls, slabs and roofs in one go. Round-trip: select
156
171
  the wall (or its type) in Bonsai and hit *Send type to composer* in the
157
172
  materialsdb panel — the construction appears under *incoming from model*
158
173
  on the constructions page for editing. Any layer's material can be
@@ -80,6 +80,17 @@ a standalone `.ifc`, or open one of your own `.ifc` files and append the
80
80
  selected materials into it. The same HTTP API powers future BIM software
81
81
  plugins (all mutating calls require a per-launch token).
82
82
 
83
+ # Colour schemes & settings :
84
+ The Settings page (top navigation) holds language and country, the active
85
+ colour scheme and an *always use category colours* switch. Materials with a
86
+ producer colour keep it unless the switch is on; the others take their
87
+ category's colour from the active scheme (built-in Lesosai, or your own
88
+ palettes). The palette editor edits all 17 category colours with colour
89
+ pickers and a live preview, saves palettes by name, renames on save, and
90
+ exports/imports them as JSON. *Apply colours to model* repaints every
91
+ materialsdb material in the open Bonsai model — choosing the target model and
92
+ showing the listener's result — without touching the material data.
93
+
83
94
  # Construction maker :
84
95
  Compose thermal constructions from materialsdb materials and compute their
85
96
  U-value (ISO 6946 / SIA 180 surface resistance presets):
@@ -125,7 +136,11 @@ constructions become typed elements (`IfcWallType`/`IfcSlabType`/
125
136
  visible in the outliner and revertible with Ctrl+Z. Each pushed type
126
137
  also carries its computed U-value as `ThermalTransmittance` in
127
138
  `Pset_WallCommon`/`Pset_SlabCommon`/`Pset_RoofCommon` (per the element
128
- type). Round-trip: select
139
+ type). The picker flags materials whose producer data changed since you
140
+ pushed them, with a changed-field report and opt-in *Update*/*Replace*
141
+ actions (in-place updates keep the entity and never rewrite layer
142
+ thicknesses); *force update used materials* refreshes every materialsdb
143
+ material used by walls, slabs and roofs in one go. Round-trip: select
129
144
  the wall (or its type) in Bonsai and hit *Send type to composer* in the
130
145
  materialsdb panel — the construction appears under *incoming from model*
131
146
  on the constructions page for editing. Any layer's material can be
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-materialsdb"
7
- version = "0.3.1"
7
+ version = "0.4.0"
8
8
  description = "A library to work with materialsdb.org open standard for building materials."
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -93,7 +93,7 @@ class Guid(str):
93
93
 
94
94
  class Brackguid(str):
95
95
  xs_type: str = "simpleType"
96
- xml_pattern:str = "\{[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\}"
96
+ xml_pattern:str = "\\{[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\\}"
97
97
  xml_name: str = "Brackguid"
98
98
 
99
99
 
@@ -50,6 +50,23 @@ def set_country(country: str):
50
50
  set_param("country", country)
51
51
 
52
52
 
53
+ def get_scheme() -> str:
54
+ return str(get_base_config().get("scheme") or "")
55
+
56
+
57
+ def set_scheme(name: str) -> None:
58
+ set_param("scheme", str(name))
59
+
60
+
61
+ def get_ignore_producer_color() -> bool:
62
+ value = get_base_config().get("ignore_producer_color", "false")
63
+ return str(value).strip().lower() in ("true", "1", "yes", "on")
64
+
65
+
66
+ def set_ignore_producer_color(value: bool) -> None:
67
+ set_param("ignore_producer_color", "true" if value else "false")
68
+
69
+
53
70
  def main():
54
71
  lang = get_lang()
55
72
  set_lang(lang)
@@ -5,7 +5,8 @@ MaterialBuilder would create (one entry per IfcMaterial); the Bonsai add-on
5
5
  maps the payload onto its open file without importing this library."""
6
6
 
7
7
  from materialsdb import config, utils
8
- from materialsdb.ifc.material_builder import PSETS, get_value
8
+ from materialsdb.ifc.material_builder import PSETS, get_value, style_for
9
+ from materialsdb.store import FINGERPRINT_SCHEME
9
10
 
10
11
 
11
12
  def _resolved_psets(layer, country) -> dict:
@@ -45,11 +46,21 @@ def build_add_materials_payload(store_, items, country=None, lang=None):
45
46
  if summary is None or material is None:
46
47
  missing.append(material_id)
47
48
  continue
49
+ fingerprint = store_.material_fingerprint(material_id)
48
50
  wanted = {str(g) for g in item.get("layer_ids") or []} or None
49
51
  name = str(utils.get_material_name(material, lang))
50
52
  description = str(utils.get_material_description(material, lang))
51
53
  category = str(getattr(material.information, "group", "") or "")
52
54
  color = getattr(material.information, "color", None)
55
+ color = int(color) if color else None
56
+ # Resolve the surface style server-side: the add-on runs in Blender's
57
+ # interpreter without access to the materialsdb colour scheme.
58
+ style_name, style_rgb = style_for(
59
+ color,
60
+ category,
61
+ scheme=config.get_scheme(),
62
+ ignore_producer_color=config.get_ignore_producer_color(),
63
+ )
53
64
  entries = []
54
65
  for layer in utils.get_material_layers(material):
55
66
  if wanted is not None and str(layer.id) not in wanted:
@@ -63,12 +74,20 @@ def build_add_materials_payload(store_, items, country=None, lang=None):
63
74
  "name": name,
64
75
  "description": description,
65
76
  "category": category,
66
- "color": int(color) if color else None,
77
+ "color": color,
78
+ "style_name": style_name,
79
+ "style_color": [round(component, 6) for component in style_rgb],
67
80
  "identity": {
68
81
  "material_id": material_id,
69
82
  "company_id": str(summary.company_id or ""),
70
83
  "company": str(summary.company or ""),
84
+ "fingerprint": fingerprint,
85
+ "fingerprint_scheme": FINGERPRINT_SCHEME,
71
86
  },
87
+ "mode": item.get("mode", "skip"),
88
+ "replaces": item.get("replaces"),
89
+ "update": item.get("update"),
90
+ "force": bool(item.get("force")),
72
91
  "psets": _resolved_psets(layer, country),
73
92
  "layer": {"layer_id": str(layer.id), "thick_m": thick / 1000} if thick else None,
74
93
  }
@@ -80,6 +99,55 @@ def build_add_materials_payload(store_, items, country=None, lang=None):
80
99
  return {"action": "add_materials", "materials": materials}, missing
81
100
 
82
101
 
102
+ def _layer_thick_m(layer, country) -> float | None:
103
+ """The layer's thickness in metres for `country`, or None when absent."""
104
+ thick = getattr(utils.get_by_country(layer.geometry or (), country), "thick", None)
105
+ return thick / 1000 if thick is not None else None
106
+
107
+
108
+ def _resolve_construction_layer(material, design_thickness_m, country, lang) -> tuple[object | None, str | None]:
109
+ """Map a construction layer's design thickness onto a materialsdb Layer.
110
+
111
+ Exact thickness match wins; a single-layer material resolves to its only
112
+ layer whatever the design thickness; multi-layer mismatches fall back to
113
+ the first layer with a warning. Returns (layer_or_None, warning_or_None)."""
114
+ layers = list(utils.get_material_layers(material))
115
+ if not layers:
116
+ name = str(utils.get_material_name(material, lang))
117
+ return None, f"{name}: material has no layer"
118
+ for layer in layers:
119
+ thick_m = _layer_thick_m(layer, country)
120
+ if thick_m is not None and abs(thick_m - design_thickness_m) <= utils.THICKNESS_EPS_M:
121
+ return layer, None
122
+ if len(layers) == 1:
123
+ return layers[0], None
124
+ name = str(utils.get_material_name(material, lang))
125
+ return layers[0], f"{name}: design thickness {round(design_thickness_m * 1000)} mm matches no layer"
126
+
127
+
128
+ def _raw_decision_layer(raw_layers, material_id, thickness_m) -> dict:
129
+ """The raw body layer carrying the decision for this validated layer.
130
+
131
+ ``validate_construction`` can drop or reorder malformed layers, so the
132
+ validated index does not address ``raw_layers``. Match on the pair
133
+ (material_id, thickness) instead; when no exact match is unambiguous the
134
+ decision fields are omitted, i.e. the layer is treated as ``skip`` rather
135
+ than inheriting another layer's mode/update/replaces."""
136
+ matches = []
137
+ for raw in raw_layers:
138
+ if not isinstance(raw, dict):
139
+ continue
140
+ if str(raw.get("material_id")) != str(material_id):
141
+ continue
142
+ try:
143
+ raw_thickness = float(raw.get("thickness_m"))
144
+ except (TypeError, ValueError):
145
+ continue
146
+ if abs(raw_thickness - thickness_m) <= utils.THICKNESS_EPS_M:
147
+ matches.append(raw)
148
+ return matches[0] if len(matches) == 1 else {}
149
+
150
+
83
151
  _DESIGN_USAGE_TO_TYPES = {
84
152
  "consDesignForWall": ["IfcWallType"],
85
153
  "consDesignForFloor": ["IfcSlabType"],
@@ -91,8 +159,9 @@ GENERIC_TYPES = ["IfcWallType", "IfcSlabType", "IfcRoofType"]
91
159
  def build_add_construction_payload(store_, body, country=None, lang=None):
92
160
  """Resolve a construction body into a self-sufficient add_construction
93
161
  payload (or (None, problems) when validation fails). Materials travel
94
- minimal — identity/name/category/color, to_ifc_layer_set-style — and
95
- find-or-create on the add-on side keys on the identity pset."""
162
+ resolved — identity/name/category/style plus their layer's materialsdb
163
+ psets — and find-or-create on the add-on side keys on
164
+ (material_id, org_layer layer_id)."""
96
165
  from materialsdb import construction as cm
97
166
 
98
167
  body = dict(body or {})
@@ -103,7 +172,11 @@ def build_add_construction_payload(store_, body, country=None, lang=None):
103
172
  return None, problems
104
173
  country = country or config.get_country()
105
174
  lang = lang or config.get_lang()
175
+ raw_layers: list = []
176
+ if isinstance(body.get("layers"), list):
177
+ raw_layers = body["layers"]
106
178
  layers = []
179
+ warnings = []
107
180
  for layer in construction.layers:
108
181
  if layer.placeholder is not None:
109
182
  layers.append(
@@ -119,23 +192,46 @@ def build_add_construction_payload(store_, body, country=None, lang=None):
119
192
  if summary is None or material is None:
120
193
  problems.append(f"unknown material id: {layer.material_id}")
121
194
  continue
195
+ resolved_layer, warning = _resolve_construction_layer(material, layer.thickness_m, country, lang)
196
+ if warning is not None:
197
+ warnings.append(warning)
198
+ raw_layer = _raw_decision_layer(raw_layers, layer.material_id, layer.thickness_m)
122
199
  color = getattr(material.information, "color", None)
200
+ color = int(color) if color else None
201
+ category = str(getattr(material.information, "group", "") or "")
202
+ style_name, style_rgb = style_for(
203
+ color,
204
+ category,
205
+ scheme=config.get_scheme(),
206
+ ignore_producer_color=config.get_ignore_producer_color(),
207
+ )
208
+ material_entry = {
209
+ "source_id": layer.material_id,
210
+ "name": str(utils.get_material_name(material, lang)),
211
+ "description": str(utils.get_material_description(material, lang)),
212
+ "category": category,
213
+ "color": color,
214
+ "style_name": style_name,
215
+ "style_color": [round(component, 6) for component in style_rgb],
216
+ "identity": {
217
+ "material_id": layer.material_id,
218
+ "company_id": str(summary.company_id or ""),
219
+ "company": str(summary.company or ""),
220
+ "fingerprint": store_.material_fingerprint(layer.material_id),
221
+ "fingerprint_scheme": FINGERPRINT_SCHEME,
222
+ },
223
+ "mode": raw_layer.get("mode", "skip"),
224
+ "replaces": raw_layer.get("replaces"),
225
+ "update": raw_layer.get("update"),
226
+ "force": bool(raw_layer.get("force")),
227
+ }
228
+ if resolved_layer is not None:
229
+ material_entry["psets"] = _resolved_psets(resolved_layer, country)
123
230
  layers.append(
124
231
  {
125
232
  "material_id": layer.material_id,
126
233
  "thickness_m": layer.thickness_m,
127
- "material": {
128
- "source_id": layer.material_id,
129
- "name": str(utils.get_material_name(material, lang)),
130
- "description": str(utils.get_material_description(material, lang)),
131
- "category": str(getattr(material.information, "group", "") or ""),
132
- "color": int(color) if color else None,
133
- "identity": {
134
- "material_id": layer.material_id,
135
- "company_id": str(summary.company_id or ""),
136
- "company": str(summary.company or ""),
137
- },
138
- },
234
+ "material": material_entry,
139
235
  }
140
236
  )
141
237
  if problems:
@@ -151,5 +247,6 @@ def build_add_construction_payload(store_, body, country=None, lang=None):
151
247
  cls: u for cls, u in cm.u_values_by_type(construction, store_, types).items() if u is not None
152
248
  },
153
249
  "layers": layers,
250
+ "warnings": warnings,
154
251
  },
155
252
  }, []