CadParamPluging/Cad/TemplateDrawingService.cs

3778 lines
144 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using CadParamPluging.Common;
using CadParamPluging.Domain.Models;
namespace CadParamPluging.Cad
{
public static class TemplateDrawingService
{
private static readonly Regex MatchFieldRegex = new Regex(
@"^\s*(交付状态|工艺方法|结构特征|特殊条件)\s*[:]\s*(.+?)\s*$",
RegexOptions.Compiled);
private static readonly Regex MTextFormatRegex = new Regex(@"\\[A-Za-z]+[^;]*;", RegexOptions.Compiled);
// 匹配MText中残留的格式代码片段如 Xt31.278; 或 qj,tz; 等)
// 匹配模式1-4个字母开头后面可以是数字/点/逗号/字母的组合,最后以分号结尾
private static readonly Regex MTextResidualFormatRegex = new Regex(@"^[A-Za-z]{1,4}[\d.,A-Za-z]*;", RegexOptions.Compiled);
// 外框图层名称集合
private static readonly HashSet<string> OuterFrameLayerNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"图框", "边框", "外框", "FRAME", "BORDER", "粗实线"
};
private static readonly Regex CaxaLayerRegex = new Regex(@"^CAXA\d$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly HashSet<string> DimensionLayerNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"尺寸标注", "标注", "DIM", "DIMENSION", "DIMENSIONS"
};
public static Document OpenTemplateDrawing(TemplateInfo template)
{
if (template == null || string.IsNullOrWhiteSpace(template.FilePath))
{
throw new ArgumentException("Template path is invalid");
}
var docMgr = Application.DocumentManager;
foreach (Document doc in docMgr)
{
if (string.Equals(doc.Name, template.FilePath, StringComparison.OrdinalIgnoreCase))
{
docMgr.MdiActiveDocument = doc;
return doc;
}
}
var newDoc = docMgr.Open(template.FilePath, false);
docMgr.MdiActiveDocument = newDoc;
return newDoc;
}
/// <summary>
/// 移除模板中用于“匹配模板/图纸”的参数标注文本(交付状态/工艺方法/结构特征/特殊条件)。
/// 仅清理当前生成图纸中目标空间Layout 或 ModelSpace里的文本实体/块属性。
/// </summary>
public static int RemoveMatchParameterAnnotations(CadContext ctx, string layoutName, bool scanModelSpace)
{
if (ctx == null)
{
throw new ArgumentNullException(nameof(ctx));
}
var db = ctx.Database;
var tr = ctx.Transaction;
if (scanModelSpace)
{
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
return EraseMatchTextEntities(tr, ms);
}
if (string.IsNullOrWhiteSpace(layoutName))
{
return 0;
}
var layoutDict = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead);
ObjectId layoutId = ObjectId.Null;
foreach (DBDictionaryEntry entry in layoutDict)
{
if (string.Equals(entry.Key, layoutName, StringComparison.OrdinalIgnoreCase))
{
layoutId = entry.Value;
break;
}
}
if (layoutId.IsNull)
{
return 0;
}
var layout = (Layout)tr.GetObject(layoutId, OpenMode.ForRead);
var btr = (BlockTableRecord)tr.GetObject(layout.BlockTableRecordId, OpenMode.ForWrite);
return EraseMatchTextEntities(tr, btr);
}
private static int EraseMatchTextEntities(Transaction tr, BlockTableRecord btr)
{
if (tr == null || btr == null)
{
return 0;
}
var removed = 0;
var ids = btr.Cast<ObjectId>().ToList();
foreach (var id in ids)
{
var ent = tr.GetObject(id, OpenMode.ForWrite, false) as Entity;
if (ent == null)
{
continue;
}
if (ent is DBText t)
{
if (ContainsMatchField(t.TextString))
{
ent.Erase(true);
removed++;
}
continue;
}
if (ent is MText mt)
{
var plain = SimplifyMText(mt.Contents);
if (ContainsMatchField(plain))
{
ent.Erase(true);
removed++;
}
continue;
}
if (ent is BlockReference br)
{
foreach (ObjectId attId in br.AttributeCollection)
{
var att = tr.GetObject(attId, OpenMode.ForWrite, false) as AttributeReference;
if (att == null)
{
continue;
}
if (ContainsMatchField(att.TextString))
{
try
{
att.Erase(true);
removed++;
}
catch
{
// ignore
}
}
}
}
}
return removed;
}
private static bool ContainsMatchField(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
{
return false;
}
var lines = raw
.Replace("\r\n", "\n")
.Replace("\r", "\n")
.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => (s ?? string.Empty).Trim())
.Where(s => s.Length > 0);
foreach (var line in lines)
{
if (MatchFieldRegex.IsMatch(line))
{
return true;
}
}
return false;
}
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);
// 处理每行开头可能残留的格式代码片段(如 Xt31.278; 或 qj,tz;
var lines = s.Split(new[] { '\n' }, StringSplitOptions.None);
for (var i = 0; i < lines.Length; i++)
{
lines[i] = CleanLineResidualFormat(lines[i]);
}
s = string.Join("\n", lines);
return s;
}
private static string CleanLineResidualFormat(string line)
{
if (string.IsNullOrEmpty(line))
{
return line;
}
// 循环清理行首的格式代码残留,直到没有匹配为止
var result = line;
while (true)
{
var trimmed = result.TrimStart();
var match = MTextResidualFormatRegex.Match(trimmed);
if (match.Success && match.Index == 0)
{
// 移除匹配的格式代码
var leadingSpaces = result.Length - result.TrimStart().Length;
result = new string(' ', leadingSpaces) + trimmed.Substring(match.Length);
}
else
{
break;
}
}
return result;
}
public sealed class NoteApplyResult
{
public bool Applied { get; set; }
public string RenderedText { get; set; }
public int PlaceholderCountInDwg { get; set; }
public string TargetKind { get; set; }
public string Message { get; set; }
}
private sealed class NoteCandidate
{
public int Score;
public string Kind;
public ObjectId TargetId; // Added for persistence tracking
public string PlainText;
public string OriginalContents;
public Action<string> Apply;
}
/// <summary>
/// 在实体扩展字典中保存原始模板文本,以便下次重新生成时可以再次使用
/// </summary>
private static void SetNoteTemplateData(Transaction tr, ObjectId id, string template)
{
if (string.IsNullOrEmpty(template)) return;
var ent = tr.GetObject(id, OpenMode.ForWrite) as Entity;
if (ent == null) return;
if (ent.ExtensionDictionary.IsNull)
{
ent.CreateExtensionDictionary();
}
var dict = (DBDictionary)tr.GetObject(ent.ExtensionDictionary, OpenMode.ForWrite);
const string DictName = "CadParamPluging_NoteData";
// XRecord limits text to 255 characters per element.
// Split string into 250 character chunks using DxfCode.XTextString (301)
var rb = new ResultBuffer();
var temp = template;
while (temp.Length > 0)
{
var chunk = temp.Length > 250 ? temp.Substring(0, 250) : temp;
rb.Add(new TypedValue(301, chunk)); // 301 = DxfCode.XTextString
temp = temp.Length > 250 ? temp.Substring(250) : "";
}
var xrec = new Xrecord { Data = rb };
if (dict.Contains(DictName))
{
var oldId = dict.GetAt(DictName);
var oldXrec = (Xrecord)tr.GetObject(oldId, OpenMode.ForWrite);
oldXrec.Data = rb;
}
else
{
dict.SetAt(DictName, xrec);
tr.AddNewlyCreatedDBObject(xrec, true);
}
}
private static string GetNoteTemplateData(Transaction tr, ObjectId id)
{
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent == null || ent.ExtensionDictionary.IsNull) return null;
var dict = (DBDictionary)tr.GetObject(ent.ExtensionDictionary, OpenMode.ForRead);
const string DictName = "CadParamPluging_NoteData";
if (!dict.Contains(DictName)) return null;
var xrec = (Xrecord)tr.GetObject(dict.GetAt(DictName), OpenMode.ForRead);
var rb = xrec.Data;
if (rb == null) return null;
var sb = new System.Text.StringBuilder();
bool hasCode301 = false;
foreach (var tv in rb)
{
if (tv.TypeCode == 301)
{
hasCode301 = true;
if (tv.Value is string s) sb.Append(s);
}
}
if (hasCode301) return sb.ToString();
// Fallback for old code format
foreach (var tv in rb)
{
if (tv.TypeCode == (int)DxfCode.Text)
{
return tv.Value as string;
}
}
return null;
}
private sealed class FeatureCategoryData
{
public string OriginalTemplate;
public string GroupId;
public string Role;
}
private const string FeatureCategoryDictName = "CadParamPluging_FeatureCategory";
private static void SetFeatureCategoryData(Transaction tr, ObjectId id, string originalTemplate, string groupId, string role)
{
var ent = tr.GetObject(id, OpenMode.ForWrite) as Entity;
if (ent == null) return;
if (ent.ExtensionDictionary.IsNull)
{
ent.CreateExtensionDictionary();
}
var dict = (DBDictionary)tr.GetObject(ent.ExtensionDictionary, OpenMode.ForWrite);
var rb = new ResultBuffer(
new TypedValue((int)DxfCode.Text, originalTemplate ?? string.Empty),
new TypedValue((int)DxfCode.Text, groupId ?? string.Empty),
new TypedValue((int)DxfCode.Text, role ?? string.Empty)
);
var xrec = new Xrecord { Data = rb };
if (dict.Contains(FeatureCategoryDictName))
{
var oldId = dict.GetAt(FeatureCategoryDictName);
var oldXrec = (Xrecord)tr.GetObject(oldId, OpenMode.ForWrite);
oldXrec.Data = rb;
}
else
{
dict.SetAt(FeatureCategoryDictName, xrec);
tr.AddNewlyCreatedDBObject(xrec, true);
}
}
private static FeatureCategoryData GetFeatureCategoryData(Transaction tr, ObjectId id)
{
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent == null || ent.ExtensionDictionary.IsNull) return null;
var dict = (DBDictionary)tr.GetObject(ent.ExtensionDictionary, OpenMode.ForRead);
if (!dict.Contains(FeatureCategoryDictName)) return null;
var xrec = (Xrecord)tr.GetObject(dict.GetAt(FeatureCategoryDictName), OpenMode.ForRead);
var rb = xrec.Data;
if (rb == null) return null;
var texts = new List<string>();
foreach (var tv in rb)
{
if (tv.TypeCode == (int)DxfCode.Text)
{
texts.Add(tv.Value as string);
}
}
if (texts.Count == 0) return null;
return new FeatureCategoryData
{
OriginalTemplate = texts.Count >= 1 ? texts[0] : null,
GroupId = texts.Count >= 2 ? texts[1] : null,
Role = texts.Count >= 3 ? texts[2] : null
};
}
private static string ExtractMTextPlainText(string contents)
{
if (string.IsNullOrEmpty(contents)) return string.Empty;
var s = contents;
// Common pattern we generate: {\fSimSun|...;TEXT}
var semi = s.LastIndexOf(';');
if (semi >= 0)
{
s = s.Substring(semi + 1);
}
s = s.Trim();
if (s.EndsWith("}", StringComparison.Ordinal))
{
s = s.Substring(0, s.Length - 1);
}
s = s.Replace("\\P", "\n");
return s.Trim();
}
public static NoteApplyResult ApplyNoteTemplate(CadContext ctx, string layoutName, bool scanModelSpace, TemplateSchemaDefinition schema, ParamBag bag)
{
if (ctx == null)
{
throw new ArgumentNullException(nameof(ctx));
}
if (schema == null)
{
return new NoteApplyResult { Applied = false, Message = "Schema is null" };
}
if (bag == null)
{
return new NoteApplyResult { Applied = false, Message = "ParamBag is null" };
}
if (schema.NoteBindings == null || schema.NoteBindings.Count == 0)
{
return new NoteApplyResult { Applied = false, Message = "未配置附注绑定,跳过。" };
}
var db = ctx.Database;
var tr = ctx.Transaction;
var space = GetTargetSpace(tr, db, layoutName, scanModelSpace);
if (space == null)
{
return new NoteApplyResult { Applied = false, Message = "未找到目标空间,跳过附注替换。" };
}
Func<string, string> getValueW = (k) =>
{
var val = bag.GetString(k);
if (k != null && k.StartsWith("MarkingContent", StringComparison.OrdinalIgnoreCase))
{
if (bag.GetString(k + "_ShowInNote") == "0") return "__SKIP_NOTE__";
}
return val;
};
// 1. 优先查找已经标记过的 Note Entity (重生成场景)
foreach (ObjectId id in space)
{
var storedTemplate = GetNoteTemplateData(tr, id);
if (!string.IsNullOrEmpty(storedTemplate))
{
// 找到了之前的附注,使用存储的模板重新渲染
var effectiveStored = NoteTemplateEngine.BuildEffectiveValueKeyBindings(schema.NoteBindings);
var renderedStored = NoteTemplateEngine.Render(storedTemplate, effectiveStored, getValueW);
ApplyNoteTextToObject(tr, id, "ExistingTaggedNote", renderedStored, null);
return new NoteApplyResult
{
Applied = true,
TargetKind = "ExistingTaggedNote",
PlaceholderCountInDwg = 0,
RenderedText = renderedStored,
Message = "已更新现有附注 (基于存储的模板)。"
};
}
}
var candidates = new List<NoteCandidate>();
var visitedBlocks = new HashSet<ObjectId>();
foreach (ObjectId id in space)
{
var ent = tr.GetObject(id, OpenMode.ForRead, false) as Entity;
if (ent != null)
{
CollectNoteCandidates(tr, ent, visitedBlocks, candidates);
}
}
if (candidates.Count == 0)
{
// 尝试基于白框定位自动创建附注
if (!string.IsNullOrWhiteSpace(schema.NoteTemplateText))
{
// 基于当前 Space 查找白框,以支持 Layout
var spaceEntList = space.Cast<ObjectId>()
.Select(id =>
{
try { return tr.GetObject(id, OpenMode.ForRead, false) as Entity; }
catch { return null; }
})
.Where(e => e != null && !e.IsErased)
.ToList();
var frame = ComputeWhiteFrameExtentsFromEntities(tr, spaceEntList);
if (frame.HasValue)
{
var f = frame.Value;
var w = f.MaxPoint.X - f.MinPoint.X;
var h = f.MaxPoint.Y - f.MinPoint.Y;
// 改为左下角锚定 (BottomLeft),实现"贴合左边和下边"的效果
// MText 以 BottomLeft 对齐时,插入点位于文字块左下角,内容向上生长
var insertX = f.MinPoint.X + w * 0.005; // 左边距 0.5%
var insertY = f.MinPoint.Y + h * 0.005; // 下边距 0.5%
var insertPoint = new Point3d(insertX, insertY, 0);
var effectiveForCreation = NoteTemplateEngine.BuildEffectiveValueKeyBindings(schema.NoteBindings);
var renderedForCreation = NoteTemplateEngine.Render(schema.NoteTemplateText, effectiveForCreation, getValueW);
var mt = new MText();
mt.Contents = ToMTextContents(renderedForCreation);
mt.Location = insertPoint;
mt.TextHeight = 3.0;
mt.Attachment = AttachmentPoint.BottomLeft; // 关键:锚点在左下,文字向上延伸
// 智能动态宽度:尝试检测右下角标题栏(表格)的左边界
// 修正搜索区域扩大防止表格过宽超过50%)导致检测不到左边界,从而产生重叠
// X方向从左侧 25% 开始扫描(涵盖右侧 75% 区域)
// Y方向从底部扫描至 50% 高度(防止标题栏上方有较高的更改栏)
var searchMinX = f.MinPoint.X + w * 0.25;
var searchMaxX = f.MaxPoint.X - 5.0;
var searchMinY = f.MinPoint.Y;
var searchMaxY = f.MinPoint.Y + h * 0.5;
var candidatesX = new List<double>();
foreach (var ent in spaceEntList)
{
if (ent is Line ln)
{
// 垂直线
if (Math.Abs(ln.StartPoint.X - ln.EndPoint.X) < 1.0 &&
ln.StartPoint.X > searchMinX && ln.StartPoint.X < searchMaxX &&
Math.Max(ln.StartPoint.Y, ln.EndPoint.Y) > searchMinY &&
Math.Min(ln.StartPoint.Y, ln.EndPoint.Y) < searchMaxY)
{
candidatesX.Add(ln.StartPoint.X);
}
}
else if (ent is Polyline pl)
{
for (int i = 0; i < pl.NumberOfVertices; i++)
{
var pt = pl.GetPoint3dAt(i);
if (pt.X > searchMinX && pt.X < searchMaxX && pt.Y > searchMinY && pt.Y < searchMaxY)
{
candidatesX.Add(pt.X);
}
}
}
else if (ent is BlockReference br)
{
try
{
var ext = br.GeometricExtents;
// 只要块的一部分落在搜索区域内,我们就考虑其最左边界
if (ext.MaxPoint.X > searchMinX && ext.MinPoint.X < searchMaxX &&
ext.MaxPoint.Y > searchMinY && ext.MinPoint.Y < searchMaxY)
{
// 确保最左边界在合理范围内(不小于搜索起点)
// 如果块非常大(如整个图框块),我们要小心不要把图框左边当成表格左边
// 假设表格块通常比起点 (0.25w) 要靠右
if (ext.MinPoint.X > searchMinX)
{
candidatesX.Add(ext.MinPoint.X);
}
}
}
catch { }
}
}
// 默认最左边界为图框右边界
double boundaryX = f.MaxPoint.X;
bool foundTable = false;
if (candidatesX.Count > 0)
{
// 取最小值作为表格左边界
boundaryX = candidatesX.Min();
foundTable = true;
}
double availableWidth;
if (foundTable)
{
// 如果找到了表格,宽度 = 表格左边界 - 插入点 - 留白
availableWidth = boundaryX - insertX - 5.0;
}
else
{
// 没找到表格 -> 使用更保守的默认比例 (33%),避免覆盖
availableWidth = w * 0.33;
}
// 安全检查:如果计算出的宽度太小(比如小于总宽 10%),保留最小可读宽度或回退
if (availableWidth < w * 0.10)
{
// 空间过小,强行给予 30% 宽度,可能会重叠但保证文字显示
availableWidth = w * 0.30;
}
mt.Width = availableWidth;
mt.ColorIndex = 7;
mt.Layer = "0";
try
{
var layerTbl = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
if (layerTbl.Has("TEXT"))
{
mt.Layer = "TEXT";
}
else if (layerTbl.Has("文字"))
{
mt.Layer = "文字";
}
}
catch { }
space.AppendEntity(mt);
tr.AddNewlyCreatedDBObject(mt, true);
// 关键:标记该实体,保存模板文本
SetNoteTemplateData(tr, mt.ObjectId, schema.NoteTemplateText);
return new NoteApplyResult
{
Applied = true,
TargetKind = "CreatedMText",
PlaceholderCountInDwg = 0,
RenderedText = renderedForCreation,
Message = "未找到附注定位,已自动创建并标记。"
};
}
}
return new NoteApplyResult { Applied = false, Message = "未找到附注文本目标(包含‘附注’且包含占位符*),且无法自动定位创建,跳过。" };
}
var best = candidates.OrderByDescending(c => c.Score).FirstOrDefault();
if (best == null || best.Apply == null)
{
return new NoteApplyResult { Applied = false, Message = "未找到可替换的附注文本目标,跳过。" };
}
var templateText = best.PlainText ?? string.Empty;
var placeholderCount = NoteTemplateEngine.CountPlaceholders(templateText);
if (placeholderCount <= 0)
{
return new NoteApplyResult
{
Applied = false,
TargetKind = best.Kind,
PlaceholderCountInDwg = placeholderCount,
Message = "附注目标中未检测到有效占位符(*,忽略****),跳过。"
};
}
var effective = NoteTemplateEngine.BuildEffectiveValueKeyBindings(schema.NoteBindings);
var rendered = NoteTemplateEngine.Render(templateText, effective, getValueW);
best.Apply(rendered);
if (!best.TargetId.IsNull)
{
SetNoteTemplateData(tr, best.TargetId, templateText);
}
return new NoteApplyResult
{
Applied = true,
TargetKind = best.Kind,
PlaceholderCountInDwg = placeholderCount,
RenderedText = rendered,
Message = "附注已替换并标记。"
};
}
private static BlockTableRecord GetTargetSpace(Transaction tr, Database db, string layoutName, bool scanModelSpace)
{
if (tr == null || db == null)
{
return null;
}
// Default to ModelSpace if scanModelSpace requested OR layoutName is null/empty implicitly aiming for Model
if (scanModelSpace || string.IsNullOrWhiteSpace(layoutName) || string.Equals(layoutName, "Model", StringComparison.OrdinalIgnoreCase))
{
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
return (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
}
var layoutDict = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead);
ObjectId layoutId = ObjectId.Null;
foreach (DBDictionaryEntry entry in layoutDict)
{
if (string.Equals(entry.Key, layoutName, StringComparison.OrdinalIgnoreCase))
{
layoutId = entry.Value;
break;
}
}
if (layoutId.IsNull)
{
return null;
}
var layout = (Layout)tr.GetObject(layoutId, OpenMode.ForRead);
return (BlockTableRecord)tr.GetObject(layout.BlockTableRecordId, OpenMode.ForWrite);
}
private static void CollectNoteCandidates(Transaction tr, Entity ent, HashSet<ObjectId> visitedBlocks, List<NoteCandidate> candidates)
{
if (tr == null || ent == null || candidates == null)
{
return;
}
if (ent is DBText t)
{
AddNoteCandidateIfMatch(t.TextString, "DBText", t.ObjectId, tr, candidates);
return;
}
if (ent is MText mt)
{
var plain = SimplifyMText(mt.Contents);
AddNoteCandidateIfMatch(plain, "MText", mt.ObjectId, tr, candidates, mt.Contents);
return;
}
if (ent is BlockReference br)
{
foreach (ObjectId attId in br.AttributeCollection)
{
var att = tr.GetObject(attId, OpenMode.ForRead, false) as AttributeReference;
if (att == null)
{
continue;
}
AddNoteCandidateIfMatch(att.TextString, "Attribute", attId, tr, candidates);
}
var blockId = br.BlockTableRecord;
if (blockId.IsNull)
{
return;
}
if (visitedBlocks == null)
{
visitedBlocks = new HashSet<ObjectId>();
}
if (visitedBlocks.Contains(blockId))
{
return;
}
visitedBlocks.Add(blockId);
var btr = tr.GetObject(blockId, OpenMode.ForRead, false) as BlockTableRecord;
if (btr == null)
{
return;
}
foreach (ObjectId childId in btr)
{
var child = tr.GetObject(childId, OpenMode.ForRead, false) as Entity;
if (child != null)
{
CollectNoteCandidates(tr, child, visitedBlocks, candidates);
}
}
}
}
private static void AddNoteCandidateIfMatch(string plainText, string kind, ObjectId id, Transaction tr, List<NoteCandidate> candidates, string originalContents = null)
{
if (!IsLikelyNoteText(plainText))
{
return;
}
var score = ComputeNoteScore(plainText);
if (score <= 0)
{
return;
}
candidates.Add(new NoteCandidate
{
Score = score,
Kind = kind,
TargetId = id, // Store ID
PlainText = plainText,
OriginalContents = originalContents,
Apply = rendered => ApplyNoteTextToObject(tr, id, kind, rendered, originalContents)
});
}
private static bool IsLikelyNoteText(string plain)
{
if (string.IsNullOrWhiteSpace(plain))
{
return false;
}
if (!plain.Contains("*"))
{
return false;
}
if (plain.Contains("附注"))
{
return true;
}
// fallback: numbered note line
return plain.Contains("1*") || plain.Contains("1 *");
}
private static int ComputeNoteScore(string plain)
{
if (string.IsNullOrWhiteSpace(plain))
{
return 0;
}
var score = 0;
var lines = SplitLines(plain).ToList();
if (plain.Contains("附注"))
{
score += 10;
}
if (lines.Count > 0 && string.Equals(lines[0].Trim(), "附注", StringComparison.OrdinalIgnoreCase))
{
score += 10;
}
if (plain.Contains("*"))
{
score += 5;
}
if (plain.Contains("1*"))
{
score += 3;
}
return score;
}
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());
}
private static void ApplyNoteTextToObject(Transaction tr, ObjectId id, string kind, string rendered, string originalContents = null)
{
if (tr == null || id.IsNull)
{
return;
}
rendered = rendered ?? string.Empty;
// We try multiple types regardless of recorded kind to be safe.
var obj = tr.GetObject(id, OpenMode.ForWrite, false);
if (obj is MText mt)
{
// 如果有原始内容,在原始内容中直接替换占位符以保留格式
if (!string.IsNullOrEmpty(originalContents))
{
mt.Contents = ReplaceStarsInOriginalContents(originalContents, rendered);
}
else
{
mt.Contents = ToMTextContents(rendered);
}
return;
}
if (obj is DBText t)
{
var collapsed = CollapseToSingleLine(rendered);
if (collapsed.Length > 250)
{
// DBText 在 CAD 中有硬性的 255 长度限制,长文本需升级为 MText
var newMt = new MText();
newMt.Contents = ToMTextContents(rendered); // 恢复原有折行
newMt.Location = t.Position;
newMt.TextHeight = t.Height;
newMt.Layer = t.Layer;
newMt.ColorIndex = t.ColorIndex;
newMt.TextStyleId = t.TextStyleId;
newMt.Rotation = t.Rotation;
if (!t.BlockId.IsNull)
{
var btr = (BlockTableRecord)tr.GetObject(t.BlockId, OpenMode.ForWrite);
btr.AppendEntity(newMt);
tr.AddNewlyCreatedDBObject(newMt, true);
t.Erase(true);
}
else
{
t.TextString = collapsed.Substring(0, 250);
}
}
else
{
t.TextString = collapsed;
}
return;
}
if (obj is AttributeReference att)
{
var collapsed = CollapseToSingleLine(rendered);
if (collapsed.Length > 250)
{
// AttributeReference 同样受 255 长度限制,用同等 MText 挂载并隐去原文本
var newMt = new MText();
newMt.Contents = ToMTextContents(rendered);
newMt.Location = att.Position;
newMt.TextHeight = att.Height;
newMt.Layer = att.Layer;
newMt.ColorIndex = att.ColorIndex;
newMt.TextStyleId = att.TextStyleId;
newMt.Rotation = att.Rotation;
if (!att.OwnerId.IsNull)
{
var blk = tr.GetObject(att.OwnerId, OpenMode.ForRead) as BlockReference;
if (blk != null && !blk.OwnerId.IsNull)
{
var space = (BlockTableRecord)tr.GetObject(blk.OwnerId, OpenMode.ForWrite);
space.AppendEntity(newMt);
tr.AddNewlyCreatedDBObject(newMt, true);
att.TextString = "";
att.Invisible = true;
}
else
{
att.TextString = collapsed.Substring(0, 250);
}
}
else
{
att.TextString = collapsed.Substring(0, 250);
}
}
else
{
att.TextString = collapsed;
}
}
}
private static string ReplaceStarsInOriginalContents(string originalContents, string renderedPlainText)
{
if (string.IsNullOrEmpty(originalContents) || string.IsNullOrEmpty(renderedPlainText))
{
return ToMTextContents(renderedPlainText);
}
// 从SimplifyMText处理后的纯文本和渲染后的纯文本中提取*被替换成什么
var plainTemplate = SimplifyMText(originalContents);
// 提取每个占位符被替换成的值
var replacements = ExtractPlaceholderReplacements(plainTemplate, renderedPlainText);
if (replacements == null || replacements.Count == 0)
{
string newContents = ToMTextContents(renderedPlainText);
if (originalContents.StartsWith("{") && originalContents.Contains(";"))
{
var match = System.Text.RegularExpressions.Regex.Match(originalContents, @"^(\{\\[^;]+;)");
if (match.Success)
{
newContents = match.Groups[1].Value + newContents + "}";
}
}
return newContents;
}
// 在原始MText内容中直接替换*占位符
var result = new System.Text.StringBuilder();
var replacementIndex = 0;
for (var i = 0; i < originalContents.Length;)
{
var c = originalContents[i];
// 检查是否是占位符*
if (c == '*')
{
// 计算连续的*数量
var starCount = 0;
var j = i;
while (j < originalContents.Length && originalContents[j] == '*')
{
starCount++;
j++;
}
if (starCount == 4)
{
// 保留****
result.Append("****");
i += 4;
}
else
{
// 替换每个*
for (var k = 0; k < starCount; k++)
{
if (replacementIndex < replacements.Count)
{
result.Append(replacements[replacementIndex]);
replacementIndex++;
}
else
{
result.Append('*');
}
}
i += starCount;
}
}
else
{
result.Append(c);
i++;
}
}
return result.ToString();
}
private static List<string> ExtractPlaceholderReplacements(string plainTemplate, string renderedText)
{
var replacements = new List<string>();
// 将两个文本按相同方式处理
var template = (plainTemplate ?? string.Empty)
.Replace("\r\n", "\n")
.Replace("\r", "\n");
var rendered = (renderedText ?? string.Empty)
.Replace("\r\n", "\n")
.Replace("\r", "\n");
var ti = 0; // template index
var ri = 0; // rendered index
while (ti < template.Length && ri < rendered.Length)
{
var tc = template[ti];
if (tc == '*')
{
// 检查是否是****
var starCount = 0;
var j = ti;
while (j < template.Length && template[j] == '*')
{
starCount++;
j++;
}
if (starCount == 4)
{
// ****保持不变rendered中也应该有****
ti += 4;
if (ri + 4 <= rendered.Length && rendered.Substring(ri, 4) == "****")
{
ri += 4;
}
}
else
{
// 提取替换内容
int nextTi = ti + starCount;
string nextLiteral = "";
while (nextTi < template.Length && template[nextTi] != '*')
{
nextLiteral += template[nextTi];
nextTi++;
}
if (string.IsNullOrEmpty(nextLiteral))
{
// 如果后面没有文字了,那么直接把 rendered 中剩下的所有文字都给第一颗 *
string rep = rendered.Substring(ri);
for (int k = 0; k < starCount; k++)
{
replacements.Add(k == 0 ? rep : "");
}
ri = rendered.Length;
ti += starCount;
}
else
{
// 在 rendered 中寻找 nextLiteral
int foundIndex = rendered.IndexOf(nextLiteral, ri);
if (foundIndex >= 0)
{
string rep = rendered.Substring(ri, foundIndex - ri);
for (int k = 0; k < starCount; k++)
{
replacements.Add(k == 0 ? rep : "");
}
ri = foundIndex;
ti += starCount;
}
else
{
// 找不到说明因为换行、标点清理或重编号导致了较大差异,直接回退整段格式
return new List<string>();
}
}
}
}
else
{
if (tc == rendered[ri])
{
ti++;
ri++;
}
else
{
// 不匹配说明有由于处理(如空行、或者清理了符号)导致文字不一致
// 直接返回以使用完全的 ToMTextContents(rendered) 包含 MText 的原字体格式
return new List<string>();
}
}
}
return replacements;
}
private static string ToMTextContents(string plainText)
{
if (string.IsNullOrEmpty(plainText))
{
return string.Empty;
}
var s = plainText
.Replace("\r\n", "\n")
.Replace("\r", "\n")
.Replace("\n", "\\P");
return s;
}
private static string CollapseToSingleLine(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return string.Empty;
}
var parts = (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)
.ToArray();
return string.Join(" ", parts);
}
/// <summary>
/// 确保特性分类文字周围有框体,如果没有则创建
/// </summary>
private static void EnsureFeatureCategoryBox(Transaction tr, Database db, BlockTableRecord space, List<Entity> spaceEntList, ObjectId textEntityId, Action<string> logAction)
{
// 获取文字实体的最新范围
Extents3d textExt;
try
{
var textEnt = tr.GetObject(textEntityId, OpenMode.ForRead, false) as Entity;
if (textEnt == null || textEnt.IsErased) return;
// 针对 MText使用实际文字范围而非控制框范围
if (textEnt is MText mt)
{
// MText 的 Location 是插入点,根据 Attachment 不同位置不同
// 使用 ActualWidth 和 ActualHeight 获取实际文字尺寸
var loc = mt.Location;
var actualW = mt.ActualWidth;
var actualH = mt.ActualHeight;
// 根据 Attachment 计算实际边界
double minX, maxX, minY, maxY;
switch (mt.Attachment)
{
case AttachmentPoint.TopLeft:
minX = loc.X; maxX = loc.X + actualW;
minY = loc.Y - actualH; maxY = loc.Y;
break;
case AttachmentPoint.TopCenter:
minX = loc.X - actualW / 2; maxX = loc.X + actualW / 2;
minY = loc.Y - actualH; maxY = loc.Y;
break;
case AttachmentPoint.TopRight:
minX = loc.X - actualW; maxX = loc.X;
minY = loc.Y - actualH; maxY = loc.Y;
break;
case AttachmentPoint.MiddleLeft:
minX = loc.X; maxX = loc.X + actualW;
minY = loc.Y - actualH / 2; maxY = loc.Y + actualH / 2;
break;
case AttachmentPoint.MiddleCenter:
minX = loc.X - actualW / 2; maxX = loc.X + actualW / 2;
minY = loc.Y - actualH / 2; maxY = loc.Y + actualH / 2;
break;
case AttachmentPoint.MiddleRight:
minX = loc.X - actualW; maxX = loc.X;
minY = loc.Y - actualH / 2; maxY = loc.Y + actualH / 2;
break;
case AttachmentPoint.BottomLeft:
minX = loc.X; maxX = loc.X + actualW;
minY = loc.Y; maxY = loc.Y + actualH;
break;
case AttachmentPoint.BottomCenter:
minX = loc.X - actualW / 2; maxX = loc.X + actualW / 2;
minY = loc.Y; maxY = loc.Y + actualH;
break;
case AttachmentPoint.BottomRight:
minX = loc.X - actualW; maxX = loc.X;
minY = loc.Y; maxY = loc.Y + actualH;
break;
default:
// 兜底使用 GeometricExtents
textExt = textEnt.GeometricExtents;
goto afterExtents;
}
textExt = new Extents3d(new Point3d(minX, minY, 0), new Point3d(maxX, maxY, 0));
logAction?.Invoke($"[DEBUG] MText: Loc=({loc.X:F1},{loc.Y:F1}), W={actualW:F1}, H={actualH:F1}, Attach={mt.Attachment}");
logAction?.Invoke($"[DEBUG] 计算范围: ({minX:F1},{minY:F1})->({maxX:F1},{maxY:F1})");
}
else
{
textExt = textEnt.GeometricExtents;
}
afterExtents:;
logAction?.Invoke($"[DEBUG] 最终范围: Min=({textExt.MinPoint.X:F1},{textExt.MinPoint.Y:F1}), Max=({textExt.MaxPoint.X:F1},{textExt.MaxPoint.Y:F1})");
}
catch (System.Exception ex)
{
logAction?.Invoke($"[DEBUG] 获取范围失败: {ex.Message}");
return;
}
// 检查是否已经有包围该文字的闭合 Polyline大小匹配
bool hasBox = false;
var textWidth = textExt.MaxPoint.X - textExt.MinPoint.X;
var textHeight = textExt.MaxPoint.Y - textExt.MinPoint.Y;
// 框体最大允许尺寸 = 文字尺寸 + 最大 padding (5mm)
var maxBoxWidth = textWidth + 10.0; // 两边各5mm
var maxBoxHeight = textHeight + 10.0; // 上下各5mm
foreach (var ent in spaceEntList)
{
if (ent is Polyline pl && pl.Closed && !pl.IsErased)
{
try
{
var ex = pl.GeometricExtents;
var boxWidth = ex.MaxPoint.X - ex.MinPoint.X;
var boxHeight = ex.MaxPoint.Y - ex.MinPoint.Y;
// 检查 Polyline 是否包围文字(有 0.1 的容差)
// 并且大小接近文字大小(不能太大,避免误判表格边框)
if (ex.MinPoint.X <= textExt.MinPoint.X - 0.1
&& ex.MaxPoint.X >= textExt.MaxPoint.X + 0.1
&& ex.MinPoint.Y <= textExt.MinPoint.Y - 0.1
&& ex.MaxPoint.Y >= textExt.MaxPoint.Y + 0.1
&& boxWidth <= maxBoxWidth
&& boxHeight <= maxBoxHeight)
{
hasBox = true;
logAction?.Invoke($"[DEBUG] 已找到匹配框体: 框体尺寸({boxWidth:F1}x{boxHeight:F1}), 文字尺寸({textWidth:F1}x{textHeight:F1}), 图层={pl.Layer}");
// 将框体移动到安全图层,防止被 RemoveTemplateOriginalDrawing 删除
try
{
var plWrite = (Polyline)tr.GetObject(pl.ObjectId, OpenMode.ForWrite, false);
var layerTbl = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
string safeLayer = "0";
if (layerTbl.Has("TEXT")) safeLayer = "TEXT";
else if (layerTbl.Has("文字")) safeLayer = "文字";
plWrite.Layer = safeLayer;
plWrite.ColorIndex = 7; // 设置为白色
logAction?.Invoke($"[DEBUG] 已将框体移动到安全图层: {safeLayer}, 颜色已设置为白色");
// 标记此框体,便于后续识别
var groupId = Guid.NewGuid().ToString("N");
SetFeatureCategoryData(tr, pl.ObjectId, string.Empty, groupId, "FallbackBox");
}
catch { }
break;
}
}
catch { }
}
}
// 如果没有框体,则创建一个
if (!hasBox)
{
double paddingH = 1.5;
double paddingV = 1.5;
var boxMinX = textExt.MinPoint.X - paddingH;
var boxMaxX = textExt.MaxPoint.X + paddingH;
var boxMinY = textExt.MinPoint.Y - paddingV;
var boxMaxY = textExt.MaxPoint.Y + paddingV;
logAction?.Invoke($"[DEBUG] 创建框体: ({boxMinX:F1},{boxMinY:F1})->({boxMaxX:F1},{boxMaxY:F1})");
var poly = new Polyline();
poly.AddVertexAt(0, new Point2d(boxMinX, boxMinY), 0, 0, 0);
poly.AddVertexAt(1, new Point2d(boxMaxX, boxMinY), 0, 0, 0);
poly.AddVertexAt(2, new Point2d(boxMaxX, boxMaxY), 0, 0, 0);
poly.AddVertexAt(3, new Point2d(boxMinX, boxMaxY), 0, 0, 0);
poly.Closed = true;
poly.ColorIndex = 7; // White
try
{
var layerTbl = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
string targetLayer = "0";
if (layerTbl.Has("TEXT")) targetLayer = "TEXT";
else if (layerTbl.Has("文字")) targetLayer = "文字";
poly.Layer = targetLayer;
}
catch { }
space.AppendEntity(poly);
tr.AddNewlyCreatedDBObject(poly, true);
var groupId = Guid.NewGuid().ToString("N");
try { SetFeatureCategoryData(tr, poly.ObjectId, string.Empty, groupId, "FallbackBox"); } catch { }
}
}
public static int ApplyFeatureCategory(CadContext ctx, string layoutName, bool scanModelSpace, ParamBag bag, Action<string> logAction = null)
{
if (ctx == null || bag == null)
{
return 0;
}
var category = bag.GetString("FeatureCategory");
// Logic: Is "一般件" -> show blank.
// Is "关键件"/"重要件" -> show text.
var textToShow = category;
if (string.IsNullOrWhiteSpace(textToShow) || string.Equals(textToShow, "一般件", StringComparison.OrdinalIgnoreCase))
{
textToShow = string.Empty; // Show blank
}
logAction?.Invoke($"[DEBUG] ApplyFeatureCategory: category='{category}', textToShow='{textToShow}'");
var db = ctx.Database;
var tr = ctx.Transaction;
var space = GetTargetSpace(tr, db, layoutName, scanModelSpace);
if (space == null) return 0;
var ids = space.Cast<ObjectId>().ToArray();
logAction?.Invoke($"[DEBUG] Space实体数量: {ids.Length}");
int count = 0;
// 0) If we previously created fallback feature-category texts, update or remove them (prevents duplication).
var fallbackGroupIdsToRemove = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var id in ids)
{
FeatureCategoryData fcData = null;
try { fcData = GetFeatureCategoryData(tr, id); } catch { }
if (fcData == null || string.IsNullOrWhiteSpace(fcData.Role))
{
continue;
}
if (string.Equals(fcData.Role, "FallbackText", StringComparison.OrdinalIgnoreCase))
{
var ent = tr.GetObject(id, OpenMode.ForWrite, false) as Entity;
if (ent == null) continue;
if (string.IsNullOrWhiteSpace(textToShow))
{
if (!ent.IsErased) ent.Erase();
if (!string.IsNullOrWhiteSpace(fcData.GroupId)) fallbackGroupIdsToRemove.Add(fcData.GroupId);
continue;
}
if (ent is MText mt)
{
mt.Contents = @"{\fSimSun|b0|i0|c134|p2;" + ToMTextContents(textToShow) + "}";
count++;
}
}
else if (string.Equals(fcData.Role, "FallbackBox", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(textToShow) && !string.IsNullOrWhiteSpace(fcData.GroupId))
{
fallbackGroupIdsToRemove.Add(fcData.GroupId);
}
}
}
if (fallbackGroupIdsToRemove.Count > 0)
{
foreach (var id in ids)
{
FeatureCategoryData fcData = null;
try { fcData = GetFeatureCategoryData(tr, id); } catch { }
if (fcData == null || string.IsNullOrWhiteSpace(fcData.GroupId)) continue;
if (!fallbackGroupIdsToRemove.Contains(fcData.GroupId)) continue;
var ent = tr.GetObject(id, OpenMode.ForWrite, false) as Entity;
if (ent == null) continue;
if (!ent.IsErased) ent.Erase();
}
}
// 1) Update previously replaced placeholder entities using stored original template.
// 收集被替换的占位符文字实体ID以便后续检查框体
var replacedPlaceholderIds = new List<ObjectId>();
foreach (var id in ids)
{
var ent = tr.GetObject(id, OpenMode.ForWrite, false) as Entity;
if (ent == null) continue;
if (ent is DBText t)
{
FeatureCategoryData fcData = null;
try { fcData = GetFeatureCategoryData(tr, id); } catch { }
if (fcData != null && string.Equals(fcData.Role, "Placeholder", StringComparison.OrdinalIgnoreCase))
{
var original = fcData.OriginalTemplate ?? string.Empty;
t.TextString = original.Contains("******") ? original.Replace("******", textToShow) : textToShow;
count++;
replacedPlaceholderIds.Add(id);
continue;
}
if (t.TextString != null && t.TextString.Contains("******"))
{
var original = t.TextString;
t.TextString = original.Replace("******", textToShow);
try { SetFeatureCategoryData(tr, id, original, null, "Placeholder"); } catch { }
count++;
replacedPlaceholderIds.Add(id);
}
}
else if (ent is MText mt)
{
FeatureCategoryData fcData = null;
try { fcData = GetFeatureCategoryData(tr, id); } catch { }
if (fcData != null && string.Equals(fcData.Role, "Placeholder", StringComparison.OrdinalIgnoreCase))
{
var original = fcData.OriginalTemplate ?? string.Empty;
mt.Contents = original.Contains("******") ? original.Replace("******", textToShow) : textToShow;
count++;
replacedPlaceholderIds.Add(id);
continue;
}
if (mt.Contents != null && mt.Contents.Contains("******"))
{
var original = mt.Contents;
mt.Contents = original.Replace("******", textToShow);
try { SetFeatureCategoryData(tr, id, original, null, "Placeholder"); } catch { }
count++;
replacedPlaceholderIds.Add(id);
}
}
else if (ent is BlockReference br)
{
foreach (ObjectId attId in br.AttributeCollection)
{
var att = tr.GetObject(attId, OpenMode.ForWrite, false) as AttributeReference;
if (att == null) continue;
FeatureCategoryData fcData = null;
try { fcData = GetFeatureCategoryData(tr, attId); } catch { }
if (fcData != null && string.Equals(fcData.Role, "Placeholder", StringComparison.OrdinalIgnoreCase))
{
var original = fcData.OriginalTemplate ?? string.Empty;
att.TextString = original.Contains("******") ? original.Replace("******", textToShow) : textToShow;
count++;
replacedPlaceholderIds.Add(attId);
continue;
}
if (att.TextString != null && att.TextString.Contains("******"))
{
var original = att.TextString;
att.TextString = original.Replace("******", textToShow);
try { SetFeatureCategoryData(tr, attId, original, null, "Placeholder"); } catch { }
count++;
replacedPlaceholderIds.Add(attId);
}
}
}
}
logAction?.Invoke($"[DEBUG] 替换占位符数量: {replacedPlaceholderIds.Count}, count: {count}");
// 2) 如果有被替换的占位符且内容不为空(非"一般件"),检查并确保每个占位符都有框体
if (replacedPlaceholderIds.Count > 0 && !string.IsNullOrWhiteSpace(textToShow))
{
logAction?.Invoke($"[DEBUG] 进入框体检查逻辑textToShow='{textToShow}'");
// 获取实体列表用于框体检查
var spaceEntListForBox = ids
.Select(x =>
{
try { return tr.GetObject(x, OpenMode.ForRead, false) as Entity; }
catch { return null; }
})
.Where(e => e != null && !e.IsErased)
.ToList();
foreach (var placeholderId in replacedPlaceholderIds)
{
logAction?.Invoke($"[DEBUG] 调用 EnsureFeatureCategoryBox, placeholderId={placeholderId}");
EnsureFeatureCategoryBox(tr, db, space, spaceEntListForBox, placeholderId, logAction);
}
}
// If the category is blank ("一般件"), also try to remove previously inserted bottom-right category texts/boxes (legacy, unmarked).
if (string.IsNullOrWhiteSpace(textToShow))
{
try
{
var spaceEntList = ids
.Select(x =>
{
try { return tr.GetObject(x, OpenMode.ForRead, false) as Entity; }
catch { return null; }
})
.Where(e => e != null && !e.IsErased)
.ToList();
var frame = ComputeWhiteFrameExtentsFromEntities(tr, spaceEntList);
if (frame.HasValue)
{
var f = frame.Value;
var known = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "关键件", "重要件" };
var regionMinX = f.MaxPoint.X - 220.0;
var regionMaxX = f.MaxPoint.X + 5.0;
var regionMinY = f.MinPoint.Y - 5.0;
var regionMaxY = f.MinPoint.Y + 140.0;
var candidates = new List<Tuple<ObjectId, Extents3d>>();
foreach (var e in spaceEntList)
{
try
{
var ex = e.GeometricExtents;
if (ex.MaxPoint.X < regionMinX || ex.MinPoint.X > regionMaxX || ex.MaxPoint.Y < regionMinY || ex.MinPoint.Y > regionMaxY)
{
continue;
}
if (e is DBText dt)
{
var txt = (dt.TextString ?? string.Empty).Trim();
if (known.Contains(txt)) candidates.Add(Tuple.Create(e.ObjectId, ex));
}
else if (e is MText mtt)
{
var txt = ExtractMTextPlainText(mtt.Contents);
if (known.Contains(txt)) candidates.Add(Tuple.Create(e.ObjectId, ex));
}
}
catch { }
}
if (candidates.Count > 0)
{
foreach (var c in candidates)
{
try
{
var w = tr.GetObject(c.Item1, OpenMode.ForWrite, false) as Entity;
if (w != null && !w.IsErased) w.Erase();
}
catch { }
}
foreach (var e in spaceEntList)
{
if (e is Polyline pl)
{
try
{
if (!pl.Closed) continue;
var ex = pl.GeometricExtents;
var matchesAny = candidates.Any(c =>
ex.MinPoint.X <= c.Item2.MinPoint.X - 0.1
&& ex.MaxPoint.X >= c.Item2.MaxPoint.X + 0.1
&& ex.MinPoint.Y <= c.Item2.MinPoint.Y - 0.1
&& ex.MaxPoint.Y >= c.Item2.MaxPoint.Y + 0.1);
if (matchesAny)
{
var w = tr.GetObject(pl.ObjectId, OpenMode.ForWrite, false) as Entity;
if (w != null && !w.IsErased) w.Erase();
}
}
catch { }
}
}
}
}
}
catch
{
// ignore
}
return count;
}
// Fallback: If no placeholder found AND we have text to show (e.g. "关键件"), create it manually.
if (count == 0 && !string.IsNullOrWhiteSpace(textToShow))
{
// Try to find white frame to determine position
var spaceEntList = space.Cast<ObjectId>()
.Select(id =>
{
try { return tr.GetObject(id, OpenMode.ForRead, false) as Entity; }
catch { return null; }
})
.Where(e => e != null && !e.IsErased)
.ToList();
var frame = ComputeWhiteFrameExtentsFromEntities(tr, spaceEntList);
if (frame.HasValue)
{
var f = frame.Value;
// 1) Heuristic: update/cleanup existing (older) feature-category texts in the bottom-right area
var known = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "关键件", "重要件" };
var regionMinX = f.MaxPoint.X - 220.0;
var regionMaxX = f.MaxPoint.X + 5.0;
var regionMinY = f.MinPoint.Y - 5.0;
var regionMaxY = f.MinPoint.Y + 140.0;
var candidates = new List<Tuple<ObjectId, Extents3d, string>>();
foreach (var ent in spaceEntList)
{
if (ent == null) continue;
try
{
var ex = ent.GeometricExtents;
if (ex.MaxPoint.X < regionMinX || ex.MinPoint.X > regionMaxX || ex.MaxPoint.Y < regionMinY || ex.MinPoint.Y > regionMaxY)
{
continue;
}
if (ent is DBText dt)
{
var txt = (dt.TextString ?? string.Empty).Trim();
if (known.Contains(txt))
{
candidates.Add(Tuple.Create(ent.ObjectId, ex, txt));
}
}
else if (ent is MText mtt)
{
var txt = ExtractMTextPlainText(mtt.Contents);
if (known.Contains(txt))
{
candidates.Add(Tuple.Create(ent.ObjectId, ex, txt));
}
}
}
catch
{
// ignore
}
}
if (candidates.Count > 0)
{
// Keep the right-most (then top-most) one, erase the rest.
var keep = candidates
.OrderByDescending(c => c.Item2.MaxPoint.X)
.ThenByDescending(c => c.Item2.MaxPoint.Y)
.First();
var keepId = keep.Item1;
var keepExt = keep.Item2;
foreach (var c in candidates)
{
if (c.Item1 == keepId) continue;
try
{
var e = tr.GetObject(c.Item1, OpenMode.ForWrite, false) as Entity;
if (e != null && !e.IsErased) e.Erase();
}
catch { }
}
// Remove boxes that belong to removed texts (keep the one containing kept text)
foreach (var ent in spaceEntList)
{
if (ent is Polyline pl)
{
try
{
if (!pl.Closed) continue;
var ex = pl.GeometricExtents;
var containsKept = ex.MinPoint.X <= keepExt.MinPoint.X - 0.1
&& ex.MaxPoint.X >= keepExt.MaxPoint.X + 0.1
&& ex.MinPoint.Y <= keepExt.MinPoint.Y - 0.1
&& ex.MaxPoint.Y >= keepExt.MaxPoint.Y + 0.1;
if (containsKept) continue;
var matchesAnyRemoved = candidates.Any(c => c.Item1 != keepId
&& ex.MinPoint.X <= c.Item2.MinPoint.X - 0.1
&& ex.MaxPoint.X >= c.Item2.MaxPoint.X + 0.1
&& ex.MinPoint.Y <= c.Item2.MinPoint.Y - 0.1
&& ex.MaxPoint.Y >= c.Item2.MaxPoint.Y + 0.1);
if (matchesAnyRemoved)
{
var w = tr.GetObject(pl.ObjectId, OpenMode.ForWrite, false) as Entity;
if (w != null && !w.IsErased) w.Erase();
}
}
catch { }
}
}
// Update kept text and mark it so next run won't fall back.
try
{
var e = tr.GetObject(keepId, OpenMode.ForWrite, false) as Entity;
if (e is DBText dt)
{
dt.TextString = textToShow;
SetFeatureCategoryData(tr, keepId, "******", null, "Placeholder");
// 检查是否有包围该文字的框体,如果没有则创建
EnsureFeatureCategoryBox(tr, db, space, spaceEntList, keepId, logAction);
return 1;
}
if (e is MText mtt)
{
// Preserve its formatting if any; otherwise use our default.
var plain = ExtractMTextPlainText(mtt.Contents);
if (string.Equals(plain, mtt.Contents, StringComparison.Ordinal))
{
mtt.Contents = @"{\fSimSun|b0|i0|c134|p2;" + ToMTextContents(textToShow) + "}";
}
else
{
mtt.Contents = mtt.Contents.Replace(plain, textToShow);
}
SetFeatureCategoryData(tr, keepId, "******", null, "Placeholder");
// 检查是否有包围该文字的框体,如果没有则创建
EnsureFeatureCategoryBox(tr, db, space, spaceEntList, keepId, logAction);
return 1;
}
}
catch
{
// ignore
}
}
// Strategy:
// 1. Align Right: Use f.MaxX as the right boundary anchor.
// 2. Find Table Top: Look for horizontal lines in the bottom-right quadrant to find the top of the title block.
// If found, place text above that line. If not, use a safe default from bottom.
var searchRegionMinX = f.MaxPoint.X - 200.0; // Assume title block width < 200
var searchRegionMinY = f.MinPoint.Y;
var searchRegionMaxY = f.MinPoint.Y + 80.0; // Reduce to avoid picking up drawing lines (Title block usually < 80mm)
double tableTopY = f.MinPoint.Y + 60.0; // Default fallback (e.g. 60mm from bottom)
// Scan lines to find the highest horizontal line in this region which represents the table top
var candidatesY = new List<double>();
foreach (var ent in spaceEntList)
{
if (ent is Line ln)
{
if (ln.StartPoint.X > searchRegionMinX && ln.EndPoint.X > searchRegionMinX &&
ln.StartPoint.Y > searchRegionMinY && ln.StartPoint.Y < searchRegionMaxY &&
Math.Abs(ln.StartPoint.Y - ln.EndPoint.Y) < 1.0) // Horizontal
{
candidatesY.Add(ln.StartPoint.Y);
}
}
else if (ent is Polyline pl)
{
for (int i = 0; i < pl.NumberOfVertices; i++)
{
var pt = pl.GetPoint3dAt(i);
if (pt.X > searchRegionMinX && pt.Y > searchRegionMinY && pt.Y < searchRegionMaxY)
{
candidatesY.Add(pt.Y);
}
}
}
}
if (candidatesY.Count > 0)
{
// The "Title Block" usually consists of many lines.
// We want the top-most line of the block, but "below" the drawing area.
// Let's take the max Y found in that bottom-right corner zone.
tableTopY = candidatesY.Max();
}
// Position:
// X: f.MaxX - 5 (margin) - (TextWidth/2 treated by Attachment) => Let's use TopRight or MiddleRight alignment
// Anchor at f.MaxX - 2.0 (small margin from right border line)
// Y: tableTopY + 2.0 (small gap above the table line)
// Anchor BottomRight or BottomLeft?
// User image shows text centered in a box-like area ABOVE the table rows.
// Let's align BottomRight to (MaxX - margin, TableTop + margin)
var insertX = f.MaxPoint.X - 5.0;
var insertY = tableTopY + 2.0;
var insertPoint = new Point3d(insertX, insertY, 0);
var groupId = Guid.NewGuid().ToString("N");
var textHeight = 3.2; // 9号字 (interpreted as 9pt approx 3.2mm)
var mt = new MText();
// Set font to SimSun (宋体)
mt.Contents = @"{\fSimSun|b0|i0|c134|p2;" + ToMTextContents(textToShow) + "}";
mt.Location = insertPoint;
mt.TextHeight = textHeight;
mt.Attachment = AttachmentPoint.BottomRight; // Anchor at bottom-right
mt.Width = 0; // No wrap width, auto
mt.ColorIndex = 7; // White
// Create White Border
// Estimate dimensions: Chinese char width approx equals height
// Adding padding to visually center the text in the box
double charWidthFactor = 1.0;
double estimatedTextWidth = textToShow.Length * textHeight * charWidthFactor;
double paddingH = 1.5; // Horizontal padding
double paddingV = 1.5; // Vertical padding
// Since Attachment is BottomRight:
// Text occupies roughly: [X - Width, X] x [Y, Y + Height]
// We draw box around this area with padding
var boxMinX = insertX - estimatedTextWidth - paddingH;
var boxMaxX = insertX + paddingH;
var boxMinY = insertY - paddingV;
var boxMaxY = insertY + textHeight + paddingV;
var poly = new Polyline();
poly.AddVertexAt(0, new Point2d(boxMinX, boxMinY), 0, 0, 0);
poly.AddVertexAt(1, new Point2d(boxMaxX, boxMinY), 0, 0, 0);
poly.AddVertexAt(2, new Point2d(boxMaxX, boxMaxY), 0, 0, 0);
poly.AddVertexAt(3, new Point2d(boxMinX, boxMaxY), 0, 0, 0);
poly.Closed = true;
poly.ColorIndex = 7; // White (Index 7 is White/Black depending on bg, usually White for plot)
try
{
var layerTbl = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
string targetLayer = "0";
if (layerTbl.Has("TEXT")) targetLayer = "TEXT";
else if (layerTbl.Has("文字")) targetLayer = "文字";
// Assign layer to both MText and Box
mt.Layer = targetLayer;
poly.Layer = targetLayer;
}
catch { }
space.AppendEntity(poly);
tr.AddNewlyCreatedDBObject(poly, true);
try { SetFeatureCategoryData(tr, poly.ObjectId, string.Empty, groupId, "FallbackBox"); } catch { }
space.AppendEntity(mt);
tr.AddNewlyCreatedDBObject(mt, true);
try { SetFeatureCategoryData(tr, mt.ObjectId, string.Empty, groupId, "FallbackText"); } catch { }
count++;
}
}
return count;
}
public static void KeepOnlyLayout(Document doc, string layoutName)
{
if (doc == null)
{
throw new ArgumentNullException(nameof(doc));
}
if (string.IsNullOrWhiteSpace(layoutName))
{
return;
}
using (doc.LockDocument())
{
var db = doc.Database;
var prevDb = HostApplicationServices.WorkingDatabase;
HostApplicationServices.WorkingDatabase = db;
try
{
List<string> layoutNames;
using (var tr = db.TransactionManager.StartTransaction())
{
var layoutDict = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead);
layoutNames = layoutDict.Cast<DBDictionaryEntry>().Select(e => e.Key).ToList();
tr.Commit();
}
if (!layoutNames.Any(n => string.Equals(n, layoutName, StringComparison.OrdinalIgnoreCase)))
{
return;
}
var lm = LayoutManager.Current;
lm.CurrentLayout = layoutName;
foreach (var name in layoutNames)
{
if (string.Equals(name, "Model", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (string.Equals(name, layoutName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
try
{
lm.DeleteLayout(name);
}
catch
{
// ignore
}
}
}
finally
{
HostApplicationServices.WorkingDatabase = prevDb;
}
}
}
public static void KeepOnlyModelWindow(Document doc, Extents3d window)
{
if (doc == null)
{
throw new ArgumentNullException(nameof(doc));
}
using (doc.LockDocument())
{
var db = doc.Database;
using (var tr = db.TransactionManager.StartTransaction())
{
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
var erased = 0;
var kept = 0;
foreach (ObjectId id in ms)
{
var ent = tr.GetObject(id, OpenMode.ForWrite) as Entity;
if (ent == null)
{
continue;
}
try
{
var ext = ent.GeometricExtents;
if (!Intersects(ext, window))
{
ent.Erase(true);
erased++;
}
else
{
kept++;
}
}
catch
{
// 无法获取范围的对象默认保留,避免误删关键对象
kept++;
}
}
tr.Commit();
}
}
}
private static bool Intersects(Extents3d a, Extents3d b)
{
return a.MinPoint.X <= b.MaxPoint.X
&& a.MaxPoint.X >= b.MinPoint.X
&& a.MinPoint.Y <= b.MaxPoint.Y
&& a.MaxPoint.Y >= b.MinPoint.Y;
}
public sealed class RemoveOriginalDrawingResult
{
public int CaxaLayerErased { get; set; }
public int DimensionLayerErased { get; set; }
public int DimensionLayerKept { get; set; }
public int OuterFrameErased { get; set; }
public Point3d OriginalCenter { get; set; }
public Extents3d? OriginalExtents { get; set; }
public Extents3d? WhiteFrameExtents { get; set; }
}
private static readonly Regex DimensionPlaceholderRegex = new Regex(
@"(%%c|[Φφ∅Ø]|\\U\+03A6|\\U\+2205|\\U\+00D8)\s*\*",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// 删除模板中原有的图纸图形,保留右上角区域的内容(如粗糙度标注)。
/// </summary>
/// <param name="ctx">CAD上下文</param>
/// <param name="topRightThreshold">右上角保留区域阈值0.70表示X和Y都超过70%的位置</param>
/// <returns>删除结果,包含原图形中心点位置</returns>
public static RemoveOriginalDrawingResult RemoveTemplateOriginalDrawing(CadContext ctx, double topRightThreshold = 0.70)
{
if (ctx == null)
{
throw new ArgumentNullException(nameof(ctx));
}
var result = new RemoveOriginalDrawingResult();
var db = ctx.Database;
var tr = ctx.Transaction;
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
var allEntities = ms.Cast<ObjectId>()
.Select(id => tr.GetObject(id, OpenMode.ForRead, false) as Entity)
.Where(e => e != null)
.ToList();
var layoutExtents = ComputeLayoutExtents(allEntities);
var caxaExtents = ComputeCaxaLayerExtents(tr, allEntities);
// Calculate white frame extents
var whiteFrameExtents = ComputeWhiteFrameExtentsFromEntities(tr, allEntities) ?? ComputeLayoutExtents(allEntities);
result.WhiteFrameExtents = whiteFrameExtents;
var titleBlockExtents = whiteFrameExtents.HasValue
? ComputeTitleBlockExtentsFromEntities(tr, allEntities, whiteFrameExtents.Value)
: (Extents3d?)null;
if (caxaExtents.HasValue)
{
result.OriginalExtents = caxaExtents;
result.OriginalCenter = new Point3d(
(caxaExtents.Value.MinPoint.X + caxaExtents.Value.MaxPoint.X) / 2,
(caxaExtents.Value.MinPoint.Y + caxaExtents.Value.MaxPoint.Y) / 2,
0);
}
else if (whiteFrameExtents.HasValue)
{
// Fallback to center of the upper half of the white frame if no CAXA layer found
var frame = whiteFrameExtents.Value;
var centerX = (frame.MinPoint.X + frame.MaxPoint.X) / 2.0;
var height = frame.MaxPoint.Y - frame.MinPoint.Y;
// Default to 70% height (center of upper area approximately) or just center?
// User said "upper half center". Top half is [0.5, 1.0], center is 0.75.
var centerY = frame.MinPoint.Y + height * 0.75;
result.OriginalCenter = new Point3d(centerX, centerY, 0);
}
// 使用CAXA图层范围作为图形区域用于判断右上角
var graphicExtents = caxaExtents ?? layoutExtents;
// 先计算红色线框的范围
var redFrameExtents = ComputeRedFrameExtents(tr, allEntities);
foreach (var ent in allEntities)
{
if (ent.IsErased)
{
continue;
}
var layerName = GetLayerName(tr, ent.LayerId);
// 删除红色外框线红色的Line/Polyline且在红色框的边界上
if (IsOuterFrameEntity(ent, redFrameExtents, tr))
{
// [Modified] Skip erasing outer frame to keep the template's original frame and title block
// Explicitly continue to prevent it being caught by subsequent checks (e.g. CAXA layer)
continue;
/* Original deletion logic:
var entForWrite = tr.GetObject(ent.ObjectId, OpenMode.ForWrite) as Entity;
if (entForWrite != null)
{
entForWrite.Erase(true);
result.OuterFrameErased++;
}
continue;
*/
}
if (IsWithinTitleBlock(ent, titleBlockExtents))
{
continue;
}
if (IsCaxaLayer(layerName))
{
var entForWrite = tr.GetObject(ent.ObjectId, OpenMode.ForWrite) as Entity;
if (entForWrite != null)
{
entForWrite.Erase(true);
result.CaxaLayerErased++;
}
continue;
}
if (IsDimensionLayer(layerName) || IsDimensionEntity(ent))
{
if (IsWithinTitleBlock(ent, titleBlockExtents))
{
result.DimensionLayerKept++;
continue;
}
// 基于CAXA图形区域判断是否在右上角
if (graphicExtents.HasValue && IsInTopRightCorner(ent, graphicExtents.Value, topRightThreshold))
{
result.DimensionLayerKept++;
continue;
}
var entForWrite = tr.GetObject(ent.ObjectId, OpenMode.ForWrite) as Entity;
if (entForWrite != null)
{
entForWrite.Erase(true);
result.DimensionLayerErased++;
}
}
}
try
{
// Keep the template's white frame but enforce the desired lineweight.
ApplyWhiteFrameLineWeight(ctx, LineWeight.LineWeight015);
}
catch
{
// ignore
}
return result;
}
private static int ApplyWhiteFrameLineWeight(CadContext ctx, LineWeight lineWeight)
{
if (ctx == null)
{
return 0;
}
var db = ctx.Database;
var tr = ctx.Transaction;
var frameExtents = ComputeWhiteFrameExtents(ctx);
if (!frameExtents.HasValue)
{
return 0;
}
var frame = frameExtents.Value;
var w = frame.MaxPoint.X - frame.MinPoint.X;
var h = frame.MaxPoint.Y - frame.MinPoint.Y;
var tol = Math.Max(w, h) * 0.01; // 1%
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
var updated = 0;
foreach (ObjectId id in ms)
{
var ent = tr.GetObject(id, OpenMode.ForWrite, false) as Entity;
if (ent == null || ent.IsErased)
{
continue;
}
if (!(ent is Line) && !(ent is Polyline))
{
continue;
}
if (!IsWhiteColor(ent, tr))
{
continue;
}
if (!IsOnFrameBoundary(ent, frame, tol))
{
continue;
}
try { ent.LineWeight = lineWeight; } catch { }
try
{
var layer = tr.GetObject(ent.LayerId, OpenMode.ForWrite) as LayerTableRecord;
if (layer != null)
{
layer.LineWeight = lineWeight;
}
}
catch
{
// ignore
}
updated++;
}
return updated;
}
private static bool IsOnFrameBoundary(Entity ent, Extents3d frame, double tol)
{
try
{
if (ent is Line line)
{
var sp = line.StartPoint;
var ep = line.EndPoint;
var minX = Math.Min(sp.X, ep.X);
var maxX = Math.Max(sp.X, ep.X);
var minY = Math.Min(sp.Y, ep.Y);
var maxY = Math.Max(sp.Y, ep.Y);
var isHorizontal = Math.Abs(sp.Y - ep.Y) <= tol;
if (isHorizontal)
{
var y = (sp.Y + ep.Y) / 2.0;
var onTop = Math.Abs(y - frame.MaxPoint.Y) <= tol;
var onBottom = Math.Abs(y - frame.MinPoint.Y) <= tol;
if ((onTop || onBottom)
&& minX <= frame.MinPoint.X + tol
&& maxX >= frame.MaxPoint.X - tol)
{
return true;
}
}
var isVertical = Math.Abs(sp.X - ep.X) <= tol;
if (isVertical)
{
var x = (sp.X + ep.X) / 2.0;
var onLeft = Math.Abs(x - frame.MinPoint.X) <= tol;
var onRight = Math.Abs(x - frame.MaxPoint.X) <= tol;
if ((onLeft || onRight)
&& minY <= frame.MinPoint.Y + tol
&& maxY >= frame.MaxPoint.Y - tol)
{
return true;
}
}
return false;
}
var ext = ent.GeometricExtents;
var matchX = Math.Abs(ext.MinPoint.X - frame.MinPoint.X) <= tol && Math.Abs(ext.MaxPoint.X - frame.MaxPoint.X) <= tol;
var matchY = Math.Abs(ext.MinPoint.Y - frame.MinPoint.Y) <= tol && Math.Abs(ext.MaxPoint.Y - frame.MaxPoint.Y) <= tol;
return matchX && matchY;
}
catch
{
return false;
}
}
/// <summary>
/// 删除模板中残留的“尺寸占位符”文本(例如 (Φ*)),避免与新生成的真实尺寸标注混淆。
/// </summary>
/// <remarks>
/// 仅清理文本类实体DBText/MText/块属性),不影响真正的 Dimension 实体。
/// </remarks>
public static int RemoveTemplateDimensionPlaceholderTexts(
CadContext ctx,
Extents3d? graphicExtents = null,
double topRightThreshold = 0.70,
bool scanBlockDefinitions = false)
{
if (ctx == null)
{
throw new ArgumentNullException(nameof(ctx));
}
var db = ctx.Database;
var tr = ctx.Transaction;
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
var allEntities = ms.Cast<ObjectId>()
.Select(id => tr.GetObject(id, OpenMode.ForRead, false) as Entity)
.Where(e => e != null)
.ToList();
var targetExtents = graphicExtents
?? ComputeWhiteFrameExtentsFromEntities(tr, allEntities)
?? ComputeLayoutExtents(allEntities);
var titleBlockExtents = targetExtents.HasValue
? ComputeTitleBlockExtentsFromEntities(tr, allEntities, targetExtents.Value)
: (Extents3d?)null;
var removed = 0;
foreach (ObjectId id in ms)
{
var ent = tr.GetObject(id, OpenMode.ForWrite, false) as Entity;
if (ent == null || ent.IsErased)
{
continue;
}
if (targetExtents.HasValue && IsInTopRightCorner(ent, targetExtents.Value, topRightThreshold))
{
continue;
}
if (IsWithinTitleBlock(ent, titleBlockExtents))
{
continue;
}
if (!IsWithin(ent, targetExtents))
{
continue;
}
if (ent is DBText t)
{
if (LooksLikeDimensionPlaceholder(t.TextString))
{
ent.Erase(true);
removed++;
}
continue;
}
if (ent is MText mt)
{
var plain = SimplifyMText(mt.Contents);
if (LooksLikeDimensionPlaceholder(plain) || LooksLikeDimensionPlaceholder(mt.Contents))
{
ent.Erase(true);
removed++;
}
continue;
}
if (ent is BlockReference br)
{
removed += RemovePlaceholderAttributes(tr, br, targetExtents, topRightThreshold);
// Keep default behavior conservative: do not edit block definitions unless explicitly enabled.
if (scanBlockDefinitions && IsWithin(br, targetExtents))
{
removed += RemovePlaceholderTextsInBlockDefinition(tr, br.BlockTableRecord, new HashSet<ObjectId>());
}
}
}
return removed;
}
private static int RemovePlaceholderAttributes(Transaction tr, BlockReference br, Extents3d? targetExtents, double topRightThreshold)
{
if (tr == null || br == null)
{
return 0;
}
var removed = 0;
foreach (ObjectId attId in br.AttributeCollection)
{
var att = tr.GetObject(attId, OpenMode.ForWrite, false) as AttributeReference;
if (att == null || att.IsErased)
{
continue;
}
if (targetExtents.HasValue && IsInTopRightCorner(att, targetExtents.Value, topRightThreshold))
{
continue;
}
if (!IsWithin(att, targetExtents))
{
continue;
}
if (LooksLikeDimensionPlaceholder(att.TextString))
{
try
{
att.Erase(true);
removed++;
}
catch
{
// ignore
}
}
}
return removed;
}
private static int RemovePlaceholderTextsInBlockDefinition(Transaction tr, ObjectId blockId, HashSet<ObjectId> visited)
{
if (tr == null || blockId.IsNull)
{
return 0;
}
if (visited == null)
{
visited = new HashSet<ObjectId>();
}
if (visited.Contains(blockId))
{
return 0;
}
visited.Add(blockId);
var btr = tr.GetObject(blockId, OpenMode.ForWrite, false) as BlockTableRecord;
if (btr == null)
{
return 0;
}
var removed = 0;
foreach (ObjectId childId in btr)
{
var child = tr.GetObject(childId, OpenMode.ForWrite, false) as Entity;
if (child == null || child.IsErased)
{
continue;
}
if (child is DBText t)
{
if (LooksLikeDimensionPlaceholder(t.TextString))
{
child.Erase(true);
removed++;
}
continue;
}
if (child is MText mt)
{
var plain = SimplifyMText(mt.Contents);
if (LooksLikeDimensionPlaceholder(plain) || LooksLikeDimensionPlaceholder(mt.Contents))
{
child.Erase(true);
removed++;
}
continue;
}
if (child is BlockReference br)
{
removed += RemovePlaceholderAttributes(tr, br, null, 1.0);
removed += RemovePlaceholderTextsInBlockDefinition(tr, br.BlockTableRecord, visited);
}
}
return removed;
}
private static bool LooksLikeDimensionPlaceholder(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
{
return false;
}
var s = (raw ?? string.Empty).Replace(" ", string.Empty).Replace(" ", string.Empty);
if (!s.Contains("*"))
{
return false;
}
// Typical placeholder examples: (Φ*), Φ*, %%c*, (\U+03A6*)
return DimensionPlaceholderRegex.IsMatch(s);
}
/// <summary>
/// 删除白色线框范围内上半部分的所有内容(用于清理模板残留的图形/占位符)。
/// 保留:右上角区域(如粗糙度标注)与下半部分(附注/表格)。
/// </summary>
/// <param name="keepBottomRatio">保留下半部比例0.5 表示保留下半部分)</param>
/// <param name="topRightThreshold">右上角保留阈值0.70表示X和Y都超过70%的位置</param>
public static int RemoveWhiteFrameUpperContent(CadContext ctx, double keepBottomRatio = 0.50, double topRightThreshold = 0.70)
{
if (ctx == null)
{
throw new ArgumentNullException(nameof(ctx));
}
keepBottomRatio = Math.Max(0.0, Math.Min(1.0, keepBottomRatio));
var db = ctx.Database;
var tr = ctx.Transaction;
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
var allEntities = ms.Cast<ObjectId>()
.Select(id => tr.GetObject(id, OpenMode.ForRead, false) as Entity)
.Where(e => e != null && !e.IsErased)
.ToList();
var whiteFrame = ComputeWhiteFrameExtentsFromEntities(tr, allEntities) ?? ComputeLayoutExtents(allEntities);
if (!whiteFrame.HasValue)
{
return 0;
}
var titleBlockExtents = ComputeTitleBlockExtentsFromEntities(tr, allEntities, whiteFrame.Value);
var frame = whiteFrame.Value;
var height = frame.MaxPoint.Y - frame.MinPoint.Y;
if (height <= 0)
{
return 0;
}
var cutY = frame.MinPoint.Y + height * keepBottomRatio;
// [Modified] Compute Red Frame Extents to protect it from deletion
var redFrameExtents = ComputeRedFrameExtents(tr, allEntities);
var removed = 0;
foreach (ObjectId id in ms)
{
var ent = tr.GetObject(id, OpenMode.ForWrite, false) as Entity;
if (ent == null || ent.IsErased)
{
continue;
}
// Keep white frame boundary itself.
if (IsWhiteFrameEntity(ent, frame, tr))
{
continue;
}
// [Modified] Keep Red Outer Frame
if (IsOuterFrameEntity(ent, redFrameExtents, tr))
{
continue;
}
if (!IsWithin(ent, frame))
{
continue;
}
// Keep top-right corner content.
if (IsInTopRightCorner(ent, frame, topRightThreshold))
{
continue;
}
if (IsWithinTitleBlock(ent, titleBlockExtents))
{
continue;
}
if (!TryGetEntityCenterY(ent, out var centerY))
{
continue;
}
// Keep bottom area (notes/table).
if (centerY < cutY)
{
continue;
}
try
{
ent.Erase(true);
removed++;
}
catch
{
// ignore
}
}
return removed;
}
private static bool TryGetEntityCenterY(Entity ent, out double centerY)
{
centerY = 0;
if (ent == null)
{
return false;
}
try
{
var ext = ent.GeometricExtents;
centerY = (ext.MinPoint.Y + ext.MaxPoint.Y) / 2.0;
return true;
}
catch
{
return false;
}
}
private static bool IsWithin(Entity ent, Extents3d frame)
{
if (ent == null)
{
return false;
}
try
{
var ext = ent.GeometricExtents;
return Intersects(ext, frame);
}
catch
{
return true;
}
}
private static bool IsWhiteFrameEntity(Entity ent, Extents3d frame, Transaction tr)
{
if (ent == null)
{
return false;
}
if (!(ent is Line) && !(ent is Polyline))
{
return false;
}
if (!IsWhiteColor(ent, tr))
{
return false;
}
try
{
var ext = ent.GeometricExtents;
var frameW = frame.MaxPoint.X - frame.MinPoint.X;
var frameH = frame.MaxPoint.Y - frame.MinPoint.Y;
var tol = Math.Max(frameW, frameH) * 0.01; // 1%
// On outer border lines.
var nearLeft = Math.Abs(ext.MinPoint.X - frame.MinPoint.X) < tol;
var nearRight = Math.Abs(ext.MaxPoint.X - frame.MaxPoint.X) < tol;
var nearBottom = Math.Abs(ext.MinPoint.Y - frame.MinPoint.Y) < tol;
var nearTop = Math.Abs(ext.MaxPoint.Y - frame.MaxPoint.Y) < tol;
// Rough heuristic: if it touches two sides or spans most of a side, treat as frame.
if ((nearLeft && nearBottom) || (nearLeft && nearTop) || (nearRight && nearBottom) || (nearRight && nearTop))
{
return true;
}
if (ent is Line)
{
var spansWidth = (ext.MaxPoint.X - ext.MinPoint.X) > frameW * 0.90;
var spansHeight = (ext.MaxPoint.Y - ext.MinPoint.Y) > frameH * 0.90;
if ((nearTop || nearBottom) && spansWidth)
{
return true;
}
if ((nearLeft || nearRight) && spansHeight)
{
return true;
}
}
if (ent is Polyline)
{
var spansWidth = (ext.MaxPoint.X - ext.MinPoint.X) > frameW * 0.90;
var spansHeight = (ext.MaxPoint.Y - ext.MinPoint.Y) > frameH * 0.90;
if (spansWidth && spansHeight)
{
return true;
}
}
}
catch
{
return false;
}
return false;
}
private static bool IsWithin(Entity ent, Extents3d? targetExtents)
{
if (ent == null)
{
return false;
}
if (!targetExtents.HasValue)
{
return true;
}
try
{
var ext = ent.GeometricExtents;
return Intersects(ext, targetExtents.Value);
}
catch
{
return true;
}
}
private static bool IsOuterFrameEntity(Entity ent, Extents3d? layoutExtents, Transaction tr = null)
{
if (ent == null || !layoutExtents.HasValue)
{
return false;
}
// 必须是Line或Polyline
if (!(ent is Line) && !(ent is Polyline))
{
return false;
}
// 检查颜色是否为红色ColorIndex=1或者通过图层颜色
if (!IsRedColor(ent, tr))
{
return false;
}
// 检查是否位于最外围(实体的范围接近或等于整体布局范围)
try
{
var ext = ent.GeometricExtents;
var layout = layoutExtents.Value;
var layoutWidth = layout.MaxPoint.X - layout.MinPoint.X;
var layoutHeight = layout.MaxPoint.Y - layout.MinPoint.Y;
// 容差值放宽到5%
var tolerance = Math.Max(layoutWidth, layoutHeight) * 0.05;
// 检查Line是否位于布局边界上
if (ent is Line line)
{
var startPt = line.StartPoint;
var endPt = line.EndPoint;
// 检查是否是水平线(上边或下边)
var isHorizontal = Math.Abs(startPt.Y - endPt.Y) < tolerance;
if (isHorizontal)
{
// 检查Y坐标是否在布局的上边界或下边界
var isTopEdge = Math.Abs(startPt.Y - layout.MaxPoint.Y) < tolerance;
var isBottomEdge = Math.Abs(startPt.Y - layout.MinPoint.Y) < tolerance;
if (isTopEdge || isBottomEdge)
{
// 增加长度判断:防止误删位于边界上的短线(如标题栏边框)
// 只有长度超过布局宽度 50% 的线才认为是外框
if (Math.Abs(startPt.X - endPt.X) > layoutWidth * 0.5)
{
return true;
}
}
}
// 检查是否是垂直线(左边或右边)
var isVertical = Math.Abs(startPt.X - endPt.X) < tolerance;
if (isVertical)
{
// 检查X坐标是否在布局的左边界或右边界
var isLeftEdge = Math.Abs(startPt.X - layout.MinPoint.X) < tolerance;
var isRightEdge = Math.Abs(startPt.X - layout.MaxPoint.X) < tolerance;
if (isLeftEdge || isRightEdge)
{
// 增加长度判断
if (Math.Abs(startPt.Y - endPt.Y) > layoutHeight * 0.5)
{
return true;
}
}
}
}
// Polyline可能是完整的矩形外框
if (ent is Polyline poly)
{
var entWidth = ext.MaxPoint.X - ext.MinPoint.X;
var entHeight = ext.MaxPoint.Y - ext.MinPoint.Y;
if (entWidth > layoutWidth * 0.90 && entHeight > layoutHeight * 0.90)
{
return true;
}
}
}
catch
{
return false;
}
return false;
}
private static bool IsRedColor(Entity ent, Transaction tr)
{
if (ent == null)
{
return false;
}
// 检查实体自身颜色
var colorIndex = ent.ColorIndex;
// ColorIndex=1 是红色
if (colorIndex == 1)
{
return true;
}
// ColorIndex=256 表示 ByLayer需要检查图层颜色
if (colorIndex == 256 && tr != null)
{
try
{
var layer = tr.GetObject(ent.LayerId, OpenMode.ForRead) as LayerTableRecord;
if (layer != null && layer.Color.ColorIndex == 1)
{
return true;
}
}
catch
{
// 忽略
}
}
// 检查Color属性
try
{
var color = ent.Color;
if (color != null)
{
// 红色的RGB大约是(255, 0, 0)或接近
if (color.ColorIndex == 1)
{
return true;
}
if (color.ColorMethod == Autodesk.AutoCAD.Colors.ColorMethod.ByColor)
{
var r = color.Red;
var g = color.Green;
var b = color.Blue;
// 红色R高G和B低
if (r > 200 && g < 100 && b < 100)
{
return true;
}
}
}
}
catch
{
// 忽略
}
return false;
}
private static string GetLayerName(Transaction tr, ObjectId layerId)
{
if (layerId.IsNull)
{
return string.Empty;
}
try
{
var layer = tr.GetObject(layerId, OpenMode.ForRead) as LayerTableRecord;
return layer?.Name ?? string.Empty;
}
catch
{
return string.Empty;
}
}
private static bool IsCaxaLayer(string layerName)
{
if (string.IsNullOrWhiteSpace(layerName))
{
return false;
}
return CaxaLayerRegex.IsMatch(layerName);
}
private static bool IsDimensionLayer(string layerName)
{
if (string.IsNullOrWhiteSpace(layerName))
{
return false;
}
return DimensionLayerNames.Contains(layerName);
}
private static bool IsDimensionEntity(Entity ent)
{
return ent is Dimension
|| ent is RotatedDimension
|| ent is AlignedDimension
|| ent is RadialDimension
|| ent is DiametricDimension
|| ent is ArcDimension
|| ent is OrdinateDimension
|| ent is Leader;
}
private static Extents3d? ComputeLayoutExtents(IEnumerable<Entity> entities)
{
Extents3d? result = null;
foreach (var ent in entities)
{
if (ent == null || ent.IsErased)
{
continue;
}
try
{
var ext = ent.GeometricExtents;
if (result == null)
{
result = ext;
}
else
{
result = new Extents3d(
new Point3d(
Math.Min(result.Value.MinPoint.X, ext.MinPoint.X),
Math.Min(result.Value.MinPoint.Y, ext.MinPoint.Y),
Math.Min(result.Value.MinPoint.Z, ext.MinPoint.Z)),
new Point3d(
Math.Max(result.Value.MaxPoint.X, ext.MaxPoint.X),
Math.Max(result.Value.MaxPoint.Y, ext.MaxPoint.Y),
Math.Max(result.Value.MaxPoint.Z, ext.MaxPoint.Z)));
}
}
catch
{
// 忽略无法获取范围的实体
}
}
return result;
}
private static Extents3d? ComputeRedFrameExtents(Transaction tr, IEnumerable<Entity> entities)
{
Extents3d? result = null;
foreach (var ent in entities)
{
if (ent == null || ent.IsErased)
{
continue;
}
// 只处理红色的Line或Polyline
if (!(ent is Line) && !(ent is Polyline))
{
continue;
}
if (!IsRedColor(ent, tr))
{
continue;
}
try
{
var ext = ent.GeometricExtents;
if (result == null)
{
result = ext;
}
else
{
result = new Extents3d(
new Point3d(
Math.Min(result.Value.MinPoint.X, ext.MinPoint.X),
Math.Min(result.Value.MinPoint.Y, ext.MinPoint.Y),
Math.Min(result.Value.MinPoint.Z, ext.MinPoint.Z)),
new Point3d(
Math.Max(result.Value.MaxPoint.X, ext.MaxPoint.X),
Math.Max(result.Value.MaxPoint.Y, ext.MaxPoint.Y),
Math.Max(result.Value.MaxPoint.Z, ext.MaxPoint.Z)));
}
}
catch
{
// 忽略
}
}
return result;
}
private static Extents3d? ComputeCaxaLayerExtents(Transaction tr, IEnumerable<Entity> entities)
{
Extents3d? result = null;
foreach (var ent in entities)
{
if (ent == null || ent.IsErased)
{
continue;
}
var layerName = GetLayerName(tr, ent.LayerId);
if (!IsCaxaLayer(layerName))
{
continue;
}
try
{
var ext = ent.GeometricExtents;
if (result == null)
{
result = ext;
}
else
{
result = new Extents3d(
new Point3d(
Math.Min(result.Value.MinPoint.X, ext.MinPoint.X),
Math.Min(result.Value.MinPoint.Y, ext.MinPoint.Y),
Math.Min(result.Value.MinPoint.Z, ext.MinPoint.Z)),
new Point3d(
Math.Max(result.Value.MaxPoint.X, ext.MaxPoint.X),
Math.Max(result.Value.MaxPoint.Y, ext.MaxPoint.Y),
Math.Max(result.Value.MaxPoint.Z, ext.MaxPoint.Z)));
}
}
catch
{
// 忽略
}
}
return result;
}
private static Extents3d? ComputeTitleBlockExtentsFromEntities(Transaction tr, IEnumerable<Entity> entities, Extents3d frame)
{
if (entities == null)
{
return null;
}
var w = frame.MaxPoint.X - frame.MinPoint.X;
var h = frame.MaxPoint.Y - frame.MinPoint.Y;
if (w <= 0 || h <= 0)
{
return null;
}
var searchMinX = frame.MinPoint.X + w * 0.25;
var searchMaxX = frame.MaxPoint.X - 5.0;
var searchMinY = frame.MinPoint.Y;
var searchMaxY = frame.MinPoint.Y + h * 0.50;
var candidatesX = new List<double>();
var candidatesY = new List<double>();
foreach (var ent in entities)
{
if (ent == null || ent.IsErased)
{
continue;
}
if (ent is Line ln)
{
if (Math.Abs(ln.StartPoint.X - ln.EndPoint.X) < 1.0
&& ln.StartPoint.X > searchMinX && ln.StartPoint.X < searchMaxX
&& Math.Max(ln.StartPoint.Y, ln.EndPoint.Y) > searchMinY
&& Math.Min(ln.StartPoint.Y, ln.EndPoint.Y) < searchMaxY)
{
candidatesX.Add(ln.StartPoint.X);
}
if (ln.StartPoint.X > frame.MaxPoint.X - Math.Min(200.0, w * 0.5)
&& ln.EndPoint.X > frame.MaxPoint.X - Math.Min(200.0, w * 0.5)
&& ln.StartPoint.Y > searchMinY && ln.StartPoint.Y < frame.MinPoint.Y + Math.Min(80.0, h * 0.40)
&& Math.Abs(ln.StartPoint.Y - ln.EndPoint.Y) < 1.0)
{
candidatesY.Add(ln.StartPoint.Y);
}
}
else if (ent is Polyline pl)
{
for (int i = 0; i < pl.NumberOfVertices; i++)
{
var pt = pl.GetPoint3dAt(i);
if (pt.X > searchMinX && pt.X < searchMaxX && pt.Y > searchMinY && pt.Y < searchMaxY)
{
candidatesX.Add(pt.X);
}
if (pt.X > frame.MaxPoint.X - Math.Min(200.0, w * 0.5)
&& pt.Y > searchMinY && pt.Y < frame.MinPoint.Y + Math.Min(80.0, h * 0.40))
{
candidatesY.Add(pt.Y);
}
}
}
else if (ent is BlockReference br)
{
try
{
var ext = br.GeometricExtents;
if (ext.MaxPoint.X > searchMinX && ext.MinPoint.X < searchMaxX
&& ext.MaxPoint.Y > searchMinY && ext.MinPoint.Y < searchMaxY)
{
if (ext.MinPoint.X > searchMinX)
{
candidatesX.Add(ext.MinPoint.X);
}
if (ext.MaxPoint.Y < frame.MinPoint.Y + Math.Min(80.0, h * 0.40))
{
candidatesY.Add(ext.MaxPoint.Y);
}
}
}
catch
{
// ignore
}
}
}
if (candidatesY.Count == 0)
{
return null;
}
var tableTopY = candidatesY.Max();
var boundaryX = candidatesX.Count > 0 ? candidatesX.Min() : (frame.MaxPoint.X - Math.Min(200.0, w * 0.5));
if (boundaryX >= frame.MaxPoint.X - 1.0 || tableTopY <= frame.MinPoint.Y + 1.0)
{
return null;
}
return new Extents3d(
new Point3d(boundaryX, frame.MinPoint.Y, 0),
new Point3d(frame.MaxPoint.X, tableTopY, 0));
}
private static bool IsWithinTitleBlock(Entity ent, Extents3d? titleBlock)
{
if (ent == null || !titleBlock.HasValue)
{
return false;
}
try
{
return Intersects(ent.GeometricExtents, titleBlock.Value);
}
catch
{
return true;
}
}
private static bool IsInTopRightCorner(Entity ent, Extents3d layoutExtents, double threshold)
{
try
{
var ext = ent.GeometricExtents;
var centerX = (ext.MinPoint.X + ext.MaxPoint.X) / 2;
var centerY = (ext.MinPoint.Y + ext.MaxPoint.Y) / 2;
var layoutWidth = layoutExtents.MaxPoint.X - layoutExtents.MinPoint.X;
var layoutHeight = layoutExtents.MaxPoint.Y - layoutExtents.MinPoint.Y;
var thresholdX = layoutExtents.MinPoint.X + layoutWidth * threshold;
var thresholdY = layoutExtents.MinPoint.Y + layoutHeight * threshold;
return centerX > thresholdX && centerY > thresholdY;
}
catch
{
return true;
}
}
/// <summary>
/// 检测白色外框(图纸边界)的范围
/// 白色外框通常是 ColorIndex=7 的 Line 或 Polyline构成图纸的可绘图区域
/// </summary>
public static Extents3d? ComputeWhiteFrameExtents(CadContext ctx)
{
if (ctx == null)
{
return null;
}
var db = ctx.Database;
var tr = ctx.Transaction;
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead);
var allEntities = ms.Cast<ObjectId>()
.Select(id => tr.GetObject(id, OpenMode.ForRead, false) as Entity)
.Where(e => e != null && !e.IsErased)
.ToList();
return ComputeWhiteFrameExtentsFromEntities(tr, allEntities);
}
private static Extents3d? ComputeWhiteFrameExtentsFromEntities(Transaction tr, IEnumerable<Entity> entities)
{
Extents3d? result = null;
foreach (var ent in entities)
{
if (ent == null || ent.IsErased)
{
continue;
}
// 只处理 Line 或 Polyline
if (!(ent is Line) && !(ent is Polyline))
{
continue;
}
// 检查是否是白色ColorIndex=7
if (!IsWhiteColor(ent, tr))
{
continue;
}
try
{
var ext = ent.GeometricExtents;
if (result == null)
{
result = ext;
}
else
{
result = new Extents3d(
new Point3d(
Math.Min(result.Value.MinPoint.X, ext.MinPoint.X),
Math.Min(result.Value.MinPoint.Y, ext.MinPoint.Y),
Math.Min(result.Value.MinPoint.Z, ext.MinPoint.Z)),
new Point3d(
Math.Max(result.Value.MaxPoint.X, ext.MaxPoint.X),
Math.Max(result.Value.MaxPoint.Y, ext.MaxPoint.Y),
Math.Max(result.Value.MaxPoint.Z, ext.MaxPoint.Z)));
}
}
catch
{
// 忽略
}
}
return result;
}
private static bool IsWhiteColor(Entity ent, Transaction tr)
{
if (ent == null)
{
return false;
}
// ColorIndex=7 是白色
var colorIndex = ent.ColorIndex;
if (colorIndex == 7)
{
return true;
}
// ColorIndex=256 表示 ByLayer需要检查图层颜色
if (colorIndex == 256 && tr != null)
{
try
{
var layer = tr.GetObject(ent.LayerId, OpenMode.ForRead) as LayerTableRecord;
if (layer != null && layer.Color.ColorIndex == 7)
{
return true;
}
}
catch
{
// 忽略
}
}
return false;
}
/// <summary>
/// 图形尺寸检查结果
/// </summary>
public sealed class DrawingSizeCheckResult
{
public bool NeedsScaling { get; set; }
public double ScaleFactor { get; set; } = 1.0;
public double DrawingWidth { get; set; }
public double DrawingHeight { get; set; }
public double AvailableWidth { get; set; }
public double AvailableHeight { get; set; }
public string Message { get; set; }
}
/// <summary>
/// 检查图形是否会超出图纸边界,如果超出则计算缩放比例
/// </summary>
/// <param name="whiteFrameExtents">白色外框范围</param>
/// <param name="drawingWidth">预期绘制图形的宽度</param>
/// <param name="drawingHeight">预期绘制图形的高度</param>
/// <param name="margin">边距默认50</param>
/// <returns>检查结果,包含是否需要缩放和缩放比例</returns>
public static DrawingSizeCheckResult CheckDrawingSize(Extents3d? whiteFrameExtents, double drawingWidth, double drawingHeight, double margin = 50)
{
var result = new DrawingSizeCheckResult
{
DrawingWidth = drawingWidth,
DrawingHeight = drawingHeight
};
if (!whiteFrameExtents.HasValue)
{
result.Message = "未检测到白色外框,跳过尺寸检查";
return result;
}
var frame = whiteFrameExtents.Value;
var availableWidth = frame.MaxPoint.X - frame.MinPoint.X - margin * 2;
var availableHeight = frame.MaxPoint.Y - frame.MinPoint.Y - margin * 2;
result.AvailableWidth = availableWidth;
result.AvailableHeight = availableHeight;
if (availableWidth <= 0 || availableHeight <= 0)
{
result.Message = "白色外框范围无效";
return result;
}
// 计算宽度和高度的缩放比例,取较小值(等比例缩放)
var scaleX = availableWidth / drawingWidth;
var scaleY = availableHeight / drawingHeight;
if (scaleX >= 1.0 && scaleY >= 1.0)
{
result.NeedsScaling = false;
result.ScaleFactor = 1.0;
result.Message = $"图形尺寸正常,无需缩放 (图形: {drawingWidth:F1}x{drawingHeight:F1}, 可用: {availableWidth:F1}x{availableHeight:F1})";
return result;
}
// 需要缩放,取较小的比例以确保两个方向都不超出
result.NeedsScaling = true;
result.ScaleFactor = Math.Min(scaleX, scaleY);
result.Message = $"图形超出边界,将等比例缩放至 {result.ScaleFactor:P1} (图形: {drawingWidth:F1}x{drawingHeight:F1}, 可用: {availableWidth:F1}x{availableHeight:F1})";
return result;
}
/// <summary>
/// 更新图纸标题栏中的比例 - 查找"比例"标题文字并在其下方创建比例值文本
/// </summary>
public static bool UpdateScaleAttribute(CadContext ctx, string layoutName, bool scanModelSpace, double scaleFactor)
{
if (ctx == null)
{
return false;
}
var db = ctx.Database;
var tr = ctx.Transaction;
// 格式化比例文本
var scaleText = FormatScaleText(scaleFactor);
// 在ModelSpace中查找"比例"标题文字
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
Point3d? scaleLabelPos = null;
double textHeight = 2.5;
string textStyle = "STANDARD";
// 遍历所有实体,查找"比例"文字
foreach (ObjectId id in ms)
{
var ent = tr.GetObject(id, OpenMode.ForRead, false) as Entity;
if (ent == null)
{
continue;
}
if (ent is DBText dbText)
{
var content = dbText.TextString?.Trim() ?? string.Empty;
var contentNoSpace = content.Replace(" ", "").Replace(" ", "");
// 匹配:去掉空格后是"比例"
if (contentNoSpace == "比例")
{
scaleLabelPos = dbText.Position;
textHeight = dbText.Height;
textStyle = dbText.TextStyleName ?? "STANDARD";
break;
}
}
else if (ent is MText mText)
{
var content = mText.Contents?.Trim() ?? string.Empty;
var contentNoSpace = content.Replace(" ", "").Replace(" ", "");
if (contentNoSpace == "比例")
{
scaleLabelPos = mText.Location;
textHeight = mText.TextHeight;
textStyle = mText.TextStyleName ?? "STANDARD";
break;
}
}
}
if (!scaleLabelPos.HasValue)
{
return false;
}
// 在"比例"文字下方创建比例值文本
var valuePos = new Point3d(
scaleLabelPos.Value.X,
scaleLabelPos.Value.Y - textHeight * 2.5,
scaleLabelPos.Value.Z
);
var newText = new DBText
{
Position = valuePos,
TextString = scaleText,
Height = textHeight,
ColorIndex = 7,
Layer = "0"
};
// 设置文字样式
var textStyleTbl = (TextStyleTable)tr.GetObject(db.TextStyleTableId, OpenMode.ForRead);
if (textStyleTbl.Has(textStyle))
{
newText.TextStyleId = textStyleTbl[textStyle];
}
ms.AppendEntity(newText);
tr.AddNewlyCreatedDBObject(newText, true);
return true;
}
private static string FormatScaleText(double scaleFactor)
{
if (scaleFactor >= 0.9999 && scaleFactor <= 1.0001)
{
return "1:1";
}
var ratio = 1.0 / scaleFactor;
if (Math.Abs(ratio - Math.Round(ratio)) < 0.05)
{
return $"1:{Math.Round(ratio)}";
}
return $"1:{ratio:F1}";
}
/// <summary>
/// 更新图纸标题栏中的检验类别 - 查找"检验类别"标题文字并在其下方创建检验类别值文本
/// </summary>
public static bool UpdateInspectionCategoryAttribute(CadContext ctx, string layoutName, bool scanModelSpace, string inspectionCategory)
{
if (ctx == null || string.IsNullOrWhiteSpace(inspectionCategory))
{
return false;
}
var db = ctx.Database;
var tr = ctx.Transaction;
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
Point3d? labelPos = null;
double textHeight = 2.5;
string textStyle = "STANDARD";
foreach (ObjectId id in ms)
{
var ent = tr.GetObject(id, OpenMode.ForRead, false) as Entity;
if (ent == null)
{
continue;
}
if (ent is DBText dbText)
{
var content = dbText.TextString?.Trim() ?? string.Empty;
var contentNoSpace = content.Replace(" ", "").Replace(" ", "");
if (contentNoSpace == "检验类别")
{
labelPos = dbText.Position;
textHeight = dbText.Height;
textStyle = dbText.TextStyleName ?? "STANDARD";
break;
}
}
else if (ent is MText mText)
{
var content = mText.Contents?.Trim() ?? string.Empty;
var contentNoSpace = content.Replace(" ", "").Replace(" ", "");
if (contentNoSpace == "检验类别")
{
labelPos = mText.Location;
textHeight = mText.TextHeight;
textStyle = mText.TextStyleName ?? "STANDARD";
break;
}
}
}
if (!labelPos.HasValue)
{
return false;
}
var valuePos = new Point3d(
labelPos.Value.X,
labelPos.Value.Y - textHeight * 2.5,
labelPos.Value.Z
);
var newText = new DBText
{
Position = valuePos,
TextString = inspectionCategory,
Height = textHeight,
ColorIndex = 7,
Layer = "0"
};
var textStyleTbl = (TextStyleTable)tr.GetObject(db.TextStyleTableId, OpenMode.ForRead);
if (textStyleTbl.Has(textStyle))
{
newText.TextStyleId = textStyleTbl[textStyle];
}
ms.AppendEntity(newText);
tr.AddNewlyCreatedDBObject(newText, true);
return true;
}
/// <summary>
/// 在红色边框与白色边框之间的左上角添加"内部资料,控制范围"文字标记
/// </summary>
/// <param name="ctx">CAD上下文</param>
/// <param name="layoutName">布局名称</param>
/// <param name="isModelSpace">是否为模型空间</param>
/// <returns>是否成功添加</returns>
public static bool AddInternalMaterialLabel(CadContext ctx, string layoutName, bool isModelSpace)
{
if (ctx == null)
{
return false;
}
var db = ctx.Database;
var tr = ctx.Transaction;
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
var allEntities = ms.Cast<ObjectId>()
.Select(id => tr.GetObject(id, OpenMode.ForRead, false) as Entity)
.Where(e => e != null && !e.IsErased)
.ToList();
// 获取红色边框和白色边框范围
var redFrameExtents = ComputeRedFrameExtents(tr, allEntities);
var whiteFrameExtents = ComputeWhiteFrameExtentsFromEntities(tr, allEntities);
if (!redFrameExtents.HasValue || !whiteFrameExtents.HasValue)
{
return false;
}
var redFrame = redFrameExtents.Value;
var whiteFrame = whiteFrameExtents.Value;
// 计算插入点:在红框和白框之间的左上角
// X: 红框左边界稍内侧
// Y: 红框顶部和白框顶部之间,偏上方
var insertX = redFrame.MinPoint.X + 2.0; // 距红框左边2mm
var insertY = (redFrame.MaxPoint.Y + whiteFrame.MaxPoint.Y) / 2.0; // 红框与白框顶部中间
var insertPoint = new Point3d(insertX, insertY, 0);
// 创建 MText
var mt = new MText();
// 使用宋体格式
mt.Contents = @"{\fSimSun|b0|i0|c134|p2;内部资料,控制范围}";
mt.Location = insertPoint;
mt.TextHeight = 3.5;
mt.Attachment = AttachmentPoint.MiddleLeft; // 左对齐,垂直居中
mt.Width = 0; // 不换行
mt.ColorIndex = 4; // 青色
// 尝试设置图层
try
{
var layerTbl = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
string targetLayer = "0";
if (layerTbl.Has("TEXT")) targetLayer = "TEXT";
else if (layerTbl.Has("文字")) targetLayer = "文字";
mt.Layer = targetLayer;
}
catch { }
ms.AppendEntity(mt);
tr.AddNewlyCreatedDBObject(mt, true);
return true;
}
}
}