From 8d001d0120a6a8415cb903986ce4edd1aa3d714e Mon Sep 17 00:00:00 2001
From: civilx64 <26513472+civilx64@users.noreply.github.com>
Date: Wed, 23 Oct 2024 19:38:16 -0400
Subject: [PATCH] cleanup from original python alignment prototyping
`transition_curve.py` is now obsolete and has been removed.
A python port of IfcAlignmentHelper.cpp has also been started.
It needs further development to accommodate vertical alignment.
---
src/ifcopenshell-python/.gitignore | 2 +-
.../ifcopenshell/alignment.py | 796 +++++++++++++-----
.../ifcopenshell/transition_curve.py | 186 ----
3 files changed, 590 insertions(+), 394 deletions(-)
delete mode 100644 src/ifcopenshell-python/ifcopenshell/transition_curve.py
diff --git a/src/ifcopenshell-python/.gitignore b/src/ifcopenshell-python/.gitignore
index 3d0dbe4467..07549fb389 100644
--- a/src/ifcopenshell-python/.gitignore
+++ b/src/ifcopenshell-python/.gitignore
@@ -1 +1 @@
-tests/
\ No newline at end of file
+uv.lock
\ No newline at end of file
diff --git a/src/ifcopenshell-python/ifcopenshell/alignment.py b/src/ifcopenshell-python/ifcopenshell/alignment.py
index bb43ed6b19..42f82b18fb 100644
--- a/src/ifcopenshell-python/ifcopenshell/alignment.py
+++ b/src/ifcopenshell-python/ifcopenshell/alignment.py
@@ -16,228 +16,83 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
-import operator
-from dataclasses import dataclass
+import math
+from typing import Sequence
-import numpy
+import numpy as np
import ifcopenshell
import ifcopenshell.geom
-import ifcopenshell.express
-import ifcopenshell.transition_curve
-
-# geometric primitives
-
-# @notes
-# - not sure if the separation of geometric primitives make sense
-# does it make handling the variety of distance expressions and
-# interpolation harder?
+import ifcopenshell.guid
+import ifcopenshell.template
+from ifcopenshell import entity_instance
+from ifcopenshell import ifcopenshell_wrapper
-@dataclass
-class line:
- start_point: numpy.ndarray
- direction_vector: numpy.ndarray
-
- def __call__(self, u):
- p = numpy.ndarray((3,))
- p[0:2] = self.start_point + self.direction_vector * u
- p[2] = numpy.nan
- return p
-
-
-@dataclass
-class circle:
- radius: numpy.ndarray
-
- def __call__(self, u):
- return numpy.array([self.radius * numpy.cos(u), self.radius * numpy.sin(u), numpy.nan])
-
-
-def place(matrix, func):
+def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray:
"""
- Higher order function for application of a 3x3 matrix
- to a 2D point. Assumes a functor such as line or circle.
+ Calculate the 4x4 geometric transform at a point on an alignment segment
+ @param shape_rep: The representation shape (composite curve, gradient curve, or segmented reference curve) to evaluate
+ @param dist_along: The distance along this representation at the point of interest (point to be calculated)
"""
+ supported_rep_types = ["IFCCOMPOSITECURVE", "IFCGRADIENTCURVE", "IFCSEGMENTEDREFERENCECURVE"]
+ shape_rep_type = shape_rep.is_a().upper()
+ if not shape_rep_type in supported_rep_types:
+ raise NotImplementedError(f"Expected entity type to be one of {[_ for _ in supported_rep_types]}, got '{shape_rep_type}")
- def inner(*args):
- v = func(*args)
- # homogenize
- v = numpy.insert(v[0:2], v[0:2].shape, 1, axis=-1)
- p = numpy.ndarray((3,))
- p[0:2] = (matrix @ v)[0:2]
- p[2] = numpy.nan
- return p
+ # TODO: confirm point is not beyond limits of alignment
- return inner
+ s = ifcopenshell.geom.settings()
+ piecewise_function = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data)
+
+ trans_matrix = piecewise_function.evaluate(dist_along)
+
+ return np.array(trans_matrix, dtype=np.float64).T
-# primitives for manipulating and joining curve functor domains
-
-
-def reparametrized_curve(fn, a, b):
- return lambda u: fn(a * u + b)
-
-
-def normalized_curve(fn):
- return lambda u: fn(u / fn.length)
-
-
-class trimmed_curve:
- def __init__(self, fn, length):
- self.fn = fn
- self.length = length
-
- def __call__(self, u):
- assert u >= 0.0 and u <= self.length
- return self.fn(u)
-
-
-class piecewise:
- # takes a set of functors and returns a function f(u) that delegates to the correct segment
-
- def __init__(self, fns):
- self.fns = fns
- self.length = sum(map(operator.attrgetter("length"), fns))
-
- def __call__(self, u):
- # this is silly, assuming `u` is monotonically increases we should not always start
- # searching from the first segment or at least binary search into the segment
- # lengths
- u0 = 0
- for fn in self.fns:
- u1 = u0 + fn.length
- if u >= u0 and u <= u1:
- return fn(u - u0)
- u0 = u1
-
-
-# mapping functions from IFC entities
-
-
-def map_inst(inst):
+def evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray:
"""
- Looks up one of the implementation functions below in the global namespace
+ Calculate the 4x4 geometric transform at a point on an alignment segment
+ @param segment: The segment containing the point that we would like to
+ @param dist_along: The distance along this segment at the point of interest (point to be calculated)
"""
- return globals()[f"impl_{inst.is_a()}"](inst)
+ supported_segment_types = ["IFCCURVESEGMENT"]
+ segment_type = segment.is_a().upper()
+ if not segment_type in supported_segment_types:
+ raise NotImplementedError(f"Expected entity type 'IFCCURVESEGMENT', got '{segment_type}")
+ if dist_along > segment.SegmentLength:
+ raise ValueError(f"Provided value {dist_along=} is beyond the end of the segment ({segment.SegmentLength}).")
+
+ s = ifcopenshell.geom.settings()
+ piecewise_function = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data)
+
+ trans_matrix = piecewise_function.evaluate(dist_along)
+
+ return np.array(trans_matrix, dtype=np.float64).T
-def impl_IfcLine(inst):
- return line(
- numpy.array(inst.Pnt.Coordinates),
- numpy.array(inst.Dir.Orientation.DirectionRatios) * inst.Dir.Magnitude,
- )
-
-
-def impl_IfcCircle(inst):
- return place(map_inst(inst.Position), circle(inst.Radius))
-
-
-def impl_IfcClothoid(inst):
- # @todo
- # place = map_inst(inst.Position)
- # ifcopenshell.transition_curve.TransitionCurve(
- # StartPoint = place.T[2]
- # StartDirection = numpy.arctan2(place.T[0][1], place.T[0][0]),
- # SegmentLength =
- # IsStartRadiusCCW =
- # IsEndRadiusCCW =
- # TransitionCurveType =
- # StartRadius =
- # EndRadius =
- # )
- return lambda *args: numpy.array((0.0, 0.0))
-
-
-def impl_IfcAxis2Placement2D(inst):
- arr = numpy.eye(3)
-
- if inst is None:
- return arr
-
- arr.T[2, 0:2] = inst.Location.Coordinates
-
- if inst.RefDirection is None:
- return arr
-
- arr.T[0, 0:2] = inst.RefDirection.DirectionRatios
- arr.T[0, 0:2] /= numpy.linalg.norm(arr.T[0, 0:2])
- arr.T[1, 0:2] = -arr.T[0, 1], arr.T[0, 0]
-
- return arr
-
-
-# conversion functions for semantic design parameters (not used atm)
-
-
-def convert(inst):
+def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0) -> np.ndarray:
"""
- Looks up one of the conversion functions below in the global namespace
+ Generate vertices along an alignment
+
+ @param rep_curve: The alignment's representation curve to use to generate vertices.
+
+ Note: rep_curve must be IfcCompositeCurve, IfcGradientCurve, or IfcSegmentedReferenceCurve
+
+ @param distance_interval: The distance between points along the alignment at which to generate the points
"""
- yield from globals()[f"convert_{inst.is_a()}_{inst.PredefinedType}"](inst)
+ if rep_curve is None:
+ raise ValueError("Alignment representation not found.")
-
-def convert_IfcAlignmentHorizontalSegment_LINE(data):
- xy = numpy.array(data.StartPoint.Coordinates)
- yield xy
- di = numpy.array([numpy.cos(data.StartDirection), numpy.sin(data.StartDirection)])
- yield xy + di * data.SegmentLength
-
-
-# Two approaches, either DesignParameters or Representation
-
-
-def interpret_linear_element_semantics(settings, crv):
- # traverse decomposition
- for rel in crv.IsNestedBy:
- for obj in rel.RelatedObjects:
- yield from interpret_linear_element_semantics(settings, obj)
-
- # lookup design parameters and dispatch to conversion function
- if crv.is_a("IfcAlignmentSegment"):
- dp = crv.DesignParameters
- yield from convert(dp)
-
-
-def evaluate_segment(segment):
- # print(segment)
- # print(segment.ParentCurve)
- # print()
-
- func = place(map_inst(segment.Placement), map_inst(segment.ParentCurve))
-
- # reparam so domain starts at zero
- reparam = reparametrized_curve(func, 1.0, -segment.SegmentStart[0])
-
- # embed curve length (doesn't do much, just make length recoverable)
- trimmed = trimmed_curve(reparam, segment.SegmentLength[0])
-
- return trimmed
-
-
-def interpret_linear_element_geometry(settings, crv):
- func = piecewise(
- list(
- map(
- evaluate_segment,
- crv.Representation.Representations[0].Items[0].Segments,
- )
- )
- )
-
- for u in numpy.linspace(0, func.length, num=int(numpy.ceil(func.length / 0.05))):
- yield func(u)
-
-
-interpret_linear_element = interpret_linear_element_geometry
-
-
-def create_shape(settings, elem):
- if elem.is_a("IfcLinearPositioningElement") or elem.is_a("IfcLinearElement"):
- return numpy.row_stack(list(interpret_linear_element(settings, elem)))
- else:
- return ifcopenshell.geom.create_shape(settings, elem)
+ s = ifcopenshell.geom.settings()
+ s.set("PIECEWISE_STEP_PARAM", distance_interval)
+ shape = ifcopenshell.geom.create_shape(s, rep_curve)
+ vertices = shape.verts
+ if len(vertices) == 0:
+ msg = f"[ERROR] No vertices generated by ifcopenshell.geom.create_shape()."
+ raise ValueError(msg)
+ return np.array(vertices).reshape((-1, 3))
def print_structure(alignment, indent=0):
@@ -250,17 +105,544 @@ def print_structure(alignment, indent=0):
print_structure(child, indent + 2)
+def name_segments(prefix: str, segments: Sequence[entity_instance]) -> None:
+ """
+ Sets the segment name like ("H1" for horizontal, "V1" for vertical, "C1" for cant)
+ """
+ for i, segment in enumerate(segments):
+ segment.Name = f"{prefix}{i + 1}"
+
+
+class IfcAlignmentHelper:
+ """
+ Create a new IfcAlignment including horizontal and vertical alignments by PI points.
+
+ Currently only supports horizontal lines and circular arcs (no spirals or other transitions)
+ Currently only supports parabolic vertical curves.
+ Does not yet accommodate cant alignment considerations.
+ """
+
+ # TODO: add missing functionality noted in the docstring
+
+ def __init__(self,
+ file: ifcopenshell.file = None,
+ filename: str = None,
+ creator: str = None,
+ organization: str = None,
+ application: str = None,
+ project_globalid=None,
+ project_name: str = None
+ ):
+ """
+ @param file: An existing model that the alignment will be added to
+ @param filename: Name for a new model to be created that will contain the alignment
+ @param creator: Name of the actor creating the file
+ @param organization: Name of the creator's organization
+ @param application: Name of the authoring application
+ @param project_globalid: value for the file's IfcProject.GlobalId attribute
+ @param project_name: value for the file's IfcProject.Name attribute
+ """
+ if file is None:
+ self._file = ifcopenshell.template.create(
+ filename=filename,
+ creator=creator,
+ organization=organization,
+ application=application,
+ project_globalid=project_globalid,
+ project_name=project_name,
+ schema_identifier="IFC4X3_ADD2",
+ )
+ else:
+ self._file = file
+
+ self._geom_context = self._file.by_type("IfcGeometricRepresentationContext")[0]
+ self._axis_geom_subcontext = self._file.createIfcGeometricRepresentationSubContext(
+ ContextIdentifier="Axis",
+ ContextType="Model",
+ ParentContext=self._geom_context,
+ TargetView="GRAPH_VIEW"
+ )
+
+ def _create_segment_representations(self, global_placement: entity_instance,
+ curve_segments: Sequence[entity_instance], segments: Sequence[entity_instance]):
+ for curve_segment, alignment_segment in zip(curve_segments, segments):
+ axis_representation = self._file.create_entity(
+ type="IfcShapeRepresentation",
+ ContextOfItems=self._axis_geom_subcontext,
+ RepresentationIdentifier="Axis",
+ RepresentationType="Segment",
+ Items=(curve_segment,)
+ )
+ product = self._file.create_entity(
+ type="IfcProductDefinitionShape",
+ Name=None,
+ Description=None,
+ Representations=(axis_representation,)
+ )
+ alignment_segment.ObjectPlacement = global_placement
+ alignment_segment.Representation = product
+
+ def _map_alignment_horizontal_segment(self, segment: entity_instance) -> Sequence[entity_instance]:
+ segment_type = segment.is_a().upper()
+ expected_type = "IFCALIGNMENTHORIZONTALSEGMENT"
+ if not segment_type == expected_type:
+ raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment_type}'.")
+
+ start_point = segment.StartPoint
+ start_direction = segment.StartDirection
+ start_radius = segment.StartRadiusOfCurvature
+ length = segment.SegmentLength
+ _type = segment.PredefinedType
+
+ if math.isclose(length, 0):
+ # set transition value based on whether this is the final zero-length segment
+ transition = "DISCONTINUOUS"
+ else:
+ transition = "CONTSAMEGRADIENTSAMECURVATURE"
+
+ match _type:
+ case "LINE":
+ parent_curve = self._file.create_entity(
+ type="IfcLine",
+ Pnt=self._file.create_entity(
+ type="IfcCartesianPoint",
+ Coordinates=(0., 0.),
+ ),
+ Dir=self._file.create_entity(
+ type="IfcVector",
+ Orientation=self._file.create_entity(
+ type="IfcDirection",
+ DirectionRatios=(1., 0.),
+ ),
+ Magnitude=1.,
+ )
+ )
+ curve_segment = self._file.create_entity(
+ type="IfcCurveSegment",
+ Transition=transition,
+ Placement=self._file.create_entity(
+ type="IfcAxis2Placement2D",
+ Location=start_point,
+ RefDirection=self._file.createIfcDirection(
+ (math.cos(start_direction), math.sin(start_direction)),
+ ),
+ ),
+ SegmentStart=self._file.createIfcLengthMeasure(0.),
+ SegmentLength=self._file.createIfcLengthMeasure(length),
+ ParentCurve=parent_curve,
+ )
+ result = (curve_segment, None)
+ case "CIRCULARARC":
+ parent_curve = self._file.createIfcCircle(
+ Position=self._file.createIfcAxis2Placement2D(
+ Location=self._file.createIfcCartesianPoint(Coordinates=(0., 0.)),
+ RefDirection=self._file.createIfcDirection(
+ (math.cos(start_direction), math.sin(start_direction))),
+ ),
+ Radius=abs(start_radius),
+ )
+
+ curve_segment = self._file.create_entity(
+ type="IfcCurveSegment",
+ Transition=transition,
+ Placement=self._file.create_entity(
+ type="IfcAxis2Placement2D",
+ Location=start_point,
+ RefDirection=self._file.createIfcDirection(
+ (math.cos(start_direction), math.sin(start_direction)))),
+ SegmentStart=self._file.createIfcLengthMeasure(0.),
+ SegmentLength=self._file.createIfcLengthMeasure(length * start_radius / abs(start_radius)),
+ ParentCurve=parent_curve,
+ )
+ result = (curve_segment, None)
+
+ case _:
+ result = (None, None)
+
+ return result
+
+ def _create_horizontal_alignment(
+ self, name: str, description: str, points: Sequence[Sequence[float]], radii: Sequence[float],
+ include_geometry: bool = True
+ ):
+ """
+ Create a horizontal alignment using the PI layout method.
+
+ @param name: value for Name attribute
+ @param description: value for Description attribute
+ @param points: (X, Y) pairs denoting the location of the horizontal PIs, including start (POB) and end (POE).
+ @param radii: radii values to use for transition
+ @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic
+ """
+ horizontal_segments = list() # business logic
+ horizontal_curve_segments = list() # geometry
+
+ xBT, yBT = points[0]
+ xPI, yPI = points[1]
+
+ i = 1
+
+ for radius in radii:
+ # back tangent
+ dxBT = xPI - xBT
+ dyBT = yPI - yBT
+ angleBT = math.atan2(dyBT, dxBT)
+ lengthBT = math.sqrt(dxBT * dxBT + dyBT * dyBT)
+
+ # forward tangent
+ i += 1
+ xFT, yFT = points[i]
+ dxFT = xFT - xPI
+ dyFT = yFT - yPI
+ angleFT = math.atan2(dyFT, dxFT)
+
+ delta = angleFT - angleBT
+
+ tangent = abs(radius * math.tan(delta / 2))
+
+ lc = abs(radius * delta)
+
+ radius *= delta / abs(delta)
+
+ xPC = xPI - tangent * math.cos(angleBT)
+ yPC = yPI - tangent * math.sin(angleBT)
+
+ xPT = xPI + tangent * math.cos(angleFT)
+ yPT = yPI + tangent * math.sin(angleFT)
+
+ tangent_run = lengthBT - tangent
+
+ # create back tangent run
+ pt = self._file.create_entity(
+ type="IfcCartesianPoint",
+ Coordinates=(xBT, yBT),
+ )
+ design_parameters = self._file.create_entity(
+ type="IfcAlignmentHorizontalSegment",
+ StartTag=None,
+ EndTag=None,
+ StartPoint=pt,
+ StartDirection=angleBT,
+ StartRadiusOfCurvature=0.0,
+ EndRadiusOfCurvature=0.0,
+ SegmentLength=tangent_run,
+ GravityCenterLineHeight=None,
+ PredefinedType="LINE",
+ )
+ alignment_segment = self._file.create_entity(
+ type="IfcAlignmentSegment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=None,
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ DesignParameters=design_parameters,
+ )
+ horizontal_segments.append(alignment_segment)
+
+ if include_geometry:
+ horizontal_curve_segments.append(
+ self._map_alignment_horizontal_segment(design_parameters)[0]
+ )
+
+ # create circular curve
+ pc = self._file.create_entity(
+ type="IfcCartesianPoint",
+ Coordinates=(xPC, yPC),
+ )
+ design_parameters = self._file.create_entity(
+ type="IfcAlignmentHorizontalSegment",
+ StartTag=None,
+ EndTag=None,
+ StartPoint=pc,
+ StartDirection=angleBT,
+ StartRadiusOfCurvature=float(radius),
+ EndRadiusOfCurvature=float(radius),
+ SegmentLength=lc,
+ GravityCenterLineHeight=None,
+ PredefinedType="CIRCULARARC",
+ )
+ alignment_segment = self._file.create_entity(
+ type="IfcAlignmentSegment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=None,
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ DesignParameters=design_parameters,
+ )
+ horizontal_segments.append(alignment_segment)
+
+ if include_geometry:
+ horizontal_curve_segments.append(
+ self._map_alignment_horizontal_segment(design_parameters)[0]
+ )
+
+ xBT = xPT
+ yBT = yPT
+ xPI = xFT
+ yPI = yFT
+
+ # done processing radii
+ # create last tangent run
+ dx = xPI - xBT
+ dy = yPI - yBT
+ angleBT = math.atan2(dy, dx)
+ tangent_run = math.sqrt(dx * dx + dy * dy)
+ pt = self._file.create_entity(
+ type="IfcCartesianPoint",
+ Coordinates=(xBT, yBT)
+ )
+
+ design_parameters = self._file.create_entity(
+ type="IfcAlignmentHorizontalSegment",
+ StartTag=None,
+ EndTag=None,
+ StartPoint=pt,
+ StartDirection=angleBT,
+ StartRadiusOfCurvature=0.0,
+ EndRadiusOfCurvature=0.0,
+ SegmentLength=tangent_run,
+ GravityCenterLineHeight=None,
+ PredefinedType="LINE",
+ )
+ alignment_segment = self._file.create_entity(
+ type="IfcAlignmentSegment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=None,
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ DesignParameters=design_parameters,
+ )
+ horizontal_segments.append(alignment_segment)
+ if include_geometry:
+ horizontal_curve_segments.append(
+ self._map_alignment_horizontal_segment(design_parameters)[0]
+ )
+
+ # create zero length terminator segment
+ poe = self._file.create_entity(
+ type="IfcCartesianPoint",
+ Coordinates=(xPI, yPI)
+ )
+
+ design_parameters = self._file.create_entity(
+ type="IfcAlignmentHorizontalSegment",
+ StartTag="POE",
+ EndTag="POE",
+ StartPoint=poe,
+ StartDirection=angleBT,
+ StartRadiusOfCurvature=0.0,
+ EndRadiusOfCurvature=0.0,
+ SegmentLength=0.0,
+ GravityCenterLineHeight=None,
+ PredefinedType="LINE",
+ )
+ alignment_segment = self._file.create_entity(
+ type="IfcAlignmentSegment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=None,
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ DesignParameters=design_parameters,
+ )
+ horizontal_segments.append(alignment_segment)
+ if include_geometry:
+ horizontal_curve_segments.append(
+ self._map_alignment_horizontal_segment(design_parameters)[0]
+ )
+
+ if include_geometry:
+ composite_curve = self._file.create_entity(
+ type="IfcCompositeCurve",
+ Segments=horizontal_curve_segments,
+ SelfIntersect=False,
+ )
+ else:
+ composite_curve = None
+
+ return horizontal_segments, horizontal_curve_segments, composite_curve
+
+ def _add_horizontal_alignment(
+ self, alignment_name: str, points: Sequence[Sequence[float]], radii: Sequence[float],
+ include_geometry: bool = True, alignment_description: str = None, start_station : float = 1000.,
+ ):
+ horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment(
+ alignment_name, alignment_description, points, radii, include_geometry,
+ )
+
+ name_segments(prefix="H", segments=horizontal_segments)
+
+ # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments
+ horizontal_alignment = self._file.create_entity(
+ type="IfcAlignmentHorizontal",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=f"{alignment_name} - Horizontal",
+ Description=alignment_description,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ )
+
+ nests_horizontal_segments = self._file.create_entity(
+ type="IfcRelNests",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name="Nests horizontal alignment segments under horizontal alignment",
+ RelatingObject=horizontal_alignment,
+ RelatedObjects=horizontal_segments,
+ )
+
+ placement = self._file.createIfcLocalPlacement(
+ PlacementRelTo=None,
+ RelativePlacement=self._file.createIfcAxis2Placement2D(
+ Location=self._file.createIfcCartesianPoint(
+ Coordinates=(0., 0.)
+ )
+ )
+ )
+
+ # create the alignment
+ alignment = self._file.create_entity(
+ type="IfcAlignment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=alignment_name,
+ Description=alignment_description,
+ ObjectType=None,
+ ObjectPlacement=placement,
+ Representation=None,
+ PredefinedType=None,
+ )
+
+ # create geometric representation
+ if include_geometry:
+ # create the footprint representation
+ footprint_shape_representation = self._file.create_entity(
+ type="IfcShapeRepresentation",
+ ContextOfItems=self._axis_geom_subcontext,
+ RepresentationIdentifier="FootPrint",
+ RepresentationType="Curve2D",
+ Items=(composite_curve,)
+ )
+
+ # create the alignment product definition
+ product_definition_shape = self._file.create_entity(
+ type="IfcProductDefinitionShape",
+ Name="Alignment Product Definition Shape",
+ Description=None,
+ Representations=(footprint_shape_representation,)
+
+ )
+
+ # create representations for each segment
+ self._create_segment_representations(
+ placement, horizontal_curve_segments, horizontal_segments)
+
+ # add the representation to the alignment
+ alignment.Representation = product_definition_shape
+
+
+ # create referent for start station
+ start_referent = self._file.createIfcReferent(
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name="Start Station",
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=self._file.createIfcLinearPlacement(
+ RelativePlacement=self._file.createIfcAxis2PlacementLinear(
+ Location=self._file.createIfcPointByDistanceExpression(
+ DistanceAlong=self._file.createIfcLengthMeasure(0.),
+ OffsetLateral=None,
+ OffsetVertical=None,
+ OffsetLongitudinal=None,
+ BasisCurve=composite_curve,
+ ),
+ ),
+ CartesianPosition=None,
+ ),
+ Representation=None,
+ PredefinedType="STATION",
+ )
+
+ # nest the horizontal and the referent under the alignment
+ nesting_of_alignment = self._file.create_entity(
+ type="IfcRelNests",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name="Nests horizontal alignment and referents under overall alignment",
+ RelatingObject=alignment,
+ RelatedObjects=(horizontal_alignment, start_referent)
+ )
+
+ # aggregate the horizontal under the project
+ project = self._file.by_type("IfcProject")[0]
+ alignment_within_project = self._file.createIfcRelAggregates(
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name="Aggregates alignment under the project",
+ RelatingObject=project,
+ RelatedObjects=(alignment, )
+ )
+
+ return alignment
+
+ def add_vertical_alignment(
+ self, name: str, description: str, vpoints: Sequence[Sequence[float]], vclengths: Sequence[Sequence[float]],
+ include_geometry: bool = True
+ ):
+ """
+ Create a vertical alignment using the PI layout method.
+
+ @param name: value for Name attribute
+ @param description: value for Description attribute
+ @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end.
+ @param vclengths: radii values to use for transition
+ @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic
+ """
+ pass
+
+
+ def add_alignment(self,
+ name: str, hpoints: Sequence[Sequence[float]],
+ radii: Sequence[float],
+ include_geometry: bool = True,
+ description: str = None,
+ start_station: float = 1000.,
+ ):
+ """
+ Create a new alignment with a horizontal alignment using the PI layout method
+ """
+ self._add_horizontal_alignment(alignment_name=name, points=hpoints, radii=radii,
+ include_geometry=include_geometry, alignment_description=description,
+ start_station=start_station)
+
+ def save_file(self, filename) -> None:
+ self._file.write(filename)
+
+
if __name__ == "__main__":
import sys
from matplotlib import pyplot as plt
- s = ifcopenshell.express.parse("IFC4x3_RC3.exp")
- ifcopenshell.register_schema(s)
f = ifcopenshell.open(sys.argv[1])
print_structure(f.by_type("IfcAlignment")[0])
- al_hor = f.by_type("IfcAlignmentHorizontal")[0]
- xy = create_shape({}, al_hor)
+ al_hor_rep = f.by_type("IfcCompositeCurve")[0]
- plt.plot(xy.T[0], xy.T[1])
+ xy = generate_vertices(rep_curve=al_hor_rep, distance_interval=10.0)
+
+ plt.plot(xy[0], xy[1])
plt.savefig("horizontal_alignment.png")
diff --git a/src/ifcopenshell-python/ifcopenshell/transition_curve.py b/src/ifcopenshell-python/ifcopenshell/transition_curve.py
deleted file mode 100644
index 5fc83ecba7..0000000000
--- a/src/ifcopenshell-python/ifcopenshell/transition_curve.py
+++ /dev/null
@@ -1,186 +0,0 @@
-# IfcOpenShell - IFC toolkit and geometry engine
-# Copyright (C) 2021 Thomas Krijnen
-#
-# 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.
-#
-# IfcOpenShell is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with IfcOpenShell. If not, see .
-
-
-from enum import Enum
-from dataclasses import dataclass
-import math
-
-from OCC.Core.gp import gp_Pnt2d
-from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeEdge2d
-from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeWire
-
-
-class IfcTransitionCurveType(Enum):
- """IFC 4.1 Section 8.9.2.9
- [https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/schema/ifcgeometryresource/lexical/ifctransitioncurvetype.htm]
-
- The IfcTransitionCurveType indicates the curvature of a transition curve.
- """
-
- BIQUADRATICPARABOLA = 1 # NOTE also referred to as Schramm curve.
- BLOSSCURVE = 2
- CLOTHOIDCURVE = 3
- COSINECURVE = 4
- CUBICPARABOLA = 5
- SINECURVE = 6 # NOTE also referred to as Klein curve
-
-
-@dataclass
-class TransitionCurve:
- """
- A curve that transitions between a straight line and a circular arc
- (or the reverse).
- """
-
- StartPoint: tuple # IfcSchema::IfcCartesianPoint
- StartDirection: float # IfcSchema::IfcPlaneAngleMeasure
- SegmentLength: float # IfcSchema::IfcPositiveLengthMeasure
- IsStartRadiusCCW: bool # IfcSchema::IfcBoolean
- IsEndRadiusCCW: bool # IfcSchema::IfcBoolean
- TransitionCurveType: IfcTransitionCurveType
- StartRadius: float = None # IfcSchema::IfcPositiveLengthMeasure
- EndRadius: float = None # IfcSchema::IfcPositiveLengthMeasure
-
- def _calc_biquadratic_parabola_point(self, lpt, L, R, ccw):
- x = lpt
- if x <= (L / 2):
- y = x**4 / (6 * R * L**2)
- else:
-
- yterm_1 = (-1 * x**4) / (6 * R * L**2)
- yterm_2 = (2 * x**3) / (3 * R * L)
- yterm_3 = x**2 / (2 * R)
- yterm_4 = (L * x) / (6 * R)
- yterm_5 = L**2 / (48 * R)
-
- y = yterm_1 + yterm_2 - yterm_3 + yterm_4 - yterm_5
-
- if not ccw:
- y = -y
-
- return gp_Pnt2d(x, y)
-
- def _calc_bloss_curve_point(self, lpt, L, R, ccw):
- pass
-
- def _calc_clothoid_curve_point(self, lpt, L, R, ccw):
- RL = R * L
- xterm_1 = 1
- xterm_2 = lpt**4 / (40 * RL**2)
- xterm_3 = lpt**8 / (3456 * RL**4)
- xterm_4 = lpt**12 / (599040 * RL**6)
- x = lpt * (xterm_1 - xterm_2 + xterm_3 - xterm_4)
-
- factor = lpt**3 / (6 * RL)
- yterm_1 = 1
- yterm_2 = lpt**4 / (56 * RL**2)
- yterm_3 = lpt**8 / (7040 * RL**4)
- yterm_4 = lpt**12 / (1612800 * RL**6)
-
- y = factor * (yterm_1 - yterm_2 + yterm_3 - yterm_4)
-
- if not ccw:
- y = -y
-
- return gp_Pnt2d(x, y)
-
- def _calc_cosine_curve_point(self, lpt, L, R, ccw):
- pi = math.pi
- psi_x = (pi * lpt) / L
-
- xterm_1 = (L**2) / (8.0 * pi**2 * R**2)
- xterm_2 = L / pi
- xterm_3 = psi_x**3 / (3.0)
- xterm_4 = psi_x / (2.0)
- xterm_5 = (math.sin(psi_x) * math.cos(psi_x)) / (2.0)
- xterm_6 = psi_x * math.cos(psi_x)
-
- x = lpt - xterm_1 * xterm_2 * (xterm_3 + xterm_4 - xterm_5 - (2.0 * xterm_6))
-
- # TODO: code for y - coordinate
- y = 0
-
- if not ccw:
- y = -y
-
- return gp_Pnt2d(x, y)
-
- def _calc_cubic_parabola_point(self, lpt, L, R, ccw):
-
- x = lpt
- y = math.pow(x, 3) / (6 * R * L)
- if not ccw:
- y = -y
-
- return gp_Pnt2d(x, y)
-
- def _calc_sine_curve_point(self, lpt, L, R, ccw):
- pass
-
- def _calc_transition_curve_point(self, lpt, L, R, ccw, trans_type):
-
- if trans_type == "BIQUADRATICPARABOLA":
- return self._calc_cubic_parabola_point(lpt, L, R, ccw)
- elif trans_type == "BLOSSCURVE":
- # return _calc_bloss_curve_point(lpt, L, R, ccw)
- raise ValueError(f"Transition Curve type '{trans_type}' not implemented yet.")
- elif trans_type == "CLOTHOIDCURVE":
- return self._calc_clothoid_curve_point(lpt, L, R, ccw)
- elif trans_type == "COSINECURVE":
- # return _calc_cosine_curve_point(lpt, L, R, ccw)
- raise ValueError(f"Transition Curve type '{trans_type}' not implemented yet.")
- elif trans_type == "CUBICPARABOLA":
- return self._calc_cubic_parabola_point(lpt, L, R, ccw)
- elif trans_type == "SINECURVE":
- # return _calc_sine_curve_point(lpt, L, R, ccw)
- raise ValueError(f"Transition Curve type '{trans_type}' not implemented yet.")
- else:
- raise ValueError(f"Invalid Transition Curve type '{trans_type}'.")
-
- def to_wire(self, stroking_interval=5.0):
- """convert IfcTransitionSegment2D to OCC wire
-
- :param stroking_interval: maximum curve length between points to be calculated
- :type stroking_interval: float
- :return: OCC wire containing interpolated points
- """
- points = list()
-
- L = self.SegmentLength
- R = self.EndRadius
- ccw = self.IsStartRadiusCCW
- trans_type = self.TransitionCurveType.name
-
- num_intervals = math.ceil(L / stroking_interval)
- interval_dist = L / num_intervals
- lpt = 0.0 # length along the curve at the point to be calculated
-
- for _ in range(num_intervals):
- points.append(self._calc_transition_curve_point(lpt, L, R, ccw, trans_type))
- lpt += interval_dist
-
- edges = list()
- for i in range(len(points) - 1):
- edges.append(BRepBuilderAPI_MakeEdge2d(points[i], points[i + 1]))
-
- wire = BRepBuilderAPI_MakeWire()
- for e in edges:
- wire.Add(e.Edge())
- # return wire
- return points