CreativePython 1.1.4__tar.gz → 1.1.5__tar.gz

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.
Files changed (102) hide show
  1. {creativepython-1.1.4/src/CreativePython.egg-info → creativepython-1.1.5}/PKG-INFO +1 -1
  2. {creativepython-1.1.4 → creativepython-1.1.5}/pyproject.toml +1 -1
  3. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/GuiHandler.py +15 -6
  4. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/GuiRenderer.py +22 -8
  5. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/__init__.py +6 -0
  6. {creativepython-1.1.4 → creativepython-1.1.5/src/CreativePython.egg-info}/PKG-INFO +1 -1
  7. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython.egg-info/SOURCES.txt +5 -1
  8. {creativepython-1.1.4 → creativepython-1.1.5}/src/gui.py +69 -12
  9. {creativepython-1.1.4 → creativepython-1.1.5}/src/image.py +3 -2
  10. creativepython-1.1.5/src/midi.py +732 -0
  11. {creativepython-1.1.4 → creativepython-1.1.5}/src/music.py +83 -50
  12. creativepython-1.1.5/src/osc.py +266 -0
  13. creativepython-1.1.5/tests/testCompress.py +12 -0
  14. creativepython-1.1.5/tests/testGameboard.py +49 -0
  15. creativepython-1.1.5/tests/test_midi.py +71 -0
  16. creativepython-1.1.5/tests/test_osc.py +71 -0
  17. creativepython-1.1.4/src/midi.py +0 -1007
  18. creativepython-1.1.4/src/osc.py +0 -450
  19. {creativepython-1.1.4 → creativepython-1.1.5}/LICENSE +0 -0
  20. {creativepython-1.1.4 → creativepython-1.1.5}/LICENSE-PSF +0 -0
  21. {creativepython-1.1.4 → creativepython-1.1.5}/MANIFEST.in +0 -0
  22. {creativepython-1.1.4 → creativepython-1.1.5}/README.md +0 -0
  23. {creativepython-1.1.4 → creativepython-1.1.5}/setup.cfg +0 -0
  24. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/RealtimeAudioPlayer.py +0 -0
  25. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/Java-Comparison-Tests/advMetricRunner.pythonSurvey.py +0 -0
  26. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/Java-Comparison-Tests/compareMetrics_Java-Vs-Python.py +0 -0
  27. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/RunMetrics.py +0 -0
  28. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/Surveyor.py +0 -0
  29. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/__init__.py +0 -0
  30. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/Confidence.py +0 -0
  31. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/Contig.py +0 -0
  32. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/ExtendedNote.py +0 -0
  33. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/Histogram.py +0 -0
  34. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/Judgement.py +0 -0
  35. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/Measurement.py +0 -0
  36. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/PianoRoll.py +0 -0
  37. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/PianoRollOld.py +0 -0
  38. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/__init__.py +0 -0
  39. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/test_ExtendedNote.py +0 -0
  40. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/test_Histogram.py +0 -0
  41. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/test_Measurement.py +0 -0
  42. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/test_PianoRoll_assertions.py +0 -0
  43. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/test_PianoRoll_integration.py +0 -0
  44. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/test_PianoRoll_quantization.py +0 -0
  45. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/data/test_PianoRoll_unit.py +0 -0
  46. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/Metric.py +0 -0
  47. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/ZipfMetrics.py +0 -0
  48. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/__init__.py +0 -0
  49. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ChordDensityMetric.py +0 -0
  50. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ChordDistanceMetric.py +0 -0
  51. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ChordMetric.py +0 -0
  52. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ChordNormalizedMetric.py +0 -0
  53. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ChromaticToneMetric.py +0 -0
  54. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationMetric.py +0 -0
  55. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationQuantizedMetric.py +0 -0
  56. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ContourBasslinePitchMetric.py +0 -0
  57. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationMetric.py +0 -0
  58. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationQuantizedMetric.py +0 -0
  59. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyPitchMetric.py +0 -0
  60. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/DurationBigramMetric.py +0 -0
  61. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/DurationDistanceMetric.py +0 -0
  62. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/DurationMetric.py +0 -0
  63. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedBigramMetric.py +0 -0
  64. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedDistanceMetric.py +0 -0
  65. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedMetric.py +0 -0
  66. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/HarmonicBigramMetric.py +0 -0
  67. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/HarmonicConsonanceMetric.py +0 -0
  68. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/HarmonicIntervalMetric.py +0 -0
  69. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/MelodicBigramMetric.py +0 -0
  70. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/MelodicConsonanceMetric.py +0 -0
  71. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/MelodicIntervalMetric.py +0 -0
  72. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/PitchDistanceMetric.py +0 -0
  73. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/PitchDurationMetric.py +0 -0
  74. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/PitchDurationQuantizedMetric.py +0 -0
  75. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/PitchMetric.py +0 -0
  76. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/RestMetric.py +0 -0
  77. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/__init__.py +0 -0
  78. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/test_DurationMetric.py +0 -0
  79. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_BasicIntervalsAndBigrams.py +0 -0
  80. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ChordsAndConsonance.py +0 -0
  81. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ContoursAndChromatic.py +0 -0
  82. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_QuantizedDurationsAndDistances.py +0 -0
  83. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/test_PitchMetric.py +0 -0
  84. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/simple/test_RestMetric.py +0 -0
  85. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/metrics/test_Metric.py +0 -0
  86. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/utilities/CSVWriter.py +0 -0
  87. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/utilities/PowerLawRandom.py +0 -0
  88. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/nevmuse/utilities/__init__.py +0 -0
  89. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython/notationRenderer.py +0 -0
  90. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython.egg-info/dependency_links.txt +0 -0
  91. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython.egg-info/requires.txt +0 -0
  92. {creativepython-1.1.4 → creativepython-1.1.5}/src/CreativePython.egg-info/top_level.txt +0 -0
  93. {creativepython-1.1.4 → creativepython-1.1.5}/src/bin/libportaudio.2.dylib +0 -0
  94. {creativepython-1.1.4 → creativepython-1.1.5}/src/iannix.py +0 -0
  95. {creativepython-1.1.4 → creativepython-1.1.5}/src/markov.py +0 -0
  96. {creativepython-1.1.4 → creativepython-1.1.5}/src/timer.py +0 -0
  97. {creativepython-1.1.4 → creativepython-1.1.5}/src/zipf.py +0 -0
  98. {creativepython-1.1.4 → creativepython-1.1.5}/tests/testAnimate.py +0 -0
  99. {creativepython-1.1.4 → creativepython-1.1.5}/tests/testHitTesting.py +0 -0
  100. {creativepython-1.1.4 → creativepython-1.1.5}/tests/testPeer.py +0 -0
  101. {creativepython-1.1.4 → creativepython-1.1.5}/tests/testToolTips.py +0 -0
  102. {creativepython-1.1.4 → creativepython-1.1.5}/tests/test_keyEvent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CreativePython
