rocket-welder-sdk 1.1.44__py3-none-any.whl → 1.1.46__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.
@@ -0,0 +1,494 @@
1
+ """
2
+ Stage sink and writer for VectorGraphics streaming.
3
+
4
+ Matches C# IStageSink, IStageWriter, StageSink, StageWriter
5
+ from RocketWelder.SDK.Graphics.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Dict, List, Protocol, Sequence, Tuple, runtime_checkable
11
+
12
+ from rocket_welder_sdk.transport.frame_sink import IFrameSink # noqa: TC001 - used at runtime
13
+
14
+ from .protocol import FrameType
15
+ from .rgb_color import RgbColor # noqa: TC001 - used at runtime in method bodies
16
+ from .vector_graphics_encoder import VectorGraphicsEncoder
17
+
18
+ if TYPE_CHECKING:
19
+ from .layer_canvas import ILayerCanvas
20
+
21
+
22
+ @runtime_checkable
23
+ class IStageWriter(Protocol):
24
+ """
25
+ Stage writer for vector graphics overlays.
26
+
27
+ User-facing API only - auto-flushes on close like other writers.
28
+ Matches C# IStageWriter interface.
29
+
30
+ Example:
31
+ with stage_sink.create_writer(frame_id) as writer:
32
+ # Draw on layer 0 (background)
33
+ writer[0].set_stroke(RgbColor.Red)
34
+ writer[0].draw_polygon(contour_points)
35
+
36
+ # Draw on layer 1 (labels)
37
+ writer[1].draw_text(f"Frame: {writer.frame_id}", 10, 20)
38
+ # writer auto-flushes on context exit
39
+ """
40
+
41
+ @property
42
+ def frame_id(self) -> int:
43
+ """Gets the current frame ID."""
44
+ ...
45
+
46
+ def __getitem__(self, layer_id: int) -> ILayerCanvas:
47
+ """
48
+ Gets the layer canvas for the specified layer ID.
49
+
50
+ Layers are composited with lower IDs at the back (0 = bottom).
51
+
52
+ Args:
53
+ layer_id: Layer ID (0-15)
54
+
55
+ Returns:
56
+ The layer canvas for drawing operations
57
+ """
58
+ ...
59
+
60
+ def layer(self, layer_id: int) -> ILayerCanvas:
61
+ """
62
+ Gets the layer canvas for the specified layer ID.
63
+
64
+ Alternative method syntax for the indexer.
65
+
66
+ Args:
67
+ layer_id: Layer ID (0-15)
68
+
69
+ Returns:
70
+ The layer canvas for drawing operations
71
+ """
72
+ ...
73
+
74
+ def close(self) -> None:
75
+ """Flushes and closes the writer."""
76
+ ...
77
+
78
+ def __enter__(self) -> IStageWriter:
79
+ """Context manager entry."""
80
+ ...
81
+
82
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
83
+ """Context manager exit - auto-flushes."""
84
+ ...
85
+
86
+
87
+ @runtime_checkable
88
+ class IStageSink(Protocol):
89
+ """
90
+ Factory for creating per-frame stage writers (transport-agnostic).
91
+
92
+ Follows the same pattern as ISegmentationResultSink and IKeyPointsSink.
93
+ Matches C# IStageSink interface.
94
+ """
95
+
96
+ def create_writer(self, frame_id: int) -> IStageWriter:
97
+ """
98
+ Creates a writer for the specified frame.
99
+
100
+ The writer auto-flushes on close.
101
+
102
+ Args:
103
+ frame_id: Frame identifier
104
+
105
+ Returns:
106
+ Stage writer that auto-flushes on close
107
+ """
108
+ ...
109
+
110
+ def close(self) -> None:
111
+ """Closes the sink and releases resources."""
112
+ ...
113
+
114
+
115
+ class LayerEncoder:
116
+ """
117
+ Internal layer encoder that writes operations directly to buffer.
118
+
119
+ Implements ILayerCanvas protocol by encoding operations to binary.
120
+ """
121
+
122
+ __slots__ = (
123
+ "_buffer",
124
+ "_frame_type",
125
+ "_header_reserve",
126
+ "_layer_id",
127
+ "_offset",
128
+ "_operation_count",
129
+ )
130
+
131
+ # Reserve space for layer header at start of buffer
132
+ HEADER_RESERVE = 16
133
+ LAYER_BUFFER_SIZE = 256 * 1024 # 256KB per layer to accommodate JPEG frames
134
+
135
+ def __init__(self, layer_id: int) -> None:
136
+ """
137
+ Creates a new layer encoder.
138
+
139
+ Args:
140
+ layer_id: Layer ID (0-15)
141
+ """
142
+ self._layer_id = layer_id
143
+ self._frame_type = FrameType.MASTER
144
+ self._buffer = bytearray(self.LAYER_BUFFER_SIZE)
145
+ self._offset = self.HEADER_RESERVE
146
+ self._operation_count = 0
147
+ self._header_reserve = self.HEADER_RESERVE
148
+
149
+ @property
150
+ def layer_id(self) -> int:
151
+ """The layer ID."""
152
+ return self._layer_id
153
+
154
+ def copy_encoded_data(self, dest_buffer: bytearray, dest_offset: int) -> int:
155
+ """
156
+ Copies the encoded layer data (with header) to the destination buffer.
157
+
158
+ Args:
159
+ dest_buffer: Destination buffer
160
+ dest_offset: Starting offset in destination
161
+
162
+ Returns:
163
+ Number of bytes written
164
+ """
165
+ pos = dest_offset
166
+
167
+ if self._frame_type == FrameType.MASTER:
168
+ pos += VectorGraphicsEncoder.write_layer_master(
169
+ dest_buffer, pos, self._layer_id, self._operation_count
170
+ )
171
+ data_length = self._offset - self._header_reserve
172
+ dest_buffer[pos : pos + data_length] = self._buffer[self._header_reserve : self._offset]
173
+ pos += data_length
174
+ elif self._frame_type == FrameType.REMAIN:
175
+ pos += VectorGraphicsEncoder.write_layer_remain(dest_buffer, pos, self._layer_id)
176
+ elif self._frame_type == FrameType.CLEAR:
177
+ pos += VectorGraphicsEncoder.write_layer_clear(dest_buffer, pos, self._layer_id)
178
+
179
+ return pos - dest_offset
180
+
181
+ # ============== Frame Type ==============
182
+
183
+ def master(self) -> None:
184
+ """Sets this layer to Master mode."""
185
+ self._frame_type = FrameType.MASTER
186
+
187
+ def remain(self) -> None:
188
+ """Sets this layer to Remain mode."""
189
+ self._frame_type = FrameType.REMAIN
190
+
191
+ def clear(self) -> None:
192
+ """Sets this layer to Clear mode."""
193
+ self._frame_type = FrameType.CLEAR
194
+
195
+ # ============== Context State - Styling ==============
196
+
197
+ def set_stroke(self, color: RgbColor) -> None:
198
+ """Sets the stroke color."""
199
+ self._offset += VectorGraphicsEncoder.write_set_stroke(self._buffer, self._offset, color)
200
+ self._operation_count += 1
201
+
202
+ def set_fill(self, color: RgbColor) -> None:
203
+ """Sets the fill color."""
204
+ self._offset += VectorGraphicsEncoder.write_set_fill(self._buffer, self._offset, color)
205
+ self._operation_count += 1
206
+
207
+ def set_thickness(self, width: int) -> None:
208
+ """Sets the stroke thickness."""
209
+ self._offset += VectorGraphicsEncoder.write_set_thickness(self._buffer, self._offset, width)
210
+ self._operation_count += 1
211
+
212
+ def set_font_size(self, size: int) -> None:
213
+ """Sets the font size."""
214
+ self._offset += VectorGraphicsEncoder.write_set_font_size(self._buffer, self._offset, size)
215
+ self._operation_count += 1
216
+
217
+ def set_font_color(self, color: RgbColor) -> None:
218
+ """Sets the font color."""
219
+ self._offset += VectorGraphicsEncoder.write_set_font_color(
220
+ self._buffer, self._offset, color
221
+ )
222
+ self._operation_count += 1
223
+
224
+ # ============== Context State - Transforms ==============
225
+
226
+ def translate(self, dx: float, dy: float) -> None:
227
+ """Sets the translation offset."""
228
+ self._offset += VectorGraphicsEncoder.write_set_offset(self._buffer, self._offset, dx, dy)
229
+ self._operation_count += 1
230
+
231
+ def rotate(self, degrees: float) -> None:
232
+ """Sets the rotation."""
233
+ self._offset += VectorGraphicsEncoder.write_set_rotation(
234
+ self._buffer, self._offset, degrees
235
+ )
236
+ self._operation_count += 1
237
+
238
+ def scale(self, sx: float, sy: float) -> None:
239
+ """Sets the scale."""
240
+ self._offset += VectorGraphicsEncoder.write_set_scale(self._buffer, self._offset, sx, sy)
241
+ self._operation_count += 1
242
+
243
+ def skew(self, kx: float, ky: float) -> None:
244
+ """Sets the skew."""
245
+ self._offset += VectorGraphicsEncoder.write_set_skew(self._buffer, self._offset, kx, ky)
246
+ self._operation_count += 1
247
+
248
+ def set_matrix(
249
+ self,
250
+ scale_x: float,
251
+ skew_x: float,
252
+ trans_x: float,
253
+ skew_y: float,
254
+ scale_y: float,
255
+ trans_y: float,
256
+ ) -> None:
257
+ """Sets the transformation matrix."""
258
+ self._offset += VectorGraphicsEncoder.write_set_matrix(
259
+ self._buffer, self._offset, scale_x, skew_x, trans_x, skew_y, scale_y, trans_y
260
+ )
261
+ self._operation_count += 1
262
+
263
+ # ============== Context Stack ==============
264
+
265
+ def save(self) -> None:
266
+ """Pushes the current context state."""
267
+ self._offset += VectorGraphicsEncoder.write_save_context(self._buffer, self._offset)
268
+ self._operation_count += 1
269
+
270
+ def restore(self) -> None:
271
+ """Pops and restores the context state."""
272
+ self._offset += VectorGraphicsEncoder.write_restore_context(self._buffer, self._offset)
273
+ self._operation_count += 1
274
+
275
+ def reset_context(self) -> None:
276
+ """Resets the context to defaults."""
277
+ self._offset += VectorGraphicsEncoder.write_reset_context(self._buffer, self._offset)
278
+ self._operation_count += 1
279
+
280
+ # ============== Draw Operations ==============
281
+
282
+ def draw_polygon(self, points: Sequence[Tuple[float, float]]) -> None:
283
+ """Draws a polygon."""
284
+ self._offset += VectorGraphicsEncoder.write_draw_polygon(self._buffer, self._offset, points)
285
+ self._operation_count += 1
286
+
287
+ def draw_text(self, text: str, x: int, y: int) -> None:
288
+ """Draws text."""
289
+ self._offset += VectorGraphicsEncoder.write_draw_text(
290
+ self._buffer, self._offset, text, x, y
291
+ )
292
+ self._operation_count += 1
293
+
294
+ def draw_circle(self, center_x: int, center_y: int, radius: int) -> None:
295
+ """Draws a circle."""
296
+ self._offset += VectorGraphicsEncoder.write_draw_circle(
297
+ self._buffer, self._offset, center_x, center_y, radius
298
+ )
299
+ self._operation_count += 1
300
+
301
+ def draw_rectangle(self, x: int, y: int, width: int, height: int) -> None:
302
+ """Draws a rectangle."""
303
+ self._offset += VectorGraphicsEncoder.write_draw_rect(
304
+ self._buffer, self._offset, x, y, width, height
305
+ )
306
+ self._operation_count += 1
307
+
308
+ def draw_line(self, x1: int, y1: int, x2: int, y2: int) -> None:
309
+ """Draws a line."""
310
+ self._offset += VectorGraphicsEncoder.write_draw_line(
311
+ self._buffer, self._offset, x1, y1, x2, y2
312
+ )
313
+ self._operation_count += 1
314
+
315
+ def draw_jpeg(self, jpeg_data: bytes, x: int, y: int, width: int, height: int) -> None:
316
+ """Draws a JPEG image."""
317
+ self._offset += VectorGraphicsEncoder.write_draw_jpeg(
318
+ self._buffer, self._offset, jpeg_data, x, y, width, height
319
+ )
320
+ self._operation_count += 1
321
+
322
+
323
+ class StageWriter:
324
+ """
325
+ Per-frame stage writer that auto-flushes on close.
326
+
327
+ Follows the same pattern as SegmentationResultWriter and KeyPointsWriter.
328
+ Implements IStageWriter protocol.
329
+ """
330
+
331
+ __slots__ = ("_active_layer_ids", "_buffer", "_closed", "_frame_id", "_frame_sink", "_layers")
332
+
333
+ DEFAULT_BUFFER_SIZE = 1024 * 1024 # 1MB
334
+
335
+ def __init__(
336
+ self, frame_id: int, frame_sink: IFrameSink, buffer_size: int = DEFAULT_BUFFER_SIZE
337
+ ) -> None:
338
+ """
339
+ Creates a new stage writer.
340
+
341
+ Args:
342
+ frame_id: Frame identifier
343
+ frame_sink: Transport for sending encoded frames
344
+ buffer_size: Size of the encoding buffer (default 1MB)
345
+ """
346
+ self._frame_id = frame_id
347
+ self._frame_sink = frame_sink
348
+ self._buffer = bytearray(buffer_size)
349
+ self._layers: Dict[int, LayerEncoder] = {}
350
+ self._active_layer_ids: List[int] = []
351
+ self._closed = False
352
+
353
+ @property
354
+ def frame_id(self) -> int:
355
+ """Gets the frame ID for this writer."""
356
+ return self._frame_id
357
+
358
+ def __getitem__(self, layer_id: int) -> LayerEncoder:
359
+ """Gets the layer canvas for the specified layer ID."""
360
+ return self.layer(layer_id)
361
+
362
+ def layer(self, layer_id: int) -> LayerEncoder:
363
+ """
364
+ Gets the layer canvas for the specified layer ID.
365
+
366
+ Args:
367
+ layer_id: Layer ID (0-15)
368
+
369
+ Returns:
370
+ The layer encoder for drawing operations
371
+ """
372
+ if self._closed:
373
+ raise RuntimeError("StageWriter is closed")
374
+
375
+ if layer_id not in self._layers:
376
+ self._layers[layer_id] = LayerEncoder(layer_id)
377
+
378
+ # Track that this layer was accessed
379
+ if layer_id not in self._active_layer_ids:
380
+ self._active_layer_ids.append(layer_id)
381
+
382
+ return self._layers[layer_id]
383
+
384
+ def _flush(self) -> None:
385
+ """
386
+ Encodes and sends all layer operations via transport.
387
+
388
+ Called automatically on close.
389
+ """
390
+ if not self._active_layer_ids:
391
+ return
392
+
393
+ offset = 0
394
+
395
+ # Write message header
396
+ offset += VectorGraphicsEncoder.write_message_header(
397
+ self._buffer, offset, self._frame_id, len(self._active_layer_ids)
398
+ )
399
+
400
+ # Encode each active layer
401
+ for layer_id in self._active_layer_ids:
402
+ layer = self._layers[layer_id]
403
+ offset += layer.copy_encoded_data(self._buffer, offset)
404
+
405
+ # Write end marker
406
+ offset += VectorGraphicsEncoder.write_end_marker(self._buffer, offset)
407
+
408
+ # Send via transport
409
+ self._frame_sink.write_frame(bytes(self._buffer[:offset]))
410
+
411
+ def close(self) -> None:
412
+ """Flushes and closes the writer."""
413
+ if self._closed:
414
+ return
415
+ self._closed = True
416
+
417
+ # Auto-flush on close (same pattern as other writers)
418
+ self._flush()
419
+
420
+ # Clear state
421
+ self._layers.clear()
422
+ self._active_layer_ids.clear()
423
+
424
+ def __enter__(self) -> StageWriter:
425
+ """Context manager entry."""
426
+ return self
427
+
428
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
429
+ """Context manager exit - auto-flushes."""
430
+ self.close()
431
+
432
+
433
+ class StageSink:
434
+ """
435
+ Factory for creating per-frame stage writers.
436
+
437
+ Follows the same pattern as SegmentationResultSink and KeyPointsSink.
438
+ Implements IStageSink protocol.
439
+ """
440
+
441
+ __slots__ = ("_buffer_size", "_closed", "_frame_sink", "_owns_sink")
442
+
443
+ def __init__(
444
+ self,
445
+ frame_sink: IFrameSink,
446
+ buffer_size: int = StageWriter.DEFAULT_BUFFER_SIZE,
447
+ owns_sink: bool = True,
448
+ ) -> None:
449
+ """
450
+ Creates a StageSink with the specified transport.
451
+
452
+ Args:
453
+ frame_sink: The transport for sending encoded frames
454
+ buffer_size: Size of the encoding buffer per writer (default 1MB)
455
+ owns_sink: If True, closes the sink when this factory is closed
456
+ """
457
+ self._frame_sink = frame_sink
458
+ self._buffer_size = buffer_size
459
+ self._owns_sink = owns_sink
460
+ self._closed = False
461
+
462
+ def create_writer(self, frame_id: int) -> StageWriter:
463
+ """
464
+ Creates a writer for the specified frame.
465
+
466
+ The writer auto-flushes on close.
467
+
468
+ Args:
469
+ frame_id: Frame identifier
470
+
471
+ Returns:
472
+ Stage writer that auto-flushes on close
473
+ """
474
+ if self._closed:
475
+ raise RuntimeError("StageSink is closed")
476
+
477
+ return StageWriter(frame_id, self._frame_sink, self._buffer_size)
478
+
479
+ def close(self) -> None:
480
+ """Closes the sink and releases resources."""
481
+ if self._closed:
482
+ return
483
+ self._closed = True
484
+
485
+ if self._owns_sink:
486
+ self._frame_sink.close()
487
+
488
+ def __enter__(self) -> StageSink:
489
+ """Context manager entry."""
490
+ return self
491
+
492
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
493
+ """Context manager exit."""
494
+ self.close()