[Solved] How to select a point on Sketcher's screen for use in a macro?

Need help, or want to share a macro? Post here!
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
User avatar
ifohancroft
Posts: 205
Joined: Fri May 31, 2019 11:25 pm
Location: Sofia, Bulgaria
Contact:

[Solved] How to select a point on Sketcher's screen for use in a macro?

Post by ifohancroft »

I have made a macro that creates a shape from lines and constraints it, however, it creates it on a fixed point on the screen.

I am trying to make the macro look and feel as close as possible to Sketcher's own geometry tools and for this there are a couple of things I want to do that I don't know how, but I am tackling them one at a time, so the first one is:

In a macro, after it gets executed, how do I wait for a for left mouse button click on the screen and get that click's X and Y coordinates?
Last edited by ifohancroft on Mon Sep 27, 2021 1:57 pm, edited 2 times in total.
I like making, breaking and modding stuff, using a soldering iron, code or both. https://ifohancroft.com
openBrain
Veteran
Posts: 9034
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: How to select a point on Sketcher's screen for use in a macro?

Post by openBrain »

You can use a ViewObserver filtering SoMouseEvent types. See example here : https://github.com/0penBrain/FreeCAD-ma ... og.FCMacro
User avatar
ifohancroft
Posts: 205
Joined: Fri May 31, 2019 11:25 pm
Location: Sofia, Bulgaria
Contact:

Re: How to select a point on Sketcher's screen for use in a macro?

Post by ifohancroft »

openBrain wrote: Fri Sep 24, 2021 4:10 pm You can use a ViewObserver filtering SoMouseEvent types. See example here : https://github.com/0penBrain/FreeCAD-ma ... og.FCMacro
Thank you!

Now that I know about ViewObserver, I was able to find this: https://wiki.freecadweb.org/Code_snippe ... ick_action and used the example under 'Other example with ViewObserver on a object select or view' as a base, so I made this:

Code: Select all

import FreeCAD
import FreeCADGui
import Sketcher

activeView = Gui.activeDocument().ActiveView

class ViewObserver:
    def __init__(self, view):
        self.view = view

    def logPosition(self, info):
        isLeftMouseButton = (info["Button"] == "BUTTON1")
        isClicked = (info["State"] == "DOWN")
        clickPosition = info["Position"]
        if (isLeftMouseButton and isClicked):
            documentPosition = self.view.getPoint(clickPosition)
            FreeCAD.Console.PrintMessage(str(documentPosition) + "\n---------------------------\n")
            return documentPosition

viewObserver = ViewObserver(activeView)
callback = activeView.addEventCallback("SoMouseButtonEvent",viewObserver.logPosition)
FreeCAD.Console.PrintMessage(str(callback))
However, I am having trouble figuring out how to get the position into a variable outside of the ViewObserver.

Also, couple of things that came up while I was writing this:

1. What is the general consensus on a way to structure code and limit it to the specific workbench? I mean like making sure my macro doesn't produce errors if executed while not in a sketch or without having a document? Is that even being done/needed for macros?

2. How do I make it wait until a left mouse button is pressed so the execution doesn't just continue if I have pressed the right button for example? Do I just make a loop inside of logPosition that keeps on looping until it receives a left click?
I like making, breaking and modding stuff, using a soldering iron, code or both. https://ifohancroft.com
openBrain
Veteran
Posts: 9034
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: How to select a point on Sketcher's screen for use in a macro?

Post by openBrain »

Does your macro have a GUI?

To me the best solution for you is to set the ViewObserver callback then wait it is called with a left click button. When it happens, just remove the callback and end your macro.

And yes, IMO should ensure a minimum of checks on the objects to ensure they are of the right type.
Moreover you have a special case with an observer callback. In this case, I would always place code in a try/catch and remove the observer if something goes wrong.
User avatar
ifohancroft
Posts: 205
Joined: Fri May 31, 2019 11:25 pm
Location: Sofia, Bulgaria
Contact:

Re: How to select a point on Sketcher's screen for use in a macro?

Post by ifohancroft »

