How to setup c4d.utils.ViewportSelect() and GetNearestPoint to work
-
I found this thread giving me a
RuntimeError: Object is not initialized yet
forc4d.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 theGetNearestPoint
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,
as the exception told you, you have to init your
ViewportSelect
(like most types inc4d.utils
). Check the docs for details onViewportSelect.Init()
Cheers,
zipit -
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
sinceMouseInput
gives you theBaseDraw
it is called for, its the argument calledbd
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 likeself.op = some_object
- is not pointing to a memory region anymore where an object is defined. You can check that viac4d.C4DAtom.Isalive()
.Some simple solutions:
- 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. - 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.
- 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 setself.op
.
Cheers
zipit - First of all the method expects a list of objects. So it would be
-
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.- 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 (strangelyself.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 definedAttributeError: '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' becauseViewpostSelect.Init()
expects a list ofBaseObject
as its fourth argument as I already pointed out. So if you just want the primary selected object it would have to beself.op = [doc.GetActiveObject()]
. But you always should do input validation (the method can returnNone
).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 aBaseObject
, which means that it is also aBaseList2D
, which means it is also aGelistNode
, which means it is also aC4DAtom
. As the name impliesC4DAtom
is the atomic type of c4d. Basically anything that sits in your scene graph is anC4DAtom
(actually almost anything - objects, materials, shaders, tags, animations, viewport, etc. - is even aBaseList2D
). So it would be justif 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 yourself.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 -
@zipit said in How to setup c4d.utils.ViewportSelect() and GetNearestPoint to work:
IsAllive()
Thanks Zipit,
your codeIsAllive()should beIsAlive()
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 thanPointInRange
?
I found an old C++ code which usesPointInRange
-
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 -
@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 -