diff --git a/Makefile b/Makefile index 14b0a39be..8c891fb14 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ NAME = RetopoFlow -VERSION = "v3.2.5" +VERSION = "v3.2.6" # NOTE: one of the following must be uncommented # RELEASE = "alpha" @@ -81,7 +81,7 @@ thumbnails: # create thumbnails cd help && python3 $(CREATE_THUMBNAILS) -build: check +build-github: check mkdir -p $(BUILD_DIR) mkdir -p $(BUILD_DIR)/$(NAME) @@ -89,7 +89,26 @@ build: check # note: rsync flag -a == archive (same as -rlptgoD) rsync -av --progress . $(BUILD_DIR)/$(NAME) --exclude-from="Makefile_excludes" # touch file so that we know it was packaged by us - cd $(BUILD_DIR) && echo "This file indicates that CG Cookie built this version of RetopoFlow." > $(CGCOOKIE_BUILT) + cd $(BUILD_DIR) && echo "This file indicates that CG Cookie built this version of RetopoFlow for release on GitHub." > $(CGCOOKIE_BUILT) + # run debug cleanup + cd $(BUILD_DIR) && python3 $(DEBUG_CLEANUP) "YES!" + # create thumbnails + cd $(BUILD_DIR)/$(NAME)/help && python3 $(CREATE_THUMBNAILS) + # zip it! + cd $(BUILD_DIR) && zip -r $(ZIP_FILE) $(NAME) + + @echo + @echo $(NAME)" "$(VERSION)" is ready" + +build-blendermarket: check + mkdir -p $(BUILD_DIR) + mkdir -p $(BUILD_DIR)/$(NAME) + + # copy files over to build folder + # note: rsync flag -a == archive (same as -rlptgoD) + rsync -av --progress . $(BUILD_DIR)/$(NAME) --exclude-from="Makefile_excludes" + # touch file so that we know it was packaged by us + cd $(BUILD_DIR) && echo "This file indicates that CG Cookie built this version of RetopoFlow for release on Blender Market." > $(CGCOOKIE_BUILT) # run debug cleanup cd $(BUILD_DIR) && python3 $(DEBUG_CLEANUP) "YES!" # create thumbnails diff --git a/README.md b/README.md index 06d259802..480fda2a8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ All mesh generation is quad-based (unless you explicitly create other ngons) and You may purchase RetopoFlow on the [Blender Market](https://blendermarket.com/products/retopoflow/) -Purchasing a license entitles you to tool support and helps ensure RetopoFlows continued development. +Purchasing a copy entitles you to tool support and helps ensure RetopoFlows continued development. ## Getting Support diff --git a/__init__.py b/__init__.py index 5e4aba03f..ac557cde4 100644 --- a/__init__.py +++ b/__init__.py @@ -39,7 +39,7 @@ "author": "Jonathan Denning, Jonathan Lampel, Jonathan Williamson, Patrick Moore, Patrick Crawford, Christopher Gearhart", "location": "View 3D > Header", "blender": (2, 93, 0), - "version": (3, 2, 5), + "version": (3, 2, 6), # "warning": "Alpha", # used for warning icon and text in addons panel # "warning": "Beta", # "warning": "Release Candidate 1", @@ -390,9 +390,15 @@ def in_quadview(context): return False + rf_label_extra = " (?)" + if configoptions.retopoflow_version_git: rf_label_extra = " (git)" + elif not configoptions.retopoflow_cgcookie_built: rf_label_extra = " (self)" + elif configoptions.retopoflow_github: rf_label_extra = " (github)" + elif configoptions.retopoflow_blendermarket: rf_label_extra = "" + class VIEW3D_PT_RetopoFlow(Panel): """RetopoFlow Blender Menu""" - bl_label = f'RetopoFlow {retopoflow_version}{" (git)" if configoptions.retopoflow_version_git else " (self)" if not configoptions.retopoflow_cgcookie_built else ""}' + bl_label = f'RetopoFlow {retopoflow_version}{rf_label_extra}' bl_space_type = 'VIEW_3D' bl_region_type = 'HEADER' # bl_ui_units_x = 100 diff --git a/addon_common/common/bmesh_render.py b/addon_common/common/bmesh_render.py index 5add2edce..51470ef70 100644 --- a/addon_common/common/bmesh_render.py +++ b/addon_common/common/bmesh_render.py @@ -120,12 +120,14 @@ def triangulateFace(verts): from gpu_extras.batch import batch_for_shader from .shaders import Shader +Drawing.glCheckError(f'Pre-compile check: bmesh render shader') verts_vs, verts_fs = Shader.parse_file('bmesh_render_verts.glsl', includeVersion=False) verts_shader = gpu.types.GPUShader(verts_vs, verts_fs) edges_vs, edges_fs = Shader.parse_file('bmesh_render_edges.glsl', includeVersion=False) edges_shader = gpu.types.GPUShader(edges_vs, edges_fs) faces_vs, faces_fs = Shader.parse_file('bmesh_render_faces.glsl', includeVersion=False) faces_shader = gpu.types.GPUShader(faces_vs, faces_fs) +Drawing.glCheckError(f'Compiled bmesh render shader') class BufferedRender_Batch: @@ -147,23 +149,36 @@ def __init__(self, drawtype): self.batch = None self._quarantine.setdefault(self.shader, set()) - def buffer(self, pos, norm, sel, warn): + def buffer(self, pos, norm, sel, warn, pin, seam): if self.shader == None: return if self.shader_type == 'POINTS': data = { + # repeat each value 6 times 'vert_pos': [p for p in pos for __ in range(6)], 'vert_norm': [n for n in norm for __ in range(6)], 'selected': [s for s in sel for __ in range(6)], 'warning': [w for w in warn for __ in range(6)], + 'pinned': [p for p in pin for __ in range(6)], + 'seam': [p for p in seam for __ in range(6)], 'vert_offset': [o for _ in pos for o in [(0,0), (1,0), (0,1), (0,1), (1,0), (1,1)]], } elif self.shader_type == 'LINES': data = { - 'vert_pos0': [p0 for (p0,p1) in zip(pos[0::2], pos[1::2] ) for __ in range(6)], - 'vert_pos1': [p1 for (p0,p1) in zip(pos[0::2], pos[1::2] ) for __ in range(6)], - 'vert_norm': [n0 for (n0,n1) in zip(norm[0::2],norm[1::2]) for __ in range(6)], - 'selected': [s0 for (s0,s1) in zip(sel[0::2], sel[1::2] ) for __ in range(6)], - 'warning': [s0 for (s0,s1) in zip(warn[0::2], warn[1::2] ) for __ in range(6)], + # repeat each value 6 times + # 'vert_pos0': [p0 for (p0,p1) in zip( pos[0::2], pos[1::2]) for __ in range(6)], + # 'vert_pos1': [p1 for (p0,p1) in zip( pos[0::2], pos[1::2]) for __ in range(6)], + # 'vert_norm': [n0 for (n0,n1) in zip(norm[0::2], norm[1::2]) for __ in range(6)], + # 'selected': [s0 for (s0,s1) in zip( sel[0::2], sel[1::2]) for __ in range(6)], + # 'warning': [s0 for (s0,s1) in zip(warn[0::2], warn[1::2]) for __ in range(6)], + # 'pinned': [s0 for (s0,s1) in zip( pin[0::2], pin[1::2]) for __ in range(6)], + # 'seam': [s0 for (s0,s1) in zip(seam[0::2], seam[1::2]) for __ in range(6)], + 'vert_pos0': [p0 for p0 in pos[ 0::2] for __ in range(6)], + 'vert_pos1': [p1 for p1 in pos[ 1::2] for __ in range(6)], + 'vert_norm': [n for n in norm[0::2] for __ in range(6)], + 'selected': [s for s in sel[ 0::2] for __ in range(6)], + 'warning': [w for w in warn[0::2] for __ in range(6)], + 'pinned': [p for p in pin[ 0::2] for __ in range(6)], + 'seam': [s for s in seam[0::2] for __ in range(6)], 'vert_offset': [o for _ in pos[0::2] for o in [(0,0), (0,1), (1,1), (0,0), (1,1), (1,0)]], } elif self.shader_type == 'TRIS': @@ -171,19 +186,21 @@ def buffer(self, pos, norm, sel, warn): 'vert_pos': pos, 'vert_norm': norm, 'selected': sel, + 'pinned': pin, + # 'seam': seam, } else: assert False, 'BufferedRender_Batch.buffer: Unhandled type: ' + self.shader_type - self.batch = batch_for_shader(self.shader, 'TRIS', data) # self.shader_type, data) + self.batch = batch_for_shader(self.shader, 'TRIS', data) self.count = len(pos) def set_options(self, prefix, opts): if not opts: return shader = self.shader - prefix = '%s ' % prefix if prefix else '' + prefix = f'{prefix} ' if prefix else '' def set_if_set(opt, cb): - opt = '%s%s' % (prefix, opt) + opt = f'{prefix}{opt}' if opt not in opts: return cb(opts[opt]) Drawing.glCheckError('setting %s to %s' % (str(opt), str(opts[opt]))) @@ -193,6 +210,8 @@ def set_if_set(opt, cb): set_if_set('color', lambda v: self.uniform_float('color', v)) set_if_set('color selected', lambda v: self.uniform_float('color_selected', v)) set_if_set('color warning', lambda v: self.uniform_float('color_warning', v)) + set_if_set('color pinned', lambda v: self.uniform_float('color_pinned', v)) + set_if_set('color seam', lambda v: self.uniform_float('color_seam', v)) set_if_set('hidden', lambda v: self.uniform_float('hidden', v)) set_if_set('offset', lambda v: self.uniform_float('offset', v)) set_if_set('dotoffset', lambda v: self.uniform_float('dotoffset', v)) @@ -237,16 +256,23 @@ def draw(self, opts): self.uniform_float('color', (1,1,1,0.5)) self.uniform_float('color_selected', (0.5,1,0.5,0.5)) self.uniform_float('color_warning', (1.0,0.5,0.0,0.5)) + self.uniform_float('color_pinned', (1.0,0.0,0.5,0.5)) + self.uniform_float('color_seam', (1.0,0.0,0.5,0.5)) self.uniform_float('hidden', 0.9) self.uniform_float('offset', 0) self.uniform_float('dotoffset', 0) self.uniform_float('vert_scale', (1, 1, 1)) self.uniform_float('radius', 1) #random.random()*10) - nosel = opts.get('no selection', False) - nowarn = opts.get('no warning', False) + nosel = opts.get('no selection', False) + nowarn = opts.get('no warning', False) + nopin = opts.get('no pinned', False) + noseam = opts.get('no seam', False) + self.uniform_bool('use_selection', [not nosel]) # must be a sequence!? self.uniform_bool('use_warning', [not nowarn]) # must be a sequence!? + self.uniform_bool('use_pinned', [not nopin]) # must be a sequence!? + self.uniform_bool('use_seam', [not noseam]) # must be a sequence!? self.uniform_bool('use_rounding', [self.drawtype == self.POINTS]) # must be a sequence!? self.uniform_float('matrix_m', opts['matrix model']) @@ -298,7 +324,7 @@ def draw(self, opts): self._draw(1, 1, 1) if opts['draw mirrored'] and (mx or my or mz): - self.set_options('%s mirror' % self.options_prefix, opts) + self.set_options(f'{self.options_prefix} mirror', opts) if mx: self._draw(-1, 1, 1) if my: self._draw( 1, -1, 1) if mz: self._draw( 1, 1, -1) diff --git a/addon_common/common/drawing.py b/addon_common/common/drawing.py index 2442f6272..ad24b8ea5 100644 --- a/addon_common/common/drawing.py +++ b/addon_common/common/drawing.py @@ -55,6 +55,11 @@ from .functools import find_fns +# the following line suppresses a Blender 3.1.0 bug +# https://developer.blender.org/T95592 +bgl.glGetError() + + class Cursors: # https://docs.blender.org/api/current/bpy.types.Window.html#bpy.types.Window.cursor_set _cursors = { @@ -117,70 +122,6 @@ def warp(x, y): bpy.context.window.cursor_warp(x, y) - -if bversion() >= "2.80": - import gpu - from gpu.types import GPUShader - from gpu_extras.batch import batch_for_shader - - # https://docs.blender.org/api/blender2.8/gpu.html#triangle-with-custom-shader - - def create_shader(fn_glsl): - path_here = os.path.dirname(os.path.realpath(__file__)) - path_shaders = os.path.join(path_here, 'shaders') - path_glsl = os.path.join(path_shaders, fn_glsl) - txt = open(path_glsl, 'rt').read() - vert_source, frag_source = Shader.parse_string(txt) - try: - return GPUShader(vert_source, frag_source) - except Exception as e: - print('ERROR WHILE COMPILING SHADER %s' % fn_glsl) - assert False - - # 2D point - shader_2D_point = create_shader('point_2D.glsl') - batch_2D_point = batch_for_shader(shader_2D_point, 'TRIS', {"pos": [(0,0), (1,0), (1,1), (0,0), (1,1), (0,1)]}) - - # 2D line segment - shader_2D_lineseg = create_shader('lineseg_2D.glsl') - batch_2D_lineseg = batch_for_shader(shader_2D_lineseg, 'TRIS', {"pos": [(0,0), (1,0), (1,1), (0,0), (1,1), (0,1)]}) - - # 2D circle - shader_2D_circle = create_shader('circle_2D.glsl') - # create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1) - cnt = 100 - pts = [ - p for i0 in range(cnt) - for p in [ - ((i0+0)/cnt,0), ((i0+1)/cnt,0), ((i0+1)/cnt,1), - ((i0+0)/cnt,0), ((i0+1)/cnt,1), ((i0+0)/cnt,1), - ] - ] - batch_2D_circle = batch_for_shader(shader_2D_circle, 'TRIS', {"pos": pts}) - - # 3D circle - shader_3D_circle = create_shader('circle_3D.glsl') - # create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1) - cnt = 100 - pts = [ - p for i0 in range(cnt) - for p in [ - ((i0+0)/cnt,0), ((i0+1)/cnt,0), ((i0+1)/cnt,1), - ((i0+0)/cnt,0), ((i0+1)/cnt,1), ((i0+0)/cnt,1), - ] - ] - batch_3D_circle = batch_for_shader(shader_3D_circle, 'TRIS', {"pos": pts}) - - # 3D triangle - shader_3D_triangle = create_shader('triangle_3D.glsl') - batch_3D_triangle = batch_for_shader(shader_3D_triangle, 'TRIS', {'pos': [(1,0), (0,1), (0,0)]}) - - # 3D triangle - shader_2D_triangle = create_shader('triangle_2D.glsl') - batch_2D_triangle = batch_for_shader(shader_2D_triangle, 'TRIS', {'pos': [(1,0), (0,1), (0,0)]}) - - - class Drawing: _instance = None _dpi_mult = 1 @@ -569,7 +510,15 @@ def glCheckError(title): traceback.print_stack() return True + def get_view_origin(self, *, orthographic_distance=1000): + focus = self.r3d.view_location + rot = self.r3d.view_rotation + dist = self.r3d.view_distance if self.r3d.is_perspective else orthographic_distance + return focus + (rot @ Vector((0, 0, dist))) + # # the following fails in weird ways when in orthographic projection + # center = Point2D((self.area.width / 2, self.area.height / 2)) + # return Point(region_2d_to_origin_3d(self.rgn, self.r3d, center)) def Point2D_to_Ray(self, p2d): o = Point(region_2d_to_origin_3d(self.rgn, self.r3d, p2d)) @@ -824,8 +773,80 @@ def draw(self, draw_type:"CC_DRAW"): self.glCheckError('done with draw') self._draw = None - +Drawing.glCheckError(f'pre-init check: Drawing') Drawing.initialize() +Drawing.glCheckError(f'post-init check: Drawing') + + + + +if bversion() >= "2.80": + import gpu + from gpu.types import GPUShader + from gpu_extras.batch import batch_for_shader + + # https://docs.blender.org/api/blender2.8/gpu.html#triangle-with-custom-shader + + def create_shader(fn_glsl): + path_here = os.path.dirname(os.path.realpath(__file__)) + path_shaders = os.path.join(path_here, 'shaders') + path_glsl = os.path.join(path_shaders, fn_glsl) + txt = open(path_glsl, 'rt').read() + vert_source, frag_source = Shader.parse_string(txt) + try: + Drawing.glCheckError(f'pre-compile check: {fn_glsl}') + ret = GPUShader(vert_source, frag_source) + Drawing.glCheckError(f'post-compile check: {fn_glsl}') + return ret + except Exception as e: + print('ERROR WHILE COMPILING SHADER %s' % fn_glsl) + assert False + + Drawing.glCheckError(f'Pre-compile check: point, lineseg, circle, triangle shaders') + + # 2D point + shader_2D_point = create_shader('point_2D.glsl') + batch_2D_point = batch_for_shader(shader_2D_point, 'TRIS', {"pos": [(0,0), (1,0), (1,1), (0,0), (1,1), (0,1)]}) + + # 2D line segment + shader_2D_lineseg = create_shader('lineseg_2D.glsl') + batch_2D_lineseg = batch_for_shader(shader_2D_lineseg, 'TRIS', {"pos": [(0,0), (1,0), (1,1), (0,0), (1,1), (0,1)]}) + + # 2D circle + shader_2D_circle = create_shader('circle_2D.glsl') + # create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1) + cnt = 100 + pts = [ + p for i0 in range(cnt) + for p in [ + ((i0+0)/cnt,0), ((i0+1)/cnt,0), ((i0+1)/cnt,1), + ((i0+0)/cnt,0), ((i0+1)/cnt,1), ((i0+0)/cnt,1), + ] + ] + batch_2D_circle = batch_for_shader(shader_2D_circle, 'TRIS', {"pos": pts}) + + # 3D circle + shader_3D_circle = create_shader('circle_3D.glsl') + # create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1) + cnt = 100 + pts = [ + p for i0 in range(cnt) + for p in [ + ((i0+0)/cnt,0), ((i0+1)/cnt,0), ((i0+1)/cnt,1), + ((i0+0)/cnt,0), ((i0+1)/cnt,1), ((i0+0)/cnt,1), + ] + ] + batch_3D_circle = batch_for_shader(shader_3D_circle, 'TRIS', {"pos": pts}) + + # 3D triangle + shader_3D_triangle = create_shader('triangle_3D.glsl') + batch_3D_triangle = batch_for_shader(shader_3D_triangle, 'TRIS', {'pos': [(1,0), (0,1), (0,0)]}) + + # 3D triangle + shader_2D_triangle = create_shader('triangle_2D.glsl') + batch_2D_triangle = batch_for_shader(shader_2D_triangle, 'TRIS', {'pos': [(1,0), (0,1), (0,0)]}) + + Drawing.glCheckError(f'Compiled point, lineseg, circle shaders') ###################################################################################################### diff --git a/addon_common/common/human_readable.py b/addon_common/common/human_readable.py index ad501a43e..84a75f0bc 100644 --- a/addon_common/common/human_readable.py +++ b/addon_common/common/human_readable.py @@ -182,9 +182,9 @@ def convert_human_readable_to_actions(actions): if type(actions) is str: actions = [actions] for action in actions: if platform.system() == 'Darwin': - action = action.replace('^ Ctrl+', 'CTRL+') + action = action.replace('^ Ctrl+', 'CTRL+') action = action.replace('⇧ Shift+', 'SHIFT+') - action = action.replace('⌥ Opt+', 'ALT+') + action = action.replace('⌥ Opt+', 'ALT+') action = action.replace('⌘ Cmd+', 'OSKEY+') else: action = action.replace('Ctrl+', 'CTRL+') @@ -194,4 +194,4 @@ def convert_human_readable_to_actions(actions): for hr2kmi in humanreadable_to_kmi: kmi = hr2kmi.get(action, action) ret.append(kmi) - return ret \ No newline at end of file + return ret diff --git a/addon_common/common/maths.py b/addon_common/common/maths.py index 2ef0dd52a..111527ae1 100644 --- a/addon_common/common/maths.py +++ b/addon_common/common/maths.py @@ -503,6 +503,10 @@ def average(normals): class Color(Vector): + @staticmethod + def from_ints(r, g, b, a=255): + return Color((r/255.0, g/255.0, b/255.0, a/255.0)) + @staticmethod def as_vec4(c): if type(c) in {float, int}: return Vector((c, c, c, 1.0)) @@ -627,7 +631,8 @@ def __repr__(self): return self.__str__() def eval(self, t: float): - return self.o + max(0.0, min(self.max, t)) * self.d + v = self.d * clamp(t, 0.0, self.max) + return self.o + v @classmethod def from_screenspace(cls, pos: Vector): @@ -1205,6 +1210,21 @@ def Point_within(self, point: Point, margin=0): for (v, m, M) in zip(point, self.min, self.max) ) + def closest_Point(self, point:Point): + return Point(( + clamp(point.x, self.mx, self.Mx), + clamp(point.y, self.my, self.My), + clamp(point.z, self.mz, self.Mz), + )) + + def farthest_Point(self, point:Point): + cx, cy, cz = (self.mx + self.Mx) / 2, (self.my + self.My) / 2, (self.mz + self.Mz) / 2 + return Point(( + self.mx if point.x > cx else self.Mx, + self.my if point.y > cy else self.My, + self.mz if point.z > cz else self.Mz, + )) + def get_min_dimension(self): return self.min_dim diff --git a/addon_common/common/shaders.py b/addon_common/common/shaders.py index 9d5d7b6f7..ab4433068 100644 --- a/addon_common/common/shaders.py +++ b/addon_common/common/shaders.py @@ -57,7 +57,9 @@ def shader_compile(name, shader, src): logging and error-checking not quite working :( ''' + Globals.drawing.glCheckError(f'Pre shader compile') bgl.glCompileShader(shader) + Globals.drawing.glCheckError(f'Post shader compile {name}') # report shader compilation log (if any) bufLogLen = bgl.Buffer(bgl.GL_INT, 1) diff --git a/addon_common/common/shaders/bmesh_render_edges.glsl b/addon_common/common/shaders/bmesh_render_edges.glsl index 446af959b..fe4b14582 100644 --- a/addon_common/common/shaders/bmesh_render_edges.glsl +++ b/addon_common/common/shaders/bmesh_render_edges.glsl @@ -1,9 +1,13 @@ uniform vec4 color; // color of geometry if not selected uniform vec4 color_selected; // color of geometry if selected uniform vec4 color_warning; // color of geometry if warning +uniform vec4 color_pinned; // color of geometry if pinned +uniform vec4 color_seam; // color of geometry if seam uniform bool use_selection; // false: ignore selected, true: consider selected uniform bool use_warning; // false: ignore warning, true: consider warning +uniform bool use_pinned; // false: ignore pinned, true: consider pinned +uniform bool use_seam; // false: ignore seam, true: consider seam uniform bool use_rounding; uniform mat4 matrix_m; // model xform matrix @@ -50,8 +54,10 @@ attribute vec3 vert_pos0; // position wrt model attribute vec3 vert_pos1; // position wrt model attribute vec2 vert_offset; attribute vec3 vert_norm; // normal wrt model -attribute float selected; // is vertex selected? 0=no; 1=yes -attribute float warning; // is vertex warning? 0=no; 1=yes +attribute float selected; // is edge selected? 0=no; 1=yes +attribute float warning; // is edge warning? 0=no; 1=yes +attribute float pinned; // is edge pinned? 0=no; 1=yes +attribute float seam; // is edge on seam? 0=no; 1=yes varying vec4 vPPosition; // final position (projected) @@ -142,13 +148,13 @@ void main() { gl_Position = vPPosition; - if(use_selection && selected > 0.5) { - vColor = color_selected; - } else if(use_warning && warning > 0.5) { - vColor = color_warning; - } else { - vColor = color; - } + vColor = color; + + if(use_warning && warning > 0.5) vColor = mix(vColor, color_warning, 0.75); + if(use_pinned && pinned > 0.5) vColor = mix(vColor, color_pinned, 0.75); + if(use_seam && seam > 0.5) vColor = mix(vColor, color_seam, 0.75); + if(use_selection && selected > 0.5) vColor = mix(vColor, color_selected, 0.75); + vColor.a *= 1.0 - hidden; if(debug_invert_backfacing && vCNormal.z < 0.0) { @@ -236,7 +242,7 @@ void main() { discard; return; } - alpha *= alpha_mult; + alpha *= min(1.0, alpha_mult); if(perspective) { // perspective projection @@ -250,7 +256,7 @@ void main() { discard; return; } else { - alpha *= alpha_backface; + alpha *= min(1.0, alpha_backface); } } @@ -276,7 +282,7 @@ void main() { discard; return; } else { - alpha *= alpha_backface; + alpha *= min(1.0, alpha_backface); } } @@ -288,7 +294,7 @@ void main() { ; } - alpha *= pow(max(vCNormal.z, 0.01), 0.25); + alpha *= min(1.0, pow(max(vCNormal.z, 0.01), 0.25)); outColor = coloring(vec4(rgb, alpha)); // https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API outColor = blender_srgb_to_framebuffer_space(outColor); diff --git a/addon_common/common/shaders/bmesh_render_faces.glsl b/addon_common/common/shaders/bmesh_render_faces.glsl index b27dba055..1d7d82750 100644 --- a/addon_common/common/shaders/bmesh_render_faces.glsl +++ b/addon_common/common/shaders/bmesh_render_faces.glsl @@ -1,7 +1,11 @@ uniform vec4 color; // color of geometry if not selected uniform vec4 color_selected; // color of geometry if selected +uniform vec4 color_pinned; // color of geometry if pinned +uniform vec4 color_seam; // color of geometry if seam uniform bool use_selection; // false: ignore selected, true: consider selected +uniform bool use_pinned; // false: ignore pinned, true: consider pinned +uniform bool use_seam; // false: ignore seam, true: consider seam uniform bool use_rounding; uniform mat4 matrix_m; // model xform matrix @@ -46,7 +50,9 @@ uniform float radius; attribute vec3 vert_pos; // position wrt model attribute vec3 vert_norm; // normal wrt model -attribute float selected; // is vertex selected? 0=no; 1=yes +attribute float selected; // is face selected? 0=no; 1=yes +attribute float pinned; // is face pinned? 0=no; 1=yes +attribute float seam; // is face on seam? 0=no; 1=yes varying vec4 vPPosition; // final position (projected) @@ -120,8 +126,18 @@ void main() { gl_Position = vPPosition; - vColor = (!use_selection || selected < 0.5) ? color : color_selected; - vColor.a *= (selected > 0.5) ? 1.0 : 1.0 - hidden; + vColor = color; + + // if(use_warning && warning > 0.5) vColor = mix(vColor, color_warning, 0.75); + if(use_pinned && pinned > 0.5) vColor = mix(vColor, color_pinned, 0.75); + // if(use_seam && seam > 0.5) vColor = mix(vColor, color_seam, 0.75); + if(use_selection && selected > 0.5) vColor = mix(vColor, color_selected, 0.75); + + vColor.a *= 1.0 - hidden; + // vColor.a *= 1.0 - hidden; + + // vColor = (!use_selection || selected < 0.5) ? color : color_selected; + // vColor.a *= (selected > 0.5) ? 1.0 : 1.0 - hidden; if(debug_invert_backfacing && vCNormal.z < 0.0) { vColor = vec4(vec3(1,1,1) - vColor.rgb, vColor.a); @@ -218,7 +234,7 @@ void main() { discard; return; } else { - alpha *= alpha_backface; + alpha *= min(1.0, alpha_backface); } } @@ -244,7 +260,7 @@ void main() { discard; return; } else { - alpha *= alpha_backface; + alpha *= min(1.0, alpha_backface); } } @@ -256,7 +272,7 @@ void main() { ; } - alpha *= pow(max(vCNormal.z, 0.01), 0.25); + alpha *= min(1.0, pow(max(vCNormal.z, 0.01), 0.25)); outColor = coloring(vec4(rgb, alpha)); // https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API outColor = blender_srgb_to_framebuffer_space(outColor); diff --git a/addon_common/common/shaders/bmesh_render_verts.glsl b/addon_common/common/shaders/bmesh_render_verts.glsl index 93ab2a9a2..aaa448c79 100644 --- a/addon_common/common/shaders/bmesh_render_verts.glsl +++ b/addon_common/common/shaders/bmesh_render_verts.glsl @@ -1,9 +1,13 @@ uniform vec4 color; // color of geometry if not selected uniform vec4 color_selected; // color of geometry if selected uniform vec4 color_warning; // color of geometry if warning +uniform vec4 color_pinned; // color of geometry if pinned +uniform vec4 color_seam; // color of geometry if seam uniform bool use_selection; // false: ignore selected, true: consider selected uniform bool use_warning; // false: ignore warning, true: consider warning +uniform bool use_pinned; // false: ignore pinned, true: consider pinned +uniform bool use_seam; // false: ignore seam, true: consider seam uniform bool use_rounding; uniform mat4 matrix_m; // model xform matrix @@ -51,6 +55,8 @@ attribute vec2 vert_offset; attribute vec3 vert_norm; // normal wrt model attribute float selected; // is vertex selected? 0=no; 1=yes attribute float warning; // is vertex warning? 0=no; 1=yes +attribute float pinned; // is vertex pinned? 0=no; 1=yes +attribute float seam; // is vertex along seam? 0=no; 1=yes varying vec4 vPPosition; // final position (projected) @@ -120,13 +126,13 @@ void main() { gl_Position = vPPosition; - if(use_selection && selected > 0.5) { - vColor = color_selected; - } else if(use_warning && warning > 0.5) { - vColor = color_warning; - } else { - vColor = color; - } + vColor = color; + + if(use_warning && warning > 0.5) vColor = mix(vColor, color_warning, 0.75); + if(use_pinned && pinned > 0.5) vColor = mix(vColor, color_pinned, 0.75); + if(use_seam && seam > 0.5) vColor = mix(vColor, color_seam, 0.75); + if(use_selection && selected > 0.5) vColor = mix(vColor, color_selected, 0.75); + vColor.a *= 1.0 - hidden; if(debug_invert_backfacing && vCNormal.z < 0.0) { @@ -215,7 +221,7 @@ void main() { discard; return; } - alpha *= alpha_mult; + alpha *= min(1.0, alpha_mult); } if(perspective) { @@ -230,7 +236,7 @@ void main() { discard; return; } else { - alpha *= alpha_backface; + alpha *= min(1.0, alpha_backface); } } @@ -256,7 +262,7 @@ void main() { discard; return; } else { - alpha *= alpha_backface; + alpha *= min(1.0, alpha_backface); } } @@ -268,7 +274,7 @@ void main() { ; } - alpha *= pow(max(vCNormal.z, 0.01), 0.25); + alpha *= min(1.0, pow(max(vCNormal.z, 0.01), 0.25)); outColor = coloring(vec4(rgb, alpha)); // https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API outColor = blender_srgb_to_framebuffer_space(outColor); diff --git a/addon_common/common/ui_draw.py b/addon_common/common/ui_draw.py index f6c29d1ca..c41e417c1 100644 --- a/addon_common/common/ui_draw.py +++ b/addon_common/common/ui_draw.py @@ -97,8 +97,9 @@ def init_draw(self): vertex_positions = [(0,0),(1,0),(1,1), (1,1),(0,1),(0,0)] vertex_shader, fragment_shader = Shader.parse_file('ui_element.glsl', includeVersion=False) print(f'Addon Common: compiling UI shader') + Drawing.glCheckError(f'Pre-compile check: UI Shader check') shader = gpu.types.GPUShader(vertex_shader, fragment_shader) #name='RetopoFlowUIShader' - Drawing.glCheckError(f'Compiled shader {shader}') + Drawing.glCheckError(f'Compiled UI Shader {shader}') print(f'Addon Common: batching for shader') batch = batch_for_shader(shader, 'TRIS', {"pos": vertex_positions}) Drawing.glCheckError(f'Batched for shader {batch}') @@ -177,6 +178,7 @@ def get_v(style_key, def_val): if texture_id is not None: bgl.glActiveTexture(atex) bgl.glBindTexture(bgl.GL_TEXTURE_2D, texture_id) + # Drawing.glCheckError(f'checking gl errors after binding shader and setting uniforms') batch.draw(shader) UI_Draw._draw = draw diff --git a/addon_common/common/useractions.py b/addon_common/common/useractions.py index 38d6a0689..c0f9ea503 100644 --- a/addon_common/common/useractions.py +++ b/addon_common/common/useractions.py @@ -142,7 +142,7 @@ def translate_blenderop(action, keyconfig=None): if kmi.idname != oop and kmi.idname != top: continue ret.add(kmi_details(kmi)) if not ret: - print(f'Addon Common Warning: could not translate blender op "{action}" to actions ({okeymap}->{tkeymap}, {oop}->{top})') + dprint(f'Addon Common Warning: could not translate blender op "{action}" to actions ({okeymap}->{tkeymap}, {oop}->{top})') return ret @@ -159,6 +159,17 @@ def strip_mods(action, ctrl=True, shift=True, alt=True, oskey=True, click=True, if drag_click: action = action.replace('+DRAG', '') return action +def add_mods(action, *, ctrl=False, shift=False, alt=False, oskey=False, click=False, double_click=False, drag_click=False): + if not action: return action + ctrl = 'CTRL+' if ctrl else '' + shift = 'SHIFT+' if shift else '' + alt = 'ALT+' if alt else '' + oskey = 'OSKEY+' if oskey else '' + click = '+CLICK' if click and not double_click and not drag_click else '' + double_click = '+DOUBLE' if double_click and not drag_click else '' + drag_click = '+DRAG' if drag_click else '' + return f'{ctrl}{shift}{alt}{oskey}{action}{click}{double_click}{drag_click}' + def i18n_translate(text): ''' bpy.app.translations.pgettext tries to translate the given parameter ''' return bpy.app.translations.pgettext(text) @@ -245,7 +256,6 @@ class Actions: { 'name': 'navigate', 'operators': [ - '3D View | view3d.rotate', '3D View | view3d.rotate', # Rotate View '3D View | view3d.move', # Move View '3D View | view3d.zoom', # Zoom View @@ -262,6 +272,7 @@ class Actions: '3D View | view3d.ndof_all', # NDOF Move View '3D View | view3d.view_selected', # View Selected '3D View | view3d.view_center_cursor', # Center View to Cursor + '3D View | view3d.view_center_pick', # Center View to Mouse # '3D View | view3d.navigate', # View Navigation ], }, { @@ -324,6 +335,7 @@ def __init__(self, context): self.keymap.setdefault(name, set()) for op in ops: self.keymap[name] |= translate_blenderop(op) + # print(f'useractions navigate={self.keymap["navigate"]}') self.context = context self.area = context.area @@ -416,9 +428,15 @@ def update(self, context, event, fn_debug=None): self.mousemove = (event_type in Actions.mousemove_actions) self.trackpad = (event_type in Actions.trackpad_actions) self.ndof = (event_type in Actions.ndof_actions) - self.navevent = (event_type in self.keymap['navigate']) + self.navevent = False # to be set below... was = (event_type in self.keymap['navigate']) self.mousemove_stop = not self.mousemove and self.mousemove_prev + # record held modifiers + self.ctrl = event.ctrl + self.alt = event.alt + self.shift = event.shift + self.oskey = event.oskey + if event_type in self.ignore_actions: return if fn_debug and event_type not in self.nonprintable_actions: @@ -429,6 +447,7 @@ def update(self, context, event, fn_debug=None): self.time_delta = self.time_last - time_cur self.time_last = time_cur self.trackpad = False + self.navevent = False return if self.mousemove: @@ -449,6 +468,10 @@ def update(self, context, event, fn_debug=None): self.mousedown_drag = False return + if event_type in {'LEFTMOUSE', 'MIDDLEMOUSE', 'RIGHTMOUSE'} and not pressed: + # release drag when mouse button is released + self.mousedown_drag = False + if self.trackpad: pressed = True self.scroll = (event.mouse_x - event.mouse_prev_x, event.mouse_y - event.mouse_prev_y) @@ -459,24 +482,12 @@ def update(self, context, event, fn_debug=None): self.scroll = (0, 0) if event_type in self.modifier_actions: - if event_type == 'OSKEY': - self.oskey = pressed - else: - l = event_type.startswith('LEFT_') - if event_type.endswith('_CTRL'): - self.ctrl = pressed - if l: self.ctrl_left = pressed - else: self.ctrl_right = pressed - if event_type.endswith('_SHIFT'): - self.shift = pressed - if l: self.shift_left = pressed - else: self.shift_right = pressed - if event_type.endswith('_ALT'): - self.alt = pressed - if l: self.alt_left = pressed - else: self.alt_right = pressed return # modifier keys do not "fire" pressed events + full_event_type = add_mods(event_type, ctrl=self.ctrl, alt=self.alt, shift=self.shift, oskey=self.oskey, drag_click=self.mousedown_drag) + self.navevent = (full_event_type in self.keymap['navigate']) + # if self.navevent: print(f'useractions.update navevent from {full_event_type}') + mouse_event = event_type in self.mousebutton_actions and not self.navevent if mouse_event: if pressed: @@ -601,11 +612,12 @@ def action_good(action): return any(action_good(action) for action in self.convert(actions)) def navigating(self): - actions = self.convert('navigate') - if self.trackpad: return True - if self.ndof: return True - if any(p in actions for p in self.now_pressed.values()): return True - return False + return self.navevent + # actions = self.convert('navigate') + # if self.trackpad: return True + # if self.ndof: return True + # if any(p in actions for p in self.now_pressed.values()): return True + # return False def pressed(self, actions, unpress=True, ignoremods=False, ignorectrl=False, ignoreshift=False, ignorealt=False, ignoreoskey=False, ignoremulti=False, ignoreclick=False, ignoredouble=False, ignoredrag=False, ignoremouse=False, debug=False): if actions is None: return False diff --git a/addon_common/cookiecutter/cookiecutter.py b/addon_common/cookiecutter/cookiecutter.py index a0a53566d..f455f4274 100644 --- a/addon_common/cookiecutter/cookiecutter.py +++ b/addon_common/cookiecutter/cookiecutter.py @@ -155,14 +155,16 @@ def modal(self, context, event): ret = {'RUNNING_MODAL'} else: # allow window actions to pass through to Blender - if self._cc_actions.using('blender window action'): ret = {'PASS_THROUGH'} + if self._cc_actions.using('blender window action'): + ret = {'PASS_THROUGH'} # allow navigation actions to pass through to Blender if self._cc_actions.navigating() or (self._cc_actions.timer and self._nav): # let Blender handle navigation self._cc_actions.unuse('navigate') # pass-through commands do not receive a release event self._nav = True - if not self._cc_actions.trackpad: self.drawing.set_cursor('HAND') + if not self._cc_actions.trackpad: + self.drawing.set_cursor('HAND') ret = {'PASS_THROUGH'} elif self._nav: self._nav = False diff --git a/addon_common/cookiecutter/cookiecutter_ui.py b/addon_common/cookiecutter/cookiecutter_ui.py index 5c382bc12..e7973cd18 100644 --- a/addon_common/cookiecutter/cookiecutter_ui.py +++ b/addon_common/cookiecutter/cookiecutter_ui.py @@ -57,7 +57,9 @@ } } ''' + Drawing.glCheckError(f'Pre-compile check: cover shader') shader = gpu.types.GPUShader(cover_vshader, cover_fshader) + Drawing.glCheckError(f'Post-compile check: cover shader') # create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1) batch_full = batch_for_shader(shader, 'TRIS', {"position": [(-100, -100), (300, -100), (-100, 300)]}) diff --git a/config/keymaps.py b/config/keymaps.py index 54ec8f293..ea3b3039c 100644 --- a/config/keymaps.py +++ b/config/keymaps.py @@ -21,6 +21,7 @@ import os +import re import copy import json @@ -42,6 +43,9 @@ default_rf_keymaps = { + # always pass these actions on to Blender (set in keymap editor only) + 'blender passthrough': [], + 'toggle full area': ['CTRL+UP_ARROW', 'CTRL+DOWN_ARROW'], # when mouse is hovering a widget or selected geometry, actions take precedence @@ -68,6 +72,7 @@ 'tool help': ['F2'], 'toggle ui': ['F9'], + 'reload css': ['F12'], 'autosave': ['TIMER_AUTOSAVE'], @@ -79,8 +84,6 @@ 'done alt0': ['ESC'], 'insert': ['CTRL+LEFTMOUSE', 'CTRL+LEFTMOUSE+DOUBLE'], - 'insert alt0': ['SHIFT+LEFTMOUSE', 'SHIFT+LEFTMOUSE+DOUBLE'], - 'insert alt1': ['CTRL+SHIFT+LEFTMOUSE', 'CTRL+SHIFT+LEFTMOUSE+DOUBLE'], 'quick insert': ['LEFTMOUSE'], # general commands @@ -128,6 +131,11 @@ 'pie menu alt0': ['SHIFT+Q', 'SHIFT+ACCENT_GRAVE'], 'pie menu confirm': ['LEFTMOUSE+CLICK', 'LEFTMOUSE+DRAG'], + # pinning vertices + 'pin': ['P'], + 'unpin': ['ALT+P'], + 'unpin all': ['SHIFT+ALT+P'], + # shortcuts to tools 'contours tool': ['ONE', 'CTRL+ALT+C'], 'polystrips tool': ['TWO', 'CTRL+ALT+P'], @@ -173,6 +181,7 @@ def get_keymaps(force_reload=False): for k,v in keymap_lr.items(): keymap[k] = list(v) get_keymaps.orig = copy.deepcopy(keymap) + # apply custom keymaps keymap_custom = {} path_custom = options.get_path('keymaps filename') print(f'RetopoFlow keymaps path: {path_custom}') @@ -182,10 +191,33 @@ def get_keymaps(force_reload=False): except Exception as e: print('Exception caught while trying to read custom keymaps') print(str(e)) - # apply custom keymaps for k,v in keymap_custom.items(): # print(f'keymap["{k}"] = {v} (was {keymap[k]})') keymap[k] = v + + # apply substitution + re_sub = re.compile(r'\{(?P[^}]+)\}') + new_keymap = {} + all_done = False + while not all_done: + all_done = True + for name, keys in keymap.items(): + work = list(keys) + new_keys = [] + while work: + key = work.pop(0) + m = re_sub.search(key) + if not m: + new_keys.append(key) + else: + all_done = False + sname = m.group('name') + assert sname in keymap, f'Could not find name {sname} in keymap' + for (i, newkey) in enumerate(keymap[sname]): + work.insert(i, key[:m.start()] + newkey + key[m.end():]) + new_keymap[name] = new_keys + keymap = new_keymap + get_keymaps.keymap = keymap return get_keymaps.keymap get_keymaps.keymap = None diff --git a/config/options.py b/config/options.py index 98853375d..cc28aa1a9 100644 --- a/config/options.py +++ b/config/options.py @@ -47,8 +47,8 @@ # important: update Makefile and root/__init__.py, too! # TODO: make Makefile pull version from here or some other file? # TODO: make __init__.py pull version from here or some other file? -retopoflow_version = '3.2.5' # α β -retopoflow_version_tuple = (3, 2, 5) +retopoflow_version = '3.2.6' # α β +retopoflow_version_tuple = (3, 2, 6) retopoflow_blendermarket_url = 'https://blendermarket.com/products/retopoflow' retopoflow_issues_url = "https://github.com/CGCookie/retopoflow/issues" @@ -84,26 +84,41 @@ def get_git_info(): print(e) get_git_info() -retopoflow_cgcookie_built = os.path.exists(os.path.join(os.path.dirname(__file__), '..', '.cgcookie')) +cgcookie_built_path = os.path.join(os.path.dirname(__file__), '..', '.cgcookie') +cgcookie_built = ( + open(cgcookie_built_path, 'rt').read() + if os.path.exists(cgcookie_built_path) + else '' +) +retopoflow_cgcookie_built = bool(cgcookie_built) +retopoflow_github = 'GitHub' in cgcookie_built +retopoflow_blendermarket = 'Blender Market' in cgcookie_built -def override_version_settings(): - global retopoflow_cgcookie_built, retopoflow_version_git - retopoflow_version_git = None - retopoflow_cgcookie_built = True -# override_version_settings() +def override_version_settings(**kwargs): + global retopoflow_cgcookie_built, retopoflow_version_git, retopoflow_github, retopoflow_blendermarket + if 'git' in kwargs: retopoflow_version_git = kwargs['git'] + if 'cgcookie_built' in kwargs: retopoflow_cgcookie_built = kwargs['cgcookie_built'] + if 'github' in kwargs: retopoflow_github = kwargs['github'] + if 'blendermarket' in kwargs: retopoflow_blendermarket = kwargs['blendermarket'] +override_version_settings() + +print('RetopoFlow git: %s' % str(retopoflow_version_git)) ########################################### # Get system info build_platform = bpy.app.build_platform.decode('utf-8') -retopoflow_git_version = git_info() -platform_system,platform_node,platform_release,platform_version,platform_machine,platform_processor = platform.uname() +( + platform_system, + platform_node, + platform_release, + platform_version, + platform_machine, + platform_processor, +) = platform.uname() gpu_info = gpustate.gpu_info() -print('RetopoFlow git: %s' % str(retopoflow_git_version)) - - class Options: @@ -176,16 +191,18 @@ class Options: # VISIBILITY TEST TUNING PARAMETERS 'visible bbox factor': 0.01, # rf_sources.visibility_preset_* - 'visible dist offset': 0.1, # rf_sources.visibility_preset_* + 'visible dist offset': 0.1, # rf_sources.visibility_preset_* 'selection occlusion test': True, # True: do not select occluded geometry 'selection backface test': True, # True: do not select geometry that is facing away + 'clip auto adjust': True, # True: clip settings are automatically adjusted based on view distance and source bbox 'clip override': True, # True: override with below values; False: scale by unit scale factor 'clip start override': 0.05, 'clip end override': 200.0, + 'hide cursor on tweak': True, # True: cursor is hidden when tweaking geometry + # VISUALIZATION SETTINGS - 'warn non-manifold': True, # visualize non-manifold warnings 'hide overlays': True, # hide overlays (wireframe, grid, axes, etc.) 'override shading': 'light', # light, dark, or off. Sets optimal values for backface culling, xray, shadows, cavity, outline, and matcap 'shading view': 'SOLID', @@ -206,33 +223,62 @@ class Options: 'normal offset multiplier': 1.0, 'constrain offset': True, 'ui scale': 1.0, + + # TARGET VISUALIZATION SETTINGS + # 'pin enabled' and 'pin seam' are in TARGET PINNING SETTINGS + 'warn non-manifold': True, # visualize non-manifold warnings + 'show pinned': True, # visualize pinned geometry + 'show seam': True, + 'target vert size': 4.0, 'target edge size': 1.0, 'target alpha': 1.00, - 'target hidden alpha': 0.02, - 'target alpha backface': 0.2, + 'target hidden alpha': 0.2, + 'target alpha backface': 0.1, 'target cull backfaces': False, + 'target alpha poly': 0.65, 'target alpha poly selected': 0.75, 'target alpha poly warning': 0.25, + 'target alpha poly pinned': 0.75, + 'target alpha poly seam': 0.75, 'target alpha poly mirror': 0.25, 'target alpha poly mirror selected': 0.25, 'target alpha poly mirror warning': 0.15, + 'target alpha poly mirror pinned': 0.25, + 'target alpha poly mirror seam': 0.25, + 'target alpha line': 0.10, 'target alpha line selected': 1.00, 'target alpha line warning': 0.25, + 'target alpha line pinned': 0.25, + 'target alpha line seam': 0.25, 'target alpha line mirror': 0.10, 'target alpha line mirror selected': 0.50, 'target alpha line mirror warning': 0.15, + 'target alpha line mirror pinned': 0.15, + 'target alpha line mirror seam': 0.15, + 'target alpha point': 0.25, 'target alpha point selected': 1.00, 'target alpha point warning': 0.50, + 'target alpha point pinned': 0.50, + 'target alpha point seam': 0.50, 'target alpha point mirror': 0.00, 'target alpha point mirror selected': 0.50, 'target alpha point mirror warning': 0.15, + 'target alpha point mirror pinned': 0.15, + 'target alpha point mirror seam': 0.15, 'target alpha point highlight': 1.00, + 'target alpha mirror': 1.00, + + # TARGET PINNING SETTINGS + # 'show pinned' and 'show seam' are in TARGET VISUALIZATION SETTINGS + 'pin enabled': True, + 'pin seam': True, + # ADDON UPDATER SETTINGS 'updater auto check update': True, 'updater interval months': 0, @@ -345,6 +391,7 @@ class Options: is_dirty = False # does the internal db differ from db stored in file? (need writing) last_change = 0 # when did we last changed an option? write_delay = 1.0 # seconds to wait before writing db to file + write_error = False # True when we failed to write options to file def __init__(self): self._callbacks = [] @@ -410,7 +457,7 @@ def dirty(self): Options.last_change = time.time() self.update_external_vars() - def clean(self, force=False): + def clean(self, force=False, raise_exception=True): if not Options.is_dirty: # nothing has changed return @@ -418,8 +465,20 @@ def clean(self, force=False): # we haven't waited long enough before storing db return dprint('Writing options:', Options.db) - json.dump(Options.db, open(Options.fndb, 'wt'), indent=2, sort_keys=True) - Options.is_dirty = False + try: + json.dump( + Options.db, + open(Options.fndb, 'wt'), + indent=2, + sort_keys=True, + ) + Options.is_dirty = False + except PermissionError as e: + self.write_error = True + if raise_exception: raise e + except Exception as e: + self.write_error = True + if raise_exception: raise e def read(self): Options.db = {} @@ -489,46 +548,48 @@ def get_auto_save_filepath(self): return '%s_RetopoFlow_AutoSave%s' % (base, ext) -def ints_to_Color(r, g, b, a=255): return Color((r/255.0, g/255.0, b/255.0, a/255.0)) class Themes: # fallback color for when specified key is not found - error = ints_to_Color(255, 64, 255, 255) + error = Color.from_ints(255, 64, 255, 255) common = { - 'mesh': ints_to_Color(255, 255, 255, 255), - 'warning': ints_to_Color(182, 31, 0, 128), + 'mesh': Color.from_ints(255, 255, 255, 255), + 'warning': Color.from_ints(182, 31, 0, 128), - 'stroke': ints_to_Color( 255, 255, 0, 255), - 'highlight': ints_to_Color(255, 255, 25, 255), + 'stroke': Color.from_ints( 255, 255, 0, 255), + 'highlight': Color.from_ints(255, 255, 25, 255), # RFTools - 'polystrips': ints_to_Color(0, 100, 25, 150), - 'strokes': ints_to_Color(0, 100, 90, 150), - 'tweak': ints_to_Color(229, 137, 26, 255), # Opacity is set by brush strength - 'relax': ints_to_Color(0, 135, 255, 255), # Opacity is set by brush strength + 'polystrips': Color.from_ints(0, 100, 25, 150), + 'strokes': Color.from_ints(0, 100, 90, 150), + 'tweak': Color.from_ints(229, 137, 26, 255), # Opacity is set by brush strength + 'relax': Color.from_ints(0, 135, 255, 255), # Opacity is set by brush strength } themes = { 'Blue': { - 'select': ints_to_Color( 55, 160, 255), - 'new': ints_to_Color( 40, 40, 255), - 'active': ints_to_Color( 40, 255, 255), - # 'active': ints_to_Color( 55, 160, 255), - 'warn': ints_to_Color(182, 31, 0), + 'select': Color.from_ints( 55, 160, 255), + 'new': Color.from_ints( 40, 40, 255), + 'active': Color.from_ints( 40, 255, 255), + 'warn': Color.from_ints(182, 31, 0), + 'pin': Color.from_ints(128, 128, 192), + 'seam': Color.from_ints(255, 255, 160), }, 'Green': { - 'select': ints_to_Color( 78, 207, 81), - 'new': ints_to_Color( 40, 255, 40), - 'active': ints_to_Color( 40, 255, 255), - # 'active': ints_to_Color( 78, 207, 81), - 'warn': ints_to_Color(182, 31, 0), + 'select': Color.from_ints( 78, 207, 81), + 'new': Color.from_ints( 40, 255, 40), + 'active': Color.from_ints( 40, 255, 255), + 'warn': Color.from_ints(182, 31, 0), + 'pin': Color.from_ints(128, 192, 128), + 'seam': Color.from_ints(255, 160, 255), }, 'Orange': { - 'select': ints_to_Color(255, 135, 54), - 'new': ints_to_Color(255, 128, 64), - 'active': ints_to_Color(255, 80, 64), - # 'active': ints_to_Color(255, 135, 54), - 'warn': ints_to_Color(182, 31, 0), + 'select': Color.from_ints(255, 135, 54), + 'new': Color.from_ints(255, 128, 64), + 'active': Color.from_ints(255, 80, 64), + 'warn': Color.from_ints(182, 31, 0), + 'pin': Color.from_ints(192, 160, 128), + 'seam': Color.from_ints(160, 255, 255), }, } @@ -554,20 +615,32 @@ def update_settings(self): 'target alpha poly', 'target alpha poly selected', 'target alpha poly warning', + 'target alpha poly pinned', + 'target alpha poly seam', 'target alpha poly mirror selected', 'target alpha poly mirror warning', + 'target alpha poly mirror pinned', + 'target alpha poly mirror seam', 'target alpha line', 'target alpha line selected', 'target alpha line warning', + 'target alpha line pinned', + 'target alpha line seam', 'target alpha line mirror', 'target alpha line mirror selected', 'target alpha line mirror warning', + 'target alpha line mirror pinned', + 'target alpha line mirror seam', 'target alpha point', 'target alpha point selected', 'target alpha point warning', + 'target alpha point pinned', + 'target alpha point seam', 'target alpha point mirror', 'target alpha point mirror selected', 'target alpha point mirror warning', + 'target alpha point mirror pinned', + 'target alpha point mirror seam', 'target alpha point highlight', 'target alpha mirror', ] @@ -577,6 +650,8 @@ def update_settings(self): color_mesh = themes['mesh'] color_select = themes['select'] color_warn = themes['warn'] + color_pin = themes['pin'] + color_seam = themes['seam'] color_hilight = themes['highlight'] normal_offset_multiplier = options['normal offset multiplier'] constrain_offset = options['constrain offset'] @@ -593,6 +668,8 @@ def update_settings(self): 'load verts': False, 'no selection': True, 'no warning': True, + 'no pinned': True, + 'no seam': True, 'no below': True, 'triangles only': True, # source bmeshes are triangles only! 'cull backfaces': True, @@ -606,31 +683,41 @@ def update_settings(self): self._target_settings = { 'poly color': (*color_mesh[:3], options['target alpha poly']), 'poly color selected': (*color_select[:3], options['target alpha poly selected']), - 'poly color warning': (*color_warn[:3], options['target alpha poly warning']), + 'poly color warning': (*color_warn[:3], options['target alpha poly warning']), + 'poly color pinned': (*color_pin[:3], options['target alpha poly pinned']), + 'poly color seam': (*color_seam[:3], options['target alpha poly seam']), 'poly offset': 0.000010, 'poly dotoffset': 1.0, 'poly mirror color': (*color_mesh[:3], options['target alpha poly mirror'] * mirror_alpha_factor), 'poly mirror color selected': (*color_select[:3], options['target alpha poly mirror selected'] * mirror_alpha_factor), - 'poly mirror color warning': (*color_warn[:3], options['target alpha poly mirror warning'] * mirror_alpha_factor), + 'poly mirror color warning': (*color_warn[:3], options['target alpha poly mirror warning'] * mirror_alpha_factor), + 'poly mirror color pinned': (*color_pin[:3], options['target alpha poly mirror pinned'] * mirror_alpha_factor), + 'poly mirror color seam': (*color_seam[:3], options['target alpha poly mirror seam'] * mirror_alpha_factor), 'poly mirror offset': 0.000010, 'poly mirror dotoffset': 1.0, 'line color': (*color_mesh[:3], options['target alpha line']), 'line color selected': (*color_select[:3], options['target alpha line selected']), - 'line color warning': (*color_warn[:3], options['target alpha line warning']), + 'line color warning': (*color_warn[:3], options['target alpha line warning']), + 'line color pinned': (*color_pin[:3], options['target alpha line pinned']), + 'line color seam': (*color_seam[:3], options['target alpha line seam']), 'line width': edge_size, 'line offset': 0.000012, 'line dotoffset': 1.0, 'line mirror color': (*color_mesh[:3], options['target alpha line mirror'] * mirror_alpha_factor), 'line mirror color selected': (*color_select[:3], options['target alpha line mirror selected'] * mirror_alpha_factor), - 'line mirror color warning': (*color_warn[:3], options['target alpha line mirror warning'] * mirror_alpha_factor), + 'line mirror color warning': (*color_warn[:3], options['target alpha line mirror warning'] * mirror_alpha_factor), + 'line mirror color pinned': (*color_pin[:3], options['target alpha line mirror pinned'] * mirror_alpha_factor), + 'line mirror color seam': (*color_seam[:3], options['target alpha line mirror seam'] * mirror_alpha_factor), 'line mirror width': 1.5, 'line mirror offset': 0.000012, 'line mirror dotoffset': 1.0, 'point color': (*color_mesh[:3], options['target alpha point']), 'point color selected': (*color_select[:3], options['target alpha point selected']), - 'point color warning': (*color_warn[:3], options['target alpha point warning']), + 'point color warning': (*color_warn[:3], options['target alpha point warning']), + 'point color pinned': (*color_pin[:3], options['target alpha point pinned']), + 'point color seam': (*color_seam[:3], options['target alpha point seam']), 'point color highlight': (*color_hilight[:3],options['target alpha point highlight']), 'point size': vert_size, 'point size highlight': 10.0, @@ -638,12 +725,14 @@ def update_settings(self): 'point dotoffset': 1.0, 'point mirror color': (*color_mesh[:3], options['target alpha point mirror'] * mirror_alpha_factor), 'point mirror color selected': (*color_select[:3], options['target alpha point mirror selected'] * mirror_alpha_factor), - 'point mirror color warning': (*color_warn[:3], options['target alpha point mirror warning'] * mirror_alpha_factor), + 'point mirror color warning': (*color_warn[:3], options['target alpha point mirror warning'] * mirror_alpha_factor), + 'point mirror color pinned': (*color_pin[:3], options['target alpha point mirror pinned'] * mirror_alpha_factor), + 'point mirror color seam': (*color_seam[:3], options['target alpha point mirror seam'] * mirror_alpha_factor), 'point mirror size': 3.0, 'point mirror offset': 0.000015, 'point mirror dotoffset': 1.0, - 'focus mult': 1.0, + 'focus mult': 0.0, #1.0, 'normal offset': 0.001 * normal_offset_multiplier, # pushes vertices out along normal 'constrain offset': constrain_offset, } diff --git a/config/ui.css b/config/ui.css index ad51b391e..5e55af188 100644 --- a/config/ui.css +++ b/config/ui.css @@ -655,7 +655,7 @@ label.tool > span { -label.symmetry-enable { +label.mirror-enable { display: inline; width: 33.333%; overflow-x: hidden; diff --git a/docs/_data/keymaps.yml b/docs/_data/keymaps.yml index f287ca48c..9b4dfb37c 100644 --- a/docs/_data/keymaps.yml +++ b/docs/_data/keymaps.yml @@ -1,3 +1,4 @@ +blender_passthrough: "" toggle_full_area: "Ctrl+ArrowDown, Ctrl+ArrowUp" action: "LMB+Drag" action_alt0: "Shift+LMB" @@ -16,6 +17,7 @@ all_help: "Shift+F1" general_help: "F1" tool_help: "F2" toggle_ui: "F9" +reload_css: "F12" autosave: "TIMER_AUTOSAVE" cancel: "Escape, RMB" confirm: "Enter, LMB+Click, NumEnter" @@ -23,8 +25,6 @@ confirm_drag: "LMB+Drag" done: "Tab" done_alt0: "Escape" insert: "Ctrl+LMB, Ctrl+LMB+Double" -insert_alt0: "Shift+LMB, Shift+LMB+Double" -insert_alt1: "Ctrl+Shift+LMB, Ctrl+Shift+LMB+Double" quick_insert: "LMB" grab: "G" rotate: "R" @@ -53,6 +53,9 @@ brush_strength: "Shift+F" pie_menu: "`, Q" pie_menu_alt0: "Shift+Q, ~" pie_menu_confirm: "LMB+Click, LMB+Drag" +pin: "P" +unpin: "Alt+P" +unpin_all: "Shift+Alt+P" contours_tool: "1, Ctrl+Alt+C" polystrips_tool: "2, Ctrl+Alt+P" strokes_tool: "3, Ctrl+Alt+B" diff --git a/docs/_data/options.yml b/docs/_data/options.yml index f711aec29..5d7e68db0 100644 --- a/docs/_data/options.yml +++ b/docs/_data/options.yml @@ -1,3 +1,3 @@ -rf_version: '3.2.5' +rf_version: '3.2.6' warning_max_sources: '1m' warning_max_target: '20k' \ No newline at end of file diff --git a/docs/changelist.md b/docs/changelist.md index 04eacbf53..1e8477666 100644 --- a/docs/changelist.md +++ b/docs/changelist.md @@ -2,6 +2,18 @@ This document contains details about what has changed in RetopoFlow since version 2.x. +### RetopoFlow 3.2.5→3.2.6 + +- Vertex pinning and unpinning, where pinned vertices cannot be moved +- Seam edges can be pinned +- Option to hide mouse cursor when moving geometry +- Keymap editor improvements: shows keys for done and toggle UI, added Blender passthrough, fixed many bugs +- Fixed bug where modifier key states would be out of sync if pressed or unpressed while changing view +- Added auto clip adjustment setting, which adjusts clip settings based on view position and distance to bbox of sources +- Fixed visualization bug where depth test wasn't always enabled and depth range might not be [0,1] +- Added check for and button to select vertices that are on the "wrong" side of symmetry planes. +- Fixed many bugs and cleaned up code + ### RetopoFlow 3.2.4→3.2.5 - Worked around a major crashing bug in Blender 3.0 and 3.1 diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 000000000..d744291d4 --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,80 @@ +# RetopoFlow Debugging + +If you run into an issue while using RetopoFlow, you can report it via [Product Support](https://blendermarket.com/products/retopoflow) on Blender Market or by creating an issue via GitHub RetopoFlow [Issues](https://github.com/CGCookie/retopoflow/issues/new/choose). +These RetopoFlow issues covers bugs within RetopoFlow or unexpected behavior of tools. + +Note: support via GitHub will be limited to fixing RetopoFlow bugs. +The Blender Market route will provide you premium support. + +Whichever path of support you take, there are a few things to keep in mind to help us in debugging and fixing the issue as quickly as possible. +The list below contain a few of these. + +- Explain clearly the context. For example, add a screenshot or share a .blend file that shows what the scene looked like before you had the issue, what action you did to cause the issue, and what was the result of the action. + +- Consider sharing your .blend file with us. Often times, the .blend file has a particular setting that we have not tested, and having access to your file will make reproducing your issue easier. + +- Try to reproduce the issue on the default Blender scene, or try to reproduce the issue on another machine, especially a different system (OSX, Windows, Linux) if possible. + +- Be sure to include all of the terminal / console output in your post (see below). + +- Be sure to include the machine information, Blender version, and RetopoFlow version in your post. + +- Be sure to reply to our questions. If we are unable to reproduce the issue, and it goes without any activity for a time, we will close the issue. + + +## Terminal / Console Output + +Sometimes an issue is caused by a different part of code than what is reported. +By design, we do not report all the information in RetopoFlow, but that information might be critical to solving the issue. +You can access the additional information through the system terminal / console. + +Note: There might be a lot of info in there (have to scroll), so be sure to copy _all_ of the text from the terminal / console. + +### Windows + +- Start Blender as usual +- In the Blender Menu: Windows > Toggle System Console. The system console window will now open; minimize for now. +- Start RetopoFlow +- Once issue occurs, switch to the system console. + +### OSX + +Option 1: + +- Right click on Blender app (ex: in Applications), then click New Terminal at Folder. The system terminal window will now open. +- In the terminal, type `./Contents/MacOS/Blender` to start Blender. +- Start RetopoFlow +- Once issue occurs, switch to the system terminal. + +Option 2: + +- Open Terminal (Command+Space, type Terminal) +- Open Finder, and browse to the Blender app. +- Right click on Blender, then click Show Package Contents. +- Open Contents folder, then open MacOS folder +- Drag the blender file to the Terminal window +- In Terminal, press enter. +- Start RetopoFlow +- Once issue occurs, switch to the system terminal. + +### Linux + +Option 1: + +- Right click Blender app +- Choose Edit Application +- Under Advanced tab, check "Run in terminal" +- Save and close +- Start Blender as normal, but now a terminal window will show right before Blender loads. +- Start RetopoFlow +- Once issue occurs, switch to the system terminal. + +Option 2: + +- Open system terminal / console +- Type `/path/to/blender`, where `/path/to` is the path to the blender binary. + ex: `/home/username/Downloads/Blender\ 3.0.1/blender` + ex: `/usr/bin/blender` +- Start RetopoFlow +- Once issue occurs, switch to the system terminal / console + diff --git a/docs/general.md b/docs/general.md index 107a791f0..f62f2bd99 100644 --- a/docs/general.md +++ b/docs/general.md @@ -144,11 +144,11 @@ Vertex Size and Edge Size control how large the vertices and how think the edges -## Symmetry Options +## Mirror Options -The X, Y, Z checkboxes turn on/off symmetry or mirroring along the X, Y, Z axes. -Note: symmetry utilizes the mirror modifier. +The X, Y, Z checkboxes turn on/off mirroring along the X, Y, Z axes. +Note: these options utilize the mirror modifier. -When symmetry is turned on, the mirroring planes can be visualized directly using Plane option, or indirectly by coloring the sources choosing either the Edge or Face option. +When mirroring is turned on, the mirroring planes can be visualized directly using Plane option, or indirectly by coloring the sources choosing either the Edge or Face option. The Effect setting controls the strength of the visualization. diff --git a/docs/table_of_contents.md b/docs/table_of_contents.md index 711cfd07c..48d612eb0 100644 --- a/docs/table_of_contents.md +++ b/docs/table_of_contents.md @@ -39,6 +39,7 @@ The following links provide additional information. The following provide additional information about more advanced RetopoFlow features. +- [Debbuging](debugging.md) - [Updater System](addon_updater.md) - [Keymap Editor](keymap_editor.md) diff --git a/help/addon_updater_advanced.thumb.png b/help/addon_updater_advanced.thumb.png index 28c78fc80..cc14d1a5c 100644 Binary files a/help/addon_updater_advanced.thumb.png and b/help/addon_updater_advanced.thumb.png differ diff --git a/help/addon_updater_button.thumb.png b/help/addon_updater_button.thumb.png index d7bf98565..9bfee4481 100644 Binary files a/help/addon_updater_button.thumb.png and b/help/addon_updater_button.thumb.png differ diff --git a/help/addon_updater_system.thumb.png b/help/addon_updater_system.thumb.png index 7ae179c37..e020d369b 100644 Binary files a/help/addon_updater_system.thumb.png and b/help/addon_updater_system.thumb.png differ diff --git a/help/blendermarket_screenshot.thumb.png b/help/blendermarket_screenshot.thumb.png index 184823218..c21c04e93 100644 Binary files a/help/blendermarket_screenshot.thumb.png and b/help/blendermarket_screenshot.thumb.png differ diff --git a/help/changelist.md b/help/changelist.md index 0ac4f797c..d19b5d18e 100644 --- a/help/changelist.md +++ b/help/changelist.md @@ -2,6 +2,18 @@ This document contains details about what has changed in RetopoFlow since version 2.x. +### RetopoFlow 3.2.5→3.2.6 + +- Vertex pinning and unpinning, where pinned vertices cannot be moved +- Seam edges can be pinned +- Option to hide mouse cursor when moving geometry +- Keymap editor improvements: shows keys for done and toggle UI, added Blender passthrough, fixed many bugs +- Fixed bug where modifier key states would be out of sync if pressed or unpressed while changing view +- Added auto clip adjustment setting, which adjusts clip settings based on view position and distance to bbox of sources +- Fixed visualization bug where depth test wasn't always enabled and depth range might not be [0,1] +- Added check for and button to select vertices that are on the "wrong" side of symmetry planes. +- Fixed many bugs and cleaned up code + ### RetopoFlow 3.2.4→3.2.5 - Worked around a major crashing bug in Blender 3.0 and 3.1 diff --git a/help/debugging.md b/help/debugging.md new file mode 100644 index 000000000..e4ac04685 --- /dev/null +++ b/help/debugging.md @@ -0,0 +1,79 @@ +# RetopoFlow Debugging + +If you run into an issue while using RetopoFlow, you can report it via [Product Support](https://blendermarket.com/products/retopoflow) on Blender Market or by creating an issue via GitHub RetopoFlow [Issues](https://github.com/CGCookie/retopoflow/issues/new/choose). +These RetopoFlow issues covers bugs within RetopoFlow or unexpected behavior of tools. + +Note: support via GitHub will be limited to fixing RetopoFlow bugs. +The Blender Market route will provide you premium support. + +Whichever path of support you take, there are a few things to keep in mind to help us in debugging and fixing the issue as quickly as possible. +The list below contain a few of these. + +- Explain clearly the context. For example, add a screenshot or share a .blend file that shows what the scene looked like before you had the issue, what action you did to cause the issue, and what was the result of the action. + +- Consider sharing your .blend file with us. Often times, the .blend file has a particular setting that we have not tested, and having access to your file will make reproducing your issue easier. + +- Try to reproduce the issue on the default Blender scene, or try to reproduce the issue on another machine, especially a different system (OSX, Windows, Linux) if possible. + +- Be sure to include all of the terminal / console output in your post (see below). + +- Be sure to include the machine information, Blender version, and RetopoFlow version in your post. + +- Be sure to reply to our questions. If we are unable to reproduce the issue, and it goes without any activity for a time, we will close the issue. + + +## Terminal / Console Output + +Sometimes an issue is caused by a different part of code than what is reported. +By design, we do not report all the information in RetopoFlow, but that information might be critical to solving the issue. +You can access the additional information through the system terminal / console. + +Note: There might be a lot of info in there (have to scroll), so be sure to copy _all_ of the text from the terminal / console. + +### Windows + +- Start Blender as usual +- In the Blender Menu: Windows > Toggle System Console. The system console window will now open; minimize for now. +- Start RetopoFlow +- Once issue occurs, switch to the system console. + +### OSX + +Option 1: + +- Right click on Blender app (ex: in Applications), then click New Terminal at Folder. The system terminal window will now open. +- In the terminal, type `./Contents/MacOS/Blender` to start Blender. +- Start RetopoFlow +- Once issue occurs, switch to the system terminal. + +Option 2: + +- Open Terminal (Command+Space, type Terminal) +- Open Finder, and browse to the Blender app. +- Right click on Blender, then click Show Package Contents. +- Open Contents folder, then open MacOS folder +- Drag the blender file to the Terminal window +- In Terminal, press enter. +- Start RetopoFlow +- Once issue occurs, switch to the system terminal. + +### Linux + +Option 1: + +- Right click Blender app +- Choose Edit Application +- Under Advanced tab, check "Run in terminal" +- Save and close +- Start Blender as normal, but now a terminal window will show right before Blender loads. +- Start RetopoFlow +- Once issue occurs, switch to the system terminal. + +Option 2: + +- Open system terminal / console +- Type `/path/to/blender`, where `/path/to` is the path to the blender binary. + ex: `/home/username/Downloads/Blender\ 3.0.1/blender` + ex: `/usr/bin/blender` +- Start RetopoFlow +- Once issue occurs, switch to the system terminal / console diff --git a/help/delete_dialog_pie.thumb.png b/help/delete_dialog_pie.thumb.png index 13c21195d..9ea7d6b28 100644 Binary files a/help/delete_dialog_pie.thumb.png and b/help/delete_dialog_pie.thumb.png differ diff --git a/help/general.md b/help/general.md index 27b9fbd39..ea704fa0c 100644 --- a/help/general.md +++ b/help/general.md @@ -144,10 +144,10 @@ Vertex Size and Edge Size control how large the vertices and how think the edges -## Symmetry Options +## Mirror Options -The X, Y, Z checkboxes turn on/off symmetry or mirroring along the X, Y, Z axes. -Note: symmetry utilizes the mirror modifier. +The X, Y, Z checkboxes turn on/off mirroring along the X, Y, Z axes. +Note: these options utilize the mirror modifier. -When symmetry is turned on, the mirroring planes can be visualized directly using Plane option, or indirectly by coloring the sources choosing either the Edge or Face option. +When mirroring is turned on, the mirroring planes can be visualized directly using Plane option, or indirectly by coloring the sources choosing either the Edge or Face option. The Effect setting controls the strength of the visualization. diff --git a/help/global_exception.thumb.png b/help/global_exception.thumb.png index e1a5a8504..1f4118f23 100644 Binary files a/help/global_exception.thumb.png and b/help/global_exception.thumb.png differ diff --git a/help/help_contours.thumb.png b/help/help_contours.thumb.png index 196cbdbf5..faaa5371a 100644 Binary files a/help/help_contours.thumb.png and b/help/help_contours.thumb.png differ diff --git a/help/help_knife.thumb.png b/help/help_knife.thumb.png index f2b6cb0e0..ef88393c4 100644 Binary files a/help/help_knife.thumb.png and b/help/help_knife.thumb.png differ diff --git a/help/help_loops.thumb.png b/help/help_loops.thumb.png index 3a815a4b2..908c83446 100644 Binary files a/help/help_loops.thumb.png and b/help/help_loops.thumb.png differ diff --git a/help/help_patches.thumb.png b/help/help_patches.thumb.png index b4a5b156b..b5e3f799d 100644 Binary files a/help/help_patches.thumb.png and b/help/help_patches.thumb.png differ diff --git a/help/help_polypen.thumb.png b/help/help_polypen.thumb.png index 5b04d5ce8..5a494afb4 100644 Binary files a/help/help_polypen.thumb.png and b/help/help_polypen.thumb.png differ diff --git a/help/help_polypen_modes_options_pie.thumb.png b/help/help_polypen_modes_options_pie.thumb.png index c3e6a8208..8530e4d5d 100644 Binary files a/help/help_polypen_modes_options_pie.thumb.png and b/help/help_polypen_modes_options_pie.thumb.png differ diff --git a/help/help_polystrips.thumb.png b/help/help_polystrips.thumb.png index 85fd4f6fd..149877ffd 100644 Binary files a/help/help_polystrips.thumb.png and b/help/help_polystrips.thumb.png differ diff --git a/help/help_relax.thumb.png b/help/help_relax.thumb.png index 7bd197131..10dafdd54 100644 Binary files a/help/help_relax.thumb.png and b/help/help_relax.thumb.png differ diff --git a/help/help_strokes.thumb.png b/help/help_strokes.thumb.png index ea951fbac..b209573b0 100644 Binary files a/help/help_strokes.thumb.png and b/help/help_strokes.thumb.png differ diff --git a/help/help_strokes_modes_options_pie.thumb.png b/help/help_strokes_modes_options_pie.thumb.png index 5af5dd5bf..9bb57db11 100644 Binary files a/help/help_strokes_modes_options_pie.thumb.png and b/help/help_strokes_modes_options_pie.thumb.png differ diff --git a/help/help_themes.thumb.png b/help/help_themes.thumb.png index 6f2a5f69c..92f629db8 100644 Binary files a/help/help_themes.thumb.png and b/help/help_themes.thumb.png differ diff --git a/help/help_tweak.thumb.png b/help/help_tweak.thumb.png index 5c5e310d7..87d3f9e35 100644 Binary files a/help/help_tweak.thumb.png and b/help/help_tweak.thumb.png differ diff --git a/help/install.thumb.png b/help/install.thumb.png index 4f395a904..a879f1a09 100644 Binary files a/help/install.thumb.png and b/help/install.thumb.png differ diff --git a/help/keymap_all.thumb.png b/help/keymap_all.thumb.png index 95b43aab5..8763e35cc 100644 Binary files a/help/keymap_all.thumb.png and b/help/keymap_all.thumb.png differ diff --git a/help/keymap_button.thumb.png b/help/keymap_button.thumb.png index 6a081da38..9ae8055b7 100644 Binary files a/help/keymap_button.thumb.png and b/help/keymap_button.thumb.png differ diff --git a/help/keymap_insert.thumb.png b/help/keymap_insert.thumb.png index de37d8953..465c8fce8 100644 Binary files a/help/keymap_insert.thumb.png and b/help/keymap_insert.thumb.png differ diff --git a/help/pie_menu.thumb.png b/help/pie_menu.thumb.png index 0d5a52a0d..b125806c2 100644 Binary files a/help/pie_menu.thumb.png and b/help/pie_menu.thumb.png differ diff --git a/help/retopoflow_3_feature.thumb.png b/help/retopoflow_3_feature.thumb.png index 4c24f6bd6..6dd7bef57 100644 Binary files a/help/retopoflow_3_feature.thumb.png and b/help/retopoflow_3_feature.thumb.png differ diff --git a/help/selection_options.thumb.png b/help/selection_options.thumb.png index bc2d516f7..5cec794af 100644 Binary files a/help/selection_options.thumb.png and b/help/selection_options.thumb.png differ diff --git a/help/start_rf_create_new_target.thumb.png b/help/start_rf_create_new_target.thumb.png index 9e5fcb6ed..f381b60cc 100644 Binary files a/help/start_rf_create_new_target.thumb.png and b/help/start_rf_create_new_target.thumb.png differ diff --git a/help/start_rf_quickstart.thumb.png b/help/start_rf_quickstart.thumb.png index 49084085d..ed010ecdb 100644 Binary files a/help/start_rf_quickstart.thumb.png and b/help/start_rf_quickstart.thumb.png differ diff --git a/help/start_rf_tool.thumb.png b/help/start_rf_tool.thumb.png index f4b8a7687..4d5af3d9a 100644 Binary files a/help/start_rf_tool.thumb.png and b/help/start_rf_tool.thumb.png differ diff --git a/help/table_of_contents.md b/help/table_of_contents.md index 78f88baca..0fb2548f0 100644 --- a/help/table_of_contents.md +++ b/help/table_of_contents.md @@ -39,5 +39,6 @@ The following links provide additional information. The following provide additional information about more advanced RetopoFlow features. +- [Debbuging](debugging.md) - [Updater System](addon_updater.md) - [Keymap Editor](keymap_editor.md) diff --git a/help/warning_viewlock.thumb.png b/help/warning_viewlock.thumb.png index eaa1c7866..aee15a981 100644 Binary files a/help/warning_viewlock.thumb.png and b/help/warning_viewlock.thumb.png differ diff --git a/help/warnings.thumb.png b/help/warnings.thumb.png index a49025744..83f3d9bff 100644 Binary files a/help/warnings.thumb.png and b/help/warnings.thumb.png differ diff --git a/hive.json b/hive.json index b12428593..652b88dee 100644 --- a/hive.json +++ b/hive.json @@ -1,7 +1,7 @@ { "name": "RetopoFlow", "description": "A suite of retopology tools for Blender through a unified retopology mode", - "version": "3.2.5", + "version": "3.2.6", "source": "Blender Market", "product url": "https://blendermarket.com/products/retopoflow", "documentation url": "https://docs.retopoflow.com", diff --git a/retopoflow/retopoflow.py b/retopoflow/retopoflow.py index d012bed21..ee6132c6e 100644 --- a/retopoflow/retopoflow.py +++ b/retopoflow/retopoflow.py @@ -186,6 +186,7 @@ def setup_next_stage_enter(self): ('Setting up user interface', self.setup_ui), # must be called after self.setup_target() and self.setup_rftools()!! ('Setting up undo system', self.setup_undo), # must be called after self.setup_ui()!! ('Checking auto save / save', self.check_auto_save_warnings), + ('Checking target symmetry', self.check_target_symmetry), ('Loading welcome message', self.show_welcome_message), ('Resuming help image preloading', self.preload_help_resume), ] diff --git a/retopoflow/rf/options_dialog.html b/retopoflow/rf/options_dialog.html index bb45501c2..20ae38695 100644 --- a/retopoflow/rf/options_dialog.html +++ b/retopoflow/rf/options_dialog.html @@ -1,4 +1,5 @@ @@ -76,8 +75,8 @@

