Skip to content

Commit 2bcfe93

Browse files
committed
See IfcOpenShell#3002. Spaces are now managed separately so that they don't conflict with projection polygon merging and raycasting.
1 parent 195ef74 commit 2bcfe93

4 files changed

Lines changed: 175 additions & 99 deletions

File tree

src/blenderbim/blenderbim/bim/data/assets/default.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; font-size: 4.13px; }
2323
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
2424
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
25+
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
2526
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
2627
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
2728
.IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; }

src/blenderbim/blenderbim/bim/module/drawing/decoration.py

Lines changed: 2 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1975,79 +1975,15 @@ def decorate(self, context, obj):
19751975
if verts is False:
19761976
return None, None
19771977

1978-
if not self.is_intersecting_camera(obj, context.scene.camera):
1978+
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
19791979
DecoratorData.cut_cache[element.id()] = (False, False)
19801980
return None, None
19811981

19821982
if verts is None:
1983-
verts, edges = self.bisect_mesh(obj, context.scene.camera)
1983+
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
19841984
DecoratorData.cut_cache[element.id()] = (verts, edges)
19851985
return verts, edges
19861986

1987-
def is_intersecting_camera(self, obj, camera):
1988-
# Based on separating axis theorem
1989-
plane_co = camera.matrix_world.translation
1990-
plane_no = camera.matrix_world.col[2].xyz
1991-
1992-
# Broadphase check using the bounding box
1993-
bounding_box_world_coords = [obj.matrix_world @ Vector(coord) for coord in obj.bound_box]
1994-
bounding_box_signed_distances = [plane_no.dot(v - plane_co) for v in bounding_box_world_coords]
1995-
1996-
pos_exists_bb = any(d > 0 for d in bounding_box_signed_distances)
1997-
neg_exists_bb = any(d < 0 for d in bounding_box_signed_distances)
1998-
1999-
if not (pos_exists_bb and neg_exists_bb):
2000-
return False
2001-
2002-
bm = bmesh.new()
2003-
bm.from_mesh(obj.data)
2004-
2005-
# Transform the vertices to world space
2006-
mesh_mat = obj.matrix_world
2007-
bm.transform(mesh_mat)
2008-
2009-
# Calculate the signed distances of all vertices from the plane
2010-
signed_distances = [plane_no.dot(v.co - plane_co) for v in bm.verts]
2011-
2012-
bm.free()
2013-
2014-
# Check for intersection
2015-
pos_exists = any(d > 0 for d in signed_distances)
2016-
neg_exists = any(d < 0 for d in signed_distances)
2017-
2018-
return pos_exists and neg_exists
2019-
2020-
def bisect_mesh(self, obj, camera):
2021-
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
2022-
plane_co = camera_matrix.translation
2023-
plane_no = camera_matrix.col[2].xyz
2024-
2025-
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
2026-
2027-
bm = bmesh.new()
2028-
bm.from_mesh(obj.data)
2029-
2030-
# Run the bisect operation
2031-
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
2032-
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
2033-
2034-
vert_map = {}
2035-
verts = []
2036-
edges = []
2037-
i = 0
2038-
for geom in results["geom_cut"]:
2039-
if isinstance(geom, bmesh.types.BMVert):
2040-
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
2041-
vert_map[geom.index] = i
2042-
i += 1
2043-
else:
2044-
# It seems as though edges always appear after verts
2045-
edges.append([vert_map[v.index] for v in geom.verts])
2046-
2047-
bm.free()
2048-
2049-
return verts, edges
2050-
20511987

