Zum Inhalt

apple/PhysarumApp/Sources/Model/SimFormat.swift

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)
    }
}