Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2f8766b9f | |||
| 06a7868882 | |||
| 54db203e86 | |||
| d172e06611 | |||
| b122cf153e | |||
| 5207a3b18e | |||
| ce9e73ac41 | |||
| eb7312c7a8 | |||
| d301b78ea7 | |||
| ea9b3d06cf | |||
| 4b9207546c | |||
| 662e694f21 | |||
| cd2e74e6fd | |||
| a967576d4b | |||
| 61855a3416 | |||
| 8421dd12b6 | |||
| 124bae719b | |||
| 24aad6bbf6 | |||
| 2d87523020 | |||
| f81ae0611c | |||
| 30a95ca009 | |||
| bc6a02fe10 | |||
| e46ff1a817 | |||
| f5fbdd6ccd | |||
| be72b6f2db | |||
| 60db0393cc | |||
| a156db845a | |||
| 8be5751107 | |||
| 3e0faa4866 | |||
| 34ce9049c6 | |||
| 6bc3c1e9b8 | |||
| 0cf67a5c9f | |||
| 0b4b7e2a2f | |||
| e5bd91cd9f | |||
| e647bf01de | |||
| 46f08cfdce | |||
| 91274c4542 | |||
| d9bdc34210 | |||
| 3c93683ca1 | |||
| 0d5107f09a | |||
| db45bcb4c5 | |||
| cf24657f9f | |||
| 4bb306e8a1 | |||
| bc80b3d4d6 | |||
| aadba6b3e6 |
@@ -0,0 +1,39 @@
|
||||
---
|
||||
description: SQL conventions — no table aliases; columns are always prefixed
|
||||
globs: "**/*.{js,sql,php}"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# SQL rules
|
||||
|
||||
All tables use **prefixed column names** (`sim_uuid`, `own_usr_id`, `usr_uuid`, `ekfs_agent_id`, …). There is never column ambiguity across joins, so **do not use table aliases**.
|
||||
|
||||
## Do not
|
||||
|
||||
- Short table aliases: `s`, `o`, `u`, `ekfs`, etc.
|
||||
- Qualified columns when an alias was the only reason: `s.sim_uuid`, `o.own_sim_uuid`
|
||||
|
||||
## Do
|
||||
|
||||
- Join on bare prefixed column names.
|
||||
- Use full table names (or `${qualified}` template vars) in `FROM` / `JOIN` only — no alias after the table.
|
||||
|
||||
```sql
|
||||
-- BAD
|
||||
SELECT s.sim_name, BIN_TO_UUID(s.sim_uuid) AS simulationUuid
|
||||
FROM `p42SIM`.simulations s
|
||||
INNER JOIN `p42GUI`.simowners o ON o.own_sim_uuid = s.sim_uuid
|
||||
INNER JOIN `p42GUI`.users u ON o.own_usr_id = u.usr_id
|
||||
WHERE u.usr_uuid = ? AND s.sim_uuid = UUID_TO_BIN(?)
|
||||
|
||||
-- GOOD
|
||||
SELECT sim_name, BIN_TO_UUID(sim_uuid) AS simulationUuid
|
||||
FROM `p42SIM`.simulations
|
||||
INNER JOIN `p42GUI`.simowners ON own_sim_uuid = sim_uuid
|
||||
INNER JOIN `p42GUI`.users ON own_usr_id = usr_id
|
||||
WHERE usr_uuid = ? AND sim_uuid = UUID_TO_BIN(?)
|
||||
```
|
||||
|
||||
`AS` on **result columns** (e.g. `AS simulationUuid` for the API) is fine — that is not a table alias.
|
||||
|
||||
When adding a query, read an existing join in the same repo first and match its style.
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "app/thirdparty/buildoz"]
|
||||
path = app/thirdparty/buildoz
|
||||
url = https://gitea.internike.com/nike/buildoz.git
|
||||
@@ -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>
|
||||
|
||||
@@ -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%);">∑</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>
|
||||
@@ -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>
|
||||
@@ -44,7 +44,7 @@ log(agtIds)
|
||||
await removeAgent('00000000-aaaa-bbbb-cccc-dddddddddddd')
|
||||
</div>
|
||||
<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 class="snippet" data-snippet="selectAgent" style="display:none;">
|
||||
await selectAgent('00000000-aaaa-bbbb-cccc-dddddddddddd')
|
||||
|
||||
@@ -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>
|
||||
@@ -4,4 +4,4 @@
|
||||
"y": 100,
|
||||
"z": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
"icon": "icon-bolt",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"maestro": {
|
||||
"actionsChannel": "system:requests:maestro",
|
||||
"lifecycleChannel": "system:maestro:lifecycle:[UID]"
|
||||
},
|
||||
"observer": {
|
||||
"actionsChannel": "system:requests:observer",
|
||||
"subscribeFrequencyMs": 1000,
|
||||
"cameraDebounceMs": 600
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,20 @@
|
||||
"uri": "/api/keyframes/{kfId}/agents",
|
||||
"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[UID]:agents",
|
||||
"events": [
|
||||
{
|
||||
"eventName": "move",
|
||||
|
||||
+63
-33
@@ -27,6 +27,7 @@
|
||||
|
||||
--app-menu-collapsed-width: 50px;
|
||||
--app-menu-expanded-width: 160px;
|
||||
--app-menu-hover-max-width: 400px;
|
||||
|
||||
--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 {
|
||||
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 {
|
||||
display: grid;
|
||||
/*
|
||||
@@ -299,33 +347,6 @@ article[eiccard][media] > header {
|
||||
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{
|
||||
height:100%;
|
||||
border: 2px solid var(--app-color-primary);
|
||||
@@ -366,7 +387,7 @@ input{
|
||||
padding-left: 1em;
|
||||
box-sizing: border-box;
|
||||
height: 2em;
|
||||
background-color: #6B5;
|
||||
background-color: #CFC;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -376,20 +397,29 @@ div.window > section button[eicbutton][rounded] {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
[eicdatagrid] .dataset .row:hover {
|
||||
background-color: #483;
|
||||
}
|
||||
|
||||
/* Customizations to buildoz*/
|
||||
bz-select > button{
|
||||
background: linear-gradient( to bottom, #251, #372 15%, #483 50%, #372 85%, #251 ) !important;
|
||||
color:#EEE;
|
||||
}
|
||||
bz-select > button::after{ color:#EEE; }
|
||||
bz-select option{
|
||||
bz-select option,
|
||||
div.options-container.portaled option{
|
||||
background-color: #676;
|
||||
color: #EEE;
|
||||
}
|
||||
bz-select option i{ margin-right:0.3em; }
|
||||
bz-select option i.icon-atom1{ color:#FF4; }
|
||||
bz-select option i.icon-bug{ color:#4DF; }
|
||||
bz-select > div.options-container.open { max-height: 20em; }
|
||||
bz-select option i,
|
||||
div.options-container.portaled option i{ margin-right:0.3em; }
|
||||
bz-select option i.icon-atom1,
|
||||
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-thumb { background-color:#9D8; }
|
||||
|
||||
|
||||
Binary file not shown.
+1982
-182
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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,11 @@
|
||||
"url": "/editors",
|
||||
"role": [ "*" ],
|
||||
"controller" : "/editors/EditorsController"
|
||||
},
|
||||
{
|
||||
"url": "/sims",
|
||||
"role": [ "*" ],
|
||||
"controller" : "/sims/SimsController"
|
||||
},
|
||||
{
|
||||
"url": "/system",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
],
|
||||
"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."}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ class DashboardsController extends WindozController {
|
||||
constructor(params) {
|
||||
super(params)
|
||||
this.arenaConfig = app.Assets.Store.json.arenaConfig
|
||||
this.busChannels = app.Assets.Store.json.busChannels
|
||||
this.eventsMapping = app.Assets.Store.json.eventsMapping
|
||||
console.log('=============>DashboardsController constructor')
|
||||
}
|
||||
@@ -21,6 +22,7 @@ class DashboardsController extends WindozController {
|
||||
const ttb = new app.LoadedModules.Threetobus({
|
||||
eventsMapping: this.eventsMapping,
|
||||
sceneSize: this.arenaConfig.arenaSize,
|
||||
observer: this.busChannels.observer,
|
||||
})
|
||||
ttb.initScene({
|
||||
axes: true,
|
||||
|
||||
@@ -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)
|
||||
@@ -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": [
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,13 @@ app.helpers.formBuilder = {
|
||||
}
|
||||
if(component){
|
||||
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)
|
||||
allFields.push(fieldRow)
|
||||
}
|
||||
@@ -77,6 +83,14 @@ app.helpers.formBuilder = {
|
||||
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){
|
||||
const result = {}
|
||||
document.querySelectorAll(`${rootSel} .formbuilder-field`).forEach(el => {
|
||||
@@ -89,14 +103,14 @@ app.helpers.formBuilder = {
|
||||
}
|
||||
target = target[key]
|
||||
}
|
||||
target[path[path.length - 1]] = el.value
|
||||
target[path[path.length - 1]] = this.#fieldValue(el)
|
||||
})
|
||||
return(result)
|
||||
},
|
||||
|
||||
getFieldValue(rootSel, name){
|
||||
const comp = document.querySelector(`${rootSel} .formbuilder-field[name="${name}"]`)
|
||||
if(comp) return(comp.value)
|
||||
if(comp) return(this.#fieldValue(comp))
|
||||
else return(null)
|
||||
},
|
||||
|
||||
|
||||
@@ -48,7 +48,16 @@ app.helpers.kfConsole = {
|
||||
this.kfArena.removeAgent(aid)
|
||||
},
|
||||
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) => {
|
||||
if(!Object.keys(this.kfArena.agents).includes(aid)) throw(`Agent ${aid} not on scene !`)
|
||||
|
||||
@@ -276,12 +276,19 @@ class WindozDomContent extends View {
|
||||
left = box.x ? box.x : defaults.x
|
||||
top = box.y ? box.y : defaults.y
|
||||
width = box.w ? box.w : defaults.w
|
||||
height = box.x ? box.h : defaults.h
|
||||
height = box.h ? box.h : defaults.h
|
||||
} else {
|
||||
left = defaults.x
|
||||
top = defaults.y
|
||||
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({
|
||||
width: `${width}px`,
|
||||
|
||||
@@ -43,7 +43,15 @@ class AgentsModel extends WindozModel {
|
||||
async getDefaultProps(id){
|
||||
const aprops = await this.getProperties(id)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
const kfData = Object.keys(data).map(aid => {
|
||||
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({
|
||||
aid: aid,
|
||||
type: data[aid].type,
|
||||
storeValues: storeValues,
|
||||
gpsValues: { position: data[aid].values.position, speed: data[aid].values.speed }
|
||||
gpsValues: { position: gpsPosition, speed: gpsSpeed },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
+134
-8
@@ -9,6 +9,16 @@ export class Threetobus{
|
||||
this._curEventsMapping = []
|
||||
this._stagedEventsMapping = options.eventsMapping
|
||||
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.cameras = {}
|
||||
@@ -19,8 +29,24 @@ export class Threetobus{
|
||||
}
|
||||
|
||||
busReconnect(){
|
||||
this.commitConfig() // To resubscribe...
|
||||
//TODO : Not ideal because if we're in the middle of non-commited changes...
|
||||
this.commitConfig()
|
||||
if(this._frustumWatching) this._scheduleFrustumResubscribe()
|
||||
}
|
||||
|
||||
resolveSubscriberChan(chanTemplate) {
|
||||
if(typeof(chanTemplate) !== 'string') return(null)
|
||||
const uid = app.User?.identity?.uuid
|
||||
if(!uid) return(null)
|
||||
return(chanTemplate.replace(/\[UID\]/g, uid).replace(/\{uid\}/g, uid))
|
||||
}
|
||||
|
||||
_resolvedEventsMapping() {
|
||||
if(!Array.isArray(this._stagedEventsMapping)) return([])
|
||||
return(this._stagedEventsMapping.map(chanObj => {
|
||||
const chan = this.resolveSubscriberChan(chanObj.chan)
|
||||
if(!chan) return(null)
|
||||
return({ ...chanObj, chan })
|
||||
}).filter(Boolean))
|
||||
}
|
||||
|
||||
get EventsMapping() { return this._stagedEventsMapping }
|
||||
@@ -28,15 +54,21 @@ export class Threetobus{
|
||||
set EventsMapping(newConfig) { this._stagedEventsMapping = newConfig }
|
||||
|
||||
async commitConfig(){
|
||||
const resolvedMapping = this._resolvedEventsMapping()
|
||||
const chansToAdd = []
|
||||
const chansToKeep = []
|
||||
for(const chanObj of this._stagedEventsMapping){
|
||||
for(const chanObj of resolvedMapping){
|
||||
if(this._curEventsMapping.map(item => item.chan).includes(chanObj.chan)) chansToKeep.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)))
|
||||
await app.MessageBus.subscribe(chansToAdd.map(item => item.chan))
|
||||
await app.MessageBus.unSubscribe(chansToDel.map(item => item.chan))
|
||||
const chansToDel = this._curEventsMapping.filter(item => (
|
||||
!chansToKeep.map(c => c.chan).includes(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('unSubscribe:', chansToDel)
|
||||
|
||||
@@ -59,7 +91,7 @@ export class Threetobus{
|
||||
app.MessageBus.removeBusListener(eventToDel.eventName, this.processBusEvent.bind(this, eventToDel.eventName), 'threetobus')
|
||||
}
|
||||
|
||||
this._curEventsMapping = this.deepClone(this._stagedEventsMapping)
|
||||
this._curEventsMapping = this.deepClone(resolvedMapping)
|
||||
}
|
||||
|
||||
deepClone(obj) { // Needed because structuredClone doesn't take functions (and we have transformers)
|
||||
@@ -88,7 +120,8 @@ export class Threetobus{
|
||||
// if yes : how to discriminate static value from event-mapping definition ?
|
||||
if(mapping.child) id += '_'+mapping.child
|
||||
if(id){
|
||||
const obj3D = this.scene.getObjectByName(id)
|
||||
let obj3D = this.scene.getObjectByName(id)
|
||||
if(!obj3D) obj3D = this._ensurePlaceholderAgent(id)
|
||||
if(obj3D){
|
||||
this.assignFromConfig(payload, mapping, obj3D)
|
||||
}
|
||||
@@ -161,6 +194,99 @@ export class Threetobus{
|
||||
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){
|
||||
// Scene
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
+1
Submodule app/thirdparty/buildoz added at 9690401dad
Vendored
-206
@@ -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;
|
||||
}
|
||||
Vendored
-332
@@ -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)
|
||||
|
||||
Vendored
-610
@@ -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)
|
||||
|
||||
Vendored
+5
-4
@@ -3369,10 +3369,11 @@ class DataGrid extends EicComponent {
|
||||
let actions = ui.create(`<div class="cell actions"></div`);
|
||||
|
||||
if(this.options.rowActions && Array.isArray(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>`);
|
||||
button.addEventListener('click', action.callback.bind(row.getAttribute('data-id') != '' ? JSON.parse(row.getAttribute('data-id')): this));
|
||||
actions.appendChild(button);
|
||||
for(let action of this.options.rowActions) {
|
||||
const severityAttr = action.severity ? ` ${action.severity}` : ''
|
||||
let button = ui.create(`<button eicbutton rounded${severityAttr} title="${action.title ? action.title: ''}">${action.icon}</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 {
|
||||
actions = [
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
DOMContentLoaded() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
constructor() {
|
||||
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) {
|
||||
@@ -56,14 +68,43 @@ class KeyframeView extends WindozDomContent {
|
||||
this.kfArena = new app.LoadedModules.kfArena(this.outputs.kfArenaCanvas, this.agentSprites)
|
||||
this.kfArena.onclickAgent = this.onclickAgent.bind(this)
|
||||
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.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.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){
|
||||
if(this.outputs.agentsSelector.value) this.agentPreview.setAgent(this.outputs.agentsSelector.value)
|
||||
if(!this.outputs.agentsSelector.value) return
|
||||
@@ -73,36 +114,52 @@ class KeyframeView extends WindozDomContent {
|
||||
} else {
|
||||
this.currentAgentType = await this.models.agents.getProperties(this.outputs.agentsSelector.value)
|
||||
this.fillAgentProperties('', this.currentAgentType)
|
||||
// Deselect any on-scene selection
|
||||
if(this.currentlySelectedAid){
|
||||
this.kfArena.clearHighlight3DObj(this.kfArena.scene.getObjectByName(this.currentlySelectedAid), this.kfArena.scene)
|
||||
}
|
||||
this.currentlySelectedAid = null
|
||||
this.deselectSceneAgent()
|
||||
}
|
||||
}
|
||||
|
||||
async onChangeKeyframe(event){
|
||||
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 = {
|
||||
kfId: kfData.info.ekf_uuid,
|
||||
kfName: kfData.info.ekf_name,
|
||||
prevKfId: kfData.info.ekf_prev_uuid,
|
||||
}
|
||||
this.outputs.kfName.value = kfData.info.ekf_name
|
||||
this.deselectSceneAgent()
|
||||
this.kfArena.reloadAgents(kfData.agents)
|
||||
this.outputs.agentProperties.innerHTML = ''
|
||||
this.outputs.btnAddAgent.disabled = true
|
||||
this.updateKfButtons()
|
||||
}
|
||||
|
||||
onclickAgent(obj3D){
|
||||
const aid = obj3D.name
|
||||
if(this.currentlySelectedAid == aid){ // Deselect
|
||||
this.kfArena.clearHighlight3DObj(obj3D, this.kfArena.scene)
|
||||
this.currentlySelectedAid = null
|
||||
this.outputs.btnRemoveAgent.disabled = true
|
||||
this.deselectSceneAgent()
|
||||
} else { // Select
|
||||
if(this.currentlySelectedAid){
|
||||
this.kfArena.clearHighlight3DObj(this.kfArena.scene.getObjectByName(this.currentlySelectedAid), this.kfArena.scene)
|
||||
}
|
||||
this.deselectSceneAgent()
|
||||
this.currentlySelectedAid = aid
|
||||
this.outputs.btnRemoveAgent.disabled = false
|
||||
if(this.kfArena.agents[aid]) {
|
||||
@@ -127,6 +184,10 @@ class KeyframeView extends WindozDomContent {
|
||||
onRemoveAgent(event){
|
||||
if(!this.currentlySelectedAid) return
|
||||
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){
|
||||
@@ -139,20 +200,21 @@ class KeyframeView extends WindozDomContent {
|
||||
updateKfButtons(){
|
||||
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 }
|
||||
this.outputs.btnResetKF.disabled = !this.currentKeyframe.kfId
|
||||
}
|
||||
|
||||
onPropsChanged(evt, comp){
|
||||
if(this.currentlySelectedAid && this.kfArena.agents[this.currentlySelectedAid]){
|
||||
const AgentValues = this.getFieldsValues('div[data-output="agentProperties"]')
|
||||
this.kfArena.agents[this.currentlySelectedAid].values = AgentValues
|
||||
const val = Number.parseInt(comp.value, 10)
|
||||
if((comp.name.startsWith('position.')) && (!Number.isNaN(val))){
|
||||
const val = Number(comp.value)
|
||||
if((comp.name.startsWith('position.')) && (Number.isFinite(val))){
|
||||
this.kfArena.moveAgent(this.currentlySelectedAid, {
|
||||
x: this.getFieldValue('div[data-output="agentProperties"]', 'position.x'),
|
||||
y: this.getFieldValue('div[data-output="agentProperties"]', 'position.y'),
|
||||
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, {
|
||||
x: this.getFieldValue('div[data-output="agentProperties"]', 'speed.x'),
|
||||
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)
|
||||
this.outputs.btnSaveKF.disabled = true
|
||||
this.updateKfButtons()
|
||||
ui.growl.append('Keyframe saved!','success',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'
|
||||
|
||||
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 { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js'
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -1,8 +1,15 @@
|
||||
<style>
|
||||
article[eiccard] section:has(.confirm-dialog) {
|
||||
padding: 0;
|
||||
}
|
||||
.confirm-dialog > section {
|
||||
margin:0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="confirm-dialog">
|
||||
<section>
|
||||
<alert eicalert ${severity} ${muted}>
|
||||
${message}
|
||||
</alert>
|
||||
<section eicalert ${severity}>
|
||||
${message}
|
||||
</section>
|
||||
|
||||
</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
|
||||
* @subcategory Views
|
||||
|
||||
@@ -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 {
|
||||
|
||||
constructor() {
|
||||
@@ -6,16 +18,20 @@ class SpaceView extends WindozDomContent {
|
||||
//this.tileMarkup = app.Assets.Store.html['/app/assets/html/mailing/tile.html']
|
||||
}
|
||||
|
||||
DOMContentFocused(options) {
|
||||
if(this.wasBlured){ // Avoid 2nd refesh on DomContentLoaded
|
||||
//this.refreshyoustuff()
|
||||
DOMContentFocused() {
|
||||
if(this.wasBlured && this.viewMode === '3D' && this.ttb && this.renderingEngine) {
|
||||
this.ttb.watchCameraFrustum(this.renderingEngine)
|
||||
}
|
||||
this.wasBlured = false
|
||||
}
|
||||
|
||||
DOMContentBlured(options) { this.wasBlured = true }
|
||||
DOMContentBlured() {
|
||||
this.wasBlured = true
|
||||
if(this.viewMode === '3D' && this.ttb) this.ttb.stopWatchingCameraFrustum()
|
||||
}
|
||||
|
||||
DOMContentLoaded(options) {
|
||||
this.viewMode = options.mode
|
||||
this.windowPrefsId = `live.spaceview.${options.mode}`
|
||||
for(let model in options.models) this[model] = options.models[model]
|
||||
this.ttb = options.ttb
|
||||
@@ -23,6 +39,9 @@ class SpaceView extends WindozDomContent {
|
||||
this.setupTriggers(components)
|
||||
this.setupRefs(components)
|
||||
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.outputs.settingsMenu.querySelectorAll('input[type="toggler"]').forEach(el => {
|
||||
const tog = new InputToggler(el)
|
||||
|
||||
+185
@@ -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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user