dsh-engineering-ui 0.0.0-stage → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +772 -3
  3. package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
  4. package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
  5. package/engineering/plugins/dsh-engineering-ui/lib/client.js +8259 -0
  6. package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
  7. package/engineering/plugins/dsh-engineering-ui/lib/index.js +5633 -0
  8. package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
  9. package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
  10. package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
  11. package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
  12. package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
  13. package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
  14. package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
  15. package/engineering/skills/cad-workflow/SKILL.md +132 -0
  16. package/engineering/skills/mode-selection/SKILL.md +660 -0
  17. package/engineering/skills/physics-in-loop/SKILL.md +238 -0
  18. package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
  19. package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
  20. package/engineering/skills/sw-design/SKILL.md +112 -0
  21. package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
  22. package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
  23. package/engineering/skills/sw-to-cad/SKILL.md +121 -0
  24. package/engineering/skills/webots-simulation/SKILL.md +58 -0
  25. package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
  26. package/engineering/tools/_plugin_paths.py +93 -0
  27. package/engineering/tools/_root.py +330 -0
  28. package/engineering/tools/_store.py +446 -0
  29. package/engineering/tools/ac_bridge.py +459 -0
  30. package/engineering/tools/ac_rules.json +21 -0
  31. package/engineering/tools/ac_validate.py +699 -0
  32. package/engineering/tools/ask_user.py +887 -0
  33. package/engineering/tools/build_arm1.py +46 -0
  34. package/engineering/tools/build_assembly.py +94 -0
  35. package/engineering/tools/build_base.py +57 -0
  36. package/engineering/tools/build_housing.py +70 -0
  37. package/engineering/tools/build_structural.py +43 -0
  38. package/engineering/tools/build_transmission.py +69 -0
  39. package/engineering/tools/cadx.lsp +128 -0
  40. package/engineering/tools/choice_contract.py +61 -0
  41. package/engineering/tools/conn_state.py +1671 -0
  42. package/engineering/tools/defense_gate.py +3374 -0
  43. package/engineering/tools/drawing_input.py +314 -0
  44. package/engineering/tools/env_check.py +467 -0
  45. package/engineering/tools/examples/build_axis.py +142 -0
  46. package/engineering/tools/examples/build_cup.py +56 -0
  47. package/engineering/tools/examples/build_plate.py +58 -0
  48. package/engineering/tools/examples/build_sphere.py +30 -0
  49. package/engineering/tools/examples/build_transition_duct.py +381 -0
  50. package/engineering/tools/gate_integrity.py +401 -0
  51. package/engineering/tools/mode2_flow.py +3589 -0
  52. package/engineering/tools/mode_checker.py +77 -0
  53. package/engineering/tools/mode_gate.py +8013 -0
  54. package/engineering/tools/part_registry.py +4266 -0
  55. package/engineering/tools/physics/__init__.py +50 -0
  56. package/engineering/tools/physics/_compat.py +12 -0
  57. package/engineering/tools/physics/design_state.py +142 -0
  58. package/engineering/tools/physics/domain_validator.py +751 -0
  59. package/engineering/tools/physics/fatigue.py +489 -0
  60. package/engineering/tools/physics/fea_solver.py +1163 -0
  61. package/engineering/tools/physics/feapy_solver.py +314 -0
  62. package/engineering/tools/physics/gb_materials.json +1228 -0
  63. package/engineering/tools/physics/geometry_gate.py +337 -0
  64. package/engineering/tools/physics/load_case.py +951 -0
  65. package/engineering/tools/physics/material_db.py +874 -0
  66. package/engineering/tools/physics/mesh_adapter.py +321 -0
  67. package/engineering/tools/physics/refine_rules.py +272 -0
  68. package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
  69. package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
  70. package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
  71. package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
  72. package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
  73. package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
  74. package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
  75. package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
  76. package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
  77. package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
  78. package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
  79. package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
  80. package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
  81. package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
  82. package/engineering/tools/physics/rules/housing_rules.json +48 -0
  83. package/engineering/tools/physics/rules/manifest.json +54 -0
  84. package/engineering/tools/physics/rules/mold_rules.json +48 -0
  85. package/engineering/tools/physics/rules/structural_rules.json +56 -0
  86. package/engineering/tools/physics/rules/transmission_rules.json +48 -0
  87. package/engineering/tools/physics/simulation_report.py +829 -0
  88. package/engineering/tools/physics_bridge.py +2685 -0
  89. package/engineering/tools/profile-manifest.ps1 +72 -0
  90. package/engineering/tools/run_regression.py +109 -0
  91. package/engineering/tools/settings.py +419 -0
  92. package/engineering/tools/solidworks-modeling.md +519 -0
  93. package/engineering/tools/sw_bridge.py +8215 -0
  94. package/engineering/tools/sw_guard.py +1066 -0
  95. package/engineering/tools/sw_surface.py +87 -0
  96. package/engineering/tools/swapi.py +14766 -0
  97. package/engineering/tools/verify_sync.py +218 -0
  98. package/engineering/tools/visual_check.py +2589 -0
  99. package/engineering/tools/webots_env.py +121 -0
  100. package/engineering/tools/webots_mcp/__init__.py +3 -0
  101. package/engineering/tools/webots_mcp/__main__.py +3 -0
  102. package/engineering/tools/webots_mcp/assembly.py +391 -0
  103. package/engineering/tools/webots_mcp/bootstrap.py +55 -0
  104. package/engineering/tools/webots_mcp/controller_worker.py +124 -0
  105. package/engineering/tools/webots_mcp/evidence.py +288 -0
  106. package/engineering/tools/webots_mcp/fake_worker.py +69 -0
  107. package/engineering/tools/webots_mcp/fault_resources.py +121 -0
  108. package/engineering/tools/webots_mcp/host.py +45 -0
  109. package/engineering/tools/webots_mcp/ledger.py +73 -0
  110. package/engineering/tools/webots_mcp/owned_job.py +70 -0
  111. package/engineering/tools/webots_mcp/physical_gate.py +187 -0
  112. package/engineering/tools/webots_mcp/physics.py +152 -0
  113. package/engineering/tools/webots_mcp/pipeline.py +108 -0
  114. package/engineering/tools/webots_mcp/recipes.py +129 -0
  115. package/engineering/tools/webots_mcp/repair.py +132 -0
  116. package/engineering/tools/webots_mcp/repair_trial.py +375 -0
  117. package/engineering/tools/webots_mcp/runtime.py +311 -0
  118. package/engineering/tools/webots_mcp/server.py +217 -0
  119. package/engineering/tools/webots_mcp/sw_export.py +509 -0
  120. package/engineering/tools/webots_mcp/transport.py +156 -0
  121. package/engineering/tools/workflow_gate.py +6134 -0
  122. package/package.json +64 -4
