Merge remote-tracking branch 'origin/v0.8.0' into ifcviewer-wgpu

This commit is contained in:
Thomas Krijnen
2026-07-09 13:21:39 +02:00
373 changed files with 22411 additions and 4242 deletions
+14
View File
@@ -0,0 +1,14 @@
Index,Identification,Name,Unit,Value,Quantity,Query,Property,Formula
1,E.01,Walls,m3,,,,,
2,E.01.01,Ground floor walls,m3,100,,"IfcWall, location=""Ground Floor""",GrossVolume,
2,E.01.02,First floor walls,m3,200,,"IfcWall, location=""First Floor""",GrossVolume,
1,A.02,Paintings,m2,,,,,
2,A.03,Paintings with water,m2,,,,,
3,B.05,White paintings,m2,25,,IfcWall,GrossVolume,
3,B.06,Colored paintings,m2,32,33,,,
3,B.07,Double paintings,m,,,IfcWall,,NetSideArea*2
2,C-01,Paintings with machine,m2,17,133,,,
2,C-02,Decorated paintings,m2,40,8,,,
1,D,Reinforcements,,,,,,
2,D.1,Walls reinforcements weight,kg,,,IfcWall,,Pset_ConcreteElementGeneral.ReinforcementVolumeRatio * GrossVolume
2,D.2,Beams reinforcements weight,kg,,,,,
1 Index Identification Name Unit Value Quantity Query Property Formula
2 1 E.01 Walls m3
3 2 E.01.01 Ground floor walls m3 100 IfcWall, location="Ground Floor" GrossVolume
4 2 E.01.02 First floor walls m3 200 IfcWall, location="First Floor" GrossVolume
5 1 A.02 Paintings m2
6 2 A.03 Paintings with water m2
7 3 B.05 White paintings m2 25 IfcWall GrossVolume
8 3 B.06 Colored paintings m2 32 33
9 3 B.07 Double paintings m IfcWall NetSideArea*2
10 2 C-01 Paintings with machine m2 17 133
11 2 C-02 Decorated paintings m2 40 8
12 1 D Reinforcements
13 2 D.1 Walls reinforcements weight kg IfcWall Pset_ConcreteElementGeneral.ReinforcementVolumeRatio * GrossVolume
14 2 D.2 Beams reinforcements weight kg
+1
View File
@@ -39,6 +39,7 @@ See example files as a CSV file format reference:
- Ex5 - SoR_with_description.csv (a simple SoR with description column)
- Ex6 - BoQ with categories.csv (a simple BoQ with categories columns)
- Ex7 - BoQ with Rates.csv (a simple BoQ that connect to an existing SoR. It needs an already loaded SoR.)
- Ex8 - Boq with formula.csv (a simple BoQ with formula field used to calculate quantities when specified)
- `sample_cost_schedule_house_FR.csv` / `.ods`
- `schedule.csv`, `rates.csv` (schedule of rates example)
+31 -2
View File
@@ -55,6 +55,9 @@ class CsvHeader(TypedDict):
RateSchedule: NotRequired[str]
RateID: NotRequired[str]
# Formula
Formula: NotRequired[str]
#QuantityClass: NotRequired[str]
# Currently we assume that if column is not part of the main header,
# then it is a cost value category. So here we list any additional column
@@ -65,6 +68,8 @@ MAIN_CSV_HEADER_COLUMNS.extend(
# Not sure what this for but it's present in sample .csv.
"Subtotal",
# Columns from exporter.
"ItemIsASum",
"Quantities",
"RateSubtotal",
"TotalPrice",
# Deprecated columns from exporter, shouldn't be exported any longer.
@@ -91,6 +96,8 @@ class CostItem(TypedDict):
Property: Union[str, None]
Query: Union[str, None]
Formula: Union[str, None]
#QuantityClass: Union[str, None]
class Csv2Ifc:
# Inputs.
@@ -108,6 +115,7 @@ class Csv2Ifc:
categories: dict[str, int]
has_categories: bool
has_rates: bool
has_formula: bool
def __init__(
self,
@@ -163,9 +171,12 @@ class Csv2Ifc:
if not self.headers:
self.has_categories = True
self.has_rates = False
self.has_formula = False
self.headers = {col: i for i, col in enumerate(row) if col}
if "RateSchedule" in self.headers and "RateID" in self.headers:
self.has_rates = True
if "Formula" in self.headers:
self.has_formula = True
if "Value" in self.headers:
self.has_categories = False
else:
@@ -233,6 +244,11 @@ class Csv2Ifc:
else:
cost_rate = None
if self.has_formula:
cost_formula = row[(self.headers["Formula"])] if "Formula" in self.headers else None
else:
cost_formula = None
return {
"Identification": str(identification) if identification else None,
"Name": str(name) if name else None,
@@ -244,6 +260,7 @@ class Csv2Ifc:
"Query": query,
"children": [],
"CostRate": cost_rate,
"Formula": cost_formula,
}
def create_ifc(self) -> None:
@@ -320,6 +337,7 @@ class Csv2Ifc:
if cost_rate.get("Schedule") and cost_rate.get("RateID"):
# if cost_rate["Schedule"] is not "":
rate_cost_schedule = None
schedules = self.file.by_type("IfcCostSchedule")
for schedule in schedules:
if schedule.Name == cost_rate["Schedule"]:
@@ -381,17 +399,28 @@ class Csv2Ifc:
# and some query in "Query" column.
# If query is provided it will override the defined value
# due current behaviour in cost.assign_cost_item_quantity.
if results:
if results and not cost_item["Formula"]:
ifcopenshell.api.cost.assign_cost_item_quantity(
self.file,
cost_item=cost_item["ifc"],
products=results,
prop_name=prop_name,
)
elif not quantity:
elif not quantity and not cost_item["Formula"]:
quantity = ifcopenshell.api.cost.add_cost_item_quantity(
self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class
)
if cost_item["Formula"]:
results = ifcopenshell.util.selector.filter_elements(self.file, cost_item["Query"])
results = [r for r in results]
ifc_quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["Unit"])
quantity = ifcopenshell.api.cost.assign_cost_item_quantity(
self.file,
cost_item=cost_item["ifc"],
products=results,
formula=cost_item["Formula"],
ifc_class=ifc_quantity_class,
)
self.create_cost_items(cost_item["children"], cost_item["ifc"])
+17 -11
View File
@@ -21,6 +21,7 @@ from __future__ import annotations
import argparse
import datetime
import json
import logging
import os
import time
@@ -256,26 +257,31 @@ class IfcDataGetter:
return ""
if cost_item.CostQuantities is None:
return ""
string = "["
result = []
for quantity in cost_item.CostQuantities:
string += '["'
prefix = ""
for rel in file.get_inverse(quantity):
if rel.is_a("IfcPropertySet") or rel.is_a("IfcElementQuantity"):
prop_set = rel
# Find elements that have this property set
for prop_rel in file.get_inverse(prop_set):
for prop_rel in file.get_inverse(rel):
if prop_rel.is_a("IfcRelDefinesByProperties"):
for obj in prop_rel.RelatedObjects:
if obj.is_a("IfcElement"):
string += obj.Name + " - "
string += quantity.Name
prefix += (obj.Name or "") + " - "
name = prefix + (quantity.Name or "")
# Formula is an optional IfcLabel on IfcQuantity* in IFC4+; absent in
# IFC2X3, hence the schema-safe getattr.
formula = getattr(quantity, "Formula", None) or ""
if quantity.is_a("IfcPhysicalSimpleQuantity"):
string += '", ' + str(quantity[3]) + "],"
value = quantity[3]
try:
value = float(value) if value is not None else 0.0
except (TypeError, ValueError):
value = 0.0
result.append([name, value, formula])
else:
string += ' ERROR: Only IfcPhysicalSimpleQuantity is supported", 0.0],'
string = string.removesuffix(",")
string += "]"
return string
result.append([name + " ERROR: Only IfcPhysicalSimpleQuantity is supported", 0.0, formula])
return json.dumps(result, ensure_ascii=False)
class SheetData(TypedDict):
+42
View File
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import csv
import json
import tempfile
from pathlib import Path
@@ -118,3 +119,44 @@ class TestCsv2Ifc:
writer.write()
assert len(list(Path(temp_csv_dir).glob("*.ods"))) == 1
assert len(list(Path(temp_csv_dir).glob("*.xlsx"))) == 1
class TestSerialiseCostQuantities:
def test_quantity_name_with_special_characters_round_trips_as_json(self):
ifc_file = ifcopenshell.file()
name = 'Prospetto est "Np=256,667-23"'
quantity = ifc_file.create_entity("IfcQuantityArea", Name=name, AreaValue=12.5)
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
assert json.loads(result) == [[name, 12.5, ""]]
def test_unset_name_does_not_crash(self):
ifc_file = ifcopenshell.file()
# Name left unset so quantity.Name resolves to None at access time.
quantity = ifc_file.create_entity("IfcQuantityArea", AreaValue=3.0)
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
assert json.loads(result) == [["", 3.0, ""]]
def test_formula_is_included_when_present(self):
ifc_file = ifcopenshell.file()
quantity = ifc_file.create_entity("IfcQuantityArea", Name="Area", AreaValue=12.5, Formula="Length * Width")
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
assert json.loads(result) == [["Area", 12.5, "Length * Width"]]
def test_quantity_without_formula_attribute_does_not_crash(self):
# IfcPhysicalComplexQuantity has no Formula attribute and is unsupported.
ifc_file = ifcopenshell.file()
quantity = ifc_file.create_entity("IfcPhysicalComplexQuantity", Name="Complex", Discrimination="layer")
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
assert json.loads(result) == [["Complex ERROR: Only IfcPhysicalSimpleQuantity is supported", 0.0, ""]]