Python module that get the coords of the element in 3d view closesest to mouse cursor

For a long time, coders wish for a function in Blender python API that allows snapping to the element closest to a user-defined screen position.

For a while I propose changes to Blender’s source code that would allow this feature to be added: (https://developer.blender.org/differential/query/Om7buplJQZEJ/#R)

But there are a lot of changes and developers do not feel safe in apply all of them.
So I decided to make a snap module totally in python.
The disadvantage of python is that getting the coordinates of the object is extremely slow.
The solution I had was to store these coordinates in the GPU and do the drawing of the elements to snap with OpenGL in a gpu.offscreen

Attached are the module and a simple addon that makes use of the module.

To use the addon simply search for the “Snap” Operator using the Spacebar.
The 3D cursor will snap to nearby elements. (That’s the only thing the addon does)

https://lh3.googleusercontent.com/-83FZQ2Z8STs/WbICa1KjQsI/AAAAAAAACMQ/4G3kMBss3HETw3JueRrlb11R3Yknz3I8gCL0BGAs/w525-d-h396-rw/snap_opengl2.gif

It is still in a very very early stage. It still needs testing and improvements on API.
I’ll be happy with community testing, and I’ll try to fix any bugs that are found.

Any suggestions are welcome :slight_smile:

This module and addon was made for Blender 2.79
Here is a manual on how to install addons:
https://docs.blender.org/manual/fi/dev/preferences/addons.html

Attachments

snap_context.zip (13.1 KB)space_view3d_snap.zip (15.7 KB)

Hi Mano-Wii,

Good to see you back at developing a better snapping system :slight_smile: Would it be possible to return the datablock and datablock element (face, edge or vert index) together with the coordinates and the normal?

Oh a very good idea Mano-wii,
you are a Maestro!
Thanks a lot and congratulations!
Spirou4D

what about using bvhtree for snapping?

mathutils.bvhtree is very fast once the tree is generated, I noted in a few openCL repo that you can build these trees very fast on the gpu, perhaps we can accelerate this and get that accepted?


Error…

Actually, it is only possible to return the indices of the vertices :\

With the “SnapContext” created (let’s call it sctx), the “sctx.snap_get” method returns three information:

  • The object;
  • the coordinate of the snap;
  • and a tuple with 1, 2 or 3 indices depending on the snap element (VERT, EDGE, TRI).

But it is possible to add another return depending on the need, it would be a tuple that returns 1, 2 or 3 coordinates (depending on the type of element)

How bmesh It is possible to reobtain the edge with the indices of the two vertices that compose it:


snp_obj, loc, elem = sctx.snap_get(mcursor)
if len(elem) == 2:
        v1 = bm.verts[elem[0]]
        v2 = bm.verts[elem[1]]
        bm_geom = bm.edges.get([v1, v2])

BHTree really would be practical to make raycast and get the element closest to the cursor.
However with it it would not be possible to simulate the occlusion efficiently (ignoring the elements behind a face).
BVHTree also takes up a lot of RAM. Another reason I chose OpenGL to store GPU arrays.

@Darcvizer, thank you very much for testing. :)On blender prompt, you will see more information that would be very useful to know the reason for the error.

This information is above the error message. Could you take a look?

Hey mano-wii! Looks interesting, but I’m getting mostly the same error as Darcvizer, link to full console log. :frowning:

Just a note, other than adding a GIF, I would also recommend adding some install instructions so you can get a wider variety of testers, eg:

This is an addon/script/module/thingy for Blender 2.80
Extract the “space_view3d_snap.zip” file to your “2.80/insert/directory/name/here” directory
Extract the “snap_context.zip” file to your “2.80/insert/directory/name/here” directory

Thank you for your suggestions. I added the gif. And I hope I have fixed the problem.
The attachments have been updated. Ready for testing :wink:

This line in error explain what is happening:

Error: Shader compilation failed: ERROR: 0:11: ‘flat’ : syntax error syntax error

Just remembering that the module was made for blender 2.79. (I missed this in bl_info)

Nice GIF! Just tried new version with 2.79 RC2 and latest 2.8 daily. Not working with either.

Different error this time though:

'blender-2.79-rc2-windows64\\2.79\\scripts\\addons\\snap_context\\__init__.py'
Error: Shader compilation failed: ERROR: 0:11: 'varying' : cannot be int
ERROR: 0:? : 'gl_VertexID' : variable is not available in current GLSL version

Hi,
Realy nice one !

Does it work with curves ?
Any way to add some constraints, snap as it currently does, but project the cursor location into plane / line, or do we have ot handle this part externally ?

Excellent effort mano-wii! Finally getting close to controllable snap api.
Unfortunately, i am having the same installation error as Darcvizer and nBurn

Well Pyhon is far more powerful language anyway than C/C++ , so the more code we have in Python the better, because the Blender Python API is kinda limiting at times. Frankly I am completely against non performance orientated features be integrated inside the Blender source (see pie menus). But that is quite complex discussion deserving a thread by itself.

Attached are the module and a simple addon that makes use of the module.

To use the addon simply search for the “Snap” Operator using the Spacebar.
The 3D cursor will snap to nearby elements. (That’s the only thing the addon does)

It is still in a very very early stage. It still needs testing and improvements on API.
I’ll be happy with community testing, and I’ll try to fix any bugs that are found.

Any suggestions are welcome :slight_smile:

This module and addon was made for Blender 2.79
Here is a manual on how to install addons:
https://docs.blender.org/manual/fi/dev/preferences/addons.html

First super cool stuff there, well done.

