CreativePython 1.1.0__tar.gz → 1.1.2__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 (95) hide show
  1. {creativepython-1.1.0/src/CreativePython.egg-info → creativepython-1.1.2}/PKG-INFO +1 -1
  2. {creativepython-1.1.0 → creativepython-1.1.2}/pyproject.toml +1 -1
  3. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/GuiHandler.py +38 -25
  4. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/GuiRenderer.py +69 -89
  5. {creativepython-1.1.0 → creativepython-1.1.2/src/CreativePython.egg-info}/PKG-INFO +1 -1
  6. {creativepython-1.1.0 → creativepython-1.1.2}/src/gui.py +93 -55
  7. creativepython-1.1.2/src/image.py +123 -0
  8. creativepython-1.1.0/src/image.py +0 -70
  9. {creativepython-1.1.0 → creativepython-1.1.2}/LICENSE +0 -0
  10. {creativepython-1.1.0 → creativepython-1.1.2}/LICENSE-PSF +0 -0
  11. {creativepython-1.1.0 → creativepython-1.1.2}/MANIFEST.in +0 -0
  12. {creativepython-1.1.0 → creativepython-1.1.2}/README.md +0 -0
  13. {creativepython-1.1.0 → creativepython-1.1.2}/setup.cfg +0 -0
  14. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/RealtimeAudioPlayer.py +0 -0
  15. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/__init__.py +0 -0
  16. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/advMetricRunner.pythonSurvey.py +0 -0
  17. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/compareMetrics_Java-Vs-Python.py +0 -0
  18. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/RunMetrics.py +0 -0
  19. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/Surveyor.py +0 -0
  20. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/__init__.py +0 -0
  21. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Confidence.py +0 -0
  22. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Contig.py +0 -0
  23. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/ExtendedNote.py +0 -0
  24. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Histogram.py +0 -0
  25. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Judgement.py +0 -0
  26. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Measurement.py +0 -0
  27. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/PianoRoll.py +0 -0
  28. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/PianoRollOld.py +0 -0
  29. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/__init__.py +0 -0
  30. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_ExtendedNote.py +0 -0
  31. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_Histogram.py +0 -0
  32. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_Measurement.py +0 -0
  33. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_assertions.py +0 -0
  34. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_integration.py +0 -0
  35. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_quantization.py +0 -0
  36. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_unit.py +0 -0
  37. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/Metric.py +0 -0
  38. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/ZipfMetrics.py +0 -0
  39. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/__init__.py +0 -0
  40. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDensityMetric.py +0 -0
  41. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDistanceMetric.py +0 -0
  42. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordMetric.py +0 -0
  43. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordNormalizedMetric.py +0 -0
  44. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChromaticToneMetric.py +0 -0
  45. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationMetric.py +0 -0
  46. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationQuantizedMetric.py +0 -0
  47. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslinePitchMetric.py +0 -0
  48. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationMetric.py +0 -0
  49. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationQuantizedMetric.py +0 -0
  50. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyPitchMetric.py +0 -0
  51. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationBigramMetric.py +0 -0
  52. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationDistanceMetric.py +0 -0
  53. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationMetric.py +0 -0
  54. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedBigramMetric.py +0 -0
  55. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedDistanceMetric.py +0 -0
  56. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedMetric.py +0 -0
  57. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicBigramMetric.py +0 -0
  58. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicConsonanceMetric.py +0 -0
  59. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicIntervalMetric.py +0 -0
  60. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicBigramMetric.py +0 -0
  61. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicConsonanceMetric.py +0 -0
  62. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicIntervalMetric.py +0 -0
  63. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDistanceMetric.py +0 -0
  64. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationMetric.py +0 -0
  65. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationQuantizedMetric.py +0 -0
  66. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchMetric.py +0 -0
  67. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/RestMetric.py +0 -0
  68. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/__init__.py +0 -0
  69. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_DurationMetric.py +0 -0
  70. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_BasicIntervalsAndBigrams.py +0 -0
  71. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ChordsAndConsonance.py +0 -0
  72. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ContoursAndChromatic.py +0 -0
  73. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_QuantizedDurationsAndDistances.py +0 -0
  74. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_PitchMetric.py +0 -0
  75. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_RestMetric.py +0 -0
  76. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/test_Metric.py +0 -0
  77. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/CSVWriter.py +0 -0
  78. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/PowerLawRandom.py +0 -0
  79. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/__init__.py +0 -0
  80. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/notationRenderer.py +0 -0
  81. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython.egg-info/SOURCES.txt +0 -0
  82. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython.egg-info/dependency_links.txt +0 -0
  83. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython.egg-info/requires.txt +0 -0
  84. {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython.egg-info/top_level.txt +0 -0
  85. {creativepython-1.1.0 → creativepython-1.1.2}/src/bin/libportaudio.2.dylib +0 -0
  86. {creativepython-1.1.0 → creativepython-1.1.2}/src/iannix.py +0 -0
  87. {creativepython-1.1.0 → creativepython-1.1.2}/src/markov.py +0 -0
  88. {creativepython-1.1.0 → creativepython-1.1.2}/src/midi.py +0 -0
  89. {creativepython-1.1.0 → creativepython-1.1.2}/src/music.py +0 -0
  90. {creativepython-1.1.0 → creativepython-1.1.2}/src/osc.py +0 -0
  91. {creativepython-1.1.0 → creativepython-1.1.2}/src/timer.py +0 -0
  92. {creativepython-1.1.0 → creativepython-1.1.2}/src/zipf.py +0 -0
  93. {creativepython-1.1.0 → creativepython-1.1.2}/tests/testAnimate.py +0 -0
  94. {creativepython-1.1.0 → creativepython-1.1.2}/tests/testPeer.py +0 -0
  95. {creativepython-1.1.0 → creativepython-1.1.2}/tests/test_keyEvent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CreativePython
3
- Version: 1.1.0
3
+ Version: 1.1.2
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.0"
7
+ version = "1.1.2"
8
8
  description = "A Python-based software environment for developing algorithmic art projects."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -61,16 +61,25 @@ def _createEvent(eventType, target, args=None):
61
61
  # Child process entry point
62
62
  #######################################################################################
63
63
 
64
- def _launchRenderer(childConn):
64
+ def _launchRenderer(childConn, shutdownReadConn, shutdownWriteConn):
65
65
  """
66
66
  Entry point for the GuiRenderer child process.
67
67
  Defined here so GuiHandler can reference it without importing from GuiRenderer
68
68
  globally (which would also import Qt/PySide6 in the parent process).
69
69
 
70
+ shutdownReadConn is the read end of the dedicated shutdown pipe; the child
71
+ watches it with QSocketNotifier and shuts down when it becomes readable (EOF).
72
+ shutdownWriteConn is the write end inherited from the parent (fork) or passed
73
+ via pickling (forkserver/spawn); it is closed immediately so that only the
74
+ parent's copy of the write end remains open — ensuring the child sees EOF when
75
+ the parent closes its copy.
76
+
70
77
  At runtime this function only executes in the child process.
71
78
  """
72
79
  import sys, os
73
80
 
81
+ shutdownWriteConn.close() # child must not hold the write end open
82
+
74
83
  # Suppress macOS system noise (stale XPC connections inherited via fork)
75
84
  # that would confuse students. Redirect at the OS level (fd 2) so that
76
85
  # C-level libraries (Cocoa, XPC) are also silenced.
@@ -80,7 +89,7 @@ def _launchRenderer(childConn):
80
89
  sys.stderr = open(os.devnull, 'w')
81
90
 
82
91
  from CreativePython.GuiRenderer import GuiRenderer
83
- renderer = GuiRenderer(childConn)
92
+ renderer = GuiRenderer(childConn, shutdownReadConn)
84
93
  renderer.run()
85
94
 
86
95
 
@@ -140,6 +149,15 @@ class GuiHandler:
140
149
  ctx = multiprocessing.get_context('forkserver')
141
150
  else:
142
151
  ctx = multiprocessing.get_context('fork')
152
+
153
+ # Dedicated one-directional shutdown pipe. The parent holds the write
154
+ # end; the child holds the read end. Closing the write end in the parent
155
+ # sends EOF to the child's read end, which Qt detects immediately via
156
+ # QSocketNotifier — independent of the command queue and independent of
157
+ # whether atexit runs (the OS closes the fd automatically on process death).
158
+ shutdownReadConn, shutdownWriteConn = multiprocessing.Pipe(duplex=False)
159
+ self._shutdownPipe = shutdownWriteConn
160
+
143
161
  # With 'spawn' on POSIX (fork+exec), the forked helper calls
144
162
  # get_preparation_data() which reads sys.modules['__main__'].__file__.
145
163
  # When a user script is running in PENCIL's subprocess, __main__.__file__
@@ -159,7 +177,7 @@ class GuiHandler:
159
177
  try:
160
178
  self.childProcess = ctx.Process(
161
179
  target = _launchRenderer,
162
- args = (childConn,),
180
+ args = (childConn, shutdownReadConn, shutdownWriteConn),
163
181
  daemon = True # child is killed automatically if parent dies unexpectedly
164
182
  )
165
183
  self.childProcess.start()
@@ -169,7 +187,8 @@ class GuiHandler:
169
187
  _main_mod.__file__ = _saved_file
170
188
  if _saved_spec is not _UNSET:
171
189
  _main_mod.__spec__ = _saved_spec
172
- childConn.close() # parent no longer needs the child end of the Pipe
190
+ childConn.close() # parent no longer needs the child end of the command pipe
191
+ shutdownReadConn.close() # parent no longer needs the read end of the shutdown pipe
173
192
 
174
193
  # responseId counter — each sendQuery() call gets a unique ID so the
175
194
  # listener thread can route the response back to the correct caller
@@ -214,11 +233,10 @@ class GuiHandler:
214
233
 
215
234
  def _listenForMessages(self):
216
235
  """
217
- Background thread. Reads all incoming messages from GuiRenderer and
218
- routes each one based on its type:
219
- - 'responseId' key present response for a waiting sendQuery() call
220
- - 'type' key present → event for a registered callback
221
- Exits when the pipe is closed (EOFError).
236
+ Background thread. Reads incoming messages from GuiRenderer and routes
237
+ each one: responses unblock waiting sendQuery() callers; events invoke
238
+ registered callbacks. Exits when the child closes the pipe (EOFError)
239
+ and releases any sendQuery() callers still waiting for a response.
222
240
  """
223
241
  while True:
224
242
  try:
@@ -228,7 +246,11 @@ class GuiHandler:
228
246
  elif 'type' in message:
229
247
  self._dispatchEvent(message)
230
248
  except EOFError:
231
- break # pipe closed; thread exits cleanly
249
+ break
250
+ with self._pendingLock:
251
+ for slot in self._pendingResponses.values():
252
+ slot['values'] = ['shutdown']
253
+ slot['event'].set()
232
254
 
233
255
  def _handleResponse(self, responseDict):
234
256
  """
@@ -334,23 +356,14 @@ class GuiHandler:
334
356
 
335
357
  def _shutdown(self):
336
358
  """
337
- Kills the child process immediately and closes the pipe.
359
+ Signals the child to shut down and waits for it to exit.
360
+ Closing the shutdown pipe delivers EOF to the child's QSocketNotifier,
361
+ which fires _onShutdownSignal on Qt's main thread immediately — even if
362
+ atexit does not run, since the OS closes the pipe fd on process death.
338
363
  Called automatically via atexit when the parent process exits.
339
- The child is killed with SIGKILL so it does not drain its event queue —
340
- exit() in the parent should return promptly.
341
364
  """
342
- if self.childProcess.is_alive():
343
- # SIGTERM is overridden in the child to call os._exit(0) immediately,
344
- # bypassing Qt's graceful shutdown (which drains the event queue).
345
- # Python delivers SIGTERM between bytecodes, so it fires within one
346
- # iteration of the child's _pollPipe loop — far faster than SIGKILL,
347
- # which macOS may defer while a Metal/GPU operation is in flight.
348
- self.childProcess.terminate()
349
- self.childProcess.join(timeout=0.5) # should die in < 1 ms
350
- if self.childProcess.is_alive():
351
- self.childProcess.kill() # backup: SIGKILL
352
- self.childProcess.join(timeout=2.0)
353
- self.connection.close()
365
+ self._shutdownPipe.close()
366
+ self.childProcess.join(timeout=1.0)
354
367
 
355
368
 
356
369
  #######################################################################################
@@ -75,30 +75,20 @@ class GuiRenderer:
75
75
  Unknown targets and unknown actions are silently ignored.
76
76
  """
77
77
 
78
- def __init__(self, childConn):
78
+ def __init__(self, childConn, shutdownReadConn):
79
79
  """
80
80
  Creates the QApplication and initializes all registries.
81
81
  """
82
- self.connection = childConn
82
+ self.connection = childConn
83
+ self._shutdownConn = shutdownReadConn
83
84
 
84
85
  # QApplication must be created first; it owns the Qt event loop.
85
86
  self._app = QtWidgets.QApplication([])
86
87
 
87
- # Do not quit when the last window is closed. The default behaviour
88
- # (quitOnLastWindowClosed = True) would exit the child process the
89
- # moment the user closes a Display, breaking the IPC pipe and making
90
- # it impossible to open new Displays without restarting Python.
91
- # The child only exits when the parent sends a 'shutdown' command or
92
- # kills the process directly.
88
+ # Do not quit when the last window is closed the child stays alive
89
+ # until the parent closes the shutdown pipe, triggering _onShutdownSignal.
93
90
  self._app.setQuitOnLastWindowClosed(False)
94
91
 
95
- # Qt installs a SIGTERM handler that drains the event queue before
96
- # exiting (graceful shutdown). When the parent kills this process,
97
- # we want immediate exit — override Qt's handler after QApplication
98
- # is constructed so ours takes precedence.
99
- import signal, os as _os
100
- signal.signal(signal.SIGTERM, lambda sig, frame: _os._exit(0))
101
-
102
92
  # maps objectId (int) -> mirror object (e.g. DisplayMirror, RectangleMirror)
103
93
  self._objectRegistry = {}
104
94
 
@@ -108,87 +98,58 @@ class GuiRenderer:
108
98
 
109
99
  def run(self):
110
100
  """
111
- Starts the pipe-polling timer and enters Qt's event loop.
112
- The QTimer fires _pollPipe() every 8ms (~120 Hz), draining any pending
113
- commands without blocking the Qt event loop between frames.
101
+ Wires up the command-polling timer and the shutdown notifier, then enters
102
+ Qt's event loop.
103
+ _pollPipe fires every 8ms to process incoming commands in fixed batches.
104
+ _shutdownNotifier watches the dedicated shutdown pipe fd; when the parent
105
+ closes its write end (via _shutdown() or process death), Qt fires
106
+ _onShutdownSignal on the main thread immediately — independent of the
107
+ command queue and of whether atexit runs in the parent.
114
108
  """
115
109
  self._timer = QtCore.QTimer()
116
110
  self._timer.timeout.connect(self._pollPipe)
117
111
  self._timer.start(8)
118
- self._app.exec()
119
112
 
120
- _polling = False # reentrancy guard
113
+ self._shutdownNotifier = QtCore.QSocketNotifier(
114
+ self._shutdownConn.fileno(),
115
+ QtCore.QSocketNotifier.Type.Read
116
+ )
117
+ self._shutdownNotifier.activated.connect(self._onShutdownSignal)
118
+
119
+ self._app.exec()
121
120
 
122
- # # ── Option A: no batching (drain entire queue before rendering) ─────────
123
- # def _pollPipe(self):
124
- # """
125
- # Drains all pending commands before returning to Qt's event loop.
126
- # """
127
- # while self.connection.poll():
128
- # message = self.connection.recv()
129
- # keepRunning = self._routeCommand(message)
130
- # if not keepRunning:
131
- # break
121
+ # ── Command polling ───────────────────────────────────────────────────────
132
122
 
133
- # ── Option B: fixed batch (let Qt render every N commands) ──────────────
134
123
  _BATCH_SIZE = 10
135
-
124
+
136
125
  def _pollPipe(self):
137
126
  """
138
- Drains pending commands, letting Qt process events every _BATCH_SIZE
139
- commands so the display updates incrementally during large batches.
140
- """
141
- if self._polling:
142
- return # processEvents() fired the timer again — skip
143
- self._polling = True
144
- try:
145
- count = 0
146
- while self.connection.poll():
147
- message = self.connection.recv()
148
- keepRunning = self._routeCommand(message)
149
- if not keepRunning:
150
- break
151
- count += 1
152
- if count % self._BATCH_SIZE == 0:
153
- self._app.processEvents()
154
- finally:
155
- self._polling = False
156
-
157
- # # ── Option C: adaptive batch (scales with scene complexity) ─────────────
158
- # _BATCH_MIN = 10 # batch size when scene is empty
159
- # _BATCH_MAX = 1000 # batch size ceiling
160
- # _BATCH_PER = 25 # add this many to the batch per 1000 objects in scene
161
-
162
- # def _pollPipe(self):
163
- # """
164
- # Drains pending commands, letting Qt process events at adaptive
165
- # intervals. Small scenes get frequent updates (responsive feel);
166
- # large scenes batch more commands between repaints (less overhead
167
- # from increasingly expensive repaints).
168
- # """
169
- # if self._polling:
170
- # return # processEvents() fired the timer again — skip
171
- # self._polling = True
172
- # try:
173
- # objectCount = len(self._objectRegistry)
174
- # batchSize = min(self._BATCH_MAX,
175
- # self._BATCH_MIN + objectCount * self._BATCH_PER)
176
- # count = 0
177
- # while self.connection.poll():
178
- # message = self.connection.recv()
179
- # keepRunning = self._routeCommand(message)
180
- # if not keepRunning:
181
- # break
182
- # count += 1
183
- # if count % batchSize == 0:
184
- # self._app.processEvents()
185
- # finally:
186
- # self._polling = False
127
+ Processes up to _BATCH_SIZE pending commands per timer tick, then returns
128
+ control to Qt's event loop. Keeping batches small lets Qt handle input
129
+ events and repaints between each batch, keeping the UI responsive during
130
+ heavy animation.
131
+ """
132
+ count = 0
133
+ while count < self._BATCH_SIZE and self.connection.poll():
134
+ try:
135
+ message = self.connection.recv()
136
+ except (EOFError, OSError):
137
+ # Command pipe closed — parent process died without sending a shutdown
138
+ # signal (e.g. killed by SIGTERM/SIGKILL, atexit didn't run).
139
+ # _onShutdownSignal may also fire if the OS released the shutdown pipe
140
+ # write end, but _app.quit() is safe to call from either path.
141
+ self._timer.stop()
142
+ self._shutdownNotifier.setEnabled(False)
143
+ self._app.quit()
144
+ return
145
+ self._routeCommand(message)
146
+ count += 1
187
147
 
188
148
  def _routeCommand(self, commandDict):
189
149
  """
190
150
  Routes an incoming command to the appropriate handler.
191
- Returns False if the renderer should shut down, True otherwise.
151
+ Shutdown is handled out-of-band by _onShutdownSignal; unknown actions are
152
+ silently ignored.
192
153
  """
193
154
  action = commandDict.get('action')
194
155
  target = commandDict.get('target')
@@ -196,11 +157,7 @@ class GuiRenderer:
196
157
  responseId = commandDict.get('responseId')
197
158
 
198
159
  # ── Built-in actions ──────────────────────────────────────────────────
199
- if action == 'shutdown':
200
- self._handleShutdown()
201
- return False
202
-
203
- elif action == 'ping':
160
+ if action == 'ping':
204
161
  response = _createResponse(responseId, ['pong'])
205
162
  self.connection.send(response)
206
163
 
@@ -356,9 +313,32 @@ class GuiRenderer:
356
313
  event = _createEvent(eventType, objectId, args)
357
314
  self.connection.send(event)
358
315
 
359
- def _handleShutdown(self):
360
- """Stops the polling timer and quits the Qt event loop."""
316
+ def _onShutdownSignal(self):
317
+ """
318
+ Called by Qt when the shutdown pipe becomes readable — meaning the parent
319
+ has closed its write end (via _shutdown() or process death).
320
+ Fires immediately on the Qt main thread, independent of the command queue.
321
+ Drains any remaining commands from the command pipe, sending 'shutdown'
322
+ responses to release any sendQuery() callers blocking in the parent.
323
+ Fire-and-forget commands are discarded. Then quits Qt's event loop
324
+ normally so Qt can clean up its own resources.
325
+ """
326
+ self._shutdownNotifier.setEnabled(False)
361
327
  self._timer.stop()
328
+
329
+ # Drain remaining commands. Queries get a 'shutdown' response so that
330
+ # any sendQuery() call blocking in the parent is released immediately.
331
+ # Fire-and-forget commands are discarded.
332
+ # If the command pipe is already closed, the drain is skipped gracefully.
333
+ try:
334
+ while self.connection.poll():
335
+ message = self.connection.recv()
336
+ responseId = message.get('responseId')
337
+ if responseId is not None:
338
+ self.connection.send(_createResponse(responseId, ['shutdown']))
339
+ except (EOFError, OSError):
340
+ pass # command pipe already closed; listener releases pending queries
341
+
362
342
  self._app.quit()
363
343
 
364
344
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CreativePython
3
- Version: 1.1.0
3
+ Version: 1.1.2
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
@@ -2,7 +2,6 @@
2
2
  # gui.py Version 1.0 13-Mar-2026
3
3
  # Taj Ballinger, Trevor Ritchie, Bill Manaris, and Dana Hughes
4
4
  #######################################################################################
5
-
6
5
  import numpy as np
7
6
 
8
7
  from CreativePython.GuiHandler import _createHandler
@@ -934,19 +933,19 @@ class Display(_Interactable):
934
933
  raise TypeError(f'{type(self).__name__}.addPopupMenu(): menu should be a Menu object (it was {type(menu).__name__})')
935
934
  _handler().sendCommand('addPopupMenu', self._objectId, {'menuId': menu._objectId})
936
935
 
937
- def write(self, filename, width=None, height=None):
936
+ def save(self, filename, width=None, height=None):
938
937
  """
939
938
  Saves the display's currently visible canvas to an image file.
940
939
  The file format is determined by the filename extension (e.g. .png, .jpg).
941
940
  Optional width and height resize the saved image; omitting one preserves the aspect ratio.
942
- Prints whether the write succeeded and the resolved file path.
941
+ Prints whether the save succeeded and the resolved file path.
943
942
  """
944
943
  result = _handler().sendQuery('write', self._objectId, {'filename': filename, 'width': width, 'height': height})
945
944
  success, resolvedPath = result[0], result[1]
946
945
  if success:
947
- print(f'{type(self).__name__}.write(): saved to "{resolvedPath}"')
946
+ print(f'{type(self).__name__}.save(): saved canvas to "{resolvedPath}"')
948
947
  else:
949
- print(f'{type(self).__name__}.write(): failed to save "{resolvedPath}"')
948
+ print(f'{type(self).__name__}.save(): failed to save to "{resolvedPath}"')
950
949
 
951
950
  def onClose(self, action):
952
951
  """
@@ -1312,6 +1311,34 @@ class _Drawable(_Interactable):
1312
1311
 
1313
1312
  return x, y, width, height
1314
1313
 
1314
+ def _rotatePoints(self, xPoints, yPoints):
1315
+ """
1316
+ Rotates a list of global-coordinate points around this item's center.
1317
+ Returns (xPoints, yPoints) — post-rotation, as lists of ints.
1318
+ If rotation == 0, returns the inputs unchanged.
1319
+ """
1320
+ if self._rotation != 0:
1321
+ globalX, globalY = self._getGlobalCorner()
1322
+ cx = globalX + self._width / 2
1323
+ cy = globalY + self._height / 2
1324
+
1325
+ radians = np.radians(self._rotation)
1326
+ cos = np.cos(radians)
1327
+ sin = np.sin(radians)
1328
+
1329
+ rotatedXs = []
1330
+ rotatedYs = []
1331
+ for px, py in zip(xPoints, yPoints):
1332
+ dx = px - cx
1333
+ dy = py - cy
1334
+ rotatedXs.append(int(cx + dx * cos + dy * sin))
1335
+ rotatedYs.append(int(cy - dx * sin + dy * cos))
1336
+
1337
+ xPoints = rotatedXs
1338
+ yPoints = rotatedYs
1339
+
1340
+ return xPoints, yPoints
1341
+
1315
1342
  # ── Position ───────────────────────────────────────────────────────────────
1316
1343
 
1317
1344
  def getCenter(self):
@@ -1677,9 +1704,9 @@ class Rectangle(_Graphics):
1677
1704
  })
