Script-to-graph functions added for new method

This commit is contained in:
Thomas Lovén 2021-03-26 21:05:13 +00:00
parent b1916fe55d
commit ccbe62c0d9
9 changed files with 585 additions and 63 deletions

31
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,31 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "npm: build",
"type": "npm",
"script": "build",
"problemMatcher": []
},
{
"label": "npm: watch",
"type": "npm",
"script": "watch",
"problemMatcher": [],
"presentation": {
"panel": "shared",
"group": "test"
}
},
{
"label": "npm: serve",
"type": "npm",
"script": "serve",
"problemMatcher": [],
"presentation": {
"panel": "shared",
"group": "test"
}
}
]
}

View File

@ -3,7 +3,7 @@
<head> <head>
<title>Script graph demo</title> <title>Script graph demo</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.14.0/js-yaml.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.14.0/js-yaml.min.js"></script>
<script src="index.js" type="module"></script> <script src="index.js" type="module" defer></script>
</head> </head>
<body style="margin: 24px"> <body style="margin: 24px">
<style> <style>
@ -35,36 +35,6 @@
<div> <div>
<div class="card" id="graph"> <div class="card" id="graph">
</div> </div>
<div class="card" id="graph2">
<script-graph3>
<script-graph-node></script-graph-node>
<script-graph-node
icon="check"
></script-graph-node>
<script-graph-branch>
<script-graph-node
icon="check"
></script-graph-node>
<script-graph3>
<script-graph-node
icon="check"
></script-graph-node>
<script-graph-node
icon="check"
></script-graph-node>
</script-graph3>
<script-graph-branch>
<script-graph-node
icon="check"
></script-graph-node>
<script-graph-node
icon="check"
></script-graph-node>
</script-graph-branch>
</script-graph-branch>
<script-graph-node></script-graph-node>
</script-graph3>
</div>
<div class="card code"> <div class="card code">
<h1> Selected node code </h1> <h1> Selected node code </h1>
<wc-codemirror mode="json" id="snippet"> <wc-codemirror mode="json" id="snippet">

View File

@ -6,7 +6,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c",
"watch": "rollup -c -w", "watch": "rollup -c --watch",
"serve": "http-server ." "serve": "http-server ."
}, },
"author": "", "author": "",

View File

@ -1,13 +1,11 @@
import resolve from "@rollup/plugin-node-resolve"; import nodeResolve from "@rollup/plugin-node-resolve";
import typescript from "@rollup/plugin-typescript"; import typescript from "@rollup/plugin-typescript";
export default { export default {
input: "./src/main.js", input: "src/main.js",
output: [ output: {
{
file: "index.js", file: "index.js",
format: "cjs" format: "es",
} },
], plugins: [nodeResolve(), typescript()],
plugins: [resolve(), typescript()]
}; };

View File

@ -0,0 +1,118 @@
import { css, LitElement, property, svg } from "lit-element";
import {
mdiCallSplit,
mdiAbTesting,
mdiCheck,
mdiClose,
mdiChevronRight,
mdiExclamation,
mdiTimerOutline,
mdiTrafficLight,
mdiRefresh,
mdiArrowUp,
mdiCodeJson,
mdiCheckBoxOutline,
mdiCheckboxBlankOutline,
mdiAsterisk,
mdiCircleOutline,
} from "@mdi/js";
const NODE_SIZE = 24;
const ICONS = {
"call-split": mdiCallSplit,
"ab-testing": mdiAbTesting,
check: mdiCheck,
close: mdiClose,
"chevron-right": mdiChevronRight,
exclamation: mdiExclamation,
asterisk: mdiAsterisk,
};
export class HatGraphNode extends LitElement {
@property() icon = "chevron-right";
@property() iconPath?;
connectedCallback() {
super.connectedCallback();
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0");
}
updateNode(config) {
this.dispatchEvent(
new CustomEvent("update-node", {
detail: { config },
bubbles: true,
composed: true,
})
);
}
deleteNode() {
this.dispatchEvent(new CustomEvent("delete-node", { bubbles: true }));
}
prependNode(config) {
this.dispatchEvent(
new CustomEvent("prepend-node", { detail: { config }, bubbles: true })
);
}
get width() {
return NODE_SIZE + 5;
}
get height() {
return NODE_SIZE + 5;
}
render() {
return svg`
<svg
width="${this.width}"
height="${this.height}"
viewBox="${-this.width / 2} 0 ${this.width} ${this.height}"
>
<circle
cx="0"
cy="${this.width / 2}"
r="${NODE_SIZE / 2}"
/>
<g
style="pointer-events: none"
transform="translate(${-12} ${this.width / 2 - 12})"
>
${
this.iconPath
? svg`<path d="${this.iconPath}"/>`
: ICONS[this.icon]
? svg`<path d="${ICONS[this.icon]}"/>`
: ""
}
</g>
</svg>
`;
}
static get styles() {
return css`
:host {
display: flex;
--stroke-clr: var(--stroke-color, rgb(3, 169, 244));
--hover-clr: var(--hover-color, rgb(255, 152, 0));
}
circle {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: white;
}
:host(:hover) {
--stroke-clr: var(--hover-clr);
}
:host(:focus) {
--stroke-clr: green;
outline: none;
}
`;
}
}
customElements.define("hat-graph-node", HatGraphNode);

