aboutsummaryrefslogblamecommitdiffstats
path: root/lib/chips2.js
blob: dae4f53a5b2408749f4294b92d3885a1d7aefcfd (plain) (tree)
1
2
3
4
5
6
                                 

                                         
                                     

                           








                                          


































                                                                      
                                                                                       




























                                                                      
                                                                                       
                                                                                       
         
                                          
                                                 








                                                                                                        


         
                                                









                                                                              
                             
                                                                                                      


         








                                                                                                       


         
                                             
                              
                             



                                  









                                                                   

 
















                                                                    
                                 







                                                                                         

                             


                                       
         
                               
 














                                                                 
 
                                                                                               
















                                                                        





























































                                                                                                  











































































































































































                                                                                                                                                  
                 




















                                                                                    
         












                                                                                        

 

                                      
 



                                                                                                                   
                                          









































                                                                                                                
                                                  
                                        

                                              
                                           

                                   


                                                         





                                             

                                                     
                                                          
                              






                                                     









                                                     


 
 














































































                                                                                                                                         
 
//Gods-awful type-checking logic.
//TypeName: string, Params: Array<string>
function Type(TypeName,Params=[]) {
	if (!Type.all) Type.all = {};
	this.typename = "";
	this.mode = "";
	
	if (TypeName instanceof Type) {
		const t = TypeName.copy();
		for (const k in t) {
			this[k] = t[k];
		}
		return;
	}
	
	//Union/Tuple
	if (/^\(.+\)$/.test(TypeName)) {
		const types = [];
		var depth = 0;
		var workspace = "";
		for (const c of /^\((.+)\)$/.exec(TypeName)[1]) {
			switch (c) {
				case '(':
				case '<':
					depth++;
					break;
				case ')':
				case '>':
					depth--;
					break;
				case ',':
					if (depth == 0) {
						this.mode = "tuple";
						types.push(workspace);
						workspace = "";
						continue;
					}
					break;
				case '|':
					if (depth == 0) {
						this.mode = "union";
						types.push(workspace);
						workspace = "";
						continue;
					}
					break;
			}
			workspace += c;
		}
		if (workspace) types.push(workspace);
		this.template = types.map(t => t.trim()).map(t => new Type(t, Params));
	}
	//Template
	else if (/^[^<]+<.+>$/.test(TypeName)) {
		this.typename = /^([^<]+)</.exec(TypeName)[1];
		this.mode = "templated";
		const types = [];
		var depth = 0;
		var workspace = "";
		for (const c of /^[^<]+<(.+)>$/.exec(TypeName)[1]) {
			switch (c) {
				case '(':
				case '<':
					depth++;
					break;
				case ')':
				case '>':
					depth--;
					break;
				case ',':
					if (depth == 0) {
						types.push(workspace);
						workspace = "";
						continue;
					}
					break;
			}
			workspace += c;
		}
		if (workspace) types.push(workspace);
		this.template = types.map(t => t.trim()).map(t => new Type(t, Params));
		Type.all[this.typename] = () => new Type(TypeName, Array.from(Params));
	}
	//Standard Type, or Template Param
	else if (/^[^()|<>,]+$/.test(TypeName)) {
		if (!Params.includes(TypeName)) {
			this.typename = TypeName;
			this.mode = (TypeName == 'any') ? "any" : "standard";
			if (this.mode == "standard") Type.all[this.typename] = () => new Type(TypeName);
		} else {
			this.mode = "param";
			this.typename = TypeName;
			this.template = Type.any;
		}
	}
}

Type.prototype.toString = function(info=false) {
	switch (this.mode) {
		case "tuple":
			return `(${this.template.join(',')})`;
		case "union":
			return `(${this.template.join('|')})`;
		case "templated":
			return `${this.typename}<${this.template.join(',')}>`;
		case "any":
		case "standard":
			return this.typename;
		case "param":
			return info ? `${this.typename}: ${this.template}` : this.template.toString();
	}
}

