| @@ -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) | |||||