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
-- *********
-- * Switchbone boneTag
FO_Utilities.switchTag = ".switch"
-- * Switchbone control tags:
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 = { "!t", "!x", "!y", "!r", "!s", "_step", "!hip", "!foot", 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

-- **************************************************
-- Adjust toolbar display to screen width
-- **************************************************
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

-- ************************************************************
-- Draw custom UI things in the viewport if FO_Draw is present
-- ************************************************************
function FO_Utilities:DrawMeMod(moho, view, mousePickedID)
	if FO_Draw ~= nil then
		FO_Draw:DrawMeMod(moho, view, mousePickedID)
	end
end

-- **************************************************
-- Reload tools by hopping in and out of frame 0
-- **************************************************
function FO_Utilities:ReloadTools(moho)
	local returnFrame = moho.frame
	local tempFrame = 0
	local rememberDisabledDrawingToolsNonZero = MOHO.MohoGlobals.DisableDrawingToolsNonZero
	MOHO.MohoGlobals.DisableDrawingToolsNonZero = true
	if returnFrame == tempFrame then
		tempFrame = 1
	end
	moho:SetCurFrame(tempFrame)
	moho:SetCurFrame(returnFrame)
	MOHO.MohoGlobals.DisableDrawingToolsNonZero = rememberDisabledDrawingToolsNonZero
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..":"))
	end
end

function FO_Utilities:DialogDivider(layout, title, first)
	if not first then
		layout:AddChild(LM.GUI.Divider(true), LM.GUI.ALIGN_FILL, 0) -- * Divider
	end
	if title ~= nil then
		local longEmptyString = "                                                                                                                       "
		layout:AddChild(LM.GUI.StaticText(title..":"..longEmptyString))
	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 = "/Library/Frameworks/Python.framework/Versions/3.9/bin/python3"
		Debug:Log("pythonVersion = "..pythonVersion)
		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, moho)
	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
		if self:MohoVersion(moho) < 14 then
			output = io.popen(command)
		else
			output = os.execute(command.." &") -- * The & makes it run as a background process.
		end
	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
	Debug:Log("output = "..tostring(output))
	return output -- * returns true if an error is thrown, otherwise nil.
end

-- **************************************************
-- Reveal directory in Finder/Explorer
-- **************************************************
function FO_Utilities:RevealDirectory(path)
	local command
	local output
	if self:getOS() == "win" then
		path = string.gsub(path, "/", "\\")
		command = "explorer \""..path.."\""
		output = os.execute(command)
		Debug:Log("Windows: output = "..tostring(output))
	else
		command = "open "..string.gsub(path, " ", "\\ ")
		if FO_Utilities:MohoVersion() >= 14 then -- * Without moho argument this returns 0 or 14+
			output = os.execute(command)
		else
			output = io.popen(command) -- * Doesn't work in Moho 14 anymore.
		end
		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
	local rigLayer = FO_Utilities:RigLayer(moho)--fix?
	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

-- ****************************************************************************
-- 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

-- ******************************************************
-- 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, doc)
	doc = doc or moho.document
	-- print ("FO_Utilities:AllLayers(moho)")
	local layers = {}
	local stack = {}
	local sp = 0
	for i=0, doc:CountLayers()-1 do
		local layer = doc: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


-- *********************************************************
-- Like AllLayers() but skipping non visible and STB layers
-- *********************************************************
function FO_Utilities:ShotLayers(moho, doc)
	doc = doc or moho.document
	-- print ("FO_Utilities:ShotLayers(moho, doc)")
	local layers = {}
	local stack = {}
	local sp = 0
	for i=0, doc:CountLayers()-1 do
		local layer = doc:Layer(i)
		--
		--
		if (string.match(string.lower(layer:Name()), string.lower("STB"))) then
			-- break
		elseif not layer:IsVisible() then
			-- break
		else
			--
			--
			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

