Image
FO_Utilities = {}

-- **********
-- LAYER TAGS
-- **********
FO_Utilities.rigTag = "rig"
FO_Utilities.animTag = "anim"
FO_Utilities.outfitTag = "outfit"
FO_Utilities.embedTag = "embed"
FO_Utilities.keysTag = "keys"

-- *********
-- BONE TAGS
-- *********
-- * Constraining channels
FO_Utilities.constrainRotationTag = "!r"
FO_Utilities.constrainTranslationTag = "!t"
FO_Utilities.constrainScaleTag = "!s"
-- * Constrain individual translation dimensions
FO_Utilities.constrainXTranslationTag = "!x"
FO_Utilities.constrainYTranslationTag = "!y"
-- * Forcing interpolation
FO_Utilities.forceStepTag = "_step"
-- * Specific bones
-- FO_Utilities.hipTag = ".hip"
-- FO_Utilities.footTag = ".foot"
-- * Switchbone boneTag
FO_Utilities.switchTag = ".switch"
FO_Utilities.controlTag = ".control"
-- * Switchbone position tags:
FO_Utilities.posTag = ".pos"

FO_Utilities.guiTag = "GUI"

FO_Utilities.originTag = "origin"

-- *** List of all tags (LK_SelectBone needs this)
-- FO_Utilities.boneTags = { FO_Utilities.constrainTranslationTag, FO_Utilities.constrainXTranslationTag, FO_Utilities.constrainYTranslationTag, FO_Utilities.constrainRotationTag, FO_Utilities.constrainScaleTag, FO_Utilities.forceStepTag, FO_Utilities.hipTag, FO_Utilities.footTag, FO_Utilities.controlTag, FO_Utilities.switchTag.."%d?%d?%d?", FO_Utilities.posTag.."%d?%d?%d?" }
FO_Utilities.boneTags = { FO_Utilities.constrainTranslationTag, FO_Utilities.constrainXTranslationTag, FO_Utilities.constrainYTranslationTag, FO_Utilities.constrainRotationTag, FO_Utilities.constrainScaleTag, FO_Utilities.forceStepTag, "TODO-OLDHIPTAG", "TODO-OLDFOOTTAG", FO_Utilities.controlTag, FO_Utilities.switchTag.."%d?%d?%d?", FO_Utilities.posTag.."%d?%d?%d?" }


-- * Settings:
FO_Utilities.tinyUI = false
FO_Utilities.tinyUITreshold = 1500
FO_Utilities.reverseBoneColorButtons = false
FO_Utilities.forceBigUI = false

-- * Color names:
FO_Utilities.colorNames = {}
FO_Utilities.colorNames[0] = "Plain"
FO_Utilities.colorNames[1] = "Red"
FO_Utilities.colorNames[2] = "Orange"
FO_Utilities.colorNames[3] = "Yellow"
FO_Utilities.colorNames[4] = "Green"
FO_Utilities.colorNames[5] = "Blue"
FO_Utilities.colorNames[6] = "Purple"
FO_Utilities.colorNames[7] = "Tan"
FO_Utilities.colorNames[8] = "Pink"
FO_Utilities.colorNames[9] = "Turquoise"
FO_Utilities.colorNames[10] = "CadetBlue"
FO_Utilities.colorNames[11] = "Coral"

-- * Color RGB values:
FO_Utilities.colorsR = {}
FO_Utilities.colorsG = {}
FO_Utilities.colorsB = {}
FO_Utilities.colorsR[0], FO_Utilities.colorsG[0], FO_Utilities.colorsB[0] = MOHO.MohoGlobals.ElemCol.r, MOHO.MohoGlobals.ElemCol.g, MOHO.MohoGlobals.ElemCol.b -- No color
FO_Utilities.colorsR[1], FO_Utilities.colorsG[1], FO_Utilities.colorsB[1] = 220, 64, 51  -- Red
FO_Utilities.colorsR[2], FO_Utilities.colorsG[2], FO_Utilities.colorsB[2] = 240, 160, 0 -- Orange
FO_Utilities.colorsR[3], FO_Utilities.colorsG[3], FO_Utilities.colorsB[3] = 240, 240, 0 -- Yellow
FO_Utilities.colorsR[4], FO_Utilities.colorsG[4], FO_Utilities.colorsB[4] = 64, 220, 51 -- Green
FO_Utilities.colorsR[5], FO_Utilities.colorsG[5], FO_Utilities.colorsB[5] = 89, 126, 183 -- Blue
FO_Utilities.colorsR[6], FO_Utilities.colorsG[6], FO_Utilities.colorsB[6] = 192, 96, 191 -- Purple
FO_Utilities.colorsR[7], FO_Utilities.colorsG[7], FO_Utilities.colorsB[7] = 225, 185, 107 -- Tan
FO_Utilities.colorsR[8], FO_Utilities.colorsG[8], FO_Utilities.colorsB[8] = 248, 162, 159 -- Pink
FO_Utilities.colorsR[9], FO_Utilities.colorsG[9], FO_Utilities.colorsB[9] = 109, 223, 210 -- Turquoise
FO_Utilities.colorsR[10], FO_Utilities.colorsG[10], FO_Utilities.colorsB[10] = 129, 200, 187 -- Cadet Blue
FO_Utilities.colorsR[11], FO_Utilities.colorsG[11], FO_Utilities.colorsB[11] = 248, 146, 96 -- Coral
FO_Utilities.colorsR[12], FO_Utilities.colorsG[12], FO_Utilities.colorsB[12] = MOHO.MohoGlobals.InacCol.r, MOHO.MohoGlobals.InacCol.g, MOHO.MohoGlobals.InacCol.b -- Color for hidden bones
FO_Utilities.colorsR[13], FO_Utilities.colorsG[13], FO_Utilities.colorsB[13] = MOHO.MohoGlobals.SelCol.r, MOHO.MohoGlobals.SelCol.g, MOHO.MohoGlobals.SelCol.b -- Color for selected bones

function FO_Utilities:DrawMeTinyUI(moho)
	-- * Full screen on MacOS Cintiq 21UX low res mode = 1498
	local UIbool = false
	if not FO_Utilities.forceBigUI then
		UIbool = moho.view:Graphics():Width() < FO_Utilities.tinyUITreshold
	end
	if FO_Utilities.tinyUI ~= UIbool then
		FO_Utilities.tinyUI = UIbool
		FO_Utilities:ReloadTools(moho)
	end
end

-- **************************************************
-- Reload tools by hopping in and out of frame 0
-- **************************************************
function FO_Utilities:ReloadTools(moho)
	local returnFrame = moho.frame
	local tempFrame = 0
	if returnFrame == tempFrame then
		tempFrame = 1
	end
	moho:SetCurFrame(tempFrame)
	moho:SetCurFrame(returnFrame)
end

-- **************************************************
-- Getting the skeleton
-- **************************************************
function FO_Utilities:GetSkel(moho)
	local skel = moho:Skeleton()
	if (skel == nil) then
		if (not moho.layer:IsBoneType()) then
			skel = moho:ParentSkeleton()
		end
	end
	return skel
end

function FO_Utilities:Divider(layout, title, first)
	if not first then
		layout:AddChild(LM.GUI.Divider(true), LM.GUI.ALIGN_FILL, 0)	-- * Divider
	end
	if FO_Utilities.tinyUI == false and title ~= nil then
		layout:AddChild(LM.GUI.StaticText(title..":"))
		layout:AddPadding(-15)
	end
end

function FO_Utilities:DialogDivider(layout, title, first)
	if not first then
		layout:AddChild(LM.GUI.Divider(), LM.GUI.ALIGN_FILL, 0) -- * Divider
	end
	if title ~= nil then
		local longEmptyString = "                                                                                                                       "
		layout:AddChild(LM.GUI.StaticText(title..":"..longEmptyString))
		layout:AddPadding(-15)
	end
end

-- **************************************************
-- Get Layer-matrix without Camera-matrix
-- **************************************************
function FO_Utilities:LayerMatrix(moho, layer, frame) -- actions not processed yet
	local prevMatrix = LM.Matrix:new_local()
	prevMatrix:Identity()
	repeat
		local prevLayer = layer
		local matrix = LM.Matrix:new_local()
		layer:GetLayerTransform(frame, matrix, moho.document)
		matrix:Multiply(prevMatrix)
		prevMatrix:Set(matrix)
		if layer:Parent() then
			layer = layer:Parent()
		end
	until layer == prevLayer
	-- * Subtract camera matrix:
	local cameraMatrix = LM.Matrix:new_local()
	moho.document:GetCameraMatrix(frame, cameraMatrix)
	cameraMatrix:Invert()
	cameraMatrix:Multiply(prevMatrix)
	local layerMatrixWithoutCamera = cameraMatrix
	return layerMatrixWithoutCamera
end