@@ -0,0 +1,699 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ac_validate.py — DXF / AutoCAD 图纸几何验证引擎
4
+ ==================================================
5
+ 从 CADX 提取并适配的机械制图验证引擎,供 DSH 工程模式使用。
6
+
7
+ 功能:
8
+ - 加载 DXF 文件(ezdxf)或直接读取 AutoCAD 实体(ac_bridge)
9
+ - 7 项几何/结构验证检查
10
+ - 返回 Issue 列表(含 severity / location / fix_suggestion)
11
+
12
+ 检查项(7项):
13
+ 1. UNCLOSED_POLYLINE — 应闭合但未闭合的闭合多段线
14
+ 2. OVERLAPPING_GEOMETRY — 墙体/轮廓线错误重叠
15
+ 3. ROOM_TOO_SMALL — 房间面积低于最低标准
16
+ 4. ROOM_ASPECT_RATIO — 房间长宽比异常
17
+ 5. WALL_TOO_THIN — 平行墙间距低于最小厚度
18
+ 6. MISSING_ELEMENTS — 房间缺少门/窗
19
+ 7. DIMENSION_MISMATCH — 标注值与实际几何不符
20
+
21
+ 依赖:
22
+ - ezdxf(用于 DXF 解析,可选:没有时回退到直接读取 AutoCAD)
23
+ - shapely(用于几何运算,可选:没有时跳过重叠检查)
24
+ - ac_bridge(用于实时读取 AutoCAD 实体)
25
+
26
+ 用法(DXF 文件):
27
+ from ac_validate import validate_dxf
28
+ issues = validate_dxf("output/my_drawing.dxf", rules="mechanical")
29
+
30
+ 用法(直连 AutoCAD):
31
+ import ac_bridge
32
+ from ac_validate import validate_live
33
+ ac = ac_bridge.connect()
34
+ issues = validate_live(ac, rules="mechanical")
35
+ """
36
+ import json
37
+ import math
38
+ import os
39
+ import re
40
+ from dataclasses import dataclass, field, asdict
41
+ from typing import Optional, List, Dict, Any
42
+
43
+
44
+ # ==================== Issue 数据类 ====================
45
+
46
+ @dataclass
47
+ class Issue:
48
+ """验证问题。"""
49
+ id: int
50
+ check: str # 检查类型
51
+ severity: str # CRITICAL / WARNING / INFO
52
+ message: str
53
+ location_x: float = 0.0
54
+ location_y: float = 0.0
55
+ entity_handle: str = ""
56
+ entity_layer: str = ""
57
+ rule_key: str = ""
58
+ expected: str = ""
59
+ actual: str = ""
60
+ suggested_fix: str = ""
61
+
62
+ def to_dict(self) -> dict:
63
+ return asdict(self)
64
+
65
+
66
+ # ==================== 规则加载 ====================
67
+
68
+ _RULES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ac_rules.json")
69
+
70
+ _DEFAULT_RULES = {
71
+ "min_room_area_sqm": 9.5,
72
+ "min_wall_thickness_mm": 150,
73
+ "max_wall_thickness_mm": 300,
74
+ "min_door_width_mm": 750,
75
+ "max_door_width_mm": 1200,
76
+ "min_window_area_sqm": 0.5,
77
+ "ventilation_pct_of_floor": 10,
78
+ "max_stair_riser_mm": 160,
79
+ "min_stair_tread_mm": 250,
80
+ "min_ceiling_height_m": 2.7,
81
+ "required_layers": ["WALLS", "DOORS", "WINDOWS", "DIMENSIONS"],
82
+ "layer_name_pattern": r"^[A-Z][A-Z0-9_-]*$",
83
+ "min_room_width_m": 2.4,
84
+ "min_room_length_m": 3.0,
85
+ "max_room_aspect_ratio": 3.0,
86
+ "unit_scale": 1.0,
87
+ "gap_threshold_mm": 10.0,
88
+ }
89
+
90
+
91
+ def _load_rules(rules: str = "mechanical") -> dict:
92
+ """加载验证规则。
93
+
94
+ Args:
95
+ rules: "mechanical" | "general" | "residential" | 自定义 JSON 路径
96
+ """
97
+ if os.path.isfile(rules):
98
+ with open(rules, "r", encoding="utf-8") as f:
99
+ return {**_DEFAULT_RULES, **json.load(f)}
100
+
101
+ rules_map = {
102
+ "mechanical": {
103
+ "min_room_area_sqm": 6.0,
104
+ "min_wall_thickness_mm": 100,
105
+ "max_wall_thickness_mm": 500,
106
+ "min_door_width_mm": 700,
107
+ "max_door_width_mm": 2000,
108
+ "required_layers": ["OUTLINE", "THIN", "CENTER", "HIDDEN", "DIM", "TEXT"],
109
+ "layer_name_pattern": r"^[A-Z][A-Z0-9_-]*$",
110
+ },
111
+ "general": {
112
+ "min_room_area_sqm": 4.0,
113
+ "min_wall_thickness_mm": 100,
114
+ "max_wall_thickness_mm": 500,
115
+ "min_door_width_mm": 600,
116
+ "max_door_width_mm": 2000,
117
+ "required_layers": [],
118
+ },
119
+ "residential": {
120
+ "min_room_area_sqm": 9.5,
121
+ "min_wall_thickness_mm": 150,
122
+ "max_wall_thickness_mm": 300,
123
+ "min_door_width_mm": 750,
124
+ "max_door_width_mm": 1200,
125
+ "required_layers": ["WALLS", "DOORS", "WINDOWS"],
126
+ },
127
+ }
128
+ return {**_DEFAULT_RULES, **(rules_map.get(rules, {}))}
129
+
130
+
131
+ # ==================== DXF 分析器(基于 ezdxf) ====================
132
+
133
+ class DXFAnalyzer:
134
+ """基于 ezdxf 的 DXF 文件验证引擎。"""
135
+
136
+ def __init__(self, dxf_path: str, rules: str = "mechanical"):
137
+ self.dxf_path = dxf_path
138
+ self.rules = _load_rules(rules)
139
+ self.scale = self.rules.get("unit_scale", 1.0)
140
+ self.issues: List[Issue] = []
141
+ self._counter = 0
142
+ self._doc = None
143
+ self._msp = None
144
+ self._load_dxf()
145
+
146
+ def _load_dxf(self):
147
+ """加载 DXF 文件。"""
148
+ try:
149
+ import ezdxf
150
+ self._doc = ezdxf.readfile(self.dxf_path)
151
+ self._msp = self._doc.modelspace()
152
+ except ImportError:
153
+ raise RuntimeError(
154
+ "ezdxf 未安装,无法验证 DXF 文件。\n"
155
+ "请运行: pip install ezdxf shapely"
156
+ )
157
+ except Exception as e:
158
+ raise RuntimeError(f"无法打开 DXF 文件: {e}")
159
+
160
+ def _next_id(self) -> int:
161
+ self._counter += 1
162
+ return self._counter
163
+
164
+ def _to_mm(self, value: float) -> float:
165
+ return value * self.scale
166
+
167
+ def _to_sqm(self, area: float) -> float:
168
+ return area * (self.scale ** 2) / 1_000_000
169
+
170
+ # ------------------------------------------------------------------
171
+ # 实体辅助
172
+ # ------------------------------------------------------------------
173
+
174
+ def _entities_on_layer(self, layer: str):
175
+ """按图层过滤实体(大小写不敏感)。"""
176
+ for e in self._msp:
177
+ if e.dxf.layer.upper() == layer.upper():
178
+ yield e
179
+
180
+ def _all_layers(self) -> list:
181
+ """收集文档中所有已使用的图层名称(包括实体实际所在图层)。
182
+
183
+ Bug-3 修复: 原来只读文档级图层表(可能只有 ['0', 'Defpoints']),
184
+ 但实体可能挂在未注册的自定义层上,导致 MISSING_LAYER 误报。
185
+ 现在同时扫描实体所在的图层,确保检测到所有实际使用的层。
186
+ """
187
+ layers = set()
188
+ try:
189
+ for l in self._doc.layers:
190
+ layers.add(l.dxf.name)
191
+ except Exception:
192
+ pass
193
+ # 从实体实际所在图层收集(兜底,防止文档层表不完整)
194
+ for e in self._msp:
195
+ try:
196
+ ln = getattr(e.dxf, 'layer', None)
197
+ if ln:
198
+ layers.add(ln)
199
+ except Exception:
200
+ pass
201
+ return list(layers)
202
+
203
+ def _entity_midpoint(self, entity) -> tuple:
204
+ """返回实体的中点坐标(近似)。"""
205
+ try:
206
+ if entity.dxftype() == "LINE":
207
+ s, e = entity.dxf.start, entity.dxf.end
208
+ return ((s.x + e.x) / 2, (s.y + e.y) / 2)
209
+ if entity.dxftype() == "LWPOLYLINE":
210
+ pts = list(entity.get_points(format="xy"))
211
+ if pts:
212
+ return (sum(p[0] for p in pts) / len(pts),
213
+ sum(p[1] for p in pts) / len(pts))
214
+ if entity.dxftype() == "CIRCLE":
215
+ c = entity.dxf.center
216
+ return (c.x, c.y)
217
+ except Exception:
218
+ pass
219
+ return (0.0, 0.0)
220
+
221
+ # ------------------------------------------------------------------
222
+ # 检查 1:未闭合的多段线
223
+ # ------------------------------------------------------------------
224
+
225
+ def check_unclosed_polylines(self):
226
+ gap_mm = self.rules.get("gap_threshold_mm", 10.0)
227
+ for ent in self._msp:
228
+ if ent.dxftype() != "LWPOLYLINE":
229
+ continue
230
+ if ent.closed:
231
+ continue
232
+ pts = list(ent.get_points(format="xy"))
233
+ if len(pts) < 3:
234
+ continue
235
+ dist = math.dist(pts[0], pts[-1])
236
+ if dist < gap_mm:
237
+ mx, my = self._entity_midpoint(ent)
238
+ self.issues.append(Issue(
239
+ id=self._next_id(),
240
+ check="UNCLOSED_POLYLINE",
241
+ severity="INFO",
242
+ message=f"多段线近似闭合(缺口={dist:.1f}mm),建议闭合",
243
+ location_x=mx, location_y=my,
244
+ entity_handle=ent.dxf.handle,
245
+ entity_layer=ent.dxf.layer,
246
+ suggested_fix="将多段线闭合(PEDIT → Close)",
247
+ ))
248
+ else:
249
+ mx, my = self._entity_midpoint(ent)
250
+ self.issues.append(Issue(
251
+ id=self._next_id(),
252
+ check="UNCLOSED_POLYLINE",
253
+ severity="WARNING",
254
+ message=f"未闭合多段线(缺口={dist:.1f}mm)— 图层 '{ent.dxf.layer}'",
255
+ location_x=mx, location_y=my,
256
+ entity_handle=ent.dxf.handle,
257
+ entity_layer=ent.dxf.layer,
258
+ expected="闭合多段线",
259
+ actual=f"开口(缺口={dist:.1f}mm)",
260
+ suggested_fix="闭合多段线端点",
261
+ ))
262
+
263
+ # ------------------------------------------------------------------
264
+ # 检查 2:重叠几何(需要 shapely)
265
+ # ------------------------------------------------------------------
266
+
267
+ def check_overlapping_geometry(self):
268
+ """检测重叠的墙体/轮廓线。"""
269
+ try:
270
+ from shapely.geometry import LineString, Polygon
271
+ from shapely.ops import unary_union
272
+ except ImportError:
273
+ return # shapely 未安装,跳过此检查
274
+
275
+ wall_layers = {"WALLS", "WALL", "WALLS-EXT", "WALLS-INT", "A-WALL",
276
+ "OUTLINE", "THIN"}
277
+ wall_geoms = []
278
+
279
+ for ent in self._msp:
280
+ if ent.dxf.layer.upper() not in wall_layers:
281
+ continue
282
+ if ent.dxftype() == "LINE":
283
+ s, e = ent.dxf.start, ent.dxf.end
284
+ wall_geoms.append((ent, LineString([(s.x, s.y), (e.x, e.y)])))
285
+ elif ent.dxftype() == "LWPOLYLINE":
286
+ try:
287
+ pts = [(p[0], p[1]) for p in ent.get_points(format="xy")]
288
+ if len(pts) >= 2:
289
+ geom = LineString(pts) if not ent.closed else Polygon(pts)
290
+ wall_geoms.append((ent, geom))
291
+ except Exception:
292
+ pass
293
+
294
+ for i in range(len(wall_geoms)):
295
+ for j in range(i + 1, len(wall_geoms)):
296
+ e1, g1 = wall_geoms[i]
297
+ e2, g2 = wall_geoms[j]
298
+ try:
299
+ if not g1.intersects(g2):
300
+ continue
301
+ inter = g1.intersection(g2)
302
+ if inter.is_empty:
303
+ continue
304
+ if hasattr(inter, 'area') and inter.area > 100:
305
+ cx, cy = inter.centroid.x, inter.centroid.y
306
+ self.issues.append(Issue(
307
+ id=self._next_id(),
308
+ check="OVERLAPPING_GEOMETRY",
309
+ severity="CRITICAL",
310
+ message=f"重叠几何:{e1.dxf.handle} ↔ {e2.dxf.handle}",
311
+ location_x=cx, location_y=cy,
312
+ entity_handle=e1.dxf.handle,
313
+ entity_layer=e1.dxf.layer,
314
+ suggested_fix="重新绘制重叠的墙体或轮廓线",
315
+ ))
316
+ elif hasattr(inter, 'length') and inter.length > 50:
317
+ cx, cy = inter.centroid.x, inter.centroid.y
318
+ self.issues.append(Issue(
319
+ id=self._next_id(),
320
+ check="OVERLAPPING_GEOMETRY",
321
+ severity="WARNING",
322
+ message=f"交叉/重合线段:{e1.dxf.handle} ↔ {e2.dxf.handle}(长度={inter.length:.0f}mm)",
323
+ location_x=cx, location_y=cy,
324
+ entity_handle=e1.dxf.handle,
325
+ entity_layer=e1.dxf.layer,
326
+ suggested_fix="检查线条是否意图交叉或需断开",
327
+ ))
328
+ except Exception:
329
+ pass
330
+
331
+ # ------------------------------------------------------------------
332
+ # 检查 3 & 4:房间面积与长宽比
333
+ # ------------------------------------------------------------------
334
+
335
+ def check_room_areas(self):
336
+ min_area = self.rules.get("min_room_area_sqm", 9.5)
337
+ max_ratio = self.rules.get("max_room_aspect_ratio", 3.0)
338
+ room_layers = {"ROOMS", "ROOM", "A-ROOM", "SPACE"}
339
+
340
+ try:
341
+ from shapely.geometry import Polygon
342
+ except ImportError:
343
+ return
344
+
345
+ for ent in self._msp:
346
+ if ent.dxf.layer.upper() not in room_layers:
347
+ continue
348
+ if ent.dxftype() != "LWPOLYLINE" or not ent.closed:
349
+ continue
350
+ try:
351
+ pts = [(p[0], p[1]) for p in ent.get_points(format="xy")]
352
+ if len(pts) < 3:
353
+ continue
354
+ poly = Polygon(pts)
355
+ area_sqm = self._to_sqm(poly.area)
356
+ bounds = poly.bounds
357
+ width_m = (bounds[2] - bounds[0]) * self.scale / 1000
358
+ height_m = (bounds[3] - bounds[1]) * self.scale / 1000
359
+
360
+ if area_sqm < min_area:
361
+ cx, cy = poly.centroid.x, poly.centroid.y
362
+ self.issues.append(Issue(
363
+ id=self._next_id(),
364
+ check="ROOM_TOO_SMALL",
365
+ severity="CRITICAL",
366
+ message=f"房间面积 {area_sqm:.1f}m² 低于最低标准 {min_area}m²",
367
+ location_x=cx, location_y=cy,
368
+ entity_handle=ent.dxf.handle,
369
+ entity_layer=ent.dxf.layer,
370
+ rule_key="min_room_area_sqm",
371
+ expected=f"≥ {min_area}m²",
372
+ actual=f"{area_sqm:.1f}m²",
373
+ suggested_fix=f"增大房间尺寸,当前缺口 {min_area - area_sqm:.1f}m²",
374
+ ))
375
+
376
+ if width_m > 0 and height_m > 0:
377
+ ratio = max(width_m, height_m) / min(width_m, height_m)
378
+ if ratio > max_ratio:
379
+ cx, cy = poly.centroid.x, poly.centroid.y
380
+ self.issues.append(Issue(
381
+ id=self._next_id(),
382
+ check="ROOM_ASPECT_RATIO",
383
+ severity="WARNING",
384
+ message=f"房间长宽比 {ratio:.1f} 超过最大允许值 {max_ratio}",
385
+ location_x=cx, location_y=cy,
386
+ entity_handle=ent.dxf.handle,
387
+ entity_layer=ent.dxf.layer,
388
+ expected=f"≤ {max_ratio}",
389
+ actual=f"{ratio:.1f}",
390
+ suggested_fix="调整房间比例使其更均衡",
391
+ ))
392
+ except Exception:
393
+ pass
394
+
395
+ # ------------------------------------------------------------------
396
+ # 检查 5:墙体厚度
397
+ # ------------------------------------------------------------------
398
+
399
+ def check_wall_thickness(self):
400
+ min_thick = self.rules.get("min_wall_thickness_mm", 150)
401
+ max_thick = self.rules.get("max_wall_thickness_mm", 300)
402
+ wall_layers = {"WALLS", "WALL", "WALLS-EXT", "WALLS-INT", "A-WALL", "OUTLINE"}
403
+
404
+ lines = []
405
+ for ent in self._msp:
406
+ if ent.dxf.layer.upper() not in wall_layers:
407
+ continue
408
+ if ent.dxftype() == "LINE":
409
+ s = ent.dxf.start
410
+ e = ent.dxf.end
411
+ lines.append((ent, s.x, s.y, e.x, e.y))
412
+
413
+ for i in range(len(lines)):
414
+ for j in range(i + 1, len(lines)):
415
+ e1, x1, y1, x2, y2 = lines[i]
416
+ e2, x3, y3, x4, y4 = lines[j]
417
+ # 方向向量
418
+ dx1, dy1 = x2 - x1, y2 - y1
419
+ dx2, dy2 = x4 - x3, y4 - y3
420
+ len1 = math.sqrt(dx1**2 + dy1**2)
421
+ len2 = math.sqrt(dx2**2 + dy2**2)
422
+ if len1 < 1 or len2 < 1:
423
+ continue
424
+ # 平行检测(叉积 ≈ 0)
425
+ cross = abs(dx1 * dy2 - dy1 * dx2)
426
+ if cross / (len1 * len2) > 0.05:
427
+ continue
428
+ # 中线点投影
429
+ mx2, my2 = (x3 + x4) / 2, (y3 + y4) / 2
430
+ t = ((mx2 - x1) * dx1 + (my2 - y1) * dy1) / (len1 ** 2)
431
+ if t < -0.1 or t > 1.1:
432
+ continue
433
+ proj_x = x1 + dx1 * t
434
+ proj_y = y1 + dy1 * t
435
+ dist_mm = math.sqrt((mx2 - proj_x)**2 + (my2 - proj_y)**2) * self.scale
436
+ if dist_mm > 600:
437
+ continue
438
+ cx = (x1 + x4) / 2
439
+ cy = (y1 + y4) / 2
440
+ if 10 < dist_mm < min_thick:
441
+ self.issues.append(Issue(
442
+ id=self._next_id(),
443
+ check="WALL_TOO_THIN",
444
+ severity="CRITICAL",
445
+ message=f"墙厚 {dist_mm:.0f}mm < 最小 {min_thick}mm",
446
+ location_x=cx, location_y=cy,
447
+ entity_handle=e1.dxf.handle,
448
+ entity_layer=e1.dxf.layer,
449
+ expected=f"≥ {min_thick}mm",
450
+ actual=f"{dist_mm:.0f}mm",
451
+ suggested_fix=f"加宽墙体,当前不足 {min_thick - dist_mm:.0f}mm",
452
+ ))
453
+ elif dist_mm > max_thick:
454
+ self.issues.append(Issue(
455
+ id=self._next_id(),
456
+ check="WALL_TOO_THICK",
457
+ severity="WARNING",
458
+ message=f"墙厚 {dist_mm:.0f}mm > 最大 {max_thick}mm",
459
+ location_x=cx, location_y=cy,
460
+ entity_handle=e1.dxf.handle,
461
+ entity_layer=e1.dxf.layer,
462
+ expected=f"≤ {max_thick}mm",
463
+ actual=f"{dist_mm:.0f}mm",
464
+ suggested_fix="确认墙厚是否合理,如非设计意图则缩减",
465
+ ))
466
+
467
+ # ------------------------------------------------------------------
468
+ # 检查 6:缺失元素(房间无门/窗)
469
+ # ------------------------------------------------------------------
470
+
471
+ def check_missing_elements(self):
472
+ room_layers = {"ROOMS", "ROOM", "A-ROOM", "SPACE"}
473
+ door_layers = {"DOORS", "DOOR", "A-DOOR"}
474
+ window_layers = {"WINDOWS", "WINDOW", "A-WINDOW"}
475
+
476
+ rooms, doors, windows = [], [], []
477
+
478
+ for ent in self._msp:
479
+ layer = ent.dxf.layer.upper()
480
+ if layer in room_layers and ent.dxftype() == "LWPOLYLINE" and ent.closed:
481
+ rooms.append(ent)
482
+ elif layer in door_layers:
483
+ mx, my = self._entity_midpoint(ent)
484
+ doors.append((mx, my))
485
+ elif layer in window_layers:
486
+ mx, my = self._entity_midpoint(ent)
487
+ windows.append((mx, my))
488
+
489
+ try:
490
+ from shapely.geometry import Polygon
491
+ except ImportError:
492
+ return
493
+
494
+ for ent in rooms:
495
+ try:
496
+ pts = [(p[0], p[1]) for p in ent.get_points(format="xy")]
497
+ if len(pts) < 3:
498
+ continue
499
+ poly = Polygon(pts)
500
+ buffered = poly.buffer(500 * self.scale) # 500mm 缓冲
501
+ cx, cy = poly.centroid.x, poly.centroid.y
502
+
503
+ has_door = any(buffered.contains(
504
+ Polygon([(dx-1, dy-1), (dx+1, dy-1), (dx+1, dy+1), (dx-1, dy+1)])
505
+ ) for dx, dy in doors) if doors else True
506
+
507
+ has_window = any(buffered.contains(
508
+ Polygon([(wx-1, wy-1), (wx+1, wy-1), (wx+1, wy+1), (wx-1, wy+1)])
509
+ ) for wx, wy in windows) if windows else True
510
+
511
+ if not has_door and doors:
512
+ self.issues.append(Issue(
513
+ id=self._next_id(),
514
+ check="ROOM_NO_DOOR",
515
+ severity="WARNING",
516
+ message=f"房间(图层 '{ent.dxf.layer}')附近无门",
517
+ location_x=cx, location_y=cy,
518
+ entity_handle=ent.dxf.handle,
519
+ suggested_fix="在房间墙上添加门开口",
520
+ ))
521
+ if not has_window and windows:
522
+ self.issues.append(Issue(
523
+ id=self._next_id(),
524
+ check="ROOM_NO_WINDOW",
525
+ severity="INFO",
526
+ message=f"房间(图层 '{ent.dxf.layer}')附近无窗",
527
+ location_x=cx, location_y=cy,
528
+ entity_handle=ent.dxf.handle,
529
+ suggested_fix="添加窗户以满足通风要求(≥10% 地板面积)",
530
+ ))
531
+ except Exception:
532
+ pass
533
+
534
+ # ------------------------------------------------------------------
535
+ # 检查 7:标注一致性
536
+ # ------------------------------------------------------------------
537
+
538
+ def check_dimension_consistency(self):
539
+ for ent in self._msp:
540
+ if ent.dxftype() != "DIMENSION":
541
+ continue
542
+ try:
543
+ text_override = ent.dxf.get("text", "")
544
+ if not text_override or text_override.strip() in ("", "<>"):
545
+ continue
546
+ try:
547
+ displayed = float(re.sub(r"[^\d.]", "", text_override))
548
+ except ValueError:
549
+ continue
550
+ # 实际测量值
551
+ try:
552
+ measurement = ent.dxf.get("actual_measurement", None)
553
+ if measurement is None:
554
+ continue
555
+ tolerance = max(abs(measurement) * 0.02, 1.0)
556
+ if abs(displayed - measurement) > tolerance:
557
+ mx, my = self._entity_midpoint(ent)
558
+ self.issues.append(Issue(
559
+ id=self._next_id(),
560
+ check="DIMENSION_MISMATCH",
561
+ severity="CRITICAL",
562
+ message=f"标注显示 {displayed},实际几何为 {measurement:.1f}",
563
+ location_x=mx, location_y=my,
564
+ entity_handle=ent.dxf.handle,
565
+ expected=f"{measurement:.1f}",
566
+ actual=f"{displayed}",
567
+ suggested_fix="更新标注值以匹配实际几何,或调整几何",
568
+ ))
569
+ except Exception:
570
+ pass
571
+ except Exception:
572
+ pass
573
+
574
+ # ------------------------------------------------------------------
575
+ # 检查 8:图层命名规范
576
+ # ------------------------------------------------------------------
577
+
578
+ def check_layer_naming(self):
579
+ existing = set(self._all_layers())
580
+ pattern = self.rules.get("layer_name_pattern", "")
581
+ required = self.rules.get("required_layers", [])
582
+
583
+ for req in required:
584
+ if req.upper() not in {l.upper() for l in existing}:
585
+ self.issues.append(Issue(
586
+ id=self._next_id(),
587
+ check="MISSING_LAYER",
588
+ severity="WARNING",
589
+ message=f"缺少必需图层 '{req}'",
590
+ rule_key="required_layers",
591
+ expected=req,
592
+ actual="缺失",
593
+ suggested_fix=f"创建图层 '{req}' 并将相关实体移至该图层",
594
+ ))
595
+
596
+ if pattern:
597
+ import re as _re
598
+ for ln in existing:
599
+ # ── 【BUG-06 修复·实机补强】豁免 AutoCAD/SW 固有保留层 ──────
600
+ # 原实现对 "Defpoints" 用的是【全大写】比较,而 DXF 里实际
601
+ # 存的是 "Defpoints"(首字母大写)→ 大小写不匹配,豁免失效;
602
+ # 同时完全没覆盖 "SLD-0"(SolidWorks 默认层)与工程图内部
603
+ # 的纯数字保留层("5"/"9"/"10"…)。
604
+ # 后果:每次 SW 出图都刷 4~6 条 LAYER_NAME_INVALID INFO,
605
+ # 把真正需要处理的问题淹没在噪声里。
606
+ # 现改为【大小写不敏感 + 覆盖 SLD-0/纯数字层】。
607
+ _ln_k = str(ln).strip().lower()
608
+ if _ln_k in ("0", "defpoints", "dsh_cadx_validation", "sld-0"):
609
+ continue
610
+ if _ln_k.isdigit():
611
+ continue # SW 工程图内部数字保留层
612
+ if not _re.match(pattern, ln):
613
+ self.issues.append(Issue(
614
+ id=self._next_id(),
615
+ check="LAYER_NAME_INVALID",
616
+ severity="INFO",
617
+ message=f"图层 '{ln}' 不符合命名规范",
618
+ entity_layer=ln,
619
+ rule_key="layer_name_pattern",
620
+ expected=f"正则: {pattern}",
621
+ actual=ln,
622
+ suggested_fix="重命名图层(大写、字母数字和下划线)",
623
+ ))
624
+
625
+ # ------------------------------------------------------------------
626
+ # 执行全部检查
627
+ # ------------------------------------------------------------------
628
+
629
+ def validate(self) -> List[Issue]:
630
+ self.issues.clear()
631
+ self._counter = 0
632
+ self.check_unclosed_polylines()
633
+ self.check_overlapping_geometry()
634
+ self.check_room_areas()
635
+ self.check_wall_thickness()
636
+ self.check_layer_naming()
637
+ self.check_missing_elements()
638
+ self.check_dimension_consistency()
639
+ return self.issues
640
+
641
+ def get_summary(self) -> dict:
642
+ critical = sum(1 for i in self.issues if i.severity == "CRITICAL")
643
+ warning = sum(1 for i in self.issues if i.severity == "WARNING")
644
+ info = sum(1 for i in self.issues if i.severity == "INFO")
645
+ return {
646
+ "total_issues": len(self.issues),
647
+ "critical": critical,
648
+ "warning": warning,
649
+ "info": info,
650
+ "status": "FAILED" if critical > 0 else ("REVIEW" if warning > 0 else "PASSED"),
651
+ "drawing": self.dxf_path,
652
+ "checks_run": 7,
653
+ "layers": list(self._all_layers()),
654
+ }
655
+
656
+ def get_issues_dicts(self) -> List[dict]:
657
+ return [i.to_dict() for i in self.issues]
658
+
659
+ def get_context(self) -> dict:
660
+ """返回图纸元数据(供 LLM 分析用)。"""
661
+ type_counts = {}
662
+ layer_counts = {}
663
+ for e in self._msp:
664
+ t = e.dxftype()
665
+ type_counts[t] = type_counts.get(t, 0) + 1
666
+ l = e.dxf.layer
667
+ layer_counts[l] = layer_counts.get(l, 0) + 1
668
+ return {
669
+ "file": self.dxf_path,
670
+ "entity_count": sum(type_counts.values()),
671
+ "entity_types": type_counts,
672
+ "layers": layer_counts,
673
+ }
674
+
675
+
676
+ # ==================== 便捷函数 ====================
677
+
678
+ def validate_dxf(dxf_path: str, rules: str = "mechanical") -> dict:
679
+ """验证 DXF 文件,返回 {ok, summary, issues}。"""
680
+ analyzer = DXFAnalyzer(dxf_path, rules)
681
+ analyzer.validate()
682
+ return {
683
+ "ok": True,
684
+ "summary": analyzer.get_summary(),
685
+ "issues": analyzer.get_issues_dicts(),
686
+ "context": analyzer.get_context(),
687
+ }
688
+
689
+
690
+ def validate_live(ac_bridge, rules: str = "mechanical") -> dict:
691
+ """从 Live AutoCAD 读取并验证(需要 ezdxf 导出 DXF 后验证)。
692
+
693
+ 先导出 DXF,再调用 validate_dxf。
694
+ """
695
+ try:
696
+ dxf_path = ac_bridge.export_dxf()
697
+ return validate_dxf(dxf_path, rules)
698
+ except Exception as e:
699
+ return {"ok": False, "error": f"live validation failed: {e}"}