sonolus.py 0.3.1__py3-none-any.whl → 0.3.2__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.

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

sonolus/script/runtime.py CHANGED
@@ -259,6 +259,382 @@ class _TutorialRuntimeUi:
259
259
  instruction: BasicRuntimeUiLayout
260
260
 
261
261
 
262
+ class UiLayout[T](Record):
263
+ """The layout of a UI element."""
264
+
265
+ _underlying: T
266
+
267
+ def update(
268
+ self,
269
+ anchor: Vec2 | None = None,
270
+ pivot: Vec2 | None = None,
271
+ dimensions: Vec2 | None = None,
272
+ rotation: float | None = None,
273
+ alpha: float | None = None,
274
+ horizontal_align: HorizontalAlign | None = None,
275
+ background: bool | None = None,
276
+ ):
277
+ """Update the layout properties if it's available in the current mode and do nothing otherwise."""
278
+ match self._underlying:
279
+ case RuntimeUiLayout():
280
+ self._underlying.update(
281
+ anchor=anchor,
282
+ pivot=pivot,
283
+ dimensions=dimensions,
284
+ rotation=rotation,
285
+ alpha=alpha,
286
+ horizontal_align=horizontal_align,
287
+ background=background,
288
+ )
289
+ case BasicRuntimeUiLayout():
290
+ self._underlying.update(
291
+ anchor=anchor,
292
+ pivot=pivot,
293
+ dimensions=dimensions,
294
+ rotation=rotation,
295
+ alpha=alpha,
296
+ background=background,
297
+ )
298
+ case _:
299
+ pass # do nothing
300
+
301
+ @property
302
+ def is_available(self) -> bool:
303
+ """Check if the layout is available in the current mode."""
304
+ return self._underlying is not None
305
+
306
+
307
+ class UiConfig[T](Record):
308
+ """The user configuration for a UI element."""
309
+
310
+ _underlying: T
311
+
312
+ @property
313
+ def scale(self) -> float:
314
+ """The scale of the UI element."""
315
+ match self._underlying:
316
+ case RuntimeUiConfig():
317
+ return self._underlying.scale
318
+ case _:
319
+ return 1.0 # Default scale if not available
320
+
321
+ @property
322
+ def alpha(self) -> float:
323
+ """The alpha (opacity) of the UI element."""
324
+ match self._underlying:
325
+ case RuntimeUiConfig():
326
+ return self._underlying.alpha
327
+ case _:
328
+ return 1.0 # Default alpha if not available
329
+
330
+ @property
331
+ def is_available(self) -> bool:
332
+ """Check if the config is available in the current mode."""
333
+ return self._underlying is not None
334
+
335
+
336
+ class RuntimeUi(Record):
337
+ """Holds the layouts for different UI elements across all modes."""
338
+
339
+ @property
340
+ @meta_fn
341
+ def menu(self) -> UiLayout:
342
+ """The configuration for the menu UI element.
343
+
344
+ Available in play, watch, preview, and tutorial mode.
345
+ """
346
+ match ctx().global_state.mode:
347
+ case Mode.PLAY:
348
+ return UiLayout(_PlayRuntimeUi.menu)
349
+ case Mode.WATCH:
350
+ return UiLayout(_WatchRuntimeUi.menu)
351
+ case Mode.PREVIEW:
352
+ return UiLayout(_PreviewRuntimeUi.menu)
353
+ case Mode.TUTORIAL:
354
+ return UiLayout(_TutorialRuntimeUi.menu)
355
+ case _:
356
+ raise RuntimeError("Unsupported mode for menu UI layout")
357
+
358
+ @property
359
+ @meta_fn
360
+ def menu_config(self) -> UiConfig:
361
+ """The configuration for the menu UI element.
362
+
363
+ Available in play, watch, preview, and tutorial mode.
364
+ """
365
+ match ctx().global_state.mode:
366
+ case Mode.PLAY:
367
+ return UiConfig(_PlayRuntimeUiConfigs.menu)
368
+ case Mode.WATCH:
369
+ return UiConfig(_WatchRuntimeUiConfigs.menu)
370
+ case Mode.PREVIEW:
371
+ return UiConfig(_PreviewRuntimeUiConfigs.menu)
372
+ case Mode.TUTORIAL:
373
+ return UiConfig(_TutorialRuntimeUiConfigs.menu)
374
+ case _:
375
+ raise RuntimeError("Unsupported mode for menu UI configuration")
376
+
377
+ @property
378
+ @meta_fn
379
+ def judgment(self) -> UiLayout:
380
+ """The configuration for the judgment UI element.
381
+
382
+ Available in play and watch mode.
383
+ """
384
+ match ctx().global_state.mode:
385
+ case Mode.PLAY:
386
+ return UiLayout(_PlayRuntimeUi.judgment)
387
+ case Mode.WATCH:
388
+ return UiLayout(_WatchRuntimeUi.judgment)
389
+ case _:
390
+ return UiLayout(None)
391
+
392
+ @property
393
+ @meta_fn
394
+ def judgment_config(self) -> UiConfig:
395
+ """The configuration for the judgment UI element.
396
+
397
+ Available in play and watch mode.
398
+ """
399
+ match ctx().global_state.mode:
400
+ case Mode.PLAY:
401
+ return UiConfig(_PlayRuntimeUiConfigs.judgment)
402
+ case Mode.WATCH:
403
+ return UiConfig(_WatchRuntimeUiConfigs.judgment)
404
+ case _:
405
+ return UiConfig(None)
406
+
407
+ @property
408
+ @meta_fn
409
+ def combo_value(self) -> UiLayout:
410
+ """The configuration for the combo value UI element.
411
+
412
+ Available in play and watch mode.
413
+ """
414
+ match ctx().global_state.mode:
415
+ case Mode.PLAY:
416
+ return UiLayout(_PlayRuntimeUi.combo_value)
417
+ case Mode.WATCH:
418
+ return UiLayout(_WatchRuntimeUi.combo_value)
419
+ case _:
420
+ return UiLayout(None)
421
+
422
+ @property
423
+ @meta_fn
424
+ def combo_text(self) -> UiLayout:
425
+ """The configuration for the combo text UI element.
426
+
427
+ Available in play and watch mode.
428
+ """
429
+ match ctx().global_state.mode:
430
+ case Mode.PLAY:
431
+ return UiLayout(_PlayRuntimeUi.combo_text)
432
+ case Mode.WATCH:
433
+ return UiLayout(_WatchRuntimeUi.combo_text)
434
+ case _:
435
+ return UiLayout(None)
436
+
437
+ @property
438
+ @meta_fn
439
+ def combo_config(self) -> UiConfig:
440
+ """The configuration for the combo UI element.
441
+
442
+ Available in play and watch mode.
443
+ """
444
+ match ctx().global_state.mode:
445
+ case Mode.PLAY:
446
+ return UiConfig(_PlayRuntimeUiConfigs.combo)
447
+ case Mode.WATCH:
448
+ return UiConfig(_WatchRuntimeUiConfigs.combo)
449
+ case _:
450
+ return UiConfig(None)
451
+
452
+ @property
453
+ @meta_fn
454
+ def primary_metric_bar(self) -> UiLayout:
455
+ """The configuration for the primary metric bar UI element.
456
+
457
+ Available in play and watch mode.
458
+ """
459
+ match ctx().global_state.mode:
460
+ case Mode.PLAY:
461
+ return UiLayout(_PlayRuntimeUi.primary_metric_bar)
462
+ case Mode.WATCH:
463
+ return UiLayout(_WatchRuntimeUi.primary_metric_bar)
464
+ case _:
465
+ return UiLayout(None)
466
+
467
+ @property
468
+ @meta_fn
469
+ def primary_metric_value(self) -> UiLayout:
470
+ """The configuration for the primary metric value UI element.
471
+
472
+ Available in play and watch mode.
473
+ """
474
+ match ctx().global_state.mode:
475
+ case Mode.PLAY:
476
+ return UiLayout(_PlayRuntimeUi.primary_metric_value)
477
+ case Mode.WATCH:
478
+ return UiLayout(_WatchRuntimeUi.primary_metric_value)
479
+ case _:
480
+ return UiLayout(None)
481
+
482
+ @property
483
+ @meta_fn
484
+ def primary_metric_config(self) -> UiConfig:
485
+ """The configuration for the primary metric UI element.
486
+
487
+ Available in play and watch mode.
488
+ """
489
+ match ctx().global_state.mode:
490
+ case Mode.PLAY:
491
+ return UiConfig(_PlayRuntimeUiConfigs.primary_metric)
492
+ case Mode.WATCH:
493
+ return UiConfig(_WatchRuntimeUiConfigs.primary_metric)
494
+ case _:
495
+ return UiConfig(None)
496
+
497
+ @property
498
+ @meta_fn
499
+ def secondary_metric_bar(self) -> UiLayout:
500
+ """The configuration for the secondary metric bar UI element.
501
+
502
+ Available in play and watch mode.
503
+ """
504
+ match ctx().global_state.mode:
505
+ case Mode.PLAY:
506
+ return UiLayout(_PlayRuntimeUi.secondary_metric_bar)
507
+ case Mode.WATCH:
508
+ return UiLayout(_WatchRuntimeUi.secondary_metric_bar)
509
+ case _:
510
+ return UiLayout(None)
511
+
512
+ @property
513
+ @meta_fn
514
+ def secondary_metric_value(self) -> UiLayout:
515
+ """The configuration for the secondary metric value UI element.
516
+
517
+ Available in play and watch mode.
518
+ """
519
+ match ctx().global_state.mode:
520
+ case Mode.PLAY:
521
+ return UiLayout(_PlayRuntimeUi.secondary_metric_value)
522
+ case Mode.WATCH:
523
+ return UiLayout(_WatchRuntimeUi.secondary_metric_value)
524
+ case _:
525
+ return UiLayout(None)
526
+
527
+ @property
528
+ @meta_fn
529
+ def secondary_metric_config(self) -> UiConfig:
530
+ """The configuration for the secondary metric UI element.
531
+
532
+ Available in play and watch mode.
533
+ """
534
+ match ctx().global_state.mode:
535
+ case Mode.PLAY:
536
+ return UiConfig(_PlayRuntimeUiConfigs.secondary_metric)
537
+ case Mode.WATCH:
538
+ return UiConfig(_WatchRuntimeUiConfigs.secondary_metric)
539
+ case _:
540
+ return UiConfig(None)
541
+
542
+ @property
543
+ @meta_fn
544
+ def progress(self) -> UiLayout:
545
+ """The configuration for the progress UI element.
546
+
547
+ Available in watch and preview mode.
548
+ """
549
+ match ctx().global_state.mode:
550
+ case Mode.WATCH:
551
+ return UiLayout(_WatchRuntimeUi.progress)
552
+ case Mode.PREVIEW:
553
+ return UiLayout(_PreviewRuntimeUi.progress)
554
+ case _:
555
+ return UiLayout(None)
556
+
557
+ @property
558
+ @meta_fn
559
+ def progress_config(self) -> UiConfig:
560
+ """The configuration for the progress UI element.
561
+
562
+ Available in watch and preview mode.
563
+ """
564
+ match ctx().global_state.mode:
565
+ case Mode.WATCH:
566
+ return UiConfig(_WatchRuntimeUiConfigs.progress)
567
+ case Mode.PREVIEW:
568
+ return UiConfig(_PreviewRuntimeUiConfigs.progress)
569
+ case _:
570
+ return UiConfig(None)
571
+
572
+ @property
573
+ @meta_fn
574
+ def previous(self) -> UiLayout:
575
+ """The configuration for the previous navigation UI element.
576
+
577
+ Available in tutorial mode.
578
+ """
579
+ match ctx().global_state.mode:
580
+ case Mode.TUTORIAL:
581
+ return UiLayout(_TutorialRuntimeUi.previous)
582
+ case _:
583
+ return UiLayout(None)
584
+
585
+ @property
586
+ @meta_fn
587
+ def next(self) -> UiLayout:
588
+ """The configuration for the next navigation UI element.
589
+
590
+ Available in tutorial mode.
591
+ """
592
+ match ctx().global_state.mode:
593
+ case Mode.TUTORIAL:
594
+ return UiLayout(_TutorialRuntimeUi.next)
595
+ case _:
596
+ return UiLayout(None)
597
+
598
+ @property
599
+ @meta_fn
600
+ def navigation_config(self) -> UiConfig:
601
+ """The configuration for the navigation UI element.
602
+
603
+ Available in tutorial mode.
604
+ """
605
+ match ctx().global_state.mode:
606
+ case Mode.TUTORIAL:
607
+ return UiConfig(_TutorialRuntimeUiConfigs.navigation)
608
+ case _:
609
+ return UiConfig(None)
610
+
611
+ @property
612
+ @meta_fn
613
+ def instruction(self) -> UiLayout:
614
+ """The configuration for the instruction UI element.
615
+
616
+ Available in tutorial mode.
617
+ """
618
+ match ctx().global_state.mode:
619
+ case Mode.TUTORIAL:
620
+ return UiLayout(_TutorialRuntimeUi.instruction)
621
+ case _:
622
+ return UiLayout(None)
623
+
624
+ @property
625
+ @meta_fn
626
+ def instruction_config(self) -> UiConfig:
627
+ """The configuration for the instruction UI element.
628
+
629
+ Available in tutorial mode.
630
+ """
631
+ match ctx().global_state.mode:
632
+ case Mode.TUTORIAL:
633
+ return UiConfig(_TutorialRuntimeUiConfigs.instruction)
634
+ case _:
635
+ return UiConfig(None)
636
+
637
+
262
638
  class Touch(Record):