-- **************************************************
-- Get Layer-matrix WITH Camera-matrix
-- **************************************************
function FO_Utilities:LayerMatrixWithCamera(moho, layer, frame) -- actions not processed yet
	local prevMatrix = LM.Matrix:new_local()
	prevMatrix:Identity()
	repeat
		local prevLayer = layer
		local matrix = LM.Matrix:new_local()
		layer:GetLayerTransform(frame, matrix, moho.document)
		matrix:Multiply(prevMatrix)
		prevMatrix:Set(matrix)
		if layer:Parent() then
			layer = layer:Parent()
		end
	until layer == prevLayer
	return prevMatrix
	--[[
	-- * Subtract camera matrix:
	local cameraMatrix = LM.Matrix:new_local()
	moho.document:GetCameraMatrix(frame, cameraMatrix)
	cameraMatrix:Invert()
	cameraMatrix:Multiply(prevMatrix)
	local layerMatrixWithoutCamera = cameraMatrix
	return layerMatrixWithoutCamera
	--]]
end

-- **************************************************
-- Embed layerscript on layer
-- **************************************************
function FO_Utilities:EmbedLayerScript(moho, layer, script)
	local userpath = string.gsub(moho:UserAppDir(), '\\', '/')
	local layerscriptsfolder = userpath .. "/Shared Resources/Layerscripts/"
	if not string.match(script, ".lua") then
		script = script..".lua"
	end
	local scriptfile = script
	local script = layerscriptsfolder..scriptfile
	layer:SetLayerScript(script)
	FO_Utilities:AddTag("embed", layer)
end

-- **************************************************
-- Python Script Command String
-- **************************************************
function FO_Utilities:Python(moho, pythonScript)
	-- if not string.match(pythonScript, ".py") then
	-- 	pythonScript = pythonScript..".py"
	-- end

	local pythonVersion = ""
	local pythonScriptDirectory = moho:UserAppDir().."/python/"
	pythonScriptPath = pythonScriptDirectory..pythonScript
	-- * Fix slashes per OS:
	if(FO_Utilities:getOS() == "win") then
		pythonVersion = "python"
		pythonScriptPath = string.gsub(pythonScriptPath, "/", "\\") -- * Reverse and double slashes for Windows
		-- * Add quotes around paths:
		pythonScriptPath = "\""..pythonScriptPath.."\""
	else
		pythonVersion = "python3"
		pythonScriptPath = string.gsub(pythonScriptPath, " ", "\\ ") -- * Add backslash to space
	end
	local command = pythonVersion.." "..pythonScriptPath
	return command
end

-- **************************************************
-- Run Powershell Command???
-- **************************************************
function FO_Utilities:Powershell(programWithParams)
	print ("FO_Utilities:Powershell(programWithParams = "..programWithParams..")")
	local psCmd = '"powershell.exe -Command "Start-Process cmd " -ArgumentList "/c", "\'' .. programWithParams .. '\'" -Verb RunAs -Wait"'
	--print (psCmd)
	os.execute(psCmd)
end

-- *****************************************************
-- Return a string based on any variables. By hayasidist
-- *****************************************************
function FO_Utilities:ToString(...)
	return HS_formatUserData(...)
end

-- **************************************************
-- Execute command line
-- **************************************************
function FO_Utilities:Execute(command, ask, feedback)
	Debug:Log(" - Command:")
	Debug:Log(command)
	Debug:Log("ask = "..tostring(ask))
	Debug:Log("feedback = "..tostring(feedback))
	if ask then
		local messages = string.split(command, " %-")
		table.insert(messages, "Are you sure?")
		local dlog = FO_Utilities:AreYouSureDialog(moho, "Execute command?", messages)
		if (dlog:DoModal() == LM.GUI.MSG_CANCEL) then
			return "cancelled"
		end
	end
	Debug:Log("Executing command!")
	local output
	if self:getOS() == ("unix") then
		output = io.popen(command)
	else
		-- os.execute(' "start "any title" "C:\\Program Files\\Lost Marble\\Moho 13.5\\Moho.exe" -r "w:\\zombies.moho" -f PNG -o "R:\\\\_out\\\\zombies\\\\" " ')
		output = os.execute(command)
	end
	return output -- * returns true if an error is thrown, otherwise nil.
end

-- **************************************************
-- Reveal directory in Finder/Explorer
-- **************************************************
function FO_Utilities:RevealDirectory(path)
	local command
	if self:getOS() == "win" then
		path = string.gsub(path, "/", "\\")
		command = "explorer \""..path.."\""
		local output = os.execute(command)
		Debug:Log("Windows: output = "..tostring(output))
	else
		command = "open "..string.gsub(path, " ", "\\ ")
		local output = io.popen(command)
		if output == nil then
			FO_Utilities:Alert("Directory doesn't exist (yet), or you are not connected to the server!", path)
		end
	end
end

-- **************************************************
-- Getting the rig layer:
-- **************************************************
function FO_Utilities:RigLayer(moho)
	local skel = FO_Utilities:GetSkel(moho)
	local layer = moho.layer
	local tags = layer:UserTags()
	if string.match(tags, FO_Utilities.rigTag) then
		return layer
	end
	--
	while (layer ~= nil and not string.match(tags, FO_Utilities.rigTag)) do
		layer = layer:Parent()
		if (layer ~= nil) then
			tags = layer:UserTags()
			if string.match(tags, FO_Utilities.rigTag) then
				return layer
			end
		end
	end
	--
	-- * FAILED TO FIND A RIG-TAGGED BONE LAYER!!!")
	if (moho.layer:LayerType() == 4 or moho.layer:LayerType() == 5 ) then -- 4 = LT_BONE -- 5 = LT_SWITCH
		return moho.layer
	end
	-- *** Trying again, but this time, we're not going to look for a tag, but any bone layer.
	layer = moho.layer
	while layer ~= nil and (not (layer:LayerType() == 4) or not (layer:LayerType() == 5)) do
		layer = layer:Parent()
		if layer ~= nil then
			if layer:LayerType() == 4 or layer:LayerType() == 5 then
				return layer
			end
		end
	end
	-- * ...ALSO FAILED TO FIND AN UN-TAGGED BONE LAYER!!!")
	return nil
end

-- *******************************************************
-- Gets color for bone according to comment, returns color
-- *******************************************************
function FO_Utilities:LegacyBoneCommentColor(moho, boneID)
	-- print ("bonecommentcolor being called")
	local color = 0
	if rigLayer == nil then -- * help is dit verstandig?
		return color
	end
	local comment = rigLayer:UserComments()
	if comment == nil then
		local skel = FO_Utilities:GetSkel(moho)
		color = skel:Bone(boneID):Tags()
		return color
	end
	local boneNumber = boneID+1
	if string.len(comment) < boneNumber * 3 then
		return 0
	end
	-- *** WAARSCHUWING: comment:sub crashed keihard als hij niks kan vinden ***
	color = comment:sub(boneNumber*2-1+boneNumber-1, boneNumber*2-1+boneNumber) -- variant om "01.02.03." te lezen
	if string.match(color, "%d%d") then
		color = tonumber(color)
	else
		local skel = FO_Utilities:GetSkel(moho)
		local bone = skel:Bone(boneID)
		if bone ~= nil then
			color = bone:Tags()
		end
	end
	return color
end

-- **************************************************
-- Replaces original function!!!
-- **************************************************
function MOHO.BuildBoneMenu(menu, skel, baseMsg, dummyMsg, moho) -- original doesn't accept the moho argument!
	local vanilla = false
	if MohoMode ~= nil then
		if MohoMode.vanilla then
			vanilla = true
		end
	end
	if not vanilla then
		FO_Utilities:BuildBoneMenu(menu, skel, baseMsg, dummyMsg, moho)
	else
		menu:RemoveAllItems()
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (bone:Name() ~= "") then
				menu:AddItem(bone:Name(), 0, baseMsg + i)
			end
		end
		if (menu:CountItems() == 0) then
			menu:AddItem(MOHO.Localize("/Scripts/Utility/None=<None>"), 0, dummyMsg)
			menu:SetEnabled(dummyMsg, false)
		end
	end
end

-- ****************************************************************************
-- Returns duration as a formatted string, for example: "6 Minutes, 30 Seconds"
-- ****************************************************************************
function FO_Utilities:DisplayDuration(time)
	local days = math.floor(time/86400)
	local hours = math.floor(math.fmod(time, 86400)/3600)
	local minutes = math.floor(math.fmod(time,3600)/60)
	local seconds = math.floor(math.fmod(time,60))
	local msg = ""
	if days > 0 then
		msg = string.format("%d Days, ", days)
	end
	if hours > 0 then
		msg = msg..string.format("%d Hours, ", hours)
	end
	if minutes > 0 then
		msg = msg..string.format("%d Minutes, ", minutes)
	end
	msg = msg..string.format("%d Seconds", seconds)
	return msg
end