openBrain wrote: Fri Sep 24, 2021 5:49 pm Does your macro have a GUI?
No. It does not. I execute it from the Macros window. I used to have an icon for it in the toolbar, but that's about it. I am just trying to add click coordinates for them so I can choose where to place the sketches inside of a Sketcher document.


openBrain wrote: Fri Sep 24, 2021 5:49 pm To me the best solution for you is to set the ViewObserver callback then wait it is called with a left click button. When it happens, just remove the callback and end your macro.
How do I get the click coordinates into a variable outside of the ViewObserver?

Also, am I going about it the wrong way? Instead of trying to get the coordinates into a variable outside of the observer, then draw my sketch using those coordinates, am I supposed to make a class that draws my sketch, making that class be a callback for the active view's SoMouseButtonEvent and get the coordinates inside it?


openBrain wrote: Fri Sep 24, 2021 5:49 pm And yes, IMO should ensure a minimum of checks on the objects to ensure they are of the right type.
Makes sense. Thank you!


openBrain wrote: Fri Sep 24, 2021 5:49 pm Moreover you have a special case with an observer callback. In this case, I would always place code in a try/catch and remove the observer if something goes wrong.
Does it have to be like this? I did it this way because I don't know another and I thought I was supposed to do it this way?
I like making, breaking and modding stuff, using a soldering iron, code or both. https://ifohancroft.com
User avatar
ifohancroft
Posts: 205
Joined: Fri May 31, 2019 11:25 pm
Location: Sofia, Bulgaria
Contact:

Re: How to select a point on Sketcher's screen for use in a macro?

Post by ifohancroft »

I think I got it.

It seems my confusion was coming from not understanding the concept well enough.

I think I managed to wrap my head around it.

In a bit I'll post what I came up with.
I like making, breaking and modding stuff, using a soldering iron, code or both. https://ifohancroft.com
openBrain
Veteran
Posts: 9034
Joined: Fri Nov 09, 2018 5:38 pm
Contact:

Re: How to select a point on Sketcher's screen for use in a macro?

Post by openBrain »

ifohancroft wrote: Fri Sep 24, 2021 6:05 pm No. It does not. I execute it from the Macros window. I used to have an icon for it in the toolbar, but that's about it. I am just trying to add click coordinates for them so I can choose where to place the sketches inside of a Sketcher document.
In this case you have to inform the user the macro will 'pause' waiting for a click. Another cool way is to change the mouse pointer when waiting the position click, then restore it.
How do I get the click coordinates into a variable outside of the ViewObserver?
You can look at the example code I pointed out above. You can use a callback or a signal/slot. Actually there aren't really solution with single thread to really 'pause' the macro execution as it will also pause the event loop, hence freeze FreeCAD.
Also, am I going about it the wrong way? Instead of trying to get the coordinates into a variable outside of the observer, then draw my sketch using those coordinates, am I supposed to make a class that draws my sketch, making that class be a callback for the active view's SoMouseButtonEvent and get the coordinates inside it?
I would have did it this way (I think). Instantiate a class that draws your sketch and declare one of its member function as being the ViewObserver callback. When it's called with correct arguments, remove the callback, finish the job and quit.
Does it have to be like this? I did it this way because I don't know another and I thought I was supposed to do it this way?
The problem with the code above is that you never remove the callback, so it will stay active and each time you'll run the macro, a new one will be created, and all existing will be called when clicking.
edwilliams16
Veteran
Posts: 3108
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: How to select a point on Sketcher's screen for use in a macro?

Post by edwilliams16 »

https://github.com/edwilliams16/FreeCAD ... armacro.py is some code I wrote that takes two clicks and a number, n, to draw an n-pointed star shaped object in sketcher. You might find it has some useful code.
User avatar
ifohancroft
Posts: 205
Joined: Fri May 31, 2019 11:25 pm
Location: Sofia, Bulgaria
Contact:

Re: How to select a point on Sketcher's screen for use in a macro?

Post by ifohancroft »

Apparently it took me about 9 hours, but I am very happy with the result:

Code: Select all

import FreeCAD
import FreeCADGui
import Sketcher
import PySide2

from PySide2 import QtGui, QtWidgets
from PySide2.QtWidgets import QApplication
from PySide2.QtGui import QPixmap, QCursor

