3778 lines
144 KiB
C#
3778 lines
144 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|