Type.prototype._resolve = function(key, val) {
	if (this.mode != "param") {
		if (val === undefined && this.template?.length === 1) {
			val = key;
			key = 0;
		}
		if (this.template?.length) this.template = this.template.map(t => t.resolve(key, val));
	} else {
		if (this.typename == key) this.template = new Type(val)
	}
}

Type.prototype.resolve = function(key, val) {
	const t = this.copy();
	t._resolve(key, val);
	return t;
}

Type.prototype.copy = function() {
	if (this.mode != "param") {
		const t = new Type(this.toString());
		for (const k in this.template)
			t.template[k] = this.template[k].copy();
		return t;
	} else {
		const t = new Type(this.typename, [this.typename]);
		t._resolve(this.typename, this.toString())
		return t;
	}
}

Type.prototype.getClasses = function() {
	if (this.mode == "param") return this.template.getClasses();
	const ret = new Set();
	switch (this.mode) {
		case 'standard':
			switch (this.typename) {
				case 'bool':
				case 'int':
				case 'float':
				case 'string':
					ret.add(this.typename);
					break;
				default:
					ret.add('special');
					break;
			}
			break;
		case 'templated':
			if (this.template.length == 1) {
				if (this.typename == "List") ret.add("list");
				this.template[0].getClasses().forEach(ret.add.bind(ret));
			} else {
				ret.add('any');
			}
			
			break;
		case 'union':
		case 'tuple':
		case 'any':
			ret.add('any');
			break;
	}
	return Array.from(ret);
}
Type.prototype.getParams = function(set=new Set()) {
	switch(this.mode) {
		case 'param':
			set.add(this.typename);
			this.template.getParams(set);
			break;
		case 'templated':
		case 'union':
		case 'tuple':
			this.template.map(t => t.getParams(set));
			break;
	}
	return set;
}


//just makes my life that much easier when writing code. Type.any is nicer than new Type('any')
Type.typeDef = (function(typename, name, params=[]) {
	Object.defineProperty(this, typename, {
		get: () => new Type(name, params)
	})
}).bind(Type)
Type.QuickTD = (function(name, params=[]) {
	Object.defineProperty(this, (new Type(name, params)).typename, {
		get: () => new Type(name, params)
	})
}).bind(Type)

Type.QuickTD("any");
Type.QuickTD("int");
Type.QuickTD("float");
Type.QuickTD("string");
Type.QuickTD("bool");
Type.QuickTD("Vector3");
Type.typeDef("list", "List<T>", ["T"]);

function intersect(type1, type2) {
	const T1 = new Type(type1.toString());
	const T2 = new Type(type2.toString());

	if(T1 == T2) return T1;

	if (T1.mode == "any") return T2;
	if (T2.mode == "any") return T1;
	if (T1.mode == T2.mode) {
		switch (T1.mode) {
			case "standard":
				return (String(T1) == String(T2)) ? T1 : null;
			case "templated":
			case "tuple":
				if (
					T1.typename != T2.typename ||
					T1.template.length != T2.template.length
				) return null;
				var t = T1.copy();
				for (const k in T1.template)
					t.template[k] = intersect(T1.template[k], T2.template[k]);
				if (t.template.includes(null))
					return null;
				else return t;
			case "union":
				const rettempl = Array.from(
					new Set(
						T1.template.map(
							t => T2.template
								.map(t2 => intersect(t,t2))
								.filter(t => t ?? false))
						.flat().map(String)
					)).map(n => new Type(n));
				if (rettempl.length > 1) {
					const ret = T1.copy();
					ret.template = rettempl;
					return ret;
				}
				else if (rettempl.length) return rettempl[0];
				return null
		}
	} else if ([T1.mode,T2.mode].includes("union")) {
		const UT = (T1.mode == "union") ? T1 : T2;
		const ST = (T1.mode == "union") ? T2 : T1;
		const isect = Array.from(
			new Set(
				UT.template
				.map(t => intersect(ST, t))
				.filter(t => t ?? false)
				.map(String)
			)).map(n => new Type(n));
		if (isect.length > 1) {
			const ret = UT.copy();
			ret.template = isect;
			return ret;
		}
		else if (isect.length) return isect[0];
	}
	return null;
}
function walkDownParams(t, tres) {
	let ret = {};
	if (t.mode == "param") {
		ret = {[t.typename]: tres};
	}
	else if (
		t.mode == tres.mode &&
		t.typename == tres.typename &&
		t.template?.length == tres.template?.length
	) {
		for (const p in t.template)
			ret = {...ret, ...walkDownParams(t.template[p], tres.template[p])};
	}
	return ret;
}

