4
$\begingroup$

I'm trying to find a point on an edge closest to 3d cursor with .intersect_point_line() but I'm getting a wrong point as a result, here I create an Empty in the result coordinate.

enter image description here

What am I doing wrong? When I'm printing out the results the distances are correct but it looks like verts indices are wrong..? My code:

import bpy
import bmesh
import math
import mathutils

def main():
    ob = bpy.context.view_layer.objects.active
    if not bpy.context.view_layer.objects.active:
        return
    me = ob.data
    bm = bmesh.new()
    bm.from_mesh(me)

    cursor_pos= bpy.context.scene.cursor.location.copy()
    bm.faces.ensure_lookup_table()
        
    print("start")
    print("cursor_location: ", cursor_pos)

    dist = 1000000
    good_res = None
    
    for index, edge in enumerate(bm.faces[0].edges):
        res = mathutils.geometry.intersect_point_line(cursor_pos, edge.verts[0].co, edge.verts[1].co)
        if res[1] < dist:
            dist = res[1]
            good_res = res[0]

    bpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD', location=good_res, scale=(1, 1, 1))

main()
$\endgroup$
7
$\begingroup$

Global coords, and calculate the distance.

Firstly mesh coordinates are local and the scene cursor location is global. Either convert scene cursor to local

scene_cursor_local = ob.matrix_world.inverted() @ scene.cursor.location

and then the result back to global when adding the empty.

ob.matrix_world @ good_pos

This would require only 2 matrix multiplications.

or convert the mesh coordinates to global (have done this below)

bm.transform(ob.matrix_world)

since we are not writing back to the bmesh so it will not be reflected permanently.

What closest point on line does.

>>> geometry.intersect_point_line(
intersect_point_line(pt, line_p1, line_p2)
.. function:: intersect_point_line(pt, line_p1, line_p2)
Takes a point and a line and returns a tuple with the closest point on the line and its distance from the first point of the line as a percentage of the length of the line.
:arg pt: Point
:type pt: :class:`mathutils.Vector`
:arg line_p1: First point of the line
:type line_p1: :class:`mathutils.Vector`
:arg line_p1: Second point of the line
:type line_p1: :class:`mathutils.Vector`
:rtype: (:class:`mathutils.Vector`, float)

Test runs.

>>> geometry.intersect_point_line((0, 0, 1), (0, 0, 0), (0, 0, 2))
(Vector((0.0, 0.0, 1.0)), 0.5)

>>> geometry.intersect_point_line((0, 1, 1), (0, 0, 0), (0, 0, 2))
(Vector((0.0, 0.0, 1.0)), 0.5)

>>> 

It finds returns the point on the line closest to the point, and what percentage this is from the start point, ie 0 is the start point, 1 the other end. It does not go outside the line.

In example above notice both have the same result, whereas one is right in the middle of the line, the other is not. Also evident in question image, result is least ratio from corner aka edge vert 0. Flipping the normals will change the vertex order will again shift that result, if it is 10% from corner (as is) will be 90% way with verts wound the other way.

Instead calculate the distance by subtracting the hit from the cursor location.

Test code:

Have changed up code to add empty at closes point of all edges, rather than only that of zeroth face.

import bpy
import bmesh
from mathutils.geometry import intersect_point_line as ipl

def main(scene, ob):
    me = ob.data
    bm = bmesh.new()
    bm.from_mesh(me)
    bm.transform(ob.matrix_world) # put in global coords
    cursor_pos = scene.cursor.location.copy()
        
    print("start")
    print("cursor_location: ", cursor_pos)

    dist = 1000000
    good_res = None
    
    for edge in bm.edges:
        hit, pc = ipl(cursor_pos, edge.verts[0].co, edge.verts[1].co)
        d = (cursor_pos - hit).length
        if d < dist:
            dist = d
            good_res = hit

    bpy.ops.object.empty_add(
        type='PLAIN_AXES', 
        align='WORLD', 
        location=good_res, 
        )

main(bpy.context.scene, bpy.context.object)

Other constructs for min / max

Instead of the set a super high value as max and test during each iteration, there are other ways to find a minimum and maximum (numpy is extremely good at this)

Another is to make a list comprehension of all the hits and sort them. The zeroth will be the closest, the last the furthest away.

    hits = sorted(
        [ipl(cursor_pos, edge.verts[0].co, edge.verts[1].co)[0]
        for edge in bm.edges],
        key = lambda p : (p - cursor_pos).length
        )
    

    bpy.ops.object.empty_add(
        type='PLAIN_AXES', 
        align='WORLD', 
        location=hits[0], 
        )
$\endgroup$
2
  • 3
    $\begingroup$ Ahhh, I didn't read the function description properly! thank you so much (and thank you for sort example, I've been struggling with min-max) $\endgroup$ Oct 12 '20 at 12:29
  • 2
    $\begingroup$ Pleasure. Good question, you've had a go, demonstrated result and expected result. $\endgroup$
    – batFINGER
    Oct 12 '20 at 12:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.