1678
1705
 
1679
1706
  def __str__(self):
1680
- x1, y1 = self._getGlobalCorner()
1681
- x2 = x1 + self._width
1682
- y2 = y1 + self._height
1707
+ xPoints, yPoints = self._getEndpoints()
1708
+ x1, y1 = xPoints[0], yPoints[0]
1709
+ x2, y2 = xPoints[2], yPoints[2]
1683
1710
  rotation = self.getRotation()
1684
1711
  color = self.getColor()
1685
1712
  fill = self.getFilled()
@@ -1688,21 +1715,28 @@ class Rectangle(_Graphics):
1688
1715
 
1689
1716
  # ── Coordinates ────────────────────────────────────────────────────────────────
1690
1717
 
1691
- def getEndPoints(self):
1718
+ def _getEndpoints(self):
1692
1719
  """
1693
- Returns [xPoints, yPoints]
1720
+ Returns (xPoints, yPoints) — the four pre-rotation corners, in global coordinates.
1721
+ Corner order: top-left, bottom-left, bottom-right, top-right.
1694
1722
  """
1695
- # rebuild global xPoints and yPoints, accounting for position and rotation changes
1696
- x1 = self._localCornerX
1697
- x2 = self._width
1698
- y1 = self._localCornerY
1699
- y2 = self._height
1723
+ globalX, globalY = self._getGlobalCorner()
1724
+ w = self._width
1725
+ h = self._height
1700
1726
 
