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

This commit is contained in:
Josef Wienerroither
2026-03-22 09:20:00 +01:00
141 changed files with 1730 additions and 796 deletions
+3 -3
View File
@@ -34,8 +34,8 @@ client_id, client_secret = "", ""
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
self.server.auth_code = query.get("code", [""])[0] # type: ignore
self.server.auth_state = query.get("state", [""])[0] # type: ignore
self.server.auth_code = query.get("code", [""])[0]
self.server.auth_state = query.get("state", [""])[0]
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
@@ -255,7 +255,7 @@ class BcfClient:
project_id: str = "",
topics: str = "",
query_string: Optional[str] = None,
) -> list[Any]:
) -> None:
# return self.get(
# f"/projects/{project_id}/topics",
# {
+14 -10
View File
@@ -173,16 +173,17 @@ def assert_viewpoints(viewpoints):
assert viewpoint.snapshot is not None
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
exceptions=expected_exception,
default_visibility=False,
),
@@ -193,6 +194,7 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
field_of_view=60,
aspect_ratio=1.0,
),
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
)
@@ -200,16 +202,17 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
assert viewpoint.snapshot is not None
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
exceptions=expected_exception,
default_visibility=True,
),
@@ -220,6 +223,7 @@ def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, ex
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
field_of_view=60,
aspect_ratio=1.0,
),
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
)
+1 -2
View File
@@ -232,8 +232,7 @@ endif
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
# Required for hipped roof generation
# TODO: Use official repo once https://github.com/prochitecture/bpypolyskel/pull/22 is merged.
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/Andrej730/bpypolyskel.git@pyproject_toml" --no-deps -w wheels/
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/prochitecture/bpypolyskel" --no-deps -w wheels/
# folder for executable files
mkdir -p build/bonsai/libs/bin
+1 -3
View File
@@ -72,9 +72,7 @@ class IfcExporter:
def set_header(self):
self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
self.file.header.file_name.time_stamp = (
datetime.datetime.utcnow().replace(tzinfo=datetime.UTC).astimezone().replace(microsecond=0).isoformat()
)
self.file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
self.file.header.file_name.originating_system = "{} {}".format(
self.get_application_name(), tool.Blender.get_bonsai_version()
+8 -5
View File
@@ -45,16 +45,19 @@ from bonsai.bim.module.nest.decorator import NestDecorator
cwd = os.path.dirname(os.path.realpath(__file__))
global_subscription_owner = object()
# Separate owner for per-object msgbus subscriptions (name, active_material_index).
# Using a dedicated owner allows clearing all per-object subscriptions at once
# during undo/redo without affecting other global subscriptions.
object_subscription_owner = object()
def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None:
try:
obj.name
except:
# The object is invalid but somehow still has a callback. Clear all
# msgbus subscriptions to prevent useless further triggers.
bpy.msgbus.clear_by_owner(obj)
return # In case the object RNA is gone during an undo / redo operation
# The object is invalid but somehow still has a callback.
# This can occur during undo/redo when the Python wrapper is stale.
return
# Blender names are up to 63 UTF-8 bytes
if len(bytes(obj.name, "utf-8")) >= 63:
return
@@ -189,7 +192,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type
return
bpy.msgbus.subscribe_rna(
key=subscribe_to,
owner=obj,
owner=object_subscription_owner,
args=(
obj,
data_path,
+4 -9
View File
@@ -316,11 +316,8 @@ class IfcStore:
del IfcStore.id_map[data["id"]]
if "guid" in data:
del IfcStore.guid_map[data["guid"]]
obj = IfcStore.get_object_by_name(data["obj"])
if obj is None:
# obj was just created during this step and didn't existed before.
return
bpy.msgbus.clear_by_owner(obj)
# Note: msgbus subscriptions are cleared globally during
# rebuild_element_maps which runs after every undo/redo.
@staticmethod
def commit_link_element(data: OperationData) -> None:
@@ -367,10 +364,8 @@ class IfcStore:
del IfcStore.id_map[data["id"]]
if "guid" in data:
del IfcStore.guid_map[data["guid"]]
obj = IfcStore.get_object_by_name(data["obj"])
# obj might be removed after unlink.
if not obj:
bpy.msgbus.clear_by_owner(obj)
# Note: msgbus subscriptions are cleared globally during
# rebuild_element_maps which runs after every undo/redo.
@staticmethod
def unlink_element(
+2 -2
View File
@@ -64,8 +64,8 @@ class MaterialCreator:
mesh: Union[OBJECT_DATA_TYPE, None],
shape_has_openings: bool,
) -> None:
if ((rep := getattr(element, "Representation", ...) is not ...) and not rep) or (
(rep := getattr(element, "RepresentationMaps", ...) is not ...) and not rep
if (((rep := getattr(element, "Representation", ...)) is not ... and not rep) or
((rep := getattr(element, "RepresentationMaps", ...)) is not ... and not rep)
):
return
@@ -101,7 +101,7 @@ class AggregateDecorator:
cls.is_installed = False
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
+1 -1
View File
@@ -230,7 +230,7 @@ class BcfTopic(PropertyGroup):
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
global RELATED_TOPICS_ENUM_ITEMS
global RELATED_TOPICS_ENUM_ITEMS # ty: ignore[unresolved-global]
props = self
active_topic = props.active_topic
active_related_topics = active_topic.related_topics.keys()
+4 -4
View File
@@ -46,26 +46,26 @@ def get_libraries(self, context):
def get_namespaces(self, context):
global NAMESPACES_ENUM_ITEMS
global NAMESPACES_ENUM_ITEMS # ty: ignore[unresolved-global]
NAMESPACES_ENUM_ITEMS = [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces]
return NAMESPACES_ENUM_ITEMS
def get_brick_entity_classes(self, context):
global ENTITY_CLASSES_ENUM_ITEMS
global ENTITY_CLASSES_ENUM_ITEMS # ty: ignore[unresolved-global]
entity = self.brick_entity_create_type
ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
return ENTITY_CLASSES_ENUM_ITEMS
def get_brick_roots(self, context):
global BRICK_ROOTS_ENUM_ITEMS
global BRICK_ROOTS_ENUM_ITEMS # ty: ignore[unresolved-global]
BRICK_ROOTS_ENUM_ITEMS = [(root, root, "") for root in BrickStore.root_classes]
return BRICK_ROOTS_ENUM_ITEMS
def get_brick_relations(self, context):
global BRICK_RELATIONS_ENUM_ITEMS
global BRICK_RELATIONS_ENUM_ITEMS # ty: ignore[unresolved-global]
BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
for relation in BrickschemaData.data["active_relations"]:
if relation["predicate_name"] == "label":
@@ -1285,7 +1285,7 @@ class SnapManager:
continue
coords = np.empty(vertex_count * 3, dtype=np.float32)
mesh.vertices.foreach_get("co", coords) # type: ignore[arg-type]
mesh.vertices.foreach_get("co", coords)
coords = coords.reshape(-1, 3)
matrix = np.array(obj_eval.matrix_world, dtype=np.float32)
@@ -456,7 +456,8 @@ def format_distance(
tx_dist = fmt % d_cm
else:
tx_dist = fmt % value
assert f"Unexpected unit_system - '{unit_system}'."
# tx_dist = fmt % value
return tx_dist
@@ -1426,6 +1426,7 @@ class CreateDrawing(bpy.types.Operator):
"/Pset_.*Common/.Status",
"EPset_Status.Status",
"EPset_Status.UserDefinedStatus",
"Material.Name",
]
group = root.find("{http://www.w3.org/2000/svg}g")
@@ -27,7 +27,6 @@ import ifcopenshell.api.pset
import ifcopenshell.util.element
from bpy.props import (
BoolProperty,
BoolVectorProperty,
CollectionProperty,
EnumProperty,
FloatProperty,
@@ -366,9 +366,6 @@ class BaseLinesShader(BaseShader):
}
"""
def __init__(self, gap_size=16):
super().__init__(gap_size=gap_size)
def glenable(self):
super().glenable()
@@ -816,7 +816,6 @@ class BIM_PT_text(Panel):
for i, literal_data in enumerate(text_data["Literals"]):
box = self.layout.box()
box.label(text=f"Literal[{i}]:")
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
for attribute in literal_data:
@@ -1066,7 +1066,7 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
cls.poll_message_set("Only available from Outliner.")
return False
def execute(self, context):
def execute(self, context): # ty:ignore[override-of-final-method]
if len(getattr(context, "selected_ids", [])) == 0:
return {"FINISHED"}
@@ -2289,7 +2289,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
elif obj in pprops.clipping_planes_objs:
self.report({"ERROR"}, "Clipping planes cannot be edited")
elif element:
if not obj.data:
if not obj.data or obj.type not in ("MESH", "CURVE"):
self.report({"INFO"}, "No geometry to edit")
elif tool.Geometry.is_locked(element):
self.report({"ERROR"}, lock_error_message(obj.name))
@@ -139,7 +139,9 @@ def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.typ
tool.Georeference.set_coordinates(
"blender",
ifcopenshell.util.geolocation.enh2xyz(
*local_coordinates,
local_coordinates[0],
local_coordinates[1],
local_coordinates[2],
float(props.blender_offset_x),
float(props.blender_offset_y),
float(props.blender_offset_z),
@@ -162,7 +164,9 @@ def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types
tool.Georeference.set_coordinates(
"blender",
ifcopenshell.util.geolocation.enh2xyz(
*local_coordinates,
local_coordinates[0],
local_coordinates[1],
local_coordinates[2],
float(props.blender_offset_x),
float(props.blender_offset_y),
float(props.blender_offset_z),
@@ -267,6 +271,8 @@ class BIMGeoreferenceProperties(PropertyGroup):
x_axis_ordinate: str
x_axis_is_null: bool
model_is_georeferenced: bool
model_crs: str
model_origin: str
model_origin_si: str
model_project_north: str
+1 -1
View File
@@ -27,7 +27,7 @@ from bonsai.bim.prop import StrProperty
class BIMCityJsonProperties(PropertyGroup):
def get_lods(self, context):
global LODS_ENUM_ITEMS
global LODS_ENUM_ITEMS # ty: ignore[unresolved-global]
LODS_ENUM_ITEMS = [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods]
return LODS_ENUM_ITEMS
+1 -1
View File
@@ -320,7 +320,7 @@ class RadianceExporterProperties(PropertyGroup):
)
def get_subcategories(self, context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
global SUBCATEGORIES_ENUM_ITEMS
global SUBCATEGORIES_ENUM_ITEMS # ty: ignore[unresolved-global]
if self.category in spectraldb:
SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()]
else:
@@ -717,7 +717,11 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
self.props.material_set_item_material = str(material_set_item.Material.id())
self.props.material_set_item_attributes.clear()
bonsai.bim.helper.import_attributes(material_set_item, self.props.material_set_item_attributes)
bonsai.bim.helper.import_attributes(
material_set_item,
self.props.material_set_item_attributes,
callback=self.import_attributes_callback,
)
if material_set_item.is_a("IfcMaterialProfile"):
if material_set_item.Profile and material_set_item.Profile.ProfileName:
@@ -725,6 +729,29 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
return {"FINISHED"}
def import_attributes_callback(
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
) -> None | Literal[True]:
if data["type"] != "IfcMaterialLayer" or name != "IsVentilated" or not prop:
return None
# Keep null semantics unchanged on export, but avoid an empty UI selection.
prop.data_type = "enum"
prop.special_type = "LOGICAL"
prop.enum_items = json.dumps(("TRUE", "FALSE", "UNKNOWN"))
value = data[name]
if value == "UNKNOWN":
prop.enum_value = "UNKNOWN"
elif value is None:
# Keep visible default as FALSE, but preserve null semantics on save.
prop.enum_value = "FALSE"
prop.is_null = True
else:
prop.enum_value = "TRUE" if value else "FALSE"
return True
class DisableEditingMaterialSetItem(bpy.types.Operator):
bl_idname = "bim.disable_editing_material_set_item"
+2 -2
View File
@@ -227,7 +227,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001)
if not all(is_parallel12, is_parallel13, is_parallel21, is_parallel23):
if not all([is_parallel12, is_parallel13, is_parallel21, is_parallel23]):
fitting_type = "WYE"
if not fitting_type:
@@ -903,7 +903,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
radius: bpy.props.FloatProperty(
"Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
name="Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
)
def _execute(self, context):
+29 -138
View File
@@ -151,29 +151,11 @@ class FilledOpeningGenerator:
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
assert representation
# Check if mapped representation - PRESERVE the mapping structure
if (
representation.RepresentationType == "MappedRepresentation"
and len(representation.Items) == 1
and representation.Items[0].is_a("IfcMappedItem")
):
# Store the existing RepresentationMap to reuse it
existing_mapping_source = representation.Items[0].MappingSource
reuse_mapped_representation = True
else:
representation = ifcopenshell.util.representation.resolve_representation(representation)
if not reuse_mapped_representation:
# Check for library template before generating from filling
template_rep = self.get_opening_template_from_type(filling)
if template_rep:
representation = template_rep
else:
representation = self.generate_opening_from_filling(
filling, filling_obj, opening_thickness_si=opening_thickness_si
)
representation = ifcopenshell.util.representation.resolve_representation(representation)
else:
representation = self.generate_opening_from_filling(
filling, filling_obj, opening_thickness_si=opening_thickness_si
)
# Create mapped representation
if reuse_mapped_representation:
@@ -247,109 +229,38 @@ class FilledOpeningGenerator:
voided_element = opening.VoidsElements[0].RelatingBuildingElement
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
# ALWAYS preserve the existing opening representation (Tessellation, SweptSolid, etc.)
preserved_representation = None
if opening_rep:
if (
opening_rep.RepresentationType == "MappedRepresentation"
and len(opening_rep.Items) == 1
and opening_rep.Items[0].is_a("IfcMappedItem")
):
# For mapped representations, copy the underlying representation
preserved_representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
opening_rep.Items[0].MappingSource.MappedRepresentation,
exclude=["IfcGeometricRepresentationContext"],
)
else:
# For direct representations (non-mapped), copy them too
preserved_representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), opening_rep, exclude=["IfcGeometricRepresentationContext"]
)
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
# Priority order for choosing representation:
# 1. Existing occurrence with MappedRepresentation (preserve mapping!)
# 2. Library template with Tessellation
# 3. Preserved representation from old opening (maintain user's work)
# 4. Generate from filling (last resort)
representation_to_use = None
reuse_mapped_representation = False
existing_mapping_source = None
if existing_opening_occurrence:
representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
if (
representation
and representation.RepresentationType == "MappedRepresentation"
and len(representation.Items) == 1
and representation.Items[0].is_a("IfcMappedItem")
):
# PRESERVE the mapped structure - reuse the same RepresentationMap
existing_mapping_source = representation.Items[0].MappingSource
reuse_mapped_representation = True
else:
representation_to_use = ifcopenshell.util.representation.resolve_representation(representation)
if not representation_to_use and not reuse_mapped_representation:
template_rep = self.get_opening_template_from_type(filling)
if template_rep and template_rep.RepresentationType == "Tessellation":
representation_to_use = template_rep
if not representation_to_use and not reuse_mapped_representation and preserved_representation:
representation_to_use = preserved_representation
if not representation_to_use and not reuse_mapped_representation:
representation = ifcopenshell.util.representation.resolve_representation(representation)
mapped_representation = ifcopenshell.api.geometry.map_representation(
tool.Ifc.get(), representation=representation
)
ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(), product=opening, representation=mapped_representation
)
else:
opening_obj = tool.Ifc.get_object(opening)
if opening_obj:
tool.Ifc.unlink(element=opening)
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
filling_obj = tool.Ifc.get_object(filling)
representation_to_use = self.generate_opening_from_filling(filling, filling_obj)
# Create the mapped representation
if reuse_mapped_representation:
# Reuse existing RepresentationMap - don't create a new one!
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
new_mapped_item = tool.Ifc.get().create_entity(
"IfcMappedItem",
MappingSource=existing_mapping_source,
MappingTarget=tool.Ifc.get().create_entity(
"IfcCartesianTransformationOperator3D",
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 1.0, 0.0)),
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
Scale=1.0,
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)),
),
)
mapped_representation = tool.Ifc.get().create_entity(
"IfcShapeRepresentation",
ContextOfItems=context,
RepresentationIdentifier="Body",
RepresentationType="MappedRepresentation",
Items=[new_mapped_item],
)
else:
representation = self.generate_opening_from_filling(filling, filling_obj)
mapped_representation = ifcopenshell.api.geometry.map_representation(
tool.Ifc.get(), representation=representation_to_use
tool.Ifc.get(), representation=representation
)
ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(), product=opening, representation=mapped_representation
)
ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(), product=opening, representation=mapped_representation
)
# update voided object representation...
# update voided object representation or all it's parts if it's an aggregate
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
for voided_element in voided_elements:
voided_obj = tool.Ifc.get_object(voided_element)
@@ -363,36 +274,6 @@ class FilledOpeningGenerator:
representation=representation,
)
def get_opening_template_from_type(
self, filling: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
"""
Check if the filling's type has a stored opening template from library import.
"""
element_type = ifcopenshell.util.element.get_type(filling)
if not element_type:
return None
desc = element_type.Description
if not desc or "||BonsaiOpeningTemplate:" not in desc:
return None
# Extract template ID
marker = desc.split("||BonsaiOpeningTemplate:")[-1]
template_id = int(marker.split("||")[0])
try:
template_rep = tool.Ifc.get().by_id(template_id)
# Make a copy so we don't reuse the same representation instance
copied = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), template_rep, exclude=["IfcGeometricRepresentationContext"]
)
return copied
except:
return None
def generate_opening_from_filling(
self,
filling: ifcopenshell.entity_instance,
@@ -659,6 +540,16 @@ class AddBoolean(Operator, tool.Ifc.Operator):
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
rep_obj = tool.Geometry.get_geometry_props().representation_obj
if booleans:
# Users typically select two top-level items and expect the
# operand to be absorbed into the boolean, not remain as a
# standalone item alongside it.
representation = tool.Geometry.get_active_representation(rep_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
second_items_set = set(second_items)
new_items = [i for i in representation.Items if i not in second_items_set]
if new_items:
representation.Items = new_items
rep_element = tool.Ifc.get_entity(rep_obj)
tool.Model.mark_manual_booleans(rep_element, booleans)
tool.Geometry.reload_representation(rep_obj)
@@ -694,10 +694,14 @@ def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[
new_settings = settings.copy()
new_settings["context"] = box_context
new_box = ifcopenshell.api.geometry.add_representation(ifc_file, should_run_listeners=False, **new_settings)
new_box = ifcopenshell.api.geometry.add_representation(
ifc_file,
should_run_listeners=False, # ty:ignore[unknown-argument]
**new_settings,
)
ifcopenshell.api.geometry.assign_representation(
ifc_file,
should_run_listeners=False,
should_run_listeners=False, # ty:ignore[unknown-argument]
product=product,
representation=new_box,
)
+40 -16
View File
@@ -18,7 +18,7 @@
import copy
from math import atan2, degrees, pi, radians
from typing import Any, Literal, Optional, Union
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
import bpy
import ifcopenshell
@@ -49,7 +49,7 @@ ProfileFrom2PointsReturn = Union[dict[str, Any], None]
class DumbProfileGenerator:
def __init__(self, relating_type):
def __init__(self, relating_type: ifcopenshell.entity_instance):
self.relating_type = relating_type
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -201,7 +201,7 @@ class DumbProfileGenerator:
class DumbProfileRegenerator:
def regenerate_from_profile_def(self, profile):
def regenerate_from_profile_def(self, profile: ifcopenshell.entity_instance) -> None:
self.file = tool.Ifc.get()
objs = []
if not profile:
@@ -221,7 +221,7 @@ class DumbProfileRegenerator:
for element in self.get_element_types_using_profile(profile):
tool.Model.mark_thumbnail_for_update(element)
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
def regenerate_from_profile(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
self.file = ifc_file
objs = []
profile = settings["profile"].Profile
@@ -233,7 +233,7 @@ class DumbProfileRegenerator:
objs.append(obj)
DumbProfileRecalculator().recalculate(objs)
def get_elements_using_profile(self, profile):
def get_elements_using_profile(self, profile: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
results = []
profile_sets = [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
@@ -252,7 +252,9 @@ class DumbProfileRegenerator:
results.extend(rel.RelatedObjects)
return results
def get_element_types_using_profile(self, profile):
def get_element_types_using_profile(
self, profile: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
results = []
profile_sets = [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
@@ -269,12 +271,18 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.extend_profile"
bl_label = "Extend Profile"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty()
join_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[("-", "Unjoin", ""), ("L", "L", ""), ("V", "V", ""), ("T", "T", "")],
default="-",
)
if TYPE_CHECKING:
join_type: Literal["-", "L", "V", "T"]
def _execute(self, context):
selected_objs = context.selected_objects
joiner = DumbProfileJoiner()
if not self.join_type:
if self.join_type == "-":
for obj in selected_objs:
joiner.unjoin(obj)
return {"FINISHED"}
@@ -626,11 +634,15 @@ class DumbProfileJoiner:
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[1] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -673,11 +685,15 @@ class DumbProfileJoiner:
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[0] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -721,7 +737,9 @@ class DumbProfileJoiner:
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[1] = intersect
else:
plane = self.get_profile_plane(
@@ -729,7 +747,9 @@ class DumbProfileJoiner:
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(
@@ -742,7 +762,9 @@ class DumbProfileJoiner:
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[0] = intersect
else:
plane = self.get_profile_plane(
@@ -750,7 +772,9 @@ class DumbProfileJoiner:
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(
+2 -2
View File
@@ -31,11 +31,11 @@ def calculate_quantities(usecase_path, ifc_file: ifcopenshell.file, settings):
return
task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask"))
qto = ifcopenshell.api.pset.add_qto(
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities"
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities" # ty:ignore[unknown-argument]
)
ifcopenshell.api.pset.edit_qto(
ifc_file,
should_run_listeners=False,
should_run_listeners=False, # ty:ignore[unknown-argument]
qto=qto,
properties={
"StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days,
@@ -1268,27 +1268,6 @@ class DumbWallJoiner:
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
return wall2
def join_Z(self, wall1, slab2):
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
for rel in element1.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.Description == "TOP":
ifcopenshell.api.geometry.disconnect_element(
tool.Ifc.get(),
relating_element=rel.RelatingElement,
related_element=element1,
)
ifcopenshell.api.geometry.connect_element(
tool.Ifc.get(),
relating_element=element2,
related_element=element1,
description="TOP",
)
tool.Model.recreate_wall(element1, wall1)
def set_axis(self, wall, p1, p2):
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -1333,29 +1312,6 @@ class DumbWallJoiner:
self.set_axis(element1, p1, p2)
tool.Model.recreate_wall(element1, wall1)
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
axis1 = tool.Model.get_wall_axis(wall1)
axis2 = tool.Model.get_wall_axis(wall2)
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
if intersect:
intersect, _ = intersect
else:
return
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
ifcopenshell.api.geometry.connect_path(
tool.Ifc.get(),
related_element=element1,
relating_element=element2,
relating_connection="ATPATH",
related_connection=connection,
description="BUTT",
)
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
wall1 = tool.Ifc.get_entity(obj1)
wall2 = tool.Ifc.get_entity(obj2)
@@ -101,7 +101,7 @@ class NestDecorator:
cls.is_installed = False
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
@@ -76,7 +76,7 @@ classes = (
operator.UnlinkIfc,
operator.UnloadLink,
workspace.ExploreHotkey,
workspace.GenerateUVMap,
operator.GenerateUVMap,
prop.LibraryBreadcrumb,
prop.LibraryElement,
prop.FilterCategory,
+198 -99
View File
@@ -178,9 +178,18 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
bl_description = (
"Select an IFC file that can be used as a library.\n\nALT+click to reload the current loaded library file."
)
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
append_all: bpy.props.BoolProperty(default=False)
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
filter_glob: bpy.props.StringProperty(
default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
append_all: bpy.props.BoolProperty(default=False) # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path", default=False
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
filter_glob: str
append_all: bool
use_relative_path: bool
reload_previous_file = False
@@ -558,7 +567,11 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator):
class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.append_library_element_by_query"
bl_label = "Append Library Element By Query"
query: bpy.props.StringProperty(name="Query")
query: bpy.props.StringProperty(name="Query") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
query: str
@classmethod
def poll(cls, context):
@@ -587,9 +600,16 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
"Append element to the current project.\n\n"
"ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)"
)
definition: bpy.props.IntProperty()
prop_index: bpy.props.IntProperty()
assume_unique_by_name: bpy.props.BoolProperty(name="Assume Unique By Name", default=True, options={"SKIP_SAVE"})
definition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
prop_index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
assume_unique_by_name: bpy.props.BoolProperty(
name="Assume Unique By Name", default=True, options={"SKIP_SAVE"}
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
definition: int
prop_index: int
assume_unique_by_name: bool
file: ifcopenshell.file
@@ -618,8 +638,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
if not element:
return {"FINISHED"}
if element.is_a("IfcTypeProduct"):
# Store opening template from library if it exists
self.store_opening_template_from_library(element, library_file)
self.import_type_from_ifc(element, context)
elif element.is_a("IfcProduct"):
# NOTE: Non-types are not exposed in UI directly
@@ -720,53 +738,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
ifc_importer.create_style(element)
def store_opening_template_from_library(
self, element: ifcopenshell.entity_instance, library_file: ifcopenshell.file
) -> None:
"""
Find an opening representation in the library and copy it to the current file
as a template. Store the template ID on the type for later retrieval.
"""
try:
library_element = library_file.by_guid(element.GlobalId)
except:
return
# Find occurrences with openings in the library
library_occurrences = ifcopenshell.util.element.get_types(library_element)
for occurrence in library_occurrences:
if not getattr(occurrence, "FillsVoids", None):
continue
library_opening = occurrence.FillsVoids[0].RelatingOpeningElement
library_opening_rep = ifcopenshell.util.representation.get_representation(
library_opening, "Model", "Body", "MODEL_VIEW"
)
if not library_opening_rep:
continue
# Check if mapped representation
if (
library_opening_rep.RepresentationType == "MappedRepresentation"
and len(library_opening_rep.Items) == 1
and library_opening_rep.Items[0].is_a("IfcMappedItem")
):
mapped_rep = library_opening_rep.Items[0].MappingSource.MappedRepresentation
# Store ALL representation types (Tessellation, SweptSolid, etc.)
template_rep = ifcopenshell.util.element.copy_deep(
self.file, mapped_rep, exclude=["IfcGeometricRepresentationContext"]
)
# Store reference in type's Description
current_desc = element.Description or ""
element.Description = f"{current_desc}||BonsaiOpeningTemplate:{template_rep.id()}"
return
break
class EditProjectLibrary(bpy.types.Operator):
bl_idname = "bim.edit_project_library"
@@ -988,24 +959,28 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
bl_label = "Load Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load an existing IFC project"
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"})
is_advanced: bpy.props.BoolProperty(
filepath: bpy.props.StringProperty(
subtype="FILE_PATH", options={"SKIP_SAVE"}
) # pyright: ignore[reportRedeclaration]
filter_glob: bpy.props.StringProperty(
default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
is_advanced: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Enable Advanced Mode",
description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings",
default=False,
)
use_relative_path: bpy.props.BoolProperty(
use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Use Relative Path",
description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved",
default=False,
)
should_start_fresh_session: bpy.props.BoolProperty(
should_start_fresh_session: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Should Start Fresh Session",
description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option",
default=True,
)
import_without_ifc_data: bpy.props.BoolProperty(
import_without_ifc_data: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Import Without IFC Data",
description=(
"Import IFC objects as Blender objects without any IFC metadata and authoring capabilities."
@@ -1013,9 +988,20 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
),
default=False,
)
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
use_detailed_tooltip: bpy.props.BoolProperty(
default=False, options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
filename_ext = ".ifc"
if TYPE_CHECKING:
filepath: str
filter_glob: str
is_advanced: bool
use_relative_path: bool
should_start_fresh_session: bool
import_without_ifc_data: bool
use_detailed_tooltip: bool
@classmethod
def description(cls, context, properties):
tooltip = cls.bl_description
@@ -1314,7 +1300,10 @@ class ToggleFilterCategories(bpy.types.Operator):
bl_idname = "bim.toggle_filter_categories"
bl_label = "Toggle Filter Categories"
bl_options = {"REGISTER", "UNDO"}
should_select: bpy.props.BoolProperty(name="Should Select", default=True)
should_select: bpy.props.BoolProperty(name="Should Select", default=True) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
should_select: bool
def execute(self, context):
props = tool.Project.get_project_props()
@@ -1338,6 +1327,14 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
default=False,
)
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
query: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
name="Query",
description=(
"Custom selector query to use to load element from a linked model. E.g. 'IfcElement'.\n\n"
"Default query - IfcElement, but excluding IfcProxy, IfcSpatialStructureElement, IfcSpatialElement, IfcFeatureElement."
),
)
filename_ext = ".ifc"
if TYPE_CHECKING:
@@ -1347,20 +1344,25 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
filter_glob: str
use_relative_path: bool
use_cache: bool
query: str
def draw(self, context):
assert self.layout
pprops = tool.Project.get_project_props()
row = self.layout.row()
row.prop(self, "use_relative_path")
row = self.layout.row()
row.prop(self, "use_cache")
row = self.layout.row()
row.prop(pprops, "false_origin_mode")
row.label(text="False Origin Mode:")
row = self.layout.row()
row.prop(pprops, "false_origin_mode", text="")
if pprops.false_origin_mode == "MANUAL":
row = self.layout.row()
row.prop(pprops, "false_origin")
row = self.layout.row()
row.prop(pprops, "project_north")
self.layout.prop(self, "query", placeholder="IfcElement")
def _execute(self, context):
start = time.time()
@@ -1393,7 +1395,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
new.ifc_definition_id = reference.id()
new.name = filepath
new.filepath = filepath
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache)
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache, query=self.query)
class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
@@ -1401,7 +1403,11 @@ class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Unlink IFC"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Remove the selected file from the link list"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def _execute(self, context):
props = tool.Project.get_project_props()
@@ -1421,7 +1427,11 @@ class UnloadLink(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Unload Link"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Unload the selected linked file"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def _execute(self, context):
link = tool.Project.get_project_props().links[self.link_index]
@@ -1446,10 +1456,12 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration]
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
use_cache: bool
query: str
def _execute(self, context):
self.link = tool.Project.get_project_props().links[self.link_index]
@@ -1491,8 +1503,20 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
def link_ifc(self) -> Union[set[str], None]:
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5")
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
if not self.use_cache and blend_filepath.exists():
def should_clear_cache() -> bool:
if not self.use_cache:
return True
if not blend_filepath.exists():
return False
data = json.loads(json_filepath.read_text())
# Empty 'query' - model loaded without custom query.
# Missing 'query' - model was loaded before custom queries were introduced in Bonsai.
query = data.get("query", "")
return query != self.query
if should_clear_cache():
os.remove(blend_filepath)
if not blend_filepath.exists():
@@ -1520,7 +1544,7 @@ def run():
pprops.project_north = "{pprops.project_north}"
# Use absolute path to be safe from cwd changes.
try:
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}")
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}", query={repr(self.query)})
except RuntimeError as e:
# Operator failed (returned CANCELLED with error report)
print(f"Failed to load linked project: {{e}}")
@@ -1606,7 +1630,11 @@ class ReloadLink(bpy.types.Operator):
bl_label = "Reload Link"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Reload the selected file"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def execute(self, context):
bpy.ops.bim.unload_link(link_index=self.link_index)
@@ -1618,7 +1646,11 @@ class ToggleLinkSelectability(bpy.types.Operator):
bl_label = "Toggle Link Selectability"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle selectability"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def execute(self, context):
props = tool.Project.get_project_props()
@@ -1788,7 +1820,11 @@ class SelectLinkHandle(bpy.types.Operator):
bl_label = "Select Link Handle"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select link empty object handle"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def execute(self, context):
props = tool.Project.get_project_props()
@@ -1846,11 +1882,28 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
supported_filexts = (".ifc", ".ifczip", ".ifcjson")
filter_glob: bpy.props.StringProperty(default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"})
json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version")
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
filter_glob: bpy.props.StringProperty(
default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
json_version: bpy.props.EnumProperty(
items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version"
) # pyright: ignore[reportRedeclaration]
json_compact: bpy.props.BoolProperty(
name="Export Compact IFCJSON", default=False
) # pyright: ignore[reportRedeclaration]
should_save_as: bpy.props.BoolProperty(
name="Should Save As", default=False, options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path", default=False
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
filter_glob: str
json_version: str
json_compact: bool
should_save_as: bool
use_relative_path: bool
@classmethod
def poll(cls, context):
@@ -2000,6 +2053,12 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file."
bl_options = {"REGISTER", "UNDO"}
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""See ``bim.link_ifc``."""
if TYPE_CHECKING:
query: str
file: ifcopenshell.file
meshes: dict[str, bpy.types.Mesh]
# Material names is derived from diffuse as in 'r-g-b-a'.
@@ -2049,14 +2108,17 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
self.elements = set(self.file.by_type("IfcElement"))
if self.file.schema in ("IFC2X3", "IFC4"):
self.elements |= set(self.file.by_type("IfcProxy"))
if self.file.schema == "IFC2X3":
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
if self.query:
self.elements = ifcopenshell.util.selector.filter_elements(self.file, self.query)
else:
self.elements |= set(self.file.by_type("IfcSpatialElement"))
self.elements -= set(self.file.by_type("IfcFeatureElement"))
self.elements = set(self.file.by_type("IfcElement"))
if self.file.schema in ("IFC2X3", "IFC4"):
self.elements |= set(self.file.by_type("IfcProxy"))
if self.file.schema == "IFC2X3":
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
else:
self.elements |= set(self.file.by_type("IfcSpatialElement"))
self.elements -= set(self.file.by_type("IfcFeatureElement"))
if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin:
tool.Loader.set_manual_blender_offset(self.file)
@@ -2081,6 +2143,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
"false_origin_mode": pprops.false_origin_mode,
"false_origin": pprops.false_origin,
"project_north": pprops.project_north,
"query": self.query,
}
with open(self.json_filepath, "w") as f:
json.dump(data, f)
@@ -2495,7 +2558,7 @@ class EnableCulling(bpy.types.Operator):
self.total_mousemoves = 0
self.cullable_objects = []
def modal(self, context, event):
def modal(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
if not LinksData.enable_culling:
for obj in bpy.context.visible_objects:
if obj.type == "MESH" and obj.name.startswith("Ifc"):
@@ -2526,7 +2589,7 @@ class EnableCulling(bpy.types.Operator):
return {"PASS_THROUGH"}
def is_view_changed(self, context):
def is_view_changed(self, context: bpy.types.Context) -> bool:
view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix
vp_matrix = projection_matrix @ view_matrix
@@ -2541,7 +2604,7 @@ class EnableCulling(bpy.types.Operator):
return True
return False
def is_object_in_view(self, obj, context, camera_position):
def is_object_in_view(self, obj: bpy.types.Object, context: bpy.types.Context, camera_position: Vector) -> bool:
# Get the view matrix and the projection matrix from the active viewport
view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix
@@ -2568,7 +2631,7 @@ class EnableCulling(bpy.types.Operator):
return False
return True
def invoke(self, context, event):
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set["rna_enums.OperatorReturnItems"]:
LinksData.enable_culling = True
self.cullable_objects = []
for obj in bpy.context.visible_objects:
@@ -2855,8 +2918,16 @@ class IFCFileHandlerOperator(bpy.types.Operator):
bl_label = "Import .ifc file"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"})
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"})
directory: bpy.props.StringProperty(
subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"}
) # pyright: ignore[reportRedeclaration]
files: bpy.props.CollectionProperty(
type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"}
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
directory: str
files: list[bpy.types.OperatorFileListElement]
def invoke(self, context, event):
# Keeping code in .invoke() as we'll probably add some
@@ -2907,7 +2978,10 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
bl_label = "Measure Tool"
bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty()
measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
measure_type: str
@classmethod
def poll(cls, context):
@@ -3003,7 +3077,10 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator):
bl_label = "Measure Face Area Tool"
bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty()
measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
measure_type: str
@classmethod
def poll(cls, context):
@@ -3107,7 +3184,10 @@ class ClearMeasurement(bpy.types.Operator):
@classmethod
def poll(cls, context):
polyline_props = tool.Model.get_polyline_props()
return len(polyline_props.measurement_polyline) > 0
if len(polyline_props.measurement_polyline) > 0:
return True
cls.poll_message_set("No measurement to clear.")
return False
def execute(self, context):
polyline_props = tool.Model.get_polyline_props()
@@ -3211,7 +3291,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
super().invoke(context, event)
return {"RUNNING_MODAL"}
def cancel_tool(self, context):
def cancel_tool(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
context.workspace.status_text_set(text=None)
if hasattr(self, "tool_state"):
self.tool_state.plane_method = None
@@ -3219,7 +3299,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
tool.Blender.update_viewport()
return {"CANCELLED"}
def handle_custom_instructions(self, context):
def handle_custom_instructions(self, context: bpy.types.Context) -> None:
if len(self.selected_points) == 0:
instruction_text = "Click First Point on Image"
elif len(self.selected_points) == 1:
@@ -3234,14 +3314,14 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
context.workspace.status_text_set(text=instruction_text)
def calculate_distance(self):
def calculate_distance(self) -> None:
if len(self.selected_points) == 2:
point1 = self.selected_points[0]
point2 = self.selected_points[1]
distance_3d = (point2 - point1).length
self.calculated_distance = distance_3d / self.unit_scale
def apply_scaling(self, context):
def apply_scaling(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
if len(self.selected_points) != 2:
self.report({"ERROR"}, "Two points must be selected")
return {"CANCELLED"}
@@ -3299,7 +3379,10 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
bl_idname = "bim.load_blend_metadata_and_ifc"
bl_label = "Load Blend Metadata and IFC"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(name="IFC File Path", default="")
filepath: bpy.props.StringProperty(name="IFC File Path", default="") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
filepath: str
def execute(self, context):
ifc_file = self.filepath
@@ -3333,3 +3416,19 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
bpy.app.handlers.load_post.append(load_handler)
bpy.ops.wm.open_mainfile(filepath=metadata_path)
return {"FINISHED"}
class GenerateUVMap(bpy.types.Operator):
bl_idname = "bim.generate_uv_map"
bl_label = "Generate UV Map"
bl_description = "Generate UV map for selected mesh."
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
if not obj or not isinstance(obj.data, bpy.types.Mesh):
self.report({"ERROR"}, "No valid mesh selected.")
return {"CANCELLED"}
tool.Loader.load_generated_uv_map(obj.data)
self.report({"INFO"}, "Generated UV map for selected mesh.")
return {"FINISHED"}
+1 -1
View File
@@ -496,7 +496,7 @@ class BIM_PT_links(Panel):
row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index
else:
row.operator("bim.load_link", text="", icon="LINKED").link_index = index
row.operator("bim.unlink_ifc", text="", icon="X").link_index = index
row.operator("bim.unlink_ifc", text="", icon="X").link_index = index
self.layout.template_list("BIM_UL_links", "", self.props, "links", self.props, "active_link_index")
if LinksData.enable_culling:
@@ -71,24 +71,26 @@ class ExploreTool(bpy.types.WorkSpaceTool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_M")
row = layout.row(align=True)
op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT")
op.hotkey = "S_M"
row = layout.row(align=True)
row.prop(prop, "measurement_type", text="Measure Type", expand=True, icon_only=True, emboss=True)
row = layout.row(align=True)
op = row.operator("bim.clear_measurement", text="", icon="X")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_S")
row = layout.row(align=True)
op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE")
op.hotkey = "S_S"
op.description = "Scale Image Annotation. Allows to scale an IfcReferenceImage. Select image, select tool. Check lower left corner instructions to select two points and provide real distance between them"
op.description = (
"Scale Image Annotation.\n\n"
"Allows to scale an IfcReferenceImage.\n\n"
"Select image, select tool. "
"Check lower left corner instructions to select two points and provide real distance between them"
)
op = row.operator("bim.generate_uv_map", text="Generate UV Map", icon="UV")
op.description = "Generate UV map for selected mesh."
row = layout.row(align=True)
row.operator("bim.generate_uv_map", icon="UV")
class ExploreHotkey(bpy.types.Operator):
@@ -156,20 +158,3 @@ class ExploreHotkey(bpy.types.Operator):
def hotkey_A_H(self) -> None:
bpy.ops.bim.hide_queried_linked_element(unhide_all=True)
class GenerateUVMap(bpy.types.Operator):
bl_idname = "bim.generate_uv_map"
bl_label = "Generate UV Map"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
description: bpy.props.StringProperty()
def execute(self, context):
obj = context.active_object
if not obj or not hasattr(obj, "data") or not hasattr(obj.data, "polygons"):
self.report({"ERROR"}, "No valid mesh selected.")
return {"CANCELLED"}
tool.Loader.load_generated_uv_map(obj.data)
self.report({"INFO"}, "Generated UV map for selected mesh.")
return {"FINISHED"}
@@ -321,7 +321,7 @@ def get_gross_perimeter(o: bpy.types.Object) -> float:
return gross_perimeter
def get_space_net_perimeter(obj: bpy.types.Object) -> float:
def get_space_net_perimeter(obj: bpy.types.Object) -> None:
pass
@@ -619,7 +619,7 @@ class SelectFilterElements(bpy.types.Operator):
return {"FINISHED"}
class ApplyFilterFromText(Operator, tool.Ifc.Operator):
class ApplyFilterFromText(Operator):
bl_idname = "bim.apply_filter_from_text"
bl_label = "Apply Filter Configuration"
bl_description = "Apply the JSON filter configuration from the current text block"
@@ -1440,7 +1440,7 @@ class ShowAllElements(Operator):
return {"FINISHED"}
class SelectSimilar(Operator, tool.Ifc.Operator):
class SelectSimilar(Operator):
bl_idname = "bim.select_similar"
bl_label = "Select Similar"
bl_options = {"REGISTER", "UNDO"}
@@ -28,6 +28,7 @@ import ifcopenshell.util.representation
import ifcopenshell.util.unit
import ifcopenshell.util.unit as ifcunit
import numpy as np
import numpy.typing as npt
from mathutils import Vector
import bonsai.tool as tool
@@ -478,7 +479,7 @@ class ShaderInfo:
"""get the args to the point shader"""
location = np.array(location)
indices = []
direction_dict = {
direction_dict: dict[str, tuple[npt.NDArray, ...]] = {
"fx": (np.array((1, 0, 0)), np.array((0, 1, 0)), np.array((0, 0, 1))),
"fy": (np.array((0, 1, 0)), np.array((1, 0, 0)), np.array((0, 0, 1))),
"fz": (np.array((0, 0, 1)), np.array((0, 1, 0)), np.array((1, 0, 0))),
@@ -83,7 +83,7 @@ class DecorationShader:
PARALLEL DISTRIBUTED FORCE,
DISTRIBUTED MOMENT,
"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out.smooth("VEC3", "forces")
vert_out.smooth("VEC3", "co")
@@ -203,7 +203,7 @@ class DecorationShader:
"""param: pattern: type of pattern
SINGLE FORCE,
SINGLE MOMENT"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments]
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
@@ -253,7 +253,7 @@ class DecorationShader:
def get_planar_shader(self) -> gpu.types.GPUShader:
"""shader for planar loads"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments]
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
-2
View File
@@ -787,7 +787,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
doc: DocPreferences
default_parameters: DefaultParameters
container_hide_show_isolate: bool
mass_time_units_in_wizard: bool
chain_filter_with_set_operations: bool
save_metadata_blend_file: bool
metadata_blend_file_suffix: str
@@ -986,7 +985,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "container_hide_show_isolate")
layout.prop(self, "mass_time_units_in_wizard")
row = layout.row(align=True)
row.prop(self, "chain_filter_with_set_operations")
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
+1 -4
View File
@@ -432,10 +432,7 @@ def update_drawing_name(
if drawing_tool.get_name(drawing) != name:
ifc.run("attribute.edit_attributes", product=drawing, attributes={"Name": name})
# Update the camera object name
camera = ifc.get_object(drawing)
if camera and camera.name != name:
camera.name = name
drawing_tool.set_camera_name(drawing, name)
group = drawing_tool.get_drawing_group(drawing)
if drawing_tool.get_name(group) != name:
+1 -43
View File
@@ -109,7 +109,7 @@ def align_walls(
align_type: AlignType,
):
reference_obj = blender.get_active_object(is_selected=True)
if not (e := ifc.get_entity(reference_obj) or not model.get_usage_type(e) == "LAYER2"):
if not reference_obj or not (e := ifc.get_entity(reference_obj)) or not model.get_usage_type(e) == "LAYER2":
reference_obj = None
objs = [
o
@@ -159,48 +159,6 @@ def extend_wall_to_slab(
model.reload_body_representation(wall_objs)
def join_walls_TZ(
ifc: type[tool.Ifc],
blender: type[tool.Blender],
geometry: type[tool.Geometry],
joiner: DumbWallJoiner,
model: type[tool.Model],
) -> None:
selected_objs = [
o
for o in blender.get_selected_objects()
if (e := ifc.get_entity(o)) and model.get_usage_type(e) in ("LAYER2", "LAYER3")
]
if len(selected_objs) < 2:
raise RequireAtLeastTwoLayeredElements(
"Two or more vertically or horizontally layered elements must be selected to connect their paths together"
)
for obj in selected_objs:
geometry.clear_scale(obj)
elements = [ifc.get_entity(o) for o in blender.get_selected_objects()]
layer2_elements = []
layer3_elements = []
for element in elements:
usage = model.get_usage_type(element)
if usage == "LAYER2":
layer2_elements.append(element)
elif usage == "LAYER3":
layer3_elements.append(element)
if layer3_elements:
target = ifc.get_object(layer3_elements[0])
for element in layer2_elements:
joiner.join_Z(ifc.get_object(element), target)
else:
if not (active_obj := blender.get_active_object()):
active_obj = selected_objs[0]
for obj in selected_objs:
if obj == active_obj:
continue
joiner.join_T(obj, active_obj)
class RequireTwoWallsError(Exception):
pass
+9
View File
@@ -349,7 +349,10 @@ class Drawing:
def enable_editing_text(cls, obj): pass
def ensure_unique_drawing_name(cls, name): pass
def ensure_unique_identification(cls, identification): pass
def export_font_size(cls, obj): pass
def export_symbol(cls, obj): pass
def export_text_literal_attributes(cls, obj): pass
def export_wrap_length(cls, obj): pass
def generate_drawing_matrix(cls, target_view, location_hint): pass
def generate_drawing_name(cls, target_view, location_hint): pass
def generate_reference_attributes(cls, reference, **attributes): pass
@@ -402,6 +405,7 @@ class Drawing:
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
def run_type_assign_type(cls, element=None, relating_type=None): pass
def select_assigned_product(cls, drawing): pass
def set_camera_name(cls, drawing, name): pass
def set_drawing_collection_name(cls, drawing, collection): pass
def set_name(cls, element, name): pass
def setup_annotation_object(cls, obj, object_type): pass
@@ -620,7 +624,10 @@ class Model:
def import_rectangle(cls, obj, position, profile): pass
def load_openings(cls, openings): pass
def purge_scene_openings(cls): pass
def recalculate_walls(cls, objs): pass
def regenerate_array(cls, parent, data): pass
def regenerate_profile(cls, obj): pass
def regenerate_slab(cls, obj): pass
def reload_body_representation(cls, obj_or_objects): pass
def replace_object_ifc_representation(cls, ifc_file, ifc_context, obj, new_representation): pass
@@ -1119,6 +1126,8 @@ class Type:
def get_representation_context(cls, representation): pass
def get_type_occurrences(cls, element_type): pass
def has_material_usage(cls, element): pass
def record_material_usage_attributes(cls, element): pass
def restore_material_usage_attributes(cls, element, usage_attributes): pass
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
def run_geometry_switch_representation(cls, obj=None, representation=None): pass
+28 -9
View File
@@ -49,21 +49,40 @@ class Aggregate(bonsai.core.tool.Aggregate):
related_object = tool.Ifc.get_entity(related_obj)
if not relating_object or not related_object:
return False
if relating_object == related_object:
return False
is_compatible_class = False
if (relating_object.is_a("IfcElement") or relating_object.is_a("IfcElementType")) and related_object.is_a(
"IfcElement"
):
return True
if tool.Ifc.get_schema() == "IFC2X3":
is_compatible_class = True
elif tool.Ifc.get_schema() == "IFC2X3":
if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
return True
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
return True
is_compatible_class = True
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
is_compatible_class = True
else:
if relating_object.is_a("IfcSpatialElement") and related_object.is_a("IfcSpatialElement"):
return True
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
return True
return False
is_compatible_class = True
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
is_compatible_class = True
if not is_compatible_class:
return False
# Prevent cyclic references: walk up the full hierarchy from the
# proposed parent and reject if we encounter the proposed child.
ancestor = ifcopenshell.util.element.get_parent(relating_object)
seen = {relating_object}
while ancestor:
if ancestor == related_object:
return False
if ancestor in seen:
break
seen.add(ancestor)
ancestor = ifcopenshell.util.element.get_parent(ancestor)
return True
@classmethod
def has_physical_body_representation(cls, element: ifcopenshell.entity_instance) -> bool:
+6 -3
View File
@@ -32,6 +32,8 @@ import bonsai.core.tool
import bonsai.tool as tool
if TYPE_CHECKING:
from bsdd.bsdd import ClassContractV1, ClassPropertyContractV1, PropertyContractV5
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDDictionary
@@ -39,8 +41,8 @@ class Bsdd(bonsai.core.tool.Bsdd):
default_identifier_url = "https://identifier.buildingsmart.org"
default_api_url = "https://api.bsdd.buildingsmart.org/api/"
client = bsdd.Client()
bsdd_classes: dict[str, dict] = {}
bsdd_properties: dict[str, dict] = {}
bsdd_classes: dict[str, ClassContractV1] = {}
bsdd_properties: dict[str, ClassPropertyContractV1 | PropertyContractV5] = {}
@classmethod
def identifier_url(cls) -> str:
@@ -267,7 +269,8 @@ class Bsdd(bonsai.core.tool.Bsdd):
@classmethod
def get_bsdd_property(cls, uri: str) -> dict:
if not (bsdd_property := cls.bsdd_properties.get(uri, {})):
bsdd_property = cls.client.get_property(uri, include_classes=True)
# Cache miss occurs for keyword search mode, for classes cache is prepopulated.
bsdd_property = cls.client.get_property(uri)
cls.bsdd_properties[uri] = bsdd_property
return bsdd_property
+1 -1
View File
@@ -154,7 +154,7 @@ class Cost(bonsai.core.tool.Cost):
device = aud.Device()
# chaching.mp3 is by Lucish_ CC-BY-3.0 https://freesound.org/people/Lucish_/sounds/554841/
filepath = tool.Blender.get_data_dir_path("chaching.mp3").__str__()
sound = aud.Sound(filepath)
sound = aud.Sound(filepath) # ty:ignore[too-many-positional-arguments]
device.play(sound)
@classmethod
+8
View File
@@ -857,6 +857,8 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None:
if not literal_attributes:
return
assert (element := tool.Ifc.get_entity(obj))
assert (rep := cls.get_annotation_representation(element))
to_remove = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
@@ -1288,6 +1290,12 @@ class Drawing(bonsai.core.tool.Drawing):
def get_representation(cls, element, context):
return ifcopenshell.util.representation.get_representation(element, context)
@classmethod
def set_camera_name(cls, drawing: ifcopenshell.entity_instance, name: str) -> None:
camera = tool.Ifc.get_object(drawing)
if camera and camera.name != name:
camera.name = name
@classmethod
def set_drawing_collection_name(
cls, drawing: ifcopenshell.entity_instance, collection: bpy.types.Collection
+4 -3
View File
@@ -1407,8 +1407,6 @@ class Geometry(bonsai.core.tool.Geometry):
:param representation_item: item to remove.
:param element: item's element. Is used to unmark manual booleans.
"""
# NOTE: we assume it's not the last representation item
# otherwise we probably would need to remove representation too
# NOTE: a lot of shared code with `geometry.remove_representation`
ifc_file = tool.Ifc.get()
shape_aspects: list[ifcopenshell.entity_instance] = []
@@ -1467,7 +1465,10 @@ class Geometry(bonsai.core.tool.Geometry):
cls.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
if representation:
representation.Items = tuple(set(representation.Items) - {representation_item})
new_items = tuple(set(representation.Items) - {representation_item})
if not new_items:
return
representation.Items = new_items
also_consider = list(consider_inverses)
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
+6 -4
View File
@@ -197,12 +197,16 @@ class Ifc(bonsai.core.tool.Ifc):
if not cls.get():
return
# Clear all per-object msgbus subscriptions at once using the dedicated
# owner. After undo/redo, per-object Python wrappers have new
# identities so clearing by individual obj would miss stale
# subscriptions registered with the old wrappers.
bpy.msgbus.clear_by_owner(bonsai.bim.handler.object_subscription_owner)
for obj in bpy.data.objects:
if obj.library:
continue
bpy.msgbus.clear_by_owner(obj)
element = cls.get_entity(obj)
if not element:
continue
@@ -217,8 +221,6 @@ class Ifc(bonsai.core.tool.Ifc):
if obj.library:
continue
bpy.msgbus.clear_by_owner(obj)
style = cls.get_entity(obj)
if not style:
continue
+1 -1
View File
@@ -284,7 +284,7 @@ class IfcGit:
if re.match("^Ifc", obj.name):
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.orphans_purge(do_recursive=True)
bpy.data.orphans_purge(do_recursive=True) # ty:ignore[unknown-argument]
settings = import_ifc.IfcImportSettings.factory(bpy.context, path_ifc, logging.getLogger("ImportIFC"))
settings.should_setup_viewport_camera = False
+17 -3
View File
@@ -45,9 +45,23 @@ class Nest(bonsai.core.tool.Nest):
related_object = tool.Ifc.get_entity(related_obj)
if not relating_object or not related_object:
return False
if relating_object.is_a("IfcElement") and related_object.is_a("IfcElement"):
return True
return False
if relating_object == related_object:
return False
is_compatible_class = relating_object.is_a("IfcElement") and related_object.is_a("IfcElement")
if not is_compatible_class:
return False
# Prevent cyclic references: walk up the full hierarchy from the
# proposed parent and reject if we encounter the proposed child.
ancestor = ifcopenshell.util.element.get_parent(relating_object)
seen = {relating_object}
while ancestor:
if ancestor == related_object:
return False
if ancestor in seen:
break
seen.add(ancestor)
ancestor = ifcopenshell.util.element.get_parent(ancestor)
return True
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
+10 -32
View File
@@ -93,38 +93,16 @@ class Root(bonsai.core.tool.Root):
elif dest.is_a("IfcTypeProduct"):
if not source.RepresentationMaps:
return copied_entities
# Copy representation maps while preserving mapped representation structures
new_maps = []
for i, rep_map in enumerate(source.RepresentationMaps):
source_rep = rep_map.MappedRepresentation
# Copy the map itself
new_map = ifcopenshell.util.element.copy(tool.Ifc.get(), rep_map)
# Handle the mapped representation - preserve mapping structure if present
if (
source_rep.RepresentationType == "MappedRepresentation"
and len(source_rep.Items) == 1
and source_rep.Items[0].is_a("IfcMappedItem")
):
# This is a mapped representation - preserve the structure
new_rep = ifcopenshell.util.element.copy(tool.Ifc.get(), source_rep)
new_rep.Items = [ifcopenshell.util.element.copy(tool.Ifc.get(), item) for item in source_rep.Items]
new_map.MappedRepresentation = new_rep
else:
# Not a mapped representation - use copy_deep as before
new_map.MappedRepresentation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
source_rep,
exclude=["IfcGeometricRepresentationContext"],
exclude_callback=exclude_callback,
copied_entities=copied_entities,
)
new_maps.append(new_map)
dest.RepresentationMaps = new_maps
dest.RepresentationMaps = [
ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
m,
exclude=["IfcGeometricRepresentationContext"],
exclude_callback=exclude_callback,
copied_entities=copied_entities,
)
for m in source.RepresentationMaps
]
return copied_entities
@classmethod
+1 -10
View File
@@ -75,16 +75,7 @@ class Type(bonsai.core.tool.Type):
@classmethod
def get_model_types(cls) -> list[ifcopenshell.entity_instance]:
ifc_file = tool.Ifc.get()
types = ifc_file.by_type("IfcElementType")
if tool.Ifc.get_schema() == "IFC2X3":
types += ifc_file.by_type("IfcWindowStyle")
types += ifc_file.by_type("IfcDoorStyle")
types += ifc_file.by_type("IfcSpatialStructureElementType")
else:
types += ifc_file.by_type("IfcSpatialElementType")
types += ifc_file.by_type("IfcTypeProduct", include_subtypes=False)
return types
return tool.Ifc.get().by_type("IfcTypeProduct")
@classmethod
def get_object_data(cls, obj: bpy.types.Object) -> Union[bpy.types.ID, None]:
@@ -19,4 +19,5 @@ This chapter covers how you can help contribute to Bonsai.
undo_system
writing_docs
debugging
maintenance
ide/index
@@ -0,0 +1,65 @@
Maintenance
===========
This page documents what needs to be updated in various maintenance scenarios.
Python Version Added or Removed
--------------------------------
When adding or removing a supported Python version, update the following:
.. list-table::
:header-rows: 1
* - File
- What to update
* - ``.github/workflows/ci-black-formatting.yaml``
- ``MIN_IOS_PY_VERSION``
* - ``.github/workflows/ci-ifcopenshell-python-pypi.yml``
- ``pyver`` matrix
* - ``.github/workflows/ci-ifcopenshell-python.yml``
- ``pyver`` matrix
* - ``nix/build-all.py``
- ``PYTHON_VERSIONS`` list
* - ``src/bsdd/pyproject.toml``
- ``requires-python``
* - ``src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst``
- add or remove the row in the ZIP packages table
* - ``src/ifcopenshell-python/Makefile``
- ``SUPPORTED_PYVERSIONS``
* - ``src/ifcopenshell-python/pyproject.toml``
- ``requires-python``
* - ``src/ifcopenshell-python/test/test_package.py``
- ``SUPPORTED_PY_VERSIONS`` tuple
* - ``win/build-all-win.py``
- ``PYTHON_VERSIONS`` list
Blender Version Updated
-----------------------
When a new Blender version is released and supported:
.. list-table::
:header-rows: 1
* - File
- What to update
* - ``.github/workflows/ci-bonsai-daily.yml``
- Blender download URL
Blender's Bundled Python Version Updated
-----------------------------------------
When Blender ships with a new Python version:
.. list-table::
:header-rows: 1
* - File
- What to update
* - ``.github/workflows/ci-black-formatting.yaml``
- ``MIN_BLENDER_PY_VERSION``
* - ``src/bonsai/Makefile``
- ``SUPPORTED_PYVERSIONS``
* - ``src/bonsai/scripts/dev_environment.py``
- ``PYTHON_VERSION`` mapping (Blender version, bundled Python version)
+23
View File
@@ -0,0 +1,23 @@
"""Clone or update Bonsai external dependencies.
Must be run from the repository root.
"""
import subprocess
from pathlib import Path
DEPS = [
("https://projects.blender.org/pioverfour/sun_position.git", "sun_position"),
("https://github.com/kevancress/MeasureIt_ARCH", "MeasureIt_ARCH"),
("https://github.com/nortikin/sverchok.git", "sverchok"),
]
base = Path("src/bonsai/external_dependencies")
base.mkdir(parents=True, exist_ok=True)
for url, name in DEPS:
path = base / name
if not path.exists():
subprocess.check_call(["git", "clone", url, str(path)])
else:
subprocess.check_call(["git", "-C", str(path), "pull", "--rebase"])
+2 -2
View File
@@ -273,10 +273,10 @@ if BPY_IS_LOADED:
f"Couldn't find locale path in the source directory, creating dummy directory: {source_locale_path}.",
)
from ui_translate.settings import ( # pyright: ignore[reportMissingImports]
from ui_translate.settings import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
settings as ui_translate_settings,
)
from ui_translate.update_ui import ( # pyright: ignore[reportMissingImports]
from ui_translate.update_ui import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
UI_OT_i18n_updatetranslation_init_settings,
)
+3 -1
View File
@@ -23,7 +23,9 @@ import bpy
# sys.path.append('C:\Program Files\Python37\Lib\site-packages')
import lxml.etree
from bspy import Gbxml # pyright: ignore[reportMissingImports]
from bspy import ( # ty: ignore[unresolved-import]
Gbxml, # pyright: ignore[reportMissingImports]
)
class GbxmlExporter:
@@ -22,7 +22,7 @@
from math import pi
from pathlib import Path
import boltspy as bolts # pyright: ignore[reportMissingImports]
import boltspy as bolts # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
import ifcopenshell.api
import ifcopenshell.api.material
import ifcopenshell.api.project
+1 -1
View File
@@ -31,7 +31,7 @@ import ifcopenshell.api.spatial
import ifcopenshell.api.unit
import ifcopenshell.guid
import numpy as np
import pymeshlab # pyright: ignore[reportMissingImports]
import pymeshlab # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
class Obj2Ifc:
+1 -1
View File
@@ -31,7 +31,7 @@ import ifcopenshell.api.spatial
import ifcopenshell.api.unit
import ifcopenshell.guid
import numpy as np
import pywavefront # pyright: ignore[reportMissingImports]
import pywavefront # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
class Obj2Ifc:
@@ -385,6 +385,51 @@ Scenario: Edit text - change literal
When I click "Edit Text"
Then I see "Hello World"
Scenario: Add text literal
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I save IFC project
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I press "bim.add_annotation"
And the object "IfcAnnotation/TEXT" is selected
And I look at the "BIM_PT_text" panel
And I click "Enable Editing Text"
And I click the "ADD" after the text "Literals:"
And I set the "2nd Literal" property to "New Literal"
When I click "Edit Text"
Then I see "New Literal"
Scenario: Remove text literal
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I save IFC project
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I press "bim.add_annotation"
And the object "IfcAnnotation/TEXT" is selected
And I look at the "BIM_PT_text" panel
And I click "Enable Editing Text"
And I set the "Literal" property to "Keep This"
And I click the "ADD" after the text "Literals:"
And I set the "2nd Literal" property to "Remove This"
And I click "Edit Text"
And I click "Enable Editing Text"
When I click the "2nd" "X"
And I click "Edit Text"
Then I see "Keep This"
And I don't see "Remove This"
Scenario: Add reference image
Given an empty IFC project
And I save IFC project
@@ -422,6 +422,24 @@ Scenario: Enable editing material set item
When I press "bim.enable_editing_material_set_item(material_set_item={material_profile})"
Then nothing happens
Scenario: Edit layer item defaults null IsVentilated to FALSE in UI
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
And the variable "layer" is "{ifc}.by_type('IfcMaterialLayer')[0].id()"
And I press "bim.enable_editing_material_set_item(material_set_item={layer})"
When I evaluate expression "attrs = bpy.context.active_object.BIMObjectMaterialProperties.material_set_item_attributes; is_vent = next(a for a in attrs if a.name == 'IsVentilated'); assert is_vent.enum_value == 'FALSE'; assert is_vent.is_null is True"
And I press "bim.edit_material_set_item(material_set_item={layer})"
Then I evaluate expression "assert {ifc}.by_id({layer}).IsVentilated is None"
Scenario: Add material set layer
Given an empty IFC project
And I add a cube
+45 -5
View File
@@ -242,7 +242,7 @@ class TemplateListItemSpy(PanelSpy):
self.spied_props: list[dict[str, Any]] = []
self.spied_operators: list[dict[str, Any]] = []
if len(signature(blender_panel.draw_item).parameters) == 8:
blender_panel.draw_item(
blender_panel.draw_item( # ty:ignore[missing-argument]
self,
bpy.context,
self,
@@ -610,8 +610,9 @@ def i_see_the_prop_property_is_value(prop, value):
@then(parsers.parse('I set the "{prop}" property to "{value}"'))
def i_set_the_prop_property_to_value(prop: str, value: str):
"""
:param prop: Could be either property name, property text, property icon
or property index (e.g. "1st", "2nd", "5th").
:param prop: Could be either property name, property text, property icon,
property index (e.g. "1st", "2nd", "5th"), or Nth named property
(e.g. "2nd Literal" for the 2nd property called "Literal").
:param value:
For boolean propeties - 'TRUE' or 'FALSE'.
"""
@@ -619,12 +620,28 @@ def i_set_the_prop_property_to_value(prop: str, value: str):
assert panel_spy
panel_spy.refresh_spy()
is_nth = False
if prop[0].isnumeric() and prop.endswith(("st", "nd", "th")):
is_nth_named = False
nth_target = 0
prop_name = prop
if " " in prop and prop[0].isnumeric():
parts = prop.split(" ", 1)
if parts[0].endswith(("st", "nd", "th")):
is_nth_named = True
nth_target = int(parts[0][:-2]) - 1
prop_name = parts[1]
elif prop[0].isnumeric() and prop.endswith(("st", "nd", "th")):
is_nth = True
named_count = 0
for nth, spied_prop in enumerate(panel_spy.spied_props):
if is_nth and nth != int(prop[:-2]) - 1:
continue
if not is_nth and prop not in (spied_prop["name"], spied_prop["text"], spied_prop["icon"]):
if is_nth_named:
if prop_name not in (spied_prop["name"], spied_prop["text"], spied_prop["icon"]):
continue
if named_count != nth_target:
named_count += 1
continue
elif not is_nth and prop not in (spied_prop["name"], spied_prop["text"], spied_prop["icon"]):
continue
if spied_prop["prop_type"] == "BOOLEAN":
if value == "TRUE":
@@ -873,6 +890,29 @@ def i_click_button(button):
_i_click_button_on_panel(button, panel_spy)
@given(parsers.parse('I click the "{nth}" "{button}"'))
@when(parsers.parse('I click the "{nth}" "{button}"'))
@then(parsers.parse('I click the "{nth}" "{button}"'))
def i_click_the_nth_button(nth, button):
"""
:param nth: Ordinal like "1st", "2nd", "3rd" to select the Nth matching button.
:param button: The text or icon of the button to click.
"""
assert panel_spy
panel_spy.refresh_spy()
target = int(nth[:-2]) - 1
count = 0
for spied_operator in panel_spy.spied_operators:
if spied_operator["text"] == button or spied_operator["icon"] == button:
if count == target:
spied_operator["operator"]("INVOKE_DEFAULT", **spied_operator["kwargs"])
panel_spy.is_spy_dirty = True
return
count += 1
debug = "\n".join([f"{i} {v}" for i, v in enumerate(panel_spy.spied_operators)])
assert False, f"Could not find {nth} {button}:\n{debug}"
@given(parsers.parse('I click the "{button}" after the text "{text}"'))
@when(parsers.parse('I click the "{button}" after the text "{text}"'))
@then(parsers.parse('I click the "{button}" after the text "{text}"'))
+10 -3
View File
@@ -35,9 +35,14 @@ class TestDisableEditingText:
class TestEditText:
def test_run(self, drawing):
drawing.synchronise_ifc_and_text_attributes("obj").should_be_called()
drawing.update_text_size_pset("obj").should_be_called()
drawing.update_text_annotation_properties("obj").should_be_called()
drawing.export_text_literal_attributes("obj").should_be_called().will_return("literal_attributes")
drawing.export_font_size("obj").should_be_called().will_return("font_size")
drawing.edit_text_font_size("obj", "font_size").should_be_called()
drawing.export_wrap_length("obj").should_be_called().will_return("wrap_length")
drawing.edit_text_wrap_length("obj", "wrap_length").should_be_called()
drawing.export_symbol("obj").should_be_called().will_return("symbol")
drawing.edit_text_symbol("obj", "symbol").should_be_called()
drawing.edit_text_literals("obj", "literal_attributes").should_be_called()
drawing.disable_editing_text("obj").should_be_called()
subject.edit_text(drawing, obj="obj")
@@ -466,6 +471,7 @@ class TestRemoveDrawing:
class TestUpdateDrawingName:
def test_do_not_update_if_name_unchanged(self, ifc, drawing):
drawing.get_name("drawing").should_be_called().will_return("name")
drawing.set_camera_name("drawing", "name").should_be_called()
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
drawing.get_name("group").should_be_called().will_return("name")
drawing.get_drawing_collection("drawing").should_be_called().will_return("collection")
@@ -482,6 +488,7 @@ class TestUpdateDrawingName:
def test_run(self, ifc, drawing):
drawing.get_name("drawing").should_be_called().will_return("oldname")
ifc.run("attribute.edit_attributes", product="drawing", attributes={"Name": "name"}).should_be_called()
drawing.set_camera_name("drawing", "name").should_be_called()
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
drawing.get_name("group").should_be_called().will_return("oldname")
ifc.run("attribute.edit_attributes", product="group", attributes={"Name": "name"}).should_be_called()
+4 -2
View File
@@ -23,6 +23,7 @@ from test.core.bootstrap import georeference, ifc
class TestAddGeoreferencing:
def test_run(self, georeference):
georeference.add_georeferencing().should_be_called()
georeference.set_model_origin().should_be_called()
subject.add_georeferencing(georeference)
@@ -35,9 +36,10 @@ class TestEnableEditingGeoreferencing:
class TestRemoveGeoreferencing:
def test_run(self, ifc):
def test_run(self, ifc, georeference):
ifc.run("georeference.remove_georeferencing").should_be_called()
subject.remove_georeferencing(ifc)
georeference.set_model_origin().should_be_called()
subject.remove_georeferencing(ifc, georeference)
class TestDisableEditingGeoreferencing:
+5 -3
View File
@@ -22,8 +22,9 @@ from test.core.bootstrap import geometry, ifc, model, type
class TestAssignType:
def test_assigning_and_switching_to_an_existing_type_data(self, ifc, model, type):
type.record_material_usage_attributes("element").should_be_called().will_return(None)
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
type.has_material_usage("element").should_be_called().will_return(False)
model.get_usage_type("type").should_be_called(2).will_return(None)
ifc.get_object("type").should_be_called().will_return("type_obj")
type.get_object_data("type_obj").should_be_called().will_return("type_obj_data")
type.change_object_data("obj", "type_obj_data", is_global=False).should_be_called()
@@ -31,9 +32,10 @@ class TestAssignType:
type.disable_editing("obj").should_be_called()
subject.assign_type(ifc, model, type, element="element", type="type")
def test_assigning_and_not_changing_data_if_the_type_has_no_data(self, ifc, type):
def test_assigning_and_not_changing_data_if_the_type_has_no_data(self, ifc, model, type):
type.record_material_usage_attributes("element").should_be_called().will_return(None)
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
type.has_material_usage("element").should_be_called().will_return(False)
model.get_usage_type("type").should_be_called(2).will_return(None)
ifc.get_object("type").should_be_called().will_return("type_obj")
type.get_object_data("type_obj").should_be_called().will_return(None)
ifc.get_object("element").should_be_called().will_return("obj")
+37
View File
@@ -18,6 +18,7 @@
import bpy
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.root
@@ -99,6 +100,42 @@ class TestCanAggregate(NewFile):
subelement_obj = bpy.data.objects.new("Object", None)
assert subject.can_aggregate(element_obj, subelement_obj) is False
def test_element_cannot_aggregate_to_itself(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcElementAssembly()
element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj)
assert subject.can_aggregate(element_obj, element_obj) is False
def test_cyclic_aggregation_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assembly_a = ifc.createIfcElementAssembly()
assembly_a_obj = bpy.data.objects.new("AssemblyA", None)
tool.Ifc.link(assembly_a, assembly_a_obj)
beam = ifc.createIfcBeam()
beam_obj = bpy.data.objects.new("Beam", None)
tool.Ifc.link(beam, beam_obj)
ifcopenshell.api.aggregate.assign_object(ifc, products=[beam], relating_object=assembly_a)
assert subject.can_aggregate(beam_obj, assembly_a_obj) is False
def test_deep_cyclic_aggregation_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assembly_a = ifc.createIfcElementAssembly()
assembly_a_obj = bpy.data.objects.new("AssemblyA", None)
tool.Ifc.link(assembly_a, assembly_a_obj)
assembly_b = ifc.createIfcElementAssembly()
assembly_b_obj = bpy.data.objects.new("AssemblyB", None)
tool.Ifc.link(assembly_b, assembly_b_obj)
beam = ifc.createIfcBeam()
beam_obj = bpy.data.objects.new("Beam", None)
tool.Ifc.link(beam, beam_obj)
ifcopenshell.api.aggregate.assign_object(ifc, products=[assembly_b], relating_object=assembly_a)
ifcopenshell.api.aggregate.assign_object(ifc, products=[beam], relating_object=assembly_b)
assert subject.can_aggregate(beam_obj, assembly_a_obj) is False
class TestHasPhysicalBodyRepresentation(NewFile):
def test_run(self):
@@ -42,6 +42,7 @@ class TestAddClassificationReferenceFromBSDD(NewFile):
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
obj = bpy.data.objects["Cube"]
bpy.ops.bim.assign_class(ifc_class="IfcSpace", predefined_type="SPACE", userdefined_type="")
tool.Blender.set_active_object(obj)
element = tool.Ifc.get_entity(obj)
assert element
@@ -66,6 +67,7 @@ class TestAddClassificationReferenceFromBSDD(NewFile):
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
obj = bpy.data.objects["Cube"]
bpy.ops.bim.assign_class(ifc_class="IfcSpace", predefined_type="SPACE", userdefined_type="")
tool.Blender.set_active_object(obj)
element = tool.Ifc.get_entity(obj)
assert element
@@ -110,6 +112,7 @@ class TestAddClassificationReferenceFromBSDD(NewFile):
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
obj = bpy.data.objects["Cube"]
bpy.ops.bim.assign_class(ifc_class="IfcSpace", predefined_type="SPACE", userdefined_type="")
tool.Blender.set_active_object(obj)
element = tool.Ifc.get_entity(obj)
assert element
+4
View File
@@ -176,6 +176,7 @@ class TestStairCalculatedParams(NewFile):
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.1, 0.4)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] += -0.2 + 0.1
self.compare_data(pset_data, calculated_data)
@@ -183,6 +184,7 @@ class TestStairCalculatedParams(NewFile):
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.0, None)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
self.compare_data(pset_data, calculated_data)
@@ -190,6 +192,7 @@ class TestStairCalculatedParams(NewFile):
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (None, 0.0)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
self.compare_data(pset_data, calculated_data)
@@ -197,6 +200,7 @@ class TestStairCalculatedParams(NewFile):
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each
self.compare_data(pset_data, calculated_data)
+37
View File
@@ -19,6 +19,7 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.nest
import ifcopenshell.api.spatial
import bonsai.core.tool
@@ -51,6 +52,42 @@ class TestCanNest(NewFile):
subelement_obj = bpy.data.objects.new("Object", None)
assert subject.can_nest(element_obj, subelement_obj) is False
def test_element_cannot_nest_to_itself(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj)
assert subject.can_nest(element_obj, element_obj) is False
def test_cyclic_nesting_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall_a = ifc.createIfcWall()
wall_a_obj = bpy.data.objects.new("WallA", None)
tool.Ifc.link(wall_a, wall_a_obj)
wall_b = ifc.createIfcWall()
wall_b_obj = bpy.data.objects.new("WallB", None)
tool.Ifc.link(wall_b, wall_b_obj)
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_b], relating_object=wall_a)
assert subject.can_nest(wall_b_obj, wall_a_obj) is False
def test_deep_cyclic_nesting_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall_a = ifc.createIfcWall()
wall_a_obj = bpy.data.objects.new("WallA", None)
tool.Ifc.link(wall_a, wall_a_obj)
wall_b = ifc.createIfcWall()
wall_b_obj = bpy.data.objects.new("WallB", None)
tool.Ifc.link(wall_b, wall_b_obj)
wall_c = ifc.createIfcWall()
wall_c_obj = bpy.data.objects.new("WallC", None)
tool.Ifc.link(wall_c, wall_c_obj)
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_b], relating_object=wall_a)
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_c], relating_object=wall_b)
assert subject.can_nest(wall_c_obj, wall_a_obj) is False
class TestDisableEditing(NewFile):
def test_run(self):
+1 -1
View File
@@ -365,7 +365,7 @@ class TestLoadingIfcSqlite(NewFile):
sql_type="SQLite",
)
patcher.patch()
tmp_file = Path(tempfile.mktemp(suffix=".ifcsqlite"))
tmp_file = Path(tempfile.mkstemp(suffix=".ifcsqlite")[1])
ifcpatch.write(patcher.get_output(), tmp_file)
elements_with_meshes = [
+41
View File
@@ -0,0 +1,41 @@
aiohttp
beautifulsoup4
boto3
botocore
brickschema
cjio >=0.8, <0.10
debugpy
ezdxf
fake-bpy-module-latest
git+https://github.com/prochitecture/bpypolyskel
git+https://github.com/Andrej730/IFC2JSON_python.git@pyproject_toml
gitpython
isodate
lark
lxml
lxml-stubs
markdown-it-py
natsort
numpy
odfpy
openpyxl
pandas
pillow
platformdirs
pygments
pyradiance
pystache
pytest
pytest_bdd
pytest_blender
python-dateutil
python-socketio
pytz
rdflib
requests
shapely
svgwrite
typing-extensions
typst
tzfpy
xsdata
+1 -1
View File
@@ -7,7 +7,7 @@ from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
from .type_hints import *
from type_hints import *
def _lower_first(s: str) -> str:
+1 -1
View File
@@ -955,7 +955,7 @@ def get_unit_type_name(ifc_file: ifcopenshell.file, unit_type: str) -> Union[str
return val(unit.Currency)
def get_unit_name(ifc_file: ifcopenshell.entity_instance, unit: ifcopenshell.entity_instance) -> Union[str, None]:
def get_unit_name(unit: ifcopenshell.entity_instance) -> Union[str, None]:
if unit.is_a("IfcNamedUnit"):
return val(unit.Name)
+1 -1
View File
@@ -953,7 +953,7 @@ def get_unit_type_name(ifc_file: ifcopenshell.file, unit_type: str) -> Union[str
return val(unit.Currency)
def get_unit_name(ifc_file: ifcopenshell.entity_instance, unit: ifcopenshell.entity_instance) -> Union[str, None]:
def get_unit_name(unit: ifcopenshell.entity_instance) -> Union[str, None]:
if unit.is_a("IfcNamedUnit"):
return val(unit.Name)
@@ -29,8 +29,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst)
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
}
Eigen::Vector3d o, axis(0, 0, 1), refDirection;
taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
Eigen::Vector3d o = m->components().col(3).head<3>();
o = m->components().col(3).head<3>();
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
// 1) Axis is given but not RefDirection
@@ -38,12 +40,43 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst)
// 3) Neither Axis or RefDirection are provided
// 4) Both Axis and RefDirection are provided
Eigen::Vector3d z = inst->Axis() ? *taxonomy::cast<taxonomy::direction3>(map(inst->Axis()))->components_ : Eigen::Vector3d(0,0,1); // Axis is (0,0,1) when omitted
Eigen::Vector3d rd = inst->RefDirection() ? *taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()))->components_ : m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
Eigen::Vector3d y = z.cross(rd);
Eigen::Vector3d x = y.cross(z);
const bool hasAxis = inst->Axis() != nullptr;
const bool hasRef = inst->RefDirection() != nullptr;
return taxonomy::make<taxonomy::matrix4>(o, z, x);
/*
if (hasAxis != hasRef) {
Logger::Warning("Axis and RefDirection should be specified together", inst);
}
*/
if (hasAxis && !hasRef) {
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
axis = *a->components_;
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
// refDirection is not necessarily orthogonal to axis.
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
refDirection = axis.cross(refDirection).cross(axis);
} else if (!hasAxis && hasRef) {
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
refDirection = *r->components_;
Eigen::Vector3d up(0, 0, 1);
axis = refDirection.cross(up.cross(refDirection));
} else if (!hasAxis && !hasRef) {
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
Eigen::Vector3d up(0, 0, 1);
axis = refDirection.cross(up.cross(refDirection));
} else {
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
axis = *a->components_;
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
refDirection = *r->components_;
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
}
// axis and refDirection need to be orthogonal
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
}
#endif
+7 -25
View File
@@ -27,29 +27,15 @@ SED:=sed -i '' -e
endif
endif
# TODO: we should simplify this at some point...
ifeq ($(PYVERSION), py39)
PYNUMBER:=39
endif
ifeq ($(PYVERSION), py310)
PYNUMBER:=310
endif
ifeq ($(PYVERSION), py311)
PYNUMBER:=311
endif
ifeq ($(PYVERSION), py312)
PYNUMBER:=312
endif
ifeq ($(PYVERSION), py313)
PYNUMBER:=313
endif
ifeq ($(PYVERSION), py314)
PYNUMBER:=314
endif
ifndef PYNUMBER
$(error Unsupported PYVERSION '$(PYVERSION)')
SUPPORTED_PYVERSIONS := py310 py311 py312 py313 py314
ifeq ($(filter $(PYVERSION),$(SUPPORTED_PYVERSIONS)),)
$(error Unsupported PYVERSION=$(PYVERSION). Must be one of $(SUPPORTED_PYVERSIONS))
endif
PYMINOR:=$(subst py3,,$(PYVERSION))
PYNUMBER:=3$(PYMINOR)
# We actually do support glibc 2.28-2.30 (see #5636)
# but those are old and there's no demand for it.
ifeq ($(PLATFORM), linux64)
@@ -93,10 +79,6 @@ test-parallel:
test-mathutils:
pytest -p no:pytest-blender test/util/test_shape_builder.py
.PHONY: build-ids-docs
build-ids-docs:
mkdir -p test/build
cd test && python ids_doc_generator.py
.PHONY: qa
qa:
@@ -257,7 +257,8 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce
"``join({{separator}}, {{values}})``", "``join(""-"", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated."
"``{{value1}}[+-*/]{{value2}}``", "``{{z}}+3``", "``5``", "Does arithmetic. Typical operators such as +, -, \*, and / are allowed and can be mixed with other variables and formatting functions."
When using queries in an IfcAnnotation tag surround with backticks.
Examples:
````number({{Qto_WallBaseQuantities.Width}}, ",",".")```` or
````round({{Qto_BuildingElementProxyQuantities.NetVolume}},.1)````
When using queries in an IfcAnnotation tag surround with backticks. Examples:
- ````number({{Qto_WallBaseQuantities.Width}}, ",",".")````
- ````round({{Qto_BuildingElementProxyQuantities.NetVolume}},.1)````
- ````join(", OVER ", reverse({{material.item.Material.Name}}))````
@@ -111,8 +111,8 @@ __all__ = [
]
try:
from .stream import stream, stream_entity
from .stream import stream as _stream
from .stream import stream, stream_entity # ty: ignore[possibly-missing-import]
from .stream import stream as _stream # ty: ignore[possibly-missing-import]
except:
pass
@@ -199,11 +199,13 @@ def open(
for ty in bypass_types:
f.bypass_type(ty)
if mmap:
f.initialize(str(path.absolute()), mmap=mmap)
# mmap parameter is only available for builds with USE_MMAP, not used in our main builds
f.initialize(str(path.absolute()), mmap=mmap) # type: ignore[unknown-argument]
else:
f.initialize(str(path.absolute()))
elif mmap:
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap)
# mmap parameter is only available for builds with USE_MMAP, not used in our main builds
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap) # type: ignore[unknown-argument]
else:
f = ifcopenshell_wrapper.open(str(path.absolute()))
return file(f)
@@ -42,7 +42,6 @@ import importlib
import inspect
import json
from collections.abc import Callable
from functools import partial
from typing import TYPE_CHECKING, Any, Optional
import numpy
@@ -90,11 +89,7 @@ def renamed_arguments_deprecation(
# "group.add_group": partial(
# renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
# ),
ARGUMENTS_DEPRECATION: dict[str, Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]] = {
"control.assign_control": partial(
batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects"
),
}
ARGUMENTS_DEPRECATION: dict[str, Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]] = {}
CACHED_USECASE_CLASSES: dict[str, Callable] = {}
@@ -51,8 +51,10 @@ def remove_context(file: ifcopenshell.file, context: ifcopenshell.entity_instanc
new = context.ParentContext
for inverse in file.get_inverse(context):
if inverse.is_a("IfcCoordinateOperation"):
# Trick to make sure the coordinate operation is not referenced
# by a context so we can delete it safely
inverse.SourceCRS = inverse.TargetCRS
ifcopenshell.util.element.remove_deep(file, inverse)
ifcopenshell.util.element.remove_deep2(file, inverse)
else:
ifcopenshell.util.element.replace_attribute(inverse, context, new)
file.remove(context)
@@ -59,6 +59,6 @@ def edit_cost_value(
value["ValueComponent"],
)
value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"])
if old_unit_basis and file.get_total_inverses(old_unit_basis) == 0:
ifcopenshell.util.element.remove_deep(file, old_unit_basis)
if old_unit_basis:
ifcopenshell.util.element.remove_deep2(file, old_unit_basis)
setattr(cost_value, name, value)
@@ -31,17 +31,6 @@ def add_boolean(
) -> list[ifcopenshell.entity_instance]:
"""Adds a boolean operation to two or more representation items
If an IfcBooleanOperand is part of the top level items in an
IfcShapeRepresentation, it will be removed from that level whilst being
added to the IfcBooleanResult. This is because it is generally intuitive
that an item is either participating in a boolean operation, or being an
item in its own right, but not both.
However, if an IfcBooleanOperand is part of another boolean operation
already, it will not be removed from the existing operation. A new
operation will be created, and therefore it will participate in two
operations.
This function protects against recursive booleans.
After a boolean operation is made, since the items of
@@ -101,9 +90,6 @@ def add_boolean(
booleans = []
for second_item in second_items:
for inverse in file.get_inverse(second_item):
if inverse.is_a("IfcShapeRepresentation"):
inverse.Items = list(set(inverse.Items) - {second_item})
if first.is_a("IfcTesselatedFaceSet"):
first.Closed = True # For now, trust the user to do the right thing.
if second_item.is_a("IfcTesselatedFaceSet"):
@@ -20,11 +20,11 @@ from __future__ import annotations
import math
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
import bmesh # pyright: ignore[reportMissingImports]
import bpy # pyright: ignore[reportMissingImports]
import bmesh # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
import bpy # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
import numpy as np
import numpy.typing as npt
from mathutils import Matrix, Vector # pyright: ignore[reportMissingImports]
from mathutils import Matrix, Vector # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
@@ -83,6 +83,7 @@ def validate_type(
if remaining_items:
ifcopenshell.api.geometry.add_boolean(file, preferred_item, remaining_items, "UNION")
representation.Items = [i for i in representation.Items if i not in remaining_items]
representation.RepresentationType = ifcopenshell.util.representation.guess_type(representation.Items)
if representation.RepresentationType == "CSG":
@@ -42,7 +42,5 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance
ifcopenshell.api.grid.remove_grid_axis(model, axis=axis_2)
"""
axis_curve = axis.AxisCurve
if file.get_total_inverses(axis_curve) == 1:
ifcopenshell.util.element.remove_deep(file, axis_curve)
file.remove(axis_curve)
file.remove(axis)
ifcopenshell.util.element.remove_deep2(file, axis_curve)
@@ -19,7 +19,9 @@
from typing import Union
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.owner
import ifcopenshell.api.spatial
import ifcopenshell.guid
import ifcopenshell.util.element
@@ -137,7 +139,10 @@ def assign_object(
if not objects_to_change:
return is_nested_by
# NOTE: An object can both be nested and assigned to a container or an aggregate.
# Can be either only nested, aggregated, or contained at the same time.
possibly_contained = [o for o in objects_without_nests if hasattr(o, "ContainedInStructure")]
ifcopenshell.api.spatial.unassign_container(file, products=possibly_contained)
ifcopenshell.api.aggregate.unassign_object(file, products=objects_without_nests)
# unassign elements from previous nests
for nests in previous_nests_rels:
@@ -431,7 +431,7 @@ class Usecase:
)
ifcopenshell.api.type.assign_type(
self.file,
should_run_listeners=False,
should_run_listeners=False, # ty:ignore[unknown-argument]
related_objects=[element],
relating_type=new_type,
should_map_representations=False,
@@ -53,9 +53,7 @@ def create_file(version: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4") -> ifcope
"""
file = ifcopenshell.file(schema=version)
file.header.file_name.name = "/dev/null" # Hehehe
file.header.file_name.time_stamp = (
datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone().replace(microsecond=0).isoformat()
)
file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
file.header.file_name.originating_system = "IfcOpenShell {}".format(ifcopenshell.version)
file.header.file_name.authorization = "Nobody"
@@ -22,9 +22,9 @@ import ifcopenshell.util.element
def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.entity_instance) -> None:
"""Removes a property template
Note that a property set template should always have at least one
property template to be valid, so take care when removing property
templates.
Note that a property set template should always have at least one property
template to be valid. So a property set template will not be removed if it
is the only template ina a property ste template.
:param prop_template: The IfcSimplePropertyTemplate to remove.
:return: None
@@ -43,10 +43,8 @@ def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.en
ifcopenshell.api.pset_template.remove_prop_template(model, prop_template=prop2)
"""
for inverse in file.get_inverse(prop_template):
if len(inverse.HasPropertyTemplates) == 1:
inverse.HasPropertyTemplates = []
else:
if len(inverse.HasPropertyTemplates) > 1:
has_property_templates = list(inverse.HasPropertyTemplates)
has_property_templates.remove(prop_template)
inverse.HasPropertyTemplates = has_property_templates
ifcopenshell.util.element.remove_deep(file, prop_template)
ifcopenshell.util.element.remove_deep2(file, prop_template)
@@ -38,4 +38,4 @@ def remove_pset_template(file: ifcopenshell.file, pset_template: ifcopenshell.en
# Let's remove the template.
ifcopenshell.api.pset_template.remove_pset_template(model, pset_template=template)
"""
ifcopenshell.util.element.remove_deep(file, pset_template)
ifcopenshell.util.element.remove_deep2(file, pset_template)
@@ -79,5 +79,5 @@ def add_resource_quantity(
old_quantity = resource.BaseQuantity
resource.BaseQuantity = quantity
if old_quantity:
ifcopenshell.util.element.remove_deep(file, old_quantity)
ifcopenshell.util.element.remove_deep2(file, old_quantity)
return quantity
@@ -47,4 +47,4 @@ def remove_resource_quantity(file: ifcopenshell.file, resource: ifcopenshell.ent
old_quantity = resource.BaseQuantity
resource.BaseQuantity = None
if old_quantity:
ifcopenshell.util.element.remove_deep(file, old_quantity)
ifcopenshell.util.element.remove_deep2(file, old_quantity)
@@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Any, Optional
import ifcopenshell
if TYPE_CHECKING:
import bpy # pyright: ignore[reportMissingImports]
import bpy # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
def add_surface_textures(
@@ -65,6 +65,8 @@ def disconnect_port(file: ifcopenshell.file, port: ifcopenshell.entity_instance)
rels += port.ConnectedFrom or ()
for rel in rels:
rel.RelatingPort.FlowDirection = None
rel.RelatedPort.FlowDirection = None
history = rel.OwnerHistory
file.remove(rel)
if history:
@@ -47,4 +47,5 @@ def remove_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance) ->
unit_assignment.Units = units
else:
file.remove(unit_assignment)
ifcopenshell.util.element.remove_deep(file, unit)
# TODO handle other possible unit inverses
ifcopenshell.util.element.remove_deep2(file, unit)
@@ -33,14 +33,14 @@ def _has_occ():
# Previous versions (pythonocc<=0.17.3) are using just OCC.
try:
import OCC.Core.BRepTools # pyright: ignore[reportMissingImports]
import OCC.Core.BRepTools # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
return True
except ImportError:
pass
try:
import OCC.BRepTools # noqa: F401 # pyright: ignore[reportMissingImports]
import OCC.BRepTools # noqa: F401 # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
return True
except ImportError:
@@ -28,7 +28,7 @@ from ..file import file
from . import has_occ
if TYPE_CHECKING:
from OCC.Core import TopoDS # pyright: ignore[reportMissingImports]
from OCC.Core import TopoDS # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
IteratorOutput = Union["ShapeElementType", "utils.shape_tuple"]
@@ -47,9 +47,9 @@ if has_occ:
from . import occ_utils as utils
try:
from OCC.Core import TopoDS # pyright: ignore[reportMissingImports]
from OCC.Core import TopoDS # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
except ImportError:
from OCC import TopoDS # pyright: ignore[reportMissingImports]
from OCC import TopoDS # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
def wrap_shape_creation(settings: settings, shape: ifcopenshell_wrapper.Element):
if getattr(settings, "use_python_opencascade", False):
@@ -89,6 +89,7 @@ SETTING = Literal[
"keep-bounding-boxes",
"layerset-first",
"length-unit",
"make-volume",
"max-offset-deviation",
"max-offset",
"mesher-angular-deflection",
@@ -124,6 +125,7 @@ SERIALIZER_SETTING = Literal[
"ecef",
"digits",
"wkt-use-section",
"separate-z-up-node",
]
# NOTE: hybrid-cgal-simple-opencascade is added just as an example
@@ -26,17 +26,33 @@ import warnings
from collections.abc import Iterable
from typing import NamedTuple, Union
import OCC # pyright: ignore[reportMissingImports]
import OCC # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
from typing_extensions import assert_never
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
try:
from OCC.Core import AIS, BRepTools, Graphic3d, Quantity, TopoDS, V3d, gp # pyright: ignore[reportMissingImports]
from OCC.Core import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
AIS,
BRepTools,
Graphic3d,
Quantity,
TopoDS,
V3d,
gp,
)
USE_OCCT_HANDLE = False
except ImportError:
from OCC import AIS, BRepTools, Graphic3d, Quantity, TopoDS, V3d, gp # pyright: ignore[reportMissingImports]
from OCC import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
AIS,
BRepTools,
Graphic3d,
Quantity,
TopoDS,
V3d,
gp,
)
USE_OCCT_HANDLE = True
@@ -68,7 +84,7 @@ DEFAULT_STYLES = {
def initialize_display():
import OCC.Display.SimpleGui # pyright: ignore[reportMissingImports]
import OCC.Display.SimpleGui # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
global handle, main_loop, add_menu, add_function_to_menu
handle, main_loop, add_menu, add_function_to_menu = OCC.Display.SimpleGui.init_display()
@@ -104,6 +104,7 @@ class IfcSpfHeader:
https://standards.buildingsmart.org/documents/Implementation/ImplementationGuide_IFCHeaderData_Version_1.0.2.pdf
"""
def __init__(self, *args): ...
def file(self, *args): ...
def file_description_py(self): ...
def file_name_py(self): ...
@@ -113,7 +114,8 @@ class IfcSpfHeader:
def write(self, out): ...
class BRep(Representation):
def as_compound(self, force_meters): ...
def __init__(self, settings, entity, id, shapes): ...
def as_compound(self, force_meters=False): ...
def begin(self): ...
def calculate_projected_surface_area(self, ax, along_x, along_y, along_z): ...
def calculate_surface_area(self, arg2): ...
@@ -125,6 +127,7 @@ class BRep(Representation):
def size(self): ...
class BRepElement(Element):
def __init__(self, id, parent_id, name, type, guid, context, trsf, geometry, product): ...
def calculate_projected_surface_area(self, along_x, along_y, along_z): ...
@property
def geometry(self) -> BRep: ...
@@ -135,6 +138,7 @@ class BRepElement(Element):
def volume(self): ...
class ColladaSerializer(WriteOnlyGeometrySerializer):
def __init__(self, dae_filename, geometry_settings, settings): ...
def finalize(self): ...
def isTesselated(self): ...
def object_id(self, o): ...
@@ -150,8 +154,9 @@ class ConversionResult:
def Shape(self): ...
def Style(self): ...
def StylePtr(self): ...
def __init__(self, *args): ...
def append(self, trsf): ...
def apply_transform(self, unit_scale): ...
def apply_transform(self, unit_scale=1.0): ...
def hasStyle(self): ...
def prepend(self, trsf): ...
def setStyle(self, newStyle): ...
@@ -159,6 +164,7 @@ class ConversionResult:
class ConversionResultShape:
def Serialize(self, place, arg3): ...
def Triangulate(self, *args): ...
def __init__(self, *args, **kwargs): ...
def add(self, arg2): ...
def area(self): ...
def axis(self): ...
@@ -193,6 +199,7 @@ class ConversionResultShape:
def wrap_in_compound(self): ...
class DoubleArray3:
def __init__(self, *args): ...
def back(self): ...
def begin(self): ...
def empty(self): ...
@@ -206,6 +213,7 @@ class DoubleArray3:
def swap(self, v): ...
class Element:
def __init__(self, settings, id, parent_id, name, type, guid, context, trsf, product): ...
# TODO: Remove from the wrapper?
def SetParents(self, newparents): ...
# TODO: could it be None?
@@ -266,6 +274,7 @@ class Element:
class GeometrySerializer:
READ_BREP: Any
READ_TRIANGULATION: Any
def __init__(self, *args, **kwargs): ...
def geometry_settings(self, *args): ...
def isTesselated(self): ...
def object_id(self, o): ...
@@ -275,6 +284,7 @@ class GeometrySerializer:
def write(self, *args): ...
class GltfSerializer(WriteOnlyGeometrySerializer):
def __init__(self, filename, geometry_settings, settings): ...
def finalize(self): ...
def isTesselated(self): ...
def ready(self): ...
@@ -284,6 +294,7 @@ class GltfSerializer(WriteOnlyGeometrySerializer):
def writeHeader(self): ...
class HdfSerializer(GeometrySerializer):
def __init__(self, hdf_filename, geometry_settings, settings, read_only=False): ...
def finalize(self): ...
def isTesselated(self): ...
def read(self, *args): ...
@@ -295,6 +306,7 @@ class HdfSerializer(GeometrySerializer):
def writeHeader(self): ...
class IfcBaseEntity(entity_instance):
def __init__(self, *args, **kwargs): ...
def declaration(self): ...
def get(self, name): ...
def get_inverse(self, name): ...
@@ -302,26 +314,31 @@ class IfcBaseEntity(entity_instance):
def set_id(self, i): ...
class IfcBaseType(entity_instance):
def __init__(self, *args, **kwargs): ...
def declaration(self): ...
class IfcEntityInstanceData: ...
class IfcEntityInstanceData:
def __init__(self, *args, **kwargs): ...
class IfcLateBoundEntity(IfcBaseEntity):
def __init__(self, decl, data): ...
def declaration(self): ...
class InstanceStreamer:
def __init__(self, *args): ...
def bypassTypes(self, type_names): ...
def bypassed_instances(self): ...
coerce_attribute_count: bool
def hasSemicolon(self): ...
def inverses(self, *args): ...
def pushPage(self, page): ...
def readInstancePy(self, type_as_declaration_instance): ...
def readInstancePy(self, type_as_declaration_instance=False): ...
def references(self, *args): ...
def semicolonCount(self): ...
def status(self): ...
class Iterator:
def __init__(self, *args): ...
initialization_outcome_: Any
processed_: Any
def bounds_max(self): ...
@@ -367,25 +384,36 @@ class Iterator:
class JsonSerializer:
JSON_DIALECT_CREOOX: Any
def __init__(self, *args): ...
def finalize(self): ...
def ready(self): ...
def setFile(self, arg2): ...
def writeHeader(self): ...
# TODO: MakeVolume is ignored in SWIG, remove from stub once build is bumped.
class MakeVolume:
defaultvalue: Any
description: Any
name: Any
class OpaqueCoordinate_3:
def __init__(self, *args): ...
def get(self, i): ...
def set(self, i, n): ...
class OpaqueCoordinate_4:
def __init__(self, *args): ...
def get(self, i): ...
def set(self, i, n): ...
class OpaqueNumber:
def __init__(self, *args, **kwargs): ...
def clone(self): ...
def to_double(self): ...
def to_string(self): ...
class Representation:
def __init__(self, settings, entity, id): ...
def entity(self): ...
@property
def id(self) -> str:
@@ -398,18 +426,21 @@ class Representation:
def settings(self): ...
class RocksDBPrefixIterator:
def __init__(self, storage, prefix): ...
def key(self): ...
def next(self): ...
def valid(self): ...
def value(self): ...
class RocksDbSerializer:
def __init__(self, *args): ...
def finalize(self): ...
def ready(self): ...
def setFile(self, arg2): ...
def writeHeader(self): ...
class Serialization(Representation):
def __init__(self, brep): ...
@property
def brep_data(self): ...
@property
@@ -418,6 +449,7 @@ class Serialization(Representation):
def surface_styles(self): ...
class SerializedElement(Element):
def __init__(self, shape_model): ...
@property
def geometry(self) -> Serialization: ...
@@ -434,6 +466,7 @@ class Settings:
def setting_names(self): ...
class SvgSerializer(WriteOnlyGeometrySerializer):
def __init__(self, out_filename, geometry_settings, settings): ...
SH_NONE: Any
SH_FULL: Any
SH_LEFT: Any
@@ -479,8 +512,8 @@ class SvgSerializer(WriteOnlyGeometrySerializer):
def setPrintSpaceNames(self, b): ...
def setProfileThreshold(self, i): ...
def setScale(self, s): ...
def setSectionHeight(self, h, storey): ...
def setSectionHeightsFromStoreys(self, offset): ...
def setSectionHeight(self, h, storey=None): ...
def setSectionHeightsFromStoreys(self, offset=1.2): ...
def setSectionRef(self, s): ...
def setSegmentProjection(self, b): ...
def setSpaceNameTransform(self, v): ...
@@ -497,22 +530,25 @@ class SvgSerializer(WriteOnlyGeometrySerializer):
def writeHeader(self): ...
class SwigPyIterator:
def __init__(self, *args, **kwargs): ...
def advance(self, n): ...
def copy(self): ...
def decr(self, n): ...
def decr(self, n=1): ...
def distance(self, x): ...
def equal(self, x): ...
def incr(self, n): ...
def incr(self, n=1): ...
def next(self): ...
def previous(self): ...
def value(self): ...
class Transformation:
def __init__(self, settings, matrix): ...
def data(self): ...
@property
def matrix(self): ...
class Triangulation(Representation):
def __init__(self, *args): ...
def addEdge(self, item_id, style, i0, i1): ...
def addFace(self, *args): ...
def addNormal(self, X, Y, Z): ...
@@ -567,21 +603,24 @@ class Triangulation(Representation):
def verts_buffer(self) -> bytes: ...
class TriangulationElement(Element):
def __init__(self, *args): ...
@property
def geometry(self) -> Triangulation: ...
def geometry_pointer(self): ...
class TtlWktSerializer(WriteOnlyGeometrySerializer):
def __init__(self, filename, geometry_settings, settings): ...
def finalize(self): ...
def isTesselated(self): ...
def ready(self): ...
def setFile(self, arg2): ...
def setUnitNameAndMagnitude(self, arg2, arg3): ...
def ttl_object_id(self, o, postfix): ...
def ttl_object_id(self, o, postfix=None): ...
def write(self, *args): ...
def writeHeader(self): ...
class WaveFrontOBJSerializer(WriteOnlyGeometrySerializer):
def __init__(self, obj_filename, mtl_filename, geometry_settings, settings): ...
def finalize(self): ...
def isTesselated(self): ...
def ready(self): ...
@@ -592,9 +631,11 @@ class WaveFrontOBJSerializer(WriteOnlyGeometrySerializer):
def writeMaterial(self, style): ...
class WriteOnlyGeometrySerializer(GeometrySerializer):
def __init__(self, *args, **kwargs): ...
def read(self, *args): ...
class XmlSerializer:
def __init__(self, file, xml_filename): ...
def finalize(self): ...
def ready(self): ...
def setFile(self, arg2): ...
@@ -603,6 +644,7 @@ class XmlSerializer:
class _SwigNonDynamicMeta(type): ...
class abstract_arrangement:
def __init__(self, *args, **kwargs): ...
def get_face_pairs(self): ...
def merge(self, edge_indices): ...
def num_edges(self): ...
@@ -610,6 +652,7 @@ class abstract_arrangement:
def write(self, polygons, progress): ...
class aggregation_type(parameter_type):
def __init__(self, type_of_aggregation, bound1, bound2, type_of_element): ...
array_type: Any
bag_type: Any
list_type: Any
@@ -622,6 +665,7 @@ class aggregation_type(parameter_type):
def type_of_element(self) -> parameter_type: ...
class attribute:
def __init__(self, name, type_of_attribute, optional): ...
def name(self) -> str: ...
def optional(self) -> bool: ...
def type_of_attribute(self) -> parameter_type: ...
@@ -662,11 +706,13 @@ class bspline_surface(surface):
def kind(self): ...
class buffer:
def __init__(self, *args): ...
def filename(self): ...
def get_value(self): ...
def is_ready(self): ...
class cant_function(function_item):
def __init__(self, *args): ...
def calc_hash(self): ...
def clone_(self): ...
def end(self): ...
@@ -694,6 +740,7 @@ class clash:
p2: Any
class clashes:
def __init__(self, *args): ...
def append(self, x): ...
def assign(self, n, x): ...
def back(self): ...
@@ -727,6 +774,7 @@ class collection:
def matrix(self): ...
class colour(item):
def __init__(self, *args): ...
def r(self) -> float: ...
def g(self) -> float: ...
def b(self) -> float: ...
@@ -740,6 +788,7 @@ class colour(item):
def kind(self) -> int: ...
class context:
def __init__(self, *args): ...
def add(self, segments): ...
def build(self): ...
def get_face_pairs(self): ...
@@ -749,7 +798,8 @@ class context:
def write(self, arg2): ...
class curve(geom_item):
def print_impl(self, o, classname, indent): ...
def __init__(self, *args, **kwargs): ...
def print_impl(self, o, classname, indent=0): ...
class cylinder(surface):
radius: Any
@@ -760,6 +810,7 @@ class cylinder(surface):
def matrix(self): ...
class declaration:
def __init__(self, name, index_in_schema): ...
def _is(self, *args: Union[str, declaration]) -> bool: ...
def as_entity(self) -> Union[entity, None]: ...
def as_enumeration_type(self) -> Union[enumeration_type, None]: ...
@@ -775,6 +826,7 @@ class declaration:
def type(self): ...
class direction3:
def __init__(self, *args): ...
def calc_hash(self): ...
def clone_(self): ...
@property
@@ -786,6 +838,7 @@ class drawing_meta:
pln_3d: Any
class edge(trimmed_curve):
def __init__(self, *args): ...
def calc_hash(self): ...
def clone_(self): ...
def kind(self): ...
@@ -800,6 +853,7 @@ class ellipse(curve):
def matrix(self): ...
class entity(declaration):
def __init__(self, name, is_abstract, index_in_schema, supertype): ...
def all_attributes(self) -> tuple[attribute, ...]: ...
def all_inverse_attributes(self) -> tuple[inverse_attribute, ...]: ...
def argument_types(self) -> tuple[str, ...]:
@@ -828,6 +882,7 @@ class entity(declaration):
def supertype(self) -> Union[entity, None]: ...
class entity_instance:
def __init__(self, *args, **kwargs): ...
file: ifcopenshell.file
"""Reference to IFC file to prevent it's garbage collection, if entity is still used."""
@@ -881,11 +936,12 @@ class entity_instance:
def setArgumentAsNull(self, i): ...
def setArgumentAsString(self, i, a): ...
def set_attribute_value(self, *args): ...
def toString(self, arg2, upper): ...
def to_string(self, valid_spf): ...
def toString(self, arg2, upper=False): ...
def to_string(self, valid_spf=True): ...
def unset_attribute_value(self, i): ...
class enumeration_type(declaration):
def __init__(self, name, index_in_schema, enumeration_items): ...
def argument_types(self) -> tuple[str, ...]: ...
def as_enumeration_type(self) -> enumeration_type: ...
def enumeration_items(self) -> tuple[str, ...]: ...
@@ -898,6 +954,7 @@ class enumeration_type(declaration):
...
class extrusion(sweep):
def __init__(self, m, basis, dir, d): ...
depth: Any
direction: Any
def calc_hash(self): ...
@@ -919,7 +976,8 @@ class face:
class file:
def FreshId(self): ...
def add(self, entity: entity_instance, id: int) -> entity_instance: ...
def __init__(self, *args): ...
def add(self, entity: entity_instance, id: int = -1) -> entity_instance: ...
def addEntities(self, entities): ...
def add_type_ref(self, new_entity): ...
def batch(self) -> None:
@@ -1010,9 +1068,9 @@ class file:
def storage_mode(self): ...
def to_string(self): ...
@staticmethod
def traverse(instance: entity_instance, max_level: int) -> tuple[entity_instance, ...]: ...
def traverse(instance: entity_instance, max_level: int = -1) -> tuple[entity_instance, ...]: ...
@staticmethod
def traverse_breadth_first(instance: entity_instance, max_level: int) -> tuple[entity_instance, ...]: ...
def traverse_breadth_first(instance: entity_instance, max_level: int = -1) -> tuple[entity_instance, ...]: ...
def types(self) -> tuple[str, ...]:
"""Return a tuple of classes present in the file.
@@ -1029,9 +1087,11 @@ class file_open_status:
UNSUPPORTED_SCHEMA: int
INVALID_SYNTAX: int
UNKNOWN: int
def __init__(self, *args): ...
def value(self): ...
class fn_evaluator:
def __init__(self, *args, **kwargs): ...
settings_: Any
def clone(self): ...
def end(self): ...
@@ -1040,6 +1100,7 @@ class fn_evaluator:
def start(self): ...
class function_item(implicit_item):
def __init__(self, *args, **kwargs): ...
def calc_hash(self): ...
def end(self): ...
def kind(self): ...
@@ -1047,10 +1108,12 @@ class function_item(implicit_item):
def start(self): ...
class function_item_evaluator:
def __init__(self, *args): ...
def evaluate(self, *args): ...
def evaluation_points(self, *args): ...
class functor_item(function_item):
def __init__(self, *args): ...
def calc_hash(self): ...
def clone_(self): ...
def end(self): ...
@@ -1058,6 +1121,7 @@ class functor_item(function_item):
def start(self): ...
class geom_item(item):
def __init__(self, *args, **kwargs): ...
matrix: Any
surface_style: Any
@@ -1071,6 +1135,7 @@ class geometry_conversion_result:
representation: Any
class gradient_function(function_item):
def __init__(self, *args): ...
def calc_hash(self): ...
def clone_(self): ...
def end(self): ...
@@ -1082,9 +1147,12 @@ class gradient_function(function_item):
class equal_functor: ...
class hash_functor: ...
class horizontal_plan_at_element: ...
class implicit_item(geom_item): ...
class implicit_item(geom_item):
def __init__(self, *args, **kwargs): ...
class inverse_attribute:
def __init__(self, name, type_of_aggregation, bound1, bound2, entity_reference, attribute_reference): ...
bag_type: Any
set_type: Any
unspecified_type: Any
@@ -1097,6 +1165,7 @@ class inverse_attribute:
def type_of_aggregation_string(self): ...
class item:
def __init__(self, *args, **kwargs): ...
instance: Any
orientation: Any
def calc_hash(self): ...
@@ -1119,6 +1188,7 @@ class line(curve):
def matrix(self): ...
class line_segment:
def __init__(self, *args): ...
def back(self): ...
def begin(self): ...
def empty(self): ...
@@ -1143,7 +1213,8 @@ class loft:
class loop:
closed: Any
external: Any
fi: Any
function_item: Any
tags: Any
def calc_hash(self): ...
def calculate_linear_edge_curves(self): ...
def centroid(self): ...
@@ -1160,6 +1231,7 @@ class matrix4(item):
AFFINE_W_UNIFORM_SCALE: Any
AFFINE_W_NONUNIFORM_SCALE: Any
OTHER: Any
def __init__(self, *args): ...
tag: Any
def calc_hash(self): ...
def clone_(self): ...
@@ -1172,6 +1244,7 @@ class matrix4(item):
def pre_multiply_scale(self, s): ...
class named_type(parameter_type):
def __init__(self, declared_type): ...
def _is(self, *args): ...
def as_named_type(self) -> named_type: ...
def declared_type(self) -> declaration: ...
@@ -1190,6 +1263,7 @@ class offset_curve(curve):
def kind(self): ...
class offset_function(function_item):
def __init__(self, *args): ...
def calc_hash(self): ...
def clone_(self): ...
def end(self): ...
@@ -1211,6 +1285,7 @@ class parameter_type:
def as_simple_type(self) -> Union[simple_type, None]: ...
class piecewise_function(function_item):
def __init__(self, *args): ...
def calc_hash(self): ...
def clone_(self): ...
def end(self): ...
@@ -1230,6 +1305,7 @@ class plane(surface):
def matrix(self): ...
class point3:
def __init__(self, *args): ...
def calc_hash(self): ...
def clone_(self): ...
@property
@@ -1251,6 +1327,7 @@ class ray_intersection_result:
style_index: Any
class ray_intersection_results:
def __init__(self, *args): ...
def append(self, x): ...
def assign(self, n, x): ...
def back(self): ...
@@ -1275,6 +1352,7 @@ class ray_intersection_results:
def swap(self, v): ...
class revolve(sweep):
def __init__(self, m, basis, pnt, dir, a): ...
angle: Any
axis_origin: Any
direction: Any
@@ -1285,6 +1363,7 @@ class revolve(sweep):
def matrix(self): ...
class schema_definition:
def __init__(self, name, declarations, factory): ...
def declaration_by_name(self, *args: str) -> declaration:
"""
:return: ``declaration`` but upcasted to the most advanced available type
@@ -1306,6 +1385,7 @@ class schema_definition:
def name(self) -> str: ...
class select_type(declaration):
def __init__(self, name, index_in_schema, select_list): ...
def as_select_type(self) -> select_type: ...
def select_list(self) -> tuple[declaration, ...]: ...
@@ -1320,6 +1400,7 @@ class shell:
def print_impl(self, o, indent): ...
class simple_type(parameter_type):
def __init__(self, declared_type): ...
binary_type: Any
boolean_type: Any
integer_type: Any
@@ -1349,6 +1430,7 @@ class sphere(surface):
def matrix(self): ...
class style(item):
def __init__(self, *args): ...
diffuse: colour
name: str
"""E.g. 'IfcSurfaceStyleShading-218', where 218 is style's STEP id."""
@@ -1383,9 +1465,11 @@ class style(item):
def kind(self) -> int: ...
class surface(geom_item): ...
class surface(geom_item):
def __init__(self, *args, **kwargs): ...
class svg_groups_of_line_segments:
def __init__(self, *args): ...
def append(self, x): ...
def assign(self, n, x): ...
def back(self): ...
@@ -1410,6 +1494,7 @@ class svg_groups_of_line_segments:
def swap(self, v): ...
class svg_groups_of_polygons:
def __init__(self, *args): ...
def append(self, x): ...
def assign(self, n, x): ...
def back(self): ...
@@ -1434,6 +1519,7 @@ class svg_groups_of_polygons:
def swap(self, v): ...
class svg_line_segments:
def __init__(self, *args): ...
def append(self, x): ...
def assign(self, n, x): ...
def back(self): ...
@@ -1458,6 +1544,7 @@ class svg_line_segments:
def swap(self, v): ...
class svg_loop:
def __init__(self, *args): ...
def append(self, x): ...
def assign(self, n, x): ...
def back(self): ...
@@ -1482,6 +1569,7 @@ class svg_loop:
def swap(self, v): ...
class svg_loops:
def __init__(self, *args): ...
def append(self, x): ...
def assign(self, n, x): ...
def back(self): ...
@@ -1506,6 +1594,7 @@ class svg_loops:
def swap(self, v): ...
class svg_point:
def __init__(self, *args): ...
def back(self): ...
def begin(self): ...
def empty(self): ...
@@ -1519,6 +1608,7 @@ class svg_point:
def swap(self, v): ...
class svg_polygons:
def __init__(self, *args): ...
def append(self, x): ...
def assign(self, n, x): ...
def back(self): ...
@@ -1543,9 +1633,11 @@ class svg_polygons:
def swap(self, v): ...
class sweep(geom_item):
def __init__(self, *args, **kwargs): ...
basis: Any
class sweep_along_curve(sweep):
def __init__(self, *args): ...
curve: Any
surface: Any
direction: Any
@@ -1563,6 +1655,7 @@ class torus(surface):
def matrix(self): ...
class tree:
def __init__(self, *args): ...
def add_element(self, *args): ...
def add_file(self, *args): ...
def clash_clearance_many(self, set_a, set_b, clearance, check_all): ...
@@ -1575,7 +1668,7 @@ class tree:
def protrusion_distances(self): ...
def select(self, *args): ...
def select_box(self, *args): ...
def select_ray(self, p0, d, length): ...
def select_ray(self, p0, d, length=1000.0): ...
def styles(self): ...
def uint8_to_b64(self, uuids_array): ...
@staticmethod
@@ -1583,6 +1676,7 @@ class tree:
def write_h5(self): ...
class trimmed_curve(geom_item):
def __init__(self, *args, **kwargs): ...
basis: Any
curve_sense: Any
start: Any
@@ -1594,6 +1688,7 @@ class type_by_kind:
max: Any
class type_declaration(declaration):
def __init__(self, name, index_in_schema, declared_type): ...
def argument_types(self): ...
def as_type_declaration(self) -> type_declaration: ...
def declared_type(self): ...
@@ -1612,7 +1707,7 @@ def create_epeck(*args): ...
def create_shape(*args): ...
def flatten(deep): ...
def get_feature(x): ...
def get_info_cpp(v, include_identifier): ...
def get_info_cpp(v, include_identifier=True): ...
def get_log(): ...
def guess_file_type(fn): ...
def helmert_curve_point(A0, A1, A2, s): ...
@@ -1622,14 +1717,14 @@ def line_segments_to_polygons(s, eps, segments): ...
def map_shape(settings, instance): ...
def nary_union(sequence): ...
def new_IfcBaseClass(schema_identifier: str, name: str) -> entity_instance: ...
def open(fn, readonly): ...
def open(fn, readonly=False): ...
def parse_ifcxml(filename): ...
def polygons_to_svg(*args): ...
def read(data): ...
def register_schema(arg1): ...
def schema_by_name(arg1: str) -> schema_definition: ...
def schema_names() -> tuple[str, ...]: ...
def serialise(schema_name, shape_str, advanced): ...
def serialise(schema_name, shape_str, advanced=True): ...
def set_feature(x, v): ...
def set_log_format_json(): ...
def set_log_format_text(): ...
@@ -1234,7 +1234,9 @@ def get_controls(element: ifcopenshell.entity_instance) -> Generator[ifcopenshel
yield rel.RelatingControl
def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
def get_parent(
element: ifcopenshell.entity_instance, ifc_class: Optional[str] = None
) -> Union[ifcopenshell.entity_instance, None]:
"""Get the parent in the spatial heirarchy
IFC features a spatial hierarchy tree of all objects. Each spatial element
@@ -1251,6 +1253,8 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti
- Voiding: the opening voids another physical element, such as a hole in a wall
:param element: Any physical or spatial element in the tree
:param ifc_class: Optionally filter the type of parent you're after. For
example, you may be after the storey, not a space.
:return: Its parent. This must exist for any valid file, or None if we've reached the IfcProject.
Example:
@@ -1260,7 +1264,7 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti
element = file.by_type("IfcWall")[0]
parent = ifcopenshell.util.element.get_parent(element)
"""
return (
parent = (
get_container(element, should_get_direct=True)
or get_aggregate(element)
or get_nest(element)
@@ -1268,6 +1272,16 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti
or get_voided_element(element)
)
if not ifc_class:
return parent
while parent:
if parent.is_a(ifc_class):
return parent
parent = get_parent(parent)
return None
def get_filled_void(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""If the element is filling a void, get the void
@@ -57,11 +57,28 @@ def get_function_node_name(node: ast.FunctionDef) -> Union[SubnameType, None]:
:return: Function node name as ``SubnameType`` or ``None``, if function wasn't processed and can be skipped.
"""
node_name = node.name
if node_name.startswith("_") and node_name not in ("_is",):
is_init = node_name == "__init__"
if node_name.startswith("_") and node_name not in ("_is",) and not is_init:
return None
arg_nodes = node.args.args
defaults = [None] * (len(arg_nodes) - len(node.args.defaults)) + node.args.defaults
args: list[str] = []
for arg, default in zip(arg_nodes, defaults):
if default is None:
args.append(arg.arg)
else:
args.append(f"{arg.arg}={ast.unparse(default)}")
if arg := node.args.vararg:
args.append(f"*{arg.arg}")
if arg := node.args.kwarg:
args.append(f"**{arg.arg}")
# Skip non-informative constructors.
if is_init and args == ["self"]:
return None
args = [a.arg for a in node.args.args]
if node.args.vararg:
args.append("*args")
node_name = f"def {node.name}"
node_name = f"{node_name}({', '.join(args)}): ..."

Some files were not shown because too many files have changed in this diff Show More