import CoreGraphics
import Foundation
/// Integer simulation-field dimensions.
struct FieldSize: Equatable { let width: Int; let height: Int }
/// User-selectable field format. `.auto` matches the screen; the rest are fixed.
enum SimFormat: String, CaseIterable { case auto, square, portrait, landscape }
/// View aspect → field dimensions: short edge fixed at `base`, long edge proportional,
/// capped at `maxLong`. Square (or degenerate) view → base×base.
func fieldSize(viewWidth: Double, viewHeight: Double, base: Int = 720, maxLong: Int = 1600) -> FieldSize {
guard viewWidth > 0, viewHeight > 0 else { return FieldSize(width: base, height: base) }
let lo = min(viewWidth, viewHeight), hi = max(viewWidth, viewHeight)
let long = min(Int((Double(base) * hi / lo).rounded()), maxLong)
return viewHeight >= viewWidth
? FieldSize(width: base, height: long) // portrait / square
: FieldSize(width: long, height: base) // landscape
}
/// Scale a preset's agentCount (tuned at the `baseline`² area) to the actual field
/// area, so trail density is preserved; clamped to [100, tierMax].
func densityScaledAgentCount(_ presetCount: Int, fieldWidth: Int, fieldHeight: Int,
baseline: Int = 512, tierMax: Int = 500_000) -> Int {
let scale = Double(fieldWidth * fieldHeight) / Double(baseline * baseline)
let scaled = Int((Double(presetCount) * scale).rounded())
return min(max(scaled, 100), tierMax)
}
/// Device-class workload tier for the LIVE sim path. Presets are tuned at 512²;
/// phones run a field near that baseline (a 720-base field is ~2× the area and was
/// a primary thermal driver), Macs keep the larger field; iPad runs its own conservative
/// tablet tier. Capture/export is offline and deliberately untiered — it keeps the
/// 720/1600 defaults.
struct FieldTier: Equatable {
let base: Int
let maxLong: Int
let agentCap: Int
private let id: Int // distinguishes tiers even when workload values coincide (e.g. phone vs tablet today)
static let phone = FieldTier(base: 512, maxLong: 1280, agentCap: 250_000, id: 0)
static let tablet = FieldTier(base: 512, maxLong: 1280, agentCap: 250_000, id: 1) // own seam for later iPad tuning
static let desktop = FieldTier(base: 720, maxLong: 1600, agentCap: 500_000, id: 2)
static let tv = FieldTier(base: 512, maxLong: 1280, agentCap: 250_000, id: 3) // mains-powered but fanless; own seam for on-device tuning
static var current: FieldTier { fieldTier(for: currentDeviceIdiom) }
}
/// Pure idiom → tier mapping (testable without a real device).
func fieldTier(for idiom: DeviceIdiom) -> FieldTier {
switch idiom {
case .phone: return .phone
case .pad: return .tablet
case .mac: return .desktop
case .tv: return .tv
}
}
func targetFieldSize(viewWidth: Double, viewHeight: Double, format: SimFormat,
tier: FieldTier) -> FieldSize {
targetFieldSize(viewWidth: viewWidth, viewHeight: viewHeight, format: format,
base: tier.base, maxLong: tier.maxLong)
}
func densityScaledAgentCount(_ presetCount: Int, fieldWidth: Int, fieldHeight: Int,
tier: FieldTier) -> Int {
densityScaledAgentCount(presetCount, fieldWidth: fieldWidth, fieldHeight: fieldHeight,
tierMax: tier.agentCap)
}
private let fixedLongRatio = 16.0 / 9.0
/// Target field for a given view + format.
func targetFieldSize(viewWidth: Double, viewHeight: Double, format: SimFormat,
base: Int = 720, maxLong: Int = 1600) -> FieldSize {
switch format {
case .auto:
return fieldSize(viewWidth: viewWidth, viewHeight: viewHeight, base: base, maxLong: maxLong)
case .square:
return FieldSize(width: base, height: base)
case .portrait:
return FieldSize(width: base, height: min(Int((Double(base) * fixedLongRatio).rounded()), maxLong))
case .landscape:
return FieldSize(width: min(Int((Double(base) * fixedLongRatio).rounded()), maxLong), height: base)
}
}