Piotr Labudda 6 năm trước cách đây
mục cha
commit
8009577513

+ 17 - 12
SE/se-lib/Route/Test/Neuron.php

@@ -11,19 +11,24 @@ class Route_Test_Neuron extends RouteBase {// TODO: UrlActionBase @see Route_Url
         echo UI::h('div', [ 'id' => "p5widget-test-neuron" ]);
         // echo UI::h('script', ['src'=>"https://d3js.org/d3.v5.min.js", 'type'=>"text/javascript"]);
         echo UI::h('script', ['src'=>"static/vendor.js?v=71baa97d", 'type'=>"text/javascript"]);
-        UI::inlineJS(__FILE__ . '.view.js', [
+        UI::inlineJS(__FILE__ . '.makeNeuronStore.js', [
+            'MAKE_STORE_FUNCTION_NAME' => "makeNeuronStore",
+        ]);
+        UI::inlineJS(__FILE__ . '.NeuronView.js', [
+            'MAKE_STORE_FUNCTION_NAME' => "makeNeuronStore",
             'HTML_ID' => "p5widget-test-neuron",
-            'INITIAL_DATA' => implode("\n", [
-                "A",
-                "AAA",
-                "ABC",
-                "AAB",
-                "ACCCB",
-                "CCC",
-                "ABCDE",
-                "AA",
-                "BB",
-            ]),
+            'INITIAL_DATA' => "AABBBBAABBAABB",
+            // 'INITIAL_DATA' => implode("\n", [
+            //     "A",
+            //     "AAA",
+            //     "ABC",
+            //     "AAB",
+            //     "ACCCB",
+            //     "CCC",
+            //     "ABCDE",
+            //     "AA",
+            //     "BB",
+            // ]),
         ]);
         throw new Exception("TODO");
     }

+ 653 - 0
SE/se-lib/Route/Test/Neuron.php.NeuronView.js

@@ -0,0 +1,653 @@
+var createReactClass = window.p5VendorJs.createReactClass;
+var h = window.p5VendorJs.React.createElement;
+var ReactDOM = window.p5VendorJs.ReactDOM;
+// var AsyncTypeahead = window.p5VendorJs.AsyncTypeahead;
+// var swal = window.swal;
+if (!HTML_ID) throw "Missing HTML_ID";
+if (!MAKE_STORE_FUNCTION_NAME) throw "Missing MAKE_STORE_FUNCTION_NAME";
+// if (!d3) throw "Missing d3 (https://d3js.org/d3.v5.min.js)";
+var DBG = DBG || false;
+var DBG1 = true;
+
+// Receptor is special Neuron which reads input (letters in this case)
+//  Receptor.ui: { cx, cy, r } // circle
+// Neuron: { charge, label, draw, source }
+//  Neuron.ui: { cx, cy, rx, ry } // eclipse
+//  Neuron.source: [ { neuron: Neuron | Receptor, atCharge }, ... ]
+
+// Receptor.neighbours: [] list of other receptors with max(receptors.charge)
+
+// makeNeuron cases:
+// stream: AAAAA and only A has charge then create Neuron over A and discharge and lower max charge
+// stream: AAAAA and receptor B has charge then create Neuron between A and B and discharge (closer to receptor with higher charge)
+
+var DEFAULT_CONFIG = {
+	ui_output_width: 1000,
+	ui_output_height: 300,
+	ui_max_receptor_r: 12,
+	ui_space_y: 30,
+	config_anim_speed: 500,
+	config_read_speed_multiplier: 4,
+	config_charge_receptor_at_input: 1,
+	config_max_receptor_charge: 1,
+	config_max_neuron_charge: 1.5,
+	config_discharge_per_tick: 0.1,
+	config_discharge_max_in_new_neuron_from_one: 1, // 0.7
+	config_strategy_overcharge: "LEAVE_ALMOST_MAX", // "REMOVE_CHARGE" | "LEAVE_HALF_CHARGE" | "LEAVE_ALMOST_MAX"
+}
+var DEFAULT_NEURON = {
+	value: '',
+	charge: 0,
+	maxCharge: DEFAULT_CONFIG.config_max_neuron_charge,
+	uiShape: "circle",
+	ui: { cx: 0, cy: 0 },
+	source: [],
+	next: [],
+}
+
+function LOG__render(listLogEntries) {
+	// console.log("DBG", listLogEntries)
+	return h('details', { open: false, style: { margin: "12px 0", border: "1px solid #aaa", borderRadius: "6px", padding: "12px", backgroundColor: "#eee", } }, [
+		h('summary', { style: { cursor: "pointer" } }, "Log"),
+		h('div', { style: { border: "1px solid #eee", backgroundColor: "#fff" } }, [
+			h('table', { className: "table table-condensed table-bordered table-hover" }, [
+				h('thead', {}, LOG__renderTheadLog(listLogEntries)),
+				h('tbody', {}, LOG__renderTbodyLog(listLogEntries)),
+			]),
+		]),
+	]);
+}
+function LOG__renderTheadLog(listLogEntries) {
+	if (!listLogEntries.length) return null;
+	var lastLog = listLogEntries[listLogEntries.length - 1]
+	return [
+		h('tr', {},
+			[h('th', {}, "Lp.")]
+				.concat(
+					lastLog.map(function (nodeInfo) {
+						return h('th', {
+							style: { color: (nodeInfo.type === 'receptor') ? "orange" : "blue" },
+						}, "'" + nodeInfo.value + "'");
+					})
+				)
+		)
+	];
+}
+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)
+				)
+		)
+	})
+}
+function LOG__renderCellNodeChargeLog(nodeInfo) {
+	if (null === nodeInfo) return null;
+	return h('td', {
+		style: {
+			color: (nodeInfo.charge > 0.1) ? "#000" : "#aaa",
+			backgroundColor: (nodeInfo.charge > 0) ? ((nodeInfo.type === 'receptor') ? "#fa0" : "#00d0ff") : "none",
+		},
+	}, nodeInfo.charge.toFixed(2));
+}
+
+
+
+var NeuronView = createReactClass({
+	_inputNode: null,
+	_outputNode: null,
+	_receptor: [],
+	_neuron: [],
+	_store: null,
+	_log: [],
+	getInitialState: function () {
+		return Object.assign({}, DEFAULT_CONFIG, this.getStateFromStore(), {
+			inputText: this.props.initialData || '', // TODO: INITIAL_DATA
+
+			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 })
+	},
+	componentWillUnmount: function () {
+		if (this._unsubscribe) this._unsubscribe()
+	},
+	storeUpdated: function () {
+		DBG && console.log('DBG:NeuronView:storeUpdated');
+		this._log.push(this.props.store.getNodesChargeState())
+		this.setState(this.getStateFromStore())
+	},
+	getStateFromStore: function () {
+		var state = this.props.store.getState();
+
+		return {
+			doAnim: state.doAnim,
+			animPos: state.animPos,
+			inputReadPos: state.inputReadPos,
+			uiOutputHeight: state.uiOutputHeight,
+		};
+	},
+
+	handleChangeInput: function (event) {
+		this.setState({
+			inputText: event.target.value,
+		})
+	},
+	setInputRet: function (reactEl) { this._inputNode = reactEl; },
+	setOutputRet: function (reactEl) { this._outputNode = reactEl; },
+	handleReset: function (event) {
+		event.preventDefault();
+		DBG && console.log('DBG:NeuronView:handleReset...');
+		this.props.store.dispatch({ type: 'INIT', inputText: this.state.inputText, config: this.state })
+	},
+	handlePause: function (event) {
+		event.preventDefault();
+		this.props.store.dispatch({ type: 'PAUSE' })
+	},
+	handlePlay: function (event) {
+		event.preventDefault();
+		this.props.store.dispatch({ type: 'PLAY' })
+	},
+	getReceptor: function (char) {
+		var receptor = this._receptor.filter(function (receptor) {
+			return (char == receptor.value);
+		});
+		if (!receptor.length) throw "BUG: no input receptor found for char '" + char + "'"; // TODO: create on demand?
+		return receptor[0];
+	},
+	selectReceptor: function (receptorIdx) {
+		this.setState({ selectedReceptorIdx: receptorIdx })
+	},
+	handleClickReceptor: function (event) {
+		var data_receptor_idx = event.target.getAttribute('data_receptor_idx')
+		DBG && console.log('DBG:NeuronView:handleClickReceptor', { data_receptor_idx, target: event.target });
+		this.selectReceptor(data_receptor_idx)
+	},
+
+	// renderConnections: function (neuron, idx) {
+	//     DBG && console.log('DBG:NeuronView:renderConnections', { neuron, p1: neuron.source[0].ui, p2: (neuron.source.length > 1) ? neuron.source[1].ui : null });
+	//     return h('g', {}, [
+	//         h('line', {
+	//             x1: neuron.source[0].ui.cx, y1: neuron.source[0].ui.cy,
+	//             x2: neuron.ui.cx, y2: neuron.ui.cy,
+	//             style: {
+	//                 stroke: "#46b8da",
+	//                 strokeWidth: "2"
+	//             },
+	//         }),
+	//         (neuron.source.length > 1)
+	//         ?   h('line', {
+	//                 x1: neuron.source[1].ui.cx, y1: neuron.source[1].ui.cy,
+	//                 x2: neuron.ui.cx, y2: neuron.ui.cy,
+	//                 style: {
+	//                     stroke: "#46b8da",
+	//                     strokeWidth: "2"
+	//                 },
+	//             })
+	//         : null,
+	//     ]);
+	// },
+	renderConnection: function (conn, idx) {
+		var uiPos = this.getConnectionUIPos(conn)
+		DBG && console.log('DBG:NeuronView:renderConnection', { conn, uiPos });
+
+		return h('g', { p5_node_id: "conn-idx-" + idx }, [
+			h('line', Object.assign({}, uiPos, {
+				style: {
+					stroke: "#46b8da",
+					strokeWidth: "2"
+				},
+			})),
+		]);
+
+		// fromIdx: 0
+		// fromType: "receptor"
+		// timesCreated: 1
+		// toIdx: 2
+	},
+	getConnectionUIPos: function (conn) { // @return { x1, y1, x2, y2 }
+		// fromIdx: 0
+		// fromType: "receptor"
+		// 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);
+			default: {
+				DBG && console.warn("Not implemented render charge type '" + conn.fromType + "'", { conn })
+				return null;
+			}
+		}
+	},
+	getConnectionUIPosFromReceptor: function (fromIdx, toIdx) { // @return { x1, y1, x2, y2 }
+		var sourceNode = this.props.store.getReceptor(fromIdx)
+		var destNode = this.props.store.getNeuron(toIdx)
+		DBG && console.log('DBG:NeuronView:getConnectionUIPosFromReceptor', { fromIdx, toIdx, sourceNode: sourceNode, destNode: destNode });
+		if (!sourceNode || !destNode) {
+			DBG && console.warn("Missing source or dest node at renderConnection", { fromIdx, toIdx })
+			return null;
+		}
+
+		return {
+			x1: sourceNode.ui.cx, y1: sourceNode.ui.cy,
+			x2: destNode.ui.cx, y2: destNode.ui.cy,
+		};
+	},
+	getConnectionUIPosFromNeuron: function (fromIdx, toIdx) { // @return { x1, y1, x2, y2 }
+		var sourceNode = this.props.store.getNeuron(fromIdx)
+		var destNode = this.props.store.getNeuron(toIdx)
+		DBG && console.log('DBG:NeuronView:getConnectionUIPosFromNeuron', { fromIdx, toIdx, sourceNode: sourceNode, destNode: destNode });
+		if (!sourceNode || !destNode) {
+			DBG && console.warn("Missing source or dest node at getConnectionUIPosFromNeuron", { fromIdx, toIdx })
+			return null;
+		}
+
+		return {
+			x1: sourceNode.ui.cx, y1: sourceNode.ui.cy,
+			x2: destNode.ui.cx, y2: destNode.ui.cy,
+		};
+	},
+	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);
+			default: {
+				DBG && console.warn("Not implemented render charge type '" + charge.nodeType + "'", { charge, idx })
+				return null;
+			}
+		}
+	},
+	renderChargeOnReceptor: function (idx, charge, chargeIdx) {
+		var sourceNode = this.props.store.getReceptor(idx)
+		if (!sourceNode) {
+			DBG && console.warn("Missing source node at renderChargeOnReceptor", { charge, idx })
+			return null;
+		}
+		var uiNodePos = sourceNode.ui
+		var uiPos = {
+			cx: uiNodePos.cx,
+			cy: uiNodePos.cy,
+		}
+		DBG && console.log("DBG:NeuronView:renderChargeOnReceptor", { uiNodePos, uiPos })
+
+		return h('g', { p5_node_id: "charge-idx-" + chargeIdx, p5_node_type: "charge-on-receptor" }, [
+			h('circle', Object.assign({}, uiPos, {
+				r: sourceNode.ui.r + 3,
+				stroke: "#00d0ff",
+				fill: "none",
+				strokeWidth: "3px",
+			}))
+		]);
+	},
+	renderChargeOnNeuron: function (idx, charge, chargeIdx) {
+		var sourceNode = this.props.store.getNeuron(idx)
+		if (!sourceNode) {
+			DBG && console.warn("Missing source node at renderChargeOnNeuron", { charge, idx })
+			return null;
+		}
+		var uiNodePos = sourceNode.ui
+		var uiPos = {
+			cx: uiNodePos.cx,
+			cy: uiNodePos.cy,
+		}
+		DBG && console.log("DBG:NeuronView:renderChargeOnNeuron", { uiNodePos, uiPos })
+
+		return h('g', { p5_node_id: "charge-idx-" + chargeIdx, p5_node_type: "charge-on-neuron" }, [
+			h('circle', Object.assign({}, uiPos, {
+				r: sourceNode.ui.ry + 3,
+				stroke: "#00d0ff",
+				fill: "none",
+				strokeWidth: "3px",
+			}))
+		]);
+	},
+	renderChargeOnConnection: function (idx, charge, chargeIdx) {
+		var sourceNode = this.props.store.getConnection(idx)
+		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)
+		// var chargePr = (100 * chargeValue) / this.state.config_max_neuron_charge;
+		var chargePr = (100 * chargeValue) / neuron.maxCharge;
+		var fontColor = (chargePr < 30) ? "#000" : "#fff";
+		return h('g', { p5_node_id: "neuron-idx-" + idx, p5_max_charge: neuron.maxCharge }, [
+			h(neuron.uiShape, Object.assign({}, neuron.ui, {
+				stroke: "#46b8da", strokeWidth: 1,
+				fill: (chargePr > 100) ? "#f00" : "hsl(194, 67%, " + (100 - chargePr) + "%)",
+				// data_neuron_idx: idx,
+				// onClick: this.handleClickNeuron,
+				style: { cursor: "pointer" },
+			}), [
+					h('title', {}, this.viewNeuronTitle(neuron.value)),
+				]),
+			h('text', {
+				x: neuron.ui.cx, y: neuron.ui.cy + 1, fill: fontColor,
+				dominantBaseline: "middle", textAnchor: "middle",
+				style: { fontSize: "10px", cursor: "pointer" },
+				// data_neuron_idx: idx,
+				// onClick: this.handleClickNeuron,
+			}, chargeValue.toFixed(1)),
+		]);
+	},
+	viewNeuronValue: function (value) {
+		return value
+			.replace(new RegExp("\n", "g"), "\\n")
+			.replace(new RegExp("\t", "g"), "\\t")
+			.replace(new RegExp(" ", "g"), "_")
+			;
+	},
+	viewNeuronTitle: function (value) {
+		return this.viewNeuronValue(value);
+	},
+	renderReceptor: function (receptor, idx) {
+		// orange: hsl(40, 100%, 50%) --> hsl(0, 100%, 50%) -> red (overcharged)
+		// var totalReceptors = this.state.receptor.length;
+		// var cx = Math.ceil(this.state.ui_output_width / ( totalReceptors + 1 ));
+		// var r = Math.min(Math.ceil(this.state.ui_output_width / ( totalReceptors + 10 ) / 2), this.state.ui_max_receptor_r);
+		// DBG && console.log('DBG:NeuronView:renderReceptor', { receptor, idx, totalReceptors, r });
+		// var xCenter = cx + cx * idx - r;
+		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 fontColor = "#fff" // (chargePr < 50) ? "#000" : "#fff";
+		// this.state.config_max_receptor_charge = 100%
+		// charge = x% (max 100)
+		return h('g', { p5_node_id: "receptor-idx-" + idx }, [
+			h(receptor.uiShape, Object.assign({}, receptor.ui, {
+				stroke: "#46b8da", strokeWidth: 1,
+				fill: shapeColor,
+				data_receptor_idx: idx,
+				onClick: this.handleClickReceptor,
+				style: { cursor: "pointer" },
+			}), [
+					h('title', {}, this.viewReceptorTitle(receptor.value)),
+				]),
+			h('text', {
+				x: receptor.ui.cx, y: receptor.ui.cy + 1, fill: fontColor,
+				dominantBaseline: "middle", textAnchor: "middle",
+				style: { fontSize: "10px", cursor: "pointer" },
+				data_receptor_idx: idx,
+				onClick: this.handleClickReceptor,
+			}, charge.toFixed(1)),
+			h('text', {
+				x: receptor.ui.cx, y: receptor.ui.cy - 20, fill: "#000",
+				dominantBaseline: "middle", textAnchor: "middle",
+				style: { fontSize: "10px", cursor: "pointer" },
+				data_receptor_idx: idx,
+				onClick: this.handleClickReceptor,
+			}, this.viewReceptorLetter(receptor.value)),
+		]);
+	},
+	viewReceptorLetter: function (letter) {
+		switch (letter) {
+			case "\t": return "\\t";
+			case " ": return "_";
+			case "\n": return "\\n";
+			default: return letter;
+		}
+	},
+	viewReceptorTitle: function (letter) {
+		switch (letter) {
+			case "\t": return "tab";
+			case " ": return "space";
+			case "\n": return "new line";
+			default: return letter;
+		}
+	},
+	renderReceptorInfo: function () {
+		if (null === this.state.selectedReceptorIdx) return null;
+		var receptor = this._receptor[this.state.selectedReceptorIdx];
+		return h('div', {}, [
+			"Receptor: '" + this.viewReceptorTitle(receptor.value) + "' charge('" + receptor.charge.toFixed(2) + "')",
+		]);
+	},
+	renderAllReceptorInfo: function () {
+		var state = this.props.store.getState()
+
+		return h('div', {}, state.receptor.map(this.renderAllReceptorInfo_item));
+	},
+	renderAllReceptorInfo_item: function (receptor, idx) {
+		var charge = this.props.store.getReceptorCharge(idx)
+
+		return h('div', {}, ["Receptor: '" + this.viewReceptorTitle(receptor.value) + "' charge('" + charge.toFixed(2) + "')"]);
+	},
+	renderAllNeuronInfo: function () {
+		var state = this.props.store.getState()
+
+		return h('div', {}, state.neuron.map(this.renderAllNeuronInfo_item));
+	},
+	renderAllNeuronInfo_item: function (neuron, idx) {
+		var charge = this.props.store.getNeuronCharge(idx)
+
+		return h('div', {}, ["Neuron: '" + neuron.value + "' charge('" + charge.toFixed(2) + "')"]);
+	},
+	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,
+				onChange: this.handleChangeInput,
+				value: this.state.inputText,
+				rows: 10,
+				style: {
+					width: "100%",
+					padding: "12px",
+					marginTop: "6px",
+					backgroundColor: "#fff",
+				},
+			}),
+		]);
+	},
+	renderConfig: function () {
+		DBG && console.log("DBG:NeuronView:renderConfig", { state: this.state })
+		return h('details', { open: true, style: { margin: "12px 0", border: "1px solid #aaa", borderRadius: "6px", padding: "12px", backgroundColor: "#eee", } }, [
+			h('summary', { style: { cursor: "pointer" } }, "Config"),
+			h('div', { style: { padding: "12px", backgroundColor: "#fff", } }, [
+				h('div', {}, "Anim speed:"),
+				h('input', {
+					className: "form-control input-sm",
+					type: "number",
+					value: this.state.config_anim_speed,
+					name: "config_anim_speed",
+					onChange: this.handleChangeConfig,
+				}),
+				h('div', { style: { marginTop: "12px" } }),
+				h('div', {}, "Read speed (anim speed multiplier):"),
+				h('input', {
+					className: "form-control input-sm",
+					type: "number",
+					value: this.state.config_read_speed_multiplier,
+					name: "config_read_speed_multiplier",
+					onChange: this.handleChangeConfig,
+				}),
+				h('div', { style: { marginTop: "12px" } }),
+				h('div', {}, "Charge receptor at input:"),
+				h('input', {
+					className: "form-control input-sm",
+					type: "number",
+					value: this.state.config_charge_receptor_at_input,
+					name: "config_charge_receptor_at_input",
+					onChange: this.handleChangeConfig,
+				}),
+				h('div', { style: { marginTop: "12px" } }),
+				h('div', {}, "Max receptor charge:"),
+				h('input', {
+					className: "form-control input-sm",
+					type: "number",
+					value: this.state.config_max_receptor_charge,
+					name: "config_max_receptor_charge",
+					onChange: this.handleChangeConfig,
+				}),
+				h('div', { style: { marginTop: "12px" } }),
+				h('div', {}, "Max neuron charge:"),
+				h('input', {
+					className: "form-control input-sm",
+					type: "number",
+					value: this.state.config_max_neuron_charge,
+					name: "config_max_neuron_charge",
+					onChange: this.handleChangeConfig,
+				}),
+				h('div', { style: { marginTop: "12px" } }),
+				h('div', {}, "Discharge per tick:"),
+				h('input', {
+					className: "form-control input-sm",
+					type: "number",
+					step: "0.1",
+					value: this.state.config_discharge_per_tick,
+					name: "config_discharge_per_tick",
+					onChange: this.handleChangeConfig,
+				}),
+				h('div', { style: { marginTop: "12px" } }),
+				// config_strategy_overcharge: "LEAVE_HALF_CHARGE", // "REMOVE_CHARGE" | "LEAVE_HALF_CHARGE" | "LEAVE_ALMOST_MAX"
+				h('div', {}, "Overcharge strategy:"),
+				h('select', {
+					className: "form-control input-sm",
+					type: "number",
+					step: "0.1",
+					value: this.state.config_strategy_overcharge,
+					name: "config_strategy_overcharge",
+					onChange: this.handleChangeConfig,
+				}, ["REMOVE_CHARGE", "LEAVE_HALF_CHARGE", "LEAVE_ALMOST_MAX"].map(function (strategy) {
+					return h('option', { value: strategy }, strategy);
+				})),
+			]),
+		]);
+	},
+	handleChangeConfig: function (event) {
+		var value = event.target.value;
+		var name = event.target.getAttribute('name');
+		var state = this.state;
+		state[name] = value;
+		this.setState(state);
+	},
+	renderLog: function () {
+		return LOG__render(this._log)
+	},
+	handleRefreshLog: function () {
+		if (!this._log.length) return;
+		var lastLog = this._log[this._log.length - 1]
+		console.table(
+			[
+				lastLog.receptor.map(function (node) { return "R:'" + node.value + "'"; })
+					.concat(
+						lastLog.neuron.map(function (node) { return "N:'" + node.value + "'"; })
+					)
+			].concat(
+				this._log.map(function (log) {
+					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", {
+			state: this.state,
+			receptor: [].concat(this.props.store.getListReceptor()),
+			neuron: [].concat(this.props.store.getListNeuron()),
+			connection: [].concat(this.props.store.getListConnection()),
+			charge: [].concat(this.props.store.getListCharge()),
+		});
+
+		var state = this.props.store.getState()
+
+		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" } }, [
+								state.connection.map(this.renderConnection),
+								state.receptor.map(this.renderReceptor),
+								state.neuron.map(this.renderNeuron),
+								state.charge.map(this.renderCharge),
+							]),
+							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('td', { style: { verticalAlign: "top" } }, [
+							// this.renderReceptorInfo(),
+							this.renderAllReceptorInfo(),
+							this.renderAllNeuronInfo(),
+						]),
+					]),
+				]),
+			]),
+			h('div', {}, [
+				h('button', { className: "btn btn-primary", onClick: this.handleReset }, "Uruchom"),
+				" ",
+				" (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.handleRefreshLog }, [h('i', { className: "glyphicon glyphicon-refresh" }), " ", "Log"])
+			]),
+			this.renderLog(),
+			h('table', { className: "table" }, [
+				h('tbody', {}, [
+					h('tr', {}, [
+						h('td', { style: { width: "50%" } }, [
+							this.renderInput(),
+						]),
+						h('td', { style: { width: "50%" } }, [
+							this.renderConfig(),
+						]),
+					]),
+				]),
+			]),
+
+			h('div', {}, [
+				"TODO: receptory...",
+			]),
+		]);
+	}
+});
+
+// export default NeuronView
+ReactDOM.render(h(NeuronView, {
+	store: global[MAKE_STORE_FUNCTION_NAME](),
+	initialData: INITIAL_DATA || "",
+}), document.getElementById(HTML_ID))