162
src/hat-graph/hat-graph.ts Normal file
View File

@ -0,0 +1,162 @@
import { css, html, LitElement, property, svg } from "lit-element";
import "./hat-graph-node";
const BRANCH_HEIGHT = 30;
const BRANCH_CURVATURE = 25;
const VERTICAL_SPACING = 10;
const NODE_SIZE = 24;
export class HatGraph extends LitElement {
@property() _num_items = 0;
@property({ reflect: true }) branching?;
get width() {
let w = 0;
if (this.branching !== undefined) {
for (const c of this.children) {
if (c.slot === "head") continue;
w += (c as any).width;
}
} else {
for (const c of this.children) {
w = Math.max(w, (c as any).width);
}
}
return w;
}
get height() {
let h = 0;
if (this.branching !== undefined) {
for (const c of this.children) {
if (c.slot === "head") continue;
h = Math.max(h, (c as any).height);
}
h += 2 * BRANCH_HEIGHT;
} else {
for (const c of this.children) {
h += (c as any).height + VERTICAL_SPACING;
}
h;
}
return h;
}
async updateChildren() {
this._num_items = this.children.length;
}
render() {
let branch_x = [];
let total = 0;
if (this.branching !== undefined) {
for (const c of Array.from(this.children)) {
if (c.slot === "head") continue;
const rect = c.getBoundingClientRect();
branch_x.push(rect.width / 2 + total);
total += rect.width;
}
}
const line_end = this.height - BRANCH_HEIGHT;
return html`
<svg width="${this.width}" height="${this.height}">
<rect
x="0"
y="0"
width="${this.width}"
height="${this.height}"
fill="white"
/>
${this.branching !== undefined
? branch_x.map((x) => {
return svg`
<path
class="line"
d="
M ${this.width / 2} 0
C ${this.width / 2} ${BRANCH_CURVATURE}
${x} ${BRANCH_HEIGHT - BRANCH_CURVATURE}
${x} ${BRANCH_HEIGHT}
L ${x} ${line_end}
C ${x} ${line_end + BRANCH_CURVATURE}
${this.width / 2} ${this.height - BRANCH_CURVATURE}
${this.width / 2} ${this.height}
"
/>
`;
})
: svg`
<path
class="line"
d="
M ${this.width / 2} 0
L ${this.width / 2} ${this.height}
"
/>
`}
</svg>
<slot name="head"> </slot>
<div
id="branches"
class="${this.querySelector(":scope > [slot='head']") ? "" : "no-head"}"
>
<slot @slotchange=${this.updateChildren}></slot>
</div>
`;
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
--stroke-clr: var(--stroke-color, rgb(3, 169, 244));
--hover-clr: var(--hover-color, rgb(255, 152, 0));
}
#branches {
position: absolute;
left: 0;
display: flex;
flex-direction: row;
align-items: center;
}
:host([branching]) #branches {
top: ${BRANCH_HEIGHT}px;
flex-direction: row;
align-items: start;
}
:host(:not([branching])) #branches {
top: ${VERTICAL_SPACING + NODE_SIZE}px; /* SHould be something else*/
flex-direction: column;
}
:host(:not([branching])) #branches.no-head {
top: 0;
}
path.line {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
:host(:not([branching])) ::slotted(*) {
margin-bottom: 10px;
}
::slotted(:last-child) {
margin-bottom: 0;
}
::slotted([slot="head"]) {
position: absolute;
top: ${BRANCH_HEIGHT / 2}px;
left: 50%;
transform: translate(-50%, -50%);
}
:host(:focus-within) ::slotted([slot="head"]) {
--stroke-color: green;
}
`;
}
}
customElements.define("hat-graph", HatGraph);

193
src/hat-graph/make-graph.ts Normal file
View File

@ -0,0 +1,193 @@
import { HatGraphNode } from "./hat-graph-node";
import {
mdiCallSplit,
mdiAbTesting,
mdiCheck,
mdiClose,
mdiChevronRight,
mdiExclamation,
mdiTimerOutline,
mdiTrafficLight,
mdiRefresh,
mdiArrowUp,
mdiCodeJson,
mdiCheckBoxOutline,
mdiCheckboxBlankOutline,
mdiAsterisk,
} from "@mdi/js";
import { HatGraph } from "./hat-graph";
import { html } from "lit-element";
import { render } from "lit-html";
const OPTIONS = [
"condition",
"delay",
"device_id",
"event",
"scene",
"service",
"wait_template",
"repeat",
"choose",
];
const ICONS = {
new: mdiAsterisk,
service: mdiChevronRight,
condition: mdiAbTesting,
TRUE: mdiCheck,
FALSE: mdiClose,
delay: mdiTimerOutline,
wait_template: mdiTrafficLight,
event: mdiExclamation,
repeat: mdiRefresh,
repeatReturn: mdiArrowUp,
choose: mdiCallSplit,
chooseChoice: mdiCheckBoxOutline,
chooseDefault: mdiCheckboxBlankOutline,
YAML: mdiCodeJson,
};
const SPECIAL_NODES = {
condition: makeConditionNode,
choose: makeChooseNode,
repeat: makeRepeatNode,
};
function makeConditionNode(config) {
const graph = document.createElement("hat-graph") as HatGraph;
graph.branching = true;
const focused = () =>
graph.dispatchEvent(
new CustomEvent("node-selected", { detail: { config }, bubbles: true })
);
render(
html`
<hat-graph-node slot="head" .iconPath=${mdiAbTesting} @focus=${focused}>
</hat-graph-node>
<hat-graph-node .iconPath=${mdiCheck} @focus=${focused}></hat-graph-node>
<hat-graph-node .iconPath=${mdiClose} @focus=${focused}></hat-graph-node>
`,
graph
);
return graph;
}
function makeChooseNode(config) {
const graph = document.createElement("hat-graph") as HatGraph;
graph.branching = true;
const focused = () =>
graph.dispatchEvent(
new CustomEvent("node-selected", { detail: { config }, bubbles: true })
);
render(
html`
<hat-graph-node slot="head" iconPath="${mdiCallSplit}" @focus=${focused}>
</hat-graph-node>
${config.choose?.map((branch) => {
return html`
<hat-graph>
<hat-graph-node
slot="head"
.iconPath=${mdiCheckBoxOutline}
@focus=${focused}
></hat-graph-node>
${branch.sequence.map((node) => makeNode(node))}
</hat-graph>
`;
})}
<hat-graph>
<hat-graph-node
slot="head"
.iconPath=${mdiCheckboxBlankOutline}
@focus=${focused}
></hat-graph-node>
${config.default.map((node) => makeNode(node))}
</hat-graph>
`,
graph
);
return graph;
}
function makeRepeatNode(config) {
const graph = document.createElement("hat-graph") as HatGraph;
graph.branching = true;
const focused = () =>
graph.dispatchEvent(
new CustomEvent("node-selected", { detail: { config }, bubbles: true })
);
render(
html`
<hat-graph-node
.iconPath=${mdiArrowUp}
@focus=${focused}
></hat-graph-node>
<hat-graph>
${config.repeat.sequence.map((node) => makeNode(node))}
</hat-graph>
`,
graph
);
return graph;
}
function makeNode(config) {
const type = OPTIONS.find((key) => key in config) || "yaml";
if (type in SPECIAL_NODES) {
return SPECIAL_NODES[type](config);
}
const node = document.createElement("hat-graph-node") as HatGraphNode;
node.iconPath = ICONS[type];
node.addEventListener("focus", (ev) => {
node.dispatchEvent(
new CustomEvent("node-selected", { detail: { config }, bubbles: true })
);
});
return node;
}
export function makeGraph(nodes) {
const graph = document.createElement("hat-graph") as HatGraph;
for (const [i, nodeConfig] of nodes.entries()) {
const node = makeNode(nodeConfig);
node.addEventListener("update-node", (ev) => {
ev.stopPropagation();
const config = [...nodes];
config[i] = ev.detail.config;
graph.dispatchEvent(
new CustomEvent("update-node", { detail: { config }, bubbles: true })
);
});
node.addEventListener("delete-node", (ev) => {
ev.stopPropagation();
const config = [...nodes];
config.splice(i, 1);
graph.dispatchEvent(
new CustomEvent("update-node", { detail: { config }, bubbles: true })
);
});
graph.appendChild(node);
}
return graph;
}

View File

@ -1,8 +1,11 @@
import {demoConfig} from "./demo-config"; import { demoConfig2 } from "./demo-config";
import "@vanillawc/wc-codemirror"; import "@vanillawc/wc-codemirror";
import "./script-graph"; import "./script-graph";
import "./script-graph3"; // import "./script-graph3";
import "./hat-graph/hat-graph-node";
import "./hat-graph/hat-graph";
import { makeGraph } from "./hat-graph/make-graph";
import { ActionHandler } from "./script-to-graph"; import { ActionHandler } from "./script-to-graph";
@ -12,7 +15,7 @@ let index_counter = 0;
let nodes = []; let nodes = [];
window.onload = () => { window.onload = () => {
return;
//const graph2 = document.createElement("script-graph3"); //const graph2 = document.createElement("script-graph3");
//document.querySelector("#graph2").appendChild(graph2); //document.querySelector("#graph2").appendChild(graph2);
@ -20,10 +23,10 @@ window.onload = () => {
const fullcode = document.querySelector("#fullcode"); const fullcode = document.querySelector("#fullcode");
fullcode.mode = "yaml"; fullcode.mode = "yaml";
window.setTimeout(()=> fullcode.value = jsyaml.safeDump(src), 100); window.setTimeout(() => (fullcode.value = jsyaml.safeDump(src)), 100);
const updateButton = document.querySelector("#updateButton"); const updateButton = document.querySelector("#updateButton");
updateButton.addEventListener('click', () => { updateButton.addEventListener("click", () => {
src = jsyaml.safeLoad(fullcode.value); src = jsyaml.safeLoad(fullcode.value);
index_counter = 0; index_counter = 0;
nodes = []; nodes = [];
@ -43,11 +46,67 @@ window.onload = () => {
graph.tree = tr.graph; graph.tree = tr.graph;
const code = document.querySelector("#snippet"); const code = document.querySelector("#snippet");
code.value = jsyaml.safeDump(action); code.value = jsyaml.safeDump(action);
document.querySelector("#saveSnippet").onclick = () => update(jsyaml.safeLoad(code.value)); document.querySelector("#saveSnippet").onclick = () =>
document.querySelector("#deleteSnippet").onclick = () => update(jsyaml.safeLoad(null)); update(jsyaml.safeLoad(code.value));
} document.querySelector("#deleteSnippet").onclick = () =>
update(jsyaml.safeLoad(null));
};
graph.tree = tr.graph; graph.tree = tr.graph;
document.querySelector("#graph").appendChild(graph); document.querySelector("#graph").appendChild(graph);
};
function nodeSelected(ev) {
console.log(ev);
const code = document.querySelector("#snippet");
code.value = jsyaml.safeDump(ev.detail.config);
code.currentNode = ev.target;
} }
function rebuildGraph(config) {
const graphCard = document.querySelector("#graph");
while (graphCard.firstChild) graphCard.removeChild(graphCard.firstChild);
const graph = makeGraph(config);
graph.addEventListener("node-selected", nodeSelected);
graph.addEventListener("update-node", (ev) => {
const code = document.querySelector("#fullcode");
code.value = jsyaml.safeDump(ev.detail.config);
rebuildGraph(ev.detail.config);
});
graphCard.appendChild(graph);
}
function setup() {
let src = demoConfig2;
const fullcode = document.querySelector("#fullcode");
fullcode.mode = "yaml";
window.setTimeout(() => (fullcode.value = jsyaml.safeDump(src)), 100);
rebuildGraph(src);
const updateButton = document.querySelector("#updateButton");
updateButton.addEventListener("click", () => {
src = jsyaml.safeLoad(fullcode.value);
rebuildGraph(src);
});
document.querySelector("#saveSnippet").onclick = () => {
const code = document.querySelector("#snippet");
console.log(code.currentNode);
if (code.currentNode) {
code.currentNode.updateNode(jsyaml.safeLoad(code.value));
}
};
document.querySelector("#deleteSnippet").onclick = () => {
const code = document.querySelector("#snippet");
console.log(code.currentNode);
if (code.currentNode) {
code.currentNode.deleteNode();
}
};
}
setup();

View File

@ -1,18 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "es2017",
"module": "ESNext",
"moduleResolution": "node", "moduleResolution": "node",
"sourceMap": true, "resolveJsonModule": true,
"emitDecoratorMetadata": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "experimentalDecorators": true
"removeComments": false, }
"noImplicitAny": false
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
} }