-- ********************************************************************************************************
-- Finds all layers in current document and returns 'layers', sets checkmarks for layers that aren't hidden
-- ********************************************************************************************************
function FO_Utilities:BuildBoneMenu(menu, skel, baseMsg, dummyMsg, moho)
	menu:RemoveAllItems()
	-- * Bone Tables
	local boneTable = {}
	local smartBoneTable = {}
	local shyBoneTable = {}
	local unnamedBonesTable = {}
	-- * Get Bones
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		local nameless = string.match(bone:Name(), "B%d%d?%d?")
		if moho ~= nil and FO_Utilities:IsASmartBone(moho, skel, bone) then
			table.insert(smartBoneTable, {boneName = bone:Name(), int = 0, msg = baseMsg + i, selected = bone.fSelected, hidden = bone.fHidden})
		elseif not bone.fShy and not nameless then
			table.insert(boneTable, {boneName = bone:Name(), int = 0, msg = baseMsg + i, selected = bone.fSelected, hidden = bone.fHidden})
		elseif nameless then
			table.insert(unnamedBonesTable, {boneName = bone:Name(), int = 0, msg = baseMsg + i, selected = bone.fSelected, hidden = bone.fHidden})
		else
			table.insert(shyBoneTable, {boneName = bone:Name(), int = 0, msg = baseMsg + i, selected = bone.fSelected, hidden = bone.fHidden})
		end
	end
	-- * Sort bones alphabetically
	table.sort(smartBoneTable, function(a,b) return a.boneName < b.boneName end)
	table.sort(boneTable, function(a,b) return a.boneName < b.boneName end)
	table.sort(shyBoneTable, function(a,b) return a.boneName < b.boneName end)
	table.sort(unnamedBonesTable, function(a,b) return a.boneName < b.boneName end)
	-- * Add Bones to Dropdown Menu
	FO_Utilities:AddBoneTableToMenu(smartBoneTable, "SMARTBONES", menu)
	FO_Utilities:AddBoneTableToMenu(boneTable, "BONES", menu)
	FO_Utilities:AddBoneTableToMenu(shyBoneTable, "SHY BONES", menu)
	FO_Utilities:AddBoneTableToMenu(unnamedBonesTable, "NAMELESS BONES", menu)
	--
	if (menu:CountItems() == 0) then
		menu:AddItem(MOHO.Localize("/Scripts/Utility/None=<None>"), 0, dummyMsg)
		menu:SetEnabled(dummyMsg, false)
	end
end

-- **************************************************
-- Replaces original function!!!
-- **************************************************
function MOHO.BuildBoneChoiceMenu(menu, skel, baseMsg, exclude, moho)
	local vanilla = false
	if MohoMode ~= nil then
		if MohoMode.vanilla then
			vanilla = true
		end
	end
	if not vanilla then
		-- * new function
		menu:RemoveAllItems()
		menu:AddItem(MOHO.Localize("/Scripts/Utility/None=<None>"), 0, baseMsg)
		-- * Bone Tables
		local boneTable = {}
		local smartBoneTable = {}
		local shyBoneTable = {}
		local unnamedBonesTable = {}
		-- * Get Bones
		for i = 0, skel:CountBones() - 1 do
			if i ~= exclude then
				local bone = skel:Bone(i)
				local nameless = string.match(bone:Name(), "B%d%d?%d?")
				if moho ~= nil and FO_Utilities:IsASmartBone(moho, skel, bone) then
					table.insert(smartBoneTable, {boneName = bone:Name(), int = 0, msg = baseMsg + 1 + i, selected = bone.fSelected, hidden = false})
				elseif not bone.fShy and not nameless then
					table.insert(boneTable, {boneName = bone:Name(), int = 0, msg = baseMsg + 1 + i, selected = bone.fSelected, hidden = false})
				elseif nameless then
					table.insert(unnamedBonesTable, {boneName = bone:Name(), int = 0, msg = baseMsg + 1 + i, selected = bone.fSelected, hidden = false})
				else
					table.insert(shyBoneTable, {boneName = bone:Name(), int = 0, msg = baseMsg + 1 + i, selected = bone.fSelected, hidden = false})
				end
			end
		end
		-- * Sort bones alphabetically
		table.sort(smartBoneTable, function(a,b) return a.boneName < b.boneName end)
		table.sort(boneTable, function(a,b) return a.boneName < b.boneName end)
		table.sort(shyBoneTable, function(a,b) return a.boneName < b.boneName end)
		table.sort(unnamedBonesTable, function(a,b) return a.boneName < b.boneName end)
		-- * Add Bones to Dropdown Menu
		FO_Utilities:AddBoneTableToMenu(smartBoneTable, "SMARTBONES", menu)
		FO_Utilities:AddBoneTableToMenu(boneTable, "BONES", menu)
		FO_Utilities:AddBoneTableToMenu(shyBoneTable, "SHY BONES", menu)
		FO_Utilities:AddBoneTableToMenu(unnamedBonesTable, "NAMELESS BONES", menu)
		-- * end new function
	else
		-- * Original function!
		menu:RemoveAllItems()
		menu:AddItem(MOHO.Localize("/Scripts/Utility/None=<None>"), 0, baseMsg)
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (i ~= exclude and bone:Name() ~= "") then
				menu:AddItem(bone:Name(), 0, baseMsg + 1 + i)
			end
		end
		-- * End Original function!
	end
end

function FO_Utilities:AddBoneTableToMenu(bTable, title, menu)
	if #bTable ~= 0 then
		local label = "___________[ "..#bTable.." "..title.." ]"
		menu:AddItem(label, 0, dummyMsg)
		menu:SetEnabled(dummyMsg, false)
		--
		for i = 1, #bTable do
			local boneName = bTable[i].boneName
			local int = bTable[i].int
			local msg = bTable[i].msg
			local selected = bTable[i].selected
			local hidden = bTable[i].hidden
			if (boneName ~= "") then
				menu:AddItem(boneName, int, msg)
				menu:SetChecked(msg, selected)
				menu:SetEnabled(msg, not hidden)
			end
		end
		--
	end
end

-- ******************************************************
-- Return filename of the embedded layerscript of a layer
-- ******************************************************
function FO_Utilities:LayerScript(layer)
	local embeddedscript = layer:LayerScript()
	if embeddedscript ~= "" then
		embeddedscript = self:FileName(embeddedscript)
	else
		embeddedscript = nil
	end
	return embeddedscript
end

-- ************************************************************
-- Finds all layercomps in current document and returns 'comps'
-- ************************************************************
function FO_Utilities:AllComps(moho)
	local doc = moho.document
	local comps = {}
	for i = 0, doc:CountLayerComps() do
		local comp = doc:GetLayerComp(i)
		table.insert(comps, comp)
	end
	return comps
end

-- *********************************************************
-- Finds all layers in current document and returns 'layers'
-- *********************************************************
function FO_Utilities:AllLayers(moho)
	-- print ("FO_Utilities:AllLayers(moho)")
	local layers = {}
	local stack = {}
	local sp = 0
	for i=0, moho.document:CountLayers()-1 do
		local layer = moho.document:Layer(i)
		table.insert(layers, layer)
		local group = nil
		local layerID = 0
		while true do
			if (layer:IsGroupType()) then
				table.insert(stack, {group, layerID -1})
				sp = sp+1
				group = moho:LayerAsGroup(layer)
				layerID = group:CountLayers()
			end
			if (layerID > 0) then
				layerID = layerID -1
				layer = group:Layer--[[ByDepth]](layerID)
				table.insert(layers, layer)
			else
				layerID = -1
				while (sp > 0) do
					group, layerID = stack[sp][1], stack[sp][2]
					table.remove(stack)
					sp = sp -1
					if (layerID >= 0) then
						layer = group:Layer--[[ByDepth]](layerID)
						table.insert(layers, layer)
						break
					end
				end
			end
			if (layerID < 0) then
				break
			end
		end
	end
	return layers
end

-- *********************************************************
-- Finds all layers in current document and returns 'layers'
-- *********************************************************
function FO_Utilities:LayerInclChildLayers(moho, layer)
	local layers = {}
	local stack = {}
	local sp = 0
	table.insert(layers, layer)
	if (layer:IsGroupType()) then
		moho:LayerAsGroup(layer)
		for i=0, layer:CountLayers()-1 do
			local layer = layer:Layer(i)
			table.insert(layers, layer)
			local group = nil
			local layerID = 0
			while true do
				if (layer:IsGroupType()) then
					table.insert(stack, {group, layerID -1})
					sp = sp+1
					group = moho:LayerAsGroup(layer)
					layerID = group:CountLayers()
				end
				if (layerID > 0) then
					layerID = layerID -1
					layer = group:Layer--[[ByDepth]](layerID)
					table.insert(layers, layer)
				else
					layerID = -1
					while (sp > 0) do
						group, layerID = stack[sp][1], stack[sp][2]
						table.remove(stack)
						sp = sp -1
						if (layerID >= 0) then
							layer = group:Layer--[[ByDepth]](layerID)
							table.insert(layers, layer)
							break
						end
					end
				end
				if (layerID < 0) then
					break
				end
			end
		end
	end
	return layers
end

-- **********************************************************
-- Returns full layerstructure of specified layer as a string
-- For example: "~/Forest/CrazyGuy/Arm/HandSwitch/Open"
-- **********************************************************
function FO_Utilities:LayerStructure(layer)
	structure = layer:Name()
	while layer:Parent() ~= nil do
		layer = layer:Parent()
		structure = layer:Name().."/"..structure
	end
	structure = "~/"..structure
	return structure
