Adornment mislaignment issue

import * as go from "gojs";
import { getFillColor } from "./template-utils";

const $ = go.GraphObject.make;

export const circleAnimationAdornment = (state = "default") => {
  const $ = go.GraphObject.make;
  const color = getFillColor(state);

  return $(
    go.Adornment,
    "Spot",
    {
      name: "ANIMATION_ADORNMENT",
      layerName: "Tool",
    },
    $(go.Placeholder),
    $(go.Shape, "Circle", {
      name: "GLOW_EFFECT",
      width: 110,
      height: 110,
      stroke: color,
      strokeWidth: 6,
      fill: "transparent",
      opacity: 0.5,
    }),
    $(go.Shape, "Circle", {
      name: "RIPPLE_EFFECT",
      width: 100,
      height: 100,
      stroke: color,
      strokeWidth: 3,
      fill: "transparent",
      opacity: 0.7,
    })
  );
};

import * as go from "gojs";
import { FROM_PORT, SELECTION_ADORNMENT_COLOR, TO_PORT } from "../constants";
import {
  hideAddNodeAdornment,
  showAddNodeAdornment,
  zoomInNodeIcon,
  zoomOutNodeIcon,
} from "./template-utils";
import "./shapes";

const $ = go.GraphObject.make;

export const circleNodeTemplate = $(
  go.Node,
  {
    isShadowed: true,
    shadowBlur: 12,
    shadowOffset: new go.Point(0, 6),
    shadowColor: "rgba(122,124, 141, 0.2)",
    selectionAdornmentTemplate: $(
      go.Adornment,
      "Spot",
      {
        name: "SELECTIONADORNMENTTEMPLATEGO",
        zOrder: 99, // zOrder should be less than zOrder of AddNodeAdornment
        mouseEnter: (e, adornment) => {
          zoomInNodeIcon(adornment.adornedObject.part);
          showAddNodeAdornment(adornment.adornedObject.part);
        },
        mouseLeave: (e, adornment, nextObj) => {
          zoomOutNodeIcon(adornment.adornedObject.part);
          hideAddNodeAdornment(adornment.adornedObject.part, nextObj);
        },
      },
      $(go.Placeholder),
      $(go.Shape, "Circle", {
        strokeWidth: 5,
        stroke: SELECTION_ADORNMENT_COLOR,
        width: 95,
        height: 95,
        fill: "transparent",
      })
    ),
    selectionObjectName: "SELECTIONADORNMENTGO",
  },
  $(
    go.Panel,
    "Spot",
    $(
      go.Panel,
      "Spot",
      {
        mouseEnter: (e, node) => {
          zoomInNodeIcon(node);
          showAddNodeAdornment(node);
        },
        mouseLeave: (e, node, nextObj) => {
          zoomOutNodeIcon(node);
          hideAddNodeAdornment(node, nextObj);
        },
      },
      $(go.Shape, "Circle", {
        width: 100,
        height: 100,
        fill: "white",
        stroke: "#E4E5E8",
        name: "SELECTIONADORNMENTGO",
      }),
      $(
        go.Panel,
        "Spot",
        { isClipping: true },
        $(go.Shape, "Circle", {
          width: 75,
          height: 75,
          fill: "transparent",
          stroke: "transparent",
        }),
        $(
          go.Picture,
          { width: 45, height: 45, name: "NODEICON" },
          new go.Binding("source", "_src")
        )
      ),
      $(go.Shape, "Circle", {
        cursor: "pointer",
        fill: "transparent",
        stroke: "transparent",
        width: 1,
        height: 1,
        portId: TO_PORT,
        alignment: go.Spot.Left,
        toSpot: go.Spot.Left,
        toLinkable: true,
      }),
      $(go.Shape, "Circle", {
        fill: "transparent",
        stroke: "transparent",
        width: 1,
        height: 1,
        portId: FROM_PORT,
        alignment: go.Spot.Right,
        fromSpot: go.Spot.Right,
        fromLinkable: true,
        name: "FROMPORTGO",
      }),
      $(go.Shape, "Circle", {
        width: 100,
        height: 100,
        fill: "#263238",
        opacity: 0.2,
        name: "HOVEROVERLAY",
        stroke: "transparent",
        visible: false,
      }),
      $(go.Shape, "Circle", {
        name: "CENTER_ANCHOR",
        width: 5,
        height: 5,
        stroke: "red", // ❗ temporary to debug — red dot
        fill: "red",
        alignment: go.Spot.Center,
      })
    ),
    $(
      go.Panel,
      "Vertical",
      {
        alignment: new go.Spot(0.5, 1, 0, 32),
        width: 195,
        // define a tooltip for each node
        toolTip: new go.Adornment(
          "Auto", // that has several labels around it
          { background: "black" }
        ) // avoid hiding tooltip when mouse moves
          .add(
            // new go.Placeholder(), // placeholder will be a bit bigger than node
            new go.TextBlock({ margin: 4, stroke: "white" }).bind(
              "text",
              "hoverDescription"
            )
          )
          .bind("visible", "", (data) => data.hoverDescription?.length > 0), // end Adornment
      },
      $(
        go.TextBlock,
        {
          font: "16px Inter",
          height: 30,
          spacingAbove: 10,
          spacingBelow: 10,
          overflow: go.TextOverflow.Ellipsis,
        },
        new go.Binding("text", "description"),
        new go.Binding("visible", "", (data) => !!data.description)
      ),
      $(
        go.TextBlock,
        {
          font: "14px Inter",
          height: 30,
          overflow: go.TextOverflow.Ellipsis,
        },
        new go.Binding("text", "name"),
        new go.Binding("font", "", (data) => {
          return data.description ? "14px Inter" : "16px Inter";
        }),
        new go.Binding("spacingAbove", "", (data) => {
          return data.description ? 0 : 10;
        }),
        new go.Binding("spacingBelow", "", (data) => {
          return data.description ? 0 : 10;
        })
      ),
      $(
        go.Panel,
        "Spot",
        {
          name: "NODENUMBER",
        },
        $(go.Shape, "CapsuleH", {
          fill: "#cfd8dc",
          stroke: "transparent",
          // strokeWidth: 1,
          width: 30,
          height: 25,
        }),
        $(
          go.TextBlock,
          { stroke: "black" },
          new go.Binding("text", "nodeNumber")
        ),
        new go.Binding("visible", "", (data) => !!data.nodeNumber)
      )
    ),
    $(
      go.Panel,
      "Spot",
      {
        alignment: new go.Spot(0.8, 0.1),
        name: "ERRORNODEGO",
        isActionable: true,
        cursor: "pointer",
      },
      $(
        go.Shape,
        "CapsuleH",
        {
          fill: "red",
          stroke: "white",
          strokeWidth: 3,
          width: 30,
          height: 25,
        },
        new go.Binding("fill", "", (data) => {
          return data?.warnings?.length > 0 ? "#fb8c00" : "red";
        })
      ),
      $(
        go.TextBlock,
        { stroke: "white" },
        new go.Binding(
          "text",
          "",
          (data) => (data?.errors?.length || 0) + (data.warnings?.length || 0)
        )
      ),
      $(go.Shape, "CapsuleH", {
        fill: "transparent",
        stroke: "transparent",
        width: 30,
        height: 25,
        mouseEnter: (e, thisObj) => {
          thisObj.fill = "rgba(38, 50, 56, 0.3)";
        },
        mouseLeave: (e, thisObj) => {
          thisObj.fill = "transparent";
        },
      }),
      new go.Binding("visible", "", (data) => {
        return data?.errors?.length > 0 || data?.warnings?.length > 0;
      })
    )
  ),
  new go.Binding("location", "location", go.Point.parse).makeTwoWay(
    go.Point.stringify
  )
);

