From 58021e54675b73cf1af14d86632f46c0c9d67d8d Mon Sep 17 00:00:00 2001 From: jon denning Date: Fri, 4 Mar 2022 12:01:16 -0700 Subject: [PATCH 01/17] version bump. makefile will gen specific zips now --- Makefile | 13 +++++++------ __init__.py | 4 ++-- config/options.py | 4 ++-- help/changelist.md | 4 ++++ hive.json | 2 +- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 8c891fb14..56074d004 100644 --- a/Makefile +++ b/Makefile @@ -17,12 +17,12 @@ NAME = RetopoFlow -VERSION = "v3.2.6" +VERSION = "v3.2.7" # NOTE: one of the following must be uncommented -# RELEASE = "alpha" +RELEASE = "alpha" # RELEASE = "beta" -RELEASE = "official" +# RELEASE = "official" ifeq ($(RELEASE), "official") @@ -38,7 +38,8 @@ DEBUG_CLEANUP = $(shell pwd)/addon_common/scripts/strip_debugging.py DOCS_REBUILD = $(shell pwd)/scripts/prep_help_for_online.py CREATE_THUMBNAILS = $(shell pwd)/scripts/create_thumbnails.py CGCOOKIE_BUILT = $(NAME)/.cgcookie -ZIP_FILE = $(NAME)_$(ZIP_VERSION).zip +ZIP_GH = $(NAME)_$(ZIP_VERSION)-GitHub.zip +ZIP_BM = $(NAME)_$(ZIP_VERSION)-BlenderMarket.zip .DEFAULT_GOAL := build @@ -95,7 +96,7 @@ build-github: check # create thumbnails cd $(BUILD_DIR)/$(NAME)/help && python3 $(CREATE_THUMBNAILS) # zip it! - cd $(BUILD_DIR) && zip -r $(ZIP_FILE) $(NAME) + cd $(BUILD_DIR) && zip -r $(ZIP_GH) $(NAME) @echo @echo $(NAME)" "$(VERSION)" is ready" @@ -114,7 +115,7 @@ build-blendermarket: check # create thumbnails cd $(BUILD_DIR)/$(NAME)/help && python3 $(CREATE_THUMBNAILS) # zip it! - cd $(BUILD_DIR) && zip -r $(ZIP_FILE) $(NAME) + cd $(BUILD_DIR) && zip -r $(ZIP_BM) $(NAME) @echo @echo $(NAME)" "$(VERSION)" is ready" diff --git a/__init__.py b/__init__.py index ac557cde4..b4e9cfdb9 100644 --- a/__init__.py +++ b/__init__.py @@ -39,8 +39,8 @@ "author": "Jonathan Denning, Jonathan Lampel, Jonathan Williamson, Patrick Moore, Patrick Crawford, Christopher Gearhart", "location": "View 3D > Header", "blender": (2, 93, 0), - "version": (3, 2, 6), - # "warning": "Alpha", # used for warning icon and text in addons panel + "version": (3, 2, 7), + "warning": "Alpha", # used for warning icon and text in addons panel # "warning": "Beta", # "warning": "Release Candidate 1", # "warning": "Release Candidate 2", diff --git a/config/options.py b/config/options.py index cc28aa1a9..81a32ee75 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.6' # α β -retopoflow_version_tuple = (3, 2, 6) +retopoflow_version = '3.2.7α' # α β +retopoflow_version_tuple = (3, 2, 7) retopoflow_blendermarket_url = 'https://blendermarket.com/products/retopoflow' retopoflow_issues_url = "https://github.com/CGCookie/retopoflow/issues" diff --git a/help/changelist.md b/help/changelist.md index d19b5d18e..86fe88949 100644 --- a/help/changelist.md +++ b/help/changelist.md @@ -2,6 +2,10 @@ This document contains details about what has changed in RetopoFlow since version 2.x. +### RetopoFlow 3.2.6→3.2.7 + +- ... + ### RetopoFlow 3.2.5→3.2.6 - Vertex pinning and unpinning, where pinned vertices cannot be moved diff --git a/hive.json b/hive.json index 652b88dee..34b6a1057 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.6", + "version": "3.2.7", "source": "Blender Market", "product url": "https://blendermarket.com/products/retopoflow", "documentation url": "https://docs.retopoflow.com", From d6bc86901e58f272b45bc2ba63acf667777b67e4 Mon Sep 17 00:00:00 2001 From: jon denning Date: Sun, 6 Mar 2022 21:25:34 -0600 Subject: [PATCH 02/17] fix issue #1097 --- addon_common/common/useractions.py | 14 +++++++++++--- help/changelist.md | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/addon_common/common/useractions.py b/addon_common/common/useractions.py index c0f9ea503..b9e1a5e47 100644 --- a/addon_common/common/useractions.py +++ b/addon_common/common/useractions.py @@ -484,8 +484,15 @@ def update(self, context, event, fn_debug=None): if event_type in self.modifier_actions: 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 len(self.now_pressed) == 0: + # nav events should only occur when no other keys are pressed + 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 @@ -506,7 +513,8 @@ def update(self, context, event, fn_debug=None): if 'WHEELUPMOUSE' in ftype or 'WHEELDOWNMOUSE' in ftype: # mouse wheel actions have no release, so handle specially self.just_pressed = ftype - self.now_pressed[event_type] = ftype + else: + self.now_pressed[event_type] = ftype self.last_pressed = ftype else: if event_type in self.now_pressed: diff --git a/help/changelist.md b/help/changelist.md index 86fe88949..f2adc1f64 100644 --- a/help/changelist.md +++ b/help/changelist.md @@ -4,6 +4,7 @@ This document contains details about what has changed in RetopoFlow since versio ### RetopoFlow 3.2.6→3.2.7 +- Fixed bug when pressing MMB while moving geometry with LMB - ... ### RetopoFlow 3.2.5→3.2.6 From 632483fd48911c2cc4bc263415d90c4c19f3d640 Mon Sep 17 00:00:00 2001 From: jon denning Date: Wed, 16 Mar 2022 08:34:09 -0400 Subject: [PATCH 03/17] fixed issue #1100, code cleanup and refactor, added PP insert dist option --- addon_common/common/maths.py | 31 +++++-------------- config/options.py | 1 + retopoflow/rf/rf_target.py | 2 +- retopoflow/rfmesh/rfmesh.py | 30 +++++++++--------- retopoflow/rfmesh/rfmesh_render.py | 7 +++-- retopoflow/rfmesh/rfmesh_wrapper.py | 5 ++- retopoflow/rftool_polypen/polypen.py | 21 +++++++------ .../rftool_polypen/polypen_options.html | 4 +++ 8 files changed, 48 insertions(+), 53 deletions(-) diff --git a/addon_common/common/maths.py b/addon_common/common/maths.py index 111527ae1..00fc53bd4 100644 --- a/addon_common/common/maths.py +++ b/addon_common/common/maths.py @@ -134,9 +134,7 @@ def average(vecs): for v in vecs: vx,vy,vz = v ax,ay,az,ac = ax+vx,ay+vy,az+vz,ac+1 - if ac == 0: - return Vec((0, 0, 0)) - return Vec((ax / ac, ay / ac, az / ac)) + return Vec((ax / ac, ay / ac, az / ac)) if ac else Vec((0, 0, 0)) class Index2D: @@ -216,9 +214,7 @@ def average(points): x += p.x y += p.y c += 1 - if c == 0: - return Point2D((0, 0)) - return Point2D((x / c, y / c)) + return Point2D((x / c, y / c)) if c else Point2D((0, 0)) @staticmethod def weighted_average(weight_points): @@ -227,9 +223,7 @@ def weighted_average(weight_points): x += p.x * w y += p.y * w c += w - if c == 0: - return Point2D((0, 0)) - return Point2D((x / c, y / c)) + return Point2D((x / c, y / c)) if c else Point2D((0, 0)) class RelPoint2D(Vector, Entity2D): @@ -288,9 +282,7 @@ def average(points): x += p.x y += p.y c += 1 - if c == 0: - return RelPoint2D((0, 0)) - return RelPoint2D((x / c, y / c)) + return RelPoint2D((x / c, y / c)) if c else RelPoint2D((0, 0)) @staticmethod def weighted_average(weight_points): @@ -299,9 +291,7 @@ def weighted_average(weight_points): x += p.x * w y += p.y * w c += w - if c == 0: - return RelPoint2D((0, 0)) - return RelPoint2D((x / c, y / c)) + return RelPoint2D((x / c, y / c)) if c else RelPoint2D((0, 0)) RelPoint2D.ZERO = RelPoint2D((0,0)) @@ -365,9 +355,7 @@ def average(points): y += p.y z += p.z c += 1 - if c == 0: - return Point((0, 0, 0)) - return Point((x / c, y / c, z / c)) + return Point((x / c, y / c, z / c)) if c else Point((0, 0, 0)) @staticmethod def weighted_average(weight_points): @@ -377,9 +365,7 @@ def weighted_average(weight_points): y += p.y * w z += p.z * w c += w - if c == 0: - return Point((0, 0, 0)) - return Point((x / c, y / c, z / c)) + return Point((x / c, y / c, z / c)) if c else Point((0, 0, 0)) class Direction2D(Vector, Entity2D): @stats_wrapper @@ -498,8 +484,7 @@ def average(normals): for n in normals: v += n c += 1 - if c: return Normal(v) - return v + return Normal(v) if c else v class Color(Vector): diff --git a/config/options.py b/config/options.py index 81a32ee75..06dc08edb 100644 --- a/config/options.py +++ b/config/options.py @@ -321,6 +321,7 @@ class Options: 'knife snap dist': 5, # pixels away to snap 'polypen merge dist': 10, # pixels away to merge + 'polypen insert dist': 15, # pixels away for inserting new vertex in existing geo 'polypen automerge': True, 'polypen insert mode': 'Tri/Quad', diff --git a/retopoflow/rf/rf_target.py b/retopoflow/rf/rf_target.py index 50fd4b087..860714aef 100644 --- a/retopoflow/rf/rf_target.py +++ b/retopoflow/rf/rf_target.py @@ -611,7 +611,7 @@ def new2D_vert_point(self, xy:Point2D): xyz,norm,_,_ = self.raycast_sources_Point2D(xy) if not xyz or not norm: return None rfvert = self.rftarget.new_vert(xyz, norm) - if rfvert.normal.dot(self.Point2D_to_Direction(xy)) > 0 and self.is_visible(rfvert.co): + if rfvert.normal.dot(self.Point2D_to_Direction(xy)) >= 0 and self.is_visible(rfvert.co): self._detected_bad_normals = True return rfvert diff --git a/retopoflow/rfmesh/rfmesh.py b/retopoflow/rfmesh/rfmesh.py index 5419dedf7..85cb9a63b 100644 --- a/retopoflow/rfmesh/rfmesh.py +++ b/retopoflow/rfmesh/rfmesh.py @@ -143,7 +143,8 @@ def __setup__( self.triangulate() for bmv in self.bme.verts: - bmv.normal_update() + if bmv.link_faces: + bmv.normal_update() # setup finishing self.selection_center = Point((0, 0, 0)) @@ -1656,14 +1657,14 @@ def collapse_edges_faces(self, nearest): for f in v.link_faces: if f not in faces: continue working_next |= {v_ for v_ in f.verts if v_ in remaining} - average = Point.average([v.co for v in working]) + average = Point.average(v.co for v in working) p, n, _, _ = nearest(average) - bmv = self.new_vert(p, n) + rfv = self.new_vert(p, n) for v in working: - bmv = bmv.merge_robust(v) - bmv.co = p - bmv.normal = n - bmv.select = True + rfv = rfv.merge_robust(v) + rfv.co = p + rfv.normal = n + rfv.select = True def delete_selection(self, del_empty_edges=True, del_empty_verts=True, del_verts=True, del_edges=True, del_faces=True): @@ -1715,7 +1716,7 @@ def dissolve_faces(self, faces, use_verts=True): dissolve_faces(self.bme, faces=faces, use_verts=use_verts) def update_verts_faces(self, verts): - faces = set(f for v in verts if v.is_valid for f in self._unwrap(v).link_faces) + faces = { f for v in verts if v.is_valid for f in self._unwrap(v).link_faces } for bmf in faces: n = compute_normal(v.co for v in bmf.verts) vnorm = sum((v.normal for v in bmf.verts), Vector()) @@ -1796,11 +1797,11 @@ def snap_verts_filter(self, nearest, fn_filter): ''' snap verts when fn_filter returns True ''' - for v in self.get_verts(): - if not fn_filter(v): continue - xyz,norm,_,_ = nearest(v.co) - v.co = xyz - v.normal = norm + for rfv in self.get_verts(): + if not fn_filter(rfv): continue + xyz,norm,_,_ = nearest(rfv.co) + rfv.co = xyz + rfv.normal = norm self.dirty() # def snap_all_verts(self, nearest): @@ -1847,7 +1848,8 @@ def flip_face_normals(self): bmf.normal_flip() for bmv in bmf.verts: verts.add(bmv) for bmv in verts: - bmv.normal_update() + if bmv.link_faces: + bmv.normal_update() self.dirty() def recalculate_face_normals(self, *, verts=None, faces=None): diff --git a/retopoflow/rfmesh/rfmesh_render.py b/retopoflow/rfmesh/rfmesh_render.py index ce93da951..3759f6a22 100644 --- a/retopoflow/rfmesh/rfmesh_render.py +++ b/retopoflow/rfmesh/rfmesh_render.py @@ -377,8 +377,11 @@ def seam_face(g): 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() + for bmv in self.bmesh.verts: + if bmv.link_faces: + bmv.normal_update() + # for bmelem in chain(self.bmesh.faces, self.bmesh.edges): + # bmelem.normal_update() self._is_loading = True self._is_loaded = False diff --git a/retopoflow/rfmesh/rfmesh_wrapper.py b/retopoflow/rfmesh/rfmesh_wrapper.py index 92ad62450..d648d9672 100644 --- a/retopoflow/rfmesh/rfmesh_wrapper.py +++ b/retopoflow/rfmesh/rfmesh_wrapper.py @@ -307,8 +307,7 @@ def dissolve(self): vert_dissolve(bmv) def compute_normal(self): - ''' computes normal as average of normals of all linked faces ''' - return Normal(sum((f.compute_normal() for f in self.link_faces), Vec((0,0,0)))) + return Normal.average(f.compute_normal() for f in self.link_faces) class RFEdge(BMElemWrapper): @@ -628,7 +627,7 @@ def compute_normal(self): for bmv in vs: bmv0,bmv1,bmv2 = bmv1,bmv2,bmv v0,v1 = -v1,bmv2.co-bmv1.co - an = an + Normal(v0.cross(v1)) + an = an + v0.cross(v1) return self.l2w_normal(Normal(an)) def is_flipped(self): diff --git a/retopoflow/rftool_polypen/polypen.py b/retopoflow/rftool_polypen/polypen.py index 1cb7df0b8..8af9cdf68 100644 --- a/retopoflow/rftool_polypen/polypen.py +++ b/retopoflow/rftool_polypen/polypen.py @@ -140,8 +140,9 @@ def set_next_state(self, force=False): num_verts = len(self.sel_verts) num_edges = len(self.sel_edges) num_faces = len(self.sel_faces) + self.insert_edge,_ = self.rfcontext.accel_nearest2D_edge(max_dist=options['polypen insert dist']) - if self.nearest_edge and self.nearest_edge.select: # overriding: if hovering over a selected edge, knife it! + if self.insert_edge and self.insert_edge.select: # overriding: if hovering over a selected edge, knife it! self.next_state = 'knife selected edge' elif options['polypen insert mode'] == 'Tri/Quad': @@ -204,7 +205,7 @@ def set_next_state(self, force=False): if num_verts == 0: self.next_state = 'new vertex' else: - if self.nearest_edge: + if self.insert_edge: self.next_state = 'vert-edge' else: self.next_state = 'vert-edge-vert' @@ -390,7 +391,7 @@ def _insert(self): if not bmv: self.rfcontext.undo_cancel() return 'main' - bme0,bmv2 = self.nearest_edge.split() + bme0,bmv2 = self.insert_edge.split() bmv.merge(bmv2) self.rfcontext.select(bmv) self.mousedown = self.actions.mousedown @@ -423,7 +424,7 @@ def _insert(self): if not bmv1: self.rfcontext.undo_cancel() return 'main' - if dist is not None and dist < self.rfcontext.drawing.scale(15): + if dist is not None and dist < self.rfcontext.drawing.scale(options['polypen insert dist']): if bmv0 in nearest_edge.verts: # selected vert already part of edge; split bme0,bmv2 = nearest_edge.split() @@ -581,7 +582,7 @@ def _insert(self): if not bmv: self.rfcontext.undo_cancel() return 'main' - if d is not None and d < self.rfcontext.drawing.scale(15): + if d is not None and d < self.rfcontext.drawing.scale(options['polypen insert dist']): bme0,bmv2 = nearest_edge.split() bmv.merge(bmv2) self.rfcontext.select(bmv) @@ -759,8 +760,8 @@ def draw_postpixel(self): CC_DRAW.line_width(2) if self.next_state == 'knife selected edge': - bmv1,bmv2 = self.nearest_edge.verts - faces = self.nearest_edge.link_faces + bmv1,bmv2 = self.insert_edge.verts + faces = self.insert_edge.link_faces if faces: for f in faces: lco = [] @@ -778,7 +779,7 @@ def draw_postpixel(self): e1,d = self.rfcontext.nearest2D_edge(edges=self.vis_edges) if e1: bmv1,bmv2 = e1.verts - if d is not None and d < self.rfcontext.drawing.scale(15): + if d is not None and d < self.rfcontext.drawing.scale(options['polypen insert dist']): f = next(iter(e1.link_faces), None) if f: lco = [] @@ -805,7 +806,7 @@ def draw_postpixel(self): e1,d = self.rfcontext.nearest2D_edge(edges=self.vis_edges) if e1: bmv1,bmv2 = e1.verts - if d is not None and d < self.rfcontext.drawing.scale(15): + if d is not None and d < self.rfcontext.drawing.scale(options['polypen insert dist']): f = next(iter(e1.link_faces), None) if f: lco = [] @@ -834,7 +835,7 @@ def draw_postpixel(self): e0,_ = self.rfcontext.nearest2D_edge(edges=self.sel_edges) #next(iter(self.sel_edges)) if not e0: return e1,d = self.rfcontext.nearest2D_edge(edges=self.vis_edges) - if e1 and d < self.rfcontext.drawing.scale(15) and e0 == e1: + if e1 and d < self.rfcontext.drawing.scale(options['polypen insert dist']) and e0 == e1: bmv1,bmv2 = e1.verts p0 = hit_pos f = next(iter(e1.link_faces), None) diff --git a/retopoflow/rftool_polypen/polypen_options.html b/retopoflow/rftool_polypen/polypen_options.html index f63ddc10f..b6b63ec6b 100644 --- a/retopoflow/rftool_polypen/polypen_options.html +++ b/retopoflow/rftool_polypen/polypen_options.html @@ -14,6 +14,10 @@

