mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Merge remote-tracking branch 'origin/v0.8.0' into tfk-rocksdb-storage
This commit is contained in:
@@ -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])
|
||||
|
||||
+5
-3
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user