/**
 * Stop animation and restore stroke based on node's final state.
 * @param {go.Node} node
 */
export const stopNodeAnimation = (node) => {
  if (!node || !node.diagram) return;

  const diagram = node.diagram;

  // 🛑 Stop animation
  if (node._runningAnimation) {
    node._runningAnimation.stop();
    node._runningAnimation = null;
  }

  // 🧼 Remove animation adornment
  const adornment = node.findAdornment("ANIMATION_ADORNMENT");
  if (adornment) diagram.remove(adornment);

  // ✅ Apply stroke color to main shape based on stored state
  const state = node._effectiveState;
  const color = getFillColor(state);
  const shape = node.findObject("SELECTIONADORNMENTGO");

  if (shape && color) {
    shape.stroke = color;
  }
};

/**
 * Animate node with ripple/glow, using a tool-layer adornment.
 * @param {go.Node} node
 * @param {string} state
 * @param {boolean} isAborted
 */
export const animateNode = (node, state, isAborted = false) => {
  if (!node || !node.diagram) return;

  const diagram = node.diagram;
  const effectiveState = isAborted ? "aborted" : state;
  // const strokeColor = getFillColor(effectiveState);
  const runOnce = ["end_node", "error", "aborted"].includes(effectiveState);

  // Save the state on the node for later stroke restoration
  node._effectiveState = effectiveState;

  // 🧼 Remove existing animation adornment if any
  const oldAdornment = node.findAdornment("ANIMATION_ADORNMENT");
  if (oldAdornment) diagram.remove(oldAdornment);

  // ✅ Create new animation adornment
  const adornment = circleAnimationAdornment(effectiveState);
  adornment.adornedObject = node;
  diagram.add(adornment);

  const anchor = node.findObject("CENTER_ANCHOR");
  if (anchor) {
    const center = anchor.getDocumentPoint(go.Spot.Center);
    adornment.location = center; // 🎯 place (0,0) of adornment here
  } else {
    adornment.location = node.getDocumentPoint(go.Spot.Center);
  }

  // 🔁 Animate
  const glow = adornment.findObject("GLOW_EFFECT");
  const ripple = adornment.findObject("RIPPLE_EFFECT");
  if (!glow || !ripple) return;

  const animation = new go.Animation();
  animation.duration = 1000;
  animation.reversible = true;
  animation.runCount = runOnce ? 1 : Number.POSITIVE_INFINITY;

  animation.add(glow, "opacity", 0.5, 0.2);
  animation.add(ripple, "width", 100, 200);
  animation.add(ripple, "height", 100, 200);
  animation.add(ripple, "opacity", 0.7, 0);

  node._runningAnimation = animation;

  if (runOnce) {
    animation.finished = () => stopNodeAnimation(node);
  }
  diagram.commandHandler.scrollToPart(node);
  animation.start();
};

