working on simManage
This commit is contained in:
@@ -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.
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
"y": 100,
|
"y": 100,
|
||||||
"z": 100
|
"z": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"maestro": {
|
||||||
|
"actionsChannel": "system:requests:maestro",
|
||||||
|
"lifecycleChannel": "system:maestro:lifecycle:[UID]"
|
||||||
|
},
|
||||||
|
"observer": {
|
||||||
|
"actionsChannel": "system:requests:observer",
|
||||||
|
"subscribeFrequencyMs": 1000,
|
||||||
|
"cameraDebounceMs": 600
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,20 +44,12 @@
|
|||||||
"method": "POST"
|
"method": "POST"
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"uri": "/api/sims/{simId}",
|
"uri": "/api/sims/{simulationUuid}",
|
||||||
"method": "GET"
|
"method": "GET"
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"uri": "/api/sims",
|
"uri": "/api/sims",
|
||||||
"method": "PUT"
|
"method": "PUT"
|
||||||
},
|
|
||||||
"start": {
|
|
||||||
"uri": "/api/sims/{simId}/start",
|
|
||||||
"method": "PUT"
|
|
||||||
},
|
|
||||||
"pause": {
|
|
||||||
"uri": "/api/sims/{simId}/pause",
|
|
||||||
"method": "PUT"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"chan": "system:gps:agents",
|
"chan": "system:observer:subscribed[UID]:agents",
|
||||||
"events": [
|
"events": [
|
||||||
{
|
{
|
||||||
"eventName": "move",
|
"eventName": "move",
|
||||||
|
|||||||
@@ -397,6 +397,10 @@ div.window > section button[eicbutton][rounded] {
|
|||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[eicdatagrid] .dataset .row:hover {
|
||||||
|
background-color: #483;
|
||||||
|
}
|
||||||
|
|
||||||
/* Customizations to buildoz*/
|
/* Customizations to buildoz*/
|
||||||
bz-select > button{
|
bz-select > button{
|
||||||
background: linear-gradient( to bottom, #251, #372 15%, #483 50%, #372 85%, #251 ) !important;
|
background: linear-gradient( to bottom, #251, #372 15%, #483 50%, #372 85%, #251 ) !important;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
],
|
],
|
||||||
"json": [
|
"json": [
|
||||||
{"name":"global/app-menu-map.json"},
|
{"name":"global/app-menu-map.json"},
|
||||||
|
{"id":"busChannels", "name":"global/busChannels.json"},
|
||||||
{"path": "/app/controllers/common/", "name": "errorController.json", "comment": "Trick to preload errorController stuff, to still have error messages if S3 is down."}
|
{"path": "/app/controllers/common/", "name": "errorController.json", "comment": "Trick to preload errorController stuff, to still have error messages if S3 is down."}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ class DashboardsController extends WindozController {
|
|||||||
constructor(params) {
|
constructor(params) {
|
||||||
super(params)
|
super(params)
|
||||||
this.arenaConfig = app.Assets.Store.json.arenaConfig
|
this.arenaConfig = app.Assets.Store.json.arenaConfig
|
||||||
|
this.busChannels = app.Assets.Store.json.busChannels
|
||||||
this.eventsMapping = app.Assets.Store.json.eventsMapping
|
this.eventsMapping = app.Assets.Store.json.eventsMapping
|
||||||
console.log('=============>DashboardsController constructor')
|
console.log('=============>DashboardsController constructor')
|
||||||
}
|
}
|
||||||
@@ -21,6 +22,7 @@ class DashboardsController extends WindozController {
|
|||||||
const ttb = new app.LoadedModules.Threetobus({
|
const ttb = new app.LoadedModules.Threetobus({
|
||||||
eventsMapping: this.eventsMapping,
|
eventsMapping: this.eventsMapping,
|
||||||
sceneSize: this.arenaConfig.arenaSize,
|
sceneSize: this.arenaConfig.arenaSize,
|
||||||
|
observer: this.busChannels.observer,
|
||||||
})
|
})
|
||||||
ttb.initScene({
|
ttb.initScene({
|
||||||
axes: true,
|
axes: true,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class SimsController extends WindozController {
|
|||||||
static: true,
|
static: true,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
withSettings: false,
|
withSettings: false,
|
||||||
windowStyle: WindozDomContent.boxFromPrefs('sims.managesimview', { x: 50, y: 50, w: 800, h: 600 }),
|
windowStyle: WindozDomContent.boxFromPrefs('sims.managesimview', { x: 50, y: 50, w: 1000, h: 600 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
models: models,
|
models: models,
|
||||||
|
|||||||
+57
-18
@@ -1,20 +1,36 @@
|
|||||||
class SimsModel extends WindozModel {
|
class SimsModel extends WindozBusModel {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.ressource = '/sims'
|
this.ressource = '/sims'
|
||||||
|
this.busChannels = app.Assets.Store.json.busChannels
|
||||||
}
|
}
|
||||||
|
|
||||||
async list() {
|
async list() {
|
||||||
let endpoint = {...app.config.api[this.ressource].list}
|
const endpoint = {...app.config.api[this.ressource].list}
|
||||||
return(
|
const [listResponse, statusList] = await Promise.all([
|
||||||
this.request(endpoint.uri, endpoint.method)
|
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(simId) {
|
async get(simulationUuid) {
|
||||||
let endpoint = {...app.config.api[this.ressource].get}
|
let endpoint = {...app.config.api[this.ressource].get}
|
||||||
endpoint.uri = endpoint.uri.replace('{simId}', simId)
|
endpoint.uri = endpoint.uri.replace('{simulationUuid}', simulationUuid)
|
||||||
return(
|
return(
|
||||||
this.request(endpoint.uri, endpoint.method)
|
this.request(endpoint.uri, endpoint.method)
|
||||||
)
|
)
|
||||||
@@ -27,20 +43,43 @@ class SimsModel extends WindozModel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(simId) {
|
async startSimulation(simulationUuid) {
|
||||||
let endpoint = {...app.config.api[this.ressource].start}
|
return(this.busActionRequest(
|
||||||
endpoint.uri = endpoint.uri.replace('{simId}', simId)
|
this.busChannels.maestro.actionsChannel,
|
||||||
return(
|
'STARTSIMULATION',
|
||||||
this.request(endpoint.uri, endpoint.method)
|
{
|
||||||
)
|
simulationUuid,
|
||||||
|
infraId: null,
|
||||||
|
},
|
||||||
|
60000
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause(simId) {
|
async stopSimulation(simulationUuid) {
|
||||||
let endpoint = {...app.config.api[this.ressource].pause}
|
return(this.busActionRequest(
|
||||||
endpoint.uri = endpoint.uri.replace('{simId}', simId)
|
this.busChannels.maestro.actionsChannel,
|
||||||
return(
|
'STOPSIMULATION',
|
||||||
this.request(endpoint.uri, endpoint.method)
|
{ 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
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+134
-8
@@ -9,6 +9,16 @@ export class Threetobus{
|
|||||||
this._curEventsMapping = []
|
this._curEventsMapping = []
|
||||||
this._stagedEventsMapping = options.eventsMapping
|
this._stagedEventsMapping = options.eventsMapping
|
||||||
this.sceneSize = options.sceneSize
|
this.sceneSize = options.sceneSize
|
||||||
|
this._observerConfig = {
|
||||||
|
actionsChannel: 'system:requests:observer',
|
||||||
|
subscribeFrequencyMs: 1000,
|
||||||
|
cameraDebounceMs: 600,
|
||||||
|
...options.observer,
|
||||||
|
}
|
||||||
|
this._frustumRenderEngine = null
|
||||||
|
this._frustumDebounceTimer = null
|
||||||
|
this._frustumCameraChangeHandler = null
|
||||||
|
this._frustumWatching = false
|
||||||
this.commitConfig()
|
this.commitConfig()
|
||||||
|
|
||||||
this.cameras = {}
|
this.cameras = {}
|
||||||
@@ -19,8 +29,24 @@ export class Threetobus{
|
|||||||
}
|
}
|
||||||
|
|
||||||
busReconnect(){
|
busReconnect(){
|
||||||
this.commitConfig() // To resubscribe...
|
this.commitConfig()
|
||||||
//TODO : Not ideal because if we're in the middle of non-commited changes...
|
if(this._frustumWatching) this._scheduleFrustumResubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
get EventsMapping() { return this._stagedEventsMapping }
|
||||||
@@ -28,15 +54,21 @@ export class Threetobus{
|
|||||||
set EventsMapping(newConfig) { this._stagedEventsMapping = newConfig }
|
set EventsMapping(newConfig) { this._stagedEventsMapping = newConfig }
|
||||||
|
|
||||||
async commitConfig(){
|
async commitConfig(){
|
||||||
|
const resolvedMapping = this._resolvedEventsMapping()
|
||||||
const chansToAdd = []
|
const chansToAdd = []
|
||||||
const chansToKeep = []
|
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)
|
if(this._curEventsMapping.map(item => item.chan).includes(chanObj.chan)) chansToKeep.push(chanObj)
|
||||||
else chansToAdd.push(chanObj)
|
else chansToAdd.push(chanObj)
|
||||||
}
|
}
|
||||||
const chansToDel = this._curEventsMapping.filter(item => (!chansToKeep.map(c=>c.chan).includes(item.chan) && !chansToAdd.map(c=>c.chan).includes(item.chan)))
|
const chansToDel = this._curEventsMapping.filter(item => (
|
||||||
await app.MessageBus.subscribe(chansToAdd.map(item => item.chan))
|
!chansToKeep.map(c => c.chan).includes(item.chan)
|
||||||
await app.MessageBus.unSubscribe(chansToDel.map(item => item.chan))
|
&& !chansToAdd.map(c => c.chan).includes(item.chan)
|
||||||
|
))
|
||||||
|
if(app.MessageBus?.connected) {
|
||||||
|
if(chansToAdd.length) await app.MessageBus.subscribe(chansToAdd.map(item => item.chan))
|
||||||
|
if(chansToDel.length) await app.MessageBus.unSubscribe(chansToDel.map(item => item.chan))
|
||||||
|
}
|
||||||
// console.log('subscribe:', chansToAdd.map(item => item.chan))
|
// console.log('subscribe:', chansToAdd.map(item => item.chan))
|
||||||
// console.log('unSubscribe:', chansToDel)
|
// console.log('unSubscribe:', chansToDel)
|
||||||
|
|
||||||
@@ -59,7 +91,7 @@ export class Threetobus{
|
|||||||
app.MessageBus.removeBusListener(eventToDel.eventName, this.processBusEvent.bind(this, eventToDel.eventName), '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)
|
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 yes : how to discriminate static value from event-mapping definition ?
|
||||||
if(mapping.child) id += '_'+mapping.child
|
if(mapping.child) id += '_'+mapping.child
|
||||||
if(id){
|
if(id){
|
||||||
const obj3D = this.scene.getObjectByName(id)
|
let obj3D = this.scene.getObjectByName(id)
|
||||||
|
if(!obj3D) obj3D = this._ensurePlaceholderAgent(id)
|
||||||
if(obj3D){
|
if(obj3D){
|
||||||
this.assignFromConfig(payload, mapping, obj3D)
|
this.assignFromConfig(payload, mapping, obj3D)
|
||||||
}
|
}
|
||||||
@@ -161,6 +194,99 @@ export class Threetobus{
|
|||||||
return(path.split('.').reduce((acc, key) => acc?.[key], obj))
|
return(path.split('.').reduce((acc, key) => acc?.[key], obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watchCameraFrustum(renderEngine) {
|
||||||
|
if(!renderEngine || renderEngine.mode !== '3D') return
|
||||||
|
if(!app.MessageBus?.config?.enabled) {
|
||||||
|
console.warn('[Threetobus] MessageBus disabled — camera frustum watch skipped')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._frustumRenderEngine = renderEngine
|
||||||
|
this._frustumWatching = true
|
||||||
|
|
||||||
|
if(!this._frustumCameraChangeHandler) {
|
||||||
|
this._frustumCameraChangeHandler = () => this._scheduleFrustumResubscribe()
|
||||||
|
renderEngine.controls.addEventListener('change', this._frustumCameraChangeHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.MessageBus.whenConnected(() => {
|
||||||
|
this.commitConfig()
|
||||||
|
this._scheduleFrustumResubscribe()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopWatchingCameraFrustum() {
|
||||||
|
this._frustumWatching = false
|
||||||
|
if(this._frustumDebounceTimer) {
|
||||||
|
clearTimeout(this._frustumDebounceTimer)
|
||||||
|
this._frustumDebounceTimer = null
|
||||||
|
}
|
||||||
|
if(this._frustumRenderEngine?.controls && this._frustumCameraChangeHandler) {
|
||||||
|
this._frustumRenderEngine.controls.removeEventListener('change', this._frustumCameraChangeHandler)
|
||||||
|
}
|
||||||
|
this._frustumRenderEngine = null
|
||||||
|
this._frustumCameraChangeHandler = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_scheduleFrustumResubscribe() {
|
||||||
|
if(!this._frustumWatching) return
|
||||||
|
if(this._frustumDebounceTimer) clearTimeout(this._frustumDebounceTimer)
|
||||||
|
this._frustumDebounceTimer = setTimeout(() => {
|
||||||
|
this._frustumDebounceTimer = null
|
||||||
|
this._resubscribeCameraFrustum()
|
||||||
|
}, this._observerConfig.cameraDebounceMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _resubscribeCameraFrustum() {
|
||||||
|
if(!this._frustumWatching || !this._frustumRenderEngine) return
|
||||||
|
if(!app.MessageBus?.connected) return
|
||||||
|
|
||||||
|
const camera = this._frustumRenderEngine.camera
|
||||||
|
if(!camera?.isPerspectiveCamera) return
|
||||||
|
|
||||||
|
const planes = this._cameraFrustumPlanes(camera)
|
||||||
|
const chan = this._observerConfig.actionsChannel
|
||||||
|
|
||||||
|
try {
|
||||||
|
await app.MessageBus.requestBusAction(
|
||||||
|
chan,
|
||||||
|
'SUBSCRIBEFRUSTUM',
|
||||||
|
{
|
||||||
|
planes,
|
||||||
|
frequency: this._observerConfig.subscribeFrequencyMs,
|
||||||
|
},
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
} catch(err) {
|
||||||
|
console.warn('[Threetobus] SUBSCRIBEFRUSTUM failed:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_cameraFrustumPlanes(camera) {
|
||||||
|
camera.updateMatrixWorld(true)
|
||||||
|
const matrix = new THREE.Matrix4().multiplyMatrices(
|
||||||
|
camera.projectionMatrix,
|
||||||
|
camera.matrixWorldInverse
|
||||||
|
)
|
||||||
|
const frustum = new THREE.Frustum().setFromProjectionMatrix(matrix)
|
||||||
|
return(frustum.planes.map(plane => ({
|
||||||
|
nx: plane.normal.x,
|
||||||
|
ny: plane.normal.y,
|
||||||
|
nz: plane.normal.z,
|
||||||
|
d: plane.constant,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensurePlaceholderAgent(agentId) {
|
||||||
|
const geo = new THREE.BoxGeometry(0.4, 0.4, 0.4)
|
||||||
|
const mat = new THREE.MeshStandardMaterial({ color: 0x44aa88 })
|
||||||
|
const obj3D = new THREE.Mesh(geo, mat)
|
||||||
|
obj3D.name = agentId
|
||||||
|
this.tweensRegistry[agentId] = { move: null, rotate: null }
|
||||||
|
this.scene.add(obj3D)
|
||||||
|
return(obj3D)
|
||||||
|
}
|
||||||
|
|
||||||
initScene(options){
|
initScene(options){
|
||||||
// Scene
|
// Scene
|
||||||
this.scene = new THREE.Scene()
|
this.scene = new THREE.Scene()
|
||||||
|
|||||||
Vendored
+5
-4
@@ -3369,10 +3369,11 @@ class DataGrid extends EicComponent {
|
|||||||
let actions = ui.create(`<div class="cell actions"></div`);
|
let actions = ui.create(`<div class="cell actions"></div`);
|
||||||
|
|
||||||
if(this.options.rowActions && Array.isArray(this.options.rowActions)) {
|
if(this.options.rowActions && Array.isArray(this.options.rowActions)) {
|
||||||
for(let action of this.options.rowActions) {
|
for(let action of this.options.rowActions) {
|
||||||
let button = ui.create(`<button eicbutton rounded title="${action.title ? action.title: ''}">${action.icon}</button>`);
|
const severityAttr = action.severity ? ` ${action.severity}` : ''
|
||||||
button.addEventListener('click', action.callback.bind(row.getAttribute('data-id') != '' ? JSON.parse(row.getAttribute('data-id')): this));
|
let button = ui.create(`<button eicbutton rounded${severityAttr} title="${action.title ? action.title: ''}">${action.icon}</button>`)
|
||||||
actions.appendChild(button);
|
button.addEventListener('click', action.callback.bind(row.getAttribute('data-id') != '' ? JSON.parse(row.getAttribute('data-id')): this))
|
||||||
|
actions.appendChild(button)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ class CreateSimView extends WindozDomContent {
|
|||||||
this.models.sims.create({
|
this.models.sims.create({
|
||||||
kfId: this.outputs.keyframesSelector.value,
|
kfId: this.outputs.keyframesSelector.value,
|
||||||
simName: this.outputs.simName.value
|
simName: this.outputs.simName.value
|
||||||
}).then(data => ui.growl.append('Simulation created!','success',3000))
|
}).then(data => {
|
||||||
|
const simulationUuid = data?.payload?.simulationUuid ?? 'unknown'
|
||||||
|
ui.growl.append(`Simulation created (${simulationUuid})`, 'success', 4000)
|
||||||
|
})
|
||||||
this.unload()
|
this.unload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +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">
|
<article eiccard class="manage-sim">
|
||||||
<header>
|
<header>
|
||||||
<h1>Play / Pause a simulation</h1>
|
<h1>Play / Pause / Stop a simulation</h1>
|
||||||
</header>
|
</header>
|
||||||
<section>
|
<section>
|
||||||
<p>TODO: simulation play / pause controls</p>
|
<div eicdatagrid class="sim-list"></div>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -1,14 +1,204 @@
|
|||||||
class ManageSimView extends WindozDomContent {
|
class ManageSimView extends WindozDomContent {
|
||||||
|
|
||||||
|
#lifecycleEventTypes = [
|
||||||
|
'onYourMarks',
|
||||||
|
'bigBang',
|
||||||
|
'simulationPaused',
|
||||||
|
'simulationResumed',
|
||||||
|
'simulationStopped',
|
||||||
|
'simulationPrepareFailed',
|
||||||
|
]
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
Object.assign(this, app.helpers.basicDialogs)
|
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) {
|
async DOMContentLoaded(options) {
|
||||||
this.models = options.models
|
this.models = options.models
|
||||||
|
this.wasBlured = false
|
||||||
|
this._refreshSeq = 0
|
||||||
|
this.lifecycleSubscribed = false
|
||||||
ui.eicfy(this.el)
|
ui.eicfy(this.el)
|
||||||
// TODO: implement
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,16 +18,20 @@ class SpaceView extends WindozDomContent {
|
|||||||
//this.tileMarkup = app.Assets.Store.html['/app/assets/html/mailing/tile.html']
|
//this.tileMarkup = app.Assets.Store.html['/app/assets/html/mailing/tile.html']
|
||||||
}
|
}
|
||||||
|
|
||||||
DOMContentFocused(options) {
|
DOMContentFocused() {
|
||||||
if(this.wasBlured){ // Avoid 2nd refesh on DomContentLoaded
|
if(this.wasBlured && this.viewMode === '3D' && this.ttb && this.renderingEngine) {
|
||||||
//this.refreshyoustuff()
|
this.ttb.watchCameraFrustum(this.renderingEngine)
|
||||||
}
|
}
|
||||||
this.wasBlured = false
|
this.wasBlured = false
|
||||||
}
|
}
|
||||||
|
|
||||||
DOMContentBlured(options) { this.wasBlured = true }
|
DOMContentBlured() {
|
||||||
|
this.wasBlured = true
|
||||||
|
if(this.viewMode === '3D' && this.ttb) this.ttb.stopWatchingCameraFrustum()
|
||||||
|
}
|
||||||
|
|
||||||
DOMContentLoaded(options) {
|
DOMContentLoaded(options) {
|
||||||
|
this.viewMode = options.mode
|
||||||
this.windowPrefsId = `live.spaceview.${options.mode}`
|
this.windowPrefsId = `live.spaceview.${options.mode}`
|
||||||
for(let model in options.models) this[model] = options.models[model]
|
for(let model in options.models) this[model] = options.models[model]
|
||||||
this.ttb = options.ttb
|
this.ttb = options.ttb
|
||||||
@@ -35,6 +39,9 @@ class SpaceView extends WindozDomContent {
|
|||||||
this.setupTriggers(components)
|
this.setupTriggers(components)
|
||||||
this.setupRefs(components)
|
this.setupRefs(components)
|
||||||
this.renderingEngine = this.ttb.startRendering(this.outputs.ttbCanvas, options.mode)
|
this.renderingEngine = this.ttb.startRendering(this.outputs.ttbCanvas, options.mode)
|
||||||
|
if(options.mode === '3D') {
|
||||||
|
this.ttb.watchCameraFrustum(this.renderingEngine)
|
||||||
|
}
|
||||||
this.output('settingsMenu',app.Assets.Store.html.spaceViewSetting)
|
this.output('settingsMenu',app.Assets.Store.html.spaceViewSetting)
|
||||||
this.outputs.settingsMenu.querySelectorAll('input[type="toggler"]').forEach(el => {
|
this.outputs.settingsMenu.querySelectorAll('input[type="toggler"]').forEach(el => {
|
||||||
const tog = new InputToggler(el)
|
const tog = new InputToggler(el)
|
||||||
|
|||||||
@@ -95,3 +95,11 @@ way of coexist both actions and events.
|
|||||||
|
|
||||||
Describe a proposal that stays close to what we have, but separates events from actions.
|
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