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.
This commit is contained in:
civilx64
2024-10-23 19:38:16 -04:00
committed by Thomas Krijnen
parent ea48996f0d
commit 8d001d0120
3 changed files with 590 additions and 394 deletions
+1 -1
View File
@@ -1 +1 @@
tests/ uv.lock
+589 -207
View File
@@ -16,228 +16,83 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import operator
from dataclasses import dataclass import math
from typing import Sequence
import numpy import numpy as np
import ifcopenshell import ifcopenshell
import ifcopenshell.geom import ifcopenshell.geom
import ifcopenshell.express import ifcopenshell.guid
import ifcopenshell.transition_curve import ifcopenshell.template
from ifcopenshell import entity_instance
# geometric primitives from ifcopenshell import ifcopenshell_wrapper
# @notes
# - not sure if the separation of geometric primitives make sense
# does it make handling the variety of distance expressions and
# interpolation harder?
@dataclass def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray:
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):
""" """
Higher order function for application of a 3x3 matrix Calculate the 4x4 geometric transform at a point on an alignment segment
to a 2D point. Assumes a functor such as line or circle. @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): # TODO: confirm point is not beyond limits of alignment
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
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 evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray:
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):
""" """
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): def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0) -> np.ndarray:
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):
""" """
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.")
s = ifcopenshell.geom.settings()
def convert_IfcAlignmentHorizontalSegment_LINE(data): s.set("PIECEWISE_STEP_PARAM", distance_interval)
xy = numpy.array(data.StartPoint.Coordinates) shape = ifcopenshell.geom.create_shape(s, rep_curve)
yield xy vertices = shape.verts
di = numpy.array([numpy.cos(data.StartDirection), numpy.sin(data.StartDirection)]) if len(vertices) == 0:
yield xy + di * data.SegmentLength msg = f"[ERROR] No vertices generated by ifcopenshell.geom.create_shape()."
raise ValueError(msg)
return np.array(vertices).reshape((-1, 3))
# 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)
def print_structure(alignment, indent=0): def print_structure(alignment, indent=0):
@@ -250,17 +105,544 @@ def print_structure(alignment, indent=0):
print_structure(child, indent + 2) 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__": if __name__ == "__main__":
import sys import sys
from matplotlib import pyplot as plt from matplotlib import pyplot as plt
s = ifcopenshell.express.parse("IFC4x3_RC3.exp")
ifcopenshell.register_schema(s)
f = ifcopenshell.open(sys.argv[1]) f = ifcopenshell.open(sys.argv[1])
print_structure(f.by_type("IfcAlignment")[0]) print_structure(f.by_type("IfcAlignment")[0])
al_hor = f.by_type("IfcAlignmentHorizontal")[0] al_hor_rep = f.by_type("IfcCompositeCurve")[0]
xy = create_shape({}, al_hor)
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") plt.savefig("horizontal_alignment.png")
@@ -1,186 +0,0 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# 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 <http://www.gnu.org/licenses/>.
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