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.
- package/LICENSE +21 -0
- package/README.md +772 -3
- package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
- package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
- package/engineering/plugins/dsh-engineering-ui/lib/client.js +8259 -0
- package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
- package/engineering/plugins/dsh-engineering-ui/lib/index.js +5633 -0
- package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
- package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
- package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
- package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
- package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
- package/engineering/skills/cad-workflow/SKILL.md +132 -0
- package/engineering/skills/mode-selection/SKILL.md +660 -0
- package/engineering/skills/physics-in-loop/SKILL.md +238 -0
- package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
- package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
- package/engineering/skills/sw-design/SKILL.md +112 -0
- package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
- package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
- package/engineering/skills/sw-to-cad/SKILL.md +121 -0
- package/engineering/skills/webots-simulation/SKILL.md +58 -0
- package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
- package/engineering/tools/_plugin_paths.py +93 -0
- package/engineering/tools/_root.py +330 -0
- package/engineering/tools/_store.py +446 -0
- package/engineering/tools/ac_bridge.py +459 -0
- package/engineering/tools/ac_rules.json +21 -0
- package/engineering/tools/ac_validate.py +699 -0
- package/engineering/tools/ask_user.py +887 -0
- package/engineering/tools/build_arm1.py +46 -0
- package/engineering/tools/build_assembly.py +94 -0
- package/engineering/tools/build_base.py +57 -0
- package/engineering/tools/build_housing.py +70 -0
- package/engineering/tools/build_structural.py +43 -0
- package/engineering/tools/build_transmission.py +69 -0
- package/engineering/tools/cadx.lsp +128 -0
- package/engineering/tools/choice_contract.py +61 -0
- package/engineering/tools/conn_state.py +1671 -0
- package/engineering/tools/defense_gate.py +3374 -0
- package/engineering/tools/drawing_input.py +314 -0
- package/engineering/tools/env_check.py +467 -0
- package/engineering/tools/examples/build_axis.py +142 -0
- package/engineering/tools/examples/build_cup.py +56 -0
- package/engineering/tools/examples/build_plate.py +58 -0
- package/engineering/tools/examples/build_sphere.py +30 -0
- package/engineering/tools/examples/build_transition_duct.py +381 -0
- package/engineering/tools/gate_integrity.py +401 -0
- package/engineering/tools/mode2_flow.py +3589 -0
- package/engineering/tools/mode_checker.py +77 -0
- package/engineering/tools/mode_gate.py +8013 -0
- package/engineering/tools/part_registry.py +4266 -0
- package/engineering/tools/physics/__init__.py +50 -0
- package/engineering/tools/physics/_compat.py +12 -0
- package/engineering/tools/physics/design_state.py +142 -0
- package/engineering/tools/physics/domain_validator.py +751 -0
- package/engineering/tools/physics/fatigue.py +489 -0
- package/engineering/tools/physics/fea_solver.py +1163 -0
- package/engineering/tools/physics/feapy_solver.py +314 -0
- package/engineering/tools/physics/gb_materials.json +1228 -0
- package/engineering/tools/physics/geometry_gate.py +337 -0
- package/engineering/tools/physics/load_case.py +951 -0
- package/engineering/tools/physics/material_db.py +874 -0
- package/engineering/tools/physics/mesh_adapter.py +321 -0
- package/engineering/tools/physics/refine_rules.py +272 -0
- package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
- package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
- package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
- package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
- package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
- package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
- package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
- package/engineering/tools/physics/rules/housing_rules.json +48 -0
- package/engineering/tools/physics/rules/manifest.json +54 -0
- package/engineering/tools/physics/rules/mold_rules.json +48 -0
- package/engineering/tools/physics/rules/structural_rules.json +56 -0
- package/engineering/tools/physics/rules/transmission_rules.json +48 -0
- package/engineering/tools/physics/simulation_report.py +829 -0
- package/engineering/tools/physics_bridge.py +2685 -0
- package/engineering/tools/profile-manifest.ps1 +72 -0
- package/engineering/tools/run_regression.py +109 -0
- package/engineering/tools/settings.py +419 -0
- package/engineering/tools/solidworks-modeling.md +519 -0
- package/engineering/tools/sw_bridge.py +8215 -0
- package/engineering/tools/sw_guard.py +1066 -0
- package/engineering/tools/sw_surface.py +87 -0
- package/engineering/tools/swapi.py +14766 -0
- package/engineering/tools/verify_sync.py +218 -0
- package/engineering/tools/visual_check.py +2589 -0
- package/engineering/tools/webots_env.py +121 -0
- package/engineering/tools/webots_mcp/__init__.py +3 -0
- package/engineering/tools/webots_mcp/__main__.py +3 -0
- package/engineering/tools/webots_mcp/assembly.py +391 -0
- package/engineering/tools/webots_mcp/bootstrap.py +55 -0
- package/engineering/tools/webots_mcp/controller_worker.py +124 -0
- package/engineering/tools/webots_mcp/evidence.py +288 -0
- package/engineering/tools/webots_mcp/fake_worker.py +69 -0
- package/engineering/tools/webots_mcp/fault_resources.py +121 -0
- package/engineering/tools/webots_mcp/host.py +45 -0
- package/engineering/tools/webots_mcp/ledger.py +73 -0
- package/engineering/tools/webots_mcp/owned_job.py +70 -0
- package/engineering/tools/webots_mcp/physical_gate.py +187 -0
- package/engineering/tools/webots_mcp/physics.py +152 -0
- package/engineering/tools/webots_mcp/pipeline.py +108 -0
- package/engineering/tools/webots_mcp/recipes.py +129 -0
- package/engineering/tools/webots_mcp/repair.py +132 -0
- package/engineering/tools/webots_mcp/repair_trial.py +375 -0
- package/engineering/tools/webots_mcp/runtime.py +311 -0
- package/engineering/tools/webots_mcp/server.py +217 -0
- package/engineering/tools/webots_mcp/sw_export.py +509 -0
- package/engineering/tools/webots_mcp/transport.py +156 -0
- package/engineering/tools/workflow_gate.py +6134 -0
- package/package.json +64 -4
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
r"""webots_env.py — 工程模式【Webots 仿真可用性自检】(流程里的"能不能用"判据)
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
【为什么需要它】
|
|
6
|
+
Webots 那条链默认是**关的**(preset 里 mcp-webots 行 disabled),要同时满足
|
|
7
|
+
六个环境条件才会真装配:
|
|
8
|
+
|
|
9
|
+
DSH_WEBOTS_ENABLE=1 / DSH_WEBOTS_PYTHON / DSH_WEBOTS_INPUT_ROOT
|
|
10
|
+
DSH_STATE_DIR / DSH_ENGINEERING_ROOT / WEBOTS_HOME
|
|
11
|
+
|
|
12
|
+
缺哪一项,表现出来都是一模一样的"工具不见了 / 一调就 CALIBRATION_REQUIRED"。
|
|
13
|
+
没有这个自检,AI 和用户都只能靠猜;更糟的是可能把"没配好"说成"仿真通过"。
|
|
14
|
+
|
|
15
|
+
本模块只做**只读判断**:不启动 Webots、不启动 MCP、不写任何文件、不碰 SW。
|
|
16
|
+
|
|
17
|
+
【与其它模块的关系】
|
|
18
|
+
· 身份/授权:lib/webots.js 的 resolveWebotsContext(Node 半,逐请求)
|
|
19
|
+
· 证据读取:webots_mcp.host(Python 半,只读)
|
|
20
|
+
· 本模块:**配置面**的体检 —— 两者都不替代
|
|
21
|
+
|
|
22
|
+
用法:
|
|
23
|
+
python webots_env.py # 人类可读
|
|
24
|
+
python webots_env.py --json # 机器可读(AI 直接读 ok/blocking 字段)
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
|
|
33
|
+
try:
|
|
34
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
# 缺任一项 → 整条链不会装配(与 preset-engineering.patch.yml 的 disabled 表达式一致)
|
|
39
|
+
REQUIRED = [
|
|
40
|
+
("DSH_WEBOTS_ENABLE", "唯一开关,必须精确等于 '1'"),
|
|
41
|
+
("DSH_WEBOTS_PYTHON", "装好 webots_mcp/requirements.txt 的解释器绝对路径"),
|
|
42
|
+
("DSH_WEBOTS_INPUT_ROOT", "已存在的只读模型输入根(绝对路径)"),
|
|
43
|
+
("DSH_STATE_DIR", "已存在的绝对状态目录;Node 与 Python 必须解析到同一处"),
|
|
44
|
+
("DSH_ENGINEERING_ROOT", "工程模式根的 engineering 目录(cwd 为其 tools)"),
|
|
45
|
+
("WEBOTS_HOME", "Webots 安装根(例如 D:\\Webots)"),
|
|
46
|
+
]
|
|
47
|
+
OPTIONAL = [("DSH_WEBOTS_RUNTIME_CONFIG", "可选的已批准运行配置 JSON(绝对路径)")]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _check_name(name, hint, path_like):
|
|
51
|
+
raw = (os.environ.get(name) or "").strip()
|
|
52
|
+
if not raw:
|
|
53
|
+
return {"name": name, "ok": False, "value": None, "hint": hint,
|
|
54
|
+
"reason": "未设置"}
|
|
55
|
+
if name == "DSH_WEBOTS_ENABLE":
|
|
56
|
+
return {"name": name, "ok": raw == "1", "value": raw, "hint": hint,
|
|
57
|
+
"reason": None if raw == "1" else "必须为 '1'(当前 %r)" % raw}
|
|
58
|
+
if path_like:
|
|
59
|
+
ab = os.path.isabs(raw)
|
|
60
|
+
exists = os.path.exists(raw)
|
|
61
|
+
ok = ab and exists
|
|
62
|
+
if ok:
|
|
63
|
+
reason = None
|
|
64
|
+
elif not ab:
|
|
65
|
+
reason = "必须是绝对路径"
|
|
66
|
+
else:
|
|
67
|
+
reason = "路径不存在"
|
|
68
|
+
return {"name": name, "ok": ok, "value": raw, "hint": hint, "reason": reason}
|
|
69
|
+
return {"name": name, "ok": True, "value": raw, "hint": hint, "reason": None}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def probe():
|
|
73
|
+
checks = []
|
|
74
|
+
for name, hint in REQUIRED:
|
|
75
|
+
# DSH_WEBOTS_ENABLE 不是路径;其余五个都要是存在的绝对路径
|
|
76
|
+
checks.append(_check_name(name, hint, name != "DSH_WEBOTS_ENABLE"))
|
|
77
|
+
optional = [_check_name(n, h, True) for n, h in OPTIONAL]
|
|
78
|
+
blocking = [c for c in checks if not c["ok"]]
|
|
79
|
+
enabled = not blocking
|
|
80
|
+
return {
|
|
81
|
+
"ok": True,
|
|
82
|
+
"sim_available": enabled,
|
|
83
|
+
# ⚠️ 可用 != 就绪:就算配齐,也只是"能装配"。
|
|
84
|
+
# 真 READY 还需要真实控制器握手 + 机器人身份(见 integration-contract)。
|
|
85
|
+
"ready": False,
|
|
86
|
+
"ready_reason": "配置齐备只代表可装配;READY 需真实握手与身份确认,本版未实现",
|
|
87
|
+
"checks": checks,
|
|
88
|
+
"optional": optional,
|
|
89
|
+
"blocking": [c["name"] for c in blocking],
|
|
90
|
+
"next_action": ("可直接用 mcp__webots__* 工具" if enabled
|
|
91
|
+
else "先补 " + ", ".join(c["name"] for c in blocking)),
|
|
92
|
+
"notes": [
|
|
93
|
+
"本自检不启动 Webots / MCP,不写任何文件,不申请 SW 锁。",
|
|
94
|
+
"缺项时正确行为是报告'仿真不可用',不是伪造结果。",
|
|
95
|
+
],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main(argv):
|
|
100
|
+
report = probe()
|
|
101
|
+
if "--json" in argv:
|
|
102
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
103
|
+
else:
|
|
104
|
+
print("Webots 仿真可用性自检")
|
|
105
|
+
print("=" * 46)
|
|
106
|
+
for c in report["checks"]:
|
|
107
|
+
mark = "OK " if c["ok"] else "缺 "
|
|
108
|
+
print("[%s] %-26s %s" % (mark, c["name"], c["value"] or "-"))
|
|
109
|
+
if c["reason"]:
|
|
110
|
+
print(" -> %s(%s)" % (c["reason"], c["hint"]))
|
|
111
|
+
for c in report["optional"]:
|
|
112
|
+
print("[可选] %-24s %s" % (c["name"], c["value"] or "-"))
|
|
113
|
+
print("-" * 46)
|
|
114
|
+
print("可装配:%s" % ("是" if report["sim_available"] else "否"))
|
|
115
|
+
print("就绪 :否(%s)" % report["ready_reason"])
|
|
116
|
+
print("下一步:%s" % report["next_action"])
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Pure snapshot mapping. Persistent-reference resolution belongs to the SW adapter."""
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def map_candidates(components, mappings, *, target, snapshot, current):
|
|
6
|
+
"""Return all CAD candidates, refusing stale contexts; never authorize a write."""
|
|
7
|
+
required = ("source_revision", "configuration", "generation")
|
|
8
|
+
for context in (snapshot, current):
|
|
9
|
+
if any(not isinstance(context.get(k), str) or not context[k].strip() for k in required[:2]) or type(context.get("generation")) is not int or context["generation"] < 0:
|
|
10
|
+
raise ValueError("INVALID_SNAPSHOT_CONTEXT")
|
|
11
|
+
if any(snapshot.get(k) is None or snapshot.get(k) != current.get(k) for k in required):
|
|
12
|
+
raise ValueError("STALE_OR_UNKNOWN_SNAPSHOT")
|
|
13
|
+
by_id = {}
|
|
14
|
+
for component in components:
|
|
15
|
+
cid = component.get("id")
|
|
16
|
+
if not isinstance(cid, str) or not cid or cid in by_id:
|
|
17
|
+
raise ValueError("INVALID_OR_DUPLICATE_COMPONENT_ID")
|
|
18
|
+
by_id[cid] = component
|
|
19
|
+
candidates = set()
|
|
20
|
+
for mapping in mappings:
|
|
21
|
+
value = mapping.get("target")
|
|
22
|
+
valid = isinstance(value, str) and bool(value.strip())
|
|
23
|
+
if isinstance(value, dict):
|
|
24
|
+
valid = set(value) == {"kind", "id"} and all(isinstance(value[k], str) and value[k].strip() for k in value)
|
|
25
|
+
if not valid:
|
|
26
|
+
raise ValueError("INVALID_MAPPING_TARGET")
|
|
27
|
+
ids = mapping.get("component_ids")
|
|
28
|
+
if not isinstance(ids, list) or not ids or any(not isinstance(i, str) or i not in by_id for i in ids):
|
|
29
|
+
raise ValueError("INVALID_MAPPING_COMPONENTS")
|
|
30
|
+
if mapping.get("target") == target:
|
|
31
|
+
candidates.update(ids)
|
|
32
|
+
return [deepcopy(by_id[cid]) for cid in sorted(candidates)]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def component_targets(components, mappings, *, component_id, snapshot, current):
|
|
36
|
+
"""Reverse lookup in the same validated snapshot; merged bodies remain many-to-one."""
|
|
37
|
+
map_candidates(components, mappings, target=None, snapshot=snapshot, current=current)
|
|
38
|
+
if component_id not in {c["id"] for c in components}:
|
|
39
|
+
raise ValueError("UNKNOWN_COMPONENT")
|
|
40
|
+
return [deepcopy(m["target"]) for m in mappings if component_id in m["component_ids"]]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def classify_motion(*, translation_dof, rotation_dof, solver_status, complete):
|
|
44
|
+
"""Classify an adapter's solved relative DOFs, never infer them from mate names.
|
|
45
|
+
|
|
46
|
+
Counts alone do not establish axes or global mechanism mobility.
|
|
47
|
+
"""
|
|
48
|
+
if complete is not True or solver_status != "solved":
|
|
49
|
+
raise ValueError("UNRESOLVED_OR_OVERCONSTRAINED_MATES")
|
|
50
|
+
if any(type(n) is not int or not 0 <= n <= 3 for n in (translation_dof, rotation_dof)):
|
|
51
|
+
raise ValueError("INVALID_DOF")
|
|
52
|
+
kinds = {(0, 0): "fixed", (0, 1): "revolute", (1, 0): "prismatic"}
|
|
53
|
+
try:
|
|
54
|
+
return kinds[translation_dof, rotation_dof]
|
|
55
|
+
except KeyError:
|
|
56
|
+
raise ValueError("UNSUPPORTED_MOBILITY") from None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_tree(links, joints):
|
|
60
|
+
"""Reject loops, multiple parents and disconnected links; never cut edges."""
|
|
61
|
+
if not links or any(not isinstance(x, str) or not x for x in links) or len(set(links)) != len(links):
|
|
62
|
+
raise ValueError("INVALID_LINKS")
|
|
63
|
+
parents, names = {}, set()
|
|
64
|
+
for joint in joints:
|
|
65
|
+
name, parent, child = (joint.get(k) for k in ("id", "parent", "child"))
|
|
66
|
+
if not isinstance(name, str) or not name or name in names:
|
|
67
|
+
raise ValueError("INVALID_JOINT_ID")
|
|
68
|
+
names.add(name)
|
|
69
|
+
if parent not in links or child not in links or parent == child:
|
|
70
|
+
raise ValueError("INVALID_JOINT_ENDPOINT")
|
|
71
|
+
if child in parents:
|
|
72
|
+
raise ValueError("MULTIPLE_PARENTS_OR_CLOSED_LOOP")
|
|
73
|
+
parents[child] = parent
|
|
74
|
+
for link in links:
|
|
75
|
+
seen = set()
|
|
76
|
+
while link in parents:
|
|
77
|
+
if link in seen:
|
|
78
|
+
raise ValueError("CLOSED_LOOP_UNSUPPORTED")
|
|
79
|
+
seen.add(link)
|
|
80
|
+
link = parents[link]
|
|
81
|
+
roots = set(links) - parents.keys()
|
|
82
|
+
if len(roots) != 1:
|
|
83
|
+
raise ValueError("DISCONNECTED_ASSEMBLY")
|
|
84
|
+
return next(iter(roots))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_joint(joint):
|
|
88
|
+
"""Validate explicit SI joint geometry; no defaults for unknown capture data."""
|
|
89
|
+
from math import isfinite, sqrt
|
|
90
|
+
kind = joint.get("type")
|
|
91
|
+
if kind not in ("fixed", "revolute", "prismatic"):
|
|
92
|
+
raise ValueError("UNSUPPORTED_JOINT")
|
|
93
|
+
if joint.get("units") != "SI" or joint.get("axis_frame") != "joint":
|
|
94
|
+
raise ValueError("UNKNOWN_JOINT_CONVENTION")
|
|
95
|
+
def vector(key):
|
|
96
|
+
value = joint.get(key)
|
|
97
|
+
if not isinstance(value, (list, tuple)) or len(value) != 3 or any(type(n) not in (int, float) or not isfinite(n) for n in value):
|
|
98
|
+
raise ValueError("INVALID_" + key.upper())
|
|
99
|
+
return value
|
|
100
|
+
vector("origin_xyz_m")
|
|
101
|
+
vector("origin_rpy_rad")
|
|
102
|
+
if kind != "fixed":
|
|
103
|
+
axis = vector("axis")
|
|
104
|
+
if abs(sqrt(sum(n*n for n in axis)) - 1) > 1e-9:
|
|
105
|
+
raise ValueError("AXIS_NOT_UNIT")
|
|
106
|
+
limits = joint.get("limits", {})
|
|
107
|
+
for key in ("lower", "upper", "effort", "velocity"):
|
|
108
|
+
value = limits.get(key)
|
|
109
|
+
if type(value) not in (int, float) or not isfinite(value):
|
|
110
|
+
raise ValueError("INVALID_JOINT_LIMIT")
|
|
111
|
+
if limits["lower"] > limits["upper"] or limits["effort"] <= 0 or limits["velocity"] <= 0:
|
|
112
|
+
raise ValueError("INVALID_JOINT_LIMIT")
|
|
113
|
+
return deepcopy(joint)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def resolved_component(component, *, resolution, snapshot, current):
|
|
117
|
+
"""Check adapter evidence for an exact occurrence; does not invoke SW or allow edits.
|
|
118
|
+
|
|
119
|
+
resolution must be freshly produced by a trusted SW adapter, not model text.
|
|
120
|
+
Opaque references are not compared across rebuilds: the adapter must resolve them.
|
|
121
|
+
"""
|
|
122
|
+
map_candidates([component], [], target=None, snapshot=snapshot, current=current)
|
|
123
|
+
required = ("owner_document", "instance_path", "configuration", "persistent_reference")
|
|
124
|
+
if any(not isinstance(component.get(k), str) or not component[k].strip() for k in required):
|
|
125
|
+
raise ValueError("MISSING_PERSISTENT_OCCURRENCE_IDENTITY")
|
|
126
|
+
if resolution.get("method") != "IModelDocExtension.GetObjectByPersistReference3" or resolution.get("status") != "resolved" or resolution.get("object_type") != "IComponent2":
|
|
127
|
+
raise ValueError("UNRESOLVED_COMPONENT_REFERENCE")
|
|
128
|
+
if resolution.get("snapshot") != current or any(resolution.get(k) != component[k] for k in required):
|
|
129
|
+
raise ValueError("REFERENCE_CONTEXT_OR_OCCURRENCE_MISMATCH")
|
|
130
|
+
if resolution.get("component_id") != component["id"]:
|
|
131
|
+
raise ValueError("REFERENCE_COMPONENT_MISMATCH")
|
|
132
|
+
return deepcopy(component)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def capture_reference(extension, obj):
|
|
136
|
+
"""Capture opaque bytes with the owning document's native extension."""
|
|
137
|
+
import base64
|
|
138
|
+
raw = extension.GetPersistReference3(obj)
|
|
139
|
+
if raw is None or isinstance(raw, (str, int, bool)):
|
|
140
|
+
raise ValueError("PERSISTENT_REFERENCE_UNAVAILABLE")
|
|
141
|
+
try:
|
|
142
|
+
raw = bytes(raw)
|
|
143
|
+
except (TypeError, ValueError):
|
|
144
|
+
raise ValueError("INVALID_PERSISTENT_REFERENCE_BYTES") from None
|
|
145
|
+
if not raw:
|
|
146
|
+
raise ValueError("PERSISTENT_REFERENCE_UNAVAILABLE")
|
|
147
|
+
return base64.b64encode(raw).decode("ascii")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def resolve_reference(extension, token, *, invoke=None):
|
|
151
|
+
"""Resolve without attaching/launching SW. Optional invoke supports offline fakes.
|
|
152
|
+
|
|
153
|
+
Production path uses explicit pywin32 SAFEARRAY/UI1 and BYREF/I4; must be
|
|
154
|
+
calibrated against the installed SW typelib before enabling automatic writes.
|
|
155
|
+
"""
|
|
156
|
+
import base64
|
|
157
|
+
if not isinstance(token, str) or not token:
|
|
158
|
+
raise ValueError("INVALID_PERSISTENT_REFERENCE")
|
|
159
|
+
raw = base64.b64decode(token, validate=True)
|
|
160
|
+
if not raw:
|
|
161
|
+
raise ValueError("INVALID_PERSISTENT_REFERENCE")
|
|
162
|
+
if invoke is None:
|
|
163
|
+
import pythoncom
|
|
164
|
+
from win32com.client import VARIANT
|
|
165
|
+
reference = VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_UI1, list(raw))
|
|
166
|
+
error = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
|
|
167
|
+
obj = extension.GetObjectByPersistReference3(reference, error)
|
|
168
|
+
status = error.value
|
|
169
|
+
else:
|
|
170
|
+
obj, status = invoke(extension, raw)
|
|
171
|
+
if type(status) is not int or status != 0 or obj is None:
|
|
172
|
+
raise ValueError("PERSISTENT_REFERENCE_NOT_RESOLVED")
|
|
173
|
+
return obj
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def capture_component_mates(component, extension):
|
|
177
|
+
"""Read raw native mates without inventing a DOF solution or omitting failures."""
|
|
178
|
+
result = []
|
|
179
|
+
for mate in component.GetMates() or ():
|
|
180
|
+
record = {"status": "unknown", "persistent_reference": None, "entities": []}
|
|
181
|
+
try:
|
|
182
|
+
record["persistent_reference"] = capture_reference(extension, mate)
|
|
183
|
+
record["mate_type_raw"] = int(mate.Type)
|
|
184
|
+
count = mate.GetMateEntityCount()
|
|
185
|
+
if type(count) is not int or not 1 <= count <= 64:
|
|
186
|
+
raise ValueError("INVALID_MATE_ENTITY_COUNT")
|
|
187
|
+
for i in range(count):
|
|
188
|
+
entity = mate.MateEntity(i)
|
|
189
|
+
referenced = entity.ReferenceComponent
|
|
190
|
+
record["entities"].append({
|
|
191
|
+
"component_reference": capture_reference(extension, referenced) if referenced is not None else None,
|
|
192
|
+
"reference_type_raw": int(entity.ReferenceType2),
|
|
193
|
+
"entity_params_raw": list(entity.EntityParams) if entity.EntityParams is not None else None,
|
|
194
|
+
})
|
|
195
|
+
record["status"] = "captured-not-solved"
|
|
196
|
+
except Exception as error:
|
|
197
|
+
# Preserve the mate and failure; in-place/unsupported mates are not dropped.
|
|
198
|
+
record["error"] = type(error).__name__ + ": " + str(error)
|
|
199
|
+
result.append(record)
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def solve_axial_bundle(constraints, *, complete, frame, tolerance=1e-8):
|
|
204
|
+
"""Solve a narrow exact coaxial bundle from normalized captured geometry.
|
|
205
|
+
|
|
206
|
+
Supported primitives: coaxial (4 constraints), axial_position (1),
|
|
207
|
+
rotation_lock (1). No raw enum guessing, general rank solver or loop cutting.
|
|
208
|
+
Adapter must establish full active bundle and one common owning assembly frame.
|
|
209
|
+
"""
|
|
210
|
+
from math import isfinite, sqrt
|
|
211
|
+
if complete is not True or not isinstance(frame, str) or not frame.strip():
|
|
212
|
+
raise ValueError("INCOMPLETE_CONSTRAINT_BUNDLE")
|
|
213
|
+
if type(tolerance) not in (int, float) or not isfinite(tolerance) or not 0 < tolerance < 0.01:
|
|
214
|
+
raise ValueError("INVALID_GEOMETRIC_TOLERANCE")
|
|
215
|
+
groups = {}
|
|
216
|
+
for item in constraints:
|
|
217
|
+
kind = item.get("kind")
|
|
218
|
+
if kind not in ("coaxial", "axial_position", "rotation_lock") or kind in groups:
|
|
219
|
+
raise ValueError("UNSUPPORTED_OR_REDUNDANT_CONSTRAINT")
|
|
220
|
+
if item.get("frame") != frame or item.get("status") != "active-satisfied":
|
|
221
|
+
raise ValueError("UNVERIFIED_CONSTRAINT_CONTEXT")
|
|
222
|
+
groups[kind] = item
|
|
223
|
+
if "coaxial" not in groups:
|
|
224
|
+
raise ValueError("MISSING_COAXIAL_CONSTRAINT")
|
|
225
|
+
def vec(value):
|
|
226
|
+
if not isinstance(value, (tuple, list)) or len(value) != 3 or any(type(x) not in (int, float) or not isfinite(x) for x in value):
|
|
227
|
+
raise ValueError("INVALID_CONSTRAINT_GEOMETRY")
|
|
228
|
+
return value
|
|
229
|
+
def axis(value):
|
|
230
|
+
value = vec(value)
|
|
231
|
+
if abs(sqrt(sum(x*x for x in value))-1) > tolerance:
|
|
232
|
+
raise ValueError("INVALID_CONSTRAINT_AXIS")
|
|
233
|
+
return value
|
|
234
|
+
item = groups["coaxial"]
|
|
235
|
+
a, b = axis(item.get("parent_axis")), axis(item.get("child_axis"))
|
|
236
|
+
p, q = vec(item.get("parent_point_m")), vec(item.get("child_point_m"))
|
|
237
|
+
dot = sum(x*y for x,y in zip(a,b))
|
|
238
|
+
if abs(abs(dot)-1) > tolerance:
|
|
239
|
+
raise ValueError("AXES_NOT_PARALLEL")
|
|
240
|
+
d = [q[i]-p[i] for i in range(3)]
|
|
241
|
+
along = sum(d[i]*a[i] for i in range(3))
|
|
242
|
+
if sqrt(sum((d[i]-along*a[i])**2 for i in range(3))) > tolerance:
|
|
243
|
+
raise ValueError("AXES_NOT_COINCIDENT")
|
|
244
|
+
if "axial_position" in groups:
|
|
245
|
+
lock = groups["axial_position"]
|
|
246
|
+
normal = axis(lock.get("normal"))
|
|
247
|
+
if abs(abs(sum(a[i]*normal[i] for i in range(3)))-1) > tolerance:
|
|
248
|
+
raise ValueError("POSITION_CONSTRAINT_NOT_AXIAL")
|
|
249
|
+
residual = lock.get("residual_m")
|
|
250
|
+
if type(residual) not in (int, float) or not isfinite(residual) or abs(residual) > tolerance:
|
|
251
|
+
raise ValueError("UNSATISFIED_AXIAL_POSITION")
|
|
252
|
+
if "rotation_lock" in groups and groups["rotation_lock"].get("locked") is not True:
|
|
253
|
+
raise ValueError("UNVERIFIED_ROTATION_LOCK")
|
|
254
|
+
t, r = int("axial_position" not in groups), int("rotation_lock" not in groups)
|
|
255
|
+
kind = classify_motion(translation_dof=t, rotation_dof=r, solver_status="solved", complete=True)
|
|
256
|
+
return {"type": kind, "translation_dof": t, "rotation_dof": r, "axis": list(a), "point_m": list(p), "frame": frame, "provenance": "static-derived", "physical_validation": "not-run"}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def solve_captured_mates(records, *, parent_reference, child_reference, frame, complete, tolerance=1e-8):
|
|
260
|
+
"""Native enum/EntityParams -> narrow axial solution, with explicit capture context.
|
|
261
|
+
|
|
262
|
+
Each record additionally needs owning_frame, active=True and lock_rotation
|
|
263
|
+
(for concentric), obtained by the trusted capture caller from feature data.
|
|
264
|
+
"""
|
|
265
|
+
from math import isfinite
|
|
266
|
+
if not isinstance(parent_reference, str) or not parent_reference or not isinstance(child_reference, str) or not child_reference or parent_reference == child_reference:
|
|
267
|
+
raise ValueError("INVALID_OCCURRENCE_PAIR")
|
|
268
|
+
if type(tolerance) not in (int,float) or not isfinite(tolerance) or not 0 < tolerance < 0.01:
|
|
269
|
+
raise ValueError("INVALID_GEOMETRIC_TOLERANCE")
|
|
270
|
+
constraints = []
|
|
271
|
+
for record in records:
|
|
272
|
+
if record.get("status") != "captured-not-solved" or record.get("owning_frame") != frame or record.get("active") is not True:
|
|
273
|
+
raise ValueError("UNKNOWN_MATE_STATE_OR_FRAME")
|
|
274
|
+
entities = record.get("entities", [])
|
|
275
|
+
if len(entities) != 2:
|
|
276
|
+
raise ValueError("UNSUPPORTED_ENTITY_COUNT")
|
|
277
|
+
by_ref = {e.get("component_reference"): e for e in entities}
|
|
278
|
+
if set(by_ref) != {parent_reference, child_reference}:
|
|
279
|
+
raise ValueError("MATE_OCCURRENCE_MISMATCH")
|
|
280
|
+
a, b = by_ref[parent_reference], by_ref[child_reference]
|
|
281
|
+
vectors = []
|
|
282
|
+
for entity in (a,b):
|
|
283
|
+
values = entity.get("entity_params_raw")
|
|
284
|
+
if not isinstance(values, list) or len(values) < 6 or any(type(v) not in (int,float) or not isfinite(v) for v in values):
|
|
285
|
+
raise ValueError("INVALID_MATE_PARAMETERS")
|
|
286
|
+
if abs(sum(v*v for v in values[3:6])-1) > tolerance:
|
|
287
|
+
raise ValueError("NONUNIT_NATIVE_ENTITY_DIRECTION")
|
|
288
|
+
vectors.append(values)
|
|
289
|
+
p, q = vectors
|
|
290
|
+
common = {"frame": frame, "status": "active-satisfied"}
|
|
291
|
+
kind = record.get("mate_type_raw")
|
|
292
|
+
if type(kind) is not int:
|
|
293
|
+
raise ValueError("INVALID_NATIVE_MATE_TYPE")
|
|
294
|
+
if kind == 1 and a.get("reference_type_raw") == b.get("reference_type_raw") == 4:
|
|
295
|
+
constraints.append(dict(common, kind="coaxial", parent_axis=p[3:6], child_axis=q[3:6], parent_point_m=p[:3], child_point_m=q[:3]))
|
|
296
|
+
locked = record.get("lock_rotation")
|
|
297
|
+
if type(locked) is not bool:
|
|
298
|
+
raise ValueError("UNKNOWN_CONCENTRIC_ROTATION_LOCK")
|
|
299
|
+
if locked:
|
|
300
|
+
constraints.append(dict(common, kind="rotation_lock", locked=True))
|
|
301
|
+
elif kind == 0 and a.get("reference_type_raw") == b.get("reference_type_raw") == 3:
|
|
302
|
+
if abs(abs(sum(p[i]*q[i] for i in range(3,6)))-1) > tolerance:
|
|
303
|
+
raise ValueError("COINCIDENT_PLANES_NOT_PARALLEL")
|
|
304
|
+
constraints.append(dict(common, kind="axial_position", normal=p[3:6], residual_m=sum((q[i]-p[i])*p[i+3] for i in range(3))))
|
|
305
|
+
else:
|
|
306
|
+
raise ValueError("UNSUPPORTED_NATIVE_MATE")
|
|
307
|
+
return solve_axial_bundle(constraints, complete=complete, frame=frame, tolerance=tolerance)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def capture_mate_features(model, *, owning_frame, configuration, sw_this_configuration=1):
|
|
311
|
+
"""Read this document's MateGroup features; never recurse into subassemblies.
|
|
312
|
+
|
|
313
|
+
Owning frame is caller's source-snapshot ID for this exact document instance.
|
|
314
|
+
Does not establish global completeness across flexible/nested assemblies.
|
|
315
|
+
"""
|
|
316
|
+
from types import SimpleNamespace
|
|
317
|
+
if not isinstance(owning_frame, str) or not owning_frame.strip():
|
|
318
|
+
raise ValueError("OWNING_FRAME_REQUIRED")
|
|
319
|
+
if model.ConfigurationManager.ActiveConfiguration.Name != configuration:
|
|
320
|
+
raise ValueError("CONFIGURATION_MISMATCH")
|
|
321
|
+
extension = model.Extension
|
|
322
|
+
result = []
|
|
323
|
+
feature = model.FirstFeature()
|
|
324
|
+
visited = set()
|
|
325
|
+
while feature is not None:
|
|
326
|
+
token = capture_reference(extension, feature)
|
|
327
|
+
if token in visited:
|
|
328
|
+
raise ValueError("CYCLIC_FEATURE_TRAVERSAL")
|
|
329
|
+
visited.add(token)
|
|
330
|
+
if feature.GetTypeName2() == "MateGroup":
|
|
331
|
+
sub = feature.GetFirstSubFeature()
|
|
332
|
+
sub_seen = set()
|
|
333
|
+
while sub is not None:
|
|
334
|
+
sub_token = capture_reference(extension, sub)
|
|
335
|
+
if sub_token in sub_seen:
|
|
336
|
+
raise ValueError("CYCLIC_MATE_TRAVERSAL")
|
|
337
|
+
sub_seen.add(sub_token)
|
|
338
|
+
record = {"status": "unknown", "feature_reference": sub_token, "owning_frame": owning_frame, "configuration": configuration, "active": None}
|
|
339
|
+
try:
|
|
340
|
+
states = sub.IsSuppressed2(sw_this_configuration, None)
|
|
341
|
+
if not isinstance(states, (tuple,list)) or len(states) != 1 or type(states[0]) is not bool:
|
|
342
|
+
raise ValueError("UNKNOWN_SUPPRESSION_STATE")
|
|
343
|
+
record["active"] = not states[0]
|
|
344
|
+
mate = sub.GetSpecificFeature2()
|
|
345
|
+
raw = capture_component_mates(SimpleNamespace(GetMates=lambda: [mate]), extension)[0]
|
|
346
|
+
record.update(raw)
|
|
347
|
+
if record.get("mate_type_raw") == 1:
|
|
348
|
+
locked = sub.GetDefinition().LockRotation
|
|
349
|
+
if type(locked) is not bool:
|
|
350
|
+
raise ValueError("UNKNOWN_LOCK_ROTATION")
|
|
351
|
+
record["lock_rotation"] = locked
|
|
352
|
+
except Exception as error:
|
|
353
|
+
record["status"] = "unknown"
|
|
354
|
+
record["error"] = type(error).__name__ + ": " + str(error)
|
|
355
|
+
result.append(record)
|
|
356
|
+
sub = sub.GetNextSubFeature()
|
|
357
|
+
feature = feature.GetNextFeature()
|
|
358
|
+
if model.ConfigurationManager.ActiveConfiguration.Name != configuration:
|
|
359
|
+
raise ValueError("CONFIGURATION_CHANGED_DURING_CAPTURE")
|
|
360
|
+
return result
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def joint_from_solution(solution, *, joint_id, parent, child, parent_rotation, parent_translation_m, owning_frame, limits=None):
|
|
364
|
+
"""Express an assembly-frame axial solution in a parent-aligned joint frame.
|
|
365
|
+
|
|
366
|
+
parent_rotation/translation map parent link -> solution owning frame. Child
|
|
367
|
+
link geometry must subsequently be expressed in this selected joint frame.
|
|
368
|
+
"""
|
|
369
|
+
from math import isfinite
|
|
370
|
+
if not isinstance(owning_frame, str) or not owning_frame.strip() or solution.get("frame") != owning_frame:
|
|
371
|
+
raise ValueError("SOLUTION_PARENT_FRAME_MISMATCH")
|
|
372
|
+
if any(not isinstance(v, str) or not v.strip() for v in (joint_id,parent,child)) or parent == child:
|
|
373
|
+
raise ValueError("INVALID_JOINT_ENDPOINT")
|
|
374
|
+
r = parent_rotation
|
|
375
|
+
if not isinstance(r,(list,tuple)) or len(r)!=3 or any(not isinstance(row,(list,tuple)) or len(row)!=3 for row in r):
|
|
376
|
+
raise ValueError("INVALID_PARENT_ROTATION")
|
|
377
|
+
values = [x for row in r for x in row] + list(parent_translation_m)
|
|
378
|
+
if len(parent_translation_m)!=3 or any(type(x) not in (int,float) or not isfinite(x) for x in values):
|
|
379
|
+
raise ValueError("INVALID_PARENT_TRANSFORM")
|
|
380
|
+
det = r[0][0]*(r[1][1]*r[2][2]-r[1][2]*r[2][1])-r[0][1]*(r[1][0]*r[2][2]-r[1][2]*r[2][0])+r[0][2]*(r[1][0]*r[2][1]-r[1][1]*r[2][0])
|
|
381
|
+
if abs(det-1)>1e-9 or any(abs(sum(r[k][i]*r[k][j] for k in range(3))-(i==j))>1e-9 for i in range(3) for j in range(3)):
|
|
382
|
+
raise ValueError("INVALID_PARENT_ROTATION")
|
|
383
|
+
axis, point = solution["axis"], solution["point_m"]
|
|
384
|
+
if any(not isinstance(v,(list,tuple)) or len(v)!=3 or any(type(x) not in (int,float) or not isfinite(x) for x in v) for v in (axis,point)):
|
|
385
|
+
raise ValueError("INVALID_SOLUTION_GEOMETRY")
|
|
386
|
+
local_axis = [sum(r[k][i]*axis[k] for k in range(3)) for i in range(3)]
|
|
387
|
+
local_point = [sum(r[k][i]*(point[k]-parent_translation_m[k]) for k in range(3)) for i in range(3)]
|
|
388
|
+
joint = dict(id=joint_id,parent=parent,child=child,type=solution["type"],units="SI",axis_frame="joint",axis=local_axis,origin_xyz_m=local_point,origin_rpy_rad=[0,0,0])
|
|
389
|
+
if limits is not None:
|
|
390
|
+
joint["limits"] = deepcopy(limits)
|
|
391
|
+
return validate_joint(joint)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Owned process entry point. Both native pipes are drained even after the log cap."""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
env = os.environ.copy()
|
|
11
|
+
port, robot, world = env["A04_PORT"], env["A04_ROBOT"], env["A04_WORLD"]
|
|
12
|
+
home = Path(env["WEBOTS_HOME"])
|
|
13
|
+
binaries = home / "msys64/mingw64/bin"
|
|
14
|
+
env["PYTHONPATH"] = str(home / "lib/controller/python")
|
|
15
|
+
env["PATH"] = str(Path(sys.executable).parent) + os.pathsep + env["PATH"]
|
|
16
|
+
ready = threading.Event()
|
|
17
|
+
expected = f"ipc://{port}/{robot}".encode()
|
|
18
|
+
|
|
19
|
+
def launch(name, args):
|
|
20
|
+
proc = subprocess.Popen(args, env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
21
|
+
stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
22
|
+
def drain(pipe, suffix):
|
|
23
|
+
tail = b""
|
|
24
|
+
remaining = 65536
|
|
25
|
+
with pipe, open(name + "." + suffix, "xb") as log:
|
|
26
|
+
while chunk := pipe.read1(8192):
|
|
27
|
+
if name == "webots" and suffix == "stdout":
|
|
28
|
+
lines = (tail + chunk).split(b"\n")
|
|
29
|
+
tail = lines.pop()[-512:]
|
|
30
|
+
if any(line.strip() == expected for line in lines):
|
|
31
|
+
ready.set()
|
|
32
|
+
if remaining:
|
|
33
|
+
saved = chunk[:remaining]
|
|
34
|
+
log.write(saved)
|
|
35
|
+
log.flush()
|
|
36
|
+
remaining -= len(saved)
|
|
37
|
+
for label, pipe in (("stdout",proc.stdout),("stderr",proc.stderr)):
|
|
38
|
+
threading.Thread(target=drain,args=(pipe,label),daemon=True).start()
|
|
39
|
+
return proc
|
|
40
|
+
|
|
41
|
+
launch("webots", [str(binaries / "webots.exe"), "--batch", "--minimize","--mode=fast",
|
|
42
|
+
"--no-rendering","--stdout","--stderr","--extern-urls",f"--port={port}",world])
|
|
43
|
+
if not ready.wait(25):
|
|
44
|
+
raise TimeoutError("No matching extern URL on owned launcher output")
|
|
45
|
+
ctrl = launch("controller", [str(binaries / "webots-controller.exe"), "--protocol=ipc",
|
|
46
|
+
f"--port={port}",f"--robot-name={robot}",str(Path(__file__).with_name("controller_worker.py"))])
|
|
47
|
+
return ctrl.wait()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
try:
|
|
52
|
+
sys.exit(main())
|
|
53
|
+
except Exception as error:
|
|
54
|
+
Path("bootstrap-error.txt").write_text(str(error)[:2048], encoding="utf-8")
|
|
55
|
+
raise
|