Quick shift when starting to drag a node

Yeah :).

Different topic but quick question: why when DnD the Diamond, on mouse click and slight mouse move, at first it moves the shape a lot. I’m using the stayInGroup with Grid Enabled. Some coordinates seems not correctly calculated. Any thoughts ?

    // this is a Part.dragComputation function for limiting where a Node may be dragged
    stayInGroup(part, pt, gridpt) {
        // don't constrain top-level nodes
        const grp = part.containingGroup;

        if (grp === null) return gridpt;

        // try to stay within the background Shape of the Group
        const back = grp.resizeObject;

        if (back === null) return gridpt;

        // allow dragging a Node out of a Group if the Shift key is down
        //if (part.diagram.lastInput.shift) return pt;
        const p1 = back.getDocumentPoint(go.Spot.TopLeft);
        const p2 = back.getDocumentPoint(go.Spot.BottomRight);
        const b = part.actualBounds;
        const loc = part.location;

        // no placeholder -- just assume some Margin
        const m = this.GroupMargin;

        // now limit the location appropriately
        const x = Math.max(p1.x + m.left, Math.min(gridpt.x, p2.x - m.right - b.width - 1)) + (loc.x - b.x);
        const y = Math.max(p1.y + m.top, Math.min(gridpt.y, p2.y - m.bottom - b.height - 1)) + (loc.y - b.y);

        return new go.Point(x, y);
    }

cara1

I don’t see that problem in BPMN Editor

Even if I set DraggingTool.isGridSnapEnabled to true, each dragged node snaps to the (default) 10x10 grid during the drag, but the initial snap is never more than needed for the node location to go to the nearest grid point.

Is Node.locationSpot still Spot.Center?

It is not BPMN example anymore. Here’s the actual full code. Node.locationSpot is set.

import * as go from 'gojs/release/go-module';
import 'gojs/extensionsJSM/Figures';
import './Icons';
import { DrawCommandHandler } from './DrawCommandHandler.js';

export default class Diagram {
	diagram = null;

	GroupMargin = new go.Margin(5, 5);

	#gridEnabled = false;

	get gridEnabled() {
		return this.#gridEnabled;
	}

	set gridEnabled(isEnabled) {
		this.#gridEnabled = isEnabled;

		const { diagram } = this;

		diagram.startTransaction('grid');
		diagram.grid.visible = isEnabled;

		const { draggingTool, resizingTool } = diagram.toolManager;

		if (isEnabled) {
			draggingTool.isGridSnapEnabled = true;
			resizingTool.isGridSnapEnabled = true;
		} else {
			draggingTool.isGridSnapEnabled = false;
			resizingTool.isGridSnapEnabled = false;
		}

		diagram.commitTransaction('grid');
	}

	// this is a Part.dragComputation function for limiting where a Node may be dragged
	stayInGroup(part, pt, gridpt) {
		// don't constrain top-level nodes
		const grp = part.containingGroup;

		if (grp === null) return gridpt;

		// try to stay within the background Shape of the Group
		const back = grp.resizeObject;

		if (back === null) return gridpt;

		// allow dragging a Node out of a Group if the Shift key is down
		//if (part.diagram.lastInput.shift) return pt;
		const p1 = back.getDocumentPoint(go.Spot.TopLeft);
		const p2 = back.getDocumentPoint(go.Spot.BottomRight);
		const b = part.actualBounds;
		const loc = part.location;

		// no placeholder -- just assume some Margin
		const m = this.GroupMargin;

		// now limit the location appropriately
		const x =
			Math.max(
				p1.x + m.left,
				Math.min(gridpt.x, p2.x - m.right - b.width - 1)
			) +
			(loc.x - b.x);
		const y =
			Math.max(
				p1.y + m.top,
				Math.min(gridpt.y, p2.y - m.bottom - b.height - 1)
			) +
			(loc.y - b.y);

		return new go.Point(x, y);
	}

