running algo each time an edge is added
This commit is contained in:
parent
fc2604376a
commit
08d2cbb00d
5 changed files with 238 additions and 82 deletions
201
frontend/src/ConvoAlgo/index.js
Normal file
201
frontend/src/ConvoAlgo/index.js
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
// an array of synapses
|
||||||
|
// an array of topics
|
||||||
|
|
||||||
|
// a focal node
|
||||||
|
|
||||||
|
/*
|
||||||
|
step 1
|
||||||
|
generate an object/array that represents the intended layout
|
||||||
|
|
||||||
|
|
||||||
|
step 2
|
||||||
|
generate x,y coordinates for every topic in the layout object
|
||||||
|
|
||||||
|
step 3
|
||||||
|
set end states for every topic
|
||||||
|
|
||||||
|
Step 4
|
||||||
|
animate
|
||||||
|
*/
|
||||||
|
|
||||||
|
// synapses = [{ topic1_id: 4, topic2_id: 5, direction: 'from-to' }]
|
||||||
|
|
||||||
|
export const generateLayoutObject = (topics, synapses, focalTopicId) => {
|
||||||
|
let layout = [] // will be the final output
|
||||||
|
const usedTopics = {} // will store the topics that have been placed into islands
|
||||||
|
let newRoot
|
||||||
|
let currentTopic
|
||||||
|
|
||||||
|
const addParentsAndChildren = (topic) => {
|
||||||
|
if (!topic.id) return topic
|
||||||
|
|
||||||
|
usedTopics[topic.id] = true
|
||||||
|
|
||||||
|
topic.parents = []
|
||||||
|
topic.children = []
|
||||||
|
|
||||||
|
let filteredParentIds = synapses.filter(synapse => {
|
||||||
|
return synapse.topic2_id === topic.id
|
||||||
|
&& !usedTopics[synapse.topic1_id]
|
||||||
|
&& synapse.category === 'from-to'
|
||||||
|
}).map(synapse => synapse.topic1_id)
|
||||||
|
|
||||||
|
let filteredChildrenIds = synapses.filter(synapse => {
|
||||||
|
return synapse.topic1_id === topic.id
|
||||||
|
&& !usedTopics[synapse.topic2_id]
|
||||||
|
&& synapse.category === 'from-to'
|
||||||
|
|
||||||
|
}).map(synapse => synapse.topic2_id)
|
||||||
|
|
||||||
|
filteredParentIds.forEach(parentId => {
|
||||||
|
let parent = {
|
||||||
|
id: parentId
|
||||||
|
}
|
||||||
|
topic.parents.push(addParentsAndChildren(parent))
|
||||||
|
})
|
||||||
|
|
||||||
|
filteredChildrenIds.forEach(childId => {
|
||||||
|
let child = {
|
||||||
|
id: childId
|
||||||
|
}
|
||||||
|
topic.children.push(addParentsAndChildren(child))
|
||||||
|
})
|
||||||
|
|
||||||
|
return topic
|
||||||
|
}
|
||||||
|
|
||||||
|
// start with the focal node, and build its island
|
||||||
|
currentTopic = topics.find(t => t.id === focalTopicId)
|
||||||
|
if (!currentTopic) {
|
||||||
|
console.log('you didnt pass a valid focalTopicId')
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
newRoot = {
|
||||||
|
id: currentTopic.id
|
||||||
|
}
|
||||||
|
layout.push(addParentsAndChildren(newRoot))
|
||||||
|
|
||||||
|
//
|
||||||
|
// go through the the topics again, and build the island for the first topic that isn't
|
||||||
|
// yet in the usedTopics object (in any island). recurse
|
||||||
|
topics.forEach(topic => {
|
||||||
|
if (topic && topic.id && !usedTopics[topic.id]) {
|
||||||
|
newRoot = {
|
||||||
|
id: topic.id
|
||||||
|
}
|
||||||
|
layout.push(addParentsAndChildren(newRoot))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const generateObjectCoordinates = (layoutObject, focalTopicId, focalCoords) => {
|
||||||
|
const coords = {}
|
||||||
|
|
||||||
|
const traverseIsland = (island, func, parent, child) => {
|
||||||
|
func(island, parent, child)
|
||||||
|
if (island.parents) {
|
||||||
|
island.parents.forEach(p => traverseIsland(p, func, null, island))
|
||||||
|
}
|
||||||
|
if (island.children) {
|
||||||
|
island.children.forEach(c => traverseIsland(c, func, island, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionTopic = (topic, parent, child) => {
|
||||||
|
if (topic.id === focalTopicId) {
|
||||||
|
// set the focalCoord to be what it already was
|
||||||
|
coords[topic.id] = focalCoords
|
||||||
|
} else if (!parent && !child) {
|
||||||
|
coords[topic.id] = {x: 0, y: 150}
|
||||||
|
} else if (parent) {
|
||||||
|
coords[topic.id] = {
|
||||||
|
x: coords[parent.id].x + 250,
|
||||||
|
y: coords[parent.id].y
|
||||||
|
}
|
||||||
|
} else if (child) {
|
||||||
|
coords[topic.id] = {
|
||||||
|
x: coords[child.id].x - 250,
|
||||||
|
y: coords[child.id].y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lay all of them out as if there were no other ones
|
||||||
|
layoutObject.forEach(island => {
|
||||||
|
traverseIsland(island, positionTopic)
|
||||||
|
})
|
||||||
|
|
||||||
|
// calculate the bounds of each island
|
||||||
|
|
||||||
|
// reposition the islands according to the bounds
|
||||||
|
return coords
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLayoutForData = (topics, synapses, focalTopicId, focalCoords) => {
|
||||||
|
return generateObjectCoordinates(generateLayoutObject(topics, synapses, focalTopicId), focalTopicId, focalCoords)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// if we've placed a node into an island, we need to NOT place it in any other islands
|
||||||
|
// Every node should only appear in one island
|
||||||
|
|
||||||
|
// the pseudo-focal node
|
||||||
|
|
||||||
|
|
||||||
|
// the top level array represents islands
|
||||||
|
// every island has some sort of 'focal' node
|
||||||
|
/*
|
||||||
|
var example = [
|
||||||
|
// the island that contains the focal node
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
parents: [
|
||||||
|
{
|
||||||
|
id: 25,
|
||||||
|
parents: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 25,
|
||||||
|
parents: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
children: [{
|
||||||
|
id: 26,
|
||||||
|
children: []
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
// all other islands should not contain children on the top level node
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
// parents may contain children
|
||||||
|
parents: [
|
||||||
|
{
|
||||||
|
id: 100,
|
||||||
|
parents: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
parents: [],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 103,
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
parents: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
parents: []
|
||||||
|
},
|
||||||
|
]
|
||||||
|
*/
|
|
@ -23,7 +23,6 @@ const Control = {
|
||||||
node.selected = true
|
node.selected = true
|
||||||
node.setData('dim', 30, 'current')
|
node.setData('dim', 30, 'current')
|
||||||
Selected.Nodes.push(node)
|
Selected.Nodes.push(node)
|
||||||
Engine.setNodeSleeping(node.getData('body_id'), true)
|
|
||||||
},
|
},
|
||||||
deselectAllNodes: function() {
|
deselectAllNodes: function() {
|
||||||
var l = Selected.Nodes.length
|
var l = Selected.Nodes.length
|
||||||
|
@ -40,7 +39,6 @@ const Control = {
|
||||||
// remove the node
|
// remove the node
|
||||||
Selected.Nodes.splice(
|
Selected.Nodes.splice(
|
||||||
Selected.Nodes.indexOf(node), 1)
|
Selected.Nodes.indexOf(node), 1)
|
||||||
Engine.setNodeSleeping(node.getData('body_id'), false)
|
|
||||||
},
|
},
|
||||||
deleteSelected: function() {
|
deleteSelected: function() {
|
||||||
if (!Active.Map) return
|
if (!Active.Map) return
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import Matter, { Vector, Sleeping, World, Constraint, Composite, Runner, Common, Body, Bodies, Events } from 'matter-js'
|
//import Matter, { Vector, Sleeping, World, Constraint, Composite, Runner, Common, Body, Bodies, Events } from 'matter-js'
|
||||||
import { last, sortBy, values } from 'lodash'
|
import { last, sortBy, values } from 'lodash'
|
||||||
|
|
||||||
import $jit from '../patched/JIT'
|
import $jit from '../patched/JIT'
|
||||||
|
import { getLayoutForData } from '../ConvoAlgo'
|
||||||
|
|
||||||
import Active from './Active'
|
import Active from './Active'
|
||||||
import Create from './Create'
|
import Create from './Create'
|
||||||
|
@ -11,58 +12,46 @@ import JIT from './JIT'
|
||||||
import Visualize from './Visualize'
|
import Visualize from './Visualize'
|
||||||
|
|
||||||
const Engine = {
|
const Engine = {
|
||||||
focusBody: null,
|
|
||||||
newNodeConstraint: null,
|
|
||||||
newNodeBody: Bodies.circle(Mouse.newNodeCoords.x, Mouse.newNodeCoords.y, 1),
|
|
||||||
init: (serverData) => {
|
init: (serverData) => {
|
||||||
Engine.engine = Matter.Engine.create()
|
|
||||||
Events.on(Engine.engine, 'afterUpdate', Engine.callUpdate)
|
|
||||||
if (!serverData.ActiveMapper) Engine.engine.world.gravity.scale = 0
|
|
||||||
else {
|
|
||||||
Engine.engine.world.gravity.y = 0
|
|
||||||
Engine.engine.world.gravity.x = -1
|
|
||||||
Body.setStatic(Engine.newNodeBody, true)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
run: init => {
|
run: init => {
|
||||||
if (init) {
|
if (init) {
|
||||||
if (Active.Mapper) World.addBody(Engine.engine.world, Engine.newNodeBody)
|
|
||||||
Visualize.mGraph.graph.eachNode(Engine.addNode)
|
|
||||||
DataModel.Synapses.each(s => Engine.addEdge(s.get('edge')))
|
|
||||||
if (Active.Mapper && Object.keys(Visualize.mGraph.graph.nodes).length) {
|
if (Active.Mapper && Object.keys(Visualize.mGraph.graph.nodes).length) {
|
||||||
Engine.setFocusNode(Engine.findFocusNode(Visualize.mGraph.graph.nodes))
|
Engine.setFocusNode(Engine.findFocusNode(Visualize.mGraph.graph.nodes))
|
||||||
|
Engine.runLayout(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Engine.runner = Matter.Runner.run(Engine.engine)
|
|
||||||
},
|
},
|
||||||
endActiveMap: () => {
|
endActiveMap: () => {
|
||||||
Engine.runner && Runner.stop(Engine.runner)
|
|
||||||
Matter.Engine.clear(Engine.engine)
|
|
||||||
},
|
},
|
||||||
setNodePos: (id, x, y) => {
|
runLayout: init => {
|
||||||
const body = Composite.get(Engine.engine.world, id, 'body')
|
const synapses = DataModel.Synapses.map(s => s.attributes)
|
||||||
Body.setPosition(body, { x, y })
|
const topics = DataModel.Topics.map(t => t.attributes)
|
||||||
Body.setVelocity(body, Vector.create(0, 0))
|
const focalNodeId = Create.newSynapse.focusNode.getData('topic').id
|
||||||
Body.setAngularVelocity(body, 0)
|
const focalCoords = init ? { x: 0, y: 0 } : Create.newSynapse.focusNode.pos
|
||||||
Body.setAngle(body, 0)
|
const layout = getLayoutForData(topics, synapses, focalNodeId, focalCoords)
|
||||||
},
|
Visualize.mGraph.graph.eachNode(n => {
|
||||||
setNodeSleeping: (id, isSleeping) => {
|
let calculatedCoords = layout[n.id]
|
||||||
const body = Composite.get(Engine.engine.world, id, 'body')
|
if (!calculatedCoords) {
|
||||||
Sleeping.set(body, isSleeping)
|
calculatedCoords = {x: 0, y: 0}
|
||||||
if (!isSleeping) {
|
}
|
||||||
Body.setVelocity(body, Vector.create(0, 0))
|
const endPos = new $jit.Complex(calculatedCoords.x, calculatedCoords.y)
|
||||||
Body.setAngularVelocity(body, 0)
|
n.setPos(endPos, 'end')
|
||||||
Body.setAngle(body, 0)
|
})
|
||||||
}
|
Visualize.mGraph.animate({
|
||||||
|
modes: ['linear'],
|
||||||
|
transition: $jit.Trans.Elastic.easeOut,
|
||||||
|
duration: 200,
|
||||||
|
onComplete: () => {}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
addNode: node => {
|
addNode: node => {
|
||||||
let body = Bodies.circle(node.pos.x, node.pos.y, 100)
|
//Engine.runLayout()
|
||||||
body.node_id = node.id
|
|
||||||
node.setData('body_id', body.id)
|
|
||||||
World.addBody(Engine.engine.world, body)
|
|
||||||
},
|
},
|
||||||
removeNode: node => {
|
removeNode: node => {
|
||||||
|
//Engine.runLayout()
|
||||||
},
|
},
|
||||||
findFocusNode: nodes => {
|
findFocusNode: nodes => {
|
||||||
return last(sortBy(values(nodes), n => new Date(n.getData('topic').get('created_at'))))
|
return last(sortBy(values(nodes), n => new Date(n.getData('topic').get('created_at'))))
|
||||||
|
@ -70,50 +59,19 @@ const Engine = {
|
||||||
setFocusNode: node => {
|
setFocusNode: node => {
|
||||||
if (!Active.Mapper) return
|
if (!Active.Mapper) return
|
||||||
Create.newSynapse.focusNode = node
|
Create.newSynapse.focusNode = node
|
||||||
const body = Composite.get(Engine.engine.world, node.getData('body_id'), 'body')
|
Mouse.focusNodeCoords = node.pos
|
||||||
Engine.focusBody = body
|
Mouse.newNodeCoords = {
|
||||||
let constraint
|
x: node.x + 200,
|
||||||
if (Engine.newNodeConstraint) {
|
y: node.y
|
||||||
Engine.newNodeConstraint.bodyA = body
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
constraint = Constraint.create({
|
|
||||||
bodyA: body,
|
|
||||||
bodyB: Engine.newNodeBody,
|
|
||||||
length: JIT.ForceDirected.graphSettings.levelDistance,
|
|
||||||
stiffness: 0.2
|
|
||||||
})
|
|
||||||
World.addConstraint(Engine.engine.world, constraint)
|
|
||||||
Engine.newNodeConstraint = constraint
|
|
||||||
}
|
}
|
||||||
|
Create.newSynapse.updateForm()
|
||||||
|
Create.newTopic.position()
|
||||||
},
|
},
|
||||||
addEdge: edge => {
|
addEdge: edge => {
|
||||||
const bodyA = Composite.get(Engine.engine.world, edge.nodeFrom.getData('body_id'), 'body')
|
Engine.runLayout()
|
||||||
const bodyB = Composite.get(Engine.engine.world, edge.nodeTo.getData('body_id'), 'body')
|
|
||||||
let constraint = Constraint.create({
|
|
||||||
bodyA,
|
|
||||||
bodyB,
|
|
||||||
length: JIT.ForceDirected.graphSettings.levelDistance,
|
|
||||||
stiffness: 0.2
|
|
||||||
})
|
|
||||||
edge.setData('constraint_id', constraint.id)
|
|
||||||
World.addConstraint(Engine.engine.world, constraint)
|
|
||||||
},
|
},
|
||||||
removeEdge: synapse => {
|
removeEdge: edge => {
|
||||||
|
//Engine.runLayout()
|
||||||
},
|
|
||||||
callUpdate: () => {
|
|
||||||
Engine.engine.world.bodies.forEach(b => {
|
|
||||||
const node = Visualize.mGraph.graph.getNode(b.node_id)
|
|
||||||
const newPos = new $jit.Complex(b.position.x, b.position.y)
|
|
||||||
node && node.setPos(newPos, 'current')
|
|
||||||
})
|
|
||||||
if (Active.Mapper) {
|
|
||||||
if (Engine.focusBody) Mouse.focusNodeCoords = Engine.focusBody.position
|
|
||||||
Create.newSynapse.updateForm()
|
|
||||||
Create.newTopic.position()
|
|
||||||
}
|
|
||||||
Visualize.mGraph.plot()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1024,7 +1024,6 @@ const JIT = {
|
||||||
n.pos.setp(theta, rho)
|
n.pos.setp(theta, rho)
|
||||||
} else {
|
} else {
|
||||||
n.pos.setc(x, y)
|
n.pos.setc(x, y)
|
||||||
Engine.setNodePos(n.getData('body_id'), x, y)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Active.Map) {
|
if (Active.Map) {
|
||||||
|
|
|
@ -97,7 +97,7 @@ const Visualize = {
|
||||||
})
|
})
|
||||||
|
|
||||||
//const startPos = new $jit.Complex(0, 0)
|
//const startPos = new $jit.Complex(0, 0)
|
||||||
const endPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc'))
|
const endPos = new $jit.Complex(0, 0)
|
||||||
//n.setPos(startPos, 'start')
|
//n.setPos(startPos, 'start')
|
||||||
//n.setPos(endPos, 'end')
|
//n.setPos(endPos, 'end')
|
||||||
n.setPos(endPos, 'current')
|
n.setPos(endPos, 'current')
|
||||||
|
|
Loading…
Add table
Reference in a new issue