using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Geometry; using Autodesk.AutoCAD.Runtime; //using Newtonsoft.Json; namespace BoreholeTool { public class BoreholeHybridCommand { // ================= 参数配置 ================= private const string FrameLayerName = "0"; // 行对齐容差 (用于地层数字提取,必须严格) private const double RowAlignmentTolerance = 4.0; // 表头提取容差 (普通短标签) private const double HeaderStrictDx = 50.0; private const double HeaderStrictDy = 5.0; private class TextEntity { public string Text { get; set; } public Point3d Position { get; set; } public double X => Position.X; public double Y => Position.Y; public ObjectId Id { get; set; } } private class FrameInfo { public Extents3d Bounds { get; set; } public string HoleNumber { get; set; } } [CommandMethod("ExportBoreholeHybrid")] public void ExportBoreholeHybrid() { Document doc = Application.DocumentManager.MdiActiveDocument; Editor ed = doc.Editor; PromptSelectionOptions opts = new PromptSelectionOptions(); opts.MessageForAdding = "\n请框选所有表格区域: "; SelectionSet ss = ed.GetSelection(opts).Value; if (ss == null || ss.Count == 0) return; List allTexts = new List(); List rawFrames = new List(); // 1. 读取数据 using (Transaction tr = doc.TransactionManager.StartTransaction()) { foreach (SelectedObject sobj in ss) { Entity ent = tr.GetObject(sobj.ObjectId, OpenMode.ForRead) as Entity; if ((ent is Polyline || ent is Polyline2d) && ent.Layer == FrameLayerName) { if (ent.Bounds.HasValue) rawFrames.Add(ent.Bounds.Value); } else if (ent is DBText dbText) { allTexts.Add(new TextEntity { Text = dbText.TextString.Trim(), Position = dbText.Position, Id = dbText.ObjectId }); } else if (ent is MText mText) { allTexts.Add(new TextEntity { Text = mText.Text.Trim(), Position = mText.Location, Id = mText.ObjectId }); } } tr.Commit(); } // 2. 过滤嵌套图框 List uniqueFrames = FilterNestedFrames(rawFrames); if (uniqueFrames.Count == 0) { ed.WriteMessage("\n未找到图框。"); return; } ed.WriteMessage($"\n识别到 {uniqueFrames.Count} 个图框,正在进行孔号分组..."); // 3. 预扫描孔号并分组 var framesWithHole = PreScanHoleNumbers(uniqueFrames, allTexts); var groupedFrames = framesWithHole .Where(f => !string.IsNullOrEmpty(f.HoleNumber)) .GroupBy(f => f.HoleNumber) .ToList(); List finalLogs = new List(); // 4. 按孔号处理 foreach (var group in groupedFrames) { string currentHoleNo = group.Key; // A. 确定主表 (Master Frame) // 通常取列表中的第一个,或者Y坐标最高的那个(假设是第一页) // 我们这里简单取 List 的第一个,因为 PreScan 顺序通常对应选择顺序 var masterFrameInfo = group.First(); Extents3d masterBounds = masterFrameInfo.Bounds; // B. 获取主表的文字 (用于提取 Header) var masterTexts = allTexts .Where(t => t.X >= masterBounds.MinPoint.X && t.X <= masterBounds.MaxPoint.X && t.Y >= masterBounds.MinPoint.Y && t.Y <= masterBounds.MaxPoint.Y) .ToList(); // C. 获取该孔所有表的文字 (用于提取 Layers) var allPageTexts = new List(); foreach (var f in group) { var pageTexts = allTexts .Where(t => t.X >= f.Bounds.MinPoint.X && t.X <= f.Bounds.MaxPoint.X && t.Y >= f.Bounds.MinPoint.Y && t.Y <= f.Bounds.MaxPoint.Y) .ToList(); allPageTexts.AddRange(pageTexts); } try { BoreholeLog log = new BoreholeLog(); // Step 1: 仅从主表提取 Header (避免空白表头干扰) log.Header = ExtractHeaderOnly(masterTexts); // 确保孔号存在 if (string.IsNullOrEmpty(log.Header.HoleNumber)) log.Header.HoleNumber = currentHoleNo; // Step 2: 从所有合并的文字中提取 Layers (解决跨页描述) log.Layers = ExtractLayersOnly(allPageTexts); // 排序 log.Layers = log.Layers.OrderBy(l => l.BottomDepth).ToList(); finalLogs.Add(log); } catch (Autodesk.AutoCAD.Runtime.Exception ex) { ed.WriteMessage($"\n解析孔 {currentHoleNo} 失败: {ex.Message}"); } } // 输出 #region Json //string json = JsonConvert.SerializeObject(finalLogs, Formatting.Indented); //string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "BoreholeHybrid.json"); //File.WriteAllText(path, json); //ed.WriteMessage($"\n处理完成!共 {finalLogs.Count} 个钻孔。文件: {path}"); #endregion } // ---------------------------------------------------------------- // 模块 1: Header 提取 (仅针对单张表) // ---------------------------------------------------------------- private HeaderInfo ExtractHeaderOnly(List texts) { HeaderInfo h = new HeaderInfo(); foreach (var t in texts) { string txt = t.Text.Replace(" ", ""); if (txt.Contains("勘察单位")) h.SurveyUnit = GetValueStrict(texts, t); if (txt.Contains("工程名称")) h.ProjectName = GetValueStrict(texts, t); if (txt.Contains("里程")) h.Mileage = GetValueStrict(texts, t); if (txt.Contains("设计结构底板标高")) h.DesignElevation = GetValueStrict(texts, t); if (txt.Contains("钻孔编号")) h.HoleNumber = GetValueStrict(texts, t); if (txt.Contains("钻孔类别")) h.HoleType = GetValueStrict(texts, t); if (txt.Contains("X=")) h.CoordinateX = ExtractDouble(txt); if (txt.Contains("Y=")) h.CoordinateY = ExtractDouble(txt); if (txt.Contains("孔口标高")) h.HoleElevation = GetValueStrict(texts, t); if (txt.Contains("孔口直径")) h.Diameter = GetValueStrict(texts, t); if (txt.Contains("开工日期")) h.StartDate = GetValueStrict(texts, t); if (txt.Contains("竣工日期")) h.EndDate = GetValueStrict(texts, t); if (txt.Contains("稳定水位")) h.StableWaterLevel = GetValueStrict(texts, t); if (txt.Contains("初见水位")) h.InitialWaterLevel = GetValueStrict(texts, t); } return h; } // ---------------------------------------------------------------- // 模块 2: Layers 提取 (针对合并后的全量文字) // ---------------------------------------------------------------- private List ExtractLayersOnly(List texts) { List layers = new List(); // 1. 找到所有层号 (Anchors) - 包含所有页面的 var layerAnchors = texts .Where(t => Regex.IsMatch(t.Text, @"^<[\d-]+>$")) .OrderByDescending(t => t.Y) // 注意:跨页合并后,Y坐标是绝对坐标,排序可能不完全代表深度顺序,但没关系 .ToList(); if (layerAnchors.Count == 0) { // Fallback: 纯数字编号 var numericAnchors = texts.Where(t => Regex.IsMatch(t.Text, @"^\d+(-\d+)?$")).ToList(); if (numericAnchors.Count > 0) { double minX = numericAnchors.Min(t => t.X); layerAnchors = numericAnchors .Where(t => t.X < minX + 200.0) .OrderByDescending(t => t.Y) .ToList(); } } foreach (var anchor in layerAnchors) { LayerData layer = new LayerData { LayerNo = anchor.Text }; // A. 提取数值 (标高/深度/厚度) // 必须严格限制在 anchor 的同一行 (Strict Y) var lineNumbers = texts .Where(t => t.Id != anchor.Id) .Where(t => t.X > anchor.X) .Where(t => Math.Abs(t.Y - anchor.Y) < RowAlignmentTolerance) // 严格同行 .Where(t => IsPureNumber(t.Text)) .OrderBy(t => t.X) .Take(3) .ToList(); if (lineNumbers.Count > 0) layer.BottomElevation = ExtractDouble(lineNumbers[0].Text); if (lineNumbers.Count > 1) layer.BottomDepth = ExtractDouble(lineNumbers[1].Text); if (lineNumbers.Count > 2) layer.Thickness = ExtractDouble(lineNumbers[2].Text); // B. 提取描述 // 全局搜索以 anchor.Text 开头的文字 string searchPrefix = anchor.Text.Trim(); var descEntity = texts .Where(t => t.Id != anchor.Id) .Where(t => t.Text.Trim().StartsWith(searchPrefix)) // 核心:前缀匹配 .OrderByDescending(t => t.Text.Length) // 取最长的,最稳 .FirstOrDefault(); if (descEntity != null) { layer.Description = descEntity.Text; } layers.Add(layer); } return layers; } // ---------------------------------------------------------------- // 辅助功能 // ---------------------------------------------------------------- private List PreScanHoleNumbers(List frames, List allTexts) { var result = new List(); foreach (var frame in frames) { var textsInFrame = allTexts .Where(t => t.X >= frame.MinPoint.X && t.X <= frame.MaxPoint.X && t.Y >= frame.MinPoint.Y && t.Y <= frame.MaxPoint.Y) .ToList(); if (textsInFrame.Count < 5) continue; // 尝试找孔号 string holeNo = ""; var labelNode = textsInFrame.FirstOrDefault(t => t.Text.Replace(" ", "").Contains("钻孔编号")); if (labelNode != null) { holeNo = GetValueStrict(textsInFrame, labelNode); } // 必须找到孔号才加入分组,否则可能是图例或其他表格 if (!string.IsNullOrEmpty(holeNo)) { result.Add(new FrameInfo { Bounds = frame, HoleNumber = holeNo }); } } return result; } private List FilterNestedFrames(List frames) { if (frames == null || frames.Count == 0) return new List(); frames.Sort((a, b) => { double areaA = (a.MaxPoint.X - a.MinPoint.X) * (a.MaxPoint.Y - a.MinPoint.Y); double areaB = (b.MaxPoint.X - b.MinPoint.X) * (b.MaxPoint.Y - b.MinPoint.Y); return areaB.CompareTo(areaA); }); List validFrames = new List(); foreach (var candidate in frames) { bool isNested = false; foreach (var existing in validFrames) { if (candidate.MinPoint.X >= existing.MinPoint.X && candidate.MaxPoint.X <= existing.MaxPoint.X && candidate.MinPoint.Y >= existing.MinPoint.Y && candidate.MaxPoint.Y <= existing.MaxPoint.Y) { isNested = true; break; } } if (!isNested) validFrames.Add(candidate); } return validFrames; } private string GetValueStrict(List allTexts, TextEntity label) { var target = allTexts .Where(t => t != label) .Where(t => t.X > label.X && t.X < label.X + HeaderStrictDx) .Where(t => Math.Abs(t.Y - label.Y) < HeaderStrictDy) .OrderBy(t => t.X) .FirstOrDefault(); return target?.Text ?? ""; } private bool IsPureNumber(string text) => Regex.IsMatch(text.Trim(), @"^-?\d+(\.\d+)?$"); private double ExtractDouble(string text) { if (string.IsNullOrEmpty(text)) return 0; var match = Regex.Match(text, @"-?\d+(\.\d+)?"); return match.Success ? double.Parse(match.Value) : 0; } public class BoreholeLog { public HeaderInfo Header { get; set; } = new HeaderInfo(); public List Layers { get; set; } = new List(); } public class HeaderInfo { public string SurveyUnit { get; set; } public string ProjectName { get; set; } public string Mileage { get; set; } public string DesignElevation { get; set; } public string HoleNumber { get; set; } public string HoleType { get; set; } public double CoordinateX { get; set; } public double CoordinateY { get; set; } public string HoleElevation { get; set; } public string Diameter { get; set; } public string StartDate { get; set; } public string EndDate { get; set; } public string StableWaterLevel { get; set; } public string InitialWaterLevel { get; set; } } public class LayerData { public string LayerNo { get; set; } public double BottomElevation { get; set; } public double BottomDepth { get; set; } public double Thickness { get; set; } public string Description { get; set; } } } }