Removing polling checks in guitktk

Updating a rectangular bounding box of a selection was a bit of a hack in guitktk: every frame the selection was checked for changes and the rectangle recreated if needed. In this post, I'll show how to remove that check using "formulas" for parameter values.

In guitktk, when a selected item changed, the selection rectangle is updated.

Selection rectangles

Previously, a new rectangle was created and replaced the old rectangle like so.

children = [
    Node("line", p_start=upper_left, p_end=(lower_right[0], upper_left[1])),
    Node("line", p_start=(lower_right[0], upper_left[1]), p_end=lower_right),
    Node("line", p_start=lower_right, p_end=(upper_left[0], lower_right[1])),
    Node("line", p_start=(upper_left[0], lower_right[1]), p_end=upper_left)]
old_rectangle.replace(children)

The same happened for the large rectangle containing all selections.

if changed:
    if len(self["selection"]) <= 1:
        self["selection_bbox"].replace([])
    else:
        upper_left, lower_right = self["selection"].bbox(self["selection"].combined_transform())
        children = [
            Node("line", p_start=upper_left, p_end=(lower_right[0], upper_left[1])),
            Node("line", p_start=(lower_right[0], upper_left[1]), p_end=lower_right),
            Node("line", p_start=lower_right, p_end=(upper_left[0], lower_right[1])),
            Node("line", p_start=(upper_left[0], lower_right[1]), p_end=upper_left)]
        self["selection_bbox"].replace(children)
    self["selection"].updated = time.time()

While this worked well, this code polls the section every frame and feels out of place. One way around this would be to add callbacks but we'll do a bit better than that.

To avoid this, we'll make use of formulas

def rectangle(**params):
    newnode = Node("path", skip_points=True,
                   p_topleft=exr("`self.parent.corners[0]"),
                   p_botright=exr("`self.parent.corners[1]"),
                   topright=exr("topright(`self.corners, (`self.parent).transform)"),
                   botleft=exr("botleft(`self.corners, (`self.parent).transform)"),
                   children=[
        Node("line", start=exr("`self.parent.topleft"),
             end=exr("`self.parent.topright")),
        Node("line", start=exr("`self.parent.topright"),
             end=exr("`self.parent.botright")),
        Node("line", start=exr("`self.parent.botright"),
             end=exr("`self.parent.botleft")),
        Node("line", start=exr("`self.parent.botleft"),
             end=exr("`self.parent.topleft")),],
                   **params)
    return newnode

This rectangle function is only called once during initialization and is the only child of this selection_bbox group

Node("group", id="selection_bbox",
     stroke_color=(0.5, 0, 0), dash=([5,5],0), skip_points=True,
     children=[rectangle(corners=exc("(`selection).bbox()"))])

exc and exr create formulas/expressions with different reevaluation strategies [recalc]. When a selected object is altered, the node with id selection changes values causing the corners parameter of the selection_bbox's child. corners is set to the selection bounding box's corners as a tuple of arrays (something like, (array([0, 0]), array([100, 200])).

Then parameters of the child that depend on corners are recalculated [recalc]. They are p_topleft, topright, p_botright and botleft. Then, since these values have change, the start and end points of the four lines in this path are updated.

But what happens if no element is selected. Then (`selection).bbox() evaluates to (None, None) and a lot of other values would evalute to None giving many errors when drawn.

Instead, we'll make the selection_bbox group invisible when that happens. In fact, we'll also make it invisible when only one element is selected. This is done by setting is visible parameter to the formula exc("len(`selection) > 1").

Node("group", id="selection_bbox",
     stroke_color=(0.5, 0, 0), dash=([5,5],0), skip_points=True,
     children=[rectangle(corners=exc("(`selection).bbox()"),, 
                         visible=exc("len(`selection) > 1"))]),

Now we have the same outcome as before without polling.

We can then use minor variations of the rectangle function to draw rectangles.

def rectangle2(**params):
    return Node("path",
                corners=exr("(`self.topleft.value, `self.botright.value)"),
                topright=exr("topright(`self.corners, (`self.parent).transform)"),
                botleft=exr("botleft(`self.corners, (`self.parent).transform)"),
                children = [
        Node("line", start=exr("`self.parent.topleft"),
             end=exr("`self.parent.topright")),
        Node("line", start=exr("`self.parent.topright"),
             end=exr("`self.parent.botright")),
        Node("line", start=exr("`self.parent.botright"),
             end=exr("`self.parent.botleft")),
        Node("line", start=exr("`self.parent.botleft"),
             end=exr("`self.parent.topleft")),
                   ], **params)

def add_rectangle():
    doc["drawing"].append(rectangle2(p_topleft=doc["editor.mouse_xy"],
                                     p_botright=doc["editor.mouse_xy"] + P(50, 50)))

Rectangles

Footnotes

[recalc] The time at which recalculation actually occurs varies. exr formulas are (always) reevaluated when the parameter is read. Ex formulas are recalculated when the terms they depend on change value and that value is cached when reading. exc formulas are like Ex but their cache is initialized at the first read (after a value changes).

Further readings

Apparatus - a hybrid graphics editor and programming environment for creating interactive diagrams. It heavily inspired the formulas portion of guitktk.

Posted on Mar 3, 2018

Blog index RSS feed