| @@ -0,0 +1,497 @@ | |||
| #!/usr/bin/env python3 | |||
| import sys | |||
| import os | |||
| import re | |||
| import json | |||
| import xml.etree.ElementTree | |||
| if sys.version_info < (3, 6): | |||
| print("Python 3.6 or higher required") | |||
| exit(1) | |||
| class UserException(Exception): | |||
| pass | |||
| def input_default(prompt, default=""): | |||
| str = input(f"{prompt} [{default}]: ") | |||
| if str == "": | |||
| return default | |||
| return str | |||
| def is_valid_slug(slug): | |||
| return re.match(r'^[a-zA-Z0-9_\-]+$', slug) != None | |||
| def slug_to_identifier(slug): | |||
| if len(slug) == 0 or slug[0].isdigit(): | |||
| slug = "_" + slug | |||
| slug = slug[0].upper() + slug[1:] | |||
| slug = slug.replace('-', '_') | |||
| return slug | |||
| def usage(script): | |||
| text = f"""Usage: {script} <command> ... | |||
| Run commands without arguments for command help. | |||
| Commands: | |||
| createplugin <slug> | |||
| createmodule <module slug> | |||
| createmanifest | |||
| """ | |||
| print(text) | |||
| def usage_create_plugin(script): | |||
| text = f"""Usage: {script} createplugin <slug> | |||
| A directory <slug> will be created in the current working directory and seeded with initial files. | |||
| """ | |||
| print(text) | |||
| def create_plugin(slug): | |||
| # Check slug | |||
| if not is_valid_slug(slug): | |||
| raise UserException("Slug must only contain ASCII letters, numbers, '-', and '_'.") | |||
| # Check if plugin directory exists | |||
| plugin_dir = os.path.join(slug, '') | |||
| if os.path.exists(plugin_dir): | |||
| raise UserException(f"Directory {plugin_dir} already exists") | |||
| # Query manifest information | |||
| manifest = {} | |||
| manifest['slug'] = slug | |||
| manifest['name'] = input_default("Plugin name", slug) | |||
| manifest['version'] = input_default("Version", "1.0.0") | |||
| manifest['license'] = input_default("License (if open-source, use license identifier from https://spdx.org/licenses/)", "proprietary") | |||
| manifest['author'] = input_default("Author") | |||
| manifest['authorEmail'] = input_default("Author email (optional)") | |||
| manifest['authorUrl'] = input_default("Author website URL (optional)") | |||
| manifest['pluginUrl'] = input_default("Plugin website URL (optional)") | |||
| manifest['manualUrl'] = input_default("Manual website URL (optional)") | |||
| manifest['sourceUrl'] = input_default("Source code URL (optional)") | |||
| manifest['donateUrl'] = input_default("Donate URL (optional)") | |||
| manifest['modules'] = [] | |||
| # Create plugin directory | |||
| os.mkdir(plugin_dir) | |||
| # Dump JSON | |||
| manifest_filename = os.path.join(plugin_dir, 'plugin.json') | |||
| with open(manifest_filename, "w") as f: | |||
| json.dump(manifest, f, indent="\t") | |||
| print(f"Manifest created at {manifest_filename}") | |||
| # Create subdirectories | |||
| os.mkdir(os.path.join(plugin_dir, "src")) | |||
| os.mkdir(os.path.join(plugin_dir, "res")) | |||
| # Create Makefile | |||
| makefile = """# If RACK_DIR is not defined when calling the Makefile, default to two directories above | |||
| RACK_DIR ?= ../.. | |||
| # FLAGS will be passed to both the C and C++ compiler | |||
| FLAGS += | |||
| CFLAGS += | |||
| CXXFLAGS += | |||
| # Careful about linking to shared libraries, since you can't assume much about the user's environment and library search path. | |||
| # Static libraries are fine, but they should be added to this plugin's build system. | |||
| LDFLAGS += | |||
| # Add .cpp files to the build | |||
| SOURCES += $(wildcard src/*.cpp) | |||
| # Add files to the ZIP package when running `make dist` | |||
| # The compiled plugin and "plugin.json" are automatically added. | |||
| DISTRIBUTABLES += res | |||
| DISTRIBUTABLES += $(wildcard LICENSE*) | |||
| # Include the Rack plugin Makefile framework | |||
| include $(RACK_DIR)/plugin.mk | |||
| """ | |||
| with open(os.path.join(plugin_dir, "Makefile"), "w") as f: | |||
| f.write(makefile) | |||
| # Create plugin.hpp | |||
| plugin_hpp = """#include "rack.hpp" | |||
| using namespace rack; | |||
| // Declare the Plugin, defined in plugin.cpp | |||
| extern Plugin *pluginInstance; | |||
| // Declare each Model, defined in each module source file | |||
| """ | |||
| with open(os.path.join(plugin_dir, "src/plugin.hpp"), "w") as f: | |||
| f.write(plugin_hpp) | |||
| # Create plugin.cpp | |||
| plugin_cpp = """#include "plugin.hpp" | |||
| Plugin *pluginInstance; | |||
| void init(Plugin *p) { | |||
| pluginInstance = p; | |||
| // Any other plugin initialization may go here. | |||
| // As an alternative, consider lazy-loading assets and lookup tables when your module is created to reduce startup times of Rack. | |||
| } | |||
| """ | |||
| with open(os.path.join(plugin_dir, "src/plugin.cpp"), "w") as f: | |||
| f.write(plugin_cpp) | |||
| git_ignore = """/build | |||
| /dist | |||
| /plugin.so | |||
| /plugin.dylib | |||
| /plugin.dll | |||
| .DS_Store | |||
| """ | |||
| with open(os.path.join(plugin_dir, ".gitignore"), "w") as f: | |||
| f.write(git_ignore) | |||
| print(f"Created template plugin in {plugin_dir}") | |||
| os.system(f"cd {plugin_dir} && git init") | |||
| print(f"You may use `make`, `make clean`, `make dist`, `make install`, etc in the {plugin_dir} directory.") | |||
| def usage_create_module(script): | |||
| text = f"""Usage: {script} createmodule <module slug> | |||
| Must be called in a plugin directory. | |||
| A panel file must exist in res/<module slug>.svg. | |||
| A source file will be created at src/<module slug>.cpp. | |||
| Instructions for creating a panel: | |||
| - Only Inkscape is supported by this script and Rack's SVG renderer. | |||
| - Create a document with units in "mm", height of 128.5 mm, and width of a multiple of 5.08 mm (1 HP in Eurorack). | |||
| - Design the panel. | |||
| - Create a layer named "widgets". | |||
| - For each component, create a shape on the widgets layer. | |||
| - Use a circle to place a component by its center. | |||
| The size of the circle does not matter, only the center point. | |||
| A `create*Centered()` call is generated in C++. | |||
| - Use a rectangle to to place a component by its top-left point. | |||
| This should only be used when the shape's size is equal to the component's size in C++. | |||
| A `create*()` call is generated in C++. | |||
| - Set the color of each shape depending on the component's type. | |||
| - Param: red #ff0000 | |||
| - Input: green #00ff00 | |||
| - Output: blue #0000ff | |||
| - Light: magenta #ff00ff | |||
| - Custom widgets: yellow #ffff00 | |||
| - Hide the widgets layer and save to res/<module slug>.svg. | |||
| """ | |||
| print(text) | |||
| def create_module(slug): | |||
| # Check slug | |||
| if not is_valid_slug(slug): | |||
| raise UserException("Slug must only contain ASCII letters, numbers, '-', and '_'.") | |||
| # Read manifest | |||
| manifest_filename = 'plugin.json' | |||
| manifest = None | |||
| with open(manifest_filename, "r") as f: | |||
| manifest = json.load(f) | |||
| # Check if module manifest exists | |||
| module_manifests = filter(lambda m: m['slug'] == slug, manifest['modules']) | |||
| if module_manifests: | |||
| # Add module to manifest | |||
| module_manifest = {} | |||
| module_manifest['slug'] = slug | |||
| module_manifest['name'] = input_default("Module name", slug) | |||
| module_manifest['description'] = input_default("One-line description (optional)") | |||
| tags = input_default("Tags (comma-separated, see https://github.com/VCVRack/Rack/blob/v1/src/plugin.cpp#L543 for list)") | |||
| tags = tags.split(",") | |||
| tags = [tag.strip() for tag in tags] | |||
| if len(tags) == 1 and tags[0] == "": | |||
| tags = [] | |||
| module_manifest['tags'] = tags | |||
| manifest['modules'].append(module_manifest) | |||
| # Write manifest | |||
| with open(manifest_filename, "w") as f: | |||
| json.dump(manifest, f, indent="\t") | |||
| print(f"Added {slug} to plugin.json") | |||
| # Check filenames | |||
| panel_filename = f"res/{slug}.svg" | |||
| source_filename = f"src/{slug}.cpp" | |||
| if os.path.exists(source_filename): | |||
| if input_default(f"{source_filename} already exists. Overwrite?", "n").lower() != "y": | |||
| return | |||
| # Read SVG XML | |||
| tree = None | |||
| try: | |||
| tree = xml.etree.ElementTree.parse(panel_filename) | |||
| except FileNotFoundError: | |||
| raise UserException(f"Panel not found at {panel_filename}") | |||
| components = panel_to_components(tree) | |||
| print(f"Components extracted from {panel_filename}") | |||
| # Write source | |||
| source = components_to_source(components, slug) | |||
| with open(source_filename, "w") as f: | |||
| f.write(source) | |||
| print(f"Source file generated at {source_filename}") | |||
| def panel_to_components(tree): | |||
| ns = { | |||
| "svg": "http://www.w3.org/2000/svg", | |||
| "inkscape": "http://www.inkscape.org/namespaces/inkscape", | |||
| } | |||
| # Get widgets layer | |||
| root = tree.getroot() | |||
| groups = root.findall(".//svg:g[@inkscape:label='widgets']", ns) | |||
| if len(groups) < 1: | |||
| raise UserException("Could not find \"widgets\" layer on panel") | |||
| # Get circles and rects | |||
| widgets_group = groups[0] | |||
| circles = widgets_group.findall(".//svg:circle", ns) | |||
| rects = widgets_group.findall(".//svg:rect", ns) | |||
| components = {} | |||
| components['params'] = [] | |||
| components['inputs'] = [] | |||
| components['outputs'] = [] | |||
| components['lights'] = [] | |||
| components['widgets'] = [] | |||
| for el in circles + rects: | |||
| c = {} | |||
| # Get name | |||
| name = el.get('inkscape:label') | |||
| if name is None: | |||
| name = el.get('id') | |||
| name = slug_to_identifier(name).upper() | |||
| c['name'] = name | |||
| # Get color | |||
| style = el.get('style') | |||
| color_match = re.search(r'fill:\S*#(.{6});', style) | |||
| color = color_match.group(1) | |||
| c['color'] = color | |||
| # Get position | |||
| if el.tag == "{http://www.w3.org/2000/svg}rect": | |||
| x = float(el.get('x')) | |||
| y = float(el.get('y')) | |||
| width = float(el.get('width')) | |||
| height = float(el.get('height')) | |||
| c['x'] = round(x, 3) | |||
| c['y'] = round(y, 3) | |||
| c['width'] = round(width, 3) | |||
| c['height'] = round(height, 3) | |||
| elif el.tag == "{http://www.w3.org/2000/svg}circle": | |||
| cx = float(el.get('cx')) | |||
| cy = float(el.get('cy')) | |||
| c['cx'] = round(cx, 3) | |||
| c['cy'] = round(cy, 3) | |||
| if color == 'ff0000': | |||
| components['params'].append(c) | |||
| if color == '00ff00': | |||
| components['inputs'].append(c) | |||
| if color == '0000ff': | |||
| components['outputs'].append(c) | |||
| if color == 'ff00ff': | |||
| components['lights'].append(c) | |||
| if color == 'ffff00': | |||
| components['widgets'].append(c) | |||
| return components | |||
| def components_to_source(components, slug): | |||
| identifier = slug_to_identifier(slug) | |||
| source = "" | |||
| source += f"""#include "plugin.hpp" | |||
| struct {identifier} : Module {{""" | |||
| # Params | |||
| source += """ | |||
| enum ParamIds {""" | |||
| for c in components['params']: | |||
| source += f""" | |||
| {c['name']}_PARAM,""" | |||
| source += """ | |||
| NUM_PARAMS | |||
| };""" | |||
| # Inputs | |||
| source += """ | |||
| enum InputIds {""" | |||
| for c in components['inputs']: | |||
| source += f""" | |||
| {c['name']}_INPUT,""" | |||
| source += """ | |||
| NUM_INPUTS | |||
| };""" | |||
| # Outputs | |||
| source += """ | |||
| enum OutputIds {""" | |||
| for c in components['outputs']: | |||
| source += f""" | |||
| {c['name']}_OUTPUT,""" | |||
| source += """ | |||
| NUM_OUTPUTS | |||
| };""" | |||
| # Lights | |||
| source += """ | |||
| enum LightIds {""" | |||
| for c in components['lights']: | |||
| source += f""" | |||
| {c['name']}_LIGHT,""" | |||
| source += """ | |||
| NUM_LIGHTS | |||
| };""" | |||
| source += f""" | |||
| {identifier}() {{ | |||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);""" | |||
| for c in components['params']: | |||
| source += f""" | |||
| params[{c['name']}_PARAM].config(0.f, 1.f, 0.f, "");""" | |||
| source += """ | |||
| } | |||
| void process(const ProcessArgs &args) override { | |||
| } | |||
| };""" | |||
| source += f""" | |||
| struct {identifier}Widget : ModuleWidget {{ | |||
| {identifier}Widget({identifier} *module) {{ | |||
| setModule(module); | |||
| setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/{slug}.svg"))); | |||
| addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0))); | |||
| addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); | |||
| addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||
| addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));""" | |||
| # Params | |||
| if len(components['params']) > 0: | |||
| source += "\n" | |||
| for c in components['params']: | |||
| if 'cx' in c: | |||
| source += f""" | |||
| addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec({c['cx']}, {c['cy']})), module, {identifier}::{c['name']}_PARAM));""" | |||
| else: | |||
| source += f""" | |||
| addParam(createParam<RoundBlackKnob>(mm2px(Vec({c['x']}, {c['y']})), module, {identifier}::{c['name']}_PARAM));""" | |||
| # Inputs | |||
| if len(components['inputs']) > 0: | |||
| source += "\n" | |||
| for c in components['inputs']: | |||
| if 'cx' in c: | |||
| source += f""" | |||
| addInput(createInputCentered<PJ301MPort>(mm2px(Vec({c['cx']}, {c['cy']})), module, {identifier}::{c['name']}_INPUT));""" | |||
| else: | |||
| source += f""" | |||
| addInput(createInput<PJ301MPort>(mm2px(Vec({c['x']}, {c['y']})), module, {identifier}::{c['name']}_INPUT));""" | |||
| # Outputs | |||
| if len(components['outputs']) > 0: | |||
| source += "\n" | |||
| for c in components['outputs']: | |||
| if 'cx' in c: | |||
| source += f""" | |||
| addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec({c['cx']}, {c['cy']})), module, {identifier}::{c['name']}_OUTPUT));""" | |||
| else: | |||
| source += f""" | |||
| addOutput(createOutput<PJ301MPort>(mm2px(Vec({c['x']}, {c['y']})), module, {identifier}::{c['name']}_OUTPUT));""" | |||
| # Lights | |||
| if len(components['lights']) > 0: | |||
| source += "\n" | |||
| for c in components['lights']: | |||
| if 'cx' in c: | |||
| source += f""" | |||
| addChild(createLightCentered<MediumLight<RedLight>>(mm2px(Vec({c['cx']}, {c['cy']})), module, {identifier}::{c['name']}_LIGHT));""" | |||
| else: | |||
| source += f""" | |||
| addChild(createLight<MediumLight<RedLight>>(mm2px(Vec({c['x']}, {c['y']})), module, {identifier}::{c['name']}_LIGHT));""" | |||
| # Widgets | |||
| if len(components['widgets']) > 0: | |||
| source += "\n" | |||
| for c in components['widgets']: | |||
| if 'cx' in c: | |||
| source += f""" | |||
| addChild(createWidgetCentered<Widget>(mm2px(Vec({c['cx']}, {c['cy']}))));""" | |||
| else: | |||
| source += f""" | |||
| // mm2px(Vec({c['width']}, {c['height']})) | |||
| addChild(createWidget<Widget>(mm2px(Vec({c['x']}, {c['y']}))));""" | |||
| source += f""" | |||
| }} | |||
| }}; | |||
| Model *model{identifier} = createModel<{identifier}, {identifier}Widget>("{slug}");""" | |||
| return source | |||
| def parse_args(args): | |||
| if len(args) >= 2: | |||
| if args[1] == 'createplugin': | |||
| if len(args) >= 3: | |||
| create_plugin(args[2]) | |||
| return | |||
| usage_create_plugin(args[0]) | |||
| return | |||
| if args[1] == 'createmodule': | |||
| if len(args) >= 3: | |||
| create_module(args[2]) | |||
| return | |||
| usage_create_module(args[0]) | |||
| return | |||
| usage(args[0]) | |||
| if __name__ == "__main__": | |||
| try: | |||
| parse_args(sys.argv) | |||
| except KeyboardInterrupt: | |||
| pass | |||
| except UserException as e: | |||
| print(e) | |||
| sys.exit(1) | |||