263
639
  """Data of a touch event."""
264
640
 
@@ -733,6 +1109,13 @@ preview_ui_configs = _PreviewRuntimeUiConfigs
733
1109
  tutorial_ui = _TutorialRuntimeUi
734
1110
  tutorial_ui_configs = _TutorialRuntimeUiConfigs
735
1111
 
1112
+ _runtime_ui = RuntimeUi()
1113
+
1114
+
1115
+ def runtime_ui() -> RuntimeUi:
1116
+ """Get the runtime UI configuration."""
1117
+ return _runtime_ui
1118
+
736
1119
 
737
1120
  def canvas() -> _PreviewRuntimeCanvas:
738
1121
  """Get the preview canvas."""
sonolus/script/stream.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import cast, dataclass_transform
4
4
 
5
- from sonolus.backend.ir import IRExpr, IRInstr, IRPureInstr, IRConst
5
+ from sonolus.backend.ir import IRConst, IRExpr, IRInstr, IRPureInstr
6
6
  from sonolus.backend.mode import Mode
7
7
  from sonolus.backend.ops import Op
8
8
  from sonolus.script.internal.context import ctx
@@ -14,6 +14,7 @@ from sonolus.script.internal.value import BackingValue, Value
14
14
  from sonolus.script.iterator import SonolusIterator