function Port(name, type, parent, input) {
	//basics
	this.parent = parent;
	this.name = name;
	//what have we here
	this.base = (type == "exec") ? "exec" : parent.passed.reduce((t, k) => t.resolve(k, parent.params[k]), new Type(type, parent.passed));
	this.resolve()
	//what does it do
	this.conn = input ? null : new Set();
	this.side = input ? "in" : "out";
	//enter your mommy's SSN
	this.id = crypto.randomUUID();
	Port.ports.set(this.id, this);
	//on TV!
	this._el = {};
	Object.defineProperty(this, 'el', {get: () => this._el, set: val => {this._el.id = ''; this._el = val; val.id = this.id}})
}
Port.ports = new Map();
Port.prototype.resolve = function () {
	this.type =
		(this.base == "exec") ?
		"exec" :
		this.parent.passed.reduce((t, k) => t.resolve(k, this.parent.params[k]), new Type(this.base, this.parent.passed));
}
Port.prototype.getIDStr = function () {
	return `${this.parent.id}/${this.side}/${this.id}`;
}
Port.getByID = function(str) {
	const [id, side, id2] = str.split('/');
	if (
		id !== undefined &&
		side !== undefined &&
		id2 !== undefined
	) {
		const side = Chip.chips.get(id)[side+'s'];
		return side.filter(p => p.id == id2)[0];
	}
}
Port.prototype.remove = function(cb = false) {
	if (this.side == "in")
		unwireDataIn(this);
	else
		unwireDataOut(this);

	if (cb) this.parent[this.side + 's'] = this.parent[this.side + 's'].filter(p => p != this);

	Port.ports.delete(this.id);
}


function paramRecurse(ports, portCB, cancelCB=()=>false, seenchips=new Set()) {
	for (const p of ports) {
		if (cancelCB(p)) return;
		const c = p.parent;
		if (seenchips.has(c)) continue;
		portCB(p);
		seenchips.add(c);
		const params = p.base.getParams();
		if (!params.size) continue;
		paramRecurse([...c.ins, ...c.outs].filter(fp => {
			for (const par of fp.base.getParams())
				if (params.has(par)) return true;
			return false;
		}).flatMap(p => (p.conn instanceof Set) ? [...p.conn] : p.conn).filter(p => p), portCB, cancelCB, seenchips);
	}
	return seenchips;
}

//TODO:
//Please someone other than me go in and optimize this absolute shitshow.
//There are better ways than this to write code but I am too tired to write them.
//Of the things for future me, or some kind soul:
//  refactor this to only do one pass of the graph.
//  cache things like getParams() and toString() on first call.
//  do something clever to progressively update the types for everything. maybe make related wires share a reference to an object and mutate that.
//Sorry in advance to all people on potato computers trying to run this slow piece of junk

