adxc 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adxc/__init__.py +103 -0
- adxc/cell.py +558 -0
- adxc/fields.py +427 -0
- adxc/library.py +161 -0
- adxc/nozzle.py +107 -0
- adxc/nozzles.py +3 -0
- adxc/protobuf.py +129 -0
- adxc/translator.py +663 -0
- adxc-0.1.0.dist-info/METADATA +117 -0
- adxc-0.1.0.dist-info/RECORD +13 -0
- adxc-0.1.0.dist-info/WHEEL +5 -0
- adxc-0.1.0.dist-info/licenses/LICENSE +21 -0
- adxc-0.1.0.dist-info/top_level.txt +1 -0
adxc/__init__.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import zipfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
|
|
7
|
+
from .fields import SensorField
|
|
8
|
+
from .library import Robot, Track, Gantry, Positioner, Process, WireArcMode, SensorShape, Sensor
|
|
9
|
+
from .cell import Cell, _build_adxc, _uri_to_path
|
|
10
|
+
from .nozzle import Nozzle
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _validate_meshes(cell):
|
|
14
|
+
objects = list(cell.robot.heads if cell.robot is not None else ())
|
|
15
|
+
objects.extend(cell.unknown_pending_heads)
|
|
16
|
+
objects.extend(cell.cell_elements)
|
|
17
|
+
for obj in objects:
|
|
18
|
+
filepath = getattr(obj, "filepath", "")
|
|
19
|
+
if filepath:
|
|
20
|
+
source = _uri_to_path(filepath)
|
|
21
|
+
if not source.is_file():
|
|
22
|
+
raise FileNotFoundError(f"Mesh file not found: {source}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def save(value, output_dir="."):
|
|
26
|
+
"""Save one cell or nozzle using a default path in the current directory."""
|
|
27
|
+
output_dir = Path(output_dir)
|
|
28
|
+
if isinstance(value, Cell):
|
|
29
|
+
_validate_meshes(value)
|
|
30
|
+
file_stem = value.name.replace(" ", "_")
|
|
31
|
+
output_dir = output_dir / value.name
|
|
32
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
path = output_dir / f"{file_stem}.adxc"
|
|
34
|
+
path.write_bytes(_build_adxc(value))
|
|
35
|
+
return path
|
|
36
|
+
if isinstance(value, Nozzle):
|
|
37
|
+
path = output_dir if output_dir.suffix == ".adxn" else output_dir / value.filename
|
|
38
|
+
return value.write(path)
|
|
39
|
+
raise TypeError("value must be a Cell or Nozzle")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def savezip(value, output_dir="."):
|
|
43
|
+
"""Save a cell or one or more nozzles as an archive."""
|
|
44
|
+
output_dir = Path(output_dir)
|
|
45
|
+
|
|
46
|
+
if isinstance(value, Cell):
|
|
47
|
+
|
|
48
|
+
_validate_meshes(value)
|
|
49
|
+
cell = copy.deepcopy(value)
|
|
50
|
+
resources = {}
|
|
51
|
+
for obj in ([*cell.robot.heads] if cell.robot is not None else []) + cell.unknown_pending_heads + cell.cell_elements:
|
|
52
|
+
if obj.filepath:
|
|
53
|
+
source = _uri_to_path(obj.filepath)
|
|
54
|
+
resources[source.name] = source
|
|
55
|
+
obj.filepath = f"./resource/{source.name}"
|
|
56
|
+
stem = value.name
|
|
57
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
zip_path = output_dir / f"{stem}.adxczip"
|
|
59
|
+
|
|
60
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
61
|
+
folder = Path(tmp) / stem
|
|
62
|
+
resource_folder = folder / "resource"
|
|
63
|
+
resource_folder.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
file_stem = stem.replace(" ", "_")
|
|
65
|
+
(folder / f"{file_stem}.adxc").write_bytes(_build_adxc(cell))
|
|
66
|
+
for name, source in resources.items():
|
|
67
|
+
(resource_folder / name).write_bytes(source.read_bytes())
|
|
68
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
69
|
+
for file in folder.rglob("*"):
|
|
70
|
+
if file.is_file():
|
|
71
|
+
archive.write(file, file.relative_to(folder))
|
|
72
|
+
return zip_path
|
|
73
|
+
|
|
74
|
+
if isinstance(value, Nozzle):
|
|
75
|
+
nozzles = [value]
|
|
76
|
+
|
|
77
|
+
elif isinstance(value, (list, tuple)) and all(isinstance(item, Nozzle) for item in value):
|
|
78
|
+
nozzles = list(value)
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
raise TypeError("value must be a Cell, Nozzle, or list of Nozzle objects")
|
|
82
|
+
|
|
83
|
+
if not nozzles:
|
|
84
|
+
raise ValueError("at least one nozzle is required")
|
|
85
|
+
|
|
86
|
+
path = output_dir if output_dir.suffix == ".zip" else output_dir / "nozzles.zip"
|
|
87
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
89
|
+
temporary_directory = Path(temporary_directory)
|
|
90
|
+
files = []
|
|
91
|
+
for nozzle in nozzles:
|
|
92
|
+
temporary_path = temporary_directory / nozzle.filename
|
|
93
|
+
nozzle.write(temporary_path)
|
|
94
|
+
files.append(temporary_path)
|
|
95
|
+
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
96
|
+
for temporary_path in files:
|
|
97
|
+
archive.write(temporary_path, temporary_path.name)
|
|
98
|
+
return path
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"Cell", "Process", "SensorShape", "Sensor", "SensorField", "WireArcMode", "Robot", "Track",
|
|
102
|
+
"Gantry", "Positioner", "save", "savezip",
|
|
103
|
+
]
|
adxc/cell.py
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import struct, copy, time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from dataclasses import dataclass, fields
|
|
5
|
+
from urllib.parse import urlparse, unquote
|
|
6
|
+
from .protobuf import write_protobuf
|
|
7
|
+
from .library import *
|
|
8
|
+
from .fields import *
|
|
9
|
+
|
|
10
|
+
# fixed HeadMessage slots; a sensor shape's leading params fill these positionally.
|
|
11
|
+
_SENSOR_SLOTS = ("sensor_near", "sensor_far", "sensor_size", "sensor_height")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Head(HeadFields):
|
|
16
|
+
|
|
17
|
+
def add_toolframe(
|
|
18
|
+
self,
|
|
19
|
+
name="",
|
|
20
|
+
translation=None,
|
|
21
|
+
rotation=None,
|
|
22
|
+
process=None,
|
|
23
|
+
mode=None,
|
|
24
|
+
sensor=None,
|
|
25
|
+
near=None,
|
|
26
|
+
far=None,
|
|
27
|
+
length=None,
|
|
28
|
+
radius=None,
|
|
29
|
+
width=None,
|
|
30
|
+
height=None,
|
|
31
|
+
sensor_translation=None,
|
|
32
|
+
sensor_rotation=None,
|
|
33
|
+
):
|
|
34
|
+
|
|
35
|
+
if isinstance(name, SensorField):
|
|
36
|
+
sensor, name = name, ""
|
|
37
|
+
if isinstance(sensor, SensorField):
|
|
38
|
+
sensor = {field.name: getattr(sensor, field.name)
|
|
39
|
+
for field in fields(sensor) if getattr(sensor, field.name) is not None}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if sensor is not None and not isinstance(sensor, dict):
|
|
43
|
+
sensor = {"shape": sensor}
|
|
44
|
+
sensor.update({name: value for name, value in {
|
|
45
|
+
"near": near, "far": far, "length": length,
|
|
46
|
+
"radius": radius, "width": width,
|
|
47
|
+
"height": height,
|
|
48
|
+
}.items() if value is not None})
|
|
49
|
+
if sensor_translation is not None:
|
|
50
|
+
sensor["translation"] = sensor_translation
|
|
51
|
+
if sensor_rotation is not None:
|
|
52
|
+
sensor["rotation"] = sensor_rotation
|
|
53
|
+
elif sensor is not None and any(value is not None for value in (
|
|
54
|
+
near, far, length, radius, width, height,
|
|
55
|
+
sensor_translation, sensor_rotation)):
|
|
56
|
+
raise TypeError("sensor dimensions must be passed as keyword arguments, not with a sensor dict")
|
|
57
|
+
process_kwargs = {}
|
|
58
|
+
if process:
|
|
59
|
+
process = Process.resolve(process)
|
|
60
|
+
process_kwargs["process"] = GUID(process.equipment_id)
|
|
61
|
+
process_mode = process.mode
|
|
62
|
+
if mode is not None:
|
|
63
|
+
process_mode = mode if isinstance(mode, int) else WireArcMode[str(mode).upper()].value
|
|
64
|
+
process_kwargs["mode"] = process_mode
|
|
65
|
+
if sensor:
|
|
66
|
+
shape = SensorShape.resolve(sensor.get("shape"))
|
|
67
|
+
allowed = shape.params
|
|
68
|
+
for pname, pval in sensor.items():
|
|
69
|
+
if pname in ("shape", "translation", "rotation"):
|
|
70
|
+
continue
|
|
71
|
+
if pname not in allowed:
|
|
72
|
+
raise ValueError(f"{shape.label} accepts {list(allowed)}; got unexpected {pname!r}")
|
|
73
|
+
process_kwargs[_SENSOR_SLOTS[allowed.index(pname)]] = float(pval)
|
|
74
|
+
sensor["shape"] = shape.label
|
|
75
|
+
process_kwargs["sensor_on"] = shape.code
|
|
76
|
+
if sensor.get("translation") is not None:
|
|
77
|
+
process_kwargs["sensor_translation"] = _as_vec(sensor["translation"])
|
|
78
|
+
if sensor.get("rotation") is not None:
|
|
79
|
+
process_kwargs["sensor_rotation"] = _as_vec(sensor["rotation"])
|
|
80
|
+
|
|
81
|
+
self._frames.append({
|
|
82
|
+
"name": name,
|
|
83
|
+
"translation": translation,
|
|
84
|
+
"rotation": rotation,
|
|
85
|
+
"process": process,
|
|
86
|
+
"mode": mode,
|
|
87
|
+
"sensor": sensor,
|
|
88
|
+
"process_kwargs": process_kwargs
|
|
89
|
+
})
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _as_vec(v):
|
|
94
|
+
if v is None or isinstance(v, EmptyMessage):
|
|
95
|
+
return EmptyMessage()
|
|
96
|
+
if isinstance(v, Vec3):
|
|
97
|
+
return EmptyMessage() if not any((v.x, v.y, v.z)) else v
|
|
98
|
+
return EmptyMessage() if not any(v) else Vec3(*v)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _normalize_transform(value):
|
|
102
|
+
return EmptyMessage() if value is None or not any(value) else Vec3(*value)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _normalize_translation_rotation(value):
|
|
106
|
+
for field_name in ("translation", "rotation"):
|
|
107
|
+
field_value = getattr(value, field_name)
|
|
108
|
+
if isinstance(field_value, (list, tuple)):
|
|
109
|
+
setattr(value, field_name, _normalize_transform(field_value))
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _make_holders():
|
|
114
|
+
return [ToolFrame(ToolFrameData(field_3=None, name="fake_holder")),
|
|
115
|
+
ToolFrame(ToolFrameData(field_3=None, name="fake_holder"))]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _sensor_settings(sensor):
|
|
119
|
+
if not sensor:
|
|
120
|
+
return None
|
|
121
|
+
shape = SensorShape.resolve(sensor.get("shape"))
|
|
122
|
+
parameters = SensorParameters(
|
|
123
|
+
shape=shape.code,
|
|
124
|
+
near_or_length=(sensor.get("far") if shape.label in ("linear", "conic", "rectangle")
|
|
125
|
+
else sensor.get("length")),
|
|
126
|
+
far_or_size=(sensor.get("width") or sensor.get("radius")),
|
|
127
|
+
height=sensor.get("height"),
|
|
128
|
+
)
|
|
129
|
+
return SensorSettings(
|
|
130
|
+
shape=shape.code,
|
|
131
|
+
translation=_as_vec(sensor.get("translation")),
|
|
132
|
+
rotation=_as_vec(sensor.get("rotation")),
|
|
133
|
+
parameters=parameters,
|
|
134
|
+
offset=_as_vec(sensor.get("translation")),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_EMPTY_FRAME = {"name": "", "translation": None, "rotation": None,
|
|
139
|
+
"process": None, "mode": None, "sensor": None}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def finalize_head(head: Head):
|
|
143
|
+
"""Return (HeadMessage, [Tool, ...]). One head layout; the toolframe count
|
|
144
|
+
sets multi_process (0 for <=1, 1 for 2+). Process is PER-TOOLFRAME: a single
|
|
145
|
+
toolframe folds it into the head; 2+ put each on its own toolframe."""
|
|
146
|
+
holders = _make_holders()
|
|
147
|
+
|
|
148
|
+
if len(head._frames) <= 1: # ---- single-process ----
|
|
149
|
+
f = head._frames[0] if head._frames else dict(
|
|
150
|
+
_EMPTY_FRAME, translation=head.translation, rotation=head.rotation)
|
|
151
|
+
proc = f.get("process_kwargs", {})
|
|
152
|
+
tcp_t, tcp_r = _as_vec(f["translation"]), _as_vec(f["rotation"])
|
|
153
|
+
holders[0].data.translation = copy.deepcopy(tcp_t) # both holders mirror the TCP
|
|
154
|
+
holders[1].data.translation = copy.deepcopy(tcp_t)
|
|
155
|
+
msg = HeadMessage(
|
|
156
|
+
id=head.id, filepath=head.filepath,
|
|
157
|
+
translation=tcp_t, rotation=tcp_r,
|
|
158
|
+
model=head.model, brand=head.brand,
|
|
159
|
+
toolframes=holders, file_name=head.file_name, multi_process=0, **proc,
|
|
160
|
+
)
|
|
161
|
+
tools = [Tool(used=-1, name=f["name"] or head.tool_id or "tool1",
|
|
162
|
+
head=head.id, toolframe=null_guid())]
|
|
163
|
+
return msg, tools
|
|
164
|
+
|
|
165
|
+
user, tools = [], [] # ---- multi-process ----
|
|
166
|
+
# Per-toolframe process GUID goes at 1.5.25[i].1.12. The richer .2 settings
|
|
167
|
+
# block (.2.10 shape/params per process) is the next step - see TODO below.
|
|
168
|
+
for index, f in enumerate(head._frames):
|
|
169
|
+
frame_id = GUID()
|
|
170
|
+
proc_guid = f.get("process_kwargs", {}).get("process", null_guid())
|
|
171
|
+
user.append(ToolFrame(ToolFrameData(
|
|
172
|
+
translation=_as_vec(f["translation"]), rotation=_as_vec(f["rotation"]),
|
|
173
|
+
name=f["name"] or f"Name {index + 1}", id=frame_id,
|
|
174
|
+
nozzle_holder_ref=proc_guid, # field 12 = per-toolframe process GUID
|
|
175
|
+
created=int(time.time() * 1000),
|
|
176
|
+
), settings=_sensor_settings(f["sensor"])))
|
|
177
|
+
tools.append(Tool(used=-1, name=f["name"] or f"tool{index + 1}",
|
|
178
|
+
head=head.id, toolframe=frame_id))
|
|
179
|
+
|
|
180
|
+
msg = HeadMessage(
|
|
181
|
+
id=head.id,
|
|
182
|
+
filepath=head.filepath,
|
|
183
|
+
model=head.model,
|
|
184
|
+
brand=head.brand,
|
|
185
|
+
toolframes=holders + user,
|
|
186
|
+
file_name=head.file_name,
|
|
187
|
+
multi_process=1,
|
|
188
|
+
)
|
|
189
|
+
return msg, tools
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class Cell(CellField):
|
|
194
|
+
|
|
195
|
+
def add_workframe(self,
|
|
196
|
+
workoffset_index=1,
|
|
197
|
+
id=None,
|
|
198
|
+
name="",
|
|
199
|
+
translation=None,
|
|
200
|
+
rotation=None,
|
|
201
|
+
min_x=None,
|
|
202
|
+
min_y=None,
|
|
203
|
+
max_x=None,
|
|
204
|
+
max_y=None,
|
|
205
|
+
grid_spacing=None,
|
|
206
|
+
hidden=False
|
|
207
|
+
):
|
|
208
|
+
|
|
209
|
+
self.work_frames[0].hidden = True
|
|
210
|
+
|
|
211
|
+
workframe = WorkFrame(
|
|
212
|
+
name=name, translation=translation, rotation=rotation,
|
|
213
|
+
min_x=min_x, min_y=min_y, max_x=max_x, max_y=max_y,
|
|
214
|
+
grid_spacing=grid_spacing, hidden=hidden,
|
|
215
|
+
)
|
|
216
|
+
if id is not None:
|
|
217
|
+
workframe.id = id
|
|
218
|
+
|
|
219
|
+
if len(self.work_frames) == 1:
|
|
220
|
+
workframe.active = 1
|
|
221
|
+
|
|
222
|
+
if isinstance(workframe.translation, (list, tuple)):
|
|
223
|
+
workframe.translation = (EmptyMessage()
|
|
224
|
+
if not any(workframe.translation)
|
|
225
|
+
else Vec3(*workframe.translation)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if isinstance(workframe.rotation, (list, tuple)):
|
|
229
|
+
rx, ry, rz = workframe.rotation
|
|
230
|
+
# AdaOne's workframe rotation message stores Y in slot 1 and X in slot 2.
|
|
231
|
+
workframe.rotation = (EmptyMessage()
|
|
232
|
+
if not any(workframe.rotation)
|
|
233
|
+
else Vec3(ry, rx, rz)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self.work_frames.append(workframe)
|
|
237
|
+
|
|
238
|
+
self.kinematics.workoffsets = self.kinematics.workoffsets or WorkOffsets()
|
|
239
|
+
self.kinematics.workoffsets.offsets[workoffset_index].workframe = workframe.id
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def add_robot(self, definition):
|
|
243
|
+
|
|
244
|
+
self.work_frames[0].hidden = True
|
|
245
|
+
|
|
246
|
+
robot_chain = GUID()
|
|
247
|
+
target = RobotPosition(
|
|
248
|
+
guid=GUID(),
|
|
249
|
+
joint_angles=struct.pack("<6f", 0.0, 0.0, -90.0, 0.0, 15.0, 0.0),
|
|
250
|
+
external_axes=struct.pack("<6f", *([float("inf")] * 6)),
|
|
251
|
+
flag=1,
|
|
252
|
+
name="Default home position",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
robot_base = GUID()
|
|
256
|
+
self.robot = Robot(
|
|
257
|
+
kinematic_chain=robot_chain,
|
|
258
|
+
robot_model=GUID(definition["model"]),
|
|
259
|
+
current_position=target.joint_angles,
|
|
260
|
+
targets=[target],
|
|
261
|
+
robot_base=robot_base,
|
|
262
|
+
default_target=target.guid,
|
|
263
|
+
axis_max=struct.pack("<6f", *definition["axis_max"]),
|
|
264
|
+
axis_min=struct.pack("<6f", *definition["axis_min"]),
|
|
265
|
+
)
|
|
266
|
+
self.kinematics.robot_base = robot_chain
|
|
267
|
+
self.kinematics.workoffsets = WorkOffsets(robot_chain=robot_chain)
|
|
268
|
+
self.kinematics.workoffsets.offsets[0].workframe = robot_base
|
|
269
|
+
self.kinematics.print_chain = [Chain(robot=GUID(guid)) for guid in definition["chains"]]
|
|
270
|
+
for axis in self.kinematics.external_axes:
|
|
271
|
+
axis.parent_chain = self.robot.kinematic_chain
|
|
272
|
+
|
|
273
|
+
# pending heads added before the robot now belong to it; the tool wiring
|
|
274
|
+
# happens in _finalize_heads (count-driven), not here.
|
|
275
|
+
self.robot.heads.extend(self.unknown_pending_heads)
|
|
276
|
+
self.unknown_pending_heads.clear()
|
|
277
|
+
if self.robot.heads:
|
|
278
|
+
self.robot.active_head = self.robot.heads[-1]
|
|
279
|
+
|
|
280
|
+
self.kinematics.version = 0
|
|
281
|
+
self.kinematics.app_name = "AdaOne"
|
|
282
|
+
return self
|
|
283
|
+
|
|
284
|
+
def add_track(self,
|
|
285
|
+
definition,
|
|
286
|
+
kinematic_chain=None,
|
|
287
|
+
equipment_id=None,
|
|
288
|
+
translation=None,
|
|
289
|
+
rotation=None,
|
|
290
|
+
axis_mode=1,
|
|
291
|
+
track_properties=None,
|
|
292
|
+
equipment_kind=None,
|
|
293
|
+
stroke=None
|
|
294
|
+
):
|
|
295
|
+
self.work_frames[0].hidden = True
|
|
296
|
+
|
|
297
|
+
track = Equipment(
|
|
298
|
+
kinematic_chain=kinematic_chain,
|
|
299
|
+
equipment_id=equipment_id,
|
|
300
|
+
translation=translation,
|
|
301
|
+
rotation=rotation,
|
|
302
|
+
axis_mode=axis_mode,
|
|
303
|
+
track_properties=track_properties,
|
|
304
|
+
equipment_kind=equipment_kind,
|
|
305
|
+
stroke=stroke,
|
|
306
|
+
)
|
|
307
|
+
_normalize_translation_rotation(track)
|
|
308
|
+
|
|
309
|
+
track.kinematic_chain = track.kinematic_chain or GUID()
|
|
310
|
+
track.equipment_id = track.equipment_id or GUID(definition["equipment_id"])
|
|
311
|
+
track.track_properties = track.track_properties or TrackProperties()
|
|
312
|
+
track.track_properties.size = definition["size"]
|
|
313
|
+
track.track_properties.rotation = definition.get("rotation") or None
|
|
314
|
+
track.track_properties.riser = definition.get("riser") or None
|
|
315
|
+
track.field_5 = track.field_5 or struct.pack("<f", 0.0)
|
|
316
|
+
track.stroke = 0.0 if track.stroke is None else track.stroke
|
|
317
|
+
track.stroke_range = track.stroke_range or struct.pack("<2f", 0.0, definition["length"])
|
|
318
|
+
self.equipment.append(track)
|
|
319
|
+
self.kinematics.external_axes.append(
|
|
320
|
+
ExternalAxis(
|
|
321
|
+
track_chain=track.kinematic_chain,
|
|
322
|
+
axis_type=definition["axis_type"],
|
|
323
|
+
parent_chain=(self.robot.kinematic_chain
|
|
324
|
+
if self.robot is not None else null_guid()),
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
return self
|
|
328
|
+
|
|
329
|
+
def add_gantry(self,
|
|
330
|
+
definition,
|
|
331
|
+
kinematic_chain=None,
|
|
332
|
+
equipment_id=None,
|
|
333
|
+
translation=None,
|
|
334
|
+
rotation=None,
|
|
335
|
+
axis_mode=3,
|
|
336
|
+
track_properties=None,
|
|
337
|
+
equipment_kind=None,
|
|
338
|
+
stroke=None
|
|
339
|
+
):
|
|
340
|
+
|
|
341
|
+
self.work_frames[0].hidden = True
|
|
342
|
+
|
|
343
|
+
gantry = Equipment(
|
|
344
|
+
kinematic_chain=kinematic_chain, equipment_id=equipment_id,
|
|
345
|
+
translation=translation, rotation=rotation,
|
|
346
|
+
axis_mode=axis_mode, track_properties=track_properties,
|
|
347
|
+
equipment_kind=equipment_kind, stroke=stroke,
|
|
348
|
+
)
|
|
349
|
+
_normalize_translation_rotation(gantry)
|
|
350
|
+
|
|
351
|
+
gantry.kinematic_chain = gantry.kinematic_chain or GUID()
|
|
352
|
+
gantry.equipment_id = gantry.equipment_id or GUID(definition["equipment_id"])
|
|
353
|
+
gantry.axis_mode = 3
|
|
354
|
+
gantry.field_5 = gantry.field_5 or struct.pack("<3f", 0.0, 0.0, 0.0)
|
|
355
|
+
gantry.track_properties = None
|
|
356
|
+
self.equipment.append(gantry)
|
|
357
|
+
parent_chain = self.robot.kinematic_chain if self.robot else null_guid()
|
|
358
|
+
|
|
359
|
+
self.kinematics.external_axes.append(
|
|
360
|
+
ExternalAxis(
|
|
361
|
+
axis_type=3,
|
|
362
|
+
track_chain=gantry.kinematic_chain,
|
|
363
|
+
parent_chain=parent_chain
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
for axis_index in range(1, definition["axes"] + 1):
|
|
367
|
+
self.kinematics.external_axes.append(
|
|
368
|
+
ExternalAxis(
|
|
369
|
+
axis_type=3,
|
|
370
|
+
axis_index=axis_index,
|
|
371
|
+
track_chain=gantry.kinematic_chain,
|
|
372
|
+
parent_chain=parent_chain
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
return self
|
|
376
|
+
|
|
377
|
+
def add_positioner(self,
|
|
378
|
+
definition,
|
|
379
|
+
kinematic_chain=None,
|
|
380
|
+
equipment_id=None,
|
|
381
|
+
translation=None,
|
|
382
|
+
rotation=None,
|
|
383
|
+
axis_mode=0,
|
|
384
|
+
track_properties=None,
|
|
385
|
+
equipment_kind=None,
|
|
386
|
+
stroke=None
|
|
387
|
+
):
|
|
388
|
+
|
|
389
|
+
self.work_frames[0].hidden = True
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
positioner = Equipment(
|
|
393
|
+
kinematic_chain=kinematic_chain, equipment_id=equipment_id,
|
|
394
|
+
translation=translation, rotation=rotation,
|
|
395
|
+
axis_mode=axis_mode, track_properties=track_properties,
|
|
396
|
+
equipment_kind=equipment_kind, stroke=stroke,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
for field_name in ("translation", "rotation"):
|
|
400
|
+
value = getattr(positioner, field_name)
|
|
401
|
+
if isinstance(value, (list, tuple)):
|
|
402
|
+
setattr(positioner, field_name, EmptyMessage() if not any(value) else Vec3(
|
|
403
|
+
x=value[0], y=value[1], z=value[2]
|
|
404
|
+
))
|
|
405
|
+
|
|
406
|
+
positioner.kinematic_chain = positioner.kinematic_chain or GUID()
|
|
407
|
+
positioner.equipment_id = positioner.equipment_id or GUID(definition["equipment_id"])
|
|
408
|
+
positioner.axis_mode = 0
|
|
409
|
+
positioner.track_properties = None
|
|
410
|
+
positioner.field_5 = positioner.field_5 or struct.pack("<2f", *definition["home_position"])
|
|
411
|
+
positioner.equipment_kind = 2
|
|
412
|
+
self.equipment.append(positioner)
|
|
413
|
+
axes = definition["axes"]
|
|
414
|
+
axis_indices = [None] if axes == 1 else list(range(1, axes + 1))
|
|
415
|
+
|
|
416
|
+
for axis_index in axis_indices:
|
|
417
|
+
self.kinematics.external_axes.append(
|
|
418
|
+
ExternalAxis(axis_index=axis_index,
|
|
419
|
+
track_chain=positioner.kinematic_chain,
|
|
420
|
+
axis_enabled=1, axis_type=0,
|
|
421
|
+
parent_chain=null_guid())
|
|
422
|
+
)
|
|
423
|
+
return self
|
|
424
|
+
|
|
425
|
+
def add_head(self,
|
|
426
|
+
tool_id=None,
|
|
427
|
+
id=None,
|
|
428
|
+
filepath="",
|
|
429
|
+
translation=None,
|
|
430
|
+
rotation=None,
|
|
431
|
+
model="",
|
|
432
|
+
brand="",
|
|
433
|
+
nozzle_holder_ref=None,
|
|
434
|
+
file_name="",
|
|
435
|
+
frame=None,
|
|
436
|
+
name=None
|
|
437
|
+
):
|
|
438
|
+
|
|
439
|
+
self.work_frames[0].hidden = True
|
|
440
|
+
|
|
441
|
+
if frame is not None and not isinstance(frame, Frame):
|
|
442
|
+
raise TypeError("frame must be a Frame instance")
|
|
443
|
+
|
|
444
|
+
if frame is not None:
|
|
445
|
+
translation = translation if translation is not None else frame.translation
|
|
446
|
+
rotation = rotation if rotation is not None else frame.rotation
|
|
447
|
+
|
|
448
|
+
head = Head(
|
|
449
|
+
id=id, filepath=filepath, translation=translation, rotation=rotation,
|
|
450
|
+
model=model, brand=brand, nozzle_holder_ref=nozzle_holder_ref,
|
|
451
|
+
file_name=file_name,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
head.tool_id = tool_id or name
|
|
455
|
+
|
|
456
|
+
normalized = str(head.filepath).replace("\\", "/")
|
|
457
|
+
head.id = head.id or GUID()
|
|
458
|
+
head.filepath = _file_uri(head.filepath)
|
|
459
|
+
head.file_name = head.file_name or normalized.rsplit("/", 1)[-1]
|
|
460
|
+
head.model = head.model or f"Process head {len(self.robot.heads) if self.robot else 0}"
|
|
461
|
+
head.nozzle_holder_ref = head.nozzle_holder_ref or null_guid()
|
|
462
|
+
|
|
463
|
+
if self.robot is None:
|
|
464
|
+
self.unknown_pending_heads.append(head)
|
|
465
|
+
else:
|
|
466
|
+
self.robot.heads.append(head)
|
|
467
|
+
self.robot.active_head = self.robot.heads[-1]
|
|
468
|
+
|
|
469
|
+
return head
|
|
470
|
+
|
|
471
|
+
def add_element(self,
|
|
472
|
+
name="",
|
|
473
|
+
filepath="",
|
|
474
|
+
translation=None,
|
|
475
|
+
rotation=None,
|
|
476
|
+
workframe=None,
|
|
477
|
+
payload=None,
|
|
478
|
+
parent=None,
|
|
479
|
+
appearance=None,
|
|
480
|
+
id=None,
|
|
481
|
+
):
|
|
482
|
+
|
|
483
|
+
self.work_frames[0].hidden = True
|
|
484
|
+
|
|
485
|
+
element = CellElement(
|
|
486
|
+
id=id, filepath=filepath, translation=translation, rotation=rotation,
|
|
487
|
+
payload=payload, parent=parent, workframe=workframe,
|
|
488
|
+
appearance=appearance, name=name,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
if isinstance(element.workframe, WorkFrame):
|
|
492
|
+
element.workframe = element.workframe.id
|
|
493
|
+
|
|
494
|
+
if element.parent is None and self.robot is not None:
|
|
495
|
+
element.parent = self.robot.robot_base
|
|
496
|
+
|
|
497
|
+
for field_name in ("translation", "rotation"):
|
|
498
|
+
value = getattr(element, field_name)
|
|
499
|
+
if isinstance(value, (list, tuple)):
|
|
500
|
+
setattr(element, field_name, EmptyMessage() if not any(value) else Vec3(
|
|
501
|
+
x=value[0], y=value[1], z=value[2]
|
|
502
|
+
))
|
|
503
|
+
|
|
504
|
+
normalized = str(element.filepath).replace("\\", "/")
|
|
505
|
+
uri = _file_uri(element.filepath)
|
|
506
|
+
element.id = element.id or GUID()
|
|
507
|
+
element.filepath = uri
|
|
508
|
+
element.name = element.name or Path(normalized).stem
|
|
509
|
+
self.cell_elements.append(element)
|
|
510
|
+
return self
|
|
511
|
+
|
|
512
|
+
def _file_uri(filepath):
|
|
513
|
+
normalized = str(filepath).replace("\\", "/")
|
|
514
|
+
if normalized.startswith("file://"):
|
|
515
|
+
return normalized
|
|
516
|
+
if len(normalized) >= 2 and normalized[1] == ":":
|
|
517
|
+
return "file:///" + normalized
|
|
518
|
+
return Path(normalized).resolve().as_uri()
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _uri_to_path(uri: str) -> Path:
|
|
522
|
+
raw = unquote(urlparse(uri).path)
|
|
523
|
+
if raw and raw[0] == "/" and len(raw) > 2 and raw[2] == ":":
|
|
524
|
+
raw = raw[1:]
|
|
525
|
+
return Path(raw)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _finalize_heads(cell: Cell) -> None:
|
|
529
|
+
"""Convert every accumulator Head into its concrete single/multi message and
|
|
530
|
+
wire the resulting tools into kinematics. The toolframe count decides the
|
|
531
|
+
layout (0-1 -> single, 2+ -> multi)."""
|
|
532
|
+
concrete: dict[int, object] = {}
|
|
533
|
+
|
|
534
|
+
def convert(head):
|
|
535
|
+
if id(head) in concrete:
|
|
536
|
+
return concrete[id(head)]
|
|
537
|
+
message, tools = finalize_head(head)
|
|
538
|
+
concrete[id(head)] = message
|
|
539
|
+
if tools:
|
|
540
|
+
robot_chain = cell.robot.kinematic_chain if cell.robot is not None else None
|
|
541
|
+
cell.kinematics.tools = cell.kinematics.tools or Tools(robot_chain=robot_chain)
|
|
542
|
+
cell.kinematics.tools.tools.extend(tools)
|
|
543
|
+
return message
|
|
544
|
+
|
|
545
|
+
if cell.robot is not None:
|
|
546
|
+
cell.robot.heads = [convert(head) if isinstance(head, Head) else head
|
|
547
|
+
for head in cell.robot.heads]
|
|
548
|
+
if isinstance(cell.robot.active_head, Head):
|
|
549
|
+
cell.robot.active_head = concrete.get(id(cell.robot.active_head),
|
|
550
|
+
convert(cell.robot.active_head))
|
|
551
|
+
|
|
552
|
+
cell.unknown_pending_heads = [convert(head) if isinstance(head, Head) else head
|
|
553
|
+
for head in cell.unknown_pending_heads]
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _build_adxc(cell: Cell) -> bytes:
|
|
557
|
+
_finalize_heads(cell)
|
|
558
|
+
return write_protobuf(cell)
|