15
15
  from sonolus.script.num import Num
16
16
  from sonolus.script.record import Record
17
+ from sonolus.script.runtime import delta_time, time
17
18
  from sonolus.script.values import sizeof
18
19
 
19
20
 
@@ -174,10 +175,11 @@ class _SparseStreamBacking(BackingValue):
174
175
  )
175
176
 
176
177
 
177
- class Stream[T](Record, BackingValue):
178
+ class Stream[T](Record):
178
179
  """Represents a stream.
179
180
 
180
- Most users should use `@stream` to declare streams and stream groups rather than using this class directly.
181
+ Most users should use [`@streams`][sonolus.script.stream.streams] to declare streams and stream groups rather than
182
+ using this class directly.
181
183
 
182
184
  If used directly, it is important that streams do not overlap. No other streams should have an offset in
183
185
  `range(self.offset, self.offset + max(1, sizeof(self.element_type())))`, or they will overlap and interfere
@@ -248,6 +250,12 @@ class Stream[T](Record, BackingValue):
248
250
  _check_can_read_stream()
249
251
  return _stream_get_next_key(self.offset, key)
250
252
 
253
+ def next_key_or_default(self, key: int | float, default: int | float) -> int:
254
+ """Get the next key, or the default value if there is no next key."""
255
+ _check_can_read_stream()
256
+ next_key = self.next_key(key)
257
+ return next_key if next_key > key else default
258
+
251
259
  def previous_key(self, key: int | float) -> int:
252
260
  """Get the previous key, or the key unchanged if it is the first key or the stream is empty.
