1197 lines
45 KiB
C#
1197 lines
45 KiB
C#
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
Console.OutputEncoding = Encoding.UTF8;
|
|
|
|
try
|
|
{
|
|
var options = CliOptions.Parse(args);
|
|
if (string.IsNullOrWhiteSpace(options.BrepReportPath) || !File.Exists(options.BrepReportPath))
|
|
throw new FileNotFoundException("Missing B-rep report. Pass --brep-report <section_brep_report.json>.");
|
|
if (string.IsNullOrWhiteSpace(options.DimensionsPath) || !File.Exists(options.DimensionsPath))
|
|
throw new FileNotFoundException("Missing dimensions file. Pass --dimensions <dimension anchors json>.");
|
|
|
|
Directory.CreateDirectory(options.OutputDir);
|
|
var report = DimensionFaceMatcher.Run(options);
|
|
var outputPath = options.OutputPath ?? Path.Combine(options.OutputDir, "dimension_face_matches.json");
|
|
File.WriteAllText(outputPath, JsonSerializer.Serialize(report, JsonDefaults.Options), new UTF8Encoding(false));
|
|
|
|
Console.WriteLine($"Dimension-face matches written: {outputPath}");
|
|
Console.WriteLine($"Dimensions: {report.Dimensions.Count}; unique: {report.Dimensions.Count(d => d.IsUnique)}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine(ex.ToString());
|
|
Environment.ExitCode = 1;
|
|
}
|
|
|
|
static class DimensionFaceMatcher
|
|
{
|
|
public static MatchReport Run(CliOptions options)
|
|
{
|
|
using var brepDoc = JsonDocument.Parse(File.ReadAllText(options.BrepReportPath));
|
|
using var dimensionDoc = JsonDocument.Parse(File.ReadAllText(options.DimensionsPath));
|
|
|
|
var faces = LoadFaces(brepDoc.RootElement);
|
|
if (faces.Count == 0)
|
|
throw new InvalidOperationException("No faces found. Expected section_brep_report.json with assemblyFaces.");
|
|
|
|
var view = ReadViewSpec(dimensionDoc.RootElement, options);
|
|
var projectedFaces = ProjectFaces(faces, view);
|
|
var modelBounds = Bounds2D.Union(projectedFaces.Select(face => face.ProjectedBBox));
|
|
projectedFaces = projectedFaces
|
|
.Select(face => face with
|
|
{
|
|
NormalizedCenter = modelBounds.Normalize(face.ProjectedCenter),
|
|
NormalizedBBox = modelBounds.Normalize(face.ProjectedBBox)
|
|
})
|
|
.ToList();
|
|
|
|
var dimensions = LoadDimensions(dimensionDoc.RootElement);
|
|
if (dimensions.Count == 0)
|
|
throw new InvalidOperationException("No dimensions found. Expected a JSON array or an object with dimensions.");
|
|
|
|
var drawingBounds = ReadDrawingBounds(dimensionDoc.RootElement)
|
|
?? Bounds2D.Union(dimensions.SelectMany(d => d.AllBoxes()));
|
|
dimensions = dimensions.Select(d => d.Normalize(drawingBounds)).ToList();
|
|
|
|
var results = dimensions
|
|
.Select(dimension => MatchDimension(dimension, projectedFaces, options.Top))
|
|
.ToList();
|
|
|
|
return new MatchReport
|
|
{
|
|
Schema = "dimension_face_match_report_v1",
|
|
BrepReportPath = Path.GetFullPath(options.BrepReportPath),
|
|
DimensionsPath = Path.GetFullPath(options.DimensionsPath),
|
|
View = view,
|
|
ModelProjectedBounds = modelBounds,
|
|
DrawingBounds = drawingBounds,
|
|
FaceCount = faces.Count,
|
|
Dimensions = results
|
|
};
|
|
}
|
|
|
|
static DimensionMatchResult MatchDimension(DrawingDimension dimension, IReadOnlyList<ProjectedFace> faces, int top)
|
|
{
|
|
var anchors = dimension.Anchors.Count > 0 ? dimension.Anchors : [dimension.FallbackAnchor()];
|
|
var anchorA = anchors[0];
|
|
var anchorB = anchors.Count > 1 ? anchors[1] : anchors[0];
|
|
|
|
var anchorCandidatesA = faces
|
|
.Select(face => ScoreAnchorFace(dimension, anchorA, face))
|
|
.OrderByDescending(score => score.Score)
|
|
.Take(30)
|
|
.ToList();
|
|
var anchorCandidatesB = faces
|
|
.Select(face => ScoreAnchorFace(dimension, anchorB, face))
|
|
.OrderByDescending(score => score.Score)
|
|
.Take(30)
|
|
.ToList();
|
|
|
|
var pairScores = new List<FacePairCandidate>();
|
|
foreach (var a in anchorCandidatesA)
|
|
{
|
|
foreach (var b in anchorCandidatesB)
|
|
{
|
|
if (a.Face.FaceRef == b.Face.FaceRef)
|
|
continue;
|
|
pairScores.Add(ScoreFacePair(dimension, a, b, swapped: false));
|
|
}
|
|
}
|
|
|
|
if (anchors.Count > 1)
|
|
{
|
|
foreach (var a in anchorCandidatesA)
|
|
{
|
|
foreach (var b in anchorCandidatesB)
|
|
{
|
|
if (a.Face.FaceRef == b.Face.FaceRef)
|
|
continue;
|
|
pairScores.Add(ScoreFacePair(dimension, b, a, swapped: true));
|
|
}
|
|
}
|
|
}
|
|
|
|
var distinctPairScores = pairScores
|
|
.GroupBy(score => UnorderedPairKey(score.FaceA.FaceRef, score.FaceB.FaceRef), StringComparer.OrdinalIgnoreCase)
|
|
.Select(group => group.OrderByDescending(score => score.Score).First())
|
|
.OrderByDescending(score => score.Score)
|
|
.ToList();
|
|
|
|
var best = distinctPairScores.Take(Math.Max(top, 1)).ToList();
|
|
var topScore = best.FirstOrDefault()?.Score ?? 0.0;
|
|
var secondScore = distinctPairScores
|
|
.Skip(1)
|
|
.FirstOrDefault()?.Score ?? 0.0;
|
|
|
|
return new DimensionMatchResult
|
|
{
|
|
DimensionId = dimension.Id,
|
|
Text = dimension.Text,
|
|
Kind = dimension.Kind,
|
|
MeasurementMm = dimension.MeasurementMm,
|
|
BBox = dimension.BBox,
|
|
AnchorCount = anchors.Count,
|
|
TopCandidates = best,
|
|
IsUnique = topScore >= 0.72 && topScore - secondScore >= 0.12,
|
|
TopScore = Math.Round(topScore, 4),
|
|
SecondScore = Math.Round(secondScore, 4),
|
|
ScoreMargin = Math.Round(topScore - secondScore, 4)
|
|
};
|
|
}
|
|
|
|
static string UnorderedPairKey(string a, string b) =>
|
|
string.Compare(a, b, StringComparison.OrdinalIgnoreCase) <= 0 ? $"{a}||{b}" : $"{b}||{a}";
|
|
|
|
static AnchorFaceScore ScoreAnchorFace(DrawingDimension dimension, DimensionAnchor anchor, ProjectedFace face)
|
|
{
|
|
var positionScore = ScoreDistance(anchor.NormalizedMidpoint, face.NormalizedCenter, 0.18);
|
|
var bboxScore = anchor.NormalizedBBox is { } anchorBox
|
|
? ScoreBoxSimilarity(anchorBox, face.NormalizedBBox)
|
|
: 0.5;
|
|
var ownerScore = ScoreOwner(dimension.OwnerComponents, face);
|
|
var typeScore = ScoreFaceKindForDimension(dimension.Kind, face.FaceKind);
|
|
|
|
var score = 0.50 * positionScore +
|
|
0.18 * bboxScore +
|
|
0.20 * ownerScore +
|
|
0.12 * typeScore;
|
|
|
|
return new AnchorFaceScore
|
|
{
|
|
Face = face,
|
|
Score = Clamp01(score),
|
|
PositionScore = positionScore,
|
|
BBoxScore = bboxScore,
|
|
OwnerScore = ownerScore,
|
|
TypeScore = typeScore
|
|
};
|
|
}
|
|
|
|
static FacePairCandidate ScoreFacePair(DrawingDimension dimension, AnchorFaceScore a, AnchorFaceScore b, bool swapped)
|
|
{
|
|
var value = ScoreValue(dimension, a.Face, b.Face);
|
|
var relation = ScoreRelation(dimension.Kind, a.Face, b.Face);
|
|
var anchorScore = (a.Score + b.Score) / 2.0;
|
|
var roleScore = ScoreRole(dimension, a.Face, b.Face);
|
|
|
|
var score = 0.45 * value.Score +
|
|
0.35 * anchorScore +
|
|
0.12 * relation +
|
|
0.08 * roleScore;
|
|
|
|
return new FacePairCandidate
|
|
{
|
|
Score = Math.Round(Clamp01(score), 4),
|
|
FaceA = FaceSummary.From(a.Face),
|
|
FaceB = FaceSummary.From(b.Face),
|
|
ExpectedMeasurementMm = value.ExpectedMeasurementMm,
|
|
MeasurementErrorMm = value.ErrorMm,
|
|
Assignment = swapped ? "anchor_b_to_face_a__anchor_a_to_face_b" : "anchor_a_to_face_a__anchor_b_to_face_b",
|
|
Components = new CandidateScoreBreakdown
|
|
{
|
|
ValueScore = Math.Round(value.Score, 4),
|
|
AnchorScore = Math.Round(anchorScore, 4),
|
|
RelationScore = Math.Round(relation, 4),
|
|
RoleScore = Math.Round(roleScore, 4),
|
|
AnchorAScore = Math.Round(a.Score, 4),
|
|
AnchorBScore = Math.Round(b.Score, 4),
|
|
AnchorAPositionScore = Math.Round(a.PositionScore, 4),
|
|
AnchorBPositionScore = Math.Round(b.PositionScore, 4)
|
|
},
|
|
Evidence = BuildEvidence(dimension, a.Face, b.Face, value)
|
|
};
|
|
}
|
|
|
|
static ValueScore ScoreValue(DrawingDimension dimension, ProjectedFace a, ProjectedFace b)
|
|
{
|
|
if (dimension.MeasurementMm is not { } measured || measured <= 0)
|
|
return new ValueScore(0.5, null, null);
|
|
|
|
var expected = ExpectedMeasurement(dimension.Kind, a, b);
|
|
if (expected is not { } value || value <= 0)
|
|
return new ValueScore(0.35, null, null);
|
|
|
|
var error = Math.Abs(measured - value);
|
|
var tolerance = Math.Max(0.5, measured * 0.025);
|
|
var score = 1.0 / (1.0 + error / tolerance);
|
|
return new ValueScore(Clamp01(score), Math.Round(value, 4), Math.Round(error, 4));
|
|
}
|
|
|
|
static double? ExpectedMeasurement(string kind, ProjectedFace a, ProjectedFace b)
|
|
{
|
|
if (kind is "diameter" or "cylindrical_fit")
|
|
{
|
|
var diameters = new[] { a.DiameterMm, b.DiameterMm }
|
|
.Where(v => v is > 0)
|
|
.Select(v => v!.Value)
|
|
.ToList();
|
|
if (diameters.Count > 0)
|
|
return diameters.Average();
|
|
}
|
|
|
|
if (kind is "plane_distance" or "linear_distance")
|
|
{
|
|
if (a.FaceKind == "plane" && b.FaceKind == "plane" && a.Normal.Length >= 3 && b.Normal.Length >= 3)
|
|
{
|
|
var dot = Math.Abs(Vec.Dot(Vec.Normalize(a.Normal), Vec.Normalize(b.Normal)));
|
|
if (dot >= 0.92)
|
|
return Math.Abs(Vec.Dot(Vec.Sub(b.CenterMm, a.CenterMm), Vec.Normalize(a.Normal)));
|
|
}
|
|
}
|
|
|
|
if (kind is "axis_distance")
|
|
{
|
|
if (a.FaceKind == "cylinder" && b.FaceKind == "cylinder" && a.Axis.Length >= 3 && b.Axis.Length >= 3 && a.AxisPointMm.Length >= 3 && b.AxisPointMm.Length >= 3)
|
|
return AxisDistanceMm(a.AxisPointMm, a.Axis, b.AxisPointMm);
|
|
}
|
|
|
|
return Distance2D(a.ProjectedCenter, b.ProjectedCenter);
|
|
}
|
|
|
|
static double ScoreRelation(string kind, ProjectedFace a, ProjectedFace b)
|
|
{
|
|
if (kind is "diameter" or "cylindrical_fit")
|
|
{
|
|
if (a.FaceKind != "cylinder" || b.FaceKind != "cylinder")
|
|
return 0.15;
|
|
var axisScore = 0.5;
|
|
if (a.Axis.Length >= 3 && b.Axis.Length >= 3)
|
|
axisScore = Math.Abs(Vec.Dot(Vec.Normalize(a.Axis), Vec.Normalize(b.Axis)));
|
|
var radiusScore = 0.5;
|
|
if (a.RadiusMm is > 0 && b.RadiusMm is > 0)
|
|
{
|
|
var tolerance = Math.Max(0.5, Math.Max(a.RadiusMm.Value, b.RadiusMm.Value) * 0.03);
|
|
radiusScore = 1.0 / (1.0 + Math.Abs(a.RadiusMm.Value - b.RadiusMm.Value) / tolerance);
|
|
}
|
|
return Clamp01(0.55 * axisScore + 0.45 * radiusScore);
|
|
}
|
|
|
|
if (kind is "plane_distance" or "linear_distance")
|
|
{
|
|
if (a.FaceKind != "plane" || b.FaceKind != "plane")
|
|
return 0.3;
|
|
if (a.Normal.Length < 3 || b.Normal.Length < 3)
|
|
return 0.55;
|
|
return Clamp01(Math.Abs(Vec.Dot(Vec.Normalize(a.Normal), Vec.Normalize(b.Normal))));
|
|
}
|
|
|
|
return a.FaceKind == b.FaceKind ? 0.65 : 0.45;
|
|
}
|
|
|
|
static double ScoreRole(DrawingDimension dimension, ProjectedFace a, ProjectedFace b)
|
|
{
|
|
var text = $"{dimension.Text} {a.FunctionalRole} {b.FunctionalRole} {a.SurfaceProcessRole} {b.SurfaceProcessRole}".ToLowerInvariant();
|
|
if (dimension.Kind is "cylindrical_fit" or "diameter")
|
|
{
|
|
var fitText = text.Contains('φ') || text.Contains("%%c") || text.Contains("h7") || text.Contains("h6") || text.Contains("fit") || text.Contains("bore") || text.Contains("bearing") || text.Contains("cylindrical");
|
|
return fitText ? 0.9 : 0.55;
|
|
}
|
|
|
|
if (dimension.Kind is "plane_distance" or "linear_distance")
|
|
{
|
|
var planeText = text.Contains("plane") || text.Contains("planar") || text.Contains("contact") || text.Contains("mating") || text.Contains("distance");
|
|
return planeText ? 0.8 : 0.55;
|
|
}
|
|
|
|
return 0.55;
|
|
}
|
|
|
|
static List<string> BuildEvidence(DrawingDimension dimension, ProjectedFace a, ProjectedFace b, ValueScore value)
|
|
{
|
|
var evidence = new List<string>();
|
|
if (dimension.MeasurementMm != null && value.ExpectedMeasurementMm != null)
|
|
evidence.Add($"measurement {dimension.MeasurementMm:0.####}mm vs expected {value.ExpectedMeasurementMm:0.####}mm");
|
|
evidence.Add($"face kinds: {a.FaceKind} <-> {b.FaceKind}");
|
|
evidence.Add($"components: {a.ComponentDisplayNameOrName} <-> {b.ComponentDisplayNameOrName}");
|
|
if (!string.IsNullOrWhiteSpace(a.FunctionalRole) || !string.IsNullOrWhiteSpace(b.FunctionalRole))
|
|
evidence.Add($"functional roles: {a.FunctionalRole} <-> {b.FunctionalRole}");
|
|
return evidence;
|
|
}
|
|
|
|
static double ScoreFaceKindForDimension(string kind, string faceKind) =>
|
|
kind switch
|
|
{
|
|
"diameter" or "cylindrical_fit" => faceKind == "cylinder" ? 1.0 : 0.15,
|
|
"plane_distance" => faceKind == "plane" ? 1.0 : 0.25,
|
|
"linear_distance" => faceKind == "plane" ? 0.75 : faceKind == "cylinder" ? 0.45 : 0.35,
|
|
"axis_distance" => faceKind == "cylinder" ? 1.0 : 0.2,
|
|
_ => 0.5
|
|
};
|
|
|
|
static double ScoreOwner(IReadOnlyList<string> owners, ProjectedFace face)
|
|
{
|
|
if (owners.Count == 0)
|
|
return 1.0;
|
|
|
|
foreach (var owner in owners)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(owner))
|
|
continue;
|
|
if (ContainsNormalized(face.ComponentName, owner) ||
|
|
ContainsNormalized(face.ComponentDisplayName, owner) ||
|
|
ContainsNormalized(face.ComponentPath, owner) ||
|
|
ContainsNormalized(face.ComponentFileName, owner))
|
|
return 1.0;
|
|
}
|
|
|
|
return 0.2;
|
|
}
|
|
|
|
static bool ContainsNormalized(string left, string right) =>
|
|
NormalizeText(left).Contains(NormalizeText(right), StringComparison.OrdinalIgnoreCase) ||
|
|
NormalizeText(right).Contains(NormalizeText(left), StringComparison.OrdinalIgnoreCase);
|
|
|
|
static string NormalizeText(string value) =>
|
|
new(value.Where(ch => !char.IsWhiteSpace(ch) && ch is not '-' and not '_' and not '.').ToArray());
|
|
|
|
static double ScoreBoxSimilarity(Bounds2D a, Bounds2D b)
|
|
{
|
|
var center = ScoreDistance(a.Center, b.Center, 0.20);
|
|
var aw = Math.Max(a.Width, 1e-9);
|
|
var ah = Math.Max(a.Height, 1e-9);
|
|
var bw = Math.Max(b.Width, 1e-9);
|
|
var bh = Math.Max(b.Height, 1e-9);
|
|
var size = 1.0 / (1.0 + Math.Abs(Math.Log(aw / bw)) + Math.Abs(Math.Log(ah / bh)));
|
|
return Clamp01(0.65 * center + 0.35 * size);
|
|
}
|
|
|
|
static double ScoreDistance(Point2D a, Point2D b, double tolerance)
|
|
{
|
|
var d = Distance2D(a, b);
|
|
return Clamp01(1.0 / (1.0 + d / Math.Max(tolerance, 1e-9)));
|
|
}
|
|
|
|
static List<ProjectedFace> ProjectFaces(IReadOnlyList<ModelFace> faces, ViewSpec view)
|
|
{
|
|
return faces.Select(face =>
|
|
{
|
|
var center = ProjectPoint(face.CenterMm, view);
|
|
var points = Corners(face.BBoxMm).Select(point => ProjectPoint(point, view)).ToList();
|
|
var box = Bounds2D.Union(points);
|
|
return new ProjectedFace(
|
|
face.FaceRef,
|
|
face.ComponentName,
|
|
face.ComponentDisplayName,
|
|
face.ComponentPath,
|
|
face.ComponentFileName,
|
|
face.FaceIndex,
|
|
face.FaceKind,
|
|
face.CenterMm,
|
|
face.BBoxMm,
|
|
face.Normal,
|
|
face.AxisPointMm,
|
|
face.Axis,
|
|
face.RadiusMm,
|
|
face.DiameterMm,
|
|
face.FunctionalRole,
|
|
face.SurfaceProcessRole,
|
|
center,
|
|
box,
|
|
new Point2D(0, 0),
|
|
new Bounds2D(0, 0, 0, 0));
|
|
}).ToList();
|
|
}
|
|
|
|
static Point2D ProjectPoint(double[] p, ViewSpec view)
|
|
{
|
|
var basis = ViewBasis.FromDirection(view.Direction);
|
|
var u = Vec.Dot(p, basis.U);
|
|
var v = Vec.Dot(p, basis.V);
|
|
var radians = view.RollDeg * Math.PI / 180.0;
|
|
var cos = Math.Cos(radians);
|
|
var sin = Math.Sin(radians);
|
|
return new Point2D(u * cos - v * sin, u * sin + v * cos);
|
|
}
|
|
|
|
static List<ModelFace> LoadFaces(JsonElement root)
|
|
{
|
|
if (!TryGetProperty(root, "assemblyFaces", out var facesEl) || facesEl.ValueKind != JsonValueKind.Array)
|
|
return [];
|
|
|
|
var faces = new List<ModelFace>();
|
|
foreach (var faceEl in facesEl.EnumerateArray())
|
|
{
|
|
var componentName = ReadString(faceEl, "componentName");
|
|
var faceIndex = ReadInt(faceEl, "faceIndex") ?? 0;
|
|
var bbox = ReadDoubleArray(faceEl, "bBoxMm", "bboxMm", "bbox_mm");
|
|
var center = ReadDoubleArray(faceEl, "centerMm", "center_mm");
|
|
if (bbox.Length < 6 || center.Length < 3)
|
|
continue;
|
|
|
|
var face = new ModelFace
|
|
{
|
|
ComponentName = componentName,
|
|
ComponentDisplayName = ReadString(faceEl, "componentDisplayName"),
|
|
ComponentPath = ReadString(faceEl, "componentPath"),
|
|
ComponentFileName = ReadString(faceEl, "componentFileName"),
|
|
FaceIndex = faceIndex,
|
|
FaceRef = $"{componentName}:face#{faceIndex}",
|
|
FaceKind = ReadString(faceEl, "faceKind", "kind").ToLowerInvariant(),
|
|
AreaMm2 = ReadDouble(faceEl, "areaMm2", "area_mm2") ?? 0,
|
|
CenterMm = center,
|
|
BBoxMm = bbox,
|
|
Normal = ReadDoubleArray(faceEl, "normal"),
|
|
AxisPointMm = ReadDoubleArray(faceEl, "axisPointMm", "axis_point_mm"),
|
|
Axis = ReadDoubleArray(faceEl, "axis"),
|
|
RadiusMm = ReadDouble(faceEl, "radiusMm", "radius_mm"),
|
|
DiameterMm = ReadDouble(faceEl, "diameterMm", "diameter_mm"),
|
|
FunctionalRole = ReadString(faceEl, "functionalRole", "functional_role"),
|
|
SurfaceProcessRole = ReadString(faceEl, "surfaceProcessRole", "surface_process_role")
|
|
};
|
|
faces.Add(face);
|
|
}
|
|
|
|
return faces;
|
|
}
|
|
|
|
static List<DrawingDimension> LoadDimensions(JsonElement root)
|
|
{
|
|
var array = root.ValueKind == JsonValueKind.Array
|
|
? root
|
|
: TryGetProperty(root, "dimensions", out var dimensionsEl) && dimensionsEl.ValueKind == JsonValueKind.Array
|
|
? dimensionsEl
|
|
: default;
|
|
if (array.ValueKind != JsonValueKind.Array)
|
|
return [];
|
|
|
|
var result = new List<DrawingDimension>();
|
|
var index = 0;
|
|
foreach (var dimEl in array.EnumerateArray())
|
|
{
|
|
index++;
|
|
var bbox = ReadBounds2D(dimEl, "bbox", "box");
|
|
var measurement = ReadDouble(dimEl, "measurementMm", "measurement_mm", "measurement", "valueMm", "value_mm");
|
|
var text = FirstNonEmpty(
|
|
ReadString(dimEl, "text"),
|
|
ReadString(dimEl, "textOverride", "text_override"),
|
|
ReadString(dimEl, "rawText", "raw_text"));
|
|
|
|
var anchors = LoadAnchors(dimEl);
|
|
if (anchors.Count == 0 && bbox != null)
|
|
{
|
|
anchors.Add(new DimensionAnchor
|
|
{
|
|
Id = "bbox_center",
|
|
BBox = bbox,
|
|
Midpoint = bbox.Value.Center
|
|
});
|
|
}
|
|
|
|
var owners = ReadStringArray(dimEl, "ownerComponents", "owner_components", "components");
|
|
AddIfNotEmpty(owners, ReadString(dimEl, "ownerComponentA", "owner_component_a", "componentA", "component_a"));
|
|
AddIfNotEmpty(owners, ReadString(dimEl, "ownerComponentB", "owner_component_b", "componentB", "component_b"));
|
|
|
|
result.Add(new DrawingDimension
|
|
{
|
|
Id = FirstNonEmpty(ReadString(dimEl, "id"), ReadString(dimEl, "handle"), $"dimension_{index:0000}"),
|
|
Text = text,
|
|
Kind = InferKind(FirstNonEmpty(ReadString(dimEl, "kind"), ReadString(dimEl, "relationType", "relation_type")), text),
|
|
MeasurementMm = measurement,
|
|
BBox = bbox,
|
|
Anchors = anchors,
|
|
OwnerComponents = owners
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static List<DimensionAnchor> LoadAnchors(JsonElement dimEl)
|
|
{
|
|
var anchors = new List<DimensionAnchor>();
|
|
if (TryGetProperty(dimEl, "anchors", out var anchorsEl) && anchorsEl.ValueKind == JsonValueKind.Array)
|
|
{
|
|
var index = 0;
|
|
foreach (var anchorEl in anchorsEl.EnumerateArray())
|
|
{
|
|
index++;
|
|
var anchor = ReadAnchor(anchorEl, $"anchor_{index}");
|
|
if (anchor != null)
|
|
anchors.Add(anchor);
|
|
}
|
|
}
|
|
|
|
foreach (var key in new[] { "anchorA", "anchorB", "anchor_a", "anchor_b" })
|
|
{
|
|
if (TryGetProperty(dimEl, key, out var anchorEl) && anchorEl.ValueKind == JsonValueKind.Object)
|
|
{
|
|
var anchor = ReadAnchor(anchorEl, key);
|
|
if (anchor != null)
|
|
anchors.Add(anchor);
|
|
}
|
|
}
|
|
|
|
return anchors;
|
|
}
|
|
|
|
static DimensionAnchor? ReadAnchor(JsonElement anchorEl, string fallbackId)
|
|
{
|
|
var bbox = ReadBounds2D(anchorEl, "bbox", "box");
|
|
var midpoint = ReadPoint2D(anchorEl, "midpoint", "center", "point");
|
|
var start = ReadPoint2D(anchorEl, "start");
|
|
var end = ReadPoint2D(anchorEl, "end");
|
|
if (midpoint == null && start != null && end != null)
|
|
midpoint = new Point2D((start.Value.X + end.Value.X) / 2.0, (start.Value.Y + end.Value.Y) / 2.0);
|
|
if (bbox == null && start != null && end != null)
|
|
bbox = Bounds2D.Union([start.Value, end.Value]);
|
|
if (midpoint == null && bbox != null)
|
|
midpoint = bbox.Value.Center;
|
|
if (midpoint == null)
|
|
return null;
|
|
|
|
return new DimensionAnchor
|
|
{
|
|
Id = FirstNonEmpty(ReadString(anchorEl, "id"), fallbackId),
|
|
BBox = bbox,
|
|
Midpoint = midpoint.Value,
|
|
Start = start,
|
|
End = end
|
|
};
|
|
}
|
|
|
|
static ViewSpec ReadViewSpec(JsonElement root, CliOptions options)
|
|
{
|
|
var direction = options.ViewDirection;
|
|
var roll = options.RollDeg;
|
|
if (TryGetProperty(root, "view", out var viewEl) && viewEl.ValueKind == JsonValueKind.Object)
|
|
{
|
|
direction = FirstNonEmpty(ReadString(viewEl, "direction", "viewDirection", "view_direction"), direction);
|
|
roll = ReadDouble(viewEl, "rollDeg", "roll_deg", "roll") ?? roll;
|
|
}
|
|
|
|
return new ViewSpec
|
|
{
|
|
Direction = NormalizeDirection(direction),
|
|
RollDeg = NormalizeRoll(roll)
|
|
};
|
|
}
|
|
|
|
static Bounds2D? ReadDrawingBounds(JsonElement root)
|
|
{
|
|
if (TryGetProperty(root, "view", out var viewEl) && viewEl.ValueKind == JsonValueKind.Object)
|
|
{
|
|
var viewBox = ReadBounds2D(viewEl, "drawingBBox", "drawing_bbox", "mainViewBBox", "main_view_bbox");
|
|
if (viewBox != null)
|
|
return viewBox;
|
|
}
|
|
|
|
return ReadBounds2D(root, "drawingBBox", "drawing_bbox", "mainViewBBox", "main_view_bbox");
|
|
}
|
|
|
|
static string InferKind(string explicitKind, string text)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(explicitKind))
|
|
return explicitKind.Trim().ToLowerInvariant();
|
|
|
|
var lower = text.ToLowerInvariant();
|
|
if (lower.Contains("h7") || lower.Contains("h6") || lower.Contains("g6") || lower.Contains("f7") || lower.Contains("fit"))
|
|
return "cylindrical_fit";
|
|
if (text.Contains('Φ') || lower.Contains("%%c") || lower.Contains("diameter"))
|
|
return "diameter";
|
|
if (lower.Contains("axis") || lower.Contains("center"))
|
|
return "axis_distance";
|
|
return "linear_distance";
|
|
}
|
|
|
|
static string NormalizeDirection(string value)
|
|
{
|
|
var v = value.Trim().ToUpperInvariant();
|
|
return v switch
|
|
{
|
|
"X" or "+X" or "PX" => "+X",
|
|
"-X" or "NX" => "-X",
|
|
"Y" or "+Y" or "PY" => "+Y",
|
|
"-Y" or "NY" => "-Y",
|
|
"Z" or "+Z" or "PZ" => "+Z",
|
|
"-Z" or "NZ" => "-Z",
|
|
_ => throw new ArgumentException($"Unsupported view direction `{value}`. Use +X/-X/+Y/-Y/+Z/-Z.")
|
|
};
|
|
}
|
|
|
|
static double NormalizeRoll(double value)
|
|
{
|
|
var roll = value % 360.0;
|
|
if (roll < 0)
|
|
roll += 360.0;
|
|
return roll;
|
|
}
|
|
|
|
static double AxisDistanceMm(double[] pointA, double[] axisA, double[] pointB)
|
|
{
|
|
var axis = Vec.Normalize(axisA);
|
|
var delta = Vec.Sub(pointB, pointA);
|
|
var along = Vec.Mul(axis, Vec.Dot(delta, axis));
|
|
return Vec.Length(Vec.Sub(delta, along));
|
|
}
|
|
|
|
static IEnumerable<double[]> Corners(double[] box)
|
|
{
|
|
var xs = new[] { box[0], box[3] };
|
|
var ys = new[] { box[1], box[4] };
|
|
var zs = new[] { box[2], box[5] };
|
|
foreach (var x in xs)
|
|
foreach (var y in ys)
|
|
foreach (var z in zs)
|
|
yield return [x, y, z];
|
|
}
|
|
|
|
static double Distance2D(Point2D a, Point2D b)
|
|
{
|
|
var dx = a.X - b.X;
|
|
var dy = a.Y - b.Y;
|
|
return Math.Sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
static double Clamp01(double value) => Math.Min(1.0, Math.Max(0.0, value));
|
|
|
|
static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
|
|
{
|
|
if (element.ValueKind == JsonValueKind.Object)
|
|
{
|
|
foreach (var property in element.EnumerateObject())
|
|
{
|
|
if (property.NameEquals(name) || string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
value = property.Value;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
static string ReadString(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (TryGetProperty(element, name, out var value))
|
|
{
|
|
if (value.ValueKind == JsonValueKind.String)
|
|
return value.GetString() ?? "";
|
|
if (value.ValueKind is JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False)
|
|
return value.ToString();
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
static double? ReadDouble(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!TryGetProperty(element, name, out var value))
|
|
continue;
|
|
if (value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var number))
|
|
return number;
|
|
if (value.ValueKind == JsonValueKind.String && double.TryParse(value.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out number))
|
|
return number;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static int? ReadInt(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!TryGetProperty(element, name, out var value))
|
|
continue;
|
|
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number))
|
|
return number;
|
|
if (value.ValueKind == JsonValueKind.String && int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out number))
|
|
return number;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static double[] ReadDoubleArray(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!TryGetProperty(element, name, out var value))
|
|
continue;
|
|
if (value.ValueKind == JsonValueKind.Array)
|
|
return value.EnumerateArray()
|
|
.Select(v => v.ValueKind == JsonValueKind.Number && v.TryGetDouble(out var d) ? d : double.NaN)
|
|
.Where(d => !double.IsNaN(d))
|
|
.ToArray();
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
static List<string> ReadStringArray(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!TryGetProperty(element, name, out var value))
|
|
continue;
|
|
if (value.ValueKind == JsonValueKind.Array)
|
|
return value.EnumerateArray()
|
|
.Select(v => v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : v.ToString())
|
|
.Where(v => !string.IsNullOrWhiteSpace(v))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
if (value.ValueKind == JsonValueKind.String)
|
|
return value.GetString()!
|
|
.Split([';', ',', '|'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.ToList();
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
static Bounds2D? ReadBounds2D(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!TryGetProperty(element, name, out var value))
|
|
continue;
|
|
if (value.ValueKind == JsonValueKind.Object)
|
|
{
|
|
var minX = ReadDouble(value, "minX", "min_x", "xMin", "xmin");
|
|
var minY = ReadDouble(value, "minY", "min_y", "yMin", "ymin");
|
|
var maxX = ReadDouble(value, "maxX", "max_x", "xMax", "xmax");
|
|
var maxY = ReadDouble(value, "maxY", "max_y", "yMax", "ymax");
|
|
if (minX != null && minY != null && maxX != null && maxY != null)
|
|
return new Bounds2D(minX.Value, minY.Value, maxX.Value, maxY.Value).Ordered();
|
|
}
|
|
if (value.ValueKind == JsonValueKind.Array)
|
|
{
|
|
var values = value.EnumerateArray()
|
|
.Where(v => v.ValueKind == JsonValueKind.Number)
|
|
.Select(v => v.GetDouble())
|
|
.ToList();
|
|
if (values.Count >= 4)
|
|
return new Bounds2D(values[0], values[1], values[2], values[3]).Ordered();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static Point2D? ReadPoint2D(JsonElement element, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (!TryGetProperty(element, name, out var value))
|
|
continue;
|
|
if (value.ValueKind == JsonValueKind.Array)
|
|
{
|
|
var values = value.EnumerateArray()
|
|
.Where(v => v.ValueKind == JsonValueKind.Number)
|
|
.Select(v => v.GetDouble())
|
|
.ToList();
|
|
if (values.Count >= 2)
|
|
return new Point2D(values[0], values[1]);
|
|
}
|
|
if (value.ValueKind == JsonValueKind.Object)
|
|
{
|
|
var x = ReadDouble(value, "x", "X");
|
|
var y = ReadDouble(value, "y", "Y");
|
|
if (x != null && y != null)
|
|
return new Point2D(x.Value, y.Value);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static string FirstNonEmpty(params string[] values) => values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v))?.Trim() ?? "";
|
|
|
|
static void AddIfNotEmpty(List<string> values, string value)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(value) && !values.Contains(value, StringComparer.OrdinalIgnoreCase))
|
|
values.Add(value);
|
|
}
|
|
}
|
|
|
|
static class JsonDefaults
|
|
{
|
|
public static readonly JsonSerializerOptions Options = new()
|
|
{
|
|
WriteIndented = true,
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
};
|
|
}
|
|
|
|
sealed class CliOptions
|
|
{
|
|
public string BrepReportPath { get; set; } = "";
|
|
public string DimensionsPath { get; set; } = "";
|
|
public string OutputDir { get; set; } = Path.Combine(Environment.CurrentDirectory, "runtime", "dimension-face-matcher");
|
|
public string? OutputPath { get; set; }
|
|
public string ViewDirection { get; set; } = "+X";
|
|
public double RollDeg { get; set; }
|
|
public int Top { get; set; } = 8;
|
|
|
|
public static CliOptions Parse(string[] args)
|
|
{
|
|
var options = new CliOptions();
|
|
for (var i = 0; i < args.Length; i++)
|
|
{
|
|
var arg = args[i];
|
|
string Next()
|
|
{
|
|
if (i + 1 >= args.Length)
|
|
throw new ArgumentException($"Missing value for {arg}");
|
|
return args[++i];
|
|
}
|
|
|
|
switch (arg)
|
|
{
|
|
case "--brep-report":
|
|
options.BrepReportPath = Next();
|
|
break;
|
|
case "--dimensions":
|
|
options.DimensionsPath = Next();
|
|
break;
|
|
case "--output-dir":
|
|
options.OutputDir = Next();
|
|
break;
|
|
case "--output":
|
|
options.OutputPath = Next();
|
|
break;
|
|
case "--view-direction":
|
|
options.ViewDirection = Next();
|
|
break;
|
|
case "--roll":
|
|
case "--roll-deg":
|
|
options.RollDeg = double.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "--top":
|
|
options.Top = int.Parse(Next(), CultureInfo.InvariantCulture);
|
|
break;
|
|
case "-h":
|
|
case "--help":
|
|
PrintUsage();
|
|
Environment.Exit(0);
|
|
break;
|
|
default:
|
|
throw new ArgumentException($"Unknown argument: {arg}");
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
static void PrintUsage()
|
|
{
|
|
Console.WriteLine("""
|
|
Usage:
|
|
dotnet run --project tools\drawing-diagnostics\DimensionFaceMatcher\DimensionFaceMatcher.csproj -- \
|
|
--brep-report <section_brep_report.json> \
|
|
--dimensions <dimension_anchors.json> \
|
|
--view-direction +X \
|
|
--roll 270 \
|
|
--output-dir runtime\dimension-face-matcher
|
|
|
|
Dimension input schema:
|
|
{
|
|
"view": {
|
|
"direction": "+X",
|
|
"rollDeg": 270,
|
|
"drawingBBox": { "minX": 0, "minY": 0, "maxX": 300, "maxY": 200 }
|
|
},
|
|
"dimensions": [
|
|
{
|
|
"id": "dim_001",
|
|
"text": "Φ30 H7/h6",
|
|
"measurementMm": 30,
|
|
"kind": "cylindrical_fit",
|
|
"ownerComponents": ["轴", "轴承"],
|
|
"anchors": [
|
|
{ "id": "a", "bbox": { "minX": 10, "minY": 20, "maxX": 10, "maxY": 60 } },
|
|
{ "id": "b", "bbox": { "minX": 45, "minY": 20, "maxX": 45, "maxY": 60 } }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""");
|
|
}
|
|
}
|
|
|
|
sealed record MatchReport
|
|
{
|
|
public string Schema { get; init; } = "";
|
|
public string BrepReportPath { get; init; } = "";
|
|
public string DimensionsPath { get; init; } = "";
|
|
public ViewSpec View { get; init; } = new();
|
|
public Bounds2D ModelProjectedBounds { get; init; }
|
|
public Bounds2D DrawingBounds { get; init; }
|
|
public int FaceCount { get; init; }
|
|
public List<DimensionMatchResult> Dimensions { get; init; } = [];
|
|
}
|
|
|
|
sealed record DimensionMatchResult
|
|
{
|
|
public string DimensionId { get; init; } = "";
|
|
public string Text { get; init; } = "";
|
|
public string Kind { get; init; } = "";
|
|
public double? MeasurementMm { get; init; }
|
|
public Bounds2D? BBox { get; init; }
|
|
public int AnchorCount { get; init; }
|
|
public bool IsUnique { get; init; }
|
|
public double TopScore { get; init; }
|
|
public double SecondScore { get; init; }
|
|
public double ScoreMargin { get; init; }
|
|
public List<FacePairCandidate> TopCandidates { get; init; } = [];
|
|
}
|
|
|
|
sealed record FacePairCandidate
|
|
{
|
|
public double Score { get; init; }
|
|
public FaceSummary FaceA { get; init; } = new();
|
|
public FaceSummary FaceB { get; init; } = new();
|
|
public double? ExpectedMeasurementMm { get; init; }
|
|
public double? MeasurementErrorMm { get; init; }
|
|
public string Assignment { get; init; } = "";
|
|
public CandidateScoreBreakdown Components { get; init; } = new();
|
|
public List<string> Evidence { get; init; } = [];
|
|
}
|
|
|
|
sealed record FaceSummary
|
|
{
|
|
public string FaceRef { get; init; } = "";
|
|
public string ComponentName { get; init; } = "";
|
|
public string ComponentDisplayName { get; init; } = "";
|
|
public int FaceIndex { get; init; }
|
|
public string FaceKind { get; init; } = "";
|
|
public string FunctionalRole { get; init; } = "";
|
|
public string SurfaceProcessRole { get; init; } = "";
|
|
public double[] CenterMm { get; init; } = [];
|
|
public double[] BBoxMm { get; init; } = [];
|
|
public Point2D ProjectedCenter { get; init; }
|
|
public Bounds2D ProjectedBBox { get; init; }
|
|
public double? RadiusMm { get; init; }
|
|
public double? DiameterMm { get; init; }
|
|
|
|
public static FaceSummary From(ProjectedFace face) => new()
|
|
{
|
|
FaceRef = face.FaceRef,
|
|
ComponentName = face.ComponentName,
|
|
ComponentDisplayName = face.ComponentDisplayName,
|
|
FaceIndex = face.FaceIndex,
|
|
FaceKind = face.FaceKind,
|
|
FunctionalRole = face.FunctionalRole,
|
|
SurfaceProcessRole = face.SurfaceProcessRole,
|
|
CenterMm = face.CenterMm,
|
|
BBoxMm = face.BBoxMm,
|
|
ProjectedCenter = face.ProjectedCenter,
|
|
ProjectedBBox = face.ProjectedBBox,
|
|
RadiusMm = face.RadiusMm,
|
|
DiameterMm = face.DiameterMm
|
|
};
|
|
}
|
|
|
|
sealed record CandidateScoreBreakdown
|
|
{
|
|
public double ValueScore { get; init; }
|
|
public double AnchorScore { get; init; }
|
|
public double RelationScore { get; init; }
|
|
public double RoleScore { get; init; }
|
|
public double AnchorAScore { get; init; }
|
|
public double AnchorBScore { get; init; }
|
|
public double AnchorAPositionScore { get; init; }
|
|
public double AnchorBPositionScore { get; init; }
|
|
}
|
|
|
|
sealed record DrawingDimension
|
|
{
|
|
public string Id { get; init; } = "";
|
|
public string Text { get; init; } = "";
|
|
public string Kind { get; init; } = "linear_distance";
|
|
public double? MeasurementMm { get; init; }
|
|
public Bounds2D? BBox { get; init; }
|
|
public List<DimensionAnchor> Anchors { get; init; } = [];
|
|
public List<string> OwnerComponents { get; init; } = [];
|
|
|
|
public DrawingDimension Normalize(Bounds2D bounds) => this with
|
|
{
|
|
BBox = BBox == null ? null : bounds.Normalize(BBox.Value),
|
|
Anchors = Anchors.Select(a => a.Normalize(bounds)).ToList()
|
|
};
|
|
|
|
public DimensionAnchor FallbackAnchor() => new()
|
|
{
|
|
Id = "dimension_bbox_center",
|
|
BBox = BBox,
|
|
Midpoint = BBox?.Center ?? new Point2D(0, 0),
|
|
NormalizedMidpoint = BBox?.Center ?? new Point2D(0, 0),
|
|
NormalizedBBox = BBox
|
|
};
|
|
|
|
public IEnumerable<Bounds2D> AllBoxes()
|
|
{
|
|
if (BBox != null)
|
|
yield return BBox.Value;
|
|
foreach (var anchor in Anchors)
|
|
{
|
|
if (anchor.BBox != null)
|
|
yield return anchor.BBox.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
sealed record DimensionAnchor
|
|
{
|
|
public string Id { get; init; } = "";
|
|
public Bounds2D? BBox { get; init; }
|
|
public Point2D Midpoint { get; init; }
|
|
public Point2D? Start { get; init; }
|
|
public Point2D? End { get; init; }
|
|
public Point2D NormalizedMidpoint { get; init; }
|
|
public Bounds2D? NormalizedBBox { get; init; }
|
|
|
|
public DimensionAnchor Normalize(Bounds2D bounds) => this with
|
|
{
|
|
NormalizedMidpoint = bounds.Normalize(Midpoint),
|
|
NormalizedBBox = BBox == null ? null : bounds.Normalize(BBox.Value)
|
|
};
|
|
}
|
|
|
|
sealed record ModelFace
|
|
{
|
|
public string FaceRef { get; init; } = "";
|
|
public string ComponentName { get; init; } = "";
|
|
public string ComponentDisplayName { get; init; } = "";
|
|
public string ComponentPath { get; init; } = "";
|
|
public string ComponentFileName { get; init; } = "";
|
|
public int FaceIndex { get; init; }
|
|
public string FaceKind { get; init; } = "";
|
|
public double AreaMm2 { get; init; }
|
|
public double[] CenterMm { get; init; } = [];
|
|
public double[] BBoxMm { get; init; } = [];
|
|
public double[] Normal { get; init; } = [];
|
|
public double[] AxisPointMm { get; init; } = [];
|
|
public double[] Axis { get; init; } = [];
|
|
public double? RadiusMm { get; init; }
|
|
public double? DiameterMm { get; init; }
|
|
public string FunctionalRole { get; init; } = "";
|
|
public string SurfaceProcessRole { get; init; } = "";
|
|
}
|
|
|
|
sealed record ProjectedFace(
|
|
string FaceRef,
|
|
string ComponentName,
|
|
string ComponentDisplayName,
|
|
string ComponentPath,
|
|
string ComponentFileName,
|
|
int FaceIndex,
|
|
string FaceKind,
|
|
double[] CenterMm,
|
|
double[] BBoxMm,
|
|
double[] Normal,
|
|
double[] AxisPointMm,
|
|
double[] Axis,
|
|
double? RadiusMm,
|
|
double? DiameterMm,
|
|
string FunctionalRole,
|
|
string SurfaceProcessRole,
|
|
Point2D ProjectedCenter,
|
|
Bounds2D ProjectedBBox,
|
|
Point2D NormalizedCenter,
|
|
Bounds2D NormalizedBBox)
|
|
{
|
|
public string ComponentDisplayNameOrName => string.IsNullOrWhiteSpace(ComponentDisplayName) ? ComponentName : ComponentDisplayName;
|
|
}
|
|
|
|
sealed record AnchorFaceScore
|
|
{
|
|
public ProjectedFace Face { get; init; } = null!;
|
|
public double Score { get; init; }
|
|
public double PositionScore { get; init; }
|
|
public double BBoxScore { get; init; }
|
|
public double OwnerScore { get; init; }
|
|
public double TypeScore { get; init; }
|
|
}
|
|
|
|
readonly record struct ValueScore(double Score, double? ExpectedMeasurementMm, double? ErrorMm);
|
|
|
|
sealed record ViewSpec
|
|
{
|
|
public string Direction { get; init; } = "+X";
|
|
public double RollDeg { get; init; }
|
|
}
|
|
|
|
readonly record struct ViewBasis(double[] U, double[] V)
|
|
{
|
|
public static ViewBasis FromDirection(string direction) => direction switch
|
|
{
|
|
"+X" => new([0, 1, 0], [0, 0, 1]),
|
|
"-X" => new([0, -1, 0], [0, 0, 1]),
|
|
"+Y" => new([-1, 0, 0], [0, 0, 1]),
|
|
"-Y" => new([1, 0, 0], [0, 0, 1]),
|
|
"+Z" => new([1, 0, 0], [0, 1, 0]),
|
|
"-Z" => new([-1, 0, 0], [0, 1, 0]),
|
|
_ => throw new ArgumentException($"Unsupported direction `{direction}`")
|
|
};
|
|
}
|
|
|
|
readonly record struct Point2D(double X, double Y);
|
|
|
|
readonly record struct Bounds2D(double MinX, double MinY, double MaxX, double MaxY)
|
|
{
|
|
public double Width => Math.Max(MaxX - MinX, 1e-9);
|
|
public double Height => Math.Max(MaxY - MinY, 1e-9);
|
|
public Point2D Center => new((MinX + MaxX) / 2.0, (MinY + MaxY) / 2.0);
|
|
|
|
public Bounds2D Ordered() => new(Math.Min(MinX, MaxX), Math.Min(MinY, MaxY), Math.Max(MinX, MaxX), Math.Max(MinY, MaxY));
|
|
|
|
public Point2D Normalize(Point2D point) => new((point.X - Center.X) / Width, (point.Y - Center.Y) / Height);
|
|
|
|
public Bounds2D Normalize(Bounds2D box)
|
|
{
|
|
var a = Normalize(new Point2D(box.MinX, box.MinY));
|
|
var b = Normalize(new Point2D(box.MaxX, box.MaxY));
|
|
return new Bounds2D(a.X, a.Y, b.X, b.Y).Ordered();
|
|
}
|
|
|
|
public static Bounds2D Union(IEnumerable<Point2D> points)
|
|
{
|
|
var list = points.ToList();
|
|
if (list.Count == 0)
|
|
return new Bounds2D(-0.5, -0.5, 0.5, 0.5);
|
|
return new Bounds2D(
|
|
list.Min(p => p.X),
|
|
list.Min(p => p.Y),
|
|
list.Max(p => p.X),
|
|
list.Max(p => p.Y));
|
|
}
|
|
|
|
public static Bounds2D Union(IEnumerable<Bounds2D> boxes)
|
|
{
|
|
var list = boxes.ToList();
|
|
if (list.Count == 0)
|
|
return new Bounds2D(-0.5, -0.5, 0.5, 0.5);
|
|
return new Bounds2D(
|
|
list.Min(b => b.MinX),
|
|
list.Min(b => b.MinY),
|
|
list.Max(b => b.MaxX),
|
|
list.Max(b => b.MaxY));
|
|
}
|
|
}
|
|
|
|
static class Vec
|
|
{
|
|
public static double Dot(double[] a, double[] b) => a.Zip(b).Sum(pair => pair.First * pair.Second);
|
|
public static double[] Sub(double[] a, double[] b) => a.Zip(b).Select(pair => pair.First - pair.Second).ToArray();
|
|
public static double[] Mul(double[] a, double scale) => a.Select(v => v * scale).ToArray();
|
|
public static double Length(double[] a) => Math.Sqrt(a.Sum(v => v * v));
|
|
public static double[] Normalize(double[] a)
|
|
{
|
|
var length = Length(a);
|
|
return length <= 1e-9 ? a.ToArray() : a.Select(v => v / length).ToArray();
|
|
}
|
|
}
|