hey Walter,

Above is the code for my circlNodeTemplate and circleNodeAdornment (which is shown when animate node is called)

However, I am facing an issue with placing the adornment over the circle (see screenshot)

can u please help to fix this issue?

I can’t – I don’t know what these do:

function hideAddNodeAdornment() {}
function showAddNodeAdornment() {}
function zoomInNodeIcon() {}
function zoomOutNodeIcon() {}

And what model data are you using?

export const zoomInNodeIcon = (node) => {
  const animation = new go.Animation();
  animation.duration = 300;
  const iconPanel = node.findObject("NODEICON");
  const hoverPanel = node.findObject("HOVEROVERLAY");
  hoverPanel.visible = true;
  animation.add(iconPanel, "scale", 1, 1.1);
  animation.start();
};
export const zoomOutNodeIcon = (node) => {
  const animation = new go.Animation();
  animation.duration = 300;
  const iconPanel = node.findObject("NODEICON");
  const hoverPanel = node.findObject("HOVEROVERLAY");
  hoverPanel.visible = false;
  animation.add(iconPanel, "scale", 1.1, 1);
  animation.start();
};
const removeAdornment = (node) => {
  node.part.removeAdornment("ADDNODEADORNMENT");
  addNodeAdornment.adornedObject = null;
};
export const showAddNodeAdornment = (node) => {
  const animation = new go.Animation();
  animation.duration = 300;
  const mainNodeObject = node.part.findObject("FROMPORTGO");
  if (!addNodeAdornment.adornedObject) {
    addNodeAdornment.adornedObject = mainNodeObject;
    addNodeAdornment.mouseLeave = () => {
      removeAdornment(node);
    };
    node.part.addAdornment("ADDNODEADORNMENT", addNodeAdornment);
  }
  animation.add(addNodeAdornment, "scale", 0.3, 1);
  animation.start();
};
export const hideAddNodeAdornment = (node, nextObj) => {
  if (
    nextObj?.part.name !== "ADDNODEADORNMENTTEMPLATEGO" &&
    nextObj?.part.name !== "SELECTIONADORNMENTTEMPLATEGO"
  ) {
    removeAdornment(node);
  }
};

These functions basically show a + icon at the right edge of the Node and have nothing to do with the misaligned Shapes.

Model:

d.model = new go.GraphLinksModel({
        linkKeyProperty: "key",
        nodeCategoryProperty: "template",
      });

I want that the GLOW_EFFECT and RIPPLE_EFFECT are over the CIrcle Shape.

I found it confusing trying to understand your code. If those functions weren’t important, why did you show them in your template?

The Adornment has a Placeholder, and your circles are centered about that Placeholder, so you want the Adornment.adornedObject to be the thing that is adorned, not the whole Node. And you don’t need to try to set the Adornment.location, because that is done automatically by the presence of the Placeholder.

  // ✅ Create new animation adornment
  const adornment = circleAnimationAdornment(effectiveState);
  adornment.adornedObject = node.findObject("SELECTIONADORNMENTGO");
  diagram.add(adornment);

  // const anchor = node.findObject("CENTER_ANCHOR");
  // if (anchor) {
  //   const center = anchor.getDocumentPoint(go.Spot.Center);
  //   adornment.location = center; // 🎯 place (0,0) of adornment here
  // } else {
  //   adornment.location = node.getDocumentPoint(go.Spot.Center);
  // }