diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 7a5576a1e2..cd7e0471ec 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1053,7 +1053,7 @@ class Loader(bonsai.core.tool.Loader): styles[style] = i for layer in layer_set.MaterialLayers[:-1]: prev_co = co.copy() - co.y = layer.LayerThickness * cls.unit_scale * sense_factor + co.y += layer.LayerThickness * cls.unit_scale * sense_factor bisect_geom = bmesh.ops.bisect_plane( bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no ) diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index 18c6267d39..ad382b101a 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -12,11 +12,12 @@ import ifcopenshell.util.shape_builder import ifcopenshell.util.element # from ifcopenshell.util.shape_builder import VectorType, SequenceOfVectors +from itertools import cycle from collections import namedtuple f = ifcopenshell.api.project.create_file() -ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject") +project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject") meters = ifcopenshell.api.unit.add_si_unit(f) ifcopenshell.api.unit.assign_unit(f, units=[meters]) @@ -31,6 +32,7 @@ body = ifcopenshell.api.context.add_context( material1 = ifcopenshell.api.material.add_material(f, name="material1", category="material1") material2 = ifcopenshell.api.material.add_material(f, name="material2", category="material2") site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite") +ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project) builder = ifcopenshell.util.shape_builder.ShapeBuilder(f) style = ifcopenshell.api.style.add_style(f) @@ -68,23 +70,32 @@ def test_wall(offset, p1, p2, p3, p4): ifcopenshell.api.material.assign_material(f, products=[wall_type_a], material=set_a) ifcopenshell.api.material.assign_material(f, products=[wall_type_b], material=set_b) - for i, rotation in enumerate((-90, -60, -120, 90, 60, 120)): + for i, rotation in enumerate((-90, -75, -105, 90, 75, 105)): for i2, connection in enumerate(("ATEND", "ATSTART", "MIX")): + # if rotation != -90: + # continue + # if connection != "ATEND": + # continue wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"A{p1}{p2}") wall_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"B{p3}{p4}") + wall_c = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"C{p3}{p4}") - ifcopenshell.api.spatial.assign_container(f, products=[wall_a, wall_b], relating_structure=site) + ifcopenshell.api.spatial.assign_container(f, products=[wall_a, wall_b, wall_c], relating_structure=site) ifcopenshell.api.type.assign_type(f, related_objects=[wall_a], relating_type=wall_type_a) ifcopenshell.api.type.assign_type(f, related_objects=[wall_b], relating_type=wall_type_b) + ifcopenshell.api.type.assign_type(f, related_objects=[wall_c], relating_type=wall_type_b) axis_a = builder.polyline(((0.0, 0.0), (1.0, 0.0))) axis_b = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + axis_c = builder.polyline(((0.0, 0.0), (1.0, 0.0))) rep_a = builder.get_representation(axis, [axis_a]) rep_b = builder.get_representation(axis, [axis_b]) + rep_c = builder.get_representation(axis, [axis_c]) ifcopenshell.api.geometry.assign_representation(f, product=wall_a, representation=rep_a) ifcopenshell.api.geometry.assign_representation(f, product=wall_b, representation=rep_b) + ifcopenshell.api.geometry.assign_representation(f, product=wall_c, representation=rep_c) x_offset = i * 2 x_offset += i2 * (2 * 6) @@ -95,8 +106,12 @@ def test_wall(offset, p1, p2, p3, p4): matrix_b = np.eye(4) matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0) + matrix_c = np.eye(4) + matrix_c = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_c + matrix_c[:, 3][0:3] = (0.5 + x_offset, 0.5 + offset, 0) ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_c, matrix=matrix_c) ifcopenshell.api.geometry.connect_path( f, @@ -105,6 +120,13 @@ def test_wall(offset, p1, p2, p3, p4): relating_connection="ATEND", related_connection="ATEND", ) + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall_c, + related_element=wall_a, + relating_connection="ATEND", + related_connection="ATPATH", + ) elif connection == "ATSTART": sign_offset = 0 if rotation < 0 else 1 matrix_a = np.eye(4) @@ -142,6 +164,69 @@ def test_wall(offset, p1, p2, p3, p4): Foo(f, body, axis).regenerate(wall_a) Foo(f, body, axis).regenerate(wall_b) + Foo(f, body, axis).regenerate(wall_c) + + +def create_type(name, layers): + wall_type = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name=name) + layer_set = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet") + materials = cycle((material1, material2)) + for layer in layers: + material = next(materials) + item = ifcopenshell.api.material.add_layer(f, layer_set=layer_set, material=material, name="structure") + item.Priority = layer[0] + item.LayerThickness = layer[1] + ifcopenshell.api.material.assign_material(f, products=[wall_type], material=layer_set) + return wall_type + + +def test_atpath(offset): + 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") + ifcopenshell.api.spatial.assign_container(f, products=[wall_a], relating_structure=site) + ifcopenshell.api.type.assign_type(f, related_objects=[wall_a], relating_type=wall_type_a) + axis_a = builder.polyline(((0.0, 0.0), (30.0, 0.0))) + rep_a = builder.get_representation(axis, [axis_a]) + ifcopenshell.api.geometry.assign_representation(f, product=wall_a, representation=rep_a) + matrix_a = np.eye(4) + matrix_a[:, 3][0:3] = (0, 0 + offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) + + def create_branch(name, p1, p2, p3, x, y, rotation): + wall_type = create_type(name, [(p1, 0.05), (p2, 0.1), (p3, 0.05)]) + wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"{name}{p1}{p2}{p3}") + ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=site) + ifcopenshell.api.type.assign_type(f, related_objects=[wall], relating_type=wall_type) + axis_a = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + rep_a = builder.get_representation(axis, [axis_a]) + ifcopenshell.api.geometry.assign_representation(f, product=wall, representation=rep_a) + matrix_a = np.eye(4) + matrix_a = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_a + matrix_a[:, 3][0:3] = (x, y + offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall, matrix=matrix_a) + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall, + related_element=wall_a, + relating_connection="ATEND", + related_connection="ATPATH", + ) + Foo(f, body, axis).regenerate(wall) + + create_branch("B", 1, 1, 1, 1, 1, -75) + create_branch("C", 1, 2, 3, 2, 1, -75) + create_branch("D", 1, 4, 2, 3, 1, -75) + create_branch("E", 4, 4, 4, 4, 1, -75) + create_branch("F", 4, 2, 4, 5, 1, -75) + + create_branch("B", 1, 1, 1, 0.5, -1, 75) + create_branch("C", 1, 2, 3, 1.5, -1, 75) + create_branch("D", 1, 4, 2, 2.5, -1, 75) + 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) PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") @@ -162,8 +247,13 @@ class Foo: reference = self.get_reference_line(wall) self.reference_p1, self.reference_p2 = reference axes = self.get_axes(wall, reference, layers) + self.miny = axes[0][0][1] + self.maxy = axes[-1][0][1] self.end_point = None self.start_points = [] + self.split_points = [] + self.maxpath_points = [] + self.minpath_points = [] self.end_points = [] for rel in wall.ConnectedTo: if rel.is_a("IfcRelConnectsPathElements"): @@ -183,9 +273,6 @@ class Foo: continue self.join(wall, wall2, layers1, layers2, rel.RelatedConnectionType, rel.RelatingConnectionType) - # for rel in wall.ConnectedFrom: - # if rel.is_a("IfcRelConnectsPathElements"): - # connection = rel.RelatedConnectionType if not self.start_points: minx = axes[0][0][0] self.start_points = [ @@ -202,18 +289,60 @@ class Foo: print(self.start_points) print(self.end_points) - points = [] - if self.start_points[0][1] < self.start_points[-1][1]: - points.extend((self.start_points)) - else: - points.extend(reversed(self.start_points)) - if self.end_points[0][1] > self.end_points[-1][1]: - points.extend((self.end_points)) - else: - points.extend(reversed(self.end_points)) + if self.start_points[0][1] > self.start_points[-1][1]: # Canonicalise to the +Y direction + self.start_points.reverse() + if self.end_points[0][1] > self.end_points[-1][1]: # Canonicalise to the +Y direction + self.end_points.reverse() builder = ifcopenshell.util.shape_builder.ShapeBuilder(wall.file) - item = builder.extrude(builder.polyline(points, closed=True), magnitude=1.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 + 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 + + 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=1.0) 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) @@ -230,6 +359,8 @@ class Foo: def join(self, wall1, wall2, layers1, layers2, connection1, connection2): if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED": return + if connection1 == "ATPATH" and connection2 == "ATPATH": + return print("joining", wall1, layers1, connection1) print("to", wall2, layers2, connection2) @@ -277,51 +408,130 @@ class Foo: print(axes1) print(axes2) # Checked + if connection1 == "ATPATH": + first_axis2 = axes2[0] + last_axis2 = axes2[-1] + first_y = axes1[0][0][1] + last_y = axes1[-1][0][1] + p0 = np.array((self.intersect_axis(*first_axis2, y=first_y), first_y)) + pN = np.array((self.intersect_axis(*last_axis2, y=first_y), first_y)) - last_y = axes1[-1][0][1] - ys = iter([a[0][1] for a in axes1]) - print("ys are", [a[0][1] for a in axes1]) - - last_axis2 = axes2[-1] - axes2 = iter(axes2) - axis2 = next(axes2) - y = next(ys) - x = self.intersect_axis(*axis2, y=y) - points = [np.array((x, y))] - print("first point", points) - - layers1 = iter(layers1) - layers2 = iter(layers2) - layer1 = next(layers1, None) - layer2 = next(layers2, None) - - while layer1 and layer2: - print("considering", layer1, layer2) - if layer1.priority > layer2.priority: + # Generate CurveOnRelating/RelatedElement + points = [p0] + axes2 = iter(axes2) + axis2 = next(axes2) + for layer2 in layers2: + ys = iter([a[0][1] for a in axes1]) + y = next(ys) + for layer1 in layers1: + if layer2.priority <= layer1.priority: + break + y = next(ys) + p1 = np.array((self.intersect_axis(*axis2, y=y), y)) axis2 = next(axes2) - x = self.intersect_axis(*axis2, y=y) - layer2 = next(layers2, None) - elif layer2.priority > layer1.priority: - y = next(ys) - x = self.intersect_axis(*axis2, y=y) - layer1 = next(layers1, None) - else: - y = next(ys) - x = self.intersect_axis(*next(axes2), y=y) - layer1 = next(layers1, None) - layer2 = next(layers2, None) - points.append(np.array((x, y))) + p2 = np.array((self.intersect_axis(*axis2, y=y), y)) + if points and np.allclose(points[-1], p1): + points[-1] = p2 # Just slide along previous point + else: + points.extend((p1, p2)) - print("points", points) - if points[-1][1] != last_y: - points.append(np.array((self.intersect_axis(*last_axis2, y=last_y), last_y))) - print("fpoints", points) - if connection1 == "ATSTART": - self.start_points = points - self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) - elif connection1 == "ATEND": - self.end_points = points - self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + # The curve must end at pN + if not np.allclose(points[-1], pN): + points.append(pN) + + # Categorise our points into a segment that either splits or cuts the wall + split_ys = {first_y, last_y} + segment = [] + for point in points: + segment.append(point) + if len(segment) == 1: # Not enough points to categorise the segment + continue + elif {segment[0][1], segment[-1][1]} == split_ys: # This segment splits the wall + if segment[0][1] > segment[-1][1]: # Go in the +Y direction + segment.reverse() + self.split_points.append(segment) + segment = [] + elif segment[0][1] == segment[-1][1]: # This segment cuts some of the wall + if segment[0][1] == self.maxy: # Go in the +X direction + if segment[0][0] > segment[-1][0]: + segment.reverse() + self.maxpath_points.append(segment) + elif segment[0][1] == self.miny: # Go in the -X direction + if segment[-1][0] > segment[0][0]: + segment.reverse() + self.minpath_points.append(segment) + segment = [] + elif connection2 == "ATPATH": + points = [] + ys = iter([a[0][1] for a in axes1]) + y = next(ys) + for layer1 in layers1: + axes2_iter = iter(axes2) + axis2 = next(axes2_iter) + for layer2 in layers2: + if layer1.priority <= layer2.priority: + break + axis2 = next(axes2_iter) + x = self.intersect_axis(*axis2, y=y) + p1 = np.array((x, y)) + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + p2 = np.array((x, y)) + if points and np.allclose(points[-1], p1): + points.append(p2) + else: + points.extend((p1, p2)) + + if connection1 == "ATSTART": + self.start_points = points + self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + elif connection1 == "ATEND": + self.end_points = points + self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + else: + last_y = axes1[-1][0][1] + ys = iter([a[0][1] for a in axes1]) + + last_axis2 = axes2[-1] + axes2 = iter(axes2) + axis2 = next(axes2) + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + points = [np.array((x, y))] + + layers1 = iter(layers1) + layers2 = iter(layers2) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + + # This creates "mitering" behaviour which is an ambiguity by bSI. + while layer1 and layer2: + print("considering", layer1, layer2) + if layer1.priority > layer2.priority: + axis2 = next(axes2) + x = self.intersect_axis(*axis2, y=y) + layer2 = next(layers2, None) + elif layer2.priority > layer1.priority: + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + layer1 = next(layers1, None) + else: + y = next(ys) + x = self.intersect_axis(*next(axes2), y=y) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + points.append(np.array((x, y))) + + print("points", points) + if points[-1][1] != last_y: + points.append(np.array((self.intersect_axis(*last_axis2, y=last_y), last_y))) + + if connection1 == "ATSTART": + self.start_points = points + self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + elif connection1 == "ATEND": + self.end_points = points + self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) def get_layers(self, wall) -> list: material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True) @@ -387,6 +597,7 @@ 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) f.write("/home/dion/wall.ifc")