	constructor(diagramElement) {
		const $ = go.GraphObject.make;
		const { GroupMargin } = this;

		this.diagram = new go.Diagram(diagramElement, {
			nodeTemplateMap: this.initNodes(),
			groupTemplateMap: this.initGroups(),
			linkTemplate: this.initLink(),
			commandHandler: new DrawCommandHandler(),
			'commandHandler.arrowKeyBehavior': 'move', // default to having arrow keys move selected nodes
			'resizingTool.computeMinSize': function () {
				// method override
				const group = this.adornedObject.part;
				const membnds = group.diagram.computePartsBounds(
					group.memberParts
				);
				membnds.addMargin(GroupMargin);
				membnds.unionPoint(group.location);
				return membnds.size;
			},
			layout: $(
				go.LayeredDigraphLayout, // automatically lay out the lane's subgraph
				{
					isInitial: false, // don't even do initial layout
					isOngoing: false, // don't invalidate layout when nodes or links are added or removed
					direction: 0,
					columnSpacing: 10,
					layeringOption:
						go.LayeredDigraphLayout.LayerLongestPathSource,
					setsPortSpots: false,
				}
			),
			grid: $(
				go.Panel,
				'Grid',
				{
					name: 'GRID',
					visible: false,
					gridCellSize: new go.Size(10, 10),
					gridOrigin: new go.Point(0, 0),
				},
				$(go.Shape, 'LineH', {
					stroke: '#EEEEEE',
					strokeWidth: 0.5,
					interval: 1,
				}),
				$(go.Shape, 'LineH', {
					stroke: '#E1E1E1',
					strokeWidth: 0.5,
					interval: 5,
				}),
				$(go.Shape, 'LineH', {
					stroke: '#E1E1E1',
					strokeWidth: 1.0,
					interval: 10,
				}),
				$(go.Shape, 'LineV', {
					stroke: '#EEEEEE',
					strokeWidth: 0.5,
					interval: 1,
				}),
				$(go.Shape, 'LineV', {
					stroke: '#E1E1E1',
					strokeWidth: 0.5,
					interval: 5,
				}),
				$(go.Shape, 'LineV', {
					stroke: '#E1E1E1',
					strokeWidth: 1.0,
					interval: 10,
				})
			),
		});

		this.gridEnabled = true;
	}

	setModel(model) {
		const { nodes, links, initialLayoutEnabled } = model;
		const { diagram } = this;

		this.diagram.model = new go.GraphLinksModel({
			linkFromPortIdProperty: 'fromPort', // required information:
			linkToPortIdProperty: 'toPort', // identifies data property names
			nodeDataArray: nodes,
			linkDataArray: links,
		});

		if (initialLayoutEnabled) {
			diagram.layoutDiagram(true);
		}

		diagram.model.undoManager.isEnabled = true;
		diagram.isModified = false;
	}

	initLink() {
		const $ = go.GraphObject.make;

		return $(
			go.Link, // the whole link panel
			{
				routing: go.Link.AvoidsNodes,
				curve: go.Link.JumpOver,
				corner: 5,
				toShortLength: 4,
				toEndSegmentLength: 20,
				relinkableFrom: true,
				relinkableTo: true,
				reshapable: true,
				// mouse-overs subtly highlight links:
				mouseEnter: (e, link) =>
					(link.findObject('HIGHLIGHT').stroke =
						'rgba(30,144,255,0.2)'),
				mouseLeave: (e, link) =>
					(link.findObject('HIGHLIGHT').stroke = 'transparent'),
				selectionAdorned: false,
			},
			new go.Binding('points').makeTwoWay(),
			$(
				go.Shape, // the highlight shape, normally transparent
				{
					isPanelMain: true,
					strokeWidth: 8,
					stroke: 'transparent',
					name: 'HIGHLIGHT',
				}
			),
			$(
				go.Shape, // the link path shape
				{ isPanelMain: true, strokeWidth: 1 },
				new go.Binding('stroke', 'color')
			),
			$(
				go.Shape,
				{ toArrow: 'Standard', strokeWidth: 0, scale: 4 / 3 },
				new go.Binding('fill', 'color'),
				new go.Binding('scale', 'thickness', (t) => (2 + t) / 3)
			),
			$(
				go.Panel,
				'Auto', // the link label, normally not visible
				{
					visible: false,
					name: 'LABEL',
					segmentOffset: new go.Point(0, 0),
					segmentOrientation: go.Link.OrientUpright,
					//segmentOffset: new go.Point(-12, -12),
				},
				new go.Binding('visible', 'text', function (t) {
					return !!t;
				}),
				$(
					go.Shape,
					'RoundedRectangle', // the label shape
					{ fill: '#FFF', strokeWidth: 0 }
				),
				$(
					go.TextBlock,
					{
						textAlign: 'center',
						stroke: '#333333',
						editable: false,
					},
					new go.Binding('text')
				)
			)
		);
	}