20521988
class DecorationsHandler:
20531989
decorators_classes = [

src/blenderbim/blenderbim/bim/module/drawing/operator.py

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -485,10 +485,13 @@ def generate_linework(self, context):
485485
self.serialiser.finalize()
486486
results = self.svg_buffer.get_value()
487487

488+
root = etree.fromstring(results)
489+
self.move_projection_to_bottom(root)
490+
self.merge_linework_and_add_metadata(root)
491+
488492
if self.camera.data.BIMCameraProperties.calculate_shapely_surfaces:
489493
# shapely variant
490-
root = etree.fromstring(results)
491-
group = root.findall('.//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}name]')[0]
494+
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
492495
nm = group.attrib["{http://www.ifcopenshell.org/ns}name"]
493496
m4 = np.array(json.loads(group.attrib["{http://www.ifcopenshell.org/ns}plane"]))
494497
m3 = np.array(json.loads(group.attrib["{http://www.ifcopenshell.org/ns}matrix3"]))
@@ -503,7 +506,7 @@ def generate_linework(self, context):
503506

504507
for projection in projections:
505508
boundary_lines = []
506-
for path in projection.findall('./{http://www.w3.org/2000/svg}path'):
509+
for path in projection.findall("./{http://www.w3.org/2000/svg}path"):
507510
start, end = [co[1:].split(",") for co in path.attrib["d"].split()]
508511
boundary_lines.append(shapely.LineString([start, end]))
509512
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
@@ -517,23 +520,39 @@ def generate_linework(self, context):
517520
internal_point = centroid if polygon.contains(centroid) else polygon.representative_point()
518521
if internal_point:
519522
internal_point = [internal_point.x, internal_point.y]
520-
a, b = self.drawing_to_model_co(m44, m4, internal_point, 0.0), self.drawing_to_model_co(m44, m4, internal_point, -100.0)
523+
a, b = self.drawing_to_model_co(m44, m4, internal_point, 0.0), self.drawing_to_model_co(
524+
m44, m4, internal_point, -100.0
525+
)
521526
inside_elements = [e for e in tree.select(self.pythonize(a)) if not e.is_a("IfcAnnotation")]
522527
if not inside_elements:
523-
elements = [e for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a)) if not e.instance.is_a("IfcAnnotation")]
528+
elements = [
529+
e
530+
for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a))
531+
if not e.instance.is_a("IfcAnnotation")
532+
]
524533
if elements:
525-
path = etree.SubElement(group, "path")
526-
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
534+
path = etree.Element("path")
535+
d = (
536+
"M"
537+
+ " L".join(
538+
[",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]
539+
)
540+
+ " Z"
541+
)
527542
for interior in polygon.interiors:
528-
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
543+
d += (
544+
" M"
545+
+ " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]])
546+
+ " Z"
547+
)
529548
path.attrib["d"] = d
530549
classes = self.get_svg_classes(ifc.by_id(elements[0].instance.id()))
531550
classes.append("surface")
532551
path.set("class", " ".join(list(classes)))
533-
534-
results = etree.tostring(root)
552+
group.insert(0, path)
535553

536554
if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces:
555+
results = etree.tostring(root).decode("utf8")
537556
svg_data_1 = results
538557
from xml.dom.minidom import parseString
539558

@@ -624,7 +643,11 @@ def yield_groups(n):
624643
if iteration != num_passes:
625644
semantics[pi] = (inside_elements[0], -1)
626645
else:
627-
elements = [e for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a)) if not e.instance.is_a("IfcAnnotation")]
646+
elements = [
647+
e
648+
for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a))
649+
if not e.instance.is_a("IfcAnnotation")
650+
]
628651

629652
if elements:
630653
classes = self.get_svg_classes(ifc.by_id(elements[0].instance.id()))
@@ -682,16 +705,56 @@ def format(x):
682705
# This generally shouldn't happen
683706
g1.appendChild(g2)
684707

