Merge remote-tracking branch 'origin/v0.8.0' into tfk-rocksdb-storage

This commit is contained in:
Thomas Krijnen
2025-09-10 10:06:01 +02:00
181 changed files with 12075 additions and 2648 deletions
@@ -53,6 +53,22 @@ pre_listeners: dict[str, dict] = {}
post_listeners: dict[str, dict] = {}
def batching_argument_deprecation(
usecase_path: str, settings: dict, prev_argument: str, new_argument: str, replace_usecase: Optional[str] = None
) -> tuple[str, dict]:
if replace_usecase is not None:
print(f"WARNING. `{usecase_path}` api method is deprecated and should be replaced with `{replace_usecase}`.")
if prev_argument in settings:
print(
f"WARNING. `{prev_argument}` argument is deprecated for API method "
f'"{usecase_path}" and should be replaced with `{new_argument}`.'
)
settings = settings | {new_argument: [settings[prev_argument]]}
settings.pop(prev_argument)
return (replace_usecase or usecase_path, settings)
def renamed_arguments_deprecation(
usecase_path: str, settings: dict, arguments_remapped: dict[str, str]
) -> tuple[str, dict]:
@@ -71,7 +87,11 @@ 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]]]] = {}
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"
),
}
CACHED_USECASE_CLASSES: dict[str, Callable] = {}
@@ -91,43 +91,42 @@ def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, seg
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
zero_length_segment = segment_nest.RelatedObjects[-1]
# DesignParameters.StartPoint for IfcAlignmentHorizontalSegment is automatically updated when the
# geometric representation is updated because the semantic and geometric data use the same IfcPoint.
# This is not the case of IfcAlignmentVerticalSegment and IfcAlignmentCantSegment. For these
# segment types, the design parameters of the zero length segment must be updated explicitly.
if zero_length_segment.DesignParameters.is_a(
"IfcAlignmentVerticalSegment"
) or zero_length_segment.DesignParameters.is_a("IfcAlignmentCantSegment"):
# get the geometric representation for the new segment
mapped_segments = ifcopenshell.api.alignment.get_mapped_segments(segment)
mapped_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
mapped_segments = ifcopenshell.api.alignment.get_mapped_segments(segment)
mapped_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
# compute the end point matrix
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, mapped_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
# compute the end point matrix
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, mapped_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
# update the zero length segment semantic representation parameters
if zero_length_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
y = float(end[1, 3]) / unit_scale
zero_length_segment.DesignParameters.StartHeight = y
dx = float(end[0, 0])
dy = float(end[1, 0])
zero_length_segment.DesignParameters.StartGradient = dy / dx
zero_length_segment.DesignParameters.EndGradient = zero_length_segment.DesignParameters.StartGradient
else:
z = float(end[2, 3]) / unit_scale
dx = float(end[0, 1])
dy = float(end[1, 1])
dz = float(end[2, 1])
ds = math.sqrt(dx * dx + dy * dy)
slope = dz / ds
railhead = layout.RailHeadDistance
# update the zero length segment semantic representation parameters
if zero_length_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
zero_length_segment.DesignParameters.StartPoint.Coordinates = (x,y)
zero_length_segment.DesignParameters.StartDirection = dy / dx
elif zero_length_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
y = float(end[1, 3]) / unit_scale
zero_length_segment.DesignParameters.StartHeight = y
dx = float(end[0, 0])
dy = float(end[1, 0])
zero_length_segment.DesignParameters.StartGradient = dy / dx
zero_length_segment.DesignParameters.EndGradient = zero_length_segment.DesignParameters.StartGradient
else:
z = float(end[2, 3]) / unit_scale
dx = float(end[0, 1])
dy = float(end[1, 1])
dz = float(end[2, 1])
ds = math.sqrt(dx * dx + dy * dy)
slope = dz / ds
railhead = layout.RailHeadDistance
zero_length_segment.DesignParameters.StartCantLeft = z + slope * railhead / 2.0
zero_length_segment.DesignParameters.StartCantRight = z - slope * railhead / 2.0
zero_length_segment.DesignParameters.StartCantLeft = z + slope * railhead / 2.0
zero_length_segment.DesignParameters.StartCantRight = z - slope * railhead / 2.0
# updated the referent's name because the referent is now at a new station
start_dist_along = 0.0
@@ -57,4 +57,4 @@ def get_mapped_segments(layout_segment: entity_instance) -> Sequence[entity_inst
if segment_count == 1:
return (curve.Segments[index - segment_count], None)
else:
return (curve.Segments[index - segment_count], curve.Segment[index])
return (curve.Segments[index - segment_count], curve.Segments[index])
@@ -40,6 +40,8 @@ def layout_horizontal_alignment_by_pi_method(
if not (len(hpoints) - 2 == len(radii)):
raise ValueError("radii should have two fewer elements that hpoints")
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
xBT, yBT = hpoints[0]
xPI, yPI = hpoints[1]
@@ -84,7 +86,7 @@ def layout_horizontal_alignment_by_pi_method(
StartTag=None,
EndTag=None,
StartPoint=pt,
StartDirection=angleBT,
StartDirection=angleBT / angle_unit_scale,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=tangent_run,
@@ -102,7 +104,7 @@ def layout_horizontal_alignment_by_pi_method(
StartTag=None,
EndTag=None,
StartPoint=pc,
StartDirection=angleBT,
StartDirection=angleBT / angle_unit_scale,
StartRadiusOfCurvature=float(radius),
EndRadiusOfCurvature=float(radius),
SegmentLength=lc,
@@ -130,7 +132,7 @@ def layout_horizontal_alignment_by_pi_method(
StartTag=None,
EndTag=None,
StartPoint=pt,
StartDirection=angleBT,
StartDirection=angleBT / angle_unit_scale,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=tangent_run,
@@ -25,9 +25,9 @@ from typing import Union
def assign_control(
file: ifcopenshell.file,
relating_control: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
related_objects: list[ifcopenshell.entity_instance],
) -> Union[ifcopenshell.entity_instance, None]:
"""Assigns a planning control or constraint to an object
"""Assigns a planning control or constraint to a list of objects.
IFC can describe concepts that control other objects. For example, a
planning calendar controls the availability of working days for
@@ -42,7 +42,7 @@ def assign_control(
:param relating_control: The IfcControl entity that is creating the
control or constraint
:param related_object: The IfcObjectDefinition that is being controlled
:param related_objects: The list of IfcObjectDefinition that is being controlled
:return: The newly created IfcRelAssignsToControl. If relationship already
existed before and wasn't changed then returns None.
@@ -59,7 +59,7 @@ def assign_control(
# All subtasks will inherit this calendar, so assigning a single
# calendar to the root task effectively defines a "default" calendar
ifcopenshell.api.control.assign_control(model,
relating_control=calendar, related_object=task)
relating_control=calendar, related_objects=[task])
# Another common example might be relating a cost item and a product
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
@@ -67,22 +67,33 @@ def assign_control(
cost_item = ifcopenshell.api.cost.add_cost_item(model,
cost_schedule=schedule)
ifcopenshell.api.control.assign_control(model,
relating_control=cost_item, related_object=wall)
relating_control=cost_item, related_objects=[wall])
"""
if related_object.HasAssignments:
for assignment in related_object.HasAssignments:
if assignment.is_a("IfcRelAssignsToControl") and assignment.RelatingControl == relating_control:
return
# Filter out already assigned objects.
related_objects_set = set(related_objects)
objects_to_assign: set[ifcopenshell.entity_instance] = set()
control_assignments = set(relating_control.Controls)
if control_assignments:
for obj in related_objects_set:
existing_assignment = next((a for a in obj.HasAssignments if a in control_assignments), None)
# Skip objects already assigned to this control.
if existing_assignment:
continue
objects_to_assign.add(obj)
else:
objects_to_assign = related_objects_set
if not objects_to_assign:
return None
controls: Union[ifcopenshell.entity_instance, None]
controls = next(iter(relating_control.Controls), None)
if controls:
if related_object in controls.RelatedObjects:
return
related_objects = set(controls.RelatedObjects)
related_objects.add(related_object)
controls.RelatedObjects = list(related_objects)
related_objects_new: list[ifcopenshell.entity_instance] = list(controls.RelatedObjects)
related_objects_new.extend(objects_to_assign)
controls.RelatedObjects = list(related_objects_new)
ifcopenshell.api.owner.update_owner_history(file, element=controls)
else:
controls = file.create_entity(
@@ -90,7 +101,7 @@ def assign_control(
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
"RelatedObjects": [related_object],
"RelatedObjects": list(objects_to_assign),
"RelatingControl": relating_control,
},
)
@@ -19,21 +19,19 @@
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.util.element
from typing import Union
def unassign_control(
file: ifcopenshell.file,
relating_control: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
related_objects: list[ifcopenshell.entity_instance],
) -> None:
"""Unassigns a planning control or constraint to an object
:param relating_control: The IfcControl entity that is creating the
control or constraint
:param related_object: The IfcObjectDefinition that is being controlled
:return: If the control still is related to other objects, the
IfcRelAssignsToControl is returned, otherwise None.
:param related_objects: The list IfcObjectDefinitions that is being controlled
:return: None
Example:
@@ -45,23 +43,23 @@ def unassign_control(
cost_item = ifcopenshell.api.cost.add_cost_item(model,
cost_schedule=schedule)
ifcopenshell.api.control.assign_control(model,
relating_control=cost_item, related_object=wall)
relating_control=cost_item, related_objects=[wall])
# And now let's change our mind
ifcopenshell.api.control.unassign_control(model,
relating_control=cost_item, related_object=wall)
relating_control=cost_item, related_objects=[wall])
"""
for rel in related_object.HasAssignments or []:
if not rel.is_a("IfcRelAssignsToControl") or rel.RelatingControl != relating_control:
continue
if len(rel.RelatedObjects) == 1:
related_objects_set = set(related_objects)
control_assignments = set(relating_control.Controls)
rels = set(rel for obj in related_objects_set for rel in obj.HasAssignments if rel in control_assignments)
for rel in rels:
related_objects_new = set(rel.RelatedObjects) - related_objects_set
if related_objects_new:
rel.RelatedObjects = list(related_objects_new)
ifcopenshell.api.owner.update_owner_history(file, element=rel)
else:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
return
related_objects = list(rel.RelatedObjects)
related_objects.remove(related_object)
rel.RelatedObjects = related_objects
ifcopenshell.api.owner.update_owner_history(file, element=rel)
return rel
@@ -59,7 +59,7 @@ def add_cost_item(
cost_item_ = ifcopenshell.api.root.create_entity(file, ifc_class="IfcCostItem")
if cost_schedule:
ifcopenshell.api.control.assign_control(file, cost_schedule, cost_item_)
ifcopenshell.api.control.assign_control(file, cost_schedule, [cost_item_])
elif cost_item:
ifcopenshell.api.nest.assign_object(file, related_objects=[cost_item_], relating_object=cost_item)
return cost_item_
@@ -66,7 +66,7 @@ def add_cost_item_quantity(
schedule = ifcopenshell.api.cost.add_cost_schedule(model)
item = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=schedule)
ifcopenshell.api.control.assign_control(model,
relating_control=item, related_object=chair)
relating_control=item, related_objects=[chair])
# Let's assume we want to count the amount of chairs to calculate our cost item
# Because this is an IfcQuantityCount the count will be automatically set to "1" chair
@@ -122,7 +122,7 @@ class Usecase:
) -> ifcopenshell.entity_instance:
return ifcopenshell.api.control.assign_control(
self.file,
related_object=related_object,
related_objects=[related_object],
relating_control=cost_item,
)
@@ -57,7 +57,7 @@ def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcop
concrete = ifcopenshell.api.resource.add_resource(model,
ifc_class="IfcConstructionMaterialResource", parent_resource=crew)
ifcopenshell.api.control.assign_control(model,
relating_control=item, related_object=concrete)
relating_control=item, related_objects=[concrete])
# ... which has a unit price of 42.0 per m3
value = ifcopenshell.api.cost.add_cost_value(model, parent=concrete)
ifcopenshell.api.cost.edit_cost_value(model, cost_value=value,
@@ -72,7 +72,7 @@ def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcop
equipment = ifcopenshell.api.resource.add_resource(model,
ifc_class="IfcConstructionEquipmentResource", parent_resource=crew)
ifcopenshell.api.control.assign_control(model,
relating_control=item, related_object=equipment)
relating_control=item, related_objects=[equipment])
# ... with a fixed price of 50,000
value = ifcopenshell.api.cost.add_cost_value(model, parent=concrete)
ifcopenshell.api.cost.edit_cost_value(model, cost_value=value,
@@ -45,5 +45,5 @@ def copy_cost_schedule(
if isinstance(duplicated_cost_item, list):
# All other nested items are not connected to the cost schedule explicitly.
duplicated_cost_item = duplicated_cost_item[0]
ifcopenshell.api.control.assign_control(file, new_schedule, duplicated_cost_item)
ifcopenshell.api.control.assign_control(file, new_schedule, [duplicated_cost_item])
return new_schedule
@@ -86,7 +86,7 @@ class Usecase:
for product in products:
ifcopenshell.api.control.unassign_control(
self.file,
related_object=product,
related_objects=[product],
relating_control=cost_item,
)
self.update_cost_item_count(cost_item)
@@ -22,6 +22,7 @@ import ifcopenshell.api.owner
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
from ifcopenshell.util.shape_builder import ShapeBuilder
from typing import Optional, Union, Any
NPArrayOfFloats = npt.NDArray[np.float64]
@@ -74,6 +75,7 @@ class Usecase:
if not hasattr(self.settings["product"], "ObjectPlacement"):
return
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.builder = ShapeBuilder(self.file)
if not self.settings["is_si"]:
self.convert_matrix_to_si(self.settings["matrix"])
@@ -183,25 +185,12 @@ class Usecase:
o = np.array((m[0][3], m[1][3], m[2][3]))
object_matrix = ifcopenshell.util.placement.a2p(o, z, x)
relative_placement_matrix = np.linalg.inv(relating_object_matrix) @ object_matrix
return self.create_ifc_axis_2_placement_3d(
relative_placement_matrix[:, 3][0:3],
return self.builder.create_axis2_placement_3d(
self.convert_si_to_unit(relative_placement_matrix[:, 3][0:3]),
relative_placement_matrix[:, 2][0:3],
relative_placement_matrix[:, 0][0:3],
)
def create_ifc_axis_2_placement_3d(
self, point: NPArrayOfFloats, up: NPArrayOfFloats, forward: NPArrayOfFloats
) -> ifcopenshell.entity_instance:
return self.file.createIfcAxis2Placement3D(
self.create_cartesian_point(point),
self.file.createIfcDirection(up.tolist()),
self.file.createIfcDirection(forward.tolist()),
)
def create_cartesian_point(self, co: NPArrayOfFloats) -> ifcopenshell.entity_instance:
co = self.convert_si_to_unit(co)
return self.file.createIfcCartesianPoint(co.tolist())
def convert_si_to_unit(self, co: NPArrayOfFloats) -> NPArrayOfFloats:
return co / self.unit_scale
@@ -18,9 +18,9 @@
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.util.unit
import numpy as np
from ifcopenshell.util.shape_builder import ShapeBuilder
from math import sin, cos, radians
@@ -62,6 +62,7 @@ def edit_wcs(
ifcopenshell.api.georeference.edit_wcs(model)
"""
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
builder = ShapeBuilder(file)
if np.isclose(rotation, 0):
xaxis_x = 1.0
xaxis_y = 0.0
@@ -74,14 +75,10 @@ def edit_wcs(
old_wcs = context.WorldCoordinateSystem
if context.CoordinateSpaceDimension == 3:
if is_si:
point = file.createIfcCartesianPoint((x / unit_scale, y / unit_scale, z / unit_scale))
xyz = (x / unit_scale, y / unit_scale, z / unit_scale)
else:
point = file.createIfcCartesianPoint((x, y, z))
placement = file.createIfcAxis2Placement3D(
point,
file.createIfcDirection((0.0, 0.0, 1.0)),
file.createIfcDirection((xaxis_x, xaxis_y, 0.0)),
)
xyz = (x, y, z)
placement = builder.create_axis2_placement_3d(xyz, (0.0, 0.0, 1.0), (xaxis_x, xaxis_y, 0.0))
elif context.CoordinateSpaceDimension == 2:
if is_si:
point = file.createIfcCartesianPoint((x / unit_scale, y / unit_scale))
@@ -138,7 +138,7 @@ def add_task(
task.Identification = identification
task.IsMilestone = False
if work_schedule:
ifcopenshell.api.control.assign_control(file, work_schedule, task)
ifcopenshell.api.control.assign_control(file, work_schedule, [task])
elif parent_task:
rel = ifcopenshell.api.nest.assign_object(
file,
@@ -75,7 +75,7 @@ def add_work_calendar(
# We associate the calendar with the construction root task. All
# subtasks underneath the construction work task will also inherit
# this calendar by default (though you can override them).
ifcopenshell.api.control.assign_control(model, relating_control=calendar, related_object=task)
ifcopenshell.api.control.assign_control(model, relating_control=calendar, related_objects=[task])
"""
work_calendar = ifcopenshell.api.root.create_entity(
file,
@@ -47,5 +47,5 @@ def copy_work_schedule(
duplicated_tasks = ifcopenshell.api.sequence.duplicate_task(file, task)[1]
# All other nested items are not connected to the work schedule explicitly.
duplicated_task = duplicated_tasks[0]
ifcopenshell.api.control.assign_control(file, new_schedule, duplicated_task)
ifcopenshell.api.control.assign_control(file, new_schedule, [duplicated_task])
return new_schedule
@@ -79,7 +79,7 @@ class Usecase:
assert isinstance(res, list)
current, duplicate = res
ifcopenshell.api.control.assign_control(
self.file, relating_control=baseline_work_schedule, related_object=duplicate[0]
self.file, relating_control=baseline_work_schedule, related_objects=[duplicate[0]]
)
for i, task in enumerate(current):
self.create_baseline_reference(task, duplicate[i])
@@ -54,7 +54,7 @@ def remove_work_calendar(file: ifcopenshell.file, work_calendar: ifcopenshell.en
ifcopenshell.api.control.unassign_control(
file,
relating_control=work_calendar,
related_object=related_object,
related_objects=[related_object],
)
# Currently in API work times are created already attached
+59 -12
View File
@@ -18,11 +18,13 @@
from __future__ import annotations
import re
import json
import numpy as np
import numpy.typing as npt
import ifcopenshell
import ifcopenshell.util.attribute
import ifcopenshell.util.schema
from pathlib import Path
from typing import Any, NoReturn, Union, Optional, TYPE_CHECKING
from typing import Any, NoReturn, Union, Optional, TYPE_CHECKING, TypedDict
from . import ifcopenshell_wrapper
from .file import file
from .entity_instance import entity_instance
@@ -36,8 +38,33 @@ except ImportError as e:
print(f"No SQL support: {e}")
class GeometryCache(TypedDict):
shapes: dict[int, GeometryCacheShape]
geometry: dict[str, GeometryCacheGeometry]
class GeometryCacheShape(TypedDict):
co: list[float]
"""Object location."""
matrix: npt.NDArray[np.float64]
geometry: Union[str, None]
"""Element's geometry id (same value as in ``Representation.id``).
Is set to ``None` when no geometry is available for the element.
"""
class GeometryCacheGeometry(TypedDict):
verts: npt.NDArray[np.float64]
edges: npt.NDArray[np.int32]
faces: npt.NDArray[np.int32]
material_ids: npt.NDArray[np.int32]
materials: list[int]
class sqlite(file):
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4"
mvd_str: str
"""As in `header.file_description.description`."""
def __init__(self, filepath: str):
"""
@@ -53,7 +80,6 @@ class sqlite(file):
if not Path(filepath).exists():
raise FileNotFoundError(f"File doesn't exist: {filepath}")
self.wrapped_data = None
self.history_size = 64
self.history = []
self.future = []
@@ -81,7 +107,8 @@ class sqlite(file):
except:
assert False, "SQLite schema not supported."
self.schema = row[1]
self._schema = row[1]
self.mvd_str = row[2]
self.ifc_schema = ifcopenshell.schema_by_name(self.schema)
self.cursor.execute("SELECT ifc_id, ifc_class FROM id_map")
@@ -234,29 +261,30 @@ class sqlite(file):
return True
return False
def get_geometry(self, ids: list[int]) -> dict[str, dict]:
def get_geometry(self, ids: list[int]) -> GeometryCache:
import numpy as np
ids_csv = ",".join(map(str, ids))
query = f"SELECT ifc_id, x, y, z, matrix, geometry, verts, edges, faces, material_ids, materials FROM shape LEFT JOIN geometry ON shape.geometry = geometry.id WHERE `ifc_id` IN ({ids_csv})"
self.cursor.execute(query)
rows = self.cursor.fetchall()
shapes = {}
geometry = {}
shapes: dict[int, GeometryCacheShape] = {}
geometry: dict[str, GeometryCacheGeometry] = {}
for row in rows:
if row["geometry"] and row["geometry"] not in geometry:
# Same data types as in ifcopenshell.util.shape.
geometry[row["geometry"]] = {
"verts": np.frombuffer(row["verts"]).tolist() if row["verts"] else [],
"edges": np.frombuffer(row["edges"], dtype=np.int64).tolist() if row["edges"] else [],
"faces": np.frombuffer(row["faces"], dtype=np.int64).tolist() if row["faces"] else [],
"verts": np.frombuffer(row["verts"], dtype="d") if row["verts"] else np.empty(0, dtype="d"),
"edges": np.frombuffer(row["edges"], dtype="i") if row["edges"] else np.empty(0, dtype="i"),
"faces": np.frombuffer(row["faces"], dtype="i") if row["faces"] else np.empty(0, dtype="i"),
"material_ids": (
np.frombuffer(row["material_ids"], dtype=np.int64).tolist() if row["material_ids"] else []
np.frombuffer(row["material_ids"], dtype="i") if row["material_ids"] else np.empty(0, dtype="i")
),
"materials": json.loads(row["materials"]) if row["materials"] else [],
}
shapes[row["ifc_id"]] = {
"co": [row["x"], row["y"], row["z"]],
"matrix": np.copy(np.frombuffer(row["matrix"]).reshape((4, 4))),
"matrix": np.copy(np.frombuffer(row["matrix"], dtype="d").reshape((4, 4))),
"geometry": row["geometry"],
}
ids_without_geometry = set(ids) - set(shapes.keys())
@@ -272,6 +300,21 @@ class sqlite(file):
# Override to avoid clean up data unrelated to sqlite file.
pass
def wrapped_data(self) -> NoReturn:
class_name = str(type(self))
raise Exception(
f"No `wrapped_data` for {class_name}. `ifcopenshell.{class_name}` is probably confused with `ifcopenshell.file`."
)
@property
def schema(self) -> ifcopenshell.util.schema.IFC_SCHEMA:
return self._schema
@property
def schema_identifier(self) -> str:
# The best option we've got for mimicing `file.schema_identifier`.
return self._schema
class sqlite_entity(entity_instance):
sqlite_wrapper: sqlite_wrapper
@@ -449,6 +492,10 @@ class sqlite_entity(entity_instance):
info.update(self.sqlite_wrapper.attribute_cache)
return info
@property
def file(self) -> sqlite:
return self.sqlite_wrapper.file
class sqlite_wrapper:
def __init__(self, id: int, ifc_class: str, file: sqlite):
+22 -10
View File
@@ -87,10 +87,7 @@ try:
return (int(items[0]), str(items[1]), items[2])
class stream(file):
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4"
def __init__(self, filepath: str):
self.wrapped_data = None
self.history_size = 64
self.history = []
self.future = []
@@ -109,9 +106,9 @@ try:
# common.INT doesn't support negative integers.
grammar = r"""
start: "#" NUMBER "=" TYPE "(" args ")" ";"
args: arg ("," arg)*
arg: STRING -> string
| FLOAT -> float
| IFCINT -> ifcint
@@ -121,21 +118,21 @@ try:
| REFERENCE -> reference
| list -> list
| inline_type -> inline_type
list: "(" arg? ("," arg)* ")"
inline_type: TYPE "(" arg ")"
REFERENCE: "#" /[0-9]+/
TYPE: CNAME
NUMBER: INT
STRING: "'" /([^']|'')*/ "'"
IFCINT: /-?[0-9]+/
FLOAT: /-?[0-9]+\.[0-9]*([Ee]-?[0-9]+)?/
NULL: "$"
DERIVED: "*"
ENUM: "." CNAME "."
%import common.INT
%import common.CNAME
"""
@@ -176,7 +173,7 @@ try:
self.class_map.setdefault(ifc_class, []).append(step_id)
self.id_offset[step_id] = offset
elif line.startswith("FILE_SCHEMA"):
self.schema = line.split("'")[1]
self._schema = line.split("'")[1]
self.ifc_schema = ifcopenshell.schema_by_name(self.schema)
for ifc_class in exclude_classes:
declaration = self.ifc_schema.declaration_by_name(ifc_class)
@@ -290,6 +287,21 @@ try:
# Override to avoid clean up unrelated to stream file.
pass
def wrapped_data(self) -> NoReturn:
class_name = str(type(self))
raise Exception(
f"No `wrapped_data` for {class_name}. `ifcopenshell.{class_name}` is probably confused with `ifcopenshell.file`."
)
@property
def schema(self) -> ifcopenshell.util.schema.IFC_SCHEMA:
return self._schema
@property
def schema_identifier(self) -> str:
# The best option we've got for mimicing `file.schema_identifier`.
return self._schema
class stream_entity(entity_instance):
stream_wrapper: stream_wrapper
@@ -21,6 +21,7 @@ import numpy as np
import ifcopenshell
from typing import Any, Union
from dataclasses import dataclass
from ifcopenshell.util.shape_builder import ShapeBuilder
@dataclass
@@ -78,9 +79,7 @@ class Clipping:
if not ifc_file:
ifc_file = first_operand.file
location = ifc_file.createIfcCartesianPoint([i / unit_scale for i in self.location])
direction = ifc_file.createIfcDirection(self.normal)
builder = ShapeBuilder(ifc_file)
normal = np.array(self.normal)
if np.allclose(normal, np.array([0.0, 0.0, 1.0]), atol=1e-2) or np.allclose(
@@ -92,9 +91,9 @@ class Clipping:
x_axis = np.cross(normal, arbitrary_vector)
x_axis /= np.linalg.norm(x_axis)
x_axis = ifc_file.createIfcDirection(x_axis.tolist())
plane = ifc_file.createIfcPlane(ifc_file.createIfcAxis2Placement3D(location, direction, x_axis))
placement = builder.create_axis2_placement_3d([i / unit_scale for i in self.location], self.normal, x_axis)
plane = ifc_file.create_entity("IfcPlane", placement)
second_operand = ifc_file.createIfcHalfSpaceSolid(plane, False)
return ifc_file.createIfcBooleanClippingResult("DIFFERENCE", first_operand, second_operand)
@@ -1169,6 +1169,25 @@ def get_groups(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entit
return groups
def get_controls(element: ifcopenshell.entity_instance) -> Generator[ifcopenshell.entity_instance]:
"""
Retrieves the controls of an element.
:param element: The IFC element
:return: Generator of IfcControl elements assigned to the element.
Example:
.. code:: python
task = file.by_type("IfcTask")[0]
control = ifcopenshell.util.element.get_controls(task)[0]
"""
for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToControl"):
yield rel.RelatingControl
def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""Get the parent in the spatial heirarchy
@@ -23,6 +23,7 @@ import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
from typing import NamedTuple, Optional, Union
from decimal import Decimal, ROUND_HALF_UP
MatrixType = ifcopenshell.util.placement.MatrixType
@@ -39,39 +40,54 @@ class HelmertTransformation(NamedTuple):
factor_z: float
def dms2dd(degrees: int, minutes: int, seconds: int, ms: int = 0) -> float:
"""Convert degrees, minutes, and (milli)seconds to decimal degrees
def dms2dd(degrees: int, minutes: int, seconds: int, us: int = 0) -> float:
"""Convert degrees, minutes, and (micro)seconds to decimal degrees
All components must be either positive or negative.
:param degrees: The degrees component
:param minutes: The minutes component
:param seconds: The seconds component
:param ms: The milliseconds component
:param us: The microseconds component
:return: The angle in decimal degrees.
"""
dd = float(degrees) + float(minutes) / 60.0 + float(seconds) / (3600.0) + float(ms / 3600000000.0)
return dd
all_positive_or_zero = degrees >= 0 and minutes >= 0 and seconds >= 0 and us >= 0
all_negative_or_zero = degrees <= 0 and minutes <= 0 and seconds <= 0 and us <= 0
assert all_positive_or_zero or all_negative_or_zero
return degrees + minutes / 60.0 + seconds / 3600.0 + us / 3600000000.0
def dd2dms(dd: float, use_ms: bool = False) -> Union[tuple[float, float, float, float], tuple[float, float, float]]:
"""Convert decimal degrees to degrees, minutes, and (milli)seconds format
def dd2dms(dd: float, use_us: bool = False) -> Union[tuple[int, int, int, int], tuple[int, int, float]]:
"""Convert decimal degrees to degrees, minutes, and (micro)seconds format
:param dd: The decimal degrees
:param use_ms: True if to include milliseconds and false otherwise. Defaults to false.
:return: The angle in a tuple of either 3 or 4 values, being degrees,
minutes, seconds, and optionally milliseconds.
:param use_us: True if to include microseconds and false otherwise. Defaults to false.
:return: The angle in a tuple of either 3 or 4 values,
4 values: integer number of degrees, integer number of minutes, integer number of seconds and integer number of microseconds
3 values: integer number of degrees, integer number of minutes, and a float number for seconds
:note: the tuple follows the format of IfcCompoundPlaneAngleMeasure. Namely all of its components are either positive or negative.
"""
dd = float(dd)
sign = 1 if dd >= 0 else -1
dd = abs(dd)
if use_ms:
seconds, ms = divmod(dd * 60 * 60 * 1000000, 1000000)
minutes, seconds = divmod(dd * 60 * 60, 60)
degrees, minutes = divmod(minutes, 60)
if dd < 0:
degrees = -degrees
if use_ms:
return (int(degrees) * sign, int(minutes) * sign, int(seconds) * sign, int(ms) * sign)
return (int(degrees) * sign, int(minutes) * sign, int(seconds) * sign)
dd_decimal = Decimal(str(dd))
degrees = int(dd_decimal)
degrees_decimal = Decimal(degrees)
fractional_part = dd_decimal - degrees_decimal
minutes_decimal = fractional_part * Decimal(60)
minutes = int(minutes_decimal)
minutes_decimal_int = Decimal(minutes)
seconds_decimal = (minutes_decimal - minutes_decimal_int) * Decimal(60)
if use_us:
seconds = int(seconds_decimal)
seconds_decimal_int = Decimal(seconds)
microseconds_decimal = (seconds_decimal - seconds_decimal_int) * Decimal(1000000)
microseconds = int(microseconds_decimal.quantize(Decimal(1), rounding=ROUND_HALF_UP))
return (degrees, minutes, seconds, microseconds)
else:
seconds_float = float(seconds_decimal)
return (degrees, minutes, seconds_float)
def xyz2enh(
@@ -692,7 +692,8 @@ def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUN
:returns: The scale factor
"""
if (
unit_type
type(ifc_file) is ifcopenshell.file
and unit_type
not in ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema_identifier)
.declaration_by_name("IfcUnitEnum")
.enumeration_items()
@@ -27,23 +27,33 @@ class TestAssignControl(test.bootstrap.IFC4):
control = ifcopenshell.api.cost.add_cost_schedule(self.file)
# simple assignment
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall)
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall])
assert relation
assert len(self.file.by_type("IfcRelAssignsToControl")) == 1
assert relation.RelatingControl == control
assert relation.RelatedObjects == (wall,)
# trying to establish existing relationship
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall)
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall])
assert relation is None
# assigning same control to another object
wall1 = self.file.createIfcWall()
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall1)
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall1])
assert relation is not None
assert len(self.file.by_type("IfcRelAssignsToControl")) == 1
assert relation.RelatingControl == control
assert set(relation.RelatedObjects) == set((wall, wall1))
def test_batch_assignment(self):
walls = [self.file.createIfcWall() for _ in range(5)]
control = ifcopenshell.api.cost.add_cost_schedule(self.file)
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=walls)
assert relation
assert len(self.file.by_type("IfcRelAssignsToControl")) == 1
assert relation.RelatingControl == control
assert set(relation.RelatedObjects) == set(walls)
class TestAssignControlIFC2X3(test.bootstrap.IFC2X3, TestAssignControl):
pass
@@ -27,15 +27,17 @@ class TestUnassignControl(test.bootstrap.IFC4):
control = ifcopenshell.api.cost.add_cost_schedule(self.file)
# assign and unassign
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall)
ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_object=wall)
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall])
assert relation
ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_objects=[wall])
assert len(self.file.by_type("IfcRelAssignsToControl")) == 0
# 1 control 2 related objects
wall1 = self.file.createIfcWall()
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall)
ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall1)
ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_object=wall1)
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall])
assert relation
ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall1])
ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_objects=[wall1])
assert len(self.file.by_type("IfcRelAssignsToControl")) == 1
assert relation.RelatedObjects == (wall,)
@@ -30,7 +30,7 @@ class TestAddCostItemQuantity(test.bootstrap.IFC4):
schedule = ifcopenshell.api.cost.add_cost_schedule(self.file)
item = ifcopenshell.api.cost.add_cost_item(self.file, cost_schedule=schedule)
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
ifcopenshell.api.control.assign_control(self.file, relating_control=item, related_object=wall)
ifcopenshell.api.control.assign_control(self.file, relating_control=item, related_objects=[wall])
quantities = []
for quantity_type in quantity_types:
@@ -95,7 +95,7 @@ class TestEditTaskTime(test.bootstrap.IFC4):
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
calendar = ifcopenshell.api.sequence.add_work_calendar(self.file)
task = self.file.createIfcTask()
ifcopenshell.api.control.assign_control(self.file, relating_control=calendar, related_object=task)
ifcopenshell.api.control.assign_control(self.file, relating_control=calendar, related_objects=[task])
task_time = ifcopenshell.api.sequence.add_task_time(self.file, task=task)
ifcopenshell.api.sequence.edit_task_time(
self.file,
@@ -195,7 +195,7 @@ class TestEditTaskTime(test.bootstrap.IFC4):
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
calendar = ifcopenshell.api.sequence.add_work_calendar(self.file)
task = self.file.createIfcTask()
ifcopenshell.api.control.assign_control(self.file, relating_control=calendar, related_object=task)
ifcopenshell.api.control.assign_control(self.file, relating_control=calendar, related_objects=[task])
task_time = ifcopenshell.api.sequence.add_task_time(self.file, task=task)
ifcopenshell.api.sequence.edit_task_time(
self.file,
@@ -38,7 +38,7 @@ class TestRemoveWorkCalendar(test.bootstrap.IFC4):
# Assign tasks.
task = ifcopenshell.api.sequence.add_task(self.file)
ifcopenshell.api.control.assign_control(self.file, work_calendar, task)
ifcopenshell.api.control.assign_control(self.file, work_calendar, [task])
ifcopenshell.api.sequence.remove_work_calendar(self.file, work_calendar)
+22 -2
View File
@@ -16,19 +16,39 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.cost
import ifcopenshell.api.root
import test.bootstrap
import ifcopenshell.api
import ifcopenshell.api.control
import ifcopenshell.api.sequence
import ifcopenshell.util.element
from datetime import datetime
from typing import Union
def deprecation_check(test):
def new_test(self):
assert datetime.now().date() < datetime(2024, 8, 1).date(), "API arguments are completely deprecated"
assert datetime.now().date() < datetime(2026, 1, 9).date(), "API arguments are completely deprecated"
test(self)
return new_test
class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4):
pass
@deprecation_check
def test_assigning_control(self):
model = self.file
element = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
control = ifcopenshell.api.cost.add_cost_schedule(model)
ifcopenshell.api.control.assign_control(model, relating_control=control, related_objects=[element])
assert list(ifcopenshell.util.element.get_controls(element)) == [control]
@deprecation_check
def test_unassigning_control(self):
TestTemporarySupportForDeprecatedAPIArguments.test_assigning_control(self)
model = self.file
element = model.by_type("IfcWall")[0]
control = model.by_type("IfcCostSchedule")[0]
ifcopenshell.api.control.unassign_control(model, relating_control=control, related_objects=[element])
assert list(ifcopenshell.util.element.get_controls(element)) == []
@@ -145,14 +145,20 @@ class TestAssignType(test.bootstrap.IFC4):
This is because the type will have its own PredefinedType, and the element's PredefinedType
will conflict with it. (See #7006)
"""
is_ifc2x3 = self.file.schema == "IFC2X3"
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
element_type.PredefinedType = "MOVABLE"
element_type.PredefinedType = "POLYGONAL"
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
element.PredefinedType = "USERDEFINED"
if not is_ifc2x3:
# In IFC2X3, there seems to be no example when both type and occurence have PredefinedType.
# So we just ignore it.
element.PredefinedType = "USERDEFINED"
element.ObjectType = "Test"
ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
assert element.PredefinedType is None
if not is_ifc2x3:
assert element.PredefinedType is None
assert element.ObjectType is None
def test_keep_predefined_type_if_type_assignment_is_notdefined(self):
@@ -160,16 +166,26 @@ class TestAssignType(test.bootstrap.IFC4):
if an element has a PredefinedType, it will be removed when assigning a type.(See #7006)
This behavior needs to be blocked if the PredefinedType of the typing Entity is set to "NOTDEFINED". (See #7011)
"""
is_ifc2x3 = self.file.schema == "IFC2X3"
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
element_type.PredefinedType = "NOTDEFINED"
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
element.PredefinedType = "USERDEFINED"
if not is_ifc2x3:
# In IFC2X3, there seems to be no example when both type and occurence have PredefinedType.
# So we just ignore it.
element.PredefinedType = "USERDEFINED"
element.ObjectType = "Test"
ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
assert element.PredefinedType == "USERDEFINED"
if not is_ifc2x3:
assert element.PredefinedType == "USERDEFINED"
assert element.ObjectType == "Test"
class TestAssignTypeIFC2X3(test.bootstrap.IFC2X3, TestAssignType):
pass
class TestAssignTypeIFC4X3(test.bootstrap.IFC4X3, TestAssignType):
pass
+8 -3
View File
@@ -47,15 +47,20 @@ class TestPackageSupportedPlatforms:
response = conn.getresponse()
build_html = response.read().decode("utf-8")
def find_make_var(var_name: str) -> str:
line = next(l for l in text.splitlines() if l.startswith(f"{var_name}:="))
return line.partition(":=")[2]
BINARY_VERSION = find_make_var("BINARY_VERSION")
URL_TYPES = ("IOS_URL", "IFCCONVERT_URL")
missing_urls: set[str] = set()
for url_type in URL_TYPES:
line = next(l for l in text.splitlines() if l.startswith(f"{url_type}:="))
_, _, url_template = line.partition(":=")
url_template = find_make_var(url_type)
url_template = url_template.replace("$(", "{").replace(")", "}")
for platform in SUPPORTED_PLATFORMS:
for pyver in SUPPORTED_PY_VERSIONS:
url = url_template.format(PYNUMBER=pyver, PLATFORM=platform)
url = url_template.format(PYNUMBER=pyver, PLATFORM=platform, BINARY_VERSION=BINARY_VERSION)
if url not in build_html:
missing_urls.add(url)
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.control
import ifcopenshell.api.cost
import ifcopenshell.api.profile
import pytest
import test.bootstrap
@@ -926,6 +928,23 @@ class TestGetGroupsIFC2X3(test.bootstrap.IFC2X3, TestGetGroupsIFC4):
pass
class TestGetControls(test.bootstrap.IFC2X3):
def test_run(self):
model = self.file
element = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
control = ifcopenshell.api.cost.add_cost_schedule(model)
ifcopenshell.api.control.assign_control(model, related_objects=[element], relating_control=control)
assert list(subject.get_controls(element)) == [control]
class TestGetControlsIFC4(test.bootstrap.IFC4, TestGetControls):
pass
class TestGetControlsIFC4X3(test.bootstrap.IFC4X3, TestGetControls):
pass
class TestGetAggregateIFC4(test.bootstrap.IFC4):
def test_getting_the_containing_aggregate_of_a_subelement(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
@@ -468,3 +468,29 @@ class TestAngle2YAxis(test.bootstrap.IFC4):
assert np.allclose(subject.angle2yaxis(45), (-a, a))
assert np.allclose(subject.angle2yaxis(-135), (a, -a))
assert np.allclose(subject.angle2yaxis(135), (-a, -a))
class TestDMS2DDandDD2DMS(test.bootstrap.IFC4):
def test_dms2dd_and_dd2dms(self):
test_cases_3tuple = [
(35.41, (35, 24, 36.0)),
(-116.89, (-116, -53, -24.0)),
]
test_cases_4tuple = [
(40.431389, (40, 25, 53, 400)),
(-4.248056, (-4, -14, -53, -1600)),
(-35.401389, (-35, -24, -5, -400)),
(148.981667, (148, 58, 54, 1200)),
]
for dd, dms in test_cases_3tuple:
d, m, s = subject.dd2dms(dd)
assert (d, m, s) == dms
dd_converted = subject.dms2dd(dms[0], dms[1], dms[2])
assert dd_converted == dd
for dd, dms in test_cases_4tuple:
d, m, s, us = subject.dd2dms(dd, use_us=True)
assert (d, m, s, us) == dms
dd_converted = subject.dms2dd(dms[0], dms[1], dms[2], dms[3])
assert dd_converted == dd