hitTestNode function

bool hitTestNode(
  1. Offset point,
  2. SceneNode node
)

Returns true if point hits node in scene coordinates.

Implementation

bool hitTestNode(Offset point, SceneNode node) {
  if (!point.dx.isFinite || !point.dy.isFinite) return false;
  if (!node.isVisible || !node.isSelectable) return false;
  if (!node.transform.isFinite) return false;

  switch (node.type) {
    case NodeType.image:
    case NodeType.text:
    case NodeType.rect:
      final inverse = node.transform.invert();
      final baseHitPadding = clampNonNegativeFinite(node.hitPadding);
      if (inverse == null) {
        final paddingScene = baseHitPadding + kHitSlop;
        return node.boundsWorld.inflate(paddingScene).contains(point);
      }
      final localPoint = inverse.applyToPoint(point);
      final paddingScene = baseHitPadding + kHitSlop;
      final paddingX =
          paddingScene *
          math.sqrt(inverse.a * inverse.a + inverse.c * inverse.c);
      final paddingY =
          paddingScene *
          math.sqrt(inverse.b * inverse.b + inverse.d * inverse.d);
      final bounds = node.localBounds;
      return Rect.fromLTRB(
        bounds.left - paddingX,
        bounds.top - paddingY,
        bounds.right + paddingX,
        bounds.bottom + paddingY,
      ).contains(localPoint);
    case NodeType.path:
      final pathNode = node as PathNode;
      final baseHitPadding = clampNonNegativeFinite(pathNode.hitPadding);
      if (pathNode.fillColor != null) {
        final padding = baseHitPadding + kHitSlop;
        if (!pathNode.boundsWorld.inflate(padding).contains(point)) {
          return false;
        }
        final localPath = pathNode.buildLocalPath(copy: false);
        if (localPath == null) return false;
        final inverse = pathNode.transform.invert();
        if (inverse == null) return true;
        final localPoint = inverse.applyToPoint(point);
        if (localPath.contains(localPoint)) return true;

        // Union semantics: when a path has both fill and stroke, allow hits on
        // the stroke even if the point lies outside the filled interior.
        //
        // Stage A (coarse): use an inflated AABB check for the stroke area.
        if (pathNode.strokeColor != null) {
          final baseStrokeWidth = clampNonNegativeFinite(pathNode.strokeWidth);
          if (baseStrokeWidth > 0) {
            // Note: boundsWorld already includes stroke thickness via
            // PathNode.localBounds; only apply selection tolerances here.
            final strokePadding = baseHitPadding + kHitSlop;
            return pathNode.boundsWorld.inflate(strokePadding).contains(point);
          }
        }
        return false;
      }
      if (pathNode.strokeColor != null) {
        final baseStrokeWidth = clampNonNegativeFinite(pathNode.strokeWidth);
        if (baseStrokeWidth <= 0) return false;
        // Note: boundsWorld already includes stroke thickness via
        // PathNode.localBounds; only apply selection tolerances here.
        final padding = baseHitPadding + kHitSlop;
        return pathNode.boundsWorld.inflate(padding).contains(point);
      }
      return false;
    case NodeType.line:
      final line = node as LineNode;
      final inverse = line.transform.invert();
      final baseHitPadding = clampNonNegativeFinite(line.hitPadding);
      if (inverse == null) {
        final paddingScene = baseHitPadding + kHitSlop;
        return line.boundsWorld.inflate(paddingScene).contains(point);
      }
      final localPoint = inverse.applyToPoint(point);
      final baseThickness = clampNonNegativeFinite(line.thickness);
      final paddingScene = baseHitPadding + kHitSlop;
      final paddingLocal = _sceneScalarToLocalMax(inverse, paddingScene);
      final effectiveThickness = baseThickness + 2 * paddingLocal;
      return hitTestLine(localPoint, line.start, line.end, effectiveThickness);
    case NodeType.stroke:
      final stroke = node as StrokeNode;
      final inverse = stroke.transform.invert();
      final baseHitPadding = clampNonNegativeFinite(stroke.hitPadding);
      if (inverse == null) {
        final paddingScene = baseHitPadding + kHitSlop;
        return stroke.boundsWorld.inflate(paddingScene).contains(point);
      }
      final localPoint = inverse.applyToPoint(point);
      final hitPaddingLocal = _sceneScalarToLocalMax(inverse, baseHitPadding);
      final hitSlopLocal = _sceneScalarToLocalMax(inverse, kHitSlop);
      return hitTestStroke(
        localPoint,
        stroke.points,
        stroke.thickness,
        hitPadding: hitPaddingLocal,
        hitSlop: hitSlopLocal,
      );
  }
}