Gizmos for parametric objects in Bonsai

edited February 14 in General

I have been experimenting with drawing gizmos in the viewport for parametric objects, I'll share a standalone script you can just run in the script editor to test out. Feel free to give any constructive feedback.

Things to keep in mind / to improve :

  • Use custom meshes for arrow gizmos or communicate better what does what. Unfortunately ressources on how to use gizmos is ... pretty scarce so it's a lot of trial and error.
  • No snapping
  • Support all parameters
  • No idea about performance but I didn't notice anything drastic
    Cheers
import bpy

from bpy.types import Gizmo
from mathutils import Vector, Matrix
from math import pi


class BIM_OT_toggle_gizmo(bpy.types.Operator):
    bl_idname = "serendipicad.toggle_gizmo"
    bl_label = "Toggle Gizmo"
    obj: bpy.props.StringProperty()
    prop_path: bpy.props.StringProperty()

    def execute(self, context):
        obj = bpy.data.objects[self.obj]
        parts = self.prop_path.split(".")
        for part in parts[:-1]:
            obj = getattr(obj, part)
        setattr(obj, parts[-1], not getattr(obj, parts[-1]))
        return {"FINISHED"}


class GizmoLock(Gizmo):
    bl_idname = "VIEW3D_GT_lock"

    __slots__ = (
        "custom_shape_closed",
        "custom_shape_open",
    )

    tris_closed = [
        (-0.3519617021083832, 0.7437955141067505, 0.0),
        (-0.3048076927661896, 0.9197763204574585, 0.0),
        (-0.4225003123283386, 0.9877263307571411, 0.0),
        (-0.4225003123283386, 0.9877263307571411, 0.0),
        (-0.3048076927661896, 0.9197763204574585, 0.0),
        (-0.1759808510541916, 1.0486031770706177, 0.0),
        (-0.24393069744110107, 1.1662957668304443, 0.0),
        (-0.1759808510541916, 1.0486031770706177, 0.0),
        (2.9078805141580233e-08, 1.0957571268081665, 0.0),
        (2.9078805141580233e-08, 1.2316569089889526, 0.0),
        (2.9078805141580233e-08, 1.0957571268081665, 0.0),
        (0.1759808510541916, 1.0486031770706177, 0.0),
        (0.243930846452713, 1.1662957668304443, 0.0),
        (0.1759808510541916, 1.0486031770706177, 0.0),
        (0.30480796098709106, 0.9197763204574585, 0.0),
        (0.4225005805492401, 0.9877263307571411, 0.0),
        (0.30480796098709106, 0.9197763204574585, 0.0),
        (0.35196200013160706, 0.7437955141067505, 0.0),
        (-0.48786139488220215, 0.7437955141067505, 0.0),
        (0.48786139488220215, 4.5077928945147505e-08, 0.0),
        (0.48786139488220215, 0.7437955141067505, 0.0),
        (-0.3519617021083832, 0.7437955141067505, 0.0),
        (-0.4225003123283386, 0.9877263307571411, 0.0),
        (-0.48786139488220215, 0.7437955141067505, 0.0),
        (-0.4225003123283386, 0.9877263307571411, 0.0),
        (-0.1759808510541916, 1.0486031770706177, 0.0),
        (-0.24393069744110107, 1.1662957668304443, 0.0),
        (-0.24393069744110107, 1.1662957668304443, 0.0),
        (2.9078805141580233e-08, 1.0957571268081665, 0.0),
        (2.9078805141580233e-08, 1.2316569089889526, 0.0),
        (2.9078805141580233e-08, 1.2316569089889526, 0.0),
        (0.1759808510541916, 1.0486031770706177, 0.0),
        (0.243930846452713, 1.1662957668304443, 0.0),
        (0.243930846452713, 1.1662957668304443, 0.0),
        (0.30480796098709106, 0.9197763204574585, 0.0),
        (0.4225005805492401, 0.9877263307571411, 0.0),
        (0.4225005805492401, 0.9877263307571411, 0.0),
        (0.35196200013160706, 0.7437955141067505, 0.0),
        (0.487861692905426, 0.74379563331604, 0.0),
        (-0.48786139488220215, 0.7437955141067505, 0.0),
        (-0.48786139488220215, 4.5077928945147505e-08, 0.0),
        (0.48786139488220215, 4.5077928945147505e-08, 0.0),
    ]

    tris_open = [
        (-0.12838619947433472, 1.3143587112426758, 0.0),
        (0.025773197412490845, 1.411454677581787, 0.0),
        (-0.0144234299659729, 1.541273593902588, 0.0),
        (-0.0144234299659729, 1.541273593902588, 0.0),
        (0.025773197412490845, 1.411454677581787, 0.0),
        (0.20782703161239624, 1.4184625148773193, 0.0),
        (0.23792517185211182, 1.5509872436523438, 0.0),
        (0.20782703161239624, 1.4184625148773193, 0.0),
        (0.3689943850040436, 1.3335046768188477, 0.0),
        (0.4613226056098938, 1.433225393295288, 0.0),
        (0.3689943850040436, 1.3335046768188477, 0.0),
        (0.4660903215408325, 1.1793451309204102, 0.0),
        (0.5959094166755676, 1.2195416688919067, 0.0),
        (0.4660903215408325, 1.1793451309204102, 0.0),
        (0.47309836745262146, 0.997291088104248, 0.0),
        (0.6056233048439026, 0.9671931266784668, 0.0),
        (0.47309836745262146, 0.997291088104248, 0.0),
        (0.3881405293941498, 0.8361238241195679, 0.0),
        (-0.48786139488220215, 0.7437955141067505, 0.0),
        (0.48786139488220215, 4.5077928945147505e-08, 0.0),
        (0.48786139488220215, 0.7437955141067505, 0.0),
        (-0.12838619947433472, 1.3143587112426758, 0.0),
        (-0.0144234299659729, 1.541273593902588, 0.0),
        (-0.22810709476470947, 1.406686782836914, 0.0),
        (-0.0144234299659729, 1.541273593902588, 0.0),
        (0.20782703161239624, 1.4184625148773193, 0.0),
        (0.23792517185211182, 1.5509872436523438, 0.0),
        (0.23792517185211182, 1.5509872436523438, 0.0),
        (0.3689943850040436, 1.3335046768188477, 0.0),
        (0.4613226056098938, 1.433225393295288, 0.0),
        (0.4613226056098938, 1.433225393295288, 0.0),
        (0.4660903215408325, 1.1793451309204102, 0.0),
        (0.5959094166755676, 1.2195416688919067, 0.0),
        (0.5959094166755676, 1.2195416688919067, 0.0),
        (0.47309836745262146, 0.997291088104248, 0.0),
        (0.6056233048439026, 0.9671931266784668, 0.0),
        (0.6056233048439026, 0.9671931266784668, 0.0),
        (0.3881405293941498, 0.8361238241195679, 0.0),
        (0.48786142468452454, 0.74379563331604, 0.0),
        (-0.48786139488220215, 0.7437955141067505, 0.0),
        (-0.48786139488220215, 4.5077928945147505e-08, 0.0),
        (0.48786139488220215, 4.5077928945147505e-08, 0.0),
    ]

    def draw(self, context):
        self.draw_custom_shape(self.get_custom_shape(context))

    def get_custom_shape(self, context):
        return (
            self.custom_shape_closed
            if context.active_object.BIMStairProperties.total_length_lock
            else self.custom_shape_open
        )

    def setup(self):
        if not hasattr(self, "custom_shape_closed"):
            self.custom_shape_closed = self.new_custom_shape("TRIS", self.tris_closed)
        if not hasattr(self, "custom_shape_open"):
            self.custom_shape_open = self.new_custom_shape("TRIS", self.tris_open)

    def draw_select(self, context, select_id):
        self.draw_custom_shape(self.get_custom_shape(context), select_id=select_id)