end

-- **************************************************
-- List files in specific directory, returns 'files'
-- **************************************************
function FO_Utilities:ListFiles(directory, moho)
	local files = {}
	moho:BeginFileListing(directory, true)
	local file = moho:GetNextFile()
	while file ~= nil do
		if file ~= ".DS_Store" then
			table.insert(files, file)
		end
		file = moho:GetNextFile()
	end
	table.sort(files, function(a, b) return a:upper() < b:upper() end) -- * Sorts table alphabetically
	return files
end

-- **************************************************
-- Check if a certain file exists
-- **************************************************
function FO_Utilities:FileExists(moho, filePath)
	-- * Check if file exists:
	filePath = string.gsub(filePath, "\\", "/")
	local lastslashpos = (filePath:reverse()):find("%/") -- find last slash
	local fileName = (filePath:sub(-lastslashpos+1)) -- filename only
	local fileDir = string.gsub(filePath, string.gsub(fileName, "%-", "%%-"), "") -- * Catch dash characters
	moho:BeginFileListing(fileDir, true)
	local fileFound = false
	local file = moho:GetNextFile()
	while file ~= nil and not fileFound do
		if file == fileName then
			fileFound = true
		else
			file = moho:GetNextFile()
		end
	end
	return fileFound
end

-- **************************************************
-- Get file directory
-- **************************************************
function FO_Utilities:FileDirectory(filePath)
	filePath = string.gsub(filePath, "\\", "/")
	local lastslashpos = (filePath:reverse()):find("%/") -- find last slash
	local fileDir = string.sub(filePath, 0, -lastslashpos-1)
	return fileDir
end

-- **************************************************
-- Get Project directory
-- **************************************************
function FO_Utilities:GetProjectDirectory(path)
	-- * Find Project directory:
	local projectDirectory = FO_Utilities:FileDirectory(path)
	local foundProjectDirectory = false
	local lookForDirectories = { "/Shots/", "/Rigs/", "/Actions/" } -- * This are always located in the root of the Project directory.
	for i = 1, #lookForDirectories do
		if not foundProjectDirectory then
			local lookFor = lookForDirectories[i]
			local firstDirPos = (string.lower(path)):find(string.lower(lookFor))
			if firstDirPos ~= nil then
				projectDirectory = string.sub(path, 0, firstDirPos-1)
				foundProjectDirectory = true
			else
				projectDirectory = FO_Utilities:FileDirectory(path)
			end
		end
	end
	return projectDirectory
end

-- ****************************************************
-- List layers with a specific tag, returns 'taglayers'
-- ****************************************************
function FO_Utilities:ListLayersWithTag(tag, moho)
	local taglayers = {}
	local layers = FO_Utilities:AllLayers(moho)
	local i
	for i = 1, #layers do
		layer = layers[i]
		local tags = layer:UserTags()
		--local tag = "rig"
		if string.match(tags, tag) then
			table.insert(taglayers, layer)
		end
	end
	return taglayers
end

-- ****************************************************
-- 
-- ****************************************************
function FO_Utilities:LayerHasTag(layer, tag)
	local tags = layer:UserTags()
	return (string.match(string.lower(tags), string.lower(tag)))
end

-- **************************************************
-- Find a layer by name
-- **************************************************
function FO_Utilities:AnyLayerByName(moho, name, types)
	local layers = FO_Utilities:AllLayers(moho)
	for i = 1, #layers do
		local layer = layers[i]
		if types ~= nil then
			local layerType = layer:LayerType()
			for j = 1, #types do
				local type = types[j]
				if layerType ~= type then
					if layer:Name() == name then
						return layer
					end
				end
			end
		else
			if layer:Name() == name then
				return layer
			end
		end
	end
	return nil
end
-- **************************************************
-- LAYER TYPES
-- **************************************************
-- 0	LT_UNKNOWN		Unkown layer type
-- 1	LT_VECTOR		Vector layer type
-- 2	LT_IMAGE		Image layer type
-- 3	LT_GROUP		Group layer type
-- 4	LT_BONE			Bone layer type
-- 5	LT_SWITCH		Switch layer type
-- 6	LT_PARTICLE		Particle layer
-- 7	LT_NOTE			Note layer type
-- 8	LT_3D			3D layer type
-- 9	LT_AUDIO		Audio layer type
-- 10	LT_PATCH		Patch layer type
-- 11	LT_TEXT			Text layer type
-- **************************************************


-- **************************************************
-- Toggles a tag on selected layers
-- **************************************************
function FO_Utilities:ToggleTagSelectedLayers(tag, moho)
	local selCount = moho.document:CountSelectedLayers()
	local layer = moho.layer
	local tagMatch = false
	local tagNotfound = false
	local doAddTag = false
	local tags
	--Round 1 - Check if any of the selected layers already has the tag but other don't. Because in that case we should add the tag to all layers which don't have it yet.
	for i = 0, selCount - 1 do
		local layer = moho.document:GetSelectedLayer(i)
		tags = layer:UserTags()
		-- * Is ["..tag.."] in ["..tags.."]?")
		if string.match(tags, tag) then
			tagMatch = true
		else
			tagNotfound = true
		end
	end
	--Decide if we need to add or remove the tag from all layers
	if tagNotfound == true then-- and tagMatch == false then
		doAddTag = true
	end
	-- * doAddTag: "..tostring(doAddTag))
	--Round 2 - Actually do it per layer
	for i = 0, selCount - 1 do
		local layer = moho.document:GetSelectedLayer(i)
		local tags = layer:UserTags()
		if doAddTag == false then -- Remove tag from layer
			self:RemoveTag(tag, layer, moho)
		elseif doAddTag == true then -- Add tag to each layer
			self:AddTag(tag, layer, moho)
		end
	end
end

-- *****************************************************************************************
-- Check if layer should never be considered an animation layer (particles, references, etc)
-- *****************************************************************************************
function FO_Utilities:NonKeysTagLayer(layer, moho)
	local skipLayer = false
	-- * Skip Particles:
	local parentLayer = layer:Parent() -- * todo, check entire parent hierarchy
	if parentLayer ~= nil then
		if moho:LayerAsParticle(parentLayer) ~= nil then
			skipLayer = true
		end
	end
	return skipLayer
end

-- **************************************************
-- Adds a tag to a layer
-- **************************************************
function FO_Utilities:AddTag(tag, layer)
	tags = layer:UserTags()
	if tags == "" then
		tags = tag
	elseif not string.match(tags, tag) then
		tags = tags .. ", " .. tag -- Add tag to tags string
		tags = self:CleanString(tags)
	end
	layer:SetUserTags(tags)
end

-- **************************************************
-- Cleans string of unnecessary spaces and commas
-- **************************************************
function FO_Utilities:CleanString(str)
	-- * Remove spaces before a comma:
	while string.match(str, " ,") do
		str = string.gsub(str, " , ", ",")
	end
	-- * Remove double commas:
	while string.match(str, ",,") do
		str = string.gsub(str, ",,", ",")
	end
	-- * Add spaces after comma's:
	str = string.gsub(str, ",",", ")
	-- *
	if string.sub(str,1, 1) == "," then -- * Check if the string starts with a comma
		str = string.sub(str, 2) -- * Remove the first character of the string because it's a comma
	end
	-- * Remove double spaces:
	while string.match(str, "  ") do
		str = string.gsub(str, "  ", " ")
	end
	-- * Remove space at the end of string:
	if string.sub(str, 0, 1) == " " then
		str = str:sub(2, -1)
	end
	-- * Remove space at the end of string:
	if string.sub(str, -1) == " " then
		str = str:sub(1, -2)
	end
	return str
end

-- **************************************************
-- Adds a tag to all selected layers
-- **************************************************
function FO_Utilities:AddTagToSelectedLayers(tag, moho) --FOUT
	-- * FO_Utilities:AddTagToSelectedLayers("..tag..", moho)")
	local selCount = moho.document:CountSelectedLayers()
	for i = 0, selCount - 1 do
		local layer = moho.document:GetSelectedLayer(i)
		self:AddLayerTag(tag, layer, moho)
	end
end

-- **************************************************
-- Removes a tag to all selected layers
-- **************************************************
function FO_Utilities:RemoveTagFromSelectedLayers(tag, moho) --FOUT
	local selCount = moho.document:CountSelectedLayers()
	for i = 0, selCount - 1 do
		local layer = moho.document:GetSelectedLayer(i)
		self:RemoveLayerTag(tag, layer, moho)
	end
end

-- **************************************************
-- Removes a tag from a layer
-- **************************************************
function FO_Utilities:RemoveTag(tag, layer, moho)
	-- * FO_Utilities:RemoveTag("..tag..", "..layer:Name()..", moho)")
	local tags = layer:UserTags()
	local commaTag = "," .. tag
	local commaSpaceTag = ", " .. tag
	tags = tags:gsub(commaSpaceTag, "") -- Remove ", anim" from tags string
	tags = tags:gsub(commaTag, "") -- remove ",anim" from string, in case it doesn't have the space
	tags = tags:gsub(tag, "") -- remove tag from tags string if it wasn't already deleted with comma and/or space
	tags = self:CleanString(tags)
	layer:SetUserTags(tags)
