diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 5d1192f420..c98f915134 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1091,6 +1091,8 @@ class Loader(bonsai.core.tool.Loader): def get_extrusion_vector(cls, wall): if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): for item in ifcopenshell.util.representation.resolve_representation(body).Items: + while item.is_a("IfcBooleanResult"): + item = item.FirstOperand if item.is_a("IfcExtrudedAreaSolid"): return Vector(item.ExtrudedDirection.DirectionRatios) return Vector([0.0, 0.0, 1.0]) diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index 0714053d52..b53ae6b21f 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -11,6 +11,9 @@ import ifcopenshell.api.geometry import ifcopenshell.util.shape_builder import ifcopenshell.util.element +# https://stackoverflow.com/a/9184560/9627415 +# Possible optimisation to linalg.norm? + # from ifcopenshell.util.shape_builder import VectorType, SequenceOfVectors from itertools import cycle from collections import namedtuple @@ -164,8 +167,8 @@ def test_wall(offset, p1, p2, p3, p4, a1=None, a2=None): ) Foo(f, body, axis).regenerate(wall_a, angle=a1) - Foo(f, body, axis).regenerate(wall_b) - Foo(f, body, axis).regenerate(wall_c) + Foo(f, body, axis).regenerate(wall_b, angle=a1) + Foo(f, body, axis).regenerate(wall_c, angle=a1) def create_type(name, layers): @@ -181,7 +184,7 @@ def create_type(name, layers): return wall_type -def test_atpath(offset): +def test_atpath(offset, angle=None): offset *= 1.5 wall_type_a = create_type("A", [(1, 0.05), (2, 0.1), (3, 0.05)]) wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="A123") @@ -213,7 +216,7 @@ def test_atpath(offset): relating_connection="ATEND", related_connection="ATPATH", ) - Foo(f, body, axis).regenerate(wall) + Foo(f, body, axis).regenerate(wall, angle=angle) create_branch("B", 1, 1, 1, 1, 1, -75) create_branch("C", 1, 2, 3, 2, 1, -75) @@ -227,7 +230,7 @@ def test_atpath(offset): create_branch("E", 4, 4, 4, 3.5, -1, 75) create_branch("F", 4, 2, 4, 4.5, -1, 75) - Foo(f, body, axis).regenerate(wall_a) + Foo(f, body, axis).regenerate(wall_a, angle=angle) PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") @@ -238,25 +241,32 @@ class Foo: self.file = file self.body = body self.axis = axis + self.is_angled = False def regenerate(self, wall, angle=None): print("-" * 100) print(wall) + self.fallback_angle = angle layers = self.get_layers(wall) if not layers: return reference = self.get_reference_line(wall) self.reference_p1, self.reference_p2 = reference - self.angle = angle or self.get_angle(wall) - axes = self.get_axes(wall, reference, layers, self.angle) + self.wall_vectors = self.get_wall_vectors(wall) + axes = self.get_axes(wall, reference, layers, self.wall_vectors["a"]) self.miny = axes[0][0][1] self.maxy = axes[-1][0][1] self.end_point = None self.start_points = [] + self.start_vector = np.array((0.0, 0.0, 1.0)) + self.start_offset = 0.0 + self.atpath_points = [] self.split_points = [] self.maxpath_points = [] self.minpath_points = [] self.end_points = [] + self.end_vector = np.array((0.0, 0.0, 1.0)) + self.end_offset = 0.0 for rel in wall.ConnectedTo: if rel.is_a("IfcRelConnectsPathElements"): wall2 = rel.RelatedElement @@ -297,56 +307,133 @@ class Foo: self.end_points.reverse() builder = ifcopenshell.util.shape_builder.ShapeBuilder(wall.file) - # A wall footprint may be multiple profiles if the wall is split into two due to an ATPATH connection - profiles = [] - split_points = sorted(self.split_points, key=lambda x: x[0][0]) # Sort islands in the +X direction - split_points.insert(0, self.start_points) - split_points.append(self.end_points) - split_points = iter(split_points) - while True: - # Draw each profile as clockwise starting from (minx, miny) - start_split = next(split_points, None) - if not start_split: - break - end_split = next(split_points, None) - if not end_split: - break - maxy_minx = start_split[-1][0] - maxy_maxx = end_split[-1][0] - miny_minx = start_split[0][0] - miny_maxx = end_split[0][0] - # Do more defensive checks here - points = start_split - remaining_path_points = [] - for maxpath_points in self.maxpath_points: - if maxpath_points[0][0] > maxy_minx and maxpath_points[-1][0] < maxy_maxx: - points.extend(maxpath_points) - else: - remaining_path_points.append(maxpath_points) - self.maxpath_points = remaining_path_points - points.extend(end_split[::-1]) - remaining_path_points = [] - for minpath_points in self.minpath_points: - if minpath_points[0][0] < miny_maxx and minpath_points[-1][0] > miny_minx: - points.extend(minpath_points) - else: - remaining_path_points.append(minpath_points) - self.minpath_points = remaining_path_points + if self.is_angled: + start_points = [p.copy() for p in self.start_points] + end_points = [p.copy() for p in self.end_points] + if self.end_offset > 0: + for point in end_points: + point[0] += self.end_offset + if self.start_offset < 0: + for point in start_points: + point[0] += self.start_offset + points = [] + points.extend(start_points) + end_points.reverse() + points.extend(end_points) + item = builder.extrude( + builder.polyline(points, closed=True), + magnitude=self.wall_vectors["d"], + extrusion_vector=self.wall_vectors["z"], + ) - profiles.append(builder.profile(builder.polyline(points, closed=True))) + operands = [] + if not np.allclose(self.start_vector, np.array((0.0, 0.0, 1.0))): + points = self.start_points.copy() + while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]): + points.pop(0) + while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]): + points.pop() + newx = min([p[0] for p in points]) - abs(self.start_offset) + p1 = points[-1].copy() + p1[0] = newx + p2 = p1.copy() + p2[1] = points[0][1] + points.extend((p1, p2)) + magnitude = np.linalg.norm(self.start_vector * (self.wall_vectors["h"] / self.start_vector[2])) + operands.append( + builder.extrude( + builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.start_vector + ) + ) - for points in self.maxpath_points + self.minpath_points: - profiles.append(builder.profile(builder.polyline(points, closed=True))) + if not np.allclose(self.end_vector, np.array((0.0, 0.0, 1.0))): + points = self.end_points.copy() + while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]): + points.pop(0) + while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]): + points.pop() - if len(profiles) > 1: - profile = wall.file.createIfcCompositeProfileDef("AREA", Profiles=profiles) + newx = max([p[0] for p in points]) + abs(self.end_offset) + p1 = points[-1].copy() + p1[0] = newx + p2 = p1.copy() + p2[1] = points[0][1] + points.extend((p1, p2)) + magnitude = np.linalg.norm(self.end_vector * (self.wall_vectors["h"] / self.end_vector[2])) + operands.append( + builder.extrude( + builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.end_vector + ) + ) + + for atpath_vector, points in self.atpath_points: + if len(points) <= 2: + continue + magnitude = np.linalg.norm(atpath_vector * (self.wall_vectors["h"] / atpath_vector[2])) + operands.append( + builder.extrude( + builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=atpath_vector + ) + ) + + if operands: + item = ifcopenshell.api.geometry.add_boolean(wall.file, first_item=item, second_items=operands)[-1] else: - profile = profiles[0] + # A wall footprint may be multiple profiles if the wall is split into two due to an ATPATH connection + profiles = [] + split_points = sorted(self.split_points, key=lambda x: x[0][0]) # Sort islands in the +X direction + start_points = [p.copy() for p in self.start_points] + end_points = [p.copy() for p in self.end_points] + split_points.insert(0, start_points) + split_points.append(end_points) + split_points = iter(split_points) - item = builder.extrude( - profile, magnitude=1.0, extrusion_vector=np.array([0.0, sin(self.angle), cos(self.angle)]) - ) + while True: + # Draw each profile as clockwise starting from (minx, miny) + start_split = next(split_points, None) + if not start_split: + break + end_split = next(split_points, None) + if not end_split: + break + maxy_minx = start_split[-1][0] + maxy_maxx = end_split[-1][0] + miny_minx = start_split[0][0] + miny_maxx = end_split[0][0] + # Do more defensive checks here + points = start_split + + remaining_path_points = [] + for maxpath_points in self.maxpath_points: + if maxpath_points[0][0] > maxy_minx and maxpath_points[-1][0] < maxy_maxx: + print("adding maxpath points", maxpath_points) + points.extend(maxpath_points) + else: + remaining_path_points.append(maxpath_points) + self.maxpath_points = remaining_path_points + + points.extend(end_split[::-1]) + + remaining_path_points = [] + for minpath_points in self.minpath_points: + if minpath_points[0][0] < miny_maxx and minpath_points[-1][0] > miny_minx: + points.extend(minpath_points) + else: + remaining_path_points.append(minpath_points) + self.minpath_points = remaining_path_points + + profiles.append(builder.profile(builder.polyline(points, closed=True))) + + for points in self.maxpath_points + self.minpath_points: + profiles.append(builder.profile(builder.polyline(points, closed=True))) + + if len(profiles) > 1: + profile = wall.file.createIfcCompositeProfileDef("AREA", Profiles=profiles) + else: + profile = profiles[0] + + item = builder.extrude(profile, magnitude=self.wall_vectors["d"], extrusion_vector=self.wall_vectors["z"]) rep = builder.get_representation(self.body, items=[item]) if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body): ifcopenshell.util.element.replace_element(old_rep, rep) @@ -371,8 +458,9 @@ class Foo: # axes = self.get_axes(wall2, layers2) reference1 = self.get_reference_line(wall1) reference2 = self.get_reference_line(wall2) - axes1 = self.get_axes(wall1, reference1, layers1, self.angle) - axes2 = self.get_axes(wall2, reference2, layers2, self.get_angle(wall2)) + wall_vectors2 = self.get_wall_vectors(wall2) + axes1 = self.get_axes(wall1, reference1, layers1, self.wall_vectors["a"]) + axes2 = self.get_axes(wall2, reference2, layers2, wall_vectors2["a"]) matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)) matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) print(axes1) @@ -384,6 +472,8 @@ class Foo: axis[1] = (matrix1i @ matrix2 @ np.concatenate((axis[1], (0, 1))))[:2] reference2[0] = (matrix1i @ matrix2 @ np.concatenate((reference2[0], (0, 1))))[:2] reference2[1] = (matrix1i @ matrix2 @ np.concatenate((reference2[1], (0, 1))))[:2] + wall_vectors2["z"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["z"], 0.0))[:3] + wall_vectors2["y"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["y"], 0.0))[:3] # Sort axes from interior to exterior if connection1 == "ATEND": @@ -446,6 +536,8 @@ class Foo: # Categorise our points into a segment that either splits or cuts the wall split_ys = {first_y, last_y} segment = [] + atpath_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.atpath_points.append((atpath_vector, points)) for point in points: segment.append(point) if len(segment) == 1: # Not enough points to categorise the segment @@ -488,9 +580,13 @@ class Foo: if connection1 == "ATSTART": self.start_points = points + self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0] self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) elif connection1 == "ATEND": self.end_points = points + self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0] self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) else: last_y = axes1[-1][0][1] @@ -532,9 +628,13 @@ class Foo: if connection1 == "ATSTART": self.start_points = points + self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0] self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) elif connection1 == "ATEND": self.end_points = points + self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0] self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) def get_layers(self, wall) -> list: @@ -581,14 +681,38 @@ class Foo: return [np.array(points[1]), np.array(points[0])] return [np.array((0.0, 0.0)), np.array((1.0, 0.0))] - def get_angle(self, wall): + def get_wall_vectors(self, wall): if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): for item in ifcopenshell.util.representation.resolve_representation(body).Items: + while item.is_a("IfcBooleanResult"): + item = item.FirstOperand if item.is_a("IfcExtrudedAreaSolid"): - return ifcopenshell.util.shape_builder.np_angle_signed( - np.array((0.0, 1.0)), np.array(item.ExtrudedDirection.DirectionRatios[1:]) - ) - return 0.0 + z = np.array(item.ExtrudedDirection.DirectionRatios) + z /= np.linalg.norm(z) + y = np.cross(z, np.array((1.0, 0.0, 0.0))) + d = item.Depth + h = (z * d)[2] + a = ifcopenshell.util.shape_builder.np_angle_signed(np.array((0.0, 1.0)), z[1:]) + if not ifcopenshell.util.shape_builder.is_x(a, 0): + self.is_angled = True + return {"z": z, "y": y, "a": a, "d": d, "h": h} + elif self.fallback_angle: + a = self.fallback_angle + z = np.array([0.0, sin(a), cos(a)]) + y = np.cross(z, np.array((1.0, 0.0, 0.0))) + h = 1.0 # unit scale + d = np.linalg.norm(z * (h / z[2])) + if not ifcopenshell.util.shape_builder.is_x(a, 0): + self.is_angled = True + return {"z": z, "y": y, "a": a, "d": d, "h": h} + # unit scale + return {"z": np.array((0.0, 0.0, 1.0)), "y": np.array((0.0, 1.0, 0.0)), "a": 0.0, "d": 1.0, "h": 1.0} + + def get_join_vector(self, y1, y2): + result = np.cross(y1, y2) + if result[2] < 0: + return result * -1 + return result def get_axes(self, wall, reference, layers: list[PrioritisedLayer], angle: float): axes = [[p.copy() for p in reference]] @@ -605,13 +729,13 @@ class Foo: return axes -test_wall(0, 1, 1, 1, 1) -test_wall(1, 2, 1, 1, 2) -test_wall(2, 2, 1, 1, 1) -test_wall(3, 1, 2, 1, 1) -test_wall(4, 1, 2, 1, 2) -test_wall(5, 3, 1, 2, 4) -test_atpath(7) +test_wall(0, 1, 1, 1, 1, radians(10)) +test_wall(1, 2, 1, 1, 2, radians(10)) +test_wall(2, 2, 1, 1, 1, radians(10)) +test_wall(3, 1, 2, 1, 1, radians(10)) +test_wall(4, 1, 2, 1, 2, radians(10)) +test_wall(5, 3, 1, 2, 4, radians(10)) +test_atpath(7, radians(10)) f.write("/home/dion/wall.ifc")