From 21563bbba8410bdf7c39849604d813e4953905ae Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Sun, 21 Jul 2019 17:37:15 -0700 Subject: [PATCH 01/60] Quiet down logging from select packages --- go_tri.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go_tri.py b/go_tri.py index 7bf1938..00db3ca 100644 --- a/go_tri.py +++ b/go_tri.py @@ -294,6 +294,9 @@ def go_web(self): if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s') + # Particularly noisy packages + logging.getLogger("parso").setLevel(logging.WARNING) + logging.getLogger("sacn").setLevel(logging.WARNING) parser = argparse.ArgumentParser(description='Triangle Light Control') From 43b9288faf51e6c5d68f65f62679062975266534 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Sun, 21 Jul 2019 17:44:38 -0700 Subject: [PATCH 02/60] Only enable streaming logging for project --- go_tri.py | 41 +++++++++++++++++++++-------------------- osc_serve.py | 14 +++++++------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/go_tri.py b/go_tri.py index 00db3ca..54139de 100644 --- a/go_tri.py +++ b/go_tri.py @@ -17,6 +17,8 @@ # Prints stack trace on failure faulthandler.enable() +logger = logging.getLogger("pyramidtriangles") + def speed_interpolation(val): """ @@ -91,7 +93,7 @@ def process_command(self, msg): if isinstance(msg, str): if msg == "shutdown": self.running = False - logging.info("ShowRunner shutting down") + logger.info("ShowRunner shutting down") elif msg == "clear": self.clear() time.sleep(2) @@ -103,7 +105,7 @@ def process_command(self, msg): self.max_show_time = int(msg.split(':')[1]) elif isinstance(msg, tuple): - logging.debug(f'OSC: {msg}') + logger.debug(f'OSC: {msg}') (addr, val) = msg addr = addr.split('/z')[0] @@ -140,10 +142,10 @@ def next_show(self, name=None): if name in self.shows: show = self.shows[name] else: - logging.warning(f'unknown show: {name}') + logger.warning(f'unknown show: {name}') if not show: - logging.info("choosing random show") + logger.info("choosing random show") (name, show) = next(self.randseq) self.clear() @@ -188,7 +190,7 @@ def run(self): self.next_show() except Exception: - logging.exception("unexpected exception in show loop!") + logger.exception("unexpected exception in show loop!") if self.fail_hard: raise else: @@ -199,7 +201,7 @@ def osc_listener(q, port=5700): """Create the OSC Listener thread""" listen_address = ('0.0.0.0', port) - logging.info(f'Starting OSC Listener on {listen_address}') + logger.info(f'Starting OSC Listener on {listen_address}') osc_serve.create_server(listen_address, q) @@ -225,7 +227,7 @@ def _create_services(self): try: osc_listener(self.queue) except Exception: - logging.warning("Can't create OSC listener", exc_info=True) + logger.warning("Can't create OSC listener", exc_info=True) # Show runner self.runner = ShowRunner(self.tri_model, self.queue, args.max_time, fail_hard=args.fail_hard) @@ -235,14 +237,14 @@ def _create_services(self): def start(self): if self.running: - logging.warning("start() called, but tri_grid is already running!") + logger.warning("start() called, but tri_grid is already running!") return try: self.runner.start() self.running = True except Exception: - logging.exception("Exception starting tri_grid!!") + logger.exception("Exception starting tri_grid!!") def stop(self): if self.running: # should be safe to call multiple times @@ -254,11 +256,11 @@ def stop(self): self.running = False except Exception: - logging.exception("Exception stopping tri_grid!!") + logger.exception("Exception stopping tri_grid!!") def go_headless(self): """Run without the web interface""" - logging.info("Running without web interface") + logger.info("Running without web interface") try: while True: time.sleep(999) # control-c breaks out of time.sleep @@ -269,7 +271,7 @@ def go_headless(self): def go_web(self): """Run with the web interface""" - logging.info("Running with web interface") + logger.info("Running with web interface") show_names = [name for (name, cls) in shows.load_shows()] print(f'shows: {show_names}') @@ -293,10 +295,9 @@ def go_web(self): if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s') - # Particularly noisy packages - logging.getLogger("parso").setLevel(logging.WARNING) - logging.getLogger("sacn").setLevel(logging.WARNING) + console = logging.StreamHandler() + console.setFormatter(logging.Formatter('%(levelname)s - %(message)s')) + logger.addHandler(console) parser = argparse.ArgumentParser(description='Triangle Light Control') @@ -314,18 +315,18 @@ def go_web(self): args = parser.parse_args() if args.list: - logging.info("Available shows: %s", ', '.join([name for (name, cls) in shows.load_shows()])) + logger.info("Available shows: %s", ', '.join([name for (name, cls) in shows.load_shows()])) sys.exit(0) if args.simulator: sim_host = "localhost" sim_port = 4444 - logging.info(f'Using TriSimulator at {sim_host}:{sim_port}') + logger.info(f'Using TriSimulator at {sim_host}:{sim_port}') model = SimulatorModel(sim_host, port=sim_port, model_json='./data/pixel_map.json') triangle_grid = triangle_grid.make_tri(model, 5) else: - logging.info("Starting SACN") + logger.info("Starting SACN") from model.sacn_model import sACN model = sACN(max_dmx=800, model_json="./data/pixel_map.json") @@ -336,6 +337,6 @@ def go_web(self): app.start() # start related service threads app.go_web() # enter main blocking event loop except Exception: - logging.exception("Unhandled exception running TRI!") + logger.exception("Unhandled exception running TRI!") finally: app.stop() diff --git a/osc_serve.py b/osc_serve.py index d24e9e0..4bf7288 100644 --- a/osc_serve.py +++ b/osc_serve.py @@ -7,16 +7,18 @@ THROTTLE_TIME = 0.1 # seconds +logger = logging.getLogger("pyramidtriangles") + def server_test(): - logging.info("Instantiating OSCServer:") + logger.info("Instantiating OSCServer:") osc.osc_startup() osc.osc_udp_server('0.0.0.0', 5700, "main") def printing_handler(addr, tags, stuff, source): msg_string = "%s [%s] %s" % (addr, tags, str(stuff)) - logging.info("OSCServer Got: '%s' from %s", msg_string, source) + logger.info("OSCServer Got: '%s' from %s", msg_string, source) # send a reply to the client. msg = oscbuildparse.OSCMessage("/printed", None, msg_string) @@ -25,13 +27,13 @@ def printing_handler(addr, tags, stuff, source): osc.osc_method("/print", printing_handler, argscheme=oscmethod.OSCARG_ADDRESS + oscmethod.OSCARG_DATAUNPACK) - logging.info("Starting OSC server. Use ctrl-C to quit.") + logger.info("Starting OSC server. Use ctrl-C to quit.") try: while True: osc.osc_process() except KeyboardInterrupt: - logging.info("Closing OSC server") + logger.info("Closing OSC server") osc.osc_terminate() @@ -43,7 +45,7 @@ def handler(addr, tags, data, source): sincelast = now - last_msg[addr] if sincelast >= THROTTLE_TIME: - logging.debug("%s [%s] %s", addr, tags, str(data)) + logger.debug("%s [%s] %s", addr, tags, str(data)) last_msg[addr] = now queue.put((addr, data)) @@ -53,6 +55,4 @@ def handler(addr, tags, data, source): if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s') - server_test() From cba7723ee5ad6e912f8e5fe39f450436c26569cb Mon Sep 17 00:00:00 2001 From: John Major Date: Sun, 21 Jul 2019 17:48:23 -0700 Subject: [PATCH 03/60] ? --- shows/random_cells.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shows/random_cells.py b/shows/random_cells.py index 7f639dd..eccf1c2 100644 --- a/shows/random_cells.py +++ b/shows/random_cells.py @@ -9,6 +9,7 @@ def __init__(self, tri_grid, frame_delay = 0.1): self.frame_delay = frame_delay self.n_cells = len(self.tri_grid.get_cells()) + from IPython import embed; embed() def next_frame(self): while True: From 2f183498d44205f8a40679adccb4864c59787ae0 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Mon, 22 Jul 2019 12:41:10 -0700 Subject: [PATCH 04/60] Rename test_shows to match style of others --- tests/{shows_test.py => test_shows.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{shows_test.py => test_shows.py} (100%) diff --git a/tests/shows_test.py b/tests/test_shows.py similarity index 100% rename from tests/shows_test.py rename to tests/test_shows.py From d3a001f81e509a8c1567c1ef1e8d5c8caccc2d3f Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Thu, 18 Jul 2019 20:30:24 -0700 Subject: [PATCH 05/60] Import Ponzi color code --- poetry.lock | 43 +++++- ponzicolor/__init__.py | 14 ++ ponzicolor/blend.py | 23 ++++ ponzicolor/color.py | 124 +++++++++++++++++ ponzicolor/linear.py | 66 ++++++++++ ponzicolor/scale.py | 35 +++++ ponzicolor/space.py | 151 +++++++++++++++++++++ pyproject.toml | 1 + tests/__init__.py | 0 tests/test_color.py | 292 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 746 insertions(+), 3 deletions(-) create mode 100644 ponzicolor/__init__.py create mode 100644 ponzicolor/blend.py create mode 100644 ponzicolor/color.py create mode 100644 ponzicolor/linear.py create mode 100644 ponzicolor/scale.py create mode 100644 ponzicolor/space.py create mode 100644 tests/__init__.py create mode 100644 tests/test_color.py diff --git a/poetry.lock b/poetry.lock index 9b743e8..3fe4530 100644 --- a/poetry.lock +++ b/poetry.lock @@ -76,6 +76,18 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.4.1" +[[package]] +category = "main" +description = "Color math and conversion library." +name = "colormath" +optional = false +python-versions = "*" +version = "3.0.0" + +[package.dependencies] +networkx = ">=2.0" +numpy = "*" + [[package]] category = "main" description = "Better living through Python with decorators" @@ -171,7 +183,18 @@ description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false python-versions = ">=3.4" -version = "7.1.0" +version = "7.2.0" + +[[package]] +category = "main" +description = "Python package for creating and manipulating graphs and networks" +name = "networkx" +optional = false +python-versions = ">=3.5" +version = "2.3" + +[package.dependencies] +decorator = ">=4.3.0" [[package]] category = "main" @@ -350,6 +373,17 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" version = "1.12.0" +[[package]] +category = "main" +description = "Color scales and color conversion made easy for Python." +name = "spectra" +optional = false +python-versions = "*" +version = "0.0.11" + +[package.dependencies] +colormath = ">=3.0.0" + [[package]] category = "main" description = "Objects and routines pertaining to date and time (tempora)" @@ -423,7 +457,7 @@ python-versions = ">=2.7" version = "0.5.2" [metadata] -content-hash = "56bbf34095ab39c12e98a9c830cae130df03478dce540375e3cd83f127c3432d" +content-hash = "cb1f76cd122797fe3bc03995c73dbe83f23fcabba37a533d0d15b8e685268920" python-versions = "^3.7" [metadata.hashes] @@ -435,6 +469,7 @@ backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", cheroot = ["1593fa2a42b18744ac485aadf5fec4a29ebfee00ba3937a2269b8ffc94447879", "f6a85e005adb5bc5f3a92b998ff0e48795d4d98a0fbb7edde47a7513d4100601"] cherrypy = ["48de31ba3db04c5354a0fcf8acf21a9c5190380013afca746d50237c9ebe70f0", "641b51570158dd301da5d569085c04fe7f2cc98474d103f8137d8dbe36b2f6e7"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] +colormath = ["3d4605af344527da0e4f9f504fad7ddbebda35322c566a6c72e28edb1ff31217"] decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"] importlib-metadata = ["6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", "cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db"] ipython = ["11067ab11d98b1e6c7f0993506f7a5f8a91af420f7e82be6575fcb7a6ca372a0", "60bc55c2c1d287161191cc2469e73c116d9b634cff25fe214a43cba7cec94c79"] @@ -443,7 +478,8 @@ ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c6 jedi = ["53c850f1a7d3cfcd306cc513e2450a54bdf5cacd7604b74e42dd1f0758eaaf36", "e07457174ef7cb2342ff94fa56484fe41cec7ef69b0059f01d3f812379cb6f7c"] jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] -more-itertools = ["3ad685ff8512bf6dc5a8b82ebf73543999b657eded8c11803d9ba6b648986f4d", "8bb43d1f51ecef60d81854af61a3a880555a14643691cc4b64a6ee269c78f09a"] +more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] +networkx = ["8311ddef63cf5c5c5e7c1d0212dd141d9a1fe3f474915281b73597ed5f1d4e3d"] numpy = ["0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", "141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", "14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", "27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", "2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", "3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", "52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", "6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", "7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", "7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", "94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", "a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", "ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", "b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", "b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", "cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", "d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", "dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", "dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", "e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", "ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", "f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", "f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7"] ola = ["60d263f99e95da94bbc9baa5737625070c016def4aad52cbaeced9ed9913eab2", "fc96b689000812cf7a2af44cf6c7b4cf1b37944c1ee1860d97eba06aa0cbcf66"] osc4py3 = ["f45364b793270dfec860b399af6b5b361e54d440f14bf43d33b83a0b615a714c"] @@ -462,6 +498,7 @@ pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d74 pywin32 = ["22e218832a54ed206452c8f3ca9eff07ef327f8e597569a4c2828be5eaa09a77", "32b37abafbfeddb0fe718008d6aada5a71efa2874f068bee1f9e703983dcc49a", "35451edb44162d2f603b5b18bd427bc88fcbc74849eaa7a7e7cfe0f507e5c0c8", "4eda2e1e50faa706ff8226195b84fbcbd542b08c842a9b15e303589f85bfb41c", "5f265d72588806e134c8e1ede8561739071626ea4cc25c12d526aa7b82416ae5", "6852ceac5fdd7a146b570655c37d9eacd520ed1eaeec051ff41c6fc94243d8bf", "6dbc4219fe45ece6a0cc6baafe0105604fdee551b5e876dc475d3955b77190ec", "9bd07746ce7f2198021a9fa187fa80df7b221ec5e4c234ab6f00ea355a3baf99"] sacn = ["764d891e90fabf32161d636375558a57d9a830ec53f9e168f464dd5bd8564d51"] six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] +spectra = ["8eb362a5187cb63cee13cd01186799c0c791a3ad3bec57b279132e12521762b8"] tempora = ["cb60b1d2b1664104e307f8e5269d7f4acdb077c82e35cd57246ae14a3427d2d6", "d28a03d2f64ee81aec6e6bff374127ef306fe00c1b7e27c7ff1618344221a699"] traitlets = ["9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"] typing = ["38566c558a0a94d6531012c8e917b1b8518a41e418f7f15f00e129cc80162ad3", "53765ec4f83a2b720214727e319607879fec4acde22c4fbb54fa2604e79e44ce", "84698954b4e6719e912ef9a42a2431407fe3755590831699debda6fba92aac55"] diff --git a/ponzicolor/__init__.py b/ponzicolor/__init__.py new file mode 100644 index 0000000..8ca7e60 --- /dev/null +++ b/ponzicolor/__init__.py @@ -0,0 +1,14 @@ +""" +Color implements RGBW color values, with the RGB portion able to be modeled +in CIE perceptual color spaces. +""" + +# +# a Python port of go-colorful +# Copyright © 2013 Lucas Beyer +# https://github.com/lucasb-eyer/go-colorful +# + +from .color import Color, color, white +from .scale import Scale +from .space import RGB, Lab, HCL diff --git a/ponzicolor/blend.py b/ponzicolor/blend.py new file mode 100644 index 0000000..9b14a33 --- /dev/null +++ b/ponzicolor/blend.py @@ -0,0 +1,23 @@ +from .space import HCL, Lab + +# Blending colors depends on what color space the colors are represented in. + + +def hcl(a: HCL, b: HCL, t: float) -> HCL: + h = _interpolate_angle(a.h, b.h, t) + c = scale(a.c, b.c, t) + l = scale(a.l, b.l, t) + return HCL(h, c, l) + + +def lab(a: Lab, b: Lab, t: float) -> Lab: + return Lab(l=scale(a.l, b.l, t), a=scale(a.a, b.a, t), b=scale(a.b, b.b, t)) + + +def scale(a: float, b: float, t: float) -> float: + return a + t * (b - a) + + +def _interpolate_angle(a: float, b: float, t: float) -> float: + delta = ((b - a) % 360.0 + 540.0) % 360.0 - 180 + return (a + t * delta + 360.0) % 360.0 diff --git a/ponzicolor/color.py b/ponzicolor/color.py new file mode 100644 index 0000000..45170dc --- /dev/null +++ b/ponzicolor/color.py @@ -0,0 +1,124 @@ +from typing import NamedTuple, Optional, Tuple, Union + +from . import blend +from .space import ( + RGB, + Lab, + HCL, + XYZ, + LinearRGB, + hcl_to_lab, + lab_to_hcl, + lab_to_xyz, + xyz_to_lab, + xyz_to_linear_rgb, + linear_rgb_to_xyz, + srgb_to_linear_rgb, + linear_rgb_to_srgb, +) + + +class Color(NamedTuple): + r: float # [0-1] + g: float # [0-1] + b: float # [0-1] + w: float # [0-1] + + @classmethod + def white(cls, w: float) -> "Color": + return cls(0.0, 0.0, 0.0, w) + + @classmethod + def from_hex(cls, h: str) -> "Color": + if h.startswith("#"): + h = h[1:] + if len(h) == 6: + h += "00" + return Color(*tuple(int(h[i : i + 2], 16) / 255 for i in range(0, 8, 2))) + + @classmethod + def from_rgb(cls, rgb: RGB, w: float = 0.0) -> "Color": + return cls(rgb.r, rgb.g, rgb.b, w) + + @classmethod + def from_linear_rgb(cls, rgb: LinearRGB, w: float = 0.0) -> "Color": + return cls.from_rgb(linear_rgb_to_srgb(rgb), w) + + @classmethod + def from_xyz(cls, xyz: XYZ, w: float = 0.0) -> "Color": + return cls.from_linear_rgb(xyz_to_linear_rgb(xyz), w) + + @classmethod + def from_lab(cls, lab: Lab, w: float = 0.0) -> "Color": + return cls.from_xyz(lab_to_xyz(lab), w) + + @classmethod + def from_hcl(cls, hcl: HCL, w: float = 0.0) -> "Color": + return cls.from_lab(hcl_to_lab(hcl), w) + + @property + def valid(self) -> bool: + return all(0.0 <= c <= 1.0 for c in self) + + def clamp(self) -> "Color": + def c(v: float) -> float: + return max(0.0, min(v, 1.0)) + + return Color(c(self.r), c(self.g), c(self.b), c(self.w)) + + def blend(self, other: "Color", bias: float) -> "Color": + return Color.from_hcl( + blend.hcl(self.hcl, other.hcl, bias), w=blend.scale(self.w, other.w, bias) + ) + + @property + def dmx(self) -> Tuple[int, int, int, int]: + def c(v: float) -> int: + return int(v * 255.0 + 0.5) + + return (c(self.r), c(self.g), c(self.b), c(self.w)) + + @property + def hex(self) -> str: + return "#%02X%02X%02X%02X" % self.dmx + + @property + def rgb(self) -> RGB: + return RGB(self.r, self.g, self.b) + + @property + def linear_rgb(self) -> LinearRGB: + return srgb_to_linear_rgb(self.rgb) + + @property + def xyz(self) -> XYZ: + return linear_rgb_to_xyz(self.linear_rgb) + + @property + def lab(self) -> Lab: + return xyz_to_lab(self.xyz) + + @property + def hcl(self) -> HCL: + return lab_to_hcl(self.lab) + + @property + def hue(self) -> float: + return self.hcl.h + + +def color(chroma: Union[str, RGB, Lab, HCL], white: float = 0.0) -> Color: + if isinstance(chroma, str): + return Color.from_hex(chroma) + if isinstance(chroma, HCL): + return Color.from_hcl(chroma, white) + elif isinstance(chroma, Lab): + return Color.from_lab(chroma, white) + elif isinstance(chroma, RGB): + return Color.from_rgb(chroma, white) + else: + raise TypeError("%r is not an RGB, Lab, or HCL color" % (chroma,)) + + +def white(w: float) -> Color: + return Color.white(w) diff --git a/ponzicolor/linear.py b/ponzicolor/linear.py new file mode 100644 index 0000000..e548bf1 --- /dev/null +++ b/ponzicolor/linear.py @@ -0,0 +1,66 @@ +def linearize(v: float) -> float: + return ((v + 0.055) / 1.055) ** 2.4 if v > 0.04045 else v / 12.92 + + +def linearize_fast(v: float) -> float: + v1 = v - 0.5 + v2 = v1 * v1 + v3 = v2 * v1 + v4 = v2 * v2 + + return ( + -0.248750514614486 + + 0.925583310193438 * v + + 1.16740237321695 * v2 + + 0.280457026598666 * v3 + - 0.0757991963780179 * v4 + ) + + +def delinearize(v: float) -> float: + return 1.055 * (v ** (1.0 / 2.4)) - 0.055 if v > 0.0031308 else 12.92 * v + + +def delinearize_fast(v: float) -> float: + if v > 0.2: + v1 = v - 0.6 + v2 = v1 * v1 + v3 = v2 * v1 + v4 = v2 * v2 + v5 = v3 * v2 + return ( + 0.442430344268235 + + 0.592178981271708 * v + - 0.287864782562636 * v2 + + 0.253214392068985 * v3 + - 0.272557158129811 * v4 + + 0.325554383321718 * v5 + ) + elif v > 0.03: + v1 = v - 0.115 + v2 = v1 * v1 + v3 = v2 * v1 + v4 = v2 * v2 + v5 = v3 * v2 + return ( + 0.194915592891669 + + 1.55227076330229 * v + - 3.93691860257828 * v2 + + 18.0679839248761 * v3 + - 101.468750302746 * v4 + + 632.341487393927 * v5 + ) + else: + v1 = v - 0.015 + v2 = v1 * v1 + v3 = v2 * v1 + v4 = v2 * v2 + v5 = v3 * v2 + return ( + 0.0519565234928877 + + 5.09316778537561 * v + - 99.0338180489702 * v2 + + 3484.52322764895 * v3 + - 150028.083412663 * v4 + + 7168008.42971613 * v5 + ) diff --git a/ponzicolor/scale.py b/ponzicolor/scale.py new file mode 100644 index 0000000..7cdbf54 --- /dev/null +++ b/ponzicolor/scale.py @@ -0,0 +1,35 @@ +from itertools import islice + +from typing import List, Sequence, Tuple +from .color import Color + + +class Scale: + """ + Scale is a linear color gradient. + """ + + points: List[Tuple[Color, float]] + + @classmethod + def of(cls, *colors: Color) -> "Scale": + return cls.linear(colors) + + @classmethod + def linear(cls, colors: Sequence[Color]) -> "Scale": + s = 1.0 / len(colors) + return cls([(color, i * s) for i, color in enumerate(colors)]) + + def __init__(self, points: Sequence[Tuple[Color, float]]): + self.points = list(points) + + def __call__(self, t: float) -> Color: + if t < 0.0 or t > 1.0: + raise ValueError(f"Scale must be called with 0 ≤ t ≤ 1, not {t}.") + + for (c1, p1), (c2, p2) in zip(self.points, islice(self.points, 1, None)): + if p1 <= t <= p2: + bias = (t - p1) / (p2 - p1) + return c1.blend(c2, bias).clamp() + else: + return self.points[-1][0] diff --git a/ponzicolor/space.py b/ponzicolor/space.py new file mode 100644 index 0000000..60354ea --- /dev/null +++ b/ponzicolor/space.py @@ -0,0 +1,151 @@ +from math import atan2, cos, degrees, sin, sqrt, radians +from typing import Callable, NamedTuple + +from . import linear + + +class RGB(NamedTuple): + """ + RGB is an sRGB color. + """ + + r: float # [0-1] + g: float # [0-1] + b: float # [0-1] + + @property + def valid(self) -> bool: + return 0.0 <= self.r <= 1.0 and 0.0 <= self.g <= 1.0 and 0.0 <= self.b <= 1.0 + + def clamp(self) -> "RGB": + def c(v: float) -> float: + return max(0.0, min(v, 1.0)) + + return RGB(c(self.r), c(self.g), c(self.b)) + + +class Lab(NamedTuple): + """ + Lab is a color in the CIE L*a*b* perceptually-uniform color space. + """ + + l: float + a: float + b: float + + +class HCL(NamedTuple): + """ + HCL is a color in the CIE L*C*h° color space, a polar projection of L*a*b*. + It's basically a superior HSV. + """ + + h: float # hue [0-360) + c: float # chroma [0-1] + l: float # luminance [0-1] + + +class XYZ(NamedTuple): + """ + XYZ is a color in CIE's standard color space. + """ + + x: float + y: float + z: float + + +class LinearRGB(NamedTuple): + """ + RGB is a linear color. + """ + + r: float # [0-1] + g: float # [0-1] + b: float # [0-1] + + +# Reference white points + +D50 = XYZ(0.96422, 1.00000, 0.82521) +D65 = XYZ(0.95047, 1.00000, 1.08883) + + +def hcl_to_lab(hcl: HCL) -> Lab: + h_rad = radians(hcl.h) + a = hcl.c * cos(h_rad) + b = hcl.c * sin(h_rad) + return Lab(hcl.l, a, b) + + +def lab_to_hcl(lab: Lab) -> HCL: + t = 1.0e-4 + h = ( + degrees(atan2(lab.b, lab.a)) % 360.0 + if abs(lab.b - lab.a) > t and abs(lab.a) > t + else 0.0 + ) + c = sqrt(lab.a ** 2 + lab.b ** 2) + l = lab.l + + return HCL(h, c, l) + + +def lab_to_xyz(lab: Lab, white_ref: XYZ = D65) -> XYZ: + def finv(t: float) -> float: + return ( + t ** 3 + if t > 6.0 / 29.0 + else 3.0 * 6.0 / 29.0 * 6.0 / 29.0 * (t - 4.0 / 29.0) + ) + + l2 = (lab.l + 0.16) / 1.16 + return XYZ( + white_ref.x * finv(l2 + lab.a / 5.0), + white_ref.y * finv(l2), + white_ref.z * finv(l2 - lab.b / 2.0), + ) + + +def xyz_to_lab(xyz: XYZ, white_ref: XYZ = D65) -> Lab: + def f(t: float) -> float: + return ( + t ** (1 / 3) + if t > 6.0 / 29.0 * 6.0 / 29.0 * 6.0 / 29.0 + else t / 3.0 * 29.0 / 6.0 * 29.0 / 6.0 + 4.0 / 29.0 + ) + + fy = f(xyz.y / white_ref.y) + return Lab( + 1.16 * fy - 0.16, + 5.0 * (f(xyz.x / white_ref.x) - fy), + 2.0 * (fy - f(xyz.z / white_ref.z)), + ) + + +def xyz_to_linear_rgb(xyz: XYZ) -> LinearRGB: + return LinearRGB( + 3.2404542 * xyz.x - 1.5371385 * xyz.y - 0.4985314 * xyz.z, + -0.9692660 * xyz.x + 1.8760108 * xyz.y + 0.0415560 * xyz.z, + 0.0556434 * xyz.x - 0.2040259 * xyz.y + 1.0572252 * xyz.z, + ) + + +def linear_rgb_to_xyz(rgb: LinearRGB) -> XYZ: + return XYZ( + 0.4124564 * rgb.r + 0.3575761 * rgb.g + 0.1804375 * rgb.b, + 0.2126729 * rgb.r + 0.7151522 * rgb.g + 0.0721750 * rgb.b, + 0.0193339 * rgb.r + 0.1191920 * rgb.g + 0.9503041 * rgb.b, + ) + + +def linear_rgb_to_srgb( + rgb: LinearRGB, delinearize: Callable[[float], float] = linear.delinearize +) -> RGB: + return RGB(delinearize(rgb.r), delinearize(rgb.g), delinearize(rgb.b)) + + +def srgb_to_linear_rgb( + rgb: RGB, linearize: Callable[[float], float] = linear.linearize +) -> LinearRGB: + return LinearRGB(linearize(rgb.r), linearize(rgb.g), linearize(rgb.b)) diff --git a/pyproject.toml b/pyproject.toml index bfd6655..192777a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ numpy = "^1.16.4" sacn = "^1.3" ola = "^0.10.7" jinja2 = "^2.10" +spectra = "^0.0.11" [tool.poetry.dev-dependencies] pytest = "^3.0" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_color.py b/tests/test_color.py new file mode 100644 index 0000000..abd9d9b --- /dev/null +++ b/tests/test_color.py @@ -0,0 +1,292 @@ +from typing import NamedTuple + +from pytest import approx + +from ponzicolor.color import color +from ponzicolor.space import ( + RGB, + Lab, + HCL, + XYZ, + LinearRGB, + hcl_to_lab, + lab_to_hcl, + lab_to_xyz, + xyz_to_lab, + xyz_to_linear_rgb, + linear_rgb_to_xyz, + srgb_to_linear_rgb, + linear_rgb_to_srgb, +) +from ponzicolor.linear import delinearize, linearize + + +class Case(NamedTuple): + hex: str + rgb: RGB + lrgb: LinearRGB + xyz: XYZ + lab: Lab + hcl: HCL + + +cases = [ + Case( + "#ffffff", + RGB(1.0, 1.0, 1.0), + LinearRGB(1, 1, 1), + XYZ(0.950470, 1.000000, 1.088830), + Lab(1.000000, 0.000000, 0.000000), + HCL(0.0000, 0.000000, 1.000000), + ), + Case( + "#80ffff", + RGB(0.5, 1.0, 1.0), + LinearRGB(0.21404114048223255, 1, 1), + XYZ(0.626296, 0.832848, 1.073634), + Lab(0.931390, -0.353319, -0.108946), + HCL(197.1371, 0.369735, 0.931390), + ), + Case( + "#ff80ff", + RGB(1.0, 0.5, 1.0), + LinearRGB(1, 0.21404114048223255, 1), + XYZ(0.669430, 0.437920, 0.995150), + Lab(0.720892, 0.651673, -0.422133), + HCL(327.0661, 0.776450, 0.720892), + ), + Case( + "#ffff80", + RGB(1.0, 1.0, 0.5), + LinearRGB(1, 1, 0.21404114048223255), + XYZ(0.808654, 0.943273, 0.341930), + Lab(0.977637, -0.165795, 0.602017), + HCL(105.3975, 0.624430, 0.977637), + ), + Case( + "#8080ff", + RGB(0.5, 0.5, 1.0), + LinearRGB(0.21404114048223255, 0.21404114048223255, 1), + XYZ(0.345256, 0.270768, 0.979954), + Lab(0.590453, 0.332846, -0.637099), + HCL(297.5843, 0.718805, 0.590453), + ), + Case( + "#ff8080", + RGB(1.0, 0.5, 0.5), + LinearRGB(1, 0.21404114048223255, 0.21404114048223255), + XYZ(0.527613, 0.381193, 0.248250), + Lab(0.681085, 0.483884, 0.228328), + HCL(25.2610, 0.535049, 0.681085), + ), + Case( + "#80ff80", + RGB(0.5, 1.0, 0.5), + LinearRGB(0.21404114048223255, 1, 0.21404114048223255), + XYZ(0.484480, 0.776121, 0.326734), + Lab(0.906026, -0.600870, 0.498993), + HCL(140.2920, 0.781050, 0.906026), + ), + Case( + "#808080", + RGB(0.5, 0.5, 0.5), + LinearRGB(0.21404114048223255, 0.21404114048223255, 0.21404114048223255), + XYZ(0.203440, 0.214041, 0.233054), + Lab(0.533890, 0.000000, 0.000000), + HCL(0.0000, 0.000000, 0.533890), + ), + Case( + "#00ffff", + RGB(0.0, 1.0, 1.0), + LinearRGB(0.0, 1.0, 1.0), + XYZ(0.538014, 0.787327, 1.069496), + Lab(0.911132, -0.480875, -0.141312), + HCL(196.3762, 0.501209, 0.911132), + ), + Case( + "#ff00ff", + RGB(1.0, 0.0, 1.0), + LinearRGB(1.0, 0.0, 1.0), + XYZ(0.592894, 0.284848, 0.969638), + Lab(0.603242, 0.982343, -0.608249), + HCL(328.2350, 1.155407, 0.603242), + ), + Case( + "#ffff00", + RGB(1.0, 1.0, 0.0), + LinearRGB(1.0, 1.0, 0.0), + XYZ(0.770033, 0.927825, 0.138526), + Lab(0.971393, -0.215537, 0.944780), + HCL(102.8512, 0.969054, 0.971393), + ), + Case( + "#0000ff", + RGB(0.0, 0.0, 1.0), + LinearRGB(0.0, 0.0, 1.0), + XYZ(0.180437, 0.072175, 0.950304), + Lab(0.322970, 0.791875, -1.078602), + HCL(306.2849, 1.338076, 0.322970), + ), + Case( + "#00ff00", + RGB(0.0, 1.0, 0.0), + LinearRGB(0.0, 1.0, 0.0), + XYZ(0.357576, 0.715152, 0.119192), + Lab(0.877347, -0.861827, 0.831793), + HCL(136.0160, 1.197759, 0.877347), + ), + Case( + "#ff0000", + RGB(1.0, 0.0, 0.0), + LinearRGB(1.0, 0.0, 0.0), + XYZ(0.412456, 0.212673, 0.019334), + Lab(0.532408, 0.800925, 0.672032), + HCL(39.9990, 1.045518, 0.532408), + ), + Case( + "#000000", + RGB(0.0, 0.0, 0.0), + LinearRGB(0.0, 0.0, 0.0), + XYZ(0.000000, 0.000000, 0.000000), + Lab(0.000000, 0.000000, 0.000000), + HCL(0.0000, 0.000000, 0.000000), + ), +] + + +def test_linear_rgb(): + for c in cases: + actual = srgb_to_linear_rgb(c.rgb) + assert actual.r == approx(c.lrgb.r, 0.0001) + assert actual.g == approx(c.lrgb.g, 0.0001) + assert actual.b == approx(c.lrgb.b, 0.0001) + for c in cases: + actual = linear_rgb_to_srgb(c.lrgb) + assert actual.r == approx(c.rgb.r, 0.0001) + assert actual.g == approx(c.rgb.g, 0.0001) + assert actual.b == approx(c.rgb.b, 0.0001) + + +def test_xyz(): + for c in cases: + actual = linear_rgb_to_xyz(srgb_to_linear_rgb(c.rgb)) + assert actual.x == approx(c.xyz.x, 0.0001) + assert actual.y == approx(c.xyz.y, 0.0001) + assert actual.z == approx(c.xyz.z, 0.0001) + for c in cases: + actual = linear_rgb_to_srgb(xyz_to_linear_rgb(c.xyz)) + assert actual.r == approx(c.rgb.r, 0.0001, 0.0001) + assert actual.g == approx(c.rgb.g, 0.0001, 0.0001) + assert actual.b == approx(c.rgb.b, 0.0001, 0.0001) + + +def test_lab(): + for c in cases: + actual = xyz_to_lab(linear_rgb_to_xyz(srgb_to_linear_rgb(c.rgb))) + assert actual.l == approx(c.lab.l, 0.0001, 0.0001) + assert actual.a == approx(c.lab.a, 0.0001, 0.0001) + assert actual.b == approx(c.lab.b, 0.0001, 0.0001) + for c in cases: + actual = linear_rgb_to_srgb(xyz_to_linear_rgb(lab_to_xyz(c.lab))) + assert actual.r == approx(c.rgb.r, 0.0001, 0.0001) + assert actual.g == approx(c.rgb.g, 0.0001, 0.0001) + assert actual.b == approx(c.rgb.b, 0.0001, 0.0001) + + +def test_hcl(): + for c in cases: + actual = lab_to_hcl(xyz_to_lab(linear_rgb_to_xyz(srgb_to_linear_rgb(c.rgb)))) + assert actual.h == approx(c.hcl.h, 0.0001, 0.0001) + assert actual.c == approx(c.hcl.c, 0.0001, 0.0001) + assert actual.l == approx(c.hcl.l, 0.0001, 0.0001) + for c in cases: + actual = linear_rgb_to_srgb(xyz_to_linear_rgb(lab_to_xyz(hcl_to_lab(c.hcl)))) + assert actual.r == approx(c.rgb.r, 0.0001, 0.0001) + assert actual.g == approx(c.rgb.g, 0.0001, 0.0001) + assert actual.b == approx(c.rgb.b, 0.0001, 0.0001) + + +def test_blend(): + assert color("#2E4057").blend(color("#048BA8"), 0.1).hex == "#2F476000" + assert color("#2E4057").blend(color("#048BA8"), 0.2).hex == "#2F4E6900" + assert color("#2E4057").blend(color("#048BA8"), 0.3).hex == "#2F557100" + assert color("#2E4057").blend(color("#048BA8"), 0.4).hex == "#2E5C7A00" + assert color("#2E4057").blend(color("#048BA8"), 0.5).hex == "#2B648200" + assert color("#2E4057").blend(color("#048BA8"), 0.6).hex == "#286B8A00" + assert color("#2E4057").blend(color("#048BA8"), 0.7).hex == "#23739200" + assert color("#2E4057").blend(color("#048BA8"), 0.8).hex == "#1D7B9A00" + assert color("#2E4057").blend(color("#048BA8"), 0.9).hex == "#1483A100" + + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.1).hex == "#DCD8E600" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.2).hex == "#D8D3E300" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.3).hex == "#D3CDE000" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.4).hex == "#CFC8DD00" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.5).hex == "#CBC2DA00" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.6).hex == "#C6BCD700" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.7).hex == "#C2B7D400" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.8).hex == "#BEB1D000" + assert color("#E1DEE9").blend(color("#B6A6CA"), 0.9).hex == "#BAACCD00" + + assert color("#DD403A").blend(color("#B8B42D"), 0.1).hex == "#DE4E3200" + assert color("#DD403A").blend(color("#B8B42D"), 0.2).hex == "#DD5B2A00" + assert color("#DD403A").blend(color("#B8B42D"), 0.3).hex == "#DB682300" + assert color("#DD403A").blend(color("#B8B42D"), 0.4).hex == "#D9741C00" + assert color("#DD403A").blend(color("#B8B42D"), 0.5).hex == "#D5801600" + assert color("#DD403A").blend(color("#B8B42D"), 0.6).hex == "#D18B1200" + assert color("#DD403A").blend(color("#B8B42D"), 0.7).hex == "#CC961300" + assert color("#DD403A").blend(color("#B8B42D"), 0.8).hex == "#C6A01900" + assert color("#DD403A").blend(color("#B8B42D"), 0.9).hex == "#BFAA2200" + + assert color("#3D348B").blend(color("#E6AF2E"), 0.1).hex == "#6A328B00" + assert color("#3D348B").blend(color("#E6AF2E"), 0.2).hex == "#8E2F8700" + assert color("#3D348B").blend(color("#E6AF2E"), 0.3).hex == "#AC2F7F00" + assert color("#3D348B").blend(color("#E6AF2E"), 0.4).hex == "#C5347300" + assert color("#3D348B").blend(color("#E6AF2E"), 0.5).hex == "#D8416600" + assert color("#3D348B").blend(color("#E6AF2E"), 0.6).hex == "#E6535800" + assert color("#3D348B").blend(color("#E6AF2E"), 0.7).hex == "#EE694A00" + assert color("#3D348B").blend(color("#E6AF2E"), 0.8).hex == "#F0803D00" + assert color("#3D348B").blend(color("#E6AF2E"), 0.9).hex == "#EE973200" + + assert color("#191716").blend(color("#E6AF2E"), 0.1).hex == "#2E221C00" + assert color("#191716").blend(color("#E6AF2E"), 0.2).hex == "#432F2100" + assert color("#191716").blend(color("#E6AF2E"), 0.3).hex == "#593C2500" + assert color("#191716").blend(color("#E6AF2E"), 0.4).hex == "#6E4A2800" + assert color("#191716").blend(color("#E6AF2E"), 0.5).hex == "#83582B00" + assert color("#191716").blend(color("#E6AF2E"), 0.6).hex == "#98682D00" + assert color("#191716").blend(color("#E6AF2E"), 0.7).hex == "#AC782F00" + assert color("#191716").blend(color("#E6AF2E"), 0.8).hex == "#C08A2F00" + assert color("#191716").blend(color("#E6AF2E"), 0.9).hex == "#D49C2F00" + + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.1).hex == "#BEE9C100" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.2).hex == "#BDE6C000" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.3).hex == "#BBE2C000" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.4).hex == "#BADFC000" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.5).hex == "#B9DBBF00" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.6).hex == "#B9D8BE00" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.7).hex == "#B8D4BE00" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.8).hex == "#B7D0BD00" + assert color("#BFEDC1").blend(color("#B6C9BB"), 0.9).hex == "#B7CDBC00" + + +def test_linearize(): + assert linearize(0.000000) == approx(0.000000) + assert linearize(0.040000) == approx(0.003096, 0.0001) + assert linearize(0.100000) == approx(0.010023, 0.0001) + assert linearize(0.200000) == approx(0.033105, 0.0001) + assert linearize(0.250000) == approx(0.050876, 0.0001) + assert linearize(0.500000) == approx(0.214041, 0.0001) + assert linearize(0.750000) == approx(0.522522, 0.0001) + assert linearize(1.000000) == approx(1.000000, 0.0001) + + +def test_delinearize(): + assert delinearize(0.000000) == approx(0.000000, 0.0001, 0.0001) + assert delinearize(0.003000) == approx(0.038760, 0.0001, 0.0001) + assert delinearize(0.010000) == approx(0.099853, 0.0001, 0.0001) + assert delinearize(0.050000) == approx(0.247801, 0.0001, 0.0001) + assert delinearize(0.100000) == approx(0.349190, 0.0001, 0.0001) + assert delinearize(0.200000) == approx(0.484529, 0.0001, 0.0001) + assert delinearize(0.250000) == approx(0.537099, 0.0001, 0.0001) + assert delinearize(0.500000) == approx(0.735357, 0.0001, 0.0001) + assert delinearize(0.750000) == approx(0.880825, 0.0001, 0.0001) + assert delinearize(1.000000) == approx(1.000000, 0.0001, 0.0001) From 3332dc12d8c379f386fdb79959ab9fb5bde6fedc Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Mon, 22 Jul 2019 13:14:38 -0700 Subject: [PATCH 06/60] Minor simulator class cleanup --- model/sacn_model.py | 9 ++++--- model/simulator.py | 60 +++++++++++++++++++-------------------------- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/model/sacn_model.py b/model/sacn_model.py index e9282b9..a348b80 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -14,10 +14,13 @@ class sACN(ModelBase): def __init__(self, max_dmx, model_json=None): # XXX any way to check if this is a valid connection? - self.sender = sacn.sACNsender(bind_address="192.168.1.210", universeDiscovery=False) # Must supply an IP address to bind to that is in the same subnet as the devices routiung the universes. Might haave to assign a second IP to the eth adapter to get this to work (this is what I had to do on my mac) + # Must supply an IP address to bind to that is in the same subnet as the devices routing the universes. Might + # have to assign a second IP to the eth adapter to get this to work (this is what I had to do on my mac) + self.sender = sacn.sACNsender(bind_address="192.168.1.210", universeDiscovery=False) self.sender.start() self.PIXEL_MAP = None - self.leds = {} # dictionary which will hold an array of 512 int's for each universe, universes are keys to the arrays. + # dictionary which will hold an array of 512 int's for each universe, universes are keys to the arrays. + self.leds = {} self._map_leds(model_json) # Keys for LEDs are integers representing universes, each universe has an array of possible DMX channels @@ -27,7 +30,7 @@ def __del__(self): self.sender.stop() # If the object is destructing, close the sender connection def _map_leds(self, f): - # Loads a json file with mapping info describing your leds. + # Loads a json file with mapping info describing your LEDs. # The json file is formatted as a dictionary of numbers (as strings sadly, b/c json is weird # each key in the dict is a fixtureUID. # each array that fixtureUID returns is of the format [universeUID, DMXstart#] diff --git a/model/simulator.py b/model/simulator.py index e10cefc..bf403fb 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -1,74 +1,64 @@ """ Model to communicate with a Simulator over a TCP socket. -Panels are numbered as strings of the form '12b', indicating 'business' or 'party' side of the sheep. - XXX Should this class be able to do range checks on cell ids? """ +import logging import socket import json from .modelbase import ModelBase SIM_DEFAULT = (188, 210, 229) # BCD2E5, "off" color for simulator +logger = logging.getLogger("pyramidtriangles") class SimulatorModel(ModelBase): - def __init__(self, hostname, port=4444, debug=False, model_json=None): - self.CELL_MAP = None + def __init__(self, hostname, port=4444, model_json=None): + self.hostname = hostname + self.port = port + self._map_leds(model_json) - self.server = (hostname, port) - self.debug = debug - self.sock = None + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((self.hostname, self.port)) # map of cells to be set on the next call to go self.dirty = {} - self.connect() - - def connect(self): - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.connect(self.server) - # XXX throw an exception if the socket isn't available - def __repr__(self): - return "SimulatorModel(%s, port=%d, debug=%s)" % (self.server[0], self.server[1], self.debug) + return f'{__class__.__name__} (hostname={self.hostname}, port={self.port})' # Loaders def _map_leds(self, f): - # Loads a json file with mapping info describing your leds. - # The json file is formatted as a dictionary of numbers (as strings sadly, b/c json is weird - # each key in the dict is a fixtureUID. - # each array that fixtureUID returns is of the format [universeUID, DMXstart#] + # Loads a json file with mapping info describing your LEDs. + # The json file is formatted as a dictionary of numbers each key in the dict is a fixtureUID. + # Each array that fixtureUID returns is of the format [universeUID, DMXstart#]. with open(f, 'r') as json_file: self.CELL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) # Model basics def set_cell(self, cell, color): - cell = cell + 1 # Simulator cells not 0 based - try: - if cell in self.CELL_MAP: - ux = self.CELL_MAP[cell][0] - ix = self.CELL_MAP[cell][1] - 1 - sim_key = cell - # The simulator does not care about universes, but does care about UIDs. I'm manufacturing one by joinng the Universe and fixture ID into the key. - self.dirty[sim_key] = color - else: - print("WARNING: {0} not in cell ID MAP".format(cell)) - - except: - pass + cell += 1 # Simulator cells not 0 based + if cell not in self.CELL_MAP: + raise ValueError(f'Cell {cell} not in CELL_MAP') + + ux = self.CELL_MAP[cell][0] + ix = self.CELL_MAP[cell][1] - 1 + sim_key = cell + # The simulator does not care about universes, but does care about UIDs. I'm manufacturing one by joining the + # Universe and fixture ID into the key. + self.dirty[sim_key] = color def set_pixel(self, pixel, color, cellid): self.set_cell(cellid, color) def go(self): for num in self.dirty: - color = self.dirty[num] + color = self.dirty[num] msg = f'b {num} {color.r},{color.g},{color.b}\n' - if self.debug: - print(msg) + + logger.debug(msg) self.sock.send(msg.encode()) self.dirty = {} From b6f38329c0d391d94cb7e36ceda28230fd965479 Mon Sep 17 00:00:00 2001 From: John Major Date: Mon, 22 Jul 2019 21:17:47 -0700 Subject: [PATCH 07/60] ... --- color.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/color.py b/color.py index 3232311..e715469 100644 --- a/color.py +++ b/color.py @@ -100,21 +100,6 @@ __all__=['RGB', 'HSV', 'Hex', 'Color', 'RGBW'] -def saturation(rgb): - low = float(min(rgb.r, rgb.g, rgb.b)) - high = float(max(rgb.r, rgb.g, rgb.b)) - ret = 0 - if low > 0 and high > 0: - ret = round(100.0*((high-low)/high)) - return ret - -def getWhiteColor(rgb): - ret = 0 - try: - ret = int(round((255.0-saturation(rgb)) / 255.0 * (float(rgb.r) + float(rgb.b) + float(rgb.g))/3.0)) - except: - print("Error", rgb) - return ret def clamp(val, min_value, max_value): "Restrict a value between a minimum and a maximum value" @@ -139,12 +124,6 @@ def rgb_to_hsv(rgb): ret.append(rgb[-1]) return tuple(ret) -def rgbw_to_hsv(rgbw): - "convert a rgbw[0:3][0-255] tuple to hsv[0.0-1.0], plus w" - f = float(255) - ret = colorsys.rgb_to_hsv(rgbw[0]/f, rgbw[1]/f, rgbw[2]/f) - ret.append(rgbw[-1]) - return tuple(ret) def hsv_to_rgbw(hsv): assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) @@ -157,7 +136,7 @@ def hsv_to_rgbw(hsv): def hsv_to_rgb(hsv): assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) -# from IPython import embed; embed() + _rgb = colorsys.hsv_to_rgb(*tuple(hsv[0:3])) r = int(_rgb[0] * 0xff) g = int(_rgb[1] * 0xff) From cc2d135446a41b62e1ed91c0141da4f588147bfb Mon Sep 17 00:00:00 2001 From: John Major Date: Mon, 22 Jul 2019 21:31:12 -0700 Subject: [PATCH 08/60] moved color.py from my GD fork to here, fixed the shows trying to natively use RGBW --- color.py | 428 +++++++++++++++++++++----------- color1.py | 340 +++++++++++++++++++++++++ color2.py | 278 --------------------- shows/__init__.py | 2 + shows/left_to_right.py | 4 +- shows/left_to_right_and_back.py | 6 +- shows/one_by_one.py | 4 +- shows/random_cells.py | 8 +- shows/up_down.py | 6 +- 9 files changed, 641 insertions(+), 435 deletions(-) create mode 100644 color1.py delete mode 100644 color2.py diff --git a/color.py b/color.py index e715469..79e005c 100644 --- a/color.py +++ b/color.py @@ -1,44 +1,32 @@ """ Color -Color class that can be used interchangably as RGB or HSV org RGBW or HEX with -seamless translation. Use whichever is more convenient at the -time - RGB for familiarity, HSV to fade colors easily, RGBW as the new hotness +Color class that allows you to initialize a color in any of HSV, RGB, Hex, HSI color spaces. Once initialized, the corresponding RGBW values are calculated and you may modify the object in RGB or HSV color spaces( ie: by re-setting any component of HSV or RGB (ie, just resetting the R value) and all RGB/HSV/RGBW values will be recalculated. -The Color object takes rgb, hsv, rgbw, hex constructors and represents them all as hsv, doing the appropriate transformations when you wish to pull back any of the color types supported. This meanst the core data was stored as a 3 member hsv tuple. To enable RGBW support, I had to bolt on a 4th member of the tuple, holding the W value. Since all of our code emits colors as RGB(W now), I accepted this bit of dirty business b/c the shows we write will expect tuples of 4 to map properly to our pixes. The RGBW tuples will have the correct W value, even if the 4th value in HSV makes no sense (if you wish to grab the valid hsv tuple, you may still do so with tuple()[0:3] -So, when constructing Color objects, beside the original constructor signature, I've added 2 optional fields: -w=int (0 by default) -recalculate_w = boolean (True by default) - -w is set to zero by default, but will be calculated for the RGB values generated during the Color object construction. UNLESS you set recalculate_w=False, in which case it will be set to zero and remain zero even if you update the Color object - -This is the cool part of the Color object, once created, you have RGB & HSV values available to you interchangibly. If you change Color.r = 255, the corresponding Color.hsv will aslo change. If you have set recalculate_w == True, then when you reset any color value after object creation, the new appropriate RGBW w value will be calc'd and set. If set to False, it will remain as you set it. If you directly change the w value, it will not trigger recalculation. You may also at any time use Color.recalculate_w(True/False) to globally turn on/off w recalculation. - -So how does this all work with our models? Our original models expected a 3 member tuple of (r,g,b) per LED. Now, all LEDs expect a 4 member tuple(r,g,b,w). So, all of the existing shows that use RGB/HEX/HSV native color objects will send the correct (r,g,b,w) tuple now. - -I DID NOT make the choice to make this module support 3 chanel LEDs at this time. +The main goal of this class is to translate various color spaces into RGBW for use in RGBW pixels. +NOTE! this package will not control 3 channel RGB LEDs properly. +The color representation is maintained in HSV interanlly and translates to RGB (and RGBW, but not interactively). +Use whichever is more convenient at the time - RGB for familiarity, HSV to fade colors easily. RGB values range from 0 to 255 HSV values range from 0.0 to 1.0 -RGBW values range from 0 to 255 - >>> red = RGB(255, 0 ,0) - >>> green = HSV(0.33, 1.0, 1.0) - >>> ref = RGBW(255,0,0,85) *More on how W is dealt with latter + >>> red = RGB(255, 0 ,0) (RGBW = ) + >>> green = HSV(0.33, 1.0, 1.0) (RGBW = ) Colors may also be specified as hexadecimal string: >>> blue = Hex('#0000ff') -All three RGBW, RGB and HSV components are available as attributes +Both RGB and HSV components are available as attributes and may be set. - >>> red.r + >>> red.rgb_r 255 - >>> red.g = 128 + >>> red.rgb_g = 128 >>> red.rgb (255, 128, 0) @@ -50,7 +38,7 @@ >>> red = RGB(255,0,0) >>> purple = red.copy() - >>> purple.b = 255 + >>> purple.rgb_b = 255 >>> red.rgb (255, 0, 0) >>> purple.rgb @@ -67,39 +55,27 @@ ... print col.rgb ... col.v -= 0.1 ... - (0, 255, 0, 85) - (0, 229, 0, 76) - (0, 204, 0, 68) - (0, 178, 0, 59) - (0, 153, 0, 51) - (0, 127, 0, 42) - (0, 102, 0, 34) - (0, 76, 0, 25) - (0, 51, 0, 17) - (0, 25, 0, 8) -NOTE the W value is also calculated as it is expected to be used. - - -RGBW Handling -To keep the core functionality of this module, and include support for RGBW, I increased the expected tuple from 3 to 4 in length, with the last member of the tuple being the W value (in RGB space). - -Since the core of this module uses HSV as the reference point (THANKS GREG!), I had to do a LOT of hoop jumping to make this all work. + (0, 255, 0) + (0, 229, 0) + (0, 204, 0) + (0, 178, 0) + (0, 153, 0) + (0, 127, 0) + (0, 102, 0) + (0, 76, 0) + (0, 51, 0) + (0, 25, 0) -Basically, you can instantiate any of the non-RGBW types as usual and they will automatically calculate the appropriate W value. This recalculation can get tricky, but more on that in a moment. +RGBW -So, to set red: - r = = RGB(255,0,0) - ...the w value will be caluclated and appended - print.rgb - (255, 0, 0, 85) +To get the (r,g,b,w) tuples back from a Color object, simpy call Color.rgbw and you will return the (r,g,b,w) tuple. -I h """ import colorsys +import math from copy import deepcopy -__all__=['RGB', 'HSV', 'Hex', 'Color', 'RGBW'] - +__all__=['RGB', 'HSV', 'Hex', 'Color', 'HSI', 'RGBW'] def clamp(val, min_value, max_value): "Restrict a value between a minimum and a maximum value" @@ -107,99 +83,270 @@ def clamp(val, min_value, max_value): def is_hsv_tuple(hsv): "check that a tuple contains 3 values between 0.0 and 1.0" - return len(hsv) == 4 and all([(0.0 <= t <= 1.0) for t in hsv[0:3]]) + return len(hsv) == 3 and all([(0.0 <= t <= 1.0) for t in hsv]) -def is_rgb_tuple(rgb): - "check that a tuple contains 3 values between 0 and 255" - return len(rgb) == 4 and all([(0 <= t <= 255) for t in rgb]) +def is_hsi_tuple(hsi): + ret = True + if len(hsi) != 3: + ret = False + if hsi[0] < 0 or hsi[0] > 360: + ret = False + if hsi[1] <0.0 or hsi[1] > 1.0: + ret = False + if hsi[2] <0.0 or hsi[2] > 1.0: + ret = False + + return(ret) def is_rgbw_tuple(rgbw): - "check for valid RGBW tuple" + "check that rgbw tuple is as expected" return len(rgbw) == 4 and all([(0 <= t <= 255) for t in rgbw]) +def is_rgb_tuple(rgb): + "check that a tuple contains 3 values between 0 and 255" + return len(rgb) == 3 and all([(0 <= t <= 255) for t in rgb]) + def rgb_to_hsv(rgb): "convert a rgb[0-255] tuple to hsv[0.0-1.0]" f = float(255) - ret = list(colorsys.rgb_to_hsv(rgb[0]/f, rgb[1]/f, rgb[2]/f)) - ret.append(rgb[-1]) - return tuple(ret) - + return colorsys.rgb_to_hsv(rgb[0]/f, rgb[1]/f, rgb[2]/f) -def hsv_to_rgbw(hsv): +def hsv_to_rgb(hsv): assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) - _rgb = colorsys.hsv_to_rgb(*tuple(hsv[0:3])) + _rgb = colorsys.hsv_to_rgb(*hsv) r = int(_rgb[0] * 0xff) g = int(_rgb[1] * 0xff) b = int(_rgb[2] * 0xff) - return (r, g, b, hsv[-1]) + return (r,g,b) + +def constrain(val, min, max): + ret = val + if val <= min: + ret = min + if val >= max: + ret=max + return ret + +#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white +def hsi2rgb(H,S,I): + r = 0.0 + g = 0.0 + b = 0.0 + + H = math.fmod(H,360.0) + H = 3.14159*H/180.0 + S = constrain(S, 0.0,1.0) + I = constrain(I, 0.0,1.0) + + if H < 2.09439: + r = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + g = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + b = 255.0*I/3.0*(1.0-S) + elif H < 4.188787: + H = H - 2.09439 + g = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + b = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + r = 255.0*I/3.0*(1.0-S) + else: + H = H - 4.188787 + b = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + r = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + g = 255.0*I/3.0*(1.0-S) + + return ( constrain(int(r*3.0),0,255), constrain(int(g*3.0),0,255), constrain(int(b*3.0)\ +,0,255 )) #for some reason, the rgb numbers need to be X3... + + +#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white +def hsi2rgb(H,S,I): + r = 0.0 + g = 0.0 + b = 0.0 + + H = math.fmod(H,360.0) + H = 3.14159*H/180.0 + S = constrain(S, 0.0,1.0) + I = constrain(I, 0.0,1.0) + + if H < 2.09439: + r = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + g = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + b = 255.0*I/3.0*(1.0-S) + elif H < 4.188787: + H = H - 2.09439 + g = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + b = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + r = 255.0*I/3.0*(1.0-S) + else: + H = H - 4.188787 + b = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) + r = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) + g = 255.0*I/3.0*(1.0-S) + + return ( constrain(int(r*3.0),0,255), constrain(int(g*3.0),0,255), constrain(int(b*3.0)\ +,0,255 )) #for some reason, the rgb numbers need to be X3... + + +#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white +def hsi2rgbw(H,S,I): + r = 0 + g = 0 + b = 0 + w = 0 + cos_h = 0.0 + cos_1047_h = 0.0 + + H = float(math.fmod(H,360)) # cycle H around to 0-360 degrees + H = 3.14159*H/180.0 # Convert to radians. + S = constrain(S,0.0,1.0) + I = constrain(I,0.0,1.0) + + if(H < 2.09439): + cos_h = math.cos(H) + cos_1047_h = math.cos(1.047196667-H) + r = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) + g = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) + b = 0.0 + w = 255.0*(1.0-S)*I + elif(H < 4.188787): + H = H - 2.09439 + cos_h = math.cos(H) + cos_1047_h = math.cos(1.047196667-H) + g = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) + b = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) + r = 0.0 + w = 255.0*(1.0-S)*I + else: + H = H - 4.188787 + cos_h = math.cos(H) + cos_1047_h = math.cos(1.047196667-H) + b = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) + r = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) + g = 0.0 + w = 255.0*(1.0-S)*I + + return (int(constrain(r*3,0,255)), int(constrain(g*3,0,255)), int(constrain(b*3,0,255)) , int(constrain(w,0,255))) #for some reason, the rgb numbers need to be X3... + + +#https://en.wikipedia.org/wiki/HSL_and_HSV +def hsv2hsl(h,s,v): + h = constrain(h,0.0,360.0) + s = constrain(s,0.0,1.0) + v = constrain(v,0.0,1.0) + + Hhsl = h + Lhsl = v-(v*s/2.0) + Shsl = 0 + if Lhsl > 0.0 and Lhsl < 1.0: + Shsl = (v-Lhsl)/min(Lhsl, 1.0-Lhsl) + + return(Hhsl,Lhsl,Shsl) + + +#https://en.wikipedia.org/wiki/HSL_and_HSV +def hsl2hsv(h,s,l): + h =constrain(h,0.0,360.0) + s =constrain(s,0.0,1.0) + l =constrain(l,0.0,1.0) + + Hhsv = h + Vhsv = l + (s*min(l, 1.0-l)) + Shsv = 0 + if Vhsv > 0.0: + Shsv = 2.0-(2.0*l/Vhsv) + return(Hhsv,Shsv,Vhsv) + + +#https://en.wikipedia.org/wiki/HSL_and_HSV +def rgb2hsi(red,green,blue): + r = constrain(float(red)/255.0,0.0,1.0) + g = constrain(float(green)/255.0, 0.0,1.0) + b = constrain(float(blue)/255.0,0.0,1.0) + intensity = 0.33333*(r+g+b) + + M = max(r,g,b) + m = min(r,g,b) + C = M - m + + saturation = 0.0 + if intensity == 0.0: + saturation = 0.0 + else: + saturation = (1.0-(m/intensity)) + + hue = 0 + if M == m: + hue = 0 + if M == r: + if M == m: + hue = 0.0 + else: + hue = 60.0* (0.0 + ((g-b) / (M-m))) + if M == g: + if M == m: + hue = 0.0 + else: + hue = 60.0* (2.0 + ((b-r) / (M-m))) + if M == b: + if M == m: + hue = 0.0 + else: + hue = 60.0 * (4.0 + ((r-g) / (M-m))) + if hue < 0.0: + hue = hue + 360 + return(hue,abs(saturation),intensity) -def hsv_to_rgb(hsv): - assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) - _rgb = colorsys.hsv_to_rgb(*tuple(hsv[0:3])) - r = int(_rgb[0] * 0xff) - g = int(_rgb[1] * 0xff) - b = int(_rgb[2] * 0xff) - return (r, g, b, hsv[-1]) +def HSI(h,s,i): + "Create new HSI color" + t = (h,s,i) + assert is_hsi_tuple(t) + return RGB( hsi2rgb(h,s,i) ) -def RGBW(r,g,b,w, recalculate_w=True): - "Create new RGBW Color" - t = (r, g, b, w) - assert is_rgbw_tuple(t) - return(Color(rgb_to_hsv(t), recalculate_w)) -def RGB(r,g,b,w=0, recalculate_w=True): +def RGB(r,g,b): "Create a new RGB color" - t = (r, g, b, w) + t = (r,g,b) assert is_rgb_tuple(t) - return Color(rgb_to_hsv(t), recalculate_w) + return Color(rgb_to_hsv(t)) -def HSV(h,s,v, w=0.0, recalculate_w=True): +def HSV(h,s,v): "Create a new HSV color" - return Color((h, s, v, w), recalculate_w) + return Color((h,s,v)) + +def HSL(h,s,l): + "Create new HSL color" + (h,s,v) = hsl2hsv(h,s,l) + return Color(h,s,v) -def Hex(value, recalculate_w=True): +def Hex(value): "Create a new Color from a hex string" value = value.lstrip('#') lv = len(value) - rgb_t = (int(value[i:i+lv/3], 16) for i in range(0, lv, lv/3)) - r = next(rgb_t) - g = next(rgb_t) - b = next(rgb_t) - return RGB(r, g, b, 0, recalculate_w=True) #JEM -not sure what to do here + rgb_t = (int(value[i:i+int(lv/3)], 16) for i in range(0, lv, int(lv/3))) + return RGB(*rgb_t) class Color(object): - - def __init__(self, hsv_tuple, recalculate_w=True): - self.recalculate_w = recalculate_w + def __init__(self, hsv_tuple): self._set_hsv(hsv_tuple) - + + def __repr__(self): + return "rgb=%s hsv=%s" % (self.rgb, self.hsv) def copy(self): return deepcopy(self) - def _set_hsv(self, hsv_tuple, preserve_w=False): + def _set_hsv(self, hsv_tuple): assert is_hsv_tuple(hsv_tuple) + # convert to a list for component reassignment self.hsv_t = list(hsv_tuple) - #I'm trting to solve the problem of the c - if preserve_w is True: - pass - elif self.recalculate_w is True: - new_w = int(getWhiteColor(self)) - l = list(hsv_tuple) - l[-1] = new_w - hsv_tuple = tuple(l) - self.hsv_t = list(hsv_tuple) - else: - pass #all done - @property def rgbw(self): - "returns a rgbw[0-255] tuple" - return hsv_to_rgbw(self.hsv_t) - + "returns a tuple of 4 values each in the range of 0-255" + hsi = rgb2hsi(self.rgb[0], self.rgb[1], self.rgb[2]) + return hsi2rgbw( hsi[0], hsi[1], hsi[2] ) @property def rgb(self): @@ -228,7 +375,6 @@ def h(self): @h.setter def h(self, val): - assert 0.0 <= val <= 1.0 v = clamp(val, 0.0, 1.0) self.hsv_t[0] = round(v, 8) @@ -238,7 +384,6 @@ def s(self): @s.setter def s(self, val): - assert 0.0 <= val <= 1.0 v = clamp(val, 0.0, 1.0) self.hsv_t[1] = round(v, 8) @@ -248,73 +393,70 @@ def v(self): @v.setter def v(self, val): - assert 0.0 <= val <= 1.0 v = clamp(val, 0.0, 1.0) - new_hsv = self.hsv_t - new_hsv[2] = round(v, 8) - self._set_hsv(new_hsv) + self.hsv_t[2] = round(v, 8) + - """ - Properties representing individual RGB components + + """ + Properties representing individual RGB components """ @property - def r(self): + def rgb_r(self): return self.rgb[0] - @r.setter - def r(self, val): + @rgb_r.setter + def rgb_r(self, val): assert 0 <= val <= 255 - r, g, b, w = self.rgb - new = (val, g, b, w) + r,g,b = self.rgb + new = (val, g, b) assert is_rgb_tuple(new) self._set_hsv(rgb_to_hsv(new)) @property - def g(self): + def rgb_g(self): return self.rgb[1] - @g.setter - def g(self, val): + @rgb_g.setter + def rgb_g(self, val): assert 0 <= val <= 255 - r, g, b, w = self.rgb - new = (r, val, b, w) + r,g,b = self.rgb + new = (r, val, b) assert is_rgb_tuple(new) self._set_hsv(rgb_to_hsv(new)) @property - def b(self): + def rgb_b(self): return self.rgb[2] - @b.setter - def b(self, val): + @rgb_b.setter + def rgb_b(self, val): assert 0 <= val <= 255 - r, g, b, w = self.rgb - new = (r, g, val, w) + r,g,b = self.rgb + new = (r, g, val) assert is_rgb_tuple(new) self._set_hsv(rgb_to_hsv(new)) + """ + Properties representing individual RGBW components + """ @property - def w(self): - return int(self.rgbw[-1]) + def r(self): + return self.rgbw[0] - @w.setter - def w(self, val): - assert 0 <= val <= 255 - r, g, b, w = self.rgbw - new = (r, g, b, val) - self._set_hsv(rgb_to_hsv(new), preserve_w=True) + @property + def g(self): + return self.rgbw[1] @property - def recalculate_w(self): - return self._recalculate_w + def b(self): + return self.rgbw[2] + + @property + def w(self): + return self.rgbw[3] - @recalculate_w.setter - def recalculate_w(self, val=True): - self._recalculate_w = val - - - if __name__=='__main__': import doctest doctest.testmod() diff --git a/color1.py b/color1.py new file mode 100644 index 0000000..db8126d --- /dev/null +++ b/color1.py @@ -0,0 +1,340 @@ +""" +Color + +Color class that can be used interchangably as RGB or HSV org RGBW or HEX with +seamless translation. Use whichever is more convenient at the +time - RGB for familiarity, HSV to fade colors easily, RGBW as the new hotness + +The Color object takes rgb, hsv, rgbw, hex constructors and represents them all as hsv, doing the appropriate transformations when you wish to pull back any of the color types supported. This meanst the core data was stored as a 3 member hsv tuple. To enable RGBW support, I had to bolt on a 4th member of the tuple, holding the W value. Since all of our code emits colors as RGB(W now), I accepted this bit of dirty business b/c the shows we write will expect tuples of 4 to map properly to our pixes. The RGBW tuples will have the correct W value, even if the 4th value in HSV makes no sense (if you wish to grab the valid hsv tuple, you may still do so with tuple()[0:3] + +So, when constructing Color objects, beside the original constructor signature, I've added 2 optional fields: +w=int (0 by default) +recalculate_w = boolean (True by default) + +w is set to zero by default, but will be calculated for the RGB values generated during the Color object construction. UNLESS you set recalculate_w=False, in which case it will be set to zero and remain zero even if you update the Color object + +This is the cool part of the Color object, once created, you have RGB & HSV values available to you interchangibly. If you change Color.r = 255, the corresponding Color.hsv will aslo change. If you have set recalculate_w == True, then when you reset any color value after object creation, the new appropriate RGBW w value will be calc'd and set. If set to False, it will remain as you set it. If you directly change the w value, it will not trigger recalculation. You may also at any time use Color.recalculate_w(True/False) to globally turn on/off w recalculation. + +So how does this all work with our models? Our original models expected a 3 member tuple of (r,g,b) per LED. Now, all LEDs expect a 4 member tuple(r,g,b,w). So, all of the existing shows that use RGB/HEX/HSV native color objects will send the correct (r,g,b,w) tuple now. + +I DID NOT make the choice to make this module support 3 chanel LEDs at this time. + + +RGB values range from 0 to 255 +HSV values range from 0.0 to 1.0 +RGBW values range from 0 to 255 + + >>> red = RGB(255, 0 ,0) + >>> green = HSV(0.33, 1.0, 1.0) + >>> ref = RGBW(255,0,0,85) *More on how W is dealt with latter + +Colors may also be specified as hexadecimal string: + + >>> blue = Hex('#0000ff') + +All three RGBW, RGB and HSV components are available as attributes +and may be set. + + >>> red.r + 255 + + >>> red.g = 128 + >>> red.rgb + (255, 128, 0) + + >>> red.hsv + (0.08366013071895424, 1.0, 1.0) + +These objects are mutable, so you may want to make a +copy before changing a Color that may be shared + + >>> red = RGB(255,0,0) + >>> purple = red.copy() + >>> purple.b = 255 + >>> red.rgb + (255, 0, 0) + >>> purple.rgb + (255, 0, 255) + +Brightness can be adjusted by setting the 'v' property, even +when you're working in RGB. + +For example: to gradually dim a color +(ranges from 0.0 to 1.0) + + >>> col = RGB(0,255,0) + >>> while col.v > 0: + ... print col.rgb + ... col.v -= 0.1 + ... + (0, 255, 0, 85) + (0, 229, 0, 76) + (0, 204, 0, 68) + (0, 178, 0, 59) + (0, 153, 0, 51) + (0, 127, 0, 42) + (0, 102, 0, 34) + (0, 76, 0, 25) + (0, 51, 0, 17) + (0, 25, 0, 8) +NOTE the W value is also calculated as it is expected to be used. + + +RGBW Handling +To keep the core functionality of this module, and include support for RGBW, I increased the expected tuple from 3 to 4 in length, with the last member of the tuple being the W value (in RGB space). + +Since the core of this module uses HSV as the reference point (THANKS GREG!), I had to do a LOT of hoop jumping to make this all work. + +Basically, you can instantiate any of the non-RGBW types as usual and they will automatically calculate the appropriate W value. This recalculation can get tricky, but more on that in a moment. + +So, to set red: + r = = RGB(255,0,0) + ...the w value will be caluclated and appended + print.rgb + (255, 0, 0, 85) + +""" +import colorsys +from copy import deepcopy + +__all__=['RGB', 'HSV', 'Hex', 'Color', 'RGBW'] + +def saturation(rgb): + low = float(min(rgb.r, rgb.g, rgb.b)) + high = float(max(rgb.r, rgb.g, rgb.b)) + ret = 0 + if low > 0 and high > 0: + ret = round(100.0*((high-low)/high)) + return ret + +def getWhiteColor(rgb): + ret = 0 + try: + ret = int(round((255.0-saturation(rgb)) / 255.0 * (float(rgb.r) + float(rgb.b) + float(rgb.g))/3.0)) + except: + print("Error", rgb) + return ret + +def clamp(val, min_value, max_value): + "Restrict a value between a minimum and a maximum value" + return max(min(val, max_value), min_value) + +def is_hsv_tuple(hsv): + "check that a tuple contains 3 values between 0.0 and 1.0" + return len(hsv) == 4 and all([(0.0 <= t <= 1.0) for t in hsv[0:3]]) + +def is_rgb_tuple(rgb): + "check that a tuple contains 3 values between 0 and 255" + return len(rgb) == 4 and all([(0 <= t <= 255) for t in rgb]) + +def is_rgbw_tuple(rgbw): + "check for valid RGBW tuple" + return len(rgbw) == 4 and all([(0 <= t <= 255) for t in rgbw]) + +def rgb_to_hsv(rgb): + "convert a rgb[0-255] tuple to hsv[0.0-1.0]" + f = float(255) + ret = list(colorsys.rgb_to_hsv(rgb[0]/f, rgb[1]/f, rgb[2]/f)) + ret.append(rgb[-1]) + return tuple(ret) + +def rgbw_to_hsv(rgbw): + "convert a rgbw[0:3][0-255] tuple to hsv[0.0-1.0], plus w" + f = float(255) + ret = colorsys.rgb_to_hsv(rgbw[0]/f, rgbw[1]/f, rgbw[2]/f) + ret.append(rgbw[-1]) + return tuple(ret) + +def hsv_to_rgbw(hsv): + assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) + _rgb = colorsys.hsv_to_rgb(*tuple(hsv[0:3])) + r = int(_rgb[0] * 0xff) + g = int(_rgb[1] * 0xff) + b = int(_rgb[2] * 0xff) + return (r, g, b, hsv[-1]) + + +def hsv_to_rgb(hsv): + assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) +# from IPython import embed; embed() + _rgb = colorsys.hsv_to_rgb(*tuple(hsv[0:3])) + r = int(_rgb[0] * 0xff) + g = int(_rgb[1] * 0xff) + b = int(_rgb[2] * 0xff) + return (r, g, b, hsv[-1]) + +def RGBW(r,g,b,w, recalculate_w=False): + "Create new RGBW Color" + t = (r, g, b, w) + assert is_rgbw_tuple(t) + return(Color(rgb_to_hsv(t), recalculate_w)) + +def RGB(r,g,b,w=0, recalculate_w=False): + "Create a new RGB color" + t = (r, g, b, w) + assert is_rgb_tuple(t) + return Color(rgb_to_hsv(t), recalculate_w) + +def HSV(h,s,v, w=0.0, recalculate_w=False): + "Create a new HSV color" + return Color((h, s, v, w), recalculate_w) + +def Hex(value, recalculate_w=True): + "Create a new Color from a hex string" + value = value.lstrip('#') + lv = len(value) + rgb_t = (int(value[i:i+int(lv/3)], 16) for i in range(0, lv, int(lv/3))) + r = next(rgb_t) + g = next(rgb_t) + b = next(rgb_t) + return RGB(r, g, b, 0, recalculate_w=False) #JEM -not sure what to do here + +class Color(object): + + def __init__(self, hsv_tuple, recalculate_w=True): + self.recalculate_w = recalculate_w + self._set_hsv(hsv_tuple) + + + def copy(self): + return deepcopy(self) + + def _set_hsv(self, hsv_tuple, preserve_w=False): + assert is_hsv_tuple(hsv_tuple) + self.hsv_t = list(hsv_tuple) + + #I'm trting to solve the problem of the c + if preserve_w is True: + pass + elif self.recalculate_w is True: + new_w = int(getWhiteColor(self)) + l = list(hsv_tuple) + l[-1] = new_w + hsv_tuple = tuple(l) + self.hsv_t = list(hsv_tuple) + else: + pass #all done + + @property + def rgbw(self): + "returns a rgbw[0-255] tuple" + return hsv_to_rgbw(self.hsv_t) + + + @property + def rgb(self): + "returns a rgb[0-255] tuple" + return hsv_to_rgb(self.hsv_t) + + @property + def hsv(self): + "returns a hsv[0.0-1.0] tuple" + return tuple(self.hsv_t) + + @property + def hex(self): + "returns a hexadecimal string" + return '#%02x%02x%02x' % self.rgb + + """ + Properties representing individual HSV compnents + Adjusting 'H' shifts the color around the color wheel + Adjusting 'S' adjusts the saturation of the color + Adjusting 'V' adjusts the brightness/intensity of the color + """ + @property + def h(self): + return self.hsv_t[0] + + @h.setter + def h(self, val): + assert 0.0 <= val <= 1.0 + v = clamp(val, 0.0, 1.0) + self.hsv_t[0] = round(v, 8) + + @property + def s(self): + return self.hsv_t[1] + + @s.setter + def s(self, val): + assert 0.0 <= val <= 1.0 + v = clamp(val, 0.0, 1.0) + self.hsv_t[1] = round(v, 8) + + @property + def v(self): + return self.hsv_t[2] + + @v.setter + def v(self, val): + assert 0.0 <= val <= 1.0 + v = clamp(val, 0.0, 1.0) + new_hsv = self.hsv_t + new_hsv[2] = round(v, 8) + self._set_hsv(new_hsv) + + + """ + Properties representing individual RGB components + """ + @property + def r(self): + return self.rgb[0] + + @r.setter + def r(self, val): + assert 0 <= val <= 255 + r, g, b, w = self.rgb + new = (val, g, b, w) + assert is_rgb_tuple(new) + self._set_hsv(rgb_to_hsv(new)) + + @property + def g(self): + return self.rgb[1] + + @g.setter + def g(self, val): + assert 0 <= val <= 255 + r, g, b, w = self.rgb + new = (r, val, b, w) + assert is_rgb_tuple(new) + self._set_hsv(rgb_to_hsv(new)) + + @property + def b(self): + return self.rgb[2] + + @b.setter + def b(self, val): + assert 0 <= val <= 255 + r, g, b, w = self.rgb + new = (r, g, val, w) + assert is_rgb_tuple(new) + self._set_hsv(rgb_to_hsv(new)) + + @property + def w(self): + return int(self.rgbw[-1]) + + @w.setter + def w(self, val): + assert 0 <= val <= 255 + r, g, b, w = self.rgbw + new = (r, g, b, val) + self._set_hsv(rgb_to_hsv(new), preserve_w=True) + + @property + def recalculate_w(self): + return self._recalculate_w + + @recalculate_w.setter + def recalculate_w(self, val=True): + self._recalculate_w = val + + + +if __name__=='__main__': + import doctest + doctest.testmod() diff --git a/color2.py b/color2.py deleted file mode 100644 index f09d319..0000000 --- a/color2.py +++ /dev/null @@ -1,278 +0,0 @@ -"""JEM Playing Around With RGB<->HSI<->RGBW conversion formulas""" -""" Butchered the original color.py module to do this work, please do not consider this as production useful""" - -import colorsys -import math -import numpy as np -from copy import deepcopy - -__all__=['RGB', 'HSV', 'Hex', 'Color', 'RGBW'] - - -def constrain(val, min, max): - ret = val - if val <= min: - ret = min - if val >= max: - ret=max - return ret - - -def is_hsv_tuple(hsv): - "check that a tuple contains 3 values between 0.0 and 1.0" - return len(hsv) == 4 and all([(0.0 <= t <= 1.0) for t in hsv[0:3]]) - - -def is_rgb_tuple(rgb): - "check that a tuple contains 3 values between 0 and 255" - return len(rgb) == 4 and all([(0 <= t <= 255) for t in rgb]) - - -def is_rgbw_tuple(rgbw): - "check for valid RGBW tuple" - return len(rgbw) == 4 and all([(0 <= t <= 255) for t in rgbw]) - - -def test_hsi2rgbw(h,s,i): - (r,g,b,w) = hsi2rgbw(h,s,i) - print("ORIG-HSI: {0} / {1} / {2} ||| HSI: {3} / {4} / {5} ".format(h,s,i,r,g,b,w)) - - -def test_rgb2hsi2rgb(r,g,b): - (h,s,i) = rgb2hsi(r,g,b) - print("ORIGRGB: {0} / {1} / {2} ||| HSI: {3} / {4} / {5} ".format(r,g,b,h,s,i)) - (rr,gg,bb) = hsi2rgbA(h,s,i) - print("CONV RGB: {0} / {1} / {2} ||| HSI: {3} / {4} / {5} ".format(rr,gg,bb,h,s,i)) - - -def rgb_hsi_tests(): - colors = { - 'white' : ((1,1,1),(1.0,1.0,1.0)), #ok - 'olive' : ((.75,.75,0),(60.0,1.0,.5)), #ok - 'teal' : ((.5,1.0,1.0),(180.0,.4,.833)), #ok - 'purple' : ((.75,.25,.75),(300.0, .571, .583)), #ok - 'blue2' : ((.255,.104, .918),(251.1,.756,.426)), #ok - 'green2': ((.116,.675,.255),(134.9, .667, .349)), # ok - 'orange' : ((.931,.463,.316),(14.3, .446, .570)), #ok - 'yellow' : ((.998,.974,.532),(56.9, .363 ,.835)), #ok - 'dusty_blue' : ((.495,.493,.721),(240.5, .135, .570 )), #ok - 'dark' : ((0.0,0.0,0.0),(0.0,0.0,0.0)) #ok - } - - for i in colors: - print(i) - r,g,b = colors[i][0] - R = constrin(r*255.0,0,255) - G = constrain(g*255.0,0,255) - B = constrain(b*255.0,0,255) - - (h,s,i) = colors[i][1] - (H2,S2,I2) = rgb2hsi(R,G,B) - print("EXPECTED:: R: {0} / G: {1} / B: {2} || H: {3} / S: {4} / I: {5}".format(R,G,B,h,s,i)) - print("Calculated: ..........................................h= {0} / s= {1} / i {2}".format(round(H2,4),round(S2,4),round(I2,4))) - - -#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white -def hsi2rgb(H,S,I): - r = 0.0 - g = 0.0 - b = 0.0 - - H = math.fmod(H,360.0) - H = 3.14159*H/180.0 - S = constrain(S, 0.0,1.0) - I = constrain(I, 0.0,1.0) - - if H < 2.09439: - r = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) - g = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) - b = 255.0*I/3.0*(1.0-S) - elif H < 4.188787: - H = H - 2.09439 - g = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) - b = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) - r = 255.0*I/3.0*(1.0-S) - else: - H = H - 4.188787 - b = 255.0*I/3.0*(1.0+S*math.cos(H)/math.cos(1.047196667-H)) - r = 255.0*I/3.0*(1.0+S*(1.0-math.cos(H)/math.cos(1.047196667-H))) - g = 255.0*I/3.0*(1.0-S) - - return ( constrain(int(r*3.0),0,255), constrain(int(g*3.0),0,255), constrain(int(b*3.0),0,255 )) #for some reason, the rgb numbers need to be X3... - - -#https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white -def hsi2rgbw(H,S,I): - r = 0 - g = 0 - b = 0 - w = 0 - cos_h = 0.0 - cos_1047_h = 0.0 - - H = float(math.fmod(H,360)) # cycle H around to 0-360 degrees - H = 3.14159*H/180.0 # Convert to radians. - S = constrain(S,0.0,1.0) - I = constrain(I,0.0,1.0) - - if(H < 2.09439): - cos_h = math.cos(H) - cos_1047_h = math.cos(1.047196667-H) - r = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) - g = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) - b = 0.0 - w = 255.0*(1.0-S)*I - elif(H < 4.188787): - H = H - 2.09439 - cos_h = math.cos(H) - cos_1047_h = math.cos(1.047196667-H) - g = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) - b = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) - r = 0.0 - w = 255.0*(1.0-S)*I - else: - H = H - 4.188787 - cos_h = math.cos(H) - cos_1047_h = math.cos(1.047196667-H) - b = S*255.0*I/3.0*(1.0+cos_h/cos_1047_h) - r = S*255.0*I/3.0*(1.0+(1.0-cos_h/cos_1047_h)) - g = 0.0 - w = 255.0*(1.0-S)*I - - return (r*3,g*3,b*3,w) #for some reason, the rgb numbers need to be X3... - - - -def rgb2hsi(red,green,blue): - r = constrain(float(red)/255.0,0.0,1.0) - g = constrain(float(green)/255.0, 0.0,1.0) - b = constrain(float(blue)/255.0,0.0,1.0) - intensity = 0.33333*(r+g+b) - - M = max(r,g,b) - m = min(r,g,b) - C = M - m - - saturation = 0.0 - if intensity == 0.0: - saturation = 0.0 - else: - saturation = (1-(m/intensity)) - - hue = 0 - if M == m: - hue = 0 - if M == r: - print("A",M,m) - if M == m: - hue = 0.0 - else: - hue = 60.0* (0.0 + ((g-b) / (M-m))) - if M == g: - print("B") - if M == m: - hue = 0.0 - else: - hue = 60.0* (2.0 + ((b-r) / (M-m))) - if M == b: - print("C") - if M == m: - hue = 0.0 - else: - hue = 60.0 * (4.0 + ((r-g) / (M-m))) - if hue < 0.0: - print("D") - hue = hue + 360 - - return(hue,abs(saturation),intensity) - - - - -def HSI(h,s,i): - return Color(0,0,0,0) - - -def RGBW(r,g,b,w): - "Create new RGBW Color" - t = (r,g,b,w) - assert is_rgbw_tuple(t) - return(Color(t)) - -def RGB(r,g,b): - "Create a new RGB color" - return Color(hsi2rgbw(rgb2hsi(r,g,b))) - - - -def Hex(value): - "Create a new Color from a hex string" - value = value.lstrip('#') - lv = len(value) - rgb_t = (int(value[i:i+lv/3], 16) for i in range(0, lv, lv/3)) - r = rgb_t.next() - g = rgb_t.next() - b = rgb_t.next() - return (r,g,b) - - -class Color(object): - - def __init__(self, rgbw_tuple): -# from IPython import embed; embed() - print(rgbw_tuple) - self.rgbw = rgbw_tuple - - - def copy(self): - return deepcopy(self) - - - - def rgbw(self): - "returns a rgbw[0-255] tuple" - return self.rgbw - - - - """ - Properties representing individual RGBW components - """ - @property - def r(self): - return self.rgbw[0] - - @r.setter - def r(self, val): - self.rgbw[0] = val - - @property - def g(self): - return self.rgbw[1] - - @g.setter - def g(self, val): - self.rgbw[1] = val - - @property - def b(self): - return self.rgbw[2] - - @b.setter - def b(self, val): - self.rgbw[2] = val - - @property - def w(self): - return rgbw[3] - - @w.setter - def w(self,val): - self.rgbw[3] = val - - - - -if __name__=='__main__': - import doctest - doctest.testmod() diff --git a/shows/__init__.py b/shows/__init__.py index c7e6475..4c7eeb1 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -5,3 +5,5 @@ from .random_cells import Random from .showbase import ShowBase, load_shows, random_shows from .up_down import UpDown +from .cycle_hsv import CycleHSV +from .cycle_hsv2 import CycleHSV2 diff --git a/shows/left_to_right.py b/shows/left_to_right.py index 5b90461..e3631ee 100644 --- a/shows/left_to_right.py +++ b/shows/left_to_right.py @@ -1,5 +1,5 @@ from .showbase import ShowBase -from color import RGBW +from color import RGB class LeftToRight(ShowBase): @@ -23,7 +23,7 @@ def next_frame(self): if cell is None: pass else: - self.tri_grid.set_cell_by_cellid(cell.get_id(), RGBW(255, 255, 25, 25)) + self.tri_grid.set_cell_by_cellid(cell.get_id(), RGB( 255, 25, 25)) x += 1 x = 0 y += 1 diff --git a/shows/left_to_right_and_back.py b/shows/left_to_right_and_back.py index b6ae5c2..9a551c3 100644 --- a/shows/left_to_right_and_back.py +++ b/shows/left_to_right_and_back.py @@ -1,5 +1,5 @@ from .showbase import ShowBase -from color import RGBW +from color import RGB import time @@ -30,7 +30,7 @@ def next_frame(self): r=255 g = 0 for pix in range(6): - self.tri_grid.set_pixel(cell.get_pixels()[pix], RGBW(r, g, 0, 1), cell.get_id()) + self.tri_grid.set_pixel(cell.get_pixels()[pix], RGB(r, g, 0), cell.get_id()) time.sleep(.2) self.tri_grid.go() g += 40 @@ -56,7 +56,7 @@ def next_frame(self): g = 255 b = 0 for pix in range(6): - self.tri_grid.set_pixel(cell.get_pixels()[5-pix], RGBW(0, g, b, 1), cell.get_id()) + self.tri_grid.set_pixel(cell.get_pixels()[5-pix], RGB(0, g, b), cell.get_id()) time.sleep(.2) self.tri_grid.go() g -= 40 diff --git a/shows/one_by_one.py b/shows/one_by_one.py index 62eb76f..5100560 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -1,5 +1,5 @@ from .showbase import ShowBase -from color import RGBW +from color import RGB class OneByOne(ShowBase): @@ -15,7 +15,7 @@ def next_frame(self): while True: self.tri_grid.clear() print(cell_n) - self.tri_grid.set_cell_by_cellid(self.tri_grid.get_cells()[cell_n].get_id(), RGBW(255, 255, 25, 25)) + self.tri_grid.set_cell_by_cellid(self.tri_grid.get_cells()[cell_n].get_id(), RGB(255, 255, 25)) if cell_n >= ncells: cell_n = -1 diff --git a/shows/random_cells.py b/shows/random_cells.py index eccf1c2..f8d916c 100644 --- a/shows/random_cells.py +++ b/shows/random_cells.py @@ -1,5 +1,5 @@ from .showbase import ShowBase -from color import RGBW +from color import RGB import random as rnd @@ -7,14 +7,14 @@ class Random(ShowBase): def __init__(self, tri_grid, frame_delay = 0.1): self.tri_grid = tri_grid self.frame_delay = frame_delay - self.n_cells = len(self.tri_grid.get_cells()) from IPython import embed; embed() def next_frame(self): + while True: self.tri_grid.clear() - self.tri_grid.set_cell_by_cellid(rnd.randint(1, self.n_cells-2), RGBW(200, 255, 25, 25)) - self.tri_grid.set_cell_by_cellid(rnd.randint(1, self.n_cells-2), RGBW(200, 10, 25, 25)) + self.tri_grid.set_cell_by_cellid(rnd.randint(1, self.n_cells-2), RGB(200, 255, 25)) + self.tri_grid.set_cell_by_cellid(rnd.randint(1, self.n_cells-2), RGB(200, 10, 25)) yield self.frame_delay diff --git a/shows/up_down.py b/shows/up_down.py index 3b97563..0a8f07e 100644 --- a/shows/up_down.py +++ b/shows/up_down.py @@ -1,5 +1,5 @@ from .showbase import ShowBase -from color import RGBW +from color import RGB class UpDown(ShowBase): @@ -17,12 +17,12 @@ def next_frame(self): print('up') for i in self.cells.get_up_cells(): print("Up", i.get_id()) - self.cells.set_cell_by_cellid(i.get_id(), RGBW(255, 255, 255, 255)) + self.cells.set_cell_by_cellid(i.get_id(), RGB( 255, 255, 255)) else: print('down') for i in self.cells.get_down_cells(): print("down", i.get_id()) - self.cells.set_cell_by_cellid(i.get_id(), RGBW(255, 0, 0, 0)) + self.cells.set_cell_by_cellid(i.get_id(), RGB( 255, 0, 0)) if a == "up": a = "down" From 8835e03a1b20726b5c2e023be9b693c7995880ee Mon Sep 17 00:00:00 2001 From: John Major Date: Mon, 22 Jul 2019 21:38:52 -0700 Subject: [PATCH 09/60] missing show --- shows/__init__.py | 2 +- shows/cycle_hsv.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 shows/cycle_hsv.py diff --git a/shows/__init__.py b/shows/__init__.py index 4c7eeb1..f82a68e 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -6,4 +6,4 @@ from .showbase import ShowBase, load_shows, random_shows from .up_down import UpDown from .cycle_hsv import CycleHSV -from .cycle_hsv2 import CycleHSV2 + diff --git a/shows/cycle_hsv.py b/shows/cycle_hsv.py new file mode 100644 index 0000000..03a0e9c --- /dev/null +++ b/shows/cycle_hsv.py @@ -0,0 +1,74 @@ +from .showbase import ShowBase +from color import HSV as hsv +from color1 import HSV as hsv1 + +import random as rnd +import time + +class CycleHSV(ShowBase): + def __init__(self, tri_grid, frame_delay = 0.1): + self.tri_grid = tri_grid + self.frame_delay = frame_delay + self.n_cells = len(self.tri_grid.get_cells()) +# from IPython import embed; embed() + + + def next_frame(self): + + self.tri_grid.clear() + time.sleep(3) + + while True: + self.ca= hsv(0.0,0.0,0.0) + self.cb= hsv1(0.0,0.0,0.0) + while self.ca.v < 1.0: + self.ca.v += 0.0008 + self.tri_grid.set_all_cells(self.ca) + self.tri_grid.go() + time.sleep(.01) + print('CA', self.ca.hsv) + + while self.ca.s < 1.0: + self.ca.s += 0.0008 + self.tri_grid.set_all_cells(self.ca) + self.tri_grid.go() + time.sleep(.01) + print('CA', self.ca.hsv) + + while self.ca.h < 1.0: + self.ca.h += 0.0008 + self.tri_grid.set_all_cells(self.ca) + self.tri_grid.go() + time.sleep(.01) + print('CA', self.ca.hsv) + + self.tri_grid.clear() + time.sleep(3) + + while self.cb.v < 1.0: + self.cb.v += 0.0008 + self.tri_grid.set_all_cells(self.cb) + self.tri_grid.go() + time.sleep(.01) + print('CB', self.cb.hsv) + + while self.cb.s < 1.0: + self.cb.s += 0.0008 + self.tri_grid.set_all_cells(self.cb) + self.tri_grid.go() + time.sleep(.01) + print('CB', self.cb.hsv) + + while self.cb.h < 1.0: + self.cb.h += 0.0008 + self.tri_grid.set_all_cells(self.cb) + self.tri_grid.go() + time.sleep(.01) + print('CB', self.cb.hsv) + + self.tri_grid.clear() + self.tri_grid.set_all_cells(hsv(1.0,1.0,1.0)) + time.sleep(3) + self.tri_grid.clear() + + yield self.frame_delay From a5fdc0ab45ff5dbb5902105e11c7238b8eaea256 Mon Sep 17 00:00:00 2001 From: John Major Date: Mon, 22 Jul 2019 21:43:22 -0700 Subject: [PATCH 10/60] fixed RGBW dependency --- triangle_grid.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/triangle_grid.py b/triangle_grid.py index c907325..9bdafc7 100644 --- a/triangle_grid.py +++ b/triangle_grid.py @@ -1,4 +1,4 @@ -from color import RGBW +from color import RGB def make_tri(model, n_rows): @@ -153,7 +153,7 @@ def go(self): self._model.go() def clear(self): - self.set_all_cells(RGBW(0, 0, 0, 0)) + self.set_all_cells(RGB( 0, 0, 0)) self.go() def get_cells(self): @@ -215,7 +215,7 @@ def set_pixel(self, pixel, color, cellid): self._model.set_pixel(pixel, color, cellid) ###Have to pass the cellid through b/c the simulator does not understand pixels def clear(self): - self.set_all_cells(RGBW(0, 0, 0, 0)) + self.set_all_cells(RGB( 0, 0, 0)) self.go() def go(self): From 869e4e51bbaf449bf19c7ba5d2be48488aeca9ed Mon Sep 17 00:00:00 2001 From: John Major Date: Mon, 22 Jul 2019 22:10:22 -0700 Subject: [PATCH 11/60] Edge of strobing ability --- model/sacn_model.py | 2 +- shows/__init__.py | 2 +- shows/strobe.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 shows/strobe.py diff --git a/model/sacn_model.py b/model/sacn_model.py index a348b80..5529caf 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -16,7 +16,7 @@ def __init__(self, max_dmx, model_json=None): # Must supply an IP address to bind to that is in the same subnet as the devices routing the universes. Might # have to assign a second IP to the eth adapter to get this to work (this is what I had to do on my mac) - self.sender = sacn.sACNsender(bind_address="192.168.1.210", universeDiscovery=False) + self.sender = sacn.sACNsender(bind_address="192.168.1.113", universeDiscovery=False) self.sender.start() self.PIXEL_MAP = None # dictionary which will hold an array of 512 int's for each universe, universes are keys to the arrays. diff --git a/shows/__init__.py b/shows/__init__.py index f82a68e..8d42a22 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -6,4 +6,4 @@ from .showbase import ShowBase, load_shows, random_shows from .up_down import UpDown from .cycle_hsv import CycleHSV - +from .strobe import Strobe diff --git a/shows/strobe.py b/shows/strobe.py new file mode 100644 index 0000000..6a44e60 --- /dev/null +++ b/shows/strobe.py @@ -0,0 +1,30 @@ +from .showbase import ShowBase +from color import RGB +import random as rnd +import time + +class Strobe(ShowBase): + def __init__(self, tri_grid, frame_delay = 0.002): + self.tri_grid = tri_grid + self.frame_delay = frame_delay + self.n_cells = len(self.tri_grid.get_cells()) + + + def next_frame(self): + + while True: + self.tri_grid.clear() + self.tri_grid.set_all_cells( RGB(200, 5, 5)) + self.tri_grid.go() + time.sleep(0.02) + self.tri_grid.set_all_cells( RGB(100,100,100)) + self.tri_grid.go() + time.sleep(0.01) + self.tri_grid.set_all_cells( RGB(5, 5, 255)) + time.sleep(0.02) + self.tri_grid.go() + self.tri_grid.set_all_cells( RGB(100,100,100)) + self.tri_grid.go() + time.sleep(0.01) + + yield self.frame_delay From 3437e891fa8ea93d5b192ba957b8ed411011ecae Mon Sep 17 00:00:00 2001 From: John Major Date: Mon, 22 Jul 2019 22:12:31 -0700 Subject: [PATCH 12/60] ok, moving to a branch --- color.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/color.py b/color.py index 79e005c..8a32df7 100644 --- a/color.py +++ b/color.py @@ -258,10 +258,10 @@ def hsl2hsv(h,s,l): #https://en.wikipedia.org/wiki/HSL_and_HSV -def rgb2hsi(red,green,blue): - r = constrain(float(red)/255.0,0.0,1.0) - g = constrain(float(green)/255.0, 0.0,1.0) - b = constrain(float(blue)/255.0,0.0,1.0) +def rgb2hsi(r, g, b): + r = constrain(float(r)/255.0,0.0,1.0) + g = constrain(float(g)/255.0, 0.0,1.0) + b = constrain(float(b)/255.0,0.0,1.0) intensity = 0.33333*(r+g+b) M = max(r,g,b) From 1959f040927eeefad69b4e8140dc5351ffd46209 Mon Sep 17 00:00:00 2001 From: John Major Date: Tue, 23 Jul 2019 01:24:16 -0700 Subject: [PATCH 13/60] Fixed simulator to pull RGB values from color object instead of rgb from the RGBW string. Trimmed simulator triangle grid to 11 rows, which will be our grid length --- Simulators/TriangleSimulator/mapping_tri.csv | 150 +------- .../TriangleSimulator/triangleCellMapping.csv | 137 +------ color.py | 37 +- color1.py | 340 ------------------ data/pixel_map.json | 137 +------ go_tri.py | 2 +- model/ola_model.py | 4 +- model/sacn_model.py | 4 +- model/simulator.py | 6 +- shows/cycle_hsv.py | 27 -- shows/one_by_one.py | 2 +- shows/up_down.py | 14 +- 12 files changed, 47 insertions(+), 813 deletions(-) delete mode 100644 color1.py diff --git a/Simulators/TriangleSimulator/mapping_tri.csv b/Simulators/TriangleSimulator/mapping_tri.csv index 3e8ac1a..4b5e360 100644 --- a/Simulators/TriangleSimulator/mapping_tri.csv +++ b/Simulators/TriangleSimulator/mapping_tri.csv @@ -119,152 +119,4 @@ 118,"118" 119,"119" 120,"120" -121,"121" -122,"122" -123,"123" -124,"124" -125,"125" -126,"126" -127,"127" -128,"128" -129,"129" -130,"130" -131,"131" -132,"132" -133,"133" -134,"134" -135,"135" -136,"136" -137,"137" -138,"138" -139,"139" -140,"140" -141,"141" -142,"142" -143,"143" -144,"144" -145,"145" -146,"146" -147,"147" -148,"148" -149,"149" -150,"150" -151,"151" -152,"152" -153,"153" -154,"154" -155,"155" -156,"156" -157,"157" -158,"158" -159,"159" -160,"160" -161,"161" -162,"162" -163,"163" -164,"164" -165,"165" -166,"166" -167,"167" -168,"168" -169,"169" -170,"170" -171,"171" -172,"172" -173,"173" -174,"174" -175,"175" -176,"176" -177,"177" -178,"178" -179,"179" -180,"180" -181,"181" -182,"182" -183,"183" -184,"184" -185,"185" -186,"186" -187,"187" -188,"188" -189,"189" -190,"190" -191,"191" -192,"192" -193,"193" -194,"194" -195,"195" -196,"196" -197,"197" -198,"198" -199,"199" -200,"200" -201,"201" -202,"202" -203,"203" -204,"204" -205,"205" -206,"206" -207,"207" -208,"208" -209,"209" -210,"210" -211,"211" -212,"212" -213,"213" -214,"214" -215,"215" -216,"216" -217,"217" -218,"218" -219,"219" -220,"220" -221,"221" -222,"222" -223,"223" -224,"224" -225,"225" -226,"226" -227,"227" -228,"228" -229,"229" -230,"230" -231,"231" -232,"232" -233,"233" -234,"234" -235,"235" -236,"236" -237,"237" -238,"238" -239,"239" -240,"240" -241,"241" -242,"242" -243,"243" -244,"244" -245,"245" -246,"246" -247,"247" -248,"248" -249,"249" -250,"250" -251,"251" -252,"252" -253,"253" -254,"254" -255,"255" -256,"256" -257,"257" -258,"258" -259,"259" -260,"260" -261,"261" -262,"262" -263,"263" -264,"264" -265,"265" -266,"266" -267,"267" -268,"268" -269,"269" +121,"121" \ No newline at end of file diff --git a/Simulators/TriangleSimulator/triangleCellMapping.csv b/Simulators/TriangleSimulator/triangleCellMapping.csv index 52e45ac..609b8a8 100644 --- a/Simulators/TriangleSimulator/triangleCellMapping.csv +++ b/Simulators/TriangleSimulator/triangleCellMapping.csv @@ -120,139 +120,4 @@ triangle,512.5,412.5,493.75,443.75,531.25,443.75,117 triangle,512.5,412.5,550.0,412.5,531.25,443.75,118 triangle,550.0,412.5,531.25,443.75,568.75,443.75,119 triangle,550.0,412.5,587.5,412.5,568.75,443.75,120 -triangle,587.5,412.5,568.75,443.75,606.25,443.75,121 -triangle,193.75,443.75,175.0,475.0,212.5,475.0,122 -triangle,193.75,443.75,231.25,443.75,212.5,475.0,123 -triangle,231.25,443.75,212.5,475.0,250.0,475.0,124 -triangle,231.25,443.75,268.75,443.75,250.0,475.0,125 -triangle,268.75,443.75,250.0,475.0,287.5,475.0,126 -triangle,268.75,443.75,306.25,443.75,287.5,475.0,127 -triangle,306.25,443.75,287.5,475.0,325.0,475.0,128 -triangle,306.25,443.75,343.75,443.75,325.0,475.0,129 -triangle,343.75,443.75,325.0,475.0,362.5,475.0,130 -triangle,343.75,443.75,381.25,443.75,362.5,475.0,131 -triangle,381.25,443.75,362.5,475.0,400.0,475.0,132 -triangle,381.25,443.75,418.75,443.75,400.0,475.0,133 -triangle,418.75,443.75,400.0,475.0,437.5,475.0,134 -triangle,418.75,443.75,456.25,443.75,437.5,475.0,135 -triangle,456.25,443.75,437.5,475.0,475.0,475.0,136 -triangle,456.25,443.75,493.75,443.75,475.0,475.0,137 -triangle,493.75,443.75,475.0,475.0,512.5,475.0,138 -triangle,493.75,443.75,531.25,443.75,512.5,475.0,139 -triangle,531.25,443.75,512.5,475.0,550.0,475.0,140 -triangle,531.25,443.75,568.75,443.75,550.0,475.0,141 -triangle,568.75,443.75,550.0,475.0,587.5,475.0,142 -triangle,568.75,443.75,606.25,443.75,587.5,475.0,143 -triangle,606.25,443.75,587.5,475.0,625.0,475.0,144 -triangle,175.0,475.0,156.25,506.25,193.75,506.25,145 -triangle,175.0,475.0,212.5,475.0,193.75,506.25,146 -triangle,212.5,475.0,193.75,506.25,231.25,506.25,147 -triangle,212.5,475.0,250.0,475.0,231.25,506.25,148 -triangle,250.0,475.0,231.25,506.25,268.75,506.25,149 -triangle,250.0,475.0,287.5,475.0,268.75,506.25,150 -triangle,287.5,475.0,268.75,506.25,306.25,506.25,151 -triangle,287.5,475.0,325.0,475.0,306.25,506.25,152 -triangle,325.0,475.0,306.25,506.25,343.75,506.25,153 -triangle,325.0,475.0,362.5,475.0,343.75,506.25,154 -triangle,362.5,475.0,343.75,506.25,381.25,506.25,155 -triangle,362.5,475.0,400.0,475.0,381.25,506.25,156 -triangle,400.0,475.0,381.25,506.25,418.75,506.25,157 -triangle,400.0,475.0,437.5,475.0,418.75,506.25,158 -triangle,437.5,475.0,418.75,506.25,456.25,506.25,159 -triangle,437.5,475.0,475.0,475.0,456.25,506.25,160 -triangle,475.0,475.0,456.25,506.25,493.75,506.25,161 -triangle,475.0,475.0,512.5,475.0,493.75,506.25,162 -triangle,512.5,475.0,493.75,506.25,531.25,506.25,163 -triangle,512.5,475.0,550.0,475.0,531.25,506.25,164 -triangle,550.0,475.0,531.25,506.25,568.75,506.25,165 -triangle,550.0,475.0,587.5,475.0,568.75,506.25,166 -triangle,587.5,475.0,568.75,506.25,606.25,506.25,167 -triangle,587.5,475.0,625.0,475.0,606.25,506.25,168 -triangle,625.0,475.0,606.25,506.25,643.75,506.25,169 -triangle,156.25,506.25,137.5,537.5,175.0,537.5,170 -triangle,156.25,506.25,193.75,506.25,175.0,537.5,171 -triangle,193.75,506.25,175.0,537.5,212.5,537.5,172 -triangle,193.75,506.25,231.25,506.25,212.5,537.5,173 -triangle,231.25,506.25,212.5,537.5,250.0,537.5,174 -triangle,231.25,506.25,268.75,506.25,250.0,537.5,175 -triangle,268.75,506.25,250.0,537.5,287.5,537.5,176 -triangle,268.75,506.25,306.25,506.25,287.5,537.5,177 -triangle,306.25,506.25,287.5,537.5,325.0,537.5,178 -triangle,306.25,506.25,343.75,506.25,325.0,537.5,179 -triangle,343.75,506.25,325.0,537.5,362.5,537.5,180 -triangle,343.75,506.25,381.25,506.25,362.5,537.5,181 -triangle,381.25,506.25,362.5,537.5,400.0,537.5,182 -triangle,381.25,506.25,418.75,506.25,400.0,537.5,183 -triangle,418.75,506.25,400.0,537.5,437.5,537.5,184 -triangle,418.75,506.25,456.25,506.25,437.5,537.5,185 -triangle,456.25,506.25,437.5,537.5,475.0,537.5,186 -triangle,456.25,506.25,493.75,506.25,475.0,537.5,187 -triangle,493.75,506.25,475.0,537.5,512.5,537.5,188 -triangle,493.75,506.25,531.25,506.25,512.5,537.5,189 -triangle,531.25,506.25,512.5,537.5,550.0,537.5,190 -triangle,531.25,506.25,568.75,506.25,550.0,537.5,191 -triangle,568.75,506.25,550.0,537.5,587.5,537.5,192 -triangle,568.75,506.25,606.25,506.25,587.5,537.5,193 -triangle,606.25,506.25,587.5,537.5,625.0,537.5,194 -triangle,606.25,506.25,643.75,506.25,625.0,537.5,195 -triangle,643.75,506.25,625.0,537.5,662.5,537.5,196 -triangle,137.5,537.5,118.75,568.75,156.25,568.75,197 -triangle,137.5,537.5,175.0,537.5,156.25,568.75,198 -triangle,175.0,537.5,156.25,568.75,193.75,568.75,199 -triangle,175.0,537.5,212.5,537.5,193.75,568.75,200 -triangle,212.5,537.5,193.75,568.75,231.25,568.75,201 -triangle,212.5,537.5,250.0,537.5,231.25,568.75,202 -triangle,250.0,537.5,231.25,568.75,268.75,568.75,203 -triangle,250.0,537.5,287.5,537.5,268.75,568.75,204 -triangle,287.5,537.5,268.75,568.75,306.25,568.75,205 -triangle,287.5,537.5,325.0,537.5,306.25,568.75,206 -triangle,325.0,537.5,306.25,568.75,343.75,568.75,207 -triangle,325.0,537.5,362.5,537.5,343.75,568.75,208 -triangle,362.5,537.5,343.75,568.75,381.25,568.75,209 -triangle,362.5,537.5,400.0,537.5,381.25,568.75,210 -triangle,400.0,537.5,381.25,568.75,418.75,568.75,211 -triangle,400.0,537.5,437.5,537.5,418.75,568.75,212 -triangle,437.5,537.5,418.75,568.75,456.25,568.75,213 -triangle,437.5,537.5,475.0,537.5,456.25,568.75,214 -triangle,475.0,537.5,456.25,568.75,493.75,568.75,215 -triangle,475.0,537.5,512.5,537.5,493.75,568.75,216 -triangle,512.5,537.5,493.75,568.75,531.25,568.75,217 -triangle,512.5,537.5,550.0,537.5,531.25,568.75,218 -triangle,550.0,537.5,531.25,568.75,568.75,568.75,219 -triangle,550.0,537.5,587.5,537.5,568.75,568.75,220 -triangle,587.5,537.5,568.75,568.75,606.25,568.75,221 -triangle,587.5,537.5,625.0,537.5,606.25,568.75,222 -triangle,625.0,537.5,606.25,568.75,643.75,568.75,223 -triangle,625.0,537.5,662.5,537.5,643.75,568.75,224 -triangle,662.5,537.5,643.75,568.75,681.25,568.75,225 -triangle,118.75,568.75,100.0,600.0,137.5,600.0,226 -triangle,118.75,568.75,156.25,568.75,137.5,600.0,227 -triangle,156.25,568.75,137.5,600.0,175.0,600.0,228 -triangle,156.25,568.75,193.75,568.75,175.0,600.0,229 -triangle,193.75,568.75,175.0,600.0,212.5,600.0,230 -triangle,193.75,568.75,231.25,568.75,212.5,600.0,231 -triangle,231.25,568.75,212.5,600.0,250.0,600.0,232 -triangle,231.25,568.75,268.75,568.75,250.0,600.0,233 -triangle,268.75,568.75,250.0,600.0,287.5,600.0,234 -triangle,268.75,568.75,306.25,568.75,287.5,600.0,235 -triangle,306.25,568.75,287.5,600.0,325.0,600.0,236 -triangle,306.25,568.75,343.75,568.75,325.0,600.0,237 -triangle,343.75,568.75,325.0,600.0,362.5,600.0,238 -triangle,343.75,568.75,381.25,568.75,362.5,600.0,239 -triangle,381.25,568.75,362.5,600.0,400.0,600.0,240 -triangle,381.25,568.75,418.75,568.75,400.0,600.0,241 -triangle,418.75,568.75,400.0,600.0,437.5,600.0,242 -triangle,418.75,568.75,456.25,568.75,437.5,600.0,243 -triangle,456.25,568.75,437.5,600.0,475.0,600.0,244 -triangle,456.25,568.75,493.75,568.75,475.0,600.0,245 -triangle,493.75,568.75,475.0,600.0,512.5,600.0,246 -triangle,493.75,568.75,531.25,568.75,512.5,600.0,247 -triangle,531.25,568.75,512.5,600.0,550.0,600.0,248 -triangle,531.25,568.75,568.75,568.75,550.0,600.0,249 -triangle,568.75,568.75,550.0,600.0,587.5,600.0,250 -triangle,568.75,568.75,606.25,568.75,587.5,600.0,251 -triangle,606.25,568.75,587.5,600.0,625.0,600.0,252 -triangle,606.25,568.75,643.75,568.75,625.0,600.0,253 -triangle,643.75,568.75,625.0,600.0,662.5,600.0,254 -triangle,643.75,568.75,681.25,568.75,662.5,600.0,255 -triangle,681.25,568.75,662.5,600.0,700.0,600.0,256 +triangle,587.5,412.5,568.75,443.75,606.25,443.75,121 \ No newline at end of file diff --git a/color.py b/color.py index 8a32df7..4602ea6 100644 --- a/color.py +++ b/color.py @@ -1,20 +1,22 @@ """ Color -Color class that allows you to initialize a color in any of HSV, RGB, Hex, HSI color spaces. Once initialized, the corresponding RGBW values are calculated and you may modify the object in RGB or HSV color spaces( ie: by re-setting any component of HSV or RGB (ie, just resetting the R value) and all RGB/HSV/RGBW values will be recalculated. +Color class that allows you to initialize a color in any of HSV, HSL, HSI, RGB, Hex color spaces. Once initialized, the corresponding RGBW values are calculated and you may modify the object in RGB or HSV color spaces( ie: by re-setting any component of HSV or RGB (ie, just resetting the R value) and all RGB/HSV/RGBW values will be recalculated. As of now, you can not work in RGBW directly as we have not written the conversions from RGBW back to one of the standard color spaces. (annoying, but so it goes). The main goal of this class is to translate various color spaces into RGBW for use in RGBW pixels. NOTE! this package will not control 3 channel RGB LEDs properly. -The color representation is maintained in HSV interanlly and translates to RGB (and RGBW, but not interactively). +The color representation is maintained in HSV interanlly and translated to RGB (and RGBW, but only for retrieval). Use whichever is more convenient at the time - RGB for familiarity, HSV to fade colors easily. RGB values range from 0 to 255 -HSV values range from 0.0 to 1.0 +HSV values range from 0.0 to 1.0 *Note the H value has been normalized to range between 0-1 in instead of 0-360 to allow for easier cycling of values. +HSL/HSI values range from 0-360 for H, 0-1 for S/[L|I] - >>> red = RGB(255, 0 ,0) (RGBW = ) - >>> green = HSV(0.33, 1.0, 1.0) (RGBW = ) + >>> red = RGB(255, 0 ,0) (RGBW = 255,0,0,0) + >>> green = HSV(0.33, 1.0, 1.0) (RGBW = 5, 254, 0, 0) + >>> fuschia = RGB(180, 48, 229) (RGBW = 130, 0 , 182, 47) Colors may also be specified as hexadecimal string: @@ -85,7 +87,7 @@ def is_hsv_tuple(hsv): "check that a tuple contains 3 values between 0.0 and 1.0" return len(hsv) == 3 and all([(0.0 <= t <= 1.0) for t in hsv]) -def is_hsi_tuple(hsi): +def is_hsi_hsl_tuple(hsi): ret = True if len(hsi) != 3: ret = False @@ -297,14 +299,17 @@ def rgb2hsi(r, g, b): return(hue,abs(saturation),intensity) +def RGBW(r, g, b, w): + "Create RGBW color" + raise Exception("Gotcha! We can't yet reverse calculate RGBW back to any other color spaces.... work in one of the other spaces and get your RGBW values back from Color.rgbw. Sorry.") + def HSI(h,s,i): "Create new HSI color" t = (h,s,i) - assert is_hsi_tuple(t) + assert is_hsi_hsl_tuple(t) return RGB( hsi2rgb(h,s,i) ) - def RGB(r,g,b): "Create a new RGB color" t = (r,g,b) @@ -317,8 +322,11 @@ def HSV(h,s,v): def HSL(h,s,l): "Create new HSL color" - (h,s,v) = hsl2hsv(h,s,l) - return Color(h,s,v) + t = (h,s,l) + assert is_hsi_hsl_tuple(t) + (h,s,v) = hsl2hsv(t[0], t[1], t[2]) + print(h,s,v) + return Color((constrain(h/360.0,0.0,1.0),s,v)) def Hex(value): "Create a new Color from a hex string" @@ -362,6 +370,13 @@ def hsv(self): def hex(self): "returns a hexadecimal string" return '#%02x%02x%02x' % self.rgb + + @property + def hsl(self): + "returns HSL tuple" + (h,s,l) = hsv2hsl(self.hsv_t[0], self.hsv_t[1], self.hsv_t[2]) + h = constrain(h*360.0, 0.0,360.0) + return (h,s,l) """ Properties representing individual HSV compnents @@ -460,3 +475,5 @@ def w(self): if __name__=='__main__': import doctest doctest.testmod() + + diff --git a/color1.py b/color1.py deleted file mode 100644 index db8126d..0000000 --- a/color1.py +++ /dev/null @@ -1,340 +0,0 @@ -""" -Color - -Color class that can be used interchangably as RGB or HSV org RGBW or HEX with -seamless translation. Use whichever is more convenient at the -time - RGB for familiarity, HSV to fade colors easily, RGBW as the new hotness - -The Color object takes rgb, hsv, rgbw, hex constructors and represents them all as hsv, doing the appropriate transformations when you wish to pull back any of the color types supported. This meanst the core data was stored as a 3 member hsv tuple. To enable RGBW support, I had to bolt on a 4th member of the tuple, holding the W value. Since all of our code emits colors as RGB(W now), I accepted this bit of dirty business b/c the shows we write will expect tuples of 4 to map properly to our pixes. The RGBW tuples will have the correct W value, even if the 4th value in HSV makes no sense (if you wish to grab the valid hsv tuple, you may still do so with tuple()[0:3] - -So, when constructing Color objects, beside the original constructor signature, I've added 2 optional fields: -w=int (0 by default) -recalculate_w = boolean (True by default) - -w is set to zero by default, but will be calculated for the RGB values generated during the Color object construction. UNLESS you set recalculate_w=False, in which case it will be set to zero and remain zero even if you update the Color object - -This is the cool part of the Color object, once created, you have RGB & HSV values available to you interchangibly. If you change Color.r = 255, the corresponding Color.hsv will aslo change. If you have set recalculate_w == True, then when you reset any color value after object creation, the new appropriate RGBW w value will be calc'd and set. If set to False, it will remain as you set it. If you directly change the w value, it will not trigger recalculation. You may also at any time use Color.recalculate_w(True/False) to globally turn on/off w recalculation. - -So how does this all work with our models? Our original models expected a 3 member tuple of (r,g,b) per LED. Now, all LEDs expect a 4 member tuple(r,g,b,w). So, all of the existing shows that use RGB/HEX/HSV native color objects will send the correct (r,g,b,w) tuple now. - -I DID NOT make the choice to make this module support 3 chanel LEDs at this time. - - -RGB values range from 0 to 255 -HSV values range from 0.0 to 1.0 -RGBW values range from 0 to 255 - - >>> red = RGB(255, 0 ,0) - >>> green = HSV(0.33, 1.0, 1.0) - >>> ref = RGBW(255,0,0,85) *More on how W is dealt with latter - -Colors may also be specified as hexadecimal string: - - >>> blue = Hex('#0000ff') - -All three RGBW, RGB and HSV components are available as attributes -and may be set. - - >>> red.r - 255 - - >>> red.g = 128 - >>> red.rgb - (255, 128, 0) - - >>> red.hsv - (0.08366013071895424, 1.0, 1.0) - -These objects are mutable, so you may want to make a -copy before changing a Color that may be shared - - >>> red = RGB(255,0,0) - >>> purple = red.copy() - >>> purple.b = 255 - >>> red.rgb - (255, 0, 0) - >>> purple.rgb - (255, 0, 255) - -Brightness can be adjusted by setting the 'v' property, even -when you're working in RGB. - -For example: to gradually dim a color -(ranges from 0.0 to 1.0) - - >>> col = RGB(0,255,0) - >>> while col.v > 0: - ... print col.rgb - ... col.v -= 0.1 - ... - (0, 255, 0, 85) - (0, 229, 0, 76) - (0, 204, 0, 68) - (0, 178, 0, 59) - (0, 153, 0, 51) - (0, 127, 0, 42) - (0, 102, 0, 34) - (0, 76, 0, 25) - (0, 51, 0, 17) - (0, 25, 0, 8) -NOTE the W value is also calculated as it is expected to be used. - - -RGBW Handling -To keep the core functionality of this module, and include support for RGBW, I increased the expected tuple from 3 to 4 in length, with the last member of the tuple being the W value (in RGB space). - -Since the core of this module uses HSV as the reference point (THANKS GREG!), I had to do a LOT of hoop jumping to make this all work. - -Basically, you can instantiate any of the non-RGBW types as usual and they will automatically calculate the appropriate W value. This recalculation can get tricky, but more on that in a moment. - -So, to set red: - r = = RGB(255,0,0) - ...the w value will be caluclated and appended - print.rgb - (255, 0, 0, 85) - -""" -import colorsys -from copy import deepcopy - -__all__=['RGB', 'HSV', 'Hex', 'Color', 'RGBW'] - -def saturation(rgb): - low = float(min(rgb.r, rgb.g, rgb.b)) - high = float(max(rgb.r, rgb.g, rgb.b)) - ret = 0 - if low > 0 and high > 0: - ret = round(100.0*((high-low)/high)) - return ret - -def getWhiteColor(rgb): - ret = 0 - try: - ret = int(round((255.0-saturation(rgb)) / 255.0 * (float(rgb.r) + float(rgb.b) + float(rgb.g))/3.0)) - except: - print("Error", rgb) - return ret - -def clamp(val, min_value, max_value): - "Restrict a value between a minimum and a maximum value" - return max(min(val, max_value), min_value) - -def is_hsv_tuple(hsv): - "check that a tuple contains 3 values between 0.0 and 1.0" - return len(hsv) == 4 and all([(0.0 <= t <= 1.0) for t in hsv[0:3]]) - -def is_rgb_tuple(rgb): - "check that a tuple contains 3 values between 0 and 255" - return len(rgb) == 4 and all([(0 <= t <= 255) for t in rgb]) - -def is_rgbw_tuple(rgbw): - "check for valid RGBW tuple" - return len(rgbw) == 4 and all([(0 <= t <= 255) for t in rgbw]) - -def rgb_to_hsv(rgb): - "convert a rgb[0-255] tuple to hsv[0.0-1.0]" - f = float(255) - ret = list(colorsys.rgb_to_hsv(rgb[0]/f, rgb[1]/f, rgb[2]/f)) - ret.append(rgb[-1]) - return tuple(ret) - -def rgbw_to_hsv(rgbw): - "convert a rgbw[0:3][0-255] tuple to hsv[0.0-1.0], plus w" - f = float(255) - ret = colorsys.rgb_to_hsv(rgbw[0]/f, rgbw[1]/f, rgbw[2]/f) - ret.append(rgbw[-1]) - return tuple(ret) - -def hsv_to_rgbw(hsv): - assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) - _rgb = colorsys.hsv_to_rgb(*tuple(hsv[0:3])) - r = int(_rgb[0] * 0xff) - g = int(_rgb[1] * 0xff) - b = int(_rgb[2] * 0xff) - return (r, g, b, hsv[-1]) - - -def hsv_to_rgb(hsv): - assert is_hsv_tuple(hsv), "malformed hsv tuple:" + str(hsv) -# from IPython import embed; embed() - _rgb = colorsys.hsv_to_rgb(*tuple(hsv[0:3])) - r = int(_rgb[0] * 0xff) - g = int(_rgb[1] * 0xff) - b = int(_rgb[2] * 0xff) - return (r, g, b, hsv[-1]) - -def RGBW(r,g,b,w, recalculate_w=False): - "Create new RGBW Color" - t = (r, g, b, w) - assert is_rgbw_tuple(t) - return(Color(rgb_to_hsv(t), recalculate_w)) - -def RGB(r,g,b,w=0, recalculate_w=False): - "Create a new RGB color" - t = (r, g, b, w) - assert is_rgb_tuple(t) - return Color(rgb_to_hsv(t), recalculate_w) - -def HSV(h,s,v, w=0.0, recalculate_w=False): - "Create a new HSV color" - return Color((h, s, v, w), recalculate_w) - -def Hex(value, recalculate_w=True): - "Create a new Color from a hex string" - value = value.lstrip('#') - lv = len(value) - rgb_t = (int(value[i:i+int(lv/3)], 16) for i in range(0, lv, int(lv/3))) - r = next(rgb_t) - g = next(rgb_t) - b = next(rgb_t) - return RGB(r, g, b, 0, recalculate_w=False) #JEM -not sure what to do here - -class Color(object): - - def __init__(self, hsv_tuple, recalculate_w=True): - self.recalculate_w = recalculate_w - self._set_hsv(hsv_tuple) - - - def copy(self): - return deepcopy(self) - - def _set_hsv(self, hsv_tuple, preserve_w=False): - assert is_hsv_tuple(hsv_tuple) - self.hsv_t = list(hsv_tuple) - - #I'm trting to solve the problem of the c - if preserve_w is True: - pass - elif self.recalculate_w is True: - new_w = int(getWhiteColor(self)) - l = list(hsv_tuple) - l[-1] = new_w - hsv_tuple = tuple(l) - self.hsv_t = list(hsv_tuple) - else: - pass #all done - - @property - def rgbw(self): - "returns a rgbw[0-255] tuple" - return hsv_to_rgbw(self.hsv_t) - - - @property - def rgb(self): - "returns a rgb[0-255] tuple" - return hsv_to_rgb(self.hsv_t) - - @property - def hsv(self): - "returns a hsv[0.0-1.0] tuple" - return tuple(self.hsv_t) - - @property - def hex(self): - "returns a hexadecimal string" - return '#%02x%02x%02x' % self.rgb - - """ - Properties representing individual HSV compnents - Adjusting 'H' shifts the color around the color wheel - Adjusting 'S' adjusts the saturation of the color - Adjusting 'V' adjusts the brightness/intensity of the color - """ - @property - def h(self): - return self.hsv_t[0] - - @h.setter - def h(self, val): - assert 0.0 <= val <= 1.0 - v = clamp(val, 0.0, 1.0) - self.hsv_t[0] = round(v, 8) - - @property - def s(self): - return self.hsv_t[1] - - @s.setter - def s(self, val): - assert 0.0 <= val <= 1.0 - v = clamp(val, 0.0, 1.0) - self.hsv_t[1] = round(v, 8) - - @property - def v(self): - return self.hsv_t[2] - - @v.setter - def v(self, val): - assert 0.0 <= val <= 1.0 - v = clamp(val, 0.0, 1.0) - new_hsv = self.hsv_t - new_hsv[2] = round(v, 8) - self._set_hsv(new_hsv) - - - """ - Properties representing individual RGB components - """ - @property - def r(self): - return self.rgb[0] - - @r.setter - def r(self, val): - assert 0 <= val <= 255 - r, g, b, w = self.rgb - new = (val, g, b, w) - assert is_rgb_tuple(new) - self._set_hsv(rgb_to_hsv(new)) - - @property - def g(self): - return self.rgb[1] - - @g.setter - def g(self, val): - assert 0 <= val <= 255 - r, g, b, w = self.rgb - new = (r, val, b, w) - assert is_rgb_tuple(new) - self._set_hsv(rgb_to_hsv(new)) - - @property - def b(self): - return self.rgb[2] - - @b.setter - def b(self, val): - assert 0 <= val <= 255 - r, g, b, w = self.rgb - new = (r, g, val, w) - assert is_rgb_tuple(new) - self._set_hsv(rgb_to_hsv(new)) - - @property - def w(self): - return int(self.rgbw[-1]) - - @w.setter - def w(self, val): - assert 0 <= val <= 255 - r, g, b, w = self.rgbw - new = (r, g, b, val) - self._set_hsv(rgb_to_hsv(new), preserve_w=True) - - @property - def recalculate_w(self): - return self._recalculate_w - - @recalculate_w.setter - def recalculate_w(self, val=True): - self._recalculate_w = val - - - -if __name__=='__main__': - import doctest - doctest.testmod() diff --git a/data/pixel_map.json b/data/pixel_map.json index 79f6129..99fe04f 100644 --- a/data/pixel_map.json +++ b/data/pixel_map.json @@ -119,140 +119,5 @@ "118": [1, 469], "119": [1, 473], "120": [1, 477], -"121": [1, 481], -"122": [1, 485], -"123": [1, 489], -"124": [1, 493], -"125": [1, 497], -"126": [1, 501], -"127": [1, 505], -"128": [1, 509], -"129": [2, 1 ], -"130": [2, 5], -"131": [2, 9], -"132": [2, 13], -"133": [2, 17], -"134": [2, 21], -"135": [2, 25], -"136": [2, 29], -"137": [2, 33], -"138": [2, 37], -"139": [2, 41], -"140": [2, 45], -"141": [2, 49], -"142": [2, 53], -"143": [2, 57], -"144": [2, 61], -"145": [2, 65], -"146": [2, 69], -"147": [2, 73], -"148": [2, 77], -"149": [2, 81], -"150": [2, 85], -"151": [2, 89], -"152": [2, 93], -"153": [2, 97], -"154": [2, 101], -"155": [2, 105], -"156": [2, 109], -"157": [2, 113], -"158": [2, 117], -"159": [2, 121], -"160": [2, 125], -"161": [2, 129], -"162": [2, 133], -"163": [2, 137], -"164": [2, 141], -"165": [2, 145], -"166": [2, 149], -"167": [2, 153], -"168": [2, 157], -"169": [2, 161], -"170": [2, 165], -"171": [2, 169], -"172": [2, 173], -"173": [2, 177], -"174": [2, 181], -"175": [2, 185], -"176": [2, 189], -"177": [2, 193], -"178": [2, 197], -"179": [2, 201], -"180": [2, 205], -"181": [2, 209], -"182": [2, 213], -"183": [2, 217], -"184": [2, 221], -"185": [2, 225], -"186": [2, 229], -"187": [2, 233], -"188": [2, 237], -"189": [2, 241], -"190": [2, 245], -"191": [2, 249], -"192": [2, 253], -"193": [2, 257], -"194": [2, 261], -"195": [2, 265], -"196": [2, 269], -"197": [2, 273], -"198": [2, 277], -"199": [2, 281], -"200": [2, 285], -"201": [2, 289], -"202": [2, 293], -"203": [2, 297], -"204": [2, 301], -"205": [2, 305], -"206": [2, 309], -"207": [2, 313], -"208": [2, 317], -"209": [2, 321], -"210": [2, 325], -"211": [2, 329], -"212": [2, 333], -"213": [2, 337], -"214": [2, 341], -"215": [2, 345], -"216": [2, 349], -"217": [2, 353], -"218": [2, 357], -"219": [2, 361], -"220": [2, 365], -"221": [2, 369], -"222": [2, 373], -"223": [2, 377], -"224": [2, 381], -"225": [2, 385], -"226": [2, 389], -"227": [2, 393], -"228": [2, 397], -"229": [2, 401], -"230": [2, 405], -"231": [2, 409], -"232": [2, 413], -"233": [2, 417], -"234": [2, 421], -"235": [2, 425], -"236": [2, 429], -"237": [2, 433], -"238": [2, 437], -"239": [2, 441], -"240": [2, 445], -"241": [2, 449], -"242": [2, 453], -"243": [2, 457], -"244": [2, 461], -"245": [2, 465], -"246": [2, 469], -"247": [2, 473], -"248": [2, 477], -"249": [2, 481], -"250": [2, 485], -"251": [2, 489], -"252": [2, 493], -"253": [2, 497], -"254": [2, 501], -"255": [2, 505], -"256": [2, 509] +"121": [1, 481] } diff --git a/go_tri.py b/go_tri.py index 54139de..f8c78b7 100644 --- a/go_tri.py +++ b/go_tri.py @@ -324,7 +324,7 @@ def go_web(self): logger.info(f'Using TriSimulator at {sim_host}:{sim_port}') model = SimulatorModel(sim_host, port=sim_port, model_json='./data/pixel_map.json') - triangle_grid = triangle_grid.make_tri(model, 5) + triangle_grid = triangle_grid.make_tri(model, 11) #Our panels will only have 11 rows else: logger.info("Starting SACN") from model.sacn_model import sACN diff --git a/model/ola_model.py b/model/ola_model.py index fed3f39..ac3149d 100644 --- a/model/ola_model.py +++ b/model/ola_model.py @@ -46,8 +46,8 @@ def set_pixel(self, pixel, color, cellid=None): ux = self.PIXEL_MAP[pixel][0] ix = self.PIXEL_MAP[pixel][1] - 1 # dmx is 1-based, python lists are 0-based - self.leds[ux][ix] = color.g - self.leds[ux][ix+1] = color.r + self.leds[ux][ix] = color.r + self.leds[ux][ix+1] = color.g self.leds[ux][ix+2] = color.b self.leds[ux][ix+3] = color.w else: diff --git a/model/sacn_model.py b/model/sacn_model.py index 5529caf..fcce1b9 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -51,8 +51,8 @@ def set_pixel(self, pixel, color, cellid=None): ux = self.PIXEL_MAP[pixel][0] ix = self.PIXEL_MAP[pixel][1] - 1 # dmx is 1-based, python lists are 0-based - self.leds[ux][ix] = color.g - self.leds[ux][ix+1] = color.r + self.leds[ux][ix] = color.r + self.leds[ux][ix+1] = color.g self.leds[ux][ix+2] = color.b self.leds[ux][ix+3] = color.w else: diff --git a/model/simulator.py b/model/simulator.py index bf403fb..af79284 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -55,8 +55,10 @@ def set_pixel(self, pixel, color, cellid): def go(self): for num in self.dirty: - color = self.dirty[num] - msg = f'b {num} {color.r},{color.g},{color.b}\n' + r = self.dirty[num].rgb[0] + g = self.dirty[num].rgb[1] + b = self.dirty[num].rgb[2] + msg = f'b {num} {r},{g},{b}\n' logger.debug(msg) self.sock.send(msg.encode()) diff --git a/shows/cycle_hsv.py b/shows/cycle_hsv.py index 03a0e9c..a2413aa 100644 --- a/shows/cycle_hsv.py +++ b/shows/cycle_hsv.py @@ -1,6 +1,5 @@ from .showbase import ShowBase from color import HSV as hsv -from color1 import HSV as hsv1 import random as rnd import time @@ -20,7 +19,6 @@ def next_frame(self): while True: self.ca= hsv(0.0,0.0,0.0) - self.cb= hsv1(0.0,0.0,0.0) while self.ca.v < 1.0: self.ca.v += 0.0008 self.tri_grid.set_all_cells(self.ca) @@ -45,30 +43,5 @@ def next_frame(self): self.tri_grid.clear() time.sleep(3) - while self.cb.v < 1.0: - self.cb.v += 0.0008 - self.tri_grid.set_all_cells(self.cb) - self.tri_grid.go() - time.sleep(.01) - print('CB', self.cb.hsv) - - while self.cb.s < 1.0: - self.cb.s += 0.0008 - self.tri_grid.set_all_cells(self.cb) - self.tri_grid.go() - time.sleep(.01) - print('CB', self.cb.hsv) - - while self.cb.h < 1.0: - self.cb.h += 0.0008 - self.tri_grid.set_all_cells(self.cb) - self.tri_grid.go() - time.sleep(.01) - print('CB', self.cb.hsv) - - self.tri_grid.clear() - self.tri_grid.set_all_cells(hsv(1.0,1.0,1.0)) - time.sleep(3) - self.tri_grid.clear() yield self.frame_delay diff --git a/shows/one_by_one.py b/shows/one_by_one.py index 5100560..4a281cd 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -3,7 +3,7 @@ class OneByOne(ShowBase): - def __init__(self, tri_grid, frame_delay=1.5): + def __init__(self, tri_grid, frame_delay=0.25): self.tri_grid = tri_grid self.frame_delay = frame_delay diff --git a/shows/up_down.py b/shows/up_down.py index 0a8f07e..58f1a1c 100644 --- a/shows/up_down.py +++ b/shows/up_down.py @@ -4,29 +4,29 @@ class UpDown(ShowBase): def __init__(self, tri_grid, frame_delay=2): - self.cells = tri_grid + self.tri_grid = tri_grid self.frame_delay = frame_delay def next_frame(self): a = "up" while True: - self.cells.clear() + self.tri_grid.clear() if a == "up": print('up') - for i in self.cells.get_up_cells(): + for i in self.tri_grid.get_up_cells(): print("Up", i.get_id()) - self.cells.set_cell_by_cellid(i.get_id(), RGB( 255, 255, 255)) + self.tri_grid.set_cell_by_cellid(i.get_id(), RGB( 0, 255, 255)) else: print('down') - for i in self.cells.get_down_cells(): + for i in self.tri_grid.get_down_cells(): print("down", i.get_id()) - self.cells.set_cell_by_cellid(i.get_id(), RGB( 255, 0, 0)) + self.tri_grid.set_cell_by_cellid(i.get_id(), RGB( 255, 0, 200)) if a == "up": a = "down" else: a = "up" - + self.tri_grid.go() yield self.frame_delay From f5b2d6fa9bf9efca4fc913422586302dc755accb Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Mon, 22 Jul 2019 13:18:56 -0700 Subject: [PATCH 14/60] Import simplification --- go_tri.py | 3 +-- model/__init__.py | 6 ++++++ tests/test_web.py | 2 +- web/__init__.py | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 model/__init__.py create mode 100644 web/__init__.py diff --git a/go_tri.py b/go_tri.py index f8c78b7..c909005 100644 --- a/go_tri.py +++ b/go_tri.py @@ -7,7 +7,7 @@ import threading import cherrypy -from model.simulator import SimulatorModel +from model import SimulatorModel, sACN import osc_serve import triangle_grid import shows @@ -327,7 +327,6 @@ def go_web(self): triangle_grid = triangle_grid.make_tri(model, 11) #Our panels will only have 11 rows else: logger.info("Starting SACN") - from model.sacn_model import sACN model = sACN(max_dmx=800, model_json="./data/pixel_map.json") triangle_grid = triangle_grid.make_tri(model, 3) diff --git a/model/__init__.py b/model/__init__.py new file mode 100644 index 0000000..ef8a8f7 --- /dev/null +++ b/model/__init__.py @@ -0,0 +1,6 @@ +# These imports include submodules under the `model` namespace (e.g. model.SimulatorModel is available). +from .modelbase import ModelBase +from .mirror import MirrorModel +from .ola_model import OLAModel +from .sacn_model import sACN +from .simulator import SimulatorModel diff --git a/tests/test_web.py b/tests/test_web.py index 7297f10..67041e4 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -3,7 +3,7 @@ from jinja2 import escape from cherrypy.test import helper -from web.web import TriangleWeb +from web import TriangleWeb command_queue = queue.LifoQueue() diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..7011911 --- /dev/null +++ b/web/__init__.py @@ -0,0 +1 @@ +from .web import TriangleWeb From 5dc8a7eec9748356f4fe0c9c7e32cbf46e1ae658 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Sun, 21 Jul 2019 13:12:03 -0700 Subject: [PATCH 15/60] __init__ and import changes to make tests easier --- tests/test_triangle_grid.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/test_triangle_grid.py diff --git a/tests/test_triangle_grid.py b/tests/test_triangle_grid.py new file mode 100644 index 0000000..e69de29 From eaf287ca53c6faf87676f750b3253581d59688cc Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Sun, 21 Jul 2019 13:13:49 -0700 Subject: [PATCH 16/60] Whitespace and formatting only --- triangle_grid.py | 117 ++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/triangle_grid.py b/triangle_grid.py index 9bdafc7..63ffe5f 100644 --- a/triangle_grid.py +++ b/triangle_grid.py @@ -29,35 +29,33 @@ def __init__(self, model, n_rows): self._n_rows = n_rows self._len_of_last_row = calc_last_row_len(n_rows) - self._triangle_grid = [[None for i in range(self._len_of_last_row)] for j in range(self._n_rows)] #B/C this way of building a 2d array give you buggy garbage where setting [0][0] assigns all rows at posn [0] the value you are seetting[[None]*self.len_of_last_row]*self.n_rows + # B/C this way of building a 2d array give you buggy garbage where setting [0][0] assigns all rows at posn [0] + # the value you are setting[[None]*self.len_of_last_row]*self.n_rows + self._triangle_grid = [[None for i in range(self._len_of_last_row)] for j in range(self._n_rows)] self._build_triangle_array_and_grid(n_rows) - def __str__(self): - return str("ME") - def __repr__(self): - print("what is this for?") + """String representation of object when printed""" + return f'{__class__.__name__} (n_rows={self._n_rows}, model={self._model})' - #OMG, this is such a nighmare... but it works... + # OMG, this is such a nightmare... but it works... def _build_triangle_array_and_grid(self, n_rows): self._cells = [] if n_rows < 1: - raise Exception('row num must be 1+', n_rows) + raise ValueError('row num must be 1+', n_rows) if n_rows > 16: - raise Exception('row num must be <15') - if len(self._cells) > 0: - raise Exception('You have already built a triangle grid, clear this one first') + raise ValueError('row num must be <15', n_rows) - #By 'Y' I mean column..... sorry - grid_y_start_pos = (int(self._len_of_last_row)-1)//2 + # By 'Y' I mean column..... sorry + grid_y_start_pos = (self._len_of_last_row-1)//2 grid_y_curr_pos = grid_y_start_pos row_len = 0 end_cell_id = 0 cell_id = 0 end_cell_id = 0 - top_pixel = 73 #pointy top - btm_pixel = 67 #flat btm + top_pixel = 73 # pointy top + btm_pixel = 67 # flat btm curr_row = 0 while curr_row < n_rows: # print "Curr Row:", curr_row @@ -106,24 +104,24 @@ def _build_triangle_array_and_grid(self, n_rows): is_btm_edge = False cells_added += 1 - tco = None #triagnel cell object + tco = None # triangle cell object if up_down == 'up': - tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [top_pixel, top_pixel-1, top_pixel-2, top_pixel-3, top_pixel-4, top_pixel-5]) + tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [top_pixel, top_pixel-1, top_pixel-2, top_pixel-3, top_pixel-4, top_pixel-5]) self._cells.append(tco) top_pixel -= 6 up_down = 'down' else: - if curr_row >1: - tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [btm_pixel, btm_pixel+1, btm_pixel+2, btm_pixel+3, btm_pixel+4, btm_pixel+5]) + if curr_row > 1: + tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [btm_pixel, btm_pixel+1, btm_pixel+2, btm_pixel+3, btm_pixel+4, btm_pixel+5]) self._cells.append(tco) btm_pixel += 6 else: - tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [btm_pixel, btm_pixel-1, btm_pixel-2, btm_pixel-3, btm_pixel-4, btm_pixel-5]) + tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [btm_pixel, btm_pixel-1, btm_pixel-2, btm_pixel-3, btm_pixel-4, btm_pixel-5]) self._cells.append(tco) btm_pixel -= 6 up_down = 'up' - #Add Cell To Triangle Grid! + # Add Cell To Triangle Grid! tco.add_row_col(curr_row, grid_y_curr_pos) self._triangle_grid[curr_row][grid_y_curr_pos] = tco grid_y_curr_pos += 1 @@ -134,7 +132,7 @@ def _build_triangle_array_and_grid(self, n_rows): row_pos += 1 - cell_id +=1 + cell_id += 1 if cell_id == end_cell_id: up_down = 'up' @@ -143,7 +141,7 @@ def _build_triangle_array_and_grid(self, n_rows): grid_y_curr_pos = grid_y_start_pos - curr_row-1 curr_row += 1 - btm_pixel -= 2 #(curr_row*6)+7 + btm_pixel -= 2 # (curr_row*6)+7 if curr_row > 1: btm_pixel -= (curr_row*6)+9+9 @@ -153,6 +151,7 @@ def go(self): self._model.go() def clear(self): + """Clears grid by setting all cells to 0.""" self.set_all_cells(RGB( 0, 0, 0)) self.go() @@ -163,7 +162,7 @@ def get_triangle_grid(self): return self._triangle_grid def get_cell_by_grid_coords(self, rown, coln): - "RETURNS cell object or None if no cell is mapped to the coord" + """Returns cell object, or None if no cell is mapped to the coord""" if rown > self._n_rows: return None @@ -190,7 +189,7 @@ def set_cell_by_cellid(self, cell, color): self._model.set_pixel(pixel, color, cell) def get_all_cells(self): - "Return the list of valid cell IDs" + """Return the list of valid cell IDs""" return self._cells def set_cells_by_cellid(self, cells, color): @@ -212,16 +211,10 @@ def set_all_cells(self, color): self._model.set_pixel(pixel, color, cell.get_id()) def set_pixel(self, pixel, color, cellid): - self._model.set_pixel(pixel, color, cellid) ###Have to pass the cellid through b/c the simulator does not understand pixels + # Have to pass the cellid through b/c the simulator does not understand pixels + self._model.set_pixel(pixel, color, cellid) - def clear(self): - self.set_all_cells(RGB( 0, 0, 0)) - self.go() - - def go(self): - self._model.go() - - # convenience methods for grabbing useful parts of the triangle grid + # Convenience methods for grabbing useful parts of the triangle grid. def get_left_side_cells(self): cells = [] @@ -229,7 +222,7 @@ def get_left_side_cells(self): if i.is_left_edge(): cells.append(i) return cells - + def get_right_side_cells(self): cells = [] for i in self._cells: @@ -258,23 +251,19 @@ def get_down_cells(self): cells.append(i) return cells - def is_edge(self, cell): - if cell._l_edge: - return True - elif cell._r_edge: - return True - elif cell._btm_side: - return True - else: - return False + @staticmethod + def is_edge(cell): + return cell._l_edge or cell._r_edge or cell._btm_side # Triangle Grid Helper Functions def get_edge_neighbors_by_coord(self, row, col): - "returned in a tuple of (left neighbor, middle neighbor, right neighbor)" - "Where left is the edge directly to the left of the cell, regardless of up/down orientation" - "Where middle is either the top or bottom neighbor depending where the edge is. the cell knows its up/down orientation" - "Right neighbor is the cell immediately to the right." + """ + Returns a tuple of (left neighbor, middle neighbor, right neighbor) + Left neighbor is the edge directly to the left of the cell, regardless of up/down orientation + Middle neighbor is either the top or bottom neighbor depending where the edge is. the cell knows its up/down orientation + Right neighbor is the cell immediately to the right. + """ cell = self.get_cell_by_grid_coords(row, col) if cell is None: return None @@ -292,12 +281,14 @@ def get_edge_neighbors_by_coord(self, row, col): r = self.get_cell_by_grid_coords(row, col+1) return (l, m, r) - def get_edge_neighbors_by_cell(self, cell): + def get_edge_neighbors_by_cell(self, cell): return self.get_edge_neighbors_by_coord(cell.get_row_num(), cell.get_col_pos()) def get_vertex_neighbors_by_coord(self, row, col): - "Return the neighbors whose vertexes are point to point" - "Uses same logic as edge_neighbors, left, middle, right" + """ + Return the neighbors whose vertexes are point to point + Uses same logic as edge_neighbors, left, middle, right + """ cell = self.get_cell_by_grid_coords(row, col) if cell is None: return None @@ -320,20 +311,21 @@ def get_vertex_neighbors_by_cell(self, cell): return self.get_vertex_neighbors_by_coord(cell.get_row_num(), cell.get_col_pos()) def get_hexagon_from_btm_cell_by_coords(self, row, col): - "Given the coordinates of an up facing cell, return the surrounding cells which will make " - "A hexagon with the specified cell as the base" - "the tuple will begin with the upper left cell, then it's upper neighbor, then its upper right" - "neighbor.... and so on" + """ + Given the coordinates of an up facing cell, return the surrounding cells which will make a hexagon with the + specified cell as the base. The tuple will begin with the upper left cell, then its upper neighbor, then its + upper right neighbor... and so on. + """ cell = self.get_cell_by_grid_coords(row, col) if cell is None: return None else: a = self.get_cell_by_grid_coords(row, col-1) - b =self.get_cell_by_grid_coords(row-1, col-1) - c =self.get_cell_by_grid_coords(row-1, col) - d =self.get_cell_by_grid_coords(row-1, col+1) - e =self.get_cell_by_grid_coords(row, col+1) + b = self.get_cell_by_grid_coords(row-1, col-1) + c = self.get_cell_by_grid_coords(row-1, col) + d = self.get_cell_by_grid_coords(row-1, col+1) + e = self.get_cell_by_grid_coords(row, col+1) return (cell, a, b, c, d, e) def get_hexagon_from_btm_cell_by_cell(self, cell): @@ -341,8 +333,7 @@ def get_hexagon_from_btm_cell_by_cell(self, cell): class TriangleCell(object): - def __init__(self, cell_id, row, up_down, is_top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, pixels=[]): - + def __init__(self, cell_id, row, up_down, is_top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, pixels=[]): self._id = cell_id self._row_n = row self._row_pos = row_pos @@ -351,14 +342,14 @@ def __init__(self, cell_id, row, up_down, is_top, l_corner, r_corner, is_l_edg self._l_edge = True self._r_edge = is_r_edge if cell_id in [1, 3]: - self._r_edge= True + self._r_edge = True self._top_cell = is_top self._btm_right = r_corner self._btm_left = l_corner self._pointy_side = up_down self._btm_side = is_btm_edge self._up_down = up_down - self._pixels = pixels #do we really need a pixel class? + self._pixels = pixels # do we really need a pixel class? def add_row_col(self, r, c): self._grid_row_n = r @@ -367,7 +358,7 @@ def add_row_col(self, r, c): def get_id(self): return self._id - def get_pixels(self,oriented=True): + def get_pixels(self, oriented=True): if oriented: if self._up_down is 'up': return self._pixels From 19960e75b1567b74751cf95056ea57df9a35c07c Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Sun, 21 Jul 2019 13:14:11 -0700 Subject: [PATCH 17/60] Initial test and refactor --- tests/test_triangle_grid.py | 31 +++++++++++ triangle_grid.py | 107 +++++++++++++----------------------- 2 files changed, 70 insertions(+), 68 deletions(-) diff --git a/tests/test_triangle_grid.py b/tests/test_triangle_grid.py index e69de29..51b25fc 100644 --- a/tests/test_triangle_grid.py +++ b/tests/test_triangle_grid.py @@ -0,0 +1,31 @@ +import triangle_grid +from model import ModelBase + + +class FakeModel(ModelBase): + def set_pixel(self, pixel, color, cell_id): + pass + + def go(self): + pass + + +def triangular_number(n): + return (n * (n + 1)) // 2 + + +# TODO: Figure out why cell count is off from expected +def test_builds_triangle(): + for row_count in range(1, 15): + cell_count = triangular_number(row_count) + print(row_count, cell_count) + + triangle = triangle_grid.make_tri(FakeModel(), row_count) + cells = triangle.get_all_cells() + assert len(cells) == cell_count, f'cell count {len(cells)} != expected {cell_count} with rows {row_count}' + + +def test_row_len(): + # (1, 1), (2, 3), (3, 5), (4, 7)... + for (row, length) in enumerate(range(1, 16, 2), start=1): + assert triangle_grid.row_len(row) == length diff --git a/triangle_grid.py b/triangle_grid.py index 63ffe5f..26bc89a 100644 --- a/triangle_grid.py +++ b/triangle_grid.py @@ -5,17 +5,9 @@ def make_tri(model, n_rows): return TriangleGrid(model, n_rows) -def calc_last_row_len(max_row): - row_len = 1 - curr_row = 1 - - while curr_row < max_row: - if max_row == 1: - row_len = 1 - else: - row_len += 2 - curr_row += 1 - return row_len +def row_len(row): + """Returns count of elements in nth row of equilateral triangle.""" + return row * 2 - 1 ## ## Triangle Grid class to represent one strip @@ -27,7 +19,7 @@ class TriangleGrid(object): def __init__(self, model, n_rows): self._model = model self._n_rows = n_rows - self._len_of_last_row = calc_last_row_len(n_rows) + self._len_of_last_row = row_len(n_rows) # B/C this way of building a 2d array give you buggy garbage where setting [0][0] assigns all rows at posn [0] # the value you are setting[[None]*self.len_of_last_row]*self.n_rows @@ -217,39 +209,19 @@ def set_pixel(self, pixel, color, cellid): # Convenience methods for grabbing useful parts of the triangle grid. def get_left_side_cells(self): - cells = [] - for i in self._cells: - if i.is_left_edge(): - cells.append(i) - return cells + return [cell for cell in self._cells if cell.is_left_edge()] def get_right_side_cells(self): - cells = [] - for i in self._cells: - if i.is_right_edge(): - cells.append(i) - return cells + return [cell for cell in self._cells if cell.is_right_edge()] def get_bottom_side_cells(self): - cells = [] - for i in self._cells: - if i.is_bottom_edge(): - cells.append(i) - return cells + return [cell for cell in self._cells if cell.is_bottom_edge()] def get_up_cells(self): - cells = [] - for i in self._cells: - if i.is_up(): - cells.append(i) - return cells + return [cell for cell in self._cells if cell.is_up()] def get_down_cells(self): - cells = [] - for i in self._cells: - if i.is_down(): - cells.append(i) - return cells + return [cell for cell in self._cells if cell.is_down()] @staticmethod def is_edge(cell): @@ -267,18 +239,18 @@ def get_edge_neighbors_by_coord(self, row, col): cell = self.get_cell_by_grid_coords(row, col) if cell is None: return None + + l = None + m = None + r = None + if cell.is_up(): + l = self.get_cell_by_grid_coords(row, col-1) + m = self.get_cell_by_grid_coords(row+1, col) + r = self.get_cell_by_grid_coords(row, col+1) else: - l = None - m = None - r = None - if cell.is_up(): - l = self.get_cell_by_grid_coords(row, col-1) - m = self.get_cell_by_grid_coords(row+1, col) - r = self.get_cell_by_grid_coords(row, col+1) - else: - l = self.get_cell_by_grid_coords(row, col-1) - m = self.get_cell_by_grid_coords(row-1, col) - r = self.get_cell_by_grid_coords(row, col+1) + l = self.get_cell_by_grid_coords(row, col-1) + m = self.get_cell_by_grid_coords(row-1, col) + r = self.get_cell_by_grid_coords(row, col+1) return (l, m, r) def get_edge_neighbors_by_cell(self, cell): @@ -292,20 +264,19 @@ def get_vertex_neighbors_by_coord(self, row, col): cell = self.get_cell_by_grid_coords(row, col) if cell is None: return None - else: - l = None - m = None - r = None - if cell.is_up(): - l = self.get_cell_by_grid_coords(row+1, col-1) - m = self.get_cell_by_grid_coords(row-1, col) - r = self.get_cell_by_grid_coords(row+1, col+1) - else: - l = self.get_cell_by_grid_coords(row-1, col-1) - m = self.get_cell_by_grid_coords(row+1, col) - r = self.get_cell_by_grid_coords(row-1, col+1) - return (l, m, r) + l = None + m = None + r = None + if cell.is_up(): + l = self.get_cell_by_grid_coords(row+1, col-1) + m = self.get_cell_by_grid_coords(row-1, col) + r = self.get_cell_by_grid_coords(row+1, col+1) + else: + l = self.get_cell_by_grid_coords(row-1, col-1) + m = self.get_cell_by_grid_coords(row+1, col) + r = self.get_cell_by_grid_coords(row-1, col+1) + return (l, m, r) def get_vertex_neighbors_by_cell(self, cell): return self.get_vertex_neighbors_by_coord(cell.get_row_num(), cell.get_col_pos()) @@ -320,13 +291,13 @@ def get_hexagon_from_btm_cell_by_coords(self, row, col): cell = self.get_cell_by_grid_coords(row, col) if cell is None: return None - else: - a = self.get_cell_by_grid_coords(row, col-1) - b = self.get_cell_by_grid_coords(row-1, col-1) - c = self.get_cell_by_grid_coords(row-1, col) - d = self.get_cell_by_grid_coords(row-1, col+1) - e = self.get_cell_by_grid_coords(row, col+1) - return (cell, a, b, c, d, e) + + a = self.get_cell_by_grid_coords(row, col-1) + b = self.get_cell_by_grid_coords(row-1, col-1) + c = self.get_cell_by_grid_coords(row-1, col) + d = self.get_cell_by_grid_coords(row-1, col+1) + e = self.get_cell_by_grid_coords(row, col+1) + return (cell, a, b, c, d, e) def get_hexagon_from_btm_cell_by_cell(self, cell): return self.get_hexagon_from_btm_cell_by_coords(cell.get_row_num(), cell.get_col_pos()) From 006a512bda325d22862d6f50f1b082e5646d08eb Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Tue, 23 Jul 2019 00:56:45 -0700 Subject: [PATCH 18/60] Bunch of changes, still need mapping of cell to pixel in models --- go_tri.py | 4 +- model/mirror.py | 22 +- model/modelbase.py | 12 +- model/sacn_model.py | 13 +- model/simulator.py | 16 +- shows/left_to_right.py | 2 +- shows/left_to_right_and_back.py | 8 +- shows/one_by_one.py | 4 +- shows/random_cells.py | 10 +- shows/strobe.py | 13 +- shows/up_down.py | 12 +- tests/test_triangle_grid.py | 63 +++- triangle_grid.py | 497 +++++++++++--------------------- 13 files changed, 296 insertions(+), 380 deletions(-) diff --git a/go_tri.py b/go_tri.py index c909005..581e7d2 100644 --- a/go_tri.py +++ b/go_tri.py @@ -324,12 +324,12 @@ def go_web(self): logger.info(f'Using TriSimulator at {sim_host}:{sim_port}') model = SimulatorModel(sim_host, port=sim_port, model_json='./data/pixel_map.json') - triangle_grid = triangle_grid.make_tri(model, 11) #Our panels will only have 11 rows + triangle_grid = triangle_grid.make_triangle(model, 11) # Our panels will only have 11 rows else: logger.info("Starting SACN") model = sACN(max_dmx=800, model_json="./data/pixel_map.json") - triangle_grid = triangle_grid.make_tri(model, 3) + triangle_grid = triangle_grid.make_triangle(model, 3) app = TriangleServer(triangle_grid, args) try: diff --git a/model/mirror.py b/model/mirror.py index 43ede90..c5ae029 100644 --- a/model/mirror.py +++ b/model/mirror.py @@ -3,7 +3,21 @@ Completely agnostic as to what the cell ids look like, they are just passed through to the underlying model. """ -from .modelbase import ModelBase +from itertools import zip_longest + +from typing import Iterator, List, Type + +from .modelbase import ModelBase, PixelBase + + +class MirrorPixel(PixelBase): + def __init__(self, pixel_generators: List[Iterator[Type[PixelBase]]]): + self.generator = zip_longest(pixel_generators) + + def set_color(self, color): + pixels = next(self.generator) + for pixel in pixels: + pixel.set_color(color) class MirrorModel(ModelBase): @@ -16,10 +30,8 @@ def __init__(self, *models): def add_model(self, model): self.models.append(model) - # Model basics - def set_pixel(self, pixel, color, cellid=None): - for m in self.models: - m.set_pixel(pixel, color, cellid) + def get_pixels(self, cell_id): + return [model.get_pixels(cell_id) for model in self.models] def go(self): for m in self.models: diff --git a/model/modelbase.py b/model/modelbase.py index 47d59d6..4c20380 100644 --- a/model/modelbase.py +++ b/model/modelbase.py @@ -1,12 +1,20 @@ from abc import ABC, abstractmethod +from typing import Iterator, Type + + +# TODO: Maybe just use a function instead of a class. +class PixelBase(ABC): + @abstractmethod + def set_color(self, color): + """Set the color for a pixel.""" class ModelBase(ABC): """Abstract base class for simulators.""" @abstractmethod - def set_pixel(self, pixel, color, cell_id): - """Set the color for a pixel.""" + def get_pixels(self, cell_id) -> Iterator[Type[PixelBase]]: + """Returns iterator for Pixels in a Cell""" @abstractmethod def go(self): diff --git a/model/sacn_model.py b/model/sacn_model.py index fcce1b9..511437d 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -6,8 +6,9 @@ """ import json import sacn +from typing import Iterator, Type -from .modelbase import ModelBase +from .modelbase import ModelBase, PixelBase class sACN(ModelBase): @@ -45,7 +46,11 @@ def _map_leds(self, f): self.sender[universe].multicast = True self.leds[universe] = [0] * 512 - # Model basics + def get_pixels(self, cell_id) -> Iterator[Type[PixelBase]]: + # TODO: Need to map cell_id to series of pixels + return None + + # TODO: Delete this def set_pixel(self, pixel, color, cellid=None): if pixel in self.PIXEL_MAP: ux = self.PIXEL_MAP[pixel][0] @@ -58,10 +63,6 @@ def set_pixel(self, pixel, color, cellid=None): else: print(f'WARNING: {pixel} not in pixel ID MAP') - def set_pixels(self, pixels, color): - for pixel in pixels: - self.set_pixel(pixel, color) - def go(self): for ux in self.leds: self.sender[ux].dmx_data = self.leds[ux] diff --git a/model/simulator.py b/model/simulator.py index af79284..c953548 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -6,7 +6,9 @@ import logging import socket import json +from typing import Iterator, Type +from modelbase import PixelBase from .modelbase import ModelBase SIM_DEFAULT = (188, 210, 229) # BCD2E5, "off" color for simulator @@ -38,6 +40,17 @@ def _map_leds(self, f): self.CELL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) # Model basics + def get_pixels(self, cell_id) -> Iterator[Type[PixelBase]]: + class Pixel(PixelBase): + def __init__(self, setter, cell): + self.setter = setter + self.cell = cell + + def set_color(self, color): + self.setter(self.cell, color) + + return iter([Pixel(self.set_cell, cell_id)]) + def set_cell(self, cell, color): cell += 1 # Simulator cells not 0 based if cell not in self.CELL_MAP: @@ -50,9 +63,6 @@ def set_cell(self, cell, color): # Universe and fixture ID into the key. self.dirty[sim_key] = color - def set_pixel(self, pixel, color, cellid): - self.set_cell(cellid, color) - def go(self): for num in self.dirty: r = self.dirty[num].rgb[0] diff --git a/shows/left_to_right.py b/shows/left_to_right.py index e3631ee..c064477 100644 --- a/shows/left_to_right.py +++ b/shows/left_to_right.py @@ -23,7 +23,7 @@ def next_frame(self): if cell is None: pass else: - self.tri_grid.set_cell_by_cellid(cell.get_id(), RGB( 255, 25, 25)) + self.tri_grid.set_cell_by_id(cell.id, RGB(255, 255, 25)) x += 1 x = 0 y += 1 diff --git a/shows/left_to_right_and_back.py b/shows/left_to_right_and_back.py index 9a551c3..4edf19d 100644 --- a/shows/left_to_right_and_back.py +++ b/shows/left_to_right_and_back.py @@ -29,8 +29,8 @@ def next_frame(self): else: r=255 g = 0 - for pix in range(6): - self.tri_grid.set_pixel(cell.get_pixels()[pix], RGB(r, g, 0), cell.get_id()) + for pixel in self.tri_grid.get_pixels(cell.id): + pixel.set_color(RGB(r, g, 0)) time.sleep(.2) self.tri_grid.go() g += 40 @@ -55,8 +55,8 @@ def next_frame(self): else: g = 255 b = 0 - for pix in range(6): - self.tri_grid.set_pixel(cell.get_pixels()[5-pix], RGB(0, g, b), cell.get_id()) + for pixel in self.tri_grid.get_pixels(cell.id): + pixel.set_color(RGB(0, g, b)) time.sleep(.2) self.tri_grid.go() g -= 40 diff --git a/shows/one_by_one.py b/shows/one_by_one.py index 4a281cd..472d927 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -8,14 +8,14 @@ def __init__(self, tri_grid, frame_delay=0.25): self.frame_delay = frame_delay def next_frame(self): - ncells = len(self.tri_grid.get_cells())-1 + ncells = len(self.tri_grid.cells) - 1 self.tri_grid.clear() cell_n = 0 while True: self.tri_grid.clear() print(cell_n) - self.tri_grid.set_cell_by_cellid(self.tri_grid.get_cells()[cell_n].get_id(), RGB(255, 255, 25)) + self.tri_grid.set_cell_by_id(self.tri_grid.cells[cell_n].id, RGB(255, 255, 25)) if cell_n >= ncells: cell_n = -1 diff --git a/shows/random_cells.py b/shows/random_cells.py index f8d916c..d46ecfc 100644 --- a/shows/random_cells.py +++ b/shows/random_cells.py @@ -4,17 +4,17 @@ class Random(ShowBase): - def __init__(self, tri_grid, frame_delay = 0.1): + def __init__(self, tri_grid, frame_delay=0.1): self.tri_grid = tri_grid self.frame_delay = frame_delay - self.n_cells = len(self.tri_grid.get_cells()) - from IPython import embed; embed() + + self.n_cells = len(self.tri_grid.cells) def next_frame(self): while True: self.tri_grid.clear() - self.tri_grid.set_cell_by_cellid(rnd.randint(1, self.n_cells-2), RGB(200, 255, 25)) - self.tri_grid.set_cell_by_cellid(rnd.randint(1, self.n_cells-2), RGB(200, 10, 25)) + self.tri_grid.set_cell_by_id(rnd.randint(1, self.n_cells - 2), RGB(200, 255, 25)) + self.tri_grid.set_cell_by_id(rnd.randint(1, self.n_cells - 2), RGB(200, 10, 25)) yield self.frame_delay diff --git a/shows/strobe.py b/shows/strobe.py index 6a44e60..ba057ae 100644 --- a/shows/strobe.py +++ b/shows/strobe.py @@ -1,30 +1,29 @@ from .showbase import ShowBase from color import RGB -import random as rnd import time + class Strobe(ShowBase): def __init__(self, tri_grid, frame_delay = 0.002): self.tri_grid = tri_grid self.frame_delay = frame_delay self.n_cells = len(self.tri_grid.get_cells()) - def next_frame(self): while True: self.tri_grid.clear() - self.tri_grid.set_all_cells( RGB(200, 5, 5)) + self.tri_grid.set_all_cells(RGB(200, 5, 5)) self.tri_grid.go() time.sleep(0.02) - self.tri_grid.set_all_cells( RGB(100,100,100)) + self.tri_grid.set_all_cells(RGB(100, 100, 100)) self.tri_grid.go() time.sleep(0.01) - self.tri_grid.set_all_cells( RGB(5, 5, 255)) + self.tri_grid.set_all_cells(RGB(5, 5, 255)) time.sleep(0.02) self.tri_grid.go() - self.tri_grid.set_all_cells( RGB(100,100,100)) + self.tri_grid.set_all_cells(RGB(100, 100, 100)) self.tri_grid.go() time.sleep(0.01) - + yield self.frame_delay diff --git a/shows/up_down.py b/shows/up_down.py index 58f1a1c..df29a36 100644 --- a/shows/up_down.py +++ b/shows/up_down.py @@ -15,14 +15,14 @@ def next_frame(self): if a == "up": print('up') - for i in self.tri_grid.get_up_cells(): - print("Up", i.get_id()) - self.tri_grid.set_cell_by_cellid(i.get_id(), RGB( 0, 255, 255)) + for i in self.tri_grid.cells.up_cells: + print("Up", i.id) + self.tri_grid.cells.set_cell_by_id(i.id, RGB(0, 255, 255)) else: print('down') - for i in self.tri_grid.get_down_cells(): - print("down", i.get_id()) - self.tri_grid.set_cell_by_cellid(i.get_id(), RGB( 255, 0, 200)) + for i in self.tri_grid.cells.down_cells: + print("down", i.id) + self.tri_grid.cells.set_cell_by_id(i.id, RGB(255, 0, 200)) if a == "up": a = "down" diff --git a/tests/test_triangle_grid.py b/tests/test_triangle_grid.py index 51b25fc..aa78ec4 100644 --- a/tests/test_triangle_grid.py +++ b/tests/test_triangle_grid.py @@ -1,31 +1,74 @@ +from typing import Iterator, Type + import triangle_grid from model import ModelBase +from modelbase import PixelBase class FakeModel(ModelBase): - def set_pixel(self, pixel, color, cell_id): + def get_pixels(self, cell_id) -> Iterator[Type[PixelBase]]: pass def go(self): pass -def triangular_number(n): - return (n * (n + 1)) // 2 +def test_coordinate_conversion(): + for curr_id in range(256): + (row, column) = triangle_grid.TriangleGrid.id_to_coordinates(curr_id) + reverse_id = triangle_grid.TriangleGrid.coordinates_to_id(row, column) + assert reverse_id == curr_id -# TODO: Figure out why cell count is off from expected def test_builds_triangle(): for row_count in range(1, 15): - cell_count = triangular_number(row_count) - print(row_count, cell_count) + triangle = triangle_grid.make_triangle(FakeModel(), row_count) + + expected_cell_count = triangle_grid.triangular_number(row_count) + assert triangle.row_count == row_count + assert len(triangle.cells) == expected_cell_count,\ + f'cell count {len(triangle.cells)} != expected {expected_cell_count} with rows {row_count}' + + # Each edge has the same number of elements are the number or total rows. + assert row_count ==\ + len(triangle.bottom_side_cells) == len(triangle.left_side_cells) == len(triangle.right_side_cells) + + +def test_cell_attributes(): + triangle = triangle_grid.make_triangle(FakeModel(), 3) + assert len(triangle.cells) == 9 - triangle = triangle_grid.make_tri(FakeModel(), row_count) - cells = triangle.get_all_cells() - assert len(cells) == cell_count, f'cell count {len(cells)} != expected {cell_count} with rows {row_count}' + top = triangle.get_cell_by_coordinates(0, 0) + assert top.is_top_corner and top.is_left_edge and top.is_right_edge and top.is_up + assert not top.is_bottom_edge + + left_corner = triangle.get_cell_by_coordinates(2, 0) + assert left_corner.is_left_corner and left_corner.is_left_edge and left_corner.is_bottom_edge and left_corner.is_up + assert not left_corner.is_right_edge + + right_corner = triangle.get_cell_by_coordinates(2, 4) + assert right_corner.is_right_corner and right_corner.is_right_edge and right_corner.is_bottom_edge + assert right_corner.is_up + assert not right_corner.is_left_edge + + inner = triangle.get_cell_by_coordinates(1, 1) + assert inner.is_down + assert not inner.is_left_edge and not inner.is_right_edge and not inner.is_bottom_edge def test_row_len(): # (1, 1), (2, 3), (3, 5), (4, 7)... for (row, length) in enumerate(range(1, 16, 2), start=1): - assert triangle_grid.row_len(row) == length + assert triangle_grid.row_length(row) == length + + +def test_triangle_number(): + for (n, number) in [ + (1, 1), + (2, 4), + (3, 9), + (4, 16), + (5, 25), + (6, 36) + ]: + assert triangle_grid.triangular_number(n) == number diff --git a/triangle_grid.py b/triangle_grid.py index 26bc89a..507dfe6 100644 --- a/triangle_grid.py +++ b/triangle_grid.py @@ -1,370 +1,213 @@ +from functools import lru_cache +from typing import NamedTuple + from color import RGB -def make_tri(model, n_rows): - return TriangleGrid(model, n_rows) +@lru_cache(maxsize=128) +def triangular_number(n): + """Returns the number of elements in an equilateral triangle of n rows.""" + # Typically the triangle number is (n(n+1))/2 but our triangle has rows of 1, 3, 5... + return n ** 2 -def row_len(row): +def row_length(n): """Returns count of elements in nth row of equilateral triangle.""" - return row * 2 - 1 - -## -## Triangle Grid class to represent one strip -## + return n * 2 - 1 -# Some reading on grids http://www-cs-students.stanford.edu/~amitp/game-programming/grids/ class TriangleGrid(object): - def __init__(self, model, n_rows): + def __init__(self, model, row_count): + if row_count < 1: + raise ValueError(f'n_rows={row_count} must be positive') + self._model = model - self._n_rows = n_rows - self._len_of_last_row = row_len(n_rows) + self._row_count = row_count + + self._cells = [] + for cell_id in range(0, triangular_number(self.row_count)): + self._cells.append(TriangleCell(id=cell_id, num_rows=self.row_count)) + + @staticmethod + def coordinates_to_id(row, column): + """Converts zero-indexed (row, column) coordinates to zero-indexed ID.""" + if row < 0 or column < 0: + raise ValueError(f'(row={row}, column={column}) must be non-negative') + return triangular_number(row) + column + + @staticmethod + @lru_cache(maxsize=512) + def id_to_coordinates(cell_id): + """Converts zero-indexed ID to zero-indexed (row, column) coordinates.""" + if cell_id < 0: + raise ValueError(f'cell_id={cell_id} must be non-negative') - # B/C this way of building a 2d array give you buggy garbage where setting [0][0] assigns all rows at posn [0] - # the value you are setting[[None]*self.len_of_last_row]*self.n_rows - self._triangle_grid = [[None for i in range(self._len_of_last_row)] for j in range(self._n_rows)] - self._build_triangle_array_and_grid(n_rows) + current_row = 1 + while triangular_number(current_row) <= cell_id: + current_row += 1 + return current_row - 1, cell_id - triangular_number(current_row - 1) def __repr__(self): - """String representation of object when printed""" - return f'{__class__.__name__} (n_rows={self._n_rows}, model={self._model})' + """String representation of object.""" + return f'{__class__.__name__} (n_rows={self.row_count}, model={self._model})' - # OMG, this is such a nightmare... but it works... - def _build_triangle_array_and_grid(self, n_rows): - self._cells = [] - if n_rows < 1: - raise ValueError('row num must be 1+', n_rows) - if n_rows > 16: - raise ValueError('row num must be <15', n_rows) - - # By 'Y' I mean column..... sorry - grid_y_start_pos = (self._len_of_last_row-1)//2 - grid_y_curr_pos = grid_y_start_pos - - row_len = 0 - end_cell_id = 0 - cell_id = 0 - end_cell_id = 0 - top_pixel = 73 # pointy top - btm_pixel = 67 # flat btm - curr_row = 0 - while curr_row < n_rows: -# print "Curr Row:", curr_row - left_set = False - cells_added = 0 - up_down = 'up' - l_corner = False - r_corner = False - top = False - if curr_row == n_rows: - l_corner = True - else: - l_corner = False - - is_l_edge = False - is_r_edge = False - is_btm_edge = False - - loop_start = True - row_pos = 1 - while cell_id <= end_cell_id: - if curr_row == n_rows and cell_id == end_cell_id+1: - r_corner = True - else: - r_corner = False - - if curr_row == 0: - top = True - else: - top = False - if loop_start is True: - is_l_edge = True - else: - is_l_edge = False - - if cell_id == end_cell_id+1: - is_r_edge = True - else: - is_r_edge = False - if curr_row == n_rows: - if up_down in 'up': - is_btm_edge = True - else: - is_btm_edge = False - else: - is_btm_edge = False - cells_added += 1 - - tco = None # triangle cell object - if up_down == 'up': - tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [top_pixel, top_pixel-1, top_pixel-2, top_pixel-3, top_pixel-4, top_pixel-5]) - self._cells.append(tco) - top_pixel -= 6 - up_down = 'down' - else: - if curr_row > 1: - tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [btm_pixel, btm_pixel+1, btm_pixel+2, btm_pixel+3, btm_pixel+4, btm_pixel+5]) - self._cells.append(tco) - btm_pixel += 6 - else: - tco = TriangleCell(cell_id, curr_row, up_down, top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, [btm_pixel, btm_pixel-1, btm_pixel-2, btm_pixel-3, btm_pixel-4, btm_pixel-5]) - self._cells.append(tco) - btm_pixel -= 6 - up_down = 'up' - - # Add Cell To Triangle Grid! - tco.add_row_col(curr_row, grid_y_curr_pos) - self._triangle_grid[curr_row][grid_y_curr_pos] = tco - grid_y_curr_pos += 1 - - if l_corner is True: - l_corner = False - loop_start = False - - row_pos += 1 - - cell_id += 1 - - if cell_id == end_cell_id: - up_down = 'up' - end_cell_id += 2 + cells_added - - grid_y_curr_pos = grid_y_start_pos - curr_row-1 - - curr_row += 1 - btm_pixel -= 2 # (curr_row*6)+7 - if curr_row > 1: - btm_pixel -= (curr_row*6)+9+9 - - top_pixel -= (curr_row*6)+9 + @property + def row_count(self): + """Returns the number of rows in the triangle.""" + return self._row_count + + @property + def cells(self): + return self._cells def go(self): self._model.go() def clear(self): """Clears grid by setting all cells to 0.""" - self.set_all_cells(RGB( 0, 0, 0)) + for cell in self._cells: + self.set_cell_by_id(cell, RGB(0, 0, 0)) self.go() - def get_cells(self): - return self._cells + def get_cell_by_coordinates(self, row, column): + """Returns Cell for (row, column) coordinates, or ValueError if coordinates are out of bounds.""" + if row > self.row_count or column > row_length(self.row_count): + raise ValueError(f'(row={row}, column={column}) out of bounds for triangle with {self.row_count} rows') - def get_triangle_grid(self): - return self._triangle_grid - - def get_cell_by_grid_coords(self, rown, coln): - """Returns cell object, or None if no cell is mapped to the coord""" - - if rown > self._n_rows: - return None - elif coln > self._len_of_last_row: - return None - else: - return self._triangle_grid[rown][coln] + return self._cells[self.coordinates_to_id(row, column)] def get_cell_by_id(self, cell_id): - return self._cells[cell_id+1] - - def get_cell_by_array_posn(self, arr_pos): - return self._cells[arr_pos] - - def set_cell(self, cell, color): - if cell is None: - print("WARNING: Skipping 'Nonetype' cell") - else: - for pixel in cell.get_pixels(): - self._model.set_pixel(pixel, color, cell.get_id()) + return self._cells[cell_id] - def set_cell_by_cellid(self, cell, color): - for pixel in self._cells[cell].get_pixels(): - self._model.set_pixel(pixel, color, cell) + def set_cell_by_id(self, cell_id, color): + for pixel in self._model.get_pixels(cell_id): + pixel.set_color(color) - def get_all_cells(self): - """Return the list of valid cell IDs""" - return self._cells - - def set_cells_by_cellid(self, cells, color): - for cell in cells: - for pixel in self._cells[cell].get_pixels(): - self._model.set_pixel(pixel, color, cell.get_id()) - - def set_cells(self, cells, color): - for cell in cells: - if cell is None: - print("WARNING: Skipping 'Nonetype' cell") - else: - for pixel in cell.get_pixels(): - self._model.set_pixel(pixel, color, cell.get_id()) - - def set_all_cells(self, color): - for cell in self._cells: - for pixel in cell.get_pixels(): - self._model.set_pixel(pixel, color, cell.get_id()) - - def set_pixel(self, pixel, color, cellid): - # Have to pass the cellid through b/c the simulator does not understand pixels - self._model.set_pixel(pixel, color, cellid) + def get_pixels(self, cell_id): + return self._model.get_pixels(cell_id) # Convenience methods for grabbing useful parts of the triangle grid. + @property + def left_side_cells(self): + return [cell for cell in self._cells if cell.is_left_edge] - def get_left_side_cells(self): - return [cell for cell in self._cells if cell.is_left_edge()] - - def get_right_side_cells(self): - return [cell for cell in self._cells if cell.is_right_edge()] + @property + def right_side_cells(self): + return [cell for cell in self._cells if cell.is_right_edge] - def get_bottom_side_cells(self): - return [cell for cell in self._cells if cell.is_bottom_edge()] + @property + def bottom_side_cells(self): + return [cell for cell in self._cells if cell.is_bottom_edge] - def get_up_cells(self): - return [cell for cell in self._cells if cell.is_up()] + @property + def up_cells(self): + return [cell for cell in self._cells if cell.is_up] - def get_down_cells(self): - return [cell for cell in self._cells if cell.is_down()] - - @staticmethod - def is_edge(cell): - return cell._l_edge or cell._r_edge or cell._btm_side + @property + def down_cells(self): + return [cell for cell in self._cells if cell.is_down] # Triangle Grid Helper Functions - def get_edge_neighbors_by_coord(self, row, col): """ - Returns a tuple of (left neighbor, middle neighbor, right neighbor) + Returns a tuple of (left neighbor, middle neighbor, right neighbor). Left neighbor is the edge directly to the left of the cell, regardless of up/down orientation Middle neighbor is either the top or bottom neighbor depending where the edge is. the cell knows its up/down orientation Right neighbor is the cell immediately to the right. """ - cell = self.get_cell_by_grid_coords(row, col) - if cell is None: - return None - - l = None - m = None - r = None - if cell.is_up(): - l = self.get_cell_by_grid_coords(row, col-1) - m = self.get_cell_by_grid_coords(row+1, col) - r = self.get_cell_by_grid_coords(row, col+1) - else: - l = self.get_cell_by_grid_coords(row, col-1) - m = self.get_cell_by_grid_coords(row-1, col) - r = self.get_cell_by_grid_coords(row, col+1) - return (l, m, r) - - def get_edge_neighbors_by_cell(self, cell): - return self.get_edge_neighbors_by_coord(cell.get_row_num(), cell.get_col_pos()) + cell = self.get_cell_by_coordinates(row, col) - def get_vertex_neighbors_by_coord(self, row, col): - """ - Return the neighbors whose vertexes are point to point - Uses same logic as edge_neighbors, left, middle, right - """ - cell = self.get_cell_by_grid_coords(row, col) - if cell is None: - return None - - l = None - m = None - r = None - if cell.is_up(): - l = self.get_cell_by_grid_coords(row+1, col-1) - m = self.get_cell_by_grid_coords(row-1, col) - r = self.get_cell_by_grid_coords(row+1, col+1) - else: - l = self.get_cell_by_grid_coords(row-1, col-1) - m = self.get_cell_by_grid_coords(row+1, col) - r = self.get_cell_by_grid_coords(row-1, col+1) - return (l, m, r) - - def get_vertex_neighbors_by_cell(self, cell): - return self.get_vertex_neighbors_by_coord(cell.get_row_num(), cell.get_col_pos()) - - def get_hexagon_from_btm_cell_by_coords(self, row, col): - """ - Given the coordinates of an up facing cell, return the surrounding cells which will make a hexagon with the - specified cell as the base. The tuple will begin with the upper left cell, then its upper neighbor, then its - upper right neighbor... and so on. - """ + if cell.is_up: + left = self.get_cell_by_coordinates(row, col - 1) + middle = self.get_cell_by_coordinates(row + 1, col) + right = self.get_cell_by_coordinates(row, col + 1) + return left, middle, right + + left = self.get_cell_by_coordinates(row, col - 1) + middle = self.get_cell_by_coordinates(row - 1, col) + right = self.get_cell_by_coordinates(row, col + 1) + return left, middle, right - cell = self.get_cell_by_grid_coords(row, col) - if cell is None: - return None - - a = self.get_cell_by_grid_coords(row, col-1) - b = self.get_cell_by_grid_coords(row-1, col-1) - c = self.get_cell_by_grid_coords(row-1, col) - d = self.get_cell_by_grid_coords(row-1, col+1) - e = self.get_cell_by_grid_coords(row, col+1) - return (cell, a, b, c, d, e) - - def get_hexagon_from_btm_cell_by_cell(self, cell): - return self.get_hexagon_from_btm_cell_by_coords(cell.get_row_num(), cell.get_col_pos()) - - -class TriangleCell(object): - def __init__(self, cell_id, row, up_down, is_top, l_corner, r_corner, is_l_edge, is_r_edge, is_btm_edge, row_pos, pixels=[]): - self._id = cell_id - self._row_n = row - self._row_pos = row_pos - self._l_edge = is_l_edge - if cell_id in [1, 3]: - self._l_edge = True - self._r_edge = is_r_edge - if cell_id in [1, 3]: - self._r_edge = True - self._top_cell = is_top - self._btm_right = r_corner - self._btm_left = l_corner - self._pointy_side = up_down - self._btm_side = is_btm_edge - self._up_down = up_down - self._pixels = pixels # do we really need a pixel class? - - def add_row_col(self, r, c): - self._grid_row_n = r - self._grid_col_n = c - - def get_id(self): - return self._id - - def get_pixels(self, oriented=True): - if oriented: - if self._up_down is 'up': - return self._pixels - else: - if self._row_n == 2: ####FIXME Something wrrong with bottom cells in row 2 and maybe deeper - return self._pixels - else: - return self._pixels[::-1] - else: - return self._pixels - - def get_row_num(self): - return self._grid_row_n - def get_col_pos(self): - return self._grid_col_n - def is_left_edge(self): - return self._l_edge - def is_right_edge(self): - return self._r_edge - def is_bottom_edge(self): - return self._btm_side - def is_top(self): - return self._top_cell - def is_right_btm_corner(self): - return self._btm_right - def is_left_btm_corner(self): - return self._btm_left - def row_num(self): - return self._row_n - def is_up(self): - return self._up_down in "up" - def is_down(self): - return self._up_down in "down" - - -class TriangleCellPixel(object): - def __init__(self): - pass + def get_edge_neighbors_by_cell(self, cell_id): + return self.get_edge_neighbors_by_coord(*self.id_to_coordinates(cell_id)) + + def get_vertex_neighbors_by_coord(self, row, col): + """Return the neighbors whose vertexes are point to point.""" + cell = self.get_cell_by_coordinates(row, col) + + if cell.is_up: + left = self.get_cell_by_coordinates(row + 1, col - 1) + middle = self.get_cell_by_coordinates(row - 1, col) + right = self.get_cell_by_coordinates(row + 1, col + 1) + return left, middle, right + + left = self.get_cell_by_coordinates(row - 1, col - 1) + middle = self.get_cell_by_coordinates(row + 1, col) + right = self.get_cell_by_coordinates(row - 1, col + 1) + return left, middle, right + + def get_vertex_neighbors_by_cell(self, cell_id): + return self.get_vertex_neighbors_by_coord(*self.id_to_coordinates(cell_id)) + + +class TriangleCell(NamedTuple): + """A Cell contains multiple LED pixels.""" + id: int + num_rows: int + + @property + def row(self) -> int: + """Returns zero-indexed row containing of the cell.""" + (row, column) = TriangleGrid.id_to_coordinates(self.id) + return row + + @property + def row_position(self) -> int: + """Returns zero-indexed position of the cell within a triangle row.""" + (row, column) = TriangleGrid.id_to_coordinates(self.id) + return column + + @property + def is_left_edge(self) -> bool: + """Returns True if cell is along the left edge of the greater triangle.""" + return self.row_position == 0 + + @property + def is_right_edge(self) -> bool: + """Returns True if cell is along the right edge of the greater triangle.""" + return self.row_position == row_length(self.row + 1) - 1 + + @property + def is_bottom_edge(self) -> bool: + """Returns True if cell is along the bottom edge of the greater triangle.""" + return self.row + 1 == self.num_rows and self.is_up + + @property + def is_top_corner(self) -> bool: + """Returns True if cell is the top corner of the greater triangle.""" + return self.id == 0 + + @property + def is_right_corner(self) -> bool: + """Returns True if cell is the right corner of the greater triangle.""" + return self.is_bottom_edge and self.is_right_edge + + @property + def is_left_corner(self) -> bool: + """Returns True if cell is the left corner of the greater triangle.""" + return self.is_bottom_edge and self.is_left_edge + + @property + def is_up(self) -> bool: + """Returns True if triangle shaped cell orients with a corner upward.""" + return self.row_position % 2 == 0 + + @property + def is_down(self) -> bool: + """Returns True if triangle shaped cell orients with a corner downward.""" + return not self.is_up + + +def make_triangle(model, row_count) -> TriangleGrid: + """Helper function to build a Triangle with row_count rows.""" + return TriangleGrid(model, row_count) From 92dba7c4b3175e57ee975a8700574046b188b784 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Wed, 24 Jul 2019 01:28:53 -0700 Subject: [PATCH 19/60] Implemented old (incorrect) mapping --- go_tri.py | 4 ++-- model/__init__.py | 1 + model/mapping.py | 17 +++++++++++++++++ model/mirror.py | 26 +++++++++++--------------- model/modelbase.py | 14 ++++---------- model/ola_model.py | 33 ++++++++++++++++++++++----------- model/sacn_model.py | 36 ++++++++++++++++++++---------------- model/simulator.py | 18 ++++++------------ tests/test_triangle_grid.py | 6 +++--- 9 files changed, 86 insertions(+), 69 deletions(-) create mode 100644 model/mapping.py diff --git a/go_tri.py b/go_tri.py index 581e7d2..d87d88d 100644 --- a/go_tri.py +++ b/go_tri.py @@ -7,7 +7,7 @@ import threading import cherrypy -from model import SimulatorModel, sACN +from model import SimulatorModel, sACN, demo_triangle_mapping import osc_serve import triangle_grid import shows @@ -327,7 +327,7 @@ def go_web(self): triangle_grid = triangle_grid.make_triangle(model, 11) # Our panels will only have 11 rows else: logger.info("Starting SACN") - model = sACN(max_dmx=800, model_json="./data/pixel_map.json") + model = sACN(model_json="./data/pixel_map.json", pixelmap=demo_triangle_mapping()) triangle_grid = triangle_grid.make_triangle(model, 3) diff --git a/model/__init__.py b/model/__init__.py index ef8a8f7..8d78e86 100644 --- a/model/__init__.py +++ b/model/__init__.py @@ -1,4 +1,5 @@ # These imports include submodules under the `model` namespace (e.g. model.SimulatorModel is available). +from .mapping import demo_triangle_mapping from .modelbase import ModelBase from .mirror import MirrorModel from .ola_model import OLAModel diff --git a/model/mapping.py b/model/mapping.py new file mode 100644 index 0000000..ed21a11 --- /dev/null +++ b/model/mapping.py @@ -0,0 +1,17 @@ +from typing import Dict, List +PixelMap = Dict[int, List[int]] + + +# This cell -> pixels mapping is created from an old version, but it should produce something. +def demo_triangle_mapping() -> PixelMap: + return { + 0: [73, 72, 71, 70, 69, 68], + 1: [52, 51, 50, 49, 48, 47], + 2: [65, 64, 63, 62, 61, 60], + 3: [46, 45, 44, 43, 42, 41], + 4: [19, 18, 17, 16, 15, 14], + 5: [27, 28, 29, 30, 31, 32], + 6: [13, 12, 11, 10, 9, 8], + 7: [33, 34, 35, 36, 37, 38], + 8: [7, 6, 5, 4, 3, 2] + } diff --git a/model/mirror.py b/model/mirror.py index c5ae029..84b6734 100644 --- a/model/mirror.py +++ b/model/mirror.py @@ -4,20 +4,10 @@ Completely agnostic as to what the cell ids look like, they are just passed through to the underlying model. """ from itertools import zip_longest +from typing import Callable, Iterator -from typing import Iterator, List, Type - -from .modelbase import ModelBase, PixelBase - - -class MirrorPixel(PixelBase): - def __init__(self, pixel_generators: List[Iterator[Type[PixelBase]]]): - self.generator = zip_longest(pixel_generators) - - def set_color(self, color): - pixels = next(self.generator) - for pixel in pixels: - pixel.set_color(color) +from color import Color +from .modelbase import ModelBase class MirrorModel(ModelBase): @@ -30,8 +20,14 @@ def __init__(self, *models): def add_model(self, model): self.models.append(model) - def get_pixels(self, cell_id): - return [model.get_pixels(cell_id) for model in self.models] + def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + generators = zip_longest([model.get_pixels(cell_id) for model in self.models]) + + def set_color(color: Color): + pixels = next(generators) + for pixel in pixels: + pixel.set_color(color) + yield set_color def go(self): for m in self.models: diff --git a/model/modelbase.py b/model/modelbase.py index 4c20380..cb52e76 100644 --- a/model/modelbase.py +++ b/model/modelbase.py @@ -1,20 +1,14 @@ from abc import ABC, abstractmethod -from typing import Iterator, Type - - -# TODO: Maybe just use a function instead of a class. -class PixelBase(ABC): - @abstractmethod - def set_color(self, color): - """Set the color for a pixel.""" +from color import Color +from typing import Callable, Iterator class ModelBase(ABC): """Abstract base class for simulators.""" @abstractmethod - def get_pixels(self, cell_id) -> Iterator[Type[PixelBase]]: - """Returns iterator for Pixels in a Cell""" + def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + """Returns iterator for Pixel setting functions in a Cell""" @abstractmethod def go(self): diff --git a/model/ola_model.py b/model/ola_model.py index ac3149d..1bcf17a 100644 --- a/model/ola_model.py +++ b/model/ola_model.py @@ -7,17 +7,24 @@ """ import array import json -import ola +import logging +from typing import Iterator, Callable +from color import Color +import ola +from .mapping import PixelMap from .modelbase import ModelBase +logger = logging.getLogger("pyramidtriangles") + class OLAModel(ModelBase): - def __init__(self, max_dmx, model_json=None): + def __init__(self, max_dmx, model_json: str, pixelmap: PixelMap): # XXX any way to check if this is a valid connection? self.PIXEL_MAP = None self._map_leds(model_json) + self._pixelmap = pixelmap self.wrapper = ola.ClientWrapper() self.client = self.wrapper.Client() # Keys for LEDs are integers representing universes, each universe has an array of possible DMX channels @@ -41,17 +48,21 @@ def _map_leds(self, f): self.PIXEL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) # Model basics - def set_pixel(self, pixel, color, cellid=None): - if pixel in self.PIXEL_MAP: - ux = self.PIXEL_MAP[pixel][0] + def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + for pixel in self._pixelmap[cell_id]: + if pixel not in self.PIXEL_MAP: + logger.warning(f'{pixel} not in sACN pixel ID map') + + ux = self.PIXEL_MAP[pixel][0] ix = self.PIXEL_MAP[pixel][1] - 1 # dmx is 1-based, python lists are 0-based - self.leds[ux][ix] = color.r - self.leds[ux][ix+1] = color.g - self.leds[ux][ix+2] = color.b - self.leds[ux][ix+3] = color.w - else: - print(f'WARNING: {pixel} not in pixel ID MAP') + def set_color(color): + self.leds[ux][ix] = color.r + self.leds[ux][ix + 1] = color.g + self.leds[ux][ix + 2] = color.b + self.leds[ux][ix + 3] = color.w + + yield set_color def go(self): data_to_send = {} diff --git a/model/sacn_model.py b/model/sacn_model.py index 511437d..703b152 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -5,14 +5,19 @@ have one LED each. """ import json +import logging +from typing import Callable, Iterator + +from color import Color import sacn -from typing import Iterator, Type +from .mapping import PixelMap +from .modelbase import ModelBase -from .modelbase import ModelBase, PixelBase +logger = logging.getLogger("pyramidtriangles") class sACN(ModelBase): - def __init__(self, max_dmx, model_json=None): + def __init__(self, model_json: str, pixelmap: PixelMap): # XXX any way to check if this is a valid connection? # Must supply an IP address to bind to that is in the same subnet as the devices routing the universes. Might @@ -23,6 +28,7 @@ def __init__(self, max_dmx, model_json=None): # dictionary which will hold an array of 512 int's for each universe, universes are keys to the arrays. self.leds = {} self._map_leds(model_json) + self._pixelmap = pixelmap # Keys for LEDs are integers representing universes, each universe has an array of possible DMX channels # Pixels are an LED represented by 4 DMX addresses @@ -46,22 +52,20 @@ def _map_leds(self, f): self.sender[universe].multicast = True self.leds[universe] = [0] * 512 - def get_pixels(self, cell_id) -> Iterator[Type[PixelBase]]: - # TODO: Need to map cell_id to series of pixels - return None + def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + for pixel in self._pixelmap[cell_id]: + if pixel not in self.PIXEL_MAP: + logger.warning(f'{pixel} not in sACN pixel ID map') - # TODO: Delete this - def set_pixel(self, pixel, color, cellid=None): - if pixel in self.PIXEL_MAP: - ux = self.PIXEL_MAP[pixel][0] + ux = self.PIXEL_MAP[pixel][0] ix = self.PIXEL_MAP[pixel][1] - 1 # dmx is 1-based, python lists are 0-based - self.leds[ux][ix] = color.r - self.leds[ux][ix+1] = color.g - self.leds[ux][ix+2] = color.b - self.leds[ux][ix+3] = color.w - else: - print(f'WARNING: {pixel} not in pixel ID MAP') + def set_color(color): + self.leds[ux][ix] = color.r + self.leds[ux][ix + 1] = color.g + self.leds[ux][ix + 2] = color.b + self.leds[ux][ix + 3] = color.w + yield set_color def go(self): for ux in self.leds: diff --git a/model/simulator.py b/model/simulator.py index c953548..db94b66 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -6,9 +6,9 @@ import logging import socket import json -from typing import Iterator, Type +from typing import Callable, Iterator -from modelbase import PixelBase +from color import Color from .modelbase import ModelBase SIM_DEFAULT = (188, 210, 229) # BCD2E5, "off" color for simulator @@ -40,16 +40,10 @@ def _map_leds(self, f): self.CELL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) # Model basics - def get_pixels(self, cell_id) -> Iterator[Type[PixelBase]]: - class Pixel(PixelBase): - def __init__(self, setter, cell): - self.setter = setter - self.cell = cell - - def set_color(self, color): - self.setter(self.cell, color) - - return iter([Pixel(self.set_cell, cell_id)]) + def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + def set_color(color: Color): + self.set_cell(cell_id, color) + return iter([set_color]) def set_cell(self, cell, color): cell += 1 # Simulator cells not 0 based diff --git a/tests/test_triangle_grid.py b/tests/test_triangle_grid.py index aa78ec4..f545f54 100644 --- a/tests/test_triangle_grid.py +++ b/tests/test_triangle_grid.py @@ -1,12 +1,12 @@ -from typing import Iterator, Type +from typing import Callable, Iterator +from color import Color import triangle_grid from model import ModelBase -from modelbase import PixelBase class FakeModel(ModelBase): - def get_pixels(self, cell_id) -> Iterator[Type[PixelBase]]: + def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: pass def go(self): From cbfacc3bbe6449aea8acfb5ce3590c5feac5785a Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Wed, 24 Jul 2019 02:12:09 -0700 Subject: [PATCH 20/60] Solve conflicts --- model/simulator.py | 4 +- shows/cycle_hsv.py | 41 ++++++++------- shows/left_to_right.py | 30 ++++------- shows/left_to_right_and_back.py | 88 ++++++++++++--------------------- shows/one_by_one.py | 7 +-- shows/random_cells.py | 4 +- shows/strobe.py | 2 +- shows/up_down.py | 17 ++++--- triangle_grid.py | 9 ++-- 9 files changed, 85 insertions(+), 117 deletions(-) diff --git a/model/simulator.py b/model/simulator.py index db94b66..7888206 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -40,12 +40,12 @@ def _map_leds(self, f): self.CELL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) # Model basics - def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + def get_pixels(self, cell_id: int) -> Iterator[Callable[[Color], None]]: def set_color(color: Color): self.set_cell(cell_id, color) return iter([set_color]) - def set_cell(self, cell, color): + def set_cell(self, cell: int, color: Color): cell += 1 # Simulator cells not 0 based if cell not in self.CELL_MAP: raise ValueError(f'Cell {cell} not in CELL_MAP') diff --git a/shows/cycle_hsv.py b/shows/cycle_hsv.py index a2413aa..8fd5d16 100644 --- a/shows/cycle_hsv.py +++ b/shows/cycle_hsv.py @@ -1,47 +1,46 @@ -from .showbase import ShowBase +import time + from color import HSV as hsv +from triangle_grid import TriangleGrid +from .showbase import ShowBase -import random as rnd -import time class CycleHSV(ShowBase): - def __init__(self, tri_grid, frame_delay = 0.1): + def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.1): self.tri_grid = tri_grid self.frame_delay = frame_delay - self.n_cells = len(self.tri_grid.get_cells()) + self.n_cells = len(self.tri_grid.cells) # from IPython import embed; embed() - def next_frame(self): self.tri_grid.clear() time.sleep(3) while True: - self.ca= hsv(0.0,0.0,0.0) - while self.ca.v < 1.0: - self.ca.v += 0.0008 - self.tri_grid.set_all_cells(self.ca) + ca = hsv(0.0, 0.0, 0.0) + while ca.v < 1.0: + ca.v += 0.0008 + self.tri_grid.set_all_cells(ca) self.tri_grid.go() time.sleep(.01) - print('CA', self.ca.hsv) + print('CA', ca.hsv) - while self.ca.s < 1.0: - self.ca.s += 0.0008 - self.tri_grid.set_all_cells(self.ca) + while ca.s < 1.0: + ca.s += 0.0008 + self.tri_grid.set_all_cells(ca) self.tri_grid.go() time.sleep(.01) - print('CA', self.ca.hsv) - - while self.ca.h < 1.0: - self.ca.h += 0.0008 - self.tri_grid.set_all_cells(self.ca) + print('CA', ca.hsv) + + while ca.h < 1.0: + ca.h += 0.0008 + self.tri_grid.set_all_cells(ca) self.tri_grid.go() time.sleep(.01) - print('CA', self.ca.hsv) + print('CA', ca.hsv) self.tri_grid.clear() time.sleep(3) - yield self.frame_delay diff --git a/shows/left_to_right.py b/shows/left_to_right.py index c064477..08ebcd3 100644 --- a/shows/left_to_right.py +++ b/shows/left_to_right.py @@ -1,34 +1,22 @@ -from .showbase import ShowBase from color import RGB +from .showbase import ShowBase +from triangle_grid import TriangleGrid, row_length class LeftToRight(ShowBase): - def __init__(self, tri_grid, frame_delay=1.0): + def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 1.0): self.tri_grid = tri_grid self.frame_delay = frame_delay def next_frame(self): - xlen = len(self.tri_grid._triangle_grid) - ylen = len(self.tri_grid._triangle_grid[0]) - x = 0 - y = 0 while True: self.tri_grid.clear() - print(f"x={x} y={y}") - if y < ylen: - for rows in self.tri_grid._triangle_grid: - cell = self.tri_grid._triangle_grid[x][y] - print("AAA", x, y, rows) - if cell is None: - pass - else: - self.tri_grid.set_cell_by_id(cell.id, RGB(255, 255, 25)) - x += 1 - x = 0 - y += 1 - else: - x = 0 - y = 0 + for row in range(self.tri_grid.row_count): + for column in range(row_length(row)): + print(f"column={column} row={row}") + + cell = self.tri_grid.get_cell_by_coordinates(row, column) + self.tri_grid.set_cell_by_id(cell.id, RGB(255, 255, 25)) yield self.frame_delay diff --git a/shows/left_to_right_and_back.py b/shows/left_to_right_and_back.py index 4edf19d..8878a2e 100644 --- a/shows/left_to_right_and_back.py +++ b/shows/left_to_right_and_back.py @@ -1,73 +1,49 @@ -from .showbase import ShowBase -from color import RGB import time +from color import RGB +from .showbase import ShowBase +from triangle_grid import TriangleGrid, row_length + class LeftToRightAndBack(ShowBase): - def __init__(self, tri_grid, frame_delay=1.0): + def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 1.0): self.tri_grid = tri_grid self.frame_delay = frame_delay def next_frame(self): - xlen = len(self.tri_grid._triangle_grid) - ylen = len(self.tri_grid._triangle_grid[0]) - x = 0 - y = 0 fwd = True - pix = 0 while True: - - if fwd is True: + if fwd: self.tri_grid.clear() - if y < ylen: - for rows in self.tri_grid._triangle_grid: - cell = self.tri_grid._triangle_grid[x][y] - - if cell is None: - pass - else: - r=255 - g = 0 - for pixel in self.tri_grid.get_pixels(cell.id): - pixel.set_color(RGB(r, g, 0)) - time.sleep(.2) - self.tri_grid.go() - g += 40 - r -= 3 - x += 1 - x = 0 - y += 1 + for row in range(self.tri_grid.row_count): + for column in range(row_length(row)): + cell = self.tri_grid.get_cell_by_coordinates(row, column) - else: - x = 0 - y = ylen-1 - fwd = False - else: + r = 255 + g = 0 + for pixel in self.tri_grid.get_pixels(cell.id): + pixel(RGB(r, g, 0)) + time.sleep(.2) + self.tri_grid.go() + g += 40 + r -= 3 - if y >= 0: - for rows in self.tri_grid._triangle_grid: + fwd = False - cell = self.tri_grid._triangle_grid[x][y] - - if cell is None: - pass - else: - g = 255 - b = 0 - for pixel in self.tri_grid.get_pixels(cell.id): - pixel.set_color(RGB(0, g, b)) - time.sleep(.2) - self.tri_grid.go() - g -= 40 - b += 40 - x += 1 - - y -= 1 - x = 0 - else: - y = 0 - x = 0 - fwd = True + else: + for row in reversed(range(self.tri_grid.row_count)): + for column in range(row_length(row)): + cell = self.tri_grid.get_cell_by_coordinates(row, column) + + g = 255 + b = 0 + for pixel in self.tri_grid.get_pixels(cell.id): + pixel(RGB(0, g, b)) + time.sleep(.2) + self.tri_grid.go() + g -= 40 + b += 40 + fwd = True yield self.frame_delay diff --git a/shows/one_by_one.py b/shows/one_by_one.py index 472d927..b8a6f4a 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -1,9 +1,10 @@ -from .showbase import ShowBase from color import RGB +from triangle_grid import TriangleGrid +from .showbase import ShowBase class OneByOne(ShowBase): - def __init__(self, tri_grid, frame_delay=0.25): + def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.25): self.tri_grid = tri_grid self.frame_delay = frame_delay @@ -15,7 +16,7 @@ def next_frame(self): while True: self.tri_grid.clear() print(cell_n) - self.tri_grid.set_cell_by_id(self.tri_grid.cells[cell_n].id, RGB(255, 255, 25)) + self.tri_grid.set_cell_by_id(cell_n, RGB(255, 255, 25)) if cell_n >= ncells: cell_n = -1 diff --git a/shows/random_cells.py b/shows/random_cells.py index d46ecfc..284d67d 100644 --- a/shows/random_cells.py +++ b/shows/random_cells.py @@ -14,7 +14,7 @@ def next_frame(self): while True: self.tri_grid.clear() - self.tri_grid.set_cell_by_id(rnd.randint(1, self.n_cells - 2), RGB(200, 255, 25)) - self.tri_grid.set_cell_by_id(rnd.randint(1, self.n_cells - 2), RGB(200, 10, 25)) + self.tri_grid.set_cell_by_id(rnd.randint(0, self.n_cells - 2), RGB(200, 255, 25)) + self.tri_grid.set_cell_by_id(rnd.randint(0, self.n_cells - 2), RGB(200, 10, 25)) yield self.frame_delay diff --git a/shows/strobe.py b/shows/strobe.py index ba057ae..129f4ee 100644 --- a/shows/strobe.py +++ b/shows/strobe.py @@ -20,8 +20,8 @@ def next_frame(self): self.tri_grid.go() time.sleep(0.01) self.tri_grid.set_all_cells(RGB(5, 5, 255)) - time.sleep(0.02) self.tri_grid.go() + time.sleep(0.02) self.tri_grid.set_all_cells(RGB(100, 100, 100)) self.tri_grid.go() time.sleep(0.01) diff --git a/shows/up_down.py b/shows/up_down.py index df29a36..e51b73a 100644 --- a/shows/up_down.py +++ b/shows/up_down.py @@ -1,9 +1,10 @@ -from .showbase import ShowBase from color import RGB +from triangle_grid import TriangleGrid +from .showbase import ShowBase class UpDown(ShowBase): - def __init__(self, tri_grid, frame_delay=2): + def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 2.0): self.tri_grid = tri_grid self.frame_delay = frame_delay @@ -15,14 +16,14 @@ def next_frame(self): if a == "up": print('up') - for i in self.tri_grid.cells.up_cells: - print("Up", i.id) - self.tri_grid.cells.set_cell_by_id(i.id, RGB(0, 255, 255)) + for cell in self.tri_grid.up_cells: + print("Up", cell.id) + self.tri_grid.set_cell_by_id(cell.id, RGB(0, 255, 255)) else: print('down') - for i in self.tri_grid.cells.down_cells: - print("down", i.id) - self.tri_grid.cells.set_cell_by_id(i.id, RGB(255, 0, 200)) + for cell in self.tri_grid.down_cells: + print("down", cell.id) + self.tri_grid.set_cell_by_id(cell.id, RGB(255, 0, 200)) if a == "up": a = "down" diff --git a/triangle_grid.py b/triangle_grid.py index 507dfe6..1cf61c3 100644 --- a/triangle_grid.py +++ b/triangle_grid.py @@ -66,7 +66,7 @@ def go(self): def clear(self): """Clears grid by setting all cells to 0.""" for cell in self._cells: - self.set_cell_by_id(cell, RGB(0, 0, 0)) + self.set_cell_by_id(cell.id, RGB(0, 0, 0)) self.go() def get_cell_by_coordinates(self, row, column): @@ -80,8 +80,11 @@ def get_cell_by_id(self, cell_id): return self._cells[cell_id] def set_cell_by_id(self, cell_id, color): - for pixel in self._model.get_pixels(cell_id): - pixel.set_color(color) + [pixel(color) for pixel in self._model.get_pixels(cell_id)] + + def set_all_cells(self, color): + for cell in self.cells: + self.set_cell_by_id(cell.id, color) def get_pixels(self, cell_id): return self._model.get_pixels(cell_id) From 1e7e9fe295c9f6cc575279d8a4ccd9ba291fddba Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Wed, 24 Jul 2019 11:38:41 -0700 Subject: [PATCH 21/60] More tests and fixes some bugs --- go_tri.py | 6 +- triangle_grid.py => grid.py | 127 ++++++++++++++++++++------------ shows/cycle_hsv.py | 2 +- shows/left_to_right.py | 2 +- shows/left_to_right_and_back.py | 2 +- shows/one_by_one.py | 2 +- shows/up_down.py | 2 +- tests/test_grid.py | 124 +++++++++++++++++++++++++++++++ tests/test_triangle_grid.py | 74 ------------------- 9 files changed, 213 insertions(+), 128 deletions(-) rename triangle_grid.py => grid.py (59%) create mode 100644 tests/test_grid.py delete mode 100644 tests/test_triangle_grid.py diff --git a/go_tri.py b/go_tri.py index d87d88d..0bc8581 100644 --- a/go_tri.py +++ b/go_tri.py @@ -9,7 +9,7 @@ import cherrypy from model import SimulatorModel, sACN, demo_triangle_mapping import osc_serve -import triangle_grid +import grid import shows import util from web.web import TriangleWeb @@ -324,12 +324,12 @@ def go_web(self): logger.info(f'Using TriSimulator at {sim_host}:{sim_port}') model = SimulatorModel(sim_host, port=sim_port, model_json='./data/pixel_map.json') - triangle_grid = triangle_grid.make_triangle(model, 11) # Our panels will only have 11 rows + triangle_grid = grid.make_triangle(model, 11) # Our panels will only have 11 rows else: logger.info("Starting SACN") model = sACN(model_json="./data/pixel_map.json", pixelmap=demo_triangle_mapping()) - triangle_grid = triangle_grid.make_triangle(model, 3) + triangle_grid = grid.make_triangle(model, 3) app = TriangleServer(triangle_grid, args) try: diff --git a/triangle_grid.py b/grid.py similarity index 59% rename from triangle_grid.py rename to grid.py index 1cf61c3..391699f 100644 --- a/triangle_grid.py +++ b/grid.py @@ -1,17 +1,20 @@ from functools import lru_cache -from typing import NamedTuple +import logging +from typing import List, NamedTuple, Tuple -from color import RGB +from color import Color, RGB + +logger = logging.getLogger('pyramidtriangles') @lru_cache(maxsize=128) -def triangular_number(n): +def triangular_number(n: int) -> int: """Returns the number of elements in an equilateral triangle of n rows.""" # Typically the triangle number is (n(n+1))/2 but our triangle has rows of 1, 3, 5... return n ** 2 -def row_length(n): +def row_length(n: int) -> int: """Returns count of elements in nth row of equilateral triangle.""" return n * 2 - 1 @@ -29,7 +32,7 @@ def __init__(self, model, row_count): self._cells.append(TriangleCell(id=cell_id, num_rows=self.row_count)) @staticmethod - def coordinates_to_id(row, column): + def coordinates_to_id(row: int, column: int) -> int: """Converts zero-indexed (row, column) coordinates to zero-indexed ID.""" if row < 0 or column < 0: raise ValueError(f'(row={row}, column={column}) must be non-negative') @@ -37,7 +40,7 @@ def coordinates_to_id(row, column): @staticmethod @lru_cache(maxsize=512) - def id_to_coordinates(cell_id): + def id_to_coordinates(cell_id: int) -> Tuple[int, int]: """Converts zero-indexed ID to zero-indexed (row, column) coordinates.""" if cell_id < 0: raise ValueError(f'cell_id={cell_id} must be non-negative') @@ -52,12 +55,17 @@ def __repr__(self): return f'{__class__.__name__} (n_rows={self.row_count}, model={self._model})' @property - def row_count(self): + def row_count(self) -> int: """Returns the number of rows in the triangle.""" return self._row_count @property - def cells(self): + def size(self) -> int: + """Returns the number of cells in the triangle.""" + return len(self._cells) + + @property + def cells(self) -> List['TriangleCell']: return self._cells def go(self): @@ -65,92 +73,119 @@ def go(self): def clear(self): """Clears grid by setting all cells to 0.""" - for cell in self._cells: - self.set_cell_by_id(cell.id, RGB(0, 0, 0)) + self.set_all_cells(RGB(0, 0, 0)) self.go() - def get_cell_by_coordinates(self, row, column): - """Returns Cell for (row, column) coordinates, or ValueError if coordinates are out of bounds.""" - if row > self.row_count or column > row_length(self.row_count): - raise ValueError(f'(row={row}, column={column}) out of bounds for triangle with {self.row_count} rows') + def get_cell_by_coordinates(self, row: int, column: int) -> 'TriangleCell': + """Returns Cell for (row, column) coordinates, or None if coordinates are out of bounds.""" + if not 0 <= row < self.row_count or not 0 <= column < row_length(row + 1): + logger.debug(f'(row={row}, column={column}) out of bounds for triangle with {self.row_count} rows') + return None return self._cells[self.coordinates_to_id(row, column)] - def get_cell_by_id(self, cell_id): + def get_cell_by_id(self, cell_id: int) -> 'TriangleCell': + """Returns Cell for cell ID, or None if cell ID out of bounds.""" + if not 0 <= cell_id < len(self._cells): + return None return self._cells[cell_id] - def set_cell_by_id(self, cell_id, color): + def get_pixels(self, cell_id): + return self._model.get_pixels(cell_id) + + def set_cell_by_id(self, cell_id: int, color: Color): [pixel(color) for pixel in self._model.get_pixels(cell_id)] - def set_all_cells(self, color): + def set_all_cells(self, color: Color): for cell in self.cells: self.set_cell_by_id(cell.id, color) - def get_pixels(self, cell_id): - return self._model.get_pixels(cell_id) - # Convenience methods for grabbing useful parts of the triangle grid. @property - def left_side_cells(self): + def left_side_cells(self) -> List['TriangleCell']: return [cell for cell in self._cells if cell.is_left_edge] @property - def right_side_cells(self): + def right_side_cells(self) -> List['TriangleCell']: return [cell for cell in self._cells if cell.is_right_edge] @property - def bottom_side_cells(self): + def bottom_side_cells(self) -> List['TriangleCell']: return [cell for cell in self._cells if cell.is_bottom_edge] @property - def up_cells(self): + def up_cells(self) -> List['TriangleCell']: return [cell for cell in self._cells if cell.is_up] @property - def down_cells(self): + def down_cells(self) -> List['TriangleCell']: return [cell for cell in self._cells if cell.is_down] # Triangle Grid Helper Functions - def get_edge_neighbors_by_coord(self, row, col): + def edge_neighbors_by_coord(self, row: int, col: int) -> Tuple['TriangleCell', 'TriangleCell', 'TriangleCell']: """ - Returns a tuple of (left neighbor, middle neighbor, right neighbor). - Left neighbor is the edge directly to the left of the cell, regardless of up/down orientation - Middle neighbor is either the top or bottom neighbor depending where the edge is. the cell knows its up/down orientation + Returns a tuple of (left, middle, right) cells that share an edge with the given (row, column) cell. + + Left neighbor is the edge directly to the left of the cell, regardless of up/down orientation. + Middle neighbor is either the top or bottom neighbor depending where the edge is. Right neighbor is the cell immediately to the right. """ cell = self.get_cell_by_coordinates(row, col) - - if cell.is_up: - left = self.get_cell_by_coordinates(row, col - 1) - middle = self.get_cell_by_coordinates(row + 1, col) - right = self.get_cell_by_coordinates(row, col + 1) - return left, middle, right + if cell is None: + return None, None, None left = self.get_cell_by_coordinates(row, col - 1) - middle = self.get_cell_by_coordinates(row - 1, col) right = self.get_cell_by_coordinates(row, col + 1) + + if cell.is_up: + middle = self.get_cell_by_coordinates(row + 1, col + 1) + else: + middle = self.get_cell_by_coordinates(row - 1, col - 1) + return left, middle, right - def get_edge_neighbors_by_cell(self, cell_id): - return self.get_edge_neighbors_by_coord(*self.id_to_coordinates(cell_id)) + def edge_neighbors_by_cell(self, cell_id: int) -> Tuple['TriangleCell', 'TriangleCell', 'TriangleCell']: + """ + Returns a tuple of (left, middle, right) cells that share an edge with the given cell ID. + + Left neighbor is the cell directly to the left. + Middle neighbor is either the top or bottom neighbor depending where the edge is. + Right neighbor is the cell directly to the right. + """ + return self.edge_neighbors_by_coord(*self.id_to_coordinates(cell_id)) + + def vertex_neighbors_by_coord(self, row: int, col: int) -> Tuple['TriangleCell', 'TriangleCell', 'TriangleCell']: + """ + Returns a tuple of (left, middle, right) cells that share a vertex with the given (row, column) cell. - def get_vertex_neighbors_by_coord(self, row, col): - """Return the neighbors whose vertexes are point to point.""" + Left neighbor is the cell opposite the left vertex of the given cell. + Middle neighbor is the cell opposite the middle (up or down) vertex of the given cell. + Right neighbor is the cell opposite the right vertex of the given cell. + """ cell = self.get_cell_by_coordinates(row, col) + if cell is None: + return None, None, None if cell.is_up: left = self.get_cell_by_coordinates(row + 1, col - 1) - middle = self.get_cell_by_coordinates(row - 1, col) - right = self.get_cell_by_coordinates(row + 1, col + 1) + middle = self.get_cell_by_coordinates(row - 1, col - 1) + right = self.get_cell_by_coordinates(row + 1, col + 3) return left, middle, right - left = self.get_cell_by_coordinates(row - 1, col - 1) - middle = self.get_cell_by_coordinates(row + 1, col) + left = self.get_cell_by_coordinates(row - 1, col - 2) + middle = self.get_cell_by_coordinates(row + 1, col + 1) right = self.get_cell_by_coordinates(row - 1, col + 1) return left, middle, right - def get_vertex_neighbors_by_cell(self, cell_id): - return self.get_vertex_neighbors_by_coord(*self.id_to_coordinates(cell_id)) + def vertex_neighbors_by_cell(self, cell_id: int) -> Tuple['TriangleCell', 'TriangleCell', 'TriangleCell']: + """ + Returns a tuple of (left, middle, right) cells that share a vertex with the given cell ID. + + Left neighbor is the cell opposite the left vertex of the given cell. + Middle neighbor is the cell opposite the middle (up or down) vertex of the given cell. + Right neighbor is the cell opposite the right vertex of the given cell. + """ + return self.vertex_neighbors_by_coord(*self.id_to_coordinates(cell_id)) class TriangleCell(NamedTuple): diff --git a/shows/cycle_hsv.py b/shows/cycle_hsv.py index 8fd5d16..f799dc5 100644 --- a/shows/cycle_hsv.py +++ b/shows/cycle_hsv.py @@ -1,7 +1,7 @@ import time from color import HSV as hsv -from triangle_grid import TriangleGrid +from grid import TriangleGrid from .showbase import ShowBase diff --git a/shows/left_to_right.py b/shows/left_to_right.py index 08ebcd3..b031cb0 100644 --- a/shows/left_to_right.py +++ b/shows/left_to_right.py @@ -1,6 +1,6 @@ from color import RGB from .showbase import ShowBase -from triangle_grid import TriangleGrid, row_length +from grid import TriangleGrid, row_length class LeftToRight(ShowBase): diff --git a/shows/left_to_right_and_back.py b/shows/left_to_right_and_back.py index 8878a2e..4c7d81b 100644 --- a/shows/left_to_right_and_back.py +++ b/shows/left_to_right_and_back.py @@ -2,7 +2,7 @@ from color import RGB from .showbase import ShowBase -from triangle_grid import TriangleGrid, row_length +from grid import TriangleGrid, row_length class LeftToRightAndBack(ShowBase): diff --git a/shows/one_by_one.py b/shows/one_by_one.py index b8a6f4a..00f59d0 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -1,5 +1,5 @@ from color import RGB -from triangle_grid import TriangleGrid +from grid import TriangleGrid from .showbase import ShowBase diff --git a/shows/up_down.py b/shows/up_down.py index e51b73a..87d0c11 100644 --- a/shows/up_down.py +++ b/shows/up_down.py @@ -1,5 +1,5 @@ from color import RGB -from triangle_grid import TriangleGrid +from grid import TriangleGrid from .showbase import ShowBase diff --git a/tests/test_grid.py b/tests/test_grid.py new file mode 100644 index 0000000..a1b4733 --- /dev/null +++ b/tests/test_grid.py @@ -0,0 +1,124 @@ +from typing import Callable, Iterator + +from color import Color +import grid +from model import ModelBase + + +class FakeModel(ModelBase): + def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + pass + + def go(self): + pass + + +def test_coordinate_conversion(): + for curr_id in range(256): + (row, column) = grid.TriangleGrid.id_to_coordinates(curr_id) + reverse_id = grid.TriangleGrid.coordinates_to_id(row, column) + assert reverse_id == curr_id + + +def test_builds_triangle(): + for row_count in range(1, 15): + triangle = grid.make_triangle(FakeModel(), row_count) + + expected_cell_count = grid.triangular_number(row_count) + assert triangle.row_count == row_count + assert triangle.size == len(triangle.cells) == expected_cell_count,\ + f'cell count {len(triangle.cells)} != expected {expected_cell_count} with rows {row_count}' + + # Each edge has the same number of elements are the number or total rows. + assert row_count ==\ + len(triangle.bottom_side_cells) == len(triangle.left_side_cells) == len(triangle.right_side_cells) + + +def test_cells_out_of_bounds(): + triangle = grid.make_triangle(FakeModel(), 2) + + assert triangle.get_cell_by_coordinates(-1, 0) is None + assert triangle.get_cell_by_coordinates(0, -1) is None + assert triangle.get_cell_by_coordinates(0, 1) is None # Only (0, 0) in first row. + assert triangle.get_cell_by_coordinates(1, 3) is None + assert triangle.get_cell_by_coordinates(2, 0) is None # Row 2 does not exist. + + assert triangle.get_cell_by_id(-1) is None + assert triangle.get_cell_by_id(triangle.size) is None + + middle_cell = triangle.get_cell_by_id(2) + assert middle_cell is not None + for neighbor in triangle.vertex_neighbors_by_cell(2): + assert neighbor is None + + +def test_cell_attributes(): + triangle = grid.make_triangle(FakeModel(), 3) + assert len(triangle.cells) == 9 + + top = triangle.get_cell_by_coordinates(0, 0) + assert top.is_top_corner and top.is_left_edge and top.is_right_edge and top.is_up + assert not top.is_bottom_edge + + left_corner = triangle.get_cell_by_coordinates(2, 0) + assert left_corner.is_left_corner and left_corner.is_left_edge and left_corner.is_bottom_edge and left_corner.is_up + assert not left_corner.is_right_edge + + right_corner = triangle.get_cell_by_coordinates(2, 4) + assert right_corner.is_right_corner and right_corner.is_right_edge and right_corner.is_bottom_edge + assert right_corner.is_up + assert not right_corner.is_left_edge + + inner = triangle.get_cell_by_coordinates(1, 1) + assert inner.is_down + assert not inner.is_left_edge and not inner.is_right_edge and not inner.is_bottom_edge + + +def test_cell_neighbors(): + triangle = grid.make_triangle(FakeModel(), 5) + + # Upward facing cell + (left, middle, right) = triangle.edge_neighbors_by_cell(6) + assert left.id == 5 + assert middle.id == 12 + assert right.id == 7 + + (left, middle, right) = triangle.vertex_neighbors_by_cell(6) + assert left.id == 10 + assert middle.id == 2 + assert right.id == 14 + + # Downward facing cell + (left, middle, right) = triangle.edge_neighbors_by_cell(5) + assert left.id == 4 + assert middle.id == 1 + assert right.id == 6 + + (left, middle, right) = triangle.vertex_neighbors_by_cell(5) + assert left is None + assert middle.id == 11 + assert right.id == 3 + + # Invalid cell + (left, middle, right) = triangle.edge_neighbors_by_coord(1, -1) + assert left is middle is right is None + (left, middle, right) = triangle.vertex_neighbors_by_coord(1, -1) + assert left is middle is right is None + + +def test_row_len(): + # (1, 1), (2, 3), (3, 5), (4, 7)... + for (row, length) in enumerate(range(1, 16, 2), start=1): + assert grid.row_length(row) == length + + +def test_triangle_number(): + for (n, number) in [ + (1, 1), + (2, 4), + (3, 9), + (4, 16), + (5, 25), + (6, 36) + ]: + assert grid.triangular_number(n) == number diff --git a/tests/test_triangle_grid.py b/tests/test_triangle_grid.py deleted file mode 100644 index f545f54..0000000 --- a/tests/test_triangle_grid.py +++ /dev/null @@ -1,74 +0,0 @@ -from typing import Callable, Iterator - -from color import Color -import triangle_grid -from model import ModelBase - - -class FakeModel(ModelBase): - def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: - pass - - def go(self): - pass - - -def test_coordinate_conversion(): - for curr_id in range(256): - (row, column) = triangle_grid.TriangleGrid.id_to_coordinates(curr_id) - reverse_id = triangle_grid.TriangleGrid.coordinates_to_id(row, column) - assert reverse_id == curr_id - - -def test_builds_triangle(): - for row_count in range(1, 15): - triangle = triangle_grid.make_triangle(FakeModel(), row_count) - - expected_cell_count = triangle_grid.triangular_number(row_count) - assert triangle.row_count == row_count - assert len(triangle.cells) == expected_cell_count,\ - f'cell count {len(triangle.cells)} != expected {expected_cell_count} with rows {row_count}' - - # Each edge has the same number of elements are the number or total rows. - assert row_count ==\ - len(triangle.bottom_side_cells) == len(triangle.left_side_cells) == len(triangle.right_side_cells) - - -def test_cell_attributes(): - triangle = triangle_grid.make_triangle(FakeModel(), 3) - assert len(triangle.cells) == 9 - - top = triangle.get_cell_by_coordinates(0, 0) - assert top.is_top_corner and top.is_left_edge and top.is_right_edge and top.is_up - assert not top.is_bottom_edge - - left_corner = triangle.get_cell_by_coordinates(2, 0) - assert left_corner.is_left_corner and left_corner.is_left_edge and left_corner.is_bottom_edge and left_corner.is_up - assert not left_corner.is_right_edge - - right_corner = triangle.get_cell_by_coordinates(2, 4) - assert right_corner.is_right_corner and right_corner.is_right_edge and right_corner.is_bottom_edge - assert right_corner.is_up - assert not right_corner.is_left_edge - - inner = triangle.get_cell_by_coordinates(1, 1) - assert inner.is_down - assert not inner.is_left_edge and not inner.is_right_edge and not inner.is_bottom_edge - - -def test_row_len(): - # (1, 1), (2, 3), (3, 5), (4, 7)... - for (row, length) in enumerate(range(1, 16, 2), start=1): - assert triangle_grid.row_length(row) == length - - -def test_triangle_number(): - for (n, number) in [ - (1, 1), - (2, 4), - (3, 9), - (4, 16), - (5, 25), - (6, 36) - ]: - assert triangle_grid.triangular_number(n) == number From 622abf2f50168c5105f7e7eb35d56aab01f2c4fb Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Wed, 24 Jul 2019 12:31:34 -0700 Subject: [PATCH 22/60] Fix shows --- go_tri.py | 4 +-- grid/__init__.py | 1 + grid.py => grid/grid.py | 24 +++++++++++-- shows/left_to_right.py | 26 ++++++++++---- shows/left_to_right_and_back.py | 60 ++++++++++++++------------------- 5 files changed, 70 insertions(+), 45 deletions(-) create mode 100644 grid/__init__.py rename grid.py => grid/grid.py (91%) diff --git a/go_tri.py b/go_tri.py index 0bc8581..f4d9fe0 100644 --- a/go_tri.py +++ b/go_tri.py @@ -7,12 +7,12 @@ import threading import cherrypy +import grid from model import SimulatorModel, sACN, demo_triangle_mapping import osc_serve -import grid import shows import util -from web.web import TriangleWeb +from web import TriangleWeb # Prints stack trace on failure faulthandler.enable() diff --git a/grid/__init__.py b/grid/__init__.py new file mode 100644 index 0000000..1243a97 --- /dev/null +++ b/grid/__init__.py @@ -0,0 +1 @@ +from .grid import TriangleGrid, TriangleCell, make_triangle, row_length, triangular_number diff --git a/grid.py b/grid/grid.py similarity index 91% rename from grid.py rename to grid/grid.py index 391699f..e39711c 100644 --- a/grid.py +++ b/grid/grid.py @@ -1,6 +1,6 @@ from functools import lru_cache import logging -from typing import List, NamedTuple, Tuple +from typing import Callable, Iterator, List, NamedTuple, Tuple from color import Color, RGB @@ -88,15 +88,35 @@ def get_cell_by_id(self, cell_id: int) -> 'TriangleCell': """Returns Cell for cell ID, or None if cell ID out of bounds.""" if not 0 <= cell_id < len(self._cells): return None + return self._cells[cell_id] - def get_pixels(self, cell_id): + def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + """Returns iterator of functions to set each Pixel's color.""" + if not 0 <= cell_id < len(self._cells): + return [] + return self._model.get_pixels(cell_id) + def set_cell_by_coordinates(self, row: int, column: int, color: Color): + """Sets cell pixels to color for given coordinates.""" + cell = self.get_cell_by_coordinates(row, column) + if cell is None: + logger.warning(f'cell coordinates (row={row}, column={column}) invalid') + return + + [pixel(color) for pixel in self._model.get_pixels(cell.id)] + def set_cell_by_id(self, cell_id: int, color: Color): + """Sets cell pixels to color for given ID.""" + if not 0 <= cell_id < len(self._cells): + logger.warning(f'cell ID {cell_id} invalid') + return + [pixel(color) for pixel in self._model.get_pixels(cell_id)] def set_all_cells(self, color: Color): + """Sets all cell to color.""" for cell in self.cells: self.set_cell_by_id(cell.id, color) diff --git a/shows/left_to_right.py b/shows/left_to_right.py index b031cb0..6808e60 100644 --- a/shows/left_to_right.py +++ b/shows/left_to_right.py @@ -9,14 +9,26 @@ def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 1.0): self.frame_delay = frame_delay def next_frame(self): + row_count = self.tri_grid.row_count + while True: - self.tri_grid.clear() + # Sequence: + # [[(row-1, 0)], + # [(row-2, 0), (row-1, 1)], + # [(row-3, 0), (row-2, 1), (row-1, 2)]], ... + for bottom_column in range(row_length(row_count)): + self.tri_grid.clear() + row_to_start = row_count - 1 - bottom_column - for row in range(self.tri_grid.row_count): - for column in range(row_length(row)): - print(f"column={column} row={row}") + for curr_column in range(bottom_column + 1): + curr_row = row_to_start + curr_column + if not 0 <= curr_row < row_count: + continue + if curr_column >= row_length(curr_row + 1): + continue - cell = self.tri_grid.get_cell_by_coordinates(row, column) - self.tri_grid.set_cell_by_id(cell.id, RGB(255, 255, 25)) + print(f"row={curr_row} column={curr_column}") + self.tri_grid.set_cell_by_coordinates(curr_row, curr_column, RGB(255, 255, 25)) + self.tri_grid.go() - yield self.frame_delay + yield self.frame_delay diff --git a/shows/left_to_right_and_back.py b/shows/left_to_right_and_back.py index 4c7d81b..801abfb 100644 --- a/shows/left_to_right_and_back.py +++ b/shows/left_to_right_and_back.py @@ -1,5 +1,3 @@ -import time - from color import RGB from .showbase import ShowBase from grid import TriangleGrid, row_length @@ -11,39 +9,33 @@ def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 1.0): self.frame_delay = frame_delay def next_frame(self): + row_count = self.tri_grid.row_count fwd = True + while True: - if fwd: + # Forward sequence: + # [[(row-1, 0)], + # [(row-2, 0), (row-1, 1)], + # [(row-3, 0), (row-2, 1), (row-1, 2)]], ... + bottom_column_sequence = range(row_length(row_count)) + if not fwd: + bottom_column_sequence = reversed(bottom_column_sequence) + + for bottom_column in bottom_column_sequence: self.tri_grid.clear() + row_to_start = row_count - 1 - bottom_column + + for curr_column in range(bottom_column + 1): + curr_row = row_to_start + curr_column + if not 0 <= curr_row < row_count: + continue + if curr_column >= row_length(curr_row + 1): + continue + + print(f'row={curr_row} column={curr_column}') + self.tri_grid.set_cell_by_coordinates(curr_row, curr_column, RGB(255, 255, 25)) + self.tri_grid.go() + + yield self.frame_delay - for row in range(self.tri_grid.row_count): - for column in range(row_length(row)): - cell = self.tri_grid.get_cell_by_coordinates(row, column) - - r = 255 - g = 0 - for pixel in self.tri_grid.get_pixels(cell.id): - pixel(RGB(r, g, 0)) - time.sleep(.2) - self.tri_grid.go() - g += 40 - r -= 3 - - fwd = False - - else: - for row in reversed(range(self.tri_grid.row_count)): - for column in range(row_length(row)): - cell = self.tri_grid.get_cell_by_coordinates(row, column) - - g = 255 - b = 0 - for pixel in self.tri_grid.get_pixels(cell.id): - pixel(RGB(0, g, b)) - time.sleep(.2) - self.tri_grid.go() - g -= 40 - b += 40 - fwd = True - - yield self.frame_delay + fwd = not fwd # Flips direction From 4d6b6604c9c8c1221af0e66e624448d502d6448b Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Wed, 24 Jul 2019 14:07:17 -0700 Subject: [PATCH 23/60] Introduce traversals --- grid/traversal.py | 49 +++++++++++++++++++++++++++++++++ shows/left_to_right.py | 23 ++++------------ shows/left_to_right_and_back.py | 32 ++++++++------------- tests/test_traversal.py | 19 +++++++++++++ 4 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 grid/traversal.py create mode 100644 tests/test_traversal.py diff --git a/grid/traversal.py b/grid/traversal.py new file mode 100644 index 0000000..e4467c1 --- /dev/null +++ b/grid/traversal.py @@ -0,0 +1,49 @@ +from typing import Iterator, List, Tuple + +from .grid import row_length + + +def left_to_right(row_count: int) -> Iterator[List[Tuple[int, int]]]: + """Generates a left-to-right vertical sequence of coordinates.""" + if not row_count > 0: + raise ValueError(f'traversal requires row_count({row_count}) > 0') + + # Forward sequence: + # [[(row-1, 0)], + # [(row-2, 0), (row-1, 1)], + # [(row-3, 0), (row-2, 1), (row-1, 2)]], ... + for bottom_column in range(row_length(row_count)): + row_to_start = row_count - 1 - bottom_column + + coordinates = [] + for curr_column in range(bottom_column + 1): + curr_row = row_to_start + curr_column + if not 0 <= curr_row < row_count: + continue + if curr_column >= row_length(curr_row + 1): + continue + + coordinates.append((curr_row, curr_column)) + + yield coordinates + + +def right_to_left(row_count: int) -> Iterator[List[Tuple[int, int]]]: + """Generates a right-to-left vertical sequence of coordinates.""" + if not row_count > 0: + raise ValueError(f'traversal requires row_count({row_count}) > 0') + + for bottom_column in reversed(range(row_length(row_count))): + row_to_start = row_count - 1 - bottom_column + + coordinates = [] + for curr_column in range(bottom_column + 1): + curr_row = row_to_start + curr_column + if not 0 <= curr_row < row_count: + continue + if curr_column >= row_length(curr_row + 1): + continue + + coordinates.append((curr_row, curr_column)) + + yield coordinates diff --git a/shows/left_to_right.py b/shows/left_to_right.py index 6808e60..89c0aa6 100644 --- a/shows/left_to_right.py +++ b/shows/left_to_right.py @@ -1,6 +1,6 @@ from color import RGB from .showbase import ShowBase -from grid import TriangleGrid, row_length +from grid import TriangleGrid, traversal class LeftToRight(ShowBase): @@ -12,23 +12,12 @@ def next_frame(self): row_count = self.tri_grid.row_count while True: - # Sequence: - # [[(row-1, 0)], - # [(row-2, 0), (row-1, 1)], - # [(row-3, 0), (row-2, 1), (row-1, 2)]], ... - for bottom_column in range(row_length(row_count)): + for points in traversal.left_to_right(row_count): self.tri_grid.clear() - row_to_start = row_count - 1 - bottom_column - for curr_column in range(bottom_column + 1): - curr_row = row_to_start + curr_column - if not 0 <= curr_row < row_count: - continue - if curr_column >= row_length(curr_row + 1): - continue - - print(f"row={curr_row} column={curr_column}") - self.tri_grid.set_cell_by_coordinates(curr_row, curr_column, RGB(255, 255, 25)) - self.tri_grid.go() + for (row, column) in points: + print(f"row={row} column={column}") + self.tri_grid.set_cell_by_coordinates(row, column, RGB(255, 255, 25)) + self.tri_grid.go() yield self.frame_delay diff --git a/shows/left_to_right_and_back.py b/shows/left_to_right_and_back.py index 801abfb..fcbde5b 100644 --- a/shows/left_to_right_and_back.py +++ b/shows/left_to_right_and_back.py @@ -1,6 +1,6 @@ from color import RGB from .showbase import ShowBase -from grid import TriangleGrid, row_length +from grid import TriangleGrid, traversal class LeftToRightAndBack(ShowBase): @@ -13,29 +13,19 @@ def next_frame(self): fwd = True while True: - # Forward sequence: - # [[(row-1, 0)], - # [(row-2, 0), (row-1, 1)], - # [(row-3, 0), (row-2, 1), (row-1, 2)]], ... - bottom_column_sequence = range(row_length(row_count)) - if not fwd: - bottom_column_sequence = reversed(bottom_column_sequence) - - for bottom_column in bottom_column_sequence: - self.tri_grid.clear() - row_to_start = row_count - 1 - bottom_column + if fwd: + sequence = traversal.left_to_right(row_count) + else: + sequence = traversal.right_to_left(row_count) - for curr_column in range(bottom_column + 1): - curr_row = row_to_start + curr_column - if not 0 <= curr_row < row_count: - continue - if curr_column >= row_length(curr_row + 1): - continue + for points in sequence: + self.tri_grid.clear() - print(f'row={curr_row} column={curr_column}') - self.tri_grid.set_cell_by_coordinates(curr_row, curr_column, RGB(255, 255, 25)) - self.tri_grid.go() + for (row, column) in points: + print(f'row={row} column={column}') + self.tri_grid.set_cell_by_coordinates(row, column, RGB(255, 255, 25)) + self.tri_grid.go() yield self.frame_delay fwd = not fwd # Flips direction diff --git a/tests/test_traversal.py b/tests/test_traversal.py new file mode 100644 index 0000000..c9069df --- /dev/null +++ b/tests/test_traversal.py @@ -0,0 +1,19 @@ +from grid import traversal + + +def test_left_to_right(): + sequence = list(traversal.left_to_right(1)) + assert [[(0, 0)]] == sequence + + sequence = list(traversal.left_to_right(2)) + assert [[(1, 0)], [(0, 0), (1, 1)], [(1, 2)]] == sequence + + sequence = list(traversal.left_to_right(3)) + assert [[(2, 0)], [(1, 0), (2, 1)], [(0, 0), (1, 1), (2, 2)], [(1, 2), (2, 3)], [(2, 4)]] == sequence + + +def test_right_to_left(): + for rows in range(1, 12): + sequence = list(traversal.right_to_left(rows)) + expected = list(reversed(list(traversal.left_to_right(rows)))) + assert expected == sequence From a1192c67e721260221c19ba2bc5c5ae317496a52 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Wed, 24 Jul 2019 14:12:05 -0700 Subject: [PATCH 24/60] Clearer coords on Cell --- grid/grid.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/grid/grid.py b/grid/grid.py index e39711c..01d6a28 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -214,31 +214,27 @@ class TriangleCell(NamedTuple): num_rows: int @property - def row(self) -> int: - """Returns zero-indexed row containing of the cell.""" - (row, column) = TriangleGrid.id_to_coordinates(self.id) - return row - - @property - def row_position(self) -> int: - """Returns zero-indexed position of the cell within a triangle row.""" - (row, column) = TriangleGrid.id_to_coordinates(self.id) - return column + def coordinates(self) -> Tuple[int, int]: + """Returns zero-indexed (row, column) containing the cell.""" + return TriangleGrid.id_to_coordinates(self.id) @property def is_left_edge(self) -> bool: """Returns True if cell is along the left edge of the greater triangle.""" - return self.row_position == 0 + (row, column) = TriangleGrid.id_to_coordinates(self.id) + return column == 0 @property def is_right_edge(self) -> bool: """Returns True if cell is along the right edge of the greater triangle.""" - return self.row_position == row_length(self.row + 1) - 1 + (row, column) = self.coordinates + return column + 1 == row_length(row + 1) @property def is_bottom_edge(self) -> bool: """Returns True if cell is along the bottom edge of the greater triangle.""" - return self.row + 1 == self.num_rows and self.is_up + (row, column) = self.coordinates + return row + 1 == self.num_rows and self.is_up @property def is_top_corner(self) -> bool: @@ -258,7 +254,8 @@ def is_left_corner(self) -> bool: @property def is_up(self) -> bool: """Returns True if triangle shaped cell orients with a corner upward.""" - return self.row_position % 2 == 0 + (row, column) = TriangleGrid.id_to_coordinates(self.id) + return column % 2 == 0 @property def is_down(self) -> bool: From daf54d7cc5dc772d49d5d7b41f8827751584e90d Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Wed, 24 Jul 2019 14:24:51 -0700 Subject: [PATCH 25/60] More type hints --- grid/grid.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/grid/grid.py b/grid/grid.py index 01d6a28..88e8f36 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -1,8 +1,9 @@ from functools import lru_cache import logging -from typing import Callable, Iterator, List, NamedTuple, Tuple +from typing import Callable, Iterator, List, NamedTuple, Tuple, Type from color import Color, RGB +from model import ModelBase logger = logging.getLogger('pyramidtriangles') @@ -20,7 +21,7 @@ def row_length(n: int) -> int: class TriangleGrid(object): - def __init__(self, model, row_count): + def __init__(self, model: Type[ModelBase], row_count: int): if row_count < 1: raise ValueError(f'n_rows={row_count} must be positive') @@ -263,6 +264,6 @@ def is_down(self) -> bool: return not self.is_up -def make_triangle(model, row_count) -> TriangleGrid: +def make_triangle(model: Type[ModelBase], row_count: int) -> TriangleGrid: """Helper function to build a Triangle with row_count rows.""" return TriangleGrid(model, row_count) From b8b8601ed9f11d6eb2ba9c1278bd3e8e5746fbd0 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Wed, 24 Jul 2019 14:25:46 -0700 Subject: [PATCH 26/60] Remove unnecessary memoization --- grid/grid.py | 1 - 1 file changed, 1 deletion(-) diff --git a/grid/grid.py b/grid/grid.py index 88e8f36..c03aa9f 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -8,7 +8,6 @@ logger = logging.getLogger('pyramidtriangles') -@lru_cache(maxsize=128) def triangular_number(n: int) -> int: """Returns the number of elements in an equilateral triangle of n rows.""" # Typically the triangle number is (n(n+1))/2 but our triangle has rows of 1, 3, 5... From 39b1737066a1e5fe28cdf5348909d3db9d2a7cb9 Mon Sep 17 00:00:00 2001 From: John Major Date: Thu, 25 Jul 2019 01:09:23 -0700 Subject: [PATCH 27/60] correct cell mapping for 2 row triangle wirred from bottom left. We might want to reconsdier the grid mapping being used to be more flexible to allow modeling multiple triangle grids placed apart from each other more naturally. --- go_tri.py | 2 +- model/mapping.py | 10 ++++++++++ model/sacn_model.py | 2 +- shows/one_by_one.py | 2 +- shows/random_cells.py | 2 ++ 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/go_tri.py b/go_tri.py index f4d9fe0..fa1c45d 100644 --- a/go_tri.py +++ b/go_tri.py @@ -329,7 +329,7 @@ def go_web(self): logger.info("Starting SACN") model = sACN(model_json="./data/pixel_map.json", pixelmap=demo_triangle_mapping()) - triangle_grid = grid.make_triangle(model, 3) + triangle_grid = grid.make_triangle(model, 2) app = TriangleServer(triangle_grid, args) try: diff --git a/model/mapping.py b/model/mapping.py index ed21a11..2076a01 100644 --- a/model/mapping.py +++ b/model/mapping.py @@ -4,6 +4,16 @@ # This cell -> pixels mapping is created from an old version, but it should produce something. def demo_triangle_mapping() -> PixelMap: + return { + #Wiring coming in from the bottom left, this is the order of cells following the pixels + 1: [ 2, 3, 4, 5, 6, 7, 8], + 3: [ 9, 10, 11, 12, 13, 14, 15, 16], + 2: [ 26, 27, 28, 29, 30, 31, 32, 33], + 0: [ 34, 35, 36, 37, 38, 39, 40, 41] # the last cell should the the top most cell of the triangle grid + } + + +def demo_triangle_mapping_old() -> PixelMap: return { 0: [73, 72, 71, 70, 69, 68], 1: [52, 51, 50, 49, 48, 47], diff --git a/model/sacn_model.py b/model/sacn_model.py index 703b152..e04f8e1 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -29,7 +29,7 @@ def __init__(self, model_json: str, pixelmap: PixelMap): self.leds = {} self._map_leds(model_json) self._pixelmap = pixelmap - +# from IPython import embed; embed() # Keys for LEDs are integers representing universes, each universe has an array of possible DMX channels # Pixels are an LED represented by 4 DMX addresses diff --git a/shows/one_by_one.py b/shows/one_by_one.py index 00f59d0..1376ae5 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -4,7 +4,7 @@ class OneByOne(ShowBase): - def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.25): + def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.9): self.tri_grid = tri_grid self.frame_delay = frame_delay diff --git a/shows/random_cells.py b/shows/random_cells.py index 284d67d..d698aca 100644 --- a/shows/random_cells.py +++ b/shows/random_cells.py @@ -10,6 +10,8 @@ def __init__(self, tri_grid, frame_delay=0.1): self.n_cells = len(self.tri_grid.cells) + from IPython import embed; embed() + def next_frame(self): while True: From 90624acc4151b1ae3d737e1b72bc7268e0649d65 Mon Sep 17 00:00:00 2001 From: John Major Date: Thu, 25 Jul 2019 02:25:33 -0700 Subject: [PATCH 28/60] Added some object setters, added hex finding (it's clunky w/out the 2d grid...), added marching hex show and fixed the pixel mappings for the cells) --- grid/grid.py | 57 ++++++++++++++++++++++++++++++++++++++++- model/mapping.py | 8 +++--- model/sacn_model.py | 2 +- shows/__init__.py | 1 + shows/marching_hexes.py | 30 ++++++++++++++++++++++ 5 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 shows/marching_hexes.py diff --git a/grid/grid.py b/grid/grid.py index c03aa9f..0a096a6 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -112,9 +112,24 @@ def set_cell_by_id(self, cell_id: int, color: Color): if not 0 <= cell_id < len(self._cells): logger.warning(f'cell ID {cell_id} invalid') return - [pixel(color) for pixel in self._model.get_pixels(cell_id)] + + def set_cell(self, cell, color: Color): + "Set cell pixels to a color" + if cell is not None: + if not 0 <= cell.id < len(self._cells): + logger.warning(f'cell ID {cell_id} invalid') + return + [pixel(color) for pixel in self._model.get_pixels(cell.id)] + + + def set_cells(self, cells: List['TriangleCell'], color: Color): + "set a list of cells to a color" + for cell in cells: + self.set_cell(cell, color) + + def set_all_cells(self, color: Color): """Sets all cell to color.""" for cell in self.cells: @@ -208,6 +223,46 @@ def vertex_neighbors_by_cell(self, cell_id: int) -> Tuple['TriangleCell', 'Trian return self.vertex_neighbors_by_coord(*self.id_to_coordinates(cell_id)) + def hexagon_from_btm_cell_by_coords(self, row: int, col: int) -> Tuple['TriangleCell','TriangleCell','TriangleCell','TriangleCell','TriangleCell','TriangleCell']: + "Given the coordinates of an up facing cell, return the surrounding cells which will make " + "A hexagon with the specified cell as the base" + "the tuple will begin with the btm upward facing triangle at the base of the hex" + "the next cell in the tuple will be the next triangle in the hexagon moving counterclockwise" + "None will be entered for cells off the grid" + + btm_cell = self.get_cell_by_coordinates(row, col) + a,b,c,d,e,f = btm_cell, None, None, None, None, None + if a.is_left_edge: + b = self.edge_neighbors_by_cell(a.id)[2] + if b is not None: + c = self.edge_neighbors_by_cell(b.id)[1] + if c is not None: + d = self.edge_neighbors_by_cell(c.id)[0] + if d is not None: + e = self.edge_neighbors_by_cell(d.id)[0] + if e is not None: + f = self.edge_neighbors_by_cell(e.id)[1] + elif a.is_right_edge: + f = self.edge_neighbors_by_cell(a.id)[0] + if f is not None: + e = self.edge_neighbors_by_cell(f.id)[1] + if e is not None: + d = self.edge_neighbors_by_cell(e.id)[2] + if d is not None: + c = self.edge_neighbors_by_cell(d.id)[2] + if c is not None: + b = self.edge_neighbors_by_cell(c.id)[1] + else: + b = self.edge_neighbors_by_cell(a.id)[2] + c = self.edge_neighbors_by_cell(b.id)[1] + d = self.edge_neighbors_by_cell(c.id)[0] + e = self.edge_neighbors_by_cell(d.id)[0] + f = self.edge_neighbors_by_cell(e.id)[1] + + return (a,b,c,d,e,f) + + + class TriangleCell(NamedTuple): """A Cell contains multiple LED pixels.""" id: int diff --git a/model/mapping.py b/model/mapping.py index 2076a01..7fb2b33 100644 --- a/model/mapping.py +++ b/model/mapping.py @@ -6,10 +6,10 @@ def demo_triangle_mapping() -> PixelMap: return { #Wiring coming in from the bottom left, this is the order of cells following the pixels - 1: [ 2, 3, 4, 5, 6, 7, 8], - 3: [ 9, 10, 11, 12, 13, 14, 15, 16], - 2: [ 26, 27, 28, 29, 30, 31, 32, 33], - 0: [ 34, 35, 36, 37, 38, 39, 40, 41] # the last cell should the the top most cell of the triangle grid + 1: [ 2, 3, 4, 5, 6, 7, 8, 9], + 3: [ 10, 11, 12, 13, 14, 15, 16, 17], + 2: [ 27, 28, 29, 30, 31, 32, 33, 34], + 0: [ 35, 36, 37, 38, 39, 40, 41, 42] # the last cell should the the top most cell of the triangle grid } diff --git a/model/sacn_model.py b/model/sacn_model.py index e04f8e1..2e7b887 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -58,7 +58,7 @@ def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: logger.warning(f'{pixel} not in sACN pixel ID map') ux = self.PIXEL_MAP[pixel][0] - ix = self.PIXEL_MAP[pixel][1] - 1 # dmx is 1-based, python lists are 0-based + ix = self.PIXEL_MAP[pixel][1] -1 # dmx is 1-based, python lists are 0-based def set_color(color): self.leds[ux][ix] = color.r diff --git a/shows/__init__.py b/shows/__init__.py index 8d42a22..3238131 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -7,3 +7,4 @@ from .up_down import UpDown from .cycle_hsv import CycleHSV from .strobe import Strobe +from .marching_hexes import MarchingHexes diff --git a/shows/marching_hexes.py b/shows/marching_hexes.py new file mode 100644 index 0000000..164b4e3 --- /dev/null +++ b/shows/marching_hexes.py @@ -0,0 +1,30 @@ +from .showbase import ShowBase +from color import HSV +import random as rnd +import time + +class MarchingHexes(ShowBase): + def __init__(self, tri_grid, frame_delay=0.1): + self.tri_grid = tri_grid + self.frame_delay = frame_delay + + self.n_cells = len(self.tri_grid.cells) + + def next_frame(self): + hsv = HSV(1.0,.9,.5) + while True: + self.tri_grid.clear() + for cell in self.tri_grid.cells: + if cell.is_up: + self.tri_grid.set_cells(self.tri_grid.hexagon_from_btm_cell_by_coords(cell.coordinates[0], cell.coordinates[1]), hsv) + self.tri_grid.go() + time.sleep(1) + + if hsv.h >= 1.0: + hsv.h = 0.0 + else: + hsv.h += 0.2 + + + + yield self.frame_delay From 9c187c03c3994f9256a3292f40d07aa1b324ad5d Mon Sep 17 00:00:00 2001 From: John Major Date: Thu, 25 Jul 2019 02:51:12 -0700 Subject: [PATCH 29/60] remove embed from being triggered --- shows/random_cells.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shows/random_cells.py b/shows/random_cells.py index d698aca..e29cb7a 100644 --- a/shows/random_cells.py +++ b/shows/random_cells.py @@ -10,7 +10,7 @@ def __init__(self, tri_grid, frame_delay=0.1): self.n_cells = len(self.tri_grid.cells) - from IPython import embed; embed() +# from IPython import embed; embed() def next_frame(self): From 8a2397be9e2592bc908dd283d258bba99e352ceb Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Thu, 25 Jul 2019 14:18:23 -0700 Subject: [PATCH 30/60] Rename pixel setter to be clearer --- grid/grid.py | 23 +++++++++++++++-------- model/__init__.py | 2 +- model/mirror.py | 8 ++++---- model/modelbase.py | 13 +++++++++++-- model/ola_model.py | 7 +++---- model/sacn_model.py | 7 +++---- model/simulator.py | 7 ++++--- shows/one_by_one.py | 15 ++++++--------- tests/test_grid.py | 2 +- 9 files changed, 48 insertions(+), 36 deletions(-) diff --git a/grid/grid.py b/grid/grid.py index 0a096a6..b75a1c7 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -1,9 +1,9 @@ from functools import lru_cache import logging -from typing import Callable, Iterator, List, NamedTuple, Tuple, Type +from typing import Iterator, List, NamedTuple, Tuple, Type from color import Color, RGB -from model import ModelBase +from model import ModelBase, SetColorFunc logger = logging.getLogger('pyramidtriangles') @@ -91,12 +91,19 @@ def get_cell_by_id(self, cell_id: int) -> 'TriangleCell': return self._cells[cell_id] - def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: - """Returns iterator of functions to set each Pixel's color.""" + def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: + """ + Returns an iterator to set each Pixel's color within the given cell ID. + + Example: + for pixel in grid.set_pixels_by_cell_id(0): + pixel(color) + time.sleep(0.1) + """ if not 0 <= cell_id < len(self._cells): return [] - return self._model.get_pixels(cell_id) + return self._model.set_pixels_by_cellid(cell_id) def set_cell_by_coordinates(self, row: int, column: int, color: Color): """Sets cell pixels to color for given coordinates.""" @@ -105,14 +112,14 @@ def set_cell_by_coordinates(self, row: int, column: int, color: Color): logger.warning(f'cell coordinates (row={row}, column={column}) invalid') return - [pixel(color) for pixel in self._model.get_pixels(cell.id)] + [pixel(color) for pixel in self._model.set_pixels_by_cellid(cell.id)] def set_cell_by_id(self, cell_id: int, color: Color): """Sets cell pixels to color for given ID.""" if not 0 <= cell_id < len(self._cells): logger.warning(f'cell ID {cell_id} invalid') return - [pixel(color) for pixel in self._model.get_pixels(cell_id)] + [pixel(color) for pixel in self._model.set_pixels_by_cellid(cell_id)] def set_cell(self, cell, color: Color): @@ -121,7 +128,7 @@ def set_cell(self, cell, color: Color): if not 0 <= cell.id < len(self._cells): logger.warning(f'cell ID {cell_id} invalid') return - [pixel(color) for pixel in self._model.get_pixels(cell.id)] + [pixel(color) for pixel in self._model.set_pixels_by_cellid(cell.id)] def set_cells(self, cells: List['TriangleCell'], color: Color): diff --git a/model/__init__.py b/model/__init__.py index 8d78e86..a9adbdd 100644 --- a/model/__init__.py +++ b/model/__init__.py @@ -1,6 +1,6 @@ # These imports include submodules under the `model` namespace (e.g. model.SimulatorModel is available). from .mapping import demo_triangle_mapping -from .modelbase import ModelBase +from .modelbase import ModelBase, SetColorFunc from .mirror import MirrorModel from .ola_model import OLAModel from .sacn_model import sACN diff --git a/model/mirror.py b/model/mirror.py index 84b6734..b05e90d 100644 --- a/model/mirror.py +++ b/model/mirror.py @@ -4,10 +4,10 @@ Completely agnostic as to what the cell ids look like, they are just passed through to the underlying model. """ from itertools import zip_longest -from typing import Callable, Iterator +from typing import Iterator from color import Color -from .modelbase import ModelBase +from .modelbase import ModelBase, SetColorFunc class MirrorModel(ModelBase): @@ -20,8 +20,8 @@ def __init__(self, *models): def add_model(self, model): self.models.append(model) - def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: - generators = zip_longest([model.get_pixels(cell_id) for model in self.models]) + def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: + generators = zip_longest([model.set_pixels_by_cellid(cell_id) for model in self.models]) def set_color(color: Color): pixels = next(generators) diff --git a/model/modelbase.py b/model/modelbase.py index cb52e76..bb65627 100644 --- a/model/modelbase.py +++ b/model/modelbase.py @@ -2,13 +2,22 @@ from color import Color from typing import Callable, Iterator +SetColorFunc = Callable[[Color], None] + class ModelBase(ABC): """Abstract base class for simulators.""" @abstractmethod - def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: - """Returns iterator for Pixel setting functions in a Cell""" + def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: + """ + Returns an iterator to set each Pixel's color within the given cell ID. + + Example: + for pixel in grid.set_pixels_by_cell_id(0): + pixel(color) + time.sleep(0.1) + """ @abstractmethod def go(self): diff --git a/model/ola_model.py b/model/ola_model.py index 1bcf17a..32879f5 100644 --- a/model/ola_model.py +++ b/model/ola_model.py @@ -8,12 +8,11 @@ import array import json import logging -from typing import Iterator, Callable +from typing import Iterator -from color import Color import ola from .mapping import PixelMap -from .modelbase import ModelBase +from .modelbase import ModelBase, SetColorFunc logger = logging.getLogger("pyramidtriangles") @@ -48,7 +47,7 @@ def _map_leds(self, f): self.PIXEL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) # Model basics - def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: for pixel in self._pixelmap[cell_id]: if pixel not in self.PIXEL_MAP: logger.warning(f'{pixel} not in sACN pixel ID map') diff --git a/model/sacn_model.py b/model/sacn_model.py index 2e7b887..ca05991 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -6,12 +6,11 @@ """ import json import logging -from typing import Callable, Iterator +from typing import Iterator -from color import Color import sacn from .mapping import PixelMap -from .modelbase import ModelBase +from .modelbase import ModelBase, SetColorFunc logger = logging.getLogger("pyramidtriangles") @@ -52,7 +51,7 @@ def _map_leds(self, f): self.sender[universe].multicast = True self.leds[universe] = [0] * 512 - def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: for pixel in self._pixelmap[cell_id]: if pixel not in self.PIXEL_MAP: logger.warning(f'{pixel} not in sACN pixel ID map') diff --git a/model/simulator.py b/model/simulator.py index 7888206..fe1a096 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -6,10 +6,10 @@ import logging import socket import json -from typing import Callable, Iterator +from typing import Iterator from color import Color -from .modelbase import ModelBase +from .modelbase import ModelBase, SetColorFunc SIM_DEFAULT = (188, 210, 229) # BCD2E5, "off" color for simulator logger = logging.getLogger("pyramidtriangles") @@ -40,9 +40,10 @@ def _map_leds(self, f): self.CELL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) # Model basics - def get_pixels(self, cell_id: int) -> Iterator[Callable[[Color], None]]: + def set_pixels_by_cellid(self, cell_id: int) -> Iterator[SetColorFunc]: def set_color(color: Color): self.set_cell(cell_id, color) + # Iterator of one for the simulator, which doesn't have pixels within a cell. return iter([set_color]) def set_cell(self, cell: int, color: Color): diff --git a/shows/one_by_one.py b/shows/one_by_one.py index 1376ae5..475ea86 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -11,15 +11,12 @@ def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.9): def next_frame(self): ncells = len(self.tri_grid.cells) - 1 self.tri_grid.clear() - cell_n = 0 while True: - self.tri_grid.clear() - print(cell_n) - self.tri_grid.set_cell_by_id(cell_n, RGB(255, 255, 25)) + for cell in range(ncells): + self.tri_grid.clear() + print(cell) + for pixel in self.tri_grid.set_pixels_by_cellid(cell): + pixel(RGB(255, 255, 25)) - if cell_n >= ncells: - cell_n = -1 - cell_n += 1 - - yield self.frame_delay + yield self.frame_delay diff --git a/tests/test_grid.py b/tests/test_grid.py index a1b4733..699bdc4 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -6,7 +6,7 @@ class FakeModel(ModelBase): - def get_pixels(self, cell_id) -> Iterator[Callable[[Color], None]]: + def set_pixels_by_cellid(self, cell_id) -> Iterator[Callable[[Color], None]]: pass def go(self): From 99b5eca9f4f29f8975ea45b3558023939799032c Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Thu, 25 Jul 2019 23:30:30 -0700 Subject: [PATCH 31/60] Warp show --- grid/traversal.py | 24 +++++++++++++++++++++++- shows/__init__.py | 1 + shows/warp.py | 22 ++++++++++++++++++++++ tests/test_traversal.py | 8 ++++---- 4 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 shows/warp.py diff --git a/grid/traversal.py b/grid/traversal.py index e4467c1..8dd0294 100644 --- a/grid/traversal.py +++ b/grid/traversal.py @@ -1,10 +1,11 @@ -from typing import Iterator, List, Tuple +from typing import Iterable, Iterator, List, Tuple from .grid import row_length def left_to_right(row_count: int) -> Iterator[List[Tuple[int, int]]]: """Generates a left-to-right vertical sequence of coordinates.""" + if not row_count > 0: raise ValueError(f'traversal requires row_count({row_count}) > 0') @@ -30,6 +31,7 @@ def left_to_right(row_count: int) -> Iterator[List[Tuple[int, int]]]: def right_to_left(row_count: int) -> Iterator[List[Tuple[int, int]]]: """Generates a right-to-left vertical sequence of coordinates.""" + if not row_count > 0: raise ValueError(f'traversal requires row_count({row_count}) > 0') @@ -47,3 +49,23 @@ def right_to_left(row_count: int) -> Iterator[List[Tuple[int, int]]]: coordinates.append((curr_row, curr_column)) yield coordinates + + +def concentric(row_count: int) -> Iterator[Iterable[Tuple[int, int]]]: + """Concentric traversal out to in.""" + + if not row_count > 0: + raise ValueError(f'traversal requires row_count({row_count}) > 0') + + for i in range(row_count): + # Accumulate for bottom row + bottom_row = row_count - 1 - i + bottoms = {(bottom_row, col) for col in range(row_length(bottom_row))} + + # Accumulate i-th column of each row + lefts = {(row, i) for row in range(row_count)} + + # Accumulate (n-i)th column of each row + rights = {(row, bottom_row) for row in range(row_count)} + + yield set().union(bottoms, lefts, rights) diff --git a/shows/__init__.py b/shows/__init__.py index 3238131..bff483c 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -8,3 +8,4 @@ from .cycle_hsv import CycleHSV from .strobe import Strobe from .marching_hexes import MarchingHexes +from .warp import Warp diff --git a/shows/warp.py b/shows/warp.py new file mode 100644 index 0000000..8d145c2 --- /dev/null +++ b/shows/warp.py @@ -0,0 +1,22 @@ +from color import HSV +from .showbase import ShowBase +from grid import TriangleGrid, traversal + + +class Warp(ShowBase): + def __init__(self, grid: TriangleGrid, frame_delay: float = 0.2): + self.grid = grid + self.frame_delay = frame_delay + + def next_frame(self): + row_count = self.grid.row_count + + while True: + for points in traversal.concentric(row_count): + self.grid.clear() + + for (row, column) in points: + self.grid.set_cell_by_coordinates(row, column, HSV(.1, .5, .9)) + + self.grid.go() + yield self.frame_delay diff --git a/tests/test_traversal.py b/tests/test_traversal.py index c9069df..678c411 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -3,17 +3,17 @@ def test_left_to_right(): sequence = list(traversal.left_to_right(1)) - assert [[(0, 0)]] == sequence + assert sequence == [[(0, 0)]] sequence = list(traversal.left_to_right(2)) - assert [[(1, 0)], [(0, 0), (1, 1)], [(1, 2)]] == sequence + assert sequence == [[(1, 0)], [(0, 0), (1, 1)], [(1, 2)]] sequence = list(traversal.left_to_right(3)) - assert [[(2, 0)], [(1, 0), (2, 1)], [(0, 0), (1, 1), (2, 2)], [(1, 2), (2, 3)], [(2, 4)]] == sequence + assert sequence == [[(2, 0)], [(1, 0), (2, 1)], [(0, 0), (1, 1), (2, 2)], [(1, 2), (2, 3)], [(2, 4)]] def test_right_to_left(): for rows in range(1, 12): sequence = list(traversal.right_to_left(rows)) expected = list(reversed(list(traversal.left_to_right(rows)))) - assert expected == sequence + assert sequence == expected From 6cd98ee74e14316197867e90295ea2890e30ea67 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Fri, 26 Jul 2019 00:29:20 -0700 Subject: [PATCH 32/60] Fix Warp show --- grid/traversal.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/grid/traversal.py b/grid/traversal.py index 8dd0294..7ace895 100644 --- a/grid/traversal.py +++ b/grid/traversal.py @@ -57,15 +57,31 @@ def concentric(row_count: int) -> Iterator[Iterable[Tuple[int, int]]]: if not row_count > 0: raise ValueError(f'traversal requires row_count({row_count}) > 0') - for i in range(row_count): - # Accumulate for bottom row + for i in range(row_count // 2): bottom_row = row_count - 1 - i - bottoms = {(bottom_row, col) for col in range(row_length(bottom_row))} + # Increases 0, 2, 4,... + left_column = i * 2 - # Accumulate i-th column of each row - lefts = {(row, i) for row in range(row_count)} + # Accumulate for bottom row + bottoms = [] + for col in range(left_column, row_length(bottom_row + 1) - left_column): + bottoms.append((bottom_row, col)) + # Accumulate i-th column of each row + lefts = [] # Accumulate (n-i)th column of each row - rights = {(row, bottom_row) for row in range(row_count)} + rights = [] + + for row in range(left_column, bottom_row): + # Decreases last, last - 2, last - 4,... + right_column = row_length(row + 1) - 1 - left_column + + if left_column <= right_column: + lefts.append((row, left_column)) + rights.append((row, right_column)) + + if left_column + 1 <= right_column: + lefts.append((row, left_column + 1)) + rights.append((row, right_column - 1)) yield set().union(bottoms, lefts, rights) From 377157980f6da6ddb26b00c803e278e9d44c77b7 Mon Sep 17 00:00:00 2001 From: John Major Date: Fri, 26 Jul 2019 02:13:22 -0700 Subject: [PATCH 33/60] trying to sweep left to right pixel lighting --- color.py | 31 ++++++++++++++------- model/mapping.py | 14 +--------- shows/cycle_hsv.py | 60 ++++++++++++++++++++++++++++++++++++++--- shows/left_to_right.py | 44 ++++++++++++++++++++++-------- shows/marching_hexes.py | 2 +- shows/one_by_one.py | 5 ++-- shows/strobe.py | 8 +++--- 7 files changed, 120 insertions(+), 44 deletions(-) diff --git a/color.py b/color.py index 4602ea6..e1e4d0b 100644 --- a/color.py +++ b/color.py @@ -310,15 +310,15 @@ def HSI(h,s,i): assert is_hsi_hsl_tuple(t) return RGB( hsi2rgb(h,s,i) ) -def RGB(r,g,b): +def RGB(r,g,b, x=False): "Create a new RGB color" t = (r,g,b) assert is_rgb_tuple(t) - return Color(rgb_to_hsv(t)) + return Color(rgb_to_hsv(t), x) -def HSV(h,s,v): +def HSV(h,s,v, x=False): "Create a new HSV color" - return Color((h,s,v)) + return Color((h,s,v),x) def HSL(h,s,l): "Create new HSL color" @@ -336,8 +336,9 @@ def Hex(value): return RGB(*rgb_t) class Color(object): - def __init__(self, hsv_tuple): + def __init__(self, hsv_tuple, only_rgb=False): self._set_hsv(hsv_tuple) + self.only_rgb = only_rgb def __repr__(self): return "rgb=%s hsv=%s" % (self.rgb, self.hsv) @@ -458,19 +459,31 @@ def rgb_b(self, val): """ @property def r(self): - return self.rgbw[0] + if self.only_rgb: + return self.rgb[0] + else: + return self.rgbw[0] @property def g(self): - return self.rgbw[1] + if self.only_rgb: + return self.rgb[1] + else: + return self.rgbw[1] @property def b(self): - return self.rgbw[2] + if self.only_rgb: + return self.rgb[2] + else: + return self.rgbw[2] @property def w(self): - return self.rgbw[3] + if self.only_rgb: + return 0 + else: + return self.rgbw[3] if __name__=='__main__': import doctest diff --git a/model/mapping.py b/model/mapping.py index 7fb2b33..9eaa2cf 100644 --- a/model/mapping.py +++ b/model/mapping.py @@ -8,20 +8,8 @@ def demo_triangle_mapping() -> PixelMap: #Wiring coming in from the bottom left, this is the order of cells following the pixels 1: [ 2, 3, 4, 5, 6, 7, 8, 9], 3: [ 10, 11, 12, 13, 14, 15, 16, 17], - 2: [ 27, 28, 29, 30, 31, 32, 33, 34], + 2: [ 34, 33, 32, 31, 30, 29,28, 27 ], 0: [ 35, 36, 37, 38, 39, 40, 41, 42] # the last cell should the the top most cell of the triangle grid } -def demo_triangle_mapping_old() -> PixelMap: - return { - 0: [73, 72, 71, 70, 69, 68], - 1: [52, 51, 50, 49, 48, 47], - 2: [65, 64, 63, 62, 61, 60], - 3: [46, 45, 44, 43, 42, 41], - 4: [19, 18, 17, 16, 15, 14], - 5: [27, 28, 29, 30, 31, 32], - 6: [13, 12, 11, 10, 9, 8], - 7: [33, 34, 35, 36, 37, 38], - 8: [7, 6, 5, 4, 3, 2] - } diff --git a/shows/cycle_hsv.py b/shows/cycle_hsv.py index f799dc5..aab4956 100644 --- a/shows/cycle_hsv.py +++ b/shows/cycle_hsv.py @@ -14,33 +14,85 @@ def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.1): def next_frame(self): + + while True: + + ca = hsv(0.0, 0.0, 0.0, True) + while ca.v < 1.0: + ca.v += 0.0008 + self.tri_grid.set_all_cells(ca) + self.tri_grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + while ca.s < 1.0: + ca.s += 0.0008 + self.tri_grid.set_all_cells(ca) + self.tri_grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + while ca.h < 1.0: + ca.h += 0.0008 + self.tri_grid.set_all_cells(ca) + self.tri_grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + self.tri_grid.clear() time.sleep(3) + while True: - ca = hsv(0.0, 0.0, 0.0) + ca = hsv(0.0, 0.0, 0.0, False) while ca.v < 1.0: ca.v += 0.0008 self.tri_grid.set_all_cells(ca) self.tri_grid.go() time.sleep(.01) - print('CA', ca.hsv) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + while ca.s < 1.0: ca.s += 0.0008 self.tri_grid.set_all_cells(ca) self.tri_grid.go() time.sleep(.01) - print('CA', ca.hsv) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + while ca.h < 1.0: ca.h += 0.0008 self.tri_grid.set_all_cells(ca) self.tri_grid.go() time.sleep(.01) - print('CA', ca.hsv) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + + self.tri_grid.clear() + time.sleep(3) + + while True: + ca = hsv(0.0, 0.0, 1.0, False) + + while ca.s < 1.0: + ca.s += 0.0008 + self.tri_grid.set_all_cells(ca) + self.tri_grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + + while ca.h < 1.0: + ca.h += 0.0008 + self.tri_grid.set_all_cells(ca) + self.tri_grid.go() + time.sleep(.01) + print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) + self.tri_grid.clear() time.sleep(3) + yield self.frame_delay diff --git a/shows/left_to_right.py b/shows/left_to_right.py index 89c0aa6..a997e54 100644 --- a/shows/left_to_right.py +++ b/shows/left_to_right.py @@ -1,23 +1,45 @@ -from color import RGB +from color import HSV from .showbase import ShowBase from grid import TriangleGrid, traversal - +import time class LeftToRight(ShowBase): - def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 1.0): + def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.2): self.tri_grid = tri_grid self.frame_delay = frame_delay def next_frame(self): row_count = self.tri_grid.row_count - while True: - for points in traversal.left_to_right(row_count): - self.tri_grid.clear() - - for (row, column) in points: - print(f"row={row} column={column}") - self.tri_grid.set_cell_by_coordinates(row, column, RGB(255, 255, 25)) + hsv = HSV(0.5,0.2,.75) +# from IPython import embed; embed() + pix_arr = [] + a_ctr = 0 + for points in traversal.left_to_right(row_count): + + for (row, column) in points: + cell = self.tri_grid.get_cell_by_coordinates(row, column) + b_ctr = 0 + for pixel in list( self.tri_grid.set_pixels_by_cellid(cell.id) ): + if len(pix_arr) <= a_ctr+b_ctr: + pix_arr.append([]) + pix_arr[a_ctr+b_ctr].append(pixel) + pixel(hsv) + self.tri_grid.go() + b_ctr += 1 + a_ctr += 4 + - self.tri_grid.go() + while True: + for i in pix_arr: + for ii in i: + print(ii) + ii(hsv) + self.tri_grid.go() + time.sleep(0.2) + + hsv.h += .1 + if hsv.h >= 1.0: + hsv.h = 0.0 yield self.frame_delay + diff --git a/shows/marching_hexes.py b/shows/marching_hexes.py index 164b4e3..137a21e 100644 --- a/shows/marching_hexes.py +++ b/shows/marching_hexes.py @@ -11,7 +11,7 @@ def __init__(self, tri_grid, frame_delay=0.1): self.n_cells = len(self.tri_grid.cells) def next_frame(self): - hsv = HSV(1.0,.9,.5) + hsv = HSV(1.0,1,1) while True: self.tri_grid.clear() for cell in self.tri_grid.cells: diff --git a/shows/one_by_one.py b/shows/one_by_one.py index 475ea86..da1e492 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -9,7 +9,7 @@ def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.9): self.frame_delay = frame_delay def next_frame(self): - ncells = len(self.tri_grid.cells) - 1 + ncells = len(self.tri_grid.cells) self.tri_grid.clear() while True: @@ -17,6 +17,7 @@ def next_frame(self): self.tri_grid.clear() print(cell) for pixel in self.tri_grid.set_pixels_by_cellid(cell): + print(pixel) pixel(RGB(255, 255, 25)) - yield self.frame_delay + yield self.frame_delay diff --git a/shows/strobe.py b/shows/strobe.py index 129f4ee..78830cb 100644 --- a/shows/strobe.py +++ b/shows/strobe.py @@ -4,10 +4,10 @@ class Strobe(ShowBase): - def __init__(self, tri_grid, frame_delay = 0.002): + def __init__(self, tri_grid, frame_delay = 0.02): self.tri_grid = tri_grid self.frame_delay = frame_delay - self.n_cells = len(self.tri_grid.get_cells()) + self.n_cells = len(self.tri_grid._cells) def next_frame(self): @@ -18,12 +18,12 @@ def next_frame(self): time.sleep(0.02) self.tri_grid.set_all_cells(RGB(100, 100, 100)) self.tri_grid.go() - time.sleep(0.01) + time.sleep(0.02) self.tri_grid.set_all_cells(RGB(5, 5, 255)) self.tri_grid.go() time.sleep(0.02) self.tri_grid.set_all_cells(RGB(100, 100, 100)) self.tri_grid.go() - time.sleep(0.01) + time.sleep(0.02) yield self.frame_delay From a44db535ca5cac3cde1fef01ff9ba831c1b76e9c Mon Sep 17 00:00:00 2001 From: John Major Date: Wed, 31 Jul 2019 01:42:19 -0700 Subject: [PATCH 34/60] mapped 11 row panel. Needs IRL testing --- data/pixel_map.json | 1033 ++++++++++++++++++++++++++++- model/mapping.py | 133 +++- scripts/build_pixel_map_json.py | 16 + scripts/build_tri_cell_mapping.py | 51 ++ 4 files changed, 1223 insertions(+), 10 deletions(-) create mode 100644 scripts/build_pixel_map_json.py create mode 100644 scripts/build_tri_cell_mapping.py diff --git a/data/pixel_map.json b/data/pixel_map.json index 99fe04f..d212421 100644 --- a/data/pixel_map.json +++ b/data/pixel_map.json @@ -119,5 +119,1036 @@ "118": [1, 469], "119": [1, 473], "120": [1, 477], -"121": [1, 481] +"121": [1, 481], +"122": [1, 485], +"123": [1, 489], +"124": [1, 493], +"125": [1, 497], +"126": [1, 501], +"127": [1, 505], +"128": [1, 509], +"129": [2, 1], +"130": [2, 5], +"131": [2, 9], +"132": [2, 13], +"133": [2, 17], +"134": [2, 21], +"135": [2, 25], +"136": [2, 29], +"137": [2, 33], +"138": [2, 37], +"139": [2, 41], +"140": [2, 45], +"141": [2, 49], +"142": [2, 53], +"143": [2, 57], +"144": [2, 61], +"145": [2, 65], +"146": [2, 69], +"147": [2, 73], +"148": [2, 77], +"149": [2, 81], +"150": [2, 85], +"151": [2, 89], +"152": [2, 93], +"153": [2, 97], +"154": [2, 101], +"155": [2, 105], +"156": [2, 109], +"157": [2, 113], +"158": [2, 117], +"159": [2, 121], +"160": [2, 125], +"161": [2, 129], +"162": [2, 133], +"163": [2, 137], +"164": [2, 141], +"165": [2, 145], +"166": [2, 149], +"167": [2, 153], +"168": [2, 157], +"169": [2, 161], +"170": [2, 165], +"171": [2, 169], +"172": [2, 173], +"173": [2, 177], +"174": [2, 181], +"175": [2, 185], +"176": [2, 189], +"177": [2, 193], +"178": [2, 197], +"179": [2, 201], +"180": [2, 205], +"181": [2, 209], +"182": [2, 213], +"183": [2, 217], +"184": [2, 221], +"185": [2, 225], +"186": [2, 229], +"187": [2, 233], +"188": [2, 237], +"189": [2, 241], +"190": [2, 245], +"191": [2, 249], +"192": [2, 253], +"193": [2, 257], +"194": [2, 261], +"195": [2, 265], +"196": [2, 269], +"197": [2, 273], +"198": [2, 277], +"199": [2, 281], +"200": [2, 285], +"201": [2, 289], +"202": [2, 293], +"203": [2, 297], +"204": [2, 301], +"205": [2, 305], +"206": [2, 309], +"207": [2, 313], +"208": [2, 317], +"209": [2, 321], +"210": [2, 325], +"211": [2, 329], +"212": [2, 333], +"213": [2, 337], +"214": [2, 341], +"215": [2, 345], +"216": [2, 349], +"217": [2, 353], +"218": [2, 357], +"219": [2, 361], +"220": [2, 365], +"221": [2, 369], +"222": [2, 373], +"223": [2, 377], +"224": [2, 381], +"225": [2, 385], +"226": [2, 389], +"227": [2, 393], +"228": [2, 397], +"229": [2, 401], +"230": [2, 405], +"231": [2, 409], +"232": [2, 413], +"233": [2, 417], +"234": [2, 421], +"235": [2, 425], +"236": [2, 429], +"237": [2, 433], +"238": [2, 437], +"239": [2, 441], +"240": [2, 445], +"241": [2, 449], +"242": [2, 453], +"243": [2, 457], +"244": [2, 461], +"245": [2, 465], +"246": [2, 469], +"247": [2, 473], +"248": [2, 477], +"249": [2, 481], +"250": [2, 485], +"251": [2, 489], +"252": [2, 493], +"253": [2, 497], +"254": [2, 501], +"255": [2, 505], +"256": [2, 509], +"257": [3, 1], +"258": [3, 5], +"259": [3, 9], +"260": [3, 13], +"261": [3, 17], +"262": [3, 21], +"263": [3, 25], +"264": [3, 29], +"265": [3, 33], +"266": [3, 37], +"267": [3, 41], +"268": [3, 45], +"269": [3, 49], +"270": [3, 53], +"271": [3, 57], +"272": [3, 61], +"273": [3, 65], +"274": [3, 69], +"275": [3, 73], +"276": [3, 77], +"277": [3, 81], +"278": [3, 85], +"279": [3, 89], +"280": [3, 93], +"281": [3, 97], +"282": [3, 101], +"283": [3, 105], +"284": [3, 109], +"285": [3, 113], +"286": [3, 117], +"287": [3, 121], +"288": [3, 125], +"289": [3, 129], +"290": [3, 133], +"291": [3, 137], +"292": [3, 141], +"293": [3, 145], +"294": [3, 149], +"295": [3, 153], +"296": [3, 157], +"297": [3, 161], +"298": [3, 165], +"299": [3, 169], +"300": [3, 173], +"301": [3, 177], +"302": [3, 181], +"303": [3, 185], +"304": [3, 189], +"305": [3, 193], +"306": [3, 197], +"307": [3, 201], +"308": [3, 205], +"309": [3, 209], +"310": [3, 213], +"311": [3, 217], +"312": [3, 221], +"313": [3, 225], +"314": [3, 229], +"315": [3, 233], +"316": [3, 237], +"317": [3, 241], +"318": [3, 245], +"319": [3, 249], +"320": [3, 253], +"321": [3, 257], +"322": [3, 261], +"323": [3, 265], +"324": [3, 269], +"325": [3, 273], +"326": [3, 277], +"327": [3, 281], +"328": [3, 285], +"329": [3, 289], +"330": [3, 293], +"331": [3, 297], +"332": [3, 301], +"333": [3, 305], +"334": [3, 309], +"335": [3, 313], +"336": [3, 317], +"337": [3, 321], +"338": [3, 325], +"339": [3, 329], +"340": [3, 333], +"341": [3, 337], +"342": [3, 341], +"343": [3, 345], +"344": [3, 349], +"345": [3, 353], +"346": [3, 357], +"347": [3, 361], +"348": [3, 365], +"349": [3, 369], +"350": [3, 373], +"351": [3, 377], +"352": [3, 381], +"353": [3, 385], +"354": [3, 389], +"355": [3, 393], +"356": [3, 397], +"357": [3, 401], +"358": [3, 405], +"359": [3, 409], +"360": [3, 413], +"361": [3, 417], +"362": [3, 421], +"363": [3, 425], +"364": [3, 429], +"365": [3, 433], +"366": [3, 437], +"367": [3, 441], +"368": [3, 445], +"369": [3, 449], +"370": [3, 453], +"371": [3, 457], +"372": [3, 461], +"373": [3, 465], +"374": [3, 469], +"375": [3, 473], +"376": [3, 477], +"377": [3, 481], +"378": [3, 485], +"379": [3, 489], +"380": [3, 493], +"381": [3, 497], +"382": [3, 501], +"383": [3, 505], +"384": [3, 509], +"385": [4, 1], +"386": [4, 5], +"387": [4, 9], +"388": [4, 13], +"389": [4, 17], +"390": [4, 21], +"391": [4, 25], +"392": [4, 29], +"393": [4, 33], +"394": [4, 37], +"395": [4, 41], +"396": [4, 45], +"397": [4, 49], +"398": [4, 53], +"399": [4, 57], +"400": [4, 61], +"401": [4, 65], +"402": [4, 69], +"403": [4, 73], +"404": [4, 77], +"405": [4, 81], +"406": [4, 85], +"407": [4, 89], +"408": [4, 93], +"409": [4, 97], +"410": [4, 101], +"411": [4, 105], +"412": [4, 109], +"413": [4, 113], +"414": [4, 117], +"415": [4, 121], +"416": [4, 125], +"417": [4, 129], +"418": [4, 133], +"419": [4, 137], +"420": [4, 141], +"421": [4, 145], +"422": [4, 149], +"423": [4, 153], +"424": [4, 157], +"425": [4, 161], +"426": [4, 165], +"427": [4, 169], +"428": [4, 173], +"429": [4, 177], +"430": [4, 181], +"431": [4, 185], +"432": [4, 189], +"433": [4, 193], +"434": [4, 197], +"435": [4, 201], +"436": [4, 205], +"437": [4, 209], +"438": [4, 213], +"439": [4, 217], +"440": [4, 221], +"441": [4, 225], +"442": [4, 229], +"443": [4, 233], +"444": [4, 237], +"445": [4, 241], +"446": [4, 245], +"447": [4, 249], +"448": [4, 253], +"449": [4, 257], +"450": [4, 261], +"451": [4, 265], +"452": [4, 269], +"453": [4, 273], +"454": [4, 277], +"455": [4, 281], +"456": [4, 285], +"457": [4, 289], +"458": [4, 293], +"459": [4, 297], +"460": [4, 301], +"461": [4, 305], +"462": [4, 309], +"463": [4, 313], +"464": [4, 317], +"465": [4, 321], +"466": [4, 325], +"467": [4, 329], +"468": [4, 333], +"469": [4, 337], +"470": [4, 341], +"471": [4, 345], +"472": [4, 349], +"473": [4, 353], +"474": [4, 357], +"475": [4, 361], +"476": [4, 365], +"477": [4, 369], +"478": [4, 373], +"479": [4, 377], +"480": [4, 381], +"481": [4, 385], +"482": [4, 389], +"483": [4, 393], +"484": [4, 397], +"485": [4, 401], +"486": [4, 405], +"487": [4, 409], +"488": [4, 413], +"489": [4, 417], +"490": [4, 421], +"491": [4, 425], +"492": [4, 429], +"493": [4, 433], +"494": [4, 437], +"495": [4, 441], +"496": [4, 445], +"497": [4, 449], +"498": [4, 453], +"499": [4, 457], +"500": [4, 461], +"501": [4, 465], +"502": [4, 469], +"503": [4, 473], +"504": [4, 477], +"505": [4, 481], +"506": [4, 485], +"507": [4, 489], +"508": [4, 493], +"509": [4, 497], +"510": [4, 501], +"511": [4, 505], +"512": [4, 509], +"513": [5, 1], +"514": [5, 5], +"515": [5, 9], +"516": [5, 13], +"517": [5, 17], +"518": [5, 21], +"519": [5, 25], +"520": [5, 29], +"521": [5, 33], +"522": [5, 37], +"523": [5, 41], +"524": [5, 45], +"525": [5, 49], +"526": [5, 53], +"527": [5, 57], +"528": [5, 61], +"529": [5, 65], +"530": [5, 69], +"531": [5, 73], +"532": [5, 77], +"533": [5, 81], +"534": [5, 85], +"535": [5, 89], +"536": [5, 93], +"537": [5, 97], +"538": [5, 101], +"539": [5, 105], +"540": [5, 109], +"541": [5, 113], +"542": [5, 117], +"543": [5, 121], +"544": [5, 125], +"545": [5, 129], +"546": [5, 133], +"547": [5, 137], +"548": [5, 141], +"549": [5, 145], +"550": [5, 149], +"551": [5, 153], +"552": [5, 157], +"553": [5, 161], +"554": [5, 165], +"555": [5, 169], +"556": [5, 173], +"557": [5, 177], +"558": [5, 181], +"559": [5, 185], +"560": [5, 189], +"561": [5, 193], +"562": [5, 197], +"563": [5, 201], +"564": [5, 205], +"565": [5, 209], +"566": [5, 213], +"567": [5, 217], +"568": [5, 221], +"569": [5, 225], +"570": [5, 229], +"571": [5, 233], +"572": [5, 237], +"573": [5, 241], +"574": [5, 245], +"575": [5, 249], +"576": [5, 253], +"577": [5, 257], +"578": [5, 261], +"579": [5, 265], +"580": [5, 269], +"581": [5, 273], +"582": [5, 277], +"583": [5, 281], +"584": [5, 285], +"585": [5, 289], +"586": [5, 293], +"587": [5, 297], +"588": [5, 301], +"589": [5, 305], +"590": [5, 309], +"591": [5, 313], +"592": [5, 317], +"593": [5, 321], +"594": [5, 325], +"595": [5, 329], +"596": [5, 333], +"597": [5, 337], +"598": [5, 341], +"599": [5, 345], +"600": [5, 349], +"601": [5, 353], +"602": [5, 357], +"603": [5, 361], +"604": [5, 365], +"605": [5, 369], +"606": [5, 373], +"607": [5, 377], +"608": [5, 381], +"609": [5, 385], +"610": [5, 389], +"611": [5, 393], +"612": [5, 397], +"613": [5, 401], +"614": [5, 405], +"615": [5, 409], +"616": [5, 413], +"617": [5, 417], +"618": [5, 421], +"619": [5, 425], +"620": [5, 429], +"621": [5, 433], +"622": [5, 437], +"623": [5, 441], +"624": [5, 445], +"625": [5, 449], +"626": [5, 453], +"627": [5, 457], +"628": [5, 461], +"629": [5, 465], +"630": [5, 469], +"631": [5, 473], +"632": [5, 477], +"633": [5, 481], +"634": [5, 485], +"635": [5, 489], +"636": [5, 493], +"637": [5, 497], +"638": [5, 501], +"639": [5, 505], +"640": [5, 509], +"641": [6, 1], +"642": [6, 5], +"643": [6, 9], +"644": [6, 13], +"645": [6, 17], +"646": [6, 21], +"647": [6, 25], +"648": [6, 29], +"649": [6, 33], +"650": [6, 37], +"651": [6, 41], +"652": [6, 45], +"653": [6, 49], +"654": [6, 53], +"655": [6, 57], +"656": [6, 61], +"657": [6, 65], +"658": [6, 69], +"659": [6, 73], +"660": [6, 77], +"661": [6, 81], +"662": [6, 85], +"663": [6, 89], +"664": [6, 93], +"665": [6, 97], +"666": [6, 101], +"667": [6, 105], +"668": [6, 109], +"669": [6, 113], +"670": [6, 117], +"671": [6, 121], +"672": [6, 125], +"673": [6, 129], +"674": [6, 133], +"675": [6, 137], +"676": [6, 141], +"677": [6, 145], +"678": [6, 149], +"679": [6, 153], +"680": [6, 157], +"681": [6, 161], +"682": [6, 165], +"683": [6, 169], +"684": [6, 173], +"685": [6, 177], +"686": [6, 181], +"687": [6, 185], +"688": [6, 189], +"689": [6, 193], +"690": [6, 197], +"691": [6, 201], +"692": [6, 205], +"693": [6, 209], +"694": [6, 213], +"695": [6, 217], +"696": [6, 221], +"697": [6, 225], +"698": [6, 229], +"699": [6, 233], +"700": [6, 237], +"701": [6, 241], +"702": [6, 245], +"703": [6, 249], +"704": [6, 253], +"705": [6, 257], +"706": [6, 261], +"707": [6, 265], +"708": [6, 269], +"709": [6, 273], +"710": [6, 277], +"711": [6, 281], +"712": [6, 285], +"713": [6, 289], +"714": [6, 293], +"715": [6, 297], +"716": [6, 301], +"717": [6, 305], +"718": [6, 309], +"719": [6, 313], +"720": [6, 317], +"721": [6, 321], +"722": [6, 325], +"723": [6, 329], +"724": [6, 333], +"725": [6, 337], +"726": [6, 341], +"727": [6, 345], +"728": [6, 349], +"729": [6, 353], +"730": [6, 357], +"731": [6, 361], +"732": [6, 365], +"733": [6, 369], +"734": [6, 373], +"735": [6, 377], +"736": [6, 381], +"737": [6, 385], +"738": [6, 389], +"739": [6, 393], +"740": [6, 397], +"741": [6, 401], +"742": [6, 405], +"743": [6, 409], +"744": [6, 413], +"745": [6, 417], +"746": [6, 421], +"747": [6, 425], +"748": [6, 429], +"749": [6, 433], +"750": [6, 437], +"751": [6, 441], +"752": [6, 445], +"753": [6, 449], +"754": [6, 453], +"755": [6, 457], +"756": [6, 461], +"757": [6, 465], +"758": [6, 469], +"759": [6, 473], +"760": [6, 477], +"761": [6, 481], +"762": [6, 485], +"763": [6, 489], +"764": [6, 493], +"765": [6, 497], +"766": [6, 501], +"767": [6, 505], +"768": [6, 509], +"769": [7, 1], +"770": [7, 5], +"771": [7, 9], +"772": [7, 13], +"773": [7, 17], +"774": [7, 21], +"775": [7, 25], +"776": [7, 29], +"777": [7, 33], +"778": [7, 37], +"779": [7, 41], +"780": [7, 45], +"781": [7, 49], +"782": [7, 53], +"783": [7, 57], +"784": [7, 61], +"785": [7, 65], +"786": [7, 69], +"787": [7, 73], +"788": [7, 77], +"789": [7, 81], +"790": [7, 85], +"791": [7, 89], +"792": [7, 93], +"793": [7, 97], +"794": [7, 101], +"795": [7, 105], +"796": [7, 109], +"797": [7, 113], +"798": [7, 117], +"799": [7, 121], +"800": [7, 125], +"801": [7, 129], +"802": [7, 133], +"803": [7, 137], +"804": [7, 141], +"805": [7, 145], +"806": [7, 149], +"807": [7, 153], +"808": [7, 157], +"809": [7, 161], +"810": [7, 165], +"811": [7, 169], +"812": [7, 173], +"813": [7, 177], +"814": [7, 181], +"815": [7, 185], +"816": [7, 189], +"817": [7, 193], +"818": [7, 197], +"819": [7, 201], +"820": [7, 205], +"821": [7, 209], +"822": [7, 213], +"823": [7, 217], +"824": [7, 221], +"825": [7, 225], +"826": [7, 229], +"827": [7, 233], +"828": [7, 237], +"829": [7, 241], +"830": [7, 245], +"831": [7, 249], +"832": [7, 253], +"833": [7, 257], +"834": [7, 261], +"835": [7, 265], +"836": [7, 269], +"837": [7, 273], +"838": [7, 277], +"839": [7, 281], +"840": [7, 285], +"841": [7, 289], +"842": [7, 293], +"843": [7, 297], +"844": [7, 301], +"845": [7, 305], +"846": [7, 309], +"847": [7, 313], +"848": [7, 317], +"849": [7, 321], +"850": [7, 325], +"851": [7, 329], +"852": [7, 333], +"853": [7, 337], +"854": [7, 341], +"855": [7, 345], +"856": [7, 349], +"857": [7, 353], +"858": [7, 357], +"859": [7, 361], +"860": [7, 365], +"861": [7, 369], +"862": [7, 373], +"863": [7, 377], +"864": [7, 381], +"865": [7, 385], +"866": [7, 389], +"867": [7, 393], +"868": [7, 397], +"869": [7, 401], +"870": [7, 405], +"871": [7, 409], +"872": [7, 413], +"873": [7, 417], +"874": [7, 421], +"875": [7, 425], +"876": [7, 429], +"877": [7, 433], +"878": [7, 437], +"879": [7, 441], +"880": [7, 445], +"881": [7, 449], +"882": [7, 453], +"883": [7, 457], +"884": [7, 461], +"885": [7, 465], +"886": [7, 469], +"887": [7, 473], +"888": [7, 477], +"889": [7, 481], +"890": [7, 485], +"891": [7, 489], +"892": [7, 493], +"893": [7, 497], +"894": [7, 501], +"895": [7, 505], +"896": [7, 509], +"897": [8, 1], +"898": [8, 5], +"899": [8, 9], +"900": [8, 13], +"901": [8, 17], +"902": [8, 21], +"903": [8, 25], +"904": [8, 29], +"905": [8, 33], +"906": [8, 37], +"907": [8, 41], +"908": [8, 45], +"909": [8, 49], +"910": [8, 53], +"911": [8, 57], +"912": [8, 61], +"913": [8, 65], +"914": [8, 69], +"915": [8, 73], +"916": [8, 77], +"917": [8, 81], +"918": [8, 85], +"919": [8, 89], +"920": [8, 93], +"921": [8, 97], +"922": [8, 101], +"923": [8, 105], +"924": [8, 109], +"925": [8, 113], +"926": [8, 117], +"927": [8, 121], +"928": [8, 125], +"929": [8, 129], +"930": [8, 133], +"931": [8, 137], +"932": [8, 141], +"933": [8, 145], +"934": [8, 149], +"935": [8, 153], +"936": [8, 157], +"937": [8, 161], +"938": [8, 165], +"939": [8, 169], +"940": [8, 173], +"941": [8, 177], +"942": [8, 181], +"943": [8, 185], +"944": [8, 189], +"945": [8, 193], +"946": [8, 197], +"947": [8, 201], +"948": [8, 205], +"949": [8, 209], +"950": [8, 213], +"951": [8, 217], +"952": [8, 221], +"953": [8, 225], +"954": [8, 229], +"955": [8, 233], +"956": [8, 237], +"957": [8, 241], +"958": [8, 245], +"959": [8, 249], +"960": [8, 253], +"961": [8, 257], +"962": [8, 261], +"963": [8, 265], +"964": [8, 269], +"965": [8, 273], +"966": [8, 277], +"967": [8, 281], +"968": [8, 285], +"969": [8, 289], +"970": [8, 293], +"971": [8, 297], +"972": [8, 301], +"973": [8, 305], +"974": [8, 309], +"975": [8, 313], +"976": [8, 317], +"977": [8, 321], +"978": [8, 325], +"979": [8, 329], +"980": [8, 333], +"981": [8, 337], +"982": [8, 341], +"983": [8, 345], +"984": [8, 349], +"985": [8, 353], +"986": [8, 357], +"987": [8, 361], +"988": [8, 365], +"989": [8, 369], +"990": [8, 373], +"991": [8, 377], +"992": [8, 381], +"993": [8, 385], +"994": [8, 389], +"995": [8, 393], +"996": [8, 397], +"997": [8, 401], +"998": [8, 405], +"999": [8, 409], +"1000": [8, 413], +"1001": [8, 417], +"1002": [8, 421], +"1003": [8, 425], +"1004": [8, 429], +"1005": [8, 433], +"1006": [8, 437], +"1007": [8, 441], +"1008": [8, 445], +"1009": [8, 449], +"1010": [8, 453], +"1011": [8, 457], +"1012": [8, 461], +"1013": [8, 465], +"1014": [8, 469], +"1015": [8, 473], +"1016": [8, 477], +"1017": [8, 481], +"1018": [8, 485], +"1019": [8, 489], +"1020": [8, 493], +"1021": [8, 497], +"1022": [8, 501], +"1023": [8, 505], +"1024": [8, 509], +"1025": [9, 1], +"1026": [9, 5], +"1027": [9, 9], +"1028": [9, 13], +"1029": [9, 17], +"1030": [9, 21], +"1031": [9, 25], +"1032": [9, 29], +"1033": [9, 33], +"1034": [9, 37], +"1035": [9, 41], +"1036": [9, 45], +"1037": [9, 49], +"1038": [9, 53], +"1039": [9, 57], +"1040": [9, 61], +"1041": [9, 65], +"1042": [9, 69], +"1043": [9, 73], +"1044": [9, 77], +"1045": [9, 81], +"1046": [9, 85], +"1047": [9, 89], +"1048": [9, 93], +"1049": [9, 97], +"1050": [9, 101], +"1051": [9, 105], +"1052": [9, 109], +"1053": [9, 113], +"1054": [9, 117], +"1055": [9, 121], +"1056": [9, 125], +"1057": [9, 129], +"1058": [9, 133], +"1059": [9, 137], +"1060": [9, 141], +"1061": [9, 145], +"1062": [9, 149], +"1063": [9, 153], +"1064": [9, 157], +"1065": [9, 161], +"1066": [9, 165], +"1067": [9, 169], +"1068": [9, 173], +"1069": [9, 177], +"1070": [9, 181], +"1071": [9, 185], +"1072": [9, 189], +"1073": [9, 193], +"1074": [9, 197], +"1075": [9, 201], +"1076": [9, 205], +"1077": [9, 209], +"1078": [9, 213], +"1079": [9, 217], +"1080": [9, 221], +"1081": [9, 225], +"1082": [9, 229], +"1083": [9, 233], +"1084": [9, 237], +"1085": [9, 241], +"1086": [9, 245], +"1087": [9, 249], +"1088": [9, 253], +"1089": [9, 257], +"1090": [9, 261], +"1091": [9, 265], +"1092": [9, 269], +"1093": [9, 273], +"1094": [9, 277], +"1095": [9, 281], +"1096": [9, 285], +"1097": [9, 289], +"1098": [9, 293], +"1099": [9, 297], +"1100": [9, 301], +"1101": [9, 305], +"1102": [9, 309], +"1103": [9, 313], +"1104": [9, 317], +"1105": [9, 321], +"1106": [9, 325], +"1107": [9, 329], +"1108": [9, 333], +"1109": [9, 337], +"1110": [9, 341], +"1111": [9, 345], +"1112": [9, 349], +"1113": [9, 353], +"1114": [9, 357], +"1115": [9, 361], +"1116": [9, 365], +"1117": [9, 369], +"1118": [9, 373], +"1119": [9, 377], +"1120": [9, 381], +"1121": [9, 385], +"1122": [9, 389], +"1123": [9, 393], +"1124": [9, 397], +"1125": [9, 401], +"1126": [9, 405], +"1127": [9, 409], +"1128": [9, 413], +"1129": [9, 417], +"1130": [9, 421], +"1131": [9, 425], +"1132": [9, 429], +"1133": [9, 433], +"1134": [9, 437], +"1135": [9, 441], +"1136": [9, 445], +"1137": [9, 449], +"1138": [9, 453], +"1139": [9, 457], +"1140": [9, 461], +"1141": [9, 465], +"1142": [9, 469], +"1143": [9, 473], +"1144": [9, 477], +"1145": [9, 481], +"1146": [9, 485], +"1147": [9, 489], +"1148": [9, 493], +"1149": [9, 497], +"1150": [9, 501], +"1151": [9, 505], +"1152": [9, 509] } diff --git a/model/mapping.py b/model/mapping.py index 9eaa2cf..d92ed66 100644 --- a/model/mapping.py +++ b/model/mapping.py @@ -2,14 +2,129 @@ PixelMap = Dict[int, List[int]] -# This cell -> pixels mapping is created from an old version, but it should produce something. +# This cell -> pixels mapping is created from an old version, but it should produce something. def demo_triangle_mapping() -> PixelMap: return { - #Wiring coming in from the bottom left, this is the order of cells following the pixels - 1: [ 2, 3, 4, 5, 6, 7, 8, 9], - 3: [ 10, 11, 12, 13, 14, 15, 16, 17], - 2: [ 34, 33, 32, 31, 30, 29,28, 27 ], - 0: [ 35, 36, 37, 38, 39, 40, 41, 42] # the last cell should the the top most cell of the triangle grid - } - - + #Wiring coming in from the bottom left, this is the order of cells following the pixels + 1: [1052,1053,1054,1055,1056,1057,1058,1059], + 2: [1019,1020,1021,1022,1023,1024,1025,1026], + 3: [1051,1052,1053,1054,1055,1056,1057,1058], + 4: [1027,1028,1029,1030,1031,1032,1033,1034], + 5: [970,971,972,973,974,975,976,977], + 6: [1018,1019,1020,1021,1022,1023,1024,1025], + 7: [978,979,980,981,982,983,984,985], + 8: [1026,1027,1028,1029,1030,1031,1032,1033], + 9: [986,987,988,989,990,991,992,993], + 10: [905,906,907,908,909,910,911,912], + 11: [969,970,971,972,973,974,975,976], + 12: [913,914,915,916,917,918,919,920], + 13: [977,978,979,980,981,982,983,984], + 14: [921,922,923,924,925,926,927,928], + 15: [985,986,987,988,989,990,991,992], + 16: [929,930,931,932,933,934,935,936], + 17: [824,825,826,827,828,829,830,831], + 18: [904,905,906,907,908,909,910,911], + 19: [832,833,834,835,836,837,838,839], + 20: [912,913,914,915,916,917,918,919], + 21: [840,841,842,843,844,845,846,847], + 22: [920,921,922,923,924,925,926,927], + 23: [848,849,850,851,852,853,854,855], + 24: [928,929,930,931,932,933,934,935], + 25: [856,857,858,859,860,861,862,863], + 26: [727,728,729,730,731,732,733,734], + 27: [823,824,825,826,827,828,829,830], + 28: [735,736,737,738,739,740,741,742], + 29: [831,832,833,834,835,836,837,838], + 30: [743,744,745,746,747,748,749,750], + 31: [839,840,841,842,843,844,845,846], + 32: [751,752,753,754,755,756,757,758], + 33: [847,848,849,850,851,852,853,854], + 34: [759,760,761,762,763,764,765,766], + 35: [855,856,857,858,859,860,861,862], + 36: [767,768,769,770,771,772,773,774], + 37: [614,615,616,617,618,619,620,621], + 38: [726,727,728,729,730,731,732,733], + 39: [622,623,624,625,626,627,628,629], + 40: [734,735,736,737,738,739,740,741], + 41: [630,631,632,633,634,635,636,637], + 42: [742,743,744,745,746,747,748,749], + 43: [638,639,640,641,642,643,644,645], + 44: [750,751,752,753,754,755,756,757], + 45: [646,647,648,649,650,651,652,653], + 46: [758,759,760,761,762,763,764,765], + 47: [654,655,656,657,658,659,660,661], + 48: [766,767,768,769,770,771,772,773], + 49: [662,663,664,665,666,667,668,669], + 50: [485,486,487,488,489,490,491,492], + 51: [613,614,615,616,617,618,619,620], + 52: [493,494,495,496,497,498,499,500], + 53: [621,622,623,624,625,626,627,628], + 54: [501,502,503,504,505,506,507,508], + 55: [629,630,631,632,633,634,635,636], + 56: [509,510,511,512,513,514,515,516], + 57: [637,638,639,640,641,642,643,644], + 58: [517,518,519,520,521,522,523,524], + 59: [645,646,647,648,649,650,651,652], + 60: [525,526,527,528,529,530,531,532], + 61: [653,654,655,656,657,658,659,660], + 62: [533,534,535,536,537,538,539,540], + 63: [661,662,663,664,665,666,667,668], + 64: [541,542,543,544,545,546,547,548], + 65: [340,341,342,343,344,345,346,347], + 66: [484,485,486,487,488,489,490,491], + 67: [348,349,350,351,352,353,354,355], + 68: [492,493,494,495,496,497,498,499], + 69: [356,357,358,359,360,361,362,363], + 70: [500,501,502,503,504,505,506,507], + 71: [364,365,366,367,368,369,370,371], + 72: [508,509,510,511,512,513,514,515], + 73: [372,373,374,375,376,377,378,379], + 74: [516,517,518,519,520,521,522,523], + 75: [380,381,382,383,384,385,386,387], + 76: [524,525,526,527,528,529,530,531], + 77: [388,389,390,391,392,393,394,395], + 78: [532,533,534,535,536,537,538,539], + 79: [396,397,398,399,400,401,402,403], + 80: [540,541,542,543,544,545,546,547], + 81: [404,405,406,407,408,409,410,411], + 82: [179,180,181,182,183,184,185,186], + 83: [339,340,341,342,343,344,345,346], + 84: [187,188,189,190,191,192,193,194], + 85: [347,348,349,350,351,352,353,354], + 86: [195,196,197,198,199,200,201,202], + 87: [355,356,357,358,359,360,361,362], + 88: [203,204,205,206,207,208,209,210], + 89: [363,364,365,366,367,368,369,370], + 90: [211,212,213,214,215,216,217,218], + 91: [371,372,373,374,375,376,377,378], + 92: [219,220,221,222,223,224,225,226], + 93: [379,380,381,382,383,384,385,386], + 94: [227,228,229,230,231,232,233,234], + 95: [387,388,389,390,391,392,393,394], + 96: [235,236,237,238,239,240,241,242], + 97: [395,396,397,398,399,400,401,402], + 98: [243,244,245,246,247,248,249,250], + 99: [403,404,405,406,407,408,409,410], + 100: [251,252,253,254,255,256,257,258], + 101: [2,3,4,5,6,7,8,9], + 102: [178,179,180,181,182,183,184,185], + 103: [10,11,12,13,14,15,16,17], + 104: [186,187,188,189,190,191,192,193], + 105: [18,19,20,21,22,23,24,25], + 106: [194,195,196,197,198,199,200,201], + 107: [26,27,28,29,30,31,32,33], + 108: [202,203,204,205,206,207,208,209], + 109: [34,35,36,37,38,39,40,41], + 110: [210,211,212,213,214,215,216,217], + 111: [42,43,44,45,46,47,48,49], + 112: [218,219,220,221,222,223,224,225], + 113: [50,51,52,53,54,55,56,57], + 114: [226,227,228,229,230,231,232,233], + 115: [58,59,60,61,62,63,64,65], + 116: [234,235,236,237,238,239,240,241], + 117: [66,67,68,69,70,71,72,73], + 118: [242,243,244,245,246,247,248,249], + 119: [74,75,76,77,78,79,80,81], + 120: [250,251,252,253,254,255,256,257], + 121: [82,83,84,85,86,87,88,89] +} diff --git a/scripts/build_pixel_map_json.py b/scripts/build_pixel_map_json.py new file mode 100644 index 0000000..195395c --- /dev/null +++ b/scripts/build_pixel_map_json.py @@ -0,0 +1,16 @@ +import os +import sys + + +num_ux = int(sys.argv[1]) + +pixel_num = 1 +print("{") +for ux in range(1, num_ux+1): + dmx_start = 1 + while dmx_start < 512: + print('"{0}": [{1}, {2}],'.format(pixel_num,ux,dmx_start)) + pixel_num += 1 + dmx_start += 4 + +print("}") diff --git a/scripts/build_tri_cell_mapping.py b/scripts/build_tri_cell_mapping.py new file mode 100644 index 0000000..fb2ad07 --- /dev/null +++ b/scripts/build_tri_cell_mapping.py @@ -0,0 +1,51 @@ +import os +import sys + +curr_cell = 1 +end_led = 1059 +n = end_led + +ds = {1:1, + 2:3, + 3:5, + 4:7, + 5:9, + 6:11, + 7:13, + 8:15, + 9:17, + 10:19, + 11:21 + } +u = end_led -7 +d = end_led + +for i in range (1,12): + n_cells = ds[i] + + is_up = True + tot_u_add = 0 + tot_d_add = 0 + for cell in range (1, n_cells+1): + if is_up: + print('{0}: [{1},{2},{3},{4},{5},{6},{7},{8}],'.format(curr_cell, u, u+1, u+2 , u+3, u+4, u+5, u+6, u+7)) + is_up = False + u += 8 + tot_u_add += 8 + else: + print('{0}: [{1},{2},{3},{4},{5},{6},{7},{8}],'.format(curr_cell, d ,d+1, d+2, d+3, d+4,d+5,d+6, d+7)) + d += 8 + tot_d_add +=8 + is_up = True + + curr_cell += 1 + + md = 0 + if i == 1: + md = (i*8)+tot_d_add + else: + md = 9+((n_cells)*8)+tot_d_add + d -= md + + mu = 9+((n_cells+2)*8)+tot_u_add + u -= mu From 8b73c6560ea9778d4823cdb507773fa7bd3f2b47 Mon Sep 17 00:00:00 2001 From: John Major Date: Wed, 31 Jul 2019 23:27:04 -0700 Subject: [PATCH 35/60] oops- 0 not 1 --- scripts/build_tri_cell_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_tri_cell_mapping.py b/scripts/build_tri_cell_mapping.py index fb2ad07..01d78b6 100644 --- a/scripts/build_tri_cell_mapping.py +++ b/scripts/build_tri_cell_mapping.py @@ -1,7 +1,7 @@ import os import sys -curr_cell = 1 +curr_cell = 0 end_led = 1059 n = end_led From 5737ee20d0273bdeaa701db68def3fa8cb30c644 Mon Sep 17 00:00:00 2001 From: John Major Date: Thu, 1 Aug 2019 00:44:49 -0700 Subject: [PATCH 36/60] fixed f'd up first attemt at mapping --- model/mapping.py | 246 +++++++++++++++--------------- scripts/build_tri_cell_mapping.py | 47 +++--- 2 files changed, 139 insertions(+), 154 deletions(-) diff --git a/model/mapping.py b/model/mapping.py index d92ed66..c953410 100644 --- a/model/mapping.py +++ b/model/mapping.py @@ -1,130 +1,128 @@ from typing import Dict, List PixelMap = Dict[int, List[int]] - -# This cell -> pixels mapping is created from an old version, but it should produce something. +# This cell -> pixels mapping is created from an old version, but it should produce something. def demo_triangle_mapping() -> PixelMap: return { - #Wiring coming in from the bottom left, this is the order of cells following the pixels - 1: [1052,1053,1054,1055,1056,1057,1058,1059], - 2: [1019,1020,1021,1022,1023,1024,1025,1026], - 3: [1051,1052,1053,1054,1055,1056,1057,1058], - 4: [1027,1028,1029,1030,1031,1032,1033,1034], - 5: [970,971,972,973,974,975,976,977], - 6: [1018,1019,1020,1021,1022,1023,1024,1025], - 7: [978,979,980,981,982,983,984,985], - 8: [1026,1027,1028,1029,1030,1031,1032,1033], - 9: [986,987,988,989,990,991,992,993], - 10: [905,906,907,908,909,910,911,912], - 11: [969,970,971,972,973,974,975,976], - 12: [913,914,915,916,917,918,919,920], - 13: [977,978,979,980,981,982,983,984], - 14: [921,922,923,924,925,926,927,928], - 15: [985,986,987,988,989,990,991,992], - 16: [929,930,931,932,933,934,935,936], - 17: [824,825,826,827,828,829,830,831], - 18: [904,905,906,907,908,909,910,911], - 19: [832,833,834,835,836,837,838,839], - 20: [912,913,914,915,916,917,918,919], - 21: [840,841,842,843,844,845,846,847], - 22: [920,921,922,923,924,925,926,927], - 23: [848,849,850,851,852,853,854,855], - 24: [928,929,930,931,932,933,934,935], - 25: [856,857,858,859,860,861,862,863], - 26: [727,728,729,730,731,732,733,734], - 27: [823,824,825,826,827,828,829,830], - 28: [735,736,737,738,739,740,741,742], - 29: [831,832,833,834,835,836,837,838], - 30: [743,744,745,746,747,748,749,750], - 31: [839,840,841,842,843,844,845,846], - 32: [751,752,753,754,755,756,757,758], - 33: [847,848,849,850,851,852,853,854], - 34: [759,760,761,762,763,764,765,766], - 35: [855,856,857,858,859,860,861,862], - 36: [767,768,769,770,771,772,773,774], - 37: [614,615,616,617,618,619,620,621], - 38: [726,727,728,729,730,731,732,733], - 39: [622,623,624,625,626,627,628,629], - 40: [734,735,736,737,738,739,740,741], - 41: [630,631,632,633,634,635,636,637], - 42: [742,743,744,745,746,747,748,749], - 43: [638,639,640,641,642,643,644,645], - 44: [750,751,752,753,754,755,756,757], - 45: [646,647,648,649,650,651,652,653], - 46: [758,759,760,761,762,763,764,765], - 47: [654,655,656,657,658,659,660,661], - 48: [766,767,768,769,770,771,772,773], - 49: [662,663,664,665,666,667,668,669], - 50: [485,486,487,488,489,490,491,492], - 51: [613,614,615,616,617,618,619,620], - 52: [493,494,495,496,497,498,499,500], - 53: [621,622,623,624,625,626,627,628], - 54: [501,502,503,504,505,506,507,508], - 55: [629,630,631,632,633,634,635,636], - 56: [509,510,511,512,513,514,515,516], - 57: [637,638,639,640,641,642,643,644], - 58: [517,518,519,520,521,522,523,524], - 59: [645,646,647,648,649,650,651,652], - 60: [525,526,527,528,529,530,531,532], - 61: [653,654,655,656,657,658,659,660], - 62: [533,534,535,536,537,538,539,540], - 63: [661,662,663,664,665,666,667,668], - 64: [541,542,543,544,545,546,547,548], - 65: [340,341,342,343,344,345,346,347], - 66: [484,485,486,487,488,489,490,491], - 67: [348,349,350,351,352,353,354,355], - 68: [492,493,494,495,496,497,498,499], - 69: [356,357,358,359,360,361,362,363], - 70: [500,501,502,503,504,505,506,507], - 71: [364,365,366,367,368,369,370,371], - 72: [508,509,510,511,512,513,514,515], - 73: [372,373,374,375,376,377,378,379], - 74: [516,517,518,519,520,521,522,523], - 75: [380,381,382,383,384,385,386,387], - 76: [524,525,526,527,528,529,530,531], - 77: [388,389,390,391,392,393,394,395], - 78: [532,533,534,535,536,537,538,539], - 79: [396,397,398,399,400,401,402,403], - 80: [540,541,542,543,544,545,546,547], - 81: [404,405,406,407,408,409,410,411], - 82: [179,180,181,182,183,184,185,186], - 83: [339,340,341,342,343,344,345,346], - 84: [187,188,189,190,191,192,193,194], - 85: [347,348,349,350,351,352,353,354], - 86: [195,196,197,198,199,200,201,202], - 87: [355,356,357,358,359,360,361,362], - 88: [203,204,205,206,207,208,209,210], - 89: [363,364,365,366,367,368,369,370], - 90: [211,212,213,214,215,216,217,218], - 91: [371,372,373,374,375,376,377,378], - 92: [219,220,221,222,223,224,225,226], - 93: [379,380,381,382,383,384,385,386], - 94: [227,228,229,230,231,232,233,234], - 95: [387,388,389,390,391,392,393,394], - 96: [235,236,237,238,239,240,241,242], - 97: [395,396,397,398,399,400,401,402], - 98: [243,244,245,246,247,248,249,250], - 99: [403,404,405,406,407,408,409,410], - 100: [251,252,253,254,255,256,257,258], - 101: [2,3,4,5,6,7,8,9], - 102: [178,179,180,181,182,183,184,185], - 103: [10,11,12,13,14,15,16,17], - 104: [186,187,188,189,190,191,192,193], - 105: [18,19,20,21,22,23,24,25], - 106: [194,195,196,197,198,199,200,201], - 107: [26,27,28,29,30,31,32,33], - 108: [202,203,204,205,206,207,208,209], - 109: [34,35,36,37,38,39,40,41], - 110: [210,211,212,213,214,215,216,217], - 111: [42,43,44,45,46,47,48,49], - 112: [218,219,220,221,222,223,224,225], - 113: [50,51,52,53,54,55,56,57], - 114: [226,227,228,229,230,231,232,233], - 115: [58,59,60,61,62,63,64,65], - 116: [234,235,236,237,238,239,240,241], - 117: [66,67,68,69,70,71,72,73], - 118: [242,243,244,245,246,247,248,249], - 119: [74,75,76,77,78,79,80,81], - 120: [250,251,252,253,254,255,256,257], - 121: [82,83,84,85,86,87,88,89] +0: [1051,1052,1053,1054,1055,1056,1057,1058], +1: [1018,1019,1020,1021,1022,1023,1024,1025], +2: [1050,1051,1052,1053,1054,1055,1056,1057], +3: [1026,1027,1028,1029,1030,1031,1032,1033], +4: [969,970,971,972,973,974,975,976], +5: [1017,1018,1019,1020,1021,1022,1023,1024], +6: [977,978,979,980,981,982,983,984], +7: [1025,1026,1027,1028,1029,1030,1031,1032], +8: [985,986,987,988,989,990,991,992], +9: [904,905,906,907,908,909,910,911], +10: [968,969,970,971,972,973,974,975], +11: [912,913,914,915,916,917,918,919], +12: [976,977,978,979,980,981,982,983], +13: [920,921,922,923,924,925,926,927], +14: [984,985,986,987,988,989,990,991], +15: [928,929,930,931,932,933,934,935], +16: [823,824,825,826,827,828,829,830], +17: [903,904,905,906,907,908,909,910], +18: [831,832,833,834,835,836,837,838], +19: [911,912,913,914,915,916,917,918], +20: [839,840,841,842,843,844,845,846], +21: [919,920,921,922,923,924,925,926], +22: [847,848,849,850,851,852,853,854], +23: [927,928,929,930,931,932,933,934], +24: [855,856,857,858,859,860,861,862], +25: [726,727,728,729,730,731,732,733], +26: [822,823,824,825,826,827,828,829], +27: [734,735,736,737,738,739,740,741], +28: [830,831,832,833,834,835,836,837], +29: [742,743,744,745,746,747,748,749], +30: [838,839,840,841,842,843,844,845], +31: [750,751,752,753,754,755,756,757], +32: [846,847,848,849,850,851,852,853], +33: [758,759,760,761,762,763,764,765], +34: [854,855,856,857,858,859,860,861], +35: [766,767,768,769,770,771,772,773], +36: [613,614,615,616,617,618,619,620], +37: [725,726,727,728,729,730,731,732], +38: [621,622,623,624,625,626,627,628], +39: [733,734,735,736,737,738,739,740], +40: [629,630,631,632,633,634,635,636], +41: [741,742,743,744,745,746,747,748], +42: [637,638,639,640,641,642,643,644], +43: [749,750,751,752,753,754,755,756], +44: [645,646,647,648,649,650,651,652], +45: [757,758,759,760,761,762,763,764], +46: [653,654,655,656,657,658,659,660], +47: [765,766,767,768,769,770,771,772], +48: [661,662,663,664,665,666,667,668], +49: [484,485,486,487,488,489,490,491], +50: [612,613,614,615,616,617,618,619], +51: [492,493,494,495,496,497,498,499], +52: [620,621,622,623,624,625,626,627], +53: [500,501,502,503,504,505,506,507], +54: [628,629,630,631,632,633,634,635], +55: [508,509,510,511,512,513,514,515], +56: [636,637,638,639,640,641,642,643], +57: [516,517,518,519,520,521,522,523], +58: [644,645,646,647,648,649,650,651], +59: [524,525,526,527,528,529,530,531], +60: [652,653,654,655,656,657,658,659], +61: [532,533,534,535,536,537,538,539], +62: [660,661,662,663,664,665,666,667], +63: [540,541,542,543,544,545,546,547], +64: [339,340,341,342,343,344,345,346], +65: [483,484,485,486,487,488,489,490], +66: [347,348,349,350,351,352,353,354], +67: [491,492,493,494,495,496,497,498], +68: [355,356,357,358,359,360,361,362], +69: [499,500,501,502,503,504,505,506], +70: [363,364,365,366,367,368,369,370], +71: [507,508,509,510,511,512,513,514], +72: [371,372,373,374,375,376,377,378], +73: [515,516,517,518,519,520,521,522], +74: [379,380,381,382,383,384,385,386], +75: [523,524,525,526,527,528,529,530], +76: [387,388,389,390,391,392,393,394], +77: [531,532,533,534,535,536,537,538], +78: [395,396,397,398,399,400,401,402], +79: [539,540,541,542,543,544,545,546], +80: [403,404,405,406,407,408,409,410], +81: [178,179,180,181,182,183,184,185], +82: [338,339,340,341,342,343,344,345], +83: [186,187,188,189,190,191,192,193], +84: [346,347,348,349,350,351,352,353], +85: [194,195,196,197,198,199,200,201], +86: [354,355,356,357,358,359,360,361], +87: [202,203,204,205,206,207,208,209], +88: [362,363,364,365,366,367,368,369], +89: [210,211,212,213,214,215,216,217], +90: [370,371,372,373,374,375,376,377], +91: [218,219,220,221,222,223,224,225], +92: [378,379,380,381,382,383,384,385], +93: [226,227,228,229,230,231,232,233], +94: [386,387,388,389,390,391,392,393], +95: [234,235,236,237,238,239,240,241], +96: [394,395,396,397,398,399,400,401], +97: [242,243,244,245,246,247,248,249], +98: [402,403,404,405,406,407,408,409], +99: [250,251,252,253,254,255,256,257], +100: [1,2,3,4,5,6,7,8], +101: [177,178,179,180,181,182,183,184], +102: [9,10,11,12,13,14,15,16], +103: [185,186,187,188,189,190,191,192], +104: [17,18,19,20,21,22,23,24], +105: [193,194,195,196,197,198,199,200], +106: [25,26,27,28,29,30,31,32], +107: [201,202,203,204,205,206,207,208], +108: [33,34,35,36,37,38,39,40], +109: [209,210,211,212,213,214,215,216], +110: [41,42,43,44,45,46,47,48], +111: [217,218,219,220,221,222,223,224], +112: [49,50,51,52,53,54,55,56], +113: [225,226,227,228,229,230,231,232], +114: [57,58,59,60,61,62,63,64], +115: [233,234,235,236,237,238,239,240], +116: [65,66,67,68,69,70,71,72], +117: [241,242,243,244,245,246,247,248], +118: [73,74,75,76,77,78,79,80], +119: [249,250,251,252,253,254,255,256], +120: [81,82,83,84,85,86,87,88] } diff --git a/scripts/build_tri_cell_mapping.py b/scripts/build_tri_cell_mapping.py index 01d78b6..ab3dc1e 100644 --- a/scripts/build_tri_cell_mapping.py +++ b/scripts/build_tri_cell_mapping.py @@ -2,50 +2,37 @@ import sys curr_cell = 0 -end_led = 1059 -n = end_led -ds = {1:1, - 2:3, - 3:5, - 4:7, - 5:9, - 6:11, - 7:13, - 8:15, - 9:17, - 10:19, - 11:21 +#Built from https://docs.google.com/spreadsheets/d/16Ys242V437N968W5UU2WQdonFJ6SwZJeYmEXGbC9cYo/edit#gid=0 +ds = {1: [1, 1051,0], + 2: [3, 1018,1050], + 3: [5, 969, 1017], + 4: [7, 904, 968], + 5: [9, 823, 903], + 6: [11, 726,822], + 7: [13, 613, 725], + 8: [15, 484,612], + 9: [17, 339, 483], + 10: [19, 178,338], + 11: [21, 1, 177] } -u = end_led -7 -d = end_led -for i in range (1,12): - n_cells = ds[i] +for row in ds: + n_cells = ds[row][0] # ncells in row + u = ds[row][1] # up cell counter + d = ds[row][2] # down cell counter is_up = True - tot_u_add = 0 - tot_d_add = 0 for cell in range (1, n_cells+1): if is_up: print('{0}: [{1},{2},{3},{4},{5},{6},{7},{8}],'.format(curr_cell, u, u+1, u+2 , u+3, u+4, u+5, u+6, u+7)) is_up = False u += 8 - tot_u_add += 8 + else: print('{0}: [{1},{2},{3},{4},{5},{6},{7},{8}],'.format(curr_cell, d ,d+1, d+2, d+3, d+4,d+5,d+6, d+7)) d += 8 - tot_d_add +=8 is_up = True curr_cell += 1 - md = 0 - if i == 1: - md = (i*8)+tot_d_add - else: - md = 9+((n_cells)*8)+tot_d_add - d -= md - - mu = 9+((n_cells+2)*8)+tot_u_add - u -= mu From d6f97a6176e0a597ba69e5643e3bc5ed366f5e66 Mon Sep 17 00:00:00 2001 From: John Major Date: Fri, 2 Aug 2019 01:33:54 -0700 Subject: [PATCH 37/60] New pixel mapping assuming 4 full strands per triangle --- data/pixel_map.json | 1752 ++++++++++++++++--------------- scripts/build_pixel_map_json.py | 14 +- 2 files changed, 910 insertions(+), 856 deletions(-) diff --git a/data/pixel_map.json b/data/pixel_map.json index d212421..aad874e 100644 --- a/data/pixel_map.json +++ b/data/pixel_map.json @@ -299,856 +299,904 @@ "298": [3, 165], "299": [3, 169], "300": [3, 173], -"301": [3, 177], -"302": [3, 181], -"303": [3, 185], -"304": [3, 189], -"305": [3, 193], -"306": [3, 197], -"307": [3, 201], -"308": [3, 205], -"309": [3, 209], -"310": [3, 213], -"311": [3, 217], -"312": [3, 221], -"313": [3, 225], -"314": [3, 229], -"315": [3, 233], -"316": [3, 237], -"317": [3, 241], -"318": [3, 245], -"319": [3, 249], -"320": [3, 253], -"321": [3, 257], -"322": [3, 261], -"323": [3, 265], -"324": [3, 269], -"325": [3, 273], -"326": [3, 277], -"327": [3, 281], -"328": [3, 285], -"329": [3, 289], -"330": [3, 293], -"331": [3, 297], -"332": [3, 301], -"333": [3, 305], -"334": [3, 309], -"335": [3, 313], -"336": [3, 317], -"337": [3, 321], -"338": [3, 325], -"339": [3, 329], -"340": [3, 333], -"341": [3, 337], -"342": [3, 341], -"343": [3, 345], -"344": [3, 349], -"345": [3, 353], -"346": [3, 357], -"347": [3, 361], -"348": [3, 365], -"349": [3, 369], -"350": [3, 373], -"351": [3, 377], -"352": [3, 381], -"353": [3, 385], -"354": [3, 389], -"355": [3, 393], -"356": [3, 397], -"357": [3, 401], -"358": [3, 405], -"359": [3, 409], -"360": [3, 413], -"361": [3, 417], -"362": [3, 421], -"363": [3, 425], -"364": [3, 429], -"365": [3, 433], -"366": [3, 437], -"367": [3, 441], -"368": [3, 445], -"369": [3, 449], -"370": [3, 453], -"371": [3, 457], -"372": [3, 461], -"373": [3, 465], -"374": [3, 469], -"375": [3, 473], -"376": [3, 477], -"377": [3, 481], -"378": [3, 485], -"379": [3, 489], -"380": [3, 493], -"381": [3, 497], -"382": [3, 501], -"383": [3, 505], -"384": [3, 509], -"385": [4, 1], -"386": [4, 5], -"387": [4, 9], -"388": [4, 13], -"389": [4, 17], -"390": [4, 21], -"391": [4, 25], -"392": [4, 29], -"393": [4, 33], -"394": [4, 37], -"395": [4, 41], -"396": [4, 45], -"397": [4, 49], -"398": [4, 53], -"399": [4, 57], -"400": [4, 61], -"401": [4, 65], -"402": [4, 69], -"403": [4, 73], -"404": [4, 77], -"405": [4, 81], -"406": [4, 85], -"407": [4, 89], -"408": [4, 93], -"409": [4, 97], -"410": [4, 101], -"411": [4, 105], -"412": [4, 109], -"413": [4, 113], -"414": [4, 117], -"415": [4, 121], -"416": [4, 125], -"417": [4, 129], -"418": [4, 133], -"419": [4, 137], -"420": [4, 141], -"421": [4, 145], -"422": [4, 149], -"423": [4, 153], -"424": [4, 157], -"425": [4, 161], -"426": [4, 165], -"427": [4, 169], -"428": [4, 173], -"429": [4, 177], -"430": [4, 181], -"431": [4, 185], -"432": [4, 189], -"433": [4, 193], -"434": [4, 197], -"435": [4, 201], -"436": [4, 205], -"437": [4, 209], -"438": [4, 213], -"439": [4, 217], -"440": [4, 221], -"441": [4, 225], -"442": [4, 229], -"443": [4, 233], -"444": [4, 237], -"445": [4, 241], -"446": [4, 245], -"447": [4, 249], -"448": [4, 253], -"449": [4, 257], -"450": [4, 261], -"451": [4, 265], -"452": [4, 269], -"453": [4, 273], -"454": [4, 277], -"455": [4, 281], -"456": [4, 285], -"457": [4, 289], -"458": [4, 293], -"459": [4, 297], -"460": [4, 301], -"461": [4, 305], -"462": [4, 309], -"463": [4, 313], -"464": [4, 317], -"465": [4, 321], -"466": [4, 325], -"467": [4, 329], -"468": [4, 333], -"469": [4, 337], -"470": [4, 341], -"471": [4, 345], -"472": [4, 349], -"473": [4, 353], -"474": [4, 357], -"475": [4, 361], -"476": [4, 365], -"477": [4, 369], -"478": [4, 373], -"479": [4, 377], -"480": [4, 381], -"481": [4, 385], -"482": [4, 389], -"483": [4, 393], -"484": [4, 397], -"485": [4, 401], -"486": [4, 405], -"487": [4, 409], -"488": [4, 413], -"489": [4, 417], -"490": [4, 421], -"491": [4, 425], -"492": [4, 429], -"493": [4, 433], -"494": [4, 437], -"495": [4, 441], -"496": [4, 445], -"497": [4, 449], -"498": [4, 453], -"499": [4, 457], -"500": [4, 461], -"501": [4, 465], -"502": [4, 469], -"503": [4, 473], -"504": [4, 477], -"505": [4, 481], -"506": [4, 485], -"507": [4, 489], -"508": [4, 493], -"509": [4, 497], -"510": [4, 501], -"511": [4, 505], -"512": [4, 509], -"513": [5, 1], -"514": [5, 5], -"515": [5, 9], -"516": [5, 13], -"517": [5, 17], -"518": [5, 21], -"519": [5, 25], -"520": [5, 29], -"521": [5, 33], -"522": [5, 37], -"523": [5, 41], -"524": [5, 45], -"525": [5, 49], -"526": [5, 53], -"527": [5, 57], -"528": [5, 61], -"529": [5, 65], -"530": [5, 69], -"531": [5, 73], -"532": [5, 77], -"533": [5, 81], -"534": [5, 85], -"535": [5, 89], -"536": [5, 93], -"537": [5, 97], -"538": [5, 101], -"539": [5, 105], -"540": [5, 109], -"541": [5, 113], -"542": [5, 117], -"543": [5, 121], -"544": [5, 125], -"545": [5, 129], -"546": [5, 133], -"547": [5, 137], -"548": [5, 141], -"549": [5, 145], -"550": [5, 149], -"551": [5, 153], -"552": [5, 157], -"553": [5, 161], -"554": [5, 165], -"555": [5, 169], -"556": [5, 173], -"557": [5, 177], -"558": [5, 181], -"559": [5, 185], -"560": [5, 189], -"561": [5, 193], -"562": [5, 197], -"563": [5, 201], -"564": [5, 205], -"565": [5, 209], -"566": [5, 213], -"567": [5, 217], -"568": [5, 221], -"569": [5, 225], -"570": [5, 229], -"571": [5, 233], -"572": [5, 237], -"573": [5, 241], -"574": [5, 245], -"575": [5, 249], -"576": [5, 253], -"577": [5, 257], -"578": [5, 261], -"579": [5, 265], -"580": [5, 269], -"581": [5, 273], -"582": [5, 277], -"583": [5, 281], -"584": [5, 285], -"585": [5, 289], -"586": [5, 293], -"587": [5, 297], -"588": [5, 301], -"589": [5, 305], -"590": [5, 309], -"591": [5, 313], -"592": [5, 317], -"593": [5, 321], -"594": [5, 325], -"595": [5, 329], -"596": [5, 333], -"597": [5, 337], -"598": [5, 341], -"599": [5, 345], -"600": [5, 349], -"601": [5, 353], -"602": [5, 357], -"603": [5, 361], -"604": [5, 365], -"605": [5, 369], -"606": [5, 373], -"607": [5, 377], -"608": [5, 381], -"609": [5, 385], -"610": [5, 389], -"611": [5, 393], -"612": [5, 397], -"613": [5, 401], -"614": [5, 405], -"615": [5, 409], -"616": [5, 413], -"617": [5, 417], -"618": [5, 421], -"619": [5, 425], -"620": [5, 429], -"621": [5, 433], -"622": [5, 437], -"623": [5, 441], -"624": [5, 445], -"625": [5, 449], -"626": [5, 453], -"627": [5, 457], -"628": [5, 461], -"629": [5, 465], -"630": [5, 469], -"631": [5, 473], -"632": [5, 477], -"633": [5, 481], -"634": [5, 485], -"635": [5, 489], -"636": [5, 493], -"637": [5, 497], -"638": [5, 501], -"639": [5, 505], -"640": [5, 509], -"641": [6, 1], -"642": [6, 5], -"643": [6, 9], -"644": [6, 13], -"645": [6, 17], -"646": [6, 21], -"647": [6, 25], -"648": [6, 29], -"649": [6, 33], -"650": [6, 37], -"651": [6, 41], -"652": [6, 45], -"653": [6, 49], -"654": [6, 53], -"655": [6, 57], -"656": [6, 61], -"657": [6, 65], -"658": [6, 69], -"659": [6, 73], -"660": [6, 77], -"661": [6, 81], -"662": [6, 85], -"663": [6, 89], -"664": [6, 93], -"665": [6, 97], -"666": [6, 101], -"667": [6, 105], -"668": [6, 109], -"669": [6, 113], -"670": [6, 117], -"671": [6, 121], -"672": [6, 125], -"673": [6, 129], -"674": [6, 133], -"675": [6, 137], -"676": [6, 141], -"677": [6, 145], -"678": [6, 149], -"679": [6, 153], -"680": [6, 157], -"681": [6, 161], -"682": [6, 165], -"683": [6, 169], -"684": [6, 173], -"685": [6, 177], -"686": [6, 181], -"687": [6, 185], -"688": [6, 189], -"689": [6, 193], -"690": [6, 197], -"691": [6, 201], -"692": [6, 205], -"693": [6, 209], -"694": [6, 213], -"695": [6, 217], -"696": [6, 221], -"697": [6, 225], -"698": [6, 229], -"699": [6, 233], -"700": [6, 237], -"701": [6, 241], -"702": [6, 245], -"703": [6, 249], -"704": [6, 253], -"705": [6, 257], -"706": [6, 261], -"707": [6, 265], -"708": [6, 269], -"709": [6, 273], -"710": [6, 277], -"711": [6, 281], -"712": [6, 285], -"713": [6, 289], -"714": [6, 293], -"715": [6, 297], -"716": [6, 301], -"717": [6, 305], -"718": [6, 309], -"719": [6, 313], -"720": [6, 317], -"721": [6, 321], -"722": [6, 325], -"723": [6, 329], -"724": [6, 333], -"725": [6, 337], -"726": [6, 341], -"727": [6, 345], -"728": [6, 349], -"729": [6, 353], -"730": [6, 357], -"731": [6, 361], -"732": [6, 365], -"733": [6, 369], -"734": [6, 373], -"735": [6, 377], -"736": [6, 381], -"737": [6, 385], -"738": [6, 389], -"739": [6, 393], -"740": [6, 397], -"741": [6, 401], -"742": [6, 405], -"743": [6, 409], -"744": [6, 413], -"745": [6, 417], -"746": [6, 421], -"747": [6, 425], -"748": [6, 429], -"749": [6, 433], -"750": [6, 437], -"751": [6, 441], -"752": [6, 445], -"753": [6, 449], -"754": [6, 453], -"755": [6, 457], -"756": [6, 461], -"757": [6, 465], -"758": [6, 469], -"759": [6, 473], -"760": [6, 477], -"761": [6, 481], -"762": [6, 485], -"763": [6, 489], -"764": [6, 493], -"765": [6, 497], -"766": [6, 501], -"767": [6, 505], -"768": [6, 509], -"769": [7, 1], -"770": [7, 5], -"771": [7, 9], -"772": [7, 13], -"773": [7, 17], -"774": [7, 21], -"775": [7, 25], -"776": [7, 29], -"777": [7, 33], -"778": [7, 37], -"779": [7, 41], -"780": [7, 45], -"781": [7, 49], -"782": [7, 53], -"783": [7, 57], -"784": [7, 61], -"785": [7, 65], -"786": [7, 69], -"787": [7, 73], -"788": [7, 77], -"789": [7, 81], -"790": [7, 85], -"791": [7, 89], -"792": [7, 93], -"793": [7, 97], -"794": [7, 101], -"795": [7, 105], -"796": [7, 109], -"797": [7, 113], -"798": [7, 117], -"799": [7, 121], -"800": [7, 125], -"801": [7, 129], -"802": [7, 133], -"803": [7, 137], -"804": [7, 141], -"805": [7, 145], -"806": [7, 149], -"807": [7, 153], -"808": [7, 157], -"809": [7, 161], -"810": [7, 165], -"811": [7, 169], -"812": [7, 173], -"813": [7, 177], -"814": [7, 181], -"815": [7, 185], -"816": [7, 189], -"817": [7, 193], -"818": [7, 197], -"819": [7, 201], -"820": [7, 205], -"821": [7, 209], -"822": [7, 213], -"823": [7, 217], -"824": [7, 221], -"825": [7, 225], -"826": [7, 229], -"827": [7, 233], -"828": [7, 237], -"829": [7, 241], -"830": [7, 245], -"831": [7, 249], -"832": [7, 253], -"833": [7, 257], -"834": [7, 261], -"835": [7, 265], -"836": [7, 269], -"837": [7, 273], -"838": [7, 277], -"839": [7, 281], -"840": [7, 285], -"841": [7, 289], -"842": [7, 293], -"843": [7, 297], -"844": [7, 301], -"845": [7, 305], -"846": [7, 309], -"847": [7, 313], -"848": [7, 317], -"849": [7, 321], -"850": [7, 325], -"851": [7, 329], -"852": [7, 333], -"853": [7, 337], -"854": [7, 341], -"855": [7, 345], -"856": [7, 349], -"857": [7, 353], -"858": [7, 357], -"859": [7, 361], -"860": [7, 365], -"861": [7, 369], -"862": [7, 373], -"863": [7, 377], -"864": [7, 381], -"865": [7, 385], -"866": [7, 389], -"867": [7, 393], -"868": [7, 397], -"869": [7, 401], -"870": [7, 405], -"871": [7, 409], -"872": [7, 413], -"873": [7, 417], -"874": [7, 421], -"875": [7, 425], -"876": [7, 429], -"877": [7, 433], -"878": [7, 437], -"879": [7, 441], -"880": [7, 445], -"881": [7, 449], -"882": [7, 453], -"883": [7, 457], -"884": [7, 461], -"885": [7, 465], -"886": [7, 469], -"887": [7, 473], -"888": [7, 477], -"889": [7, 481], -"890": [7, 485], -"891": [7, 489], -"892": [7, 493], -"893": [7, 497], -"894": [7, 501], -"895": [7, 505], -"896": [7, 509], -"897": [8, 1], -"898": [8, 5], -"899": [8, 9], -"900": [8, 13], -"901": [8, 17], -"902": [8, 21], -"903": [8, 25], -"904": [8, 29], -"905": [8, 33], -"906": [8, 37], -"907": [8, 41], -"908": [8, 45], -"909": [8, 49], -"910": [8, 53], -"911": [8, 57], -"912": [8, 61], -"913": [8, 65], -"914": [8, 69], -"915": [8, 73], -"916": [8, 77], -"917": [8, 81], -"918": [8, 85], -"919": [8, 89], -"920": [8, 93], -"921": [8, 97], -"922": [8, 101], -"923": [8, 105], -"924": [8, 109], -"925": [8, 113], -"926": [8, 117], -"927": [8, 121], -"928": [8, 125], -"929": [8, 129], -"930": [8, 133], -"931": [8, 137], -"932": [8, 141], -"933": [8, 145], -"934": [8, 149], -"935": [8, 153], -"936": [8, 157], -"937": [8, 161], -"938": [8, 165], -"939": [8, 169], -"940": [8, 173], -"941": [8, 177], -"942": [8, 181], -"943": [8, 185], -"944": [8, 189], -"945": [8, 193], -"946": [8, 197], -"947": [8, 201], -"948": [8, 205], -"949": [8, 209], -"950": [8, 213], -"951": [8, 217], -"952": [8, 221], -"953": [8, 225], -"954": [8, 229], -"955": [8, 233], -"956": [8, 237], -"957": [8, 241], -"958": [8, 245], -"959": [8, 249], -"960": [8, 253], -"961": [8, 257], -"962": [8, 261], -"963": [8, 265], -"964": [8, 269], -"965": [8, 273], -"966": [8, 277], -"967": [8, 281], -"968": [8, 285], -"969": [8, 289], -"970": [8, 293], -"971": [8, 297], -"972": [8, 301], -"973": [8, 305], -"974": [8, 309], -"975": [8, 313], -"976": [8, 317], -"977": [8, 321], -"978": [8, 325], -"979": [8, 329], -"980": [8, 333], -"981": [8, 337], -"982": [8, 341], -"983": [8, 345], -"984": [8, 349], -"985": [8, 353], -"986": [8, 357], -"987": [8, 361], -"988": [8, 365], -"989": [8, 369], -"990": [8, 373], -"991": [8, 377], -"992": [8, 381], -"993": [8, 385], -"994": [8, 389], -"995": [8, 393], -"996": [8, 397], -"997": [8, 401], -"998": [8, 405], -"999": [8, 409], -"1000": [8, 413], -"1001": [8, 417], -"1002": [8, 421], -"1003": [8, 425], -"1004": [8, 429], -"1005": [8, 433], -"1006": [8, 437], -"1007": [8, 441], -"1008": [8, 445], -"1009": [8, 449], -"1010": [8, 453], -"1011": [8, 457], -"1012": [8, 461], -"1013": [8, 465], -"1014": [8, 469], -"1015": [8, 473], -"1016": [8, 477], -"1017": [8, 481], -"1018": [8, 485], -"1019": [8, 489], -"1020": [8, 493], -"1021": [8, 497], -"1022": [8, 501], -"1023": [8, 505], -"1024": [8, 509], -"1025": [9, 1], -"1026": [9, 5], -"1027": [9, 9], -"1028": [9, 13], -"1029": [9, 17], -"1030": [9, 21], -"1031": [9, 25], -"1032": [9, 29], -"1033": [9, 33], -"1034": [9, 37], -"1035": [9, 41], -"1036": [9, 45], -"1037": [9, 49], -"1038": [9, 53], -"1039": [9, 57], -"1040": [9, 61], -"1041": [9, 65], -"1042": [9, 69], -"1043": [9, 73], -"1044": [9, 77], -"1045": [9, 81], -"1046": [9, 85], -"1047": [9, 89], -"1048": [9, 93], -"1049": [9, 97], -"1050": [9, 101], -"1051": [9, 105], -"1052": [9, 109], -"1053": [9, 113], -"1054": [9, 117], -"1055": [9, 121], -"1056": [9, 125], -"1057": [9, 129], -"1058": [9, 133], -"1059": [9, 137], -"1060": [9, 141], -"1061": [9, 145], -"1062": [9, 149], -"1063": [9, 153], -"1064": [9, 157], -"1065": [9, 161], -"1066": [9, 165], -"1067": [9, 169], -"1068": [9, 173], -"1069": [9, 177], -"1070": [9, 181], -"1071": [9, 185], -"1072": [9, 189], -"1073": [9, 193], -"1074": [9, 197], -"1075": [9, 201], -"1076": [9, 205], -"1077": [9, 209], -"1078": [9, 213], -"1079": [9, 217], -"1080": [9, 221], -"1081": [9, 225], -"1082": [9, 229], -"1083": [9, 233], -"1084": [9, 237], -"1085": [9, 241], -"1086": [9, 245], -"1087": [9, 249], -"1088": [9, 253], -"1089": [9, 257], -"1090": [9, 261], -"1091": [9, 265], -"1092": [9, 269], -"1093": [9, 273], -"1094": [9, 277], -"1095": [9, 281], -"1096": [9, 285], -"1097": [9, 289], -"1098": [9, 293], -"1099": [9, 297], -"1100": [9, 301], -"1101": [9, 305], -"1102": [9, 309], -"1103": [9, 313], -"1104": [9, 317], -"1105": [9, 321], -"1106": [9, 325], -"1107": [9, 329], -"1108": [9, 333], -"1109": [9, 337], -"1110": [9, 341], -"1111": [9, 345], -"1112": [9, 349], -"1113": [9, 353], -"1114": [9, 357], -"1115": [9, 361], -"1116": [9, 365], -"1117": [9, 369], -"1118": [9, 373], -"1119": [9, 377], -"1120": [9, 381], -"1121": [9, 385], -"1122": [9, 389], -"1123": [9, 393], -"1124": [9, 397], -"1125": [9, 401], -"1126": [9, 405], -"1127": [9, 409], -"1128": [9, 413], -"1129": [9, 417], -"1130": [9, 421], -"1131": [9, 425], -"1132": [9, 429], -"1133": [9, 433], -"1134": [9, 437], -"1135": [9, 441], -"1136": [9, 445], -"1137": [9, 449], -"1138": [9, 453], -"1139": [9, 457], -"1140": [9, 461], -"1141": [9, 465], -"1142": [9, 469], -"1143": [9, 473], -"1144": [9, 477], -"1145": [9, 481], -"1146": [9, 485], -"1147": [9, 489], -"1148": [9, 493], -"1149": [9, 497], -"1150": [9, 501], -"1151": [9, 505], -"1152": [9, 509] +"301": [4, 1], +"302": [4, 5], +"303": [4, 9], +"304": [4, 13], +"305": [4, 17], +"306": [4, 21], +"307": [4, 25], +"308": [4, 29], +"309": [4, 33], +"310": [4, 37], +"311": [4, 41], +"312": [4, 45], +"313": [4, 49], +"314": [4, 53], +"315": [4, 57], +"316": [4, 61], +"317": [4, 65], +"318": [4, 69], +"319": [4, 73], +"320": [4, 77], +"321": [4, 81], +"322": [4, 85], +"323": [4, 89], +"324": [4, 93], +"325": [4, 97], +"326": [4, 101], +"327": [4, 105], +"328": [4, 109], +"329": [4, 113], +"330": [4, 117], +"331": [4, 121], +"332": [4, 125], +"333": [4, 129], +"334": [4, 133], +"335": [4, 137], +"336": [4, 141], +"337": [4, 145], +"338": [4, 149], +"339": [4, 153], +"340": [4, 157], +"341": [4, 161], +"342": [4, 165], +"343": [4, 169], +"344": [4, 173], +"345": [4, 177], +"346": [4, 181], +"347": [4, 185], +"348": [4, 189], +"349": [4, 193], +"350": [4, 197], +"351": [4, 201], +"352": [4, 205], +"353": [4, 209], +"354": [4, 213], +"355": [4, 217], +"356": [4, 221], +"357": [4, 225], +"358": [4, 229], +"359": [4, 233], +"360": [4, 237], +"361": [4, 241], +"362": [4, 245], +"363": [4, 249], +"364": [4, 253], +"365": [4, 257], +"366": [4, 261], +"367": [4, 265], +"368": [4, 269], +"369": [4, 273], +"370": [4, 277], +"371": [4, 281], +"372": [4, 285], +"373": [4, 289], +"374": [4, 293], +"375": [4, 297], +"376": [4, 301], +"377": [4, 305], +"378": [4, 309], +"379": [4, 313], +"380": [4, 317], +"381": [4, 321], +"382": [4, 325], +"383": [4, 329], +"384": [4, 333], +"385": [4, 337], +"386": [4, 341], +"387": [4, 345], +"388": [4, 349], +"389": [4, 353], +"390": [4, 357], +"391": [4, 361], +"392": [4, 365], +"393": [4, 369], +"394": [4, 373], +"395": [4, 377], +"396": [4, 381], +"397": [4, 385], +"398": [4, 389], +"399": [4, 393], +"400": [4, 397], +"401": [4, 401], +"402": [4, 405], +"403": [4, 409], +"404": [4, 413], +"405": [4, 417], +"406": [4, 421], +"407": [4, 425], +"408": [4, 429], +"409": [4, 433], +"410": [4, 437], +"411": [4, 441], +"412": [4, 445], +"413": [4, 449], +"414": [4, 453], +"415": [4, 457], +"416": [4, 461], +"417": [4, 465], +"418": [4, 469], +"419": [4, 473], +"420": [4, 477], +"421": [4, 481], +"422": [4, 485], +"423": [4, 489], +"424": [4, 493], +"425": [4, 497], +"426": [4, 501], +"427": [4, 505], +"428": [4, 509], +"429": [5, 1], +"430": [5, 5], +"431": [5, 9], +"432": [5, 13], +"433": [5, 17], +"434": [5, 21], +"435": [5, 25], +"436": [5, 29], +"437": [5, 33], +"438": [5, 37], +"439": [5, 41], +"440": [5, 45], +"441": [5, 49], +"442": [5, 53], +"443": [5, 57], +"444": [5, 61], +"445": [5, 65], +"446": [5, 69], +"447": [5, 73], +"448": [5, 77], +"449": [5, 81], +"450": [5, 85], +"451": [5, 89], +"452": [5, 93], +"453": [5, 97], +"454": [5, 101], +"455": [5, 105], +"456": [5, 109], +"457": [5, 113], +"458": [5, 117], +"459": [5, 121], +"460": [5, 125], +"461": [5, 129], +"462": [5, 133], +"463": [5, 137], +"464": [5, 141], +"465": [5, 145], +"466": [5, 149], +"467": [5, 153], +"468": [5, 157], +"469": [5, 161], +"470": [5, 165], +"471": [5, 169], +"472": [5, 173], +"473": [5, 177], +"474": [5, 181], +"475": [5, 185], +"476": [5, 189], +"477": [5, 193], +"478": [5, 197], +"479": [5, 201], +"480": [5, 205], +"481": [5, 209], +"482": [5, 213], +"483": [5, 217], +"484": [5, 221], +"485": [5, 225], +"486": [5, 229], +"487": [5, 233], +"488": [5, 237], +"489": [5, 241], +"490": [5, 245], +"491": [5, 249], +"492": [5, 253], +"493": [5, 257], +"494": [5, 261], +"495": [5, 265], +"496": [5, 269], +"497": [5, 273], +"498": [5, 277], +"499": [5, 281], +"500": [5, 285], +"501": [5, 289], +"502": [5, 293], +"503": [5, 297], +"504": [5, 301], +"505": [5, 305], +"506": [5, 309], +"507": [5, 313], +"508": [5, 317], +"509": [5, 321], +"510": [5, 325], +"511": [5, 329], +"512": [5, 333], +"513": [5, 337], +"514": [5, 341], +"515": [5, 345], +"516": [5, 349], +"517": [5, 353], +"518": [5, 357], +"519": [5, 361], +"520": [5, 365], +"521": [5, 369], +"522": [5, 373], +"523": [5, 377], +"524": [5, 381], +"525": [5, 385], +"526": [5, 389], +"527": [5, 393], +"528": [5, 397], +"529": [5, 401], +"530": [5, 405], +"531": [5, 409], +"532": [5, 413], +"533": [5, 417], +"534": [5, 421], +"535": [5, 425], +"536": [5, 429], +"537": [5, 433], +"538": [5, 437], +"539": [5, 441], +"540": [5, 445], +"541": [5, 449], +"542": [5, 453], +"543": [5, 457], +"544": [5, 461], +"545": [5, 465], +"546": [5, 469], +"547": [5, 473], +"548": [5, 477], +"549": [5, 481], +"550": [5, 485], +"551": [5, 489], +"552": [5, 493], +"553": [5, 497], +"554": [5, 501], +"555": [5, 505], +"556": [5, 509], +"557": [6, 1], +"558": [6, 5], +"559": [6, 9], +"560": [6, 13], +"561": [6, 17], +"562": [6, 21], +"563": [6, 25], +"564": [6, 29], +"565": [6, 33], +"566": [6, 37], +"567": [6, 41], +"568": [6, 45], +"569": [6, 49], +"570": [6, 53], +"571": [6, 57], +"572": [6, 61], +"573": [6, 65], +"574": [6, 69], +"575": [6, 73], +"576": [6, 77], +"577": [6, 81], +"578": [6, 85], +"579": [6, 89], +"580": [6, 93], +"581": [6, 97], +"582": [6, 101], +"583": [6, 105], +"584": [6, 109], +"585": [6, 113], +"586": [6, 117], +"587": [6, 121], +"588": [6, 125], +"589": [6, 129], +"590": [6, 133], +"591": [6, 137], +"592": [6, 141], +"593": [6, 145], +"594": [6, 149], +"595": [6, 153], +"596": [6, 157], +"597": [6, 161], +"598": [6, 165], +"599": [6, 169], +"600": [6, 173], +"601": [7, 1], +"602": [7, 5], +"603": [7, 9], +"604": [7, 13], +"605": [7, 17], +"606": [7, 21], +"607": [7, 25], +"608": [7, 29], +"609": [7, 33], +"610": [7, 37], +"611": [7, 41], +"612": [7, 45], +"613": [7, 49], +"614": [7, 53], +"615": [7, 57], +"616": [7, 61], +"617": [7, 65], +"618": [7, 69], +"619": [7, 73], +"620": [7, 77], +"621": [7, 81], +"622": [7, 85], +"623": [7, 89], +"624": [7, 93], +"625": [7, 97], +"626": [7, 101], +"627": [7, 105], +"628": [7, 109], +"629": [7, 113], +"630": [7, 117], +"631": [7, 121], +"632": [7, 125], +"633": [7, 129], +"634": [7, 133], +"635": [7, 137], +"636": [7, 141], +"637": [7, 145], +"638": [7, 149], +"639": [7, 153], +"640": [7, 157], +"641": [7, 161], +"642": [7, 165], +"643": [7, 169], +"644": [7, 173], +"645": [7, 177], +"646": [7, 181], +"647": [7, 185], +"648": [7, 189], +"649": [7, 193], +"650": [7, 197], +"651": [7, 201], +"652": [7, 205], +"653": [7, 209], +"654": [7, 213], +"655": [7, 217], +"656": [7, 221], +"657": [7, 225], +"658": [7, 229], +"659": [7, 233], +"660": [7, 237], +"661": [7, 241], +"662": [7, 245], +"663": [7, 249], +"664": [7, 253], +"665": [7, 257], +"666": [7, 261], +"667": [7, 265], +"668": [7, 269], +"669": [7, 273], +"670": [7, 277], +"671": [7, 281], +"672": [7, 285], +"673": [7, 289], +"674": [7, 293], +"675": [7, 297], +"676": [7, 301], +"677": [7, 305], +"678": [7, 309], +"679": [7, 313], +"680": [7, 317], +"681": [7, 321], +"682": [7, 325], +"683": [7, 329], +"684": [7, 333], +"685": [7, 337], +"686": [7, 341], +"687": [7, 345], +"688": [7, 349], +"689": [7, 353], +"690": [7, 357], +"691": [7, 361], +"692": [7, 365], +"693": [7, 369], +"694": [7, 373], +"695": [7, 377], +"696": [7, 381], +"697": [7, 385], +"698": [7, 389], +"699": [7, 393], +"700": [7, 397], +"701": [7, 401], +"702": [7, 405], +"703": [7, 409], +"704": [7, 413], +"705": [7, 417], +"706": [7, 421], +"707": [7, 425], +"708": [7, 429], +"709": [7, 433], +"710": [7, 437], +"711": [7, 441], +"712": [7, 445], +"713": [7, 449], +"714": [7, 453], +"715": [7, 457], +"716": [7, 461], +"717": [7, 465], +"718": [7, 469], +"719": [7, 473], +"720": [7, 477], +"721": [7, 481], +"722": [7, 485], +"723": [7, 489], +"724": [7, 493], +"725": [7, 497], +"726": [7, 501], +"727": [7, 505], +"728": [7, 509], +"729": [8, 1], +"730": [8, 5], +"731": [8, 9], +"732": [8, 13], +"733": [8, 17], +"734": [8, 21], +"735": [8, 25], +"736": [8, 29], +"737": [8, 33], +"738": [8, 37], +"739": [8, 41], +"740": [8, 45], +"741": [8, 49], +"742": [8, 53], +"743": [8, 57], +"744": [8, 61], +"745": [8, 65], +"746": [8, 69], +"747": [8, 73], +"748": [8, 77], +"749": [8, 81], +"750": [8, 85], +"751": [8, 89], +"752": [8, 93], +"753": [8, 97], +"754": [8, 101], +"755": [8, 105], +"756": [8, 109], +"757": [8, 113], +"758": [8, 117], +"759": [8, 121], +"760": [8, 125], +"761": [8, 129], +"762": [8, 133], +"763": [8, 137], +"764": [8, 141], +"765": [8, 145], +"766": [8, 149], +"767": [8, 153], +"768": [8, 157], +"769": [8, 161], +"770": [8, 165], +"771": [8, 169], +"772": [8, 173], +"773": [8, 177], +"774": [8, 181], +"775": [8, 185], +"776": [8, 189], +"777": [8, 193], +"778": [8, 197], +"779": [8, 201], +"780": [8, 205], +"781": [8, 209], +"782": [8, 213], +"783": [8, 217], +"784": [8, 221], +"785": [8, 225], +"786": [8, 229], +"787": [8, 233], +"788": [8, 237], +"789": [8, 241], +"790": [8, 245], +"791": [8, 249], +"792": [8, 253], +"793": [8, 257], +"794": [8, 261], +"795": [8, 265], +"796": [8, 269], +"797": [8, 273], +"798": [8, 277], +"799": [8, 281], +"800": [8, 285], +"801": [8, 289], +"802": [8, 293], +"803": [8, 297], +"804": [8, 301], +"805": [8, 305], +"806": [8, 309], +"807": [8, 313], +"808": [8, 317], +"809": [8, 321], +"810": [8, 325], +"811": [8, 329], +"812": [8, 333], +"813": [8, 337], +"814": [8, 341], +"815": [8, 345], +"816": [8, 349], +"817": [8, 353], +"818": [8, 357], +"819": [8, 361], +"820": [8, 365], +"821": [8, 369], +"822": [8, 373], +"823": [8, 377], +"824": [8, 381], +"825": [8, 385], +"826": [8, 389], +"827": [8, 393], +"828": [8, 397], +"829": [8, 401], +"830": [8, 405], +"831": [8, 409], +"832": [8, 413], +"833": [8, 417], +"834": [8, 421], +"835": [8, 425], +"836": [8, 429], +"837": [8, 433], +"838": [8, 437], +"839": [8, 441], +"840": [8, 445], +"841": [8, 449], +"842": [8, 453], +"843": [8, 457], +"844": [8, 461], +"845": [8, 465], +"846": [8, 469], +"847": [8, 473], +"848": [8, 477], +"849": [8, 481], +"850": [8, 485], +"851": [8, 489], +"852": [8, 493], +"853": [8, 497], +"854": [8, 501], +"855": [8, 505], +"856": [8, 509], +"857": [9, 1], +"858": [9, 5], +"859": [9, 9], +"860": [9, 13], +"861": [9, 17], +"862": [9, 21], +"863": [9, 25], +"864": [9, 29], +"865": [9, 33], +"866": [9, 37], +"867": [9, 41], +"868": [9, 45], +"869": [9, 49], +"870": [9, 53], +"871": [9, 57], +"872": [9, 61], +"873": [9, 65], +"874": [9, 69], +"875": [9, 73], +"876": [9, 77], +"877": [9, 81], +"878": [9, 85], +"879": [9, 89], +"880": [9, 93], +"881": [9, 97], +"882": [9, 101], +"883": [9, 105], +"884": [9, 109], +"885": [9, 113], +"886": [9, 117], +"887": [9, 121], +"888": [9, 125], +"889": [9, 129], +"890": [9, 133], +"891": [9, 137], +"892": [9, 141], +"893": [9, 145], +"894": [9, 149], +"895": [9, 153], +"896": [9, 157], +"897": [9, 161], +"898": [9, 165], +"899": [9, 169], +"900": [9, 173], +"901": [10, 1], +"902": [10, 5], +"903": [10, 9], +"904": [10, 13], +"905": [10, 17], +"906": [10, 21], +"907": [10, 25], +"908": [10, 29], +"909": [10, 33], +"910": [10, 37], +"911": [10, 41], +"912": [10, 45], +"913": [10, 49], +"914": [10, 53], +"915": [10, 57], +"916": [10, 61], +"917": [10, 65], +"918": [10, 69], +"919": [10, 73], +"920": [10, 77], +"921": [10, 81], +"922": [10, 85], +"923": [10, 89], +"924": [10, 93], +"925": [10, 97], +"926": [10, 101], +"927": [10, 105], +"928": [10, 109], +"929": [10, 113], +"930": [10, 117], +"931": [10, 121], +"932": [10, 125], +"933": [10, 129], +"934": [10, 133], +"935": [10, 137], +"936": [10, 141], +"937": [10, 145], +"938": [10, 149], +"939": [10, 153], +"940": [10, 157], +"941": [10, 161], +"942": [10, 165], +"943": [10, 169], +"944": [10, 173], +"945": [10, 177], +"946": [10, 181], +"947": [10, 185], +"948": [10, 189], +"949": [10, 193], +"950": [10, 197], +"951": [10, 201], +"952": [10, 205], +"953": [10, 209], +"954": [10, 213], +"955": [10, 217], +"956": [10, 221], +"957": [10, 225], +"958": [10, 229], +"959": [10, 233], +"960": [10, 237], +"961": [10, 241], +"962": [10, 245], +"963": [10, 249], +"964": [10, 253], +"965": [10, 257], +"966": [10, 261], +"967": [10, 265], +"968": [10, 269], +"969": [10, 273], +"970": [10, 277], +"971": [10, 281], +"972": [10, 285], +"973": [10, 289], +"974": [10, 293], +"975": [10, 297], +"976": [10, 301], +"977": [10, 305], +"978": [10, 309], +"979": [10, 313], +"980": [10, 317], +"981": [10, 321], +"982": [10, 325], +"983": [10, 329], +"984": [10, 333], +"985": [10, 337], +"986": [10, 341], +"987": [10, 345], +"988": [10, 349], +"989": [10, 353], +"990": [10, 357], +"991": [10, 361], +"992": [10, 365], +"993": [10, 369], +"994": [10, 373], +"995": [10, 377], +"996": [10, 381], +"997": [10, 385], +"998": [10, 389], +"999": [10, 393], +"1000": [10, 397], +"1001": [10, 401], +"1002": [10, 405], +"1003": [10, 409], +"1004": [10, 413], +"1005": [10, 417], +"1006": [10, 421], +"1007": [10, 425], +"1008": [10, 429], +"1009": [10, 433], +"1010": [10, 437], +"1011": [10, 441], +"1012": [10, 445], +"1013": [10, 449], +"1014": [10, 453], +"1015": [10, 457], +"1016": [10, 461], +"1017": [10, 465], +"1018": [10, 469], +"1019": [10, 473], +"1020": [10, 477], +"1021": [10, 481], +"1022": [10, 485], +"1023": [10, 489], +"1024": [10, 493], +"1025": [10, 497], +"1026": [10, 501], +"1027": [10, 505], +"1028": [10, 509], +"1029": [11, 1], +"1030": [11, 5], +"1031": [11, 9], +"1032": [11, 13], +"1033": [11, 17], +"1034": [11, 21], +"1035": [11, 25], +"1036": [11, 29], +"1037": [11, 33], +"1038": [11, 37], +"1039": [11, 41], +"1040": [11, 45], +"1041": [11, 49], +"1042": [11, 53], +"1043": [11, 57], +"1044": [11, 61], +"1045": [11, 65], +"1046": [11, 69], +"1047": [11, 73], +"1048": [11, 77], +"1049": [11, 81], +"1050": [11, 85], +"1051": [11, 89], +"1052": [11, 93], +"1053": [11, 97], +"1054": [11, 101], +"1055": [11, 105], +"1056": [11, 109], +"1057": [11, 113], +"1058": [11, 117], +"1059": [11, 121], +"1060": [11, 125], +"1061": [11, 129], +"1062": [11, 133], +"1063": [11, 137], +"1064": [11, 141], +"1065": [11, 145], +"1066": [11, 149], +"1067": [11, 153], +"1068": [11, 157], +"1069": [11, 161], +"1070": [11, 165], +"1071": [11, 169], +"1072": [11, 173], +"1073": [11, 177], +"1074": [11, 181], +"1075": [11, 185], +"1076": [11, 189], +"1077": [11, 193], +"1078": [11, 197], +"1079": [11, 201], +"1080": [11, 205], +"1081": [11, 209], +"1082": [11, 213], +"1083": [11, 217], +"1084": [11, 221], +"1085": [11, 225], +"1086": [11, 229], +"1087": [11, 233], +"1088": [11, 237], +"1089": [11, 241], +"1090": [11, 245], +"1091": [11, 249], +"1092": [11, 253], +"1093": [11, 257], +"1094": [11, 261], +"1095": [11, 265], +"1096": [11, 269], +"1097": [11, 273], +"1098": [11, 277], +"1099": [11, 281], +"1100": [11, 285], +"1101": [11, 289], +"1102": [11, 293], +"1103": [11, 297], +"1104": [11, 301], +"1105": [11, 305], +"1106": [11, 309], +"1107": [11, 313], +"1108": [11, 317], +"1109": [11, 321], +"1110": [11, 325], +"1111": [11, 329], +"1112": [11, 333], +"1113": [11, 337], +"1114": [11, 341], +"1115": [11, 345], +"1116": [11, 349], +"1117": [11, 353], +"1118": [11, 357], +"1119": [11, 361], +"1120": [11, 365], +"1121": [11, 369], +"1122": [11, 373], +"1123": [11, 377], +"1124": [11, 381], +"1125": [11, 385], +"1126": [11, 389], +"1127": [11, 393], +"1128": [11, 397], +"1129": [11, 401], +"1130": [11, 405], +"1131": [11, 409], +"1132": [11, 413], +"1133": [11, 417], +"1134": [11, 421], +"1135": [11, 425], +"1136": [11, 429], +"1137": [11, 433], +"1138": [11, 437], +"1139": [11, 441], +"1140": [11, 445], +"1141": [11, 449], +"1142": [11, 453], +"1143": [11, 457], +"1144": [11, 461], +"1145": [11, 465], +"1146": [11, 469], +"1147": [11, 473], +"1148": [11, 477], +"1149": [11, 481], +"1150": [11, 485], +"1151": [11, 489], +"1152": [11, 493], +"1153": [11, 497], +"1154": [11, 501], +"1155": [11, 505], +"1156": [11, 509], +"1157": [12, 1], +"1158": [12, 5], +"1159": [12, 9], +"1160": [12, 13], +"1161": [12, 17], +"1162": [12, 21], +"1163": [12, 25], +"1164": [12, 29], +"1165": [12, 33], +"1166": [12, 37], +"1167": [12, 41], +"1168": [12, 45], +"1169": [12, 49], +"1170": [12, 53], +"1171": [12, 57], +"1172": [12, 61], +"1173": [12, 65], +"1174": [12, 69], +"1175": [12, 73], +"1176": [12, 77], +"1177": [12, 81], +"1178": [12, 85], +"1179": [12, 89], +"1180": [12, 93], +"1181": [12, 97], +"1182": [12, 101], +"1183": [12, 105], +"1184": [12, 109], +"1185": [12, 113], +"1186": [12, 117], +"1187": [12, 121], +"1188": [12, 125], +"1189": [12, 129], +"1190": [12, 133], +"1191": [12, 137], +"1192": [12, 141], +"1193": [12, 145], +"1194": [12, 149], +"1195": [12, 153], +"1196": [12, 157], +"1197": [12, 161], +"1198": [12, 165], +"1199": [12, 169], +"1200": [12, 173] } diff --git a/scripts/build_pixel_map_json.py b/scripts/build_pixel_map_json.py index 195395c..651f507 100644 --- a/scripts/build_pixel_map_json.py +++ b/scripts/build_pixel_map_json.py @@ -8,9 +8,15 @@ print("{") for ux in range(1, num_ux+1): dmx_start = 1 - while dmx_start < 512: - print('"{0}": [{1}, {2}],'.format(pixel_num,ux,dmx_start)) - pixel_num += 1 - dmx_start += 4 + if ux in (3,6,9,12): + while dmx_start < 176: + print('"{0}": [{1}, {2}],'.format(pixel_num,ux,dmx_start)) + pixel_num += 1 + dmx_start += 4 + else: + while dmx_start < 512: + print('"{0}": [{1}, {2}],'.format(pixel_num,ux,dmx_start)) + pixel_num += 1 + dmx_start += 4 print("}") From fa255d9984ec7051148dc41f54eec5a54beb033a Mon Sep 17 00:00:00 2001 From: John Major Date: Sat, 3 Aug 2019 14:54:21 -0700 Subject: [PATCH 38/60] sort of working mapping --- model/mapping.py | 246 +++++++++++++++--------------- scripts/build_tri_cell_mapping.py | 20 +-- 2 files changed, 134 insertions(+), 132 deletions(-) diff --git a/model/mapping.py b/model/mapping.py index c953410..887bd46 100644 --- a/model/mapping.py +++ b/model/mapping.py @@ -1,128 +1,130 @@ from typing import Dict, List PixelMap = Dict[int, List[int]] -# This cell -> pixels mapping is created from an old version, but it should produce something. + +# This cell -> pixels mapping is created from an old version, but it should produce something. def demo_triangle_mapping() -> PixelMap: return { -0: [1051,1052,1053,1054,1055,1056,1057,1058], -1: [1018,1019,1020,1021,1022,1023,1024,1025], -2: [1050,1051,1052,1053,1054,1055,1056,1057], -3: [1026,1027,1028,1029,1030,1031,1032,1033], -4: [969,970,971,972,973,974,975,976], -5: [1017,1018,1019,1020,1021,1022,1023,1024], -6: [977,978,979,980,981,982,983,984], -7: [1025,1026,1027,1028,1029,1030,1031,1032], -8: [985,986,987,988,989,990,991,992], -9: [904,905,906,907,908,909,910,911], -10: [968,969,970,971,972,973,974,975], -11: [912,913,914,915,916,917,918,919], -12: [976,977,978,979,980,981,982,983], -13: [920,921,922,923,924,925,926,927], -14: [984,985,986,987,988,989,990,991], -15: [928,929,930,931,932,933,934,935], -16: [823,824,825,826,827,828,829,830], -17: [903,904,905,906,907,908,909,910], -18: [831,832,833,834,835,836,837,838], -19: [911,912,913,914,915,916,917,918], -20: [839,840,841,842,843,844,845,846], -21: [919,920,921,922,923,924,925,926], -22: [847,848,849,850,851,852,853,854], -23: [927,928,929,930,931,932,933,934], -24: [855,856,857,858,859,860,861,862], -25: [726,727,728,729,730,731,732,733], -26: [822,823,824,825,826,827,828,829], -27: [734,735,736,737,738,739,740,741], -28: [830,831,832,833,834,835,836,837], -29: [742,743,744,745,746,747,748,749], -30: [838,839,840,841,842,843,844,845], -31: [750,751,752,753,754,755,756,757], -32: [846,847,848,849,850,851,852,853], -33: [758,759,760,761,762,763,764,765], -34: [854,855,856,857,858,859,860,861], -35: [766,767,768,769,770,771,772,773], -36: [613,614,615,616,617,618,619,620], -37: [725,726,727,728,729,730,731,732], -38: [621,622,623,624,625,626,627,628], -39: [733,734,735,736,737,738,739,740], -40: [629,630,631,632,633,634,635,636], -41: [741,742,743,744,745,746,747,748], -42: [637,638,639,640,641,642,643,644], -43: [749,750,751,752,753,754,755,756], -44: [645,646,647,648,649,650,651,652], -45: [757,758,759,760,761,762,763,764], -46: [653,654,655,656,657,658,659,660], -47: [765,766,767,768,769,770,771,772], -48: [661,662,663,664,665,666,667,668], -49: [484,485,486,487,488,489,490,491], -50: [612,613,614,615,616,617,618,619], -51: [492,493,494,495,496,497,498,499], -52: [620,621,622,623,624,625,626,627], -53: [500,501,502,503,504,505,506,507], -54: [628,629,630,631,632,633,634,635], -55: [508,509,510,511,512,513,514,515], -56: [636,637,638,639,640,641,642,643], -57: [516,517,518,519,520,521,522,523], -58: [644,645,646,647,648,649,650,651], -59: [524,525,526,527,528,529,530,531], -60: [652,653,654,655,656,657,658,659], -61: [532,533,534,535,536,537,538,539], -62: [660,661,662,663,664,665,666,667], -63: [540,541,542,543,544,545,546,547], -64: [339,340,341,342,343,344,345,346], -65: [483,484,485,486,487,488,489,490], -66: [347,348,349,350,351,352,353,354], -67: [491,492,493,494,495,496,497,498], -68: [355,356,357,358,359,360,361,362], -69: [499,500,501,502,503,504,505,506], -70: [363,364,365,366,367,368,369,370], -71: [507,508,509,510,511,512,513,514], -72: [371,372,373,374,375,376,377,378], -73: [515,516,517,518,519,520,521,522], -74: [379,380,381,382,383,384,385,386], -75: [523,524,525,526,527,528,529,530], -76: [387,388,389,390,391,392,393,394], -77: [531,532,533,534,535,536,537,538], -78: [395,396,397,398,399,400,401,402], -79: [539,540,541,542,543,544,545,546], -80: [403,404,405,406,407,408,409,410], -81: [178,179,180,181,182,183,184,185], -82: [338,339,340,341,342,343,344,345], -83: [186,187,188,189,190,191,192,193], -84: [346,347,348,349,350,351,352,353], -85: [194,195,196,197,198,199,200,201], -86: [354,355,356,357,358,359,360,361], -87: [202,203,204,205,206,207,208,209], -88: [362,363,364,365,366,367,368,369], -89: [210,211,212,213,214,215,216,217], -90: [370,371,372,373,374,375,376,377], -91: [218,219,220,221,222,223,224,225], -92: [378,379,380,381,382,383,384,385], -93: [226,227,228,229,230,231,232,233], -94: [386,387,388,389,390,391,392,393], -95: [234,235,236,237,238,239,240,241], -96: [394,395,396,397,398,399,400,401], -97: [242,243,244,245,246,247,248,249], -98: [402,403,404,405,406,407,408,409], -99: [250,251,252,253,254,255,256,257], -100: [1,2,3,4,5,6,7,8], -101: [177,178,179,180,181,182,183,184], -102: [9,10,11,12,13,14,15,16], -103: [185,186,187,188,189,190,191,192], -104: [17,18,19,20,21,22,23,24], -105: [193,194,195,196,197,198,199,200], -106: [25,26,27,28,29,30,31,32], -107: [201,202,203,204,205,206,207,208], -108: [33,34,35,36,37,38,39,40], -109: [209,210,211,212,213,214,215,216], -110: [41,42,43,44,45,46,47,48], -111: [217,218,219,220,221,222,223,224], -112: [49,50,51,52,53,54,55,56], -113: [225,226,227,228,229,230,231,232], -114: [57,58,59,60,61,62,63,64], -115: [233,234,235,236,237,238,239,240], -116: [65,66,67,68,69,70,71,72], -117: [241,242,243,244,245,246,247,248], -118: [73,74,75,76,77,78,79,80], -119: [249,250,251,252,253,254,255,256], -120: [81,82,83,84,85,86,87,88] + #Wiring coming in from the bottom left, this is the order of cells following the pixels +0: [1059,1058,1057,1056,1055,1054,1053,1052], +1: [1042,1041,1040,1039,1038,1037,1036,1035], +2: [1034,1033,1032,1031,1030,1029,1028,1027], +3: [1026,1025,1024,1023,1022,1021,1020,1019], +4: [1009,1008,1007,1006,1005,1004,1003,1002], +5: [1001,1000,999,998,997,996,995,994], +6: [993,992,991,990,989,988,987,986], +7: [985,984,983,982,981,980,979,978], +8: [977,976,975,974,973,972,971,970], +9: [960,959,958,957,956,955,954,953], +10: [952,951,950,949,948,947,946,945], +11: [944,943,942,941,940,939,938,937], +12: [936,935,934,933,932,931,930,929], +13: [928,927,926,925,924,923,922,921], +14: [920,919,918,917,916,915,914,913], +15: [912,911,910,909,908,907,906,905], +16: [895,894,893,892,891,890,889,888], +17: [887,886,885,884,883,882,881,880], +18: [879,878,877,876,875,874,873,872], +19: [871,870,869,868,867,866,865,864], +20: [863,862,861,860,859,858,857,856], +21: [855,854,853,852,851,850,849,848], +22: [847,846,845,844,843,842,841,840], +23: [839,838,837,836,835,834,833,832], +24: [831,830,829,828,827,826,825,824], +25: [814,813,812,811,810,809,808,807], +26: [806,805,804,803,802,801,800,799], +27: [798,797,796,795,794,793,792,791], +28: [790,789,788,787,786,785,784,783], +29: [782,781,780,779,778,777,776,775], +30: [774,773,772,771,770,769,768,767], +31: [766,765,764,763,762,761,760,759], +32: [758,757,756,755,754,753,752,751], +33: [750,749,748,747,746,745,744,743], +34: [742,741,740,739,738,737,736,735], +35: [734,733,732,731,730,729,728,727], +36: [717,716,715,714,713,712,711,710], +37: [709,708,707,706,705,704,703,702], +38: [701,700,699,698,697,696,695,694], +39: [693,692,691,690,689,688,687,686], +40: [685,684,683,682,681,680,679,678], +41: [677,676,675,674,673,672,671,670], +42: [669,668,667,666,665,664,663,662], +43: [661,660,659,658,657,656,655,654], +44: [653,652,651,650,649,648,647,646], +45: [645,644,643,642,641,640,639,638], +46: [637,636,635,634,633,632,631,630], +47: [629,628,627,626,625,624,623,622], +48: [621,620,619,618,617,616,615,614], +49: [604,603,602,601,600,599,598,597], +50: [596,595,594,593,592,591,590,589], +51: [588,587,586,585,584,583,582,581], +52: [580,579,578,577,576,575,574,573], +53: [572,571,570,569,568,567,566,565], +54: [564,563,562,561,560,559,558,557], +55: [556,555,554,553,552,551,550,549], +56: [548,547,546,545,544,543,542,541], +57: [540,539,538,537,536,535,534,533], +58: [532,531,530,529,528,527,526,525], +59: [524,523,522,521,520,519,518,517], +60: [516,515,514,513,512,511,510,509], +61: [508,507,506,505,504,503,502,501], +62: [500,499,498,497,496,495,494,493], +63: [492,491,490,489,488,487,486,485], +64: [475,474,473,472,471,470,469,468], +65: [467,466,465,464,463,462,461,460], +66: [459,458,457,456,455,454,453,452], +67: [451,450,449,448,447,446,445,444], +68: [443,442,441,440,439,438,437,436], +69: [435,434,433,432,431,430,429,428], +70: [427,426,425,424,423,422,421,420], +71: [419,418,417,416,415,414,413,412], +72: [411,410,409,408,407,406,405,404], +73: [403,402,401,400,399,398,397,396], +74: [395,394,393,392,391,390,389,388], +75: [387,386,385,384,383,382,381,380], +76: [379,378,377,376,375,374,373,372], +77: [371,370,369,368,367,366,365,364], +78: [363,362,361,360,359,358,357,356], +79: [355,354,353,352,351,350,349,348], +80: [347,346,345,344,343,342,341,340], +81: [330,329,328,327,326,325,324,323], +82: [322,321,320,319,318,317,316,315], +83: [314,313,312,311,310,309,308,307], +84: [306,305,304,303,302,301,300,299], +85: [298,297,296,295,294,293,292,291], +86: [290,289,288,287,286,285,284,283], +87: [282,281,280,279,278,277,276,275], +88: [274,273,272,271,270,269,268,267], +89: [266,265,264,263,262,261,260,259], +90: [258,257,256,255,254,253,252,251], +91: [250,249,248,247,246,245,244,243], +92: [242,241,240,239,238,237,236,235], +93: [234,233,232,231,230,229,228,227], +94: [226,225,224,223,222,221,220,219], +95: [218,217,216,215,214,213,212,211], +96: [210,209,208,207,206,205,204,203], +97: [202,201,200,199,198,197,196,195], +98: [194,193,192,191,190,189,188,187], +99: [186,185,184,183,182,181,180,179], +100: [169,168,167,166,165,164,163,162], +101: [161,160,159,158,157,156,155,154], +102: [153,152,151,150,149,148,147,146], +103: [145,144,143,142,141,140,139,138], +104: [137,136,135,134,133,132,131,130], +105: [129,128,127,126,125,124,123,122], +106: [121,120,119,118,117,116,115,114], +107: [113,112,111,110,109,108,107,106], +108: [105,104,103,102,101,100,99,98], +109: [97,96,95,94,93,92,91,90], +110: [89,88,87,86,85,84,83,82], +111: [81,80,79,78,77,76,75,74], +112: [73,72,71,70,69,68,67,66], +113: [65,64,63,62,61,60,59,58], +114: [57,56,55,54,53,52,51,50], +115: [49,48,47,46,45,44,43,42], +116: [41,40,39,38,37,36,35,34], +117: [33,32,31,30,29,28,27,26], +118: [25,24,23,22,21,20,19,18], +119: [17,16,15,14,13,12,11,10], +120: [9,8,7,6,5,4,3,2] } diff --git a/scripts/build_tri_cell_mapping.py b/scripts/build_tri_cell_mapping.py index ab3dc1e..50ec0f1 100644 --- a/scripts/build_tri_cell_mapping.py +++ b/scripts/build_tri_cell_mapping.py @@ -5,16 +5,16 @@ #Built from https://docs.google.com/spreadsheets/d/16Ys242V437N968W5UU2WQdonFJ6SwZJeYmEXGbC9cYo/edit#gid=0 ds = {1: [1, 1051,0], - 2: [3, 1018,1050], - 3: [5, 969, 1017], - 4: [7, 904, 968], - 5: [9, 823, 903], - 6: [11, 726,822], - 7: [13, 613, 725], - 8: [15, 484,612], - 9: [17, 339, 483], - 10: [19, 178,338], - 11: [21, 1, 177] + 2: [3, 1018,1050+8], + 3: [5, 969, 1017+8], + 4: [7, 904, 968+8], + 5: [9, 823, 903+8], + 6: [11, 726,822+8], + 7: [13, 613, 725+8], + 8: [15, 484,612+8], + 9: [17, 339, 483+8], + 10: [19, 178,338+8], + 11: [21, 1, 177+8] } for row in ds: From 4a569554eec35925d366368780c3ed4716e1280d Mon Sep 17 00:00:00 2001 From: Lyra Naeseth Date: Sun, 4 Aug 2019 01:12:14 -0700 Subject: [PATCH 39/60] Add new coordinate to address mapping --- grid/cells.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ investigate.py | 40 ++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 grid/cells.py create mode 100644 investigate.py diff --git a/grid/cells.py b/grid/cells.py new file mode 100644 index 0000000..d506945 --- /dev/null +++ b/grid/cells.py @@ -0,0 +1,100 @@ +from itertools import chain +from typing import List, Mapping, NamedTuple + + +class Position(NamedTuple): + row: int + col: int + + @property + def x(self) -> int: + return self.col + + @property + def y(self) -> int: + return self.row + + +UNIVERSES = { + 1: 512, + 2: 512, + 3: 44 * 4, + 4: 512, + 5: 512, + 6: 44 * 4, + 7: 512, + 8: 512, + 9: 44 * 4, + 10: 512, + 11: 512, +} + + +class Address(NamedTuple): + universe: int + offset: int + + @property + def next(self) -> "Address": + next_offset = self.offset + 4 + if next_offset >= UNIVERSES[self.universe]: + return Address(self.universe + 1, 0) + + return Address(self.universe, next_offset) + + def range(self, len: int) -> List["Address"]: + addrs = [self] + for _ in range(len - 1): + addrs.append(addrs[-1].next) + + return addrs + + +def generate(rows: int = 11, start: Address = Address(1, 4)) -> Mapping[Position, List[Address]]: + cells = {} + for row in range(rows - 1, -1, -1): + row_mapping = mouth(row, start) + cells.update(row_mapping) + + end_address = max(chain.from_iterable(row_mapping.values())) + start = end_address.next + + return cells + + +def mouth(row: int, start: Address) -> Mapping[Position, List[Address]]: + up = up_teeth(row, start, row + 1) + last_up_address = up[max(up)][-1] + first_after_gap = last_up_address.range(11)[-1] + down = down_teeth(row, first_after_gap, row) + + return {**up, **down} + + +def up_teeth(row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, List[Address]]: + cells = {} + addr = start + + for i in range(length): + addrs = addr.range(pixels_per_cell) + cells[Position(row, i * 2)] = addrs + addr = addrs[-1].next + + return cells + + +def down_teeth(row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, List[Address]]: + cells = {} + addr = start + + col = length * 2 - 1 + for i in range(length): + addrs = addr.range(pixels_per_cell) + cells[Position(row, col)] = addrs + col -= 2 + addr = addrs[-1].next + + return cells + + +CELLS = generate() diff --git a/investigate.py b/investigate.py new file mode 100644 index 0000000..8e5328f --- /dev/null +++ b/investigate.py @@ -0,0 +1,40 @@ +from itertools import cycle +import pprint +import time + +from model import sACN, demo_triangle_mapping +import grid +from grid.cells import CELLS, Position + +model = sACN(model_json="./data/pixel_map.json", + pixelmap=demo_triangle_mapping()) +tri = grid.make_triangle(model, 2) + +tri.go() + +colors = cycle(range(3)) +for cell in sorted(CELLS): + print(cell) + co = next(colors) + + for addr in CELLS[cell]: + model.leds[addr.universe][addr.offset + co] = 128 + model.leds[addr.universe][addr.offset + 3] = 64 + + tri.go() + time.sleep(0.1) + + +# for i in range(511, 0, -4): +# model.leds[3][i] = 200 +# tri.go() +# time.sleep(0.02) + +# for d in range(1, 13): +# u = model.leds[d] +# u[3] = 200 +# if d > 1: +# model.leds[d - 1][510] = 200 +# model.leds[d - 1][505] = 200 +# model.leds[d - 1][500] = 200 +# tri.go() From be124002b31fecb03a997bf1645f8850da1e4aef Mon Sep 17 00:00:00 2001 From: Lyra Naeseth Date: Mon, 5 Aug 2019 22:52:54 -0700 Subject: [PATCH 40/60] Integrate new grid cell code --- color.py | 2 +- data/pixel_map.json | 1202 ------------------------------- go_tri.py | 71 +- grid/__init__.py | 7 +- grid/cell.py | 243 +++++++ grid/cells.py | 100 --- grid/grid.py | 360 ++------- grid/select.py | 140 ++++ grid/traversal.py | 81 +-- model/__init__.py | 7 +- model/base.py | 26 + model/mapping.py | 130 ---- model/mirror.py | 27 +- model/modelbase.py | 24 - model/ola_model.py | 3 +- model/sacn_model.py | 75 +- model/simulator.py | 7 +- poetry.lock | 11 +- pyproject.toml | 1 + shows/__init__.py | 2 +- shows/cycle_hsv.py | 57 +- shows/left_to_right.py | 31 +- shows/left_to_right_and_back.py | 22 +- shows/marching_hexes.py | 32 +- shows/one_by_one.py | 17 +- shows/random_cells.py | 27 +- shows/strobe.py | 29 +- shows/up_down.py | 32 +- shows/warp.py | 12 +- 29 files changed, 723 insertions(+), 2055 deletions(-) delete mode 100644 data/pixel_map.json create mode 100644 grid/cell.py delete mode 100644 grid/cells.py create mode 100644 grid/select.py create mode 100644 model/base.py delete mode 100644 model/mapping.py delete mode 100644 model/modelbase.py diff --git a/color.py b/color.py index e1e4d0b..5f84cdf 100644 --- a/color.py +++ b/color.py @@ -335,7 +335,7 @@ def Hex(value): rgb_t = (int(value[i:i+int(lv/3)], 16) for i in range(0, lv, int(lv/3))) return RGB(*rgb_t) -class Color(object): +class Color: def __init__(self, hsv_tuple, only_rgb=False): self._set_hsv(hsv_tuple) self.only_rgb = only_rgb diff --git a/data/pixel_map.json b/data/pixel_map.json deleted file mode 100644 index aad874e..0000000 --- a/data/pixel_map.json +++ /dev/null @@ -1,1202 +0,0 @@ -{ -"1": [1, 1], -"2": [1, 5], -"3": [1, 9], -"4": [1, 13], -"5": [1, 17], -"6": [1, 21], -"7": [1, 25], -"8": [1, 29], -"9": [1, 33], -"10": [1, 37], -"11": [1, 41], -"12": [1, 45], -"13": [1, 49], -"14": [1, 53], -"15": [1, 57], -"16": [1, 61], -"17": [1, 65], -"18": [1, 69], -"19": [1, 73], -"20": [1, 77], -"21": [1, 81], -"22": [1, 85], -"23": [1, 89], -"24": [1, 93], -"25": [1, 97], -"26": [1, 101], -"27": [1, 105], -"28": [1, 109], -"29": [1, 113], -"30": [1, 117], -"31": [1, 121], -"32": [1, 125], -"33": [1, 129], -"34": [1, 133], -"35": [1, 137], -"36": [1, 141], -"37": [1, 145], -"38": [1, 149], -"39": [1, 153], -"40": [1, 157], -"41": [1, 161], -"42": [1, 165], -"43": [1, 169], -"44": [1, 173], -"45": [1, 177], -"46": [1, 181], -"47": [1, 185], -"48": [1, 189], -"49": [1, 193], -"50": [1, 197], -"51": [1, 201], -"52": [1, 205], -"53": [1, 209], -"54": [1, 213], -"55": [1, 217], -"56": [1, 221], -"57": [1, 225], -"58": [1, 229], -"59": [1, 233], -"60": [1, 237], -"61": [1, 241], -"62": [1, 245], -"63": [1, 249], -"64": [1, 253], -"65": [1, 257], -"66": [1, 261], -"67": [1, 265], -"68": [1, 269], -"69": [1, 273], -"70": [1, 277], -"71": [1, 281], -"72": [1, 285], -"73": [1, 289], -"74": [1, 293], -"75": [1, 297], -"76": [1, 301], -"77": [1, 305], -"78": [1, 309], -"79": [1, 313], -"80": [1, 317], -"81": [1, 321], -"82": [1, 325], -"83": [1, 329], -"84": [1, 333], -"85": [1, 337], -"86": [1, 341], -"87": [1, 345], -"88": [1, 349], -"89": [1, 353], -"90": [1, 357], -"91": [1, 361], -"92": [1, 365], -"93": [1, 369], -"94": [1, 373], -"95": [1, 377], -"96": [1, 381], -"97": [1, 385], -"98": [1, 389], -"99": [1, 393], -"100": [1, 397], -"101": [1, 401], -"102": [1, 405], -"103": [1, 409], -"104": [1, 413], -"105": [1, 417], -"106": [1, 421], -"107": [1, 425], -"108": [1, 429], -"109": [1, 433], -"110": [1, 437], -"111": [1, 441], -"112": [1, 445], -"113": [1, 449], -"114": [1, 453], -"115": [1, 457], -"116": [1, 461], -"117": [1, 465], -"118": [1, 469], -"119": [1, 473], -"120": [1, 477], -"121": [1, 481], -"122": [1, 485], -"123": [1, 489], -"124": [1, 493], -"125": [1, 497], -"126": [1, 501], -"127": [1, 505], -"128": [1, 509], -"129": [2, 1], -"130": [2, 5], -"131": [2, 9], -"132": [2, 13], -"133": [2, 17], -"134": [2, 21], -"135": [2, 25], -"136": [2, 29], -"137": [2, 33], -"138": [2, 37], -"139": [2, 41], -"140": [2, 45], -"141": [2, 49], -"142": [2, 53], -"143": [2, 57], -"144": [2, 61], -"145": [2, 65], -"146": [2, 69], -"147": [2, 73], -"148": [2, 77], -"149": [2, 81], -"150": [2, 85], -"151": [2, 89], -"152": [2, 93], -"153": [2, 97], -"154": [2, 101], -"155": [2, 105], -"156": [2, 109], -"157": [2, 113], -"158": [2, 117], -"159": [2, 121], -"160": [2, 125], -"161": [2, 129], -"162": [2, 133], -"163": [2, 137], -"164": [2, 141], -"165": [2, 145], -"166": [2, 149], -"167": [2, 153], -"168": [2, 157], -"169": [2, 161], -"170": [2, 165], -"171": [2, 169], -"172": [2, 173], -"173": [2, 177], -"174": [2, 181], -"175": [2, 185], -"176": [2, 189], -"177": [2, 193], -"178": [2, 197], -"179": [2, 201], -"180": [2, 205], -"181": [2, 209], -"182": [2, 213], -"183": [2, 217], -"184": [2, 221], -"185": [2, 225], -"186": [2, 229], -"187": [2, 233], -"188": [2, 237], -"189": [2, 241], -"190": [2, 245], -"191": [2, 249], -"192": [2, 253], -"193": [2, 257], -"194": [2, 261], -"195": [2, 265], -"196": [2, 269], -"197": [2, 273], -"198": [2, 277], -"199": [2, 281], -"200": [2, 285], -"201": [2, 289], -"202": [2, 293], -"203": [2, 297], -"204": [2, 301], -"205": [2, 305], -"206": [2, 309], -"207": [2, 313], -"208": [2, 317], -"209": [2, 321], -"210": [2, 325], -"211": [2, 329], -"212": [2, 333], -"213": [2, 337], -"214": [2, 341], -"215": [2, 345], -"216": [2, 349], -"217": [2, 353], -"218": [2, 357], -"219": [2, 361], -"220": [2, 365], -"221": [2, 369], -"222": [2, 373], -"223": [2, 377], -"224": [2, 381], -"225": [2, 385], -"226": [2, 389], -"227": [2, 393], -"228": [2, 397], -"229": [2, 401], -"230": [2, 405], -"231": [2, 409], -"232": [2, 413], -"233": [2, 417], -"234": [2, 421], -"235": [2, 425], -"236": [2, 429], -"237": [2, 433], -"238": [2, 437], -"239": [2, 441], -"240": [2, 445], -"241": [2, 449], -"242": [2, 453], -"243": [2, 457], -"244": [2, 461], -"245": [2, 465], -"246": [2, 469], -"247": [2, 473], -"248": [2, 477], -"249": [2, 481], -"250": [2, 485], -"251": [2, 489], -"252": [2, 493], -"253": [2, 497], -"254": [2, 501], -"255": [2, 505], -"256": [2, 509], -"257": [3, 1], -"258": [3, 5], -"259": [3, 9], -"260": [3, 13], -"261": [3, 17], -"262": [3, 21], -"263": [3, 25], -"264": [3, 29], -"265": [3, 33], -"266": [3, 37], -"267": [3, 41], -"268": [3, 45], -"269": [3, 49], -"270": [3, 53], -"271": [3, 57], -"272": [3, 61], -"273": [3, 65], -"274": [3, 69], -"275": [3, 73], -"276": [3, 77], -"277": [3, 81], -"278": [3, 85], -"279": [3, 89], -"280": [3, 93], -"281": [3, 97], -"282": [3, 101], -"283": [3, 105], -"284": [3, 109], -"285": [3, 113], -"286": [3, 117], -"287": [3, 121], -"288": [3, 125], -"289": [3, 129], -"290": [3, 133], -"291": [3, 137], -"292": [3, 141], -"293": [3, 145], -"294": [3, 149], -"295": [3, 153], -"296": [3, 157], -"297": [3, 161], -"298": [3, 165], -"299": [3, 169], -"300": [3, 173], -"301": [4, 1], -"302": [4, 5], -"303": [4, 9], -"304": [4, 13], -"305": [4, 17], -"306": [4, 21], -"307": [4, 25], -"308": [4, 29], -"309": [4, 33], -"310": [4, 37], -"311": [4, 41], -"312": [4, 45], -"313": [4, 49], -"314": [4, 53], -"315": [4, 57], -"316": [4, 61], -"317": [4, 65], -"318": [4, 69], -"319": [4, 73], -"320": [4, 77], -"321": [4, 81], -"322": [4, 85], -"323": [4, 89], -"324": [4, 93], -"325": [4, 97], -"326": [4, 101], -"327": [4, 105], -"328": [4, 109], -"329": [4, 113], -"330": [4, 117], -"331": [4, 121], -"332": [4, 125], -"333": [4, 129], -"334": [4, 133], -"335": [4, 137], -"336": [4, 141], -"337": [4, 145], -"338": [4, 149], -"339": [4, 153], -"340": [4, 157], -"341": [4, 161], -"342": [4, 165], -"343": [4, 169], -"344": [4, 173], -"345": [4, 177], -"346": [4, 181], -"347": [4, 185], -"348": [4, 189], -"349": [4, 193], -"350": [4, 197], -"351": [4, 201], -"352": [4, 205], -"353": [4, 209], -"354": [4, 213], -"355": [4, 217], -"356": [4, 221], -"357": [4, 225], -"358": [4, 229], -"359": [4, 233], -"360": [4, 237], -"361": [4, 241], -"362": [4, 245], -"363": [4, 249], -"364": [4, 253], -"365": [4, 257], -"366": [4, 261], -"367": [4, 265], -"368": [4, 269], -"369": [4, 273], -"370": [4, 277], -"371": [4, 281], -"372": [4, 285], -"373": [4, 289], -"374": [4, 293], -"375": [4, 297], -"376": [4, 301], -"377": [4, 305], -"378": [4, 309], -"379": [4, 313], -"380": [4, 317], -"381": [4, 321], -"382": [4, 325], -"383": [4, 329], -"384": [4, 333], -"385": [4, 337], -"386": [4, 341], -"387": [4, 345], -"388": [4, 349], -"389": [4, 353], -"390": [4, 357], -"391": [4, 361], -"392": [4, 365], -"393": [4, 369], -"394": [4, 373], -"395": [4, 377], -"396": [4, 381], -"397": [4, 385], -"398": [4, 389], -"399": [4, 393], -"400": [4, 397], -"401": [4, 401], -"402": [4, 405], -"403": [4, 409], -"404": [4, 413], -"405": [4, 417], -"406": [4, 421], -"407": [4, 425], -"408": [4, 429], -"409": [4, 433], -"410": [4, 437], -"411": [4, 441], -"412": [4, 445], -"413": [4, 449], -"414": [4, 453], -"415": [4, 457], -"416": [4, 461], -"417": [4, 465], -"418": [4, 469], -"419": [4, 473], -"420": [4, 477], -"421": [4, 481], -"422": [4, 485], -"423": [4, 489], -"424": [4, 493], -"425": [4, 497], -"426": [4, 501], -"427": [4, 505], -"428": [4, 509], -"429": [5, 1], -"430": [5, 5], -"431": [5, 9], -"432": [5, 13], -"433": [5, 17], -"434": [5, 21], -"435": [5, 25], -"436": [5, 29], -"437": [5, 33], -"438": [5, 37], -"439": [5, 41], -"440": [5, 45], -"441": [5, 49], -"442": [5, 53], -"443": [5, 57], -"444": [5, 61], -"445": [5, 65], -"446": [5, 69], -"447": [5, 73], -"448": [5, 77], -"449": [5, 81], -"450": [5, 85], -"451": [5, 89], -"452": [5, 93], -"453": [5, 97], -"454": [5, 101], -"455": [5, 105], -"456": [5, 109], -"457": [5, 113], -"458": [5, 117], -"459": [5, 121], -"460": [5, 125], -"461": [5, 129], -"462": [5, 133], -"463": [5, 137], -"464": [5, 141], -"465": [5, 145], -"466": [5, 149], -"467": [5, 153], -"468": [5, 157], -"469": [5, 161], -"470": [5, 165], -"471": [5, 169], -"472": [5, 173], -"473": [5, 177], -"474": [5, 181], -"475": [5, 185], -"476": [5, 189], -"477": [5, 193], -"478": [5, 197], -"479": [5, 201], -"480": [5, 205], -"481": [5, 209], -"482": [5, 213], -"483": [5, 217], -"484": [5, 221], -"485": [5, 225], -"486": [5, 229], -"487": [5, 233], -"488": [5, 237], -"489": [5, 241], -"490": [5, 245], -"491": [5, 249], -"492": [5, 253], -"493": [5, 257], -"494": [5, 261], -"495": [5, 265], -"496": [5, 269], -"497": [5, 273], -"498": [5, 277], -"499": [5, 281], -"500": [5, 285], -"501": [5, 289], -"502": [5, 293], -"503": [5, 297], -"504": [5, 301], -"505": [5, 305], -"506": [5, 309], -"507": [5, 313], -"508": [5, 317], -"509": [5, 321], -"510": [5, 325], -"511": [5, 329], -"512": [5, 333], -"513": [5, 337], -"514": [5, 341], -"515": [5, 345], -"516": [5, 349], -"517": [5, 353], -"518": [5, 357], -"519": [5, 361], -"520": [5, 365], -"521": [5, 369], -"522": [5, 373], -"523": [5, 377], -"524": [5, 381], -"525": [5, 385], -"526": [5, 389], -"527": [5, 393], -"528": [5, 397], -"529": [5, 401], -"530": [5, 405], -"531": [5, 409], -"532": [5, 413], -"533": [5, 417], -"534": [5, 421], -"535": [5, 425], -"536": [5, 429], -"537": [5, 433], -"538": [5, 437], -"539": [5, 441], -"540": [5, 445], -"541": [5, 449], -"542": [5, 453], -"543": [5, 457], -"544": [5, 461], -"545": [5, 465], -"546": [5, 469], -"547": [5, 473], -"548": [5, 477], -"549": [5, 481], -"550": [5, 485], -"551": [5, 489], -"552": [5, 493], -"553": [5, 497], -"554": [5, 501], -"555": [5, 505], -"556": [5, 509], -"557": [6, 1], -"558": [6, 5], -"559": [6, 9], -"560": [6, 13], -"561": [6, 17], -"562": [6, 21], -"563": [6, 25], -"564": [6, 29], -"565": [6, 33], -"566": [6, 37], -"567": [6, 41], -"568": [6, 45], -"569": [6, 49], -"570": [6, 53], -"571": [6, 57], -"572": [6, 61], -"573": [6, 65], -"574": [6, 69], -"575": [6, 73], -"576": [6, 77], -"577": [6, 81], -"578": [6, 85], -"579": [6, 89], -"580": [6, 93], -"581": [6, 97], -"582": [6, 101], -"583": [6, 105], -"584": [6, 109], -"585": [6, 113], -"586": [6, 117], -"587": [6, 121], -"588": [6, 125], -"589": [6, 129], -"590": [6, 133], -"591": [6, 137], -"592": [6, 141], -"593": [6, 145], -"594": [6, 149], -"595": [6, 153], -"596": [6, 157], -"597": [6, 161], -"598": [6, 165], -"599": [6, 169], -"600": [6, 173], -"601": [7, 1], -"602": [7, 5], -"603": [7, 9], -"604": [7, 13], -"605": [7, 17], -"606": [7, 21], -"607": [7, 25], -"608": [7, 29], -"609": [7, 33], -"610": [7, 37], -"611": [7, 41], -"612": [7, 45], -"613": [7, 49], -"614": [7, 53], -"615": [7, 57], -"616": [7, 61], -"617": [7, 65], -"618": [7, 69], -"619": [7, 73], -"620": [7, 77], -"621": [7, 81], -"622": [7, 85], -"623": [7, 89], -"624": [7, 93], -"625": [7, 97], -"626": [7, 101], -"627": [7, 105], -"628": [7, 109], -"629": [7, 113], -"630": [7, 117], -"631": [7, 121], -"632": [7, 125], -"633": [7, 129], -"634": [7, 133], -"635": [7, 137], -"636": [7, 141], -"637": [7, 145], -"638": [7, 149], -"639": [7, 153], -"640": [7, 157], -"641": [7, 161], -"642": [7, 165], -"643": [7, 169], -"644": [7, 173], -"645": [7, 177], -"646": [7, 181], -"647": [7, 185], -"648": [7, 189], -"649": [7, 193], -"650": [7, 197], -"651": [7, 201], -"652": [7, 205], -"653": [7, 209], -"654": [7, 213], -"655": [7, 217], -"656": [7, 221], -"657": [7, 225], -"658": [7, 229], -"659": [7, 233], -"660": [7, 237], -"661": [7, 241], -"662": [7, 245], -"663": [7, 249], -"664": [7, 253], -"665": [7, 257], -"666": [7, 261], -"667": [7, 265], -"668": [7, 269], -"669": [7, 273], -"670": [7, 277], -"671": [7, 281], -"672": [7, 285], -"673": [7, 289], -"674": [7, 293], -"675": [7, 297], -"676": [7, 301], -"677": [7, 305], -"678": [7, 309], -"679": [7, 313], -"680": [7, 317], -"681": [7, 321], -"682": [7, 325], -"683": [7, 329], -"684": [7, 333], -"685": [7, 337], -"686": [7, 341], -"687": [7, 345], -"688": [7, 349], -"689": [7, 353], -"690": [7, 357], -"691": [7, 361], -"692": [7, 365], -"693": [7, 369], -"694": [7, 373], -"695": [7, 377], -"696": [7, 381], -"697": [7, 385], -"698": [7, 389], -"699": [7, 393], -"700": [7, 397], -"701": [7, 401], -"702": [7, 405], -"703": [7, 409], -"704": [7, 413], -"705": [7, 417], -"706": [7, 421], -"707": [7, 425], -"708": [7, 429], -"709": [7, 433], -"710": [7, 437], -"711": [7, 441], -"712": [7, 445], -"713": [7, 449], -"714": [7, 453], -"715": [7, 457], -"716": [7, 461], -"717": [7, 465], -"718": [7, 469], -"719": [7, 473], -"720": [7, 477], -"721": [7, 481], -"722": [7, 485], -"723": [7, 489], -"724": [7, 493], -"725": [7, 497], -"726": [7, 501], -"727": [7, 505], -"728": [7, 509], -"729": [8, 1], -"730": [8, 5], -"731": [8, 9], -"732": [8, 13], -"733": [8, 17], -"734": [8, 21], -"735": [8, 25], -"736": [8, 29], -"737": [8, 33], -"738": [8, 37], -"739": [8, 41], -"740": [8, 45], -"741": [8, 49], -"742": [8, 53], -"743": [8, 57], -"744": [8, 61], -"745": [8, 65], -"746": [8, 69], -"747": [8, 73], -"748": [8, 77], -"749": [8, 81], -"750": [8, 85], -"751": [8, 89], -"752": [8, 93], -"753": [8, 97], -"754": [8, 101], -"755": [8, 105], -"756": [8, 109], -"757": [8, 113], -"758": [8, 117], -"759": [8, 121], -"760": [8, 125], -"761": [8, 129], -"762": [8, 133], -"763": [8, 137], -"764": [8, 141], -"765": [8, 145], -"766": [8, 149], -"767": [8, 153], -"768": [8, 157], -"769": [8, 161], -"770": [8, 165], -"771": [8, 169], -"772": [8, 173], -"773": [8, 177], -"774": [8, 181], -"775": [8, 185], -"776": [8, 189], -"777": [8, 193], -"778": [8, 197], -"779": [8, 201], -"780": [8, 205], -"781": [8, 209], -"782": [8, 213], -"783": [8, 217], -"784": [8, 221], -"785": [8, 225], -"786": [8, 229], -"787": [8, 233], -"788": [8, 237], -"789": [8, 241], -"790": [8, 245], -"791": [8, 249], -"792": [8, 253], -"793": [8, 257], -"794": [8, 261], -"795": [8, 265], -"796": [8, 269], -"797": [8, 273], -"798": [8, 277], -"799": [8, 281], -"800": [8, 285], -"801": [8, 289], -"802": [8, 293], -"803": [8, 297], -"804": [8, 301], -"805": [8, 305], -"806": [8, 309], -"807": [8, 313], -"808": [8, 317], -"809": [8, 321], -"810": [8, 325], -"811": [8, 329], -"812": [8, 333], -"813": [8, 337], -"814": [8, 341], -"815": [8, 345], -"816": [8, 349], -"817": [8, 353], -"818": [8, 357], -"819": [8, 361], -"820": [8, 365], -"821": [8, 369], -"822": [8, 373], -"823": [8, 377], -"824": [8, 381], -"825": [8, 385], -"826": [8, 389], -"827": [8, 393], -"828": [8, 397], -"829": [8, 401], -"830": [8, 405], -"831": [8, 409], -"832": [8, 413], -"833": [8, 417], -"834": [8, 421], -"835": [8, 425], -"836": [8, 429], -"837": [8, 433], -"838": [8, 437], -"839": [8, 441], -"840": [8, 445], -"841": [8, 449], -"842": [8, 453], -"843": [8, 457], -"844": [8, 461], -"845": [8, 465], -"846": [8, 469], -"847": [8, 473], -"848": [8, 477], -"849": [8, 481], -"850": [8, 485], -"851": [8, 489], -"852": [8, 493], -"853": [8, 497], -"854": [8, 501], -"855": [8, 505], -"856": [8, 509], -"857": [9, 1], -"858": [9, 5], -"859": [9, 9], -"860": [9, 13], -"861": [9, 17], -"862": [9, 21], -"863": [9, 25], -"864": [9, 29], -"865": [9, 33], -"866": [9, 37], -"867": [9, 41], -"868": [9, 45], -"869": [9, 49], -"870": [9, 53], -"871": [9, 57], -"872": [9, 61], -"873": [9, 65], -"874": [9, 69], -"875": [9, 73], -"876": [9, 77], -"877": [9, 81], -"878": [9, 85], -"879": [9, 89], -"880": [9, 93], -"881": [9, 97], -"882": [9, 101], -"883": [9, 105], -"884": [9, 109], -"885": [9, 113], -"886": [9, 117], -"887": [9, 121], -"888": [9, 125], -"889": [9, 129], -"890": [9, 133], -"891": [9, 137], -"892": [9, 141], -"893": [9, 145], -"894": [9, 149], -"895": [9, 153], -"896": [9, 157], -"897": [9, 161], -"898": [9, 165], -"899": [9, 169], -"900": [9, 173], -"901": [10, 1], -"902": [10, 5], -"903": [10, 9], -"904": [10, 13], -"905": [10, 17], -"906": [10, 21], -"907": [10, 25], -"908": [10, 29], -"909": [10, 33], -"910": [10, 37], -"911": [10, 41], -"912": [10, 45], -"913": [10, 49], -"914": [10, 53], -"915": [10, 57], -"916": [10, 61], -"917": [10, 65], -"918": [10, 69], -"919": [10, 73], -"920": [10, 77], -"921": [10, 81], -"922": [10, 85], -"923": [10, 89], -"924": [10, 93], -"925": [10, 97], -"926": [10, 101], -"927": [10, 105], -"928": [10, 109], -"929": [10, 113], -"930": [10, 117], -"931": [10, 121], -"932": [10, 125], -"933": [10, 129], -"934": [10, 133], -"935": [10, 137], -"936": [10, 141], -"937": [10, 145], -"938": [10, 149], -"939": [10, 153], -"940": [10, 157], -"941": [10, 161], -"942": [10, 165], -"943": [10, 169], -"944": [10, 173], -"945": [10, 177], -"946": [10, 181], -"947": [10, 185], -"948": [10, 189], -"949": [10, 193], -"950": [10, 197], -"951": [10, 201], -"952": [10, 205], -"953": [10, 209], -"954": [10, 213], -"955": [10, 217], -"956": [10, 221], -"957": [10, 225], -"958": [10, 229], -"959": [10, 233], -"960": [10, 237], -"961": [10, 241], -"962": [10, 245], -"963": [10, 249], -"964": [10, 253], -"965": [10, 257], -"966": [10, 261], -"967": [10, 265], -"968": [10, 269], -"969": [10, 273], -"970": [10, 277], -"971": [10, 281], -"972": [10, 285], -"973": [10, 289], -"974": [10, 293], -"975": [10, 297], -"976": [10, 301], -"977": [10, 305], -"978": [10, 309], -"979": [10, 313], -"980": [10, 317], -"981": [10, 321], -"982": [10, 325], -"983": [10, 329], -"984": [10, 333], -"985": [10, 337], -"986": [10, 341], -"987": [10, 345], -"988": [10, 349], -"989": [10, 353], -"990": [10, 357], -"991": [10, 361], -"992": [10, 365], -"993": [10, 369], -"994": [10, 373], -"995": [10, 377], -"996": [10, 381], -"997": [10, 385], -"998": [10, 389], -"999": [10, 393], -"1000": [10, 397], -"1001": [10, 401], -"1002": [10, 405], -"1003": [10, 409], -"1004": [10, 413], -"1005": [10, 417], -"1006": [10, 421], -"1007": [10, 425], -"1008": [10, 429], -"1009": [10, 433], -"1010": [10, 437], -"1011": [10, 441], -"1012": [10, 445], -"1013": [10, 449], -"1014": [10, 453], -"1015": [10, 457], -"1016": [10, 461], -"1017": [10, 465], -"1018": [10, 469], -"1019": [10, 473], -"1020": [10, 477], -"1021": [10, 481], -"1022": [10, 485], -"1023": [10, 489], -"1024": [10, 493], -"1025": [10, 497], -"1026": [10, 501], -"1027": [10, 505], -"1028": [10, 509], -"1029": [11, 1], -"1030": [11, 5], -"1031": [11, 9], -"1032": [11, 13], -"1033": [11, 17], -"1034": [11, 21], -"1035": [11, 25], -"1036": [11, 29], -"1037": [11, 33], -"1038": [11, 37], -"1039": [11, 41], -"1040": [11, 45], -"1041": [11, 49], -"1042": [11, 53], -"1043": [11, 57], -"1044": [11, 61], -"1045": [11, 65], -"1046": [11, 69], -"1047": [11, 73], -"1048": [11, 77], -"1049": [11, 81], -"1050": [11, 85], -"1051": [11, 89], -"1052": [11, 93], -"1053": [11, 97], -"1054": [11, 101], -"1055": [11, 105], -"1056": [11, 109], -"1057": [11, 113], -"1058": [11, 117], -"1059": [11, 121], -"1060": [11, 125], -"1061": [11, 129], -"1062": [11, 133], -"1063": [11, 137], -"1064": [11, 141], -"1065": [11, 145], -"1066": [11, 149], -"1067": [11, 153], -"1068": [11, 157], -"1069": [11, 161], -"1070": [11, 165], -"1071": [11, 169], -"1072": [11, 173], -"1073": [11, 177], -"1074": [11, 181], -"1075": [11, 185], -"1076": [11, 189], -"1077": [11, 193], -"1078": [11, 197], -"1079": [11, 201], -"1080": [11, 205], -"1081": [11, 209], -"1082": [11, 213], -"1083": [11, 217], -"1084": [11, 221], -"1085": [11, 225], -"1086": [11, 229], -"1087": [11, 233], -"1088": [11, 237], -"1089": [11, 241], -"1090": [11, 245], -"1091": [11, 249], -"1092": [11, 253], -"1093": [11, 257], -"1094": [11, 261], -"1095": [11, 265], -"1096": [11, 269], -"1097": [11, 273], -"1098": [11, 277], -"1099": [11, 281], -"1100": [11, 285], -"1101": [11, 289], -"1102": [11, 293], -"1103": [11, 297], -"1104": [11, 301], -"1105": [11, 305], -"1106": [11, 309], -"1107": [11, 313], -"1108": [11, 317], -"1109": [11, 321], -"1110": [11, 325], -"1111": [11, 329], -"1112": [11, 333], -"1113": [11, 337], -"1114": [11, 341], -"1115": [11, 345], -"1116": [11, 349], -"1117": [11, 353], -"1118": [11, 357], -"1119": [11, 361], -"1120": [11, 365], -"1121": [11, 369], -"1122": [11, 373], -"1123": [11, 377], -"1124": [11, 381], -"1125": [11, 385], -"1126": [11, 389], -"1127": [11, 393], -"1128": [11, 397], -"1129": [11, 401], -"1130": [11, 405], -"1131": [11, 409], -"1132": [11, 413], -"1133": [11, 417], -"1134": [11, 421], -"1135": [11, 425], -"1136": [11, 429], -"1137": [11, 433], -"1138": [11, 437], -"1139": [11, 441], -"1140": [11, 445], -"1141": [11, 449], -"1142": [11, 453], -"1143": [11, 457], -"1144": [11, 461], -"1145": [11, 465], -"1146": [11, 469], -"1147": [11, 473], -"1148": [11, 477], -"1149": [11, 481], -"1150": [11, 485], -"1151": [11, 489], -"1152": [11, 493], -"1153": [11, 497], -"1154": [11, 501], -"1155": [11, 505], -"1156": [11, 509], -"1157": [12, 1], -"1158": [12, 5], -"1159": [12, 9], -"1160": [12, 13], -"1161": [12, 17], -"1162": [12, 21], -"1163": [12, 25], -"1164": [12, 29], -"1165": [12, 33], -"1166": [12, 37], -"1167": [12, 41], -"1168": [12, 45], -"1169": [12, 49], -"1170": [12, 53], -"1171": [12, 57], -"1172": [12, 61], -"1173": [12, 65], -"1174": [12, 69], -"1175": [12, 73], -"1176": [12, 77], -"1177": [12, 81], -"1178": [12, 85], -"1179": [12, 89], -"1180": [12, 93], -"1181": [12, 97], -"1182": [12, 101], -"1183": [12, 105], -"1184": [12, 109], -"1185": [12, 113], -"1186": [12, 117], -"1187": [12, 121], -"1188": [12, 125], -"1189": [12, 129], -"1190": [12, 133], -"1191": [12, 137], -"1192": [12, 141], -"1193": [12, 145], -"1194": [12, 149], -"1195": [12, 153], -"1196": [12, 157], -"1197": [12, 161], -"1198": [12, 165], -"1199": [12, 169], -"1200": [12, 173] -} diff --git a/go_tri.py b/go_tri.py index fa1c45d..c4e2827 100644 --- a/go_tri.py +++ b/go_tri.py @@ -7,8 +7,9 @@ import threading import cherrypy -import grid -from model import SimulatorModel, sACN, demo_triangle_mapping +from grid import Grid +from model import sACN +import netifaces import osc_serve import shows import util @@ -36,14 +37,15 @@ def speed_interpolation(val): else: return hi_interp(val) + low_interp = util.make_interpolater(0.0, 0.5, 2.0, 1.0) -hi_interp = util.make_interpolater(0.5, 1.0, 1.0, 0.5) +hi_interp = util.make_interpolater(0.5, 1.0, 1.0, 0.5) class ShowRunner(threading.Thread): - def __init__(self, model, queue, max_showtime=240, fail_hard=True): + def __init__(self, grid, queue, max_showtime=240, fail_hard=True): super(ShowRunner, self).__init__(name="ShowRunner") - self.model = model + self.grid = grid self.queue = queue self.fail_hard = fail_hard @@ -133,8 +135,8 @@ def process_command(self, msg): print("ignoring unknown msg:", str(msg)) def clear(self): - """Clears contained model.""" - self.model.clear() + """Clears contained grid.""" + self.grid.clear() def next_show(self, name=None): show = None @@ -151,7 +153,7 @@ def next_show(self, name=None): self.clear() self.prev_show = self.show - self.show = show(self.model) + self.show = show(self.grid) print(f'next show: {name}') self.framegen = self.show.next_frame() self.show_params = hasattr(self.show, 'set_param') @@ -176,7 +178,7 @@ def run(self): self.check_queue() d = self.get_next_frame() - self.model.go() + self.grid.go() if d: real_d = d * self.speed_x time.sleep(real_d) @@ -206,9 +208,9 @@ def osc_listener(q, port=5700): class TriangleServer(object): - def __init__(self, tri_model, args): + def __init__(self, grid, args): self.args = args - self.tri_model = tri_model + self.grid = grid self.queue = queue.LifoQueue() @@ -230,7 +232,8 @@ def _create_services(self): logger.warning("Can't create OSC listener", exc_info=True) # Show runner - self.runner = ShowRunner(self.tri_model, self.queue, args.max_time, fail_hard=args.fail_hard) + self.runner = ShowRunner( + self.grid, self.queue, args.max_time, fail_hard=args.fail_hard) if args.shows: print("setting show:", args.shows[0]) self.runner.next_show(args.shows[0]) @@ -301,12 +304,16 @@ def go_web(self): parser = argparse.ArgumentParser(description='Triangle Light Control') + parser.add_argument('-r', '--rows', type=int, + default=11, help='Rows per panel') parser.add_argument('--max-time', type=float, default=float(60), help='Maximum number of seconds a show will run (default 60)') + parser.add_argument('--bind', help='Local address to use for sACN') parser.add_argument('--simulator', dest='simulator', action='store_true') - parser.add_argument('--list', action='store_true', help='List available shows') + parser.add_argument('--list', action='store_true', + help='List available shows') parser.add_argument('shows', metavar='show_name', type=str, nargs='*', help='name of show (or shows) to run') parser.add_argument('--fail-hard', type=bool, default=True, @@ -315,23 +322,41 @@ def go_web(self): args = parser.parse_args() if args.list: - logger.info("Available shows: %s", ', '.join([name for (name, cls) in shows.load_shows()])) + logger.info("Available shows: %s", ', '.join( + [name for (name, cls) in shows.load_shows()])) sys.exit(0) if args.simulator: - sim_host = "localhost" - sim_port = 4444 - logger.info(f'Using TriSimulator at {sim_host}:{sim_port}') + parser.error('The simulator is broken') + + # sim_host = "localhost" + # sim_port = 4444 + # logger.info(f'Using TriSimulator at {sim_host}:{sim_port}') - model = SimulatorModel(sim_host, port=sim_port, model_json='./data/pixel_map.json') - triangle_grid = grid.make_triangle(model, 11) # Our panels will only have 11 rows + # model = SimulatorModel(sim_host, port=sim_port, + # model_json='./data/pixel_map.json') else: - logger.info("Starting SACN") - model = sACN(model_json="./data/pixel_map.json", pixelmap=demo_triangle_mapping()) + bind = args.bind + if not bind: + gateways = netifaces.gateways()[netifaces.AF_INET] + + for _, interface, _ in gateways: + for a in netifaces.ifaddresses(interface).get(netifaces.AF_INET, []): + if a['addr'].startswith('192.168'): + logger.info(f"Auto-detected local IP: {a['addr']}") + bind = a.addr + break + if bind: + break + + if not bind: + parser.error('Failed to auto-detect local IP; use --bind') + + logger.info("Starting sACN") + model = sACN(bind, args.rows) - triangle_grid = grid.make_triangle(model, 2) + app = TriangleServer(Grid(model, args.rows), args) - app = TriangleServer(triangle_grid, args) try: app.start() # start related service threads app.go_web() # enter main blocking event loop diff --git a/grid/__init__.py b/grid/__init__.py index 1243a97..9562882 100644 --- a/grid/__init__.py +++ b/grid/__init__.py @@ -1 +1,6 @@ -from .grid import TriangleGrid, TriangleCell, make_triangle, row_length, triangular_number + +from .cell import Address, Cell, Direction, Position, Orientation +from .grid import Grid, Location, Pixel, Query, Selector +from .select import (every, left_edge, right_edge, bottom_edge, pointed, + pointed_up, pointed_down, edge_neighbors, vertex_neighbors, hexagon) +from .traversal import sweep, left_to_right, right_to_left diff --git a/grid/cell.py b/grid/cell.py new file mode 100644 index 0000000..b309036 --- /dev/null +++ b/grid/cell.py @@ -0,0 +1,243 @@ +from enum import IntEnum +from functools import lru_cache +from itertools import chain +from typing import List, Mapping, NamedTuple + + +class Orientation(IntEnum): + POINT_UP = 1 + POINT_DOWN = -1 + + def invert(self) -> "Orientation": + return Orientation.POINT_UP if self is Orientation.POINT_DOWN else Orientation.POINT_DOWN + + +class Direction(IntEnum): + LEFT_TO_RIGHT = 1 + NATURAL = 0 + RIGHT_TO_LEFT = -1 + + def natural_for(self, orientation: Orientation) -> bool: + """ + Returns true if the order of pixel addresses within the cell + matches the desired direction. + """ + + return self is Direction.NATURAL or self == orientation + + +def row_length(n: int) -> int: + """Returns count of elements in nth row of equilateral triangle.""" + return n * 2 - 1 + + +class Cell(NamedTuple): + """ + A Cell stores the properties of a "cell" (mini-triangle) within one of + the panels. + """ + + position: "Position" + orientation: Orientation + addresses: List["Address"] + + row_count = 11 + + def pixel_addresses(self, direction: Direction = Direction.LEFT_TO_RIGHT) -> List["Address"]: + return (self.addresses + if direction.natural_for(self.orientation) + else reversed(self.addresses)) + + @property + def coordinates(self) -> "Position": + return self.position + + @property + def row(self) -> int: + return self.position.row + + @property + def col(self) -> int: + return self.position.col + + @property + def id(self) -> int: + return self.position.id + + @property + def is_up(self) -> bool: + return self.orientation is Orientation.POINT_UP + + @property + def is_down(self) -> bool: + return self.orientation is Orientation.POINT_DOWN + + @property + def is_left_edge(self) -> bool: + """Returns True if cell is along the left edge of the greater triangle.""" + return self.col == 0 + + @property + def is_right_edge(self) -> bool: + """Returns True if cell is along the right edge of the greater triangle.""" + (row, column) = self.position + return column + 1 == row_length(row + 1) + + @property + def is_bottom_edge(self) -> bool: + """Returns True if cell is along the bottom edge of the greater triangle.""" + return self.row + 1 == self.row_count and self.is_up + + @property + def is_top_corner(self) -> bool: + """Returns True if cell is the top corner of the greater triangle.""" + return self.id == 0 + + @property + def is_right_corner(self) -> bool: + """Returns True if cell is the right corner of the greater triangle.""" + return self.is_bottom_edge and self.is_right_edge + + @property + def is_left_corner(self) -> bool: + """Returns True if cell is the left corner of the greater triangle.""" + return self.is_bottom_edge and self.is_left_edge + + +def triangular_number(n: int) -> int: + """Returns the number of elements in an equilateral triangle of n rows.""" + # Typically the triangle number is (n(n+1))/2 but our triangle has rows of 1, 3, 5... + return n ** 2 + + +class Position(NamedTuple): + row: int + col: int + + @classmethod + @lru_cache(maxsize=512) + def from_id(cls, id: int) -> "Position": + row_below = 1 + while triangular_number(row_below) <= id: + row_below += 1 + + row = row_below - 1 + col = id - triangular_number(row) + return cls(row, col) + + @property + def id(self) -> int: + return triangular_number(self.row) + self.col + + @property + def x(self) -> int: + return self.col + + @property + def y(self) -> int: + return self.row + + def adjust(self, row: int, col: int) -> "Position": + return type(self)(self.row + row, self.col + col) + + +class Address(NamedTuple): + """ + Address refers to a cell's DMX address within one of the + triangle panels. + """ + + universe: int + offset: int + + @property + def next(self) -> "Address": + next_offset = self.offset + 4 + if next_offset >= universe_size(self.universe): + return Address(self.universe + 1, 0) + + return Address(self.universe, next_offset) + + def skip(self, n: int) -> "Address": + addr = self + for _ in range(n): + addr = addr.next + + return addr + + def range(self, len: int) -> List["Address"]: + addrs = [self] + for _ in range(len - 1): + addrs.append(addrs[-1].next) + + return addrs + + +def universe_count(row_count: int, start: Address = Address(1, 4)) -> int: + return row_count # XXX(lyra): I don't think this is real + + +def universe_size(universe_id: int) -> int: + """ + Gives the number of connected channels in each DMX universe. + + Every third universe is shorter. (Also, not all channels are + visible; some correspond to the strip segments between rows.) + """ + + return 512 if universe_id % 3 != 0 else (44 * 4) + + +def generate(rows: int = 11, start: Address = Address(1, 4)) -> Mapping[Position, Cell]: + cells = {} + for row in range(rows - 1, -1, -1): + row_mapping = mouth(row, start) + cells.update(row_mapping) + + end_address = max(chain.from_iterable( + cell.addresses for cell in row_mapping.values())) + start = end_address.next + + return cells + + +def mouth(row: int, start: Address) -> Mapping[Position, Cell]: + up = up_teeth(row, start, row + 1) + last_up_address = up[max(up)].addresses[-1] + first_after_gap = last_up_address.range(11)[-1] + down = down_teeth(row, first_after_gap, row) + + return {**up, **down} + + +def up_teeth(row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, Cell]: + cells = {} + addr = start + + for i in range(length): + pos = Position(row, i * 2) + addrs = addr.range(pixels_per_cell) + cells[pos] = Cell(pos, Orientation.POINT_UP, addrs) + + addr = addrs[-1].next + + return cells + + +def down_teeth(row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, Cell]: + cells = {} + addr = start + + col = length * 2 - 1 + for _ in range(length): + pos = Position(row, col) + addrs = addr.range(pixels_per_cell) + cells[pos] = Cell(pos, Orientation.POINT_DOWN, addrs) + + col -= 2 + addr = addrs[-1].next + + return cells + + +CELLS = generate() diff --git a/grid/cells.py b/grid/cells.py deleted file mode 100644 index d506945..0000000 --- a/grid/cells.py +++ /dev/null @@ -1,100 +0,0 @@ -from itertools import chain -from typing import List, Mapping, NamedTuple - - -class Position(NamedTuple): - row: int - col: int - - @property - def x(self) -> int: - return self.col - - @property - def y(self) -> int: - return self.row - - -UNIVERSES = { - 1: 512, - 2: 512, - 3: 44 * 4, - 4: 512, - 5: 512, - 6: 44 * 4, - 7: 512, - 8: 512, - 9: 44 * 4, - 10: 512, - 11: 512, -} - - -class Address(NamedTuple): - universe: int - offset: int - - @property - def next(self) -> "Address": - next_offset = self.offset + 4 - if next_offset >= UNIVERSES[self.universe]: - return Address(self.universe + 1, 0) - - return Address(self.universe, next_offset) - - def range(self, len: int) -> List["Address"]: - addrs = [self] - for _ in range(len - 1): - addrs.append(addrs[-1].next) - - return addrs - - -def generate(rows: int = 11, start: Address = Address(1, 4)) -> Mapping[Position, List[Address]]: - cells = {} - for row in range(rows - 1, -1, -1): - row_mapping = mouth(row, start) - cells.update(row_mapping) - - end_address = max(chain.from_iterable(row_mapping.values())) - start = end_address.next - - return cells - - -def mouth(row: int, start: Address) -> Mapping[Position, List[Address]]: - up = up_teeth(row, start, row + 1) - last_up_address = up[max(up)][-1] - first_after_gap = last_up_address.range(11)[-1] - down = down_teeth(row, first_after_gap, row) - - return {**up, **down} - - -def up_teeth(row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, List[Address]]: - cells = {} - addr = start - - for i in range(length): - addrs = addr.range(pixels_per_cell) - cells[Position(row, i * 2)] = addrs - addr = addrs[-1].next - - return cells - - -def down_teeth(row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, List[Address]]: - cells = {} - addr = start - - col = length * 2 - 1 - for i in range(length): - addrs = addr.range(pixels_per_cell) - cells[Position(row, col)] = addrs - col -= 2 - addr = addrs[-1].next - - return cells - - -CELLS = generate() diff --git a/grid/grid.py b/grid/grid.py index b75a1c7..2d185e6 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -1,330 +1,96 @@ -from functools import lru_cache import logging -from typing import Iterator, List, NamedTuple, Tuple, Type +from typing import Callable, Iterator, Iterable, List, Mapping, NamedTuple, Union, Type from color import Color, RGB -from model import ModelBase, SetColorFunc +from model import ModelBase +from .cell import generate, Address, Cell, Direction, Position, Orientation logger = logging.getLogger('pyramidtriangles') +Location = Union[Position, int] -def triangular_number(n: int) -> int: - """Returns the number of elements in an equilateral triangle of n rows.""" - # Typically the triangle number is (n(n+1))/2 but our triangle has rows of 1, 3, 5... - return n ** 2 +Query = Callable[['Grid'], Iterable[Cell]] +Selector = Union[Location, + Cell, + Iterable[Cell], + Query] -def row_length(n: int) -> int: - """Returns count of elements in nth row of equilateral triangle.""" - return n * 2 - 1 +class Pixel(NamedTuple): + cell: Cell + address: Address + model: Type[ModelBase] + def set(self, color: Color): + self.model.set(self.address, color) -class TriangleGrid(object): - def __init__(self, model: Type[ModelBase], row_count: int): - if row_count < 1: - raise ValueError(f'n_rows={row_count} must be positive') - - self._model = model - self._row_count = row_count - - self._cells = [] - for cell_id in range(0, triangular_number(self.row_count)): - self._cells.append(TriangleCell(id=cell_id, num_rows=self.row_count)) - - @staticmethod - def coordinates_to_id(row: int, column: int) -> int: - """Converts zero-indexed (row, column) coordinates to zero-indexed ID.""" - if row < 0 or column < 0: - raise ValueError(f'(row={row}, column={column}) must be non-negative') - return triangular_number(row) + column - - @staticmethod - @lru_cache(maxsize=512) - def id_to_coordinates(cell_id: int) -> Tuple[int, int]: - """Converts zero-indexed ID to zero-indexed (row, column) coordinates.""" - if cell_id < 0: - raise ValueError(f'cell_id={cell_id} must be non-negative') - - current_row = 1 - while triangular_number(current_row) <= cell_id: - current_row += 1 - return current_row - 1, cell_id - triangular_number(current_row - 1) - - def __repr__(self): - """String representation of object.""" - return f'{__class__.__name__} (n_rows={self.row_count}, model={self._model})' - - @property - def row_count(self) -> int: - """Returns the number of rows in the triangle.""" - return self._row_count - - @property - def size(self) -> int: - """Returns the number of cells in the triangle.""" - return len(self._cells) - - @property - def cells(self) -> List['TriangleCell']: - return self._cells - - def go(self): - self._model.go() - - def clear(self): - """Clears grid by setting all cells to 0.""" - self.set_all_cells(RGB(0, 0, 0)) - self.go() + def __call__(self, color: Color): + self.set(color) - def get_cell_by_coordinates(self, row: int, column: int) -> 'TriangleCell': - """Returns Cell for (row, column) coordinates, or None if coordinates are out of bounds.""" - if not 0 <= row < self.row_count or not 0 <= column < row_length(row + 1): - logger.debug(f'(row={row}, column={column}) out of bounds for triangle with {self.row_count} rows') - return None - return self._cells[self.coordinates_to_id(row, column)] +class Grid(Mapping[Location, Cell]): + row_count: int + _model: Type[ModelBase] + _cells: List[Cell] - def get_cell_by_id(self, cell_id: int) -> 'TriangleCell': - """Returns Cell for cell ID, or None if cell ID out of bounds.""" - if not 0 <= cell_id < len(self._cells): - return None - - return self._cells[cell_id] - - def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: - """ - Returns an iterator to set each Pixel's color within the given cell ID. - - Example: - for pixel in grid.set_pixels_by_cell_id(0): - pixel(color) - time.sleep(0.1) - """ - if not 0 <= cell_id < len(self._cells): - return [] - - return self._model.set_pixels_by_cellid(cell_id) - - def set_cell_by_coordinates(self, row: int, column: int, color: Color): - """Sets cell pixels to color for given coordinates.""" - cell = self.get_cell_by_coordinates(row, column) - if cell is None: - logger.warning(f'cell coordinates (row={row}, column={column}) invalid') - return - - [pixel(color) for pixel in self._model.set_pixels_by_cellid(cell.id)] - - def set_cell_by_id(self, cell_id: int, color: Color): - """Sets cell pixels to color for given ID.""" - if not 0 <= cell_id < len(self._cells): - logger.warning(f'cell ID {cell_id} invalid') - return - [pixel(color) for pixel in self._model.set_pixels_by_cellid(cell_id)] - - - def set_cell(self, cell, color: Color): - "Set cell pixels to a color" - if cell is not None: - if not 0 <= cell.id < len(self._cells): - logger.warning(f'cell ID {cell_id} invalid') - return - [pixel(color) for pixel in self._model.set_pixels_by_cellid(cell.id)] - - - def set_cells(self, cells: List['TriangleCell'], color: Color): - "set a list of cells to a color" - for cell in cells: - self.set_cell(cell, color) - - - def set_all_cells(self, color: Color): - """Sets all cell to color.""" - for cell in self.cells: - self.set_cell_by_id(cell.id, color) - - # Convenience methods for grabbing useful parts of the triangle grid. - @property - def left_side_cells(self) -> List['TriangleCell']: - return [cell for cell in self._cells if cell.is_left_edge] - - @property - def right_side_cells(self) -> List['TriangleCell']: - return [cell for cell in self._cells if cell.is_right_edge] - - @property - def bottom_side_cells(self) -> List['TriangleCell']: - return [cell for cell in self._cells if cell.is_bottom_edge] + def __init__(self, model: Type[ModelBase], row_count: int = 11): + if row_count < 1: + raise ValueError(f'Grid(row_count={row_count}) is invalid') - @property - def up_cells(self) -> List['TriangleCell']: - return [cell for cell in self._cells if cell.is_up] + self.row_count = row_count + self._model = model + self._cells = list(generate(rows=row_count).values()) @property - def down_cells(self) -> List['TriangleCell']: - return [cell for cell in self._cells if cell.is_down] + def cells(self) -> List[Cell]: + return list(self._cells) - # Triangle Grid Helper Functions - def edge_neighbors_by_coord(self, row: int, col: int) -> Tuple['TriangleCell', 'TriangleCell', 'TriangleCell']: - """ - Returns a tuple of (left, middle, right) cells that share an edge with the given (row, column) cell. - - Left neighbor is the edge directly to the left of the cell, regardless of up/down orientation. - Middle neighbor is either the top or bottom neighbor depending where the edge is. - Right neighbor is the cell immediately to the right. - """ - cell = self.get_cell_by_coordinates(row, col) - if cell is None: - return None, None, None - - left = self.get_cell_by_coordinates(row, col - 1) - right = self.get_cell_by_coordinates(row, col + 1) - - if cell.is_up: - middle = self.get_cell_by_coordinates(row + 1, col + 1) + def select(self, sel: Selector) -> Iterable[Cell]: + if isinstance(sel, (int, Position)): + cells = [self[sel]] + elif isinstance(sel, Cell): + cells = [sel] + elif isinstance(sel, Iterable): + cells = sel + elif callable(sel): + cells = sel(self) else: - middle = self.get_cell_by_coordinates(row - 1, col - 1) + raise TypeError(f'invalid Cell selector {sel}') - return left, middle, right + return cells - def edge_neighbors_by_cell(self, cell_id: int) -> Tuple['TriangleCell', 'TriangleCell', 'TriangleCell']: + def pixels(self, sel: Selector, direction: Direction = Direction.NATURAL) -> Iterator[Pixel]: """ - Returns a tuple of (left, middle, right) cells that share an edge with the given cell ID. - - Left neighbor is the cell directly to the left. - Middle neighbor is either the top or bottom neighbor depending where the edge is. - Right neighbor is the cell directly to the right. + Yield the settable pixels of one or more cells. """ - return self.edge_neighbors_by_coord(*self.id_to_coordinates(cell_id)) - def vertex_neighbors_by_coord(self, row: int, col: int) -> Tuple['TriangleCell', 'TriangleCell', 'TriangleCell']: - """ - Returns a tuple of (left, middle, right) cells that share a vertex with the given (row, column) cell. + for cell in self.select(sel): + for addr in cell.pixel_addresses(direction): + yield Pixel(cell, addr, self._model) - Left neighbor is the cell opposite the left vertex of the given cell. - Middle neighbor is the cell opposite the middle (up or down) vertex of the given cell. - Right neighbor is the cell opposite the right vertex of the given cell. - """ - cell = self.get_cell_by_coordinates(row, col) - if cell is None: - return None, None, None + def set(self, sel: Selector, color: Color): + for pixel in self.pixels(sel): + pixel.set(color) - if cell.is_up: - left = self.get_cell_by_coordinates(row + 1, col - 1) - middle = self.get_cell_by_coordinates(row - 1, col - 1) - right = self.get_cell_by_coordinates(row + 1, col + 3) - return left, middle, right - - left = self.get_cell_by_coordinates(row - 1, col - 2) - middle = self.get_cell_by_coordinates(row + 1, col + 1) - right = self.get_cell_by_coordinates(row - 1, col + 1) - return left, middle, right - - def vertex_neighbors_by_cell(self, cell_id: int) -> Tuple['TriangleCell', 'TriangleCell', 'TriangleCell']: + def go(self): """ - Returns a tuple of (left, middle, right) cells that share a vertex with the given cell ID. - - Left neighbor is the cell opposite the left vertex of the given cell. - Middle neighbor is the cell opposite the middle (up or down) vertex of the given cell. - Right neighbor is the cell opposite the right vertex of the given cell. + Flush the underlying model (render its current state). """ - return self.vertex_neighbors_by_coord(*self.id_to_coordinates(cell_id)) - - - def hexagon_from_btm_cell_by_coords(self, row: int, col: int) -> Tuple['TriangleCell','TriangleCell','TriangleCell','TriangleCell','TriangleCell','TriangleCell']: - "Given the coordinates of an up facing cell, return the surrounding cells which will make " - "A hexagon with the specified cell as the base" - "the tuple will begin with the btm upward facing triangle at the base of the hex" - "the next cell in the tuple will be the next triangle in the hexagon moving counterclockwise" - "None will be entered for cells off the grid" - - btm_cell = self.get_cell_by_coordinates(row, col) - a,b,c,d,e,f = btm_cell, None, None, None, None, None - if a.is_left_edge: - b = self.edge_neighbors_by_cell(a.id)[2] - if b is not None: - c = self.edge_neighbors_by_cell(b.id)[1] - if c is not None: - d = self.edge_neighbors_by_cell(c.id)[0] - if d is not None: - e = self.edge_neighbors_by_cell(d.id)[0] - if e is not None: - f = self.edge_neighbors_by_cell(e.id)[1] - elif a.is_right_edge: - f = self.edge_neighbors_by_cell(a.id)[0] - if f is not None: - e = self.edge_neighbors_by_cell(f.id)[1] - if e is not None: - d = self.edge_neighbors_by_cell(e.id)[2] - if d is not None: - c = self.edge_neighbors_by_cell(d.id)[2] - if c is not None: - b = self.edge_neighbors_by_cell(c.id)[1] - else: - b = self.edge_neighbors_by_cell(a.id)[2] - c = self.edge_neighbors_by_cell(b.id)[1] - d = self.edge_neighbors_by_cell(c.id)[0] - e = self.edge_neighbors_by_cell(d.id)[0] - f = self.edge_neighbors_by_cell(e.id)[1] - - return (a,b,c,d,e,f) - - - -class TriangleCell(NamedTuple): - """A Cell contains multiple LED pixels.""" - id: int - num_rows: int - - @property - def coordinates(self) -> Tuple[int, int]: - """Returns zero-indexed (row, column) containing the cell.""" - return TriangleGrid.id_to_coordinates(self.id) - - @property - def is_left_edge(self) -> bool: - """Returns True if cell is along the left edge of the greater triangle.""" - (row, column) = TriangleGrid.id_to_coordinates(self.id) - return column == 0 - - @property - def is_right_edge(self) -> bool: - """Returns True if cell is along the right edge of the greater triangle.""" - (row, column) = self.coordinates - return column + 1 == row_length(row + 1) - - @property - def is_bottom_edge(self) -> bool: - """Returns True if cell is along the bottom edge of the greater triangle.""" - (row, column) = self.coordinates - return row + 1 == self.num_rows and self.is_up - - @property - def is_top_corner(self) -> bool: - """Returns True if cell is the top corner of the greater triangle.""" - return self.id == 0 - - @property - def is_right_corner(self) -> bool: - """Returns True if cell is the right corner of the greater triangle.""" - return self.is_bottom_edge and self.is_right_edge + self._model.go() - @property - def is_left_corner(self) -> bool: - """Returns True if cell is the left corner of the greater triangle.""" - return self.is_bottom_edge and self.is_left_edge + def clear(self, color: Color = RGB(0, 0, 0)): + self.set(self.cells, color) + self.go() - @property - def is_up(self) -> bool: - """Returns True if triangle shaped cell orients with a corner upward.""" - (row, column) = TriangleGrid.id_to_coordinates(self.id) - return column % 2 == 0 + def __getitem__(self, loc: Location) -> Cell: + cell_id = loc if isinstance(loc, int) else loc.id + return self._cells[cell_id] - @property - def is_down(self) -> bool: - """Returns True if triangle shaped cell orients with a corner downward.""" - return not self.is_up + def __iter__(self): + return (cell.position for cell in self._cells) + def __len__(self) -> int: + return len(self._cells) -def make_triangle(model: Type[ModelBase], row_count: int) -> TriangleGrid: - """Helper function to build a Triangle with row_count rows.""" - return TriangleGrid(model, row_count) + def __repr__(self): + return f'<{type(self).__name__} rows={self.row_count} {self._model}>' diff --git a/grid/select.py b/grid/select.py new file mode 100644 index 0000000..8361ed5 --- /dev/null +++ b/grid/select.py @@ -0,0 +1,140 @@ + +from typing import Iterable, List, NamedTuple, Sequence, Tuple + +from .cell import Cell, Orientation, row_length +from .grid import Grid, Query, Location + + +def query(grid: Grid, q: Query) -> Iterable[Cell]: + return q(grid) + + +def every(grid: Grid) -> List[Cell]: + return grid.cells + + +def left_edge(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_left_edge] + + +def right_edge(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_right_edge] + + +def bottom_edge(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_bottom_edge] + + +def pointed(orientation: Orientation) -> Query: + def query(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.orientation is orientation] + + +def pointed_up(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_up] + + +def pointed_down(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_down] + + +class Neighbors(NamedTuple): + left: Cell + middle: Cell + right: Cell + + +def edge_neighbors(loc: Location) -> Query: + """ + Queries a tuple of (left, middle, right) cells that share an edge with the given (row, column) cell. + + Left neighbor is the edge directly to the left of the cell, regardless of up/down orientation. + Middle neighbor is either the top or bottom neighbor depending where the edge is. + Right neighbor is the cell immediately to the right. + """ + + def query(grid: Grid) -> Neighbors: + cell = grid.get(loc) + if cell is None: + return Neighbors(None, None, None) + + pos = cell.position + + left = grid.get(pos.adjust(0, -1)) + middle = (grid.get(pos.adjust(1, 1)) + if cell.is_up + else grid.get(pos.adjust(-1, -1))) + right = grid.get(pos.adjust(0, 1)) + + return Neighbors(left, middle, right) + + return query + + +def vertex_neighbors(loc: Location) -> Query: + """ + Queries a tuple of (left, middle, right) cells that share a vertex with the given (row, column) cell. + + Left neighbor is the cell opposite the left vertex of the given cell. + Middle neighbor is the cell opposite the middle (up or down) vertex of the given cell. + Right neighbor is the cell opposite the right vertex of the given cell. + """ + + def query(grid: Grid) -> Neighbors: + cell = grid.get(loc) + if cell is None: + return Neighbors(None, None, None) + + pos = cell.position + + if cell.is_up: + left = grid.get(pos.adjust(1, -1)) + middle = grid.get(pos.adjust(-1, -1)) + right = grid.get(pos.adjust(1, 3)) + else: + left = grid.get(pos.adjust(-1, -2)) + middle = grid.get(pos.adjust(1, 1)) + right = grid.get(pos.adjust(-1, 1)) + + return Neighbors(left, middle, right) + + return query + + +def hexagon(base_loc: Location) -> Query: + def hexagon_query(grid: Grid) -> Sequence[Cell]: + def neighbors(cell) -> Neighbors: + return Neighbors(query(grid, edge_neighbors(cell.id))) + + btm_cell = grid[base_loc] + a, b, c, d, e, f = btm_cell, None, None, None, None, None + if a.is_left_edge: + b = neighbors(a).right + if b is not None: + c = neighbors(b).middle + if c is not None: + d = neighbors(c).left + if d is not None: + e = neighbors(d).left + if e is not None: + f = neighbors(e).middle + elif a.is_right_edge: + f = neighbors(a).left + if f is not None: + e = neighbors(f).middle + if e is not None: + d = neighbors(e).right + if d is not None: + c = neighbors(d).right + if c is not None: + b = neighbors(c).middle + else: + b = neighbors(a).right + c = neighbors(b).middle + d = neighbors(c).left + e = neighbors(d).left + f = neighbors(e).middle + + return (a, b, c, d, e, f) + + return hexagon_query diff --git a/grid/traversal.py b/grid/traversal.py index 7ace895..b237997 100644 --- a/grid/traversal.py +++ b/grid/traversal.py @@ -1,41 +1,24 @@ -from typing import Iterable, Iterator, List, Tuple +import enum +from typing import Iterator, Sequence -from .grid import row_length +from .cell import Direction, Position, row_length -def left_to_right(row_count: int) -> Iterator[List[Tuple[int, int]]]: - """Generates a left-to-right vertical sequence of coordinates.""" +def sweep(direction: Direction, row_count: int) -> Iterator[Sequence[Position]]: + """ + Generates a left-to-right or right-to-left vertical sequence of coordinates. + """ + if direction is Direction.NATURAL: + raise ValueError('Direction.NATURAL is invalid with traversal.sweep()') if not row_count > 0: raise ValueError(f'traversal requires row_count({row_count}) > 0') - # Forward sequence: - # [[(row-1, 0)], - # [(row-2, 0), (row-1, 1)], - # [(row-3, 0), (row-2, 1), (row-1, 2)]], ... - for bottom_column in range(row_length(row_count)): - row_to_start = row_count - 1 - bottom_column - - coordinates = [] - for curr_column in range(bottom_column + 1): - curr_row = row_to_start + curr_column - if not 0 <= curr_row < row_count: - continue - if curr_column >= row_length(curr_row + 1): - continue - - coordinates.append((curr_row, curr_column)) - - yield coordinates - + row_lengths = range(row_length(row_count)) + if direction == Direction.RIGHT_TO_LEFT: + row_lengths = reversed(row_lengths) -def right_to_left(row_count: int) -> Iterator[List[Tuple[int, int]]]: - """Generates a right-to-left vertical sequence of coordinates.""" - - if not row_count > 0: - raise ValueError(f'traversal requires row_count({row_count}) > 0') - - for bottom_column in reversed(range(row_length(row_count))): + for bottom_column in row_lengths: row_to_start = row_count - 1 - bottom_column coordinates = [] @@ -46,42 +29,14 @@ def right_to_left(row_count: int) -> Iterator[List[Tuple[int, int]]]: if curr_column >= row_length(curr_row + 1): continue - coordinates.append((curr_row, curr_column)) + coordinates.append(Position(curr_row, curr_column)) yield coordinates -def concentric(row_count: int) -> Iterator[Iterable[Tuple[int, int]]]: - """Concentric traversal out to in.""" - - if not row_count > 0: - raise ValueError(f'traversal requires row_count({row_count}) > 0') - - for i in range(row_count // 2): - bottom_row = row_count - 1 - i - # Increases 0, 2, 4,... - left_column = i * 2 - - # Accumulate for bottom row - bottoms = [] - for col in range(left_column, row_length(bottom_row + 1) - left_column): - bottoms.append((bottom_row, col)) - - # Accumulate i-th column of each row - lefts = [] - # Accumulate (n-i)th column of each row - rights = [] - - for row in range(left_column, bottom_row): - # Decreases last, last - 2, last - 4,... - right_column = row_length(row + 1) - 1 - left_column - - if left_column <= right_column: - lefts.append((row, left_column)) - rights.append((row, right_column)) +def left_to_right(row_count: int) -> Iterator[Sequence[Position]]: + return sweep(Direction.LEFT_TO_RIGHT, row_count) - if left_column + 1 <= right_column: - lefts.append((row, left_column + 1)) - rights.append((row, right_column - 1)) - yield set().union(bottoms, lefts, rights) +def right_to_left(row_count: int) -> Iterator[Sequence[Position]]: + return sweep(Direction.RIGHT_TO_LEFT, row_count) diff --git a/model/__init__.py b/model/__init__.py index a9adbdd..e8b5fec 100644 --- a/model/__init__.py +++ b/model/__init__.py @@ -1,7 +1,6 @@ # These imports include submodules under the `model` namespace (e.g. model.SimulatorModel is available). -from .mapping import demo_triangle_mapping -from .modelbase import ModelBase, SetColorFunc +from .base import ModelBase from .mirror import MirrorModel -from .ola_model import OLAModel +# from .ola_model import OLAModel from .sacn_model import sACN -from .simulator import SimulatorModel +# from .simulator import SimulatorModel diff --git a/model/base.py b/model/base.py new file mode 100644 index 0000000..176f456 --- /dev/null +++ b/model/base.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod +from color import Color +from grid.cell import Address, universe_count, universe_size +from typing import Callable, List, Mapping + +SetColorFunc = Callable[[Color], None] + + +class ModelBase(ABC): + """Abstract base class for simulators.""" + + @abstractmethod + def set(self, addr: Address, color: Color): + """ + Set one pixel to a particular color. + """ + + @abstractmethod + def go(self): + """Flush all buffered data out to devices.""" + + +def map_leds(row_count: int) -> Mapping[int, List[int]]: + count = universe_count(row_count) + + return {i + 1: [0] * universe_size(i + 1) for i in range(count)} diff --git a/model/mapping.py b/model/mapping.py deleted file mode 100644 index 887bd46..0000000 --- a/model/mapping.py +++ /dev/null @@ -1,130 +0,0 @@ -from typing import Dict, List -PixelMap = Dict[int, List[int]] - - -# This cell -> pixels mapping is created from an old version, but it should produce something. -def demo_triangle_mapping() -> PixelMap: - return { - #Wiring coming in from the bottom left, this is the order of cells following the pixels -0: [1059,1058,1057,1056,1055,1054,1053,1052], -1: [1042,1041,1040,1039,1038,1037,1036,1035], -2: [1034,1033,1032,1031,1030,1029,1028,1027], -3: [1026,1025,1024,1023,1022,1021,1020,1019], -4: [1009,1008,1007,1006,1005,1004,1003,1002], -5: [1001,1000,999,998,997,996,995,994], -6: [993,992,991,990,989,988,987,986], -7: [985,984,983,982,981,980,979,978], -8: [977,976,975,974,973,972,971,970], -9: [960,959,958,957,956,955,954,953], -10: [952,951,950,949,948,947,946,945], -11: [944,943,942,941,940,939,938,937], -12: [936,935,934,933,932,931,930,929], -13: [928,927,926,925,924,923,922,921], -14: [920,919,918,917,916,915,914,913], -15: [912,911,910,909,908,907,906,905], -16: [895,894,893,892,891,890,889,888], -17: [887,886,885,884,883,882,881,880], -18: [879,878,877,876,875,874,873,872], -19: [871,870,869,868,867,866,865,864], -20: [863,862,861,860,859,858,857,856], -21: [855,854,853,852,851,850,849,848], -22: [847,846,845,844,843,842,841,840], -23: [839,838,837,836,835,834,833,832], -24: [831,830,829,828,827,826,825,824], -25: [814,813,812,811,810,809,808,807], -26: [806,805,804,803,802,801,800,799], -27: [798,797,796,795,794,793,792,791], -28: [790,789,788,787,786,785,784,783], -29: [782,781,780,779,778,777,776,775], -30: [774,773,772,771,770,769,768,767], -31: [766,765,764,763,762,761,760,759], -32: [758,757,756,755,754,753,752,751], -33: [750,749,748,747,746,745,744,743], -34: [742,741,740,739,738,737,736,735], -35: [734,733,732,731,730,729,728,727], -36: [717,716,715,714,713,712,711,710], -37: [709,708,707,706,705,704,703,702], -38: [701,700,699,698,697,696,695,694], -39: [693,692,691,690,689,688,687,686], -40: [685,684,683,682,681,680,679,678], -41: [677,676,675,674,673,672,671,670], -42: [669,668,667,666,665,664,663,662], -43: [661,660,659,658,657,656,655,654], -44: [653,652,651,650,649,648,647,646], -45: [645,644,643,642,641,640,639,638], -46: [637,636,635,634,633,632,631,630], -47: [629,628,627,626,625,624,623,622], -48: [621,620,619,618,617,616,615,614], -49: [604,603,602,601,600,599,598,597], -50: [596,595,594,593,592,591,590,589], -51: [588,587,586,585,584,583,582,581], -52: [580,579,578,577,576,575,574,573], -53: [572,571,570,569,568,567,566,565], -54: [564,563,562,561,560,559,558,557], -55: [556,555,554,553,552,551,550,549], -56: [548,547,546,545,544,543,542,541], -57: [540,539,538,537,536,535,534,533], -58: [532,531,530,529,528,527,526,525], -59: [524,523,522,521,520,519,518,517], -60: [516,515,514,513,512,511,510,509], -61: [508,507,506,505,504,503,502,501], -62: [500,499,498,497,496,495,494,493], -63: [492,491,490,489,488,487,486,485], -64: [475,474,473,472,471,470,469,468], -65: [467,466,465,464,463,462,461,460], -66: [459,458,457,456,455,454,453,452], -67: [451,450,449,448,447,446,445,444], -68: [443,442,441,440,439,438,437,436], -69: [435,434,433,432,431,430,429,428], -70: [427,426,425,424,423,422,421,420], -71: [419,418,417,416,415,414,413,412], -72: [411,410,409,408,407,406,405,404], -73: [403,402,401,400,399,398,397,396], -74: [395,394,393,392,391,390,389,388], -75: [387,386,385,384,383,382,381,380], -76: [379,378,377,376,375,374,373,372], -77: [371,370,369,368,367,366,365,364], -78: [363,362,361,360,359,358,357,356], -79: [355,354,353,352,351,350,349,348], -80: [347,346,345,344,343,342,341,340], -81: [330,329,328,327,326,325,324,323], -82: [322,321,320,319,318,317,316,315], -83: [314,313,312,311,310,309,308,307], -84: [306,305,304,303,302,301,300,299], -85: [298,297,296,295,294,293,292,291], -86: [290,289,288,287,286,285,284,283], -87: [282,281,280,279,278,277,276,275], -88: [274,273,272,271,270,269,268,267], -89: [266,265,264,263,262,261,260,259], -90: [258,257,256,255,254,253,252,251], -91: [250,249,248,247,246,245,244,243], -92: [242,241,240,239,238,237,236,235], -93: [234,233,232,231,230,229,228,227], -94: [226,225,224,223,222,221,220,219], -95: [218,217,216,215,214,213,212,211], -96: [210,209,208,207,206,205,204,203], -97: [202,201,200,199,198,197,196,195], -98: [194,193,192,191,190,189,188,187], -99: [186,185,184,183,182,181,180,179], -100: [169,168,167,166,165,164,163,162], -101: [161,160,159,158,157,156,155,154], -102: [153,152,151,150,149,148,147,146], -103: [145,144,143,142,141,140,139,138], -104: [137,136,135,134,133,132,131,130], -105: [129,128,127,126,125,124,123,122], -106: [121,120,119,118,117,116,115,114], -107: [113,112,111,110,109,108,107,106], -108: [105,104,103,102,101,100,99,98], -109: [97,96,95,94,93,92,91,90], -110: [89,88,87,86,85,84,83,82], -111: [81,80,79,78,77,76,75,74], -112: [73,72,71,70,69,68,67,66], -113: [65,64,63,62,61,60,59,58], -114: [57,56,55,54,53,52,51,50], -115: [49,48,47,46,45,44,43,42], -116: [41,40,39,38,37,36,35,34], -117: [33,32,31,30,29,28,27,26], -118: [25,24,23,22,21,20,19,18], -119: [17,16,15,14,13,12,11,10], -120: [9,8,7,6,5,4,3,2] -} diff --git a/model/mirror.py b/model/mirror.py index b05e90d..99de832 100644 --- a/model/mirror.py +++ b/model/mirror.py @@ -1,33 +1,26 @@ -""" -Proof of concept that dealing with mirroring across multiple backend models is best done at this layer. -Completely agnostic as to what the cell ids look like, they are just passed through to the underlying model. -""" from itertools import zip_longest from typing import Iterator from color import Color -from .modelbase import ModelBase, SetColorFunc +from grid.cell import Address +from .base import ModelBase, SetColorFunc class MirrorModel(ModelBase): + """ + Proof of concept that dealing with mirroring across multiple backend models is best done at this layer. + """ + def __init__(self, *models): - self.models = [] - if models: - for m in models: - self.add_model(m) + self.models = list(models) def add_model(self, model): self.models.append(model) - def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: - generators = zip_longest([model.set_pixels_by_cellid(cell_id) for model in self.models]) - - def set_color(color: Color): - pixels = next(generators) - for pixel in pixels: - pixel.set_color(color) - yield set_color + def set(self, addr: Address, color: Color): + for m in self.models: + m.set(addr, color) def go(self): for m in self.models: diff --git a/model/modelbase.py b/model/modelbase.py deleted file mode 100644 index bb65627..0000000 --- a/model/modelbase.py +++ /dev/null @@ -1,24 +0,0 @@ -from abc import ABC, abstractmethod -from color import Color -from typing import Callable, Iterator - -SetColorFunc = Callable[[Color], None] - - -class ModelBase(ABC): - """Abstract base class for simulators.""" - - @abstractmethod - def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: - """ - Returns an iterator to set each Pixel's color within the given cell ID. - - Example: - for pixel in grid.set_pixels_by_cell_id(0): - pixel(color) - time.sleep(0.1) - """ - - @abstractmethod - def go(self): - """Flush all buffered data out to devices.""" diff --git a/model/ola_model.py b/model/ola_model.py index 32879f5..2bbaca8 100644 --- a/model/ola_model.py +++ b/model/ola_model.py @@ -11,12 +11,13 @@ from typing import Iterator import ola +from .base import ModelBase, SetColorFunc from .mapping import PixelMap -from .modelbase import ModelBase, SetColorFunc logger = logging.getLogger("pyramidtriangles") +# XXX(lyra): this is no longer a valid ModelBase class OLAModel(ModelBase): def __init__(self, max_dmx, model_json: str, pixelmap: PixelMap): # XXX any way to check if this is a valid connection? diff --git a/model/sacn_model.py b/model/sacn_model.py index ca05991..9f0d6b4 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -4,67 +4,48 @@ Pixels are representations of the addressable unit in your object. Cells can have multiple pixels in this model only have one LED each. """ -import json import logging -from typing import Iterator +from typing import Iterator, List, Mapping import sacn -from .mapping import PixelMap -from .modelbase import ModelBase, SetColorFunc +from color import Color +from grid.cell import Address, universe_count, universe_size +from .base import ModelBase, map_leds logger = logging.getLogger("pyramidtriangles") class sACN(ModelBase): - def __init__(self, model_json: str, pixelmap: PixelMap): - # XXX any way to check if this is a valid connection? - - # Must supply an IP address to bind to that is in the same subnet as the devices routing the universes. Might - # have to assign a second IP to the eth adapter to get this to work (this is what I had to do on my mac) - self.sender = sacn.sACNsender(bind_address="192.168.1.113", universeDiscovery=False) + def __init__(self, bind_address: str, row_count: int): + self.sender = sacn.sACNsender(bind_address, universeDiscovery=False) self.sender.start() - self.PIXEL_MAP = None + # dictionary which will hold an array of 512 int's for each universe, universes are keys to the arrays. - self.leds = {} - self._map_leds(model_json) - self._pixelmap = pixelmap -# from IPython import embed; embed() - # Keys for LEDs are integers representing universes, each universe has an array of possible DMX channels - # Pixels are an LED represented by 4 DMX addresses + self.leds = map_leds(row_count) + for ux in sorted(self.leds): + print(f'{ux}: {len(self.leds[ux])}') + print(f'Universes: {len(self.leds)}') + for universe_index in self.leds: + self.sender.activate_output(universe_index) + self.sender[universe_index].multicast = True def __del__(self): self.sender.stop() # If the object is destructing, close the sender connection - def _map_leds(self, f): - # Loads a json file with mapping info describing your LEDs. - # The json file is formatted as a dictionary of numbers (as strings sadly, b/c json is weird - # each key in the dict is a fixtureUID. - # each array that fixtureUID returns is of the format [universeUID, DMXstart#] - # initializing just 4 universes!!! Need to make this more configurable. - with open(f, 'r') as json_file: - self.PIXEL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) - - for i in self.PIXEL_MAP: - universe = int(self.PIXEL_MAP[i][0]) - if universe not in self.leds.keys(): - self.sender.activate_output(universe) - self.sender[universe].multicast = True - self.leds[universe] = [0] * 512 - - def set_pixels_by_cellid(self, cell_id) -> Iterator[SetColorFunc]: - for pixel in self._pixelmap[cell_id]: - if pixel not in self.PIXEL_MAP: - logger.warning(f'{pixel} not in sACN pixel ID map') - - ux = self.PIXEL_MAP[pixel][0] - ix = self.PIXEL_MAP[pixel][1] -1 # dmx is 1-based, python lists are 0-based - - def set_color(color): - self.leds[ux][ix] = color.r - self.leds[ux][ix + 1] = color.g - self.leds[ux][ix + 2] = color.b - self.leds[ux][ix + 3] = color.w - yield set_color + def set(self, addr: Address, color: Color): + try: + channels = self.leds[addr.universe] + except KeyError: + raise IndexError( + f'attempt to set channel in undefined universe {addr.universe}') + + # our the Color tuples have their channels in the same order as sACN + for i, c in enumerate(color.rgbw): + try: + channels[addr.offset + i] = c + except IndexError: + raise IndexError( + f'internal error in sACN model; failed to assign to universe {addr.universe}, address {addr.offset}') def go(self): for ux in self.leds: diff --git a/model/simulator.py b/model/simulator.py index fe1a096..f7afdc5 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -9,12 +9,14 @@ from typing import Iterator from color import Color -from .modelbase import ModelBase, SetColorFunc +from .base import ModelBase, SetColorFunc SIM_DEFAULT = (188, 210, 229) # BCD2E5, "off" color for simulator logger = logging.getLogger("pyramidtriangles") +# XXX(lyra): not yet updated! the simulator doesn't (yet) work with individual +# LEDs; all cells are uniform. class SimulatorModel(ModelBase): def __init__(self, hostname, port=4444, model_json=None): self.hostname = hostname @@ -37,7 +39,8 @@ def _map_leds(self, f): # The json file is formatted as a dictionary of numbers each key in the dict is a fixtureUID. # Each array that fixtureUID returns is of the format [universeUID, DMXstart#]. with open(f, 'r') as json_file: - self.CELL_MAP = json.load(json_file, object_hook=lambda d: {int(k): v for (k, v) in d.items()}) + self.CELL_MAP = json.load(json_file, object_hook=lambda d: { + int(k): v for (k, v) in d.items()}) # Model basics def set_pixels_by_cellid(self, cell_id: int) -> Iterator[SetColorFunc]: diff --git a/poetry.lock b/poetry.lock index 3fe4530..39dc9e1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -185,6 +185,14 @@ optional = false python-versions = ">=3.4" version = "7.2.0" +[[package]] +category = "main" +description = "Portable network interface information." +name = "netifaces" +optional = false +python-versions = "*" +version = "0.10.9" + [[package]] category = "main" description = "Python package for creating and manipulating graphs and networks" @@ -457,7 +465,7 @@ python-versions = ">=2.7" version = "0.5.2" [metadata] -content-hash = "cb1f76cd122797fe3bc03995c73dbe83f23fcabba37a533d0d15b8e685268920" +content-hash = "2fd2a0a06749304d2ee2d6f808d1d2c33573b5059aab4896e635faa500bc38aa" python-versions = "^3.7" [metadata.hashes] @@ -479,6 +487,7 @@ jedi = ["53c850f1a7d3cfcd306cc513e2450a54bdf5cacd7604b74e42dd1f0758eaaf36", "e07 jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] +netifaces = ["078986caf4d6a602a4257d3686afe4544ea74362b8928e9f4389b5cd262bc215", "0c4304c6d5b33fbd9b20fdc369f3a2fef1a8bbacfb6fd05b9708db01333e9e7b", "2dee9ffdd16292878336a58d04a20f0ffe95555465fee7c9bd23b3490ef2abf3", "3095218b66d359092b82f07c5422293c2f6559cf8d36b96b379cc4cdc26eeffa", "30ed89ab8aff715caf9a9d827aa69cd02ad9f6b1896fd3fb4beb998466ed9a3c", "4921ed406386246b84465950d15a4f63480c1458b0979c272364054b29d73084", "563a1a366ee0fb3d96caab79b7ac7abd2c0a0577b157cc5a40301373a0501f89", "5b3167f923f67924b356c1338eb9ba275b2ba8d64c7c2c47cf5b5db49d574994", "6d84e50ec28e5d766c9911dce945412dc5b1ce760757c224c71e1a9759fa80c2", "755050799b5d5aedb1396046f270abfc4befca9ccba3074f3dbbb3cb34f13aae", "75d3a4ec5035db7478520ac547f7c176e9fd438269e795819b67223c486e5cbe", "7a25a8e28281504f0e23e181d7a9ed699c72f061ca6bdfcd96c423c2a89e75fc", "7cc6fd1eca65be588f001005446a47981cbe0b2909f5be8feafef3bf351a4e24", "86b8a140e891bb23c8b9cb1804f1475eb13eea3dbbebef01fcbbf10fbafbee42", "ad10acab2ef691eb29a1cc52c3be5ad1423700e993cc035066049fa72999d0dc", "b2ff3a0a4f991d2da5376efd3365064a43909877e9fabfa801df970771161d29", "b47e8f9ff6846756be3dc3fb242ca8e86752cd35a08e06d54ffc2e2a2aca70ea", "da298241d87bcf468aa0f0705ba14572ad296f24c4fda5055d6988701d6fd8e1", "db881478f1170c6dd524175ba1c83b99d3a6f992a35eca756de0ddc4690a1940", "f0427755c68571df37dc58835e53a4307884a48dec76f3c01e33eb0d4a3a81d7", "f8885cc48c8c7ad51f36c175e462840f163cb4687eeb6c6d7dfaf7197308e36b", "f911b7f0083d445c8d24cfa5b42ad4996e33250400492080f5018a28c026db2b"] networkx = ["8311ddef63cf5c5c5e7c1d0212dd141d9a1fe3f474915281b73597ed5f1d4e3d"] numpy = ["0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", "141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", "14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", "27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", "2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", "3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", "52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", "6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", "7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", "7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", "94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", "a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", "ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", "b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", "b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", "cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", "d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", "dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", "dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", "e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", "ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", "f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", "f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7"] ola = ["60d263f99e95da94bbc9baa5737625070c016def4aad52cbaeced9ed9913eab2", "fc96b689000812cf7a2af44cf6c7b4cf1b37944c1ee1860d97eba06aa0cbcf66"] diff --git a/pyproject.toml b/pyproject.toml index 192777a..0a30dd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ sacn = "^1.3" ola = "^0.10.7" jinja2 = "^2.10" spectra = "^0.0.11" +netifaces = "^0.10.9" [tool.poetry.dev-dependencies] pytest = "^3.0" diff --git a/shows/__init__.py b/shows/__init__.py index bff483c..bea2c2c 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -8,4 +8,4 @@ from .cycle_hsv import CycleHSV from .strobe import Strobe from .marching_hexes import MarchingHexes -from .warp import Warp +# from .warp import Warp # XXX: missing traversal.concentric() diff --git a/shows/cycle_hsv.py b/shows/cycle_hsv.py index aab4956..0e79bcf 100644 --- a/shows/cycle_hsv.py +++ b/shows/cycle_hsv.py @@ -1,76 +1,67 @@ import time from color import HSV as hsv -from grid import TriangleGrid +from grid import Grid, every from .showbase import ShowBase class CycleHSV(ShowBase): - def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.1): - self.tri_grid = tri_grid + def __init__(self, grid: Grid, frame_delay: float = 0.1): + self.grid = grid self.frame_delay = frame_delay - self.n_cells = len(self.tri_grid.cells) -# from IPython import embed; embed() + self.n_cells = len(self.grid) def next_frame(self): - - while True: - ca = hsv(0.0, 0.0, 0.0, True) while ca.v < 1.0: ca.v += 0.0008 - self.tri_grid.set_all_cells(ca) - self.tri_grid.go() + self.grid.set(every, ca) + self.grid.go() time.sleep(.01) print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) while ca.s < 1.0: ca.s += 0.0008 - self.tri_grid.set_all_cells(ca) - self.tri_grid.go() + self.grid.set(every, ca) + self.grid.go() time.sleep(.01) print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) while ca.h < 1.0: ca.h += 0.0008 - self.tri_grid.set_all_cells(ca) - self.tri_grid.go() + self.grid.set(every, ca) + self.grid.go() time.sleep(.01) print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) - - self.tri_grid.clear() + self.grid.clear() time.sleep(3) - while True: ca = hsv(0.0, 0.0, 0.0, False) while ca.v < 1.0: ca.v += 0.0008 - self.tri_grid.set_all_cells(ca) - self.tri_grid.go() + self.grid.set(every, ca) + self.grid.go() time.sleep(.01) print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) - while ca.s < 1.0: ca.s += 0.0008 - self.tri_grid.set_all_cells(ca) - self.tri_grid.go() + self.grid.set(every, ca) + self.grid.go() time.sleep(.01) print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) - while ca.h < 1.0: ca.h += 0.0008 - self.tri_grid.set_all_cells(ca) - self.tri_grid.go() + self.grid.set(every, ca) + self.grid.go() time.sleep(.01) print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) - - self.tri_grid.clear() + self.grid.clear() time.sleep(3) while True: @@ -78,21 +69,19 @@ def next_frame(self): while ca.s < 1.0: ca.s += 0.0008 - self.tri_grid.set_all_cells(ca) - self.tri_grid.go() + self.grid.set(every, ca) + self.grid.go() time.sleep(.01) print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) while ca.h < 1.0: ca.h += 0.0008 - self.tri_grid.set_all_cells(ca) - self.tri_grid.go() + self.grid.set(every, ca) + self.grid.go() time.sleep(.01) print('CA', ca.hsv, 'RGB', ca.rgb, 'RGBW', ca.rgbw) - - self.tri_grid.clear() + self.grid.clear() time.sleep(3) - yield self.frame_delay diff --git a/shows/left_to_right.py b/shows/left_to_right.py index a997e54..0b147b0 100644 --- a/shows/left_to_right.py +++ b/shows/left_to_right.py @@ -1,45 +1,44 @@ from color import HSV from .showbase import ShowBase -from grid import TriangleGrid, traversal +from grid import Grid, left_to_right import time + class LeftToRight(ShowBase): - def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.2): - self.tri_grid = tri_grid + def __init__(self, grid: Grid, frame_delay: float = 0.2): + self.grid = grid self.frame_delay = frame_delay def next_frame(self): - row_count = self.tri_grid.row_count + row_count = self.grid.row_count - hsv = HSV(0.5,0.2,.75) -# from IPython import embed; embed() + hsv = HSV(0.5, 0.2, .75) +# from IPython import embed; embed() pix_arr = [] a_ctr = 0 - for points in traversal.left_to_right(row_count): - - for (row, column) in points: - cell = self.tri_grid.get_cell_by_coordinates(row, column) + for points in left_to_right(row_count): + + for pos in points: + cell = self.grid[pos] b_ctr = 0 - for pixel in list( self.tri_grid.set_pixels_by_cellid(cell.id) ): + for pixel in list(self.grid.pixels(cell.id)): if len(pix_arr) <= a_ctr+b_ctr: pix_arr.append([]) pix_arr[a_ctr+b_ctr].append(pixel) pixel(hsv) - self.tri_grid.go() + self.grid.go() b_ctr += 1 a_ctr += 4 - while True: for i in pix_arr: for ii in i: print(ii) ii(hsv) - self.tri_grid.go() + self.grid.go() time.sleep(0.2) - + hsv.h += .1 if hsv.h >= 1.0: hsv.h = 0.0 yield self.frame_delay - diff --git a/shows/left_to_right_and_back.py b/shows/left_to_right_and_back.py index fcbde5b..d483717 100644 --- a/shows/left_to_right_and_back.py +++ b/shows/left_to_right_and_back.py @@ -1,31 +1,27 @@ from color import RGB from .showbase import ShowBase -from grid import TriangleGrid, traversal +from grid import Grid, Direction, sweep class LeftToRightAndBack(ShowBase): - def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 1.0): - self.tri_grid = tri_grid + def __init__(self, grid: Grid, frame_delay: float = 1.0): + self.grid = grid self.frame_delay = frame_delay def next_frame(self): - row_count = self.tri_grid.row_count fwd = True while True: - if fwd: - sequence = traversal.left_to_right(row_count) - else: - sequence = traversal.right_to_left(row_count) + direction = Direction.LEFT_TO_RIGHT if fwd else Direction.RIGHT_TO_LEFT + sequence = sweep(direction, self.grid.row_count) for points in sequence: - self.tri_grid.clear() + self.grid.clear() - for (row, column) in points: - print(f'row={row} column={column}') - self.tri_grid.set_cell_by_coordinates(row, column, RGB(255, 255, 25)) + for pos in points: + self.grid.set(pos, RGB(255, 255, 25)) - self.tri_grid.go() + self.grid.go() yield self.frame_delay fwd = not fwd # Flips direction diff --git a/shows/marching_hexes.py b/shows/marching_hexes.py index 137a21e..192646c 100644 --- a/shows/marching_hexes.py +++ b/shows/marching_hexes.py @@ -1,30 +1,28 @@ from .showbase import ShowBase from color import HSV +from grid import hexagon, pointed_up import random as rnd import time + class MarchingHexes(ShowBase): - def __init__(self, tri_grid, frame_delay=0.1): - self.tri_grid = tri_grid + def __init__(self, grid, frame_delay=0.1): + self.grid = grid self.frame_delay = frame_delay - self.n_cells = len(self.tri_grid.cells) + self.n_cells = len(self.grid.cells) def next_frame(self): - hsv = HSV(1.0,1,1) + hsv = HSV(1.0, 1, 1) + while True: - self.tri_grid.clear() - for cell in self.tri_grid.cells: - if cell.is_up: - self.tri_grid.set_cells(self.tri_grid.hexagon_from_btm_cell_by_coords(cell.coordinates[0], cell.coordinates[1]), hsv) - self.tri_grid.go() - time.sleep(1) - - if hsv.h >= 1.0: - hsv.h = 0.0 - else: - hsv.h += 0.2 - - + self.grid.clear() + + for cell in self.grid.select(pointed_up): + self.grid.set(hexagon(cell.coordinates), hsv) + self.grid.go() + time.sleep(1) + + hsv.h = 0.0 if hsv.h >= 1.0 else hsv.h + 0.2 yield self.frame_delay diff --git a/shows/one_by_one.py b/shows/one_by_one.py index da1e492..232b712 100644 --- a/shows/one_by_one.py +++ b/shows/one_by_one.py @@ -1,23 +1,20 @@ from color import RGB -from grid import TriangleGrid +from grid import Grid from .showbase import ShowBase class OneByOne(ShowBase): - def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 0.9): - self.tri_grid = tri_grid + def __init__(self, grid: Grid, frame_delay: float = 0.9): + self.grid = grid self.frame_delay = frame_delay def next_frame(self): - ncells = len(self.tri_grid.cells) - self.tri_grid.clear() + ncells = len(self.grid) while True: for cell in range(ncells): - self.tri_grid.clear() - print(cell) - for pixel in self.tri_grid.set_pixels_by_cellid(cell): - print(pixel) - pixel(RGB(255, 255, 25)) + self.grid.clear() + for pixel in self.grid.pixels(cell): + pixel.set(RGB(255, 255, 25)) yield self.frame_delay diff --git a/shows/random_cells.py b/shows/random_cells.py index e29cb7a..99b0137 100644 --- a/shows/random_cells.py +++ b/shows/random_cells.py @@ -1,22 +1,31 @@ +from typing import Deque +import random + from .showbase import ShowBase from color import RGB -import random as rnd +from grid.cell import Cell class Random(ShowBase): - def __init__(self, tri_grid, frame_delay=0.1): - self.tri_grid = tri_grid + def __init__(self, grid, frame_delay=0.1): + self.grid = grid self.frame_delay = frame_delay - self.n_cells = len(self.tri_grid.cells) - # from IPython import embed; embed() - def next_frame(self): + def shuffle(self) -> Deque[Cell]: + cells = self.grid.cells + random.shuffle(cells) + return Deque(cells) + + def next_frame(self): + cells = self.shuffle() while True: - self.tri_grid.clear() - self.tri_grid.set_cell_by_id(rnd.randint(0, self.n_cells - 2), RGB(200, 255, 25)) - self.tri_grid.set_cell_by_id(rnd.randint(0, self.n_cells - 2), RGB(200, 10, 25)) + if len(cells) == 0: + cells = self.shuffle() + + self.grid.clear() + self.grid.set(cells.popleft(), RGB(200, 10, 25)) yield self.frame_delay diff --git a/shows/strobe.py b/shows/strobe.py index 78830cb..bcae18b 100644 --- a/shows/strobe.py +++ b/shows/strobe.py @@ -1,29 +1,30 @@ +import time + from .showbase import ShowBase from color import RGB -import time +from grid import every class Strobe(ShowBase): - def __init__(self, tri_grid, frame_delay = 0.02): - self.tri_grid = tri_grid + def __init__(self, grid, frame_delay=0.02): + self.grid = grid self.frame_delay = frame_delay - self.n_cells = len(self.tri_grid._cells) + self.n_cells = len(self.grid._cells) def next_frame(self): - while True: - self.tri_grid.clear() - self.tri_grid.set_all_cells(RGB(200, 5, 5)) - self.tri_grid.go() + self.grid.clear() + self.grid.set(every, RGB(200, 5, 5)) + self.grid.go() time.sleep(0.02) - self.tri_grid.set_all_cells(RGB(100, 100, 100)) - self.tri_grid.go() + self.grid.set(every, RGB(100, 100, 100)) + self.grid.go() time.sleep(0.02) - self.tri_grid.set_all_cells(RGB(5, 5, 255)) - self.tri_grid.go() + self.grid.set(every, RGB(5, 5, 255)) + self.grid.go() time.sleep(0.02) - self.tri_grid.set_all_cells(RGB(100, 100, 100)) - self.tri_grid.go() + self.grid.set(every, RGB(100, 100, 100)) + self.grid.go() time.sleep(0.02) yield self.frame_delay diff --git a/shows/up_down.py b/shows/up_down.py index 87d0c11..aee9d6f 100644 --- a/shows/up_down.py +++ b/shows/up_down.py @@ -1,33 +1,23 @@ from color import RGB -from grid import TriangleGrid +from grid import Grid, Orientation, pointed from .showbase import ShowBase class UpDown(ShowBase): - def __init__(self, tri_grid: TriangleGrid, frame_delay: float = 2.0): - self.tri_grid = tri_grid + def __init__(self, grid: Grid, frame_delay: float = 2.0): + self.grid = grid self.frame_delay = frame_delay def next_frame(self): - a = "up" + orientation = Orientation.POINT_UP while True: - self.tri_grid.clear() + self.grid.clear() + color = (RGB(0, 255, 255) + if orientation is Orientation.POINT_UP + else RGB(255, 0, 200)) + self.grid.set(pointed(orientation), color) - if a == "up": - print('up') - for cell in self.tri_grid.up_cells: - print("Up", cell.id) - self.tri_grid.set_cell_by_id(cell.id, RGB(0, 255, 255)) - else: - print('down') - for cell in self.tri_grid.down_cells: - print("down", cell.id) - self.tri_grid.set_cell_by_id(cell.id, RGB(255, 0, 200)) - - if a == "up": - a = "down" - else: - a = "up" - self.tri_grid.go() + self.grid.go() + orientation = orientation.invert() yield self.frame_delay diff --git a/shows/warp.py b/shows/warp.py index 8d145c2..9ab1d57 100644 --- a/shows/warp.py +++ b/shows/warp.py @@ -1,22 +1,20 @@ from color import HSV from .showbase import ShowBase -from grid import TriangleGrid, traversal +from grid import Grid, concentric class Warp(ShowBase): - def __init__(self, grid: TriangleGrid, frame_delay: float = 0.2): + def __init__(self, grid: Grid, frame_delay: float = 0.2): self.grid = grid self.frame_delay = frame_delay def next_frame(self): - row_count = self.grid.row_count - while True: - for points in traversal.concentric(row_count): + for points in concentric(self.grid.row_count): self.grid.clear() - for (row, column) in points: - self.grid.set_cell_by_coordinates(row, column, HSV(.1, .5, .9)) + for pos in points: + self.grid.set(pos, HSV(.1, .5, .9)) self.grid.go() yield self.frame_delay From a15416cfc8d6c6e354e1bbfc55fe208aeaa1d334 Mon Sep 17 00:00:00 2001 From: Lyra Naeseth Date: Mon, 5 Aug 2019 23:15:32 -0700 Subject: [PATCH 41/60] Remove cruft --- model/sacn_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/model/sacn_model.py b/model/sacn_model.py index 9f0d6b4..3f4985c 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -17,14 +17,14 @@ class sACN(ModelBase): def __init__(self, bind_address: str, row_count: int): - self.sender = sacn.sACNsender(bind_address, universeDiscovery=False) + self.sender = sacn.sACNsender( + bind_address=bind_address, + universeDiscovery=False, + ) self.sender.start() # dictionary which will hold an array of 512 int's for each universe, universes are keys to the arrays. self.leds = map_leds(row_count) - for ux in sorted(self.leds): - print(f'{ux}: {len(self.leds[ux])}') - print(f'Universes: {len(self.leds)}') for universe_index in self.leds: self.sender.activate_output(universe_index) self.sender[universe_index].multicast = True From 2e4c09c08d25c29f4246f4eabfc54d7d996ecb28 Mon Sep 17 00:00:00 2001 From: Lyra Naeseth Date: Wed, 7 Aug 2019 23:19:00 -0700 Subject: [PATCH 42/60] Fix IP detection --- go_tri.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go_tri.py b/go_tri.py index c4e2827..75882f2 100644 --- a/go_tri.py +++ b/go_tri.py @@ -344,7 +344,7 @@ def go_web(self): for a in netifaces.ifaddresses(interface).get(netifaces.AF_INET, []): if a['addr'].startswith('192.168'): logger.info(f"Auto-detected local IP: {a['addr']}") - bind = a.addr + bind = a['addr'] break if bind: break From ebe1258f3a08ab41a997c22c10bc80c4fed8f058 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Thu, 15 Aug 2019 16:10:30 -0700 Subject: [PATCH 43/60] Fixes simulator with a little hack on model.set() --- .../TriangleSimulator/TriangleSimulator.pde | 203 ++++----------- Simulators/TriangleSimulator/mapping_tri.csv | 122 --------- .../TriangleSimulator/triangleCellMapping.csv | 243 +++++++++--------- go_tri.py | 13 +- grid/grid.py | 10 +- model/__init__.py | 2 +- model/base.py | 8 +- model/mirror.py | 8 +- model/ola_model.py | 2 +- model/sacn_model.py | 9 +- model/simulator.py | 61 ++--- 11 files changed, 220 insertions(+), 461 deletions(-) delete mode 100644 Simulators/TriangleSimulator/mapping_tri.csv diff --git a/Simulators/TriangleSimulator/TriangleSimulator.pde b/Simulators/TriangleSimulator/TriangleSimulator.pde index ccfd963..ade5d89 100644 --- a/Simulators/TriangleSimulator/TriangleSimulator.pde +++ b/Simulators/TriangleSimulator/TriangleSimulator.pde @@ -1,60 +1,36 @@ /* Playing around with hex drawing, as described at: http://www.redblobgames.com/grids/hexagons/ - + Grid shapes are described in arrays of arrays: {skip_count, fill_count, skip_count ...} */ import processing.net.*; import java.util.regex.*; -Table table; boolean DRAW_LABELS = true; -//boolean POINTY_TOP = true; -boolean DARK_MODE = false; - - - - - // model vars -HexForm honeycomb = null; -HashMap labels = null; +TriangleForm grid = null; // network vars int port = 4444; Server _server; StringBuffer _buf = new StringBuffer(); -color defaultHexLine() { - return color(255,255,255); -} - -color defaultHexFill() { - return color(255,0,255); -} - -PVector a = new PVector(400, 100); -PVector b = new PVector(100, 600); -PVector c = new PVector(700, 600); -float hue; - +color defaultLineColor = color(255, 255, 255); +color defaultFillColor = color(255, 0, 255); void setup() { size(800,700); rotate(radians(0)); frameRate(30); - // println(PFont.list()); PFont f = createFont("Helvetica", 12, true); textFont(f, 12); - labels = loadLabels("mapping_tri.csv"); - - honeycomb = makeSimpleGrid(8,1,500,300); - //honeycomb = makeHexForm(SNOWFLAKE, 50, 50); + grid = makeSimpleGrid(); _server = new Server(this, port); println("server listening:" + _server); @@ -64,49 +40,40 @@ void drawCheckbox(int x, int y, boolean checked) { int size = 20; stroke(0); fill(255); - rect(x,y,size,size); + rect(x, y, size, size); if (checked) { - line(x,y,x+size,y+size); - line(x+size,y,x,y+size); + line(x, y, x+size, y+size); + line(x+size, y, x, y+size); } } - void drawBottomControls() { // draw a bottom white region - fill(255,255,255); - rect(0,500,500,50); - + fill(255, 255, 255); + rect(0, 500, 500, 50); + // draw checkboxes stroke(0); fill(255); - drawCheckbox(20,510, DRAW_LABELS); // label checkbox - // drawCheckbox(190,510, POINTY_TOP); // pointy-top checkbox - drawCheckbox(360,510, DARK_MODE); // dark mode - + drawCheckbox(20, 510, DRAW_LABELS); // label checkbox + // draw text labels fill(0); textAlign(LEFT); text("Draw Labels", 50, 525); - text("Pointy Top", 220, 525); - text("Dark Mode", 390, 525); } - void mouseClicked() { - //println("click! x:" + mouseX + " y:" + mouseY); if (mouseX > 20 && mouseX < 40 && mouseY > 510 && mouseY < 530) { // clicked draw labels button DRAW_LABELS = !DRAW_LABELS; - } else if (mouseX > 360 && mouseX < 380 && mouseY > 510 && mouseY < 530) { - DARK_MODE = !DARK_MODE; } } void draw() { background(250); drawBottomControls(); - honeycomb.draw(); + grid.draw(); pollServer(); } @@ -125,7 +92,6 @@ void pollServer() { while (ix > -1) { String msg = _buf.substring(0, ix); msg = msg.trim(); - //println(msg); processCommand(msg); _buf.delete(0, ix+1); ix = _buf.indexOf("\n"); @@ -136,8 +102,7 @@ void pollServer() { } } -//Pattern cmd_pattern = Pattern.compile("^\\s*(\\d+)\\s+(\\d+),(\\d+),(\\d+)\\s*$"); -Pattern cmd_pattern = Pattern.compile("^\\s*(p|b|a|f )\\s+(\\d+)\\s+(\\d+),(\\d+),(\\d+)\\s*$"); +Pattern cmd_pattern = Pattern.compile("^\\s*(\\d+)\\s+(\\d+),(\\d+),(\\d+)\\s*$"); void processCommand(String cmd) { Matcher m = cmd_pattern.matcher(cmd); @@ -145,38 +110,17 @@ void processCommand(String cmd) { println("ignoring input!"); return; } - String side = m.group(1); - int cell = Integer.valueOf(m.group(2)); - int r = Integer.valueOf(m.group(3)); - int g = Integer.valueOf(m.group(4)); - int b = Integer.valueOf(m.group(5)); + int cell = Integer.valueOf(m.group(1)); + int r = Integer.valueOf(m.group(2)); + int g = Integer.valueOf(m.group(3)); + int b = Integer.valueOf(m.group(4)); - honeycomb.setCellColor(cell, color(r,g,b)); + grid.setCellColor(cell, color(r,g,b)); } -/* -mappings* Load label mapping file - */ -HashMap loadLabels(String labelFile) { - HashMap labels = new HashMap(); - Table table = loadTable(labelFile); - - println(table.getRowCount() + " total rows in table"); - - for (TableRow row : table.rows()) { - int id = row.getInt(0); - String coord = row.getString(1); - labels.put(id, coord); - } - return labels; -} - - - - -HexForm makeSimpleGrid(int rows, int cols, int start_x, int start_y) { - HexForm form = new HexForm(); - table = loadTable("triangleCellMapping.csv", "header"); +TriangleForm makeSimpleGrid() { + TriangleForm form = new TriangleForm(); + Table table = loadTable("triangleCellMapping.csv", "header"); for (TableRow row : table.rows()) { String shape = row.getString("shape"); int x1 = row.getInt("x1"); @@ -186,59 +130,47 @@ HexForm makeSimpleGrid(int rows, int cols, int start_x, int start_y) { int x3 = row.getInt("x3"); int y3 = row.getInt("y3"); int id = row.getInt("id"); - //triangle(x1,y1,x2,y2,x3,y3); - // print(shape); - form.add(new Hex(x1,y1,x2,y2,x3,y3,id),id); - } + form.add(new Triangle(x1, y1, x2, y2, x3, y3, id), id); + } return form; } -class HexForm { - ArrayList hexes; - //HashMap hexesById; - - HexForm() { - hexes = new ArrayList(); - //hexesById = new HashMap(); +class TriangleForm { + ArrayList triangles = new ArrayList(); + + TriangleForm() { } - - void add(Hex h, int hexId) { -// int hexId = hexes.size(); -// if (labels != null) { -// h.setId(labels.get(hexId)); -// } else { -// h.setId(String.valueOf(hexId)); -// } - print("HEXID:", hexId); - h.setId(hexId); - hexes.add(h); + + void add(Triangle triangle, int id) { + print("Triangle ID:", id); + triangle.setId(id); + triangles.add(triangle); } - + int size() { - return hexes.size(); + return triangles.size(); } - + void draw() { - for (Hex h : hexes) { - h.draw(); + for (Triangle triangle : triangles) { + triangle.draw(); } } - + // XXX probably need a better API here! void setCellColor(int i, color c) { - if (i >= hexes.size()) { - println("invalid offset for HexForm.setColor: i only have " + hexes.size() + " hexes"); -// hexes.get(1).setColor(255); - - } else { - hexes.get(i).setColor(c); + if (i >= triangles.size()) { + println("invalid offset for HexForm.setColor: i only have " + triangles.size() + " hexes"); + return; } + + triangles.get(i).setColor(c); } } -class Hex { +class Triangle { int id = 0; // optional int x1; int y1; @@ -250,8 +182,8 @@ class Hex { Integer c; // can store color/int or null - Hex(int x1, int y1,int x2, int y2,int x3, int y3, int id) { - print("CreateHex\n"); + Triangle(int x1, int y1,int x2, int y2,int x3, int y3, int id) { + print("CreateTriangle\n"); this.x1 = x1; this.y1 = y1; this.x2 = x2; @@ -267,28 +199,25 @@ class Hex { this.id = id; print("pass"); } - + void setColor(color c) { this.c = c; } - void draw() { - color fill_color = (this.c != null) ? c : defaultHexFill(); + color fill_color = (this.c != null) ? c : defaultFillColor; fill(fill_color); - stroke(defaultHexLine()); + stroke(defaultLineColor); beginShape(); - triangle(this.x1,this.y1,this.x2,this.y2,this.x3,this.y3); - + triangle(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); + endShape(CLOSE); - + // draw text label if (DRAW_LABELS && this.id != 0) { -// fill(defaultHexLine()); -// if (this.cell == 0) { - fill(defaultHexLine()); - + fill(defaultLineColor); + if (this.y1 == this.y2) { textAlign(CENTER); print(this.id,"pointy"); @@ -298,26 +227,8 @@ class Hex { print(this.id,"flat"); text(this.id,this.x1,this.y1+30); } -// this.cell = 1; -// } else { -// textAlign(BOTTOM); -// text(this.id,this.x1+20,this.y3); -// this.cell = 0; - // } - // if (this.id % 2 == 0) { - // textAlign(CENTER); - // text(this.id,this.x1,this.y1);/ -// - // } else { - // textAlign(CENTER); - // text(this.id,this.x3+5,this.y3); -// - // } - - // print(this.id, this.x1, this.y2,this.x2, this.y2,this.x3, this.y3 ); } + noFill(); - - } } diff --git a/Simulators/TriangleSimulator/mapping_tri.csv b/Simulators/TriangleSimulator/mapping_tri.csv deleted file mode 100644 index 4b5e360..0000000 --- a/Simulators/TriangleSimulator/mapping_tri.csv +++ /dev/null @@ -1,122 +0,0 @@ -0,"0" -1,"1" -2,"2" -3,"3" -4,"4" -5,"5" -6,"6" -7,"7" -8,"8" -9,"9" -10,"10" -11,"11" -12,"12" -13,"13" -14,"14" -15,"15" -16,"16" -17,"17" -18,"18" -19,"19" -20,"20" -21,"21" -22,"22" -23,"23" -24,"24" -25,"25" -26,"26" -27,"27" -28,"28" -29,"29" -30,"30" -31,"31" -32,"32" -33,"33" -34,"34" -35,"35" -36,"36" -37,"37" -38,"38" -39,"39" -40,"40" -41,"41" -42,"42" -43,"43" -44,"44" -45,"45" -46,"46" -47,"47" -48,"48" -49,"49" -50,"50" -51,"51" -52,"52" -53,"53" -54,"54" -55,"55" -56,"56" -57,"57" -58,"58" -59,"59" -60,"60" -61,"61" -62,"62" -63,"63" -64,"64" -65,"65" -66,"66" -67,"67" -68,"68" -69,"69" -70,"70" -71,"71" -72,"72" -73,"73" -74,"74" -75,"75" -76,"76" -77,"77" -78,"78" -79,"79" -80,"80" -81,"81" -82,"82" -83,"83" -84,"84" -85,"85" -86,"86" -87,"87" -88,"88" -89,"89" -90,"90" -91,"91" -92,"92" -93,"93" -94,"94" -95,"95" -96,"96" -97,"97" -98,"98" -99,"99" -100,"100" -101,"101" -102,"102" -103,"103" -104,"104" -105,"105" -106,"106" -107,"107" -108,"108" -109,"109" -110,"110" -111,"111" -112,"112" -113,"113" -114,"114" -115,"115" -116,"116" -117,"117" -118,"118" -119,"119" -120,"120" -121,"121" \ No newline at end of file diff --git a/Simulators/TriangleSimulator/triangleCellMapping.csv b/Simulators/TriangleSimulator/triangleCellMapping.csv index 609b8a8..f4cc1f4 100644 --- a/Simulators/TriangleSimulator/triangleCellMapping.csv +++ b/Simulators/TriangleSimulator/triangleCellMapping.csv @@ -1,123 +1,122 @@ shape,x1,y1,x2,y2,x3,y3,id -triangle,0,0,0,0,0,0,0 -triangle,400.0,100.0,381.25,131.25,418.75,131.25,1 -triangle,381.25,131.25,362.5,162.5,400.0,162.5,2 -triangle,381.25,131.25,418.75,131.25,400.0,162.5,3 -triangle,418.75,131.25,400.0,162.5,437.5,162.5,4 -triangle,362.5,162.5,343.75,193.75,381.25,193.75,5 -triangle,362.5,162.5,400.0,162.5,381.25,193.75,6 -triangle,400.0,162.5,381.25,193.75,418.75,193.75,7 -triangle,400.0,162.5,437.5,162.5,418.75,193.75,8 -triangle,437.5,162.5,418.75,193.75,456.25,193.75,9 -triangle,343.75,193.75,325.0,225.0,362.5,225.0,10 -triangle,343.75,193.75,381.25,193.75,362.5,225.0,11 -triangle,381.25,193.75,362.5,225.0,400.0,225.0,12 -triangle,381.25,193.75,418.75,193.75,400.0,225.0,13 -triangle,418.75,193.75,400.0,225.0,437.5,225.0,14 -triangle,418.75,193.75,456.25,193.75,437.5,225.0,15 -triangle,456.25,193.75,437.5,225.0,475.0,225.0,16 -triangle,325.0,225.0,306.25,256.25,343.75,256.25,17 -triangle,325.0,225.0,362.5,225.0,343.75,256.25,18 -triangle,362.5,225.0,343.75,256.25,381.25,256.25,19 -triangle,362.5,225.0,400.0,225.0,381.25,256.25,20 -triangle,400.0,225.0,381.25,256.25,418.75,256.25,21 -triangle,400.0,225.0,437.5,225.0,418.75,256.25,22 -triangle,437.5,225.0,418.75,256.25,456.25,256.25,23 -triangle,437.5,225.0,475.0,225.0,456.25,256.25,24 -triangle,475.0,225.0,456.25,256.25,493.75,256.25,25 -triangle,306.25,256.25,287.5,287.5,325.0,287.5,26 -triangle,306.25,256.25,343.75,256.25,325.0,287.5,27 -triangle,343.75,256.25,325.0,287.5,362.5,287.5,28 -triangle,343.75,256.25,381.25,256.25,362.5,287.5,29 -triangle,381.25,256.25,362.5,287.5,400.0,287.5,30 -triangle,381.25,256.25,418.75,256.25,400.0,287.5,31 -triangle,418.75,256.25,400.0,287.5,437.5,287.5,32 -triangle,418.75,256.25,456.25,256.25,437.5,287.5,33 -triangle,456.25,256.25,437.5,287.5,475.0,287.5,34 -triangle,456.25,256.25,493.75,256.25,475.0,287.5,35 -triangle,493.75,256.25,475.0,287.5,512.5,287.5,36 -triangle,287.5,287.5,268.75,318.75,306.25,318.75,37 -triangle,287.5,287.5,325.0,287.5,306.25,318.75,38 -triangle,325.0,287.5,306.25,318.75,343.75,318.75,39 -triangle,325.0,287.5,362.5,287.5,343.75,318.75,40 -triangle,362.5,287.5,343.75,318.75,381.25,318.75,41 -triangle,362.5,287.5,400.0,287.5,381.25,318.75,42 -triangle,400.0,287.5,381.25,318.75,418.75,318.75,43 -triangle,400.0,287.5,437.5,287.5,418.75,318.75,44 -triangle,437.5,287.5,418.75,318.75,456.25,318.75,45 -triangle,437.5,287.5,475.0,287.5,456.25,318.75,46 -triangle,475.0,287.5,456.25,318.75,493.75,318.75,47 -triangle,475.0,287.5,512.5,287.5,493.75,318.75,48 -triangle,512.5,287.5,493.75,318.75,531.25,318.75,49 -triangle,268.75,318.75,250.0,350.0,287.5,350.0,50 -triangle,268.75,318.75,306.25,318.75,287.5,350.0,51 -triangle,306.25,318.75,287.5,350.0,325.0,350.0,52 -triangle,306.25,318.75,343.75,318.75,325.0,350.0,53 -triangle,343.75,318.75,325.0,350.0,362.5,350.0,54 -triangle,343.75,318.75,381.25,318.75,362.5,350.0,55 -triangle,381.25,318.75,362.5,350.0,400.0,350.0,56 -triangle,381.25,318.75,418.75,318.75,400.0,350.0,57 -triangle,418.75,318.75,400.0,350.0,437.5,350.0,58 -triangle,418.75,318.75,456.25,318.75,437.5,350.0,59 -triangle,456.25,318.75,437.5,350.0,475.0,350.0,60 -triangle,456.25,318.75,493.75,318.75,475.0,350.0,61 -triangle,493.75,318.75,475.0,350.0,512.5,350.0,62 -triangle,493.75,318.75,531.25,318.75,512.5,350.0,63 -triangle,531.25,318.75,512.5,350.0,550.0,350.0,64 -triangle,250.0,350.0,231.25,381.25,268.75,381.25,65 -triangle,250.0,350.0,287.5,350.0,268.75,381.25,66 -triangle,287.5,350.0,268.75,381.25,306.25,381.25,67 -triangle,287.5,350.0,325.0,350.0,306.25,381.25,68 -triangle,325.0,350.0,306.25,381.25,343.75,381.25,69 -triangle,325.0,350.0,362.5,350.0,343.75,381.25,70 -triangle,362.5,350.0,343.75,381.25,381.25,381.25,71 -triangle,362.5,350.0,400.0,350.0,381.25,381.25,72 -triangle,400.0,350.0,381.25,381.25,418.75,381.25,73 -triangle,400.0,350.0,437.5,350.0,418.75,381.25,74 -triangle,437.5,350.0,418.75,381.25,456.25,381.25,75 -triangle,437.5,350.0,475.0,350.0,456.25,381.25,76 -triangle,475.0,350.0,456.25,381.25,493.75,381.25,77 -triangle,475.0,350.0,512.5,350.0,493.75,381.25,78 -triangle,512.5,350.0,493.75,381.25,531.25,381.25,79 -triangle,512.5,350.0,550.0,350.0,531.25,381.25,80 -triangle,550.0,350.0,531.25,381.25,568.75,381.25,81 -triangle,231.25,381.25,212.5,412.5,250.0,412.5,82 -triangle,231.25,381.25,268.75,381.25,250.0,412.5,83 -triangle,268.75,381.25,250.0,412.5,287.5,412.5,84 -triangle,268.75,381.25,306.25,381.25,287.5,412.5,85 -triangle,306.25,381.25,287.5,412.5,325.0,412.5,86 -triangle,306.25,381.25,343.75,381.25,325.0,412.5,87 -triangle,343.75,381.25,325.0,412.5,362.5,412.5,88 -triangle,343.75,381.25,381.25,381.25,362.5,412.5,89 -triangle,381.25,381.25,362.5,412.5,400.0,412.5,90 -triangle,381.25,381.25,418.75,381.25,400.0,412.5,91 -triangle,418.75,381.25,400.0,412.5,437.5,412.5,92 -triangle,418.75,381.25,456.25,381.25,437.5,412.5,93 -triangle,456.25,381.25,437.5,412.5,475.0,412.5,94 -triangle,456.25,381.25,493.75,381.25,475.0,412.5,95 -triangle,493.75,381.25,475.0,412.5,512.5,412.5,96 -triangle,493.75,381.25,531.25,381.25,512.5,412.5,97 -triangle,531.25,381.25,512.5,412.5,550.0,412.5,98 -triangle,531.25,381.25,568.75,381.25,550.0,412.5,99 -triangle,568.75,381.25,550.0,412.5,587.5,412.5,100 -triangle,212.5,412.5,193.75,443.75,231.25,443.75,101 -triangle,212.5,412.5,250.0,412.5,231.25,443.75,102 -triangle,250.0,412.5,231.25,443.75,268.75,443.75,103 -triangle,250.0,412.5,287.5,412.5,268.75,443.75,104 -triangle,287.5,412.5,268.75,443.75,306.25,443.75,105 -triangle,287.5,412.5,325.0,412.5,306.25,443.75,106 -triangle,325.0,412.5,306.25,443.75,343.75,443.75,107 -triangle,325.0,412.5,362.5,412.5,343.75,443.75,108 -triangle,362.5,412.5,343.75,443.75,381.25,443.75,109 -triangle,362.5,412.5,400.0,412.5,381.25,443.75,110 -triangle,400.0,412.5,381.25,443.75,418.75,443.75,111 -triangle,400.0,412.5,437.5,412.5,418.75,443.75,112 -triangle,437.5,412.5,418.75,443.75,456.25,443.75,113 -triangle,437.5,412.5,475.0,412.5,456.25,443.75,114 -triangle,475.0,412.5,456.25,443.75,493.75,443.75,115 -triangle,475.0,412.5,512.5,412.5,493.75,443.75,116 -triangle,512.5,412.5,493.75,443.75,531.25,443.75,117 -triangle,512.5,412.5,550.0,412.5,531.25,443.75,118 -triangle,550.0,412.5,531.25,443.75,568.75,443.75,119 -triangle,550.0,412.5,587.5,412.5,568.75,443.75,120 -triangle,587.5,412.5,568.75,443.75,606.25,443.75,121 \ No newline at end of file +triangle,400.0,100.0,381.25,131.25,418.75,131.25,0 +triangle,381.25,131.25,362.5,162.5,400.0,162.5,1 +triangle,381.25,131.25,418.75,131.25,400.0,162.5,2 +triangle,418.75,131.25,400.0,162.5,437.5,162.5,3 +triangle,362.5,162.5,343.75,193.75,381.25,193.75,4 +triangle,362.5,162.5,400.0,162.5,381.25,193.75,5 +triangle,400.0,162.5,381.25,193.75,418.75,193.75,6 +triangle,400.0,162.5,437.5,162.5,418.75,193.75,7 +triangle,437.5,162.5,418.75,193.75,456.25,193.75,8 +triangle,343.75,193.75,325.0,225.0,362.5,225.0,9 +triangle,343.75,193.75,381.25,193.75,362.5,225.0,10 +triangle,381.25,193.75,362.5,225.0,400.0,225.0,11 +triangle,381.25,193.75,418.75,193.75,400.0,225.0,12 +triangle,418.75,193.75,400.0,225.0,437.5,225.0,13 +triangle,418.75,193.75,456.25,193.75,437.5,225.0,14 +triangle,456.25,193.75,437.5,225.0,475.0,225.0,15 +triangle,325.0,225.0,306.25,256.25,343.75,256.25,16 +triangle,325.0,225.0,362.5,225.0,343.75,256.25,17 +triangle,362.5,225.0,343.75,256.25,381.25,256.25,18 +triangle,362.5,225.0,400.0,225.0,381.25,256.25,19 +triangle,400.0,225.0,381.25,256.25,418.75,256.25,20 +triangle,400.0,225.0,437.5,225.0,418.75,256.25,21 +triangle,437.5,225.0,418.75,256.25,456.25,256.25,22 +triangle,437.5,225.0,475.0,225.0,456.25,256.25,23 +triangle,475.0,225.0,456.25,256.25,493.75,256.25,24 +triangle,306.25,256.25,287.5,287.5,325.0,287.5,25 +triangle,306.25,256.25,343.75,256.25,325.0,287.5,26 +triangle,343.75,256.25,325.0,287.5,362.5,287.5,27 +triangle,343.75,256.25,381.25,256.25,362.5,287.5,28 +triangle,381.25,256.25,362.5,287.5,400.0,287.5,29 +triangle,381.25,256.25,418.75,256.25,400.0,287.5,30 +triangle,418.75,256.25,400.0,287.5,437.5,287.5,31 +triangle,418.75,256.25,456.25,256.25,437.5,287.5,32 +triangle,456.25,256.25,437.5,287.5,475.0,287.5,33 +triangle,456.25,256.25,493.75,256.25,475.0,287.5,34 +triangle,493.75,256.25,475.0,287.5,512.5,287.5,35 +triangle,287.5,287.5,268.75,318.75,306.25,318.75,36 +triangle,287.5,287.5,325.0,287.5,306.25,318.75,37 +triangle,325.0,287.5,306.25,318.75,343.75,318.75,38 +triangle,325.0,287.5,362.5,287.5,343.75,318.75,39 +triangle,362.5,287.5,343.75,318.75,381.25,318.75,40 +triangle,362.5,287.5,400.0,287.5,381.25,318.75,41 +triangle,400.0,287.5,381.25,318.75,418.75,318.75,42 +triangle,400.0,287.5,437.5,287.5,418.75,318.75,43 +triangle,437.5,287.5,418.75,318.75,456.25,318.75,44 +triangle,437.5,287.5,475.0,287.5,456.25,318.75,45 +triangle,475.0,287.5,456.25,318.75,493.75,318.75,46 +triangle,475.0,287.5,512.5,287.5,493.75,318.75,47 +triangle,512.5,287.5,493.75,318.75,531.25,318.75,48 +triangle,268.75,318.75,250.0,350.0,287.5,350.0,49 +triangle,268.75,318.75,306.25,318.75,287.5,350.0,50 +triangle,306.25,318.75,287.5,350.0,325.0,350.0,51 +triangle,306.25,318.75,343.75,318.75,325.0,350.0,52 +triangle,343.75,318.75,325.0,350.0,362.5,350.0,53 +triangle,343.75,318.75,381.25,318.75,362.5,350.0,54 +triangle,381.25,318.75,362.5,350.0,400.0,350.0,55 +triangle,381.25,318.75,418.75,318.75,400.0,350.0,56 +triangle,418.75,318.75,400.0,350.0,437.5,350.0,57 +triangle,418.75,318.75,456.25,318.75,437.5,350.0,58 +triangle,456.25,318.75,437.5,350.0,475.0,350.0,59 +triangle,456.25,318.75,493.75,318.75,475.0,350.0,60 +triangle,493.75,318.75,475.0,350.0,512.5,350.0,61 +triangle,493.75,318.75,531.25,318.75,512.5,350.0,62 +triangle,531.25,318.75,512.5,350.0,550.0,350.0,63 +triangle,250.0,350.0,231.25,381.25,268.75,381.25,64 +triangle,250.0,350.0,287.5,350.0,268.75,381.25,65 +triangle,287.5,350.0,268.75,381.25,306.25,381.25,66 +triangle,287.5,350.0,325.0,350.0,306.25,381.25,67 +triangle,325.0,350.0,306.25,381.25,343.75,381.25,68 +triangle,325.0,350.0,362.5,350.0,343.75,381.25,69 +triangle,362.5,350.0,343.75,381.25,381.25,381.25,70 +triangle,362.5,350.0,400.0,350.0,381.25,381.25,71 +triangle,400.0,350.0,381.25,381.25,418.75,381.25,72 +triangle,400.0,350.0,437.5,350.0,418.75,381.25,73 +triangle,437.5,350.0,418.75,381.25,456.25,381.25,74 +triangle,437.5,350.0,475.0,350.0,456.25,381.25,75 +triangle,475.0,350.0,456.25,381.25,493.75,381.25,76 +triangle,475.0,350.0,512.5,350.0,493.75,381.25,77 +triangle,512.5,350.0,493.75,381.25,531.25,381.25,78 +triangle,512.5,350.0,550.0,350.0,531.25,381.25,79 +triangle,550.0,350.0,531.25,381.25,568.75,381.25,80 +triangle,231.25,381.25,212.5,412.5,250.0,412.5,81 +triangle,231.25,381.25,268.75,381.25,250.0,412.5,82 +triangle,268.75,381.25,250.0,412.5,287.5,412.5,83 +triangle,268.75,381.25,306.25,381.25,287.5,412.5,84 +triangle,306.25,381.25,287.5,412.5,325.0,412.5,85 +triangle,306.25,381.25,343.75,381.25,325.0,412.5,86 +triangle,343.75,381.25,325.0,412.5,362.5,412.5,87 +triangle,343.75,381.25,381.25,381.25,362.5,412.5,88 +triangle,381.25,381.25,362.5,412.5,400.0,412.5,89 +triangle,381.25,381.25,418.75,381.25,400.0,412.5,90 +triangle,418.75,381.25,400.0,412.5,437.5,412.5,91 +triangle,418.75,381.25,456.25,381.25,437.5,412.5,92 +triangle,456.25,381.25,437.5,412.5,475.0,412.5,93 +triangle,456.25,381.25,493.75,381.25,475.0,412.5,94 +triangle,493.75,381.25,475.0,412.5,512.5,412.5,95 +triangle,493.75,381.25,531.25,381.25,512.5,412.5,96 +triangle,531.25,381.25,512.5,412.5,550.0,412.5,97 +triangle,531.25,381.25,568.75,381.25,550.0,412.5,98 +triangle,568.75,381.25,550.0,412.5,587.5,412.5,99 +triangle,212.5,412.5,193.75,443.75,231.25,443.75,100 +triangle,212.5,412.5,250.0,412.5,231.25,443.75,101 +triangle,250.0,412.5,231.25,443.75,268.75,443.75,102 +triangle,250.0,412.5,287.5,412.5,268.75,443.75,103 +triangle,287.5,412.5,268.75,443.75,306.25,443.75,104 +triangle,287.5,412.5,325.0,412.5,306.25,443.75,105 +triangle,325.0,412.5,306.25,443.75,343.75,443.75,106 +triangle,325.0,412.5,362.5,412.5,343.75,443.75,107 +triangle,362.5,412.5,343.75,443.75,381.25,443.75,108 +triangle,362.5,412.5,400.0,412.5,381.25,443.75,109 +triangle,400.0,412.5,381.25,443.75,418.75,443.75,110 +triangle,400.0,412.5,437.5,412.5,418.75,443.75,111 +triangle,437.5,412.5,418.75,443.75,456.25,443.75,112 +triangle,437.5,412.5,475.0,412.5,456.25,443.75,113 +triangle,475.0,412.5,456.25,443.75,493.75,443.75,114 +triangle,475.0,412.5,512.5,412.5,493.75,443.75,115 +triangle,512.5,412.5,493.75,443.75,531.25,443.75,116 +triangle,512.5,412.5,550.0,412.5,531.25,443.75,117 +triangle,550.0,412.5,531.25,443.75,568.75,443.75,118 +triangle,550.0,412.5,587.5,412.5,568.75,443.75,119 +triangle,587.5,412.5,568.75,443.75,606.25,443.75,120 \ No newline at end of file diff --git a/go_tri.py b/go_tri.py index 75882f2..fd28b13 100644 --- a/go_tri.py +++ b/go_tri.py @@ -8,7 +8,7 @@ import cherrypy from grid import Grid -from model import sACN +from model import sACN, SimulatorModel import netifaces import osc_serve import shows @@ -327,14 +327,11 @@ def go_web(self): sys.exit(0) if args.simulator: - parser.error('The simulator is broken') + sim_host = "localhost" + sim_port = 4444 + logger.info(f'Using TriSimulator at {sim_host}:{sim_port}') - # sim_host = "localhost" - # sim_port = 4444 - # logger.info(f'Using TriSimulator at {sim_host}:{sim_port}') - - # model = SimulatorModel(sim_host, port=sim_port, - # model_json='./data/pixel_map.json') + model = SimulatorModel(sim_host, port=sim_port) else: bind = args.bind if not bind: diff --git a/grid/grid.py b/grid/grid.py index 2d185e6..72cba3d 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -2,8 +2,8 @@ from typing import Callable, Iterator, Iterable, List, Mapping, NamedTuple, Union, Type from color import Color, RGB -from model import ModelBase -from .cell import generate, Address, Cell, Direction, Position, Orientation +from model import ModelBase, SimulatorModel +from .cell import generate, Address, Cell, Direction, Position logger = logging.getLogger('pyramidtriangles') @@ -22,7 +22,11 @@ class Pixel(NamedTuple): model: Type[ModelBase] def set(self, color: Color): - self.model.set(self.address, color) + # For ease, we set by cell ID in the simulator + if isinstance(self.model, SimulatorModel): + self.model.set(self.cell.id, color) + else: + self.model.set(self.address, color) def __call__(self, color: Color): self.set(color) diff --git a/model/__init__.py b/model/__init__.py index e8b5fec..69e194e 100644 --- a/model/__init__.py +++ b/model/__init__.py @@ -3,4 +3,4 @@ from .mirror import MirrorModel # from .ola_model import OLAModel from .sacn_model import sACN -# from .simulator import SimulatorModel +from .simulator import SimulatorModel diff --git a/model/base.py b/model/base.py index 176f456..c15b212 100644 --- a/model/base.py +++ b/model/base.py @@ -1,18 +1,18 @@ from abc import ABC, abstractmethod from color import Color from grid.cell import Address, universe_count, universe_size -from typing import Callable, List, Mapping - -SetColorFunc = Callable[[Color], None] +from typing import List, Mapping, Union class ModelBase(ABC): """Abstract base class for simulators.""" @abstractmethod - def set(self, addr: Address, color: Color): + def set(self, addr: Union[Address, int], color: Color): """ Set one pixel to a particular color. + + addr is an Address, except in the case of the simulator it is a cell ID. """ @abstractmethod diff --git a/model/mirror.py b/model/mirror.py index 99de832..7486309 100644 --- a/model/mirror.py +++ b/model/mirror.py @@ -1,10 +1,8 @@ - -from itertools import zip_longest -from typing import Iterator +from typing import Union from color import Color from grid.cell import Address -from .base import ModelBase, SetColorFunc +from .base import ModelBase class MirrorModel(ModelBase): @@ -18,7 +16,7 @@ def __init__(self, *models): def add_model(self, model): self.models.append(model) - def set(self, addr: Address, color: Color): + def set(self, addr: Union[Address, int], color: Color): for m in self.models: m.set(addr, color) diff --git a/model/ola_model.py b/model/ola_model.py index 2bbaca8..5ece519 100644 --- a/model/ola_model.py +++ b/model/ola_model.py @@ -11,7 +11,7 @@ from typing import Iterator import ola -from .base import ModelBase, SetColorFunc +from .base import ModelBase from .mapping import PixelMap logger = logging.getLogger("pyramidtriangles") diff --git a/model/sacn_model.py b/model/sacn_model.py index 3f4985c..4432959 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -5,11 +5,11 @@ have one LED each. """ import logging -from typing import Iterator, List, Mapping +from typing import Union import sacn from color import Color -from grid.cell import Address, universe_count, universe_size +from grid.cell import Address from .base import ModelBase, map_leds logger = logging.getLogger("pyramidtriangles") @@ -32,7 +32,10 @@ def __init__(self, bind_address: str, row_count: int): def __del__(self): self.sender.stop() # If the object is destructing, close the sender connection - def set(self, addr: Address, color: Color): + def set(self, addr: Union[Address, int], color: Color): + if not isinstance(addr, Address): + raise TypeError('Expected set() with DMX address') + try: channels = self.leds[addr.universe] except KeyError: diff --git a/model/simulator.py b/model/simulator.py index f7afdc5..d345193 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -1,74 +1,43 @@ """ Model to communicate with a Simulator over a TCP socket. - -XXX Should this class be able to do range checks on cell ids? """ import logging +import queue import socket -import json -from typing import Iterator +from typing import Union from color import Color -from .base import ModelBase, SetColorFunc +from grid import Address +from .base import ModelBase SIM_DEFAULT = (188, 210, 229) # BCD2E5, "off" color for simulator logger = logging.getLogger("pyramidtriangles") -# XXX(lyra): not yet updated! the simulator doesn't (yet) work with individual -# LEDs; all cells are uniform. class SimulatorModel(ModelBase): - def __init__(self, hostname, port=4444, model_json=None): + def __init__(self, hostname: str, port: int): self.hostname = hostname self.port = port - self._map_leds(model_json) - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((self.hostname, self.port)) - # map of cells to be set on the next call to go - self.dirty = {} + # queue of 'dirty' messages to send + self.message_queue = queue.SimpleQueue() def __repr__(self): return f'{__class__.__name__} (hostname={self.hostname}, port={self.port})' - # Loaders - def _map_leds(self, f): - # Loads a json file with mapping info describing your LEDs. - # The json file is formatted as a dictionary of numbers each key in the dict is a fixtureUID. - # Each array that fixtureUID returns is of the format [universeUID, DMXstart#]. - with open(f, 'r') as json_file: - self.CELL_MAP = json.load(json_file, object_hook=lambda d: { - int(k): v for (k, v) in d.items()}) - - # Model basics - def set_pixels_by_cellid(self, cell_id: int) -> Iterator[SetColorFunc]: - def set_color(color: Color): - self.set_cell(cell_id, color) - # Iterator of one for the simulator, which doesn't have pixels within a cell. - return iter([set_color]) + def set(self, addr: Union[Address, int], color: Color): + if not isinstance(addr, int): + raise TypeError('Cannot set DMX addresses in Simulator') - def set_cell(self, cell: int, color: Color): - cell += 1 # Simulator cells not 0 based - if cell not in self.CELL_MAP: - raise ValueError(f'Cell {cell} not in CELL_MAP') - - ux = self.CELL_MAP[cell][0] - ix = self.CELL_MAP[cell][1] - 1 - sim_key = cell - # The simulator does not care about universes, but does care about UIDs. I'm manufacturing one by joining the - # Universe and fixture ID into the key. - self.dirty[sim_key] = color + # Enqueue a message to simulator, sets address + msg = f"{str(addr)} {','.join(map(str, color.rgb))}\n" + self.message_queue.put(msg) def go(self): - for num in self.dirty: - r = self.dirty[num].rgb[0] - g = self.dirty[num].rgb[1] - b = self.dirty[num].rgb[2] - msg = f'b {num} {r},{g},{b}\n' - + while not self.message_queue.empty(): + msg = self.message_queue.get() logger.debug(msg) self.sock.send(msg.encode()) - - self.dirty = {} From cd04eb04598c67d20609b2c6d50e9c8b8673cfe3 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Thu, 15 Aug 2019 20:19:49 -0700 Subject: [PATCH 44/60] Pass cell in addition to address to model --- grid/grid.py | 6 +----- model/base.py | 6 +++--- model/mirror.py | 5 ++--- model/sacn_model.py | 7 ++----- model/simulator.py | 10 +++------- 5 files changed, 11 insertions(+), 23 deletions(-) diff --git a/grid/grid.py b/grid/grid.py index 72cba3d..ec5bce7 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -22,11 +22,7 @@ class Pixel(NamedTuple): model: Type[ModelBase] def set(self, color: Color): - # For ease, we set by cell ID in the simulator - if isinstance(self.model, SimulatorModel): - self.model.set(self.cell.id, color) - else: - self.model.set(self.address, color) + self.model.set(self.cell, self.address, color) def __call__(self, color: Color): self.set(color) diff --git a/model/base.py b/model/base.py index c15b212..44225e3 100644 --- a/model/base.py +++ b/model/base.py @@ -1,14 +1,14 @@ from abc import ABC, abstractmethod from color import Color -from grid.cell import Address, universe_count, universe_size -from typing import List, Mapping, Union +from grid.cell import Address, universe_count, universe_size, Cell +from typing import List, Mapping class ModelBase(ABC): """Abstract base class for simulators.""" @abstractmethod - def set(self, addr: Union[Address, int], color: Color): + def set(self, cell: Cell, addr: Address, color: Color): """ Set one pixel to a particular color. diff --git a/model/mirror.py b/model/mirror.py index 7486309..1b6dddc 100644 --- a/model/mirror.py +++ b/model/mirror.py @@ -1,7 +1,6 @@ -from typing import Union from color import Color -from grid.cell import Address +from grid.cell import Address, Cell from .base import ModelBase @@ -16,7 +15,7 @@ def __init__(self, *models): def add_model(self, model): self.models.append(model) - def set(self, addr: Union[Address, int], color: Color): + def set(self, cell: Cell, addr: Address, color: Color): for m in self.models: m.set(addr, color) diff --git a/model/sacn_model.py b/model/sacn_model.py index 4432959..9a60e59 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -9,7 +9,7 @@ import sacn from color import Color -from grid.cell import Address +from grid.cell import Address, Cell from .base import ModelBase, map_leds logger = logging.getLogger("pyramidtriangles") @@ -32,10 +32,7 @@ def __init__(self, bind_address: str, row_count: int): def __del__(self): self.sender.stop() # If the object is destructing, close the sender connection - def set(self, addr: Union[Address, int], color: Color): - if not isinstance(addr, Address): - raise TypeError('Expected set() with DMX address') - + def set(self, cell: Cell, addr: Address, color: Color): try: channels = self.leds[addr.universe] except KeyError: diff --git a/model/simulator.py b/model/simulator.py index d345193..97cdb3d 100644 --- a/model/simulator.py +++ b/model/simulator.py @@ -4,10 +4,9 @@ import logging import queue import socket -from typing import Union from color import Color -from grid import Address +from grid import Address, Cell from .base import ModelBase SIM_DEFAULT = (188, 210, 229) # BCD2E5, "off" color for simulator @@ -28,12 +27,9 @@ def __init__(self, hostname: str, port: int): def __repr__(self): return f'{__class__.__name__} (hostname={self.hostname}, port={self.port})' - def set(self, addr: Union[Address, int], color: Color): - if not isinstance(addr, int): - raise TypeError('Cannot set DMX addresses in Simulator') - + def set(self, cell: Cell, addr: Address, color: Color): # Enqueue a message to simulator, sets address - msg = f"{str(addr)} {','.join(map(str, color.rgb))}\n" + msg = f"{str(cell.id)} {','.join(map(str, color.rgb))}\n" self.message_queue.put(msg) def go(self): From 56119bc6ca3e40d30daf0c06ffd7d5d4ed3c55e8 Mon Sep 17 00:00:00 2001 From: Lyra Naeseth Date: Thu, 15 Aug 2019 20:27:35 -0700 Subject: [PATCH 45/60] small fixes --- go_tri.py | 8 +++++--- grid/grid.py | 16 ++++++++++++++-- grid/select.py | 7 +++++-- model/sacn_model.py | 2 +- shows/__init__.py | 4 ++++ shows/index_debug.py | 29 +++++++++++++++++++++++++++++ shows/universe_debug.py | 22 ++++++++++++++++++++++ 7 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 shows/index_debug.py create mode 100644 shows/universe_debug.py diff --git a/go_tri.py b/go_tri.py index 75882f2..abfc8b8 100644 --- a/go_tri.py +++ b/go_tri.py @@ -342,15 +342,17 @@ def go_web(self): for _, interface, _ in gateways: for a in netifaces.ifaddresses(interface).get(netifaces.AF_INET, []): - if a['addr'].startswith('192.168'): - logger.info(f"Auto-detected local IP: {a['addr']}") + if a['addr'].startswith('192.168.0'): + logger.info( + f"Auto-detected Pyramid local IP: {a['addr']}") bind = a['addr'] break if bind: break if not bind: - parser.error('Failed to auto-detect local IP; use --bind') + parser.error( + 'Failed to auto-detect local IP. Are you on Pyramid Scheme wifi or ethernet?') logger.info("Starting sACN") model = sACN(bind, args.rows) diff --git a/grid/grid.py b/grid/grid.py index 2d185e6..a66f8d4 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -39,7 +39,10 @@ def __init__(self, model: Type[ModelBase], row_count: int = 11): self.row_count = row_count self._model = model - self._cells = list(generate(rows=row_count).values()) + + cells_by_id = {cell.id: cell + for cell in generate(rows=row_count).values()} + self._cells = [cells_by_id[i] for i in range(len(cells_by_id))] @property def cells(self) -> List[Cell]: @@ -84,7 +87,16 @@ def clear(self, color: Color = RGB(0, 0, 0)): def __getitem__(self, loc: Location) -> Cell: cell_id = loc if isinstance(loc, int) else loc.id - return self._cells[cell_id] + + try: + cell = self._cells[cell_id] + except IndexError: + raise KeyError(cell_id) + else: + if isinstance(loc, Position) and loc != cell.position: + logger.warn('got wrong cell: expected %r, got %r', + loc, cell.position) + return cell def __iter__(self): return (cell.position for cell in self._cells) diff --git a/grid/select.py b/grid/select.py index 8361ed5..a8617e5 100644 --- a/grid/select.py +++ b/grid/select.py @@ -29,6 +29,8 @@ def pointed(orientation: Orientation) -> Query: def query(grid: Grid) -> List[Cell]: return [cell for cell in grid.cells if cell.orientation is orientation] + return query + def pointed_up(grid: Grid) -> List[Cell]: return [cell for cell in grid.cells if cell.is_up] @@ -104,7 +106,7 @@ def query(grid: Grid) -> Neighbors: def hexagon(base_loc: Location) -> Query: def hexagon_query(grid: Grid) -> Sequence[Cell]: def neighbors(cell) -> Neighbors: - return Neighbors(query(grid, edge_neighbors(cell.id))) + return Neighbors(*query(grid, edge_neighbors(cell.id))) btm_cell = grid[base_loc] a, b, c, d, e, f = btm_cell, None, None, None, None, None @@ -135,6 +137,7 @@ def neighbors(cell) -> Neighbors: e = neighbors(d).left f = neighbors(e).middle - return (a, b, c, d, e, f) + hexa = list(filter(None, [a, b, c, d, e, f])) + return hexa return hexagon_query diff --git a/model/sacn_model.py b/model/sacn_model.py index 3f4985c..57176d1 100644 --- a/model/sacn_model.py +++ b/model/sacn_model.py @@ -39,7 +39,7 @@ def set(self, addr: Address, color: Color): raise IndexError( f'attempt to set channel in undefined universe {addr.universe}') - # our the Color tuples have their channels in the same order as sACN + # our Color tuples have their channels in the same order as sACN for i, c in enumerate(color.rgbw): try: channels[addr.offset + i] = c diff --git a/shows/__init__.py b/shows/__init__.py index bea2c2c..0eeca29 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -8,4 +8,8 @@ from .cycle_hsv import CycleHSV from .strobe import Strobe from .marching_hexes import MarchingHexes + +from .index_debug import IndexDebug +from .universe_debug import UniverseDebug + # from .warp import Warp # XXX: missing traversal.concentric() diff --git a/shows/index_debug.py b/shows/index_debug.py new file mode 100644 index 0000000..af7b95a --- /dev/null +++ b/shows/index_debug.py @@ -0,0 +1,29 @@ +from color import HSV as hsv +from grid import Grid, every +from .showbase import ShowBase + + +class IndexDebug(ShowBase): + def __init__(self, grid: Grid, frame_delay: float = 0.05): + self.grid = grid + self.frame_delay = frame_delay + self.n_cells = len(self.grid) + + def next_frame(self): + while True: + for cell in sorted(self.grid.cells): + universe = max(a.universe for a in cell.addresses) + hue = 1.0 - (cell.position.row / self.grid.row_count) * 0.9 + + self.grid.clear() + self.grid.set(cell.position, hsv(hue, 0.8, 0.9)) + self.grid.go() + yield self.frame_delay + + for cell in sorted(self.grid.cells): + universe = max(a.universe for a in cell.addresses) + hue = min(0.9, (universe - 1) * 0.1) + + self.grid.set(cell, hsv(hue, 0.8, 0.9)) + self.grid.go() + yield self.frame_delay diff --git a/shows/universe_debug.py b/shows/universe_debug.py new file mode 100644 index 0000000..4ca4720 --- /dev/null +++ b/shows/universe_debug.py @@ -0,0 +1,22 @@ +from color import HSV as hsv +from grid import Grid, every +from .showbase import ShowBase + + +class UniverseDebug(ShowBase): + def __init__(self, grid: Grid, frame_delay: float = 0.1): + self.grid = grid + self.frame_delay = frame_delay + self.n_cells = len(self.grid) + + def next_frame(self): + self.grid.clear() + + while True: + for cell in self.grid.cells: + universe = max(a.universe for a in cell.addresses) + hue = min(0.9, (universe - 1) * 0.1) + self.grid.set(cell, hsv(hue, 0.8, 0.9)) + + self.grid.go() + yield self.frame_delay From 57eeb2c01ee75f889267b309641cfa32916b3f79 Mon Sep 17 00:00:00 2001 From: John Major Date: Thu, 15 Aug 2019 23:57:32 -0700 Subject: [PATCH 46/60] ok, I can light pixels again --- shows/__init__.py | 1 + shows/top_down.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 shows/top_down.py diff --git a/shows/__init__.py b/shows/__init__.py index 0eeca29..3de47a8 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -8,6 +8,7 @@ from .cycle_hsv import CycleHSV from .strobe import Strobe from .marching_hexes import MarchingHexes +from .top_down import TopDown from .index_debug import IndexDebug from .universe_debug import UniverseDebug diff --git a/shows/top_down.py b/shows/top_down.py new file mode 100644 index 0000000..47d3452 --- /dev/null +++ b/shows/top_down.py @@ -0,0 +1,34 @@ +"""Simpe Demo Show. Move from Top of Triangle To Bottom, lighting each row at a time""" + +from .showbase import ShowBase +from color import HSV +from grid import hexagon, pointed_up +import random as rnd +import time +from grid.cell import Direction, Position, row_length + + +class TopDown(ShowBase): + def __init__(self, grid, frame_delay=0.1): + self.grid = grid + self.frame_delay = frame_delay + + self.n_cells = len(self.grid.cells) +# from IPython import embed; embed() + + def next_frame(self): + + self.grid.clear() + + while True: + hsv = HSV(1.0, 1, 1) + + for row in range (0,11): + for col in range (0, row_length(row+1)): ### row+1 b/c the row_length function expects 1 indexed row nums + self.grid.set(self.grid.select(Position(row=row, col=col)), hsv) + self.grid.go() + time.sleep(1.5) + + hsv.h -= 0.08 + + yield self.frame_delay From 633ee87ad2fc29883d3fa569a589d87d0eb5f6d9 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Fri, 16 Aug 2019 00:20:19 -0700 Subject: [PATCH 47/60] Adds a comment to hexagon() --- grid/select.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/grid/select.py b/grid/select.py index a8617e5..bf0a110 100644 --- a/grid/select.py +++ b/grid/select.py @@ -104,12 +104,30 @@ def query(grid: Grid) -> Neighbors: def hexagon(base_loc: Location) -> Query: + """ + Selector for hexagon pattern of cells surrounding a starting cell. + + Given a starting location, returns a function + func(Grid) -> [Neighbor, Neighbor, Neighbor, Neighbor, Neighbor, Neighbor] + + For example, starting with an up-facing cell, numbered with 1's here, there would be a total of 6 neighbors in the + hexagon (including cell 1). Here's an attempt at a drawing of neighbors 1, 2, 3, 4, 5, and 6: + + 3 444 5 + 333 4 555 + 222 1 666 + 2 111 6 + + There will be fewer neighbors if on an edge of the greater triangle. + """ def hexagon_query(grid: Grid) -> Sequence[Cell]: + # Helper function that returns edge neighbors (sharing a wall with) of a given cell. def neighbors(cell) -> Neighbors: return Neighbors(*query(grid, edge_neighbors(cell.id))) btm_cell = grid[base_loc] a, b, c, d, e, f = btm_cell, None, None, None, None, None + if a.is_left_edge: b = neighbors(a).right if b is not None: @@ -120,6 +138,7 @@ def neighbors(cell) -> Neighbors: e = neighbors(d).left if e is not None: f = neighbors(e).middle + elif a.is_right_edge: f = neighbors(a).left if f is not None: From 3d2149934a1a6376ea5d73a50e80d6dfd24e76c5 Mon Sep 17 00:00:00 2001 From: Lyra Naeseth Date: Fri, 16 Aug 2019 01:05:49 -0700 Subject: [PATCH 48/60] Geometry fixes - Add a Geometry type to encapsulate triangle row logic - Add methods to `Cell` for accessing neighboring positions - Add an `inset` selector to select inner triangles - Add a new `Stardate` show that uses it --- go_tri.py | 4 ++-- grid/__init__.py | 3 ++- grid/cell.py | 60 +++++++++++++++++++++++++++++++---------------- grid/geom.py | 23 ++++++++++++++++++ grid/grid.py | 19 +++++++++------ grid/select.py | 45 ++++++++++++++++++++++++++++++++++- shows/__init__.py | 7 +++--- shows/showbase.py | 2 +- shows/stargate.py | 37 +++++++++++++++++++++++++++++ 9 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 grid/geom.py create mode 100644 shows/stargate.py diff --git a/go_tri.py b/go_tri.py index 1066018..c33195b 100644 --- a/go_tri.py +++ b/go_tri.py @@ -7,7 +7,7 @@ import threading import cherrypy -from grid import Grid +from grid import Geometry, Grid from model import sACN, SimulatorModel import netifaces import osc_serve @@ -354,7 +354,7 @@ def go_web(self): logger.info("Starting sACN") model = sACN(bind, args.rows) - app = TriangleServer(Grid(model, args.rows), args) + app = TriangleServer(Grid(model, Geometry(args.rows)), args) try: app.start() # start related service threads diff --git a/grid/__init__.py b/grid/__init__.py index 9562882..8e13937 100644 --- a/grid/__init__.py +++ b/grid/__init__.py @@ -1,6 +1,7 @@ from .cell import Address, Cell, Direction, Position, Orientation +from .geom import Geometry from .grid import Grid, Location, Pixel, Query, Selector -from .select import (every, left_edge, right_edge, bottom_edge, pointed, +from .select import (every, on_edge, left_edge, right_edge, bottom_edge, inset, pointed, pointed_up, pointed_down, edge_neighbors, vertex_neighbors, hexagon) from .traversal import sweep, left_to_right, right_to_left diff --git a/grid/cell.py b/grid/cell.py index b309036..f3e7988 100644 --- a/grid/cell.py +++ b/grid/cell.py @@ -1,7 +1,9 @@ from enum import IntEnum from functools import lru_cache from itertools import chain -from typing import List, Mapping, NamedTuple +from typing import List, Mapping, Optional, NamedTuple + +from .geom import Geometry class Orientation(IntEnum): @@ -40,8 +42,7 @@ class Cell(NamedTuple): position: "Position" orientation: Orientation addresses: List["Address"] - - row_count = 11 + geom: Geometry def pixel_addresses(self, direction: Direction = Direction.LEFT_TO_RIGHT) -> List["Address"]: return (self.addresses @@ -64,6 +65,22 @@ def col(self) -> int: def id(self) -> int: return self.position.id + @property + def above(self) -> Optional["Position"]: + return self.position.adjust(row=-1, col=-1) if self.row > 0 else None + + @property + def below(self) -> Optional["Position"]: + return self.position.adjust(row=1, col=1) if self.row + 1 < self.geom.rows else None + + @property + def left(self) -> Optional["Position"]: + return self.position.adjust(col=-1) if self.col > 0 else None + + @property + def right(self) -> Optional["Position"]: + return self.position.adjust(col=1) if self.col + 1 < self.geom.row_length(self.row) else None + @property def is_up(self) -> bool: return self.orientation is Orientation.POINT_UP @@ -72,6 +89,10 @@ def is_up(self) -> bool: def is_down(self) -> bool: return self.orientation is Orientation.POINT_DOWN + @property + def is_edge(self) -> bool: + return self.is_left_edge or self.is_right_edge or self.is_bottom_edge + @property def is_left_edge(self) -> bool: """Returns True if cell is along the left edge of the greater triangle.""" @@ -80,13 +101,12 @@ def is_left_edge(self) -> bool: @property def is_right_edge(self) -> bool: """Returns True if cell is along the right edge of the greater triangle.""" - (row, column) = self.position - return column + 1 == row_length(row + 1) + return self.col + 1 == self.geom.row_length(self.row) @property def is_bottom_edge(self) -> bool: """Returns True if cell is along the bottom edge of the greater triangle.""" - return self.row + 1 == self.row_count and self.is_up + return self.row + 1 == self.geom.rows and self.is_up @property def is_top_corner(self) -> bool: @@ -103,6 +123,9 @@ def is_left_corner(self) -> bool: """Returns True if cell is the left corner of the greater triangle.""" return self.is_bottom_edge and self.is_left_edge + def __hash__(self): + return hash((type(self), self.position)) + def triangular_number(n: int) -> int: """Returns the number of elements in an equilateral triangle of n rows.""" @@ -137,7 +160,7 @@ def x(self) -> int: def y(self) -> int: return self.row - def adjust(self, row: int, col: int) -> "Position": + def adjust(self, row: int = 0, col: int = 0) -> "Position": return type(self)(self.row + row, self.col + col) @@ -188,10 +211,10 @@ def universe_size(universe_id: int) -> int: return 512 if universe_id % 3 != 0 else (44 * 4) -def generate(rows: int = 11, start: Address = Address(1, 4)) -> Mapping[Position, Cell]: +def generate(geom: Geometry, start: Address = Address(1, 4)) -> Mapping[Position, Cell]: cells = {} - for row in range(rows - 1, -1, -1): - row_mapping = mouth(row, start) + for row in range(geom.rows - 1, -1, -1): + row_mapping = mouth(geom, row, start) cells.update(row_mapping) end_address = max(chain.from_iterable( @@ -201,30 +224,30 @@ def generate(rows: int = 11, start: Address = Address(1, 4)) -> Mapping[Position return cells -def mouth(row: int, start: Address) -> Mapping[Position, Cell]: - up = up_teeth(row, start, row + 1) +def mouth(geom: Geometry, row: int, start: Address) -> Mapping[Position, Cell]: + up = up_teeth(geom, row, start, row + 1) last_up_address = up[max(up)].addresses[-1] first_after_gap = last_up_address.range(11)[-1] - down = down_teeth(row, first_after_gap, row) + down = down_teeth(geom, row, first_after_gap, row) return {**up, **down} -def up_teeth(row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, Cell]: +def up_teeth(geom: Geometry, row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, Cell]: cells = {} addr = start for i in range(length): pos = Position(row, i * 2) addrs = addr.range(pixels_per_cell) - cells[pos] = Cell(pos, Orientation.POINT_UP, addrs) + cells[pos] = Cell(pos, Orientation.POINT_UP, addrs, geom) addr = addrs[-1].next return cells -def down_teeth(row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, Cell]: +def down_teeth(geom: Geometry, row: int, start: Address, length: int, pixels_per_cell: int = 8) -> Mapping[Position, Cell]: cells = {} addr = start @@ -232,12 +255,9 @@ def down_teeth(row: int, start: Address, length: int, pixels_per_cell: int = 8) for _ in range(length): pos = Position(row, col) addrs = addr.range(pixels_per_cell) - cells[pos] = Cell(pos, Orientation.POINT_DOWN, addrs) + cells[pos] = Cell(pos, Orientation.POINT_DOWN, addrs, geom) col -= 2 addr = addrs[-1].next return cells - - -CELLS = generate() diff --git a/grid/geom.py b/grid/geom.py new file mode 100644 index 0000000..c55f11d --- /dev/null +++ b/grid/geom.py @@ -0,0 +1,23 @@ +from typing import NamedTuple + + +class Geometry(NamedTuple): + """ + Geometry represents the dimensions of a panel or side of the car. + """ + + rows: int + + def row_length(self, n: int) -> int: + if n < 0 or n >= self.rows: + raise IndexError(f'row {n} out of range ({self.rows} rows total)') + + return (n + 1) * 2 - 1 + + def midpoint(self, row: int) -> int: + length = self.row_length(row) + return int(length - (length / 2)) + + @property + def cell_count(self) -> int: + return sum(self.row_length(i) for i in range(self.rows)) diff --git a/grid/grid.py b/grid/grid.py index 72fdc75..64de9d2 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -4,6 +4,7 @@ from color import Color, RGB from model import ModelBase, SimulatorModel from .cell import generate, Address, Cell, Direction, Position +from .geom import Geometry logger = logging.getLogger('pyramidtriangles') @@ -29,21 +30,25 @@ def __call__(self, color: Color): class Grid(Mapping[Location, Cell]): - row_count: int + geom: Geometry _model: Type[ModelBase] _cells: List[Cell] - def __init__(self, model: Type[ModelBase], row_count: int = 11): - if row_count < 1: - raise ValueError(f'Grid(row_count={row_count}) is invalid') + def __init__(self, model: Type[ModelBase], geom: Geometry = Geometry(rows=11)): + if geom.rows < 1: + raise ValueError(f'Geometry(rows={geom.rows}) is invalid') - self.row_count = row_count + self.geom = geom self._model = model cells_by_id = {cell.id: cell - for cell in generate(rows=row_count).values()} + for cell in generate(geom).values()} self._cells = [cells_by_id[i] for i in range(len(cells_by_id))] + @property + def row_count(self) -> int: + return self.geom.rows + @property def cells(self) -> List[Cell]: return list(self._cells) @@ -53,7 +58,7 @@ def select(self, sel: Selector) -> Iterable[Cell]: cells = [self[sel]] elif isinstance(sel, Cell): cells = [sel] - elif isinstance(sel, Iterable): + elif isinstance(sel, Iterable) and not isinstance(self, str): cells = sel elif callable(sel): cells = sel(self) diff --git a/grid/select.py b/grid/select.py index a8617e5..a450e87 100644 --- a/grid/select.py +++ b/grid/select.py @@ -2,7 +2,7 @@ from typing import Iterable, List, NamedTuple, Sequence, Tuple from .cell import Cell, Orientation, row_length -from .grid import Grid, Query, Location +from .grid import Grid, Position, Query, Location def query(grid: Grid, q: Query) -> Iterable[Cell]: @@ -13,6 +13,10 @@ def every(grid: Grid) -> List[Cell]: return grid.cells +def on_edge(grid: Grid) -> List[Cell]: + return [cell for cell in grid.cells if cell.is_edge] + + def left_edge(grid: Grid) -> List[Cell]: return [cell for cell in grid.cells if cell.is_left_edge] @@ -25,6 +29,45 @@ def bottom_edge(grid: Grid) -> List[Cell]: return [cell for cell in grid.cells if cell.is_bottom_edge] +def inset(distance: int) -> Query: + """ + Selects an inner triangle, `distance` cells away from the edges. + """ + + def query(grid: Grid) -> List[Cell]: + # find the top point + top_row = distance * 2 + top_col = grid.geom.midpoint(top_row) + cells = {grid[Position(top_row, top_col)]} + edge_cells = set() + + bottom_row = grid.row_count - distance - 1 + for prev_row in range(top_row, bottom_row): + prev_cells = [c for c in cells if c.row == prev_row] + edge_cells = {min(prev_cells), max(prev_cells)} + midpoint = grid.geom.midpoint(prev_row) + + for cell in edge_cells: + below = cell.below + if below is None: + continue + + # TODO(lyra): grid[grid[]] ugh + if cell.col <= midpoint: + cells.add(grid[below]) + cells.add(grid[grid[below].left]) + if cell.col >= midpoint: + cells.add(grid[below]) + cells.add(grid[grid[below].right]) + + for col in range(min(edge_cells).col, max(edge_cells).col + 1): + cells.add(grid[Position(bottom_row, col)]) + + return list(cells) + + return query + + def pointed(orientation: Orientation) -> Query: def query(grid: Grid) -> List[Cell]: return [cell for cell in grid.cells if cell.orientation is orientation] diff --git a/shows/__init__.py b/shows/__init__.py index 0eeca29..b054c66 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -1,13 +1,14 @@ # These imports include submodules under the `shows` namespace (e.g. shows.UpDown is available). +from .cycle_hsv import CycleHSV from .left_to_right import LeftToRight from .left_to_right_and_back import LeftToRightAndBack +from .marching_hexes import MarchingHexes from .one_by_one import OneByOne from .random_cells import Random from .showbase import ShowBase, load_shows, random_shows -from .up_down import UpDown -from .cycle_hsv import CycleHSV +from .stargate import Stargate from .strobe import Strobe -from .marching_hexes import MarchingHexes +from .up_down import UpDown from .index_debug import IndexDebug from .universe_debug import UniverseDebug diff --git a/shows/showbase.py b/shows/showbase.py index 9d48ae8..3028c34 100644 --- a/shows/showbase.py +++ b/shows/showbase.py @@ -38,7 +38,7 @@ def random_shows(no_repeat: float = 1/3) -> Iterator[Tuple[str, Type[ShowBase]]] while True: show = choice(seq) - while show[0] in seen: + while show[0] in seen or 'Debug' in show[0]: show = choice(seq) seen.append(show[0]) diff --git a/shows/stargate.py b/shows/stargate.py new file mode 100644 index 0000000..b7c4595 --- /dev/null +++ b/shows/stargate.py @@ -0,0 +1,37 @@ +import random + +from color import Color, HSV +from grid import Grid, inset +from .showbase import ShowBase + + +class Stargate(ShowBase): + def __init__(self, grid: Grid, frame_delay: float = 0.25): + self.grid = grid + self.frame_delay = frame_delay + self.hue = 0 + + def generate_color(self) -> Color: + hue = self.hue + self.hue += random.uniform(0.1, 0.2) + if self.hue >= 1: + self.hue -= 1 + + return HSV(hue, random.uniform(0.7, 0.9), 0.7) + + def next_frame(self): + self.grid.clear() + yield self.frame_delay + + colors = [self.generate_color()] + + while True: + for distance, color in enumerate(colors): + self.grid.set(inset(distance), color) + + self.grid.go() + yield self.frame_delay + + colors.insert(0, self.generate_color()) + if len(colors) > 4: + colors.pop() From 76a01b7392796f5bdc575e81469c4afe83cf10a7 Mon Sep 17 00:00:00 2001 From: John Major Date: Fri, 16 Aug 2019 20:18:38 -0700 Subject: [PATCH 49/60] playing with shows --- shows/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shows/__init__.py b/shows/__init__.py index 3de47a8..b47e424 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -9,6 +9,7 @@ from .strobe import Strobe from .marching_hexes import MarchingHexes from .top_down import TopDown +from .two_hexes import TwoHexes from .index_debug import IndexDebug from .universe_debug import UniverseDebug From 37879cef00504d80b1a65c4f970e613faa60ec8c Mon Sep 17 00:00:00 2001 From: John Major Date: Sat, 17 Aug 2019 05:10:36 -0700 Subject: [PATCH 50/60] Repaired the LeftToRightAndBackAgain Show --- grid/cell.py | 2 +- shows/left_to_right_and_back.py | 50 ++++++++++++++++++++++++--------- shows/random_cells.py | 2 +- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/grid/cell.py b/grid/cell.py index b309036..196c48c 100644 --- a/grid/cell.py +++ b/grid/cell.py @@ -41,7 +41,7 @@ class Cell(NamedTuple): orientation: Orientation addresses: List["Address"] - row_count = 11 + row_count = 11 #why is this hard coded? (jem) def pixel_addresses(self, direction: Direction = Direction.LEFT_TO_RIGHT) -> List["Address"]: return (self.addresses diff --git a/shows/left_to_right_and_back.py b/shows/left_to_right_and_back.py index d483717..074da20 100644 --- a/shows/left_to_right_and_back.py +++ b/shows/left_to_right_and_back.py @@ -1,7 +1,9 @@ -from color import RGB +from color import HSV from .showbase import ShowBase from grid import Grid, Direction, sweep - +from grid.cell import Direction, Position, row_length +from grid import traversal +import time class LeftToRightAndBack(ShowBase): def __init__(self, grid: Grid, frame_delay: float = 1.0): @@ -9,19 +11,41 @@ def __init__(self, grid: Grid, frame_delay: float = 1.0): self.frame_delay = frame_delay def next_frame(self): - fwd = True + n_rows = self.grid.row_count + hsv = HSV(0.0,0.9,.5) - while True: - direction = Direction.LEFT_TO_RIGHT if fwd else Direction.RIGHT_TO_LEFT - sequence = sweep(direction, self.grid.row_count) + pix_arr = [] + a_ctr = 0 + for points in traversal.left_to_right(n_rows): + for (row, col) in points: + cell = self.grid.select(Position(row=row,col=col)) + b_ctr = 0 + for pixel_address in cell[0].pixel_addresses(): + if len(pix_arr) <= a_ctr+b_ctr: + pix_arr.append([]) + pix_arr[a_ctr+b_ctr].append(pixel_address) + b_ctr += 1 + a_ctr += 4 + self.grid.clear() - for points in sequence: - self.grid.clear() - for pos in points: - self.grid.set(pos, RGB(255, 255, 25)) - self.grid.go() - yield self.frame_delay + while True: - fwd = not fwd # Flips direction + for i in pix_arr: #yes, i + for ii in i: #yes, ii! + self.grid._model.set(ii, hsv) + self.grid.go() + hsv.h += .09 + if hsv.h >= 1.0: + hsv.h = 0.0 + time.sleep(0.8) + + for i in reversed(pix_arr): + for ii in reversed(i): + self.grid._model.set(ii, hsv) + self.grid.go() + hsv.h += .09 + if hsv.h >= 1.0: + hsv.h = 0.0 + time.sleep(0.8) diff --git a/shows/random_cells.py b/shows/random_cells.py index 99b0137..1298277 100644 --- a/shows/random_cells.py +++ b/shows/random_cells.py @@ -11,7 +11,7 @@ def __init__(self, grid, frame_delay=0.1): self.grid = grid self.frame_delay = frame_delay -# from IPython import embed; embed() + from IPython import embed; embed() def shuffle(self) -> Deque[Cell]: cells = self.grid.cells From 8d22aa0d800cf1bc15ba609ffccb324a3e8c178d Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Sat, 17 Aug 2019 16:13:13 -0700 Subject: [PATCH 51/60] Comment edits to incoporate jem's comments --- grid/select.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/grid/select.py b/grid/select.py index bf0a110..cbbf71f 100644 --- a/grid/select.py +++ b/grid/select.py @@ -109,16 +109,16 @@ def hexagon(base_loc: Location) -> Query: Given a starting location, returns a function func(Grid) -> [Neighbor, Neighbor, Neighbor, Neighbor, Neighbor, Neighbor] + The neighbors, surrounding cells, make a hexagon with the starting location cell as the base. For neighbor cells + off the grid, 'None' is returned. For example, starting with an up-facing cell, numbered with 1's here, there would be a total of 6 neighbors in the - hexagon (including cell 1). Here's an attempt at a drawing of neighbors 1, 2, 3, 4, 5, and 6: + hexagon (including cell 1), in a clock-wise order. Here's an attempt at a drawing of neighbors 1, 2, 3, 4, 5, and 6: - 3 444 5 - 333 4 555 - 222 1 666 - 2 111 6 - - There will be fewer neighbors if on an edge of the greater triangle. + 5 444 3 + 555 4 333 + 666 1 222 + 6 111 2 """ def hexagon_query(grid: Grid) -> Sequence[Cell]: # Helper function that returns edge neighbors (sharing a wall with) of a given cell. From 6f8bd4e0d5456b693fcc2b31185e07e3132bf39c Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Thu, 15 Aug 2019 23:25:44 -0700 Subject: [PATCH 52/60] Fixes grid tests (and some bugs) in the new paradigm --- grid/cell.py | 17 ++------ grid/geom.py | 6 +++ grid/grid.py | 5 ++- grid/select.py | 4 +- grid/traversal.py | 11 +++-- tests/test_cell.py | 38 ++++++++++++++++ tests/test_geom.py | 19 ++++++++ tests/test_grid.py | 106 ++++++++++++++++++--------------------------- 8 files changed, 122 insertions(+), 84 deletions(-) create mode 100644 tests/test_cell.py create mode 100644 tests/test_geom.py diff --git a/grid/cell.py b/grid/cell.py index f3e7988..cb5c306 100644 --- a/grid/cell.py +++ b/grid/cell.py @@ -28,11 +28,6 @@ def natural_for(self, orientation: Orientation) -> bool: return self is Direction.NATURAL or self == orientation -def row_length(n: int) -> int: - """Returns count of elements in nth row of equilateral triangle.""" - return n * 2 - 1 - - class Cell(NamedTuple): """ A Cell stores the properties of a "cell" (mini-triangle) within one of @@ -127,12 +122,6 @@ def __hash__(self): return hash((type(self), self.position)) -def triangular_number(n: int) -> int: - """Returns the number of elements in an equilateral triangle of n rows.""" - # Typically the triangle number is (n(n+1))/2 but our triangle has rows of 1, 3, 5... - return n ** 2 - - class Position(NamedTuple): row: int col: int @@ -141,16 +130,16 @@ class Position(NamedTuple): @lru_cache(maxsize=512) def from_id(cls, id: int) -> "Position": row_below = 1 - while triangular_number(row_below) <= id: + while Geometry.triangular_number(row_below) <= id: row_below += 1 row = row_below - 1 - col = id - triangular_number(row) + col = id - Geometry.triangular_number(row) return cls(row, col) @property def id(self) -> int: - return triangular_number(self.row) + self.col + return Geometry.triangular_number(self.row) + self.col @property def x(self) -> int: diff --git a/grid/geom.py b/grid/geom.py index c55f11d..aa17eeb 100644 --- a/grid/geom.py +++ b/grid/geom.py @@ -21,3 +21,9 @@ def midpoint(self, row: int) -> int: @property def cell_count(self) -> int: return sum(self.row_length(i) for i in range(self.rows)) + + @staticmethod + def triangular_number(n: int) -> int: + """Returns the number of elements in an equilateral triangle of n rows.""" + # Typically the triangle number is (n(n+1))/2 but our triangle has rows of 1, 3, 5... + return n ** 2 diff --git a/grid/grid.py b/grid/grid.py index 64de9d2..e5e6620 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -92,6 +92,8 @@ def clear(self, color: Color = RGB(0, 0, 0)): def __getitem__(self, loc: Location) -> Cell: cell_id = loc if isinstance(loc, int) else loc.id + if cell_id < 0: + raise KeyError(cell_id) try: cell = self._cells[cell_id] @@ -99,8 +101,9 @@ def __getitem__(self, loc: Location) -> Cell: raise KeyError(cell_id) else: if isinstance(loc, Position) and loc != cell.position: - logger.warn('got wrong cell: expected %r, got %r', + logger.warning('got wrong cell: expected %r, got %r', loc, cell.position) + raise KeyError(loc) return cell def __iter__(self): diff --git a/grid/select.py b/grid/select.py index 44fb093..3d0227e 100644 --- a/grid/select.py +++ b/grid/select.py @@ -1,7 +1,7 @@ -from typing import Iterable, List, NamedTuple, Sequence, Tuple +from typing import Iterable, List, NamedTuple, Sequence -from .cell import Cell, Orientation, row_length +from .cell import Cell, Orientation from .grid import Grid, Position, Query, Location diff --git a/grid/traversal.py b/grid/traversal.py index b237997..3a14bbc 100644 --- a/grid/traversal.py +++ b/grid/traversal.py @@ -1,7 +1,8 @@ -import enum + from typing import Iterator, Sequence -from .cell import Direction, Position, row_length +from .cell import Direction, Position +from .grid import Geometry def sweep(direction: Direction, row_count: int) -> Iterator[Sequence[Position]]: @@ -14,7 +15,9 @@ def sweep(direction: Direction, row_count: int) -> Iterator[Sequence[Position]]: if not row_count > 0: raise ValueError(f'traversal requires row_count({row_count}) > 0') - row_lengths = range(row_length(row_count)) + geom = Geometry(rows=row_count) + + row_lengths = range(geom.row_length(row_count - 1)) if direction == Direction.RIGHT_TO_LEFT: row_lengths = reversed(row_lengths) @@ -26,7 +29,7 @@ def sweep(direction: Direction, row_count: int) -> Iterator[Sequence[Position]]: curr_row = row_to_start + curr_column if not 0 <= curr_row < row_count: continue - if curr_column >= row_length(curr_row + 1): + if curr_column >= geom.row_length(curr_row): continue coordinates.append(Position(curr_row, curr_column)) diff --git a/tests/test_cell.py b/tests/test_cell.py new file mode 100644 index 0000000..14b2d3c --- /dev/null +++ b/tests/test_cell.py @@ -0,0 +1,38 @@ +from color import Color +from grid import Grid, Position, Geometry, Cell, Address +from model import ModelBase + + +class FakeModel(ModelBase): + def set(self, cell: Cell, addr: Address, color: Color): + pass + + def go(self): + pass + + +def test_coordinate_symmetry(): + for curr_id in range(256): + assert Position.from_id(curr_id).id == curr_id + + +def test_cell_attributes(): + triangle = Grid(model=FakeModel(), geom=Geometry(rows=3)) + assert len(triangle.cells) == 9 + + top = triangle[Position(0, 0)] + assert top.is_top_corner and top.is_left_edge and top.is_right_edge and top.is_up and top.is_edge + assert not top.is_bottom_edge + + left_corner = triangle[Position(2, 0)] + assert left_corner.is_left_corner and left_corner.is_left_edge and left_corner.is_bottom_edge and left_corner.is_up + assert not left_corner.is_right_edge + + right_corner = triangle[Position(2, 4)] + assert right_corner.is_right_corner and right_corner.is_right_edge and right_corner.is_bottom_edge + assert right_corner.is_up + assert not right_corner.is_left_edge + + inner = triangle[Position(1, 1)] + assert inner.is_down + assert not inner.is_left_edge and not inner.is_right_edge and not inner.is_bottom_edge and not inner.is_edge diff --git a/tests/test_geom.py b/tests/test_geom.py new file mode 100644 index 0000000..33a3c82 --- /dev/null +++ b/tests/test_geom.py @@ -0,0 +1,19 @@ +from grid import Geometry + + +def test_row_len(): + # (1, 1), (2, 3), (3, 5), (4, 7)... + for (row, length) in enumerate(range(1, 16, 2), start=1): + assert Geometry(rows=row).row_length(row - 1) == length + + +def test_triangle_number(): + for (n, number) in [ + (1, 1), + (2, 4), + (3, 9), + (4, 16), + (5, 25), + (6, 36) + ]: + assert Geometry.triangular_number(n) == number diff --git a/tests/test_grid.py b/tests/test_grid.py index 699bdc4..023f2d5 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -1,124 +1,104 @@ -from typing import Callable, Iterator +from pytest import raises from color import Color -import grid +from grid import ( + Position, Geometry, Grid, Cell, Address, bottom_edge, left_edge, right_edge, vertex_neighbors, edge_neighbors +) from model import ModelBase class FakeModel(ModelBase): - def set_pixels_by_cellid(self, cell_id) -> Iterator[Callable[[Color], None]]: + def set(self, cell: Cell, addr: Address, color: Color): pass def go(self): pass -def test_coordinate_conversion(): - for curr_id in range(256): - (row, column) = grid.TriangleGrid.id_to_coordinates(curr_id) - reverse_id = grid.TriangleGrid.coordinates_to_id(row, column) - assert reverse_id == curr_id - - -def test_builds_triangle(): +def test_triangle_counts(): for row_count in range(1, 15): - triangle = grid.make_triangle(FakeModel(), row_count) - - expected_cell_count = grid.triangular_number(row_count) + triangle = Grid(model=FakeModel(), geom=Geometry(rows=row_count)) assert triangle.row_count == row_count - assert triangle.size == len(triangle.cells) == expected_cell_count,\ + + expected_cell_count = Geometry.triangular_number(row_count) + assert len(triangle) == len(triangle.cells) == expected_cell_count,\ f'cell count {len(triangle.cells)} != expected {expected_cell_count} with rows {row_count}' # Each edge has the same number of elements are the number or total rows. - assert row_count ==\ - len(triangle.bottom_side_cells) == len(triangle.left_side_cells) == len(triangle.right_side_cells) + assert row_count == len(bottom_edge(triangle)) == len(left_edge(triangle)) == len(right_edge(triangle)) def test_cells_out_of_bounds(): - triangle = grid.make_triangle(FakeModel(), 2) + triangle = Grid(model=FakeModel(), geom=Geometry(rows=2)) + + with raises(KeyError): + triangle[Position(-1, 0)] + with raises(KeyError): + triangle[Position(0, -1)] + with raises(KeyError): + triangle[Position(0, 1)] # Only (0, 0) in first row. + with raises(KeyError): + triangle[Position(1, 3)] + with raises(KeyError): + triangle[Position(2, 0)] # Row 2 does not exist. - assert triangle.get_cell_by_coordinates(-1, 0) is None - assert triangle.get_cell_by_coordinates(0, -1) is None - assert triangle.get_cell_by_coordinates(0, 1) is None # Only (0, 0) in first row. - assert triangle.get_cell_by_coordinates(1, 3) is None - assert triangle.get_cell_by_coordinates(2, 0) is None # Row 2 does not exist. + with raises(KeyError): + triangle[-1] + with raises(KeyError): + triangle[len(triangle)] - assert triangle.get_cell_by_id(-1) is None - assert triangle.get_cell_by_id(triangle.size) is None + assert triangle[2] is not None - middle_cell = triangle.get_cell_by_id(2) - assert middle_cell is not None - for neighbor in triangle.vertex_neighbors_by_cell(2): - assert neighbor is None + assert not any(triangle.select(vertex_neighbors(2))) def test_cell_attributes(): - triangle = grid.make_triangle(FakeModel(), 3) + triangle = Grid(model=FakeModel(), geom=Geometry(rows=3)) assert len(triangle.cells) == 9 - top = triangle.get_cell_by_coordinates(0, 0) - assert top.is_top_corner and top.is_left_edge and top.is_right_edge and top.is_up + top = triangle[Position(0, 0)] + assert top.is_top_corner and top.is_left_edge and top.is_right_edge and top.is_up and top.is_edge assert not top.is_bottom_edge - left_corner = triangle.get_cell_by_coordinates(2, 0) + left_corner = triangle[Position(2, 0)] assert left_corner.is_left_corner and left_corner.is_left_edge and left_corner.is_bottom_edge and left_corner.is_up assert not left_corner.is_right_edge - right_corner = triangle.get_cell_by_coordinates(2, 4) + right_corner = triangle[Position(2, 4)] assert right_corner.is_right_corner and right_corner.is_right_edge and right_corner.is_bottom_edge assert right_corner.is_up assert not right_corner.is_left_edge - inner = triangle.get_cell_by_coordinates(1, 1) + inner = triangle[Position(1, 1)] assert inner.is_down - assert not inner.is_left_edge and not inner.is_right_edge and not inner.is_bottom_edge + assert not inner.is_left_edge and not inner.is_right_edge and not inner.is_bottom_edge and not inner.is_edge def test_cell_neighbors(): - triangle = grid.make_triangle(FakeModel(), 5) + triangle = Grid(model=FakeModel(), geom=Geometry(rows=5)) # Upward facing cell - (left, middle, right) = triangle.edge_neighbors_by_cell(6) + (left, middle, right) = triangle.select(edge_neighbors(6)) assert left.id == 5 assert middle.id == 12 assert right.id == 7 - (left, middle, right) = triangle.vertex_neighbors_by_cell(6) + (left, middle, right) = triangle.select(vertex_neighbors(6)) assert left.id == 10 assert middle.id == 2 assert right.id == 14 # Downward facing cell - (left, middle, right) = triangle.edge_neighbors_by_cell(5) + (left, middle, right) = triangle.select(edge_neighbors(5)) assert left.id == 4 assert middle.id == 1 assert right.id == 6 - (left, middle, right) = triangle.vertex_neighbors_by_cell(5) + (left, middle, right) = triangle.select(vertex_neighbors(5)) assert left is None assert middle.id == 11 assert right.id == 3 # Invalid cell - (left, middle, right) = triangle.edge_neighbors_by_coord(1, -1) - assert left is middle is right is None - (left, middle, right) = triangle.vertex_neighbors_by_coord(1, -1) - assert left is middle is right is None - - -def test_row_len(): - # (1, 1), (2, 3), (3, 5), (4, 7)... - for (row, length) in enumerate(range(1, 16, 2), start=1): - assert grid.row_length(row) == length - - -def test_triangle_number(): - for (n, number) in [ - (1, 1), - (2, 4), - (3, 9), - (4, 16), - (5, 25), - (6, 36) - ]: - assert grid.triangular_number(n) == number + assert not any(triangle.select(edge_neighbors(Position(1, -1)))) + assert not any(triangle.select(vertex_neighbors(Position(1, -1)))) From 56c4e48acceb6428551946821f447d98d38d2d91 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Fri, 16 Aug 2019 00:50:44 -0700 Subject: [PATCH 53/60] Restore warp show --- grid/grid.py | 2 +- grid/select.py | 8 +++++--- grid/traversal.py | 1 - shows/__init__.py | 3 +-- shows/warp.py | 18 +++++++++++------- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/grid/grid.py b/grid/grid.py index 64de9d2..b667c03 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -2,7 +2,7 @@ from typing import Callable, Iterator, Iterable, List, Mapping, NamedTuple, Union, Type from color import Color, RGB -from model import ModelBase, SimulatorModel +from model import ModelBase from .cell import generate, Address, Cell, Direction, Position from .geom import Geometry diff --git a/grid/select.py b/grid/select.py index 44fb093..7e88fa5 100644 --- a/grid/select.py +++ b/grid/select.py @@ -1,7 +1,6 @@ +from typing import Iterable, List, NamedTuple, Sequence -from typing import Iterable, List, NamedTuple, Sequence, Tuple - -from .cell import Cell, Orientation, row_length +from .cell import Cell, Orientation from .grid import Grid, Position, Query, Location @@ -60,6 +59,9 @@ def query(grid: Grid) -> List[Cell]: cells.add(grid[below]) cells.add(grid[grid[below].right]) + if not edge_cells: + return [] + for col in range(min(edge_cells).col, max(edge_cells).col + 1): cells.add(grid[Position(bottom_row, col)]) diff --git a/grid/traversal.py b/grid/traversal.py index b237997..7b6c33f 100644 --- a/grid/traversal.py +++ b/grid/traversal.py @@ -1,4 +1,3 @@ -import enum from typing import Iterator, Sequence from .cell import Direction, Position, row_length diff --git a/shows/__init__.py b/shows/__init__.py index b054c66..25269ec 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -9,8 +9,7 @@ from .stargate import Stargate from .strobe import Strobe from .up_down import UpDown +from .warp import Warp from .index_debug import IndexDebug from .universe_debug import UniverseDebug - -# from .warp import Warp # XXX: missing traversal.concentric() diff --git a/shows/warp.py b/shows/warp.py index 9ab1d57..c0eb898 100644 --- a/shows/warp.py +++ b/shows/warp.py @@ -1,6 +1,6 @@ -from color import HSV +from randomcolor import random_color from .showbase import ShowBase -from grid import Grid, concentric +from grid import Grid, inset class Warp(ShowBase): @@ -8,13 +8,17 @@ def __init__(self, grid: Grid, frame_delay: float = 0.2): self.grid = grid self.frame_delay = frame_delay + # Not sure of the proper formula for this, but allows running on normal or mega triangle. + self.max_distance = 0 + while grid.select(inset(self.max_distance)): + self.max_distance += 1 + def next_frame(self): + color = random_color(hue='purple') + while True: - for points in concentric(self.grid.row_count): + for distance in range(self.max_distance): self.grid.clear() - - for pos in points: - self.grid.set(pos, HSV(.1, .5, .9)) - + self.grid.set(inset(distance), color) self.grid.go() yield self.frame_delay From aaa71c6b9e650f1310df69b2c96f828087a86205 Mon Sep 17 00:00:00 2001 From: Justin Cummins Date: Sat, 17 Aug 2019 19:13:04 -0700 Subject: [PATCH 54/60] Adds Coordinate for alternate spatial reasoning --- grid/__init__.py | 2 +- grid/cell.py | 42 +++++++++++++++++++++++++++++++---------- grid/grid.py | 20 +++++++++++++++----- shows/marching_hexes.py | 2 +- tests/test_cell.py | 25 ++++++++++++++++++++++-- 5 files changed, 72 insertions(+), 19 deletions(-) diff --git a/grid/__init__.py b/grid/__init__.py index 8e13937..0d2fa61 100644 --- a/grid/__init__.py +++ b/grid/__init__.py @@ -1,5 +1,5 @@ -from .cell import Address, Cell, Direction, Position, Orientation +from .cell import Address, Cell, Direction, Position, Orientation, Coordinate from .geom import Geometry from .grid import Grid, Location, Pixel, Query, Selector from .select import (every, on_edge, left_edge, right_edge, bottom_edge, inset, pointed, diff --git a/grid/cell.py b/grid/cell.py index cb5c306..8d3b173 100644 --- a/grid/cell.py +++ b/grid/cell.py @@ -45,8 +45,8 @@ def pixel_addresses(self, direction: Direction = Direction.LEFT_TO_RIGHT) -> Lis else reversed(self.addresses)) @property - def coordinates(self) -> "Position": - return self.position + def coordinate(self) -> "Coordinate": + return Coordinate.from_pos(self.position, self.geom) @property def row(self) -> int: @@ -123,6 +123,11 @@ def __hash__(self): class Position(NamedTuple): + """ + Position is (row, column) where the top row is 0, and every row begins with column 0. + + Position(0, 0) is the apex of the triangle. + """ row: int col: int @@ -141,18 +146,35 @@ def from_id(cls, id: int) -> "Position": def id(self) -> int: return Geometry.triangular_number(self.row) + self.col - @property - def x(self) -> int: - return self.col - - @property - def y(self) -> int: - return self.row - def adjust(self, row: int = 0, col: int = 0) -> "Position": return type(self)(self.row + row, self.col + col) +class Coordinate(NamedTuple): + """ + Coordinate is (x, y) such that the left-most, bottom triangle is (0, 0). + + For Coordinate(x, y), the triangle above is Coordinate(x, y + 1), left is Coordinate(x - 1, y), below is + Coordinate(x, y - 1), and right is Coordinate(x + 1, y). + + Coordinate differs from `Position`. Position(0, 0) is the apex of the whole triangle, whereas Coordinate(0, 0) + refers to the left corner. + + Coordinates are a different way to spatially reason than Position. + """ + x: int + y: int + + @classmethod + def from_pos(cls, pos: Position, geom: Geometry) -> "Coordinate": + y = geom.rows - 1 - pos.row + x = pos.col + y + return cls(x, y) + + def pos(self, geom: Geometry): + return Position(geom.rows - 1 - self.y, self.x - self.y) + + class Address(NamedTuple): """ Address refers to a cell's DMX address within one of the diff --git a/grid/grid.py b/grid/grid.py index 7bfe449..8536073 100644 --- a/grid/grid.py +++ b/grid/grid.py @@ -3,12 +3,12 @@ from color import Color, RGB from model import ModelBase -from .cell import generate, Address, Cell, Direction, Position +from .cell import generate, Address, Cell, Direction, Position, Coordinate from .geom import Geometry logger = logging.getLogger('pyramidtriangles') -Location = Union[Position, int] +Location = Union[Coordinate, Position, int] Query = Callable[['Grid'], Iterable[Cell]] Selector = Union[Location, @@ -54,7 +54,7 @@ def cells(self) -> List[Cell]: return list(self._cells) def select(self, sel: Selector) -> Iterable[Cell]: - if isinstance(sel, (int, Position)): + if isinstance(sel, (int, Coordinate, Position)): cells = [self[sel]] elif isinstance(sel, Cell): cells = [sel] @@ -91,7 +91,13 @@ def clear(self, color: Color = RGB(0, 0, 0)): self.go() def __getitem__(self, loc: Location) -> Cell: - cell_id = loc if isinstance(loc, int) else loc.id + if isinstance(loc, Position): + cell_id = loc.id + elif isinstance(loc, Coordinate): + cell_id = loc.pos(self.geom).id + else: + cell_id = loc + if cell_id < 0: raise KeyError(cell_id) @@ -102,7 +108,11 @@ def __getitem__(self, loc: Location) -> Cell: else: if isinstance(loc, Position) and loc != cell.position: logger.warning('got wrong cell: expected %r, got %r', - loc, cell.position) + loc, cell.position) + raise KeyError(loc) + elif isinstance(loc, Coordinate) and loc != cell.coordinate: + logger.warning('got wrong cell: expected %r, got %r', + loc, cell.coordinate) raise KeyError(loc) return cell diff --git a/shows/marching_hexes.py b/shows/marching_hexes.py index 192646c..286dbca 100644 --- a/shows/marching_hexes.py +++ b/shows/marching_hexes.py @@ -19,7 +19,7 @@ def next_frame(self): self.grid.clear() for cell in self.grid.select(pointed_up): - self.grid.set(hexagon(cell.coordinates), hsv) + self.grid.set(hexagon(cell.position), hsv) self.grid.go() time.sleep(1) diff --git a/tests/test_cell.py b/tests/test_cell.py index 14b2d3c..f0934b2 100644 --- a/tests/test_cell.py +++ b/tests/test_cell.py @@ -1,5 +1,5 @@ from color import Color -from grid import Grid, Position, Geometry, Cell, Address +from grid import Grid, Position, Geometry, Cell, Address, Coordinate from model import ModelBase @@ -11,11 +11,32 @@ def go(self): pass -def test_coordinate_symmetry(): +def test_position_symmetry(): for curr_id in range(256): assert Position.from_id(curr_id).id == curr_id +def test_coordinate_symmetry(): + geom = Geometry(rows=3) + values = ( + ((0, 0), (2, 2)), + ((1, 0), (1, 1)), + ((1, 1), (2, 1)), + ((1, 2), (3, 1)), + ((2, 0), (0, 0)), + ((2, 1), (1, 0)), + ((2, 2), (2, 0)), + ((2, 3), (3, 0)), + ((2, 4), (4, 0)), + ) + + for ((row, col), (x, y)) in values: + pos = Position(row, col) + coord = Coordinate(x, y) + assert Coordinate.from_pos(pos, geom) == coord + assert coord.pos(geom) == pos + + def test_cell_attributes(): triangle = Grid(model=FakeModel(), geom=Geometry(rows=3)) assert len(triangle.cells) == 9 From 9b4a49066dfb3c513e439f3b63aac26eb7717f8a Mon Sep 17 00:00:00 2001 From: John Major Date: Sun, 18 Aug 2019 15:38:23 -0700 Subject: [PATCH 55/60] moving branches --- shows/__init__.py | 1 + util.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/shows/__init__.py b/shows/__init__.py index b47e424..aeccac0 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -10,6 +10,7 @@ from .marching_hexes import MarchingHexes from .top_down import TopDown from .two_hexes import TwoHexes +from .tendrils import Tendrils from .index_debug import IndexDebug from .universe_debug import UniverseDebug diff --git a/util.py b/util.py index 7018eaf..89006f1 100644 --- a/util.py +++ b/util.py @@ -9,3 +9,10 @@ def make_interpolater(in_min, in_max, out_min, out_max): scaleFactor = float(outSpan) / float(inSpan) return lambda value: out_min + (value - in_min) * scaleFactor + +from color import HSV +import random + +def choose_random_hsv(): + + return(HSV(random.uniform(0.0,1.0), random.uniform(0.0,1.0), random.uniform(0.0,1.0))) From 305aec7c6d28a9a68d96f05ddd78d233a00d0e5b Mon Sep 17 00:00:00 2001 From: John Major Date: Sun, 18 Aug 2019 18:26:38 -0700 Subject: [PATCH 56/60] working tendrils show --- morph.py => util/morph.py | 0 util/util.py | 6 ++++++ 2 files changed, 6 insertions(+) rename morph.py => util/morph.py (100%) create mode 100644 util/util.py diff --git a/morph.py b/util/morph.py similarity index 100% rename from morph.py rename to util/morph.py diff --git a/util/util.py b/util/util.py new file mode 100644 index 0000000..bb30081 --- /dev/null +++ b/util/util.py @@ -0,0 +1,6 @@ +from Color import HSV +import random + +def choose_random_hsv(): + + return(HSV(random.uniform(0.0,1.0), random.uniform(0.0,1.0), random.uniform(0.0,1.0))) From c990d0b8fcb32274ee0dbff626c75e71577b3104 Mon Sep 17 00:00:00 2001 From: John Major Date: Sun, 18 Aug 2019 19:32:07 -0700 Subject: [PATCH 57/60] row_length moved --- shows/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shows/__init__.py b/shows/__init__.py index 25269ec..886c50d 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -10,6 +10,7 @@ from .strobe import Strobe from .up_down import UpDown from .warp import Warp +from .tendrils import tendrils from .index_debug import IndexDebug from .universe_debug import UniverseDebug From 21671efd7afde816400eb605ed0551c9b7efc829 Mon Sep 17 00:00:00 2001 From: John Major Date: Sun, 18 Aug 2019 19:34:02 -0700 Subject: [PATCH 58/60] some util functions --- morph.py => util/morph.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename morph.py => util/morph.py (100%) diff --git a/morph.py b/util/morph.py similarity index 100% rename from morph.py rename to util/morph.py From b04686cbcf819cad5dce6444cd39f93951a9f706 Mon Sep 17 00:00:00 2001 From: John Major Date: Sun, 18 Aug 2019 19:41:24 -0700 Subject: [PATCH 59/60] what the fuck is going on.... --- go_tri.py | 4 ++-- shows/__init__.py | 2 +- util.py => util/util.py | 12 ++++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) rename util.py => util/util.py (53%) diff --git a/go_tri.py b/go_tri.py index c33195b..9472b2b 100644 --- a/go_tri.py +++ b/go_tri.py @@ -38,8 +38,8 @@ def speed_interpolation(val): return hi_interp(val) -low_interp = util.make_interpolater(0.0, 0.5, 2.0, 1.0) -hi_interp = util.make_interpolater(0.5, 1.0, 1.0, 0.5) +low_interp = util.util.make_interpolater(0.0, 0.5, 2.0, 1.0) +hi_interp = util.util.make_interpolater(0.5, 1.0, 1.0, 0.5) class ShowRunner(threading.Thread): diff --git a/shows/__init__.py b/shows/__init__.py index 886c50d..b31adbe 100644 --- a/shows/__init__.py +++ b/shows/__init__.py @@ -10,7 +10,7 @@ from .strobe import Strobe from .up_down import UpDown from .warp import Warp -from .tendrils import tendrils +from .tendrils import Tendrils from .index_debug import IndexDebug from .universe_debug import UniverseDebug diff --git a/util.py b/util/util.py similarity index 53% rename from util.py rename to util/util.py index 7018eaf..06f2165 100644 --- a/util.py +++ b/util/util.py @@ -1,11 +1,19 @@ +from color import HSV +import random + +def choose_random_hsv(): + return(HSV(random.uniform(0.0,1.0), random.uniform(0.0,1.0), random.uniform(0.0,1.0))) + + # http://stackoverflow.com/questions/1969240/mapping-a-range-of-values-to-another def make_interpolater(in_min, in_max, out_min, out_max): """Return a function that translates from one range to another""" - # Figure out how wide each range is + # Figure out how wide each range is inSpan = in_max - in_min outSpan = out_max - out_min - # Compute the scale factor between left and right values + # Compute the scale factor between left and right values scaleFactor = float(outSpan) / float(inSpan) return lambda value: out_min + (value - in_min) * scaleFactor + From 3672fca1016fe9229a9023bcc781296185cbf7bf Mon Sep 17 00:00:00 2001 From: John Major Date: Sun, 18 Aug 2019 19:51:22 -0700 Subject: [PATCH 60/60] left out crap --- go_tri.py | 4 ++-- util.py | 18 ------------------ util/util.py | 22 +++++++++++++++++++++- 3 files changed, 23 insertions(+), 21 deletions(-) delete mode 100644 util.py diff --git a/go_tri.py b/go_tri.py index abfc8b8..bdc2f3d 100644 --- a/go_tri.py +++ b/go_tri.py @@ -38,8 +38,8 @@ def speed_interpolation(val): return hi_interp(val) -low_interp = util.make_interpolater(0.0, 0.5, 2.0, 1.0) -hi_interp = util.make_interpolater(0.5, 1.0, 1.0, 0.5) +low_interp = util.util.make_interpolater(0.0, 0.5, 2.0, 1.0) +hi_interp = util.util.make_interpolater(0.5, 1.0, 1.0, 0.5) class ShowRunner(threading.Thread): diff --git a/util.py b/util.py deleted file mode 100644 index 89006f1..0000000 --- a/util.py +++ /dev/null @@ -1,18 +0,0 @@ -# http://stackoverflow.com/questions/1969240/mapping-a-range-of-values-to-another -def make_interpolater(in_min, in_max, out_min, out_max): - """Return a function that translates from one range to another""" - # Figure out how wide each range is - inSpan = in_max - in_min - outSpan = out_max - out_min - - # Compute the scale factor between left and right values - scaleFactor = float(outSpan) / float(inSpan) - - return lambda value: out_min + (value - in_min) * scaleFactor - -from color import HSV -import random - -def choose_random_hsv(): - - return(HSV(random.uniform(0.0,1.0), random.uniform(0.0,1.0), random.uniform(0.0,1.0))) diff --git a/util/util.py b/util/util.py index bb30081..9c2cb45 100644 --- a/util/util.py +++ b/util/util.py @@ -1,6 +1,26 @@ -from Color import HSV +from color import HSV import random def choose_random_hsv(): return(HSV(random.uniform(0.0,1.0), random.uniform(0.0,1.0), random.uniform(0.0,1.0))) + + +# http://stackoverflow.com/questions/1969240/mapping-a-range-of-values-to-another +def make_interpolater(in_min, in_max, out_min, out_max): + """Return a function that translates from one range to another""" + # Figure out how wide each range is + inSpan = in_max - in_min + outSpan = out_max - out_min + + # Compute the scale factor between left and right values + scaleFactor = float(outSpan) / float(inSpan) + + return lambda value: out_min + (value - in_min) * scaleFactor + +from color import HSV +import random + +def choose_random_hsv(): + + return(HSV(random.uniform(0.0,1.0), random.uniform(0.0,1.0), random.uniform(0.0,1.0)))