Преглед изворни кода

U Neuron to handle serial input

Piotr Labudda пре 6 година
родитељ
комит
9f3aed0ae6

+ 22 - 1
SE/se-lib/Route/Test/Neuron.php

@@ -17,8 +17,10 @@ class Route_Test_Neuron extends RouteBase {// TODO: UrlActionBase @see Route_Url
         UI::inlineJS(__FILE__ . '.NeuronView.js', [
             'MAKE_STORE_FUNCTION_NAME' => "makeNeuronStore",
             'HTML_ID' => "p5widget-test-neuron",
+
+            'INPUT_TYPE' => "text",
             // 'INITIAL_DATA' => "AABBBBAABBAABB",
-            'INITIAL_DATA' => "BCAAAAABCABCABCABCABCABCABCABC",
+            'INITIAL_DATA' => explode("", "BCAAAAABCABCABCABCABCABCABCABC"),
             // 'INITIAL_DATA' => implode("\n", [
             //     "A",
             //     "AAA",
@@ -30,6 +32,25 @@ class Route_Test_Neuron extends RouteBase {// TODO: UrlActionBase @see Route_Url
             //     "AA",
             //     "BB",
             // ]),
+
+            'INPUT_TYPE' => "key",
+            'INITIAL_DATA' => [
+                [ "A" ],
+                [ "A" ],
+                [ "A" ],
+                [ "A", "B" ],
+                [ "A", "B" ],
+                [ "A", "B", "C" ],
+                [ "A", "B" ],
+                [ "A", "B" ],
+                [ "A", "B", "D" ],
+                [ "A", "B", "C", "D" ],
+                [ "C", "D" ],
+                [ "C", "D" ],
+                [ "C", "D" ],
+                [ "A", "B", "D" ],
+                [ "A", "D" ],
+            ],
         ]);
         throw new Exception("TODO");
     }

+ 223 - 88
SE/se-lib/Route/Test/Neuron.php.NeuronView.js

@@ -27,7 +27,8 @@ var DEFAULT_CONFIG = {
 	ui_output_height: 300,
 	ui_max_receptor_r: 12,
 	ui_space_y: 30,
-	config_anim_speed: 200,
+	config_anim_speed: 500,
+	config_read_input_keys_speed: 700,
 	config_read_speed_multiplier: 4,
 	config_charge_receptor_at_input: 1,
 	config_max_receptor_charge: 1,
@@ -63,7 +64,7 @@ function LOG__renderTheadLog(listLogEntries) {
 	var lastLog = listLogEntries[listLogEntries.length - 1]
 	return [
 		h('tr', {},
-			[h('th', {}, "Lp.")]
+			[ h('th', {}, "Lp.") ]
 				.concat(
 					lastLog.map(function (nodeInfo) {
 						return h('th', {
@@ -78,10 +79,10 @@ function LOG__renderTbodyLog(listLogEntries) {
 	if (!listLogEntries.length) return null;
 	return listLogEntries.map(function (log, lpLog) {
 		return h('tr', {},
-			[h('th', { style: { color: "#ddd" } }, '' + (lpLog + 1) + '.')]
-				.concat(
-					log.map(LOG__renderCellNodeChargeLog)
-				)
+			[ h('th', { style: { color: "#ddd" } }, '' + (lpLog + 1) + '.') ]
+			.concat(
+				log.map(LOG__renderCellNodeChargeLog)
+			)
 		)
 	})
 }
@@ -90,34 +91,104 @@ function LOG__renderCellNodeChargeLog(nodeInfo) {
 	return h('td', {
 		style: {
 			color: (nodeInfo.charge > 0.1) ? "#000" : "#aaa",
-			backgroundColor: (nodeInfo.charge > 0) ? ((nodeInfo.type === 'receptor') ? "#fa0" : "#00d0ff") : "none",
+			backgroundColor: (nodeInfo.charge > 0) ? ( (nodeInfo.type === 'receptor') ? "#fa0" : "#00d0ff" ) : "none",
 		},
 	}, nodeInfo.charge.toFixed(2));
 }
 
+var p5Utils__ArrayDistinctFilter = function (value, idx, self) {
+	return (idx === self.indexOf(value));
+};
 
 
 var NeuronView = createReactClass({
-	_inputNode: null,
-	_outputNode: null,
+	// _inputNode: null,
+	// _outputNode: null,
 	_receptor: [],
 	_neuron: [],
 	_store: null,
 	_log: [],
+	_readInputLineIntervalID: null,
 	getInitialState: function () {
 		return Object.assign({}, DEFAULT_CONFIG, this.getStateFromStore(), {
-			inputText: this.props.initialData || '', // TODO: INITIAL_DATA
+			inputText: (this.props.initialData || []).join(''),
 
+			inputType: this.props.inputType || "text",
+			inputKeysUp: "",
 			animLastLetter: null,
 			selectedReceptorIdx: null,
 		});
 	},
 	componentDidMount: function () {
 		this._unsubscribe = this.props.store.subscribe(this.storeUpdated)
-		this.props.store.dispatch({ type: 'INIT', inputText: this.state.inputText, config: this.state })
+		document.addEventListener('keyup', this.handleKeyUp)
+		document.addEventListener('keydown', this.handleKeyDown)
+
+		this.startNewAnim(this.props.initialData);
+
+		this._readInputLineIntervalID = setInterval(this.inputReadKeys, this.state.config_read_input_keys_speed)
+	},
+	startNewAnim: function (initialData) {
+		this.log = [];
+		switch (this.state.inputType) {
+			case "text": this.props.store.dispatch({ type: 'INIT', input: this.prepareInputFromText(this.state.inputText), config: this.state }); break;
+			case "key": this.props.store.dispatch({ type: 'INIT', input: initialData || [], config: this.state }); break;
+		}
+	},
+	prepareInputFromText: function (text) {
+		return text.split('').map(function (char) { return char.split(''); })
+	},
+	handleKeyUp: function (event) {
+		DBG && console.log("DBG:NeuronView:handleKeyUp", { keyCode: event.keyCode })
+		if (32 === event.keyCode) {
+			event.preventDefault()
+			var state = this.props.store.getState()
+			if (state.doAnim) {
+				this.props.store.dispatch({ type: 'PAUSE' })
+			} else {
+				this.props.store.dispatch({ type: 'PLAY' })
+			}
+		}
+	},
+	handleKeyDown: function (event) {
+		if (32 === event.keyCode) event.preventDefault() // avoid page scroll after hit space btn
+		// [A-Z] = [65-90]
+		if ("key" === this.state.inputType && event.keyCode >= 65 && event.keyCode <= 90) {
+			this.setState({
+				inputKeysUp: this.state.inputKeysUp + String.fromCharCode(event.keyCode)
+			});
+		}
+	},
+	inputReadKeys: function () {
+		if ("key" !== this.state.inputType) return;
+
+		var inputKeysUp = this.state.inputKeysUp
+		var uniqInputKeys = inputKeysUp.split('').filter(p5Utils__ArrayDistinctFilter)
+		this.setState({
+			inputKeysUp: "",
+		});
+		// if (uniqInputKeys.length > 0) {
+			this.props.store.dispatch({ type: 'INPUT_ADD_LINE', input: [ uniqInputKeys ] })
+		// }
+	},
+	handleToggleInput: function (event) {
+		// p5_node_id: "input_type-text",
+		// p5_node_id: "input_type-key",
+		event.preventDefault()
+		var targetNodeType = event.target.getAttribute("p5_node_id")
+		if (!targetNodeType) return;
+		var toType = targetNodeType.substr("input_type-".length)
+		DBG && console.log("DBG:handleToggleInput", { target: targetNodeType, toType })
+		if (toType !== this.state.inputType) {
+			this.setState({ inputType: toType })
+			this.startNewAnim()
+		}
 	},
 	componentWillUnmount: function () {
 		if (this._unsubscribe) this._unsubscribe()
+		document.removeEventListener('keyup', this.handleKeyUp)
+		document.removeEventListener('keydown', this.handleKeyDown)
+		if (this._readInputLineIntervalID) clearInterval(this._readInputLineIntervalID)
 	},
 	storeUpdated: function () {
 		DBG && console.log('DBG:NeuronView:storeUpdated');
@@ -139,14 +210,14 @@ var NeuronView = createReactClass({
 		this.setState({
 			inputText: event.target.value,
 		})
+		this.props.store.dispatch({ type: 'INPUT_UPDATED', input: this.prepareInputFromText(event.target.value) })
 	},
-	setInputRet: function (reactEl) { this._inputNode = reactEl; },
-	setOutputRet: function (reactEl) { this._outputNode = reactEl; },
+	// setInputRet: function (reactEl) { this._inputNode = reactEl; },
+	// setOutputRet: function (reactEl) { this._outputNode = reactEl; },
 	handleReset: function (event) {
 		event.preventDefault();
 		DBG && console.log('DBG:NeuronView:handleReset...');
-		this._log = []
-		this.props.store.dispatch({ type: 'INIT', inputText: this.state.inputText, config: this.state })
+		this.startNewAnim()
 	},
 	handlePause: function (event) {
 		event.preventDefault();
@@ -219,8 +290,8 @@ var NeuronView = createReactClass({
 		// timesCreated: 1
 		// toIdx: 2
 		switch (conn.fromType) {
-			case 'receptor': return this.getConnectionUIPosFromReceptor(conn.fromIdx, conn.toIdx);
-			case 'neuron': return this.getConnectionUIPosFromNeuron(conn.fromIdx, conn.toIdx);
+			case 'receptor': return this.getConnectionUIPosFromReceptor(conn.from, conn.to);
+			case 'neuron': return this.getConnectionUIPosFromNeuron(conn.from, conn.to);
 			default: {
 				DBG && console.warn("Not implemented render charge type '" + conn.fromType + "'", { conn })
 				return null;
@@ -257,24 +328,21 @@ var NeuronView = createReactClass({
 	},
 	renderCharge: function (charge, idx) {
 		DBG && console.log("DBG:NeuronView:renderCharge", { charge, idx })
-		// charge: 1
-		// nodeIdx: 9
-		// nodeType: "connection"
 		switch (charge.nodeType) {
-			case 'receptor': return this.renderChargeOnReceptor(charge.nodeIdx, charge.charge, idx);
-			case 'neuron': return this.renderChargeOnNeuron(charge.nodeIdx, charge.charge, idx);
-			case 'connection': return this.renderChargeOnConnection(charge.nodeIdx, charge.charge, idx);
-			case 'dbl_conn': return this.renderChargeOnDoubleConnection(charge.nodeIdx, charge.charge, idx);
+			case 'receptor': return this.renderChargeOnReceptor(charge.node, charge.value, idx);
+			case 'neuron': return this.renderChargeOnNeuron(charge.node, charge.value, idx);
+			case 'connection': return this.renderChargeOnConnection(charge.node, charge.value, idx);
+			case 'doubleConn': return this.renderChargeOnDoubleConnection(charge.node, charge.value, idx);
 			default: {
 				DBG && console.warn("Not implemented render charge type '" + charge.nodeType + "'", { charge, idx })
 				return null;
 			}
 		}
 	},
-	renderChargeOnReceptor: function (idx, charge, chargeIdx) {
+	renderChargeOnReceptor: function (idx, value, chargeIdx) {
 		var sourceNode = this.props.store.getReceptor(idx)
 		if (!sourceNode) {
-			DBG && console.warn("Missing source node at renderChargeOnReceptor", { charge, idx })
+			DBG && console.warn("Missing source node at renderChargeOnReceptor", { charge: value, idx })
 			return null;
 		}
 		var uiNodePos = sourceNode.ui
@@ -293,10 +361,10 @@ var NeuronView = createReactClass({
 			}))
 		]);
 	},
-	renderChargeOnNeuron: function (idx, charge, chargeIdx) {
+	renderChargeOnNeuron: function (idx, value, chargeIdx) {
 		var sourceNode = this.props.store.getNeuron(idx)
 		if (!sourceNode) {
-			DBG && console.warn("Missing source node at renderChargeOnNeuron", { charge, idx })
+			DBG && console.warn("Missing source node at renderChargeOnNeuron", { charge: value, idx })
 			return null;
 		}
 		var uiNodePos = sourceNode.ui
@@ -315,60 +383,45 @@ var NeuronView = createReactClass({
 			}))
 		]);
 	},
-	renderChargeOnConnection: function (idx, charge, chargeIdx) {
-		var sourceNode = this.props.store.getConnection(idx)
-		if (!sourceNode) {
-			DBG && console.warn("Missing source node at renderChargeOnConnection", { charge, idx })
+	renderChargeOnConnection: function (idx, value, chargeIdx) {
+		var connNode = this.props.store.getConnection(idx)
+		if (!connNode) {
+			DBG && console.warn("Missing source node at renderChargeOnConnection", { charge: value, idx })
 			return null;
 		}
-		var uiConnPos = this.getConnectionUIPos(sourceNode) // @return { x1, y1, x2, y2 }
+		var uiConnPos = this.getConnectionUIPos(connNode) // @return { x1, y1, x2, y2 }
 		var uiPos = {
 			cx: Math.min(uiConnPos.x1, uiConnPos.x2) + Math.abs((uiConnPos.x1 - uiConnPos.x2) / 2),
 			cy: Math.min(uiConnPos.y1, uiConnPos.y2) + Math.abs((uiConnPos.y1 - uiConnPos.y2) / 2),
 		}
 		DBG && console.log("DBG:NeuronView:renderChargeOnConnection", { uiConnPos, uiPos })
-		// sourceNode: { fromType: 'receptor', fromIdx: overcharged.idx, toIdx: idxNewNeuron, timesCreated: 1 }
+		// connNode: { fromType: 'receptor', fromIdx: overcharged.idx, toIdx: idxNewNeuron, timesCreated: 1 }
 
 		return h('g', { p5_node_id: "charge-idx-" + chargeIdx, p5_node_type: "charge-on-conn" }, [
 			h('circle', Object.assign({}, uiPos, {
-				r: 5,
+				r: 8,
 				stroke: "#00d0ff",
 				fill: "#fbfbfb",
 				strokeWidth: "3px",
-			}))
+			})),
+			h('text', {
+				x: uiPos.cx, y: uiPos.cy + 1, fill: "#777",
+				dominantBaseline: "middle", textAnchor: "middle",
+				style: { fontSize: "8px", cursor: "pointer" },
+				// data_neuron_idx: idx,
+				// onClick: this.handleClickNeuron,
+			}, value.toFixed(1)),
 		]);
 	},
-	renderChargeOnDoubleConnection: function (idx, charge, chargeIdx) {
-		var connNode = this.props.store.getDoubleConnection(idx)
+	renderChargeOnDoubleConnection: function (idx, value, chargeIdx) {
+		var doubleConnNode = this.props.store.getDoubleConnection(idx)
 		var chargeNode = this.props.store.getCharge(chargeIdx)
-		var connFromNode = this.props.store.getConnection(connNode.connIdx[0])
-		var connToNode = this.props.store.getConnection(connNode.connIdx[1])
-		DBG1 && console.warn("TODO:NeuronView:renderChargeOnDoubleConnection", { idx, charge, chargeIdx, connNode, chargeNode })
+		DBG && console.warn("TODO:NeuronView:renderChargeOnDoubleConnection", { idx, charge: value, chargeIdx, doubleConnNode, chargeNode })
 
 		return h(React.Fragment, {}, [
-			this.renderChargeOnConnection(connNode.connIdx[0], chargeNode.fromCharge, '' + chargeIdx + '-from'),
-			this.renderChargeOnConnection(connNode.connIdx[1], chargeNode.toCharge, '' + chargeIdx + '-to'),
+			this.renderChargeOnConnection(doubleConnNode.fromConn[0], chargeNode.connCharge[0], '' + chargeIdx + '-0'),
+			this.renderChargeOnConnection(doubleConnNode.fromConn[1], chargeNode.connCharge[1], '' + chargeIdx + '-1'),
 		]);
-		// if (!sourceNode) {
-		//     DBG && console.warn("Missing source node at renderChargeOnConnection", { charge, idx })
-		//     return null;
-		// }
-		// var uiConnPos = this.getConnectionUIPos(sourceNode) // @return { x1, y1, x2, y2 }
-		// var uiPos = {
-		//     cx: Math.min(uiConnPos.x1, uiConnPos.x2) + Math.abs((uiConnPos.x1 - uiConnPos.x2) / 2),
-		//     cy: Math.min(uiConnPos.y1, uiConnPos.y2) + Math.abs((uiConnPos.y1 - uiConnPos.y2) / 2),
-		// }
-		// DBG && console.log("DBG:NeuronView:renderChargeOnConnection", { uiConnPos, uiPos })
-		// // sourceNode: { fromType: 'receptor', fromIdx: overcharged.idx, toIdx: idxNewNeuron, timesCreated: 1 }
-
-		// return h('g', { p5_node_id: "charge-idx-" + chargeIdx, p5_node_type: "charge-on-conn" }, [
-		//     h('circle', Object.assign({}, uiPos, {
-		//         r: 5,
-		//         stroke: "#00d0ff",
-		//         fill: "#fbfbfb",
-		//         strokeWidth: "3px",
-		//     }))
-		// ]);
 	},
 	renderNeuron: function (neuron, idx) {
 		var chargeValue = this.props.store.getNeuronCharge(idx)
@@ -383,8 +436,8 @@ var NeuronView = createReactClass({
 				// onClick: this.handleClickNeuron,
 				style: { cursor: "pointer" },
 			}), [
-					h('title', {}, this.viewNeuronTitle(neuron.value)),
-				]),
+				h('title', {}, this.viewNeuronTitle(neuron.value)),
+			]),
 			h('text', {
 				x: neuron.ui.cx, y: neuron.ui.cy + 1, fill: fontColor,
 				dominantBaseline: "middle", textAnchor: "middle",
@@ -399,7 +452,7 @@ var NeuronView = createReactClass({
 			.replace(new RegExp("\n", "g"), "\\n")
 			.replace(new RegExp("\t", "g"), "\\t")
 			.replace(new RegExp(" ", "g"), "_")
-			;
+		;
 	},
 	viewNeuronTitle: function (value) {
 		return this.viewNeuronValue(value);
@@ -414,7 +467,7 @@ var NeuronView = createReactClass({
 		var charge = this.props.store.getReceptorCharge(idx) // receptor.charge
 		var chargePr = (100 * charge) / this.state.config_max_receptor_charge
 		// var shapeColor = (chargePr > 100) ? "#f00" : "hsl(194, 67%, " + (100 - chargePr) + "%)"; // blue -> dark blue -> red
-		var shapeColor = (chargePr > 100) ? "#f00" : "hsl(" + (40 - chargePr * 2 / 5).toFixed() + ", 100%, 50%)" // orange -> dark orange -> red
+		var shapeColor = (chargePr > 100) ? "#f00" : "hsl(" + ( 40 - chargePr * 2 / 5 ).toFixed() + ", 100%, 50%)" // orange -> dark orange -> red
 		var fontColor = "#fff" // (chargePr < 50) ? "#000" : "#fff";
 		// this.state.config_max_receptor_charge = 100%
 		// charge = x% (max 100)
@@ -426,8 +479,8 @@ var NeuronView = createReactClass({
 				onClick: this.handleClickReceptor,
 				style: { cursor: "pointer" },
 			}), [
-					h('title', {}, this.viewReceptorTitle(receptor.value)),
-				]),
+				h('title', {}, this.viewReceptorTitle(receptor.value)),
+			]),
 			h('text', {
 				x: receptor.ui.cx, y: receptor.ui.cy + 1, fill: fontColor,
 				dominantBaseline: "middle", textAnchor: "middle",
@@ -489,9 +542,24 @@ var NeuronView = createReactClass({
 	},
 	renderInput: function () {
 		return h('details', { open: true, style: { margin: "12px 0", border: "1px solid #aaa", borderRadius: "6px", padding: "12px", backgroundColor: "#eee", } }, [
-			h('summary', { style: { cursor: "pointer" } }, "Input"),
-			h('textarea', {
-				ref: this.setInputRet,
+			h('summary', { style: { cursor: "pointer", overflow: "hidden" } }, [
+				"Input",
+				h('span', { className: "pull-right" }, [
+					h('label', { style: { paddingRight: "6px" } }, "type:"),
+					h('button', {
+						className: "btn btn-sm btn-default" + ( this.state.inputType === 'text' ? " active" : "" ),
+						p5_node_id: "input_type-text",
+						onClick: this.handleToggleInput,
+					}, "text"),
+					h('button', {
+						className: "btn btn-sm btn-default" + ( this.state.inputType === 'key' ? " active" : "" ),
+						p5_node_id: "input_type-key",
+						onClick: this.handleToggleInput,
+					}, "key"),
+				])
+			]),
+			(this.state.inputType === 'text') && h('textarea', {
+				// ref: this.setInputRet,
 				onChange: this.handleChangeInput,
 				value: this.state.inputText,
 				rows: 10,
@@ -502,6 +570,19 @@ var NeuronView = createReactClass({
 					backgroundColor: "#fff",
 				},
 			}),
+			(this.state.inputType === 'key') && h('div', {
+				style: {
+					width: "100%",
+					padding: "12px",
+					marginTop: "6px",
+					backgroundColor: "#fff",
+					border: "1px solid #333",
+				},
+			}, [
+				h('p', {}, [
+					"Use keyboard keys from A to Z"
+				]),
+			]),
 		]);
 	},
 	renderConfig: function () {
@@ -573,7 +654,7 @@ var NeuronView = createReactClass({
 					value: this.state.config_strategy_overcharge,
 					name: "config_strategy_overcharge",
 					onChange: this.handleChangeConfig,
-				}, ["REMOVE_CHARGE", "LEAVE_HALF_CHARGE", "LEAVE_ALMOST_MAX"].map(function (strategy) {
+				}, [ "REMOVE_CHARGE", "LEAVE_HALF_CHARGE", "LEAVE_ALMOST_MAX" ].map(function (strategy) {
 					return h('option', { value: strategy }, strategy);
 				})),
 			]),
@@ -600,16 +681,16 @@ var NeuronView = createReactClass({
 					)
 			].concat(
 				this._log.map(function (log) {
-					return log.receptor.map(function (node) { return node.charge })
-						.concat(
-							log.neuron.map(function (node) { return node.charge })
-						)
+				return log.receptor.map(function (node) { return node.charge })
+					.concat(
+						log.neuron.map(function (node) { return node.charge })
+					)
 				})
 			)
 		)
 	},
 	render: function () {
-		DBG1 && console.log("DBG:NeuronView:render", {
+		DBG && console.log("DBG:NeuronView:render", {
 			state: this.state,
 			receptor: [].concat(this.props.store.getListReceptor()),
 			neuron: [].concat(this.props.store.getListNeuron()),
@@ -618,28 +699,81 @@ var NeuronView = createReactClass({
 		});
 
 		var state = this.props.store.getState()
+		var input = this.props.store.getInput()
+		var receptorLabels = state.receptor.map(function (receptor) { return receptor.value; })
 
 		return h('div', {}, [
 			h('table', { className: "table", p5_node_id: "p5-neuron-output-table" }, [
 				h('tbody', {}, [
 					h('tr', {}, [
 						h('td', { style: { width: this.state.ui_output_width + 20 } }, [
-							h('svg', { ref: this.setOutputRet, height: this.state.uiOutputHeight, width: this.state.ui_output_width, style: { border: "1px solid #eee" } }, [
+							h('svg', {
+								// ref: this.setOutputRet,
+								height: this.state.uiOutputHeight, width: this.state.ui_output_width,
+								style: { border: "1px solid #eee" }
+							}, [
 								state.connection.map(this.renderConnection),
 								state.receptor.map(this.renderReceptor),
 								state.neuron.map(this.renderNeuron),
 								state.charge.map(this.renderCharge),
 							]),
-							h('div', {
+							// TODO: more matrix
+							// h('svg', {
+							//     // ref: this.setOutputRet,
+							//     height: this.state.uiOutputHeight, width: this.state.ui_output_width,
+							//     style: { border: "1px solid #eee" }
+							// }, [
+							//     state.connection.map(this.renderConnection),
+							//     state.receptor.map(this.renderReceptor),
+							//     state.neuron.map(this.renderNeuron),
+							//     state.charge.map(this.renderCharge),
+							// ]),
+							(this.state.inputType === 'text') && h('div', {
 								style: {
 									padding: "6px",
 									border: "1px solid #eee",
 								}
 							}, [
-									h('b', {}, "Input: "),
-									h('span', {}, this.state.inputText.substr(0, this.state.inputReadPos)),
-									h('span', { style: { color: "#ddd" } }, this.state.inputText.substr(this.state.inputReadPos)),
-								]),
+								h('b', {}, "Input: "),
+								h('span', {}, this.state.inputText.substr(0, this.state.inputReadPos)),
+								h('span', { style: { color: "#ddd" } }, this.state.inputText.substr(this.state.inputReadPos)),
+							]),
+							(this.state.inputType === 'key') && h('div', {
+								style: {
+									padding: "6px",
+									border: "1px solid #eee",
+								}
+							}, [
+								h('b', {}, "Input keys: "),
+								h('span', {}, this.state.inputKeysUp.split('').filter(p5Utils__ArrayDistinctFilter).join('')),
+								(!this.state.inputKeysUp.length) && h('em', { style: { color: "#aaa" } }, "(Use keyboard keys from A to Z)"),
+								h('div', {
+									style: { overflowX: "scroll", marginBottom: "12px" },
+								}, h('table', { className: "table-bordered" }, [
+									h('tbody', {}, receptorLabels.map(function (label) {
+										return h('tr', {}, [
+											h('th', {
+												style: {
+													padding: "0 4px",
+													color: "#fff",
+													backgroundColor: "#fa0",
+												},
+											}, label + ":"),
+										].concat(input.map(function (line, idx) {
+											var isLabelInLine = (-1 !== line.indexOf(label))
+											return h('td', {
+												style: {
+													padding: "0 2px",
+													color: isLabelInLine ? "#333" : "#ddd",
+													backgroundColor: isLabelInLine ? "#333" : "#ddd",
+													opacity: idx > state.inputReadPos ? "0.7" : "1",
+													borderRight: idx === state.inputReadPos ? "3px solid #f00" : "1px solid #ddd",
+												},
+											}, isLabelInLine ? "x" : "_");
+										})));
+									}))
+								])),
+							]),
 						]),
 						h('td', { style: { verticalAlign: "top" } }, [
 							// this.renderReceptorInfo(),
@@ -655,8 +789,8 @@ var NeuronView = createReactClass({
 				" (animPos:" + this.state.animPos + ") ",
 				" ",
 				state.doAnim
-					? h('button', { className: "btn btn-default", onClick: this.handlePause }, [h('i', { className: "glyphicon glyphicon-pause" }), "Stop"])
-					: h('button', { className: "btn btn-default", onClick: this.handlePlay }, [h('i', { className: "glyphicon glyphicon-play" }), "Start"])
+					? h('button', { className: "btn btn-default", onClick: this.handlePause }, [ h('i', { className: "glyphicon glyphicon-pause" }), "Stop" ])
+					: h('button', { className: "btn btn-default", onClick: this.handlePlay }, [ h('i', { className: "glyphicon glyphicon-play" }), "Start"])
 				,
 				h('button', { className: "btn btn-default", onClick: this.handleRefreshLog }, [h('i', { className: "glyphicon glyphicon-refresh" }), " ", "Log"])
 			]),
@@ -684,5 +818,6 @@ var NeuronView = createReactClass({
 // export default NeuronView
 ReactDOM.render(h(NeuronView, {
 	store: global[MAKE_STORE_FUNCTION_NAME](),
-	initialData: INITIAL_DATA || "",
-}), document.getElementById(HTML_ID))
+	initialData: INITIAL_DATA || [],
+	inputType: INPUT_TYPE || "text",
+}), document.getElementById(HTML_ID))

+ 250 - 139
SE/se-lib/Route/Test/Neuron.php.makeNeuronStore.js

@@ -2,6 +2,8 @@ var MAKE_STORE_FUNCTION_NAME = MAKE_STORE_FUNCTION_NAME || 'makeNeuronStore'
 var DBG = DBG || false;
 var DBG1 = true;
 
+// DEFAULT_CONFIG.config_strategy_overcharge: "LEAVE_ALMOST_MAX", // "REMOVE_CHARGE" | "LEAVE_HALF_CHARGE" | "LEAVE_ALMOST_MAX"
+
 var DEFAULT_NEURON = {
 	value: '',
 	charge: 0,
@@ -26,24 +28,35 @@ function makeDefaultNeuronsStoreState() {
 	}
 }
 
+var p5Utils__ArrayDistinctFilter = function (value, idx, self) {
+	return (idx === self.indexOf(value));
+};
+var p5Utils__ArrayDiff = function (a, b) { // a - b @return array with items from `a` that are not in `b`
+	return a.filter(function (x) {
+		return (-1 === b.indexOf(x));
+	})
+};
+var p5Utils__inputToFlatString = function (input) {
+	return input.reduce(function (ret, line) {
+		return ret.concat(line).concat(['\n'])
+	}, []).join('')
+};
+
 
 function makeNeuronStore() {
 	DBG && console.log("DBG:NeuronStore:makeNeuronStore");
 	var _state = makeDefaultNeuronsStoreState()
 	var _config = {}
-	var _inputText = ''
+	var _input = []
 	var _animIntervalID = null
 	var _callback = null
 
-	function stateReset(inputText, config) {
-		_inputText = inputText
+	function stateReset(input, config) {
+		_input = input
 		_config = config
 		_state = makeDefaultNeuronsStoreState()
 
-		var distinct = function (value, idx, self) {
-			return (idx === self.indexOf(value));
-		};
-		var foundLetters = inputText.split("").filter(distinct);
+		var foundLetters = _input.reduce(function (ret, line) { return ret.concat(line) }, []).filter(p5Utils__ArrayDistinctFilter);
 		_state.receptor = foundLetters.sort().map(makeReceptor);
 		_state.mapReceptorChar = _state.receptor.map(function (node) { return node.value; });
 
@@ -52,17 +65,97 @@ function makeNeuronStore() {
 		_notifySubscribers();
 	}
 
-	function readCharFromInput() {
-		if (_state.inputReadPos >= _inputText.length) return null;
+	function inputUpdated(newInput) {
+		var oldInput = _input
+		DBG && console.log("DBG:NeuronStore:inputUpdated", { newInput, oldInput })
+		if (!newInput || !newInput.length) {
+			// Missing input
+			return;
+		}
+		if (newInput.length < oldInput.length) {
+			stateReset(newInput, _config)
+			startAnimation()
+			return;
+		}
+		if (p5Utils__inputToFlatString(newInput.slice(0, oldInput.length)) !== p5Utils__inputToFlatString(oldInput)) {
+			stateReset(newInput, _config)
+			startAnimation()
+			return;
+		}
+		DBG && console.log("DBG:NeuronStore:inputUpdated", { msg: "TODO: add input => add Receptors" })
+		_input = newInput
+		{ // add new receptors if needed
+			var addInput = newInput.slice(oldInput.length)
+			var newLetters = addInput.reduce(function (ret, line) { return ret.concat(line) }, []).filter(p5Utils__ArrayDistinctFilter)
+			var oldLetters = oldInput.reduce(function (ret, line) { return ret.concat(line) }, []).filter(p5Utils__ArrayDistinctFilter)
+			var addLetters = p5Utils__ArrayDiff(newLetters.sort(), oldLetters)
+			DBG && console.log("DBG:NeuronStore:inputUpdated", { msg: "TODO: add input => add Receptors", addLetters })
+			if (!addLetters.length) return;
+
+			var totalReceptors = oldLetters.length + addLetters.length
+			var oldFirstCx = Math.ceil(_config.ui_output_width / (oldLetters.length + 1));
+			var newFirstCx = Math.ceil(_config.ui_output_width / (totalReceptors + 1));
+			var moveXFactor = Number((newFirstCx / oldFirstCx).toFixed(2))
+			DBG && console.log("DBG:NeuronStore:inputUpdated", { msg: "TODO: add input => add Receptors", moveXFactor, oldFirstCx, newFirstCx })
+			var allLetters = [].concat(oldLetters, addLetters)
+			_state.receptor = _state.receptor.map(function (receptor) {
+				receptor.ui.cx = receptor.ui.cx * moveXFactor
+				return receptor;
+			})
+			_state.neuron = _state.neuron.map(function (neuron) {
+				neuron.ui.cx = neuron.ui.cx * moveXFactor
+				return neuron;
+			})
+			addLetters.forEach(function (letter, idx) {
+				_state.receptor.push(makeReceptor(letter, idx + oldLetters.length, allLetters))
+			})
+			_state.mapReceptorChar = _state.receptor.map(function (node) { return node.value; });
+		}
+	}
+
+	function inputAddLine(inputLine) { // inputLine: [ [ "A", "B", ... ], ... ]
+		DBG && console.log("DBG:NeuronStore:inputAddLine", { msg: "TODO: add input => add Receptors", keys: inputLine, _input: [].concat(_input) })
+		var oldInput = [].concat(_input)
+		var newLetters = inputLine.reduce(function (ret, line) { return ret.concat(line) }, []).filter(p5Utils__ArrayDistinctFilter)
+		_input.push(newLetters)
+		{ // add new receptors if needed
+			var oldLetters = oldInput.reduce(function (ret, line) { return ret.concat(line) }, []).filter(p5Utils__ArrayDistinctFilter)
+			var addLetters = p5Utils__ArrayDiff(newLetters.sort(), oldLetters)
+			DBG && console.log("DBG:NeuronStore:inputAddLine", { msg: "TODO: add input => add Receptors.2", newLetters, oldLetters, addLetters })
+			if (!addLetters.length) return;
+
+			var totalReceptors = oldLetters.length + addLetters.length
+			var oldFirstCx = Math.ceil(_config.ui_output_width / (oldLetters.length + 1));
+			var newFirstCx = Math.ceil(_config.ui_output_width / (totalReceptors + 1));
+			var moveXFactor = Number((newFirstCx / oldFirstCx).toFixed(2))
+			DBG && console.log("DBG:NeuronStore:inputAddLine", { msg: "TODO: add input => add Receptors.3", moveXFactor, oldFirstCx, newFirstCx })
+			var allLetters = [].concat(oldLetters, addLetters)
+			_state.receptor = _state.receptor.map(function (receptor) {
+				receptor.ui.cx = receptor.ui.cx * moveXFactor
+				return receptor;
+			})
+			_state.neuron = _state.neuron.map(function (neuron) {
+				neuron.ui.cx = neuron.ui.cx * moveXFactor
+				return neuron;
+			})
+			addLetters.forEach(function (letter, idx) {
+				_state.receptor.push(makeReceptor(letter, idx + oldLetters.length, allLetters))
+			})
+			_state.mapReceptorChar = _state.receptor.map(function (node) { return node.value; });
+		}
+	}
+
+	function readLineFromInput() {
+		if (_state.inputReadPos >= _input.length) return null;
 
-		var char = _inputText.charAt(_state.inputReadPos)
+		var line = _input[_state.inputReadPos]
 		_state.inputReadPos += 1
 
-		return char;
+		return line;
 	}
 
 	function forwardAnim() {
-		DBG1 && console.log("DBG:NeuronStore:forwardAnim:doAnim = '" + (_state.doAnim ? 1 : 0) + "'");
+		DBG && console.log("DBG:NeuronStore:forwardAnim:doAnim = '" + (_state.doAnim ? 1 : 0) + "'");
 		if (!_state.doAnim) return;
 
 		// 1. read from input if its time
@@ -79,27 +172,26 @@ function makeNeuronStore() {
 		// 1. read from input if its time
 		if (0 === _state.animPos % _config.config_read_speed_multiplier) {
 			// read from input = add charge to receptor with given letter
-			var char = readCharFromInput()
-			if (null === char) {
-				DBG1 && console.log("DBG:NeuronStore:forwardAnim:1(readInput): STOP - end of input", { inputLength: _inputText.length, animPos: _state.animPos })
-				_state.doAnim = false
-				_notifySubscribers()
-				return;
-			}
+			var line = readLineFromInput()
+			if (null === line) {
+				DBG && console.log("DBG:NeuronStore:forwardAnim:1(readInput): STOP - end of input / discharge all", { inputLength: _input.length, animPos: _state.animPos })
+			} else {
+				line.forEach(function (label) {
+					var foundReceptorIdx = getReceptorIdx(label)
+					DBG && console.log("DBG:NeuronStore:forwardAnim:1(readInput): label('" + label + "', [" + label.charCodeAt(0) + "])", { animPos: _state.animPos, label, len: _input.length, doAnim: _state.doAnim, foundReceptorIdx })
+					if (-1 === foundReceptorIdx) {
+						DBG && console.warn("BUG: receptor '" + label + "' not found (NeuronStore:forwardAnim:readInput)")
+						_state.doAnim = false
+						_notifySubscribers()
+						return;
+					}
+					this_charge_add(_config.config_charge_receptor_at_input, { type: 'receptor', idx: foundReceptorIdx })
+				})
 
-			var foundReceptorIdx = getReceptorIdx(char)
-			DBG1 && console.log("DBG:NeuronStore:forwardAnim:1(readInput): char('" + char + "', [" + char.charCodeAt(0) + "])", { animPos: _state.animPos, char, len: _inputText.length, doAnim: _state.doAnim, foundReceptorIdx })
-			if (-1 === foundReceptorIdx) {
-				DBG1 && console.warn("BUG: receptor '" + char + "' not found (NeuronStore:forwardAnim:readInput)")
-				_state.doAnim = false
+				_state.animPos += 1
 				_notifySubscribers()
 				return;
 			}
-			chargeAdd(_config.config_charge_receptor_at_input, { type: 'receptor', idx: foundReceptorIdx })
-
-			_state.animPos += 1
-			_notifySubscribers()
-			return;
 		}
 
 
@@ -137,9 +229,9 @@ function makeNeuronStore() {
 					if (null === ret) return { idx: idx, charge: charge }
 					return (charge > ret.charge) ? { idx: idx, charge: charge } : ret;
 				}, null)
-				DBG1 && console.log("TODO:NeuronStore:forwardAnim:2.1(check overcharged)/neuron.1", { overcharged, firstNeuron, secondNeuronWithCharge })
+				DBG && console.log("DBG:NeuronStore:forwardAnim:2.1(check overcharged)/neuron.1", { overcharged, firstNeuron, secondNeuronWithCharge })
 				if (null !== secondNeuronWithCharge) {
-					// is already exists - check doubleConn: { from: [ neuronIdx, neuronIdx ], to: neuronIdx }
+					// is already exists - check doubleConn
 					var existingConn = _state.doubleConn.reduce(function (ret, dblConn) {
 						if (ret) return ret;
 						if (dblConn.from[0] === overcharged.idx && dblConn.from[1] === secondNeuronWithCharge.idx) return dblConn;
@@ -147,23 +239,23 @@ function makeNeuronStore() {
 						return ret;
 					}, null)
 					var toNode = _state.neuron[secondNeuronWithCharge.idx]
-					DBG1 && console.log("TODO:NeuronStore:forwardAnim:2.1(check overcharged)/neuron.2(secondNeuronWithCharge)", { from: firstNeuron.value, to: toNode.value, existingConn })
+					DBG && console.log("DBG:NeuronStore:forwardAnim:2.1(check overcharged)/neuron.2(secondNeuronWithCharge)", { from: firstNeuron.value, to: toNode.value, existingConn })
 					if (existingConn) { // charge Neuron existingConn.to
 						var idxNewNeuron = existingConn.to
-						chargeRemove({ type: 'neuron', idx: overcharged.idx })
-						chargeRemove({ type: 'neuron', idx: secondNeuronWithCharge.idx })
+						this_charge_remove({ type: 'neuron', idx: overcharged.idx })
+						this_charge_remove({ type: 'neuron', idx: secondNeuronWithCharge.idx })
 						switch (_config.config_strategy_overcharge) {
 							case "REMOVE_CHARGE": break;
 							case "LEAVE_HALF_CHARGE": {
-								chargeAdd(overcharged.charge / 2, { type: 'neuron', idx: overcharged.idx })
-								chargeAdd(secondNeuronWithCharge.charge / 2, { type: 'neuron', idx: secondNeuronWithCharge.idx })
+								this_charge_add(overcharged.charge / 2, { type: 'neuron', idx: overcharged.idx })
+								this_charge_add(secondNeuronWithCharge.charge / 2, { type: 'neuron', idx: secondNeuronWithCharge.idx })
 							} break;
 							case "LEAVE_ALMOST_MAX": {
-								chargeAdd(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
-								chargeAdd(getNeuronAlmostMaxCharge(secondNeuronWithCharge.idx), { type: 'neuron', idx: secondNeuronWithCharge.idx })
+								this_charge_add(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
+								this_charge_add(getNeuronAlmostMaxCharge(secondNeuronWithCharge.idx), { type: 'neuron', idx: secondNeuronWithCharge.idx })
 							} break;
 						}
-						chargeAdd(overcharged.charge + secondNeuronWithCharge.charge, { type: 'neuron', idx: existingConn.to })
+						this_charge_add(overcharged.charge + secondNeuronWithCharge.charge, { type: 'neuron', idx: existingConn.to })
 						// _state.animPos += 1
 						// dischargeAll()
 						_notifySubscribers()
@@ -171,33 +263,32 @@ function makeNeuronStore() {
 					} else { // create new
 						var newNeuron = makeNeuronFromTwoNeurons(overcharged.idx, overcharged.charge, secondNeuronWithCharge.idx, secondNeuronWithCharge.charge)
 						if (!newNeuron) {
-							DBG1 && console.log("TODO:NeuronStore:forwardAnim:2(check overcharged)/neuron.2.1(!newNeuron)", { overcharged })
+							DBG && console.log("DBG:NeuronStore:forwardAnim:2(check overcharged)/neuron.2.1(!newNeuron)", { overcharged })
 
 							// _state.animPos += 1
 							dischargeAll()
 							_notifySubscribers()
 							return;
 						} else {
-							_state.neuron.push(newNeuron)
+							var idxNewNeuron = this_neuron_add(newNeuron)
 							updateOuputHeight(newNeuron)
-							var idxNewNeuron = _state.neuron.length - 1
-							_state.connection.push({ fromType: 'neuron', fromIdx: overcharged.idx, toIdx: idxNewNeuron, timesCreated: 1 })
-							_state.connection.push({ fromType: 'neuron', fromIdx: secondNeuronWithCharge.idx, toIdx: idxNewNeuron, timesCreated: 1 })
-							_state.doubleConn.push({ from: [overcharged.idx, secondNeuronWithCharge.idx], to: idxNewNeuron, connIdx: [_state.connection.length - 2, _state.connection.length - 1] })
-							chargeRemove({ type: 'neuron', idx: overcharged.idx })
-							chargeRemove({ type: 'neuron', idx: secondNeuronWithCharge.idx })
+							var conn1Idx = this_connection_add({ fromType: 'neuron', from: overcharged.idx, to: idxNewNeuron, timesCreated: 1 })
+							var conn2Idx = this_connection_add({ fromType: 'neuron', from: secondNeuronWithCharge.idx, to: idxNewNeuron, timesCreated: 1 })
+							var dblConnIdx = this_doubleConn_add({ from: [overcharged.idx, secondNeuronWithCharge.idx], to: idxNewNeuron, fromConn: [conn1Idx, conn2Idx] })
+							this_charge_remove({ type: 'neuron', idx: overcharged.idx })
+							this_charge_remove({ type: 'neuron', idx: secondNeuronWithCharge.idx })
+							var firstCharge = getNeuronAlmostMaxCharge(overcharged.idx)
+							var secondCharge = getNeuronAlmostMaxCharge(secondNeuronWithCharge.idx)
+							this_charge_add((firstCharge + secondCharge) / 2, { type: 'doubleConn', idx: dblConnIdx, connCharge: [firstCharge, secondCharge] })
 							switch (_config.config_strategy_overcharge) {
 								case "REMOVE_CHARGE": break;
 								case "LEAVE_HALF_CHARGE": {
-									chargeAdd(overcharged.charge / 2, { type: 'neuron', idx: overcharged.idx })
-									chargeAdd(secondNeuronWithCharge.charge / 2, { type: 'neuron', idx: secondNeuronWithCharge.idx })
+									this_charge_add(overcharged.charge / 2, { type: 'neuron', idx: overcharged.idx })
+									this_charge_add(secondNeuronWithCharge.charge / 2, { type: 'neuron', idx: secondNeuronWithCharge.idx })
 								} break;
 								case "LEAVE_ALMOST_MAX": {
-									// chargeAdd(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
-									// chargeAdd(getNeuronAlmostMaxCharge(secondNeuronWithCharge.idx), { type: 'neuron', idx: secondNeuronWithCharge.idx })
-									var fromCharge = getNeuronAlmostMaxCharge(overcharged.idx)
-									var toCharge = getNeuronAlmostMaxCharge(secondNeuronWithCharge.idx)
-									chargeAdd((fromCharge + toCharge) / 2, { type: 'dbl_conn', idx: _state.doubleConn.length - 1, fromCharge: fromCharge, toCharge: toCharge })
+									// this_charge_add(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
+									// this_charge_add(getNeuronAlmostMaxCharge(secondNeuronWithCharge.idx), { type: 'neuron', idx: secondNeuronWithCharge.idx })
 								} break;
 							}
 							// _state.animPos += 1
@@ -215,14 +306,14 @@ function makeNeuronStore() {
 					// TODO: check if already exists! if yes then charge else create new
 					var idxFoundConnection = _state.connection.reduce(function (ret, conn, idx) {
 						if (ret > -1) return ret;
-						if (conn.fromType === 'neuron' && conn.fromIdx === overcharged.idx) return idx;
+						if (conn.fromType === 'neuron' && conn.from === overcharged.idx) return idx;
 						return ret;
 					}, -1)
-					DBG1 && console.log("TODO:NeuronStore:forwardAnim:2(check overcharged)/neuron.2(!secondNeuronWithCharge)", { idxFoundConnection })
+					DBG && console.log("DBG:NeuronStore:forwardAnim:2(check overcharged)/neuron.2(!secondNeuronWithCharge)", { idxFoundConnection })
 					if (-1 === idxFoundConnection) {
 						var newNeuron = makeNeuronFromOneNeuron(overcharged.idx, overcharged.charge)
 						if (!newNeuron) {
-							DBG1 && console.log("TODO:NeuronStore:forwardAnim:2(check overcharged)/neuron.2.1(!newNeuron)", { overcharged })
+							DBG && console.log("DBG:NeuronStore:forwardAnim:2(check overcharged)/neuron.2.1(!newNeuron)", { overcharged })
 
 							// _state.animPos += 1
 							dischargeAll()
@@ -232,15 +323,15 @@ function makeNeuronStore() {
 							_state.neuron.push(newNeuron)
 							updateOuputHeight(newNeuron)
 							var idxNewNeuron = _state.neuron.length - 1
-							_state.connection.push({ fromType: 'neuron', fromIdx: overcharged.idx, toIdx: idxNewNeuron, timesCreated: 1 })
-							chargeRemove({ type: 'neuron', idx: overcharged.idx })
+							this_connection_add({ fromType: 'neuron', from: overcharged.idx, to: idxNewNeuron, timesCreated: 1 })
+							this_charge_remove({ type: 'neuron', idx: overcharged.idx })
 							switch (_config.config_strategy_overcharge) {
 								case "REMOVE_CHARGE": break;
 								case "LEAVE_HALF_CHARGE": {
-									chargeAdd(overcharged.charge / 2, { type: 'neuron', idx: overcharged.idx })
+									this_charge_add(overcharged.charge / 2, { type: 'neuron', idx: overcharged.idx })
 								} break;
 								case "LEAVE_ALMOST_MAX": {
-									chargeAdd(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
+									this_charge_add(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
 								} break;
 							}
 
@@ -250,17 +341,17 @@ function makeNeuronStore() {
 					} else {
 						_state.connection[idxFoundConnection].timesCreated += 1
 						// chargeMove(overcharged.charge, { type: 'receptor', idx: overcharged.idx }, { type: 'connection', idx: idxFoundConnection })
-						chargeRemove({ type: 'neuron', idx: overcharged.idx })
+						this_charge_remove({ type: 'neuron', idx: overcharged.idx })
 						switch (_config.config_strategy_overcharge) {
 							case "REMOVE_CHARGE": break;
 							case "LEAVE_HALF_CHARGE": {
-								chargeAdd(overcharged.charge / 2, { type: 'neuron', idx: overcharged.idx })
+								this_charge_add(overcharged.charge / 2, { type: 'neuron', idx: overcharged.idx })
 							} break;
 							case "LEAVE_ALMOST_MAX": {
-								chargeAdd(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
+								this_charge_add(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
 							} break;
 						}
-						chargeAdd(overcharged.charge, { type: 'connection', idx: idxFoundConnection })
+						this_charge_add(overcharged.charge, { type: 'connection', idx: idxFoundConnection })
 					}
 
 					_notifySubscribers()
@@ -271,82 +362,76 @@ function makeNeuronStore() {
 				// return;
 			}
 
-			if (overchargedReceptorIdx.length > 0) { // TODO: while ?
-				if (1 === overchargedReceptorIdx.length) {
+			if (overchargedReceptorIdx.length > 0) {
+				while (overchargedReceptorIdx.length) {
 					var overcharged = overchargedReceptorIdx.shift()
-					// Restriction: only one connection between nodes
+					// Restriction: only one connection between Receptor and Neuron
 					{
 						var idxFoundConnection = _state.connection.reduce(function (ret, conn, idx) {
 							if (ret > -1) return ret;
-							if (conn.fromType === 'receptor' && conn.fromIdx === overcharged.idx) return idx;
+							if (conn.fromType === 'receptor' && conn.from === overcharged.idx) return idx;
 							return ret;
 						}, -1)
-						DBG1 && console.log("DBG:NeuronStore:forwardAnim:2(check overcharged)/receptor", { idxFoundConnection, from: { type: 'receptor', idx: overcharged.idx }, conn: [].concat(_state.connection) })
+						DBG && console.log("DBG:NeuronStore:forwardAnim:2(check overcharged)/receptor", { idxFoundConnection, from: { type: 'receptor', idx: overcharged.idx }, conn: [].concat(_state.connection) })
 						if (-1 === idxFoundConnection) {
 							var newNeuron = makeNeuronFromOneReceptor(overcharged.idx, overcharged.charge)
 							_state.neuron.push(newNeuron)
 							var idxNewNeuron = _state.neuron.length - 1
-							_state.connection.push({ fromType: 'receptor', fromIdx: overcharged.idx, toIdx: idxNewNeuron, timesCreated: 1 })
-							idxFoundConnection = _state.connection.length - 1
-							chargeRemove({ type: 'receptor', idx: overcharged.idx })
+							idxFoundConnection = this_connection_add({ fromType: 'receptor', from: overcharged.idx, to: idxNewNeuron, timesCreated: 1 })
+							this_charge_remove({ type: 'receptor', idx: overcharged.idx })
 							// switch (_config.config_strategy_overcharge) {
 							//     case "REMOVE_CHARGE": break;
 							//     case "LEAVE_HALF_CHARGE": {
-							//         chargeAdd(overcharged.charge / 2, { type: 'receptor', idx: overcharged.idx })
+							//         this_charge_add(overcharged.charge / 2, { type: 'receptor', idx: overcharged.idx })
 							//     } break;
 							//     case "LEAVE_ALMOST_MAX": {
-							//         chargeAdd(getReceptorAlmostMaxCharge(overcharged.idx), { type: 'receptor', idx: overcharged.idx })
+							//         this_charge_add(getReceptorAlmostMaxCharge(overcharged.idx), { type: 'receptor', idx: overcharged.idx })
 							//     } break;
 							// }
-							chargeAdd(overcharged.charge, { type: 'connection', idx: idxFoundConnection })
+							this_charge_add(overcharged.charge, { type: 'connection', idx: idxFoundConnection })
 						} else {
 							_state.connection[idxFoundConnection].timesCreated += 1
 							// chargeMove(overcharged.charge, { type: 'receptor', idx: overcharged.idx }, { type: 'connection', idx: idxFoundConnection })
-							chargeRemove({ type: 'receptor', idx: overcharged.idx })
+							this_charge_remove({ type: 'receptor', idx: overcharged.idx })
 							// switch (_config.config_strategy_overcharge) {
 							//     case "REMOVE_CHARGE": break;
 							//     case "LEAVE_HALF_CHARGE": {
-							//         chargeAdd(overcharged.charge / 2, { type: 'receptor', idx: overcharged.idx })
+							//         this_charge_add(overcharged.charge / 2, { type: 'receptor', idx: overcharged.idx })
 							//     } break;
 							//     case "LEAVE_ALMOST_MAX": {
-							//         chargeAdd(getReceptorAlmostMaxCharge(overcharged.idx), { type: 'receptor', idx: overcharged.idx })
+							//         this_charge_add(getReceptorAlmostMaxCharge(overcharged.idx), { type: 'receptor', idx: overcharged.idx })
 							//     } break;
 							// }
-							chargeAdd(overcharged.charge, { type: 'connection', idx: idxFoundConnection })
+							this_charge_add(overcharged.charge, { type: 'connection', idx: idxFoundConnection })
 						}
 					}
-
-					// _state.animPos += 1
-					// dischargeAll()
-					_notifySubscribers()
-					return;
-				} else {
-					DBG1 && console.log("TODO:NeuronStore:forwardAnim:2(check overcharged)/receptor", { TODO: "more then 1 receptor overcharged!" })
 				}
+				_notifySubscribers()
+				return;
 			}
 
-			// DBG1 && console.log("DBG:NeuronStore:forwardAnim:2(check overcharged)", { overchargedNeuronIdx, overchargedReceptorIdx })
+			// DBG && console.log("DBG:NeuronStore:forwardAnim:2(check overcharged)", { overchargedNeuronIdx, overchargedReceptorIdx })
 		}
 
 		{ // 3. move all charges (Receptor -> Connection, Connection -> Neuron, Neuron -> Connection)
-			DBG1 && console.log("TODO:NeuronStore:forwardAnim:3(move all charges)", { charge: [].concat(_state.charge) })
+			DBG && console.log("TODO:NeuronStore:forwardAnim:3(move all charges)", { charge: [].concat(_state.charge) })
 			var isChargeTypeConnection = function (charge) {
 				if ("connection" === charge.nodeType) return true;
-				if ("dbl_conn" === charge.nodeType) return true;
+				if ("doubleConn" === charge.nodeType) return true;
 				return false;
 			}
 			var toAddChargeNeuronIdxList = _state.charge.filter(isChargeTypeConnection).map(function (charge) {
-				DBG1 && console.log("TODO:NeuronStore:forwardAnim:3(move all charges).LOOP", { charge })
+				DBG && console.log("TODO:NeuronStore:forwardAnim:3(move all charges).LOOP", { charge })
 				switch (charge.nodeType) {
-					case "connection": return { charge: charge.charge, toIdx: _state.connection[charge.nodeIdx].toIdx };
-					case "dbl_conn": return { charge: charge.charge, toIdx: _state.doubleConn[charge.nodeIdx].to };
+					case "connection": return { charge: charge.value, to: _state.connection[charge.node].to };
+					case "doubleConn": return { charge: charge.value, to: _state.doubleConn[charge.node].to };
 				}
 				return null;
 			})
 
 			_state.charge = _state.charge.filter(function (charge) { return !isChargeTypeConnection(charge); })
 			toAddChargeNeuronIdxList.forEach(function (toAddCharge) {
-				chargeAdd(toAddCharge.charge, { type: 'neuron', idx: toAddCharge.toIdx })
+				this_charge_add(toAddCharge.charge, { type: 'neuron', idx: toAddCharge.to })
 			})
 		}
 		{ // 4. discharge all
@@ -359,27 +444,32 @@ function makeNeuronStore() {
 	function dischargeAll() {
 		_state.charge = _state.charge.map(function (charge) {
 			return Object.assign({}, charge, {
-				charge: Math.max(0, charge.charge - _config.config_discharge_per_tick),
+				value: Math.max(0, charge.value - _config.config_discharge_per_tick),
 			})
 		}).filter(function (charge) {
-			return charge.charge > 0;
+			return charge.value > 0;
 		})
 	}
 	function makeNeuronFromTwoNeurons(fromNeuronIdx, fromNeuronCharge, toNeuronIdx, toNeuronCharge) {
 		var fromNode = _state.neuron[fromNeuronIdx]
 		var toNode = _state.neuron[toNeuronIdx]
 		var maxCharge = getNewNeuronMaxCharge(fromNode)
-		DBG1 && console.log("DBG:NeuronStore:makeNeuronFromTwoNeurons", { fromNode, toNode, maxCharge })
+		DBG && console.log("DBG:NeuronStore:makeNeuronFromTwoNeurons", { fromNode, toNode, maxCharge })
 		if (maxCharge < _config.config_discharge_per_tick) return null;
 
 		var uiNewNodePos = { cx: 0, cy: 0 }
 		{ // closer to node with more charge (first)
 			var xDiff = Math.abs(fromNode.ui.cx - toNode.ui.cx)
 			var isFirstOnRight = (fromNode.ui.cx > toNode.ui.cx)
-			// fromNeuronCharge + toNeuronCharge -- xDiff
-			// toNeuronCharge -- x ==> x = toNeuronCharge * xDiff / (fromNeuronCharge + toNeuronCharge)
-			var xToFirst = (toNeuronCharge * xDiff) / (fromNeuronCharge + toNeuronCharge)
-			uiNewNodePos.cx = fromNode.ui.cx + (isFirstOnRight ? -1 : 1) * xToFirst
+			if (0 === xDiff) {
+				uiNewNodePos.cx = fromNode.ui.cx + (isFirstOnRight ? -1 : 1) * 20
+			} else {
+				// fromNeuronCharge + toNeuronCharge -- xDiff
+				// toNeuronCharge -- x ==> x = toNeuronCharge * xDiff / (fromNeuronCharge + toNeuronCharge)
+				var xToFirst = (toNeuronCharge * xDiff) / (fromNeuronCharge + toNeuronCharge)
+				xToFirst = Math.max(40, xToFirst)
+				uiNewNodePos.cx = fromNode.ui.cx + (isFirstOnRight ? -1 : 1) * xToFirst
+			}
 		}
 		uiNewNodePos.cy = (fromNode.ui.cy != toNode.ui.cy)
 			? Math.max(fromNode.ui.cy, toNode.ui.cy) + 10 + _config.ui_space_y / 2
@@ -390,15 +480,14 @@ function makeNeuronStore() {
 			if (uiNewNodePos.cx > neuron.ui.cx + 10) return ret;
 			if (ret < neuron.ui.cy - 10) return ret;
 			if (ret > neuron.ui.cy + 10) return ret;
-			return ret + 10 + _config.ui_space_y;
+			return ret + 10 + _config.ui_space_y / 2;
 		}, uiNewNodePos.cy)
 
-		DBG1 && console.log("DBG:NeuronStore:makeNeuronFromTwoNeurons", { uiNewNodePos, fromNode, toNode, newNodeMaxCharge: getNewNeuronMaxCharge(fromNode) })
+		DBG && console.log("DBG:NeuronStore:makeNeuronFromTwoNeurons", { uiNewNodePos, fromNode, toNode, newNodeMaxCharge: getNewNeuronMaxCharge(fromNode) })
 		// var value = '' + (_state.neuron.length + 1);
-		var value = [fromNode.value, toNode.value].join('')
+		var value = [toNode.value, fromNode.value].join('')
 		return Object.assign({}, DEFAULT_NEURON, {
 			value: value,
-			charge: 0,
 			maxCharge: getNewNeuronMaxCharge(fromNode),
 			uiShape: "ellipse",
 			ui: Object.assign({}, uiNewNodePos, {
@@ -412,10 +501,9 @@ function makeNeuronStore() {
 		// var value = '' + (_state.neuron.length + 1);
 		var value = [sourceNode.value, ""].join('') // TODO: same name what Receptor
 
-		DBG1 && console.log("DBG:NeuronStore:makeNeuronFromOneReceptor", { sourceNode, newNodeMaxCharge: getNewNeuronMaxCharge(sourceNode) })
+		DBG && console.log("DBG:NeuronStore:makeNeuronFromOneReceptor", { sourceNode, newNodeMaxCharge: getNewNeuronMaxCharge(sourceNode) })
 		return Object.assign({}, DEFAULT_NEURON, {
 			value: value,
-			charge: 0,
 			maxCharge: getNewNeuronMaxCharge(sourceNode),
 			uiShape: "ellipse",
 			ui: {
@@ -433,10 +521,9 @@ function makeNeuronStore() {
 		var maxCharge = getNewNeuronMaxCharge(sourceNode)
 		if (maxCharge < _config.config_discharge_per_tick) return null;
 
-		DBG1 && console.log("DBG:NeuronStore:makeNeuronFromOneNeuron", { sourceNode, newNodeMaxCharge: maxCharge })
+		DBG && console.log("DBG:NeuronStore:makeNeuronFromOneNeuron", { sourceNode, newNodeMaxCharge: maxCharge })
 		return Object.assign({}, DEFAULT_NEURON, {
 			value: value,
-			charge: 0,
 			maxCharge: maxCharge,
 			uiShape: "ellipse",
 			ui: {
@@ -451,30 +538,52 @@ function makeNeuronStore() {
 		// return sourceNode.maxCharge * _config.config_discharge_max_in_new_neuron_from_one;
 		return sourceNode.maxCharge - _config.config_discharge_per_tick;
 	}
-	function chargeAdd(charge, to) {
+
+	function this_neuron_add(neuron) {
+		_state.neuron.push(neuron)
+
+		return _state.neuron.length - 1;
+	}
+
+	function this_doubleConn_add(doubleConn) {
+		_state.doubleConn.push(doubleConn)
+
+		return _state.doubleConn.length - 1;
+	}
+
+	function this_connection_add(conn) {
+		_state.connection.push(conn)
+
+		return _state.connection.length - 1;
+	}
+
+	function this_charge_add(charge, to) {
 		var foundChargeIdx = _state.charge.reduce(function (ret, charge, idx) {
-			if (charge.nodeType === to.type && charge.nodeIdx === to.idx) return idx;
+			if (charge.nodeType === to.type && charge.node === to.idx) return idx;
 			return ret;
 		}, -1)
-		DBG1 && console.log("DBG:NeuronStore:chargeAdd", { charge, to, foundChargeIdx })
+		DBG && console.log("DBG:NeuronStore:this_charge_add.1", { charge, to, foundChargeIdx })
+		var chargeIdx = foundChargeIdx
 		if (-1 === foundChargeIdx) {
-			_state.charge.push({ charge: charge, nodeType: to.type, nodeIdx: to.idx })
-			foundChargeIdx = _state.charge.length - 1
+			_state.charge.push({ value: charge, nodeType: to.type, node: to.idx })
+			chargeIdx = _state.charge.length - 1
 		} else {
-			_state.charge[foundChargeIdx].charge += charge
+			_state.charge[chargeIdx].value += charge
 		}
-		if ('dbl_conn' === to.type) {
-			if (!_state.charge[foundChargeIdx].hasOwnProperty('fromCharge')) _state.charge[foundChargeIdx].fromCharge = 0
-			if (!_state.charge[foundChargeIdx].hasOwnProperty('toCharge')) _state.charge[foundChargeIdx].toCharge = 0
-			_state.charge[foundChargeIdx].fromCharge += to.fromCharge
-			_state.charge[foundChargeIdx].toCharge += to.toCharge
+		if ('doubleConn' === to.type) { // connCharge: [ firstCharge, secondCharge ]
+			if (-1 === foundChargeIdx) {
+				_state.charge[chargeIdx].connCharge = to.connCharge
+			} else {
+				_state.charge[chargeIdx].connCharge[0] += to.connCharge[0]
+				_state.charge[chargeIdx].connCharge[1] += to.connCharge[1]
+			}
 		}
-		DBG1 && console.log("DBG:NeuronStore:chargeAdd.2", { state_charge: [].concat(_state.charge) })
-		return foundChargeIdx;
+		DBG && console.log("DBG:NeuronStore:this_charge_add.2", { state_charge: [].concat(_state.charge) })
+		return chargeIdx;
 	}
-	function chargeRemove(from) {
-		_state.charge = _state.charge.filter(function (item) {
-			return !(from.type === item.nodeType && from.idx === item.nodeIdx);
+	function this_charge_remove(from) {
+		_state.charge = _state.charge.filter(function (charge) {
+			return !(from.type === charge.nodeType && from.idx === charge.node);
 		})
 	}
 
@@ -485,13 +594,15 @@ function makeNeuronStore() {
 	}
 
 	function dispatch(payload) {
-		DBG1 && console.log("DBG:NeuronStore:dispatch('" + payload.type + "')", payload);
+		DBG && console.log("DBG:NeuronStore:dispatch('" + payload.type + "')", payload);
 		switch (payload.type) {
-			case 'INIT': stateReset(payload.inputText, payload.config); startAnimation(); break;
+			case 'INIT': stateReset(payload.input, payload.config); startAnimation(); break;
+			case 'INPUT_UPDATED': inputUpdated(payload.input); break;
+			case 'INPUT_ADD_LINE': inputAddLine(payload.input); break;
 			case 'PAUSE': _state.doAnim = false; _notifySubscribers(); break;
 			case 'PLAY': _state.doAnim = true; break;
 			default: {
-				DBG1 && console.warn("Not implemented dispatch action type '" + payload.type + "'")
+				DBG && console.warn("Not implemented dispatch action type '" + payload.type + "'")
 			}
 		}
 	}
@@ -535,23 +646,23 @@ function makeNeuronStore() {
 	}
 	function getReceptorCharge(idx) {
 		return _state.charge.filter(function (charge) {
-			return ('receptor' === charge.nodeType && idx === charge.nodeIdx);
+			return ('receptor' === charge.nodeType && idx === charge.node);
 		}).reduce(function (ret, charge) {
-			return ret + charge.charge;
+			return ret + charge.value;
 		}, 0)
 	}
 	function getNeuronCharge(idx) {
 		return _state.charge.filter(function (charge) {
-			return ('neuron' === charge.nodeType && idx === charge.nodeIdx);
+			return ('neuron' === charge.nodeType && idx === charge.node);
 		}).reduce(function (ret, charge) {
-			return ret + charge.charge;
+			return ret + charge.value;
 		}, 0)
 	}
 	function getReceptorAlmostMaxCharge(idx) {
 		return _state.receptor[idx].maxCharge - _config.config_discharge_per_tick;
 	}
 	function getNeuronAlmostMaxCharge(idx) {
-		DBG1 && console.log("DBG:getNeuronAlmostMaxCharge(" + idx + ")", { charge: _state.neuron[idx].maxCharge - _config.config_discharge_per_tick })
+		DBG && console.log("DBG:getNeuronAlmostMaxCharge(" + idx + ")", { charge: _state.neuron[idx].maxCharge - _config.config_discharge_per_tick })
 		return _state.neuron[idx].maxCharge - _config.config_discharge_per_tick;
 	}
 
@@ -559,10 +670,10 @@ function makeNeuronStore() {
 		var receptor = [].concat(_state.receptor);
 		var neuron = [].concat(_state.neuron);
 		_state.charge.forEach(function (charge) {
-			// _state.charge: [ { charge: number, nodeType: (receptor|neuron), nodeIdx: int }, ...  ] groupd by node (type/idx)
+			// _state.charge: [ { value: number, nodeType: (receptor|neuron), node: int }, ...  ] groupd by node (type/idx)
 			switch (charge.nodeType) {
-				case 'receptor': receptor[charge.nodeIdx].charge = charge.charge; break;
-				case 'neuron': neuron[charge.nodeIdx].charge = charge.charge; break;
+				case 'receptor': receptor[charge.node].charge = charge.value; break;
+				case 'neuron': neuron[charge.node].charge = charge.value; break;
 			}
 		})
 
@@ -580,7 +691,7 @@ function makeNeuronStore() {
 			.concat(
 				neuron.map(getNodeInfoFunction('neuron'))
 			)
-			;
+		;
 	}
 
 	return {
@@ -599,9 +710,9 @@ function makeNeuronStore() {
 		getListCharge: function () { return _state.charge; },
 		getReceptorCharge: getReceptorCharge,
 		getNeuronCharge: getNeuronCharge,
+		getInput: function () { return [].concat(_input); },
 	}
 }
 
-
-global[MAKE_STORE_FUNCTION_NAME] = makeNeuronStore
 // export default makeNeuronStore
+global[MAKE_STORE_FUNCTION_NAME] = makeNeuronStore