dimine-python-sdk 0.2.6__py3-none-any.whl → 0.2.8__py3-none-any.whl

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 (145) hide show
  1. dimine_python_sdk/__init__.py +1 -1
  2. dimine_python_sdk/cli/__init__.py +33 -0
  3. dimine_python_sdk/cli/commands/app.py +318 -0
  4. dimine_python_sdk/cli/commands/file.py +24 -4
  5. dimine_python_sdk/cli/commands/layer.py +9 -8
  6. dimine_python_sdk/conn/base_conn_client.py +2 -2
  7. dimine_python_sdk/conn/conn_client.py +48 -9
  8. dimine_python_sdk/docs/examples/cli_usage.md +16 -1
  9. dimine_python_sdk/docs/examples/open_pit_blasting_design.md +227 -0
  10. dimine_python_sdk/docs/examples/summary.md +1 -0
  11. dimine_python_sdk/docs/guides/cli.md +55 -3
  12. dimine_python_sdk/docs/guides/lib/SUMMARY.md +1 -0
  13. dimine_python_sdk/docs/guides/lib/blasting/SUMMARY.md +9 -0
  14. dimine_python_sdk/docs/guides/lib/blasting/charge_from_template.md +188 -0
  15. dimine_python_sdk/docs/guides/lib/blasting/config_databases.md +279 -0
  16. dimine_python_sdk/docs/guides/lib/blasting/design_flow.md +305 -0
  17. dimine_python_sdk/docs/guides/lib/blasting/overview.md +286 -0
  18. dimine_python_sdk/docs/guides/lib/io/dmg_file.md +11 -1
  19. dimine_python_sdk/docs/guides/lib/io/dmo_file.md +233 -159
  20. dimine_python_sdk/lib/blasting/__init__.py +171 -0
  21. dimine_python_sdk/lib/blasting/analysis.py +61 -0
  22. dimine_python_sdk/lib/blasting/api.py +136 -0
  23. dimine_python_sdk/lib/blasting/charge_design.py +207 -0
  24. dimine_python_sdk/lib/blasting/charge_template.py +433 -0
  25. dimine_python_sdk/lib/blasting/defaults.py +104 -0
  26. dimine_python_sdk/lib/blasting/dmo_export.py +124 -0
  27. dimine_python_sdk/lib/blasting/errors.py +37 -0
  28. dimine_python_sdk/lib/blasting/explosive_db.py +423 -0
  29. dimine_python_sdk/lib/blasting/hole_layout.py +265 -0
  30. dimine_python_sdk/lib/blasting/hole_param.py +375 -0
  31. dimine_python_sdk/lib/blasting/models.py +354 -0
  32. dimine_python_sdk/lib/blasting/network.py +254 -0
  33. dimine_python_sdk/lib/blasting/report.py +95 -0
  34. dimine_python_sdk/lib/blasting/template_adapt.py +649 -0
  35. dimine_python_sdk/lib/io/dmg_file.py +31 -13
  36. dimine_python_sdk/lib/io/dmo_file.py +2039 -327
  37. dimine_python_sdk/lib/io/dmt_file.py +12 -3
  38. dimine_python_sdk/lib/native/data_table.py +25 -3
  39. dimine_python_sdk/lib/native/geometry.py +6 -0
  40. dimine_python_sdk/lib/prospecting/compositing.py +6 -1
  41. dimine_python_sdk/lib/prospecting/high_grade.py +18 -1
  42. dimine_python_sdk/logger.py +1 -1
  43. dimine_python_sdk/models/__init__.py +16 -1
  44. dimine_python_sdk/models/geometry.py +91 -0
  45. dimine_python_sdk/models/types.py +31 -76
  46. dimine_python_sdk/site/404.html +241 -13
  47. dimine_python_sdk/site/examples/cli_usage.html +280 -19
  48. dimine_python_sdk/site/examples/contour_modeling_visualization.html +241 -13
  49. dimine_python_sdk/site/examples/import_csv_to_drill_db.html +241 -13
  50. dimine_python_sdk/site/examples/lib_file_usage.html +241 -13
  51. dimine_python_sdk/site/examples/open_pit_blasting_design.html +2569 -0
  52. dimine_python_sdk/site/examples/reserve_estimation_pipeline.html +243 -13
  53. dimine_python_sdk/site/examples/summary.html +242 -13
  54. dimine_python_sdk/site/guides/SUMMARY.html +241 -13
  55. dimine_python_sdk/site/guides/cli.html +379 -17
  56. dimine_python_sdk/site/guides/conn/SUMMARY.html +241 -13
  57. dimine_python_sdk/site/guides/conn/data_edit.html +241 -13
  58. dimine_python_sdk/site/guides/conn/data_get.html +241 -13
  59. dimine_python_sdk/site/guides/conn/features.html +241 -13
  60. dimine_python_sdk/site/guides/conn/io.html +241 -13
  61. dimine_python_sdk/site/guides/lib/SUMMARY.html +243 -14
  62. dimine_python_sdk/site/guides/lib/blasting/SUMMARY.html +2177 -0
  63. dimine_python_sdk/site/guides/lib/blasting/charge_from_template.html +2588 -0
  64. dimine_python_sdk/site/guides/lib/blasting/config_databases.html +2955 -0
  65. dimine_python_sdk/site/guides/lib/blasting/design_flow.html +2815 -0
  66. dimine_python_sdk/site/guides/lib/blasting/overview.html +3322 -0
  67. dimine_python_sdk/site/guides/lib/exploitation/SUMMARY.html +245 -17
  68. dimine_python_sdk/site/guides/lib/exploitation/contour_modeling.html +244 -16
  69. dimine_python_sdk/site/guides/lib/exploitation/implicit_modeling.html +244 -16
  70. dimine_python_sdk/site/guides/lib/exploitation/laneway.html +244 -16
  71. dimine_python_sdk/site/guides/lib/io/SUMMARY.html +241 -13
  72. dimine_python_sdk/site/guides/lib/io/dmb_file.html +241 -13
  73. dimine_python_sdk/site/guides/lib/io/dmd_file.html +241 -13
  74. dimine_python_sdk/site/guides/lib/io/dmf_file.html +241 -13
  75. dimine_python_sdk/site/guides/lib/io/dmg_file.html +252 -14
  76. dimine_python_sdk/site/guides/lib/io/dmo_file.html +377 -78
  77. dimine_python_sdk/site/guides/lib/io/dmt_file.html +241 -13
  78. dimine_python_sdk/site/guides/lib/io/file_conversion.html +241 -13
  79. dimine_python_sdk/site/guides/lib/prospecting/SUMMARY.html +241 -13
  80. dimine_python_sdk/site/guides/lib/prospecting/drill_db.html +241 -13
  81. dimine_python_sdk/site/guides/lib/prospecting/reserve_estimation.html +241 -13
  82. dimine_python_sdk/site/guides/lib/visualization/SUMMARY.html +241 -13
  83. dimine_python_sdk/site/guides/lib/visualization/visualization.html +241 -13
  84. dimine_python_sdk/site/guides/models/SUMMARY.html +241 -13
  85. dimine_python_sdk/site/guides/models/data_types.html +241 -13
  86. dimine_python_sdk/site/index.html +241 -13
  87. dimine_python_sdk/site/objects.inv +0 -0
  88. dimine_python_sdk/site/quickstart.html +241 -13
  89. dimine_python_sdk/site/reference/SUMMARY.html +241 -13
  90. dimine_python_sdk/site/reference/cli/commands/camera.html +241 -13
  91. dimine_python_sdk/site/reference/cli/commands/convert.html +241 -13
  92. dimine_python_sdk/site/reference/cli/commands/datatable.html +241 -13
  93. dimine_python_sdk/site/reference/cli/commands/entity.html +241 -13
  94. dimine_python_sdk/site/reference/cli/commands/feature.html +241 -13
  95. dimine_python_sdk/site/reference/cli/commands/file.html +381 -123
  96. dimine_python_sdk/site/reference/cli/commands/geometry.html +241 -13
  97. dimine_python_sdk/site/reference/cli/commands/laneway.html +241 -13
  98. dimine_python_sdk/site/reference/cli/commands/layer.html +253 -23
  99. dimine_python_sdk/site/reference/cli/commands/text.html +241 -13
  100. dimine_python_sdk/site/reference/conn/base_conn_client.html +243 -17
  101. dimine_python_sdk/site/reference/conn/conn_client.html +886 -428
  102. dimine_python_sdk/site/reference/lib/bridge.html +580 -246
  103. dimine_python_sdk/site/reference/lib/exploitation/api.html +241 -13
  104. dimine_python_sdk/site/reference/lib/exploitation/contour_modeling.html +241 -13
  105. dimine_python_sdk/site/reference/lib/exploitation/implicit_modeling.html +241 -13
  106. dimine_python_sdk/site/reference/lib/exploitation/lane_way.html +241 -13
  107. dimine_python_sdk/site/reference/lib/exploitation/models.html +241 -13
  108. dimine_python_sdk/site/reference/lib/io/dmb_file.html +241 -13
  109. dimine_python_sdk/site/reference/lib/io/dmd_file.html +241 -13
  110. dimine_python_sdk/site/reference/lib/io/dmf_file.html +241 -13
  111. dimine_python_sdk/site/reference/lib/io/dmg_file.html +350 -70
  112. dimine_python_sdk/site/reference/lib/io/dmo_file.html +4394 -712
  113. dimine_python_sdk/site/reference/lib/io/dmt_file.html +393 -89
  114. dimine_python_sdk/site/reference/lib/io/file_converter.html +241 -13
  115. dimine_python_sdk/site/reference/lib/io/models.html +241 -13
  116. dimine_python_sdk/site/reference/lib/native/algorithm.html +241 -13
  117. dimine_python_sdk/site/reference/lib/native/data_table.html +397 -87
  118. dimine_python_sdk/site/reference/lib/native/exploitation.html +241 -13
  119. dimine_python_sdk/site/reference/lib/native/geometry.html +424 -172
  120. dimine_python_sdk/site/reference/lib/native/io.html +241 -13
  121. dimine_python_sdk/site/reference/lib/native/models.html +241 -13
  122. dimine_python_sdk/site/reference/lib/native/prospecting.html +241 -13
  123. dimine_python_sdk/site/reference/lib/prospecting/api.html +321 -45
  124. dimine_python_sdk/site/reference/lib/prospecting/block_model.html +241 -13
  125. dimine_python_sdk/site/reference/lib/prospecting/compositing.html +275 -35
  126. dimine_python_sdk/site/reference/lib/prospecting/drill_db.html +241 -13
  127. dimine_python_sdk/site/reference/lib/prospecting/ellipsoid.html +241 -13
  128. dimine_python_sdk/site/reference/lib/prospecting/high_grade.html +308 -44
  129. dimine_python_sdk/site/reference/lib/prospecting/models.html +241 -13
  130. dimine_python_sdk/site/reference/lib/visualization/exceptions.html +241 -13
  131. dimine_python_sdk/site/reference/lib/visualization/models.html +241 -13
  132. dimine_python_sdk/site/reference/lib/visualization/tin_renderer.html +241 -13
  133. dimine_python_sdk/site/reference/logger.html +241 -13
  134. dimine_python_sdk/site/reference/models/conn.html +241 -13
  135. dimine_python_sdk/site/reference/models/errors.html +241 -13
  136. dimine_python_sdk/site/reference/models/types.html +349 -497
  137. dimine_python_sdk/site/search/search_index.json +1 -1
  138. dimine_python_sdk/site/sitemap.xml +108 -84
  139. dimine_python_sdk/site/sitemap.xml.gz +0 -0
  140. dimine_python_sdk/site/summary.html +241 -13
  141. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.8.dist-info}/METADATA +1 -1
  142. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.8.dist-info}/RECORD +145 -116
  143. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.8.dist-info}/WHEEL +0 -0
  144. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.8.dist-info}/entry_points.txt +0 -0
  145. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.8.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  from typing import Any