//future me here, tbh its not too bad and modern js zooms. my perf tests have put me at ease.
//it ciuld be faster, but it should be good for normal use
function regenTypes(ports, base=Type.any) {
	if (ports instanceof Port) ports = [ports];
	for (const port of ports)
		if (!(port instanceof Port)) throw new TypeError("Can only regen Ports!");
	
	//step 1: recursively search for any issues with typing.
	//  1A: get all connected chips - Done, see paramRecurse
	//  1B: only get chips that share a chain of params. - Done, see paramRecurse
	//  1C: aggregate types as you go on. - Done
	//  1D: fail if null - Done
	const basechips = [...ports];
	let finaltype = base;
	const sc = paramRecurse(
		basechips,
		p => {finaltype = intersect(finaltype, p.base)},
		() => finaltype==null	
	);
	if (finaltype == null) return false;
	
	for (const chip of sc)
		chip.root.append(`seen! ${finaltype}`);
	
	//step 2: recursively set all relevant type params, regen UI
	paramRecurse(basechips, p => {
		let type = finaltype.copy();
		let sect = walkDownParams(p.base, type);
		console.log(sect);
		p.parent.setParams(sect);
		p.parent.regenChip();
	});
	
	//step 3: cleanup, clear markers

	return finaltype;
	
}
function wireData(port1, port2) {
	//step 1: check things
	//  1.0:  both data?
	if (!(port1.base instanceof Type && port2.base instanceof Type)) return false;
	
	//  1.1:  same side?
	if (port1.side == port2.side) return false;
	
	//  1.2:  already connected?
	const [input, output] = port1.side == "in" ? [port1, port2] : [port2, port1];
	if (output.conn.has(input)) return true;
	
	//  1.3:  disconnect input
	let oldconn;
	if (input.conn != null) {
		oldconn = input.conn;
		input.conn = null;
		oldconn.conn.delete(input)
	}
	
	//step 2: do the actual stuff
	//  2A: they're identical
	if (input.type.toString() == output.type.toString()) {
	//    2A.1:  make the connections
		output.conn.add(input);
		input.conn = output;
		Chip.regenconnections();
		return true;
	}
	
	//  2B: they're different, one+ has params
	if (output.base.getParams().size + input.base.getParams().size) {
	//    2B.1:  make the connections temporarily
		output.conn.add(input);
		input.conn = output;
	//    2B.2:  try recursing down them
		const type = regenTypes(input);
	//    2B.3:  rollback if necessarry
		if(!type) {
			unwireDataIn(input);
			if (oldconn) wireData(input, oldconn);
		}
		Chip.regenconnections();
		return true;
	}
	
	//  2C: they're different
	//    2C.1:  rollback everything
	if (oldconn) wireData(input, oldconn);
	return false;
	
	//step 3: final bits
	//  2.1:  regen connections
	Chip.regenconnections();
}
function unwireDataIn(input, regen=true) {
	const output = input.conn;
	if (output == null) return null;
	input.conn = null;
	output.conn.delete(input);
	if(regen) {
		const regens = [input, output].filter(p => p.base.getParams().size);
		regens.map(r => regenTypes(r));
	}
	Chip.regenconnections();
}
function unwireDataOut(output, regen=true) {
	const inputs = [...output.conn];
	if (inputs.length == 0) return [];
	output.conn.clear();
	for (const i of inputs)
		i.conn = null;
	if(regen) {
		const regens = [...inputs, output].filter(p => p.base.getParams().size);
		regens.map(r => regenTypes(r));
	}
	Chip.regenconnections();
}

function Chip(Entry, NodeDesc, el) {
	this.id = crypto.randomUUID();

	this.root = el;

	const cur = NodeDesc;
	const params = Object.fromEntries(Object.entries(cur.ReadonlyTypeParams).map(([n,t]) => [n, new Type(t)]));
	const passed = Object.keys(params)

	this.chip = Entry;
	this.nd = cur;
	this.defparams = {...params};
	this.params = {...params};
	this.passed = Object.keys(params);

	const portToType = (input, idx) => ((port) => new Port(port.Name, port.ReadonlyType, this, input, idx));
	let i = 0;
	this.ins  = cur.Inputs.map(portToType(true, i++));
	i = 0;
	this.outs = cur.Outputs.map(portToType(false, i++));

	this.regenChip();
	//this.regenUI();
	Chip.chips.set(this.id, this);
}
Chip.chips = new Map(); // list that chips register themselves into when constructed.
Chip.connections = new Set();
Chip.regenconnections = function() {
	Chip.connections.clear();
	for (const chip of Chip.chips.values()) {
		for (const input of chip.ins)
			if (input.type != "exec" && input.conn)
				Chip.connections.add(`${input.conn.id} => ${input.id}`);
		for (const output of chip.outs)
			if (output.type == "exec" && output.conn)
				Chip.connections.add(`${output.id} => ${output.conn.id}`);
	}
}

