RescaleTool handleArchetype Bindings not firing in the first selection

Hello!

I faced a strange behavior, when trying to add a binding to the RescalingTool handleArchetype.

I used the Rescaling.html sample and only added a binding

h.bind(new go.Binding(“strokeWidth”, “”, () => {
return 5;
}));

When I select a node for the first time, this binding will be ignored and the strokeWidth will be 1.
After deselection and reselection, the binding fires and the strokeWidth is 5.

Can you tell me, why this happens and what I could do to prevent that?

Here is the full code:

<!DOCTYPE html>
<html lang="en">
<body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/release/go.js"></script>
<p>
  This is a minimalist HTML and JavaScript skeleton of the GoJS Sample
  <a href="https://gojs.net/latest/samples/Rescaling.html">Rescaling.html</a>. It was automatically generated from a button on the sample page,
  and does not contain the full HTML. It is intended as a starting point to adapt for your own usage.
  For many samples, you may need to inspect the
  <a href="https://github.com/NorthwoodsSoftware/GoJS/blob/master/samples/Rescaling.html">full source on Github</a>
  and copy other files or scripts.
</p>
<div id="allSampleContent" class="p-4 w-full">
          
            
            

<script id="code">

class RescalingTool extends go.Tool {
    constructor(init) {
        super();
        this.name = 'Rescaling';
        this._rescaleObjectName = '';
        // internal state
        this._adornedObject = null;
        this._handle = null;
        this.originalPoint = new go.Point();
        this.originalTopLeft = new go.Point();
        this.originalScale = 1.0;
        const h = new go.Shape();
        h.desiredSize = new go.Size(8, 8);
        h.fill = 'lightblue';
        h.stroke = 'dodgerblue';
        h.strokeWidth = 1;
        h.cursor = 'nwse-resize';
        h.bind(new go.Binding("strokeWidth", "", () => {
          return 5;
        }));
        this._handleArchetype = h;
        if (init)
            Object.assign(this, init);
    }
    /**
     * Gets the {@link go.GraphObject} that is being rescaled.
     * This may be the same object as the selected {@link go.Part} or it may be contained within that Part.
     *
     * This property is also settable, but should only be set when overriding functions
     * in RescalingTool, and not during normal operation.
     */
    get adornedObject() {
        return this._adornedObject;
    }
    set adornedObject(val) {
        this._adornedObject = val;
    }
    /**
     * Gets or sets a small GraphObject that is copied as a rescale handle for the selected part.
     * By default this is a {@link go.Shape} that is a small blue square.
     * Setting this property does not raise any events.
     *
     * Here is an example of changing the default handle to be green "X":
     * ```js
     *   tool.handleArchetype =
     *     new go.Shape("XLine",
     *       { width: 8, height: 8, stroke: "green", fill: "transparent" })
     * ```
     */
    get handleArchetype() {
        return this._handleArchetype;
    }
    set handleArchetype(val) {
        this._handleArchetype = val;
    }
    /**
     * This property returns the {@link go.GraphObject} that is the tool handle being dragged by the user.
     * This will be contained by an {@link go.Adornment} whose category is "RescalingTool".
     * Its {@link go.Adornment.adornedObject} is the same as the {@link adornedObject}.
     *
     * This property is also settable, but should only be set either within an override of {@link doActivate}
     * or prior to calling {@link doActivate}.
     */
    get handle() {
        return this._handle;
    }
    set handle(val) {
        this._handle = val;
    }
    /**
     * This property returns the name of the GraphObject that identifies the object to be rescaled by this tool.
     *
     * The default value is the empty string, resulting in the whole Node being rescaled.
     * This property is used by findRescaleObject when calling {@link go.Panel.findObject}.
     */
    get rescaleObjectName() {
        return this._rescaleObjectName;
    }
    set rescaleObjectName(val) {
        this._rescaleObjectName = val;
    }
    /**
     * @param part
     */
    updateAdornments(part) {
        if (part === null || part instanceof go.Link)
            return;
        if (part.isSelected && !this.diagram.isReadOnly) {
            const rescaleObj = this.findRescaleObject(part);
            if (rescaleObj !== null &&
                part.actualBounds.isReal() &&
                part.isVisible() &&
                rescaleObj.actualBounds.isReal() &&
                rescaleObj.isVisibleObject()) {
                let adornment = part.findAdornment(this.name);
                if (adornment === null || adornment.adornedObject !== rescaleObj) {
                    adornment = this.makeAdornment(rescaleObj);
                }
                if (adornment !== null) {
                    adornment.location = rescaleObj.getDocumentPoint(go.Spot.BottomRight);
                    part.addAdornment(this.name, adornment);
                    return;
                }
            }
        }
        part.removeAdornment(this.name);
    }
    /**
     * @param rescaleObj
     */
    makeAdornment(rescaleObj) {
        const adornment = new go.Adornment();
        adornment.type = go.Panel.Position;
        adornment.locationSpot = go.Spot.Center;
        adornment.add(this._handleArchetype.copy());
        adornment.adornedObject = rescaleObj;
        return adornment;
    }
    /**
     * Return the GraphObject to be rescaled by the user.
     */
    findRescaleObject(part) {
        const obj = part.findObject(this.rescaleObjectName);
        if (obj)
            return obj;
        return part;
    }
    /**
     * This tool can start running if the mouse-down happens on a "Rescaling" handle.
     */
    canStart() {
        const diagram = this.diagram;
        if (diagram === null || diagram.isReadOnly)
            return false;
        if (!diagram.lastInput.left)
            return false;
        const h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
        return h !== null;
    }
    /**
     * Activating this tool remembers the {@link handle} that was dragged,
     * the {@link adornedObject} that is being rescaled,
     * starts a transaction, and captures the mouse.
     */
    doActivate() {
        const diagram = this.diagram;
        if (diagram === null)
            return;
        this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
        if (this._handle === null)
            return;
        const ad = this._handle.part;
        this._adornedObject = ad instanceof go.Adornment ? ad.adornedObject : null;
        if (!this._adornedObject)
            return;
        this.originalPoint = this._handle.getDocumentPoint(go.Spot.Center);
        this.originalTopLeft = this._adornedObject.getDocumentPoint(go.Spot.TopLeft);
        this.originalScale = this._adornedObject.scale;
        diagram.isMouseCaptured = true;
        diagram.delaysLayout = true;
        this.startTransaction(this.name);
        this.isActive = true;
    }
    /**
     * Stop the current transaction, forget the {@link handle} and {@link adornedObject}, and release the mouse.
     */
    doDeactivate() {
        const diagram = this.diagram;
        if (diagram === null)
            return;
        this.stopTransaction();
        this._handle = null;
        this._adornedObject = null;
        diagram.isMouseCaptured = false;
        this.isActive = false;
    }
    /**
     * Restore the original {@link go.GraphObject.scale} of the adorned object.
     */
    doCancel() {
        const diagram = this.diagram;
        if (diagram !== null)
            diagram.delaysLayout = false;
        this.scale(this.originalScale);
        this.stopTool();
    }
    /**
     * Call {@link scale} with a new scale determined by the current mouse point.
     * This determines the new scale by calling {@link computeScale}.
     */
    doMouseMove() {
        const diagram = this.diagram;
        if (this.isActive && diagram !== null) {
            const newScale = this.computeScale(diagram.lastInput.documentPoint);
            this.scale(newScale);
        }
    }
    /**
     * Call {@link scale} with a new scale determined by the most recent mouse point,
     * and commit the transaction.
     */
    doMouseUp() {
        const diagram = this.diagram;
        if (this.isActive && diagram !== null) {
            diagram.delaysLayout = false;
            const newScale = this.computeScale(diagram.lastInput.documentPoint);
            this.scale(newScale);
            this.transactionResult = this.name;
        }
        this.stopTool();
    }
    /**
     * Set the {@link go.GraphObject.scale} of the {@link findRescaleObject}.
     * @param newScale
     */
    scale(newScale) {
        if (this._adornedObject !== null) {
            this._adornedObject.scale = newScale;
        }
    }
    /**
     * Compute the new scale given a point.
     *
     * This method is called by both {@link doMouseMove} and {@link doMouseUp}.
     * This method may be overridden.
     * Please read the Introduction page on <a href="../../intro/extensions.html">Extensions</a> for how to override methods and how to call this base method.
     * @param newPoint - in document coordinates
     */
    computeScale(newPoint) {
        const scale = this.originalScale;
        const origdist = Math.sqrt(this.originalPoint.distanceSquaredPoint(this.originalTopLeft));
        const newdist = Math.sqrt(newPoint.distanceSquaredPoint(this.originalTopLeft));
        return scale * (newdist / origdist);
    }
}


  function init() {

    myDiagram = new go.Diagram('myDiagramDiv', {
      'undoManager.isEnabled': true, // enable undo & redo
      layout: new go.TreeLayout()
    });

    // install the RescalingTool as a mouse-down tool
    myDiagram.toolManager.mouseDownTools.add(new RescalingTool());

    myDiagram.nodeTemplate = new go.Node('Auto')
      .add(
        new go.Shape('RoundedRectangle', { strokeWidth: 0 }).bind('fill', 'color'),
        new go.TextBlock({ margin: 8 }).bind('text')
      ).bindTwoWay('scale');

    // but use the default Link template, by not setting Diagram.linkTemplate

    // create the model data that will be represented by Nodes and Links
    myDiagram.model = new go.GraphLinksModel({
      nodeDataArray: [
        { key: 1, text: 'Alpha', color: 'lightblue' },
        { key: 2, text: 'Beta', color: 'orange' },
        { key: 3, text: 'Gamma', color: 'lightgreen' },
        { key: 4, text: 'Delta', color: 'pink' }
      ],
      linkDataArray: [
        { from: 1, to: 2 },
        { from: 1, to: 3 },
        { from: 3, to: 4 }
      ]
  });
  }
  window.addEventListener('DOMContentLoaded', init);