end

-- ***********************************************************************
-- Get shot name based on second to last underscore (removes _v000_X.moho)
-- ***********************************************************************
function FO_Utilities:ShotName(moho)
	local path = moho.document:Path()
	if path == nil then
		print ("path == nil")
		return nil
	end
	local fileName = self:FileName(path)
	local shotName = string.gsub(fileName, "_v%d%d%d_%u+.moho", "")
	return shotName
end

-- ***********************************************************************
-- Strip everything before the last slash of a full filepath and return filename
-- ***********************************************************************
function FO_Utilities:FileName(path)
	path = string.gsub(path, "\\", "/")
	local lastslashpos = (path:reverse()):find("%/") -- find last slash
	local fileName = (path:sub(-lastslashpos+1)) -- filename only
	return fileName
end
-- *****************************************************************************************************
-- Filters layerpanel by tag and selects a layer if necessary and optionally set the timeline visibility
-- *****************************************************************************************************
function FO_Utilities:FilterTag(tag, setTimelineVisibility, moho)
	if (setTimelineVisibility == false and moho:LayersWindowGetSearchContext() == 8 and moho:LayersWindowGetSearchContextValue() == tag) then
		moho:LayersWindowSetSearchContext(0) -- 0 = LAYERWND_SEARCHCONTEXT_ALL
		moho:LayersWindowSetSearchContextValue("")
		moho:ShowLayerInLayersPalette(moho.layer)
	else
		moho:LayersWindowSetSearchContext(8) -- 8 = LAYERWND_SEARCHCONTEXT_TAGCONTAIN
		moho:LayersWindowSetSearchContextValue(tag)
		local layer = moho.layer
		first_match = nil
		tags = layer:UserTags()
		if string.match(tags, tag) then
			first_match = layer
		end
		local layers = FO_Utilities:AllLayers(moho)
		local i
		for i = 1, #layers do
			layer = layers[i]
			tags = layer:UserTags()
			if string.match(tags, tag) then
				if first_match == nil then
					first_match = layer
				end
				if (setTimelineVisibility == true) then
					layer:SetShownOnTimeline(true)
				end
			elseif (setTimelineVisibility == true) then
				layer:SetShownOnTimeline(false)
			end
		end
		if first_match ~= nil then
			moho:SetSelLayer(first_match, false, false)
		end
	end
end

-- **************************************************
-- Filters layerpanel by group name
-- **************************************************
function FO_Utilities:FilterGroupname(groupname, moho)
	if (moho:LayersWindowGetSearchContext() == 2) then
		moho:LayersWindowSetSearchContext(0) -- 0 = LAYERWND_SEARCHCONTEXT_ALL
		moho:LayersWindowSetSearchContextValue("")
	else
		moho:LayersWindowSetSearchContext(2) -- 2 = LAYERWND_SEARCHCONTEXT_GROUPNAMECONTAINS
		moho:LayersWindowSetSearchContextValue(groupname)
	end
end

-- **************************************************
-- Check whether a table contains an element
-- **************************************************
function table.contains(table, element)
	if table ~= nil then
		for _, value in pairs(table) do
			if value == element then
				return true
			end
		end
	end
	return false
end

-- **************************************************
-- Find index of element in table
-- **************************************************
function table.find(table, element)
	for index, value in pairs(table) do
		if value == alement then
			return index
		end
	end
end

-- **************************************************
-- Deletes an element from a table
-- **************************************************
function table.delete(tbl, element)
	for index, value in pairs(tbl) do
		if value == element then
			table.remove(tbl, index)
		end
	end
end

-- **************************************************
-- Round a number by x amount of decimals
-- **************************************************
function round(number, decimals)
	local power = 10^decimals
	return math.floor(number * power) / power
end

-- ***********************************************************************
-- Show all child layers of 'layer' that are tagged "anim" on the timeline
-- ***********************************************************************
function FO_Utilities:ShowAnimChildLayersOnTimeline(layer)
	local tags = layer:UserTags()
	if (string.match(tags, "rig") and not layer:SecondarySelection()) then
		return
	end
	for i = 0, layer:CountLayers() - 1 do
		local layer = layer:Layer(i)
		tags = layer:UserTags()
		local tags = layer:UserTags()
		if layer:LayerType() == 3 or layer:LayerType() == 4 or layer:LayerType() == 5 then
			self:ShowAnimChildLayersOnTimeline(layer)
		end
		if string.match(tags, "anim") and not string.match(tags, "rig") then
			layer:SetShownOnTimeline(true)
		end
	end
end

-- **************************************************
-- Recolor bones (The vanilla way)
-- **************************************************
function FO_Utilities:RecolorizeBones(vanilla, moho)
	if moho.layer:UserComments() == "" then
		return
	end	
	local skel = FO_Utilities:GetSkel(moho)
	if skel == nil then
		return
	end
	local customSkelActive = FO_Draw:WantCustomSkel(moho)
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		local boneColorComment = FO_Utilities:LegacyBoneCommentColor(moho, i)
		if vanilla and not customSkelActive then
			if boneColorComment ~= 0 then
				bone:SetTags(boneColorComment)
			end
		end
	end
	if rigLayer ~= nil then
		Debug:Log("Removed comment from "..moho.layer:Name().." ("..moho.layer:UserComments()..")")
		rigLayer:SetUserComments("")
	end
	-- * Force viewport to redraw itself:
	MOHO.Redraw()
	-- * Update UI so timeline channels show up and dissapear correctly:
	moho:UpdateUI()
end

function FO_Utilities:Alert2(msg1, msg2, msg3)
	local type = LM.GUI.ALERT_WARNING
	if msg1 == "i" then
		type = LM.GUI.ALERT_INFO
	elseif msg1 == "!" then
		type = LM.GUI.ALERT_WARNING
	elseif msg1 == "?" then
		type = LM.GUI.ALERT_QUESTION
	end
	if msg3 ~= nil then
		msg3 = "\n"..msg3
	end
	LM.GUI.Alert(icon, msg1, msg2, msg3, "OK", nil, nil)
end

-- *********************************************************
-- Simple alert dialog, takes many strings.
-- Either as a table with strings, or as multiple arguments.
-- Add a "i"/"?"/"!" to change the type of alertbox.
-- *********************************************************
function FO_Utilities:Alert(...)
	local arg = {...}
	if (type(...) == "table") then
		arg = ...
	end
	local msg1 = nil
	local msg2 = nil
	local msg3 = nil
	local type = LM.GUI.ALERT_WARNING
	for i,v in ipairs(arg) do
		if v == "i" then
			type = LM.GUI.ALERT_INFO
		elseif v == "!" then
			type = LM.GUI.ALERT_WARNING
		elseif v == "?" then
			type = LM.GUI.ALERT_QUESTION
		elseif msg1 == nil then
			msg1 = v
		elseif msg2 == nil then
			msg2 = v
		elseif msg3 == nil then
			msg3 = "\n"..v
		else
			msg3 = msg3.."\n"..v
		end
	end
	LM.GUI.Alert(type, msg1, msg2, msg3, "OK", nil, nil)
end

-- ****************************************************
-- Create simple question dialog with OK/Cancel buttons
-- ****************************************************
-- * An empty table to set up as a dialog subclass:
FO_Utilities.AreYouSureDialogTable = {}
-- * Dialog function:
function FO_Utilities:AreYouSureDialog(moho, dialogTitle, messages)
	local d = LM.GUI.SimpleDialog(dialogTitle, self.AreYouSureDialogTable)
	local l = d:GetLayout()
	d.moho = moho
	local command = ""
	local align = LM.GUI.ALIGN_LEFT
	if messages == nil then
		return d
	end
	for i = 1, #messages do
		msg = messages[i]
		if i == #messages then
			if (string.match(string.lower(dialogTitle), "command")) then -- * SHOULD WRITE A NEAT SEPARATE FUNCTION...
				l:PushH()
					d.textBox = LM.GUI.TextControl(0, "Some text for a textbox to set its length!", 0, LM_FIELD_TEXT, "Command:")
					d.textBox:SetValue(command)
					l:AddChild(d.textBox, align)
					local tip = "(Copy/paste command into Windows Command Prompt)"
					if FO_Utilities:getOS() == "unix" then
						tip = "(Copy/paste command into MacOS Terminal)"
					end
					l:AddChild(LM.GUI.StaticText(tip))
				l:Pop()
			end
			align = LM.GUI.ALIGN_RIGHT
		else
			command = command..msg
		end
		l:AddChild(LM.GUI.StaticText(msg), align)
	end
	return d
end