+ 556 - 0
SE/se-lib/Route/Test/Neuron.php.makeNeuronStore.js

@@ -0,0 +1,556 @@
+var MAKE_STORE_FUNCTION_NAME = MAKE_STORE_FUNCTION_NAME || 'makeNeuronStore'
+var DBG = DBG || false;
+var DBG1 = true;
+
+// var STRATEGY_OVERCHARGE = [
+//     "REMOVE_CHARGE",
+//     "LEAVE_HALF_CHARGE",
+//     "LEAVE_ALMOST_MAX",
+// ]
+
+var DEFAULT_CONFIG = {
+	ui_output_width: 800,
+	ui_max_receptor_r: 12,
+	ui_space_y: 50,
+	config_anim_speed: 300,
+	config_read_speed_multiplier: 4,
+	config_charge_receptor_at_input: 1,
+	config_max_receptor_charge: 1,
+	config_max_neuron_charge: 1.5,
+	config_discharge_per_tick: 0.1,
+	config_discharge_max_in_new_neuron_from_one: 0.7,
+}
+var DEFAULT_NEURON = {
+	value: '',
+	charge: 0,
+	maxCharge: DEFAULT_CONFIG.config_max_neuron_charge,
+	uiShape: "circle",
+	ui: { cx: 0, cy: 0 },
+	source: [],
+	next: [],
+}
+function makeDefaultNeuronsStoreState() {
+	return {
+		receptor: [],
+		mapReceptorChar: {},
+		neuron: [],
+		connection: [],
+		doubleConn: [], // connections between two nodes
+		charge: [], // TODO: list of [ { charge: number, nodeType: (receptor|neuron), nodeIdx: int }, ...  ] groupd by node (type/idx)
+		inputReadPos: 0,
+		animPos: 0,
+		doAnim: true,
+		uiOutputHeight: 300,
+	}
+}
+
+
+function makeNeuronStore() {
+	DBG && console.log("DBG:NeuronStore:makeNeuronStore");
+	var _state = makeDefaultNeuronsStoreState()
+	var _config = {}
+	var _inputText = ''
+	var _animIntervalID = null
+	var _callback = null
+
+	function readCharFromInput() {
+		if (_state.inputReadPos >= _inputText.length) return null;
+
+		var char = _inputText.charAt(_state.inputReadPos)
+		_state.inputReadPos += 1
+
+		return char;
+	}
+
+	function forwardAnim() {
+		DBG1 && console.log("DBG:NeuronStore:forwardAnim:doAnim = '" + (_state.doAnim ? 1 : 0) + "'");
+		if (!_state.doAnim) return;
+
+		// 1. read from input if its time
+		// 2. check overcharged nodes
+		// 2.1 check overcharged Receptor
+		// 2.2 check overcharged Neuron
+		// 3. move all charges (Receptor -> Connection, Connection -> Neuron, Neuron -> Connection)
+		// 4. discharge all
+
+		// ad.2. TODO: what to do with Charge when create new Neuron?
+		// - idea 1: use Charge to create new Neuron: rm Charge, add Neuron, add Connection
+
+
+		// 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 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
+				_notifySubscribers()
+				return;
+			}
+			chargeAdd(_config.config_charge_receptor_at_input, { type: 'receptor', idx: foundReceptorIdx })
+
+			_state.animPos += 1
+			_notifySubscribers()
+			return;
+		}
+
+
+		// 2. check overcharged nodes
+		// 2.1 check overcharged Receptor
+		// 2.2 check overcharged Neuron
+		{ // check overcharged nodes
+			var overchargedNeuronIdx = _state.neuron.reduce(function (ret, neuron, idx) {
+				var charge = getNeuronCharge(idx)
+				if (charge < neuron.maxCharge) return ret;
+
+				ret.push({ idx: idx, charge: charge })
+				return ret;
+			}, [])
+
+			var overchargedReceptorIdx = _state.receptor.reduce(function (ret, receptor, idx) {
+				var charge = getReceptorCharge(idx)
+				if (charge < _config.config_max_receptor_charge) return ret;
+
+				ret.push({ idx: idx, charge: charge })
+				return ret;
+			}, [])
+
+			if (overchargedNeuronIdx.length > 0) {
+				var overcharged = overchargedNeuronIdx[0]
+				var firstNeuron = _state.neuron[overcharged.idx]
+				var secondNeuronWithCharge = _state.neuron.reduce(function (ret, neuron, idx) { // find neuron with max charge
+					if (idx === overcharged.idx) return ret; // skip firstNeuron
+					var charge = getNeuronCharge(idx)
+					if (!charge) return ret; // skip Neuron without charge
+					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(check overcharged)/neuron.1", { overcharged, firstNeuron, secondNeuronWithCharge })
+				if (null !== secondNeuronWithCharge) {
+					// is already exists - check doubleConn: { from: [ neuronIdx, neuronIdx ], to: neuronIdx }
+					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;
+						// if (dblConn.from[1] === overcharged.idx && dblConn.from[0] === secondNeuronWithCharge.idx) return dblConn;
+						return ret;
+					}, null)
+					var TODO__value = [firstNeuron.value, _state.neuron[secondNeuronWithCharge.idx].value].join('')
+					var TODO__toNodeIdx = _state.neuron.reduce(function (ret, node, idx) {
+						if (node.value === TODO__value) return idx;
+						return ret;
+					}, null)
+					var TODO__toNode = (null !== TODO__toNodeIdx) ? _state.neuron[TODO__toNodeIdx] : null
+					if (null !== TODO__toNode && null !== existingConn && TODO__toNodeIdx !== existingConn.toNode) {
+						// existingConn = null // Froce create new connection if not the same order
+					}
+					var toNode = _state.neuron[secondNeuronWithCharge.idx]
+					DBG1 && console.log("TODO:NeuronStore:forwardAnim:2(check overcharged)/neuron.2(secondNeuronWithCharge)", { from: firstNeuron.value, to: toNode.value, existingConn, TODO__value, TODO__toNode })
+					if (existingConn) { // charge Neuron existingConn.to
+						var idxNewNeuron = existingConn.to
+						chargeRemove({ type: 'neuron', idx: overcharged.idx })
+						chargeRemove({ 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 })
+							} break;
+							case "LEAVE_ALMOST_MAX": {
+								chargeAdd(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
+								chargeAdd(getNeuronAlmostMaxCharge(secondNeuronWithCharge.idx), { type: 'neuron', idx: secondNeuronWithCharge.idx })
+							} break;
+						}
+						chargeAdd(overcharged.charge + secondNeuronWithCharge.charge, { type: 'neuron', idx: existingConn.to })
+					} else { // create new
+						var newNeuron = makeNeuronFromTwoNeurons(overcharged.idx, overcharged.charge, secondNeuronWithCharge.idx, secondNeuronWithCharge.charge)
+						_state.neuron.push(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 })
+						chargeRemove({ type: 'neuron', idx: overcharged.idx })
+						chargeRemove({ 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 })
+							} break;
+							case "LEAVE_ALMOST_MAX": {
+								chargeAdd(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
+								chargeAdd(getNeuronAlmostMaxCharge(secondNeuronWithCharge.idx), { type: 'neuron', idx: secondNeuronWithCharge.idx })
+							} break;
+						}
+					}
+
+					// _state.animPos += 1
+					_notifySubscribers()
+					return;
+				} else { // !secondNeuronWithCharge
+					// 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;
+						return ret;
+					}, -1)
+					DBG1 && console.log("TODO:NeuronStore:forwardAnim:2(check overcharged)/neuron.2(!secondNeuronWithCharge)", { idxFoundConnection })
+					if (-1 === idxFoundConnection) {
+						var newNeuron = makeNeuronFromOneNeuron(overcharged.idx, overcharged.charge)
+						_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 })
+						switch (_config.config_strategy_overcharge) {
+							case "REMOVE_CHARGE": break;
+							case "LEAVE_HALF_CHARGE": {
+								chargeAdd(overcharged.charge / 2, { type: 'neuron', idx: overcharged.idx })
+							} break;
+							case "LEAVE_ALMOST_MAX": {
+								chargeAdd(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
+							} break;
+						}
+					} else {
+						_state.connection[idxFoundConnection].timesCreated += 1
+						// chargeMove(overcharged.charge, { type: 'receptor', idx: overcharged.idx }, { type: 'connection', idx: idxFoundConnection })
+						chargeRemove({ 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 })
+							} break;
+							case "LEAVE_ALMOST_MAX": {
+								chargeAdd(getNeuronAlmostMaxCharge(overcharged.idx), { type: 'neuron', idx: overcharged.idx })
+							} break;
+						}
+						chargeAdd(overcharged.charge, { type: 'connection', idx: idxFoundConnection })
+					}
+				}
+			}
+
+			if (overchargedReceptorIdx.length > 0) { // TODO: while ?
+				if (1 === overchargedReceptorIdx.length) {
+					var overcharged = overchargedReceptorIdx.shift()
+					// Restriction: only one connection between nodes
+					{
+						var idxFoundConnection = _state.connection.reduce(function (ret, conn, idx) {
+							if (ret > -1) return ret;
+							if (conn.fromType === 'receptor' && conn.fromIdx === 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) })
+						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 })
+							switch (_config.config_strategy_overcharge) {
+								case "REMOVE_CHARGE": break;
+								case "LEAVE_HALF_CHARGE": {
+									chargeAdd(overcharged.charge / 2, { type: 'receptor', idx: overcharged.idx })
+								} break;
+								case "LEAVE_ALMOST_MAX": {
+									chargeAdd(getReceptorAlmostMaxCharge(overcharged.idx), { type: 'receptor', idx: overcharged.idx })
+								} break;
+							}
+							chargeAdd(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 })
+							switch (_config.config_strategy_overcharge) {
+								case "REMOVE_CHARGE": break;
+								case "LEAVE_HALF_CHARGE": {
+									chargeAdd(overcharged.charge / 2, { type: 'receptor', idx: overcharged.idx })
+								} break;
+								case "LEAVE_ALMOST_MAX": {
+									chargeAdd(getReceptorAlmostMaxCharge(overcharged.idx), { type: 'receptor', idx: overcharged.idx })
+								} break;
+							}
+							chargeAdd(overcharged.charge, { type: 'connection', idx: idxFoundConnection })
+						}
+					}
+
+					// _state.animPos += 1
+					_notifySubscribers()
+					return;
+				} else {
+					DBG1 && console.log("TODO:NeuronStore:forwardAnim:2(check overcharged)/receptor", { TODO: "more then 1 receptor overcharged!" })
+				}
+			}
+
+			// DBG1 && 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) })
+			var toAddChargeNeuronIdxList = _state.charge.filter(function (charge) { return ("connection" === charge.nodeType); }).map(function (charge) {
+				return { charge: charge.charge, connIdx: charge.nodeIdx };
+			})
+
+			_state.charge = _state.charge.filter(function (charge) { return !("connection" === charge.nodeType); })
+			toAddChargeNeuronIdxList.forEach(function (toAddCharge) {
+				var conn = _state.connection[toAddCharge.connIdx]
+				chargeAdd(toAddCharge.charge, { type: 'neuron', idx: conn.toIdx })
+			})
+		}
+		{ // 4. discharge all
+			_state.charge = _state.charge.map(function (charge) {
+				return Object.assign({}, charge, {
+					charge: Math.max(0, charge.charge - _config.config_discharge_per_tick),
+				})
+			}).filter(function (charge) {
+				return charge.charge > 0;
+			})
+		}
+
+		_state.animPos += 1
+		_notifySubscribers();
+	}
+	function makeNeuronFromTwoNeurons(fromNeuronIdx, fromNeuronCharge, toNeuronIdx, toNeuronCharge) {
+		var fromNode = _state.neuron[fromNeuronIdx]
+		var toNode = _state.neuron[toNeuronIdx]
+		DBG1 && console.log("DBG:NeuronStore:makeNeuronFromTwoNeurons", { fromNode, toNode })
+		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
+		}
+		uiNewNodePos.cy = _state.neuron.reduce(function (ret, neuron) {
+			if (uiNewNodePos.cx < neuron.ui.cx - 10) return ret;
+			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;
+		}, fromNode.ui.cy + 10 + _config.ui_space_y)
+
+		DBG1 && console.log("DBG:NeuronStore:makeNeuronFromTwoNeurons", { uiNewNodePos, fromNode, toNode })
+		// var value = '' + (_state.neuron.length + 1);
+		var value = [fromNode.value, toNode.value].join('')
+		return Object.assign({}, DEFAULT_NEURON, {
+			value: value,
+			charge: 0,
+			maxCharge: fromNode.maxCharge * _config.config_discharge_max_in_new_neuron_from_one,
+			uiShape: "ellipse",
+			ui: Object.assign({}, uiNewNodePos, {
+				rx: 10,
+				ry: 10,
+			}),
+		});
+	}
+	function makeNeuronFromOneReceptor(fromReceptorIdx, charge) {
+		var sourceNode = _state.receptor[fromReceptorIdx]
+		// var value = '' + (_state.neuron.length + 1);
+		var value = [sourceNode.value, ""].join('') // TODO: same name what Receptor
+
+		DBG1 && console.log("DBG:NeuronStore:makeNeuronFromOneReceptor", { sourceNode, confMaxCharge: _config.config_discharge_max_in_new_neuron_from_one })
+		return Object.assign({}, DEFAULT_NEURON, {
+			value: value,
+			charge: 0,
+			maxCharge: sourceNode.maxCharge * _config.config_discharge_max_in_new_neuron_from_one,
+			uiShape: "ellipse",
+			ui: {
+				cx: sourceNode.ui.cx,
+				cy: sourceNode.ui.cy + 10 + _config.ui_space_y,
+				rx: 10,
+				ry: 10,
+			},
+		});
+	}
+	function makeNeuronFromOneNeuron(fromNeuronIdx, charge) {
+		var sourceNode = _state.neuron[fromNeuronIdx]
+		// var value = '' + (_state.neuron.length + 1);
+		var value = [sourceNode.value, "^"].join('')
+
+		DBG1 && console.log("DBG:NeuronStore:makeNeuronFromOneNeuron", { sourceNode, confMaxCharge: _config.config_discharge_max_in_new_neuron_from_one })
+		return Object.assign({}, DEFAULT_NEURON, {
+			value: value,
+			charge: 0,
+			maxCharge: sourceNode.maxCharge * _config.config_discharge_max_in_new_neuron_from_one,
+			uiShape: "ellipse",
+			ui: {
+				cx: sourceNode.ui.cx,
+				cy: sourceNode.ui.cy + 10 + _config.ui_space_y,
+				rx: 10,
+				ry: 10,
+			},
+		});
+	}
+	function chargeAdd(charge, to) {
+		var foundChargeIdx = _state.charge.reduce(function (ret, charge, idx) {
+			if (charge.nodeType === to.type && charge.nodeIdx === to.idx) return idx;
+			return ret;
+		}, -1)
+		DBG1 && console.log("DBG:NeuronStore:chargeAdd", { charge, to, foundChargeIdx })
+		if (-1 === foundChargeIdx) {
+			_state.charge.push({ charge: charge, nodeType: to.type, nodeIdx: to.idx })
+			foundChargeIdx = _state.charge.length - 1
+		} else {
+			_state.charge[foundChargeIdx].charge += charge
+		}
+		DBG1 && console.log("DBG:NeuronStore:chargeAdd.2", { state_charge: [].concat(_state.charge) })
+		return foundChargeIdx;
+	}
+	function chargeRemove(from) {
+		_state.charge = _state.charge.filter(function (item) {
+			return !(from.type === item.nodeType && from.idx === item.nodeIdx);
+		})
+	}
+
+	function updateOuputHeight(newNeuron) {
+		if (newNeuron.ui.cy + 30 > _state.uiOutputHeight) {
+			_state.uiOutputHeight = newNeuron.ui.cy + 30
+		}
+	}
+
+	function dispatch(payload) {
+		DBG1 && console.log("DBG:NeuronStore:dispatch('" + payload.type + "')", payload);
+		switch (payload.type) {
+			case 'INIT': stateReset(payload.inputText, payload.config); startAnimation(); 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 + "'")
+			}
+		}
+	}
+
+	function subscribe(callback) {
+		_callback = callback; // TODO: array
+		return unsubscribe;
+	}
+	function unsubscribe() {
+		_callback = null
+	}
+	function _notifySubscribers() {
+		if (_callback) _callback()
+	}
+
+	function makeReceptor(value, idx, arr) { // usage: [].map(makeReceptor)
+		var totalReceptors = arr.length;
+		var cx = Math.ceil(_config.ui_output_width / (totalReceptors + 1));
+		var r = Math.min(Math.ceil(_config.ui_output_width / (totalReceptors + 10) / 2), _config.ui_max_receptor_r);
+		var xCenter = cx + cx * idx - r;
+
+		return Object.assign({}, DEFAULT_NEURON, {
+			value: value,
+			maxCharge: _config.config_max_receptor_charge,
+			uiShape: "circle",
+			ui: {
+				cx: xCenter,
+				cy: 40,
+				r: r,
+			},
+			next: [],
+		})
+	}
+	function stateReset(inputText, config) {
+		_inputText = inputText
+		_config = config
+		_state = makeDefaultNeuronsStoreState()
+
+		var distinct = function (value, idx, self) {
+			return (idx === self.indexOf(value));
+		};
+		var foundLetters = inputText.split("").filter(distinct);
+		_state.receptor = foundLetters.map(makeReceptor);
+		_state.mapReceptorChar = _state.receptor.map(function (node) { return node.value; });
+
+		if (_animIntervalID) clearInterval(_animIntervalID)
+
+		_notifySubscribers();
+	}
+	function getReceptorIdx(char) {
+		return _state.mapReceptorChar.indexOf(char)
+	}
+	function startAnimation() {
+		_state.doAnim = true
+		if (_animIntervalID) clearInterval(_animIntervalID)
+		_animIntervalID = setInterval(forwardAnim, _config.config_anim_speed)
+	}
+	function getReceptorCharge(idx) {
+		return _state.charge.filter(function (charge) {
+			return ('receptor' === charge.nodeType && idx === charge.nodeIdx);
+		}).reduce(function (ret, charge) {
+			return ret + charge.charge;
+		}, 0)
+	}
+	function getNeuronCharge(idx) {
+		return _state.charge.filter(function (charge) {
+			return ('neuron' === charge.nodeType && idx === charge.nodeIdx);
+		}).reduce(function (ret, charge) {
+			return ret + charge.charge;
+		}, 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 })
+		return _state.neuron[idx].maxCharge - _config.config_discharge_per_tick;
+	}
+
+	function getNodesChargeState() {
+		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)
+			switch (charge.nodeType) {
+				case 'receptor': receptor[charge.nodeIdx].charge = charge.charge; break;
+				case 'neuron': neuron[charge.nodeIdx].charge = charge.charge; break;
+			}
+		})
+
+		function getNodeInfoFunction(type) {
+			return function _getNodeInfo(node) {
+				return {
+					type: type,
+					value: node.value,
+					charge: node.charge,
+				}
+			}
+		}
+
+		return receptor.map(getNodeInfoFunction('receptor'))
+			.concat(
+				neuron.map(getNodeInfoFunction('neuron'))
+			)
+			;
+	}
+
+	return {
+		subscribe: subscribe,
+		dispatch: dispatch,
+		getState: function () { return _state; },
+		getNodesChargeState: getNodesChargeState,
+		getListReceptor: function () { return _state.receptor; },
+		getListNeuron: function () { return _state.neuron; },
+		getReceptor: function (idx) { return _state.receptor[idx]; },
+		getNeuron: function (idx) { return _state.neuron[idx]; },
+		getConnection: function (idx) { return _state.connection[idx]; },
+		getListConnection: function () { return _state.connection; },
+		getListCharge: function () { return _state.charge; },
+		getReceptorCharge: getReceptorCharge,
+		getNeuronCharge: getNeuronCharge,
+	}
+}
+
+
+global[MAKE_STORE_FUNCTION_NAME] = makeNeuronStore
+// export default makeNeuronStore