-- *********************************************************
-- Make sure target_doc imports all styles form source_doc
-- *********************************************************
function FO_Utilities:MatchStyles(source_doc, target_doc)
	for i = 0, source_doc:CountStyles() - 1 do
		local sourceStyle = source_doc:StyleByID(i)
		local alreadyExists = false
		for j = 0, target_doc:CountStyles() - 1 do
			local targetStyle = target_doc:StyleByID(j)
			if sourceStyle:ArePropertiesEqual(targetStyle) then
				alreadyExists = true
				break
			end
		end
		if not alreadyExists then
			target_doc:AddStyle(sourceStyle)
		end
	end
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:ListLayersOfType(type, moho)
	local typeLayers = {}
	local layers = FO_Utilities:AllLayers(moho)
	local i
	for i = 1, #layers do
		layer = layers[i]
		if layer:LayerType() == type then
			table.insert(typeLayers, layer)
		end
	end
	return typeLayers
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


-- ***********************************************************************
-- Removes (v000_X) from string
-- ***********************************************************************
function FO_Utilities:StripRigVersionFromString(str)
	return string.gsub(str, " [(][Vv]%d+_[A-z]+[)]", "")  --" %(_v%d%d%d_%u+%)"
end

-- ***********************************************************************
-- Strip everything before the last slash of a full filepath and return filename
-- ***********************************************************************
function FO_Utilities:FileName(path, stripExtension)
	-- extension = extension or true -- * Doesn't work!?
	path = string.gsub(path, "\\", "/")
	local lastslashpos = (path:reverse()):find("%/") -- * find last slash
	local fileName = (path:sub(-lastslashpos+1)) -- * filename only
	if stripExtension == true then -- * (stripExtension == nil/false will return filename WITH exentsion)
		-- * Find the position of the last dot in the filename
		local dotIndex = fileName:find("%.[^%.]*$")
		-- * If a dot is found, remove the extension
		if dotIndex then
			fileName = fileName:sub(1, dotIndex - 1)
		end
	end
	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


-- **************************************************
-- Removes element from table
-- **************************************************
function table.removeElement(tab, element)
	for i, value in pairs(tab) do
        if value == element then
        	table.remove(tab, i)
        	return
        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, FO_Utilities.rigTag) 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:IsGroupType() then
			self:ShowAnimChildLayersOnTimeline(layer)
		end
		if string.match(tags, FO_Utilities.animTag) and not string.match(tags, FO_Utilities.rigTag) then
			layer:SetShownOnTimeline(true)
		end
	end
end

-- ***********************************************************************
-- Show all child layers of 'layer' that have keys
-- ***********************************************************************
function FO_Utilities:SecondarySelectKeyedChildLayers(moho, layer)
	local tags = layer:UserTags()
	if (string.match(tags, FO_Utilities.rigTag) and not layer:SecondarySelection()) then
		return
	end
	if layer:IsGroupType() then
		for i = 0, layer:CountLayers() - 1 do
			local layer = layer:Layer(i)
			tags = layer:UserTags()
			local tags = layer:UserTags()
			if layer:IsGroupType() then
				self:SecondarySelectKeyedChildLayers(moho, layer)
			end
			if FO_Utilities:LayerIsAnimated(moho, layer) and not string.match(tags, FO_Utilities.rigTag) then
				layer:SetSecondarySelection(true)
				--
				moho:ShowLayerInLayersPalette(layer) -- * TODO, make optional?
				--
			end
		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
	local rigLayer = FO_Utilities:RigLayer(moho)--fix?
	if rigLayer ~= nil then
		Debug:Log("Removed comment from "..moho.layer:Name().." ("..moho.layer:UserComments()..")")
		rigLayer:SetUserComments("")
		-- * Force viewport to redraw itself:
		MOHO.Redraw()
		-- * Update UI so timeline channels show up and dissapear correctly:
		moho:UpdateUI()
	end
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, 0, "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, 0, 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 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
	--
	local d = LM.GUI.Alert(
		type,
		msg1,
		msg2,
		msg3,
		"Yes",
		"No",
		nil
		) -- * Returns 0 or 1
	if d == 0 then
		return true
	else
		return false
	end
