mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 19:07:57 +00:00
Strengthens implementation of station_to_string. Adds alignment name to stationing referent.
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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