mnesarco / fcscript

Python Simplified API for FreeCAD Macros
GNU General Public License v3.0
13 stars 1 forks source link

Please document the workflow you use to simplify the API #1

Open luzpaz opened 2 years ago

luzpaz commented 2 years ago

It could really be beneficial when onboarding other devs.

mnesarco commented 2 years ago

Well, macros are small quick and dirty programs to automate repetitive tasks. Usually the most time demanding task in FreeCAD is sketching things, so I started this as a set of helpers for sketching. On the other side, Qt (PySide) code is ugly and repetitive and most of the time what we really need to do is just get some inputs from the user. So I added some GUI helpers to make that just more easy and readable.

The initial project was not too ambitious, but I am open for contributions. I am a heavy user of python macros in FreeCAD and I feel the pain of some APIs, so I started this as a helper for my own macros, but I know many people are afraid of writing freecad macros just because it is... scary. Just asking for a number requires too much code, just sketching a line is hard, adding constraints....

The idea is to create wrappers/functions/utilities for the most common daily work, advanced obscure things can be done with the existing FreeCAD/OpenCascade APIs.

Other important thing is that I decided to put everything in a single python file in order to make it portable and versionable. As it is very experimental at this point, it is expected to have a lot of breaking API changes over the time, so having each version in an immutable python file allows to keep older macros working, immune to breaking changes.

luzpaz commented 2 years ago

CC @onekk, @macdroid53, @tomate44, @mwganson
Y'all might dig this.

mnesarco commented 2 years ago

Trivial Macro (14 lines including GUI):

https://user-images.githubusercontent.com/1087317/172070270-9ec6fe98-d8d8-4c02-98ad-e06b4905daa5.mp4

onekk commented 2 years ago

I someone is interested I could share some code to add or remove properties from a FeaturePython Object based on:

I'm using it to develop an app just now, so I could assure that it gave me no errors, at least on 0.19.4 as this is the target version stated by customer.

let me put together a MWE and I will post. Here, or where?

Regards

Carlo D.

mnesarco commented 2 years ago

Hi @onekk , can you elaborate a bit more about the use case for your proposal? There is an example of adding custom properties in the example 17:

https://github.com/mnesarco/fcscript/blob/6a5491f4ae169db126e48501eb0d457150a8097e/freecad/fcscript/demo/v_0_0_1.py#L337-L344

The idea is to keep the api as simple and easy as possible, it would be great to see your idea with a demo macro.

onekk commented 2 years ago

Sure, here the code:

"""Test FeaturePython Object to attach Children.

file: Fpobj.py

Author: Carlo Dormeletti
Copyright: 2022
Licence: LGPL
"""

from PySide2 import QtCore  # noqa

import FreeCAD
import FreeCADGui
from FreeCAD import Vector, Matrix  # noqa

import Part  # noqa

def get_type_prop():
    """Return a list of object properties. depending on Obj_Type."""
    prop_dict = {
        "Type_A": {
            "TA_Name":
            ["str", "Type_A Data", "Name of the Object", ""],
            "TA_Transparency":
            ["int", "Type_A Data", "Transparency", 100]},
        "Type_B": {
            "TB_Name":
            ["str", "Type_B Data", "Name of the Object", ""],
            "TB_Include_Edges":
            ["bol", "Type_B Data", "Include Edges", False],
            "TB_Transparency":
            ["int", "Type_B Data", "Transparency", 100]
        },
        "Type_C": {
            "TC_Name":
            ["str", "Type_C Data", "Name of Prism", ""],
            "TC_Direction":
            ["lst", "Type_C Data", "Direction", ["xy", "xz", "zy"]]
        }
    }

    return prop_dict

def get_other_prop(obj_type):
    """Return a list of property names not in obj_type."""
    prop_dict = get_type_prop()
    exc_prop = []
    good_prop = prop_dict[obj_type].keys()
    # print(f"good: {good_prop}")
    for key, value in prop_dict.items():
        for sub_key in value.keys():
            if sub_key not in good_prop:
                exc_prop.append(sub_key)

    return exc_prop

def get_obj_types():
    """Return alist of objects types."""
    prop_dict = get_type_prop()
    prop_list = ["None"]
    for k in prop_dict.keys():
        prop_list.append(k)

    return prop_list

