Compare commits

48 Commits

Author SHA1 Message Date
STEINNI e7b88726ec cnxId & [CUID] 2026-06-27 17:28:00 +00:00
STEINNI cf292031fd threetobus now uses [UID] template, MessageBus upgraded to cnxId 2026-06-26 21:56:19 +00:00
STEINNI aefa5ab147 no local cursor rules 2026-06-26 15:39:02 +00:00
STEINNI e2f8766b9f working on simManage 2026-06-21 21:09:21 +00:00
STEINNI 06a7868882 better default agent props & speed & position numeric, not strings 2026-06-21 12:08:25 +00:00
STEINNI 54db203e86 untested collision/near miss detection, with prisms auto-refresh 2026-06-07 10:44:28 +00:00
STEINNI d172e06611 sim creation wired to DB with ownership 2026-06-05 20:27:57 +00:00
STEINNI b122cf153e centered windozzz 2026-06-05 16:23:40 +00:00
STEINNI 5207a3b18e started sims section & axes&grid settings for KFEditor 2026-06-05 15:45:27 +00:00
STEINNI ce9e73ac41 updateAgent API & snippet in KF console 2026-06-05 09:09:53 +00:00
STEINNI eb7312c7a8 tuned confirm boxes 2026-06-05 08:57:33 +00:00
STEINNI d301b78ea7 KF Editor => Reset KF button 2026-06-05 08:33:06 +00:00
STEINNI ea9b3d06cf graflow examples cleanup 2026-03-08 14:24:03 +00:00
STEINNI 4b9207546c Add buildoz as submodule 2026-03-08 13:46:42 +00:00
STEINNI 662e694f21 remove embedded buildoz 2026-03-08 13:44:15 +00:00
STEINNI cd2e74e6fd graflow: examples tuning, nodemove debugging 2026-03-08 13:34:10 +00:00
STEINNI a967576d4b graflow: autofit OK 2026-03-07 18:36:01 +00:00
STEINNI 61855a3416 graflow: improved subflow zoom-in 2026-03-07 15:15:07 +00:00
STEINNI 8421dd12b6 graflow: improved parent align: correction on parents if fakenodes 2026-03-07 14:22:45 +00:00
STEINNI 124bae719b graflow: align parent 2026-03-06 20:14:35 +00:00
STEINNI 24aad6bbf6 graflow: improved ortho walkarounds 2026-03-06 19:11:53 +00:00
STEINNI 2d87523020 graflow: improving ortho 2026-03-04 20:48:50 +00:00
STEINNI f81ae0611c graflow: test for all directions 2026-03-02 21:27:33 +00:00
STEINNI 30a95ca009 graflow: refacto & facto of buildSegment 2026-03-02 20:45:55 +00:00
STEINNI bc6a02fe10 graflow: small fix for loop always bezier 2026-03-02 20:37:58 +00:00
STEINNI e46ff1a817 graflow: autoplace attributes & better ICMP example 2026-03-02 19:07:06 +00:00
STEINNI f5fbdd6ccd graflow: fix1 to ortho wires 2026-03-02 18:27:15 +00:00
STEINNI be72b6f2db graflow: wireType straight 2026-03-01 13:27:00 +00:00
STEINNI 60db0393cc graflow: editable=>moving nodes 2026-02-28 22:18:14 +00:00
STEINNI a156db845a graflow split tests + fixed height bug + fixed subflow reenter loses style 2026-02-28 20:55:44 +00:00
STEINNI 8be5751107 graflow: source grooming + demo panels subflow compatible 2026-02-28 11:14:04 +00:00
STEINNI 3e0faa4866 graflow: replaced fixed by absolute in subflows 2026-02-28 10:57:06 +00:00
STEINNI 34ce9049c6 graflow: several bigfixes, for subflows, longlinks-fakenodes cleanup, multipass layerordering 2026-02-27 21:42:02 +00:00
STEINNI 6bc3c1e9b8 graflow: subflow polishing 2026-02-25 20:25:53 +00:00
STEINNI 0cf67a5c9f graflow: also embed button icons 2026-02-25 18:26:28 +00:00
STEINNI 0b4b7e2a2f graflow: subflow reference nodes & exit subflow 2026-02-25 18:25:33 +00:00
STEINNI e5bd91cd9f graflow: entering subflow ok 2026-02-22 20:59:58 +00:00
STEINNI e647bf01de graflow: entering subflow ok 2026-02-22 20:58:35 +00:00
STEINNI 46f08cfdce graflow: isolated attribute 2026-02-22 14:43:46 +00:00
STEINNI 91274c4542 graflow: added autoplace align 1st & last 2026-02-21 22:24:10 +00:00
STEINNI d9bdc34210 nicsys license 2026-02-21 22:08:59 +00:00
STEINNI 3c93683ca1 graflow: added icmp example & fixed grand-slam long-links 2026-02-21 21:32:53 +00:00
STEINNI 0d5107f09a graflow: cleanup minTension VS tension 2026-02-08 12:49:13 +00:00
STEINNI db45bcb4c5 graflow: tangents of go-around finally OK 2026-02-08 12:42:46 +00:00
STEINNI cf24657f9f graflow started zomminout 2026-01-26 08:03:10 +00:00
STEINNI 4bb306e8a1 merged back lonkwires 2026-01-24 18:26:04 +00:00
STEINNI bc80b3d4d6 cherrypicked subflow example 2026-01-21 17:16:43 +00:00
STEINNI aadba6b3e6 clean-without longbranches 2026-01-21 14:45:10 +00:00
59 changed files with 6810 additions and 2782 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "app/thirdparty/buildoz"]
path = app/thirdparty/buildoz
url = https://gitea.internike.com/nike/buildoz.git
-72
View File
@@ -1,72 +0,0 @@
<style>
.bzgf-node {
font-family: Arial, Helvetica, sans-serif;
width: 160px;
height: 80px;
color: black;
text-align: center;
position: absolute;
padding: .5em;
}
.bzgf-node .body{
z-index: 1;
position: absolute;
inset: 0;
background: var(--eicui-base-color-grey-25);
border:2px solid var(--eicui-base-color-grey-50);
border-radius: 6px;
}
.bzgf-node .title {
font-weight: bold;
color: var(--app-color-black);
margin: .5em auto .2em auto;
}
.bzgf-node .subtitle {
font-size: .9em;
color: var(--eicui-base-color-primary-100);
width: 90%;
margin: auto;
}
.bzgf-node .port{
position: absolute;
height: 10px;
width: 10px;
background: var(--eicui-base-color-info-25);
border-radius: 10px;
}
.bzgf-node [data-direction="n"]{ top: -4px; left: 50%; transform: translateX(-50%);}
.bzgf-node [data-direction="s"]{ bottom: -4px; left: 50%; transform: translateX(-50%);}
.bzgf-node [data-direction="w"]{ left: -4px; top: 50%; transform: translateY(-50%);}
.bzgf-node [data-direction="e"]{ right: -4px; top: 50%; transform: translateY(-50%);}
.bzgf-node [data-id="in2"]{ top: 25%; }
.bzgf-node [data-id="in3"]{ top: 75%; }
.bzgf-node [data-id="out2"]{ top: 25%; }
.bzgf-node [data-id="out3"]{ top: 75%; }
.bzgf-wire{ stroke: var(--eicui-base-color-info); stroke-width: 4px; stroke-dasharray: 10,5; }
</style>
<template>
<div class="bzgf-node" data-nodetype="eicBasic">
<div class="body">
<div class="title">{title}</div>
<div class="subtitle">{subtitle}</div>
</div>
<div class="port" data-type="in" data-id="in1" data-direction="w"></div>
<div class="port" data-type="in" data-id="in2" data-direction="w"></div>
<div class="port" data-type="in" data-id="in3" data-direction="w"></div>
<div class="port" data-type="out" data-id="out1" data-direction="e"></div>
<div class="port" data-type="out" data-id="out2" data-direction="e"></div>
<div class="port" data-type="out" data-id="out3" data-direction="e"></div>
</div>
</template>
-173
View File
@@ -1,173 +0,0 @@
<style>
.bzgf-node {
font-family: Arial, Helvetica, sans-serif;
border-width:2px;
border-style: solid;
border-radius: 6px;
width: 160px;
height: 80px;
color: black;
text-align: center;
position: absolute;
padding: .5em;
}
.bzgf-node .title {
font-weight: bold;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2em;
color: white;
}
.bzgf-node .body { margin-top: 2em; }
.bzgf-node .body input {
width:4em;
font-size: .9em;
border-radius: 6px;
border: 1px solid #CCC;
}
.bzgf-node .body div{ font-size: 12px; text-align: left; line-height: 24px; }
.bzgf-node .port{
position: absolute;
height: 10px;
width: 10px;
border: 1px solid black;
border-radius: 10px;
}
.bzgf-node .port[data-type="in"] { background: #0F0; }
.bzgf-node .port[data-type="out"] { background: #FF0; }
.bzgf-node [data-direction="w"]{ left: -7px; top: calc(50% + 1em); transform: translateY(-50%);}
.bzgf-node [data-direction="e"]{ right: -7px; top: calc(50% + 1em); transform: translateY(-50%);}
.bzgf-node [data-direction="n"]{ top: -7px; left: 50%; transform: translateX(-50%);}
.bzgf-node [data-direction="s"]{ bottom: -7px; left: 50%; transform: translateX(-50%);}
.bzgf-node[data-nodetype="inc"]{
background: #DFD;
border-color: #090;
}
.bzgf-node[data-nodetype="inc"] .title{ background: #090; }
.bzgf-node[data-nodetype="wadder"]{
background: #DFD;
border-color: #090;
height:150px
}
.bzgf-node[data-nodetype="wadder"] .body{ display: grid; grid-gap: 4px; margin-left:0.5em; grid-template-columns: 1fr 1fr; align-items: center; }
.bzgf-node[data-nodetype="wadder"] .title{ background: #090; }
.bzgf-node[data-nodetype="wadder"] .port[data-id="inp1"] { top:51px; }
.bzgf-node[data-nodetype="wadder"] .port[data-id="inp2"] { top:75px; }
.bzgf-node[data-nodetype="wadder"] .port[data-id="inp3"] { top:99px; }
.bzgf-node[data-nodetype="wadder"] .port[data-id="inp4"] { top:123px; }
.bzgf-node[data-nodetype="wadder"] .port[data-id="inp5"] { top:147px; }
.bzgf-node[data-nodetype="factor"]{
background: #DDF;
border-color: #009;
}
.bzgf-node[data-nodetype="factor"] .title{ background: #009; }
.bzgf-node[data-nodetype="multiplier"]{
background: #DDF;
border-color: #009;
height:110px
}
.bzgf-node[data-nodetype="multiplier"] .body{
font-size:40px;
font-weight: bold;
align-items: center;
display: flex;
justify-content: center;
margin-top: 1em;
}
.bzgf-node[data-nodetype="multiplier"] .title{ background: #009; }
.bzgf-node[data-nodetype="multiplier"] .port[data-id="inp1"] { top:37px; }
.bzgf-node[data-nodetype="multiplier"] .port[data-id="inp2"] { top:63px; }
.bzgf-node[data-nodetype="multiplier"] .port[data-id="inp3"] { top:89px; }
.bzgf-node[data-nodetype="input"],
.bzgf-node[data-nodetype="console"]{
background: #CCC;
border-color: #555;
}
.bzgf-node[data-nodetype="input"] .title,
.bzgf-node[data-nodetype="console"] .title{ background: #555; }
.bzgf-wire{ stroke: #0AF; stroke-width: 2; }
</style>
<template>
<div class="bzgf-node" data-nodetype="inc">
<div class="title">Fixed Increment</div>
<div class="port" data-type="in" data-id="inp1" data-direction="w"></div>
<div class="port" data-type="out" data-id="out1" data-direction="e"></div>
<div class="body">
<label>Increment:</label><input name="incvalue" type="text" value="1">
</div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="factor">
<div class="title">Fixed factor</div>
<div class="port" data-type="in" data-id="inp1" data-direction="w"></div>
<div class="port" data-type="out" data-id="out1" data-direction="e"></div>
<div class="body">
<label>Factor:</label><input name="factvalue" type="text" value="0.5">
</div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="wadder">
<div class="title">Adder</div>
<div class="port" data-type="in" data-id="inp1" data-direction="w"></div>
<div class="port" data-type="in" data-id="inp2" data-direction="w"></div>
<div class="port" data-type="in" data-id="inp3" data-direction="w"></div>
<div class="port" data-type="in" data-id="inp4" data-direction="w"></div>
<div class="port" data-type="in" data-id="inp5" data-direction="w"></div>
<div class="port" data-type="out" data-id="out1" data-direction="e"></div>
<div class="body">
<div>
<div>x <input type="text" name="weight1" value="1"></div>
<div>x <input type="text" name="weight2" value="1"></div>
<div>x <input type="text" name="weight3" value="1"></div>
<div>x <input type="text" name="weight4" value="1"></div>
<div>x <input type="text" name="weight5" value="1"></div>
</div>
<div style="font-size:40px;font-weight: bold;transform: translateY(-50%);">&sum;</div>
</div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="multiplier">
<div class="title">Multiply</div>
<div class="port" data-type="in" data-id="inp1" data-direction="w"></div>
<div class="port" data-type="in" data-id="inp2" data-direction="w"></div>
<div class="port" data-type="in" data-id="inp3" data-direction="w"></div>
<div class="port" data-type="out" data-id="out1" data-direction="e"></div>
<div class="body">x</div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="console">
<div class="title">Console</div>
<div class="port" data-type="in" data-id="inp1" data-direction="w"></div>
<div class="body"></div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="input">
<div class="title">input</div>
<div class="port" data-type="out" data-id="out1" data-direction="e"></div>
<div class="body">
<label>Value:</label><input name="value" type="text" value="1">
</div>
</div>
</template>
-237
View File
@@ -1,237 +0,0 @@
<style>
.bzgf-node {
font-family: Arial, Helvetica, sans-serif;
border-width:2px;
border-style: solid;
border-radius: 6px;
width: 160px;
height: 80px;
color: black;
text-align: center;
position: absolute;
padding: .5em;
background: black;
}
.bzgf-node .text {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
width: 100%;
height: 2em;
color: white;
transform: translateX(-50%), translateY(-50%);
}
.bzgf-node .port{
position: absolute;
height: 6px;
width: 6px;
background: #FFF;
z-index: 99;
opacity: 0.3;
}
.bzgf-node [data-direction="w"]{ left: -7px; top: 50%; transform: translateY(-50%);}
.bzgf-node [data-direction="e"]{ right: -7px; top: 50%; transform: translateY(-50%);}
.bzgf-node [data-direction="n"]{ top: -7px; left: 50%; transform: translateX(-50%);}
.bzgf-node [data-direction="s"]{ bottom: -7px; left: 50%; transform: translateX(-50%);}
.bzgf-node[data-nodetype="start"],
.bzgf-node[data-nodetype="end"]{
border: 1px solid white;
width: 100px;
height: 30px;
border-radius: 50%;
}
.bzgf-node[data-nodetype="start"] .text,
.bzgf-node[data-nodetype="end"] .text{ width: 80%; }
.bzgf-node[data-nodetype="condition"]{
border: none;
width: 100px;
height: 100px;
}
.bzgf-node[data-nodetype="condition"] .frame{
border: 1px solid white;
transform: rotate(-45deg) scale(0.7071);
position: absolute;
inset: 0;
}
.bzgf-node[data-nodetype="condition"] .port[data-direction="w"]{ left: -5px; }
.bzgf-node[data-nodetype="condition"] [data-direction="e"]{ right: -5px; }
.bzgf-node[data-nodetype="condition"] [data-direction="n"]{ top: -5px; }
.bzgf-node[data-nodetype="condition"] [data-direction="s"]{ bottom: -5px; }
.bzgf-node[data-nodetype="process"]{
border: 1px solid white;
width: 150px;
height: 70px;
padding: 0;
}
.bzgf-node[data-nodetype="process"] .text{ width: 80%; }
.bzgf-node[data-nodetype="process"] .dbline{
border: 1px solid white;
width: 75%;
height: 100%;
position: absolute;
top: -1px;
left: 10%;
}
.bzgf-node[data-nodetype="database"]{
border: 1px solid white;
width: 100px;
height: 100px;
isolation: isolate;
}
.bzgf-node[data-nodetype="database"] .top{
z-index: 2;
height: 20%;
border: 1px solid white;
border-radius: 50%;
position: absolute;
top: -10%;
left: 0;
width: 100%;
background: black;
}
.bzgf-node[data-nodetype="database"] .text{
background: black;
width: 87%;
z-index: 1;
}
.bzgf-node[data-nodetype="database"] .bottom{
z-index: 0;
height: 20%;
border: 1px solid white;
border-radius: 50%;
position: absolute;
bottom: -10%;
left: 0;
width: 99%;
background: black;
clip-path: polygon( 0% 110%, 100% 110%, 100% 50%, 0% 50%);
}
.bzgf-node[data-nodetype="database"] [data-direction="n"]{ top: -17px; }
.bzgf-node[data-nodetype="database"] [data-direction="s"]{ bottom: -17px; }
.bzgf-node[data-nodetype="preparation"]{
width: 150px;
height: 80px;
padding: 0;
border: none;
}
.bzgf-node[data-nodetype="preparation"] .text{ width: 85%; }
.bzgf-node[data-nodetype="preparation"] .outerframe{
width: 100%;
height: 100%;
position: absolute;
background: white;
border: none;
padding: 0;
place-items: center;
clip-path: polygon(
10% 0%,
90% 0%,
100% 50%,
90% 100%,
10% 100%,
0% 50%
);
}
.bzgf-node[data-nodetype="preparation"] .innerframe{
width: calc(100% - 2px);
height: calc(100% - 2px);
position: absolute;
top: 1px;
left: 1px;
background: black;
clip-path: inherit;
display: grid;
place-items: center;
color: white;
}
.bzgf-wire{ stroke: #FF0; stroke-width: 2; }
</style>
<svg style="display:none" aria-hidden="true">
<template id="svg-arrows">
<defs>
<marker id="arrow"
viewBox="0 0 15 15"
refX="15"
refY="7"
markerWidth="15"
markerHeight="15"
orient="auto-start-reverse"
markerUnits="userSpaceOnUse">
<path d="M0,0 L15,7 L0,15 Z" fill="#FF0"/>
</marker>
</defs>
</template>
</svg>
<template>
<div class="bzgf-node" data-nodetype="start">
<div class="text">{text}</div>
<div class="port" data-id="out1" data-direction="s"></div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="end">
<div class="text">{text}</div>
<div class="port" data-id="inp1" data-direction="n"></div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="condition">
<div class="frame"></div>
<div class="text">{text}</div>
<div class="port" data-id="inp1" data-direction="n"></div>
<div class="port" data-id="out1" data-direction="w"></div>
<div class="port" data-id="out2" data-direction="s"></div>
<div class="port" data-id="out3" data-direction="e"></div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="process">
<div class="dbline"></div>
<div class="text">{text}</div>
<div class="port" data-id="inout1" data-direction="n"></div>
<div class="port" data-id="inout2" data-direction="s"></div>
<div class="port" data-id="inout3" data-direction="w"></div>
<div class="port" data-id="inout4" data-direction="e"></div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="database">
<div class="top"></div>
<div class="bottom"></div>
<div class="text">{text}</div>
<div class="port" data-id="inout1" data-direction="n"></div>
<div class="port" data-id="inout2" data-direction="s"></div>
<div class="port" data-id="inout3" data-direction="w"></div>
<div class="port" data-id="inout4" data-direction="e"></div>
</div>
</template>
<template>
<div class="bzgf-node" data-nodetype="preparation">
<div class="outerframe">
<div class="innerframe">
<div class="text">{text}</div>
</div>
</div>
<div class="port" data-id="inp1" data-direction="n"></div>
<div class="port" data-id="out1" data-direction="s"></div>
<div class="port" data-id="inout1" data-direction="w"></div>
<div class="port" data-id="inout2" data-direction="e"></div>
</div>
</template>
+1 -1
View File
@@ -44,7 +44,7 @@ log(agtIds)
await removeAgent('00000000-aaaa-bbbb-cccc-dddddddddddd') await removeAgent('00000000-aaaa-bbbb-cccc-dddddddddddd')
</div> </div>
<div class="snippet" data-snippet="updateAgent" style="display:none;"> <div class="snippet" data-snippet="updateAgent" style="display:none;">
await updateAgent('00000000-aaaa-bbbb-cccc-dddddddddddd', { position: { x: 5, y: 10, z: 0 } })
</div> </div>
<div class="snippet" data-snippet="selectAgent" style="display:none;"> <div class="snippet" data-snippet="selectAgent" style="display:none;">
await selectAgent('00000000-aaaa-bbbb-cccc-dddddddddddd') await selectAgent('00000000-aaaa-bbbb-cccc-dddddddddddd')
-108
View File
@@ -1,108 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>graflow</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="/app/thirdparty/eicui/eicui-2.0.css">
<link type="text/css" rel="stylesheet" href="../../thirdparty/buildoz/buildoz.css">
<script src="../../thirdparty/buildoz/buildoz.js"></script>
<script src="../../thirdparty/buildoz/bzGraflow.js"></script>
<style>
body{
display: grid;
grid-gap: 5px;
background:#888;
font-size: 16px;
}
.demooptions{
padding: 2px;
position: absolute;
top: 2px;
left: 2px;
width: 10em;
background: #FFFB;
border-radius: 5px;
text-align: center;
z-index:99;
font-size: .7em;
border: 1px solid #999;
}
.demooptions button{
text-transform: none;
margin: 2px;
font-size: 1em;
}
bz-graflow{
overflow: scroll;
}
bz-graflow.compunet{ grid-column: 1 / -1; width: 80vw; height: 40vh; background:black; }
bz-graflow.eic{ grid-column: 1 / -1; width: 80vw; height: 30vh; background: var(--eicui-base-color-grey-10); }
bz-graflow.organi{ width: 40vw; height: 100vh; background:black; }
</style>
<script>
window.addEventListener('load',()=>{
const grflw1 = document.querySelector('bz-graflow.compunet')
document.querySelector('[data-trigger="onAutoplace1H"]').addEventListener('click',
(evt) => { grflw1.autoPlace('horizontal', 80, 30, 1000) }
)
document.querySelector('[data-trigger="onAutoplace1V"]').addEventListener('click',
(evt) => { grflw1.autoPlace('vertical') }
)
const grflw2 = document.querySelector('bz-graflow.eic')
document.querySelector('[data-trigger="onAutoplace2H"]').addEventListener('click',
(evt) => { grflw2.autoPlace('horizontal', 80, 80, 1000) }
)
document.querySelector('[data-trigger="onAutoplace2V"]').addEventListener('click',
(evt) => { grflw2.autoPlace('vertical', 80, 80, 1000) }
)
const grflw3 = document.querySelector('bz-graflow.organi')
document.querySelector('[data-trigger="onAutoplace3H"]').addEventListener('click',
(evt) => { grflw3.autoPlace('horizontal', 80, 80, 1000) }
)
document.querySelector('[data-trigger="onAutoplace3V"]').addEventListener('click',
(evt) => { grflw3.autoPlace('vertical', 80, 50, 1000) }
)
document.querySelector('[data-id="compunet"]').addEventListener('change',
(evt) => { grflw1.setAttribute('tension', evt.target.value); grflw1.refresh() }
)
document.querySelector('[data-id="eic"]').addEventListener('change',
(evt) => { grflw2.setAttribute('tension', evt.target.value); grflw2.refresh() }
)
document.querySelector('[data-id="organi"]').addEventListener('change',
(evt) => { grflw3.setAttribute('tension', evt.target.value); grflw3.refresh() }
)
})
</script>
</head>
<body>
<bz-graflow class="compunet" flow="/app/assets/json/bzGraflow/testFlow1.json" tension="60">
<div class="demooptions"> <!-- just for demo purposes -->
<button data-trigger="onAutoplace1H">Auto-place Horizontal</button>
<button data-trigger="onAutoplace1V">Auto-place Vertical</button>
<div class-"cols-2"=""><label>tension</label><input data-id="compunet" type="number" size="2" value="60"></div>
</div>
</bz-graflow>
<bz-graflow class="eic" flow="/app/assets/json/bzGraflow/testFlowEic.json" tension="60">
<div class="demooptions"> <!-- just for demo purposes -->
<button data-trigger="onAutoplace2H">Auto-place Horizontal</button>
<button data-trigger="onAutoplace2V">Auto-place Vertical</button>
<div class-"cols-2"=""><label>tension</label><input data-id="eic" type="number" size="2" value="60"></div>
</div>
</bz-graflow>
<bz-graflow class="organi" flow="/app/assets/json/bzGraflow/testFlow2.json" tension="60">
<div class="demooptions">
<button data-trigger="onAutoplace3H">Auto-place Horizontal</button>
<button data-trigger="onAutoplace3V">Auto-place Vertical</button>
<div class-"cols-2"=""><label>tension</label><input data-id="organi" type="number" size="2" value="60"></div>
</div>
</bz-graflow>
</body>
</html>
+1 -1
View File
@@ -4,4 +4,4 @@
"y": 100, "y": 100,
"z": 100 "z": 100
} }
} }
-55
View File
@@ -1,55 +0,0 @@
{
"nodesFile": "/app/assets/html/bzGraflow/nodesTest1.html",
"flow": {
"nodes":[
{ "nodeType": "inc",
"id": "aze",
"coords": { "x": 220, "y": 120}
},
{ "nodeType": "inc",
"subflow": "/app/assets/json/bzGraflow/testSubFlow1.json",
"portLinks": [
{ "parentPort": ["in1"], "subflowPort": ["inp1"] },
{ "parentPort": ["out1"], "subflowPort": ["out1"] }
],
"id": "aze2",
"coords": { "x": 220, "y": 10}
},
{ "nodeType": "factor",
"id": "qsd",
"coords": { "x": 470, "y": 170}
},
{ "nodeType": "factor",
"id": "qsd2",
"coords": { "x": 470, "y": 50}
},
{ "nodeType": "wadder",
"id": "wcx",
"coords": { "x": 720, "y": 50}
},
{ "nodeType": "multiplier",
"id": "ert",
"coords": { "x": 550, "y": 350}
},
{ "nodeType": "input",
"id": "0000",
"coords": { "x": 20, "y": 350}
},
{ "nodeType": "console",
"id": "9999",
"coords": { "x": 800, "y": 350}
}
],
"links": [
{ "from": ["0000", "out1"], "to": ["aze", "inp1"] },
{ "from": ["aze2", "out1"], "to": ["qsd2", "inp1"] },
{ "from": ["aze", "out1"], "to": ["qsd", "inp1"] },
{ "from": ["qsd2", "out1"], "to": ["ert", "inp2"] },
{ "from": ["0000", "out1"], "to": ["aze2", "inp1"] },
{ "from": ["qsd2", "out1"], "to": ["wcx", "inp2"] },
{ "from": ["wcx", "out1"], "to": ["ert", "inp1"] },
{ "from": ["qsd", "out1"], "to": ["wcx", "inp1"] },
{ "from": ["ert", "out1"], "to": ["9999", "inp1"] }
]
}
}
-46
View File
@@ -1,46 +0,0 @@
{
"nodesFile": "/app/assets/html/bzGraflow/nodesTest2.html",
"flow": {
"nodes":[
{ "nodeType": "start",
"id": "aze",
"coords": { "x": 220, "y": 20},
"markup": { "text": "Start" }
},
{ "nodeType": "process",
"id": "aze2",
"coords": { "x": 220, "y": 120},
"markup": { "text": "x = alph - 1" }
},
{ "nodeType": "condition",
"id": "qsd",
"coords": { "x": 250, "y": 270},
"markup": { "text": "x > 0" }
},
{ "nodeType": "preparation",
"id": "qsd2",
"coords": { "x": 250, "y": 470},
"markup": { "text": "prepare SQL" }
},
{ "nodeType": "database",
"id": "wcx",
"coords": { "x": 500, "y": 450},
"markup": { "text": "MySQL<br>Store" }
},
{ "nodeType": "end",
"id": "ert",
"coords": { "x": 250, "y": 650},
"markup": { "text": "End" }
}
],
"links": [
{ "from": ["aze", "out1"], "to": ["aze2", "inout1"], "endArrow":true },
{ "from": ["aze2", "inout2"], "to": ["qsd", "inp1"], "endArrow":true },
{ "from": ["qsd", "out1"], "to": ["aze2", "inout3"], "endArrow":true },
{ "from": ["qsd", "out2"], "to": ["qsd2", "inp1"], "endArrow":true },
{ "from": ["qsd2", "inout2"], "to": ["wcx", "inout3"], "startArrow":true , "endArrow":true },
{ "from": ["qsd2", "out1"], "to": ["ert", "inp1"], "endArrow":true },
{ "from": ["qsd2", "inout1"], "to": ["qsd2", "inout1"], "endArrow":true }
]
}
}
@@ -1,59 +0,0 @@
{
"nodesFile": "/app/assets/html/bzGraflow/nodesEIC.html",
"flow": {
"nodes":[
{ "nodeType": "eicBasic",
"id": "aze",
"ncoords": { "x": 50, "y": 120},
"markup": {
"title": "Build attendees list",
"subtitle": "Build an attendees list to email"
},
"data": { "a": "a1", "b":"b1"}
},
{ "nodeType": "eicBasic",
"id": "aze2",
"ncoords": { "x": 100, "y": 220},
"markup": {
"title": "Select message",
"subtitle": "Select an email template"
},
"data": { "a": "a2", "b":"b2"}
},
{ "nodeType": "eicBasic",
"id": "aze3",
"ncoords": { "x": 150, "y": 320},
"markup": {
"title": "Data mapping",
"subtitle": "Associate content variables with attendees data"
},
"data": { "a": "a3", "b":"b3"}
},
{ "nodeType": "eicBasic",
"id": "aze4",
"ncoords": { "x": 150, "y": 320},
"markup": {
"title": "Schedule mailing",
"subtitle": "Choose time to send the mail"
},
"data": { "a": "a3", "b":"b3"}
},
{ "nodeType": "eicBasic",
"id": "aze5",
"ncoords": { "x": 150, "y": 320},
"markup": {
"title": "Stats",
"subtitle": "Access mailing statistics"
},
"data": { "a": "a3", "b":"b3"}
}
],
"links": [
{ "from": ["aze2", "out1"], "to": ["aze", "in1"] },
{ "from": ["aze2", "out2"], "to": ["aze3", "in1"] },
{ "from": ["aze", "out1"], "to": ["aze4", "in1"] },
{ "from": ["aze3", "out1"], "to": ["aze4", "in2"] },
{ "from": ["aze4", "out1"], "to": ["aze5", "in1"] }
]
}
}
+20
View File
@@ -1,4 +1,24 @@
[ [
{
"label": "Simulations Management",
"icon": "icon-lab2",
"collapsed": true,
"access": ["*"],
"items": [
{
"label": "Create a simulation",
"icon": "icon-new",
"route": "/sims/create",
"access": ["*"]
},
{
"label": "Play / Pause a simulation",
"icon": "icon-file-play",
"route": "/sims/manage",
"access": ["*"]
}
]
},
{ {
"label": "Live Arena", "label": "Live Arena",
"icon": "icon-bolt", "icon": "icon-bolt",
+11
View File
@@ -0,0 +1,11 @@
{
"maestro": {
"actionsChannel": "system:requests:maestro",
"lifecycleChannel": "system:maestro:lifecycle:[UID]"
},
"observer": {
"actionsChannel": "system:requests:observer",
"subscribeFrequencyMs": 1000,
"cameraDebounceMs": 600
}
}
+14
View File
@@ -37,6 +37,20 @@
"uri": "/api/keyframes/{kfId}/agents", "uri": "/api/keyframes/{kfId}/agents",
"method": "PUT" "method": "PUT"
} }
},
"/sims": {
"list": {
"uri": "/api/sims",
"method": "POST"
},
"get": {
"uri": "/api/sims/{simulationUuid}",
"method": "GET"
},
"create": {
"uri": "/api/sims",
"method": "PUT"
}
} }
} }
} }
@@ -1,6 +1,6 @@
[ [
{ {
"chan": "system:gps:agents", "chan": "system:observer:subscribed[CUID]:agents",
"events": [ "events": [
{ {
"eventName": "move", "eventName": "move",
+63 -33
View File
@@ -27,6 +27,7 @@
--app-menu-collapsed-width: 50px; --app-menu-collapsed-width: 50px;
--app-menu-expanded-width: 160px; --app-menu-expanded-width: 160px;
--app-menu-hover-max-width: 400px;
--app-menu-width: var(--app-menu-expanded-width); --app-menu-width: var(--app-menu-expanded-width);
@@ -80,6 +81,53 @@ menu[eicmenu] [menuitem] > a > i, menu[eicmenu] [menuitem] > .nolink > i{
menu[eicmenu] [menuitem] > a > button, menu[eicmenu] [menuitem] > .nolink button { menu[eicmenu] [menuitem] > a > button, menu[eicmenu] [menuitem] > .nolink button {
background: var(--app-color-primary); background: var(--app-color-primary);
} }
/* Submenu vertical expand/collapse (overrides EICUI height:0 which breaks transitions) */
menu[eicmenu].app-menu > [menuitem] > ul {
max-height: 30rem;
height: auto !important;
overflow: hidden;
transition: max-height 0.6s ease-in-out, opacity 0.3s ease-in-out, padding 0.3s ease-in-out;
}
menu[eicmenu].app-menu > [menuitem] > ul[collapsed] {
max-height: 0;
height: auto !important;
padding-top: 0;
padding-bottom: 0;
}
/* Collapsed app menu: show main section icons, hide sub-entries */
menu[eicmenu].app-menu[collapsed] {
width: max-content;
transition: max-width 0.3s ease-in-out;
}
menu[eicmenu].app-menu[collapsed]:not(:hover) {
max-width: var(--app-menu-collapsed-width);
overflow-x: hidden;
}
menu[eicmenu].app-menu[collapsed]:hover {
max-width: var(--app-menu-hover-max-width);
overflow-x: hidden;
}
menu[eicmenu].app-menu[collapsed]:not(:hover) > [menuitem] > .nolink {
display: flex;
justify-content: center;
padding: var(--eicui-base-spacing-xs);
}
menu[eicmenu].app-menu[collapsed]:not(:hover) > [menuitem] > .nolink > label,
menu[eicmenu].app-menu[collapsed]:not(:hover) > [menuitem] > .nolink > button {
display: none;
}
menu[eicmenu].app-menu[collapsed]:not(:hover) > [menuitem] > .nolink > i {
font-size: larger;
margin: 0;
}
menu[eicmenu].app-menu[collapsed]:not(:hover) > [menuitem] > ul {
max-height: 0 !important;
opacity: 0;
padding: 0;
}
[eicapp] .app-workspace { [eicapp] .app-workspace {
display: grid; display: grid;
/* /*
@@ -299,33 +347,6 @@ article[eiccard][media] > header {
font-size: 1.6rem; font-size: 1.6rem;
} }
[device="tablet"] section .cols-2:not(.noflex), [device="mobile"] section .cols-2:not(.noflex) {
display: flex;
flex-direction: column;
grid-template-columns: none;
align-items: initial;
}
[device="tablet"] .cols-3, [device="mobile"] .cols-3,
[device="tablet"] .cols-4, [device="mobile"] .cols-4,
[device="tablet"] .cols-5, [device="mobile"] .cols-5,
[device="mobile"] .cols-6 {
display: flex;
flex-direction: column;
grid-template-columns: none;
}
[device="tablet"] .cols-6 {
grid-template-columns: 1fr 1fr 1fr;
}
[device="tablet"] .cols-8 {
grid-template-columns: 1fr 1fr 1fr 1fr;
}
[device="mobile"] .cols-8 {
grid-template-columns: 1fr 1fr;
}
div.window > section:first-of-type > article[eiccard]:first-of-type{ div.window > section:first-of-type > article[eiccard]:first-of-type{
height:100%; height:100%;
border: 2px solid var(--app-color-primary); border: 2px solid var(--app-color-primary);
@@ -366,7 +387,7 @@ input{
padding-left: 1em; padding-left: 1em;
box-sizing: border-box; box-sizing: border-box;
height: 2em; height: 2em;
background-color: #6B5; background-color: #CFC;
border: none; border: none;
} }
@@ -376,20 +397,29 @@ div.window > section button[eicbutton][rounded] {
color: #222; color: #222;
} }
[eicdatagrid] .dataset .row:hover {
background-color: #483;
}
/* Customizations to buildoz*/ /* Customizations to buildoz*/
bz-select > button{ bz-select > button{
background: linear-gradient( to bottom, #251, #372 15%, #483 50%, #372 85%, #251 ) !important; background: linear-gradient( to bottom, #251, #372 15%, #483 50%, #372 85%, #251 ) !important;
color:#EEE; color:#EEE;
} }
bz-select > button::after{ color:#EEE; } bz-select > button::after{ color:#EEE; }
bz-select option{ bz-select option,
div.options-container.portaled option{
background-color: #676; background-color: #676;
color: #EEE; color: #EEE;
} }
bz-select option i{ margin-right:0.3em; } bz-select option i,
bz-select option i.icon-atom1{ color:#FF4; } div.options-container.portaled option i{ margin-right:0.3em; }
bz-select option i.icon-bug{ color:#4DF; } bz-select option i.icon-atom1,
bz-select > div.options-container.open { max-height: 20em; } div.options-container.portaled option i.icon-atom1{ color:#FF4; }
bz-select option i.icon-bug,
div.options-container.portaled option i.icon-bug{ color:#4DF; }
bz-select > div.options-container.open,
div.options-container.portaled.open { max-height: 20em; }
bz-toggler div.toggle-switch span.toggle-bar { background-color: #473; } bz-toggler div.toggle-switch span.toggle-bar { background-color: #473; }
bz-toggler div.toggle-switch span.toggle-thumb { background-color:#9D8; } bz-toggler div.toggle-switch span.toggle-thumb { background-color:#9D8; }
Binary file not shown.
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+618
View File
@@ -0,0 +1,618 @@
@font-face {
font-family: 'glyphs';
src: url('/app/assets/styles/fonts/glyphs.eot');
src: url('/app/assets/styles/fonts/glyphs.eot') format('embedded-opentype'),
url('/app/assets/styles/fonts/glyphs.ttf') format('truetype'),
url('/app/assets/styles/fonts/glyphs.woff') format('woff'),
url('/app/assets/styles/fonts/glyphs.svg') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'glyphs' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
span[class*="icon-"][primary]:before { color: var(--app-color-primary); }
span[class*="icon-"][secondary] { color: var(--app-color-secondary); }
span[class*="icon-"][success] { color: var(--app-color-success); }
span[class*="icon-"][danger] { color: var(--app-color-danger); }
span[class*="icon-"][warning] { color: var(--app-color-warning); }
span[class*="icon-"][accent] { color: var(--app-color-accent); }
span[class*="icon-"][info] { color: var(--app-color-info); }
[class*="icon-"][xxsmall] { font-size: var(--eicui-base-icon-size-2xs); }
[class*="icon-"][xsmall] { font-size: var(--eicui-base-icon-size-xs) !important; }
[class*="icon-"][small] { font-size: var(--eicui-base-icon-size-s) !important; }
[class*="icon-"][medium] { font-size: var(--eicui-base-icon-size-m) !important; }
[class*="icon-"][large] { font-size: var(--eicui-base-icon-size-l) !important; }
[class*="icon-"][xlarge] { font-size: var(--eicui-base-icon-size-xl) !important; }
[class*="icon-"][xxlarge] { font-size: var(--eicui-base-icon-size-2xl); }
[class*="icon-"][xxxlarge] { font-size: var(--eicui-base-icon-size-3xl); }
[class*="icon-"][xxxxlarge] { font-size: var(--eicui-base-icon-size-4xl); }
@keyframes spin {
0% { transform: rotate(0deg) ; }
100% { transform: rotate(360deg) ; }
}
@keyframes uturn {
0% { transform: rotate(-180deg) ; }
100% { transform: rotate(0deg) ; }
}
@keyframes uturn-ccw {
0% { transform: rotate(180deg) ; }
100% { transform: rotate(0deg) ; }
}
.spin { animation: spin 1s infinite linear; }
.arrow-expand:before {
content: "\f106" !important;
animation: uturn-ccw 0.3s 1 linear;
}
.arrow-collapse:before {
content: "\f107" !important;
animation: uturn 0.3s 1 normal linear;
}
.icon-backward1:before {
content: "\e905";
}
.icon-backward-step:before {
content: "\e943";
}
.icon-fast-forward:before {
content: "\e944";
}
.icon-pause2:before {
content: "\e945";
}
.icon-play1:before {
content: "\e946";
}
.icon-target1:before {
content: "\e948";
}
.icon-usb:before {
content: "\e949";
}
.icon-video-camera1:before {
content: "\e94a";
}
.icon-price-tag:before {
content: "\e93c";
}
.icon-align-center:before {
content: "\e933";
}
.icon-align-left:before {
content: "\e934";
}
.icon-align-right:before {
content: "\e935";
}
.icon-format-color:before {
content: "\e93b";
}
.icon-format-bold:before {
content: "\e936";
}
.icon-font-size-down:before {
content: "\e937";
}
.icon-format-underline:before {
content: "\e939";
}
.icon-big-bullet:before {
content: "\e928";
}
.icon-code:before {
content: "\e92b";
}
.icon-check-rounded:before {
content: "\e91f";
}
.icon-dashboard:before {
content: "\e92a";
}
.icon-more:before {
content: "\e923";
}
.icon-locked:before {
content: "\e925";
}
.icon-unlocked:before {
content: "\e926";
}
.icon-menu:before {
content: "\e920";
}
.icon-reply:before {
content: "\e921";
}
.icon-back:before {
content: "\e921";
}
.icon-link-ext:before {
content: "\e922";
}
.icon-share:before {
content: "\e929";
}
.icon-workflow:before {
content: "\e929";
}
.icon-health:before {
content: "\e927";
}
.icon-copy:before {
content: "\e915";
}
.icon-filter:before {
content: "\e91c";
}
.icon-deny:before {
content: "\e906";
}
.icon-home:before {
content: "\e91d";
}
.icon-pause1:before {
content: "\e916";
}
.icon-market:before {
content: "\e914";
}
.icon-thumbs-down:before {
content: "\e90b";
}
.icon-thumbs-up:before {
content: "\e90c";
}
.icon-trash:before {
content: "\e91e";
}
.icon-bolt:before {
content: "\e901";
}
.icon-check:before {
content: "\e902";
}
.icon-cancel:before {
content: "\e907";
}
.icon-close:before {
content: "\e908";
}
.icon-download:before {
content: "\e903";
}
.icon-envelope:before {
content: "\e909";
}
.icon-company:before {
content: "\e90a";
}
.icon-servers:before {
content: "\e904";
}
.icon-cabinet:before {
content: "\e94b";
}
.icon-cabinet1:before {
content: "\e94c";
}
.icon-camera1:before {
content: "\e94e";
}
.icon-camera2:before {
content: "\e94f";
}
.icon-film1:before {
content: "\e950";
}
.icon-chronometer:before {
content: "\e951";
}
.icon-chart:before {
content: "\e952";
}
.icon-lab:before {
content: "\e953";
}
.icon-satellite:before {
content: "\e954";
}
.icon-spaceinvaders:before {
content: "\e955";
}
.icon-bomb:before {
content: "\e957";
}
.icon-tools:before {
content: "\e958";
}
.icon-bus:before {
content: "\e959";
}
.icon-stop2:before {
content: "\e95a";
}
.icon-atom:before {
content: "\e95b";
}
.icon-globe:before {
content: "\e95c";
}
.icon-globe1:before {
content: "\e95d";
}
.icon-grid:before {
content: "\e95e";
}
.icon-flag:before {
content: "\e95f";
}
.icon-lock:before {
content: "\e960";
}
.icon-unlocked1:before {
content: "\e961";
}
.icon-camera3:before {
content: "\e962";
}
.icon-calculator:before {
content: "\e963";
}
.icon-diamond:before {
content: "\e965";
}
.icon-atom1:before {
content: "\e966";
}
.icon-syringe:before {
content: "\e967";
}
.icon-health1:before {
content: "\e968";
}
.icon-pill:before {
content: "\e969";
}
.icon-lab1:before {
content: "\e96a";
}
.icon-graph:before {
content: "\e96b";
}
.icon-review:before {
content: "\e917";
}
.icon-correction:before {
content: "\e918";
}
.icon-mediation:before {
content: "\e919";
}
.icon-writing:before {
content: "\e91b";
}
.icon-snapshot:before {
content: "\e92d";
}
.icon-toc:before {
content: "\e92e";
}
.icon-folder:before {
content: "\e92f";
}
.icon-folder-open:before {
content: "\e930";
}
.icon-folder-add:before {
content: "\e931";
}
.icon-folder-remove:before {
content: "\e932";
}
.icon-qrcode:before {
content: "\e938";
}
.icon-chat:before {
content: "\e96c";
}
.icon-bug:before {
content: "\e999";
}
.icon-font-size-up:before {
content: "\ea61";
}
.icon-format-italic:before {
content: "\ea64";
}
.icon-stop:before {
content: "\e92c";
}
.icon-play:before {
content: "\e900";
}
.icon-history:before {
content: "\e94d";
}
.icon-spinner:before {
content: "\e981";
}
.icon-cog:before {
content: "\e994";
}
.icon-star-empty:before {
content: "\e9d7";
}
.icon-star-half:before {
content: "\e9d8";
}
.icon-star-full:before {
content: "\e9d9";
}
.icon-heart:before {
content: "\e9da";
}
.icon-pdf:before {
content: "\eadf";
}
.icon-pen:before {
content: "\e910";
}
.icon-coaching:before {
content: "\e91a";
}
.icon-cart:before {
content: "\e93a";
}
.icon-phone:before {
content: "\e942";
}
.icon-hour-glass:before {
content: "\e90f";
}
.icon-refresh:before {
content: "\e984";
}
.icon-complaint:before {
content: "\e9a8";
}
.icon-evaluation:before {
content: "\e9b8";
}
.icon-link:before {
content: "\e9cb";
}
.icon-loop:before {
content: "\ea2d";
}
.icon-image:before {
content: "\e90d";
}
.icon-new:before {
content: "\e924";
}
.icon-map:before {
content: "\e947";
}
.icon-pin:before {
content: "\e947";
}
.icon-user-check:before {
content: "\e975";
}
.icon-cogs:before {
content: "\e995";
}
.icon-bin:before {
content: "\e9ac";
}
.icon-attachment:before {
content: "\e9cd";
}
.icon-xls:before {
content: "\eae2";
}
.icon-paint-format:before {
content: "\e93d";
}
.icon-camera:before {
content: "\e93e";
}
.icon-film:before {
content: "\e93f";
}
.icon-video-camera:before {
content: "\e940";
}
.icon-stack:before {
content: "\e941";
}
.icon-display:before {
content: "\e956";
}
.icon-database:before {
content: "\e964";
}
.icon-target:before {
content: "\e9b3";
}
.icon-tree:before {
content: "\e9bc";
}
.icon-play2:before {
content: "\ea15";
}
.icon-pause:before {
content: "\ea16";
}
.icon-stop1:before {
content: "\ea17";
}
.icon-previous:before {
content: "\ea18";
}
.icon-next:before {
content: "\ea19";
}
.icon-backward:before {
content: "\ea1a";
}
.icon-forward2:before {
content: "\ea1b";
}
.icon-embed:before {
content: "\ea7f";
}
.icon-steam:before {
content: "\eaac";
}
.icon-dropbox:before {
content: "\eaae";
}
.icon-tux:before {
content: "\eabd";
}
.icon-codepen:before {
content: "\eae8";
}
.icon-sort-asc:before {
content: "\e911";
}
.icon-sort-desc:before {
content: "\e912";
}
.icon-unsorted:before {
content: "\e913";
}
.icon-help:before {
content: "\e90e";
}
.icon-plus:before {
content: "\f067";
}
.icon-search:before {
content: "\f002";
}
.icon-user:before {
content: "\f007";
}
.icon-logoff:before {
content: "\f011";
}
.icon-edit:before {
content: "\f040";
}
.icon-checked:before {
content: "\f046";
}
.icon-info:before {
content: "\f05a";
}
.icon-expand:before {
content: "\f065";
}
.icon-warning:before {
content: "\f071";
}
.icon-calendar:before {
content: "\f073";
}
.icon-comment:before {
content: "\f075";
}
.icon-twitter:before {
content: "\f081";
}
.icon-facebook:before {
content: "\f082";
}
.icon-square-o:before {
content: "\f096";
}
.icon-website:before {
content: "\f0ac";
}
.icon-users:before {
content: "\f0c0";
}
.icon-list-ul:before {
content: "\f0ca";
}
.icon-table:before {
content: "\f0ce";
}
.icon-exchange:before {
content: "\f0ec";
}
.icon-alert:before {
content: "\f0f3";
}
.icon-chevron-left:before {
content: "\f100";
}
.icon-chevron-right:before {
content: "\f101";
}
.icon-angle-left:before {
content: "\f104";
}
.icon-angle-right:before {
content: "\f105";
}
.icon-angle-up:before {
content: "\f106";
}
.icon-angle-down:before {
content: "\f107";
}
.icon-zip:before {
content: "\f1c6";
}
.icon-send:before {
content: "\f1d8";
}
.icon-preview:before {
content: "\f1e5";
}
.icon-stats:before {
content: "\f200";
}
.icon-toggle-off:before {
content: "\f204";
}
.icon-toggle-on:before {
content: "\f205";
}
.icon-user-add:before {
content: "\f234";
}
.icon-calendar-plus:before {
content: "\f271";
}
.icon-calendar-minus:before {
content: "\f272";
}
.icon-calendar-failed:before {
content: "\f273";
}
.icon-calendar-check:before {
content: "\f274";
}
+2704 -553
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -20,6 +20,11 @@
"url": "/editors", "url": "/editors",
"role": [ "*" ], "role": [ "*" ],
"controller" : "/editors/EditorsController" "controller" : "/editors/EditorsController"
},
{
"url": "/sims",
"role": [ "*" ],
"controller" : "/sims/SimsController"
}, },
{ {
"url": "/system", "url": "/system",
+1
View File
@@ -14,6 +14,7 @@
], ],
"json": [ "json": [
{"name":"global/app-menu-map.json"}, {"name":"global/app-menu-map.json"},
{"id":"busChannels", "name":"global/busChannels.json"},
{"path": "/app/controllers/common/", "name": "errorController.json", "comment": "Trick to preload errorController stuff, to still have error messages if S3 is down."} {"path": "/app/controllers/common/", "name": "errorController.json", "comment": "Trick to preload errorController stuff, to still have error messages if S3 is down."}
] ]
} }
@@ -3,6 +3,7 @@ class DashboardsController extends WindozController {
constructor(params) { constructor(params) {
super(params) super(params)
this.arenaConfig = app.Assets.Store.json.arenaConfig this.arenaConfig = app.Assets.Store.json.arenaConfig
this.busChannels = app.Assets.Store.json.busChannels
this.eventsMapping = app.Assets.Store.json.eventsMapping this.eventsMapping = app.Assets.Store.json.eventsMapping
console.log('=============>DashboardsController constructor') console.log('=============>DashboardsController constructor')
} }
@@ -21,6 +22,7 @@ class DashboardsController extends WindozController {
const ttb = new app.LoadedModules.Threetobus({ const ttb = new app.LoadedModules.Threetobus({
eventsMapping: this.eventsMapping, eventsMapping: this.eventsMapping,
sceneSize: this.arenaConfig.arenaSize, sceneSize: this.arenaConfig.arenaSize,
observer: this.busChannels.observer,
}) })
ttb.initScene({ ttb.initScene({
axes: true, axes: true,
+51
View File
@@ -0,0 +1,51 @@
class SimsController extends WindozController {
constructor(params) {
super(params)
}
async create() {
const models = {
sims: new SimsModel('/sims'),
keyframes: new KeyframesModel('/keyframes')
}
this.loadWindow(
'sims/CreateSimView',
{
title: '<i class="icon-new"></i> Create a simulation',
static: true,
expanded: false,
withSettings: false,
windowStyle: WindozDomContent.boxFromPrefs('sims.createsimview', { x: 'center', y: 'center', w: 600, h: 300 }),
},
{
models: models,
}
)
}
async manage() {
const models = {
sims: new SimsModel('/sims')
}
this.loadWindow(
'sims/ManageSimView',
{
title: '<i class="icon-file-play"></i> Play / Pause a simulation',
static: true,
expanded: false,
withSettings: false,
windowStyle: WindozDomContent.boxFromPrefs('sims.managesimview', { x: 50, y: 50, w: 1000, h: 600 }),
},
{
models: models,
}
)
}
}
app.registerClass('SimsController', SimsController)
+36
View File
@@ -0,0 +1,36 @@
{
"routes": [
{
"url": "/create",
"role": [ "*" ],
"controller" : "/sims/SimsController",
"method": "create"
},
{
"url": "/manage",
"role": [ "*" ],
"controller" : "/sims/SimsController",
"method": "manage"
}
],
"models": [
"SimsModel",
"KeyframesModel"
],
"views": [
"sims/CreateSimView",
"sims/ManageSimView"
],
"controllerDependencies": [
"/helpers/basicDialogs",
"/helpers/activeAttributes"
],
"assets": {
"styles": [
],
"html": [
],
"json": [
]
}
}
+17 -3
View File
@@ -47,7 +47,13 @@ app.helpers.formBuilder = {
} }
if(component){ if(component){
component.classList.add('formbuilder-field') component.classList.add('formbuilder-field')
component.value = this.getPathInObj(fieldsValues, propName) || fieldsObj[propName].default const rawValue = this.getPathInObj(fieldsValues, propName) ?? fieldsObj[propName].default
if(fieldsObj[propName].type === 'number') {
const n = Number(rawValue)
component.value = Number.isFinite(n) ? n : 0
} else {
component.value = rawValue
}
fieldRow.append(component) fieldRow.append(component)
allFields.push(fieldRow) allFields.push(fieldRow)
} }
@@ -77,6 +83,14 @@ app.helpers.formBuilder = {
return(target[parts[parts.length - 1]] ) return(target[parts[parts.length - 1]] )
}, },
#fieldValue(el) {
if(el?.type === 'number') {
const n = Number(el.value)
return(Number.isFinite(n) ? n : el.value)
}
return(el.value)
},
getFieldsValues(rootSel){ getFieldsValues(rootSel){
const result = {} const result = {}
document.querySelectorAll(`${rootSel} .formbuilder-field`).forEach(el => { document.querySelectorAll(`${rootSel} .formbuilder-field`).forEach(el => {
@@ -89,14 +103,14 @@ app.helpers.formBuilder = {
} }
target = target[key] target = target[key]
} }
target[path[path.length - 1]] = el.value target[path[path.length - 1]] = this.#fieldValue(el)
}) })
return(result) return(result)
}, },
getFieldValue(rootSel, name){ getFieldValue(rootSel, name){
const comp = document.querySelector(`${rootSel} .formbuilder-field[name="${name}"]`) const comp = document.querySelector(`${rootSel} .formbuilder-field[name="${name}"]`)
if(comp) return(comp.value) if(comp) return(this.#fieldValue(comp))
else return(null) else return(null)
}, },
+10 -1
View File
@@ -48,7 +48,16 @@ app.helpers.kfConsole = {
this.kfArena.removeAgent(aid) this.kfArena.removeAgent(aid)
}, },
updateAgent: async (aid, properties) => { updateAgent: async (aid, properties) => {
if(!Object.keys(this.kfArena.agents).includes(aid)) throw(`Agent ${aid} not on scene !`)
const agent = this.kfArena.agents[aid]
const values = { ...agent.values, ...properties }
if(properties.position) values.position = { ...agent.values.position, ...properties.position }
if(properties.speed) values.speed = { ...agent.values.speed, ...properties.speed }
agent.values = values
if(properties.position) this.kfArena.moveAgent(aid, values.position)
if(properties.speed) this.kfArena.changeAgentSpeed(aid, values.speed)
if(this.currentlySelectedAid === aid) this.fillAgentProperties(aid, agent.props, agent.values)
this.updateKfButtons()
}, },
selectAgent: async (aid) => { selectAgent: async (aid) => {
if(!Object.keys(this.kfArena.agents).includes(aid)) throw(`Agent ${aid} not on scene !`) if(!Object.keys(this.kfArena.agents).includes(aid)) throw(`Agent ${aid} not on scene !`)
+9 -2
View File
@@ -276,12 +276,19 @@ class WindozDomContent extends View {
left = box.x ? box.x : defaults.x left = box.x ? box.x : defaults.x
top = box.y ? box.y : defaults.y top = box.y ? box.y : defaults.y
width = box.w ? box.w : defaults.w width = box.w ? box.w : defaults.w
height = box.x ? box.h : defaults.h height = box.h ? box.h : defaults.h
} else { } else {
left = defaults.x left = defaults.x
top = defaults.y top = defaults.y
width = defaults.w width = defaults.w
height = defaults.h height = defaults.h
const ws = document.querySelector('[eicapp] .app-workspace')?.getBoundingClientRect()
const areaLeft = ws?.left ?? 0
const areaTop = ws?.top ?? 0
const areaWidth = ws?.width ?? window.innerWidth
const areaHeight = ws?.height ?? window.innerHeight
if(left === 'center') left = Math.round(areaLeft + (areaWidth - width) / 2)
if(top === 'center') top = Math.round(areaTop + (areaHeight - height) / 2)
} }
return({ return({
width: `${width}px`, width: `${width}px`,
+9 -1
View File
@@ -43,7 +43,15 @@ class AgentsModel extends WindozModel {
async getDefaultProps(id){ async getDefaultProps(id){
const aprops = await this.getProperties(id) const aprops = await this.getProperties(id)
const defaults={ position: { x:0, y:0, z:0 }, speed: { x:0, y:0, z:0 }} const defaults={ position: { x:0, y:0, z:0 }, speed: { x:0, y:0, z:0 }}
for(const p in aprops) defaults[p] = aprops[p].default for(const p in aprops) {
const prop = aprops[p]
if(prop?.type === 'number') {
const n = Number(prop.default)
defaults[p] = Number.isFinite(n) ? n : 0
} else {
defaults[p] = prop.default
}
}
return(defaults) return(defaults)
} }
} }
+18 -1
View File
@@ -38,14 +38,31 @@ class KeyframesModel extends WindozModel {
) )
} }
#normalizeAxisVector(vec) {
if(!vec || typeof(vec) !== 'object') return(null)
const axes = ['x', 'y', 'z']
const out = {}
for(const axis of axes) {
const n = Number(vec[axis])
if(!Number.isFinite(n)) return(null)
out[axis] = n
}
return(out)
}
async save(kfId, data) { async save(kfId, data) {
const kfData = Object.keys(data).map(aid => { const kfData = Object.keys(data).map(aid => {
const { position, speed, ...storeValues} = data[aid].values const { position, speed, ...storeValues} = data[aid].values
const gpsPosition = this.#normalizeAxisVector(position)
const gpsSpeed = this.#normalizeAxisVector(speed)
if(!gpsPosition || !gpsSpeed) {
throw(new Error(`Agent ${aid}: position and speed must be numeric vectors`))
}
return({ return({
aid: aid, aid: aid,
type: data[aid].type, type: data[aid].type,
storeValues: storeValues, storeValues: storeValues,
gpsValues: { position: data[aid].values.position, speed: data[aid].values.speed } gpsValues: { position: gpsPosition, speed: gpsSpeed },
}) })
}) })
+86
View File
@@ -0,0 +1,86 @@
class SimsModel extends WindozBusModel {
constructor() {
super()
this.ressource = '/sims'
this.busChannels = app.Assets.Store.json.busChannels
}
async list() {
const endpoint = {...app.config.api[this.ressource].list}
const [listResponse, statusList] = await Promise.all([
this.request(endpoint.uri, endpoint.method),
this.getSimulationsStatus(),
])
const sims = listResponse?.payload ?? []
const statuses = Array.isArray(statusList) ? statusList : (statusList?.simulations ?? [])
const stateBySimulationId = new Map(
statuses.map(item => [item.simulationId, item.state])
)
return({
...listResponse,
payload: sims.map(sim => ({
...sim,
state: stateBySimulationId.get(sim.simulationUuid) ?? 'idle',
})),
})
}
async get(simulationUuid) {
let endpoint = {...app.config.api[this.ressource].get}
endpoint.uri = endpoint.uri.replace('{simulationUuid}', simulationUuid)
return(
this.request(endpoint.uri, endpoint.method)
)
}
async create(simData) {
let endpoint = {...app.config.api[this.ressource].create}
return(
this.request(endpoint.uri, endpoint.method, simData)
)
}
async startSimulation(simulationUuid) {
return(this.busActionRequest(
this.busChannels.maestro.actionsChannel,
'STARTSIMULATION',
{
simulationUuid,
infraId: null,
},
60000
))
}
async stopSimulation(simulationUuid) {
return(this.busActionRequest(
this.busChannels.maestro.actionsChannel,
'STOPSIMULATION',
{ simulationUuid },
30000
))
}
async pauseSimulation(simulationUuid) {
return(this.busActionRequest(
this.busChannels.maestro.actionsChannel,
'PAUSESIMULATION',
{ simulationUuid },
30000
))
}
async getSimulationsStatus() {
return(this.busActionRequest(
this.busChannels.maestro.actionsChannel,
'GETSIMULATIONSSTATUS',
{},
15000
))
}
}
app.registerClass('SimsModel', SimsModel)
+1 -1
View File
@@ -99,7 +99,7 @@ class Snaptobus{
processBusEvent(eventType, chan, payload, userId, x){ processBusEvent(eventType, chan, payload, userId, x){
const chanObj = this._curBusConfig.find(item => item.chan==chan) const chanObj = this._curBusConfig.find(item => app.MessageBus.chanMatch(chan, item.chan))
if(!chanObj) return if(!chanObj) return
const eventObj = chanObj.events.find(item => item.eventName==eventType) const eventObj = chanObj.events.find(item => item.eventName==eventType)
if(!eventObj) return if(!eventObj) return
+115 -6
View File
@@ -9,6 +9,16 @@ export class Threetobus{
this._curEventsMapping = [] this._curEventsMapping = []
this._stagedEventsMapping = options.eventsMapping this._stagedEventsMapping = options.eventsMapping
this.sceneSize = options.sceneSize this.sceneSize = options.sceneSize
this._observerConfig = {
actionsChannel: 'system:requests:observer',
subscribeFrequencyMs: 1000,
cameraDebounceMs: 600,
...options.observer,
}
this._frustumRenderEngine = null
this._frustumDebounceTimer = null
this._frustumCameraChangeHandler = null
this._frustumWatching = false
this.commitConfig() this.commitConfig()
this.cameras = {} this.cameras = {}
@@ -19,8 +29,8 @@ export class Threetobus{
} }
busReconnect(){ busReconnect(){
this.commitConfig() // To resubscribe... this.commitConfig()
//TODO : Not ideal because if we're in the middle of non-commited changes... if(this._frustumWatching) this._scheduleFrustumResubscribe()
} }
get EventsMapping() { return this._stagedEventsMapping } get EventsMapping() { return this._stagedEventsMapping }
@@ -34,9 +44,14 @@ export class Threetobus{
if(this._curEventsMapping.map(item => item.chan).includes(chanObj.chan)) chansToKeep.push(chanObj) if(this._curEventsMapping.map(item => item.chan).includes(chanObj.chan)) chansToKeep.push(chanObj)
else chansToAdd.push(chanObj) else chansToAdd.push(chanObj)
} }
const chansToDel = this._curEventsMapping.filter(item => (!chansToKeep.map(c=>c.chan).includes(item.chan) && !chansToAdd.map(c=>c.chan).includes(item.chan))) const chansToDel = this._curEventsMapping.filter(item => (
await app.MessageBus.subscribe(chansToAdd.map(item => item.chan)) !chansToKeep.map(c => c.chan).includes(item.chan)
await app.MessageBus.unSubscribe(chansToDel.map(item => item.chan)) && !chansToAdd.map(c => c.chan).includes(item.chan)
))
if(app.MessageBus?.connected) {
if(chansToAdd.length) await app.MessageBus.subscribe(chansToAdd.map(item => item.chan))
if(chansToDel.length) await app.MessageBus.unSubscribe(chansToDel.map(item => item.chan))
}
// console.log('subscribe:', chansToAdd.map(item => item.chan)) // console.log('subscribe:', chansToAdd.map(item => item.chan))
// console.log('unSubscribe:', chansToDel) // console.log('unSubscribe:', chansToDel)
@@ -88,7 +103,8 @@ export class Threetobus{
// if yes : how to discriminate static value from event-mapping definition ? // if yes : how to discriminate static value from event-mapping definition ?
if(mapping.child) id += '_'+mapping.child if(mapping.child) id += '_'+mapping.child
if(id){ if(id){
const obj3D = this.scene.getObjectByName(id) let obj3D = this.scene.getObjectByName(id)
if(!obj3D) obj3D = this._ensurePlaceholderAgent(id)
if(obj3D){ if(obj3D){
this.assignFromConfig(payload, mapping, obj3D) this.assignFromConfig(payload, mapping, obj3D)
} }
@@ -161,6 +177,99 @@ export class Threetobus{
return(path.split('.').reduce((acc, key) => acc?.[key], obj)) return(path.split('.').reduce((acc, key) => acc?.[key], obj))
} }
watchCameraFrustum(renderEngine) {
if(!renderEngine || renderEngine.mode !== '3D') return
if(!app.MessageBus?.config?.enabled) {
console.warn('[Threetobus] MessageBus disabled — camera frustum watch skipped')
return
}
this._frustumRenderEngine = renderEngine
this._frustumWatching = true
if(!this._frustumCameraChangeHandler) {
this._frustumCameraChangeHandler = () => this._scheduleFrustumResubscribe()
renderEngine.controls.addEventListener('change', this._frustumCameraChangeHandler)
}
app.MessageBus.whenConnected(() => {
this.commitConfig()
this._scheduleFrustumResubscribe()
})
}
stopWatchingCameraFrustum() {
this._frustumWatching = false
if(this._frustumDebounceTimer) {
clearTimeout(this._frustumDebounceTimer)
this._frustumDebounceTimer = null
}
if(this._frustumRenderEngine?.controls && this._frustumCameraChangeHandler) {
this._frustumRenderEngine.controls.removeEventListener('change', this._frustumCameraChangeHandler)
}
this._frustumRenderEngine = null
this._frustumCameraChangeHandler = null
}
_scheduleFrustumResubscribe() {
if(!this._frustumWatching) return
if(this._frustumDebounceTimer) clearTimeout(this._frustumDebounceTimer)
this._frustumDebounceTimer = setTimeout(() => {
this._frustumDebounceTimer = null
this._resubscribeCameraFrustum()
}, this._observerConfig.cameraDebounceMs)
}
async _resubscribeCameraFrustum() {
if(!this._frustumWatching || !this._frustumRenderEngine) return
if(!app.MessageBus?.connected) return
const camera = this._frustumRenderEngine.camera
if(!camera?.isPerspectiveCamera) return
const planes = this._cameraFrustumPlanes(camera)
const chan = this._observerConfig.actionsChannel
try {
await app.MessageBus.requestBusAction(
chan,
'SUBSCRIBEFRUSTUM',
{
planes,
frequency: this._observerConfig.subscribeFrequencyMs,
},
10000
)
} catch(err) {
console.warn('[Threetobus] SUBSCRIBEFRUSTUM failed:', err)
}
}
_cameraFrustumPlanes(camera) {
camera.updateMatrixWorld(true)
const matrix = new THREE.Matrix4().multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
)
const frustum = new THREE.Frustum().setFromProjectionMatrix(matrix)
return(frustum.planes.map(plane => ({
nx: plane.normal.x,
ny: plane.normal.y,
nz: plane.normal.z,
d: plane.constant,
})))
}
_ensurePlaceholderAgent(agentId) {
const geo = new THREE.BoxGeometry(0.4, 0.4, 0.4)
const mat = new THREE.MeshStandardMaterial({ color: 0x44aa88 })
const obj3D = new THREE.Mesh(geo, mat)
obj3D.name = agentId
this.tweensRegistry[agentId] = { move: null, rotate: null }
this.scene.add(obj3D)
return(obj3D)
}
initScene(options){ initScene(options){
// Scene // Scene
this.scene = new THREE.Scene() this.scene = new THREE.Scene()
Vendored Submodule
+1
Submodule app/thirdparty/buildoz added at 9690401dad
-206
View File
@@ -1,206 +0,0 @@
bz-select {
display: block;
margin: .5em 0 .5em 0;
position:relative;
}
bz-select[disabled] > button{
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
filter: grayscale(30%);
}
bz-select > button{
width:100%;
text-align: left;
font-family: sans;
font-size: .9em;
border-radius: 1em;
border: none;
padding: 0.2em .2em .3em .5em;
background: linear-gradient( to bottom, #555, #aaa 15%, #ccc 50%, #aaa 85%, #555 );
}
bz-select > button::after {
content: "\00BB";
transform: rotate(90deg);
position: absolute;
right: 0.5em;
top: 0;
pointer-events: none;
font-size: 1.5em;
color: #444;
}
bz-select > div.options-container{
pointer-events: none;
position: absolute;
top: 1.8em;
left: 0;
width: 100%;
z-index: 99;
max-height: 0;
overflow: auto;
transition: max-height 0.4s ease;
}
bz-select > div.options-container.open{ pointer-events: auto; max-height: 10em;}
bz-select option{
background-color: #DDD;
border: 1px solid black;
color: #000;
padding: 0.2em .2em .5em .5em;
margin: -1em 0 0 0;
border-radius: 1em;
height: 1em;
font-family: sans;
font-size: .9em;
opacity: 0;
pointer-events: none;
transition:
margin-top 0.3s ease,
opacity 0.3s ease;
}
bz-select option.open{
margin: 0;
opacity: 1;
pointer-events: auto;
}
bz-select option:hover{
background-color: #44F;
color: #FFF;
}
/************************************************************************************/
bz-toggler{
outline: none;
min-height: 1.5em;
padding: 0.75em;
display: inline-table;
max-width: fit-content;
transition: all 0.5s;
}
bz-toggler div.toggle-label-left{
padding-right: 0.3em;
display: inline;
vertical-align: middle;
text-align: right;
font-size:.8em;
}
bz-toggler div.toggle-label-right{
padding-left: 0.3em;
display: inline;
vertical-align: middle;
text-align: left;
font-size:.8em;
}
bz-toggler div.toggle-switch{
user-select: none;
height: inherit;
cursor: pointer;
display: inline;
vertical-align: middle;
text-align: center;
}
bz-toggler div.toggle-switch span.toggle-bar {
display: inline-block;
position: relative;
background-color: #CCC;
}
bz-toggler div.toggle-switch span.toggle-thumb {
display: inline-block;
border-radius: 50%;
background-color: white;
position: absolute;
z-index: 1;
}
bz-toggler div.toggle-switch span.toggle-thumb:not(.turned-on) {
left: 0;
transition: all 0.4s;
}
bz-toggler div.toggle-switch span.toggle-thumb.turned-on {
transition: all 0.4s;
}
bz-toggler div.toggle-switch span.toggle-bar {
width: 2em;
height: 0.75em;
border-radius: .75em;
}
bz-toggler div.toggle-switch span.toggle-thumb {
height: 1em;
width: 1em;
top: 50%;
transform: translateY(-50%);
}
bz-toggler div.toggle-switch span.toggle-thumb.turned-on {
left : 1em;
}
/************************************************************************************/
bz-slidepane {
display: block;
position: absolute;
background-color: #000A;
}
bz-slidepane[side="top"] { top:0; left:0; width: 100%; height:0; border-bottom: 2px solid #DDD; }
bz-slidepane[side="bottom"] { bottom:0; left:0; width: 100%; height:0; border-top: 2px solid #DDD;}
bz-slidepane[side="left"] { top:0; left:0; height:100%; width:0; border-right: 2px solid #DDD;}
bz-slidepane[side="right"] { top:0; right:0; height:100%; width:0; border-left: 2px solid #DDD; }
bz-slidepane[side="top"] div.handle {
position: absolute;
bottom: -12px;
left: 50%;
width: 40px;
height: 11px;
background: repeating-linear-gradient( to top, rgba(255,255,255,1) 0, rgba(255,255,255,1) 2px, rgba(0,0,0,0.2) 3px, rgba(0,0,0,0.2) 4px );
transform: translateX(-50%);
cursor: ns-resize;
}
bz-slidepane[side="bottom"] div.handle {
position: absolute;
top: -12px;
left: 50%;
width: 40px;
height: 11px;
background: repeating-linear-gradient( to bottom, rgba(255,255,255,1) 0, rgba(255,255,255,1) 2px, rgba(0,0,0,0.2) 3px, rgba(0,0,0,0.2) 4px );
transform: translateX(-50%);
cursor: ns-resize;
}
bz-slidepane[side="left"] div.handle {
position: absolute;
right: -12px;
top: 50%;
width: 11px;
height: 40px;
background: repeating-linear-gradient( to left, rgba(255,255,255,1) 0, rgba(255,255,255,1) 2px, rgba(0,0,0,0.2) 3px, rgba(0,0,0,0.2) 4px );
transform: translateY(-50%);
cursor: ew-resize;
}
bz-slidepane[side="right"] div.handle {
position: absolute;
left: -12px;
top: 50%;
width: 11px;
height: 40px;
background: repeating-linear-gradient( to right, rgba(255,255,255,1) 0, rgba(255,255,255,1) 2px, rgba(0,0,0,0.2) 3px, rgba(0,0,0,0.2) 4px );
transform: translateY(-50%);
cursor: ew-resize;
}
/************************************************************************************/
bz-graflow {
position: relative;
display: block;
width: 100vw;
height: 50vh;
box-sizing: border-box;
}
bz-graflow .bzgf-main-container{
width: 100%;
height: 100%;
position: relative;
box-sizing: border-box;
}
-332
View File
@@ -1,332 +0,0 @@
class Buildoz extends HTMLElement {
constructor(){
super() // always call super() first!
this.attrs = {}
}
static get observedAttributes(){ //observable attributes triggering attributeChangedCallback
// anything added here will be observed for all buildoz tags
// in your child, add you local 'color' observable attr with :
// return([...super.observedAttributes, 'color'])
return(['name'])
}
static define(name, cls){
const tag = `bz-${name}`
if(!customElements.get(tag)) { // no wild redefinition
customElements.define(tag, cls)
}
return cls
}
connectedCallback(){ // added to the DOM
this.classList.add('buildoz')
}
disconnectedCallback(){ // removed from the DOM
}
attributeChangedCallback(name, oldValue, newValue) {
this.attrs[name] = newValue
if(name=='name') this.name = newValue
}
getBZAttribute(attrName){ // Little helper for defaults
return(this.getAttribute(attrName) || this.defaultAttrs[attrName] )
}
}
class BZselect extends Buildoz {
#value
#fillFromMarkup = true
constructor(){
super()
this.value = null
this.open = false
this.value = null
this.generalClickEvent = null
this.defaultAttrs = {
label: 'Select...',
}
}
connectedCallback() {
super.connectedCallback()
this.button = document.createElement('button')
this.button.textContent = this.getBZAttribute('label')
this.prepend(this.button)
this.button.addEventListener('click', this.toggle.bind(this))
if(!this.optionscontainer) this.optionscontainer = document.createElement('div')
this.optionscontainer.classList.add('options-container')
this.append(this.optionscontainer)
this.options = this.querySelectorAll('option')
if(this.#fillFromMarkup){ //can only do it once and only if fillOptions was not already called !!
for(const opt of this.options){
this.optionscontainer.append(opt) // Will move is to the right parent
opt.addEventListener('click', this.onClick.bind(this))
if(opt.getAttribute('selected') !== null) this.onOption(opt.value, true)
}
this.#fillFromMarkup = false
}
}
// static get observedAttributes(){ // Only if you want actions on attr change
// return([...super.observedAttributes, 'disabled'])
// }
// attributeChangedCallback(name, oldValue, newValue) {
// super.attributeChangedCallback(name, oldValue, newValue)
// }
get value(){ return(this.#value) }
set value(v) {
this.#value = v
if(this.options && this.options.length>0) {
const opt = Array.from(this.options).find(opt => opt.value==v)
if(v || (opt && opt.textContent)) this.button.textContent = opt.textContent
else this.button.textContent = this.getBZAttribute('label')
this.dispatchEvent(new Event('change', {
bubbles: true,
composed: false,
cancelable: false
}))
}
}
toggle(){
for(const opt of this.options){
if(this.open) {
opt.classList.remove('open')
this.optionscontainer.classList.remove('open')
} else {
document.querySelectorAll('bz-select').forEach((sel) => {
if((sel!==this) && sel.open) sel.toggle()
})
opt.classList.add('open')
this.optionscontainer.classList.add('open')
}
}
this.open = !this.open
}
onClick(evt){
evt.stopPropagation()
const opt = evt.target.closest('option')
if(opt && opt.value) this.onOption(opt.value)
}
onOption(value, silent=false){
if(this.getAttribute('disabled') !== null) return
this.value = value
if(!silent) this.toggle()
}
addOption(value, markup){
// Caution: you cannot count on connectedCallback to have run already, because one might fill before adding to the DOM
const opt = document.createElement('option')
opt.setAttribute('value', value)
opt.innerHTML = markup
opt.addEventListener('click',this.onClick.bind(this))
if(!this.optionscontainer) this.optionscontainer = document.createElement('div')
this.optionscontainer.append(opt)
this.options = this.querySelectorAll('option')
this.#fillFromMarkup = false
}
fillOptions(opts, erase = true){
// Caution: you cannot count on connectedCallback to have run already, because one might fill before adding to the DOM
if(erase){
this.options = this.querySelectorAll('option')
this.options.forEach(node => { node.remove() })
this.options = this.querySelectorAll('option')
this.onOption('', true) // unselect last
}
for(const opt of opts) this.addOption(opt.value, opt.markup)
}
}
Buildoz.define('select', BZselect)
class BZtoggler extends Buildoz {
#value
constructor(){
super()
this.open = false
this.defaultAttrs = {
labelLeft:'',
labelRight:'',
trueValue: 'yes',
falseValue: 'no',
classOn:'',
classOff:'',
tabindex:0,
disabled:false
}
}
connectedCallback(){
super.connectedCallback()
this.labelRight = document.createElement('div')
this.labelRight.classList.add('toggle-label-right')
this.labelRight.innerHTML = this.getBZAttribute('labelRight')
this.labelLeft = document.createElement('div')
this.labelLeft.classList.add('toggle-label-left')
this.labelLeft.innerHTML = this.getBZAttribute('labelLeft')
this.switch = document.createElement('div')
this.switch.classList.add('toggle-switch')
this.toggleBar = document.createElement('span')
this.toggleBar.classList.add('toggle-bar')
this.thumb = document.createElement('span')
this.thumb.classList.add('toggle-thumb')
this.toggleBar.append(this.thumb)
this.switch.append(this.toggleBar)
this.appendChild(this.labelLeft)
this.appendChild(this.switch)
this.appendChild(this.labelRight)
this.setAttribute('tabindex', this.getBZAttribute('tabindex'))
this.switch.addEventListener('click', this.toggle.bind(this))
}
turnOn() {
if(this.getBZAttribute('classOff')) this.switch.classList.remove(this.getBZAttribute('classOff'))
if(this.getBZAttribute('classOn')) this.switch.classList.add(this.getBZAttribute('classOn'))
this.thumb.classList.add('turned-on')
this.#value = this.getBZAttribute('trueValue')
this.focus()
}
turnOff() {
if(this.getBZAttribute('classOn')) this.switch.classList.remove(this.getBZAttribute('classOn'))
if(this.getBZAttribute('classOff'))this.switch.classList.add(this.getBZAttribute('classOff'))
this.thumb.classList.remove('turned-on')
this.#value = this.getBZAttribute('falseValue')
this.focus()
}
toggle(event) {
if(event) { event.preventDefault(); event.stopPropagation(); }
if(this.getBZAttribute('disabled')) return
if(this.#value == this.getBZAttribute('trueValue')) this.turnOff()
else this.turnOn()
this.dispatchEvent(new Event('change', {
bubbles: true,
composed: false,
cancelable: false
}))
}
get value(){ return(this.#value) }
set value(v) {
this.#value = v
if(this.defaultAttrs){
if(this.#value == this.getBZAttribute('trueValue')) this.turnOn()
else this.turnOff()
this.dispatchEvent(new Event('change', {
bubbles: true,
composed: false,
cancelable: false
}))
}
}
}
Buildoz.define('toggler', BZtoggler)
class BZslidePane extends Buildoz {
constructor(){
super()
this.open = false
this.defaultAttrs = {
side: 'bottom'
}
this.dragMove = this.dragMove.bind(this)
this.dragEnd = this.dragEnd.bind(this)
// Fill with innerHTML or other DOM manip should not allow coating to be removed
this._observer = new MutationObserver(muts => { this.coat() })
}
connectedCallback(){
super.connectedCallback()
this.coat()
this._observer.observe(this, { childList: true }) // Do this last
}
disconnectedCallback() {
super.disconnectedCallback()
this._observer.disconnect()
}
coat(){
if(this.handle && this.querySelector(this.dispatchEvent.handle)) return
this._observer.disconnect()
if(this.querySelector(this.dispatchEvent.handle)) this.querySelector(this.dispatchEvent.handle).remove()
this.handle = document.createElement('div')
this.handle.classList.add('handle')
this.prepend(this.handle)
this.handle.addEventListener('pointerdown', this.dragStart.bind(this))
this._observer.observe(this, { childList: true })
}
dragStart(evt){
evt.target.setPointerCapture(evt.pointerId)
this.dragStartX = evt.clientX
this.dragStartY = evt.clientY
this.handle.addEventListener('pointermove', this.dragMove)
this.handle.addEventListener('pointerup', this.dragEnd)
}
dragMove(evt){
const box = this.getBoundingClientRect()
const parentBox = this.parentElement.getBoundingClientRect()
let width, height
switch(this.getAttribute('side')){
case 'top':
height = (evt.clientY > box.top) ? (evt.clientY - box.top) : 0
if(height>(parentBox.height/2)) height = Math.floor(parentBox.height/2)
this.style.height = height+'px'
break
case 'bottom':
height = (evt.clientY < box.bottom) ? (box.bottom - evt.clientY) : 0
if(height>(parentBox.height/2)) height = Math.floor(parentBox.height/2)
this.style.height = height+'px'
break
case 'left':
width = (evt.clientX > box.left) ? (evt.clientX - box.left) : 0
if(width>(parentBox.width/2)) width = Math.floor(parentBox.width/2)
this.style.width = width+'px'
break
case'right':
width = (evt.clientX < box.right) ? (box.right - evt.clientX) : 0
if(width>(parentBox.width/2)) width = Math.floor(parentBox.width/2)
this.style.width = width+'px'
break
}
}
dragEnd(evt){
evt.target.releasePointerCapture(evt.pointerId)
this.handle.removeEventListener('pointermove', this.dragMove)
this.handle.removeEventListener('pointerup', this.dragEnd)
}
}
Buildoz.define('slidepane', BZslidePane)
-610
View File
@@ -1,610 +0,0 @@
class BZgraflow extends Buildoz{
constructor(){
super()
this.defaultAttrs = { tension: 100 }
this.stagedNodes = { }
this.stagedWires = { }
this.arrowDefs = null
}
static _loadedNodeStyles = new Set() // Allow multi instances or re-loadNodes, but avoid reinjecting same styles !
connectedCallback() {
super.connectedCallback()
const flowUrl = this.getBZAttribute('flow')
if(!flowUrl) {
console.warn('BZgraflow: No flow URL !?')
return
}
this.mainContainer = document.createElement('div')
this.mainContainer.classList.add('bzgf-main-container')
this.shadow = this.mainContainer.attachShadow({ mode: 'open' })
const style = document.createElement('style')
style.textContent = `
.bzgf-wires-container,
.bzgf-nodes-container{ position: absolute; inset: 0; width: 100%; height: 100%; }
.bzgf-nodes-container .bzgf-node{ position:absolute; }
.bzgf-nodes-container .bzgf-fake-node{
position: absolute;
width: 5px;
height: 5px;
backgrround: transparent;
border-style: none;
}
`
this.shadow.appendChild(style)
this.nodesContainer = document.createElement('div')
this.nodesContainer.classList.add('bzgf-nodes-container')
this.wiresContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
this.wiresContainer.setAttribute('overflow','visible')
this.wiresContainer.classList.add('bzgf-wires-container')
this.shadow.append(this.wiresContainer)
this.shadow.append(this.nodesContainer)
this.append(this.mainContainer)
this.loadFlow(flowUrl) // Let it load async
}
error(msg, err){
this.innerHTML = `<div style="background:red;color:black;margin: auto;width: fit-content;">${msg}</div>`
if(err) console.error(msg, err)
else console.error(msg)
}
async loadFlow(url){
const res = await fetch(url+'?'+crypto.randomUUID())
const buf = await res.text()
let flowObj
try{
flowObj = JSON.parse(buf)
} catch(err){
this.error('Could not parse flow JSON!?', err)
return
}
if(!flowObj.nodesFile){
this.error('No nodesFile in JSON!?')
return
}
await this.loadNodes(flowObj.nodesFile)
this.flow = flowObj.flow
this.refresh()
}
async loadNodes(url) {
const res = await fetch(url+'?'+crypto.randomUUID())
const html = await res.text()
// Get nodes
const doc = new DOMParser().parseFromString(html, 'text/html')
this.nodesRegistry = {}
for(const tpl of doc.querySelectorAll('template')){
if(tpl.id=='svg-arrows'){
this.arrowDefs = tpl.querySelector('defs').cloneNode(true)
this.wiresContainer.appendChild(this.arrowDefs)
} else {
const rootEl = tpl.content.querySelector('.bzgf-node')
if(!rootEl) continue
this.nodesRegistry[rootEl.dataset.nodetype] = rootEl
}
}
// Now load styles (once)
if(!BZgraflow._loadedNodeStyles.has(url)) {
const styles = doc.querySelectorAll('style')
styles.forEach(styleEl => {
const style = document.createElement('style')
style.textContent = styleEl.textContent
this.shadow.appendChild(style)
})
BZgraflow._loadedNodeStyles.add(url)
}
}
addNode(node){
const id = node.id
if(!(node.nodeType in this.nodesRegistry)){ console.warn(`Unknown node type (${node.nodeType})`); return(null)}
const nodeDef = this.nodesRegistry[node.nodeType]
this.stagedNodes[id] = nodeDef.cloneNode(true)
for(const token in node.markup){
this.stagedNodes[id].innerHTML = this.stagedNodes[id].innerHTML.replace(new RegExp(`\{${token}\}`, 'g'), node.markup[token])
}
if(typeof(node.data)=='object') Object.assign(this.stagedNodes[id].dataset, node.data)
const portEls = this.stagedNodes[id].querySelectorAll('.port')
this.stagedNodes[id].style.left = (node.coords && node.coords.x) ? `${node.coords.x}px` : '10%'
this.stagedNodes[id].style.top = (node.coords && node.coords.y) ? `${node.coords.y}px` : '10%'
this.stagedNodes[id].dataset.id = id
this.stagedNodes[id].ports = Object.fromEntries(Array.from(portEls).map(item => ([item.dataset.id, { ...item.dataset, el:item }])))
this.nodesContainer.append(this.stagedNodes[id])
return(this.stagedNodes[id])
}
addFakeNode(nid, x, y, w, h){
this.stagedNodes[nid] = document.createElement('div')
this.stagedNodes[nid].classList.add('bzgf-fake-node')
this.stagedNodes[nid].style.left = `${x}px`
this.stagedNodes[nid].style.top = `${y}px`
this.stagedNodes[nid].style.width = `${w}px`
this.stagedNodes[nid].style.height = `${h}px`
this.stagedNodes[nid].dataset.id = nid
this.nodesContainer.append(this.stagedNodes[nid])
return(this.stagedNodes[nid])
}
addWire(link){
const [idNode1, idPort1] = link.from
const [idNode2, idPort2] = link.to
const path = this.bezierNodes(idNode1, idPort1, idNode2, idPort2, this.getBZAttribute('tension'))
const id = `${idNode1}_${idNode2}`
this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path')
this.stagedWires[id].setAttribute('d', path)
this.stagedWires[id].setAttribute('fill', 'none')
if(this.arrowDefs && link.endArrow) this.stagedWires[id].setAttribute('marker-end','url(#arrow)')
if(this.arrowDefs && link.startArrow) this.stagedWires[id].setAttribute('marker-start','url(#arrow)')
this.stagedWires[id].classList.add('bzgf-wire')
this.stagedWires[id].dataset.id = id
this.wiresContainer.append(this.stagedWires[id])
return(this.stagedWires[id])
}
clear(){
this.nodesContainer.innerHTML = ''
this.wiresContainer.innerHTML = ''
if(this.arrowDefs) this.wiresContainer.appendChild(this.arrowDefs)
}
refresh(){
this.clear()
let forceAutoplace = false
for(const node of this.flow.nodes){
if((!node.coords) || (!node.coords.x) ||(!node.coords.y)) forceAutoplace=true
this.addNode(node)
}
for(const link of this.flow.links){
this.addWire(link)
}
if(forceAutoplace){
const bb=this.getBoundingClientRect()
//TODO compute tensions from ports
if(bb.width > bb.height) this.autoPlace('horizontal', 80, 30, 500)
else this.autoPlace('vertical', 80, 30, 200)
}
}
bezierNodes(idNode1, idPort1, idNode2, idPort2, tensionMin=60) {
const svgRect = this.wiresContainer.getBoundingClientRect()
const node1 = this.stagedNodes[idNode1]
const port1 = node1.ports[idPort1]
const node2 = this.stagedNodes[idNode2]
const port2 = node2.ports[idPort2]
if(!node1 || !node2 || !port1 || !port2) {
console.warn('Link on bad node / port ', idNode1, idPort1, idNode2, idPort2)
return('')
}
const bb1 = port1.el.getBoundingClientRect()
const bb2 = port2.el.getBoundingClientRect()
const x1 = Math.floor(bb1.x + (bb1.width/2)) - svgRect.left
const y1 = Math.floor(bb1.y + (bb1.height/2)) - svgRect.top
const x2 = Math.floor(bb2.x + (bb2.width/2)) - svgRect.left
const y2 = Math.floor(bb2.y + (bb2.height/2)) - svgRect.top
const loop = (idNode1==idNode2) && (idPort1==idPort2)
const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1)
let tension = dist * 0.4
if(tension < tensionMin) tension = parseInt(tensionMin)
const dirVect = {
n: { x: 0, y: -1 },
s: { x: 0, y: 1 },
e: { x: 1, y: 0 },
w: { x: -1, y: 0 },
}
let c1x = x1 + (dirVect[port1.direction].x * tension)
let c1y = y1 + (dirVect[port1.direction].y * tension)
let c2x = x2 + (dirVect[port2.direction].x * tension)
let c2y = y2 + (dirVect[port2.direction].y * tension)
if(loop){
if(['n', 's'].includes(port1.direction)) {
c1x += tension
c2x -= tension
}
if(['e', 'w'].includes(port1.direction)){
c1y += 1*tension
c2y -= 1*tension
}
}
return(`M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`)
}
bezierInterNodes(idNode1, idPort1, idNode2, idPort2, interNodes, orientation='horizontal', tensionMin=60) {
const svgRect = this.wiresContainer.getBoundingClientRect()
const makeCubicBezier = (x1, y1, x2, y2, orientation) => {
const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1)
let tension = dist * 0.4
if(tension < tensionMin) tension = parseInt(tensionMin)
let c1x, c1y, c2x, c2y
if(orientation=='horizontal'){
c1x = x1 + tension
c1y = y1
c2x = x2 - tension
c2y = y2
} else {
c1x = x1
c1y = y1 + tension
c2x = x2
c2y = y2 - tension
}
return(`C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`)
}
const directPath = this.bezierNodes(idNode1, idPort1, idNode2, idPort2, tensionMin)
const startPath = directPath.substring(0,directPath.indexOf('C'))
const endPath = directPath.substring(directPath.lastIndexOf(',')+1).trim()
let path = startPath
let [ , x1, y1] = startPath.split(' ')
x1 = parseInt(x1)
y1 = parseInt(y1)
for(const interNode of interNodes){
const bb = this.stagedNodes[interNode].getBoundingClientRect()
let x2; let y2;
if(orientation=='horizontal'){
x2 = bb.x -svgRect.left
y2 =Math.floor(bb.y + (bb.height/2)) - svgRect.top
path += makeCubicBezier(x1, y1, x2, y2, orientation)
x2 += bb.width
path += ` L ${x2} ${y2} `
} else {
x2 = Math.floor(bb.x + (bb.width/2)) - svgRect.left
y2 = bb.y - svgRect.top
path += makeCubicBezier(x1, y1, x2, y2, orientation)
y2 += bb.height
path += ` L ${x2} ${y2} `
}
x1 = x2
y1 = y2
}
let [x2, y2] = endPath.split(' ')
x2 = parseInt(x2)
y2 = parseInt(y2)
path += ' '+makeCubicBezier(x1, y1, x2, y2, orientation)
return(path)
}
autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=1000){
// Loops create infinite recursion in dfs for getting parents & adjacency lists: Remove them !
let linksWithoutBackEdges
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
console.warn('Loop(s) detected... Cannot auto-place !')
const backEdges = this.findBackEdges(this.flow.nodes, this.flow.links)
linksWithoutBackEdges = this.flow.links.filter((link, idx) => (!backEdges.includes(idx)) && (link.from[0] != link.to[0]))
} else {
linksWithoutBackEdges = this.flow.links
}
const { parents, adj } = this.buildGraphStructures(this.flow.nodes, linksWithoutBackEdges)
const layers = this.computeLayers(this.flow.nodes, parents)
// Compute indexes for each layer (int part) & add sub-index for ports
// Also compute max width/height for each layer
let maxHeight = 0; let maxWidth = 0
const layerHeights = []; const layerWidths = [];
const indexes = {} // indexes[nid] = { base: <int>, ports: { [portId]: <float in [0,1)> } }
for(const layer of layers){
let totHeight = 0; let totWidth = 0
for(const [idx, nid] of layer.entries()){
const bb = this.stagedNodes[nid].getBoundingClientRect()
totHeight += bb.height + gapy
totWidth += bb.width + gapx
indexes[nid] = { base: idx, ports: this.computePortOffsets(nid, orientation) }
}
if(totHeight>maxHeight) maxHeight = totHeight
layerHeights.push(totHeight)
if(totWidth>maxWidth) maxWidth = totWidth
layerWidths.push(totWidth)
}
// If any long-links, create placeholders for skipped layers
this.flow.longLinks = this.findLongLinks(this.flow.links)
for(const link of this.flow.longLinks){
for(const layerIdx of link.skippedLayers){
const nid = `longLinkPlaceHolder_${crypto.randomUUID()}`
layers[layerIdx].push(nid)
link.interNodes.push(nid)
}
}
// Reorder layers to avoid crossings thanks to indexes
this.reorderLayers(layers, parents, indexes, orientation)
// Finally place everything
if(orientation=='horizontal'){
let x = gapx
for(const [idx, layer] of layers.entries()){
let wMax = this.getMaxWidth(layer)
let y = ((maxHeight - layerHeights[idx]) / 2) + gapy
for(const nid of layer){
if(!nid.startsWith('longLinkPlaceHolder_')) {
const bb = this.stagedNodes[nid].getBoundingClientRect()
this.moveNode(nid, x, y, orientation, tween)
y += gapy + bb.height
} else {
this.addFakeNode(nid, x, y, wMax, 10)
this.moveNode(nid, x, y, orientation, tween)
y += gapy + 10 //TODO
}
}
x += wMax + gapx
}
} else if(orientation=='vertical'){
let y = gapy
for(const [idx, layer] of layers.entries()){
let hMax = this.getMaxHeight(layer)
let x = ((maxWidth - layerWidths[idx]) / 2) + gapx
for(const nid of layer){
if(!nid.startsWith('longLinkPlaceHolder_')){
const bb = this.stagedNodes[nid].getBoundingClientRect()
this.moveNode(nid, x, y, orientation, tween)
x += gapx + bb.width
} else {
this.addFakeNode(nid, x, y, 10, hMax)
this.moveNode(nid, x, y, orientation, tween)
x += gapx + 10 //TODO
}
}
y += hMax + gapy
}
}
}
getMaxWidth(layer){
return(layer.filter(nid =>
!nid.startsWith('longLinkPlaceHolder_'))
.map(nid => this.stagedNodes[nid].getBoundingClientRect().width)
.reduce((a, b) => a > b ? a : b, 0)
)
}
getMaxHeight(layer){
return(layer.filter(nid =>
!nid.startsWith('longLinkPlaceHolder_'))
.map(nid => this.stagedNodes[nid].getBoundingClientRect().height)
.reduce((a, b) => a > b ? a : b, 0)
)
}
computePortOffsets(nid, orientation = 'horizontal'){
const node = this.stagedNodes[nid]
if(!node || !node.ports) return({})
const axis = (orientation === 'vertical') ? 'x' : 'y'
const nodeRect = node.getBoundingClientRect()
const ports = Object.entries(node.ports)
.map(([pid, p]) => {
const r = p.el.getBoundingClientRect()
const pos = (axis === 'x')
? (r.left + (r.width / 2) - nodeRect.left)
: (r.top + (r.height / 2) - nodeRect.top)
return({ pid, pos })
})
.sort((a, b) => a.pos - b.pos) // smaller pos => "higher/left" => smaller offset
const denom = ports.length + 1
const offsets = {}
for(const [rank, item] of ports.entries()){
offsets[item.pid] = rank / denom // always < 1
}
return(offsets)
}
reorderLayers(layers, parents, indexes, orientation = 'horizontal'){
const swap = (vect, todo) => {
for(const s of todo){
[vect[s[0]], vect[s[1]]] = [vect[s[1]], vect[s[0]]]
}
}
const adjIndex = (nid, portId) => {
const info = indexes?.[nid]
const base = (info && info.base !== undefined) ? info.base : 0
const off = (portId && info?.ports?.[portId] !== undefined) ? info.ports[portId] : 0
return(base + off)
}
for(const [lidx, layer] of layers.entries()){
if(lidx==0) continue
const toSwap = []
for(let i=0; i<layer.length; i++){
const nid1 = layer[i]
if(nid1.startsWith('longLinkPlaceHolder_')) continue
// some parents can be in far layers, but at least one is in the prev layer (by definition of layer)
const pnid1 = parents[nid1].find((nid => layers[lidx-1].includes(nid)))
for(let j=i+1; j<layer.length; j++){
const nid2 = layer[j]
if(nid2.startsWith('longLinkPlaceHolder_')) continue
const pnid2 = parents[nid2].find((nid => layers[lidx-1].includes(nid)))
const link1 = (pnid1) ? this.getLink(pnid1, nid1) : null
const link2 = (pnid2) ? this.getLink(pnid2, nid2) : null
const p1 = adjIndex(pnid1, link1?.from?.[1])
const p2 = adjIndex(pnid2, link2?.from?.[1])
const c1 = adjIndex(nid1, link1?.to?.[1])
const c2 = adjIndex(nid2, link2?.to?.[1])
if(((p1 - p2) * (c1 - c2)) < 0) { // crossing (now refined by per-port ordering)
toSwap.push([i, j])
}
}
}
swap(layer, toSwap)
}
}
moveNode(nid, destx, desty, orientation, duration = 200, cb) {
const t0 = performance.now()
const bb = this.stagedNodes[nid].getBoundingClientRect()
const parentbb = this.stagedNodes[nid].parentElement.getBoundingClientRect()
const x0=bb.x - parentbb.x
const y0 = bb.y - parentbb.y
function frame(t) {
const p = Math.min((t - t0) / duration, 1)
const k = p * p * (3 - 2 * p) // smoothstep
const x = x0 + (destx - x0) * k
const y = y0 + (desty - y0) * k
this.stagedNodes[nid].style.left = `${x}px`
this.stagedNodes[nid].style.top = `${y}px`
this.updateWires(nid, orientation)
if(p < 1) requestAnimationFrame(frame.bind(this))
}
requestAnimationFrame(frame.bind(this))
}
updateWires(nid, orientation){
const wires = Object.keys(this.stagedWires)
.filter(id => (id.startsWith(nid+'_')||id.endsWith('_'+nid)))
.map(id => this.stagedWires[id])
for(const wire of wires){
const [nid1, nid2] = wire.dataset.id.split('_')
const lnk = this.getLink(nid1, nid2)
const longLink = this.flow.longLinks.find(item => (item.link.from[0] == lnk.from[0] && item.link.from[1] == lnk.from[1] && item.link.to[0] == lnk.to[0] && item.link.to[1] == lnk.to[1]))
if(longLink) {
const path = this.bezierInterNodes(nid1, lnk.from[1], nid2, lnk.to[1], longLink.interNodes, orientation, this.getBZAttribute('tension'))
wire.setAttribute('d', path)
} else {
const path = this.bezierNodes(nid1, lnk.from[1], nid2, lnk.to[1], this.getBZAttribute('tension'))
wire.setAttribute('d', path)
}
}
}
getLink(nid1, nid2){
return(this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2))))
}
buildGraphStructures(nodes, links, includeLinkIndexes = false) {
const parents = {}
const adj = {}
nodes.forEach(n => {
parents[n.id] = []
adj[n.id] = []
})
links.forEach((link, idx) => {
const from = link.from[0]
const to = link.to[0]
if(link.from[0] !== link.to[0]) { // Skip self-loops
parents[to].push(from)
if(includeLinkIndexes) {
adj[from].push({ to, linkIdx: idx })
} else {
adj[from].push(to)
}
}
})
return({ parents, adj })
}
computeLayers(nodes, parents) {
const layer = {}
const dfs = (id) => {
if (layer[id] !== undefined) return(layer[id])
if (parents[id].length === 0) {
layer[id] = 0
} else {
layer[id] = 1 + Math.max(...parents[id].map(dfs))
}
return(layer[id])
}
nodes.forEach(n => dfs(n.id))
const t = []
for(const nid in layer) {
if(!t[layer[nid]]) t[layer[nid]]=[]
t[layer[nid]].push(nid)
}
return(t)
}
hasAnyLoop(nodes, links) {
if(links.some(l => l.from[0] === l.to[0])) return(true) // self-loops
const { adj } = this.buildGraphStructures(nodes, links)
const visiting = new Set();
const visited = new Set()
const dfs = (nid) => {
if(visiting.has(nid)) {
return(true)
}
if(visited.has(nid)) return(false)
visiting.add(nid)
for(const m of adj[nid]) {
if(dfs(m)) {
return(true)
}
}
visiting.delete(nid)
visited.add(nid)
return(false)
}
return(nodes.map(n => n.id).some(dfs))
}
findBackEdges(nodes, links) {
const { adj } = this.buildGraphStructures(nodes, links, true)
const color = {}; nodes.forEach(n => color[n.id] = 'white')
const backEdges = []
function dfs(u) {
color[u] = 'gray'
for (const neighbor of adj[u]) {
const v = neighbor.to
const linkIdx = neighbor.linkIdx
if (color[v] === 'gray') {
backEdges.push(linkIdx) // 👈 cycle edge - return link index
} else if (color[v] === 'white') {
dfs(v)
}
}
color[u] = 'black'
}
nodes.forEach(n => {
if (color[n.id] === 'white') dfs(n.id)
})
return(backEdges)
}
findLongLinks(links) {
let linksWithoutBackEdges
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
console.warn('Loop(s) detected... Cannot auto-place !')
const backEdges = this.findBackEdges(this.flow.nodes, this.flow.links)
linksWithoutBackEdges = this.flow.links.filter((link, idx) => (!backEdges.includes(idx)) && (link.from[0] != link.to[0]))
} else {
linksWithoutBackEdges = this.flow.links
}
/// Yes that means we ignore long & back links !
const { parents } = this.buildGraphStructures(this.flow.nodes, linksWithoutBackEdges)
const layers = this.computeLayers(this.flow.nodes, parents)
const crossLayerLinks = []
for(const link of links){
const from = link.from[0]
const to = link.to[0]
const idx1 = layers.findIndex(layer => layer.includes(from))
const idx2 = layers.findIndex(layer => layer.includes(to))
if(Math.abs(idx1-idx2)>1) {
const lowerIdx = idx1<idx2 ? idx1 : idx2
const higherIdx = idx1>idx2 ? idx1 : idx2
crossLayerLinks.push({
link,
linkIdx: link.linkIdx,
interNodes: [],
skippedLayers: Array.from({ length: higherIdx - lowerIdx - 1 }, (_, i) => lowerIdx + i + 1),
})
}
}
return(crossLayerLinks)
}
}
Buildoz.define('graflow', BZgraflow)
+5 -4
View File
@@ -3369,10 +3369,11 @@ class DataGrid extends EicComponent {
let actions = ui.create(`<div class="cell actions"></div`); let actions = ui.create(`<div class="cell actions"></div`);
if(this.options.rowActions && Array.isArray(this.options.rowActions)) { if(this.options.rowActions && Array.isArray(this.options.rowActions)) {
for(let action of this.options.rowActions) { for(let action of this.options.rowActions) {
let button = ui.create(`<button eicbutton rounded title="${action.title ? action.title: ''}">${action.icon}</button>`); const severityAttr = action.severity ? ` ${action.severity}` : ''
button.addEventListener('click', action.callback.bind(row.getAttribute('data-id') != '' ? JSON.parse(row.getAttribute('data-id')): this)); let button = ui.create(`<button eicbutton rounded${severityAttr} title="${action.title ? action.title: ''}">${action.icon}</button>`)
actions.appendChild(button); button.addEventListener('click', action.callback.bind(row.getAttribute('data-id') != '' ? JSON.parse(row.getAttribute('data-id')): this))
actions.appendChild(button)
} }
} }
@@ -1,3 +1,15 @@
/**
* _ ___ Another
* / |/ (_)______ __ _____
* / / / __(_-</ // (_-<
* /_/|_/_/\__/___/\_, /___/
* /___/
* production !
*
* Licensed under the MIT License:
* This code is free to use and modify,
* as long as the copyright notice and license are kept.
*/
class ProfilePreferencesResetDialog extends WindozDialogContent { class ProfilePreferencesResetDialog extends WindozDialogContent {
actions = [ actions = [
{ {
+12
View File
@@ -1,3 +1,15 @@
/**
* _ ___ Another
* / |/ (_)______ __ _____
* / / / __(_-</ // (_-<
* /_/|_/_/\__/___/\_, /___/
* /___/
* production !
*
* Licensed under the MIT License:
* This code is free to use and modify,
* as long as the copyright notice and license are kept.
*/
class myProfileView extends WindozDomContent { class myProfileView extends WindozDomContent {
DOMContentLoaded() { DOMContentLoaded() {
+80 -17
View File
@@ -1,8 +1,20 @@
/**
* _ ___ Another
* / |/ (_)______ __ _____
* / / / __(_-</ // (_-<
* /_/|_/_/\__/___/\_, /___/
* /___/
* production !
*
* Licensed under the MIT License:
* This code is free to use and modify,
* as long as the copyright notice and license are kept.
*/
class KeyframeView extends WindozDomContent { class KeyframeView extends WindozDomContent {
constructor() { constructor() {
super() super()
Object.assign(this, app.helpers.activeAttributes, app.helpers.formBuilder, app.helpers.kfConsole) Object.assign(this, app.helpers.activeAttributes, app.helpers.formBuilder, app.helpers.kfConsole, app.helpers.basicDialogs)
} }
DOMContentFocused(options) { DOMContentFocused(options) {
@@ -56,14 +68,43 @@ class KeyframeView extends WindozDomContent {
this.kfArena = new app.LoadedModules.kfArena(this.outputs.kfArenaCanvas, this.agentSprites) this.kfArena = new app.LoadedModules.kfArena(this.outputs.kfArenaCanvas, this.agentSprites)
this.kfArena.onclickAgent = this.onclickAgent.bind(this) this.kfArena.onclickAgent = this.onclickAgent.bind(this)
this.kfArena.startRendering() this.kfArena.startRendering()
this.output('settingsMenu', app.Assets.Store.html.spaceViewSetting)
this.outputs.settingsMenu.querySelectorAll('input[type="toggler"]').forEach(el => {
const tog = new InputToggler(el)
if(this.kfArena[tog._el.name]?.layers){
tog.value = this.kfArena.camera.layers.test(this.kfArena[tog._el.name].layers) ? 'yes' : 'no'
}
tog.onToggle = this.settingsToggle.bind(this)
})
this.outputs.btnAddAgent.disabled = true this.outputs.btnAddAgent.disabled = true
this.outputs.btnRemoveAgent.disabled = true this.outputs.btnRemoveAgent.disabled = true
this.outputs.btnSaveKF.disabled = true this.outputs.btnSaveKF.disabled = true
this.outputs.btnResetKF.disabled = true
this.outputs.kfName.addEventListener('keyup', this.updateKfButtons.bind(this)) this.outputs.kfName.addEventListener('keyup', this.updateKfButtons.bind(this))
this.currentlySelectedAid = null this.currentlySelectedAid = null
} }
settingsToggle(value, object){
if(['grid','axes'].includes(object._el.name)){
const layerId = {'grid':1,'axes':2}[object._el.name]
if(value=='yes'){
this.kfArena.camera.layers.enable(layerId)
} else {
this.kfArena.camera.layers.disable(layerId)
}
}
}
deselectSceneAgent(){
if(!this.currentlySelectedAid) return
const obj3D = this.kfArena.scene.getObjectByName(this.currentlySelectedAid)
if(obj3D) this.kfArena.clearHighlight3DObj(obj3D, this.kfArena.scene)
this.currentlySelectedAid = null
this.outputs.btnRemoveAgent.disabled = true
}
async onChangeAgent(event){ async onChangeAgent(event){
if(this.outputs.agentsSelector.value) this.agentPreview.setAgent(this.outputs.agentsSelector.value) if(this.outputs.agentsSelector.value) this.agentPreview.setAgent(this.outputs.agentsSelector.value)
if(!this.outputs.agentsSelector.value) return if(!this.outputs.agentsSelector.value) return
@@ -73,36 +114,52 @@ class KeyframeView extends WindozDomContent {
} else { } else {
this.currentAgentType = await this.models.agents.getProperties(this.outputs.agentsSelector.value) this.currentAgentType = await this.models.agents.getProperties(this.outputs.agentsSelector.value)
this.fillAgentProperties('', this.currentAgentType) this.fillAgentProperties('', this.currentAgentType)
// Deselect any on-scene selection this.deselectSceneAgent()
if(this.currentlySelectedAid){
this.kfArena.clearHighlight3DObj(this.kfArena.scene.getObjectByName(this.currentlySelectedAid), this.kfArena.scene)
}
this.currentlySelectedAid = null
} }
} }
async onChangeKeyframe(event){ async onChangeKeyframe(event){
if(!this.outputs.keyframesSelector.value) return if(!this.outputs.keyframesSelector.value) return
let kfData = await this.models.keyframes.getKeyframe(this.outputs.keyframesSelector.value).then(data => data.payload) await this.loadKeyframe(this.outputs.keyframesSelector.value)
}
async onResetKF(evt){
if(!this.currentKeyframe.kfId) return
const kfName = this.currentKeyframe.kfName
const kfId = this.currentKeyframe.kfId
await this.confirmDialog({
title: 'Reset keyframe ?',
message: `<p>You are about to discard all unsaved changes<br>and reload the saved version of "<b>${kfName}</b>".<br><br>
Are you sure?</p>`,
okLabel: 'Reset',
severity: 'warning',
okPromise: async () => {
await this.loadKeyframe(kfId)
}
})
}
async loadKeyframe(kfId){
let kfData = await this.models.keyframes.getKeyframe(kfId).then(data => data.payload)
this.currentKeyframe = { this.currentKeyframe = {
kfId: kfData.info.ekf_uuid, kfId: kfData.info.ekf_uuid,
kfName: kfData.info.ekf_name, kfName: kfData.info.ekf_name,
prevKfId: kfData.info.ekf_prev_uuid, prevKfId: kfData.info.ekf_prev_uuid,
} }
this.outputs.kfName.value = kfData.info.ekf_name this.outputs.kfName.value = kfData.info.ekf_name
this.deselectSceneAgent()
this.kfArena.reloadAgents(kfData.agents) this.kfArena.reloadAgents(kfData.agents)
this.outputs.agentProperties.innerHTML = ''
this.outputs.btnAddAgent.disabled = true
this.updateKfButtons()
} }
onclickAgent(obj3D){ onclickAgent(obj3D){
const aid = obj3D.name const aid = obj3D.name
if(this.currentlySelectedAid == aid){ // Deselect if(this.currentlySelectedAid == aid){ // Deselect
this.kfArena.clearHighlight3DObj(obj3D, this.kfArena.scene) this.deselectSceneAgent()
this.currentlySelectedAid = null
this.outputs.btnRemoveAgent.disabled = true
} else { // Select } else { // Select
if(this.currentlySelectedAid){ this.deselectSceneAgent()
this.kfArena.clearHighlight3DObj(this.kfArena.scene.getObjectByName(this.currentlySelectedAid), this.kfArena.scene)
}
this.currentlySelectedAid = aid this.currentlySelectedAid = aid
this.outputs.btnRemoveAgent.disabled = false this.outputs.btnRemoveAgent.disabled = false
if(this.kfArena.agents[aid]) { if(this.kfArena.agents[aid]) {
@@ -127,6 +184,10 @@ class KeyframeView extends WindozDomContent {
onRemoveAgent(event){ onRemoveAgent(event){
if(!this.currentlySelectedAid) return if(!this.currentlySelectedAid) return
this.kfArena.removeAgent(this.currentlySelectedAid) this.kfArena.removeAgent(this.currentlySelectedAid)
this.currentlySelectedAid = null
this.outputs.btnRemoveAgent.disabled = true
if(this.currentAgentType) this.fillAgentProperties('', this.currentAgentType)
this.updateKfButtons()
} }
async newAgent(aType, AgentValues){ async newAgent(aType, AgentValues){
@@ -139,20 +200,21 @@ class KeyframeView extends WindozDomContent {
updateKfButtons(){ updateKfButtons(){
if((Object.keys(this.kfArena.agents).length > 0) && (this.outputs.kfName.value.length > 5)) { this.outputs.btnSaveKF.disabled = false } if((Object.keys(this.kfArena.agents).length > 0) && (this.outputs.kfName.value.length > 5)) { this.outputs.btnSaveKF.disabled = false }
else { this.outputs.btnSaveKF.disabled = true } else { this.outputs.btnSaveKF.disabled = true }
this.outputs.btnResetKF.disabled = !this.currentKeyframe.kfId
} }
onPropsChanged(evt, comp){ onPropsChanged(evt, comp){
if(this.currentlySelectedAid && this.kfArena.agents[this.currentlySelectedAid]){ if(this.currentlySelectedAid && this.kfArena.agents[this.currentlySelectedAid]){
const AgentValues = this.getFieldsValues('div[data-output="agentProperties"]') const AgentValues = this.getFieldsValues('div[data-output="agentProperties"]')
this.kfArena.agents[this.currentlySelectedAid].values = AgentValues this.kfArena.agents[this.currentlySelectedAid].values = AgentValues
const val = Number.parseInt(comp.value, 10) const val = Number(comp.value)
if((comp.name.startsWith('position.')) && (!Number.isNaN(val))){ if((comp.name.startsWith('position.')) && (Number.isFinite(val))){
this.kfArena.moveAgent(this.currentlySelectedAid, { this.kfArena.moveAgent(this.currentlySelectedAid, {
x: this.getFieldValue('div[data-output="agentProperties"]', 'position.x'), x: this.getFieldValue('div[data-output="agentProperties"]', 'position.x'),
y: this.getFieldValue('div[data-output="agentProperties"]', 'position.y'), y: this.getFieldValue('div[data-output="agentProperties"]', 'position.y'),
z: this.getFieldValue('div[data-output="agentProperties"]', 'position.z'), z: this.getFieldValue('div[data-output="agentProperties"]', 'position.z'),
}) })
} else if((comp.name.startsWith('speed.')) && (!Number.isNaN(val))){ } else if((comp.name.startsWith('speed.')) && (Number.isFinite(val))){
this.kfArena.changeAgentSpeed(this.currentlySelectedAid, { this.kfArena.changeAgentSpeed(this.currentlySelectedAid, {
x: this.getFieldValue('div[data-output="agentProperties"]', 'speed.x'), x: this.getFieldValue('div[data-output="agentProperties"]', 'speed.x'),
y: this.getFieldValue('div[data-output="agentProperties"]', 'speed.y'), y: this.getFieldValue('div[data-output="agentProperties"]', 'speed.y'),
@@ -218,6 +280,7 @@ class KeyframeView extends WindozDomContent {
await this.models.keyframes.save(this.currentKeyframe.kfId, this.kfArena.agents) await this.models.keyframes.save(this.currentKeyframe.kfId, this.kfArena.agents)
this.outputs.btnSaveKF.disabled = true this.outputs.btnSaveKF.disabled = true
this.updateKfButtons()
ui.growl.append('Keyframe saved!','success',3000) ui.growl.append('Keyframe saved!','success',3000)
setTimeout(() => { this.outputs.btnSaveKF.disabled = false}, 3000) setTimeout(() => { this.outputs.btnSaveKF.disabled = false}, 3000)
} }
@@ -1,3 +1,15 @@
/**
* _ ___ Another
* / |/ (_)______ __ _____
* / / / __(_-</ // (_-<
* /_/|_/_/\__/___/\_, /___/
* /___/
* production !
*
* Licensed under the MIT License:
* This code is free to use and modify,
* as long as the copyright notice and license are kept.
*/
import * as THREE from 'three' //'/app/thirdparty/Three/three.module.js' import * as THREE from 'three' //'/app/thirdparty/Three/three.module.js'
export class AgentPreview{ export class AgentPreview{
@@ -1,3 +1,15 @@
/**
* _ ___ Another
* / |/ (_)______ __ _____
* / / / __(_-</ // (_-<
* /_/|_/_/\__/___/\_, /___/
* /___/
* production !
*
* Licensed under the MIT License:
* This code is free to use and modify,
* as long as the copyright notice and license are kept.
*/
import * as THREE from 'three' import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js' import * as TWEEN from 'three/examples/jsm/libs/tween.module.js'
+33
View File
@@ -0,0 +1,33 @@
<style>
.create-sim > section {
padding: 1em;
display: grid;
grid-gap: .5em;
}
.create-sim bz-select[data-output="keyframesSelector"] {
margin-top: 1em;
}
.create-sim .cols-2 { align-items: baseline; }
.create-sim button[eicbutton][rounded] {
background-color: #367;
color: #DDD !important;
padding: .5em 1em !important;
justify-self: center;
}
</style>
<article eiccard class="create-sim">
<header>
<h1>Create a simulation</h1>
</header>
<section>
<div class="cols-2">
<label>Simulation first keyframe:</label>
<bz-select label="Existing keyframes..." data-output="keyframesSelector"></bz-select>
</div>
<div class="cols-2">
<label>Simulation Name:</label>
<input type="text" data-output="simName" placeholder="(min 5 chars)"/>
</div>
<button eicbutton rounded data-output="btnCreateSim" data-trigger="onCreateSim">Create Simulation</button>
</section>
</article>
+37
View File
@@ -0,0 +1,37 @@
class CreateSimView extends WindozDomContent {
constructor() {
super()
Object.assign(this, app.helpers.activeAttributes, app.helpers.basicDialogs)
}
async DOMContentLoaded(options) {
this.models = options.models
const components = ui.eicfy(this.el)
this.setupTriggers(components)
this.setupRefs(components)
this.models.keyframes.list('', null).then(data => data.payload).then(kflist => {
this.outputs.keyframesSelector.fillOptions(kflist.map(item => {
return({
markup: item.ekf_name,
value: item.ekf_uuid
})
}))
})
}
onCreateSim(comp, event) {
if(!this.outputs.keyframesSelector.value) return
if(!this.outputs.simName.value) return
this.models.sims.create({
kfId: this.outputs.keyframesSelector.value,
simName: this.outputs.simName.value
}).then(data => {
const simulationUuid = data?.payload?.simulationUuid ?? 'unknown'
ui.growl.append(`Simulation created (${simulationUuid})`, 'success', 4000)
})
this.unload()
}
}
app.registerClass('CreateSimView', CreateSimView)
+18
View File
@@ -0,0 +1,18 @@
<style>
.manage-sim > section {
height: 100%;
padding: 0;
}
.manage-sim .sim-list .row {
grid-template-columns: 2fr 2fr 1fr 6em 10em;
}
.manage-sim .sim-list .cell { text-align: center; }
</style>
<article eiccard class="manage-sim">
<header>
<h1>Play / Pause / Stop a simulation</h1>
</header>
<section>
<div eicdatagrid class="sim-list"></div>
</section>
</article>
+205
View File
@@ -0,0 +1,205 @@
class ManageSimView extends WindozDomContent {
#lifecycleEventTypes = [
'onYourMarks',
'bigBang',
'simulationPaused',
'simulationResumed',
'simulationStopped',
'simulationPrepareFailed',
]
constructor() {
super()
Object.assign(this, app.helpers.activeAttributes, app.helpers.basicDialogs)
this.lifecycleChan = app.Assets.Store.json.busChannels.maestro.lifecycleChannel
this.lifecycleChan = this.lifecycleChan.replace(/\[UID\]/g, app.User.identity.uuid)
}
async DOMContentLoaded(options) {
this.models = options.models
this.wasBlured = false
this._refreshSeq = 0
this.lifecycleSubscribed = false
ui.eicfy(this.el)
const view = this
this.simGrid = new DataGrid(this.find('.sim-list'), {
headers: [
{ label: 'Simulation', sortable: true },
{ label: 'Primordial frame', sortable: true },
{ label: 'Owner', sortable: true },
{ label: 'Status', sortable: true },
{ label: '', sortable: false },
],
height: '480px',
rowActions: [
{
icon: '<i class="icon-play"></i>',
title: 'Start simulation',
severity: 'success',
callback: async function(event) {
await view.onPlaySim(this, event.currentTarget)
},
},
{
icon: '<i class="icon-pause"></i>',
title: 'Pause simulation',
severity: 'secondary',
callback: async function(event) {
await view.onPauseSim(this, event.currentTarget)
},
},
{
icon: '<i class="icon-stop"></i>',
title: 'Stop simulation',
severity: 'danger',
callback: async function(event) {
await view.onStopSim(this, event.currentTarget)
},
},
],
})
this.simGrid.enableFooter = false
await this.refreshSimList()
}
async DOMContentFocused() {
await this.subscribeLifecycle()
if(!this.wasBlured) return
this.wasBlured = false
this.refreshSimList()
}
async DOMContentBlured() {
this.wasBlured = true
await this.unsubscribeLifecycle()
}
async subscribeLifecycle() {
if(this.lifecycleSubscribed || !app.MessageBus?.connected) return
await app.MessageBus.subscribe([this.lifecycleChan])
for(const eventType of this.#lifecycleEventTypes) {
app.MessageBus.addBusListener(eventType, [this.lifecycleChan], this.lifecycleListener, 'ManageSimView')
}
this.lifecycleSubscribed = true
}
async unsubscribeLifecycle() {
if(!this.lifecycleSubscribed || !app.MessageBus?.connected) return
for(const eventType of this.#lifecycleEventTypes) {
app.MessageBus.removeBusListener(eventType, this.lifecycleListener, 'ManageSimView')
}
await app.MessageBus.unSubscribe([this.lifecycleChan])
this.lifecycleSubscribed = false
}
lifecycleListener(realChan, payload, sender) {
console.log('[ManageSimView] maestro lifecycle', { realChan, payload, sender })
}
async refreshSimList() {
const seq = ++this._refreshSeq
this.simGrid.loading = true
this.simGrid.clear()
try {
const data = await this.models.sims.list()
if(seq !== this._refreshSeq) return
const sims = data?.payload ?? []
this.simGrid.clear()
for(const sim of sims) {
const row = this.simGrid.addRow(
{ simulationUuid: sim.simulationUuid },
[
sim.sim_name ?? '',
sim.ekf_name ?? '',
sim.usr_name ?? '',
sim.state ?? 'idle',
],
true
)
this.updateSimButtons(sim.state ?? 'idle', row)
}
this.simGrid.updateFilters()
} finally {
if(seq === this._refreshSeq) this.simGrid.loading = false
}
}
updateSimButtons(state, rowEl) {
const buttons = rowEl?.querySelectorAll('.cell.actions button[eicbutton]')
if(!buttons || buttons.length < 3) return
const enabledByState = {
idle: [true, false, false],
preparing: [false, false, false],
live: [false, true, true],
paused: [true, false, true],
}
const enabled = enabledByState[state] ?? [false, false, false]
for(let i = 0; i < 3; i++) {
buttons[i].disabled = !enabled[i]
}
}
async onPlaySim(rowId, button) {
const simulationUuid = rowId?.simulationUuid
if(!simulationUuid) return
const row = button.closest('.row')
button.disabled = true
try {
const result = await this.models.sims.startSimulation(simulationUuid)
ui.growl.append(
`Simulation started (${result.agentIds?.length ?? 0} agent(s))`,
'success',
4000
)
await this.refreshSimList()
} catch(err) {
ui.growl.append(String(err), 'error', 6000)
const state = row?.querySelectorAll('.cell')[4]?.innerText?.trim() || 'idle'
this.updateSimButtons(state, row)
}
}
async onPauseSim(rowId, button) {
const simulationUuid = rowId?.simulationUuid
if(!simulationUuid) return
const row = button.closest('.row')
button.disabled = true
try {
await this.models.sims.pauseSimulation(simulationUuid)
ui.growl.append('Simulation paused', 'success', 3000)
await this.refreshSimList()
} catch(err) {
ui.growl.append(String(err), 'error', 6000)
const state = row?.querySelectorAll('.cell')[4]?.innerText?.trim() || 'idle'
this.updateSimButtons(state, row)
}
}
async onStopSim(rowId, button) {
const simulationUuid = rowId?.simulationUuid
if(!simulationUuid) return
const row = button.closest('.row')
button.disabled = true
try {
await this.models.sims.stopSimulation(simulationUuid)
ui.growl.append('Simulation stopped', 'success', 3000)
await this.refreshSimList()
} catch(err) {
ui.growl.append(String(err), 'error', 6000)
const state = row?.querySelectorAll('.cell')[4]?.innerText?.trim() || 'idle'
this.updateSimButtons(state, row)
}
}
}
app.registerClass('ManageSimView', ManageSimView)
+11 -4
View File
@@ -1,8 +1,15 @@
<style>
article[eiccard] section:has(.confirm-dialog) {
padding: 0;
}
.confirm-dialog > section {
margin:0 !important;
}
</style>
<div class="confirm-dialog"> <div class="confirm-dialog">
<section> <section eicalert ${severity}>
<alert eicalert ${severity} ${muted}> ${message}
${message}
</alert>
</section> </section>
</div> </div>
@@ -1,3 +1,15 @@
/**
* _ ___ Another
* / |/ (_)______ __ _____
* / / / __(_-</ // (_-<
* /_/|_/_/\__/___/\_, /___/
* /___/
* production !
*
* Licensed under the MIT License:
* This code is free to use and modify,
* as long as the copyright notice and license are kept.
*/
/** /**
* @category MyEic * @category MyEic
* @subcategory Views * @subcategory Views
+23 -4
View File
@@ -1,3 +1,15 @@
/**
* _ ___ Another
* / |/ (_)______ __ _____
* / / / __(_-</ // (_-<
* /_/|_/_/\__/___/\_, /___/
* /___/
* production !
*
* Licensed under the MIT License:
* This code is free to use and modify,
* as long as the copyright notice and license are kept.
*/
class SpaceView extends WindozDomContent { class SpaceView extends WindozDomContent {
constructor() { constructor() {
@@ -6,16 +18,20 @@ class SpaceView extends WindozDomContent {
//this.tileMarkup = app.Assets.Store.html['/app/assets/html/mailing/tile.html'] //this.tileMarkup = app.Assets.Store.html['/app/assets/html/mailing/tile.html']
} }
DOMContentFocused(options) { DOMContentFocused() {
if(this.wasBlured){ // Avoid 2nd refesh on DomContentLoaded if(this.wasBlured && this.viewMode === '3D' && this.ttb && this.renderingEngine) {
//this.refreshyoustuff() this.ttb.watchCameraFrustum(this.renderingEngine)
} }
this.wasBlured = false this.wasBlured = false
} }
DOMContentBlured(options) { this.wasBlured = true } DOMContentBlured() {
this.wasBlured = true
if(this.viewMode === '3D' && this.ttb) this.ttb.stopWatchingCameraFrustum()
}
DOMContentLoaded(options) { DOMContentLoaded(options) {
this.viewMode = options.mode
this.windowPrefsId = `live.spaceview.${options.mode}` this.windowPrefsId = `live.spaceview.${options.mode}`
for(let model in options.models) this[model] = options.models[model] for(let model in options.models) this[model] = options.models[model]
this.ttb = options.ttb this.ttb = options.ttb
@@ -23,6 +39,9 @@ class SpaceView extends WindozDomContent {
this.setupTriggers(components) this.setupTriggers(components)
this.setupRefs(components) this.setupRefs(components)
this.renderingEngine = this.ttb.startRendering(this.outputs.ttbCanvas, options.mode) this.renderingEngine = this.ttb.startRendering(this.outputs.ttbCanvas, options.mode)
if(options.mode === '3D') {
this.ttb.watchCameraFrustum(this.renderingEngine)
}
this.output('settingsMenu',app.Assets.Store.html.spaceViewSetting) this.output('settingsMenu',app.Assets.Store.html.spaceViewSetting)
this.outputs.settingsMenu.querySelectorAll('input[type="toggler"]').forEach(el => { this.outputs.settingsMenu.querySelectorAll('input[type="toggler"]').forEach(el => {
const tog = new InputToggler(el) const tog = new InputToggler(el)
+76 -69
View File
@@ -84,26 +84,26 @@ class MessageBus {
*/ */
constructor(config, userInfo){ constructor(config, userInfo){
this.config = config this.config = config
if(this.config.debug) console.log('Lauching Websocket worker...'); if(this.config.debug) console.log('Lauching Websocket worker...')
this.config.hostname = (('host' in this.config) && ( this.config.host!='')) ? this.config.host : document.location.hostname this.config.hostname = (('host' in this.config) && ( this.config.host!='')) ? this.config.host : document.location.hostname
this.userInfo = userInfo this.userInfo = userInfo
this.createWorker(); this.createWorker()
this.activeSubscriptions = []; this.activeSubscriptions = [];
this.promisesRegister = { }; this.promisesRegister = { }
this.bus2jsEventsRegister = []; // items: { eventType:'string', RegisteredCb: function, realCb: function } this.bus2jsEventsRegister = []; // items: { eventType:'string', RegisteredCb: function, realCb: function }
this.whenConnectedQ = []; this.whenConnectedQ = []
this.connected = false; this.connected = false
} }
/** /**
* *
*/ */
createWorker() { createWorker() {
if(!this.config.pathToWorker.endsWith('.js')) this.config.pathToWorker+='.js'; if(!this.config.pathToWorker.endsWith('.js')) this.config.pathToWorker+='.js'
this.MessageBusWorker = new Worker(this.config.pathToWorker+'?'+crypto.randomUUID()); this.MessageBusWorker = new Worker(this.config.pathToWorker+'?'+crypto.randomUUID())
this.MessageBusWorker.postMessage({ 'action':'start', 'config': this.config, 'userInfo': this.userInfo }); this.MessageBusWorker.postMessage({ 'action':'start', 'config': this.config, 'userInfo': this.userInfo });
this.MessageBusWorker.onmessage = this.receiveFromWorker.bind(this); this.MessageBusWorker.onmessage = this.receiveFromWorker.bind(this)
if(this.config.debug) console.log('Websocket worker launched.'); if(this.config.debug) console.log('Websocket worker launched.')
} }
/** /**
@@ -111,9 +111,9 @@ class MessageBus {
* @param {*} callBack * @param {*} callBack
*/ */
whenConnected(callBack){ whenConnected(callBack){
if(typeof(callBack) != 'function') return; if(typeof(callBack) != 'function') return
if(this.connected) callBack(); if(this.connected) callBack()
else this.whenConnectedQ.push(callBack); else this.whenConnectedQ.push(callBack)
} }
/** /**
@@ -135,8 +135,8 @@ class MessageBus {
* @param {*} callBack * @param {*} callBack
*/ */
ifConnected(callBack){ ifConnected(callBack){
if(typeof(callBack) != 'function') return; if(typeof(callBack) != 'function') return
if(this.connected) callBack(); if(this.connected) callBack()
} }
/** /**
@@ -152,17 +152,17 @@ class MessageBus {
* This method gives (and resolves) a promise, taking care of all lower-level details * This method gives (and resolves) a promise, taking care of all lower-level details
*/ */
requestWssGwAction(action, payload=null, timeOut=5000){ requestWssGwAction(action, payload=null, timeOut=5000){
if(!action) return; if(!action) return
let request = {'action':action, 'payload':payload}; let request = {'action':action, 'payload':payload}
request.reqid = crypto.randomUUID(); request.reqid = crypto.randomUUID()
return(new Promise((resolve, fail) => { return(new Promise((resolve, fail) => {
let timeOutID = setTimeout(() => { let timeOutID = setTimeout(() => {
fail(`Timeout (>${timeOut}ms) for action ${action}`); fail(`Timeout (>${timeOut}ms) for action ${action}`);
if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid]) if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid])
}, timeOut); }, timeOut)
this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]; this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]
this.MessageBusWorker.postMessage(request); this.MessageBusWorker.postMessage(request)
})); }))
} }
/** /**
@@ -172,18 +172,18 @@ class MessageBus {
* This method gives (and resolves) a promise, taking care of all lower-level details * This method gives (and resolves) a promise, taking care of all lower-level details
*/ */
requestBusAction(chan, action, payload=null, timeOut=5000){ requestBusAction(chan, action, payload=null, timeOut=5000){
if(!action) return; if(!action) return
let request = {'action':action, 'payload':payload}; let request = {'action':action, 'payload':payload}
request.reqid = crypto.randomUUID(); request.reqid = crypto.randomUUID()
return(new Promise((resolve, fail) => { return(new Promise((resolve, fail) => {
let timeOutID = setTimeout(() => { let timeOutID = setTimeout(() => {
fail(`Timeout (>${timeOut}ms) for action ${action}`); fail(`Timeout (>${timeOut}ms) for action ${action}`);
if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid]) if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid])
}, timeOut); }, timeOut)
this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]; this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]
if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan
this.send(chan, JSON.stringify(request)) this.send(chan, JSON.stringify(request))
})); }))
} }
/** /**
@@ -193,18 +193,18 @@ class MessageBus {
* This method gives (and resolves) a promise, taking care of all lower-level details * This method gives (and resolves) a promise, taking care of all lower-level details
*/ */
requestMidasAction(chan, action, data=null, timeOut=5000){ requestMidasAction(chan, action, data=null, timeOut=5000){
if(!action) return; if(!action) return
let request = {payload: {'action':action, 'data':data}} let request = {payload: {'action':action, 'data':data}}
request.reqid = crypto.randomUUID(); request.reqid = crypto.randomUUID()
return(new Promise((resolve, fail) => { return(new Promise((resolve, fail) => {
let timeOutID = setTimeout(() => { let timeOutID = setTimeout(() => {
fail(`Timeout (>${timeOut}ms) for action ${action}`); fail(`Timeout (>${timeOut}ms) for action ${action}`);
if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid]) if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid])
}, timeOut); }, timeOut)
this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]; this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]
if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan
this.send(chan, JSON.stringify(request)) this.send(chan, JSON.stringify(request))
})); }))
} }
/** /**
@@ -230,9 +230,9 @@ class MessageBus {
*/ */
send(chan, msg){ send(chan, msg){
// You can publish to an unsubscribed chan, userchans are the best example ! // You can publish to an unsubscribed chan, userchans are the best example !
// if(this.activeSubscriptions.indexOf(chan)<0) return; // if(this.activeSubscriptions.indexOf(chan)<0) return
var request = {'action':'PUB', 'payload': { 'chan':chan, 'msg': msg}}; var request = {'action':'PUB', 'payload': { 'chan':chan, 'msg': msg}}
this.MessageBusWorker.postMessage(request); this.MessageBusWorker.postMessage(request)
} }
/** /**
@@ -322,11 +322,13 @@ class MessageBus {
/** /**
* Helper method to match a chan with globbing * Helper method to match a chan with globbing
* *
* @param {string} myChan (no glob) * @param {string} myChan (no glob, no user expansion)
* @param {string} targetChan PATTERN (possible glob) * @param {string} targetChan PATTERN (possible glob and user expansion)
* @returns {boolean} * @returns {boolean}
*/ */
chanMatch(myChan, targetChan) { chanMatch(myChan, targetChan) {
targetChan = targetChan.replace(/\[UID\]/g, this.userInfo.uuid)
targetChan = targetChan.replace(/\[CUID\]/g, this.cnxId)
let re = new RegExp('^'+targetChan.replace(/\*/g,'(.+)')+'$','g') let re = new RegExp('^'+targetChan.replace(/\*/g,'(.+)')+'$','g')
return(myChan.match(re)!=null) return(myChan.match(re)!=null)
} }
@@ -339,28 +341,28 @@ class MessageBus {
var workermsg = e.data; var workermsg = e.data;
if('event' in workermsg){ if('event' in workermsg){
// event "ReceiveFromServer" is the general case of a message from server, found in data, with its own struct. // event "ReceiveFromServer" is the general case of a message from server, found in data, with its own struct.
// other type og event are generated by the worker, about the connection // other type of event are generated by the worker, about the connection
switch(workermsg.event){ switch(workermsg.event){
case 'ReceiveFromServer': case 'ReceiveFromServer':
this.receiveFromServer(JSON.parse(workermsg.data)); this.receiveFromServer(JSON.parse(workermsg.data))
break; break
case 'connected': case 'connected':
this.connected = true; this.connected = true
if(this.config.debug) console.log('received connected event from worker !'); if(this.config.debug) console.log('received connected event from worker !')
this.executewhenConnectedQ(); this.executewhenConnectedQ()
app.events.trigger('MessageBus.Connected'); app.events.trigger('MessageBus.Connected')
break; break
case 'closed': case 'closed':
if(this.config.debug) console.log('received closed event from worker!'); if(this.config.debug) console.log('received closed event from worker!')
this.activeSubscriptions = []; this.activeSubscriptions = []
this.callBacksRegister = { }; this.callBacksRegister = { }
this.whenConnectedQ = []; this.whenConnectedQ = []
this.connected = false; this.connected = false;
app.events.trigger('MessageBus.Closed'); app.events.trigger('MessageBus.Closed');
break; break
default: default:
if(this.config.debug) console.warn('Unknown Websocket Worker message:', workermsg); if(this.config.debug) console.warn('Unknown Websocket Worker message:', workermsg)
} }
} }
} }
@@ -377,35 +379,40 @@ class MessageBus {
receiveFromServer(srvdata) { receiveFromServer(srvdata) {
// See protocol reminder comment at the bottom // See protocol reminder comment at the bottom
if('action' in srvdata){ // Reply to a request if('action' in srvdata){ // Reply to a request
let action = srvdata.action; let action = srvdata.action
let payload = ('payload' in srvdata) ? srvdata.payload : null; let payload = ('payload' in srvdata) ? srvdata.payload : null
// Piggyback on the results of some actions for this module internal use // Piggyback on the results of some actions for this module internal use
switch(action){ switch(action){
case 'WELCOME':
console.log('Received WSS welcome', srvdata)
this.cnxId = srvdata.cnxId
this.serverTimeDelta = Date.now() - srvdata.serverTime
break
case 'SUB': case 'SUB':
if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload); if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload)
break; break
case 'SUBLST': case 'SUBLST':
if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload); if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload)
break; break
} }
app.events.trigger('MessageBus.anyAction', srvdata); app.events.trigger('MessageBus.anyAction', srvdata);
} else { // Low-level event : Redis Event, contrary to requ/reply with wssGateway, or other later } else { // Low-level event : Redis Event, contrary to requ/reply with wssGateway, or other later
if(('event' in srvdata) && (srvdata.event == 'REDISMSG')){ if(('event' in srvdata) && (srvdata.event == 'REDISMSG')){
var payload = ('payload' in srvdata) ? srvdata.payload : null; var payload = ('payload' in srvdata) ? srvdata.payload : null
if(payload && payload.msg && (payload.msg.eventType || payload.msg.action)) { if(payload && payload.msg && (payload.msg.eventType || payload.msg.action)) {
if(payload.msg.eventType){ if(payload.msg.eventType){
var eventType = payload.msg.eventType; var eventType = payload.msg.eventType
app.events.trigger('MessageBus.event.'+eventType, { app.events.trigger('MessageBus.event.'+eventType, {
chan: payload.chan, chan: payload.chan,
sender: payload.msg.sender, sender: payload.msg.sender,
eventType: payload.msg.eventType, eventType: payload.msg.eventType,
payload: payload.msg.payload, payload: payload.msg.payload,
}); })
} else if(payload.msg.action && payload.msg.reqid) { } else if(payload.msg.action && payload.msg.reqid) {
let reqid = payload.msg.reqid; let reqid = payload.msg.reqid
let action = payload.msg.action; let action = payload.msg.action
let actionPayload = ('payload' in payload.msg) ? payload.msg.payload : null; let actionPayload = ('payload' in payload.msg) ? payload.msg.payload : null
let err = ('err' in payload.msg) ? payload.msg.err : null; let err = ('err' in payload.msg) ? payload.msg.err : null
let success = payload.msg.success; let success = payload.msg.success;
if(reqid in this.promisesRegister) { if(reqid in this.promisesRegister) {
clearTimeout(this.promisesRegister[reqid][2]); // Stop timeout timer clearTimeout(this.promisesRegister[reqid][2]); // Stop timeout timer
@@ -416,12 +423,12 @@ class MessageBus {
app.events.trigger('MessageBus.anyMessage', { app.events.trigger('MessageBus.anyMessage', {
chan: payload.chan, chan: payload.chan,
msg : payload.msg, msg : payload.msg,
}); })
} else if(payload && payload.bmsg){ } else if(payload && payload.bmsg){
app.events.trigger('MessageBus.promiscuousMessage', { // Repill msg : decapsulate & use spcific event app.events.trigger('MessageBus.promiscuousMessage', { // Repill msg : decapsulate & use spcific event
chan: payload.bmsg.chan, chan: payload.bmsg.chan,
msg : payload.bmsg.msg, msg : payload.bmsg.msg,
}); })
} }
else { else {
console.warn('Weird bus message (discarted) :', srvdata) console.warn('Weird bus message (discarted) :', srvdata)
@@ -431,9 +438,9 @@ class MessageBus {
// For request-reply, settle promise // For request-reply, settle promise
if(srvdata.reqid && (srvdata.reqid in this.promisesRegister)) { if(srvdata.reqid && (srvdata.reqid in this.promisesRegister)) {
let payload = ('payload' in srvdata) ? srvdata.payload : null; let payload = ('payload' in srvdata) ? srvdata.payload : null
let err = ('err' in srvdata) ? srvdata.err : null; let err = ('err' in srvdata) ? srvdata.err : null
let success = srvdata.success; let success = srvdata.success
clearTimeout(this.promisesRegister[srvdata.reqid][2]); // Stop timeout timer clearTimeout(this.promisesRegister[srvdata.reqid][2]); // Stop timeout timer
if(success) this.promisesRegister[srvdata.reqid][0](payload); // resolve if(success) this.promisesRegister[srvdata.reqid][0](payload); // resolve
else this.promisesRegister[srvdata.reqid][1](`MsgBus action failed.\nError: ${err}`); // Fail else this.promisesRegister[srvdata.reqid][1](`MsgBus action failed.\nError: ${err}`); // Fail
+185
View File
@@ -0,0 +1,185 @@
{
"nodes": [
{
"id": "concept.p42",
"name": "P42",
"type": "concept",
"scope": "project",
"userDoc": {
"statements": [
"Project 42 (or P42 for short) is a general purpose simulation engine",
"Its goal is to provide a flexible and powerful tool for simulating and analyzing the widest possible range of complex systems",
"It is a platform for building and running simulations, for analyzing the results of those simulations",
"It aims at allowing users to easily create and spot emerging phenomenons in complex systems"
],
"humor": [
"Why 42 ? Well, the author hopes this project will help answering non-obvious questions, and you should know 42 is the answer to THE BIGGEST question."
]
},
"developperDoc": {
}
},
{
"id": "concept.godProcess",
"name": "God process",
"type": "concept",
"scope": "platform",
"userDoc": {
"statements": [
"A god-process is a process that is responsible for a specific part of the overall management of the simulation",
"Contrary to agents, it is not part of the simulation per-se, as it remains neutral to what happens in the arena of the simulation",
"Contrary to agents, it has access to everything that happens in the arena of the simulation in order to perform its tasks, and is therefore in a \"god-like\" position."
],
"humor": [
"Ironically, the author of P42 is a firm non-believer",
"In the unix world, a background process is called a \"daemon\", and therefore, you can call a god-process a \"god-daemon\""
]
},
"developperDoc": {
"constraints": [
"All god-daemons are kept in separated folders, in /op/p42GodDaemons",
"All god-daemons should depend on the same Redis P42 library that standardizes accesses to Redis Pub/Sub and Redis database",
"God-daemons can have access to the arena bus, the management bus or (generally) both.",
"God-daemons can fire events on the arena bus that will be received by agents",
"God-daemons can fire events on the management bus that will be received by the interface",
"God-daemons can fire events on the management bus that will be received by other god-daemons",
"God-daemons can fire events on the arena bus that will be received by other god-daemons",
"God-daemons must never update agents data directly (that's the agent's job)",
"Agents can make requests to God-daemons via the arena bus, and god-daemon should always reply with a valid response (even if empty or error)"
]
},
"relations": [
{ "type": "uses", "target": "concept.arenaBus" },
{ "type": "uses", "target": "concept.managementBus" }
]
},
{
"id": "concept.arenaBus",
"name": "Arena Bus",
"type": "concept",
"scope": "platform",
"userDoc": {
"statements": [
"The Arena Bus is a message bus that is used to communicate between agents and god-processes",
"It is a Redis backed PUB/SUB message bus, and is used to provide communications between agents, between agents and god-processes, and between god-processes",
"For agents, this bus represents their sole window to the simulation's world (arena)",
"For God-daemons, the bus allows to receive agent events (monitoring), send world-state change events to interested agents, and to provide agents with request-reply services"
]
},
"developperDoc": {
"constraints": [
"For scalability, the arena bus can use several different Redis daemons, each one representing a shard of the Arena (bus and agents storages)",
"To respect arena isolation, events on the arena bus shall not cary any information which is not directly related to the arena (agents, god-processes, etc.)"
]
}
},
{
"id": "concept.managementBus",
"name": "Management Bus",
"type": "concept",
"scope": "platform",
"userDoc": {
"statements": [
"The Management Bus is a message bus that is used to communicate between god-processes, and between god-processes and the interface",
"It is a Redis backed PUB/SUB message bus"
]
},
"developperDoc": {
"constraints": [
"For simplicity, the management bus only uses a single Redis daemon",
"In order to keep the arena bus as little loaded as possible, inter-god-daemons events shall use the management bus instead of the arena bus"
]
}
},
{
"id": "concept.PRNGs",
"name": "PRNGs",
"type": "concept",
"scope": "agent api",
"userDoc": {
"statements": [
"PRNGs (Pseudo-Random Number Generator) are provided to agents needing random numbers",
"Agents cannot use any other random number generator than the PRNGs provided to them by the PRGN God-Daemon",
"This ensures reproducibility of the simulation, and prevents agents from using non-reproductible randomness"
]
},
"developperDoc": {
"constraints": [
"Random numbers are provided to agents upon request to the God-Daemon PRNG service",
"Seed based PRNGs (Pseudo-Random Number Generator) are used to generate random numbers for agents",
"PRNGs must ensure a high enough entropy for each agent",
"PRNGs must ensure reproducibility from one run to the next, by storing all seeds with the arena state",
"Agents shall never have access to seeds, nor to any other information that could be used to reproduce the same random numbers",
"PRNGs must be thread-safe, and must be able to handle concurrent requests from multiple agents"
]
}
},
{
"id": "feature.horizontal-scaling",
"name": "horizontal scaling",
"type": "feature",
"scope": "performance",
"userDoc": {
"statements": [
"Horizontally scalable by design, allowing users to easily scale their simulations to handle large amounts of data and agents",
"The limit is only the computing power users are ready top pay for"
]
},
"developperDoc": {
"constraints": [
"All runners are containerized, and are executed by a dedicated container orchestrator, which uses a dedicated Redis that represents a shard of the Arena (bus and agents storages)"
],
"statements": [
"Uses a distributed architecture that uses any number of containerized runners that execute agents code, as well as the code of god-processes",
"Communication between agents, god-processes and interface is handled by two REDIS backed PUB/SUB message buses"
]
}
},
{
"id": "feature.strongArenaIsolation",
"name": "Strong arena isolation",
"type": "feature",
"scope": "simulation",
"userDoc": {
"statements": [
"Designed to maintain strong isolation between the arena of the simulation and the rest of the system",
"Agents are isolated from each other, and from the rest of the system to ensure no unwanted data contamination could tamper the agent's behavior"
]
},
"developperDoc": {
"constraints": [
"No event shall be ported bewteen arena and management busses",
"No reply to an agent request and no payload of an arena event should leak any \"overview\" of the simulation state to the agent, neither any management data of the system"
],
"statements": [
"Uses two different busses: one for letting agents communicate with each other, or with the god-processes, and a second isolated one for managing the simulation state",
"Likewise, agents internal states and properties belong to the arena, and are stored in a dedicated REDIS database, isolated from the general system databases"
]
},
"relations": [
{ "type": "uses", "target": "concept.arenaBus" },
{ "type": "uses", "target": "concept.managementBus" }
]
},
{
"id": "feature.reproductibility",
"name": "Reproductibility",
"type": "feature",
"scope": "simulation",
"userDoc": {
"statements": [
"Designed to ensure that the simulation can be reproduced exactly the same way, from one run to the next",
"Same initial conditions should lead to the same final state, unless the simulation is voluntarily stochastic (agents use non-reproductible randomness)"
]
},
"developperDoc": {
"constraints": [ ],
"statements": [ ]
},
"relations": [
{ "type": "uses", "target": "concept.PRNGs" },
{ "type": "uses", "target": "concept.managementBus" }
]
}
]
}
+107
View File
@@ -0,0 +1,107 @@
In SPARC, the app menu is created from app/assets/json/global/app-menu-map.json
Each entry defines a route.
A "high level" route is then configured in app/config/baseRoutes.json that delegates a group of URLs to a controller.
Then, a controller, in its associated .json file defines in its "routes" section the sub-route managed by the controller,
and the method to be called inside that controller.
In SPARC, models inherit from the core and from WindozModel a mecanism that gets the URL from app/assets/json/global/services.json
That configuration file associates actions grouped in sections to API urls and http method.
Sections are identified by an URL-prefix (only used as label), and correspond to API "services".
The server-side of this API is served by the node daemon in p42api.
p42api has a folder 'api' with one file / module per API service, they are all declared in and imported by api/index.js.
Each module defines a mapping object, that contains all the complete urls served, http-method, and local handling method.
We are now focusing on P42 God Daemons (in folder p42GodDaemons).
Every daemon in there is connected to several Redis. At least one Arena redis, and one System Redis.
PUB/SUB channels are prefixed accordingly, following the configuration token "chansNamespace"
and anything outside the configured combination Redis host & port + chansNamespace is illegal.
In the future, there might be several different Redis (in mesh) serving the same chansNamespace.
GPS Daemon is the global positionning system of the arena.
It holds the current position of every agent in the arena.
Agents change their 3D speed vector whenever they want by informing GPS (via the agentVectorChangeChannel)
A typical agent event looks like
{ "eventType": "change", "payload": { "newVector": { "x": 5, "y": -2.34, "z": 0 } }, "sender": "agent42" }
On every speed vector change, GPS computes the 4D worldline of the agent,
and scans all other worldlines to find those intersecting, or approaching less than a configurable distance.
Each collision/ near-miss is then registered.
As time passes, GPS fires events towards concerned agents based on this register.
On every speed vector change, this register is searched and all entries containing the agent changing its speed are deleted.
To speed-up and limit the intersection computations, we will take an approach using rectangular 4-prisms :
Each pair of wordlines to test for collision / near-miss is enclosed in a 4-prism of a certain configurable time-height (thus how far in the future we test),
and we just test for intersecting prisms.
Only if the prisms intersect do we compute the real minimal-distance and its time, that we keep in the register.
Comparing a pair of worldlines should be a separated method and the looping through all agents in a separated method as well.
The reason for this remark is to take provision for a future where I might want to paralellize this (instead of looping through), in a GPU.
We need to think about how we start a new simulation from GPS point of view :
As we are in a distributed context, here is the workflow I'm thinking of :
1. Some orchestrator process will put all agents data in the arena Redis store.
2. Then it will start all agents processes, in a special mode "onYourMarks" where they can initialize, time is frozen to zero, any non initialization computation is forbidden.
3. When each agent is initialized, it fires an arena event "readyToStart"
4. When the orchestrator has accounted for all agents, it fires an "arenaStart" event, that unleashes all agents.
Now where is GPS in all of this :
GPS should behave like an agent: listen for "onYourMarks" event, initialize, send "readToStart" when done, and start time-ticking when it received a "bigBang" event.
Contrary to agents, GPS daemon itself is supposed to be running already.
So the initialization is mainly doing a first agents scan to fill its registry. (with an unvalued T0 and collision-ticks not running). Scanning all agents means going into redis store to get their vector (its only after this step than GPS become the positions authority)
Upon receiving "bigBang", T0 is valued, and the collisionsTicks starts.
That's the general idea.
Please check if you don't see incoherence, gap or edge case.
5. GPS time communicated to agents is simulation time, thus with T(bigBang) = 0
Therefore, when the orchestrator send a "bigBang" event, GPS starts general scan
and does the pre-compute with all positions at T=0
For eventual time-elapsed computations internal to GPS, it can keep a reference, say bigBangEpoch to epoch (miliseconds)
at the moment he receives the "bigBang" event.
From then on, current Simulation time is now() - bigBangEpoch
Next step: the embrio of simOrchestrator.
Can you create a second GodDaemon called like that, aside GPS, that uses the same config, the same general pattern, adapted start & stop scripts,
So, the first thing you must understand, is that all busses, thus all meshes and all redisConnexions can carry both types of messages :
1. Request-reply: action request (possibly with reqid) requestor to provider. After action completion (or rejection), the provider answers the requestor (private chan replacing {uuid} with sender) with a reply (on the same reqid if present).
The payload must contain "action", with an imperative form.
Action must be verified to arrive on a specific chan, and the sender be allowed by access-rights.
2. Events broadcasting: not a request, just a message for anybody interested (so no "rejection" per se, but eventual ignore). No answer, but eventual local processing which might or not endup firing other event(s).
The payload must contain "eventType", with a generally a past tense. (like "he guys, I UPDATED my color")
Event routing combines chan & eventType (for example, and "updated" eventType could appear on a agentColor chan, and on a agentHealth chan, with different meanings and thus handling)
These two types can happen anywhere, it is not a "per mesh drift".
Probably, most Actions will concentrate in the SYSTEM mesh, because the management by the front-end and API is mostly user driven (user action becomes a request on the system bus).
Probably, the ARENA will be mostly event-driven, because agents have no overview of the arena (so to who ask for what?)
Nevertheless, there will be many cases where this is not true:
GODs emitting events that other gods or even the Front-End should be aware of.
The observer feeding the GUI on arena changes in an event-driven way.
Agent in the Arena requesting a service from a known god. (like "Hey GPS, give me my latest vector")
And maybe even someday clever agents that provide actions to other agents in their proximity for example.
"Action" folder was originally meant for...actions !
Now we need to find a clean, elegant, universal (across daemons & across meshes)
way of coexist both actions and events.
Describe a proposal that stays close to what we have, but separates events from actions.
We have another issue: If I'm not mistaken, a front-end "sender" UID is actually his login UID.
That means that a user opening 2 browsers is viewed as the same sender on both.
Therefore, using sender as upsert key in the subscriptions registry of observer is problematic,
because we should allow the same user to view different things on different browsers.
Maybe in the second browser he's viewing another angle, or meybe he's not viewing this particular sim at all.
Back to business.
I slightly changed the listSims query, so we can display sim name, primordial frame name and owner name.
Now that this mecanism is in place in the gateway, Let's change Observer so that it uses a combination of sender and cnxId as key for its frustum register, instead of just sender