mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
See #3002. Spaces are now managed separately so that they don't conflict with projection polygon merging and raycasting.
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
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; }
|
||||
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
|
||||
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
|
||||
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; }
|
||||
|
||||
@@ -1975,79 +1975,15 @@ class CutDecorator:
|
||||
if verts is False:
|
||||
return None, None
|
||||
|
||||
if not self.is_intersecting_camera(obj, context.scene.camera):
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
if verts is None:
|
||||
verts, edges = self.bisect_mesh(obj, context.scene.camera)
|
||||
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
|
||||
DecoratorData.cut_cache[element.id()] = (verts, edges)
|
||||
return verts, edges
|
||||
|
||||
def is_intersecting_camera(self, obj, camera):
|
||||
# Based on separating axis theorem
|
||||
plane_co = camera.matrix_world.translation
|
||||
plane_no = camera.matrix_world.col[2].xyz
|
||||
|
||||
# Broadphase check using the bounding box
|
||||
bounding_box_world_coords = [obj.matrix_world @ Vector(coord) for coord in obj.bound_box]
|
||||
bounding_box_signed_distances = [plane_no.dot(v - plane_co) for v in bounding_box_world_coords]
|
||||
|
||||
pos_exists_bb = any(d > 0 for d in bounding_box_signed_distances)
|
||||
neg_exists_bb = any(d < 0 for d in bounding_box_signed_distances)
|
||||
|
||||
if not (pos_exists_bb and neg_exists_bb):
|
||||
return False
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
|
||||
# Transform the vertices to world space
|
||||
mesh_mat = obj.matrix_world
|
||||
bm.transform(mesh_mat)
|
||||
|
||||
# Calculate the signed distances of all vertices from the plane
|
||||
signed_distances = [plane_no.dot(v.co - plane_co) for v in bm.verts]
|
||||
|
||||
bm.free()
|
||||
|
||||
# Check for intersection
|
||||
pos_exists = any(d > 0 for d in signed_distances)
|
||||
neg_exists = any(d < 0 for d in signed_distances)
|
||||
|
||||
return pos_exists and neg_exists
|
||||
|
||||
def bisect_mesh(self, obj, camera):
|
||||
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
|
||||
plane_co = camera_matrix.translation
|
||||
plane_no = camera_matrix.col[2].xyz
|
||||
|
||||
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
|
||||
# Run the bisect operation
|
||||
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
|
||||
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
|
||||
|
||||
vert_map = {}
|
||||
verts = []
|
||||
edges = []
|
||||
i = 0
|
||||
for geom in results["geom_cut"]:
|
||||
if isinstance(geom, bmesh.types.BMVert):
|
||||
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
|
||||
vert_map[geom.index] = i
|
||||
i += 1
|
||||
else:
|
||||
# It seems as though edges always appear after verts
|
||||
edges.append([vert_map[v.index] for v in geom.verts])
|
||||
|
||||
bm.free()
|
||||
|
||||
return verts, edges
|
||||
|
||||
|
||||
class DecorationsHandler:
|
||||
decorators_classes = [
|
||||
|
||||
@@ -485,10 +485,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.serialiser.finalize()
|
||||
results = self.svg_buffer.get_value()
|
||||
|
||||
root = etree.fromstring(results)
|
||||
self.move_projection_to_bottom(root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
|
||||
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]
|
||||
group = root.findall(".//{http://www.w3.org/2000/svg}g")[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"]))
|
||||
@@ -503,7 +506,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
for projection in projections:
|
||||
boundary_lines = []
|
||||
for path in projection.findall('./{http://www.w3.org/2000/svg}path'):
|
||||
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))
|
||||
@@ -517,23 +520,39 @@ class CreateDrawing(bpy.types.Operator):
|
||||
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)
|
||||
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")]
|
||||
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"
|
||||
path = etree.Element("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"
|
||||
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)
|
||||
group.insert(0, path)
|
||||
|
||||
if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces:
|
||||
results = etree.tostring(root).decode("utf8")
|
||||
svg_data_1 = results
|
||||
from xml.dom.minidom import parseString
|
||||
|
||||
@@ -624,7 +643,11 @@ class CreateDrawing(bpy.types.Operator):
|
||||
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")]
|
||||
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()))
|
||||
@@ -682,16 +705,56 @@ class CreateDrawing(bpy.types.Operator):
|
||||
# This generally shouldn't happen
|
||||
g1.appendChild(g2)
|
||||
|
||||
data = dom1.toxml()
|
||||
data = data.encode("ascii", "xmlcharrefreplace")
|
||||
|
||||
results = data
|
||||
with profile("Post processing"):
|
||||
results = dom1.toxml()
|
||||
results = results.encode("ascii", "xmlcharrefreplace")
|
||||
root = etree.fromstring(results)
|
||||
self.move_projection_to_bottom(root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
with open(svg_path, "wb") as svg:
|
||||
svg.write(etree.tostring(root))
|
||||
|
||||
# Spaces are handled as a special case, since they are often overlayed
|
||||
# in addition to elements. For example, a space should not obscure
|
||||
# other elements in projection. Spaces should also not override cut
|
||||
# elements but instead be drawn in addition to cut elements.
|
||||
spaces = tool.Drawing.get_drawing_spaces(self.camera_element)
|
||||
|
||||
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
|
||||
|
||||
self.svg_writer.calculate_scale()
|
||||
x_offset = self.svg_writer.raw_width / 2
|
||||
y_offset = self.svg_writer.raw_height / 2
|
||||
|
||||
for space in spaces:
|
||||
obj = tool.Ifc.get_object(space)
|
||||
if not obj or not tool.Drawing.is_intersecting_camera(obj, self.camera):
|
||||
continue
|
||||
verts, edges = tool.Drawing.bisect_mesh(obj, self.camera)
|
||||
verts = [self.svg_writer.project_point_onto_camera(Vector(v)) for v in verts]
|
||||
line_strings = [
|
||||
shapely.LineString(
|
||||
[
|
||||
(
|
||||
(x_offset + verts[e[0]][0]) * self.svg_writer.svg_scale,
|
||||
(y_offset - verts[e[0]][1]) * self.svg_writer.svg_scale,
|
||||
),
|
||||
(
|
||||
(x_offset + verts[e[1]][0]) * self.svg_writer.svg_scale,
|
||||
(y_offset - verts[e[1]][1]) * self.svg_writer.svg_scale,
|
||||
),
|
||||
]
|
||||
)
|
||||
for e in edges
|
||||
]
|
||||
closed_polygons = shapely.polygonize(line_strings)
|
||||
for polygon in closed_polygons.geoms:
|
||||
classes = self.get_svg_classes(space)
|
||||
path = etree.Element("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
|
||||
path.set("class", " ".join(list(classes)))
|
||||
group.append(path)
|
||||
|
||||
with open(svg_path, "wb") as svg:
|
||||
svg.write(etree.tostring(root))
|
||||
|
||||
return svg_path
|
||||
|
||||
@@ -748,13 +811,12 @@ class CreateDrawing(bpy.types.Operator):
|
||||
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))
|
||||
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
|
||||
)
|
||||
return classes
|
||||
|
||||
def merge_linework_and_add_metadata(self, root):
|
||||
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
|
||||
joined_paths = {}
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
@@ -796,16 +858,15 @@ class CreateDrawing(bpy.types.Operator):
|
||||
elif type(merged_polygons) == shapely.Polygon:
|
||||
merged_polygons = [merged_polygons]
|
||||
|
||||
root_g = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
|
||||
for polygon in merged_polygons:
|
||||
g = etree.SubElement(root, "g")
|
||||
g = etree.Element("g")
|
||||
path = etree.SubElement(g, "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
|
||||
g.set("class", " ".join(list(classes)))
|
||||
root_g.append(g)
|
||||
group.append(g)
|
||||
|
||||
def drawing_to_model_co(self, m44, m4, xy, z=0.0):
|
||||
xyzw = m44 @ np.array(xy + [z, 1.0])
|
||||
@@ -816,14 +877,12 @@ class CreateDrawing(bpy.types.Operator):
|
||||
return tuple(map(float, arr))
|
||||
|
||||
def move_projection_to_bottom(self, root):
|
||||
# https://stackoverflow.com/questions/36018627/sorting-child-elements-with-lxml-based-on-attribute-value
|
||||
# IfcConvert puts the projection afterwards which is not correct since
|
||||
# projection should be drawn underneath the cut.
|
||||
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)
|
||||
projection = group.find("{http://www.w3.org/2000/svg}g[@class='projection']")
|
||||
projection.getparent().remove(projection)
|
||||
group.insert(0, projection)
|
||||
|
||||
def generate_annotation(self, context):
|
||||
if not self.cprops.has_annotation:
|
||||
|
||||
@@ -1342,6 +1342,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
|
||||
else:
|
||||
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
|
||||
elements = {e for e in elements if e.is_a() != "IfcSpace"}
|
||||
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
|
||||
elements.update(annotations)
|
||||
|
||||
@@ -1351,6 +1352,19 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
elements -= set(ifc_file.by_type("IfcOpeningElement"))
|
||||
return elements
|
||||
|
||||
@classmethod
|
||||
def get_drawing_spaces(cls, drawing):
|
||||
ifc_file = tool.Ifc.get()
|
||||
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
|
||||
include = pset.get("Include", None)
|
||||
elements = set(ifc_file.by_type("IfcSpace"))
|
||||
if include:
|
||||
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
|
||||
exclude = pset.get("Exclude", None)
|
||||
if exclude:
|
||||
elements -= set(ifcopenshell.util.selector.Selector.parse(ifc_file, exclude, elements=elements))
|
||||
return elements
|
||||
|
||||
@classmethod
|
||||
def get_annotation_element(cls, element):
|
||||
for rel in element.HasAssignments:
|
||||
@@ -1464,3 +1478,69 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
should_sync_changes_first=True,
|
||||
)
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def is_intersecting_camera(cls, obj, camera):
|
||||
# Based on separating axis theorem
|
||||
plane_co = camera.matrix_world.translation
|
||||
plane_no = camera.matrix_world.col[2].xyz
|
||||
|
||||
# Broadphase check using the bounding box
|
||||
bounding_box_world_coords = [obj.matrix_world @ Vector(coord) for coord in obj.bound_box]
|
||||
bounding_box_signed_distances = [plane_no.dot(v - plane_co) for v in bounding_box_world_coords]
|
||||
|
||||
pos_exists_bb = any(d > 0 for d in bounding_box_signed_distances)
|
||||
neg_exists_bb = any(d < 0 for d in bounding_box_signed_distances)
|
||||
|
||||
if not (pos_exists_bb and neg_exists_bb):
|
||||
return False
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
|
||||
# Transform the vertices to world space
|
||||
mesh_mat = obj.matrix_world
|
||||
bm.transform(mesh_mat)
|
||||
|
||||
# Calculate the signed distances of all vertices from the plane
|
||||
signed_distances = [plane_no.dot(v.co - plane_co) for v in bm.verts]
|
||||
|
||||
bm.free()
|
||||
|
||||
# Check for intersection
|
||||
pos_exists = any(d > 0 for d in signed_distances)
|
||||
neg_exists = any(d < 0 for d in signed_distances)
|
||||
|
||||
return pos_exists and neg_exists
|
||||
|
||||
@classmethod
|
||||
def bisect_mesh(cls, obj, camera):
|
||||
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
|
||||
plane_co = camera_matrix.translation
|
||||
plane_no = camera_matrix.col[2].xyz
|
||||
|
||||
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
|
||||
# Run the bisect operation
|
||||
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
|
||||
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
|
||||
|
||||
vert_map = {}
|
||||
verts = []
|
||||
edges = []
|
||||
i = 0
|
||||
for geom in results["geom_cut"]:
|
||||
if isinstance(geom, bmesh.types.BMVert):
|
||||
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
|
||||
vert_map[geom.index] = i
|
||||
i += 1
|
||||
else:
|
||||
# It seems as though edges always appear after verts
|
||||
edges.append([vert_map[v.index] for v in geom.verts])
|
||||
|
||||
bm.free()
|
||||
|
||||
return verts, edges
|
||||
|
||||
Reference in New Issue
Block a user