PKyLH V9 pylux/cli.py# cli.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import logging from importlib import import_module def get_context(context_name): module_name = 'pylux.context.'+context_name try: context_module = import_module(module_name) except ImportError: print('Error: Context does not exist') return None else: context_class = context_module.get_context() return context_class def main(): context = get_context(CONFIG['cli']['default-context']) globals_dict = { 'PLOT_FILE': PLOT_FILE, 'CONFIG': CONFIG, 'LOG_LEVEL': LOG_LEVEL} context.set_globals(globals_dict) logging.basicConfig(level=LOG_LEVEL) print('Welcome to Pylux! Type \'h\' to view a list of commands.') while True: user_input = input('(pylux:'+context.name+') ') inputs = user_input.split(' ') if len(user_input) > 0: if inputs[0] == '::': globals_dict = context.get_globals() context = get_context(CONFIG['cli']['default-context']) context.set_globals(globals_dict) elif inputs[0][0] == ':': globals_dict = context.get_globals() context = get_context(inputs[0].split(':')[1]) context.set_globals(globals_dict) elif inputs[0] in context.commands: context.process(inputs) else: print('Error: Command doesn\'t exist.') if __name__ == 'pylux_root': main() PKAOHh{$pylux/settings.conf[cli] # The context to load on launch. Also the context accessed using the # :: command. default-context = editor # The tabulate printing format to use for help tables. Must be one of # [plain, simple, grid, fancy_grid, pipe, psql, orgtbl, rst] help-table-format = orgtbl # Whether to show UUIDs when listing fixtures. Must be True or False. show-uuids = False [plotter] # The default options loaded by plotter. These can all be changed on # a per-usage basis in the plotter context. # Printed page type, ISO standards only. A[0-4] paper-size = A4 # Printed orientation [landscape, portrait] orientation = landscape # Page margins in mm margin = 10 # Drawing scale 1:int scale = 50 # Background SVG image, centred on the plaster/centre intersection background-image = plot-background.svg # Standard line weights in mm. See USITT guidelines for information # on what each weighting is used for. line-weight-light = 0.4 line-weight-medium = 0.6 line-weight-heavy = 0.8 # Where to display the title block. [corner, sidebar, None] title-block = corner # If sidebar title is selected, the width of it as a percentage of # the page width. vertical-title-width-pc = 0.1 # If sidebar title is selected, the min and max widths of it. vertical-title-min-width = 50 vertical-title-max-width = 100 # If corner title is selected, the width and height of it as a # percentage of the page width. corner-title-width-pc = 0.25 corner-title-height-pc = 0.25 # If corner title is selected, the min and max widths and heights of it. corner-title-min-width = 70 corner-title-max-width = 120 corner-title-min-height = 40 corner-title-max-height = 80 # Centre line dasharray, probably best not to change this if you don't # know what it is. centre-line-dasharray = 4, 0.5, 1, 1.5 # Whether the centre line should extend over the page margins [True, False] centre-line-extend = False # Whether the plaster line should extend over the page margins [True, False] plaster-line-extend = False # How much space should be left between the plaster line and margin (in # real life metres) plaster-line-padding = 0.5 # Plaster line dasharray, again probably best not to change this. plaster-line-dasharray = 3, 0.7 PKM>HTg~pylux/clihelper.py# clihelper.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . class Interface: """Manage the CLI interactivity. Manage the interactive CLI lists, whereby a unique key which is presented to the user on the CLI returns an object, without the user having to specify the object itself. Attributes: option_list: a dictionary of the options presented to the user on the CLI. """ def __init__(self): """Create a dictionary for the options. Create a dictionary ready to populate with options, and add an entry for the special 'this' with the value None. """ self.option_list = {'this': None} def append(self, ref, object): """Add an object to the option list. Args: ref: the unique CLI identifier of the option being added. object: the object that should be returned if the user selects this option. """ self.option_list[ref] = object def get(self, refs): """Return the object of a user selection. Args: ref: the unique CLI identifier that the user selected. Returns: A list of objects that correspond to the references that were given. """ objects= [] if refs == 'all': for ref in self.option_list: if ref != 'this': objects.append(self.option_list[ref]) else: if refs == 'this': refs = self.option_list['this'] references = resolve_references(refs) for ref in references: objects.append(self.option_list[ref]) return objects def clear(self): """Clear the option list.""" self.option_list.clear() self.option_list['this'] = None def update_this(self, reference): """Update the 'this' special reference. Set the 'this' special reference to a specified reference. If the given reference is also 'this', do nothing as 'this' will already point to the desired reference. Args: reference: the reference that 'this' should point to. """ if reference != 'this': self.option_list['this'] = reference def resolve_references(user_input): """Parse the reference input. From a user input string of references, generate a list of integers that can then be passed to the Interface class to return objects. Parse comma separated values such as a,b,c and colon separated ranges such as a:b, or a combination of the two such as a,b:c,d:e,f. Args: user_input: the input string that the user entered. Returns: A list containing a list of integers. """ reference_list = [] all_input = user_input.split(',') for input_item in all_input: if ':' in input_item: limits = input_item.split(':') i = int(limits[0]) while i <= int(limits[1]): reference_list.append(i) i = i+1 else: reference_list.append(int(input_item)) reference_list.sort() return reference_list def resolve_input(inputs_list, number_args): """Parse user input that contains a multi-word argument. From a list of user arguments which have already been split, return a new list containing a set number of arguments, where the last argument is a multi-word argument is a multi-word argument. Args: inputs_list: a list containing strings which have been split from the user input using split(' '). number_args: the number of arguments the input should contain, excluding the action itself. For example, the add metadata action takes two arguments: the tag and value. Returns: A list containing a list of the arguments, where the last argument is a concatenation of any arguments that were left after processing the rest of the inputs list. For example, the metadata example above would return ['ma', 'tag', 'value which can be many words long']. """ i = 1 args_list = [] multiword_input = "" while i < number_args: args_list.append(inputs_list[i]) i = i+1 while number_args <= i <= len(inputs_list)-1: if multiword_input == "": multiword_input = multiword_input+inputs_list[i] else: multiword_input = multiword_input+' '+inputs_list[i] i = i+1 args_list.append(multiword_input) if args_list[-1] == '': args_list.pop(-1) return args_list def get_fixture_print(fixture): """Return a string that represents this fixture the best. If the fixture has a name tag, return that, if not and it has a type tag, return that, otherwise return the uuid. """ if 'name' in fixture.data: return fixture.data['name'] elif 'type' in fixture.data: return fixture.data['type'] else: return fixture.uuid PKT7H_eb||pylux/context.py# context.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from importlib import import_module class Context: """A context defines a set of commands that the user can access.""" def __init__(self, name): self.name = name def process_input(inputs): def init_globals(globals_dict): self.plot_file = globals_dict['PLOT_FILE'] self.config = globals_dict['CONFIG'] self.log_level = globals_dict['LOG_LEVEL'] PKAOH>|>|pylux/reference.py# reference.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . usitt_line_weights = { 'scenery' : 'line-weight-light', 'leader' : 'line-weight-light', 'dimension' : 'line-weight-light', 'masking' : 'line-weight-medium', 'drop' : 'line-weight-medium', 'centre' : 'line-weight-medium', 'plaster' : 'line-weight-medium', 'batten' : 'line-weight-heavy', 'fixture' : 'line-weight-heavy', 'architecture' : 'line-weight-heavy', 'border' : 'line-weight-heavy'} paper_sizes = { 'A0' : (841, 1189), 'A1' : (594, 841), 'A2' : (420, 594), 'A3' : (297, 420), 'A4' : (210, 297)} gel_colours = { # Rosco E-Colour+ 'Rose Pink' : '#FF40B9', 'R002' : '#FF40B9', 'Lavender Tint' : '#F5E6FF', 'R003' : '#F5E6FF', 'Medium Bastard Amber' : '#FAB8AC', 'R004' : '#FAB8AC', 'Pale Yellow' : '#FFFFDE', 'R007' : '#FFFFDE', 'Dark Salmon' : '#FF5E48', 'R008' : '#FF5E48', 'Pale Amber Gold' : '#FFD28A', 'R009' : '#FFD28A', 'Medium Yellow' : '#FFF30D', 'R010' : '#FFF30D', 'Straw Tint' : '#FFDBA1', 'R013' : '#FFDBA1', 'Deep Straw' : '#FDC819', 'R015' : '#FDC819', 'Surprise Peach' : '#CC5F3D', 'R017' : '#CC5F3D', 'Fire' : '#ED2000', 'R019' : '#ED2000', 'Medium Amber' : '#FF8A24', 'R020' : '#FF8A24', 'Gold Amber' : '#FF4800', 'R021' : '#FF4800', 'Dark Amber' : '#FF1900', 'R022' : '#FF1900', 'Scarlet' : '#F00E25', 'R024' : '#F00E25', 'Sunset Red' : '#FF4B2B', 'R025' : '#FF4B2B', 'Bright Red' : '#C70011', 'R026' : '#C70011', 'Medium Red' : '#A10000', 'R027' : '#A10000', 'Plasa Red' : '#BC0010', 'R029' : '#BC0010', 'Light Pink' : '#FFB8CE', 'R035' : '#FFB8CE', 'Medium Pink' : '#FF6E9E', 'R036' : '#FF6E9E', 'Pink Carnation' : '#FAC0D8', 'R039' : '#FAC0D8', 'Dark Magenta' : '#C5004F', 'R046' : '#C5004F', 'Rose Purple' : '#C43BFF', 'R048' : '#C43BFF', 'Medium Purple' : '#BE00D4', 'R049' : '#BE00D4', 'Light Lavender' : '#D7BAFF', 'R052' : '#D7BAFF', 'Paler Lavender' : '#E5DEFF', 'R053' : '#E5DEFF', 'Lavender' : '#9235FD', 'R058' : '#9235FD', 'Mist Blue' : '#D6E8FF', 'R061' : '#D6E8FF', 'Pale Blue' : '#B0D5F7', 'R063' : '#B0D5F7', 'Sky Blue' : '#597DFF', 'R068' : '#597DFF', 'Tokyo Blue' : '#3600B1', 'R071' : '#3600B1', 'Evening Blue' : '#4C79FF', 'R075' : '#4C79FF', 'Just Blue' : '#3700EB', 'R079' : '#3700EB', 'Deeper Blue' : '#1A00BF', 'R085' : '#1A00BF', 'Lime Green' : '#BEFF85', 'R088' : '#BEFF85', 'Moss Green' : '#00CD55', 'R089' : '#00CD55', 'Dark Yellow Green' : '#00870B', 'R090' : '#00870B', 'Spring Yellow' : '#F2FF30', 'R100' : '#F2FF30', 'Yellow' : '#FFEB0F', 'R101' : '#FFEB0F', 'Light Amber' : '#FFE74A', 'R102' : '#FFE74A', 'Straw' : '#FFE7C4', 'R103' : '#FFE7C4', 'Deep Amber' : '#FCD628', 'R104' : '#FCD628', 'Orange' : '#FF760D', 'R105' : '#FF760D', 'Primary Red' : '#DE0000', 'R106' : '#DE0000', 'Light Rose' : '#FF809F', 'R107' : '#FF809F', 'English Rose' : '#FAAD96', 'R108' : '#FAAD96', 'Light Salmon' : '#FF919C', 'R109' : '#FF919C', 'Middle Rose' : '#FFA3CA', 'R110' : '#FFA3CA', 'Dark Pink' : '#FF63A4', 'R111' : '#FF63A4', 'Magenta' : '#FF004D', 'R113' : '#FF004D', 'Peacock Blue' : '#00C9BF', 'R115' : '#00C9BF', 'Medium Blue Green' : '#009E96', 'R116' : '#009E96', 'Steel Blue' : '#A3E2FF', 'R117' : '#A3E2FF', 'Light Blue' : '#00B7FF', 'R118' : '#00B7FF', 'Dark Blue' : '#3300D9', 'R119' : '#3300D9', 'Deep Blue' : '#2800C9', 'R120' : '#2800C9', 'Leaf Green' : '#93FF54', 'R121' : '#93FF54', 'Fern Green' : '#74F55D', 'R122' : '#74F55D', 'Dark Green' : '#00AB44', 'R124' : '#00AB44', 'Mauve' : '#D400DB', 'R126' : '#D400DB', 'Smokey Pink' : '#BB334C', 'R127' : '#BB334C', 'Bright Pink' : '#FF177F', 'R128' : '#FF177F', 'Heavy Frost' : '#FFFFFF', 'R129' : '#FFFFFF', 'Clear' : '#FFFFFF', 'R130' : '#FFFFFF', 'Marine Blue' : '#02E3CC', 'R131' : '#02E3CC', 'Medium Blue' : '#5286FF', 'R132' : '#5286FF', 'Golden Amber' : '#F5632F', 'R134' : '#F5632F', 'Deep Golden Amber' : '#FF4A00', 'R135' : '#FF4A00', 'Pale Lavender' : '#E2C7FF', 'R136' : '#E2C7FF', 'Special Lavender' : '#B695FC', 'R137' : '#B695FC', 'Pale Green' : '#B4FFA8', 'R138' : '#B4FFA8', 'Primary Green' : '#009107', 'R139' : '#009107', 'Summer Blue' : '#38CAFF', 'R140' : '#38CAFF', 'Bright Blue' : '#00ACF0', 'R141' : '#00ACF0', 'Pale Violet' : '#AA96FF', 'R142' : '#AA96FF', 'Pale Navy Blue' : '#007194', 'R143' : '#007194', 'No Color Blue' : '#4FC4FF', 'R144' : '#4FC4FF', 'Apricot' : '#FF7438', 'R147' : '#FF7438', 'Bright Rose' : '#FF1472', 'R148' : '#FF1472', 'Gold Tint' : '#FFC0B5', 'R151' : '#FFC0B5', 'Pale Gold' : '#FFCAA8', 'R152' : '#FFCAA8', 'Pale Salmon' : '#FFB2BA', 'R153' : '#FFB2BA', 'Pale Rose' : '#FFB2BA', 'R154' : '#FFB2BA', 'Chocolate' : '#C57951', 'R156' : '#C57951', 'Pink' : '#FF4551', 'R157' : '#FF4551', 'Deep Orange' : '#FF5E00', 'R158' : '#FF5E00', 'No Color Straw' : '#FFFAE0', 'R159' : '#FFFAE0', 'Slate Blue' : '#4AABFF', 'R161' : '#4AABFF', 'Bastard Amber' : '#FFCFA8', 'R162' : '#FFCFA8', 'Flame Red' : '#F02225', 'R164' : '#F02225', 'Daylight Blue' : '#1CACFF', 'R165' : '#1CACFF', 'Pale Red' : '#FF3352', 'R166' : '#FF3352', 'Lilac Tint' : '#EBDAF5', 'R169' : '#EBDAF5', 'Deep Lavender' : '#DAADFF', 'R170' : '#DAADFF', 'Lagoon Blue' : '#00AACC', 'R172' : '#00AACC', 'Dark Steel Blue' : '#52B4FF', 'R174' : '#52B4FF', 'Loving Amber' : '#FAA498', 'R176' : '#FAA498', 'Chrome Orange' : '#FF9900', 'R179' : '#FF9900', 'Dark Lavender' : '#8B2BFF', 'R180' : '#8B2BFF', 'Congo Blue' : '#29007A', 'R181' : '#29007A', 'Light Red' : '#CC0000', 'R182' : '#CC0000', 'Moonlight Blue' : '#00BAF2', 'R183' : '#00BAF2', 'Cosmetic Peach' : '#FFFFFF', 'R184' : '#FFFFFF', 'Cosmetic Burgundy' : '#FFFFFF', 'R185' : '#FFFFFF', 'Cosmetic Silver Rose' : '#FFFFFF', 'R186' : '#FFFFFF', 'Cosmetic Rouge' : '#FFFFFF', 'R187' : '#FFFFFF', 'Cosmetic Highlight' : '#FFFFFF', 'R188' : '#FFFFFF', 'Cosmetic Silver Moss' : '#FFFFFF', 'R189' : '#FFFFFF', 'Cosmetic Emerald' : '#FFFFFF', 'R190' : '#FFFFFF', 'Cosmetic Aqua Blue' : '#FFFFFF', 'R191' : '#FFFFFF', 'Flesh Pink' : '#FF639F', 'R192' : '#FF639F', 'Rosy Amber' : '#FF454B', 'R193' : '#FF454B', 'Surprise Pink' : '#AC82FF', 'R194' : '#AC82FF', 'Zenith Blue' : '#0003CC', 'R195' : '#0003CC', 'True Blue' : '#00A1FF', 'R196' : '#00A1FF', 'Alice Blue' : '#1958CF', 'R197' : '#1958CF', 'Palace Blue' : '#43009C', 'R198' : '#43009C', 'Regal Blue' : '#3700EE', 'R199' : '#3700EE', 'Double CT Blue' : '#0F5BFF', 'R200' : '#0F5BFF', 'Full CT Blue' : '#73A9FF', 'R201' : '#73A9FF', '1/2 CT Blue' : '#B8D5FF', 'R202' : '#B8D5FF', '1/4 CT Blue' : '#E0EDFF', 'R203' : '#E0EDFF', 'Full CT Orange' : '#FF9B30', 'R204' : '#FF9B30', '1/2 CT Orange' : '#FFD28F', 'R205' : '#FFD28F', '1/4 CT Orange' : '#FFE6B8', 'R206' : '#FFE6B8', 'CT Orange + .3 Neutral Density' : '#A86300', 'R207' : '#A86300', 'CT Orange + .6 Neutral Density' : '#974400', 'R208' : '#974400', '.3 Neutral Density' : '#BFBDBD', 'R209' : '#BFBDBD', '.6 Neutral Density' : '#969595', 'R210' : '#969595', '.9 Neutral Density' : '#636262', 'R211' : '#636262', 'LCT Yellow' : '#FBFFD9', 'R212' : '#FBFFD9', 'White Flame Green' : '#E0FCB3', 'R213' : '#E0FCB3', 'Full Tough Spun' : '#FFFFFF', 'R214' : '#FFFFFF', 'Half Tough Spun' : '#FFFFFF', 'R215' : '#FFFFFF', 'White Diffusion' : '#FFFFFF', 'R216' : '#FFFFFF', 'Blue Diffusion' : '#FFFFFF', 'R217' : '#FFFFFF', 'Eighth CT Blue ' : '#EBF3FF', 'R218' : '#EBF3FF', 'Fluorescent Green ' : '#2EE8CF', 'R219' : '#2EE8CF', 'White Frost' : '#FFFFFF', 'R220' : '#FFFFFF', 'Blue Frost' : '#FFFFFF', 'R221' : '#FFFFFF', '1/8 CT Orange' : '#FFEAD1', 'R223' : '#FFEAD1', 'Daylight Blue Frost' : '#FFFFFF', 'R224' : '#FFFFFF', 'Neutral Density Frost' : '#FFFFFF', 'R225' : '#FFFFFF', 'U.V. Filter' : '#FFFFFF', 'R226' : '#FFFFFF', 'Brushed Silk' : '#FFFFFF', 'R228' : '#FFFFFF', 'Quarter Tough Spun' : '#FFFFFF', 'R229' : '#FFFFFF', 'Super Correction WF Green' : '#AD6824', 'R232' : '#AD6824', 'HMI To Tungsten' : '#FF8438', 'R236' : '#FF8438', 'C.I.D. to Tungsten' : '#F08F56', 'R237' : '#F08F56', 'C.S.I. to Tungsten' : '#E5B1A0', 'R238' : '#E5B1A0', 'Polarizer' : '#FFFFFF', 'R239' : '#FFFFFF', 'Fluorescent 5700K' : '#1AD8D8', 'R241' : '#1AD8D8', 'Fluorescent 4300K' : '#5AE2C7', 'R242' : '#5AE2C7', 'Fluorescent 3600K' : '#87E5B6', 'R243' : '#87E5B6', 'Plus Green' : '#E0FC90', 'R244' : '#E0FC90', 'Half Plus Green' : '#EAFCB8', 'R245' : '#EAFCB8', 'Quarter Plus Green' : '#F0FCD2', 'R246' : '#F0FCD2', 'Minus Green' : '#FFB8D0', 'R247' : '#FFB8D0', 'Half Minus Green' : '#FACDE0', 'R248' : '#FACDE0', 'Quarter Minus Green' : '#FADEE8', 'R249' : '#FADEE8', 'Half White Diffusion' : '#FFFFFF', 'R250' : '#FFFFFF', 'Quarter White Diffusion' : '#FFFFFF', 'R251' : '#FFFFFF', 'Eighth White Diffusion' : '#FFFFFF', 'R252' : '#FFFFFF', 'Hanover Frost' : '#FFFFFF', 'R253' : '#FFFFFF', 'HT New Hanover Frost' : '#FFFFFF', 'R254' : '#FFFFFF', 'Haarlem Frost' : '#FFFFFF', 'R255' : '#FFFFFF', 'Half Hanover Frost' : '#FFFFFF', 'R256' : '#FFFFFF', 'Quarter Hanover Frost' : '#FFFFFF', 'R257' : '#FFFFFF', 'Eighth Hanover Frost' : '#FFFFFF', 'R258' : '#FFFFFF', 'Heat Shield' : '#FFFFFF', 'R269' : '#FFFFFF', 'Scrim' : '#FFFFFF', 'R270' : '#FFFFFF', 'Mirror Silver' : '#FFFFFF', 'R271' : '#FFFFFF', 'Soft Gold Reflector' : '#FFFFFF', 'R272' : '#FFFFFF', 'Soft Silver Reflector' : '#FFFFFF', 'R273' : '#FFFFFF', 'Mirror Gold' : '#FFFFFF', 'R274' : '#FFFFFF', 'Black Scrim' : '#FFFFFF', 'R275' : '#FFFFFF', 'Eighth Plus Green' : '#F6FFE0', 'R278' : '#F6FFE0', 'Eighth Minus Green' : '#FCE8F3', 'R279' : '#FCE8F3', 'Three Quarter CT Blue' : '#9CC5FF', 'R281' : '#9CC5FF', '1.5 CT Blue' : '#759EE5', 'R283' : '#759EE5', '3/4 CT Orange' : '#F7AF5C', 'R285' : '#F7AF5C', '1.5 CT Orange' : '#F8963E', 'R286' : '#F8963E', 'Double CT Orange' : '#F77F1E', 'R287' : '#F77F1E', '.15 Neutral Density' : '#DCD9D9', 'R298' : '#DCD9D9', '1.2 Neutral Density' : '#474747', 'R299' : '#474747', 'Soft Green' : '#02E59A', 'R322' : '#02E59A', 'Jade' : '#02E2A3', 'R323' : '#02E2A3', 'Mallard Green' : '#005C46', 'R325' : '#005C46', 'Forest Green' : '#006539', 'R327' : '#006539', 'Follies Pink' : '#FF33A0', 'R328' : '#FF33A0', 'Special Rose Pink' : '#FF0D6A', 'R332' : '#FF0D6A', 'Plum' : '#CD9BD1', 'R341' : '#CD9BD1', 'Special Medium Lavender' : '#7345FF', 'R343' : '#7345FF', 'Violet' : '#A98AFF', 'R344' : '#A98AFF', 'Fuschia Pink' : '#C953DB', 'R345' : '#C953DB', 'Glacier Blue' : '#00A6FF', 'R352' : '#00A6FF', 'Lighter Blue' : '#54D5FF', 'R353' : '#54D5FF', 'Special Steel Blue ' : '#00BFD8', 'R354' : '#00BFD8', 'Special Medium Blue' : '#0236DF', 'R363' : '#0236DF', 'Cornflower' : '#5783CF', 'R366' : '#5783CF', 'Rolux' : '#FFFFFF', 'R400' : '#FFFFFF', 'Light Rolux' : '#FFFFFF', 'R401' : '#FFFFFF', 'Soft Frost' : '#FFFFFF', 'R402' : '#FFFFFF', 'Half Soft Frost' : '#FFFFFF', 'R404' : '#FFFFFF', 'Opal Frost' : '#FFFFFF', 'R410' : '#FFFFFF', 'Highlight' : '#FFFFFF', 'R414' : '#FFFFFF', 'Three Quarter White' : '#FFFFFF', 'R416' : '#FFFFFF', 'Light Opal Frost' : '#FFFFFF', 'R420' : '#FFFFFF', 'Quiet Frost' : '#FFFFFF', 'R429' : '#FFFFFF', 'Grid Cloth' : '#FFFFFF', 'R430' : '#FFFFFF', 'Light Grid Cloth' : '#FFFFFF', 'R432' : '#FFFFFF', 'Quarter Grid Cloth' : '#FFFFFF', 'R434' : '#FFFFFF', 'Full CT Straw' : '#F7BF4F', 'R441' : '#F7BF4F', 'Half CT Straw' : '#FFCE9C', 'R442' : '#FFCE9C', 'Quarter CT Straw' : '#FFE3BA', 'R443' : '#FFE3BA', 'Eighth CT Straw' : '#FFF5DC', 'R444' : '#FFF5DC', 'Three Eighths White' : '#FFFFFF', 'R450' : '#FFFFFF', 'One Sixteenth White' : '#FFFFFF', 'R452' : '#FFFFFF', 'Quiet Grid Cloth' : '#FFFFFF', 'R460' : '#FFFFFF', 'Quiet Light Grid Cloth' : '#FFFFFF', 'R462' : '#FFFFFF', 'Quiet Quarter Grid Cloth' : '#FFFFFF', 'R464' : '#FFFFFF', 'Full Atlantic Frost' : '#FFFFFF', 'R480' : '#FFFFFF', 'Half Atlantic Frost' : '#FFFFFF', 'R481' : '#FFFFFF', 'Quarter Atlantic Frost' : '#FFFFFF', 'R482' : '#FFFFFF', 'Double New Colour Blue' : '#6977FF', 'R500' : '#6977FF', 'New Colour Blue (Robertson Blue)' : '#BFC7FB', 'R501' : '#BFC7FB', 'Half New Colour Blue' : '#D9E3FF', 'R502' : '#D9E3FF', 'Quarter New Colour Blue' : '#F0F5FF', 'R503' : '#F0F5FF', 'Waterfront Green' : '#B3DCE3', 'R504' : '#B3DCE3', 'Sally Green' : '#BFFF59', 'R505' : '#BFFF59', 'Marlene' : '#F7C9A3', 'R506' : '#F7C9A3', 'Madge' : '#E93511', 'R507' : '#E93511', 'Midnight Maya' : '#1602AA', 'R508' : '#1602AA', 'Argent Blue' : '#2261D6', 'R525' : '#2261D6', 'Gold Medal' : '#F5AE3F', 'R550' : '#F5AE3F', 'Full CT Eight Five' : '#FFC470', 'R604' : '#FFC470', 'Half Mustard Yellow' : '#DFAB00', 'R642' : '#DFAB00', 'Quarter Mustard Yellow' : '#FDC200', 'R643' : '#FDC200', 'Industry Sodium' : '#D9CE73', 'R650' : '#D9CE73', 'HI Sodium' : '#FFB95C', 'R651' : '#FFB95C', 'Urban Sodium' : '#FF752B', 'R652' : '#FF752B', 'LO Sodium' : '#5E2A02', 'R653' : '#5E2A02', 'Perfect Lavender' : '#7500EB', 'R700' : '#7500EB', 'Provence' : '#9A3BFF', 'R701' : '#9A3BFF', 'Special Pale Lavender' : '#DACCFF', 'R702' : '#DACCFF', 'Cold Lavender' : '#C587FF', 'R703' : '#C587FF', 'Lily' : '#E2BAFF', 'R704' : '#E2BAFF', 'Lily Frost' : '#D59EFF', 'R705' : '#D59EFF', 'King Fals Lavender' : '#6600FF', 'R706' : '#6600FF', 'Ultimate Violet' : '#7500F2', 'R707' : '#7500F2', 'Cool Lavender' : '#BFC8FF', 'R708' : '#BFC8FF', 'Electric Lilac' : '#7394FF', 'R709' : '#7394FF', 'Spir Special Blue' : '#554CFF', 'R710' : '#554CFF', 'Cold Blue' : '#224ED4', 'R711' : '#224ED4', 'Bedford Blue' : '#3853FF', 'R712' : '#3853FF', 'Winter Blue' : '#1F009A', 'R713' : '#1F009A', 'Elysian Blue' : '#0F17FF', 'R714' : '#0F17FF', 'Cabanna Blue' : '#072EDE', 'R715' : '#072EDE', 'Mikkel Blue' : '#2600BF', 'R716' : '#2600BF', 'Shanklin Frost' : '#FFFFFF', 'R717' : '#FFFFFF', '1/2 Shanklin Frost' : '#FFFFFF', 'R718' : '#FFFFFF', 'Colour Wash Blue' : '#2265F5', 'R719' : '#2265F5', 'Daylight Frost' : '#FFFFFF', 'R720' : '#FFFFFF', 'Berry Blue' : '#0036E8', 'R721' : '#0036E8', 'Bray Blue' : '#0024C2', 'R722' : '#0024C2', 'Virgin Blue' : '#0031F7', 'R723' : '#0031F7', 'Ocean Blue' : '#2BC7FF', 'R724' : '#2BC7FF', 'Old Steel Blue' : '#8CDFFF', 'R725' : '#8CDFFF', 'QFD Blue' : '#007385', 'R727' : '#007385', 'Steel Green' : '#95DEDA', 'R728' : '#95DEDA', 'Scuba Blue' : '#007070', 'R729' : '#007070', 'Liberty Green' : '#A3F7DB', 'R730' : '#A3F7DB', 'Dirty Ice' : '#B4F0D2', 'R731' : '#B4F0D2', 'Damp Squib' : '#A8E5C7', 'R733' : '#A8E5C7', 'Velvet Green' : '#005C1D', 'R735' : '#005C1D', 'Twickenham Green' : '#0D5700', 'R736' : '#0D5700', 'Jas Green' : '#5FE300', 'R738' : '#5FE300', 'Aurora Borealis Green' : '#354D15', 'R740' : '#354D15', 'Mustard Yellow' : '#C5A100', 'R741' : '#C5A100', 'Bram Brown' : '#8E5324', 'R742' : '#8E5324', 'Dirty White' : '#F7C757', 'R744' : '#F7C757', 'Brown ' : '#753900', 'R746' : '#753900', 'Easy White' : '#CC8C7C', 'R747' : '#CC8C7C', 'Seedy Pink' : '#C23061', 'R748' : '#C23061', 'Hanover Rose' : '#FFBCBA', 'R749' : '#FFBCBA', 'Durham Frost' : '#FFFFFF', 'R750' : '#FFFFFF', 'Wheat' : '#FFEFBA', 'R763' : '#FFEFBA', 'Sun Colour Straw' : '#FFEC94', 'R764' : '#FFEC94', 'Sunlight Yellow' : '#FFEC6E', 'R765' : '#FFEC6E', 'Oklahoma Yellow' : '#FFDE24', 'R767' : '#FFDE24', 'Egg Yolk Yellow' : '#FCC200', 'R768' : '#FCC200', 'Burnt Yellow' : '#FF8A0D', 'R770' : '#FF8A0D', 'Cardbox Amber' : '#FFB28F', 'R773' : '#FFB28F', 'Soft Amber' : '#FFC49C', 'R774' : '#FFC49C', 'Soft Amber 2' : '#FFBA8C', 'R775' : '#FFBA8C', 'Nectarine' : '#FF8345', 'R776' : '#FF8345', 'Rust' : '#D94F18', 'R777' : '#D94F18', 'Millennium Gold' : '#FF4405', 'R778' : '#FF4405', 'Bastard Pink' : '#F56A2F', 'R779' : '#F56A2F', 'As Golden Amber' : '#FF3B05', 'R780' : '#FF3B05', 'Terry Red' : '#FF0F0D', 'R781' : '#FF0F0D', 'Marius Red' : '#91001B', 'R787' : '#91001B', 'Blood Red' : '#99000D', 'R789' : '#99000D', 'Moroccan Pink' : '#FF919C', 'R790' : '#FF919C', 'Moroccan Frost' : '#FFFFFF', 'R791' : '#FFFFFF', 'Vanity Fair' : '#FF12AC', 'R793' : '#FF12AC', 'Pretty N Pink' : '#FF82DE', 'R794' : '#FF82DE', 'Magical Magenta' : '#FF00C8', 'R795' : '#FF00C8', 'Deep Purple' : '#AD00CC', 'R797' : '#AD00CC', 'Chrysalis Pink' : '#7B0FFF', 'R798' : '#7B0FFF', 'Special K.H. Lavender' : '#120096', 'R799' : '#120096', 'Damson Violet' : '#8800C7', 'R5084' : '#8800C7', 'French Lilac' : '#6D00F2', 'R5085' : '#6D00F2', 'Max Blue' : '#B8D4FF', 'R5202' : '#B8D4FF', 'Ice Blue' : '#E8F4FF', 'R5211' : '#E8F4FF', 'Venetian Blue' : '#96C9FF', 'R5264' : '#96C9FF', 'Fuji Blue' : '#002DE3', 'R5287' : '#002DE3', 'Aztec Gold' : '#F2CF88', 'R5336' : '#F2CF88', 'Wisteria' : '#DFCFFF', 'R5404' : '#DFCFFF', 'Olympia Green' : '#009C72', 'R5454' : '#009C72', 'Tarragon' : '#7DFFB1', 'R5455' : '#7DFFB1', 'Grotto Green' : '#02BF9C', 'R5461' : '#02BF9C', 'Prussian Green' : '#00A6B5', 'R5463' : '#00A6B5', # Rosco Supergel 'Dempster Open White' : '#FFFFFF', 'S00' : '#FFFFFF', 'Light Bastard Amber' : '#FBB39A', 'S01' : '#FBB39A', 'Bastard Amber' : '#FFD1AC', 'S02' : '#FFD1AC', 'Dark Bastard Amber' : '#FBBA9A', 'S03' : '#FBBA9A', 'Warm Peach' : '#FF8A4A', 'S303' : '#FF8A4A', 'Medium Bastard Amber' : '#F9B09A', 'S04' : '#F9B09A', 'Pale Apricot' : '#FABCA9', 'S304' : '#FABCA9', 'Rose Tint' : '#FFD7D3', 'S05' : '#FFD7D3', 'Rose Gold' : '#F5BAB8', 'S305' : '#F5BAB8', 'No Color Straw' : '#FCFADB', 'S06' : '#FCFADB', 'Pale Yellow ' : '#FDFAD1', 'S07' : '#FDFAD1', 'Pale Amber Gold' : '#FFCB86', 'S09' : '#FFCB86', 'Medium Yellow' : '#FFF200', 'S10' : '#FFF200', 'Light Straw' : '#FFD21A', 'S11' : '#FFD21A', 'Canary' : '#FFEA00', 'S312' : '#FFEA00', 'Straw Tint ' : '#FFD88F', 'S13' : '#FFD88F', 'Light Relief Yellow' : '#FFE462', 'S313' : '#FFE462', 'Medium Straw' : '#FCD419', 'S14' : '#FCD419', 'Deep Straw ' : '#FECB00', 'S15' : '#FECB00', 'Apricot' : '#FF7418', 'S317' : '#FF7418', 'Mayan Sun' : '#FF6F29', 'S318' : '#FF6F29', 'Fire' : '#FF390B', 'S19' : '#FF390B', 'Medium Amber' : '#FF871C', 'S20' : '#FF871C', 'Golden Amber' : '#FF6613', 'S21' : '#FF6613', 'Deep Amber' : '#FF430A', 'S22' : '#FF430A', 'Orange' : '#FF5A00', 'S23' : '#FF5A00', 'Scarlet' : '#F50014', 'S24' : '#F50014', 'Gypsy Red' : '#F50F39', 'S324' : '#F50F39', 'Orange Red' : '#E51F00', 'S25' : '#E51F00', 'Light Red' : '#D70229', 'S26' : '#D70229', 'Medium Red' : '#B00202', 'S27' : '#B00202', 'Light Salmon Pink' : '#FF7A59', 'S30' : '#FF7A59', 'Salmon Pink' : '#FF847F', 'S31' : '#FF847F', 'Shell Pink' : '#FF9D8D', 'S331' : '#FF9D8D', 'Medium Salmon Pink' : '#FF413C', 'S32' : '#FF413C', 'Cherry Rose' : '#FF2957', 'S332' : '#FF2957', 'No Color Pink' : '#FFC2D0', 'S33' : '#FFC2D0', 'Light Pink' : '#FFA7BB', 'S35' : '#FFA7BB', 'Medium Pink' : '#FF6D96', 'S36' : '#FF6D96', 'Billington Pink' : '#FF73B7', 'S336' : '#FF73B7', 'True Pink' : '#FFAFC2', 'S337' : '#FFAFC2', 'Light Rose' : '#FFBBE2', 'S38' : '#FFBBE2', 'Broadway Pink' : '#FF1283', 'S339' : '#FF1283', 'Skelton Exotic Sangria' : '#E800BC', 'S39' : '#E800BC', 'Light Salmon' : '#FF4F1F', 'S40' : '#FF4F1F', 'Rose Pink' : '#FF1562', 'S342' : '#FF1562', 'Deep Pink' : '#FF3E93', 'S43' : '#FF3E93', 'Neon Pink' : '#FF397F', 'S343' : '#FF397F', 'Follies Pink' : '#FF05D3', 'S344' : '#FF05D3', 'Rose' : '#EB016D', 'S45' : '#EB016D', 'Magenta' : '#BD045D', 'S46' : '#BD045D', 'Tropical Magenta' : '#FF2DD5', 'S346' : '#FF2DD5', 'Light Rose Purple' : '#CC4EB9', 'S47' : '#CC4EB9', 'Belladonna Rose' : '#B101DD', 'S347' : '#B101DD', 'Rose Purple' : '#C800CF', 'S48' : '#C800CF', 'Purple Jazz' : '#DA2DFF', 'S348' : '#DA2DFF', 'Medium Purple' : '#C900E6', 'S49' : '#C900E6', 'Fisher Fuchsia' : '#F000FF', 'S349' : '#F000FF', 'Mauve' : '#BB002C', 'S50' : '#BB002C', 'Lavender Mist' : '#EFDCFF', 'S351' : '#EFDCFF', 'Light Lavender' : '#DDBFFF', 'S52' : '#DDBFFF', 'Pale Lavender' : '#E4DCFF', 'S53' : '#E4DCFF', 'Lilly Lavender' : '#C4ADFF', 'S353' : '#C4ADFF', 'Special Lavender' : '#E6C7FF', 'S54' : '#E6C7FF', 'Lilac' : '#C0AAFD', 'S55' : '#C0AAFD', 'Pale Violet' : '#A590FF', 'S355' : '#A590FF', 'Gypsy Lavender' : '#8C2FFF', 'S56' : '#8C2FFF', 'Middle Lavender' : '#C38DFF', 'S356' : '#C38DFF', 'Lavender' : '#B482FF', 'S57' : '#B482FF', 'Royal Lavender' : '#8A2BFF', 'S357' : '#8A2BFF', 'Deep Lavender' : '#933FFD', 'S58' : '#933FFD', 'Rose Indigo' : '#8E0AEA', 'S358' : '#8E0AEA', 'Indigo' : '#7200FF', 'S59' : '#7200FF', 'Medium Violet' : '#683FFF', 'S359' : '#683FFF', 'Mist Blue' : '#D3EAFF', 'S61' : '#D3EAFF', 'Hemsley Blue' : '#669EFC', 'S361' : '#669EFC', 'Booster Blue' : '#A1CEFF', 'S62' : '#A1CEFF', 'Pale Blue' : '#A4D3FF', 'S63' : '#A4D3FF', 'Aquamarine' : '#ABE9FF', 'S363' : '#ABE9FF', 'Light Steel Blue' : '#50AEFD', 'S64' : '#50AEFD', 'Daylight Blue' : '#00A9FF', 'S65' : '#00A9FF', 'Cool Blue' : '#94EAFF', 'S66' : '#94EAFF', 'Jordan Blue' : '#29C0F9', 'S366' : '#29C0F9', 'Light Sky Blue' : '#14A9FF', 'S67' : '#14A9FF', 'Slate Blue' : '#44A5FF', 'S367' : '#44A5FF', 'Parry Sky Blue' : '#447DFF', 'S68' : '#447DFF', 'Winkler Blue' : '#448AFF', 'S368' : '#448AFF', 'Brilliant Blue' : '#00A3F7', 'S69' : '#00A3F7', 'Tahitian Blue' : '#00C6FF', 'S369' : '#00C6FF', 'Nile Blue' : '#6CE5FF', 'S70' : '#6CE5FF', 'Italian Blue' : '#01CDDF', 'S370' : '#01CDDF', 'Sea Blue' : '#0096C7', 'S71' : '#0096C7', 'Theatre Booster 1' : '#A3A8FF', 'S371' : '#A3A8FF', 'Azure Blue' : '#55CCFF', 'S72' : '#55CCFF', 'Theatre Booster 2' : '#D9DCFF', 'S372' : '#D9DCFF', 'Peacock Blue' : '#00A4B8', 'S73' : '#00A4B8', 'Theatre Booster 3' : '#E0E9FD', 'S373' : '#E0E9FD', 'Night Blue' : '#4200FF', 'S74' : '#4200FF', 'Sea Green' : '#01A4A6', 'S374' : '#01A4A6', 'Twilight Blue' : '#007AAC', 'S75' : '#007AAC', 'Light Green Blue' : '#005773', 'S76' : '#005773', 'Iris Purple' : '#7124FF', 'S377' : '#7124FF', 'Trudy Blue' : '#6F6FFF', 'S78' : '#6F6FFF', 'Bright Blue' : '#1626FF', 'S79' : '#1626FF', 'Primary Blue' : '#0048FF', 'S80' : '#0048FF', 'Urban Blue' : '#486FFF', 'S81' : '#486FFF', 'Surprise Blue' : '#4F34F8', 'S82' : '#4F34F8', 'Congo Blue' : '#250070', 'S382' : '#250070', 'Medium Blue' : '#0228EC', 'S83' : '#0228EC', 'Sapphire Blue' : '#0022D1', 'S383' : '#0022D1', 'Zephyr Blue' : '#5767FF', 'S84' : '#5767FF', 'Midnight Blue' : '#0500D0', 'S384' : '#0500D0', 'Deep Blue' : '#0049CE', 'S85' : '#0049CE', 'Royal Blue' : '#4F02CF', 'S385' : '#4F02CF', 'Pea Green' : '#89FA19', 'S86' : '#89FA19', 'Leaf Green' : '#7BD300', 'S386' : '#7BD300', 'Gaslight Green' : '#D0F54E', 'S388' : '#D0F54E', 'Moss Green' : '#51F655', 'S89' : '#51F655', 'Chroma Green' : '#29F433', 'S389' : '#29F433', 'Dark Yellow Green' : '#007F06', 'S90' : '#007F06', 'Primary Green' : '#005E2C', 'S91' : '#005E2C', 'Pacific Green' : '#009493', 'S392' : '#009493', 'Blue Green' : '#01A3A0', 'S93' : '#01A3A0', 'Emerald Green' : '#007150', 'S393' : '#007150', 'Kelly Green' : '#00985D', 'S94' : '#00985D', 'Medium Blue Green' : '#009C91', 'S95' : '#009C91', 'Teal Green' : '#00726A', 'S395' : '#00726A', 'Lime' : '#F3FF6B', 'S96' : '#F3FF6B', 'Neutral Grey' : '#B0B4B9', 'S398' : '#B0B4B9', 'Frost' : '#FFFFFF', 'S100' : '#FFFFFF', 'Light Frost' : '#FFFFFF', 'S101' : '#FFFFFF', 'Tough Silk' : '#FFFFFF', 'S104' : '#FFFFFF', 'Matte Silk' : '#FFFFFF', 'S113' : '#FFFFFF', 'Hamburg Frost' : '#FFFFFF', 'S114' : '#FFFFFF', 'Light Hamburg Frost' : '#FFFFFF', 'S119' : '#FFFFFF', 'Red Diffusion' : '#FFFFFF', 'S120' : '#FFFFFF', 'Blue Diffusion' : '#FFFFFF', 'S121' : '#FFFFFF', 'Green Diffusion' : '#FFFFFF', 'S122' : '#FFFFFF', 'Red Cyc Silk' : '#FFFFFF', 'S124' : '#FFFFFF', 'Blue Cyc Silk' : '#FFFFFF', 'S125' : '#FFFFFF', 'Green Cyc Silk' : '#FFFFFF', 'S126' : '#FFFFFF', 'Amber Cyc Silk' : '#FFFFFF', 'S127' : '#FFFFFF', 'Quarter Hamburg Frost' : '#FFFFFF', 'S132' : '#FFFFFF', 'Subtle Hamburg Frost' : '#FFFFFF', 'S140' : '#FFFFFF', 'Light Tough Silk' : '#FFFFFF', 'S160' : '#FFFFFF', # HTML standard colours 'AliceBlue': '#F0F8FF', 'AntiqueWhite': '#FAEBD7', 'Aqua': '#00FFFF', 'Aquamarine': '#7FFFD4', 'Azure': '#F0FFFF', 'Beige': '#F5F5DC', 'Bisque': '#FFE4C4', 'Black': '#000000', 'BlanchedAlmond': '#FFEBCD', 'Blue': '#0000FF', 'BlueViolet': '#8A2BE2', 'Brown': '#A52A2A', 'BurlyWood': '#DEB887', 'CadetBlue': '#5F9EA0', 'Chartreuse': '#7FFF00', 'Chocolate': '#D2691E', 'Coral': '#FF7F50', 'CornflowerBlue': '#6495ED', 'Cornsilk': '#FFF8DC', 'Crimson': '#DC143C', 'Cyan': '#00FFFF', 'DarkBlue': '#00008B', 'DarkCyan': '#008B8B', 'DarkGoldenrod': '#B8860B', 'DarkGray': '#A9A9A9', 'DarkGreen': '#006400', 'DarkKhaki': '#BDB76B', 'DarkMagenta': '#8B008B', 'DarkOliveGreen': '#556B2F', 'DarkOrange': '#FF8C00', 'DarkOrchid': '#9932CC', 'DarkRed': '#8B0000', 'DarkSalmon': '#E9967A', 'DarkSeaGreen': '#8FBC8F', 'DarkSlateBlue': '#483D8B', 'DarkSlateGray': '#2F4F4F', 'DarkTurquoise': '#00CED1', 'DarkViolet': '#9400D3', 'DeepPink': '#FF1493', 'DeepSkyBlue': '#00BFFF', 'DimGray': '#696969', 'DodgerBlue': '#1E90FF', 'FireBrick': '#B22222', 'FloralWhite': '#FFFAF0', 'ForestGreen': '#228B22', 'Fuchsia': '#FF00FF', 'Gainsboro': '#DCDCDC', 'GhostWhite': '#F8F8FF', 'Gold': '#FFD700', 'Goldenrod': '#DAA520', 'Gray': '#808080', 'Green': '#008000', 'GreenYellow': '#ADFF2F', 'Honeydew': '#F0FFF0', 'HotPink': '#FF69B4', 'IndianRed': '#CD5C5C', 'Indigo': '#4B0082', 'Ivory': '#FFFFF0', 'Khaki': '#F0E68C', 'Lavender': '#E6E6FA', 'LavenderBlush': '#FFF0F5', 'LawnGreen': '#7CFC00', 'LemonChiffon': '#FFFACD', 'LightBlue': '#ADD8E6', 'LightCoral': '#F08080', 'LightCyan': '#E0FFFF', 'LightGoldenrodYellow': '#FAFAD2', 'LightGreen': '#90EE90', 'LightGrey': '#D3D3D3', 'LightPink': '#FFB6C1', 'LightSalmon': '#FFA07A', 'LightSeaGreen': '#20B2AA', 'LightSkyBlue': '#87CEFA', 'LightSlateGray': '#778899', 'LightSteelBlue': '#B0C4DE', 'LightYellow': '#FFFFE0', 'Lime': '#00FF00', 'LimeGreen': '#32CD32', 'Linen': '#FAF0E6', 'Magenta': '#FF00FF', 'Maroon': '#800000', 'MediumAquamarine': '#66CDAA', 'MediumBlue': '#0000CD', 'MediumOrchid': '#BA55D3', 'MediumPurple': '#9370DB', 'MediumSeaGreen': '#3CB371', 'MediumSlateBlue': '#7B68EE', 'MediumSpringGreen': '#00FA9A', 'MediumTurquoise': '#48D1CC', 'MediumVioletRed': '#C71585', 'MidnightBlue': '#191970', 'MintCream': '#F5FFFA', 'MistyRose': '#FFE4E1', 'Moccasin': '#FFE4B5', 'NavajoWhite': '#FFDEAD', 'Navy': '#000080', 'OldLace': '#FDF5E6', 'Olive': '#808000', 'OliveDrab': '#6B8E23', 'Orange': '#FFA500', 'OrangeRed': '#FF4500', 'Orchid': '#DA70D6', 'PaleGoldenrod': '#EEE8AA', 'PaleGreen': '#98FB98', 'PaleTurquoise': '#AFEEEE', 'PaleVioletRed': '#DB7093', 'PapayaWhip': '#FFEFD5', 'PeachPuff': '#FFDAB9', 'Peru': '#CD853F', 'Pink': '#FFC0CB', 'Plum': '#DDA0DD', 'PowderBlue': '#B0E0E6', 'Purple': '#800080', 'Red': '#FF0000', 'RosyBrown': '#BC8F8F', 'RoyalBlue': '#4169E1', 'SaddleBrown': '#8B4513', 'Salmon': '#FA8072', 'SandyBrown': '#F4A460', 'SeaGreen': '#2E8B57', 'Seashell': '#FFF5EE', 'Sienna': '#A0522D', 'Silver': '#C0C0C0', 'SkyBlue': '#87CEEB', 'SlateBlue': '#6A5ACD', 'SlateGray': '#708090', 'Snow': '#FFFAFA', 'SpringGreen': '#00FF7F', 'SteelBlue': '#4682B4', 'Tan': '#D2B48C', 'Teal': '#008080', 'Thistle': '#D8BFD8', 'Tomato': '#FF6347', 'Turquoise': '#40E0D0', 'Violet': '#EE82EE', 'Wheat': '#F5DEB3', 'White': '#FFFFFF', 'WhiteSmoke': '#F5F5F5', 'Yellow': '#FFFF00', 'YellowGreen': '#9ACD32' } PKuPHE}@.ifif pylux/plot.py# plot.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Interact with Pylux plot files without an XML parser. Manipulate Pylux plot files without having to use an XML parser, instead use the series of objects defined by plot which allow for quicker editing of plots, and management of the XML tree. Also provides some utility functions to make writing extensions for Pylux quicker and easier. """ import xml.etree.ElementTree as ET import uuid import math import pylux.reference as reference from pylux.exception import * class PlotFile: """Manage the Pylux plot project file. Attributes: path: the path of the project file. tree: the parsed XML tree. root: the root element of the XML tree. """ def __init__(self, path=None): """Initialise the PlotFile instance. Prepares the instance of PlotFile for a file to be loaded into it. If the path argument is given, loads the plot file at that location on the filesystem and parses its XML tree into an accessible element. Args: path: the full system path of the file to load. Raises: FileNotFoundError: if the file does not exist on the filesystem. FileFormatError: if the XML parser raises a ParseError. """ self.path = path if self.path is None: self.new() else: self.load(self.path) def load(self, path): """Load a project file. Args: path: the location of the file to load. Raeses: FileNotFoundError: if the file can not be found in the directory hierarchy. FileFormatError: if the XML parser raises a ParseError. """ try: self.tree = ET.parse(path) except ET.ParseError: raise FileFormatError else: self.root = self.tree.getroot() self.path = path def write(self): """Save the project file to its original location.""" self.tree.write(self.path, encoding='UTF-8', xml_declaration=True) def write_to(self, path): """Save the project file to a new location. Args: path: the location to save the file to. """ self.path = path self.write() def new(self): """Create a new plot file in the buffer. Overwrite the current file buffer with a new empty plot file. Create a new ElementTree in tree and set the root to be a plot element. """ self.root = ET.Element('plot') self.tree = ET.ElementTree(self.root) self.path = None class DmxRegistry: """Manage DMX registries. Now with multiple functions per channel! Exclusive!! """ def __init__(self, plot_file, universe): self.registry = {} self.universe = universe self._xml_registry = None # Find the corresponding XML registry for xml_registry in plot_file.root.findall('registry'): if xml_registry.get('universe') == self.universe: self._xml_registry = xml_registry break # If there isn't one make a new one if self._xml_registry is None: self._xml_registry = ET.Element('registry') self._xml_registry.set('universe', self.universe) plot_file.root.append(self._xml_registry) # Otherwise populate the Python registry else: for xml_channel in self._xml_registry.findall('channel'): address = int(xml_channel.get('address')) self.registry[address] = [] for xml_function in xml_channel.findall('function'): fixture_uuid = xml_function.get('uuid') function = xml_function.text self.registry[address].append((fixture_uuid, function)) def _save(self): """Saves the Python registry to the XML tree. Saves the contents of the Python DMX registry to the registry in XML and deletes any XML channels that no longer exist in the Python registry. """ # Search a channel with address in XML def get_xml_channel(self, address): for channel in self._xml_registry.findall('channel'): found_address = channel.get('address') if found_address == str(address): return channel # Add an empty channel object def add_xml_channel(self, address): new_channel = ET.SubElement(self._xml_registry, 'channel') new_channel.set('address', str(address)) # Iterate over the Python registry for address in self.registry: if self.registry[address] != None: xml_channel = get_xml_channel(self, address) # If there is no channel with this address, make one if xml_channel == None: add_xml_channel(self, address) xml_channel = get_xml_channel(self, address) # Clear the channel if it does exist else: for function in xml_channel.findall('function'): xml_channel.remove(function) # Iterate over functions and add to XML for function in self.registry[address]: fixture_uuid = function[0] fixture_function = function[1] xml_function = ET.SubElement(xml_channel, 'function') xml_function.set('uuid', fixture_uuid) xml_function.text = fixture_function else: self._xml_registry.remove(get_xml_channel(self, address)) def get_occupied(self): """Returns a list of occupied DMX channels. Returns: A list containing the addresses of the occupied channels in the Python registry. """ occupied = [] for address in self.registry: if self.registry[address] != None: occupied.append(address) occupied.sort() return occupied def get_start_address(self, n): """Returns a recommended start address for a new fixture. Finds the next run of n free DMX channels in the Python registry or returns 1 if no channels are occupied. Args: n: the number of DMX channels required by the new fixture. Returns: An integer giving the ideal DMX start address. """ occupied = self.get_occupied() if occupied == []: print('All channels are free so choosing start address 1') return 1 for i in occupied: free_from = i+1 if occupied[-1] == i: next_test = 513 else: next_test = occupied[occupied.index(i)+1] free_until = next_test-1 if free_until-free_from >= 0: print('Found free channels in the range '+ str(free_from)+':'+str(free_until)) if free_until-free_from+1 >= n: print('Automatically chose start address '+ str(free_from)) return free_from def add_function(self, address, fixture_uuid, function): if address in self.registry: self.registry[address].append((fixture_uuid, function)) else: self.registry[address] = [(fixture_uuid, function)] self._save() def remove_function(self, address, uuid): """Remove a function from an address. Remove the function from the channel that has the given fixture UUID. """ for function in self.registry[address]: if function[0] == uuid: self.registry[address].remove(function) self._save() def get_functions(self, address): if address in self.registry: return self.registry[address] else: return None class RegistryList: def __init__(self, plot_file): xml_registries = plot_file.root.findall('registry') self.registries = [] for xml_registry in xml_registries: registry = DmxRegistry(plot_file, xml_registry.get('universe')) self.registries.append(registry) class FixtureList: """Manage all the fixtures in a plot.""" def __init__(self, plot_file): """Creates fixture objects for all the fixtures in the plot.""" self._root = plot_file.root self.fixtures = [] for xml_fixture in self._root.findall('fixture'): fixture = Fixture(plot_file, uuid=xml_fixture.get('uuid')) self.fixtures.append(fixture) def remove(self, fixture): """Remove a fixture from the plot.""" self._root.remove(fixture._xml_fixture) def get_data_values(self, data_type): """Returns a list containing the values of data...etc""" data_values = [] for fixture in self.fixtures: try: data_values.append(fixture.data[data_type]) except KeyError: pass data_values = list(set(data_values)) data_values.sort() return data_values def assign_usitt_numbers(self): count = 1 hung = [] for fixture in self.fixtures: if 'posY' in fixture.data: hung.append(fixture) for fixture in sorted(hung, key=lambda fixture: fixture.data['posY']): fixture.set_data('usitt_key', str(count)) count = count+1 for fixture in self.fixtures: if fixture not in hung: fixture.set_data('usitt_key', str(None)) def get_fixtures_for_dimmer(self, dimmer): """Get a list of fixtures controlled by this fixture. If this is a dimmer fixture, get a list of fixture objects that the dimmer controls. """ if 'is_dimmer' not in dimmer.data: return [] if dimmer.data['is_dimmer'] != 'True': return [] controlled = [] for fixture in self.fixtures: if 'dimmer_uuid' in fixture.data: if fixture.data['dimmer_uuid'] == dimmer.uuid: controlled.append(fixture) return controlled class Fixture: """Manage individual fixtures. Attributes: uuid: the UUID of the fixture. data: a dictionary containing all other data for the fixture. dmx: a list of the functions of the DMX channels used by this fixture. dmx_num: the number of DMX channels required by this fixture. """ def __init__(self, plot_file, uuid=None, template=None, src_fixture=None): """Create a new fixture in Python. If uuid is given, load data from the plot file from the fixture with the corresponding UUID. If template is given, create a new fixture based on the template file. If src_fixture is given, copy the contents of an existing fixture into this one. Args: plot_file: the PlotFile object containing the plot. uuid: the UUID of the fixture to load from XML. src_fixture: the Fixture object to copy into this one. """ self.data = {} if uuid != None: self.uuid = uuid xml_fixtures = plot_file.root.findall('fixture') for xml_fixture in plot_file.root.findall('fixture'): if uuid == xml_fixture.get('uuid'): self._xml_fixture = xml_fixture for data_item in self._xml_fixture: self.data[data_item.tag] = data_item.text self._save() elif template != None: self._new_from_template(template) self._xml_fixture = ET.Element('fixture') self._xml_fixture.set('uuid', self.uuid) plot_file.root.append(self._xml_fixture) self._save() elif src_fixture != None: self._new_from_fixture(src_fixture) self._xml_fixture = ET.Element('fixture') self._xml_fixture.set('uuid', self.uuid) plot_file.root.append(self._xml_fixture) self._save() def _new_from_template(self, template_file): """Load information from a template into this fixture. Args: template: the name of the template the new fixture should copy. """ self.uuid = str(uuid.uuid4()) src_tree = ET.parse(template_file) src_root = src_tree.getroot() for xml_data in src_root: self.data[xml_data.tag] = xml_data.text def _new_from_fixture(self, src_fixture): """Copy the contents of another fixture into this one. Make a verbatim copy of an existing fixture, except create a new UUID for this fixture. Args: src_fixture: the source Fixture to be copied. """ self.uuid = str(uuid.uuid4()) for data_item in src_fixture.data: self.data[data_item] = src_fixture.data[data_item] def _save(self): """Save the Python fixture object to XML.""" # Add a new data item def add_xml_data(self, tag, value): new_data_item = ET.SubElement(self._xml_fixture, tag) new_data_item.text = value # Edit an existing data item def edit_xml_data(self, tag, new_value): self._xml_fixture.find(tag).text = new_value # Search for data in XML def get_xml_data(self, tag): try: return self._xml_fixture.find(tag) except AttributeError: return None # Iterate over the data dictionary for data_item in self.data: xml_data = get_xml_data(self, data_item) if xml_data == None: add_xml_data(self, data_item, self.data[data_item]) else: edit_xml_data(self, data_item, self.data[data_item]) # Iterate over XML fixture to remove empty data for data_item in self._xml_fixture: data_name = data_item.tag if self.data[data_name] == '': self._xml_fixture.remove(data_item) def set_data(self, name, value): """Set the value of a piece of data.""" self.data[name] = value self._save() def get_data(self, name): """Get the value of a piece of data.""" if name in self.data: return self.data[name] else: return None def address(self, registry, start_address): address = start_address for function in self.data['dmx_functions'].split(','): registry.add_function(address, self.uuid, function) address = address+1 def unaddress(self, registries): for registry in registries.registries: for address in registry.registry: registry.remove_function(address, self.uuid) def get_rotation(self): if ('posX' not in self.data or 'posY' not in self.data or 'focusX' not in self.data or 'focusY' not in self.data): return None else: posX = float(self.data['posX']) posY = float(self.data['posY']) focusX = float(self.data['focusX']) focusY = float(self.data['focusY']) return math.degrees(math.atan2((focusY-posY), (focusX-posX))) def get_colour(self): if 'gel' not in self.data: return None elif self.data['gel'] in reference.gel_colours: return reference.gel_colours[self.data['gel']] else: return None class Metadata: """Manages the metadata section of the XML file. Attributes: xml_meta: the XML object containing a the metadata. meta: a dictionary containing the metadata. """ def __init__(self, plot_file): """Find the metadata in XML and add to the attribute. Args: plot_file: the FileManager object of the project file. """ self._root = plot_file.root self.meta = {} for metaitem in self._root.findall('metadata'): self.meta[metaitem.get('name')] = metaitem.text def set_data(self, name, value): self.meta[name] = value self._save() def _save(self): """Save the metadata dictionary to XML.""" # Add a new meta item def add_xml_meta(self, name, value): new_metadata = ET.Element('metadata') new_metadata.set('name', name) new_metadata.text = value self._root.append(new_metadata) # Edit an existing meta item def edit_xml_meta(self, xml_meta, new_value): xml_meta.text = new_value # Search for meta in XML def get_xml_meta(self, name): try: for metaitem in self._root.findall('metadata'): if metaitem.get('name') == metaitem: return metaitem else: return None except AttributeError: return None # Iterate over the meta values dictionary for metaitem in self.meta: xml_meta = get_xml_meta(self, metaitem) if xml_meta == None: add_xml_meta(self, metaitem, self.meta[metaitem]) else: edit_xml_meta(self, xml_meta, self.meta[metaitem]) # Iterate over XML meta object to remove empty values for metaitem in self._root.findall('metadata'): name = metaitem.get('name') if self.meta[name] == None: self._root.remove(metaitem) class Cue: """Manages cues something something bored of docstrings.""" def __init__(self, plot_file, UUID=None): """Create an empty cue.""" self.data = {} if UUID is None: self.uuid = str(uuid.uuid4()) self.key = len(CueList(plot_file).cues)+1 self._xml_cue = ET.Element('cue') self._xml_cue.set('uuid', self.uuid) plot_file.root.append(self._xml_cue) else: self.uuid = UUID for xml_cue in plot_file.root.findall('cue'): if xml_cue.get('uuid') == self.uuid: self._xml_cue = xml_cue self.key = int(xml_cue.get('key')) for cue_data in xml_cue: self.data[cue_data.tag] = cue_data.text def set_data(self, name, value): """Set the value of name to value and save to XML.""" self.data[name] = value self._save() def get_data(self, name): """Get the value of name.""" if name in self.data: return self.data[name] else: return None def _save(self): """Save the cue to XML.""" # Find data tags already in XML data_in_xml = [] for data_item_xml in self._xml_cue: data_in_xml.append(data_item_xml.tag) # Set the sorting key self._xml_cue.set('key', str(self.key)) # Iterate through data in dict for data_item in self.data: # If data not in XML, make a new sub element if data_item not in data_in_xml: new_data_item = ET.SubElement(self._xml_cue, data_item) new_data_item.text = self.data[data_item] # Otherwise edit existing data else: for data_item_xml in self._xml_cue: if data_item_xml.tag == data_item: data_item_xml.text = self.data[data_item] # Iterate through data in XML and remove empty for data_item_xml in self._xml_cue: if self.data[data_item_xml.tag] is None: self._xml_cue.remove(data_item_xml) class CueList: """Manage all the cues in the document. Create a list contaning cue objects for every cue in the document. Also manage the keys of cues by moving cues relative to one another and remove cues entirely. Attributes: cues: a list of all the cue objects in the document. """ def __init__(self, plot_file): """Generate a list of all the cues present. Search through the plot file for any cue tags, create a Cue object for them, then add to a list. Args: plot_file: the PlotFile object containing the document. """ self._root = plot_file.root self.cues = [] for xml_cue in self._root.findall('cue'): cue_uuid = xml_cue.get('uuid') cue = Cue(plot_file, UUID=cue_uuid) self.cues.append(cue) def remove(self, cue): """Remove a cue from the plot entirely. Args: plot_file: the PlotFile object containing the document. UUID: the UUID of the cue to be deleted. """ self._root.remove(cue._xml_cue) def move_after(self, origin, dest): """Move a cue after another in the list. Manipulate the key attributes of any necessary cues to rearrange the list such that origin is placed immediately after dest. Args: plot_file: the PlotFile object containing the document. origin: the key of the cue to be moved. dest: the key of the cue after which this cue should be immediately located. """ if dest > origin: for cue in self.cues: if cue.key == origin: cue.key = dest elif origin < cue.key <= dest: cue.key = cue.key-1 cue._save() if dest < origin: for cue in self.cues: if dest < cue.key < origin: cue.key = cue.key+1 elif cue.key == origin: cue.key = dest+1 cue._save() def move_before(self, origin, dest): """Move a cue before another in the list. Manipulate the key attributes of any necessary cues to rearrange the list such that origin is placed immediately before dest. Args: plot_file: the PlotFile object containing the document. origin: the key of the cue to be moved. dest: the key of the cue before which this cue should be immediately located. """ if dest > origin: for cue in self.cues: if cue.key == origin: cue.key = dest elif dest >= cue.key > origin: cue.key = cue.key-1 cue._save() if dest < origin: for cue in self.cues: if origin > cue.key >= dest: cue.key = cue.key+1 elif cue.key == origin: cue.key = dest cue._save() def assign_identifiers(self): count = {'LX': 1, 'SX': 1, 'VX': 1} for cue in sorted(self.cues, key=lambda cue: cue.key): cue_type = cue.data['type'] cue.set_data('identifier', cue_type+str(count[cue_type])) count[cue_type] = count[cue_type]+1 class Scene: """Scenes store DMX output states.""" def __init__(self, plot_file, UUID=None): """Create an empty scene.""" self.dmx_data = {} if UUID is None: self.uuid = str(uuid.uuid4()) xml_cue = ET.Element('scene') xml_cue.set('uuid', self.uuid) plot_file.root.append(xml_cue) else: self.uuid = UUID for xml_scene in plot_file.root.findall('scene'): if xml_scene.get('uuid') == self.uuid: for dmx_info_xml in xml_scene.findall('dmx'): address = dmx_info_xml.get('address') output = int(dmx_info_xml.text) self.dmx_data[address] = output def save(self, plot_file): """Save the scene to XML.""" for xml_scene_test in plot_file.root.findall('scene'): if xml_scene_test.get('uuid') == self.uuid: xml_scene = xml_scene_test # Find DMX info already in XML dmx_in_xml = [] for dmx_info_xml in xml_scene.findall('dmx'): dmx_in_xml.append(dmx_info_xml.get('address')) # Iterate through data in DMX dict for dmx_info in self.dmx_data: # If DMX not in XML, make a new sub element if dmx_info not in dmx_in_xml: new_dmx_info = ET.SubElement(xml_scene, 'dmx') new_dmx_info.text = self.dmx_data[dmx_info] new_dmx_info.set('address', dmx_info) # Otherwise edit existing data else: for dmx_info_xml in xml_scene: if dmx_info_xml.tag == dmx_info: dmx_info_xml.text = self.dmx_data[dmx_info] # Iterate through data in XML and remove empty for dmx_info_xml in xml_scene: if self.dmx_data[dmx_info_xml.tag] is None: xml_scene.remove(dmx_info_xml) PKEO7HO..pylux/editor.py# editor.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Edit the content of Pylux plot files. editor is a CLI implementation of the Pylux plot editor, allowing for the reading and editing of Pylux plot files. """ import os import sys import pylux.plot as plot import pylux.clihelper as clihelper import runpy import logging from pylux import get_data from importlib import import_module def file_open(inputs): try: PLOT_FILE.load(inputs[1]) except IndexError: print('Error: You need to specify a file path to load') except AttributeError: pass def file_write(inputs): try: PLOT_FILE.save() except AttributeError: print('Error: No file is loaded') def file_writeas(inputs): try: PLOT_FILE.saveas(inputs[1]) except IndexError: print('Error: You need to specify a destination path!') def file_get(inputs): print('Using plot file '+PLOT_FILE.file) def file_new(inputs): PLOT_FILE.generate(os.path.expanduser(inputs[1])) PLOT_FILE.load(os.path.expanduser(inputs[1])) file_get(inputs) def metadata_list(inputs): metadata = plot.Metadata(PLOT_FILE) for i in metadata.meta: print(i+': '+metadata.meta[i]) def metadata_set(inputs): metadata = plot.Metadata(PLOT_FILE) metadata.meta[inputs[1]] = clihelper.resolve_input(inputs, 2)[-1] metadata.save() def metadata_remove(inputs): metadata = plot.Metadata(PLOT_FILE) metadata.meta[inputs[1]] = None metadata.save() def metadata_get(inputs): metadata = plot.Metadata(PLOT_FILE) print(inputs[1]+': '+metadata.meta[inputs[1]]) def fixture_new(inputs): fixture = plot.Fixture(PLOT_FILE) try: fixture.new(inputs[1], FIXTURES_DIR) except FileNotFoundError: print('Error: Couldn\'t find a fixture file with this name') else: fixture.add() fixture.save() def fixture_clone(inputs): src_fixture = INTERFACE.get(inputs[1]) if len(src_fixture) > 1: print('Error: You can only clone one fixture!') else: new_fixture = plot.Fixture(PLOT_FILE, FIXTURES_DIR) new_fixture.clone(src_fixture[0]) new_fixture.add() new_fixture.save() def fixture_list(inputs): fixtures = plot.FixtureList(PLOT_FILE) i = 1 INTERFACE.clear() for fixture in fixtures.fixtures: if 'name' in fixture.data: name = fixture.data['name'] else: name = fixture.data['type'] print('\033[4m'+str(i)+'\033[0m '+name+', id: '+fixture.uuid) INTERFACE.append(i, fixture) i = i+1 def fixture_filter(inputs): try: key = inputs[1] value = clihelper.resolve_input(inputs, 2)[-1] fixtures = plot.FixtureList(PLOT_FILE) INTERFACE.clear() i = 1 for fixture in fixtures.fixtures: if key in fixture.data: if fixture.data[key] == value: if 'name' in fixture.data: name = fixture.data['name'] else: name = fixture.data['type'] print('\033[4m'+str(i)+'\033[0m '+name+ ', id: '+fixture.uuid+', '+key+': '+value) INTERFACE.append(i, fixture) i = i+1 else: pass except IndexError: print('Error: You need to specify a key and value!') def fixture_remove(inputs): fixture_list = plot.FixtureList(PLOT_FILE) fixtures = INTERFACE.get(inputs[1]) for fixture in fixtures: fixture_list.remove(fixture) def fixture_get(inputs): fixtures = INTERFACE.get(inputs[1]) for fixture in fixtures: if inputs[2] in fixture.data: print(fixture.data[inputs[2]]) else: print(None) INTERFACE.update_this(inputs[1]) def fixture_getall(inputs): fixtures = INTERFACE.get(inputs[1]) for fixture in fixtures: for data_item in fixture.data: print(data_item+': '+str(fixture.data[data_item])) INTERFACE.update_this(inputs[1]) def fixture_set(inputs): fixtures = INTERFACE.get(inputs[1]) tag = inputs[2] value = clihelper.resolve_input(inputs, 3)[-1] for fixture in fixtures: # See if it can be automatically generated if value == 'auto': if tag == 'rotation': fixture.data['rotation'] = str(fixture.generate_rotation()) elif tag == 'colour': fixture.data['colour'] = fixture.generate_colour() else: print('Error: No automatic generation is available for ' 'this tag') # See if it is a special pseudo tag elif tag == 'position': fixture.data['posX'] = value.split(',')[0] fixture.data['posY'] = value.split(',')[1] elif tag == 'focus': fixture.data['focusX'] = value.split(',')[0] fixture.data['focusY'] = value.split(',')[1] elif tag == 'dimmer': fixture.data['dimmer_uuid'] = INTERFACE.get(inputs[3])[0].uuid fixture.data['dimmer_channel'] = inputs[4] # Otherwise just set it else: fixture.data[tag] = value fixture.save() INTERFACE.update_this(inputs[1]) def fixture_address(inputs): fixtures = INTERFACE.get(inputs[1]) if len(fixtures) > 1 and inputs[3] != 'auto': print('Error: You must specify auto if you address more than ' 'one fixture.') else: registry = plot.DmxRegistry(PLOT_FILE, inputs[2]) for fixture in fixtures: registry.address(fixture, inputs[3]) INTERFACE.update_this(inputs[1]) def fixture_purge(inputs): fixtures = INTERFACE.get(inputs[1]) for fixture in fixtures: registry = plot.DmxRegistry(PLOT_FILE, fixture.data['universe']) registry.unaddress(fixture) fixture_list = plot.FixtureList(PLOT_FILE) fixture_list.remove(fixture) def registry_list(inputs): try: registry = plot.DmxRegistry(PLOT_FILE, inputs[1]) for channel in registry.registry: uuid = registry.registry[channel][0] func = registry.registry[channel][1] print(str(format(channel, '03d'))+' uuid: '+uuid+', func: '+func) except IndexError: print('You need to specify a DMX registry!') def cue_list(inputs): cues = plot.CueList(PLOT_FILE) INTERFACE.clear() for cue in cues.cues: cue_type = cue.data['type'] cue_location = cue.data['location'] print('\033[4m'+str(cue.key)+'\033[0m ('+cue_type+') at '+ cue_location) INTERFACE.append(cue.key, cue) def cue_new(inputs): cue = plot.Cue(PLOT_FILE) cue.data['type'] = inputs[1] cue.data['location'] = clihelper.resolve_input(inputs, 2)[-1] cue.save(PLOT_FILE) def cue_remove(inputs): cues = plot.CueList(PLOT_FILE) removal_candidates = INTERFACE.get(inputs[1]) for rc in removal_candidates: cues.remove(PLOT_FILE, rc.uuid) def cue_set(inputs): cues_to_change = INTERFACE.get(inputs[1]) for cue in cues_to_change: cue.data[inputs[2]] = clihelper.resolve_input(inputs, 3)[-1] cue.save(PLOT_FILE) def cue_get(inputs): cues_to_get = INTERFACE.get(inputs[1]) for cue in cues_to_get: if inputs[2] in cue.data: print(cue.data[inputs[2]]) else: print(None) def cue_getall(inputs): cues_to_get = INTERFACE.get(inputs[1]) for cue in cues_to_get: for data_item in cue.data: print(data_item+': '+cue.data[data_item]) def cue_moveafter(inputs): cues = plot.CueList(PLOT_FILE) cues.move_after(PLOT_FILE, int(inputs[1]), int(inputs[2])) def cue_movebefore(inputs): cues = plot.CueList(PLOT_FILE) cues.move_before(PLOT_FILE, int(inputs[1]), int(inputs[2])) def utility_help(inputs): text = "" with open('help.txt') as man: for line in man: text = text+line print(text) #def extension_run(inputs): # init_globals = { # 'PLOT_FILE': PLOT_FILE, # 'CONFIG': CONFIG, # 'LOG_LEVEL': LOG_LEVEL} # extensions_dir = '/usr/share/pylux/extension/' # module_name = inputs[0].split(':')[1] # try: # runpy.run_path(extensions_dir+module_name+'.py', # init_globals=init_globals, run_name='pyext') # except FileNotFoundError: # print('No extension with this name!') def utility_clear(inputs): os.system('cls' if os.name == 'nt' else 'clear') def utility_quit(inputs): print('Autosaving changes...') file_write(inputs) sys.exit() def utility_kill(inputs): print('Ignoring changes and exiting...') sys.exit() def main(): """The main user loop.""" global INTERFACE INTERFACE = clihelper.Interface() global FIXTURES_DIR FIXTURES_DIR = get_data('fixture') functions_dict = { 'fo': file_open, 'fw': file_write, 'fW': file_writeas, 'fg': file_get, 'fn': file_new, 'mG': metadata_list, 'ms': metadata_set, 'mr': metadata_remove, 'mg': metadata_get, 'xn': fixture_new, 'xc': fixture_clone, 'xl': fixture_list, 'xf': fixture_filter, 'xr': fixture_remove, 'xg': fixture_get, 'xG': fixture_getall, 'xs': fixture_set, 'xa': fixture_address, 'xp': fixture_purge, 'rl': registry_list, 'ql': cue_list, 'qn': cue_new, 'qr': cue_remove, 'qs': cue_set, 'qg': cue_get, 'qG': cue_getall, 'qm': cue_moveafter, 'qM': cue_movebefore, # 'Xr': extension_run, 'h': utility_help, 'c': utility_clear, 'q': utility_quit, 'Q': utility_kill} print('Welcome to Pylux! Type \'h\' to view a list of commands.') # Begin the main loop logging.basicConfig(level=LOG_LEVEL) while True: user_input = input(CONFIG['cli']['prompt']+' ') inputs = user_input.split(' ') if inputs[0][0] == ':': init_globals = { 'PLOT_FILE': PLOT_FILE, 'CONFIG': CONFIG, 'LOG_LEVEL': LOG_LEVEL} context_name = inputs[0].split(':')[1] module_name = 'pylux.context.'+inputs[0].split(':')[1] try: context = import_module(module_name) except ImportError: print('Error: Context does not exist') else: context.set_globals(init_globals) while True: user_input = input('(pylux:'+context_name+') ') inputs = user_input.split(' ') if inputs[0] == '::': break else: context.process_input(inputs) elif inputs[0] in functions_dict: functions_dict[inputs[0]](inputs) else: print('Error: Command doesn\'t exist.') print('Type \'h\' for a list of available commands.') if __name__ == 'pylux_root': main() PKG; ; pylux/gplotter.py#!/usr/bin/python3 # gplotter.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import plot from tkinter import * from tkinter.ttk import * from tkinter import tix from tkinter import constants from tkinter import filedialog class GuiApp(Frame): def __init__(self, plot_file, config, master=None): Frame.__init__(self, master) self.pack() self.master = master self.plot_file = plot_file self.config = config self.create_menubar() try: self.create_fixtures_list() except AttributeError: print('no plot file') def create_menubar(self): self.menubar = Menu(self) self.master.config(menu=self.menubar) self.file_menu = Menu(self.menubar) self.menubar.add_cascade(label='File', menu=self.file_menu) self.file_menu.add_command(label='Open...', command=self.command_load_file) self.debug_menu = Menu(self.menubar) self.menubar.add_cascade(label='Debug', menu=self.debug_menu) self.debug_menu.add_command(label='GenFixList', command=self.create_fixtures_list) def create_fixtures_list(self): self.gfixtures_tree = Treeview(self, selectmode='browse', columns=['tag', 'value']) fixtures = plot.FixtureList(self.plot_file) for fixture in fixtures.fixtures: uuid = fixture.uuid olid = fixture.olid try: name = fixture.data['name'] except IndexError: name = uuid self.gfixtures_tree.insert('', 'end', uuid, text=name, values=[olid]) for data_item in fixture.data: name = data_item value = fixture.data[data_item] self.gfixtures_tree.insert(uuid, 'end', values=[name, value]) self.gfixtures_tree.pack() def command_load_file(self): gfile_dialog = filedialog.askopenfile() self.plot_file.load(gfile_dialog.name) def main(plot_file, config): root = tix.Tk() app = GuiApp(plot_file, config, master=root) app.mainloop() if __name__ == '__main__': main() PKqjPH1UPg g pylux/__main__.py# __main__.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """The __main__ module takes options and does stuff.""" import argparse import os import configparser import sys import runpy import logging import pylux.plot as plot from pylux import __version__, get_data def main(): """Initialise the argument and config parsers.""" print('This is Pylux, version '+__version__) # Initiate the argument parser and get launch arguments parser = argparse.ArgumentParser(prog='pylux', description='Create and modify OpenLighting Plot files') parser.add_argument('-v', '--version', action='version', version='%(prog`)s '+__version__) parser.add_argument('-f', '--file', dest='file', help='load this project file on launch') parser.add_argument('-g', '--gui', action='store_true', help='launch Pylux with a GUI') parser.add_argument('-V', '--verbose', dest='verbose', action='count', help='set the verbosity of output') launch_args = parser.parse_args() # Load configuration config = configparser.ConfigParser() config.read([get_data('settings.conf', location='root'), get_data('settings.conf', location='home')]) # Handle verbosity verbosity_dict = { None: (logging.WARNING, 'WARNING'), 1: (logging.INFO, 'INFO'), 2: (logging.DEBUG, 'DEBUG')} print('Logging level is '+verbosity_dict[launch_args.verbose][1]) # Load plot file plot_file = plot.PlotFile() if launch_args.file != None: plot_file.load(launch_args.file) print('Using plot file '+plot_file.path) else: print('No plot file loaded') # Prepare globals for launch init_globals = { 'PLOT_FILE': plot_file, 'CONFIG': config, 'LOG_LEVEL': verbosity_dict[launch_args.verbose][0]} if launch_args.gui == True: print('Running in GUI mode\n') runpy.run_module('pylux.gui', init_globals=init_globals, run_name='pylux_root') else: print('Running in CLI mode\n') runpy.run_module('pylux.cli', init_globals=init_globals, run_name='pylux_root') if __name__ == '__main__': main() PKyLH] ]jjpylux/__init__.py"""Pylux is a suite for the management of lighting documentation""" import os import pkg_resources __version__ = pkg_resources.get_distribution('pylux').version _ROOT = os.path.abspath(os.path.dirname(__file__)) _HOME = os.path.expanduser('~/.pylux') def get_data(path, location='auto'): if location == 'auto': if os.path.isfile(os.path.join(_HOME, path)): return os.path.join(_HOME, path) else: return os.path.join(_ROOT, path) elif location == 'root': return os.path.join(_ROOT, path) elif location == 'home': return os.path.join(_HOME, path) PKT7Hi##pylux/geditor.py# gplotter.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import pylux.plot as plot import xml.etree.ElementTree as ET import gi.repository gi.require_version('Gtk', '3.0') from gi.repository import Gtk class TextInputDialog(Gtk.Dialog): def __init__(self, parent, title): Gtk.Dialog.__init__(self, title, parent, 0, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)) self.set_default_size(150, 100) self.entry_box = Gtk.Entry() self.container_box = self.get_content_area() self.container_box.add(self.entry_box) self.show_all() class FixtureInfoWindow(Gtk.Window): def __init__(self, fixture): if 'name' in fixture.data: fixture_name = fixture.data['name'] else: fixture_name = fixture.data['type'] Gtk.Window.__init__(self, title='Editing '+fixture_name) self.set_default_size(300, 350) tree_list_model = Gtk.ListStore(str, str) for info_item in fixture.data: tree_list_model.append([info_item, fixture.data[info_item]]) self.fixture_info_tree = Gtk.TreeView(tree_list_model) tree_renderer = Gtk.CellRendererText() tag_column = Gtk.TreeViewColumn('Tag', tree_renderer, text=0) tag_column.set_sort_column_id(0) value_column = Gtk.TreeViewColumn('Value', tree_renderer, text=1) self.fixture_info_tree.append_column(tag_column) self.fixture_info_tree.append_column(value_column) self.add(self.fixture_info_tree) class FixturesWindow(Gtk.Window): def __init__(self): Gtk.Window.__init__(self, title='Fixtures') self.set_default_size(0, 500) self.box_container = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) self.add(self.box_container) # Create the fixtures list self.gui_list_fixtures() # Create fixture action buttons self.button_fixture_new = Gtk.Button(label='New fixture') self.button_fixture_new.connect('clicked', self.action_fixture_new) # Pack fixture action buttons into Box self.box_fixture_buttons = Gtk.Box(spacing=4) self.box_container.pack_start(self.box_fixture_buttons, True, True, 0) self.box_fixture_buttons.pack_start(self.button_fixture_new, True, True, 0) def gui_list_fixtures(self): """Create an empty ListBox for fixtures.""" self.listbox_fixtures = Gtk.ListBox() self.listbox_fixtures.set_selection_mode(Gtk.SelectionMode.NONE) self.box_container.pack_start(self.listbox_fixtures, True, True, 0) fixtures = plot.FixtureList(PLOT_FILE) for fixture in fixtures.fixtures: self.gui_add_fixture(fixture) def gui_add_fixture(self, fixture): """Add a fixture to the ListBox as a ListBoxRow.""" listbox_row_fixture = Gtk.ListBoxRow() grid_fixture_listbox = Gtk.Grid() listbox_row_fixture.add(grid_fixture_listbox) # LHS: name, uuid box_listbox_row_LHS = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) if 'name' in fixture.data: fixture_name = fixture.data['name'] else: fixture_name = fixture.data['type'] label_fixture_name = Gtk.Label(fixture_name, halign=1) label_fixture_uuid = Gtk.Label(halign=1) label_fixture_uuid.set_markup(''+fixture.uuid+'') box_listbox_row_LHS.pack_start(label_fixture_name, True, True, 0) box_listbox_row_LHS.pack_start(label_fixture_uuid, True, True, 0) grid_fixture_listbox.attach(box_listbox_row_LHS, 0, 0, 2, 1) # RHS: action buttons box_listbox_row_RHS = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=2) button_fixture_getall = Gtk.Button.new_from_icon_name('dialog-information', 1) button_fixture_getall.connect('clicked', self.action_fixture_getall) button_fixture_clone = Gtk.Button.new_from_icon_name('edit-copy', 1) button_fixture_clone.connect('clicked', self.action_fixture_clone) button_fixture_remove = Gtk.Button.new_from_icon_name('edit-delete', 1) button_fixture_remove.connect('clicked', self.action_fixture_remove) box_listbox_row_RHS.pack_start(button_fixture_getall, True, True, 0) box_listbox_row_RHS.pack_start(button_fixture_clone, True, True, 0) box_listbox_row_RHS.pack_start(button_fixture_remove, True, True, 0) grid_fixture_listbox.attach(box_listbox_row_RHS, 2, 0, 1, 1) self.listbox_fixtures.add(listbox_row_fixture) def action_fixture_new(self, widget): type_dialog = TextInputDialog(self, 'Fixture Type') response = type_dialog.run() if response == Gtk.ResponseType.OK: fixture_type = type_dialog.entry_box.get_text() fixture = plot.Fixture(PLOT_FILE) try: fixture.new(fixture_type, '/usr/share/pylux/fixture/') except FileNotFoundError: print('Error: Couldn\'t find a fixture file with this name') else: fixture.add() fixture.save() self.gui_add_fixture(fixture) self.show_all() type_dialog.destroy() def action_fixture_remove(self, widget): print('Removing fixture...') def action_fixture_clone(self, widget): print('Cloning fixture...') def action_fixture_getall(self, widget): fixture_info_box = widget.props.parent.props.parent.get_child_at(0,0) uuid_markup = fixture_info_box.get_children()[1].get_label() fixture_uuid = ET.fromstring(uuid_markup).text fixture = plot.Fixture(PLOT_FILE, uuid=fixture_uuid) info_window = FixtureInfoWindow(fixture) info_window.connect('delete-event', info_window.destroy) info_window.show_all() def main(): win = FixturesWindow() win.connect('delete-event', Gtk.main_quit) win.show_all() Gtk.main() if __name__ == 'pylux_root': main() PKlIHi88pylux/exception.py# exception.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Exceptions used by Pylux.""" class FileFormatError(Exception): pass PKUW)H[c#c#pylux/plotter.py#!/usr/bin/python3 # plotter.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys from __init__ import __version__ import plot import clihelper import importlib.util as IL def main(plot_file, config): """The main user loop.""" interface = clihelper.Interface() prompt = config['Settings']['prompt']+' ' fixtures_dir = '/usr/share/pylux/fixture/' print('Welcome to Pylux! Type \'h\' to view a list of commands.') # Begin the main loop while True: user_input = input(config['Settings']['prompt']+' ') inputs = [] for i in user_input.split(' '): inputs.append(i) # File actions if inputs[0] == 'fo': try: plot_file.load(inputs[1]) except UnboundLocalError: print('Error: You need to specify a file path to load') elif inputs[0] == 'fw': plot_file.save() elif inputs[0] == 'fW': try: plot_file.saveas(inputs[1]) except IndexError: print('Error: You need to specify a destination path!') elif inputs[0] == 'fg': print('Using plot file '+plot_file.file) elif inputs[0] == 'fn': plot_file.generate(os.path.expanduser(inputs[1])) plot_file.load(os.path.expanduser(inputs[1])) print('Using plot file '+plot_file.file) # Metadata actions elif inputs[0] == 'ml': metadata = plot.Metadata(plot_file) for i in metadata.meta: print(i+': '+metadata.meta[i]) elif inputs[0] == 'ms': metadata = plot.Metadata(plot_file) metadata.meta[inputs[1]] = clihelper.resolve_input(inputs, 2)[-1] metadata.save() elif inputs[0] == 'mr': metadata = plot.Metadata(plot_file) metadata.meta[inputs[1]] = None metadata.save() elif inputs[0] == 'mg': metadata = plot.Metadata(plot_file) print(inputs[1]+': '+metadata.meta[inputs[1]]) # Fixture actions elif inputs[0] == 'xn': fixture = plot.Fixture(plot_file) try: fixture.new(inputs[1], fixtures_dir) except FileNotFoundError: print('Error: Couldn\'t find a fixture file with this name') else: fixture.add() fixture.save() interface.option_list['this'] = fixture elif inputs[0] == 'xc': src_fixture = interface.get(inputs[1]) new_fixture = plot.Fixture(plot_file, fixtures_dir) new_fixture.clone(src_fixture) new_fixture.add() new_fixture.save() elif inputs[0] == 'xl': fixtures = plot.FixtureList(plot_file) i = 1 interface.clear() for fixture in fixtures.fixtures: fixture_type = fixture.data['type'] print('\033[4m'+str(i)+'\033[0m '+fixture_type+', id: '+ fixture.uuid) interface.append(i, fixture) i = i+1 elif inputs[0] == 'xf': try: key = inputs[1] value = clihelper.resolve_input(inputs, 2)[-1] fixtures = plot.FixtureList(plot_file) interface.clear() i = 1 for fixture in fixtures.fixtures: try: test_value = fixture.data[key] except KeyError: pass else: if test_value == value: fix_type = fixture.data['type'] print('\033[4m'+str(i)+'\033[0m '+fix_type+ ', id: '+fixture.uuid+', '+key+': '+value) interface.append(i, fixture) i = i+1 except IndexError: print('Error: You need to specify a key and value!') elif inputs[0] == 'xr': try: fixtures = plot.FixtureList(plot_file) fixtures.remove(interface.get(inputs[1])) except IndexError: print('Error: You need to run either xl or xf then specify the' ' interface id of the fixture you wish to remove') elif inputs[0] == 'xg': fixture = interface.get(inputs[1]) try: print(fixture.data[inputs[2]]) except KeyError: print('Error: This fixture has no data with that name') interface.option_list['this'] = fixture elif inputs[0] == 'xG': fixture = interface.get(inputs[1]) for data_item in fixture.data: print(data_item+': '+str(fixture.data[data_item])) interface.option_list['this'] = fixture elif inputs[0] == 'xs': fixture = interface.get(inputs[1]) tag = inputs[2] value = clihelper.resolve_input(inputs, 3)[-1] if value == 'auto': if tag == 'rotation': fixture.data['rotation'] = str(fixture.generate_rotation()) elif tag == 'colour': fixture.data['colour'] = fixture.generate_colour() else: print('Error: No automatic generation is available for ' 'this tag') else: fixture.data[tag] = value fixture.save() interface.option_list['this'] = fixture elif inputs[0] == 'xA': fixture = interface.get(inputs[1]) registry = plot.DmxRegistry(plot_file, inputs[2]) registry.address(fixture, inputs[3]) interface.option_list['this'] = fixture elif inputs[0] == 'xp': fixture = interface.get(inputs[1]) registry = plot.DmxRegistry(plot_file, fixture.data['universe']) registry.unaddress(fixture) fixtures = plot.FixtureList(plot_file) fixtures.remove(fixture) # DMX registry actions elif inputs[0] == 'rl': try: registry = plot.DmxRegistry(plot_file, inputs[1]) interface.clear() for channel in registry.registry: uuid = registry.registry[channel][0] func = registry.registry[channel][1] print('\033[4m'+str(format(channel, '03d'))+ '\033[0m uuid: '+uuid+', func: '+func) interface.append(channel, plot.Fixture(plot_file, uuid)) except IndexError: print('You need to specify a DMX registry!') # Extension actions elif inputs[0][0] == ':': extensions_dir = '/usr/share/pylux/extension/' module_name = inputs[0].split(':')[1] try: ext_spec = IL.spec_from_file_location(module_name, extensions_dir+module_name+'.py') ext_module = IL.module_from_spec(ext_spec) ext_spec.loader.exec_module(ext_module) except ImportError: print('No extension with this name!') else: ext_module.run_pylux_extension(plot_file) #try: #except Exception: # print('This module is not a valid Pylux extension!') # Utility actions elif inputs[0] == 'h': text = "" with open('help.txt') as man: for line in man: text = text+line print(text) elif inputs[0] == 'c': os.system('cls' if os.name == 'nt' else 'clear') elif inputs[0] == 'q': print('Autosaving changes...') plot_file.save() sys.exit() elif inputs[0] == 'Q': print('Ignoring changes and exiting...') sys.exit() else: print('Error: Command doesn\'t exist.') print('Type \'h\' for a list of available commands.') # Check that the program isn't imported, then run main if __name__ == '__main__': main() PKJBHj. import pylux.plot as plot import pylux.clihelper as clihelper import xml.etree.ElementTree as ET import gi.repository gi.require_version('Gtk', '3.0') from gi.repository import Gtk class MainWindow(Gtk.Window): """The main window in which things happen. Consists of a notebook, each page of which edits different components of the plot file. """ def __init__(self): Gtk.Window.__init__(self, title='Pylux') self.main_container = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) self.set_default_size(500, 800) self.add(self.main_container) self.main_notebook = Gtk.Notebook() self.main_container.pack_start(self.main_notebook, True, True, 0) self.main_notebook.append_page(FixturesPage(), Gtk.Label('Fixtures')) self.main_notebook.append_page(RegistriesPage(), Gtk.Label('Registries')) self.main_notebook.append_page(CuesPage(), Gtk.Label('Cues')) class FixturesPage(Gtk.ScrolledWindow): def __init__(self): Gtk.ScrolledWindow.__init__(self, None, None) self.fixture_list = Gtk.ListBox() self.add_with_viewport(self.fixture_list) self.fixtures = plot.FixtureList(PLOT_FILE).fixtures for fixture in self.fixtures: listbox_row = self.FixtureListItem(fixture) self.fixture_list.add(listbox_row) class FixtureListItem(Gtk.ListBoxRow): """Display a single fixture and some action buttons. An extension of ListBoxRow that displays a fixture's name or type, and a series of action buttons to perform actions on that fixture. """ def __init__(self, fixture): """Initialise the ListBoxRow and add the buttons.""" Gtk.ListBoxRow.__init__(self) self.fixture = fixture self.container_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.add(self.container_box) # Label showing fixture name or type self.name_label = Gtk.Label(clihelper.get_fixture_print(fixture)) self.container_box.pack_start(self.name_label, False, True, 0) # Action buttons button_info = Gtk.Button.new_from_icon_name('dialog-information', 1) button_info.connect('clicked', self.fixture_getall) button_clone = Gtk.Button.new_from_icon_name('edit-copy', 1) button_clone.connect('clicked', self.fixture_clone) button_delete = Gtk.Button.new_from_icon_name('edit-delete', 1) button_delete.connect('clicked', self.fixture_remove) self.container_box.pack_end(button_delete, False, False, 0) self.container_box.pack_end(button_clone, False, False, 0) self.container_box.pack_end(button_info, False, False, 0) def fixture_getall(self, widget): info_window = FixturesPage.FixtureInfoWindow(self.fixture) info_window.connect('delete-event', info_window.destroy) info_window.show_all() def fixture_clone(self, widget): print('Doing literally nothing.') def fixture_remove(self, widget): print('Yeah, about that...') class FixtureInfoWindow(Gtk.Window): """A window showing the result of getall.""" def __init__(self, fixture): self.fixture = fixture fixture_print = clihelper.get_fixture_print(fixture) Gtk.Window.__init__(self, title='Editing '+fixture_print) # Make the list store and populate self.list_store = Gtk.ListStore(str, str) for tag, value in self.fixture.data.items(): self.list_store.append([tag, value]) # Make the tree view from the store self.tree_view = Gtk.TreeView(self.list_store) self.add(self.tree_view) renderer = Gtk.CellRendererText() renderer_edit = Gtk.CellRendererText(editable=True) tag_column = Gtk.TreeViewColumn('Tag', renderer, text=0) self.tree_view.append_column(tag_column) value_column = Gtk.TreeViewColumn('Value', renderer_edit, text=1) self.tree_view.append_column(value_column) # Manage the selection selection = self.tree_view.get_selection() selection.connect('changed', self.selection_change) # Manage the editing renderer_edit.connect('edited', self.property_edit) def selection_change(self, selection): model, list_iter = selection.get_selected() if list_iter != None: print(model[list_iter][0]) def property_edit(self, widget, path, text): self.list_store[path][1] = text self.fixture.data[self.list_store[path][0]] = text self.fixture.save() class RegistriesPage(Gtk.ScrolledWindow): def __init__(self): Gtk.ScrolledWindow.__init__(self, None, None) self.registry_list = Gtk.ListBox() self.add_with_viewport(self.registry_list) self.registries = plot.RegistryList(PLOT_FILE).registries for registry in self.registries: listbox_row = self.RegistryListItem(registry) self.registry_list.add(listbox_row) class RegistryListItem(Gtk.ListBoxRow): def __init__(self, registry): Gtk.ListBoxRow.__init__(self) self.registry = registry self.container_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.add(self.container_box) # Label showing universe self.universe_label = Gtk.Label(registry.universe) self.container_box.pack_start(self.universe_label, False, True, 0) # Action buttons button_list = Gtk.Button.new_from_icon_name('dialog-information', 1) self.container_box.pack_end(button_list, False, False, 0) class CuesPage(Gtk.ScrolledWindow): def __init__(self): Gtk.ScrolledWindow.__init__(self, None, None) self.cues_list = Gtk.ListBox() self.add_with_viewport(self.cues_list) for cue in sorted(plot.CueList(PLOT_FILE).cues, key=lambda q: q.key): listbox_row = self.CueListItem(cue) self.cues_list.add(listbox_row) class CueListItem(Gtk.ListBoxRow): def __init__(self, cue): Gtk.ListBoxRow.__init__(self) self.cue = cue self.container_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.add(self.container_box) # Label showing key, type and location self.key_label = Gtk.Label(cue.key) self.type_label = Gtk.Label(cue.data['type']) self.location_label = Gtk.Label(cue.data['location']) self.container_box.pack_start(self.key_label, False, True, 0) self.container_box.pack_start(self.type_label, False, True, 10) self.container_box.pack_start(self.location_label, False, True, 10) # Action buttons button_list = Gtk.Button.new_from_icon_name('dialog-information', 1) button_mvup = Gtk.Button.new_from_icon_name('go-up', 1) button_mvdn = Gtk.Button.new_from_icon_name('go-down', 1) button_remove = Gtk.Button.new_from_icon_name('edit-delete', 1) self.container_box.pack_end(button_remove, False, False, 0) self.container_box.pack_end(button_mvdn, False, False, 0) self.container_box.pack_end(button_mvup, False, False, 0) self.container_box.pack_end(button_list, False, False, 0) class SplashWindow(Gtk.Window): """Window prompting user to load a plot file. Displayed when no plot file is loaded. """ def __init__(self): Gtk.Window.__init__(self, title='Welcome to Pylux') self.set_default_size(650, 400) self.main_container = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) self.add(self.main_container) self.main_container.pack_start(Gtk.Label('no plot file'), True, True, 0) def main(): if PLOT_FILE.file == None: window = SplashWindow() else: window = MainWindow() window.connect('delete-event', DEBUG__shutdown_WITH_SAVE____) window.show_all() Gtk.main() def DEBUG__shutdown_WITH_SAVE____(a, b): print(a) print(b) Gtk.main_quit() PLOT_FILE.save() if __name__ == 'pylux_root': main() PKlPHaLupylux/fixture/p650.xml Hutton P650 650 p650 PK"lPH1pylux/fixture/patt23.xml Strand Patt.23 500 patt23 3.75 8 PK]PH|+pylux/fixture/ledjrgbpar.xml LEDJ RGB PAR ledpar56 red,green,blue,colour_macro,strobe,multi_function PK lPHr煙 pylux/fixture/eurolitestrobe.xml Eurolite LED Disco Stroboscope ministrobe 0.4 PK*lPHهpylux/fixture/quartetf.xml Strand Quartet F 500 quartetf 3.3 PKSqPH; ʆpylux/fixture/par64mfl.xml PAR64 MFL par64mfl 1000 PKt]PH80x''pylux/fixture/betapack2.xml Zero88 Betapack 2 PHANTOM True 12 channel_1,channel_2,channel_3,channel_4,channel_5,channel_6 PKlPHqR||pylux/fixture/generic.xml Generic Incandescent generic PK]PH\cTpylux/fixture/showtecpar56.xml Showtec LED PAR56 ledpar56 2.3 15 red,green,blue,colour_wheel,strobe,multi_function PKlPHnX|pylux/fixture/coda1000.xml Strand Coda 1000 1000 coda1000 4.3 PKuv7H pylux/context/texlux.py# reporter.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Generate text reports from Jinja templates. TeXlux uses a Jinja2 template to generate documentation from the plot file. It was made primarily for LaTeX documentation but could just as easily be used for other formats. """ from jinja2 import Environment, FileSystemLoader import os import pylux.plot as plot import pylux.clihelper as clihelper from pylux import get_data from pylux.context.context import Context class Report: def __init__(self): self.environment = Environment(loader=FileSystemLoader(get_data('template'))) def generate(self, template): template = self.environment.get_template(template) cue_list = sorted(plot.CueList(GLOBALS['PLOT_FILE']).cues, key=lambda cue: cue.key) fixture_list = plot.FixtureList(GLOBALS['PLOT_FILE']).fixtures self.content = template.render(cues=cue_list, fixtures=fixture_list) class ReporterContext(Context): def __init__(self): self.name = 'reporter' self.init_commands() self.register('rn', self.report_new, 1) self.register('rg', self.report_get, 1) self.register('rw', self.report_write, 1) def report_new(self, parsed_input): self.report = Report() self.report.generate(parsed_input[1]+'.jinja') def report_get(self, parsed_input): print(self.report) def report_write(self, parsed_input): with open(os.path.expanduser(parsed_input[0])) as outfile: outfile.write(self.report) def get_context(): return ReporterContext() PKmMH@ (pylux/context/context.py# context.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from pylux.clihelper import resolve_input, Interface from importlib import import_module from tabulate import tabulate import logging import sys import os class Context: """A context defines a set of commands that the user can access. When a user enters a context, all commands that the user enters are processed by the context using the process function. This base context class provides a globals piping infrastructure, a processing function and some commands that should be available in all contexts. Attributes: commands: a dictionary of tuples defining commands the user can invoke. plot_file: the PlotFile object containing the plot file. config: the parsed configuration file. log_level: the logging level defined on launch. interface: the Interface object being used. """ def __init__(self): """Add the universal commands to the commands dictionary.""" self.commands = {} self.register(Command('c', self.utility_clear, [], synopsis='Clear the screen.')) self.register(Command('h', self.utility_help, ['command'], synopsis='Get information about a command.')) self.register(Command('Q', self.utility_kill, [], synopsis='Exit the program without saving any changes.')) def post_init(self): """Initialisation phase run once globals are loaded.""" return None def process(self, inputs): """From input, perform the required function call. Given the user input, search in the commands dictionary for a command with the correct mnemonic. Then parse the input using clihelper given the number of arguments required by the function. Args: inputs: the user input, split by . """ command = self.commands[inputs[0]] parsed_input = resolve_input(inputs, command.nargs) command.function(parsed_input) def set_globals(self, globals_dict): """Set globals from a dictionary. Set the attributes that are considered 'globals' from the contents of globals_dict. Args: globals_dict: a dictionary containing the values of the predefined globals. """ self.plot_file = globals_dict['PLOT_FILE'] self.config = globals_dict['CONFIG'] self.log_level = globals_dict['LOG_LEVEL'] self.interface = Interface() logging.basicConfig(level=self.log_level) self.post_init() def get_globals(self): """Get the current globals dictionary. Returns a dictionary containing the globals in their current state, ready to be passed into another context using the set_globals command. Returns: A dictionary containing the 'global' attributes. """ globals_dict = { 'PLOT_FILE': self.plot_file, 'CONFIG': self.config, 'LOG_LEVEL': self.log_level} return globals_dict def register(self, command): """Register a command in the command dictionary. Add a command to the list of commands the user can invoke. """ self.commands[command.mnemonic] = command def utility_clear(self, parsed_input): """Utility to clear the screen using system call.""" os.system('cls' if os.name == 'nt' else 'clear') def utility_kill(self, parsed_input): """Utility to exit the program without warning.""" sys.exit() def utility_help(self, parsed_input): """Print a list of all available commands.""" if len(parsed_input) > 1: if parsed_input[0] not in self.commands: print('Error: Command does not exist') else: command = self.commands[parsed_input[0]] print('Usage:') usage = ' '+command.mnemonic for arg in command.arguments: usage = usage+' '+arg print(usage) print('Description:') print(' '+str(command.synopsis)) else: command_table = [] for mnemonic in self.commands: table_row = [] command = self.commands[mnemonic] usage = mnemonic for arg in command.arguments: usage = usage+' '+arg table_row.append(usage) table_row.append(command.function.__name__) command_table.append(table_row) command_table.sort(key=lambda command: command[1]) print(tabulate(command_table, headers=['Usage', 'Function'], tablefmt=self.config['cli']['help-table-format'])) class Command: def __init__(self, mnemonic, function, arguments, synopsis=None): self.mnemonic = mnemonic self.function = function self.arguments = arguments self.nargs = len(self.arguments) self.synopsis = synopsis PKAOHRpylux/context/reporter.py# reporter.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Generate text reports from Jinja templates. Reporter uses a Jinja2 template to generate documentation from the plot file. It was made primarily for LaTeX documentation but could just as easily be used for other formats. """ from jinja2 import Environment, FileSystemLoader import os import pylux.plot as plot import pylux.clihelper as clihelper from pylux import get_data from pylux.context.context import Context, Command class Report: def __init__(self, plot_file): self.environment = Environment(lstrip_blocks=True, trim_blocks=True, loader=FileSystemLoader(get_data('template/'))) self.plot_file = plot_file def find_template(self, template): all_templates = os.listdir(get_data('template')) discovered = {} for template_file in all_templates: if template_file.split('.')[0] == template: discovered[template_file.split('.')[1]] = template_file return discovered def generate(self, template, options): """Generate a report. Args template: full name, including extension of the template options: dict of options """ def is_hung(fixture): if 'posX' not in fixture.data or 'posY' not in fixture.data: return False else: return True def is_dimmer(fixture): if 'is_dimmer' in fixture.data: if fixture.data['is_dimmer'] == 'True': return True else: return False else: return False template = self.environment.get_template(template) # Create cues list cues = plot.CueList(self.plot_file) cues.assign_identifiers(self.plot_file) cue_list = sorted(cues.cues, key=lambda cue: cue.key) # Create fixtures list fixtures = plot.FixtureList(self.plot_file) fixtures.assign_usitt_numbers() fixture_list = sorted(fixtures.fixtures, key=lambda fixture: fixture.data['usitt_key']) # Create hung fixtures list hung_fixtures = [] for fixture in fixture_list: if is_hung(fixture): hung_fixtures.append(fixture) hung_fixtures.sort(key=lambda fixture: fixture.data['usitt_key']) # Create dimmer list dimmers = [] for fixture in fixture_list: if is_dimmer(fixture): power = 0 for controlled in fixtures.get_fixtures_for_dimmer(fixture): if 'power' in controlled.data: power = power+int(controlled.data['power']) fixture.data['power'] = power dimmers.append(fixture) # Create metadata list metadata_list = plot.Metadata(self.plot_file).meta total_power = 0 for dimmer in dimmers: total_power = total_power+dimmer.data['power'] metadata_list['total_power'] = total_power # Render template self.content = template.render(cues=cue_list, fixtures=fixture_list, meta=metadata_list, hung=hung_fixtures, dimmers=dimmers, options=options) class ReporterContext(Context): def __init__(self): self.name = 'reporter' super().__init__() self.register(Command('rn', self.report_new, ['template', 'options'], synopsis='Create a new report from the Jinja template ' 'and pass in the options.')) self.register(Command('rg', self.report_get, [], synopsis='Print the report buffer.')) self.register(Command('rw', self.report_write, ['path'], synopsis='Write the report buffer to a file.')) def report_new(self, parsed_input): self.report = Report(self.plot_file) def get_options(parsed_input): if len(parsed_input) > 1: options = {} options_input = parsed_input[1].split(';') for option in options_input: option_name = option.split('=')[0] option_values = option.split('=')[1].split(',') options[option.split('=')[0]] = option.split('=')[1] return options def get_template(self, parsed_input): possible_templates = self.report.find_template(parsed_input[0]) if len(possible_templates) == 0: return None elif len(possible_templates) == 1: return list(possible_templates.values())[0] else: print('The template you entered has '+ str(len(possible_templates))+' matches: ') print(possible_templates) ext = input('Choose an extension to continue: ') return possible_templates[ext] options = get_options(parsed_input) template = get_template(self, parsed_input) if template != None: self.report.generate(template, options) def report_get(self, parsed_input): print(self.report.content) def report_write(self, parsed_input): with open(os.path.expanduser(parsed_input[0]), 'w') as outfile: outfile.write(self.report.content) def get_context(): return ReporterContext() PKQpPHn>n>pylux/context/editor.py# editor.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Edit the content of Pylux plot files. editor is a CLI implementation of the Pylux plot editor, allowing for the reading and editing of Pylux plot files. """ import os import pylux.plot as plot import pylux.clihelper as clihelper import logging from pylux.context.context import Context, Command from pylux import get_data from pylux.exception import * class EditorContext(Context): def __init__(self): """Registers commands and globals for this context.""" super().__init__() self.name = 'editor' # Register commands self.register(Command('q', self.save_quit, [])) self.register(Command('fo', self.file_open, ['path'], synopsis='Open a plot file.')) self.register(Command('fw', self.file_write, [], synopsis='Write the buffer to the original ' 'location.')) self.register(Command('fW', self.file_writeas, ['path'], synopsis='Write the buffer to a different ' 'location.')) self.register(Command('fg', self.file_get, [], synopsis='Print the location of the plot file.')) self.register(Command('fn', self.file_new, [], synopsis='Create a new plot file.')) self.register(Command('ml', self.metadata_list, [], synopsis='List all metadata values.')) self.register(Command('ms', self.metadata_set, ['name', 'value'], synopsis='Set the value of one piece of ' 'metadata.')) self.register(Command('mr', self.metadata_remove, ['name'], synopsis='Remove a piece of metadata')) self.register(Command('mg', self.metadata_get, ['name'], synopsis='Print the value of a piece of ' 'metadata.')) self.register(Command('xn', self.fixture_new, ['template'], synopsis='Create a new fixture from a ' 'template.')) self.register(Command('xc', self.fixture_clone, ['fixture'], synopsis='Create a new fixture from an existing ' 'fixture.')) self.register(Command('xl', self.fixture_list, [], synopsis='List all fixtures.')) self.register(Command('xf', self.fixture_filter, ['tag', 'value'], synopsis='List all fixtures that match certain ' 'criteria.')) self.register(Command('xr', self.fixture_remove, ['fixture'], synopsis='Remove a fixture.')) self.register(Command('xg', self.fixture_get, ['fixture', 'tag'], synopsis='Print the value of a fixture\'s tag.')) self.register(Command('xG', self.fixture_getall, ['fixture'], synopsis='Print the values of all of a ' 'fixture\'s tags.')) self.register(Command('xs', self.fixture_set, ['fixture', 'tag', 'value'], synopsis='Set the value of a fixture\'s tag.')) self.register(Command('xa', self.fixture_address, ['fixture', 'universe', 'address'], synopsis='Assign DMX addresses to a fixture.')) self.register(Command('xA', self.fixture_unaddress, ['fixture'], synopsis='Remove the DMX addresses assigned ' 'to a fixture.')) self.register(Command('rl', self.registry_list, ['universe'], synopsis='List the functions of the DMX ' 'channels in a universe.')) self.register(Command('rL', self.registry_probe, ['universe'], synopsis='List the functions of the DMX ' 'channels in a universe and, if there ' 'are any dimmers, list the fixtures ' 'that they control.')) self.register(Command('ql', self.cue_list, [], synopsis='List all the cues.')) self.register(Command('qn', self.cue_new, ['type', 'location'], synopsis='Add a new cue.')) self.register(Command('qr', self.cue_remove, ['cue'], synopsis='Remove a cue.')) self.register(Command('qs', self.cue_set, ['cue', 'tag', 'value'], synopsis='Set the value of a cue\'s tag.')) self.register(Command('qg', self.cue_get, ['cue', 'tag'], synopsis='Print the value of a cue\'s tag.')) self.register(Command('qG', self.cue_getall, ['cue'], synopsis='Print the values of all of a cue\'s ' 'tags.')) self.register(Command('qm', self.cue_moveafter, ['cue', 'dest_cue'], synopsis='Move a cue after another.')) self.register(Command('qM', self.cue_movebefore, ['cue', 'dest_cue'], synopsis='Move a cue before another.')) def save_quit(self, parsed_input): self.file_write(parsed_input) self.utility_kill(parsed_input) def file_open(self, parsed_input): try: self.plot_file.load(parsed_input[0]) except FileNotFoundError: logging.warning('No file with that name') except FileFormatError: logging.warning('File is not valid XML') def file_write(self, parsed_input): try: self.plot_file.write() except AttributeError: print('Error: No file is loaded') def file_writeas(self, parsed_input): self.plot_file.write_to(parsed_input[0]) def file_get(self, parsed_input): if self.plot_file.path is None: print('Using temporary plot file') else: print('Using plot file '+self.plot_file.path) def file_new(self, parsed_input): self.plot_file.new() def metadata_list(self, parsed_input): metadata = plot.Metadata(self.plot_file) for i in metadata.meta: print(i+': '+metadata.meta[i]) def metadata_set(self, parsed_input): metadata = plot.Metadata(self.plot_file) metadata.set_data(parsed_input[0], parsed_input[1]) def metadata_remove(self, parsed_input): metadata = plot.Metadata(self.plot_file) metadata.set_data(parsed_input[0], None) def metadata_get(self, parsed_input): metadata = plot.Metadata(self.plot_file) print(parsed_input[0]+': '+metadata.get_data(parsed_input[0])) def fixture_new(self, parsed_input): template_file = get_data('fixture/'+parsed_input[0]+'.xml') try: fixture = plot.Fixture(self.plot_file, template=template_file) except FileNotFoundError: print('Error: No template with this name') def fixture_clone(self, parsed_input): src_fixtures = self.interface.get(parsed_input[0]) for src in src_fixtures: new_fixture = plot.Fixture(self.plot_file, src_fixture=src) def fixture_list(self, parsed_input): fixtures = plot.FixtureList(self.plot_file) i = 1 self.interface.clear() for fixture in fixtures.fixtures: if 'name' in fixture.data: name = fixture.data['name'] else: name = fixture.data['type'] if self.config['cli']['show-uuids'] == 'True': print('\033[4m'+str(i)+'\033[0m '+name+', id: '+fixture.uuid) else: print('\033[4m'+str(i)+'\033[0m '+name) self.interface.append(i, fixture) i = i+1 def fixture_filter(self, parsed_input): key = parsed_input[0] value = parsed_input[1] fixtures = plot.FixtureList(self.plot_file) self.interface.clear() i = 1 for fixture in fixtures.fixtures: if key in fixture.data: if fixture.data[key] == value: if 'name' in fixture.data: name = fixture.data['name'] else: name = fixture.data['type'] if self.config['cli']['show-uuids'] == 'True': print('\033[4m'+str(i)+'\033[0m '+name+ ', id: '+fixture.uuid) else: print('\033[4m'+str(i)+'\033[0m '+name) self.interface.append(i, fixture) i = i+1 else: pass def fixture_remove(self, parsed_input): fixture_list = plot.FixtureList(self.plot_file) fixtures = self.interface.get(parsed_input[0]) registries = plot.RegistryList(self.plot_file) for fixture in fixtures: fixture.unaddress(registries) fixture_list.remove(fixture) def fixture_get(self, parsed_input): fixtures = self.interface.get(parsed_input[0]) for fixture in fixtures: print(fixture.get_data(parsed_input[1])) self.interface.update_this(parsed_input[0]) def fixture_getall(self, parsed_input): fixtures = self.interface.get(parsed_input[0]) for fixture in fixtures: for data_item in fixture.data: print(data_item+': '+str(fixture.data[data_item])) self.interface.update_this(parsed_input[0]) def fixture_set(self, parsed_input): fixtures = self.interface.get(parsed_input[0]) tag = parsed_input[1] value = parsed_input[2] for fixture in fixtures: # See if it is a special pseudo tag if tag == 'position': fixture.set_data('posX', value.split(',')[0]) fixture.set_data('posY', value.split(',')[1]) elif tag == 'focus': fixture.set_data('focusX', value.split(',')[0]) fixture.set_data('focusY', value.split(',')[1]) elif tag == 'dimmer': fixture.set_data('dimmer_uuid', self.interface.get(value.split(',')[0])[0].uuid) fixture.set_data('dimmer_channel', value.split(',')[1]) # Otherwise just set it else: fixture.set_data(tag, value) self.interface.update_this(parsed_input[0]) def fixture_address(self, parsed_input): fixtures = self.interface.get(parsed_input[0]) registry = plot.DmxRegistry(self.plot_file, parsed_input[1]) required_channels = len(fixtures[0].data['dmx_functions'].split(',')) if parsed_input[2] == 'auto': start_address = registry.get_start_address(required_channels) else: start_address = int(parsed_input[2]) fixtures[0].address(registry, start_address) self.interface.update_this(parsed_input[0]) def fixture_unaddress(self, parsed_input): fixtures = self.interface.get(parsed_input[0]) for fixture in fixtures: fixture.unaddress(self.plot_file) def registry_list(self, parsed_input): registry = plot.DmxRegistry(self.plot_file, parsed_input[0]) for channel in registry.registry: functions = registry.get_functions(channel) for function in functions: fixture = plot.Fixture(self.plot_file, uuid=function[0]) print_name = clihelper.get_fixture_print(fixture) print(str(format(channel, '03d'))+' '+print_name+' ('+ function[1]+')') def registry_probe(self, parsed_input): registry = plot.DmxRegistry(self.plot_file, parsed_input[0]) for channel in registry.registry: functions = registry.get_functions(channel) for function in functions: fixture = plot.Fixture(self.plot_file, uuid=function[0]) print_name = clihelper.get_fixture_print(fixture) print(str(format(channel, '03d'))+' '+print_name+' ('+ function[1]+')') if ('is_dimmer' in fixture.data and fixture.data['is_dimmer'] == 'True'): dimmer_chan = function[1].replace('channel_', '') fixtures = plot.FixtureList(self.plot_file) for lantern in fixtures.fixtures: if ('dimmer_uuid' in lantern.data and lantern.data['dimmer_uuid'] == function[0] and lantern.data['dimmer_channel'] == dimmer_chan): print_name = clihelper.get_fixture_print(lantern) print(' ⤷ '+print_name) def cue_list(self, parsed_input): cues = plot.CueList(self.plot_file) self.interface.clear() for cue in cues.cues: cue_type = cue.data['type'] cue_location = cue.data['location'] print('\033[4m'+str(cue.key)+'\033[0m ('+cue_type+') at '+ cue_location) self.interface.append(cue.key, cue) def cue_new(self, parsed_input): cue = plot.Cue(self.plot_file) cue.data['type'] = parsed_input[0] cue.data['location'] = parsed_input[1] cue.save(self.plot_file) def cue_remove(self, parsed_input): cues = plot.CueList(self.plot_file) removal_candidates = self.interface.get(parsed_input[0]) for rc in removal_candidates: cues.remove(self.plot_file, rc.uuid) def cue_set(self, parsed_input): cues_to_change = self.interface.get(parsed_input[0]) for cue in cues_to_change: cue.data[parsed_input[1]] = parsed_input[2] cue.save(self.plot_file) def cue_get(self, parsed_input): cues_to_get = self.interface.get(parsed_input[0]) for cue in cues_to_get: if parsed_input[1] in cue.data: print(cue.data[parsed_input[1]]) else: print(None) def cue_getall(self, parsed_input): cues_to_get = self.interface.get(parsed_input[0]) for cue in cues_to_get: for data_item in cue.data: print(data_item+': '+cue.data[data_item]) def cue_moveafter(self, parsed_input): cues = plot.CueList(self.plot_file) cues.move_after(self.plot_file, int(parsed_input[0]), int(parsed_input[1])) def cue_movebefore(self, parsed_input): cues = plot.CueList(self.plot_file) cues.move_before(self.plot_file, int(parsed_input[0]), int(parsed_input[1])) def get_context(): return EditorContext() PKEO7H`,,pylux/context/__init__.py"""Additional contexts for Pylux editor.""" PKqPH`Yc4N4Npylux/context/plotter.py# plotter.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Generate SVG lighting plots. Context that provides commands to create lighting plot images in SVG format. """ from pylux.context.context import Context, Command import os.path import logging import math import cairosvg import xml.etree.ElementTree as ET import pylux.plot as plot import pylux.clihelper as clihelper import pylux.reference as reference from pylux import get_data class LightingPlot(): def __init__(self, plot_file, options): self.fixtures = plot.FixtureList(plot_file).fixtures self.fixtures = self.get_hung_fixtures() for fixture in self.fixtures: self.set_empty_fixture_data(fixture) self.meta = plot.Metadata(plot_file).meta self.options = options def get_hung_fixtures(self): """Return a list of the fixtures that are used. Trim the fixtures list so that it only contains fixtures which are hung in the plot. In other words, remove fixtures which don\'t have position or focus attributes. Returns: A list of fixture objects which are used in the plot. """ hung_fixtures = [] for fixture in self.fixtures: if ('posX' in fixture.data and 'posY' in fixture.data and 'focusX' in fixture.data and 'focusY' in fixture.data): hung_fixtures.append(fixture) return hung_fixtures def set_empty_fixture_data(self, fixture): """If a fixture has empty data slots, set to defaults. Sets gel to white. """ if 'gel' not in fixture.data: fixture.data['gel'] = 'White' def get_page_dimensions(self): """Return the physical size of the paper. Search in the reference module for the paper size in mm then determine coordinate order based on orientation. Returns: A tuple in the form (X, Y) of the dimensions of the paper. """ paper_type = self.options['paper-size'] orientation = self.options['orientation'] dimensions = reference.paper_sizes[paper_type] if orientation == 'portrait': return dimensions elif orientation == 'landscape': return (dimensions[1], dimensions[0]) def get_margin_bounds(self): # This could be a function to get margin coordinates to make # placement easier, but it isn't return None def get_plot_size(self): """Return the physical size of the plot area. From the fixtures\' locations and focus points, find the greatest and least values of X and Y then calculate the dimensions that the plot covers. Returns: A tuple in the form (X, Y) of the dimensions of the plot. """ x_values = [] y_values = [] for fixture in self.fixtures: get_mm = lambda field: float(field)*1000 x_values.append(get_mm(fixture.data['posX'])) x_values.append(get_mm(fixture.data['focusX'])) y_values.append(get_mm(fixture.data['posY'])) y_values.append(get_mm(fixture.data['focusY'])) x_range = max(x_values) - min(x_values) y_range = max(y_values) - min(y_values) return (x_range, y_range) def can_fit_page(self): """Test if the plot can fit on the page. Uses the size of the page and scaling to determine whether the page is large enough to fit the plot. """ actual_size = self.get_plot_size() scaling = float(self.options['scale']) get_scaled = lambda dim: dim/scaling scaled_size = (get_scaled(actual_size[0]), get_scaled(actual_size[1])) paper_size = self.get_page_dimensions() remove_margin = lambda dim: dim-2*float(self.options['margin']) draw_area = (remove_margin(paper_size[0]), remove_margin(paper_size[1])) if draw_area[0] < scaled_size[0] or draw_area[1] < scaled_size[1]: return False else: return True def get_empty_plot(self): """Get an ElementTree tree with no content. Make a new ElementTree with a root svg element, set the properties of the svg element to match paper size. """ page_dims = self.get_page_dimensions() svg_root = ET.Element('svg') svg_root.set('width', str(page_dims[0])) svg_root.set('height', str(page_dims[1])) svg_root.set('xmlns', 'http://www.w3.org/2000/svg') svg_tree = ET.ElementTree(element=svg_root) return svg_tree def get_page_border(self): """Get the page border ready to be put into the plot. Returns a path element that borders the plot on all four sides. Returns: An ElementTree element - an SVG path. """ margin = float(self.options['margin']) weight = float(self.options['line-weight-heavy']) paper = self.get_page_dimensions() border = ET.Element('path') border.set('d', 'M '+str(margin)+' '+str(margin)+' ' 'L '+str(paper[0]-margin)+' '+str(margin)+' ' 'L '+str(paper[0]-margin)+' '+str(paper[1]-margin)+' ' 'L '+str(margin)+' '+str(paper[1]-margin)+' ' 'L '+str(margin)+' '+str(margin)) border.set('fill', 'white') border.set('stroke', 'black') border.set('stroke-width', str(weight)) return border def get_centre_line(self): """Get the centre line to insert. Returns a path element that represents the centre line, containing the recommended dash appearance. Returns: An ElementTree element - an SVG path. """ centre = self.get_page_dimensions()[0]/2 height = self.get_page_dimensions()[1] margin = float(self.options['margin']) centre_line = ET.Element('path') if self.options['centre-line-extend'] == 'True': centre_line.set('d', 'M '+str(centre)+' 0 ' 'L '+str(centre)+' '+str(height)) else: centre_line.set('d', 'M '+str(centre)+' '+str(margin)+' ' 'L '+str(centre)+' '+str(height-margin)) centre_line.set('stroke', 'black') centre_line.set('stroke-width', str(self.options['line-weight-medium'])) centre_line.set('stroke-dasharray', self.options['centre-line-dasharray']) return centre_line def get_plaster_line(self): """Get the plaster line to insert. Returns a path element that represents the plaster line. Returns: An ElementTree element - an SVG path. """ scale = float(self.options['scale']) padding = float(self.options['plaster-line-padding'])*1000/scale margin = float(self.options['margin']) width = self.get_page_dimensions()[0] plaster_line = ET.Element('path') if self.options['plaster-line-extend'] == 'True': plaster_line.set('d', 'M 0 '+str(margin+padding)+' ' 'L '+str(width)+' '+str(margin+padding)) else: plaster_line.set('d', 'M '+str(margin)+' '+str(margin+padding)+' ' 'L '+str(width-margin)+' '+ str(margin+padding)) plaster_line.set('stroke', 'black') plaster_line.set('stroke-width', str(self.options['line-weight-medium'])) plaster_line.set('stroke-dasharray', self.options['plaster-line-dasharray']) return plaster_line def get_plaster_coord(self): """Get the plaster line y coordinate. Returns the plaster line y coordinate to allow offsets to be calculated when plotting fixtures. Returns: A float representing the y coordinate in mm. """ scale = float(self.options['scale']) margin = float(self.options['margin']) padding = float(self.options['plaster-line-padding'])*1000/scale return margin+padding def get_background_image(self): """Get the background image from file. Returns: The first group element of the SVG image file. """ scale = float(self.options['scale']) svg_ns = {'ns0': 'http://www.w3.org/2000/svg'} xloc = self.get_page_dimensions()[0]/2 yloc = self.get_plaster_coord() image_file = os.path.expanduser(self.options['background-image']) image_tree = ET.parse(image_file) image_root = image_tree.getroot() image_group = image_root.find('ns0:g', svg_ns) image_group.set('transform', 'scale('+str(1/scale)+') ' 'translate('+str(xloc*scale)+' '+ str(yloc*scale)+')') for path in image_group: path_class = path.get('class') if path.get('class') in reference.usitt_line_weights: weight = reference.usitt_line_weights[path_class] else: weight = 'line-weight-medium' path.set('stroke-width', str(float(self.options[weight])*scale)) return image_group def get_title_block(self): if self.options['title-block'] == 'corner': return self.get_title_corner() elif self.options['title-block'] == 'sidebar': return self.get_title_sidebar() elif self.options['title-block'] == None: return None def get_title_corner(self): """Get the title block ready to be put into the plot.""" return None def get_title_sidebar(self): """Get the title block in vertical form.""" def get_sidebar_width(self): page_dims = self.get_page_dimensions() pc_width = page_dims[0]*float(self.options['vertical-title-width-pc']) if pc_width > float(self.options['vertical-title-max-width']): return float(self.options['vertical-title-max-width']) elif pc_width < float(self.options['vertical-title-min-width']): return float(self.options['vertical-title-min-width']) else: return pc_width # Create sidebar group sidebar = ET.Element('g') # Create sidebar border sidebar_width = get_sidebar_width(self) page_dims = self.get_page_dimensions() margin = float(self.options['margin']) left_border = page_dims[0]-margin-sidebar_width sidebar_box = ET.SubElement(sidebar, 'path') sidebar_box.set('d', 'M '+str(left_border)+' '+str(margin)+ ' L '+str(left_border)+' '+str(page_dims[1]-margin)) sidebar_box.set('stroke', 'black') sidebar_box.set('stroke-width', str(self.options['line-weight-heavy'])) # Create title text text_title = ET.SubElement(sidebar, 'text') text_title.text = self.meta['production'] text_title.set('text-anchor', 'middle') text_title.set('x', str(page_dims[0]-margin-0.5*sidebar_width)) text_title.set('y', str(margin+10)) text_title.set('font-size', str(7)) text_title.set('style', 'text-transform:uppercase') return sidebar def get_fixture_icon(self, fixture): """Return an SVG group for a single fixture. Search the package data for a symbol for this fixture, then transform as appropriate based on tags and plot scaling. Args: fixture: the fixture object to create an icon for. Returns: An ElementTree object representing an SVG 'g' element. """ # Get the base SVG element symbol_name = fixture.data['symbol'] tree = ET.parse(get_data('symbol/'+symbol_name+'.svg')) root = tree.getroot() svg_ns = {'ns0': 'http://www.w3.org/2000/svg'} symbol = root.find('ns0:g', svg_ns) # Transform based on scaling and data centre = self.get_page_dimensions()[0]/2 plaster = self.get_plaster_coord() scale = float(self.options['scale']) plot_pos = lambda dim: (float(fixture.data['pos'+dim])*1000) rotation = fixture.get_rotation() colour = fixture.get_colour() symbol.set('transform', 'scale( '+str(1/scale)+' ) ' 'translate('+str(centre*scale+plot_pos('X'))+' '+ str(plot_pos('Y')+plaster*scale)+') ' 'rotate('+str(rotation)+')') for path in symbol: if path.get('class') == 'outer': path.set('fill', colour) path.set('stroke-width', str(float(self.options['line-weight-heavy'])*scale)) return symbol def generate_plot(self): if not self.can_fit_page(): print('PlotterError: Plot does not fit page with this scaling') else: self.lighting_plot = self.get_empty_plot() root = self.lighting_plot.getroot() root.append(self.get_page_border()) try: root.append(self.get_background_image()) except FileNotFoundError: print('Yeah it kind of didn\'t work. Just going to ignore this') root.append(self.get_centre_line()) root.append(self.get_plaster_line()) # root.append(self.get_title_block()) for fixture in self.fixtures: root.append(self.get_fixture_icon(fixture)) class PlotOptions(): def __init__(self, config): self.options = config['plotter'] def set(self, option, value): self.options[option] = value def get(self, option): if option in self.options: return self.options[option] else: return None class FixtureSymbol: """Manages the SVG symbols for fixtures.""" def __init__(self, fixture): """Load the fixture symbol file.""" self.fixture = fixture symbol_name = fixture.data['symbol'] tree = ET.parse(get_data('symbol/'+symbol_name+'.svg')) root = tree.getroot() self.ns = {'ns0': 'http://www.w3.org/2000/svg'} self.image_group = root.find('ns0:g', self.ns) def get_fixture_group(self): """Return a transformed symbol g element.""" posX_mm = float(self.fixture.data['posX'])*1000 posY_mm = float(self.fixture.data['posY'])*1000 rotation_deg = self.fixture.data['rotation'] colour = self.fixture.data['colour'] self.image_group.set('transform', 'translate('+ str(posX_mm)+' '+str(posY_mm)+') rotate('+str(rotation_deg)+')') for path in self.image_group: if path.get('class') == 'outer': path.set('fill', colour) return self.image_group def get_fixture_beam(self): """Return a beam path element.""" posX_mm = str(float(self.fixture.data['posX'])*1000) posY_mm = str(float(self.fixture.data['posY'])*1000) focusX_mm = str(float(self.fixture.data['focusX'])*1000) focusY_mm = str(float(self.fixture.data['focusY'])*1000) beam = ET.Element('path') beam.set('d', 'M '+posX_mm+' '+posY_mm+' L '+focusX_mm+' '+focusY_mm) beam.set('stroke', 'black') beam.set('stroke-width', '6') beam.set('stroke-dasharray', '10,10') return beam def get_circuit_icon(self): """Return a circuit and connector g element.""" rotation_deg = self.fixture.data['rotation'] posX_mm = float(self.fixture.data['posX'])*1000 posY_mm = float(self.fixture.data['posY'])*1000 connector_endX = posX_mm-200*math.cos(math.radians(rotation_deg)) connector_endY = posY_mm-200*math.sin(math.radians(rotation_deg)) icon_group = ET.Element('g') connector = ET.SubElement(icon_group, 'path') connector.set('d', 'M '+str(posX_mm)+' '+str(posY_mm)+ ' L '+str(connector_endX)+' '+str(connector_endY)) connector.set('stroke', 'black') connector.set('stroke-width', '3') circle = ET.SubElement(icon_group, 'circle') circle.set('cx', str(connector_endX)) circle.set('cy', str(connector_endY)) circle.set('r', '60') circle.set('stroke', 'black') circle.set('stroke-width', '6') circle.set('fill', 'white') text = ET.SubElement(icon_group, 'text') text.text = self.fixture.data['circuit'] text.set('x', str(connector_endX)) text.set('y', str(connector_endY)) text.set('font-size', '60') return icon_group class PlotterContext(Context): def __init__(self): super().__init__() self.name = 'plotter-dev' self.register(Command('pn', self.plot_new, [], synopsis='Create a new plot.')) self.register(Command('pw', self.plot_write, ['path'], synopsis='Write the plot buffer to a file and ' 'optionally convert to another ' 'format.')) self.register(Command('pd', self.plot_dump, [])) self.register(Command('os', self.option_set, ['name', 'value'], synopsis='Set the value of an option.')) self.register(Command('og', self.option_get, ['name'], synopsis='Print the value of an option.')) self.register(Command('ol', self.option_list, [], synopsis='Print the value of all options.')) self.register(Command('deb', self.debug, [])) def post_init(self): super().post_init() self.options = PlotOptions(self.config) def debug(self, parsed_input): self.plot_new(parsed_input) self.plot_write(['devplot.svg']) def plot_new(self, parsed_input): self.plot = LightingPlot(self.plot_file, self.options.options) self.plot.generate_plot() def plot_write(self, parsed_input): if parsed_input[0].split('.')[-1] == 'svg': self.plot.lighting_plot.write(os.path.expanduser(parsed_input[0])) elif parsed_input[0].split('.')[-1] == 'pdf': plot_bytes = ET.tostring(self.plot.lighting_plot.getroot()) cairosvg.svg2pdf(bytestring=plot_bytes, write_to=parsed_input[0]) else: print('WARNING: File format not supported, writing as SVG') self.plot.lighting_plot.write(os.path.expanduser(parsed_input[0])) def plot_dump(self, parsed_input): ET.dump(self.plot.lighting_plot.getroot()) def option_set(self, parsed_input): self.options.set(parsed_input[0], parsed_input[1]) def option_get(self, parsed_input): print(self.options.get(parsed_input[0])) def option_list(self, parsed_input): for option in self.options.options: print(option+': '+str(self.options.options[option])) def get_context(): return PlotterContext() PKuMHMGGpylux/context/plotterNEW.py# plotter.py is part of Pylux # # Pylux is a program for the management of lighting documentation # Copyright 2015 Jack Page # # Pylux is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pylux is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Generate SVG lighting plots. Context that provides commands to create lighting plot images in SVG format. """ from pylux.context.context import Context, Command import os.path import logging import math import cairosvg import xml.etree.ElementTree as ET import pylux.plot as plot import pylux.clihelper as clihelper import pylux.reference as reference from pylux import get_data class LightingPlot(): def __init__(self, plot_file, options): self.fixtures = plot.FixtureList(plot_file).fixtures self.fixtures = self.get_hung_fixtures() self.meta = plot.Metadata(plot_file).meta self.options = options def get_hung_fixtures(self): """Return a list of the fixtures that are used. Trim the fixtures list so that it only contains fixtures which are hung in the plot. In other words, remove fixtures which don\'t have position or focus attributes. Returns: A list of fixture objects which are used in the plot. """ hung_fixtures = [] for fixture in self.fixtures: if ('posX' in fixture.data and 'posY' in fixture.data and 'focusX' in fixture.data and 'focusY' in fixture.data): hung_fixtures.append(fixture) return hung_fixtures def get_page_dimensions(self): """Return the physical size of the paper. Search in the reference module for the paper size in mm then determine coordinate order based on orientation. Returns: A tuple in the form (X, Y) of the dimensions of the paper. """ paper_type = self.options['paper-size'] orientation = self.options['orientation'] dimensions = reference.paper_sizes[paper_type] if orientation == 'portrait': return dimensions elif orientation == 'landscape': return (dimensions[1], dimensions[0]) def get_margin_bounds(self): # This could be a function to get margin coordinates to make # placement easier, but it isn't return None def get_plot_size(self): """Return the physical size of the plot area. From the fixtures\' locations and focus points, find the greatest and least values of X and Y then calculate the dimensions that the plot covers. Returns: A tuple in the form (X, Y) of the dimensions of the plot. """ x_values = [] y_values = [] for fixture in self.fixtures: get_mm = lambda field: float(field)*1000 x_values.append(get_mm(fixture.data['posX'])) x_values.append(get_mm(fixture.data['focusX'])) y_values.append(get_mm(fixture.data['posY'])) y_values.append(get_mm(fixture.data['focusY'])) x_range = max(x_values) - min(x_values) y_range = max(y_values) - min(y_values) return (x_range, y_range) def can_fit_page(self): """Test if the plot can fit on the page. Uses the size of the page and scaling to determine whether the page is large enough to fit the plot. """ actual_size = self.get_plot_size() scaling = float(self.options['scale']) get_scaled = lambda dim: dim/scaling scaled_size = (get_scaled(actual_size[0]), get_scaled(actual_size[1])) paper_size = self.get_page_dimensions() remove_margin = lambda dim: dim-2*float(self.options['margin']) draw_area = (remove_margin(paper_size[0]), remove_margin(paper_size[1])) if draw_area[0] < scaled_size[0] or draw_area[1] < scaled_size[1]: return False else: return True def get_empty_plot(self): """Get an ElementTree tree with no content. Make a new ElementTree with a root svg element, set the properties of the svg element to match paper size. """ page_dims = self.get_page_dimensions() svg_root = ET.Element('svg') svg_root.set('width', str(page_dims[0])) svg_root.set('height', str(page_dims[1])) svg_root.set('xmlns', 'http://www.w3.org/2000/svg') svg_tree = ET.ElementTree(element=svg_root) return svg_tree def get_page_border(self): """Get the page border ready to be put into the plot. Returns a path element that borders the plot on all four sides. Returns: An ElementTree element - an SVG path. """ margin = float(self.options['margin']) weight = float(self.options['line-weight-heavy']) paper = self.get_page_dimensions() border = ET.Element('path') border.set('d', 'M '+str(margin)+' '+str(margin)+' ' 'L '+str(paper[0]-margin)+' '+str(margin)+' ' 'L '+str(paper[0]-margin)+' '+str(paper[1]-margin)+' ' 'L '+str(margin)+' '+str(paper[1]-margin)+' ' 'L '+str(margin)+' '+str(margin)) border.set('fill', 'white') border.set('stroke', 'black') border.set('stroke-width', str(weight)) return border def get_centre_line(self): """Get the centre line to insert. Returns a path element that represents the centre line, containing the recommended dash appearance. Returns: An ElementTree element - an SVG path. """ centre = self.get_page_dimensions()[0]/2 height = self.get_page_dimensions()[1] margin = float(self.options['margin']) centre_line = ET.Element('path') if self.options['centre-line-extend'] == 'True': centre_line.set('d', 'M '+str(centre)+' 0 ' 'L '+str(centre)+' '+str(height)) else: centre_line.set('d', 'M '+str(centre)+' '+str(margin)+' ' 'L '+str(centre)+' '+str(height-margin)) centre_line.set('stroke', 'black') centre_line.set('stroke-width', str(self.options['line-weight-medium'])) centre_line.set('stroke-dasharray', self.options['centre-line-dasharray']) return centre_line def get_plaster_line(self): """Get the plaster line to insert. Returns a path element that represents the plaster line. Returns: An ElementTree element - an SVG path. """ scale = float(self.options['scale']) padding = float(self.options['plaster-line-padding'])*1000/scale margin = float(self.options['margin']) width = self.get_page_dimensions()[0] plaster_line = ET.Element('path') if self.options['plaster-line-extend'] == 'True': plaster_line.set('d', 'M 0 '+str(margin+padding)+' ' 'L '+str(width)+' '+str(margin+padding)) else: plaster_line.set('d', 'M '+str(margin)+' '+str(margin+padding)+' ' 'L '+str(width-margin)+' '+ str(margin+padding)) plaster_line.set('stroke', 'black') plaster_line.set('stroke-width', str(self.options['line-weight-medium'])) plaster_line.set('stroke-dasharray', self.options['plaster-line-dasharray']) return plaster_line def get_plaster_coord(self): """Get the plaster line y coordinate. Returns the plaster line y coordinate to allow offsets to be calculated when plotting fixtures. Returns: A float representing the y coordinate in mm. """ scale = float(self.options['scale']) margin = float(self.options['margin']) padding = float(self.options['plaster-line-padding'])*1000/scale return margin+padding def get_title_block(self): if self.options['title-block'] == 'corner': return self.get_title_corner() elif self.options['title-block'] == 'sidebar': return self.get_title_sidebar() elif self.options['title-block'] == None: return None def get_title_corner(self): """Get the title block ready to be put into the plot.""" return None def get_title_sidebar(self): """Get the title block in vertical form.""" def get_sidebar_width(self): page_dims = self.get_page_dimensions() pc_width = page_dims[0]*float(self.options['vertical-title-width-pc']) if pc_width > float(self.options['vertical-title-max-width']): return float(self.options['vertical-title-max-width']) elif pc_width < float(self.options['vertical-title-min-width']): return float(self.options['vertical-title-min-width']) else: return pc_width # Create sidebar group sidebar = ET.Element('g') # Create sidebar border sidebar_width = get_sidebar_width(self) page_dims = self.get_page_dimensions() margin = float(self.options['margin']) left_border = page_dims[0]-margin-sidebar_width sidebar_box = ET.SubElement(sidebar, 'path') sidebar_box.set('d', 'M '+str(left_border)+' '+str(margin)+ ' L '+str(left_border)+' '+str(page_dims[1]-margin)) sidebar_box.set('stroke', 'black') sidebar_box.set('stroke-width', str(self.options['line-weight-heavy'])) # Create title text text_title = ET.SubElement(sidebar, 'text') text_title.text = self.meta['production'] text_title.set('text-anchor', 'middle') text_title.set('x', str(page_dims[0]-margin-0.5*sidebar_width)) text_title.set('y', str(margin+10)) text_title.set('font-size', str(7)) text_title.set('style', 'text-transform:uppercase') return sidebar def get_fixture_icon(self, fixture): """Return an SVG group for a single fixture. Search the package data for a symbol for this fixture, then transform as appropriate based on tags and plot scaling. Args: fixture: the fixture object to create an icon for. Returns: An ElementTree object representing an SVG 'g' element. """ # Get the base SVG element symbol_name = fixture.data['symbol'] tree = ET.parse(get_data('symbol/'+symbol_name+'.svg')) root = tree.getroot() svg_ns = {'ns0': 'http://www.w3.org/2000/svg'} symbol = root.find('ns0:g', svg_ns) # Transform based on scaling and data centre = self.get_page_dimensions()[0]/2 plaster = self.get_plaster_coord() scale = float(self.options['scale']) plot_pos = lambda dim: (float(fixture.data['pos'+dim])*1000) rotation = fixture.generate_rotation() colour = fixture.generate_colour() symbol.set('transform', 'scale( '+str(1/scale)+' ) ' 'translate('+str(centre*scale+plot_pos('X'))+' '+ str(plot_pos('Y')+plaster*scale)+') ' 'rotate('+str(rotation)+')') for path in symbol: if path.get('class') == 'outer': path.set('fill', colour) path.set('stroke-width', str(float(self.options['line-weight-heavy'])*scale)) return symbol def generate_plot(self): if not self.can_fit_page(): print('PlotterError: Plot does not fit page with this scaling') else: self.lighting_plot = self.get_empty_plot() root = self.lighting_plot.getroot() root.append(self.get_page_border()) root.append(self.get_centre_line()) root.append(self.get_plaster_line()) # root.append(self.get_title_block()) for fixture in self.fixtures: root.append(self.get_fixture_icon(fixture)) class PlotOptions(): def __init__(self, config): self.options = config['plotter'] def set(self, option, value): self.options[option] = value def get(self, option): if option in self.options: return self.options[option] else: return None class FixtureSymbol: """Manages the SVG symbols for fixtures.""" def __init__(self, fixture): """Load the fixture symbol file.""" self.fixture = fixture symbol_name = fixture.data['symbol'] tree = ET.parse(get_data('symbol/'+symbol_name+'.svg')) root = tree.getroot() self.ns = {'ns0': 'http://www.w3.org/2000/svg'} self.image_group = root.find('ns0:g', self.ns) def get_fixture_group(self): """Return a transformed symbol g element.""" posX_mm = float(self.fixture.data['posX'])*1000 posY_mm = float(self.fixture.data['posY'])*1000 rotation_deg = self.fixture.data['rotation'] colour = self.fixture.data['colour'] self.image_group.set('transform', 'translate('+ str(posX_mm)+' '+str(posY_mm)+') rotate('+str(rotation_deg)+')') for path in self.image_group: if path.get('class') == 'outer': path.set('fill', colour) return self.image_group def get_fixture_beam(self): """Return a beam path element.""" posX_mm = str(float(self.fixture.data['posX'])*1000) posY_mm = str(float(self.fixture.data['posY'])*1000) focusX_mm = str(float(self.fixture.data['focusX'])*1000) focusY_mm = str(float(self.fixture.data['focusY'])*1000) beam = ET.Element('path') beam.set('d', 'M '+posX_mm+' '+posY_mm+' L '+focusX_mm+' '+focusY_mm) beam.set('stroke', 'black') beam.set('stroke-width', '6') beam.set('stroke-dasharray', '10,10') return beam def get_circuit_icon(self): """Return a circuit and connector g element.""" rotation_deg = self.fixture.data['rotation'] posX_mm = float(self.fixture.data['posX'])*1000 posY_mm = float(self.fixture.data['posY'])*1000 connector_endX = posX_mm-200*math.cos(math.radians(rotation_deg)) connector_endY = posY_mm-200*math.sin(math.radians(rotation_deg)) icon_group = ET.Element('g') connector = ET.SubElement(icon_group, 'path') connector.set('d', 'M '+str(posX_mm)+' '+str(posY_mm)+ ' L '+str(connector_endX)+' '+str(connector_endY)) connector.set('stroke', 'black') connector.set('stroke-width', '3') circle = ET.SubElement(icon_group, 'circle') circle.set('cx', str(connector_endX)) circle.set('cy', str(connector_endY)) circle.set('r', '60') circle.set('stroke', 'black') circle.set('stroke-width', '6') circle.set('fill', 'white') text = ET.SubElement(icon_group, 'text') text.text = self.fixture.data['circuit'] text.set('x', str(connector_endX)) text.set('y', str(connector_endY)) text.set('font-size', '60') return icon_group class PlotterContext(Context): def __init__(self): super().__init__() self.name = 'plotter-dev' self.register(Command('pn', self.plot_new, [], synopsis='Create a new plot.')) self.register(Command('pw', self.plot_write, ['path'], synopsis='Write the plot buffer to a file and ' 'optionally convert to another ' 'format.')) self.register(Command('pd', self.plot_dump, [])) self.register(Command('os', self.option_set, ['name', 'value'], synopsis='Set the value of an option.')) self.register(Command('og', self.option_get, ['name'], synopsis='Print the value of an option.')) self.register(Command('ol', self.option_list, [], synopsis='Print the value of all options.')) self.register(Command('deb', self.debug, [])) def post_init(self): super().post_init() self.options = PlotOptions(self.config) def debug(self, parsed_input): self.plot_new(parsed_input) self.plot_write(['devplot.svg']) def plot_new(self, parsed_input): self.plot = LightingPlot(self.plot_file, self.options.options) self.plot.generate_plot() def plot_write(self, parsed_input): if parsed_input[0].split('.')[-1] == 'svg': self.plot.lighting_plot.write(os.path.expanduser(parsed_input[0])) elif parsed_input[0].split('.')[-1] == 'pdf': plot_bytes = ET.tostring(self.plot.lighting_plot.getroot()) cairosvg.svg2pdf(bytestring=plot_bytes, write_to=parsed_input[0]) else: print('WARNING: File format not supported, writing as SVG') self.plot.lighting_plot.write(os.path.expanduser(parsed_input[0])) def plot_dump(self, parsed_input): ET.dump(self.plot.lighting_plot.getroot()) def option_set(self, parsed_input): self.options.set(parsed_input[0], parsed_input[1]) def option_get(self, parsed_input): print(self.options.get(parsed_input[0])) def option_list(self, parsed_input): for option in self.options.options: print(option+': '+str(self.options.options[option])) def get_context(): return PlotterContext() PKlIHٵJpylux/template/fixturelist.tex\documentclass[12pt]{article} \usepackage[a4paper,landscape,hmargin=0.79in,vmargin=0.79in]{geometry} \usepackage[parfill]{parskip} \usepackage{longtable} \pagestyle{empty} \linespread{1.5} \setlength{\doublerulesep}{\arrayrulewidth} \renewcommand{\tabcolsep}{0.8ex} \setlength\LTleft{0pt} \setlength\LTright{0pt} \begin{document} \begin{longtable}{@{\extracolsep{\fill}\hspace{\tabcolsep}} l l l l } \hline {\bf Fixture \#} & \multicolumn{1}{c}{\bf Type} & \multicolumn{1}{c}{\bf Power} & \multicolumn{1}{c}{\bf Gel} \\* \hline\hline {% for fixture in fixtures %} {{ fixture.data['usitt_key'] }} & {{ fixture.data['type'] }} & {{ fixture.data['power'] }}W & {{ fixture.data['gel'] }} \\ {% endfor %} \end{longtable} \end{document} PKMHKpylux/template/dimmerlist.tex\documentclass[12pt]{article} \usepackage[a4paper,hmargin=0.79in,vmargin=0.79in]{geometry} \usepackage[parfill]{parskip} \usepackage{longtable} \pagestyle{empty} \linespread{1.5} \setlength{\doublerulesep}{\arrayrulewidth} \renewcommand{\tabcolsep}{0.8ex} \setlength\LTleft{0pt} \setlength\LTright{0pt} \begin{document} \begin{longtable}{@{\extracolsep{\fill}\hspace{\tabcolsep}} l l l l} \hline {\bf Type} & {\bf Circuit} & {\bf Dimmer Channel} & {\bf Power} \\* \hline\hline {% for dimmer in dimmers %} \textbf{Dimmer: {{ dimmer.data['name'] }}{{ '}' }} \\ {% for fixture in fixtures if fixture.data['dimmer_uuid'] == dimmer.uuid %} {{ fixture.data['type'] }} & {{ fixture.data['circuit'] }} & {{ fixture.data['dimmer_channel'] }} & {{ fixture.data['power'] }}W \\ {% endfor %} \hline {\bf Dimmer Power} & & & {\bf {{ dimmer.data['power'] }}W} \\*[1.5ex] {% endfor %} \hline\hline\hline {\bf Total Power} & & & {\bf {{ meta['total_power'] }}W} \\ \end{longtable} \end{document} PKAOH*+6pylux/template/cuelist.html {% if options['style'] == 'bootstrap' %} {% else %} {% endif %} {{ meta['production'] }} Cue List