1701
- xPoints = [int(x1), int(x1), int(x2), int(x2)]
1702
- yPoints = [int(y1), int(y2), int(y2), int(y1)]
1727
+ xPoints = [int(globalX), int(globalX), int(globalX + w), int(globalX + w)]
1728
+ yPoints = [int(globalY), int(globalY + h), int(globalY + h), int(globalY)]
1703
1729
 
1704
1730
  return xPoints, yPoints
1705
1731
 
1732
+ def getEndpoints(self):
1733
+ """
1734
+ Returns (xPoints, yPoints) — the four corners after rotation, in global coordinates.
1735
+ Original corner order: top-left, bottom-left, bottom-right, top-right.
1736
+ """
1737
+ xPoints, yPoints = self._getEndpoints()
1738
+ return self._rotatePoints(xPoints, yPoints)
1739
+
1706
1740
 
1707
1741
  class Oval(_Graphics):
1708
1742
  """
@@ -1976,7 +2010,7 @@ class Line(_Graphics):
1976
2010
  })
1977
2011
 
1978
2012
  def __str__(self):
1979
- xPoints, yPoints = self.getEndPoints()
2013
+ xPoints, yPoints = self._getEndpoints()
1980
2014
  x1, x2 = xPoints
1981
2015
  y1, y2 = yPoints
1982
2016
  rotation = self.getRotation()
@@ -1986,23 +2020,26 @@ class Line(_Graphics):
1986
2020
 
1987
2021
  # ── Coordinates ────────────────────────────────────────────────────────────────
1988
2022
 
1989
- def getEndPoints(self):
2023
+ def _getEndpoints(self):
1990
2024
  """