-- ****************************************************
-- Create simple question dialog with text input
-- ****************************************************
-- * An empty table to set up as a dialog subclass:
FO_Utilities.InputTextDialogTable = {}
-- * Dialog function:
function FO_Utilities:InputTextDialog(moho, dialogTitle, label, default, messages)
	local d = LM.GUI.SimpleDialog(dialogTitle, self.InputTextDialogTable)
	local l = d:GetLayout()
	d.moho = moho
	local align = LM.GUI.ALIGN_LEFT
	d.textBox = LM.GUI.TextControl(0, "Room for a loooooooooooooong string", 0, LM_FIELD_TEXT, label)
	d.textBox:SetValue(default)
	l:AddChild(d.textBox, align)
	if messages ~= nil then
		for i = 1, #messages do
			msg = messages[i]
			print (msg)
			l:AddChild(LM.GUI.StaticText(msg), align)
		end
	end
	return d
end
function FO_Utilities.InputTextDialogTable:OnOK()
	FO_Utilities.input = self.textBox:Value()
end

-- *************************************************
-- Create simple question dialog with Yes/No buttons
-- *************************************************
function FO_Utilities:YesNoQuestion(line1, line2, line3)
	local d = LM.GUI.Alert(
		LM.GUI.ALERT_QUESTION,
		line1,
		line2,
		line3,
		"Yes",
		"No",
		nil
		) -- * Returns 0 or 1
	return d
end

-- *************************************************
-- Create simple question with three buttons
-- * TODO * Fix this, see LK_ExportActions
-- *************************************************
--[[
function FO_Utilities:MultipleChoiceQuestion(question, choice1, choice2, choice3)
	local d = LM.GUI.Alert(
		LM.GUI.ALERT_QUESTION,
		question,
		nil,
		nil,
		choice0,
		choice1,
		choice2
		) -- * Returns 0, 1 or 2
	return d
end
--]]

-- **************************************************
-- Create directory
-- **************************************************
function FO_Utilities:CreateDirectory(moho, directory)
	local directoryExists = false
	if moho ~= nil then
		directoryExists = FO_Utilities:FileExists(moho, directory)
	end
	if not directoryExists then
		if FO_Utilities:getOS() == "win" then
			directory = "\""..directory.."\""
			os.execute("mkdir "..directory)
		else
			directory = string.gsub(directory, " ", "\\ ")
			os.execute("mkdir "..directory)
		end
	end
end

-- **************************************************
-- Get Operating System, returns "win" or "unix"
-- **************************************************
function FO_Utilities:getOS()
	if os.getenv("OS") ~= nil
	then
		local opSys = string.lower(string.sub(os.getenv("OS"), 1, 3))
		if opSys == "win" then
			return "win"
		else
			return "unix"
		end
	else
		return "unix"
	end
end

-- **************************************************
-- Get Moho Version
-- **************************************************
function FO_Utilities:MohoVersion(moho)
	if moho.AppVersion ~= nil then
		local sVersion = string.gsub(moho:AppVersion(), "^(%d+)(%.%d+)(%..+)", "%1%2")
		version = tonumber(sVersion)
		return version
	end
end

-- **************************************************
-- Open text file in text editor
-- **************************************************
function FO_Utilities:EditTextFile(path)
	local command
	local editor = "Sublime Text"
	if(FO_Utilities:getOS()=="win") then
		local editorpath = "C:\\Program Files\\Sublime Text 3\\sublime_text.exe"
		path = string.gsub(path, "/", "\\") -- * Reverse and double slashes for Windows
		command = "start \""..editor.."\" \""..editorpath.."\" \""..path.."\""
	else
		path = string.gsub(path, " ", "\\ ") -- * Add backslash to space
		command = "open -a \""..editor.."\" "..path
	end
	FO_Utilities:Execute(command)
	-- ************************************************************************
	-- *** In "Sublime Text" config, add: 'open_files_in_new_window": true,'
	-- ************************************************************************
end

-- **************************************************
-- Get bone's basename without all the bonetags
-- **************************************************
function FO_Utilities:BaseName(boneName)
	local tags = FO_Utilities.boneTags
	local baseName = boneName
	for word in string.gmatch(boneName, "%S+") do
		for i = 1, #tags do
			local tag = tags[i]
			if string.match(word, tag) then
				baseName = string.gsub(baseName, tag, "")
			end
		end
	end
	baseName = FO_Utilities:CleanString(baseName)
	baseName = string.gsub(baseName, '[ \t]+%f[\r\n%z]', '') -- trim whitespace at start and end of string
	return baseName
end

-- **************************************************
-- Get distance between vectors
-- **************************************************
function FO_Utilities:Distance(moho, vector1, vector2)
	local distance = vector1 - vector2
	distance = math.abs(distance:Mag())
	return distance
end

-- **************************************************
-- Check if bone has a smartbone action
-- **************************************************
function FO_Utilities:IsASmartBone(moho, skel, bone)
	local name = bone:Name()
	if not moho.layer:HasAction(name) then
		return false
	end
	if string.find(name, "|") then -- ?
		return false
	end
	local boneID = skel:BoneID(bone)
	local layers = FO_Utilities:AllLayers(moho)
	for i = 1, #layers do
		local layer = layers[i]
		if layer:ControllingSkeleton() == skel then
			local parentBone = layer:LayerParentBone()
			if parentBone >= 0 then 
				if parentBone == boneID then
					return false
				end
			else
				local meshLayer = moho:LayerAsVector(layer)
				if meshLayer then
					local mesh = meshLayer:Mesh()
					for p=0, mesh:CountPoints()-1 do
						if mesh:Point(p).fParent == boneID then
							return false
						end
					end
				end
			end
		end
	end
	return true
end

--- **************************************************
-- Checks if layer is animated
-- **************************************************
function FO_Utilities:LayerIsAnimated(moho, layer)
	if not layer:IsReferencedLayer() then
		if not self:NonKeysTagLayer(layer, moho) then
			layer:ClearLayerKeyCount()
			if (layer:CountLayerKeys() > 2) then
				return true
			else
				return false
			end
		end
	end
end

-- **************************************************************************
-- Check if two values are equel:
-- (Credits: A.Evseeva)
-- **************************************************************************
function FO_Utilities:IsEqualValues(channel, val1, val2, accuracy)
	accuracy = accuracy or 0.0000001
	local chanType = channel:ChannelType()
	if chanType == MOHO.CHANNEL_VAL then return (math.abs(val1 - val2) < accuracy) 
	elseif chanType == MOHO.CHANNEL_VEC2 then  return (((val1-val2):Mag()) < accuracy)
	elseif chanType == MOHO.CHANNEL_VEC3 then return (((val1-val2):Mag()) < accuracy)
	elseif chanType == MOHO.CHANNEL_COLOR then return val1.r == val2.r and val1.g == val2.g and val1.b == val2.b and val1.a == val2.a 
	elseif chanType == MOHO.CHANNEL_BOOL then return (val1 == val2) 
	elseif chanType == MOHO.CHANNEL_STRING then return (val1 == val2)
	end
	return false
end

-- **************************************************
-- Split String
-- **************************************************
function string.split(s, delimiter)
	result = {}
	local first = true
	for match in (s..string.gsub(delimiter, "%%", "")):gmatch("(.-)"..delimiter) do
		if first or delimiter == " " then
			table.insert(result, match)
			first = false
		else
			table.insert(result, string.gsub(delimiter, "%%", "")..match)
		end
	end
	return result
end

-- **************************************************
-- Alternative (better?) Split String Function
-- **************************************************
--[[
function string.split(text, sep, plain)
	-- * Emulate Python split function
	-- ##########################################################################
	-- # split() function for lua
	-- # Markus Nentwig 2007
	-- # This code is in the public domain and provided without any warranty.
	-- ##########################################################################
	local result = {}
	local searchPos = 1
	while true do
		local matchStart, matchEnd = string.find(text, sep, searchPos, plain)
		if matchStart and matchEnd >= matchStart then
			-- insert string up to separator into result
			table.insert(result, string.sub(text, searchPos, matchStart-1))
			-- continue search after separator
			searchPos = matchEnd+1
		else
			-- insert whole reminder as result
			table.insert(result, string.sub(text, searchPos))
			break
		end
	end
	return result
end
--]]


-- **************************************************
-- Capitalize first character of a string
-- **************************************************
function string:CapitalizeFirst(str)
	return (str:gsub("^%l", string.upper))
end

-- **************************************************
-- Collapse all groups in the layer panel
-- **************************************************
function FO_Utilities:CollapseGroups(moho)
	local layers = FO_Utilities:AllLayers(moho)
	local i
	for i = 1, #layers do
		layer = layers[i]
		if layer:IsGroupType() == true then
			layer:Expand(false)
		end
	end
end

-- **************************************************************************
-- Dummy function to make sure no errors are thrown if Debug.lua is not found
-- **************************************************************************
Debug = {}
function Debug:Log()
	return
end

function FO_Utilities:InSmartBoneAction(moho)
	local action = moho.document:CurrentDocAction()
	if action ~= "" then
		-- * Only check if Action is a Smartbone-Action if we're in an Action:
		if moho.layer:IsSmartBoneAction(action) then
			return true
		end
	end
	return false
end

