python-materialsdb 0.3.1__py3-none-any.whl → 0.4.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/classes.py CHANGED
@@ -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
 
materialsdb/config.py CHANGED
@@ -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
  }, []