227 lines
6.5 KiB
C#
227 lines
6.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace FurnitureExporter;
|
|
|
|
public static class PanelFaceSemanticResolver
|
|
{
|
|
private const double SurfaceFaceToleranceMm = 3.0;
|
|
private const double InteriorFaceToleranceMm = 0.8;
|
|
|
|
public static string[] ResolveSurfaceFaces(
|
|
BoundingBox3D? boundingBox,
|
|
IReadOnlyList<double>? position,
|
|
IReadOnlyList<double>? normal,
|
|
double depth)
|
|
{
|
|
if (boundingBox?.Min?.Length != 3 || boundingBox.Max?.Length != 3)
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
var center = ToVector3(position);
|
|
var axis = Normalize(ToVector3(normal));
|
|
if (center == null || axis == null)
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
var faces = new List<string>();
|
|
var halfDepth = Math.Max(0, depth) / 2.0;
|
|
var dominantFaces = ResolveDominantAxisFaces(axis);
|
|
|
|
if (halfDepth > 0.01)
|
|
{
|
|
AddFace(
|
|
faces,
|
|
ResolveNearestSurfaceFace(
|
|
boundingBox,
|
|
Offset(center, axis, halfDepth),
|
|
dominantFaces.Positive,
|
|
dominantFaces.Negative,
|
|
SurfaceFaceToleranceMm));
|
|
AddFace(
|
|
faces,
|
|
ResolveNearestSurfaceFace(
|
|
boundingBox,
|
|
Offset(center, axis, -halfDepth),
|
|
dominantFaces.Negative,
|
|
dominantFaces.Positive,
|
|
SurfaceFaceToleranceMm));
|
|
}
|
|
|
|
if (faces.Count == 0)
|
|
{
|
|
AddFace(
|
|
faces,
|
|
ResolveNearestSurfaceFace(
|
|
boundingBox,
|
|
center,
|
|
dominantFaces.Positive,
|
|
dominantFaces.Negative,
|
|
SurfaceFaceToleranceMm + InteriorFaceToleranceMm));
|
|
}
|
|
|
|
return faces
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
}
|
|
|
|
public static string OppositeFace(string? face)
|
|
{
|
|
return face?.Trim().ToLowerInvariant() switch
|
|
{
|
|
"left" => "right",
|
|
"right" => "left",
|
|
"front" => "back",
|
|
"back" => "front",
|
|
"top" => "bottom",
|
|
"bottom" => "top",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
|
|
private static void AddFace(ICollection<string> faces, string? face)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(face))
|
|
{
|
|
return;
|
|
}
|
|
|
|
faces.Add(face!.Trim().ToLowerInvariant());
|
|
}
|
|
|
|
private static double[]? ToVector3(IReadOnlyList<double>? value)
|
|
{
|
|
if (value == null || value.Count < 3)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new[] { value[0], value[1], value[2] };
|
|
}
|
|
|
|
private static double[]? Normalize(double[]? value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var length = Math.Sqrt(value[0] * value[0] + value[1] * value[1] + value[2] * value[2]);
|
|
if (length < 1e-9)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new[]
|
|
{
|
|
value[0] / length,
|
|
value[1] / length,
|
|
value[2] / length,
|
|
};
|
|
}
|
|
|
|
private static double[] Offset(double[] origin, double[] direction, double distance)
|
|
{
|
|
return new[]
|
|
{
|
|
origin[0] + direction[0] * distance,
|
|
origin[1] + direction[1] * distance,
|
|
origin[2] + direction[2] * distance,
|
|
};
|
|
}
|
|
|
|
private static string? ResolveNearestSurfaceFace(BoundingBox3D box, IReadOnlyList<double> point)
|
|
{
|
|
return ResolveNearestSurfaceFace(box, point, null, null, SurfaceFaceToleranceMm);
|
|
}
|
|
|
|
private static string? ResolveNearestSurfaceFace(
|
|
BoundingBox3D box,
|
|
IReadOnlyList<double> point,
|
|
string? preferredFace,
|
|
string? secondaryFace,
|
|
double tolerance)
|
|
{
|
|
var candidates = new List<(string Face, double Distance)>();
|
|
|
|
if (IsWithinRange(point[1], box.Min[1], box.Max[1], tolerance) &&
|
|
IsWithinRange(point[2], box.Min[2], box.Max[2], tolerance))
|
|
{
|
|
candidates.Add(("left", Math.Abs(point[0] - box.Min[0])));
|
|
candidates.Add(("right", Math.Abs(point[0] - box.Max[0])));
|
|
}
|
|
|
|
if (IsWithinRange(point[0], box.Min[0], box.Max[0], tolerance) &&
|
|
IsWithinRange(point[2], box.Min[2], box.Max[2], tolerance))
|
|
{
|
|
candidates.Add(("front", Math.Abs(point[1] - box.Min[1])));
|
|
candidates.Add(("back", Math.Abs(point[1] - box.Max[1])));
|
|
}
|
|
|
|
if (IsWithinRange(point[0], box.Min[0], box.Max[0], tolerance) &&
|
|
IsWithinRange(point[1], box.Min[1], box.Max[1], tolerance))
|
|
{
|
|
candidates.Add(("bottom", Math.Abs(point[2] - box.Min[2])));
|
|
candidates.Add(("top", Math.Abs(point[2] - box.Max[2])));
|
|
}
|
|
|
|
var best = candidates
|
|
.Where(item => item.Distance <= tolerance)
|
|
.OrderBy(item => GetFacePriority(item.Face, preferredFace, secondaryFace))
|
|
.ThenBy(item => item.Distance)
|
|
.FirstOrDefault();
|
|
|
|
return string.IsNullOrWhiteSpace(best.Face) ? null : best.Face;
|
|
}
|
|
|
|
private static (string Positive, string Negative) ResolveDominantAxisFaces(IReadOnlyList<double> axis)
|
|
{
|
|
var absX = Math.Abs(axis[0]);
|
|
var absY = Math.Abs(axis[1]);
|
|
var absZ = Math.Abs(axis[2]);
|
|
|
|
if (absX >= absY && absX >= absZ)
|
|
{
|
|
return axis[0] >= 0
|
|
? ("right", "left")
|
|
: ("left", "right");
|
|
}
|
|
|
|
if (absY >= absX && absY >= absZ)
|
|
{
|
|
return axis[1] >= 0
|
|
? ("back", "front")
|
|
: ("front", "back");
|
|
}
|
|
|
|
return axis[2] >= 0
|
|
? ("top", "bottom")
|
|
: ("bottom", "top");
|
|
}
|
|
|
|
private static int GetFacePriority(string face, string? preferredFace, string? secondaryFace)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(preferredFace) &&
|
|
string.Equals(face, preferredFace, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(secondaryFace) &&
|
|
string.Equals(face, secondaryFace, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
return 2;
|
|
}
|
|
|
|
private static bool IsWithinRange(double value, double min, double max, double tolerance)
|
|
{
|
|
return value >= min - tolerance && value <= max + tolerance;
|
|
}
|
|
}
|