mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 19:02:26 +00:00
See #3002. Experimental shapely approach to polygon merging for semantic projection elements.
This commit is contained in:
@@ -26,6 +26,7 @@ import shutil
|
|||||||
import shapely
|
import shapely
|
||||||
import subprocess
|
import subprocess
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
import numpy as np
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.geom
|
import ifcopenshell.geom
|
||||||
@@ -439,6 +440,8 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
self.setup_serialiser(ifc)
|
self.setup_serialiser(ifc)
|
||||||
cache = IfcStore.get_cache()
|
cache = IfcStore.get_cache()
|
||||||
[cache.remove(guid) for guid in invalidated_guids]
|
[cache.remove(guid) for guid in invalidated_guids]
|
||||||
|
tree = ifcopenshell.geom.tree()
|
||||||
|
tree.enable_face_styles(True)
|
||||||
|
|
||||||
elements = drawing_elements.copy()
|
elements = drawing_elements.copy()
|
||||||
for body_context in body_contexts:
|
for body_context in body_contexts:
|
||||||
@@ -456,6 +459,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
for elem in self.yield_from_iterator(it):
|
for elem in self.yield_from_iterator(it):
|
||||||
processed.add(ifc.by_id(elem.id))
|
processed.add(ifc.by_id(elem.id))
|
||||||
self.serialiser.write(elem)
|
self.serialiser.write(elem)
|
||||||
|
tree.add_element(elem)
|
||||||
elements -= processed
|
elements -= processed
|
||||||
|
|
||||||
elements = drawing_elements.copy()
|
elements = drawing_elements.copy()
|
||||||
@@ -474,20 +478,60 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
for elem in self.yield_from_iterator(it):
|
for elem in self.yield_from_iterator(it):
|
||||||
processed.add(ifc.by_id(elem.id))
|
processed.add(ifc.by_id(elem.id))
|
||||||
self.serialiser.write(elem)
|
self.serialiser.write(elem)
|
||||||
|
tree.add_element(elem)
|
||||||
elements -= processed
|
elements -= processed
|
||||||
|
|
||||||
with profile("Camera element"):
|
|
||||||
# @todo tfk: is this needed?
|
|
||||||
# If the camera isn't serialised, then nothing gets serialised. Odd behaviour.
|
|
||||||
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
|
|
||||||
it = ifcopenshell.geom.iterator(geom_settings, ifc, include=[self.camera_element])
|
|
||||||
for elem in self.yield_from_iterator(it):
|
|
||||||
self.serialiser.write(elem)
|
|
||||||
|
|
||||||
with profile("Finalizing"):
|
with profile("Finalizing"):
|
||||||
self.serialiser.finalize()
|
self.serialiser.finalize()
|
||||||
results = self.svg_buffer.get_value()
|
results = self.svg_buffer.get_value()
|
||||||
|
|
||||||
|
if self.camera.data.BIMCameraProperties.calculate_surfaces:
|
||||||
|
# shapely variant
|
||||||
|
root = etree.fromstring(results)
|
||||||
|
group = root.findall('.//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}name]')[0]
|
||||||
|
nm = group.attrib["{http://www.ifcopenshell.org/ns}name"]
|
||||||
|
m4 = np.array(json.loads(group.attrib["{http://www.ifcopenshell.org/ns}plane"]))
|
||||||
|
m3 = np.array(json.loads(group.attrib["{http://www.ifcopenshell.org/ns}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)
|
||||||
|
|
||||||
|
projections = group.findall('.//{http://www.w3.org/2000/svg}g[@class="projection"]') or []
|
||||||
|
|
||||||
|
for projection in projections:
|
||||||
|
boundary_lines = []
|
||||||
|
for path in projection.findall('./{http://www.w3.org/2000/svg}path'):
|
||||||
|
start, end = [co[1:].split(",") for co in path.attrib["d"].split()]
|
||||||
|
boundary_lines.append(shapely.LineString([start, end]))
|
||||||
|
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
|
||||||
|
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
|
||||||
|
|
||||||
|
for polygon in closed_polygons.geoms:
|
||||||
|
# Less than 1mm2 is not worth styling on sheet
|
||||||
|
if polygon.area < 1:
|
||||||
|
continue
|
||||||
|
centroid = polygon.centroid
|
||||||
|
internal_point = centroid if polygon.contains(centroid) else polygon.representative_point()
|
||||||
|
if internal_point:
|
||||||
|
internal_point = [internal_point.x, internal_point.y]
|
||||||
|
a, b = self.drawing_to_model_co(m44, m4, internal_point, 0.0), self.drawing_to_model_co(m44, m4, internal_point, -100.0)
|
||||||
|
inside_elements = [e for e in tree.select(self.pythonize(a)) if not e.is_a("IfcAnnotation")]
|
||||||
|
if not inside_elements:
|
||||||
|
elements = [e for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a)) if not e.instance.is_a("IfcAnnotation")]
|
||||||
|
if elements:
|
||||||
|
path = etree.SubElement(group, "path")
|
||||||
|
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
|
||||||
|
for interior in polygon.interiors:
|
||||||
|
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
|
||||||
|
path.attrib["d"] = d
|
||||||
|
classes = self.get_svg_classes(ifc.by_id(elements[0].instance.id()))
|
||||||
|
classes.append("surface")
|
||||||
|
path.set("class", " ".join(list(classes)))
|
||||||
|
|
||||||
|
results = etree.tostring(root)
|
||||||
with profile("Post processing"):
|
with profile("Post processing"):
|
||||||
root = etree.fromstring(results)
|
root = etree.fromstring(results)
|
||||||
self.move_projection_to_bottom(root)
|
self.move_projection_to_bottom(root)
|
||||||
@@ -532,36 +576,45 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
if not it.next():
|
if not it.next():
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def get_svg_classes(self, element):
|
||||||
|
classes = [element.is_a()]
|
||||||
|
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||||
|
material_name = ""
|
||||||
|
if material:
|
||||||
|
if material.is_a("IfcMaterialLayerSet"):
|
||||||
|
material_name = material.LayerSetName or "null"
|
||||||
|
else:
|
||||||
|
material_name = getattr(material, "Name", "null") or "null"
|
||||||
|
material_name = tool.Drawing.canonicalise_class_name(material_name)
|
||||||
|
classes.append(f"material-{material_name}")
|
||||||
|
else:
|
||||||
|
classes.append(f"material-null")
|
||||||
|
|
||||||
|
for key in self.metadata:
|
||||||
|
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||||
|
if value:
|
||||||
|
classes.append(
|
||||||
|
tool.Drawing.canonicalise_class_name(key)
|
||||||
|
+ "-"
|
||||||
|
+ tool.Drawing.canonicalise_class_name(str(value))
|
||||||
|
)
|
||||||
|
return classes
|
||||||
|
|
||||||
def merge_linework_and_add_metadata(self, root):
|
def merge_linework_and_add_metadata(self, root):
|
||||||
joined_paths = {}
|
joined_paths = {}
|
||||||
|
|
||||||
ifc = tool.Ifc.get()
|
ifc = tool.Ifc.get()
|
||||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||||
classes = ["cut"]
|
|
||||||
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
||||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
classes = self.get_svg_classes(element)
|
||||||
material_name = ""
|
classes.append("cut")
|
||||||
if material:
|
|
||||||
if material.is_a("IfcMaterialLayerSet"):
|
|
||||||
material_name = material.LayerSetName or "null"
|
|
||||||
else:
|
|
||||||
material_name = getattr(material, "Name", "null") or "null"
|
|
||||||
material_name = tool.Drawing.canonicalise_class_name(material_name)
|
|
||||||
classes.append(f"material-{material_name}")
|
|
||||||
|
|
||||||
for key in self.metadata:
|
el.set("class", " ".join(classes))
|
||||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
|
||||||
if value:
|
|
||||||
classes.append(
|
|
||||||
tool.Drawing.canonicalise_class_name(key)
|
|
||||||
+ "-"
|
|
||||||
+ tool.Drawing.canonicalise_class_name(str(value))
|
|
||||||
)
|
|
||||||
|
|
||||||
el.set("class", (el.get("class", "") + " " + " ".join(classes)).strip())
|
material_name = [c for c in classes if c.split("-")[0] == "material"][0]
|
||||||
|
|
||||||
# Drawing convention states that objects with the same material are merged when cut.
|
# Drawing convention states that objects with the same material are merged when cut.
|
||||||
if material_name != "null" and el.findall("{http://www.w3.org/2000/svg}path"):
|
if material_name != "material-null" and el.findall("{http://www.w3.org/2000/svg}path"):
|
||||||
joined_paths.setdefault(material_name, []).append(el)
|
joined_paths.setdefault(material_name, []).append(el)
|
||||||
|
|
||||||
for key, els in joined_paths.items():
|
for key, els in joined_paths.items():
|
||||||
@@ -600,6 +653,14 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
g.set("class", " ".join(list(classes)))
|
g.set("class", " ".join(list(classes)))
|
||||||
root_g.append(g)
|
root_g.append(g)
|
||||||
|
|
||||||
|
def drawing_to_model_co(self, m44, m4, xy, z=0.0):
|
||||||
|
xyzw = m44 @ np.array(xy + [z, 1.0])
|
||||||
|
xyzw[1] *= -1.0
|
||||||
|
return (m4 @ xyzw)[0:3]
|
||||||
|
|
||||||
|
def pythonize(self, arr):
|
||||||
|
return tuple(map(float, arr))
|
||||||
|
|
||||||
def move_projection_to_bottom(self, root):
|
def move_projection_to_bottom(self, root):
|
||||||
# https://stackoverflow.com/questions/36018627/sorting-child-elements-with-lxml-based-on-attribute-value
|
# https://stackoverflow.com/questions/36018627/sorting-child-elements-with-lxml-based-on-attribute-value
|
||||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||||
@@ -654,20 +715,6 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
continue
|
continue
|
||||||
return space
|
return space
|
||||||
|
|
||||||
def get_classes(self, element, position, svg_writer):
|
|
||||||
classes = [position, element.is_a()]
|
|
||||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
|
||||||
if material:
|
|
||||||
classes.append("material-{}".format(re.sub("[^0-9a-zA-Z]+", "", self.get_material_name(material))))
|
|
||||||
classes.append("globalid-{}".format(element.GlobalId))
|
|
||||||
for attribute in svg_writer.annotations["attributes"]:
|
|
||||||
result = ifcopenshell.util.selector.get_element_value(element, attribute)
|
|
||||||
if result:
|
|
||||||
classes.append(
|
|
||||||
"{}-{}".format(re.sub("[^0-9a-zA-Z]+", "", attribute), re.sub("[^0-9a-zA-Z]+", "", result))
|
|
||||||
)
|
|
||||||
return classes
|
|
||||||
|
|
||||||
def get_material_name(self, element):
|
def get_material_name(self, element):
|
||||||
if hasattr(element, "Name") and element.Name:
|
if hasattr(element, "Name") and element.Name:
|
||||||
return element.Name
|
return element.Name
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ class DocProperties(PropertyGroup):
|
|||||||
|
|
||||||
|
|
||||||
class BIMCameraProperties(PropertyGroup):
|
class BIMCameraProperties(PropertyGroup):
|
||||||
|
calculate_surfaces: BoolProperty(name="Calculate Surfaces", default=False)
|
||||||
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
|
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
|
||||||
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
||||||
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
|
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ class BIM_PT_camera(Panel):
|
|||||||
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
|
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
|
||||||
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(props, "calculate_surfaces")
|
||||||
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.prop(dprops, "should_extract")
|
row.prop(dprops, "should_extract")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user