class FP_Object:
    """Create an FP_object."""

    def __init__(self, obj, sel):
        """Add properties."""
        obj.addProperty(
            "App::PropertyLink", "SourceObject", "FP_Prop",
            "Source Object").SourceObject = sel[0].Object

        obj.addProperty(
            "App::PropertyEnumeration", "Obj_Type", "FP_Prop",
            "Material Type").Obj_Type = get_obj_types()

        obj.addProperty("App::PropertyColor", "ColorData", "View", "Object Color")

        obj.ColorData = (0.0, 1.0, 0.5)

        obj.Proxy = self

    def onChanged(self, obj, prop):
        """Execute when a property has changed."""
        # print(f"obj: {obj}, prop: {prop}")
        if prop == "ColorData":
            # print("ColorData reached")
            obj.ViewObject.ShapeColor = obj.ColorData
        elif prop == "Obj_Type":
            self.set_properties(obj, obj.Obj_Type)

    def execute(self, obj):
        """Create object."""
        ch_obj = obj.SourceObject
        obj.Shape = ch_obj.Shape
        ch_obj.ViewObject.Visibility = False
        obj.ViewObject.ShapeColor = obj.ColorData

    def set_properties(self, obj, obj_type):
        """Set object properties based on dictionary."""
        # First remove properties of other obj_type if there is any
        self.remove_other_props(obj, obj_type)

        obj_props = get_type_prop()[obj_type]
        # print(obj_props)

        for key, value in obj_props.items():
            # print(f"{key}: Value: {value}")
            prop_dict = {
                "str": "App::PropertyString",
                "int": "App::PropertyInteger",
                "bol": "App::PropertyBool",
                "lst": "App::PropertyEnumeration"}

            if value[0] in prop_dict.keys():
                obj.addProperty(
                    prop_dict[value[0]],
                    key, value[1], value[2])
                setattr(obj, key, value[3])

        return

    def remove_other_props(sel, obj, obj_type):
        """Remove property not in obj_type."""
        p_list = get_other_prop(obj_type)
        # print(f"Bad: {p_list}")
        for prop_name in p_list:
            if prop_name in obj.PropertiesList:
                obj.removeProperty(prop_name)

def get_sel_objects(sel):
    """Return selected objects as list."""
    o_list = []
    for obj in sel:
        o_list.append(
            FreeCAD.ActiveDocument.getObject(obj.ObjectName))

    return o_list

class ViewProvider_EM(object):
    """ViewProvider Object."""

    def __init__(self, vobj):
        """Init things."""
        self.Object = vobj.Object
        vobj.Proxy = self

    def claimChildren(self):
        """Return objects that will be placed under it in the tree view."""
        return [self.Object.SourceObject]

    def __getstate__(self):
        """Get state."""
        return None

    def __setstate__(self, _state):
        """Set state."""
        return None

def makeFPObject(sel):
    """Command."""
    if FreeCAD.ActiveDocument is None:
        doc = FreeCAD.newDocument("FP_Doc")
        FreeCAD.setActiveDocument("FP_Doc")
    else:
        doc = FreeCAD.ActiveDocument

    obj = doc.addObject("Part::FeaturePython", "FP_Object")
    obj.Label = "FP_Object"
    FP_Object(obj, sel)

    ViewProvider_EM(obj.ViewObject)

    doc.recompute()

# Execute the action

selection = FreeCADGui.Selection.getSelectionEx()

l_sen = len(selection)

if l_sen != 1:
    msg = f"You have selected <b>{l_sen}</b> shape(s)<br>"
    msg += "You must select only 1 shape <br>"
    print(msg)
else:
    makeFPObject(selection)

Usage create a Part object (a cilinder) launche the script, and select "Obj_Type", you could tune the poperties modifying the dictionary:

    prop_dict = {
        "Type_A": {
            "TA_Name":
            ["str", "Type_A Data", "Name of the Object", ""],
            "TA_Transparency":
            ["int", "Type_A Data", "Transparency", 100]},
        "Type_B": {
            "TB_Name":
            ["str", "Type_B Data", "Name of the Object", ""],
            "TB_Include_Edges":
            ["bol", "Type_B Data", "Include Edges", False],
            "TB_Transparency":
            ["int", "Type_B Data", "Transparency", 100]
        },
        "Type_C": {
            "TC_Name":
            ["str", "Type_C Data", "Name of Prism", ""],
            "TC_Direction":
            ["lst", "Type_C Data", "Direction", ["xy", "xz", "zy"]]
        }
    }

with a couple of caveats:

Property Name as example TC_Name, TC_Direction must be unique and not duplicated, as the remove properties is based on these names.

suppported objects are scanned in:

    def set_properties(self, obj, obj_type):

using another dictionary:

            prop_dict = {
                "str": "App::PropertyString",
                "int": "App::PropertyInteger",
                "bol": "App::PropertyBool",
                "lst": "App::PropertyEnumeration"}

The magic of set and remove properties could be duplicated to retrieve data in ececute()

mnesarco commented 2 years ago

@onekk

It looks like a very specific use case. Do you think this is common enough for a general API for macros?

Part::FeaturePython objects are great, I use them a lot and I have also utilities for them in Mnesarco_Utils, but I feel that it is and advanced topic.

onekk commented 2 years ago

@onekk

It looks like a very specific use case. Do you think this is common enough for a general API for macros?

Part::FeaturePython objects are great, I use them a lot and I have also utilities for them in Mnesarco_Utils, but I feel that it is and advanced topic.

Yes and no for the "specific use case", sometimes and not very rarely you have to choose between "cases" that involve using or not using a property, The code above will permit to make even a rather complex object "simply" filling a dictionary of the properties.

Not a polemic, only to see if I could help you in some other manner. What sort of things you want, need, or are willing to talk about?

Regards

Carlo D.

mnesarco commented 2 years ago

Hi Carlo (@onekk),

