Files
blender-portable-repo/scripts/addons/RetopoFlow/retopoflow/rf/rf_piemenu.py
T
2026-03-17 14:30:01 -06:00

167 lines
7.4 KiB
Python

'''
Copyright (C) 2023 CG Cookie
http://cgcookie.com
hello@cgcookie.com
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
import math
import time
import random
from itertools import chain
from collections import deque
from ..rfmesh.rfmesh_wrapper import RFVert, RFEdge, RFFace
from ...addon_common.cookiecutter.cookiecutter import CookieCutter
from ...addon_common.common.blender import tag_redraw_all
from ...addon_common.common.decorators import timed_call
from ...addon_common.common.drawing import Cursors
from ...addon_common.common.fsm import FSM
from ...addon_common.common.maths import Vec2D, Point2D, RelPoint2D, Direction2D
from ...addon_common.common.profiler import profiler
from ...addon_common.common.ui_core import UI_Element
from ...addon_common.common.utils import normalize_triplequote, Dict
from ...config.options import options, retopoflow_files
from ...addon_common.common.timerhandler import StopwatchHandler
class RetopoFlow_PieMenu:
def which_pie_menu_section(self):
delta = self.actions.mouse - self.pie_menu_center
if delta.length < self.pie_menu_center_size / 2: return None
count = len(self.pie_menu_options)
clock_deg = (math.atan2(-delta.y, delta.x) * 180 / math.pi - self.pie_menu_rotation) % 360
section = math.floor((clock_deg + 360 / count / 2) % 360 / (360 / count))
return section
@staticmethod
def _estimate_text_height(text, wrap_width):
font_w, font_h = 6, 16 # very rough approximation!
line_count = 0
for line in text.splitlines():
text_w = max(1, len(line)) * font_w # approx width of text w/o wrapping
line_count += math.ceil(text_w / wrap_width) # approx num of lines w/ wrapping
text_h = max(1, line_count) * font_h # approx height of text w/ wrapping
return text_h
@FSM.on_state('pie menu', 'enter')
def pie_menu_enter(self):
scale = self.drawing.scale
doc_h = self.document.body.height_pixels
menu = Dict(
size = 512, # size of full menu
radius = 204, # size of option center ring (40% of menu)
option = 72, # size of option
inner = 100, # size of inner circle
)
center = self.actions.mouse - Vec2D((scale(menu.size) / 2, -scale(menu.size) / 2)) - Vec2D((0, doc_h))
ui_pie_menu_contents = self.ui_pie_menu.getElementById('pie-menu-contents')
ui_pie_menu_contents.clear_children()
ui_pie_menu_contents.style = ';'.join([
f'left:{center.x}px',
f'top:{center.y}px',
f'width:{menu.size}px',
f'height:{menu.size}px',
f'border-radius:{menu.size // 2}px',
f'padding:{menu.size // 2}px',
])
count = len(self.pie_menu_options)
self.ui_pie_sections = []
for i_option, option in enumerate(self.pie_menu_options):
if not option:
self.ui_pie_sections.append(None)
continue
if type(option) is str: option = (option,)
if type(option) is tuple: option = { k:v for k,v in zip(['text', 'value', 'image'], option) }
option.setdefault('value', option['text'])
option.setdefault('image', '')
self.pie_menu_options[i_option] = option['value']
r = ((i_option / count) * 360 + self.pie_menu_rotation) * (math.pi / 180)
left, top = scale(menu.radius) * math.cos(r) - (scale(menu.option)/2), -(scale(menu.radius) * math.sin(r) - (scale(menu.option)/2))
label = UI_Element.DIV(classes='pie-menu-option-text', innerText=option['text'])
image = None
highlight_class = 'highlighted' if option['value'] == self.pie_menu_highlighted else ''
if option['image']:
image = UI_Element.IMG(classes='pie-menu-option-image', src=option['image'], style=f'width:{menu.option}px')
else:
# TODO: actually handle vertical-align: middle!
text_h = self._estimate_text_height(option['text'], menu.option) # very rough approximation!
margin = (menu.option - text_h) // 2 # offset using margin
label.style = f'margin-top:{margin}px'
ui = UI_Element.DIV(
style=';'.join([
f'left:{int(left)}px',
f'top:{int(top)}px',
f'width:{menu.option}px',
f'height:{menu.option}px',
]),
classes=f"pie-menu-option {highlight_class}",
children=list(filter(None, [ label, image ])),
parent=ui_pie_menu_contents,
)
self.ui_pie_sections.append(ui)
UI_Element.DIV(
style=';'.join([
f'left:{-scale(menu.inner) // 2}px',
f'top:{scale(menu.inner) // 2}px',
f'width:{menu.inner}px',
f'height:{menu.inner}px',
f'border-radius:{menu.inner // 2}px',
]),
classes=f'pie-menu-inner',
parent=ui_pie_menu_contents,
)
self.ui_pie_menu.is_visible = True
self.pie_menu_center = self.actions.mouse
self.pie_menu_center_size = scale(menu.inner)
self.pie_menu_mouse = self.actions.mouse
self.document.focus(self.ui_pie_menu)
self.document.force_clean(self.actions.context)
@FSM.on_state('pie menu')
def pie_menu_main(self):
confirm_p = self.actions.pressed('pie menu confirm', ignoremods=True)
confirm_r = self.actions.released(self.pie_menu_release, ignoremods=True)
if confirm_p or confirm_r:
# setting display to none in case callback needs to show some UI
self.ui_pie_menu.is_visible = False
i_option = self.which_pie_menu_section()
option = self.pie_menu_options[i_option] if i_option is not None else None
if option is not None or self.pie_menu_always_callback:
self.pie_menu_callback(option)
return 'main' if confirm_r else 'pie menu wait'
if self.actions.pressed('cancel'):
return 'pie menu wait'
i_section = self.which_pie_menu_section()
for i_s,ui in enumerate(self.ui_pie_sections):
if not ui: continue
if i_s == i_section:
ui.add_pseudoclass('hover')
else:
ui.del_pseudoclass('hover')
@FSM.on_state('pie menu', 'exit')
def pie_menu_exit(self):
self.ui_pie_menu.is_visible = False
@FSM.on_state('pie menu wait')
def pie_menu_wait(self):
if self.actions.released(self.pie_menu_release, ignoremods=True):
return 'main'