3
- Version: 1.1.4
3
+ Version: 1.1.5
4
4
  Summary: A Python-based software environment for developing algorithmic art projects.
5
5
  Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "CreativePython"
7
- version = "1.1.4"
7
+ version = "1.1.5"
8
8
  description = "A Python-based software environment for developing algorithmic art projects."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -86,11 +86,20 @@ def _launchRenderer(childCommandConnection, childPriorityConnection, parentPrior
86
86
 
87
87
  parentPriorityConnection.close() # child must not hold the parent end open
88
88
 
89
- # Redirect stderr (fd 2) to a log file so both Python tracebacks and C-level
90
- # output from Qt/Cocoa/XPC are captured for diagnostics.
91
- # The log is removed on clean exit if nothing was written to it.
92
- _logPath = os.path.join(os.getcwd(), "PENCIL_debug.log")
93
- _logFile = open(_logPath, "w")
89
+ # Set to True to redirect stderr to PENCIL_debug.log for diagnostics.
90
+ # When False, stderr is silenced (sent to /dev/null) to suppress Qt/Cocoa noise.
91
+ _DEBUG_LOG = False
92
+
93
+ if _DEBUG_LOG:
94
+ # Redirect stderr (fd 2) to a log file so both Python tracebacks and C-level
95
+ # output from Qt/Cocoa/XPC are captured for diagnostics.
96
+ # The log is removed on clean exit if nothing was written to it.
97
+ _logPath = os.path.join(os.getcwd(), "PENCIL_debug.log")
98
+ _logFile = open(_logPath, "w")
99
+ else:
100
+ _logPath = None
101
+ _logFile = open(os.devnull, "w")
102
+
94
103
  os.dup2(_logFile.fileno(), 2)
95
104
  sys.stderr = _logFile
96
105
 
@@ -103,7 +112,7 @@ def _launchRenderer(childCommandConnection, childPriorityConnection, parentPrior
103
112
  raise
104
113
  finally:
105
114
  _logFile.flush()
106
- if os.path.getsize(_logPath) == 0:
115
+ if _DEBUG_LOG and _logPath and os.path.getsize(_logPath) == 0:
107
116
  try:
108
117
  _logFile.close()
109
118
  os.unlink(_logPath)
@@ -549,9 +549,10 @@ class _QtGraphicsItemEventMixin:
549
549
  pos = event.scenePos()
550
550
  args = [int(pos.x()), int(pos.y())]
551
551
  self._send('mouseDown', args)
552
- # register this item for drag delivery during subsequent mouse moves
552
+ # register this item for release and drag delivery during subsequent mouse events
553
553
  view = self._qtview()
554
554
  if view is not None:
555
+ view._pressedItems.append(self._mirror.objectId) # track for manual release delivery (see QtView.mouseReleaseEvent)
555
556
  if (self._mirror.objectId, 'mouseDrag') in self._mirror.guiRenderer._registeredEvents:
556
557
  view._draggingItems.append(self._mirror.objectId)
557
558
  event.ignore() # allow cascade to items below
@@ -653,6 +654,7 @@ class QtView(QtWidgets.QGraphicsView):
653
654
  self._display = displayMirror
654
655
  self._pressPos = None # QPointF scene pos at last press
655
656
  self._draggingItems = [] # objectIds wanting drag events
657
+ self._pressedItems = [] # objectIds of items that received the last mousePressEvent
656
658
 
657
659
  # ── Helpers ────────────────────────────────────────────────────────────────
658
660
 
@@ -679,16 +681,28 @@ class QtView(QtWidgets.QGraphicsView):
679
681
 
680
682
  def mouseReleaseEvent(self, event):
681
683
  x, y = self._sceneXY(event)
682
- super().mouseReleaseEvent(event) # → item cascade via mixin
684
+
685
+ # Items call event.ignore() in mousePressEvent to allow cascade, so Qt never
686
+ # sets a mouse grabber and mouseReleaseEvent is never routed to items normally.
687
+ # Manually deliver mouseUp and mouseClick using the objectIds tracked at press time,
688
+ # mirroring the _draggingItems pattern used for mouseDrag.
689
+ registered = self._display.guiRenderer._registeredEvents
690
+ pos = self.mapToScene(event.position().toPoint())
691
+ is_click = (self._pressPos is not None
692
+ and abs(pos.x() - self._pressPos.x()) <= _QtGraphicsItemEventMixin._CLICK_THRESHOLD
693
+ and abs(pos.y() - self._pressPos.y()) <= _QtGraphicsItemEventMixin._CLICK_THRESHOLD)
694
+ for objectId in self._pressedItems:
695
+ if (objectId, 'mouseUp') in registered:
696
+ self._display.guiRenderer.sendEvent('mouseUp', objectId, [x, y])
697
+ if is_click and (objectId, 'mouseClick') in registered:
698
+ self._display.guiRenderer.sendEvent('mouseClick', objectId, [x, y])
699
+
683
700
  self._sendDisplay('mouseUp', [x, y])
684
- if self._pressPos is not None:
685
- pos = self.mapToScene(event.position().toPoint())
686
- dx = abs(pos.x() - self._pressPos.x())
687
- dy = abs(pos.y() - self._pressPos.y())
688
- if dx <= _QtGraphicsItemEventMixin._CLICK_THRESHOLD and dy <= _QtGraphicsItemEventMixin._CLICK_THRESHOLD:
689
- self._sendDisplay('mouseClick', [x, y])
701
+ if is_click:
702
+ self._sendDisplay('mouseClick', [x, y])
690
703
  self._pressPos = None
691
704
  self._draggingItems = []
705
+ self._pressedItems = []
692
706
 
693
707
  def mouseMoveEvent(self, event):
694
708
  x, y = self._sceneXY(event)
@@ -1,4 +1,10 @@
1
1
  import platform, ctypes, importlib.resources
2
+ from importlib.metadata import version as _metadata_version
3
+
4
+ try:
5
+ __version__ = _metadata_version("CreativePython")
6
+ except Exception:
7
+ __version__ = "unknown"
2
8
 
3
9
 
4
10
  # import portaudio binary on MacOS
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CreativePython
3
- Version: 1.1.4
3
+ Version: 1.1.5
4
4
  Summary: A Python-based software environment for developing algorithmic art projects.
5
5
  Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
6
6
  License: MIT License
@@ -88,7 +88,11 @@ src/CreativePython/nevmuse/utilities/PowerLawRandom.py
88
88
  src/CreativePython/nevmuse/utilities/__init__.py
89
89
  src/bin/libportaudio.2.dylib
90
90
  tests/testAnimate.py
91
+ tests/testCompress.py
92
+ tests/testGameboard.py
91
93
  tests/testHitTesting.py
92
94
  tests/testPeer.py
93
95
  tests/testToolTips.py
94
- tests/test_keyEvent.py
96
+ tests/test_keyEvent.py
97
+ tests/test_midi.py
98
+ tests/test_osc.py
@@ -197,24 +197,48 @@ class Color:
197
197
  """
198
198
  return self.red
199
199
 
200
+ def setRed(self, red):
201
+ """
202
+ Sets the Color's red value (0-255).
203
+ """
204
+ self.red = int(red)
205
+
200
206
  def getGreen(self):
201
207
  """
202
208
  Returns the Color's green value (0-255).
203
209
  """
204
210
  return self.green
205
211
 
212
+ def setGreen(self, green):
213
+ """
214
+ Sets the Color's green value (0-255).
215
+ """
216
+ self.green = int(green)
217
+
206
218
  def getBlue(self):
207
219
  """
208
220
  Returns the Color's blue value (0-255).
209
221
  """
210
222
  return self.blue
211
223
 
224
+ def setBlue(self, blue):
225
+ """
226
+ Sets the Color's blue value (0-255).
227
+ """
228
+ self.blue = int(blue)
229
+
212
230
  def getAlpha(self):
213
231
  """
214
232
  Returns the Color's transparency value (0-255).
215
233
  """
216
234
  return self.alpha
217
235
 
236
+ def setAlpha(self, alpha):
237
+ """
238
+ Sets the Color's transparency value (0-255).
239
+ """
240
+ self.alpha = int(alpha)
241
+
218
242
  def getRGB(self):
219
243
  """
220
244
  Returns the Color's values (0-255) as a list.
@@ -442,9 +466,9 @@ class Font:
442
466
  """
443
467
  Creates a new Font object.
444
468
  """
445
- self._name = name
446
- self._style = style
447
- self._size = size
469
+ self.name = name
470
+ self.style = style
471
+ self.size = size
448
472
 
449
473
  def __str__(self):
450
474
  return f'Font(name = "{self.getName()}", style = {self.getStyle()}, size = {self.getSize()}")'
@@ -456,7 +480,13 @@ class Font:
456
480
  """
457
481
  Returns the Font's family name as a string.
458
482
  """
459
- return self._name
483
+ return self.name
484
+
485
+ def setName(self, name):
486
+ """
487
+ Sets the Font's family name.
488
+ """
489
+ self.name = name
460
490
 
461
491
  def getStyle(self):
462
492
  """
@@ -464,13 +494,25 @@ class Font:
464
494
  The first value represents the Font's weight.
465
495
  The second value represents whether the Font is italicized.
466
496
  """
467
- return self._style
497
+ return self.style
498
+
499
+ def setStyle(self, style):
500
+ """
501
+ Sets the Font's style (e.g., Font.PLAIN, Font.BOLD, Font.ITALIC, Font.BOLDITALIC).
502
+ """
503
+ self.style = style
468
504
 
469
505
  def getSize(self):
470
506
  """
471
507
  Returns the Font's point size.
472
508
  """
473
- return self._size
509
+ return self.size
510
+
511
+ def setSize(self, size):
512
+ """
513
+ Sets the Font's point size.
514
+ """
515
+ self.size = size
474
516
 
475
517
 
476
518
  #######################################################################################
@@ -1071,6 +1113,15 @@ class Display(_Interactable):
1071
1113
  'font': fontData,
1072
1114
  })
