diff --git a/.cursor/rules/sql-rules.mdc b/.cursor/rules/sql-rules.mdc new file mode 100644 index 0000000..865d1ba --- /dev/null +++ b/.cursor/rules/sql-rules.mdc @@ -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. diff --git a/app/assets/json/arena/arenaConfig1.json b/app/assets/json/arena/arenaConfig1.json index 609789d..75255be 100644 --- a/app/assets/json/arena/arenaConfig1.json +++ b/app/assets/json/arena/arenaConfig1.json @@ -4,4 +4,4 @@ "y": 100, "z": 100 } -} \ No newline at end of file +} diff --git a/app/assets/json/global/busChannels.json b/app/assets/json/global/busChannels.json new file mode 100644 index 0000000..4f32170 --- /dev/null +++ b/app/assets/json/global/busChannels.json @@ -0,0 +1,11 @@ +{ + "maestro": { + "actionsChannel": "system:requests:maestro", + "lifecycleChannel": "system:maestro:lifecycle:[UID]" + }, + "observer": { + "actionsChannel": "system:requests:observer", + "subscribeFrequencyMs": 1000, + "cameraDebounceMs": 600 + } +} diff --git a/app/assets/json/global/services.json b/app/assets/json/global/services.json index 5afd364..aa8e0a0 100644 --- a/app/assets/json/global/services.json +++ b/app/assets/json/global/services.json @@ -44,20 +44,12 @@ "method": "POST" }, "get": { - "uri": "/api/sims/{simId}", + "uri": "/api/sims/{simulationUuid}", "method": "GET" }, "create": { "uri": "/api/sims", "method": "PUT" - }, - "start": { - "uri": "/api/sims/{simId}/start", - "method": "PUT" - }, - "pause": { - "uri": "/api/sims/{simId}/pause", - "method": "PUT" } } } diff --git a/app/assets/json/threetobus/eventsMapping.json b/app/assets/json/threetobus/eventsMapping.json index f3ca2d9..338aa8d 100644 --- a/app/assets/json/threetobus/eventsMapping.json +++ b/app/assets/json/threetobus/eventsMapping.json @@ -1,6 +1,6 @@ [ { - "chan": "system:gps:agents", + "chan": "system:observer:subscribed[UID]:agents", "events": [ { "eventName": "move", diff --git a/app/assets/styles/app.css b/app/assets/styles/app.css index ce72c0f..166220e 100755 --- a/app/assets/styles/app.css +++ b/app/assets/styles/app.css @@ -397,6 +397,10 @@ 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; diff --git a/app/controllers/WindozAppController.json b/app/controllers/WindozAppController.json index 179b352..f3a60cc 100755 --- a/app/controllers/WindozAppController.json +++ b/app/controllers/WindozAppController.json @@ -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."} ] } diff --git a/app/controllers/live/DashboardsController.js b/app/controllers/live/DashboardsController.js index 9c989ae..a335d8e 100644 --- a/app/controllers/live/DashboardsController.js +++ b/app/controllers/live/DashboardsController.js @@ -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, diff --git a/app/controllers/sims/SimsController.js b/app/controllers/sims/SimsController.js index e745aae..6078574 100644 --- a/app/controllers/sims/SimsController.js +++ b/app/controllers/sims/SimsController.js @@ -39,7 +39,7 @@ class SimsController extends WindozController { static: true, expanded: 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, diff --git a/app/models/SimsModel.js b/app/models/SimsModel.js index abd5066..6ff02ba 100644 --- a/app/models/SimsModel.js +++ b/app/models/SimsModel.js @@ -1,20 +1,36 @@ -class SimsModel extends WindozModel { +class SimsModel extends WindozBusModel { constructor() { super() this.ressource = '/sims' + this.busChannels = app.Assets.Store.json.busChannels } async list() { - let endpoint = {...app.config.api[this.ressource].list} - return( - this.request(endpoint.uri, endpoint.method) + 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(simId) { + async get(simulationUuid) { let endpoint = {...app.config.api[this.ressource].get} - endpoint.uri = endpoint.uri.replace('{simId}', simId) + endpoint.uri = endpoint.uri.replace('{simulationUuid}', simulationUuid) return( this.request(endpoint.uri, endpoint.method) ) @@ -27,20 +43,43 @@ class SimsModel extends WindozModel { ) } - async start(simId) { - let endpoint = {...app.config.api[this.ressource].start} - endpoint.uri = endpoint.uri.replace('{simId}', simId) - return( - this.request(endpoint.uri, endpoint.method) - ) + async startSimulation(simulationUuid) { + return(this.busActionRequest( + this.busChannels.maestro.actionsChannel, + 'STARTSIMULATION', + { + simulationUuid, + infraId: null, + }, + 60000 + )) } - async pause(simId) { - let endpoint = {...app.config.api[this.ressource].pause} - endpoint.uri = endpoint.uri.replace('{simId}', simId) - return( - this.request(endpoint.uri, endpoint.method) - ) + 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 + )) } } diff --git a/app/thirdparty/Threetobus/threetobus.module.js b/app/thirdparty/Threetobus/threetobus.module.js index 3f171cb..f9bbdb2 100644 --- a/app/thirdparty/Threetobus/threetobus.module.js +++ b/app/thirdparty/Threetobus/threetobus.module.js @@ -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() diff --git a/app/thirdparty/eicui/eicui-2.1.js b/app/thirdparty/eicui/eicui-2.1.js index da3087c..aa34a30 100755 --- a/app/thirdparty/eicui/eicui-2.1.js +++ b/app/thirdparty/eicui/eicui-2.1.js @@ -3369,10 +3369,11 @@ class DataGrid extends EicComponent { let actions = ui.create(`
${action.icon}`); - 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.addEventListener('click', action.callback.bind(row.getAttribute('data-id') != '' ? JSON.parse(row.getAttribute('data-id')): this)) + actions.appendChild(button) } } diff --git a/app/views/sims/CreateSimView.js b/app/views/sims/CreateSimView.js index 5ed44c3..8baa60f 100644 --- a/app/views/sims/CreateSimView.js +++ b/app/views/sims/CreateSimView.js @@ -26,7 +26,10 @@ class CreateSimView extends WindozDomContent { this.models.sims.create({ kfId: this.outputs.keyframesSelector.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() } } diff --git a/app/views/sims/ManageSimView.html b/app/views/sims/ManageSimView.html index b49391a..1673d9c 100644 --- a/app/views/sims/ManageSimView.html +++ b/app/views/sims/ManageSimView.html @@ -1,8 +1,18 @@ +
-

Play / Pause a simulation

+

Play / Pause / Stop a simulation

-

TODO: simulation play / pause controls

+
diff --git a/app/views/sims/ManageSimView.js b/app/views/sims/ManageSimView.js index 71fbb7e..261c28d 100644 --- a/app/views/sims/ManageSimView.js +++ b/app/views/sims/ManageSimView.js @@ -1,14 +1,204 @@ class ManageSimView extends WindozDomContent { + #lifecycleEventTypes = [ + 'onYourMarks', + 'bigBang', + 'simulationPaused', + 'simulationResumed', + 'simulationStopped', + 'simulationPrepareFailed', + ] + constructor() { 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) { this.models = options.models + this.wasBlured = false + this._refreshSeq = 0 + this.lifecycleSubscribed = false 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: '', + title: 'Start simulation', + severity: 'success', + callback: async function(event) { + await view.onPlaySim(this, event.currentTarget) + }, + }, + { + icon: '', + title: 'Pause simulation', + severity: 'secondary', + callback: async function(event) { + await view.onPauseSim(this, event.currentTarget) + }, + }, + { + icon: '', + 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) + } } } diff --git a/app/views/visualisers/SpaceView.js b/app/views/visualisers/SpaceView.js index 0a2b979..f6ad89c 100644 --- a/app/views/visualisers/SpaceView.js +++ b/app/views/visualisers/SpaceView.js @@ -18,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 @@ -35,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) diff --git a/doc/prompts_blobs.txt b/doc/prompts_blobs.txt index 8b50979..3616b61 100644 --- a/doc/prompts_blobs.txt +++ b/doc/prompts_blobs.txt @@ -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. +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.