Gizmos for parametric objects in Bonsai
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:
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
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
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
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:
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
@Gorgious
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
bring in the snap doctor! ;) @bruno_perdigao
I love Gizmos everywhere for everything !
Reliable and easy locking translations in 3D to one or more axes I helpful.
Yes I think this is definitely planned, ill have a chat with Bruno :)
https://github.com/IfcOpenShell/IfcOpenShell/issues/6166