diff --git a/Cad/TemplateDrawingService.cs b/Cad/TemplateDrawingService.cs index 262e966..a6392cb 100644 --- a/Cad/TemplateDrawingService.cs +++ b/Cad/TemplateDrawingService.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Runtime; +using CadParamPluging.Common; using CadParamPluging.Domain.Models; namespace CadParamPluging.Cad @@ -180,6 +181,359 @@ namespace CadParamPluging.Cad return s; } + 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 string PlainText; + public Action Apply; + } + + 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 tr = ctx.Transaction; + var db = ctx.Database; + + var space = GetTargetSpace(tr, db, layoutName, scanModelSpace); + if (space == null) + { + return new NoteApplyResult { Applied = false, Message = "未找到目标空间,跳过附注替换。" }; + } + + var candidates = new List(); + var visitedBlocks = new HashSet(); + 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) + { + 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, bag.GetString); + best.Apply(rendered); + + 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; + } + + if (scanModelSpace) + { + var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead); + return (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead); + } + + if (string.IsNullOrWhiteSpace(layoutName)) + { + return null; + } + + 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.ForRead); + } + + private static void CollectNoteCandidates(Transaction tr, Entity ent, HashSet visitedBlocks, List 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); + 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(); + } + + 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 candidates) + { + if (!IsLikelyNoteText(plainText)) + { + return; + } + + var score = ComputeNoteScore(plainText); + if (score <= 0) + { + return; + } + + candidates.Add(new NoteCandidate + { + Score = score, + Kind = kind, + PlainText = plainText, + Apply = rendered => ApplyNoteTextToObject(tr, id, kind, rendered) + }); + } + + 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 SplitLines(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return Enumerable.Empty(); + } + + 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) + { + 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) + { + mt.Contents = ToMTextContents(rendered); + return; + } + + if (obj is DBText t) + { + t.TextString = CollapseToSingleLine(rendered); + return; + } + + if (obj is AttributeReference att) + { + att.TextString = CollapseToSingleLine(rendered); + } + } + + 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); + } + public static void KeepOnlyLayout(Document doc, string layoutName) { if (doc == null) diff --git a/CadParamPluging.csproj b/CadParamPluging.csproj index 26dc9cc..b640432 100644 --- a/CadParamPluging.csproj +++ b/CadParamPluging.csproj @@ -65,9 +65,12 @@ + + + diff --git a/Common/NotePlaceholderBinding.cs b/Common/NotePlaceholderBinding.cs new file mode 100644 index 0000000..0f357e0 --- /dev/null +++ b/Common/NotePlaceholderBinding.cs @@ -0,0 +1,11 @@ +using System; + +namespace CadParamPluging.Common +{ + [Serializable] + public class NotePlaceholderBinding + { + public int Index { get; set; } + public string ParamKey { get; set; } + } +} diff --git a/Common/NoteTemplateEngine.cs b/Common/NoteTemplateEngine.cs new file mode 100644 index 0000000..b3c58fc --- /dev/null +++ b/Common/NoteTemplateEngine.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CadParamPluging.Common +{ + public static class NoteTemplateEngine + { + public sealed class PlaceholderOccurrence + { + public int Index { get; set; } + public int Start { get; set; } + public int LineNumber { get; set; } + public int ColumnInLine { get; set; } + public string LineText { get; set; } + } + + public static int CountPlaceholders(string text) + { + if (string.IsNullOrEmpty(text)) + { + return 0; + } + + var count = 0; + for (var i = 0; i < text.Length;) + { + if (text[i] != '*') + { + i++; + continue; + } + + var j = i; + while (j < text.Length && text[j] == '*') + { + j++; + } + + var len = j - i; + if (len != 4) + { + count += len; + } + + i = j; + } + + return count; + } + + public static List ParseOccurrences(string text) + { + var result = new List(); + if (string.IsNullOrEmpty(text)) + { + return result; + } + + var s = NormalizeNewLines(text); + + // Keep empty lines for correct line number mapping. + var lines = s.Split(new[] { '\n' }, StringSplitOptions.None); + + // Build line start offsets. + var lineStartOffsets = new int[lines.Length]; + var offset = 0; + for (var i = 0; i < lines.Length; i++) + { + lineStartOffsets[i] = offset; + offset += (lines[i] ?? string.Empty).Length; + if (i < lines.Length - 1) + { + offset += 1; // '\n' + } + } + + var placeholderIndex = 0; + var lineIdx = 0; + var lineStart = lines.Length > 0 ? lineStartOffsets[0] : 0; + + for (var i = 0; i < s.Length;) + { + // Move line index if needed. + while (lineIdx + 1 < lineStartOffsets.Length && i >= lineStartOffsets[lineIdx] + (lines[lineIdx] ?? string.Empty).Length + 1) + { + lineIdx++; + lineStart = lineStartOffsets[lineIdx]; + } + + if (s[i] != '*') + { + i++; + continue; + } + + var j = i; + while (j < s.Length && s[j] == '*') + { + j++; + } + + var runLen = j - i; + if (runLen == 4) + { + i = j; + continue; + } + + var lineText = (lineIdx >= 0 && lineIdx < lines.Length) ? (lines[lineIdx] ?? string.Empty) : string.Empty; + + for (var k = 0; k < runLen; k++) + { + placeholderIndex++; + var pos = i + k; + var col = pos - lineStart; + if (col < 0) col = 0; + + result.Add(new PlaceholderOccurrence + { + Index = placeholderIndex, + Start = pos, + LineNumber = lineIdx + 1, + ColumnInLine = col, + LineText = lineText + }); + } + + i = j; + } + + return result; + } + + public static string BuildNumberedPreviewText(string templateText) + { + templateText = templateText ?? string.Empty; + var s = NormalizeNewLines(templateText); + + var sb = new StringBuilder(s.Length + 64); + var placeholderIndex = 0; + + for (var i = 0; i < s.Length;) + { + if (s[i] != '*') + { + sb.Append(s[i]); + i++; + continue; + } + + var j = i; + while (j < s.Length && s[j] == '*') + { + j++; + } + + var runLen = j - i; + if (runLen == 4) + { + sb.Append("****"); + i = j; + continue; + } + + for (var k = 0; k < runLen; k++) + { + placeholderIndex++; + sb.Append("【#"); + sb.Append(placeholderIndex); + sb.Append("】"); + } + + i = j; + } + + return sb.ToString(); + } + + public static string Render(string templateText, IEnumerable bindings, Func getValue) + { + templateText = templateText ?? string.Empty; + + var s = NormalizeNewLines(templateText); + + var map = new Dictionary(); + foreach (var b in bindings ?? Enumerable.Empty()) + { + if (b == null) + { + continue; + } + + if (b.Index <= 0) + { + continue; + } + + var key = (b.ParamKey ?? string.Empty).Trim(); + if (key.Length == 0) + { + continue; + } + + if (!map.ContainsKey(b.Index)) + { + map[b.Index] = key; + } + } + + var sb = new StringBuilder(s.Length + 64); + var placeholderIndex = 0; + + for (var i = 0; i < s.Length;) + { + if (s[i] != '*') + { + sb.Append(s[i]); + i++; + continue; + } + + var j = i; + while (j < s.Length && s[j] == '*') + { + j++; + } + + var runLen = j - i; + if (runLen == 4) + { + sb.Append("****"); + i = j; + continue; + } + + for (var k = 0; k < runLen; k++) + { + placeholderIndex++; + if (map.TryGetValue(placeholderIndex, out var key) && getValue != null) + { + var v = getValue(key); + if (!string.IsNullOrWhiteSpace(v)) + { + sb.Append(v); + continue; + } + } + sb.Append('*'); + } + + i = j; + } + + return sb.ToString(); + } + + /// + /// When the same ParamKey is bound by multiple placeholders, each placeholder should have its own value key, + /// otherwise values would overwrite each other. + /// This method returns an equivalent bindings list where ParamKey is replaced by an effective value key. + /// + public static List BuildEffectiveValueKeyBindings(IEnumerable bindings) + { + var list = (bindings ?? Enumerable.Empty()) + .Where(b => b != null && b.Index > 0 && !string.IsNullOrWhiteSpace(b.ParamKey)) + .Select(b => new NotePlaceholderBinding { Index = b.Index, ParamKey = b.ParamKey.Trim() }) + .ToList(); + + var dup = list + .GroupBy(b => b.ParamKey, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase); + + foreach (var b in list) + { + if (dup.TryGetValue(b.ParamKey, out var cnt) && cnt > 1) + { + b.ParamKey = string.Format("{0}@{1}", b.ParamKey, b.Index); + } + } + + return list; + } + + private static string NormalizeNewLines(string text) + { + return (text ?? string.Empty) + .Replace("\r\n", "\n") + .Replace("\r", "\n"); + } + } +} diff --git a/Common/ParamCatalog.cs b/Common/ParamCatalog.cs index 68483f6..f2b3934 100644 --- a/Common/ParamCatalog.cs +++ b/Common/ParamCatalog.cs @@ -69,7 +69,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "数据库下拉列表(支持数据扩充),点选映射", Order = 100, - Group = "材料" + Group = "备注参数" }, new ParamDefinition { @@ -81,7 +81,7 @@ namespace CadParamPluging.Common EnumOptions = new List { "一般件", "关键件", "重要件" }, Hint = "下拉列表(一般件/关键件/重要件),点选;关键件/重要件需加方框标注", Order = 110, - Group = "材料" + Group = "备注参数" }, new ParamDefinition { @@ -92,7 +92,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "多选下拉列表(支持数据扩充),选项用\"或\"连接", Order = 120, - Group = "材料" + Group = "备注参数" }, new ParamDefinition { @@ -103,7 +103,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "多选下拉列表(支持数据扩充),选项用\"或\"连接", Order = 130, - Group = "材料" + Group = "备注参数" }, // --- Inspection / heat treatment --- @@ -117,7 +117,7 @@ namespace CadParamPluging.Common EnumOptions = new List { "Ⅱ", "Ⅱa", "Ⅱ大", "Ⅲ", "Ⅳ" }, Hint = "单选下拉列表", Order = 200, - Group = "检验/热处理" + Group = "备注参数" }, new ParamDefinition { @@ -129,7 +129,7 @@ namespace CadParamPluging.Common EnumOptions = new List { "Ⅱ", "Ⅲ", "Ⅳ" }, Hint = "单选下拉列表", Order = 210, - Group = "检验/热处理" + Group = "备注参数" }, new ParamDefinition { @@ -141,7 +141,7 @@ namespace CadParamPluging.Common EnumOptions = new List { "预备热处理", "热处理" }, Hint = "单选下拉列表", Order = 220, - Group = "检验/热处理" + Group = "备注参数" }, new ParamDefinition { @@ -152,7 +152,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "数据库下拉列表(支持数据扩充),点选", Order = 230, - Group = "检验/热处理" + Group = "备注参数" }, new ParamDefinition { @@ -163,7 +163,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "组合选择:硬度符号+连接符+数值,可空", Order = 240, - Group = "检验/热处理" + Group = "备注参数" }, new ParamDefinition { @@ -174,7 +174,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "手动输入,可空", Order = 250, - Group = "检验/热处理" + Group = "备注参数" }, // --- Marking / notes --- @@ -187,7 +187,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "多选下拉列表:零件尾号/零件号、材料牌号、熔炼炉号、锭节号、热处理炉次号、热处理炉批号、批次号", Order = 300, - Group = "标注/说明" + Group = "备注参数" }, new ParamDefinition { @@ -198,7 +198,7 @@ namespace CadParamPluging.Common DefaultValue = "1", Hint = "手动编辑,默认为\"1\"", Order = 310, - Group = "标注/说明" + Group = "备注参数" }, new ParamDefinition { @@ -210,7 +210,7 @@ namespace CadParamPluging.Common EnumOptions = new List { "空", "0.8", "1.6", "3.2" }, Hint = "单选下拉列表,默认为空", Order = 320, - Group = "标注/说明" + Group = "备注参数" }, new ParamDefinition { @@ -222,7 +222,7 @@ namespace CadParamPluging.Common EnumOptions = new List { "空", "A", "B", "C", "D", "E", "F" }, Hint = "下拉列表(A-F),可手动编辑,支持空选项", Order = 330, - Group = "标注/说明" + Group = "备注参数" }, new ParamDefinition { @@ -233,7 +233,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "下拉列表模板\"X个A零件和X个B零件或X个C零件\",点选后可编辑", Order = 340, - Group = "标注/说明" + Group = "备注参数" }, new ParamDefinition { @@ -244,7 +244,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "下拉列表模板\"每批/每*件多投*件\",点选后可编辑", Order = 350, - Group = "标注/说明" + Group = "备注参数" }, new ParamDefinition { @@ -255,7 +255,7 @@ namespace CadParamPluging.Common DefaultValue = "", Hint = "多选下拉列表:传递抗拉强度实测值、机加夹头标注、使用温度300℃以上、每批投产数量限制,点选后可编辑", Order = 360, - Group = "标注/说明" + Group = "备注参数" }, // --- Template key fields (also exist in panel dropdowns) --- @@ -457,15 +457,37 @@ namespace CadParamPluging.Common }, new ParamDefinition { - Key = "PartDimensionsPrime", - Label = "零件尺寸(Φ'1/Φ'2/H'1等)", - Type = ParamValueType.String, + Key = "OuterDiameter1Prime", + Label = "外径Φ'1", + Type = ParamValueType.Double, Required = false, DefaultValue = "", Hint = "手动编辑", Order = 630, Group = "尺寸-环形" }, + new ParamDefinition + { + Key = "InnerDiameter2Prime", + Label = "内径Φ'2", + Type = ParamValueType.Double, + Required = false, + DefaultValue = "", + Hint = "手动编辑", + Order = 640, + Group = "尺寸-环形" + }, + new ParamDefinition + { + Key = "Height1Prime", + Label = "高度H'1", + Type = ParamValueType.Double, + Required = false, + DefaultValue = "", + Hint = "手动编辑", + Order = 650, + Group = "尺寸-环形" + }, // --- Dimensions: shaft/rod (diameter/length) --- new ParamDefinition diff --git a/Common/ParamCatalogStore.cs b/Common/ParamCatalogStore.cs index 82d8508..6598f7f 100644 --- a/Common/ParamCatalogStore.cs +++ b/Common/ParamCatalogStore.cs @@ -1,5 +1,8 @@ using System; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Xml.Serialization; namespace CadParamPluging.Common @@ -65,11 +68,51 @@ namespace CadParamPluging.Common } } + // Migration: move remark-like fields into "备注参数" group if they are still using older default groups. + var remarkKeys = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "MaterialGrade", + "FeatureCategory", + "MaterialTechnicalCondition", + "ForgingTechnicalCondition", + "InspectionCategory", + "ForgingCategory", + "HeatTreatmentState", + "HeatTreatmentProcess", + "Hardness", + "UltrasonicRequirement", + "MarkingContent", + "PartsPerForging", + "SurfaceRoughness", + "DesignRevision", + "SerialInfo", + "MachiningSpecimenRequirement", + "AdditionalNotes" + }; + foreach (var key in remarkKeys) + { + var existing = obj.FindByKey(key); + if (existing == null) + { + continue; + } + + var g = existing.Group ?? string.Empty; + if (string.IsNullOrWhiteSpace(g) + || string.Equals(g, "材料", StringComparison.OrdinalIgnoreCase) + || string.Equals(g, "检验/热处理", StringComparison.OrdinalIgnoreCase) + || string.Equals(g, "标注/说明", StringComparison.OrdinalIgnoreCase)) + { + existing.Group = "备注参数"; + } + } + // Migration: remove legacy demo parameters if they exist. obj.Items = (obj.Items ?? new System.Collections.Generic.List()) .Where(x => x != null && !string.Equals(x.Key, "Width", StringComparison.OrdinalIgnoreCase) - && !string.Equals(x.Key, "Height", StringComparison.OrdinalIgnoreCase)) + && !string.Equals(x.Key, "Height", StringComparison.OrdinalIgnoreCase) + && !string.Equals(x.Key, "PartDimensionsPrime", StringComparison.OrdinalIgnoreCase)) .ToList(); obj.Normalize(); diff --git a/Common/TemplateSchemaDefaults.cs b/Common/TemplateSchemaDefaults.cs new file mode 100644 index 0000000..2616288 --- /dev/null +++ b/Common/TemplateSchemaDefaults.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace CadParamPluging.Common +{ + public static class TemplateSchemaDefaults + { + public static TemplateSchemas CreateDefault() + { + var schemas = new TemplateSchemas(); + + // 交付状态=车加工, 工艺方法=轧制, 结构特征=环形, 特殊条件=无 + // TemplateKey: "车加工|轧制|环形|" ("无" 会被归一化为空) + schemas.Items.Add(new TemplateSchemaDefinition + { + ProjectType = "车加工", + DrawingType = "轧制", + SheetSize = "环形", + Scale = "无", + DisplayName = "环形(车加工/轧制)", + SelectedParamKeys = new List + { + "OuterDiameter1", + "OuterDiameter1TolPlus", + "OuterDiameter1TolMinus", + "InnerDiameter2", + "InnerDiameter2TolPlus", + "InnerDiameter2TolMinus", + "Height1", + "Height1TolPlus", + "Height1TolMinus", + "MinWallThickness", + "OuterDiameter1Prime", + "InnerDiameter2Prime", + "Height1Prime" + } + }); + + schemas.Normalize(); + return schemas; + } + } +} diff --git a/Common/TemplateSchemaDefinition.cs b/Common/TemplateSchemaDefinition.cs index 9ccd2a1..43ee101 100644 --- a/Common/TemplateSchemaDefinition.cs +++ b/Common/TemplateSchemaDefinition.cs @@ -18,9 +18,14 @@ namespace CadParamPluging.Common public List SelectedParamKeys { get; set; } + public string NoteTemplateText { get; set; } + public List NoteBindings { get; set; } + public TemplateSchemaDefinition() { SelectedParamKeys = new List(); + NoteTemplateText = string.Empty; + NoteBindings = new List(); } public void Normalize() @@ -31,6 +36,8 @@ namespace CadParamPluging.Common Scale = (Scale ?? string.Empty).Trim(); DisplayName = (DisplayName ?? string.Empty).Trim(); + NoteTemplateText = NoteTemplateText ?? string.Empty; + TemplateKey = TemplateKeyBuilder.Build(ProjectType, DrawingType, SheetSize, Scale); if (SelectedParamKeys == null) @@ -43,6 +50,18 @@ namespace CadParamPluging.Common .Select(s => s.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); + + if (NoteBindings == null) + { + NoteBindings = new List(); + } + + NoteBindings = NoteBindings + .Where(b => b != null && b.Index > 0 && !string.IsNullOrWhiteSpace(b.ParamKey)) + .GroupBy(b => b.Index) + .Select(g => g.First()) + .OrderBy(b => b.Index) + .ToList(); } } } diff --git a/Common/TemplateSchemaStore.cs b/Common/TemplateSchemaStore.cs index 4ba8973..c688b5c 100644 --- a/Common/TemplateSchemaStore.cs +++ b/Common/TemplateSchemaStore.cs @@ -22,10 +22,13 @@ namespace CadParamPluging.Common { try { + var defaults = TemplateSchemaDefaults.CreateDefault(); + defaults.Normalize(); + var path = GetFilePath(); if (!File.Exists(path)) { - return new TemplateSchemas(); + return defaults; } using (var stream = File.OpenRead(path)) @@ -34,7 +37,25 @@ namespace CadParamPluging.Common var obj = serializer.Deserialize(stream) as TemplateSchemas; if (obj == null) { - return new TemplateSchemas(); + return defaults; + } + + obj.Normalize(); + + // Merge: keep local modifications, but add any missing built-in defaults. + foreach (var def in defaults.Items ?? Enumerable.Empty()) + { + if (def == null || string.IsNullOrWhiteSpace(def.TemplateKey)) + { + continue; + } + + var exists = (obj.Items ?? Enumerable.Empty()) + .Any(x => x != null && string.Equals(x.TemplateKey, def.TemplateKey, StringComparison.OrdinalIgnoreCase)); + if (!exists) + { + obj.Items.Add(def); + } } obj.Normalize(); @@ -51,7 +72,7 @@ namespace CadParamPluging.Common } catch { - return new TemplateSchemas(); + return TemplateSchemaDefaults.CreateDefault(); } } diff --git a/UI/DrawingParamsForm.cs b/UI/DrawingParamsForm.cs index 1674f6b..3c486be 100644 --- a/UI/DrawingParamsForm.cs +++ b/UI/DrawingParamsForm.cs @@ -1,4 +1,5 @@ using System; +using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; @@ -12,10 +13,16 @@ namespace CadParamPluging.UI { private readonly TableLayoutPanel _grid; private readonly Dictionary _controlsByKey; + private readonly Dictionary _defsByControlKey; private readonly ParamCatalog _catalog; private readonly TemplateSchemaDefinition _schema; private readonly ToolTip _toolTip; + private readonly TabControl _notePreviewTabs; + private readonly RichTextBox _rtbNoteNumbered; + private readonly RichTextBox _rtbNoteRendered; + private Dictionary _occByIndex; + public ParamBag Result { get; private set; } public DrawingParamsForm(ParamCatalog catalog, TemplateSchemaDefinition schema) @@ -25,14 +32,16 @@ namespace CadParamPluging.UI _schema = schema; Text = "填写参数"; - Size = new Size(560, 620); + Size = new Size(980, 680); + MinimumSize = new Size(900, 620); StartPosition = FormStartPosition.CenterParent; - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - MinimizeBox = false; + FormBorderStyle = FormBorderStyle.Sizable; + MaximizeBox = true; + MinimizeBox = true; ShowInTaskbar = false; _controlsByKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + _defsByControlKey = new Dictionary(StringComparer.OrdinalIgnoreCase); _toolTip = new ToolTip(); var root = new TableLayoutPanel @@ -59,6 +68,17 @@ namespace CadParamPluging.UI AutoScroll = true }; + _notePreviewTabs = new TabControl { Dock = DockStyle.Fill }; + _rtbNoteNumbered = new RichTextBox { Dock = DockStyle.Fill, ReadOnly = true, HideSelection = false }; + _rtbNoteRendered = new RichTextBox { Dock = DockStyle.Fill, ReadOnly = true, HideSelection = false }; + + var tabNumbered = new TabPage { Text = "附注编号预览" }; + tabNumbered.Controls.Add(_rtbNoteNumbered); + var tabRendered = new TabPage { Text = "填充值预览" }; + tabRendered.Controls.Add(_rtbNoteRendered); + _notePreviewTabs.TabPages.Add(tabNumbered); + _notePreviewTabs.TabPages.Add(tabRendered); + _grid = new TableLayoutPanel { Dock = DockStyle.Top, @@ -74,6 +94,16 @@ namespace CadParamPluging.UI scrollPanel.Controls.Add(_grid); + var split = new SplitContainer + { + Dock = DockStyle.Fill, + Orientation = Orientation.Vertical + }; + split.Panel1.Controls.Add(scrollPanel); + split.Panel2.Controls.Add(_notePreviewTabs); + + split.SizeChanged += (_, __) => EnsureValidSplitterDistance(split); + var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill, @@ -87,12 +117,66 @@ namespace CadParamPluging.UI buttons.Controls.Add(btnOk); root.Controls.Add(header, 0, 0); - root.Controls.Add(scrollPanel, 0, 1); + root.Controls.Add(split, 0, 1); root.Controls.Add(buttons, 0, 2); Controls.Add(root); AcceptButton = btnOk; CancelButton = btnCancel; + + Shown += (_, __) => + { + split.Panel1MinSize = 360; + split.Panel2MinSize = 280; + EnsureValidSplitterDistance(split); + UpdateNotePreviews(); + }; + } + + private static void EnsureValidSplitterDistance(SplitContainer split) + { + if (split == null) + { + return; + } + + // SplitterDistance must be within [Panel1MinSize, Width - Panel2MinSize - SplitterWidth] + var width = split.Width; + if (width <= 0) + { + return; + } + + // If container is too narrow, collapse preview panel to avoid throwing. + var required = split.Panel1MinSize + split.Panel2MinSize + split.SplitterWidth + 8; + if (width < required) + { + split.Panel2Collapsed = true; + return; + } + + split.Panel2Collapsed = false; + + var min = split.Panel1MinSize; + var max = width - split.Panel2MinSize - split.SplitterWidth; + if (max < min) + { + // Too narrow: keep it at minimum to avoid exception. + max = min; + } + + var desired = (int)(width * 0.6); + if (desired < min) desired = min; + if (desired > max) desired = max; + + try + { + split.SplitterDistance = desired; + } + catch + { + // ignore + } } private static string BuildHeaderText(TemplateSchemaDefinition schema) @@ -108,8 +192,48 @@ namespace CadParamPluging.UI private void BuildFields() { - var keys = (_schema?.SelectedParamKeys ?? new List()).ToList(); - if (keys.Count == 0) + _occByIndex = NoteTemplateEngine.ParseOccurrences(_schema?.NoteTemplateText ?? string.Empty) + .Where(o => o != null) + .GroupBy(o => o.Index) + .Select(g => g.First()) + .ToDictionary(o => o.Index, o => o); + + var mainDefs = new List(); + var seenMain = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var key in (_schema?.SelectedParamKeys ?? new List())) + { + if (string.IsNullOrWhiteSpace(key)) + { + continue; + } + + var def = _catalog.FindByKey(key); + if (def == null) + { + continue; + } + + if (string.Equals(def.Group ?? string.Empty, "备注参数", StringComparison.OrdinalIgnoreCase)) + { + // 备注参数只在附注里填写 + continue; + } + + if (!seenMain.Add(def.Key)) + { + continue; + } + + mainDefs.Add(def); + } + + var noteBindings = (_schema?.NoteBindings ?? new List()) + .Where(b => b != null && b.Index > 0 && !string.IsNullOrWhiteSpace(b.ParamKey)) + .OrderBy(b => b.Index) + .ToList(); + + if (mainDefs.Count == 0 && noteBindings.Count == 0) { var rowIdx = _grid.RowCount++; _grid.RowStyles.Add(new RowStyle(SizeType.AutoSize)); @@ -118,24 +242,99 @@ namespace CadParamPluging.UI return; } - foreach (var key in keys) + if (mainDefs.Count > 0) { - var def = _catalog.FindByKey(key); - if (def == null) + AddSectionHeader("模板参数"); + foreach (var def in mainDefs) { - continue; + AddField(def, def.Key, def.Label, null); } + } - AddField(def); + if (noteBindings.Count > 0) + { + AddSectionHeader("备注参数(附注)"); + foreach (var b in noteBindings) + { + var baseKey = (b.ParamKey ?? string.Empty).Trim(); + var def = _catalog.FindByKey(baseKey); + if (def == null) + { + continue; + } + + // If same ParamKey is used by multiple placeholders, give each placeholder its own value key. + var valueKey = baseKey; + var dupCount = noteBindings.Count(x => string.Equals((x.ParamKey ?? string.Empty).Trim(), baseKey, StringComparison.OrdinalIgnoreCase)); + if (dupCount > 1) + { + valueKey = string.Format("{0}@{1}", baseKey, b.Index); + } + + var labelText = string.Format("{0} (占位符: #{1})", def.Label, b.Index); + var tip = BuildOccurrenceTip(b.Index); + AddField(def, valueKey, labelText, tip); + } } } - private void AddField(ParamDefinition def) + private string BuildOccurrenceTip(int placeholderIndex) + { + if (_occByIndex == null) + { + return null; + } + + if (!_occByIndex.TryGetValue(placeholderIndex, out var o) || o == null) + { + return null; + } + + var line = o.LineText ?? string.Empty; + var col = o.ColumnInLine; + if (col < 0) col = 0; + if (col >= line.Length) + { + return string.Format("第{0}行: 【#{1}】", o.LineNumber, placeholderIndex); + } + + const int ctx = 18; + var start = Math.Max(0, col - ctx); + var end = Math.Min(line.Length, col + 1 + ctx); + var prefix = start > 0 ? "…" : string.Empty; + var suffix = end < line.Length ? "…" : string.Empty; + var left = line.Substring(start, col - start); + var right = line.Substring(col + 1, end - (col + 1)); + var snippet = prefix + left + string.Format("【#{0}】", placeholderIndex) + right + suffix; + + return string.Format("第{0}行: {1}", o.LineNumber, snippet); + } + + private void AddSectionHeader(string text) + { + var rowIdx = _grid.RowCount++; + _grid.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + var lbl = new Label + { + AutoSize = true, + ForeColor = Color.DarkBlue, + Text = text, + Padding = new Padding(0, 10, 0, 4) + }; + _grid.Controls.Add(lbl, 0, rowIdx); + _grid.SetColumnSpan(lbl, 2); + } + + private void AddField(ParamDefinition def, string controlKey, string labelText, string extraToolTip) { var rowIdx = _grid.RowCount++; _grid.RowStyles.Add(new RowStyle(SizeType.AutoSize)); - var labelText = def.Label; + if (string.IsNullOrWhiteSpace(labelText)) + { + labelText = def.Label; + } + if (def.Required) { labelText += " *"; @@ -151,9 +350,96 @@ namespace CadParamPluging.UI _toolTip.SetToolTip(ctrl, def.Hint); } + if (!string.IsNullOrWhiteSpace(extraToolTip)) + { + _toolTip.SetToolTip(lbl, extraToolTip); + _toolTip.SetToolTip(ctrl, extraToolTip); + } + _grid.Controls.Add(lbl, 0, rowIdx); _grid.Controls.Add(ctrl, 1, rowIdx); - _controlsByKey[def.Key] = ctrl; + _controlsByKey[controlKey] = ctrl; + _defsByControlKey[controlKey] = def; + + AttachPreviewUpdate(ctrl); + } + + private void AttachPreviewUpdate(Control ctrl) + { + if (ctrl == null) + { + return; + } + + if (ctrl is TextBox tb) + { + tb.TextChanged += (_, __) => UpdateNotePreviews(); + return; + } + + if (ctrl is ComboBox cb) + { + cb.SelectedIndexChanged += (_, __) => UpdateNotePreviews(); + cb.TextChanged += (_, __) => UpdateNotePreviews(); + return; + } + + if (ctrl is NumericUpDown num) + { + num.ValueChanged += (_, __) => UpdateNotePreviews(); + return; + } + + if (ctrl is CheckBox chk) + { + chk.CheckedChanged += (_, __) => UpdateNotePreviews(); + } + } + + private void UpdateNotePreviews() + { + if (_schema == null) + { + _rtbNoteNumbered.Text = "(模板参数未配置)"; + _rtbNoteRendered.Text = string.Empty; + return; + } + + var noteTemplate = _schema.NoteTemplateText ?? string.Empty; + if (string.IsNullOrWhiteSpace(noteTemplate)) + { + _rtbNoteNumbered.Text = "(未配置附注模板文本:请到【设置→模板参数绑定→附注绑定】粘贴附注模板)"; + _rtbNoteRendered.Text = string.Empty; + return; + } + + _rtbNoteNumbered.Text = NoteTemplateEngine.BuildNumberedPreviewText(noteTemplate); + + var tempBag = CollectBagFromUi(); + var effective = NoteTemplateEngine.BuildEffectiveValueKeyBindings(_schema.NoteBindings); + _rtbNoteRendered.Text = NoteTemplateEngine.Render(noteTemplate, effective, tempBag.GetString); + } + + private ParamBag CollectBagFromUi() + { + var bag = new ParamBag(); + foreach (var key in _controlsByKey.Keys.ToList()) + { + if (!_controlsByKey.TryGetValue(key, out var ctrl) || ctrl == null) + { + continue; + } + + if (!_defsByControlKey.TryGetValue(key, out var def) || def == null) + { + continue; + } + + var value = ReadValue(ctrl, def); + bag.Set(key, value); + } + + return bag; } private Control CreateControl(ParamDefinition def) @@ -256,8 +542,7 @@ namespace CadParamPluging.UI continue; } - var def = _catalog.FindByKey(key); - if (def == null) + if (!_defsByControlKey.TryGetValue(key, out var def) || def == null) { continue; } @@ -270,7 +555,7 @@ namespace CadParamPluging.UI return; } - bag.Set(def.Key, value); + bag.Set(key, value); } Result = bag; diff --git a/UI/ParamDrawingPanel.cs b/UI/ParamDrawingPanel.cs index 8af9b62..0c8d4f9 100644 --- a/UI/ParamDrawingPanel.cs +++ b/UI/ParamDrawingPanel.cs @@ -589,6 +589,24 @@ namespace CadParamPluging.UI AppendLog($"已移除模板匹配参数标注文本: {removed} 处"); } + var noteResult = TemplateDrawingService.ApplyNoteTemplate( + ctx, + _selectedTemplate.LayoutName, + _selectedModelWindow.HasValue, + schema, + bag); + if (noteResult != null) + { + if (noteResult.Applied) + { + AppendLog($"附注替换成功(目标={noteResult.TargetKind}, 占位符={noteResult.PlaceholderCountInDwg})"); + } + else if (!string.IsNullOrWhiteSpace(noteResult.Message)) + { + AppendLog($"附注替换跳过: {noteResult.Message}"); + } + } + var cadService = new CadDrawingService(ctx); DomainFacade.DrawByParams(_selectedTemplate, drawingParams, cadService); ctx.Commit(); diff --git a/UI/SettingsForm.cs b/UI/SettingsForm.cs index f420978..5d2de89 100644 --- a/UI/SettingsForm.cs +++ b/UI/SettingsForm.cs @@ -142,7 +142,7 @@ namespace CadParamPluging.UI } catch (Exception ex) { - MessageBox.Show(this, $"打开模板参数绑定失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show(this, $"打开模板参数绑定失败:\n\n{ex}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } diff --git a/UI/TemplateSchemaBindingForm.cs b/UI/TemplateSchemaBindingForm.cs index bfa1092..da8e913 100644 --- a/UI/TemplateSchemaBindingForm.cs +++ b/UI/TemplateSchemaBindingForm.cs @@ -18,9 +18,22 @@ namespace CadParamPluging.UI private readonly ListBox _lbSchemas; private readonly ListView _lvParams; + private readonly TextBox _txtNoteTemplate; + private readonly Label _lblNotePlaceholderCount; + private readonly DataGridView _gvNoteBindings; + private readonly RichTextBox _rtbNotePreview; + + private readonly List _remarkParamOptions; + private readonly ParamCatalog _catalog; private TemplateSchemas _schemas; + private sealed class ParamOption + { + public string Key { get; set; } + public string Display { get; set; } + } + public TemplateSchemaBindingForm( System.Collections.Generic.IEnumerable projectTypes, System.Collections.Generic.IEnumerable drawingTypes, @@ -30,16 +43,23 @@ namespace CadParamPluging.UI { Text = "模板参数绑定"; Size = new Size(920, 620); + MinimumSize = new Size(920, 620); StartPosition = FormStartPosition.CenterParent; - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - MinimizeBox = false; + FormBorderStyle = FormBorderStyle.Sizable; + MaximizeBox = true; + MinimizeBox = true; ShowInTaskbar = false; _catalog = (catalog ?? ParamCatalog.CreateDefault()).Clone(); _catalog.Normalize(); - _schemas = TemplateSchemaStore.Load(); + _remarkParamOptions = (_catalog.Items ?? Enumerable.Empty()) + .Where(p => p != null && string.Equals(p.Group ?? string.Empty, "备注参数", StringComparison.OrdinalIgnoreCase)) + .OrderBy(p => p.Order) + .Select(p => new ParamOption { Key = p.Key, Display = string.Format("{0} ({1})", p.Label, p.Key) }) + .ToList(); + + _schemas = TemplateSchemaStore.Load() ?? new TemplateSchemas(); _schemas.Normalize(); var root = new TableLayoutPanel @@ -55,8 +75,6 @@ namespace CadParamPluging.UI root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); _lbSchemas = new ListBox { Dock = DockStyle.Fill }; - _lbSchemas.SelectedIndexChanged += (_, __) => LoadSelectedSchemaToUi(); - ReloadSchemaList(); var leftButtons = new FlowLayoutPanel { @@ -155,6 +173,80 @@ namespace CadParamPluging.UI paramsButtonPanel.Controls.Add(btnSelectAll); paramsButtonPanel.Controls.Add(btnUnselectAll); + _txtNoteTemplate = new TextBox + { + Dock = DockStyle.Fill, + Multiline = true, + ScrollBars = ScrollBars.Vertical, + Height = 120 + }; + + _lblNotePlaceholderCount = new Label + { + AutoSize = true, + ForeColor = Color.DarkBlue, + Text = "占位符: 0" + }; + + _gvNoteBindings = new DataGridView + { + Dock = DockStyle.Fill, + AutoGenerateColumns = false, + AllowUserToAddRows = false, + AllowUserToDeleteRows = false, + RowHeadersVisible = false, + SelectionMode = DataGridViewSelectionMode.FullRowSelect + }; + + var colIdx = new DataGridViewTextBoxColumn + { + HeaderText = "序号", + Width = 60, + ReadOnly = true + }; + + var colLine = new DataGridViewTextBoxColumn + { + HeaderText = "所在行", + Width = 80, + ReadOnly = true + }; + + var colSnippet = new DataGridViewTextBoxColumn + { + HeaderText = "文本定位", + AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, + ReadOnly = true + }; + + var colParam = new DataGridViewComboBoxColumn + { + HeaderText = "绑定参数(备注参数)", + Width = 320, + DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton, + DataSource = _remarkParamOptions, + DisplayMember = "Display", + ValueMember = "Key" + }; + + _gvNoteBindings.Columns.Add(colIdx); + _gvNoteBindings.Columns.Add(colLine); + _gvNoteBindings.Columns.Add(colSnippet); + _gvNoteBindings.Columns.Add(colParam); + + _gvNoteBindings.DataError += (_, __) => { }; + _gvNoteBindings.SelectionChanged += (_, __) => HighlightSelectedPlaceholderInPreview(); + + _rtbNotePreview = new RichTextBox + { + Dock = DockStyle.Fill, + ReadOnly = true, + HideSelection = false + }; + + var btnParseNote = new Button { Text = "解析占位符", AutoSize = true }; + btnParseNote.Click += (_, __) => ReloadNoteBindingsFromTemplateText(); + var rightPanel = new TableLayoutPanel { Dock = DockStyle.Fill, @@ -179,7 +271,45 @@ namespace CadParamPluging.UI paramArea.Controls.Add(paramsButtonPanel, 0, 0); paramArea.Controls.Add(_lvParams, 0, 1); - rightPanel.Controls.Add(paramArea, 0, 2); + var noteArea = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 6, + Padding = new Padding(0) + }; + noteArea.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + noteArea.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + noteArea.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + noteArea.RowStyles.Add(new RowStyle(SizeType.Percent, 60f)); + noteArea.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + noteArea.RowStyles.Add(new RowStyle(SizeType.Percent, 40f)); + + noteArea.Controls.Add(new Label { Text = "附注模板(多行文本,使用 * 作为占位符;**** 会忽略)", AutoSize = true }, 0, 0); + noteArea.Controls.Add(_txtNoteTemplate, 0, 1); + + var noteToolbar = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + FlowDirection = FlowDirection.LeftToRight, + AutoSize = true + }; + noteToolbar.Controls.Add(btnParseNote); + noteToolbar.Controls.Add(_lblNotePlaceholderCount); + noteArea.Controls.Add(noteToolbar, 0, 2); + noteArea.Controls.Add(_gvNoteBindings, 0, 3); + noteArea.Controls.Add(new Label { Text = "编号预览(用于对照绑定):", AutoSize = true }, 0, 4); + noteArea.Controls.Add(_rtbNotePreview, 0, 5); + + var tabs = new TabControl { Dock = DockStyle.Fill }; + var tabParams = new TabPage { Text = "参数绑定" }; + tabParams.Controls.Add(paramArea); + var tabNotes = new TabPage { Text = "附注绑定" }; + tabNotes.Controls.Add(noteArea); + tabs.TabPages.Add(tabParams); + tabs.TabPages.Add(tabNotes); + + rightPanel.Controls.Add(tabs, 0, 2); var bottomButtons = new FlowLayoutPanel { @@ -203,10 +333,9 @@ namespace CadParamPluging.UI BuildParamListItems(); UpdateTemplateKeyLabel(); - if (_lbSchemas.Items.Count > 0) - { - _lbSchemas.SelectedIndex = 0; - } + + _lbSchemas.SelectedIndexChanged += (_, __) => LoadSelectedSchemaToUi(); + ReloadSchemaList(); } private static ComboBox CreateCombo(System.Collections.Generic.IEnumerable items) @@ -231,7 +360,10 @@ namespace CadParamPluging.UI try { _lvParams.Items.Clear(); - foreach (var p in (_catalog.Items ?? Enumerable.Empty()).OrderBy(x => x.Order)) + foreach (var p in (_catalog.Items ?? Enumerable.Empty()) + .Where(x => x != null) + .Where(x => !string.Equals(x.Group ?? string.Empty, "备注参数", StringComparison.OrdinalIgnoreCase)) + .OrderBy(x => x.Order)) { var it = new ListViewItem(new[] { p.Label, p.Key }) { @@ -295,6 +427,7 @@ namespace CadParamPluging.UI _lbSchemas.Items.Clear(); foreach (var s in (_schemas.Items ?? Enumerable.Empty()) + .Where(x => x != null) .OrderBy(x => x.ProjectType) .ThenBy(x => x.DrawingType) .ThenBy(x => x.SheetSize) @@ -329,6 +462,7 @@ namespace CadParamPluging.UI } var ordered = (_schemas.Items ?? Enumerable.Empty()) + .Where(x => x != null) .OrderBy(x => x.ProjectType) .ThenBy(x => x.DrawingType) .ThenBy(x => x.SheetSize) @@ -341,7 +475,9 @@ namespace CadParamPluging.UI } var key = ordered[idx].TemplateKey; - return (_schemas.Items ?? new List()).FirstOrDefault(x => string.Equals(x.TemplateKey, key, StringComparison.OrdinalIgnoreCase)); + return (_schemas.Items ?? new List()) + .Where(x => x != null) + .FirstOrDefault(x => string.Equals(x.TemplateKey, key, StringComparison.OrdinalIgnoreCase)); } private void LoadSelectedSchemaToUi() @@ -359,6 +495,7 @@ namespace CadParamPluging.UI _txtDisplayName.Text = schema.DisplayName ?? string.Empty; UpdateTemplateKeyLabel(); ApplySchemaToParamList(schema); + LoadNoteBindingsToUi(schema); } private static void SelectComboByText(ComboBox cb, string value) @@ -403,7 +540,9 @@ namespace CadParamPluging.UI }; candidate.Normalize(); - if ((_schemas.Items ?? new List()).Any(x => string.Equals(x.TemplateKey, candidate.TemplateKey, StringComparison.OrdinalIgnoreCase))) + if ((_schemas.Items ?? new List()) + .Where(x => x != null) + .Any(x => string.Equals(x.TemplateKey, candidate.TemplateKey, StringComparison.OrdinalIgnoreCase))) { MessageBox.Show(this, "该模板定义已存在(四参数组合重复)。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; @@ -434,7 +573,7 @@ namespace CadParamPluging.UI } _schemas.Items = (_schemas.Items ?? Enumerable.Empty()) - .Where(x => !string.Equals(x.TemplateKey, schema.TemplateKey, StringComparison.OrdinalIgnoreCase)) + .Where(x => x != null && !string.Equals(x.TemplateKey, schema.TemplateKey, StringComparison.OrdinalIgnoreCase)) .ToList(); _schemas.Normalize(); ReloadSchemaList(); @@ -485,6 +624,185 @@ namespace CadParamPluging.UI } } + private void LoadNoteBindingsToUi(TemplateSchemaDefinition schema) + { + if (schema == null) + { + _txtNoteTemplate.Text = string.Empty; + ReloadNoteBindingsGrid(string.Empty, null); + return; + } + + _txtNoteTemplate.Text = schema.NoteTemplateText ?? string.Empty; + ReloadNoteBindingsGrid(_txtNoteTemplate.Text, schema.NoteBindings); + } + + private void ReloadNoteBindingsFromTemplateText() + { + var map = ReadNoteBindingMapFromGrid(); + + var bindings = map + .Select(kv => new NotePlaceholderBinding { Index = kv.Key, ParamKey = kv.Value }) + .ToList(); + + ReloadNoteBindingsGrid(_txtNoteTemplate.Text ?? string.Empty, bindings); + } + + private void ReloadNoteBindingsGrid(string templateText, IEnumerable bindings) + { + var occ = NoteTemplateEngine.ParseOccurrences(templateText ?? string.Empty); + var count = occ.Count; + _lblNotePlaceholderCount.Text = string.Format("占位符: {0}", count); + + _rtbNotePreview.Text = NoteTemplateEngine.BuildNumberedPreviewText(templateText ?? string.Empty); + + var map = new Dictionary(); + foreach (var b in bindings ?? Enumerable.Empty()) + { + if (b == null) + { + continue; + } + + if (b.Index <= 0) + { + continue; + } + + var key = (b.ParamKey ?? string.Empty).Trim(); + if (key.Length == 0) + { + continue; + } + + if (!map.ContainsKey(b.Index)) + { + map[b.Index] = key; + } + } + + _gvNoteBindings.SuspendLayout(); + try + { + _gvNoteBindings.Rows.Clear(); + foreach (var o in occ) + { + var idx = _gvNoteBindings.Rows.Add(); + var row = _gvNoteBindings.Rows[idx]; + row.Cells[0].Value = o.Index; + row.Cells[1].Value = string.Format("第{0}行", o.LineNumber); + row.Cells[2].Value = BuildOccurrenceSnippet(o); + row.Cells[3].Value = map.ContainsKey(o.Index) ? map[o.Index] : null; + } + } + finally + { + _gvNoteBindings.ResumeLayout(); + } + + HighlightSelectedPlaceholderInPreview(); + } + + private Dictionary ReadNoteBindingMapFromGrid() + { + var map = new Dictionary(); + foreach (DataGridViewRow row in _gvNoteBindings.Rows) + { + if (row == null) + { + continue; + } + + var idxObj = row.Cells[0].Value; + var keyObj = row.Cells[3].Value; + + int idx; + if (idxObj == null || !int.TryParse(idxObj.ToString(), out idx) || idx <= 0) + { + continue; + } + + var key = (keyObj ?? string.Empty).ToString().Trim(); + if (key.Length == 0) + { + continue; + } + + if (!map.ContainsKey(idx)) + { + map[idx] = key; + } + } + + return map; + } + + private static string BuildOccurrenceSnippet(NoteTemplateEngine.PlaceholderOccurrence o) + { + if (o == null) + { + return string.Empty; + } + + var line = o.LineText ?? string.Empty; + var token = string.Format("【#{0}】", o.Index); + + var col = o.ColumnInLine; + if (col < 0) col = 0; + if (col >= line.Length) + { + return token; + } + + const int ctx = 16; + var start = Math.Max(0, col - ctx); + var end = Math.Min(line.Length, col + 1 + ctx); + + var prefix = start > 0 ? "…" : string.Empty; + var suffix = end < line.Length ? "…" : string.Empty; + + var left = line.Substring(start, col - start); + var right = line.Substring(col + 1, end - (col + 1)); + + return prefix + left + token + right + suffix; + } + + private void HighlightSelectedPlaceholderInPreview() + { + if (_rtbNotePreview == null || string.IsNullOrEmpty(_rtbNotePreview.Text)) + { + return; + } + + if (_gvNoteBindings.SelectedRows.Count == 0) + { + return; + } + + var row = _gvNoteBindings.SelectedRows[0]; + if (row == null) + { + return; + } + + var idxObj = row.Cells[0].Value; + int idx; + if (idxObj == null || !int.TryParse(idxObj.ToString(), out idx) || idx <= 0) + { + return; + } + + var token = string.Format("【#{0}】", idx); + var pos = _rtbNotePreview.Text.IndexOf(token, StringComparison.Ordinal); + if (pos < 0) + { + return; + } + + _rtbNotePreview.Select(pos, token.Length); + _rtbNotePreview.ScrollToCaret(); + } + private void OnSave() { var schema = GetSelectedSchema(); @@ -499,14 +817,66 @@ namespace CadParamPluging.UI schema.SheetSize = _cbSheetSize.Text; schema.Scale = _cbScale.Text; schema.DisplayName = _txtDisplayName.Text; - schema.SelectedParamKeys = _lvParams.Items + + var selectedParamKeys = _lvParams.Items .Cast() .Where(i => i.Checked) .Select(i => (string)i.Tag) + .Where(k => !string.IsNullOrWhiteSpace(k)) .ToList(); + + // “备注参数”仅在附注绑定中配置,不出现在模板参数绑定里。 + selectedParamKeys = selectedParamKeys + .Where(k => + { + var def = _catalog.FindByKey(k); + if (def == null) + { + return true; + } + return !string.Equals(def.Group ?? string.Empty, "备注参数", StringComparison.OrdinalIgnoreCase); + }) + .ToList(); + + schema.SelectedParamKeys = selectedParamKeys; + + schema.NoteTemplateText = _txtNoteTemplate.Text ?? string.Empty; + var placeholderCount = NoteTemplateEngine.CountPlaceholders(schema.NoteTemplateText); + if (_gvNoteBindings.Rows.Count != placeholderCount) + { + ReloadNoteBindingsFromTemplateText(); + MessageBox.Show(this, "附注模板已变化,已重新解析占位符。请确认附注绑定后再保存。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var noteBindings = new List(); + if (placeholderCount > 0) + { + var map = ReadNoteBindingMapFromGrid(); + for (var i = 1; i <= placeholderCount; i++) + { + if (!map.TryGetValue(i, out var key) || string.IsNullOrWhiteSpace(key)) + { + MessageBox.Show(this, string.Format("请绑定第 {0} 个占位符(*)对应的参数。", i), "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + var def = _catalog.FindByKey(key); + if (def == null || !string.Equals(def.Group ?? string.Empty, "备注参数", StringComparison.OrdinalIgnoreCase)) + { + MessageBox.Show(this, string.Format("占位符 #{0} 绑定的参数不在【备注参数】分组:{1}", i, key), "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + noteBindings.Add(new NotePlaceholderBinding { Index = i, ParamKey = key.Trim() }); + } + } + + schema.NoteBindings = noteBindings; schema.Normalize(); if ((_schemas.Items ?? new List()) + .Where(x => x != null) .Any(x => !ReferenceEquals(x, schema) && string.Equals(x.TemplateKey, schema.TemplateKey, StringComparison.OrdinalIgnoreCase))) { MessageBox.Show(this, "保存失败:存在重复的四参数组合。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);