class SwitchCutout:
    def __init__(self):
        self.changeCursor()
        self.attachViewObserver()
    def attachViewObserver(self):
        if Gui.activeView() and repr(Gui.activeView()) == "View3DInventor":
            self.view = Gui.activeView()
            self.view_callback = self.view.addEventCallback("SoMouseButtonEvent", self.viewObserver)
        else:
            FreeCAD.Console.PrintMessage("ViewObserver cannot be set, no active 3D view\n")
    def detachViewObserver(self):
        self.view.removeEventCallback("SoMouseButtonEvent", self.viewObserver)
        self.resetCursor()
    def viewObserver(self, info):
        if info["State"] == "DOWN":
            if info["Button"] == "BUTTON1":
                self.origin_x = self.view.getPoint(info["Position"])[0]
                self.origin_y = self.view.getPoint(info["Position"])[1]
                self.draw()
            elif info["Button"] == "BUTTON2":
                self.detachViewObserver()
    def changeCursor(self):
        cursorImage = FreeCAD.getUserAppDataDir() + "Macro/Icons/Sketcher_Pointer_Create_Switch.svg"
        pixmap = QPixmap(cursorImage)
        cursor = QCursor(pixmap)
        QApplication.setOverrideCursor(cursor)
    def resetCursor(self):
        QApplication.restoreOverrideCursor()
    def draw(self):
        # Just a placeholder for now. I will add the drawing part later.
        FreeCAD.Console.PrintMessage("You clicked: " + str(self.origin_x) + "," + str(self.origin_y) + "\n")

SwitchCutout()
Sorry for the lack of comments or an actual drawing part. I will add those later.
I am attaching it as a zip file along with the icon it uses, in-case anyone finds it useful or wants to try it. Just unzip it in your $HOME/.FreeCAD/Macros/ folder.

There were however, a couple of unexpected things, as well as couple of improvements I would like to make. Hence the following questions:

1. The ActiveSketch object doesn't get deleted when the Sketch does. What way would you recommend to check for the presence of an active sketch before drawing geometry or adding constraints? If you try to add geometry to a deleted sketch, by using ActiveSketch, there are no errors, just nothing happens, but I'd still like to have a proper check in place.

2. My custom cursor is too big. Is it the SVG file I have used or do I have to specify the scale or something? I couldn't actually find where FreeCAD holds its UI icons, so I downloaded the SVG from the wiki, then modified it a bit.

3. I am trying to imitate Sketcher's own tools with this macro, so I've made it that once you start it, you can keep clicking in the document and it will keep "drawing", unless you hit the right mouse button. Hitting the right mouse button shows the context menu on screen. Is there a way to prevent the context screen from showing when hitting the right mouse button while the macro is working?

Also, I just discovered that hitting the right mouse button doesn't actually stop the macro. Am I removing the event callback the wrong way?

3.1. Sketcher's native tools also stop working when you select another geometry tool or constraint - Can I catch the selection of another tool, so I can stop my macro when that happens?

4. I doubt that's possible, but if I add an icon for my macro to Sketcher's toolbar (by going to Tools > Customize > Macros, selecting a pixmap, etc, then going to the Sketcher's toolbar, adding a new one, adding the icon to it, etc), is it possible to make the icon be grayed out and unclickable when I am not editing a Sketch? (I.e. the way the icons for Sketch's own tools work)?

P.S. Any tips for improving my macro further?
Attachments
TestMacro.zip
(1.89 KiB) Downloaded 38 times
I like making, breaking and modding stuff, using a soldering iron, code or both. https://ifohancroft.com
freedman
Veteran
Posts: 3441
Joined: Thu Mar 22, 2018 3:02 am
Location: Washington State, USA

Re: How to select a point on Sketcher's screen for use in a macro?

Post by freedman »

3.1. Sketcher's native tools also stop working when you select another geometry tool or constraint - Can I catch the selection of another tool, so I can stop my macro when that happens?
I had similair issues when I wanted to do something in Sketcher, I get the feeling there aren't many hooks to python. I don't have time to test but the cursor uses different images depending on tool selection, I thought that might be a way to detect states. The cursor is always default at startup.
Post Reply