PK.UHԝ* 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 . from importlib import import_module def get_context(context_name): module_name = 'pylux.context.'+context_name context_module = import_module(module_name) 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) 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() try: context = get_context(inputs[0].split(':')[1]) except ImportError or AttributeError: context.log(30, 'Context does not exist') else: context.set_globals(globals_dict) elif inputs[0] in context.commands: context.process(inputs) else: context.log(30, 'Command does not exist') if __name__ == 'pylux_root': main() PKюZH5% 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 # Whether to show focus lines for fixtures (not USITT standard) show-beams = False # SVG dasharray to use for the beams beam-dasharray = 1, 1 [advanced] # The names to use for different logging levels log-50 = CRITICAL log-40 = ERROR log-30 = WARNING log-20 = INFO log-10 = DEBUG log-1 = TRACE 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' } PKQTHefef 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() PK`SHE~K K 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: 30, 1: 20, 2: 10, 3: 1} print('Logging level is '+config['advanced']['log-'+str(verbosity_dict[launch_args.verbose])]) # 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]} 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() PKZHO]]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 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, [])) self.register(Command('h', self.utility_help, [ ('command', False, 'The command to access information about')])) self.register(Command('q', self.utility_exit, [])) self.register(Command('Q', self.utility_kill, [])) 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]] if len(inputs) < command.nargs: self.log(30, 'Not enough arguments, type \'h '+ command.mnemoic+'\' for usage.') elif len(inputs) >= command.maxargs: parsed_input = resolve_input(inputs, command.maxargs) command.function(parsed_input) else: 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() 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 log(self, level, message): level_name = self.config['advanced']['log-'+str(level)] if level >= self.log_level: print(''.join([level_name,':',self.name,':',message])) def utility_clear(self, parsed_input): '''Clear the screen.''' os.system('cls' if os.name == 'nt' else 'clear') def utility_exit(self, parsed_input): '''Quit the program and save the plot file to disk.''' try: self.plot_file.write() except AttributeError: self.log(30, 'No plot file was loaded, nothing to save') self.utility_kill(parsed_input) def utility_kill(self, parsed_input): '''Quit the program without saving any changes.''' sys.exit() def utility_help(self, parsed_input): '''Access information about a command or list all commands.''' if len(parsed_input) > 0: if parsed_input[0] not in self.commands: self.log(30, 'Command does not exist') else: command = self.commands[parsed_input[0]] print('Usage:') usage = ' '+command.mnemonic for arg in command.arguments: usage = usage+' '+arg[0] print(usage) print('Description:') print(' '+str(command.synopsis)) print('Arguments:') for arg in command.arguments: if arg[1]: req = '(Required)' else: req = '(Optional)' print(''.join([' ', arg[0], ' ', req, ': ', arg[2]])) else: command_table = [] for mnemonic in self.commands: table_row = [] command = self.commands[mnemonic] usage = mnemonic for arg in command.arguments: usage = usage+' '+arg[0] 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): self.mnemonic = mnemonic self.function = function self.synopsis = self.function.__doc__ self.arguments = arguments self.maxargs = len(self.arguments) self.nargs = 0 for arg in self.arguments: if arg[1]: self.nargs += 1 PKZH4p݆66pylux/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 from jinja2.exceptions import TemplateSyntaxError 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() 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', True, 'The Jinja template to create a report from.'), ('options', False, 'Optional arguments the template offers.')])) self.register(Command('rg', self.report_get, [])) self.register(Command('rw', self.report_write, [ ('path', True, 'The path to write the file to.')])) def report_new(self, parsed_input): '''Create a new report from a template in a temporary buffer.''' 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: self.log(10, 'Did not find any matching templates.') return None elif len(possible_templates) == 1: self.log(10, 'Found one matching template.') 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: try: self.report.generate(template, options) except TemplateSyntaxError: self.log(30, 'Template not configured properly.') def report_get(self, parsed_input): '''Print the contents of the report buffer.''' print(self.report.content) def report_write(self, parsed_input): '''Save the report buffer to a file.''' with open(os.path.expanduser(parsed_input[0]), 'w') as outfile: outfile.write(self.report.content) def get_context(): return ReporterContext() PKZH@@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' self.register(Command('fo', self.file_open, [ ('path', True, 'Path of the file to load.')])) self.register(Command('fw', self.file_write, [])) self.register(Command('fW', self.file_writeas, [ ('path', True, 'Path to save the buffer to.')])) self.register(Command('fg', self.file_get, [])) self.register(Command('fn', self.file_new, [])) self.register(Command('ml', self.metadata_list, [])) self.register(Command('ms', self.metadata_set, [ ('name', True, 'Name of the metadata to set.'), ('value', True, 'Value for the metadata to take.')])) self.register(Command('mr', self.metadata_remove, [ ('name', True, 'Name of the metadata to remove.')])) self.register(Command('mg', self.metadata_get, [ ('name', True, 'Name of the metadata to print the value of.')])) self.register(Command('xn', self.fixture_new, [ ('template', True, 'Name of the fixture file to load data from.')])) self.register(Command('xc', self.fixture_clone, [ ('fixture', True, 'The fixture to make a copy of.')])) self.register(Command('xl', self.fixture_list, [])) self.register(Command('xf', self.fixture_filter, [ ('tag', True, 'The tag to filter by.'), ('value', True, 'The value the tag must be to be displayed.')])) self.register(Command('xr', self.fixture_remove, [ ('fixture', True, 'The fixture to remove.')])) self.register(Command('xg', self.fixture_get, [ ('fixture', True, 'The fixture to get a tag from.'), ('tag', True, 'The name of the tag to print the value of.')])) self.register(Command('xG', self.fixture_getall, [ ('fixture', True, 'The fixture to print the tags of.')])) self.register(Command('xs', self.fixture_set, [ ('fixture', True, 'The fixture to set a tag of.'), ('tag', True, 'The name of the tag to set.'), ('value', True, 'The value to set the tag to.')])) self.register(Command('xa', self.fixture_address, [ ('fixture', True, 'The fixture to assign addresses to.'), ('universe', True, 'The universe to assign addresses in.'), ('address', True, 'The addresses to begin addressing at.')])) self.register(Command('xA', self.fixture_unaddress, [ ('fixture', True, 'The fixture to unassign addresses for.')])) self.register(Command('rl', self.registry_list, [ ('universe', True, 'The universe to list the used addresses of.')])) self.register(Command('rL', self.registry_probe, [ ('universe', True, 'The universe to list the used addresses of.')])) self.register(Command('ql', self.cue_list, [])) self.register(Command('qn', self.cue_new, [ ('type', True, 'The type of the cue to add.'), ('location', True, 'The cue line or visual for this cue.')])) self.register(Command('qr', self.cue_remove, [ ('cue', True, 'The cue to remove.')])) self.register(Command('qs', self.cue_set, [ ('cue', True, 'The cue to set a tag of.'), ('tag', True, 'The name of the tag to set.'), ('value', True, 'The value to set the tag to.')])) self.register(Command('qg', self.cue_get, [ ('cue', True, 'The cue to get a tag from.'), ('tag', True, 'The name of the tag to print the value of.')])) self.register(Command('qG', self.cue_getall, [ ('cue', True, 'The cue to print the tags of.')])) self.register(Command('qm', self.cue_moveafter, [ ('cue', True, 'The cue to move.'), ('dest_cue', True, 'The cue after which the cue should come.')])) self.register(Command('qM', self.cue_movebefore, [ ('cue', True, 'The cue to move.'), ('dest_cue', True, 'The cue before which the cue should come.')])) def file_open(self, parsed_input): '''Open a new plot file, discarding any present buffer.''' 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): '''Write the contents of the file buffer to its original path.''' try: self.plot_file.write() except AttributeError: print('Error: No file is loaded') def file_writeas(self, parsed_input): '''Write the contents of the file buffer to an alternative location.''' self.plot_file.write_to(parsed_input[0]) def file_get(self, parsed_input): '''Print the path from which the current file was loaded.''' 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): '''Create a new file in the buffer.''' self.plot_file.new() def metadata_list(self, parsed_input): '''List the values of all metadata in the plot file.''' metadata = plot.Metadata(self.plot_file) for meta_item in sorted(metadata.meta): print(meta_item+': '+metadata.meta[meta_item]) def metadata_set(self, parsed_input): '''Set the value of a piece of metadata.''' metadata = plot.Metadata(self.plot_file) metadata.set_data(parsed_input[0], parsed_input[1]) def metadata_remove(self, parsed_input): '''Remove a piece of metadata from the file.''' metadata = plot.Metadata(self.plot_file) metadata.set_data(parsed_input[0], None) def metadata_get(self, parsed_input): '''Print the value of a piece of metadata.''' metadata = plot.Metadata(self.plot_file) print(parsed_input[0]+': '+metadata.get_data(parsed_input[0])) def fixture_new(self, parsed_input): '''Create a new fixture from a template file.''' 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): '''Create a new fixture by copying an existing fixture.''' 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): '''List all fixtures in the plot file.''' 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): '''List all fixtures that meet a certain criterion.''' 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): '''Remove a fixture from the plot file.''' 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): '''Print the value of a fixture's tag.''' 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): '''Print the value of every tag associated with a fixture.''' 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): '''Set the value of one of a fixture's tags.''' 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): '''Assign DMX addresses to a fixture.''' 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): '''Unassign addresses in all universes for this fixture.''' fixtures = self.interface.get(parsed_input[0]) for fixture in fixtures: fixture.unaddress(plot.RegistryList(self.plot_file)) def registry_list(self, parsed_input): '''List the functions of all used channels in a registry.''' 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): '''List the functions of all used channels in a registry and also \n list any fixtures which are controlled by dimmers.''' 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): '''List all cues in the plot file.''' 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(''.join(['\033[4m',str(cue.key),'\033[0m (',cue_type, ') at \'',cue_location,'\''])) self.interface.append(cue.key, cue) def cue_new(self, parsed_input): '''Create a new cue.''' cue = plot.Cue(self.plot_file) cue.set_data('type', parsed_input[0]) cue.set_data('location', parsed_input[1]) def cue_remove(self, parsed_input): '''Remove a cue from the plot.''' cues = plot.CueList(self.plot_file) removal_candidates = self.interface.get(parsed_input[0]) for rc in removal_candidates: cues.remove(rc) def cue_set(self, parsed_input): '''Set the value of a cue's tag.''' cues_to_change = self.interface.get(parsed_input[0]) for cue in cues_to_change: cue.set_data(parsed_input[1], parsed_input[2]) def cue_get(self, parsed_input): '''Print the value of a cue's tag.''' 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): '''Print the values of all of a cue's tags.''' 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): '''Move a cue directly after another in the list.''' cues = plot.CueList(self.plot_file) cues.move_after(int(parsed_input[0]), int(parsed_input[1])) def cue_movebefore(self, parsed_input): '''Move a cue directly before another in the list.''' cues = plot.CueList(self.plot_file) cues.move_before(int(parsed_input[0]), int(parsed_input[1])) def get_context(): return EditorContext() PK@ZHn n pylux/context/runsx.py# runsx.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 . """Run the sound cues from the plot file. runsx uses ffplay to run cues with the 'SX' type in the plot file. """ import os import subprocess 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 RunSxContext(Context): def __init__(self): super().__init__() self.name = 'runsx' # Register commands self.register(Command('ql', self.cue_list, [])) self.register(Command('qp', self.cue_play, [ ('cue', True, 'The cue to play the sound file of.')])) self.register(Command('ss', self.stack_start, [ ('start', False, 'The cue from which to begin the stack.')])) self.register(Command('sa', self.stack_advance, [])) self.register(Command('sx', self.stack_exit, [])) def post_init(self): super().post_init() self.cues = plot.CueList(self.plot_file) self.cues.assign_identifiers() def cue_list(self, parsed_input): '''List all cues which are valid sound cues with files.''' for cue in sorted(self.cues.cues, key=lambda cue: cue.key): if cue.data['type'].upper() == 'SX' and 'file' in cue.data: print(''.join(['\033[4m',str(cue.key), '\033[0m at \'',cue.data['location'], '\': ',cue.data['file']])) self.interface.append(cue.key, cue) def cue_play(self, parsed_input): '''Play the file for a cue.''' to_play = self.interface.get(parsed_input[0]) files = [] for cue in to_play: files.append(cue.data['file']) for effect in files: subprocess.run(['mplayer', effect]) def stack_start(self, parsed_input): '''Begin the stack interface.''' return None def stack_advance(self, parsed_input): '''Advance the stack on to the next cue.''' return None def stack_exit(self, parsed_input): '''Leave the stack interface.''' return None def get_context(): return RunSxContext() PKEO7H`,,pylux/context/__init__.py"""Additional contexts for Pylux editor.""" PKZHPL3Q3Qpylux/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 get_fixture_beam(self, fixture): beam = ET.Element('path') scale = float(self.options['scale']) centre = self.get_page_dimensions()[0]/2 plaster = self.get_plaster_coord() startx = (float(fixture.data['posX'])*1000)*(1/scale)+centre starty = (float(fixture.data['posY'])*1000)*(1/scale)+plaster endx = (float(fixture.data['focusX'])*1000)*(1/scale)+centre endy = (float(fixture.data['focusY'])*1000)*(1/scale)+plaster beam.set('d', 'M '+str(startx)+' '+str(starty)+ ' L '+str(endx)+' '+str(endy)) beam.set('stroke', 'black') beam.set('stroke-width', self.options['line-weight-light']) beam.set('stroke-dasharray', self.options['beam-dasharray']) return beam 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()) for fixture in self.fixtures: if self.options['show-beams'] == 'True': root.append(self.get_fixture_beam(fixture)) 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' self.register(Command('pn', self.plot_new, [])) self.register(Command('pw', self.plot_write, [ ('path', True, 'The location to save the plot to.')])) self.register(Command('pd', self.plot_dump, [])) self.register(Command('os', self.option_set, [ ('name', True, 'The name of the option to set the value of.'), ('value', True, 'The new value of the option.')])) self.register(Command('og', self.option_get, [ ('name', True, 'The name of the option to print the value of.')])) self.register(Command('ol', self.option_list, [])) def post_init(self): super().post_init() self.options = PlotOptions(self.config) def plot_new(self, parsed_input): '''Create a new SVG plot in a temporary buffer.''' self.plot = LightingPlot(self.plot_file, self.options.options) self.plot.generate_plot() def plot_write(self, parsed_input): '''Write the plot to a file either as SVG or PDF.''' 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): '''Dump the plot (for debugging purposes.''' ET.dump(self.plot.lighting_plot.getroot()) def option_set(self, parsed_input): '''Set the value of an option.''' self.options.set(parsed_input[0], parsed_input[1]) def option_get(self, parsed_input): '''Print the value of an option.''' print(self.options.get(parsed_input[0])) def option_list(self, parsed_input): '''List the values of all options.''' 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() PKUHpylux/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} \hfill{\Huge\bf {{ meta['production'] }} Fixture List}\hfill \bigskip\break \hrule {\bf Production: } {{ meta['production'] }} \hfill {\bf Designer: } {{ meta['light_designer'] }} \\ {\bf Venue: } {{ meta['venue'] }} \hfill {\bf Director: } {{ meta['director'] }} \\ \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} PKZHpylux/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' %} {% endif %} {% 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} PKZH{00pylux/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 %} {% if cue.data['type'] in options['show'] or options['show'] == 'all' %} {{ cue.data['identifier'] }} & {{ cue.data['location'] }} & {{ cue.data['description'] }} & {{ cue.data['notes'] }} \\ {% endif %} {% 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 PKZHTi]6%pylux-0.2.2.dist-info/DESCRIPTION.rstPylux ===== .. image:: https://img.shields.io/pypi/v/pylux.svg .. image:: https://img.shields.io/pypi/format/pylux.svg .. image:: https://readthedocs.org/projects/pylux/badge/?version=latest Pylux is a program for creating and managing documentation for stage lighting. The program uses an XML 'plot' file to store information about a lighting project. Pylux currently has the capability to, using the aforementioned plot files, generate plaintext documentation using Jinja2 (e.g. LaTeX or HTML documents), generate scale plan views of the lighting arrangement and, by calling mplayer, play sound cues. Installation and Dependencies ----------------------------- Pylux is in the early stages of development. It is not stable enough for general use. Regular users should install Pylux from the PyPI using pip:: sudo pip3 install pylux In order to do this you will need the Python 3.5 interpreter:: sudo apt install python3.5 sudo pacman -S python If you would rather use the most recent code, you can install from the Git repository:: git clone https://github.com/jackdpage/pylux.git cd pylux sudo python3 setup.py install Dependencies will be downloaded from the PyPI on installation. You will also need to manually install mplayer to play sound cues:: sudo apt install mplayer sudo pacman -S mplayer Documentation ------------- The documentation for both users and contributers is available on `Read the Docs`_. .. _`Read the Docs`: http://pylux.readthedocs.org/ Contributing ------------ Before making a contribution, please refer to the guidelines in ``CONTRIBUTING.md``. If you are interested in contributing, 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``. PKZHP//&pylux-0.2.2.dist-info/entry_points.txt[console_scripts] pylux = pylux.__main__:main PKZH̯=(#pylux-0.2.2.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.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.2"}PKZHHTg~pylux/clihelper.pyPKT7H_eb||)pylux/context.pyPKAOH>|>|?.pylux/reference.pyPKQTHefef pylux/plot.pyPKEO7HO..=pylux/editor.pyPKG; ; #@pylux/gplotter.pyPK`SHE~K K Kpylux/__main__.pyPKyLH] ]jjWpylux/__init__.pyPKT7Hi##Ypylux/geditor.pyPKlIHi88tpylux/exception.pyPKUW)H[c#c#Yxpylux/plotter.pyPKJBHj: Ypylux/template/dimmerlist.htmlPK&6H)pylux/template/cuelist.jinjaPKlIH$P  pylux/template/hanglist.htmlPKlIHcRpylux/template/fixturelist.htmlPKxBHEDpylux/symbol/par64mfl.svgPKxBH 1pylux/symbol/p650.svgPK$)H }9>pylux/symbol/patt23.svgPK7yBHBpylux/symbol/coda1000.svgPKAOHpylux/symbol/ledpar56.svgPKAOH]rrpylux/symbol/quartetf.svgPKЅ)Ha=[pylux/symbol/generic.svgPKZHTi]6%pylux-0.2.2.dist-info/DESCRIPTION.rstPKZHP//&$pylux-0.2.2.dist-info/entry_points.txtPKZH̯=(#Q%pylux-0.2.2.dist-info/metadata.jsonPKZH