	initNodes() {
		const $ = go.GraphObject.make;
		// constants for design choices
		const GradientYellow = $(go.Brush, 'Linear', {
			0: 'LightGoldenRodYellow',
			1: '#FFFF66',
		});
		const GradientLightGreen = $(go.Brush, 'Linear', {
			0: '#E0FEE0',
			1: 'PaleGreen',
		});
		const ActivityNodeFill = $(go.Brush, 'Linear', {
			0: 'OldLace',
			1: 'PapayaWhip',
		});
		const ActivityNodeStroke = '#CDAA7D';
		const ActivityNodeWidth = 120;
		const ActivityNodeHeight = 80;

		const EventNodeSize = 42;
		const EventNodeInnerSize = EventNodeSize - 6;
		const EventEndOuterFillColor = 'pink';
		const EventBackgroundColor = GradientLightGreen;
		const EventDimensionStrokeColor = 'green';
		const EventDimensionStrokeEndColor = 'red';
		const EventNodeStrokeWidthIsEnd = 4;

		const GatewayNodeSize = 80;
		const GatewayNodeSymbolSize = 35;
		const GatewayNodeFill = GradientYellow;
		const GatewayNodeStroke = 'darkgoldenrod';
		const GatewayNodeSymbolStroke = 'darkgoldenrod';
		const GatewayNodeSymbolFill = GradientYellow;
		const GatewayNodeSymbolStrokeWidth = 3;

		function nodeEventDimensionStrokeColorConverter(s) {
			if (s === 8) return EventDimensionStrokeEndColor;
			return EventDimensionStrokeColor;
		}

		const activityNodeTemplate = $(
			go.Node,
			'Table',
			{
				locationSpot: go.Spot.Center,
				doubleClick: (e, node) => {
					this.onActivityyDoubleClick(node);
				},
			},
			new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(
				go.Point.stringify
			),
			$(
				go.Panel,
				'Auto',
				{
					name: 'PANEL',
					minSize: new go.Size(ActivityNodeWidth, ActivityNodeHeight),
					desiredSize: new go.Size(
						ActivityNodeWidth,
						ActivityNodeHeight
					),
				},
				new go.Binding('desiredSize', 'size', go.Size.parse).makeTwoWay(
					go.Size.stringify
				),
				$(
					go.Panel,
					'Spot',
					$(
						go.Shape,
						'RoundedRectangle', // the outside rounded rectangle
						{
							name: 'SHAPE',
							fill: ActivityNodeFill,
							stroke: ActivityNodeStroke,
							parameter1: 10, // corner size
							fromLinkable: true,
							toLinkable: true,
							portId: '', // the default port: if no spot on link data, use closest side
							cursor: 'pointer',
						}
					),
					// task icon
					$(
						go.Shape,
						'SetProperties', // will be SetProperties, ChangeState, ExecuteScript
						{
							alignment: new go.Spot(0, 0, 5, 5),
							alignmentFocus: go.Spot.TopLeft,
							width: 18,
							height: 18,
						},
						new go.Binding('figure', 'actionType')
					) // end Task Icon
				), // end main body rectangles spot panel
				$(
					go.TextBlock, // the center text
					{
						alignment: go.Spot.Center,
						textAlign: 'center',
						margin: 12,
					},
					new go.Binding('text')
				)
			), // end Auto Panel
			// four named ports, one on each side:
			makePort('T', go.Spot.Top, true, true),
			makePort('L', go.Spot.Left, true, true),
			makePort('R', go.Spot.Right, true, true),
			makePort('B', go.Spot.Bottom, true, true)
		); // end go.Node, which is a Spot Panel with bound itemArray

		// ------------------------------------------  Event Node Template  ----------------------------------------------
		const eventNodeTemplate = $(
			go.Node,
			'Vertical',
			{
				locationObjectName: 'SHAPE',
				locationSpot: go.Spot.Center,
				//dragComputation: this.stayInGroup.bind(this), // limit dragging of Nodes to stay within the containing Group, defined above
			},
			new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(
				go.Point.stringify
			),
			// can be resided according to the user's desires
			{ resizable: false, resizeObjectName: 'SHAPE' },
			$(
				go.Panel,
				'Spot',
				$(
					go.Shape,
					'Circle', // Outer circle
					{
						strokeWidth: 1,
						name: 'SHAPE',
						desiredSize: new go.Size(EventNodeSize, EventNodeSize),
						portId: '',
						fromLinkable: true,
						toLinkable: true,
						cursor: 'pointer',
						fromSpot: go.Spot.RightSide,
						toSpot: go.Spot.AllSides,
					},
					// allows the color to be determined by the node data
					new go.Binding('fill', 'eventDimension', function (s) {
						return s === 8
							? EventEndOuterFillColor
							: EventBackgroundColor;
					}),
					new go.Binding('strokeWidth', 'eventDimension', function (
						s
					) {
						return s === 8 ? EventNodeStrokeWidthIsEnd : 1;
					}),
					new go.Binding(
						'stroke',
						'eventDimension',
						nodeEventDimensionStrokeColorConverter
					),
					new go.Binding(
						'desiredSize',
						'size',
						go.Size.parse
					).makeTwoWay(go.Size.stringify)
				), // end main shape
				$(
					go.Shape,
					'Circle', // Inner circle
					{
						alignment: go.Spot.Center,
						desiredSize: new go.Size(
							EventNodeInnerSize,
							EventNodeInnerSize
						),
						fill: null,
					},
					new go.Binding(
						'stroke',
						'eventDimension',
						nodeEventDimensionStrokeColorConverter
					),
					new go.Binding('visible', 'eventDimension', function (s) {
						return s > 3 && s <= 7;
					}) // inner  only visible for 4 thru 7
				)
			), // end Auto Panel
			$(
				go.TextBlock,
				{
					alignment: go.Spot.Center,
					textAlign: 'center',
					margin: 5,
				},
				new go.Binding('text')
			)
		); // end go.Node Vertical

		// ------------------------------------------  Gateway Node Template   ----------------------------------------------

		function nodeGatewaySymbolTypeConverter(s) {
			return s === 0 ? 'AtLeastOne' : 'ALL';
		}

		// Define a function for creating a "port" that is normally transparent.
		// The "name" is used as the GraphObject.portId, the "spot" is used to control how links connect
		// and where the port is positioned on the node, and the boolean "output" and "input" arguments
		// control whether the user can draw links from or to the port.
		function makePort(name, spot, output, input) {
			// the port is basically just a small transparent circle
			return $(go.Shape, 'Circle', {
				fill: null, // not seen, by default; set to a translucent gray by showSmallPorts, defined below
				stroke: null,
				desiredSize: new go.Size(7, 7),
				alignment: spot, // align the port on the main Shape
				alignmentFocus: spot, // just inside the Shape
				portId: name, // declare this object to be a "port"
				fromSpot: spot,
				toSpot: spot, // declare where links may connect at this port
				fromLinkable: output,
				toLinkable: input, // declare whether the user may draw links to/from here
				cursor: 'pointer', // show a different cursor to indicate potential link point
			});
		}

		const gatewayNodeTemplate = $(
			go.Node,
			'Table',
			{
				locationSpot: go.Spot.Center,
				//dragComputation: this.stayInGroup.bind(this), // limit dragging of Nodes to stay within the containing Group, defined above
				doubleClick: (e, node) => {
					this.onGatewayDoubleClick(node);
				},
			},
			new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(
				go.Point.stringify
			),
			$(
				go.Panel,
				'Spot',
				$(go.Shape, 'Diamond', {
					name: 'SHAPE',
					strokeWidth: 1,
					fill: GatewayNodeFill,
					stroke: GatewayNodeStroke,
					desiredSize: new go.Size(GatewayNodeSize, GatewayNodeSize),
					cursor: 'pointer',
					fromLinkable: true,
					toLinkable: true,
					portId: '', // the default port: if no spot on link data, use closest side
				}), // end main shape
				$(
					go.TextBlock,
					{
						alignment: go.Spot.Center,
						stroke: GatewayNodeSymbolStroke,
						font: '25px Roboto, sans-serif',
					},
					new go.Binding('text', 'gatewayType', (gatewayType) => {
						return gatewayType === 0 ? '1+' : 'ALL';
					})
				),
				new go.Binding('visible', 'hidden', (hidden) => {
					return !hidden;
				})
			),
			// four small named ports, one on each side:
			makePort('T', go.Spot.Top, false, true),
			makePort('L', go.Spot.Left, true, true),
			makePort('R', go.Spot.Right, true, true),
			makePort('B', go.Spot.Bottom, true, false)
		); // end go.Node Vertical

		const nodeTemplateMap = new go.Map();

		nodeTemplateMap.add('activity', activityNodeTemplate);
		nodeTemplateMap.add('event', eventNodeTemplate);
		nodeTemplateMap.add('gateway', gatewayNodeTemplate);

		return nodeTemplateMap;
	}

	// ------------------------ Lanes and Pools ------------------------------------------------------------
	initGroups() {
		const $ = go.GraphObject.make;
		const ActivityNodeWidth = 120;
		const ActivityNodeHeight = 80;
		const subProcessGroupTemplate = $(
			go.Group,
			'Auto',
			{
				name: 'Step',
				selectionObjectName: 'PANEL',
				isSubGraphExpanded: true,
				resizable: true,
				resizeObjectName: 'PANEL',
				computesBoundsAfterDrag: true,
				computesBoundsIncludingLinks: false,
				layerName: 'Background', // all pools and lanes are always behind all nodes and links
				background: 'transparent', // can grab anywhere in bounds
				movable: true, // allows users to re-order by dragging
				copyable: false, // can't copy lanes or pools
				avoidable: false, // don't impede AvoidsNodes routed Links
				layout: $(
					go.LayeredDigraphLayout, // automatically lay out the lane's subgraph
					{
						isInitial: false, // don't even do initial layout
						isOngoing: false, // don't invalidate layout when nodes or links are added or removed
						direction: 0,
						columnSpacing: 50,
						setsPortSpots: false,
					}
				),
				subGraphExpandedChanged: (grp) => {
					if (grp.diagram === null) return;
					if (grp.diagram.undoManager.isUndoingRedoing) return;

					const shp = grp.resizeObject;

					if (grp.isSubGraphExpanded) {
						shp.height = grp.data.height;
						shp.width = grp.data.width;
						grp.resizable = true;
					} else {
						grp.resizable = false;

						if (!isNaN(shp.height)) {
							grp.diagram.model.set(
								grp.data,
								'height',
								shp.height
							);
						}

						if (!isNaN(shp.width)) {
							grp.diagram.model.set(grp.data, 'width', shp.width);
						}

						shp.height = NaN;
						shp.width = NaN;
					}

					// if (grp.isSubGraphExpanded) grp.isSelected = true;
					// this.assignGroupLayer(grp);
				},
				doubleClick: (e, node) => {
					this.onLaneDoubleClick(node);
				},
				selectionChanged: (part) => {
					const data = part.isSelected ? part.data : null;
					this.onLaneSelectionChanged(data);
				},
			},
			new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(
				go.Point.stringify
			),
			new go.Binding('isSubGraphExpanded', 'expanded'),
			$(go.Shape, 'RoundedRectangle', { fill: 'white' }),
			$(
				go.Panel,
				'Table',
				{
					name: 'PANEL',
					defaultAlignment: go.Spot.Left,
					minSize: new go.Size(ActivityNodeWidth, ActivityNodeHeight),
				},
				new go.Binding('desiredSize', 'size', go.Size.parse).makeTwoWay(
					go.Size.stringify
				),
				$(
					go.Panel,
					'Horizontal',
					{
						defaultAlignment: go.Spot.Left,
						margin: 5,
						alignment: go.Spot.Left,
						row: 0,
					},
					new go.Binding('alignment', 'isSubGraphExpanded', function (
						s
					) {
						return s ? go.Spot.Left : go.Spot.Center;
					}),
					$('SubGraphExpanderButton'),
					$(
						go.TextBlock,
						{ margin: new go.Margin(2, 0, 0, 5) },
						new go.Binding('text')
					)
				),
				// create a placeholder to represent the area where the contents of the group are
				$(go.Placeholder, { padding: this.GroupMargin, row: 1 })
			) // end Vertical Panel
		); // end Group

		// ------------------------------------------  Template Maps  ----------------------------------------------
		const groupTemplateMap = new go.Map();

		groupTemplateMap.add('subprocess', subProcessGroupTemplate);

		return groupTemplateMap;
	}

	undo() {
		this.diagram.commandHandler.undo();
	}

	redo() {
		this.diagram.commandHandler.redo();
	}

	cutSelection() {
		this.diagram.commandHandler.cutSelection();
	}

	copySelection() {
		this.diagram.commandHandler.copySelection();
	}

	pasteSelection() {
		this.diagram.commandHandler.pasteSelection();
	}

	deleteSelection() {
		this.diagram.commandHandler.deleteSelection();
	}

	selectAll() {
		this.diagram.commandHandler.selectAll();
	}

	destroy() {
		this.diagram.div = null;
		this.diagram = null;
	}
}

I cannot explain that behavior from the code that you provide. I notice that you commented out the Node.dragComputation settings, which I assume you did to try to narrow down the problem.