Solved How to setup c4d.utils.ViewportSelect() and GetNearestPoint to work

I found this thread giving me a RuntimeError: Object is not initialized yet for c4d.utils.ViewportSelect()

Re: utils.ViewportSelect() Problem
But no beginner solution just a hunch in this thread.

Anyway I can't get the SDK example to post any results I guess I am missing some crucial setup of the function itself. I have a Mousinput (mx, my) which I would like to feed into this util and get the GetNearestPoint

def MouseInput(self, doc, data, bd, win, msg):
        # Retrieves which clicks is currently clicked
        device = 0
        if msg[c4d.BFM_INPUT_CHANNEL ]== c4d.BFM_INPUT_MOUSELEFT:
            device = c4d.KEY_MLEFT
        else:
            return True

        # Retrieves the X/Y screen position of the mouse.
        mx = msg[c4d.BFM_INPUT_X]
        my = msg[c4d.BFM_INPUT_Y]

        # adds the position to the array
        myPoint = c4d.Vector (mx , my, 0)
        self.lines.append(myPoint)
        
        vpSelect = c4d.utils.ViewportSelect()    # Error thrown here 
        infoPoint = vpSelect.GetPixelInfoPoint(mx, my)
        
        if not infoPoint: 
                return
        i = infoPoint["i"]  # int
        op = infoPoint["op"]  # c4dBaseObject
        z = infoPoint["z"]  # float

        # Pushes an update event to Cinema 4D
        c4d.EventAdd()
        return True

Goal would be to get the Coordinates of the Point.

Thank you for your time.
mogh

hi ,
your problem is more a design problem than a technical one.

you should use the GetCursorInfo to retrieve the information of the mouse and check there what are the object / point you are overlapping.

