627 lines
21 KiB
C#
627 lines
21 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text.RegularExpressions;
|
||
using Autodesk.AutoCAD.DatabaseServices;
|
||
using Autodesk.AutoCAD.Geometry;
|
||
|
||
namespace CadParamPluging.Cad
|
||
{
|
||
public sealed class ModelSheetCandidate
|
||
{
|
||
public string Name { get; set; }
|
||
public List<string> DeliveryStatuses { get; set; }
|
||
public List<string> ProcessMethods { get; set; }
|
||
public List<string> StructuralFeatures { get; set; }
|
||
public List<string> SpecialConditions { get; set; }
|
||
public Extents3d Window { get; set; }
|
||
|
||
public ModelSheetCandidate()
|
||
{
|
||
DeliveryStatuses = new List<string>();
|
||
ProcessMethods = new List<string>();
|
||
StructuralFeatures = new List<string>();
|
||
SpecialConditions = new List<string>();
|
||
}
|
||
}
|
||
|
||
public static class TemplateModelSheetExtractor
|
||
{
|
||
private static readonly Regex FieldRegex = new Regex(
|
||
@"^\s*(交付状态|工艺方法|结构特征|特殊条件)\s*[::]\s*(.+?)\s*$",
|
||
RegexOptions.Compiled);
|
||
|
||
private static readonly Regex MTextFormatRegex = new Regex(@"\\[A-Za-z]+[^;]*;", RegexOptions.Compiled);
|
||
|
||
private sealed class TextMatch
|
||
{
|
||
public string Field;
|
||
public string Value;
|
||
public Point3d Pos;
|
||
}
|
||
|
||
private sealed class VLine
|
||
{
|
||
public double X;
|
||
public double YMin;
|
||
public double YMax;
|
||
}
|
||
|
||
private sealed class HLine
|
||
{
|
||
public double Y;
|
||
public double XMin;
|
||
public double XMax;
|
||
}
|
||
|
||
public static List<ModelSheetCandidate> ExtractCandidates(string templatePath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(templatePath))
|
||
{
|
||
throw new ArgumentNullException(nameof(templatePath));
|
||
}
|
||
|
||
var matches = new List<TextMatch>();
|
||
var vLines = new List<VLine>();
|
||
var hLines = new List<HLine>();
|
||
|
||
using (var db = new Database(false, true))
|
||
{
|
||
ReadTemplateDatabase(db, templatePath);
|
||
db.CloseInput(true);
|
||
|
||
using (var tr = db.TransactionManager.StartTransaction())
|
||
{
|
||
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
|
||
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead);
|
||
|
||
var visitedBlocks = new HashSet<ObjectId>();
|
||
foreach (ObjectId id in ms)
|
||
{
|
||
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
|
||
if (ent != null)
|
||
{
|
||
CollectEntity(ent, Matrix3d.Identity, tr, visitedBlocks, matches, vLines, hLines);
|
||
}
|
||
}
|
||
|
||
tr.Commit();
|
||
}
|
||
}
|
||
|
||
return BuildCandidates(matches, vLines, hLines);
|
||
}
|
||
|
||
private static void ReadTemplateDatabase(Database db, string templatePath)
|
||
{
|
||
try
|
||
{
|
||
db.ReadDwgFile(templatePath, FileShare.ReadWrite, true, string.Empty);
|
||
}
|
||
catch (MissingMethodException)
|
||
{
|
||
db.ReadDwgFile(templatePath, FileOpenMode.OpenForReadAndAllShare, true, string.Empty);
|
||
}
|
||
}
|
||
|
||
private static void TryCollectLine(Line ln, List<VLine> vLines, List<HLine> hLines)
|
||
{
|
||
var a = ln.StartPoint;
|
||
var b = ln.EndPoint;
|
||
var dx = Math.Abs(a.X - b.X);
|
||
var dy = Math.Abs(a.Y - b.Y);
|
||
const double tol = 1e-3;
|
||
|
||
if (dx <= tol && dy > tol)
|
||
{
|
||
vLines.Add(new VLine
|
||
{
|
||
X = (a.X + b.X) / 2.0,
|
||
YMin = Math.Min(a.Y, b.Y),
|
||
YMax = Math.Max(a.Y, b.Y)
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (dy <= tol && dx > tol)
|
||
{
|
||
hLines.Add(new HLine
|
||
{
|
||
Y = (a.Y + b.Y) / 2.0,
|
||
XMin = Math.Min(a.X, b.X),
|
||
XMax = Math.Max(a.X, b.X)
|
||
});
|
||
}
|
||
}
|
||
|
||
private static void CollectEntity(Entity ent, Matrix3d transform, Transaction tr, HashSet<ObjectId> visitedBlocks, List<TextMatch> matches, List<VLine> vLines, List<HLine> hLines)
|
||
{
|
||
if (ent is Line ln)
|
||
{
|
||
var a = ln.StartPoint.TransformBy(transform);
|
||
var b = ln.EndPoint.TransformBy(transform);
|
||
TryCollectLinePoints(a, b, vLines, hLines);
|
||
return;
|
||
}
|
||
|
||
if (ent is DBText t)
|
||
{
|
||
var p = t.Position.TransformBy(transform);
|
||
TryCollectText(t.TextString, p, matches);
|
||
return;
|
||
}
|
||
|
||
if (ent is MText mt)
|
||
{
|
||
var p = mt.Location.TransformBy(transform);
|
||
var plain = SimplifyMText(mt.Contents);
|
||
TryCollectText(plain, p, matches);
|
||
return;
|
||
}
|
||
|
||
if (ent is BlockReference br)
|
||
{
|
||
// 属性
|
||
foreach (ObjectId attId in br.AttributeCollection)
|
||
{
|
||
var att = tr.GetObject(attId, OpenMode.ForRead) as AttributeReference;
|
||
if (att == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var p = att.Position.TransformBy(transform);
|
||
TryCollectText(att.TextString, p, matches);
|
||
}
|
||
|
||
// 块定义内部
|
||
var blockId = br.BlockTableRecord;
|
||
if (blockId.IsNull || visitedBlocks.Contains(blockId))
|
||
{
|
||
return;
|
||
}
|
||
visitedBlocks.Add(blockId);
|
||
|
||
var btr = tr.GetObject(blockId, OpenMode.ForRead) as BlockTableRecord;
|
||
if (btr == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var nextTransform = br.BlockTransform * transform;
|
||
foreach (ObjectId childId in btr)
|
||
{
|
||
var child = tr.GetObject(childId, OpenMode.ForRead) as Entity;
|
||
if (child != null)
|
||
{
|
||
CollectEntity(child, nextTransform, tr, visitedBlocks, matches, vLines, hLines);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void TryCollectLinePoints(Point3d a, Point3d b, List<VLine> vLines, List<HLine> hLines)
|
||
{
|
||
var dx = Math.Abs(a.X - b.X);
|
||
var dy = Math.Abs(a.Y - b.Y);
|
||
const double tol = 1e-3;
|
||
|
||
if (dx <= tol && dy > tol)
|
||
{
|
||
vLines.Add(new VLine
|
||
{
|
||
X = (a.X + b.X) / 2.0,
|
||
YMin = Math.Min(a.Y, b.Y),
|
||
YMax = Math.Max(a.Y, b.Y)
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (dy <= tol && dx > tol)
|
||
{
|
||
hLines.Add(new HLine
|
||
{
|
||
Y = (a.Y + b.Y) / 2.0,
|
||
XMin = Math.Min(a.X, b.X),
|
||
XMax = Math.Max(a.X, b.X)
|
||
});
|
||
}
|
||
}
|
||
|
||
private static void TryCollectText(string raw, Point3d pos, List<TextMatch> matches)
|
||
{
|
||
if (matches == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var lines = SplitLines(raw).ToList();
|
||
if (lines.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var hasAnyField = false;
|
||
foreach (var line in lines)
|
||
{
|
||
if (TryParseFieldLine(line, out var field, out var value))
|
||
{
|
||
hasAnyField = true;
|
||
matches.Add(new TextMatch { Field = field, Value = value, Pos = pos });
|
||
}
|
||
}
|
||
|
||
if (!hasAnyField)
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (var line in lines)
|
||
{
|
||
if (IsSpecialConditionValueLine(line))
|
||
{
|
||
var v = NormalizeValue(line);
|
||
if (v.Length == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
matches.Add(new TextMatch { Field = "特殊条件", Value = v, Pos = pos });
|
||
}
|
||
}
|
||
}
|
||
|
||
private static bool TryParseFieldLine(string line, out string field, out string value)
|
||
{
|
||
field = null;
|
||
value = null;
|
||
|
||
if (string.IsNullOrWhiteSpace(line))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var m = FieldRegex.Match(line);
|
||
if (!m.Success)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
field = m.Groups[1].Value;
|
||
value = NormalizeValue(m.Groups[2].Value);
|
||
return !string.IsNullOrWhiteSpace(field) && !string.IsNullOrWhiteSpace(value);
|
||
}
|
||
|
||
private static bool IsSpecialConditionValueLine(string line)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(line))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (FieldRegex.IsMatch(line))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return line.IndexOf(':') < 0 && line.IndexOf(':') < 0;
|
||
}
|
||
|
||
private static IEnumerable<string> SplitLines(string text)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(text))
|
||
{
|
||
return Enumerable.Empty<string>();
|
||
}
|
||
|
||
return (text ?? string.Empty)
|
||
.Replace("\r\n", "\n")
|
||
.Replace("\r", "\n")
|
||
.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||
.Select(s => (s ?? string.Empty).Trim())
|
||
.Where(s => s.Length > 0);
|
||
}
|
||
|
||
private static string SimplifyMText(string contents)
|
||
{
|
||
if (string.IsNullOrEmpty(contents))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var s = contents
|
||
.Replace("\\P", "\n")
|
||
.Replace("\\p", "\n")
|
||
.Replace("\\~", " ");
|
||
s = s.Replace("{", string.Empty).Replace("}", string.Empty);
|
||
s = MTextFormatRegex.Replace(s, string.Empty);
|
||
return s;
|
||
}
|
||
|
||
private static string NormalizeValue(string s)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(s))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var trimmed = s.Trim();
|
||
var chars = trimmed.Where(c => !char.IsWhiteSpace(c)).ToArray();
|
||
return new string(chars);
|
||
}
|
||
|
||
private static List<ModelSheetCandidate> BuildCandidates(List<TextMatch> matches, List<VLine> vLines, List<HLine> hLines)
|
||
{
|
||
if (matches == null || matches.Count == 0)
|
||
{
|
||
return new List<ModelSheetCandidate>();
|
||
}
|
||
|
||
var clusters = Cluster(matches);
|
||
var result = new List<ModelSheetCandidate>();
|
||
var idx = 1;
|
||
|
||
foreach (var cluster in clusters)
|
||
{
|
||
var delivery = cluster.Where(m => m.Field == "交付状态").Select(m => m.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||
var process = cluster.Where(m => m.Field == "工艺方法").Select(m => m.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||
var structural = cluster.Where(m => m.Field == "结构特征").Select(m => m.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||
var special = cluster.Where(m => m.Field == "特殊条件").Select(m => m.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||
|
||
if (delivery.Count == 0 || process.Count == 0 || structural.Count == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var textExt = ExtentsFromPoints(cluster.Select(m => m.Pos));
|
||
|
||
// 优先使用“最近的四边线”构建矩形,更适配你这种 4 条直线外框。
|
||
var rectExt = FindRectangleByNearestLines(textExt, vLines, hLines) ?? FindNearestRectangleExtents(textExt, vLines, hLines);
|
||
var window = rectExt.HasValue ? Union(rectExt.Value, Expand(textExt, 200.0)) : Expand(textExt, 2000.0);
|
||
|
||
result.Add(new ModelSheetCandidate
|
||
{
|
||
Name = $"Model#{idx}",
|
||
DeliveryStatuses = delivery,
|
||
ProcessMethods = process,
|
||
StructuralFeatures = structural,
|
||
SpecialConditions = special,
|
||
Window = window
|
||
});
|
||
|
||
idx++;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private static List<List<TextMatch>> Cluster(List<TextMatch> matches)
|
||
{
|
||
var n = matches.Count;
|
||
var parent = Enumerable.Range(0, n).ToArray();
|
||
|
||
int Find(int x)
|
||
{
|
||
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
|
||
return x;
|
||
}
|
||
|
||
void UnionIdx(int a, int b)
|
||
{
|
||
var ra = Find(a);
|
||
var rb = Find(b);
|
||
if (ra != rb) parent[rb] = ra;
|
||
}
|
||
|
||
var threshold = EstimateThreshold(matches);
|
||
var threshold2 = threshold * threshold;
|
||
|
||
for (var i = 0; i < n; i++)
|
||
{
|
||
for (var j = i + 1; j < n; j++)
|
||
{
|
||
var dx = matches[i].Pos.X - matches[j].Pos.X;
|
||
var dy = matches[i].Pos.Y - matches[j].Pos.Y;
|
||
var d2 = dx * dx + dy * dy;
|
||
if (d2 <= threshold2)
|
||
{
|
||
UnionIdx(i, j);
|
||
}
|
||
}
|
||
}
|
||
|
||
var groups = new Dictionary<int, List<TextMatch>>();
|
||
for (var i = 0; i < n; i++)
|
||
{
|
||
var r = Find(i);
|
||
if (!groups.TryGetValue(r, out var list))
|
||
{
|
||
list = new List<TextMatch>();
|
||
groups[r] = list;
|
||
}
|
||
list.Add(matches[i]);
|
||
}
|
||
|
||
return groups.Values.ToList();
|
||
}
|
||
|
||
private static double EstimateThreshold(List<TextMatch> matches)
|
||
{
|
||
if (matches.Count < 2)
|
||
{
|
||
return 1000.0;
|
||
}
|
||
|
||
var dists = new List<double>();
|
||
for (var i = 0; i < matches.Count; i++)
|
||
{
|
||
var best = double.MaxValue;
|
||
for (var j = 0; j < matches.Count; j++)
|
||
{
|
||
if (i == j) continue;
|
||
var dx = matches[i].Pos.X - matches[j].Pos.X;
|
||
var dy = matches[i].Pos.Y - matches[j].Pos.Y;
|
||
var d = Math.Sqrt(dx * dx + dy * dy);
|
||
if (d < best) best = d;
|
||
}
|
||
if (best < double.MaxValue) dists.Add(best);
|
||
}
|
||
|
||
dists.Sort();
|
||
var median = dists[dists.Count / 2];
|
||
if (median <= 0) median = 500.0;
|
||
var thr = median * 4.0;
|
||
if (thr < 500.0) thr = 500.0;
|
||
if (thr > 20000.0) thr = 20000.0;
|
||
return thr;
|
||
}
|
||
|
||
private static Extents3d ExtentsFromPoints(IEnumerable<Point3d> pts)
|
||
{
|
||
var arr = (pts ?? Enumerable.Empty<Point3d>()).ToArray();
|
||
if (arr.Length == 0)
|
||
{
|
||
return new Extents3d(new Point3d(0, 0, 0), new Point3d(0, 0, 0));
|
||
}
|
||
|
||
var minX = arr.Min(p => p.X);
|
||
var minY = arr.Min(p => p.Y);
|
||
var maxX = arr.Max(p => p.X);
|
||
var maxY = arr.Max(p => p.Y);
|
||
return new Extents3d(new Point3d(minX, minY, 0), new Point3d(maxX, maxY, 0));
|
||
}
|
||
|
||
private static Extents3d Expand(Extents3d e, double margin)
|
||
{
|
||
return new Extents3d(
|
||
new Point3d(e.MinPoint.X - margin, e.MinPoint.Y - margin, 0),
|
||
new Point3d(e.MaxPoint.X + margin, e.MaxPoint.Y + margin, 0));
|
||
}
|
||
|
||
private static Extents3d Union(Extents3d a, Extents3d b)
|
||
{
|
||
return new Extents3d(
|
||
new Point3d(Math.Min(a.MinPoint.X, b.MinPoint.X), Math.Min(a.MinPoint.Y, b.MinPoint.Y), 0),
|
||
new Point3d(Math.Max(a.MaxPoint.X, b.MaxPoint.X), Math.Max(a.MaxPoint.Y, b.MaxPoint.Y), 0));
|
||
}
|
||
|
||
private static Extents3d? FindNearestRectangleExtents(Extents3d anchor, List<VLine> vLines, List<HLine> hLines)
|
||
{
|
||
if (vLines == null || hLines == null || vLines.Count < 2 || hLines.Count < 2)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var cx = (anchor.MinPoint.X + anchor.MaxPoint.X) / 2.0;
|
||
var cy = (anchor.MinPoint.Y + anchor.MaxPoint.Y) / 2.0;
|
||
|
||
var vCand = vLines.OrderBy(l => Math.Abs(l.X - cx)).Take(30).ToList();
|
||
var hCand = hLines.OrderBy(l => Math.Abs(l.Y - cy)).Take(30).ToList();
|
||
|
||
const double tol = 5.0;
|
||
Extents3d? best = null;
|
||
double bestScore = double.MaxValue;
|
||
|
||
for (var i = 0; i < vCand.Count; i++)
|
||
{
|
||
for (var j = i + 1; j < vCand.Count; j++)
|
||
{
|
||
var left = vCand[i].X < vCand[j].X ? vCand[i] : vCand[j];
|
||
var right = vCand[i].X < vCand[j].X ? vCand[j] : vCand[i];
|
||
var width = right.X - left.X;
|
||
if (width < 1000) continue;
|
||
|
||
for (var a = 0; a < hCand.Count; a++)
|
||
{
|
||
for (var b = a + 1; b < hCand.Count; b++)
|
||
{
|
||
var bottom = hCand[a].Y < hCand[b].Y ? hCand[a] : hCand[b];
|
||
var top = hCand[a].Y < hCand[b].Y ? hCand[b] : hCand[a];
|
||
var height = top.Y - bottom.Y;
|
||
if (height < 1000) continue;
|
||
|
||
// span check
|
||
if (left.YMin > bottom.Y + tol || left.YMax < top.Y - tol) continue;
|
||
if (right.YMin > bottom.Y + tol || right.YMax < top.Y - tol) continue;
|
||
if (bottom.XMin > left.X + tol || bottom.XMax < right.X - tol) continue;
|
||
if (top.XMin > left.X + tol || top.XMax < right.X - tol) continue;
|
||
|
||
var rect = new Extents3d(
|
||
new Point3d(left.X, bottom.Y, 0),
|
||
new Point3d(right.X, top.Y, 0));
|
||
|
||
var rcx = (left.X + right.X) / 2.0;
|
||
var rcy = (bottom.Y + top.Y) / 2.0;
|
||
var dist = Math.Sqrt((rcx - cx) * (rcx - cx) + (rcy - cy) * (rcy - cy));
|
||
var area = width * height;
|
||
var score = dist + area * 1e-9; // distance first, area tie-break
|
||
|
||
if (score < bestScore)
|
||
{
|
||
bestScore = score;
|
||
best = rect;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return best;
|
||
}
|
||
|
||
private static Extents3d? FindRectangleByNearestLines(Extents3d anchor, List<VLine> vLines, List<HLine> hLines)
|
||
{
|
||
if (vLines == null || hLines == null || vLines.Count == 0 || hLines.Count == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var cx = (anchor.MinPoint.X + anchor.MaxPoint.X) / 2.0;
|
||
var cy = (anchor.MinPoint.Y + anchor.MaxPoint.Y) / 2.0;
|
||
const double tol = 50.0;
|
||
|
||
var left = vLines
|
||
.Where(l => l.X < cx - tol && l.YMin - tol <= cy && l.YMax + tol >= cy)
|
||
.OrderByDescending(l => l.X)
|
||
.FirstOrDefault();
|
||
|
||
var right = vLines
|
||
.Where(l => l.X > cx + tol && l.YMin - tol <= cy && l.YMax + tol >= cy)
|
||
.OrderBy(l => l.X)
|
||
.FirstOrDefault();
|
||
|
||
var bottom = hLines
|
||
.Where(l => l.Y < cy - tol && l.XMin - tol <= cx && l.XMax + tol >= cx)
|
||
.OrderByDescending(l => l.Y)
|
||
.FirstOrDefault();
|
||
|
||
var top = hLines
|
||
.Where(l => l.Y > cy + tol && l.XMin - tol <= cx && l.XMax + tol >= cx)
|
||
.OrderBy(l => l.Y)
|
||
.FirstOrDefault();
|
||
|
||
if (left == null || right == null || bottom == null || top == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var width = right.X - left.X;
|
||
var height = top.Y - bottom.Y;
|
||
if (width < 1000 || height < 1000)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var rect = new Extents3d(new Point3d(left.X, bottom.Y, 0), new Point3d(right.X, top.Y, 0));
|
||
if (anchor.MinPoint.X < rect.MinPoint.X - tol || anchor.MaxPoint.X > rect.MaxPoint.X + tol ||
|
||
anchor.MinPoint.Y < rect.MinPoint.Y - tol || anchor.MaxPoint.Y > rect.MaxPoint.Y + tol)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return rect;
|
||
}
|
||
}
|
||
}
|