{{ meta['production'] }} Cue List

{% for cue in cues %} {% if cue.data['type'] in options['show'] or options['show'] == 'all' %} {% endfor %}
Cue # Cue Description Notes
{{ cue.data['identifier'] }} {{ cue.data['location'] }} {{ cue.data['description'] }} {{ cue.data['notes'] }}
Generated by Pylux
PKlIHUNPZpylux/template/hanglist.tex\documentclass[12pt]{article} \usepackage[a4paper,landscape,hmargin=0.79in,vmargin=0.79in]{geometry} \usepackage[parfill]{parskip} \usepackage{longtable} \pagestyle{empty} \linespread{1.5} \setlength{\doublerulesep}{\arrayrulewidth} \renewcommand{\tabcolsep}{0.8ex} \setlength\LTleft{0pt} \setlength\LTright{0pt} \begin{document} \begin{longtable}{@{\extracolsep{\fill}\hspace{\tabcolsep}} l l l l l} \hline {\bf Type} & {\bf Position} & {\bf Focus} & {\bf Circuit} & {\bf Gel} \\* \hline\hline {% for fixture in hung %} {{ fixture.data['type'] }} & {{ fixture.data['posX'] }},{{ fixture.data['posY'] }} & {{ fixture.data['focusX'] }},{{ fixture.data['focusY'] }} & {{ fixture.data['circuit'] }} & {{ fixture.data['gel'] }} \\ {% endfor %} \end{longtable} \end{document} PKlIH4'Vpylux/template/cuelist.tex\documentclass[12pt]{article} \usepackage[a4paper,landscape,hmargin=0.79in,vmargin=0.79in]{geometry} \usepackage[parfill]{parskip} \usepackage{longtable} \pagestyle{empty} \linespread{1.5} \setlength{\doublerulesep}{\arrayrulewidth} \renewcommand{\tabcolsep}{0.8ex} \setlength\LTleft{0pt} \setlength\LTright{0pt} \begin{document} \begin{longtable}{@{\extracolsep{\fill}\hspace{\tabcolsep}} l l l l } \hline {\bf Cue \#} & \multicolumn{1}{c}{\bf Cue} & \multicolumn{1}{c}{\bf Description} & \multicolumn{1}{c}{\bf Notes} \\* \hline\hline {% for cue in cues %} {{ cue.data['identifier'] }} & {{ cue.data['location'] }} & {{ cue.data['description'] }} & {{ cue.data['notes'] }} \\ {% endfor %} \end{longtable} \end{document} PKMH>: pylux/template/dimmerlist.html {{ meta['production'] }} Dimmer List

