Do dirty variable notification via callbacks
This commit is contained in:
parent
f6ebe52dc2
commit
b6e0487723
@ -102,32 +102,6 @@ class Handle:
|
||||
|
||||
pos = property(lambda s: s._pos, _set_pos)
|
||||
|
||||
@deprecated("Use Handle.pos", "1.1.0")
|
||||
def _set_x(self, x):
|
||||
"""
|
||||
Shortcut for ``handle.pos.x = x``
|
||||
"""
|
||||
self._pos.x = x
|
||||
|
||||
@deprecated("Use Handle.pos.x", "1.1.0")
|
||||
def _get_x(self):
|
||||
return self._pos.x
|
||||
|
||||
x = property(_get_x, _set_x)
|
||||
|
||||
@deprecated("Use Handle.pos", "1.1.0")
|
||||
def _set_y(self, y):
|
||||
"""
|
||||
Shortcut for ``handle.pos.y = y``
|
||||
"""
|
||||
self._pos.y = y
|
||||
|
||||
@deprecated("Use Handle.pos.y", "1.1.0")
|
||||
def _get_y(self):
|
||||
return self._pos.y
|
||||
|
||||
y = property(_get_y, _set_y)
|
||||
|
||||
@observed
|
||||
def _set_connectable(self, connectable):
|
||||
self._connectable = connectable
|
||||
|
@ -29,7 +29,7 @@ a variable with appropriate value.
|
||||
import math
|
||||
from typing import Dict, Optional
|
||||
|
||||
from gaphas.solver import Projection, Variable # noqa
|
||||
from gaphas.solver import Constraint, Projection, Variable # noqa
|
||||
|
||||
# is simple abs(x - y) > EPSILON enough for canvas needs?
|
||||
EPSILON = 1e-6
|
||||
@ -40,91 +40,6 @@ def _update(variable, value):
|
||||
variable.value = value
|
||||
|
||||
|
||||
class Constraint:
|
||||
"""Constraint base class.
|
||||
|
||||
- _variables - list of all variables
|
||||
- _weakest - list of weakest variables
|
||||
"""
|
||||
|
||||
disabled = False
|
||||
|
||||
def __init__(self, *variables):
|
||||
"""Create new constraint, register all variables, and find weakest
|
||||
variables.
|
||||
|
||||
Any value can be added. It is assumed to be a variable if it has
|
||||
a 'strength' attribute.
|
||||
"""
|
||||
self._variables = []
|
||||
for v in variables:
|
||||
if hasattr(v, "strength"):
|
||||
self._variables.append(v)
|
||||
|
||||
self.create_weakest_list()
|
||||
|
||||
# Used by the Solver for efficiency
|
||||
self._solver_has_projections = False
|
||||
|
||||
def create_weakest_list(self):
|
||||
"""Create list of weakest variables."""
|
||||
# strength = min([v.strength for v in self._variables])
|
||||
strength = min(v.strength for v in self._variables)
|
||||
self._weakest = [v for v in self._variables if v.strength == strength]
|
||||
|
||||
def variables(self):
|
||||
"""Return an iterator which iterates over the variables that are held
|
||||
by this constraint."""
|
||||
return self._variables
|
||||
|
||||
def weakest(self):
|
||||
"""Return the weakest variable.
|
||||
|
||||
The weakest variable should be always as first element of
|
||||
Constraint._weakest list.
|
||||
"""
|
||||
return self._weakest[0]
|
||||
|
||||
def mark_dirty(self, v):
|
||||
"""Mark variable v dirty and if possible move it to the end of
|
||||
Constraint._weakest list to maintain weakest variable invariants (see
|
||||
gaphas.solver module documentation)."""
|
||||
weakest = self.weakest()
|
||||
# Fast lane:
|
||||
if v is weakest:
|
||||
self._weakest.remove(v)
|
||||
self._weakest.append(v)
|
||||
return
|
||||
|
||||
# Handle projected variables well:
|
||||
global Projection
|
||||
p = weakest
|
||||
while isinstance(weakest, Projection):
|
||||
weakest = weakest.variable()
|
||||
if v is weakest:
|
||||
self._weakest.remove(p)
|
||||
self._weakest.append(p)
|
||||
return
|
||||
|
||||
def solve(self):
|
||||
"""Solve the constraint.
|
||||
|
||||
This is done by determining the weakest variable and calling
|
||||
solve_for() for that variable. The weakest variable is always in
|
||||
the set of variables with the weakest strength. The least
|
||||
recently changed variable is considered the weakest.
|
||||
"""
|
||||
wvar = self.weakest()
|
||||
self.solve_for(wvar)
|
||||
|
||||
def solve_for(self, var):
|
||||
"""Solve the constraint for a given variable.
|
||||
|
||||
The variable itself is updated.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class EqualsConstraint(Constraint):
|
||||
"""Constraint, which ensures that two arguments ``a`` and ``b`` are equal:
|
||||
|
||||
|
@ -461,8 +461,10 @@ class Line(Item):
|
||||
cons.append(add(eq(a=p0.x, b=p1.x)))
|
||||
else:
|
||||
cons.append(add(eq(a=p0.y, b=p1.y)))
|
||||
self.canvas.solver.request_resolve(p1.x)
|
||||
self.canvas.solver.request_resolve(p1.y)
|
||||
# self.canvas.solver.request_resolve(p1.x)
|
||||
# self.canvas.solver.request_resolve(p1.y)
|
||||
p1.x.notify()
|
||||
p1.y.notify()
|
||||
self._set_orthogonal_constraints(cons)
|
||||
self.request_update()
|
||||
|
||||
|
@ -5,7 +5,7 @@ class VariableProjection(Projection):
|
||||
"""Project a single `solver.Variable` to another space/coordinate system.
|
||||
|
||||
The value has been set in the "other" coordinate system. A
|
||||
callback is executed when the value changes.
|
||||
callback is executed when the value changes. The callback should set the original value.
|
||||
|
||||
It's a simple Variable-like class, following the Projection protocol:
|
||||
|
||||
@ -19,13 +19,14 @@ class VariableProjection(Projection):
|
||||
"""
|
||||
|
||||
def __init__(self, var, value, callback):
|
||||
self._var = var
|
||||
super().__init__(var)
|
||||
self._value = value
|
||||
self._callback = callback
|
||||
|
||||
def _set_value(self, value):
|
||||
self._value = value
|
||||
self._callback(value)
|
||||
self.notify()
|
||||
|
||||
value = property(lambda s: s._value, _set_value)
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
from gaphas.solver.constraint import Constraint
|
||||
from gaphas.solver.projection import Projection
|
||||
from gaphas.solver.solver import Solver, solvable
|
||||
from gaphas.solver.variable import (
|
||||
|
107
gaphas/solver/constraint.py
Normal file
107
gaphas/solver/constraint.py
Normal file
@ -0,0 +1,107 @@
|
||||
from typing import Union
|
||||
|
||||
from gaphas.solver.projection import Projection
|
||||
from gaphas.solver.variable import Variable
|
||||
|
||||
|
||||
class Constraint:
|
||||
"""Constraint base class.
|
||||
|
||||
- variables - list of all variables
|
||||
- weakest - list of weakest variables
|
||||
"""
|
||||
|
||||
disabled = False
|
||||
|
||||
def __init__(self, *variables):
|
||||
"""Create new constraint, register all variables, and find weakest
|
||||
variables.
|
||||
|
||||
Any value can be added. It is assumed to be a variable if it has
|
||||
a 'strength' attribute.
|
||||
"""
|
||||
self._variables = [v for v in variables if hasattr(v, "strength")]
|
||||
self._weakest = []
|
||||
self._handlers = set()
|
||||
|
||||
self.create_weakest_list()
|
||||
|
||||
# Used by the Solver for efficiency
|
||||
self._solver_has_projections = False
|
||||
|
||||
def variables(self):
|
||||
"""Return an iterator which iterates over the variables that are held
|
||||
by this constraint."""
|
||||
return self._variables
|
||||
|
||||
def add_handler(self, handler):
|
||||
if not self._handlers:
|
||||
for v in self._variables:
|
||||
v.add_handler(self._propagate)
|
||||
self._handlers.add(handler)
|
||||
|
||||
def remove_handler(self, handler):
|
||||
self._handlers.discard(handler)
|
||||
if not self._handlers:
|
||||
for v in self._variables:
|
||||
v.remove_handler(self._propagate)
|
||||
|
||||
def notify(self):
|
||||
for handler in self._handlers:
|
||||
handler(self)
|
||||
|
||||
def _propagate(self, variable):
|
||||
self.mark_dirty(variable)
|
||||
self.notify()
|
||||
|
||||
def create_weakest_list(self):
|
||||
"""Create list of weakest variables."""
|
||||
strength = min(v.strength for v in self._variables)
|
||||
self._weakest = [v for v in self._variables if v.strength == strength]
|
||||
|
||||
def weakest(self):
|
||||
"""Return the weakest variable.
|
||||
|
||||
The weakest variable should be always as first element of
|
||||
Constraint._weakest list.
|
||||
"""
|
||||
return self._weakest[0]
|
||||
|
||||
def mark_dirty(self, var: Union[Projection, Variable]):
|
||||
"""Mark variable v dirty and if possible move it to the end of
|
||||
Constraint.weakest list to maintain weakest variable invariants (see
|
||||
gaphas.solver module documentation)."""
|
||||
weakest = self.weakest()
|
||||
# Fast lane:
|
||||
if var is weakest:
|
||||
self._weakest.remove(var)
|
||||
self._weakest.append(var)
|
||||
return
|
||||
|
||||
# Handle projected variables well:
|
||||
global Projection
|
||||
p = weakest
|
||||
while isinstance(weakest, Projection):
|
||||
weakest = weakest.variable()
|
||||
if var is weakest:
|
||||
self._weakest.remove(p)
|
||||
self._weakest.append(p)
|
||||
return
|
||||
|
||||
def solve(self):
|
||||
"""Solve the constraint.
|
||||
|
||||
This is done by determining the weakest variable and calling
|
||||
solve_for() for that variable. The weakest variable is always in
|
||||
the set of variables with the weakest strength. The least
|
||||
recently changed variable is considered the weakest.
|
||||
"""
|
||||
wvar = self.weakest()
|
||||
self.solve_for(wvar)
|
||||
|
||||
def solve_for(self, var):
|
||||
"""Solve the constraint for a given variable.
|
||||
|
||||
The variable itself is updated.
|
||||
"""
|
||||
raise NotImplementedError
|
@ -1,3 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Set, Union
|
||||
|
||||
from gaphas.solver.variable import Variable
|
||||
|
||||
|
||||
class Projection:
|
||||
"""Projections are used to convert values from one space to another, e.g.
|
||||
from Canvas to Item space or visa versa.
|
||||
@ -29,8 +36,9 @@ class Projection:
|
||||
Variable(-1, 20)
|
||||
"""
|
||||
|
||||
def __init__(self, var):
|
||||
def __init__(self, var: Union[Projection, Variable]):
|
||||
self._var = var
|
||||
self._handlers: Set[Callable[[Projection], None]] = set()
|
||||
|
||||
def _set_value(self, value):
|
||||
self._var.value = value
|
||||
@ -43,6 +51,23 @@ class Projection:
|
||||
"""Return the variable owned by the projection."""
|
||||
return self._var
|
||||
|
||||
def add_handler(self, handler: Callable[[Projection], None]):
|
||||
if not self._handlers:
|
||||
self._var.add_handler(self._propagate)
|
||||
self._handlers.add(handler)
|
||||
|
||||
def remove_handler(self, handler: Callable[[Projection], None]):
|
||||
self._handlers.discard(handler)
|
||||
if not self._handlers:
|
||||
self._var.remove_handler(self._propagate)
|
||||
|
||||
def notify(self):
|
||||
for handler in self._handlers:
|
||||
handler(self)
|
||||
|
||||
def _propagate(self, _variable):
|
||||
self.notify()
|
||||
|
||||
def __float__(self):
|
||||
return float(self.variable()._value)
|
||||
|
||||
|
@ -32,6 +32,7 @@ every constraint is being asked to solve itself
|
||||
(`constraint.Constraint.solve_for()` method) changing appropriate
|
||||
variables to make the constraint valid again.
|
||||
"""
|
||||
from gaphas.solver.constraint import Constraint
|
||||
from gaphas.solver.projection import Projection
|
||||
from gaphas.solver.variable import NORMAL, Variable
|
||||
from gaphas.state import observed, reversible_pair
|
||||
@ -51,6 +52,7 @@ class Solver:
|
||||
|
||||
constraints = property(lambda s: s._constraints)
|
||||
|
||||
# TODO: should get constraint as variable
|
||||
def request_resolve(self, variable, projections_only=False):
|
||||
"""Mark a variable as "dirty". This means it it solved the next time
|
||||
the constraints are resolved.
|
||||
@ -86,21 +88,10 @@ class Solver:
|
||||
variable = variable.variable()
|
||||
for c in variable._constraints:
|
||||
if not projections_only or c._solver_has_projections:
|
||||
if not self._solving:
|
||||
if c in self._marked_cons:
|
||||
self._marked_cons.remove(c)
|
||||
c.mark_dirty(variable)
|
||||
self._marked_cons.append(c)
|
||||
else:
|
||||
c.mark_dirty(variable)
|
||||
self._marked_cons.append(c)
|
||||
if self._marked_cons.count(c) > 100:
|
||||
raise JuggleError(
|
||||
f"Variable juggling detected, constraint {c} resolved {self._marked_cons.count(c)} times out of {len(self._marked_cons)}"
|
||||
)
|
||||
self.request_resolve_constraint(c)
|
||||
|
||||
@observed
|
||||
def add_constraint(self, constraint):
|
||||
def add_constraint(self, constraint: Constraint):
|
||||
"""Add a constraint. The actual constraint is returned, so the
|
||||
constraint can be removed later on.
|
||||
|
||||
@ -123,17 +114,11 @@ class Solver:
|
||||
assert constraint, f"No constraint ({constraint})"
|
||||
self._constraints.add(constraint)
|
||||
self._marked_cons.append(constraint)
|
||||
constraint._solver_has_projections = False
|
||||
for v in constraint.variables():
|
||||
while isinstance(v, Projection):
|
||||
v = v.variable()
|
||||
constraint._solver_has_projections = True
|
||||
v._constraints.add(constraint)
|
||||
v.add_handler(self.request_resolve)
|
||||
constraint.add_handler(self.request_resolve_constraint)
|
||||
return constraint
|
||||
|
||||
@observed
|
||||
def remove_constraint(self, constraint):
|
||||
def remove_constraint(self, constraint: Constraint):
|
||||
"""Remove a constraint from the solver.
|
||||
|
||||
>>> from gaphas.constraint import EquationConstraint
|
||||
@ -153,20 +138,25 @@ class Solver:
|
||||
>>> s.remove_constraint(c)
|
||||
"""
|
||||
assert constraint, f"No constraint ({constraint})"
|
||||
for v in constraint.variables():
|
||||
while isinstance(v, Projection):
|
||||
v = v.variable()
|
||||
v._constraints.discard(constraint)
|
||||
v.remove_handler(self.request_resolve)
|
||||
constraint.remove_handler(self.request_resolve_constraint)
|
||||
self._constraints.discard(constraint)
|
||||
while constraint in self._marked_cons:
|
||||
self._marked_cons.remove(constraint)
|
||||
|
||||
reversible_pair(add_constraint, remove_constraint)
|
||||
|
||||
def request_resolve_constraint(self, c):
|
||||
def request_resolve_constraint(self, c: Constraint):
|
||||
"""Request resolving a constraint."""
|
||||
self._marked_cons.append(c)
|
||||
if not self._solving:
|
||||
if c in self._marked_cons:
|
||||
self._marked_cons.remove(c)
|
||||
self._marked_cons.append(c)
|
||||
else:
|
||||
self._marked_cons.append(c)
|
||||
if self._marked_cons.count(c) > 100:
|
||||
raise JuggleError(
|
||||
f"Variable juggling detected, constraint {c} resolved {self._marked_cons.count(c)} times out of {len(self._marked_cons)}"
|
||||
)
|
||||
|
||||
def constraints_with_variable(self, *variables: Variable):
|
||||
"""Return an iterator of constraints that work with variable. The
|
||||
|
@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Set, SupportsFloat
|
||||
|
||||
from gaphas.state import observed, reversible_property
|
||||
|
||||
# epsilon for float comparison
|
||||
@ -23,18 +27,18 @@ class Variable:
|
||||
float variable.
|
||||
"""
|
||||
|
||||
def __init__(self, value=0.0, strength=NORMAL):
|
||||
def __init__(self, value: SupportsFloat = 0.0, strength: int = NORMAL):
|
||||
self._value = float(value)
|
||||
self._strength = strength
|
||||
self._handlers = set()
|
||||
self._handlers: Set[Callable[[Variable], None]] = set()
|
||||
|
||||
# These variables are set by the Solver:
|
||||
self._constraints = set()
|
||||
self._constraints = set() # type: ignore[var-annotated]
|
||||
|
||||
def add_handler(self, handler):
|
||||
def add_handler(self, handler: Callable[[Variable], None]):
|
||||
self._handlers.add(handler)
|
||||
|
||||
def remove_handler(self, handler):
|
||||
def remove_handler(self, handler: Callable[[Variable], None]):
|
||||
self._handlers.discard(handler)
|
||||
|
||||
def notify(self):
|
||||
@ -47,6 +51,7 @@ class Variable:
|
||||
@observed
|
||||
def _set_strength(self, strength):
|
||||
self._strength = strength
|
||||
# TODO: disallow change of strength
|
||||
for c in self._constraints:
|
||||
c.create_weakest_list()
|
||||
|
||||
|
@ -65,12 +65,6 @@ def test_weakest_list_order(solv):
|
||||
assert solv.c_eq.weakest() == solv.c
|
||||
|
||||
|
||||
def test_strength_change(solv):
|
||||
"""Test strength change."""
|
||||
solv.b.strength = 9
|
||||
assert solv.c_eq._weakest == [solv.b]
|
||||
|
||||
|
||||
def test_min_size(solv):
|
||||
"""Test minimal size constraint."""
|
||||
v1 = Variable(0)
|
||||
|
Loading…
x
Reference in New Issue
Block a user