From 06a7868882580205b3b477ffe0eaab6bbcd5643c Mon Sep 17 00:00:00 2001 From: STEINNI Date: Sun, 21 Jun 2026 12:08:25 +0000 Subject: [PATCH] better default agent props & speed & position numeric, not strings --- app/helpers/formBuilder.js | 20 +++++++++-- app/models/AgentsModel.js | 10 +++++- app/models/KeyframesModel.js | 19 +++++++++- app/views/editors/KeyframeView.js | 6 ++-- doc/prompts_blobs.txt | 60 ++++++++++++++++++++++++++++++- 5 files changed, 106 insertions(+), 9 deletions(-) diff --git a/app/helpers/formBuilder.js b/app/helpers/formBuilder.js index bb20e7b..0be0a25 100644 --- a/app/helpers/formBuilder.js +++ b/app/helpers/formBuilder.js @@ -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) }, diff --git a/app/models/AgentsModel.js b/app/models/AgentsModel.js index 249b8cb..1ceb251 100644 --- a/app/models/AgentsModel.js +++ b/app/models/AgentsModel.js @@ -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) } } diff --git a/app/models/KeyframesModel.js b/app/models/KeyframesModel.js index fe10482..141a2ea 100644 --- a/app/models/KeyframesModel.js +++ b/app/models/KeyframesModel.js @@ -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 }, }) }) diff --git a/app/views/editors/KeyframeView.js b/app/views/editors/KeyframeView.js index fba67c8..e43c7ff 100644 --- a/app/views/editors/KeyframeView.js +++ b/app/views/editors/KeyframeView.js @@ -207,14 +207,14 @@ class KeyframeView extends WindozDomContent { 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'), diff --git a/doc/prompts_blobs.txt b/doc/prompts_blobs.txt index 1aaa99b..8b50979 100644 --- a/doc/prompts_blobs.txt +++ b/doc/prompts_blobs.txt @@ -36,4 +36,62 @@ The reason for this remark is to take provision for a future where I might want -{ "eventType": "remove", "payload": { }, "sender": "agent42" } \ No newline at end of file +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. +