Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions Aetheris.CLI.Tests/CliBaselineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,58 @@ public void Analyze_Map_RayProbe_BoxTop_Returns_Llm_Grid_Samples()
Assert.Equal(2, root.GetProperty("samples")[12].GetProperty("intersectionModes").GetProperty("analytic").GetInt32());
}

[Fact]
public void Analyze_Map_SixView_Llm_Box_Returns_Compact_Summaries()
{
var stepPath = ExportPrimitiveToTempStep(BrepPrimitives.CreateBox(10d, 6d, 4d).Value, "cli-six-view-box");
using var doc = RunAnalyzeSixViewMap(stepPath, "8x8");
var root = doc.RootElement;

Assert.Equal("six-view-summary", root.GetProperty("mode").GetString());
Assert.Equal("analyze-map-v1", root.GetProperty("mapVersion").GetString());
Assert.Equal(6, root.GetProperty("views").GetArrayLength());
foreach (var view in root.GetProperty("views").EnumerateArray())
{
var summary = view.GetProperty("summary");
Assert.Equal(64, summary.GetProperty("sampleCount").GetInt32());
Assert.Equal(64, summary.GetProperty("hitCount").GetInt32());
Assert.Equal(1d, summary.GetProperty("hitCoverage").GetDouble(), 8);
Assert.True(summary.GetProperty("surfaceFamiliesHit").TryGetProperty("plane", out _));
Assert.True(summary.GetProperty("backendCounts").GetProperty("analytic").GetInt32() > 0);
Assert.Equal(0d, summary.GetProperty("fallbackRatio").GetDouble(), 8);
Assert.True(view.TryGetProperty("compactGrid", out var compactGrid));
Assert.Equal(8, compactGrid.GetProperty("width").GetInt32());
Assert.Equal(8, compactGrid.GetProperty("height").GetInt32());
Assert.Equal(8, compactGrid.GetProperty("rows").GetArrayLength());
}
}

[Fact]
public void Analyze_Map_SixView_Llm_Torus_Discloses_Analytic_Ring_And_No_Fallback()
{
var stepPath = ExportPrimitiveToTempStep(BrepPrimitives.CreateTorus(3d, 1d).Value, "cli-six-view-torus");
using var doc = RunAnalyzeSixViewMap(stepPath, "9x9");
var root = doc.RootElement;
var top = root.GetProperty("views").EnumerateArray().Single(v => v.GetProperty("name").GetString() == "top");

Assert.True(top.GetProperty("summary").GetProperty("surfaceFamiliesHit").TryGetProperty("torus", out _));
Assert.True(top.GetProperty("summary").GetProperty("backendCounts").GetProperty("analytic").GetInt32() > 0);
Assert.Equal(0, top.GetProperty("summary").GetProperty("backendCounts").GetProperty("tessellated-fallback").GetInt32());
Assert.DoesNotContain("~", string.Concat(top.GetProperty("compactGrid").GetProperty("rows").EnumerateArray().Select(r => r.GetString())));
}

[Fact]
public void Analyze_Map_SixView_Llm_LinearExtrusion_Discloses_Fallback()
{
var stepPath = Path.Combine(RepoRoot, "testdata", "step242", "generated", "ruled-a2", "ellipse-linear-extrusion-production.step");
using var doc = RunAnalyzeSixViewMap(stepPath, "4x4");
var root = doc.RootElement;

Assert.Contains("linear-extrusion", root.GetRawText(), StringComparison.Ordinal);
Assert.Contains("tessellated-fallback", root.GetRawText(), StringComparison.Ordinal);
Assert.Contains(root.GetProperty("diagnostics").EnumerateArray(), d => d.GetString()?.Contains("used tessellated fallback", StringComparison.Ordinal) == true);
}


[Fact]
public void Analyze_Map_RayProbe_CylinderSide_Uses_Analytic_Exact_Hits()
Expand Down Expand Up @@ -1468,6 +1520,16 @@ private static JsonDocument RunAnalyzeRayMap(string stepPath, params string[] ar
return JsonDocument.Parse(stdout.ToString());
}

