Page MenuHome

[WIP] Bevel: Constant Radius Mode
Needs ReviewPublic

Authored by Hans Goudey (HooglyBoogly) on Dec 11 2019, 12:33 AM.
Tokens
"Love" token, awarded by MetinSeven."Love" token, awarded by manitwo."Love" token, awarded by amonpaike."Love" token, awarded by franMarz."Love" token, awarded by duarteframos."Like" token, awarded by diego_mb."Love" token, awarded by rl.amorato.

Details

Summary

The Problem

The way bevel handles non-square corners doesn’t reflect bevels from the real world. In the real world, bevels usually have a constant size and shape defined by the tool, and they don’t change based on the angle of the corner being beveled. But in Blender, the beveled corners do not necessarily have a consistent radius.

The Solution

Currently, bevel globally specifies how far along a vertice’s adjacent edges to attach the ends of the profile. Instead, with this solution applied, bevel will specify a global bevel radius. A circle of that radius will be placed between the adjacent edges at the location where each edge connects tangentially to the circle. Then the profile becomes a different portion of a circle depending on the angle of the corner.

UI

There is a new offset type, "Radius," and a new "Profile Type" enum, with three options, "Radius," "Superellipse," and "Custom." There are probably better names.

To Do

The 2D shapes above work correctly, but in 3D when the beveled edge turns it's not quite right. The offsets are changed slightly from what they're set to at the beginning by the offset_adjust code.

This is the test file I've been using:

Depends on D6661 and D6658

Diff Detail

Repository
rB Blender

Event Timeline

Hans Goudey (HooglyBoogly) edited the summary of this revision. (Show Details)
Hans Goudey (HooglyBoogly) edited the summary of this revision. (Show Details)
Hans Goudey (HooglyBoogly) edited the summary of this revision. (Show Details)
  • Remove some print statements
  • Use the original calculation for offset length when building the profile