Thank you very much for you interest. This project is very immature and experimental at this point. We can discuss anything we want, I am not looking for something specific. I started this for internal usage in my workflow that requires a lot of 2D sketching and some basic 3D operations. I shared some videos on twitter and people reacted asking for a release :D

I suppose the most important task now is to validate the approach, how I re-expose the existing APIs. The goal is to make it a lot more simpler for daily work macros.

I tend to see macros just like some inputs and some outputs. So I included a simplified mini GUI framework to remove all the QT boilerplate from my macros (still very incomplete). This way the macros code should be more imperative like:

  1. Get Inputs
  2. Execute
  3. Show Outputs

Python is a very versatile language, there are many underrated features like decorators that makes code a lot more readable and short. My approach is to use all the power of the language to make the API as simple and readable as possible.

I suppose it is sane to wait for some feedback from macro writers. Maybe the niche is too small and the whole project is condemned to fail. Or maybe we get a lot of feedback and can define a more precise roadmap.

Regards,

Frank.

onekk commented 2 years ago

As I'm working on some projects the API could be somewhat weird when you are dealing with Qt code.

There are at least two point:

1) Mixed use of the old PySide 1.0 syntax exposed in FC with a wrapper now PySide2 is used at least from Qt5 so I think from 0.18. So there are around mixed examples, probably it is worth trying to make them all PySide2 compliant so it is more easy to refer to PySide2 documentation.

2) Some UI things are reimplmented in FC as example the MenuBar I'm searching to add a menu item in a specific place for a WB and I'm not guessing how to do, forum was not helping a part from some links to existing code that lead to nothing, I don't want to rewrite all the menu to place a new item in a specific place, Qt has a way to do this but it seems that FC will not expose directly the needed menu. (or at least I haven't guessed why)

For your problems.

I've done some paid works and I've developed an API that use simple "definition list" to make complex interface but the code is around 400 lines to have a decent granularity managing different input types I could put together some MWE in next days, if you are interested in.

I think that it could be adapted to work in the standard FC interface, it will be simplyfing things as the user will simply define a list with "data" call a createUI() method and some other things, depending on the complexity and then data coudl be retrieved scannig the same definition "data" to obtain values.

Regards

Carlo D.

mnesarco commented 2 years ago

As I'm working on some projects the API could be somewhat weird when you are dealing with Qt code.

There are at least two point:

  1. Mixed use of the old PySide 1.0 syntax exposed in FC with a wrapper now PySide2 is used at least from Qt5 so I think from 0.18. So there are around mixed examples, probably it is worth trying to make them all PySide2 compliant so it is more easy to refer to PySide2 documentation.
  2. Some UI things are reimplmented in FC as example the MenuBar I'm searching to add a menu item in a specific place for a WB and I'm not guessing how to do, forum was not helping a part from some links to existing code that lead to nothing, I don't want to rewrite all the menu to place a new item in a specific place, Qt has a way to do this but it seems that FC will not expose directly the needed menu. (or at least I haven't guessed why)

I am not exposing PySide* APIs. If the user wants to use that, nothing will stop him/her. This API is not a replacement of the existing APIs, it is just a complement.

For your problems.

I've done some paid works and I've developed an API that use simple "definition list" to make complex interface but the code is around 400 lines to have a decent granularity managing different input types I could put together some MWE in next days, if you are interested in.

Well, I already have a GUI layer and I don't think I will replace it. I am sure your APIs are Great but I prefer contributions about enhancing this approach rather than replacing it.

    from freecad.fcscript.v_0_0_1 import *

    with Dialog("Test18: Simple Rounded Rect"):
        with Col():
            width = InputFloat(label="Width:", value=50)
            length = InputFloat(label="Length:", value=50)
            height = InputFloat(label="Height:", value=5)
            radius = InputFloat(label="Border radius:", value=3)
            @button(text="Create")
            def create():
                body = XBody(name='test18')
                sketch = body.sketch(plane='XY', name='test18_sketch')
                path = sketch.create_group()
                path.rect_rounded(w=width.value(), h=length.value(), r=radius.value())
                sketch.pad(height.value())
                recompute()

MACRO

I think that it could be adapted to work in the standard FC interface, it will be simplyfing things as the user will simply define a list with "data" call a createUI() method and some other things, depending on the complexity and then data coudl be retrieved scannig the same definition "data" to obtain values.

Model Driven GUI is an interesting concept. My current approach is more like a kind of "Immediate Mode Gui" as the layout and widgets are created and placed directly by code layout. This approach adds a lot of readability.

You are an advanced user and I understand your approach. Creating workbenches requires advanced knowledge of the FreeCAD/Coin/QT APIs, many of those APIs are poorly documented and it is a pain to use them. After some time, each workbench/mod developer creates its own helpers/apis to deal with the boilerplate. But in this project I am more interested in small macros, something more like hit and run small programs that basic users can just copy/paste and run.

I hope I managed to explain myself properly about the scope and the approach.

onekk commented 2 years ago

Yes, I see your point.

In the meantime I've read your code.

Very good work, I've to study much more to catch all the aspects.

So chapeau.

For now, I have no suggestion, only compliments, but maybe in future...