From 3e25fed2d56c9b4aaf2f886aa7a1cfb5e1b16d6a Mon Sep 17 00:00:00 2001
From: Jake <jake.read@cba.mit.edu>
Date: Sat, 20 Apr 2019 21:52:20 -0400
Subject: [PATCH] donkey slowly up the hill, state update on hunk load now
---
README.md | 22 +-
gogetter.js | 26 +-
hunks/hunks.js | 1 +
hunks/link.js | 15 +-
hunks/manager.js | 1162 +++++++++++++++++++++++---------------------
hunks/view.js | 45 +-
programs/mvnv.json | 11 +-
style.css | 17 +
8 files changed, 710 insertions(+), 589 deletions(-)
diff --git a/README.md b/README.md
index 8f5252e..857af22 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,16 @@ To wake up, I've a small list:
- same for comm/
- rebuild / use scrape.js (with //HEADER and //ENDHEADER flags)
+There's some UI leftovers in the view.
+ - want to kick around on spreading things out properly using force layout
+ - can't add links, did this actually work before ? drag correct zoom factor, and cancel other moves
+
+- towards using state-load-at-program-load to set retrycount to 0
+
+- loading is messy,
+ - load in state objects before init (rolling through much old code)
+ - view should have display location for messages you anticipate users needing to see, primarily errors that are responses to view requests: like addlink (type errors) or addhunk (resource not available from cpp) or errors from hunk v8 loading, json program loading, etc.
+
Once I'm through that, I should be back at happily opening up nautilus-via-cuttlefish. I'll polish my UI, particularly wire views.
I'll explicate the list of messages that I need to send to managers, and to links.
@@ -140,7 +150,17 @@ Messages to 254 port are debug, should just be printed. Messages to 253 are to t
What's stopping me from implementing this? Am I burnt out?
-I think this also asks me to reconsider how programs are loaded. Does it? No. It's actually a good test of the existing system.
+I think this also asks me to reconsider how programs are loaded. Does it? No. It's actually a good test of the existing system.
+
+So, there's a lot to do. It's a good thing I love JavaScript: every language's bastard son, in a King of the North kind of way.
+
+ - state objects are typed,
+ - state objects are written at program load, if they're there.
+ - does this mean that they go into 'add hunk' message ?
+ - reload link verbatim but via state-load
+ - then scrape to nautilus ... and load via ws
+ - maybe then is view cleanup ?
+
## A List of Types
diff --git a/gogetter.js b/gogetter.js
index ed0dae9..6c2dadf 100644
--- a/gogetter.js
+++ b/gogetter.js
@@ -1,12 +1,12 @@
-// fs abstraction for sys
+// fs abstraction for sys
function GoGetter() {
this.findHunks = (debug) => {
return new Promise((resolve, reject) => {
let htmlTreeDiver = (response) => {
- // do we try to pick links out of this plaintext ? like a monkey ?
- // I suppose we do
- // header, for names
+ // do we try to pick links out of this plaintext ? like a monkey ?
+ // I suppose we do
+ // header, for names
let hfront = response.indexOf('<h1>Index of /hunks') + 14
let hback = response.indexOf('</h1>')
let header = response.slice(hfront, hback)
@@ -20,7 +20,7 @@ function GoGetter() {
// git those paths (these aren't names, are they?)
let paths = response.split('<a href="/hunks/')
// rm first two, and last ()
- paths = paths.slice(2) //
+ paths = paths.slice(2) //
paths.forEach((path, index) => {
let end = path.indexOf('"')
@@ -37,10 +37,10 @@ function GoGetter() {
jQuery.get(recurse, htmlTreeDiver)
} else if (path.includes('.js')) {
//console.log('likely hunk', path)
- // secrets are rming .js
+ // secrets are rming .js
path = path.slice(0, path.indexOf('.'))
if (path === 'hunks' || path === 'manager' || path === 'template' || path.includes('hidden/')) {
- // don't add these special hunks
+ // don't add these special hunks
} else {
returned.push(path)
}
@@ -48,9 +48,9 @@ function GoGetter() {
})
if (unreturned.length === 0) {
- // TODO cull hunks and hidden paths ...
- // now, writing menu options for each path
- // sort alphabetically
+ // TODO cull hunks and hidden paths ...
+ // now, writing menu options for each path
+ // sort alphabetically
returned.sort()
//console.log('GG returns this', JSON.parse(JSON.stringify(returned)))
resolve(returned)
@@ -81,7 +81,7 @@ function GoGetter() {
window[tempGlobal] = function(module) {
cleanup()
- console.log('ADDHUNK (2) import resolves', url)
+ //console.log('ADDHUNK (2) import resolves', url)
resolve(module[Object.keys(module)[0]])
}
@@ -101,7 +101,7 @@ function GoGetter() {
return new Promise((resolve, reject) => {
$.ajax({
url: path,
- type: 'GET',
+ type: 'GET',
success: (data) => {
resolve(data)
},
@@ -113,4 +113,4 @@ function GoGetter() {
}
}
-export default GoGetter
\ No newline at end of file
+export default GoGetter
diff --git a/hunks/hunks.js b/hunks/hunks.js
index 9134a88..ad8053e 100644
--- a/hunks/hunks.js
+++ b/hunks/hunks.js
@@ -123,6 +123,7 @@ function Output(type, name) {
function State(type, name, startup) {
this.name = name
this.type = type
+ // TODO pls add check for missing startup value ?
this.value = startup
// this is still something of a hack
// during load, the manager gets in here and dishes a function 2 us
diff --git a/hunks/link.js b/hunks/link.js
index c17f88e..79994e5 100644
--- a/hunks/link.js
+++ b/hunks/link.js
@@ -22,6 +22,14 @@ function Link() {
this.inputs.data = new Input('byteArray', 'data')
this.outputs.data = new Output('byteArray', 'data')
+ this.state.inputList = new State('string', 'inputList', "msgs (byteArray)")
+ this.state.outputList = new State('string', 'outputList', "msgs (byteArray)")
+
+ let inports = [this.inputs.zero, this.inputs.zero, this.inputs.one]
+ let outports = [this.outputs.zero, this.outputs.zero]
+
+ /*
+
// these are *special link inputs* keeping track of downstream status
this.inputs.zero = new Input('any', 'zero')
this.inputs.zero.dss = 'open'
@@ -44,10 +52,13 @@ function Link() {
this.outputs.one.hold.status = 'open'
this.outputs.one.hold.msg = {}
+ */
+
this.init = () => {
// manager calls this once
// it is loaded and state is updated (from program)
this.log('hello Link')
+ // HERE write those inputs via that list
}
// so far we won't ack at the link layer,
@@ -56,8 +67,6 @@ function Link() {
// but we will need to do this for the dmarippers
// perhaps that should live in the layer that 'websocket' is at now
- let inports = [this.inputs.zero, this.inputs.zero, this.inputs.one]
- let outports = [this.outputs.zero, this.outputs.zero]
let outbuffer = new Array()
@@ -82,6 +91,7 @@ function Link() {
this.loop = () => {
+ /*
// for everything we're holding, check our outputs
for (let i in outports) {
if (outports[i].hold.status === 'occupied' && outports[i].ie) {
@@ -168,6 +178,7 @@ function Link() {
if (this.outputs.data.ie && outbuffer.length > 0) {
this.outputs.data.put(outbuffer.shift())
}
+ */
} // end loop
}
diff --git a/hunks/manager.js b/hunks/manager.js
index 6ee48c1..3f0700e 100644
--- a/hunks/manager.js
+++ b/hunks/manager.js
@@ -1,617 +1,651 @@
-import { Hunkify, Input, Output, State } from './hunks.js'
+import {
+ Hunkify,
+ Input,
+ Output,
+ State
+} from './hunks.js'
import GoGetter from '../gogetter.js'
function Manager() {
- Hunkify(this, 'Manager')
- // need this tool
- let GG = new GoGetter
-
- this.inputs.msgs = new Input('message', 'msgs')
- this.outputs.msgs = new Output('message', 'msgs')
-
- let hunkCount = 0
- let hunks = new Array()
-
- /* --------------------------- ---------------------------- */
- /* -------------------- STD MGR INTERFACE -------------------- */
- /* --------------------------- ---------------------------- */
-
- this.getHelloResponses = () => {
- // writeMessage does buffering on our end
- return [{
- text: 'list available hunks',
- header: 'listhunks',
- content: ' '
- },
- {
- text: 'describe running program',
- header: 'listprogram',
- content: ' '
- },
- {
- text: 'save running program',
- header: 'saveprogram',
- content: ' '
- }
- ]
- }
-
- /* --------------------------- ---------------------------- */
- /* ---------------------- BUILDING LIST ---------------------- */
- /* --------------------------- ---------------------------- */
-
- // for now, managers take callback paths to pipe data back through ?
- this.getListOfAvailableHunks = () => {
- return new Promise((resolve, reject) => {
- GG.findHunks().then((list) => {
- resolve(list)
- }).catch((err) => {
- reject(err)
- })
- })
+ Hunkify(this, 'Manager')
+ // need this tool
+ let GG = new GoGetter
+
+ this.inputs.msgs = new Input('message', 'msgs')
+ this.outputs.msgs = new Output('message', 'msgs')
+
+ let hunkCount = 0
+ let hunks = new Array()
+
+ /* --------------------------- ---------------------------- */
+ /* -------------------- STD MGR INTERFACE -------------------- */
+ /* --------------------------- ---------------------------- */
+
+ this.getHelloResponses = () => {
+ // writeMessage does buffering on our end
+ return [{
+ text: 'list available hunks',
+ header: 'listhunks',
+ content: ' '
+ },
+ {
+ text: 'describe running program',
+ header: 'listprogram',
+ content: ' '
+ },
+ {
+ text: 'save running program',
+ header: 'saveprogram',
+ content: ' '
+ }
+ ]
+ }
+
+ let errmsg = (message) => {
+ this.writemessage('error', message)
+ }
+
+ /* --------------------------- ---------------------------- */
+ /* ---------------------- BUILDING LIST ---------------------- */
+ /* --------------------------- ---------------------------- */
+
+ // for now, managers take callback paths to pipe data back through ?
+ this.getListOfAvailableHunks = () => {
+ return new Promise((resolve, reject) => {
+ GG.findHunks().then((list) => {
+ resolve(list)
+ }).catch((err) => {
+ reject(err)
+ })
+ })
+ }
+
+ /* --------------------------- ---------------------------- */
+ /* ----------------- REPLY WITH DESCRIPTIONS ----------------- */
+ /* --------------------------- ---------------------------- */
+
+ this.describeRunningProgram = () => {
+ for (let hnk in hunks) {
+ this.sendHunkAsDef(hunks[hnk])
}
+ }
- /* --------------------------- ---------------------------- */
- /* ----------------- REPLY WITH DESCRIPTIONS ----------------- */
- /* --------------------------- ---------------------------- */
+ this.sendHunkAsDef = (hunk) => {
+ let def = writeDefinition(hunk)
+ this.writemessage('putdef', def)
+ }
- this.describeRunningProgram = () => {
- for (let hnk in hunks) {
- this.sendHunkAsDef(hunks[hnk])
- }
+ // bit confusing this / but is functional code node oo
+ let writeDefinition = (hunk) => {
+ if (hunk.id === null) {
+ throw new Error('hunk with no id in writeHunkDefinition')
}
-
- this.sendHunkAsDef = (hunk) => {
- let def = writeDefinition(hunk)
- this.writemessage('putdef', def)
+ // ok
+ // write tree
+ let def = {
+ name: hunk.name,
+ id: hunk.id,
+ inputs: {},
+ outputs: {},
+ state: {}
}
- // bit confusing this / but is functional code node oo
- let writeDefinition = (hunk) => {
- if (hunk.id === null) {
- throw new Error('hunk with no id in writeHunkDefinition')
- }
- // ok
- // write tree
- let def = {
- name: hunk.name,
- id: hunk.id,
- inputs: {},
- outputs: {},
- state: {}
- }
-
- // add inputs
- if (Object.keys(hunk.inputs).length > 0) {
- for (let key in hunk.inputs) {
- let input = hunk.inputs[key]
- def.inputs[input.name] = {
- name: input.name,
- type: input.type
- }
- }
+ // add inputs
+ if (Object.keys(hunk.inputs).length > 0) {
+ for (let key in hunk.inputs) {
+ let input = hunk.inputs[key]
+ def.inputs[input.name] = {
+ name: input.name,
+ type: input.type
}
+ }
+ }
- // add outputs
- if (Object.keys(hunk.outputs).length > 0) {
- for (let key in hunk.outputs) {
- let output = hunk.outputs[key]
- def.outputs[output.name] = {
- name: output.name,
- type: output.type,
- connections: new Array()
- }
- if (output.connections.length > 0) {
- for (let conn in output.connections) {
- let crep = {
- input: output.connections[conn].input.name,
- parentid: output.connections[conn].parentid
- }
- def.outputs[output.name].connections.push(crep)
- }
- }
- }
+ // add outputs
+ if (Object.keys(hunk.outputs).length > 0) {
+ for (let key in hunk.outputs) {
+ let output = hunk.outputs[key]
+ def.outputs[output.name] = {
+ name: output.name,
+ type: output.type,
+ connections: new Array()
}
-
- // add outputs
- if (Object.keys(hunk.state).length > 0) {
- for (let key in hunk.state) {
- let state = hunk.state[key]
- def.state[state.name] = {
- name: state.name,
- type: state.type,
- value: state.value
- }
+ if (output.connections.length > 0) {
+ for (let conn in output.connections) {
+ let crep = {
+ input: output.connections[conn].input.name,
+ parentid: output.connections[conn].parentid
}
+ def.outputs[output.name].connections.push(crep)
+ }
}
-
- if (hunk.dom !== null) {
- //console.log('WHD has dom', hunk.dom)
- def.dom = hunk.dom
- }
-
- return def
+ }
}
- this.findHunkById = (id) => {
- let theHunk = hunks.find((hunk) => {
- return hunk.id === id
- })
- if (theHunk !== undefined) {
- return theHunk
- } else {
- return undefined
+ // add outputs
+ if (Object.keys(hunk.state).length > 0) {
+ for (let key in hunk.state) {
+ let state = hunk.state[key]
+ def.state[state.name] = {
+ name: state.name,
+ type: state.type,
+ value: state.value
}
+ }
}
- /* --------------------------- ---------------------------- */
- /* ------------------- ADD / REMOVE A HUNK ------------------- */
- /* --------------------------- ---------------------------- */
-
- this.addHunk = (hunkName, id) => {
- // hunks are named by path in js
- // so that we can add
- // so that we can link
- // so that we can go to links, websockets, node
- let path = './hunks/' + hunkName + '.js'
-
- return new Promise((resolve, reject) => {
- GG.importSource(path).then((src) => {
- try {
- let hunk = sourceToHunk(src, hunkName, id)
- hunks.push(hunk)
- this.sendHunkAsDef(hunk)
- resolve(hunk)
- } catch (err) {
- reject(err)
- }
- }).catch((err) => {
- reject(err)
- })
- })
+ if (hunk.dom !== null) {
+ //console.log('WHD has dom', hunk.dom)
+ def.dom = hunk.dom
}
- let sourceToHunk = (src, name, id) => {
- // make the instance from the constructor
- let hunk = new src()
- // name it (this is just the path)
- hunk.name = name
-
- // id it, with a new ID if it doesn't have one, or
- // with the ID from its program (occasionally provided) s
- if (id !== undefined && id !== null) {
- // TODO: BIGBOI: also check for duplicate ids ?? totally possible, very likely
- hunk.id = id
- } else {
- // TODO: probably there is a better id, and what of program loads?
- hunk.id = 'hnk_' + hunkCount
- }
- hunkCount++
-
- // now we open our doors to state changes
- if (Object.keys(hunk.state).length > 0) {
- for (let st in hunk.state) {
- hunk.state[st].hookup = (value) => {
- console.log('MGR -> VIEW STATECHANGE')
- this.writemessage('putstate', {
- id: hunk.id,
- name: hunk.state[st].name,
- value: value
- })
- }
- }
- }
-
- // startup code if it exists
- if (hunk.init != null) {
- try {
- hunk.init()
- } catch (err) {
- throw err
- }
+ return def
+ }
+
+ this.findHunkById = (id) => {
+ let theHunk = hunks.find((hunk) => {
+ return hunk.id === id
+ })
+ if (theHunk !== undefined) {
+ return theHunk
+ } else {
+ return undefined
+ }
+ }
+
+ /* --------------------------- ---------------------------- */
+ /* ------------------- ADD / REMOVE A HUNK ------------------- */
+ /* --------------------------- ---------------------------- */
+
+ this.addHunk = (hunkName, id, state) => {
+ // hunks are named by path in js
+ // so that we can add
+ // so that we can link
+ // so that we can go to links, websockets, node
+ let path = './hunks/' + hunkName + '.js'
+
+ return new Promise((resolve, reject) => {
+ GG.importSource(path).then((src) => {
+ try {
+ let hunk = sourceToHunk(src, hunkName, id, state)
+ hunks.push(hunk)
+ this.sendHunkAsDef(hunk)
+ resolve(hunk)
+ } catch (err) {
+ reject(err)
}
-
- return hunk
+ }).catch((err) => {
+ reject(err)
+ })
+ })
+ }
+
+ let sourceToHunk = (src, name, id, state) => {
+ // make the instance from the constructor
+ let hunk = new src()
+ // name it (this is just the path)
+ hunk.name = name
+
+ // id it, with a new ID if it doesn't have one, or
+ // with the ID from its program (occasionally provided) s
+ if (id !== undefined && id !== null) {
+ // TODO: BIGBOI: also check for duplicate ids ?? totally possible, very likely
+ hunk.id = id
+ } else {
+ // TODO: probably there is a better id, and what of program loads?
+ hunk.id = 'hnk_' + hunkCount
}
-
- this.removeHunk = (req) => {
- let id = req.id
- let name = req.name
- if (name == 'manager' || name == 'hidden/view') {
- throw new Error("I can't let you do that, Dave")
- return false
- } else {
- let hnk = hunks.find((element) => {
- return id === element.id
- })
- if (hnk !== undefined) {
- // rm then
- hunks.splice(hunks.indexOf(hnk), 1)
- for (let hks of hunks) {
- for (let otp of Object.keys(hks.outputs)) {
- hks.outputs[otp].unhook(id)
- }
- }
- return true
- } else {
- return false
- }
+ hunkCount++
+
+ // now we open our doors to state changes
+ if (Object.keys(hunk.state).length > 0) {
+ for (let st in hunk.state) {
+ hunk.state[st].hookup = (value) => {
+ // console.log('MGR -> VIEW STATECHANGE')
+ this.writemessage('putstate', {
+ id: hunk.id,
+ name: hunk.state[st].name,
+ value: value
+ })
}
+ }
}
- /* --------------------------- ---------------------------- */
- /* ----------------- LINKS HELLO / GOODBYTE ------------------ */
- /* --------------------------- ---------------------------- */
-
- this.addLink = (outHunkId, outputName, inHunkId, inputName) => {
- // sync, doesn't need to be a promise
- let outHunk = hunks.find((hunk) => {
- return hunk.id == outHunkId
- })
- let inHunk = hunks.find((hunk) => {
- return hunk.id == inHunkId
- })
- // console.log('found hunks', outHunk, inHunk)
- if (outHunk === undefined) {
- console.log("MGR on ADDLINK: outHunk is undefined", outHunkId)
- return false
- }
- if (inHunk === undefined) {
- console.log("MGR on ADDLINK: inHunk is undefined", inHunkId)
- return false
- }
- if (outHunk.outputs[outputName] === null || outHunk.outputs[outputName] === undefined) {
- console.log('MGR on ADDLINK: output hunk is null or undefined:', outHunk.name, outputName)
- return false
- } else if (inHunk.inputs[inputName] === null || inHunk.inputs[inputName] === undefined) {
- console.log('MGR on ADDLINK: input hunk is null or undefined:', inHunk.name, inputName)
- return false
+ // now we update it's state if we have any of *that* to do
+ if(state !== undefined && state !== null){
+ for(let ro in state){
+ // ro is the key
+ if(hunk.state[ro] !== undefined){
+ console.log('types', typeof hunk.state[ro].value, typeof state[ro])
+ if(typeof hunk.state[ro].value == typeof state[ro]){
+ hunk.state[ro].value = state[ro]
+ } else {
+ this.writemessage('error', 'problematic state content in program, not same type')
+ }
} else {
- this.log(`hooking ${outHunk.id} to ${inHunk.id}`)
- outHunk.outputs[outputName].attach(inHunk.inputs[inputName], inHunk.id)
- this.writemessage('putlink', {
- outId: outHunkId,
- outName: outputName,
- inId: inHunkId,
- inName: inputName
- })
- return true
+ this.writemessage('error', 'problematic state content in program, object not found')
}
+ }
}
- this.addLinkByObject = (fromHunk, outputName, toHunk, inputName) => {
- // TODO: not throwing errors when outputname does not exist
- // I.E. addLink doesn't actually use 'name' property, it uses [key]
- this.addLink(fromHunk.id, outputName, toHunk.id, inputName)
+ // startup code if it exists
+ if (hunk.init != null) {
+ try {
+ hunk.init()
+ } catch (err) {
+ throw err
+ }
}
- this.removeLink = (outHunkId, outputName, inHunkId, inputName) => {
- // console.log('REMOVELINK looking for hunk, with name, and in, with name', outHunkId, outputName, inHunkId, inputName)
- let outHunk = hunks.find((hunk) => {
- return hunk.id == outHunkId
- })
- let inHunk = hunks.find((hunk) => {
- return hunk.id == inHunkId
- }) // console.log('found hunks', outHunk, inHunk)
- if (outHunk === undefined || inHunk === undefined) {
- console.log("MGR on REMOVELINK: output or input hunks are undefined")
- return false
- }
- if (outHunk.outputs[outputName] === null || outHunk.outputs[outputName] === undefined) {
- console.log('MGR on REMOVELINK: output hunk is null or undefined:', outHunk.name, outputName)
- return false
- } else if (inHunk.inputs[inputName] === null || inHunk.inputs[inputName] === undefined) {
- console.log('MGR on REMOVELINK: input hunk is null or undefined:', inHunk.name, inputName)
- return false
- } else {
- this.log(`unhooking ${outHunk.id} to ${inHunk.id}`)
- if (outHunk.outputs[outputName].remove(inHunk.inputs[inputName], inHunk.id)) {
- this.writemessage('removelink', {
- outId: outHunkId,
- outName: outputName,
- inId: inHunkId,
- inName: inputName
- })
- return true
- } else {
- return false
- }
+ return hunk
+ }
+
+ this.removeHunk = (req) => {
+ let id = req.id
+ let name = req.name
+ if (name == 'manager' || name == 'hidden/view') {
+ throw new Error("I can't let you do that, Dave")
+ return false
+ } else {
+ let hnk = hunks.find((element) => {
+ return id === element.id
+ })
+ if (hnk !== undefined) {
+ // rm then
+ hunks.splice(hunks.indexOf(hnk), 1)
+ for (let hks of hunks) {
+ for (let otp of Object.keys(hks.outputs)) {
+ hks.outputs[otp].unhook(id)
+ }
}
+ return true
+ } else {
+ return false
+ }
}
-
- /* --------------------------- ---------------------------- */
- /* ---------------------- STATE CHANGES ---------------------- */
- /* --------------------------- ---------------------------- */
-
- this.stateChange = (hunkId, stateName, newValue) => {
- return new Promise((resolve, reject) => {
- this.log(`to change ${stateName} to ${newValue} in ${hunkId}`)
- let theHunk = hunks.find((hunk) => {
- return hunk.id == hunkId
- })
- if (theHunk !== undefined) {
- let theState = theHunk.state[stateName]
- if (theState !== null && theState !== undefined) {
- theState.change(newValue)
- resolve()
- } else {
- reject(new Error('@ statechange theState is undefined'))
- }
- } else {
- reject(new Error('@ statechange theHunk is undefined'))
- }
- })
+ }
+
+ /* --------------------------- ---------------------------- */
+ /* ----------------- LINKS HELLO / GOODBYTE ------------------ */
+ /* --------------------------- ---------------------------- */
+
+ this.addLink = (outHunkId, outputName, inHunkId, inputName) => {
+ // sync, doesn't need to be a promise
+ let outHunk = hunks.find((hunk) => {
+ return hunk.id == outHunkId
+ })
+ let inHunk = hunks.find((hunk) => {
+ return hunk.id == inHunkId
+ })
+ // console.log('found hunks', outHunk, inHunk)
+ if (outHunk === undefined) {
+ errmsg("MGR on ADDLINK: outHunk is undefined: " + outHunkId)
+ return false
}
-
- /* --------------------------- ---------------------------- */
- /* --------------------- PROGRAM LOADING --------------------- */
- /* --------------------------- ---------------------------- */
-
- this.addProgram = (prgname) => {
- return new Promise((resolve, reject) => {
- let unloaded = new Array()
- let loaded = new Array()
- // first, we should get the program
- // HERE consider this,
- // probably re-write addlink to auto-complete as well
- // and then: the view init ... the stuffy message buffer ... the flush ?
- GG.getJson('programs/' + prgname + '.json').then((obj) => {
- if (obj.programname !== null && obj.programname !== undefined) {
- this.log(`opening program with name: ${obj.programname}`)
- addHunkList(obj.hunks).then((newHunks) => {
- this.log(`finished adding hunks from: ${obj.programname}, now links`)
- for (let lnk of obj.links) {
- if (this.addLink(lnk.outhunk, lnk.outname, lnk.inhunk, lnk.inname)) {
- // success! continue
- } else {
- reject('err in program load, at links', lnk)
- }
- }
- resolve(newHunks)
- }).catch((err) => {
- reject(err)
- })
- } else {
- reject(new Error('program loads, not named'))
- }
- }).catch((err) => {
- reject(err)
- })
+ if (inHunk === undefined) {
+ errmsg("MGR on ADDLINK: inHunk is undefined: " + inHunkId)
+ return false
+ }
+ if (outHunk.outputs[outputName] === null || outHunk.outputs[outputName] === undefined) {
+ errmsg('MGR on ADDLINK: output is null or undefined. for hunk: ' + outHunk.id + " and output: " + outputName)
+ return false
+ } else if (inHunk.inputs[inputName] === null || inHunk.inputs[inputName] === undefined) {
+ errmsg('MGR on ADDLINK: input is null or undefined. for hunk: ' + inHunk.id + " and output: " + inputName)
+ return false
+ } else {
+ this.log(`hooking ${outHunk.id} to ${inHunk.id}`)
+ outHunk.outputs[outputName].attach(inHunk.inputs[inputName], inHunk.id)
+ this.writemessage('putlink', {
+ outId: outHunkId,
+ outName: outputName,
+ inId: inHunkId,
+ inName: inputName
+ })
+ return true
+ }
+ }
+
+ this.addLinkByObject = (fromHunk, outputName, toHunk, inputName) => {
+ // TODO: not throwing errors when outputname does not exist
+ // I.E. addLink doesn't actually use 'name' property, it uses [key]
+ this.addLink(fromHunk.id, outputName, toHunk.id, inputName)
+ }
+
+ this.removeLink = (outHunkId, outputName, inHunkId, inputName) => {
+ // console.log('REMOVELINK looking for hunk, with name, and in, with name', outHunkId, outputName, inHunkId, inputName)
+ let outHunk = hunks.find((hunk) => {
+ return hunk.id == outHunkId
+ })
+ let inHunk = hunks.find((hunk) => {
+ return hunk.id == inHunkId
+ }) // console.log('found hunks', outHunk, inHunk)
+ if (outHunk === undefined || inHunk === undefined) {
+ errmsg("MGR on REMOVELINK: output or input hunks are undefined")
+ return false
+ }
+ if (outHunk.outputs[outputName] === null || outHunk.outputs[outputName] === undefined) {
+ errmsg('MGR on REMOVELINK: output is null or undefined: ' + outHunk.name + " output: " + outputName)
+ return false
+ } else if (inHunk.inputs[inputName] === null || inHunk.inputs[inputName] === undefined) {
+ errmsg('MGR on REMOVELINK: input is null or undefined: ' + inHunk.name + "input: " + inputName)
+ return false
+ } else {
+ this.log(`unhooking ${outHunk.id} to ${inHunk.id}`)
+ if (outHunk.outputs[outputName].remove(inHunk.inputs[inputName], inHunk.id)) {
+ this.writemessage('removelink', {
+ outId: outHunkId,
+ outName: outputName,
+ inId: inHunkId,
+ inName: inputName
})
+ return true
+ } else {
+ return false
+ }
}
-
- let addHunkList = (list) => {
- return new Promise((resolve, reject) => {
- let unloaded = new Array()
- let loaded = new Array()
- // first, we should get the program
- for (let hnk in list) {
- let name = list[hnk].name
- let id = list[hnk].id
- unloaded.push(id)
- this.addHunk(name, id).then((hunk) => {
- if (unloaded.includes(hunk.id)) {
- unloaded.splice(unloaded.indexOf(hunk.id), 1)
- loaded.push(hunk)
- if (unloaded.length < 1) {
- resolve(loaded)
- }
- } else {
- reject(new Error('strange catch when adding a list of hunks'))
- }
- }).catch((err) => {
- reject(err)
- })
+ }
+
+ /* --------------------------- ---------------------------- */
+ /* ---------------------- STATE CHANGES ---------------------- */
+ /* --------------------------- ---------------------------- */
+
+ this.stateChange = (hunkId, stateName, newValue) => {
+ return new Promise((resolve, reject) => {
+ this.log(`to change ${stateName} to ${newValue} in ${hunkId}`)
+ let theHunk = hunks.find((hunk) => {
+ return hunk.id == hunkId
+ })
+ if (theHunk !== undefined) {
+ let theState = theHunk.state[stateName]
+ if (theState !== null && theState !== undefined) {
+ theState.change(newValue)
+ resolve()
+ } else {
+ reject(new Error('@ statechange theState is undefined'))
+ }
+ } else {
+ reject(new Error('@ statechange theHunk is undefined'))
+ }
+ })
+ }
+
+ /* --------------------------- ---------------------------- */
+ /* --------------------- PROGRAM LOADING --------------------- */
+ /* --------------------------- ---------------------------- */
+
+ this.addProgram = (prgname) => {
+ return new Promise((resolve, reject) => {
+ let unloaded = new Array()
+ let loaded = new Array()
+ // first, we should get the program
+ // HERE consider this,
+ // probably re-write addlink to auto-complete as well
+ // and then: the view init ... the stuffy message buffer ... the flush ?
+ GG.getJson('programs/' + prgname + '.json').then((obj) => {
+ if (obj.programname !== null && obj.programname !== undefined) {
+ this.log(`opening program with name: ${obj.programname}`)
+ addHunkList(obj.hunks).then((newHunks) => {
+ this.log(`finished adding hunks from: ${obj.programname}, now links`)
+ for (let lnk of obj.links) {
+ if (this.addLink(lnk.outhunk, lnk.outname, lnk.inhunk, lnk.inname)) {
+ // success! continue
+ } else {
+ reject('watch err in program load, at links', lnk)
+ }
}
+ resolve(newHunks)
+ }).catch((err) => {
+ reject(err)
+ })
+ } else {
+ reject(new Error('program loads, not named'))
+ }
+ }).catch((err) => {
+ reject(err)
+ })
+ })
+ }
+
+ let addHunkList = (list) => {
+ // list is str8 json 'hunks' list
+ // objects always have "name" and "id" properties,
+ // sometimes have "state" properties
+ return new Promise((resolve, reject) => {
+ let unloaded = new Array()
+ let loaded = new Array()
+ // first, we should get the program
+ for (let hnk in list) {
+ let name = list[hnk].name
+ let id = list[hnk].id
+ let state = list[hnk].state
+ unloaded.push(id)
+ this.addHunk(name, id, state).then((hunk) => {
+ if (unloaded.includes(hunk.id)) {
+ unloaded.splice(unloaded.indexOf(hunk.id), 1)
+ loaded.push(hunk)
+ if (unloaded.length < 1) {
+ resolve(loaded)
+ }
+ } else {
+ reject(new Error('strange catch when adding a list of hunks'))
+ }
+ }).catch((err) => {
+ reject(err)
})
- }
-
- /* --------------------------- ---------------------------- */
- /* ---------------------- STARTUP, LOOP ---------------------- */
- /* --------------------------- ---------------------------- */
-
- this.init = () => {
- // startup by giving ourselves an ID if we haven't been assigned one?
- // and then adding ourselves to ourselves ?
- hunks.push(this)
- this.log(`manager hello, id is ${this.id}`)
- }
-
- this.loop = () => {
- if (this.inputs.msgs.io) {
- let msg = this.inputs.msgs.get()
- let header = msg.header
- let content = msg.content
- this.log(`gets msg ${header}`)
- switch (header) {
- case 'hello':
- let options = this.getHelloResponses()
- this.writemessage('putcontextoptions', options)
- break
- case 'listhunks':
- this.getListOfAvailableHunks().then((list) => {
- let options = new Array()
- // this *probably* needs work
- for (let item of list) {
- let option = {
- text: item,
- header: 'addhunk',
- content: item
- }
- options.push(option)
- }
- this.writemessage('putcontextoptions', options)
- }).catch((err) => {
- this.log('ERR while getting list of available', err)
- this.writemessage('error', err)
- })
- break
- case 'addprogram':
- // TODO: try muxed up JSON, returns unhandled promise rejection
- // in node?
- this.addProgram(content).then((newHunks) => {
- // newhunks are automatically reported on msgs, and so are new links
- // so we just want a catch block here
- }).catch((err) => {
- this.log('ERR while adding program', err)
- })
- break
- case 'addhunk':
- // TODO same here, addhunk should return automatically
- this.log('adding this hunk', content)
- this.addHunk(content).then((hunk) => {
- this.log('added hunk with id', hunk.id)
- }).catch((err) => {
- this.log('ERR while adding hunk', err)
- this.writemessage('error', err)
- })
- break
- case 'removehunk':
- if (this.removeHunk(content)) {
- this.writemessage('removedef', content.id)
- } else {
- this.writemessage('error', 'could not remove hunk as requested')
- }
- break
- case 'addlink':
- if (this.addLink(content.outId, content.outName, content.inId, content.inName)) {
- // gr8 success, and addlink automatically updates with a message to the view
- } else {
- this.writemessage('error', 'could not add link as requested')
- }
- break
- case 'removelink':
- if (this.removeLink(content.outId, content.outName, content.inId, content.inName)) {
- // gr8 success, continue
- } else {
- this.writemessage('error', 'cannot remove link as requested')
- }
- break
- case 'statechange':
- this.stateChange(content.id, content.name, content.value).then(() => {
- // no response for this, message should pass from hunk
- }).catch((err) => {
- this.log('ERR at statechange', err)
- this.writemessage('error', 'could not change state as requested')
- })
- break
-
- default:
- console.log('MGR: msg with no switch')
- break
+ }
+ })
+ }
+
+ /* --------------------------- ---------------------------- */
+ /* ---------------------- STARTUP, LOOP ---------------------- */
+ /* --------------------------- ---------------------------- */
+
+ this.init = () => {
+ // startup by giving ourselves an ID if we haven't been assigned one?
+ // and then adding ourselves to ourselves ?
+ hunks.push(this)
+ this.log(`manager hello, id is ${this.id}`)
+ }
+
+ // flag,
+ let msgdebug = false
+
+ this.loop = () => {
+ // getting messages
+ if (this.inputs.msgs.io) {
+ let msg = this.inputs.msgs.get()
+ let header = msg.header
+ let content = msg.content
+ if(msgdebug) this.log(`gets msg ${header}`)
+ switch (header) {
+ case 'hello':
+ let options = this.getHelloResponses()
+ this.writemessage('putcontextoptions', options)
+ break
+ case 'listhunks':
+ this.getListOfAvailableHunks().then((list) => {
+ let options = new Array()
+ // this *probably* needs work
+ for (let item of list) {
+ let option = {
+ text: item,
+ header: 'addhunk',
+ content: item
+ }
+ options.push(option)
}
- } // end msgs input
-
- if (this.outmsgbuffer.length > 0) {
- let debug = true
- if (this.outputs.msgs.ie) {
- if (debug) {
- let msg = this.outmsgbuffer.shift()
- this.log(`msg out ${msg.header}`)
- //console.log(msg.content)
- this.outputs.msgs.put(msg)
+ this.writemessage('putcontextoptions', options)
+ }).catch((err) => {
+ this.writemessage('error', 'ERR while getting list of available' + err)
+ })
+ break
+ case 'addprogram':
+ // TODO: try muxed up JSON, returns unhandled promise rejection
+ // in node?
+ this.addProgram(content).then((newHunks) => {
+ // newhunks are automatically reported on msgs, and so are new links
+ // so we just want a catch block here
+ }).catch((err) => {
+ this.writemessage('error', 'ERR while adding program' + err)
+ })
+ break
+ case 'addhunk':
+ // TODO same here, addhunk should return automatically
+ this.log('adding this hunk', content)
+ this.addHunk(content).then((hunk) => {
+ this.log('added hunk with id', hunk.id)
+ }).catch((err) => {
+ this.log('ERR while adding hunk', err)
+ this.writemessage('error', 'ERR on adding hunk' + err)
+ })
+ break
+ case 'removehunk':
+ if (this.removeHunk(content)) {
+ this.writemessage('removedef', content.id)
+ } else {
+ this.writemessage('error', 'could not remove hunk as requested')
+ }
+ break
+ case 'addlink':
+ if (this.addLink(content.outId, content.outName, content.inId, content.inName)) {
+ // gr8 success, and addlink automatically updates with a message to the view
+ } else {
+ this.writemessage('error', 'could not add link as requested')
+ }
+ break
+ case 'removelink':
+ if (this.removeLink(content.outId, content.outName, content.inId, content.inName)) {
+ // gr8 success, continue
+ } else {
+ this.writemessage('error', 'cannot remove link as requested')
+ }
+ break
+ case 'statechange':
+ this.stateChange(content.id, content.name, content.value).then(() => {
+ // no response for this, message should pass from hunk
+ }).catch((err) => {
+ this.log('ERR at statechange', err)
+ this.writemessage('error', 'could not change state as requested')
+ })
+ break
+
+ default:
+ this.writemessage('error', 'manager receives message with no switch!')
+ this.log('MGR: msg with no switch')
+ break
+ }
+ } // end msgs input
+
+ if (this.outmsgbuffer.length > 0) {
+ let debug = true
+ if (this.outputs.msgs.ie) {
+ if (debug) {
+ let msg = this.outmsgbuffer.shift()
+ if (msgdebug) this.log(`msg out ${msg.header}`)
+ //console.log(msg.content)
+ this.outputs.msgs.put(msg)
+ } else {
+ this.outputs.msgs.put(this.outmsgbuffer.shift())
+ }
+ }
+ } // end msgs output check
+
+ // do checks
+ if (hunks.length > 0) {
+ // STEP ONE: transport outputs (posted on last loop) to inputs
+ hunks.forEach((hunk) => {
+ for (let key of Object.keys(hunk.outputs)) {
+ let output = hunk.outputs[key]
+ if (output.connections.length > 0) {
+ // if we have connections
+ if (output.io) {
+ // and the output is occupied (having data)
+ if (!output.posted) {
+ // if it has not been posted to inputs (it's fresh)
+ // double check that all inputs are clear
+ let clear = true
+ for (let pair of output.connections) {
+ if (pair.input.io) {
+ clear = false
+ }
+ }
+ // if all clear, move the output data to each input
+ if (clear) {
+ for (let pair of output.connections) {
+ // TODO: BIG copy per-input (within put function call)
+ pair.input.put(output.data)
+ }
+ output.posted = true
} else {
- this.outputs.msgs.put(this.outmsgbuffer.shift())
+ console.log('ERR: Output was put before inputs were clear')
}
- }
- } // end msgs output check
-
- // do checks
- if (hunks.length > 0) {
- // STEP ONE: transport outputs (posted on last loop) to inputs
- hunks.forEach((hunk) => {
- for (let key of Object.keys(hunk.outputs)) {
- let output = hunk.outputs[key]
- if (output.connections.length > 0) {
- // if we have connections
- if (output.io) {
- // and the output is occupied (having data)
- if (!output.posted) {
- // if it has not been posted to inputs (it's fresh)
- // double check that all inputs are clear
- let clear = true
- for (let pair of output.connections) {
- if (pair.input.io) {
- clear = false
- }
- }
- // if all clear, move the output data to each input
- if (clear) {
- for (let pair of output.connections) {
- // TODO: BIG copy per-input (within put function call)
- pair.input.put(output.data)
- }
- output.posted = true
- } else {
- console.log('ERR: Output was put before inputs were clear')
- }
- } else {
- // output has been posted, check if all clear
- let clear = true
- for (let pair of output.connections) {
- if (pair.input.io) {
- clear = false
- }
- }
- if (clear) {
- output.clear()
- }
- }
- } // end if-has-data
- } else {
- // and output with no connections is automatically cleared
- // into the aether
- if (output.io) {
- output.clear()
- }
- }
+ } else {
+ // output has been posted, check if all clear
+ let clear = true
+ for (let pair of output.connections) {
+ if (pair.input.io) {
+ clear = false
+ }
+ }
+ if (clear) {
+ output.clear()
}
- }) // end connection passing loop
- // STEP TWO: give each hunk some time to run
- // the 0is us, let's not call its loop
- for (let i = 1; i < hunks.length; i++) {
- // TODO: watch for ordered calls ? if one emits ... another catches,
- // we want cah-chunking ?
- hunks[i].loop()
+ }
+ } // end if-has-data
+ } else {
+ // and output with no connections is automatically cleared
+ // into the aether
+ if (output.io) {
+ output.clear()
}
- } // end if-have-hunks conditional
-
- /*
- // need this at the bootloop, not here also
- // setup to loop return
- if (this.isBrowser) {
- setTimeout(this.loop)
- } else {
- setImmediate(this.loop)
+ }
}
- */
- } // end loop
+ }) // end connection passing loop
+ // STEP TWO: give each hunk some time to run
+ // the 0is us, let's not call its loop
+ for (let i = 1; i < hunks.length; i++) {
+ // TODO: watch for ordered calls ? if one emits ... another catches,
+ // we want cah-chunking ?
+ hunks[i].loop()
+ }
+ } // end if-have-hunks conditional
+
+ /*
+ // need this at the bootloop, not here also
+ // setup to loop return
+ if (this.isBrowser) {
+ setTimeout(this.loop)
+ } else {
+ setImmediate(this.loop)
+ }
+ */
+ } // end loop
- /* --------------------------- ---------------------------- */
- /* --------------------- MESSAGES OUTPUT --------------------- */
- /* --------------------------- ---------------------------- */
+ /* --------------------------- ---------------------------- */
+ /* --------------------- MESSAGES OUTPUT --------------------- */
+ /* --------------------------- ---------------------------- */
- this.outmsgbuffer = new Array()
+ this.outmsgbuffer = new Array()
- this.writemessage = (header, content) => {
- // guaranteed consumption here
- let msg = {
- header: header,
- content: content
- }
- if (this.outputs.msgs.ie && this.outmsgbuffer.length < 1) {
- // str8 shooters
- this.log(`msg out ${header}`)
- //console.log(content)
- this.outputs.msgs.put(msg)
- } else {
- // gotta buffer
- this.outmsgbuffer.push(msg)
- this.log(`MGR OUTBUFFER LEN IS ${this.outmsgbuffer.length}`)
- }
+ this.writemessage = (header, content) => {
+ // guaranteed consumption here
+ let msg = {
+ header: header,
+ content: content
+ }
+ if (this.outputs.msgs.ie && this.outmsgbuffer.length < 1) {
+ // str8 shooters
+ if(msgdebug) this.log(`msg out ${header}`)
+ //console.log(content)
+ this.outputs.msgs.put(msg)
+ } else {
+ // gotta buffer
+ this.outmsgbuffer.push(msg)
+ this.log(`MGR OUTBUFFER LEN IS ${this.outmsgbuffer.length}`)
}
+ }
}
export default Manager
-// file scraped from cuttlefish on Thu Mar 28 2019 10:38:28 GMT-0400 (Eastern Daylight Time)
\ No newline at end of file
+// file scraped from cuttlefish on Thu Mar 28 2019 10:38:28 GMT-0400 (Eastern Daylight Time)
diff --git a/hunks/view.js b/hunks/view.js
index de7e6b7..7a61b40 100644
--- a/hunks/view.js
+++ b/hunks/view.js
@@ -46,6 +46,8 @@ function View() {
this.dom = $('<div>').addClass('view').get(0)
// for nested dom elements,
this.plane = $('<div>').addClass('plane').get(0)
+ // to log, type, etc
+ this.msgbox = $('<div>').addClass('msgbox').get(0)
// init transform
let dft = {
@@ -83,6 +85,29 @@ function View() {
// append
$(this.dom).append(this.plane)
+ $(this.dom).append(this.msgbox)
+ }
+
+ let writeToMessageBox = (data) => {
+ // write the message
+ this.msgbox.append($('<div>' + data + '</div>').addClass('msgboxmsg').get(0))
+ // def check
+ let heightcheck = () => {
+ let height = 0;
+ $(this.msgbox).children().each(function(child){
+ // jquery.each() syntax is a bit odd / different than elsewhere, sorry for inconsistency
+ height += this.clientHeight
+ })
+ return height
+ }
+ // if too tall, remove
+ if(heightcheck() > $(this.msgbox).clientHeight){
+ $(this.msgbox).children().get(0).remove()
+ // two at most, sloppy but fast
+ if(heightcheck() > $(this.msgbox).clientHeight){
+ $(this.msgbox).children().get(0).remove()
+ }
+ }
}
let mouseWheelListener = (evt) => {
@@ -626,6 +651,8 @@ function View() {
// the events
let evtDrag = (drag) => {
+ drag.preventDefault()
+ drag.stopPropagation()
let cp = readTransform(floater)
cp.x += drag.movementX
cp.y += drag.movementY
@@ -662,6 +689,8 @@ function View() {
// startup the floater
dom.addEventListener('mousedown', (down) => {
+ down.stopPropagation()
+ down.preventDefault()
console.log('MOUSEDOWN for', port.type, port.name, down)
// get the location to make the floater,
let cp = BZ.getRightHandle(dom, this.plane.subscale)
@@ -687,7 +716,7 @@ function View() {
}
let writeStateDom = (state, def) => {
- let dom = $('<li>' + state.name + '</li>').get(0)
+ let dom = $('<li>' + state.name + "(" + state.type + ")" + '</li>').get(0)
dom.id = def.id + '_state_' + state.name
switch (typeof state.value) {
case 'string':
@@ -753,11 +782,12 @@ function View() {
if (blkstate !== null && blkstate !== undefined) {
// find value / etc
let inp = $(blkstate).children('input').get(0)
+ // huh ?
if (inp !== null && inp !== undefined) {
inp.value = value
} else {
if (typeof value !== 'boolean') {
- console.log('ERR State Change for non Boolean on Boolean Type')
+ writeToMessageBox('View ERR: State Change for non Boolean on Boolean Type: ' + parentid + ", state: " + name + ", value: " + value)
} else {
$(blkstate).children('span').text(value)
}
@@ -771,6 +801,8 @@ function View() {
this.outmsgbuffer = new Array()
+ let msgdebug = false
+
this.writemessage = (header, content) => {
// guaranteed consumption here
let msg = {
@@ -779,8 +811,8 @@ function View() {
}
if (this.outputs.msgs.ie && this.outmsgbuffer.length < 1) {
// str8 shooters
- this.log('msg out', msg.header)
- console.log(msg.content)
+ if(msgdebug) this.log('msg out', msg.header)
+ if(msgdebug) console.log(msg.content)
this.outputs.msgs.put(msg)
} else {
// gotta buffer
@@ -805,7 +837,7 @@ function View() {
let pult = this.inputs.msgs.get()
let header = data.header
let content = data.content
- this.log(`gets msg ${header}`)
+ if(msgdebug) this.log(`gets msg ${header}`)
switch (header) {
case 'putcontextoptions':
addContextOptions(content)
@@ -826,8 +858,7 @@ function View() {
receiveStateChange(content.id, content.name, content.value)
break
case 'error':
- console.log('RECV MANAGER ERROR')
- console.log(content)
+ writeToMessageBox(content)
break
default:
console.log('VIEW: msg with no switch')
diff --git a/programs/mvnv.json b/programs/mvnv.json
index e9ed8a2..5c610ba 100644
--- a/programs/mvnv.json
+++ b/programs/mvnv.json
@@ -15,11 +15,18 @@
},
{
"name": "link",
- "id": "lnkone"
+ "id": "lnkone",
+ "state": {
+ "inputList": "msgs (byteArray), lnkIpOne (uint32)",
+ "outputList": "msgs (byteArray), linkReturnOne (uint32)"
+ }
},
{
"name": "comm/websocketclient",
- "id": "wsclient"
+ "id": "wsclient",
+ "state": {
+ "retrycount": 0
+ }
},
{
"name": "view",
diff --git a/style.css b/style.css
index ab603aa..6a367ea 100644
--- a/style.css
+++ b/style.css
@@ -54,6 +54,23 @@ body {
background-size: 100px 100px;
}
+.msgbox {
+ width: 300px;
+ height: 500px;
+ padding: 10px;
+ margin: 10px;
+ float: right;
+ background-color: rgb(192, 209, 237, 0.5);
+}
+
+.msgboxmsg{
+ padding: 3px;
+ margin-bottom: 3px;
+ background-color: white;
+ font-family: Courier;
+ font-size: 11px;
+}
+
.plane {
position: absolute;
width: 100px;
--
GitLab