1991
- Returns [x1, y1, x2, y2, rotation]
1992
- i.e. The values needed to construct a copy of this Line.
2025
+ Returns ([x1, x2], [y1, y2]) — the two pre-rotation endpoints, in global coordinates.
1993
2026
  """
1994
- # rebuild global xPoints and yPoints, accounting for position, dimension, and rotation changes
1995
2027
  scaleX = 0 if (self._originalWidth == 0) else (self._width / self._originalWidth)
1996
2028
  scaleY = 0 if (self._originalHeight == 0) else (self._height / self._originalHeight)
1997
2029
 
1998
2030
  globalX, globalY = self._getGlobalCorner()
1999
2031
 
2000
- x1 = int(globalX + (self._xPoints[0] * scaleX))
2001
- y1 = int(globalY + (self._yPoints[0] * scaleY))
2002
- x2 = int(globalX + (self._xPoints[1] * scaleX))
2003
- y2 = int(globalY + (self._yPoints[1] * scaleY))
2032
+ xPoints = [int(globalX + (self._xPoints[i] * scaleX)) for i in range(2)]
2033
+ yPoints = [int(globalY + (self._yPoints[i] * scaleY)) for i in range(2)]
2034
+
2035
+ return xPoints, yPoints
2004
2036
 
2005
- return [x1, x2], [y1, y2]
2037
+ def getEndpoints(self):
2038
+ """
2039
+ Returns ([x1, x2], [y1, y2]) — the two endpoints after rotation, in global coordinates.
2040
+ """
2041
+ xPoints, yPoints = self._getEndpoints()
2042
+ return self._rotatePoints(xPoints, yPoints)
2006
2043
 
2007
2044
  # ── Length ────────────────────────────────────────────────────────────────
2008
2045
 
@@ -2017,7 +2054,7 @@ class Line(_Graphics):
2017
2054
  Sets the distance between the line's endpoints (in pixels).
2018
2055
  The line's second endpoint (x2, y2) moves to match.
2019
2056
  """
