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