2025-07-01
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
'''
|
||||
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'
|
||||
Reference in New Issue
Block a user