项目文件夹

文件
2026-07-13 13:13:17 +08:00

1455 行
63 KiB
C#

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
using SharpDX;
using T3.Core.Utils;
namespace Lib.io.dmx
{
// ------------------------------------------------------------------------
// Type aliases – keep System.Numerics types distinct from SharpDX types.
// ------------------------------------------------------------------------
using Vec3 = Vector3;
using Quat = Quaternion;
[Guid("c9d7cd19-7fc6-4491-8dfa-3808725c7857")]
public sealed class PointsToDmxLights : Instance<PointsToDmxLights>
{
#region Output Slots
[Output(Guid = "8DC2DB32-D7A3-4B3A-A000-93C3107D19E4", DirtyFlagTrigger = DirtyFlagTrigger.Animated)]
public readonly Slot<List<int>> Result = new(new List<int>(20));
[Output(Guid = "da7deb8c-4218-4cae-9ec5-fd7c2e6f4c35")]
public readonly Slot<BufferWithViews> VisualizeLights = new();
#endregion
#region Enums
public enum AxisModes { Disabled, X, Y, Z }
public enum RotationOrderModes { PanThenTilt, TiltThenPan }
public enum ForwardVectorModes { X, Y, Z, NegX, NegY, NegZ }
public enum TestMode
{
Disabled = 0,
ZPositive,
ZNegative,
XPositive,
XNegative,
YPositive,
YNegative
}
#endregion
#region Private Fields & Constants
private readonly List<int> _resultItems = new(128);
private const int UniverseSize = 512;
private BufferWithViews _visualizeBuffer;
private Point[] _visualizationPoints = Array.Empty<Point>();
// Stores the previous pan/tilt (radians) per fixture – used by shortestpath logic.
private readonly List<Vector2> _lastPanTiltPerFixture = new();
private Point[] _points = Array.Empty<Point>();
private Point[] _referencePoints = Array.Empty<Point>();
private readonly List<int> _pointChannelValues = new();
private readonly StructuredBufferReadAccess _pointsBufferReader = new();
private readonly StructuredBufferReadAccess _referencePointsBufferReader = new();
// Cached forward axis – may be overridden by TestMode.
private Vec3 _cachedForwardAxis = Vec3.UnitZ;
#endregion
#region Constructor
public PointsToDmxLights()
{
Result.UpdateAction = Update;
}
#endregion
#region Input Slots
// Buffers
[Input(Guid = "61b48e46-c3d1-46e3-a470-810d55f30aa6")]
public readonly InputSlot<BufferWithViews> EffectedPoints = new();
[Input(Guid = "2bea2ccb-89f2-427b-bd9a-95c7038b715e")]
public readonly InputSlot<BufferWithViews> ReferencePoints = new();
// General behaviour
[Input(Guid = "1348ed7c-79f8-48c6-ac00-e60fb40050db")]
public readonly InputSlot<int> FixtureChannelSize = new();
[Input(Guid = "7449cd05-54be-484b-854a-d2143340f925")]
public readonly InputSlot<bool> FitInUniverse = new();
[Input(Guid = "850af6c3-d9ef-492c-9cfb-e2589ae5b9ac")]
public readonly InputSlot<bool> FillUniverse = new();
[Input(Guid = "23F23213-68E2-45F5-B452-4A86289004C0")]
public readonly InputSlot<bool> DebugToLog = new();
// TestMode dropdown (debug only)
[Input(Guid = "D8A90C30-4E5B-4F0B-BFA7-09DAF3A4C71F", MappedType = typeof(TestMode))]
public readonly InputSlot<int> TestModeSelect = new();
// POSITION
[Input(Guid = "df04fce0-c6e5-4039-b03f-e651fc0ec4a9")]
public readonly InputSlot<bool> GetPosition = new();
[Input(Guid = "628d96a8-466b-4148-9658-7786833ec989", MappedType = typeof(AxisModes))]
public readonly InputSlot<int> PositionMeasureAxis = new();
[Input(Guid = "78a7e683-f4e7-4826-8e39-c8de08e50e5e")]
public readonly InputSlot<bool> InvertPositionDirection = new();
[Input(Guid = "8880c101-403f-46e0-901e-20ec2dd333e9")]
public readonly InputSlot<Vector2> PositionDistanceRange = new();
[Input(Guid = "fc3ec0d6-8567-4d5f-9a63-5c69fb5988cb")]
public readonly InputSlot<int> PositionChannel = new();
[Input(Guid = "658a19df-e51b-45b4-9f91-cb97a891255a")]
public readonly InputSlot<int> PositionFineChannel = new();
// ROTATION
[Input(Guid = "4922acd8-ab83-4394-8118-c555385c2ce9")]
public readonly InputSlot<bool> GetRotation = new();
[Input(Guid = "032F3617-E1F3-4B41-A3BE-61DD63B9F3BA", MappedType = typeof(ForwardVectorModes))]
public readonly InputSlot<int> ForwardVector = new();
[Input(Guid = "9c235473-346b-4861-9844-4b584e09f58a", MappedType = typeof(RotationOrderModes))]
public readonly InputSlot<int> RotationOrder = new();
[Input(Guid = "49fefbdb-2652-43db-ae52-ebc2df3e2856")]
public readonly InputSlot<bool> InvertX = new();
[Input(Guid = "6d8fc457-0c80-4736-8c25-cc48f07cbbfd")]
public readonly InputSlot<bool> InvertY = new();
[Input(Guid = "0c57cdd5-e450-4425-954f-c9e4256f83e1")]
public readonly InputSlot<bool> InvertZ = new();
[Input(Guid = "1f532994-fb0e-44e4-8a80-7917e1851eae", MappedType = typeof(AxisModes))]
public readonly InputSlot<int> PanAxis = new();
[Input(Guid = "7bf3e057-b9eb-43d2-8e1a-64c1c3857ca1")]
public readonly InputSlot<bool> InvertPan = new();
[Input(Guid = "1f877cf6-10d9-4d0b-b087-974bd6855e0a", MappedType = typeof(AxisModes))]
public readonly InputSlot<int> TiltAxis = new();
[Input(Guid = "f85ecf9f-0c3d-4c10-8ba7-480aa2c7a667")]
public readonly InputSlot<bool> InvertTilt = new();
[Input(Guid = "e96655be-6bc7-4ca4-bf74-079a07570d74")]
public readonly InputSlot<bool> ShortestPathPanTilt = new();
[Input(Guid = "f50da250-606d-4a15-a25e-5458f540e527")]
public readonly InputSlot<Vector2> PanRange = new();
[Input(Guid = "9000c279-73e4-4de8-a1f8-c3914eaaf533")]
public readonly InputSlot<int> PanChannel = new();
[Input(Guid = "4d4b3425-e6ad-4834-a8a7-06c9f9c2b909")]
public readonly InputSlot<int> PanFineChannel = new();
[Input(Guid = "6e8b4125-0e8c-430b-897d-2231bb4c8f6f")]
public readonly InputSlot<Vector2> TiltRange = new();
[Input(Guid = "47d7294f-6f73-4e21-ac9a-0fc0817283fb")]
public readonly InputSlot<int> TiltChannel = new();
[Input(Guid = "4a40e022-d206-447c-bda3-d534f231c816")]
public readonly InputSlot<int> TiltFineChannel = new();
[Input(Guid = "C9D7CD19-7FC6-4491-8DFA-3808725C7859")]
public readonly InputSlot<float> PanOffset = new();
[Input(Guid = "C9D7CD19-7FC6-4491-8DFA-3808725C7860")]
public readonly InputSlot<float> TiltOffset = new();
// VISUALIZATION SETTINGS
[Input(Guid = "294B0515-B9F2-446A-8A97-01E3C8B715C0", MappedType = typeof(AxisModes))]
public readonly InputSlot<int> VisPanAxis = new();
[Input(Guid = "F98E8F19-C234-453D-9492-369F6B08035D", MappedType = typeof(AxisModes))]
public readonly InputSlot<int> VisTiltAxis = new();
// COLOUR
[Input(Guid = "5cdc69f7-45ec-4eec-bfb6-960d6245dafb")]
public readonly InputSlot<bool> GetColor = new();
[Input(Guid = "cf2c3308-8f3f-442d-a563-b419f12e7ad1")]
public readonly InputSlot<bool> RgbToCmy = new();
[Input(Guid = "013cc355-91d6-4ea6-b9f7-f1817b89e4a3")]
public readonly InputSlot<int> RedChannel = new();
[Input(Guid = "970769f4-116f-418d-87a7-cda28e44d063")]
public readonly InputSlot<int> GreenChannel = new();
[Input(Guid = "d755342b-9a9e-4c78-8376-81579d8c0909")]
public readonly InputSlot<int> BlueChannel = new();
[Input(Guid = "f13edebd-b44f-49e9-985e-7e3feb886fea")]
public readonly InputSlot<int> AlphaChannel = new();
[Input(Guid = "8ceece78-9a08-4c7b-8fea-740e8e5929a6")]
public readonly InputSlot<int> WhiteChannel = new();
[Input(Guid = "5E96A7A3-5340-43F2-96B9-9972A69421E5")]
public readonly InputSlot<bool> Is16BitColor = new();
// FEATURES (F1 / F2)
[Input(Guid = "91c78090-be10-4203-827e-d2ef1b93317e")]
public readonly InputSlot<bool> GetF1 = new();
[Input(Guid = "bec9e5a6-40a9-49b2-88bd-01a4ea03d28c")]
public readonly InputSlot<bool> GetF1ByPixel = new();
[Input(Guid = "b7061834-66aa-4f7f-91f9-10ebfe16713f")]
public readonly InputSlot<int> F1Channel = new();
[Input(Guid = "1cb93e97-0161-4a77-bbc7-ff30c1972cf8")]
public readonly InputSlot<bool> GetF2 = new();
[Input(Guid = "b8080f4e-4542-4e20-9844-8028bbaf223f")]
public readonly InputSlot<bool> GetF2ByPixel = new();
[Input(Guid = "d77be0d1-5fb9-4d26-9e4a-e16497e4759c")]
public readonly InputSlot<int> F2Channel = new();
// CUSTOM VARIABLES
[Input(Guid = "b2c3d4e5-f6a7-8901-bcde-f23456789012")]
public readonly InputSlot<List<int>> CustomVariableChannels = new();
[Input(Guid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890")]
public readonly InputSlot<List<int>> CustomVariableValues = new();
#endregion
#region Main Update Method
private void Update(EvaluationContext context)
{
var pointBuffer = EffectedPoints.GetValue(context);
var referencePointBuffer = ReferencePoints.GetValue(context);
if (pointBuffer == null || pointBuffer.Buffer == null || pointBuffer.Srv == null)
{
Log.Warning("EffectedPoints buffer is not connected or invalid.", this);
Result.Value?.Clear();
VisualizeLights.Value = null;
_lastPanTiltPerFixture.Clear();
return;
}
// Asynchronously read the structured buffers
_pointsBufferReader.InitiateRead(
pointBuffer.Buffer,
pointBuffer.Srv.Description.Buffer.ElementCount,
pointBuffer.Buffer.Description.StructureByteStride,
OnPointsReadComplete);
_pointsBufferReader.Update();
if (referencePointBuffer != null && referencePointBuffer.Buffer != null && referencePointBuffer.Srv != null)
{
_referencePointsBufferReader.InitiateRead(
referencePointBuffer.Buffer,
referencePointBuffer.Srv.Description.Buffer.ElementCount,
referencePointBuffer.Buffer.Description.StructureByteStride,
OnReferencePointsReadComplete);
_referencePointsBufferReader.Update();
}
else
{
_referencePoints = Array.Empty<Point>();
}
// Process when we actually have points
if (_points != null && _points.Length > 0)
{
if (_visualizationPoints.Length != _points.Length)
_visualizationPoints = new Point[_points.Length];
ApplyTestMode(context);
UpdateChannelData(context, _points);
Result.Value = new List<int>(_resultItems);
UpdateVisualizationBuffer();
VisualizeLights.Value = _visualizeBuffer;
}
else
{
Result.Value?.Clear();
VisualizeLights.Value = null;
_lastPanTiltPerFixture.Clear();
}
}
#endregion
#region TestMode handling
private void ApplyTestMode(EvaluationContext context)
{
var mode = (TestMode)TestModeSelect.GetValue(context);
switch (mode)
{
case TestMode.Disabled:
_cachedForwardAxis = ResolveForwardFromInput(context);
break;
case TestMode.ZPositive:
_cachedForwardAxis = Vec3.UnitZ;
break;
case TestMode.ZNegative:
_cachedForwardAxis = -Vec3.UnitZ;
break;
case TestMode.XPositive:
_cachedForwardAxis = Vec3.UnitX;
break;
case TestMode.XNegative:
_cachedForwardAxis = -Vec3.UnitX;
break;
case TestMode.YPositive:
_cachedForwardAxis = Vec3.UnitY;
break;
case TestMode.YNegative:
_cachedForwardAxis = -Vec3.UnitY;
break;
}
}
private Vec3 ResolveForwardFromInput(EvaluationContext context)
{
var mode = (ForwardVectorModes)ForwardVector.GetValue(context);
return mode switch
{
ForwardVectorModes.X => Vec3.UnitX,
ForwardVectorModes.Y => Vec3.UnitY,
ForwardVectorModes.Z => Vec3.UnitZ,
ForwardVectorModes.NegX => -Vec3.UnitX,
ForwardVectorModes.NegY => -Vec3.UnitY,
ForwardVectorModes.NegZ => -Vec3.UnitZ,
_ => Vec3.UnitZ,
};
}
#endregion
#region BufferRead Callbacks
private void OnPointsReadComplete(StructuredBufferReadAccess.ReadRequestItem readItem,
IntPtr dataPointer,
DataStream dataStream)
{
int count = readItem.ElementCount;
if (_points.Length != count)
_points = new Point[count];
using (dataStream) { dataStream.ReadRange(_points, 0, count); }
}
private void OnReferencePointsReadComplete(StructuredBufferReadAccess.ReadRequestItem readItem,
IntPtr dataPointer,
DataStream dataStream)
{
int count = readItem.ElementCount;
if (_referencePoints.Length != count)
_referencePoints = new Point[count];
using (dataStream) { dataStream.ReadRange(_referencePoints, 0, count); }
}
#endregion
#region ChannelData Generation
private void UpdateChannelData(EvaluationContext context, Point[] points)
{
int fixtureChannelSize = FixtureChannelSize.GetValue(context);
int effectedPointsCount = points.Length;
bool debugToLog = DebugToLog.GetValue(context);
// Determine fixture → pixel mapping (reference based)
int fixtureCount;
int pixelsPerFixture;
bool useReferencePoints = _referencePoints.Length > 0;
if (useReferencePoints)
{
fixtureCount = _referencePoints.Length;
if (fixtureCount == 0 || effectedPointsCount % fixtureCount != 0)
{
Log.Warning(
$"Effected points count ({effectedPointsCount}) is not a multiple of reference points count ({fixtureCount}). " +
"Falling back to 1to1 mapping.", this);
fixtureCount = effectedPointsCount;
pixelsPerFixture = 1;
useReferencePoints = false;
}
else
{
pixelsPerFixture = effectedPointsCount / fixtureCount;
}
}
else
{
fixtureCount = effectedPointsCount;
pixelsPerFixture = 1;
}
// Ensure state list size matches fixture count
while (_lastPanTiltPerFixture.Count < fixtureCount)
_lastPanTiltPerFixture.Add(new Vector2(float.NaN, float.NaN));
bool fitInUniverse = FitInUniverse.GetValue(context);
bool fillUniverse = FillUniverse.GetValue(context);
_resultItems.Clear();
_pointChannelValues.Clear();
if (fixtureChannelSize <= 0)
{
if (effectedPointsCount > 0) Log.Warning("FixtureChannelSize is 0 or less, no DMX output generated.", this);
return;
}
// Preallocate the perfixture DMX channel buffer
_pointChannelValues.Capacity = fixtureChannelSize;
for (int i = 0; i < fixtureChannelSize; i++)
_pointChannelValues.Add(0);
// Process each fixture
for (int fixtureIdx = 0; fixtureIdx < fixtureCount; fixtureIdx++)
{
bool logThisFixture = debugToLog && fixtureIdx == 0;
// Reset perfixture channel array
for (int i = 0; i < fixtureChannelSize; i++)
_pointChannelValues[i] = 0;
int firstPixelIdx = fixtureIdx * pixelsPerFixture;
Point transformPoint = points[firstPixelIdx];
Point referencePoint = useReferencePoints ? _referencePoints[fixtureIdx] : transformPoint;
if (logThisFixture) Log.Debug("--- Fixture 0 Debug ---", this);
// Process transformations
Vec3 finalVisPos;
Quat finalVisOrientation = ProcessTransformations(context,
transformPoint,
referencePoint,
useReferencePoints,
fixtureIdx,
logThisFixture,
out finalVisPos);
// Store visualisation data
for (int p = 0; p < pixelsPerFixture; ++p)
{
int curIdx = firstPixelIdx + p;
if (curIdx < _visualizationPoints.Length)
{
Point currentPoint = points[curIdx];
currentPoint.Position = finalVisPos;
currentPoint.Orientation = finalVisOrientation;
_visualizationPoints[curIdx] = currentPoint;
}
}
// Handle color, features, and custom variables
HandleColorAndFeatures(context, points, transformPoint, firstPixelIdx, pixelsPerFixture);
HandleCustomVariables(context);
// Universe fit
if (fitInUniverse)
{
int remaining = UniverseSize - (_resultItems.Count % UniverseSize);
if (fixtureChannelSize > remaining)
{
for (int i = 0; i < remaining; i++)
_resultItems.Add(0);
}
}
// Append fixture's DMX channel list
_resultItems.AddRange(_pointChannelValues);
}
// Universe fill
if (fillUniverse)
{
int remainder = _resultItems.Count % UniverseSize;
if (remainder != 0)
{
int toAdd = UniverseSize - remainder;
for (int i = 0; i < toAdd; i++)
_resultItems.Add(0);
}
}
}
#endregion
#region Transformations (Position & Rotation)
private Quat ProcessTransformations(EvaluationContext context,
Point transformPoint,
Point referencePoint,
bool useReferencePoints,
int fixtureIdx,
bool shouldLog,
out Vec3 finalVisPosition)
{
bool getRot = GetRotation.GetValue(context);
bool getPos = GetPosition.GetValue(context);
// Rotation
Quat finalOrientation = transformPoint.Orientation;
if (getRot)
finalOrientation = ProcessRotation(context,
transformPoint,
referencePoint,
useReferencePoints,
fixtureIdx,
shouldLog);
// Position
finalVisPosition = transformPoint.Position;
if (getPos)
finalVisPosition = ProcessPosition(context,
transformPoint,
referencePoint,
useReferencePoints,
shouldLog);
else if (getRot)
finalVisPosition = referencePoint.Position;
return finalOrientation;
}
#endregion
#region Position Handling
private Vec3 ProcessPosition(EvaluationContext context,
Point point,
Point referencePoint,
bool calculateRelativePosition,
bool shouldLog)
{
int channel = PositionChannel.GetValue(context);
int fineChannel = PositionFineChannel.GetValue(context);
AxisModes axis = (AxisModes)PositionMeasureAxis.GetValue(context);
if (channel <= 0 || axis == AxisModes.Disabled)
return point.Position;
bool invert = InvertPositionDirection.GetValue(context);
Vector2 range = PositionDistanceRange.GetValue(context);
if (Math.Abs(range.Y - range.X) < 1e-4f)
{
Log.Warning("PositionDistanceRange min and max are too close – will output 0.", this);
SetDmxValue(0f, channel, fineChannel, range.X, range.Y, shouldLog, "Position");
return point.Position;
}
Vec3 pos = point.Position;
Vec3 refPos = calculateRelativePosition ? referencePoint.Position : Vec3.Zero;
float distance = axis switch
{
AxisModes.X => pos.X - refPos.X,
AxisModes.Y => pos.Y - refPos.Y,
AxisModes.Z => pos.Z - refPos.Z,
_ => 0f,
};
if (invert) distance = -distance;
float clampedDist = Math.Clamp(distance, range.X, range.Y);
SetDmxValue(clampedDist,
channel,
fineChannel,
range.X,
range.Y,
shouldLog,
"Position");
// Compute position for visualization
Vec3 resultPosition = point.Position;
float finalDist = clampedDist;
switch (axis)
{
case AxisModes.X: resultPosition.X = refPos.X + finalDist; break;
case AxisModes.Y: resultPosition.Y = refPos.Y + finalDist; break;
case AxisModes.Z: resultPosition.Z = refPos.Z + finalDist; break;
}
return resultPosition;
}
#endregion
#region Rotation Handling
private Quat ProcessRotation(EvaluationContext context,
Point point,
Point referencePoint,
bool calculateRelativeRotation,
int fixtureIdx,
bool shouldLog)
{
// Axis configuration
AxisModes panAxis = (AxisModes)PanAxis.GetValue(context);
AxisModes tiltAxis = (AxisModes)TiltAxis.GetValue(context);
// Validate axes first
if (!ValidateAxes(panAxis, tiltAxis))
return point.Orientation;
int panChannel = PanChannel.GetValue(context);
int panFineChannel = PanFineChannel.GetValue(context);
int tiltChannel = TiltChannel.GetValue(context);
int tiltFineChannel = TiltFineChannel.GetValue(context);
bool panEnabled = panAxis != AxisModes.Disabled && panChannel > 0;
bool tiltEnabled = tiltAxis != AxisModes.Disabled && tiltChannel > 0;
if (shouldLog) Log.Debug($"Processing Rotation for Fixture. PanEnabled: {panEnabled}, TiltEnabled: {tiltEnabled}, Pan16Bit: {panFineChannel > 0}, Tilt16Bit: {tiltFineChannel > 0}", this);
Vec3 direction;
bool useLookAt = false;
if (calculateRelativeRotation)
{
if ((point.Position - referencePoint.Position).LengthSquared() > 0.0001f)
{
useLookAt = true;
}
}
if (useLookAt)
{
Vec3 worldDir = point.Position - referencePoint.Position;
if (worldDir.LengthSquared() < 1e-6f)
worldDir = Vec3.UnitZ;
else
worldDir = Vec3.Normalize(worldDir);
Quat refRot = referencePoint.Orientation;
if (float.IsNaN(refRot.X) || float.IsNaN(refRot.Y) || float.IsNaN(refRot.Z) || float.IsNaN(refRot.W))
{
if (shouldLog) Log.Warning("Reference rotation is invalid. Using Identity.", this);
refRot = Quat.Identity;
}
Quat invRef = Quat.Inverse(refRot);
direction = Vec3.Transform(worldDir, invRef);
if (InvertX.GetValue(context)) direction.X = -direction.X;
if (InvertY.GetValue(context)) direction.Y = -direction.Y;
if (InvertZ.GetValue(context)) direction.Z = -direction.Z;
direction = Vec3.Normalize(direction);
if (shouldLog) Log.Debug($"LookAt Mode. WorldDir: {worldDir}, LocalDir: {direction}", this);
}
else
{
Quat active = ComputeActiveRotation(point.Orientation,
referencePoint.Orientation,
calculateRelativeRotation);
if (shouldLog) Log.Debug($"Active Quaternion: {active}", this);
direction = ExtractDirection(active,
_cachedForwardAxis,
InvertX.GetValue(context),
InvertY.GetValue(context),
InvertZ.GetValue(context));
if (shouldLog) Log.Debug($"Extracted Direction Vector: {direction}", this);
}
// Raw pan/tilt from direction - IK LOGIC THAT WORKS FOR DMX
var (rawPan, rawTilt) = ComputePanTiltAngles(direction,
panAxis,
tiltAxis,
shouldLog);
if (shouldLog) Log.Debug($"Computed raw angles from direction - Pan: {rawPan * 180f / MathF.PI:F2} deg ({rawPan:F4} rad), Tilt: {rawTilt * 180f / MathF.PI:F2} deg ({rawTilt:F4} rad)", this);
// Apply pan and tilt offsets
float panOffsetRad = (PanOffset.GetValue(context) - 90f) * MathF.PI / 180f;
float tiltOffsetRad = (TiltOffset.GetValue(context) - 90f) * MathF.PI / 180f;
rawPan += panOffsetRad;
rawTilt += tiltOffsetRad;
if (shouldLog) Log.Debug($"Angles after applying offsets - Pan: {rawPan * 180f / MathF.PI:F2} deg ({rawPan:F4} rad), Tilt: {rawTilt * 180f / MathF.PI:F2} deg ({rawTilt:F4} rad)", this);
// Apply ranges, inversion, shortestpath and write DMX
float finalPan = 0f, finalTilt = 0f;
float bestPan = rawPan, bestTilt = rawTilt;
bool useShortestPath = ShortestPathPanTilt.GetValue(context);
if (useShortestPath)
{
Vector2 lastState = _lastPanTiltPerFixture[fixtureIdx];
(bestPan, bestTilt) = GetOptimizedPanTilt(rawPan, rawTilt, context, lastState);
_lastPanTiltPerFixture[fixtureIdx] = new Vector2(bestPan, bestTilt);
}
else
{
bestPan = FitToRange(rawPan, PanRange.GetValue(context), false, float.NaN);
bestTilt = FitToRange(rawTilt, TiltRange.GetValue(context), false, float.NaN);
_lastPanTiltPerFixture[fixtureIdx] = new Vector2(float.NaN, float.NaN);
}
if (panEnabled)
{
finalPan = ApplyPanRangeAndWrite(bestPan,
panChannel,
panFineChannel,
PanRange.GetValue(context),
InvertPan.GetValue(context),
shouldLog);
}
else
{
var state = _lastPanTiltPerFixture[fixtureIdx];
_lastPanTiltPerFixture[fixtureIdx] = new Vector2(float.NaN, state.Y);
}
if (tiltEnabled)
{
finalTilt = ApplyTiltRangeAndWrite(bestTilt,
tiltChannel,
tiltFineChannel,
TiltRange.GetValue(context),
InvertTilt.GetValue(context),
shouldLog);
}
else
{
var state = _lastPanTiltPerFixture[fixtureIdx];
_lastPanTiltPerFixture[fixtureIdx] = new Vector2(state.X, float.NaN);
}
// --- Visualization Calculation START ---
float visPan = finalPan;
if (panEnabled && InvertPan.GetValue(context))
{
Vector2 range = PanRange.GetValue(context);
float min = range.X * MathF.PI / 180f;
float max = range.Y * MathF.PI / 180f;
visPan = min + max - finalPan;
}
float visTilt = finalTilt;
if (tiltEnabled && InvertTilt.GetValue(context))
{
Vector2 range = TiltRange.GetValue(context);
float min = range.X * MathF.PI / 180f;
float max = range.Y * MathF.PI / 180f;
visTilt = min + max - finalTilt;
}
float panAngleForViz = visPan - panOffsetRad;
float tiltAngleForViz = visTilt - tiltOffsetRad;
// Get visualization axes (fall back to DMX axes if Disabled)
AxisModes visPanAxis = (AxisModes)VisPanAxis.GetValue(context);
if (visPanAxis == AxisModes.Disabled) visPanAxis = panAxis;
AxisModes visTiltAxis = (AxisModes)VisTiltAxis.GetValue(context);
if (visTiltAxis == AxisModes.Disabled) visTiltAxis = tiltAxis;
// Create Pan and Tilt Quaternions using the corrected physical angles
Quat panQuat = panEnabled
? Quat.CreateFromAxisAngle(GetAxisVector(visPanAxis), panAngleForViz)
: Quat.Identity;
Quat tiltQuat = tiltEnabled
? Quat.CreateFromAxisAngle(GetAxisVector(visTiltAxis), tiltAngleForViz)
: Quat.Identity;
// --- Visualization Calculation END ---
// Reassemble final rotation (relative to neutral orientation)
Quat resultRotation = (RotationOrderModes)RotationOrder.GetValue(context) == RotationOrderModes.TiltThenPan
? tiltQuat * panQuat
: panQuat * tiltQuat;
// Apply reference orientation if relative mode is active (ABSOLUTE FINAL ORIENTATION)
Quat finalOrientation = resultRotation;
if (calculateRelativeRotation)
finalOrientation = referencePoint.Orientation * resultRotation;
if (shouldLog) Log.Debug($"Re-assembled final orientation: {finalOrientation}", this);
return finalOrientation;
}
#region Rotation Helper Methods
private bool ValidateAxes(AxisModes pan, AxisModes tilt)
{
if (pan == AxisModes.Disabled && tilt == AxisModes.Disabled)
{
Log.Warning("Both Pan and Tilt axes are disabled – rotation will be ignored.", this);
return false;
}
if (pan != AxisModes.Disabled && pan == tilt)
{
Log.Warning($"Pan and Tilt axes cannot be identical ({pan}). Skipping rotation.", this);
return false;
}
Vec3 panVec = GetAxisVector(pan);
Vec3 tiltVec = GetAxisVector(tilt);
if (panVec != Vec3.Zero && tiltVec != Vec3.Zero &&
Vec3.Cross(panVec, tiltVec).LengthSquared() < 1e-6f)
{
Log.Warning($"Pan ({pan}) and Tilt ({tilt}) axes are collinear – rotation undefined.", this);
return false;
}
return true;
}
private Quat ComputeActiveRotation(Quat current, Quat reference, bool relative)
{
if (!relative) return current;
if (float.IsNaN(reference.X) || float.IsNaN(reference.Y) ||
float.IsNaN(reference.Z) || float.IsNaN(reference.W))
{
Log.Warning("Reference rotation is invalid (NaN components). Falling back to absolute rotation.", this);
return current;
}
return Quat.Inverse(reference) * current;
}
private Vec3 ExtractDirection(Quat rotation,
Vec3 forwardAxis,
bool invertX,
bool invertY,
bool invertZ)
{
Vec3 dir = Vec3.Transform(forwardAxis, rotation);
if (invertX) dir.X = -dir.X;
if (invertY) dir.Y = -dir.Y;
if (invertZ) dir.Z = -dir.Z;
return Vec3.Normalize(dir);
}
private static Vec3 GetAxisVector(AxisModes axis) => axis switch
{
AxisModes.X => Vec3.UnitX,
AxisModes.Y => Vec3.UnitY,
AxisModes.Z => Vec3.UnitZ,
_ => Vec3.Zero,
};
#endregion
// IK LOGIC THAT WORKS FOR DMX (RESTORED/KEPT FROM PREVIOUS WORKING VERSION)
private (float rawPan, float rawTilt) ComputePanTiltAngles(Vec3 direction,
AxisModes panAxis,
AxisModes tiltAxis,
bool shouldLog)
{
Vec3 panVec = GetAxisVector(panAxis);
Vec3 tiltVec = GetAxisVector(tiltAxis);
float rawPan = 0f, rawTilt = 0f;
// Handle cases where one or both axes are disabled
if (panVec == Vec3.Zero && tiltVec == Vec3.Zero)
{
return (rawPan, rawTilt);
}
// If only one axis is enabled
if (panVec == Vec3.Zero) // Only tilt enabled
{
// For tilt-only, we need to find the angle between the direction and the plane perpendicular to tilt axis
Vec3 upVec = tiltVec;
Vec3 forwardVec = FindOrthogonalVector(tiltVec);
Vec3.Normalize(Vec3.Cross(upVec, forwardVec));
// Project direction onto the plane defined by forward and right vectors
float forwardComponent = Vec3.Dot(direction, forwardVec);
float upComponent = Vec3.Dot(direction, upVec);
rawTilt = MathF.Atan2(upComponent, forwardComponent);
return (rawPan, rawTilt);
}
if (tiltVec == Vec3.Zero) // Only pan enabled
{
// For pan-only, rotate around pan axis
Vec3 forwardVec = _cachedForwardAxis;
Vec3 rightVec = Vec3.Normalize(Vec3.Cross(panVec, forwardVec));
// Remove component along pan axis
Vec3 directionInPlane = direction - panVec * Vec3.Dot(direction, panVec);
if (directionInPlane.LengthSquared() > 1e-6f)
{
directionInPlane = Vec3.Normalize(directionInPlane);
float rightComponent = Vec3.Dot(directionInPlane, rightVec);
float forwardComponent = Vec3.Dot(directionInPlane, forwardVec);
rawPan = MathF.Atan2(rightComponent, forwardComponent);
}
return (rawPan, rawTilt);
}
// Full 2-axis case - RESTORING ORIGINAL LOGIC
// Create a coordinate system where:
// - Z is the forward direction (cross product of tilt and pan axes)
// - Y is the pan axis (rotation axis for pan)
// - X is the tilt axis (rotation axis for tilt)
Vec3 localForward = Vec3.Normalize(Vec3.Cross(tiltVec, panVec));
if (localForward.LengthSquared() < 1e-6f)
{
Log.Error("Pan and Tilt axes are collinear – cannot form a proper coordinate system.", this);
return (rawPan, rawTilt);
}
// CORRECTED BASIS CONSTRUCTION
// Right vector is Cross(Pan, Forward). For Pan=Y, Forward=Z -> YxZ = X.
Vec3 localRight = Vec3.Normalize(Vec3.Cross(panVec, localForward));
// Up vector is Cross(Right, Forward). For Right=X, Forward=Z -> XxZ = -Y.
Vec3 localUp = Vec3.Normalize(Vec3.Cross(localRight, localForward));
// Transform direction into local coordinate system
float x = Vec3.Dot(direction, localRight); // Tilt axis component
float y = Vec3.Dot(direction, localUp); // Pan axis component
float z = Vec3.Dot(direction, localForward); // Forward component
// Calculate pan angle (rotation around pan axis)
rawPan = MathF.Atan2(x, z);
// Calculate tilt angle (rotation around tilt axis)
// Use atan2 for full range and better numerical stability
float horizontalMagnitude = MathF.Sqrt(x * x + z * z);
rawTilt = MathF.Atan2(y, horizontalMagnitude);
if (shouldLog)
{
Log.Debug($"Local coords - X: {x:F3}, Y: {y:F3}, Z: {z:F3}", this);
Log.Debug($"Calculated angles - Pan: {rawPan * 180f / MathF.PI:F1}°, Tilt: {rawTilt * 180f / MathF.PI:F1}°", this);
}
return (rawPan, rawTilt);
}
// Helper method to find an orthogonal vector
private Vec3 FindOrthogonalVector(Vec3 vec)
{
// Try using unit X as candidate
Vec3 candidate = Vec3.UnitX;
if (Math.Abs(Vec3.Dot(vec, candidate)) > 0.9f)
{
// If too aligned, use unit Y
candidate = Vec3.UnitY;
}
if (Math.Abs(Vec3.Dot(vec, candidate)) > 0.9f)
{
// If still too aligned, use unit Z
candidate = Vec3.UnitZ;
}
// Make orthogonal using Gram-Schmidt
return Vec3.Normalize(candidate - vec * Vec3.Dot(candidate, vec));
}
private float ApplyPanRangeAndWrite(float panVal,
int panChannel,
int panFineChannel,
Vector2 panRangeDegrees,
bool invertPan,
bool shouldLog)
{
if (panRangeDegrees.X >= panRangeDegrees.Y)
{
Log.Warning("Pan range min must be < max.", this);
SetDmxValue(0f, panChannel, panFineChannel, 0f, 1f, shouldLog, "Pan");
return 0f;
}
float panMinRad = panRangeDegrees.X * MathF.PI / 180f;
float panMaxRad = panRangeDegrees.Y * MathF.PI / 180f;
if (invertPan)
{
panVal = panMaxRad + panMinRad - panVal;
if (shouldLog) Log.Debug($"Pan (after inversion): {panVal * 180f / MathF.PI:F2} deg", this);
}
float finalPan = Math.Clamp(panVal, panMinRad, panMaxRad);
if (shouldLog)
Log.Debug($"Final Pan (clamped to {panRangeDegrees.X:F2}-{panRangeDegrees.Y:F2} deg): {finalPan * 180f / MathF.PI:F2} deg ({finalPan:F4} rad)", this);
SetDmxValue(finalPan,
panChannel,
panFineChannel,
panMinRad,
panMaxRad,
shouldLog,
"Pan");
return finalPan;
}
private float ApplyTiltRangeAndWrite(float tiltVal,
int tiltChannel,
int tiltFineChannel,
Vector2 tiltRangeDegrees,
bool invertTilt,
bool shouldLog)
{
if (tiltRangeDegrees.X >= tiltRangeDegrees.Y)
{
Log.Warning("Tilt range min must be < max.", this);
SetDmxValue(0f, tiltChannel, tiltFineChannel, 0f, 1f, shouldLog, "Tilt");
return 0f;
}
float tiltMinRad = tiltRangeDegrees.X * MathF.PI / 180f;
float tiltMaxRad = tiltRangeDegrees.Y * MathF.PI / 180f;
if (invertTilt)
{
tiltVal = tiltMaxRad + tiltMinRad - tiltVal;
if (shouldLog) Log.Debug($"Tilt (after inversion): {tiltVal * 180f / MathF.PI:F2} deg", this);
}
float finalTilt = Math.Clamp(tiltVal, tiltMinRad, tiltMaxRad);
if (shouldLog)
Log.Debug($"Final Tilt (clamped to {tiltRangeDegrees.X:F2}-{tiltRangeDegrees.Y:F2} deg): {finalTilt * 180f / MathF.PI:F2} deg ({finalTilt:F4} rad)", this);
SetDmxValue(finalTilt,
tiltChannel,
tiltFineChannel,
tiltMinRad,
tiltMaxRad,
shouldLog,
"Tilt");
return finalTilt;
}
#endregion
#region Colour / Feature Handling
private void HandleColorAndFeatures(EvaluationContext context,
Point[] points,
Point transformPoint,
int firstPixelIdx,
int pixelsPerFixture)
{
bool getColor = GetColor.GetValue(context);
bool getF1 = GetF1.GetValue(context);
bool getF2 = GetF2.GetValue(context);
bool f1ByPixel = GetF1ByPixel.GetValue(context);
bool f2ByPixel = GetF2ByPixel.GetValue(context);
bool useCmy = RgbToCmy.GetValue(context);
bool is16BitColor = Is16BitColor.GetValue(context);
int redChBase = RedChannel.GetValue(context);
int greenChBase = GreenChannel.GetValue(context);
int blueChBase = BlueChannel.GetValue(context);
int whiteChBase = WhiteChannel.GetValue(context);
int alphaChBase = AlphaChannel.GetValue(context);
int f1ChBase = F1Channel.GetValue(context);
int f2ChBase = F2Channel.GetValue(context);
bool hasAnyPerPixelAttributes = (getColor && (redChBase > 0 || greenChBase > 0 || blueChBase > 0 || whiteChBase > 0 || alphaChBase > 0)) ||
(getF1 && f1ByPixel && f1ChBase > 0) ||
(getF2 && f2ByPixel && f2ChBase > 0);
int overallMinPerPixelChannel = int.MaxValue;
int overallMaxPerPixelChannel = int.MinValue;
if (hasAnyPerPixelAttributes)
{
int channelsPerColorDmxValue = is16BitColor ? 2 : 1;
int channelsPerFeatureDmxValue = 1;
if (getColor)
{
if (redChBase > 0) { overallMinPerPixelChannel = Math.Min(overallMinPerPixelChannel, redChBase); overallMaxPerPixelChannel = Math.Max(overallMaxPerPixelChannel, redChBase + channelsPerColorDmxValue - 1); }
if (greenChBase > 0) { overallMinPerPixelChannel = Math.Min(overallMinPerPixelChannel, greenChBase); overallMaxPerPixelChannel = Math.Max(overallMaxPerPixelChannel, greenChBase + channelsPerColorDmxValue - 1); }
if (blueChBase > 0) { overallMinPerPixelChannel = Math.Min(overallMinPerPixelChannel, blueChBase); overallMaxPerPixelChannel = Math.Max(overallMaxPerPixelChannel, blueChBase + channelsPerColorDmxValue - 1); }
if (whiteChBase > 0) { overallMinPerPixelChannel = Math.Min(overallMinPerPixelChannel, whiteChBase); overallMaxPerPixelChannel = Math.Max(overallMaxPerPixelChannel, whiteChBase + channelsPerColorDmxValue - 1); }
if (alphaChBase > 0) { overallMinPerPixelChannel = Math.Min(overallMinPerPixelChannel, alphaChBase); overallMaxPerPixelChannel = Math.Max(overallMaxPerPixelChannel, alphaChBase + channelsPerColorDmxValue - 1); }
}
if (getF1 && f1ByPixel && f1ChBase > 0) { overallMinPerPixelChannel = Math.Min(overallMinPerPixelChannel, f1ChBase); overallMaxPerPixelChannel = Math.Max(overallMaxPerPixelChannel, f1ChBase + channelsPerFeatureDmxValue - 1); }
if (getF2 && f2ByPixel && f2ChBase > 0) { overallMinPerPixelChannel = Math.Min(overallMinPerPixelChannel, f2ChBase); overallMaxPerPixelChannel = Math.Max(overallMaxPerPixelChannel, f2ChBase + channelsPerFeatureDmxValue - 1); }
}
int pixelChannelStride = 0;
if (overallMinPerPixelChannel != int.MaxValue && overallMaxPerPixelChannel != int.MinValue)
{
pixelChannelStride = overallMaxPerPixelChannel - overallMinPerPixelChannel + 1;
if (DebugToLog.GetValue(context)) Log.Debug($"Calculated pixelChannelStride: {pixelChannelStride} (min: {overallMinPerPixelChannel}, max: {overallMaxPerPixelChannel})", this);
}
else if (hasAnyPerPixelAttributes && DebugToLog.GetValue(context))
{
Log.Warning("Per-pixel attributes are enabled, but all associated DMX channels are 0 or less. No per-pixel output will be generated.", this);
}
for (int pix = 0; pix < pixelsPerFixture; ++pix)
{
Point pt = points[firstPixelIdx + pix];
void WriteSinglePixelDmxValue(int baseChannel, float normalizedValue, bool is16BitChannel, string debugName)
{
if (baseChannel <= 0) return;
if (pixelChannelStride == 0) return;
int relativeOffsetFromOverallMin = baseChannel - overallMinPerPixelChannel;
int actualDmxCoarseChannel = overallMinPerPixelChannel + (pix * pixelChannelStride) + relativeOffsetFromOverallMin;
int actualDmxFineChannel = is16BitChannel ? actualDmxCoarseChannel + 1 : 0;
SetDmxValue(normalizedValue,
actualDmxCoarseChannel,
actualDmxFineChannel,
0f, 1f,
false,
$"Pixel{pix}-{debugName}");
}
if (getColor)
{
if (redChBase > 0 || greenChBase > 0 || blueChBase > 0 || whiteChBase > 0 || alphaChBase > 0)
{
float r = float.IsNaN(pt.Color.X) ? 0f : Math.Clamp(pt.Color.X, 0f, 1f);
float g = float.IsNaN(pt.Color.Y) ? 0f : Math.Clamp(pt.Color.Y, 0f, 1f);
float b = float.IsNaN(pt.Color.Z) ? 0f : Math.Clamp(pt.Color.Z, 0f, 1f);
float a = float.IsNaN(pt.Color.W) ? 1f : Math.Clamp(pt.Color.W, 0f, 1f);
if (useCmy) { r = 1f - r; g = 1f - g; b = 1f - b; }
WriteSinglePixelDmxValue(redChBase, r, is16BitColor, "Red");
if (greenChBase > 0) WriteSinglePixelDmxValue(greenChBase, g, is16BitColor, "Green");
if (blueChBase > 0) WriteSinglePixelDmxValue(blueChBase, b, is16BitColor, "Blue");
if (whiteChBase > 0)
{
float w = Math.Min(r, Math.Min(g, b));
WriteSinglePixelDmxValue(whiteChBase, w, is16BitColor, "White");
}
if (alphaChBase > 0)
{
WriteSinglePixelDmxValue(alphaChBase, a, is16BitColor, "Alpha");
}
}
}
if (getF1 && f1ByPixel && f1ChBase > 0)
{
float f1 = float.IsNaN(pt.F1) ? 0f : Math.Clamp(pt.F1, 0f, 1f);
WriteSinglePixelDmxValue(f1ChBase, f1, false, "F1");
}
if (getF2 && f2ByPixel && f2ChBase > 0)
{
float f2 = float.IsNaN(pt.F2) ? 0f : Math.Clamp(pt.F2, 0f, 1f);
WriteSinglePixelDmxValue(f2ChBase, f2, false, "F2");
}
}
// Fixturewide (nonperpixel) F1/F2
if (getF1 && !f1ByPixel && f1ChBase > 0)
{
float f1Val = float.IsNaN(transformPoint.F1) ? 0f : Math.Clamp(transformPoint.F1, 0f, 1f);
SetDmxValue(f1Val, f1ChBase, 0, 0f, 1f, DebugToLog.GetValue(context), "FixtureF1");
}
if (getF2 && !f2ByPixel && f2ChBase > 0)
{
float f2Val = float.IsNaN(transformPoint.F2) ? 0f : Math.Clamp(transformPoint.F2, 0f, 1f);
SetDmxValue(f2Val, f2ChBase, 0, 0f, 1f, DebugToLog.GetValue(context), "FixtureF2");
}
}
#endregion
#region Custom Variable Handling
private void HandleCustomVariables(EvaluationContext context)
{
const float customVarNormalizedMax = 255f;
bool shouldLog = DebugToLog.GetValue(context);
// Get the input lists
var values = CustomVariableValues.GetValue(context);
var channels = CustomVariableChannels.GetValue(context);
// Validate inputs
if (channels == null)
{
if (shouldLog) Log.Debug("CustomVariableChannels list is null, skipping custom variables.", this);
return;
}
if (channels.Count == 0)
{
if (shouldLog) Log.Debug("CustomVariableChannels list is empty, skipping custom variables.", this);
return;
}
// Create working copy of channels to avoid modifying the original
var workingChannels = new List<int>(channels);
// Auto-resize values list to match channels size
var workingValues = new List<int>();
if (values != null)
{
workingValues = new List<int>(values);
}
// Ensure values list size matches channels size
if (workingValues.Count < workingChannels.Count)
{
// Extend values with default DMX value (128)
int valuesToAdd = workingChannels.Count - workingValues.Count;
for (int i = 0; i < valuesToAdd; i++)
{
workingValues.Add(128); // Default DMX value (middle of 0-255 range)
}
if (shouldLog) Log.Debug($"Auto-resized CustomVariableValues: extended from {values?.Count ?? 0} to {workingValues.Count} elements with default value 128", this);
}
else if (workingValues.Count > workingChannels.Count)
{
// Trim excess values to match channels size
int excessCount = workingValues.Count - workingChannels.Count;
workingValues.RemoveRange(workingChannels.Count, excessCount);
if (shouldLog) Log.Debug($"Auto-trimmed CustomVariableValues: reduced from {values.Count} to {workingValues.Count} elements to match channels size", this);
}
// Process each channel-value pair
for (int i = 0; i < workingChannels.Count; i++)
{
int channel = workingChannels[i];
int value = workingValues[i];
// Validate channel number
if (channel <= 0)
{
if (shouldLog) Log.Debug($"Skipping custom variable at index {i}: invalid channel {channel}", this);
continue;
}
// Clamp value to valid DMX range
int clampedValue = Math.Clamp(value, 0, (int)customVarNormalizedMax);
// Set the DMX value
SetDmxValue(clampedValue, channel, 0, 0f, customVarNormalizedMax, shouldLog, $"CustomVar[{i}]");
}
}
#endregion
#region DMX Helper Methods
private void SetDmxValue(float value,
int coarseChannel,
int fineChannel,
float inMin,
float inMax,
bool shouldLog,
string name)
{
if (coarseChannel <= 0)
{
if (shouldLog) Log.Debug($"Skipping DMX write for {name}: Coarse Channel is 0 or less.", this);
return;
}
int listCoarseIndex = coarseChannel - 1;
if (fineChannel > 0)
{
int dmx16 = MapToDmx16(value, inMin, inMax);
if (shouldLog)
Log.Debug($"{name} DMX Channel: {coarseChannel}/{fineChannel} (16-bit), Input Value: {value:F4}, Mapped DMX (16bit): {dmx16}, Range: [{inMin:F4}, {inMax:F4}]", this);
InsertOrSet(listCoarseIndex, (dmx16 >> 8) & 0xFF);
InsertOrSet(fineChannel - 1, dmx16 & 0xFF);
}
else
{
float range = inMax - inMin;
float normalized = Math.Clamp((value - inMin) / (range), 0f, 1f);
int dmx8 = (int)Math.Round(normalized * 255.0f);
if (shouldLog)
Log.Debug($"{name} DMX Channel: {coarseChannel} (8-bit), Input Value: {value:F4}, Mapped DMX (8bit): {dmx8}, Range: [{inMin:F4}, {inMax:F4}]", this);
InsertOrSet(listCoarseIndex, dmx8);
}
}
private static int MapToDmx16(float value, float inMin, float inMax)
{
float range = inMax - inMin;
if (Math.Abs(range) < 1e-4f) return 0;
float normalized = (value - inMin) / range;
return (int)Math.Round(Math.Clamp(normalized, 0f, 1f) * 65535f);
}
private void InsertOrSet(int index, int value)
{
if (index < 0)
return;
if (index >= _pointChannelValues.Count)
{
Log.Warning($"DMX channel list index {index + 1} out of range (list size {_pointChannelValues.Count}). " +
$"Increase 'Fixture Channel Size' if you are using high channel numbers or 16-bit channels for multiple pixels.", this);
return;
}
_pointChannelValues[index] = value;
}
#endregion
#region Visualisation Buffer
private void UpdateVisualizationBuffer()
{
if (_visualizationPoints == null || _visualizationPoints.Length == 0)
{
_visualizeBuffer = null;
return;
}
int pointCount = _visualizationPoints.Length;
int stride = Point.Stride;
if (stride <= 0)
return;
Buffer buffer = null;
ShaderResourceView srv = null;
UnorderedAccessView uav = null;
if (_visualizeBuffer != null)
{
buffer = _visualizeBuffer.Buffer;
srv = _visualizeBuffer.Srv;
uav = _visualizeBuffer.Uav;
}
ResourceManager.SetupStructuredBuffer(_visualizationPoints,
stride * pointCount,
stride,
ref buffer);
ResourceManager.CreateStructuredBufferSrv(buffer, ref srv);
ResourceManager.CreateStructuredBufferUav(buffer,
UnorderedAccessViewBufferFlags.None,
ref uav);
if (_visualizeBuffer == null)
_visualizeBuffer = new BufferWithViews();
_visualizeBuffer.Buffer = buffer;
_visualizeBuffer.Srv = srv;
_visualizeBuffer.Uav = uav;
}
#endregion
#region Shortest Path Optimization
private (float pan, float tilt) GetOptimizedPanTilt(float rawPan, float rawTilt, EvaluationContext context, Vector2 lastState)
{
if (float.IsNaN(lastState.X) || float.IsNaN(lastState.Y))
{
// No previous state, just use the normal path centered in range
float pan = FitToRange(rawPan, PanRange.GetValue(context), false, float.NaN);
float tilt = FitToRange(rawTilt, TiltRange.GetValue(context), false, float.NaN);
return (pan, tilt);
}
// Candidate A: Normal path
float panA = FitToRange(rawPan, PanRange.GetValue(context), true, lastState.X);
float tiltA = FitToRange(rawTilt, TiltRange.GetValue(context), true, lastState.Y);
// Candidate B: Flipped path (pan + 180, 180 - tilt)
float panB = FitToRange(rawPan + MathF.PI, PanRange.GetValue(context), true, lastState.X);
float tiltB = FitToRange(MathF.PI - rawTilt, TiltRange.GetValue(context), true, lastState.Y);
// Calculate costs (weighted angular distance, penalizing out-of-range)
float Cost(float p, float t, Vector2 pRange, Vector2 tRange)
{
float pMin = pRange.X * MathF.PI / 180f;
float pMax = pRange.Y * MathF.PI / 180f;
float tMin = tRange.X * MathF.PI / 180f;
float tMax = tRange.Y * MathF.PI / 180f;
float pDist = Math.Abs(p - lastState.X);
float tDist = Math.Abs(t - lastState.Y);
// Add heavy penalty if outside the valid range
float pPenalty = (p < pMin || p > pMax) ? 1000f : 0f;
float tPenalty = (t < tMin || t > tMax) ? 1000f : 0f;
return pDist + tDist + pPenalty + tPenalty;
}
float costA = Cost(panA, tiltA, PanRange.GetValue(context), TiltRange.GetValue(context));
float costB = Cost(panB, tiltB, PanRange.GetValue(context), TiltRange.GetValue(context));
if (DebugToLog.GetValue(context))
{
Log.Debug($"Path A: pan={panA*180/MathF.PI:F1}, tilt={tiltA*180/MathF.PI:F1}, cost={costA:F2}", this);
Log.Debug($"Path B: pan={panB*180/MathF.PI:F1}, tilt={tiltB*180/MathF.PI:F1}, cost={costB:F2}", this);
}
return costB < costA ? (panB, tiltB) : (panA, tiltA);
}
private float FitToRange(float rawVal, Vector2 rangeDeg, bool useShortestPath, float lastValRad)
{
float val = rawVal;
float minRad = rangeDeg.X * MathF.PI / 180f;
float maxRad = rangeDeg.Y * MathF.PI / 180f;
if (!useShortestPath || float.IsNaN(lastValRad))
{
// Normalize to [-PI, PI]
val = MathUtils.Fmod(val + MathF.PI, 2 * MathF.PI) - MathF.PI;
// Shift to be close to the center of the allowed range
float rangeCenterRad = (minRad + maxRad) / 2f;
float turnsToCenter = MathF.Round((val - rangeCenterRad) / (2 * MathF.PI));
val -= turnsToCenter * 2 * MathF.PI;
}
else
{
// Shortest path logic: find the rotation multiple that gets closest to the last value
// AND respects the range if possible.
float baseVal = MathUtils.Fmod(val + MathF.PI, 2 * MathF.PI) - MathF.PI;
// Calculate the "ideal" number of turns to be closest to lastVal
float idealTurns = MathF.Round((lastValRad - baseVal) / (2 * MathF.PI));
float candidate = baseVal + idealTurns * 2 * MathF.PI;
// If the candidate is inside the range, great.
if (candidate >= minRad && candidate <= maxRad)
{
val = candidate;
}
else
{
// If not, check neighbors (idealTurns - 1, idealTurns + 1) to see if they are in range.
// We prioritize "being in range" over "being closest to lastVal" to avoid penalties.
float prev = baseVal + (idealTurns - 1) * 2 * MathF.PI;
float next = baseVal + (idealTurns + 1) * 2 * MathF.PI;
bool prevInRange = prev >= minRad && prev <= maxRad;
bool nextInRange = next >= minRad && next <= maxRad;
if (prevInRange && !nextInRange) val = prev;
else if (!prevInRange && nextInRange) val = next;
else if (prevInRange && nextInRange)
{
// Both in range? Pick closest.
if (Math.Abs(prev - lastValRad) < Math.Abs(next - lastValRad)) val = prev;
else val = next;
}
else
{
// None are in range. Stick with the closest one (candidate).
val = candidate;
}
}
}
return val;
}
#endregion
}
}