5
5
 
6
6
  DOCS_PATH = Path(__file__).parent / "docs"
7
- __VERSION__ = "0.2.6" # 由 build.bat 中的 generate_version.py 自动更新
7
+ __VERSION__ = "0.2.8" # 由 build.bat 中的 generate_version.py 自动更新
8
8
 
9
9
 
10
10
  _LAZY_IMPORTS: dict[str, Any] = {}
@@ -36,19 +36,50 @@
36
36
  dimine datatable-set --file PATH --index INT --field NAME --value VAL
37
37
  dimine convert-file --source PATH --target PATH [--overwrite BOOL]
38
38
  dimine laneway-tunnel --type INT --width FLOAT [--center-lines x,y,z;x,y,z;...] [--height FLOAT] [--width-up FLOAT] [--close] [--connectivity] [--method INT] [--no-import]
39
+ dimine start [--home PATH] [--wait SECONDS]
39
40
 
40
41
  全局选项:
41
42
  --format [json|text] 输出格式(默认: json)
42
43
  """
43
44
 
44
45
  import argparse
46
+ import gettext
45
47
  import sys
46
48
 
49
+ # ---------------------------------------------------------------------------
50
+ # 短路 gettext:argparse 在 ArgumentParser.__init__ 里固定做 3 次翻译
51
+ # (positional arguments / options / show this help message and exit),
52
+ # 每次 gettext.find() 都会遍历 locale 目录做 os.path.exists。本 CLI 会创建
53
+ # ~37 个 parser,合计 110 次翻译、约 440 次 stat,在慢盘/杀软环境下可占
54
+ # build_parser 总耗时的 90% 以上(实测 300ms+ → 8ms)。
55
+ #
56
+ # 本 CLI 没有随包分发 .mo 翻译目录,翻译结果恒等于原文;仅当确实存在
57
+ # messages.mo 时才回退到真实实现,保证将来若补翻译文件不会静默失效。
58
+ # ---------------------------------------------------------------------------
59
+ if gettext.find("messages") is None:
60
+ _real_find = gettext.find
61
+ _find_cache: dict = {}
62
+
63
+ def _cached_find(*args, **kwargs):
64
+ """按参数缓存 gettext.find,避免重复探测文件系统。"""
65
+ key = (args, tuple(sorted(kwargs.items())))
66
+ if key not in _find_cache:
67
+ _find_cache[key] = _real_find(*args, **kwargs)
68
+ return _find_cache[key]
69
+
70
+ gettext.find = _cached_find
71
+ # argparse 模块级绑定了 `from gettext import gettext as _, ngettext`,
72
+ # 须直接替换其命名空间中的引用才生效。
73
+ argparse._ = lambda message: message
74
+ argparse.ngettext = lambda singular, plural, n: singular if n == 1 else plural
75
+
47
76
  # Windows 控制台默认使用 GBK,强制改为 UTF-8 避免中文乱码
48
77
  if sys.platform == "win32":
49
78
  sys.stdout.reconfigure(encoding="utf-8")
50
79
  sys.stderr.reconfigure(encoding="utf-8")
51
80
 
81
+ from dimine_python_sdk.cli.commands.app import register_commands as register_app_commands
82
+ from dimine_python_sdk.cli.commands.app import COMMAND_HANDLERS as APP_HANDLERS
52
83
  from dimine_python_sdk.cli.commands.camera import register_commands as register_camera_commands
53
84
  from dimine_python_sdk.cli.commands.camera import COMMAND_HANDLERS as CAMERA_HANDLERS
54
85
  from dimine_python_sdk.cli.commands.text import register_commands as register_text_commands
@@ -71,6 +102,7 @@ from dimine_python_sdk.cli.commands.layer import register_commands as register_l
71
102
  from dimine_python_sdk.cli.commands.layer import COMMAND_HANDLERS as LAYER_HANDLERS
72
103
 
73
104
  ALL_HANDLERS = {}
105
+ ALL_HANDLERS.update(APP_HANDLERS)
74
106
  ALL_HANDLERS.update(ENTITY_HANDLERS)
75
107
  ALL_HANDLERS.update(FILE_HANDLERS)
76
108
  ALL_HANDLERS.update(LAYER_HANDLERS)
@@ -96,6 +128,7 @@ def build_parser() -> argparse.ArgumentParser:
96
128
  sub = parser.add_subparsers(dest="command", help="子命令")
97
129
 
98
130
  register_entity_commands(sub, format_parent)
131
+ register_app_commands(sub, format_parent)
99
132
  register_file_commands(sub, format_parent)
100
133
  register_layer_commands(sub, format_parent)
101
134
  register_feature_commands(sub, format_parent)
@@ -0,0 +1,318 @@
1
+ """Dimine 应用进程相关 CLI 命令(本地进程操作,无需 Dimine 已运行)。
2
+
3
+ 本命令从 DIMINE_HOME 定位 Dimine.exe 并在后台分离启动,用于在执行
4
+ file-tree / file-open 等远程命令前先把 Dimine 桌面程序拉起来。
5
+ """
6
+
7
+ import asyncio
8
+ import csv
9
+ import io
10
+ import logging
11
+ import os
12
+ import subprocess
13
+ import time
14
+
15
+ from dimine_python_sdk.cli.utils._utils import output_error, output_result
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _DIMINE_EXE = "Dimine.exe"
20
+ # 与 conn_client.open_client() 默认地址保持一致(Dimine 内置 WebSocket 服务)
21
+ _WS_URL = "ws://127.0.0.1:8000"
22
+
23
+
24
+ # ==============================================================================
25
+ # 进程辅助函数
26
+ # ==============================================================================
27
+
28
+ def _running_pids() -> list:
29
+ """返回当前正在运行的 Dimine.exe 进程 PID 列表(Windows)。"""
30
+ if os.name != "nt":
31
+ return []
32
+ try:
33
+ proc = subprocess.run(
34
+ ["tasklist", "/FI", f"IMAGENAME eq {_DIMINE_EXE}", "/FO", "CSV", "/NH"],
35
+ capture_output=True,
36
+ timeout=10,
37
+ )
38
+ except (OSError, subprocess.SubprocessError):
39
+ return []
40
+ text = proc.stdout.decode("utf-8", errors="replace")
41
+ pids = []
42
+ for row in csv.reader(io.StringIO(text)):
43
+ if len(row) >= 2 and row[0].strip().lower() == _DIMINE_EXE.lower():
44
+ try:
45
+ pids.append(int(row[1].strip()))
46
+ except ValueError:
47
+ pass
48
+ return pids
49
+
50
+
51
+ def _launch(exe: str, home: str, uac_timeout: float | None = None) -> int | None:
52
+ """后台分离启动 Dimine.exe,返回 pid(提权路径未探测到 pid 时为 None)。
53
+
54
+ 以 DIMINE_HOME 为工作目录,保证相对路径的 SysParameter / Parameter 等
55
+ 资源能被正确找到;子进程独立于当前控制台运行,CLI 立即返回。
56
+
57
+ 若 Dimine.exe 要求管理员权限(Popen 抛 WinError 740)而当前进程未提权,
58
+ 自动改用 runas 动词触发一次 UAC 提权启动(见 _launch_elevated)。
59
+ uac_timeout:仅提权路径使用——等待用户在 UAC 上应答的最长秒数,超时抛
60
+ RuntimeError;None 表示不限时(可 Ctrl+C 中止)。
61
+ """
62
+ env = os.environ.copy()
63
+ env["DIMINE_HOME"] = home
64
+ kwargs = dict(
65
+ cwd=home,
66
+ env=env,
67
+ stdin=subprocess.DEVNULL,
68
+ stdout=subprocess.DEVNULL,
69
+ stderr=subprocess.DEVNULL,
70
+ )
71
+ if os.name == "nt":
72
+ kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
73
+ try:
74
+ proc = subprocess.Popen([exe], **kwargs)
75
+ except OSError as exc:
76
+ # WinError 740 = ERROR_ELEVATION_REQUIRED:目标 exe manifest 要求管理员权限。
77
+ if os.name == "nt" and getattr(exc, "winerror", None) == 740:
78
+ return _launch_elevated(exe, home, uac_timeout)
79
+ raise
80
+ logger.info("已后台启动 Dimine(pid=%s exe=%s)", proc.pid, exe)
81
+ return proc.pid
82
+
83
+
84
+ def _launch_elevated(exe: str, home: str, timeout: float | None = None) -> int | None:
85
+ """通过 runas 动词触发 UAC 提权启动 Dimine.exe,返回探测到的 pid 或 None。
86
+
87
+ 提权进程由系统(consent.exe 经 UAC 确认)独立创建,调用方拿不到其句柄,
88
+ 故启动后轮询 tasklist 直至 Dimine.exe 出现以取得 pid。
89
+
90
+ ShellExecuteW(runas) 会同步阻塞到用户在 UAC 上应答,若主线程直接调用会
91
+ 卡死在 C 函数里(Ctrl+C 无效)。因此改在守护线程中执行、主线程空闲轮询:
92
+ - Ctrl+C 随时可中止(进程退出会顺带取消那次未决的提权);
93
+ - timeout>0 时,超过该秒数仍未获 UAC 应答则抛 RuntimeError;
94
+ - 用户点『否』/取消 UAC → RuntimeError(面向用户的中文提示)。
95
+
96
+ ShellExecute 子进程继承本进程环境变量,因此先把 DIMINE_HOME 临时写入
97
+ os.environ,确保目标进程能看到正确的资源目录。
98
+ """
99
+ if os.name != "nt":
100
+ raise RuntimeError("提权启动仅支持 Windows。")
101
+ import ctypes
102
+ import threading
103
+ from ctypes import wintypes
104
+
105
+ logger.warning(
106
+ "Dimine.exe 需要管理员权限,弹出 UAC 确认框以提权启动…"
107
+ + ("" if timeout is None else f"(请在 {timeout:g}s 内应答,超时自动中止)")
108
+ )
109
+
110
+ outcome: dict = {}
111
+
112
+ def _request() -> None:
113
+ """守护线程内调用 ShellExecuteW(runas),结果写入 outcome。"""
114
+ had_home = "DIMINE_HOME" in os.environ
115
+ old_home = os.environ.get("DIMINE_HOME")
116
+ os.environ["DIMINE_HOME"] = home
117
+ try:
118
+ shell32 = ctypes.windll.shell32
119
+ shell_exec = shell32.ShellExecuteW
120
+ shell_exec.argtypes = [
121
+ wintypes.HWND,
122
+ wintypes.LPCWSTR, # lpOperation = "runas"
123
+ wintypes.LPCWSTR, # lpFile
124
+ wintypes.LPCWSTR, # lpParameters
125
+ wintypes.LPCWSTR, # lpDirectory
126
+ ctypes.c_int, # nShowCmd
127
+ ]
128
+ shell_exec.restype = wintypes.HINSTANCE
129
+ # 第 6 参为工作目录(home),第 7 参 SW_SHOWNORMAL=1
130
+ outcome["ret"] = shell_exec(None, "runas", exe, "", home, 1)
131
+ except Exception as exc: # noqa: BLE001 线程内兜底,避免调用悬空
132
+ outcome["exc"] = exc
133
+ finally:
134
+ if had_home:
135
+ os.environ["DIMINE_HOME"] = old_home
136
+ else:
137
+ os.environ.pop("DIMINE_HOME", None)
138
+
139
+ threading.Thread(target=_request, name="dimine-uac", daemon=True).start()
140
+
141
+ # 主线程空闲等待:Ctrl+C 可正常中断;timeout 内未应答则放弃(退出会取消提权)
142
+ start = time.monotonic()
143
+ while "ret" not in outcome and "exc" not in outcome:
144
+ if timeout is not None and time.monotonic() - start >= timeout:
145
+ raise RuntimeError(
146
+ f"在 {timeout:g}s 内未获 UAC 应答,{os.path.basename(exe)} 未启动。"
147
+ f"请重新运行本命令,并在弹出的 UAC 窗口选择『是』;"
148
+ f"或改为以管理员身份运行。"
149
+ )
150
+ time.sleep(0.2)
151
+ if "exc" in outcome:
152
+ raise outcome["exc"]
153
+
154
+ # ShellExecute 成功时返回 >32;失败返回 <=32 的 SE_ERR_*,UAC 被用户
155
+ # 拒绝时可能返回 ERROR_CANCELLED(1223)(数值 >32,需单独识别)。
156
+ SE_ERR_ACCESSDENIED = 5
157
+ ERROR_CANCELLED = 1223
158
+ ret = outcome.get("ret")
159
+ ret = int(ret) if ret else 0
160
+ if ret <= 32 or ret in (SE_ERR_ACCESSDENIED, ERROR_CANCELLED):
161
+ raise RuntimeError(
162
+ f"启动 {os.path.basename(exe)} 需要管理员权限,但 UAC 提权被取消或启动失败"
163
+ f"(ShellExecute 返回 {ret})。请在弹出的 UAC 窗口选择『是』,"
164
+ f"或改为以管理员身份运行本命令。"
165
+ )
166
+
167
+ # 提权进程创建有一瞬延迟,轮询 tasklist 以取得新进程 pid
168
+ deadline = time.monotonic() + 10.0
169
+ while time.monotonic() < deadline:
170
+ pids = _running_pids()
171
+ if pids:
172
+ logger.info("已通过 UAC 提权启动 Dimine(pid=%s exe=%s)", pids[0], exe)
173
+ return pids[0]
174
+ time.sleep(0.2)
175
+ logger.info("已触发 UAC 提权启动 Dimine(exe=%s),暂未探测到 pid", exe)
176
+ return None
177
+
178
+
179
+ # ==============================================================================
180
+ # 远端就绪探测
181
+ # ==============================================================================
182
+
183
+ def _wait_ready(url: str, timeout: float) -> tuple:
184
+ """轮询 WebSocket 服务直至可连接或超时。
185
+
186
+ 返回 (ready, waited_seconds)。握手成功即认为 Dimine 远端服务已就绪,
187
+ 之后 file-tree / file-open 等远程命令即可直接使用。
188
+ """
189
+ start = time.monotonic()
190
+
191
+ async def _probe() -> bool:
192
+ import websockets
193
+ try:
194
+ ws = await asyncio.wait_for(
195
+ websockets.connect(url, close_timeout=1), timeout=2
196
+ )
197
+ except Exception:
198
+ return False
199
+ try:
200
+ await ws.close()
201
+ except Exception:
202
+ pass
203
+ return True
204
+
205
+ async def _poll() -> bool:
206
+ while True:
207
+ if await _probe():
208
+ return True
209
+ if time.monotonic() - start >= timeout:
210
+ return False
211
+ await asyncio.sleep(0.5)
212
+
213
+ ready = asyncio.run(_poll())
214
+ return ready, round(time.monotonic() - start, 1)
215
+
216
+
217
+ # ==============================================================================
218
+ # 命令实现
219
+ # ==============================================================================
220
+
221
+ def cmd_start(fmt: str, home: str | None = None, wait: float = 8.0) -> None:
222
+ """后台启动 Dimine(自动定位 DIMINE_HOME/Dimine.exe 并分离启动)。
223
+
224
+ - 已运行则跳过启动,仅报告当前实例,不结束任何旧进程。
225
+ - --wait N:整个启动阶段的总预算秒数(默认 8)——需要提权时等用户在 UAC
226
+ 上应答、随后等远端 WebSocket 就绪,都计入这 N 秒。
227
+ - 提权场景下 N 秒内 UAC 一直未获应答视为启动失败,报错退出。
228
+ - --wait 0:不设预算也不等待就绪(启动后立即返回,旧行为)。
229
+ """
230
+ dimine_home = (home or os.environ.get("DIMINE_HOME") or "").strip()
231
+ if not dimine_home:
232
+ output_error("未检测到 DIMINE_HOME。请设置 DIMINE_HOME 环境变量指向 Dimine 安装目录,或传入 --home。", fmt)
233
+ dimine_home = os.path.abspath(dimine_home)
234
+ exe = os.path.join(dimine_home, _DIMINE_EXE)
235
+ if not os.path.isfile(exe):
236
+ output_error(f"在 DIMINE_HOME 下未找到 {_DIMINE_EXE}: {exe}", fmt)
237
+
238
+ budget = wait if (wait and wait > 0) else None
239
+ t0 = time.monotonic()
240
+
241
+ pids = _running_pids()
242
+ started_new, launched_pid = False, None
243
+ if pids:
244
+ logger.info("Dimine 已在运行(pid=%s),跳过启动", pids)
245
+ else:
246
+ try:
247
+ launched_pid = _launch(exe, dimine_home, uac_timeout=budget)
248
+ except OSError as exc:
249
+ # 非提权类的启动失败(权限/路径/占用等),转为友好报错而非原始 traceback
250
+ logger.error("启动 Dimine 失败: %s", exc)
251
+ output_error(f"启动 Dimine 失败: {exc}", fmt)
252
+ except RuntimeError as exc:
253
+ # 提权被取消 / UAC 超时等,_launch_elevated 已翻译为面向用户的文案
254
+ logger.error("启动 Dimine 失败: %s", exc)
255
+ output_error(str(exc), fmt)
256
+ started_new = True
257
+ pids = _running_pids() or ([launched_pid] if launched_pid else [])
258
+
259
+ remote_ready, waited = None, None
260
+ ready_budget = None
261
+ if budget is not None:
262
+ # 新启动的实例把启动耗时(含等待 UAC)计入预算;已运行的实例从新预算起算
263
+ ready_budget = wait if not started_new else max(0.0, wait - (time.monotonic() - t0))
264
+ if ready_budget > 0:
265
+ remote_ready, waited = _wait_ready(_WS_URL, ready_budget)
266
+
267
+ result = {
268
+ "success": True,
269
+ "started_new": started_new,
270
+ "pid": launched_pid if started_new else (pids[0] if pids else None),
271
+ "pids": pids,
272
+ "dimine_home": dimine_home,
273
+ "exe": exe,
274
+ }
275
+ if budget is not None:
276
+ result["wait_seconds"] = wait
277
+ if remote_ready is not None:
278
+ result["remote_ready"] = bool(remote_ready)
279
+ result["waited_seconds"] = waited
280
+
281
+ if started_new:
282
+ if launched_pid:
283
+ msg = f"已后台启动 Dimine: {exe}(pid={launched_pid})"
284
+ else:
285
+ msg = f"已触发 UAC 提权启动 Dimine: {exe}(新进程 pid 暂未探测到)"
286
+ else:
287
+ msg = f"Dimine 已在运行(pid={pids}),跳过启动;本命令不会结束旧实例,如需重启请先在 Dimine 中退出"
288
+ if remote_ready is True:
289
+ msg += f",远端服务 {_WS_URL} 已在 {waited}s 内就绪"
290
+ elif remote_ready is False:
291
+ msg += f",远端服务在 {ready_budget:g}s 内未就绪,可能仍在启动或启动失败,可加大 --wait 重试"
292
+ result["message"] = msg
293
+
294
+ output_result(result, fmt)
295
+
296
+
297
+ def register_commands(subparsers, format_parent) -> None:
298
+ """向 subparsers 注册 Dimine 应用进程相关子命令。"""
299
+ p = subparsers.add_parser(
300
+ "start",
301
+ parents=[format_parent],
302
+ help="后台启动 Dimine(自动定位 DIMINE_HOME/Dimine.exe)",
303
+ )
304
+ p.add_argument("--home", default=None, help="Dimine 安装目录(默认取 DIMINE_HOME 环境变量)")
305
+ p.add_argument(
306
+ "--wait",
307
+ type=float,
308
+ default=8.0,
309
+ help=(
310
+ "启动总预算秒数:提权启动时等 UAC 应答 + 之后等远端 WebSocket 就绪,"
311
+ "合计超过则报错/返回(默认 8;0=不等待就绪且不设预算)"
312
+ ),
313
+ )
314
+
315
+
316
+ COMMAND_HANDLERS = {
317
+ "start": lambda args: cmd_start(args.format, args.home, args.wait),
318
+ }
@@ -1,5 +1,8 @@
1
1
  """文件相关 CLI 命令。"""
2
2
 
3
+ import logging
4
+ import os
5
+
3
6
  from dimine_python_sdk.cli.utils._utils import (
4
7
  get_client,
5
8
  handle_sdk_error,
@@ -11,6 +14,8 @@ from dimine_python_sdk.cli.utils._utils import (
11
14
  run_cli_async,
12
15
  )
13
16
 
17
+ logger = logging.getLogger(__name__)
18
+
14
19
 
15
20
  def cmd_file_tree(fmt: str) -> None:
16
21
  """获取当前场景中已打开的所有文件及每个文件的图层信息(需 Dimine 运行)。"""
@@ -81,7 +86,7 @@ def cmd_file_save(fmt: str, output_path: str, file_id: str | None) -> None:
81
86
 
82
87
 
83
88
  def cmd_file_open(fmt: str, path: str) -> None:
84
- """在 Dimine 中打开文件(需 Dimine 运行)。如果文件已在 Dimine 中打开,先关闭再重新打开。"""
89
+ """在 Dimine 中打开文件(需 Dimine 运行)。同名文件已打开则先关闭再重开;打开 .dmo 前会把已打开的其它 .dmo 一并关闭。"""
85
90
  err = require_remote_sdk()
86
91
  if err:
87
92
  output_error(err, fmt)
@@ -91,13 +96,28 @@ def cmd_file_open(fmt: str, path: str) -> None:
91
96
  async def _run():
92
97
  client = await get_client()
93
98
 
99
+ # 先确认待打开的新文件存在,再考虑关闭已打开文件,避免误关旧文件后才发现打不开
100
+ if not os.path.isfile(file_path):
101
+ raise_or_exit(f"文件不存在: {file_path}", fmt)
102
+
103
+ # Dimine 同一时刻只能打开一个 .dmo(炮孔数据库)文件:打开 .dmo 前若已有
104
+ # .dmo 打开须先关闭,否则 open_file 会提示 "file already opened"。
105
+ is_dmo = str(file_path).lower().endswith(".dmo")
94
106
  # 检查是否有同名文件已打开,如果有则先关闭
95
107
  target_name = path
96
108
  opened_files = await client.get_files()
97
109
  for f in opened_files:
98
- if f.name == target_name:
99
- await client.close_file(f.id)
100
- break
110
+ if is_dmo:
111
+ # DMO:关闭当前已打开的所有 .dmo 文件
112
+ if (f.name or "").lower().endswith(".dmo"):
113
+ logger.info("file-open: 打开新 .dmo 前关闭已打开文件(id=%s name=%s)", f.id, f.name)
114
+ await client.close_file(f.id)
115
+ else:
116
+ # 非 DMO:仅当同名文件已打开时先关闭再重新打开
117
+ if f.name == target_name:
118
+ logger.info("file-open: 同名文件已打开,先关闭再重开(id=%s name=%s)", f.id, f.name)
119
+ await client.close_file(f.id)
120
+ break
101
121
 
102
122
  # 打开文件
103
123
  await client.open_file(file_path)
@@ -70,8 +70,8 @@ def cmd_layer_create(fmt: str, name: str, file_id: str | None) -> None:
70
70
  handle_sdk_error(e, fmt, "创建图层失败")
71
71
 
72
72
 
73
- def cmd_layer_visibility(fmt: str, file_id: str, layer_name: str, action: str) -> None:
74
- """设置图层显示/隐藏状态(需 Dimine 运行)。"""
73
+ def cmd_layer_visibility(fmt: str, file_id: str, layer_names: list[str], action: str) -> None:
74
+ """设置图层显示/隐藏状态,支持一次操作多个图层(需 Dimine 运行)。"""
75
75
  err = require_remote_sdk()
76
76
  if err:
77
77
  output_error(err, fmt)
@@ -86,15 +86,15 @@ def cmd_layer_visibility(fmt: str, file_id: str, layer_name: str, action: str) -
86
86
 
87
87
  async def _run():
88
88
  client = await get_client()
89
- await client.set_layer_visibility(
90
- file_id=file_id,
91
- layer_name=layer_name,
89
+ await client.set_layers_visibility(
90
+ layers=[{"file": file_id, "layer": name} for name in layer_names],
92
91
  action=valid_actions[action],
93
92
  )
94
93
  action_cn = {"show": "显示", "hide": "隐藏", "exclusive_show": "排他显示"}
95
94
  return {
96
95
  "success": True,
97
- "message": f"已成功{action_cn.get(action, action)}图层 {layer_name}",
96
+ "layer_count": len(layer_names),
97
+ "message": f"已成功{action_cn.get(action, action)}图层 {'、'.join(layer_names)}",
98
98
  }
99
99
 
100
100
  try:
@@ -150,7 +150,8 @@ def register_commands(subparsers, format_parent) -> None:
150
150
 
151
151
  p = subparsers.add_parser("layer-visibility", parents=[format_parent], help="设置图层显示/隐藏状态")
152
152
  p.add_argument("--file-id", required=True, help="文件ID")
153
- p.add_argument("--layer-name", required=True, help="图层名称")
153
+ p.add_argument("--layer-name", action="append", required=True, dest="layer_names",
154
+ help="图层名称,可重复指定以批量操作")
154
155
  p.add_argument("--action", choices=["show", "hide", "exclusive_show"], default="show", help="操作类型(默认: show)")
155
156
 
156
157
 
@@ -158,5 +159,5 @@ COMMAND_HANDLERS = {
158
159
  "layers": lambda args: cmd_layers(args.format, args.file_id),
159
160
  "layer-create": lambda args: cmd_layer_create(args.format, args.name, args.file_id),
160
161
  "layer-delete": lambda args: cmd_layer_delete(args.format, args.name, args.file_id),
161
- "layer-visibility": lambda args: cmd_layer_visibility(args.format, args.file_id, args.layer_name, args.action),
162
+ "layer-visibility": lambda args: cmd_layer_visibility(args.format, args.file_id, args.layer_names, args.action),
162
163
  }
@@ -14,6 +14,7 @@ from dimine_python_sdk.logger import logger
14
14
  from dimine_python_sdk.models.conn import ConnEvent, ConnRequest, ConnResponse,CommandType
15
15
  from dimine_python_sdk.models.errors import ResponseError
16
16
 
17
+
17
18
  @dataclass
18
19
  class BaseConnClient:
19
20
  model_config = {"arbitrary_types_allowed": True}
@@ -137,8 +138,7 @@ class BaseConnClient:
137
138
  self.pending_requests.pop(request.request_id)
138
139
  raise e
139
140
  else:
140
- result = await future
141
- return result
141
+ return await future
142
142
 
143
143
  async def _send_command(self, command: CommandType, payload:Optional[Any]=None, callback: Optional[Callable] = None)->ConnResponse:
144
144
  """发送请求并等待响应(内部方法,外部不应直接调用)"""
@@ -1,14 +1,15 @@
1
1
 
2
+ from __future__ import annotations
3
+
2
4
  from contextlib import asynccontextmanager
3
5
  from pathlib import Path
4
6
  import shutil
5
7
  import tempfile
6
8
  from typing import Any, AsyncGenerator, Dict, List, Literal, Optional
7
- import numpy as np
8
9
  from dimine_python_sdk.conn.base_conn_client import BaseConnClient
9
10
  from dimine_python_sdk.models import FileInfo, ResponseStatus, CommandType,LayerInfo, FeatureInfo, SelectedItem, SelectedResult, SelectMode, VisibilityAction
10
11
 
11
- from dimine_python_sdk.models.types import GEOMETRY, Line, Point, Shell, TextInfo
12
+ from dimine_python_sdk.models.types import TextInfo
12
13
 
13
14
 
14
15
  class ConnClient(BaseConnClient):
@@ -138,6 +139,12 @@ class ConnClient(BaseConnClient):
138
139
  threshold: int,
139
140
  ) -> bool:
140
141
  """判断单个几何体是否应走本地文件传输路径。"""
142
+ # 几何类型与 numpy 均为惰性导入:远程命令路径(file-tree/layers 等)不碰它们,
143
+ # 从而避免在导入期拉起 numpy。
144
+ import numpy as np
145
+
146
+ from dimine_python_sdk.models.geometry import Line, Point, Shell
147
+
141
148
  if file_transfer is True:
142
149
  return True
143
150
  if file_transfer is False:
@@ -181,6 +188,10 @@ class ConnClient(BaseConnClient):
181
188
 
182
189
  def _entity_type_for_file_transfer(self, geom: GEOMETRY) -> str:
183
190
  """为文件传输路径计算与 DMF 实体一致的 entity_type。"""
191
+ import numpy as np
192
+
193
+ from dimine_python_sdk.models.geometry import Line, Point, Shell
194
+
184
195
  if isinstance(geom, Point):
185
196
  return "point"
186
197
  if isinstance(geom, Line):
@@ -192,6 +203,10 @@ class ConnClient(BaseConnClient):
192
203
 
193
204
  def _insert_geometry_into_dmf(self, dmf, geom: GEOMETRY) -> None:
194
205
  """将几何体写入 DMF 内存模型(图层名为 ``"0"``)。"""
206
+ import numpy as np
207
+
208
+ from dimine_python_sdk.models.geometry import Line, Point, Shell
209
+
195
210
  name = geom.feature or "0"
196
211
  if isinstance(geom, Point):
197
212
  dmf.insert_point("0", geom.geometry, name=name)
@@ -264,6 +279,10 @@ class ConnClient(BaseConnClient):
264
279
  features: 获取哪些要素相关的模型
265
280
  with_geometry: 是否返回模型坐标信息,默认 False
266
281
  """
