To make sure the operator is work, after run the script, you need select your object in object mode.
Make sure [Apply Object Transform].
Then press [Ctrl F10] on your keyboard to call the operator.
Suppose you want to get the intersection points after a cut.
Here is the result after finish the operator in the script.
#blender ver: 2.93.3
import bpy, bgl, blf, gpu, bmesh
import numpy as np
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import region_2d_to_location_3d as vec_2d_to_3d
def view3d_find( return_area = False ):
# returns first 3d view, normally we get from context
for area in bpy.context.window.screen.areas:
if area.type == 'VIEW_3D':
v3d = area.spaces[0]
rv3d = v3d.region_3d
for region in area.regions:
if region.type == 'WINDOW':
if return_area: return region, rv3d, v3d, area
return region, rv3d, v3d
return None, None
def draw_callback_px():
font_id = 0 # XXX, need to find out how best to get this.
# draw some text
blf.position(font_id, 15, 30, 0)
blf.size(font_id, 20, 72)
blf.color(font_id, 1,1,1,1)
blf.draw(font_id, CUT_OP.blf_info)
coords = []
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
def draw_line():
shader.bind()
shader.uniform_float("color", (1, 1, 0, 1))
batch = batch_for_shader(shader, 'LINES', {"pos": coords})
batch.draw(shader)
class CUT_OP(bpy.types.Operator):
bl_idname = "view3d.test_operator"
bl_label = "Simple Modal View3D Operator"
draw_handler = None
draw_handler_3d = None
blf_info = ""
def reg_handler(self):
CUT_OP.draw_handler = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, (), 'WINDOW', 'POST_PIXEL')
CUT_OP.draw_handler_3d = bpy.types.SpaceView3D.draw_handler_add(draw_line, (), 'WINDOW', 'POST_VIEW')
def kill_handler(self):
bpy.types.SpaceView3D.draw_handler_remove(CUT_OP.draw_handler, 'WINDOW')
bpy.types.SpaceView3D.draw_handler_remove(CUT_OP.draw_handler_3d, 'WINDOW')
CUT_OP.draw_handler = None
CUT_OP.draw_handler_3D = None
def modal(self, context, event):
context.area.tag_redraw()
if event.type in {'ESC', 'RIGHTMOUSE'}: return {'CANCELLED'}
if event.type == 'SPACE' and event.value == 'PRESS':
if CUT_OP.draw_handler is None: self.reg_handler()
else: self.kill_handler()
if self.def_modal(event): return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if not context.object:
self.report({'WARNING'}, "context object not found, select a object first.")
return {'CANCELLED'}
if context.area.type == 'VIEW_3D':
# Add the region OpenGL drawing callback
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
if CUT_OP.draw_handler is None: self.reg_handler()
CUT_OP.blf_info = "Left Click to get first point, SPACE to remove/add draw_handler."
coords.clear()
self.def_modal = self.modal_get_first_click
r3d, rv3d, v3d = view3d_find()
self.rv3d = rv3d
context.window_manager.modal_handler_add(self)
context.area.tag_redraw()
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
def upd_coord(self, event):
xy = (event.mouse_region_x, event.mouse_region_y)
p = vec_2d_to_3d(bpy.context.region, self.rv3d, xy, (0, 0, 0))
if len(coords) == 2: del coords[-1]
coords.append(p)
def modal_get_first_click(self, event):
if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
xy = (event.mouse_region_x, event.mouse_region_y)
print("xy: ", xy)
p = vec_2d_to_3d(bpy.context.region, self.rv3d, xy, (0, 0, 0))
print("p: ",p)
coords.append(p)
CUT_OP.blf_info = "Left Click to get second point"
self.def_modal = self.modal_get_second_click
def modal_get_second_click(self, event):
if event.type == 'MOUSEMOVE': self.upd_coord(event)
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
self.upd_coord(event)
calc_intersection_point(self.rv3d)
CUT_OP.blf_info = "call the operatorion again and press Space to remove the text and line."
return True
def calc_intersection_point(rv3d):
'''
The idea is:
First, get the projection plane from view_matrix and coords,
and then for every faces in the object, find the interection point from all line,
it will find 0 or 2 point(s) for every faces in most case, sometime it find 3 or 4 if on the corner.
'''
print("-- calc part --")
def get_view_matrix(rv3d): #Error if rv3d not find
rot = rv3d.view_rotation.to_euler()
view_matrix = rv3d.view_matrix
x, y, z = view_matrix.to_3x3()
print("view_matrix 3x3: ", x, y, z)
return x, y, z
def is_inside(p, p0, p1): # return True if p is inside the line
if not (p0[0] <= p[0] <= p1[0] or p1[0] <= p[0] <= p0[0]): return False
if not (p0[1] <= p[1] <= p1[1] or p1[1] <= p[1] <= p0[1]): return False
if not (p0[2] <= p[2] <= p1[2] or p1[2] <= p[2] <= p0[2]): return False
return True
def isect_line_plane_v3(p0, p1, p_co, p_no, epsilon=1e-6):
"""
p0, p1: Define the line.
p_co, p_no: define the plane:
p_co Is a point on the plane (plane coordinate).
p_no Is a normal vector defining the plane direction;
(does not need to be normalized).
Return a Vector or None (when the intersection can't be found).
"""
def add_v3v3(v0, v1):
return (
v0[0] + v1[0],
v0[1] + v1[1],
v0[2] + v1[2],
)
def sub_v3v3(v0, v1):
return (
v0[0] - v1[0],
v0[1] - v1[1],
v0[2] - v1[2],
)
def dot_v3v3(v0, v1):
return (
(v0[0] * v1[0]) +
(v0[1] * v1[1]) +
(v0[2] * v1[2])
)
def len_squared_v3(v0):
return dot_v3v3(v0, v0)
def mul_v3_fl(v0, f):
return (
v0[0] * f,
v0[1] * f,
v0[2] * f,
)
u = sub_v3v3(p1, p0)
dot = dot_v3v3(p_no, u)
if abs(dot) > epsilon:
# The factor of the point between p0 -> p1 (0 - 1)
# if 'fac' is between (0 - 1) the point intersects with the segment.
# Otherwise:
# < 0.0: behind p0.
# > 1.0: infront of p1.
w = sub_v3v3(p0, p_co)
fac = -dot_v3v3(p_no, w) / dot
u = mul_v3_fl(u, fac)
return add_v3v3(p0, u)
# The segment is parallel to plane.
return None
vx, vy, vz = get_view_matrix(rv3d)
#get normal of projection plane from 2 direction
co0, co1 = coords[0], coords[1]
v1, v2 = co1 - co0, vz
print("v1, v2: ", v1, v2)
nor = np.cross(v1, v2) #cross product of v1, v2
print("plane normal: ", nor)
#get context object bmesh
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(bpy.context.object.data)
points = []
coords.clear()
def fn_append2_draw_coords(p):
coords.append(p)
coords.append(p)
def fn_append_draw_coords(p):
nonlocal sw_fn_append_draw_coords
coords.append(p)
sw_fn_append_draw_coords = fn_append2_draw_coords
sw_fn_append_draw_coords = fn_append_draw_coords #change the fn when used
for face in bm.faces:
for edge in face.edges:
# find interection point from edge and projection plane
p1, p2 = edge.verts[0].co, edge.verts[1].co
p = isect_line_plane_v3(p1, p2, co1, nor)
if p is None: continue # no intersection
if is_inside(p, p1, p2): # if p on the line
points.append(p)
sw_fn_append_draw_coords(p)
print("The result: ", points)
bpy.ops.object.mode_set(mode='OBJECT')
addon_keymaps = []
def register():
bpy.utils.register_class(CUT_OP)
# Add the hotkey
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
km = wm.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D')
kmi = km.keymap_items.new("view3d.test_operator", type='F10', value='PRESS', ctrl=True)
addon_keymaps.append((km, kmi))
def unregister():
bpy.utils.unregister_class(CUT_OP)
# Remove the hotkey
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
if __name__ == "__main__":
register()