Module:Test: Difference between revisions

From Resonite Wiki
mNo edit summary
mNo edit summary
 
(32 intermediate revisions by the same user not shown)
Line 3: Line 3:
local p = {}
local p = {}


-- Type colors - RGB values to be included in the HTML output when a color is needed. This will be replaced with CSS at some point.
local ProtofluxColor = require("Module:ProtoFlux_Type_Color")
local typeColor =  
 
{
local css = {
User = '255, 128, 255',
root = function(self, rows)
Impulse = '179, 255, 255',
return string.format("color: rgb(224, 224, 224); background-color: rgb(18, 20, 28); width: 256px; display: grid; grid-template-columns: [input] 30px [label] 1fr [output] 30px [end]; grid-template-rows: 80px repeat(%s, 35px 35px) 60px;",
bool = '115, 115, 115',
rows)
AsyncImpulse = '204, 179, 255',
end,
String = '245, 31, 31',
node_title = function(self, minimal, rows)
Dummy = '255, 0, 255',
if minimal then
IFormatProvider = '168, 143, 214',
return string.format("text-align:center; font-weight: bold; font-size: 18pt;  grid-column: label; grid-row: 1/%s; align-self: center;",
float = '0, 255, 255',
(rows * 2) + 3)
ColorProfile = '255, 196, 54',
else
colorX = '255, 89, 0',
return "padding: 10px 0px 10px 0px; text-align:center; font-weight: bold; background-color: rgb(26, 41, 54); font-size: 18pt;  grid-column: input / end; grid-row: 1;"
Component = '112, 76, 85',
end
float3 = '0, 255, 255',
end,
float2 = '0, 255, 255',
node_footer = function(self, rows)
    ulong = '0, 255, 127',
return string.format("text-align: center; padding: 10px; font-size: 18pt; color: rgb(64,64,64); font-weight: bold; grid-column: label; grid-row: %s; align-self: center;",
    int = '0, 255, 0',
(rows * 2) + 3)
    Continuation = '255, 255, 178',
end,
    Slot = '191, 255, 127',
connector_row = "display: flex; min-height: 70px;",
WebsocketClient = '191, 242, 176'
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, row)
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; grid-column:label; grid-row: %s",
color and "hidden" or "visible",
color and ColorToCss(MultA(color, 0.6)) or "transparent",
is_multi and " "..self.label_multi or "",
row * 2
)
end,
output_label = function(self, color, is_multi, row)
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; grid-column: label; grid-row: %s;",
color and "hidden" or "visible",
color and ColorToCss(MultA(color, 0.6)) or "transparent",
is_multi and " "..self.label_multi or "",
(row * 2) + 1
)
end,
attachment_point = function(self, color, order, row)
return string.format(
"width: 30px; background-color: black; fill: %s; stroke: %s; order: %s; grid-row: %s/%s;",
ColorToCss(MultA(color, 0.15)),
ColorToCss(color),
order,
row * 2,
(row * 2) + 2
)
end,
global = function(self, color, row)
return string.format(
"display: flex; min-height: 70px; flex-direction: column; border-left: 10px solid %s; grid-column: input/end; grid-row:%s/%s;",
ColorToCss(color),
row * 2,
(row * 2) + 2
)
end,
global_input = "border-radius: 16px; text-align: center; display: flex; flex-direction: column; justify-content: center;"
}
}


