aboutsummaryrefslogblamecommitdiffstats
path: root/grapher/script.js
blob: 70be0788abf9dc5ae9ec1d0399a4ef63593f5f62 (plain) (tree)



































                                                                          
            








                                        

                                                




                       
          



              
              



































                                                                                                            
                          








                                                      
                                     



                                                                                        


                                                                                                                  
























                                                                                                                                                                                                 

                         
                                                                               







                                                                                                                        
                                                                              











                                                                                                                  
                                                  





























































                                                                                                                                       


                                  
                         
                              



































                                                                                                                                                                                  


                                    






                                                      













                                                                                                      

                                                                            

 


                                                       
                                                                                                                                                 

                                                            
                                                                                                                  

                                             
                             









                                               
                 
 

         
                                                           





























                                                                                                                 
                                     










                                                          


                 
                                   
                                   

                                        

                                                                      
                                                                                        

                                                                                                                                                                  

                                        





                                                                                                                                                                 
                                    
                                                                                                                              




                                                                              










                                                       



                                                   



                                                                                                            
                                     
                 




                                                                              
                                     






                                                                                                                   
                                     



                                     

                                     
           



                                                       
















                                                                                                    
                              






                                               


                                                      











                                                   
                             



                                       
                             






                                                                              
                                                                                                                              

















                                                                                                                                    

                             

                                                  

                                                                      
 
/*
Serialization format:

{
	"chips": [
		{
			"GUID": String,
			"pos": {
				"x":    Number,
				"y":    Number
			},
			"to": {
				<See the 'chip.currentOverrides' property>
			}
		}
	],

	"connections": [
		{
			"i": {
				"chipidx": Number,
				"portidx": Number
			},

			o: {
				"chipidx": Number,
				"portidx": Number
			}
		}
	]
}


*/


var r2cdata;
var graph;
var searcher;
const rootel = document.documentElement;

const chips = [];

/*
[
	{
		i: {x:Number, y:Number} | <div>,
		o: {x:Number, y:Number} | <div>
	}
]
*/
const connections = [];

var clean;
var mode;
var targ;
var start;
var wirestate;
var maintouch;
const lastmp = {x:0,y:0};

const graphPos = {x:0,y:0};

var canvas,ctx;

const allTypes = [];

function switchID(el, id) {
	const old = document.getElementById(id);
	if (old instanceof Element) old.id = '';
	if (el  instanceof Element) el .id = id;
}

function remtopx(value) {return value * parseFloat( getComputedStyle( document.documentElement ).fontSize )}

function renderCurveBetweenPorts(outx, outy, inx, iny) {
	var dist = (((outx - inx) ** 2) + ((outy - iny) ** 2)) ** 0.5;
	var heightOfCurve = Math.abs(iny - outy);
	var widthOfCurve = Math.abs(inx - outx);
	var halfWidth = (inx - outx)/2;

	var cpbasex = (Math.abs((widthOfCurve * 2) / dist * 10) + 60) * (heightOfCurve / 150)**0.8;

	var cp1x = outx + cpbasex;
	var cp2x = inx - cpbasex;

	//point(cp1x, outy);
	//point(cp2x, iny);
	//point(inx - outx, outy);

	ctx.beginPath();
	ctx.moveTo(outx,outy);
	ctx.bezierCurveTo(cp1x, outy, cp2x, iny, inx, iny);
	ctx.moveTo(outx,outy);
	ctx.closePath();
	ctx.lineWidth = 3;
	ctx.stroke();
}

function appendTypeUI(chip) {
	const data = [];

	for(const key of Object.keys(chip.typeInfo)) {
		data.push(`${key}: `);
		let m = newEl('select', 'typeSelect');
		m.classList.add(key);
		m.addEventListener('change', e => {
			if (e.target.value) chip.currentOverrides[key] = e.target.value;
			else delete chip.currentOverrides[key]

			delConnections(chip.el.querySelector('.chip'));
			chip.el.querySelector('.chip').remove();
			chip.el.querySelector('.selUI').prepend(generateChipHTML(chip.nd, chip.currentOverrides));
		});

		for(const type of chip.typeInfo[key]) {
			let opt = newEl('option');
			opt.value = (type == chip.typeInfo[key][0]) ? '' : type;
			opt.innerText = type;
			m.appendChild(opt);
		}
		data.push(m, newEl('br'))
	}

	const ui = newEl('div', 'ui');
	ui.append(...data);
	chip.el.append(ui);
}