Automerge

+
+ + +

Insert Mode

From 740b8d61afc610164be3d1beeb11d96a20ee491b Mon Sep 17 00:00:00 2001 From: jon denning Date: Wed, 16 Mar 2022 08:48:21 -0400 Subject: [PATCH 04/17] fix issue #1101 --- addon_common/common/useractions.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/addon_common/common/useractions.py b/addon_common/common/useractions.py index b9e1a5e47..3c184fb54 100644 --- a/addon_common/common/useractions.py +++ b/addon_common/common/useractions.py @@ -566,9 +566,7 @@ def from_human_readable(self, actions): def unuse(self, actions): actions = self.convert(actions) keys = [k for k,v in self.now_pressed.items() if v in actions] - # print('Actions.unuse', actions, self.now_pressed, keys) for k in keys: del self.now_pressed[k] - # print('unuse', self.just_pressed) self.mousedown = None self.mousedown_left = None self.mousedown_middle = None @@ -577,14 +575,13 @@ def unuse(self, actions): self.unpress() def unpress(self): - # print('unpress', self.just_pressed) - # for entry in enumerate(inspect.stack()): - # print(' %s' % str(entry)) if not self.just_pressed: return - if '+CLICK' in self.just_pressed: - del self.now_pressed[strip_mods(self.just_pressed)] - elif '+DOUBLE' in self.just_pressed: - del self.now_pressed[strip_mods(self.just_pressed)] + just_pressed_no_mods = strip_mods(self.just_pressed) + if just_pressed_no_mods in self.now_pressed: + if '+CLICK' in self.just_pressed: + del self.now_pressed[just_pressed_no_mods] + elif '+DOUBLE' in self.just_pressed: + del self.now_pressed[just_pressed_no_mods] self.just_pressed = None def using(self, actions, using_all=False, ignoremods=False, ignorectrl=False, ignoreshift=False, ignorealt=False, ignoreoskey=False, ignoremulti=False, ignoreclick=False, ignoredouble=False, ignoredrag=False): From dab3169523e29d4de1fdb7b6e54084bac0224065 Mon Sep 17 00:00:00 2001 From: jon denning Date: Sun, 1 May 2022 16:04:18 -0400 Subject: [PATCH 05/17] improved auto save, improved target/source code, bug fixes and code cleanup RF is now more careful with auto saving. now, a temp mesh is used when updating. if the bmesh->mesh works, then the meshes are swapped out, and the old mesh is deleted. if RF breaks while doing auto save, RF now bails as quickly as possible to avoid data loss. now using custom variables to mark target and source objects. this is useful for when finishing recovering from auto save. this also makes target/source getting code less brittle. added a "Finish Auto Save Recovery" button for when the artist opens directly an auto save file. removed old (Blender <2.80) code --- __init__.py | 73 ++++--- .../common/shaders/bmesh_render_edges.glsl | 7 +- .../common/shaders/bmesh_render_faces.glsl | 7 +- .../common/shaders/bmesh_render_verts.glsl | 7 +- addon_common/common/ui_document.py | 4 +- addon_common/cookiecutter/cookiecutter.py | 24 ++- config/options.py | 18 +- help/warnings.md | 4 + retopoflow/retopoflow.py | 7 +- retopoflow/rf/rf_blender.py | 158 ++++++++------- retopoflow/rf/rf_blendersave.py | 184 +++++++++++++----- retopoflow/rf/rf_sources.py | 2 +- retopoflow/rf/rf_target.py | 8 +- retopoflow/rf/rf_ui_alert.py | 11 +- retopoflow/rfmesh/rfmesh.py | 38 ++-- 15 files changed, 368 insertions(+), 184 deletions(-) diff --git a/__init__.py b/__init__.py index b4e9cfdb9..ebbbb7213 100644 --- a/__init__.py +++ b/__init__.py @@ -243,15 +243,7 @@ def poll(cls, context): return True def invoke(self, context, event): - auto_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode # working around blender bug, see https://github.com/CGCookie/retopoflow/issues/786 - bpy.context.preferences.edit.use_enter_edit_mode = False - for o in bpy.data.objects: o.select_set(False) - mesh = bpy.data.meshes.new('RetopoFlow') - obj = object_utils.object_data_add(context, mesh, name='RetopoFlow') - obj.select_set(True) - context.view_layer.objects.active = obj - bpy.ops.object.mode_set(mode='EDIT') - bpy.context.preferences.edit.use_enter_edit_mode = auto_edit_mode + retopoflow.RetopoFlow.create_new_target(context) return bpy.ops.cgcookie.retopoflow('INVOKE_DEFAULT') RF_classes += [VIEW3D_OT_RetopoFlow_NewTarget_Cursor] @@ -279,17 +271,7 @@ def poll(cls, context): return True def invoke(self, context, event): - active = get_active_object() - auto_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode # working around blender bug, see https://github.com/CGCookie/retopoflow/issues/786 - bpy.context.preferences.edit.use_enter_edit_mode = False - for o in bpy.data.objects: o.select_set(False) - mesh = bpy.data.meshes.new('RetopoFlow') - obj = object_utils.object_data_add(context, mesh, name='RetopoFlow') - obj.select_set(True) - context.view_layer.objects.active = obj - obj.matrix_world = active.matrix_world - bpy.ops.object.mode_set(mode='EDIT') - bpy.context.preferences.edit.use_enter_edit_mode = auto_edit_mode + retopoflow.RetopoFlow.create_new_target(context) return bpy.ops.cgcookie.retopoflow('INVOKE_DEFAULT') RF_classes += [VIEW3D_OT_RetopoFlow_NewTarget_Active] @@ -331,10 +313,10 @@ class VIEW3D_OT_RetopoFlow_Tool(retopoflow.RetopoFlow): create operator for recovering auto save ''' - class VIEW3D_OT_RetopoFlow_Recover(Operator): - bl_idname = 'cgcookie.retopoflow_recover' - bl_label = 'Recover Auto Save' - bl_description = 'Recover from RetopoFlow auto save' + class VIEW3D_OT_RetopoFlow_RecoverOpen(Operator): + bl_idname = 'cgcookie.retopoflow_recover_open' + bl_label = 'Recover: Open Last Auto Save' + bl_description = 'Recover by opening last file automatically saved by RetopoFlow' bl_space_type = 'VIEW_3D' bl_region_type = 'TOOLS' bl_options = set() @@ -342,13 +324,30 @@ class VIEW3D_OT_RetopoFlow_Recover(Operator): @classmethod def poll(cls, context): - return retopoflow.RetopoFlow.has_backup() + return retopoflow.RetopoFlow.has_auto_save() def invoke(self, context, event): - global perform_backup_recovery - retopoflow.RetopoFlow.backup_recover() + retopoflow.RetopoFlow.recover_auto_save() return {'FINISHED'} - RF_classes += [VIEW3D_OT_RetopoFlow_Recover] + RF_classes += [VIEW3D_OT_RetopoFlow_RecoverOpen] + + class VIEW3D_OT_RetopoFlow_RecoverRevert(Operator): + bl_idname = 'cgcookie.retopoflow_recover_finish' + bl_label = 'Recover: Finish Auto Save Recovery' + bl_description = 'Finish recovering open file' + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_options = set() + rf_icon = 'rf_recover_icon' + + @classmethod + def poll(cls, context): + return retopoflow.RetopoFlow.can_recover() + + def invoke(self, context, event): + retopoflow.RetopoFlow.recovery_revert() + return {'FINISHED'} + RF_classes += [VIEW3D_OT_RetopoFlow_RecoverRevert] if import_succeeded: @@ -457,6 +456,10 @@ def get_warnings(cls, context): warnings.add('save: auto save is disabled') if not retopoflow.RetopoFlow.get_auto_save_settings(context)['saved']: warnings.add('save: unsaved blender file') + if retopoflow.RetopoFlow.can_recover(): + # user directly opened an auto save file + warnings.add('save: can recover auto save') + return warnings @@ -528,6 +531,14 @@ def get_warning_subbox(label): if 'save: unsaved blender file' in warnings: box = get_warning_subbox('Auto Save / Save') box.label(text='Unsaved Blender file', icon='DOT') + if 'save: can recover auto save' in warnings: + box = get_warning_subbox('Auto Save') + box.label(text=f'Auto Save file opened', icon='DOT') + box.operator( + 'cgcookie.retopoflow_recover_finish', + text='Finish Auto Save Recovery', + icon='RECOVER_LAST', + ) # show button for more warning details layout.operator('cgcookie.retopoflow_help_warnings', icon='HELP') @@ -638,7 +649,11 @@ class VIEW3D_PT_RetopoFlow_AutoSave(Panel): def draw(self, context): layout = self.layout - layout.operator('cgcookie.retopoflow_recover', icon='RECOVER_LAST') + layout.operator( + 'cgcookie.retopoflow_recover_open', + text='Open Last Auto Save', + icon='RECOVER_LAST', + ) # if retopoflow.RetopoFlow.has_backup(): # box.label(text=options['last auto save path']) diff --git a/addon_common/common/shaders/bmesh_render_edges.glsl b/addon_common/common/shaders/bmesh_render_edges.glsl index fe4b14582..930cb9697 100644 --- a/addon_common/common/shaders/bmesh_render_edges.glsl +++ b/addon_common/common/shaders/bmesh_render_edges.glsl @@ -96,7 +96,12 @@ vec4 get_pos(vec3 p) { float focus = (view_distance - clip_start) / clip_dist + 0.04; mult = focus; } - return vec4((p + vert_norm * normal_offset * mult * unit_scaling_factor) * vert_scale, 1.0); + return vec4( + ( + p + + vert_norm * normal_offset * mult / unit_scaling_factor + ) * vert_scale, + 1.0); } vec4 xyz(vec4 v) { diff --git a/addon_common/common/shaders/bmesh_render_faces.glsl b/addon_common/common/shaders/bmesh_render_faces.glsl index 1d7d82750..cf964d87e 100644 --- a/addon_common/common/shaders/bmesh_render_faces.glsl +++ b/addon_common/common/shaders/bmesh_render_faces.glsl @@ -89,7 +89,12 @@ vec4 get_pos(vec3 p) { float focus = (view_distance - clip_start) / clip_dist + 0.04; mult = focus; } - return vec4((p + vert_norm * normal_offset * mult * unit_scaling_factor) * vert_scale, 1.0); + return vec4( + ( + p + + vert_norm * normal_offset * mult / unit_scaling_factor + ) * vert_scale, + 1.0); } vec4 xyz(vec4 v) { diff --git a/addon_common/common/shaders/bmesh_render_verts.glsl b/addon_common/common/shaders/bmesh_render_verts.glsl index aaa448c79..df36eebc7 100644 --- a/addon_common/common/shaders/bmesh_render_verts.glsl +++ b/addon_common/common/shaders/bmesh_render_verts.glsl @@ -87,7 +87,12 @@ vec4 get_pos(vec3 p) { float focus = (view_distance - clip_start) / clip_dist + 0.04; mult = focus; } - return vec4((p + vert_norm * normal_offset * mult * unit_scaling_factor) * vert_scale, 1.0); + return vec4( + ( + p + + vert_norm * normal_offset * mult / unit_scaling_factor + ) * vert_scale, + 1.0); } vec4 xyz(vec4 v) { diff --git a/addon_common/common/ui_document.py b/addon_common/common/ui_document.py index 5c4f65223..0750314e8 100644 --- a/addon_common/common/ui_document.py +++ b/addon_common/common/ui_document.py @@ -176,9 +176,9 @@ def center_on_mouse(self, element): if element is None: return def center(): element._relative_pos = None - mx,my = self.actions.mouse + mx, my = self.actions.mouse if self.actions.mouse else (100, 100) # w,h = element.width_pixels,element.height_pixels - w,h = element.width_pixels,element._dynamic_full_size.height + w, h = element.width_pixels, element._dynamic_full_size.height l = mx-w/2 t = -self._body.height_pixels + my + h/2 element.reposition(left=l, top=t) diff --git a/addon_common/cookiecutter/cookiecutter.py b/addon_common/cookiecutter/cookiecutter.py index f455f4274..0c15c73d8 100644 --- a/addon_common/cookiecutter/cookiecutter.py +++ b/addon_common/cookiecutter/cookiecutter.py @@ -119,8 +119,11 @@ def invoke(self, context, event): self.context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} - def done(self, cancel=False): - self._done = 'commit' if not cancel else 'cancel' + def done(self, *, cancel=False, emergency_bail=False): + if emergency_bail: + self._done = 'bail' + else: + self._done = 'commit' if not cancel else 'cancel' def modal(self, context, event): # print('CookieCutter.modal', event.type, time.time()) @@ -134,14 +137,15 @@ def modal(self, context, event): profiler.printfile() if self._done: - try: - if self._done == 'commit': - self.end_commit() - else: - self.end_cancel() - self.end() - except Exception as e: - self._handle_exception(e, 'call end() with %s' % self._done) + if self._done != 'bail': + try: + if self._done == 'commit': + self.end_commit() + else: + self.end_cancel() + self.end() + except Exception as e: + self._handle_exception(e, 'call end() with %s' % self._done) self._cc_ui_end() self._cc_actions_end() self._cc_exception_done() diff --git a/config/options.py b/config/options.py index 06dc08edb..7e4a14dab 100644 --- a/config/options.py +++ b/config/options.py @@ -541,12 +541,20 @@ def _setter(v): def gettersetter(self, key, getwrap=None, setwrap=None, setcallback=None): return (self.getter(key, getwrap=getwrap), self.setter(key, setwrap=setwrap, setcallback=setcallback)) - def get_auto_save_filepath(self): - if not getattr(bpy.data, 'filepath', ''): + def get_auto_save_filepath(self, *, suffix=None): + suffix = f'_{suffix}' if suffix else '' + + if not getattr(bpy.data, 'filepath', None): # not working on a saved .blend file, yet! - return os.path.join(tempfile.gettempdir(), self['backup_filename']) - base, ext = os.path.splitext(bpy.data.filepath) - return '%s_RetopoFlow_AutoSave%s' % (base, ext) + path = tempfile.gettempdir() + filename = self['backup_filename'] + else: + fullpath = os.path.abspath(bpy.data.filepath) + path, filename = os.path.split(fullpath) + suffix = f'_RetopoFlow_AutoSave{suffix}' + + base, ext = os.path.splitext(filename) + return os.path.join(path, f'{base}{suffix}{ext}') class Themes: diff --git a/help/warnings.md b/help/warnings.md index e71123f3c..093a8a250 100644 --- a/help/warnings.md +++ b/help/warnings.md @@ -34,6 +34,7 @@ Disable either of these settings in the 3D View Sidebar (`N`) before starting Re ![View Locks](warning_viewlock.png max-height:103px) + ## Auto Save / Save If Blender's auto save is disabled, any work done since the last time you saved can be lost if Blender crashes. To enable auto save, go Edit > Preferences > Save & Load > Auto Save. @@ -46,6 +47,9 @@ Temporary file path: `{`options.get_auto_save_filepath()`}` Warn if file is unsaved +If you directly open an auto saved file, some of the visual settings and mesh sizes will be different. +Clicking the "Finish Auto Save Recovery" button will recover the original visual settings and mesh sizes. + ## Performance: Target/Sources Too Large diff --git a/retopoflow/retopoflow.py b/retopoflow/retopoflow.py index ee6132c6e..dd7ca56c0 100644 --- a/retopoflow/retopoflow.py +++ b/retopoflow/retopoflow.py @@ -244,8 +244,7 @@ def start(self): self.scene_scale_set(1.0) # DO THESE BEFORE SWITCHING TO OBJECT MODE BELOW AND BEFORE SETTING UP SOURCES AND TARGET! - self.src_objects = self.get_sources() - self.tar_object = self.get_target() + self.mark_sources_target() # bpy.context.object.update_from_editmode() bpy.ops.object.mode_set(mode='OBJECT') @@ -253,14 +252,13 @@ def start(self): # get scaling factor to fit all sources into unit box print('RetopoFlow: setting up scaling factor') self.unit_scaling_factor = self.get_unit_scaling_factor() - print('Unit scaling factor:', self.unit_scaling_factor) + print(f' Unit scaling factor: {self.unit_scaling_factor}') self.scale_to_unit_box(clip_override=options['clip override'], clip_start=options['clip start override'], clip_end=options['clip end override']) self.setup_ui_blender() self.reload_stylings() # the rest of setup is handled in `loading` state and self.setup_next_stage above - # self.fsm.init(self, start='main') self.fsm.force_set_state('loading') @@ -273,6 +271,7 @@ def end(self): # one more toggle, because done_target() might push to target mesh bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='EDIT') + self.unmark_sources_target() # DO THIS AS ONE OF LAST RetopoFlow.instance = None RetopoFlow.cc_debug_print_to = 'RetopoFlow_Debug' diff --git a/retopoflow/rf/rf_blender.py b/retopoflow/rf/rf_blender.py index 8f889fe48..f43bc5196 100644 --- a/retopoflow/rf/rf_blender.py +++ b/retopoflow/rf/rf_blender.py @@ -42,22 +42,10 @@ class RetopoFlow_Blender: ''' @staticmethod - @blender_version_wrapper('<','2.80') - def is_valid_source(o, test_poly_count=True): - assert False, "TODO: NEED TO UPDATE!!! SEE 2.80+ VERSION BELOW" - if not o: return False - if type(o) is not bpy.types.Object: return False - if type(o.data) is not bpy.types.Mesh: return False - if not any(vl and ol for vl,ol in zip(bpy.context.scene.layers, o.layers)): return False - if o.hide: return False - if o.select and o == get_active_object(): return False - if test_poly_count and not o.data.polygons: return False - return True - - @staticmethod - @blender_version_wrapper('>=','2.80') def is_valid_source(o, test_poly_count=True): if not o: return False + mark = RetopoFlow_Blender.get_sources_target_mark(o) + if mark is not None: return mark == 'source' # if o == get_active_object(): return False if o == bpy.context.edit_object: return False if type(o) is not bpy.types.Object: return False @@ -67,22 +55,10 @@ def is_valid_source(o, test_poly_count=True): return True @staticmethod - @blender_version_wrapper('<','2.80') - def is_valid_target(o): - assert False, "TODO: NEED TO UPDATE!!! SEE 2.80+ VERSION BELOW" - if not o: return False - if o != get_active_object(): return False - if type(o) is not bpy.types.Object: return False - if type(o.data) is not bpy.types.Mesh: return False - if not any(vl and ol for vl,ol in zip(bpy.context.scene.layers, o.layers)): return False - if o.hide: return False - if not o.select: return False - return True - - @staticmethod - @blender_version_wrapper('>=','2.80') def is_valid_target(o): if not o: return False + mark = RetopoFlow_Blender.get_sources_target_mark(o) + if mark is not None: return mark == 'target' # if o != get_active_object(): return False if o != bpy.context.edit_object: return False if not o.visible_get(): return False @@ -108,53 +84,65 @@ def is_in_valid_mode(): return True @staticmethod - def get_sources(): - return [o for o in bpy.data.objects if RetopoFlow_Blender.is_valid_source(o)] + def mark_sources_target(): + for obj in bpy.data.objects: + if RetopoFlow_Blender.is_valid_source(obj): + # set as source + obj['RetopFlow'] = 'source' + elif RetopoFlow_Blender.is_valid_target(obj): + obj['RetopoFlow'] = 'target' + else: + obj['RetopoFlow'] = 'unused' @staticmethod - def get_target(): - o = get_active_object() - return o if RetopoFlow_Blender.is_valid_target(o) else None + def unmark_sources_target(): + for obj in bpy.data.objects: + if 'RetopoFlow' not in obj: continue + del obj['RetopoFlow'] + @staticmethod + def get_sources_target_mark(obj): + if 'RetopoFlow' not in obj: return None + return obj['RetopoFlow'] - ################################################### - # handle scaling objects and view so sources fit - # in unit box for scale-independent rendering + @staticmethod + def get_sources(): + is_valid = RetopoFlow_Blender.is_valid_source + return [ o for o in bpy.data.objects if is_valid(o) ] @staticmethod - def scale_by(factor, r3d, space, tar_object=None): - print('RetopoFlow: scaling view, sources, and target by %0.2f' % factor) + def get_target(): + is_valid = RetopoFlow_Blender.is_valid_target + return next(( o for o in bpy.data.objects if is_valid(o) ), None) + + @staticmethod + def create_new_target(context): + auto_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode # working around blender bug, see https://github.com/CGCookie/retopoflow/issues/786 + bpy.context.preferences.edit.use_enter_edit_mode = False - def scale_object(o): - for i in range(3): - for j in range(4): - o.matrix_world[i][j] *= factor + for o in bpy.data.objects: o.select_set(False) - r3d.view_distance *= factor - r3d.view_location *= factor - space.clip_start *= factor - space.clip_end *= factor + mesh = bpy.data.meshes.new('RetopoFlow') + obj = object_data_add(context, mesh, name='RetopoFlow') + + obj.select_set(True) + context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') - for src in RetopoFlow_Blender.get_sources(): scale_object(src) + bpy.context.preferences.edit.use_enter_edit_mode = auto_edit_mode - if tar_object is None: tar_object = RetopoFlow_Blender.get_target() - if tar_object: scale_object(tar_object) + ################################################### + # handle scaling objects and view so sources fit + # in unit box for scale-independent rendering def _scale_by(self, factor, clip_override=True, clip_start=None, clip_end=None, clip_restore=False): - # RetopoFlow_Blender.scale_by(factor, self.actions.r3d, self.actions.space, tar_object=getattr(self, 'tar_object', None)) r3d = self.actions.r3d space = self.actions.space - def scale_object(o): - for i in range(3): - for j in range(4): - o.matrix_world[i][j] *= factor - print('RetopoFlow: scaling view, sources, and target by %0.2f' % factor) # scale view - r3d.view_distance *= factor - r3d.view_location *= factor + self.scale_view(r3d, factor) # override/scale clipping distances if not hasattr(self, '_clip_distances'): @@ -178,11 +166,21 @@ def scale_object(o): else: space.clip_end *= factor - # scale sources - for src in self.src_objects: scale_object(src) + self.scale_sources_target(factor) - # scale target - scale_object(self.tar_object) + @staticmethod + def scale_view(r3d, factor): + r3d.view_distance *= factor + r3d.view_location *= factor + + @staticmethod + def scale_sources_target(factor): + M = Matrix.Identity(4) * factor + objects = RetopoFlow_Blender.get_sources() + objects += [RetopoFlow_Blender.get_target()] + for obj in objects: + if not obj: continue + obj.matrix_world = M @ obj.matrix_world def scale_to_unit_box(self, clip_override=True, clip_start=None, clip_end=None): self._scale_by(1.0 / self.unit_scaling_factor, clip_override=clip_override, clip_start=clip_start, clip_end=clip_end) @@ -223,7 +221,7 @@ def end_rotate_about_active(self): # IMPORTANT: changes here should also go in rf_blendersave.backup_recover() if options['rotate object'] not in bpy.data.objects: return self.del_rotate_object() - bpy.context.view_layer.objects.active = self.tar_object + bpy.context.view_layer.objects.active = self.get_target() @staticmethod def del_rotate_object(): @@ -282,6 +280,10 @@ def store_window_state(find_r3d, find_space): data_space['show_gizmo'] = space.show_gizmo data_space['show_overlays'] = space.overlay.show_overlays data_space['show_region_header'] = space.show_region_header + data_space['clip_start'] = space.clip_start + data_space['clip_end'] = space.clip_end + data_space['region_3d.view_distance'] = space.region_3d.view_distance + data_space['region_3d.view_location'] = list(space.region_3d.view_location) if hasattr(space, 'show_region_tool_header'): data_space['show_region_tool_header'] = space.show_region_tool_header data_space['shading.type'] = space.shading.type @@ -310,6 +312,10 @@ def store_window_state(find_r3d, find_space): data['selected objects'] = [o.name for o in bpy.data.objects if getattr(o, 'select', False)] data['hidden objects'] = [o.name for o in bpy.data.objects if getattr(o, 'hide', False)] + RetopoFlow_Blender.write_window_state(data) + + @staticmethod + def write_window_state(data): # store data in text block (might need to create textblock!) name = options['blender state'] texts = bpy.data.texts @@ -317,11 +323,17 @@ def store_window_state(find_r3d, find_space): text.from_string(json.dumps(data, indent=4, sort_keys=True)) @staticmethod - def restore_window_state(ignore_panels=False, ignore_mode=False): + def restore_window_state(*, ignore_panels=False, ignore_mode=False): name = options['blender state'] - texts = bpy.data.texts - if name not in texts: return # no stored blender state!?!? - data = json.loads(texts[name].as_string()) + if name not in bpy.data.texts: return # no stored blender state!?!? + data = json.loads(bpy.data.texts[name].as_string()) + + if data['retopoflow'] != retopoflow_version: + print(f'WARNING!!!') + print(f'Recovery data from a different version of RetopoFlow!') + print(f'Recovering might cause RF / Blender to crash') + print(f'Cancelling restoration') + return # bpy.context.window.screen = bpy.data.screens[data['screen name']] @@ -340,6 +352,10 @@ def restore_window_state(ignore_panels=False, ignore_mode=False): if hasattr(space, 'show_region_tool_header'): space.show_region_tool_header = data_space['show_region_tool_header'] space.shading.type = data_space['shading.type'] + space.clip_start = data_space['clip_start'] + space.clip_end = data_space['clip_end'] + space.region_3d.view_distance = data_space['region_3d.view_distance'] + space.region_3d.view_location = Vector(data_space['region_3d.view_location']) if not ignore_panels: if hasattr(space, 'show_region_toolbar') and 'toolbar' in data_space: space.show_region_toolbar = data_space['toolbar'] @@ -384,4 +400,12 @@ def restore_window_state(ignore_panels=False, ignore_mode=False): if not ignore_mode: bpy.ops.object.mode_set(mode=data['mode translated']) - return found + + + + + + + + + diff --git a/retopoflow/rf/rf_blendersave.py b/retopoflow/rf/rf_blendersave.py index 006d041e1..d53b2bd2c 100644 --- a/retopoflow/rf/rf_blendersave.py +++ b/retopoflow/rf/rf_blendersave.py @@ -33,40 +33,29 @@ from ...addon_common.common.globals import Globals from ...addon_common.common.decorators import blender_version_wrapper -from ...addon_common.common.blender import matrix_vector_mult, get_preferences, set_object_selection, set_active_object -from ...addon_common.common.blender import toggle_screen_header, toggle_screen_toolbar, toggle_screen_properties, toggle_screen_lastop +from ...addon_common.common.blender import ( + matrix_vector_mult, + get_preferences, + set_object_selection, + set_active_object, + toggle_screen_header, + toggle_screen_toolbar, + toggle_screen_properties, + toggle_screen_lastop, + show_error_message, +) from ...addon_common.common.maths import BBox from ...addon_common.common.debug import dprint from .rf_blender import RetopoFlow_Blender -@persistent -def handle_recover(*args, **kwargs): - print('RetopoFlow: handling recover from auto save') +@persistent +def revert_auto_save_after_load(*_, **__): # remove recover handler - bpy.app.handlers.load_post.remove(handle_recover) - - ################## - # restore - - # the rotate object should not exist, but just in case - if options['rotate object'] in bpy.data.objects: - bpy.data.objects.remove(bpy.data.objects[options['rotate object']], do_unlink=True) - - # grab previous blender state - if options['blender state'] not in bpy.data.texts: return # no blender state!?!? - data = json.loads(bpy.data.texts[options['blender state']].as_string()) - - # get target object and reset settings - tar_object = bpy.data.objects[data['active object']] - bpy.context.view_layer.objects.active = tar_object - tar_object.hide_viewport = False - tar_object.hide_render = False - - # restore window state (mostly tool, properties, header, etc.) - RetopoFlow_Blender.restore_window_state(ignore_panels=False, ignore_mode=False) + bpy.app.handlers.load_post.remove(revert_auto_save_after_load) + RetopoFlow_BlenderSave.recovery_revert() class RetopoFlow_BlenderSave: @@ -74,6 +63,49 @@ class RetopoFlow_BlenderSave: backup / restore methods ''' + @staticmethod + def can_recover(): + if options['rotate object'] in bpy.data.objects: return True + if options['blender state'] in bpy.data.texts: return True + return False + + @staticmethod + def recovery_revert(): + print('RetopoFlow: recovering from auto save') + + # the rotate object should not exist, but just in case + if options['rotate object'] in bpy.data.objects: + bpy.data.objects.remove( + bpy.data.objects[options['rotate object']], + do_unlink=True, + ) + + # grab previous blender state + if options['blender state'] not in bpy.data.texts: return # no blender state!?!? + data = json.loads(bpy.data.texts[options['blender state']].as_string()) + + # get target object and reset settings + tar_object = bpy.data.objects[data['active object']] + tar_object.hide_viewport = False + tar_object.hide_render = False + bpy.context.view_layer.objects.active = tar_object + tar_object.select_set(True) + + # restore window state (mostly tool, properties, header, etc.) + RetopoFlow_Blender.restore_window_state( + ignore_panels=False, + ignore_mode=False, + ) + + factor = data['unit scaling factor'] + RetopoFlow_Blender.scale_sources_target(factor) + + bpy.data.texts.remove( + bpy.data.texts[options['blender state']], + do_unlink=True, + ) + + @staticmethod def get_auto_save_settings(context): prefs = get_preferences(context) @@ -142,39 +174,99 @@ def handle_auto_save(self): self.time_to_save = time.time() + auto_save_time @staticmethod - def has_backup(): + def has_auto_save(): filepath = options['last auto save path'] return filepath and os.path.exists(filepath) @staticmethod - def backup_recover(): + def recover_auto_save(): filepath = options['last auto save path'] - if not filepath or not os.path.exists(filepath): return + print(f'backup recover: {filepath}') + if not filepath or not os.path.exists(filepath): + print(f' DOES NOT EXIST!') + return - bpy.app.handlers.load_post.append(handle_recover) + bpy.app.handlers.load_post.append(revert_auto_save_after_load) - print('backup recover:', filepath) bpy.ops.wm.open_mainfile(filepath=filepath) - + def save_emergency(self): + try: + filepath = options.get_auto_save_filepath(suffix='EMERGENCY') + bpy.ops.wm.save_as_mainfile( + filepath=filepath, + compress=True, # write compressed file + check_existing=False, # do not warn if file already exists + copy=True, # does not make saved file active + ) + except: + self.done(emergency_bail=True) + show_error_message( + "RetopoFlow crashed unexpectedly. Be sure to save your work, and report what happened so that we can try fixing it.", + "Unexpected Crash!", + ) def save_backup(self): if hasattr(self, '_backup_broken'): return if self.last_change_count == self.change_count: - dprint('skipping backup save') + print('skipping backup save') return + filepath = options.get_auto_save_filepath() - filepath1 = "%s1" % filepath - dprint('saving backup to %s' % filepath) - if os.path.exists(filepath1): os.remove(filepath1) - if os.path.exists(filepath): os.rename(filepath, filepath1) - try: - bpy.ops.wm.save_as_mainfile(filepath=filepath, check_existing=False, copy=True) - options['last auto save path'] = filepath - self.last_change_count = self.change_count - except Exception as e: + filepath1 = f'{filepath}1' + + print(f'saving backup: {filepath}') + errors = {} + + if os.path.exists(filepath): + if os.path.exists(filepath1): + try: + print(f' deleting old backup: {filepath1}') + os.remove(filepath1) + except Exception as e: + print(f' caught exception: {e}') + errors['delete old'] = e + + try: + print(f' renaming prev backup: {filepath1}') + os.rename(filepath, filepath1) + except Exception as e: + print(f' caught exception: {e}') + errors['rename prev'] = e + + if 'rename prev' not in errors: + try: + print(f' saving...') + bpy.ops.wm.save_as_mainfile( + filepath=filepath, + compress=True, # write compressed file + check_existing=False, # do not warn if file already exists + copy=True, # does not make saved file active + ) + options['last auto save path'] = filepath + self.last_change_count = self.change_count + except Exception as e: + print(f' caught exception: {e}') + errors['saving'] = e + else: + ''' + skipping normal save, because we might lose data! + ''' + errors['skipped save'] = 'error while trying to rename prev' + + if errors: self._backup_broken = True - self.alert_user(title='Could not save backup', message=f'Could not save backup file. Temporarily preventing further backup attempts. You might try saving file manually.\n\nFile path: `{filepath}`\n\nError message: "{e}"') + self.alert_user( + title='Could not save backup', + level='assert', + message=( + f'Could not save backup file. ' + f'Temporarily preventing further backup attempts. ' + f'You might try saving file manually.\n\n' + f'File paths: `{filepath}`, `{filepath1}`\n\n' + f'Errors: {errors}\n\n' + ), + ) def save_normal(self): self.blender_ui_reset() @@ -184,11 +276,11 @@ def save_normal(self): # could not save for some reason; let the artist know! self.alert_user( title='Could not save', - message='Could not save blend file.\n\n%s' % (str(e)), - level='warning' + message=f'Could not save blend file.\n\nError message: "{e}"', + level='warning', ) self.blender_ui_set() # note: filepath might not be set until after save filepath = os.path.abspath(bpy.data.filepath) - dprint('saved to %s' % filepath) + print(f'saved: {filepath}') diff --git a/retopoflow/rf/rf_sources.py b/retopoflow/rf/rf_sources.py index 576bfb9ff..83ca0e04d 100644 --- a/retopoflow/rf/rf_sources.py +++ b/retopoflow/rf/rf_sources.py @@ -43,7 +43,7 @@ class RetopoFlow_Sources: def setup_sources(self): ''' find all valid source objects, which are mesh objects that are visible and not active ''' print(' rfsources...') - self.rfsources = [RFSource.new(src) for src in self.src_objects] + self.rfsources = [RFSource.new(src) for src in self.get_sources()] print(' bboxes...') self.sources_bbox = BBox.merge([rfs.get_bbox() for rfs in self.rfsources]) dprint('%d sources found' % len(self.rfsources)) diff --git a/retopoflow/rf/rf_target.py b/retopoflow/rf/rf_target.py index 860714aef..8da487740 100644 --- a/retopoflow/rf/rf_target.py +++ b/retopoflow/rf/rf_target.py @@ -49,8 +49,9 @@ class RetopoFlow_Target: @profiler.function def setup_target(self): ''' target is the active object. must be selected and visible ''' - assert self.tar_object, 'Could not find valid target?' - self.rftarget = RFTarget.new(self.tar_object, self.unit_scaling_factor) + tar_object = self.get_target() + assert tar_object, 'Could not find valid target?' + self.rftarget = RFTarget.new(tar_object, self.unit_scaling_factor) opts = visualization.get_target_settings() self.rftarget_draw = RFMeshRender.new(self.rftarget, opts) self.rftarget_version = None @@ -106,8 +107,7 @@ def teardown_target(self): def done_target(self): del self.rftarget_draw del self.rftarget - self.tar_object.to_mesh_clear() - del self.tar_object + self.get_target().to_mesh_clear() ######################################### diff --git a/retopoflow/rf/rf_ui_alert.py b/retopoflow/rf/rf_ui_alert.py index 8135398cb..80c76a1c9 100644 --- a/retopoflow/rf/rf_ui_alert.py +++ b/retopoflow/rf/rf_ui_alert.py @@ -101,13 +101,18 @@ class RetopoFlow_UI_Alert: @CookieCutter.Exception_Callback def handle_exception(self, e): - print('RF_UI.handle_exception', e) + print('RetopoFlow_UI_Alert.handle_exception', e) if False: for entry in inspect.stack(): print(f' {entry}') message,h = Globals.debugger.get_exception_info_and_hash() message = '\n'.join(f'- {l}' for l in message.splitlines()) - self.alert_user(title='Exception caught', message=message, level='exception', msghash=h) + self.alert_user( + title='Exception caught', + message=message, + level='exception', + msghash=h, + ) if hasattr(self, 'rftool'): self.rftool._reset() def alert_user(self, message=None, title=None, level=None, msghash=None): @@ -239,6 +244,8 @@ def go(): show_quit = True darken = True elif level in {'assert', 'exception'}: + self.save_emergency() # make an emergency save! + if level == 'assert': title = 'Assert Error' + (f': {title}' if title else '!') desc = 'An internal assertion has failed.' diff --git a/retopoflow/rfmesh/rfmesh.py b/retopoflow/rfmesh/rfmesh.py index 85cb9a63b..09dddd788 100644 --- a/retopoflow/rfmesh/rfmesh.py +++ b/retopoflow/rfmesh/rfmesh.py @@ -143,7 +143,7 @@ def __setup__( self.triangulate() for bmv in self.bme.verts: - if bmv.link_faces: + if not bmv.is_wire: bmv.normal_update() # setup finishing @@ -1544,31 +1544,47 @@ def commit(self): def cancel(self): self.restore_state() + def clean(self): super().clean() + version = self.get_version() if self.editmesh_version == version: return self.editmesh_version = version - # print('CLEANING RFTARGET') - - # bpy.ops.object.editmode_toggle() - self.bme.to_mesh(self.obj.data) - # bpy.ops.object.editmode_toggle() - - # bmesh.update_edit_mesh(self.obj.data) + try: + self._clean_mesh() + self._clean_selection() + self._clean_mirror() + self._clean_displace() + except Exception as e: + print(f'Caught Exception while trying to clean RFTarget: {e}') + self.handle_exception(e) + + def _clean_mesh(self): + prev_mesh = self.obj.data + prev_mesh_name = prev_mesh.name + new_mesh = self.obj.data.copy() + self.bme.to_mesh(new_mesh) + self.obj.data = new_mesh + bpy.data.meshes.remove(prev_mesh) + new_mesh.name = prev_mesh_name + + def _clean_selection(self): for bmv,emv in zip(self.bme.verts, self.obj.data.vertices): emv.select = bmv.select for bme,eme in zip(self.bme.edges, self.obj.data.edges): eme.select = bme.select for bmf,emf in zip(self.bme.faces, self.obj.data.polygons): emf.select = bmf.select + + def _clean_mirror(self): self.mirror_mod.write() - self.clean_displace() - def clean_displace(self): + def _clean_displace(self): self.displace_mod.strength = self.displace_strength + 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) @@ -1848,7 +1864,7 @@ def flip_face_normals(self): bmf.normal_flip() for bmv in bmf.verts: verts.add(bmv) for bmv in verts: - if bmv.link_faces: + if not bmv.is_wire: bmv.normal_update() self.dirty() From 36d09957de0d7cb657eda83b2f70c476d6c6beae Mon Sep 17 00:00:00 2001 From: jon denning Date: Mon, 2 May 2022 06:40:30 -0400 Subject: [PATCH 06/17] improved clip auto adjust (issue #1116), debugged ui view clip auto adjust now has options for multipliers and clamping. these are exposed in the options. a warning shows if the ratio of max / min is too great, which can cause weird snapping issues (see issue merged in UI code changes from another project and debugged some flow updating issues. still more work to be done here. --- addon_common/common/markdown.py | 39 ++++++---- .../common/shaders/bmesh_render_edges.glsl | 2 +- .../common/shaders/bmesh_render_faces.glsl | 2 +- .../common/shaders/bmesh_render_verts.glsl | 2 +- addon_common/common/ui_core.py | 71 ++++++++++++++---- addon_common/common/ui_document.py | 8 ++- addon_common/common/ui_elements.py | 72 +++++++++++++++++-- addon_common/common/ui_layout.py | 1 + addon_common/common/ui_markdown.py | 60 ++++++++++------ addon_common/common/ui_properties.py | 13 ++++ addon_common/common/ui_styling.py | 1 + addon_common/common/ui_utilities.py | 1 + config/options.py | 14 ++-- config/ui.css | 5 ++ help/warnings.md | 4 +- help/welcome.md | 4 +- retopoflow/rf/options_dialog.html | 48 ++++++++++--- retopoflow/rf/rf_helpsystem.py | 1 + retopoflow/rf/rf_spaces.py | 36 ++++++++++ retopoflow/rf/rf_states.py | 29 -------- 20 files changed, 307 insertions(+), 106 deletions(-) diff --git a/addon_common/common/markdown.py b/addon_common/common/markdown.py index 098f1d98d..dee7a6f5b 100644 --- a/addon_common/common/markdown.py +++ b/addon_common/common/markdown.py @@ -35,30 +35,39 @@ class Markdown: # markdown inline inline_tests = { - 'br': re.compile(r'
*'), - 'img': re.compile(r'!\[(?P[^\]]*)\]\((?P[^) ]+)(?P