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.setData('dim', 30, 'current')
|
||||
Selected.Nodes.push(node)
|
||||
Engine.setNodeSleeping(node.getData('body_id'), true)
|
||||
},
|
||||
deselectAllNodes: function() {
|
||||
var l = Selected.Nodes.length
|
||||
|
@ -40,7 +39,6 @@ const Control = {
|
|||
// remove the node
|
||||
Selected.Nodes.splice(
|
||||
Selected.Nodes.indexOf(node), 1)
|
||||
Engine.setNodeSleeping(node.getData('body_id'), false)
|
||||
},
|
||||
deleteSelected: function() {
|
||||
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 $jit from '../patched/JIT'
|
||||
import { getLayoutForData } from '../ConvoAlgo'
|
||||
|
||||
import Active from './Active'
|
||||
import Create from './Create'
|
||||
|
@ -11,58 +12,46 @@ import JIT from './JIT'
|
|||
import Visualize from './Visualize'
|
||||
|
||||
const Engine = {
|
||||
focusBody: null,
|
||||
newNodeConstraint: null,
|
||||
newNodeBody: Bodies.circle(Mouse.newNodeCoords.x, Mouse.newNodeCoords.y, 1),
|
||||
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 => {
|
||||
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) {
|
||||
Engine.setFocusNode(Engine.findFocusNode(Visualize.mGraph.graph.nodes))
|
||||
Engine.runLayout(true)
|
||||
}
|
||||
}
|
||||
Engine.runner = Matter.Runner.run(Engine.engine)
|
||||
},
|
||||
endActiveMap: () => {
|
||||
Engine.runner && Runner.stop(Engine.runner)
|
||||
Matter.Engine.clear(Engine.engine)
|
||||
|
||||
},
|
||||
setNodePos: (id, x, y) => {
|
||||
const body = Composite.get(Engine.engine.world, id, 'body')
|
||||
Body.setPosition(body, { x, y })
|
||||
Body.setVelocity(body, Vector.create(0, 0))
|
||||
Body.setAngularVelocity(body, 0)
|
||||
Body.setAngle(body, 0)
|
||||
},
|
||||
setNodeSleeping: (id, isSleeping) => {
|
||||
const body = Composite.get(Engine.engine.world, id, 'body')
|
||||
Sleeping.set(body, isSleeping)
|
||||
if (!isSleeping) {
|
||||
Body.setVelocity(body, Vector.create(0, 0))
|
||||
Body.setAngularVelocity(body, 0)
|
||||
Body.setAngle(body, 0)
|
||||
}
|
||||
runLayout: init => {
|
||||
const synapses = DataModel.Synapses.map(s => s.attributes)
|
||||
const topics = DataModel.Topics.map(t => t.attributes)
|
||||
const focalNodeId = Create.newSynapse.focusNode.getData('topic').id
|
||||
const focalCoords = init ? { x: 0, y: 0 } : Create.newSynapse.focusNode.pos
|
||||
const layout = getLayoutForData(topics, synapses, focalNodeId, focalCoords)
|
||||
Visualize.mGraph.graph.eachNode(n => {
|
||||
let calculatedCoords = layout[n.id]
|
||||
if (!calculatedCoords) {
|
||||
calculatedCoords = {x: 0, y: 0}
|
||||
}
|
||||
const endPos = new $jit.Complex(calculatedCoords.x, calculatedCoords.y)
|
||||
n.setPos(endPos, 'end')
|
||||
})
|
||||
Visualize.mGraph.animate({
|
||||
modes: ['linear'],
|
||||
transition: $jit.Trans.Elastic.easeOut,
|
||||
duration: 200,
|
||||
onComplete: () => {}
|
||||
})
|
||||
},
|
||||
addNode: node => {
|
||||
let body = Bodies.circle(node.pos.x, node.pos.y, 100)
|
||||
body.node_id = node.id
|
||||
node.setData('body_id', body.id)
|
||||
World.addBody(Engine.engine.world, body)
|
||||
//Engine.runLayout()
|
||||
},
|
||||
removeNode: node => {
|
||||
|
||||
//Engine.runLayout()
|
||||
},
|
||||
findFocusNode: nodes => {
|
||||
return last(sortBy(values(nodes), n => new Date(n.getData('topic').get('created_at'))))
|
||||
|
@ -70,50 +59,19 @@ const Engine = {
|
|||
setFocusNode: node => {
|
||||
if (!Active.Mapper) return
|
||||
Create.newSynapse.focusNode = node
|
||||
const body = Composite.get(Engine.engine.world, node.getData('body_id'), 'body')
|
||||
Engine.focusBody = body
|
||||
let constraint
|
||||
if (Engine.newNodeConstraint) {
|
||||
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
|
||||
Mouse.focusNodeCoords = node.pos
|
||||
Mouse.newNodeCoords = {
|
||||
x: node.x + 200,
|
||||
y: node.y
|
||||
}
|
||||
Create.newSynapse.updateForm()
|
||||
Create.newTopic.position()
|
||||
},
|
||||
addEdge: edge => {
|
||||
const bodyA = Composite.get(Engine.engine.world, edge.nodeFrom.getData('body_id'), 'body')
|
||||
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)
|
||||
Engine.runLayout()
|
||||
},
|
||||
removeEdge: synapse => {
|
||||
|
||||
},
|
||||
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()
|
||||
removeEdge: edge => {
|
||||
//Engine.runLayout()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1024,7 +1024,6 @@ const JIT = {
|
|||
n.pos.setp(theta, rho)
|
||||
} else {
|
||||
n.pos.setc(x, y)
|
||||
Engine.setNodePos(n.getData('body_id'), x, y)
|
||||
}
|
||||
|
||||
if (Active.Map) {
|
||||
|
|
|
@ -97,7 +97,7 @@ const Visualize = {
|
|||
})
|
||||
|
||||
//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(endPos, 'end')
|
||||
n.setPos(endPos, 'current')
|
||||
|
|
Loading…
Reference in a new issue