Merge branch 'v0.8.0' into ifcmax/initial-refresh

This commit is contained in:
Josef Wienerroither
2026-01-24 09:58:11 +01:00
35 changed files with 23657 additions and 23547 deletions
+2
View File
@@ -1177,6 +1177,8 @@ if "cgal" in targets:
# Disable assembly, otherwise `emcc -c conftest.s` will crash due to assembly mismatch.
gmp_args.extend(("--disable-assembly", "--enable-cxx"))
mpfr_args.extend(("--host", "none"))
elif "x86" in arch:
gmp_args.append("--enable-fat") # See issues #7458 #7556
OLD_CC = None
if MAC_CROSS_COMPILE_INTEL:
+2 -2
View File
@@ -2,8 +2,8 @@
name = "IfcOpenShell"
version = "0.0.0"
dependencies = [
"black==25.12",
"ruff==0.14.13",
"black==26.1.0",
"ruff==0.14.14",
"poethepoet",
"gersemi==0.25.1",
]
+3 -13
View File
@@ -368,21 +368,11 @@ class BIM_PT_new_project_wizard(Panel):
row = self.layout.row()
row.prop(props, "volume_unit", text="Volume Unit")
row = self.layout.row()
row.prop(props, "mass_unit", text="Mass Unit")
row = self.layout.row()
row.prop(props, "time_unit", text="Time Unit")
prop_with_search(self.layout, pprops, "template_file", text="Template")
if tool.Blender.get_addon_preferences().mass_time_units_in_wizard:
header, body = self.layout.panel("Mass and Time Units", default_closed=True)
if header:
header.label(text="Mass and Time Units")
if body:
label = "Add Mass and Time Units" if not props.add_mass_time_units else "Remove Mass and Time Units"
body.prop(props, "add_mass_time_units", toggle=True, text=label)
if props.add_mass_time_units:
row = body.row()
row.prop(props, "mass_unit", text="Mass Unit")
row = body.row()
row.prop(props, "time_unit", text="Time Unit")
self.layout.use_property_split = True
row = self.layout.row()
row.operator("bim.create_project")
@@ -569,7 +569,7 @@ class SavePsetAsTemplate(bpy.types.Operator, tool.PsetTemplate.PsetTemplateOpera
template_file = IfcStore.pset_template_file
assert template_file
tool.PsetTemplate.add_pset_as_template(pset, template_file)
tool.PsetTemplate.add_pset_as_template(pset.Name, template_file)
template_file.write(IfcStore.pset_template_path)
bonsai.bim.handler.refresh_ui_data()
+13 -14
View File
@@ -589,6 +589,7 @@ class BIMProperties(PropertyGroup):
area_unit: EnumProperty(
default="SQUARE_METRE",
items=[
("NONE", "None", ""),
("NANO/SQUARE_METRE", "Square Nanometre", ""),
("MICRO/SQUARE_METRE", "Square Micrometre", ""),
("MILLI/SQUARE_METRE", "Square Millimetre", ""),
@@ -606,6 +607,7 @@ class BIMProperties(PropertyGroup):
volume_unit: EnumProperty(
default="CUBIC_METRE",
items=[
("NONE", "None", ""),
("NANO/CUBIC_METRE", "Cubic Nanometre", ""),
("MICRO/CUBIC_METRE", "Cubic Micrometre", ""),
("MILLI/CUBIC_METRE", "Cubic Millimetre", ""),
@@ -619,31 +621,28 @@ class BIMProperties(PropertyGroup):
],
name="IFC Volume Unit",
)
add_mass_time_units: bpy.props.BoolProperty(
name="Add Mass and Time Units",
description="Enable to define mass and time units for the project",
default=False,
)
mass_unit: EnumProperty(
items=[
("KILOGRAM", "Kilogram", "Kilograms"),
("NONE", "None", ""),
("GRAM", "Gram", "Grams"),
("POUND", "Pound", "Pounds"),
("OUNCE", "Ounce", "Ounces"),
("TONNE", "Tonne", "Metric Tons"),
("KILO/KILOGRAM", "Kilogram", "Kilograms"),
("MEGA/TONNE", "Tonne", "Metric Tons"),
("pound", "Pound", "Pounds"),
("ounce", "Ounce", "Ounces"),
],
name="Mass Unit",
default="KILOGRAM",
default="NONE",
)
time_unit: EnumProperty(
items=[
("NONE", "None", ""),
("SECOND", "Second", "Seconds"),
("MINUTE", "Minute", "Minutes"),
("HOUR", "Hour", "Hours"),
("DAY", "Day", "Days"),
("minute", "Minute", "Minutes"),
("hour", "Hour", "Hours"),
("day", "Day", "Days"),
],
name="Time Unit",
default="HOUR",
default="NONE",
)
tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities")
active_tab_visibility_index: IntProperty(name="Active Tab Visibility Index")
-6
View File
@@ -717,12 +717,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
default=False,
)
mass_time_units_in_wizard: BoolProperty(
name="Mass and time units in project wizard",
description="Show mass and time units section in the new project wizard panel",
default=False,
)
chain_filter_with_set_operations: BoolProperty(
name="NEW Filter mode: Enable chained filters with set operations",
description="Enable chaining search filters with set operations: ADD (union: combine sets), SUBTRACT (difference: remove from set), FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values",
+1 -1
View File
@@ -1124,7 +1124,7 @@ class Unit:
def enable_editing_units(cls): pass
def export_unit_attributes(cls): pass
def get_scene_unit_name(cls, unit_type): pass
def get_scene_unit_si_prefix(cls, unit_type): pass
def get_scene_unit_si_prefix(cls, name): pass
def import_unit_attributes(cls, unit): pass
def import_units(cls): pass
def is_scene_unit_metric(cls): pass
+10 -39
View File
@@ -27,46 +27,17 @@ if TYPE_CHECKING:
def assign_scene_units(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> None:
if unit.is_scene_unit_metric():
lengthunit = ifc.run(
"unit.add_si_unit", unit_type="LENGTHUNIT", prefix=unit.get_scene_unit_si_prefix("LENGTHUNIT")
)
areaunit = ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=unit.get_scene_unit_si_prefix("AREAUNIT"))
volumeunit = ifc.run(
"unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=unit.get_scene_unit_si_prefix("VOLUMEUNIT")
)
planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree")
units = [lengthunit, areaunit, volumeunit, planeangleunit]
if unit.add_mass_and_time_units():
prefix = unit.get_scene_unit_si_prefix("MASSUNIT")
if prefix == "CONVERSION":
massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT").lower())
units = []
for unit_type in ["LENGTHUNIT", "AREAUNIT", "VOLUMEUNIT", "MASSUNIT", "TIMEUNIT"]:
if name := unit.get_scene_unit_name(unit_type):
if unit.is_si_unit(name):
units.append(
ifc.run("unit.add_si_unit", unit_type=unit_type, prefix=unit.get_scene_unit_si_prefix(name))
)
else:
massunit = ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix=prefix)
prefix = unit.get_scene_unit_si_prefix("TIMEUNIT")
if prefix == "CONVERSION":
timeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("TIMEUNIT").lower())
else:
timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=prefix)
units += [massunit, timeunit]
else:
lengthunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("LENGTHUNIT"))
areaunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("AREAUNIT"))
volumeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("VOLUMEUNIT"))
planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree")
units = [lengthunit, areaunit, volumeunit, planeangleunit]
if unit.add_mass_and_time_units():
massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT").lower())
time_unit_name = unit.get_scene_unit_name("TIMEUNIT")
if time_unit_name == "SECOND":
timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None)
else:
timeunit = ifc.run("unit.add_conversion_based_unit", name=time_unit_name.lower())
units += [massunit, timeunit]
ifc.run("unit.assign_unit", units=units)
units.append(ifc.run("unit.add_conversion_based_unit", name=name))
if units:
ifc.run("unit.assign_unit", units=units)
def assign_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None:
+17 -13
View File
@@ -61,19 +61,23 @@ class PsetTemplate(bonsai.core.tool.PsetTemplate):
tool.Ifc.Operator._execute(self, context)
@classmethod
def add_pset_as_template(
cls, pset: ifcopenshell.entity_instance, template_file: ifcopenshell.file
) -> ifcopenshell.entity_instance:
# TODO: add tests.
pset_template = ifcopenshell.api.pset_template.add_pset_template(template_file, pset.Name)
for property in pset.HasProperties:
ifcopenshell.api.pset_template.add_prop_template(
template_file,
pset_template,
name=property.Name,
description=property.Description,
primary_measure_type=property.NominalValue.is_a(),
)
def add_pset_as_template(cls, pset_name: str, template_file: ifcopenshell.file) -> ifcopenshell.entity_instance:
added_prop_names = set()
pset_template = ifcopenshell.api.pset_template.add_pset_template(template_file, pset_name)
for pset in tool.Ifc.get().by_type("IfcPropertySet"):
if pset.Name != pset_name:
continue
for prop in pset.HasProperties:
if prop.Name in added_prop_names:
continue
added_prop_names.add(prop.Name)
ifcopenshell.api.pset_template.add_prop_template(
template_file,
pset_template,
name=prop.Name,
description=prop.Description,
primary_measure_type=prop.NominalValue.is_a(),
)
return pset_template
@classmethod
+17 -63
View File
@@ -330,66 +330,26 @@ class Unit(bonsai.core.tool.Unit):
return bonsai.bim.helper.export_attributes(props.unit_attributes, callback=callback)
@classmethod
def get_scene_unit_name(cls, unit_type: UNIT_TYPE) -> str:
bim_props = tool.Blender.get_bim_props()
def get_scene_unit_name(cls, unit_type: UNIT_TYPE) -> str | None:
if unit_type == "LENGTHUNIT":
assert bpy.context.scene
props = bpy.context.scene.unit_settings
if props.length_unit == "MILES":
return "mile"
elif props.length_unit == "FEET" or props.length_unit == "ADAPTIVE":
return "foot"
elif props.length_unit == "INCHES":
return "inch"
elif props.length_unit == "THOU":
return "thou"
return "foot"
elif unit_type == "AREAUNIT":
return bim_props.area_unit
elif unit_type == "VOLUMEUNIT":
return bim_props.volume_unit
elif unit_type == "MASSUNIT":
return bim_props.mass_unit.lower()
elif unit_type == "TIMEUNIT":
return bim_props.time_unit.lower()
else:
assert_never(unit_type)
name = bpy.context.scene.unit_settings.length_unit
name = {"MILES": "mile", "FEET": "foot", "INCHES": "inch", "THOU": "thou", "ADAPTIVE": "METERS"}.get(
name, name
)
if len(name) > len("METERS") and name.endswith("METERS"):
return f"{name[:-6]}/METRE"
return name
bim_props = tool.Blender.get_bim_props()
if (name := getattr(bim_props, f"{unit_type[:-4].lower()}_unit")) != "NONE":
return name
@classmethod
def get_scene_unit_si_prefix(cls, unit_type: UNIT_TYPE) -> Union[str, None]:
bim_props = tool.Blender.get_bim_props()
if unit_type == "LENGTHUNIT":
assert bpy.context.scene
props = bpy.context.scene.unit_settings
if props.length_unit == "ADAPTIVE" or props.length_unit == "METERS":
return
return props.length_unit.replace("METERS", "")
elif unit_type == "AREAUNIT":
unit = bim_props.area_unit
elif unit_type == "VOLUMEUNIT":
unit = bim_props.volume_unit
elif unit_type == "MASSUNIT":
unit = bim_props.mass_unit
if unit == "GRAM":
return None
elif unit == "KILOGRAM":
return "KILO"
elif unit == "TONNE":
return "MEGA"
elif unit in ["POUND", "OUNCE"]:
return "CONVERSION"
else:
return None
elif unit_type == "TIMEUNIT":
unit = bim_props.time_unit
if unit == "SECOND":
return None
else:
return "CONVERSION"
else:
assert_never(unit_type)
if "/" in unit:
return unit.split("/")[0]
def is_si_unit(cls, name: str) -> bool:
return name[0].isupper()
@classmethod
def get_scene_unit_si_prefix(cls, name: str) -> str | None:
return name.split("/")[0] if "/" in name else None
@classmethod
def import_unit_attributes(cls, unit: ifcopenshell.entity_instance) -> None:
@@ -504,9 +464,3 @@ class Unit(bonsai.core.tool.Unit):
elif ifc_class == "IfcMonetaryUnit":
return "COPY_ID"
return "MOD_MESHDEFORM"
@classmethod
def add_mass_and_time_units(cls) -> bool:
"""Return True if the user wants to add mass and time units, False otherwise."""
bim_props = tool.Blender.get_bim_props()
return getattr(bim_props, "add_mass_time_units", False)
@@ -0,0 +1,48 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.tool.pset_template import PsetTemplate as subject
from test.bim.bootstrap import NewFile
class TestImplementsTool(NewFile):
def test_run(self):
assert isinstance(subject(), bonsai.core.tool.PsetTemplate)
class TestAddPsetAsTemplate(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element1 = ifc.createIfcWall()
element2 = ifc.createIfcWall()
pset = ifcopenshell.api.pset.add_pset(ifc, product=element1, name="Foo")
prop = ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"Foo": "a"})
pset = ifcopenshell.api.pset.add_pset(ifc, product=element2, name="Foo")
prop = ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"Bar": "b"})
library = ifcopenshell.file()
assert (pset_template := subject.add_pset_as_template("Foo", library))
assert pset_template.is_a("IfcPropertySetTemplate")
assert len(templates := pset_template.HasPropertyTemplates) == 2
assert set(t.Name for t in templates) == {"Foo", "Bar"}
+5 -1
View File
@@ -31,12 +31,16 @@ find_package(Eigen3 REQUIRED)
target_link_libraries(
IfcGeom
${kernel_libraries}
${mapping_libraries}
${CMAKE_THREAD_LIBS_INIT}
"Eigen3::Eigen"
${CGAL_LIBRARIES}
)
if(WITH_OPENCASCADE)
target_link_libraries(IfcGeom TKernel)
endif()
if(NOT WASM_BUILD)
target_link_libraries(IfcGeom IfcParse)
endif()
+1 -1
View File
@@ -91,7 +91,7 @@ bool IfcGeom::Representation::BRep::calculate_volume(double& volume) const {
volume = 0.;
return false;
}
volume = s->area()->to_double();
volume = s->volume()->to_double();
return true;
}
+8 -3
View File
@@ -8,13 +8,13 @@ foreach(kernel ${GEOMETRY_KERNELS})
set(KERNEL_TARGET "geometry_kernel_${kernel}")
add_library(${KERNEL_TARGET} OBJECT ${IFCGEOM_FILES})
add_library(${KERNEL_TARGET} ${IFCGEOM_FILES})
set_target_properties(
${KERNEL_TARGET}
PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS" PUBLIC_HEADER "${IFCGEOM_H_FILES}"
)
list(APPEND kernel_libraries ${KERNEL_TARGET})
target_link_libraries(${KERNEL_TARGET} ${${KERNEL_UPPER}_LIBRARIES} Eigen3::Eigen)
target_link_libraries(${KERNEL_TARGET} ${${KERNEL_UPPER}_LIBRARIES} IfcGeom Eigen3::Eigen)
install(
TARGETS ${KERNEL_TARGET}
EXPORT ${IFCOPENSHELL_EXPORT_TARGETS}
@@ -27,7 +27,7 @@ foreach(kernel ${GEOMETRY_KERNELS})
set_property(TARGET ${KERNEL_TARGET} APPEND_STRING PROPERTY COMPILE_FLAGS " -DCGAL_HAS_THREADS")
set(KERNEL_TARGET_SIMPLE "${KERNEL_TARGET}_simple")
add_library(${KERNEL_TARGET_SIMPLE} OBJECT ${IFCGEOM_FILES})
add_library(${KERNEL_TARGET_SIMPLE} ${IFCGEOM_FILES})
set_target_properties(
${KERNEL_TARGET_SIMPLE}
PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIFOPSH_SIMPLE_KERNEL -DCGAL_HAS_THREADS"
@@ -42,3 +42,8 @@ foreach(kernel ${GEOMETRY_KERNELS})
endforeach()
set(kernel_libraries ${kernel_libraries} PARENT_SCOPE)
install(
FILES ifc_geomlibrary_api.h
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/ifcgeom/kernels/"
)
+4
View File
@@ -1835,6 +1835,9 @@ bool CgalKernel::convert_impl(const taxonomy::boolean_result::ptr br, Conversion
CGAL::Polygon_with_holes_2<Kernel_> pwh(p, ++it, loops.end());
CGAL::Gps_segment_traits_2<Kernel_> traits;
if (!CGAL::are_holes_and_boundary_pairwise_disjoint(pwh, traits)) {
#ifdef IFOPSH_SIMPLE_KERNEL
throw std::runtime_error("Holes are not disjoint - use a different geometry kernel");
#else
// this is very slow.
// the check is also slow...
@@ -1851,6 +1854,7 @@ bool CgalKernel::convert_impl(const taxonomy::boolean_result::ptr br, Conversion
result.difference(*it);
}
result.polygons_with_holes(std::back_inserter(pwhs));
#endif
} else {
pwhs.push_back(pwh);
}
+29 -12
View File
@@ -1792,6 +1792,10 @@ namespace IfcGeom {
auto& vs = elem->geometry().verts();
auto& fs = elem->geometry().faces();
if (vs.empty() || fs.empty()) {
return;
}
gp_Trsf tr;
tr.SetValues(
m(0, 0), m(0, 1), m(0, 2), m(0, 3),
@@ -1859,23 +1863,36 @@ namespace IfcGeom {
candidates.push_back({ std::abs(Z.Dot(ref)), ref });
}
if (candidates.empty()) {
{
gp_XYZ ref(0, 0, 1);
candidates.push_back({ std::abs(Z.Dot(ref)), ref });
gp_Ax3 ax3;
gp_Trsf trsf2;
for (size_t attempt = 0; attempt < 2; ++attempt) {
if (candidates.empty() || attempt == 1) {
{
gp_XYZ ref(0, 0, 1);
candidates.push_back({std::abs(Z.Dot(ref)), ref});
}
{
gp_XYZ ref(1, 0, 0);
candidates.push_back({std::abs(Z.Dot(ref)), ref});
}
}
auto X = std::min_element(candidates.begin(), candidates.end(), [](auto& p1, auto& p2) { return p1.first < p2.first; })->second;
{
gp_XYZ ref(1, 0, 0);
candidates.push_back({ std::abs(Z.Dot(ref)), ref });
try {
ax3 = gp_Ax3(gp::Origin(), Z, X);
trsf2.SetTransformation(gp::XOY(), ax3);
} catch (Standard_ConstructionError&) {
// Try again, likely we have all identical normals in candidates so
// we cannot find a suitable candidate and need the two default axes
continue;
}
}
}
auto X = std::min_element(candidates.begin(), candidates.end(), [](auto& p1, auto& p2) { return p1.first < p2.first; })->second;
gp_Trsf trsf2;
gp_Ax3 ax3(gp::Origin(), Z, X);
trsf2.SetTransformation(gp::XOY(), ax3);
Bnd_Box tmp;
for (auto& p : vs_transformed) {
@@ -1134,6 +1134,8 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
#endif
builder->SetFuzzyValue(fuzz);
builder->SetArguments(s1s);
// We use our own multi-threading in ifcopenshell on a per-product basis
builder->SetRunParallel(false);
copy_operand(b, b_tmp);
std::swap(b, b_tmp);
builder->SetTools(b);
+2 -1
View File
@@ -12,7 +12,8 @@ table below.
+=========================+=========================+======================+
| .ifc | .obj, .dae, .glb, .stp, | IfcConvert |
| | .igs, .xml, .svg, .h5, | |
| | .ttl, .ifc | |
| | .ttl, .ifc, .rdb, | |
| | .json (xeokit) | |
+-------------------------+-------------------------+----------------------+
| .ifc | .dae, .abc, .usd, .obj, | Bonsai_ |
| | .ply, .stl, .fbx, .glb, | |
@@ -70,7 +70,7 @@ CLI Manual
$ IfcConvert -h
IfcOpenShell IfcConvert 0.8.1-c49ca69 (OCC 7.8.1)
IfcOpenShell IfcConvert 0.8.4-158fe92 (OCC 7.8.1)
Usage: IfcConvert [options] <input.ifc> [<output>]
Converts (the geometry in) an IFC file into one of the following formats:
@@ -80,6 +80,8 @@ CLI Manual
.stp STEP Standard for the Exchange of Product Data
.igs IGES Initial Graphics Exchange Specification
.xml XML Property definitions and decomposition tree
.json JSON Property definitions and decomposition tree in xeokit json format
.rdb RocksDB RocksDB Key-Value store serialization of IFC data
.svg SVG Scalable Vector Graphics (2D floor plan)
.h5 HDF Hierarchical Data Format storing positions, normals and indices
.ttl TTL/WKT RDF Turtle with Well-Known-Text geometry
@@ -108,7 +110,8 @@ CLI Manual
Geometry options:
--kernel arg (=opencascade) Geometry kernel to use (opencascade,
cgal, cgal-simple).
cgal, cgal-simple, hybrid-cgal-simple-o
pencascade).
-j [ --threads ] arg (=1) Number of parallel processing threads
for geometry interpretation.
--center-model Centers the elements by applying the
@@ -118,6 +121,11 @@ CLI Manual
--center-model-geometry Centers the elements by applying the
center point of all mesh vertices as an
offset.
--model-offset arg Applies an arbitrary offset of form
'x;y;z' to all placements.
--model-rotation arg Applies an arbitrary quaternion
rotation of form 'x;y;z;w' to all
placements.
--include arg Specifies that the instances that match
a specific filtering criteria are to be
included in the geometrical output:
@@ -207,13 +215,12 @@ CLI Manual
of surface normals, even if the faces
are not properly oriented in the IFC
file.
--length-unit arg (= 1)
--angle-unit arg (= 1)
--precision arg (= 1e-05)
--dimensionality arg (= 1) Specifies whether to include curves
and/or surfaces and solids in the
output result. Defaults to only
surfaces and solids.
surfaces and solids (SURFACES_AND_SOLID
S). Other possible values are CURVES,
CURVES_SURFACES_AND_SOLIDS.
--layerset-first Assigns the first layer material of the
layerset to the complete product.
--disable-boolean-result Specifies whether to disable the
@@ -262,9 +269,11 @@ CLI Manual
geometrical output back to the unit of
measure in which it is defined in the
IFC file. Default is to use meters.
--context-ids arg
--context-ids arg
--context-ids arg
--context-ids arg List of comma separated context ids to
process - e.g. '15,29' (no quotes
needed).
--context-types arg Currently option has no effect.
--context-identifiers arg Currently option has no effect.
--iterator-output arg (= 0)
--disable-opening-subtractions Specifies whether to disable the
boolean subtraction of
@@ -304,10 +313,18 @@ CLI Manual
geometry output.
--circle-segments arg (= 16) Number of segments to approximate full
circles in CGAL kernel.
--cgal-smooth-angle-degrees arg (= -1)
Angle in degrees under which adjacent
facets will have averaged vertex
normals in CGAL output. NB irrespective
of original IFC geometry types.
Defaults to -1 to disable smoothing.
--keep-bounding-boxes Default is to removes IfcBoundingBox
from model prior to converting
geometry.Setting this option disables
that behaviour
--compute-curvature Specifies whether function_item_evaluat
or.evaluate() computes curvature.
--function-step-type arg (= 0) Indicates the method used for defining
step size when evaluating
function-based curves. Provides
@@ -320,12 +337,24 @@ CLI Manual
parallel. May decrease performance, but
also decrease output size (in the
future)
--model-offset arg Applies an arbitrary offset of form
'x,y,z' to all placements.
--model-rotation arg Applies an arbitrary quaternion
rotation of form 'x,y,z,w' to all
placements.
--permissive-shape-reuse Traverse geometry-level transformations
and apply to product-level placement in
order to increase reuse of geometries
--triangulation-type arg (= 0) Type of planar facet to be emitted
--cgal-original-edges Try to emit original edge face boundary
edges instead of recomputed ones based
on face normal. Falls back to
triangulated data in case of boolean
operands and faces with holes.
--cache-shapes Experimental as not all topology hash
functions fully implemented
--max-offset arg Maximum translation offset to be
observed after which median offset in
model gets removed and logged. Requires
--no-parallel-mapping.
--max-offset-deviation arg To retain field of view, completely
remove elements outside of the median
offset. Requires --no-parallel-mapping.
Serialization options:
--bounds arg Specifies the bounding rectangle, for
@@ -421,3 +450,8 @@ CLI Manual
--wkt-use-section Use a geometrical section rather than
full polyhedral output and footprint in
TTL WKT
--separate-z-up-node Introduce a separate Z-Up node into the
GlTF hierarchy instead of multiplying
the transform into the root node
matrices
@@ -19,7 +19,6 @@ edges, and faces, or alternatively an OpenCASCADE BRep.
applications. See the `Geometry iterator`_ section below after reading this
to see how to process geometry with multiple threads.
Here is a simple example of processing a single wall into a list of vertices and
faces. In this example, a ``shape`` variable is returned, which holds geometry
related information in ``shape.geometry``:
@@ -33,8 +32,12 @@ related information in ``shape.geometry``:
ifc_file = ifcopenshell.open('model.ifc')
element = ifc_file.by_type('IfcWall')[0]
# Create a shape using a hybrid of the cgal-simple geometry kernel and opencascade as a fallback
# Choosing a geometry kernel has a big impact on speed and capability.
# It is recommended to use the "hybrid-cgal-simple-opencascade" kernel.
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, element)
shape = ifcopenshell.geom.create_shape(
settings, element, geometry_library="hybrid-cgal-simple-opencascade")
# The GUID of the element we processed
print(shape.guid)
@@ -224,7 +227,8 @@ Here is a simple example in Python:
ifc_file = ifcopenshell.open('model.ifc')
settings = ifcopenshell.geom.settings()
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count())
iterator = ifcopenshell.geom.iterator(
settings, ifc_file, multiprocessing.cpu_count(), geometry_library="hybrid-cgal-simple-opencascade")
if iterator.initialize():
while True:
shape = iterator.get()
@@ -252,7 +256,9 @@ only process wall elements.
.. code-block:: python
walls = ifc.by_type('IfcWall')
iterator = ifcopenshell.geom.iterator(settings, ifc, multiprocessing.cpu_count(), include=walls)
iterator = ifcopenshell.geom.iterator(
settings, ifc, multiprocessing.cpu_count(),
include=walls, geometry_library="hybrid-cgal-simple-opencascade")
.. note::
@@ -111,6 +111,32 @@ In Python, this is set when the iterator is constructed:
import multiprocessing
iterator = ifcopenshell.geom.iterator(settings, ifc_file, num_threads=multiprocessing.cpu_count())
geometry_library
^^^^^^^^^^^^^^^^
+--------+-------------------+-------------+
| Type | IfcConvert Option | Default |
+========+===================+=============+
| STRING | ``--kernel`` | opencascade |
+--------+-------------------+-------------+
IfcOpenShell supports multiple geometry kernels to process geometry. Choosing the geometry kernel has trade-offs on geometric support, speed, and maturity. It is possible and recommended to chose a hybrid geometry kernel by providing the name ``hybrid-kernelX-kernelY``, where ``kernelX`` is the name of the first kernel to try, and ``kernelY`` is the name of the fallback kernel, for example ``hybrid-cgal-simple-opencascade``.
.. csv-table::
:header: "Comparison", "cgal-simple", "cgal", "opencascade"
"Speed", "Very fast", "Fast", "Slow"
"Curves in extrusion footprints", "Only circle and ellipse arcs are converted to polylines", "Only circle and ellipse arcs are converted to polylines", "Full support including beziers and nurbs"
"Advanced (curved) breps", "No", "No", "Full support"
"Boolean operations", "No", "Full support", "Full support"
"Boolean operations with tolerance / fuzziness handling", "No", "Only manifold inputs", "Full support, including non-manifold inputs"
"Sweeps along alignment curves", "Partial", "Partial", "Full support"
.. code-block:: python
iterator = ifcopenshell.geom.iterator(settings, ifc_file, geometry_library="hybrid-cgal-simple-opencascade")
ifcopenshell.geom.create_shape(settings, element, geometry_library="opencascade")
Iterator settings
-----------------
@@ -750,6 +750,14 @@ class AttributeGetattrTransformer(ast.NodeTransformer):
if node.attr == "create_entity":
return node
if node.attr.startswith("__"):
return node
# Don't rewrite at module scope (top-level, no indent)
enclosing_stmt = next((p for p in parents if isinstance(p, ast.stmt)), None)
if enclosing_stmt is not None and isinstance(getattr(enclosing_stmt, "parent", None), ast.Module):
return node
new_value = self.visit(node.value)
# Replace the Attribute node with a call to the built-in `getattr` function
@@ -842,18 +850,21 @@ if __name__ == "__main__":
print(
"""
def is_indeterminate(v):
return v is None or type(v).__name__ == 'indeterminate_type'
def exists(v):
if callable(v):
try: return v() is not None
except IndexError as e: return False
else: return v is not None
else: return not is_indeterminate(v)
""",
"\n",
file=output,
sep="\n",
)
print(
"def nvl(v, default): return v if v is not None else default",
"def nvl(v, default): return v if not is_indeterminate(v) else default",
"\n",
file=output,
sep="\n",
@@ -871,14 +882,14 @@ def is_entity(inst):
def express_len(v):
if isinstance(v, ifcopenshell.entity_instance) and not is_entity(v):
v = v[0]
elif v is None or v is INDETERMINATE:
elif is_indeterminate(v):
return INDETERMINATE
return len(v)
old_range = range
def range(*args):
if INDETERMINATE in args:
if any(map(is_indeterminate, args)):
return
yield from old_range(*args)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -31,6 +31,7 @@
bool check_aggregate_of_type(PyObject* aggregate, void* type_obj) {
if (!PySequence_Check(aggregate)) return false;
if (PySequence_Size(aggregate) == -1) return false;
for(Py_ssize_t i = 0; i < PySequence_Size(aggregate); ++i) {
PyObject* element = PySequence_GetItem(aggregate, i);
// This is equivalent to the PyFloat_CheckExact macro. This means
@@ -46,6 +47,7 @@
bool check_aggregate_of_aggregate_of_type(PyObject* aggregate, void* type_obj) {
if (!PySequence_Check(aggregate)) return false;
if (PySequence_Size(aggregate) == -1) return false;
for(Py_ssize_t i = 0; i < PySequence_Size(aggregate); ++i) {
PyObject* element = PySequence_GetItem(aggregate, i);
bool b = check_aggregate_of_type(element, type_obj);