2020
- xPoints, yPoints = self.getEndPoints()
2057
+ xPoints, yPoints = self.getEndpoints()
2021
2058
  x1, x2 = xPoints
2022
2059
  y1, y2 = yPoints
2023
2060
 
@@ -2111,7 +2148,7 @@ class Polyline(_Graphics):
2111
2148
  })
2112
2149
 
2113
2150
  def __str__(self):
2114
- xPoints, yPoints = self.getEndPoints()
2151
+ xPoints, yPoints = self._getEndpoints()
2115
2152
  rotation = self.getRotation()
2116
2153
  color = self.getColor()
2117
2154
  thickness = self.getThickness()
@@ -2119,27 +2156,27 @@ class Polyline(_Graphics):
2119
2156
 
2120
2157
  # ── Coordinates ────────────────────────────────────────────────────────────────
2121
2158
 
2122
- def getEndPoints(self):
2159
+ def _getEndpoints(self):
2123
2160
  """
2124
- Returns [xPoints, yPoints]
2125
- i.e. The values needed to construct a copy of this Polyline.
2161
+ Returns (xPoints, yPoints) — all pre-rotation points, in global coordinates.
2126
2162
  """
2127
- # rebuild global xPoints and yPoints, accounting for position and dimension changes
2128
2163
  scaleX = 0 if (self._originalWidth == 0) else (self._width / self._originalWidth)