282
+ import numpy as np
283
+
284
+ from dimine_python_sdk.models.geometry import Line, Point, Shell
285
+
267
286
  # 先获取要素信息,
268
287
  all_features = await self.get_features(file_id)
269
288
 
@@ -409,6 +428,8 @@ class ConnClient(BaseConnClient):
409
428
  file_transfer_threshold: int = 10000,
410
429
  ) -> None:
411
430
  """创建或更新多个模型, 如果指定了file_id和layer_id, 则将模型添加到指定的文件及图层中"""
431
+ from dimine_python_sdk.models.geometry import Line, Point, Shell
432
+
412
433
  if isinstance(geometries, (Point, Line, Shell)):
413
434
  geometries = [geometries]
414
435
  update_geometries = [g for g in geometries if g.related]
@@ -604,27 +625,45 @@ class ConnClient(BaseConnClient):
604
625
  if response.status == ResponseStatus.FAIL:
605
626
  raise Exception(response.error)
606
627
 
607
- async def set_layer_visibility(self, file_id: str, layer_name: str,
608
- action: VisibilityAction | str = VisibilityAction.SHOW) -> None:
609
- """设置图层显示/隐藏状态
628
+ async def set_layers_visibility(self, layers: List[Dict[str, str]],
629
+ action: VisibilityAction | str = VisibilityAction.SHOW) -> None:
630
+ """批量设置图层显示/隐藏状态
610
631
 
