diff --git a/Core/Solver.cs b/Core/Solver.cs index f87b0ee..e6a42a4 100644 --- a/Core/Solver.cs +++ b/Core/Solver.cs @@ -16,7 +16,7 @@ namespace FluidSim.Core private double _dt; /// CFL target for sub‑stepping (0.3‑0.8). Lower values are safer for shocks. - public double CflTarget { get; set; } = 0.8; + public double CflTarget { get; set; } = 0.9; // ---------- Timing accumulators (reset every LogInterval steps) ---------- private long _stepCount; diff --git a/Program.cs b/Program.cs index 02c910e..95e639d 100644 --- a/Program.cs +++ b/Program.cs @@ -50,7 +50,7 @@ public class Program { var window = CreateWindow(); LoadFont(); - _scenario = new TestScenario(); + _scenario = new Inline4Scenario(); _scenario.Initialize(SampleRate); _lastThrottleUpdateTime = 0.0; diff --git a/Scenarios/Inline4Scenario.cs b/Scenarios/Inline4Scenario.cs index c812729..58531b9 100644 --- a/Scenarios/Inline4Scenario.cs +++ b/Scenarios/Inline4Scenario.cs @@ -19,11 +19,15 @@ namespace FluidSim.Tests private Pipe1D intakePipeBeforeThrottle; private Volume0D intakePlenum; - // Runners + // Runners (shorter, fewer cells) private Pipe1D runner1, runner2, runner3, runner4; - // Exhaust pipes - private Pipe1D exh1, exh2, exh3, exh4; + // Exhaust collector + tailpipe + private Volume0D exhaustCollector; + private Pipe1D tailPipe; + + // Exhaust stubs (short pipes between cylinders and collector) + private Pipe1D exhStub1, exhStub2, exhStub3, exhStub4; // Links – intake private OpenEndLink intakeOpenEnd; @@ -35,11 +39,17 @@ namespace FluidSim.Tests // Intake valves private OrificeLink intakeValve1, intakeValve2, intakeValve3, intakeValve4; - // Exhaust valves + // Exhaust valves (cylinder → stub) private OrificeLink exhaustValve1, exhaustValve2, exhaustValve3, exhaustValve4; - // Exhaust open ends - private OpenEndLink exhaustOpenEnd1, exhaustOpenEnd2, exhaustOpenEnd3, exhaustOpenEnd4; + // Stub‑to‑collector orifices + private OrificeLink stubToCollector1, stubToCollector2, stubToCollector3, stubToCollector4; + + // Collector‑to‑tailpipe orifice + private OrificeLink collectorToTailpipe; + + // Exhaust open end (tailpipe exit) + private OpenEndLink exhaustOpenEnd; private Solver solver; private SoundProcessor exhaustSoundProcessor; @@ -48,7 +58,7 @@ namespace FluidSim.Tests private double dt; private int stepCount; - public double MaxThrottleArea { get; set; } = 3 * Units.cm2; + public double MaxThrottleArea { get; set; } = 10 * Units.cm2; public override void Initialize(int sampleRate) { @@ -56,23 +66,23 @@ namespace FluidSim.Tests solver = new Solver(); solver.SetTimeStep(dt); - solver.CflTarget = 0.9; + solver.CflTarget = 1; // ---- Shared crankshaft ---- crankshaft = new Crankshaft(800); crankshaft.Inertia = 1; - crankshaft.FrictionConstant = 16; - crankshaft.FrictionViscous = 0.5; + crankshaft.FrictionConstant = 3; + crankshaft.FrictionViscous = 0.2; // ---- Cylinder geometry ---- - double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2; + double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 10; double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0; - // Firing order 1-3-4-2 → phase offsets in radians - double phase0 = 0.0 * Math.PI / 180.0; - double phase1 = 180.0 * Math.PI / 180.0; - double phase2 = 540.0 * Math.PI / 180.0; - double phase3 = 360.0 * Math.PI / 180.0; + // Firing order 1-3-4-2 with 180° intervals (0°, 180°, 360°, 540°) + double phaseCyl1 = 0.0; + double phaseCyl3 = Math.PI; // 180° + double phaseCyl4 = 2.0 * Math.PI; // 360° + double phaseCyl2 = 3.0 * Math.PI; // 540° cyl1 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { @@ -80,9 +90,9 @@ namespace FluidSim.Tests IntakeValveLift = 5 * Units.mm, ExhaustValveDiameter = 28 * Units.mm, ExhaustValveLift = 5 * Units.mm, - PhaseOffset = phase0, + PhaseOffset = phaseCyl1, EnergyVariationFraction = 0.03, - MisfireProbability = 0.01 + MisfireProbability = 0.0 }; cyl2 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { @@ -90,9 +100,9 @@ namespace FluidSim.Tests IntakeValveLift = 5 * Units.mm, ExhaustValveDiameter = 28 * Units.mm, ExhaustValveLift = 5 * Units.mm, - PhaseOffset = phase1, + PhaseOffset = phaseCyl2, EnergyVariationFraction = 0.03, - MisfireProbability = 0.01 + MisfireProbability = 0.0 }; cyl3 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { @@ -100,9 +110,9 @@ namespace FluidSim.Tests IntakeValveLift = 5 * Units.mm, ExhaustValveDiameter = 28 * Units.mm, ExhaustValveLift = 5 * Units.mm, - PhaseOffset = phase2, + PhaseOffset = phaseCyl3, EnergyVariationFraction = 0.03, - MisfireProbability = 0.01 + MisfireProbability = 0.0 }; cyl4 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { @@ -110,28 +120,30 @@ namespace FluidSim.Tests IntakeValveLift = 5 * Units.mm, ExhaustValveDiameter = 28 * Units.mm, ExhaustValveLift = 5 * Units.mm, - PhaseOffset = phase3, + PhaseOffset = phaseCyl4, EnergyVariationFraction = 0.03, - MisfireProbability = 0.01 + MisfireProbability = 0.0 }; solver.AddComponent(cyl1); solver.AddComponent(cyl2); solver.AddComponent(cyl3); solver.AddComponent(cyl4); - double pipeDiameter = 2 * Units.cm; + double pipeDiameter = 4 * Units.cm; double pipeArea = Units.AreaFromDiameter(pipeDiameter); - exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f }; - intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f }; + // Sound processors (only one exhaust source now) + exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.2f }; + intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.2f }; reverb = new OutdoorExhaustReverb(sampleRate); - // ---- Intake pipe before throttle ---- - intakePipeBeforeThrottle = new Pipe1D(0.2, pipeArea, 10); + // ---- Intake pipe before throttle (shorter, fewer cells) ---- + intakePipeBeforeThrottle = new Pipe1D(0.1, pipeArea, 10); + intakePipeBeforeThrottle.Name = "Intake pipe"; solver.AddComponent(intakePipeBeforeThrottle); // ---- Plenum ---- - intakePlenum = new Volume0D(50 * Units.mL, 101325.0, 300.0); + intakePlenum = new Volume0D(100 * Units.mL, 101325.0, 300.0); var plenumInlet = intakePlenum.CreatePort(); // port 0 var plenumOut1 = intakePlenum.CreatePort(); // port 1 var plenumOut2 = intakePlenum.CreatePort(); // port 2 @@ -139,65 +151,118 @@ namespace FluidSim.Tests var plenumOut4 = intakePlenum.CreatePort(); // port 4 solver.AddComponent(intakePlenum); - // ---- Runners ---- - runner1 = new Pipe1D(0.2, pipeArea, 5); - runner2 = new Pipe1D(0.2, pipeArea, 5); - runner3 = new Pipe1D(0.2, pipeArea, 5); - runner4 = new Pipe1D(0.2, pipeArea, 5); + // ---- Intake runners (shorter, fewer cells) ---- + runner1 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 1" }; + runner2 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 2" }; + runner3 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 3" }; + runner4 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 4" }; solver.AddComponent(runner1); solver.AddComponent(runner2); solver.AddComponent(runner3); solver.AddComponent(runner4); - // ---- Exhaust pipes ---- - exh1 = new Pipe1D(0.2, pipeArea, 10); - exh2 = new Pipe1D(0.2, pipeArea, 10); - exh3 = new Pipe1D(0.2, pipeArea, 10); - exh4 = new Pipe1D(0.2, pipeArea, 10); - solver.AddComponent(exh1); - solver.AddComponent(exh2); - solver.AddComponent(exh3); - solver.AddComponent(exh4); + // ---- Exhaust collector volume ---- + exhaustCollector = new Volume0D(200 * Units.mL, 101325.0, 800.0); + var colIn1 = exhaustCollector.CreatePort(); // cylinder 1 stub + var colIn2 = exhaustCollector.CreatePort(); // cylinder 2 stub + var colIn3 = exhaustCollector.CreatePort(); // cylinder 3 stub + var colIn4 = exhaustCollector.CreatePort(); // cylinder 4 stub + var colOut = exhaustCollector.CreatePort(); // to tailpipe + solver.AddComponent(exhaustCollector); - // ---- Plenum → runner orifices ---- - plenumToRunner1 = new OrificeLink(plenumOut1, runner1, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; - plenumToRunner2 = new OrificeLink(plenumOut2, runner2, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; - plenumToRunner3 = new OrificeLink(plenumOut3, runner3, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; - plenumToRunner4 = new OrificeLink(plenumOut4, runner4, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; + // ---- Exhaust stub pipes (short connection cylinder → collector) ---- + exhStub1 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 1" }; + exhStub2 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 2" }; + exhStub3 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 3" }; + exhStub4 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 4" }; + solver.AddComponent(exhStub1); + solver.AddComponent(exhStub2); + solver.AddComponent(exhStub3); + solver.AddComponent(exhStub4); + + foreach (var p in new[] { runner1, runner2, runner3, runner4, exhStub1, exhStub2, exhStub3, exhStub4, intakePipeBeforeThrottle }) + { + p.DampingMultiplier = 0.5; + p.EnergyRelaxationRate = 0.0; + } + + // ---- Tailpipe (single exhaust pipe) ---- + tailPipe = new Pipe1D(0.5, pipeArea, 20) + { + Name = "Tailpipe", + DampingMultiplier = 0.5, + EnergyRelaxationRate = 0.0 + }; + solver.AddComponent(tailPipe); + + // ---- Plenum → runner orifices (volume port to pipe left end) ---- + plenumToRunner1 = new OrificeLink(plenumOut1, runner1, isPipeLeftEnd: true, areaProvider: () => pipeArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + plenumToRunner2 = new OrificeLink(plenumOut2, runner2, isPipeLeftEnd: true, areaProvider: () => pipeArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + plenumToRunner3 = new OrificeLink(plenumOut3, runner3, isPipeLeftEnd: true, areaProvider: () => pipeArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + plenumToRunner4 = new OrificeLink(plenumOut4, runner4, isPipeLeftEnd: true, areaProvider: () => pipeArea) + { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(plenumToRunner1); solver.AddOrificeLink(plenumToRunner2); solver.AddOrificeLink(plenumToRunner3); solver.AddOrificeLink(plenumToRunner4); - // ---- Intake valves ---- - intakeValve1 = new OrificeLink(cyl1.IntakePort, runner1, isPipeLeftEnd: false, areaProvider: () => cyl1.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; - intakeValve2 = new OrificeLink(cyl2.IntakePort, runner2, isPipeLeftEnd: false, areaProvider: () => cyl2.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; - intakeValve3 = new OrificeLink(cyl3.IntakePort, runner3, isPipeLeftEnd: false, areaProvider: () => cyl3.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; - intakeValve4 = new OrificeLink(cyl4.IntakePort, runner4, isPipeLeftEnd: false, areaProvider: () => cyl4.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; + // ---- Intake valves (cylinder port to runner right end) ---- + intakeValve1 = new OrificeLink(cyl1.IntakePort, runner1, isPipeLeftEnd: false, areaProvider: () => cyl1.IntakeValveArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + intakeValve2 = new OrificeLink(cyl2.IntakePort, runner2, isPipeLeftEnd: false, areaProvider: () => cyl2.IntakeValveArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + intakeValve3 = new OrificeLink(cyl3.IntakePort, runner3, isPipeLeftEnd: false, areaProvider: () => cyl3.IntakeValveArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + intakeValve4 = new OrificeLink(cyl4.IntakePort, runner4, isPipeLeftEnd: false, areaProvider: () => cyl4.IntakeValveArea) + { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(intakeValve1); solver.AddOrificeLink(intakeValve2); solver.AddOrificeLink(intakeValve3); solver.AddOrificeLink(intakeValve4); - // ---- Exhaust valves ---- - exhaustValve1 = new OrificeLink(cyl1.ExhaustPort, exh1, isPipeLeftEnd: true, areaProvider: () => cyl1.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; - exhaustValve2 = new OrificeLink(cyl2.ExhaustPort, exh2, isPipeLeftEnd: true, areaProvider: () => cyl2.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; - exhaustValve3 = new OrificeLink(cyl3.ExhaustPort, exh3, isPipeLeftEnd: true, areaProvider: () => cyl3.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; - exhaustValve4 = new OrificeLink(cyl4.ExhaustPort, exh4, isPipeLeftEnd: true, areaProvider: () => cyl4.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; + // ---- Exhaust valves (cylinder port to stub left end) ---- + exhaustValve1 = new OrificeLink(cyl1.ExhaustPort, exhStub1, isPipeLeftEnd: true, areaProvider: () => cyl1.ExhaustValveArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + exhaustValve2 = new OrificeLink(cyl2.ExhaustPort, exhStub2, isPipeLeftEnd: true, areaProvider: () => cyl2.ExhaustValveArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + exhaustValve3 = new OrificeLink(cyl3.ExhaustPort, exhStub3, isPipeLeftEnd: true, areaProvider: () => cyl3.ExhaustValveArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + exhaustValve4 = new OrificeLink(cyl4.ExhaustPort, exhStub4, isPipeLeftEnd: true, areaProvider: () => cyl4.ExhaustValveArea) + { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(exhaustValve1); solver.AddOrificeLink(exhaustValve2); solver.AddOrificeLink(exhaustValve3); solver.AddOrificeLink(exhaustValve4); - // ---- Exhaust open ends ---- - exhaustOpenEnd1 = new OpenEndLink(exh1, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; - exhaustOpenEnd2 = new OpenEndLink(exh2, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; - exhaustOpenEnd3 = new OpenEndLink(exh3, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; - exhaustOpenEnd4 = new OpenEndLink(exh4, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; - solver.AddOpenEndLink(exhaustOpenEnd1); - solver.AddOpenEndLink(exhaustOpenEnd2); - solver.AddOpenEndLink(exhaustOpenEnd3); - solver.AddOpenEndLink(exhaustOpenEnd4); + // ---- Stub → collector orifices (collector port to stub right end) ---- + stubToCollector1 = new OrificeLink(colIn1, exhStub1, isPipeLeftEnd: false, areaProvider: () => pipeArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + stubToCollector2 = new OrificeLink(colIn2, exhStub2, isPipeLeftEnd: false, areaProvider: () => pipeArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + stubToCollector3 = new OrificeLink(colIn3, exhStub3, isPipeLeftEnd: false, areaProvider: () => pipeArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + stubToCollector4 = new OrificeLink(colIn4, exhStub4, isPipeLeftEnd: false, areaProvider: () => pipeArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + solver.AddOrificeLink(stubToCollector1); + solver.AddOrificeLink(stubToCollector2); + solver.AddOrificeLink(stubToCollector3); + solver.AddOrificeLink(stubToCollector4); + + // ---- Collector → tailpipe (collector port to tailpipe left end) ---- + collectorToTailpipe = new OrificeLink(colOut, tailPipe, isPipeLeftEnd: true, areaProvider: () => pipeArea) + { DischargeCoefficient = 1.0, UseInertance = false }; + solver.AddOrificeLink(collectorToTailpipe); + + // ---- Exhaust open end (tailpipe exit) ---- + exhaustOpenEnd = new OpenEndLink(tailPipe, isLeftEnd: false) + { + AmbientPressure = 101325.0, + Gamma = 1.4 + }; + solver.AddOpenEndLink(exhaustOpenEnd); // ---- Intake open end ---- intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true) @@ -209,9 +274,9 @@ namespace FluidSim.Tests // ---- Throttle ---- throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false, - areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.001, 1.0)) + areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.0005, 1.0)) { - DischargeCoefficient = 0.2, + DischargeCoefficient = 0.9, UseInertance = false }; solver.AddOrificeLink(throttleOrifice); @@ -242,13 +307,10 @@ namespace FluidSim.Tests $"plenum P = {intakePlenum.Pressure / 1e5:F2} bar"); } - // Mix all exhaust sounds - float exhaustMix = exhaustSoundProcessor.Process(exhaustOpenEnd1) - + exhaustSoundProcessor.Process(exhaustOpenEnd2) - + exhaustSoundProcessor.Process(exhaustOpenEnd3) - + exhaustSoundProcessor.Process(exhaustOpenEnd4); - float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd); - return reverb.Process(exhaustMix * 0.25f + intakeDry); + // Sound: only one exhaust source now + float exhaustSound = exhaustSoundProcessor.Process(exhaustOpenEnd); + float intakeSound = intakeSoundProcessor.Process(intakeOpenEnd); + return reverb.Process(exhaustSound * 0.25f + intakeSound); } public override void Draw(RenderWindow target) @@ -256,48 +318,155 @@ namespace FluidSim.Tests float winW = target.GetView().Size.X; float winH = target.GetView().Size.Y; - float startX = 60f; - float spacing = 80f; - float intakeY = winH / 2f - 80f; - float exhaustY = winH / 2f + 80f; + // --- Layout constants --- + float leftMargin = 40f; + float plenumW = 50f, plenumH = 120f; + float cylinderWidth = 60f, cylinderMaxHeight = 180f; + float cylinderSpacing = 90f; + float cylinderTopY = winH * 0.25f; - // Plenum - float plenW = 50f, plenH = 120f; - float plenX = startX; - float plenTopY = intakeY - plenH / 2f; - DrawVolume(target, intakePlenum, plenX, plenTopY, plenW, plenH); + // Plenum position + float plenumCenterX = leftMargin + plenumW / 2f; + float plenumTopY = cylinderTopY - 20f; + DrawVolume(target, intakePlenum, plenumCenterX, plenumTopY, plenumW, plenumH); - // Helper arrays just for drawing (no closures) - var cyls = new[] { cyl1, cyl2, cyl3, cyl4 }; - var runners = new[] { runner1, runner2, runner3, runner4 }; - var exhausts = new[] { exh1, exh2, exh3, exh4 }; + // Throttle symbol (yellow rectangle) left of plenum + float throttleWidth = 8f, throttleHeight = 30f; + float throttleCenterX = leftMargin - 10f; + var throttleRect = new RectangleShape(new Vector2f(throttleWidth, throttleHeight)) + { + FillColor = Color.Yellow, + Position = new Vector2f(throttleCenterX - throttleWidth / 2f, plenumTopY + plenumH / 2f - throttleHeight / 2f) + }; + target.Draw(throttleRect); + + // Intake pipe before throttle (left of throttle) + float intakePipeEndX = throttleCenterX - throttleWidth / 2f; + float intakePipeStartX = intakePipeEndX - 100f; + float intakePipeY = plenumTopY + plenumH / 2f; + DrawPipe(target, intakePipeBeforeThrottle, intakePipeY, intakePipeStartX, intakePipeEndX); + + // Intake open end marker + var intakeMark = new CircleShape(4f) { FillColor = Color.Magenta }; + intakeMark.Position = new Vector2f(intakePipeStartX - 4f, intakePipeY - 4f); + target.Draw(intakeMark); + + // Cylinders and runners + float runnerStartX = leftMargin + plenumW; + Cylinder[] cyls = { cyl1, cyl2, cyl3, cyl4 }; + Pipe1D[] runners = { runner1, runner2, runner3, runner4 }; for (int i = 0; i < 4; i++) { - float cylX = plenX + plenW + 30f + i * spacing; - float runnerStartX = plenX + plenW + 5f; - float runnerEndX = cylX - 20f; - DrawPipe(target, runners[i], intakeY, runnerStartX, runnerEndX); - - float cylTopY = intakeY - 120f; - DrawCylinder(target, cyls[i], cylX, cylTopY, 70f, 200f); - - float exhStartX = cylX + 35f; - float exhEndX = exhStartX + 100f; - DrawPipe(target, exhausts[i], exhaustY, exhStartX, exhEndX); - - var mark = new CircleShape(4f) { FillColor = Color.Magenta }; - mark.Position = new Vector2f(exhEndX - 4f, exhaustY - 4f); - target.Draw(mark); + float cylCenterX = runnerStartX + 40f + i * cylinderSpacing; + float runnerEndX = cylCenterX; + DrawPipe(target, runners[i], plenumTopY + plenumH / 2f, runnerStartX, runnerEndX); + DrawCylinder(target, cyls[i], cylCenterX, cylinderTopY, cylinderWidth, cylinderMaxHeight); } - // Throttle symbol - var throttleRect = new RectangleShape(new Vector2f(6f, 30f)) + // Exhaust collector below cylinders + float collectorLeftX = runnerStartX + 40f - cylinderWidth / 2f; + float collectorWidth = 3 * cylinderSpacing + cylinderWidth; + float collectorTopY = cylinderTopY + cylinderMaxHeight + 40f; + float collectorHeight = 50f; + float collectorCenterX = collectorLeftX + collectorWidth / 2f; + DrawVolume(target, exhaustCollector, collectorCenterX, collectorTopY, collectorWidth, collectorHeight); + + // Tailpipe from right edge of collector + float tailStartX = collectorLeftX + collectorWidth; + float tailEndX = tailStartX + 150f; + float tailCenterY = collectorTopY + collectorHeight / 2f; + DrawPipe(target, tailPipe, tailCenterY, tailStartX, tailEndX); + + // Exhaust open end marker + var exhaustMark = new CircleShape(4f) { FillColor = Color.Magenta }; + exhaustMark.Position = new Vector2f(tailEndX - 4f, tailCenterY - 4f); + target.Draw(exhaustMark); + + // Exhaust stubs (vertical connections from cylinder bottom to collector) + Pipe1D[] stubs = { exhStub1, exhStub2, exhStub3, exhStub4 }; + for (int i = 0; i < 4; i++) { - FillColor = Color.Yellow, - Position = new Vector2f(plenX - 16f, intakeY - 15f) - }; - target.Draw(throttleRect); + float cylCenterX = runnerStartX + 40f + i * cylinderSpacing; + float vertStartY = cylinderTopY + cylinderMaxHeight; + float vertEndY = collectorTopY; + // Draw stub as a vertical pipe + DrawPipeVertical(target, stubs[i], cylCenterX, vertStartY, vertEndY); + } + } + + // Helper to draw a pipe vertically (reuse temperature coloring) + private void DrawPipeVertical(RenderWindow target, Pipe1D pipe, float centerX, float topY, float bottomY) + { + int n = pipe.CellCount; + if (n < 2) return; + + float pipeLengthPx = bottomY - topY; + float dy = pipeLengthPx / (n - 1); + + float baseRadius = 25f; + float rangeFactor = 2f; + float scaleFactor = 2f; + + static float SmoothStep(float edge0, float edge1, float x) + { + float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f); + return t * t * (3f - 2f * t); + } + + var centersY = new float[n]; + var radii = new float[n]; + var temperatures = new double[n]; + double R_gas = 287.0; + + for (int i = 0; i < n; i++) + { + double p = pipe.GetCellPressure(i); + double rho = pipe.GetCellDensity(i); + double T = p / Math.Max(rho * R_gas, 1e-12); + temperatures[i] = T; + + float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor); + radii[i] = baseRadius * (1f + deviation * scaleFactor); + if (radii[i] < 2f) radii[i] = 2f; + centersY[i] = topY + i * dy; + } + + int segmentsPerCell = 8; + int totalPoints = n + (n - 1) * segmentsPerCell; + Vertex[] stripVertices = new Vertex[totalPoints * 2]; + int idx = 0; + + for (int i = 0; i < n; i++) + { + float y = centersY[i]; + float r = radii[i]; + Color col = TemperatureColor(temperatures[i]); + + stripVertices[idx++] = new Vertex(new Vector2f(centerX - r, y), col); + stripVertices[idx++] = new Vertex(new Vector2f(centerX + r, y), col); + + if (i < n - 1) + { + for (int s = 1; s <= segmentsPerCell; s++) + { + float t = s / (float)segmentsPerCell; + float st = SmoothStep(0f, 1f, t); + float yi = centersY[i] + (centersY[i + 1] - centersY[i]) * t; + float ri = radii[i] + (radii[i + 1] - radii[i]) * st; + double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st; + Color coli = TemperatureColor(Ti); + + stripVertices[idx++] = new Vertex(new Vector2f(centerX - ri, yi), coli); + stripVertices[idx++] = new Vertex(new Vector2f(centerX + ri, yi), coli); + } + } + } + + var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length); + for (int i = 0; i < stripVertices.Length; i++) + pipeMesh[(uint)i] = stripVertices[i]; + target.Draw(pipeMesh); } } } \ No newline at end of file