osis-python 0.5.2__py3-none-any.whl → 0.5.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osis-python
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: A Python client library for Osis APDL
5
5
  Author-email: "CCCC Highway Consultant Co. Ltd." <support@osisbim.com>
6
6
  License-Expression: MIT
@@ -1,5 +1,5 @@
1
- osis_python-0.5.2.dist-info/licenses/LICENSE,sha256=T4mkSYliXdU9mYe-NfHkqg6GPhzoYaIQO_a630UzpS4,1088
2
- pyosis/__init__.py,sha256=0MXFndPyNnk54MetElaMxO4ikTZeOokTMRdtJL-IcPc,501
1
+ osis_python-0.5.4.dist-info/licenses/LICENSE,sha256=T4mkSYliXdU9mYe-NfHkqg6GPhzoYaIQO_a630UzpS4,1088
2
+ pyosis/__init__.py,sha256=N9nzd0g89imrL0phXM-9CNHnHaggd9920UZPFTH96A0,501
3
3
  pyosis/boundary/__init__.py,sha256=W5LfF-jugWa-YI8pnfELZaz0pXLa-D8TiGnBn12HGGQ,102
4
4
  pyosis/boundary/interface.py,sha256=3MG3ZwOP_HM6WF6NTs8oyaTwucdCDa5CANifOdVZ9u8,8998
5
5
  pyosis/boundary/manager.py,sha256=SZAGysQFju2yEbibTTY_y0YosDVLUvA5pq-IOPB3WN0,31839
@@ -9,16 +9,19 @@ pyosis/control/io.py,sha256=buaut3-W8kwJIe3PgjtgOU0JCSgDfOEd2cS3mxYsfuc,591
9
9
  pyosis/control/manager.py,sha256=9vzNx6E9MVxU2DVNledeWv6UQaQNr3W9q5BJrqsnxzU,6795
10
10
  pyosis/control/nonlinear.py,sha256=26W_2GhD_0cn3YQxJjgbPR1oEopJWyuFWGlgx0q023I,1604
11
11
  pyosis/core/__init__.py,sha256=KvFYe6Iaain38GbuHT4s-gmxSWYzrnJDSCu8H_0FZBA,523
12
- pyosis/core/all_func.py,sha256=iJZasXz49i7IlJ-2erNfX1s2iohNYCt0PRKrfQpgvg8,497
13
- pyosis/core/apdl_sync.py,sha256=hGN-nWBcFaPHawR8hFYB59Yc6E3Kdgw9DEO-ZpcBkdY,5572
12
+ pyosis/core/all_func.py,sha256=VhvAeMJMCQJ1redjqdkm7meTw1PJNDoRdmXXh2LzkVo,521
13
+ pyosis/core/apdl_sync.py,sha256=rW5Ot6FLpmUzpfMlrbemstnfhxD_0qtPrWgDfj9_4XY,5312
14
14
  pyosis/core/basic_manager.py,sha256=S_IVCCGZXTEmNQ-0T4MNEG4xvHMyA3Y2kmeupHCjnHU,230
15
15
  pyosis/core/build.py,sha256=imXSZ8tU1yxY-a3kgZ0BwQEQggsWlG19bxHhYbi6mus,80845
16
16
  pyosis/core/client.py,sha256=0Fx--DW0aVvqnDUP0iUTPLvnDEPKfnA6jOE-nJVQJrU,2838
17
17
  pyosis/core/command.py,sha256=dmI7UtdELTTqziHh4w2NmhsU4QagGPdu1q53NGCEQ_U,8559
18
- pyosis/core/engine.py,sha256=NGcCtZ7-IPThEm5lZX4gb9LrE60RgUmJg6bwCMwJqD8,13936
18
+ pyosis/core/engine.py,sha256=N5Xp8Z3y-e4_dI0Vg5UtDFMzJjqI8kVyGiIZyB64n3I,15132
19
19
  pyosis/core/exceptions.py,sha256=x0I0Emooejy8wF1ZBWGz7Yc1N70MitsnhhQv9txJQj8,1093
20
20
  pyosis/core/references.py,sha256=JQ8260aqVB5PhhGrqMt7W7fmrsmE69dFdrE6EyyLADg,2755
21
- pyosis/core/sync_apdl_cli.py,sha256=Zfszet2oXXBrKWyDu56em88KWTuUOHLYLYPZhjuYrV8,817
21
+ pyosis/core/sync_apdl_cli.py,sha256=xlRDhtR4fOebLvyVZT9joUp6Q0H55gIt0YDy9Cb4HB4,874
22
+ pyosis/display/__init__.py,sha256=POxWZ8I5Qanj4ETpHBZMhXNOMlsLMYRamXwP_JjSDUo,161
23
+ pyosis/display/interface.py,sha256=QCMFWG7Xxh8RDSbvdtnT3fyg90pnKAV-DX-LnL2xtUM,3133
24
+ pyosis/display/manager.py,sha256=wV_JMDFH25TkrdBn6mFZvmIcju62297sWR7nlKvBgos,3720
22
25
  pyosis/dynamic/__init__.py,sha256=ThVGZWd8qryNJc99IPKJ7oA10hg1skiHTYvWPBelB2U,314
23
26
  pyosis/dynamic/load_to_mass.py,sha256=xmSC16ug2C8HaY6rEwxfSX3puF5qjsUPsu44Skl8pyA,2940
24
27
  pyosis/dynamic/manager.py,sha256=v5SyICAIimFcJwmHPL8AF49PrqsS8kNivz1OQaqTR10,25829
@@ -26,7 +29,7 @@ pyosis/dynamic/modal.py,sha256=__as3Nl7Ks2ntgcZwAA9UhvsvTfBl-1zSUOTsnPVf-Y,640
26
29
  pyosis/dynamic/seismic.py,sha256=8DIh7xkUgl0UwweDq4WoYSNzMxpLnpDaM4W_7u3rg_A,6380
27
30
  pyosis/element/__init__.py,sha256=fZgmxJiNhCe3Ki8vNEdZOhyinxwvlF6Jcu-DgD-c6YM,92
28
31
  pyosis/element/interface.py,sha256=xLd2ljq0qW5uvqV0Lo2e6Ly7NShh_p2I5vQNdcA8P4Q,9872
29
- pyosis/element/manager.py,sha256=1FRk9LaKloNwiAw5vHjgYvh4zs1ntIFTv4l7RY0fVpQ,28352
32
+ pyosis/element/manager.py,sha256=tjvKDjj7_mt6dsvd1ABobdUA1IxH7Nrfy-5SKKNkT5o,28776
30
33
  pyosis/general/__init__.py,sha256=dyvge-GgUpT3BeDwm_Lss1xqNlxAQdt63WJ7QcuZ_O4,24
31
34
  pyosis/general/interface.py,sha256=YRXQ-iXYVhiUBiUd7fpA0NOtL5VSueOHcobj625aQeU,5382
32
35
  pyosis/geometry/__init__.py,sha256=mlAXlxaohcEcm8-6anyDo1fevh_IzYz2OXBn0_DIrCY,99
@@ -54,8 +57,8 @@ pyosis/post/design_check.py,sha256=vJzpu63yBjU4QDUIxvLOhUvqPrZ56q301ADCgHdu0eA,1
54
57
  pyosis/post/display.py,sha256=BdoHetcM69Ez4s0-Zx3MreIcadxJPv2U2cx0E0PJTVw,2006
55
58
  pyosis/post/manager.py,sha256=wj0nGIqdlRhu9zqWxZAAhNAt9FB6vX0-hVtIYcdC6zU,19927
56
59
  pyosis/project/__init__.py,sha256=hYfnubp0bbHJK2tBBH12XDs3CeeO0d7whzILDxl4044,78