Will we need an even-more-generalized version of the overflow popover from D5832: UI: Add the rest of bevel's options to the active tool to handle the UI end of this? Something like

  • with some tweaks to layout.popover() might work.

    @Asher (ThatAsherGuy) That looks great! And the other options are in the icon dropdown?

    And it's clever to use extra as a sort of enum.

    Is there Python code for that? Might be worth posting that in a paste for now? It might be a bit late for 2.82.

    In general the "Profile Type" idea makes some of the UI difficult. With "Radius" selected there are no options, then a slider for "Superellipse," and a whole widget for "Custom."

    Right now I'm basing this off of the commit before the overlay refactor because of T72152, but the UI commit could be merged in to have both changes together.

    @Hans Goudey (HooglyBoogly) Here's what I did in space_toolsystem_toolbar:

    1 @ToolDef.from_fn
    2 def bevel():
    3 def draw_settings(context, layout, tool, *, extra='0'):
    4 props = tool.operator_properties("mesh.bevel")
    5 region_type = context.region.type
    6 row = layout.row(align=True)
    7 row.ui_units_x = 6
    8
    9 if extra == '0':
    10 if props.offset_type == 'PERCENT':
    11 row.prop(props, "offset_pct")
    12 else:
    13 offset_text = "Width"
    14 if props.offset_type == 'DEPTH':
    15 offset_text = "Depth"
    16 elif props.offset_type == 'OFFSET':
    17 offset_text = "Offset"
    18 row.prop(props, "offset", text=offset_text)
    19 if region_type == 'TOOL_HEADER':
    20 row.popover("TOPBAR_PT_tool_settings_extra_1", text="")
    21 else:
    22 layout.prop(props, "offset_type")
    23
    24 row = layout.row(align=True)
    25 row.ui_units_x = 5
    26 row.prop(props, "segments")
    27
    28 row = layout.row(align=True)
    29 row.ui_units_x = 6
    30 row.prop(props, "profile", slider=True)
    31 if region_type == 'TOOL_HEADER':
    32 row.popover("TOPBAR_PT_tool_settings_extra_2", text="")
    33
    34 if region_type == 'TOOL_HEADER':
    35 layout.popover("TOPBAR_PT_tool_settings_extra_3", text="", icon='TOOL_SETTINGS')
    36 else:
    37 extra = '3'
    38
    39 if extra == '1':
    40 if not region_type == 'TOOL_HEADER':
    41 extra = '2'
    42 col = layout.column(align=True)
    43
    44 sub = col.row(align=True)
    45 sub.prop(props, "offset_type", expand=True)
    46
    47 col.prop(props, "vertex_only")
    48 col.prop(props, "clamp_overlap")
    49 col.prop(props, "loop_slide")
    50
    51 if extra == '2':
    52 if not region_type == 'TOOL_HEADER':
    53 extra = '3'
    54 row = layout.row(align=True)
    55 layout.prop(props, "use_custom_profile")
    56 if props.use_custom_profile:
    57 tool_settings = context.tool_settings
    58 layout.template_curveprofile(tool_settings, "custom_bevel_profile_preset")
    59
    60 if (extra == '3') or (region_type != 'TOOL_HEADER'):
    61 if not region_type == 'TOOL_HEADER':
    62 layout.prop(props, "vertex_only")
    63 layout.prop(props, "clamp_overlap")
    64 layout.prop(props, "loop_slide")
    65
    66 layout.prop(props, "mark_seam")
    67 layout.prop(props, "mark_sharp")
    68 layout.prop(props, "harden_normals")
    69
    70 layout.prop(props, "material")
    71
    72 col = layout.column(align=False)
    73 col.alignment = 'EXPAND'
    74 col.use_property_split = True
    75 col.use_property_decorate = False
    76 col.prop(props, "miter_outer", text="Outer Miter")
    77 col.prop(props, "miter_inner", text="Inner Miter")
    78 if props.miter_inner == 'ARC':
    79 layout.prop(props, "spread")
    80
    81 if not region_type == 'TOOL_HEADER':
    82 layout.prop(props, "use_custom_profile")
    83 if props.use_custom_profile:
    84 tool_settings = context.tool_settings
    85 layout.template_curveprofile(tool_settings, "custom_bevel_profile_preset")
    86
    87 return dict(
    88 idname="builtin.bevel",
    89 label="Bevel",
    90 icon="ops.mesh.bevel",
    91 widget="VIEW3D_GGT_tool_generic_handle_normal",
    92 keymap=(),
    93 draw_settings=draw_settings,
    94 )
    and complimentary tweaks to space_topbar.py:
    1class TOPBAR_PT_tool_settings_extra_1(Panel):
    2 """
    3 Popover panel for adding extra options that don't fit in the tool settings header
    4 """
    5 bl_idname = "TOPBAR_PT_tool_settings_extra_1"
    6 bl_region_type = 'HEADER'
    7 bl_space_type = 'TOPBAR'
    8 bl_label = "Extra Options"
    9
    10 def draw(self, context):
    11 from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
    12 layout = self.layout
    13
    14 # Get the active tool
    15 space_type, mode = ToolSelectPanelHelper._tool_key_from_context(context)
    16 cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
    17 item, tool, _ = cls._tool_get_active(context, space_type, mode, with_icon=True)
    18 if item is None:
    19 return
    20
    21 # Draw the extra settings
    22 item.draw_settings(context, layout, tool, extra='1')
    23
    24
    25class TOPBAR_PT_tool_settings_extra_2(Panel):
    26 """
    27 Popover panel for adding extra options that don't fit in the tool settings header
    28 """
    29 bl_idname = "TOPBAR_PT_tool_settings_extra_2"
    30 bl_region_type = 'HEADER'
    31 bl_space_type = 'TOPBAR'
    32 bl_label = "Extra Options"
    33
    34 def draw(self, context):
    35 from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
    36 layout = self.layout
    37
    38 # Get the active tool
    39 space_type, mode = ToolSelectPanelHelper._tool_key_from_context(context)
    40 cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
    41 item, tool, _ = cls._tool_get_active(context, space_type, mode, with_icon=True)
    42 if item is None:
    43 return
    44
    45 # Draw the extra settings
    46 item.draw_settings(context, layout, tool, extra='2')
    47
    48class TOPBAR_PT_tool_settings_extra_3(Panel):
    49 """
    50 Popover panel for adding extra options that don't fit in the tool settings header
    51 """
    52 bl_idname = "TOPBAR_PT_tool_settings_extra_3"
    53 bl_region_type = 'HEADER'
    54 bl_space_type = 'TOPBAR'
    55 bl_label = "Extra Options"
    56
    57 def draw(self, context):
    58 from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
    59 layout = self.layout
    60
    61 # Get the active tool
    62 space_type, mode = ToolSelectPanelHelper._tool_key_from_context(context)
    63 cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
    64 item, tool, _ = cls._tool_get_active(context, space_type, mode, with_icon=True)
    65 if item is None:
    66 return
    67
    68 # Draw the extra settings
    69 item.draw_settings(context, layout, tool, extra='3')

    It's generic enough that you can use it to add up to three popovers to something, but it isn't generic enough to support more than three.

    Interesting. It looks like we need a way to give the popover some information about which UI it should draw. Maybe updating popover is the way to go. Not sure though.

    Yeah, I haven't done enough with the C side of the UI code to have a good sense of how much work that'd take. @Harley Acheson (harley) might be able to give us some insights, though.

    I am wondering about the effect you intend for the Cube.003 object in the latest version of your test file. You have a circle aligned showing that you intend the tangent circle to go between the two non-beveled edges between the beveled ones. That makes sense from one point of view -- looking at the curvature of the profile at that corner. But it has several downsides:

    1. What do you do if there are not exactly two non-beveled edges between two beveled ones?
    2. The other end of the edge may have a completely different angle, and so the bevel width will be different there, and so the bevel width will be variable over the length of the edge. That may be OK, but it is also not usually what is desired. So I don't think this should be the default width method if we go with this idea. In any case, you need to prevent "adjust_offsets" from running if you are doing this, since the whole point of that is to make the widths as even as possible along the length of an edge.

    An alternative idea would be to make the circle tangent to the planes that the beveled edge is between. That is, use the angle between the planes to set the offset that gives a tangent circle of a given radius.

    Hans Goudey (HooglyBoogly) planned changes to this revision.Mon, Dec 30, 9:35 PM

    An alternative idea would be to make the circle tangent to the planes that the beveled edge is between. That is, use the angle between the planes to set the offset that gives a tangent circle of a given radius.

    I think I tried this at one point but decided to pursue using the angle between the edges that constrain the profile. I would be happy to check it out again though. If there are two planes on both sides of the profile (i.e. a bevel chain), would you average the angles between both of the planes?

    Both methods have their downsides though. Using the two edges that the profile start and end slide along guarantees (at least in many situations) that the radius will be the specified radius, whereas I think using the angle between the planes will do that less often.

    One of the trickier parts is getting the initial offset_r set properly and having the subsequent calculate_profile call agree with that offset. One way to do that is to always use the angle between the offset edges, but I think that doesn't work as well when the boundverts don't exactly slide along edges.

    The non-chain bevels definitely cause some issues here, but I think answering these questions generally should help clarify what to do there.

    Here is an example of varying bevel offsets along a chain based on a changing angle. Each bevel circle has the same radius. This is a pretty simple situation but it looks pretty good to me.

    Hans Goudey (HooglyBoogly) edited the summary of this revision. (Show Details)
    • Working versioning
    • Disabled offset_adjust
    • Rebased to recent bevel patches D6661 and D6658

    In most basic situations it works now, but it still needs further testing and refinement.

    • Update active tool UI
    • Remove two empty lines
    Hans Goudey (HooglyBoogly) retitled this revision from [WIP] Bevel: Add Radius Offset Type and Profile Method to [WIP] Bevel: Constant Radius Mode.Fri, Jan 24, 1:15 AM

    If, as you say, this needs further testing and refinement, should I wait until you think it is ready? Or would you like to get this into the 2.83 beta to get users to try it out now? As far as I can tell, this should not affect existing uses of bevel (right?), so there is low risk. But we do want to make sure that this doesn't turn into a source of a large number of immediate bug reports.

    If, as you say, this needs further testing and refinement, should I wait until you think it is ready? Or would you like to get this into the 2.83 beta to get users to try it out now? As far as I can tell, this should not affect existing uses of bevel (right?), so there is low risk. But we do want to make sure that this doesn't turn into a source of a large number of immediate bug reports.

    There's still two pretty obvious bugs that I'd like to fix before getting this into 2.83, but I agree it's a low risk thing to add, so that would be a good place to test and get feedback.