private static JsonDocument RunAnalyzeSixViewMap(string stepPath, string resolution)
{
var stdout = new StringWriter();
var stderr = new StringWriter();
var exitCode = Aetheris.CLI.CliRunner.Run(["analyze", "map", stepPath, "--views", "six", "--resolution", resolution, "--llm", "--json"], stdout, stderr);
Assert.Equal(0, exitCode);
Assert.True(string.IsNullOrWhiteSpace(stderr.ToString()), stderr.ToString());
return JsonDocument.Parse(stdout.ToString());
}

private static JsonDocument RunAnalyzeSection(string stepPath, string planeFlag, double offset)
{
var stdout = new StringWriter();
Expand Down
45 changes: 40 additions & 5 deletions Aetheris.CLI/CliRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private sealed record CompareSideResult(
private const string TopLevelUsage = "Usage: aetheris <build|analyze|trace|canon|asm|experimental> <path> [options]";
private const string BuildUsage = "Usage: aetheris build <file.firmament> [--out <path>] [--json]";
private const string AnalyzeUsage = "Usage: aetheris analyze <file.step> [--face <id>] [--edge <id>] [--vertex <id>] [--json]";
private const string AnalyzeMapUsage = "Usage: aetheris analyze map <file.step> --plane <xy|xz|yz> --direction <+x|-x|+y|-y|+z|-z> --resolution <NxM> [--point <u,v>] --json";
private const string AnalyzeMapUsage = "Usage: aetheris analyze map <file.step> (--plane <xy|xz|yz> --direction <+x|-x|+y|-y|+z|-z> | --views six --llm) --resolution <NxM> [--point <u,v>] --json";
private const string AnalyzeSectionUsage = "Usage: aetheris analyze section <file.step> (--xy|--xz|--yz) --offset <value> --json";
private const string AnalyzeVolumeUsage = "Usage: aetheris analyze volume <file.step> [--approximate --resolution <N>] [--json]";
private const string AnalyzeCompareUsage = "Usage: aetheris analyze compare <reference.step> <candidate.step> [--approximate-volume --resolution <N>] [--json]";
Expand Down Expand Up @@ -1762,8 +1762,10 @@ private static int RunAnalyzeMap(string[] args, TextWriter stdout, TextWriter st
int? cols = null;
string? plane = null;
string? direction = null;
string? views = null;
(double U, double V)? point = null;
var json = false;
var llm = false;

for (var i = 1; i < args.Length; i++)
{
Expand Down Expand Up @@ -1814,6 +1816,17 @@ private static int RunAnalyzeMap(string[] args, TextWriter stdout, TextWriter st
case "--direction" when i + 1 < args.Length:
direction = args[++i];
break;
case "--views" when i + 1 < args.Length:
views = args[++i];
break;
case "--views":
stderr.WriteLine("Analyze map option --views requires six.");
stderr.WriteLine(AnalyzeMapUsage);
return 1;
case "--llm":
case "--summary":
llm = true;
break;
case "--direction":
stderr.WriteLine("Analyze map option --direction requires +x, -x, +y, -y, +z, or -z.");
stderr.WriteLine(AnalyzeMapUsage);
Expand Down Expand Up @@ -1852,6 +1865,14 @@ private static int RunAnalyzeMap(string[] args, TextWriter stdout, TextWriter st
}
}

var sixViewMode = string.Equals(views, "six", StringComparison.OrdinalIgnoreCase);
if (views is not null && !sixViewMode)
{
stderr.WriteLine("Analyze map option --views currently supports only 'six'.");
stderr.WriteLine(AnalyzeMapUsage);
return 1;
}

var legacyViewMode = plane is null && view.HasValue;
if (legacyViewMode)
{
Expand All @@ -1868,12 +1889,18 @@ private static int RunAnalyzeMap(string[] args, TextWriter stdout, TextWriter st
};
}

if (plane is null || direction is null || (viewOptionCount > 0 && viewOptionCount != 1))
if (!sixViewMode && (plane is null || direction is null || (viewOptionCount > 0 && viewOptionCount != 1)))
{
stderr.WriteLine("Analyze map requires --plane and --direction (or one legacy view option --top|--bottom|--front|--back|--left|--right).");
return 1;
}

if (sixViewMode && (point.HasValue || plane is not null || direction is not null || viewOptionCount > 0 || !llm))
{
stderr.WriteLine("Analyze map --views six requires --llm or --summary and cannot be combined with --point, --plane, --direction, or legacy view flags.");
return 1;
}

if (!rows.HasValue || !cols.HasValue)
{
stderr.WriteLine("Analyze map requires both --rows <N> and --cols <N>.");
Expand All @@ -1889,9 +1916,11 @@ private static int RunAnalyzeMap(string[] args, TextWriter stdout, TextWriter st
object map;
try
{
map = legacyViewMode
map = sixViewMode
? StepAnalyzer.AnalyzeSixViewMapSummary(stepPath, cols.Value, rows.Value)
: legacyViewMode
? StepAnalyzer.AnalyzeMap(stepPath, view.GetValueOrDefault(), rows.Value, cols.Value)
: StepAnalyzer.AnalyzeRayMap(stepPath, plane, direction, cols.Value, rows.Value, point);
: StepAnalyzer.AnalyzeRayMap(stepPath, plane!, direction!, cols.Value, rows.Value, point);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -2255,13 +2284,19 @@ private static void WriteAnalyzeMapHelp(TextWriter stdout)
stdout.WriteLine(AnalyzeMapUsage);
stdout.WriteLine();
stdout.WriteLine("Required:");
stdout.WriteLine(" exactly one view: --top | --bottom | --front | --back | --left | --right");
stdout.WriteLine(" either --plane <xy|xz|yz> with --direction <axis>, exactly one legacy view, or --views six --llm.");
stdout.WriteLine(" legacy views: --top | --bottom | --front | --back | --left | --right");
stdout.WriteLine(" --rows <N> Positive integer row count.");
stdout.WriteLine(" --cols <N> Positive integer column count.");
stdout.WriteLine(" --resolution NxM Alternative to --cols N --rows M.");
stdout.WriteLine(" --json Required output mode.");
stdout.WriteLine();
stdout.WriteLine("Six-view convention:");
stdout.WriteLine(" top xy/-z, bottom xy/+z, right yz/-x, left yz/+x, back xz/+y, front xz/-y.");
stdout.WriteLine();
stdout.WriteLine("Example:");
stdout.WriteLine(" aetheris analyze map part.step --top --rows 48 --cols 64 --json");
stdout.WriteLine(" aetheris analyze map part.step --views six --resolution 32x32 --llm --json");
}

private static void WriteAnalyzeSectionHelp(TextWriter stdout)
Expand Down
5 changes: 5 additions & 0 deletions Aetheris.CLI/StepAnalysisModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ public sealed record RayMapHit(double T, Point3D Position, int? FaceIndex, strin
public sealed record RayMapSample(int I, int J, double U, double V, bool Hit, RayMapHit? FirstHit, RayMapHit? LastHit, int HitCount, IReadOnlyList<RayMapHit> Hits, IReadOnlyDictionary<string, int> IntersectionModes);
public sealed record RayMapSummary(double HitCoverage, double[]? HeightRange, IReadOnlyDictionary<string, int> SurfaceFamiliesHit, int AnalyticHitCount, int CirHitCount, int TessellatedFallbackHitCount, int UnsupportedSampleCount);
public sealed record RayMapResult(string Mode, string Plane, string Direction, int[]? Resolution, double[]? Point, RayMapBounds Bounds, IReadOnlyList<RayMapSample> Samples, int HitCount, IReadOnlyList<RayMapHit>? Hits, RayMapSummary Summary, string IntersectionMode, string BackendPolicy, IReadOnlyList<string> Diagnostics);
public sealed record SixViewMapResult(string Mode, string MapVersion, int[] Resolution, IReadOnlyList<SixViewMapView> Views, IReadOnlyList<string> Diagnostics);
public sealed record SixViewMapView(string Name, string Plane, string Direction, SixViewMapSummary Summary, CompactGrid? CompactGrid, IReadOnlyList<string> MeasuredSummary);
public sealed record SixViewMapSummary(int SampleCount, int HitCount, double HitCoverage, double[]? HeightRange, IReadOnlyList<DominantBand> DominantBands, IReadOnlyDictionary<string, int> SurfaceFamiliesHit, IReadOnlyDictionary<string, int> BackendCounts, double FallbackRatio);
public sealed record DominantBand(double? Value, int SampleCount, double Coverage, string? Meaning, bool MostlyFallback);
public sealed record CompactGrid(string Encoding, int Width, int Height, IReadOnlyDictionary<string, string> Legend, IReadOnlyList<string> Rows);

public enum SectionPlaneFamily
{
Expand Down
Loading
Loading