first commit
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
static class PartModelProcessing
|
||||
{
|
||||
public const string DirectPartBrepSource = "part_brep_direct";
|
||||
|
||||
public static List<ComponentHighlightImageRequest> BuildComponentHighlightImageRequests(SectionBrepReport report) => [];
|
||||
|
||||
public static List<FeatureHighlightImageRequest> BuildFeatureHighlightImageRequests(SectionBrepReport report)
|
||||
{
|
||||
var result = new List<FeatureHighlightImageRequest>();
|
||||
var component = report.AssemblyComponents
|
||||
.FirstOrDefault(c => c.ReviewScope.Equals("part_design_required", StringComparison.OrdinalIgnoreCase))
|
||||
?? report.AssemblyComponents.FirstOrDefault();
|
||||
if (component == null)
|
||||
return result;
|
||||
|
||||
var facesByRef = report.AssemblyFaces
|
||||
.GroupBy(FaceRef, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var feature in report.FeatureGraph.Features
|
||||
.Where(f => f.FaceRefs.Count > 0)
|
||||
.Where(f => SameComponent(f, component))
|
||||
.OrderBy(f => FeatureHighlightPriority(f.Type))
|
||||
.ThenBy(f => f.Id, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var targetFaces = feature.FaceRefs
|
||||
.Select(faceRef => facesByRef.TryGetValue(faceRef, out var face) ? face : null)
|
||||
.OfType<AssemblyFaceEvidence>()
|
||||
.ToList();
|
||||
if (targetFaces.Count == 0)
|
||||
continue;
|
||||
|
||||
var viewChoice = ChooseBestObliqueFeatureView(feature, targetFaces, report.AssemblyFaces);
|
||||
result.Add(new FeatureHighlightImageRequest
|
||||
{
|
||||
PlanId = $"feature_highlight_{SanitizeId(feature.Id)}",
|
||||
ComponentId = component.Id,
|
||||
InstanceName = component.InstanceName,
|
||||
DisplayName = component.DisplayName,
|
||||
ComponentPath = component.Path,
|
||||
FeatureId = feature.Id,
|
||||
FeatureType = feature.Type,
|
||||
FaceRefs = feature.FaceRefs.Distinct(StringComparer.OrdinalIgnoreCase).ToList(),
|
||||
Views = [viewChoice.ViewName],
|
||||
PreferredDirectionMm = Vec.Round(viewChoice.Direction),
|
||||
BlockingFaceRefs = viewChoice.BlockingFaceRefs,
|
||||
SelectionBasis = "feature_graph_face_refs_grouped_before_image_export"
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<ComponentContextImageRequest> BuildComponentContextImageRequests(SectionBrepReport report) => [];
|
||||
|
||||
public static AutoSectionImagePlan BuildAxisSectionPlan(SectionBrepReport report)
|
||||
{
|
||||
var plan = new AutoSectionImagePlan
|
||||
{
|
||||
MaxRequests = 1
|
||||
};
|
||||
|
||||
var axisFace = report.AssemblyFaces
|
||||
.Where(face => face.FaceKind.Equals("cylinder", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(face => face.Axis.Length >= 3 && face.AxisPointMm.Length >= 3)
|
||||
.OrderByDescending(face => face.AreaMm2)
|
||||
.ThenByDescending(face => face.RadiusMm)
|
||||
.FirstOrDefault();
|
||||
if (axisFace == null)
|
||||
return plan;
|
||||
|
||||
var containedAxis = Vec.Normalize(axisFace.Axis);
|
||||
var coaxialFaces = report.AssemblyFaces
|
||||
.Where(face => face.FaceKind.Equals("cylinder", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(face => face.Axis.Length >= 3 && face.AxisPointMm.Length >= 3)
|
||||
.Where(face => Math.Abs(Vec.Dot(Vec.Normalize(face.Axis), containedAxis)) >= 0.95)
|
||||
.ToList();
|
||||
if (!HasInternalRotationalStructure(coaxialFaces))
|
||||
return plan;
|
||||
|
||||
var target = string.IsNullOrWhiteSpace(axisFace.ComponentDisplayName)
|
||||
? axisFace.ComponentName
|
||||
: axisFace.ComponentDisplayName;
|
||||
var planeCandidates = ChooseSectionPlaneKeys(containedAxis);
|
||||
for (var i = 0; i < Math.Min(plan.MaxRequests, planeCandidates.Count); i++)
|
||||
{
|
||||
var candidate = planeCandidates[i];
|
||||
plan.Requests.Add(new AutoSectionImageRequest
|
||||
{
|
||||
Request = $"part_axis_center_section_{i + 1}",
|
||||
Rule = "For SLDPRT rotational parts with internal/coaxial structure, create a section plane through the main cylindrical axis.",
|
||||
Target = target,
|
||||
Reason = "Part input has no assembly components to hide or highlight; section evidence is generated directly from the opened part body.",
|
||||
PlaneKey = candidate.PlaneKey,
|
||||
OffsetMm = 0,
|
||||
OriginMm = Vec.Round(axisFace.AxisPointMm),
|
||||
PreferredNormalMm = Vec.Round(candidate.Normal),
|
||||
PreferredContainedAxisMm = Vec.Round(containedAxis),
|
||||
SourceRefs = [$"{axisFace.ComponentName}:face#{axisFace.FaceIndex}"]
|
||||
});
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
public static List<string> BuildSectionViewRequests(SectionBrepReport report)
|
||||
{
|
||||
return report.SectionImagePlan.Requests
|
||||
.Where(request => !string.IsNullOrWhiteSpace(request.PlaneKey))
|
||||
.Take(2)
|
||||
.Select(request =>
|
||||
$"{request.PlaneKey}:{request.Request}|target={SanitizeMetadata(request.Target)};reason={SanitizeMetadata(request.Reason)}")
|
||||
.ToList();
|
||||
}
|
||||
|
||||
static bool HasInternalRotationalStructure(IReadOnlyList<AssemblyFaceEvidence> coaxialFaces)
|
||||
{
|
||||
if (coaxialFaces.Count < 2)
|
||||
return false;
|
||||
|
||||
var radiusLayers = coaxialFaces
|
||||
.Where(face => face.RadiusMm > 0)
|
||||
.Select(face => Math.Round(face.RadiusMm, 1))
|
||||
.Distinct()
|
||||
.Count();
|
||||
if (radiusLayers >= 2)
|
||||
return true;
|
||||
|
||||
return coaxialFaces.Any(face =>
|
||||
face.FunctionalRole.Contains("hole", StringComparison.OrdinalIgnoreCase) ||
|
||||
face.FunctionalRole.Contains("bore", StringComparison.OrdinalIgnoreCase) ||
|
||||
face.SurfaceProcessRole.Contains("hole", StringComparison.OrdinalIgnoreCase) ||
|
||||
face.SurfaceProcessRole.Contains("bore", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
static List<(string PlaneKey, double[] Normal)> ChooseSectionPlaneKeys(double[] containedAxis)
|
||||
{
|
||||
var candidates = new List<(string PlaneKey, double[] Normal)>
|
||||
{
|
||||
("front", [0.0, 0.0, 1.0]),
|
||||
("top", [0.0, 1.0, 0.0]),
|
||||
("right", [1.0, 0.0, 0.0])
|
||||
};
|
||||
|
||||
return candidates
|
||||
.OrderBy(candidate => Math.Abs(Vec.Dot(containedAxis, candidate.Normal)))
|
||||
.ThenBy(candidate => candidate.PlaneKey, StringComparer.OrdinalIgnoreCase)
|
||||
.Take(2)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
static string SanitizeMetadata(string value) =>
|
||||
(value ?? "").Replace(';', ',').Replace('|', '/').Replace('\r', ' ').Replace('\n', ' ').Trim();
|
||||
|
||||
static bool SameComponent(MechanicalFeature feature, AssemblyComponentSummary component)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(feature.ComponentPath) &&
|
||||
!string.IsNullOrWhiteSpace(component.Path) &&
|
||||
feature.ComponentPath.Equals(component.Path, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return feature.ComponentName.Equals(component.InstanceName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
static int FeatureHighlightPriority(string featureType)
|
||||
{
|
||||
if (featureType.Contains("unknown", StringComparison.OrdinalIgnoreCase) ||
|
||||
featureType.Contains("complex", StringComparison.OrdinalIgnoreCase))
|
||||
return 0;
|
||||
if (featureType.Contains("hole", StringComparison.OrdinalIgnoreCase))
|
||||
return 1;
|
||||
if (featureType.Contains("cylindrical", StringComparison.OrdinalIgnoreCase))
|
||||
return 2;
|
||||
if (featureType.Contains("planar", StringComparison.OrdinalIgnoreCase))
|
||||
return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
static (string ViewName, double[] Direction, List<string> BlockingFaceRefs) ChooseBestObliqueFeatureView(
|
||||
MechanicalFeature feature,
|
||||
IReadOnlyList<AssemblyFaceEvidence> targetFaces,
|
||||
IReadOnlyList<AssemblyFaceEvidence> allFaces)
|
||||
{
|
||||
var targetBox = UnionBoxes(targetFaces.Select(f => f.BBoxMm));
|
||||
if (targetBox.Length < 6)
|
||||
return ("isometric", [1.0, 1.0, 1.0], []);
|
||||
|
||||
var targetRefs = feature.FaceRefs.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var others = allFaces
|
||||
.Where(face => !targetRefs.Contains(FaceRef(face)))
|
||||
.Where(face => face.BBoxMm.Length >= 6)
|
||||
.ToList();
|
||||
|
||||
var isPlanarTarget = IsPlanarFeatureTarget(feature, targetFaces);
|
||||
var preferredViewSide = DeterminePreferredViewSide(feature, targetFaces, allFaces, targetBox);
|
||||
var candidates = BuildObliqueCandidateDirections(false);
|
||||
candidates = EnforcePreferredViewSide(candidates, preferredViewSide, isPlanarTarget);
|
||||
|
||||
var projectedTargetAreas = candidates
|
||||
.Select(direction => ProjectBox(targetBox, direction).Area)
|
||||
.ToList();
|
||||
var maxTargetArea = Math.Max(1.0, projectedTargetAreas.Max());
|
||||
|
||||
var bestScore = double.MaxValue;
|
||||
var bestDirection = candidates[0];
|
||||
var bestBlocking = new List<string>();
|
||||
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
var direction = candidates[i];
|
||||
var targetProjection = ProjectBox(targetBox, direction);
|
||||
var blockers = new List<(string Ref, double Penalty)>();
|
||||
var score = (maxTargetArea - targetProjection.Area) / maxTargetArea * (isPlanarTarget ? 0.08 : 0.35);
|
||||
var absDirection = direction.Select(Math.Abs).ToArray();
|
||||
var maxComponent = Math.Max(absDirection[0], Math.Max(absDirection[1], absDirection[2]));
|
||||
var minComponent = Math.Min(absDirection[0], Math.Min(absDirection[1], absDirection[2]));
|
||||
score += (maxComponent - minComponent) / Math.Max(maxComponent, 0.001) * (isPlanarTarget ? 0.04 : 0.25);
|
||||
score += PreferredViewSidePenalty(direction, preferredViewSide, isPlanarTarget);
|
||||
score += StableNonPlanarAzimuthPenalty(feature, targetFaces, direction, preferredViewSide, isPlanarTarget);
|
||||
|
||||
foreach (var other in others)
|
||||
{
|
||||
var otherProjection = ProjectBox(other.BBoxMm, direction);
|
||||
var overlap = OverlapArea(targetProjection, otherProjection);
|
||||
if (overlap <= 0.001)
|
||||
continue;
|
||||
|
||||
var inFrontGap = otherProjection.MaxDepth - targetProjection.MinDepth;
|
||||
if (inFrontGap <= 0)
|
||||
continue;
|
||||
|
||||
var penalty = overlap / Math.Max(targetProjection.Area, 1.0) * (1.0 + Math.Min(inFrontGap, 100.0) / 200.0);
|
||||
score += penalty;
|
||||
blockers.Add((FaceRef(other), penalty));
|
||||
}
|
||||
|
||||
if (score < bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestDirection = direction;
|
||||
bestBlocking = blockers
|
||||
.OrderByDescending(item => item.Penalty)
|
||||
.Take(8)
|
||||
.Select(item => item.Ref)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return (ObliqueDirectionName(bestDirection), bestDirection, bestBlocking);
|
||||
}
|
||||
|
||||
static bool IsPlanarFeatureTarget(MechanicalFeature feature, IReadOnlyList<AssemblyFaceEvidence> targetFaces)
|
||||
{
|
||||
if (feature.Type.Contains("planar", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return targetFaces.Count > 0 &&
|
||||
targetFaces.All(face => face.FaceKind.Equals("plane", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
static double[] DeterminePreferredViewSide(
|
||||
MechanicalFeature feature,
|
||||
IReadOnlyList<AssemblyFaceEvidence> targetFaces,
|
||||
IReadOnlyList<AssemblyFaceEvidence> allFaces,
|
||||
double[] targetBox)
|
||||
{
|
||||
var planeNormal = WeightedAverageNormal(targetFaces
|
||||
.Where(face => face.FaceKind.Equals("plane", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(face => face.Normal.Length >= 3));
|
||||
if (planeNormal.Length >= 3)
|
||||
return planeNormal;
|
||||
|
||||
var componentFaces = allFaces
|
||||
.Where(face => face.ComponentName.Equals(feature.ComponentName, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(face => face.BBoxMm.Length >= 6)
|
||||
.ToList();
|
||||
if (componentFaces.Count == 0)
|
||||
componentFaces = allFaces.Where(face => face.BBoxMm.Length >= 6).ToList();
|
||||
|
||||
var modelBox = UnionBoxes(componentFaces.Select(face => face.BBoxMm));
|
||||
var targetCenter = BoxCenter(targetBox);
|
||||
var modelCenter = BoxCenter(modelBox);
|
||||
if (targetCenter.Length < 3 || modelCenter.Length < 3 || modelBox.Length < 6)
|
||||
return [];
|
||||
|
||||
var verticalOffset = targetCenter[1] - modelCenter[1];
|
||||
if (Math.Abs(verticalOffset) >= 0.001)
|
||||
return verticalOffset > 0
|
||||
? [0.0, 1.0, 0.0]
|
||||
: [0.0, -1.0, 0.0];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
static double[] WeightedAverageNormal(IEnumerable<AssemblyFaceEvidence> faces)
|
||||
{
|
||||
var sum = new[] { 0.0, 0.0, 0.0 };
|
||||
var totalWeight = 0.0;
|
||||
foreach (var face in faces)
|
||||
{
|
||||
var normal = Vec.Normalize(face.Normal);
|
||||
if (normal.Length < 3)
|
||||
continue;
|
||||
|
||||
var weight = Math.Max(face.AreaMm2, 1.0);
|
||||
sum = Vec.Add(sum, Vec.Mul(normal, weight));
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
if (totalWeight <= 0.0 || Vec.Norm(sum) < 1e-6)
|
||||
return [];
|
||||
|
||||
return Vec.Normalize(sum);
|
||||
}
|
||||
|
||||
static List<double[]> EnforcePreferredViewSide(List<double[]> candidates, double[] preferredViewSide, bool preferSteeperPlanarOblique)
|
||||
{
|
||||
if (preferredViewSide.Length < 3)
|
||||
return candidates;
|
||||
|
||||
var preferred = Vec.Normalize(preferredViewSide);
|
||||
var minimumAlignment = preferSteeperPlanarOblique ? 0.30 : 0.7;
|
||||
var maximumAlignment = preferSteeperPlanarOblique ? 0.78 : 1.0;
|
||||
var filtered = candidates
|
||||
.Where(direction =>
|
||||
{
|
||||
var alignment = Vec.Dot(Vec.Normalize(direction), preferred);
|
||||
return alignment >= minimumAlignment && alignment <= maximumAlignment;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (filtered.Count == 0 && preferSteeperPlanarOblique)
|
||||
{
|
||||
filtered = candidates
|
||||
.Where(direction => Vec.Dot(Vec.Normalize(direction), preferred) >= minimumAlignment)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return filtered.Count > 0 ? filtered : candidates;
|
||||
}
|
||||
|
||||
static double PreferredViewSidePenalty(double[] direction, double[] preferredViewSide, bool preferSteeperPlanarOblique)
|
||||
{
|
||||
if (preferredViewSide.Length < 3)
|
||||
return 0.0;
|
||||
|
||||
var alignment = Vec.Dot(Vec.Normalize(direction), Vec.Normalize(preferredViewSide));
|
||||
if (preferSteeperPlanarOblique)
|
||||
{
|
||||
const double targetPlanarAlignment = 0.45;
|
||||
return Math.Abs(alignment - targetPlanarAlignment) * 0.55;
|
||||
}
|
||||
|
||||
return (1.0 - alignment) * 0.55;
|
||||
}
|
||||
|
||||
static double StableNonPlanarAzimuthPenalty(
|
||||
MechanicalFeature feature,
|
||||
IReadOnlyList<AssemblyFaceEvidence> targetFaces,
|
||||
double[] direction,
|
||||
double[] preferredViewSide,
|
||||
bool isPlanarTarget)
|
||||
{
|
||||
if (isPlanarTarget || preferredViewSide.Length < 3)
|
||||
return 0.0;
|
||||
|
||||
var hasCurvedOrUnknownTarget = targetFaces.Any(face =>
|
||||
face.FaceKind.Equals("cylinder", StringComparison.OrdinalIgnoreCase) ||
|
||||
face.FaceKind.Equals("cone", StringComparison.OrdinalIgnoreCase) ||
|
||||
face.FaceKind.Equals("other", StringComparison.OrdinalIgnoreCase));
|
||||
if (!hasCurvedOrUnknownTarget)
|
||||
return 0.0;
|
||||
|
||||
var preferred = Vec.Normalize(preferredViewSide);
|
||||
if (preferred.Length < 3 || Math.Abs(preferred[1]) < 0.5)
|
||||
return 0.0;
|
||||
|
||||
// When curved/unknown faces only tell us "upper/lower", X/Z is otherwise decided
|
||||
// by small bbox-score differences. Keep a stable oblique quadrant for direct part
|
||||
// images so adjacent main-axis cylindrical features are viewed consistently.
|
||||
var stableOblique = Vec.Normalize([1.0, preferred[1] >= 0 ? 2.0 : -2.0, -1.0]);
|
||||
var alignment = Vec.Dot(Vec.Normalize(direction), stableOblique);
|
||||
var weight = feature.Type.Contains("main_axis_cylindrical", StringComparison.OrdinalIgnoreCase)
|
||||
? 0.35
|
||||
: 0.18;
|
||||
|
||||
return (1.0 - alignment) * weight;
|
||||
}
|
||||
|
||||
static double[] ProjectOntoPlane(double[] vector, double[] planeNormal)
|
||||
{
|
||||
var normal = Vec.Normalize(planeNormal);
|
||||
var v = Vec.Normalize(vector);
|
||||
if (normal.Length < 3 || v.Length < 3)
|
||||
return [];
|
||||
|
||||
return Vec.Normalize(Vec.Sub(v, Vec.Mul(normal, Vec.Dot(v, normal))));
|
||||
}
|
||||
|
||||
static string ObliqueDirectionName(double[] direction)
|
||||
{
|
||||
static string Sign(double value, string axis) => value >= 0 ? $"{axis}p" : $"{axis}n";
|
||||
var ax = Math.Abs(direction.ElementAtOrDefault(0));
|
||||
var ay = Math.Abs(direction.ElementAtOrDefault(1));
|
||||
var az = Math.Abs(direction.ElementAtOrDefault(2));
|
||||
var min = Math.Max(0.001, Math.Min(ax, Math.Min(ay, az)));
|
||||
var wx = Math.Max(1, (int)Math.Round(ax / min));
|
||||
var wy = Math.Max(1, (int)Math.Round(ay / min));
|
||||
var wz = Math.Max(1, (int)Math.Round(az / min));
|
||||
var signName = $"oblique_{Sign(direction.ElementAtOrDefault(0), "x")}_{Sign(direction.ElementAtOrDefault(1), "y")}_{Sign(direction.ElementAtOrDefault(2), "z")}";
|
||||
return wx == 1 && wy == 1 && wz == 1
|
||||
? signName
|
||||
: $"{signName}_x{wx}_y{wy}_z{wz}";
|
||||
}
|
||||
|
||||
static List<double[]> BuildObliqueCandidateDirections(bool includeSteepPlanarObliques)
|
||||
{
|
||||
var signs = new[] { -1.0, 1.0 };
|
||||
var result = new List<double[]>();
|
||||
|
||||
foreach (var sx in signs)
|
||||
foreach (var sy in signs)
|
||||
foreach (var sz in signs)
|
||||
{
|
||||
result.Add(Vec.Normalize([sx, sy, sz]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static double[] UnionBoxes(IEnumerable<double[]> boxes)
|
||||
{
|
||||
var valid = boxes.Where(box => box.Length >= 6).ToList();
|
||||
if (valid.Count == 0)
|
||||
return [];
|
||||
|
||||
return
|
||||
[
|
||||
valid.Min(box => box[0]),
|
||||
valid.Min(box => box[1]),
|
||||
valid.Min(box => box[2]),
|
||||
valid.Max(box => box[3]),
|
||||
valid.Max(box => box[4]),
|
||||
valid.Max(box => box[5])
|
||||
];
|
||||
}
|
||||
|
||||
static double[] BoxCenter(double[] box) =>
|
||||
box.Length >= 6
|
||||
? [(box[0] + box[3]) / 2.0, (box[1] + box[4]) / 2.0, (box[2] + box[5]) / 2.0]
|
||||
: [];
|
||||
|
||||
static ProjectedBox ProjectBox(double[] box, double[] viewDirection)
|
||||
{
|
||||
var d = Vec.Normalize(viewDirection);
|
||||
var up = Math.Abs(Vec.Dot(d, [0.0, 1.0, 0.0])) > 0.92 ? new[] { 0.0, 0.0, 1.0 } : new[] { 0.0, 1.0, 0.0 };
|
||||
var u = Vec.Normalize(Vec.Cross(up, d));
|
||||
var v = Vec.Normalize(Vec.Cross(d, u));
|
||||
|
||||
var corners = BoxCorners(box);
|
||||
var uValues = corners.Select(p => Vec.Dot(p, u)).ToList();
|
||||
var vValues = corners.Select(p => Vec.Dot(p, v)).ToList();
|
||||
var dValues = corners.Select(p => Vec.Dot(p, d)).ToList();
|
||||
|
||||
return new ProjectedBox(
|
||||
uValues.Min(),
|
||||
uValues.Max(),
|
||||
vValues.Min(),
|
||||
vValues.Max(),
|
||||
dValues.Min(),
|
||||
dValues.Max());
|
||||
}
|
||||
|
||||
static List<double[]> BoxCorners(double[] box) =>
|
||||
[
|
||||
[box[0], box[1], box[2]],
|
||||
[box[0], box[1], box[5]],
|
||||
[box[0], box[4], box[2]],
|
||||
[box[0], box[4], box[5]],
|
||||
[box[3], box[1], box[2]],
|
||||
[box[3], box[1], box[5]],
|
||||
[box[3], box[4], box[2]],
|
||||
[box[3], box[4], box[5]]
|
||||
];
|
||||
|
||||
static double OverlapArea(ProjectedBox a, ProjectedBox b)
|
||||
{
|
||||
var width = Math.Max(0.0, Math.Min(a.MaxU, b.MaxU) - Math.Max(a.MinU, b.MinU));
|
||||
var height = Math.Max(0.0, Math.Min(a.MaxV, b.MaxV) - Math.Max(a.MinV, b.MinV));
|
||||
return width * height;
|
||||
}
|
||||
|
||||
static string FaceRef(AssemblyFaceEvidence face) => $"{face.ComponentName}:face#{face.FaceIndex}";
|
||||
|
||||
static string SanitizeId(string value)
|
||||
{
|
||||
var chars = value.Select(ch => char.IsLetterOrDigit(ch) ? ch : '_').ToArray();
|
||||
return new string(chars).Trim('_');
|
||||
}
|
||||
}
|
||||
|
||||
sealed record ProjectedBox(double MinU, double MaxU, double MinV, double MaxV, double MinDepth, double MaxDepth)
|
||||
{
|
||||
public double Area => Math.Max(0.0, MaxU - MinU) * Math.Max(0.0, MaxV - MinV);
|
||||
}
|
||||
Reference in New Issue
Block a user