253
261
 
@@ -256,6 +264,12 @@ class Stream[T](Record, BackingValue):
256
264
  _check_can_read_stream()
257
265
  return _stream_get_previous_key(self.offset, key)
258
266
 
267
+ def previous_key_or_default(self, key: int | float, default: int | float) -> int:
268
+ """Get the previous key, or the default value if there is no previous key."""
269
+ _check_can_read_stream()
270
+ previous_key = self.previous_key(key)
271
+ return previous_key if previous_key < key else default
272
+
259
273
  def has_next_key(self, key: int | float) -> bool:
260
274
  """Check if there is a next key after the given key in the stream."""
261
275
  _check_can_read_stream()
@@ -317,6 +331,22 @@ class Stream[T](Record, BackingValue):
317
331
  _check_can_read_stream()
318
332
  return _StreamAscIterator(self, self.next_key_inclusive(start))
319
333
 
334
+ def iter_items_since_previous_frame(self) -> SonolusIterator[tuple[int | float, T]]:
335
+ """Iterate over the items in the stream since the last frame.
336
+
337
+ This is a convenience method that iterates over the items in the stream occurring after the time of the
338
+ previous frame and up to and including the current time.
339
+
340
+ Usage:
341
+ ```python
342
+ stream = ...
343
+ for key, value in stream.iter_items_since_previous_frame():
344
+ do_something(key, value)
345
+ ```
346
+ """
347
+ _check_can_read_stream()
348
+ return _StreamBoundedAscIterator(self, self.next_key(time() - delta_time()), time())
349
+
320
350
  def iter_items_from_desc(self, start: int | float, /) -> SonolusIterator[tuple[int | float, T]]:
321
351
  """Iterate over the items in the stream in descending order starting from the given key.
322
352
 
@@ -347,6 +377,22 @@ class Stream[T](Record, BackingValue):
347
377
  _check_can_read_stream()
348
378
  return _StreamAscKeyIterator(self, self.next_key_inclusive(start))
349
379
 
380
+ def iter_keys_since_previous_frame(self) -> SonolusIterator[int | float]:
381
+ """Iterate over the keys in the stream since the last frame.
382
+
383
+ This is a convenience method that iterates over the keys in the stream occurring after the time of the
384
+ previous frame and up to and including the current time.
385
+
386
+ Usage:
387
+ ```python
388
+ stream = ...
389
+ for key in stream.iter_keys_since_previous_frame():
390
+ do_something(key)
391
+ ```
392
+ """
393
+ _check_can_read_stream()
394
+ return _StreamBoundedAscKeyIterator(self, self.next_key(time() - delta_time()), time())
395
+
350
396
  def iter_keys_from_desc(self, start: int | float, /) -> SonolusIterator[int | float]:
351
397
  """Iterate over the keys in the stream in descending order starting from the given key.
352
398
 
@@ -377,6 +423,22 @@ class Stream[T](Record, BackingValue):
377
423
  _check_can_read_stream()
378
424
  return _StreamAscValueIterator(self, self.next_key_inclusive(start))
379
425
 
426
+ def iter_values_since_previous_frame(self) -> SonolusIterator[T]:
427
+ """Iterate over the values in the stream since the last frame.
428
+
429
+ This is a convenience method that iterates over the values in the stream occurring after the time of the
430
+ previous frame and up to and including the current time.
431
+
432
+ Usage:
433
+ ```python
434
+ stream = ...
435
+ for value in stream.iter_values_since_previous_frame():
436
+ do_something(value)
437
+ ```
438
+ """
439
+ _check_can_read_stream()
440
+ return _StreamBoundedAscValueIterator(self, self.next_key(time() - delta_time()), time())
441
+
380
442
  def iter_values_from_desc(self, start: int | float, /) -> SonolusIterator[T]:
381
443
  """Iterate over the values in the stream in descending order starting from the given key.
382
444
 
@@ -396,7 +458,8 @@ class Stream[T](Record, BackingValue):
396
458
  class StreamGroup[T, Size](Record):
397
459
  """Represents a group of streams.
398
460
 
399
- Most users should use `@stream` to declare stream groups rather than using this class directly.
461
+ Most users should use [`@streams`][sonolus.script.stream.streams] to declare stream groups rather than using this
462
+ class directly.
400
463
 
401
464
  Usage:
402
465
  Declaring a stream group:
@@ -444,13 +507,28 @@ class _StreamAscIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
444
507
  current_key: int | float
445
508
 
446
509
  def has_next(self) -> bool:
447
- return self.stream.next_key(self.current_key) > self.current_key
510
+ return self.current_key in self.stream
448
511
 
449
512
  def get(self) -> tuple[int | float, T]:
450
513
  return self.current_key, self.stream[self.current_key]
451
514
 
452
515
  def advance(self):
453
- self.current_key = self.stream.next_key(self.current_key)
516
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
517
+
518
+
519
+ class _StreamBoundedAscIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
520
+ stream: Stream[T]
521
+ current_key: int | float
522
+ end_key: int | float
523
+
524
+ def has_next(self) -> bool:
525
+ return self.current_key in self.stream and self.current_key <= self.end_key
526
+
527
+ def get(self) -> tuple[int | float, T]:
528
+ return self.current_key, self.stream[self.current_key]
529
+
530
+ def advance(self):
531
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
454
532
 
455
533
 
456
534
  class _StreamDescIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
@@ -458,13 +536,13 @@ class _StreamDescIterator[T](Record, SonolusIterator[tuple[int | float, T]]):
458
536
  current_key: int | float