function delConnections(el) {
	let tmp = connections.filter(con => !(((con.i instanceof Node) && el.contains(con.i)) ||
	                                      ((con.o instanceof Node) && el.contains(con.o)) || 
																				(con.i == el) || (con.i == el)));
	connections.length = 0;
	connections.push(...tmp);
}

function newChip(GUID) {
	const types = {};
	const typeParams = r2cdata.Nodes[GUID].NodeDescs[0].ReadonlyTypeParams;
	for (const desc of Object.keys(typeParams))
		types[desc] = [
			`${desc}: ${typeParams[desc]}`,
			...(typeParams[desc] == 'any' ? allTypes : typeParams[desc].match(/^\((.+)\)$/)[1].split(' | '))
		];
			
	const ne = newEl('div', 'chipbox');
	const chipcontainer = newEl('div', 'selUI');
	chipcontainer.append(generateChipHTML(r2cdata.Nodes[GUID].NodeDescs));
	ne.append(chipcontainer);
	graph.append(ne);
	
	let newchipx = -graphPos.x + (graph.getClientRects()[0].width  / 2) - (ne.getClientRects()[0].width  / 2);
	let newchipy = -graphPos.y + (graph.getClientRects()[0].height / 2) - (ne.getClientRects()[0].height / 2);
	ne.style.setProperty('--chipOffsetX', newchipx);
	ne.style.setProperty('--chipOffsetY', newchipy);

	const chip = {
		el: ne,
		typeInfo: types,
		currentOverrides: {},
		nd: r2cdata.Nodes[GUID].NodeDescs,
		GUID: GUID,
	};
	
	appendTypeUI(chip);
	chips.push(chip);

	return chip;
}
function newChipHandler({GUID}) {
	newChip(GUID);
}

function serializeGraph() {
	const ret = {
		chips: [],
		connections: []
	};

	for (const chip of chips) {
		let x = Number(chip.el.style.getPropertyValue('--chipOffsetX'));
		let y = Number(chip.el.style.getPropertyValue('--chipOffsetY'));
		
		ret.chips.push({
			GUID: chip.GUID,
			pos: {
				x: isNaN(x) ? 0 : x,
				y: isNaN(y) ? 0 : y
			},
			to: chip.currentOverrides
		})
	}

	for(const con of connections) {
		if ((con.i instanceof Node) && (con.o instanceof Node)) {
			const sCon = {
				i: {
					chipidx: -1,
					portidx: -1
				},

				o: {
					chipidx: -1,
					portidx: -1
				}
			}
			for (const key of Object.keys(chips)) {
				if (chips[key].el.contains(con.i)) {
					sCon.i.chipidx = Number(key);
					sCon.i.portidx = Array.from(chips[key].el.querySelector('.input').children).indexOf(con.i) / 2
				}
				if (chips[key].el.contains(con.o)) {
					sCon.o.chipidx = Number(key);
					sCon.o.portidx = Array.from(chips[key].el.querySelector('.output').children).indexOf(con.o) / 2
				}
			}
			ret.connections.push(sCon);
		}
	}
	return ret;
}

function deserializeGraph(graph) {
	for(const chip of chips) {
		chip.el.remove();
	}
	chips.length = 0;
	connections.length = 0
	for (const chip of graph.chips) {
		const newchip = newChip(chip.GUID);

		newchip.el.style.setProperty('--chipOffsetX', chip.pos.x);
		newchip.el.style.setProperty('--chipOffsetY', chip.pos.y);

		newchip.currentOverrides = chip.to;

		for (const ov of Object.entries(chip.to)) {
			const selector = newchip.el.querySelector(`select.${ov[0]}`);
			if (!selector) {console.warn(`unused type override in deserialization ${ov[0]}: ${ov[1]}`); continue;}
			if (![...selector.options].map(opt => opt.value).includes(ov[1])) {console.warn(`invalid type override in deserialization ${ov[0]}: ${ov[1]}`); continue;}
			selector.value = ov[1];
		}

		newchip.el.querySelector('.chip').remove();
		newchip.el.querySelector('.selUI').prepend(generateChipHTML(newchip.nd, newchip.currentOverrides));
	}

	for (const con of graph.connections) {
		const inputschip = chips[con.i.chipidx];
		if(!inputschip) {console.warn(`invalid connection ${JSON.stringify(con)}`); continue;}
		const input = inputschip.el.querySelector('.input').children[con.i.portidx * 2];
		if(!input) {console.warn(`invalid connection ${JSON.stringify(con)}`); continue;}

		const outputschip = chips[con.o.chipidx];
		if(!outputschip) {console.warn(`invalid connection ${JSON.stringify(con)}`); continue;}
		const output = outputschip.el.querySelector('.output').children[con.o.portidx * 2];
		if(!output) {console.warn(`invalid connection ${JSON.stringify(con)}`); continue;}

		connections.push({
			i: input,
			o: output
		});
	}
}
function loadGraphHandler({graph}) {
	deserializeGraph(graph);
}

