diff --git a/src/ifcopenshell-python/ifcopenshell/alignment.py b/src/ifcopenshell-python/ifcopenshell/alignment.py index 06f51d26b0..9dc5c9bf3a 100644 --- a/src/ifcopenshell-python/ifcopenshell/alignment.py +++ b/src/ifcopenshell-python/ifcopenshell/alignment.py @@ -1,9 +1,11 @@ +import operator + from dataclasses import dataclass import numpy -import ifcopenshell -import ifcopenshell.geom +import ifcopenshell +import ifcopenshell.geom import ifcopenshell.express import ifcopenshell.transition_curve @@ -14,42 +16,90 @@ import ifcopenshell.transition_curve # does it make handling the variety of distance expressions and # interpolation harder? + @dataclass class line: start_point: numpy.ndarray direction_vector: numpy.ndarray - - def __call__(self, u): - return self.start_point + \ - self.direction_vector * u + + 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) - ]) - + 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 to a 2D point. Assumes a functor such as line or circle. """ + def inner(*args): v = func(*args) # homogenize - v = numpy.insert(v, v.shape[-1], 1, axis=-1) - return (matrix @ v)[0:2] + 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 +# 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): """ Looks up one of the implementation functions below in the global namespace @@ -60,14 +110,12 @@ def map_inst(inst): def impl_IfcLine(inst): return line( numpy.array(inst.Pnt.Coordinates), - numpy.array(inst.Dir.Orientation.DirectionRatios) * inst.Dir.Magnitude + numpy.array(inst.Dir.Orientation.DirectionRatios) * inst.Dir.Magnitude, ) def impl_IfcCircle(inst): - return place(map_inst(inst.Position), circle( - inst.Radius - )) + return place(map_inst(inst.Position), circle(inst.Radius)) def impl_IfcClothoid(inst): @@ -76,109 +124,120 @@ def impl_IfcClothoid(inst): # 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 = + # SegmentLength = + # IsStartRadiusCCW = + # IsEndRadiusCCW = + # TransitionCurveType = + # StartRadius = + # EndRadius = # ) - return lambda *args: numpy.array((0.,0.)) + 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] - + 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 """ yield from globals()[f"convert_{inst.is_a()}_{inst.PredefinedType}"](inst) - + def convert_IfcAlignmentHorizontalSegment_LINE(data): xy = numpy.array(data.StartPoint.Coordinates) yield xy - di = numpy.array([ - numpy.cos(data.StartDirection), - numpy.sin(data.StartDirection) - ]) + di = numpy.array([numpy.cos(data.StartDirection), numpy.sin(data.StartDirection)]) yield xy + di * data.SegmentLength - -# Two approaches, either DesignParameters or Representation + + +# 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 interpret_linear_element_geometry(settings, crv): - for segment in crv.Representation.Representations[0].Items[0].Segments: - - print(segment) - print(segment.ParentCurve) - print() - - func = place( - map_inst(segment.Placement), - map_inst(segment.ParentCurve) - ) - for u in numpy.linspace( - segment.SegmentStart[0], - segment.SegmentStart[0] + segment.SegmentLength[0], - num=32 - ): - yield func(u) - - +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): """ Debugging function to print alignment decomposition """ - print(" " * indent, str(alignment)[0:100]) - for rel in alignment.IsNestedBy: - for child in rel.RelatedObjects: - print_structure(child, indent+2) + print(" " * indent, str(alignment)[0:100]) + for rel in alignment.IsNestedBy: + for child in rel.RelatedObjects: + print_structure(child, indent + 2) 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]) @@ -186,6 +245,6 @@ if __name__ == "__main__": al_hor = f.by_type("IfcAlignmentHorizontal")[0] xy = create_shape({}, al_hor) - + plt.plot(xy.T[0], xy.T[1]) plt.savefig("horizontal_alignment.png")