working graph editor
This commit is contained in:
parent
f6b4e43509
commit
0645651fd9
177
src/App.svelte
177
src/App.svelte
|
@ -1,12 +1,17 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { addNode, nodes, inputs, output, filters, previewCommand } from "./stores.js";
|
||||
import Input from "./Input.svelte";
|
||||
import Output from "./Output.svelte";
|
||||
import {
|
||||
selectedFilter,
|
||||
addNode,
|
||||
nodes,
|
||||
inputs,
|
||||
output,
|
||||
filters,
|
||||
previewCommand,
|
||||
} from "./stores.js";
|
||||
import Filter from "./Filter.svelte";
|
||||
import FilterPicker from "./FilterPicker.svelte";
|
||||
import Graph from "./Graph.svelte";
|
||||
// import GraphOld from "./GraphOld.svelte";
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import { fetchFile, toBlobURL } from "@ffmpeg/util";
|
||||
import { dndzone } from "svelte-dnd-action";
|
||||
|
@ -46,7 +51,11 @@
|
|||
for (let vid of $inputs) {
|
||||
await ffmpeg.writeFile(vid.name, await fetchFile("/" + vid.name));
|
||||
}
|
||||
let clist = $previewCommand.replaceAll('"', '').replace("ffmpeg", "").split(" ").filter(i => i.trim() != '');
|
||||
let clist = $previewCommand
|
||||
.replaceAll('"', "")
|
||||
.replace("ffmpeg", "")
|
||||
.split(" ")
|
||||
.filter((i) => i.trim() != "");
|
||||
console.log("command", clist);
|
||||
// command.push("-pix_fmt");
|
||||
// command.push("yuv420p");
|
||||
|
@ -84,11 +93,6 @@
|
|||
ffmpegLoaded = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function handleFilterSort(e) {
|
||||
filters.set(e.detail.items);
|
||||
}
|
||||
|
@ -105,50 +109,49 @@
|
|||
<main>
|
||||
<section class="header">
|
||||
<h1>FFmpeg Explorer</h1>
|
||||
<div class="help">
|
||||
<p>
|
||||
A tool to help you explore <a href="https://www.ffmpeg.org/" target="_blank">FFmpeg</a>
|
||||
filters and options. To use: select one or more input videos (there are currently two options),
|
||||
export and add some filters, and then hit "render" to preview the output in browser. Note: this
|
||||
is a work in progress, many things may still be broken! Only audio to audio and video to video
|
||||
filters are included. If it hangs/crashes refresh the page. Post issues/feedback to
|
||||
filters. To use:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Add filters from the list on the left.</li>
|
||||
<li>Click on filters in the center panel to edit options.</li>
|
||||
<li>Hit "render" to preview the output in browser.</li>
|
||||
<li>For more complex filtergraphs, disable "automatic layout."</li>
|
||||
</ol>
|
||||
<p>
|
||||
Note: this is a work in progress, many things may still be broken! If it hangs/crashes
|
||||
refresh the page. Post issues/feedback to
|
||||
<a href="https://github.com/antiboredom/ffmpeg-explorer/" target="_blank">GitHub</a>. By
|
||||
<a href="https://lav.io" target="_blank">Sam Lavigne</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<!-- {message} -->
|
||||
<section class="command">
|
||||
<h3>Output Command</h3>
|
||||
<div class="inner-command">
|
||||
<textarea readonly class="actual-command" bind:this={commandRef}>{$previewCommand}</textarea>
|
||||
<textarea readonly class="actual-command" bind:this={commandRef} on:click={() => commandRef.select()}>{$previewCommand}</textarea>
|
||||
<div>
|
||||
<button on:click={copyCommand}>Copy Command</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="inputs">
|
||||
<div class="section-header">
|
||||
<h3>Inputs</h3>
|
||||
<button on:click={newInput}>Add Input</button>
|
||||
</div>
|
||||
{#each $nodes as node, index}
|
||||
{#if node.nodeType === "input"}
|
||||
<Input bind:filename={node.data.name} id={node.id} {index} />
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section class="log">
|
||||
<h3>FFmpeg Log</h3>
|
||||
<textarea readonly class="the-log" bind:this={logbox}>{log}</textarea>
|
||||
</section>
|
||||
|
||||
<section class="preview">
|
||||
<div class="vid-holder">
|
||||
{#if rendering}
|
||||
<div class="rendering-video"><span>Rendering...</span></div>
|
||||
{/if}
|
||||
<video controls src={videoValue} />
|
||||
<div style="text-align: right;margin-top:5px;">
|
||||
</div>
|
||||
<div style="text-align: right;padding-top:5px;">
|
||||
<button on:click={render} disabled={!ffmpegLoaded || rendering}>
|
||||
{#if ffmpegLoaded}
|
||||
{#if rendering}
|
||||
|
@ -163,58 +166,39 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="output">
|
||||
<h3>Output</h3>
|
||||
{#each $nodes as node}
|
||||
{#if node.nodeType==="output"}
|
||||
<Output bind:filename={node.data.name} />
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section class="filters">
|
||||
<h3>Filters (click to add)</h3>
|
||||
<div class="inner-filters">
|
||||
<div class="filter-picker">
|
||||
<FilterPicker select={"video"} />
|
||||
</div>
|
||||
<div
|
||||
class="filters-holder"
|
||||
use:dndzone={{ items: $filters }}
|
||||
on:consider={handleFilterSort}
|
||||
on:finalize={handleFilterSort}
|
||||
>
|
||||
{#each $nodes as f (f.id)}
|
||||
{#if f.nodeType === "filter"}
|
||||
<div class="filter">
|
||||
<Filter bind:filter={f.data} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- <section class="graph"> -->
|
||||
<!-- <GraphOld /> -->
|
||||
<!-- </section> -->
|
||||
|
||||
<section class="graph">
|
||||
<Graph />
|
||||
</section>
|
||||
|
||||
<section class="filter-editor">
|
||||
{#if $selectedFilter && $nodes.length > 0 && $nodes[$selectedFilter]}
|
||||
<Filter bind:filter={$nodes[$selectedFilter].data} />
|
||||
{/if}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: 300px 1fr 1fr 1fr 1fr 300px;
|
||||
grid-template-areas:
|
||||
"hdr cmd cmd"
|
||||
"inp log prv"
|
||||
"out log prv"
|
||||
"flt flt flt";
|
||||
"hdr log log log prv prv"
|
||||
"hdr cmd cmd cmd prv prv"
|
||||
"flt gra gra gra gra edt";
|
||||
/* grid-template-rows: 16% 17% 77%; */
|
||||
grid-template-rows: 15% 15% calc(70% - 40px);
|
||||
padding: 10px;
|
||||
grid-gap: 20px;
|
||||
padding: 20px;
|
||||
height: 100vh;
|
||||
border: 1px solid blue;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
section {
|
||||
|
@ -228,6 +212,7 @@
|
|||
|
||||
.header {
|
||||
grid-area: hdr;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.command {
|
||||
|
@ -236,17 +221,26 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
grid-area: inp;
|
||||
}
|
||||
|
||||
.preview {
|
||||
grid-area: prv;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.output {
|
||||
grid-area: out;
|
||||
.preview video {
|
||||
width: 100%;
|
||||
/* object-fit: contain; */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vid-holder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.log {
|
||||
|
@ -255,19 +249,25 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inner-filters {
|
||||
.filters {
|
||||
grid-area: flt;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.graph {
|
||||
grid-area: gra;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filters {
|
||||
grid-area: flt;
|
||||
.filter-editor {
|
||||
grid-area: edt;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filter-picker {
|
||||
max-height: 500px;
|
||||
width: 400px;
|
||||
|
||||
position: sticky;
|
||||
/* max-height: 500px; */
|
||||
/* width: 400px; */
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: scroll;
|
||||
|
@ -302,14 +302,20 @@
|
|||
.inner-command {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
textarea {
|
||||
outline: none;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.actual-command {
|
||||
border: none;
|
||||
margin-right: 10px;
|
||||
resize: none;
|
||||
flex: 1;
|
||||
font: inherit;
|
||||
padding: 5px;
|
||||
|
@ -324,6 +330,7 @@
|
|||
.the-log {
|
||||
border: none;
|
||||
resize: none;
|
||||
padding: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
.rendering-video {
|
||||
|
@ -339,6 +346,17 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.help {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
ol {
|
||||
margin: 5px 0px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
ol li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1400px) {
|
||||
.filters-holder {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
@ -368,9 +386,6 @@
|
|||
padding: 10px;
|
||||
box-shadow: 2px 2px 0px #000;
|
||||
}
|
||||
.inner-filters {
|
||||
display: block;
|
||||
}
|
||||
.filter-picker {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
description: "",
|
||||
};
|
||||
|
||||
let show = false;
|
||||
let show = true;
|
||||
|
||||
function remove() {
|
||||
removeNode(filter.id);
|
||||
|
@ -27,12 +27,6 @@
|
|||
<div class="filter-holder">
|
||||
<div class="head">
|
||||
<div class="name"><h3>{filter.name}</h3></div>
|
||||
<div>
|
||||
{#if filter.params && filter.params.length > 0}
|
||||
<button on:click={() => (show = !show)}>{show ? "Hide" : "Show"} Options</button>
|
||||
{/if}
|
||||
<button on:click={remove}>X</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
|
@ -93,6 +87,8 @@
|
|||
/* border: 1px solid #999; */
|
||||
border: 1px solid var(--b1);
|
||||
/* box-shadow: 5px 5px 0px #000; */
|
||||
overflow-y: scroll;
|
||||
flex: 1;
|
||||
}
|
||||
.filter-holder,
|
||||
input,
|
||||
|
@ -123,6 +119,10 @@
|
|||
}
|
||||
.p-value {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
input[type="range"] {
|
||||
margin-right: 5px;
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<div class="holder">
|
||||
<div class="search">
|
||||
<input placeholder="Search Filters" on:keyup={update} bind:value={q} type="text" />
|
||||
<input placeholder="Search Filters" on:keyup={update} bind:value={q} type="text" /><button on:click={() => {reset(); update();}}>X</button>
|
||||
<select on:change={reset} bind:value={select}>
|
||||
<option value="video">Video Filters</option>
|
||||
<option value="audio">Audio Filters</option>
|
||||
|
@ -73,10 +73,14 @@
|
|||
}
|
||||
.search {
|
||||
display: flex;
|
||||
justify-items: stretch;
|
||||
justify-content: stretch;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
button {
|
||||
margin-left: 1px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.type {
|
||||
|
|
|
@ -9,4 +9,4 @@
|
|||
if ($auto) fitView();
|
||||
});
|
||||
</script>
|
||||
<button>Fit</button>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { nodes, edges, auto } from "./stores.js";
|
||||
import { addNode, nodes, edges, auto, selectedFilter } from "./stores.js";
|
||||
import {
|
||||
SvelteFlowProvider,
|
||||
SvelteFlow,
|
||||
|
@ -14,36 +14,63 @@
|
|||
ffmpeg: Node,
|
||||
};
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
deletable: true,
|
||||
};
|
||||
|
||||
function onEdgeUpdate(e) {
|
||||
console.log(e);
|
||||
function onClick(e) {
|
||||
if (e.detail.nodeType === "filter") {
|
||||
const newSelected = $nodes.findIndex((n) => n.id === e.detail.id);
|
||||
if (newSelected > -1) {
|
||||
$selectedFilter = newSelected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onEdgeUpdateStart(e) {
|
||||
console.log(e);
|
||||
function addInput() {
|
||||
addNode({ name: "shoe.mp4" }, "input");
|
||||
}
|
||||
|
||||
function onEdgeUpdateEnd(e) {
|
||||
console.log(e);
|
||||
}
|
||||
function onMoveStart(e) {
|
||||
console.log(e);
|
||||
}
|
||||
function onConnect(e) {
|
||||
console.log("connect", e);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<SvelteFlowProvider>
|
||||
<div class="holder">
|
||||
<FitComp />
|
||||
<label for="auto"><input id="auto" type="checkbox" bind:checked={$auto} />Automatic Layout</label>
|
||||
<div style="width: 900px; height: 500px;">
|
||||
<SvelteFlow {nodeTypes} {nodes} {edges} snapGrid={[10, 10]} fitView>
|
||||
<div class="nav">
|
||||
<button on:click={addInput}>Add Input</button>
|
||||
<label for="auto"
|
||||
><input id="auto" type="checkbox" bind:checked={$auto} />Automatic Layout</label
|
||||
>
|
||||
</div>
|
||||
<div class="flow">
|
||||
<div style="height: 100%; width: 100%">
|
||||
<SvelteFlow
|
||||
nodesConnectable={!auto}
|
||||
panOnDrag={!auto}
|
||||
edgesUpdatable={!auto}
|
||||
connectOnClick={true}
|
||||
nodesFocusable={!auto}
|
||||
edgesFocusable={!auto}
|
||||
on:nodeclick={onClick}
|
||||
{nodeTypes}
|
||||
{nodes}
|
||||
{edges}
|
||||
snapGrid={[10, 10]}
|
||||
fitView
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
</SvelteFlow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SvelteFlowProvider>
|
||||
|
||||
<style>
|
||||
.holder {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
.flow {
|
||||
flex: 1;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
13
src/app.css
13
src/app.css
|
@ -24,6 +24,11 @@ body {
|
|||
font:
|
||||
16px Times,
|
||||
serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
textrea,
|
||||
select,
|
||||
|
@ -32,7 +37,9 @@ button {
|
|||
font: inherit;
|
||||
}
|
||||
|
||||
button, input:not([type="range"]) , select {
|
||||
button,
|
||||
input:not([type="range"]),
|
||||
select {
|
||||
border: 1px solid var(--b1);
|
||||
background-color: white;
|
||||
box-shadow: 2px 2px 0px var(--b2);
|
||||
|
@ -43,9 +50,7 @@ button:active {
|
|||
left: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
|
|
|
@ -1,28 +1,96 @@
|
|||
<script lang="ts">
|
||||
import { Handle, Position } from "@xyflow/svelte";
|
||||
import { removeNode } from "../stores.js";
|
||||
|
||||
export let data = { name: "", inputs: [], outputs: [], onChange: () => {} };
|
||||
export let data = { nodeType: "", name: "", inputs: [], outputs: [] };
|
||||
export let id;
|
||||
|
||||
function remove() {
|
||||
removeNode(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="node">
|
||||
<div class="node {data.nodeType}">
|
||||
<div class="head">
|
||||
<div class="node-type">{data.nodeType}</div>
|
||||
<button on:click={remove}>X</button>
|
||||
</div>
|
||||
<div class="body">
|
||||
{#if data.nodeType == "input"}
|
||||
<select bind:value={data.name}>
|
||||
<option value="punch.mp4">punch.mp4</option>
|
||||
<option value="shoe.mp4">shoe.mp4</option>
|
||||
</select>
|
||||
{:else}
|
||||
{data.name}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#each data.inputs as inp, index}
|
||||
<Handle type="target" position={Position.Left} id={inp + "_" + index} style="top: {index*10}px; background-color: {inp == 'v' ? 'blue' : 'red'}" />
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={inp + "_" + index}
|
||||
class="handle {inp}"
|
||||
style="top: {index * 12 + 4}px; left: -7px;">{inp}</Handle
|
||||
>
|
||||
{/each}
|
||||
{#each data.outputs as out, index}
|
||||
<Handle type="source" id={out + "_" + index} position={Position.Right} style="top: {index*10}px; background-color: {out == 'v' ? 'blue' : 'red'}" />
|
||||
<Handle
|
||||
type="source"
|
||||
id={out + "_" + index}
|
||||
position={Position.Right}
|
||||
class="handle {out}"
|
||||
style="top: {index * 12 + 4}px; left: 107%;">{out}</Handle
|
||||
>
|
||||
{/each}
|
||||
<!-- <Handle type="source" position={Position.Right} id="a" style="top: 10px;" on:connect /> -->
|
||||
<!-- <Handle -->
|
||||
<!-- type="source" -->
|
||||
<!-- position={Position.Right} -->
|
||||
<!-- id="b" -->
|
||||
<!-- style="top: auto; bottom: 10px;" -->
|
||||
<!-- on:connect -->
|
||||
<!-- /> -->
|
||||
|
||||
<style>
|
||||
:global(:root) {
|
||||
--edge-color: var(--b1) !important;
|
||||
--edge-color-selected: black;
|
||||
}
|
||||
:global(.svelte-flow__node) {
|
||||
box-shadow: 2px 2px 0px var(--b2);
|
||||
}
|
||||
:global(.svelte-flow__node.selected) {
|
||||
outline: 1px solid var(--b1) !important;
|
||||
}
|
||||
:global(.handle) {
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
border: 1px solid var(--b1) !important;
|
||||
border-radius: 0px !important;
|
||||
background-color: white !important;
|
||||
font-size: 10px;
|
||||
line-height: 6px;
|
||||
}
|
||||
:global(.handle.a) {
|
||||
/* border: 1px solid var(--b2) !important; */
|
||||
}
|
||||
.node {
|
||||
padding: 5px;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.head button {
|
||||
font-size: 10px;
|
||||
line-height: 8px;
|
||||
padding: 2px 2px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.node-type {
|
||||
font-size: 0.8em;
|
||||
color: #999;
|
||||
}
|
||||
.node.input {
|
||||
}
|
||||
.node.filter {
|
||||
}
|
||||
.node.output {
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { writable, derived, get } from "svelte/store";
|
|||
export const nodes = writable([]);
|
||||
export const edges = writable([]);
|
||||
export const auto = writable(true);
|
||||
export const selectedFilter = writable();
|
||||
|
||||
const PREFIX = "";
|
||||
|
||||
|
@ -146,6 +147,8 @@ export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => {
|
|||
const source = $nodes.find((n) => n.id === e.source);
|
||||
const target = $nodes.find((n) => n.id === e.target);
|
||||
|
||||
if (source && target) {
|
||||
|
||||
if (source.nodeType === "input") {
|
||||
if (e.sourceHandle.includes("v")) {
|
||||
edgeIds[e.id] = inputIds[source.id] + ":v";
|
||||
|
@ -160,6 +163,7 @@ export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => {
|
|||
edgeIds[e.id] = outType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let n of $nodes.filter((n) => n.nodeType == "filter")) {
|
||||
let cmd = "";
|
||||
|
@ -330,6 +334,7 @@ export function addNode(data, type) {
|
|||
}
|
||||
}
|
||||
|
||||
data.nodeType = type;
|
||||
data.inputs = ins;
|
||||
data.outputs = outs;
|
||||
|
||||
|
@ -347,9 +352,9 @@ export function addNode(data, type) {
|
|||
const isAuto = get(auto);
|
||||
|
||||
if (isAuto) {
|
||||
const w = 100;
|
||||
const w = 120;
|
||||
const h = 50;
|
||||
const margin = 10;
|
||||
const margin = 50;
|
||||
let prev = null;
|
||||
|
||||
for (let n of _nodes) {
|
||||
|
@ -362,7 +367,7 @@ export function addNode(data, type) {
|
|||
for (let n of _nodes) {
|
||||
if (n.nodeType === "filter") {
|
||||
let _w = prev && prev.width ? prev.width : w;
|
||||
n.position = { x: prev ? prev.position.x + _w + margin : 0, y: -30 };
|
||||
n.position = { x: prev ? prev.position.x + _w + margin : 0, y: -50 };
|
||||
prev = n;
|
||||
}
|
||||
}
|
||||
|
@ -374,8 +379,13 @@ export function addNode(data, type) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (node.nodeType === "filter") {
|
||||
selectedFilter.set(_nodes.length - 1);
|
||||
}
|
||||
return _nodes;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
export function removeNode(id) {
|
||||
|
|
Loading…
Reference in New Issue