mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
#1153 Implement angular dimension annotations
This commit is contained in:
@@ -31,6 +31,7 @@ text { fill: black; stroke: none; }
|
||||
.PredefinedType-SECTIONLEVEL { marker-start: url(#section-level-marker); stroke-dasharray: 12.5, 3, 3, 3; }
|
||||
.PredefinedType-PLANLEVEL { marker-end: url(#plan-level-marker); }
|
||||
.PredefinedType-DIMENSION { marker-start: url(#dimension-marker-start); marker-end: url(#dimension-marker-end); }
|
||||
.PredefinedType-ANGLE { marker-start: url(#angle-marker-start); marker-end: url(#angle-marker-end); }
|
||||
.PredefinedType-RADIUS { marker-end: url(#radius-marker-end); }
|
||||
.PredefinedType-DIAMETER { marker-start: url(#diameter-marker-start); marker-end: url(#diameter-marker-end); }
|
||||
.PredefinedType-HIDDENLINE { stroke-dasharray: 3, 2; }
|
||||
|
||||
@@ -21,6 +21,18 @@
|
||||
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
|
||||
</g>
|
||||
</marker>
|
||||
<marker id="angle-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
|
||||
<g>
|
||||
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />
|
||||
<path d="M 1 0 L 1 14" class="annotation" style="stroke-width:1;" />
|
||||
</g>
|
||||
</marker>
|
||||
<marker id="angle-marker-end" markerHeight="14" markerWidth="11" orient="auto" refX="10" refY="7">
|
||||
<g>
|
||||
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
|
||||
<path d="M 10 0 L 10 14" class="annotation" style="stroke-width:1;" />
|
||||
</g>
|
||||
</marker>
|
||||
<marker id="diameter-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
|
||||
<g>
|
||||
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />
|
||||
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -223,6 +223,9 @@ class CadArcFrom2Points(bpy.types.Operator):
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
if bpy.context.mode != "EDIT_MESH":
|
||||
return {"CANCELLED"}
|
||||
obj = bpy.context.active_object
|
||||
@@ -233,12 +236,13 @@ class CadArcFrom2Points(bpy.types.Operator):
|
||||
if not center:
|
||||
return {"CANCELLED"}
|
||||
mesh = obj.data
|
||||
mw = obj.matrix_world
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
selected_verts = [v for v in bm.verts if v.select]
|
||||
if len(selected_verts) != 2:
|
||||
return {"CANCELLED"}
|
||||
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, selected_verts[0].co)
|
||||
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, selected_verts[1].co)
|
||||
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
|
||||
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
|
||||
l1 = v1 - center
|
||||
l2 = v2 - center
|
||||
angle = l1.angle_signed(l2)
|
||||
@@ -252,10 +256,16 @@ class CadArcFrom2Points(bpy.types.Operator):
|
||||
v = selected_verts[0]
|
||||
bm.verts.remove(selected_verts[1])
|
||||
axis = region_3d.view_rotation @ mathutils.Vector((0, 0, 1))
|
||||
bmesh.ops.spin(bm, geom=[v], axis=axis, cent=cursor, steps=self.resolution * 4, angle=-angle)
|
||||
bmesh.ops.spin(
|
||||
bm,
|
||||
geom=[v],
|
||||
axis=mw.inverted().to_quaternion() @ axis,
|
||||
cent=mw.inverted() @ cursor,
|
||||
steps=self.resolution * 4,
|
||||
angle=-angle,
|
||||
)
|
||||
bmesh.update_edit_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -291,15 +301,16 @@ class CadArcFrom3Points(bpy.types.Operator):
|
||||
if len(selected_verts) != 3:
|
||||
return {"CANCELLED"}
|
||||
pts = [v.co for v in selected_verts]
|
||||
center = tool.Cad.generate_3PT(pts, obj, self.resolution * 4)
|
||||
center = tool.Cad.get_center_of_arc(pts, obj)
|
||||
if not center:
|
||||
return {"CANCELLED"}
|
||||
|
||||
bpy.context.scene.cursor.location = center
|
||||
if self.only_recalculate_center:
|
||||
bm.free()
|
||||
return {"FINISHED"}
|
||||
|
||||
center = obj.matrix_world.inverted() @ center
|
||||
|
||||
def get_distance_to_other_points(vert):
|
||||
other_verts = [v for v in selected_verts if v != vert]
|
||||
total_vector = mathutils.Vector((0, 0, 0))
|
||||
@@ -331,5 +342,4 @@ class CadArcFrom3Points(bpy.types.Operator):
|
||||
bmesh.ops.spin(bm, geom=[v], axis=normal, cent=center, steps=self.resolution * 4, angle=-angle)
|
||||
bmesh.update_edit_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -164,10 +164,11 @@ class Annotator:
|
||||
obj = bpy.data.objects.new(object_type, data)
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.ObjectType == object_type:
|
||||
return obj
|
||||
if object_type != "ANGLE":
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.ObjectType == object_type:
|
||||
return obj
|
||||
if data_type == "mesh":
|
||||
data = bpy.data.meshes.new(object_type)
|
||||
elif data_type == "curve":
|
||||
|
||||
@@ -20,7 +20,9 @@ import os
|
||||
import re
|
||||
import bpy
|
||||
import math
|
||||
import bmesh
|
||||
import pystache
|
||||
import mathutils
|
||||
import xml.etree.ElementTree as ET
|
||||
import svgwrite
|
||||
import ifcopenshell
|
||||
@@ -153,6 +155,8 @@ class SvgWriter:
|
||||
self.draw_stair_annotation(obj)
|
||||
elif element.ObjectType == "DIMENSION":
|
||||
self.draw_dimension_annotations(obj)
|
||||
elif element.ObjectType == "ANGLE":
|
||||
self.draw_angle_annotations(obj)
|
||||
elif element.ObjectType == "RADIUS":
|
||||
self.draw_radius_annotations(obj)
|
||||
elif element.ObjectType == "DIAMETER":
|
||||
@@ -623,6 +627,113 @@ class SvgWriter:
|
||||
)
|
||||
)
|
||||
|
||||
def draw_angle_annotations(self, obj):
|
||||
# This implementation uses an SVG arc, which means that it can only draw
|
||||
# arcs that are orthogonal to the view (e.g. not arcs in 3D).
|
||||
# Gosh this is bad code :(
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
|
||||
points = [v.co for v in obj.data.vertices][:3]
|
||||
center = tool.Cad.get_center_of_arc(points, obj)
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bm.verts.ensure_lookup_table()
|
||||
arc_end_verts = [v for v in bm.verts if len(v.link_edges) == 1]
|
||||
arc_end_pts = [matrix_world @ v.co for v in arc_end_verts]
|
||||
|
||||
# Probably need this when rewriting to use an SVG polyline instead of an arc
|
||||
# arc_path = [arc_end_verts[0]]
|
||||
# while True:
|
||||
# last_point = arc_end_verts[0]
|
||||
# found_another_point = False
|
||||
# for edge in last_point.link_edges:
|
||||
# v = edge.other_vert(last_point)
|
||||
# if v not in arc_path:
|
||||
# found_another_point = True
|
||||
# arc_path.append(v)
|
||||
# if not found_another_point:
|
||||
# break
|
||||
|
||||
distance_between_end_verts = (arc_end_verts[0].co - arc_end_verts[1].co).length
|
||||
arc_mid_vert = arc_end_verts[0].link_edges[0].other_vert(arc_end_verts[0])
|
||||
is_reflex = 0 if (arc_mid_vert.co - arc_end_verts[1].co).length < distance_between_end_verts else 1
|
||||
|
||||
bm.free()
|
||||
|
||||
# Calculate the angle
|
||||
# This is the true normal in 3D, whereas the camera projection we use is the drawing direction.
|
||||
# This assumes (because we use SVG arcs) that the radius is always orthogonal to our view.
|
||||
# When rewriting to use polylines, we should use this normal instead.
|
||||
# normal = mathutils.geometry.normal([arc_end_pts[0], arc_end_pts[1], center])
|
||||
normal = Vector(self.camera_projection)
|
||||
dir1 = (arc_end_pts[0] - center).normalized()
|
||||
dir2 = (arc_end_pts[1] - center).normalized()
|
||||
|
||||
# Let's get the matrix that represents the coordinate system of the arc.
|
||||
# This matrix allows us to get 2D vectors for calculating the signed arc angle.
|
||||
z = normal
|
||||
x = (arc_end_pts[0] - center).normalized()
|
||||
y = z.cross(x)
|
||||
arc_matrix = mathutils.Matrix([x, y, z]).transposed().to_4x4()
|
||||
|
||||
dir1 = ((arc_matrix.inverted() @ arc_end_pts[0]) - (arc_matrix.inverted() @ center)).normalized()
|
||||
dir2 = ((arc_matrix.inverted() @ arc_end_pts[1]) - (arc_matrix.inverted() @ center)).normalized()
|
||||
angle = -dir1.xy.angle_signed(dir2.xy)
|
||||
|
||||
#if is_reflex:
|
||||
# angle = angle % (math.pi * 2)
|
||||
|
||||
# Center of gravity of all vertices, used to help position the text
|
||||
cog = Vector((0, 0, 0))
|
||||
for vert in obj.data.vertices:
|
||||
cog += vert.co
|
||||
cog = matrix_world @ (cog / len(obj.data.vertices))
|
||||
|
||||
radius = ((matrix_world @ obj.data.vertices[0].co) - center).length
|
||||
|
||||
arc_midpoint = center + ((cog - center).normalized() * radius)
|
||||
|
||||
text_position = self.project_point_onto_camera(arc_midpoint)
|
||||
text_position = Vector(((x_offset + text_position.x) * self.scale, (y_offset - text_position.y) * self.scale))
|
||||
|
||||
center_projected = self.project_point_onto_camera(center)
|
||||
center_position = Vector(
|
||||
((x_offset + center_projected.x) * self.scale, (y_offset - center_projected.y) * self.scale)
|
||||
)
|
||||
text_offset = (text_position - center_position).xy.normalized() * 5
|
||||
text_position += text_offset
|
||||
|
||||
text_style = {
|
||||
"font-size": annotation.Annotator.get_svg_text_size(2.5),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
}
|
||||
|
||||
angle_text = abs(round(math.degrees(angle), 3))
|
||||
if is_reflex:
|
||||
angle_text = 360 - angle_text
|
||||
self.svg.add(self.svg.text(f"{angle_text}deg", insert=tuple(text_position), **text_style))
|
||||
|
||||
# Draw SVG arc, see for details: http://xahlee.info/js/svg_circle_arc.html
|
||||
arc_proj_end_pts = [self.project_point_onto_camera(v) for v in arc_end_pts]
|
||||
p1 = Vector(((x_offset + arc_proj_end_pts[0].x) * self.scale, (y_offset - arc_proj_end_pts[0].y) * self.scale))
|
||||
p2 = Vector(((x_offset + arc_proj_end_pts[1].x) * self.scale, (y_offset - arc_proj_end_pts[1].y) * self.scale))
|
||||
r = radius * self.scale
|
||||
# reflex = 1 if angle > math.pi else 0
|
||||
reflex = is_reflex
|
||||
if reflex:
|
||||
sense = 0 if angle > 0 else 1
|
||||
else:
|
||||
sense = 1 if angle > 0 else 0
|
||||
d = f"M {p1.x} {p1.y} A {r} {r} 0 {reflex} {sense} {p2.x} {p2.y}"
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
|
||||
def draw_radius_annotations(self, obj):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
@@ -303,10 +303,9 @@ class Cad:
|
||||
return bm
|
||||
|
||||
@classmethod
|
||||
def generate_3PT(cls, pts, obj, nv, mode=1):
|
||||
def get_center_of_arc(cls, pts, obj):
|
||||
mw = obj.matrix_world
|
||||
V = Vector
|
||||
nv = max(3, nv)
|
||||
|
||||
# construction
|
||||
v1, v2, v3, v4 = V(pts[0]), V(pts[1]), V(pts[1]), V(pts[2])
|
||||
|
||||
@@ -38,18 +38,19 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
@classmethod
|
||||
def create_annotation_object(cls, object_type):
|
||||
data_type = {
|
||||
"DIMENSION": "curve",
|
||||
"RADIUS": "curve",
|
||||
"ANGLE": "mesh",
|
||||
"BREAKLINE": "mesh",
|
||||
"DIAMETER": "curve",
|
||||
"DIMENSION": "curve",
|
||||
"FILL_AREA": "mesh",
|
||||
"HIDDEN_LINE": "mesh",
|
||||
"LINEWORK": "mesh",
|
||||
"PLAN_LEVEL": "curve",
|
||||
"RADIUS": "curve",
|
||||
"SECTION_LEVEL": "curve",
|
||||
"STAIR_ARROW": "curve",
|
||||
"TEXT": "empty",
|
||||
"TEXT_LEADER": "curve",
|
||||
"STAIR_ARROW": "curve",
|
||||
"HIDDEN_LINE": "mesh",
|
||||
"PLAN_LEVEL": "curve",
|
||||
"SECTION_LEVEL": "curve",
|
||||
"BREAKLINE": "mesh",
|
||||
"FILL_AREA": "mesh",
|
||||
"LINEWORK": "mesh",
|
||||
}[object_type]
|
||||
obj = annotation.Annotator.get_annotation_obj(object_type, data_type)
|
||||
if object_type == "FILL_AREA":
|
||||
|
||||
Reference in New Issue
Block a user