mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 10:06:47 +00:00
See #3002. Experimental SVGFill approach to polygon merging.
This commit is contained in:
@@ -485,7 +485,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.serialiser.finalize()
|
||||
results = self.svg_buffer.get_value()
|
||||
|
||||
if self.camera.data.BIMCameraProperties.calculate_surfaces:
|
||||
if self.camera.data.BIMCameraProperties.calculate_shapely_surfaces:
|
||||
# shapely variant
|
||||
root = etree.fromstring(results)
|
||||
group = root.findall('.//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}name]')[0]
|
||||
@@ -532,6 +532,160 @@ class CreateDrawing(bpy.types.Operator):
|
||||
path.set("class", " ".join(list(classes)))
|
||||
|
||||
results = etree.tostring(root)
|
||||
|
||||
if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces:
|
||||
svg_data_1 = results
|
||||
from xml.dom.minidom import parseString
|
||||
|
||||
def yield_groups(n):
|
||||
if n.nodeType == n.ELEMENT_NODE and n.tagName == "g":
|
||||
yield n
|
||||
for c in n.childNodes:
|
||||
yield from yield_groups(c)
|
||||
|
||||
dom1 = parseString(svg_data_1)
|
||||
svg1 = dom1.childNodes[0]
|
||||
groups1 = [g for g in yield_groups(svg1) if g.getAttribute("class") == "projection"]
|
||||
|
||||
ls_groups = ifcopenshell.ifcopenshell_wrapper.svg_to_line_segments(results, "projection")
|
||||
|
||||
for i, (ls, g1) in enumerate(zip(ls_groups, groups1)):
|
||||
projection, g1 = g1, g1.parentNode
|
||||
|
||||
svgfill_context = ifcopenshell.ifcopenshell_wrapper.context(
|
||||
ifcopenshell.ifcopenshell_wrapper.EXACT_CONSTRUCTIONS, 1.0e-3
|
||||
)
|
||||
|
||||
# EXACT_CONSTRUCTIONS is significantly faster than FILTERED_CARTESIAN_QUOTIENT
|
||||
# remove duplicates (without tolerance)
|
||||
ls = [l for l in map(tuple, set(map(frozenset, ls))) if len(l) == 2 and l[0] != l[1]]
|
||||
svgfill_context.add(ls)
|
||||
|
||||
num_passes = 0
|
||||
|
||||
for iteration in range(num_passes + 1):
|
||||
# initialize empty group, note that in the current approach only one
|
||||
# group is stored
|
||||
ps = ifcopenshell.ifcopenshell_wrapper.svg_groups_of_polygons()
|
||||
if iteration != 0 or svgfill_context.build():
|
||||
svgfill_context.write(ps)
|
||||
|
||||
if iteration != num_passes:
|
||||
pairs = svgfill_context.get_face_pairs()
|
||||
semantics = [None] * (max(pairs) + 1)
|
||||
|
||||
# Reserialize cells into an SVG string
|
||||
svg_data_2 = ifcopenshell.ifcopenshell_wrapper.polygons_to_svg(ps, True)
|
||||
|
||||
# We parse both SVG files to create on document with the combination of sections from
|
||||
# the output directly from the serializer and the cells found from the hidden line
|
||||
# rendering
|
||||
dom2 = parseString(svg_data_2)
|
||||
svg2 = dom2.childNodes[0]
|
||||
# file 2 only has the groups we are interested in.
|
||||
# in fact in the approach, it's only a single group
|
||||
|
||||
g2 = list(yield_groups(svg2))[0]
|
||||
|
||||
# These are attributes on the original group that we can use to reconstruct
|
||||
# a 4x4 matrix of the projection used in the SVG generation process
|
||||
nm = g1.getAttribute("ifc:name")
|
||||
m4 = np.array(json.loads(g1.getAttribute("ifc:plane")))
|
||||
m3 = np.array(json.loads(g1.getAttribute("ifc:matrix3")))
|
||||
m44 = np.eye(4)
|
||||
m44[0][0:2] = m3[0][0:2]
|
||||
m44[1][0:2] = m3[1][0:2]
|
||||
m44[0][3] = m3[0][2]
|
||||
m44[1][3] = m3[1][2]
|
||||
m44 = np.linalg.inv(m44)
|
||||
|
||||
# Loop over the cell paths
|
||||
for pi, p in enumerate(g2.getElementsByTagName("path")):
|
||||
|
||||
d = p.getAttribute("d")
|
||||
coords = [co[1:].split(",") for co in d.split() if co[1:]]
|
||||
polygon = shapely.Polygon(coords)
|
||||
# 1mm2 polygons aren't worth styling in paperspace. Raycasting is expensive!
|
||||
if polygon.area < 1:
|
||||
continue
|
||||
# point inside is an attribute that comes from line_segments_to_polygons() polygons_to_svg?
|
||||
# it is an arbitrary point guaranteed to be inside the polygon and outside
|
||||
# of any potential inner bounds. We can use this to construct a ray to find
|
||||
# the face of the IFC element that the cell belongs to.
|
||||
assert p.hasAttribute("ifc:pointInside")
|
||||
|
||||
xy = list(map(float, p.getAttribute("ifc:pointInside").split(",")))
|
||||
|
||||
a, b = self.drawing_to_model_co(m44, m4, xy, 0.0), self.drawing_to_model_co(m44, m4, xy, -100.0)
|
||||
|
||||
inside_elements = [e for e in tree.select(self.pythonize(a)) if not e.is_a("IfcAnnotation")]
|
||||
if inside_elements:
|
||||
elements = None
|
||||
if iteration != num_passes:
|
||||
semantics[pi] = (inside_elements[0], -1)
|
||||
else:
|
||||
elements = [e for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a)) if not e.instance.is_a("IfcAnnotation")]
|
||||
|
||||
if elements:
|
||||
classes = self.get_svg_classes(ifc.by_id(elements[0].instance.id()))
|
||||
classes.append("projection")
|
||||
|
||||
if iteration != num_passes:
|
||||
semantics[pi] = elements[0]
|
||||
else:
|
||||
classes = ["projection"]
|
||||
|
||||
p.setAttribute("style", "")
|
||||
p.setAttribute("class", " ".join(classes))
|
||||
|
||||
if iteration != num_passes:
|
||||
to_remove = []
|
||||
|
||||
for he_idx in range(0, len(pairs), 2):
|
||||
# @todo instead of ray_distance, better do (x.point - y.point).dot(x.normal)
|
||||
# to see if they're coplanar, because ray-distance will be different in case
|
||||
# of element surfaces non-orthogonal to the view direction
|
||||
|
||||
def format(x):
|
||||
if x is None:
|
||||
return None
|
||||
elif isinstance(x, tuple):
|
||||
# found to be inside element using tree.select() no face or style info
|
||||
return x
|
||||
else:
|
||||
return (x.instance.is_a(), x.ray_distance, tuple(x.position))
|
||||
|
||||
pp = pairs[he_idx : he_idx + 2]
|
||||
if pp == (-1, -1):
|
||||
continue
|
||||
data = list(map(format, map(semantics.__getitem__, pp)))
|
||||
if None not in data and data[0][0] == data[1][0] and abs(data[0][1] - data[1][1]) < 1.0e-5:
|
||||
to_remove.append(he_idx // 2)
|
||||
# Print edge index and semantic data
|
||||
# print(he_idx // 2, *data)
|
||||
|
||||
svgfill_context.merge(to_remove)
|
||||
|
||||
# Swap the XML nodes from the files
|
||||
# Remove the original hidden line node we still have in the serializer output
|
||||
g1.removeChild(projection)
|
||||
g2.setAttribute("class", "projection")
|
||||
# Find the children of the projection node parent
|
||||
children = [x for x in g1.childNodes if x.nodeType == x.ELEMENT_NODE]
|
||||
if children:
|
||||
# Insert the new semantically enriched cell-based projection node
|
||||
# *before* the node with sections from the serializer. SVG derives
|
||||
# draw order from node order in the DOM so sections are draw over
|
||||
# the projections.
|
||||
g1.insertBefore(g2, children[0])
|
||||
else:
|
||||
# This generally shouldn't happen
|
||||
g1.appendChild(g2)
|
||||
|
||||
data = dom1.toxml()
|
||||
data = data.encode("ascii", "xmlcharrefreplace")
|
||||
|
||||
results = data
|
||||
with profile("Post processing"):
|
||||
root = etree.fromstring(results)
|
||||
self.move_projection_to_bottom(root)
|
||||
@@ -664,6 +818,9 @@ class CreateDrawing(bpy.types.Operator):
|
||||
def move_projection_to_bottom(self, root):
|
||||
# https://stackoverflow.com/questions/36018627/sorting-child-elements-with-lxml-based-on-attribute-value
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces:
|
||||
# SVGFill already places the projection in the right spot.
|
||||
return
|
||||
# group[:] = sorted(group, key=lambda e : "projection" in e.get("class"))
|
||||
if group is not None:
|
||||
group[:] = reversed(group)
|
||||
|
||||
@@ -330,7 +330,8 @@ class DocProperties(PropertyGroup):
|
||||
|
||||
|
||||
class BIMCameraProperties(PropertyGroup):
|
||||
calculate_surfaces: BoolProperty(name="Calculate Surfaces", default=False)
|
||||
calculate_shapely_surfaces: BoolProperty(name="Calculate Shapely Surfaces", default=False)
|
||||
calculate_svgfill_surfaces: BoolProperty(name="Calculate SVGFill Surfaces", default=False)
|
||||
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
|
||||
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
||||
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
|
||||
|
||||
@@ -63,7 +63,9 @@ class BIM_PT_camera(Panel):
|
||||
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "calculate_surfaces")
|
||||
row.prop(props, "calculate_shapely_surfaces")
|
||||
row = layout.row()
|
||||
row.prop(props, "calculate_svgfill_surfaces")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(dprops, "should_extract")
|
||||
|
||||
Reference in New Issue
Block a user