685-
data = dom1.toxml()
686-
data = data.encode("ascii", "xmlcharrefreplace")
687-
688-
results = data
689-
with profile("Post processing"):
708+
results = dom1.toxml()
709+
results = results.encode("ascii", "xmlcharrefreplace")
690710
root = etree.fromstring(results)
691-
self.move_projection_to_bottom(root)
692-
self.merge_linework_and_add_metadata(root)
693-
with open(svg_path, "wb") as svg:
694-
svg.write(etree.tostring(root))
711+
712+
# Spaces are handled as a special case, since they are often overlayed
713+
# in addition to elements. For example, a space should not obscure
714+
# other elements in projection. Spaces should also not override cut
715+
# elements but instead be drawn in addition to cut elements.
716+
spaces = tool.Drawing.get_drawing_spaces(self.camera_element)
717+
718+
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
719+
720+
self.svg_writer.calculate_scale()
721+
x_offset = self.svg_writer.raw_width / 2
722+
y_offset = self.svg_writer.raw_height / 2
723+
724+
for space in spaces:
725+
obj = tool.Ifc.get_object(space)
726+
if not obj or not tool.Drawing.is_intersecting_camera(obj, self.camera):
727+
continue
728+
verts, edges = tool.Drawing.bisect_mesh(obj, self.camera)
729+
verts = [self.svg_writer.project_point_onto_camera(Vector(v)) for v in verts]
730+
line_strings = [
731+
shapely.LineString(
732+
[
733+
(
734+
(x_offset + verts[e[0]][0]) * self.svg_writer.svg_scale,
735+
(y_offset - verts[e[0]][1]) * self.svg_writer.svg_scale,
736+
),
737+
(
738+
(x_offset + verts[e[1]][0]) * self.svg_writer.svg_scale,
739+
(y_offset - verts[e[1]][1]) * self.svg_writer.svg_scale,
740+
),
741+
]
742+
)
743+
for e in edges
744+
]
745+
closed_polygons = shapely.polygonize(line_strings)
746+
for polygon in closed_polygons.geoms:
747+
classes = self.get_svg_classes(space)
748+
path = etree.Element("path")
749+
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
750+
for interior in polygon.interiors:
751+
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
752+
path.attrib["d"] = d
753+
path.set("class", " ".join(list(classes)))
754+
group.append(path)
755+
756+
with open(svg_path, "wb") as svg:
757+
svg.write(etree.tostring(root))
695758

696759
return svg_path
697760

@@ -748,13 +811,12 @@ def get_svg_classes(self, element):
748811
value = ifcopenshell.util.selector.get_element_value(element, key)
749812
if value:
750813
classes.append(
751-
tool.Drawing.canonicalise_class_name(key)
752-
+ "-"
753-
+ tool.Drawing.canonicalise_class_name(str(value))
814+
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
754815
)
755816
return classes
756817

757818
def merge_linework_and_add_metadata(self, root):
819+
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
758820
joined_paths = {}
759821

760822
ifc = tool.Ifc.get()
@@ -796,16 +858,15 @@ def merge_linework_and_add_metadata(self, root):
796858
elif type(merged_polygons) == shapely.Polygon:
797859
merged_polygons = [merged_polygons]
798860

799-
root_g = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
800861
for polygon in merged_polygons:
801-
g = etree.SubElement(root, "g")
862+
g = etree.Element("g")
802863
path = etree.SubElement(g, "path")
803864
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
804865
for interior in polygon.interiors:
805866
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
806867
path.attrib["d"] = d
807868
g.set("class", " ".join(list(classes)))
808-
root_g.append(g)
869+
group.append(g)
809870

810871
def drawing_to_model_co(self, m44, m4, xy, z=0.0):
811872
xyzw = m44 @ np.array(xy + [z, 1.0])
@@ -816,14 +877,12 @@ def pythonize(self, arr):
816877
return tuple(map(float, arr))
817878

818879
def move_projection_to_bottom(self, root):
819-
# https://stackoverflow.com/questions/36018627/sorting-child-elements-with-lxml-based-on-attribute-value
880+
# IfcConvert puts the projection afterwards which is not correct since
881+
# projection should be drawn underneath the cut.
820882
group = root.find("{http://www.w3.org/2000/svg}g")
821-
if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces:
822-
# SVGFill already places the projection in the right spot.
823-
return
824-
# group[:] = sorted(group, key=lambda e : "projection" in e.get("class"))
825-
if group is not None:
826-
group[:] = reversed(group)
883+
projection = group.find("{http://www.w3.org/2000/svg}g[@class='projection']")
884+
projection.getparent().remove(projection)
885+
group.insert(0, projection)
827886

