Browse Source

Add helper script for creating plugins

tags/v1.0.0
Andrew Belt 5 years ago
parent
commit
2bfde326b6
1 changed files with 497 additions and 0 deletions
  1. +497
    -0
      helper.py

+ 497
- 0
helper.py View File

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

Loading…
Cancel
Save