Line 33: Line 87:
local outputs = mw.text.jsonDecode(frame.args.Outputs or '[]')
local outputs = mw.text.jsonDecode(frame.args.Outputs or '[]')
local globals = mw.text.jsonDecode(frame.args.Globals or '[]')
local globals = mw.text.jsonDecode(frame.args.Globals or '[]')
local minimal = frame.args.Minimal
local processedInputs = {}
local processedInputs = {}
local processedOutputs = {}
local processedOutputs = {}
for i=1,#inputs do
for i=1,#inputs do
for j=1,(inputs and inputs[i].Multi or 1) do
local multi = tonumber(inputs[i] and inputs[i].Multi or 1)
for j=1,multi do
inputs[i].MultiIndex = j
inputs[i].MultiIndex = j
table.insert(processedInputs, { Name=inputs[i].Name, Type=inputs[i].Type, Multi=inputs[i].Multi, MultiIndex=j })
table.insert(processedInputs, { Name=inputs[i].Name, Type=inputs[i].Type, Multi=multi, MultiIndex=j })
end
end
end
end
for i=1,#outputs do
for i=1,#outputs do
for j=1,(outputs and outputs[i].Multi or 1) do
local multi = tonumber(outputs[i] and outputs[i].Multi or 1)
table.insert(processedOutputs, { Name=outputs[i].Name, Type=outputs[i].Type, Multi=outputs[i].Multi, MultiIndex=j })
for j=1,multi do
table.insert(processedOutputs, { Name=outputs[i].Name, Type=outputs[i].Type, Multi=multi, MultiIndex=j })
end
end
end
end
Line 53: Line 110:
local maxRows = math.max(#processedInputs, #processedOutputs)
local maxRows = math.max(#processedInputs, #processedOutputs)
-- Create an HTML div element to contain our node UI
-- Create an HTML div element to contain our node UI
local protofluxContainer = mw.html.create( 'div' )
local protofluxContainer = mw.html.create( 'div' )
protofluxContainer
protofluxContainer
    :attr('class', frame.args.Inline and '' or 'floatright')
    :attr('class', frame.args.Inline and '' or 'floatright')
:cssText('color: rgb(224, 224, 224); background-color: rgb(18, 20, 28); width: 256px; display: grid; grid-template-columns: [input] 30px [label] 1fr [output] 30px; grid-template-rows: 80px repeat(' .. maxRows ..', 35px 35px) 60px;')
:cssText(css:root(maxRows + #globals))
if not minimal then
:tag('div') -- HTML div to contain the node title
protofluxContainer
:cssText(css:node_title(mimimal, maxRows))
:tag('div') -- HTML div to contain the node title
:wikitext(frame.args.Name)
:cssText('padding: 10px 0px 10px 0px; text-align:center; font-weight: bold; background-color: rgb(26, 41, 54); font-size: 18pt;  grid-column: input / output; grid-row: 1;')
:done() -- Close node title div
:wikitext(frame.args.Name)
:done() -- Close node title div
else
protofluxContainer
:tag('div') -- HTML div to contain the node title
:cssText('text-align:center; font-weight: bold; font-size: 18pt;  grid-column: input / output; grid-row: 1/'.. ((maxRows - 1) * 2) + 3 ..'; align-self: center;')
:wikitext(frame.args.Name)
:done() -- Close node title div
end
-- Iterate over each row, and populate it with the inputs/outputs. If the node is asymmetric,
-- 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.
-- the value passed for either input or output might be nil.
for i=1,maxRows do
for i=1,maxRows do
CreateConnectorRow(i, protofluxContainer, processedInputs[i], processedOutputs[i], minimal)
CreateConnectorRow(protofluxContainer, processedInputs[i], processedOutputs[i], minimal, i)
end
end
Line 81: Line 130:
-- of the node, and so don't need to be balanced in any way.
-- of the node, and so don't need to be balanced in any way.
for i=1,#globals do
for i=1,#globals do
--CreateGlobalsRow(protofluxContainer, globals[i])
CreateGlobalsRow(protofluxContainer, globals[i], i + maxRows)
end
end
protofluxContainer
protofluxContainer
:tag('div') -- HTML div to contain the node category footer
:tag('div') -- HTML div to contain the node category footer
:cssText('text-align: center; padding: 10px; font-size: 18pt; color: rgb(64,64,64); font-weight: bold; grid-row:' .. ((maxRows) * 2 + 1) .. '; grid-column: input/output')
:cssText(css:node_footer(maxRows + #globals))
:wikitext(frame.args.Category)
:wikitext(frame.args.Category)
:done() -- Close category footer div
:done() -- Close category footer div
Line 92: Line 141:
-- Return the HTML generated above to the wiki page this script is invoked from.
-- Return the HTML generated above to the wiki page this script is invoked from.
return tostring(protofluxContainer) .. '[[Category:ProtoFlux:All]]'
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
end


--- Creates a new ProtoFlux connector row in the output
--- 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 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 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.
-- @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(Index, Container, Input, Output, Minimal)
function CreateConnectorRow(container, input, output, minimal, row)


if input then
-- Create the input (left) attachement point
local color, is_impulse = GetColor(input)
CreateConnectorAttachmentPoint(Index, Container, Input, true)
local hover_text = connector_hover_text(input)
 
-- Create the input (left) attachement point
local c = Container
local segments = GetTypeSegments(input)
if Minimal then
CreateConnectorAttachmentPoint(container, hover_text, color, segments, is_impulse, true, row)
else
if Input and Input.Multi and Input.MultiIndex == 1 then
if input.Multi and input.MultiIndex > 1 then
c
local label = container
:tag('div') -- HTML div to contain the input label
:attr('title', Input and (Input.Name .. ' <' .. Input.Type .. '>') or '') -- Add a basic mouseover description
:cssText('text-align: left; overflow: hidden; text-overflow: ellipsis; padding-left: 4px;  border-right: 20px solid #11151d; border-bottom: 4px solid #11151d; border-top: 4px solid #11151d; background-color: transparent;')
:wikitext(Input and Input.Name or '' )
:done() -- Close input label div
elseif Input and Input.Multi and Input.Multi == Input.MultiIndex  then
c
:tag('div') -- HTML div to contain the input label
:tag('div') -- HTML div to contain the input label
:attr('title', Input and (Input.Name .. ' <' .. Input.Type .. '>') or '') -- Add a basic mouseover description
:attr('title', hover_text) -- Add a basic mouseover description
:cssText('display:flex; gap: 4px; grid-column: label; grid-row: ' .. Index .. ';')
:cssText(css:input_label(nil, nil, row))
:tag('div')
if input.Multi == input.MultiIndex then
:cssText('font-size: 1.75rem; width: 1em; height: 1em; background-color:grey; border-radius: 2em; display:flex; align-items:center; justify-content:center;')
label
:wikitext('+')
:done()
:tag('div')
:tag('div')
:cssText('font-size: 1.75rem; width: 1em; height: 1em; background-color:grey; border-radius: 2em; display:flex; align-items:center; justify-content:center;')
:cssText('position:relative; top:2rem; display: flex; gap: 4px;')
:wikitext('-')
:tag('div')
:done()
: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
else
c
container
:tag('div') -- HTML div to contain the input label
:tag('div') -- HTML div to contain the input label
:attr('title', Input and (Input.Name .. ' <' .. Input.Type .. '>') or '') -- Add a basic mouseover description
:attr('title', hover_text) -- Add a basic mouseover description
:cssText('text-align: left; overflow: hidden; text-overflow: ellipsis; padding-left: 4px;  border-right: 20px solid #11151d; border-bottom: 4px solid #11151d; border-top: 4px solid #11151d; background-color:' .. GetTypeColor(Input, 0.6) .. '; grid-column: label; grid-row: ' .. Index .. ';')
:cssText(css:input_label(color, input.Multi > 1, row))
:wikitext(Input and Input.Name or '' )
:wikitext(input.Name or '')
:done() -- Close input label div
:done() -- Close input label div
end
end
if Output and Output.Multi and Output.MultiIndex == 1 then
end -- if input
c
 
:tag('div') -- HTML div to contain the output label
if output then
:attr('title', Output and (Output.Name .. ' <' .. Output.Type .. '>') or '') -- Add a basic mouseover description
local color, is_impulse = GetColor(output)
:cssText('text-align: right; overflow: hidden; text-overflow: ellipsis; padding-right: 4px; border-left: 20px solid #11151d; border-bottom: 4px solid #11151d; border-top: 4px solid #11151d; background-color: transparent;')
local hover_text = connector_hover_text(output)
:wikitext(Output and Output.Name or '')
 
:done() -- Close output label div
if output.Multi and output.MultiIndex > 1 then
elseif Output and Output.Multi and Output.Multi == Output.MultiIndex then
local label = container
c
:tag('div') -- HTML div to contain the output label
:tag('div') -- HTML div to contain the output label
:attr('title', Output and (Output.Name .. ' <' .. Output.Type .. '>') or '') -- Add a basic mouseover description
:attr('title', hover_text) -- Add a basic mouseover description
:cssText('display:flex; gap: 4px; grid-column: label; grid-row: ' .. Index .. ';')
:cssText(css:output_label(nil, nil, row))
if output.Multi == output.MultiIndex then
label
:tag('div')
:tag('div')
:cssText('font-size: 1.75rem; width: 1em; height: 1em; background-color:grey; border-radius: 2em; display:flex; align-items:center; justify-content:center;')
:cssText('position:relative; display: flex; justify-content: flex-end; gap: 4px;')
:wikitext('+')
:tag('div')
:done()
:cssText(css.multi_button)
:tag('div')
:wikitext('+')
:cssText('font-size: 1.75rem; width: 1em; height: 1em; background-color:grey; border-radius: 2em; display:flex; align-items:center; justify-content:center;')
:done()
:wikitext('-')
:tag('div')
:done()
:cssText(css.multi_button)
:wikitext('-')
:done()
:tag('div')
:cssText(css:multi_bar(false, output.Multi))
end
else
else
c
container
:tag('div') -- HTML div to contain the output label
:tag('div') -- HTML div to contain the output label
:attr('title', Output and (Output.Name .. ' <' .. Output.Type .. '>') or '') -- Add a basic mouseover description
:attr('title', hover_text) -- Add a basic mouseover description
:cssText('text-align: right; overflow: hidden; text-overflow: ellipsis; padding-right: 4px; border-left: 20px solid #11151d; border-bottom: 4px solid #11151d; border-top: 4px solid #11151d; background-color: ' .. GetTypeColor(Output, 0.6) .. '; grid-column: label; grid-row: ' .. (((Index - 1) * 2) + 1) .. ';')
:cssText(css:output_label(color, output.Multi > 1, row))
:wikitext(Output and Output.Name or '')
:wikitext(output.Name or '')
:done() -- Close output label div
:done() -- Close output label div
end
end
end
 
-- Create the output (right) attachment point
-- Create the output (right) attachment point
CreateConnectorAttachmentPoint(Index, Container, Output, false);
local segments = GetTypeSegments(output)
CreateConnectorAttachmentPoint(container, hover_text, color, segments, is_impulse, false, row);
end -- if output`
end
end


--- Creates a new ProtoFlux global input field row in the output
--- 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 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.
-- @param global Table containing a Name and Type for the global field on this row.
function CreateGlobalsRow(Container, Global)
function CreateGlobalsRow(container, global, row)
Container
local color = GetTypeColor(global)
local hover_text = connector_hover_text(global)
 
container
:tag( 'div' )
:tag( 'div' )
:attr('title', Global.Name .. ' <' .. Global.Type .. '>') -- Add a basic mouseover description
:attr('title', hover_text) -- Add a basic mouseover description
:cssText('display: flex; min-height: 70px; flex-direction: column; border-left: 10px solid ' .. GetTypeColor(Global, 1.0) .. ';')
:cssText(css:global(color, row))
:tag( 'div' )
:tag( 'div' )
:cssText('display: flex; flex-direction: row; align-items: center; flex-grow:1; overflow: hidden;')
:cssText('display: flex; flex-direction: row; align-items: center; flex-grow:1; overflow: hidden;')
:tag( 'span' )
:tag( 'span' )
:cssText('text-align: center; overflow: hidden; text-overflow: ellipsis; font-size: 14pt; font-weight: bold; flex-grow:1;')
:cssText('text-align: center; overflow: hidden; text-overflow: ellipsis; font-size: 14pt; font-weight: bold; flex-grow:1;')
:wikitext(Global.Name)
:wikitext(global.Name)
:done()
:done()
:done()
:done()
:tag( 'div' )
:tag( 'div' )
:attr('style', 'display:flex; gap: 10px; flex-grow: 1;')
:attr('style', 'display: flex; gap: 10px; flex-grow: 1;')
:tag('div')
:tag('div')
:cssText('border-radius: 16px; background-color: #777; font-style: italic; text-align:center; flex-grow: 3; display: flex; flex-direction: column; justify-content: center;')
:cssText('background-color: #777; font-style: italic; flex-grow: 3; '..css.global_input)
:tag( 'span' )
:tag( 'span' )
:wikitext('null')
:wikitext('null')
:done()
:done()
:done()
:done()
:tag( 'div')
:tag( 'div' )
:cssText('border-radius: 16px; background-color: #333; text-align:center; flex-grow: 1; display: flex; flex-direction: column; justify-content: center;')
:cssText('background-color: #333; flex-grow: 1; '..css.global_input)
:tag ( 'span' )
:tag ( 'span' )
:wikitext('∅')   
:wikitext('∅')   
Line 199: Line 270:


--- Creates a new input or output attachement point for a connector row.
--- 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 container The Scribunto HTML node we'll be creating this row inside of. Should be a container of some sort.
-- @param Connector Table containing a Name (Optional) and Type (Required) for the input/output attachment point.
-- @param hover_text Text to show when hovering, possibly nil
-- @param isInput True if this is an input side attachment point, false if it is an output side attachment point.
-- @param color The color of this attachment point (table with r, g, b, optionally a).
function CreateConnectorAttachmentPoint(Index, Container, Connector, isInput)
-- @param segments The number of segments for the type, used for vectors or matrices.
if Connector and (Connector.Type == "Impulse" or Connector.Type == "AsyncImpulse" or Connector.Type == "Continuation")  then
-- @param is_impulse True if this is an impulse, false if it is a data type.
Container
-- @param is_input True if this is an input side attachment point, false if it is an output side attachment point.
:tag( 'div' )
function CreateConnectorAttachmentPoint(container, hover_text, color, segments, is_impulse, is_input, row)
:attr('title', Connector and (Connector.Name .. ' <' .. Connector.Type .. '>') or '') -- Add a basic mouseover description
local connector_type = nil
:cssText('background-color: black; fill: ' .. GetTypeColor(Connector, 0.3) .. '; stroke: ' .. GetTypeColor(Connector, 1.0) .. '; grid-column: ' .. (isInput and 'input' or 'output') .. '; grid-row: ' .. (((Index - 1) * 2) + 1) .. '/' .. (((Index - 1) * 2) + 3) .. ';')
if is_impulse then connector_type = "InputArrow"
:wikitext(mw.getCurrentFrame():expandTemplate({title = 'ProtoFluxConnector', args = {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, row))
:wikitext(mw.getCurrentFrame():expandTemplate({title = 'ProtoFluxConnector', args = {Type=connector_type, Segments=segments}}))
end


elseif Connector then
--- Returns a CSS color for the data type or impulse and whether the type is an impulse
Container
-- @param connector Table containing a Name (Optional) and Type (Required) for the input/output.
:tag( 'div' )
function GetColor(connector)
:attr('title', Connector and (Connector.Name .. ' <' .. Connector.Type .. '>') or '') -- Add a basic mouseover description
if connector == nil then error("connector is nil") end
:cssText('background-color: black; fill: ' .. GetTypeColor(Connector, 0.3) .. '; stroke: ' .. GetTypeColor(Connector, 1.0) .. '; grid-column: ' .. (isInput and 'input' or 'output') .. '; grid-row: ' .. (((Index - 1) * 2) + 1)  .. '/' .. (((Index - 1) * 2) + 3) .. ';')
if connector == nil or connector.Type == nil then error("Missing Type") end
:wikitext(mw.getCurrentFrame():expandTemplate({title = 'ProtoFluxConnector', args = {Type=(isInput and "InputBox" or "OutputBox")}}))
local color = ProtofluxColor.get_impulse_color(connector.Type)
if color ~= nil then
return color, true
end
end
return GetTypeColor(connector), false
end
end


--- Returns an RGBA value representing the type color within Resonite
--- Returns a CSS color for the data type
-- @param Connector Table containing a Name (Optional) and Type (Required) for the input/output attachment point.
-- @param connector Table containing a Name (Optional) and Type (Required) for the input/output or global.
-- @param Alpha The alpha to use in the RGBA value.  
function GetTypeColor(connector)
function GetTypeColor(Connector, Alpha)
if connector == nil then error("connector is nil") end
return (Connector and 'rgba(' .. (typeColor[Connector.Type] or '0, 0, 0') .. ',' .. Alpha .. ')' or 'rgba(0, 0, 0, 0)')
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
end


return p
return p

Latest revision as of 17:58, 24 February 2024

Documentation for this module may be created at Module:Test/doc

-- 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 = function(self, rows)
		return string.format("color: rgb(224, 224, 224); background-color: rgb(18, 20, 28); width: 256px; display: grid; grid-template-columns: [input] 30px [label] 1fr [output] 30px [end]; grid-template-rows: 80px repeat(%s, 35px 35px) 60px;",
		rows)
	end,
	node_title = function(self, minimal, rows)
		if minimal then
			return string.format("text-align:center; font-weight: bold; font-size: 18pt;  grid-column: label; grid-row: 1/%s; align-self: center;",
			(rows * 2) + 3)
		else
			return "padding: 10px 0px 10px 0px; text-align:center; font-weight: bold; background-color: rgb(26, 41, 54); font-size: 18pt;  grid-column: input / end; grid-row: 1;"
		end
	end,
	node_footer = function(self, rows)
		return string.format("text-align: center; padding: 10px; font-size: 18pt; color: rgb(64,64,64); font-weight: bold; grid-column: label; grid-row: %s; align-self: center;",
		(rows * 2) + 3)
	end,
	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, row)
		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; grid-column:label; grid-row: %s",
			color and "hidden" or "visible",
			color and ColorToCss(MultA(color, 0.6)) or "transparent",
			is_multi and " "..self.label_multi or "",
			row * 2
		)
	end,
	output_label = function(self, color, is_multi, row)
		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; grid-column: label; grid-row: %s;",
			color and "hidden" or "visible",
			color and ColorToCss(MultA(color, 0.6)) or "transparent",
			is_multi and " "..self.label_multi or "",
			(row * 2) + 1
		)
	end,
	attachment_point = function(self, color, order, row)
		return string.format(
			"width: 30px; background-color: black; fill: %s; stroke: %s; order: %s; grid-row: %s/%s;",
			ColorToCss(MultA(color, 0.15)),
			ColorToCss(color),
			order,
			row * 2,
			(row * 2) + 2
		)
	end,
	global = function(self, color, row)
		return string.format(
			"display: flex; min-height: 70px; flex-direction: column; border-left: 10px solid %s; grid-column: input/end; grid-row:%s/%s;",
			ColorToCss(color),
			row * 2,
			(row * 2) + 2
		)
	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 '[]')
	
	
	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)
	
		-- 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(maxRows + #globals))
		:tag('div') -- HTML div to contain the node title
			:cssText(css:node_title(mimimal, maxRows))
			:wikitext(frame.args.Name)
			:done() -- Close node title div
	
	-- 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], minimal, 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], i + maxRows)
	end
	
	protofluxContainer
		:tag('div') -- HTML div to contain the node category footer
			:cssText(css:node_footer(maxRows + #globals))
			: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, minimal, row)


	if input then
	
		local color, is_impulse = GetColor(input)
		local hover_text = connector_hover_text(input)
		
		-- Create the input (left) attachement point
		local segments = GetTypeSegments(input)
		CreateConnectorAttachmentPoint(container, hover_text, color, segments, is_impulse, true, row)
		
		if input.Multi and input.MultiIndex > 1 then
			local label = container
				:tag('div') -- HTML div to contain the input label
					:attr('title', hover_text) -- Add a basic mouseover description
					:cssText(css:input_label(nil, nil, row))
			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
			container
				: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, row))
					:wikitext(input.Name or '')
					:done() -- Close input label div
		end
	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 = container
				:tag('div') -- HTML div to contain the output label
					:attr('title', hover_text) -- Add a basic mouseover description
					:cssText(css:output_label(nil, nil, row))
			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
			container
				: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, row))
					:wikitext(output.Name or '')
					:done() -- Close output label div
		end

		-- Create the output (right) attachment point
		local segments = GetTypeSegments(output)
		CreateConnectorAttachmentPoint(container, hover_text, color, segments, is_impulse, false, row);
	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, row)
	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, row))
			: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, row)
	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, row))
			: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