# Python Algorithm for points snapped to a mesh

does somebody know the algorithm or has an idea for this problem: I want to draw line (2d or 3d space) into the viewport and want to project points onto a mesh that are snapped around the covered object (line and edge loop) - here an image for better understanding:

The contours addon does this, but I cant really extract the algorithm from the code.

Thx!

EDIT: With the operator from "X Y" we get this result when objects get more complex, but ok, looks like the points are all on the object's surface.

When I just drae the points it looks alright, but the distance of the points depends on the resolution of the mesh - it should be a constant distance:

I guess simple solution is to just add the point if the distance to a previous point is greater than a certain value. Algorithm could get quite slow for really high poly meshes :-)

• object.ray_cast() can do that. Here is the api link: docs.blender.org/api/current/…
– X Y
Mar 13, 2022 at 18:07
• Yes, but I was asking about an algorith to wrap points around a mesh. Problems that are to solve here: How to find the next points on the mesh, the direction, the distance and so on. Mar 14, 2022 at 10:22
• The display line through the mesh because the order of the faces in the mesh is not stable. There has a easy way to fix it: make the batch draw separated, every batch draw one line only.
– X Y
Mar 16, 2022 at 11:13
• If you want to draw a curve with the gpu module in viewport. It will become more hard to do. You need find the correct orders or the points and may be do a Bézier curve algorithm to show in the viewport.
– X Y
Mar 16, 2022 at 11:23
• My idea is to start with the plane (X1, X2, Viewport), then raycast from X1 onto the mesh, and then again from the hit point in the same direction. Then find the center of the two points and regard this as center. Then from the center raycast in a circle (n times) and collect the hit points. Voila. Mar 17, 2022 at 4:44

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 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 = []

def draw_line():

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:
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.area.tag_redraw()
return {'RUNNING_MODAL'}
else:
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).
"""

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)

# 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')

def register():
bpy.utils.register_class(CUT_OP)

wm = bpy.context.window_manager
if kc:
kmi = km.keymap_items.new("view3d.test_operator", type='F10', value='PRESS', ctrl=True)

def unregister():
bpy.utils.unregister_class(CUT_OP)

# Remove the hotkey