1073
1115
 
1116
+ # ── Compatibility Aliases ─────────────────────────────────────────────────────────
1117
+
1118
+ def drawText(self, text, x, y, color=Color.BLACK, font=None):
1119
+ """
1120
+ Paints a text label onto the display's draw layer.
1121
+ Unlike add(), this is a one-time paint — no object is created or returned.
1122
+ The result is permanent until clearDrawing() is called.
1123
+ """
1124
+ self.drawLabel(text, x, y, color, font)
1074
1125
 
1075
1126
  #######################################################################################
1076
1127
  # Drawable - items that can be added to a Display
@@ -2179,6 +2230,7 @@ class Icon(_Graphics):
2179
2230
  self._height = height if height is not None else 0
2180
2231
  self._originalWidth = self._width
2181
2232
  self._originalHeight = self._height
2233
+ self._pixelCache = None # lazily populated on getPixel/getPixels; invalidated by crop/setPixel/setPixels
2182
2234
 
2183
2235
  _handler().sendCommand('create', self._objectId, {
2184
2236
  'type': 'Icon',
@@ -2224,8 +2276,9 @@ class Icon(_Graphics):
2224
2276
  """
2225
2277
  Crop the icon to the specified rectangle, relative to the icon's position.
2226
2278
  """
2227
- self._width = int(width)
2228
- self._height = int(height)
2279
+ self._width = int(width)
2280
+ self._height = int(height)
2281
+ self._pixelCache = None # invalidate local cache
2229
2282
  _handler().sendCommand('crop', self._objectId, {'x': x, 'y': y, 'width': width, 'height': height})
2230
2283
 
2231
2284
  # ── Pixel Manipulation ────────────────────────────────────────────────────────────
@@ -2234,27 +2287,31 @@ class Icon(_Graphics):
2234
2287
  """
2235
2288
  Returns the [r, g, b] color of a given pixel in the icon.
2236
2289
  """
2237
- result = _handler().sendQuery('getPixel', self._objectId, {'column': column, 'row': row})
2238
- return result
2290
+ if self._pixelCache is None: # fetch local cache, if needed
2291
+ self._pixelCache = _handler().sendQuery('getPixels', self._objectId)
2292
+ return self._pixelCache[row][column]
2239
2293
 
2240
2294
  def setPixel(self, column, row, color):
2241
2295
  """
2242
2296
  Sets the [r, g, b] color of a given pixel in the icon.
2243
2297
  """
2244
2298
  r, g, b = color
2299
+ self._pixelCache = None # invalidate local cache
2245
2300
  _handler().sendCommand('setPixel', self._objectId, {'column': column, 'row': row, 'color': [r, g, b]})
2246
2301
 
2247
2302
  def getPixels(self):
2248
2303
  """
2249
2304
  Returns the [r, g, b] color of all pixels in the icon as a 2-dimensional array.
2250
2305
  """
2251
- result = _handler().sendQuery('getPixels', self._objectId)
2252
- return result[0]
2306
+ if self._pixelCache is None: # fetch local cache, if needed
2307
+ self._pixelCache = _handler().sendQuery('getPixels', self._objectId)
2308
+ return self._pixelCache
2253
2309
 
2254
2310
  def setPixels(self, pixels):
2255
2311
  """
2256
2312
  Sets the [r, g, b] color of all pixels in the icon from a 2-dimensional array.
2257
2313
  """
2314
+ self._pixelCache = None # invalidate local cache
2258
2315
  _handler().sendCommand('setPixels', self._objectId, {'pixels': pixels})
2259
2316
 
2260
2317
 
@@ -24,8 +24,9 @@ class Image:
24
24
  # (int, None) -> the width of a square, blank canvas
25
25
  # (int, int) -> the width and height of a blank canvas
26
26
 
27
- self._display = Display() # create a new, blank Display
28
- self.read(arg1, arg2) # read() handles loading the icon, plus setting the title and size of the Display
27
+ self._display = Display() # the display the image is on
28
+ self._icon = None # the icon that holds the image data (created in read())
29
+ self.read(arg1, arg2) # read() loads the icon and sets Display properties
29
30
 
30
31
  def show(self):
31
32
  """