Something that could help you is the static function (static mean you don't need an instance of that object to call/use it) PickObject.

You can use that function to retrieve the object you have under the mouse, and after that you can use GetNearestPoint to retrieve the points for each object (or the closest as i did).

once you have collected those information you have to draw them. (I've used a Vector(-1) to see if my coordinates is updated or not, but a bool would be better.

I've update my example (without your addition sorry). If i remember correctly you were trying to draw a line between two points

of course i don't check for example if the object is a polygon object or not and some other check that should be there.



import c4d
import os

# Be sure to use a unique ID obtained from www.plugincafe.com
PLUGIN_ID = 1000001


class SettingsDialog(c4d.gui.SubDialog):
    """
    Dialog to display option in the ToolData, in this case the Sphere size.
    """
    parameters = {}

    def __init__(self, arg):
        # Checks if the argument passed is a dictionary
        if not isinstance(arg, dict):
            raise TypeError("arg is not a dict.")

        self.parameters = arg

    def CreateLayout(self):
        """
        This Method is called automatically when Cinema 4D Create the Layout (display) of the GeDialog.
        """
        value = self.parameters["line_color"]
        radius = self.parameters['radius']
        if self.GroupBegin(id=1000, flags=c4d.BFH_SCALEFIT, cols=1, rows=1):
            self.GroupBorderSpace(10, 10, 10, 10)

            # Creates a Static text and a number input
            self.AddColorChooser(id=1001, flags=c4d.BFV_TOP | c4d.BFH_LEFT, initw=120,  layoutflags=c4d.DR_COLORFIELD_NO_BRIGHTNESS)
            if self.GroupBegin(id=1000, flags=c4d.BFH_SCALEFIT, cols=2, rows=1):
                # Creates a Static text and a number input
                self.AddStaticText(id=1002, flags=c4d.BFH_MASK, initw=120, name="Radius", borderstyle=c4d.BORDER_NONE)
                self.AddEditNumberArrows(id=1003, flags=c4d.BFH_MASK)
            self.GroupEnd()

               # Defines the default values
            self.SetFloat(id=1003, value=radius, min=0, max=300)
            # Defines the default values
            self.SetColorField(id=1001, color=value, brightness = 1.0, maxbrightness = 1.0, flags = 0)
        self.GroupEnd()
        return True

    def Command(self, messageId, msg):
        """
          This Method is called automatically when the user clicks on a gadget and/or changes its value this function will be called.
          It is also called when a string menu item is selected.
         :param messageId: The ID of the gadget that triggered the event.
         :type messageId: int
         :param bc: The original message container
         :type bc: c4d.BaseContainer
         :return: False if there was an error, otherwise True.
         """
        # When the user change the Gadget with the ID 1002 (the input number field)

        if messageId == 1001:
            # Stores the value in the parameter variable
            self.parameters["line_color"] = self.GetColorField(1001)["color"]
            # Update the view to redraw the lines
            c4d.DrawViews(c4d.DA_ONLY_ACTIVE_VIEW | c4d.DA_NO_THREAD | c4d.DA_NO_ANIMATION)
        elif messageId == 1003:
            self.parameters['radius'] = self.GetFloat(1003)
        
        return True


class DrawLine(c4d.plugins.ToolData):
    """Inherit from ToolData to create your own tool"""
    def __init__(self):
        self.lines = []
        self.mouseCoordinate = c4d.Vector(-1) # needed to draw a line to the mouse cursor
        self.circleCoordinate = c4d.Vector(-1) # will serve to draw the circle (spanned point)
        self.data = {"line_color":c4d.Vector(0, 0, 1), "radius": 20.0}


    def GetState(self, doc):
        """
        Called by Cinema 4D to know if the tool can be used currently
        :param doc: The current active document.
        :type doc: c4d.documents.BaseDocument
        :return: True if the tool can be used, otherwise False.
        """
        return c4d.CMD_ENABLED

    def MouseInput(self, doc, data, bd, win, msg):
        """
        Called by Cinema 4D, when the user click on the viewport and the tool is active.
        Mainly the place to do moue interaction from the viewport.
        :param doc: The current active document.
        :type doc: c4d.documents.BaseDocument
        :param data:  The tool settings container.
        :type data: c4d.BaseContainer
        :param bd:  The BaseDraw object of the active editor view.
        :type bd: c4d.BaseDraw
        :param win: The EditorWindow object for the active editor view.
        :type win: c4d.gui.EditorWindow
        :param msg: The original message container.
        :type msg: c4d.BaseContainer
        :return: False if a problem occurred during this function.
        """
        # Retrieves which clicks is currently clicked
        device = 0
        if msg[c4d.BFM_INPUT_CHANNEL ] == c4d.BFM_INPUT_MOUSELEFT:
            device = c4d.KEY_MLEFT
        else:
            return True
   
        self.lines.append(self.circleCoordinate)
             
        
        # Pushes an update event to Cinema 4D
        c4d.EventAdd()
        return True

    def Draw(self, doc, data, bd, bh, bt, flags):
        """
        Called by Cinema 4d when the display is updated to display some visual element of your tool in the 3D view.
        :param doc: The current active document.
        :type doc: c4d.documents.BaseDocument
        :param data: The tool settings container.
        :type data: c4d.BaseContainer.
        :param bd: The editor's view.
        :type bd: c4d.BaseDraw
        :param bh: The BaseDrawHelp editor's view.
        :type bh: c4d.plugins.BaseDrawHelp
        :param bt: The calling thread.
        :type bt: c4d.threading.BaseThread
        :param flags: The current drawing pass.
        :type flags: TOOLDRAWFLAGS
        :return: The result of the drawing (most likely c4d.DRAWRESULT_OK)
        """

        # Resets the drawing matrix to the world space matrix.
        bd.SetMatrix_Matrix(None, c4d.Matrix())
        


        # set the color for already drawn lines
        color = self.data["line_color"]
        bd.SetPen(color)
        
        if not flags:
            # draw lines for each group of two points
            for pa, pb in zip(self.lines[::2], self.lines[1::2]):
                #bd.DrawLine2D(bd.WS(pa), bd.WS(pb))
                bd.DrawLine(pa, pb, c4d.NOCLIP_Z | c4d.NOCLIP_D)

      
        #Draw the circle
        
        if self.circleCoordinate == c4d.Vector(-1):
            # draw the circle around the mouse in red
            bd.SetPen(c4d.Vector(1, 0, 0))
            bd.DrawCircle2D(self.mouseCoordinate.x, self.mouseCoordinate.y, self.data["radius"])
        else:
            # the circle is snapped, draw it in green
            bd.SetPen(c4d.Vector(0, 1, 0))
            screeCoordinate = bd.WS(self.circleCoordinate)
            bd.DrawCircle2D(screeCoordinate.x, screeCoordinate.y, self.data["radius"])

        # if the len for lines is odd, we need to draw a line to the mouse cursor's position
        if len(self.lines) % 2 == 1 and self.mouseCoordinate != c4d.Vector(-1):
            lastPoint = self.lines[-1]
            # the last line is green
            bd.SetPen(c4d.Vector(0, 1, 0))
            if self.circleCoordinate != c4d.Vector(-1):
                bd.DrawLine2D(bd.WS(lastPoint), bd.WS(self.circleCoordinate))
            else:
                bd.DrawLine2D(bd.WS(lastPoint), self.mouseCoordinate)



        return c4d.TOOLDRAW_NONE

    def GetCursorInfo(self, doc, data, bd, x, y, bc):
        """
        Called by Cinema 4D when the cursor is over editor window to get the state of the mouse pointer.
        :param doc: The current active document.
        :type doc: c4d.documents.BaseDocument
        :param data: The tool settings container.
        :type data: c4d.BaseContainer
        :param bd: The editor's view.
        :type bd: c4d.BaseDraw
        :param x: The x coordinate of the mouse cursor relative to the top-left of the currently active editor view.
        :type x: float
        :param y:The y coordinate of the mouse cursor relative to the top-left of the currently active editor view.
        :type y: float
        :param bc: The container to store the result in.
        :type bc: c4d.BaseContainer
        :return:
        """
        # If the cursor has left a user area, set the mouseCoordinate to -1 and return True
        if bc.GetId() == c4d.BFM_CURSORINFO_REMOVE:
            self.circleCoordinate = c4d.Vector(-1)
            self.mouseCoordinate = c4d.Vector(-1)

            return True
        # if the mouse is coming back to the viewport, maybe we need to draw the line from last point, we ask for a redraw
        c4d.DrawViews()
        
        # Set the radius of the search function
        radius = self.data["radius"]
        mouseCursor = c4d.Vector(x, y, 0)
        # Retrieves the object that are near the mouse using the static method PickObject
        objects = c4d.utils.ViewportSelect.PickObject(bd, doc, x, y, radius, c4d.VIEWPORT_PICK_FLAGS_NONE)
        
        # Creates a new viewport select
        vpSelect = c4d.utils.ViewportSelect()
        frame = bd.GetFrame()
        w = frame['cr'] - frame['cl'] + 1
        h = frame['cb'] - frame['ct'] + 1
            

        if objects:
            nearestPoints = [] # stores the nearest point for each object
            # Init the ViewportSelect with the object
            if not vpSelect.Init(w, h, bd, objects, c4d.Mpoints, True, c4d.VIEWPORTSELECTFLAGS_NONE):
                raise ValueError("can't init the viewportSelect with the object")

            for op in objects:
                point = vpSelect.GetNearestPoint(op, x, y, radius)
                if point:
                    # Store de point coordinate in world coordinates
                    nearestPoints.append(op.GetPoint(point['i']) * op.GetMg())
                else:
                    nearestPoints.append(c4d.Vector(-1))

            # Calculates the point distance to the mouse
            pointsDistance = [ (bd.WS(p) - mouseCursor).GetLengthSquared() for p in nearestPoints]

            # Retrieves the index of the closest point
            index = pointsDistance.index(min(pointsDistance))

            self.circleCoordinate = nearestPoints[index]
            self.mouseCoordinate = mouseCursor
        else:
            self.mouseCoordinate = mouseCursor
            self.circleCoordinate = c4d.Vector(-1)
        
      
        
        # Sets the cursor.
        bc.SetInt32(c4d.RESULT_CURSOR, c4d.MOUSE_POINT_HAND)
        return True

    def AllocSubDialog(self, bc):
        """
        Called by Cinema 4D To allocate the Tool Dialog Option.
        :param bc: Currently not used.
        :type bc: c4d.BaseContainer
        :return: The allocated sub dialog.
        """

        return SettingsDialog(getattr(self, "data", {"line_color": c4d.Vector(1, 0, 0), "radius" : 20.0}))


if __name__ == "__main__":
    # Registers the tool plugin
    c4d.plugins.RegisterToolPlugin(id=PLUGIN_ID,
                                   str="draw line",
                                   info=0, icon=None,
                                   help="drawing line",
                                   dat=DrawLine())

Cheers,
Manuel

MAXON SDK Specialist

MAXON Registered Developer

Hi,

as the exception told you, you have to init your ViewportSelect (like most types in c4d.utils). Check the docs for details on ViewportSelect.Init()

Cheers,
zipit

MAXON SDK Specialist
developers.maxon.net

thanks Zipit even though I am walking in the dark I got some output.

Ok i put the below code at the beginning of my mousinput,
but i get an Error which does not make sense for me.
I have an selected polygon object in point mode in my scene ?

vpSelect.Init(width, height, bd, self.op, c4d.Mpolyedgepoint, True, c4d.VIEWPORTSELECTFLAGS_IGNORE_HIDDEN_SEL)
ReferenceError: the object 'c4d.PolygonObject' is not alive```
    bd = doc.GetActiveBaseDraw()
    frame = bd.GetFrame()
    left = frame["cl"]
    right = frame["cr"]
    top = frame["ct"]
    bottom = frame["cb"]
    width = right - left + 1
    height = bottom - top + 1
    vpSelect = c4d.utils.ViewportSelect()
    vpSelect.Init(width, height, bd, self.op, c4d.Mpolyedgepoint, True, c4d.VIEWPORTSELECTFLAGS_IGNORE_HIDDEN_SEL)

thanks in advance
mogh

Hi,

first of all - you do not need to get the active BaseDraw since MouseInput gives you the BaseDraw it is called for, its the argument called bd in your example. Secondly objects in c4d can be alive or not, which is a colorful metaphor for the condition that a pointer to an object - which you effectively store when you do something like self.op = some_object - is not pointing to a memory region anymore where an object is defined. You can check that via c4d.C4DAtom.Isalive().

Some simple solutions:

  1. First of all the method expects a list of objects. So it would be [self.op] anyways, but you could just give it all objects in your scene.
  2. Calculate the object with the bounding box closest to the mouse point(er) in world space coordinates and pass that object as it will also contain the closest point, vertex, edge or polygon. This will be for obvious reasons much more performant.
  3. Make sure the reference to self.op is still alive when the method is called. To give you tips on that we would need more information on where and when you set self.op.

Cheers
zipit

MAXON SDK Specialist
developers.maxon.net

Thank you for your time Zipit,
I trie to be as precise as posible when asking as a beginner but sometimes we have to run around chasing hunches and asking stupid questions in forums to get somewhere. hope you don't mind.

  1. Sounds cool but the user normaly has only one object selected multi object selection could be an obtion in the future but sounds to complicated for me at this moment.

anyway I set self.op in init (strangely self.op = doc.GetActiveObject() does not work)

    def __init__(self):
        doc = c4d.documents.GetActiveDocument()
        self.op = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE)
    # .... snippet

    # .... snippet
    if self.op.C4DAtom.Isalive():
            vpSelect.Init(width, height, bd, self.op, c4d.Mpolyedgepoint, True, c4d.VIEWPORTSELECTFLAGS_IGNORE_HIDDEN_SEL)
            infoPoint = vpSelect.GetPixelInfoPoint(mx, my)

to give you all the info here is the whole code so far
current error is depending on my isalive usage ...

Traceback (most recent call last):
*File "C:\Users\weigajan\AppData\Roaming\Maxon\Maxon Cinema 4D R21_64C2B3BD\plugins\Py-AVT\clickline.pyp", line 142, in MouseInput
if op.c4d.C4DAtom.Isalive():
NameError: global name 'op' is not defined

AttributeError: 'list' object has no attribute 'C4DAtom'
Traceback (most recent call last):
File "C:\Users\weigajan\AppData\Roaming\Maxon\Maxon Cinema 4D R21_64C2B3BD\plugins\Py-AVT\clickline.pyp", line 142, in MouseInput
if self.op.C4DAtom.Isalive():
AttributeError: 'list' object has no attribute 'C4DAtom'*

import c4d
import os
from c4d import gui, plugins, utils
PLUGIN_ID = 1000001 #Dummy ID  # Be sure to use a unique ID obtained from www.plugincafe.com

class SettingsDialog(c4d.gui.SubDialog):
    """
    Dialog to display option in the ToolData, in this case the Sphere size.
    """
    parameters = {}

    def __init__(self, arg):
        # Checks if the argument passed is a dictionary
        if not isinstance(arg, dict):
            raise TypeError("arg is not a dict.")

        self.parameters = arg

    def CreateLayout(self):
        """
        This Method is called automatically when Cinema 4D Create the Layout (display) of the GeDialog.
        """
        value = self.parameters["line_color"]
        if self.GroupBegin(id=1000, flags=c4d.BFH_SCALEFIT, cols=1, rows=2):
            self.GroupBorderSpace(10, 10, 10, 10)

            # Creates a Static text and a number input
            self.element = self.AddStaticText( id=2003, flags=c4d.BFH_SCALEFIT, initw=1, name="Line Color: ", borderstyle=c4d.BORDER_NONE )
            self.AddColorChooser(id=1001, flags=c4d.BFV_TOP|c4d.BFH_SCALEFIT, initw=1,  layoutflags=c4d.DR_COLORFIELD_NO_BRIGHTNESS)

            # Defines the default values
            self.SetColorField(id=1001, color=value, brightness = 1.0, maxbrightness = 1.0, flags = 0)
        self.GroupEnd()
        return True

    def Command(self, messageId, msg):

        # When the user change the Gadget with the ID 1002 (the input number field)

        if messageId == 1001:
            # Stores the value in the parameter variable
            self.parameters["line_color"] = self.GetColorField(1001)["color"]
            # Update the view to redraw the lines
            c4d.DrawViews(c4d.DA_ONLY_ACTIVE_VIEW | c4d.DA_NO_THREAD | c4d.DA_NO_ANIMATION)
        return True

class DrawLine(c4d.plugins.ToolData):
    """Inherit from ToolData to create your own tool"""
    def __init__(self):
        doc = c4d.documents.GetActiveDocument()
        self.op = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE)
        #self.op = doc.GetActiveObject()
        self.lines = []
        self.mouseCoordinate = c4d.Vector(-1)
        self.data = {"line_color":c4d.Vector(1, 1, 0)}
        #self.vpSelect = c4d.utils.ViewportSelect()

    def GetState(self, doc):
        if len(doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_0))!=1: return False
        if doc.GetMode() != c4d.Mpoints: return False

        return c4d.CMD_ENABLED

    def Debug(self, msg):
        c4d.CallCommand(13957)  # Konsole loeschen
        
        #print "Konsole geloescht."
        return True

    def KeyboardInput(self, doc, data, bd, win, msg):
        key = msg.GetLong( c4d.BFM_INPUT_CHANNEL )
        cstr = msg.GetString( c4d.BFM_INPUT_ASC )
        if key == c4d.KEY_ESC:
            # do what you want
            print "ESC Key Pressed."
            self.lines = []
            print "Lines Flushed."
            self.Debug(msg)
            # return True to signal that the key is processed            
            return True
        return False

    def MouseInput(self, doc, data, bd, win, msg):
        frame = bd.GetFrame()
        left = frame["cl"]
        right = frame["cr"]
        top = frame["ct"]
        bottom = frame["cb"]
        width = right - left + 1
        height = bottom - top + 1
        vpSelect = c4d.utils.ViewportSelect()        

        # Retrieves which clicks is currently clicked
        device = 0
        if msg[c4d.BFM_INPUT_CHANNEL ]== c4d.BFM_INPUT_MOUSELEFT:
            device = c4d.KEY_MLEFT
        else:
            return True

        # Retrieves the X/Y screen position of the mouse.
        mx = msg[c4d.BFM_INPUT_X]
        my = msg[c4d.BFM_INPUT_Y]

        # adds the position to the array
        myPoint = c4d.Vector (mx , my, 0)
        #my3dpoint = bd.SW(myPoint)
        self.lines.append(myPoint)
        
        if self.op.C4DAtom.Isalive():
            vpSelect.Init(width, height, bd, self.op, c4d.Mpolyedgepoint, True, c4d.VIEWPORTSELECTFLAGS_IGNORE_HIDDEN_SEL)
            infoPoint = vpSelect.GetPixelInfoPoint(mx, my)
        
        #if not infoPoint: return
        #i = infoPoint["i"]  # int
        #op = infoPoint["op"]  # c4dBaseObject
        #z = infoPoint["z"]  # float

        # Pushes an update event to Cinema 4D
        c4d.EventAdd()
        return True

    def Draw(self, doc, data, bd, bh, bt, flags):
        # Resets the drawing matrix to the world space matrix.
        bd.SetMatrix_Matrix(None, c4d.Matrix())
        #bd.SetMatrix_Camera()
        
        # set the color for already drawn lines
        color = self.data["line_color"]
        bd.SetPen(color)
        if not flags:
            # draw lines for each group of two points
            for pa, pb in zip(self.lines[::2], self.lines[1::2]):
                bd.DrawLine2D(pa, pb)                
                #bd.DrawLine(pa, pb, 0)

        # if the len for lines is odd, we need to draw a line to the mouse cursor's position
        if len(self.lines) % 2 == 1 and self.mouseCoordinate != c4d.Vector(-1):
            lastPoint = self.lines[-1]
            # the last line is green
            bd.SetPen(c4d.Vector(0, 1, 0))
            bd.DrawLine2D(lastPoint, self.mouseCoordinate)

        return c4d.TOOLDRAW_NONE

    def GetCursorInfo(self, doc, data, bd, x, y, bc):
        # If the cursor has left a user area, set the mouseCoordinate to -1 and return True
        if bc.GetId() == c4d.BFM_CURSORINFO_REMOVE:
            self.mouseCoordinate = c4d.Vector(-1)
            return True
        # if the mouse is coming back to the viewport, maybe we need to draw the line from last point, we ask for a redraw
        c4d.DrawViews()
        # we set the mouse coordinate
        self.mouseCoordinate = c4d.Vector(x, y, 0)
        
        # Sets the cursor.
        if len(self.lines) < 2:
            bc.SetInt32(c4d.RESULT_CURSOR, c4d.MOUSE_CROSS)
        else:
            bc.SetInt32(c4d.RESULT_CURSOR, c4d.MOUSE_POINT_HAND)
        return True

    def AllocSubDialog(self, bc):
        return SettingsDialog(getattr(self, "data", {"line_color": c4d.Vector(1, 0, 0)}))

if __name__ == "__main__":
    # Registers the tool plugin
    c4d.plugins.RegisterToolPlugin(id=PLUGIN_ID,
                                   str="Draw line",
                                   info=0, icon=None,
                                   help="drawing line",
                                   dat=DrawLine())

Hi,

I'll answer in ABP style:

  • self.op = doc.GetActiveObject() probably 'does not work' because ViewpostSelect.Init() expects a list of BaseObject as its fourth argument as I already pointed out. So if you just want the primary selected object it would have to be self.op = [doc.GetActiveObject()]. But you always should do input validation (the method can return None).
  • if op.c4d.C4DAtom.Isalive(): raises an exception because you misunderstood a crucial concept at least in this case. I cannot explain it in all detail here, but you should read up on object oriented programming and inheritance. The gist in this case is, that op is a BaseObject, which means that it is also a BaseList2D, which means it is also a GelistNode, which means it is also a C4DAtom. As the name implies C4DAtom is the atomic type of c4d. Basically anything that sits in your scene graph is an C4DAtom (actually almost anything - objects, materials, shaders, tags, animations, viewport, etc. - is even a BaseList2D). So it would be just if op.IsAlive(): #yadayada (its written with a capital a).
  • The ToolData type is somewhat lacking regarding a bultin logic for the target of your tool (hence the need for your self.op thing). How you handle this depends on what you are trying to do. One way to do it could be to add a property to your object. Something like this:
class MyCoolTool(ToolData):
    """
    """

    def __init__(self):
        """
        """
        self._active_object = None

    @property
    def active_object(self):
        """ You could incorporate all kinds of logic here, like for example allowing the user
             to switch objects mid-tool-execution. This version will just grab the primary
             active object the first time it is called and handle a dead reference on later
             calls by returning None then.
        """
        if self._active_object is None:
            doc = c4d.documents.GetActiveDocument()
            # Note that _active_object can still be None after the follwoing line.
            self._active_object = doc.GetActiveObject()
        if self._active_object is not None and self._active_object.IsAllive():
            return self._active_object
        # Not needed as methods implcitly return None, just for clarity here
        return None

    def method_that_relies_on_active_object(self, *args, **kwargs):
        """
        """
        if self.active_object is None:
            # do soemthing to signal that there is no operand for your tool operation
            return False
        op = self.active_object

        # ...
        return True

I hope this helps.

Cheers,
zipit

MAXON SDK Specialist
developers.maxon.net

@zipit said in How to setup c4d.utils.ViewportSelect() and GetNearestPoint to work:

IsAllive()

Thanks Zipit,
your code IsAllive() should be IsAlive() I guess ...

anyway I managed to get a Solid "none" feedback now. but need to sleep more to get somewhere ...

        if self.active_object is None:
            print "no active Object"
            # do soemthing to signal that there is no operand for your tool operation
            return False
        op = self.active_object
        
        vpSelect.Init(width, height, bd, [op], c4d.Mpolyedgepoint, True, c4d.VIEWPORTSELECTFLAGS_IGNORE_HIDDEN_SEL)

        infoPoint = vpSelect.GetPixelInfoPoint(mx, my)
        print infoPoint
        
        nearest = vpSelect.GetNearestPoint(op, mx, my, 5, onlyselected=True)        
        print nearest

Is GetNearestPoint better for perfomance than PointInRange ?
I found an old C++ code which uses PointInRange

hi ,
your problem is more a design problem than a technical one.

you should use the GetCursorInfo to retrieve the information of the mouse and check there what are the object / point you are overlapping.

Something that could help you is the static function (static mean you don't need an instance of that object to call/use it) PickObject.

You can use that function to retrieve the object you have under the mouse, and after that you can use GetNearestPoint to retrieve the points for each object (or the closest as i did).

once you have collected those information you have to draw them. (I've used a Vector(-1) to see if my coordinates is updated or not, but a bool would be better.

I've update my example (without your addition sorry). If i remember correctly you were trying to draw a line between two points

of course i don't check for example if the object is a polygon object or not and some other check that should be there.



import c4d
import os

# Be sure to use a unique ID obtained from www.plugincafe.com
PLUGIN_ID = 1000001


class SettingsDialog(c4d.gui.SubDialog):
    """
    Dialog to display option in the ToolData, in this case the Sphere size.
    """
    parameters = {}

    def __init__(self, arg):
        # Checks if the argument passed is a dictionary
        if not isinstance(arg, dict):
            raise TypeError("arg is not a dict.")

        self.parameters = arg

    def CreateLayout(self):
        """
        This Method is called automatically when Cinema 4D Create the Layout (display) of the GeDialog.
        """
        value = self.parameters["line_color"]
        radius = self.parameters['radius']
        if self.GroupBegin(id=1000, flags=c4d.BFH_SCALEFIT, cols=1, rows=1):
            self.GroupBorderSpace(10, 10, 10, 10)

            # Creates a Static text and a number input
            self.AddColorChooser(id=1001, flags=c4d.BFV_TOP | c4d.BFH_LEFT, initw=120,  layoutflags=c4d.DR_COLORFIELD_NO_BRIGHTNESS)
            if self.GroupBegin(id=1000, flags=c4d.BFH_SCALEFIT, cols=2, rows=1):
                # Creates a Static text and a number input
                self.AddStaticText(id=1002, flags=c4d.BFH_MASK, initw=120, name="Radius", borderstyle=c4d.BORDER_NONE)
                self.AddEditNumberArrows(id=1003, flags=c4d.BFH_MASK)
            self.GroupEnd()

               # Defines the default values
            self.SetFloat(id=1003, value=radius, min=0, max=300)
            # Defines the default values
            self.SetColorField(id=1001, color=value, brightness = 1.0, maxbrightness = 1.0, flags = 0)
        self.GroupEnd()
        return True

    def Command(self, messageId, msg):
        """
          This Method is called automatically when the user clicks on a gadget and/or changes its value this function will be called.
          It is also called when a string menu item is selected.
         :param messageId: The ID of the gadget that triggered the event.
         :type messageId: int
         :param bc: The original message container
         :type bc: c4d.BaseContainer
         :return: False if there was an error, otherwise True.
         """
        # When the user change the Gadget with the ID 1002 (the input number field)

        if messageId == 1001:
            # Stores the value in the parameter variable
            self.parameters["line_color"] = self.GetColorField(1001)["color"]
            # Update the view to redraw the lines
            c4d.DrawViews(c4d.DA_ONLY_ACTIVE_VIEW | c4d.DA_NO_THREAD | c4d.DA_NO_ANIMATION)
        elif messageId == 1003:
            self.parameters['radius'] = self.GetFloat(1003)
        
        return True


class DrawLine(c4d.plugins.ToolData):
    """Inherit from ToolData to create your own tool"""
    def __init__(self):
        self.lines = []
        self.mouseCoordinate = c4d.Vector(-1) # needed to draw a line to the mouse cursor
        self.circleCoordinate = c4d.Vector(-1) # will serve to draw the circle (spanned point)
        self.data = {"line_color":c4d.Vector(0, 0, 1), "radius": 20.0}


    def GetState(self, doc):
        """
        Called by Cinema 4D to know if the tool can be used currently
        :param doc: The current active document.
        :type doc: c4d.documents.BaseDocument
        :return: True if the tool can be used, otherwise False.
        """
        return c4d.CMD_ENABLED

    def MouseInput(self, doc, data, bd, win, msg):
        """
        Called by Cinema 4D, when the user click on the viewport and the tool is active.
        Mainly the place to do moue interaction from the viewport.
        :param doc: The current active document.
        :type doc: c4d.documents.BaseDocument
        :param data:  The tool settings container.
        :type data: c4d.BaseContainer
        :param bd:  The BaseDraw object of the active editor view.
        :type bd: c4d.BaseDraw
        :param win: The EditorWindow object for the active editor view.
        :type win: c4d.gui.EditorWindow
        :param msg: The original message container.
        :type msg: c4d.BaseContainer
        :return: False if a problem occurred during this function.
        """
        # Retrieves which clicks is currently clicked
        device = 0
        if msg[c4d.BFM_INPUT_CHANNEL ] == c4d.BFM_INPUT_MOUSELEFT:
            device = c4d.KEY_MLEFT
        else:
            return True
   
        self.lines.append(self.circleCoordinate)
             
        
        # Pushes an update event to Cinema 4D
        c4d.EventAdd()
        return True

    def Draw(self, doc, data, bd, bh, bt, flags):
        """
        Called by Cinema 4d when the display is updated to display some visual element of your tool in the 3D view.
        :param doc: The current active document.
        :type doc: c4d.documents.BaseDocument
        :param data: The tool settings container.
        :type data: c4d.BaseContainer.
        :param bd: The editor's view.
        :type bd: c4d.BaseDraw
        :param bh: The BaseDrawHelp editor's view.
        :type bh: c4d.plugins.BaseDrawHelp
        :param bt: The calling thread.
        :type bt: c4d.threading.BaseThread
        :param flags: The current drawing pass.
        :type flags: TOOLDRAWFLAGS
        :return: The result of the drawing (most likely c4d.DRAWRESULT_OK)
        """

        # Resets the drawing matrix to the world space matrix.
        bd.SetMatrix_Matrix(None, c4d.Matrix())
        


        # set the color for already drawn lines
        color = self.data["line_color"]
        bd.SetPen(color)
        
        if not flags:
            # draw lines for each group of two points
            for pa, pb in zip(self.lines[::2], self.lines[1::2]):
                #bd.DrawLine2D(bd.WS(pa), bd.WS(pb))
                bd.DrawLine(pa, pb, c4d.NOCLIP_Z | c4d.NOCLIP_D)

      
        #Draw the circle
        
        if self.circleCoordinate == c4d.Vector(-1):
            # draw the circle around the mouse in red
            bd.SetPen(c4d.Vector(1, 0, 0))
            bd.DrawCircle2D(self.mouseCoordinate.x, self.mouseCoordinate.y, self.data["radius"])
        else:
            # the circle is snapped, draw it in green
            bd.SetPen(c4d.Vector(0, 1, 0))
            screeCoordinate = bd.WS(self.circleCoordinate)
            bd.DrawCircle2D(screeCoordinate.x, screeCoordinate.y, self.data["radius"])

        # if the len for lines is odd, we need to draw a line to the mouse cursor's position
        if len(self.lines) % 2 == 1 and self.mouseCoordinate != c4d.Vector(-1):
            lastPoint = self.lines[-1]
            # the last line is green
            bd.SetPen(c4d.Vector(0, 1, 0))
            if self.circleCoordinate != c4d.Vector(-1):
                bd.DrawLine2D(bd.WS(lastPoint), bd.WS(self.circleCoordinate))
            else:
                bd.DrawLine2D(bd.WS(lastPoint), self.mouseCoordinate)



        return c4d.TOOLDRAW_NONE

    def GetCursorInfo(self, doc, data, bd, x, y, bc):
        """
        Called by Cinema 4D when the cursor is over editor window to get the state of the mouse pointer.
        :param doc: The current active document.
        :type doc: c4d.documents.BaseDocument
        :param data: The tool settings container.
        :type data: c4d.BaseContainer
        :param bd: The editor's view.
        :type bd: c4d.BaseDraw
        :param x: The x coordinate of the mouse cursor relative to the top-left of the currently active editor view.
        :type x: float
        :param y:The y coordinate of the mouse cursor relative to the top-left of the currently active editor view.
        :type y: float
        :param bc: The container to store the result in.
        :type bc: c4d.BaseContainer
        :return:
        """
        # If the cursor has left a user area, set the mouseCoordinate to -1 and return True
        if bc.GetId() == c4d.BFM_CURSORINFO_REMOVE:
            self.circleCoordinate = c4d.Vector(-1)
            self.mouseCoordinate = c4d.Vector(-1)

            return True
        # if the mouse is coming back to the viewport, maybe we need to draw the line from last point, we ask for a redraw
        c4d.DrawViews()
        
        # Set the radius of the search function
        radius = self.data["radius"]
        mouseCursor = c4d.Vector(x, y, 0)
        # Retrieves the object that are near the mouse using the static method PickObject
        objects = c4d.utils.ViewportSelect.PickObject(bd, doc, x, y, radius, c4d.VIEWPORT_PICK_FLAGS_NONE)
        
        # Creates a new viewport select
        vpSelect = c4d.utils.ViewportSelect()
        frame = bd.GetFrame()
        w = frame['cr'] - frame['cl'] + 1
        h = frame['cb'] - frame['ct'] + 1
            

        if objects:
            nearestPoints = [] # stores the nearest point for each object
            # Init the ViewportSelect with the object
            if not vpSelect.Init(w, h, bd, objects, c4d.Mpoints, True, c4d.VIEWPORTSELECTFLAGS_NONE):
                raise ValueError("can't init the viewportSelect with the object")

            for op in objects:
                point = vpSelect.GetNearestPoint(op, x, y, radius)
                if point:
                    # Store de point coordinate in world coordinates
                    nearestPoints.append(op.GetPoint(point['i']) * op.GetMg())
                else:
                    nearestPoints.append(c4d.Vector(-1))

            # Calculates the point distance to the mouse
            pointsDistance = [ (bd.WS(p) - mouseCursor).GetLengthSquared() for p in nearestPoints]

            # Retrieves the index of the closest point
            index = pointsDistance.index(min(pointsDistance))

            self.circleCoordinate = nearestPoints[index]
            self.mouseCoordinate = mouseCursor
        else:
            self.mouseCoordinate = mouseCursor
            self.circleCoordinate = c4d.Vector(-1)
        
      
        
        # Sets the cursor.
        bc.SetInt32(c4d.RESULT_CURSOR, c4d.MOUSE_POINT_HAND)
        return True

    def AllocSubDialog(self, bc):
        """
        Called by Cinema 4D To allocate the Tool Dialog Option.
        :param bc: Currently not used.
        :type bc: c4d.BaseContainer
        :return: The allocated sub dialog.
        """

        return SettingsDialog(getattr(self, "data", {"line_color": c4d.Vector(1, 0, 0), "radius" : 20.0}))


if __name__ == "__main__":
    # Registers the tool plugin
    c4d.plugins.RegisterToolPlugin(id=PLUGIN_ID,
                                   str="draw line",
                                   info=0, icon=None,
                                   help="drawing line",
                                   dat=DrawLine())

Cheers,
Manuel

MAXON SDK Specialist

MAXON Registered Developer

@m_magalhaes

This morning: uff more learning and work.
This Afternoon lets try this code ... mind blown ... thank you !

I'll mark this as solved hence I think ist a good way to close a thread with a working prototype for others to learn.

Thank You again, @zipit & @m_magalhaes
mogh