-- **************************************************
-- Removes 'forbidden' keys on bones such as:
-- T/S on sb-dials
-- R/S on sb-targets
-- Any keys except a T on frame 1 for the GUI-bone
-- **************************************************
function FO_Utilities:RemoveForbiddenBoneKeys(moho)
	local skel = FO_Utilities:GetSkel(moho)
	if skel == nil then
		return
	end
	local inSmartboneAction = self:InSmartBoneAction(moho)
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		local boneName = bone:Name()
		if self:IsGUIBone(bone) then -- * GUI-bone
			bone.fAnimPos:ClearAfter(1)
			bone.fAnimAngle:ClearAfter(1)
			bone.fAnimScale:Clear()
		elseif not inSmartboneAction and bone.fShy then -- * Shy bones can be keyed in Smartbone actions, but not in the Mainline!
			bone.fAnimPos:Clear()
			bone.fAnimAngle:Clear()
			bone.fAnimScale:Clear()
		elseif FO_Utilities:IsSmartboneDial(skel, bone) then -- * Smartbone-dial
			bone.fAnimPos:Clear()
			bone.fAnimScale:Clear()
		elseif bone:IsZeroLength() and bone:IsLabelShowing() == true then -- * Smartbone-target
			bone.fAnimAngle:Clear()
			bone.fAnimScale:Clear()
		elseif string.match(bone:Name(), self.switchTag) then -- * Switch (slave) bones
			bone.fAnimPos:Clear()
			bone.fAnimAngle:Clear()
			bone.fAnimScale:Clear()
		end
		-- * Remove keys from fully constrained bones:
		if LK_BoneSettings:Constraint(bone.fAnimAngle) or bone.fMinConstraint == 0 and bone.fMaxConstraint == 0 then
			-- * TODO channel:Clear() BREAKS REFERENCED CHANNELS! FIX THIS!
			bone.fAnimAngle:Clear() -- * TODO
		end
		if LK_BoneSettings:Constraint(bone.fAnimPos) then
			bone.fAnimPos:Clear() -- * TODO
		end
		if LK_BoneSettings:Constraint(bone.fAnimScale) then
			bone.fAnimScale:Clear() -- * TODO
		end
	end
end

-- **************************************************
-- Paints keys the same color as the bones they belong too
-- **************************************************
function FO_Utilities:PaintKeys(moho)
	local c, i
	local doc = moho.document
	local layer = moho.layer
	local cvStep
	local totalSel = 0
	if layer:IsBoneType() then
		local boneLayer = moho:LayerAsBone(layer)
		local skel = boneLayer:Skeleton()
		if skel:CountBones() > 0 then -- if some ...
			local channels = {}
			for i = 0, skel:CountBones() - 1 do
				bone = skel:Bone(i)
				boneColor = bone:Tags() 
				totalSel = 0 -- no keys so far for this bone
				c = 0
				c = c + 1; channels[c] = skel:Bone(i).fAnimPos
				c = c + 1; channels[c] = skel:Bone(i).fAnimAngle
				c = c + 1; channels[c] = skel:Bone(i).fAnimScale
				c = c + 1; channels[c] = skel:Bone(i).fAnimParent
				c = c + 1; channels[c] = skel:Bone(i).fTargetBone
				c = c + 1; channels[c] = skel:Bone(i).fFlipH
				c = c + 1; channels[c] = skel:Bone(i).fFlipV
				c = c + 1; channels[c] = skel:Bone(i).fIKGlobalAngle
				c = c + 1; channels[c] = skel:Bone(i).fIKLock
				c = c + 1; channels[c] = skel:Bone(i).fIKParentTarget
				c = c + 1; channels[c] = skel:Bone(i).fPhysicsMotorSpeed
				for c = 1, #channels do
					channel = channels[c]
					local endKey = channel:Duration()
					local thisKey = 0 -- the id of the key being processed
					local keyCount = channel:CountKeys() -- (-1 to ignore frame 0)
					local keysFound = 0
					local frameNum = endKey
					while keysFound < keyCount do
						thisKey = channel:GetClosestKeyID(frameNum)
						keyFrameNum = channel:GetKeyWhen(thisKey)
						keysFound = 1 + keysFound
						if (keyFrameNum ~= 0) then -- todo, check if this breaks referenced channels
							local interp = MOHO.InterpSetting:new_local()
							channel:GetKeyInterp(keyFrameNum, interp)
							interp.tags = boneColor
							channel:SetKeyInterp(keyFrameNum, interp)
						end
						frameNum = keyFrameNum - 1
					end
				end
			end
		end
	end
	self:StepKeys(moho) --todo betere plek, en misschien totaal niet in zn eigen functie maar inbakken bij het zetten en cleanen van keys.
	-- Update UI
	--moho:UpdateSelectedChannels() -- ?
	--moho.layer:UpdateCurFrame() -- ?
	--moho:UpdateUI()
end

-- ****************************************************************************************************************
-- *** Set keys from bones marked '!step' to step. ***
-- ****************************************************************************************************************
function FO_Utilities:StepKeys(moho)
	local c, i, j, k, n, p, t
	local doc = moho.document
	local layer = moho.layer
	local cvStep
	local totalSel = 0
	if layer:IsBoneType() then
		local boneLayer = moho:LayerAsBone(layer)
		local skel = boneLayer:Skeleton()
		if skel:CountBones() > 0 then -- if some ...
			local channels = {}
			for i = 0, skel:CountBones() - 1 do
				bone = skel:Bone(i)
				boneName = bone:Name()
				if LK_BoneSettings:ForceStep(bone) then
					totalSel = 0 -- no keys so far for this bone
					c = 0
					c = c + 1; channels[c] = skel:Bone(i).fAnimPos
					c = c + 1; channels[c] = skel:Bone(i).fAnimAngle
					c = c + 1; channels[c] = skel:Bone(i).fAnimScale
					c = c + 1; channels[c] = skel:Bone(i).fAnimParent
					c = c + 1; channels[c] = skel:Bone(i).fTargetBone
					c = c + 1; channels[c] = skel:Bone(i).fFlipH
					c = c + 1; channels[c] = skel:Bone(i).fFlipV
					c = c + 1; channels[c] = skel:Bone(i).fIKGlobalAngle
					c = c + 1; channels[c] = skel:Bone(i).fIKLock
					c = c + 1; channels[c] = skel:Bone(i).fIKParentTarget
					c = c + 1; channels[c] = skel:Bone(i).fPhysicsMotorSpeed
					for c = 1, #channels do
						channel = channels[c]
						local endKey = channel:Duration()
						local thisKey = 0 -- the id of the key being processed
						local keyCount = channel:CountKeys() -- (-1 to ignore frame 0)
						local keysFound = 0
						local frameNum = endKey
						while keysFound < keyCount do
							thisKey = channel:GetClosestKeyID(frameNum)
							keyFrameNum = channel:GetKeyWhen(thisKey)
							keysFound = 1 + keysFound
							if (keyFrameNum ~= 0) then -- todo, check if this breaks referenced channels
								local interp = MOHO.InterpSetting:new_local()
								channel:GetKeyInterp(keyFrameNum, interp)
								interp.interpMode = 3 -- 3 = INTERP_STEP
								channel:SetKeyInterp(keyFrameNum, interp)
							end
							frameNum = keyFrameNum - 1
						end
					end
				end
			end
		end
	end
end


-- **************************************************
-- Sets a keyframe on all sub-switch-layers that are checked as visible in the timeline
-- **************************************************
function FO_Utilities:KeyAllCheckedSubSwitches(moho)
	-- * Code doesn't work!? TODO
	local layers = FO_Utilities:AllLayers(moho)
	for i = 1, #layers do
		local layer = layers[i]	
		local switchLayer = moho:LayerAsSwitch(layer)
		if (layer:IsAncestorSelected() == true and layer:IsShownOnTimeline() == true and switchLayer) then
			local switchValue = switchLayer:GetValue(moho.frame)
			switchLayer:SetValue(moho.frame, switchValue)
		end
	end
end

function FO_Utilities:IsSmartboneDial(skel, bone)
	return (bone:IsLabelShowing() == true and self:IsGUIBone(skel:Bone(bone.fParent)))
end

function FO_Utilities:IsSmartboneTarget(bone)
	return (bone:IsZeroLength() and bone:IsLabelShowing() == true)
end

function FO_Utilities:IsGUIBone(bone)
	if bone == nil then -- * In case we are checking if a bone's parent is a GUIbone and it has no parent.
		return false
	end
	return (bone:Name() == self.guiTag)
end

function FO_Utilities:IsPinBone(bone)
	return (bone.fLength == 0 and not bone:IsLabelShowing() == true)
end

function FO_Utilities:GuiID(skel)
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if FO_Utilities:IsGUIBone(bone) then
			return i
		end
	end
	return -1
end

-- function FO_Utilities:IsHipBone(bone)
-- 	return (string.match(bone:Name(), self.hipTag))
-- end