class MY_GIZMOGROUP_GT(bpy.types.GizmoGroup):
    bl_idname = "MY_GIZMOGROUP_GT"
    bl_label = "My Gizmo Group"
    bl_space_type = "VIEW_3D"
    bl_region_type = "WINDOW"
    bl_options = {"3D", "PERSISTENT"}

    attr_names = (
        "width",
        "height",
        "total_length_target",
        "nosing_length",
        "nosing_depth",
        "tread_run",
        "tread_depth",
        "tread_rise",
    )
    red = (1, 0.2, 0.2)
    green = (0.2, 0.8, 0.2)
    blue = (0.2, 0.2, 1)
    gizmo_scale_basis = 0.75
    colors = {
        "width": green,
        "height": blue,
        "total_length_target": red,
        "nosing_length": red,
        "nosing_depth": blue,
        "tread_run": red,
        "tread_depth": blue,
        "tread_rise": blue,
    }

    @classmethod
    def poll(cls, context):
        return (
            context.active_object
            and hasattr(context.active_object, "BIMStairProperties")
            and context.active_object.BIMStairProperties.is_editing
        )

    def get_gizmo_matrix_width(self, props):
        return Matrix.Rotation(-pi / 2, 4, Vector((1, 0, 0)))

    def get_gizmo_matrix_height(self, props):
        return Matrix.Translation(((props.number_of_treads + 1) * props.tread_run, 0, 0))

    def get_gizmo_matrix_total_length_target(self, props):
        return Matrix.Translation((0, 0, props.height)) @ Matrix.Rotation(pi / 2, 4, (0, 1, 0))

    def get_gizmo_matrix_nosing_length(self, props):
        return Matrix.Translation((0, 0, props.height / (props.number_of_treads + 1))) @ Matrix.Rotation(
            -pi / 2, 4, (0, 1, 0)
        )

    def get_gizmo_matrix_nosing_depth(self, props):
        return Matrix.Translation(
            (-props.nosing_length, 0, props.height / (props.number_of_treads + 1))
        ) @ Matrix.Rotation(-pi, 4, (0, 1, 0))

    def get_gizmo_matrix_tread_run(self, props):
        return Matrix.Translation((0, 0, props.height / (props.number_of_treads + 1))) @ Matrix.Rotation(
            pi / 2, 4, (0, 1, 0)
        )

    def get_gizmo_matrix_tread_depth(self, props):
        return Matrix.Translation(((props.number_of_treads + 1) * props.tread_run, 0, props.height)) @ Matrix.Rotation(
            pi, 4, (0, 1, 0)
        )

    def get_gizmo_matrix_tread_rise(self, props):
        return Matrix.Translation((props.tread_run, 0, 0))

    def setup(self, context):
        def add_gizmo_prop(prop, move_get_cb_override=None, move_set_cb_override=None):
            gizmo = self.gizmos.new("GIZMO_GT_arrow_3d")

            def move_get_cb(p):
                props = bpy.context.active_object.BIMStairProperties
                return getattr(props, p)

            def move_set_cb(p, value):
                props = bpy.context.active_object.BIMStairProperties
                setattr(props, p, value)

            gizmo.target_set_handler(
                "offset",
                get=lambda: move_get_cb_override(prop) if move_get_cb_override else move_get_cb(prop),
                set=lambda value: (
                    move_set_cb_override(prop, value) if move_set_cb_override else move_set_cb(prop, value)
                ),
            )

            gizmo.color = self.colors.get(prop, (1, 1, 1))
            setattr(self, f"gizmo_{attr_name}", gizmo)
            gizmo.scale_basis = self.gizmo_scale_basis
            gizmo.alpha = 1
            gizmo.alpha_highlight = 1

        for attr_name in self.attr_names:
            if attr_name == "tread_rise":

                def move_get_cb(p):
                    props = bpy.context.active_object.BIMStairProperties
                    return props.height / (props.number_of_treads + 1)

                def move_set_cb(p, value):
                    props = bpy.context.active_object.BIMStairProperties
                    props.height = value * (props.number_of_treads + 1)

                add_gizmo_prop(attr_name, move_get_cb_override=move_get_cb, move_set_cb_override=move_set_cb)
            else:
                add_gizmo_prop(attr_name)

        self.gizmo_lock_length = self.gizmos.new("VIEW3D_GT_lock")

        props = self.gizmo_lock_length.target_set_operator("serendipicad.toggle_gizmo")
        props.obj = context.active_object.name
        props.prop_path = "BIMStairProperties.total_length_lock"
        self.gizmo_lock_length.scale_basis = 0.5
        self.gizmo_lock_length.use_draw_modal = True
        self.gizmo_lock_length.alpha = 1
        self.gizmo_lock_length.alpha_highlight = 1

    def refresh(self, context):
        obj = context.active_object
        props = obj.BIMStairProperties
        for attr_name in self.attr_names:
            gizmo = getattr(self, f"gizmo_{attr_name}", None)
            if gizmo is None:
                continue
            gizmo.matrix_basis = obj.matrix_world @ getattr(self, f"get_gizmo_matrix_{attr_name}")(props)

            if attr_name == "total_length_target":
                gizmo.scale_basis = 0 if props.total_length_lock else self.gizmo_scale_basis
            if attr_name == "tread_depth":
                gizmo.scale_basis = 0 if props.stair_type == "GENERIC" else self.gizmo_scale_basis
            if attr_name == "nosing_depth":
                gizmo.scale_basis = 0 if props.nosing_length == 0 else self.gizmo_scale_basis

        matrix = obj.matrix_world @ Matrix.Rotation(-pi / 2, 4, Vector((0, 0, 1)))
        matrix @= Matrix.Translation((-props.width / 2, props.total_length_target + 0.5, props.height))
        self.gizmo_lock_length.matrix_basis = matrix
        self.gizmo_lock_length.color = self.red if props.total_length_lock else self.green
        self.gizmo_lock_length.color_highlight = [c + 0.5 for c in self.gizmo_lock_length.color]