459
537
 
460
538
  def has_next(self) -> bool:
461
- return self.stream.previous_key(self.current_key) < self.current_key
539
+ return self.current_key in self.stream
462
540
 
463
541
  def get(self) -> tuple[int | float, T]:
464
542
  return self.current_key, self.stream[self.current_key]
465
543
 
466
544
  def advance(self):
467
- self.current_key = self.stream.previous_key(self.current_key)
545
+ self.current_key = self.stream.previous_key_or_default(self.current_key, self.current_key - 1)
468
546
 
469
547
 
470
548
  class _StreamAscKeyIterator[T](Record, SonolusIterator[int | float]):
@@ -472,13 +550,28 @@ class _StreamAscKeyIterator[T](Record, SonolusIterator[int | float]):
472
550
  current_key: int | float
473
551
 
474
552
  def has_next(self) -> bool:
475
- return self.stream.next_key(self.current_key) > self.current_key
553
+ return self.current_key in self.stream
554
+
555
+ def get(self) -> int | float:
556
+ return self.current_key
557
+
558
+ def advance(self):
559
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
560
+
561
+
562
+ class _StreamBoundedAscKeyIterator[T](Record, SonolusIterator[int | float]):
563
+ stream: Stream[T]
564
+ current_key: int | float
565
+ end_key: int | float
566
+
567
+ def has_next(self) -> bool:
568
+ return self.current_key in self.stream and self.current_key <= self.end_key
476
569
 
477
570
  def get(self) -> int | float:
478
571
  return self.current_key
479
572
 
480
573
  def advance(self):
481
- self.current_key = self.stream.next_key(self.current_key)
574
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
482
575
 
483
576
 
484
577
  class _StreamDescKeyIterator[T](Record, SonolusIterator[int | float]):
@@ -486,13 +579,13 @@ class _StreamDescKeyIterator[T](Record, SonolusIterator[int | float]):
486
579
  current_key: int | float
487
580
 
488
581
  def has_next(self) -> bool:
489
- return self.stream.previous_key(self.current_key) < self.current_key
582
+ return self.current_key in self.stream
490
583
 
491
584
  def get(self) -> int | float:
492
585
  return self.current_key
493
586
 
494
587
  def advance(self):
495
- self.current_key = self.stream.previous_key(self.current_key)
588
+ self.current_key = self.stream.previous_key_or_default(self.current_key, self.current_key - 1)
496
589
 
497
590
 
498
591
  class _StreamAscValueIterator[T](Record, SonolusIterator[T]):
@@ -500,13 +593,28 @@ class _StreamAscValueIterator[T](Record, SonolusIterator[T]):
500
593
  current_key: int | float
501
594
 
502
595
  def has_next(self) -> bool:
503
- return self.stream.next_key(self.current_key) > self.current_key
596
+ return self.current_key in self.stream
597
+
598
+ def get(self) -> T:
599
+ return self.stream[self.current_key]
600
+
601
+ def advance(self):
602
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
603
+
604
+
605
+ class _StreamBoundedAscValueIterator[T](Record, SonolusIterator[T]):
606
+ stream: Stream[T]
607
+ current_key: int | float
608
+ end_key: int | float
609
+
610
+ def has_next(self) -> bool:
611
+ return self.current_key in self.stream and self.current_key <= self.end_key
504
612
 
505
613
  def get(self) -> T:
506
614
  return self.stream[self.current_key]
507
615
 
508
616
  def advance(self):
509
- self.current_key = self.stream.next_key(self.current_key)
617
+ self.current_key = self.stream.next_key_or_default(self.current_key, self.current_key + 1)
510
618
 
511
619
 
512
620
  class _StreamDescValueIterator[T](Record, SonolusIterator[T]):
@@ -514,13 +622,13 @@ class _StreamDescValueIterator[T](Record, SonolusIterator[T]):
514
622
  current_key: int | float
515
623
 
516
624
  def has_next(self) -> bool:
517
- return self.stream.previous_key(self.current_key) < self.current_key
625
+ return self.current_key in self.stream
518
626
 
519
627
  def get(self) -> T:
520
628
  return self.stream[self.current_key]
521
629
 
522
630
  def advance(self):
523
- self.current_key = self.stream.previous_key(self.current_key)
631
+ self.current_key = self.stream.previous_key_or_default(self.current_key, self.current_key - 1)
524
632
 
525
633
 
526
634
  @native_function(Op.StreamGetNextKey)