2129
2164
  scaleY = 0 if (self._originalHeight == 0) else (self._height / self._originalHeight)
2130
2165
 
2131
2166
  globalX, globalY = self._getGlobalCorner()
2132
- xPoints = []
2133
- yPoints = []
2134
2167
 
2135
- for i in range(len(self._xPoints)):
2136
- x = int(globalX + (self._xPoints[i] * scaleX))
2137
- y = int(globalY + (self._yPoints[i] * scaleY))
2138
- xPoints.append(x)
2139
- yPoints.append(y)
2168
+ xPoints = [int(globalX + (self._xPoints[i] * scaleX)) for i in range(len(self._xPoints))]
2169
+ yPoints = [int(globalY + (self._yPoints[i] * scaleY)) for i in range(len(self._yPoints))]
2140
2170
 
2141
2171
  return xPoints, yPoints
2142
2172
 
2173
+ def getEndpoints(self):
2174
+ """
2175
+ Returns (xPoints, yPoints) — all points after rotation, in global coordinates.
2176
+ """
2177
+ xPoints, yPoints = self._getEndpoints()
2178
+ return self._rotatePoints(xPoints, yPoints)
2179
+
2143
2180
 
2144
2181
  class Polygon(_Graphics):
2145
2182
  """
@@ -2184,7 +2221,7 @@ class Polygon(_Graphics):
2184
2221
  })
2185
2222
 
2186
2223
  def __str__(self):
2187
- xPoints, yPoints = self.getEndPoints()
2224
+ xPoints, yPoints = self._getEndpoints()
2188
2225
  rotation = self.getRotation()
2189
2226
  color = self.getColor()
2190
2227
  fill = self.getFilled()
@@ -2193,26 +2230,27 @@ class Polygon(_Graphics):
2193
2230
 
2194
2231
  # ── Coordinates ────────────────────────────────────────────────────────────────
2195
2232
 
2196
- def getEndPoints(self):
2233
+ def _getEndpoints(self):
2197
2234
  """
