2025-07-01
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
'''
|
||||
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/>.
|
||||
'''
|
||||
|
||||
__all__ = []
|
||||
@@ -0,0 +1,307 @@
|
||||
'''
|
||||
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
|
||||
|
||||
import gpu
|
||||
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
from ..rfwidget import RFWidget
|
||||
|
||||
from ...addon_common.common.fsm import FSM
|
||||
from ...addon_common.common.blender import tag_redraw_all
|
||||
from ...addon_common.common.boundvar import BoundBool, BoundInt, BoundFloat
|
||||
from ...addon_common.common.drawing import DrawCallbacks
|
||||
from ...addon_common.common.globals import Globals
|
||||
from ...addon_common.common import gpustate
|
||||
from ...addon_common.common.maths import Vec, Point, Point2D, Direction, Color, Vec2D
|
||||
from ...config.options import themes, options
|
||||
|
||||
|
||||
class RFWidget_BrushFalloff_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, falloff, strength, fill_color=Color((1,1,1,1)), 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_BrushFalloff(RFWidget):
|
||||
rfw_name = 'Brush Falloff'
|
||||
rfw_cursor = 'CROSSHAIR'
|
||||
|
||||
@RFWidget.on_init
|
||||
def init(self):
|
||||
self.action_name = action_name
|
||||
self.outer_color = outer_color
|
||||
self.inner_color = inner_color
|
||||
self.fill_color = fill_color
|
||||
self.color_mult_below = below_alpha
|
||||
self.redraw_on_mouse = True
|
||||
self.last_mouse = None
|
||||
self.last_view = None
|
||||
self.hit = False
|
||||
self.hit_scale = 1.0
|
||||
|
||||
@FSM.on_state('main')
|
||||
def main(self):
|
||||
self.update_mouse()
|
||||
|
||||
if self.rfcontext.actions.pressed('brush radius increase'):
|
||||
self.radius += 10
|
||||
tag_redraw_all('BrushFalloff increase radius')
|
||||
return
|
||||
if self.rfcontext.actions.pressed('brush radius decrease'):
|
||||
self.radius -= 10
|
||||
tag_redraw_all('BrushFalloff decrease radius')
|
||||
return
|
||||
|
||||
if self.rfcontext.actions.pressed('brush radius'):
|
||||
self._dist_to_var_fn = self.dist_to_radius
|
||||
self._var_to_dist_fn = self.radius_to_dist
|
||||
return 'change'
|
||||
if self.rfcontext.actions.pressed('brush strength'):
|
||||
self._dist_to_var_fn = self.dist_to_strength
|
||||
self._var_to_dist_fn = self.strength_to_dist
|
||||
return 'change'
|
||||
if self.rfcontext.actions.pressed('brush falloff'):
|
||||
self._dist_to_var_fn = self.dist_to_falloff
|
||||
self._var_to_dist_fn = self.falloff_to_dist
|
||||
return 'change'
|
||||
|
||||
@FSM.on_state('change', 'enter')
|
||||
def change_enter(self):
|
||||
dist = self._var_to_dist_fn()
|
||||
actions = self.rfcontext.actions
|
||||
self._change_pre = dist
|
||||
self._change_center = actions.mouse - Vec2D((dist, 0))
|
||||
self._timer = self.actions.start_timer(120)
|
||||
tag_redraw_all('BrushFalloff change_enter')
|
||||
|
||||
@FSM.on_state('change')
|
||||
def change(self):
|
||||
assert self._dist_to_var_fn
|
||||
actions = self.rfcontext.actions
|
||||
|
||||
if actions.pressed('cancel', ignoremods=True, ignoredrag=True):
|
||||
self._dist_to_var_fn(self._change_pre)
|
||||
return 'main'
|
||||
if actions.pressed({'confirm', 'confirm drag'}, ignoremods=True):
|
||||
return 'main'
|
||||
|
||||
dist = (self._change_center - actions.mouse).length
|
||||
self._dist_to_var_fn(dist)
|
||||
|
||||
@FSM.on_state('change', 'exit')
|
||||
def change_exit(self):
|
||||
self._dist_to_var_fn = None
|
||||
self._var_to_dist_fn = None
|
||||
self._timer.done()
|
||||
tag_redraw_all('BrushFalloff change_exit')
|
||||
|
||||
@DrawCallbacks.on_draw('post3d')
|
||||
@FSM.onlyinstate('main')
|
||||
def draw_brush(self):
|
||||
if not self.hit: return
|
||||
|
||||
ff = math.pow(0.5, 1.0 / max(self.falloff, 0.0001))
|
||||
p, n = self.hit_p, self.hit_n
|
||||
ro = self.radius * self.hit_scale
|
||||
ri = ro * ff
|
||||
rm = (ro + ri) / 2.0
|
||||
co, ci, cc = self.outer_color, self.inner_color, self.fill_color * self.fill_color_scale
|
||||
|
||||
fwd = Direction(self.rfcontext.Vec_forward()) * (self.hit_depth * 0.0005)
|
||||
|
||||
# draw below
|
||||
gpustate.depth_mask(False)
|
||||
gpustate.depth_test('GREATER')
|
||||
Globals.drawing.draw3D_circle(p-fwd*1.0, rm, cc * self.color_mult_below, n=n, width=ro - ri)
|
||||
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, ri, 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, rm, cc, n=n, width=ro - ri)
|
||||
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, ri, ci, n=n, width=2*self.hit_scale)
|
||||
|
||||
# reset
|
||||
gpustate.depth_test('LESS_EQUAL')
|
||||
gpustate.depth_mask(True)
|
||||
|
||||
@DrawCallbacks.on_draw('post2d')
|
||||
@FSM.onlyinstate('change')
|
||||
def draw_brush_sizing(self):
|
||||
#r = (self._change_center - self.actions.mouse).length
|
||||
r = self.radius
|
||||
co = self.outer_color
|
||||
ci = self.inner_color
|
||||
cc = self.fill_color * self.fill_color_scale
|
||||
ff = math.pow(0.5, 1.0 / max(self.falloff, 0.0001))
|
||||
fs = (1-ff) * self.radius
|
||||
Globals.drawing.draw2D_circle(self._change_center, r-fs/2, cc, width=fs)
|
||||
Globals.drawing.draw2D_circle(self._change_center, r, co, width=1)
|
||||
Globals.drawing.draw2D_circle(self._change_center, r*ff, ci, width=1)
|
||||
|
||||
|
||||
##################
|
||||
# getters
|
||||
|
||||
def get_scaled_radius(self):
|
||||
return self.hit_scale * self.radius
|
||||
|
||||
def get_scaled_size(self):
|
||||
return self.hit_scale * self.size
|
||||
|
||||
def get_strength_dist(self, dist:float):
|
||||
return max(0.0, min(1.0, (1.0 - math.pow(dist / self.get_scaled_radius(), self.falloff)))) * self.strength
|
||||
|
||||
def get_strength_Point(self, point:Point):
|
||||
if not self.hit_p: return 0.0
|
||||
return self.get_strength_dist((point - self.hit_p).length)
|
||||
|
||||
|
||||
###################
|
||||
# radius
|
||||
|
||||
@property
|
||||
def radius(self):
|
||||
return radius.get()
|
||||
@radius.setter
|
||||
def radius(self, v):
|
||||
radius.set(max(1, float(v)))
|
||||
|
||||
def radius_to_dist(self):
|
||||
return self.radius
|
||||
|
||||
def dist_to_radius(self, d):
|
||||
self.radius = max(1, int(d))
|
||||
|
||||
def radius_gettersetter(self):
|
||||
def getter():
|
||||
return int(self.radius)
|
||||
def setter(v):
|
||||
self.radius = max(1, int(v))
|
||||
return (getter, setter)
|
||||
|
||||
def get_radius_boundvar(self):
|
||||
return radius
|
||||
|
||||
##################
|
||||
# strength
|
||||
|
||||
@property
|
||||
def strength(self):
|
||||
return strength.get()
|
||||
@strength.setter
|
||||
def strength(self, v):
|
||||
# print('strength', v)
|
||||
strength.set(max(0.01, min(1.0, float(v))))
|
||||
|
||||
def strength_to_dist(self):
|
||||
return self.radius * (1.0 - self.strength)
|
||||
|
||||
def dist_to_strength(self, d):
|
||||
self.strength = 1.0 - max(0.01, min(1.0, d / self.radius))
|
||||
|
||||
def strength_gettersetter(self):
|
||||
def getter():
|
||||
return int(self.strength * 100)
|
||||
def setter(v):
|
||||
self.strength = max(1, min(100, v)) / 100
|
||||
return (getter, setter)
|
||||
|
||||
def get_strength_boundvar(self):
|
||||
return strength
|
||||
|
||||
##################
|
||||
# falloff
|
||||
|
||||
@property
|
||||
def falloff(self):
|
||||
return falloff.get()
|
||||
@falloff.setter
|
||||
def falloff(self, v):
|
||||
# print('falloff', v)
|
||||
falloff.set(max(0.0, min(100.0, float(v))))
|
||||
|
||||
def falloff_to_dist(self):
|
||||
return self.radius * math.pow(0.5, 1.0 / max(self.falloff, 0.0001))
|
||||
|
||||
def dist_to_falloff(self, d):
|
||||
self.falloff = math.log(0.5) / math.log(max(0.01, min(0.99, d / self.radius)))
|
||||
|
||||
def falloff_gettersetter(self):
|
||||
def getter():
|
||||
return int(100 * math.pow(0.5, 1.0 / max(self.falloff, 0.0001)))
|
||||
def setter(v):
|
||||
self.falloff = math.log(0.5) / math.log(max(0.01, min(0.99, v / 100)))
|
||||
pass
|
||||
return (getter, setter)
|
||||
|
||||
def get_falloff_boundvar(self):
|
||||
return falloff
|
||||
|
||||
##################
|
||||
# fill_color_scale
|
||||
|
||||
@property
|
||||
def fill_color_scale(self):
|
||||
return Color((1, 1, 1, self.strength * (options['brush max alpha'] - options['brush min alpha']) + options['brush min alpha']))
|
||||
|
||||
##################
|
||||
# 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
|
||||
|
||||
# figure out how much to scale so that the brush drawn in 3D appears the same size on screen
|
||||
xy = self.actions.mouse
|
||||
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
|
||||
|
||||
rmat = Matrix.Rotation(Direction.Z.angle(n), 4, Direction.Z.cross(n))
|
||||
|
||||
self.hit = True
|
||||
self.hit_scale = scale
|
||||
self.hit_p = p
|
||||
self.hit_n = n
|
||||
self.hit_depth = depth
|
||||
self.hit_x = Vec(rmat @ Direction.X)
|
||||
self.hit_y = Vec(rmat @ Direction.Y)
|
||||
self.hit_z = Vec(rmat @ Direction.Z)
|
||||
self.hit_rmat = rmat
|
||||
|
||||
return RFWidget_BrushFalloff
|
||||
@@ -0,0 +1,246 @@
|
||||
'''
|
||||
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
|
||||
@@ -0,0 +1,60 @@
|
||||
'''
|
||||
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.blender import tag_redraw_all
|
||||
from ...addon_common.common.maths import Vec, Point, Point2D, Direction, Color
|
||||
from ...config.options import themes
|
||||
|
||||
|
||||
'''
|
||||
RFWidget_Default has no callbacks/actions.
|
||||
This RFWidget is useful for very simple cursor setting.
|
||||
'''
|
||||
|
||||
class RFWidget_Default_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=None, cursor='DEFAULT'):
|
||||
class RFWidget_Default(RFWidget):
|
||||
rfw_name = 'Default'
|
||||
rfw_cursor = cursor
|
||||
|
||||
@RFWidget.on_init
|
||||
def init(self):
|
||||
self.action_name = action_name
|
||||
|
||||
@FSM.on_state('main')
|
||||
def modal_main(self):
|
||||
pass
|
||||
|
||||
return RFWidget_Default
|
||||
@@ -0,0 +1,60 @@
|
||||
'''
|
||||
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.blender import tag_redraw_all
|
||||
from ...addon_common.common.maths import Vec, Point, Point2D, Direction, Color
|
||||
from ...config.options import themes
|
||||
|
||||
|
||||
'''
|
||||
RFWidget_Default has no callbacks/actions.
|
||||
This RFWidget is useful for very simple cursor setting.
|
||||
'''
|
||||
|
||||
class RFWidget_Hidden_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=None, cursor='NONE'):
|
||||
class RFWidget_Hidden(RFWidget):
|
||||
rfw_name = 'Hidden'
|
||||
rfw_cursor = cursor
|
||||
|
||||
@RFWidget.on_init
|
||||
def init(self):
|
||||
self.action_name = action_name
|
||||
|
||||
@FSM.on_state('main')
|
||||
def modal_main(self):
|
||||
pass
|
||||
|
||||
return RFWidget_Hidden
|
||||
@@ -0,0 +1,106 @@
|
||||
'''
|
||||
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 import gpustate
|
||||
from ...addon_common.common.fsm import FSM
|
||||
from ...addon_common.common.globals import Globals
|
||||
from ...addon_common.common.blender import tag_redraw_all
|
||||
from ...addon_common.common.drawing import DrawCallbacks
|
||||
from ...addon_common.common.maths import Vec, Point, Point2D, Direction, Color
|
||||
from ...config.options import themes
|
||||
|
||||
|
||||
'''
|
||||
RFWidget_LineCut handles a line cut in screen space.
|
||||
When cutting, a line segment is drawn from mouse down to current mouse position with a small circle at the very center
|
||||
'''
|
||||
|
||||
class RFWidget_LineCut_Factory:
|
||||
'''
|
||||
This function 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, line_color=None, circle_color=Color((1,1,1,0.5)), circle_border_color=Color((0,0,0,0.5))):
|
||||
class RFWidget_LineCut(RFWidget):
|
||||
rfw_name = 'Line'
|
||||
rfw_cursor = 'CROSSHAIR'
|
||||
|
||||
@RFWidget.on_init
|
||||
def init(self):
|
||||
self.action_name = action_name
|
||||
self.line2D = [None, None]
|
||||
self.line_color = line_color
|
||||
self.circle_color = circle_color
|
||||
self.circle_border_color = circle_border_color
|
||||
|
||||
@FSM.on_state('main')
|
||||
def modal_main(self):
|
||||
if self.actions.pressed('insert'):
|
||||
return 'line'
|
||||
|
||||
@FSM.on_state('line', 'enter')
|
||||
def modal_line_enter(self):
|
||||
self.line2D = [self.actions.mouse, None]
|
||||
tag_redraw_all('Line line_enter')
|
||||
|
||||
@FSM.on_state('line')
|
||||
def modal_line(self):
|
||||
if self.actions.released('insert'):
|
||||
self.callback_actions(self.action_name)
|
||||
return 'main'
|
||||
|
||||
if self.actions.pressed('cancel'):
|
||||
self.line2D = [None, None]
|
||||
self.actions.unuse('insert', ignoremods=True, ignoremulti=True)
|
||||
return 'main'
|
||||
|
||||
if self.line2D[1] != self.actions.mouse:
|
||||
self.line2D[1] = self.actions.mouse
|
||||
tag_redraw_all('Line line')
|
||||
self.callback_actioning(self.action_name)
|
||||
|
||||
@FSM.on_state('line', 'exit')
|
||||
def modal_line_exit(self):
|
||||
tag_redraw_all('Line line_exit')
|
||||
|
||||
@DrawCallbacks.on_draw('post2d')
|
||||
@FSM.onlyinstate('line')
|
||||
def draw_line(self):
|
||||
#cr,cg,cb,ca = self.line_color
|
||||
p0,p1 = self.line2D
|
||||
if not p0 or not p1: return
|
||||
ctr = p0 + (p1-p0)/2
|
||||
|
||||
gpustate.blend('ALPHA')
|
||||
Globals.drawing.draw2D_line(p0, p1, self.line_color or themes['stroke'], width=2, stipple=[2, 2]) # self.line_color)
|
||||
Globals.drawing.draw2D_circle(ctr, 10, self.circle_border_color, width=3) # dark rim
|
||||
Globals.drawing.draw2D_circle(ctr, 10, self.circle_color, width=1) # light center
|
||||
|
||||
return RFWidget_LineCut
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
'''
|
||||
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 import gpustate
|
||||
from ...addon_common.common.fsm import FSM
|
||||
from ...addon_common.common.globals import Globals
|
||||
from ...addon_common.common.blender import tag_redraw_all
|
||||
from ...addon_common.common.drawing import DrawCallbacks
|
||||
from ...addon_common.common.maths import Vec, Point, Point2D, Direction, Color
|
||||
from ...config.options import themes
|
||||
|
||||
|
||||
'''
|
||||
RFWidget_LineCut handles a line cut in screen space.
|
||||
When cutting, a line segment is drawn from mouse down to current mouse position with a small circle at the very center
|
||||
'''
|
||||
|
||||
class RFWidget_SelectBox_Factory:
|
||||
'''
|
||||
This function 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, *, set_color=None, add_color=None, del_color=None):
|
||||
class RFWidget_SelectBox(RFWidget):
|
||||
rfw_name = 'Select Box'
|
||||
rfw_cursor = 'CROSSHAIR'
|
||||
|
||||
@RFWidget.on_init
|
||||
def init(self):
|
||||
self.action_name = action_name
|
||||
self.box2D = [None, None]
|
||||
self.set_color = set_color or themes['set select']
|
||||
self.add_color = add_color or themes['add select']
|
||||
self.del_color = del_color or themes['del select']
|
||||
|
||||
@FSM.on_state('main')
|
||||
def modal_main(self):
|
||||
if self.actions.pressed({'select box'}, ignoremods=True):
|
||||
return 'box'
|
||||
|
||||
def quickselect_start(self):
|
||||
self._fsm.force_set_state('box')
|
||||
|
||||
@FSM.on_state('box', 'enter')
|
||||
def modal_line_enter(self):
|
||||
self.box2D = [self.actions.mouse, None]
|
||||
self.mods = None
|
||||
tag_redraw_all('Line line_enter')
|
||||
|
||||
@FSM.on_state('box')
|
||||
def modal_line(self):
|
||||
if self.actions.released('select box', ignoremods=True):
|
||||
self.callback_actions(self.action_name)
|
||||
return 'main'
|
||||
|
||||
if self.actions.pressed('cancel'):
|
||||
self.box2D = [None, None]
|
||||
self.actions.unuse('select box', ignoremods=True, ignoremulti=True)
|
||||
return 'main'
|
||||
|
||||
new_mods = {
|
||||
'ctrl': self.actions.ctrl,
|
||||
'alt': self.actions.alt,
|
||||
'shift': self.actions.shift,
|
||||
'oskey': self.actions.oskey,
|
||||
}
|
||||
if self.box2D[1] != self.actions.mouse or new_mods != self.mods:
|
||||
self.box2D[1] = self.actions.mouse
|
||||
self.mods = new_mods
|
||||
tag_redraw_all('boxing')
|
||||
self.callback_actioning(self.action_name)
|
||||
|
||||
@FSM.on_state('box', 'exit')
|
||||
def modal_line_exit(self):
|
||||
tag_redraw_all('Line line_exit')
|
||||
|
||||
@DrawCallbacks.on_draw('post2d')
|
||||
@FSM.onlyinstate('box')
|
||||
def draw_line(self):
|
||||
#cr,cg,cb,ca = self.line_color
|
||||
p0,p1 = self.box2D
|
||||
if not p0 or not p1: return
|
||||
|
||||
x0, y0 = p0
|
||||
x1, y1 = p1
|
||||
if self.mods['ctrl']: c = self.del_color
|
||||
elif self.mods['shift']: c = self.add_color
|
||||
else: c = self.set_color
|
||||
|
||||
gpustate.blend('ALPHA')
|
||||
Globals.drawing.draw2D_line((x0, y0), (x1, y0), c, width=1, stipple=[2, 2]) # self.line_color)
|
||||
Globals.drawing.draw2D_line((x1, y0), (x1, y1), c, width=1, stipple=[2, 2]) # self.line_color)
|
||||
Globals.drawing.draw2D_line((x1, y1), (x0, y1), c, width=1, stipple=[2, 2]) # self.line_color)
|
||||
Globals.drawing.draw2D_line((x0, y1), (x0, y0), c, width=1, stipple=[2, 2]) # self.line_color)
|
||||
|
||||
return RFWidget_SelectBox
|
||||
|
||||
Reference in New Issue
Block a user