diff --git a/data/BAAAHS.scene b/data/BAAAHS.scene index 0069853656..81c87016c6 100644 --- a/data/BAAAHS.scene +++ b/data/BAAAHS.scene @@ -1 +1,239 @@ -{"model":{"title":"BAAAHS","entities":[{"type":"Import","title":"BAAAHS","objData":"# OBJ model file (sort of)\n# Exported from SketchUp with BAAAHS::Geometry and massaged by @tgvarik\n# File units = inches\n\nv -20.536 90.552 -71.329\nv -60.471 118.515 -106.139\nv -38.673 161.32 -82.139\nv -20.536 90.552 72.052\nv -38.673 161.32 82.861\nv -60.471 118.515 106.861\nv -57.929 75.089 -106.139\nv -57.929 75.089 106.861\nv -54.59 17.938 -82.139\nv -54.59 17.938 82.861\nv -79.542 190.665 -58.139\nv -107.735 184.672 -70.139\nv -79.542 190.665 58.861\nv -107.735 184.672 70.861\nv -97.991 127.29 -106.139\nv -97.991 127.29 106.861\nv -130.695 94.586 -106.139\nv -130.695 94.586 106.861\nv -77.314 25.919 -94.139\nv -77.314 25.919 94.861\nv -108.212 22.449 -82.139\nv -108.212 22.449 82.861\nv -63.915 1.188 -74.139\nv -96.665 1.188 -74.139\nv -96.665 1.188 74.861\nv -63.915 1.188 74.861\nv -130.907 162.735 -94.139\nv -144.1 176.943 -70.139\nv -130.907 162.735 94.861\nv -144.1 176.943 70.861\nv -167.131 12.657 -74.139\nv -167.131 12.657 74.861\nv -177.484 111.006 -106.139\nv -179.55 137.571 -94.139\nv -179.55 137.571 94.861\nv -177.484 111.006 106.861\nv -211.423 68.356 -106.139\nv -211.423 68.356 106.861\nv -153.052 167.001 76.922\nv -267.065 189.739 78.928\nv -271.6 144.033 82.861\nv -271.6 144.033 -82.139\nv -252.672 120.23 -89.688\nv -232.54 94.912 -97.717\nv -252.672 120.23 90.41\nv -232.54 94.912 98.439\nv -237.042 27.111 -70.139\nv -237.042 27.111 70.861\nv -350.686 196.403 -46.139\nv -279.94 189.739 -70.139\nv -336.282 157.617 -90.139\nv -311.668 191.493 77.517\nv -350.686 196.403 46.861\nv -336.282 157.617 90.861\nv -276.091 168.647 -74.139\nv -321.878 118.831 -94.139\nv -321.878 118.831 94.861\nv -313.542 82.725 -82.139\nv -313.542 82.725 82.861\nv -304.721 41.104 -82.139\nv -304.721 41.104 82.861\nv -386.74 137.917 -82.139\nv -386.74 137.917 82.861\nv -341.744 119.776 -106.139\nv -341.744 119.776 106.861\nv -321.914 29.939 -82.139\nv -321.914 29.939 82.861\nv -362.395 120.552 -106.139\nv -362.395 120.552 106.861\nv -330.551 15.163 -70.139\nv -357.679 26.124 -82.139\nv -357.679 26.124 82.861\nv -330.551 15.163 70.861\nv -337.673 1.188 -66.139\nv -385.253 3.731 -66.139\nv -391.397 17.717 -70.139\nv -337.673 1.188 66.861\nv -385.253 3.731 66.861\nv -391.397 17.717 70.861\nv -411.149 155.054 -54.139\nv -411.149 155.054 54.861\nv -402.339 93.794 -64.139\nv -402.339 93.794 64.861\nv -102 194.61 -46.799\nv -102 194.61 47.522\nv -380.917 175.728 -3.639\nv -396.033 165.391 -28.889\nv -385.697 172.459 5.611\nv -398.423 163.757 30.236\nv -415.748 129.593 -46.514\nv -415.748 129.593 47.236\nv -402.189 65.133 -32.11\nv -397.816 136.359 -29.822\nv -399.245 142.234 30.803\nv -399.569 133.761 31.026\nv -402.189 65.133 32.833\nv -399.453 109.694 1.218\nv -388.07 165.391 -1.327\nv -82.8 216.565 -46.499\nv -68.4 202.165 -53.7\nv -39.6 216.565 -46.499\nv -46.782 201.518 -53.7\nv -32.4 216.565 -53.7\nv -32.4 216.565 -96.899\nv -46.8 202.165 -82.499\nv -68.4 202.165 -82.499\nv -82.8 216.565 -96.899\nv -46.8 223.765 -53.7\nv -75.573 223.765 -53.7\nv -75.573 223.765 -89.7\nv -46.8 223.765 -89.7\nv -32.4 216.565 97.275\nv -46.782 201.518 54.075\nv -32.4 216.565 54.075\nv -39.6 216.565 46.875\nv -68.4 202.165 54.075\nv -82.8 216.565 46.875\nv -68.4 202.165 82.875\nv -82.8 216.565 97.275\nv -46.8 202.165 82.875\nv -46.8 223.765 54.075\nv -75.573 223.765 54.075\nv -75.573 223.765 90.075\nv -46.8 223.765 90.075\nv -82.907 238.165 -25.2\nv -90 194.61 -46.799\nv -111.6 202.165 -25.2\nv -82.907 238.165 25.2\nv -111.6 202.165 25.2\nv -90 194.61 46.8\nv -77.069 210.834 -46.799\nv -77.15 211.214 -46.499\nv -79.872 231.865 31.5\nv -72.502 216.565 46.8\nv -25.307 238.165 -25.2\nv -32.4 216.565 -46.799\nv -25.307 238.165 25.2\nv -32.4 216.565 46.8\nv -40.197 215.565 -46.799\nv -32.4 158.965 -46.799\nv -32.4 158.965 46.8\nv -24.995 219.033 -36.925\nv -16.066 222.01 -25.021\nv -10.8 223.765 -18\nv -24.995 219.033 36.926\nv -16.066 222.01 25.022\nv -10.8 223.765 18\nv -97.254 220.165 0\nv -26.664 186.774 -39.149\nv -24.169 183.658 -35.825\nv -26.662 216.81 -39.149\nv -26.997 186.326 39.598\nv -24.169 183.658 35.826\nv -26.997 216.361 39.598\nv -10.8 216.361 10.798\nv -10.8 216.361 -10.797\nv -10.8 166.374 -22.099\nv -10.8 122.965 -25.2\nv -12.987 171.504 -24.18\nv -20.966 181.245 -32.412\nv -10.8 166.374 22.1\nv -10.8 122.965 25.2\nv -12.987 171.504 24.181\nv -20.966 181.245 32.413\nv -10.8 150.392 -10.797\nv -10.8 150.392 10.798\nv -19.748 96.708 -36.997\nv -28.695 70.451 -48.794\nv -19.748 96.708 37.359\nv -28.695 70.451 49.517\nv -54.59 17.938 -60.764\nv -47.029 33.271 57.991\nv -54.59 17.938 61.486\nv -63.915 1.188 -57.139\nv -63.915 1.188 57.861\nv -15.274 109.836 6.08\nv -21.985 90.144 -21.357\nv -47.029 33.271 -21.29\nv -47.029 33.271 -57.269\nv -47.029 33.271 25.638\nv -33.063 61.593 -42.241\nv -42.259 42.943 5.703\nv -36.258 55.113 39.666\nv -4.77 216.361 10.798\nv -10.743 223.561 17.998\nv -4.77 216.361 -10.797\nv -10.762 223.539 -17.975\nv -19.17 223.561 -32.397\nv -10.909 223.561 -18.184\nv -19.28 218.749 -37.21\nv -12.745 215.389 -25.519\nv -14.34 214.945 -28.487\nv -15.793 213.643 -31.27\nv -16.396 212.612 -32.47\nv -17.1 211.575 -33.628\nv -18.178 208.88 -35.398\nv -18.855 205.74 -36.511\nv -19.086 202.371 -36.891\nv -19.28 188.714 -37.21\nv -18.855 199.001 -36.511\nv -18.178 195.862 -35.398\nv -17.1 193.167 -33.628\nv -15.697 191.1 -31.322\nv -14.063 189.803 -28.637\nv -12.311 189.362 -25.758\nv -11.97 171.926 -25.197\nv -11.97 189.449 -25.197\nv 1.195 158.761 -25.197\nv -10.692 189.553 -25.197\nv -8.747 189.95 -25.197\nv -6.825 190.595 -25.197\nv -5.079 191.706 -25.197\nv -3.427 193.045 -25.197\nv -2.851 193.631 -25.197\nv -2.659 193.886 -25.197\nv 16.83 174.397 -25.197\nv -1.751 195.334 -23.531\nv -1.162 197.249 -21.761\nv -1.086 198.339 -20.936\nv -1.059 199.577 -20.042\nv 31.23 180.361 -10.797\nv -1.495 201.947 -18.674\nv -1.598 202.371 -18.447\nv -2.637 204.743 -17.505\nv -3.059 205.57 -17.219\nv -4.193 207.142 -16.908\nv -5.574 208.832 -16.69\nv -6.033 209.253 -16.717\nv -7.853 210.725 -16.963\nv -8.196 211.574 -17.517\nv -9.564 213.644 -19.841\nv -11.116 214.945 -22.57\nv 31.23 180.361 10.798\nv -4.558 205.896 18.049\nv -6.345 208.02 17.81\nv -7.221 208.94 17.779\nv -8.345 209.824 17.949\nv -8.971 211.646 19.2\nv -10.176 213.715 21.612\nv -11.581 215.016 24.42\nv -13.088 215.46 27.435\nv -19.17 216.361 39.598\nv -14.595 215.016 30.449\nv -15.999 213.715 33.257\nv -17.205 211.646 35.669\nv -18.13 208.95 37.52\nv -18.712 205.81 38.683\nv -18.911 202.44 39.08\nv -19.17 186.326 39.598\nv -18.712 199.07 38.683\nv -18.13 195.93 37.52\nv -17.205 193.233 35.669\nv -15.999 191.164 33.257\nv -14.595 189.863 30.449\nv -13.088 189.42 27.435\nv -11.97 189.749 25.198\nv -11.97 171.926 25.198\nv -23.508 223.561 32.398\nv -19.17 223.561 32.398\nv 1.195 158.761 25.198\nv -4.77 144.361 10.798\nv -4.77 144.361 -10.797\nv -23.508 223.561 -32.397\nv 24.03 144.361 -10.797\nv 16.83 158.761 -25.197\nv 31.204 151.535 -10.797\nv 31.23 151.561 -3.597\nv 31.204 151.535 10.798\nv 16.83 158.761 25.198\nv 16.83 174.397 25.198\nv -2.022 199.516 20.767\nv -2.148 200.137 20.417\nv -2.75 202.44 19.214\nv -3.134 203.281 18.891\nv -4.427 205.719 18.081\nv -2.026 197.045 22.518\nv -2.551 194.972 24.354\nv -2.996 194.223 25.198\nv -4.539 192.572 25.198\nv -4.672 192.457 25.198\nv -6.288 191.432 25.198\nv -8.133 190.545 25.198\nv -10.045 189.897 25.198\nv 24.03 144.361 10.798\nv -383.415 173.341 -13.553\nv -409.199 186.189 -13.534\nv -416.097 205.35 -27.934\nv -383.14 173.927 8.066\nv -409.199 186.189 8.066\nv -370.494 183.891 22.466\nv -416.097 205.35 22.466\nv -377.008 186.956 -27.934\nv -406.517 208.799 15.238\nv -362.53 188.101 15.238\nv -406.517 208.799 -17.659\nv -362.53 188.101 -17.659\no 1D\nf 1 2 3\nl 1 2\nl 2 3\nl 1 3\no 1P\nf 4 5 6\nl 4 5\nl 5 6\nl 4 6\no 2D\nf 1 7 2\nl 1 7\nl 7 2\nl 1 2\no 2P\nf 4 6 8\nl 4 6\nl 6 8\nl 4 8\no 3D\nf 1 9 7\nl 1 9\nl 9 7\nl 1 7\no 3P\nf 4 8 10\nl 4 8\nl 8 10\nl 4 10\no 4D\nf 11 3 12\nl 11 3\nl 3 12\nl 11 12\no 4P\nf 5 13 14\nl 5 13\nl 13 14\nl 5 14\no 5D\nf 3 2 12\nl 3 2\nl 2 12\nl 3 12\no 5P\nf 5 14 6\nl 5 14\nl 14 6\nl 5 6\no 6D\nf 2 15 12\nl 2 15\nl 15 12\nl 2 12\no 6P\nf 6 14 16\nl 6 14\nl 14 16\nl 6 16\no 7D\nf 15 7 17\nf 7 15 2\nl 7 17\nl 15 17\nl 15 2\nl 7 2\no 7P\nf 8 16 18\nf 16 8 6\nl 16 18\nl 8 18\nl 8 6\nl 16 6\no 8D\nf 9 19 7\nf 7 19 17\nl 9 19\nl 9 7\nl 19 17\nl 7 17\no 8P\nf 8 18 20\nf 10 8 20\nl 8 18\nl 18 20\nl 8 10\nl 20 10\no 9D\nf 19 21 17\nl 19 21\nl 21 17\nl 19 17\no 9P\nf 20 18 22\nl 20 18\nl 18 22\nl 20 22\no 10D\nf 9 23 19\nf 23 24 19\nf 19 24 21\nl 9 23\nl 9 19\nl 23 24\nl 24 21\nl 19 21\no 10P\nf 20 22 25\nf 26 20 25\nf 10 20 26\nl 20 22\nl 22 25\nl 25 26\nl 20 10\nl 26 10\no 11D\nf 12 27 28\nl 12 27\nl 27 28\nl 12 28\no 11P\nf 29 14 30\nl 29 14\nl 14 30\nl 29 30\no 12D\nf 15 27 12\nl 15 27\nl 27 12\nl 15 12\no 12P\nf 16 14 29\nl 16 14\nl 14 29\nl 16 29\no 13D\nf 15 17 27\nl 15 17\nl 17 27\nl 15 27\no 13P\nf 16 29 18\nl 16 29\nl 29 18\nl 16 18\no 14D\nf 21 31 17\nl 21 31\nl 31 17\nl 21 17\no 14P\nf 22 18 32\nl 22 18\nl 18 32\nl 22 32\no 15D\nf 27 33 34\nf 34 28 27\nl 27 33\nl 33 34\nl 34 28\nl 27 28\no 15P\nf 35 29 30\nf 29 35 36\nl 29 30\nl 35 30\nl 35 36\nl 29 36\no 16D\nf 17 33 27\nl 17 33\nl 33 27\nl 17 27\no 16P\nf 18 29 36\nl 18 29\nl 29 36\nl 18 36\no 17D\nf 17 37 33\nl 17 37\nl 37 33\nl 17 33\no 17P\nf 18 36 38\nl 18 36\nl 36 38\nl 18 38\no 18D\nf 17 31 37\nl 17 31\nl 31 37\nl 17 37\no 18P\nf 18 38 32\nl 18 38\nl 38 32\nl 18 32\no 19AP\nf 39 40 41\nl 39 40\nl 40 41\nl 39 41\no 19BP\nf 39 41 35\nl 39 41\nl 41 35\nl 39 35\no 20D\nf 33 42 34\nl 33 42\nl 42 34\nl 33 34\no 20P\nf 36 35 41\nl 36 35\nl 35 41\nl 36 41\no 21D\nf 33 43 42\nf 43 33 44\nf 44 33 37\nl 43 42\nl 33 42\nl 43 44\nl 33 37\nl 44 37\no 21P\nf 45 36 41\nf 36 45 46\nf 36 46 38\nl 36 41\nl 45 41\nl 45 46\nl 46 38\nl 36 38\no 22D\nf 37 47 44\nl 37 47\nl 47 44\nl 37 44\no 22P\nf 38 46 48\nl 38 46\nl 46 48\nl 38 48\no 23D\nf 31 47 37\nl 31 47\nl 47 37\nl 31 37\no 23P\nf 32 38 48\nl 32 38\nl 38 48\nl 32 48\no 24D\nf 49 50 51\nl 49 50\nl 50 51\nl 49 51\no 24P\nf 52 53 54\nl 52 53\nl 53 54\nl 52 54\no 25D\nf 55 51 50\nl 55 51\nl 51 50\nl 55 50\no 25P\nf 40 52 54\nl 40 52\nl 52 54\nl 40 54\no 26D\nf 42 56 55\nf 55 56 51\nl 42 56\nl 42 55\nl 56 51\nl 55 51\no 26P\nf 41 54 57\nf 40 54 41\nl 54 57\nl 41 57\nl 54 40\nl 41 40\no 27D\nf 42 58 56\nl 42 58\nl 58 56\nl 42 56\no 27P\nf 41 57 59\nl 41 57\nl 57 59\nl 41 59\no 28D\nf 43 58 42\nl 43 58\nl 58 42\nl 43 42\no 28P\nf 45 41 59\nl 45 41\nl 41 59\nl 45 59\no 29D\nf 44 58 43\nl 44 58\nl 58 43\nl 44 43\no 29P\nf 46 45 59\nl 46 45\nl 45 59\nl 46 59\no 30D\nf 44 60 58\nl 44 60\nl 60 58\nl 44 58\no 30P\nf 46 59 61\nl 46 59\nl 59 61\nl 46 61\no 31D\nf 44 47 60\nl 44 47\nl 47 60\nl 44 60\no 31P\nf 46 61 48\nl 46 61\nl 61 48\nl 46 48\no 32D\nf 51 62 49\nl 51 62\nl 62 49\nl 51 49\no 32P\nf 54 53 63\nl 54 53\nl 53 63\nl 54 63\no 33D\nf 56 64 51\nl 56 64\nl 64 51\nl 56 51\no 33P\nf 57 54 65\nl 57 54\nl 54 65\nl 57 65\no 34D\nf 60 66 58\nf 58 66 64\nf 58 64 56\nl 60 66\nl 60 58\nl 66 64\nl 64 56\nl 58 56\no 34P\nf 61 59 67\nf 59 57 67\nf 57 65 67\nl 61 59\nl 61 67\nl 59 57\nl 57 65\nl 67 65\no 35D\nf 51 68 62\nl 51 68\nl 68 62\nl 51 62\no 35P\nf 54 63 69\nl 54 63\nl 63 69\nl 54 69\no 36D\nf 51 64 68\nl 51 64\nl 64 68\nl 51 68\no 36P\nf 54 69 65\nl 54 69\nl 69 65\nl 54 65\no 37D\nf 70 71 68\nf 66 70 68\nf 66 68 64\nl 70 71\nl 71 68\nl 70 66\nl 68 64\nl 66 64\no 37P\nf 65 69 72\nf 65 72 73\nf 65 73 67\nl 65 69\nl 69 72\nl 72 73\nl 73 67\nl 65 67\no 38D\nf 74 75 71\nf 71 75 76\nf 70 74 71\nl 74 75\nl 75 76\nl 71 76\nl 74 70\nl 71 70\no 38P\nf 73 72 77\nf 77 72 78\nf 72 79 78\nl 73 72\nl 73 77\nl 77 78\nl 72 79\nl 78 79\no 39D\nf 49 62 80\nl 49 62\nl 62 80\nl 49 80\no 39P\nf 53 81 63\nl 53 81\nl 81 63\nl 53 63\no 40D\nf 80 62 82\nl 80 62\nl 62 82\nl 80 82\no 40P\nf 81 83 63\nl 81 83\nl 83 63\nl 81 63\no 41D\nf 62 68 82\nl 62 68\nl 68 82\nl 62 82\no 41P\nf 63 83 69\nl 63 83\nl 83 69\nl 63 69\no 42D\nf 68 76 82\nl 68 76\nl 76 82\nl 68 82\no 42P\nf 69 83 79\nl 69 83\nl 83 79\nl 69 79\no 43D\nf 71 76 68\nl 71 76\nl 76 68\nl 71 68\no 43P\nf 72 69 79\nl 72 69\nl 69 79\nl 72 79\n# Panel doesn't exist.\n#o 44D\n#f 84 11 12\n#l 84 11\n#l 11 12\n#l 84 12\n# Panel doesn't exist.\n#o 44P\n#f 13 85 14\n#l 13 85\n#l 85 14\n#l 13 14\n# Panel doesn't exist.\n#o 45D\n#f 84 12 28\n#l 84 12\n#l 12 28\n#l 84 28\n# Panel doesn't exist.\n#o 45P\n#f 14 85 30\n#l 14 85\n#l 85 30\n#l 14 30\no R1\nf 53 49 86\nl 53 49\nl 49 86\nl 53 86\no R2D\nf 87 49 80\nf 49 87 86\nl 49 80\nl 87 80\nl 87 86\nl 49 86\no R2P\nf 88 53 86\nf 53 88 89\nf 53 89 81\nl 53 86\nl 88 86\nl 88 89\nl 89 81\nl 53 81\no R3D\nf 87 80 90\nl 87 80\nl 80 90\nl 87 90\no R3P\nf 81 89 91\nl 81 89\nl 89 91\nl 81 91\no R4D\nf 92 87 90\nf 87 92 93\nl 87 90\nl 92 90\nl 92 93\nl 87 93\no R4P\nf 91 89 94\nf 91 94 95\nf 91 95 96\nl 91 89\nl 89 94\nl 94 95\nl 95 96\nl 91 96\n# Panel doesn't exist.\n#o R8D\n#f 93 92 96\n#f 93 96 97\n#l 93 92\n#l 92 96\n#l 96 97\n#l 93 97\n# Panel doesn't exist.\n#o R8P\n#f 95 97 96\n#l 95 97\n#l 97 96\n#l 95 96\no R5D\nf 80 82 90\nl 80 82\nl 82 90\nl 80 90\no R5P\nf 81 91 83\nl 81 91\nl 91 83\nl 81 83\no R6D\nf 90 82 92\nl 90 82\nl 82 92\nl 90 92\no R6P\nf 91 96 83\nl 91 96\nl 96 83\nl 91 83\no R7D\nf 82 76 92\nl 82 76\nl 76 92\nl 82 92\no R7P\nf 83 96 79\nl 83 96\nl 96 79\nl 83 79\n# Panel doesn't exist.\n#o R9D\n#f 86 93 98\n#f 86 98 88\n#f 86 87 93\n#l 93 98\n#l 98 88\n#l 86 88\n#l 86 87\n#l 93 87\n# Panel doesn't exist.\n#o R9P\n#f 98 89 88\n#f 98 94 89\n#l 89 88\n#l 98 88\n#l 98 94\n#l 89 94\no EarBD\nf 99 100 101\nf 100 102 101\nf 101 102 103\nf 103 102 104\nf 105 104 102\nf 105 106 104\nf 107 104 106\nf 99 107 100\nf 106 100 107\nf 106 105 100\nf 105 102 100\nl 99 100\nl 100 101\nl 99 101\nl 100 102\nl 101 102\nl 102 103\nl 101 103\nl 102 104\nl 103 104\nl 104 105\nl 102 105\nl 105 106\nl 104 106\nl 104 107\nl 106 107\nl 99 107\nl 100 107\nl 100 106\nl 100 105\no EarAD\nf 103 108 101\nf 108 109 101\nf 99 101 109\nf 107 110 104\nf 111 104 110\nf 104 111 103\nf 108 103 111\nf 111 110 108\nf 109 108 110\nf 109 110 99\nf 107 99 110\nl 103 108\nl 108 101\nl 103 101\nl 108 109\nl 101 109\nl 101 99\nl 109 99\nl 107 110\nl 110 104\nl 107 104\nl 104 111\nl 110 111\nl 103 111\nl 103 104\nl 108 111\nl 108 110\nl 109 110\nl 99 110\nl 99 107\no EarBP\nf 112 113 114\nf 113 115 114\nf 113 116 115\nf 116 117 115\nf 116 118 117\nf 119 117 118\nf 119 118 112\nf 120 112 118\nf 120 113 112\nf 116 113 120\nf 118 116 120\nl 112 113\nl 113 114\nl 112 114\nl 113 115\nl 114 115\nl 113 116\nl 115 116\nl 116 117\nl 115 117\nl 116 118\nl 117 118\nl 117 119\nl 118 119\nl 112 118\nl 112 119\nl 112 120\nl 118 120\nl 113 120\nl 116 120\no EarAP\nf 114 115 121\nf 121 115 122\nf 117 122 115\nf 117 119 122\nf 123 122 119\nf 122 123 121\nf 124 121 123\nf 124 112 121\nf 114 121 112\nf 119 112 123\nf 124 123 112\nl 114 115\nl 115 121\nl 114 121\nl 115 122\nl 121 122\nl 122 117\nl 115 117\nl 117 119\nl 122 119\nl 122 123\nl 119 123\nl 121 123\nl 121 124\nl 123 124\nl 124 112\nl 121 112\nl 114 112\nl 119 112\nl 123 112\no F19D\nf 125 126 127\nl 125 126\nl 126 127\nl 125 127\no F19P\nf 128 129 130\nl 128 129\nl 129 130\nl 128 130\no F18D\nf 125 131 126\nf 131 125 132\nl 131 126\nl 125 126\nl 125 132\nl 131 132\no F18P\nf 130 133 128\nf 133 130 134\nl 133 128\nl 130 128\nl 130 134\nl 133 134\no F17D\nf 135 136 125\nl 135 136\nl 136 125\nl 135 125\no F17P\nf 137 128 138\nl 137 128\nl 128 138\nl 137 138\no F20D\nf 136 132 125\nf 132 136 131\nf 131 136 139\nl 132 125\nl 136 125\nl 132 131\nl 136 139\nl 131 139\no F20P\nf 133 138 128\nf 138 133 134\nl 138 128\nl 133 128\nl 133 134\nl 138 134\no F16D\nf 131 140 126\nf 140 131 139\nf 140 139 136\nl 140 126\nl 131 126\nl 131 139\nl 139 136\nl 140 136\no F16P\nf 141 134 130\nf 134 141 138\nl 134 130\nl 141 130\nl 141 138\nl 134 138\no F12D\nf 135 142 136\nf 142 135 143\nf 143 135 144\nl 142 136\nl 135 136\nl 142 143\nl 135 144\nl 143 144\no F12P\nf 145 137 138\nf 137 145 146\nf 137 146 147\nl 137 138\nl 145 138\nl 145 146\nl 146 147\nl 137 147\no F21D\nf 148 125 127\nf 125 148 128\nl 125 127\nl 148 127\nl 148 128\nl 125 128\no F21P\nf 128 148 129\nl 128 148\nl 148 129\nl 128 129\no F13D\nf 140 149 150\nf 149 140 136\nf 149 136 151\nf 151 136 142\nl 149 150\nl 140 150\nl 140 136\nl 149 151\nl 136 142\nl 151 142\no F13P\nf 141 152 138\nf 152 141 153\nf 138 152 154\nf 138 154 145\nl 141 138\nl 141 153\nl 152 153\nl 152 154\nl 154 145\nl 138 145\no F1D\nf 140 3 11\nl 140 3\nl 3 11\nl 140 11\no F1P\nf 13 5 141\nl 13 5\nl 5 141\nl 13 141\no F10\nf 147 155 156\nf 147 156 144\nl 147 155\nl 155 156\nl 156 144\nl 147 144\no F22\nf 125 137 135\nf 137 125 128\nl 137 135\nl 125 135\nl 125 128\nl 137 128\no F11\nf 137 144 135\nf 144 137 147\nl 144 135\nl 137 135\nl 137 147\nl 144 147\no F14D\nf 140 157 158\nf 157 140 150\nf 157 150 159\nf 159 150 160\nl 157 158\nl 140 158\nl 140 150\nl 157 159\nl 150 160\nl 159 160\no F14P\nf 161 141 162\nf 141 161 153\nf 153 161 163\nf 153 163 164\nl 141 162\nl 161 162\nl 141 153\nl 161 163\nl 163 164\nl 153 164\no F15\nf 158 165 162\nf 165 158 157\nf 162 165 166\nf 161 162 166\nl 158 162\nl 158 157\nl 165 157\nl 165 166\nl 162 161\nl 166 161\no F2D\nf 3 140 158\nl 3 140\nl 140 158\nl 3 158\no F2P\nf 5 162 141\nl 5 162\nl 162 141\nl 5 141\no F4D\nf 1 167 168\nf 167 1 158\nl 167 168\nl 1 168\nl 1 158\nl 167 158\no F4P\nf 169 4 170\nf 4 169 162\nl 4 170\nl 169 170\nl 169 162\nl 4 162\no F5D\nf 1 168 171\nl 1 168\nl 168 171\nl 1 171\no F5P\nf 172 4 173\nf 4 172 170\nl 4 173\nl 172 173\nl 172 170\nl 4 170\no F6D\nf 1 171 9\nl 1 171\nl 171 9\nl 1 9\n# F6P doesn't exist because of the exit.\n#o F6P\n#f 173 4 10\n#l 173 4\n#l 4 10\n#l 173 10\no F7D\nf 9 171 23\nf 171 174 23\nl 9 171\nl 9 23\nl 171 174\nl 23 174\n# F7P doesn't exist because of the exit.\n#o F7P\n#f 173 10 26\n#f 173 26 175\n#l 173 10\n#l 10 26\n#l 26 175\n#l 173 175\no F10D\nf 158 176 167\nl 158 176\nl 176 167\nl 158 167\no F10P\nf 176 162 169\nf 162 176 158\nl 162 169\nl 176 169\nl 176 158\nl 162 158\no F25\nf 176 169 177\nl 176 169\nl 169 177\nl 176 177\no F22D\nf 167 177 168\nf 167 176 177\nl 177 168\nl 167 168\nl 167 176\nl 177 176\no F22P\nf 169 170 177\nl 169 170\nl 170 177\nl 169 177\no F23D\nf 178 179 168\nl 178 179\nl 179 168\nl 178 168\no F23P\nf 170 172 180\nl 170 172\nl 172 180\nl 170 180\no F24D\nf 181 182 178\nl 181 182\nl 182 178\nl 181 178\no F24P\nf 182 180 178\nf 180 182 183\nl 180 178\nl 182 178\nl 182 183\nl 180 183\no F3D\nf 3 158 1\nl 3 158\nl 158 1\nl 3 1\no F3P\nf 5 4 162\nl 5 4\nl 4 162\nl 5 162\no Face\nf 184 155 185\nf 186 155 184\nf 155 186 156\nf 186 187 156\nf 188 187 186\nf 187 188 189\nf 187 188 189\nf 188 187 186\nf 186 187 187\nf 188 186 190\nf 191 190 186\nf 192 190 191\nf 193 190 192\nf 190 193 194\nf 195 190 194\nf 196 190 195\nf 197 190 196\nf 198 190 197\nf 198 199 190\nf 200 199 198\nf 201 199 200\nf 202 199 201\nf 203 199 202\nf 204 199 203\nf 199 204 205\nf 205 206 199\nf 206 205 207\nf 207 208 206\nf 208 207 209\nf 208 209 210\nf 208 210 211\nf 208 211 211\nf 208 211 212\nf 208 212 213\nf 208 213 214\nf 208 214 215\nf 208 215 216\nf 217 216 215\nf 216 217 218\nf 216 218 219\nf 216 219 220\nf 220 221 216\nf 222 221 220\nf 223 221 222\nf 224 221 223\nf 225 221 224\nf 226 221 225\nf 226 186 221\nf 227 186 226\nf 228 186 227\nf 186 228 229\nf 230 186 229\nf 186 230 231\nf 186 231 232\nf 186 232 191\nf 186 233 221\nf 233 186 184\nf 184 234 233\nf 184 235 234\nf 184 236 235\nf 236 184 237\nf 184 238 237\nf 238 184 239\nf 239 184 240\nf 240 184 241\nf 242 241 184\nf 242 243 241\nf 242 244 243\nf 242 245 244\nf 242 246 245\nf 242 247 246\nf 242 248 247\nf 249 248 242\nf 248 249 250\nf 250 249 251\nf 251 249 252\nf 252 249 253\nf 253 249 254\nf 254 249 255\nf 249 256 255\nf 256 249 257\nf 249 163 257\nf 249 164 163\nf 249 153 164\nf 249 152 153\nf 152 249 152\nf 249 154 152\nf 154 249 242\nf 258 154 242\nf 154 258 154\nf 258 242 259\nf 259 242 184\nf 259 184 185\nf 259 185 258\nf 161 257 163\nf 257 161 260\nf 260 161 261\nf 261 161 166\nf 161 163 166\nf 166 262 261\nf 262 166 165\nf 165 208 262\nf 157 208 165\nf 157 206 208\nf 206 157 159\nf 206 159 199\nf 160 199 159\nf 160 150 199\nf 150 149 199\nf 151 199 149\nf 199 151 190\nf 263 190 151\nf 190 263 188\nf 189 188 263\nf 187 189 263\nf 187 187 189\nf 187 187 156\nf 151 149 149\nf 208 264 262\nf 264 208 265\nf 208 216 265\nf 221 265 216\nf 265 221 266\nf 221 267 266\nf 267 221 268\nf 268 221 233\nf 233 269 268\nf 269 233 270\nf 233 271 270\nf 233 272 271\nf 233 273 272\nf 233 274 273\nf 233 275 274\nf 233 234 275\nf 276 270 271\nf 277 270 276\nf 270 277 278\nf 278 260 270\nf 279 260 278\nf 280 260 279\nf 281 260 280\nf 282 260 281\nf 283 260 282\nf 283 257 260\nf 257 283 256\nf 270 260 269\nf 260 284 269\nf 284 260 261\nf 262 284 261\nf 284 262 264\nf 266 284 264\nf 284 266 267\nf 267 266 266\nf 266 266 265\nf 266 264 265\nf 284 267 268\nf 284 268 268\nf 268 269 268\nf 268 269 284\nl 184 155\nl 155 185\nl 184 185\nl 155 186\nl 184 186\nl 186 156\nl 155 156\nl 186 187\nl 156 187\nl 187 188\nl 186 188\nl 188 189\nl 187 189\nl 187 187\nl 186 190\nl 188 190\nl 190 191\nl 186 191\nl 192 190\nl 192 191\nl 193 190\nl 192 193\nl 193 194\nl 194 190\nl 195 190\nl 194 195\nl 196 190\nl 195 196\nl 197 190\nl 196 197\nl 198 190\nl 197 198\nl 198 199\nl 199 190\nl 199 200\nl 198 200\nl 199 201\nl 200 201\nl 199 202\nl 201 202\nl 199 203\nl 202 203\nl 199 204\nl 203 204\nl 204 205\nl 199 205\nl 205 206\nl 199 206\nl 205 207\nl 206 207\nl 207 208\nl 206 208\nl 207 209\nl 208 209\nl 209 210\nl 208 210\nl 210 211\nl 208 211\nl 211 211\nl 211 212\nl 208 212\nl 212 213\nl 208 213\nl 213 214\nl 208 214\nl 214 215\nl 208 215\nl 215 216\nl 208 216\nl 216 217\nl 215 217\nl 217 218\nl 216 218\nl 218 219\nl 216 219\nl 219 220\nl 216 220\nl 220 221\nl 216 221\nl 221 222\nl 220 222\nl 221 223\nl 222 223\nl 221 224\nl 223 224\nl 221 225\nl 224 225\nl 221 226\nl 225 226\nl 186 226\nl 186 221\nl 186 227\nl 226 227\nl 186 228\nl 227 228\nl 228 229\nl 186 229\nl 186 230\nl 229 230\nl 230 231\nl 186 231\nl 231 232\nl 186 232\nl 232 191\nl 186 233\nl 221 233\nl 184 233\nl 184 234\nl 233 234\nl 184 235\nl 234 235\nl 184 236\nl 235 236\nl 184 237\nl 236 237\nl 184 238\nl 237 238\nl 184 239\nl 238 239\nl 184 240\nl 239 240\nl 184 241\nl 240 241\nl 241 242\nl 184 242\nl 242 243\nl 241 243\nl 242 244\nl 243 244\nl 242 245\nl 244 245\nl 242 246\nl 245 246\nl 242 247\nl 246 247\nl 242 248\nl 247 248\nl 248 249\nl 242 249\nl 249 250\nl 248 250\nl 249 251\nl 250 251\nl 249 252\nl 251 252\nl 249 253\nl 252 253\nl 249 254\nl 253 254\nl 249 255\nl 254 255\nl 249 256\nl 255 256\nl 249 257\nl 256 257\nl 249 163\nl 257 163\nl 249 164\nl 163 164\nl 249 153\nl 164 153\nl 249 152\nl 153 152\nl 152 152\nl 249 154\nl 152 154\nl 242 154\nl 154 258\nl 242 258\nl 154 154\nl 242 259\nl 258 259\nl 184 259\nl 185 259\nl 185 258\nl 257 161\nl 163 161\nl 161 260\nl 257 260\nl 161 261\nl 260 261\nl 161 166\nl 261 166\nl 163 166\nl 166 262\nl 261 262\nl 166 165\nl 262 165\nl 208 165\nl 208 262\nl 208 157\nl 165 157\nl 206 157\nl 157 159\nl 206 159\nl 199 159\nl 199 160\nl 159 160\nl 160 150\nl 199 150\nl 150 149\nl 199 149\nl 199 151\nl 149 151\nl 190 151\nl 190 263\nl 151 263\nl 188 263\nl 189 263\nl 187 263\nl 149 149\nl 208 264\nl 262 264\nl 208 265\nl 264 265\nl 216 265\nl 221 265\nl 221 266\nl 265 266\nl 221 267\nl 266 267\nl 268 221\nl 268 267\nl 268 233\nl 269 233\nl 268 269\nl 270 233\nl 269 270\nl 271 233\nl 270 271\nl 272 233\nl 271 272\nl 273 233\nl 272 273\nl 274 233\nl 273 274\nl 275 233\nl 274 275\nl 275 234\nl 270 276\nl 271 276\nl 270 277\nl 276 277\nl 277 278\nl 270 278\nl 278 260\nl 270 260\nl 279 260\nl 278 279\nl 280 260\nl 279 280\nl 281 260\nl 280 281\nl 282 260\nl 281 282\nl 283 260\nl 282 283\nl 283 257\nl 283 256\nl 269 260\nl 284 260\nl 269 284\nl 284 261\nl 284 262\nl 284 264\nl 284 266\nl 264 266\nl 284 267\nl 266 266\nl 268 284\nl 268 268\no TailB\nf 285 286 287\nf 288 286 285\nf 288 289 286\nf 290 289 288\nf 289 290 291\nf 292 285 287\nf 286 291 287\nf 291 286 289\nl 285 286\nl 286 287\nl 285 287\nl 286 288\nl 285 288\nl 288 289\nl 286 289\nl 289 290\nl 288 290\nl 290 291\nl 289 291\nl 285 292\nl 287 292\nl 286 291\nl 287 291\no TailA\nf 290 293 291\nf 293 290 294\nf 293 294 295\nf 296 295 294\nf 295 296 287\nf 292 287 296\nf 295 287 291\nf 295 291 293\nl 290 293\nl 293 291\nl 290 291\nl 290 294\nl 293 294\nl 294 295\nl 293 295\nl 295 296\nl 294 296\nl 296 287\nl 295 287\nl 287 292\nl 296 292\nl 291 287\nl 291 295\n","objDataIsFileRef":false,"metadata":{"type":"baaahs.model.StrandCountEntityMetadataProvider","data":{"1D":540,"1P":540,"2D":360,"2P":360,"3D":540,"3P":540,"4D":240,"4P":240,"5D":660,"5P":660,"6D":420,"6P":420,"7D":780,"7P":780,"8D":900,"8P":900,"9D":420,"9P":420,"10D":420,"10P":420,"11D":180,"11P":180,"12D":300,"12P":300,"13D":300,"13P":360,"14D":780,"14P":780,"15D":540,"15P":540,"16D":540,"16P":540,"17D":420,"17P":420,"18D":1020,"18P":1020,"19AP":960,"19BP":780,"20D":420,"20P":420,"21D":900,"21P":900,"22D":360,"22P":360,"23D":660,"23P":660,"24D":660,"24P":360,"25D":240,"25P":300,"26D":660,"26P":900,"27D":360,"27P":360,"28D":360,"28P":360,"29D":360,"29P":420,"30D":600,"30P":600,"31D":840,"31P":840,"32D":540,"32P":540,"33D":180,"33P":180,"34D":660,"34P":660,"35D":300,"35P":300,"36D":120,"36P":120,"37D":1080,"37P":1080,"38D":420,"38P":420,"39D":480,"39P":480,"40D":360,"40P":360,"41D":300,"41P":300,"42D":780,"42P":780,"43D":600,"43P":600,"44D":120,"44P":120,"45D":180,"45P":180,"51":540,"52D":540,"52P":660,"53D":120,"53P":120,"54D":420,"54P":360,"55D":0,"55P":0,"56D":120,"56P":120,"57D":300,"57P":300,"58D":420,"58P":420,"59D":240,"59P":180,"60D":2160,"60P":2160,"61D":240,"61P":240,"62D":120,"62P":120,"63D":300,"63P":300,"64D":240,"64P":180,"65D":660,"65P":720,"66D":120,"66P":120,"67D":360,"67P":180,"68D":180,"68P":180,"69D":300,"69P":300,"70":60,"71":0,"72":300,"80D":360,"80P":360,"83":480,"84D":900,"84P":900,"85D":300,"85P":300,"86D":240,"86P":240,"87D":300,"87P":300,"88D":120,"88P":120,"89D":180,"89P":240,"90":180,"91D":180,"91P":300,"92D":240,"92P":240,"93D":180,"93P":180,"Face":3480,"Tail":0,"F19P":240}}},{"type":"MovingHead","title":"leftEye","description":"Left Eye","position":{"x":-11.0,"y":202.361,"z":-24.5},"rotation":{"x":0.0,"y":0.15708,"z":-1.5708},"baseDmxChannel":1},{"type":"MovingHead","title":"rightEye","description":"Right Eye","position":{"x":-11.0,"y":202.361,"z":27.5},"rotation":{"x":0.0,"y":-0.15708,"z":-1.5708},"baseDmxChannel":17}],"units":"Inches"},"controllers":{"DMX:directDmx":{"type":"DirectDMX","fixtures":[{"entityId":"leftEye","fixtureConfig":{"type":"MovingHead","adapter":{"type":"Shenzarpy"}},"transportConfig":{"type":"DMX","startChannel":1}},{"entityId":"rightEye","fixtureConfig":{"type":"MovingHead","adapter":{"type":"Shenzarpy"}},"transportConfig":{"type":"DMX","startChannel":17}}]}}} \ No newline at end of file +{ + "model": { + "title": "BAAAHS", + "entities": [ + { + "type": "Import", + "title": "BAAAHS", + "objData": "# OBJ model file (sort of)\n# Exported from SketchUp with BAAAHS::Geometry and massaged by @tgvarik\n# File units = inches\n\nv -20.536 90.552 -71.329\nv -60.471 118.515 -106.139\nv -38.673 161.32 -82.139\nv -20.536 90.552 72.052\nv -38.673 161.32 82.861\nv -60.471 118.515 106.861\nv -57.929 75.089 -106.139\nv -57.929 75.089 106.861\nv -54.59 17.938 -82.139\nv -54.59 17.938 82.861\nv -79.542 190.665 -58.139\nv -107.735 184.672 -70.139\nv -79.542 190.665 58.861\nv -107.735 184.672 70.861\nv -97.991 127.29 -106.139\nv -97.991 127.29 106.861\nv -130.695 94.586 -106.139\nv -130.695 94.586 106.861\nv -77.314 25.919 -94.139\nv -77.314 25.919 94.861\nv -108.212 22.449 -82.139\nv -108.212 22.449 82.861\nv -63.915 1.188 -74.139\nv -96.665 1.188 -74.139\nv -96.665 1.188 74.861\nv -63.915 1.188 74.861\nv -130.907 162.735 -94.139\nv -144.1 176.943 -70.139\nv -130.907 162.735 94.861\nv -144.1 176.943 70.861\nv -167.131 12.657 -74.139\nv -167.131 12.657 74.861\nv -177.484 111.006 -106.139\nv -179.55 137.571 -94.139\nv -179.55 137.571 94.861\nv -177.484 111.006 106.861\nv -211.423 68.356 -106.139\nv -211.423 68.356 106.861\nv -153.052 167.001 76.922\nv -267.065 189.739 78.928\nv -271.6 144.033 82.861\nv -271.6 144.033 -82.139\nv -252.672 120.23 -89.688\nv -232.54 94.912 -97.717\nv -252.672 120.23 90.41\nv -232.54 94.912 98.439\nv -237.042 27.111 -70.139\nv -237.042 27.111 70.861\nv -350.686 196.403 -46.139\nv -279.94 189.739 -70.139\nv -336.282 157.617 -90.139\nv -311.668 191.493 77.517\nv -350.686 196.403 46.861\nv -336.282 157.617 90.861\nv -276.091 168.647 -74.139\nv -321.878 118.831 -94.139\nv -321.878 118.831 94.861\nv -313.542 82.725 -82.139\nv -313.542 82.725 82.861\nv -304.721 41.104 -82.139\nv -304.721 41.104 82.861\nv -386.74 137.917 -82.139\nv -386.74 137.917 82.861\nv -341.744 119.776 -106.139\nv -341.744 119.776 106.861\nv -321.914 29.939 -82.139\nv -321.914 29.939 82.861\nv -362.395 120.552 -106.139\nv -362.395 120.552 106.861\nv -330.551 15.163 -70.139\nv -357.679 26.124 -82.139\nv -357.679 26.124 82.861\nv -330.551 15.163 70.861\nv -337.673 1.188 -66.139\nv -385.253 3.731 -66.139\nv -391.397 17.717 -70.139\nv -337.673 1.188 66.861\nv -385.253 3.731 66.861\nv -391.397 17.717 70.861\nv -411.149 155.054 -54.139\nv -411.149 155.054 54.861\nv -402.339 93.794 -64.139\nv -402.339 93.794 64.861\nv -102 194.61 -46.799\nv -102 194.61 47.522\nv -380.917 175.728 -3.639\nv -396.033 165.391 -28.889\nv -385.697 172.459 5.611\nv -398.423 163.757 30.236\nv -415.748 129.593 -46.514\nv -415.748 129.593 47.236\nv -402.189 65.133 -32.11\nv -397.816 136.359 -29.822\nv -399.245 142.234 30.803\nv -399.569 133.761 31.026\nv -402.189 65.133 32.833\nv -399.453 109.694 1.218\nv -388.07 165.391 -1.327\nv -82.8 216.565 -46.499\nv -68.4 202.165 -53.7\nv -39.6 216.565 -46.499\nv -46.782 201.518 -53.7\nv -32.4 216.565 -53.7\nv -32.4 216.565 -96.899\nv -46.8 202.165 -82.499\nv -68.4 202.165 -82.499\nv -82.8 216.565 -96.899\nv -46.8 223.765 -53.7\nv -75.573 223.765 -53.7\nv -75.573 223.765 -89.7\nv -46.8 223.765 -89.7\nv -32.4 216.565 97.275\nv -46.782 201.518 54.075\nv -32.4 216.565 54.075\nv -39.6 216.565 46.875\nv -68.4 202.165 54.075\nv -82.8 216.565 46.875\nv -68.4 202.165 82.875\nv -82.8 216.565 97.275\nv -46.8 202.165 82.875\nv -46.8 223.765 54.075\nv -75.573 223.765 54.075\nv -75.573 223.765 90.075\nv -46.8 223.765 90.075\nv -82.907 238.165 -25.2\nv -90 194.61 -46.799\nv -111.6 202.165 -25.2\nv -82.907 238.165 25.2\nv -111.6 202.165 25.2\nv -90 194.61 46.8\nv -77.069 210.834 -46.799\nv -77.15 211.214 -46.499\nv -79.872 231.865 31.5\nv -72.502 216.565 46.8\nv -25.307 238.165 -25.2\nv -32.4 216.565 -46.799\nv -25.307 238.165 25.2\nv -32.4 216.565 46.8\nv -40.197 215.565 -46.799\nv -32.4 158.965 -46.799\nv -32.4 158.965 46.8\nv -24.995 219.033 -36.925\nv -16.066 222.01 -25.021\nv -10.8 223.765 -18\nv -24.995 219.033 36.926\nv -16.066 222.01 25.022\nv -10.8 223.765 18\nv -97.254 220.165 0\nv -26.664 186.774 -39.149\nv -24.169 183.658 -35.825\nv -26.662 216.81 -39.149\nv -26.997 186.326 39.598\nv -24.169 183.658 35.826\nv -26.997 216.361 39.598\nv -10.8 216.361 10.798\nv -10.8 216.361 -10.797\nv -10.8 166.374 -22.099\nv -10.8 122.965 -25.2\nv -12.987 171.504 -24.18\nv -20.966 181.245 -32.412\nv -10.8 166.374 22.1\nv -10.8 122.965 25.2\nv -12.987 171.504 24.181\nv -20.966 181.245 32.413\nv -10.8 150.392 -10.797\nv -10.8 150.392 10.798\nv -19.748 96.708 -36.997\nv -28.695 70.451 -48.794\nv -19.748 96.708 37.359\nv -28.695 70.451 49.517\nv -54.59 17.938 -60.764\nv -47.029 33.271 57.991\nv -54.59 17.938 61.486\nv -63.915 1.188 -57.139\nv -63.915 1.188 57.861\nv -15.274 109.836 6.08\nv -21.985 90.144 -21.357\nv -47.029 33.271 -21.29\nv -47.029 33.271 -57.269\nv -47.029 33.271 25.638\nv -33.063 61.593 -42.241\nv -42.259 42.943 5.703\nv -36.258 55.113 39.666\nv -4.77 216.361 10.798\nv -10.743 223.561 17.998\nv -4.77 216.361 -10.797\nv -10.762 223.539 -17.975\nv -19.17 223.561 -32.397\nv -10.909 223.561 -18.184\nv -19.28 218.749 -37.21\nv -12.745 215.389 -25.519\nv -14.34 214.945 -28.487\nv -15.793 213.643 -31.27\nv -16.396 212.612 -32.47\nv -17.1 211.575 -33.628\nv -18.178 208.88 -35.398\nv -18.855 205.74 -36.511\nv -19.086 202.371 -36.891\nv -19.28 188.714 -37.21\nv -18.855 199.001 -36.511\nv -18.178 195.862 -35.398\nv -17.1 193.167 -33.628\nv -15.697 191.1 -31.322\nv -14.063 189.803 -28.637\nv -12.311 189.362 -25.758\nv -11.97 171.926 -25.197\nv -11.97 189.449 -25.197\nv 1.195 158.761 -25.197\nv -10.692 189.553 -25.197\nv -8.747 189.95 -25.197\nv -6.825 190.595 -25.197\nv -5.079 191.706 -25.197\nv -3.427 193.045 -25.197\nv -2.851 193.631 -25.197\nv -2.659 193.886 -25.197\nv 16.83 174.397 -25.197\nv -1.751 195.334 -23.531\nv -1.162 197.249 -21.761\nv -1.086 198.339 -20.936\nv -1.059 199.577 -20.042\nv 31.23 180.361 -10.797\nv -1.495 201.947 -18.674\nv -1.598 202.371 -18.447\nv -2.637 204.743 -17.505\nv -3.059 205.57 -17.219\nv -4.193 207.142 -16.908\nv -5.574 208.832 -16.69\nv -6.033 209.253 -16.717\nv -7.853 210.725 -16.963\nv -8.196 211.574 -17.517\nv -9.564 213.644 -19.841\nv -11.116 214.945 -22.57\nv 31.23 180.361 10.798\nv -4.558 205.896 18.049\nv -6.345 208.02 17.81\nv -7.221 208.94 17.779\nv -8.345 209.824 17.949\nv -8.971 211.646 19.2\nv -10.176 213.715 21.612\nv -11.581 215.016 24.42\nv -13.088 215.46 27.435\nv -19.17 216.361 39.598\nv -14.595 215.016 30.449\nv -15.999 213.715 33.257\nv -17.205 211.646 35.669\nv -18.13 208.95 37.52\nv -18.712 205.81 38.683\nv -18.911 202.44 39.08\nv -19.17 186.326 39.598\nv -18.712 199.07 38.683\nv -18.13 195.93 37.52\nv -17.205 193.233 35.669\nv -15.999 191.164 33.257\nv -14.595 189.863 30.449\nv -13.088 189.42 27.435\nv -11.97 189.749 25.198\nv -11.97 171.926 25.198\nv -23.508 223.561 32.398\nv -19.17 223.561 32.398\nv 1.195 158.761 25.198\nv -4.77 144.361 10.798\nv -4.77 144.361 -10.797\nv -23.508 223.561 -32.397\nv 24.03 144.361 -10.797\nv 16.83 158.761 -25.197\nv 31.204 151.535 -10.797\nv 31.23 151.561 -3.597\nv 31.204 151.535 10.798\nv 16.83 158.761 25.198\nv 16.83 174.397 25.198\nv -2.022 199.516 20.767\nv -2.148 200.137 20.417\nv -2.75 202.44 19.214\nv -3.134 203.281 18.891\nv -4.427 205.719 18.081\nv -2.026 197.045 22.518\nv -2.551 194.972 24.354\nv -2.996 194.223 25.198\nv -4.539 192.572 25.198\nv -4.672 192.457 25.198\nv -6.288 191.432 25.198\nv -8.133 190.545 25.198\nv -10.045 189.897 25.198\nv 24.03 144.361 10.798\nv -383.415 173.341 -13.553\nv -409.199 186.189 -13.534\nv -416.097 205.35 -27.934\nv -383.14 173.927 8.066\nv -409.199 186.189 8.066\nv -370.494 183.891 22.466\nv -416.097 205.35 22.466\nv -377.008 186.956 -27.934\nv -406.517 208.799 15.238\nv -362.53 188.101 15.238\nv -406.517 208.799 -17.659\nv -362.53 188.101 -17.659\no 1D\nf 1 2 3\nl 1 2\nl 2 3\nl 1 3\no 1P\nf 4 5 6\nl 4 5\nl 5 6\nl 4 6\no 2D\nf 1 7 2\nl 1 7\nl 7 2\nl 1 2\no 2P\nf 4 6 8\nl 4 6\nl 6 8\nl 4 8\no 3D\nf 1 9 7\nl 1 9\nl 9 7\nl 1 7\no 3P\nf 4 8 10\nl 4 8\nl 8 10\nl 4 10\no 4D\nf 11 3 12\nl 11 3\nl 3 12\nl 11 12\no 4P\nf 5 13 14\nl 5 13\nl 13 14\nl 5 14\no 5D\nf 3 2 12\nl 3 2\nl 2 12\nl 3 12\no 5P\nf 5 14 6\nl 5 14\nl 14 6\nl 5 6\no 6D\nf 2 15 12\nl 2 15\nl 15 12\nl 2 12\no 6P\nf 6 14 16\nl 6 14\nl 14 16\nl 6 16\no 7D\nf 15 7 17\nf 7 15 2\nl 7 17\nl 15 17\nl 15 2\nl 7 2\no 7P\nf 8 16 18\nf 16 8 6\nl 16 18\nl 8 18\nl 8 6\nl 16 6\no 8D\nf 9 19 7\nf 7 19 17\nl 9 19\nl 9 7\nl 19 17\nl 7 17\no 8P\nf 8 18 20\nf 10 8 20\nl 8 18\nl 18 20\nl 8 10\nl 20 10\no 9D\nf 19 21 17\nl 19 21\nl 21 17\nl 19 17\no 9P\nf 20 18 22\nl 20 18\nl 18 22\nl 20 22\no 10D\nf 9 23 19\nf 23 24 19\nf 19 24 21\nl 9 23\nl 9 19\nl 23 24\nl 24 21\nl 19 21\no 10P\nf 20 22 25\nf 26 20 25\nf 10 20 26\nl 20 22\nl 22 25\nl 25 26\nl 20 10\nl 26 10\no 11D\nf 12 27 28\nl 12 27\nl 27 28\nl 12 28\no 11P\nf 29 14 30\nl 29 14\nl 14 30\nl 29 30\no 12D\nf 15 27 12\nl 15 27\nl 27 12\nl 15 12\no 12P\nf 16 14 29\nl 16 14\nl 14 29\nl 16 29\no 13D\nf 15 17 27\nl 15 17\nl 17 27\nl 15 27\no 13P\nf 16 29 18\nl 16 29\nl 29 18\nl 16 18\no 14D\nf 21 31 17\nl 21 31\nl 31 17\nl 21 17\no 14P\nf 22 18 32\nl 22 18\nl 18 32\nl 22 32\no 15D\nf 27 33 34\nf 34 28 27\nl 27 33\nl 33 34\nl 34 28\nl 27 28\no 15P\nf 35 29 30\nf 29 35 36\nl 29 30\nl 35 30\nl 35 36\nl 29 36\no 16D\nf 17 33 27\nl 17 33\nl 33 27\nl 17 27\no 16P\nf 18 29 36\nl 18 29\nl 29 36\nl 18 36\no 17D\nf 17 37 33\nl 17 37\nl 37 33\nl 17 33\no 17P\nf 18 36 38\nl 18 36\nl 36 38\nl 18 38\no 18D\nf 17 31 37\nl 17 31\nl 31 37\nl 17 37\no 18P\nf 18 38 32\nl 18 38\nl 38 32\nl 18 32\no 19AP\nf 39 40 41\nl 39 40\nl 40 41\nl 39 41\no 19BP\nf 39 41 35\nl 39 41\nl 41 35\nl 39 35\no 20D\nf 33 42 34\nl 33 42\nl 42 34\nl 33 34\no 20P\nf 36 35 41\nl 36 35\nl 35 41\nl 36 41\no 21D\nf 33 43 42\nf 43 33 44\nf 44 33 37\nl 43 42\nl 33 42\nl 43 44\nl 33 37\nl 44 37\no 21P\nf 45 36 41\nf 36 45 46\nf 36 46 38\nl 36 41\nl 45 41\nl 45 46\nl 46 38\nl 36 38\no 22D\nf 37 47 44\nl 37 47\nl 47 44\nl 37 44\no 22P\nf 38 46 48\nl 38 46\nl 46 48\nl 38 48\no 23D\nf 31 47 37\nl 31 47\nl 47 37\nl 31 37\no 23P\nf 32 38 48\nl 32 38\nl 38 48\nl 32 48\no 24D\nf 49 50 51\nl 49 50\nl 50 51\nl 49 51\no 24P\nf 52 53 54\nl 52 53\nl 53 54\nl 52 54\no 25D\nf 55 51 50\nl 55 51\nl 51 50\nl 55 50\no 25P\nf 40 52 54\nl 40 52\nl 52 54\nl 40 54\no 26D\nf 42 56 55\nf 55 56 51\nl 42 56\nl 42 55\nl 56 51\nl 55 51\no 26P\nf 41 54 57\nf 40 54 41\nl 54 57\nl 41 57\nl 54 40\nl 41 40\no 27D\nf 42 58 56\nl 42 58\nl 58 56\nl 42 56\no 27P\nf 41 57 59\nl 41 57\nl 57 59\nl 41 59\no 28D\nf 43 58 42\nl 43 58\nl 58 42\nl 43 42\no 28P\nf 45 41 59\nl 45 41\nl 41 59\nl 45 59\no 29D\nf 44 58 43\nl 44 58\nl 58 43\nl 44 43\no 29P\nf 46 45 59\nl 46 45\nl 45 59\nl 46 59\no 30D\nf 44 60 58\nl 44 60\nl 60 58\nl 44 58\no 30P\nf 46 59 61\nl 46 59\nl 59 61\nl 46 61\no 31D\nf 44 47 60\nl 44 47\nl 47 60\nl 44 60\no 31P\nf 46 61 48\nl 46 61\nl 61 48\nl 46 48\no 32D\nf 51 62 49\nl 51 62\nl 62 49\nl 51 49\no 32P\nf 54 53 63\nl 54 53\nl 53 63\nl 54 63\no 33D\nf 56 64 51\nl 56 64\nl 64 51\nl 56 51\no 33P\nf 57 54 65\nl 57 54\nl 54 65\nl 57 65\no 34D\nf 60 66 58\nf 58 66 64\nf 58 64 56\nl 60 66\nl 60 58\nl 66 64\nl 64 56\nl 58 56\no 34P\nf 61 59 67\nf 59 57 67\nf 57 65 67\nl 61 59\nl 61 67\nl 59 57\nl 57 65\nl 67 65\no 35D\nf 51 68 62\nl 51 68\nl 68 62\nl 51 62\no 35P\nf 54 63 69\nl 54 63\nl 63 69\nl 54 69\no 36D\nf 51 64 68\nl 51 64\nl 64 68\nl 51 68\no 36P\nf 54 69 65\nl 54 69\nl 69 65\nl 54 65\no 37D\nf 70 71 68\nf 66 70 68\nf 66 68 64\nl 70 71\nl 71 68\nl 70 66\nl 68 64\nl 66 64\no 37P\nf 65 69 72\nf 65 72 73\nf 65 73 67\nl 65 69\nl 69 72\nl 72 73\nl 73 67\nl 65 67\no 38D\nf 74 75 71\nf 71 75 76\nf 70 74 71\nl 74 75\nl 75 76\nl 71 76\nl 74 70\nl 71 70\no 38P\nf 73 72 77\nf 77 72 78\nf 72 79 78\nl 73 72\nl 73 77\nl 77 78\nl 72 79\nl 78 79\no 39D\nf 49 62 80\nl 49 62\nl 62 80\nl 49 80\no 39P\nf 53 81 63\nl 53 81\nl 81 63\nl 53 63\no 40D\nf 80 62 82\nl 80 62\nl 62 82\nl 80 82\no 40P\nf 81 83 63\nl 81 83\nl 83 63\nl 81 63\no 41D\nf 62 68 82\nl 62 68\nl 68 82\nl 62 82\no 41P\nf 63 83 69\nl 63 83\nl 83 69\nl 63 69\no 42D\nf 68 76 82\nl 68 76\nl 76 82\nl 68 82\no 42P\nf 69 83 79\nl 69 83\nl 83 79\nl 69 79\no 43D\nf 71 76 68\nl 71 76\nl 76 68\nl 71 68\no 43P\nf 72 69 79\nl 72 69\nl 69 79\nl 72 79\n# Panel doesn't exist.\n#o 44D\n#f 84 11 12\n#l 84 11\n#l 11 12\n#l 84 12\n# Panel doesn't exist.\n#o 44P\n#f 13 85 14\n#l 13 85\n#l 85 14\n#l 13 14\n# Panel doesn't exist.\n#o 45D\n#f 84 12 28\n#l 84 12\n#l 12 28\n#l 84 28\n# Panel doesn't exist.\n#o 45P\n#f 14 85 30\n#l 14 85\n#l 85 30\n#l 14 30\no R1\nf 53 49 86\nl 53 49\nl 49 86\nl 53 86\no R2D\nf 87 49 80\nf 49 87 86\nl 49 80\nl 87 80\nl 87 86\nl 49 86\no R2P\nf 88 53 86\nf 53 88 89\nf 53 89 81\nl 53 86\nl 88 86\nl 88 89\nl 89 81\nl 53 81\no R3D\nf 87 80 90\nl 87 80\nl 80 90\nl 87 90\no R3P\nf 81 89 91\nl 81 89\nl 89 91\nl 81 91\no R4D\nf 92 87 90\nf 87 92 93\nl 87 90\nl 92 90\nl 92 93\nl 87 93\no R4P\nf 91 89 94\nf 91 94 95\nf 91 95 96\nl 91 89\nl 89 94\nl 94 95\nl 95 96\nl 91 96\n# Panel doesn't exist.\n#o R8D\n#f 93 92 96\n#f 93 96 97\n#l 93 92\n#l 92 96\n#l 96 97\n#l 93 97\n# Panel doesn't exist.\n#o R8P\n#f 95 97 96\n#l 95 97\n#l 97 96\n#l 95 96\no R5D\nf 80 82 90\nl 80 82\nl 82 90\nl 80 90\no R5P\nf 81 91 83\nl 81 91\nl 91 83\nl 81 83\no R6D\nf 90 82 92\nl 90 82\nl 82 92\nl 90 92\no R6P\nf 91 96 83\nl 91 96\nl 96 83\nl 91 83\no R7D\nf 82 76 92\nl 82 76\nl 76 92\nl 82 92\no R7P\nf 83 96 79\nl 83 96\nl 96 79\nl 83 79\n# Panel doesn't exist.\n#o R9D\n#f 86 93 98\n#f 86 98 88\n#f 86 87 93\n#l 93 98\n#l 98 88\n#l 86 88\n#l 86 87\n#l 93 87\n# Panel doesn't exist.\n#o R9P\n#f 98 89 88\n#f 98 94 89\n#l 89 88\n#l 98 88\n#l 98 94\n#l 89 94\no EarBD\nf 99 100 101\nf 100 102 101\nf 101 102 103\nf 103 102 104\nf 105 104 102\nf 105 106 104\nf 107 104 106\nf 99 107 100\nf 106 100 107\nf 106 105 100\nf 105 102 100\nl 99 100\nl 100 101\nl 99 101\nl 100 102\nl 101 102\nl 102 103\nl 101 103\nl 102 104\nl 103 104\nl 104 105\nl 102 105\nl 105 106\nl 104 106\nl 104 107\nl 106 107\nl 99 107\nl 100 107\nl 100 106\nl 100 105\no EarAD\nf 103 108 101\nf 108 109 101\nf 99 101 109\nf 107 110 104\nf 111 104 110\nf 104 111 103\nf 108 103 111\nf 111 110 108\nf 109 108 110\nf 109 110 99\nf 107 99 110\nl 103 108\nl 108 101\nl 103 101\nl 108 109\nl 101 109\nl 101 99\nl 109 99\nl 107 110\nl 110 104\nl 107 104\nl 104 111\nl 110 111\nl 103 111\nl 103 104\nl 108 111\nl 108 110\nl 109 110\nl 99 110\nl 99 107\no EarBP\nf 112 113 114\nf 113 115 114\nf 113 116 115\nf 116 117 115\nf 116 118 117\nf 119 117 118\nf 119 118 112\nf 120 112 118\nf 120 113 112\nf 116 113 120\nf 118 116 120\nl 112 113\nl 113 114\nl 112 114\nl 113 115\nl 114 115\nl 113 116\nl 115 116\nl 116 117\nl 115 117\nl 116 118\nl 117 118\nl 117 119\nl 118 119\nl 112 118\nl 112 119\nl 112 120\nl 118 120\nl 113 120\nl 116 120\no EarAP\nf 114 115 121\nf 121 115 122\nf 117 122 115\nf 117 119 122\nf 123 122 119\nf 122 123 121\nf 124 121 123\nf 124 112 121\nf 114 121 112\nf 119 112 123\nf 124 123 112\nl 114 115\nl 115 121\nl 114 121\nl 115 122\nl 121 122\nl 122 117\nl 115 117\nl 117 119\nl 122 119\nl 122 123\nl 119 123\nl 121 123\nl 121 124\nl 123 124\nl 124 112\nl 121 112\nl 114 112\nl 119 112\nl 123 112\no F19D\nf 125 126 127\nl 125 126\nl 126 127\nl 125 127\no F19P\nf 128 129 130\nl 128 129\nl 129 130\nl 128 130\no F18D\nf 125 131 126\nf 131 125 132\nl 131 126\nl 125 126\nl 125 132\nl 131 132\no F18P\nf 130 133 128\nf 133 130 134\nl 133 128\nl 130 128\nl 130 134\nl 133 134\no F17D\nf 135 136 125\nl 135 136\nl 136 125\nl 135 125\no F17P\nf 137 128 138\nl 137 128\nl 128 138\nl 137 138\no F20D\nf 136 132 125\nf 132 136 131\nf 131 136 139\nl 132 125\nl 136 125\nl 132 131\nl 136 139\nl 131 139\no F20P\nf 133 138 128\nf 138 133 134\nl 138 128\nl 133 128\nl 133 134\nl 138 134\no F16D\nf 131 140 126\nf 140 131 139\nf 140 139 136\nl 140 126\nl 131 126\nl 131 139\nl 139 136\nl 140 136\no F16P\nf 141 134 130\nf 134 141 138\nl 134 130\nl 141 130\nl 141 138\nl 134 138\no F12D\nf 135 142 136\nf 142 135 143\nf 143 135 144\nl 142 136\nl 135 136\nl 142 143\nl 135 144\nl 143 144\no F12P\nf 145 137 138\nf 137 145 146\nf 137 146 147\nl 137 138\nl 145 138\nl 145 146\nl 146 147\nl 137 147\no F21D\nf 148 125 127\nf 125 148 128\nl 125 127\nl 148 127\nl 148 128\nl 125 128\no F21P\nf 128 148 129\nl 128 148\nl 148 129\nl 128 129\no F13D\nf 140 149 150\nf 149 140 136\nf 149 136 151\nf 151 136 142\nl 149 150\nl 140 150\nl 140 136\nl 149 151\nl 136 142\nl 151 142\no F13P\nf 141 152 138\nf 152 141 153\nf 138 152 154\nf 138 154 145\nl 141 138\nl 141 153\nl 152 153\nl 152 154\nl 154 145\nl 138 145\no F1D\nf 140 3 11\nl 140 3\nl 3 11\nl 140 11\no F1P\nf 13 5 141\nl 13 5\nl 5 141\nl 13 141\no F10\nf 147 155 156\nf 147 156 144\nl 147 155\nl 155 156\nl 156 144\nl 147 144\no F22\nf 125 137 135\nf 137 125 128\nl 137 135\nl 125 135\nl 125 128\nl 137 128\no F11\nf 137 144 135\nf 144 137 147\nl 144 135\nl 137 135\nl 137 147\nl 144 147\no F14D\nf 140 157 158\nf 157 140 150\nf 157 150 159\nf 159 150 160\nl 157 158\nl 140 158\nl 140 150\nl 157 159\nl 150 160\nl 159 160\no F14P\nf 161 141 162\nf 141 161 153\nf 153 161 163\nf 153 163 164\nl 141 162\nl 161 162\nl 141 153\nl 161 163\nl 163 164\nl 153 164\no F15\nf 158 165 162\nf 165 158 157\nf 162 165 166\nf 161 162 166\nl 158 162\nl 158 157\nl 165 157\nl 165 166\nl 162 161\nl 166 161\no F2D\nf 3 140 158\nl 3 140\nl 140 158\nl 3 158\no F2P\nf 5 162 141\nl 5 162\nl 162 141\nl 5 141\no F4D\nf 1 167 168\nf 167 1 158\nl 167 168\nl 1 168\nl 1 158\nl 167 158\no F4P\nf 169 4 170\nf 4 169 162\nl 4 170\nl 169 170\nl 169 162\nl 4 162\no F5D\nf 1 168 171\nl 1 168\nl 168 171\nl 1 171\no F5P\nf 172 4 173\nf 4 172 170\nl 4 173\nl 172 173\nl 172 170\nl 4 170\no F6D\nf 1 171 9\nl 1 171\nl 171 9\nl 1 9\n# F6P doesn't exist because of the exit.\n#o F6P\n#f 173 4 10\n#l 173 4\n#l 4 10\n#l 173 10\no F7D\nf 9 171 23\nf 171 174 23\nl 9 171\nl 9 23\nl 171 174\nl 23 174\n# F7P doesn't exist because of the exit.\n#o F7P\n#f 173 10 26\n#f 173 26 175\n#l 173 10\n#l 10 26\n#l 26 175\n#l 173 175\no F10D\nf 158 176 167\nl 158 176\nl 176 167\nl 158 167\no F10P\nf 176 162 169\nf 162 176 158\nl 162 169\nl 176 169\nl 176 158\nl 162 158\no F25\nf 176 169 177\nl 176 169\nl 169 177\nl 176 177\no F22D\nf 167 177 168\nf 167 176 177\nl 177 168\nl 167 168\nl 167 176\nl 177 176\no F22P\nf 169 170 177\nl 169 170\nl 170 177\nl 169 177\no F23D\nf 178 179 168\nl 178 179\nl 179 168\nl 178 168\no F23P\nf 170 172 180\nl 170 172\nl 172 180\nl 170 180\no F24D\nf 181 182 178\nl 181 182\nl 182 178\nl 181 178\no F24P\nf 182 180 178\nf 180 182 183\nl 180 178\nl 182 178\nl 182 183\nl 180 183\no F3D\nf 3 158 1\nl 3 158\nl 158 1\nl 3 1\no F3P\nf 5 4 162\nl 5 4\nl 4 162\nl 5 162\no Face\nf 184 155 185\nf 186 155 184\nf 155 186 156\nf 186 187 156\nf 188 187 186\nf 187 188 189\nf 187 188 189\nf 188 187 186\nf 186 187 187\nf 188 186 190\nf 191 190 186\nf 192 190 191\nf 193 190 192\nf 190 193 194\nf 195 190 194\nf 196 190 195\nf 197 190 196\nf 198 190 197\nf 198 199 190\nf 200 199 198\nf 201 199 200\nf 202 199 201\nf 203 199 202\nf 204 199 203\nf 199 204 205\nf 205 206 199\nf 206 205 207\nf 207 208 206\nf 208 207 209\nf 208 209 210\nf 208 210 211\nf 208 211 211\nf 208 211 212\nf 208 212 213\nf 208 213 214\nf 208 214 215\nf 208 215 216\nf 217 216 215\nf 216 217 218\nf 216 218 219\nf 216 219 220\nf 220 221 216\nf 222 221 220\nf 223 221 222\nf 224 221 223\nf 225 221 224\nf 226 221 225\nf 226 186 221\nf 227 186 226\nf 228 186 227\nf 186 228 229\nf 230 186 229\nf 186 230 231\nf 186 231 232\nf 186 232 191\nf 186 233 221\nf 233 186 184\nf 184 234 233\nf 184 235 234\nf 184 236 235\nf 236 184 237\nf 184 238 237\nf 238 184 239\nf 239 184 240\nf 240 184 241\nf 242 241 184\nf 242 243 241\nf 242 244 243\nf 242 245 244\nf 242 246 245\nf 242 247 246\nf 242 248 247\nf 249 248 242\nf 248 249 250\nf 250 249 251\nf 251 249 252\nf 252 249 253\nf 253 249 254\nf 254 249 255\nf 249 256 255\nf 256 249 257\nf 249 163 257\nf 249 164 163\nf 249 153 164\nf 249 152 153\nf 152 249 152\nf 249 154 152\nf 154 249 242\nf 258 154 242\nf 154 258 154\nf 258 242 259\nf 259 242 184\nf 259 184 185\nf 259 185 258\nf 161 257 163\nf 257 161 260\nf 260 161 261\nf 261 161 166\nf 161 163 166\nf 166 262 261\nf 262 166 165\nf 165 208 262\nf 157 208 165\nf 157 206 208\nf 206 157 159\nf 206 159 199\nf 160 199 159\nf 160 150 199\nf 150 149 199\nf 151 199 149\nf 199 151 190\nf 263 190 151\nf 190 263 188\nf 189 188 263\nf 187 189 263\nf 187 187 189\nf 187 187 156\nf 151 149 149\nf 208 264 262\nf 264 208 265\nf 208 216 265\nf 221 265 216\nf 265 221 266\nf 221 267 266\nf 267 221 268\nf 268 221 233\nf 233 269 268\nf 269 233 270\nf 233 271 270\nf 233 272 271\nf 233 273 272\nf 233 274 273\nf 233 275 274\nf 233 234 275\nf 276 270 271\nf 277 270 276\nf 270 277 278\nf 278 260 270\nf 279 260 278\nf 280 260 279\nf 281 260 280\nf 282 260 281\nf 283 260 282\nf 283 257 260\nf 257 283 256\nf 270 260 269\nf 260 284 269\nf 284 260 261\nf 262 284 261\nf 284 262 264\nf 266 284 264\nf 284 266 267\nf 267 266 266\nf 266 266 265\nf 266 264 265\nf 284 267 268\nf 284 268 268\nf 268 269 268\nf 268 269 284\nl 184 155\nl 155 185\nl 184 185\nl 155 186\nl 184 186\nl 186 156\nl 155 156\nl 186 187\nl 156 187\nl 187 188\nl 186 188\nl 188 189\nl 187 189\nl 187 187\nl 186 190\nl 188 190\nl 190 191\nl 186 191\nl 192 190\nl 192 191\nl 193 190\nl 192 193\nl 193 194\nl 194 190\nl 195 190\nl 194 195\nl 196 190\nl 195 196\nl 197 190\nl 196 197\nl 198 190\nl 197 198\nl 198 199\nl 199 190\nl 199 200\nl 198 200\nl 199 201\nl 200 201\nl 199 202\nl 201 202\nl 199 203\nl 202 203\nl 199 204\nl 203 204\nl 204 205\nl 199 205\nl 205 206\nl 199 206\nl 205 207\nl 206 207\nl 207 208\nl 206 208\nl 207 209\nl 208 209\nl 209 210\nl 208 210\nl 210 211\nl 208 211\nl 211 211\nl 211 212\nl 208 212\nl 212 213\nl 208 213\nl 213 214\nl 208 214\nl 214 215\nl 208 215\nl 215 216\nl 208 216\nl 216 217\nl 215 217\nl 217 218\nl 216 218\nl 218 219\nl 216 219\nl 219 220\nl 216 220\nl 220 221\nl 216 221\nl 221 222\nl 220 222\nl 221 223\nl 222 223\nl 221 224\nl 223 224\nl 221 225\nl 224 225\nl 221 226\nl 225 226\nl 186 226\nl 186 221\nl 186 227\nl 226 227\nl 186 228\nl 227 228\nl 228 229\nl 186 229\nl 186 230\nl 229 230\nl 230 231\nl 186 231\nl 231 232\nl 186 232\nl 232 191\nl 186 233\nl 221 233\nl 184 233\nl 184 234\nl 233 234\nl 184 235\nl 234 235\nl 184 236\nl 235 236\nl 184 237\nl 236 237\nl 184 238\nl 237 238\nl 184 239\nl 238 239\nl 184 240\nl 239 240\nl 184 241\nl 240 241\nl 241 242\nl 184 242\nl 242 243\nl 241 243\nl 242 244\nl 243 244\nl 242 245\nl 244 245\nl 242 246\nl 245 246\nl 242 247\nl 246 247\nl 242 248\nl 247 248\nl 248 249\nl 242 249\nl 249 250\nl 248 250\nl 249 251\nl 250 251\nl 249 252\nl 251 252\nl 249 253\nl 252 253\nl 249 254\nl 253 254\nl 249 255\nl 254 255\nl 249 256\nl 255 256\nl 249 257\nl 256 257\nl 249 163\nl 257 163\nl 249 164\nl 163 164\nl 249 153\nl 164 153\nl 249 152\nl 153 152\nl 152 152\nl 249 154\nl 152 154\nl 242 154\nl 154 258\nl 242 258\nl 154 154\nl 242 259\nl 258 259\nl 184 259\nl 185 259\nl 185 258\nl 257 161\nl 163 161\nl 161 260\nl 257 260\nl 161 261\nl 260 261\nl 161 166\nl 261 166\nl 163 166\nl 166 262\nl 261 262\nl 166 165\nl 262 165\nl 208 165\nl 208 262\nl 208 157\nl 165 157\nl 206 157\nl 157 159\nl 206 159\nl 199 159\nl 199 160\nl 159 160\nl 160 150\nl 199 150\nl 150 149\nl 199 149\nl 199 151\nl 149 151\nl 190 151\nl 190 263\nl 151 263\nl 188 263\nl 189 263\nl 187 263\nl 149 149\nl 208 264\nl 262 264\nl 208 265\nl 264 265\nl 216 265\nl 221 265\nl 221 266\nl 265 266\nl 221 267\nl 266 267\nl 268 221\nl 268 267\nl 268 233\nl 269 233\nl 268 269\nl 270 233\nl 269 270\nl 271 233\nl 270 271\nl 272 233\nl 271 272\nl 273 233\nl 272 273\nl 274 233\nl 273 274\nl 275 233\nl 274 275\nl 275 234\nl 270 276\nl 271 276\nl 270 277\nl 276 277\nl 277 278\nl 270 278\nl 278 260\nl 270 260\nl 279 260\nl 278 279\nl 280 260\nl 279 280\nl 281 260\nl 280 281\nl 282 260\nl 281 282\nl 283 260\nl 282 283\nl 283 257\nl 283 256\nl 269 260\nl 284 260\nl 269 284\nl 284 261\nl 284 262\nl 284 264\nl 284 266\nl 264 266\nl 284 267\nl 266 266\nl 268 284\nl 268 268\no TailB\nf 285 286 287\nf 288 286 285\nf 288 289 286\nf 290 289 288\nf 289 290 291\nf 292 285 287\nf 286 291 287\nf 291 286 289\nl 285 286\nl 286 287\nl 285 287\nl 286 288\nl 285 288\nl 288 289\nl 286 289\nl 289 290\nl 288 290\nl 290 291\nl 289 291\nl 285 292\nl 287 292\nl 286 291\nl 287 291\no TailA\nf 290 293 291\nf 293 290 294\nf 293 294 295\nf 296 295 294\nf 295 296 287\nf 292 287 296\nf 295 287 291\nf 295 291 293\nl 290 293\nl 293 291\nl 290 291\nl 290 294\nl 293 294\nl 294 295\nl 293 295\nl 295 296\nl 294 296\nl 296 287\nl 295 287\nl 287 292\nl 296 292\nl 291 287\nl 291 295\n", + "objDataIsFileRef": false, + "metadata": { + "type": "baaahs.model.StrandCountEntityMetadataProvider", + "data": { + "1D": 540, + "1P": 540, + "2D": 360, + "2P": 360, + "3D": 540, + "3P": 540, + "4D": 240, + "4P": 240, + "5D": 660, + "5P": 660, + "6D": 420, + "6P": 420, + "7D": 780, + "7P": 780, + "8D": 900, + "8P": 900, + "9D": 420, + "9P": 420, + "10D": 420, + "10P": 420, + "11D": 180, + "11P": 180, + "12D": 300, + "12P": 300, + "13D": 300, + "13P": 360, + "14D": 780, + "14P": 780, + "15D": 540, + "15P": 540, + "16D": 540, + "16P": 540, + "17D": 420, + "17P": 420, + "18D": 1020, + "18P": 1020, + "19AP": 960, + "19BP": 780, + "20D": 420, + "20P": 420, + "21D": 900, + "21P": 900, + "22D": 360, + "22P": 360, + "23D": 660, + "23P": 660, + "24D": 660, + "24P": 360, + "25D": 240, + "25P": 300, + "26D": 660, + "26P": 900, + "27D": 360, + "27P": 360, + "28D": 360, + "28P": 360, + "29D": 360, + "29P": 420, + "30D": 600, + "30P": 600, + "31D": 840, + "31P": 840, + "32D": 540, + "32P": 540, + "33D": 180, + "33P": 180, + "34D": 660, + "34P": 660, + "35D": 300, + "35P": 300, + "36D": 120, + "36P": 120, + "37D": 1080, + "37P": 1080, + "38D": 420, + "38P": 420, + "39D": 480, + "39P": 480, + "40D": 360, + "40P": 360, + "41D": 300, + "41P": 300, + "42D": 780, + "42P": 780, + "43D": 600, + "43P": 600, + "44D": 120, + "44P": 120, + "45D": 180, + "45P": 180, + "51": 540, + "52D": 540, + "52P": 660, + "53D": 120, + "53P": 120, + "54D": 420, + "54P": 360, + "55D": 0, + "55P": 0, + "56D": 120, + "56P": 120, + "57D": 300, + "57P": 300, + "58D": 420, + "58P": 420, + "59D": 240, + "59P": 180, + "60D": 2160, + "60P": 2160, + "61D": 240, + "61P": 240, + "62D": 120, + "62P": 120, + "63D": 300, + "63P": 300, + "64D": 240, + "64P": 180, + "65D": 660, + "65P": 720, + "66D": 120, + "66P": 120, + "67D": 360, + "67P": 180, + "68D": 180, + "68P": 180, + "69D": 300, + "69P": 300, + "70": 60, + "71": 0, + "72": 300, + "80D": 360, + "80P": 360, + "83": 480, + "84D": 900, + "84P": 900, + "85D": 300, + "85P": 300, + "86D": 240, + "86P": 240, + "87D": 300, + "87P": 300, + "88D": 120, + "88P": 120, + "89D": 180, + "89P": 240, + "90": 180, + "91D": 180, + "91P": 300, + "92D": 240, + "92P": 240, + "93D": 180, + "93P": 180, + "Face": 3480, + "Tail": 0, + "F19P": 240 + } + } + }, + { + "type": "MovingHead", + "title": "leftEye", + "description": "Left Eye", + "position": { + "x": -11.0, + "y": 202.361, + "z": -24.5 + }, + "rotation": { + "x": 0.0, + "y": 0.15708, + "z": -1.5708 + }, + "baseDmxChannel": 1 + }, + { + "type": "MovingHead", + "title": "rightEye", + "description": "Right Eye", + "position": { + "x": -11.0, + "y": 202.361, + "z": 27.5 + }, + "rotation": { + "x": 0.0, + "y": -0.15708, + "z": -1.5708 + }, + "baseDmxChannel": 17 + } + ], + "units": "Inches" + }, + "controllers": { + "DMX:directDmx": { + "type": "DirectDMX", + "fixtures": [ + { + "entityId": "leftEye", + "fixtureConfig": { + "type": "MovingHead", + "adapter": { + "type": "Shenzarpy" + } + }, + "transportConfig": { + "type": "DMX", + "startChannel": 1 + } + }, + { + "entityId": "rightEye", + "fixtureConfig": { + "type": "MovingHead", + "adapter": { + "type": "Shenzarpy" + } + }, + "transportConfig": { + "type": "DMX", + "startChannel": 17 + } + } + ] + } + } +} \ No newline at end of file diff --git a/data/config.json b/data/config.json index 3ddb9e02b9..cca00db7e0 100644 --- a/data/config.json +++ b/data/config.json @@ -1 +1 @@ -{"runningShowPath":"BRC 2024.sparkle","runningScenePath":"BRC23.scene","version":0} \ No newline at end of file +{"runningShowPath":"BRC 2024.sparkle","runningScenePath":"BAAAHS.scene","version":0} \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index bb023e81fb..6f8e3b5a1e 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -290,6 +290,9 @@ tasks.withType(Test::class) { useJUnitPlatform { excludeTags("glsl") } + + // Copy in system properties. + systemProperties = System.getProperties().asIterable().associate { it.key.toString() to it.value } } tasks.named("jvmTest") { diff --git a/shared/src/commonMain/kotlin/baaahs/controller/Controller.kt b/shared/src/commonMain/kotlin/baaahs/controller/Controller.kt index e5e678acb3..dbd473d909 100644 --- a/shared/src/commonMain/kotlin/baaahs/controller/Controller.kt +++ b/shared/src/commonMain/kotlin/baaahs/controller/Controller.kt @@ -9,24 +9,54 @@ import kotlinx.serialization.Serializable /** A Controller represents a physical device directly connected to one or more fixtures. */ interface Controller { val controllerId: ControllerId - val state: ControllerState val defaultFixtureOptions: FixtureOptions? val transportType: TransportType val defaultTransportConfig: TransportConfig? - fun createTransport( - entity: Model.Entity?, - fixtureConfig: FixtureConfig, - transportConfig: TransportConfig? - ): Transport - + /** + * Retrieves a list of fixture mappings that do not have associated names. + * This could be useful for fixtures that are automatically discovered or + * do not need explicit naming. + * + * @return A list of `FixtureMapping` instances without names. + */ fun getAnonymousFixtureMappings(): List + + /** Called before each frame is rendered. */ fun beforeFrame() {} + + /** Called after each frame has been rendered and [baaahs.gl.render.RenderTarget.sendFrame] has been called. */ fun afterFrame() {} + + /** + * Creates a [FixtureResolver] that is responsible for constructing transport instances + * necessary to communicate with all the fixtures associated with this controller. + * + * A single fixture resolver will be used to resolve all fixtures associated with this + * controller. That might be useful if, e.g., fixtures are allocated to sequential DMX + * channels. + * + * @return A new instance of `FixtureResolver`. + */ + fun createFixtureResolver(): FixtureResolver - fun beforeFixtureResolution() {} - fun afterFixtureResolution() {} + + /** + * Releases any resources associated with this controller + * and performs any necessary cleanup operations. + * + * Called by [ControllersManager] when [ControllerManager.onChange] for this controller returns null. + */ + fun release() {} +} + +interface FixtureResolver { + fun createTransport( + entity: Model.Entity?, + fixtureConfig: FixtureConfig, + transportConfig: TransportConfig? + ): Transport } open class NullController( @@ -34,13 +64,11 @@ open class NullController( override val defaultFixtureOptions: FixtureOptions? = null, override val defaultTransportConfig: TransportConfig? = null ) : Controller { - override val state: ControllerState = - State("Null Controller", "N/A", null) override val transportType: TransportType get() = DmxTransportType @Serializable - class State( + class NullState( override val title: String, override val address: String?, override val onlineSince: Instant?, @@ -49,11 +77,13 @@ open class NullController( override val lastErrorAt: Instant? = null ) : ControllerState() - override fun createTransport( - entity: Model.Entity?, - fixtureConfig: FixtureConfig, - transportConfig: TransportConfig? - ): Transport = NullTransport + override fun createFixtureResolver(): FixtureResolver = object : FixtureResolver { + override fun createTransport( + entity: Model.Entity?, + fixtureConfig: FixtureConfig, + transportConfig: TransportConfig? + ): Transport = NullTransport + } override fun getAnonymousFixtureMappings(): List = emptyList() diff --git a/shared/src/commonMain/kotlin/baaahs/controller/ControllerManager.kt b/shared/src/commonMain/kotlin/baaahs/controller/ControllerManager.kt index d896cd6af1..84d6abb2af 100644 --- a/shared/src/commonMain/kotlin/baaahs/controller/ControllerManager.kt +++ b/shared/src/commonMain/kotlin/baaahs/controller/ControllerManager.kt @@ -1,16 +1,64 @@ package baaahs.controller +import baaahs.fixtures.FixtureMapping import baaahs.fixtures.FixtureOptions +import baaahs.scene.ControllerConfig import baaahs.scene.MutableControllerConfig -import baaahs.scene.OpenControllerConfig +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract -/** A ControllerManager discovers and registers controllers with its [ControllerListener]. */ -interface ControllerManager { +/** + * Instances of `ControllerManager` are responsible for creating controllers + * from configuration, and discovering controllers. + * + * When configuration for this type of controller is found, or changed, or removed, + * [onChange] is called, and should return a [Controller] if it makes sense to. + * + * When a new controller is discovered, the ControllerManager should call [onStateChange]. + * Its [onChange] will then immediately be called. + */ +interface ControllerManager { val controllerType: String - fun addListener(controllerListener: ControllerListener) - fun removeListener(controllerListener: ControllerListener) - fun onConfigChange(controllerConfigs: Map>) + fun addStateChangeListener(listener: ControllerStateChangeListener) + fun removeStateChangeListener(listener: ControllerStateChangeListener) + fun onStateChange(controllerId: ControllerId, changeState: (fromState: S?) -> S?) + + /** + * Called by the [ControllersManager] when a scene is initially loaded, or when a controller configuration + * has been edited, as well as when the controller's state changes (as notified via + * [ControllerStateChangeListener]). + * + * Changes in the controller configuration and state are automatically synchronized with the client for + * display in the UI. + * + * @param controllerId The unique identifier of the controller. + * @param oldController Previously returned controller, if any. + * @param config Previous configuration of the controller, if any. + * @param state Previous state of the controller, if any. + * @param newConfig New configuration to be applied to the controller. + * @param newState New state to be applied to the controller. + * @return If `oldController` is returned, no action is taken. + * + * If `oldController` is not null and null is returned, any mapped fixtures are released and the old + * controller is disposed of. + * + * If `oldController` is null and null is returned, no action is taken. + * + * If `oldController` is null and a new controller is returned, the controller becomes active and + * any mapped fixtures are bound to it. + * + * If `oldController` is not null and a aifferent new controller is returned, the old controller is + * released, the new controller becomes active, and any mapped fixtures are moved to the new controller. + */ + fun onChange( + controllerId: ControllerId, + oldController: T?, + controllerConfig: Change, + controllerState: Change, + fixtureMappings: Change> + ): T? + fun start() fun reset() {} fun stop() @@ -33,20 +81,45 @@ interface ControllerManager { } } -abstract class BaseControllerManager( +@OptIn(ExperimentalContracts::class) +class Change(val oldValue: T, val newValue: T) { + val changed: Boolean get() = oldValue != newValue + val remainedNull: Boolean get() = oldValue == null && newValue == null + val becameNull: Boolean get() = oldValue != null && newValue == null + val becameNotNull: Boolean get() = oldValue == null && newValue != null + val remainedNotNull: Boolean get() = oldValue != null && newValue != null +} + +fun interface ControllerStateChangeListener { + /** + * Invoked by [ControllerManager] when the state of a controller changes. + * + * [ControllerManager.onChange] will immediately be invoked with the affected controller, + * along with its old state, so any state change handling can be performed. + * + * State change is automatically propagated to the client for UI display. + * + * @param controllerId The unique identifier of the controller whose state has changed. + * @param state The new state of the controller. + */ + fun onStateChange(controllerId: ControllerId, changeState: (fromState: S?) -> S?) +} + + +abstract class BaseControllerManager( override val controllerType: String -) : ControllerManager { - private val listeners: MutableList = mutableListOf() +) : ControllerManager { + private val listeners: MutableList> = mutableListOf() - override fun addListener(controllerListener: ControllerListener) { - listeners.add(controllerListener) + override fun addStateChangeListener(listener: ControllerStateChangeListener) { + listeners.add(listener) } - override fun removeListener(controllerListener: ControllerListener) { - listeners.remove(controllerListener) + override fun removeStateChangeListener(listener: ControllerStateChangeListener) { + listeners.remove(listener) } - fun notifyListeners(block: ControllerListener.() -> Unit) { - listeners.forEach(block) + override fun onStateChange(controllerId: ControllerId, changeState: (fromState: S?) -> S?) { + listeners.forEach { listener -> listener.onStateChange(controllerId, changeState) } } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/baaahs/controller/ControllerState.kt b/shared/src/commonMain/kotlin/baaahs/controller/ControllerState.kt new file mode 100644 index 0000000000..c9d18d5af2 --- /dev/null +++ b/shared/src/commonMain/kotlin/baaahs/controller/ControllerState.kt @@ -0,0 +1,18 @@ +package baaahs.controller + +import baaahs.ui.Observable +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable + +@Serializable +abstract class ControllerState : Observable() { + abstract val title: String + abstract val address: String? + abstract val onlineSince: Instant? + abstract val firmwareVersion: String? + abstract val lastErrorMessage: String? + abstract val lastErrorAt: Instant? + + open fun matches(controllerMatcher: ControllerMatcher): Boolean = + controllerMatcher.matches(title, address) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/baaahs/controller/ControllersManager.kt b/shared/src/commonMain/kotlin/baaahs/controller/ControllersManager.kt index b6885573be..d9c6437f96 100644 --- a/shared/src/commonMain/kotlin/baaahs/controller/ControllersManager.kt +++ b/shared/src/commonMain/kotlin/baaahs/controller/ControllersManager.kt @@ -1,89 +1,82 @@ package baaahs.controller +import baaahs.PubSub import baaahs.fixtures.Fixture import baaahs.fixtures.FixtureListener +import baaahs.fixtures.FixtureMapping import baaahs.getBang import baaahs.mapping.MappingManager +import baaahs.plugin.Plugins +import baaahs.scene.ControllerConfig +import baaahs.scene.OpenControllerConfig import baaahs.scene.OpenScene import baaahs.scene.SceneProvider import baaahs.sm.server.FrameListener +import baaahs.sm.webapi.Topics import baaahs.ui.addObserver import baaahs.util.Logger class ControllersManager( - private val controllerManagers: List, + private val controllerManagers: List>, private val mappingManager: MappingManager, private val sceneProvider: SceneProvider, private val fixtureListeners: List, - private val controllerListeners: List = emptyList() + pubSub: PubSub.Server, + plugins: Plugins ) : FrameListener { private val byType = controllerManagers.associateBy { it.controllerType } - private var deferFixtureRefresh = false - private val controllers = mutableMapOf() private var scene: OpenScene? = sceneProvider.openScene + private val controllerInfos = mutableMapOf() + + private var controllerStates by pubSub.state( + Topics.createControllerStates(plugins), emptyMap(), allowClientUpdates = false) {} init { controllerManagers.forEach { controllerManager -> - controllerManager.addListener(object : ControllerListener { - override fun onAdd(controller: Controller) { - logger.debug { "onAdd(${controller.controllerId})" } - if (controllers.containsKey(controller.controllerId)) - error("Already know about ${controller.controllerId}") - - val liveController = LiveController(controller) - controllers[controller.controllerId] = liveController - - if (!deferFixtureRefresh) - refreshControllerFixtures(listOf(liveController)) - controllerListeners.forEach { it.onAdd(controller) } - } - - override fun onRemove(controller: Controller) { - logger.debug { "onRemove(${controller.controllerId})" } - val liveController = controllers.remove(controller.controllerId) - ?: error("Don't know about ${controller.controllerId}") - - fixturesChanged(removed = liveController.fixtures) - controllerListeners.forEach { it.onRemove(controller) } - } - - override fun onError(controller: Controller) { - controllerListeners.forEach { it.onError(controller) } - onRemove(controller) - } - }) + controllerManager.addStateChangeListener { controllerId, changeState -> + gatherChanges { + handleStateChange(controllerId, changeState) + }.updateFixtures() + } } } fun start() { mappingManager.addObserver { - refreshControllerFixtures() + // When the MappingManager comes online, all fixtures may need a refresh. + refreshControllerFixtures(controllerInfos.keys) } sceneProvider.addObserver { onSceneChange() - refreshControllerFixtures() } controllerManagers.forEach { it.start() } onSceneChange() - refreshControllerFixtures() } - private fun refreshControllerFixtures( - controllers: Collection = this.controllers.values - ) { + private fun refreshControllerFixtures(controllerIds: Collection) { val addFixtures = arrayListOf() val removeFixtures = arrayListOf() - controllers.forEach { liveController -> - val controller = liveController.controller - removeFixtures.addAll(liveController.fixtures) - liveController.fixtures.clear() + for (controllerId in controllerIds) { + val controllerInfo = controllerInfos[controllerId] + if (controllerInfo == null) { + logger.warn { "Found no controllerInfo for $controllerId, skipping." } + continue + } + + val controller = controllerInfo.controller + removeFixtures.addAll(controllerInfo.fixtures) + if (controllerInfo.release) { + controllerInfos.remove(controllerId) + controller?.release() + continue + } val scene = this.scene - if (scene != null && mappingManager.dataHasLoaded) { + if (scene != null && mappingManager.dataHasLoaded && controller != null) { val newFixtures = scene.resolveFixtures(controller, mappingManager) - liveController.fixtures.addAll(newFixtures) + controllerInfo.fixtures = newFixtures addFixtures.addAll(newFixtures) } } @@ -92,33 +85,148 @@ class ControllersManager( } override fun beforeFrame() { - controllers.values.forEach { it.controller.beforeFrame() } + controllerInfos.values.forEach { it.controller?.beforeFrame() } } override fun afterFrame() { - controllers.values.forEach { it.controller.afterFrame() } + controllerInfos.values.forEach { it.controller?.afterFrame() } } private fun onSceneChange() { this.scene = sceneProvider.openScene - val managerConfig = scene?.let { - it.controllers.entries - .groupByTo(hashMapOf()) { (_, v) -> v.controllerType } - .mapKeys { (k, _) -> byType.getBang(k, "controller manager") } - .mapValues { (_, v) -> v.associate { (k, v) -> k to v } } - } ?: emptyMap() - - try { - deferFixtureRefresh = true - controllerManagers.forEach { controllerManager -> - controllerManager.onConfigChange(managerConfig[controllerManager] ?: emptyMap()) + val incomingConfigs: Map> = + (scene?.controllers ?: emptyMap>()) + as Map> + val priorConfigs = controllerInfos.filterValues { it.controllerConfig != null } + + val incomingFixtureMappings = scene?.fixtureMappings ?: emptyMap() + + val removedConfigs = priorConfigs.keys - incomingConfigs.keys + val addedConfigs = incomingConfigs.keys - priorConfigs.keys + val changedConfigs = incomingConfigs.keys.intersect(priorConfigs.keys) + .filter { controllerId -> priorConfigs[controllerId]!!.controllerState != incomingConfigs[controllerId] } + + gatherChanges { + (removedConfigs + changedConfigs + addedConfigs).forEach { controllerId -> + handleConfigChange( + controllerId, + incomingConfigs[controllerId]?.controllerConfig, + incomingFixtureMappings[controllerId] ?: emptyList() + ) } - } finally { - deferFixtureRefresh = false + }.updateFixtures() + } + + internal inner class ChangeGatherer { + private val changedControllerIds = mutableSetOf() + fun controllerChanged(id: ControllerId): Unit { + changedControllerIds.add(id) + } + + fun updateFixtures() { + refreshControllerFixtures(changedControllerIds) } } + private fun gatherChanges(block: ChangeGatherer.() -> Unit): ChangeGatherer = + ChangeGatherer().apply(block) + + private fun ChangeGatherer.handleConfigChange( + controllerId: ControllerId, + controllerConfig: ControllerConfig?, + fixtureMappings: List + ) { + val controllerInfo = controllerInfos[controllerId] + val oldControllerConfig = controllerInfo?.controllerConfig + val oldFixtureMappings = controllerInfo?.fixtureMappings ?: emptyList() + + if (controllerConfig == oldControllerConfig && fixtureMappings == oldFixtureMappings) + return + + sendChange( + controllerId, + controllerInfo?.controller, + controllerConfig, + controllerInfo?.controllerState, + fixtureMappings + ) + } + + private fun ChangeGatherer.handleStateChange( + controllerId: ControllerId, + changeState: (S?) -> S? + ) { + val controllerInfo = controllerInfos[controllerId] + val oldState = controllerInfo?.controllerState as S? + val newState = changeState(oldState) + if (newState == oldState) + return + + sendChange( + controllerId, + controllerInfo?.controller, + controllerInfo?.controllerConfig, + newState, + controllerInfo?.fixtureMappings + ) + } + + private fun ChangeGatherer.sendChange( + controllerId: ControllerId, + controller: Controller?, + controllerConfig: ControllerConfig?, + controllerState: ControllerState?, + fixtureMappings: List? + ) { + val controllerManager: ControllerManager = + byType.getBang(controllerId.controllerType, "controller manager") + as ControllerManager + + val controllerInfo = controllerInfos.getOrPut(controllerId) { ControllerInfo() } + val fromConfig = controllerInfo.controllerConfig + val fromState = controllerInfo.controllerState + val fromFixtureMappings = controllerInfo.fixtureMappings + logger.debug { "${controllerManager.controllerType}: update $controllerId: config=$controllerConfig state=$controllerState" } + val newController = + controllerManager.onChange( + controllerId, + controller, + Change(fromConfig, controllerConfig), + Change(fromState, controllerState), + Change(fromFixtureMappings, fixtureMappings ?: emptyList())) + logger.debug { " --> controller=${if (newController == controller) "SAME" else newController?.let { "new controller"} ?: "NULL" }" } + + controllerInfo.controller = newController + + controllerInfo.controllerConfig = controllerConfig + controllerInfo.controllerState = controllerState + controllerInfo.fixtureMappings = fixtureMappings ?: emptyList() + + if (newController == controller) return + + if (newController == null) { + controllerInfo.release = true + // Special case: if the controller is going away, we still need a handle + // to it so we can release it in a moment. + controllerInfo.controller = controller + } else { + if (fromState != controllerState) { + val newValue = controllerState + + controllerStates = controllerStates.toMutableMap().also { map -> + if (newValue != null) { + map[controllerId] = newValue + } else { + map.remove(controllerId) + } + } + } + } + + controllerChanged(controllerId) + } + fun reset() { controllerManagers.forEach { it.reset() @@ -126,7 +234,7 @@ class ControllersManager( } fun logStatus() { - val controllerCounts = controllers.keys + val controllerCounts = controllerInfos.keys .groupBy { it.controllerType } .mapValues { (_, v) -> v.size } .entries.sortedBy { (k, _) -> k } @@ -144,12 +252,20 @@ class ControllersManager( fixtureListeners.forEach { it.fixturesChanged(added, removed) } } - private class LiveController( - val controller: Controller, - val fixtures: MutableList = arrayListOf() - ) + private class ControllerInfo( + var controller: Controller? = null, + var controllerConfig: ControllerConfig? = null, + var controllerState: ControllerState? = null, + var fixtureMappings: List = emptyList(), + var fixtures: List = emptyList() + ) { + var release = false + } companion object { private val logger = Logger() } -} \ No newline at end of file +} + +fun generify(manager: ControllerManager<*, *, *>): ControllerManager = + manager as ControllerManager \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/baaahs/controller/ControllersPublisher.kt b/shared/src/commonMain/kotlin/baaahs/controller/ControllersPublisher.kt deleted file mode 100644 index 2888f90a02..0000000000 --- a/shared/src/commonMain/kotlin/baaahs/controller/ControllersPublisher.kt +++ /dev/null @@ -1,42 +0,0 @@ -package baaahs.controller - -import baaahs.PubSub -import baaahs.plugin.Plugins -import baaahs.sm.webapi.Topics -import baaahs.ui.Observable -import kotlinx.datetime.Instant -import kotlinx.serialization.Serializable - -class ControllersPublisher( - pubSub: PubSub.Server, - plugins: Plugins -) : ControllerListener { - private val controllerStatesChannel = pubSub.openChannel(Topics.createControllerStates(plugins), emptyMap()) {} - private val controllerStates = mutableMapOf() - - override fun onAdd(controller: Controller) { - controllerStates[controller.controllerId] = controller.state - controllerStatesChannel.onChange(controllerStates) - } - - override fun onRemove(controller: Controller) { - controllerStates.remove(controller.controllerId) - controllerStatesChannel.onChange(controllerStates) - } - - override fun onError(controller: Controller) { - } -} - -@Serializable -abstract class ControllerState : Observable() { - abstract val title: String - abstract val address: String? - abstract val onlineSince: Instant? - abstract val firmwareVersion: String? - abstract val lastErrorMessage: String? - abstract val lastErrorAt: Instant? - - open fun matches(controllerMatcher: ControllerMatcher): Boolean = - controllerMatcher.matches(title, address) -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/baaahs/controller/SacnController.kt b/shared/src/commonMain/kotlin/baaahs/controller/SacnController.kt index a129f78e2f..4bbcbf86be 100644 --- a/shared/src/commonMain/kotlin/baaahs/controller/SacnController.kt +++ b/shared/src/commonMain/kotlin/baaahs/controller/SacnController.kt @@ -6,53 +6,41 @@ import baaahs.io.ByteArrayWriter import baaahs.model.Model import baaahs.net.Network import baaahs.util.Logger -import kotlinx.datetime.Instant class SacnController( val id: String, - private val sacnLink: SacnLink, - private val address: Network.Address, + sacnLink: SacnLink, + address: Network.Address, override val defaultFixtureOptions: FixtureOptions?, override val defaultTransportConfig: TransportConfig?, private val universeCount: Int, - private val onlineSince: Instant?, private val universeListener: Dmx.UniverseListener? = null ) : Controller { override val controllerId: ControllerId = ControllerId(SacnManager.controllerTypeName, id) - override val state: ControllerState get() = SacnManager.State( - controllerId.name(), address.asString(), onlineSince, null, - sacnLink.lastError?.message, - sacnLink.lastErrorAt - ) override val transportType: TransportType get() = DmxTransportType private val dmxUniverses = DmxUniverses(universeCount) - private var dynamicDmxAllocator: DynamicDmxAllocator? = null private val node = sacnLink.deviceAt(address) val stats get() = node.stats private var sequenceNumber = 0 - override fun beforeFixtureResolution() { - dynamicDmxAllocator = DynamicDmxAllocator(dmxUniverses.channelsPerUniverse) - } - - override fun afterFixtureResolution() { - dynamicDmxAllocator = null - } + override fun createFixtureResolver(): FixtureResolver = object : FixtureResolver { + val dynamicDmxAllocator = DynamicDmxAllocator(dmxUniverses.channelsPerUniverse) - override fun createTransport( - entity: Model.Entity?, - fixtureConfig: FixtureConfig, - transportConfig: TransportConfig? - ): Transport { - val staticDmxMapping = dynamicDmxAllocator!!.allocate( - fixtureConfig.componentCount, fixtureConfig.bytesPerComponent, - transportConfig as DmxTransportConfig? - ) + override fun createTransport( + entity: Model.Entity?, + fixtureConfig: FixtureConfig, + transportConfig: TransportConfig? + ): Transport { + val staticDmxMapping = dynamicDmxAllocator.allocate( + fixtureConfig.componentCount, fixtureConfig.bytesPerComponent, + transportConfig as DmxTransportConfig? + ) return SacnTransport(transportConfig, staticDmxMapping) .also { dmxUniverses.validate(staticDmxMapping) } + } } override fun getAnonymousFixtureMappings(): List = emptyList() @@ -98,7 +86,7 @@ class SacnController( universeListener?.onSend(controllerId.name(), dmxUniverses.channels) } - fun release() { + override fun release() { logger.debug { "Releasing SacnController $id." } } diff --git a/shared/src/commonMain/kotlin/baaahs/controller/SacnManager.kt b/shared/src/commonMain/kotlin/baaahs/controller/SacnManager.kt index aba30f4c14..1a1802143c 100644 --- a/shared/src/commonMain/kotlin/baaahs/controller/SacnManager.kt +++ b/shared/src/commonMain/kotlin/baaahs/controller/SacnManager.kt @@ -1,29 +1,25 @@ package baaahs.controller +import baaahs.controller.SacnManager.SacnState import baaahs.device.PixelArrayDevice import baaahs.dmx.Dmx import baaahs.dmx.Dmx.Companion.channelsPerUniverse import baaahs.dmx.DmxTransportConfig import baaahs.dmx.DmxUniverses import baaahs.dmx.DynamicDmxAllocator -import baaahs.fixtures.ConfigPreview -import baaahs.fixtures.FixtureOptions -import baaahs.fixtures.FixturePreview -import baaahs.fixtures.TransportConfig +import baaahs.fixtures.* import baaahs.net.Network -import baaahs.scene.* +import baaahs.scene.ControllerConfig +import baaahs.scene.MutableControllerConfig +import baaahs.scene.MutableSacnControllerConfig +import baaahs.scene.PreviewBuilder import baaahs.util.Clock -import baaahs.util.Delta import baaahs.util.Logger import baaahs.util.coroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive import kotlin.coroutines.CoroutineContext @@ -33,80 +29,51 @@ class SacnManager( private val coroutineContext: CoroutineContext, private val clock: Clock, private val universeListener: Dmx.UniverseListener? = null -) : BaseControllerManager(controllerTypeName) { +) : BaseControllerManager(controllerTypeName) { private val senderCid = "SparkleMotion000".encodeToByteArray() private val sacnLink = SacnLink(link, senderCid, "SparkleMotion", clock) - private var lastConfig: Map> = emptyMap() - private var controllers: Map = emptyMap() - private var discoveredControllers: MutableMap = hashMapOf() + private var wledDiscoveryJob: Job? = null override fun start() { - startWledDiscovery() + wledDiscoveryJob = CoroutineScope(Dispatchers.Default + CoroutineName("WLED Discovery")) + .launch { + listenForWleds(link) + } } - override fun onConfigChange(controllerConfigs: Map>) { - handleConfigs(controllerConfigs.values - .filterIsInstance>() + override fun onChange( + controllerId: ControllerId, + oldController: SacnController?, + controllerConfig: Change, + controllerState: Change, + fixtureMappings: Change> + ): SacnController? { + val newConfig = controllerConfig.newValue + val newState = controllerState.newValue + + val address = newState?.address + ?: newConfig?.address + ?: return null + + val pixelCount = newState?.pixelCount + val bytesPerPixel = if (newState?.isRgbw == true) 4 else 3 + val universeCount = DynamicDmxAllocator() + .allocate(pixelCount ?: 0, bytesPerPixel) + .calculateEndUniverse(channelsPerUniverse) + return SacnController( + controllerId.id, sacnLink, link.createAddress(address), + newConfig?.defaultFixtureOptions.merge(PixelArrayDevice.Options(pixelCount)), + newConfig?.defaultTransportConfig, + newConfig?.universes ?: universeCount, + universeListener ) } - inline fun Map.filterByType(): Map = - buildMap { - this@filterByType.forEach { (k, v) -> - if (v is T) put(k, v) - } - } - override fun reset() { - discoveredControllers.forEach { (_, controller) -> - controller.release() - notifyListeners { onRemove(controller) } - } - discoveredControllers.clear() } override fun stop() { - TODO("not implemented") - } - - private fun handleConfigs(configs: List>) { - val configMap = configs.associateBy { it.id } - controllers = buildMap { - Delta.diff( - lastConfig, - configMap, - object : Delta.MapChangeListener> { - override fun onAdd(key: ControllerId, value: OpenControllerConfig) { - val controllerConfig = value.controllerConfig - val controller = SacnController( - key.id, sacnLink, link.createAddress(controllerConfig.address), - value.defaultFixtureOptions, value.defaultTransportConfig, - controllerConfig.universes, clock.now(), - universeListener - ) - put(controller.controllerId, controller) - notifyListeners { onAdd(controller) } - } - - override fun onRemove(key: ControllerId, value: OpenControllerConfig) { - val oldController = controllers[key] - if (oldController == null) { - logger.warn { "Unknown controller \"$key\" removed." } - } else { - oldController.release() - notifyListeners { onRemove(oldController) } - } - } - } - ) - } - lastConfig = configMap - } - - private fun startWledDiscovery() { - CoroutineScope(Dispatchers.Default).launch { - listenForWleds(link) - } + wledDiscoveryJob?.cancel() } private fun listenForWleds(link: Network.Link) { @@ -140,19 +107,15 @@ class SacnManager( withContext(this@SacnManager.coroutineContext) { val pixelCount = wledJson.info.leds.count - val bytesPerPixel = if (wledJson.info.leds.rgbw) 4 else 3 - - val universeCount = DynamicDmxAllocator() - .allocate(pixelCount, bytesPerPixel) - .calculateEndUniverse(channelsPerUniverse) - val sacnController = SacnController( - id, sacnLink, wledAddress, - PixelArrayDevice.Options(pixelCount), - null, universeCount, onlineSince, - universeListener - ) - discoveredControllers[sacnController.controllerId] = sacnController - notifyListeners { onAdd(sacnController) } + val isRgbw = wledJson.info.leds.rgbw + + val controllerId = ControllerId(controllerTypeName, id) + onStateChange(controllerId) { + SacnState( + id, wledAddress.asString(), onlineSince, "WLED ver ${wledJson.info.ver}", + pixelCount = pixelCount, isRgbw = isRgbw + ) + } } } } @@ -161,13 +124,15 @@ class SacnManager( } @Serializable - data class State( + data class SacnState( override val title: String, override val address: String, override val onlineSince: Instant?, override val firmwareVersion: String? = null, override val lastErrorMessage: String? = null, - override val lastErrorAt: Instant? = null + override val lastErrorAt: Instant? = null, + val pixelCount: Int, + val isRgbw: Boolean ) : ControllerState() companion object : ControllerManager.Meta { @@ -186,8 +151,8 @@ class SacnManager( ): MutableControllerConfig = MutableSacnControllerConfig( state?.title ?: controllerId?.id ?: "New sACN Controller", - (state as? State)?.address ?: "", - 1, mutableListOf(), null, null + (state as? SacnState)?.address ?: "", + 1, null, null ) } } @@ -195,10 +160,8 @@ class SacnManager( @Serializable @SerialName("SACN") data class SacnControllerConfig( override val title: String, - val address: String, - val universes: Int, - override val fixtures: List = emptyList(), - @SerialName("defaultFixtureConfig") + val address: String, // TODO: Should be optional. + val universes: Int, // TODO: Should be optional. override val defaultFixtureOptions: FixtureOptions? = null, override val defaultTransportConfig: TransportConfig? = null ) : ControllerConfig { @@ -206,38 +169,29 @@ data class SacnControllerConfig( override val emptyTransportConfig: TransportConfig get() = DmxTransportConfig() - @Transient - private var dmxAllocator: DynamicDmxAllocator? = null - - override fun edit(fixtureMappings: MutableList): MutableControllerConfig = + override fun edit(): MutableControllerConfig = MutableSacnControllerConfig( - title, address, universes, fixtureMappings, defaultFixtureOptions?.edit(), defaultTransportConfig?.edit() + title, address, universes, defaultFixtureOptions?.edit(), defaultTransportConfig?.edit() ) - // TODO: This is pretty dumb, find a better way to do this. - override fun buildFixturePreviews(sceneOpener: SceneOpener): List { - dmxAllocator = DynamicDmxAllocator() - try { - return super.buildFixturePreviews(sceneOpener) - } finally { - dmxAllocator = null - } - } - - override fun createFixturePreview(fixtureOptions: FixtureOptions, transportConfig: TransportConfig): FixturePreview { - val staticDmxMapping = dmxAllocator!!.allocate( - fixtureOptions.componentCount!!, - fixtureOptions.bytesPerComponent, - transportConfig as DmxTransportConfig - ) - val dmxUniverses = DmxUniverses(universes) - val dmxPreview = staticDmxMapping.preview(dmxUniverses) + override fun createPreviewBuilder(): PreviewBuilder = object : PreviewBuilder { + val dmxAllocator = DynamicDmxAllocator() - return object : FixturePreview { - override val fixtureOptions: ConfigPreview - get() = fixtureOptions.preview() - override val transportConfig: ConfigPreview - get() = dmxPreview + override fun createFixturePreview(fixtureOptions: FixtureOptions, transportConfig: TransportConfig): FixturePreview { + val staticDmxMapping = dmxAllocator.allocate( + fixtureOptions.componentCount ?: error("No component count."), + fixtureOptions.bytesPerComponent, + transportConfig as DmxTransportConfig + ) + val dmxUniverses = DmxUniverses(universes) + val dmxPreview = staticDmxMapping.preview(dmxUniverses) + + return object : FixturePreview { + override val fixtureOptions: ConfigPreview + get() = fixtureOptions.preview() + override val transportConfig: ConfigPreview + get() = dmxPreview + } } } } diff --git a/shared/src/commonMain/kotlin/baaahs/di/Modules.kt b/shared/src/commonMain/kotlin/baaahs/di/Modules.kt index 4491a965d0..ca2b5c62ca 100644 --- a/shared/src/commonMain/kotlin/baaahs/di/Modules.kt +++ b/shared/src/commonMain/kotlin/baaahs/di/Modules.kt @@ -6,7 +6,6 @@ import baaahs.app.settings.FeatureFlagsManager import baaahs.app.settings.Provider import baaahs.client.EventManager import baaahs.controller.ControllersManager -import baaahs.controller.ControllersPublisher import baaahs.controller.SacnManager import baaahs.dmx.Dmx import baaahs.dmx.DmxManager @@ -164,7 +163,6 @@ interface PinkyModule : KModule { ) } scoped { FixturePublisher(get(), get()) } - scoped { ControllersPublisher(get(), get()) } scoped { ControllersManager( get(named("ControllerManagers")), get(), get(), @@ -172,9 +170,7 @@ interface PinkyModule : KModule { get(), get(), ), - listOf( - get() - ) + get(), get() ) } scoped { ProdBrainSimulator(get(), get()) } diff --git a/shared/src/commonMain/kotlin/baaahs/dmx/DirectDmxController.kt b/shared/src/commonMain/kotlin/baaahs/dmx/DirectDmxController.kt index 64c76b0d13..5d8169115f 100644 --- a/shared/src/commonMain/kotlin/baaahs/dmx/DirectDmxController.kt +++ b/shared/src/commonMain/kotlin/baaahs/dmx/DirectDmxController.kt @@ -3,26 +3,24 @@ package baaahs.dmx import baaahs.controller.Controller import baaahs.controller.ControllerId import baaahs.controller.ControllerState +import baaahs.controller.FixtureResolver import baaahs.fixtures.* import baaahs.io.ByteArrayWriter import baaahs.model.Model -import baaahs.scene.* -import baaahs.util.Clock +import baaahs.scene.ControllerConfig +import baaahs.scene.MutableControllerConfig +import baaahs.scene.MutableDirectDmxControllerConfig +import baaahs.scene.PreviewBuilder import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient class DirectDmxController( private val device: Dmx.Device, - clock: Clock, universeListener: Dmx.UniverseListener? = null ) : Controller { override val controllerId: ControllerId get() = ControllerId(controllerType, device.id) - private val startedAt = clock.now() - override val state: ControllerState = - State(device.name, "N/A", startedAt) override val defaultFixtureOptions: FixtureOptions? get() = null override val transportType: TransportType @@ -30,31 +28,25 @@ class DirectDmxController( override val defaultTransportConfig: TransportConfig? get() = null private val universe = device.asUniverse(universeListener) - private var dynamicDmxAllocator: DynamicDmxAllocator? = null - override fun beforeFixtureResolution() { - dynamicDmxAllocator = DynamicDmxAllocator() - } - - override fun afterFixtureResolution() { - dynamicDmxAllocator = null - } - - override fun createTransport( - entity: Model.Entity?, - fixtureConfig: FixtureConfig, - transportConfig: TransportConfig? - ): Transport { - val staticDmxMapping = dynamicDmxAllocator!!.allocate( - fixtureConfig.componentCount, fixtureConfig.bytesPerComponent, - transportConfig as DmxTransportConfig - ) - return DirectDmxTransport(transportConfig, staticDmxMapping) + override fun createFixtureResolver(): FixtureResolver = object : FixtureResolver { + val dynamicDmxAllocator = DynamicDmxAllocator() + + override fun createTransport( + entity: Model.Entity?, + fixtureConfig: FixtureConfig, + transportConfig: TransportConfig? + ): Transport { + val staticDmxMapping = dynamicDmxAllocator.allocate( + fixtureConfig.componentCount, fixtureConfig.bytesPerComponent, + transportConfig as DmxTransportConfig + ) + return DirectDmxTransport(transportConfig, staticDmxMapping) + } } - @Serializable - data class State( + data class DirectDmxState( override val title: String, override val address: String?, override val onlineSince: Instant?, @@ -128,8 +120,6 @@ object DmxTransportType : TransportType { @SerialName("DirectDMX") data class DirectDmxControllerConfig( override val title: String = "Direct DMX", - override val fixtures: List = emptyList(), - @SerialName("defaultFixtureConfig") override val defaultFixtureOptions: FixtureOptions? = null, override val defaultTransportConfig: TransportConfig? = null ) : ControllerConfig { @@ -138,37 +128,26 @@ data class DirectDmxControllerConfig( override val emptyTransportConfig: TransportConfig get() = DmxTransportConfig() - @Transient - private var dmxAllocator: DynamicDmxAllocator? = null - - override fun edit(fixtureMappings: MutableList): MutableControllerConfig = - MutableDirectDmxControllerConfig( - title, fixtureMappings, defaultFixtureOptions?.edit(), defaultTransportConfig?.edit() - ) - - // TODO: This is pretty dumb, find a better way to do this. - override fun buildFixturePreviews(sceneOpener: SceneOpener): List { - dmxAllocator = DynamicDmxAllocator() - try { - return super.buildFixturePreviews(sceneOpener) - } finally { - dmxAllocator = null - } - } - - override fun createFixturePreview(fixtureOptions: FixtureOptions, transportConfig: TransportConfig): FixturePreview { - val staticDmxMapping = dmxAllocator!!.allocate( - fixtureOptions.componentCount ?: 1, - fixtureOptions.bytesPerComponent, - transportConfig as DmxTransportConfig - ) - val dmxPreview = staticDmxMapping.preview(DmxUniverses(1)) - - return object : FixturePreview { - override val fixtureOptions: ConfigPreview - get() = fixtureOptions.preview() - override val transportConfig: ConfigPreview - get() = dmxPreview + override fun edit(): MutableControllerConfig = + MutableDirectDmxControllerConfig(title, defaultFixtureOptions?.edit(), defaultTransportConfig?.edit()) + + override fun createPreviewBuilder(): PreviewBuilder = object : PreviewBuilder { + val dmxAllocator = DynamicDmxAllocator() + + override fun createFixturePreview(fixtureOptions: FixtureOptions, transportConfig: TransportConfig): FixturePreview { + val staticDmxMapping = dmxAllocator.allocate( + fixtureOptions.componentCount ?: error("No component count."), + fixtureOptions.bytesPerComponent, + transportConfig as DmxTransportConfig + ) + val dmxPreview = staticDmxMapping.preview(DmxUniverses(1)) + + return object : FixturePreview { + override val fixtureOptions: ConfigPreview + get() = fixtureOptions.preview() + override val transportConfig: ConfigPreview + get() = dmxPreview + } } } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/baaahs/dmx/DmxManager.kt b/shared/src/commonMain/kotlin/baaahs/dmx/DmxManager.kt index a0dc4342bc..7503f1ac62 100644 --- a/shared/src/commonMain/kotlin/baaahs/dmx/DmxManager.kt +++ b/shared/src/commonMain/kotlin/baaahs/dmx/DmxManager.kt @@ -2,13 +2,14 @@ package baaahs.dmx import baaahs.PubSub import baaahs.controller.BaseControllerManager +import baaahs.controller.Change import baaahs.controller.ControllerId import baaahs.controller.ControllerManager import baaahs.controller.ControllerState +import baaahs.fixtures.FixtureMapping import baaahs.plugin.Plugins import baaahs.scene.MutableControllerConfig import baaahs.scene.MutableDirectDmxControllerConfig -import baaahs.scene.OpenControllerConfig import baaahs.sim.FakeDmxUniverse import baaahs.util.Clock import baaahs.util.Logger @@ -33,7 +34,7 @@ interface DmxManager { ): MutableControllerConfig = MutableDirectDmxControllerConfig( state?.title ?: controllerId?.id ?: "Direct DMX", - mutableListOf(), null, null + null, null ) } } @@ -45,7 +46,7 @@ class DmxManagerImpl( pubSub: PubSub.Server, private val universeListener: DmxUniverseListener? = null, plugins: Plugins -) : BaseControllerManager(DmxManager.controllerTypeName), DmxManager { +) : BaseControllerManager(DmxManager.controllerTypeName), DmxManager { private val attachedDevices = findAttachedDevices() override val dmxUniverse = findDmxUniverse(attachedDevices) @@ -56,19 +57,40 @@ class DmxManagerImpl( }) } + override fun onChange( + controllerId: ControllerId, + oldController: DirectDmxController?, + controllerConfig: Change, + controllerState: Change, + fixtureMappings: Change> + ): DirectDmxController? { + if (controllerConfig.newValue == null && controllerState.newValue == null) return null + + val device = attachedDevices.find { it.id == controllerId.id } + ?: run { + logger.error { "No such DMX device $controllerId." } + return null + } + + return DirectDmxController(device, universeListener) + } + override fun start() { if (attachedDevices.isNotEmpty()) { attachedDevices.forEach { device -> - notifyListeners { - onAdd(DirectDmxController(device, clock, universeListener)) + val controllerId = ControllerId(controllerType, device.id) + onStateChange(controllerId) { fromState -> + DirectDmxController.DirectDmxState( + "${device.name} (${device.id})", + null, + clock.now(), + null, null + ) } } } } - override fun onConfigChange(controllerConfigs: Map>) { - } - override fun stop() { TODO("not implemented") } diff --git a/shared/src/commonMain/kotlin/baaahs/fixtures/FixtureMapping.kt b/shared/src/commonMain/kotlin/baaahs/fixtures/FixtureMapping.kt index 912b89a13c..f7d9f70582 100644 --- a/shared/src/commonMain/kotlin/baaahs/fixtures/FixtureMapping.kt +++ b/shared/src/commonMain/kotlin/baaahs/fixtures/FixtureMapping.kt @@ -1,7 +1,9 @@ package baaahs.fixtures import baaahs.controller.Controller +import baaahs.controller.FixtureResolver import baaahs.model.Model +import baaahs.scene.ControllerConfig import kotlinx.serialization.SerialName data class FixtureMapping( @@ -24,21 +26,31 @@ data class FixtureMapping( .reduce { acc, config -> acc.plus(config) } } - fun resolveTransportConfig(default: TransportConfig, controllerDefault: TransportConfig?): TransportConfig = - default + listOfNotNull( - controllerDefault, - transportConfig - ).reduceOrNull { acc, config -> acc.plus(config) } + fun resolveTransportConfig(controller: ControllerConfig): TransportConfig { + val transportDefault = controller.emptyTransportConfig + val controllerDefault = controller.defaultTransportConfig + return reduce(transportDefault, controllerDefault, this@FixtureMapping.transportConfig) + } + + fun resolveTransportConfig(controller: Controller): TransportConfig { + val transportDefault = controller.transportType.emptyConfig + val controllerDefault = controller.defaultTransportConfig + return reduce(transportDefault, controllerDefault, transportConfig) + } + + private fun reduce(vararg transportConfigs: TransportConfig?): TransportConfig { + return transportConfigs.toList().filterNotNull() + .reduce { acc, config -> acc + config } + } - fun buildFixture(controller: Controller, model: Model): Fixture { + fun buildFixture(controller: Controller, fixtureResolver: FixtureResolver, model: Model): Fixture { val fixtureOptions = resolveFixtureOptions(controller.defaultFixtureOptions) val fixtureConfig = fixtureOptions.toConfig(entity, model, defaultComponentCount = 1) val name = "${entity?.name ?: "???"}@${controller.controllerId.name()}" - val transportConfig = resolveTransportConfig( - controller.transportType.emptyConfig, controller.defaultTransportConfig) - val transport = controller.createTransport(entity, fixtureConfig, transportConfig) + val transportConfig = resolveTransportConfig(controller) + val transport = fixtureResolver.createTransport(entity, fixtureConfig, transportConfig) return Fixture(entity, fixtureConfig.componentCount, name, transport, fixtureConfig.fixtureType, fixtureConfig) } diff --git a/shared/src/commonMain/kotlin/baaahs/fixtures/FixtureOptions.kt b/shared/src/commonMain/kotlin/baaahs/fixtures/FixtureOptions.kt index 170164cf69..4310d23cdc 100644 --- a/shared/src/commonMain/kotlin/baaahs/fixtures/FixtureOptions.kt +++ b/shared/src/commonMain/kotlin/baaahs/fixtures/FixtureOptions.kt @@ -25,6 +25,13 @@ interface FixtureOptions { fun toConfig(entity: Model.Entity?, model: Model, defaultComponentCount: Int? = null): FixtureConfig } +fun FixtureOptions?.merge(options: FixtureOptions): FixtureOptions? = + when { + this == null -> options + this::class == options::class -> this + options + else -> this + } + /** Finalized configuration for a fixture. */ interface FixtureConfig { val componentCount: Int diff --git a/shared/src/commonMain/kotlin/baaahs/gl/render/ComponentRenderEngine.kt b/shared/src/commonMain/kotlin/baaahs/gl/render/ComponentRenderEngine.kt index 5bb83c44a4..58dffa4751 100644 --- a/shared/src/commonMain/kotlin/baaahs/gl/render/ComponentRenderEngine.kt +++ b/shared/src/commonMain/kotlin/baaahs/gl/render/ComponentRenderEngine.kt @@ -134,6 +134,14 @@ class ComponentRenderEngine( if (renderTargetsToRemove.isNotEmpty()) { // TODO(question for xian): ("remove TBD"), how do we do this? // renderTargets.removeAll(renderTargetsToRemove) + // ^^ doing this breaks rendering for some reason. + + // Let's just mark them as zombies for now. + for (renderTarget in renderTargetsToRemove) { + renderTarget.isZombie = true + } + + // TODO: We should reclaim dead buffer space at some point too. // context: https://github.com/baaahs/sparklemotion/pull/613/files#r1748974029 } @@ -162,7 +170,12 @@ class ComponentRenderEngine( } fun logStatus() { - logger.info { "Rendering $componentCount components for ${renderTargets.size} ${fixtureType.title} fixtures."} + val actuallyRenderedComponentCount = + renderTargets.filter { !it.isZombie }.sumOf { it.componentCount } + logger.info { + "Rendering $actuallyRenderedComponentCount components for ${renderTargets.size} ${fixtureType.title} fixtures" + + " (with ${componentCount - actuallyRenderedComponentCount} zombie components)." + } } val Int.bufWidth: Int get() = max(minTextureWidth, min(this, maxFramebufferWidth)) diff --git a/shared/src/commonMain/kotlin/baaahs/gl/render/RenderTarget.kt b/shared/src/commonMain/kotlin/baaahs/gl/render/RenderTarget.kt index 44fcdb6f7c..fd246b420f 100644 --- a/shared/src/commonMain/kotlin/baaahs/gl/render/RenderTarget.kt +++ b/shared/src/commonMain/kotlin/baaahs/gl/render/RenderTarget.kt @@ -26,6 +26,7 @@ class FixtureRenderTarget( ) : RenderTarget { var program: GlslProgram? = null private set + var isZombie = false val fixtureResults = resultStorage.getFixtureResults(fixture, component0Index) diff --git a/shared/src/commonMain/kotlin/baaahs/migrator/util.kt b/shared/src/commonMain/kotlin/baaahs/migrator/util.kt index 70b5fb49de..331570e0c7 100644 --- a/shared/src/commonMain/kotlin/baaahs/migrator/util.kt +++ b/shared/src/commonMain/kotlin/baaahs/migrator/util.kt @@ -37,6 +37,20 @@ fun MutableMap.replaceJsonObj(name: String, block: (JsonObj this[name] = block(origEl) } +fun MutableMap.editJsonObj(name: String, block: (MutableMap) -> Unit) { + val origEl = this[name] ?: buildJsonObject { } + if (origEl !is JsonObject) error("\"$name\" entry is a ${origEl::class.simpleName}, not an object.") + val mutableObj = origEl.toMutableMap() + block(mutableObj) + this[name] = mutableObj.toJsonObj() +} + +fun MutableMap.editEachJsonObj(block: (key: String, value: MutableMap) -> Unit) { + entries.toList().forEach { (key, value) -> + this[key] = value.jsonObject.edit { block(key, this) } + } +} + val JsonElement.type get(): String? = this.jsonObject["type"]?.jsonPrimitive?.contentOrNull diff --git a/shared/src/commonMain/kotlin/baaahs/model/MovingHead.kt b/shared/src/commonMain/kotlin/baaahs/model/MovingHead.kt index d4a59ec713..1dd03e2265 100644 --- a/shared/src/commonMain/kotlin/baaahs/model/MovingHead.kt +++ b/shared/src/commonMain/kotlin/baaahs/model/MovingHead.kt @@ -8,6 +8,7 @@ import baaahs.dmx.Boryli import baaahs.dmx.Dmx import baaahs.dmx.LixadaMiniMovingHead import baaahs.dmx.Shenzarpy +import baaahs.fixtures.FixtureOptions import baaahs.geom.EulerAngle import baaahs.geom.Vector3F import baaahs.scale @@ -82,6 +83,8 @@ class MovingHead( val adapter: MovingHeadAdapter, override val locator: EntityLocator = EntityLocator.next(), ) : Model.BaseEntity() { + override val defaultFixtureOptions: FixtureOptions? + get() = MovingHeadDevice.Options(adapter) override val bounds: Pair get() = transformation.position.let { it to it } diff --git a/shared/src/commonMain/kotlin/baaahs/plugin/Plugins.kt b/shared/src/commonMain/kotlin/baaahs/plugin/Plugins.kt index c7b5aaf9d4..21263729c6 100644 --- a/shared/src/commonMain/kotlin/baaahs/plugin/Plugins.kt +++ b/shared/src/commonMain/kotlin/baaahs/plugin/Plugins.kt @@ -553,10 +553,10 @@ sealed class Plugins( } polymorphic(ControllerState::class) { - subclass(BrainManager.State::class, BrainManager.State.serializer()) - subclass(DirectDmxController.State::class, DirectDmxController.State.serializer()) - subclass(SacnManager.State::class, SacnManager.State.serializer()) - subclass(NullController.State::class, NullController.State.serializer()) + subclass(BrainManager.BrainState::class, BrainManager.BrainState.serializer()) + subclass(DirectDmxController.DirectDmxState::class, DirectDmxController.DirectDmxState.serializer()) + subclass(SacnManager.SacnState::class, SacnManager.SacnState.serializer()) + subclass(NullController.NullState::class, NullController.NullState.serializer()) } } } diff --git a/shared/src/commonMain/kotlin/baaahs/scene/ControllerEditorPanel.kt b/shared/src/commonMain/kotlin/baaahs/scene/ControllerEditorPanel.kt index 4cac866b5d..df744b08ce 100644 --- a/shared/src/commonMain/kotlin/baaahs/scene/ControllerEditorPanel.kt +++ b/shared/src/commonMain/kotlin/baaahs/scene/ControllerEditorPanel.kt @@ -15,6 +15,7 @@ abstract class ControllerEditorPanel( class EditingController( val controllerId: ControllerId, val config: T, + val fixtureMappings: MutableList, val onChange: () -> Unit ) { fun getEditorPanelViews(): List = diff --git a/shared/src/commonMain/kotlin/baaahs/scene/MutableScene.kt b/shared/src/commonMain/kotlin/baaahs/scene/MutableScene.kt index 7b759dba4b..d84c470f5b 100644 --- a/shared/src/commonMain/kotlin/baaahs/scene/MutableScene.kt +++ b/shared/src/commonMain/kotlin/baaahs/scene/MutableScene.kt @@ -26,12 +26,13 @@ import baaahs.ui.View class MutableScene( val model: MutableModel, - val controllers: MutableMap + val controllers: MutableMap, + val fixtureMappings: MutableMap> ) : MutableDocument { constructor( title: String, block: MutableScene.() -> Unit = {} - ) : this(MutableModel(title, mutableListOf(), ModelUnit.default, 0f), mutableMapOf()) { + ) : this(MutableModel(title, mutableListOf(), ModelUnit.default, 0f), mutableMapOf(), mutableMapOf()) { this.block() } @@ -51,7 +52,8 @@ class MutableScene( return Scene( model = model.build(sceneBuilder), entities = sceneBuilder.entityIds.all(), - controllers = controllers.mapValues { (_, v) -> v.build(sceneBuilder) } + controllers = controllers.mapValues { (_, v) -> v.build(sceneBuilder) }, + fixtureMappings = fixtureMappings.mapValues { (_, v) -> v.map { it.build(sceneBuilder) } } ) } @@ -61,7 +63,6 @@ class MutableScene( interface MutableControllerConfig { val controllerMeta: ControllerManager.Meta var title: String - val fixtures: MutableList var defaultFixtureOptions: MutableFixtureOptions? var defaultTransportConfig: MutableTransportConfig? val supportedTransportTypes: List @@ -78,7 +79,6 @@ interface MutableControllerConfig { class MutableBrainControllerConfig( override var title: String, var address: String?, - override val fixtures: MutableList, override var defaultFixtureOptions: MutableFixtureOptions?, override var defaultTransportConfig: MutableTransportConfig? ) : MutableControllerConfig { @@ -88,9 +88,7 @@ class MutableBrainControllerConfig( get() = listOf(BrainTransportType) override fun build(sceneBuilder: SceneBuilder): ControllerConfig = - BrainControllerConfig( - title, address, fixtures.map { it.build(sceneBuilder) }, - defaultFixtureOptions?.build(), defaultTransportConfig?.build() + BrainControllerConfig(title, address, defaultFixtureOptions?.build(), defaultTransportConfig?.build() ) override fun getEditorPanels(editingController: EditingController<*>): List> = @@ -99,7 +97,6 @@ class MutableBrainControllerConfig( class MutableDirectDmxControllerConfig( override var title: String, - override val fixtures: MutableList, override var defaultFixtureOptions: MutableFixtureOptions?, override var defaultTransportConfig: MutableTransportConfig? ) : MutableControllerConfig { @@ -109,11 +106,7 @@ class MutableDirectDmxControllerConfig( get() = listOf(DmxTransportType) override fun build(sceneBuilder: SceneBuilder): ControllerConfig = - DirectDmxControllerConfig( - title, fixtures.map { it.build(sceneBuilder) }, - defaultFixtureOptions?.build(), - defaultTransportConfig?.build() - ) + DirectDmxControllerConfig(title, defaultFixtureOptions?.build(), defaultTransportConfig?.build()) override fun getEditorPanels(editingController: EditingController<*>): List> = listOf(DirectDmxControllerEditorPanel) @@ -123,7 +116,6 @@ class MutableSacnControllerConfig( override var title: String, var address: String, var universes: Int, - override val fixtures: MutableList, override var defaultFixtureOptions: MutableFixtureOptions?, override var defaultTransportConfig: MutableTransportConfig?, ) : MutableControllerConfig { @@ -134,7 +126,7 @@ class MutableSacnControllerConfig( override fun build(sceneBuilder: SceneBuilder): ControllerConfig = SacnControllerConfig( - title, address, universes, fixtures.map { it.build(sceneBuilder) }, + title, address, universes, defaultFixtureOptions?.build(), defaultTransportConfig?.build() ) diff --git a/shared/src/commonMain/kotlin/baaahs/scene/MutableSceneBuilder.kt b/shared/src/commonMain/kotlin/baaahs/scene/MutableSceneBuilder.kt index fb898d6c61..95c016c5db 100644 --- a/shared/src/commonMain/kotlin/baaahs/scene/MutableSceneBuilder.kt +++ b/shared/src/commonMain/kotlin/baaahs/scene/MutableSceneBuilder.kt @@ -20,12 +20,14 @@ class MutableSceneBuilder( fun build(): MutableScene { val model = scene.model.edit() - val controllers = scene.controllers.mapValues { (_, controllerConfig) -> - val fixtureMappings = controllerConfig.fixtures.map { it.edit() }.toMutableList() - controllerConfig.edit(fixtureMappings) - }.toMutableMap() - - return MutableScene(model, controllers) + val controllers = scene.controllers + .mapValues { (_, controllerConfig) -> controllerConfig.edit() } + .toMutableMap() + val fixtureMappings = scene.fixtureMappings + .mapValues { (_, fixtureMappings) -> fixtureMappings.map { it.edit() }.toMutableList() } + .toMutableMap() + + return MutableScene(model, controllers, fixtureMappings) } fun ModelData.edit(): MutableModel { diff --git a/shared/src/commonMain/kotlin/baaahs/scene/OpenScene.kt b/shared/src/commonMain/kotlin/baaahs/scene/OpenScene.kt index b6f487fd1e..9e16a465aa 100644 --- a/shared/src/commonMain/kotlin/baaahs/scene/OpenScene.kt +++ b/shared/src/commonMain/kotlin/baaahs/scene/OpenScene.kt @@ -15,6 +15,7 @@ import baaahs.util.Logger class OpenScene( val model: Model, val controllers: Map> = emptyMap(), + val fixtureMappings: Map> = emptyMap(), val isFallback: Boolean = false ) : OpenDocument { override val title: String @@ -23,45 +24,29 @@ class OpenScene( val allProblems: List get() = buildList { model.visit { entity -> addAll(entity.problems) } - - controllers.forEach { (controllerId, openControllerConfig) -> - openControllerConfig.controllerConfig.fixtures.forEach { data -> - val entity = data.entityId?.let { model.findEntityByNameOrNull(it) } - - if (data.entityId != null && entity == null) { - add( - Problem( - "No such entity \"${data.entityId}\".", - "No such entity \"${data.entityId}\" found in model, " + - "but there's a fixture mapping from \"${controllerId.name()}\" for it." - ) - ) - } - } - } // controllers.values.visit { controller -> addAll(controller.problems) } // fixtures.visit { fixture -> addAll(fixture.problems) } } fun resolveFixtures(controller: Controller, mappingManager: MappingManager): List { - controller.beforeFixtureResolution() - try { - return relevantFixtureMappings(controller, mappingManager).map { mapping -> - mapping.buildFixture(controller, model) - } - } finally { - controller.afterFixtureResolution() + val fixtureMappings = relevantFixtureMappings(controller.controllerId, mappingManager, getAnonymousFixtureMappings = { + controller.getAnonymousFixtureMappings() + }) + val fixtureResolver = controller.createFixtureResolver() + return fixtureMappings.map { mapping -> + mapping.buildFixture(controller, fixtureResolver, model) } } - fun relevantFixtureMappings(controller: Controller, mappingManager: MappingManager): List { - val openConfig = controllers[controller.controllerId] - val mappingsFromScene = openConfig?.fixtureMappings - ?: emptyList() - - val mappingsFromLegacy = mappingManager.findMappings(controller.controllerId) + fun relevantFixtureMappings( + controllerId: ControllerId, + mappingManager: MappingManager, + getAnonymousFixtureMappings: () -> List + ): List { + val mappingsFromScene = fixtureMappings[controllerId] ?: emptyList() + val mappingsFromLegacy = mappingManager.findMappings(controllerId) return (mappingsFromScene + mappingsFromLegacy) - .ifEmpty { controller.getAnonymousFixtureMappings() } + .ifEmpty { getAnonymousFixtureMappings() } } companion object { @@ -71,8 +56,7 @@ class OpenScene( class OpenControllerConfig( val id: ControllerId, - val controllerConfig: T, - val fixtureMappings: List + val controllerConfig: T ) { val controllerType: String get() = controllerConfig.controllerType @@ -86,6 +70,6 @@ class OpenControllerConfig( get() = controllerConfig.defaultTransportConfig override fun toString(): String { - return "OpenControllerConfig(id=$id, controllerConfig=$controllerConfig, fixtureMappings=$fixtureMappings)" + return "OpenControllerConfig(id=$id, controllerConfig=$controllerConfig)" } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/baaahs/scene/Scene.kt b/shared/src/commonMain/kotlin/baaahs/scene/Scene.kt index 082e9a686a..8d3cf08787 100644 --- a/shared/src/commonMain/kotlin/baaahs/scene/Scene.kt +++ b/shared/src/commonMain/kotlin/baaahs/scene/Scene.kt @@ -20,7 +20,8 @@ import kotlinx.serialization.modules.SerializersModule data class Scene( val model: ModelData, val entities: Map = emptyMap(), - val controllers: Map = emptyMap() + val controllers: Map = emptyMap(), + val fixtureMappings: Map> = emptyMap() ) { val title get() = model.title @@ -34,8 +35,7 @@ data class Scene( ModelData("Fallback Scene", listOf("grid"), ModelUnit.Centimeters), mapOf( "grid" to GridData("Grid", columns = 320, rows = 240, columnGap = 1.25f, rowGap = 1.25f, zigZag = true) - ), - emptyMap() + ) ) fun createTopic( @@ -58,26 +58,16 @@ data class Scene( interface ControllerConfig { val controllerType: String val title: String - val fixtures: List val defaultFixtureOptions: FixtureOptions? val emptyTransportConfig: TransportConfig val defaultTransportConfig: TransportConfig? - fun edit(fixtureMappings: MutableList): MutableControllerConfig + fun edit(): MutableControllerConfig - fun buildFixturePreviews(sceneOpener: SceneOpener): List { - return fixtures.map { fixtureMappingData -> - try { - val fixtureMapping = with (sceneOpener) { fixtureMappingData.open() } - val fixtureOptions = fixtureMapping.resolveFixtureOptions(defaultFixtureOptions) - val transportConfig = fixtureMapping.resolveTransportConfig(emptyTransportConfig, defaultTransportConfig) - createFixturePreview(fixtureOptions, transportConfig) - } catch (e: Exception) { - FixturePreviewError(e) - } - } - } + fun createPreviewBuilder(): PreviewBuilder +} +fun interface PreviewBuilder { fun createFixturePreview(fixtureOptions: FixtureOptions, transportConfig: TransportConfig): FixturePreview } diff --git a/shared/src/commonMain/kotlin/baaahs/scene/SceneOpener.kt b/shared/src/commonMain/kotlin/baaahs/scene/SceneOpener.kt index cc3f9c7599..b2bde7221b 100644 --- a/shared/src/commonMain/kotlin/baaahs/scene/SceneOpener.kt +++ b/shared/src/commonMain/kotlin/baaahs/scene/SceneOpener.kt @@ -28,13 +28,11 @@ class SceneOpener( return OpenScene( scene.model.open(), scene.controllers.mapValues { (controllerId, controllerConfig) -> - OpenControllerConfig( - controllerId, - controllerConfig, - controllerConfig.fixtures.map { fixtureMappingData -> - fixtureMappingData.open() - }) + OpenControllerConfig(controllerId, controllerConfig) }, + scene.fixtureMappings.mapValues { (_, fixtureMappings) -> + fixtureMappings.map { it.open() } + }, isFallback = scene == Scene.Fallback ) } diff --git a/shared/src/commonMain/kotlin/baaahs/scene/migration/AllSceneMigrations.kt b/shared/src/commonMain/kotlin/baaahs/scene/migration/AllSceneMigrations.kt index 9222242fe8..df939b102b 100644 --- a/shared/src/commonMain/kotlin/baaahs/scene/migration/AllSceneMigrations.kt +++ b/shared/src/commonMain/kotlin/baaahs/scene/migration/AllSceneMigrations.kt @@ -4,5 +4,6 @@ import baaahs.migrator.DataMigrator val AllSceneMigrations: List = listOf( V1_GridDirectionBackwards, - V2_ModelEntityIds + V2_ModelEntityIds, + V3_MoveFixtureMappings, ) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/baaahs/scene/migration/V3_MoveFixtureMappings.kt b/shared/src/commonMain/kotlin/baaahs/scene/migration/V3_MoveFixtureMappings.kt new file mode 100644 index 0000000000..7536be77b7 --- /dev/null +++ b/shared/src/commonMain/kotlin/baaahs/scene/migration/V3_MoveFixtureMappings.kt @@ -0,0 +1,45 @@ +package baaahs.scene.migration + +import baaahs.migrator.DataMigrator +import baaahs.migrator.edit +import baaahs.migrator.editEachJsonObj +import baaahs.migrator.editJsonObj +import baaahs.migrator.toJsonObj +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.jsonArray + +/** + * Grid directions were backwards. + */ +@Suppress("ClassName") +object V3_MoveFixtureMappings : DataMigrator.Migration(3) { + override fun migrate(from: JsonObject): JsonObject { + val fixtureMappingsByControllerId = mutableMapOf>() + + return from.edit { + // Migrate fixture mappings from within controllers to top-level map. + editJsonObj("controllers") { controllers -> + controllers.editEachJsonObj { controllerId, controller -> + val fixtureMappings = controller.remove("fixtures")?.jsonArray + if (fixtureMappings != null && fixtureMappings.isNotEmpty()) { + val controllerFixtureMappings = + fixtureMappingsByControllerId.getOrPut(controllerId) { mutableListOf() } + fixtureMappings.forEach { controllerFixtureMappings.add(it) } + } + + val defaultFixtureConfig = controller.remove("defaultFixtureConfig") + if (defaultFixtureConfig != null) { + controller["defaultFixtureOptions"] = defaultFixtureConfig + } + } + } + + this["fixtureMappings"] = fixtureMappingsByControllerId + .mapValues { (k, v) -> JsonArray(v) } + .toJsonObj() + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/baaahs/sm/brain/BrainManager.kt b/shared/src/commonMain/kotlin/baaahs/sm/brain/BrainManager.kt index b48089e13d..e9dde3600b 100644 --- a/shared/src/commonMain/kotlin/baaahs/sm/brain/BrainManager.kt +++ b/shared/src/commonMain/kotlin/baaahs/sm/brain/BrainManager.kt @@ -36,12 +36,11 @@ import kotlin.time.Duration.Companion.seconds class BrainManager( private val firmwareDaddy: FirmwareDaddy, - link: Network.Link, + private val link: Network.Link, private val networkStats: Pinky.NetworkStats, private val clock: Clock, coroutineContext: CoroutineContext -) : BaseControllerManager(controllerTypeName) { - private val controllerConfigs: MutableMap> = mutableMapOf() +) : BaseControllerManager(controllerTypeName) { private var isStartedUp = false private var mapperMessageCallback: ((MapperHelloMessage) -> Unit)? = null @@ -49,8 +48,9 @@ class BrainManager( override suspend fun receive(fromAddress: Network.Address, fromPort: Int, bytes: ByteArray) { if (!isStartedUp) return + val message = parse(bytes) CoroutineScope(coroutineContext).launch { - when (val message = parse(bytes)) { + when (message) { is BrainHelloMessage -> foundBrain(fromAddress, message) is PingMessage -> receivedPing(fromAddress, message) is MapperHelloMessage -> mapperMessageCallback?.invoke(message) @@ -68,9 +68,31 @@ class BrainManager( mapperMessageCallback = handler } - override fun onConfigChange(controllerConfigs: Map>) { - this.controllerConfigs.clear() // TODO: should apply any changes. - this.controllerConfigs.putAll(controllerConfigs) + override fun onChange( + controllerId: ControllerId, + oldController: BrainController?, + controllerConfig: Change, + controllerState: Change, + fixtureMappings: Change> + ): BrainController? { + val newConfig = controllerConfig.newValue + val newState = controllerState.newValue + + val brainId = BrainId(controllerId.id) + val address = newState?.address ?: newConfig?.address ?: return null + + if (newState?.isGoingOffline == true) { + activeBrains.remove(brainId) + return null + } + + return BrainController( + link.createAddress(address), brainId, + newState?.firmwareVersion, + newConfig?.defaultFixtureOptions, + newConfig?.defaultTransportConfig, + newState?.isSimulatedBrain == true + ).also { activeBrains[brainId] = it } } override fun start() { @@ -93,7 +115,7 @@ class BrainManager( "at $brainAddress [firmware=${msg.firmwareVersion}]" } - // Decide whether or not to tell this brain it should use a different firmware + // Decide whether to tell this brain it should use a different firmware if (firmwareDaddy.doesntLikeThisVersion(msg.firmwareVersion)) { // You need the new hotness bro logger.warn { @@ -116,31 +138,27 @@ class BrainManager( removeBrain(brainId) } - val config = controllerConfigs[brainId.asControllerId()] - val controller = BrainController( - brainAddress, brainId, msg, - config?.defaultFixtureOptions, - config?.defaultTransportConfig, - isSimulatedBrain - ) - activeBrains[brainId] = controller - notifyListeners { onAdd(controller) } + onStateChange(brainId.asControllerId()) { fromState -> + BrainState( + brainId.uuid, brainAddress.asString(), clock.now(), + msg.firmwareVersion, null, null, isSimulatedBrain + ) + } } fun removeBrain(brainId: BrainId) { - activeBrains.remove(brainId)?.let { - notifyListeners { onRemove(it) } + onStateChange(brainId.asControllerId()) { fromState -> + fromState?.copy(isGoingOffline = true) } } inner class BrainController( - public val brainAddress: Network.Address, + val brainAddress: Network.Address, private val brainId: BrainId, - private val helloMessage: BrainHelloMessage, + val firmwareVersion: String?, override val defaultFixtureOptions: FixtureOptions?, override val defaultTransportConfig: TransportConfig?, - private val isSimulatedBrain: Boolean, - private val startedAt: Instant = clock.now() + private val isSimulatedBrain: Boolean ) : Controller { override val controllerId: ControllerId get() = brainId.asControllerId() @@ -150,22 +168,19 @@ class BrainManager( var lastErrorAt: Instant? = null internal set - override val state: ControllerState get() = State( - brainId.uuid, brainAddress.asString(), startedAt, helloMessage.firmwareVersion, - lastError?.message, lastErrorAt - ) - override val transportType: TransportType get() = BrainTransportType - override fun createTransport( - entity: Model.Entity?, - fixtureConfig: FixtureConfig, - transportConfig: TransportConfig? - ): Transport = BrainTransport( - this, brainAddress, brainId, isSimulatedBrain, - transportConfig = transportConfig - ) + override fun createFixtureResolver(): FixtureResolver = object : FixtureResolver { + override fun createTransport( + entity: Model.Entity?, + fixtureConfig: FixtureConfig, + transportConfig: TransportConfig? + ): Transport = BrainTransport( + this@BrainController, brainAddress, brainId, isSimulatedBrain, + transportConfig = transportConfig + ) + } override fun getAnonymousFixtureMappings(): List { return listOf(FixtureMapping( @@ -179,13 +194,15 @@ class BrainManager( } @Serializable - data class State( + data class BrainState( override val title: String, override val address: String?, override val onlineSince: Instant?, override val firmwareVersion: String?, override val lastErrorMessage: String? = null, - override val lastErrorAt: Instant? = null + override val lastErrorAt: Instant? = null, + val isSimulatedBrain: Boolean = false, + val isGoingOffline: Boolean = false, ) : ControllerState() inner class BrainTransport( @@ -257,9 +274,12 @@ class BrainManager( udpSocket.sendUdp(brainAddress, Ports.BRAIN, message) } catch (e: Exception) { // Couldn't send to Brain? Schedule to remove it. - brainController.lastError = e - brainController.lastErrorAt = now - notifyListeners { onError(brainController) } + onStateChange(brainId.asControllerId()) { fromState -> + fromState?.copy( + lastErrorMessage = e.message, + lastErrorAt = now + ) + } // pendingBrains[brainId] = this logger.error(e) { "Error sending to $brainId, will take offline" } @@ -312,7 +332,7 @@ class BrainManager( state?.title ?: controllerId?.id ?: "brainXXXX", - null, mutableListOf(), null, null + null, null, null ) } } @@ -337,8 +357,6 @@ data class BrainInfo( data class BrainControllerConfig( override val title: String, val address: String? = null, - override val fixtures: List = emptyList(), - @SerialName("defaultFixtureConfig") override val defaultFixtureOptions: FixtureOptions? = null, override val defaultTransportConfig: TransportConfig? = null ) : ControllerConfig { @@ -347,17 +365,19 @@ data class BrainControllerConfig( override val emptyTransportConfig: TransportConfig get() = BrainTransportConfig() - override fun edit(fixtureMappings: MutableList): MutableControllerConfig = + override fun edit(): MutableControllerConfig = MutableBrainControllerConfig( - title, address, fixtureMappings, defaultFixtureOptions?.edit(), defaultTransportConfig?.edit() + title, address, defaultFixtureOptions?.edit(), defaultTransportConfig?.edit() ) - override fun createFixturePreview(fixtureOptions: FixtureOptions, transportConfig: TransportConfig): FixturePreview { - return object : FixturePreview { - override val fixtureOptions: ConfigPreview - get() = fixtureOptions.preview() - override val transportConfig: ConfigPreview - get() = transportConfig.preview() + override fun createPreviewBuilder(): PreviewBuilder = object : PreviewBuilder { + override fun createFixturePreview(fixtureOptions: FixtureOptions, transportConfig: TransportConfig): FixturePreview { + return object : FixturePreview { + override val fixtureOptions: ConfigPreview + get() = fixtureOptions.preview() + override val transportConfig: ConfigPreview + get() = transportConfig.preview() + } } } } diff --git a/shared/src/commonMain/kotlin/baaahs/sm/server/StageManager.kt b/shared/src/commonMain/kotlin/baaahs/sm/server/StageManager.kt index 4234fbcf28..423f140df0 100644 --- a/shared/src/commonMain/kotlin/baaahs/sm/server/StageManager.kt +++ b/shared/src/commonMain/kotlin/baaahs/sm/server/StageManager.kt @@ -302,6 +302,9 @@ class StageManager( } interface FrameListener { + /** Called before each frame is rendered. */ fun beforeFrame() + + /** Called after each frame has been rendered and [baaahs.gl.render.RenderTarget.sendFrame] has been called. */ fun afterFrame() } diff --git a/shared/src/commonMain/kotlin/baaahs/sm/webapi/Topics.kt b/shared/src/commonMain/kotlin/baaahs/sm/webapi/Topics.kt index 8411aa1473..236db2bf5a 100644 --- a/shared/src/commonMain/kotlin/baaahs/sm/webapi/Topics.kt +++ b/shared/src/commonMain/kotlin/baaahs/sm/webapi/Topics.kt @@ -26,7 +26,7 @@ object Topics { fun createControllerStates(plugins: Plugins) = PubSub.Topic( - "controllers", MapSerializer(ControllerId.serializer(), ControllerState.serializer()), + "controllerStates", MapSerializer(ControllerId.serializer(), ControllerState.serializer()), plugins.serialModule ) diff --git a/shared/src/commonTest/kotlin/baaahs/PinkySpec.kt b/shared/src/commonTest/kotlin/baaahs/PinkySpec.kt index f6578ffb21..2dee8da50a 100644 --- a/shared/src/commonTest/kotlin/baaahs/PinkySpec.kt +++ b/shared/src/commonTest/kotlin/baaahs/PinkySpec.kt @@ -4,6 +4,7 @@ import baaahs.app.settings.FeatureFlags import baaahs.app.settings.ObservableProvider import baaahs.client.EventManager import baaahs.controller.ControllersManager +import baaahs.controller.generify import baaahs.dmx.Dmx import baaahs.dmx.DmxManager import baaahs.fixtures.FixtureManagerImpl @@ -89,7 +90,7 @@ class PinkySpec : DescribeSpec({ } val mappingManager by value { MappingManagerImpl(mappingStore, sceneMonitor, coroutineScope) } val controllersManager by value { - ControllersManager(listOf(brainManager), mappingManager, sceneMonitor, listOf(fixtureManager)) + ControllersManager(listOf(generify(brainManager)), mappingManager, sceneMonitor, listOf(fixtureManager), pubSub, plugins) } val renderAndSendFrame by value { diff --git a/shared/src/commonTest/kotlin/baaahs/TestUtil.kt b/shared/src/commonTest/kotlin/baaahs/TestUtil.kt index b3b17f99e4..a315a1df97 100644 --- a/shared/src/commonTest/kotlin/baaahs/TestUtil.kt +++ b/shared/src/commonTest/kotlin/baaahs/TestUtil.kt @@ -27,6 +27,8 @@ import baaahs.shows.FakeGlContext import baaahs.shows.FakeShowPlayer import baaahs.util.Clock import baaahs.util.asInstant +import io.kotest.core.NamedTag +import io.kotest.core.spec.style.scopes.TestWithConfigBuilder import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactly import kotlinx.coroutines.CoroutineDispatcher @@ -118,6 +120,7 @@ val TestModelSurfaceData = testModelSurfaceData("Panel") val TestSceneData = Scene( ModelData("Test Model", listOf("panel1")), mapOf("panel1" to TestModelSurfaceData), + emptyMap(), emptyMap() ) val TestModel = TestSceneData.open().model @@ -127,10 +130,15 @@ fun modelForTest(entities: List) = Model("Test Model", entities) @Deprecated("Use sceneDataForTest().", ReplaceWith("sceneDataForTest(entities).model")) fun modelForTest(vararg entities: Model.Entity) = Model("Test Model", entities.toList()) -fun sceneDataForTest(vararg entities: EntityData, callback: MutableScene.() -> Unit = {}) = MutableScene( - MutableModel("Test Model", entities.map { it.edit() }.toMutableList(), ModelUnit.Centimeters, 0f), - mutableMapOf() -).apply(callback).build(SceneBuilder()) +fun sceneDataForTest(vararg entities: EntityData, callback: MutableScene.() -> Unit = {}) = + sceneDataForTest(entities.toList(), callback) + +fun sceneDataForTest(entities: List, callback: MutableScene.() -> Unit = {}) = + MutableScene( + MutableModel("Test Model", entities.map { it.edit() }.toMutableList(), ModelUnit.Centimeters, 0f), + mutableMapOf(), + mutableMapOf() + ).apply(callback).build(SceneBuilder()) class TestRenderContext( vararg val modelEntities: Model.Entity = arrayOf(FakeModelEntity("device1")) @@ -196,3 +204,7 @@ fun expectEmptyMap(block: () -> Map<*, *>) { val collection = block() assertEquals(0, collection.size, "Expected 0 items but have: ${collection.keys}") } + +val focused = NamedTag("Focused") +suspend fun TestWithConfigBuilder.focused(test: suspend io.kotest.core.test.TestScope.() -> kotlin.Unit): Unit = + config(tags = setOf(focused), test = test) \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/baaahs/controller/SacnIntegrationSpec.kt b/shared/src/commonTest/kotlin/baaahs/controller/SacnIntegrationSpec.kt index bc1763ac28..6fce04c29e 100644 --- a/shared/src/commonTest/kotlin/baaahs/controller/SacnIntegrationSpec.kt +++ b/shared/src/commonTest/kotlin/baaahs/controller/SacnIntegrationSpec.kt @@ -1,7 +1,6 @@ package baaahs.controller -import baaahs.FakeClock -import baaahs.ImmediateDispatcher +import baaahs.* import baaahs.controllers.FakeMappingManager import baaahs.device.PixelArrayDevice import baaahs.device.PixelFormat @@ -11,14 +10,13 @@ import baaahs.fixtures.FixtureListener import baaahs.fixtures.Transport import baaahs.geom.Vector3F import baaahs.gl.override +import baaahs.gl.testPlugins import baaahs.kotest.value import baaahs.mapper.MappingSession import baaahs.mapper.SessionMappingResults import baaahs.model.LightBarData import baaahs.net.TestNetwork -import baaahs.only import baaahs.scene.* -import baaahs.sceneDataForTest import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactly @@ -30,25 +28,30 @@ import kotlinx.coroutines.InternalCoroutinesApi class SacnIntegrationSpec : DescribeSpec({ describe("SACN integration") { val link by value { TestNetwork().link("sacn") } + val pubSub by value { FakePubSub().server } val controllerConfigs by value { mapOf() } + val fixtureMappings by value { mapOf>() } val sacn1Id by value { ControllerId(SacnManager.controllerTypeName, "sacn1") } val bar1 by value { entityData("bar1") } val bar2 by value { entityData("bar2") } val scene by value { sceneDataForTest(bar1, bar2) { - controllers.putAll(controllerConfigs) + this.controllers.putAll(controllerConfigs) + this.fixtureMappings.putAll(fixtureMappings) } } val sacnManager by value { SacnManager(link, ImmediateDispatcher, FakeClock()) } val listener by value { SpyFixtureListener() } val sacn1Fixtures by value { emptyList() } val mappingManager by value { FakeMappingManager(emptyMap()) } val openScene by value { scene.open() } + val sceneProvider by value { SceneMonitor(openScene) } val controllersManager by value { - ControllersManager(listOf(sacnManager), mappingManager, SceneMonitor(openScene), listOf(listener)) + ControllersManager( + listOf(generify(sacnManager)), mappingManager, sceneProvider, + listOf(listener), pubSub, testPlugins()) } beforeEach { controllersManager.start() - sacnManager.onConfigChange(openScene.controllers) } context("with no declared controllers") { @@ -60,9 +63,12 @@ class SacnIntegrationSpec : DescribeSpec({ context("with a controller which has two fixtures") { override(controllerConfigs) { mapOf(sacn1Id to MutableSacnControllerConfig - ("SACN Controller", "192.168.1.150", 1, sacn1Fixtures.toMutableList(), null, null) + ("SACN Controller", "192.168.1.150", 1, null, null) ) } + override(fixtureMappings) { + mapOf(sacn1Id to sacn1Fixtures.toMutableList()) + } val bar1Mapping by value { fixtureMappingData(bar1.edit(), 0, 2, false) } val bar2Mapping by value { fixtureMappingData(bar2.edit(), 6, 2, false) } @@ -104,9 +110,12 @@ class SacnIntegrationSpec : DescribeSpec({ override(bar2Bytes) { PixelColors(602, 2) } override(controllerConfigs) { mapOf(sacn1Id to MutableSacnControllerConfig( - "SACN Controller", "192.168.1.150", 2, sacn1Fixtures.toMutableList(), null, null + "SACN Controller", "192.168.1.150", 2, null, null )) } + override(fixtureMappings) { + mapOf(sacn1Id to sacn1Fixtures.toMutableList()) + } it("sends a DMX frame to multiple universes") { link.packetsToSend.size.shouldBe(2) diff --git a/shared/src/commonTest/kotlin/baaahs/controllers/ControllersManagerSpec.kt b/shared/src/commonTest/kotlin/baaahs/controllers/ControllersManagerSpec.kt index 350b4c6398..27bd9b0b62 100644 --- a/shared/src/commonTest/kotlin/baaahs/controllers/ControllersManagerSpec.kt +++ b/shared/src/commonTest/kotlin/baaahs/controllers/ControllersManagerSpec.kt @@ -1,5 +1,8 @@ +@file:OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class) + package baaahs.controllers +import baaahs.FakePubSub import baaahs.controller.ControllersManager import baaahs.describe import baaahs.device.EnumeratedPixelLocations @@ -13,11 +16,14 @@ import baaahs.fixtures.FixtureMapping import baaahs.geom.Vector3F import baaahs.gl.override import baaahs.gl.render.FixtureTypeForTest +import baaahs.gl.testPlugins import baaahs.glsl.LinearSurfacePixelStrategy import baaahs.kotest.value +import baaahs.model.EntityData import baaahs.model.FakeModelEntityData -import baaahs.model.MovingHead +import baaahs.model.MovingHeadData import baaahs.only +import baaahs.scene.MutableFixtureOptions import baaahs.scene.SceneMonitor import baaahs.sceneDataForTest import io.kotest.core.spec.style.DescribeSpec @@ -27,24 +33,29 @@ import io.kotest.matchers.properties.shouldHaveValue import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeSameInstanceAs import io.kotest.matchers.types.shouldBeTypeOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.InternalCoroutinesApi import kotlin.random.Random @Suppress("unused") class ControllersManagerSpec : DescribeSpec({ describe { + val pubSub by value { FakePubSub().server } val modelFixtureType by value { FixtureTypeForTest() } - val modelEntity by value { FakeModelEntityData("panel", modelFixtureType) } - val fakeControllerConfig by value { MutableFakeControllerConfig("controller1", mutableListOf(), null, null) } + val modelEntityData by value { FakeModelEntityData("panel", modelFixtureType) } + val defaultFixtureOptions by value { null } + val fakeControllerConfig by value { MutableFakeControllerConfig("controller1", defaultFixtureOptions, null) } val fakeControllerId by value { fakeControllerConfig.likelyControllerId } - val scene by value { sceneDataForTest(modelEntity) { + val scene by value { sceneDataForTest(modelEntityData) { controllers.putAll(mapOf(fakeControllerId to fakeControllerConfig)) } } val openScene by value { scene.open() } val model by value { openScene.model } + val modelEntity by value { model.findEntityByName(modelEntityData.title) } val legacyMappings by value { mapOf(fakeControllerId to listOf( FixtureMapping( - model.findEntityByName(modelEntity.title), + model.findEntityByName(modelEntityData.title), modelEntity.fixtureType.emptyOptions )) ) @@ -56,11 +67,12 @@ class ControllersManagerSpec : DescribeSpec({ val changes by value { fixtureListener.changes } val sceneMonitor by value { SceneMonitor(openScene) } val controllersManager by value { - ControllersManager(controllerManagers, mappingManager, sceneMonitor, listOf(fixtureListener)) + ControllersManager(controllerManagers, mappingManager, sceneMonitor, + listOf(fixtureListener), pubSub, testPlugins()) } val fakeController by value { fakeControllerMgr.controllers.only("controller") } - xcontext("when model and mapping data haven't loaded yet") { + context("when model and mapping data haven't loaded yet") { override(sceneMonitor) { SceneMonitor() } beforeEach { controllersManager.start() } @@ -97,7 +109,7 @@ class ControllersManagerSpec : DescribeSpec({ } } - xcontext("when mapping data has loaded") { + context("when mapping data has loaded") { beforeEach { mappingManager.dataHasLoaded = true controllersManager.start() @@ -122,14 +134,14 @@ class ControllersManagerSpec : DescribeSpec({ context("with no mapping results") { value(legacyMappings) { mapOf(fakeControllerId to emptyList()) } - xit("ignores the controller") { + it("ignores the controller") { fixtureListener.changes.shouldBeEmpty() } context("and the controller specifies an anonymous fixture") { override(fakeControllerConfig) { MutableFakeControllerConfig( - "controller1", mutableListOf(), null, null, + "controller1", null, null, anonymousFixtureMapping = FixtureMapping( null, PixelArrayDevice.Options( pixelCount = 3, @@ -141,7 +153,7 @@ class ControllersManagerSpec : DescribeSpec({ } val expectedPixelLocations by value { - val locations = LinearSurfacePixelStrategy(Random(1)).forUnknownEntity(3, model!!) + val locations = LinearSurfacePixelStrategy(Random(1)).forUnknownEntity(3, model) EnumeratedPixelLocations(locations) } @@ -151,7 +163,7 @@ class ControllersManagerSpec : DescribeSpec({ addedFixture.transport.shouldBeSameInstanceAs(fakeController.transport) } - xit("generates pixel positions within the model bounds") { + it("generates pixel positions within the model bounds") { addedFixture::componentCount.shouldHaveValue(3) addedFixture.fixtureConfig.shouldBeTypeOf { it.pixelLocations.shouldBe(expectedPixelLocations) @@ -160,12 +172,12 @@ class ControllersManagerSpec : DescribeSpec({ } } - xcontext("with a mapping result pointing to an entity") { + context("with a mapping result pointing to an entity") { value(legacyMappings) { mapOf( fakeControllerId to listOf( FixtureMapping( - openScene.model.findEntityByName(modelEntity.title), + modelEntity, PixelArrayDevice.Options( 3, PixelFormat.RGB8, pixelArrangement = LinearSurfacePixelStrategy(Random(1)) @@ -200,7 +212,7 @@ class ControllersManagerSpec : DescribeSpec({ mapOf( fakeControllerId to listOf( FixtureMapping( - openScene.model.findEntityByName(modelEntity.title), + openScene.model.findEntityByName(modelEntityData.title), PixelArrayDevice.Options( 3, pixelLocations = listOf( @@ -230,10 +242,8 @@ class ControllersManagerSpec : DescribeSpec({ } } - xcontext("when controller provides a default fixture config") { - value(fakeController) { - FakeController("controller1", modelFixtureType.Options(59, 3)) - } + context("when controller has a default fixture config") { + override(defaultFixtureOptions) { modelFixtureType.Options(59, 3).edit() } it("finds model entity mapping for the controller and creates a fixture with options from the model") { addedFixture.modelEntity.shouldBe(modelEntity) @@ -264,7 +274,7 @@ class ControllersManagerSpec : DescribeSpec({ } context("when controller provides a default fixture config for a different fixture type") { - value(fakeController) { FakeController("controller1", PixelArrayDevice.Options(4321)) } + override(defaultFixtureOptions) { PixelArrayDevice.Options(4321).edit() } it("ignores it, because we use the most specific fixture type to filter out others") { addedFixture.modelEntity.shouldBe(modelEntity) @@ -278,8 +288,8 @@ class ControllersManagerSpec : DescribeSpec({ } } - xcontext("when the fixture type specifies defaultPixelCount") { - value(modelEntity) { MovingHead("mover", baseDmxChannel = 1, adapter = Shenzarpy) } + context("when the fixture type specifies defaultPixelCount") { + value(modelEntityData) { MovingHeadData("mover", baseDmxChannel = 1, adapter = Shenzarpy) } it("creates an appropriate fixture") { addedFixture.modelEntity.shouldBe(modelEntity) @@ -290,8 +300,10 @@ class ControllersManagerSpec : DescribeSpec({ } } - xcontext("when the scene is closed") { + context("when the scene is closed") { + val firstController by value { fakeController } beforeEach { + firstController.run {} sceneMonitor.onChange(null) } @@ -303,6 +315,7 @@ class ControllersManagerSpec : DescribeSpec({ removedFixture.shouldBeSameInstanceAs(previouslyAddedFixture) fakeControllerMgr.controllers.shouldBeEmpty() + firstController.released.shouldBeTrue() } } } diff --git a/shared/src/commonTest/kotlin/baaahs/controllers/FakeController.kt b/shared/src/commonTest/kotlin/baaahs/controllers/FakeController.kt index bb2d4186bf..a142619130 100644 --- a/shared/src/commonTest/kotlin/baaahs/controllers/FakeController.kt +++ b/shared/src/commonTest/kotlin/baaahs/controllers/FakeController.kt @@ -15,24 +15,24 @@ class FakeController( override val defaultTransportConfig: TransportConfig? = null, private val anonymousFixtureMapping: FixtureMapping? = null ) : Controller { - override val state: ControllerState = object : ControllerState() { - override val title: String get() = TODO("not implemented") - override val address: String get() = TODO("not implemented") - override val onlineSince: Instant? get() = TODO("not implemented") - override val firmwareVersion: String get() = TODO("not implemented") - override val lastErrorMessage: String get() = TODO("Not yet implemented") - override val lastErrorAt: Instant? get() = TODO("Not yet implemented") - } override val transportType: TransportType get() = FakeTransportType lateinit var transport: FakeTransport override val controllerId: ControllerId = ControllerId(type, name) - override fun createTransport( - entity: Model.Entity?, - fixtureConfig: FixtureConfig, - transportConfig: TransportConfig? - ): Transport = FakeTransport(transportConfig).also { transport = it } + var released = false + + override fun createFixtureResolver(): FixtureResolver = object : FixtureResolver { + override fun createTransport( + entity: Model.Entity?, + fixtureConfig: FixtureConfig, + transportConfig: TransportConfig? + ): Transport = FakeTransport(transportConfig).also { transport = it } + } + + override fun release() { + released = true + } override fun getAnonymousFixtureMappings(): List = listOfNotNull(anonymousFixtureMapping) @@ -53,6 +53,15 @@ class FakeController( } } + class FakeControllerState : ControllerState() { + override val title: String get() = TODO("not implemented") + override val address: String get() = TODO("not implemented") + override val onlineSince: Instant? get() = TODO("not implemented") + override val firmwareVersion: String get() = TODO("not implemented") + override val lastErrorMessage: String get() = TODO("Not yet implemented") + override val lastErrorAt: Instant? get() = TODO("Not yet implemented") + } + companion object { const val type = "FAKE" } @@ -113,7 +122,6 @@ object FakeTransportType : TransportType { class FakeControllerConfig( override val title: String = "fake controller", - override val fixtures: List = emptyList(), override val defaultFixtureOptions: FixtureOptions? = null, override val defaultTransportConfig: TransportConfig? = null, val anonymousFixtureMapping: FixtureMapping? = null @@ -125,20 +133,16 @@ class FakeControllerConfig( val controllerId = ControllerId(controllerType, title) - override fun edit(fixtureMappings: MutableList): MutableControllerConfig = + override fun edit(): MutableControllerConfig = MutableFakeControllerConfig( - title, fixtureMappings, defaultFixtureOptions?.edit(), defaultTransportConfig?.edit() + title, defaultFixtureOptions?.edit(), defaultTransportConfig?.edit() ) - override fun createFixturePreview( - fixtureOptions: FixtureOptions, - transportConfig: TransportConfig - ): FixturePreview = TODO("not implemented") + override fun createPreviewBuilder(): PreviewBuilder = TODO("not implemented") } class MutableFakeControllerConfig( override var title: String, - override val fixtures: MutableList, override var defaultFixtureOptions: MutableFixtureOptions?, override var defaultTransportConfig: MutableTransportConfig?, val anonymousFixtureMapping: FixtureMapping? = null @@ -152,7 +156,7 @@ class MutableFakeControllerConfig( override fun build(sceneBuilder: SceneBuilder): FakeControllerConfig = FakeControllerConfig( - title, fixtures.map { it.build(sceneBuilder) }, + title, defaultFixtureOptions?.build(), defaultTransportConfig?.build(), anonymousFixtureMapping @@ -165,30 +169,31 @@ class MutableFakeControllerConfig( class FakeControllerManager( startingControllers: List = emptyList() -) : BaseControllerManager("FAKE") { +) : BaseControllerManager("FAKE") { var hasStarted: Boolean = false val controllers = startingControllers.toMutableList() override fun start() { if (hasStarted) error("Already started!") hasStarted = true - controllers.forEach { notifyListeners { onAdd(it) } } } - override fun onConfigChange(controllerConfigs: Map>) { - if (hasStarted) { - controllers.forEach { notifyListeners { onRemove(it) } } - } + override fun onChange( + controllerId: ControllerId, + oldController: FakeController?, + controllerConfig: Change, + controllerState: Change, + fixtureMappings: Change> + ): FakeController? { + if (!controllerConfig.changed) return oldController - controllers.clear() + oldController?.let { controllers.remove(it) } - controllers.addAll(controllerConfigs.values.map { config -> - with (config.controllerConfig as FakeControllerConfig) { + return controllerConfig.newValue?.let { + with(it) { FakeController(title, defaultFixtureOptions, defaultTransportConfig, anonymousFixtureMapping) + .also { controllers.add(it) } } - }) - if (hasStarted) { - controllers.forEach { notifyListeners { onAdd(it) } } } } @@ -207,7 +212,7 @@ class FakeControllerManager( state: ControllerState? ): MutableControllerConfig { val title = state?.title ?: controllerId?.id ?: "Fake" - return MutableFakeControllerConfig(title, mutableListOf(), null, null) + return MutableFakeControllerConfig(title, null, null) } } } \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/baaahs/dmx/DirectDmxControllerSpec.kt b/shared/src/commonTest/kotlin/baaahs/dmx/DirectDmxControllerSpec.kt index 545a0b92f5..79062542ab 100644 --- a/shared/src/commonTest/kotlin/baaahs/dmx/DirectDmxControllerSpec.kt +++ b/shared/src/commonTest/kotlin/baaahs/dmx/DirectDmxControllerSpec.kt @@ -1,41 +1,76 @@ package baaahs.dmx import baaahs.* -import baaahs.controller.Controller -import baaahs.device.MovingHeadDevice -import baaahs.fixtures.FixtureMapping -import baaahs.fixtures.FixtureOptions -import baaahs.fixtures.TransportConfig +import baaahs.controller.ControllerId +import baaahs.controllers.FakeMappingManager +import baaahs.gl.override import baaahs.kotest.value -import baaahs.model.Model -import baaahs.model.MovingHead +import baaahs.model.MovingHeadData +import baaahs.scene.MutableDirectDmxControllerConfig +import baaahs.scene.MutableFixtureMapping import baaahs.sim.FakeDmxUniverse import baaahs.sim.SimDmxDevice import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.* +import io.kotest.matchers.collections.shouldHaveSize @Suppress("unused") class DirectDmxControllerSpec : DescribeSpec({ describe { val testAdapter by value { TestMovingHeadAdapter(10) } - val entity by value { MovingHead("mover", adapter = testAdapter, baseDmxChannel = -1) } - val model by value { modelForTest(listOfNotNull(entity)) } - val mappingFixtureOptions by value { MovingHeadDevice.Options(testAdapter) } - val mappingTransportConfig by value { DmxTransportConfig() } - val mapping by value { - FixtureMapping(entity, mappingFixtureOptions, mappingTransportConfig) + val entity1 by value { MovingHeadData("mover1", adapter = testAdapter, baseDmxChannel = -1) } + val entity2 by value { MovingHeadData("mover2", adapter = testAdapter, baseDmxChannel = -1) } + val controllerConfig by value { DirectDmxControllerConfig() } + val dmxDevice by value { SimDmxDevice(FakeDmxUniverse()) } + val controllerId by value { ControllerId(controllerConfig.controllerType, dmxDevice.id) } + val transportConfig1 by value { MutableDmxTransportConfig(null, null, null) } + val transportConfig2 by value { MutableDmxTransportConfig(null, null, null) } + val scene by value { sceneDataForTest(entity1, entity2) { + controllers[controllerId] = MutableDirectDmxControllerConfig(dmxDevice.id, null, null) + fixtureMappings[controllerId] = mutableListOf( + MutableFixtureMapping(this.model.entities[0], null, transportConfig1), + MutableFixtureMapping(this.model.entities[1], null, transportConfig2), + ) + } } + val openScene by value { scene.open() } +// val openControllerConfig by value { openScene.controllers.getBang(controllerId, "controller") } +// val mappingFixtureOptions by value { MovingHeadDevice.Options(testAdapter) } +// val mappingTransportConfig by value { DmxTransportConfig() } +// val fixtureMapping by value { openScene.fixtureMappings.getBang(controllerId, "fixture mapping") } + val controller by value { DirectDmxController(dmxDevice) } + val fixtures by value { + openScene.resolveFixtures(controller, FakeMappingManager()) } - val controller by value { - DirectDmxController(SimDmxDevice(FakeDmxUniverse()), FakeClock()) + val fixture1 by value { fixtures[0] } + val fixture2 by value { fixtures[1] } + + it("should create both fixtures") { + fixtures.shouldHaveSize(2) + } + + context("when start channel is not specified") { + it("starts at 0") { + val transport1 = fixture1.transport as DirectDmxController.DirectDmxTransport + transport1.startChannel.shouldBe(0) + transport1.endChannel.shouldBe(9) + val transport2 = fixture2.transport as DirectDmxController.DirectDmxTransport + transport2.startChannel.shouldBe(10) + transport2.endChannel.shouldBe(19) + } } - val fixture by value { mapping.buildFixture(controller, model) } context("when start channel is specified") { + override(transportConfig1) { + MutableDmxTransportConfig(2, null, null) + } + it("starts there") { - controller.beforeFixtureResolution() - val transport = fixture.transport as DirectDmxController.DirectDmxTransport - transport.startChannel.shouldBe(0) - transport.endChannel.shouldBe(9) + val transport1 = fixture1.transport as DirectDmxController.DirectDmxTransport + transport1.startChannel.shouldBe(2) + transport1.endChannel.shouldBe(11) + val transport2 = fixture2.transport as DirectDmxController.DirectDmxTransport + transport2.startChannel.shouldBe(12) + transport2.endChannel.shouldBe(21) } } } diff --git a/shared/src/commonTest/kotlin/baaahs/fixtures/FixtureMappingSpec.kt b/shared/src/commonTest/kotlin/baaahs/fixtures/FixtureMappingSpec.kt index 57bb5bb0ea..6fe551c8f7 100644 --- a/shared/src/commonTest/kotlin/baaahs/fixtures/FixtureMappingSpec.kt +++ b/shared/src/commonTest/kotlin/baaahs/fixtures/FixtureMappingSpec.kt @@ -1,18 +1,26 @@ package baaahs.fixtures +import baaahs.* import baaahs.controller.Controller +import baaahs.controller.ControllerId import baaahs.controllers.FakeController +import baaahs.controllers.FakeMappingManager import baaahs.controllers.FakeTransportConfig -import baaahs.describe import baaahs.device.EnumeratedPixelLocations import baaahs.device.PixelArrayDevice +import baaahs.dmx.DirectDmxController import baaahs.geom.Vector3F import baaahs.gl.override import baaahs.kotest.value +import baaahs.model.EntityData import baaahs.model.LightBar -import baaahs.model.Model -import baaahs.modelForTest -import baaahs.testModelSurface +import baaahs.model.LightBarData +import baaahs.scene.FixtureMappingData +import baaahs.scene.MutableDirectDmxControllerConfig +import baaahs.scene.MutableEntity +import baaahs.scene.MutableFixtureMapping +import baaahs.sim.FakeDmxUniverse +import baaahs.sim.SimDmxDevice import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.properties.shouldHaveValue import io.kotest.matchers.shouldBe @@ -22,21 +30,27 @@ import io.kotest.matchers.types.shouldBeTypeOf class FixtureMappingSpec : DescribeSpec({ describe { context("buildFixture") { - val entity by value { testModelSurface("surface", expectedPixelCount = null) } - val model by value { modelForTest(listOfNotNull(entity)) } + val entity by value { testModelSurfaceData("surface", expectedPixelCount = null) } + val controllerId by value { ControllerId(FakeController.type, "fake") } val mappingFixtureOptions by value { PixelArrayDevice.Options() } val mappingTransportConfig by value { FakeTransportConfig(777) } - val mapping by value { FixtureMapping(entity, mappingFixtureOptions, mappingTransportConfig) } + val mapping by value { MutableFixtureMapping(entity?.edit(), mappingFixtureOptions.edit(), mappingTransportConfig?.edit()) } + val scene by value { sceneDataForTest(listOfNotNull(entity)) { + controllers[controllerId] = MutableDirectDmxControllerConfig("uart1", null, null) + fixtureMappings[controllerId] = mutableListOf(mapping) + } } + val openScene by value { scene.open() } val controllerDefaultFixtureOptions by value { null } val controllerDefaultTransportConfig by value { null } val controller by value { FakeController("fake", controllerDefaultFixtureOptions, controllerDefaultTransportConfig) } - val fixture by value { mapping.buildFixture(controller, model) } + val fixtures by value { openScene.resolveFixtures(controller, FakeMappingManager()) } + val fixture by value { fixtures.only("fixture") } context("with a mapped entity") { it("creates a fixture for that entity") { - fixture::modelEntity.shouldHaveValue(entity) + fixture.modelEntity!!.title.shouldBe(entity!!.title) fixture::componentCount.shouldHaveValue(1) fixture.fixtureConfig.shouldBeTypeOf { it::gammaCorrection.shouldHaveValue(1f) @@ -49,7 +63,7 @@ class FixtureMappingSpec : DescribeSpec({ } context("whose model entity specifies a surface fixture config") { - override(entity) { testModelSurface("surface", expectedPixelCount = 123) } + override(entity) { testModelSurfaceData("surface", expectedPixelCount = 123) } it("creates a fixture with that config") { fixture.componentCount shouldBe 123 @@ -82,7 +96,7 @@ class FixtureMappingSpec : DescribeSpec({ context("whose model entity is a pixel array") { override(entity) { - LightBar("light bar", startVertex = Vector3F.origin, endVertex = Vector3F.unit3d) + LightBarData("light bar", startVertex = Vector3F.origin, endVertex = Vector3F.unit3d) } override(mappingFixtureOptions) { PixelArrayDevice.Options(pixelCount = 3) } diff --git a/shared/src/commonTest/kotlin/baaahs/gl/render/ComponentRenderEngineSpec.kt b/shared/src/commonTest/kotlin/baaahs/gl/render/ComponentRenderEngineSpec.kt index bddc60f84e..a32af6e541 100644 --- a/shared/src/commonTest/kotlin/baaahs/gl/render/ComponentRenderEngineSpec.kt +++ b/shared/src/commonTest/kotlin/baaahs/gl/render/ComponentRenderEngineSpec.kt @@ -19,12 +19,14 @@ import baaahs.kotest.value import baaahs.model.Model import baaahs.only import baaahs.plugin.SerializerRegistrar +import baaahs.scene.EditingController import baaahs.scene.MutableFixtureOptions import baaahs.show.* import baaahs.show.Shader import baaahs.show.live.LinkedPatch import baaahs.shows.FakeGlContext import baaahs.shows.FakeShowPlayer +import baaahs.ui.View import com.danielgergely.kgl.* import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.booleans.shouldBeFalse @@ -403,6 +405,21 @@ class FixtureTypeForTest( override fun toString(): String = id + inner class MutableOptions( + var componentCount: Int?, + var bytesPerComponent: Int, + var pixelLocations: List? + ) : MutableFixtureOptions { + override val fixtureType: FixtureType + get() = this@FixtureTypeForTest + + override fun build(): FixtureOptions = + Options(componentCount, bytesPerComponent, pixelLocations) + + override fun getEditorView(editingController: EditingController<*>): View = + TODO("not implemented") + } + inner class Options( override val componentCount: Int? = null, override val bytesPerComponent: Int, @@ -411,7 +428,8 @@ class FixtureTypeForTest( override val fixtureType: FixtureType get() = this@FixtureTypeForTest - override fun edit(): MutableFixtureOptions = TODO("not implemented") + override fun edit(): MutableFixtureOptions = + MutableOptions(componentCount, bytesPerComponent, pixelLocations) override fun plus(other: FixtureOptions?): FixtureOptions = if (other == null) this diff --git a/shared/src/commonTest/kotlin/baaahs/scene/MutableSceneSpec.kt b/shared/src/commonTest/kotlin/baaahs/scene/MutableSceneSpec.kt index 632d58b7be..4312aae8ce 100644 --- a/shared/src/commonTest/kotlin/baaahs/scene/MutableSceneSpec.kt +++ b/shared/src/commonTest/kotlin/baaahs/scene/MutableSceneSpec.kt @@ -45,7 +45,7 @@ class MutableSceneSpec : DescribeSpec({ ) ) } val sacn1Config by value { - MutableSacnControllerConfig("SACN Controller", "192.168.1.150", 1, sacn1Fixtures.toMutableList(), null, null) + MutableSacnControllerConfig("SACN Controller", "192.168.1.150", 1, null, null) } val sacn1Id by value { ControllerId(SacnManager.controllerTypeName, sacn1Config.suggestId()) } override(controllerConfigs) { mapOf(sacn1Id to sacn1Config) } diff --git a/shared/src/commonTest/kotlin/baaahs/scene/OpenSceneSpec.kt b/shared/src/commonTest/kotlin/baaahs/scene/OpenSceneSpec.kt index a5637439a2..8b1be8631b 100644 --- a/shared/src/commonTest/kotlin/baaahs/scene/OpenSceneSpec.kt +++ b/shared/src/commonTest/kotlin/baaahs/scene/OpenSceneSpec.kt @@ -23,7 +23,7 @@ class OpenSceneSpec : DescribeSpec({ val transportConfig by value { DmxTransportConfig(0) } val controllerFixtureMappingData by value { null } val controllerFixtures by value { listOfNotNull(controllerFixtureMappingData) } - val controllerConfig by value { MutableFakeControllerConfig("fake", controllerFixtures.toMutableList(), null, null) } + val controllerConfig by value { MutableFakeControllerConfig("fake", null, null) } val legacyMappingData by value { null } val mappingManager by value { FakeMappingManager(mapOf(controllerConfig.likelyControllerId to listOfNotNull(legacyMappingData))) @@ -31,12 +31,15 @@ class OpenSceneSpec : DescribeSpec({ val openScene by value { sceneDataForTest(surface123) { controllers.put(controllerConfig.likelyControllerId, controllerConfig) + fixtureMappings.put(controllerConfig.likelyControllerId, controllerFixtures.toMutableList()) }.open() } val controller by value { FakeController(controllerConfig.likelyControllerId.id) } val relevantMappings by value { - openScene.relevantFixtureMappings(controller, mappingManager) + openScene.relevantFixtureMappings(controller.controllerId, mappingManager, getAnonymousFixtureMappings = { + controller.getAnonymousFixtureMappings() + }) } context("with no mappings anywhere") { diff --git a/shared/src/commonTest/kotlin/baaahs/scene/migration/V3_MoveFixtureMappingsSpec.kt b/shared/src/commonTest/kotlin/baaahs/scene/migration/V3_MoveFixtureMappingsSpec.kt new file mode 100644 index 0000000000..1499fd002f --- /dev/null +++ b/shared/src/commonTest/kotlin/baaahs/scene/migration/V3_MoveFixtureMappingsSpec.kt @@ -0,0 +1,141 @@ +package baaahs.scene.migration + +import baaahs.describe +import baaahs.gl.override +import baaahs.gl.testPlugins +import baaahs.kotest.value +import baaahs.toBeSpecified +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.core.spec.style.DescribeSpec +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject + +@Suppress("ClassName") +class V3_MoveFixtureMappingsSpec : DescribeSpec({ + describe { + val migration by value { V3_MoveFixtureMappings } + val json by value { Json { serializersModule = testPlugins().serialModule } } + val fromJson by value { toBeSpecified() } + val fromJsonObj by value { json.parseToJsonElement(fromJson) as JsonObject } + val toJsonObj by value { migration.migrate(fromJsonObj) } + + context("migration to move fixture mappings from within controllers to top level and rename a controller config key") { + override(fromJson) { + /**language=json*/ + """ + { + "model": { + "title": "Scene", + "entityIds": ["djLightbox", "grid", "grid2"] + }, + "controllers": { + "SACN:main": { + "type": "SACN", + "title": "Main", + "defaultFixtureConfig": { + "type": "PixelArray", + "pixelFormat": "GRB8" + }, + "fixtures": [ + { + "entityId": "djLightbox", + "fixtureConfig": { + "type": "PixelArray", + "pixelCount": 240 + } + } + ] + }, + "Brain:DEADBEEF": { + "type": "Brain", + "title": "DEADBEEF", + "fixtures": [ + { + "entityId": "grid" + } + ] + } + }, + "entities": { + "djLightbox": { + "type": "Grid", + "title": "DJ Lightbox" + }, + "grid": { + "type": "Grid", + "instance": 1 + }, + "grid2": { + "type": "Grid", + "instance": 2 + } + } + } + """.trimIndent() + + } + + it("moves things about") { + toJsonObj.toString().shouldEqualJson( + /**language=json*/ + """ + { + "model": { + "title": "Scene", + "entityIds": [ + "djLightbox", + "grid", + "grid2" + ] + }, + "controllers": { + "SACN:main": { + "type": "SACN", + "title": "Main", + "defaultFixtureOptions": { + "type": "PixelArray", + "pixelFormat": "GRB8" + } + }, + "Brain:DEADBEEF": { + "type": "Brain", + "title": "DEADBEEF" + } + }, + "entities": { + "djLightbox": { + "type": "Grid", + "title": "DJ Lightbox" + }, + "grid": { + "type": "Grid", + "instance": 1 + }, + "grid2": { + "type": "Grid", + "instance": 2 + } + }, + "fixtureMappings": { + "SACN:main": [ + { + "entityId": "djLightbox", + "fixtureConfig": { + "type": "PixelArray", + "pixelCount": 240 + } + } + ], + "Brain:DEADBEEF": [ + { + "entityId": "grid" + } + ] + } + } + """.trimIndent() + ) + } + } + } +}) \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/baaahs/sm/brain/BrainManagerSpec.kt b/shared/src/commonTest/kotlin/baaahs/sm/brain/BrainManagerSpec.kt index 4b545ad7ea..135cc7b854 100644 --- a/shared/src/commonTest/kotlin/baaahs/sm/brain/BrainManagerSpec.kt +++ b/shared/src/commonTest/kotlin/baaahs/sm/brain/BrainManagerSpec.kt @@ -1,13 +1,17 @@ +@file:OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class) + package baaahs.sm.brain import baaahs.* import baaahs.controller.ControllerId import baaahs.controller.ControllersManager import baaahs.controller.SpyFixtureListener +import baaahs.controller.generify import baaahs.controllers.FakeMappingManager import baaahs.device.PixelArrayDevice import baaahs.device.PixelFormat import baaahs.gl.override +import baaahs.gl.testPlugins import baaahs.kotest.value import baaahs.net.TestNetwork import baaahs.scene.MutableBrainControllerConfig @@ -19,15 +23,20 @@ import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.properties.shouldHaveValue +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.InternalCoroutinesApi class BrainManagerSpec : DescribeSpec({ describe { val link by value { TestNetwork().link("brainlink") } + val pubSub by value { FakePubSub().server } val surface1 by value { entityDataForTest("surface1") } val controllerConfigs by value { mapOf() } + val fixtureMappings by value { mapOf>() } val brain1Id by value { ControllerId(BrainManager.controllerTypeName, "brain1") } val scene by value { sceneDataForTest(surface1) { controllers.putAll(controllerConfigs) + this.fixtureMappings.putAll(fixtureMappings) } } val openScene by value { scene.open() } val brainManager by value { @@ -43,15 +52,15 @@ class BrainManagerSpec : DescribeSpec({ val brain1Fixtures by value { emptyList() } val mappingManager by value { FakeMappingManager(emptyMap()) } val controllersManager by value { - ControllersManager(listOf(brainManager), mappingManager, SceneMonitor(openScene), listOf(listener)) + ControllersManager(listOf(generify(brainManager)), mappingManager, SceneMonitor(openScene), + listOf(listener), pubSub, testPlugins()) } beforeEach { controllersManager.start() - brainManager.onConfigChange(openScene.controllers) } - xcontext("with no declared controllers") { + context("with no declared controllers") { it("no notifications are sent") { listener.added.shouldBeEmpty() } @@ -63,12 +72,14 @@ class BrainManagerSpec : DescribeSpec({ brain1Id to MutableBrainControllerConfig( "Brain 1", null, - fixtures = brain1Fixtures.toMutableList(), defaultFixtureOptions = PixelArrayDevice.MutableOptions(123, null, null, null), null ) ) } + override(fixtureMappings) { + mapOf(brain1Id to brain1Fixtures.toMutableList()) + } val surface1Mapping by value { MutableFixtureMapping( @@ -79,7 +90,7 @@ class BrainManagerSpec : DescribeSpec({ } override(brain1Fixtures) { listOf(surface1Mapping) } - xcontext("without any word from the actual controller") { + context("without any word from the actual controller") { it("no notifications are sent") { listener.added.shouldBeEmpty() } @@ -94,7 +105,7 @@ class BrainManagerSpec : DescribeSpec({ listener.added.map { it.name }.shouldContainExactly("surface1@Brain:brain1") } - xit("applies that config") { + it("applies that config") { val fixture = listener.added.only("fixture") fixture::componentCount shouldHaveValue 123 } diff --git a/shared/src/jsMain/kotlin/baaahs/client/SceneEditorClient.kt b/shared/src/jsMain/kotlin/baaahs/client/SceneEditorClient.kt index be5bc4f652..e0bee33ccc 100644 --- a/shared/src/jsMain/kotlin/baaahs/client/SceneEditorClient.kt +++ b/shared/src/jsMain/kotlin/baaahs/client/SceneEditorClient.kt @@ -7,7 +7,6 @@ import baaahs.fixtures.FixtureInfo import baaahs.plugin.ClientPlugins import baaahs.scene.MutableControllerConfig import baaahs.sm.webapi.Topics -import baaahs.subscribeProperty class SceneEditorClient( private val plugins: ClientPlugins, @@ -19,8 +18,12 @@ class SceneEditorClient( pubSub.addStateChangeListener(it) } - private val controllerStates by subscribeProperty(pubSub, Topics.createControllerStates(plugins), emptyMap()) { facade.notifyChanged() } - private val fixtures by subscribeProperty(pubSub, Topics.createFixtures(plugins), emptyList()) { facade.notifyChanged() } + private val controllerStates by pubSub.state(Topics.createControllerStates(plugins), emptyMap()) { + facade.notifyChanged() + } + private val fixtures by pubSub.state(Topics.createFixtures(plugins), emptyList()) { + facade.notifyChanged() + } inner class Facade : baaahs.ui.Facade() { val plugins: ClientPlugins diff --git a/shared/src/jsMain/kotlin/baaahs/mapper/ControllerConfigEditorView.kt b/shared/src/jsMain/kotlin/baaahs/mapper/ControllerConfigEditorView.kt index b121c79e2b..7372ba4607 100644 --- a/shared/src/jsMain/kotlin/baaahs/mapper/ControllerConfigEditorView.kt +++ b/shared/src/jsMain/kotlin/baaahs/mapper/ControllerConfigEditorView.kt @@ -2,6 +2,7 @@ package baaahs.mapper import baaahs.app.ui.appContext import baaahs.controller.ControllerId +import baaahs.fixtures.FixturePreviewError import baaahs.scene.EditingController import baaahs.scene.MutableFixtureMapping import baaahs.scene.MutableScene @@ -33,21 +34,45 @@ private val ControllerConfigEditorView = xComponent .also { props.mutableScene.controllers[props.controllerId] = it } } - val editingController = EditingController(props.controllerId, mutableControllerConfig, props.onEdit) + val mutableFixtureMappings = memo(props.mutableScene, props.controllerId) { + props.mutableScene.fixtureMappings[props.controllerId] + ?: mutableListOf() + } + + val editingController = EditingController(props.controllerId, mutableControllerConfig, mutableFixtureMappings, props.onEdit) val recentlyAddedFixtureMappingRef = ref(null) - val handleNewFixtureMappingClick by mouseEventHandler(mutableControllerConfig, props.onEdit) { + val handleNewFixtureMappingClick by mouseEventHandler(mutableFixtureMappings, props.onEdit) { val newMapping = MutableFixtureMapping(null, null, null) - mutableControllerConfig.fixtures.add(newMapping) + mutableFixtureMappings.add(newMapping) recentlyAddedFixtureMappingRef.current = newMapping props.onEdit() } val handleDeleteFixtureMapping by handler(mutableControllerConfig, props.onEdit) { fixtureMapping: MutableFixtureMapping -> - mutableControllerConfig.fixtures.remove(fixtureMapping) + mutableFixtureMappings.remove(fixtureMapping) props.onEdit() } + val sceneBuilder = SceneBuilder() + val tempScene = props.mutableScene.build(sceneBuilder) + val tempController = mutableControllerConfig.build(sceneBuilder) + val previewBuilder = tempController.createPreviewBuilder() + val sceneOpener = SceneOpener(tempScene) + .also { it.open() } + val fixturePreviews = mutableFixtureMappings.map { mapping -> + try { + val fixtureMappingData = mapping.build(sceneBuilder) + val fixtureMapping = with (sceneOpener) { fixtureMappingData.open() } + val fixtureOptions = fixtureMapping.resolveFixtureOptions(tempController.defaultFixtureOptions) + val transportConfig = fixtureMapping.resolveTransportConfig(tempController) + previewBuilder.createFixturePreview(fixtureOptions, transportConfig) + } catch (e: Exception) { + FixturePreviewError(e) + } + } + val mutableFixtureMappingToPreview = mutableFixtureMappings.zip(fixturePreviews) + Container { attrs.disableGutters = true attrs.sx { @@ -111,7 +136,7 @@ private val ControllerConfigEditorView = xComponent Typography { attrs.className = -styles.accordionPreview - +mutableControllerConfig.fixtures.joinToString(", ") { + +mutableFixtureMappings.joinToString(", ") { it.entity?.title ?: "Anonymous" } } @@ -119,13 +144,7 @@ private val ControllerConfigEditorView = xComponent AccordionDetails { attrs.className = -styles.accordionDetails - val sceneBuilder = SceneBuilder() - val tempScene = props.mutableScene.build(sceneBuilder) - val tempController = mutableControllerConfig.build(sceneBuilder) - val sceneOpener = SceneOpener(tempScene) - .also { it.open() } - val fixturePreviews = tempController.buildFixturePreviews(sceneOpener) - mutableControllerConfig.fixtures.zip(fixturePreviews).forEach { (mutableFixtureMapping, fixturePreview) -> + mutableFixtureMappingToPreview.forEach { (mutableFixtureMapping, fixturePreview) -> fixtureMappingEditor { attrs.mutableScene = props.mutableScene attrs.editingController = editingController diff --git a/shared/src/jsMain/kotlin/baaahs/mapper/ControllerConfigurerView.kt b/shared/src/jsMain/kotlin/baaahs/mapper/ControllerConfigurerView.kt index 7c50d3a007..5062dc512e 100644 --- a/shared/src/jsMain/kotlin/baaahs/mapper/ControllerConfigurerView.kt +++ b/shared/src/jsMain/kotlin/baaahs/mapper/ControllerConfigurerView.kt @@ -14,6 +14,7 @@ import baaahs.ui.components.collapsibleSearchBox import baaahs.ui.components.listAndDetail import kotlinx.css.Color import kotlinx.css.RuleSet +import kotlinx.css.backgroundColor import kotlinx.css.color import materialui.icon import mui.material.* @@ -121,7 +122,7 @@ private val ControllerConfigurerView = xComponent("Contro div(+styles.statusDot) { inlineStyles { - color = when { + backgroundColor = when { state?.lastErrorAt != null -> Color.red state?.onlineSince != null -> Color.green else -> Color.grey diff --git a/shared/src/jsMain/kotlin/baaahs/mapper/ControllerEditorStyles.kt b/shared/src/jsMain/kotlin/baaahs/mapper/ControllerEditorStyles.kt index 918b58b321..39c43e83b6 100644 --- a/shared/src/jsMain/kotlin/baaahs/mapper/ControllerEditorStyles.kt +++ b/shared/src/jsMain/kotlin/baaahs/mapper/ControllerEditorStyles.kt @@ -3,6 +3,7 @@ package baaahs.mapper import baaahs.app.ui.StyleConstants import baaahs.ui.asColor import baaahs.ui.important +import baaahs.ui.inset import baaahs.ui.selector import kotlinx.css.* import kotlinx.css.FlexDirection.column @@ -56,7 +57,8 @@ class ControllerEditorStyles(val theme: Theme) : StyleSheet("app-ui-scene-editor val statusDot by css { width = 10.px height = 10.px - backgroundColor = Color.red + backgroundColor = Color.grey + border = Border(1.px, inset, Color.lightGrey) borderRadius = 50.pct display = Display.inlineBlock } diff --git a/shared/src/jsMain/kotlin/baaahs/mapper/FixtureMappingEditorView.kt b/shared/src/jsMain/kotlin/baaahs/mapper/FixtureMappingEditorView.kt index c75c12dc63..d225445f93 100644 --- a/shared/src/jsMain/kotlin/baaahs/mapper/FixtureMappingEditorView.kt +++ b/shared/src/jsMain/kotlin/baaahs/mapper/FixtureMappingEditorView.kt @@ -65,10 +65,11 @@ private val FixtureMappingEditorView = xComponent("Fi val fixturePreview = props.fixturePreview if (fixturePreview is FixturePreviewError) { Chip { - attrs.color = ChipColor.secondary + attrs.color = ChipColor.error attrs.variant = ChipVariant.outlined attrs.label = buildElement { +"Error: ${fixturePreview.e.message}" } } + this@xComponent.logger.warn(fixturePreview.e) { "Fixture preview error." } } else { configPreview { attrs.configPreview = fixturePreview.fixtureOptions diff --git a/shared/src/jsMain/kotlin/baaahs/sim/FixturesSimulator.kt b/shared/src/jsMain/kotlin/baaahs/sim/FixturesSimulator.kt index 37c2ee0c7a..3042882d9e 100644 --- a/shared/src/jsMain/kotlin/baaahs/sim/FixturesSimulator.kt +++ b/shared/src/jsMain/kotlin/baaahs/sim/FixturesSimulator.kt @@ -83,9 +83,9 @@ class FixturesSimulator( } else { // TODO: create the appropriate controller simulators for fixtures that are mapped. val mappedEntities = buildSet { - newOpenScene.controllers.forEach { (controllerId, openControllerConfig) -> - openControllerConfig.controllerConfig.fixtures.forEach { fixtureMappingData -> - add(fixtureMappingData.entityId) + newOpenScene.fixtureMappings.forEach { (controllerId, fixtureMappings) -> + fixtureMappings.forEach { fixtureMapping -> + add(fixtureMapping.entity) } } }