5.14 시작전
This commit is contained in:
@@ -155,7 +155,9 @@ internal static class CurrentApiCutDiagnostics
|
||||
districtCache,
|
||||
electionType,
|
||||
station,
|
||||
options.RegionScope == "all" || IsNormalPanseMapTemplate(template))
|
||||
options.RegionScope == "all" || IsNormalPanseMapTemplate(template),
|
||||
template,
|
||||
preElectionHistoryService)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -620,6 +622,10 @@ internal static class CurrentApiCutDiagnostics
|
||||
? overview.TurnoutVotes
|
||||
: primaryItem?.TurnoutVotes ?? 0;
|
||||
|
||||
var snapshotReferenceTimeLabel = includeNationalSlot
|
||||
? overview.ReferenceTimeLabel
|
||||
: FirstNonWhiteSpace(primaryItem?.ReferenceTimeLabel, overview.ReferenceTimeLabel);
|
||||
|
||||
return new ElectionDataSnapshot
|
||||
{
|
||||
BroadcastPhase = BroadcastPhase.PreElection,
|
||||
@@ -634,7 +640,8 @@ internal static class CurrentApiCutDiagnostics
|
||||
CountedVotesFromApi = null,
|
||||
RemainingVotesFromApi = null,
|
||||
CountedRateFromApi = null,
|
||||
ReceivedAt = DateTimeOffset.Now,
|
||||
ReceivedAt = overview.ReceivedAt == default ? DateTimeOffset.Now : overview.ReceivedAt,
|
||||
ReferenceTimeLabel = snapshotReferenceTimeLabel,
|
||||
TurnoutBoardSlots = turnoutBoardSlots,
|
||||
NationalTurnoutRateOverride = overview.NationalTurnoutRate
|
||||
};
|
||||
@@ -1104,6 +1111,7 @@ internal static class CurrentApiCutDiagnostics
|
||||
RemainingVotesFromApi = null,
|
||||
CountedRateFromApi = null,
|
||||
ReceivedAt = overview.ReceivedAt == default ? DateTimeOffset.Now : overview.ReceivedAt,
|
||||
ReferenceTimeLabel = FirstNonWhiteSpace(item.ReferenceTimeLabel, overview.ReferenceTimeLabel),
|
||||
NationalTurnoutRateOverride = overview.NationalTurnoutRate
|
||||
};
|
||||
}
|
||||
@@ -1318,8 +1326,15 @@ internal static class CurrentApiCutDiagnostics
|
||||
IDictionary<string, IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>> districtCache,
|
||||
string electionType,
|
||||
BroadcastStationProfile station,
|
||||
bool useAllRegions = false)
|
||||
bool useAllRegions,
|
||||
FormatTemplateDefinition template,
|
||||
PreElectionHistoryService preElectionHistoryService)
|
||||
{
|
||||
if (UsesHistoricalStoredOptions(template))
|
||||
{
|
||||
return GetStoredHistoryDistricts(electionType, preElectionHistoryService);
|
||||
}
|
||||
|
||||
var regionFilters = useAllRegions ? Array.Empty<string>() : station.RegionFilters;
|
||||
var cacheKey = $"{electionType}|{string.Join(",", regionFilters)}";
|
||||
if (!districtCache.TryGetValue(cacheKey, out var districts))
|
||||
@@ -1333,6 +1348,59 @@ internal static class CurrentApiCutDiagnostics
|
||||
return districts;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> GetStoredHistoryDistricts(
|
||||
string electionType,
|
||||
PreElectionHistoryService preElectionHistoryService)
|
||||
{
|
||||
return preElectionHistoryService
|
||||
.GetSelectionRecords(electionType)
|
||||
.Where(record => !string.IsNullOrWhiteSpace(ResolveStoredHistoryDisplayName(record)))
|
||||
.OrderBy(record => SbsElectionApiClient.ResolveBasicApiSidoCode(record.RegionName), StringComparer.Ordinal)
|
||||
.ThenBy(record => ResolveStoredHistoryDisplayName(record), StringComparer.Ordinal)
|
||||
.Select(CreateStoredHistoryDistrict)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static SbsElectionApiClient.DistrictSelectionOption CreateStoredHistoryDistrict(
|
||||
PreElectionHistoryRecord record)
|
||||
{
|
||||
var regionName = string.IsNullOrWhiteSpace(record.RegionName)
|
||||
? record.DisplayName
|
||||
: record.RegionName;
|
||||
var districtName = string.IsNullOrWhiteSpace(record.DistrictName)
|
||||
? ResolveStoredHistoryDisplayName(record)
|
||||
: record.DistrictName;
|
||||
var parentRegionCode = SbsElectionApiClient.ResolveBasicApiSidoCode(regionName);
|
||||
var districtCode = string.Equals(
|
||||
PreElectionHistoryService.NormalizeElectionType(record.ElectionType),
|
||||
"기초단체장",
|
||||
StringComparison.Ordinal)
|
||||
? record.Key
|
||||
: parentRegionCode;
|
||||
|
||||
return new SbsElectionApiClient.DistrictSelectionOption(
|
||||
ResolveStoredHistoryDisplayName(record),
|
||||
districtCode,
|
||||
regionName,
|
||||
districtName,
|
||||
parentRegionCode);
|
||||
}
|
||||
|
||||
private static string ResolveStoredHistoryDisplayName(PreElectionHistoryRecord record)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(record.DisplayName))
|
||||
{
|
||||
return record.DisplayName;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(record.DistrictName))
|
||||
{
|
||||
return record.DistrictName;
|
||||
}
|
||||
|
||||
return record.RegionName ?? string.Empty;
|
||||
}
|
||||
|
||||
private static IEnumerable<SbsElectionApiClient.DistrictSelectionOption> ResolveTargets(
|
||||
IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> districts,
|
||||
BroadcastStationProfile station,
|
||||
@@ -1398,7 +1466,8 @@ internal static class CurrentApiCutDiagnostics
|
||||
CountedVotesFromApi = refreshResult.CountedVotes,
|
||||
RemainingVotesFromApi = refreshResult.RemainingVotes,
|
||||
CountedRateFromApi = refreshResult.CountedRate,
|
||||
ReceivedAt = refreshResult.ReceivedAt == default ? DateTimeOffset.Now : refreshResult.ReceivedAt
|
||||
ReceivedAt = refreshResult.ReceivedAt == default ? DateTimeOffset.Now : refreshResult.ReceivedAt,
|
||||
ReferenceTimeLabel = refreshResult.ReferenceTimeLabel
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2297,12 +2366,22 @@ internal static class CurrentApiCutDiagnostics
|
||||
return "기초단체장";
|
||||
}
|
||||
|
||||
if (ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(template.Name))
|
||||
{
|
||||
return "광역단체장";
|
||||
}
|
||||
|
||||
return ResolveScheduleElectionType(template.Name, phase, defaultElectionType);
|
||||
}
|
||||
|
||||
private static string ResolveScheduleElectionType(string? formatName, BroadcastPhase phase, string defaultElectionType)
|
||||
{
|
||||
var resolvedFormatName = formatName ?? string.Empty;
|
||||
if (ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(resolvedFormatName))
|
||||
{
|
||||
return "광역단체장";
|
||||
}
|
||||
|
||||
if (resolvedFormatName.Contains("교육감", StringComparison.Ordinal))
|
||||
{
|
||||
return "교육감";
|
||||
@@ -2343,6 +2422,12 @@ internal static class CurrentApiCutDiagnostics
|
||||
return defaultElectionType;
|
||||
}
|
||||
|
||||
private static bool UsesHistoricalStoredOptions(FormatTemplateDefinition template)
|
||||
{
|
||||
return ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(template.Name) ||
|
||||
ScheduleTemplatePolicy.IsHistoricalWinnerFormat(template.Name);
|
||||
}
|
||||
|
||||
private static bool SupportsPreElectionTurnout(string? electionType)
|
||||
{
|
||||
return string.Equals(electionType, "광역단체장", StringComparison.Ordinal) ||
|
||||
|
||||
@@ -274,6 +274,7 @@ internal static class CutFileAudit
|
||||
payload.CounterNumberKeys,
|
||||
Array.Empty<KarismaChartCellUpdate>(),
|
||||
Array.Empty<KarismaPositionUpdate>(),
|
||||
Array.Empty<KarismaCropKeyUpdate>(),
|
||||
payload.StyleColorUpdates,
|
||||
payload.VisibilityUpdates,
|
||||
CancellationToken.None)
|
||||
@@ -524,6 +525,10 @@ internal static class CutFileAudit
|
||||
string scenario,
|
||||
string frameLabel)
|
||||
{
|
||||
_ = ShowWindow(pgmWindow.Handle, ShowWindowRestore);
|
||||
_ = SetForegroundWindow(pgmWindow.Handle);
|
||||
Thread.Sleep(80);
|
||||
|
||||
var fileName = $"{result.Index:000}_{SanitizeFileName(result.FolderName)}_{SanitizeFileName(result.BaseName)}_{scenario.ToLowerInvariant()}_{frameLabel}.png";
|
||||
var outputPath = Path.Combine(options.CapturePath, fileName);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
|
||||
@@ -961,6 +966,12 @@ internal static class CutFileAudit
|
||||
{
|
||||
var normalizedBaseName = NormalizeVariantName(result.BaseName);
|
||||
var explicitBaseName = TryResolveExplicitRgbSpec(result.FolderName, normalizedBaseName);
|
||||
if (explicitBaseName is not null && string.IsNullOrWhiteSpace(explicitBaseName))
|
||||
{
|
||||
mappingKind = "explicit-none";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(explicitBaseName) &&
|
||||
rgbCatalog.TryGetValue(BuildRgbCatalogKey(result.FolderName, explicitBaseName), out var explicitSpec))
|
||||
{
|
||||
@@ -1027,11 +1038,12 @@ internal static class CutFileAudit
|
||||
Add("Elect2026_Normal_민방", "모든후보_교육감", "모든후보_교육감");
|
||||
Add("Elect2026_Normal_민방", "사전_역대당선", "사전_역대당선자", "사전_역대당선자_기초단체장");
|
||||
Add("Elect2026_Normal_민방", "사전_역대당선_교육감", "사전_역대당선자_교육감");
|
||||
Add("Elect2026_Normal_민방", "이시각1위_광역단체장", "이시각1위_광역단체장");
|
||||
Add("Elect2026_Normal_민방", "이시각1위_광역단체장_5760", "이시각1위_광역단체장_5760");
|
||||
Add("Elect2026_Normal_민방", "이시각1위_기초단체장(5760동일)", "이시각1위_기초단체장");
|
||||
Add("Elect2026_Normal_민방", string.Empty, "사전_역대투표율");
|
||||
Add("Elect2026_Normal_민방", "이시각1위_광역단체장", "이시각1위_광역단체장", "이시각1위_광역단체장_HD");
|
||||
Add("Elect2026_Normal_민방", "이시각1위_광역단체장_5760", "이시각1위_광역단체장_5760", "이시각1위_광역단체장_L");
|
||||
Add("Elect2026_Normal_민방", "이시각1위_기초단체장(5760동일)", "이시각1위_기초단체장", "이시각1위_기초단체장_HD", "이시각1위_기초단체장_L");
|
||||
Add("Elect2026_Normal_민방", "접전,초접전", "접전_광역단체장", "접전_기초단체장", "초접전_광역단체장", "초접전_기초단체장");
|
||||
Add("Elect2026_Normal_민방", "판세_광역단체장", "판세_광역단체장", "판세_기초단체장");
|
||||
Add("Elect2026_Normal_민방", "판세_광역단체장", "판세_광역단체장", "판세_기초단체장", "역대시도판세_광역단체장", "역대시도판세_기초단체장");
|
||||
Add("Elect2026_Bottom_민방", "1-2위, 1-3위, 이시각1위", "1-2위_광역단체장", "1-2위_기초단체장", "1-3위_광역단체장", "1-3위_기초단체장", "1위_광역단체장", "1위_기초단체장");
|
||||
Add("Elect2026_Bottom_민방", "당선", "당선_광역단체장", "당선_광역의원", "당선_기초단체장", "당선_기초의원");
|
||||
Add("Elect2026_Bottom_민방", "모든후보", "전후보_광역단체장", "전후보_기초단체장");
|
||||
@@ -1296,29 +1308,62 @@ internal static class CutFileAudit
|
||||
|
||||
private static PgmWindow? TryFindPgmWindow()
|
||||
{
|
||||
var process = Process.GetProcessesByName("Tornado3")
|
||||
.FirstOrDefault(candidate => string.Equals(candidate.MainWindowTitle, "PGM", StringComparison.Ordinal));
|
||||
if (process is null || process.MainWindowHandle == IntPtr.Zero)
|
||||
var handle = IntPtr.Zero;
|
||||
var tornadoProcessIds = Process.GetProcessesByName("Tornado3")
|
||||
.Select(process => process.Id)
|
||||
.ToHashSet();
|
||||
|
||||
EnumWindows((candidateHandle, lParam) =>
|
||||
{
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = GetWindowThreadProcessId(candidateHandle, out var processId);
|
||||
if (!tornadoProcessIds.Contains((int)processId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var titleLength = GetWindowTextLength(candidateHandle);
|
||||
if (titleLength <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var title = new StringBuilder(titleLength + 1);
|
||||
_ = GetWindowText(candidateHandle, title, title.Capacity);
|
||||
if (string.Equals(title.ToString(), "PGM", StringComparison.Ordinal))
|
||||
{
|
||||
handle = candidateHandle;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (TryGetDwmExtendedFrameBounds(process.MainWindowHandle, out var dwmBounds))
|
||||
if (TryGetDwmExtendedFrameBounds(handle, out var dwmBounds))
|
||||
{
|
||||
return new PgmWindow(process.MainWindowHandle, dwmBounds);
|
||||
return new PgmWindow(handle, dwmBounds);
|
||||
}
|
||||
|
||||
if (TryGetClientBounds(process.MainWindowHandle, out var clientBounds))
|
||||
if (TryGetClientBounds(handle, out var clientBounds))
|
||||
{
|
||||
return new PgmWindow(process.MainWindowHandle, clientBounds);
|
||||
return new PgmWindow(handle, clientBounds);
|
||||
}
|
||||
|
||||
if (!GetWindowRect(process.MainWindowHandle, out var rect))
|
||||
if (!GetWindowRect(handle, out var rect))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PgmWindow(process.MainWindowHandle, new Rect(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top));
|
||||
return new PgmWindow(handle, new Rect(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top));
|
||||
}
|
||||
|
||||
private static bool TryGetDwmExtendedFrameBounds(IntPtr handle, out Rect bounds)
|
||||
@@ -1544,6 +1589,27 @@ internal static class CutFileAudit
|
||||
return value.Replace("|", "\\|", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowTextLength(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetWindowRect(IntPtr hWnd, out NativeRect lpRect);
|
||||
@@ -1560,6 +1626,8 @@ internal static class CutFileAudit
|
||||
private static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out NativeRect pvAttribute, int cbAttribute);
|
||||
|
||||
private const int DwmwaExtendedFrameBounds = 9;
|
||||
private const int ShowWindowRestore = 9;
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
private readonly record struct AuditScene(string ScenePath, string RelativePath, string FolderName, string BaseName, BroadcastChannel Channel);
|
||||
private readonly record struct AuditChannelBinding(int OutputChannelIndex, int LayerNo);
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<Compile Include="..\..\Tornado3_2026Election\Services\ITornado3Adapter.cs" Link="AppSource\Services\ITornado3Adapter.cs" />
|
||||
<Compile Include="..\..\Tornado3_2026Election\Services\KarismaChartCellUpdate.cs" Link="AppSource\Services\KarismaChartCellUpdate.cs" />
|
||||
<Compile Include="..\..\Tornado3_2026Election\Services\KarismaCounterNumberKeyUpdate.cs" Link="AppSource\Services\KarismaCounterNumberKeyUpdate.cs" />
|
||||
<Compile Include="..\..\Tornado3_2026Election\Services\KarismaCropKeyUpdate.cs" Link="AppSource\Services\KarismaCropKeyUpdate.cs" />
|
||||
<Compile Include="..\..\Tornado3_2026Election\Services\KarismaEventHandler.cs" Link="AppSource\Services\KarismaEventHandler.cs" />
|
||||
<Compile Include="..\..\Tornado3_2026Election\Services\KarismaPositionUpdate.cs" Link="AppSource\Services\KarismaPositionUpdate.cs" />
|
||||
<Compile Include="..\..\Tornado3_2026Election\Services\KarismaSceneResolutionReader.cs" Link="AppSource\Services\KarismaSceneResolutionReader.cs" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using KAsyncEngineLib;
|
||||
@@ -319,6 +321,12 @@ if (args.Length > 0 && string.Equals(args[0], "--validate-live-cuts", StringComp
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 0 && string.Equals(args[0], "--audit-party-colors-live", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Environment.ExitCode = await LiveCutValidation.RunPartyColorAuditAsync(args[1..]).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 0 && string.Equals(args[0], "--validate-current-api-cuts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Environment.ExitCode = await CurrentApiCutDiagnostics.RunAsync(args[1..]).ConfigureAwait(false);
|
||||
@@ -876,6 +884,14 @@ static Task<SaveSceneImageProbeResult> SaveSceneImageAsync(SaveSceneImageOptions
|
||||
var operationResult = ApplySceneOperation(handler, scene!, operation, options.Connection.Timeout);
|
||||
if (!string.Equals(operationResult.Result, eKResult.RESULT_SUCCESS.ToString(), StringComparison.Ordinal))
|
||||
{
|
||||
if (operation.ContinueOnFailure)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[SAVE-IMAGE] Optional operation {operationResult.Method} failed for '{operationResult.ObjectName}': " +
|
||||
$"{operationResult.Result} {operationResult.Detail}");
|
||||
continue;
|
||||
}
|
||||
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(
|
||||
true,
|
||||
@@ -1001,31 +1017,38 @@ static Task<SaveSceneImageProbeResult> SaveSceneImageAsync(SaveSceneImageOptions
|
||||
}
|
||||
}
|
||||
|
||||
var positionKeyUpdates = new List<PositionKeyUpdate>();
|
||||
if (options.PositionKey is not null)
|
||||
{
|
||||
positionKeyUpdates.Add(options.PositionKey);
|
||||
}
|
||||
|
||||
positionKeyUpdates.AddRange(options.PositionKeys);
|
||||
foreach (var positionKeyUpdate in positionKeyUpdates)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[SAVE-IMAGE] Setting position key object={options.PositionKey.ObjectName} index={options.PositionKey.KeyIndex} " +
|
||||
$"value=({options.PositionKey.X},{options.PositionKey.Y},{options.PositionKey.Z}) vector={options.PositionKey.VectorType}...");
|
||||
var sceneObject = scene.GetObject(options.PositionKey.ObjectName);
|
||||
$"[SAVE-IMAGE] Setting position key object={positionKeyUpdate.ObjectName} index={positionKeyUpdate.KeyIndex} " +
|
||||
$"value=({positionKeyUpdate.X},{positionKeyUpdate.Y},{positionKeyUpdate.Z}) vector={positionKeyUpdate.VectorType}...");
|
||||
var sceneObject = scene.GetObject(positionKeyUpdate.ObjectName);
|
||||
if (sceneObject is null)
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", "FAILED", options.OutputPath, $"Object '{options.PositionKey.ObjectName}' was not found."));
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", "FAILED", options.OutputPath, $"Object '{positionKeyUpdate.ObjectName}' was not found."));
|
||||
return;
|
||||
}
|
||||
|
||||
handler.ResetPositionKeyTask();
|
||||
sceneObject.SetPositionKey(
|
||||
options.PositionKey.KeyIndex,
|
||||
options.PositionKey.X,
|
||||
options.PositionKey.Y,
|
||||
options.PositionKey.Z,
|
||||
options.PositionKey.VectorType);
|
||||
positionKeyUpdate.KeyIndex,
|
||||
positionKeyUpdate.X,
|
||||
positionKeyUpdate.Y,
|
||||
positionKeyUpdate.Z,
|
||||
positionKeyUpdate.VectorType);
|
||||
|
||||
if (!WaitForTaskWithMessagePump(handler.PositionKeyTask, options.Connection.Timeout))
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", "TIMEOUT", options.OutputPath, $"OnSetPositionKey timed out for '{options.PositionKey.ObjectName}'." ));
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", "TIMEOUT", options.OutputPath, $"OnSetPositionKey timed out for '{positionKeyUpdate.ObjectName}'." ));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1033,7 +1056,7 @@ static Task<SaveSceneImageProbeResult> SaveSceneImageAsync(SaveSceneImageOptions
|
||||
if (positionKeyResult != eKResult.RESULT_SUCCESS)
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", positionKeyResult.ToString(), options.OutputPath, $"OnSetPositionKey result={positionKeyResult} object={options.PositionKey.ObjectName}"));
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", positionKeyResult.ToString(), options.OutputPath, $"OnSetPositionKey result={positionKeyResult} object={positionKeyUpdate.ObjectName}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1179,53 +1202,161 @@ static Task<SaveSceneImageProbeResult> SaveSceneImageAsync(SaveSceneImageOptions
|
||||
}
|
||||
}
|
||||
|
||||
var outputDirectory = Path.GetDirectoryName(options.OutputPath);
|
||||
if (!string.IsNullOrWhiteSpace(outputDirectory))
|
||||
foreach (var positionUpdate in options.PostPositions)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
|
||||
if (File.Exists(options.OutputPath))
|
||||
{
|
||||
File.Delete(options.OutputPath);
|
||||
}
|
||||
|
||||
Console.WriteLine("[SAVE-IMAGE] Calling SaveSceneImage()...");
|
||||
handler.ResetSaveSceneImageTask();
|
||||
scene.SaveSceneImage(options.OutputPath, options.Width, options.Height, options.Frame);
|
||||
|
||||
if (!WaitForTaskWithMessagePump(handler.SaveSceneImageTask, options.Connection.Timeout))
|
||||
{
|
||||
completion.TrySetResult(new SaveSceneImageProbeResult(true, "SUCCESS", "TIMEOUT", options.OutputPath, "OnSaveSceneImage timed out."));
|
||||
return;
|
||||
}
|
||||
|
||||
var saveResult = handler.SaveSceneImageTask.Result;
|
||||
if (saveResult != eKResult.RESULT_SUCCESS)
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", saveResult.ToString(), options.OutputPath, $"OnSaveSceneImage result={saveResult}"));
|
||||
return;
|
||||
}
|
||||
|
||||
var fileWaitDeadline = DateTime.UtcNow + options.Connection.Timeout;
|
||||
while (DateTime.UtcNow < fileWaitDeadline)
|
||||
{
|
||||
if (File.Exists(options.OutputPath))
|
||||
Console.WriteLine(
|
||||
$"[SAVE-IMAGE] Setting post-chart position object={positionUpdate.ObjectName} " +
|
||||
$"value=({positionUpdate.X},{positionUpdate.Y},{positionUpdate.Z}) vector={positionUpdate.VectorType}...");
|
||||
var sceneObject = scene.GetObject(positionUpdate.ObjectName);
|
||||
if (sceneObject is null)
|
||||
{
|
||||
var info = new FileInfo(options.OutputPath);
|
||||
if (info.Length > 0)
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", "SUCCESS", options.OutputPath, $"Saved {info.Length} bytes."));
|
||||
return;
|
||||
}
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", "FAILED", options.OutputPath, $"Object '{positionUpdate.ObjectName}' was not found."));
|
||||
return;
|
||||
}
|
||||
|
||||
Thread.Sleep(50);
|
||||
handler.ResetPositionTask();
|
||||
sceneObject.SetPosition(
|
||||
positionUpdate.X,
|
||||
positionUpdate.Y,
|
||||
positionUpdate.Z,
|
||||
positionUpdate.VectorType);
|
||||
|
||||
if (!WaitForTaskWithMessagePump(handler.PositionTask, options.Connection.Timeout))
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", "TIMEOUT", options.OutputPath, $"OnSetPosition timed out for '{positionUpdate.ObjectName}'." ));
|
||||
return;
|
||||
}
|
||||
|
||||
var positionResult = handler.PositionTask.Result;
|
||||
if (positionResult != eKResult.RESULT_SUCCESS)
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", positionResult.ToString(), options.OutputPath, $"OnSetPosition result={positionResult} object={positionUpdate.ObjectName}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
completion.TrySetResult(new SaveSceneImageProbeResult(true, "SUCCESS", "FAILED", options.OutputPath, "Image file was not created."));
|
||||
foreach (var positionKeyUpdate in options.PostPositionKeys)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[SAVE-IMAGE] Setting post-chart position key object={positionKeyUpdate.ObjectName} index={positionKeyUpdate.KeyIndex} " +
|
||||
$"value=({positionKeyUpdate.X},{positionKeyUpdate.Y},{positionKeyUpdate.Z}) vector={positionKeyUpdate.VectorType}...");
|
||||
var sceneObject = scene.GetObject(positionKeyUpdate.ObjectName);
|
||||
if (sceneObject is null)
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", "FAILED", options.OutputPath, $"Object '{positionKeyUpdate.ObjectName}' was not found."));
|
||||
return;
|
||||
}
|
||||
|
||||
handler.ResetPositionKeyTask();
|
||||
sceneObject.SetPositionKey(
|
||||
positionKeyUpdate.KeyIndex,
|
||||
positionKeyUpdate.X,
|
||||
positionKeyUpdate.Y,
|
||||
positionKeyUpdate.Z,
|
||||
positionKeyUpdate.VectorType);
|
||||
|
||||
if (!WaitForTaskWithMessagePump(handler.PositionKeyTask, options.Connection.Timeout))
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", "TIMEOUT", options.OutputPath, $"OnSetPositionKey timed out for '{positionKeyUpdate.ObjectName}'." ));
|
||||
return;
|
||||
}
|
||||
|
||||
var positionKeyResult = handler.PositionKeyTask.Result;
|
||||
if (positionKeyResult != eKResult.RESULT_SUCCESS)
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", positionKeyResult.ToString(), options.OutputPath, $"OnSetPositionKey result={positionKeyResult} object={positionKeyUpdate.ObjectName}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var captures = new List<(string OutputPath, int Frame)>();
|
||||
if (options.Frames.Count > 0)
|
||||
{
|
||||
var captureDirectory = options.OutputDirectory ?? options.OutputPath;
|
||||
foreach (var captureFrame in options.Frames)
|
||||
{
|
||||
captures.Add((
|
||||
Path.GetFullPath(Path.Combine(
|
||||
captureDirectory,
|
||||
string.Format(CultureInfo.InvariantCulture, options.OutputPattern, captureFrame))),
|
||||
captureFrame));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
captures.Add((options.OutputPath, options.Frame));
|
||||
}
|
||||
|
||||
long totalBytes = 0;
|
||||
foreach (var capture in captures)
|
||||
{
|
||||
var outputDirectory = Path.GetDirectoryName(capture.OutputPath);
|
||||
if (!string.IsNullOrWhiteSpace(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
|
||||
if (File.Exists(capture.OutputPath))
|
||||
{
|
||||
File.Delete(capture.OutputPath);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[SAVE-IMAGE] Calling SaveSceneImage() frame={capture.Frame} output={capture.OutputPath}...");
|
||||
handler.ResetSaveSceneImageTask();
|
||||
scene.SaveSceneImage(capture.OutputPath, options.Width, options.Height, capture.Frame);
|
||||
|
||||
if (!WaitForTaskWithMessagePump(handler.SaveSceneImageTask, options.Connection.Timeout))
|
||||
{
|
||||
completion.TrySetResult(new SaveSceneImageProbeResult(true, "SUCCESS", "TIMEOUT", capture.OutputPath, "OnSaveSceneImage timed out."));
|
||||
return;
|
||||
}
|
||||
|
||||
var saveResult = handler.SaveSceneImageTask.Result;
|
||||
if (saveResult != eKResult.RESULT_SUCCESS)
|
||||
{
|
||||
completion.TrySetResult(
|
||||
new SaveSceneImageProbeResult(true, "SUCCESS", saveResult.ToString(), capture.OutputPath, $"OnSaveSceneImage result={saveResult}"));
|
||||
return;
|
||||
}
|
||||
|
||||
var savedThisFrame = false;
|
||||
var fileWaitDeadline = DateTime.UtcNow + options.Connection.Timeout;
|
||||
while (DateTime.UtcNow < fileWaitDeadline)
|
||||
{
|
||||
if (File.Exists(capture.OutputPath))
|
||||
{
|
||||
var info = new FileInfo(capture.OutputPath);
|
||||
if (info.Length > 0)
|
||||
{
|
||||
totalBytes += info.Length;
|
||||
savedThisFrame = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
|
||||
if (!savedThisFrame)
|
||||
{
|
||||
completion.TrySetResult(new SaveSceneImageProbeResult(true, "SUCCESS", "FAILED", capture.OutputPath, "Image file was not created."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var resultOutput = options.Frames.Count > 0
|
||||
? options.OutputDirectory ?? options.OutputPath
|
||||
: options.OutputPath;
|
||||
var detail = captures.Count == 1
|
||||
? $"Saved {totalBytes} bytes."
|
||||
: $"Saved {captures.Count} frames ({totalBytes} bytes).";
|
||||
completion.TrySetResult(new SaveSceneImageProbeResult(true, "SUCCESS", "SUCCESS", resultOutput, detail));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -3430,6 +3561,9 @@ internal sealed record SaveSceneImageOptions(
|
||||
int Width,
|
||||
int Height,
|
||||
int Frame,
|
||||
IReadOnlyList<int> Frames,
|
||||
string? OutputDirectory,
|
||||
string OutputPattern,
|
||||
string? SetObjectName,
|
||||
string? SetObjectValue,
|
||||
string? VisibleObjectName,
|
||||
@@ -3442,6 +3576,9 @@ internal sealed record SaveSceneImageOptions(
|
||||
PositionUpdate? Position,
|
||||
IReadOnlyList<PositionUpdate> Positions,
|
||||
PositionKeyUpdate? PositionKey,
|
||||
IReadOnlyList<PositionKeyUpdate> PositionKeys,
|
||||
IReadOnlyList<PositionUpdate> PostPositions,
|
||||
IReadOnlyList<PositionKeyUpdate> PostPositionKeys,
|
||||
string? ChartObjectName,
|
||||
string? ChartCsvPath,
|
||||
IReadOnlyList<ChartCellUpdate> ChartCells,
|
||||
@@ -3455,6 +3592,9 @@ internal sealed record SaveSceneImageOptions(
|
||||
string? scenePath = null;
|
||||
string? sceneAlias = null;
|
||||
string? outputPath = null;
|
||||
string? outputDirectory = null;
|
||||
string outputPattern = "frame_{0:D4}.png";
|
||||
IReadOnlyList<int> frames = Array.Empty<int>();
|
||||
string? setObjectName = null;
|
||||
string? setObjectValue = null;
|
||||
string? visibleObjectName = null;
|
||||
@@ -3474,6 +3614,9 @@ internal sealed record SaveSceneImageOptions(
|
||||
string? positionKeyObjectName = null;
|
||||
int positionKeyIndex = 1;
|
||||
string? positionKeyRaw = null;
|
||||
string? positionKeysRaw = null;
|
||||
string? postPositionsRaw = null;
|
||||
string? postPositionKeysRaw = null;
|
||||
string? chartObjectName = null;
|
||||
string? chartCsvPath = null;
|
||||
string? chartCellsRaw = null;
|
||||
@@ -3497,6 +3640,12 @@ internal sealed record SaveSceneImageOptions(
|
||||
case "--output" when index + 1 < args.Length:
|
||||
outputPath = args[++index];
|
||||
break;
|
||||
case "--output-dir" when index + 1 < args.Length:
|
||||
outputDirectory = args[++index];
|
||||
break;
|
||||
case "--output-pattern" when index + 1 < args.Length:
|
||||
outputPattern = args[++index];
|
||||
break;
|
||||
case "--set-object" when index + 1 < args.Length:
|
||||
setObjectName = args[++index];
|
||||
break;
|
||||
@@ -3561,6 +3710,15 @@ internal sealed record SaveSceneImageOptions(
|
||||
case "--position-key" when index + 1 < args.Length:
|
||||
positionKeyRaw = args[++index];
|
||||
break;
|
||||
case "--position-keys" when index + 1 < args.Length:
|
||||
positionKeysRaw = args[++index];
|
||||
break;
|
||||
case "--post-positions" when index + 1 < args.Length:
|
||||
postPositionsRaw = args[++index];
|
||||
break;
|
||||
case "--post-position-keys" when index + 1 < args.Length:
|
||||
postPositionKeysRaw = args[++index];
|
||||
break;
|
||||
case "--chart-object" when index + 1 < args.Length:
|
||||
chartObjectName = args[++index];
|
||||
break;
|
||||
@@ -3591,6 +3749,9 @@ internal sealed record SaveSceneImageOptions(
|
||||
frame = parsedFrame;
|
||||
index++;
|
||||
break;
|
||||
case "--frames" when index + 1 < args.Length:
|
||||
frames = ParseFrameSequence(args[++index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3599,7 +3760,18 @@ internal sealed record SaveSceneImageOptions(
|
||||
throw new ArgumentException("--scene is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(outputPath))
|
||||
if (frames.Count > 0)
|
||||
{
|
||||
outputDirectory ??= outputPath;
|
||||
if (string.IsNullOrWhiteSpace(outputDirectory))
|
||||
{
|
||||
throw new ArgumentException("--output-dir is required when --frames is provided.");
|
||||
}
|
||||
|
||||
outputDirectory = Path.GetFullPath(outputDirectory);
|
||||
outputPath ??= outputDirectory;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
throw new ArgumentException("--output is required.");
|
||||
}
|
||||
@@ -3618,6 +3790,9 @@ internal sealed record SaveSceneImageOptions(
|
||||
width,
|
||||
height,
|
||||
frame,
|
||||
frames,
|
||||
outputDirectory,
|
||||
outputPattern,
|
||||
setObjectName,
|
||||
setObjectValue,
|
||||
visibleObjectName,
|
||||
@@ -3630,6 +3805,9 @@ internal sealed record SaveSceneImageOptions(
|
||||
ParsePosition(positionObjectName, positionRaw),
|
||||
ParsePositions(positionsRaw),
|
||||
ParsePositionKey(positionKeyObjectName, positionKeyIndex, positionKeyRaw),
|
||||
ParsePositionKeys(positionKeysRaw),
|
||||
ParsePositions(postPositionsRaw),
|
||||
ParsePositionKeys(postPositionKeysRaw),
|
||||
chartObjectName,
|
||||
chartCsvPath,
|
||||
ParseChartCells(chartCellsRaw),
|
||||
@@ -3638,6 +3816,53 @@ internal sealed record SaveSceneImageOptions(
|
||||
ParsePathModifications(modifyPathRaw));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<int> ParseFrameSequence(string value)
|
||||
{
|
||||
var frames = new List<int>();
|
||||
foreach (var token in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var rangeMatch = Regex.Match(token, @"^(?<start>-?\d+)-(?<end>-?\d+)(?::(?<step>\d+))?$", RegexOptions.CultureInvariant);
|
||||
if (rangeMatch.Success)
|
||||
{
|
||||
var start = int.Parse(rangeMatch.Groups["start"].Value, CultureInfo.InvariantCulture);
|
||||
var end = int.Parse(rangeMatch.Groups["end"].Value, CultureInfo.InvariantCulture);
|
||||
var step = rangeMatch.Groups["step"].Success
|
||||
? int.Parse(rangeMatch.Groups["step"].Value, CultureInfo.InvariantCulture)
|
||||
: 1;
|
||||
if (step <= 0)
|
||||
{
|
||||
throw new ArgumentException("--frames range step must be greater than zero.");
|
||||
}
|
||||
|
||||
if (start <= end)
|
||||
{
|
||||
for (var frame = start; frame <= end; frame += step)
|
||||
{
|
||||
frames.Add(frame);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var frame = start; frame >= end; frame -= step)
|
||||
{
|
||||
frames.Add(frame);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out var singleFrame))
|
||||
{
|
||||
throw new ArgumentException($"Invalid frame token: {token}");
|
||||
}
|
||||
|
||||
frames.Add(singleFrame);
|
||||
}
|
||||
|
||||
return frames.Distinct().ToArray();
|
||||
}
|
||||
|
||||
private static CloneObjectUpdate? ParseCloneObject(string? sourceObjectName, string? variableName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sourceObjectName) || string.IsNullOrWhiteSpace(variableName))
|
||||
@@ -3776,6 +4001,38 @@ internal sealed record SaveSceneImageOptions(
|
||||
return new PositionKeyUpdate(objectName, keyIndex, x, y, z, vectorType);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PositionKeyUpdate> ParsePositionKeys(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return Array.Empty<PositionKeyUpdate>();
|
||||
}
|
||||
|
||||
var updates = new List<PositionKeyUpdate>();
|
||||
foreach (var token in raw.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var nameParts = token.Split('=', 2, StringSplitOptions.TrimEntries);
|
||||
if (nameParts.Length != 2)
|
||||
{
|
||||
throw new ArgumentException($"Invalid position key update: {token}");
|
||||
}
|
||||
|
||||
var objectAndKey = nameParts[0].Split('#', 2, StringSplitOptions.TrimEntries);
|
||||
if (objectAndKey.Length != 2 || !int.TryParse(objectAndKey[1], out var keyIndex))
|
||||
{
|
||||
throw new ArgumentException($"Invalid position key object/index: {nameParts[0]}");
|
||||
}
|
||||
|
||||
var update = ParsePositionKey(objectAndKey[0], keyIndex, nameParts[1]);
|
||||
if (update is not null)
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ChartCellUpdate> ParseChartCells(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
@@ -4410,6 +4667,8 @@ internal sealed class SceneValidationOperation
|
||||
public int A { get; set; } = 255;
|
||||
|
||||
public bool Visible { get; set; }
|
||||
|
||||
public bool ContinueOnFailure { get; set; }
|
||||
}
|
||||
|
||||
internal sealed record SceneOperationValidationResult(string ObjectName, string Method, string Payload, string Result, string Detail);
|
||||
|
||||
Reference in New Issue
Block a user