mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-22 13:05:59 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b429dc605 | |||
| f3cdf7ee4e | |||
| 58565b8c21 | |||
| ada5c384dc | |||
| 0cd8c76b8e | |||
| d1a3257d55 | |||
| 209d487f5f | |||
| 9673f01c21 | |||
| cf88d5a665 | |||
| 4cb6ce7035 | |||
| 472482a87c | |||
| 1af5cc9bbe | |||
| 307836049f | |||
| 223f358f28 | |||
| 8dc1ee55b5 | |||
| 33f61ef344 |
@@ -462,7 +462,6 @@ class PolylineOperator:
|
||||
self.tool_state.axis_method = None
|
||||
self.tool_state.plane_method = None
|
||||
self.tool_state.mode = "Mouse"
|
||||
tool.Raycast.clear_snap_objs()
|
||||
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
||||
for obj in self.visible_objs:
|
||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||
|
||||
+621
-439
File diff suppressed because it is too large
Load Diff
@@ -390,37 +390,47 @@ class Snap(bonsai.core.tool.Snap):
|
||||
detected_snaps.append(point)
|
||||
|
||||
# Objects
|
||||
objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, objs_2d_bbox)
|
||||
closest_snaps = tool.Raycast.ray_cast_and_get_closest_to_camera_snaps(context, event, objs_to_raycast)
|
||||
detected_snaps.extend(closest_snaps)
|
||||
|
||||
xray_mode = (space.shading.type == "SOLID" and space.shading.show_xray) or (
|
||||
space.shading.type == "WIREFRAME" and space.shading.show_xray_wireframe
|
||||
)
|
||||
objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, objs_2d_bbox)
|
||||
snap_faces, closest_obj = tool.Raycast.get_gpu_solid_snaps(context, event, objs_to_raycast)
|
||||
wireframe_snaps, _ = tool.Raycast.get_gpu_wireframe_snaps(context, event, objs_to_raycast)
|
||||
|
||||
for snap_obj in objs_to_raycast:
|
||||
for snap in closest_snaps:
|
||||
if snap_obj.obj == snap["object"]:
|
||||
if xray_mode:
|
||||
if "face_index" in snap and snap["face_index"] is not None:
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj)
|
||||
for point in snap_points:
|
||||
point["group"] = "Object"
|
||||
detected_snaps.append(point)
|
||||
else:
|
||||
# If it is a solid object that is closest to camera it ignores all the rest
|
||||
if (
|
||||
"is_closest_to_camera" in snap
|
||||
and snap["is_closest_to_camera"]
|
||||
and snap["group"] == "Object"
|
||||
):
|
||||
closest_snap = [snap] # discards objects that aren't the closest
|
||||
if "face_index" in snap and snap["face_index"] is not None:
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj)
|
||||
for point in snap_points:
|
||||
point["group"] = "Object"
|
||||
closest_snap.append(point)
|
||||
detected_snaps = closest_snap
|
||||
if not xray_mode:
|
||||
for snap in snap_faces:
|
||||
if snap["object"] == closest_obj:
|
||||
detected_snaps.append(snap)
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity(
|
||||
context, event, snap["object"], snap["object"].data.polygons[snap["face_index"]]
|
||||
)
|
||||
for point in snap_points:
|
||||
point["group"] = "Object"
|
||||
detected_snaps.append(point)
|
||||
|
||||
# Get wireframe snaps that are not occluded by face snap
|
||||
ray_origin = tool.Raycast.get_viewport_ray_data(context, event)[0]
|
||||
occl_dist = (snap["point"] - ray_origin).length + 1e-4
|
||||
visible_wireframe_snaps = [
|
||||
w for w in wireframe_snaps if (w["point"] - ray_origin).length <= occl_dist
|
||||
]
|
||||
detected_snaps.extend(visible_wireframe_snaps)
|
||||
continue
|
||||
|
||||
if not snap_faces:
|
||||
detected_snaps.extend(wireframe_snaps)
|
||||
|
||||
else:
|
||||
# Doesn't include face snaps, only their edges and vertices
|
||||
for snap in snap_faces:
|
||||
snap_points = tool.Raycast.ray_cast_by_proximity(
|
||||
context, event, snap["object"], snap["object"].data.polygons[snap["face_index"]]
|
||||
)
|
||||
for point in snap_points:
|
||||
point["group"] = "Object"
|
||||
detected_snaps.append(point)
|
||||
|
||||
detected_snaps.extend(wireframe_snaps)
|
||||
|
||||
# snap to cut geometry (e.g. in plan view)
|
||||
if CutDecorator.installed:
|
||||
|
||||
@@ -62,9 +62,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
|
||||
longitudes.push_back(*pbde->DistanceAlong()->as<IfcSchema::IfcLengthMeasure>(true) * length_unit_);
|
||||
|
||||
Eigen::Vector3d po(
|
||||
pbde->OffsetLateral().get_value_or(0.),
|
||||
pbde->OffsetLateral().get_value_or(0.) * length_unit_,
|
||||
// @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane
|
||||
pbde->OffsetVertical().get_value_or(0.),
|
||||
pbde->OffsetVertical().get_value_or(0.) * length_unit_,
|
||||
0.);
|
||||
|
||||
profile_offsets.push_back(po);
|
||||
|
||||
@@ -64,9 +64,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) {
|
||||
longitudes.push_back(*pbde->DistanceAlong()->as<IfcSchema::IfcLengthMeasure>(true) * length_unit_);
|
||||
|
||||
Eigen::Vector3d po(
|
||||
pbde->OffsetLateral().get_value_or(0.),
|
||||
pbde->OffsetLateral().get_value_or(0.) * length_unit_,
|
||||
// @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane
|
||||
pbde->OffsetVertical().get_value_or(0.),
|
||||
pbde->OffsetVertical().get_value_or(0.) * length_unit_,
|
||||
0.);
|
||||
|
||||
profile_offsets.push_back(po);
|
||||
|
||||
@@ -51,6 +51,10 @@ def create(
|
||||
|
||||
If geometric representations are created, the alignment stationing referent is also created using the start_station value. IfcReferent.ObjectPlacement
|
||||
is required for linear positiion elements and IfcLinearPlacement is defined relative to alignment curve geometry.
|
||||
This referent's Name follows the same "<alignment name> <station>" convention update_key_point_referents() uses
|
||||
for its own key-point referents (e.g. "MyAlignment 49+00.00"), so that every referent nested under an alignment
|
||||
is identifiable by name alone, without needing to inspect its Pset_Stationing or placement to know which
|
||||
alignment it belongs to.
|
||||
|
||||
:param file:
|
||||
:param name: name assigned to IfcAlignment.Name
|
||||
@@ -86,7 +90,7 @@ def create(
|
||||
if include_geometry:
|
||||
_create_geometric_representation(file, alignment)
|
||||
|
||||
referent_name = ifcopenshell.util.alignment.station_as_string(file, start_station)
|
||||
referent_name = f"{name} {ifcopenshell.util.alignment.station_as_string(file, start_station)}"
|
||||
referent = ifcopenshell.api.alignment.add_stationing_referent(file, referent_name, alignment, 0.0, start_station)
|
||||
|
||||
for layout in alignment_layouts:
|
||||
|
||||
@@ -128,6 +128,11 @@ def create_as_polyline(
|
||||
|
||||
The IfcAlignment is aggreated to IfcProject
|
||||
|
||||
The stationing referent created from start_station has Name "<alignment name> <station>"
|
||||
(e.g. "MyAlignment 49+00.00"), the same convention update_key_point_referents() and
|
||||
create() use for their own referents, so every referent nested under an alignment is
|
||||
identifiable by name alone.
|
||||
|
||||
:param file:
|
||||
:param name: name assigned to IfcAlignment.Name
|
||||
:param points: sequence of points defining the polyline
|
||||
@@ -142,8 +147,8 @@ def create_as_polyline(
|
||||
_create_polyline_representation(file, alignment, points)
|
||||
|
||||
# define stationing
|
||||
name = ifcopenshell.util.alignment.station_as_string(file, start_station)
|
||||
referent = ifcopenshell.api.alignment.add_stationing_referent(file, name, alignment, 0.0, start_station)
|
||||
referent_name = f"{alignment.Name} {ifcopenshell.util.alignment.station_as_string(file, start_station)}"
|
||||
referent = ifcopenshell.api.alignment.add_stationing_referent(file, referent_name, alignment, 0.0, start_station)
|
||||
|
||||
# IFC 4.1.4.1.1 Alignment Aggregation To Project
|
||||
project = file.by_type("IfcProject")[0]
|
||||
|
||||
@@ -115,11 +115,19 @@ def update_key_point_referents(
|
||||
get_stationing_nest) -- key-point referents never belong in either of those.
|
||||
|
||||
:param layout: IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
|
||||
:param rel_nests: an existing IfcRelNests to (re)populate; its RelatingObject must be the
|
||||
IfcAlignment that nests `layout` (TypeError is raised otherwise). If omitted, a new
|
||||
IfcRelNests is always created and related to that IfcAlignment -- there is no implicit
|
||||
search for or reuse of a previously created nest. Callers who want to regenerate into an
|
||||
existing nest must pass it back in explicitly via `rel_nests`.
|
||||
:param rel_nests: an existing IfcRelNests to (re)populate; its RelatingObject must be an
|
||||
IfcAlignment (TypeError is raised otherwise), but need not be the IfcAlignment that
|
||||
directly nests `layout` -- passing an ancestor's own IfcRelNests is supported
|
||||
specifically so that a vertical/cant layout living under a child IfcAlignment (per CT
|
||||
4.1.4.4.1.2, once a second vertical layout is added) can still have its key-point
|
||||
referents named after and nested to the top-level parent alignment, matching how the
|
||||
alignment's horizontal key points are named, rather than a generic "Child of X" name.
|
||||
When `rel_nests` is given, `rel_nests.RelatingObject` -- not `layout`'s own direct
|
||||
parent -- is used for both the created referents' Name and the returned IfcRelNests. If
|
||||
omitted, a new IfcRelNests is always created and related to `layout`'s own direct
|
||||
parent alignment -- there is no implicit search for or reuse of a previously created
|
||||
nest. Callers who want to regenerate into an existing nest must pass it back in
|
||||
explicitly via `rel_nests`.
|
||||
:param clear: if True, deletes all IfcReferent currently in rel_nests.RelatedObjects (and their
|
||||
Pset_Stationing) before regenerating. If False (default), new referents are appended to
|
||||
whatever already exists -- no deduplication.
|
||||
@@ -155,17 +163,25 @@ def update_key_point_referents(
|
||||
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
|
||||
)
|
||||
|
||||
alignment = ifcopenshell.api.alignment.get_alignment(layout)
|
||||
if alignment is None:
|
||||
layout_alignment = ifcopenshell.api.alignment.get_alignment(layout)
|
||||
if layout_alignment is None:
|
||||
raise ValueError(f"{layout.is_a()} #{layout.id()} is not nested under an IfcAlignment.")
|
||||
|
||||
# `alignment` is used below for referent naming (and as the fallback-placement basis) --
|
||||
# it defaults to layout's own direct parent, but an explicitly passed rel_nests overrides
|
||||
# it with rel_nests.RelatingObject instead (see the rel_nests docstring above). Station
|
||||
# computation always uses layout_alignment, unaffected by this -- get_alignment_start_station
|
||||
# already walks up to the true top-level alignment's own stationing referent regardless of
|
||||
# which (possibly child) alignment it's given.
|
||||
if rel_nests is not None:
|
||||
if not rel_nests.RelatingObject.is_a("IfcAlignment"):
|
||||
raise TypeError(
|
||||
f"Expected rel_nests.RelatingObject to be IfcAlignment, instead received "
|
||||
f"{rel_nests.RelatingObject.is_a()}"
|
||||
)
|
||||
alignment = rel_nests.RelatingObject
|
||||
else:
|
||||
alignment = layout_alignment
|
||||
rel_nests = file.createIfcRelNests(
|
||||
GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=()
|
||||
)
|
||||
@@ -185,7 +201,7 @@ def update_key_point_referents(
|
||||
)
|
||||
return rel_nests
|
||||
|
||||
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
|
||||
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, layout_alignment)
|
||||
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
|
||||
is_horizontal = layout.is_a("IfcAlignmentHorizontal")
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import ifcopenshell.util.sequence
|
||||
|
||||
def create_baseline(
|
||||
file: ifcopenshell.file, work_schedule: ifcopenshell.entity_instance, name: Optional[str] = None
|
||||
) -> None:
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Creates a baseline for your Work Schedule
|
||||
|
||||
Using a IfcWorkSchdule having PredefinedType=PLANNED,
|
||||
@@ -42,7 +42,7 @@ def create_baseline(
|
||||
* Same Construction Resources
|
||||
* Same Resource Relationships
|
||||
|
||||
:param work_schedule: The planned work_schedule to baseline
|
||||
:param work_schedule: The planned work schedule to baseline
|
||||
:param name: baseline work schedule name
|
||||
:return: The baseline work_schedule
|
||||
|
||||
@@ -51,7 +51,7 @@ def create_baseline(
|
||||
.. code:: python
|
||||
|
||||
# We have a Work Schedule
|
||||
planned_work_schedule = WorkSchedule(name="Design new feature",predefinedType="PLANNED", deadline="2023-03-01")
|
||||
planned_work_schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Planned Construction Schedule")
|
||||
|
||||
# And now we have a baseline for our Work Schedule
|
||||
baseline_work_schedule = ifcopenshell.api.sequence.create_baseline(file, work_schedule=planned_work_schedule, name="Baseline 1")
|
||||
@@ -64,24 +64,23 @@ def create_baseline(
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
|
||||
def execute(self, work_schedule: ifcopenshell.entity_instance, name: Union[str, None]) -> None:
|
||||
# create work schedule
|
||||
if not work_schedule.PredefinedType == "PLANNED":
|
||||
return
|
||||
def execute(
|
||||
self, work_schedule: ifcopenshell.entity_instance, name: Union[str, None]
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if work_schedule.PredefinedType != "PLANNED":
|
||||
raise ValueError("Only a PLANNED work schedule can be baselined.")
|
||||
baseline_work_schedule = ifcopenshell.api.sequence.add_work_schedule(
|
||||
self.file, name=work_schedule.Name, predefined_type="BASELINE"
|
||||
self.file, name=name or work_schedule.Name, predefined_type="BASELINE"
|
||||
)
|
||||
baseline_work_schedule.Name = name
|
||||
self.create_baseline_reference(work_schedule, baseline_work_schedule)
|
||||
for summary_task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
|
||||
res = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task)
|
||||
assert isinstance(res, list)
|
||||
current, duplicate = res
|
||||
current, duplicate = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task)
|
||||
ifcopenshell.api.control.assign_control(
|
||||
self.file, relating_control=baseline_work_schedule, related_objects=[duplicate[0]]
|
||||
)
|
||||
for i, task in enumerate(current):
|
||||
self.create_baseline_reference(task, duplicate[i])
|
||||
return baseline_work_schedule
|
||||
|
||||
def create_baseline_reference(
|
||||
self, relating_object: ifcopenshell.entity_instance, related_object: ifcopenshell.entity_instance
|
||||
|
||||
@@ -69,21 +69,21 @@ def station_as_string(file: ifcopenshell.file, sta: float):
|
||||
Returns a stringized version of a station. Example 100.0 is 1+00.00 as a stationing string.
|
||||
If the project units are SI-based, the string is in the format xxx+yyy.zzz
|
||||
If the project units are Emperial-based, the string is in the format xx+yy.zz
|
||||
|
||||
:param station: the station to be stringized
|
||||
:return: stringized station
|
||||
"""
|
||||
|
||||
unit_type = ifcopenshell.util.unit.get_project_unit(file, "LENGTHUNIT")
|
||||
project_unit_to_metres = ifcopenshell.util.unit.calculate_unit_scale(file)
|
||||
if unit_type.is_a("IfcConversionBasedUnit"):
|
||||
station = ifcopenshell.util.unit.convert(
|
||||
sta, from_unit=unit_type.Name, from_prefix=None, to_unit="foot", to_prefix=None
|
||||
)
|
||||
# xx+yy.zz display is inherently foot-based, regardless of which foot variant
|
||||
# (international vs. US survey, etc.) the project's own unit actually is.
|
||||
station = sta * project_unit_to_metres / 0.3048
|
||||
plus_seperator = 2
|
||||
precision = 2
|
||||
else:
|
||||
station = ifcopenshell.util.unit.convert(
|
||||
sta, from_unit=unit_type.Name, from_prefix=unit_type.Prefix, to_unit="meter", to_prefix=None
|
||||
)
|
||||
station = sta * project_unit_to_metres
|
||||
plus_seperator = 3
|
||||
precision = 3
|
||||
|
||||
|
||||
@@ -962,7 +962,8 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "
|
||||
)
|
||||
|
||||
unit_assignment = get_unit_assignment(file_patched)
|
||||
unit_assignment.Units = [new_length, *(u for u in unit_assignment.Units if u.UnitType != new_length.UnitType)]
|
||||
# UnitType not available on IfcMonetaryUnit
|
||||
unit_assignment.Units = [new_length, *(u for u in unit_assignment.Units if getattr(u, 'UnitType', None) != new_length.UnitType)]
|
||||
if not file_patched.get_total_inverses(old_length):
|
||||
ifcopenshell.util.element.remove_deep2(file_patched, old_length)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def test_add_stationing_to_alignment():
|
||||
referent = stationing_nest.RelatedObjects[0]
|
||||
|
||||
assert referent.PredefinedType == "STATION"
|
||||
assert referent.Name == "2+000.000"
|
||||
assert referent.Name == "TestAlignment 2+000.000"
|
||||
assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing")
|
||||
assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing", prop="Station") == 2000.0
|
||||
assert referent.ObjectPlacement != None
|
||||
|
||||
@@ -20,6 +20,39 @@
|
||||
import ifcopenshell.api.alignment
|
||||
import ifcopenshell.api.context
|
||||
import ifcopenshell.api.unit
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def test_create_stationing_referent_name_includes_alignment_name():
|
||||
"""create() creates an initial stationing IfcReferent from start_station
|
||||
(see add_stationing_referent()). Its Name must include the alignment's
|
||||
own name, the same "<alignment name> <station>" convention
|
||||
update_key_point_referents() uses for its own referents -- otherwise
|
||||
this referent is indistinguishable by name alone from the same-named
|
||||
referent of any OTHER alignment in the same file, unlike every other
|
||||
referent in the model."""
|
||||
file = ifcopenshell.file(schema="IFC4X3_ADD2")
|
||||
project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="Test")
|
||||
length = ifcopenshell.api.unit.add_conversion_based_unit(file, name="foot")
|
||||
ifcopenshell.api.unit.assign_unit(file, units=[length])
|
||||
geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model")
|
||||
ifcopenshell.api.context.add_context(
|
||||
file,
|
||||
context_type="Model",
|
||||
context_identifier="Axis",
|
||||
target_view="MODEL_VIEW",
|
||||
parent=geometric_representation_context,
|
||||
)
|
||||
|
||||
alignment = ifcopenshell.api.alignment.create(file, "TestAlignment", start_station=4900.0)
|
||||
|
||||
referents = [
|
||||
r
|
||||
for r in ifcopenshell.util.element.get_components(alignment)
|
||||
if r.is_a("IfcReferent") and ifcopenshell.util.element.get_pset(r, name="Pset_Stationing", prop="Station") == 4900.0
|
||||
]
|
||||
assert len(referents) == 1
|
||||
assert referents[0].Name == "TestAlignment 49+00.00"
|
||||
|
||||
|
||||
def test_create():
|
||||
@@ -84,3 +117,4 @@ def test_create():
|
||||
|
||||
|
||||
test_create()
|
||||
test_create_stationing_referent_name_includes_alignment_name()
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import ifcopenshell.api.alignment
|
||||
import ifcopenshell.api.unit
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def test_create_as_polyline():
|
||||
@@ -40,10 +41,19 @@ def test_create_as_polyline():
|
||||
file.createIfcCartesianPoint((-585.0, 3275.2, 56.2)),
|
||||
]
|
||||
|
||||
alignment = ifcopenshell.api.alignment.create_as_polyline(file, "A1", points)
|
||||
alignment = ifcopenshell.api.alignment.create_as_polyline(file, "A1", points, start_station=100.0)
|
||||
curve = ifcopenshell.api.alignment.get_curve(alignment)
|
||||
assert curve.is_a("IfcPolyline")
|
||||
assert len(curve.Points) == 10
|
||||
|
||||
# stationing referent's Name must include the alignment's own name, the
|
||||
# same "<alignment name> <station>" convention create() and
|
||||
# update_key_point_referents() use -- previously this reassigned the
|
||||
# local `name` variable (shadowing the "A1" parameter) to just the bare
|
||||
# station string, losing the alignment name entirely.
|
||||
referents = [r for r in ifcopenshell.util.element.get_components(alignment) if r.is_a("IfcReferent")]
|
||||
assert len(referents) == 1
|
||||
assert referents[0].Name == "A1 0+100.000"
|
||||
|
||||
|
||||
test_create_as_polyline()
|
||||
|
||||
@@ -373,6 +373,41 @@ def test_start_station_composes_for_child_alignment():
|
||||
assert stations == pytest.approx([100.0, 600.0, 900.0])
|
||||
|
||||
|
||||
def test_rel_nests_from_ancestor_used_for_naming_and_nesting():
|
||||
"""A vertical layout living under a child alignment (once a second vertical layout is
|
||||
added, per CT 4.1.4.4.1.2) can still have its key-point referents named after and nested
|
||||
to an ancestor alignment's own rel_nests -- e.g. the same one already holding that
|
||||
ancestor's horizontal key points -- rather than the child's generic "Child of X" name."""
|
||||
file = _new_file()
|
||||
alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=False, start_station=100.0)
|
||||
horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
|
||||
horizontal_nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal)
|
||||
horizontal_count = len(horizontal_nest.RelatedObjects)
|
||||
|
||||
ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
|
||||
ifcopenshell.api.alignment.add_vertical_layout(file, alignment) # forces the child-alignment split
|
||||
child_alignment = alignment.IsDecomposedBy[0].RelatedObjects[-1]
|
||||
child_vertical = ifcopenshell.api.alignment.get_vertical_layout(child_alignment)
|
||||
|
||||
dp = file.createIfcAlignmentVerticalSegment(
|
||||
StartDistAlong=0.0,
|
||||
HorizontalLength=500.0,
|
||||
StartHeight=10.0,
|
||||
StartGradient=0.01,
|
||||
EndGradient=0.01,
|
||||
PredefinedType="CONSTANTGRADIENT",
|
||||
)
|
||||
ifcopenshell.api.alignment.create_layout_segment(file, child_vertical, dp)
|
||||
|
||||
result = ifcopenshell.api.alignment.update_key_point_referents(file, child_vertical, rel_nests=horizontal_nest)
|
||||
|
||||
assert result == horizontal_nest
|
||||
assert result.RelatingObject == alignment
|
||||
assert len(result.RelatedObjects) == horizontal_count + 2
|
||||
assert all(r.Name.startswith("A1 ") for r in result.RelatedObjects)
|
||||
assert not any("Child of" in r.Name for r in result.RelatedObjects)
|
||||
|
||||
|
||||
def test_returns_ifc_rel_nests():
|
||||
file = _new_file()
|
||||
alignment = _build_alignment(file)
|
||||
@@ -399,4 +434,5 @@ test_cant_layout_boundary_labels()
|
||||
test_no_real_segments_produces_no_referents()
|
||||
test_single_real_segment_produces_only_boundary_labels()
|
||||
test_start_station_composes_for_child_alignment()
|
||||
test_rel_nests_from_ancestor_used_for_naming_and_nesting()
|
||||
test_returns_ifc_rel_nests()
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2026 IfcOpenShell contributors
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.sequence
|
||||
import ifcopenshell.util.sequence
|
||||
import test.bootstrap
|
||||
|
||||
|
||||
class TestCreateBaseline(test.bootstrap.IFC4):
|
||||
def create_planned_schedule(self, name="Design & Build"):
|
||||
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||
return ifcopenshell.api.sequence.add_work_schedule(self.file, name=name, predefined_type="PLANNED")
|
||||
|
||||
def test_returns_the_created_baseline_schedule(self):
|
||||
planned = self.create_planned_schedule()
|
||||
root_task = ifcopenshell.api.sequence.add_task(self.file, work_schedule=planned, name="Design")
|
||||
|
||||
baseline = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned, name="Baseline 1")
|
||||
|
||||
assert baseline.is_a("IfcWorkSchedule")
|
||||
assert baseline.Name == "Baseline 1"
|
||||
assert baseline.PredefinedType == "BASELINE"
|
||||
baseline_roots = ifcopenshell.util.sequence.get_root_tasks(baseline)
|
||||
assert [task.Name for task in baseline_roots] == [root_task.Name]
|
||||
assert baseline_roots != [root_task]
|
||||
|
||||
def test_falls_back_to_the_planned_schedule_name(self):
|
||||
planned = self.create_planned_schedule()
|
||||
|
||||
baseline = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned)
|
||||
|
||||
assert baseline.Name == "Design & Build"
|
||||
|
||||
def test_leaves_the_name_null_when_both_names_are_omitted(self):
|
||||
planned = self.create_planned_schedule()
|
||||
planned.Name = None
|
||||
|
||||
baseline = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned)
|
||||
|
||||
assert baseline.Name is None
|
||||
|
||||
def test_rejects_a_non_planned_schedule(self):
|
||||
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||
actual = ifcopenshell.api.sequence.add_work_schedule(self.file, predefined_type="ACTUAL")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=actual)
|
||||
|
||||
def test_baselines_a_schedule_without_tasks(self):
|
||||
planned = self.create_planned_schedule()
|
||||
|
||||
baseline = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned, name="Baseline 1")
|
||||
|
||||
assert ifcopenshell.util.sequence.get_root_tasks(baseline) == []
|
||||
|
||||
def test_baselines_every_root_task(self):
|
||||
planned = self.create_planned_schedule()
|
||||
ifcopenshell.api.sequence.add_task(self.file, work_schedule=planned, name="Design")
|
||||
ifcopenshell.api.sequence.add_task(self.file, work_schedule=planned, name="Construction")
|
||||
|
||||
baseline = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned, name="Baseline 1")
|
||||
|
||||
baseline_roots = ifcopenshell.util.sequence.get_root_tasks(baseline)
|
||||
assert sorted(task.Name for task in baseline_roots) == ["Construction", "Design"]
|
||||
|
||||
def test_baselines_nested_tasks(self):
|
||||
planned = self.create_planned_schedule()
|
||||
root_task = ifcopenshell.api.sequence.add_task(self.file, work_schedule=planned, name="Construction")
|
||||
ifcopenshell.api.sequence.add_task(self.file, parent_task=root_task, name="Foundations")
|
||||
ifcopenshell.api.sequence.add_task(self.file, parent_task=root_task, name="Superstructure")
|
||||
|
||||
baseline = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned, name="Baseline 1")
|
||||
|
||||
baseline_root = ifcopenshell.util.sequence.get_root_tasks(baseline)[0]
|
||||
nested = ifcopenshell.util.sequence.get_nested_tasks(baseline_root)
|
||||
assert sorted(task.Name for task in nested) == ["Foundations", "Superstructure"]
|
||||
assert len(self.file.by_type("IfcTask")) == 6
|
||||
|
||||
def test_baselines_task_attributes_and_times(self):
|
||||
planned = self.create_planned_schedule()
|
||||
task = ifcopenshell.api.sequence.add_task(
|
||||
self.file, work_schedule=planned, name="Foundations", identification="A1", description="Pour concrete"
|
||||
)
|
||||
ifcopenshell.api.sequence.add_task_time(self.file, task=task)
|
||||
ifcopenshell.api.sequence.edit_task_time(
|
||||
self.file, task_time=task.TaskTime, attributes={"ScheduleDuration": "P5D"}
|
||||
)
|
||||
|
||||
baseline = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned, name="Baseline 1")
|
||||
|
||||
baseline_task = ifcopenshell.util.sequence.get_root_tasks(baseline)[0]
|
||||
assert baseline_task.Identification == "A1"
|
||||
assert baseline_task.Description == "Pour concrete"
|
||||
assert baseline_task.TaskTime != task.TaskTime
|
||||
assert baseline_task.TaskTime.ScheduleDuration == "P5D"
|
||||
|
||||
def test_baselines_sequence_relationships_between_tasks(self):
|
||||
planned = self.create_planned_schedule()
|
||||
root_task = ifcopenshell.api.sequence.add_task(self.file, work_schedule=planned, name="Construction")
|
||||
predecessor = ifcopenshell.api.sequence.add_task(self.file, parent_task=root_task, name="Foundations")
|
||||
successor = ifcopenshell.api.sequence.add_task(self.file, parent_task=root_task, name="Superstructure")
|
||||
ifcopenshell.api.sequence.assign_sequence(self.file, relating_process=predecessor, related_process=successor)
|
||||
|
||||
baseline = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned, name="Baseline 1")
|
||||
|
||||
baseline_root = ifcopenshell.util.sequence.get_root_tasks(baseline)[0]
|
||||
nested = {task.Name: task for task in ifcopenshell.util.sequence.get_nested_tasks(baseline_root)}
|
||||
rels = nested["Foundations"].IsPredecessorTo
|
||||
assert len(rels) == 1
|
||||
assert rels[0].RelatedProcess == nested["Superstructure"]
|
||||
|
||||
def test_references_the_planned_schedule_and_tasks(self):
|
||||
planned = self.create_planned_schedule()
|
||||
root_task = ifcopenshell.api.sequence.add_task(self.file, work_schedule=planned, name="Construction")
|
||||
subtask = ifcopenshell.api.sequence.add_task(self.file, parent_task=root_task, name="Foundations")
|
||||
|
||||
baseline = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned, name="Baseline 1")
|
||||
|
||||
baseline_root = ifcopenshell.util.sequence.get_root_tasks(baseline)[0]
|
||||
baseline_subtask = ifcopenshell.util.sequence.get_nested_tasks(baseline_root)[0]
|
||||
references = {
|
||||
rel.RelatingObject: list(rel.RelatedObjects) for rel in self.file.by_type("IfcRelDefinesByObject")
|
||||
}
|
||||
assert references[planned] == [baseline]
|
||||
assert references[root_task] == [baseline_root]
|
||||
assert references[subtask] == [baseline_subtask]
|
||||
|
||||
def test_reuses_the_existing_reference_for_further_baselines(self):
|
||||
planned = self.create_planned_schedule()
|
||||
|
||||
first = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned, name="Baseline 1")
|
||||
second = ifcopenshell.api.sequence.create_baseline(self.file, work_schedule=planned, name="Baseline 2")
|
||||
|
||||
assert len(planned.Declares) == 1
|
||||
assert list(planned.Declares[0].RelatedObjects) == [first, second]
|
||||
@@ -77,10 +77,72 @@ def _test_us_stations():
|
||||
assert s == "-1234+56.79"
|
||||
|
||||
|
||||
def _test_custom_named_conversion_based_unit_stations():
|
||||
"""Regression test: station_as_string() must work for an
|
||||
IfcConversionBasedUnit whose Name isn't one of the fixed set
|
||||
ifcopenshell.util.unit.si_conversions recognises (e.g. a project that,
|
||||
reasonably, names its foot-based unit something other than the bare
|
||||
"foot" IfcOpenShell's own add_conversion_based_unit() produces -- for
|
||||
instance to distinguish the US survey foot, 1200/3937 m exactly, from
|
||||
the international foot, 0.3048 m exactly, which differ by ~2 ppm and
|
||||
are NOT interchangeable once a project is tied to a US state plane CRS,
|
||||
virtually all of which are defined in US survey feet).
|
||||
|
||||
Previously, station_as_string() converted via
|
||||
ifcopenshell.util.unit.convert(), which looks up the conversion factor
|
||||
BY NAME in si_conversions -- silently substituting a factor of 1.0
|
||||
(i.e. treating the value as if it were already in the display unit) for
|
||||
any unrecognised name, rather than raising an error. For a project unit
|
||||
like "US survey foot" this inflated every station string by the
|
||||
project-unit<->metre ratio (~3.28x), even though the underlying
|
||||
Pset_Stationing.Station numeric value written by
|
||||
ifcopenshell.api.alignment.create()/update_key_point_referents was
|
||||
correct throughout -- only the display text was wrong.
|
||||
"""
|
||||
file = ifcopenshell.file(schema="IFC4X3_ADD2")
|
||||
project = file.createIfcProject(GlobalId=ifcopenshell.guid.new(), Name="Test")
|
||||
|
||||
# Hand-built rather than via add_conversion_based_unit(), since that
|
||||
# API also resolves its conversion factor by name (si_conversions) and
|
||||
# can't produce a custom name paired with a specific factor.
|
||||
si_unit = file.createIfcSIUnit(UnitType="LENGTHUNIT", Name="METRE")
|
||||
value_component = file.create_entity("IfcReal", wrappedValue=1200.0 / 3937.0) # US survey foot, exact
|
||||
conversion_factor = file.createIfcMeasureWithUnit(value_component, si_unit)
|
||||
exponents = file.createIfcDimensionalExponents(1, 0, 0, 0, 0, 0, 0)
|
||||
length = file.createIfcConversionBasedUnit(exponents, "LENGTHUNIT", "US survey foot", conversion_factor)
|
||||
ifcopenshell.api.unit.assign_unit(file, units=[length])
|
||||
|
||||
# US survey foot and international foot differ by ~2 ppm. At small
|
||||
# station values that's invisible at 2-decimal-place precision, so
|
||||
# these match _test_us_stations()'s "foot" case exactly.
|
||||
s = sta.station_as_string(file, 0.0)
|
||||
assert s == "0+00.00"
|
||||
|
||||
s = sta.station_as_string(file, 100.00)
|
||||
assert s == "1+00.00"
|
||||
|
||||
s = sta.station_as_string(file, -100.00)
|
||||
assert s == "-1+00.00"
|
||||
|
||||
# At a large enough station, ~2 ppm DOES become visible at 2 decimal
|
||||
# places (123456.789 * 2e-6 =~ 0.25) -- this is the real, correct US
|
||||
# survey foot vs. international foot difference, not a bug. Before the
|
||||
# fix, the name-based lookup's silent 1.0 fallback inflated this same
|
||||
# input by ~3.28x to "1234+57.036" -> "4050+82.90"-ish territory, wildly
|
||||
# different from either correct answer -- so this still exercises the
|
||||
# regression, it's just not identical to the "foot" case's value.
|
||||
s = sta.station_as_string(file, 123456.789)
|
||||
assert s == "1234+57.04"
|
||||
|
||||
s = sta.station_as_string(file, -123456.789)
|
||||
assert s == "-1234+57.04"
|
||||
|
||||
|
||||
def test_station_as_string():
|
||||
_test_si_stations()
|
||||
_test_si_stations_millimeter()
|
||||
_test_us_stations()
|
||||
_test_custom_named_conversion_based_unit_stations()
|
||||
|
||||
|
||||
test_station_as_string()
|
||||
|
||||
Reference in New Issue
Block a user