Vendor helgoboss/helgobox (ReaLearn) as basis for custom UI fork
Stripped upstream git history; starting point for replacing the native SWELL/Win32 mapping UI with something more suited to bulk editing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import {createSVGWindow} from "svgdom";
|
||||
import svgjs from "@svgdotjs/svg.js";
|
||||
|
||||
const {SVG, registerWindow} = svgjs;
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import pako from "pako";
|
||||
import got from "got";
|
||||
import '@svgdotjs/svg.topath.js';
|
||||
|
||||
const script_dir = path.resolve('doc/svg-gen');
|
||||
|
||||
// We need to embed the CSS into the SVG, otherwise the browser won't load it, tried it.
|
||||
// (see https://stackoverflow.com/questions/18434094/how-to-style-svg-with-external-css).
|
||||
const stylesheet = fs.readFileSync(path.resolve(script_dir, 'styles.css'));
|
||||
|
||||
generate()
|
||||
|
||||
async function generate() {
|
||||
fs.writeFileSync('doc/architecture/images/modules.svg', await generateModulesDiagram())
|
||||
fs.writeFileSync('doc/architecture/images/onion-layers.svg', generateOnionLayersDiagram())
|
||||
fs.writeFileSync('doc/architecture/images/components.svg', await generateComponentDiagram())
|
||||
fs.writeFileSync('doc/architecture/images/components-midi-fx.svg', await generateComponentDiagram([
|
||||
'role-realtime-general',
|
||||
'role-realtime-midi-general',
|
||||
'role-realtime-midi-fx'
|
||||
]))
|
||||
fs.writeFileSync('doc/architecture/images/components-midi-device.svg', await generateComponentDiagram([
|
||||
'role-realtime-general',
|
||||
'role-realtime-midi-general',
|
||||
'role-realtime-midi-device'
|
||||
]))
|
||||
fs.writeFileSync('doc/architecture/images/components-osc.svg', await generateComponentDiagram([
|
||||
'role-realtime-general',
|
||||
'role-realtime-osc',
|
||||
]))
|
||||
fs.writeFileSync('doc/architecture/images/components-management.svg', await generateComponentDiagram([
|
||||
'role-sync-data',
|
||||
'role-notify',
|
||||
]))
|
||||
}
|
||||
|
||||
async function generateModulesDiagram() {
|
||||
const draw = await loadDotToSvgCanvas('modules.dot');
|
||||
return draw.svg();
|
||||
}
|
||||
|
||||
async function generateComponentDiagram(shownEdgeClasses = undefined) {
|
||||
const draw = await loadDotToSvgCanvas('components.dot');
|
||||
if (shownEdgeClasses) {
|
||||
draw.style('.edge', {visibility: 'hidden'})
|
||||
for (let edgeClass of shownEdgeClasses) {
|
||||
draw.style(`.edge.${edgeClass}`, {visibility: 'visible'})
|
||||
}
|
||||
}
|
||||
return draw.svg();
|
||||
}
|
||||
|
||||
function generateOnionLayersDiagram() {
|
||||
const draw = initSvgCanvas();
|
||||
const width = 410;
|
||||
const height = 410;
|
||||
draw.size(width, height);
|
||||
embedStylesheet(draw);
|
||||
|
||||
// Default attributes
|
||||
const defaultFontSize = 15;
|
||||
const defaultArrowColor = 'black';
|
||||
const defaultArrowWidth = 2;
|
||||
const baseFont = {
|
||||
size: defaultFontSize,
|
||||
}
|
||||
const arrowPattern = createArrowPattern('rule-arrow');
|
||||
const defaultArrowHead = arrowHead();
|
||||
|
||||
// Layers
|
||||
const infrastructureLayer = layer(4, 'layer-infrastructure', 'infrastructure', [
|
||||
'GUI',
|
||||
'API',
|
||||
'Persistence',
|
||||
'Server',
|
||||
]);
|
||||
layer(3, 'layer-management', 'management');
|
||||
layer(2, 'layer-processing', 'processing');
|
||||
const baseLayer = layer(1, 'layer-base', 'base');
|
||||
|
||||
// Arrows
|
||||
drawArrow(infrastructureLayer.x(), infrastructureLayer.cy(), baseLayer.cx(), baseLayer.cy(), {
|
||||
text: 'may use code in',
|
||||
patternOrColor: arrowPattern,
|
||||
width: 10,
|
||||
drawHead: false,
|
||||
cssClass: 'rule-arrow',
|
||||
useClipping: true,
|
||||
});
|
||||
|
||||
function arrowHead() {
|
||||
return draw.marker(10, 7, (add) => {
|
||||
add.polygon('0,0 10,3.5 0,7').addClass('arrow-head');
|
||||
});
|
||||
}
|
||||
|
||||
function arcPath(radius, cx, cy, sweep) {
|
||||
return [
|
||||
// Start at 9
|
||||
['M', cx - radius, cy],
|
||||
// Go to 3
|
||||
['A', -radius, -radius, 0, 0, sweep, cx + radius, cy],
|
||||
];
|
||||
}
|
||||
|
||||
function layer(index, cssClass, label, components = []) {
|
||||
const g = draw.group().addClass(cssClass);
|
||||
const spacing = 50;
|
||||
const radius = index * spacing;
|
||||
const circle = g
|
||||
.circle(radius * 2)
|
||||
.center(width / 2, height / 2)
|
||||
.fill('none')
|
||||
.stroke({color: 'black'})
|
||||
.addClass('layerpart-circle')
|
||||
const pathRadius = radius - spacing / 2;
|
||||
const radiusFix = defaultFontSize / 3;
|
||||
const upperArc = arcPath(
|
||||
pathRadius - radiusFix,
|
||||
circle.cx(),
|
||||
circle.cy(),
|
||||
1
|
||||
);
|
||||
g.textPath(label, upperArc)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('startOffset', '50%')
|
||||
.font(baseFont)
|
||||
.addClass('layerpart-label')
|
||||
const lowerArc = arcPath(
|
||||
pathRadius + radiusFix,
|
||||
circle.cx(),
|
||||
circle.cy(),
|
||||
0
|
||||
);
|
||||
for (let i = 0; i < components.length; i++) {
|
||||
const segmentLength = (1 / components.length) * 100;
|
||||
const offset = i * segmentLength + segmentLength / 2;
|
||||
g.textPath(components[i], lowerArc)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('startOffset', `${offset}%`)
|
||||
.font(baseFont)
|
||||
.addClass('layerpart-secondary-label');
|
||||
}
|
||||
return circle;
|
||||
}
|
||||
|
||||
function createArrowPattern(cssClass = undefined) {
|
||||
return draw.pattern(30, 20, (add) => {
|
||||
const g = add.group().addClass(cssClass);
|
||||
g.line(0, 3.5, 10, 3.5)
|
||||
.stroke({color: defaultArrowColor, width: 1})
|
||||
.addClass('arrow-pattern-path');
|
||||
g.polygon('10,0 20,3.5 10,7').addClass('arrow-pattern-head');
|
||||
});
|
||||
}
|
||||
|
||||
function drawArrow(x1, y1, x2, y2, {
|
||||
patternOrColor = defaultArrowColor,
|
||||
width = defaultArrowWidth,
|
||||
head = defaultArrowHead,
|
||||
text,
|
||||
drawHead = true,
|
||||
cssClass = undefined,
|
||||
useClipping = false,
|
||||
}) {
|
||||
// Group for clipping
|
||||
const outer_group = draw.group().addClass(cssClass);
|
||||
const inner_group = outer_group.group();
|
||||
// Arrow itself
|
||||
const line = useClipping ? inner_group.line(x1 - 100, y1, x2 + 100, y2) : inner_group.line(x1, y1, x2, y2);
|
||||
const path = line.toPath().addClass('arrow-path');
|
||||
path.stroke({color: patternOrColor, width});
|
||||
// Head
|
||||
if (drawHead) {
|
||||
path.marker('end', head.size(8, 8))
|
||||
}
|
||||
// Text
|
||||
outer_group.textPath()
|
||||
.plot(path.array())
|
||||
.text(add => {
|
||||
add.tspan(text).dy(-10)
|
||||
})
|
||||
.addClass('arrowpart-label')
|
||||
.font({...baseFont, anchor: 'middle', startOffset: '50%'});
|
||||
// Clip (useful for keeping CSS transform animation within bounds)
|
||||
if (useClipping) {
|
||||
const clip = inner_group.clip()
|
||||
.add(inner_group.polygon().plot([[x1, y1 - width], [x2, y2 - width], [x2, y1 + width], [12, y2 + width]]));
|
||||
inner_group.clipWith(clip);
|
||||
}
|
||||
return outer_group;
|
||||
}
|
||||
|
||||
return draw.svg();
|
||||
}
|
||||
|
||||
async function loadDotToSvgCanvas(dotFileName) {
|
||||
const dot = fs.readFileSync(path.resolve(script_dir, dotFileName));
|
||||
const svg = await convertDotToSvg(dot);
|
||||
const qualifiedSvg = svg.replace(/<(!DOCTYPE[^>\[]+(\[[^\]]+)?)+[^>]+>/g, '');
|
||||
const window = createSVGWindow()
|
||||
const document = window.document
|
||||
registerWindow(window, document)
|
||||
const originalDraw = SVG(document.documentElement);
|
||||
originalDraw.svg(qualifiedSvg, true);
|
||||
const draw = SVG(document.documentElement);
|
||||
embedStylesheet(draw);
|
||||
return draw;
|
||||
}
|
||||
|
||||
async function convertDotToSvg(dot) {
|
||||
const data = Buffer.from(dot, 'utf8')
|
||||
const compressed = pako.deflate(data, {level: 9})
|
||||
const body = Buffer.from(compressed)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-').replace(/\//g, '_')
|
||||
const response = await got(`https://kroki.io/graphviz/svg/${body}`);
|
||||
return response.body
|
||||
}
|
||||
|
||||
function initSvgCanvas() {
|
||||
const window = createSVGWindow()
|
||||
const document = window.document
|
||||
registerWindow(window, document)
|
||||
return SVG(document.documentElement);
|
||||
}
|
||||
|
||||
function embedStylesheet(draw) {
|
||||
draw.element('style').words(stylesheet)
|
||||
}
|
||||
Reference in New Issue
Block a user