var locked = false;

function lockGraph() {
	locked = true;
	document.getElementById('searchbox').remove();
	document.getElementById('helpbox')  .remove();
	document.getElementById('plbox')  .remove();
}

async function getPermalink() {
	const linker = document.getElementById('linktarget');
	const linkerp = document.getElementById('linkprefix');
	const ab = document.getElementById('authorbox');

	const ret = await fetch('https://graphpl.aleteoryx.me/save', {
		method: 'POST',
		body:   JSON.stringify({...serializeGraph(), author: ab.value ? ab.value : undefined})
	}).then(m => m.text())
	console.log(ret);
	linkerp.innerText = "Success! Permalinked at:"
	linker.href      = `https://circuits.aleteoryx.me/grapher/pl#${ret}`
	linker.innerText = `https://circuits.aleteoryx.me/grapher/pl#${ret}`
}

window.onload = async function() {
	graph = document.getElementById("graph");
	searcher = document.getElementById("searcher");
	r2cdata = await fetch(/*"https://raw.githubusercontent.com/tyleo-rec/CircuitsV2Resources/master/misc/circuitsv2.json"/*/"/circuits.json")
				   .then(res => res.json());

	allTypes.push(...ListAllTypes(r2cdata.Nodes).sort((a,b) => (a.toLowerCase() > b.toLowerCase()) ? 1 : -1));

	window.onmessage = function({data}) {
		clean = false
		switch(data.type) {
		case 'newChip':
			newChipHandler(data);
			break;
		case 'loadGraph':
			loadGraphHandler(data);
			break;
		case 'lock':
			lockGraph();
			break;
		}

	}

	function clickToWire(e) {start = performance.now();
			targ = e.target;
			if (e.target.parentElement.matches('.input')) {
				if (!e.target.matches('.exec')) delConnections(e.target);
				mode = 'wire_i-o';
				wirestate = {
					i: e.target,
					o: lastmp
				};
				connections.push(wirestate);
			}

			else if (e.target.parentElement.matches('.output')) {
				if (e.target.matches('.exec')) delConnections(e.target);
				mode = 'wire_o-i';
				wirestate = {
					i: lastmp,
					o: e.target
				};
				connections.push(wirestate);
			}

			else if (targ = (
				() => {
					for (const node of chips) if (node.el.contains(e.target)) return node.el;
					return false;
				})()
			)
				mode = 'drag';

			else if (e.target == graph) switchID(null, 'selected')
			clean = false
	}
	
	graph.addEventListener('mousedown', function(e) {
		if ((e.buttons & 1) && !locked) {
			clickToWire(e);
		}
	});
	graph.addEventListener('touchstart', function(e) {
		if (e.touches.length <= 1) {
			maintouch = e.changedTouches[0];
			clickToWire(e);
		}
	});

	function unclickToWire(e) {
			let newCon;
			switch (mode) {
			case 'wire_i-o':
				connections.pop()
				newCon = {i: wirestate.i, o: e.target}
				if (e.target.matches('.exec')) delConnections(e.target);
				if (e.target.parentElement.matches('.output') && (newCon.i.nextElementSibling.innerText == newCon.o.nextElementSibling.innerText))
					connections.push(newCon);
				break;
			case 'wire_o-i':
				connections.pop()
				newCon = {o: wirestate.o, i: e.target}
				if (!e.target.matches('.exec') && !e.target.matches('#graph')) delConnections(e.target);
				if (e.target.parentElement.matches('.input') && (newCon.i.nextElementSibling.innerText == newCon.o.nextElementSibling.innerText))
					connections.push(newCon);
				break;
			case 'drag':
				if ((performance.now() - start) < 300 && !targ.querySelector('.selUI').matches('#selected')) {
					switchID(targ.children[0], 'selected')
				}
				break;
			}
			mode = null;
			clean = false;
	}
	rootel.addEventListener("mouseup", e => {
		if ((e.button == 0) && !locked) {
			unclickToWire(e);
		}
	});
	graph.addEventListener('touchend', e => {
		if (e.changedTouches[0] == maintouch) {
			maintouch = null;
			unclickToWire(e);
		}
	});

	rootel.addEventListener("mousemove", e => {
		if (e.buttons & 2) {
			mode = '';
			let tmp = connections.pop();
			if ((tmp.i instanceof Element) && (tmp.o instanceof Element)) connections.push(tmp);
			clean = false
		}
		if (e.buttons & 4) {
			graphPos.x += e.clientX - lastmp.x;
			graphPos.y += e.clientY - lastmp.y;
			graph.style.setProperty('--graphOffsetX', graphPos.x);
			graph.style.setProperty('--graphOffsetY', graphPos.y);
			clean = false
		}
		switch (mode) {
		case 'drag':
			let newchipx = Number(targ.style.getPropertyValue('--chipOffsetX')) + e.clientX - lastmp.x;
			let newchipy = Number(targ.style.getPropertyValue('--chipOffsetY')) + e.clientY - lastmp.y;
			targ.style.setProperty('--chipOffsetX', newchipx);
			targ.style.setProperty('--chipOffsetY', newchipy);
			clean = false
			break;
		}
		lastmp.x = e.clientX;
		lastmp.y = e.clientY;
		if (mode) 
			clean = false
	});
	rootel.addEventListener('touchdrag', e => {
		lastmp.x = e.changedTouches[0].clientX;
		lastmp.y = e.changedTouches[0].clientY;
	})


	root.addEventListener("mousemove", e => {
  	root.style.setProperty('--mouse-x', (e.clientX - graphPos.x - searcher.clientWidth) + "px");
  	root.style.setProperty('--mouse-y', (e.clientY - graphPos.y) + "px");
	});

	function deleteSel() {
		let sel = document.getElementById("selected").parentElement;
		if (!sel) return;
		delConnections(sel);
		{
			let tmp = chips.filter(chip => !(chip.el == sel));
			chips.length = 0;
			chips.push(...tmp);
		}
		sel.remove();
		clean = false;
	}

	root.addEventListener("keydown", e => {
		switch (e.code) {
		case 'Delete':
			deleteSel();
			break;
		case 'KeyS':
			console.log(serializeGraph());
			break;
		}
	});


	canvas = document.getElementById('canvas');
	ctx = canvas.getContext('2d');
	canvas.width = window.innerWidth;
	canvas.height = window.innerHeight;

	window.addEventListener("resize", e => {
		canvas.width = window.innerWidth;
		canvas.height = window.innerHeight;
		clean = false
	});

	(async function updateanim(t) {

		if(!clean) { 
		ctx.clearRect(0,0,window.innerWidth,window.innerHeight);
		for(const wire of connections) {
			const points = [];

			for (const point of [wire.o, wire.i]) {
				if (point instanceof Element) {
					let rects = point.getClientRects()[0];
					points.push(point == wire.i ? rects.left - remtopx(1.5) : rects.right + remtopx(1.1));
					points.push((rects.top + rects.bottom) / 2);
				} else {
					points.push(point.x);
					points.push(point.y);
				}
			}

			if ((((points[0] >= -10) && (points[0] <= window.innerWidth)) &&
			     ((points[1] >= -10) && (points[1] <= window.innerHeight))) ||
				  (((points[2] >= -10) && (points[2] <= window.innerWidth)) &&
				   ((points[3] >= -10) && (points[3] <= window.innerHeight)))) {

				var m = null;
				for (const cls of (wire.i instanceof Element ? wire.i : wire.o).classList) m = m || portColors[cls];
				ctx.strokeStyle = m;
				renderCurveBetweenPorts(...points);
			}
		}
		clean = true;
		}
		requestAnimationFrame(updateanim);
	})()

	if(window.opener) opener.postMessage({type: 'grapherLoaded'});
}