Files
SzmediTools/Szmedi.CADkits/TerrainTools.cs

279 lines
15 KiB
C#
Raw Normal View History

2025-12-23 21:37:02 +08:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Windows.Forms.Integration;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Colors;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using Szmedi.CADkits;
using Application = Autodesk.AutoCAD.ApplicationServices.Application;
public class TerrainTools
{
private static PaletteSet ps;
public const string CheckLineLayer = "校对线";
[CommandMethod("MoveTerrainElevation")]
public void MoveTerrainElevation()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null) return;
Database db = doc.Database;
Editor ed = doc.Editor;
try
{
// --- Phase 1: User Input ---
#region User Input Section
string targetBlockName; string blockLayerName; string textLayerName; double maxDistance = 5.0;
using (Transaction tr = db.TransactionManager.StartTransaction()) { PromptEntityOptions peoBlock = new PromptEntityOptions("\n请选择一个样例地形点块 (用于确定块名和图层): "); peoBlock.SetRejectMessage("\n选择的不是块参照。"); peoBlock.AddAllowedClass(typeof(BlockReference), true); PromptEntityResult perBlock = ed.GetEntity(peoBlock); if (perBlock.Status != PromptStatus.OK) { ed.WriteMessage("\n操作已取消。"); return; } BlockReference sampleBlockRef = (BlockReference)tr.GetObject(perBlock.ObjectId, OpenMode.ForRead); targetBlockName = sampleBlockRef.IsDynamicBlock ? ((BlockTableRecord)tr.GetObject(sampleBlockRef.DynamicBlockTableRecord, OpenMode.ForRead)).Name : sampleBlockRef.Name; blockLayerName = sampleBlockRef.Layer; ed.WriteMessage($"\n已选择样例块: '{targetBlockName}', 所在图层: '{blockLayerName}'"); tr.Commit(); }
using (Transaction tr = db.TransactionManager.StartTransaction()) { PromptEntityOptions peoText = new PromptEntityOptions("\n请选择一个样例标高文字 (用于确定图层): "); peoText.SetRejectMessage("\n选择的不是 MText 或 DBText。"); peoText.AddAllowedClass(typeof(MText), true); peoText.AddAllowedClass(typeof(DBText), true); PromptEntityResult perText = ed.GetEntity(peoText); if (perText.Status != PromptStatus.OK) { ed.WriteMessage("\n操作已取消。"); return; } Entity sampleTextEnt = (Entity)tr.GetObject(perText.ObjectId, OpenMode.ForRead); textLayerName = sampleTextEnt.Layer; ed.WriteMessage($"\n已选择样例文字所在图层: '{textLayerName}'"); tr.Commit(); }
PromptDoubleOptions pdo = new PromptDoubleOptions($"\n请输入查找标高文字的最大距离(图形单位) <{maxDistance}>: "); pdo.AllowNegative = false; pdo.AllowZero = false; pdo.AllowNone = true; pdo.DefaultValue = maxDistance; PromptDoubleResult pdr = ed.GetDouble(pdo); if (pdr.Status != PromptStatus.OK && pdr.Status != PromptStatus.None) { ed.WriteMessage("\n操作已取消。"); return; }
if (pdr.Status == PromptStatus.OK) maxDistance = pdr.Value; ed.WriteMessage($"\n最大匹配距离设置为: {maxDistance}");
#endregion
// --- Phase 2: Automatic Matching ---
List<BlockRefWithPosition> terrainBlocks;
List<TextWithPosition> elevationTexts;
Dictionary<ObjectId, ObjectId> validPairs;
int updatedCount = 0;
using (Transaction tr = db.TransactionManager.StartTransaction())
{
BlockTableRecord btr = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
terrainBlocks = CollectBlocks(tr, btr, targetBlockName, blockLayerName);
if (terrainBlocks.Count == 0) { ed.WriteMessage($"\n错误: 在图层 '{blockLayerName}' 上未找到名为 '{targetBlockName}' 的块。"); return; }
elevationTexts = CollectFilteredTexts(tr, btr, textLayerName);
if (elevationTexts.Count == 0) { ed.WriteMessage($"\n警告: 在图层 '{textLayerName}' 上未找到任何有效的标高文字。"); tr.Commit(); return; }
ed.WriteMessage($"\n正在使用贪婪匹配算法分析 {terrainBlocks.Count} 个块...");
validPairs = GetScoredValidPairs(terrainBlocks, elevationTexts, maxDistance);
ed.WriteMessage($"\n分析完成。找到 {validPairs.Count} 个最优匹配对。");
if (validPairs.Count > 0)
{
CreateLayerIfNotExists(db, tr, CheckLineLayer, 8);
ProgressMeter pm = new ProgressMeter();
pm.SetLimit(validPairs.Count);
pm.Start("正在更新已自动匹配的块...");
var textDataMap = elevationTexts.ToDictionary(t => t.ObjectId, t => t);
foreach (var pair in validPairs)
{
if (double.TryParse(textDataMap[pair.Value].TextContent, out double elevationZ))
{
BlockReference blockRef = (BlockReference)tr.GetObject(pair.Key, OpenMode.ForWrite);
blockRef.Position = new Point3d(blockRef.Position.X, blockRef.Position.Y, elevationZ);
Line line = new Line(blockRef.Position, textDataMap[pair.Value].Position);
line.Layer = CheckLineLayer;
line.ColorIndex = 256;
btr.AppendEntity(line);
tr.AddNewlyCreatedDBObject(line, true);
updatedCount++;
}
}
pm.Stop();
}
tr.Commit();
}
// --- Phase 3: Reporting and Launching Review Palette ---
ed.WriteMessage($"\n\n--- 自动处理报告 ---\n成功自动更新 {updatedCount} 个块。\n{terrainBlocks.Count - updatedCount} 个块需要审核。");
if (terrainBlocks.Count > 0)
{
if (ps == null) { ps = new PaletteSet("审核并校对高程点", new Guid("C4E61D33-3162-4889-A263-6A1D2C52D26C")); }
if (ps.Count > 0) { ps.Remove(0); }
var reviewControl = new ReviewAndMatchWindow(terrainBlocks, validPairs, elevationTexts, CheckLineLayer, ps);
ElementHost host = new ElementHost { Dock = DockStyle.Fill, Child = reviewControl };
ps.Add("ReviewAndMatch", host);
ps.Visible = true;
}
}
catch (System.Exception ex) { ed.WriteMessage($"\n发生严重错误: {ex.Message}\n{ex.StackTrace}"); }
}
private Dictionary<ObjectId, ObjectId> GetScoredValidPairs(List<BlockRefWithPosition> blocks, List<TextWithPosition> texts, double maxDist)
{
// 1. 预先计算每个文字的竞争情况
var textCompetition = new Dictionary<ObjectId, double>();
foreach (var text in texts)
{
var distancesToBlocks = blocks
.Select(b => new Point2d(b.Position.X, b.Position.Y).GetDistanceTo(new Point2d(text.Position.X, text.Position.Y)))
.OrderBy(d => d)
.ToList();
if (distancesToBlocks.Count >= 2)
{
double d1 = distancesToBlocks[0]; // 最近距离
double d2 = distancesToBlocks[1]; // 次近距离
// 竞争度比率d1/d2越小说明越没有争议。范围 [0, 1]
textCompetition[text.ObjectId] = (d2 > 0.001) ? (d1 / d2) : 1.0;
}
else
{
textCompetition[text.ObjectId] = 0.0; // 只有一个块,没有竞争
}
}
// 2. 计算所有可能配对的综合得分
var allPossiblePairs = new List<Tuple<ObjectId, ObjectId, double>>(); // BlockId, TextId, Score
foreach (var block in blocks)
{
foreach (var text in texts)
{
double dist = new Point2d(block.Position.X, block.Position.Y)
.GetDistanceTo(new Point2d(text.Position.X, text.Position.Y));
if (dist <= maxDist)
{
// 距离分:距离越小,分数越高。用 1/dist 标准化
double distanceScore = 1.0 / (dist + 0.001); // 加一个小数防止除以0
// 唯一性分:竞争度越小(比率越小),分数越高
double uniquenessScore = 1.0 - textCompetition[text.ObjectId]; // 范围 [0, 1]
// **综合得分 = 70% 距离分 + 30% 唯一性分**
double finalScore = 0.7 * distanceScore + 0.3 * uniquenessScore * distanceScore; // 让唯一性也受距离影响
allPossiblePairs.Add(new Tuple<ObjectId, ObjectId, double>(block.ObjectId, text.ObjectId, finalScore));
}
}
}
// 3. 按综合得分从高到低排序
var sortedPairs = allPossiblePairs.OrderByDescending(p => p.Item3).ToList();
// 4. 贪婪匹配最高分的
var validPairs = new Dictionary<ObjectId, ObjectId>();
var matchedBlocks = new HashSet<ObjectId>();
var matchedTexts = new HashSet<ObjectId>();
foreach (var pair in sortedPairs)
{
ObjectId blockId = pair.Item1;
ObjectId textId = pair.Item2;
if (!matchedBlocks.Contains(blockId) && !matchedTexts.Contains(textId))
{
validPairs.Add(blockId, textId);
matchedBlocks.Add(blockId);
matchedTexts.Add(textId);
}
}
return validPairs;
}
public static void CleanAllCheckLines()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null) return;
Database db = doc.Database;
Editor ed = doc.Editor;
int cleanedCount = 0;
using (doc.LockDocument())
using (Transaction tr = db.TransactionManager.StartTransaction())
{
try
{
LayerTable lt = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
if (!lt.Has(CheckLineLayer))
{
ed.WriteMessage($"\n图层 '{CheckLineLayer}' 不存在,无需清理。");
return;
}
LayerTableRecord layer = (LayerTableRecord)tr.GetObject(lt[CheckLineLayer], OpenMode.ForRead);
if (layer.IsLocked)
{
layer.UpgradeOpen();
layer.IsLocked = false;
}
BlockTableRecord btr = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForRead);
foreach (ObjectId id in btr)
{
if (id.ObjectClass.DxfName.Equals("LINE", StringComparison.OrdinalIgnoreCase))
{
Line line = tr.GetObject(id, OpenMode.ForRead) as Line;
if (line != null && line.Layer.Equals(CheckLineLayer, StringComparison.OrdinalIgnoreCase))
{
line.UpgradeOpen();
line.Erase();
cleanedCount++;
}
}
}
tr.Commit();
ed.WriteMessage($"\n已清理 {cleanedCount} 条校对线。");
ed.Regen();
}
catch (System.Exception ex)
{
ed.WriteMessage($"\n清理时发生错误: {ex.Message}");
tr.Abort();
}
}
}
/// <summary>
/// Implements a greedy algorithm to find the best one-to-one block/text pairings.
/// </summary>
private Dictionary<ObjectId, ObjectId> GetGreedyValidPairs(List<BlockRefWithPosition> blocks, List<TextWithPosition> texts, double maxDist)
{
// 1. Create a list of all possible pairs within the max distance
var allPossiblePairs = new List<Tuple<ObjectId, ObjectId, double>>();
foreach (var block in blocks)
{
foreach (var text in texts)
{
double dist = new Point2d(block.Position.X, block.Position.Y)
.GetDistanceTo(new Point2d(text.Position.X, text.Position.Y));
if (dist <= maxDist)
{
allPossiblePairs.Add(new Tuple<ObjectId, ObjectId, double>(block.ObjectId, text.ObjectId, dist));
}
}
}
// 2. Sort all pairs by distance, ascending
var sortedPairs = allPossiblePairs.OrderBy(p => p.Item3).ToList();
// 3. Iterate and match, ensuring one-to-one mapping
var validPairs = new Dictionary<ObjectId, ObjectId>();
var matchedBlocks = new HashSet<ObjectId>();
var matchedTexts = new HashSet<ObjectId>();
foreach (var pair in sortedPairs)
{
ObjectId blockId = pair.Item1;
ObjectId textId = pair.Item2;
// If neither the block nor the text has been matched yet, it's a valid pair
if (!matchedBlocks.Contains(blockId) && !matchedTexts.Contains(textId))
{
validPairs.Add(blockId, textId);
matchedBlocks.Add(blockId);
matchedTexts.Add(textId);
}
}
return validPairs;
}
#region Other Helper Methods (Unchanged)
private List<BlockRefWithPosition> CollectBlocks(Transaction tr, BlockTableRecord btr, string targetBlockName, string blockLayerName) { var list = new List<BlockRefWithPosition>(); foreach (ObjectId id in btr) if (id.ObjectClass.IsDerivedFrom(RXObject.GetClass(typeof(BlockReference)))) { var br = (BlockReference)tr.GetObject(id, OpenMode.ForRead); string name = br.IsDynamicBlock ? ((BlockTableRecord)tr.GetObject(br.DynamicBlockTableRecord, OpenMode.ForRead)).Name : br.Name; if (br.Layer.Equals(blockLayerName, StringComparison.OrdinalIgnoreCase) && name.Equals(targetBlockName, StringComparison.OrdinalIgnoreCase)) list.Add(new BlockRefWithPosition(id, br.Position)); } return list; }
private List<TextWithPosition> CollectFilteredTexts(Transaction tr, BlockTableRecord btr, string textLayerName) { var list = new List<TextWithPosition>(); Regex rgx = new Regex(@"[\u4e00-\u9fa5]"); foreach (ObjectId id in btr) { if (id.ObjectClass.IsDerivedFrom(RXObject.GetClass(typeof(DBText))) || id.ObjectClass.IsDerivedFrom(RXObject.GetClass(typeof(MText)))) { var ent = (Entity)tr.GetObject(id, OpenMode.ForRead); if (ent.Layer.Equals(textLayerName, StringComparison.OrdinalIgnoreCase)) { string txt = (ent is MText mt) ? mt.Contents : ((DBText)ent).TextString; if (!string.IsNullOrWhiteSpace(txt) && !rgx.IsMatch(txt) && txt.Contains(".") && double.TryParse(txt, out _)) { try { Extents3d ext = ent.GeometricExtents; Point3d center = ext.MinPoint + (ext.MaxPoint - ext.MinPoint) * 0.5; list.Add(new TextWithPosition(id, center, txt)); } catch { } } } } } return list; }
private void CreateLayerIfNotExists(Database db, Transaction tr, string layerName, short colorIndex) { LayerTable lt = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead); if (!lt.Has(layerName)) { lt.UpgradeOpen(); var ltr = new LayerTableRecord { Name = layerName, Color = Color.FromColorIndex(ColorMethod.ByAci, colorIndex) }; lt.Add(ltr); tr.AddNewlyCreatedDBObject(ltr, true); } }
#endregion
}