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,314 @@
1
+ """Prepare drawing pages and embedded PDF text for visual interpretation.
2
+
3
+ Text is evidence, not confirmed dimensions. No measurements are inferred.
4
+ """
5
+ import argparse
6
+ from collections import Counter
7
+ import json
8
+ from pathlib import Path
9
+ import re
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+
15
+
16
+ def json_value(value):
17
+ if value is None or isinstance(value, (str, int, float, bool)):
18
+ return value
19
+ try:
20
+ return [json_value(item) for item in value]
21
+ except TypeError:
22
+ return str(value)
23
+
24
+
25
+ def converted_dwg_tags(stream, warnings):
26
+ from ezdxf.lldxf.tagger import ascii_tags_loader
27
+ from ezdxf.lldxf.tags import group_tags
28
+
29
+ section = None
30
+ for tags in group_tags(ascii_tags_loader(stream)):
31
+ if tags[0].value == "SECTION":
32
+ section = tags.get_first_value(2, None)
33
+ if (section == "OBJECTS" and tags[0].value == "DICTIONARY"
34
+ and tags.get_first_value(330, None) == "0"):
35
+ removed = 0
36
+ index = len(tags) - 1
37
+ while index > 0:
38
+ if (tags[index - 1].code == 3 and tags[index - 1].value == ""
39
+ and tags[index].code in (350, 360) and tags[index].value == "0"):
40
+ del tags[index - 1:index + 1]
41
+ removed += 1
42
+ index -= 1
43
+ index -= 1
44
+ if removed:
45
+ warnings.append("DWG 转换结果的根管理字典含 %d 个空名字、空指针条目,读取时已忽略;"
46
+ "其余标签及原始转换文件保持不变。仍需对照原图核验转换完整性。" % removed)
47
+ yield from tags
48
+
49
+
50
+ def read_dxf(source, result, output_dir, preview_limit):
51
+ try:
52
+ import ezdxf
53
+ from ezdxf.lldxf.encoding import decode_dxf_unicode
54
+ except ImportError as exc:
55
+ raise RuntimeError("读取 DXF 需要安装 ezdxf;现有工程的 ac_validate.py 也使用该库") from exc
56
+ try:
57
+ from ezdxf.lldxf.validator import is_binary_dxf_file
58
+
59
+ if "converted_dxf" in result and not is_binary_dxf_file(source):
60
+ from ezdxf.document import Drawing
61
+ from ezdxf.filemanagement import dxf_file_info
62
+
63
+ with source.open(encoding=dxf_file_info(source).encoding, errors="surrogateescape") as stream:
64
+ doc = Drawing.load(converted_dwg_tags(stream, result["warnings"]))
65
+ doc.filename = str(source)
66
+ else:
67
+ doc = ezdxf.readfile(str(source))
68
+ except (ezdxf.DXFError, ValueError) as exc:
69
+ raise RuntimeError("无法解析 DXF,请从原 CAD 导出标准 DXF 或 PDF: " + str(exc)) from exc
70
+ layouts = list(doc.layouts)
71
+ blocks = [block for block in doc.blocks if not block.block_record.is_any_layout]
72
+ entities = []
73
+ spaces = [(layout, "layout") for layout in layouts] + [(block, "block_definition") for block in blocks]
74
+ for space, space_kind in spaces:
75
+ for entity in space:
76
+ kind = entity.dxftype()
77
+ item = {"type": kind, "handle": entity.dxf.get("handle"),
78
+ "layer": entity.dxf.get("layer"),
79
+ "space": space.name, "source_space": space_kind,
80
+ "attributes": {k: json_value(v) for k, v in entity.dxf.all_existing_dxf_attribs().items()}}
81
+ if kind == "LWPOLYLINE":
82
+ item["points_xyseb"] = [list(point) for point in entity.get_points("xyseb")]
83
+ if kind == "POLYLINE":
84
+ item["vertices"] = [{k: json_value(v) for k, v in p.dxf.all_existing_dxf_attribs().items()} for p in entity.vertices]
85
+ if kind == "SPLINE":
86
+ item["control_points"] = [list(p) for p in entity.control_points]
87
+ item["fit_points"] = [list(p) for p in entity.fit_points]
88
+ item["knots"] = [float(k) for k in entity.knots]
89
+ item["weights"] = [float(w) for w in entity.weights]
90
+ if kind == "MTEXT":
91
+ item["raw_text"] = entity.text
92
+ if kind in ("TEXT", "MTEXT"):
93
+ item["plain_text"] = entity.plain_text()
94
+ if kind == "DIMENSION":
95
+ geometry = entity.get_geometry_block()
96
+ item["rendered_text"] = []
97
+ for part in geometry or []:
98
+ if part.dxftype() not in ("TEXT", "MTEXT"):
99
+ continue
100
+ layer = doc.layers.get(part.dxf.layer) if part.dxf.layer in doc.layers else None
101
+ if part.dxf.get("invisible") or layer and (layer.is_off() or layer.is_frozen()):
102
+ continue
103
+ item["rendered_text"].append(part.plain_text())
104
+ if entity.dxf.get("dimstyle", "STANDARD") in doc.dimstyles:
105
+ item["dimension_style_overrides"] = {
106
+ k: json_value(v) for k, v in entity.override().dimstyle_attribs.items()}
107
+ else:
108
+ result["warnings"].append("标注 %s 的样式 %s 缺失,无法核对显示规则。" %
109
+ (entity.dxf.handle, entity.dxf.dimstyle))
110
+ if geometry is None:
111
+ result["warnings"].append("标注 %s(%s)没有显示图形,预览可能缺少该尺寸;不会根据定义点补画。" %
112
+ (entity.dxf.handle, space.name))
113
+ if kind == "INSERT":
114
+ item["attached_attributes"] = [{k: json_value(v) for k, v in attribute.dxf.all_existing_dxf_attribs().items()}
115
+ for attribute in entity.attribs]
116
+ if doc.blocks.get(entity.dxf.name) is None:
117
+ result["warnings"].append("块引用 %s 的块 %s 缺失,图形无法完整显示。" %
118
+ (entity.dxf.handle, entity.dxf.name))
119
+ entities.append(item)
120
+ for block in blocks:
121
+ if block.block.dxf.flags & 12:
122
+ result["warnings"].append("外部引用 %s 未加载:%s;预览不包含该文件的图形。" %
123
+ (block.name, block.block.dxf.get("xref_path", "")))
124
+ output_dir = Path(output_dir).resolve()
125
+ output_dir.mkdir(parents=True, exist_ok=True)
126
+ evidence_path = output_dir / "dxf-entities.json"
127
+ styles = [{k: json_value(v) for k, v in style.dxf.all_existing_dxf_attribs().items()} for style in doc.dimstyles]
128
+ with evidence_path.open("x", encoding="utf-8") as evidence:
129
+ json.dump({"entities": entities, "dimension_styles": styles,
130
+ "block_definitions": [{k: json_value(v) for k, v in block.block.dxf.all_existing_dxf_attribs().items()}
131
+ for block in blocks],
132
+ "source_warnings": result["warnings"]}, evidence, ensure_ascii=False)
133
+ if len(result["warnings"]) > 8:
134
+ result["warnings"] = result["warnings"][:7] + [
135
+ "其余 %d 条源数据缺失告警见 entity_evidence 的 source_warnings;不能视为完整图纸。" % (len(result["warnings"]) - 7)]
136
+ previews = []
137
+ preview_chars = 0
138
+ for entity in entities:
139
+ if entity["source_space"] != "layout" or entity["type"] not in ("DIMENSION", "TEXT", "MTEXT"):
140
+ continue
141
+ layer = doc.layers.get(entity["layer"]) if entity["layer"] in doc.layers else None
142
+ if entity["attributes"].get("invisible") or layer and (layer.is_off() or layer.is_frozen()):
143
+ continue
144
+ if entity["type"] == "DIMENSION" and not entity.get("rendered_text"):
145
+ continue
146
+ text = " / ".join(entity.get("rendered_text", [])) or entity.get("plain_text", "")
147
+ text = re.sub(r"(?<!\\)\\U\+[0-9A-Fa-f]{4}(?![0-9A-Fa-f])",
148
+ lambda match: (decode_dxf_unicode(match.group())
149
+ if not 0xD800 <= int(match.group()[3:], 16) <= 0xDFFF else match.group()), text)
150
+ item = {"type": entity["type"], "handle": entity["handle"], "layer": entity["layer"],
151
+ "space": entity["space"], "source_space": entity["source_space"], "text": text[:300]}
152
+ if entity["type"] == "DIMENSION":
153
+ item["text_override"] = entity["attributes"].get("text", "")
154
+ size = len(json.dumps(item, ensure_ascii=False))
155
+ if len(previews) >= 20 or preview_chars + size > 6000:
156
+ break
157
+ previews.append(item)
158
+ preview_chars += size
159
+ result.update(ok=any(len(layout) for layout in layouts), pages=0, entity_count=len(entities),
160
+ entity_counts=dict(Counter(e["type"] for e in entities)),
161
+ entity_evidence=str(evidence_path),
162
+ annotation_preview=previews,
163
+ drawing_units_code=doc.header.get("$INSUNITS", 0),
164
+ layout_count=len(layouts), source_space="layouts_and_block_definitions")
165
+ if not result["ok"]:
166
+ result["warnings"].append("所有模型空间和图纸布局均无图元,没有可读取的图纸内容。")
167
+ result["warnings"].extend([
168
+ "DIMENSION 原始属性及文字只是候选,尺寸值、标注样式、文字覆盖和对应几何必须核对;文件单位代码不保证标注单位。",
169
+ "原始数据包含布局和块定义;文字摘要排除块定义、隐藏图元及关闭或冻结图层。块坐标未经插入变换,不能当作实际位置;显示尺寸文字取自原有标注图形,不重新计算。摘要最多 20 条且截断长文字。",
170
+ "预览受字体、外部引用、代理对象和布局视口支持影响,可能有缺失,不可仅凭预览认定图纸完整;缺失、冲突和不清晰数据应集中询问用户。"])
171
+ if preview_limit:
172
+ try:
173
+ from ezdxf.addons.drawing.matplotlib import qsave
174
+ from PIL import Image
175
+ except ImportError:
176
+ result["warnings"].append("缺少 matplotlib,DXF 预览不可用;请安装 matplotlib 或使用 CAD 打开。")
177
+ else:
178
+ nonempty_layouts = [(index, layout) for index, layout in enumerate(layouts, 1) if len(layout)]
179
+ for index, layout in nonempty_layouts[:preview_limit]:
180
+ image_path = output_dir / ("dxf-preview-%04d.png" % index)
181
+ if image_path.exists():
182
+ raise FileExistsError("预览图片已存在,请选择空目录")
183
+ try:
184
+ qsave(layout, image_path, bg="#FFFFFF", dpi=120, size_inches=(12, 9))
185
+ if not image_path.is_file() or not image_path.stat().st_size:
186
+ raise RuntimeError("预览图片为空")
187
+ with Image.open(image_path) as image:
188
+ if all(low == high == 255 for low, high in image.convert("RGB").getextrema()):
189
+ raise RuntimeError("预览仅有白色背景,没有可见图形,不能视为图纸已显示")
190
+ result["images"].append({"page": index, "path": str(image_path), "layout": layout.name})
191
+ except Exception as exc:
192
+ result["warnings"].append("布局 %s 预览失败: %s" % (layout.name, exc))
193
+ result["pages"] = len(result["images"])
194
+ if len(nonempty_layouts) > preview_limit:
195
+ result["warnings"].append("仅尝试预览前 %d 个非空空间,共有 %d 个;可调整 --preview-limit。" % (preview_limit, len(nonempty_layouts)))
196
+ return result
197
+
198
+
199
+ def run(command):
200
+ return subprocess.run(command, capture_output=True, timeout=120, check=True)
201
+
202
+
203
+ def prepare(source, output_dir, dwg2dxf=None, preview_limit=3, page=None):
204
+ source = Path(source).resolve(strict=True)
205
+ result = {"ok": False, "input": str(source), "pages": 0,
206
+ "extracted_text": [], "images": [], "ocr": "not_run",
207
+ "warnings": ["图中文字仅供候选,必须结合页图确认数值、尺寸对象和单位;缺失或模糊时向用户询问。禁止从像素比例推测尺寸。"]}
208
+ suffix = source.suffix.lower()
209
+ if page is not None and suffix != ".pdf":
210
+ raise ValueError("--page 仅用于 PDF")
211
+ if suffix == ".dxf":
212
+ return read_dxf(source, result, output_dir, preview_limit)
213
+ if suffix == ".dwg":
214
+ local_converter = Path(sys.executable).with_name("dwg2dxf.exe")
215
+ converter = dwg2dxf or shutil.which("dwg2dxf") or (local_converter if local_converter.is_file() else None)
216
+ if not converter:
217
+ raise RuntimeError("未找到 LibreDWG 的 dwg2dxf;请提供 --dwg2dxf 可执行文件路径,或先转换为 DXF。")
218
+ output_dir = Path(output_dir).resolve()
219
+ output_dir.mkdir(parents=True, exist_ok=True)
220
+ converted = output_dir / "converted.dxf"
221
+ if converted.exists():
222
+ raise FileExistsError("转换结果已存在,请选择空目录")
223
+ with tempfile.TemporaryDirectory(prefix="dwg-input-", dir=output_dir) as tmp:
224
+ copied = Path(tmp) / "input.dwg"
225
+ shutil.copyfile(source, copied)
226
+ run([str(converter), "-o", str(converted), str(copied)])
227
+ if not converted.is_file() or converted.stat().st_size == 0:
228
+ raise RuntimeError("DWG 转换器未生成有效输出")
229
+ result["converted_dxf"] = str(converted)
230
+ result["warnings"].append("DWG 已经转换为 DXF;转换可能丢失信息,需对照原图核验。")
231
+ return read_dxf(converted, result, output_dir, preview_limit)
232
+ if suffix in {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"}:
233
+ from PIL import Image
234
+ with Image.open(source) as image:
235
+ if getattr(image, "n_frames", 1) != 1:
236
+ raise ValueError("多页图片暂不支持,请先拆分为单页图片")
237
+ if image.format not in {"PNG", "JPEG", "WEBP"}:
238
+ out = Path(output_dir).resolve()
239
+ out.mkdir(parents=True, exist_ok=True)
240
+ preview = out / "image.png"
241
+ with preview.open("xb") as target:
242
+ image.convert("RGB").save(target, "PNG")
243
+ else:
244
+ image.load()
245
+ preview = source
246
+ result.update(ok=True, pages=1, images=[{"page": 1, "path": str(preview)}])
247
+ result["warnings"].append("图片没有嵌入文字提取结果;本工具没有运行 OCR,需用视觉工具查看原图。")
248
+ return result
249
+ if suffix != ".pdf":
250
+ raise ValueError("仅支持 PDF、DXF 和常见图片格式;DWG 需要先转换")
251
+ programs = {name: shutil.which(name) for name in ("pdfinfo", "pdftotext", "pdftoppm")}
252
+ missing = [name for name, path in programs.items() if not path]
253
+ if missing:
254
+ raise RuntimeError("缺少 PDF 工具: " + ", ".join(missing))
255
+ info = run([programs["pdfinfo"], str(source)]).stdout.decode("utf-8", "replace")
256
+ match = re.search(r"^Pages:\s*(\d+)", info, re.MULTILINE)
257
+ if not match or int(match.group(1)) < 1:
258
+ raise RuntimeError("无法读取 PDF 页数")
259
+ count = int(match.group(1))
260
+ output_dir = Path(output_dir).resolve()
261
+ output_dir.mkdir(parents=True, exist_ok=True)
262
+ if page is not None and not 1 <= page <= count:
263
+ raise ValueError("请求的页码超出范围,图纸共 %d 页" % count)
264
+ pages = [page] if page is not None else range(1, min(count, 3) + 1)
265
+ existing = [output_dir / ("page-%04d.png" % p) for p in pages]
266
+ if any(path.exists() for path in existing):
267
+ raise FileExistsError("输出图片已存在,请选择空目录")
268
+ result["pages"] = count
269
+ if count > len(pages):
270
+ result["warnings"].append("还有未读页;使用 --page N 查看其余页再判断图纸是否完整。")
271
+ for page in pages:
272
+ prefix = output_dir / ("page-%04d" % page)
273
+ image = prefix.with_suffix(".png")
274
+ run([programs["pdftoppm"], "-f", str(page), "-l", str(page), "-singlefile", "-cropbox", "-r", "150", "-png", str(source), str(prefix)])
275
+ if not image.is_file() or image.stat().st_size == 0:
276
+ raise RuntimeError("PDF 页面渲染失败: " + str(page))
277
+ text = run([programs["pdftotext"], "-f", str(page), "-l", str(page), "-layout", "-enc", "UTF-8", str(source), "-"]).stdout.decode("utf-8", "replace").strip("\f\n\r ")
278
+ result["images"].append({"page": page, "path": str(image)})
279
+ item = {"page": page, "source": "pdf_embedded_text", "text": text[:2400]}
280
+ if len(text) > 2400:
281
+ text_file = prefix.with_suffix(".txt")
282
+ with text_file.open("x", encoding="utf-8") as full:
283
+ full.write(text)
284
+ item["full_text_path"] = str(text_file)
285
+ result["extracted_text"].append(item)
286
+ if not text:
287
+ result["warnings"].append("第 %d 页没有可提取的嵌入文字;可能是扫描图,需查看页图。" % page)
288
+ result["warnings"].append("未运行 OCR;嵌入文字可能缺字或顺序错误,不能单独确认尺寸。")
289
+ result["ok"] = True
290
+ return result
291
+
292
+
293
+ def main():
294
+ if hasattr(sys.stdout, "reconfigure"):
295
+ sys.stdout.reconfigure(encoding="utf-8")
296
+ parser = argparse.ArgumentParser(description=__doc__)
297
+ parser.add_argument("input")
298
+ parser.add_argument("--page", type=int, help="只读取指定 PDF 页(从 1 开始);默认返回前 3 页")
299
+ parser.add_argument("--output-dir", required=True, help="PDF 页面图片输出目录;已有同名页面不会覆盖")
300
+ parser.add_argument("--dwg2dxf", help="LibreDWG dwg2dxf 可执行文件路径(不填则查找 PATH)")
301
+ parser.add_argument("--preview-limit", type=int, default=3, help="DXF/DWG 预览最多的模型空间和布局数(0–20,默认 3)")
302
+ args = parser.parse_args()
303
+ try:
304
+ if not 0 <= args.preview_limit <= 20:
305
+ raise ValueError("--preview-limit 必须在 0 到 20 之间")
306
+ result = prepare(args.input, args.output_dir, args.dwg2dxf, args.preview_limit, args.page)
307
+ except (OSError, ValueError, RuntimeError, SyntaxError, ImportError, subprocess.SubprocessError) as exc:
308
+ result = {"ok": False, "input": args.input, "error": str(exc), "ocr": "not_run"}
309
+ print(json.dumps(result, ensure_ascii=False))
310
+ return 0 if result["ok"] else 1
311
+
312
+
313
+ if __name__ == "__main__":
314
+ sys.exit(main())