包含: - furniture-3d-system:Next.js 16 全栈应用(资产中心、产品管理、3D装配动画) - autocad-plugin:AutoCAD 家具导出插件源码 - 资产中心改造:编辑弹窗化、删除功能、Toast反馈、自定义Select组件 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
620 lines
23 KiB
C#
620 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace FurnitureExporter;
|
|
|
|
public class AssemblySemanticAnalyzer
|
|
{
|
|
private const double FaceGapToleranceMm = 3.0;
|
|
private const double RelaxedFaceGapToleranceMm = 10.0;
|
|
private const double FaceOverlapRatioMin = 0.25;
|
|
private const double OutsideMarginMm = 30.0;
|
|
private const double MinPreassembleDistanceMm = 80.0;
|
|
private const double HighConfidenceThreshold = 0.75;
|
|
|
|
private static readonly string[] MinFaces = { "left", "front", "bottom" };
|
|
private static readonly string[] MaxFaces = { "right", "back", "top" };
|
|
|
|
public AssemblyData Analyze(
|
|
IReadOnlyList<PanelInfo> panels,
|
|
IReadOnlyList<HoleInfo> holes,
|
|
IReadOnlyList<HoleGroupInfo> holeGroups,
|
|
IReadOnlyList<HardwareType> hardwareTypes)
|
|
{
|
|
_ = holes;
|
|
_ = holeGroups;
|
|
|
|
var result = new AssemblyData();
|
|
if (panels == null || panels.Count == 0)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
var boxes = panels.ToDictionary(panel => panel.Id, BuildBox, StringComparer.OrdinalIgnoreCase);
|
|
var modelBox = MergeBoxes(boxes.Values);
|
|
var hardwareSignals = BuildHardwareSignals(hardwareTypes ?? Array.Empty<HardwareType>());
|
|
var pairRelations = new List<PairRelation>();
|
|
|
|
for (var i = 0; i < panels.Count; i++)
|
|
{
|
|
for (var j = i + 1; j < panels.Count; j++)
|
|
{
|
|
var relation = BuildPairRelation(
|
|
boxes[panels[i].Id],
|
|
boxes[panels[j].Id],
|
|
hardwareSignals.TryGetValue(PairKey(panels[i].Id, panels[j].Id), out var signal) ? signal : null);
|
|
if (relation != null)
|
|
{
|
|
pairRelations.Add(relation);
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Connections = pairRelations
|
|
.Select(relation => new AssemblyConnectionInfo
|
|
{
|
|
SourcePanelId = relation.PanelAId,
|
|
TargetPanelId = relation.PanelBId,
|
|
SelfFace = relation.FaceFromA,
|
|
TargetFace = relation.FaceFromB,
|
|
ConnectionType = relation.ConnectionType,
|
|
Confidence = Math.Round(relation.Confidence, 4),
|
|
})
|
|
.OrderBy(item => item.SourcePanelId, StringComparer.OrdinalIgnoreCase)
|
|
.ThenBy(item => item.TargetPanelId, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
foreach (var panel in panels)
|
|
{
|
|
var semantic = BuildPanelAssembly(panel, boxes, modelBox, pairRelations);
|
|
result.PanelAssemblies.Add(semantic);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static PanelAssemblyInfo BuildPanelAssembly(
|
|
PanelInfo panel,
|
|
IReadOnlyDictionary<string, PanelBox> boxes,
|
|
PanelBox modelBox,
|
|
IReadOnlyList<PairRelation> pairRelations)
|
|
{
|
|
var box = boxes[panel.Id];
|
|
var related = pairRelations
|
|
.Select(relation =>
|
|
{
|
|
if (string.Equals(relation.PanelAId, panel.Id, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return new PanelRelationView(
|
|
relation.PanelBId,
|
|
relation.FaceFromA,
|
|
relation.ConnectionType,
|
|
relation.Confidence,
|
|
relation.Score,
|
|
relation.HasHardware);
|
|
}
|
|
|
|
if (string.Equals(relation.PanelBId, panel.Id, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return new PanelRelationView(
|
|
relation.PanelAId,
|
|
relation.FaceFromB,
|
|
relation.ConnectionType,
|
|
relation.Confidence,
|
|
relation.Score,
|
|
relation.HasHardware);
|
|
}
|
|
|
|
return null;
|
|
})
|
|
.Where(item => item != null)
|
|
.OrderByDescending(item => item!.Score)
|
|
.ToList();
|
|
|
|
var targets = related
|
|
.Where(item => item!.Score >= 0.42)
|
|
.Select(item => item!.TargetPanelId)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.Take(4)
|
|
.ToList();
|
|
|
|
var dependsOn = related
|
|
.Where(item => item!.Score >= 0.55)
|
|
.Select(item => item!.TargetPanelId)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.Take(3)
|
|
.ToList();
|
|
|
|
var primary = related.FirstOrDefault();
|
|
var warnings = new List<string>();
|
|
var choice = ChooseDirection(panel, box, modelBox, primary?.SelfFace, primary?.HasHardware ?? false);
|
|
|
|
if (choice == null)
|
|
{
|
|
warnings.Add("未找到明确装配方向,建议在 Web 端人工补全。");
|
|
return new PanelAssemblyInfo
|
|
{
|
|
PanelId = panel.Id,
|
|
MountMode = "unknown",
|
|
AssemblyAxis = new[] { 0.0, 0.0, 0.0 },
|
|
AssemblyDirection = "unknown",
|
|
PreAssembleOffset = null,
|
|
TravelDistance = 0,
|
|
DependsOn = dependsOn,
|
|
PrimarySupportPanelId = primary?.TargetPanelId,
|
|
TargetPanelIds = targets,
|
|
Mates = BuildMateMap(related),
|
|
Confidence = 0.22,
|
|
Warnings = warnings,
|
|
AutoGenerated = true,
|
|
};
|
|
}
|
|
|
|
if (choice.Source == "relation" && !(primary?.HasHardware ?? false))
|
|
{
|
|
warnings.Add("当前方向主要依据板件配合面推导,建议结合孔位或五金关系复核。");
|
|
}
|
|
if (choice.Source == "fallback")
|
|
{
|
|
warnings.Add("当前方向主要依据板件类型和空间位置兜底,建议人工复核。");
|
|
}
|
|
if (!IsLikelyBoard(panel))
|
|
{
|
|
warnings.Add("当前实体尺寸更像块状件而不是标准板件,语义仅作兜底参考。");
|
|
}
|
|
if (targets.Count == 0)
|
|
{
|
|
warnings.Add("未找到明确目标板件,预装距离按整体模型边界兜底计算。");
|
|
}
|
|
|
|
var referenceBox = targets
|
|
.Select(id => boxes.TryGetValue(id, out var targetBox) ? targetBox : null)
|
|
.Where(item => item != null)
|
|
.Cast<PanelBox>()
|
|
.ToList();
|
|
var travel = CalculateTravel(box, referenceBox.Count > 0 ? MergeBoxes(referenceBox) : modelBox, choice.Direction);
|
|
var confidence = choice.Confidence;
|
|
if (targets.Count == 0) confidence = Math.Min(confidence, 0.58);
|
|
if (!IsLikelyBoard(panel)) confidence -= 0.14;
|
|
confidence = Clamp01(confidence);
|
|
if (confidence < HighConfidenceThreshold)
|
|
{
|
|
warnings.Add("当前语义置信度偏低,建议由产品人员在 Web 端确认后再用于正式步骤。");
|
|
}
|
|
|
|
return new PanelAssemblyInfo
|
|
{
|
|
PanelId = panel.Id,
|
|
MountMode = choice.MountMode,
|
|
AssemblyAxis = AxisVector(choice.Direction),
|
|
AssemblyDirection = choice.Direction,
|
|
PreAssembleOffset = OffsetVector(choice.Direction, travel),
|
|
TravelDistance = Math.Round(travel, 2),
|
|
DependsOn = dependsOn,
|
|
PrimarySupportPanelId = primary?.TargetPanelId,
|
|
TargetPanelIds = targets,
|
|
Mates = BuildMateMap(related),
|
|
Confidence = Math.Round(confidence, 4),
|
|
Warnings = warnings.Distinct(StringComparer.OrdinalIgnoreCase).ToList(),
|
|
AutoGenerated = true,
|
|
};
|
|
}
|
|
|
|
private static DirectionChoice? ChooseDirection(
|
|
PanelInfo panel,
|
|
PanelBox box,
|
|
PanelBox modelBox,
|
|
string? primaryFace,
|
|
bool hasHardware)
|
|
{
|
|
var normalizedType = (panel.PanelType ?? string.Empty).Trim().ToLowerInvariant();
|
|
if (normalizedType == "top_panel") return new DirectionChoice("top", "drop_in", 0.88, "fallback");
|
|
if (normalizedType == "bottom_panel") return new DirectionChoice("bottom", "drop_in", 0.84, "fallback");
|
|
if (normalizedType == "shelf") return new DirectionChoice("top", "drop_in", 0.78, "fallback");
|
|
if (normalizedType == "back_panel")
|
|
{
|
|
return new DirectionChoice(box.Center[1] >= modelBox.Center[1] ? "back" : "front", "slide_in", 0.76, "fallback");
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(primaryFace))
|
|
{
|
|
var direction = OppositeFace(primaryFace!);
|
|
if (direction != "unknown")
|
|
{
|
|
return new DirectionChoice(
|
|
direction,
|
|
direction == "top" || direction == "bottom" ? "drop_in" : "slide_in",
|
|
hasHardware ? 0.9 : 0.68,
|
|
"relation");
|
|
}
|
|
}
|
|
|
|
if (normalizedType == "side_panel" || normalizedType == "vertical_divider")
|
|
{
|
|
return new DirectionChoice(box.Center[0] >= modelBox.Center[0] ? "right" : "left", "slide_in", 0.64, "fallback");
|
|
}
|
|
if (normalizedType == "horizontal_divider")
|
|
{
|
|
return new DirectionChoice(box.Center[1] >= modelBox.Center[1] ? "back" : "front", "slide_in", 0.62, "fallback");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static PairRelation? BuildPairRelation(PanelBox boxA, PanelBox boxB, HardwareSignal? hardwareSignal)
|
|
{
|
|
var gapTolerance = hardwareSignal == null ? FaceGapToleranceMm : RelaxedFaceGapToleranceMm;
|
|
FaceCandidate? best = null;
|
|
|
|
for (var axis = 0; axis < 3; axis++)
|
|
{
|
|
var axisA = (axis + 1) % 3;
|
|
var axisB = (axis + 2) % 3;
|
|
var overlapA = Overlap(boxA.Min[axisA], boxA.Max[axisA], boxB.Min[axisA], boxB.Max[axisA]);
|
|
var overlapB = Overlap(boxA.Min[axisB], boxA.Max[axisB], boxB.Min[axisB], boxB.Max[axisB]);
|
|
var ratioA = overlapA / Math.Max(1.0, Math.Min(boxA.Size[axisA], boxB.Size[axisA]));
|
|
var ratioB = overlapB / Math.Max(1.0, Math.Min(boxA.Size[axisB], boxB.Size[axisB]));
|
|
if (ratioA < FaceOverlapRatioMin || ratioB < FaceOverlapRatioMin) continue;
|
|
|
|
var aBeforeB = boxA.Center[axis] <= boxB.Center[axis];
|
|
var gap = aBeforeB
|
|
? Math.Abs(boxA.Max[axis] - boxB.Min[axis])
|
|
: Math.Abs(boxB.Max[axis] - boxA.Min[axis]);
|
|
if (gap > gapTolerance) continue;
|
|
|
|
var faceFromA = aBeforeB ? MaxFaces[axis] : MinFaces[axis];
|
|
var faceFromB = OppositeFace(faceFromA);
|
|
var score = (1.0 - gap / Math.Max(gapTolerance, 0.001)) * 0.45 + ((ratioA + ratioB) / 2.0) * 0.55;
|
|
var candidate = new FaceCandidate(faceFromA, faceFromB, score);
|
|
if (best == null || candidate.Score > best.Value.Score)
|
|
{
|
|
best = candidate;
|
|
}
|
|
}
|
|
|
|
if (best != null)
|
|
{
|
|
var confidence = 0.54 + best.Value.Score * 0.22 + (hardwareSignal?.Confidence ?? 0) * 0.12;
|
|
return new PairRelation(
|
|
boxA.Panel.Id,
|
|
boxB.Panel.Id,
|
|
best.Value.FaceFromA,
|
|
best.Value.FaceFromB,
|
|
hardwareSignal?.ConnectionType ?? "adjacent_face",
|
|
Clamp01(confidence),
|
|
best.Value.Score + (hardwareSignal?.Confidence ?? 0) * 0.2,
|
|
hardwareSignal != null);
|
|
}
|
|
|
|
if (hardwareSignal == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var delta = new[]
|
|
{
|
|
boxB.Center[0] - boxA.Center[0],
|
|
boxB.Center[1] - boxA.Center[1],
|
|
boxB.Center[2] - boxA.Center[2],
|
|
};
|
|
var dominantAxis = 0;
|
|
if (Math.Abs(delta[1]) > Math.Abs(delta[dominantAxis])) dominantAxis = 1;
|
|
if (Math.Abs(delta[2]) > Math.Abs(delta[dominantAxis])) dominantAxis = 2;
|
|
|
|
var derivedFace = delta[dominantAxis] >= 0 ? MaxFaces[dominantAxis] : MinFaces[dominantAxis];
|
|
return new PairRelation(
|
|
boxA.Panel.Id,
|
|
boxB.Panel.Id,
|
|
derivedFace,
|
|
OppositeFace(derivedFace),
|
|
hardwareSignal.ConnectionType,
|
|
Clamp01(0.48 + hardwareSignal.Confidence * 0.24),
|
|
0.5 + hardwareSignal.Confidence * 0.3,
|
|
true);
|
|
}
|
|
|
|
private static Dictionary<string, HardwareSignal> BuildHardwareSignals(IReadOnlyList<HardwareType> hardwareTypes)
|
|
{
|
|
var signals = new Dictionary<string, HardwareSignal>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var hardwareType in hardwareTypes)
|
|
{
|
|
foreach (var instance in hardwareType.Instances ?? new List<HardwareInstance>())
|
|
{
|
|
var primary = instance.PrimaryPanelId?.Trim();
|
|
var secondary = instance.SecondaryPanelId?.Trim();
|
|
if (string.IsNullOrWhiteSpace(primary) ||
|
|
string.IsNullOrWhiteSpace(secondary) ||
|
|
string.Equals(primary, secondary, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var key = PairKey(primary!, secondary!);
|
|
if (!signals.TryGetValue(key, out var signal))
|
|
{
|
|
signal = new HardwareSignal();
|
|
signals[key] = signal;
|
|
}
|
|
|
|
signal.Count++;
|
|
signal.ConfidenceSum += Clamp01(instance.BindingConfidence ?? 0.65);
|
|
if (!string.IsNullOrWhiteSpace(instance.BoundHoleGroupId)) signal.ConnectionType = "hole_group_hardware";
|
|
else if ((instance.BoundHoleIds?.Length ?? 0) > 0) signal.ConnectionType = "hole_hardware";
|
|
else
|
|
{
|
|
var bindingMode = instance.BindingMode;
|
|
if (bindingMode != null &&
|
|
bindingMode.Length > 0 &&
|
|
bindingMode.StartsWith("panel_", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
signal.ConnectionType = "panel_hardware";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return signals;
|
|
}
|
|
|
|
private static PanelAssemblyMateMap BuildMateMap(IEnumerable<PanelRelationView?> related)
|
|
{
|
|
var map = new PanelAssemblyMateMap();
|
|
foreach (var item in related.Where(static entry => entry is { Score: >= 0.42 }).Take(6))
|
|
{
|
|
AddMate(map, item!.SelfFace, item.TargetPanelId);
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
private static void AddMate(PanelAssemblyMateMap map, string face, string targetPanelId)
|
|
{
|
|
List<string>? bucket = face switch
|
|
{
|
|
"left" => map.Left ??= new List<string>(),
|
|
"right" => map.Right ??= new List<string>(),
|
|
"front" => map.Front ??= new List<string>(),
|
|
"back" => map.Back ??= new List<string>(),
|
|
"top" => map.Top ??= new List<string>(),
|
|
"bottom" => map.Bottom ??= new List<string>(),
|
|
_ => null,
|
|
};
|
|
if (bucket == null) return;
|
|
if (!bucket.Contains(targetPanelId, StringComparer.OrdinalIgnoreCase)) bucket.Add(targetPanelId);
|
|
}
|
|
|
|
private static double CalculateTravel(PanelBox panelBox, PanelBox referenceBox, string direction)
|
|
{
|
|
double travel = direction switch
|
|
{
|
|
"left" => panelBox.Max[0] - referenceBox.Min[0] + OutsideMarginMm,
|
|
"right" => referenceBox.Max[0] - panelBox.Min[0] + OutsideMarginMm,
|
|
"front" => panelBox.Max[1] - referenceBox.Min[1] + OutsideMarginMm,
|
|
"back" => referenceBox.Max[1] - panelBox.Min[1] + OutsideMarginMm,
|
|
"bottom" => panelBox.Max[2] - referenceBox.Min[2] + OutsideMarginMm,
|
|
"top" => referenceBox.Max[2] - panelBox.Min[2] + OutsideMarginMm,
|
|
_ => 0,
|
|
};
|
|
return Math.Round(Math.Max(MinPreassembleDistanceMm, travel), 2);
|
|
}
|
|
|
|
private static double[] AxisVector(string direction)
|
|
{
|
|
return direction switch
|
|
{
|
|
"left" => new[] { -1.0, 0.0, 0.0 },
|
|
"right" => new[] { 1.0, 0.0, 0.0 },
|
|
"front" => new[] { 0.0, -1.0, 0.0 },
|
|
"back" => new[] { 0.0, 1.0, 0.0 },
|
|
"bottom" => new[] { 0.0, 0.0, -1.0 },
|
|
"top" => new[] { 0.0, 0.0, 1.0 },
|
|
_ => new[] { 0.0, 0.0, 0.0 },
|
|
};
|
|
}
|
|
|
|
private static double[]? OffsetVector(string direction, double travel)
|
|
{
|
|
var axis = AxisVector(direction);
|
|
if (axis[0] == 0 && axis[1] == 0 && axis[2] == 0) return null;
|
|
return new[]
|
|
{
|
|
Math.Round(axis[0] * travel, 2),
|
|
Math.Round(axis[1] * travel, 2),
|
|
Math.Round(axis[2] * travel, 2),
|
|
};
|
|
}
|
|
|
|
private static bool IsLikelyBoard(PanelInfo panel)
|
|
{
|
|
if (panel.Size == null || panel.Size.Length < 3) return false;
|
|
var thickness = Math.Max(panel.Size[2], 1.0);
|
|
return thickness <= 50 && panel.Size[0] / thickness >= 2.5 && panel.Size[1] / thickness >= 1.5;
|
|
}
|
|
|
|
private static PanelBox BuildBox(PanelInfo panel)
|
|
{
|
|
if (panel.BoundingBox != null &&
|
|
panel.BoundingBox.Min?.Length == 3 &&
|
|
panel.BoundingBox.Max?.Length == 3)
|
|
{
|
|
return new PanelBox(panel, panel.BoundingBox.Min, panel.BoundingBox.Max);
|
|
}
|
|
|
|
var center = panel.Position?.Length == 3 ? panel.Position : new[] { 0.0, 0.0, 0.0 };
|
|
var size = panel.Size?.Length == 3 ? panel.Size : new[] { 0.0, 0.0, 0.0 };
|
|
var normal = panel.Normal?.Length == 3 ? panel.Normal : new[] { 0.0, 0.0, 1.0 };
|
|
var thicknessAxis = Math.Abs(normal[0]) >= Math.Abs(normal[1]) && Math.Abs(normal[0]) >= Math.Abs(normal[2]) ? 0 :
|
|
Math.Abs(normal[1]) >= Math.Abs(normal[2]) ? 1 : 2;
|
|
var axes = Enumerable.Range(0, 3).Where(axis => axis != thicknessAxis).ToArray();
|
|
var axisSizes = new double[3];
|
|
axisSizes[thicknessAxis] = size[2];
|
|
axisSizes[axes[0]] = size[0];
|
|
axisSizes[axes[1]] = size[1];
|
|
|
|
return new PanelBox(
|
|
panel,
|
|
new[]
|
|
{
|
|
center[0] - axisSizes[0] / 2.0,
|
|
center[1] - axisSizes[1] / 2.0,
|
|
center[2] - axisSizes[2] / 2.0,
|
|
},
|
|
new[]
|
|
{
|
|
center[0] + axisSizes[0] / 2.0,
|
|
center[1] + axisSizes[1] / 2.0,
|
|
center[2] + axisSizes[2] / 2.0,
|
|
});
|
|
}
|
|
|
|
private static PanelBox MergeBoxes(IEnumerable<PanelBox> boxes)
|
|
{
|
|
var min = new[] { double.MaxValue, double.MaxValue, double.MaxValue };
|
|
var max = new[] { double.MinValue, double.MinValue, double.MinValue };
|
|
foreach (var box in boxes)
|
|
{
|
|
for (var axis = 0; axis < 3; axis++)
|
|
{
|
|
min[axis] = Math.Min(min[axis], box.Min[axis]);
|
|
max[axis] = Math.Max(max[axis], box.Max[axis]);
|
|
}
|
|
}
|
|
|
|
return new PanelBox(new PanelInfo { Id = "__merged__" }, min, max);
|
|
}
|
|
|
|
private static string PairKey(string panelIdA, string panelIdB)
|
|
{
|
|
return string.Compare(panelIdA, panelIdB, StringComparison.OrdinalIgnoreCase) <= 0
|
|
? $"{panelIdA}::{panelIdB}"
|
|
: $"{panelIdB}::{panelIdA}";
|
|
}
|
|
|
|
private static string OppositeFace(string face)
|
|
{
|
|
return face switch
|
|
{
|
|
"left" => "right",
|
|
"right" => "left",
|
|
"front" => "back",
|
|
"back" => "front",
|
|
"top" => "bottom",
|
|
"bottom" => "top",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
|
|
private static double Overlap(double minA, double maxA, double minB, double maxB)
|
|
{
|
|
return Math.Max(0.0, Math.Min(maxA, maxB) - Math.Max(minA, minB));
|
|
}
|
|
|
|
private static double Clamp01(double value)
|
|
{
|
|
if (value < 0) return 0;
|
|
if (value > 1) return 1;
|
|
return value;
|
|
}
|
|
|
|
private readonly struct FaceCandidate
|
|
{
|
|
public FaceCandidate(string faceFromA, string faceFromB, double score)
|
|
{
|
|
FaceFromA = faceFromA;
|
|
FaceFromB = faceFromB;
|
|
Score = score;
|
|
}
|
|
|
|
public string FaceFromA { get; }
|
|
public string FaceFromB { get; }
|
|
public double Score { get; }
|
|
}
|
|
|
|
private sealed class PairRelation
|
|
{
|
|
public PairRelation(string panelAId, string panelBId, string faceFromA, string faceFromB, string connectionType, double confidence, double score, bool hasHardware)
|
|
{
|
|
PanelAId = panelAId;
|
|
PanelBId = panelBId;
|
|
FaceFromA = faceFromA;
|
|
FaceFromB = faceFromB;
|
|
ConnectionType = connectionType;
|
|
Confidence = confidence;
|
|
Score = score;
|
|
HasHardware = hasHardware;
|
|
}
|
|
|
|
public string PanelAId { get; }
|
|
public string PanelBId { get; }
|
|
public string FaceFromA { get; }
|
|
public string FaceFromB { get; }
|
|
public string ConnectionType { get; }
|
|
public double Confidence { get; }
|
|
public double Score { get; }
|
|
public bool HasHardware { get; }
|
|
}
|
|
|
|
private sealed class PanelRelationView
|
|
{
|
|
public PanelRelationView(string targetPanelId, string selfFace, string connectionType, double confidence, double score, bool hasHardware)
|
|
{
|
|
TargetPanelId = targetPanelId;
|
|
SelfFace = selfFace;
|
|
ConnectionType = connectionType;
|
|
Confidence = confidence;
|
|
Score = score;
|
|
HasHardware = hasHardware;
|
|
}
|
|
|
|
public string TargetPanelId { get; }
|
|
public string SelfFace { get; }
|
|
public string ConnectionType { get; }
|
|
public double Confidence { get; }
|
|
public double Score { get; }
|
|
public bool HasHardware { get; }
|
|
}
|
|
|
|
private sealed class PanelBox
|
|
{
|
|
public PanelBox(PanelInfo panel, IReadOnlyList<double> min, IReadOnlyList<double> max)
|
|
{
|
|
Panel = panel;
|
|
Min = min.ToArray();
|
|
Max = max.ToArray();
|
|
Size = new[] { Math.Abs(Max[0] - Min[0]), Math.Abs(Max[1] - Min[1]), Math.Abs(Max[2] - Min[2]) };
|
|
Center = new[] { (Min[0] + Max[0]) / 2.0, (Min[1] + Max[1]) / 2.0, (Min[2] + Max[2]) / 2.0 };
|
|
}
|
|
|
|
public PanelInfo Panel { get; }
|
|
public double[] Min { get; }
|
|
public double[] Max { get; }
|
|
public double[] Size { get; }
|
|
public double[] Center { get; }
|
|
}
|
|
|
|
private sealed class DirectionChoice
|
|
{
|
|
public DirectionChoice(string direction, string mountMode, double confidence, string source)
|
|
{
|
|
Direction = direction;
|
|
MountMode = mountMode;
|
|
Confidence = confidence;
|
|
Source = source;
|
|
}
|
|
|
|
public string Direction { get; }
|
|
public string MountMode { get; }
|
|
public double Confidence { get; }
|
|
public string Source { get; }
|
|
}
|
|
|
|
private sealed class HardwareSignal
|
|
{
|
|
public int Count { get; set; }
|
|
public double ConfidenceSum { get; set; }
|
|
public string ConnectionType { get; set; } = "panel_hardware";
|
|
public double Confidence => Count <= 0 ? 0 : ConfidenceSum / Count;
|
|
}
|
|
}
|