end


function FO_Utilities:MultipleChoiceQuestion(...)--question, choice0, choice1, choice2) --last three must be choices or nil
	local arg = {...}
	if (type(...) == "table") then
		arg = ...
	end
	if #arg < 4 then
		print("ERROR too little arguments, at least 4 needed, last 3 are buttons/answers (can be nil)")
	end
	local question = arg[1]
	local choice0 = arg[#arg-2]
	local choice1 = arg[#arg-1]
	local choice2 = arg[#arg]
	-- # Remove answers:
	table.remove(arg, #arg)
	table.remove(arg, #arg)
	table.remove(arg, #arg)
	local messages = arg
	-- print("question = "..question)
	-- print("choice0 = "..choice0)
	-- print("choice1 = "..choice1)
	-- print("choice2 = "..choice2)
	-- -- * Convert messages:
	local bigMessage = ""
	for i = 2, #messages do
		bigMessage = bigMessage..messages[i].."\n"
	end
	-- * Multiple choice dialog:
	local answer = LM.GUI.Alert(
		LM.GUI.ALERT_QUESTION,
		messages[1],
		bigMessage,
		nil,
		choice0,
		choice1,
		choice2) -- * Returns 0, 1 or 2
	-- print("answer = "..tostring(answer))
	return answer
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)
	local helper -- * Uh
	if moho == nil then
		if MOHO.ScriptInterfaceHelper == nil then
			-- print("MOHO.ScriptInterfaceHelper == nil")
			return 0.0 -- * dumb broken temp solution hack TODO
		end
		helper = MOHO.ScriptInterfaceHelper:new_local()
		moho = helper:MohoObject()
	end
	if moho.AppVersion ~= nil then
		local sVersion = string.gsub(moho:AppVersion(), "^(%d+)(%.%d+)(%..+)", "%1%2")
		version = tonumber(sVersion)
	end
	if helper ~= nil then
		helper:delete()
	end
	return version
end

-- **************************************************
-- Open text file in text editor
-- **************************************************
function FO_Utilities:EditTextFile(path, moho)
	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, false, false, moho)
	-- ************************************************************************
	-- *** 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(0)
		elseif not inSmartboneAction and bone.fShy then -- * Shy bones can be keyed in Smartbone actions, but not in the Mainline!
			bone.fAnimPos:Clear(0)
			bone.fAnimAngle:Clear(0)
			bone.fAnimScale:Clear(0)
		elseif FO_Utilities:IsSmartboneDial(skel, bone) then -- * Smartbone-dial
			bone.fAnimPos:Clear(0)
			bone.fAnimScale:Clear(0)
		elseif bone:IsZeroLength() and bone:IsLabelShowing() == true then -- * Smartbone-target
			bone.fAnimAngle:Clear(0)
			bone.fAnimScale:Clear(0)
		elseif string.match(bone:Name(), self.switchTag) then -- * Switch (slave) bones
			bone.fAnimPos:Clear(0)
			bone.fAnimAngle:Clear(0)
			bone.fAnimScale:Clear(0)
		end
		if LK_BoneSettings ~= nil then
			-- * 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(0) -- * ??? 0 is frameZero argument, could go wrong?
			end
			if LK_BoneSettings:Constraint(bone.fAnimPos) then
				bone.fAnimPos:Clear(0) -- * ??? 0 is frameZero argument, could go wrong?
			end
			if LK_BoneSettings:Constraint(bone.fAnimScale) then
				bone.fAnimScale:Clear(0) -- * ??? 0 is frameZero argument, could go wrong?
			end
		end
	end
end

-- **************************************************
-- Paints keys the same color as the bones they belong too
-- **************************************************
function FO_Utilities:PaintKeys(moho)
	if not self.colorKeys then
		return
	end
	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
				local bone = skel:Bone(i)
				local 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
					local 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)
						local 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)
	if LK_BoneSettings == nil then
		return
	end
	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
				local bone = skel:Bone(i)
				local 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
						local 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)
							local 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
	FO_Utilities:StepBools(moho)
end

-- **************************************************
-- Sets all bool keys to green/red
-- **************************************************
function FO_Utilities:StepBools(moho, allLayers)
	allLayers = allLayers or false
	local layers = {}
	if allLayers then
		layers = FO_Utilities:AllLayers(moho)
	else
		for i = 0, moho.document:CountSelectedLayers() - 1 do
			local layer = moho.document:GetSelectedLayer(i)
			table.insert(layers, layer)
		end
	end

	for i = 1, #layers do
		local layer = layers[i]
		-- print(layer:Name())
	-- for i = 0, moho.document:CountSelectedLayers() - 1 do
		-- local layer = moho.document:GetSelectedLayer(i)
		local chInfo  =  MOHO.MohoLayerChannel:new_local()
		-- * Iterate channels:
		for i = 0, layer:CountChannels()-1 do
			layer:GetChannelInfo(i, chInfo)
			if not chInfo.selectionBased then
				for j=0, chInfo.subChannelCount-1 do
					local subChannel = layer:Channel(i, j, moho.document)
					local channel = nil
					if subChannel:ChannelType() == MOHO.CHANNEL_BOOL then
						channel = moho:ChannelAsAnimBool(subChannel)
						if channel and not (chInfo.name:Buffer() == "All Channels") then
							local endKey = channel:Duration()
							local thisKey = 0 -- * ID of the key being processed.
							local keyCount = channel:CountKeys() -- * Including frame 0.
							local keysFound = 0
							local frameNum = endKey
							while keysFound < keyCount do
								thisKey = channel:GetClosestKeyID(frameNum)
								frameNum = channel:GetKeyWhen(thisKey)
								if subChannel:ChannelType() == MOHO.CHANNEL_BOOL  then
									local interp = MOHO.InterpSetting:new_local()
									channel:GetKeyInterp(frameNum, interp)
									interp.interpMode = 3 -- * 3 = INTERP_STEP
									if channel:GetValue(frameNum) then
										interp.tags = 4 -- * 4 = Green
									else
										interp.tags = 1 -- * 1 = Red
									end
									channel:SetKeyInterp(frameNum, interp)
								end
								keysFound = 1 + keysFound
								frameNum = frameNum - 1
							end
						end
					end
				end
			end
		end
	end
end

function FO_Utilities:PointsColorKeys(moho)
	return -- * Disabled, buggy
	-- local mesh = moho:DrawingMesh()
	-- for i = 0, mesh:CountGroups()-1 do
	-- 	local group = mesh:Group(i)
	-- 	for j = 0, group:CountPoints()-1 do
	-- 		local groupName = group:Name()
	-- 		if groupName ~= "Shy" then
	-- 			local color = 12
	-- 			for c = 1, #FO_Utilities.colorNames do
	-- 				local colorName = FO_Utilities.colorNames[c]
	-- 				if string.lower(groupName) == string.lower(colorName) then
	-- 					color = c
	-- 				end
	-- 			end
	-- 			local point = group:Point(j)
	-- 			local channels = {}
	-- 			table.insert(channels, point.fAnimPos)
	-- 			table.insert(channels, point.fWidth)
	-- 			-- * AS 11.0
	-- 			table.insert(channels, point.fColor)
	-- 			table.insert(channels, point.fColorStrength)
	-- 			for j = 0,  point:CountCurves()-1 do
	-- 				local curvePointID = mesh:PointID(point) --?
	-- 				local curve = point:Curve(j, curvePointID) --?
	-- 				-- curve, curvePointID = point:Curve(j, curvePointID) --?
	-- 				-- curve, curvePointID = point:Curve(j, 0) --?
	-- 				-- * AS 6.1
	-- 				if curve.Curvature ~= nil then
	-- 					table.insert(channels, curve:Curvature(curvePointID))
	-- 				end
	-- 			end
	-- 			-- *
	-- 			for i = 1, #channels do
	-- 				local channel = channels[i]
	-- 				--
	-- 				local channel = channels[i]
	-- 				local endKey = channel:Duration()
	-- 				local thisKey = 0 -- * ID of the key being processed.
	-- 				local keyCount = channel:CountKeys() -- * Including frame 0.
	-- 				local keysFound = 0
	-- 				local frameNum = endKey
	-- 				while keysFound < keyCount do
	-- 					thisKey = channel:GetClosestKeyID(frameNum)
	-- 					frameNum = channel:GetKeyWhen(thisKey)
	-- 					local interp = MOHO.InterpSetting:new_local()
	-- 					channel:GetKeyInterp(frameNum, interp)
	-- 					interp.tags = color
	-- 					channel:SetKeyInterp(frameNum, interp)
	-- 					keysFound = 1 + keysFound
	-- 					frameNum = frameNum - 1
	-- 				end
	-- 				--
	-- 			end
	-- 		end
	-- 	end
	-- end
	-- moho:UpdateUI()
	--
	--
	-- local mesh = moho:DrawingMesh()
	-- for i = 0, mesh:CountPoints() - 1 do
	-- 	local point = mesh:Point(i)
	-- 	local channels = {}
	-- 	table.insert(channels, point.fAnimPos)
	-- 	table.insert(channels, point.fWidth)
	-- 	-- * AS 11.0
	-- 	table.insert(channels, point.fColor)
	-- 	table.insert(channels, point.fColorStrength)
	-- 	for j = 0,  point:CountCurves()-1 do
	-- 		curve, curvePointID = point:Curve(j, curvePointID)
	-- 		-- * AS 6.1
	-- 		if curve.Curvature ~= nil then
	-- 			table.insert(channels, curve:Curvature(curvePointID))
	-- 		end
	-- 	end
	-- 	-- *
	-- 	for i = 1, #channels do
	-- 		local channel = channels[i]
	-- 		--
	-- 	end
	-- end
end

-- **************************************************
-- Sets percentage keys to green/orange/red
-- **************************************************
function FO_Utilities:PercentageKeyColors(moho)
	for i = 0, moho.document:CountSelectedLayers() - 1 do
		local layer = moho.document:GetSelectedLayer(i)
		channels = { layer.fAlpha }
		for i = 1, #channels do
			local channel = channels[i]
			local endKey = channel:Duration()
			local thisKey = 0 -- * ID of the key being processed.
			local keyCount = channel:CountKeys() -- * Including frame 0.
			local keysFound = 0
			local frameNum = endKey
			while keysFound < keyCount do
				thisKey = channel:GetClosestKeyID(frameNum)
				frameNum = channel:GetKeyWhen(thisKey)
				local interp = MOHO.InterpSetting:new_local()
				channel:GetKeyInterp(frameNum, interp)
				local percentage = channel:GetValue(frameNum) * 100
				percentage = math.floor(percentage + 0.5)
				if percentage == 100 then
					interp.tags = 4 -- * 4 = Green
				elseif percentage == 0 then
					interp.tags = 1 -- * 1 = Red
				elseif LK_LayerOpacity ~= nil and percentage == LK_LayerOpacity.presetValue then
					interp.tags = 6 -- * 6 = Purple
				else
					interp.tags = 2 -- * 2 = Orange
				end
				channel:SetKeyInterp(frameNum, interp)
				keysFound = 1 + keysFound
				frameNum = frameNum - 1
			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(layer, 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


-- **************************************************
-- Removes element from table
-- **************************************************
function table.reverse(tab) -- * temp disabled, needs new lua version
    -- for i = 1, #tab//2, 1 do
    --     tab[i], tab[#tab-i+1] = tab[#tab-i+1], tab[i]
    -- end
    return tab
end

FO_Utilities
Listed

Author: Lukas View Script
Script type: Utility

Uploaded: Apr 19 2022, 07:03

Last modified: Dec 01 2023, 08:07

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: 3142