Module:ColinTheCat/ProtoFlux

From Resonite Wiki
Example Name
Input1
Output1
Input2
Output2
Global1
null
Global2
null
Example Category
Example Name
Input1
Output1
Input2
Output2
+
-
+
-
Example Category
Impulse Multiplexer
*
Impulses
Index
+
-
Flow

-- Package definition to return to Scribunto - this allows us to define methods that can be called
-- when this module is targeted.
local p = {}

local ProtofluxColor = require("Module:ProtoFlux_Type_Color")

local css = {
	root = "color: rgb(224, 224, 224); background-color: rgb(18, 20, 28); box-shadow: 0 2px 4px 2px #0005; "
		.. "width: 256px; display: flex; flex-direction: column; align-items: stretch;",
	node_title = "padding: 10px 0px 10px 0px; text-align: center; font-weight: bold; background-color: rgb(26, 41, 54); font-size: 18pt; flex-grow: 1;",
	node_footer = "text-align: center; padding: 10px; font-size: 18pt; color: rgb(64,64,64); font-weight: bold;",
	connector_row = "display: flex; min-height: 70px;",
	connector_row_labels = "flex-grow: 2; overflow: visible; display: flex; flex-direction: column; justify-content: space-between; order: 2;",
	multi_button = "font-size: 1.75rem; width: 1em; height: 1em; background-color: #2c2f35; border-radius: 2em; "
		.. "display:flex; align-items:center; justify-content: center;",
	multi_bar = function(self, is_input, multi)
		return string.format(
			"background-color: white; opacity: 0.5; width: 6px; height: %spx; position: absolute; bottom: 40px; %s: 13px;",
			70 * (multi - 2) + 50,
			is_input and "left" or "right"
		)
	end,
	label = "text-overflow: ellipsis; border-bottom: 4px solid #11151d; border-top: 4px solid #11151d; ",
	label_multi = "font-weight: bold; font-size: 1.4rem; line-height: 1;",
	input_label = function(self, color, is_multi)
		if is_multi then color = nil end
		return string.format(
			self.label
			.."text-align: left; overflow: %s; padding-left: 4px; border-right: 20px solid #11151d; "
			.."background-color: %s;%s",
			color and "hidden" or "visible",
			color and ColorToCss(MultA(color, 0.6)) or "transparent",
			is_multi and " "..self.label_multi or ""
		)
	end,
	output_label = function(self, color, is_multi)
		if is_multi then color = nil end
		return string.format(
			self.label
			.."text-align: right; overflow: %s; padding-right: 4px; border-left: 20px solid #11151d; "
			.."background-color: %s;%s",
			color and "hidden" or "visible",
			color and ColorToCss(MultA(color, 0.6)) or "transparent",
			is_multi and " "..self.label_multi or ""
		)
	end,
	attachment_point = function(self, color, order)
		return string.format(
			"width: 30px; background-color: black; fill: %s; stroke: %s; order: %s;",
			ColorToCss(MultA(color, 0.15)),
			ColorToCss(color),
			order
		)
	end,
	global = function(self, color)
		return string.format(
			"display: flex; min-height: 70px; flex-direction: column; border-left: 10px solid %s;",
			ColorToCss(color)
		)
	end,
	global_input = "border-radius: 16px; text-align: center; display: flex; flex-direction: column; justify-content: center;"
}