Options

General
-
-

Start Up and Quit

+
+ Start Up and Quit
-
+
+

Interface

@@ -139,7 +142,7 @@

Interface

-
+
Viewport Display
@@ -189,27 +192,33 @@

Selection Theme

View Clipping

-
-
- - -
-
- - -
-
-
-
- - +
+ +
+
+ + +
+
+ + +
-
- - +
+
+ + +
+
+ + +
@@ -221,6 +230,14 @@

Target Mesh Drawing

Non-manifold Outline + +
@@ -375,7 +392,7 @@

Presets

-
+
Target Visibility
@@ -396,7 +413,7 @@

Reveal

-
+
Target Cleaning
@@ -428,45 +445,87 @@

Normals

+
+

Symmetry

+
+ +
+
+ +
+
+ Pinning / Seams +
+
+

Geometry Pinning

+
+ + + + + +
+
+
+

Seams

+
+ + + + +
+
-
- Symmetry +
+ Symmetry
-
diff --git a/retopoflow/rf/rf_blender.py b/retopoflow/rf/rf_blender.py index 5db1e2273..8f889fe48 100644 --- a/retopoflow/rf/rf_blender.py +++ b/retopoflow/rf/rf_blender.py @@ -159,10 +159,12 @@ def scale_object(o): # override/scale clipping distances if not hasattr(self, '_clip_distances'): # store original clip distances + print(f'RetopoFlow: storing clip distances: {space.clip_start} {space.clip_end}') self._clip_distances = { 'start': space.clip_start, 'end': space.clip_end, } + if clip_restore: space.clip_start = self._clip_distances['start'] space.clip_end = self._clip_distances['end'] diff --git a/retopoflow/rf/rf_keymapsystem.py b/retopoflow/rf/rf_keymapsystem.py index f95251b66..85150bc5a 100644 --- a/retopoflow/rf/rf_keymapsystem.py +++ b/retopoflow/rf/rf_keymapsystem.py @@ -32,7 +32,7 @@ from ...addon_common.common.human_readable import convert_actions_to_human_readable from ...config.options import options, retopoflow_version, retopoflow_helpdocs_url, retopoflow_blendermarket_url -from ...config.keymaps import get_keymaps, reset_all_keymaps, save_custom_keymaps, reset_keymap +from ...config.keymaps import get_keymaps, reset_all_keymaps, save_custom_keymaps, reset_keymap, default_rf_keymaps class RetopoFlow_KeymapSystem: @staticmethod @@ -141,19 +141,19 @@ def edit_capture(): capture_edit_key_span() def edit_lmb(): clear_edit_key_span() - edit_data['key'] = 'LMB' + edit_data['key'] = 'LEFTMOUSE' def edit_mmb(): clear_edit_key_span() - edit_data['key'] = 'MMB' + edit_data['key'] = 'MIDDLEMOUSE' def edit_rmb(): clear_edit_key_span() - edit_data['key'] = 'RMB' + edit_data['key'] = 'RIGHTMOUSE' def edit_wheelup(): clear_edit_key_span() - edit_data['key'] = 'WheelUp' + edit_data['key'] = 'WHEELUPMOUSE' def edit_wheeldown(): clear_edit_key_span() - edit_data['key'] = 'WheelDown' + edit_data['key'] = 'WHEELDOWNMOUSE' def edit_capture_key(event): if event.key is None or event.key == 'NONE': return ui_button = self.document.body.getElementById('edit-key-span') @@ -315,11 +315,13 @@ def rebuild(): ('confirm', 'Confirm'), ('confirm drag', 'Confirm with Drag (sometimes this is needed for certain actions)'), ('cancel', 'Cancel'), + ('done', 'Quit RetopoFlow'), + ('done alt0', 'Quit RetopoFlow (alternative)'), + ('toggle ui', 'Toggle UI visibility'), + ('blender passthrough', 'Blender passthrough'), ]), ('Insert, Move, Rotate, Scale', [ ('insert', 'Insert new geometry'), - # ('insert alt0', 'Insert new geometry (alt0)'), - # ('insert alt1', 'Insert new geometry (alt1)'), ('quick insert', 'Quick insert (Knife, Loops)'), ('increase count', 'Increase Count'), ('decrease count', 'Decrease Count'), @@ -338,13 +340,10 @@ def rebuild(): ('fill', 'Patches: fill'), ('knife reset', 'Knife: reset'), ]), - ('Selection, Hiding/Reveal', [ + ('Selection', [ ('select all', 'Select all'), ('select invert', 'Select invert'), ('deselect all', 'Deselect all'), - ('hide selected', 'Hide selected geometry'), - ('hide unselected', 'Hide unselected geometry'), - ('reveal hidden', 'Reveal hidden geometry'), ('select single', 'Select single item (default depends on Blender selection setting)'), ('select single add', 'Add single item to selection (default depends on Blender selection setting)'), ('select smart', 'Smart selection (default depends on Blender selection setting)'), @@ -353,6 +352,14 @@ def rebuild(): ('select paint add', 'Paint to add to selection (default depends on Blender selection setting)'), ('select path add', 'Select along shortest path (default depends on Blender selection setting)'), ]), + ('Geometry Attributes', [ + ('hide selected', 'Hide selected geometry'), + ('hide unselected', 'Hide unselected geometry'), + ('reveal hidden', 'Reveal hidden geometry'), + ('pin', 'Pin selected geometry'), + ('unpin', 'Unpin selected geometry'), + ('unpin all', 'Unpin all geometry'), + ]), ('Switching Between Tools', [ ('contours tool', 'Switch to Contours'), ('polystrips tool', 'Switch to PolyStrips'), @@ -380,6 +387,7 @@ def rebuild(): ('Pie Menus', [ ('pie menu', 'Show pie menu'), ('pie menu alt0', 'Show tool/alt pie menu'), + ('pie menu confirm', 'Confirm pie menu selection'), ]), ('Help', [ ('all help', 'Show all help'), @@ -388,3 +396,23 @@ def rebuild(): ]), ] +ignored_keys = { + 'autosave', + 'grease clear', 'grease pencil tool', + 'stretch tool', + 'toggle full area', + 'reload css', +} + +# check that all keymaps are able to be edited +def check_keymap_editor(): + flattened_details = { key for (_, keyset) in keymap_details for (key, _) in keyset } + default_keys = set(default_rf_keymaps.keys()) - ignored_keys + missing_keys = default_keys - flattened_details + extra_keys = flattened_details - default_keys + if not missing_keys and not extra_keys: return + print(f'Error detected in keymap editor') + if missing_keys: print(f'Missing Keys: {sorted(missing_keys)}\nEither add to keymap_details or ignored_keys in rf_keymapsystem.py') + if extra_keys: print(f'Extra Keys: {sorted(extra_keys)}\nRemove these from keymap_details') + assert False +check_keymap_editor() diff --git a/retopoflow/rf/rf_sources.py b/retopoflow/rf/rf_sources.py index ff814ed0c..576bfb9ff 100644 --- a/retopoflow/rf/rf_sources.py +++ b/retopoflow/rf/rf_sources.py @@ -21,7 +21,7 @@ import bpy import time -from math import isinf +from math import isinf, isnan from ...config.options import visualization, options from ...addon_common.common.maths import BBox @@ -102,18 +102,24 @@ def get_rfsource_snap(self, rfsource): # ray casting functions def raycast_sources_Ray(self, ray:Ray): - bp,bn,bi,bd = None,None,None,None + bp,bn,bi,bd,bo = None,None,None,None,None for rfsource in self.rfsources: if not self.get_rfsource_snap(rfsource): continue hp,hn,hi,hd = rfsource.raycast(ray) - if hp is None: continue - if isinf(hd): continue - if bp and bd < hd: continue - bp,bn,bi,bd = hp,hn,hi,hd + if hp is None: continue # did we miss? + if isinf(hd): continue # is distance infinitely far away? + if isnan(hd): continue # is distance NaN? (issue #1062) + if bp and bd < hd: continue # have we seen a closer hit already? + bp,bn,bi,bd,bo = hp,hn,hi,hd,rfsource return (bp,bn,bi,bd) def raycast_sources_Ray_all(self, ray:Ray): - return [hit for rfsource in self.rfsources for hit in rfsource.raycast_all(ray) if self.get_rfsource_snap(rfsource)] + return [ + hit + for rfsource in self.rfsources + for hit in rfsource.raycast_all(ray) + if self.get_rfsource_snap(rfsource) + ] def raycast_sources_Point2D(self, xy:Point2D): if xy is None: return None,None,None,None diff --git a/retopoflow/rf/rf_spaces.py b/retopoflow/rf/rf_spaces.py index 6390eb101..87aab82dd 100644 --- a/retopoflow/rf/rf_spaces.py +++ b/retopoflow/rf/rf_spaces.py @@ -118,8 +118,8 @@ def Point_to_Ray(self, xyz:Point, min_dist=0, max_dist_offset=0): def size2D_to_size(self, size2D:float, xy:Point2D, depth:float): # computes size of 3D object at distance (depth) as it projects to 2D size # TODO: there are more efficient methods of computing this! - # note: scaling then unscaling by inverse of near clip to account for numerical instability with small clip_start values - scale = 1.0 / self.actions.space.clip_start + # note: scaling then unscaling helps with numerical instability when clip_start is small + scale = 1000.0 # 1.0 / self.actions.space.clip_start p3d0 = self.Point2D_to_Point(xy, depth) p3d1 = self.Point2D_to_Point(xy + Vec2D((scale * size2D, 0)), depth) return (p3d0 - p3d1).length / scale diff --git a/retopoflow/rf/rf_states.py b/retopoflow/rf/rf_states.py index 318e7a731..f455f2bde 100644 --- a/retopoflow/rf/rf_states.py +++ b/retopoflow/rf/rf_states.py @@ -50,7 +50,24 @@ def update(self, timer=True): self.fsm.update() return - options.clean() + options.clean(raise_exception=False) + if options.write_error and not hasattr(self, '_write_error_reported'): + # could not write options to file for some reason + # issue #1070 + self._write_error_reported = True + self.alert_user( + '\n'.join([ + f'Could not write options to file (incorrect permissions).', + f'', + f'Check that you have permission to write to `{options.options_filename}` to the RetopoFlow add-on folder.', + f'', + f'Or, try: uninstall RetopoFlow from Blender, restart Blender, then install the latest version of RetopoFlow from the Blender Market.', + f'', + f'Note: You can continue using RetopoFlow, but any changes to options will not be saved.', + f'This error will not be reported again during the current RetopoFlow session.' + ]), + level='error', + ) if timer: self.rftool._callback('timer') @@ -74,6 +91,7 @@ def update(self, timer=True): view_version = self.get_view_version() if self.view_version != view_version: + self.update_clip_settings(rescale=False) self.view_version = view_version self.rftool._callback('view change') if self.rftool.rfwidget: @@ -83,6 +101,35 @@ def update(self, timer=True): fpsdiv = self.document.body.getElementById('fpsdiv') if fpsdiv: fpsdiv.innerText = 'UI FPS: %.2f' % self.document._draw_fps + def update_clip_settings(self, *, rescale=True): + if options['clip auto adjust']: + # adjust clipping settings + view_origin = self.drawing.get_view_origin(orthographic_distance=1000) + view_focus = self.actions.r3d.view_location + bbox = self.sources_bbox + closest = bbox.closest_Point(view_origin) + farthest = bbox.farthest_Point(view_origin) + self.drawing.space.clip_start = max( + 0.0001, + (view_origin - closest).length * 0.001, + ) + self.drawing.space.clip_end = (view_origin - farthest).length * 100 + # print(f'clip auto adjusting') + # print(f' origin: {view_origin}') + # print(f' focus: {view_focus}') + # print(f' closest: {closest}') + # print(f' farthest: {farthest}') + # print(f' dist from origin to closest: {(view_origin - closest).length}') + # print(f' dist from origin to farthest: {(view_origin - farthest).length}') + # print(f' dist from origin to focus: {(view_origin - view_focus).length}') + elif rescale: + self.unscale_from_unit_box() + self.scale_to_unit_box( + clip_override=options['clip override'], + clip_start=options['clip start override'], + clip_end=options['clip end override'], + ) + def which_pie_menu_section(self): delta = self.actions.mouse - self.pie_menu_center @@ -169,22 +216,24 @@ def pie_menu_wait(self): if self.actions.released(self.pie_menu_release, ignoremods=True): return 'main' + def should_pass_through(self, context, event): + return self.actions.using('blender passthrough') @FSM.on_state('main') def modal_main(self): # if self.actions.just_pressed: print('modal_main', self.actions.just_pressed) if self.rftool._fsm.state == 'main' and (not self.rftool.rfwidget or self.rftool.rfwidget._fsm.state == 'main'): - if self.actions.pressed({'done'}): + # exit + if self.actions.pressed('done'): if options['confirm tab quit']: self.show_quit_dialog() else: self.done() return - if options['escape to quit'] and self.actions.pressed({'done alt0'}): + if options['escape to quit'] and self.actions.pressed('done alt0'): self.done() return - # handle help actions if self.actions.pressed('all help'): self.helpsystem_open('table_of_contents.md') @@ -230,18 +279,25 @@ def modal_main(self): ], self.select_rftool, highlighted=self.rftool) return + # debugging # if self.actions.pressed('SHIFT+F5'): breakit = 42 / 0 # if self.actions.pressed('SHIFT+F6'): assert False # if self.actions.pressed('SHIFT+F7'): self.alert_user(message='Foo', level='exception', msghash='2ec5e386ae05c1abeb66dce8e1f1cb95') + # if self.actions.pressed('F7'): + # assert False, 'test exception throwing' + # # self.alert_user(title='Test', message='foo bar', level='warning', msghash=None) + # return - if self.actions.pressed('SHIFT+F10'): - profiler.clear() - return - if self.actions.pressed('SHIFT+F11'): - profiler.printout() - self.document.debug_print() - return - if self.actions.pressed('F12'): + # profiler + # if self.actions.pressed('SHIFT+F10'): + # profiler.clear() + # return + # if self.actions.pressed('SHIFT+F11'): + # profiler.printout() + # self.document.debug_print() + # return + + if self.actions.pressed('reload css'): print('RetopoFlow: Reloading stylings') self.reload_stylings() return @@ -255,12 +311,7 @@ def modal_main(self): self.select_rftool(rftool, quick=True) return 'quick switch' - # if self.actions.pressed('F7'): - # assert False, 'test exception throwing' - # # self.alert_user(title='Test', message='foo bar', level='warning', msghash=None) - # return - - # handle undo/redo + # undo/redo if self.actions.pressed('blender undo'): self.undo_pop() if self.rftool: self.rftool._reset() @@ -270,40 +321,38 @@ def modal_main(self): if self.rftool: self.rftool._reset() return - # handle selection # if self.actions.just_pressed: print('modal_main', self.actions.just_pressed) + + # handle selection if self.actions.pressed('select all'): # print('modal_main:selecting all toggle') self.undo_push('select all') self.select_toggle() return - if self.actions.pressed('deselect all'): self.undo_push('deselect all') self.deselect_all() return - if self.actions.pressed('select invert'): self.undo_push('select invert') self.select_invert() return + # hide/reveal if self.actions.pressed('hide selected'): self.hide_selected() return - if self.actions.pressed('hide unselected'): self.hide_unselected() return - if self.actions.pressed('reveal hidden'): self.reveal_hidden() return + # delete if self.actions.pressed('delete'): self.show_delete_dialog() return - if self.actions.pressed('delete pie menu'): def callback(option): if not option: return @@ -320,10 +369,22 @@ def callback(option): ], callback, release='delete pie menu', always_callback=True, rotate=-60) return + # smoothing if self.actions.pressed('smooth edge flow'): self.smooth_edge_flow(iterations=options['smooth edge flow iterations']) return + # pin/unpin + if self.actions.pressed('pin'): + self.pin_selected() + return + if self.actions.pressed('unpin'): + self.unpin_selected() + return + if self.actions.pressed('unpin all'): + self.unpin_all() + return + return self.modal_main_rest() def modal_main_rest(self): diff --git a/retopoflow/rf/rf_target.py b/retopoflow/rf/rf_target.py index bc23ca30d..50fd4b087 100644 --- a/retopoflow/rf/rf_target.py +++ b/retopoflow/rf/rf_target.py @@ -75,6 +75,29 @@ def hide_target(self): self.rftarget.obj_viewport_hide() self.rftarget.obj_render_hide() + def check_target_symmetry(self): + bad = self.rftarget.check_symmetry() + if not bad: return + + message = ['\n'.join([ + f'Symmetry is enabled on the {", ".join(bad)} {"axis" if len(bad)==1 else "axes"}, but vertices were found on the "wrong" side of the symmetry {"plane" if len(bad)==1 else "planes"}.', + f'', + f'Editing these vertices will cause them to snap to the symmetry plane.', + f'(Editing vertices on the "correct" side of symmetry will work as expected)', + f'', + f'You can see these vertices by clicking Select Bad Symmetry button under Target Cleaning > Symmetry', + ])] + + self.alert_user( + title='Bad Target Symmetry', + message='\n\n'.join(message), + level='warning', + ) + + def select_bad_symmetry(self): + self.deselect_all() + self.rftarget.select_bad_symmetry() + def teardown_target(self): # IMPORTANT: changes here should also go in rf_blendersave.backup_recover() self.rftarget.obj_viewport_unhide() @@ -367,9 +390,9 @@ def iter_faces(self): ######################################## # symmetry utils - def apply_symmetry(self): - self.undo_push('applying symmetry') - self.rftarget.apply_symmetry(self.nearest_sources_Point) + def apply_mirror_symmetry(self): + self.undo_push('applying mirror symmetry') + self.rftarget.apply_mirror_symmetry(self.nearest_sources_Point) @profiler.function def clip_pointloop(self, pointloop, connected): @@ -573,6 +596,7 @@ def set2D_crawl_vert(self, vert:RFVert, xy:Point2D): def new_vert_point(self, xyz:Point): + if not xyz: return None xyz,norm,_,_ = self.nearest_sources_Point(xyz) if not xyz or not norm: return None rfvert = self.rftarget.new_vert(xyz, norm) @@ -746,6 +770,28 @@ def select_inner_edge_loop(self, edge, **kwargs): eloop,connected = self.get_inner_edge_loop(edge) self.rftarget.select(eloop, **kwargs) + def pin_selected(self): + self.undo_push('pinning selected') + self.rftarget.pin_selected() + self.dirty() + def unpin_selected(self): + self.undo_push('unpinning selected') + self.rftarget.unpin_selected() + self.dirty() + def unpin_all(self): + self.undo_push('unpinning all') + self.rftarget.unpin_all() + self.dirty() + + def mark_seam_selected(self): + self.undo_push('pinning selected') + self.rftarget.mark_seam_selected() + self.dirty() + def clear_seam_selected(self): + self.undo_push('unpinning selected') + self.rftarget.clear_seam_selected() + self.dirty() + def hide_selected(self): self.undo_push('hide selected') selected = set() diff --git a/retopoflow/rf/rf_ui.py b/retopoflow/rf/rf_ui.py index 76b6ed1e6..c1c4fec18 100644 --- a/retopoflow/rf/rf_ui.py +++ b/retopoflow/rf/rf_ui.py @@ -46,7 +46,7 @@ from ...config.options import ( options, themes, visualization, retopoflow_issues_url, retopoflow_tip_url, - retopoflow_version, retopoflow_version_git, retopoflow_cgcookie_built, + retopoflow_version, retopoflow_version_git, retopoflow_cgcookie_built, # these are needed for UI build_platform, platform_system, platform_node, platform_release, platform_version, platform_machine, platform_processor, ) diff --git a/retopoflow/rf/rf_ui_alert.py b/retopoflow/rf/rf_ui_alert.py index 621a5f777..8135398cb 100644 --- a/retopoflow/rf/rf_ui_alert.py +++ b/retopoflow/rf/rf_ui_alert.py @@ -46,7 +46,7 @@ from ...config.options import ( options, themes, visualization, retopoflow_issues_url, retopoflow_tip_url, - retopoflow_version, retopoflow_version_git, retopoflow_cgcookie_built, + retopoflow_version, retopoflow_version_git, retopoflow_cgcookie_built, retopoflow_github, retopoflow_blendermarket, build_platform, platform_system, platform_node, platform_release, platform_version, platform_machine, platform_processor, gpu_info, @@ -62,8 +62,15 @@ def get_environment_details(): env_details += [f'- RetopoFlow: {retopoflow_version}'] if retopoflow_version_git: env_details += [f'- RF git: {retopoflow_version_git}'] - if retopoflow_cgcookie_built: - env_details += ['- CG Cookie built'] + elif retopoflow_cgcookie_built: + if retopoflow_github: + env_details += ['- CG Cookie built for GitHub'] + elif retopoflow_blendermarket: + env_details += ['- CG Cookie built for Blender Market'] + else: + env_details += ['- CG Cookie built for ??'] + else: + env_details += ['- Self built'] env_details += [f'- Blender: {blender_version} {blender_branch} {blender_date}'] env_details += [f'- Platform: {platform_system}, {platform_release}, {platform_version}, {platform_machine}, {platform_processor}'] env_details += [f'- GPU: {gpu_info}'] diff --git a/retopoflow/rfmesh/rfmesh.py b/retopoflow/rfmesh/rfmesh.py index 28f52a480..5419dedf7 100644 --- a/retopoflow/rfmesh/rfmesh.py +++ b/retopoflow/rfmesh/rfmesh.py @@ -22,6 +22,7 @@ import math import copy import heapq +import random from dataclasses import dataclass, field import bpy @@ -141,6 +142,9 @@ def __setup__( # print('RFMesh.__setup__: triangulating') self.triangulate() + for bmv in self.bme.verts: + bmv.normal_update() + # setup finishing self.selection_center = Point((0, 0, 0)) self.store_state() @@ -242,22 +246,10 @@ def restore_state(self): def get_obj_name(self): return self.obj.name - @blender_version_wrapper('<', '2.80') - def obj_viewport_hide_get(self): return self.obj.hide - @blender_version_wrapper('>=', '2.80') def obj_viewport_hide_get(self): return self.obj.hide_viewport - @blender_version_wrapper('<', '2.80') - def obj_viewport_hide_set(self, v): self.obj.hide = v - @blender_version_wrapper('>=', '2.80') def obj_viewport_hide_set(self, v): self.obj.hide_viewport = v - @blender_version_wrapper('<','2.80') - def obj_select_get(self): return self.obj.select - @blender_version_wrapper('>=','2.80') def obj_select_get(self): return self.obj.select_get() - @blender_version_wrapper('<','2.80') - def obj_select_set(self, v): self.obj.select = v - @blender_version_wrapper('>=','2.80') def obj_select_set(self, v): self.obj.select_set(v) def obj_render_hide_get(self): return self.obj.hide_render @@ -664,11 +656,12 @@ def _unwrap(self, elem): def raycast(self, ray:Ray): ray_local = self.xform.w2l_ray(ray) p,n,i,d = self.get_bvh().ray_cast(ray_local.o, ray_local.d, ray_local.max) - if p is None: return (None,None,None,None) + if p is None: return (None, None, None, None) #if not self.get_bbox().Point_within(p, margin=1): # return (None,None,None,None) p_w,n_w = self.xform.l2w_point(p), self.xform.l2w_normal(n) d_w = (ray.o - p_w).length + if math.isinf(d_w) or math.isnan(d_w): return (None, None, None, None) return (p_w,n_w,i,d_w) def raycast_all(self, ray:Ray): @@ -1369,6 +1362,10 @@ def __setup__(self, obj:bpy.types.Object): def __str__(self): return '' % self.obj.name + @property + def layer_pin(self): + return None + class RFTarget(RFMesh): @@ -1415,6 +1412,11 @@ def __setup__(self, obj:bpy.types.Object, unit_scaling_factor:float, rftarget_co self.yz_symmetry_accel = yz_symmetry_accel self.unit_scaling_factor = unit_scaling_factor + @property + def layer_pin(self): + il = self.bme.verts.layers.int + return il['pin'] if 'pin' in il else il.new('pin') + def setup_mirror(self): self.mirror_mod = ModifierWrapper_Mirror.get_from_object(self.obj) if not self.mirror_mod: @@ -1445,11 +1447,26 @@ def get_point_symmetry(self, point, from_world=True): px,py,pz = point threshold = self.mirror_mod.symmetry_threshold * self.unit_scaling_factor / 2.0 symmetry = set() - if self.mirror_mod.x and px <= threshold: symmetry.add('x') + if self.mirror_mod.x and px <= threshold: symmetry.add('x') if self.mirror_mod.y and -py <= threshold: symmetry.add('y') - if self.mirror_mod.z and pz <= threshold: symmetry.add('z') + if self.mirror_mod.z and pz <= threshold: symmetry.add('z') return symmetry + def check_symmetry(self): + threshold = self.mirror_mod.symmetry_threshold * self.unit_scaling_factor / 2.0 + ret = list() + if self.mirror_mod.x and any(bmv.co.x < -threshold for bmv in self.bme.verts): ret.append('X') + if self.mirror_mod.y and any(bmv.co.y > threshold for bmv in self.bme.verts): ret.append('Y') + if self.mirror_mod.z and any(bmv.co.z < -threshold for bmv in self.bme.verts): ret.append('Z') + return ret + + def select_bad_symmetry(self): + threshold = self.mirror_mod.symmetry_threshold * self.unit_scaling_factor / 2.0 + for bmv in self.bme.verts: + if self.mirror_mod.x and bmv.co.x < -threshold: bmv.select = True + if self.mirror_mod.y and bmv.co.y > threshold: bmv.select = True + if self.mirror_mod.z and bmv.co.z < -threshold: bmv.select = True + def snap_to_symmetry(self, point, symmetry, from_world=True, to_world=True): if not symmetry and from_world == to_world: return point if from_world: point = self.xform.w2l_point(point) @@ -1555,7 +1572,7 @@ def enable_symmetry(self, axis): self.mirror_mod.enable_axis(axis) def disable_symmetry(self, axis): self.mirror_mod.disable_axis(axis) def has_symmetry(self, axis): return self.mirror_mod.is_enabled_axis(axis) - def apply_symmetry(self, nearest): + def apply_mirror_symmetry(self, nearest): out = [] def apply_mirror_and_return_geom(axis): return mirror( @@ -1798,6 +1815,23 @@ def snap_selected_verts(self, nearest): # def snap_unselected_verts(self, nearest): # self.snap_verts_filter(nearest, lambda v: v.unselect) + def pin_selected(self): + for v in self.get_verts(): + if v.select: v.pinned = True + def unpin_selected(self): + for v in self.get_verts(): + if v.select: v.pinned = False + def unpin_all(self): + for v in self.get_verts(): + v.pinned = False + + def mark_seam_selected(self): + for v in self.get_edges(): + if v.select: v.seam = True + def clear_seam_selected(self): + for v in self.get_edges(): + if v.select: v.seam = False + def remove_all_doubles(self, dist): bmv = [v for v in self.bme.verts if not v.hide] remove_doubles(self.bme, verts=bmv, dist=dist) diff --git a/retopoflow/rfmesh/rfmesh_render.py b/retopoflow/rfmesh/rfmesh_render.py index 122a6a9e7..ce93da951 100644 --- a/retopoflow/rfmesh/rfmesh_render.py +++ b/retopoflow/rfmesh/rfmesh_render.py @@ -26,6 +26,7 @@ import time import random +from itertools import chain from queue import Queue from concurrent.futures import ThreadPoolExecutor @@ -159,7 +160,7 @@ def dirty(self): @profiler.function def add_buffered_render(self, draw_type, data, static): batch = BufferedRender_Batch(draw_type) - batch.buffer(data['vco'], data['vno'], data['sel'], data['warn']) + batch.buffer(data['vco'], data['vno'], data['sel'], data['warn'], data['pin'], data['seam']) if static: self.buffered_renders_static.append(batch) else: self.buffered_renders_dynamic.append(batch) @@ -204,6 +205,8 @@ def _gather_data(self): mirror_y = 'y' in mirror_axes mirror_z = 'z' in mirror_axes + layer_pin = self.rfmesh.layer_pin + def gather(verts, edges, faces, static): vert_count = 100000 edge_count = 50000 @@ -233,6 +236,21 @@ def warn_edge(g): def warn_face(g): return 1.0 + def pin_vert(g): + if not layer_pin: return 0.0 + return 1.0 if g[layer_pin] else 0.0 + def pin_edge(g): + return 1.0 if all(pin_vert(v) for v in g.verts) else 0.0 + def pin_face(g): + return 1.0 if all(pin_vert(v) for v in g.verts) else 0.0 + + def seam_vert(g): + return 1.0 if any(e.seam for e in g.link_edges) else 0.0 + def seam_edge(g): + return 1.0 if g.seam else 0.0 + def seam_face(g): + return 0.0 + try: time_start = time.time() @@ -255,19 +273,29 @@ def warn_face(g): for bmv in verts ], 'vno': [ - tuple(bmf.normal) + tuple(bmv.normal) for bmf, verts in tri_faces[i0:i1] for bmv in verts ], 'sel': [ sel(bmf) for bmf, verts in tri_faces[i0:i1] - for bmv in verts + for _ in verts ], 'warn': [ warn_face(bmf) for bmf, verts in tri_faces[i0:i1] - for bmv in verts + for _ in verts + ], + 'pin': [ + pin_face(bmf) + for bmf, verts in tri_faces[i0:i1] + for _ in verts + ], + 'seam': [ + seam_face(bmf) + for bmf, verts in tri_faces[i0:i1] + for _ in verts ], 'idx': None, # list(range(len(tri_faces)*3)), } @@ -295,12 +323,22 @@ def warn_face(g): 'sel': [ sel(bme) for bme in edges[i0:i1] - for bmv in bme.verts + for _ in bme.verts ], 'warn': [ warn_edge(bme) for bme in edges[i0:i1] - for bmv in bme.verts + for _ in bme.verts + ], + 'pin': [ + pin_edge(bme) + for bme in edges[i0:i1] + for _ in bme.verts + ], + 'seam': [ + seam_edge(bme) + for bme in edges[i0:i1] + for _ in bme.verts ], 'idx': None, # list(range(len(self.bmesh.edges)*2)), } @@ -315,11 +353,13 @@ def warn_face(g): for i0 in range(0, l, vert_count): i1 = min(l, i0 + vert_count) vert_data = { - 'vco': [tuple(bmv.co) for bmv in verts[i0:i1]], - 'vno': [tuple(bmv.normal) for bmv in verts[i0:i1]], - 'sel': [sel(bmv) for bmv in verts[i0:i1]], - 'warn': [warn_vert(bmv) for bmv in verts[i0:i1]], - 'idx': None, # list(range(len(self.bmesh.verts))), + 'vco': [tuple(bmv.co) for bmv in verts[i0:i1]], + 'vno': [tuple(bmv.normal) for bmv in verts[i0:i1]], + 'sel': [sel(bmv) for bmv in verts[i0:i1]], + 'warn': [warn_vert(bmv) for bmv in verts[i0:i1]], + 'pin': [pin_vert(bmv) for bmv in verts[i0:i1]], + 'seam': [seam_vert(bmv) for bmv in verts[i0:i1]], + 'idx': None, # list(range(len(self.bmesh.verts))), } if self.async_load: self.buf_data_queue.put((BufferedRender_Batch.POINTS, vert_data, static)) @@ -336,6 +376,10 @@ def warn_face(g): print('EXCEPTION WHILE GATHERING: ' + str(e)) raise e + # self.bmesh.verts.ensure_lookup_table() + for bmelem in chain(self.bmesh.verts): # chain(self.bmesh.faces, self.bmesh.edges, self.bmesh.verts): + bmelem.normal_update() + self._is_loading = True self._is_loaded = False @@ -420,7 +464,9 @@ def draw( if not self.buffered_renders_static and not self.buffered_renders_dynamic: return try: + bgl.glEnable(bgl.GL_DEPTH_TEST) bgl.glDepthMask(bgl.GL_FALSE) # do not overwrite the depth buffer + bgl.glDepthRange(0, 1) opts = dict(self.opts) @@ -443,6 +489,8 @@ def draw( bmegl.glSetDefaultOptions() opts['no warning'] = not options['warn non-manifold'] + opts['no pinned'] = not options['show pinned'] + opts['no seam'] = not options['show seam'] opts['cull backfaces'] = cull_backfaces opts['alpha backface'] = alpha_backface @@ -460,9 +508,7 @@ def draw( opts['line mirror hidden'] = 1 - alpha_below opts['point hidden'] = 1 - alpha_below opts['point mirror hidden'] = 1 - alpha_below - for buffered_render in self.buffered_renders_static: - buffered_render.draw(opts) - for buffered_render in self.buffered_renders_dynamic: + for buffered_render in chain(self.buffered_renders_static, self.buffered_renders_dynamic): buffered_render.draw(opts) # geometry above @@ -473,9 +519,7 @@ def draw( opts['line mirror hidden'] = 1 - alpha_above opts['point hidden'] = 1 - alpha_above opts['point mirror hidden'] = 1 - alpha_above - for buffered_render in self.buffered_renders_static: - buffered_render.draw(opts) - for buffered_render in self.buffered_renders_dynamic: + for buffered_render in chain(self.buffered_renders_static, self.buffered_renders_dynamic): buffered_render.draw(opts) bgl.glDepthFunc(bgl.GL_LEQUAL) diff --git a/retopoflow/rfmesh/rfmesh_wrapper.py b/retopoflow/rfmesh/rfmesh_wrapper.py index 685327fbe..92ad62450 100644 --- a/retopoflow/rfmesh/rfmesh_wrapper.py +++ b/retopoflow/rfmesh/rfmesh_wrapper.py @@ -39,6 +39,8 @@ Vec2D, Point, Point2D, Vec, Direction, Normal, ) +from ...config.options import options + ''' BMElemWrapper wraps BMverts, BMEdges, BMFaces to automagically handle @@ -143,7 +145,7 @@ def __getattr__(self, k): class RFVert(BMElemWrapper): def __repr__(self): - return '' % repr(self.bmelem) + return f'' @staticmethod def get_link_edges(rfverts): @@ -160,7 +162,9 @@ def co(self): @co.setter def co(self, co): if any(math.isnan(v) for v in co): return - assert not any(math.isnan(v) for v in co), 'Setting RFVert.co to ' + str(co) + # assert not any(math.isnan(v) for v in co), f'Setting RFVert.co to {co}' + if options['pin enabled'] and self.pinned: return + if options['pin seam'] and self.seam: return co = self.symmetry_real(co, to_world=False) # # the following does not work well, because new verts have co=(0,0,0) # mm = BMElemWrapper.mirror_mod @@ -173,6 +177,17 @@ def co(self, co): # co = rft.snap_to_symmetry(co, mm._symmetry, to_world=False, from_world=False) self.bmelem.co = co + @property + def pinned(self): + return bool(self.bmelem[self.rftarget.layer_pin]) + @pinned.setter + def pinned(self, v): + self.bmelem[self.rftarget.layer_pin] = 1 if bool(v) else 0 + + @property + def seam(self): + return any(e.seam for e in self.bmelem.link_edges) + @property def normal(self): return self.l2w_normal(self.bmelem.normal) diff --git a/retopoflow/rftool.py b/retopoflow/rftool.py index 9f7243737..ad8f5f0d5 100644 --- a/retopoflow/rftool.py +++ b/retopoflow/rftool.py @@ -24,7 +24,7 @@ from ..addon_common.common.blender import BlenderIcon from ..addon_common.common.fsm import FSM from ..addon_common.common.functools import find_fns -from ..addon_common.common.drawing import DrawCallbacks +from ..addon_common.common.drawing import DrawCallbacks, Cursors from ..addon_common.common.boundvar import ( BoundVar, BoundBool, @@ -120,6 +120,7 @@ def __init__(self, rfcontext, start='main', reset_state=None): RFTool.drawing = rfcontext.drawing RFTool.actions = rfcontext.actions RFTool.document = rfcontext.document + self.rfwidges = {} self.rfwidget = None self._fsm = FSM(self, start=start, reset_state=reset_state) self._draw = DrawCallbacks(self) @@ -162,3 +163,18 @@ def _draw_post2d(self): self._draw.post2d() @property def icon_id(cls): return BlenderIcon.icon_id(cls.icon) + + def clear_widget(self): + self.set_widget(None) + def set_widget(self, widget): + self.rfwidget = self.rfwidgets[widget] if type(widget) is str else widget + if self.rfwidget: self.rfwidget.set_cursor() + else: Cursors.set('DEFAULT') + + def handle_inactive_passthrough(self): + for rfwidget in self.rfwidgets.values(): + if self.rfwidget == rfwidget: continue + if rfwidget.inactive_passthrough(): + self.set_widget(rfwidget) + return True + return False diff --git a/retopoflow/rftool_contours/contours.py b/retopoflow/rftool_contours/contours.py index f8aa68442..f6b7bbcd3 100644 --- a/retopoflow/rftool_contours/contours.py +++ b/retopoflow/rftool_contours/contours.py @@ -34,7 +34,7 @@ from ...addon_common.common.debug import dprint from ...addon_common.common.fsm import FSM from ...addon_common.common.blender import matrix_vector_mult -from ...addon_common.common.drawing import Drawing, Cursors, DrawCallbacks +from ...addon_common.common.drawing import Drawing, DrawCallbacks from ...addon_common.common.maths import Point, Normal, Vec2D, Plane, Vec from ...addon_common.common.profiler import profiler from ...addon_common.common.utils import iter_pairs @@ -64,8 +64,8 @@ class Contours(RFTool, Contours_Ops, Contours_Props, Contours_Utils): statusbar = '{{insert}} Insert contour\t{{increase count}} Increase segments\t{{decrease count}} Decrease segments\t{{fill}} Bridge' ui_config = 'contours_options.html' - RFWidget_Default = RFWidget_Default_Factory.create('Contours default') - RFWidget_Move = RFWidget_Default_Factory.create('Contours move', 'HAND') + RFWidget_Default = RFWidget_Default_Factory.create() + RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND') RFWidget_LineCut = RFWidget_LineCut_Factory.create('Contours line cut') @RFTool.on_init @@ -75,7 +75,7 @@ def init(self): 'cut': self.RFWidget_LineCut(self), 'hover': self.RFWidget_Move(self), } - self.rfwidget = None + self.clear_widget() @RFTool.on_reset def reset(self): @@ -170,17 +170,13 @@ def main(self): self.hovering_sel_edge,_ = self.rfcontext.accel_nearest2D_edge(max_dist=options['action dist'], selected_only=True) if self.actions.using_onlymods('insert'): - self.rfwidget = self.rfwidgets['cut'] + self.set_widget('cut') elif self.hovering_sel_edge: - self.rfwidget = self.rfwidgets['hover'] + self.set_widget('hover') else: - self.rfwidget = self.rfwidgets['default'] + self.set_widget('default') - for rfwidget in self.rfwidgets.values(): - if self.rfwidget == rfwidget: continue - if rfwidget.inactive_passthrough(): - self.rfwidget = rfwidget - return + if self.handle_inactive_passthrough(): return if self.actions.pressed('grab'): ''' grab for translations ''' @@ -452,6 +448,7 @@ def grab_enter(self): self.rfcontext.split_target_visualization(verts=[v for vs in self.move_verts for v in vs]) self.rfcontext.set_accel_defer(True) + @FSM.on_state('grab') @profiler.function def grab(self): diff --git a/retopoflow/rftool_knife/knife.py b/retopoflow/rftool_knife/knife.py index 50209ed34..9628cfc1c 100644 --- a/retopoflow/rftool_knife/knife.py +++ b/retopoflow/rftool_knife/knife.py @@ -27,6 +27,7 @@ from ..rftool import RFTool from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory +from ..rfwidgets.rfwidget_hidden import RFWidget_Hidden_Factory from ..rfmesh.rfmesh_wrapper import RFVert, RFEdge, RFFace from ...addon_common.common.drawing import ( @@ -59,9 +60,10 @@ class Knife(RFTool): statusbar = '{{insert}} Insert' ui_config = 'knife_options.html' - RFWidget_Default = RFWidget_Default_Factory.create('Knife default') - RFWidget_Knife = RFWidget_Default_Factory.create('Knife knife', 'KNIFE') - RFWidget_Move = RFWidget_Default_Factory.create('Knife move', 'HAND') + RFWidget_Default = RFWidget_Default_Factory.create() + RFWidget_Knife = RFWidget_Default_Factory.create(cursor='KNIFE') + RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND') + RFWidget_Hidden = RFWidget_Hidden_Factory.create() @RFTool.on_init def init(self): @@ -69,6 +71,7 @@ def init(self): 'default': self.RFWidget_Default(self), 'knife': self.RFWidget_Knife(self), 'hover': self.RFWidget_Move(self), + 'hidden': self.RFWidget_Hidden(self), } self.rfwidget = None self.first_time = True @@ -116,11 +119,20 @@ def set_next_state(self, force=False): self.nearest_face,_ = self.rfcontext.accel_nearest2D_face(max_dist=options['action dist']) self.nearest_geom = self.nearest_vert or self.nearest_edge or self.nearest_face + def ensure_all_valid(self): + self.sel_verts = [v for v in self.sel_verts if v.is_valid] + self.sel_edges = [e for e in self.sel_edges if e.is_valid] + self.sel_faces = [f for f in self.sel_faces if f.is_valid] + + self.vis_verts = [v for v in self.vis_verts if v.is_valid] + self.vis_edges = [e for e in self.vis_edges if e.is_valid] + self.vis_faces = [f for f in self.vis_faces if f.is_valid] + @FSM.on_state('quick', 'enter') def quick_enter(self): self.quick_knife = True - self.rfwidget = self.rfwidgets['knife'] + self.set_widget('knife') @FSM.on_state('quick') def quick_main(self): @@ -165,17 +177,13 @@ def main(self): self.previs_timer.enable(self.actions.using_onlymods('insert')) if self.actions.using_onlymods('insert'): - self.rfwidget = self.rfwidgets['knife'] + self.set_widget('knife') elif self.nearest_geom and self.nearest_geom.select: - self.rfwidget = self.rfwidgets['hover'] + self.set_widget('hover') else: - self.rfwidget = self.rfwidgets['default'] + self.set_widget('default') - for rfwidget in self.rfwidgets.values(): - if self.rfwidget == rfwidget: continue - if rfwidget.inactive_passthrough(): - self.rfwidget = rfwidget - return + if self.handle_inactive_passthrough(): return if self.actions.pressed('insert'): return 'insert' @@ -485,8 +493,12 @@ def move_enter(self): self.rfcontext.split_target_visualization_selected() self.rfcontext.set_accel_defer(True) + if options['hide cursor on tweak']: self.set_widget('hidden') + + # filter out any deleted bmverts (issue #1075) or bmverts that are not on screen + self.bmverts = [(bmv, xy) for (bmv, xy) in self.bmverts if bmv and bmv.is_valid and xy] + @FSM.on_state('move') - @profiler.function def modal_move(self): if self.move_done_pressed and self.actions.pressed(self.move_done_pressed): self.defer_recomputing = False @@ -612,9 +624,12 @@ def draw_lines(self, coords, poly_alpha=0.2): def draw_postpixel(self): # TODO: put all logic into set_next_state(), such as vertex snapping, edge splitting, etc. + # make sure that all our data structs contain valid data (hasn't been deleted) + self.ensure_all_valid() + #if self.rfcontext.nav or self.mode != 'main': return if self._fsm.state != 'quick': - if not self.actions.using_onlymods('insert'): return #'insert alt1'?? + if not self.actions.using_onlymods('insert'): return hit_pos = self.actions.hit_pos if self.knife_start is None and len(self.sel_verts) == 0: diff --git a/retopoflow/rftool_loops/loops.py b/retopoflow/rftool_loops/loops.py index 9363c7c16..c53892b6b 100644 --- a/retopoflow/rftool_loops/loops.py +++ b/retopoflow/rftool_loops/loops.py @@ -28,6 +28,7 @@ from ..rftool import RFTool from ..rfmesh.rfmesh import RFVert, RFEdge, RFFace from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory +from ..rfwidgets.rfwidget_hidden import RFWidget_Hidden_Factory from ...addon_common.common.maths import ( Point, Vec, Normal, Direction, @@ -55,9 +56,10 @@ class Loops(RFTool): quick_shortcut = 'loops quick' statusbar = '{{insert}} Insert edge loop\t{{smooth edge flow}} Smooth edge flow' - RFWidget_Default = RFWidget_Default_Factory.create('Loops default') - RFWidget_Move = RFWidget_Default_Factory.create('Loops move', 'HAND') - RFWidget_Crosshair = RFWidget_Default_Factory.create('Loops crosshair', 'CROSSHAIR') + RFWidget_Default = RFWidget_Default_Factory.create() + RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND') + RFWidget_Crosshair = RFWidget_Default_Factory.create(cursor='CROSSHAIR') + RFWidget_Hidden = RFWidget_Hidden_Factory.create() @RFTool.on_init def init(self): @@ -65,6 +67,7 @@ def init(self): 'default': self.RFWidget_Default(self), 'cut': self.RFWidget_Crosshair(self), 'hover': self.RFWidget_Move(self), + 'hidden': self.RFWidget_Hidden(self), } self.rfwidget = None self.previs_timer = self.actions.start_timer(120.0, enabled=False) @@ -116,7 +119,7 @@ def filter_edge_selection(self, bme, no_verts_select=True, ratio=0.33): @FSM.on_state('quick', 'enter') def quick_enter(self): self.hovering_sel_edge = None - self.rfwidget = self.rfwidgets['cut'] + self.set_widget('cut') @FSM.on_state('quick') def quick_main(self): @@ -145,17 +148,13 @@ def main(self): self.previs_timer.enable(self.actions.using_onlymods('insert')) if self.actions.using_onlymods('insert'): - self.rfwidget = self.rfwidgets['cut'] + self.set_widget('cut') elif self.hovering_edge: - self.rfwidget = self.rfwidgets['hover'] + self.set_widget('hover') else: - self.rfwidget = self.rfwidgets['default'] + self.set_widget('default') - for rfwidget in self.rfwidgets.values(): - if self.rfwidget == rfwidget: continue - if rfwidget.inactive_passthrough(): - self.rfwidget = rfwidget - return + if self.handle_inactive_passthrough(): return if self.hovering_edge: #print(f'hovering edge {self.actions.using("action")} {self.hovering_edge} {self.hovering_sel_edge}') @@ -276,7 +275,7 @@ def compute_percent(): self.rfcontext.undo_cancel() return self.move_done_pressed = None - self.move_done_released = ['insert', 'insert alt0'] + self.move_done_released = 'insert' self.move_cancelled = 'cancel' self.rfcontext.undo_push('slide edge loop/strip') return 'slide' @@ -504,6 +503,7 @@ def fn(bmv, side): def slide_enter(self): self.previs_timer.start() self.rfcontext.set_accel_defer(True) + self.set_widget('hidden' if options['hide cursor on tweak'] else 'hover') tag_redraw_all('entering slide') @FSM.on_state('slide') @@ -518,8 +518,6 @@ def slide(self): self.rfcontext.undo_cancel() return 'main' - self.rfwidget = self.rfwidgets['hover'] - if not self.actions.mousemove_stop: return # # only update loop on timer events and when mouse has moved # if not self.rfcontext.actions.timer: return diff --git a/retopoflow/rftool_patches/patches.py b/retopoflow/rftool_patches/patches.py index a72fe367b..309ba5ae7 100644 --- a/retopoflow/rftool_patches/patches.py +++ b/retopoflow/rftool_patches/patches.py @@ -27,6 +27,7 @@ from ..rftool import RFTool from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory +from ..rfwidgets.rfwidget_hidden import RFWidget_Hidden_Factory from ...addon_common.common.drawing import ( CC_DRAW, @@ -58,14 +59,16 @@ class Patches(RFTool): statusbar = '{{action alt1}} Toggle vertex as a corner\t{{increase count}} Increase segments\t{{decrease count}} Decrease Segments\t{{fill}} Create patch' ui_config = 'patches_options.html' - RFWidget_Default = RFWidget_Default_Factory.create('Patches default') - RFWidget_Move = RFWidget_Default_Factory.create('Patches move', 'HAND') + RFWidget_Default = RFWidget_Default_Factory.create() + RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND') + RFWidget_Hidden = RFWidget_Hidden_Factory.create() @RFTool.on_init def init(self): self.rfwidgets = { 'default': self.RFWidget_Default(self), - 'hover': self.RFWidget_Move(self), + 'hover': self.RFWidget_Move(self), + 'hidden': self.RFWidget_Hidden(self), } self.rfwidget = None self.corners = {} @@ -133,15 +136,11 @@ def main(self): self.hovering_sel_face,_ = self.rfcontext.accel_nearest2D_face(max_dist=options['action dist'], selected_only=True) if self.hovering_sel_edge or self.hovering_sel_face: - self.rfwidget = self.rfwidgets['hover'] + self.set_widget('hover') else: - self.rfwidget = self.rfwidgets['default'] + self.set_widget('default') - for rfwidget in self.rfwidgets.values(): - if self.rfwidget == rfwidget: continue - if rfwidget.inactive_passthrough(): - self.rfwidget = rfwidget - return + if self.handle_inactive_passthrough(): return if self.hovering_sel_edge or self.hovering_sel_face: if self.actions.pressed('action'): @@ -241,6 +240,8 @@ def move_enter(self): self._timer = self.actions.start_timer(120) + if options['hide cursor on tweak']: self.set_widget('hidden') + @FSM.on_state('move') def move_main(self): released = self.rfcontext.actions.released diff --git a/retopoflow/rftool_polypen/polypen.py b/retopoflow/rftool_polypen/polypen.py index af5f2ecc1..1cb7df0b8 100644 --- a/retopoflow/rftool_polypen/polypen.py +++ b/retopoflow/rftool_polypen/polypen.py @@ -26,6 +26,7 @@ from ..rftool import RFTool from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory +from ..rfwidgets.rfwidget_hidden import RFWidget_Hidden_Factory from ..rfmesh.rfmesh_wrapper import RFVert, RFEdge, RFFace from ...addon_common.common.drawing import ( @@ -57,10 +58,11 @@ class PolyPen(RFTool): statusbar = '{{insert}} Insert' ui_config = 'polypen_options.html' - RFWidget_Default = RFWidget_Default_Factory.create('PolyPen default') - RFWidget_Crosshair = RFWidget_Default_Factory.create('PolyPen crosshair', 'CROSSHAIR') - RFWidget_Move = RFWidget_Default_Factory.create('PolyPen move', 'HAND') - RFWidget_Knife = RFWidget_Default_Factory.create('PolyPen knife', 'KNIFE') + RFWidget_Default = RFWidget_Default_Factory.create() + RFWidget_Crosshair = RFWidget_Default_Factory.create(cursor='CROSSHAIR') + RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND') + RFWidget_Knife = RFWidget_Default_Factory.create(cursor='KNIFE') + RFWidget_Hidden = RFWidget_Hidden_Factory.create() @RFTool.on_init def init(self): @@ -69,6 +71,7 @@ def init(self): 'insert': self.RFWidget_Crosshair(self), 'hover': self.RFWidget_Move(self), 'knife': self.RFWidget_Knife(self), + 'hidden': self.RFWidget_Hidden(self), } self.rfwidget = None self.update_state_info() @@ -223,19 +226,15 @@ def main(self): self.previs_timer.enable(self.actions.using_onlymods('insert')) if self.actions.using_onlymods('insert'): if self.next_state == 'knife selected edge': - self.rfwidget = self.rfwidgets['knife'] + self.set_widget('knife') else: - self.rfwidget = self.rfwidgets['insert'] + self.set_widget('insert') elif self.nearest_geom and self.nearest_geom.select: - self.rfwidget = self.rfwidgets['hover'] + self.set_widget('hover') else: - self.rfwidget = self.rfwidgets['default'] + self.set_widget('default') - for rfwidget in self.rfwidgets.values(): - if self.rfwidget == rfwidget: continue - if rfwidget.inactive_passthrough(): - self.rfwidget = rfwidget - return + if self.handle_inactive_passthrough(): return if self.actions.pressed('pie menu alt0'): def callback(option): @@ -570,7 +569,7 @@ def _insert(self): self.rfcontext.select(bmv1, only=False) xy = self.rfcontext.Point_to_Point2D(bmv1.co) if not xy: - dprint('Could not insert: ' + str(bmv3.co)) + dprint('Could not insert: ' + str(bmv1.co)) self.rfcontext.undo_cancel() return 'main' self.bmverts = [(bmv1, xy)] if bmv1 else [] @@ -657,6 +656,8 @@ def move_enter(self): self.previs_timer.start() self.rfcontext.set_accel_defer(True) + if options['hide cursor on tweak']: self.set_widget('hidden') + @FSM.on_state('move') @profiler.function def modal_move(self): @@ -746,7 +747,7 @@ def draw_postpixel(self): # TODO: put all logic into set_next_state(), such as vertex snapping, edge splitting, etc. #if self.rfcontext.nav or self.mode != 'main': return - if not self.actions.using_onlymods('insert'): return # 'insert alt1'?? + if not self.actions.using_onlymods('insert'): return hit_pos = self.actions.hit_pos if not hit_pos: return diff --git a/retopoflow/rftool_polystrips/polystrips.py b/retopoflow/rftool_polystrips/polystrips.py index 4e63578f9..769b2ce4c 100644 --- a/retopoflow/rftool_polystrips/polystrips.py +++ b/retopoflow/rftool_polystrips/polystrips.py @@ -59,7 +59,8 @@ from ...config.options import options, themes from ..rfwidgets.rfwidget_brushstroke import RFWidget_BrushStroke_Factory -from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory +from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory +from ..rfwidgets.rfwidget_hidden import RFWidget_Hidden_Factory from ...addon_common.common.boundvar import BoundBool, BoundInt, BoundFloat, BoundString @@ -73,13 +74,14 @@ class PolyStrips(RFTool, PolyStrips_Props, PolyStrips_Ops): statusbar = '{{insert}} Insert strip of quads\t{{brush radius}} Brush size\t{{action}} Grab selection\t{{increase count}} Increase segments\t{{decrease count}} Decrease segments' ui_config = 'polystrips_options.html' - RFWidget_Default = RFWidget_Default_Factory.create('PolyStrips default') + RFWidget_Default = RFWidget_Default_Factory.create() + RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND') + RFWidget_Hidden = RFWidget_Hidden_Factory.create() RFWidget_BrushStroke = RFWidget_BrushStroke_Factory.create( 'PolyStrips stroke', BoundInt('''options['polystrips radius']''', min_value=1), outer_border_color=themes['polystrips'] ) - RFWidget_Move = RFWidget_Default_Factory.create('PolyStrips move', 'HAND') @RFTool.on_init def init(self): @@ -87,6 +89,7 @@ def init(self): 'default': self.RFWidget_Default(self), 'brushstroke': self.RFWidget_BrushStroke(self), 'move': self.RFWidget_Move(self), + 'hidden': self.RFWidget_Hidden(self), } self.rfwidget = None @@ -199,19 +202,15 @@ def main(self): self.hovering_strips.add(strip) if self.actions.using_onlymods('insert'): - self.rfwidget = self.rfwidgets['brushstroke'] + self.set_widget('brushstroke') elif self.hovering_handles: - self.rfwidget = self.rfwidgets['move'] + self.set_widget('move') elif self.hovering_sel_face: - self.rfwidget = self.rfwidgets['move'] + self.set_widget('move') else: - self.rfwidget = self.rfwidgets['default'] + self.set_widget('default') - for rfwidget in self.rfwidgets.values(): - if self.rfwidget == rfwidget: continue - if rfwidget.inactive_passthrough(): - self.rfwidget = rfwidget - return + if self.handle_inactive_passthrough(): return # handle edits if self.hovering_handles: @@ -292,13 +291,13 @@ def movehandle_enter(self): self.sel_cbpts = [(cbpt, cbpt in inners, Point(cbpt), self.rfcontext.Point_to_Point2D(cbpt)) for cbpt in cbpts] self.mousedown = self.actions.mouse - self.rfwidget = self.rfwidgets['move'] self.move_done_pressed = 'confirm' self.move_done_released = 'action' self.move_cancelled = 'cancel' self.rfcontext.undo_push('manipulate bezier') self._timer = self.actions.start_timer(120.0) self.rfcontext.set_accel_defer(True) + self.set_widget('hidden' if options['hide cursor on tweak'] else 'move') @FSM.on_state('move handle') def movehandle(self): @@ -371,13 +370,13 @@ def rotate_enter(self): for strip in self.mod_strips: strip.capture_edges() self.mousedown = self.actions.mouse - self.rfwidget = self.rfwidgets['move'] self.move_done_pressed = 'confirm' self.move_done_released = 'action alt0' self.move_cancelled = 'cancel' self.rfcontext.undo_push('rotate') self._timer = self.actions.start_timer(120.0) self.rfcontext.set_accel_defer(True) + self.set_widget('hidden' if options['hide cursor on tweak'] else 'move') @FSM.on_state('rotate') @profiler.function @@ -453,7 +452,6 @@ def scale_canenter(self): @FSM.on_state('scale', 'enter') def scale_enter(self): self.mousedown = self.actions.mouse - self.rfwidget = None #self.rfwidgets['default'] self.rfcontext.undo_push('scale') self.move_done_pressed = None self.move_done_released = 'action' @@ -483,6 +481,8 @@ def scale_enter(self): self._timer = self.actions.start_timer(120.0) self.rfcontext.set_accel_defer(True) + self.set_widget('hidden' if options['hide cursor on tweak'] else 'default') # None + @FSM.on_state('scale') @profiler.function def scale(self): @@ -531,7 +531,6 @@ def moveall_canenter(self): def moveall_enter(self): lmb_drag = self.actions.using('action') self.actions.unpress() - self.rfwidget = None # self.rfwidgets['default'] self.rfcontext.undo_push('move grabbed') self.moveall_opts = { 'mousedown': self.actions.mouse, @@ -542,6 +541,7 @@ def moveall_enter(self): } self.rfcontext.split_target_visualization_selected() self.rfcontext.set_accel_defer(True) + self.set_widget('hidden' if options['hide cursor on tweak'] else 'default') # None @FSM.on_state('move all') @profiler.function diff --git a/retopoflow/rftool_polystrips/polystrips_ops.py b/retopoflow/rftool_polystrips/polystrips_ops.py index 1c28e8bcb..6b941314b 100644 --- a/retopoflow/rftool_polystrips/polystrips_ops.py +++ b/retopoflow/rftool_polystrips/polystrips_ops.py @@ -73,11 +73,11 @@ def intersect_face(pt): def snap_point(p2D_init, dist): p = raycast(p2D_init)[0] - if p is None: - # did not hit source, so find nearest point on source to where the point would have been - r = Point2D_to_Ray(p2D_init) - p = nearest_sources_Point(r.eval(dist))[0] - return p + if p: return p + # did not hit source, so find nearest point on source to where the point would have been + r = Point2D_to_Ray(p2D_init) + p = r.eval(dist) + return nearest_sources_Point(p)[0] def create_edge(center, tangent, mult, perpendicular): nonlocal new_geom diff --git a/retopoflow/rftool_relax/relax.py b/retopoflow/rftool_relax/relax.py index 03084c88a..48e028610 100644 --- a/retopoflow/rftool_relax/relax.py +++ b/retopoflow/rftool_relax/relax.py @@ -50,7 +50,7 @@ class Relax(RFTool): statusbar = '{{brush}} Relax\t{{brush alt}} Relax selection\t{{brush radius}} Brush size\t{{brush strength}} Brush strength\t{{brush falloff}} Brush falloff' ui_config = 'relax_options.html' - RFWidget_Default = RFWidget_Default_Factory.create('Relax default') + RFWidget_Default = RFWidget_Default_Factory.create() RFWidget_BrushFalloff = RFWidget_BrushFalloff_Factory.create( 'Relax brush', BoundInt('''options['relax radius']''', min_value=1), @@ -130,9 +130,9 @@ def reset(self): @FSM.on_state('main') def main(self): if self.actions.using_onlymods(['brush', 'brush alt', 'brush radius', 'brush falloff', 'brush strength']): - self.rfwidget = self.rfwidgets['brushstroke'] + self.set_widget('brushstroke') else: - self.rfwidget = self.rfwidgets['default'] + self.set_widget('default') if self.rfcontext.actions.pressed(['brush', 'brush alt'], unpress=False): self.sel_only = self.rfcontext.actions.using('brush alt') diff --git a/retopoflow/rftool_strokes/strokes.py b/retopoflow/rftool_strokes/strokes.py index ab00f2eeb..8292adf3a 100644 --- a/retopoflow/rftool_strokes/strokes.py +++ b/retopoflow/rftool_strokes/strokes.py @@ -32,8 +32,9 @@ from ..rftool import RFTool from ..rfwidget import RFWidget -from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory +from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory from ..rfwidgets.rfwidget_brushstroke import RFWidget_BrushStroke_Factory +from ..rfwidgets.rfwidget_hidden import RFWidget_Hidden_Factory from ...addon_common.common.debug import dprint @@ -69,13 +70,14 @@ class Strokes(RFTool): statusbar = '{{insert}} Insert edge strip and bridge\t{{increase count}} Increase segments\t{{decrease count}} Decrease segments' ui_config = 'strokes_options.html' - RFWidget_Default = RFWidget_Default_Factory.create('Strokes default') + RFWidget_Default = RFWidget_Default_Factory.create() + RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND') + RFWidget_Hidden = RFWidget_Hidden_Factory.create() RFWidget_BrushStroke = RFWidget_BrushStroke_Factory.create( 'Strokes stroke', BoundInt('''options['strokes radius']''', min_value=1), outer_border_color=themes['strokes'], ) - RFWidget_Move = RFWidget_Default_Factory.create('Strokes move', 'HAND') @property def cross_count(self): @@ -103,8 +105,9 @@ def loop_count(self, v): def init(self): self.rfwidgets = { 'default': self.RFWidget_Default(self), - 'brush': self.RFWidget_BrushStroke(self), + 'brush': self.RFWidget_BrushStroke(self), 'hover': self.RFWidget_Move(self), + 'hidden': self.RFWidget_Hidden(self), } self.rfwidget = None self.strip_crosses = None @@ -170,7 +173,6 @@ def update_target(self): @RFTool.on_target_change @RFTool.on_view_change - @profiler.function def update(self): if self.defer_recomputing: return @@ -198,7 +200,6 @@ def filter_edge_selection(self, bme): return bme.select or len(bme.link_faces) < 2 @FSM.on_state('main') - @profiler.function def modal_main(self): if not self.actions.using('action', ignoredrag=True): # only update while not pressing action, because action includes drag, and @@ -223,17 +224,13 @@ def modal_main(self): self.connection_pre = None if self.actions.using_onlymods('insert'): - self.rfwidget = self.rfwidgets['brush'] + self.set_widget('brush') elif self.hovering_sel_edge: - self.rfwidget = self.rfwidgets['hover'] + self.set_widget('hover') else: - self.rfwidget = self.rfwidgets['default'] + self.set_widget('default') - for rfwidget in self.rfwidgets.values(): - if self.rfwidget == rfwidget: continue - if rfwidget.inactive_passthrough(): - self.rfwidget = rfwidget - return + if self.handle_inactive_passthrough(): return if self.rfcontext.actions.pressed('pie menu alt0'): def callback(option): @@ -331,21 +328,24 @@ def stroking(self): def stroke(self): # called when artist finishes a stroke - Point_to_Point2D = self.rfcontext.Point_to_Point2D + Point_to_Point2D = self.rfcontext.Point_to_Point2D raycast_sources_Point2D = self.rfcontext.raycast_sources_Point2D - accel_nearest2D_vert = self.rfcontext.accel_nearest2D_vert + accel_nearest2D_vert = self.rfcontext.accel_nearest2D_vert # filter stroke down where each pt is at least 1px away to eliminate local wiggling radius = self.rfwidgets['brush'].radius stroke = self.rfwidgets['brush'].stroke2D stroke = process_stroke_filter(stroke) - #stroke = process_stroke_source(stroke, raycast_sources_Point2D, is_point_on_mirrored_side=self.rfcontext.is_point_on_mirrored_side) - #stroke = process_stroke_source(stroke, raycast_sources_Point2D, Point_to_Point2D=Point_to_Point2D, mirror_point=self.rfcontext.mirror_point) - stroke = process_stroke_source(stroke, raycast_sources_Point2D, Point_to_Point2D=Point_to_Point2D, clamp_point_to_symmetry=self.rfcontext.clamp_point_to_symmetry) + stroke = process_stroke_source( + stroke, + raycast_sources_Point2D, + Point_to_Point2D=Point_to_Point2D, + clamp_point_to_symmetry=self.rfcontext.clamp_point_to_symmetry, + ) stroke3D = [raycast_sources_Point2D(s)[0] for s in stroke] stroke3D = [s for s in stroke3D if s] - # bail if there isn't enough stroke data to work with + # bail if there aren't enough stroke data points to work with if len(stroke3D) < 2: return self.strip_stroke3D = stroke3D @@ -360,30 +360,31 @@ def stroke(self): # is the stroke in a circle? note: circle must have a large enough radius cyclic = (stroke[0] - stroke[-1]).length < radius and any((s-stroke[0]).length > radius for s in stroke) + # need to determine shape of extrusion + # key: |- stroke (‾_/\) + # C corner in stroke (roughly 90° angle, but not easy to detect. what if the stroke loops over itself?) + # ǁ= edges + # O vertex under stroke + # X corner vertex (edges change direction) + # notes: + # - vertex under stroke must be at beginning or ending of stroke + # - vertices are "under stroke" if they are selected or if "Snap Stroke to Unselected" is enabled + + # Strip Cycle L-shape C-shape U-shape Equals T-shape I-shape O-shape D-shape + # | /‾‾‾\ | O------ ǁ ǁ ====== ===O=== ===O=== X=====O O-----C + # | | | | ǁ ǁ ǁ | | ǁ | ǁ | + # | \___/ O====== X====== O-----O ------ | ===O=== X=====O O-----C + + # so far only Strip, Cycle, L, U, Strip are implemented. C, T, I, O, D are not yet implemented + + # L vs C: there is a corner vertex in the edges (could we extend the L shape??) + # D has corners in the stroke, which will be tricky to determine... use acceleration? + if extrude: if cyclic: - print(f'Extrude Cycle') + # print(f'Extrude Cycle') self.replay = self.extrude_cycle else: - # need to determine shape of extrusion - # key: |- stroke - # C corner in stroke (roughly 90* angle, but not easy to detect) - # ǁ= edges - # O vertex under stroke - # X corner vertex (edges change direction) - # notes: - # - vertex under stroke must be at beginning or ending of stroke - # - vertices are "under stroke" if they are selected or if "Snap Stroke to Unselected" is enabled - # - L, U, Strip are implemented. C, T, I, O, D are not implemented - - # L-shape C-shape U-shape Strip T-shape I-shape O-shape D-shape - # | O------ ǁ ǁ ====== ===O=== ===O=== X=====O O-----C - # | ǁ ǁ ǁ | | ǁ | ǁ | - # O====== X====== O-----O ------ | ===O=== X=====O O-----C - - # L vs C: there is a corner vertex in the edges (could we extend the L shape??) - # D has corners in the stroke, which will be tricky to determine... use acceleration? - sel_verts = self.rfcontext.get_selected_verts() sel_edges = self.rfcontext.get_selected_edges() s0, s1 = Point_to_Point2D(stroke3D[0]), Point_to_Point2D(stroke3D[-1]) @@ -397,24 +398,24 @@ def stroke(self): if not bmv0_sel or not bmv1_sel: bmv = bmv0 if bmv0_sel else bmv1 if len(set(bmv.link_edges) & sel_edges) == 1: - print(f'Extrude L or C') + # print(f'Extrude L or C') self.replay = self.extrude_l else: - print(f'Extrude I or T') + # print(f'Extrude I or T') self.replay = self.extrude_t else: - print(f'Extrude U or O') + # print(f'Extrude U or O') # XXX: I-shaped extrusions? self.replay = self.extrude_u else: - print(f'Extrude Strip') - self.replay = self.extrude_strip + # print(f'Extrude Strip') + self.replay = self.extrude_equals else: if cyclic: - print(f'Create Cycle') + # print(f'Create Cycle') self.replay = self.create_cycle else: - print(f'Create Strip') + # print(f'Create Strip') self.replay = self.create_strip # print(self.replay) @@ -439,6 +440,13 @@ def create_cycle(self): percentages = [i / crosses for i in range(crosses)] nstroke = restroke(stroke, percentages) + if len(nstroke) <= 2: + # too few vertices for a cycle + self.rfcontext.alert_user( + 'Could not find create cycle from stroke. Please try again.' + ) + return + with self.defer_recomputing_while(): verts = [self.rfcontext.new2D_vert_point(s) for s in nstroke] edges = [self.rfcontext.new_edge([v0, v1]) for (v0, v1) in iter_pairs(verts, wrap=True)] @@ -742,14 +750,15 @@ def extrude_l(self): if all(lst) and not has_duplicates(lst): new_face(lst) bmv1 = nverts[0] - nedges.append(bmv0.shared_edge(bmv1)) + if bmv0 and bmv1: + nedges.append(bmv0.shared_edge(bmv1)) bmv0 = bmv1 self.rfcontext.select(nedges) self.just_created = True @RFTool.dirty_when_done - def extrude_strip(self): + def extrude_equals(self): Point_to_Point2D = self.rfcontext.Point_to_Point2D stroke = [Point_to_Point2D(s) for s in self.strip_stroke3D] if not all(stroke): return # part of stroke cannot project @@ -775,8 +784,8 @@ def extrude_strip(self): if not options['strokes snap stroke'] and bmv1 and not bmv1.select: bmv1 = None edges0 = walk_to_corner(bmv0, edges) if bmv0 else [] edges1 = walk_to_corner(bmv1, edges) if bmv1 else [] - edges0 = [e for e in edges0 if e.is_valid] - edges1 = [e for e in edges1 if e.is_valid] + edges0 = [e for e in edges0 if e.is_valid] if edges0 else None + edges1 = [e for e in edges1 if e.is_valid] if edges1 else None if edges0 and edges1 and len(edges0) != len(edges1): self.rfcontext.alert_user( 'Edge strips near ends of stroke have different counts. Make sure your stroke is accurate.' @@ -876,8 +885,8 @@ def extrude_strip(self): self.rfcontext.new_edge([v0, v1]) prev = cur - edges0 = [e for e in edges0 if e.is_valid] - edges1 = [e for e in edges1 if e.is_valid] + edges0 = [e for e in edges0 if e.is_valid] if edges0 else None + edges1 = [e for e in edges1 if e.is_valid] if edges1 else None if edges0: side_verts = get_strip_verts(edges0) @@ -952,6 +961,8 @@ def move_enter(self): self.rfcontext.set_accel_defer(True) self._timer = self.actions.start_timer(120) + if options['hide cursor on tweak']: self.set_widget('hidden') + @FSM.on_state('move') @RFTool.dirty_when_done @profiler.function diff --git a/retopoflow/rftool_tweak/tweak.py b/retopoflow/rftool_tweak/tweak.py index bc9ae7fb0..ecb48f7a4 100644 --- a/retopoflow/rftool_tweak/tweak.py +++ b/retopoflow/rftool_tweak/tweak.py @@ -51,7 +51,7 @@ class Tweak(RFTool): statusbar = '{{brush}} Tweak\t{{brush alt}} Tweak selection\t{{brush radius}} Brush size\t{{brush strength}} Brush strength\t{{brush falloff}} Brush falloff' ui_config = 'tweak_options.html' - RFWidget_Default = RFWidget_Default_Factory.create('Tweak default') + RFWidget_Default = RFWidget_Default_Factory.create() RFWidget_BrushFalloff = RFWidget_BrushFalloff_Factory.create( 'Tweak brush', BoundInt('''options['tweak radius']''', min_value=1), @@ -108,9 +108,9 @@ def reset(self): @FSM.on_state('main') def main(self): if self.actions.using_onlymods(['brush', 'brush alt', 'brush radius', 'brush falloff', 'brush strength']): - self.rfwidget = self.rfwidgets['brushstroke'] + self.set_widget('brushstroke') else: - self.rfwidget = self.rfwidgets['default'] + self.set_widget('default') if self.rfcontext.actions.pressed(['brush', 'brush alt'], unpress=False): self.sel_only = self.rfcontext.actions.using('brush alt') diff --git a/retopoflow/rfwidget.py b/retopoflow/rfwidget.py index 561424ded..99abb42e8 100644 --- a/retopoflow/rfwidget.py +++ b/retopoflow/rfwidget.py @@ -20,6 +20,7 @@ ''' from ..addon_common.common.debug import debugger +from ..addon_common.common.drawing import Cursors from ..addon_common.common.fsm import FSM from ..addon_common.common.drawing import DrawCallbacks from ..addon_common.common.functools import find_fns @@ -155,6 +156,9 @@ def wrapper(*args, **kwargs): return ret return wrapper + def set_cursor(self): + Cursors.set(self.rfw_cursor) + def inactive_passthrough(self): pass def _draw_pre3d(self): self._draw.pre3d() diff --git a/retopoflow/rfwidgets/rfwidget_brushfalloff.py b/retopoflow/rfwidgets/rfwidget_brushfalloff.py index 5110da657..2dea46d34 100644 --- a/retopoflow/rfwidgets/rfwidget_brushfalloff.py +++ b/retopoflow/rfwidgets/rfwidget_brushfalloff.py @@ -38,7 +38,7 @@ class RFWidget_BrushFalloff_Factory: ''' This is a class factory. It is needed, because the FSM is shared across instances. - RFTools might need to share RFWidges that are independent of each other. + RFTools might need to share RFWidgets that are independent of each other. ''' @staticmethod diff --git a/retopoflow/rfwidgets/rfwidget_brushstroke.py b/retopoflow/rfwidgets/rfwidget_brushstroke.py index 7f38ffb21..148b5e43d 100644 --- a/retopoflow/rfwidgets/rfwidget_brushstroke.py +++ b/retopoflow/rfwidgets/rfwidget_brushstroke.py @@ -38,7 +38,7 @@ class RFWidget_BrushStroke_Factory: ''' This is a class factory. It is needed, because the FSM is shared across instances. - RFTools might need to share RFWidges that are independent of each other. + RFTools might need to share RFWidgets that are independent of each other. ''' @staticmethod diff --git a/retopoflow/rfwidgets/rfwidget_default.py b/retopoflow/rfwidgets/rfwidget_default.py index 5114d1627..fa131ca9e 100644 --- a/retopoflow/rfwidgets/rfwidget_default.py +++ b/retopoflow/rfwidgets/rfwidget_default.py @@ -40,11 +40,11 @@ class RFWidget_Default_Factory: ''' This is a class factory. It is needed, because the FSM is shared across instances. - RFTools might need to share RFWidges that are independent of each other. + RFTools might need to share RFWidgets that are independent of each other. ''' @staticmethod - def create(action_name, cursor='DEFAULT'): + def create(*, action_name=None, cursor='DEFAULT'): class RFWidget_Default(RFWidget): rfw_name = 'Default' rfw_cursor = cursor diff --git a/retopoflow/rfwidgets/rfwidget_hidden.py b/retopoflow/rfwidgets/rfwidget_hidden.py new file mode 100644 index 000000000..5fe183b0e --- /dev/null +++ b/retopoflow/rfwidgets/rfwidget_hidden.py @@ -0,0 +1,60 @@ +''' +Copyright (C) 2021 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.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 diff --git a/retopoflow/rfwidgets/rfwidget_linecut.py b/retopoflow/rfwidgets/rfwidget_linecut.py index 9fce68894..b880ed0e8 100644 --- a/retopoflow/rfwidgets/rfwidget_linecut.py +++ b/retopoflow/rfwidgets/rfwidget_linecut.py @@ -42,7 +42,7 @@ 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 RFWidges that are independent of each other. + RFTools might need to share RFWidgets that are independent of each other. ''' @staticmethod