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.
- {creativepython-1.1.0/src/CreativePython.egg-info → creativepython-1.1.2}/PKG-INFO +1 -1
- {creativepython-1.1.0 → creativepython-1.1.2}/pyproject.toml +1 -1
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/GuiHandler.py +38 -25
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/GuiRenderer.py +69 -89
- {creativepython-1.1.0 → creativepython-1.1.2/src/CreativePython.egg-info}/PKG-INFO +1 -1
- {creativepython-1.1.0 → creativepython-1.1.2}/src/gui.py +93 -55
- creativepython-1.1.2/src/image.py +123 -0
- creativepython-1.1.0/src/image.py +0 -70
- {creativepython-1.1.0 → creativepython-1.1.2}/LICENSE +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/LICENSE-PSF +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/MANIFEST.in +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/README.md +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/setup.cfg +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/RealtimeAudioPlayer.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/__init__.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/advMetricRunner.pythonSurvey.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/compareMetrics_Java-Vs-Python.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/RunMetrics.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/Surveyor.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/__init__.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Confidence.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Contig.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/ExtendedNote.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Histogram.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Judgement.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Measurement.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/PianoRoll.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/PianoRollOld.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/__init__.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_ExtendedNote.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_Histogram.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_Measurement.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_assertions.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_integration.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_quantization.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_unit.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/Metric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/ZipfMetrics.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/__init__.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDensityMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDistanceMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChordNormalizedMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ChromaticToneMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationQuantizedMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslinePitchMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationQuantizedMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyPitchMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationBigramMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationDistanceMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedBigramMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedDistanceMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicBigramMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicConsonanceMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicIntervalMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicBigramMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicConsonanceMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicIntervalMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDistanceMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationQuantizedMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/PitchMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/RestMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/__init__.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_DurationMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_BasicIntervalsAndBigrams.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ChordsAndConsonance.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ContoursAndChromatic.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_QuantizedDurationsAndDistances.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_PitchMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/test_RestMetric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/test_Metric.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/CSVWriter.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/PowerLawRandom.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/__init__.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/notationRenderer.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython.egg-info/SOURCES.txt +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython.egg-info/dependency_links.txt +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython.egg-info/requires.txt +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython.egg-info/top_level.txt +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/bin/libportaudio.2.dylib +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/iannix.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/markov.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/midi.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/music.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/osc.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/timer.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/src/zipf.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/tests/testAnimate.py +0 -0
- {creativepython-1.1.0 → creativepython-1.1.2}/tests/testPeer.py +0 -0
- {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.
|
|
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.
|
|
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()
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
|
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
|
|
88
|
-
#
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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 == '
|
|
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
|
|
360
|
-
"""
|
|
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.
|
|
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
|
|
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
|
|
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__}.
|
|
946
|
+
print(f'{type(self).__name__}.save(): saved canvas to "{resolvedPath}"')
|
|
948
947
|
else:
|
|
949
|
-
print(f'{type(self).__name__}.
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
y2
|
|
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
|
|
1718
|
+
def _getEndpoints(self):
|
|
1692
1719
|
"""
|
|
1693
|
-
Returns
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
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(
|
|
1702
|
-
yPoints = [int(
|
|
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.
|
|
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
|
|
2023
|
+
def _getEndpoints(self):
|
|
1990
2024
|
"""
|
|
1991
|
-
Returns [x1,
|
|
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
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
2159
|
+
def _getEndpoints(self):
|
|
2123
2160
|
"""
|
|
2124
|
-
Returns
|
|
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
|
-
|
|
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.
|
|
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
|
|
2233
|
+
def _getEndpoints(self):
|
|
2197
2234
|
"""
|
|
2198
|
-
Returns
|
|
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
|
-
|
|
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
|
-
# ──
|
|
2299
|
+
# ── Save ────────────────────────────────────────────────────────────────
|
|
2262
2300
|
|
|
2263
|
-
def
|
|
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__}.
|
|
2311
|
+
print(f'{type(self).__name__}.save(): saved canvas to "{resolvedPath}"')
|
|
2274
2312
|
else:
|
|
2275
|
-
print(f'{type(self).__name__}.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/ExtendedNote.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/Measurement.py
RENAMED
|
File without changes
|
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/PianoRollOld.py
RENAMED
|
File without changes
|
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_ExtendedNote.py
RENAMED
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_Histogram.py
RENAMED
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_Measurement.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_unit.py
RENAMED
|
File without changes
|
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/ZipfMetrics.py
RENAMED
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/simple/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/metrics/test_Metric.py
RENAMED
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/CSVWriter.py
RENAMED
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/PowerLawRandom.py
RENAMED
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython/nevmuse/utilities/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-1.1.0 → creativepython-1.1.2}/src/CreativePython.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|