CadParamPluging/Cad/TemplateModelSheetExtractor.cs

627 lines
21 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}