releaseFromStateScale function

ScaleRelease releaseFromStateScale({
  1. required double width,
  2. required double baseWidth,
  3. required ShrinkBounds? shrink,
  4. required ExpandBounds? expand,
  5. required double velocity,
})

Computes the ScaleRelease plan given gesture-end scale-axis state.

width is the current rect width; baseWidth is the rest width (scale = 1.0). shrink/expand hold the configured minScale/maxScale plus their decay configs. velocity is in width-units per second.

Implementation

ScaleRelease releaseFromStateScale({
  required double width,
  required double baseWidth,
  required ShrinkBounds? shrink,
  required ExpandBounds? expand,
  required double velocity,
}) {
  if (baseWidth <= 0) {
    return const ScaleRelease(
      direction: .idle,
      startZone: .shrink,
      endZone: .shrink,
    );
  }

  // Effective scale-axis boundaries. Null thresholds mean "no past zone on
  // that side" — modeled with an out-of-reach width.
  final shrinkLow = shrink?.minScale != null ? shrink!.minScale! * baseWidth : -baseWidth * 100;
  final expandHigh = expand?.maxScale != null ? expand!.maxScale! * baseWidth : baseWidth * 100;
  final dispCenter = baseWidth;

  final shrinkDc = shrink?.decay ?? _defaultDecay;
  final expandDc = expand?.decay ?? _defaultDecay;

  ScaleZone zoneOf(double w) {
    if (w < shrinkLow) return .pastShrink;
    if (w > expandHigh) return .pastExpand;
    if (w <= dispCenter) return .shrink;
    return .expand;
  }

  final startZone = zoneOf(width);

  if (velocity.abs() <= _velocityFloor) {
    if (width < shrinkLow) {
      return ScaleRelease(
        direction: .idle,
        startZone: startZone,
        endZone: startZone,
        settle: _rubberFling(startPos: width, startVel: velocity, targetPos: shrinkLow, settle: shrinkDc.settle),
      );
    }
    if (width > expandHigh) {
      return ScaleRelease(
        direction: .idle,
        startZone: startZone,
        endZone: startZone,
        settle: _rubberFling(startPos: width, startVel: velocity, targetPos: expandHigh, settle: expandDc.settle),
      );
    }
    // Expanded within max → stay zoomed (X/Y will shift-to-fit).
    if (width >= dispCenter - 0.5) {
      return ScaleRelease(
        direction: .idle,
        startZone: startZone,
        endZone: startZone,
      );
    }
    // Shrunk in display → snap up to base.
    return ScaleRelease(
      direction: .idle,
      startZone: startZone,
      endZone: startZone,
      settle: _rubberFling(startPos: width, startVel: velocity, targetPos: dispCenter, settle: shrinkDc.settle),
    );
  }

  Decay? decayAt(ScaleZone zone, bool outward) {
    final (isShrink, isPast) = switch (zone) {
      .pastShrink => (true, true),
      .shrink => (true, false),
      .expand => (false, false),
      .pastExpand => (false, true),
    };
    final dc = isShrink ? shrinkDc : expandDc;
    final extending = (isShrink && !outward) || (!isShrink && outward);
    if (isPast) {
      return extending ? dc.extendingPastDisplay : dc.retractingPastDisplay;
    }
    return extending ? dc.extending : dc.retracting;
  }

  double? exitBoundaryAt(ScaleZone zone, bool outward) {
    if (outward) {
      switch (zone) {
        case .pastShrink: return shrinkLow;
        case .shrink: return dispCenter;
        case .expand: return expandHigh;
        case .pastExpand: return null;
      }
    } else {
      switch (zone) {
        case .pastExpand: return expandHigh;
        case .expand: return dispCenter;
        case .shrink: return shrinkLow;
        case .pastShrink: return null;
      }
    }
  }

  final phases = <AxisFling>[];
  var w = width;
  var v = velocity;
  var endPos = width;
  for (var i = 0; i < _maxPhases; i++) {
    if (v.abs() <= _velocityFloor) break;
    final outward = v > 0;
    final zone = zoneOf(w);
    final pick = decayAt(zone, outward);
    if (pick == null) break;

    final step = _runPhase(
      pos: w,
      vel: v,
      decay: pick,
      exitBoundary: exitBoundaryAt(zone, outward),
    );
    phases.add(step.fling);
    endPos = step.endPos;
    v = step.endVel;
    if (step.stopped) break;
    w = step.endPos + v.sign * 0.01;
  }

  final endZone = zoneOf(endPos);
  final ScaleDir direction = velocity > 0 ? .outward : .inward;

  // Settle:
  // - past shrink → rubber to shrinkLow (min cap).
  // - past expand → rubber to expandHigh (max cap).
  // - shrunk in display → snap up to base.
  // - expanded in display or at base → stay (X/Y shift-to-fit if needed).
  AxisFling? settle;
  if (endZone case .pastShrink) {
    settle = _rubberFling(startPos: endPos, startVel: v, targetPos: shrinkLow, settle: shrinkDc.settle);
  } else if (endZone case .pastExpand) {
    settle = _rubberFling(startPos: endPos, startVel: v, targetPos: expandHigh, settle: expandDc.settle);
  } else if (endPos < dispCenter - 0.5) {
    settle = _rubberFling(startPos: endPos, startVel: v, targetPos: dispCenter, settle: shrinkDc.settle);
  }

  return ScaleRelease(
    direction: direction,
    startZone: startZone,
    endZone: endZone,
    decay: phases,
    settle: settle,
  );
}