// lib/editor/engine/stroke_bounds.dart
//
// Axis-aligned bounds of strokes in normalized content coordinates. Used for
// broad-phase culling (don't paint/erase/hit-test strokes whose box is off the
// viewport — the infinite board's R1 perf primitive), and as a cheap pre-filter
// before the exact per-point eraser test.
//
// Pure geometry over EditorStroke; no widgets/storage; fully unit-tested.

import 'dart:ui' show Rect;

import 'stroke_model.dart';

/// Tight axis-aligned bounds of [stroke] in normalized coords, or null when the
/// stroke has no points. A single-point stroke yields a zero-size rect at that
/// point.
Rect? strokeBounds(EditorStroke stroke) {
  if (stroke.points.isEmpty) return null;
  var minX = double.infinity, minY = double.infinity;
  var maxX = double.negativeInfinity, maxY = double.negativeInfinity;
  for (final p in stroke.points) {
    if (p.x < minX) minX = p.x;
    if (p.y < minY) minY = p.y;
    if (p.x > maxX) maxX = p.x;
    if (p.y > maxY) maxY = p.y;
  }
  return Rect.fromLTRB(minX, minY, maxX, maxY);
}

/// Union bounds of [strokes], or null when none have points.
Rect? strokesBounds(Iterable<EditorStroke> strokes) {
  Rect? acc;
  for (final stroke in strokes) {
    final b = strokeBounds(stroke);
    if (b == null) continue;
    acc = acc == null ? b : acc.expandToInclude(b);
  }
  return acc;
}

/// Whether [stroke]'s bounds overlap [viewport] (broad-phase visibility test).
/// Empty strokes are never visible. Touching edges count as overlapping.
bool strokeIntersects(EditorStroke stroke, Rect viewport) {
  final b = strokeBounds(stroke);
  if (b == null) return false;
  return b.left <= viewport.right &&
      b.right >= viewport.left &&
      b.top <= viewport.bottom &&
      b.bottom >= viewport.top;
}
