New "cut decorator" that draws cut lines in drawings.

This commit is contained in:
Dion Moult
2023-04-12 21:25:56 +10:00
parent cbe71f221c
commit 9170e5ca65
5 changed files with 171 additions and 23 deletions
@@ -30,6 +30,7 @@ def refresh():
SchedulesData.is_loaded = False
DrawingsData.is_loaded = False
DecoratorData.data = {}
DecoratorData.cut_cache = {}
class ProductAssignmentsData:
@@ -152,6 +153,7 @@ FONT_SIZES = {
class DecoratorData:
# stores 1 type of data per object
data = {}
cut_cache = {}
# used by Ifc Annotations with ObjectType = "BATTING"
@classmethod
@@ -30,6 +30,7 @@ from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from bpy_extras.view3d_utils import location_3d_to_region_2d
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
from blenderbim.bim.module.drawing.data import DecoratorData
from blenderbim.bim.module.drawing.shaders import BASE_LIB_GLSL, BASE_DEF_GLSL
@@ -242,7 +243,7 @@ class BaseDecorator:
vertices = [obj.matrix_world @ v.co for v in bm.verts]
return vertices, indices
def decorate(self, context, object):
def decorate(self, context, obj):
"""perform actual drawing stuff"""
raise NotImplementedError()
@@ -450,7 +451,7 @@ class DimensionDecorator(BaseDecorator):
layout(lines) in;
layout(triangle_strip, max_vertices=MAX_POINTS) out;
void oblique_dimension_head(in vec4 dir, in float size,
void oblique_dimension_head(in vec4 dir, in float size,
in float angle, out vec4 head[OBLIQUE_HEAD_VERTS]) {
float c = cos(angle), s = sin(angle);
vec4 ortho = vec4(-dir.y, dir.x, 0, 0);
@@ -495,22 +496,22 @@ class DimensionDecorator(BaseDecorator):
// stem
do_edge_verts(p0, p1);
EndPrimitive();
EndPrimitive();
} else {
vec4 head[3];
arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head);
// start edge arrow
do_edge_verts( p0, WIN2CLIP( (p0w + head[1]) ) );
do_edge_verts_win( p0w + head[1], p0w + head[2] );
do_edge_verts( WIN2CLIP( p0w + head[2] ), p0 );
do_edge_verts( p0, WIN2CLIP( (p0w + head[1]) ) );
do_edge_verts_win( p0w + head[1], p0w + head[2] );
do_edge_verts( WIN2CLIP( p0w + head[2] ), p0 );
EndPrimitive();
// end edge arrow
do_edge_verts( p1, WIN2CLIP( p1w - head[1] ) );
do_edge_verts_win( p1w - head[1], p1w - head[2] );
do_edge_verts( WIN2CLIP( p1w - head[2] ), p1 );
do_edge_verts( p1, WIN2CLIP( p1w - head[1] ) );
do_edge_verts_win( p1w - head[1], p1w - head[2] );
do_edge_verts( WIN2CLIP( p1w - head[2] ), p1 );
EndPrimitive();
// stem, with gaps for arrows
@@ -569,7 +570,7 @@ class AngleDecorator(BaseDecorator):
layout(triangle_strip, max_vertices=MAX_POINTS) out;
in uint type[];
in vec4 v_next_vert[];
// per edge shader
void main() {
// default setup for macro to work
@@ -981,7 +982,7 @@ class StairDecorator(BaseDecorator):
if (t0 == 1u) {
vec4 head[CIRCLE_SEGS];
circle_head(viewportDrawingScale * CIRCLE_SIZE, head);
for(int i=0; i<CIRCLE_SEGS-1; i++) {
do_edge_verts_win(p0w + head[i], p0w + head[i+1]);
}
@@ -1048,7 +1049,7 @@ class HiddenDecorator(BaseDecorator):
dist = 0;
do_vertex_win( p0w + gap, dir.xy);
dist = length(edge);
do_vertex_win( p1w - gap, dir.xy);
EndPrimitive();
@@ -1093,7 +1094,7 @@ class MiscDecorator(BaseDecorator):
layout(triangle_strip, max_vertices=MAX_POINTS) out;
out float dist; // distance from starging point along segment
void main() {
DEFAULT_SETUP();
@@ -1106,10 +1107,10 @@ class MiscDecorator(BaseDecorator):
vec4 p;
// NB: something should be used to affect position, otherwise compiler eliminates winsize
dist = 0;
do_vertex_win( p0w + gap, dir.xy);
dist = length(edge);
do_vertex_win( p1w - gap, dir.xy);
EndPrimitive();
@@ -1298,7 +1299,7 @@ class SectionLevelDecorator(LevelDecorator):
// stem
dist = 0;
do_vertex_win( p0w + gap, dir.xy);
dist = length(edge);
do_vertex_win( p1w - gap, dir.xy);
EndPrimitive();
@@ -1436,14 +1437,14 @@ class BattingDecorator(BaseDecorator):
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1), pmw = (p0w + p1w) * .5;
vec4 edge = p1w - p0w, dir = normalize(edge);
float segment_width = batting_thickness_winspace / 2.5;
vec2 batting_dimensions = vec2(segment_width, batting_thickness_winspace);
// need to multiply by viewportDrawingScale
// so the drawing will stay consistent on zoom in / zoom out
// but since we use winspace batting dimensions we skip it
mat2 m_edge_space = mat2( dir.xy, dir.yx*vec2(1,-1) );
// simplified rectangle + cross version to indicate batting
vec4 off_y = vec4(m_edge_space * ( vec2(0, 0.5) * batting_dimensions ), 0, 0);
do_edge_verts_win( p0w + off_y, p0w - off_y );
@@ -1467,7 +1468,7 @@ class BattingDecorator(BaseDecorator):
// pattern_segment_data[2] = vec2(0, 0.2);
// pattern_segment_data[3] = vec2(0.5, 0);
// pattern_segment_data[4] = vec2(1.0, 0.2);
// pattern_segment_data[5] = vec2(0.5, 0.8);
// pattern_segment_data[5] = vec2(0.5, 0.8);
// pattern_segment_data[6] = vec2(1.0, 1.0);
// zigzag pattern
// pattern_segment_data[0] = vec2(0, 1);
@@ -1830,7 +1831,7 @@ class TextDecorator(BaseDecorator):
vec4 p0 = gl_in[0].gl_Position;
vec4 p0w = CLIP2WIN(p0);
vec4 p;
vec4 head[CIRCLE_SEGS_ASTERISK];
circle_head_asterisk(viewportDrawingScale * CIRCLE_SIZE, head);
@@ -1887,6 +1888,147 @@ class TextDecorator(BaseDecorator):
line_i += 1
class CutDecorator:
installed = None
cache = {}
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def __call__(self, context):
for obj in self.get_objects(None):
self.decorate(context, obj)
def get_objects(self, collection):
return [o for o in bpy.context.visible_objects if o.type == "MESH"]
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def decorate(self, context, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
all_vertices, all_edges = None, None
else:
all_vertices, all_edges = DecoratorData.cut_cache.get(element.id(), (None, None))
if all_vertices is False:
return
if not self.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.cut_cache[element.id()] = (False, False)
return
if all_vertices is None:
all_vertices, all_edges = self.bisect_mesh(obj, context.scene.camera)
DecoratorData.cut_cache[element.id()] = (all_vertices, all_edges)
gpu.state.point_size_set(2)
gpu.state.blend_set("ALPHA")
### Actually drawing
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 3.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader.bind()
green = (0.545, 0.863, 0, 1)
white = (0, 0, 0, 1)
color = green if obj.select_get() else white
self.draw_batch("LINES", all_vertices, color, all_edges)
self.draw_batch("POINTS", all_vertices, color)
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 = [
DimensionDecorator,
@@ -23,8 +23,7 @@ from bpy.app.handlers import persistent
@persistent
def toggleDecorationsOnLoad(*args):
toggle = bpy.context.scene.DocProperties.should_draw_decorations
if toggle:
if bpy.context.scene.DocProperties.should_draw_decorations:
decoration.DecorationsHandler.install(bpy.context)
else:
decoration.DecorationsHandler.uninstall()
@@ -39,6 +39,7 @@ import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.sheeter as sheeter
import blenderbim.bim.module.drawing.scheduler as scheduler
import blenderbim.bim.module.drawing.helper as helper
from blenderbim.bim.module.drawing.decoration import CutDecorator
from blenderbim.bim.module.drawing.data import DecoratorData
import blenderbim.bim.export_ifc
from lxml import etree
@@ -989,11 +990,13 @@ class ActivateView(bpy.types.Operator):
drawing: bpy.props.IntProperty()
def execute(self, context):
core.activate_drawing_view(tool.Ifc, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
drawing = tool.Ifc.get().by_id(self.drawing)
core.activate_drawing_view(tool.Ifc, tool.Drawing, drawing=drawing)
bpy.context.scene.DocProperties.active_drawing_id = self.drawing
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.activate_drawing_style()
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
CutDecorator.install(context)
return {"FINISHED"}
@@ -117,6 +117,7 @@ class Drawing(blenderbim.core.tool.Drawing):
camera.location = (0, 0, 1.5) # The view shall be 1.5m above the origin
camera.data.type = "ORTHO"
camera.data.ortho_scale = 50 # The default of 6m is too small
camera.data.clip_start = 0.002 # 2mm is close to zero but allows any GPU-drawn lines to be visible.
camera.data.clip_end = 10 # A slightly more reasonable default
if bpy.context.scene.unit_settings.system == "IMPERIAL":
camera.data.BIMCameraProperties.diagram_scale = '1/8"=1\'-0"|1/96'
@@ -495,6 +496,7 @@ class Drawing(blenderbim.core.tool.Drawing):
camera = bpy.data.cameras.new(tool.Loader.get_mesh_name(geometry))
camera.type = "ORTHO"
camera.ortho_scale = width if width > height else height
camera.clip_start = 0.002
camera.clip_end = depth
if width > height: