documentationfor yFiles for HTML 2.6

Snapping

When talking about snapping we have to define the terms snap line and snap result, first. A snap line is a (mostly invisible) line related to a fixed object to which a moved object might snap. A snap result represents a snap line to which a moved object actually snaps at its current position. A moved object can have more than one snap result at the same location, i.e. snap to multiple snap lines at the same time. These snap lines are visualized.

You can set the color of the snap line visualization using the resource keySNAP_LINE_STROKE_KEY:

graphComponent.resources.set(SnapLine.SNAP_LINE_STROKE_KEY, Stroke.BLUE)

Alternatively, you can use the CSS classes that are provided by the default snap line visualizations, see Styling of Snap Lines.

Adding Custom Snap Lines

To add custom snap lines for all elements except labels, register a listener to the GraphSnapContext’s CollectSnapLines event. The arguments of that event have a method to add additional snap lines. Note that these snap lines always have to be orthogonal. If you also want to provide snap lines for labels, you should register an appropriate listener to the LabelSnapContext’s CollectSnapLines event. Snap lines for labels can also be non-orthogonal.

Adding a custom snap line for all graph elements
/**
 * @param {!GraphSnapContext} graphSnapContext
 * @param {!LabelSnapContext} labelSnapContext
 * @param {!Point} start
 * @param {!Point} end
 */
function addSnapLine(graphSnapContext, labelSnapContext, start, end) {
  const snapLine = createSnapLine(start, end)
  if (snapLine !== null) {
    // add a snap line for all graph elements expect labels
    graphSnapContext.addCollectSnapLinesListener((source, args) => args.addAdditionalSnapLine(snapLine))
    // add a snap line for labels
    labelSnapContext.addCollectSnapLinesListener((source, args) => args.addSnapLine(snapLine))
  }
}

/**
 * @param {!Point} start
 * @param {!Point} end
 * @returns {?OrthogonalSnapLine}
 */
function createSnapLine(start, end) {
  // calculate the center of the snap line
  const center = start.add(end).multiply(0.5)
  // create a vertical snap line
  if (start.x === end.x) {
    return new OrthogonalSnapLine(
      SnapLineOrientation.VERTICAL,
      SnapLineSnapTypes.CENTER,
      SnapLineVisualizationType.FIXED_LINE,
      center,
      start.y,
      end.y,
      null,
      50
    )
  }
  // create a horizontal snap line
  if (start.y === end.y) {
    return new OrthogonalSnapLine(
      SnapLineOrientation.HORIZONTAL,
      SnapLineSnapTypes.CENTER,
      SnapLineVisualizationType.FIXED_LINE,
      center,
      start.x,
      end.x,
      null,
      50
    )
  }
  // we do not add non-orthogonal snap lines
  return null
}function addSnapLine(
  graphSnapContext: GraphSnapContext,
  labelSnapContext: LabelSnapContext,
  start: Point,
  end: Point
): void {
  const snapLine = createSnapLine(start, end)
  if (snapLine !== null) {
    // add a snap line for all graph elements expect labels
    graphSnapContext.addCollectSnapLinesListener((source, args) => args.addAdditionalSnapLine(snapLine))
    // add a snap line for labels
    labelSnapContext.addCollectSnapLinesListener((source, args) => args.addSnapLine(snapLine))
  }
}

function createSnapLine(start: Point, end: Point): OrthogonalSnapLine | null {
  // calculate the center of the snap line
  const center = start.add(end).multiply(0.5)
  // create a vertical snap line
  if (start.x === end.x) {
    return new OrthogonalSnapLine(
      SnapLineOrientation.VERTICAL,
      SnapLineSnapTypes.CENTER,
      SnapLineVisualizationType.FIXED_LINE,
      center,
      start.y,
      end.y,
      null,
      50
    )
  }
  // create a horizontal snap line
  if (start.y === end.y) {
    return new OrthogonalSnapLine(
      SnapLineOrientation.HORIZONTAL,
      SnapLineSnapTypes.CENTER,
      SnapLineVisualizationType.FIXED_LINE,
      center,
      start.x,
      end.x,
      null,
      50
    )
  }
  // we do not add non-orthogonal snap lines
  return null
}
Moving a node close to the custom snap line

Note that the snap line appears only if you move a graph element close to it. The following snippet illustrates how to always display the snap line.

Painting the snap line
/**
 * @param {!Point} start
 * @param {!Point} end
 */
function paintSnapLine(start, end) {
  const lineElement = document.createElementNS('http://www.w3.org/2000/svg', 'line')
  lineElement.setAttribute('x1', String(start.x))
  lineElement.setAttribute('y1', String(start.y))
  lineElement.setAttribute('x2', String(end.x))
  lineElement.setAttribute('y2', String(end.y))
  lineElement.setAttribute('stroke', 'black')

  graphComponent.backgroundGroup.addChild(new SvgVisual(lineElement), ICanvasObjectDescriptor.VISUAL)
}function paintSnapLine(start: Point, end: Point): void {
  const lineElement = document.createElementNS('http://www.w3.org/2000/svg', 'line')
  lineElement.setAttribute('x1', String(start.x))
  lineElement.setAttribute('y1', String(start.y))
  lineElement.setAttribute('x2', String(end.x))
  lineElement.setAttribute('y2', String(end.y))
  lineElement.setAttribute('stroke', 'black')

  graphComponent.backgroundGroup.addChild(new SvgVisual(lineElement), ICanvasObjectDescriptor.VISUAL)
}
Painting the snap line

Each graph item provides a default set of snap lines. A node, for example, provides snap lines at its borders and its center. You can change this behavior by decorating the default snap line provider. The following example shows how to add an additional snap line 10 pixels above the top border of each node.

Adding snap lines to nodes
const nodeDecorator = graphComponent.graph.decorator.nodeDecorator

nodeDecorator.snapLineProviderDecorator.setImplementationWrapper((node, wrapped) =>
  ISnapLineProvider.create((context, evt, item) => {
    // add default snap lines
    if (wrapped !== null) {
      wrapped.addSnapLines(context, evt, item)
    }
    // add a snap lines 10 pixels above the top border of a node
    if (item instanceof INode) {
      const layout = item.layout.toRect()
      // calculate the center of the snap line
      const center = new Point(layout.x + layout.width / 2, layout.y - 10)
      const snapLine = new OrthogonalSnapLine(
        SnapLineOrientation.HORIZONTAL,
        SnapLineSnapTypes.BOTTOM,
        SnapLineVisualizationType.FIXED_LINE,
        center,
        layout.x - 50,
        layout.maxX + 50,
        node,
        100
      )
      evt.addAdditionalSnapLine(snapLine)
    }
  })
)
const nodeDecorator = graphComponent.graph.decorator.nodeDecorator

nodeDecorator.snapLineProviderDecorator.setImplementationWrapper((node, wrapped) =>
  ISnapLineProvider.create((context: GraphSnapContext, evt: CollectGraphSnapLinesEventArgs, item: IModelItem) => {
    // add default snap lines
    if (wrapped !== null) {
      wrapped.addSnapLines(context, evt, item)
    }
    // add a snap lines 10 pixels above the top border of a node
    if (item instanceof INode) {
      const layout = item.layout.toRect()
      // calculate the center of the snap line
      const center = new Point(layout.x + layout.width / 2, layout.y - 10)
      const snapLine = new OrthogonalSnapLine(
        SnapLineOrientation.HORIZONTAL,
        SnapLineSnapTypes.BOTTOM,
        SnapLineVisualizationType.FIXED_LINE,
        center,
        layout.x - 50,
        layout.maxX + 50,
        node,
        100
      )
      evt.addAdditionalSnapLine(snapLine)
    }
  })
)
Additional snap line of a node

GraphSnapContext provides a number of properties which determine for which objects snap lines should be collected.

Property Description
collectEdgeSnapLinesWhether to provide snap lines for orthogonal edge segments.
collectNodeSizesWhether to provide snap lines for sizes of fixed nodes.
collectNodeSnapLinesWhether to provide snap lines for fixed nodes (parallel to borders, at border, at center).
collectPortSnapLinesWhether to provide snap lines for ports (orthogonal snap lines at the port’s x and y coordinates).
collectNodePairCenterSnapLinesWhether to provide snap lines for equal distances between two nodes to which moved nodes will snap.
collectNodePairSegmentSnapLinesWhether to provide snap lines for equal distances between two nodes to which moved orthogonal edge segments will snap.
nodeToNodeDistanceDefines the distance to a fixed node’s border for snap lines to which moved nodes can snap.
nodeToEdgeDistanceDefines the distance to an orthogonal edge segment for snap lines to which moved nodes can snap.
edgeToEdgeDistanceDefines the distance to an orthogonal edge segment for snap lines to which moved orthogonal edge segments.
cropSnapLinesWhether to crop snap lines at obstacles.

Adding Custom Snap Results

In the same way you can modify the set of SnapResults collected during the move or edit operation. Grid snapping, for example, provides the center of a node as result to snap to the grid. In the next example we additionally specify the top left corner of the node as snap result.

Adding grid snap results to nodes
const nodeDecorator = graphComponent.graph.decorator.nodeDecorator

class CustomNodeSnapResultProvider extends NodeSnapResultProvider {
  /**
   * @param {!GraphSnapContext} context
   * @param {!CollectSnapResultsEventArgs} args
   * @param {!Rect} suggestedLayout
   * @param {!INode} node
   */
  collectGridSnapResults(context, args, suggestedLayout, node) {
    // add the default grid snap results (center of the node)
    super.collectGridSnapResults(context, args, suggestedLayout, node)
    // add the top left corner of the node as grid snap result
    this.addGridSnapResult(context, args, suggestedLayout.topLeft, node)
  }
}

nodeDecorator.nodeSnapResultProviderDecorator.setImplementation(new CustomNodeSnapResultProvider())
const nodeDecorator = graphComponent.graph.decorator.nodeDecorator

class CustomNodeSnapResultProvider extends NodeSnapResultProvider {
  collectGridSnapResults(
    context: GraphSnapContext,
    args: CollectSnapResultsEventArgs,
    suggestedLayout: Rect,
    node: INode
  ): void {
    // add the default grid snap results (center of the node)
    super.collectGridSnapResults(context, args, suggestedLayout, node)
    // add the top left corner of the node as grid snap result
    this.addGridSnapResult(context, args, suggestedLayout.topLeft, node)
  }
}

nodeDecorator.nodeSnapResultProviderDecorator.setImplementation(new CustomNodeSnapResultProvider())
Additional snap result of a node

GraphSnapContext provides a number of properties to determine which moved object should snap to a specific kind of snap lines, i.e. the actual kind of snap results to collect.

Property Collect snap results for
snapNodesToSnapLinesnodes
snapSegmentsToSnapLinesorthogonal edge segments
snapBendsToSnapLinesbends
snapBendAdjacentSegmentsbends, in a way that the two adjacent segments will become horizontally or vertically oriented
snapPortAdjacentSegmentsnodes, in a way that the first and last segments of adjacent edges will become horizontally or vertically oriented
snapOrthogonalMovementall moved objects, in a way the movement becomes constrained to one axis

The source code of the Custom Snapping demo shows how to customize snapping behavior and how to add custom snap lines.