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

247 lines
9.8 KiB
Python

'''
Copyright (C) 2023 CG Cookie
http://cgcookie.com
hello@cgcookie.com
Created by Jonathan Denning, Jonathan Williamson
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 random
from mathutils import Matrix, Vector
from ..rfwidget import RFWidget
from ...addon_common.common.fsm import FSM
from ...addon_common.common.globals import Globals
from ...addon_common.common import gpustate
from ...addon_common.common.blender import tag_redraw_all
from ...addon_common.common.drawing import DrawCallbacks
from ...addon_common.common.boundvar import BoundBool, BoundInt, BoundFloat
from ...addon_common.common.maths import Vec, Point, Point2D, Direction, Color, Vec2D
from ...config.options import themes
class RFWidget_BrushStroke_Factory:
'''
This is a class factory. It is needed, because the FSM is shared across instances.
RFTools might need to share RFWidgets that are independent of each other.
'''
@staticmethod
def create(action_name, radius, outer_border_color=Color((0,0,0,0.5)), outer_color=Color((1,1,1,1)), inner_color=Color((1,1,1,0.5)), below_alpha=Color((1,1,1,0.25))):
class RFWidget_BrushStroke(RFWidget):
rfw_name = 'Brush Stroke'
rfw_cursor = 'CROSSHAIR'
@RFWidget.on_init
def init(self):
self.action_name = action_name
self.stroke2D = []
self.tightness = 0.95
self.redraw_on_mouse = True
self.sizing_pos = None
self.outer_border_color = outer_border_color
self.outer_color = outer_color
self.inner_color = inner_color
self.color_mult_below = below_alpha
self.last_mouse = None
self.last_view = None
self.hit = False
self.hit_scale = 1.0
self.inner_radius = 10.0
@FSM.on_state('main', 'enter')
def modal_main_enter(self):
self.rfw_cursor = 'CROSSHAIR'
tag_redraw_all('BrushStroke main_enter')
@FSM.on_state('main')
def modal_main(self):
self.update_mouse()
if self.actions.pressed('insert'):
return 'stroking'
if self.rfcontext.actions.pressed('brush radius increase'):
self.radius += 10
tag_redraw_all('BrushStroke increase radius')
return
if self.rfcontext.actions.pressed('brush radius decrease'):
self.radius -= 10
tag_redraw_all('BrushStroke decrease radius')
return
if self.actions.pressed('brush radius'):
return 'brush sizing'
def inactive_passthrough(self):
if self.actions.pressed('brush radius'):
self._fsm.force_set_state('brush sizing')
return True
@FSM.on_state('stroking', 'enter')
def modal_line_enter(self):
xy = self.actions.mouse
p,n,_,_ = self.rfcontext.raycast_sources_mouse()
if p: xy = self.rfcontext.Point_to_Point2D(p)
self.stroke2D = [xy]
tag_redraw_all('BrushStroke line_enter')
@FSM.on_state('stroking')
def modal_line(self):
self.update_mouse()
if self.actions.released('insert'):
# TODO: tessellate the last steps?
xy = self.actions.mouse
p,n,_,_ = self.rfcontext.raycast_sources_mouse()
if p: xy = self.rfcontext.Point_to_Point2D(p)
self.stroke2D.append(xy)
self.callback_actions(self.action_name)
return 'main'
if self.actions.pressed('cancel'):
self.stroke2D = []
self.actions.unuse('insert', ignoremods=True, ignoremulti=True)
return 'main'
xy = self.actions.mouse
p,n,_,_ = self.rfcontext.raycast_sources_mouse()
if p: xy = self.rfcontext.Point_to_Point2D(p)
lpos, cpos = self.stroke2D[-1], xy
npos = lpos + (cpos - lpos) * (1 - self.tightness)
self.stroke2D.append(npos)
tag_redraw_all('BrushStroke line')
self.callback_actioning(self.action_name)
@FSM.on_state('stroking', 'exit')
def modal_line_exit(self):
tag_redraw_all('BrushStroke line_exit')
@FSM.on_state('brush sizing', 'enter')
def modal_brush_sizing_enter(self):
if self.actions.mouse.x > self.actions.size.x / 2:
self.sizing_pos = self.actions.mouse - Vec2D((self.radius, 0))
else:
self.sizing_pos = self.actions.mouse + Vec2D((self.radius, 0))
self.rfw_cursor = 'MOVE_X'
tag_redraw_all('BrushStroke brush_sizing_enter')
@FSM.on_state('brush sizing')
def modal_brush_sizing(self):
if self.actions.pressed('confirm'):
self.radius = (self.sizing_pos - self.actions.mouse).length
return 'main'
if self.actions.pressed('cancel'):
return 'main'
###################
# radius
@property
def radius(self):
return radius.get()
@radius.setter
def radius(self, v):
radius.set(max(1, float(v)))
def get_radius_boundvar(self):
return radius
###################
# draw functions
@DrawCallbacks.on_draw('post3d')
@FSM.onlyinstate({'main','stroking'})
def draw_brush(self):
if not self.hit: return
p, n = self.hit_p, self.hit_n
ro = self.radius * self.hit_scale
rh = self.inner_radius * self.hit_scale # ro * 0.5
co, ci, cb = self.outer_color, self.inner_color, self.outer_border_color
gpustate.depth_mask(False)
fwd = Direction(self.rfcontext.Vec_forward()) * (self.hit_depth * 0.0005)
# draw below
gpustate.depth_test('GREATER_EQUAL')
Globals.drawing.draw3D_circle(p - fwd * 1.0, ro, cb * self.color_mult_below, n=n, width=8*self.hit_scale)
Globals.drawing.draw3D_circle(p - fwd * 2.0, ro, co * self.color_mult_below, n=n, width=2*self.hit_scale)
Globals.drawing.draw3D_circle(p - fwd * 2.0, rh, ci * self.color_mult_below, n=n, width=2*self.hit_scale)
# draw above
gpustate.depth_test('LESS_EQUAL')
Globals.drawing.draw3D_circle(p - fwd * 1.0, ro, cb, n=n, width=8*self.hit_scale)
Globals.drawing.draw3D_circle(p - fwd * 2.0, ro, co, n=n, width=2*self.hit_scale)
Globals.drawing.draw3D_circle(p - fwd * 2.0, rh, ci, n=n, width=2*self.hit_scale)
# reset
gpustate.depth_test('LESS_EQUAL')
gpustate.depth_mask(True)
@DrawCallbacks.on_draw('post2d')
@FSM.onlyinstate('stroking')
def draw_line(self):
# draw brush strokes (screen space)
#cr,cg,cb,ca = self.line_color
gpustate.blend('ALPHA')
Globals.drawing.draw2D_linestrip(self.stroke2D, themes['stroke'], width=2, stipple=[5, 5])
@DrawCallbacks.on_draw('post2d')
@FSM.onlyinstate('brush sizing')
def draw_brush_sizing(self):
gpustate.blend('ALPHA')
r = (self.sizing_pos - self.actions.mouse).length
rh = self.inner_radius
# Globals.drawing.draw2D_circle(self.sizing_pos, r*0.75, self.fill_color, width=r*0.5)
Globals.drawing.draw2D_circle(self.sizing_pos, r*1.0, self.outer_border_color, width=7)
Globals.drawing.draw2D_circle(self.sizing_pos, r*1.0, self.outer_color, width=1)
Globals.drawing.draw2D_circle(self.sizing_pos, rh, self.inner_color, width=1)
##################
# mouse
def update_mouse(self):
recompute = False
recompute |= (self.last_mouse != self.actions.mouse)
recompute |= (self.last_view != self.rfcontext.get_view_version())
if not recompute: return
self.last_mouse = self.actions.mouse
self.last_view = self.rfcontext.get_view_version()
self.hit = False
p,n,_,_ = self.rfcontext.raycast_sources_mouse()
if not p: return
depth = self.rfcontext.Point_to_depth(p)
if not depth: return
scale = self.rfcontext.size2D_to_size(1.0, depth)
if scale is None: return
self.hit = True
self.hit_scale = scale
self.hit_p = p
self.hit_n = n
self.hit_depth = depth
return RFWidget_BrushStroke