hitTestNode function
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,
);
}
}