-- **************************************************
-- Freeze shown bones
-- **************************************************
function FO_Utilities:FreezeShownBones(moho)
	local skel = FO_Utilities:GetSkel(moho)
	if skel == nil then
		return
	end
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if not bone.fHidden then
			local setT = true
			local setS = true
			local setR = true
			-- * Smartbonedials:
			if FO_Utilities:IsSmartboneDial(skel, bone) then
			-- if FO_Utilities:IsSmartboneDial(skel, bone) then
				setS = false
				setT = false
			--[[ * Disabled smartbone-Targets:
			elseif bone:IsZeroLength() and bone:IsLabelShowing() == true then
				setS = false
				setR = false
			--]]
			end
			-- Check bonetags:
			local boneName = bone:Name()
			if (LK_BoneSettings:Constraint(bone.fAnimPos)) then
				setT = false
			end
			if (LK_BoneSettings:Constraint(bone.fAnimScale)) then
				setS = false
			end
			if (LK_BoneSettings:Constraint(bone.fAnimAngle)) then
				setR = false
			end
			-- Set allowed keys:
			if setT then
				local pos = bone.fAnimPos:GetValue(moho.frame)
				bone.fAnimPos:SetValue(moho.frame, pos)
			end
			if setS then
				local scale = bone.fAnimScale:GetValue(moho.frame)
				bone.fAnimScale:SetValue(moho.frame, scale)
			end
			if setR then
				local angle = bone.fAnimAngle:GetValue(moho.frame)
				bone.fAnimAngle:SetValue(moho.frame, angle)
			end
		end
	end
end

-- **************************************************
-- Rename bone including smartbone actions
-- **************************************************
function FO_Utilities:RenameBoneAndAction(bone, newName, moho)
	local oldName = bone:Name()
	bone:SetName(newName)
	local layer = moho.layer
	if layer:HasAction(oldName) then
		layer:RenameAction(oldName, newName)
	end
	if layer:HasAction(oldName.." 2") then
		layer:RenameAction(oldName.." 2", newName.." 2")
	end
end


-- **************************************************
-- Find Nearest Point on a line
-- **************************************************
function FO_Utilities:NearestPointOnLine(linePoint, lineDirection, pos)
	lineDirection:NormMe()
	local v = pos - linePoint
	local d = lineDirection:Dot(v)
	local nearestPos = linePoint + lineDirection * d
	return nearestPos
end

-- **************************************************
-- Try to go to the next (or previous) Switch Key
-- **************************************************
function FO_Utilities:GoToNextSwitchKey(moho, previous)
	previous = previous or false
	local FBFLayer = nil -- * Reset
	if moho.layer:LayerType() == MOHO.LT_SWITCH then
		FBFLayer = moho:LayerAsSwitch(moho.layer )-- * Current layer is a switch!
	elseif moho.layer:AncestorSwitchLayer() ~= nil then
		FBFLayer = moho.layer:AncestorSwitchLayer() -- * Parent layer is switch!
	end
	if FBFLayer ~= nil then
		self:PrevNextSwitchKey(FBFLayer, moho.frame, previous, true, moho)
	end
end

-- **************************************************
-- Go to the next (or previous) switch key
-- **************************************************
function FO_Utilities:PrevNextSwitchKey(switchLayer, startFrame, previous, gotoframe, moho)
	-- *
    local channel = switchLayer:SwitchValues()
    local endFrame = 1
    if gotoframe or (channel:CountKeys() > 1) then
    	endFrame = 0
    end
    local found_key = false
	if not previous then
		local lastKey = channel:CountKeys()
		local lastKeyFrame = channel:GetKeyWhen(lastKey-1)
		endFrame = lastKeyFrame
	end
	-- *
	if (endFrame < moho.frame and not previous) then
		return found_key
	end
	--
    local increment
    local timing_offset
    if endFrame > startFrame then
        increment = 1
    else
        increment = -1
    end
    startFrame = startFrame + increment
    -- *
    if switchLayer.TotalTimingOffset ~= nil then
    	timing_offset = switchLayer:TotalTimingOffset()
    else
		timing_offset = 0
    end
    -- * Go through next/previous frames and stop when a keyframe is found
    for frame = startFrame, endFrame, increment do
    	-- * Going trough frames: " .. frame)
		if channel.HasKey ~= nil and channel:HasKey(frame) then
			found_key = true
		end
	    if found_key then
	    	if gotoframe then
		        moho:SetCurFrame(frame-timing_offset)
	        end
	        break
	    end
	end
    return found_key
end

-- **************************************************
-- Expose bones by color and toggle back:
-- **************************************************
function FO_Utilities:ToggleExposeColorBones(skel, color)
	-- * Get hidden en visible bones:
	local hiddenBones = {}
	local visibleBones = {}
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		local boneColor = bone:Tags()
		if not bone.fShy then
			if bone.fHidden then
				table.insert(hiddenBones, boneColor)
			elseif not bone.fHidden then
				table.insert(visibleBones, boneColor)
			end
		end
	end
	-- *
	local anyColorBoneExists = {}
	local anyColorBoneIsVisible = {}
	-- *
	for i = 0, 11 do -- * 11 = last color
		if table.contains(hiddenBones, i) then
			table.insert(anyColorBoneExists, i)
		elseif table.contains(visibleBones, i) then
			table.insert(anyColorBoneExists, i)
			table.insert(anyColorBoneIsVisible, i)
		end
	end
	-- *
	if table.contains(anyColorBoneExists, color) then -- IF minstens 1 bone van deze kleur (gekozen) bestaat THEN
		local expose = false
		for i = 0, 11 do
			if i ~= color then
				if table.contains(anyColorBoneIsVisible, i) then -- IF minstens 1 bone van deze kleur (any) is niet hidden THEN
					expose = true
					break
				end
			end
		end
		if not expose then
			for i = 0, skel:CountBones() - 1 do
				local bone = skel:Bone(i)
				if not bone.fShy then
					bone.fHidden = false
				elseif bone.fShy and layer:IsSmartBoneAction(action) and boneColor ~= color then
					bone.fHidden = false
				end
			end
		else
			for i = 0, skel:CountBones() - 1 do
				local bone = skel:Bone(i)
				local boneColor = bone:Tags()
				if boneColor == color and not bone.fShy then
					bone.fHidden = false
				else
					bone.fHidden = true
				end
			end
		end
	end
end

-- ********************************************************************
-- Get root bones (hip and feet, or simply all bones without a parent):
-- ********************************************************************
function FO_Utilities:RootBones(skel, color)
	local rootBones = {}
	if #rootBones == 0 then
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if not bone.fHidden then
				local bone = skel:Bone(i)
				if bone.fParent == -1 then
					if color == nil or bone:Tags()==color then
						table.insert(rootBones, bone)
					end
				end
			end
		end
	end
	return rootBones
end

-- **************************************************
-- Toggles a tag in selected bones' names
-- **************************************************
function FO_Utilities:ToggleTagSelectedBones(tag, moho)
	local skel = moho:Skeleton()
	local boneCount = skel:CountBones()
	local tagMatch = false
	local tagNotfound = false
	local doAddTag = false
	-- * Round 1 - Check if any of the selected bones already has the tag but other don't. Because in that case we should add the tag to all bones which don't have it yet.")
	for i = 0, boneCount - 1 do
		local bone = skel:Bone(i)
		if bone.fSelected then
			tags = bone:Name()
			if string.match(tags, tag) then
				tagMatch = true
			else
				tagNotfound = true
			end
		end
	end
	-- * Decide if we need to add or remove the tag from all bones")
	if tagNotfound == true then --or tagMatch == false then
		Debug:Log ("We have not found the tag in any of the selected bones' names.")
		doAddTag = true
	end
	-- * Round 2 - Actually do it per selected bone")
	for i = 0, boneCount - 1 do
		local bone = skel:Bone(i)
		if bone.fSelected then
			if doAddTag == false then -- Remove tag from bone
				FO_Utilities:RemoveTagFromBone(tag, bone, moho)
			elseif doAddTag == true then -- Add tag to each bone
				FO_Utilities:AddTagToBone(tag, bone, moho)
			end
		end
	end
	moho:UpdateUI() -- So the toolbar's widgets get updated.
end

-- **************************************************
-- Adds a tag to a bone's name
-- **************************************************
function FO_Utilities:AddTagToBone(tag, bone, moho) -- moho eraf gehaald?
	local boneName = bone:Name()
	if boneName == "" then
		boneName = tag
	elseif not string.match(boneName, tag) then
		boneName = boneName .. " " .. tag -- Adds tag to boneName
	end
	boneName = FO_Utilities:CleanString(boneName)
	FO_Utilities:RenameBoneAndAction(bone, boneName, moho)
end

-- **************************************************
-- Removes a tag from a bone's name
-- **************************************************
function FO_Utilities:RemoveTagFromBone(tag, bone, moho)
	local boneName = bone:Name()
	local spaceTag = " " .. tag
	boneName = boneName:gsub(spaceTag, "")
	boneName = boneName:gsub(tag, "")
	boneName = FO_Utilities:CleanString(boneName)
	FO_Utilities:RenameBoneAndAction(bone, boneName, moho)
end

FO_Utilities
Listed

Author: Lukas View Script
Script type: Utility

Uploaded: Apr 19 2022, 07:03

Last modified: Jul 18 2022, 12:08

Utility file needed for scripts by Lukas Krepel, Frame Order
This script, and all other scripts on this site are distributed as free software under the GNU General Public License 3.0 or later.
Downloads count: 818