''' 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 . ''' 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