{{ meta['production'] }} Dimmer List

{% for dimmer in dimmers %}

Dimmer: {{ dimmer.data['name'] }}

{% for fixture in fixtures if fixture.data['dimmer_uuid'] == dimmer.uuid %} {% endfor %}
Fixture # Type Circuit Dimmer Channel Power
{{ fixture.data['usitt_key'] }} {{ fixture.data['type'] }} {{ fixture.data['circuit'] }} {{ fixture.data['dimmer_channel'] }} {{ fixture.data['power'] }}W

Dimmer Power: {{ dimmer.data['power'] }}W

{% endfor %}

Total Power: {{ meta['total_power'] }}W

Generated by Pylux
PK&6H)pylux/template/cuelist.jinja\documentclass[12pt]{article} \usepackage[a4paper,landscape,hmargin=0.79in,vmargin=0.79in]{geometry} \usepackage[parfill]{parskip} \usepackage{longtable} \pagestyle{empty} \linespread{1.5} \setlength{\doublerulesep}{\arrayrulewidth} \renewcommand{\tabcolsep}{0.8ex} \setlength\LTleft{0pt} \setlength\LTright{0pt} \begin{document} \newcounter{LX} \newcounter{SX} \newcounter{VX} \setcounter{LX}{1} \setcounter{SX}{1} \setcounter{VX}{1} \begin{longtable}{@{\extracolsep{\fill}\hspace{\tabcolsep}} l l l l } \hline {\bf Cue \#} & \multicolumn{1}{c}{\bf Cue} & \multicolumn{1}{c}{\bf Description} & \multicolumn{1}{c}{\bf Notes} \\* \hline\hline {% for cue in cues %} {{ cue.data['type'] }}\arabic{{ '{' }}{{ cue.data['type'] }}{{ '}' }} & {{ cue.data['location'] }} & {{ cue.data['description'] }} & {{ cue.data['notes'] }} \\ \addtocounter{{ '{' }}{{ cue.data['type'] }}{{ '}' }}{1} {% endfor %} \end{longtable} \end{document} PKlIH$P pylux/template/hanglist.html {{ meta['production'] }} Hanging List

{{ meta['production'] }} Hanging List

{% for fixture in hung %} {% endfor %}
Type Position Focus Circuit Gel
{{ fixture.data['type'] }} {{ fixture.data['posX'] }},{{ fixture.data['posY'] }} {{ fixture.data['focusX'] }},{{ fixture.data['focusY'] }} {{ fixture.data['circuit'] }} {{ fixture.data['gel'] }}
Generated by Pylux
PKlIHcpylux/template/fixturelist.html {{ meta['production'] }} Fixture List

{{ meta['production'] }} Fixture List

{% for fixture in fixtures %} {% endfor %}
Fixture # Type Power Gel
{{ fixture.data['usitt_key'] }} {{ fixture.data['type'] }} {{ fixture.data['power'] }}W {{ fixture.data['gel'] }}
Generated by Pylux
PKxBHEDpylux/symbol/par64mfl.svg PKxBH 1pylux/symbol/p650.svg PK$)H }9pylux/symbol/patt23.svg PK7yBHBpylux/symbol/coda1000.svg PKAOHpylux/symbol/ledpar56.svg PKAOH]rrpylux/symbol/quartetf.svg PKЅ)Ha=pylux/symbol/generic.svg PKvPH ]`ee%pylux-0.2.0.dist-info/DESCRIPTION.rstPylux ===== [![PyPI](https://img.shields.io/pypi/v/pylux.svg)](https://pypi.python.org/pypi/pylux/) [![PyPI](https://img.shields.io/pypi/format/pylux.svg)](https://pypi.python.org/pypi/pylux#license) [![Documentation Status](https://readthedocs.org/projects/pylux/badge/?version=latest)](http://pylux.readthedocs.org/en/latest/?badge=latest) Pylux is a program for the management of lighting documentation written in Python. It uses its XML files called plots to store information about a lighting project. The Pylux program comes with multiple 'contexts'. Each context has a specific command and feature set. Currently included are ``editor`` which allows for the editing of the aforementioned XML plots, ``reporter`` which creates reports from Jinja2 templates and ``plotter`` which creates SVG diagrams of the plot. Installation and Dependencies ----------------------------- Pylux is written in Python 3; you will need the Python 3 interpreter to run it. You can either download a source distribution, then install by running ``` sudo python setup.py install ``` or you can install directly from the PyPI using pip: ``` pip install pylux ``` To install either way you will need Python, setuptools and pip, which are available from http://python.org or you can install them using your package manager: ``` sudo apt install python3 python3-pip python3-setuptools ``` ``` sudo pacman -S python python-pip python-setuptools ``` Contributing ------------ If you are interested in contributing towards this project, there are many ways in which you can help: + [Python] writing code; + [XML] making fixture templates; + [SVG] making fixture symbols; + [Jinja] making ``reporter`` templates; + [English] submitting bug reports and feature requests. License ------- Pylux is licensed under the GNU GPL v3.0. A full copy of the license is available in the file ``COPYING``. PKvPHP//&pylux-0.2.0.dist-info/entry_points.txt[console_scripts] pylux = pylux.__main__:main PKvPHr UU#pylux-0.2.0.dist-info/metadata.json{"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Information Technology", "Topic :: Office/Business", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5"], "extensions": {"python.commands": {"wrap_console": {"pylux": "pylux.__main__:main"}}, "python.details": {"contacts": [{"email": "jdpboc98@gmail.com", "name": "Jack Page", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://os.pwrg.uk/software/pylux"}}, "python.exports": {"console_scripts": {"pylux": "pylux.__main__:main"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "keywords": ["lighting", "theatre", "stage", "tech"], "license": "GPLv3+", "metadata_version": "2.0", "name": "pylux", "run_requires": [{"requires": ["Jinja2", "cairosvg", "tabulate"]}], "summary": "A program for managing lighting documentation.", "version": "0.2.0"}PKvPHHTg~pylux/clihelper.pyPKT7H_eb|||(pylux/context.pyPKAOH>|>|&-pylux/reference.pyPKuPHE}@.ifif pylux/plot.pyPKEO7HO..(pylux/editor.pyPKG; ; ?pylux/gplotter.pyPKqjPH1UPg g xJpylux/__main__.pyPKyLH] ]jjVpylux/__init__.pyPKT7Hi##Xpylux/geditor.pyPKlIHi88spylux/exception.pyPKUW)H[c#c#`wpylux/plotter.pyPKJBHjn>pylux/context/editor.pyPKEO7H`,,@pylux/context/__init__.pyPKqPH`Yc4N4N$Apylux/context/plotter.pyPKuMHMGGpylux/context/plotterNEW.pyPKlIHٵJmpylux/template/fixturelist.texPKMHKpylux/template/dimmerlist.texPKAOH*+6pylux/template/cuelist.htmlPKlIHUNPZpylux/template/hanglist.texPKlIH4'V pylux/template/cuelist.texPKMH>: ,pylux/template/dimmerlist.htmlPK&6H)pylux/template/cuelist.jinjaPKlIH$P _pylux/template/hanglist.htmlPKlIHc%pylux/template/fixturelist.htmlPKxBHEDppylux/symbol/par64mfl.svgPKxBH 1pylux/symbol/p650.svgPK$)H }9pylux/symbol/patt23.svgPK7yBHBbpylux/symbol/coda1000.svgPKAOHpylux/symbol/ledpar56.svgPKAOH]rrpylux/symbol/quartetf.svgPKЅ)Ha=.pylux/symbol/generic.svgPKvPH ]`ee%xpylux-0.2.0.dist-info/DESCRIPTION.rstPKvPHP//& pylux-0.2.0.dist-info/entry_points.txtPKvPHr UU# pylux-0.2.0.dist-info/metadata.jsonPKvPH