57
- pyosis/project/interface.py,sha256=_pDttdXT4Kw8iXfVX3pLlr4vKY5XI27EwbvZAOr9wSg,1294
58
- pyosis/project/manager.py,sha256=IRAZbfCrt-rTZ6PWHhvkuA8qGjIgavQXyR8bEUMo-NQ,2734
60
+ pyosis/project/interface.py,sha256=Iiku4SsBlBkH216Hrb2E3VrqzHnoLW6tlGwxfFDwbpU,1522
61
+ pyosis/project/manager.py,sha256=iVgSFQXsbIo-qTALN7bEltM7m5NWEBSB7ZoJ-FnmM58,3545
59
62
  pyosis/property/__init__.py,sha256=xGdJItBRic3rd-6i5vyjGfA3KY-e9-WuOquIEnjPP_4,191
60
63
  pyosis/property/component_thickness.py,sha256=jrbmc4TwulxakRXKJpyC0oDue5EKLDODcKZHObf2tTw,728
61
64
  pyosis/property/coordinate.py,sha256=xFfuzXCzBWqYsDxHifpYCYgC_to9kC4LKD2yTX2CtvM,2668
@@ -96,11 +99,11 @@ pyosis/thickness/manager.py,sha256=ALJXBQEwIdIXmoYcjyvrYdh1mzNEygSf5Eo6y__0u6c,4
96
99
  pyosis/transfer/__init__.py,sha256=OZVxVnjhDGe4fHC6pjtOaKPzAKuoDp3-XkNFzDWepsE,623
97
100
  pyosis/transfer/generator.py,sha256=T3YLf4TrqAF4FFIa84opclmNDCw5WpvbNIsKC2rvKcU,3541
98
101
  pyosis/transfer/matrix.py,sha256=sHw9X34-PUeY1EdoNklgK0LUHwbBPnhkhk0R9ME0XG0,3378
99
- pyosis/transfer/out_to_python.py,sha256=rqNii7SBOLOYp8yGnh0F_LERS99JjMyyVCxBXVP99_4,8592
100
- pyosis/transfer/parser.py,sha256=KM2eK7SZTgn2ooAOv5gvqHNCw85DTrpoOB_393wzbMI,6422
101
- pyosis/transfer/routes.py,sha256=P5Iy6raVHRKqVrPmVgnoFKjmGwdNqyioiLrSwk6G85U,8077
102
+ pyosis/transfer/out_to_python.py,sha256=WMzu1GaVvzvO1oq9ihyyp9dn2xY4s6ualW5M7CO98Cg,14464
103
+ pyosis/transfer/parser.py,sha256=R_aBlYJTe53hD1sX5PBv86bhj88NQwANFRN4pGYudQ8,6774
104
+ pyosis/transfer/routes.py,sha256=GAgrx71utq1tToxQKtfYeBwBvy7bktvFWEvWq3FPYIk,8267
102
105
  pyosis/transfer/split.py,sha256=hpBe9D9WbjbKsovx4hM3X5RjTrruWNkuFmZt2ed0fME,1284
103
- osis_python-0.5.2.dist-info/METADATA,sha256=uCoC9QTxfh27Sp9XIpxmISg3IeNp5HKigrEhh16epJw,11330
104
- osis_python-0.5.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
105
- osis_python-0.5.2.dist-info/top_level.txt,sha256=6VDNySkQKwipRXcN8ayKVci4vAXi4oPyWLqFhNVcIfM,7
106
- osis_python-0.5.2.dist-info/RECORD,,
106
+ osis_python-0.5.4.dist-info/METADATA,sha256=KK6oUHASR6A6uzfKiE2BrL0Q6lOTSNBO9itDyoHs3aA,11330
107
+ osis_python-0.5.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
108
+ osis_python-0.5.4.dist-info/top_level.txt,sha256=6VDNySkQKwipRXcN8ayKVci4vAXi4oPyWLqFhNVcIfM,7
109
+ osis_python-0.5.4.dist-info/RECORD,,
pyosis/__init__.py CHANGED
@@ -7,7 +7,7 @@ A library for extending OSIS functionality.
7
7
  A library that allows users to use CAE with great flexibility.
8
8
  """
9
9
 
10
- __version__ = "0.5.2"
10
+ __version__ = "0.5.4"
11
11
  __author__ = "CCCC Highway Consultant Co. Ltd." # 中交公路规划设计院 - 工程技术研究中心 - lzh - wjh - myf
12
12
 
13
13
  from .core.engine import OSISEngine
pyosis/core/all_func.py CHANGED
@@ -19,4 +19,5 @@ from ..settlement import *
19
19
  from ..stability import *
20
20
 
21
21
  from ..post import *
22
- from ..result import *
22
+ from ..result import *
23
+ from ..display import *
pyosis/core/apdl_sync.py CHANGED
@@ -7,9 +7,9 @@
7
7
  4. 有变化 → parse → 写 prep → 存本次 hash
8
8
 
9
9
  用法:
10
- result = engine.sync_apdl()
11
- if result and result.changed:
12
- print(result.summary())
10
+ changed = engine.sync_apdl()
11
+ if changed:
12
+ print("命令流已变化,已写 prep")
13
13
  """
14
14
 
15
15
  from __future__ import annotations
@@ -21,7 +21,6 @@ from dataclasses import dataclass
21
21
  from pathlib import Path
22
22
  from typing import TYPE_CHECKING
23
23
 
24
- from ..transfer import parse_text
25
24
  from ..transfer.out_to_python import write_prep_outputs
26
25
 
27
26
  if TYPE_CHECKING:
@@ -39,19 +38,9 @@ _TIME_LINE_PATTERN = re.compile(
39
38
  )
40
39
 
41
40
 
42
- @dataclass
43
- class ApdlSyncResult:
44
- """APDL 同步结果。"""
45
-
46
- changed: bool = False
47
-
48
- def summary(self) -> str:
49
- return "命令流已变化,已写 prep" if self.changed else "命令流未变化"
50
-
51
-
52
41
  @dataclass
53
42
  class ApdlSessionStore:
54
- """记录最近一次同步的 .out 路径与 hash(供调试/展示,不由 ApdlSyncResult 重复携带)。"""
43
+ """记录最近一次同步的 .out 路径与 hash(供调试/展示)。"""
55
44
 
56
45
  last_path: str = ""
57
46
  last_hash: str = ""
@@ -162,11 +151,14 @@ def perform_apdl_sync(
162
151
  engine: "OSISEngine",
163
152
  path: str | None = None,
164
153
  *,
165
- force_export: bool = True,
166
- ) -> ApdlSyncResult | None:
154
+ force_export: bool = False,
155
+ ) -> bool | None:
167
156
  """执行 APDL 同步:export → 比 hash → 有变化则写 prep(不执行 prep)。
168
157
 
169
- 无项目路径时返回 None。
158
+ Returns:
159
+ True - 命令流已变化,已写 prep
160
+ False - 命令流未变化
161
+ None - 未获取到项目路径(无法同步)
170
162
  """
171
163
  if _get_project_dir(engine) is None:
172
164
  return None
