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,58 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 示例1:带孔板(拉伸 + 圆孔贯穿切除 + 圆角 + 倒角)
4
+ ====================================================
5
+ 设计需求: 120×80×10mm 底座板,中心 D20 通孔,四条竖直棱边 R5 圆角,
6
+ 顶面外边 C1 倒角。
7
+
8
+ 执行: python sw_bridge.py run examples\build_plate.py
9
+ (或在 DSH 里说:"用 SolidWorks 建模一个 120×80×10 的板,中心直径 20 的
10
+ 通孔,四条竖直棱边倒圆角 R5,保存为 DSH_带孔板")
11
+
12
+ 本示例完全通用:不依赖任何本机路径,保存到脚本所在目录。
13
+ """
14
+ import os
15
+
16
+ # sw_bridge 的 run 会把包目录加入 sys.path,因此可 import swapi
17
+ import swapi
18
+
19
+ # ---- 1. 新建零件(自动探测模板)----
20
+ m = swapi.new_part()
21
+ print("STEP1 new part:", m.title)
22
+
23
+ # ---- 2. 前视基准面画 120x80 矩形,拉伸 10mm ----
24
+ m.begin_sketch("Front Plane")
25
+ m.rect(0, 0, 120, 80)
26
+ m.end_sketch()
27
+ m.extrude(10)
28
+ print("STEP2 extrude 120x80x10 OK")
29
+
30
+ # ---- 3. 顶面 (z=10mm) 画 D20 圆,完全贯穿切除 ----
31
+ m.begin_sketch_on_face(0, 0, 10)
32
+ m.circle(0, 0, 10)
33
+ m.end_sketch()
34
+ m.cut(through=True)
35
+ print("STEP3 cut D20 through OK")
36
+
37
+ # ---- 4. 四条竖直棱边倒圆角 R5 ----
38
+ # 竖直棱边: x=±60, y=±40, z 从 0 到 10;取棱边中点 z=5
39
+ corners = [
40
+ (60, 40, 5), (-60, 40, 5), (-60, -40, 5), (60, -40, 5),
41
+ ]
42
+ m.clear_selection()
43
+ r = m.fillet(5, corners)
44
+ print("STEP4 fillet R5 ->", "OK" if r is not None else "FAIL")
45
+
46
+ # ---- 5. 顶面四条外边倒角 C1 ----
47
+ # 顶面边: z=10 处的四条边,取各边中点
48
+ top_edges = [
49
+ (0, 40, 10), (0, -40, 10), (60, 0, 10), (-60, 0, 10),
50
+ ]
51
+ m.clear_selection()
52
+ c = m.chamfer(1, top_edges, angle_deg=45)
53
+ print("STEP5 chamfer C1 ->", "OK" if c is not None else "FAIL")
54
+
55
+ # ---- 6. 保存(不校验;用户约定:建模后跳过 massprops 检验)----
56
+ out = os.path.join(os.path.dirname(os.path.abspath(__file__)),
57
+ "DSH_带孔板.sldprt")
58
+ print("STEP6 save:", m.save(out))
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 示例:球体(使用 create_sphere 方法)
4
+ =================================================
5
+ 设计需求: 创建 D50mm 球体,球心在原点。
6
+
7
+ 执行: python sw_bridge.py run examples\build_sphere.py
8
+ (或在 DSH 里说:"用 SolidWorks 建模一个直径 50mm 的球体")
9
+
10
+ 注意:create_sphere() 使用半圆弧旋转法,比 revolve(360) 更可靠,
11
+ 避免了草图旋转的几何条件限制。
12
+ """
13
+ import os
14
+
15
+ # sw_bridge 的 run 会把包目录加入 sys.path,因此可 import swapi
16
+ import swapi
17
+
18
+ # ---- 1. 新建零件(自动探测模板)----
19
+ m = swapi.new_part()
20
+ print("STEP1 new part:", m.title)
21
+
22
+ # ---- 2. 创建球体(D50mm,球心在原点)----
23
+ feat = m.create_sphere(cx=0, cy=0, cz=0, radius=25)
24
+ print("STEP2 create_sphere OK")
25
+
26
+ # ---- 3. 保存 ----
27
+ out = os.path.join(os.path.dirname(os.path.abspath(__file__)),
28
+ "DSH_球体.sldprt")
29
+ print("STEP3 save:", m.save(out))
30
+ print("DONE - 球体已保存到:", out)
@@ -0,0 +1,381 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Create or resize a circle-to-ellipse surface duct via sw_bridge run.
3
+
4
+ DSH_DUCT_SPEC points to an absolute JSON file. Dimensions are millimeters.
5
+ create: {"mode":"create", "inlet_diameter_mm":40, "outlet_width_mm":60,
6
+ "outlet_height_mm":30, "length_mm":80, "output_path":"...SLDPRT"}
7
+ resize: {"mode":"resize", "outlet_width_mm":70, "output_path":"...SLDPRT"}
8
+
9
+ Uses the native loft extension in sw_surface.
10
+ """
11
+ import json
12
+ import math
13
+ import os
14
+ import tempfile
15
+ import pythoncom
16
+ import win32com.client
17
+ import swapi
18
+ import sw_bridge
19
+ import sw_surface
20
+ from gate_integrity import _sha256
21
+
22
+ INLET = "DSH_Duct_Inlet"
23
+ OUTLET = "DSH_Duct_Outlet"
24
+ PLANE = "DSH_Duct_OutletPlane"
25
+ LOFT = "DSH_Duct_Surface"
26
+
27
+
28
+ def prop(obj, name):
29
+ value = getattr(obj, name)
30
+ return value() if callable(value) and not hasattr(value, "_oleobj_") else value
31
+
32
+
33
+ def same_path(doc, path):
34
+ actual = os.path.normcase(os.path.realpath(str(prop(doc, "GetPathName"))))
35
+ expected = os.path.normcase(os.path.realpath(path))
36
+ if actual != expected:
37
+ raise RuntimeError("Document path mismatch; refusing to modify or close")
38
+
39
+
40
+ def positive(spec, name):
41
+ value = spec[name]
42
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value <= 0:
43
+ raise ValueError(name + " must be a positive finite number")
44
+ return float(value)
45
+
46
+
47
+ def body_count(part, kind):
48
+ bodies = part.GetBodies2(kind, False)
49
+ if bodies is None:
50
+ return 0
51
+ if isinstance(bodies, (tuple, list)):
52
+ return len(bodies)
53
+ raise RuntimeError("Unrecognized body query result")
54
+
55
+
56
+ def xyz(point):
57
+ return tuple(float(prop(point, axis)) for axis in ("X", "Y", "Z"))
58
+
59
+
60
+ def dist(a, b):
61
+ return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))
62
+
63
+
64
+ def ellipse_data(segment):
65
+ segment = win32com.client.CastTo(segment, "ISketchEllipse")
66
+ center = xyz(prop(segment, "GetCenterPoint2"))
67
+ major = xyz(prop(segment, "GetMajorPoint2"))
68
+ minor = xyz(prop(segment, "GetMinorPoint2"))
69
+ return center, major, minor, 2000 * dist(center, major), 2000 * dist(center, minor)
70
+
71
+
72
+ def assert_ellipse(segment, width, height):
73
+ data = ellipse_data(segment)
74
+ if abs(data[3] - width) > 0.01 or abs(data[4] - height) > 0.01:
75
+ raise RuntimeError("Outlet ellipse axes mismatch: %.6f x %.6f mm" % data[3:])
76
+ return data
77
+
78
+
79
+ def named_feature(doc, name):
80
+ feature = win32com.client.CastTo(doc, "IPartDoc").FeatureByName(name)
81
+ if feature is None:
82
+ raise RuntimeError("Required feature missing: " + name)
83
+ return feature
84
+
85
+
86
+ def single_segment(doc, name):
87
+ sketch = prop(named_feature(doc, name), "GetSpecificFeature2")
88
+ segments = prop(sketch, "GetSketchSegments")
89
+ if not isinstance(segments, (tuple, list)) or len(segments) != 1:
90
+ raise RuntimeError("Expected exactly one segment in " + name)
91
+ return segments[0]
92
+
93
+
94
+ def sketch(doc, name):
95
+ return prop(named_feature(doc, name), "GetSpecificFeature2")
96
+
97
+
98
+ def sketch_model_point(sw, sk, coords):
99
+ utility = prop(sw, "GetMathUtility")
100
+ values = win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, tuple(coords))
101
+ point = utility.CreatePoint(values)
102
+ transform = prop(prop(sk, "ModelToSketchTransform"), "Inverse")
103
+ mapped = point.MultiplyTransform(transform)
104
+ return tuple(float(v) for v in prop(mapped, "ArrayData"))
105
+
106
+
107
+ def inspect(sw, doc):
108
+ if prop(doc, "GetConfigurationCount") != 1:
109
+ raise RuntimeError("Duct requires exactly one configuration; refusing to edit or save")
110
+ part = win32com.client.CastTo(doc, "IPartDoc")
111
+ counts = body_count(part, 0), body_count(part, 1)
112
+ if counts != (0, 1):
113
+ raise RuntimeError("Expected zero solid and one surface body, got %r" % (counts,))
114
+ warning = win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_BOOL, False)
115
+ feature = win32com.client.dynamic.DumbDispatch(named_feature(doc, LOFT))
116
+ feature._FlagAsMethod("GetErrorCode2")
117
+ code = feature.GetErrorCode2(warning)
118
+ if int(code) != 0:
119
+ raise RuntimeError("Surface feature reports code %r (warning=%r)" % (code, warning.value))
120
+ named_feature(doc, PLANE)
121
+ arc = win32com.client.CastTo(single_segment(doc, INLET), "ISketchArc")
122
+ radius = float(prop(arc, "GetRadius"))
123
+ inlet = 2000 * radius
124
+ center = xyz(prop(arc, "GetCenterPoint2"))
125
+ outlet = ellipse_data(single_segment(doc, OUTLET))
126
+ if dist(center, (0.0, 0.0, 0.0)) > 1e-5 or dist(outlet[0], (0.0, 0.0, 0.0)) > 1e-5:
127
+ raise RuntimeError("Section centers are not at their sketch origins")
128
+ inlet_sk, outlet_sk = sketch(doc, INLET), sketch(doc, OUTLET)
129
+ inlet_origin = sketch_model_point(sw, inlet_sk, (0.0, 0.0, 0.0))
130
+ outlet_origin = sketch_model_point(sw, outlet_sk, (0.0, 0.0, 0.0))
131
+ inlet_z = sketch_model_point(sw, inlet_sk, (0.0, 0.0, 1.0))
132
+ outlet_z = sketch_model_point(sw, outlet_sk, (0.0, 0.0, 1.0))
133
+ axis = tuple(z - o for z, o in zip(inlet_z, inlet_origin))
134
+ other_axis = tuple(z - o for z, o in zip(outlet_z, outlet_origin))
135
+ separation = tuple(b - a for a, b in zip(inlet_origin, outlet_origin))
136
+ axis_norm = dist(axis, (0.0, 0.0, 0.0))
137
+ other_norm = dist(other_axis, (0.0, 0.0, 0.0))
138
+ if axis_norm < 1e-12 or other_norm < 1e-12:
139
+ raise RuntimeError("Invalid sketch normal")
140
+ normal = tuple(v / axis_norm for v in axis)
141
+ parallel = abs(sum(a * b for a, b in zip(normal, other_axis))) / other_norm
142
+ projection = sum(a * b for a, b in zip(normal, separation))
143
+ lateral = tuple(d - projection * n for d, n in zip(separation, normal))
144
+ length = 1000 * abs(projection)
145
+ if length <= 0 or parallel < 0.999999 or dist(lateral, (0.0, 0.0, 0.0)) > 1e-5:
146
+ raise RuntimeError("Section planes are not parallel and coaxial")
147
+ # Each sampled profile point must lie on a distinct open boundary edge.
148
+ bodies = part.GetBodies2(1, False)
149
+ edges = prop(bodies[0], "GetEdges")
150
+ if not isinstance(edges, (tuple, list)):
151
+ raise RuntimeError("Unable to enumerate surface edges")
152
+ boundary = []
153
+ for edge in edges:
154
+ adjacent = prop(edge, "GetTwoAdjacentFaces2")
155
+ if not isinstance(adjacent, (tuple, list)) or len(adjacent) != 2:
156
+ raise RuntimeError("Unable to determine edge adjacency")
157
+ if sum(face is not None for face in adjacent) == 1:
158
+ boundary.append(edge)
159
+ inlet_points = [tuple(center[i] + (sign * radius if i == 0 else 0) for i in range(3)) for sign in (-1, 1)]
160
+ outlet_points = [outlet[1], tuple(2 * c - p for c, p in zip(outlet[0], outlet[1]))]
161
+ matches = []
162
+ for sk, points in ((inlet_sk, inlet_points), (outlet_sk, outlet_points)):
163
+ side = set()
164
+ for point in points:
165
+ model_point = sketch_model_point(sw, sk, point)
166
+ found = set()
167
+ for index, edge in enumerate(boundary):
168
+ closest = edge.GetClosestPointOn(*model_point)
169
+ if isinstance(closest, (tuple, list)) and len(closest) >= 3 and dist(model_point, closest[:3]) < 1e-5:
170
+ found.add(index)
171
+ if not found:
172
+ raise RuntimeError("Section point has no open boundary edge")
173
+ side.update(found)
174
+ matches.append(side)
175
+ if matches[0] & matches[1]:
176
+ raise RuntimeError("Inlet and outlet boundaries are not distinct")
177
+ return inlet, outlet[3], outlet[4], length
178
+
179
+
180
+ def rebuild(doc):
181
+ if not doc.ForceRebuild3(False):
182
+ raise RuntimeError("SolidWorks rebuild failed")
183
+
184
+
185
+ def save(doc, path):
186
+ # Silent save-as adopts the new pathname; option 2 would keep editing the source.
187
+ error = doc.SaveAs3(path, 0, 1)
188
+ if error != 0:
189
+ raise RuntimeError("SolidWorks save failed: %r" % (error,))
190
+ if not os.path.isfile(path) or os.path.getsize(path) == 0:
191
+ raise RuntimeError("Save did not create a nonempty file")
192
+ same_path(doc, path)
193
+ if bool(prop(doc, "GetSaveFlag")):
194
+ raise RuntimeError("Document is still modified after save")
195
+
196
+
197
+ def rename_feature(sw, doc, original, new, kinds):
198
+ feature = prop(doc, "FirstFeature")
199
+ while feature is not None:
200
+ kind = prop(feature, "GetTypeName2")
201
+ if kind in kinds:
202
+ candidate = prop(feature, "GetSpecificFeature2")
203
+ if candidate is not None and int(sw.IsSame(original, candidate)) == 1:
204
+ feature.Name = new
205
+ named_feature(doc, new)
206
+ return feature
207
+ feature = prop(feature, "GetNextFeature")
208
+ raise RuntimeError("Cannot match created object to its feature: " + new)
209
+
210
+
211
+ def create(sw, spec, path):
212
+ inlet = positive(spec, "inlet_diameter_mm")
213
+ width = positive(spec, "outlet_width_mm")
214
+ height = positive(spec, "outlet_height_mm")
215
+ length = positive(spec, "length_mm")
216
+ if width <= height:
217
+ raise ValueError("outlet_width_mm must be greater than outlet_height_mm")
218
+ if sw.GetOpenDocumentByName(path) is not None:
219
+ raise RuntimeError("Target document is already open")
220
+ if os.path.exists(path):
221
+ raise FileExistsError("Output already exists")
222
+ if not os.path.isdir(os.path.dirname(path)):
223
+ raise FileNotFoundError("Output directory does not exist")
224
+ template = swapi.get_part_template(sw)
225
+ if not template:
226
+ raise RuntimeError("No part template")
227
+ doc = sw.NewDocument(template, 0, 0.1, 0.1)
228
+ if doc is None:
229
+ raise RuntimeError("Failed to create part")
230
+ m = sw_surface.SurfaceModel(sw, doc)
231
+ m.begin_sketch("Front Plane")
232
+ active_inlet = m.skm.ActiveSketch
233
+ m.circle(0, 0, inlet / 2)
234
+ m.end_sketch(merge=False)
235
+ rename_feature(sw, doc, active_inlet, INLET, ("ProfileFeature", "Sketch"))
236
+ plane = m._make_offset_plane("Front Plane", length)
237
+ if not plane.get("ok") or plane.get("feature") is None:
238
+ raise RuntimeError("Offset plane failed: %r" % (plane,))
239
+ # InsertRefPlane returns IRefPlane; naming and selection belong to IFeature.
240
+ plane_feature = rename_feature(sw, doc, plane["feature"], PLANE, ("RefPlane",))
241
+ m.begin_sketch_on_feature(plane_feature, label=PLANE)
242
+ active_outlet = m.skm.ActiveSketch
243
+ segment = m.ellipse(0, 0, width, height)
244
+ assert_ellipse(segment, width, height)
245
+ m.end_sketch(merge=False)
246
+ rename_feature(sw, doc, active_outlet, OUTLET, ("ProfileFeature", "Sketch"))
247
+ loft = m.loft_surface([INLET, OUTLET])
248
+ if loft is None:
249
+ raise RuntimeError("Surface loft failed")
250
+ loft.Name = LOFT
251
+ rebuild(doc)
252
+ actual = inspect(sw, doc)
253
+ if any(abs(a - b) > 0.01 for a, b in zip(actual, (inlet, width, height, length))):
254
+ raise RuntimeError("Created section dimensions differ: %r" % (actual,))
255
+ # Unique title avoids colliding with an unrelated open document's title.
256
+ temp_dir = tempfile.mkdtemp(prefix=".dsh_duct_", dir=os.path.dirname(path))
257
+ temp_path = os.path.join(temp_dir, os.path.basename(temp_dir) + ".SLDPRT")
258
+ save(doc, temp_path)
259
+ same_path(doc, temp_path)
260
+ if bool(prop(doc, "GetSaveFlag")):
261
+ raise RuntimeError("New document is dirty; refusing to close")
262
+ sw.CloseDoc(str(prop(doc, "GetTitle")))
263
+ if sw.GetOpenDocumentByName(temp_path) is not None:
264
+ raise RuntimeError("Created document did not close; output remains at " + temp_path)
265
+ # Windows rename fails if the destination was created by another process.
266
+ os.rename(temp_path, path)
267
+ os.rmdir(temp_dir)
268
+ doc, info = sw_bridge._open_doc6(sw, path, 1, options=1)
269
+ if doc is None or not info.get("ok"):
270
+ raise RuntimeError("Published file saved but reopening failed: %r" % (info,))
271
+ same_path(doc, path)
272
+ reopened = inspect(sw, doc)
273
+ if any(abs(a - b) > 0.01 for a, b in zip(reopened, actual)):
274
+ raise RuntimeError("Reopened dimensions differ: %r" % (reopened,))
275
+ return {"ok": True, "mode": "create", "path": path, "sections_and_length_mm": reopened}
276
+
277
+
278
+ def resize(sw, spec, path):
279
+ width = positive(spec, "outlet_width_mm")
280
+ if not os.path.isfile(path):
281
+ raise ValueError("Existing SLDPRT required for resize")
282
+ doc = sw.GetOpenDocumentByName(path)
283
+ if doc is not None:
284
+ active = sw.ActiveDoc
285
+ if active is None:
286
+ raise RuntimeError("Target is open but not the active document")
287
+ same_path(active, path)
288
+ doc = active
289
+ else:
290
+ doc, info = sw_bridge._open_doc6(sw, path, 1, options=1)
291
+ if doc is None or not info.get("ok"):
292
+ raise RuntimeError("Cannot open part: %r" % (info,))
293
+ same_path(doc, path)
294
+ if bool(prop(doc, "GetSaveFlag")):
295
+ raise RuntimeError("Opened document is already modified")
296
+ inlet, old_width, height, length = inspect(sw, doc)
297
+ if width <= height:
298
+ raise ValueError("New width must exceed current outlet height")
299
+ doc.ClearSelection2(True)
300
+ if not named_feature(doc, OUTLET).Select2(False, 0):
301
+ raise RuntimeError("Could not select named outlet sketch")
302
+ doc.EditSketch()
303
+ attempted = False
304
+ try:
305
+ active = doc.SketchManager.ActiveSketch
306
+ if active is None or int(sw.IsSame(active, sketch(doc, OUTLET))) != 1:
307
+ raise RuntimeError("Unexpected active sketch; refusing to edit")
308
+ segment = win32com.client.CastTo(single_segment(doc, OUTLET), "ISketchEllipse")
309
+ center, major, minor, _, _ = assert_ellipse(segment, old_width, height)
310
+ target = tuple(c + (p - c) * width / old_width for c, p in zip(center, major))
311
+ attempted = True
312
+ if not prop(segment, "GetMajorPoint2").SetCoords(*target):
313
+ raise RuntimeError("SolidWorks rejected in-place axis edit")
314
+ assert_ellipse(segment, width, height)
315
+ doc.SketchManager.InsertSketch(True)
316
+ if doc.SketchManager.ActiveSketch is not None:
317
+ raise RuntimeError("Could not exit the sketch")
318
+ rebuild(doc)
319
+ actual = inspect(sw, doc)
320
+ if any(abs(a - b) > 0.01 for a, b in zip(actual, (inlet, width, height, length))):
321
+ raise RuntimeError("Rebuilt dimensions differ: %r" % (actual,))
322
+ except Exception as error:
323
+ try:
324
+ if doc.SketchManager.ActiveSketch is None:
325
+ doc.ClearSelection2(True)
326
+ if not named_feature(doc, OUTLET).Select2(False, 0):
327
+ raise RuntimeError("Cannot select sketch for recovery")
328
+ doc.EditSketch()
329
+ active = doc.SketchManager.ActiveSketch
330
+ if active is None or int(sw.IsSame(active, sketch(doc, OUTLET))) != 1:
331
+ raise RuntimeError("Cannot safely identify sketch for recovery")
332
+ if attempted:
333
+ original = win32com.client.CastTo(single_segment(doc, OUTLET), "ISketchEllipse")
334
+ if not prop(original, "GetMajorPoint2").SetCoords(*major):
335
+ raise RuntimeError("SolidWorks refused to restore the original axis")
336
+ assert_ellipse(original, old_width, height)
337
+ doc.SketchManager.InsertSketch(True)
338
+ if doc.SketchManager.ActiveSketch is not None:
339
+ raise RuntimeError("Cannot exit recovered sketch")
340
+ rebuild(doc)
341
+ inspect(sw, doc)
342
+ except Exception as recovery:
343
+ raise RuntimeError("Edit failed (%r); automatic recovery failed (%r). No save attempted; inspect the open document." % (error, recovery)) from error
344
+ raise RuntimeError("Edit failed and geometry was restored; no save attempted: %r" % (error,)) from error
345
+ same_path(doc, path)
346
+ save(doc, path)
347
+ return {"ok": True, "mode": "resize", "path": path,
348
+ "old_width_mm": old_width, "sections_mm": actual}
349
+
350
+
351
+ def main(sw):
352
+ spec_path = os.environ.get("DSH_DUCT_SPEC", "")
353
+ if not os.path.isabs(spec_path):
354
+ raise ValueError("DSH_DUCT_SPEC must be an absolute JSON file path")
355
+ with open(spec_path, encoding="utf-8-sig") as stream:
356
+ spec = json.load(stream)
357
+ if not isinstance(spec, dict):
358
+ raise ValueError("Specification must be a JSON object")
359
+ mode = spec.get("mode")
360
+ required = {"mode", "output_path", "outlet_width_mm"}
361
+ if mode == "create":
362
+ required.update(("inlet_diameter_mm", "outlet_height_mm", "length_mm"))
363
+ elif mode != "resize":
364
+ raise ValueError("mode must be create or resize")
365
+ if set(spec) != required:
366
+ raise ValueError("Missing fields: %s; unsupported fields: %s" %
367
+ (sorted(required - spec.keys()), sorted(spec.keys() - required)))
368
+ path = spec.get("output_path", "")
369
+ if not isinstance(path, str) or not os.path.isabs(path) or not path.lower().endswith(".sldprt"):
370
+ raise ValueError("output_path must be an absolute .SLDPRT path")
371
+ path = os.path.abspath(path)
372
+ if mode == "create":
373
+ return create(sw, spec, path)
374
+ return resize(sw, spec, path)
375
+
376
+
377
+ __RESULT__ = main(sw)
378
+ __RESULT__["artifacts"] = [__RESULT__["path"]]
379
+ __RESULT__["geometry_sha256"] = _sha256(__RESULT__["path"])
380
+ if not sw_surface.saved_duct_geometry_matches(sw, __RESULT__["path"], __RESULT__["geometry_sha256"]):
381
+ raise RuntimeError("Saved part does not match the expected single lofted sheet")