--- Method that generates the HTML output
--	@param frame A Scribunto frame instance. frame.args contains the parameters passed into the module call
function p.GenerateUI( frame )
	-- Parse the JSON input, returning an empty array if no argument was passed
	local inputs = mw.text.jsonDecode(frame.args.Inputs or '[]')
	local outputs = mw.text.jsonDecode(frame.args.Outputs or '[]')
	local globals = mw.text.jsonDecode(frame.args.Globals or '[]')
	
	-- Create an HTML div element to contain our node UI
	local protofluxContainer = mw.html.create( 'div' )
	protofluxContainer
	    :attr('class', frame.args.Inline and '' or 'floatright')
		:cssText(css.root)
		:tag('div') -- HTML div to contain the node title
			:cssText(css.node_title)
			:wikitext(frame.args.Name)
			:done() -- Close node title div
	
	local processedInputs = {}
	local processedOutputs = {}
	
	for i=1,#inputs do
		local multi = tonumber(inputs[i] and inputs[i].Multi or 1)
		for j=1,multi do
			inputs[i].MultiIndex = j
			table.insert(processedInputs, { Name=inputs[i].Name, Type=inputs[i].Type, Multi=multi, MultiIndex=j })
		end
	end
	
	for i=1,#outputs do
		local multi = tonumber(outputs[i] and outputs[i].Multi or 1)
		for j=1,multi do
			table.insert(processedOutputs, { Name=outputs[i].Name, Type=outputs[i].Type, Multi=multi, MultiIndex=j })
		end
	end
	
	-- Calculate the larger number of rows required (either for inputs or outputs, if the node is asymmetric)
	local maxRows = math.max(#processedInputs, #processedOutputs)
	
	
	-- Iterate over each row, and populate it with the inputs/outputs. If the node is asymmetric,
	-- the value passed for either input or output might be nil.
	
	for i=1,maxRows do
		CreateConnectorRow(protofluxContainer, processedInputs[i], processedOutputs[i])
	end
	
	-- Iterate over each global value in the node, and create a row. These elements take up the entire width
	-- of the node, and so don't need to be balanced in any way.
	for i=1,#globals do
		CreateGlobalsRow(protofluxContainer, globals[i])
	end
	
	protofluxContainer
		:tag('div') -- HTML div to contain the node category footer
			:cssText(css.node_footer)
			:wikitext(frame.args.Category)
			:done() -- Close category footer div
	
	-- Return the HTML generated above to the wiki page this script is invoked from.
	return tostring(protofluxContainer) .. '[[Category:ProtoFlux:All]]'
end

--- Creates a text which should be shown when hovering over a connector
-- @param Input Table containing a Name (optional) and Type for the input on this row.
function connector_hover_text(connector)
	if connector.Name == nil then
		return ("<%s>"):format(connector.Type)
	end
	return ("%s <%s>"):format(connector.Name, connector.Type)
end

--- Creates a new ProtoFlux connector row in the output
-- @param container The Scribunto HTML node we'll be creating this row inside of. Should be a container of some sort.
-- @param input Table containing a Name and Type for the input on this row. Can be nil if no input is to be placed on this row.
-- @param output Table containing a Name and Type for the output on this row. Can be nil if no output is to be placed on this row.
function CreateConnectorRow(container, input, output)
	local connector_row = container
		:tag('div') -- HTML div to contain the connector row. We specify a min-height of 70px for consistency.
			:cssText(css.connector_row)

	local connector_row_labels = connector_row
		:tag('div') -- HTML div to contain the input and output labels in the connector row
			:cssText(css.connector_row_labels)

	if input then
		local color, is_impulse = GetColor(input)
		local hover_text = connector_hover_text(input)

		if input.Multi and input.MultiIndex > 1 then
			local label = connector_row_labels
				:tag('div') -- HTML div to contain the input label
					:attr('title', hover_text) -- Add a basic mouseover description
					:cssText(css:input_label())
			if input.Multi == input.MultiIndex then
				label
					:tag('div')
						:cssText('position:relative; top:2rem; display: flex; gap: 4px;')
						:tag('div')
							:cssText(css.multi_button)
							:wikitext('+')
							:done()
						:tag('div')
							:cssText(css.multi_button)
							:wikitext('-')
							:done()
						:tag('div')
							:cssText(css:multi_bar(true, input.Multi))
			end
		else
			connector_row_labels
				:tag('div') -- HTML div to contain the input label
					:attr('title', hover_text) -- Add a basic mouseover description
					:cssText(css:input_label(color, input.Multi > 1))
					:wikitext(input.Name or '')
					:done() -- Close input label div
		end

		-- Create the input (left) attachement point
		local segments = GetTypeSegments(input)
		CreateConnectorAttachmentPoint(connector_row, hover_text, color, segments, is_impulse, true)
	else -- if input
		connector_row:tag("div"):cssText("width: 30px; order: 1;")
		connector_row_labels:tag("div")
	end -- if input

	if output then
		local color, is_impulse = GetColor(output)
		local hover_text = connector_hover_text(output)

		if output.Multi and output.MultiIndex > 1 then
			local label = connector_row_labels
				:tag('div') -- HTML div to contain the output label
					:attr('title', hover_text) -- Add a basic mouseover description
					:cssText(css:output_label())
			if output.Multi == output.MultiIndex then
				label
					:tag('div')
						:cssText('position:relative; display: flex; justify-content: flex-end; gap: 4px;')
						:tag('div')
							:cssText(css.multi_button)
							:wikitext('+')
							:done()
						:tag('div')
							:cssText(css.multi_button)
							:wikitext('-')
							:done()
						:tag('div')
							:cssText(css:multi_bar(false, output.Multi))
			end
		else
			connector_row_labels
				:tag('div') -- HTML div to contain the output label
					:attr('title', hover_text) -- Add a basic mouseover description
					:cssText(css:output_label(color, output.Multi > 1))
					:wikitext(output.Name or '')
					:done() -- Close output label div
		end

		-- Create the output (right) attachment point
		local segments = GetTypeSegments(output)
		CreateConnectorAttachmentPoint(connector_row, hover_text, color, segments, is_impulse, false);
	else -- if output
		connector_row:tag("div"):cssText("width: 30px; order: 3;")
	end -- if output
end

--- Creates a new ProtoFlux global input field row in the output
--	@param container The Scribunto HTML node we'll be creating this row inside of. Should be a container of some sort.
--	@param global Table containing a Name and Type for the global field on this row.
function CreateGlobalsRow(container, global)
	local color = GetTypeColor(global)
	local hover_text = connector_hover_text(global)

	container
		:tag( 'div' )
			:attr('title', hover_text) -- Add a basic mouseover description
			:cssText(css:global(color))
			:tag( 'div' )
				:cssText('display: flex; flex-direction: row; align-items: center; flex-grow:1; overflow: hidden;')
				:tag( 'span' )
					:cssText('text-align: center; overflow: hidden; text-overflow: ellipsis; font-size: 14pt; font-weight: bold; flex-grow:1;')
					:wikitext(global.Name)
					:done()
				:done()
			:tag( 'div' )
				:attr('style', 'display: flex; gap: 10px; flex-grow: 1;')
				:tag('div')
					:cssText('background-color: #777; font-style: italic; flex-grow: 3; '..css.global_input)
					:tag( 'span' )
						:wikitext('null')
						:done()
					:done()
				:tag( 'div' )
					:cssText('background-color: #333; flex-grow: 1; '..css.global_input)
					:tag ( 'span' )
						:wikitext('∅')   
end

--- Creates a new input or output attachement point for a connector row.
--	@param container The Scribunto HTML node we'll be creating this row inside of. Should be a container of some sort.
--  @param hover_text Text to show when hovering, possibly nil
--	@param color The color of this attachment point (table with r, g, b, optionally a).
--	@param segments The number of segments for the type, used for vectors or matrices.
--	@param is_impulse True if this is an impulse, false if it is a data type.
--	@param is_input True if this is an input side attachment point, false if it is an output side attachment point.
function CreateConnectorAttachmentPoint(container, hover_text, color, segments, is_impulse, is_input)
	local connector_type = nil
	if is_impulse then connector_type = "InputArrow"
	elseif is_input then connector_type = "InputBox"
	else connector_type = "OutputBox"
	end

	container
		:tag( 'div' )
			:attr('title', hover_text or "") -- Add a basic mouseover description
			:cssText(css:attachment_point(color, is_input and 1 or 3))
			:wikitext(mw.getCurrentFrame():expandTemplate({title = 'ProtoFluxConnector', args = {Type=connector_type, Segments=segments}}))
end

--- Returns a CSS color for the data type or impulse and whether the type is an impulse
-- @param connector Table containing a Name (Optional) and Type (Required) for the input/output.
function GetColor(connector)
	if connector == nil then error("connector is nil") end
	if connector == nil or connector.Type == nil then error("Missing Type") end
	local color = ProtofluxColor.get_impulse_color(connector.Type)
	if color ~= nil then
		return color, true
	end
	return GetTypeColor(connector), false
end

--- Returns a CSS color for the data type
-- @param connector Table containing a Name (Optional) and Type (Required) for the input/output or global.
function GetTypeColor(connector)
	if connector == nil then error("connector is nil") end
	if connector == nil or connector.Type == nil then error("Missing Type") end
	local color = ProtofluxColor.get_type_color(connector.Type)
	return MultRGB(color, 1.5)
end

function MultRGB(color, fact)
	return {
		r=color.r*fact,
		g=color.g*fact,
		b=color.b*fact,
		a=color.a,
	}
end

function MultA(color, fact)
	return {
		r=color.r,
		g=color.g,
		b=color.b,
		a=(color.a or 1)*fact,
	}
end

function ColorToCss(color)
	return ("rgba(%s,%s,%s,%s)"):format(color.r*255, color.g*255, color.b*255, color.a or 1)
end

local type_segments = {
	enabled = {
		bool=true,
		byte=true,
		sbyte=true,
		ushort=true,
		short=true,
		uint=true,
		int=true,
		ulong=true,
		long=true,
		float=true,
		double=true,
		decimal=true,
	},
	suffix = {
		["2"]=2,
		["3"]=3,
		["4"]=4,
		--["2x2"]=2,
		--["3x3"]=3,
		--["4x4"]=4,
	},
}


--- Returns a CSS color for the data type
-- @param connector Table containing a Name (Optional) and Type (Required) for the input/output.
function GetTypeSegments(connector)
	if connector == nil then error("connector is nil") end
	if connector == nil or connector.Type == nil then error("Missing Type") end
	local base = connector.Type:match("^%a+")
	if not type_segments.enabled[base] then return 1 end
	local suffix = connector.Type:sub(#base+1)
	return type_segments.suffix[suffix] or 1
end

return p