See #1227. Implement ATPATH wall connections (going both ways).

This commit is contained in:
Dion Moult
2025-02-26 17:56:29 +11:00
parent 0f3de599a6
commit 34acd6de35
2 changed files with 270 additions and 59 deletions
+1 -1
View File
@@ -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
)
+269 -58
View File
@@ -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")