I also battle with BGL and OpenGL and I know it can be a huge pain at times trying to figure things out. So deep respect for your effort.

I have looked at the code or tried this yet but my only suggestion at the time is to add it as icon in the snap tool icons and of course inside the snap menu so it does not feel like an “outside” thing.

Cool, thanks. Is there an efficient way to get the faces that have the 3 vert of the tri?

Module and addon updated to fix bugs :slight_smile: (I’ll add version in the next to facilitate).

Curve support will be added in the future once the module is working properly on all platforms.
Constraints must be outside the module. It’s up to the programmer. The module itself should be as basic as possible.

@kilon, thanks for the suggestion. Since the addon was made just for testing and as an example to help coders use the module, it should be as simple as possible.
So only the operator is enough.

@bliblubli, my solution was this:


if len(elem) == 3:
        tri = [
            bm.verts[elem[0]],
            bm.verts[elem[1]],
            bm.verts[elem[2]],
        ]


        faces = set(tri[0].link_faces).intersection(tri[1].link_faces, tri[2].link_faces)
        if len(faces) == 1:
            bm_geom = faces.pop()
        else:
            i = -2
            edge = None
            while not edge and i != 1:
                edge = bm.edges.get([tri[i], tri[i + 1]])
                i += 1
            if edge:
                for l in edge.link_loops:
                    if l.link_loop_next.vert == tri[i] or l.link_loop_prev.vert == tri[i - 2]:
                        bm_geom = l.face
                        break

But I still don’t know if it’s the most efficient

can it snap middle of the faces and edges?

With a few changes, the code can return the coordinates of the vertices of the element:

@@ -117,11 +117,11 @@ class SnapContext(): 
         if gpu_data.draw_tris:
             if index < snap_obj.data[1].num_tris:
                 tri_verts = gpu_data.get_tri_verts(index)
                 tri_co = [snap_obj.mat * Vector(v) for v in gpu_data.get_tri_co(index)]
-                return intersect_ray_tri(*tri_co, *self.last_ray, False), tri_verts
+                return intersect_ray_tri(*tri_co, *self.last_ray, False), tri_verts, tri_co
 
             index -= gpu_data.num_tris
 
         if gpu_data.draw_edges:
             if index < snap_obj.data[1].num_edges:
@@ -132,28 +132,29 @@ class SnapContext():
                 if (self._snap_mode) & VERT and (fac < 0.25 or fac > 0.75):
                     co = edge_co[0] if fac < 0.5 else edge_co[1]
                     proj_co = project_co_v3(self, co)
                     dist = self.mval - proj_co
                     if abs(dist.x) < self._dist_px and abs(dist.y) < self._dist_px:
-                        return co, (edge_verts[0] if fac < 0.5 else edge_verts[1],)
+                        return co, (edge_verts[0] if fac < 0.5 else edge_verts[1],), co
 
                 if fac <= 0.0:
                     co = edge_co[0]
                 elif fac >= 1.0:
                     co = edge_co[1]
                 else:
                     co = edge_co[0] + fac * (edge_co[1] - edge_co[0])
 
-                return co, edge_verts
+                return co, edge_verts, edge_co
 
             index -= gpu_data.num_edges
 
         if gpu_data.draw_verts:
             if index < snap_obj.data[1].num_verts:
-                return snap_obj.mat * Vector(gpu_data.get_loosevert_co(index)), (gpu_data.get_loosevert_index(index),)
+                co = Vector(gpu_data.get_loosevert_co(index))
+                return snap_obj.mat * co, (gpu_data.get_loosevert_index(index),), (co, )
 
-        return None, None
+        return None, None, None
 
 
     def _get_snap_obj_by_obj(self, obj):
         for snap_obj in self.snap_objects:
             if obj == snap_obj.data[0]:
@@ -224,11 +225,11 @@ class SnapContext():
     def get_ray(self, mval):
         self.last_ray = _get_ray(self.region, self.rv3d, mval)
         return self.last_ray
 
     def snap_get(self, mval):
-        ret = None, None
+        ret = None, None, None
         self.mval[:] = mval
         snap_vert = self._snap_mode & VERT != 0
         snap_edge = self._snap_mode & EDGE != 0
         snap_face = self._snap_mode & FACE != 0
 
@@ -287,11 +288,11 @@ class SnapContext():
             ret = self._get_loc(snap_obj, index)
 
         self._offscreen.unbind()
         gpu_Indices_restore_state()
 
-        return snap_obj, ret[0], ret[1]
+        return snap_obj, ret[0], ret[1], ret[2]
 
     def free(self):
         self.__del__()
         self.freed = True
 

With the coordinates of the edge, just get the middle, project it and test if it is within the threshold.


from snap_context.utils_projection import project_co_v3
sctx = ... (create snap context)

snp_obj, loc, elem, elem_co = sctx.snap_get(mcursor)

if len(elem_co) == 2:
    middle = snp_obj.mat * (0.5 * (elem_co[0] + elem_co[1]))
    proj_mid = project_co_v3(sctx, middle)
    dist = mcursor - proj_mid
    if abs(dist.x) < sctx._dist_px and abs(dist.y) < sctx._dist_px:
       print("snap to the middle of the edge", middle)
    

Getting the middle of the faces is more complicated. I fear only be possible for bmesh

thank you guy… I’m looking snap codes inside various addons and this can simplify so much cad like modeling tools addons. Curve (with interactive sampling adjust), origin(pivot) and center snap can be wonderful future extras.

Hello. Can I use this for my unfinished addon? If yes, then perhaps you could give me a couple of tips? Thanks a lot