828887
def generate_annotation(self, context):
829888
if not self.cprops.has_annotation:

src/blenderbim/blenderbim/tool/drawing.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,7 @@ def get_drawing_elements(cls, drawing):
13421342
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
13431343
else:
13441344
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
1345+
elements = {e for e in elements if e.is_a() != "IfcSpace"}
13451346
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
13461347
elements.update(annotations)
13471348

@@ -1351,6 +1352,19 @@ def get_drawing_elements(cls, drawing):
13511352
elements -= set(ifc_file.by_type("IfcOpeningElement"))
13521353
return elements
13531354

1355+
@classmethod
1356+
def get_drawing_spaces(cls, drawing):
1357+
ifc_file = tool.Ifc.get()
1358+
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
1359+
include = pset.get("Include", None)
1360+
elements = set(ifc_file.by_type("IfcSpace"))
1361+
if include:
1362+
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
1363+
exclude = pset.get("Exclude", None)
1364+
if exclude:
1365+
elements -= set(ifcopenshell.util.selector.Selector.parse(ifc_file, exclude, elements=elements))
1366+
return elements
1367+
13541368
@classmethod
13551369
def get_annotation_element(cls, element):
13561370
for rel in element.HasAssignments:
@@ -1464,3 +1478,69 @@ def activate_drawing(cls, camera):
14641478
should_sync_changes_first=True,
14651479
)
14661480
break
1481+
1482+
@classmethod
1483+
def is_intersecting_camera(cls, obj, camera):
1484+
# Based on separating axis theorem
1485+
plane_co = camera.matrix_world.translation
1486+
plane_no = camera.matrix_world.col[2].xyz
1487+
1488+
# Broadphase check using the bounding box
1489+
bounding_box_world_coords = [obj.matrix_world @ Vector(coord) for coord in obj.bound_box]
1490+
bounding_box_signed_distances = [plane_no.dot(v - plane_co) for v in bounding_box_world_coords]
1491+
1492+
pos_exists_bb = any(d > 0 for d in bounding_box_signed_distances)
1493+
neg_exists_bb = any(d < 0 for d in bounding_box_signed_distances)
1494+
1495+
if not (pos_exists_bb and neg_exists_bb):
1496+
return False
1497+
1498+
bm = bmesh.new()
1499+
bm.from_mesh(obj.data)
1500+
1501+
# Transform the vertices to world space
1502+
mesh_mat = obj.matrix_world
1503+
bm.transform(mesh_mat)
1504+
1505+
# Calculate the signed distances of all vertices from the plane
1506+
signed_distances = [plane_no.dot(v.co - plane_co) for v in bm.verts]
1507+
1508+
bm.free()
1509+
1510+
# Check for intersection
1511+
pos_exists = any(d > 0 for d in signed_distances)
1512+
neg_exists = any(d < 0 for d in signed_distances)
1513+
1514+
return pos_exists and neg_exists
1515+
1516+
@classmethod
1517+
def bisect_mesh(cls, obj, camera):
1518+
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
1519+
plane_co = camera_matrix.translation
1520+
plane_no = camera_matrix.col[2].xyz
1521+
1522+
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
1523+
1524+
bm = bmesh.new()
1525+
bm.from_mesh(obj.data)
1526+
1527+
# Run the bisect operation
1528+
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
1529+
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
1530+
1531+
vert_map = {}
1532+
verts = []
1533+
edges = []
1534+
i = 0
1535+
for geom in results["geom_cut"]:
1536+
if isinstance(geom, bmesh.types.BMVert):
1537+
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
1538+
vert_map[geom.index] = i
1539+
i += 1
1540+
else:
1541+
# It seems as though edges always appear after verts
1542+
edges.append([vert_map[v.index] for v in geom.verts])
1543+
1544+
bm.free()
1545+
1546+
return verts, edges

0 commit comments

Comments
 (0)