</script>

<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 400px"></div>
  <p>
    Selecting a node will show a rescaling handle that when dragged will modify the node's <a>GraphObject.scale</a>
    property.
  </p>
  <p>
    Just as the <a>ResizingTool</a> changes the <a>GraphObject.desiredSize</a> of an object, and just as the <a>RotatingTool</a> changes the
    <a>GraphObject.angle</a> of an object, the <a>RescalingTool</a> changes the <a>GraphObject.scale</a> of an object.
  </p>
  <p>This extension tool is defined in its own file, as <a href="https://cdn.jsdelivr.net/npm/[email protected]/dist/extensions/RescalingTool.js">RescalingTool.js</a>.</p>
</div>

          
        </div>
</body>
</html>

That was never officially supported behavior, but by coincidence we have already added support for that in the upcoming release, 3.0.14. Hopefully we’ll publish that version very soon.

Ah that’s good to hear!
I really like the feature of handles that adapt to the current diagram scale, so they always have a constant screen size no matter how zoomed in or out. So you can always interact with the diagram parts.

Looking forward to 3.0.14
Thanks for the fast response!

I’ve just tested this using the not-yet-released code:

const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "ViewportBoundsChanged": e => {
        if (e.subject.scale !== e.diagram.scale) {
          const m = e.diagram.model;
          m.commit(m => m.set(m.modelData, "scale", e.diagram.scale), null);
        }
      },
      "undoManager.isEnabled": true
    });

myDiagram.toolManager.mouseDownTools.add(new RescalingTool({
  handleArchetype:
    new go.Shape("Circle", { alignment: go.Spot.BottomRight, width: 10, height: 10, fill: "magenta", cursor: "pointer" })
      .bindModel("scale", "scale", s => 1/s)
}));

That’s a nice feature that allows me to not have to update the target bindings in the zoom diagram listener. But it does not solve the problem with the untriggered binding on first selection, does it?

You can try it when 3.0.14 is released. Alas, we’ve discovered another (unrelated) bug that we ought to fix…