2198
- Returns [xPoints, yPoints, rotation]
2235
+ Returns (xPoints, yPoints) — all pre-rotation points, in global coordinates.
2199
2236
  """
2200
- # rebuild global xPoints and yPoints, accounting for position and dimension changes
2201
2237
  scaleX = 0 if (self._originalWidth == 0) else (self._width / self._originalWidth)
2202
2238
  scaleY = 0 if (self._originalHeight == 0) else (self._height / self._originalHeight)
2203
2239
 
2204
2240
  globalX, globalY = self._getGlobalCorner()
2205
- xPoints = []
2206
- yPoints = []
2207
2241
 
2208
- for i in range(len(self._xPoints)):
2209
- x = int(globalX + (self._xPoints[i] * scaleX))
2210
- y = int(globalY + (self._yPoints[i] * scaleY))
2211
- xPoints.append(x)
2212
- yPoints.append(y)
2242
+ xPoints = [int(globalX + (self._xPoints[i] * scaleX)) for i in range(len(self._xPoints))]
2243
+ yPoints = [int(globalY + (self._yPoints[i] * scaleY)) for i in range(len(self._yPoints))]
2213
2244
 
2214
2245
  return xPoints, yPoints
2215
2246
 
2247
+ def getEndpoints(self):
2248
+ """
2249
+ Returns (xPoints, yPoints) — all points after rotation, in global coordinates.
2250
+ """
2251
+ xPoints, yPoints = self._getEndpoints()
2252
+ return self._rotatePoints(xPoints, yPoints)
2253
+
2216
2254
 
2217
2255
  class Icon(_Graphics):
2218
2256
  """
@@ -2258,9 +2296,9 @@ class Icon(_Graphics):
2258
2296
  rotation = self.getRotation()
2259
2297
  return f'Icon(filename = "{filename}", width = {width}, height = {height}, rotation = {rotation})'
2260
2298
 
2261
- # ── Write ────────────────────────────────────────────────────────────────
2299
+ # ── Save ────────────────────────────────────────────────────────────────
2262
2300
 
