From 9dffe592533d254636e0cb31d20f10ec01baa61c Mon Sep 17 00:00:00 2001 From: bonjorno7 Date: Sun, 19 Nov 2023 18:58:08 +0100 Subject: [PATCH 1/6] Add context manager for safe constraints You add the constraints as normal, and when the context manager exits it checks if the sketch still solves. If it doesn't, it removes the last constraint and checks again. This continues until either the sketch solves, or all the constraints that were added inside the context manager are removed. The fact that the loop checks whether the sketch solves one more time after the list of new constraints is empty is deliberate; it makes sure the sketch is up to date. The loop doesn't check whether the list is empty, because when it's empty the sketch should solve (hence the check to make sure it solved before any of the constraints were added). --- operators/utilities.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/operators/utilities.py b/operators/utilities.py index 495a219c..5566b9e8 100644 --- a/operators/utilities.py +++ b/operators/utilities.py @@ -1,4 +1,5 @@ import logging +from contextlib import contextmanager import bpy from bpy.types import Context, Operator @@ -7,6 +8,9 @@ from .. import global_data from ..declarations import GizmoGroups, WorkSpaceTools from ..converters import update_convertor_geometry +from ..model.group_constraints import SlvsConstraints +from ..model.sketch import SlvsSketch +from ..solver import solve_system from ..utilities.preferences import use_experimental, get_prefs from ..utilities.data_handling import entities_3d @@ -201,3 +205,25 @@ def select_target_ob(context, sketch): if target_ob.name in context.view_layer.objects: target_ob.select_set(True) context.view_layer.objects.active = target_ob + + +@contextmanager +def safe_constraints(context: Context, sketch: SlvsSketch = None, constraints: SlvsConstraints = None): + sketch = sketch or context.scene.sketcher.active_sketch + constraints = constraints or context.scene.sketcher.constraints + + solve = solve_system(context, sketch) + count = len(list(constraints.all)) + + try: + yield constraints + + finally: + # Don't bother if the sketch can't be solved. + if not solve: + return + + # Remove the newest constraint until the sketch solves. + new = list(constraints.all)[count:] + while not solve_system(context, sketch): + constraints.remove(new.pop()) From aa1f2839347808216cf22bad03756a3b65e807b1 Mon Sep 17 00:00:00 2001 From: bonjorno7 Date: Sun, 19 Nov 2023 19:01:37 +0100 Subject: [PATCH 2/6] Use safe_constraints for add_line_2d This operator was the simplest to add it to, and serves as an example of how to use the context manager. --- operators/add_line_2d.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/operators/add_line_2d.py b/operators/add_line_2d.py index eca58bee..5590e4d0 100644 --- a/operators/add_line_2d.py +++ b/operators/add_line_2d.py @@ -12,7 +12,7 @@ from ..solver import solve_system from .base_2d import Operator2d from .constants import types_point_2d -from .utilities import ignore_hover +from .utilities import ignore_hover, safe_constraints logger = logging.getLogger(__name__) @@ -54,16 +54,16 @@ def main(self, context: Context): self.target.construction = True # auto vertical/horizontal constraint - constraints = context.scene.sketcher.constraints vec_dir = self.target.direction_vec() if vec_dir.length: angle = vec_dir.angle(Vector((1, 0))) - threshold = 0.1 - if angle < threshold or angle > HALF_TURN - threshold: - constraints.add_horizontal(self.target, sketch=self.sketch) - elif (QUARTER_TURN - threshold) < angle < (QUARTER_TURN + threshold): - constraints.add_vertical(self.target, sketch=self.sketch) + + with safe_constraints(context, sketch=self.sketch) as constraints: + if angle < threshold or angle > HALF_TURN - threshold: + constraints.add_horizontal(self.target, sketch=self.sketch) + elif (QUARTER_TURN - threshold) < angle < (QUARTER_TURN + threshold): + constraints.add_vertical(self.target, sketch=self.sketch) ignore_hover(self.target) return True From e1063b29a6fd83809a294c28664356d68ca51727 Mon Sep 17 00:00:00 2001 From: bonjorno7 Date: Mon, 20 Nov 2023 23:27:20 +0100 Subject: [PATCH 3/6] Don't rely on order for safe_constraints Instead of removing the newest constraint until the sketch solves, it just removes all of the new constraints if the sketch fails to solve. It calls solve again at the end to make sure it's updated. --- operators/utilities.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/operators/utilities.py b/operators/utilities.py index 5566b9e8..616e979f 100644 --- a/operators/utilities.py +++ b/operators/utilities.py @@ -212,18 +212,21 @@ def safe_constraints(context: Context, sketch: SlvsSketch = None, constraints: S sketch = sketch or context.scene.sketcher.active_sketch constraints = constraints or context.scene.sketcher.constraints - solve = solve_system(context, sketch) - count = len(list(constraints.all)) + solvable = sketch.solver_state == "OKAY" + old = set(constraints.all) try: yield constraints finally: - # Don't bother if the sketch can't be solved. - if not solve: + if not solvable: return - # Remove the newest constraint until the sketch solves. - new = list(constraints.all)[count:] - while not solve_system(context, sketch): - constraints.remove(new.pop()) + # TODO: Remove only conflicting constraints. + if not sketch.solve(context): + new = set(constraints.all) - old + + for constraint in new: + constraints.remove(constraint) + + sketch.solve(context) From f0dc384c5e3b6f10c29f7698dd358ba1396e9a8e Mon Sep 17 00:00:00 2001 From: bonjorno7 Date: Wed, 22 Nov 2023 16:34:22 +0100 Subject: [PATCH 4/6] Add update_entities option for Solver.solve Also added the report and update_entities arguments to solve_system and SlvsSketch.solve, so they're available regardless of how you call solve. --- model/sketch.py | 4 ++-- solver.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/model/sketch.py b/model/sketch.py index c0b5a81a..1180f8ad 100644 --- a/model/sketch.py +++ b/model/sketch.py @@ -100,8 +100,8 @@ def is_visible(self, context): def get_solver_state(self): return bpyEnum(global_data.solver_state_items, identifier=self.solver_state) - def solve(self, context): - return solve_system(context, sketch=self) + def solve(self, context, report=True, update_entities=True): + return solve_system(context, sketch=self, report=report, update_entities=update_entities) @classmethod def is_sketch(cls): diff --git a/solver.py b/solver.py index f95645c6..09ee6580 100644 --- a/solver.py +++ b/solver.py @@ -192,7 +192,7 @@ def needs_update(self, e): # TODO: skip entities that aren't in active group return True - def solve(self, report=True): + def solve(self, report=True, update_entities=True): self.report = report self._init_slvs_data() @@ -249,11 +249,12 @@ def _get_msg_failed(): logger.debug(_get_msg_failed()) # Update entities from solver - for e in self.entities: - if not self.needs_update(e): - continue + if update_entities: + for e in self.entities: + if not self.needs_update(e): + continue - e.update_from_slvs(self.solvesys) + e.update_from_slvs(self.solvesys) def _get_msg_update(): msg = "Update entities from solver:" @@ -269,6 +270,6 @@ def _get_msg_update(): return self.ok -def solve_system(context, sketch=None): +def solve_system(context, sketch=None, report=True, update_entities=True): solver = Solver(context, sketch) - return solver.solve() + return solver.solve(report=report, update_entities=update_entities) From 5f07fba0dbfcb47dec48e92c4903134489f68ad9 Mon Sep 17 00:00:00 2001 From: bonjorno7 Date: Wed, 22 Nov 2023 16:37:23 +0100 Subject: [PATCH 5/6] Remove only failed constraints in safe_constraints There's still two solve calls, but now they don't update entities at least. The first call is to see if any constraints failed, the second to update the UI after removing those constraints (otherwise it says "Inconsistent" during and after the operation). --- operators/utilities.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/operators/utilities.py b/operators/utilities.py index 616e979f..785944ee 100644 --- a/operators/utilities.py +++ b/operators/utilities.py @@ -8,9 +8,9 @@ from .. import global_data from ..declarations import GizmoGroups, WorkSpaceTools from ..converters import update_convertor_geometry +from ..model.base_constraint import GenericConstraint from ..model.group_constraints import SlvsConstraints from ..model.sketch import SlvsSketch -from ..solver import solve_system from ..utilities.preferences import use_experimental, get_prefs from ..utilities.data_handling import entities_3d @@ -213,7 +213,7 @@ def safe_constraints(context: Context, sketch: SlvsSketch = None, constraints: S constraints = constraints or context.scene.sketcher.constraints solvable = sketch.solver_state == "OKAY" - old = set(constraints.all) + old: set[GenericConstraint] = set(constraints.all) try: yield constraints @@ -222,11 +222,11 @@ def safe_constraints(context: Context, sketch: SlvsSketch = None, constraints: S if not solvable: return - # TODO: Remove only conflicting constraints. - if not sketch.solve(context): - new = set(constraints.all) - old + if not sketch.solve(context, update_entities=False): + new: set[GenericConstraint] = set(constraints.all) - old for constraint in new: - constraints.remove(constraint) + if constraint.failed: + constraints.remove(constraint) - sketch.solve(context) + sketch.solve(context, update_entities=False) From 1c74d60b663ac047286a275eb1060a8791f689c9 Mon Sep 17 00:00:00 2001 From: bonjorno7 Date: Wed, 22 Nov 2023 16:46:24 +0100 Subject: [PATCH 6/6] Add remove_only_failed option to safe_constraints False by default, meaning it will remove all new constraints if any of them failed. If set to True, it will only remove new constraints that failed, and keep successful new constraints. --- operators/utilities.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/operators/utilities.py b/operators/utilities.py index 785944ee..a3a4aa62 100644 --- a/operators/utilities.py +++ b/operators/utilities.py @@ -208,7 +208,12 @@ def select_target_ob(context, sketch): @contextmanager -def safe_constraints(context: Context, sketch: SlvsSketch = None, constraints: SlvsConstraints = None): +def safe_constraints( + context: Context, + sketch: SlvsSketch = None, + constraints: SlvsConstraints = None, + remove_only_failed=False, +): sketch = sketch or context.scene.sketcher.active_sketch constraints = constraints or context.scene.sketcher.constraints @@ -226,7 +231,7 @@ def safe_constraints(context: Context, sketch: SlvsSketch = None, constraints: S new: set[GenericConstraint] = set(constraints.all) - old for constraint in new: - if constraint.failed: + if constraint.failed or not remove_only_failed: constraints.remove(constraint) sketch.solve(context, update_entities=False)