Chip.prototype.setParams = function (params) {
	for (const p of Object.keys(params).filter(p => this.passed.includes(p)))
		this.params[p] = params[p];
	for (const port of [...this.ins, ...this.outs])
		port.resolve();
}

Chip.prototype.regenChip = function () {
	this.root.querySelector(".chip")?.remove();
	const chip = newEl('div', 'chip');
	chip.id = this.id;
	const header = newEl('div', 'chipheader');
	header.innerText = this.nd.Name;
	const input = newEl('div', 'input');
	const output = newEl('div', 'output');
	chip.append(header, input, output);
	const genPort = (port) => {
		let classes,name;
		if (port.type != "exec") {
			classes = port.type.getClasses();
			name = port.type.toString();
		} else {
			classes = ["exec"];
			name = "exec";
		}
		
		const ret = newEl('div', '');
		ret.id = port.id
		ret.innerHTML = port.name + "&nbsp;";
		classes.forEach(p => ret.classList.add(p))
		port.el = ret;

		const tooltip = newEl('div', 'type');
		tooltip.innerText = name;

		return [ret, tooltip];
	}

	input.append(...this.ins.flatMap(genPort));
	output.append(...this.outs.flatMap(genPort));
	this.root.prepend(chip);
}
Chip.prototype.remove = function() {
	for (const p of [...this.ins, ...this.outs])
		p.remove();
	Chip.chips.delete(this.id);
	this.root.remove();
	//regenconnections
}



//chips.js reimplemented with the above

let root = document.documentElement;
if (window.location.pathname != '/grapher/') {
	root.addEventListener("mousemove", e => {
  	root.style.setProperty('--mouse-x', e.clientX + "px");
  	root.style.setProperty('--mouse-y', e.clientY + "px");
	});
}

const portColors = {
	float:   '#186adc',
	int:     '#0f6522',
	exec:    '#f55b18',
	string:  '#794284',
	bool:    '#ea2e50',
	any:     '#f6eee8',
	special: '#f4c61e'
}
const	typeRegex = /(?:^|(?<=<))(?:int|float|bool|string|exec)(?:$|(?=>))/;
const unionRegex = /^T\d*$|(?<=List<)T\d*(?=>)/;

function computeType(tn, TypeParams, to) {
	if (tn != "exec") {
		const t = Object.entries({...TypeParams, ...to}).reduce((t,i) => t.resolve(...i), new Type(tn, Object.keys(TypeParams)));
		return {typeclass: t.getClasses(), type: t.toString()};
	} else {
		return {typeclass: ['exec'], type: 'exec'};
	}
}

//keeping this the same for now
function generateChipHTML(NodeDescs, typeoverride = undefined) {
	for (let cur of NodeDescs) {
		let ins = cur.Inputs;
		let outs = cur.Outputs;

		const root = newEl('div', 'chip');
		const header = newEl('div', 'chipheader');
		header.innerText = cur.Name;
		const input = newEl('div', 'input');
		const output = newEl('div', 'output');
		root.append(header, input, output);

		for (const inp of ins) {
			//work out the type
			let {typeclass, type} = computeType(inp.ReadonlyType, cur.ReadonlyTypeParams, typeoverride);

			const port = newEl('div', '');
			port.innerHTML = inp.Name + "&nbsp;";
			typeclass.map(port.classList.add.bind(port.classList));

			const tooltip = newEl('div', 'type');
			tooltip.innerText = type;

			input.append(port, tooltip);
		}

		for (const out of outs) {
			//work out the type
			let {typeclass, type} = computeType(out.ReadonlyType, cur.ReadonlyTypeParams, typeoverride);

			const port = newEl('div', '');
			port.innerHTML = out.Name + "&nbsp;";
			typeclass.map(port.classList.add.bind(port.classList));

			const tooltip = newEl('div', 'type');
			tooltip.innerText = type;

			output.append(port, tooltip);
		}

		return root;
	}

}

function ListAllTypes(Nodes) {
	return Object.keys(Type.all);
}