611
632
  Args:
612
- file_id: 文件ID或文件名称
613
- layer_name: 图层名称
633
+ layers: 要操作的图层列表,每个元素包含:
634
+ - file: 文件ID或文件名称
635
+ - layer: 图层名称
614
636
  action: 操作类型,可选 show(显示) | hide(隐藏) | exclusive_show(排他性显示)
615
637
 
616
638
  Raises:
617
639
  Exception: 操作失败时抛出异常
618
640
  """
641
+ if not layers:
642
+ return
619
643
  payload = {
620
- "file": file_id,
621
- "layer": layer_name,
644
+ "layers": layers,
622
645
  "action": str(action)
623
646
  }
624
647
  response = await self._send_command(CommandType.LAYER_VISIBILITY, payload)
625
648
  if response.status == ResponseStatus.FAIL:
626
649
  raise Exception(response.error)
627
650
 
651
+ async def set_layer_visibility(self, file_id: str, layer_name: str,
652
+ action: VisibilityAction | str = VisibilityAction.SHOW) -> None:
653
+ """设置单个图层的显示/隐藏状态
654
+
655
+ Args:
656
+ file_id: 文件ID或文件名称
657
+ layer_name: 图层名称
658
+ action: 操作类型,可选 show(显示) | hide(隐藏) | exclusive_show(排他性显示)
659
+
660
+ Raises:
661
+ Exception: 操作失败时抛出异常
662
+ """
663
+ await self.set_layers_visibility(
664
+ [{"file": file_id, "layer": layer_name}], action=action
665
+ )
666
+
628
667
  async def set_file_visibility(self, file_id: str,
629
668
  action: VisibilityAction | str = VisibilityAction.SHOW) -> None:
630
669
  """设置文件显示/隐藏状态
