mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
See #1227. Implement wall layers in SVG drawings.
This commit is contained in:
@@ -1760,19 +1760,19 @@ class CutDecorator:
|
||||
prev_co = None
|
||||
if not usage:
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
no = self.get_extrusion_vector(element).normalized()
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
elif usage.LayerSetDirection == "AXIS2":
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = self.get_extrusion_vector(element).normalized()
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = self.get_extrusion_vector(element).normalized()
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = self.get_extrusion_vector(element).normalized()
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
no *= sense_factor
|
||||
slice_plane_geom = []
|
||||
@@ -1797,19 +1797,19 @@ class CutDecorator:
|
||||
bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True
|
||||
)
|
||||
edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)]
|
||||
fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges)
|
||||
bmesh.ops.edgenet_fill(bm_fill, edges=edges)
|
||||
if i != 0:
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
bisect = bmesh.ops.bisect_plane(
|
||||
bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True
|
||||
)
|
||||
edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)]
|
||||
fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges)
|
||||
bmesh.ops.edgenet_fill(bm_fill, edges=edges)
|
||||
|
||||
verts, tris = self.bisect_mesh_tris(obj, bm_fill, context.scene.camera)
|
||||
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
|
||||
|
||||
verts, edges = self.bisect_mesh(obj, bm, slice_plane_geom, context.scene.camera)
|
||||
verts, edges = tool.Drawing.bisect_bmesh(obj, bm, slice_plane_geom, context.scene.camera)
|
||||
DecoratorData.slice_cache[element.id()] = (verts, edges)
|
||||
bm_original.free()
|
||||
|
||||
@@ -1823,40 +1823,6 @@ class CutDecorator:
|
||||
colour = styles[0].SurfaceColour
|
||||
return (colour.Red, colour.Green, colour.Blue, 1)
|
||||
|
||||
def get_extrusion_vector(self, wall):
|
||||
if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
return Vector(item.ExtrudedDirection.DirectionRatios)
|
||||
return Vector([0.0, 0.0, 1.0])
|
||||
|
||||
def bisect_mesh(self, obj, bm, geom, 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
|
||||
|
||||
# Run the bisect operation
|
||||
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])
|
||||
|
||||
return verts, edges
|
||||
|
||||
def bisect_mesh_tris(self, obj, bm, camera):
|
||||
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
|
||||
plane_co = camera_matrix.translation
|
||||
|
||||
@@ -562,6 +562,105 @@ class CreateDrawing(bpy.types.Operator):
|
||||
path.attrib["d"] = d
|
||||
group.append(g)
|
||||
|
||||
def generate_wall_layers(self, context: bpy.types.Context, root):
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
if "projection" in el.get("class", "").split():
|
||||
continue
|
||||
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
||||
if not (obj := tool.Ifc.get_object(element)):
|
||||
continue
|
||||
if not (material := ifcopenshell.util.element.get_material(element)):
|
||||
continue
|
||||
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
|
||||
continue
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
usage = material
|
||||
layer_set = material.ForLayerSet
|
||||
offset = usage.OffsetFromReferenceLine * self.unit_scale
|
||||
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
usage = None
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
|
||||
camera_matrix_i = context.scene.camera.matrix_world.inverted()
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
raw_width, raw_height = self.get_camera_dimensions()
|
||||
x_offset = raw_width / 2
|
||||
y_offset = raw_height / 2
|
||||
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
|
||||
|
||||
mesh = obj.data
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
|
||||
prev_co = None
|
||||
if not usage:
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
elif usage.LayerSetDirection == "AXIS2":
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
prev_co = co.copy()
|
||||
co += no * layer.LayerThickness * self.unit_scale
|
||||
|
||||
bm_fill = bm.copy()
|
||||
if i != last_i:
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
bisect = bmesh.ops.bisect_plane(
|
||||
bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True
|
||||
)
|
||||
edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)]
|
||||
bmesh.ops.edgenet_fill(bm_fill, edges=edges)
|
||||
if i != 0:
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
bisect = bmesh.ops.bisect_plane(
|
||||
bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True
|
||||
)
|
||||
edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)]
|
||||
bmesh.ops.edgenet_fill(bm_fill, edges=edges)
|
||||
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
verts, edges = tool.Drawing.bisect_bmesh(obj, bm_fill, geom, context.scene.camera)
|
||||
|
||||
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
|
||||
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
|
||||
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
|
||||
g.attrib["{http://www.ifcopenshell.org/ns}layer-id"] = str(layer.id())
|
||||
|
||||
lines = []
|
||||
for edge in edges:
|
||||
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
|
||||
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
|
||||
coords = [start, end]
|
||||
d = " ".join(
|
||||
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
|
||||
)
|
||||
d = "M{}".format(d[1:])
|
||||
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
|
||||
path.attrib["d"] = d
|
||||
group.append(g)
|
||||
|
||||
bm.free()
|
||||
|
||||
def generate_freestyle_linework(self, context: bpy.types.Context) -> str | None:
|
||||
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
|
||||
return
|
||||
@@ -769,10 +868,12 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
|
||||
self.remove_cut_linework(root)
|
||||
self.generate_bisect_linework(context, root)
|
||||
self.generate_wall_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE":
|
||||
self.move_projection_to_bottom(root)
|
||||
self.generate_wall_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
|
||||
@@ -1101,7 +1202,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
# the style of the face when running tree.select_ray()
|
||||
# tree.enable_face_styles(True)
|
||||
|
||||
def get_svg_classes(self, element):
|
||||
def get_svg_classes(self, element, layer=None):
|
||||
classes = [element.is_a()]
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
material_name = ""
|
||||
@@ -1113,7 +1214,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
material_name = tool.Drawing.canonicalise_class_name(material_name)
|
||||
classes.append(f"material-{material_name}")
|
||||
else:
|
||||
classes.append(f"material-null")
|
||||
classes.append("material-null")
|
||||
|
||||
if layer:
|
||||
classes.append(layer.is_a())
|
||||
material_name = layer.Material.Name or "null"
|
||||
material_name = tool.Drawing.canonicalise_class_name(material_name)
|
||||
classes.append(f"layer-material-{material_name}")
|
||||
|
||||
for key in self.metadata:
|
||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||
@@ -1152,6 +1259,23 @@ class CreateDrawing(bpy.types.Operator):
|
||||
except:
|
||||
continue
|
||||
|
||||
def get_element_by_id(self, step_id):
|
||||
try:
|
||||
step_id = int(step_id)
|
||||
except:
|
||||
return
|
||||
try:
|
||||
return tool.Ifc.get().by_id(step_id)
|
||||
except:
|
||||
props = tool.Project.get_project_props()
|
||||
for link in props.links:
|
||||
if link.name not in IfcStore.session_files:
|
||||
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
|
||||
try:
|
||||
return IfcStore.session_files[link.name].by_id(step_id)
|
||||
except:
|
||||
continue
|
||||
|
||||
def remove_cut_linework(self, root):
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
if "projection" not in el.get("class", "").split():
|
||||
@@ -1163,15 +1287,15 @@ class CreateDrawing(bpy.types.Operator):
|
||||
join_criteria = join_criteria.split(",")
|
||||
else:
|
||||
# Drawing convention states that same objects classes with the same material are merged when cut.
|
||||
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status"]
|
||||
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
joined_paths = {}
|
||||
self.is_manifold_cache = {}
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
||||
layer = self.get_element_by_id(el.get("{http://www.ifcopenshell.org/ns}layer-id"))
|
||||
|
||||
if "projection" in el.get("class", "").split():
|
||||
classes = self.get_svg_classes(element)
|
||||
@@ -1179,7 +1303,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
el.set("class", " ".join(classes))
|
||||
continue
|
||||
else:
|
||||
classes = self.get_svg_classes(element)
|
||||
classes = self.get_svg_classes(element, layer)
|
||||
classes.append("cut")
|
||||
el.set("class", " ".join(classes))
|
||||
|
||||
@@ -1188,7 +1312,12 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if not obj: # This is a linked model object. For now, do nothing.
|
||||
continue
|
||||
|
||||
if not self.is_manifold(obj):
|
||||
if (material := ifcopenshell.util.element.get_material(element)) and material.is_a() in (
|
||||
"IfcMaterialLayerSet",
|
||||
"IfcMaterialLayerSetUsage",
|
||||
):
|
||||
pass # These are always manifold
|
||||
elif not self.is_manifold(obj):
|
||||
continue
|
||||
|
||||
# An element group will contain a bunch of paths representing the
|
||||
@@ -1273,6 +1402,15 @@ class CreateDrawing(bpy.types.Operator):
|
||||
else:
|
||||
keys.append(key)
|
||||
|
||||
if layer:
|
||||
for query in join_criteria:
|
||||
key = ifcopenshell.util.selector.get_element_value(layer, query)
|
||||
print("got layer key", query, key)
|
||||
if isinstance(key, (list, tuple)):
|
||||
keys.extend(key)
|
||||
else:
|
||||
keys.append(key)
|
||||
|
||||
hash_keys = hash(tuple(keys))
|
||||
|
||||
if el.findall("{http://www.w3.org/2000/svg}path"):
|
||||
@@ -1289,7 +1427,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
for path in el.findall("{http://www.w3.org/2000/svg}path"):
|
||||
for subpath in path.attrib["d"].split("M")[1:]:
|
||||
subpath_co = "M" + subpath.strip(" Z")
|
||||
coords = [[float(o) for o in co[1:].split(",")] for co in subpath_co.split()]
|
||||
# Round due to inaccuracies from Blender meshes and bisection
|
||||
coords = [[round(float(o), 3) for o in co[1:].split(",")] for co in subpath_co.split()]
|
||||
if subpath.strip().lower().endswith("z"):
|
||||
coords.append(coords[0])
|
||||
if len(coords) > 2 and coords[0] == coords[-1]:
|
||||
|
||||
@@ -2112,6 +2112,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
|
||||
@classmethod
|
||||
def bisect_mesh(cls, obj: bpy.types.Object, camera: bpy.types.Object) -> tuple[list[Vector], list[list[int]]]:
|
||||
# TODO consolidate with other bisect functions
|
||||
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
|
||||
plane_co = camera_matrix.translation
|
||||
plane_no = camera_matrix.col[2].xyz
|
||||
@@ -2121,10 +2122,38 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
|
||||
return cls.bisect_mesh_with_plane(obj, plane_co, plane_no, global_offset=global_offset)
|
||||
|
||||
@classmethod
|
||||
def bisect_bmesh(cls, obj, bm, geom, camera):
|
||||
# TODO consolidate with other bisect functions
|
||||
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
|
||||
|
||||
# Run the bisect operation
|
||||
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])
|
||||
|
||||
return verts, edges
|
||||
|
||||
@classmethod
|
||||
def bisect_mesh_with_plane(
|
||||
cls, obj: bpy.types.Object, plane_co: Vector, plane_no: Vector, global_offset: Optional[Vector] = None
|
||||
) -> tuple[list[Vector], list[list[int]]]:
|
||||
# TODO consolidate with other bisect functions
|
||||
if global_offset is None:
|
||||
global_offset = Vector()
|
||||
|
||||
@@ -2152,6 +2181,16 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
|
||||
return verts, edges
|
||||
|
||||
@classmethod
|
||||
def get_extrusion_vector(cls, wall):
|
||||
if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
return Vector(item.ExtrudedDirection.DirectionRatios)
|
||||
return Vector([0.0, 0.0, 1.0])
|
||||
|
||||
@classmethod
|
||||
def get_scale_ratio(cls, scale: str) -> float:
|
||||
numerator, denominator = scale.split("/")
|
||||
|
||||
Reference in New Issue
Block a user