rocket-welder-sdk 1.1.44__py3-none-any.whl → 1.1.45__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.
- rocket_welder_sdk/__init__.py +26 -0
- rocket_welder_sdk/graphics/__init__.py +42 -0
- rocket_welder_sdk/graphics/layer_canvas.py +157 -0
- rocket_welder_sdk/graphics/protocol.py +72 -0
- rocket_welder_sdk/graphics/rgb_color.py +109 -0
- rocket_welder_sdk/graphics/stage.py +494 -0
- rocket_welder_sdk/graphics/vector_graphics_encoder.py +575 -0
- rocket_welder_sdk/high_level/connection_strings.py +85 -0
- rocket_welder_sdk/rocket_welder_client.py +210 -12
- rocket_welder_sdk/session_id.py +1 -0
- {rocket_welder_sdk-1.1.44.dist-info → rocket_welder_sdk-1.1.45.dist-info}/METADATA +1 -1
- {rocket_welder_sdk-1.1.44.dist-info → rocket_welder_sdk-1.1.45.dist-info}/RECORD +14 -8
- {rocket_welder_sdk-1.1.44.dist-info → rocket_welder_sdk-1.1.45.dist-info}/WHEEL +1 -1
- {rocket_welder_sdk-1.1.44.dist-info → rocket_welder_sdk-1.1.45.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VectorGraphics Protocol V2 Encoder.
|
|
3
|
+
|
|
4
|
+
Matches C# VectorGraphicsEncoderV2 from BlazorBlaze.VectorGraphics.Protocol.
|
|
5
|
+
|
|
6
|
+
Wire format:
|
|
7
|
+
Message:
|
|
8
|
+
[GlobalFrameId: 8 bytes LE]
|
|
9
|
+
[LayerCount: 1 byte]
|
|
10
|
+
For each layer:
|
|
11
|
+
[LayerBlock...]
|
|
12
|
+
[EndMarker: 0xFF 0xFF]
|
|
13
|
+
|
|
14
|
+
LayerBlock:
|
|
15
|
+
[LayerId: 1 byte]
|
|
16
|
+
[FrameType: 1 byte] // 0x00=Master, 0x01=Remain, 0x02=Clear
|
|
17
|
+
If FrameType == Master:
|
|
18
|
+
[OpCount: varint]
|
|
19
|
+
[Operations...]
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import struct
|
|
25
|
+
from typing import Sequence, Tuple
|
|
26
|
+
|
|
27
|
+
from rocket_welder_sdk.varint import write_varint, write_zigzag
|
|
28
|
+
|
|
29
|
+
from .protocol import END_MARKER_BYTE1, END_MARKER_BYTE2, FrameType, OpType, PropertyId
|
|
30
|
+
from .rgb_color import RgbColor # noqa: TC001 - used at runtime in method bodies
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VectorGraphicsEncoder:
|
|
34
|
+
"""
|
|
35
|
+
Protocol V2 encoder for stateful canvas API with multi-layer support.
|
|
36
|
+
|
|
37
|
+
Provides static methods for encoding VectorGraphics protocol messages.
|
|
38
|
+
All methods write to a bytearray at a given offset and return bytes written.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def write_message_header(
|
|
43
|
+
buffer: bytearray, offset: int, frame_id: int, layer_count: int
|
|
44
|
+
) -> int:
|
|
45
|
+
"""
|
|
46
|
+
Write message header with frame ID and layer count.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
buffer: Destination buffer
|
|
50
|
+
offset: Starting offset
|
|
51
|
+
frame_id: Frame identifier (64-bit unsigned)
|
|
52
|
+
layer_count: Number of layers (0-255)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Number of bytes written (9)
|
|
56
|
+
"""
|
|
57
|
+
struct.pack_into("<Q", buffer, offset, frame_id)
|
|
58
|
+
buffer[offset + 8] = layer_count
|
|
59
|
+
return 9
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def write_end_marker(buffer: bytearray, offset: int) -> int:
|
|
63
|
+
"""
|
|
64
|
+
Write end marker (0xFF 0xFF).
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
buffer: Destination buffer
|
|
68
|
+
offset: Starting offset
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Number of bytes written (2)
|
|
72
|
+
"""
|
|
73
|
+
buffer[offset] = END_MARKER_BYTE1
|
|
74
|
+
buffer[offset + 1] = END_MARKER_BYTE2
|
|
75
|
+
return 2
|
|
76
|
+
|
|
77
|
+
# ============== Layer Block ==============
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def write_layer_master(buffer: bytearray, offset: int, layer_id: int, op_count: int) -> int:
|
|
81
|
+
"""
|
|
82
|
+
Write layer block header for Master frame type.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
buffer: Destination buffer
|
|
86
|
+
offset: Starting offset
|
|
87
|
+
layer_id: Layer ID (0-255)
|
|
88
|
+
op_count: Number of operations
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Number of bytes written
|
|
92
|
+
"""
|
|
93
|
+
buffer[offset] = layer_id
|
|
94
|
+
buffer[offset + 1] = FrameType.MASTER
|
|
95
|
+
return 2 + write_varint(buffer, offset + 2, op_count)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def write_layer_remain(buffer: bytearray, offset: int, layer_id: int) -> int:
|
|
99
|
+
"""
|
|
100
|
+
Write layer block for Remain frame type (keep previous content).
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
buffer: Destination buffer
|
|
104
|
+
offset: Starting offset
|
|
105
|
+
layer_id: Layer ID (0-255)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Number of bytes written (2)
|
|
109
|
+
"""
|
|
110
|
+
buffer[offset] = layer_id
|
|
111
|
+
buffer[offset + 1] = FrameType.REMAIN
|
|
112
|
+
return 2
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def write_layer_clear(buffer: bytearray, offset: int, layer_id: int) -> int:
|
|
116
|
+
"""
|
|
117
|
+
Write layer block for Clear frame type (clear to transparent).
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
buffer: Destination buffer
|
|
121
|
+
offset: Starting offset
|
|
122
|
+
layer_id: Layer ID (0-255)
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Number of bytes written (2)
|
|
126
|
+
"""
|
|
127
|
+
buffer[offset] = layer_id
|
|
128
|
+
buffer[offset + 1] = FrameType.CLEAR
|
|
129
|
+
return 2
|
|
130
|
+
|
|
131
|
+
# ============== Context Operations - Styling ==============
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def write_set_stroke(buffer: bytearray, offset: int, color: RgbColor) -> int:
|
|
135
|
+
"""
|
|
136
|
+
Write SetContext for stroke color.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
buffer: Destination buffer
|
|
140
|
+
offset: Starting offset
|
|
141
|
+
color: Stroke color
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Number of bytes written (7)
|
|
145
|
+
"""
|
|
146
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
147
|
+
buffer[offset + 1] = 1 # 1 field
|
|
148
|
+
buffer[offset + 2] = PropertyId.STROKE
|
|
149
|
+
buffer[offset + 3] = color.r
|
|
150
|
+
buffer[offset + 4] = color.g
|
|
151
|
+
buffer[offset + 5] = color.b
|
|
152
|
+
buffer[offset + 6] = color.a
|
|
153
|
+
return 7
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def write_set_fill(buffer: bytearray, offset: int, color: RgbColor) -> int:
|
|
157
|
+
"""
|
|
158
|
+
Write SetContext for fill color.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
buffer: Destination buffer
|
|
162
|
+
offset: Starting offset
|
|
163
|
+
color: Fill color
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Number of bytes written (7)
|
|
167
|
+
"""
|
|
168
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
169
|
+
buffer[offset + 1] = 1
|
|
170
|
+
buffer[offset + 2] = PropertyId.FILL
|
|
171
|
+
buffer[offset + 3] = color.r
|
|
172
|
+
buffer[offset + 4] = color.g
|
|
173
|
+
buffer[offset + 5] = color.b
|
|
174
|
+
buffer[offset + 6] = color.a
|
|
175
|
+
return 7
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def write_set_thickness(buffer: bytearray, offset: int, thickness: int) -> int:
|
|
179
|
+
"""
|
|
180
|
+
Write SetContext for stroke thickness.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
buffer: Destination buffer
|
|
184
|
+
offset: Starting offset
|
|
185
|
+
thickness: Stroke thickness in pixels
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Number of bytes written
|
|
189
|
+
"""
|
|
190
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
191
|
+
buffer[offset + 1] = 1
|
|
192
|
+
buffer[offset + 2] = PropertyId.THICKNESS
|
|
193
|
+
return 3 + write_varint(buffer, offset + 3, thickness)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def write_set_font_size(buffer: bytearray, offset: int, size: int) -> int:
|
|
197
|
+
"""
|
|
198
|
+
Write SetContext for font size.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
buffer: Destination buffer
|
|
202
|
+
offset: Starting offset
|
|
203
|
+
size: Font size in pixels
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Number of bytes written
|
|
207
|
+
"""
|
|
208
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
209
|
+
buffer[offset + 1] = 1
|
|
210
|
+
buffer[offset + 2] = PropertyId.FONT_SIZE
|
|
211
|
+
return 3 + write_varint(buffer, offset + 3, size)
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def write_set_font_color(buffer: bytearray, offset: int, color: RgbColor) -> int:
|
|
215
|
+
"""
|
|
216
|
+
Write SetContext for font color.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
buffer: Destination buffer
|
|
220
|
+
offset: Starting offset
|
|
221
|
+
color: Font color
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Number of bytes written (7)
|
|
225
|
+
"""
|
|
226
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
227
|
+
buffer[offset + 1] = 1
|
|
228
|
+
buffer[offset + 2] = PropertyId.FONT_COLOR
|
|
229
|
+
buffer[offset + 3] = color.r
|
|
230
|
+
buffer[offset + 4] = color.g
|
|
231
|
+
buffer[offset + 5] = color.b
|
|
232
|
+
buffer[offset + 6] = color.a
|
|
233
|
+
return 7
|
|
234
|
+
|
|
235
|
+
# ============== Context Operations - Transforms ==============
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def write_set_offset(buffer: bytearray, offset: int, x: float, y: float) -> int:
|
|
239
|
+
"""
|
|
240
|
+
Write SetContext for translation offset.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
buffer: Destination buffer
|
|
244
|
+
offset: Starting offset
|
|
245
|
+
x: X translation
|
|
246
|
+
y: Y translation
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Number of bytes written
|
|
250
|
+
"""
|
|
251
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
252
|
+
buffer[offset + 1] = 1
|
|
253
|
+
buffer[offset + 2] = PropertyId.OFFSET
|
|
254
|
+
pos = offset + 3
|
|
255
|
+
pos += write_zigzag(buffer, pos, int(x))
|
|
256
|
+
pos += write_zigzag(buffer, pos, int(y))
|
|
257
|
+
return pos - offset
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def write_set_rotation(buffer: bytearray, offset: int, degrees: float) -> int:
|
|
261
|
+
"""
|
|
262
|
+
Write SetContext for rotation in degrees.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
buffer: Destination buffer
|
|
266
|
+
offset: Starting offset
|
|
267
|
+
degrees: Rotation angle in degrees
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Number of bytes written (7)
|
|
271
|
+
"""
|
|
272
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
273
|
+
buffer[offset + 1] = 1
|
|
274
|
+
buffer[offset + 2] = PropertyId.ROTATION
|
|
275
|
+
struct.pack_into("<f", buffer, offset + 3, degrees)
|
|
276
|
+
return 7
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def write_set_scale(buffer: bytearray, offset: int, scale_x: float, scale_y: float) -> int:
|
|
280
|
+
"""
|
|
281
|
+
Write SetContext for scale.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
buffer: Destination buffer
|
|
285
|
+
offset: Starting offset
|
|
286
|
+
scale_x: X scale factor
|
|
287
|
+
scale_y: Y scale factor
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Number of bytes written (11)
|
|
291
|
+
"""
|
|
292
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
293
|
+
buffer[offset + 1] = 1
|
|
294
|
+
buffer[offset + 2] = PropertyId.SCALE
|
|
295
|
+
struct.pack_into("<ff", buffer, offset + 3, scale_x, scale_y)
|
|
296
|
+
return 11
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def write_set_skew(buffer: bytearray, offset: int, skew_x: float, skew_y: float) -> int:
|
|
300
|
+
"""
|
|
301
|
+
Write SetContext for skew.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
buffer: Destination buffer
|
|
305
|
+
offset: Starting offset
|
|
306
|
+
skew_x: X skew factor
|
|
307
|
+
skew_y: Y skew factor
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Number of bytes written (11)
|
|
311
|
+
"""
|
|
312
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
313
|
+
buffer[offset + 1] = 1
|
|
314
|
+
buffer[offset + 2] = PropertyId.SKEW
|
|
315
|
+
struct.pack_into("<ff", buffer, offset + 3, skew_x, skew_y)
|
|
316
|
+
return 11
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def write_set_matrix(
|
|
320
|
+
buffer: bytearray,
|
|
321
|
+
offset: int,
|
|
322
|
+
scale_x: float,
|
|
323
|
+
skew_x: float,
|
|
324
|
+
trans_x: float,
|
|
325
|
+
skew_y: float,
|
|
326
|
+
scale_y: float,
|
|
327
|
+
trans_y: float,
|
|
328
|
+
) -> int:
|
|
329
|
+
"""
|
|
330
|
+
Write SetContext for full transformation matrix (6 floats).
|
|
331
|
+
|
|
332
|
+
Matrix layout matches SKMatrix:
|
|
333
|
+
| ScaleX SkewX TransX |
|
|
334
|
+
| SkewY ScaleY TransY |
|
|
335
|
+
| Persp0 Persp1 Persp2 | (not sent, assumed identity)
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
buffer: Destination buffer
|
|
339
|
+
offset: Starting offset
|
|
340
|
+
scale_x, skew_x, trans_x: First row
|
|
341
|
+
skew_y, scale_y, trans_y: Second row
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Number of bytes written (27)
|
|
345
|
+
"""
|
|
346
|
+
buffer[offset] = OpType.SET_CONTEXT
|
|
347
|
+
buffer[offset + 1] = 1
|
|
348
|
+
buffer[offset + 2] = PropertyId.MATRIX
|
|
349
|
+
struct.pack_into(
|
|
350
|
+
"<ffffff", buffer, offset + 3, scale_x, skew_x, trans_x, skew_y, scale_y, trans_y
|
|
351
|
+
)
|
|
352
|
+
return 27
|
|
353
|
+
|
|
354
|
+
# ============== Context Stack ==============
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def write_save_context(buffer: bytearray, offset: int) -> int:
|
|
358
|
+
"""
|
|
359
|
+
Write SaveContext operation.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
buffer: Destination buffer
|
|
363
|
+
offset: Starting offset
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Number of bytes written (1)
|
|
367
|
+
"""
|
|
368
|
+
buffer[offset] = OpType.SAVE_CONTEXT
|
|
369
|
+
return 1
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def write_restore_context(buffer: bytearray, offset: int) -> int:
|
|
373
|
+
"""
|
|
374
|
+
Write RestoreContext operation.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
buffer: Destination buffer
|
|
378
|
+
offset: Starting offset
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Number of bytes written (1)
|
|
382
|
+
"""
|
|
383
|
+
buffer[offset] = OpType.RESTORE_CONTEXT
|
|
384
|
+
return 1
|
|
385
|
+
|
|
386
|
+
@staticmethod
|
|
387
|
+
def write_reset_context(buffer: bytearray, offset: int) -> int:
|
|
388
|
+
"""
|
|
389
|
+
Write ResetContext operation.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
buffer: Destination buffer
|
|
393
|
+
offset: Starting offset
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Number of bytes written (1)
|
|
397
|
+
"""
|
|
398
|
+
buffer[offset] = OpType.RESET_CONTEXT
|
|
399
|
+
return 1
|
|
400
|
+
|
|
401
|
+
# ============== Draw Operations ==============
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def write_draw_polygon(
|
|
405
|
+
buffer: bytearray, offset: int, points: Sequence[Tuple[float, float]]
|
|
406
|
+
) -> int:
|
|
407
|
+
"""
|
|
408
|
+
Write DrawPolygon operation with delta-encoded points.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
buffer: Destination buffer
|
|
412
|
+
offset: Starting offset
|
|
413
|
+
points: Sequence of (x, y) tuples
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Number of bytes written
|
|
417
|
+
"""
|
|
418
|
+
pos = offset
|
|
419
|
+
buffer[pos] = OpType.DRAW_POLYGON
|
|
420
|
+
pos += 1
|
|
421
|
+
pos += write_varint(buffer, pos, len(points))
|
|
422
|
+
|
|
423
|
+
if len(points) > 0:
|
|
424
|
+
# First point - absolute
|
|
425
|
+
first_x = int(points[0][0])
|
|
426
|
+
first_y = int(points[0][1])
|
|
427
|
+
pos += write_zigzag(buffer, pos, first_x)
|
|
428
|
+
pos += write_zigzag(buffer, pos, first_y)
|
|
429
|
+
|
|
430
|
+
# Subsequent points - delta encoded
|
|
431
|
+
last_x = first_x
|
|
432
|
+
last_y = first_y
|
|
433
|
+
for i in range(1, len(points)):
|
|
434
|
+
x = int(points[i][0])
|
|
435
|
+
y = int(points[i][1])
|
|
436
|
+
pos += write_zigzag(buffer, pos, x - last_x)
|
|
437
|
+
pos += write_zigzag(buffer, pos, y - last_y)
|
|
438
|
+
last_x = x
|
|
439
|
+
last_y = y
|
|
440
|
+
|
|
441
|
+
return pos - offset
|
|
442
|
+
|
|
443
|
+
@staticmethod
|
|
444
|
+
def write_draw_text(buffer: bytearray, offset: int, text: str, x: int, y: int) -> int:
|
|
445
|
+
"""
|
|
446
|
+
Write DrawText operation.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
buffer: Destination buffer
|
|
450
|
+
offset: Starting offset
|
|
451
|
+
text: Text to draw
|
|
452
|
+
x: X position
|
|
453
|
+
y: Y position
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Number of bytes written
|
|
457
|
+
"""
|
|
458
|
+
pos = offset
|
|
459
|
+
buffer[pos] = OpType.DRAW_TEXT
|
|
460
|
+
pos += 1
|
|
461
|
+
pos += write_zigzag(buffer, pos, x)
|
|
462
|
+
pos += write_zigzag(buffer, pos, y)
|
|
463
|
+
|
|
464
|
+
text_bytes = text.encode("utf-8")
|
|
465
|
+
pos += write_varint(buffer, pos, len(text_bytes))
|
|
466
|
+
buffer[pos : pos + len(text_bytes)] = text_bytes
|
|
467
|
+
pos += len(text_bytes)
|
|
468
|
+
|
|
469
|
+
return pos - offset
|
|
470
|
+
|
|
471
|
+
@staticmethod
|
|
472
|
+
def write_draw_circle(
|
|
473
|
+
buffer: bytearray, offset: int, center_x: int, center_y: int, radius: int
|
|
474
|
+
) -> int:
|
|
475
|
+
"""
|
|
476
|
+
Write DrawCircle operation.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
buffer: Destination buffer
|
|
480
|
+
offset: Starting offset
|
|
481
|
+
center_x: Center X coordinate
|
|
482
|
+
center_y: Center Y coordinate
|
|
483
|
+
radius: Circle radius
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Number of bytes written
|
|
487
|
+
"""
|
|
488
|
+
pos = offset
|
|
489
|
+
buffer[pos] = OpType.DRAW_CIRCLE
|
|
490
|
+
pos += 1
|
|
491
|
+
pos += write_zigzag(buffer, pos, center_x)
|
|
492
|
+
pos += write_zigzag(buffer, pos, center_y)
|
|
493
|
+
pos += write_varint(buffer, pos, radius)
|
|
494
|
+
return pos - offset
|
|
495
|
+
|
|
496
|
+
@staticmethod
|
|
497
|
+
def write_draw_rect(
|
|
498
|
+
buffer: bytearray, offset: int, x: int, y: int, width: int, height: int
|
|
499
|
+
) -> int:
|
|
500
|
+
"""
|
|
501
|
+
Write DrawRect operation.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
buffer: Destination buffer
|
|
505
|
+
offset: Starting offset
|
|
506
|
+
x: X position
|
|
507
|
+
y: Y position
|
|
508
|
+
width: Rectangle width
|
|
509
|
+
height: Rectangle height
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Number of bytes written
|
|
513
|
+
"""
|
|
514
|
+
pos = offset
|
|
515
|
+
buffer[pos] = OpType.DRAW_RECT
|
|
516
|
+
pos += 1
|
|
517
|
+
pos += write_zigzag(buffer, pos, x)
|
|
518
|
+
pos += write_zigzag(buffer, pos, y)
|
|
519
|
+
pos += write_varint(buffer, pos, width)
|
|
520
|
+
pos += write_varint(buffer, pos, height)
|
|
521
|
+
return pos - offset
|
|
522
|
+
|
|
523
|
+
@staticmethod
|
|
524
|
+
def write_draw_line(buffer: bytearray, offset: int, x1: int, y1: int, x2: int, y2: int) -> int:
|
|
525
|
+
"""
|
|
526
|
+
Write DrawLine operation.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
buffer: Destination buffer
|
|
530
|
+
offset: Starting offset
|
|
531
|
+
x1, y1: Start point
|
|
532
|
+
x2, y2: End point
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Number of bytes written
|
|
536
|
+
"""
|
|
537
|
+
pos = offset
|
|
538
|
+
buffer[pos] = OpType.DRAW_LINE
|
|
539
|
+
pos += 1
|
|
540
|
+
pos += write_zigzag(buffer, pos, x1)
|
|
541
|
+
pos += write_zigzag(buffer, pos, y1)
|
|
542
|
+
pos += write_zigzag(buffer, pos, x2)
|
|
543
|
+
pos += write_zigzag(buffer, pos, y2)
|
|
544
|
+
return pos - offset
|
|
545
|
+
|
|
546
|
+
@staticmethod
|
|
547
|
+
def write_draw_jpeg(
|
|
548
|
+
buffer: bytearray, offset: int, jpeg_data: bytes, x: int, y: int, width: int, height: int
|
|
549
|
+
) -> int:
|
|
550
|
+
"""
|
|
551
|
+
Write DrawJpeg operation with raw JPEG data.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
buffer: Destination buffer
|
|
555
|
+
offset: Starting offset
|
|
556
|
+
jpeg_data: Raw JPEG bytes
|
|
557
|
+
x: X position
|
|
558
|
+
y: Y position
|
|
559
|
+
width: Display width
|
|
560
|
+
height: Display height
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Number of bytes written
|
|
564
|
+
"""
|
|
565
|
+
pos = offset
|
|
566
|
+
buffer[pos] = OpType.DRAW_JPEG
|
|
567
|
+
pos += 1
|
|
568
|
+
pos += write_zigzag(buffer, pos, x)
|
|
569
|
+
pos += write_zigzag(buffer, pos, y)
|
|
570
|
+
pos += write_varint(buffer, pos, width)
|
|
571
|
+
pos += write_varint(buffer, pos, height)
|
|
572
|
+
pos += write_varint(buffer, pos, len(jpeg_data))
|
|
573
|
+
buffer[pos : pos + len(jpeg_data)] = jpeg_data
|
|
574
|
+
pos += len(jpeg_data)
|
|
575
|
+
return pos - offset
|
|
@@ -317,3 +317,88 @@ class SegmentationConnectionString:
|
|
|
317
317
|
|
|
318
318
|
def __str__(self) -> str:
|
|
319
319
|
return self.value
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass(frozen=True)
|
|
323
|
+
class GraphicsConnectionString:
|
|
324
|
+
"""
|
|
325
|
+
Strongly-typed connection string for Stage (graphics) output.
|
|
326
|
+
|
|
327
|
+
Supported protocols:
|
|
328
|
+
- file:///path/to/file.bin - File output (absolute path)
|
|
329
|
+
- socket:///tmp/socket.sock - Unix domain socket
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
value: str
|
|
333
|
+
protocol: TransportProtocol
|
|
334
|
+
address: str
|
|
335
|
+
parameters: Dict[str, str] = field(default_factory=dict)
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
def default(cls) -> GraphicsConnectionString:
|
|
339
|
+
"""Default connection string for Stage."""
|
|
340
|
+
return cls.parse("socket:///tmp/rocket-welder-stage.sock")
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def from_environment(
|
|
344
|
+
cls, variable_name: str = "STAGE_CONNECTION_STRING"
|
|
345
|
+
) -> GraphicsConnectionString:
|
|
346
|
+
"""Create from environment variable or use default."""
|
|
347
|
+
value = os.environ.get(variable_name)
|
|
348
|
+
return cls.parse(value) if value else cls.default()
|
|
349
|
+
|
|
350
|
+
@classmethod
|
|
351
|
+
def parse(cls, s: str) -> GraphicsConnectionString:
|
|
352
|
+
"""Parse a connection string."""
|
|
353
|
+
result = cls.try_parse(s)
|
|
354
|
+
if result is None:
|
|
355
|
+
raise ValueError(f"Invalid Graphics connection string: {s}")
|
|
356
|
+
return result
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def try_parse(cls, s: str) -> Optional[GraphicsConnectionString]:
|
|
360
|
+
"""Try to parse a connection string."""
|
|
361
|
+
if not s or not s.strip():
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
s = s.strip()
|
|
365
|
+
parameters: Dict[str, str] = {}
|
|
366
|
+
|
|
367
|
+
# Extract query parameters
|
|
368
|
+
endpoint_part = s
|
|
369
|
+
if "?" in s:
|
|
370
|
+
endpoint_part, query = s.split("?", 1)
|
|
371
|
+
for key, values in parse_qs(query).items():
|
|
372
|
+
parameters[key.lower()] = values[0] if values else ""
|
|
373
|
+
|
|
374
|
+
# Parse protocol and address
|
|
375
|
+
scheme_end = endpoint_part.find("://")
|
|
376
|
+
if scheme_end <= 0:
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
schema_str = endpoint_part[:scheme_end]
|
|
380
|
+
path_part = endpoint_part[scheme_end + 3 :] # skip "://"
|
|
381
|
+
|
|
382
|
+
protocol = TransportProtocol.try_parse(schema_str)
|
|
383
|
+
if protocol is None:
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
# Build address based on protocol type
|
|
387
|
+
if protocol.is_file:
|
|
388
|
+
# file:///absolute/path -> /absolute/path
|
|
389
|
+
address = path_part if path_part.startswith("/") else "/" + path_part
|
|
390
|
+
elif protocol.is_socket:
|
|
391
|
+
# socket:///tmp/sock -> /tmp/sock
|
|
392
|
+
address = path_part if path_part.startswith("/") else "/" + path_part
|
|
393
|
+
else:
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
return cls(
|
|
397
|
+
value=s,
|
|
398
|
+
protocol=protocol,
|
|
399
|
+
address=address,
|
|
400
|
+
parameters=parameters,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def __str__(self) -> str:
|
|
404
|
+
return self.value
|