python-materialsdb 0.3.0__tar.gz → 0.3.2__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.
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/PKG-INFO +1 -1
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/pyproject.toml +1 -1
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/cache.py +69 -16
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/listener.py +15 -4
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/ifc/material_builder.py +54 -24
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/serialiser.py +27 -12
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/python_materialsdb.egg-info/PKG-INFO +1 -1
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_bonsai_insert.py +199 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_build_bonsai_addon.py +16 -2
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_cache.py +11 -8
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_listener_payload.py +26 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_material_builder.py +46 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/LICENSE.md +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/README.md +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/setup.cfg +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/__init__.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/classes.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/config.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/construction.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/__init__.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/__main__.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/discovery.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/server.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/static/app-constructions.js +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/static/app.js +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/static/constructions.html +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/static/index.html +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/static/picker-core.js +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/ifc/__init__.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/ifc/material_psets.json +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/ifc/project_library.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/query.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/schema/MaterialsDBIndex100.xsd +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/schema/materialsdb102.xsd +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/schema/materialsdb103.xsd +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/store.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/summary.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/utils.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/python_materialsdb.egg-info/SOURCES.txt +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/python_materialsdb.egg-info/dependency_links.txt +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/python_materialsdb.egg-info/entry_points.txt +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/python_materialsdb.egg-info/requires.txt +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/python_materialsdb.egg-info/top_level.txt +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_bonsai_discovery.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_construction.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_construction_frontend.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_construction_ifc.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_gui_server.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_picker_frontend.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_project_library.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_query.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_serialiser.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_store.py +0 -0
- {python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/tests/test_summary.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-materialsdb"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
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"
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import pathlib
|
|
3
3
|
import urllib.request
|
|
4
4
|
from collections import namedtuple
|
|
5
|
+
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
|
5
6
|
|
|
6
7
|
from lxml import etree
|
|
7
8
|
|
|
@@ -85,25 +86,77 @@ def _plan_index(index):
|
|
|
85
86
|
return _IndexPlan(index, new_index, unchanged, todos)
|
|
86
87
|
|
|
87
88
|
|
|
88
|
-
def update_producers_data(url_list=MATERIALSDBINDEXURLLIST, on_progress=None):
|
|
89
|
-
"""
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
def update_producers_data(url_list=MATERIALSDBINDEXURLLIST, on_progress=None, max_workers: int = 8):
|
|
90
|
+
"""Update the cached producer files from the index URLs.
|
|
91
|
+
|
|
92
|
+
Downloads run on a bounded thread pool (max_workers). on_progress(done,
|
|
93
|
+
total, name) is called once per completed download from the calling
|
|
94
|
+
thread with a monotonic running total; returning a truthy value stops
|
|
95
|
+
NEW submissions (queued jobs are abandoned, in-flight ones finish but
|
|
96
|
+
no longer count toward the partial Report). A download error stops the
|
|
97
|
+
run and the first exception is re-raised after the pool drains.
|
|
98
|
+
"""
|
|
92
99
|
plans = [_plan_index(index) for index in url_list]
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
jobs = [
|
|
101
|
+
(plan, company, cached_producer, producer_path)
|
|
102
|
+
for plan in plans
|
|
103
|
+
for (company, cached_producer, producer_path) in plan.todos
|
|
104
|
+
]
|
|
105
|
+
total = len(jobs)
|
|
106
|
+
workers = max(1, min(int(max_workers), total or 1))
|
|
107
|
+
|
|
108
|
+
existing: list = []
|
|
109
|
+
updated: list = []
|
|
110
|
+
deleted: list = []
|
|
95
111
|
done = 0
|
|
112
|
+
cancel_requested = False
|
|
113
|
+
failure = None
|
|
114
|
+
|
|
115
|
+
def _download(job):
|
|
116
|
+
_, company, cached_producer, producer_path = job
|
|
117
|
+
deleted_path = None
|
|
118
|
+
if cached_producer is not None:
|
|
119
|
+
deleted_path = get_producers_dir() / pathlib.Path(cached_producer.get("href")).name
|
|
120
|
+
deleted_path.unlink(True)
|
|
121
|
+
urllib.request.urlretrieve(company.get("href"), producer_path)
|
|
122
|
+
return deleted_path
|
|
123
|
+
|
|
124
|
+
pool = ThreadPoolExecutor(max_workers=workers)
|
|
125
|
+
pending = list(jobs) # pops from the FRONT, submission order preserved
|
|
126
|
+
futures: dict = {}
|
|
127
|
+
try:
|
|
128
|
+
while futures or pending:
|
|
129
|
+
while pending and len(futures) < workers:
|
|
130
|
+
job = pending.pop(0)
|
|
131
|
+
futures[pool.submit(_download, job)] = job
|
|
132
|
+
if not futures:
|
|
133
|
+
break
|
|
134
|
+
ready, _ = wait(set(futures), return_when=FIRST_COMPLETED)
|
|
135
|
+
for future in ready:
|
|
136
|
+
job = futures.pop(future)
|
|
137
|
+
try:
|
|
138
|
+
deleted_path = future.result()
|
|
139
|
+
except Exception as err: # noqa: BLE001 - re-raised below, caller decides
|
|
140
|
+
if failure is None:
|
|
141
|
+
failure = err
|
|
142
|
+
break
|
|
143
|
+
if deleted_path is not None:
|
|
144
|
+
deleted.append(deleted_path)
|
|
145
|
+
updated.append(job[3])
|
|
146
|
+
done += 1
|
|
147
|
+
if on_progress is not None and on_progress(done, total, pathlib.Path(job[3]).name):
|
|
148
|
+
cancel_requested = True
|
|
149
|
+
break
|
|
150
|
+
if cancel_requested or failure is not None:
|
|
151
|
+
break
|
|
152
|
+
finally:
|
|
153
|
+
pool.shutdown(wait=True, cancel_futures=True)
|
|
154
|
+
|
|
155
|
+
if failure is not None:
|
|
156
|
+
raise failure
|
|
157
|
+
if cancel_requested:
|
|
158
|
+
return Report(existing, updated, deleted)
|
|
96
159
|
for plan in plans:
|
|
97
|
-
for company, cached_producer, producer_path in plan.todos:
|
|
98
|
-
if cached_producer is not None:
|
|
99
|
-
cached_path = get_producers_dir() / pathlib.Path(cached_producer.get("href")).name
|
|
100
|
-
deleted.append(cached_path)
|
|
101
|
-
cached_path.unlink(True)
|
|
102
|
-
urllib.request.urlretrieve(company.get("href"), producer_path)
|
|
103
|
-
updated.append(producer_path)
|
|
104
|
-
done += 1
|
|
105
|
-
if on_progress is not None and on_progress(done, total, pathlib.Path(company.get("href")).name):
|
|
106
|
-
return Report(existing, updated, deleted)
|
|
107
160
|
if plan.todos:
|
|
108
161
|
plan.new_index.write(str(get_cached_index_path(plan.index)))
|
|
109
162
|
existing.extend(plan.unchanged)
|
|
@@ -5,7 +5,7 @@ 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
9
|
|
|
10
10
|
|
|
11
11
|
def _resolved_psets(layer, country) -> dict:
|
|
@@ -50,6 +50,10 @@ def build_add_materials_payload(store_, items, country=None, lang=None):
|
|
|
50
50
|
description = str(utils.get_material_description(material, lang))
|
|
51
51
|
category = str(getattr(material.information, "group", "") or "")
|
|
52
52
|
color = getattr(material.information, "color", None)
|
|
53
|
+
color = int(color) if color else None
|
|
54
|
+
# Resolve the surface style server-side: the add-on runs in Blender's
|
|
55
|
+
# interpreter without access to the materialsdb colour scheme.
|
|
56
|
+
style_name, style_rgb = style_for(color, category)
|
|
53
57
|
entries = []
|
|
54
58
|
for layer in utils.get_material_layers(material):
|
|
55
59
|
if wanted is not None and str(layer.id) not in wanted:
|
|
@@ -63,7 +67,9 @@ def build_add_materials_payload(store_, items, country=None, lang=None):
|
|
|
63
67
|
"name": name,
|
|
64
68
|
"description": description,
|
|
65
69
|
"category": category,
|
|
66
|
-
"color":
|
|
70
|
+
"color": color,
|
|
71
|
+
"style_name": style_name,
|
|
72
|
+
"style_color": [round(component, 6) for component in style_rgb],
|
|
67
73
|
"identity": {
|
|
68
74
|
"material_id": material_id,
|
|
69
75
|
"company_id": str(summary.company_id or ""),
|
|
@@ -120,6 +126,9 @@ def build_add_construction_payload(store_, body, country=None, lang=None):
|
|
|
120
126
|
problems.append(f"unknown material id: {layer.material_id}")
|
|
121
127
|
continue
|
|
122
128
|
color = getattr(material.information, "color", None)
|
|
129
|
+
color = int(color) if color else None
|
|
130
|
+
category = str(getattr(material.information, "group", "") or "")
|
|
131
|
+
style_name, style_rgb = style_for(color, category)
|
|
123
132
|
layers.append(
|
|
124
133
|
{
|
|
125
134
|
"material_id": layer.material_id,
|
|
@@ -128,8 +137,10 @@ def build_add_construction_payload(store_, body, country=None, lang=None):
|
|
|
128
137
|
"source_id": layer.material_id,
|
|
129
138
|
"name": str(utils.get_material_name(material, lang)),
|
|
130
139
|
"description": str(utils.get_material_description(material, lang)),
|
|
131
|
-
"category":
|
|
132
|
-
"color":
|
|
140
|
+
"category": category,
|
|
141
|
+
"color": color,
|
|
142
|
+
"style_name": style_name,
|
|
143
|
+
"style_color": [round(component, 6) for component in style_rgb],
|
|
133
144
|
"identity": {
|
|
134
145
|
"material_id": layer.material_id,
|
|
135
146
|
"company_id": str(summary.company_id or ""),
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/ifc/material_builder.py
RENAMED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from typing import TypedDict
|
|
5
6
|
|
|
6
7
|
from materialsdb import config, utils
|
|
7
8
|
|
|
8
9
|
MATERIALSDB_PSET = "materialsdb"
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
class CategoryStyle(TypedDict):
|
|
13
|
+
"""One palette entry. `hatch` is reserved for future pattern support."""
|
|
14
|
+
|
|
15
|
+
hatch: str
|
|
16
|
+
color: tuple[int, int, int]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CATEGORIES: dict[str, CategoryStyle] = {
|
|
11
20
|
"Others": {"hatch": "", "color": (255, 255, 255)},
|
|
12
21
|
"Water_Proof": {"hatch": "", "color": (255, 255, 255)},
|
|
13
22
|
"Vapour_Proof": {"hatch": "", "color": (0, 0, 0)},
|
|
@@ -27,6 +36,29 @@ CATEGORIES = {
|
|
|
27
36
|
"Soil": {"hatch": "", "color": (142, 84, 52)},
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
# Named colour schemes: a category -> palette. Only the default ships today;
|
|
40
|
+
# more schemes (and a GUI selector) are planned.
|
|
41
|
+
SCHEMES: dict[str, dict[str, CategoryStyle]] = {"Lesosai": CATEGORIES}
|
|
42
|
+
DEFAULT_SCHEME = "Lesosai"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _rgb_int_components(color: int) -> tuple[float, float, float]:
|
|
46
|
+
"""Decimal 0xRRGGBB -> normalised (r, g, b) in 0..1 (IfcColourRgb units)."""
|
|
47
|
+
return (((color >> 16) & 255) / 255, ((color >> 8) & 255) / 255, (color & 255) / 255)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def style_for(color, category, scheme=None):
|
|
51
|
+
"""Resolve a material's (IfcSurfaceStyle.Name, (r, g, b) in 0..1).
|
|
52
|
+
|
|
53
|
+
The producer colour wins when present; otherwise the category colour from
|
|
54
|
+
the scheme (default 'Lesosai' = CATEGORIES). Unknown categories fall back
|
|
55
|
+
to 'Others'."""
|
|
56
|
+
if color:
|
|
57
|
+
return f"color {int(color)}", _rgb_int_components(int(color))
|
|
58
|
+
palette = SCHEMES.get(scheme or DEFAULT_SCHEME, CATEGORIES)
|
|
59
|
+
category = category if category in palette else "Others"
|
|
60
|
+
return f"category {category}", tuple(component / 255 for component in palette[category]["color"])
|
|
61
|
+
|
|
30
62
|
|
|
31
63
|
def clean_psets(psets):
|
|
32
64
|
new_dict = {}
|
|
@@ -71,6 +103,7 @@ class MaterialBuilder:
|
|
|
71
103
|
name = utils.get_material_name(material, self.lang)
|
|
72
104
|
description = utils.get_material_description(material, self.lang)
|
|
73
105
|
category = str(material.information.group or "")
|
|
106
|
+
color = material.information.color
|
|
74
107
|
if not with_layers:
|
|
75
108
|
# reuse an already-present material instead of duplicating it;
|
|
76
109
|
# its identity pset is already in place
|
|
@@ -79,15 +112,9 @@ class MaterialBuilder:
|
|
|
79
112
|
return [existing]
|
|
80
113
|
ifc_material = self.file.createIfcMaterial(name, str(description), str(category))
|
|
81
114
|
self._create_identity_pset(ifc_material, material, company_id, company, verxml)
|
|
115
|
+
self._add_style_chain(color, category)
|
|
82
116
|
return [ifc_material]
|
|
83
|
-
color
|
|
84
|
-
surface_style = self.get_surface_style(color, category)
|
|
85
|
-
styled_item = self.file.createIfcStyledItem(Styles=[surface_style])
|
|
86
|
-
self.file.createIfcStyledRepresentation(
|
|
87
|
-
ContextOfItems=self._get_context(),
|
|
88
|
-
RepresentationIdentifier="Body",
|
|
89
|
-
Items=[styled_item],
|
|
90
|
-
)
|
|
117
|
+
self._add_style_chain(color, category)
|
|
91
118
|
created = []
|
|
92
119
|
wanted = {str(g) for g in layer_ids} if layer_ids is not None else None
|
|
93
120
|
for layer in utils.get_material_layers(material):
|
|
@@ -191,29 +218,32 @@ class MaterialBuilder:
|
|
|
191
218
|
Material=ifc_material,
|
|
192
219
|
)
|
|
193
220
|
|
|
194
|
-
def
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
221
|
+
def _add_style_chain(self, color, category, scheme=None):
|
|
222
|
+
"""Create the floating styled chain MaterialBuilder uses for a
|
|
223
|
+
material's colour (shared style + one styled item per build)."""
|
|
224
|
+
surface_style = self.get_surface_style(color, category, scheme=scheme)
|
|
225
|
+
styled_item = self.file.createIfcStyledItem(Styles=[surface_style])
|
|
226
|
+
self.file.createIfcStyledRepresentation(
|
|
227
|
+
ContextOfItems=self._get_context(),
|
|
228
|
+
RepresentationIdentifier="Body",
|
|
229
|
+
Items=[styled_item],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def get_surface_style(self, color, category, scheme=None):
|
|
233
|
+
name, rgb = style_for(color, category, scheme=scheme)
|
|
201
234
|
if name in self._styles and self._styles[name] in self.file:
|
|
202
235
|
return self._styles[name]
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
SurfaceColour=self.file.createIfcColourRgb(None, *CATEGORIES[category]["color"]),
|
|
208
|
-
)
|
|
209
|
-
surface_style = self.file.createIfcSurfaceStyle(Name=name, Side="BOTH", Styles=[style])
|
|
236
|
+
shading = self.file.createIfcSurfaceStyleShading(
|
|
237
|
+
SurfaceColour=self.file.createIfcColourRgb(None, *rgb),
|
|
238
|
+
)
|
|
239
|
+
surface_style = self.file.createIfcSurfaceStyle(Name=name, Side="BOTH", Styles=[shading])
|
|
210
240
|
self._styles[name] = surface_style
|
|
211
241
|
return surface_style
|
|
212
242
|
|
|
213
243
|
def color_xml_to_ifc(self, color: int):
|
|
214
244
|
"""Color definition in xml is obscur. We assume that it is a decimal color.
|
|
215
245
|
See: https://stackoverflow.com/a/2262152/4098083"""
|
|
216
|
-
return self.file.createIfcColourRgb(
|
|
246
|
+
return self.file.createIfcColourRgb(None, *_rgb_int_components(int(color)))
|
|
217
247
|
|
|
218
248
|
|
|
219
249
|
def _materials_of(pset):
|
|
@@ -25,9 +25,31 @@ def get_xml_schema() -> str:
|
|
|
25
25
|
return str(Path(__file__).parent / "schema/materialsdb103.xsd")
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
@cache
|
|
29
|
+
def cached_type_hints(cls) -> dict:
|
|
30
|
+
return typing.get_type_hints(cls)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cache
|
|
34
|
+
def _tag_local_name(tag: str) -> str:
|
|
35
|
+
m = re.search("{.*}(.*)", tag)
|
|
36
|
+
return m.group(1) if m else tag
|
|
37
|
+
|
|
38
|
+
|
|
28
39
|
def get_element_name(element: objectify.ObjectifiedElement) -> str:
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
return _tag_local_name(element.tag)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cache
|
|
44
|
+
def _is_optional(type_hint):
|
|
45
|
+
return typing.get_origin(type_hint) is typing.Union and typing.get_args(type_hint)[1] is type(None)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cache
|
|
49
|
+
def _strip_optional(type_hint):
|
|
50
|
+
if _is_optional(type_hint):
|
|
51
|
+
return typing.get_args(type_hint)[0]
|
|
52
|
+
return type_hint
|
|
31
53
|
|
|
32
54
|
|
|
33
55
|
def create_element_maker():
|
|
@@ -49,11 +71,6 @@ def get_valid_root(tree: objectify.ObjectifiedElement) -> objectify.ObjectifiedE
|
|
|
49
71
|
return root
|
|
50
72
|
|
|
51
73
|
|
|
52
|
-
@cache
|
|
53
|
-
def cached_type_hints(cls) -> dict:
|
|
54
|
-
return typing.get_type_hints(cls)
|
|
55
|
-
|
|
56
|
-
|
|
57
74
|
class XmlDeserialiser:
|
|
58
75
|
def __init__(self):
|
|
59
76
|
self.schema = etree.XMLSchema(file=get_xml_schema())
|
|
@@ -93,7 +110,7 @@ class XmlDeserialiser:
|
|
|
93
110
|
value = element.get(attrib)
|
|
94
111
|
if value is None:
|
|
95
112
|
continue
|
|
96
|
-
base_class =
|
|
113
|
+
base_class = _strip_optional(type_hints[attrib])
|
|
97
114
|
|
|
98
115
|
kwargs[attrib] = base_class(value)
|
|
99
116
|
for child_name in getattr(element_class, "xml_elements", ()):
|
|
@@ -127,12 +144,10 @@ class XmlDeserialiser:
|
|
|
127
144
|
return instance
|
|
128
145
|
|
|
129
146
|
def strip_optional(self, type_hint):
|
|
130
|
-
|
|
131
|
-
return typing.get_args(type_hint)[0]
|
|
132
|
-
return type_hint
|
|
147
|
+
return _strip_optional(type_hint)
|
|
133
148
|
|
|
134
149
|
def is_optional(self, type_hint):
|
|
135
|
-
return
|
|
150
|
+
return _is_optional(type_hint)
|
|
136
151
|
|
|
137
152
|
def cls_name(self, name: str) -> str:
|
|
138
153
|
return name if name[0].isupper() else name.title()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import sys
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
@@ -8,6 +9,7 @@ pytest.importorskip("ifcopenshell")
|
|
|
8
9
|
import ifcopenshell
|
|
9
10
|
import ifcopenshell.api
|
|
10
11
|
import ifcopenshell.util.element
|
|
12
|
+
import ifcopenshell.util.representation
|
|
11
13
|
|
|
12
14
|
from materialsdb.gui.listener import build_add_materials_payload
|
|
13
15
|
from materialsdb.store import MaterialStore
|
|
@@ -45,6 +47,31 @@ def _payload(store_):
|
|
|
45
47
|
return payload
|
|
46
48
|
|
|
47
49
|
|
|
50
|
+
def _file_with_body_context():
|
|
51
|
+
"""A scratch IFC4 file carrying a Model/Body subcontext so _apply_style
|
|
52
|
+
proceeds (plain scratch files have no context and skip styling)."""
|
|
53
|
+
file = ifcopenshell.file(schema="IFC4")
|
|
54
|
+
model = file.create_entity(
|
|
55
|
+
"IfcGeometricRepresentationContext",
|
|
56
|
+
ContextIdentifier=None,
|
|
57
|
+
ContextType="Model",
|
|
58
|
+
CoordinateSpaceDimension=3,
|
|
59
|
+
Precision=1e-5,
|
|
60
|
+
)
|
|
61
|
+
file.create_entity(
|
|
62
|
+
"IfcGeometricRepresentationSubContext",
|
|
63
|
+
ContextIdentifier="Body",
|
|
64
|
+
ContextType="Model",
|
|
65
|
+
ParentContext=model,
|
|
66
|
+
TargetView="MODEL_VIEW",
|
|
67
|
+
)
|
|
68
|
+
return file
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _shading_of(style):
|
|
72
|
+
return next(item for item in style.Styles if item.is_a("IfcSurfaceStyleShading"))
|
|
73
|
+
|
|
74
|
+
|
|
48
75
|
def test_apply_creates_materials_psets_layers(store):
|
|
49
76
|
file = ifcopenshell.file(schema="IFC4")
|
|
50
77
|
|
|
@@ -91,6 +118,178 @@ def test_apply_add_materials_creates_schema_valid_entities(store):
|
|
|
91
118
|
assert len(identity) == 2
|
|
92
119
|
|
|
93
120
|
|
|
121
|
+
def test_apply_add_materials_writes_valid_surface_style(store):
|
|
122
|
+
"""Regression: style.add_style + add_surface_style left IfcSurfaceStyle
|
|
123
|
+
with Styles=$ when add_surface_style raised, and the malformed entity was
|
|
124
|
+
later reused via assign_material_style (crash on next model open). The
|
|
125
|
+
style must be built in one shot and land on the material."""
|
|
126
|
+
file = _file_with_body_context()
|
|
127
|
+
|
|
128
|
+
apply_add_materials(file, _payload(store))
|
|
129
|
+
|
|
130
|
+
styles = file.by_type("IfcSurfaceStyle")
|
|
131
|
+
assert styles, "the payload carries a colour, so a style must exist"
|
|
132
|
+
assert all(style.Styles for style in styles) # IFC4 Styles is SET [1:?], never NULL/empty
|
|
133
|
+
assert _shading_of(styles[0]).SurfaceColour is not None
|
|
134
|
+
|
|
135
|
+
material = file.by_type("IfcMaterial")[0]
|
|
136
|
+
assert material.HasRepresentation, "the style must be assigned to the material"
|
|
137
|
+
styled_rep = material.HasRepresentation[0].Representations[0]
|
|
138
|
+
body = ifcopenshell.util.representation.get_context(file, "Model", "Body", "MODEL_VIEW")
|
|
139
|
+
assert styled_rep.ContextOfItems == body
|
|
140
|
+
styled_items = [item for item in styled_rep.Items if item.is_a("IfcStyledItem")]
|
|
141
|
+
assert any(styles[0] in item.Styles for item in styled_items)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_apply_add_materials_shares_one_style_per_color(store):
|
|
145
|
+
file = _file_with_body_context()
|
|
146
|
+
|
|
147
|
+
apply_add_materials(file, _payload(store)) # both layers share colour 16711680
|
|
148
|
+
|
|
149
|
+
assert len(file.by_type("IfcSurfaceStyle")) == 1
|
|
150
|
+
assert len(file.by_type("IfcStyledItem")) == 2 # one assignment per material
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_apply_never_reuses_malformed_surface_style(store):
|
|
154
|
+
file = _file_with_body_context()
|
|
155
|
+
malformed = ifcopenshell.api.run("style.add_style", file, name="color 16711680")
|
|
156
|
+
assert not malformed.Styles
|
|
157
|
+
|
|
158
|
+
apply_add_materials(file, _payload(store))
|
|
159
|
+
|
|
160
|
+
assigned = [
|
|
161
|
+
item
|
|
162
|
+
for rep in file.by_type("IfcMaterialDefinitionRepresentation")
|
|
163
|
+
for representation in rep.Representations
|
|
164
|
+
for item in representation.Items
|
|
165
|
+
if item.is_a("IfcStyledItem")
|
|
166
|
+
]
|
|
167
|
+
assert assigned, "a styled item must be created"
|
|
168
|
+
assert all(malformed not in item.Styles for item in assigned)
|
|
169
|
+
assert any(style.Styles and style.Name == "color 16711680" for style in file.by_type("IfcSurfaceStyle"))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _strip_representations(file):
|
|
173
|
+
for material in file.by_type("IfcMaterial"):
|
|
174
|
+
for representation in list(material.HasRepresentation or ()):
|
|
175
|
+
file.remove(representation)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_apply_add_materials_styles_reused_material(store):
|
|
179
|
+
"""A material already present (identity pset) but colourless -- e.g. pushed
|
|
180
|
+
by the pre-fix add-on -- must get its style on a later push."""
|
|
181
|
+
file = _file_with_body_context()
|
|
182
|
+
payload = _payload(store)
|
|
183
|
+
apply_add_materials(file, payload)
|
|
184
|
+
_strip_representations(file)
|
|
185
|
+
assert not file.by_type("IfcMaterialDefinitionRepresentation")
|
|
186
|
+
|
|
187
|
+
assert apply_add_materials(file, payload) == 0 # nothing re-created
|
|
188
|
+
|
|
189
|
+
for material in file.by_type("IfcMaterial"):
|
|
190
|
+
assert material.HasRepresentation
|
|
191
|
+
assert [style.Name for style in file.by_type("IfcSurfaceStyle")] == ["color 16711680"]
|
|
192
|
+
assert all(style.Styles for style in file.by_type("IfcSurfaceStyle"))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_apply_add_construction_styles_reused_material(store):
|
|
196
|
+
file = _file_with_body_context()
|
|
197
|
+
apply_add_materials(file, _payload(store)) # Isolant A already in the model
|
|
198
|
+
_strip_representations(file)
|
|
199
|
+
|
|
200
|
+
summary = apply_add_construction(file, CONSTRUCTION_PAYLOAD)
|
|
201
|
+
|
|
202
|
+
assert summary["materials_created"] == 1 # Beton B only; Isolant A reused
|
|
203
|
+
isolant = next(material for material in file.by_type("IfcMaterial") if material.Name == "Isolant A")
|
|
204
|
+
assert isolant.HasRepresentation
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_apply_add_material_uses_category_scheme_colour(store):
|
|
208
|
+
from materialsdb.ifc.material_builder import CATEGORIES
|
|
209
|
+
|
|
210
|
+
file = _file_with_body_context()
|
|
211
|
+
payload, missing = build_add_materials_payload(store, [{"id": "00000000-0000-0000-0000-000000000002"}])
|
|
212
|
+
assert missing == [] # Beton B carries no producer colour
|
|
213
|
+
|
|
214
|
+
apply_add_materials(file, payload)
|
|
215
|
+
|
|
216
|
+
styles = file.by_type("IfcSurfaceStyle")
|
|
217
|
+
assert [style.Name for style in styles] == ["category Concrete"]
|
|
218
|
+
colour = _shading_of(styles[0]).SurfaceColour
|
|
219
|
+
expected = CATEGORIES["Concrete"]["color"]
|
|
220
|
+
assert (round(colour.Red * 255), round(colour.Green * 255), round(colour.Blue * 255)) == expected
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_apply_preserves_third_party_style_on_reused_material(store):
|
|
224
|
+
file = _file_with_body_context()
|
|
225
|
+
payload = _payload(store)
|
|
226
|
+
apply_add_materials(file, payload)
|
|
227
|
+
material = file.by_type("IfcMaterial")[0]
|
|
228
|
+
styled_item = next(
|
|
229
|
+
item
|
|
230
|
+
for representation in material.HasRepresentation
|
|
231
|
+
for styled in representation.Representations
|
|
232
|
+
for item in styled.Items
|
|
233
|
+
if item.is_a("IfcStyledItem")
|
|
234
|
+
)
|
|
235
|
+
custom = ifcopenshell.api.run("style.add_style", file, name="MyCustom")
|
|
236
|
+
ifcopenshell.api.run(
|
|
237
|
+
"style.add_surface_style",
|
|
238
|
+
file,
|
|
239
|
+
style=custom,
|
|
240
|
+
ifc_class="IfcSurfaceStyleShading",
|
|
241
|
+
attributes={"SurfaceColour": {"Name": None, "Red": 0.1, "Green": 0.2, "Blue": 0.3}},
|
|
242
|
+
)
|
|
243
|
+
styled_item.Styles = (custom,)
|
|
244
|
+
|
|
245
|
+
apply_add_materials(file, payload) # existing key -> would re-style
|
|
246
|
+
|
|
247
|
+
assert styled_item.Styles[0].Name == "MyCustom" # third-party style kept
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_apply_updates_our_style_on_colour_change(store):
|
|
251
|
+
file = _file_with_body_context()
|
|
252
|
+
payload = _payload(store)
|
|
253
|
+
apply_add_materials(file, payload)
|
|
254
|
+
material = file.by_type("IfcMaterial")[0]
|
|
255
|
+
|
|
256
|
+
changed = copy.deepcopy(payload)
|
|
257
|
+
for entry in changed["materials"]:
|
|
258
|
+
entry["color"] = 255
|
|
259
|
+
entry["style_name"] = "color 255"
|
|
260
|
+
entry["style_color"] = [0.0, 0.0, 1.0]
|
|
261
|
+
apply_add_materials(file, changed)
|
|
262
|
+
|
|
263
|
+
styled_item = next(
|
|
264
|
+
item
|
|
265
|
+
for representation in material.HasRepresentation
|
|
266
|
+
for styled in representation.Representations
|
|
267
|
+
for item in styled.Items
|
|
268
|
+
if item.is_a("IfcStyledItem")
|
|
269
|
+
)
|
|
270
|
+
assert styled_item.Styles[0].Name == "color 255" # our style refreshed
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_apply_styling_idempotent(store):
|
|
274
|
+
file = _file_with_body_context()
|
|
275
|
+
payload = _payload(store)
|
|
276
|
+
|
|
277
|
+
apply_add_materials(file, payload)
|
|
278
|
+
counts = (
|
|
279
|
+
len(file.by_type("IfcSurfaceStyle")),
|
|
280
|
+
len(file.by_type("IfcStyledItem")),
|
|
281
|
+
len(file.by_type("IfcMaterialDefinitionRepresentation")),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
apply_add_materials(file, payload)
|
|
285
|
+
|
|
286
|
+
assert (
|
|
287
|
+
len(file.by_type("IfcSurfaceStyle")),
|
|
288
|
+
len(file.by_type("IfcStyledItem")),
|
|
289
|
+
len(file.by_type("IfcMaterialDefinitionRepresentation")),
|
|
290
|
+
) == counts
|
|
291
|
+
|
|
292
|
+
|
|
94
293
|
def test_apply_is_idempotent(store):
|
|
95
294
|
file = ifcopenshell.file(schema="IFC4")
|
|
96
295
|
payload = _payload(store)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
import sys
|
|
2
3
|
import zipfile
|
|
3
4
|
from pathlib import Path
|
|
@@ -8,13 +9,22 @@ sys.path.insert(0, str(ROOT / "dev_utils"))
|
|
|
8
9
|
import build_bonsai_addon # ty: ignore[unresolved-import] - dev_util on a side path
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
def _manifest_version():
|
|
13
|
+
text = (ROOT / "bonsai_addon" / "blender_manifest.toml").read_text(encoding="utf-8")
|
|
14
|
+
match = re.search(r'(?m)^version = "([^"]+)"$', text)
|
|
15
|
+
assert match is not None
|
|
16
|
+
return match.group(1)
|
|
17
|
+
|
|
18
|
+
|
|
11
19
|
def test_default_build_unchanged(tmp_path):
|
|
20
|
+
manifest_version = _manifest_version()
|
|
21
|
+
|
|
12
22
|
out = build_bonsai_addon.build(out_dir=tmp_path)
|
|
13
23
|
|
|
14
24
|
assert out.name == "materialsdb_listener.zip"
|
|
15
25
|
with zipfile.ZipFile(out) as zf:
|
|
16
26
|
assert "blender_manifest.toml" in zf.namelist()
|
|
17
|
-
assert 'version = "
|
|
27
|
+
assert f'version = "{manifest_version}"' in zf.read("blender_manifest.toml").decode("utf-8")
|
|
18
28
|
assert "blender_manifest.toml".replace(".", "/") not in zf.namelist() # manifest at zip ROOT
|
|
19
29
|
|
|
20
30
|
|
|
@@ -30,8 +40,12 @@ def test_version_injected_into_manifest_and_name(tmp_path):
|
|
|
30
40
|
def test_version_injection_is_literal_independent(tmp_path):
|
|
31
41
|
"""The injection must not depend on the manifest's current version
|
|
32
42
|
literal (it changes on every release bump)."""
|
|
43
|
+
|
|
44
|
+
current = _manifest_version()
|
|
45
|
+
stale = "0.0.1" if current != "0.0.1" else "0.0.2"
|
|
46
|
+
|
|
33
47
|
out = build_bonsai_addon.build(version="3.2.1", out_dir=tmp_path)
|
|
34
48
|
|
|
35
49
|
with zipfile.ZipFile(out) as zf:
|
|
36
50
|
assert 'version = "3.2.1"' in zf.read("blender_manifest.toml").decode("utf-8")
|
|
37
|
-
assert 'version = "
|
|
51
|
+
assert f'version = "{stale}"' not in zf.read("blender_manifest.toml").decode("utf-8")
|
|
@@ -33,7 +33,7 @@ def cache_env(tmp_path, monkeypatch):
|
|
|
33
33
|
# defaults are bound at def time, so patch __defaults__ too
|
|
34
34
|
single = [cache.MATERIALSDBINDEXURLLIST[0]]
|
|
35
35
|
monkeypatch.setattr(cache, "MATERIALSDBINDEXURLLIST", single)
|
|
36
|
-
monkeypatch.setattr(cache.update_producers_data, "__defaults__", (single, None))
|
|
36
|
+
monkeypatch.setattr(cache.update_producers_data, "__defaults__", (single, None, 8))
|
|
37
37
|
monkeypatch.setattr(cache.updates_available, "__defaults__", (single,))
|
|
38
38
|
recorded: _Recorded = {"urls": [], "dests": [], "index": INDEX_XML}
|
|
39
39
|
|
|
@@ -57,9 +57,11 @@ def test_first_refresh_downloads_all_with_global_total(cache_env):
|
|
|
57
57
|
on_progress=lambda done, total, name: progress.append((done, total, name)) or False
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
assert recorded["urls"] ==
|
|
61
|
-
assert progress == [
|
|
62
|
-
assert [
|
|
60
|
+
assert set(recorded["urls"]) == {"https://x.org/a.xml", "https://x.org/b.xml"}
|
|
61
|
+
assert [p[0] for p in progress] == [1, 2] # monotonic done
|
|
62
|
+
assert {p[1] for p in progress} == {2} # global total reached before any download finished
|
|
63
|
+
assert {p[2] for p in progress} == {"a.xml", "b.xml"}
|
|
64
|
+
assert {p.name for p in report.updated} == {"a.xml", "b.xml"}
|
|
63
65
|
assert report.existing == []
|
|
64
66
|
assert (cache_dir / "Producers" / "a.xml").exists() # downloads land in Producers dir
|
|
65
67
|
assert (cache_dir / "ProducerIndex.xml").exists() # new index persisted
|
|
@@ -68,7 +70,7 @@ def test_first_refresh_downloads_all_with_global_total(cache_env):
|
|
|
68
70
|
def test_second_refresh_uses_existing_files(cache_env):
|
|
69
71
|
cache, _, recorded = cache_env
|
|
70
72
|
report = cache.update_producers_data()
|
|
71
|
-
assert
|
|
73
|
+
assert {p.name for p in report.updated} == {"a.xml", "b.xml"}
|
|
72
74
|
recorded["urls"].clear()
|
|
73
75
|
|
|
74
76
|
progress = []
|
|
@@ -100,9 +102,9 @@ def test_outdated_single_producer_updates_with_total_one(cache_env):
|
|
|
100
102
|
on_progress=lambda done, total, name: progress.append((done, total, name)) or False
|
|
101
103
|
)
|
|
102
104
|
|
|
103
|
-
assert recorded["urls"] == ["https://x.org/a.xml"]
|
|
105
|
+
assert recorded["urls"] == ["https://x.org/a.xml"] # single job — deterministic
|
|
104
106
|
assert progress == [(1, 1, "a.xml")]
|
|
105
|
-
assert
|
|
107
|
+
assert {p.name for p in record.updated} == {"a.xml"}
|
|
106
108
|
assert [p.name for p in record.deleted] == ["a.xml"] # stale file removed before re-download
|
|
107
109
|
assert [p.name for p in record.existing] == ["b.xml"]
|
|
108
110
|
|
|
@@ -128,7 +130,8 @@ def test_cancel_stops_between_downloads(cache_env):
|
|
|
128
130
|
def cancel_after_first(done, total, name):
|
|
129
131
|
return done >= 1 # truthy → abort
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
# single worker: b.html cannot start before the callback runs
|
|
134
|
+
report = cache.update_producers_data(on_progress=cancel_after_first, max_workers=1)
|
|
132
135
|
|
|
133
136
|
assert recorded["urls"] == ["https://x.org/a.xml"] # second download never starts
|
|
134
137
|
assert [p.name for p in report.updated] == ["a.xml"]
|
|
@@ -260,3 +260,29 @@ def test_construction_payload_no_u_values_when_lambda_unresolvable(store):
|
|
|
260
260
|
|
|
261
261
|
assert payload is None
|
|
262
262
|
assert problems == ["unknown material id: 00000000-0000-0000-0000-000000000004"]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_payload_carries_resolved_producer_style(store):
|
|
266
|
+
payload, missing = build_add_materials_payload(store, [{"id": "00000000-0000-0000-0000-000000000001"}])
|
|
267
|
+
|
|
268
|
+
assert missing == []
|
|
269
|
+
first = payload["materials"][0]
|
|
270
|
+
assert first["style_name"] == "color 16711680" # producer colour wins
|
|
271
|
+
assert first["style_color"] == [1.0, 0.0, 0.0] # normalised 0xFF0000
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_construction_payload_carries_category_scheme_style(store):
|
|
275
|
+
from materialsdb.ifc.material_builder import CATEGORIES
|
|
276
|
+
|
|
277
|
+
payload, problems = build_add_construction_payload(
|
|
278
|
+
store,
|
|
279
|
+
body={
|
|
280
|
+
"name": "wall",
|
|
281
|
+
"layers": [{"material_id": "00000000-0000-0000-0000-000000000002", "thickness_m": 0.15}],
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
assert problems == []
|
|
286
|
+
material = payload["construction"]["layers"][0]["material"]
|
|
287
|
+
assert material["style_name"] == "category Concrete" # no producer colour -> scheme
|
|
288
|
+
assert material["style_color"] == [round(component / 255, 6) for component in CATEGORIES["Concrete"]["color"]]
|
|
@@ -303,3 +303,49 @@ def test_org_layer_pset_exposes_layer_id_and_thickness(mini_source):
|
|
|
303
303
|
"00000000-0000-0000-0000-0000000000a1": 0.2, # 200 mm -> meters
|
|
304
304
|
"00000000-0000-0000-0000-0000000000a2": 0.1,
|
|
305
305
|
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_style_for_prefers_producer_colour_and_normalises():
|
|
309
|
+
from materialsdb.ifc.material_builder import style_for
|
|
310
|
+
|
|
311
|
+
name, rgb = style_for(16711680, "Insulation") # 0xFF0000
|
|
312
|
+
|
|
313
|
+
assert name == "color 16711680"
|
|
314
|
+
assert rgb == (1.0, 0.0, 0.0) # IfcColourRgb components are normalised 0..1
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_style_for_falls_back_to_scheme_category_colour():
|
|
318
|
+
from materialsdb.ifc.material_builder import CATEGORIES, style_for
|
|
319
|
+
|
|
320
|
+
name, rgb = style_for(None, "Concrete")
|
|
321
|
+
|
|
322
|
+
assert name == "category Concrete"
|
|
323
|
+
assert rgb == tuple(component / 255 for component in CATEGORIES["Concrete"]["color"])
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_style_for_unknown_category_falls_back_to_others():
|
|
327
|
+
from materialsdb.ifc.material_builder import CATEGORIES, style_for
|
|
328
|
+
|
|
329
|
+
name, rgb = style_for(None, "Air") # not a scheme category
|
|
330
|
+
|
|
331
|
+
assert name == "category Others"
|
|
332
|
+
assert rgb == tuple(component / 255 for component in CATEGORIES["Others"]["color"])
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_build_without_layers_styles_the_material(mini_source):
|
|
336
|
+
"""The to_ifc_layer_set path builds materials with with_layers=False; they
|
|
337
|
+
must carry a colour too, or exported constructions show no style."""
|
|
338
|
+
file = ifcopenshell.file(schema="IFC4")
|
|
339
|
+
builder = MaterialBuilder(file)
|
|
340
|
+
|
|
341
|
+
created = builder.build(mini_source.material[0], company="Mini SA", with_layers=False)
|
|
342
|
+
|
|
343
|
+
assert len(created) == 1
|
|
344
|
+
assert not file.by_type("IfcMaterialLayer") # still material-only
|
|
345
|
+
styles = file.by_type("IfcSurfaceStyle")
|
|
346
|
+
assert [style.Name for style in styles] == ["color 16711680"] # producer colour wins
|
|
347
|
+
assert all(style.Styles for style in styles)
|
|
348
|
+
colour = styles[0].Styles[0].SurfaceColour
|
|
349
|
+
assert (colour.Red, colour.Green, colour.Blue) == (1.0, 0.0, 0.0)
|
|
350
|
+
assert len(file.by_type("IfcStyledItem")) == 1
|
|
351
|
+
assert len(file.by_type("IfcStyledRepresentation")) == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/static/constructions.html
RENAMED
|
File without changes
|
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/gui/static/picker-core.js
RENAMED
|
File without changes
|
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/ifc/material_psets.json
RENAMED
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/ifc/project_library.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/schema/MaterialsDBIndex100.xsd
RENAMED
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/schema/materialsdb102.xsd
RENAMED
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/materialsdb/schema/materialsdb103.xsd
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/python_materialsdb.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/python_materialsdb.egg-info/requires.txt
RENAMED
|
File without changes
|
{python_materialsdb-0.3.0 → python_materialsdb-0.3.2}/src/python_materialsdb.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|