WIP: display ghost nodes when adding a new edge
This commit is contained in:
parent
7a2777df96
commit
9ab5eec5e9
@ -213,7 +213,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
</svg>
|
</svg>
|
||||||
New State
|
New State
|
||||||
</div>
|
</div>
|
||||||
<div class="menubottom menu-lightmode">
|
<div id="add-edge" class="menubottom menu-lightmode">
|
||||||
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
|
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
|
||||||
<polyline points="1,1 9,9" style="fill:none;stroke-width:2" stroke-linecap="round" />
|
<polyline points="1,1 9,9" style="fill:none;stroke-width:2" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
110
editor.ts
110
editor.ts
@ -202,13 +202,14 @@ export function search(text: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function format(possibleTargets: (WF.WFEdge | WF.WFNode)[], results: IndexSearchResult, heading: string) {
|
function format(possibleTargets: (WF.WFEdge | WF.WFNode | WF.WFGhostNode)[], results: IndexSearchResult, heading: string) {
|
||||||
var h = document.createElement('h3');
|
var h = document.createElement('h3');
|
||||||
h.innerHTML = heading;
|
h.innerHTML = heading;
|
||||||
searchResultList.appendChild(h);
|
searchResultList.appendChild(h);
|
||||||
results.forEach(result => {
|
results.forEach(result => {
|
||||||
var target: WF.WFEdge | WF.WFNode | null = null;
|
var target: WF.WFEdge | WF.WFNode | null = null;
|
||||||
possibleTargets.forEach(stateOrEdge => {
|
possibleTargets.forEach(stateOrEdge => {
|
||||||
|
if (stateOrEdge instanceof WF.WFGhostNode) return;
|
||||||
if (stateOrEdge.id === result)
|
if (stateOrEdge.id === result)
|
||||||
target = stateOrEdge;
|
target = stateOrEdge;
|
||||||
});
|
});
|
||||||
@ -413,6 +414,7 @@ export function selectViewer() {
|
|||||||
highlightedTargets = [];
|
highlightedTargets = [];
|
||||||
selectedActor.value = NO_ACTOR;
|
selectedActor.value = NO_ACTOR;
|
||||||
workflow.states.forEach(st => {
|
workflow.states.forEach(st => {
|
||||||
|
if (st instanceof WF.WFGhostNode) return;
|
||||||
if (st.stateData.viewerNames.includes(selectedViewer.value)) {
|
if (st.stateData.viewerNames.includes(selectedViewer.value)) {
|
||||||
highlightedSources.push(st.id);
|
highlightedSources.push(st.id);
|
||||||
}
|
}
|
||||||
@ -491,6 +493,7 @@ document.getElementById('side-panel-focus')?.addEventListener('click', _ => focu
|
|||||||
document.getElementById('side-panel-delete')?.addEventListener('click', _ => removeSelection());
|
document.getElementById('side-panel-delete')?.addEventListener('click', _ => removeSelection());
|
||||||
document.getElementById('file-panel-cancel')?.addEventListener('click', _ => closeFileDisplay());
|
document.getElementById('file-panel-cancel')?.addEventListener('click', _ => closeFileDisplay());
|
||||||
document.getElementById('add-state')?.addEventListener('click', _ => addState());
|
document.getElementById('add-state')?.addEventListener('click', _ => addState());
|
||||||
|
document.getElementById('add-edge')?.addEventListener('click', _ => addEdge());
|
||||||
document.getElementById('edge-from')?.addEventListener('click', _ => markEdgeFrom());
|
document.getElementById('edge-from')?.addEventListener('click', _ => markEdgeFrom());
|
||||||
document.getElementById('edge-to')?.addEventListener('click', _ => markEdgeTo());
|
document.getElementById('edge-to')?.addEventListener('click', _ => markEdgeTo());
|
||||||
document.getElementById('close-side-panel')?.addEventListener('click', _ => deselect());
|
document.getElementById('close-side-panel')?.addEventListener('click', _ => deselect());
|
||||||
@ -566,25 +569,50 @@ export function addState() {
|
|||||||
var x = newStateCoords.x;
|
var x = newStateCoords.x;
|
||||||
var y = newStateCoords.y;
|
var y = newStateCoords.y;
|
||||||
var state : WF.WFNode = { id: 'state_' + nodeId,
|
var state : WF.WFNode = { id: 'state_' + nodeId,
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
name: 'state_' + nodeId,
|
name: 'state_' + nodeId,
|
||||||
fx: x,
|
fx: x,
|
||||||
fy: y,
|
fy: y,
|
||||||
val: 5,
|
val: 5,
|
||||||
stateData: new WF.StateData({ abbreviation: `S${nodeId}`, final: 'false' }) };
|
stateData: new WF.StateData({ abbreviation: `S${nodeId}`, final: 'false' }) };
|
||||||
workflow.states.push(state);
|
workflow.states.push(state);
|
||||||
updateGraph();
|
updateGraph();
|
||||||
select(state);
|
select(state);
|
||||||
nodeIndex.add(state.id, state.name);
|
nodeIndex.add(state.id, state.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addEdge() {
|
||||||
|
var x = newStateCoords.x;
|
||||||
|
var y = newStateCoords.y;
|
||||||
|
var ghostState = new WF.WFGhostNode({
|
||||||
|
id: `@@ghost@(${x},${y})`,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
name: 'Drag me!',
|
||||||
|
fx: x,
|
||||||
|
fy: y,
|
||||||
|
val: 5 });
|
||||||
|
var ghostState2 = new WF.WFGhostNode({
|
||||||
|
id: `@@ghost@(${x+200},${y})`,
|
||||||
|
x: x + 200,
|
||||||
|
y: y,
|
||||||
|
name: 'Drag me!',
|
||||||
|
fx: x + 200,
|
||||||
|
fy: y,
|
||||||
|
val: 5 });
|
||||||
|
workflow.states.push(ghostState, ghostState2);
|
||||||
|
console.log('is ghost:', ghostState instanceof WF.WFGhostNode, ghostState2 instanceof WF.WFGhostNode);
|
||||||
|
updateGraph();
|
||||||
|
connect(ghostState, ghostState2);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new action between two states.
|
* Adds a new action between two states.
|
||||||
* @param source The source state.
|
* @param source The source state.
|
||||||
* @param target The target state.
|
* @param target The target state.
|
||||||
*/
|
*/
|
||||||
function connect(source: WF.WFNode, target: WF.WFNode) {
|
function connect(source: WF.WFNode | WF.WFGhostNode, target: WF.WFNode | WF.WFGhostNode) {
|
||||||
let linkId = actionIdCounter ++;
|
let linkId = actionIdCounter ++;
|
||||||
var action : WF.WFEdge = new WF.WFEdge({
|
var action : WF.WFEdge = new WF.WFEdge({
|
||||||
id: (linkId).toString(),
|
id: (linkId).toString(),
|
||||||
@ -817,6 +845,7 @@ function prepareWorkflow() {
|
|||||||
});
|
});
|
||||||
//Identify all viewers of every state
|
//Identify all viewers of every state
|
||||||
workflow.states.forEach(st => {
|
workflow.states.forEach(st => {
|
||||||
|
if (st instanceof WF.WFGhostNode) return;
|
||||||
if (st.name === '@@INIT') {
|
if (st.name === '@@INIT') {
|
||||||
initState = st;
|
initState = st;
|
||||||
} else if (st.stateData.viewers.length() === 0) {
|
} else if (st.stateData.viewers.length() === 0) {
|
||||||
@ -858,6 +887,7 @@ function prepareWorkflow() {
|
|||||||
|
|
||||||
//Compute abbreviations of the names of all states
|
//Compute abbreviations of the names of all states
|
||||||
workflow.states.forEach(state => {
|
workflow.states.forEach(state => {
|
||||||
|
if (state instanceof WF.WFGhostNode) return;
|
||||||
// var label = node.name.substring(0, 5);
|
// var label = node.name.substring(0, 5);
|
||||||
var label = state.name.split(' '); // [node.name.substring(0, 6), node.name.substring(6, 12), node.name.substring(12, 18)];
|
var label = state.name.split(' '); // [node.name.substring(0, 6), node.name.substring(6, 12), node.name.substring(12, 18)];
|
||||||
for (var i = 0; i < label.length; i++) {
|
for (var i = 0; i < label.length; i++) {
|
||||||
@ -940,12 +970,12 @@ const edgeColourMostSubtle = '#99999944';
|
|||||||
* @param node
|
* @param node
|
||||||
* @returns The colour the given node should have.
|
* @returns The colour the given node should have.
|
||||||
*/
|
*/
|
||||||
function getNodeColour(node: WF.WFNode) {
|
function getNodeColour(node: WF.WFNode | WF.WFGhostNode) {
|
||||||
var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER)
|
var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER)
|
||||||
|| highlightedSources.includes(node.id) || highlightedTargets.includes(node.id)
|
|| highlightedSources.includes(node.id) || highlightedTargets.includes(node.id)
|
||||||
var alpha = standard ? 'ff' : '55';
|
var alpha = standard ? 'ff' : '55';
|
||||||
var isSelected = selection === node || rightSelection === node;
|
var isSelected = selection === node || rightSelection === node;
|
||||||
if (node.stateData && node.stateData.final !== 'false' && node.stateData.final !== '') {
|
if (node instanceof WF.WFNode && node.stateData.final !== 'false' && node.stateData.final !== '') {
|
||||||
if (node.stateData.final === 'true' || node.stateData.final === 'ok') {
|
if (node.stateData.final === 'true' || node.stateData.final === 'ok') {
|
||||||
return (isSelected ? '#3ac713' : '#31a810') + alpha;
|
return (isSelected ? '#3ac713' : '#31a810') + alpha;
|
||||||
} else if (node.stateData.final === 'not-ok') {
|
} else if (node.stateData.final === 'not-ok') {
|
||||||
@ -955,6 +985,8 @@ function getNodeColour(node: WF.WFNode) {
|
|||||||
}
|
}
|
||||||
} else if (node.name === '@@INIT') {
|
} else if (node.name === '@@INIT') {
|
||||||
return (isSelected ? '#ffbc15' : '#eeaa00') + alpha;
|
return (isSelected ? '#ffbc15' : '#eeaa00') + alpha;
|
||||||
|
} else if (node instanceof WF.WFGhostNode) {
|
||||||
|
return '#cafc03ff';
|
||||||
} else {
|
} else {
|
||||||
return (isSelected ? '#538cd9' : '#3679d2') + alpha;
|
return (isSelected ? '#538cd9' : '#3679d2') + alpha;
|
||||||
}
|
}
|
||||||
@ -1081,26 +1113,42 @@ function getEdgeColour(edge: LinkObject) {
|
|||||||
.linkDirectionalParticleColor(() => darkMode ? '#ffffff55' : '#00000055')
|
.linkDirectionalParticleColor(() => darkMode ? '#ffffff55' : '#00000055')
|
||||||
.linkDirectionalParticleWidth((edge: LinkObject) => (isHighlightedActorEdge(edge as WF.WFEdge)) ? 3 : 0)
|
.linkDirectionalParticleWidth((edge: LinkObject) => (isHighlightedActorEdge(edge as WF.WFEdge)) ? 3 : 0)
|
||||||
.nodeCanvasObject((node: NodeObject, ctx: CanvasRenderingContext2D) => {
|
.nodeCanvasObject((node: NodeObject, ctx: CanvasRenderingContext2D) => {
|
||||||
const wfNode = node as WF.WFNode;
|
const wfNode = (node instanceof WF.WFNode) ? node as WF.WFNode : node as WF.WFGhostNode;
|
||||||
ctx.fillStyle = getNodeColour(wfNode);
|
ctx.fillStyle = getNodeColour(wfNode);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(wfNode.x, wfNode.y, 2*wfNode.val, 0, 2 * Math.PI, false);
|
ctx.arc(wfNode.x, wfNode.y, 2*wfNode.val, 0, 2 * Math.PI, false);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
if (node === selection || node === rightSelection) {
|
|
||||||
ctx.strokeStyle = darkMode ? 'white' : 'black';
|
var selected = (node === selection || node === rightSelection);
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = selected ? 1 : 0.2;
|
||||||
|
|
||||||
|
if (node instanceof WF.WFGhostNode) {
|
||||||
|
ctx.save()
|
||||||
ctx.setLineDash([1, 2]);
|
ctx.setLineDash([1, 2]);
|
||||||
|
ctx.strokeStyle = darkMode ? 'white' : 'black';
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
} else {
|
||||||
|
ctx.save();
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
if (selected)
|
||||||
|
ctx.strokeStyle = darkMode ? 'white' : 'black';
|
||||||
|
else
|
||||||
|
ctx.strokeStyle = !darkMode ? 'white' : 'black';
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! (wfNode.stateData && wfNode.stateData.abbreviation)) return;
|
|
||||||
|
|
||||||
ctx.fillStyle = 'white';
|
ctx.fillStyle = 'white';
|
||||||
ctx.font = '4px Inter';
|
ctx.font = '4px Inter';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.fillText(wfNode.stateData.abbreviation, wfNode.x, wfNode.y);
|
|
||||||
|
if (wfNode instanceof WF.WFNode && wfNode.stateData.abbreviation)
|
||||||
|
ctx.fillText(wfNode.stateData.abbreviation, wfNode.x, wfNode.y);
|
||||||
|
else if (wfNode instanceof WF.WFGhostNode)
|
||||||
|
ctx.fillText(wfNode.text, wfNode.x, wfNode.y);
|
||||||
})
|
})
|
||||||
.onNodeDragEnd((node: NodeObject) => {
|
.onNodeDragEnd((node: NodeObject) => {
|
||||||
node.fx = node.x;
|
node.fx = node.x;
|
||||||
@ -1155,6 +1203,32 @@ function getEdgeColour(edge: LinkObject) {
|
|||||||
})
|
})
|
||||||
.autoPauseRedraw(false);
|
.autoPauseRedraw(false);
|
||||||
|
|
||||||
|
//Remove all ghost states from the force computations to enable dragging them onto normal states
|
||||||
|
var oldChargeInit = <any>((<any>wfGraph.d3Force('charge')).initialize);
|
||||||
|
var oldCenterInit = <any>((<any>wfGraph.d3Force('center')).initialize);
|
||||||
|
var oldLinkInit = <any>((<any>wfGraph.d3Force('link')).initialize);
|
||||||
|
(<any>wfGraph.d3Force('charge')).initialize = function(_nodes: NodeObject[], ...args: any) {
|
||||||
|
var nodes : WF.WFNode[] = [];
|
||||||
|
_nodes.forEach(node => (node instanceof WF.WFNode && nodes.push(node)));
|
||||||
|
console.log('rem. total:', nodes.length); //TODO already store them instead of computing each tick
|
||||||
|
oldChargeInit(nodes, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
(<any>wfGraph.d3Force('center')).initialize = function(_nodes: NodeObject[], ...args: any) {
|
||||||
|
var nodes : WF.WFNode[] = [];
|
||||||
|
_nodes.forEach(node => (node instanceof WF.WFNode && nodes.push(node)));
|
||||||
|
console.log('rem. total:', nodes.length);
|
||||||
|
oldCenterInit(nodes, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
(<any>wfGraph.d3Force('link')).initialize = function(_nodes: NodeObject[], ...args: any) {
|
||||||
|
var nodes : WF.WFNode[] = [];
|
||||||
|
_nodes.forEach(node => (node instanceof WF.WFNode && nodes.push(node)));
|
||||||
|
console.log('rem. total:', nodes.length);
|
||||||
|
oldLinkInit(nodes, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
updateGraph();
|
updateGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
start.sh
6
start.sh
@ -5,9 +5,9 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
|
||||||
echo 'Transpiling to JS...'
|
# echo 'Transpiling to JS...'
|
||||||
npx tsc
|
# npx tsc
|
||||||
echo 'Generating Webpack bundle...'
|
echo 'Transpiling to JS & generating Webpack bundle...'
|
||||||
npx webpack
|
npx webpack
|
||||||
echo 'Starting server...'
|
echo 'Starting server...'
|
||||||
npx http-server --cors -o ./editor.html
|
npx http-server --cors -o ./editor.html
|
||||||
|
|||||||
45
workflow.ts
45
workflow.ts
@ -20,6 +20,16 @@ export type NodeFormat = {
|
|||||||
val: number
|
val: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GhostNodeFormat = {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
fx: number,
|
||||||
|
fy: number,
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
val: number
|
||||||
|
}
|
||||||
|
|
||||||
export type SDFormat = {
|
export type SDFormat = {
|
||||||
abbreviation: string,
|
abbreviation: string,
|
||||||
final: boolean | string,
|
final: boolean | string,
|
||||||
@ -30,8 +40,8 @@ export type SDFormat = {
|
|||||||
|
|
||||||
export type EdgeFormat = {
|
export type EdgeFormat = {
|
||||||
actionData: ADFormat,
|
actionData: ADFormat,
|
||||||
source: WFNode | string,
|
source: WFNode | WFGhostNode | string,
|
||||||
target: WFNode | string,
|
target: WFNode | WFGhostNode | string,
|
||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
nodePairId : string
|
nodePairId : string
|
||||||
@ -82,7 +92,7 @@ export type MessageFormat = {
|
|||||||
|
|
||||||
export class Workflow {
|
export class Workflow {
|
||||||
|
|
||||||
states: WFNode[];
|
states: (WFNode | WFGhostNode)[];
|
||||||
actions: WFEdge[];
|
actions: WFEdge[];
|
||||||
|
|
||||||
constructor(json: WF) {
|
constructor(json: WF) {
|
||||||
@ -95,7 +105,7 @@ export class Workflow {
|
|||||||
}
|
}
|
||||||
// Resolve ID refs to nodes
|
// Resolve ID refs to nodes
|
||||||
var actions = json.actions.map(action => {
|
var actions = json.actions.map(action => {
|
||||||
function transformRef(ref: string | WFNode) {
|
function transformRef(ref: string | WFNode | WFGhostNode) {
|
||||||
if (ref instanceof String)
|
if (ref instanceof String)
|
||||||
stateMap.has(<string>ref)
|
stateMap.has(<string>ref)
|
||||||
&& (ref = <WFNode>stateMap.get(<string>ref))
|
&& (ref = <WFNode>stateMap.get(<string>ref))
|
||||||
@ -155,11 +165,34 @@ export class StateData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class WFGhostNode implements NodeObject {
|
||||||
|
|
||||||
|
text: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
fx: number;
|
||||||
|
fy: number;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
val: number;
|
||||||
|
|
||||||
|
constructor(json: GhostNodeFormat) {
|
||||||
|
this.text = 'Drag me!';
|
||||||
|
this.x = json.x;
|
||||||
|
this.y = json.y;
|
||||||
|
this.fx = json.fx;
|
||||||
|
this.fy = json.fy;
|
||||||
|
this.id = json.id;
|
||||||
|
this.name = json.name;
|
||||||
|
this.val = json.val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class WFEdge implements LinkObject {
|
export class WFEdge implements LinkObject {
|
||||||
|
|
||||||
actionData: ActionData;
|
actionData: ActionData;
|
||||||
source: WFNode;
|
source: WFNode | WFGhostNode;
|
||||||
target: WFNode;
|
target: WFNode | WFGhostNode;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
nodePairId : string;
|
nodePairId : string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user