better default agent props & speed & position numeric, not strings
This commit is contained in:
@@ -47,7 +47,13 @@ app.helpers.formBuilder = {
|
|||||||
}
|
}
|
||||||
if(component){
|
if(component){
|
||||||
component.classList.add('formbuilder-field')
|
component.classList.add('formbuilder-field')
|
||||||
component.value = this.getPathInObj(fieldsValues, propName) || fieldsObj[propName].default
|
const rawValue = this.getPathInObj(fieldsValues, propName) ?? fieldsObj[propName].default
|
||||||
|
if(fieldsObj[propName].type === 'number') {
|
||||||
|
const n = Number(rawValue)
|
||||||
|
component.value = Number.isFinite(n) ? n : 0
|
||||||
|
} else {
|
||||||
|
component.value = rawValue
|
||||||
|
}
|
||||||
fieldRow.append(component)
|
fieldRow.append(component)
|
||||||
allFields.push(fieldRow)
|
allFields.push(fieldRow)
|
||||||
}
|
}
|
||||||
@@ -77,6 +83,14 @@ app.helpers.formBuilder = {
|
|||||||
return(target[parts[parts.length - 1]] )
|
return(target[parts[parts.length - 1]] )
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#fieldValue(el) {
|
||||||
|
if(el?.type === 'number') {
|
||||||
|
const n = Number(el.value)
|
||||||
|
return(Number.isFinite(n) ? n : el.value)
|
||||||
|
}
|
||||||
|
return(el.value)
|
||||||
|
},
|
||||||
|
|
||||||
getFieldsValues(rootSel){
|
getFieldsValues(rootSel){
|
||||||
const result = {}
|
const result = {}
|
||||||
document.querySelectorAll(`${rootSel} .formbuilder-field`).forEach(el => {
|
document.querySelectorAll(`${rootSel} .formbuilder-field`).forEach(el => {
|
||||||
@@ -89,14 +103,14 @@ app.helpers.formBuilder = {
|
|||||||
}
|
}
|
||||||
target = target[key]
|
target = target[key]
|
||||||
}
|
}
|
||||||
target[path[path.length - 1]] = el.value
|
target[path[path.length - 1]] = this.#fieldValue(el)
|
||||||
})
|
})
|
||||||
return(result)
|
return(result)
|
||||||
},
|
},
|
||||||
|
|
||||||
getFieldValue(rootSel, name){
|
getFieldValue(rootSel, name){
|
||||||
const comp = document.querySelector(`${rootSel} .formbuilder-field[name="${name}"]`)
|
const comp = document.querySelector(`${rootSel} .formbuilder-field[name="${name}"]`)
|
||||||
if(comp) return(comp.value)
|
if(comp) return(this.#fieldValue(comp))
|
||||||
else return(null)
|
else return(null)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,15 @@ class AgentsModel extends WindozModel {
|
|||||||
async getDefaultProps(id){
|
async getDefaultProps(id){
|
||||||
const aprops = await this.getProperties(id)
|
const aprops = await this.getProperties(id)
|
||||||
const defaults={ position: { x:0, y:0, z:0 }, speed: { x:0, y:0, z:0 }}
|
const defaults={ position: { x:0, y:0, z:0 }, speed: { x:0, y:0, z:0 }}
|
||||||
for(const p in aprops) defaults[p] = aprops[p].default
|
for(const p in aprops) {
|
||||||
|
const prop = aprops[p]
|
||||||
|
if(prop?.type === 'number') {
|
||||||
|
const n = Number(prop.default)
|
||||||
|
defaults[p] = Number.isFinite(n) ? n : 0
|
||||||
|
} else {
|
||||||
|
defaults[p] = prop.default
|
||||||
|
}
|
||||||
|
}
|
||||||
return(defaults)
|
return(defaults)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,31 @@ class KeyframesModel extends WindozModel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#normalizeAxisVector(vec) {
|
||||||
|
if(!vec || typeof(vec) !== 'object') return(null)
|
||||||
|
const axes = ['x', 'y', 'z']
|
||||||
|
const out = {}
|
||||||
|
for(const axis of axes) {
|
||||||
|
const n = Number(vec[axis])
|
||||||
|
if(!Number.isFinite(n)) return(null)
|
||||||
|
out[axis] = n
|
||||||
|
}
|
||||||
|
return(out)
|
||||||
|
}
|
||||||
|
|
||||||
async save(kfId, data) {
|
async save(kfId, data) {
|
||||||
const kfData = Object.keys(data).map(aid => {
|
const kfData = Object.keys(data).map(aid => {
|
||||||
const { position, speed, ...storeValues} = data[aid].values
|
const { position, speed, ...storeValues} = data[aid].values
|
||||||
|
const gpsPosition = this.#normalizeAxisVector(position)
|
||||||
|
const gpsSpeed = this.#normalizeAxisVector(speed)
|
||||||
|
if(!gpsPosition || !gpsSpeed) {
|
||||||
|
throw(new Error(`Agent ${aid}: position and speed must be numeric vectors`))
|
||||||
|
}
|
||||||
return({
|
return({
|
||||||
aid: aid,
|
aid: aid,
|
||||||
type: data[aid].type,
|
type: data[aid].type,
|
||||||
storeValues: storeValues,
|
storeValues: storeValues,
|
||||||
gpsValues: { position: data[aid].values.position, speed: data[aid].values.speed }
|
gpsValues: { position: gpsPosition, speed: gpsSpeed },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -207,14 +207,14 @@ class KeyframeView extends WindozDomContent {
|
|||||||
if(this.currentlySelectedAid && this.kfArena.agents[this.currentlySelectedAid]){
|
if(this.currentlySelectedAid && this.kfArena.agents[this.currentlySelectedAid]){
|
||||||
const AgentValues = this.getFieldsValues('div[data-output="agentProperties"]')
|
const AgentValues = this.getFieldsValues('div[data-output="agentProperties"]')
|
||||||
this.kfArena.agents[this.currentlySelectedAid].values = AgentValues
|
this.kfArena.agents[this.currentlySelectedAid].values = AgentValues
|
||||||
const val = Number.parseInt(comp.value, 10)
|
const val = Number(comp.value)
|
||||||
if((comp.name.startsWith('position.')) && (!Number.isNaN(val))){
|
if((comp.name.startsWith('position.')) && (Number.isFinite(val))){
|
||||||
this.kfArena.moveAgent(this.currentlySelectedAid, {
|
this.kfArena.moveAgent(this.currentlySelectedAid, {
|
||||||
x: this.getFieldValue('div[data-output="agentProperties"]', 'position.x'),
|
x: this.getFieldValue('div[data-output="agentProperties"]', 'position.x'),
|
||||||
y: this.getFieldValue('div[data-output="agentProperties"]', 'position.y'),
|
y: this.getFieldValue('div[data-output="agentProperties"]', 'position.y'),
|
||||||
z: this.getFieldValue('div[data-output="agentProperties"]', 'position.z'),
|
z: this.getFieldValue('div[data-output="agentProperties"]', 'position.z'),
|
||||||
})
|
})
|
||||||
} else if((comp.name.startsWith('speed.')) && (!Number.isNaN(val))){
|
} else if((comp.name.startsWith('speed.')) && (Number.isFinite(val))){
|
||||||
this.kfArena.changeAgentSpeed(this.currentlySelectedAid, {
|
this.kfArena.changeAgentSpeed(this.currentlySelectedAid, {
|
||||||
x: this.getFieldValue('div[data-output="agentProperties"]', 'speed.x'),
|
x: this.getFieldValue('div[data-output="agentProperties"]', 'speed.x'),
|
||||||
y: this.getFieldValue('div[data-output="agentProperties"]', 'speed.y'),
|
y: this.getFieldValue('div[data-output="agentProperties"]', 'speed.y'),
|
||||||
|
|||||||
+59
-1
@@ -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" }
|
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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user