Files
SzmediTools/Szmedi.CADkits/TerrainTools.cs
2025-12-23 21:37:02 +08:00

279 lines
15 KiB
C#
Raw 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.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
}