2263
- def write(self, filename, width=None, height=None):
2301
+ def save(self, filename, width=None, height=None):
2264
2302
  """
2265
2303
  Saves the icon to an image file.
2266
2304
  The file format is determined by the filename extension (e.g. .png, .jpg).
@@ -2270,9 +2308,9 @@ class Icon(_Graphics):
2270
2308
  result = _handler().sendQuery('write', self._objectId, {'filename': filename, 'width': width, 'height': height})
2271
2309
  success, resolvedPath = result[0], result[1]
2272
2310
  if success:
2273
- print(f'{type(self).__name__}.write(): saved to "{resolvedPath}"')
2311
+ print(f'{type(self).__name__}.save(): saved canvas to "{resolvedPath}"')
2274
2312
  else:
2275
- print(f'{type(self).__name__}.write(): failed to save "{resolvedPath}"')
2313
+ print(f'{type(self).__name__}.save(): failed to save "{resolvedPath}"')
2276
2314
 
2277
2315
  # ── Crop ────────────────────────────────────────────────────────────────
2278
2316
 
@@ -0,0 +1,123 @@
1
+ ################################################################################
2
+ # image.py Version 1.0 30-Jan-2025
3
+ # Taj Ballinger, Trevor Ritchie, and Bill Manaris
4
+ #
5
+ ##############################################################
6
+ ##################
7
+
8
+ from gui import Display, Icon
9
+
10
+ class Image:
11
+ """
12
+ Display window for rendering images.
13
+ """
14
+ def __init__(self, arg1, arg2=None):
15
+ """
16
+ Creates a new image display.
17
+ """
18
+ # JythonMusic's Image class has overloaded constructors,
19
+ # but Python doesn't allow that. We replicate an overload based on the
20
+ # data types of arg1 and arg2.
21
+ #
22
+ # (str, None) -> a filename for an image to load
23
+ # (str, int) -> a filename for an image to load, scaled to a width
24
+ # (int, None) -> the width of a square, blank canvas
25
+ # (int, int) -> the width and height of a blank canvas
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
29
+
30
+ def show(self):
31
+ """
32
+ Shows the display window.
33
+ """
34
+ self._display.show()
35
+
36
+ def hide(self):
37
+ """
38
+ Hides the display window.
39
+ """
40
+ self._display.hide()
41
+
42
+ def getWidth(self):
43
+ """
44
+ Returns the display's canvas width (in pixels).
45
+ """
46
+ return self._display.getWidth()
47
+
48
+ def getHeight(self):
49
+ """
50
+ Returns the display's canvas height (in pixels).
51
+ """
52
+ return self._display.getHeight()
53
+
54
+ # pixel manipulation wrappers
55
+
56
+ def getPixel(self, col, row):
57
+ """
58
+ Returns the [r, g, b] color of a given pixel in the image.
59
+ """
60
+ return self._icon.getPixel(col, row)
61
+
62
+ def setPixel(self, col, row, RGBList):
63
+ """
64
+ Sets the [r, g, b] color of a given pixel in the image.
65
+ """
66
+ self._icon.setPixel(col, row, RGBList)
67
+
68
+ def getPixels(self):
69
+ """
70
+ Returns the [r, g, b] color of all pixels in the icon as a 2-dimensional array.
71
+ """
72
+ return self._icon.getPixels()
73
+
74
+ def setPixels(self, pixels):
75
+ """
76
+ Sets the [r, g, b] color of all pixels in the icon from a 2-dimensional array.
77
+ """
78
+ self._icon.setPixels(pixels)
79
+
80
+ def write(self, filename):
81
+ """
82
+ Saves the display's currently visible canvas to an image file.
83
+ The file format is determined by the filename extension (e.g. .png, .jpg).
84
+ """
85
+ self._display.save(filename)
86
+
87
+ def read(self, arg1, arg2=None):
88
+ """
89
+ Updates the image with a new filename.
90
+ arg1 and arg2 have the same overloaded behavior as Image's constructor.
91
+ (str, None) -> a filename for an image to load
92
+ (str, int) -> a filename for an image to load, scaled to a width
93
+ (int, None) -> the width of a square, blank canvas
94
+ (int, int) -> the width and height of a blank canvas
95
+ """
96
+ # first, load the icon
97
+ if isinstance(arg1, str):
98
+ # arg1 is a filename
99
+ title = arg1
100
+ icon = Icon(arg1, arg2) # load icon with optional resizing
101
+ else:
102
+ # arg1 is a image size
103
+ title = "Image"
104
+ icon = Icon("", arg1, arg2) # load blank icon, sized to given dimensions
105
+
106
+ # remove the previous icon, if needed
107
+ if self._icon is not None:
108
+ self._display.remove(self._icon)
109
+
110
+ # next, update the Display
111
+ width, height = icon.getSize()
112
+ self._display.setSize(width, height)
113
+ self._display.setTitle(title)
114
+
115
+ # finally, add new icon to the display
116
+ self._icon = icon
117
+ self._display.add(icon)
118
+
119
+
120
+ ###### Unit Tests ###################################
121
+
122
+ if __name__ == "__main__":
123
+ pass
@@ -1,70 +0,0 @@
1
- ################################################################################
2
- # image.py Version 1.0 30-Jan-2025
3
- # Taj Ballinger, Trevor Ritchie, and Bill Manaris
4
- #
5
- ##############################################################
6
- ##################
7
-
8
- from gui import Display, Icon
9
-
10
- class Image(Display):
11
- """
12
- Display window for rendering images.
13
- """
14
- def __init__(self, arg1, arg2=None):
15
- """
16
- Creates a new image display.
17
- """
18
- # JythonMusic's Image class has overloaded constructors,
19
- # but Python doesn't allow that. We replicate an overload based on the
20
- # data types of arg1 and arg2.
21
- #
22
- # (str, None) -> a filename for an image to load
23
- # (str, int) -> a filename for an image to load, scaled to a width
24
- # (int, None) -> the width of a square, blank canvas
25
- # (int, int) -> the width and height of a blank canvas
26
-
27
- if isinstance(arg1, str):
28
- # this is a filename
29
- title = arg1
30
- self._icon = Icon(arg1, arg2) # load icon with optional parameter
31
-
32
- else:
33
- title = "Image"
34
- self._icon = Icon("", arg1, arg2) # load blank icon with given dimensions
35
-
36
- width, height = self._icon.getSize()
37
-
38
- Display.__init__(self, title, width, height)
39
- self.add(self._icon)
40
-
41
- # pixel manipulation wrappers
42
-
43
- def getPixel(self, col, row):
44
- """
45
- Returns the [r, g, b] color of a given pixel in the image.
46
- """
47
- return self._icon.getPixel(col, row)
48
-
49
- def setPixel(self, col, row, RGBList):
50
- """
51
- Sets the [r, g, b] color of a given pixel in the image.
52
- """
53
- self._icon.setPixel(col, row, RGBList)
54
-
55
- def getPixels(self):
56
- """
57
- Returns the [r, g, b] color of all pixels in the icon as a 2-dimensional array.
58
- """
59
- return self._icon.getPixels()
60
-
61
- def setPixels(self, pixels):
62
- """
63
- Sets the [r, g, b] color of all pixels in the icon from a 2-dimensional array.
64
- """
65
- self._icon.setPixels(pixels)
66
-
67
- ###### Unit Tests ###################################
68
-
69
- if __name__ == "__main__":
70
- pass
File without changes
File without changes
File without changes