+ 0 - 759
SE/se-lib/Route/Test/Neuron.php.view.js

@@ -1,759 +0,0 @@
-var createReactClass = window.p5VendorJs.createReactClass;
-var h = window.p5VendorJs.React.createElement;
-var ReactDOM = window.p5VendorJs.ReactDOM;
-// var AsyncTypeahead = window.p5VendorJs.AsyncTypeahead;
-// var swal = window.swal;
-if (!HTML_ID) throw "Missing HTML_ID";
-// if (!d3) throw "Missing d3 (https://d3js.org/d3.v5.min.js)";
-var DBG = DBG || false;
-var DBG1 = true;
-
-// Receptor is special Neuron which reads input (letters in this case)
-//  Receptor.ui: { cx, cy, r } // circle
-// Neuron: { charge, label, draw, source }
-//  Neuron.ui: { cx, cy, rx, ry } // eclipse
-//  Neuron.source: [ { neuron: Neuron | Receptor, atCharge }, ... ]
-
-// Receptor.neighbours: [] list of other receptors with max(receptors.charge)
-
-// makeNeuron cases:
-// stream: AAAAA and only A has charge then create Neuron over A and discharge and lower max charge
-// stream: AAAAA and receptor B has charge then create Neuron between A and B and discharge (closer to receptor with higher charge)
-
-var DEFAULT_CONFIG = {
-	ui_output_width: 800,
-	ui_max_receptor_r: 12,
-	ui_space_y: 30,
-	config_read_speed: 500,
-	config_charge_receptor_at_input: 1,
-	config_max_receptor_charge: 1.5,
-	config_discharge_per_tick: 0.1,
-	config_discharge_max_in_new_neuron_from_one: 0.7,
-}
-var DEFAULT_NEURON = {
-	value: '',
-	charge: 0,
-	maxCharge: DEFAULT_CONFIG.config_max_receptor_charge,
-	uiShape: "circle",
-	ui: { cx: 0, cy: 0 },
-	source: [],
-	next: [],
-}
-
-function state__getInitial(state, letters) {
-	function makeReceptor(letter, idx, arr) {
-		var totalReceptors = arr.length;
-		var cx = Math.ceil(state.ui_output_width / (totalReceptors + 1));
-		var r = Math.min(Math.ceil(state.ui_output_width / (totalReceptors + 10) / 2), state.ui_max_receptor_r);
-		var xCenter = cx + cx * idx - r;
-
-		return Object.assign({}, DEFAULT_NEURON, {
-			value: letter,
-			maxCharge: state.config_max_receptor_charge,
-			uiShape: "circle",
-			ui: {
-				cx: xCenter,
-				cy: 40,
-				r: r,
-			},
-			next: [],
-		})
-	}
-
-	return {
-		receptor: letters.map(makeReceptor),
-	}
-}
-function state__readFromInput(state, char) {
-	// 1. charge receptor with letter `char`
-	var listNeuron = state.neuron;
-	var listReceptor = state.receptor;
-	var mapLetterToReceptor = {}
-	for (var i = 0, l = listReceptor.length; i < l; i++) {
-		mapLetterToReceptor[listReceptor[i].value] = listReceptor[i];
-	}
-	// var toChangeReceptor = getReceptor(listReceptor, char)
-	var toChangeReceptor = mapLetterToReceptor[char]
-	DBG && console.log("DBG:anim", { char, len: state.inputText.length, toChangeReceptor, mapLetterToReceptor: mapLetterToReceptor });
-	toChangeReceptor.charge += state.config_charge_receptor_at_input;
-
-	// 2. check if any receptor is over charged
-	if (toChangeReceptor.charge > state.config_max_receptor_charge) {
-		var closestNodeWithCharge = findReceptorClosestNodeWithCharge(state, toChangeReceptor)
-		DBG1 && console.log("DBG:overcharge.1:", { char, len: state.inputText.length, closestNodeWithCharge, receptor: state.receptor });
-		if (!closestNodeWithCharge) {
-			{ // TODO: export to funtion makeNeuronFromOneNode(state, listReceptor, listNeuron, toChangeReceptor)
-				var neuron = makeNeuronFromOneNode(state, toChangeReceptor)
-				// TODO: neuron may already exists? check neuron.source if already has defined next
-				var alreadyExistNeuron = toChangeReceptor.next.filter(function (nextNeuron) {
-					return (nextNeuron.source.length === 1);
-				})
-				DBG1 && console.log("DBG:overcharge.2:", { alreadyExistNeuron });
-				if (alreadyExistNeuron.length) {
-					alreadyExistNeuron[0].charge += toChangeReceptor.charge
-					toChangeReceptor.charge = 0
-				} else {
-					toChangeReceptor.next.push(neuron)
-					listNeuron.push(neuron)
-					toChangeReceptor.charge = 0
-				}
-			}
-		} else { // (closestNodeWithCharge)
-			{ // TODO: export to funtion makeNeuronFromTwoNodes(state, listReceptor, listNeuron, toChangeReceptor, closestNodeWithCharge)
-				var neuron = makeNeuronFromTwoNodes(state, toChangeReceptor, closestNodeWithCharge)
-				// TODO: neuron may already exists? check neuron.source if already has defined next
-				var alreadyExistNeuron = toChangeReceptor.next.filter(function (nextNeuron) {
-					return (nextNeuron.source.length === 2 && (
-						(nextNeuron.source[0].value === toChangeReceptor.value && nextNeuron.source[1].value === closestNodeWithCharge.value)
-						|| (nextNeuron.source[0].value === closestNodeWithCharge.value && nextNeuron.source[1].value === toChangeReceptor.value)
-					));
-				})
-				DBG1 && console.log("DBG:overcharge.2:", { alreadyExistNeuron });
-				if (alreadyExistNeuron.length) {
-					alreadyExistNeuron[0].charge += toChangeReceptor.charge + closestNodeWithCharge.charge
-					toChangeReceptor.charge = 0
-					closestNodeWithCharge.charge = 0
-				} else {
-					toChangeReceptor.next.push(neuron)
-					closestNodeWithCharge.next.push(neuron)
-					listNeuron.push(neuron)
-					toChangeReceptor.charge = 0
-					closestNodeWithCharge.charge = 0
-				}
-			}
-		}
-
-		// TODO: dont use last letter -- use findReceptorClosestNodeWithCharge
-		// if no closest neuron with charge then create new neuron over receptor
-		// if (null !== state.animLastLetter && state.animLastLetter !== toChangeReceptor.letter) {
-		//     DBG1 && console.log("DBG:check overcharged", { toChangeReceptor, closestNodeWithCharge: closestNodeWithCharge, last_letter: state.animLastLetter });
-		//     listNeuron = makeNueron(listReceptor, listNeuron, toChangeReceptor.letter, state.animLastLetter);
-		//     // discharge receptors to 0
-		//     var toDischarge = [toChangeReceptor.letter, state.animLastLetter];
-		//     listReceptor = listReceptor.map(function (receptor) {
-		//         return (-1 !== toDischarge.indexOf(receptor.letter))
-		//             ? Object.assign({}, receptor, { charge: 0 })
-		//             : receptor
-		//         ;
-		//     })
-		// }
-	}
-
-	// 3. discharge all receptors and nodes by 0.1 (config_discharge_per_tick)
-	var dischargeNode = makeDischargeNodeFun(state);
-	return {
-		receptor: listReceptor.map(dischargeNode),
-		neuron: listNeuron.map(dischargeNode),
-	}
-}
-function state__fromOverchargedNeurons(state, overchargedNeurons) {
-	var listNeuron = state.neuron;
-	var listReceptor = state.receptor;
-
-	overchargedNeurons = overchargedNeurons.sort(sortNeuronByChargeCallback)
-	DBG1 && console.log("DBG:state:overcharged", { overchargedNeurons })
-	if (overchargedNeurons.length === 1) {
-		var toChangeReceptor = overchargedNeurons[0]
-		{ // TODO: export to funtion makeNeuronFromOneNode(state, listReceptor, listNeuron, toChangeReceptor)
-			var neuron = makeNeuronFromOneNode(state, toChangeReceptor)
-			// TODO: neuron may already exists? check neuron.source if already has defined next
-			var alreadyExistNeuron = toChangeReceptor.next.filter(function (nextNeuron) {
-				return (nextNeuron.source.length === 1);
-			})
-			DBG1 && console.log("DBG:overcharge.2:", { alreadyExistNeuron: Object.assign({}, alreadyExistNeuron) });
-			if (alreadyExistNeuron.length) {
-				alreadyExistNeuron[0].charge += toChangeReceptor.charge
-				toChangeReceptor.charge = 0
-			} else {
-				toChangeReceptor.next.push(neuron)
-				listNeuron.push(neuron)
-				toChangeReceptor.charge = 0
-			}
-		}
-	} else {
-		var toChangeReceptor = overchargedNeurons[0]
-		var closestNodeWithCharge = overchargedNeurons[1]
-		{ // TODO: export to funtion makeNeuronFromTwoNodes(state, listReceptor, listNeuron, toChangeReceptor, closestNodeWithCharge)
-			var neuron = makeNeuronFromTwoNodes(state, toChangeReceptor, closestNodeWithCharge)
-			// TODO: neuron may already exists? check neuron.source if already has defined next
-			var alreadyExistNeuron = toChangeReceptor.next.filter(function (nextNeuron) {
-				return (nextNeuron.source.length === 2 && (
-					(nextNeuron.source[0].value === toChangeReceptor.value && nextNeuron.source[1].value === closestNodeWithCharge.value)
-					|| (nextNeuron.source[0].value === closestNodeWithCharge.value && nextNeuron.source[1].value === toChangeReceptor.value)
-				));
-			})
-			DBG1 && console.log("DBG:overcharge.2:", { alreadyExistNeuron });
-			if (alreadyExistNeuron.length) {
-				alreadyExistNeuron[0].charge += toChangeReceptor.charge + closestNodeWithCharge.charge
-				toChangeReceptor.charge = 0
-				closestNodeWithCharge.charge = 0
-			} else {
-				toChangeReceptor.next.push(neuron)
-				closestNodeWithCharge.next.push(neuron)
-				listNeuron.push(neuron)
-				toChangeReceptor.charge = 0
-				closestNodeWithCharge.charge = 0
-			}
-		}
-	}
-
-	return {
-		receptor: listReceptor,
-		neuron: listNeuron,
-	}
-}
-function sortNeuronByChargeCallback(n1, n2) {
-	if (n1.charge > n2.charge) return 1;
-	if (n1.charge < n2.charge) return -1;
-	return 0;
-}
-function getReceptor(listReceptor, char) {
-	var receptor = listReceptor.filter(function (receptor) {
-		return (char == receptor.value);
-	});
-	if (!receptor.length) throw "BUG: no input receptor found for char '" + char + "'"; // TODO: create on demand?
-	return receptor[0];
-}
-function findReceptorClosestNodeWithCharge(state, node) {
-	var skipSelfValue = node.value;
-	var maxChargedReceptor = state.receptor.reduce(function (ret, receptor) {
-		if (receptor.value === skipSelfValue) return ret;
-		if (0 === receptor.charge) return ret;
-		if (!ret) return receptor;
-		return (receptor.charge > ret.charge) ? receptor : ret;
-	}, null)
-	return maxChargedReceptor;
-}
-function makeNeuronFromOneNode(state, sourceNode) {
-	return Object.assign({}, DEFAULT_NEURON, {
-		value: '' + (state.neuron.length + 1),
-		charge: 0, // sourceNode.charge,
-		maxCharge: sourceNode.maxCharge * state.config_discharge_max_in_new_neuron_from_one, // maybe?: sourceNode.charge
-		uiShape: "ellipse",
-		ui: {
-			cx: sourceNode.ui.cx,
-			cy: sourceNode.ui.cy + 10 + state.ui_space_y,
-			rx: 10,
-			ry: 10,
-		},
-		source: [sourceNode],
-	});
-}
-function makeNeuronFromTwoNodes(state, first, second) {
-	var value = '' + first.value + second.value;
-	var newNeuronX = (first.ui.cx + second.ui.cx) / 2 // center between nodes
-	{ // closer to node with more charge (first)
-		var xDiff = Math.abs(first.ui.cx - second.ui.cx)
-		var isFirstOnRight = (first.ui.cx > second.ui.cx)
-		// first.charge + second.charge -- xDiff
-		// second.charge -- x ==> x = second.charge * 100 / (first.charge + second.charge)
-		var xToFirst = (second.charge * 100) / (first.charge + second.charge)
-		var newNeuronX = first.ui.cx + (isFirstOnRight ? -1 : 1) * xToFirst
-	}
-	var newNeuronY = state.neuron.reduce(function (ret, neuron) {
-		if (newNeuronX < neuron.ui.cx - 10) return ret;
-		if (newNeuronX > 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 + state.ui_space_y;
-	}, first.ui.cy + 10 + state.ui_space_y)
-
-	return Object.assign({}, DEFAULT_NEURON, {
-		value: value,
-		charge: 0, // (first.charge + second.charge) / 2,
-		maxCharge: ((first.maxCharge + second.maxCharge) / 2) * state.config_discharge_max_in_new_neuron_from_one,
-		uiShape: "ellipse",
-		ui: {
-			cx: newNeuronX,
-			cy: newNeuronY,
-			rx: 10 * value.length,
-			ry: 10,
-		},
-		source: [first, second],
-	});
-}
-function getNeuron(listNeuron, value) {
-	var neuron = listNeuron.filter(function (neuron) {
-		return (value == neuron.letter);
-	});
-	return (neuron.length) ? neuron[0] : null;
-}
-function makeDischargeNodeFun(state) {
-	return function dischargeNode(neuron) {
-		return (neuron.charge > 0)
-			? Object.assign({}, neuron, {
-				charge: Math.max(0, neuron.charge - state.config_discharge_per_tick),
-			})
-			: neuron
-			;
-	}
-}
-function isNeuronOvercharged(neuron) {
-	return (neuron.charge > neuron.maxCharge);
-}
-
-
-
-
-function LOG__render(listLogEntries) {
-	return h('details', { open: false, style: { margin: "12px 0", border: "1px solid #aaa", borderRadius: "6px", padding: "12px", backgroundColor: "#eee", } }, [
-		h('summary', { style: { cursor: "pointer" } }, "Log"),
-		h('div', { style: { border: "1px solid #eee", backgroundColor: "#fff" } }, [
-			h('table', { className: "table table-border" }, [
-				h('thead', {}, LOG__renderTheadLog(listLogEntries)),
-				h('tbody', {}, LOG__renderTbodyLog(listLogEntries)),
-			]),
-		]),
-	]);
-}
-function LOG__renderTheadLog(listLogEntries) {
-	if (!listLogEntries.length) return null;
-	var lastLog = listLogEntries[listLogEntries.length - 1]
-	var colsR = lastLog.receptor.map(function (node) { return node.value; })
-	var colsN = lastLog.neuron.map(function (node) { return node.value; })
-	return [
-		h('tr', {},
-			[h('th', {}, "Lp.")]
-				.concat(
-					colsR.map(function (label) { return h('th', {}, "R:'" + label + "'"); })
-				)
-				.concat(
-					colsN.map(function (label) { return h('th', {}, "N:'" + label + "'"); })
-				)
-		)
-	];
-}
-function LOG__renderTbodyLog(listLogEntries) {
-	if (!listLogEntries.length) return null;
-	var lastLog = listLogEntries[listLogEntries.length - 1]
-	var colsR = lastLog.receptor.map(function (node) { return node.value; })
-	var colsN = lastLog.neuron.map(function (node) { return node.value; })
-	return listLogEntries.map(function (log, lpLog) {
-		var row = colsR.map(function (label, idx) { return log.receptor[idx].charge; })
-			.concat(
-				colsN.map(function (label, idx) { return (log.neuron[idx]) ? log.neuron[idx].charge : null; })
-			)
-		return h('tr', {}, [h('th', { style: { color: "#ddd" } }, '' + (lpLog + 1) + '.')]
-			.concat(
-				row.map(LOG__renderCellNodeChargeLog)
-			)
-		)
-	})
-}
-function LOG__renderCellNodeChargeLog(charge) {
-	if (null === charge) return null;
-	return h('td', {
-		style: {
-			color: (charge > 0) ? "#000" : "#aaa",
-		},
-	}, charge.toFixed(2));
-}
-
-
-
-var NeuronView = createReactClass({
-	_input: null,
-	_output: null,
-	_animTimoutID: null,
-	_log: [],
-	getInitialState: function () {
-
-		return Object.assign({}, DEFAULT_CONFIG, {
-			inputText: this.props.initialData || '', // TODO: INITIAL_DATA
-			receptor: [],
-			neuron: [],
-			doAnim: false,
-			animPos: 0,
-			animLastLetter: null,
-			selectedReceptorIdx: null,
-		});
-	},
-
-	handleChangeInput: function (event) {
-		this.setState({
-			inputText: event.target.value,
-		})
-	},
-	setInputRet: function (reactEl) { this._input = reactEl; },
-	setOutputRet: function (reactEl) { this._output = reactEl; },
-	componentDidMount: function () {
-		this.startAnimation();
-	},
-	handleExec: function (event) {
-		event.preventDefault();
-		DBG1 && console.log('DBG:handleExec...');
-		if (this._animTimoutID) clearTimeout(this._animTimoutID);
-		this.setState({
-			neuron: [],
-			receptor: [],
-		})
-		setTimeout(this.startAnimation, this.state.config_read_speed * 2)
-	},
-	handlePause: function (event) {
-		event.preventDefault();
-		this.setState({ doAnim: false })
-	},
-	handleStart: function (event) {
-		event.preventDefault();
-		this.forwardAnim(true)
-	},
-	startAnimation: function (event) {
-		var distinct = function (value, idx, self) {
-			return (idx === self.indexOf(value));
-		};
-		// var foundLetters = this.state.inputText.split("\n").reduce(function (ret, line) {
-		// 	return ret.concat(
-		// 		line.split('').filter(distinct)
-		// 	).filter(distinct);
-		// }, []);
-		var foundLetters = this.state.inputText.split("").filter(distinct);
-		DBG1 && console.warn('DBG:handleExec:foundLetters', { foundLetters });
-
-		this._log = []
-		this.setState(Object.assign(
-			{},
-			state__getInitial(this.state, foundLetters),
-			{
-				animPos: 0,
-				neuron: [],
-				doAnim: true,
-			}
-		));
-
-		setTimeout(this.forwardAnim, this.state.config_read_speed)
-	},
-	getReceptor: function (char) {
-		var receptor = this.state.receptor.filter(function (receptor) {
-			return (char == receptor.value);
-		});
-		if (!receptor.length) throw "BUG: no input receptor found for char '" + char + "'"; // TODO: create on demand?
-		return receptor[0];
-	},
-	forwardAnim: function (forceUpdateDoAnim) {
-		this._logState(this.state)
-		var doAnim = (arguments.length > 0) ? forceUpdateDoAnim : this.state.doAnim;
-		var animPos = this.state.animPos;
-		var overchargedNeurons = this.state.neuron.filter(isNeuronOvercharged)
-		if (overchargedNeurons.length === 0) {
-			animPos += 1;
-		} else {
-			DBG1 && console.log("DBG:anim TODO: neuron overcharged", { overchargedNeurons });
-		}
-
-		if (animPos >= this.state.inputText.length) {
-			DBG1 && console.log("DBG:anim STOP - end of input", { inputLength: this.state.inputText.length, animPos: this.state.animPos });
-			return;
-		}
-
-		var char = this.state.inputText.charAt(animPos)
-		DBG && console.log("DBG:anim", { animPos, char, len: this.state.inputText.length, doAnim });
-
-		this.setState(Object.assign(
-			{},
-			(overchargedNeurons.length) ? state__fromOverchargedNeurons(this.state, overchargedNeurons) : state__readFromInput(this.state, char),
-			{
-				animLastLetter: char,
-				animPos: animPos,
-				doAnim: doAnim,
-			}
-		));
-
-		if (doAnim) {
-			this._animTimoutID = setTimeout(this.forwardAnim, this.state.config_read_speed)
-		}
-	},
-	_logState: function (state) {
-		function getNodeInfo(node) {
-			return {
-				value: node.value,
-				charge: node.charge,
-			}
-		}
-
-		this._log.push({
-			receptor: state.receptor.map(getNodeInfo),
-			neuron: state.neuron.map(getNodeInfo),
-		})
-	},
-	selectReceptor: function (receptorIdx) {
-		this.setState({ selectedReceptorIdx: receptorIdx })
-	},
-	handleClickReceptor: function (event) {
-		var data_receptor_idx = event.target.getAttribute('data_receptor_idx')
-		DBG1 && console.log('DBG:handleClickReceptor', { data_receptor_idx, target: event.target });
-		this.selectReceptor(data_receptor_idx)
-	},
-
-	renderConnections: function (neuron, idx) {
-		DBG1 && console.log('DBG:renderConnections', { neuron, p1: neuron.source[0].ui, p2: (neuron.source.length > 1) ? neuron.source[1].ui : null });
-		return h('g', {}, [
-			h('line', {
-				x1: neuron.source[0].ui.cx, y1: neuron.source[0].ui.cy,
-				x2: neuron.ui.cx, y2: neuron.ui.cy,
-				style: {
-					stroke: "#46b8da",
-					strokeWidth: "2"
-				},
-			}),
-			(neuron.source.length > 1)
-				? h('line', {
-					x1: neuron.source[1].ui.cx, y1: neuron.source[1].ui.cy,
-					x2: neuron.ui.cx, y2: neuron.ui.cy,
-					style: {
-						stroke: "#46b8da",
-						strokeWidth: "2"
-					},
-				})
-				: null,
-		]);
-	},
-	renderNeuron: function (neuron, idx) {
-		var chargePr = (100 * neuron.charge) / this.state.config_max_receptor_charge;
-		var fontColor = (chargePr < 50) ? "#000" : "#fff";
-		return h('g', {}, [
-			h(neuron.uiShape, Object.assign({}, neuron.ui, {
-				stroke: "#46b8da", strokeWidth: 1,
-				fill: (chargePr > 100) ? "#f00" : "hsl(194, 67%, " + (100 - chargePr) + "%)",
-				// data_neuron_idx: idx,
-				// onClick: this.handleClickNeuron,
-				style: { cursor: "pointer" },
-			}), [
-					h('title', {}, this.viewNeuronTitle(neuron.value)),
-				]),
-			h('text', {
-				x: neuron.ui.cx, y: neuron.ui.cy + 1, fill: fontColor,
-				dominantBaseline: "middle", textAnchor: "middle",
-				style: { fontSize: "10px", cursor: "pointer" },
-				// data_neuron_idx: idx,
-				// onClick: this.handleClickNeuron,
-			}, neuron.charge.toFixed(1)),
-		]);
-	},
-	viewNeuronValue: function (value) {
-		return value
-			.replace(new RegExp("\n", "g"), "\\n")
-			.replace(new RegExp("\t", "g"), "\\t")
-			.replace(new RegExp(" ", "g"), "_")
-			;
-	},
-	viewNeuronTitle: function (value) {
-		return this.viewNeuronValue(value);
-	},
-	renderReceptor: function (receptor, idx) {
-		// orange: hsl(40, 100%, 50%) --> hsl(0, 100%, 50%) -> red (overcharged)
-		// var totalReceptors = this.state.receptor.length;
-		// var cx = Math.ceil(this.state.ui_output_width / ( totalReceptors + 1 ));
-		// var r = Math.min(Math.ceil(this.state.ui_output_width / ( totalReceptors + 10 ) / 2), this.state.ui_max_receptor_r);
-		// DBG && console.log('DBG:renderReceptor', { receptor, idx, totalReceptors, r });
-		// var xCenter = cx + cx * idx - r;
-		var chargePr = (100 * receptor.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 fontColor = "#fff"; // (chargePr < 50) ? "#000" : "#fff";
-		// this.state.config_max_receptor_charge = 100%
-		// receptor.charge = x% (max 100)
-		return h('g', {}, [
-			h(receptor.uiShape, Object.assign({}, receptor.ui, {
-				stroke: "#46b8da", strokeWidth: 1,
-				fill: shapeColor,
-				data_receptor_idx: idx,
-				onClick: this.handleClickReceptor,
-				style: { cursor: "pointer" },
-			}), [
-					h('title', {}, this.viewReceptorTitle(receptor.value)),
-				]),
-			h('text', {
-				x: receptor.ui.cx, y: receptor.ui.cy + 1, fill: fontColor,
-				dominantBaseline: "middle", textAnchor: "middle",
-				style: { fontSize: "10px", cursor: "pointer" },
-				data_receptor_idx: idx,
-				onClick: this.handleClickReceptor,
-			}, receptor.charge.toFixed(1)),
-			h('text', {
-				x: receptor.ui.cx, y: receptor.ui.cy - 20, fill: "#000",
-				dominantBaseline: "middle", textAnchor: "middle",
-				style: { fontSize: "10px", cursor: "pointer" },
-				data_receptor_idx: idx,
-				onClick: this.handleClickReceptor,
-			}, this.viewReceptorLetter(receptor.value)),
-		]);
-	},
-	viewReceptorLetter: function (letter) {
-		switch (letter) {
-			case "\t": return "\\t";
-			case " ": return "_";
-			case "\n": return "\\n";
-			default: return letter;
-		}
-	},
-	viewReceptorTitle: function (letter) {
-		switch (letter) {
-			case "\t": return "tab";
-			case " ": return "space";
-			case "\n": return "new line";
-			default: return letter;
-		}
-	},
-	renderReceptorInfo: function () {
-		if (null === this.state.selectedReceptorIdx) return null;
-		var receptor = this.state.receptor[this.state.selectedReceptorIdx];
-		return h('div', {}, [
-			"Receptor: '" + this.viewReceptorTitle(receptor.value) + "' charge('" + receptor.charge.toFixed(2) + "')",
-		]);
-	},
-	renderAllReceptorInfo: function () {
-		return h('div', {}, this.state.receptor.map(this.renderAllReceptorInfo_item));
-	},
-	renderAllReceptorInfo_item: function (receptor) {
-		return h('div', {}, ["Receptor: '" + this.viewReceptorTitle(receptor.value) + "' charge('" + receptor.charge.toFixed(2) + "')"]);
-	},
-	renderAllNeuronInfo: function () {
-		return h('div', {}, this.state.neuron.map(this.renderAllNeuronInfo_item));
-	},
-	renderAllNeuronInfo_item: function (neuron) {
-		return h('div', {}, ["Neuron: '" + neuron.value + "' charge('" + neuron.charge.toFixed(2) + "')"]);
-	},
-	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,
-				onChange: this.handleChangeInput,
-				value: this.state.inputText,
-				rows: 10,
-				style: {
-					width: "100%",
-					padding: "12px",
-					marginTop: "6px",
-					backgroundColor: "#fff",
-				},
-			}),
-		]);
-	},
-	renderConfig: 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" } }, "Config"),
-			h('div', { style: { padding: "12px", backgroundColor: "#fff", } }, [
-				h('div', {}, "Read speed:"),
-				h('input', {
-					className: "form-control input-sm",
-					type: "number",
-					value: this.state.config_read_speed,
-					name: "config_read_speed",
-					onChange: this.handleChangeConfig,
-				}),
-				h('div', { style: { marginTop: "12px" } }),
-				h('div', {}, "Charge receptor at input:"),
-				h('input', {
-					className: "form-control input-sm",
-					type: "number",
-					value: this.state.config_charge_receptor_at_input,
-					name: "config_charge_receptor_at_input",
-					onChange: this.handleChangeConfig,
-				}),
-				h('div', { style: { marginTop: "12px" } }),
-				h('div', {}, "Max receptor charge:"),
-				h('input', {
-					className: "form-control input-sm",
-					type: "number",
-					value: this.state.config_max_receptor_charge,
-					name: "config_max_receptor_charge",
-					onChange: this.handleChangeConfig,
-				}),
-				h('div', { style: { marginTop: "12px" } }),
-				h('div', {}, "Discharge per tick:"),
-				h('input', {
-					className: "form-control input-sm",
-					type: "number",
-					step: "0.1",
-					value: this.state.config_discharge_per_tick,
-					name: "config_discharge_per_tick",
-					onChange: this.handleChangeConfig,
-				}),
-			]),
-		]);
-	},
-	handleChangeConfig: function (event) {
-		var value = event.target.value;
-		var name = event.target.getAttribute('name');
-		var state = this.state;
-		state[name] = value;
-		this.setState(state);
-	},
-	renderLog: function () {
-		return LOG__render(this._log)
-	},
-	handleRefreshLog: function () {
-		if (!this._log.length) return;
-		var lastLog = this._log[this._log.length - 1]
-		console.table(
-			[
-				lastLog.receptor.map(function (node) { return "R:'" + node.value + "'"; })
-					.concat(
-						lastLog.neuron.map(function (node) { return "N:'" + node.value + "'"; })
-					)
-			].concat(
-				this._log.map(function (log) {
-					return log.receptor.map(function (node) { return node.charge })
-						.concat(
-							log.neuron.map(function (node) { return node.charge })
-						)
-				})
-			)
-		)
-	},
-	render: function () {
-		DBG1 && console.log("DBG:render", { state: this.state });
-		return h('div', {}, [
-			h('table', { className: "table table-border", 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: 300, width: this.state.ui_output_width, style: { border: "1px solid #eee" } }, [
-								this.state.neuron.map(this.renderConnections),
-								this.state.receptor.map(this.renderReceptor),
-								this.state.neuron.map(this.renderNeuron),
-							]),
-						]),
-						h('td', { style: { verticalAlign: "top" } }, [
-							this.renderReceptorInfo(),
-							this.renderAllReceptorInfo(),
-							this.renderAllNeuronInfo(),
-						]),
-					]),
-				]),
-			]),
-			h('div', {}, [
-				h('button', { className: "btn btn-primary", onClick: this.handleExec }, "Uruchom"),
-				" ",
-				this.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.handleStart }, [h('i', { className: "glyphicon glyphicon-play" }), "Start"])
-				,
-				h('button', { className: "btn btn-default", onClick: this.handleRefreshLog }, [h('i', { className: "glyphicon glyphicon-refresh" }), " ", "Log"])
-			]),
-			this.renderLog(),
-			h('table', { className: "table table-border" }, [
-				h('tbody', {}, [
-					h('tr', {}, [
-						h('td', { style: { width: "50%" } }, [
-							this.renderInput(),
-						]),
-						h('td', { style: { width: "50%" } }, [
-							this.renderConfig(),
-						]),
-					]),
-				]),
-			]),
-
-			h('div', {}, [
-				"TODO: receptory...",
-			]),
-		]);
-	}
-});
-
-
-ReactDOM.render(h(NeuronView, {
-	initialData: INITIAL_DATA || "",
-}), document.getElementById(HTML_ID))