@@ -176,6 +176,7 @@ $ dimine geometry-create --geometries '[{"type":"line","file":"f-001","layer":"l
176
176
  $ dimine layer-visibility --file-id f-20240527-001 --layer-name 矿体 --action exclusive_show
177
177
  {
178
178
  "success": true,
179
+ "layer_count": 1,
179
180
  "message": "已成功排他显示图层 矿体"
180
181
  }
181
182
  ```
@@ -186,11 +187,25 @@ $ dimine layer-visibility --file-id f-20240527-001 --layer-name 矿体 --action
186
187
  $ dimine layer-visibility --file-id f-20240527-001 --layer-name 巷道 --action hide
187
188
  {
188
189
  "success": true,
190
+ "layer_count": 1,
189
191
  "message": "已成功隐藏图层 巷道"
190
192
  }
191
193
  ```
192
194
 
193
- ### 3. 隐藏整个文件的所有图层
195
+ ### 3. 批量操作同一文件下的多个图层
196
+
197
+ `--layer-name` 可重复指定,一次批量操作多个图层。
198
+
199
+ ```bash
200
+ $ dimine layer-visibility --file-id f-20240527-001 --layer-name 矿体 --layer-name 巷道 --action exclusive_show
201
+ {
202
+ "success": true,
203
+ "layer_count": 2,
204
+ "message": "已成功排他显示图层 矿体、巷道"
205
+ }
206
+ ```
207
+
208
+ ### 4. 隐藏整个文件的所有图层
194
209
 
195
210
  ```bash
196
211
  $ dimine file-visibility --file-id f-20240527-001 --action hide