// ============================================================ // CadBridge:WinForms 宿主与网页 CAD(CAD-Viewer)之间的互操作桥 // · 下行(C#→网页):PostMessage 发 startPick/zoomToHandle/deleteHandles/clearHighlight // · 上行(网页→C#):WebMessageReceived 收 paPicked/entityErased/paExited // · 扒图结果与实体删除事件以 C# 事件形式提供给主窗口(替代 AutoCAD 版 // 的 PaTextSelector.BatchTextsPicked 与 Database.ObjectErased) // ============================================================ using System; using System.Collections.Generic; using System.Web.Script.Serialization; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; namespace 成套报价软件 { public static class CadBridge { private static WebView2 _wv; private static readonly JavaScriptSerializer _json = new JavaScriptSerializer(); /// 网页 CAD 是否已挂载(WebView2 已附加)。 public static bool IsViewerReady { get { return _wv != null && _wv.CoreWebView2 != null; } } /// 网页 CAD 当前打开的图纸名(去扩展名;paPicked 消息携带更新,未带为空)。 /// 扒图写入箱号「图纸」列用。 public static string CurrentDrawingName { get; private set; } /// 记录网页 CAD 上报的图纸名(去扩展名;路径分隔符兼容 / 与 \)。 private static void SetCurrentDrawingName(string raw) { if (string.IsNullOrWhiteSpace(raw)) return; try { CurrentDrawingName = System.IO.Path.GetFileNameWithoutExtension(raw.Replace('/', '\\')); } catch { CurrentDrawingName = raw; } } /// 网页 CAD 中实体被删除(句柄十六进制字符串)。 public static event Action EntityErased; /// 网页 CAD 加载渲染进度变化(文本已拼好,直接显示: /// "加载中 35% · 阶段 PARSE · 12.3s" / "渲染完成 · 45.2s" / "打开失败")。 public static event Action 加载状态; /// 网页 CAD 扒图完成一批(图号/箱号/元件)。 public static event Action> PaPicked; /// 用户按 ESC 退出网页扒图连续模式。 public static event Action PaExited; /// 用户按 F1(焦点在网页或宿主时都触发)——元件两两合并。 public static event Action F1Requested; /// 用户按 F2——元件三个一组合并。 public static event Action F2Requested; /// 用户按 F3——元件四个一组合并。 public static event Action F3Requested; /// 触发 F1 请求(消息过滤器等宿主侧来源调用)。 public static void RaiseF1() { if (F1Requested != null) F1Requested(); } /// 触发 F2 请求(消息过滤器等宿主侧来源调用)。 public static void RaiseF2() { if (F2Requested != null) F2Requested(); } /// 触发 F3 请求(消息过滤器等宿主侧来源调用)。 public static void RaiseF3() { if (F3Requested != null) F3Requested(); } /// 把 WebView2 挂到桥上(在控件初始化完成后调用一次)。 public static void Attach(WebView2 wv) { if (_wv == wv) return; if (_wv != null) _wv.WebMessageReceived -= OnMessage; _wv = wv; if (_wv != null) _wv.WebMessageReceived += OnMessage; } private static void OnMessage(object sender, CoreWebView2WebMessageReceivedEventArgs e) { try { if (e.TryGetWebMessageAsString() != null) return; // 只处理 JSON 消息 } catch { } try { var msg = _json.Deserialize>(e.WebMessageAsJson); if (msg == null || !msg.ContainsKey("type")) return; string type = msg["type"] as string; if (type == "paPicked") { int mode = Convert.ToInt32(msg["mode"]); // 网页端随批携带当前图纸文件名(未带时保留上次值) if (msg.ContainsKey("fileName")) SetCurrentDrawingName(Convert.ToString(msg["fileName"])); // 诊断:记录 fileName 是否上报(排查网页旧缓存/未带字段)——logs\cadbridge.log try { string logDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); if (!System.IO.Directory.Exists(logDir)) System.IO.Directory.CreateDirectory(logDir); System.IO.File.AppendAllText(System.IO.Path.Combine(logDir, "cadbridge.log"), DateTime.Now.ToString("HH:mm:ss.fff") + " paPicked mode=" + mode + " fileName=[" + (msg.ContainsKey("fileName") ? Convert.ToString(msg["fileName"]) : "<无字段>") + "] Current=[" + (CurrentDrawingName ?? "") + "]" + Environment.NewLine); } catch { } var results = new List(); var arr = msg["results"] as System.Collections.ArrayList; if (arr != null) { foreach (Dictionary item in arr) { results.Add(new MyApp.PaTextResult { Text = Convert.ToString(item["text"]) ?? "", FrameHandle = Convert.ToString(item["frameHandle"]) ?? "", TextHandle = Convert.ToString(item["textHandle"]) ?? "" }); } } // WebMessageReceived 在 UI 线程触发,可直接调用主窗口 if (PaPicked != null) PaPicked((MyApp.PaMode)mode, results); } else if (type == "drawingName") { // 网页打开图纸时主动上报文件名(不等扒图)——右键新建箱号也要用到 SetCurrentDrawingName(Convert.ToString(msg["fileName"])); } else if (type == "drawingOpened") { // ★ 网页图纸就绪(真名通知)——读该图的蒙版回灌。 // 存储主位置=图纸旁边的 sidecar 文件 <图纸完整路径>.cadmask.json // (与 PDF 的 xxx.pdf.pdfmask.json 同模式,数据跟图纸文件走); // sidecar 不存在时兜底读软件目录的旧存档(cadbt\扒图蒙版\,迁移期兼容) string key = msg.ContainsKey("key") ? Convert.ToString(msg["key"]) : ""; string dwgPath = msg.ContainsKey("dwgPath") ? Convert.ToString(msg["dwgPath"]) : ""; // ★ 去重:同 key 3 秒内的重复通知丢弃(实测出现过 16 连发风暴, // 每发都触发回灌+挂起,恢复调度全部错乱) if (key == _lastNotifyKey && (DateTime.Now - _lastNotifyAt).TotalSeconds < 3) { 记日志("drawingOpened 重复通知已忽略: [" + key + "]"); return; } _lastNotifyKey = key; _lastNotifyAt = DateTime.Now; 记日志("收到 drawingOpened key=[" + key + "] dwgPath=[" + dwgPath + "]"); _lastMaskKey = key; _lastDwgPath = dwgPath; 回灌蒙版文件(key, dwgPath); } else if (type == "framesSaved") { // ★ 网页每次画框/删框/合并后上报——写入该图纸的蒙版存档 string key = msg.ContainsKey("key") ? Convert.ToString(msg["key"]) : ""; string dwgPath = msg.ContainsKey("dwgPath") ? Convert.ToString(msg["dwgPath"]) : ""; if (!string.IsNullOrWhiteSpace(key) && msg.ContainsKey("frames")) { try { string json = _json.Serialize(msg["frames"]); // 主存储:图纸旁边的 sidecar(有完整路径时);兜底:软件目录旧位置 if (!string.IsNullOrWhiteSpace(dwgPath)) { string sidecar = dwgPath + ".cadmask.json"; System.IO.File.WriteAllText(sidecar, json); } else { string f = 蒙版文件路径(key); System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(f)); System.IO.File.WriteAllText(f, json); } } catch (Exception exW) { 记日志("蒙版文件写入失败[" + key + "]: " + exW.Message); } } } else if (type == "entityErased") { string handle = Convert.ToString(msg["handle"]); if (!string.IsNullOrWhiteSpace(handle) && EntityErased != null) EntityErased(handle); } else if (type == "paError") { string errMsg = Convert.ToString(msg["message"]); 记日志("paError: " + errMsg); // ★ 先落日志(模态弹窗会打断渲染,进度类上报绝不弹) System.Windows.Forms.MessageBox.Show( "网页 CAD 返回错误:\n" + errMsg, "扒数据", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Warning); } else if (type == "loadProgress") { // ★ 三段进度:pct 0~100=文件解析;CONVERT_DONE=解析完/正式渲染开始; // RENDER=渲染中(r=渲染百分比);RENDER_DONE=渲染完成(画面就绪,secs=全程耗时) int pct = msg.ContainsKey("pct") ? Convert.ToInt32(msg["pct"]) : 0; string stage = msg.ContainsKey("stage") ? Convert.ToString(msg["stage"]) : ""; string secs = msg.ContainsKey("secs") ? Convert.ToString(msg["secs"]) : "0"; int r = msg.ContainsKey("r") ? Convert.ToInt32(msg["r"]) : 0; string text; if (pct < 0) { text = "打开失败(文件无法解析)"; 记日志("[加载] " + text); } else if (stage == "RENDER_DONE") { text = "渲染完成 · " + secs + "s"; 记日志("[加载] 渲染完成(含正式渲染), 全程 " + secs + " 秒"); } else if (stage == "RENDER") { text = "渲染中 " + r + "% · " + secs + "s"; if ((DateTime.Now - _lastProgLogAt).TotalSeconds >= 5) { _lastProgLogAt = DateTime.Now; 记日志("[加载] " + text); } } else if (stage == "CONVERT_DONE") { text = "解析完成(" + secs + "s), 正式渲染中..."; 记日志("[加载] 文件解析完成 " + secs + " 秒, 正式渲染开始"); } else if (pct >= 100) { text = "解析完成(" + secs + "s)"; } else { text = "加载中 " + pct + "% · " + stage + " · " + secs + "s"; if ((DateTime.Now - _lastProgLogAt).TotalSeconds >= 5) { _lastProgLogAt = DateTime.Now; 记日志("[加载] " + text); } } if (加载状态 != null) 加载状态(text); } else if (type == "webLog") { // ★ 网页端诊断通道:只写 cadbridge.log,绝不弹窗(蒙版恢复各阶段/页面异常) 记日志(Convert.ToString(msg["message"])); } else if (type == "f1") { if (F1Requested != null) F1Requested(); } else if (type == "f2") { if (F2Requested != null) F2Requested(); } else if (type == "f3") { if (F3Requested != null) F3Requested(); } else if (type == "paExited") { if (PaExited != null) PaExited(); } } catch (Exception) { // 消息格式异常直接忽略,不影响宿主 } } private static void Post(object msg) { if (!IsViewerReady) return; try { _wv.CoreWebView2.PostWebMessageAsJson(_json.Serialize(msg)); } catch { } } // ---------- 蒙版文件持久化(cadbt\扒图蒙版\<图纸名键>.json) ---------- /// 图纸键(网页 framesKey 同款:cadbt_pa_frames:名字)对应的蒙版文件路径。 /// 键里的':'换成'_'并清洗文件名非法字符。 private static string 蒙版文件路径(string key) { string safe = ""; char[] bad = System.IO.Path.GetInvalidFileNameChars(); foreach (char c in (key ?? "")) { bool invalid = c == ':' || c == '\\' || c == '/'; if (!invalid) foreach (char b in bad) if (b == c) { invalid = true; break; } safe += invalid ? '_' : c; } string dir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cadbt", "扒图蒙版"); return System.IO.Path.Combine(dir, safe + ".json"); } /// 宿主"打开CAD图纸"按钮待加载的图纸完整路径(取文件通道按此回字节)。 internal static string 待加载图纸路径 = ""; /// 最后一次 drawingOpened 的 key/完整路径(手动"显示蒙版"按钮回灌用)。 private static string _lastMaskKey = ""; private static string _lastDwgPath = ""; private static string _lastNotifyKey = ""; private static DateTime _lastNotifyAt = DateTime.MinValue; /// 中间进度日志节流(loadProgress 每秒一条,日志 5 秒一条)。 private static DateTime _lastProgLogAt = DateTime.MinValue; /// 宿主端"打开CAD图纸"按钮:设置待加载路径并通知网页从同源通道加载。 /// 蒙版 sidecar(.cadmask.json)就存在图纸旁边(与 PDF 同模式)。 public static void OpenLocalDrawing(string dwgPath, string name) { 待加载图纸路径 = dwgPath; Post(new { type = "openLocalFile", url = "http://localhost:5173/localfile/current", name = name, dwgPath = dwgPath }); 记日志("宿主打开CAD图纸: " + dwgPath); } /// 手动"显示蒙版"按钮:先回灌存档(网页只 stash),再下令绘制—— /// 用户点它时图纸必然可见=渲染稳定,与扒图场景同级,绕开自动时机的所有坑。 public static void ReloadMasks() { if (!string.IsNullOrWhiteSpace(_lastMaskKey)) { 回灌蒙版文件(_lastMaskKey, _lastDwgPath); } Post(new { type = "drawMasks" }); 记日志("【显示蒙版】按钮: 回灌+绘制指令已发送"); } /// 读该图纸的蒙版并回灌网页(restoreFramesFromFile)。 /// 优先级:①图纸旁边的 sidecar(.cadmask.json) ②软件目录旧存档(迁移兼容)。 private static void 回灌蒙版文件(string key, string dwgPath) { try { if (string.IsNullOrWhiteSpace(key)) return; string json = null, 来源 = ""; if (!string.IsNullOrWhiteSpace(dwgPath)) { string sidecar = dwgPath + ".cadmask.json"; if (System.IO.File.Exists(sidecar)) { json = System.IO.File.ReadAllText(sidecar); 来源 = "sidecar(图纸同目录)"; } } if (json == null) { string f = 蒙版文件路径(key); if (System.IO.File.Exists(f)) { json = System.IO.File.ReadAllText(f); 来源 = "旧位置(软件目录)"; } } if (json == null) { 记日志("蒙版存档不存在(跳过回灌): key=" + key + " dwgPath=" + dwgPath); return; } var frames = _json.Deserialize(json); if (frames != null && frames.Count > 0) { Post(new { type = "restoreFramesFromFile", frames = frames }); 记日志("蒙版回灌(" + 来源 + "): " + key + " 共 " + frames.Count + " 框"); } } catch (Exception exR) { 记日志("蒙版文件回灌失败[" + key + "]: " + exR.Message); } } /// cadbridge 诊断日志(logs\cadbridge.log,与 paPicked 诊断同文件)。 private static void 记日志(string msg) { try { string logDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); if (!System.IO.Directory.Exists(logDir)) System.IO.Directory.CreateDirectory(logDir); System.IO.File.AppendAllText(System.IO.Path.Combine(logDir, "cadbridge.log"), DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + Environment.NewLine); } catch { } } /// /// 发送扒图命令。接受原 CAD 命令名(CADPATU/CADPAXH/CADPAYJ/CADPACL) /// 或网页命令名(patu/paxh/payj)。 /// public static void SendPaCommand(string cmd) { if (string.IsNullOrWhiteSpace(cmd)) return; string c = cmd.Trim().ToUpperInvariant(); int mode = 0; if (c == "CADPATU" || c == "PATU") mode = 0; else if (c == "CADPAXH" || c == "PAXH") mode = 1; else if (c == "CADPAYJ" || c == "PAYJ") mode = 2; else if (c == "CADPACL" || c == "PACL") { ClearHighlight(); return; } else return; Post(new { type = "startPick", mode = mode }); } /// 缩放居中到指定 Handle 的实体并高亮(点击表格行定位框)。 public static void ZoomToFrameHandle(string handle) { if (string.IsNullOrWhiteSpace(handle)) return; Post(new { type = "zoomToHandle", handle = handle }); } /// 批量删除指定 Handle 的实体(删表格行联动删框)。 public static void DeleteFramesByHandles(IEnumerable handles) { if (handles == null) return; var list = new List(); foreach (string h in handles) if (!string.IsNullOrWhiteSpace(h)) list.Add(h); if (list.Count == 0) return; Post(new { type = "deleteHandles", handles = list }); } /// /// 合并蒙版:保留 keep 句柄的蒙版并扩大到所有 removes 蒙版的并集, /// 被合并行的蒙版从网页 CAD 移除(元件合并时蒙版同步合并)。 /// public static void MergeMasks(string keep, System.Collections.Generic.IEnumerable removes) { if (string.IsNullOrWhiteSpace(keep)) return; var list = new List(); if (removes != null) foreach (string h in removes) if (!string.IsNullOrWhiteSpace(h) && h != keep) list.Add(h); if (list.Count == 0) return; Post(new { type = "mergeMasks", keep = keep, removes = list }); } /// 清除网页 CAD 中的所有高亮。 public static void ClearHighlight() { Post(new { type = "clearHighlight" }); } /// 显示箱号组虚线框:把该箱号的箱号图框+全部元件图框求并集, /// 网页 CAD 画一个橙色虚线矩形(点元件表/箱号表行时调用)。 public static void SetGroupFrames(System.Collections.Generic.IEnumerable handles) { if (handles == null) return; var list = new List(); foreach (string h in handles) if (!string.IsNullOrWhiteSpace(h)) list.Add(h); Post(new { type = "groupHighlight", handles = list }); } /// 清除箱号组虚线框(点图号表行时)。 public static void ClearGroupFrames() { Post(new { type = "groupHighlightClear" }); } /// 画组虚线框并缩放到"装下整个组框"(点箱号/元件行跳转时)。 /// focusHandle=点中的那个框:网页端对它画黄色高亮框。 public static void ZoomToGroupFrames(System.Collections.Generic.IEnumerable handles, string focusHandle = null) { if (handles == null) return; var list = new List(); foreach (string h in handles) if (!string.IsNullOrWhiteSpace(h)) list.Add(h); Post(new { type = "zoomToGroup", handles = list, focus = focusHandle ?? "" }); } } }