diff --git a/helper.py b/helper.py new file mode 100755 index 00000000..f6b5abb0 --- /dev/null +++ b/helper.py @@ -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} ... +Run commands without arguments for command help. + +Commands: + createplugin + createmodule + createmanifest +""" + print(text) + + +def usage_create_plugin(script): + text = f"""Usage: {script} createplugin + +A directory 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 + +Must be called in a plugin directory. +A panel file must exist in res/.svg. +A source file will be created at src/.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/.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(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild(createWidget(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(mm2px(Vec({c['cx']}, {c['cy']})), module, {identifier}::{c['name']}_PARAM));""" + else: + source += f""" + addParam(createParam(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(mm2px(Vec({c['cx']}, {c['cy']})), module, {identifier}::{c['name']}_INPUT));""" + else: + source += f""" + addInput(createInput(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(mm2px(Vec({c['cx']}, {c['cy']})), module, {identifier}::{c['name']}_OUTPUT));""" + else: + source += f""" + addOutput(createOutput(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>(mm2px(Vec({c['cx']}, {c['cy']})), module, {identifier}::{c['name']}_LIGHT));""" + else: + source += f""" + addChild(createLight>(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(mm2px(Vec({c['cx']}, {c['cy']}))));""" + else: + source += f""" + // mm2px(Vec({c['width']}, {c['height']})) + addChild(createWidget(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)