@@ -186,12 +178,11 @@ def perform_apdl_sync(
186
178
  session.update_file(str(out_path), file_hash)
187
179
 
188
180
  if file_hash == last_hash:
189
- return ApdlSyncResult(changed=False)
181
+ return False
190
182
 
191
- parsed = parse_text(text)
192
- write_prep_outputs(parsed, prep_dir)
183
+ write_prep_outputs(out_path, prep_dir)
193
184
 
194
185
  # 未执行 prep,OSIS 未变
195
186
  _save_persisted_hash(state_path, out_path, file_hash)
196
187
 
197
- return ApdlSyncResult(changed=True)
188
+ return True
pyosis/core/engine.py CHANGED
@@ -22,7 +22,7 @@ from __future__ import annotations
22
22
  import os
23
23
  from typing import Any
24
24
 
25
- from .apdl_sync import ApdlSyncResult, ApdlSessionStore, perform_apdl_sync
25
+ from .apdl_sync import ApdlSessionStore, perform_apdl_sync
26
26
  from ..material.manager import MaterialManager, material_manager
27
27
  from ..section.manager import SectionManager, section_manager
28
28
  from ..node.manager import NodeManager, node_manager
@@ -41,6 +41,7 @@ from ..stability.manager import StabilityManager, stability_manager
41
41
  from ..dynamic.manager import DynamicManager, dynamic_manager
42
42
  from ..post.manager import PostManager, post_manager
43
43
  from ..result.manager import ResultManager, result_manager
44
+ from ..display.manager import DisplayManager, display_manager
44
45
  from ..general.interface import (
45
46
  osis_matrix,
46
47
  output_result_for_calc_book,
@@ -78,6 +79,7 @@ class OSISEngine:
78
79
  - dynamic: 动力分析管理器(荷载转换质量、模态、反应谱)
79
80
  - post: 后处理管理器(荷载组合、规范验算、结果显示)
80
81
  - result: 结果导出管理器(工况结果、包络结果、验算结果)
82
+ - display: 显示管理器(边界 / 荷载 / 钢束显隐等)
81
83
 
82
84
  通用操作可直接通过 engine 调用:
83
85
  - run(), solve(), replot(), clear(), clc()
@@ -114,6 +116,9 @@ class OSISEngine:
114
116
  ### 后处理 ###
115
117
  self._post = post_manager
116
118
  self._result = result_manager
119
+
120
+ ### 显示 ###
121
+ self._display = display_manager
117
122
 
118
123
  # OSIS端口与网址
119
124
  if port is None:
@@ -228,6 +233,22 @@ class OSISEngine:
228
233
  """结果导出管理器(工况结果、包络结果、验算结果)"""
229
234
  return self._result
230
235
 
236
+ ### 显示 ###
237
+
238
+ @property
239
+ def display(self) -> DisplayManager:
240
+ """显示管理器(边界 / 荷载 / 钢束显隐等)"""
241
+ return self._display
242
+
243
+
244
+ def start(self) -> None:
245
+ """开启 OSIS 软件"""
246
+ ...
247
+
248
+ def close(self) -> None:
249
+ """关闭 OSIS 软件"""
250
+ ...
251
+
231
252
  # ──────────────────────────────────────────
232
253
  # 通用操作(直接暴露 general 和 core 的函数)
233
254
  # ──────────────────────────────────────────
@@ -297,9 +318,14 @@ class OSISEngine:
297
318
  # 便捷方法(委托给 project manager)
298
319
  # ──────────────────────────────────────────
299
320
 
300
- def save_project(self) -> None:
301
- """保存当前项目"""
302
- self._project.save()
321
+ def save_project(self, filepath: str = "") -> None:
322
+ """保存项目
323
+
324
+ Args:
325
+ filepath: 工程文件路径。为空时使用当前项目路径;
326
+ 非空时使用传入路径。
327
+ """
328
+ self._project.save(filepath)
303
329
 
304
330
  def new_project(self, type: int = 1, filepath: str = "") -> None:
305
331
  """新建项目
@@ -387,8 +413,21 @@ class OSISEngine:
387
413
  return f"OSISEngine(project_path={self.project.get_directory()})"
388
414
 
389
415
  def sync_apdl(
390
- self,
391
- path: str | None = None,
392
- ) -> ApdlSyncResult:
393
- """通过 APDL 导出从 OSIS 同步数据。OSIS 界面操作后调用。"""
394
- return perform_apdl_sync(self, path)
416
+ self,
417
+ path: str | None = None,
418
+ *,
419
+ force_export: bool = False,
420
+ ) -> bool | None:
421
+ """通过 APDL 导出从 OSIS 同步数据。OSIS 界面操作后调用。
422
+
423
+ Args:
424
+ path: 自定义 .out 文件路径,None 用项目下默认 _pyosis_sync.out。
425
+ force_export: False(默认)若 .out 已存在则直接复用,仅当 hash 与上次不一致才写 prep;
426
+ True 时总是重新执行 export_apdl。
427
+
428
+ Returns:
429
+ True - 命令流已变化,已写 prep
430
+ False - 命令流未变化
431
+ None - 未获取到项目路径
432
+ """
433
+ return perform_apdl_sync(self, path, force_export=force_export)
@@ -20,16 +20,16 @@ from .engine import OSISEngine
20
20
  def main() -> int:
21
21
  engine = OSISEngine()
22
22
  try:
23
- result = perform_apdl_sync(engine)
23
+ changed = perform_apdl_sync(engine)
24
24
  except Exception as exc:
25
25
  print(f"sync_apdl 失败: {exc}", file=sys.stderr)
26
26
  return 1
27
27
 
28
- if result is None:
28
+ if changed is None:
29
29
  print("跳过:未获取到项目路径")
30
30
  return 0
31
31
 
32
- print(result.summary())
32
+ print("命令流已变化,已写 prep" if changed else "命令流未变化")
33
33
  return 0
34
34
 
35
35
 
@@ -0,0 +1,8 @@
1
+ """
2
+ pyosis.display
3
+
4
+ 显示控制(显隐、视图方向、显示开关等)
5
+ """
6
+
7
+ from .interface import *
8
+ from .manager import DisplayManager, display_manager
@@ -0,0 +1,103 @@
1
+ """显示控制命令接口
2
+
3
+ 控制边界、荷载、钢束显隐,以及视图方向、显示开关等。
4
+ """
5
+
6
+ from typing import Literal, Sequence, Union
7
+
8
+ from ..core import REGISTRY
9
+
10
+
11
+ @REGISTRY.register("DispCtrl")
12
+ def osis_disp_ctrl(
13
+ eObject: Literal["bc", "boundary", "lg", "load", "td", "tendon"],
14
+ eType: str,
15
+ ids: Union[str, int, Sequence[Union[str, int]]],
16
+ bShow: Literal[0, 1] = 1,
17
+ ):
18
+ """控制边界 / 荷载 / 钢束的显示与隐藏。
19
+
20
+ 格式: ``dispctrl,<Object>,<Type>,<Number/Name...>,<0|1>``
21
+
22
+ Args:
23
+ eObject (str): 对象类型
24
+ * ``bc`` / ``boundary`` = 边界
25
+ * ``lg`` / ``load`` = 荷载
26
+ * ``td`` / ``tendon`` = 钢束
27
+ eType (str): 子类型
28
+ * 边界: ``all``, ``general``, ``mstslv``, ``release``, ``elstcspt``,
29
+ ``generalelstcspt``, ``rigid``, ``secfactor``
30
+ * 荷载: ``all``, ``gravity``, ``nforce``, ``line``, ``pointforce``,
31
+ ``pointmoment``, ``displacement``, ``initial``, ``utemp``, ``gtemp``,
32
+ ``pst``, ``cforce``, ``elementsurface``
33
+ * 钢束: 通常为 ``all``
34
+ ids: 编号或名称
35
+ * ``all`` = 全部
36
+ * 单个编号 / 名称,或多个组成的序列(如 ``[1, 2, "3to10"]``、``["T1", "T2"]``)
37
+ bShow (int): ``0`` = 隐藏,``1`` = 显示
38
+
39
+ Returns:
40
+ tuple (bool, str): 是否成功,失败原因
41
+
42
+ Examples:
43
+ >>> # 显示全部边界
44
+ >>> osis_disp_ctrl("bc", "all", "all", 1)
45
+ >>> # 显示一般边界 1、2、3to10
46
+ >>> osis_disp_ctrl("bc", "general", [1, 2, "3to10"], 1)
47
+ >>> # 显示全部节点力荷载
48
+ >>> osis_disp_ctrl("lg", "nforce", "all", 1)
49
+ >>> # 显示名为 DeadLoad 的荷载
50
+ >>> osis_disp_ctrl("lg", "all", "DeadLoad", 1)
51
+ >>> # 隐藏钢束 1、2、3to10
52
+ >>> osis_disp_ctrl("td", "all", [1, 2, "3to10"], 0)
53
+ >>> # 显示钢束 T1、T2
54
+ >>> osis_disp_ctrl("td", "all", ["T1", "T2"], 1)
55
+ """
56
+ pass
57
+
58
+
59
+ @REGISTRY.register("/control")
60
+ def osis_control(
61
+ eAction: str,
62
+ eArg: str | None = None,
63
+ ):
64
+ """视图 / 界面控制命令。
65
+
66
+ 格式: ``/control,<Action>[,<Arg>]``
67
+
68
+ Args:
69
+ eAction (str): 子命令
70
+ * ``view`` = 切换视图方向(配合 eArg)
71
+ * 其他如 ``quickCreateModel`` 等
72
+ eArg (str | None): 子命令参数
73
+ * 当 eAction 为 ``view`` 时: ``standard`` / ``top`` / ``right`` / ``front``
74
+
75
+ Returns:
76
+ tuple (bool, str): 是否成功,失败原因
77
+
78
+ Examples:
79
+ >>> osis_control("view", "standard")
80
+ >>> osis_control("view", "top")
81
+ >>> osis_control("view", "right")
82
+ >>> osis_control("view", "front")
83
+ """
84
+ pass
85
+
86
+
87
+ @REGISTRY.register("Plsm")
88
+ def osis_plsm(bFlag: Literal[0, 1] = 1):
89
+ """显示开关(Plsm)。
90
+
91
+ 格式: ``plsm,<0|1>``
92
+
93
+ Args:
94
+ bFlag (int): ``0`` = 关,``1`` = 开
95
+
96
+ Returns:
97
+ tuple (bool, str): 是否成功,失败原因
98
+
99
+ Examples:
100
+ >>> osis_plsm(1)
101
+ >>> osis_plsm(0)
102
+ """
103
+ pass
@@ -0,0 +1,115 @@
1
+ """显示管理器 - 控制显隐、视图方向与显示开关
2
+
3
+ 设计理念:
4
+ - 隐藏底层命令接口细节,提供原生 Python 风格 API
5
+ - 无状态设计,操作直接提交到 OSIS
6
+
7
+ 用法:
8
+ >>> from pyosis.display import display_manager
9
+ >>> display_manager.disp_ctrl("bc", "all", "all", 1)
10
+ >>> display_manager.set_view("top")
11
+ >>> display_manager.set_plsm(1)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Literal, Sequence, Union
17
+
18
+ from .interface import osis_control, osis_disp_ctrl, osis_plsm
19
+
20
+
21
+ class DisplayManager:
22
+ """显示管理器
23
+
24
+ 控制边界 / 荷载 / 钢束显隐、视图方向与显示开关。
25
+ """
26
+
27
+ def disp_ctrl(
28
+ self,
29
+ obj: Literal["bc", "boundary", "lg", "load", "td", "tendon"],
30
+ type: str,
31
+ ids: Union[str, int, Sequence[Union[str, int]]],
32
+ show: Literal[0, 1] = 1,
33
+ ) -> None:
34
+ """控制边界 / 荷载 / 钢束的显示与隐藏。
35
+
36
+ Args:
37
+ obj: 对象类型
38
+ * ``bc`` / ``boundary`` = 边界
39
+ * ``lg`` / ``load`` = 荷载
40
+ * ``td`` / ``tendon`` = 钢束
41
+ type: 子类型(``all`` 或具体类型,见 ``osis_disp_ctrl`` 文档)
42
+ ids: 编号或名称(``all`` / 单个 / 序列)
43
+ show: ``0`` = 隐藏,``1`` = 显示
44
+
45
+ Raises:
46
+ RuntimeError: 操作失败时抛出异常
47
+
48
+ Examples:
49
+ >>> display_manager.disp_ctrl("bc", "all", "all", 1)
50
+ >>> display_manager.disp_ctrl("bc", "general", [1, 2, "3to10"], 1)
51
+ >>> display_manager.disp_ctrl("lg", "nforce", "all", 1)
52
+ >>> display_manager.disp_ctrl("td", "all", ["T1", "T2"], 1)
53
+ """
54
+ ok, err = osis_disp_ctrl(obj, type, ids, show)
55
+ if not ok:
56
+ raise RuntimeError(f"显示控制失败: {err}")
57
+
58
+ def control(self, action: str, arg: str | None = None) -> None:
59
+ """视图 / 界面控制(对应 ``/control,...``)。
60
+
61
+ Args:
62
+ action: 子命令,如 ``view``、``quickCreateModel``
63
+ arg: 子命令参数;``view`` 时为 ``standard`` / ``top`` / ``right`` / ``front``
64
+
65
+ Raises:
66
+ RuntimeError: 操作失败时抛出异常
67
+ """
68
+ ok, err = osis_control(action, arg)
69
+ if not ok:
70
+ raise RuntimeError(f"界面控制失败: {err}")
71
+
72
+ def set_view(
73
+ self,
74
+ view: Literal["standard", "top", "right", "front"] = "standard",
75
+ ) -> None:
76
+ """切换视图方向。
77
+
78
+ Args:
79
+ view: ``standard`` / ``top`` / ``right`` / ``front``
80
+
81
+ Raises:
82
+ RuntimeError: 操作失败时抛出异常
83
+
84
+ Examples:
85
+ >>> display_manager.set_view("top")
86
+ >>> display_manager.set_view("front")
87
+ """
88
+ self.control("view", view)
89
+
90
+ def set_plsm(self, enabled: Literal[0, 1] = 1) -> None:
91
+ """显示开关(Plsm)。
92
+
93
+ Args:
94
+ enabled: ``0`` = 关,``1`` = 开
95
+
96
+ Raises:
97
+ RuntimeError: 操作失败时抛出异常
98
+
99
+ Examples:
100
+ >>> display_manager.set_plsm(1)
101
+ >>> display_manager.set_plsm(0)
102
+ """
103
+ ok, err = osis_plsm(enabled)
104
+ if not ok:
105
+ raise RuntimeError(f"设置 Plsm 失败: {err}")
106
+
107
+ def __repr__(self) -> str:
108
+ return "DisplayManager()"
109
+
110
+
111
+ # ═════════════════════════════════════════════
112
+ # 全局单例
113
+ # ═════════════════════════════════════════════
114
+
115
+ display_manager = DisplayManager()
pyosis/element/manager.py CHANGED
@@ -298,6 +298,15 @@ class ElementGroupManager:
298
298
  Args:
299
299
  name: 单元组名称
300
300
  op: 操作
301
+ * c = 创建
302
+ * a = 添加
303
+ * s = 替换
304
+ * r = 移除
305
+ * aa = 添加全部
306
+ * ra = 移除全部
307
+ * m = 修改组名
308
+ * d = 删除
309
+ *param (str | int): 待操作的编号,支持的格式:*,*to*;*by*,仅用于替换。例子:2,3,5,8to10,2by3,5by6,8by10 重合的编号自动忽略
301
310
 
302
311
  Returns:
303
312
  ElementGroup: 创建的单元组对象
@@ -43,7 +43,8 @@ def save_project(filePath) -> Tuple[bool, str]:
43
43
  保存项目
44
44
 
45
45
  Args:
46
- filepath: 为空或者当前工程文件路径
46
+ filePath: 工程文件路径。为空时由上层补全为当前工程路径;
47
+ 非空时使用该路径。
47
48
 
48
49
  Returns:
49
50
  Tuple[bool, str]
@@ -62,3 +63,13 @@ def save_project_as(filePath) -> Tuple[bool, str]:
62
63
  Tuple[bool, str]
63
64
  """
64
65
  pass
66
+
67
+ @REGISTRY.register("/Close")
68
+ def close_osis() -> Tuple[bool, str]:
69
+ """
70
+ 关闭 OSIS 项目
71
+
72
+ Returns:
73
+ Tuple[bool, str]
74
+ """
75
+ pass
pyosis/project/manager.py CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  用法:
4
4
  >>> from pyosis.project import project_manager
5
- >>> project_manager.create(type=1, filepath="D:/Models/bridge.osis")
6
- >>> project_manager.open("D:/Models/bridge.osis")
5
+ >>> project_manager.create(type=1, filepath="D:/Models/bridge.sis")
6
+ >>> project_manager.open("D:/Models/bridge.sis")
7
7
  >>> project_manager.save()
8
- >>> project_manager.save_as("D:/Models/bridge_v2.osis")
8
+ >>> project_manager.save("D:/Models/bridge.sis")
9
+ >>> project_manager.save_as("D:/Models/bridge_v2.sis")
10
+ >>> project_manager.close()
9
11
  >>> project_manager.get_directory()
10
12
  """
11
13
 
@@ -16,6 +18,7 @@ from .interface import (
16
18
  open_project,
17
19
  save_project,
18
20
  save_project_as,
21
+ close_osis,
19
22
  get_project_directory,
20
23
  )
21
24
 
@@ -53,13 +56,19 @@ class ProjectManager:
53
56
  if not ok:
54
57
  raise RuntimeError(f"打开项目失败: {err}")
55
58
 
56
- def save(self) -> None:
57
- """保存当前项目
59
+ def save(self, filepath: str = "") -> None:
60
+ """保存项目
61
+
62
+ Args:
63
+ filepath: 工程文件路径。为空时使用当前项目路径;
64
+ 非空时使用传入路径。
58
65
 
59
66
  Raises:
60
67
  RuntimeError: 保存失败时抛出异常
61
68
  """
62
- ok, err = save_project("")
69
+ if not filepath:
70
+ filepath = self._current_project_file()
71
+ ok, err = save_project(filepath)
63
72
  if not ok:
64
73
  raise RuntimeError(f"保存项目失败: {err}")
65
74
 
@@ -76,6 +85,16 @@ class ProjectManager:
76
85
  if not ok:
77
86
  raise RuntimeError(f"另存为失败: {err}")
78
87
 
88
+ def close(self) -> None:
89
+ """关闭 OSIS 项目
90
+
91
+ Raises:
92
+ RuntimeError: 关闭失败时抛出异常
93
+ """
94
+ ok, err = close_osis()
95
+ if not ok:
96
+ raise RuntimeError(f"关闭软件失败: {err}")
97
+
79
98
  def get_directory(self) -> str:
80
99
  """获取当前项目目录
81
100
 
@@ -90,6 +109,11 @@ class ProjectManager:
90
109
  except Exception as e:
91
110
  raise RuntimeError(f"获取项目目录失败: {e}")
92
111
 
112
+ def _current_project_file(self) -> str:
113
+ """由当前项目目录推导工程文件路径(目录同名 .sis)。"""
114
+ directory = self.get_directory().rstrip("\\/")
115
+ return f"{directory}.sis"
116
+
93
117
  def __repr__(self) -> str:
94
118
  return "ProjectManager()"
95
119
 
@@ -1,21 +1,53 @@
1
1
  """transfer 同步链路的纯文本处理工具:把 .out 文本转换为 pyosis prep 模块。
2
2
 
3
- 只负责文本解析与 Python 代码生成:
4
- - 解析 OSIS 导出的命令流(parse_text)
5
- - 按模块分桶并生成 prep 模块(_1_control.py … _10_stage.py + main.py)
3
+ 只负责文本解析与 Python 代码生成:
4
+ - 读 .out 源文件
5
+ - 用 parser 解析出命令流,并按 //--- MODULE_NAME --- 标记分桶
6
+ - 第一个模块标记之前的"前言"命令(典型为 clear/clc)写到 main.py
7
+ - 每个模块的命令写到 _1_control.py … _10_stage.py
6
8
 
7
- 不负责:
9
+ 不负责:
8
10
  - .out 文件的获取(export_apdl) —— 由调用方管理
9
- - prep 模块的执行(import + builder 调用) —— 由调用方管理
11
+ - prep 模块的执行(import + builder 调用) —— 由 main.py 调度
10
12
  """
11
13
 
12
14
  from __future__ import annotations
13
15
 
16
+ import inspect
17
+ import re
18
+ import sys
14
19
  from collections import defaultdict
15
20
  from pathlib import Path
21
+ from typing import Iterable
16
22
 
17
23
  from pyosis.transfer.generator import generate_lines
18
- from pyosis.transfer.parser import ParsedCommand
24
+ from pyosis.transfer.parser import (
25
+ BLANK_RE,
26
+ COMMENT_LINE_RE,
27
+ ParsedCommand,
28
+ _parse_physical_line,
29
+ iter_physical_lines,
30
+ strip_inline_comment,
31
+ )
32
+
33
+ # 匹配链式调用中的 .get(...) 部分(如 engine.section.get("k").set_offset(...))
34
+ _CHAIN_GET_RE = re.compile(r"""\.get\((?:"[^"]*"|'[^']*'|[^()]*)\)""")
35
+
36
+ # .out 中模块标记行的正则: //----- CONTROL ----- 之类
37
+ MODULE_PATTERN = re.compile(r"^\s*//-+\s*(\w+)\s*-*")
38
+ # 已知的模块名(用于过滤 .out 中的非模块横线注释,例如 //--- VERSION: x.xx ---//)
39
+ KNOWN_MODULES = frozenset({
40
+ "CONTROL",
41
+ "PROPERTY",
42
+ "MATERIAL",
43
+ "SECTION",
44
+ "NODE",
45
+ "ELEMENT",
46
+ "BOUNDARY",
47
+ "LOADCASE",
48
+ "ANALYSIS",
49
+ "STAGE",
50
+ })
19
51
 
20
52
  # 模块文件名
21
53
  MODULE_FILES = {
@@ -47,28 +79,6 @@ MODULE_BUILDERS = {
47
79
 
48
80
  MODULE_ORDER = tuple(MODULE_FILES.keys())
49
81
 
50
-
51
- # 按模块分桶
52
- def _bucket_by_module(parsed: list[ParsedCommand]) -> dict[str, list[ParsedCommand]]:
53
- buckets: dict[str, list[ParsedCommand]] = defaultdict(list)
54
- for cmd in parsed:
55
- buckets[cmd.module or "PREAMBLE"].append(cmd)
56
- return buckets
57
-
58
-
59
- # 合并 preamble 到 control
60
- def _merge_preamble_into_control(
61
- buckets: dict[str, list[ParsedCommand]],
62
- ) -> dict[str, list[ParsedCommand]]:
63
- if "PREAMBLE" not in buckets:
64
- return buckets
65
- merged = dict(buckets)
66
- preamble = merged.pop("PREAMBLE")
67
- merged.setdefault("CONTROL", [])
68
- merged["CONTROL"] = preamble + merged["CONTROL"]
69
- return merged
70
-
71
-
72
82
  # 模块职责一句话(给 AI 看的提示)
73
83
  MODULE_PURPOSE = {
74
84
  "CONTROL": "全局控制参数(重力、非线性、收缩徐变开关等)",
@@ -84,51 +94,199 @@ MODULE_PURPOSE = {
84
94
  }
85
95
 
86
96
 
87
- # 写 prep 模块时,按调用特征(method + 第一个 string 参数)分段,加注释
88
- def _group_key(line: str) -> str | None:
89
- """提取一行的"分组键":method 链 + 第一个 string 参数。
90
-
91
- 例:
92
- engine.load.get("防撞护栏右").create("LINE", ...) -> load.get+防撞护栏右
93
- engine.element.group.create("0号块单元", ...) -> element.group.create+0号块单元
94
- engine.element.create(1, "BEAM3D", ...) -> element.create+BEAM3D
95
- engine.control.set_gravity_acceleration(9.8) -> control.set_gravity_acceleration+-
97
+ # 按模块分桶
98
+ def _split_by_module(text: str) -> tuple[list[ParsedCommand], dict[str, list[ParsedCommand]]]:
99
+ """走一遍 .out 文本,把命令按 //--- MODULE_NAME --- 标记分桶。
96
100
 
97
- 无特征(如赋值)返 None。
101
+ Returns:
102
+ (preamble, {MODULE: [cmd, ...]})
103
+ - preamble: 第一个模块标记之前的命令(典型为 clear/clc)
104
+ - by_module: 每个模块的命令列表,按 .out 出现顺序
98
105
  """
99
- s = line.strip()
100
- # 找第一个 '('
101
- i = s.find("(")
102
- if i < 0:
103
- return None
104
- # method 链 = 整行直到 "("
105
- method = s[:i]
106
- # 第一个 string 参数
107
- rest = s[i + 1:].lstrip()
108
- if not rest.startswith('"'):
109
- return None
110
- j = rest.find('"', 1)
111
- if j < 0:
106
+ preamble: list[ParsedCommand] = []
107
+ by_module: dict[str, list[ParsedCommand]] = defaultdict(list)
108
+ current: str | None = None
109
+
110
+ for line in iter_physical_lines(text):
111
+ m = MODULE_PATTERN.match(line)
112
+ if m:
113
+ name = m.group(1).upper()
114
+ current = name if name in KNOWN_MODULES else None
115
+ continue
116
+ if COMMENT_LINE_RE.match(line) or BLANK_RE.match(line):
117
+ continue
118
+ line = strip_inline_comment(line)
119
+ if BLANK_RE.match(line):
120
+ continue
121
+ cmds = _parse_physical_line(line)
122
+ if current is None:
123
+ preamble.extend(cmds)
124
+ else:
125
+ by_module[current].extend(cmds)
126
+
127
+ return preamble, dict(by_module)
128
+
129
+
130
+ # 读取 .out 文件内容(容错编码)
131
+ def _read_out_text(path: Path) -> str:
132
+ for encoding in ("gbk", "utf-8", "gb2312", "gb18030", "latin-1"):
133
+ try:
134
+ return path.read_text(encoding=encoding)
135
+ except UnicodeDecodeError:
136
+ continue
137
+ raise RuntimeError(f"无法解码 .out 文件: {path}")
138
+
139
+
140
+ # ──────────────────────────────────────────────────────────────────────
141
+ # 方法 docstring 注释:在每个被调用方法的第一次出现处,插入其 docstring 第一行
142
+ # ──────────────────────────────────────────────────────────────────────
143
+
144
+ _ENGINE_INSTANCE: object | None = None
145
+
146
+
147
+ def _get_engine() -> object:
148
+ """懒加载一个 OSISEngine 实例,只读属性,不会真正连 OSIS。"""
149
+ global _ENGINE_INSTANCE
150
+ if _ENGINE_INSTANCE is None:
151
+ from pyosis.core.engine import OSISEngine
152
+ _ENGINE_INSTANCE = OSISEngine()
153
+ return _ENGINE_INSTANCE
154
+
155
+
156
+ def _strip_outer_args(line: str) -> str:
157
+ """剥离最外层调用的实参列表,返回接收者表达式。
158
+
159
+ 取的是最后一个顶层 `(`,这样链式调用也能保留中间的 .get(...)。
160
+ engine.control.set_gravity_acceleration(9.8) → engine.control.set_gravity_acceleration
161
+ engine.section.get(1).set_offset("Middle", 0) → engine.section.get(1).set_offset
162
+ engine.matrix("M", [[1]]) → engine.matrix
163
+ """
164
+ depth = 0
165
+ last_paren = -1
166
+ in_str = False
167
+ sc: str | None = None
168
+ for i, ch in enumerate(line):
169
+ if in_str:
170
+ if ch == sc and (i == 0 or line[i - 1] != "\\"):
171
+ in_str = False
172
+ continue
173
+ if ch in ('"', "'"):
174
+ in_str = True
175
+ sc = ch
176
+ continue
177
+ if ch == "(":
178
+ if depth == 0:
179
+ last_paren = i
180
+ depth += 1
181
+ elif ch == ")":
182
+ depth -= 1
183
+ return line[:last_paren] if last_paren >= 0 else line
184
+
185
+
186
+ def _doc_first_line(doc: str | None) -> str:
187
+ if not doc:
188
+ return ""
189
+ return doc.strip().split("\n", 1)[0].strip()
190
+
191
+
192
+ def _resolve_chain_target(head: str, engine: object) -> object | None:
193
+ """从 'engine.X.method'(已折叠 .get)解析出真正的可调用对象。
194
+
195
+ 1) 直接 getattr 链:engine.section.create → SectionManager.create;engine.matrix → OSISEngine.matrix
196
+ 2) 链式调用:head 折叠后是 engine.section.set_offset(或嵌套的 engine.live.case.include),
197
+ 中间某一层管理器没有这个属性。从该层往前找带 .get 的管理器,用其返回类型注解
198
+ (Section / LiveCase / TendonShape 等)找到方法本体。
199
+ """
200
+ parts = head.split(".")
201
+ if len(parts) < 2 or parts[0] != "engine":
112
202
  return None
113
- return f"{method}|{rest[1:j]}"
114
-
115
-
116
- def _insert_group_comments(lines: list[str]) -> list[str]:
117
- """在 group key 变化处插入 `# ---- <method>: <name> ----` 注释。
118
203
 
119
- 输入 lines 假设跟 generator 输出对齐(**每行无缩进**,已有 `engine.xxx` 前缀)。
120
- 注释也保持无缩进,跟代码一致;最终由 `_write_prep_module` 统一加 def body 缩进。
121
- """
204
+ # 1) 直接 getattr 链
205
+ try:
206
+ obj = engine
207
+ for p in parts[1:]:
208
+ obj = getattr(obj, p)
209
+ if callable(obj):
210
+ return obj
211
+ except AttributeError:
212
+ pass
213
+
214
+ if len(parts) < 3:
215
+ return None
216
+ method_name = parts[-1]
217
+
218
+ # 2) 从尾往头试每一层,看它是不是一个带 .get 的管理器
219
+ for cut in range(len(parts) - 1, 1, -1):
220
+ try:
221
+ mgr_obj = engine
222
+ for p in parts[1:cut]:
223
+ mgr_obj = getattr(mgr_obj, p)
224
+ except AttributeError:
225
+ continue
226
+ get_method = type(mgr_obj).__dict__.get("get")
227
+ if get_method is None:
228
+ continue
229
+ try:
230
+ sig = inspect.signature(get_method)
231
+ except (ValueError, TypeError):
232
+ continue
233
+ ann = sig.return_annotation
234
+ if ann is inspect.Parameter.empty:
235
+ continue
236
+ if isinstance(ann, str):
237
+ mod = sys.modules.get(get_method.__module__)
238
+ if mod is None:
239
+ continue
240
+ try:
241
+ ann = eval(ann, vars(mod))
242
+ except Exception:
243
+ continue
244
+ candidates: list[type] = []
245
+ if isinstance(ann, type):
246
+ candidates.append(ann)
247
+ else:
248
+ for arg in getattr(ann, "__args__", ()):
249
+ if isinstance(arg, type):
250
+ candidates.append(arg)
251
+ else:
252
+ for inner in getattr(arg, "__args__", ()):
253
+ if isinstance(inner, type):
254
+ candidates.append(inner)
255
+ for cand in candidates:
256
+ target = vars(cand).get(method_name)
257
+ if target is not None and callable(target):
258
+ return target
259
+ return None
260
+
261
+
262
+ def _method_doc_for(line: str) -> str:
263
+ """取这一行生成代码所调用方法的 docstring 第一行。非 engine 调用返空串。"""
264
+ s = line.strip()
265
+ if not s.startswith("engine."):
266
+ return ""
267
+ head = _CHAIN_GET_RE.sub("", _strip_outer_args(s))
268
+ # head 形如 'engine.section.set_offset' 或 'engine.control.set_gravity_acceleration'
269
+ target = _resolve_chain_target(head, _get_engine())
270
+ if target is None:
271
+ return ""
272
+ return _doc_first_line(getattr(target, "__doc__", None))
273
+
274
+
275
+ def _insert_method_doc_comments(lines: list[str]) -> list[str]:
276
+ """在每个被调用的独特方法的第一次出现处,插入一行 # <docstring 第一行>。"""
277
+ seen: set[str] = set()
122
278
  out: list[str] = []
123
- prev_key: str | None = None
124
279
  for line in lines:
125
- key = _group_key(line)
126
- if key is not None and key != prev_key:
127
- method, name = key.split("|", 1)
128
- short = method.rsplit(".", 1)[-1] # 最后一个方法名
129
- out.append(f"# ---- {short}: {name} ----")
280
+ s = line.strip()
281
+ if s.startswith("engine."):
282
+ # 用与解析时相同的 head 作为去重键,保证多次调用同一方法只注一次
283
+ head = _CHAIN_GET_RE.sub("", _strip_outer_args(s))
284
+ if head not in seen:
285
+ doc = _method_doc_for(line)
286
+ if doc:
287
+ out.append(f"# {doc}")
288
+ seen.add(head)
130
289
  out.append(line)
131
- prev_key = key if key is not None else prev_key
132
290
  return out
133
291
 
134
292
 
@@ -139,7 +297,7 @@ def _write_prep_module(prep_dir: Path, module: str, lines: list[str]) -> Path:
139
297
  生成顺序:
140
298
  1. docstring(说明模块职责)
141
299
  2. imports
142
- 3. builder 函数(按 group 加注释)
300
+ 3. builder 函数
143
301
  4. if __name__ == "__main__": 单跑入口
144
302
  """
145
303
  fname = MODULE_FILES[module]
@@ -151,12 +309,8 @@ def _write_prep_module(prep_dir: Path, module: str, lines: list[str]) -> Path:
151
309
  doc = f"OSIS 命令流 {module} 模块 — 当前 .out 中无该段,无需调用。"
152
310
  fn_body = [" pass"]
153
311
  else:
154
- doc = (
155
- f"OSIS 命令流 {module} 模块 — {purpose}\n\n"
156
- f"由 pyosis.transfer.out_to_python 从 .out 自动生成。"
157
- f"按调用特征分组(注释头 # ---- method: name ----),同组相邻命令共享同一上下文。"
158
- )
159
- annotated = _insert_group_comments(lines)
312
+ doc = f"OSIS 命令流 {module} 模块 — {purpose}"
313
+ annotated = _insert_method_doc_comments(lines)
160
314
  fn_body = [f" {line}" for line in annotated]
161
315
 
162
316
  body_lines = [
@@ -179,20 +333,21 @@ def _write_prep_module(prep_dir: Path, module: str, lines: list[str]) -> Path:
179
333
  return path
180
334
 
181
335
 
182
- def _write_main_py(prep_dir: Path) -> Path:
183
- """写入 main.py:依次执行 _1_control … _10_stage。"""
184
- import_lines = [
185
- f"from {Path(fname).stem} import {MODULE_BUILDERS[mod]}"
186
- for mod in MODULE_ORDER
187
- for fname in [MODULE_FILES[mod]]
188
- ]
189
- call_lines = [f" {MODULE_BUILDERS[mod]}(eng)" for mod in MODULE_ORDER]
336
+ def _iter_module_call_lines() -> Iterable[str]:
337
+ """main.py 中依次调用各模块 builder 的语句(4 空格缩进)。"""
338
+ for mod in MODULE_ORDER:
339
+ yield f" {MODULE_BUILDERS[mod]}(engine)"
340
+
341
+
342
+ def _write_main_py(prep_dir: Path, preamble_lines: list[str]) -> Path:
343
+ """写入 main.py:先执行 preamble(典型为 clear/clc),再依次执行 _1.._10 builder。"""
344
+ import_lines = [f"from {Path(MODULE_FILES[mod]).stem} import {MODULE_BUILDERS[mod]}" for mod in MODULE_ORDER]
190
345
 
191
346
  body_lines = [
192
347
  '"""main.py — 入口脚本,按 _1.._10 顺序依次执行 prep 模块。',
193
348
  "",
194
- "可单独跑(`python main.py`),也可被 import 后调 main(engine)。",
195
- "默认使用 _0_engine 模块级单例 OSISEngine,也可注入外部 engine。",
349
+ "可直接 `python main.py`,或被 import 后调 main()。",
350
+ "engine 用 _0_engine 模块级单例,跟同包内的 prep 模块保持一致。",
196
351
  '"""',
197
352
  "",
198
353
  "from __future__ import annotations",
@@ -203,37 +358,51 @@ def _write_main_py(prep_dir: Path) -> Path:
203
358
  "# 让 main.py 不管从哪个目录跑都能 import 同目录的 prep modules",
204
359
  "sys.path.insert(0, str(Path(__file__).resolve().parent))",
205
360
  "",
206
- "from _0_engine import engine as default_engine",
361
+ "from _0_engine import engine",
207
362
  *import_lines,
208
363
  "",
209
- "def main(engine=None) -> None:",
210
- " eng = default_engine if engine is None else engine",
211
- *call_lines,
364
+ "def main() -> None:",
365
+ ]
366
+ for line in preamble_lines:
367
+ body_lines.append(f" {line}")
368
+ body_lines.append("")
369
+ body_lines.extend(_iter_module_call_lines())
370
+ body_lines.extend([
212
371
  "",
213
372
  'if __name__ == "__main__":',
214
373
  " main()",
215
374
  "",
216
- ]
375
+ ])
376
+
217
377
  path = prep_dir / "main.py"
218
378
  path.write_text("\n".join(body_lines), encoding="utf-8")
219
379
  return path
220
380
 
221
381
 
222
- def write_prep_outputs(parsed: list[ParsedCommand], prep_dir: Path) -> tuple[int, list[Path]]:
223
- """写入 _0_engine.py 与 _1~_10 prep 模块 + main.py,返回 (代码行数, 文件路径列表)。
382
+ def write_prep_outputs(out_path: Path, prep_dir: Path) -> tuple[int, list[Path]]:
383
+ """读 .out 源文件,生成 _0_engine.py + _1~_10 prep 模块 + main.py,返回 (代码行数, 文件路径列表)。
224
384
 
225
- 每个标准模块都会写入文件;.out 中无对应命令时生成 pass stub,避免遗留旧 prep。
385
+ 每个标准模块都会写入文件;.out 中无对应命令时生成 pass stub,避免遗留旧 prep。
386
+
387
+ Args:
388
+ out_path: 源 .out 文件路径(由 apdl_sync 管理获取,这里只读取)
389
+ prep_dir: prep 输出目录(必须已存在)
226
390
  """
391
+ text = _read_out_text(out_path)
392
+ preamble, by_module = _split_by_module(text)
393
+
227
394
  code_line_count = 0
228
395
  prep_paths: list[Path] = []
229
- buckets = _merge_preamble_into_control(_bucket_by_module(parsed))
230
396
 
231
397
  for module in MODULE_ORDER:
232
- cmds = buckets.get(module, [])
398
+ cmds = by_module.get(module, [])
233
399
  lines = generate_lines(cmds) if cmds else []
234
400
  code_line_count += len(lines)
235
401
  prep_paths.append(_write_prep_module(prep_dir, module, lines))
236
402
 
403
+ # 前言(典型 clear/clc)放到 main.py 顶部;无前言时为空列表
404
+ preamble_lines = generate_lines(preamble) if preamble else []
405
+
237
406
  (prep_dir / "_0_engine.py").write_text(
238
407
  '"""模块级单例 OSISEngine,供同包内其他 prep 模块 import 使用。\n\n'
239
408
  "不要在主代码里直接 new OSISEngine();通过 main.py 调度。\n"
@@ -242,5 +411,5 @@ def write_prep_outputs(parsed: list[ParsedCommand], prep_dir: Path) -> tuple[int
242
411
  "engine = OSISEngine()\n",
243
412
  encoding="utf-8",
244
413
  )
245
- prep_paths.append(_write_main_py(prep_dir))
246
- return code_line_count, prep_paths
414
+ prep_paths.append(_write_main_py(prep_dir, preamble_lines))
415
+ return code_line_count, prep_paths
pyosis/transfer/parser.py CHANGED
@@ -1,5 +1,9 @@
1
1
  """命令流解析器.
2
2
 
3
+ 职责:把输入的一整段 OSIS 命令流文本解析成多条 ParsedCommand.
4
+ 不做任何 .out 格式相关的事(比如识别 //--- CONTROL --- 这类模块标记),
5
+ 那是 out_to_python 的职责.
6
+
3
7
  支持:
4
8
  - 行注释(//、#)
5
9
  - 多行续行(行末逗号 / 下一行首字符为空白+逗号)
@@ -13,7 +17,7 @@ from __future__ import annotations
13
17
 
14
18
  import re
15
19
  from dataclasses import dataclass
16
- from typing import List
20
+ from typing import Iterator, List
17
21
 
18
22
  from .split import split_cmd, split_commands
19
23
 
@@ -23,24 +27,10 @@ _GLUED_TAIL_COMMANDS = ("SectionOffset",)
23
27
  # 矩阵赋值命令正则表达式
24
28
  MATRIX_ASSIGN_RE = re.compile(r"^(\w+)\[([\d,\s]+)\]\s*=\s*(.+)$")
25
29
  # 注释行正则表达式
26
- _COMMENT_LINE_RE = re.compile(r"^\s*(//|#)")
30
+ COMMENT_LINE_RE = re.compile(r"^\s*(//|#)")
27
31
  # 空白行正则表达式
28
- _BLANK_RE = re.compile(r"^\s*$")
29
- # 模块标记正则表达式
30
- MODULE_PATTERN = re.compile(r"//-+\s*(\w+)\s*-*")
31
- # 用于识别命令所属模块
32
- KNOWN_MODULES = frozenset({
33
- "CONTROL",
34
- "PROPERTY",
35
- "MATERIAL",
36
- "SECTION",
37
- "NODE",
38
- "ELEMENT",
39
- "BOUNDARY",
40
- "LOADCASE",
41
- "ANALYSIS",
42
- "STAGE",
43
- })
32
+ BLANK_RE = re.compile(r"^\s*$")
33
+
44
34
 
45
35
  # 已解析的 OSIS 命令
46
36
  @dataclass
@@ -54,7 +44,6 @@ class ParsedCommand:
54
44
  matrix_name: str = ""
55
45
  matrix_indices: tuple[int, ...] = ()
56
46
  matrix_value: str = ""
57
- module: str | None = None # "CONTROL" | "SECTION" | ... | None
58
47
 
59
48
 
60
49
  def _join_continuation_lines(lines: List[str]) -> List[str]:
@@ -117,8 +106,33 @@ def _split_glued_command_fields(fields: List[str]) -> List[List[str]]:
117
106
  return groups
118
107
 
119
108
 
109
+ # 去除一行内的 // 注释(保留 // 之前的有效命令部分)
110
+ def strip_inline_comment(line: str) -> str:
111
+ if "//" not in line:
112
+ return line
113
+ pos = line.find("//")
114
+ if pos == 0:
115
+ return ""
116
+ if line[pos - 1] in (" ", "\t"):
117
+ return line[:pos].rstrip()
118
+ return line
119
+
120
+
121
+ # 迭代"物理命令行":合并续行;不过滤注释/空行,留给调用方决定
122
+ def iter_physical_lines(text: str) -> Iterator[str]:
123
+ """逐行 yield 物理命令行(已合并续行)。
124
+
125
+ 不做任何注释/空行过滤——调用方按需处理:
126
+ - parse_text 会跳过 //、#、空行(以及 //--- MODULE_NAME --- 这种模块标记);
127
+ 因为 parser 不识别模块,所以这条规则保持宽泛。
128
+ - out_to_python._split_by_module 同样跳过注释/空行,但用 MODULE_PATTERN
129
+ 提前捕获模块标记以切换 current_module。
130
+ """
131
+ yield from _join_continuation_lines(text.splitlines())
132
+
133
+
120
134
  # 创建已解析的命令
121
- def _make_parsed_command(fields: List[str], source: str, module: str | None) -> ParsedCommand:
135
+ def _make_parsed_command(fields: List[str], source: str) -> ParsedCommand:
122
136
  first = fields[0] if fields else ""
123
137
  return ParsedCommand(
124
138
  raw=source,
@@ -126,11 +140,11 @@ def _make_parsed_command(fields: List[str], source: str, module: str | None) ->
126
140
  name=first,
127
141
  source=source,
128
142
  kind="normal",
129
- module=module,
130
143
  )
131
144
 
145
+
132
146
  # 解析一条命令,返回已解析的命令列表
133
- def _parse_one_command(source: str, module:str|None) -> List[ParsedCommand]:
147
+ def _parse_one_command(source: str) -> List[ParsedCommand]:
134
148
  fields = split_cmd(source)
135
149
  first = fields[0] if fields else ""
136
150
  lower_first = first.lower()
@@ -143,7 +157,6 @@ def _parse_one_command(source: str, module:str|None) -> List[ParsedCommand]:
143
157
  name=first,
144
158
  source=source,
145
159
  kind="matrix_dim",
146
- module=module,
147
160
  )
148
161
  ]
149
162
 
@@ -161,7 +174,6 @@ def _parse_one_command(source: str, module:str|None) -> List[ParsedCommand]:
161
174
  matrix_name=assign_match.group(1),
162
175
  matrix_indices=indices,
163
176
  matrix_value=assign_match.group(3).strip(),
164
- module=module,
165
177
  )
166
178
  ]
167
179
 
@@ -171,46 +183,34 @@ def _parse_one_command(source: str, module:str|None) -> List[ParsedCommand]:
171
183
  if not group:
172
184
  continue
173
185
  sub_source = ",".join(group)
174
- result.append(_make_parsed_command(group, sub_source, module=module))
186
+ result.append(_make_parsed_command(group, sub_source))
175
187
  return result
176
188
 
177
189
 
190
+ # 解析一行物理命令为多条 ParsedCommand(处理 ; 分隔)
191
+ def _parse_physical_line(line: str) -> List[ParsedCommand]:
192
+ out: List[ParsedCommand] = []
193
+ for sub in split_commands(line):
194
+ out.extend(_parse_one_command(sub))
195
+ return out
196
+
197
+
178
198
  # 解析 OSIS 命令流文本,返回已解析的命令列表
179
199
  def parse_text(text: str) -> List[ParsedCommand]:
180
200
  """解析 OSIS 命令流文本。
181
201
 
182
202
  Returns:
183
203
  ParsedCommand 列表(不含注释、空行)。
204
+ 不识别 //--- MODULE_NAME --- 这类 .out 模块标记,那是 out_to_python 的职责。
184
205
  """
185
- lines = text.splitlines()
186
- physical_lines = _join_continuation_lines(lines)
187
- current_module: str | None = None
188
206
  commands: List[ParsedCommand] = []
189
-
190
- for physical in physical_lines:
191
- stripped = physical.strip()
192
- # 识别模块标记
193
- m = MODULE_PATTERN.match(stripped)
194
- if m:
195
- name = m.group(1).upper()
196
- current_module = name if name in KNOWN_MODULES else None
207
+ for line in iter_physical_lines(text):
208
+ if COMMENT_LINE_RE.match(line):
197
209
  continue
198
-
199
- if _COMMENT_LINE_RE.match(physical):
200
- continue
201
- if _BLANK_RE.match(physical):
210
+ if BLANK_RE.match(line):
202
211
  continue
203
- if "//" in physical:
204
- hash_pos = physical.find("//")
205
- if hash_pos == 0:
206
- continue
207
- if hash_pos > 0 and physical[hash_pos - 1] in (" ", "\t"):
208
- physical = physical[:hash_pos].rstrip()
209
- if not physical:
212
+ line = strip_inline_comment(line)
213
+ if BLANK_RE.match(line):
210
214
  continue
211
-
212
- for sub in split_commands(physical):
213
- for cmd in _parse_one_command(sub, module=current_module):
214
- commands.append(cmd)
215
-
216
- return commands
215
+ commands.extend(_parse_physical_line(line))
216
+ return commands
pyosis/transfer/routes.py CHANGED
@@ -173,6 +173,12 @@ ROUTES = {
173
173
  "/Open": "engine.open_project",
174
174
  "/Save": "engine.save_project",
175
175
  "/SaveAs": "engine.project.save_as",
176
+ "/Close": "engine.close",
177
+
178
+ # ─── DISPLAY ───
179
+ "DispCtrl": "engine.display.disp_ctrl",
180
+ "/control": "engine.display.control",
181
+ "Plsm": "engine.display.set_plsm",
176
182
 
177
183
  # ─── GENERAL ───
178
184
  "Clear": "engine.clear",