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,509 @@
|
|
|
1
|
+
"""Unwired, bounded part-export adapter. Importing this module never opens COM.
|
|
2
|
+
|
|
3
|
+
Caller must supply an already authorized, exclusively owned SW part document.
|
|
4
|
+
No application discovery, locking, configuration switching, saving or deployment.
|
|
5
|
+
Production MCP wiring remains outside this adapter; captures never authorize physics.
|
|
6
|
+
"""
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
import hashlib
|
|
9
|
+
import math
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import xml.etree.ElementTree as ET
|
|
12
|
+
|
|
13
|
+
from .physics import (mass_properties, sw_inertia_tensor, sw_transform, transform_mass,
|
|
14
|
+
merge_rigid_bodies, validate_rotation, _checked, _numbers)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _require_clean(model):
|
|
18
|
+
rebuild = model.Extension.NeedsRebuild2
|
|
19
|
+
if model.GetSaveFlag() is not False or type(rebuild) not in (int, bool) or rebuild != 0:
|
|
20
|
+
raise ValueError('DIRTY_REBUILD_REQUIRED_OR_UNKNOWN')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _source_state(model, expected, doc_type=1):
|
|
24
|
+
if model.GetType() != doc_type:
|
|
25
|
+
raise ValueError('DOCUMENT_TYPE_MISMATCH')
|
|
26
|
+
path = Path(model.GetPathName())
|
|
27
|
+
if not path.is_absolute() or not path.is_file() or path.resolve() != Path(expected['path']).resolve():
|
|
28
|
+
raise ValueError('SOURCE_PATH_MISMATCH')
|
|
29
|
+
_require_clean(model)
|
|
30
|
+
config = model.ConfigurationManager.ActiveConfiguration.Name
|
|
31
|
+
if not config or config != expected['configuration']:
|
|
32
|
+
raise ValueError('CONFIGURATION_MISMATCH')
|
|
33
|
+
with path.open('rb') as stream:
|
|
34
|
+
digest = hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
35
|
+
if digest != expected['sha256']:
|
|
36
|
+
raise ValueError('SOURCE_HASH_MISMATCH')
|
|
37
|
+
return {'path': str(path.resolve()), 'configuration': config, 'sha256': digest}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _component_state(component):
|
|
41
|
+
return {'instance_path': component.Name2, 'configuration': component.ReferencedConfiguration,
|
|
42
|
+
'source_file': component.GetPathName(), 'suppression_raw': component.GetSuppression2(),
|
|
43
|
+
'transform_raw': list(component.Transform2.ArrayData)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _mass_readout(mp, frame):
|
|
47
|
+
if mp is None: raise ValueError('MASS_PROPERTIES_UNAVAILABLE')
|
|
48
|
+
mp.UseSystemUnits = True
|
|
49
|
+
if mp.UseSystemUnits is not True: raise ValueError('SYSTEM_UNITS_NOT_CONFIRMED')
|
|
50
|
+
raw = {'mass': mp.Mass, 'center_of_mass': list(mp.CenterOfMass),
|
|
51
|
+
'moment': list(mp.GetMomentOfInertia(0)), 'user_assigned': mp.UserAssigned}
|
|
52
|
+
body = mass_properties(raw['mass'], raw['center_of_mass'], sw_inertia_tensor(raw['moment']),
|
|
53
|
+
length_unit='m', mass_unit='kg', frame=frame)
|
|
54
|
+
return raw, body
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def capture_context_component(model, component, *, expected_source, expected_component_source,
|
|
58
|
+
sw_version, provenance='unknown', max_triangles=100000,
|
|
59
|
+
invoke_bodies=None):
|
|
60
|
+
"""Read actual assembly-context solids (including assembly-cut user bodies).
|
|
61
|
+
|
|
62
|
+
AddBodies on the assembly mass object already accounts for component poses.
|
|
63
|
+
Face mesh coordinates are retained raw with UNKNOWN frame until real native
|
|
64
|
+
calibration; they cannot be fed to the part-local export path by accident.
|
|
65
|
+
invoke_bodies is the offline seam for GetBodies3's COM out argument only.
|
|
66
|
+
"""
|
|
67
|
+
if type(max_triangles) is not int or not 1 <= max_triangles <= 100000:
|
|
68
|
+
raise ValueError('triangle budget must be 1..100000')
|
|
69
|
+
if provenance not in ('unknown', 'fixture', 'real-sw-export') or not isinstance(sw_version, str) or not sw_version.strip():
|
|
70
|
+
raise ValueError('explicit version/provenance required')
|
|
71
|
+
source = _source_state(model, expected_source, 2)
|
|
72
|
+
before = _component_state(component)
|
|
73
|
+
if type(before['suppression_raw']) is not int or before['suppression_raw'] not in (2,3):
|
|
74
|
+
raise ValueError('COMPONENT_NOT_RESOLVED')
|
|
75
|
+
sw_transform(before['transform_raw'])
|
|
76
|
+
if model.SelectionManager.GetSelectedObjectCount2(-1) != 0:
|
|
77
|
+
raise ValueError('SELECTION_MUST_BE_EMPTY')
|
|
78
|
+
path = Path(before['source_file'])
|
|
79
|
+
if not path.is_absolute() or path.resolve() != Path(expected_component_source['path']).resolve() or before['configuration'] != expected_component_source['configuration']:
|
|
80
|
+
raise ValueError('COMPONENT_SOURCE_MISMATCH')
|
|
81
|
+
child_doc = component.GetModelDoc2()
|
|
82
|
+
if child_doc is None or child_doc.GetType() != 1 or Path(child_doc.GetPathName()).resolve() != path.resolve():
|
|
83
|
+
raise ValueError('COMPONENT_DOCUMENT_UNAVAILABLE_OR_MISMATCH')
|
|
84
|
+
# Referenced assembly configuration may differ from the part's active one;
|
|
85
|
+
# do not switch it, but never attribute unsaved geometry to a saved file hash.
|
|
86
|
+
_require_clean(child_doc)
|
|
87
|
+
def check_child_file():
|
|
88
|
+
with path.open('rb') as stream:
|
|
89
|
+
if hashlib.file_digest(stream, 'sha256').hexdigest() != expected_component_source['sha256']:
|
|
90
|
+
raise ValueError('COMPONENT_SOURCE_HASH_MISMATCH')
|
|
91
|
+
check_child_file()
|
|
92
|
+
if invoke_bodies is None:
|
|
93
|
+
def invoke_bodies(comp, kind):
|
|
94
|
+
import pythoncom
|
|
95
|
+
from win32com.client import VARIANT
|
|
96
|
+
info = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_VARIANT, None)
|
|
97
|
+
bodies = comp.GetBodies3(kind, info)
|
|
98
|
+
return bodies, info.value
|
|
99
|
+
solids, info = invoke_bodies(component, 0)
|
|
100
|
+
surfaces, _ = invoke_bodies(component, 1)
|
|
101
|
+
if not isinstance(surfaces, (list, tuple)):
|
|
102
|
+
raise ValueError('UNKNOWN_CONTEXT_SURFACE_QUERY')
|
|
103
|
+
if surfaces or not isinstance(solids, (list,tuple)) or not 1 <= len(solids) <= 10000:
|
|
104
|
+
raise ValueError('MISSING_SURFACE_OR_OVER_BUDGET_CONTEXT_BODIES')
|
|
105
|
+
if not isinstance(info, (list,tuple)) or len(info) != len(solids) or any(type(x) is not int for x in info):
|
|
106
|
+
raise ValueError('UNKNOWN_CONTEXT_BODY_INFO')
|
|
107
|
+
mp = model.Extension.CreateMassProperty()
|
|
108
|
+
if mp is None or mp.AddBodies(tuple(solids)) is not True:
|
|
109
|
+
raise ValueError('CONTEXT_BODIES_NOT_ACCEPTED')
|
|
110
|
+
raw, physics = _mass_readout(mp, 'root-assembly')
|
|
111
|
+
vertices, face_count = [], 0
|
|
112
|
+
for body in solids:
|
|
113
|
+
faces = body.GetFaces()
|
|
114
|
+
if not isinstance(faces, (list,tuple)) or not faces:
|
|
115
|
+
raise ValueError('MISSING_CONTEXT_FACES')
|
|
116
|
+
face_count += len(faces)
|
|
117
|
+
if face_count > 10000: raise ValueError('CONTEXT_FACE_BUDGET')
|
|
118
|
+
for face in faces:
|
|
119
|
+
points = _triangles(face.GetTessTriangles(True), max_triangles)
|
|
120
|
+
if len(vertices)+len(points) > 9*max_triangles:
|
|
121
|
+
raise ValueError('CONTEXT_TESSELLATION_BUDGET')
|
|
122
|
+
vertices.extend(points)
|
|
123
|
+
check_child_file()
|
|
124
|
+
_require_clean(child_doc)
|
|
125
|
+
if _component_state(component) != before or _source_state(model, expected_source, 2) != source:
|
|
126
|
+
raise ValueError('CONTEXT_CHANGED_DURING_CAPTURE')
|
|
127
|
+
return {'source':source, 'component_source':deepcopy(expected_component_source), 'component':before,
|
|
128
|
+
'source_revision':source['sha256'], 'provenance':provenance, 'sw_version':sw_version,
|
|
129
|
+
'physics':physics, 'raw_mass_properties':raw, 'body_info_raw':list(info),
|
|
130
|
+
'vertices_raw_system_units':vertices, 'mesh_frame':'unknown', 'ready_for_physics':False,
|
|
131
|
+
'methods':{'bodies':'IComponent2.GetBodies3', 'mass':'IMassProperty.AddBodies/GetMomentOfInertia(0)',
|
|
132
|
+
'mesh':'IFace2.GetTessTriangles(True)', 'component_transform_applied_to_mass':False},
|
|
133
|
+
'missing_information':['installed COM marshaling and user-body density calibration',
|
|
134
|
+
'context face mesh frame and geometric tolerance', 'collision coverage']}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def capture_assembly(model, *, expected_source, sw_version, provenance='unknown',
|
|
138
|
+
max_components=256, max_triangles=100000, generation=None,
|
|
139
|
+
capture_mode='standalone', invoke_bodies=None):
|
|
140
|
+
"""Capture occurrence poses and either standalone or assembly-context evidence.
|
|
141
|
+
|
|
142
|
+
All occurrences, including failed/unsupported ones, are retained. Containers
|
|
143
|
+
carry no mass to avoid double counting. Source-file deduplication is forbidden.
|
|
144
|
+
Context mode binds AddBodies mass to the same native occurrence and A02
|
|
145
|
+
reference. Its mesh frame remains unknown; standalone equivalence is unproved.
|
|
146
|
+
Neither capture mode can authorize a physics run or CAD write.
|
|
147
|
+
"""
|
|
148
|
+
if type(max_components) is not int or not 1 <= max_components <= 1000:
|
|
149
|
+
raise ValueError('component budget must be 1..1000')
|
|
150
|
+
if type(max_triangles) is not int or not 1 <= max_triangles <= 100000:
|
|
151
|
+
raise ValueError('triangle budget must be 1..100000')
|
|
152
|
+
if provenance not in ('unknown', 'fixture', 'real-sw-export') or not isinstance(sw_version, str) or not sw_version.strip():
|
|
153
|
+
raise ValueError('explicit version/provenance required')
|
|
154
|
+
if capture_mode not in ('standalone', 'context'):
|
|
155
|
+
raise ValueError('INVALID_CAPTURE_MODE')
|
|
156
|
+
if capture_mode == 'context' and generation is None:
|
|
157
|
+
raise ValueError('CONTEXT_CAPTURE_REQUIRES_GENERATION')
|
|
158
|
+
source = _source_state(model, expected_source, 2)
|
|
159
|
+
snapshot_context = {'source_revision':source['sha256'], 'configuration':source['configuration'],
|
|
160
|
+
'generation':generation}
|
|
161
|
+
native = model.GetComponents(False)
|
|
162
|
+
if not isinstance(native, (list, tuple)) or not 1 <= len(native) <= max_components:
|
|
163
|
+
raise ValueError('MISSING_OR_OVER_BUDGET_COMPONENTS')
|
|
164
|
+
references, root_mates = None, None
|
|
165
|
+
if generation is not None:
|
|
166
|
+
if type(generation) is not int or generation < 0:
|
|
167
|
+
raise ValueError('INVALID_GENERATION')
|
|
168
|
+
from .assembly import capture_reference, capture_mate_features
|
|
169
|
+
references = [capture_reference(model.Extension, component) for component in native]
|
|
170
|
+
if len(set(references)) != len(references):
|
|
171
|
+
raise ValueError('DUPLICATE_COMPONENT_REFERENCE')
|
|
172
|
+
root_mates = capture_mate_features(model, owning_frame='root-assembly',
|
|
173
|
+
configuration=source['configuration'])
|
|
174
|
+
records, captured_docs, captured_components, total = [], [], [], 0
|
|
175
|
+
for index, component in enumerate(native):
|
|
176
|
+
record = {'id': f'occurrence-{index}', 'identity_status': 'snapshot-local-only',
|
|
177
|
+
'persistent_reference': None, 'status': 'unknown'}
|
|
178
|
+
if references is not None:
|
|
179
|
+
record.update(persistent_reference=references[index], identity_status='native-reference-unresolved',
|
|
180
|
+
owner_document=source['path'], owner_configuration=source['configuration'],
|
|
181
|
+
snapshot_context=deepcopy(snapshot_context))
|
|
182
|
+
try:
|
|
183
|
+
record.update(instance_path=component.Name2, configuration=component.ReferencedConfiguration,
|
|
184
|
+
source_file=component.GetPathName(), suppression_raw=component.GetSuppression2())
|
|
185
|
+
if type(record['suppression_raw']) is not int or record['suppression_raw'] not in (2, 3):
|
|
186
|
+
raise ValueError('COMPONENT_NOT_RESOLVED')
|
|
187
|
+
raw_transform = list(component.Transform2.ArrayData)
|
|
188
|
+
r, t = sw_transform(raw_transform)
|
|
189
|
+
record.update(rotation=r, translation_m=t, transform_raw=raw_transform,
|
|
190
|
+
transform_frame='root-assembly', transform_method='IComponent2.Transform2')
|
|
191
|
+
captured_components.append((component, {key:deepcopy(record[key]) for key in
|
|
192
|
+
('instance_path','configuration','source_file','suppression_raw','transform_raw')}))
|
|
193
|
+
doc = component.GetModelDoc2()
|
|
194
|
+
if doc is None: raise ValueError('COMPONENT_DOCUMENT_UNAVAILABLE')
|
|
195
|
+
if doc.GetType() == 2:
|
|
196
|
+
record['status'] = 'container-no-mass'
|
|
197
|
+
elif doc.GetType() == 1:
|
|
198
|
+
child_path = Path(record['source_file'])
|
|
199
|
+
if not child_path.is_absolute(): raise ValueError('UNSAVED_OR_VIRTUAL_COMPONENT')
|
|
200
|
+
with child_path.open('rb') as stream:
|
|
201
|
+
digest = hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
202
|
+
expected = {'path': str(child_path), 'sha256': digest, 'configuration': record['configuration']}
|
|
203
|
+
if capture_mode == 'context':
|
|
204
|
+
# Referenced configuration belongs to the occurrence, not the active part document.
|
|
205
|
+
active_expected = dict(expected, configuration=doc.ConfigurationManager.ActiveConfiguration.Name)
|
|
206
|
+
_source_state(doc, active_expected)
|
|
207
|
+
snap = capture_context_component(model, component, expected_source=expected_source,
|
|
208
|
+
expected_component_source=expected, sw_version=sw_version, provenance=provenance,
|
|
209
|
+
max_triangles=max_triangles, invoke_bodies=invoke_bodies)
|
|
210
|
+
total += len(snap['vertices_raw_system_units'])//9
|
|
211
|
+
record.update(capture=snap, status='captured-context-mesh-frame-unknown')
|
|
212
|
+
captured_docs.append((doc, active_expected))
|
|
213
|
+
else:
|
|
214
|
+
snap = capture_part(doc, expected_source=expected, sw_version=sw_version,
|
|
215
|
+
provenance=provenance, max_triangles=max_triangles)
|
|
216
|
+
total += len(snap['vertices_m'])//9
|
|
217
|
+
record.update(capture=snap, status='captured-standalone-part-not-context-validated')
|
|
218
|
+
captured_docs.append((doc, expected))
|
|
219
|
+
if total > max_triangles: raise ValueError('ASSEMBLY_TESSELLATION_BUDGET')
|
|
220
|
+
else:
|
|
221
|
+
raise ValueError('UNSUPPORTED_COMPONENT_DOCUMENT_TYPE')
|
|
222
|
+
except Exception as error:
|
|
223
|
+
record['status'] = 'unsupported'
|
|
224
|
+
record['error'] = type(error).__name__ + ': ' + str(error)
|
|
225
|
+
records.append(record)
|
|
226
|
+
if total > max_triangles:
|
|
227
|
+
raise ValueError('ASSEMBLY_TESSELLATION_BUDGET')
|
|
228
|
+
for doc, expected in captured_docs:
|
|
229
|
+
_source_state(doc, expected)
|
|
230
|
+
for component, expected in captured_components:
|
|
231
|
+
if _component_state(component) != expected:
|
|
232
|
+
raise ValueError('COMPONENT_CHANGED_DURING_CAPTURE')
|
|
233
|
+
if references is not None:
|
|
234
|
+
# Re-enumerate: checking only the original objects misses inserted/removed occurrences.
|
|
235
|
+
after = model.GetComponents(False)
|
|
236
|
+
if not isinstance(after, (list, tuple)) or len(after) != len(native):
|
|
237
|
+
raise ValueError('COMPONENT_SET_CHANGED')
|
|
238
|
+
after_refs = [capture_reference(model.Extension, component) for component in after]
|
|
239
|
+
if len(set(after_refs)) != len(after_refs) or set(after_refs) != set(references):
|
|
240
|
+
raise ValueError('COMPONENT_SET_CHANGED')
|
|
241
|
+
if [capture_reference(model.Extension, component) for component in native] != references:
|
|
242
|
+
raise ValueError('COMPONENT_REFERENCE_CHANGED')
|
|
243
|
+
if capture_mate_features(model, owning_frame='root-assembly', configuration=source['configuration']) != root_mates:
|
|
244
|
+
raise ValueError('MATES_CHANGED_DURING_CAPTURE')
|
|
245
|
+
if _source_state(model, expected_source, 2) != source:
|
|
246
|
+
raise ValueError('ASSEMBLY_CHANGED_DURING_CAPTURE')
|
|
247
|
+
return {'source': source, 'source_revision': source['sha256'], 'sw_version': sw_version,
|
|
248
|
+
'provenance': provenance, 'components': records, 'ready_for_physics': False,
|
|
249
|
+
'snapshot_context':snapshot_context, 'capture_mode':capture_mode,
|
|
250
|
+
'root_mates':root_mates, 'mates_complete':False,
|
|
251
|
+
'missing_information': ['A02 persistent occurrence identity and complete mate solution',
|
|
252
|
+
'assembly-context geometry equivalence', 'validated collisions',
|
|
253
|
+
'real SW and Webots calibration']}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _triangles(raw, max_triangles):
|
|
257
|
+
if not isinstance(raw, (list, tuple)) or not raw or len(raw) % 9 or len(raw)//9 > max_triangles:
|
|
258
|
+
raise ValueError('MISSING_INVALID_OR_OVER_BUDGET_TESSELLATION')
|
|
259
|
+
vertices = _numbers(raw, len(raw))
|
|
260
|
+
for i in range(0, len(vertices), 9):
|
|
261
|
+
a, b, c = (vertices[i+k:i+k+3] for k in (0,3,6))
|
|
262
|
+
u, v = ([b[k]-a[k] for k in range(3)], [c[k]-a[k] for k in range(3)])
|
|
263
|
+
normal = [u[1]*v[2]-u[2]*v[1], u[2]*v[0]-u[0]*v[2], u[0]*v[1]-u[1]*v[0]]
|
|
264
|
+
if not all(math.isfinite(x) for x in normal) or not any(normal):
|
|
265
|
+
raise ValueError('DEGENERATE_TESSELLATION')
|
|
266
|
+
return vertices
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def capture_part(model, *, expected_source, sw_version, provenance='unknown', max_triangles=100000):
|
|
270
|
+
"""Capture current saved part via named APIs, with source checks on both sides.
|
|
271
|
+
|
|
272
|
+
Tests use fake objects. Real SW call signatures/geometry must be calibrated
|
|
273
|
+
before this adapter is connected to a live host. No fallback to old arrays.
|
|
274
|
+
"""
|
|
275
|
+
if type(max_triangles) is not int or not 1 <= max_triangles <= 100000:
|
|
276
|
+
raise ValueError('triangle budget must be 1..100000')
|
|
277
|
+
if provenance not in ('unknown', 'fixture', 'real-sw-export') or not isinstance(sw_version, str) or not sw_version.strip():
|
|
278
|
+
raise ValueError('explicit version/provenance required')
|
|
279
|
+
source = _source_state(model, expected_source)
|
|
280
|
+
if model.SelectionManager.GetSelectedObjectCount2(-1) != 0:
|
|
281
|
+
raise ValueError('SELECTION_MUST_BE_EMPTY: will not clear user selection')
|
|
282
|
+
solids = model.GetBodies2(0, False)
|
|
283
|
+
visible = model.GetBodies2(0, True)
|
|
284
|
+
surfaces = model.GetBodies2(1, False)
|
|
285
|
+
if any(not isinstance(value, (list, tuple)) for value in (solids, visible, surfaces)):
|
|
286
|
+
raise ValueError('UNKNOWN_PART_BODY_QUERY')
|
|
287
|
+
if not solids or len(solids) != len(visible) or surfaces:
|
|
288
|
+
raise ValueError('HIDDEN_SURFACE_OR_MISSING_BODIES_UNSUPPORTED')
|
|
289
|
+
count = model.GetTessTriangleCount()
|
|
290
|
+
if type(count) is not int or not 1 <= count <= max_triangles:
|
|
291
|
+
raise ValueError('TESSELLATION_BUDGET')
|
|
292
|
+
mp = model.Extension.CreateMassProperty()
|
|
293
|
+
raw, body = _mass_readout(mp, 'part-local')
|
|
294
|
+
vertices = _triangles(model.GetTessTriangles(True), max_triangles)
|
|
295
|
+
if len(vertices)//9 != count: raise ValueError('TESSELLATION_COUNT_CHANGED')
|
|
296
|
+
if _source_state(model, expected_source) != source:
|
|
297
|
+
raise ValueError('SOURCE_CHANGED_DURING_CAPTURE')
|
|
298
|
+
return {'source': source, 'source_revision': source['sha256'], 'sw_version': sw_version,
|
|
299
|
+
'provenance': provenance, 'physics': body, 'vertices_m': vertices,
|
|
300
|
+
'raw_mass_properties': raw,
|
|
301
|
+
'methods': {'mass': 'IModelDocExtension.CreateMassProperty/IMassProperty',
|
|
302
|
+
'inertia': 'IMassProperty.GetMomentOfInertia(0)',
|
|
303
|
+
'mesh': 'IPartDoc.GetTessTriangles(True)', 'UseSystemUnits': True}}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def part_bundle(snapshot):
|
|
307
|
+
"""Return URDF + local-metre OBJ and S04 data payloads, without writing files.
|
|
308
|
+
|
|
309
|
+
Visual tessellation is never silently promoted to a validated collision body.
|
|
310
|
+
This bundle alone is therefore NOT ready for physical simulation.
|
|
311
|
+
"""
|
|
312
|
+
body = _checked(snapshot['physics'], snapshot['physics']['frame'])
|
|
313
|
+
vertices = _triangles(snapshot['vertices_m'], 100000)
|
|
314
|
+
number = lambda x: format(x, '.17g')
|
|
315
|
+
xyz = lambda v: ' '.join(map(number, v))
|
|
316
|
+
root = ET.Element('robot', name='sw_part')
|
|
317
|
+
link = ET.SubElement(root, 'link', name='part')
|
|
318
|
+
inertial = ET.SubElement(link, 'inertial')
|
|
319
|
+
ET.SubElement(inertial, 'origin', xyz=xyz(body['com_m']), rpy='0 0 0')
|
|
320
|
+
ET.SubElement(inertial, 'mass', value=number(body['mass_kg']))
|
|
321
|
+
a = body['inertia_kg_m2']
|
|
322
|
+
ET.SubElement(inertial, 'inertia', **{name: number(a[i][j]) for name,i,j in
|
|
323
|
+
[('ixx',0,0),('ixy',0,1),('ixz',0,2),('iyy',1,1),('iyz',1,2),('izz',2,2)]})
|
|
324
|
+
visual = ET.SubElement(link, 'visual')
|
|
325
|
+
ET.SubElement(visual, 'origin', xyz='0 0 0', rpy='0 0 0')
|
|
326
|
+
ET.SubElement(ET.SubElement(visual, 'geometry'), 'mesh', filename='visual.obj', scale='1 1 1')
|
|
327
|
+
obj = ['# SW display tessellation, metres, part-local; collision unvalidated']
|
|
328
|
+
obj += ['v '+xyz(vertices[i:i+3]) for i in range(0,len(vertices),3)]
|
|
329
|
+
obj += [f'f {i+1} {i+2} {i+3}' for i in range(0,len(vertices)//3,3)]
|
|
330
|
+
missing = ['real SW API calibration', 'validated collision geometry and coverage',
|
|
331
|
+
'urdf2webots conversion and precision audit', 'real Webots physics']
|
|
332
|
+
return {'robot.urdf': ET.tostring(root, encoding='unicode'), 'visual.obj': '\n'.join(obj)+'\n',
|
|
333
|
+
'vertices_m': vertices,
|
|
334
|
+
'manifest_data': {'source': deepcopy(snapshot['source']),
|
|
335
|
+
'capture_provenance': snapshot.get('provenance', 'unknown'),
|
|
336
|
+
'capture_methods': deepcopy(snapshot.get('methods', {})),
|
|
337
|
+
'raw_mass_properties': deepcopy(snapshot['raw_mass_properties']),
|
|
338
|
+
'components': [{'id': 'part', 'persistent_reference': None,
|
|
339
|
+
'identity_status': 'unknown'}],
|
|
340
|
+
'rigid_bodies': [body], 'joints': []},
|
|
341
|
+
'conversion_data': {'inputs': [deepcopy(snapshot['source'])],
|
|
342
|
+
'tools': [{'name': 'SW', 'version': snapshot['sw_version']}],
|
|
343
|
+
'mappings': [{'target': {'kind': 'urdf_link', 'id': 'part'}, 'component_ids': ['part']}],
|
|
344
|
+
'approximations': ['display tessellation; error not calibrated'],
|
|
345
|
+
'omissions': ['collision geometry'], 'support': 'approximate',
|
|
346
|
+
'physical_validation': 'not_run', 'missing_information': missing}}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def rigid_group_bundle(components, *, frame, source_frame='part-local'):
|
|
350
|
+
"""Export one explicitly fixed group; input captures are component-local.
|
|
351
|
+
|
|
352
|
+
Each member: id, capture, rotation, translation_m (source frame to output link).
|
|
353
|
+
root-assembly mode requires independently established mesh_frame evidence;
|
|
354
|
+
native context captures currently emit unknown and are rejected.
|
|
355
|
+
Caller must establish fixed membership and assembly-context geometry
|
|
356
|
+
equivalence. This pure function does not infer either from part filenames.
|
|
357
|
+
"""
|
|
358
|
+
if source_frame not in ('part-local', 'root-assembly'):
|
|
359
|
+
raise ValueError('UNSUPPORTED_GROUP_SOURCE_FRAME')
|
|
360
|
+
members = list(components)
|
|
361
|
+
bodies, vertices, sources = [], [], []
|
|
362
|
+
ids = [m['id'] for m in members]
|
|
363
|
+
if not ids or any(not isinstance(i, str) or not i.strip() for i in ids) or len(set(ids)) != len(ids):
|
|
364
|
+
raise ValueError('unique component ids required')
|
|
365
|
+
for member in members:
|
|
366
|
+
snap = member['capture']
|
|
367
|
+
if member.get('transform_frame', frame) != frame:
|
|
368
|
+
raise ValueError('COMPONENT_OUTPUT_FRAME_MISMATCH')
|
|
369
|
+
r, t = validate_rotation(member['rotation']), _numbers(member['translation_m'], 3)
|
|
370
|
+
body = transform_mass(snap['physics'], r, t, source_frame=source_frame, target_frame=frame)
|
|
371
|
+
if source_frame == 'root-assembly':
|
|
372
|
+
if snap.get('mesh_frame') != 'root-assembly':
|
|
373
|
+
raise ValueError('CONTEXT_MESH_FRAME_UNPROVEN')
|
|
374
|
+
raw = _triangles(snap['vertices_raw_system_units'], 100000)
|
|
375
|
+
else:
|
|
376
|
+
raw = _triangles(snap['vertices_m'], 100000)
|
|
377
|
+
bodies.append((member['id'], body))
|
|
378
|
+
if len(vertices) + len(raw) > 900000:
|
|
379
|
+
raise ValueError('GROUP_TESSELLATION_BUDGET')
|
|
380
|
+
for offset in range(0, len(raw), 3):
|
|
381
|
+
vertices.extend(sum(r[i][j]*raw[offset+j] for j in range(3))+t[i] for i in range(3))
|
|
382
|
+
sources.append(deepcopy(snap['source']))
|
|
383
|
+
merged = merge_rigid_bodies(bodies, frame=frame)
|
|
384
|
+
combined = {'physics': merged, 'vertices_m': vertices, 'source': {'members': sources},
|
|
385
|
+
'raw_mass_properties': [deepcopy(m['capture']['raw_mass_properties']) for m in members],
|
|
386
|
+
'sw_version': members[0]['capture']['sw_version']}
|
|
387
|
+
bundle = part_bundle(combined)
|
|
388
|
+
bundle['manifest_data']['rigid_bodies'] = [merged]
|
|
389
|
+
bundle['manifest_data']['components'] = [
|
|
390
|
+
{'id': m['id'], 'source': deepcopy(m['capture']['source']),
|
|
391
|
+
'provenance': m['capture']['provenance'], 'methods': deepcopy(m['capture']['methods']),
|
|
392
|
+
'rotation': deepcopy(m['rotation']), 'translation_m': deepcopy(m['translation_m']),
|
|
393
|
+
'source_transform': deepcopy(m.get('source_transform')),
|
|
394
|
+
'persistent_reference': None, 'identity_status': 'unknown'} for m in members]
|
|
395
|
+
for exported, member in zip(bundle['manifest_data']['components'], members):
|
|
396
|
+
for key in ('persistent_reference', 'identity_status', 'owner_document', 'owner_configuration',
|
|
397
|
+
'instance_path', 'configuration', 'snapshot_context'):
|
|
398
|
+
if key in member:
|
|
399
|
+
exported[key] = deepcopy(member[key])
|
|
400
|
+
bundle['conversion_data']['inputs'] = sources
|
|
401
|
+
bundle['conversion_data']['tools'] = [{'name': 'SW', 'version': v} for v in
|
|
402
|
+
sorted({m['capture']['sw_version'] for m in members})]
|
|
403
|
+
bundle['conversion_data']['mappings'] = [{'target': {'kind': 'urdf_link', 'id': 'part'},
|
|
404
|
+
'component_ids': ids}]
|
|
405
|
+
bundle['conversion_data']['missing_information'].append('fixed-group proof and assembly-context geometry equivalence')
|
|
406
|
+
return bundle
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def reframe_component(component, *, link_rotation, link_translation_m, owning_frame, link_frame):
|
|
410
|
+
"""Root-relative component and link poses -> component pose in link frame.
|
|
411
|
+
|
|
412
|
+
Both input poses must refer to the same captured assembly-root frame. Mesh
|
|
413
|
+
and physics remain untouched until rigid_group_bundle applies this pose once.
|
|
414
|
+
"""
|
|
415
|
+
if (not isinstance(owning_frame, str) or not owning_frame.strip() or
|
|
416
|
+
component.get('transform_frame') != owning_frame or
|
|
417
|
+
not isinstance(link_frame, str) or not link_frame.startswith('link:') or len(link_frame) == 5):
|
|
418
|
+
raise ValueError('COMPONENT_LINK_FRAME_MISMATCH')
|
|
419
|
+
rc = validate_rotation(component['rotation'])
|
|
420
|
+
tc = _numbers(component['translation_m'], 3)
|
|
421
|
+
rl = validate_rotation(link_rotation)
|
|
422
|
+
tl = _numbers(link_translation_m, 3)
|
|
423
|
+
result = dict(component)
|
|
424
|
+
result['source_transform'] = {'frame': owning_frame, 'rotation': deepcopy(rc), 'translation_m': list(tc),
|
|
425
|
+
'raw': deepcopy(result.pop('transform_raw', None))}
|
|
426
|
+
result['transform_frame'] = link_frame
|
|
427
|
+
result['rotation'] = [[sum(rl[k][i]*rc[k][j] for k in range(3)) for j in range(3)] for i in range(3)]
|
|
428
|
+
result['translation_m'] = [sum(rl[k][i]*(tc[k]-tl[k]) for k in range(3)) for i in range(3)]
|
|
429
|
+
return result
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def jointed_bundle(links, joints):
|
|
433
|
+
"""Serialize A02-validated joints and link-local rigid-group bundles.
|
|
434
|
+
|
|
435
|
+
A02 remains the single tree/joint validator. Before I01 its module must be
|
|
436
|
+
supplied from that worktree for source-only seam tests. Never reads external
|
|
437
|
+
XML/OBJ: regenerates mesh and inertial XML from the validated numeric payload.
|
|
438
|
+
"""
|
|
439
|
+
from .assembly import validate_joint, validate_tree
|
|
440
|
+
if not isinstance(links, (list, tuple)) or not 1 <= len(links) <= 256:
|
|
441
|
+
raise ValueError('LINK_BUDGET')
|
|
442
|
+
ids = [link['id'] for link in links]
|
|
443
|
+
checked_joints = [validate_joint(j) for j in joints]
|
|
444
|
+
root_id = validate_tree(ids, checked_joints)
|
|
445
|
+
robot = ET.Element('robot', name='sw_assembly')
|
|
446
|
+
files, components, bodies, mappings, inputs, raw_mass, seen = {}, [], [], [], [], [], set()
|
|
447
|
+
audit = {key: [] for key in ('tools', 'omissions', 'approximations', 'missing_information')}
|
|
448
|
+
vertex_count = 0
|
|
449
|
+
for index, link in enumerate(links):
|
|
450
|
+
name, bundle = link['id'], link['bundle']
|
|
451
|
+
data = bundle['manifest_data']
|
|
452
|
+
if len(data['rigid_bodies']) != 1:
|
|
453
|
+
raise ValueError('ONE_RIGID_BODY_PER_LINK_REQUIRED')
|
|
454
|
+
original = data['rigid_bodies'][0]
|
|
455
|
+
body = _checked(original, 'link:'+name)
|
|
456
|
+
member_ids = original.get('component_ids')
|
|
457
|
+
member_records = data['components']
|
|
458
|
+
if (not isinstance(member_ids, list) or not member_ids or
|
|
459
|
+
any(not isinstance(cid, str) or not cid.strip() for cid in member_ids) or
|
|
460
|
+
len(set(member_ids)) != len(member_ids) or set(member_ids) & seen or
|
|
461
|
+
[c['id'] for c in member_records] != member_ids):
|
|
462
|
+
raise ValueError('INVALID_OR_REUSED_RIGID_MEMBERS')
|
|
463
|
+
seen.update(member_ids)
|
|
464
|
+
body['component_ids'] = list(member_ids)
|
|
465
|
+
body['id'] = name
|
|
466
|
+
vertices = _triangles(bundle['vertices_m'], 100000)
|
|
467
|
+
vertex_count += len(vertices)
|
|
468
|
+
if vertex_count > 900000:
|
|
469
|
+
raise ValueError('ASSEMBLY_TESSELLATION_BUDGET')
|
|
470
|
+
fresh = part_bundle({'physics':body, 'vertices_m':vertices, 'source':data['source'],
|
|
471
|
+
'raw_mass_properties':data['raw_mass_properties'], 'sw_version':'see-inputs'})
|
|
472
|
+
element = ET.fromstring(fresh['robot.urdf']).find('link')
|
|
473
|
+
element.set('name', name)
|
|
474
|
+
mesh_path = f'meshes/link_{index}.obj'
|
|
475
|
+
element.find('visual/geometry/mesh').set('filename', mesh_path)
|
|
476
|
+
robot.append(element)
|
|
477
|
+
files[mesh_path] = fresh['visual.obj']
|
|
478
|
+
components.extend(deepcopy(member_records)); bodies.append(body)
|
|
479
|
+
inputs.extend(deepcopy(bundle['conversion_data']['inputs']))
|
|
480
|
+
for key, values in audit.items():
|
|
481
|
+
# ponytail: bounded 256-link audit lists; use canonical keys if this budget grows.
|
|
482
|
+
for value in bundle['conversion_data'].get(key, []):
|
|
483
|
+
if value not in values: values.append(deepcopy(value))
|
|
484
|
+
raw_mass.append(deepcopy(data['raw_mass_properties']))
|
|
485
|
+
mappings.append({'target':{'kind':'urdf_link','id':name}, 'component_ids':list(member_ids)})
|
|
486
|
+
number = lambda x: format(x, '.17g')
|
|
487
|
+
xyz = lambda v: ' '.join(map(number, v))
|
|
488
|
+
for j in checked_joints:
|
|
489
|
+
element = ET.SubElement(robot, 'joint', name=j['id'], type=j['type'])
|
|
490
|
+
ET.SubElement(element, 'parent', link=j['parent'])
|
|
491
|
+
ET.SubElement(element, 'child', link=j['child'])
|
|
492
|
+
ET.SubElement(element, 'origin', xyz=xyz(j['origin_xyz_m']), rpy=xyz(j['origin_rpy_rad']))
|
|
493
|
+
if j['type'] != 'fixed':
|
|
494
|
+
ET.SubElement(element, 'axis', xyz=xyz(j['axis']))
|
|
495
|
+
ET.SubElement(element, 'limit', **{k:number(j['limits'][k]) for k in ('lower','upper','effort','velocity')})
|
|
496
|
+
files['robot.urdf'] = ET.tostring(robot, encoding='unicode')
|
|
497
|
+
files['manifest_data'] = {'source':{'inputs':inputs}, 'components':components,
|
|
498
|
+
'rigid_bodies':bodies, 'joints':checked_joints, 'raw_mass_properties':raw_mass}
|
|
499
|
+
files['conversion_data'] = {'inputs':inputs, 'root_link':root_id, 'mappings':mappings,
|
|
500
|
+
'support':'approximate', 'approximations':['display tessellation; error not calibrated'],
|
|
501
|
+
'omissions':['collision geometry'], 'physical_validation':'not_run',
|
|
502
|
+
'missing_information':['trusted source snapshot and full mate completeness',
|
|
503
|
+
'assembly-context geometry calibration', 'collision coverage',
|
|
504
|
+
'converter precision and real SW/Webots validation']}
|
|
505
|
+
for key, values in audit.items():
|
|
506
|
+
target = files['conversion_data'].setdefault(key, [])
|
|
507
|
+
for value in values:
|
|
508
|
+
if value not in target: target.append(value)
|
|
509
|
+
return files
|