bpy.utils.register_class(BIM_OT_toggle_gizmo)
bpy.utils.register_class(GizmoLock)
bpy.utils.register_class(MY_GIZMOGROUP_GT)
Tagged:
steverugiNigelRoeltlangtheoryshawbruno_perdigaoAndrej730sjb007walpaduarteframosand 11 others.

Comments

  • @Gorgious
    cool stuff!
    From my personal wishlist: a Gizmo, or some shortcut, to vertically extend the height of walls against a beam, similar to what available with walls/slabs, I currently have to manually take measurements for that.
    ..or is there already and I missed it? ;)
    thanks

    NigelwalpaOwura_quatomkarinca
  • love, love it.
    Playing off @steverugi comment, would be awesome to select multiple gizmos, across multiple objects at the same time, and move them all together.
    idea brought up here: https://github.com/IfcOpenShell/IfcOpenShell/issues/1324#issuecomment-780935328

    steverugi
  • edited February 14

    Another experiment on walls :

    I do think that snapping must be supported before further work on the matter to make it really useful.

    @theoryshaw I believe code could be extended to work on all selected objects rather than active one. Might be very hacky though. I think a constraint based parametric system would be more suitable for your need. eg assign specific properties from several objects to a group and when modifying the group the values are updated and geometry, too.

    Edit : Well it already kinda does

    theoryshawzoomerOwura_quAceatomkarinca
  • Wow, this absolutely amazing, great work.
    While I generally dislike working with gizmos (as opposed to hotkeys), this is a great for usability, and a big step to make Bonsai more user friendly for less technical users.
    This is an excellent step in answer > @Moult question:

    What's stopping us from unhesitatingly recommending Bonsai (...)?

    To make it truly usable I also think supporting snapping is a must though, since in this field we tend to dislike eyeballing stuff. ;)
    It does not sound trivial though, especially with multiple selections

    Talmacsi
  • @Gorgious

    I do think that snapping must be supported before further work on the matter to make it really useful.

    1000+
    @duarteframos
    I too prefer hotkey, for smoother workflow, but maybe it's a matter of taste. For vertically extend walls against beams I won't complain if a gizmo were available right now though :D

    duarteframos
  • bring in the snap doctor! ;) @bruno_perdigao

    steverugiduarteframosemiliotassoMassimoviktorbruno_perdigaoOwura_quGorgiousTalmacsi
  • edited February 15

    I love Gizmos everywhere for everything !
    Reliable and easy locking translations in 3D to one or more axes I helpful.

    Owura_qu
  • Yes I think this is definitely planned, ill have a chat with Bruno :)

    duarteframosOwura_quGorgious
Sign In or Register to comment.