HeyVern's scripts and tools - (focus on bones)

Moho allows users to write new tools and plugins. Discuss scripting ideas and problems here.

Moderators: Víctor Paredes, Belgarath, slowtiger

User avatar
Lukas
Posts: 1297
Joined: Fri Apr 09, 2010 9:00 am
Location: Netherlands
Contact:

Re: HeyVern's scripts and tools - (focus on bones)

Post by Lukas »

synthsin75 wrote: Wed Feb 09, 2022 5:12 am Yeah, I think we've talked about Moho not quite updating the moho.document reference before, especially with new, tabbed documents. Have you tried by having the script only have one open document at a time?
I got the basics of exporting and importing actions to moho files to work (with the keys in the mainline), during export I ended up simply modifying the current file and saving it as a new file for each action. When reverting back to the original file (because I also delete all layers except for the selected layer and its childlayers) it somehow crashes on the 2nd or 3rd time running the script. No idea why, especially because the first run always goes fine.

Code: Select all

-- **************************************************
-- Provide Moho with the name of this script object
-- ***************************************************

ScriptName = "LK_ExportActions"

-- ***************************************************
-- General information about this script
-- ***************************************************

LK_ExportActions = {}

function LK_ExportActions:Name()
	return "LK_ExportActions"
end

function LK_ExportActions:Version()
	return "0.1"
end

function LK_ExportActions:Description()
	return "Export Actions"
end

function LK_ExportActions:Creator()
	return "Lukas Krepel, Frame Order"
end

function LK_ExportActions:UILabel()
	return "Export Actions"
end

function LK_ExportActions:ColorizeIcon()
	return true
end

-- ***************************************************
-- The guts of this script
-- ***************************************************

function LK_ExportActions:IsRelevant(moho)
	if MohoMode ~= nil then
		return MohoMode.experimental
	else
		return true
	end
end

function LK_ExportActions:IsEnabled(moho)
	if MohoMode ~= nil then
		return MohoMode.experimental
	else
		return true
	end
end

function LK_ExportActions:Run(moho)
	-- * Directories:
	local path = moho.document:Path()
	if path == nil then
		print ("Save your file before exporting actions!")
		return
	end
	local layer = moho.layer
	local rigName = layer:Name()
	-- * Remove version from name. For example, if the rig layer is called "CrazyGuy (v003_L)", we are going to remove " (v003_L)" and use "CrazyGuy"
	if string.match(rigName, " %(") then
		rigName = string.sub(rigName, 0, string.find(rigName, " %(")-1)
	end
	local actionsDirectory = FO_Utilities:FileDirectory(path).."/Actions" -- * No last Slash here for FileExists check!
	if not FO_Utilities:FileExists(moho, actionsDirectory) then
		FO_Utilities:Alert({"Please make sure the 'Actions' directory exists:", "'"..actionsDirectory.."/'"})
		return
	end
	-- * Count poses and animations:
	local poses = {}
	local animations = {}
	for a=0, layer:CountActions()-1 do
		actionName = layer:ActionName(a)
		if not layer:IsSmartBoneAction(actionName) then
			local duration = layer:ActionDuration(actionName)
			if duration == 1 then
				table.insert(poses, actionName)
			elseif duration > 1 then
				table.insert(animations, actionName)
			end
		end
	end
	-- *
	if #poses == 0 and #animations == 0 then
		FO_Utilities:Alert(layer:Name().." has no Poses / Animations.")
		return
	end
	-- * Are you sure?
	moho:FileSave()
	local startLayerID = moho.document:LayerID(moho.layer)
	-- * Delete all other layers from project:
	self:MakeLayerSolo(moho)
	FO_Utilities:CreateDirectory(moho, actionsDirectory)
	local rigActionsDirectory = actionsDirectory.."/"..rigName.."/"
	FO_Utilities:CreateDirectory(moho, rigActionsDirectory)
	-- *
	local poses = {}
	local animations = {}
	for a=0, layer:CountActions()-1 do
		actionName = layer:ActionName(a)
		if not layer:IsSmartBoneAction(actionName) then
			local duration = layer:ActionDuration(actionName)
			if duration == 1 then
				table.insert(poses, actionName)
			elseif duration > 1 then
				table.insert(animations, actionName)
			end
		end
	end
	local posesDirectory = rigActionsDirectory.."Poses/"
	local animationsDirectory = rigActionsDirectory.."Animations/"
	-- *
	local messages = { "Created Moho-files in: '"..rigActionsDirectory.."'"}
	-- *
	if #poses > 0 then
		FO_Utilities:CreateDirectory(moho, posesDirectory)
		table.insert(messages, "In 'Pose(s)' subfolder:")
	end
	-- * First we create the files:
	for i = 1, #poses do
		local actionName = poses[i]
		local fileName = actionName..".moho"
		local actionPath = posesDirectory..fileName
		moho:FileSaveAs(actionPath)
		moho.document:ClearAnimation(0, false)
		table.insert(messages, i.." - "..fileName)
		moho.layer:InsertAction(actionName, 1, false)
		moho:FileSave()
	end
	-- *
	if #animations > 0 then
		FO_Utilities:CreateDirectory(moho, animationsDirectory)
		table.insert(messages, "In 'Animation(s)' subfolder:")
	end
	-- *
	for i = 1, #animations do
		local actionName = animations[i]
		local fileName = actionName..".moho"
		local actionPath = animationsDirectory..fileName
		moho:FileSaveAs(actionPath)
		moho.document:ClearAnimation(0, false)
		table.insert(messages, i.." - "..fileName)
		moho.layer:InsertAction(actionName, 1, false)
		moho:FileSave()
	end
	-- *
	local closePath = moho.document:Path()
	if closePath ~= path then
		moho:FileOpen(path)
		moho:FileOpen(closePath)
		moho:FileClose()
	end
	-- *
	FO_Utilities:Alert(messages)
end

function LK_ExportActions:MakeLayerSolo(moho)
	local layer0 = moho.document:Layer(0)
	moho:PlaceLayerBehindAnother(moho.layer, layer0)
	for i = moho.document:CountLayers()-1, 1, -1 do
		moho:DeleteLayer(moho.document:Layer(i))
	end
end


----------
----------

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"

-- *** 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?" }

-- * Settings:
FO_Utilities.tinyUI = false
FO_Utilities.tinyUITreshold = 1500
FO_Utilities.reverseBoneColorButtons = true
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

-- **************************************************
-- 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/Embedded Scripts/"
	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

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

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

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

-- ****************************************************
-- List layers with a specific tag, returns 'taglayers'
-- ****************************************************
function FO_Utilities:ListLayersWithTag(tag, moho) -- todo taglayers was ooit global, returned nu!
	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

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

-- **************************************************
-- 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", -- returns 0
		"No", -- returns 1
		nil
		)
	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

-- **************************************************
-- Paint keys of bones the same color as the bone
-- **************************************************
function FO_Utilities:PaintKeys(moho)
	local c, i
	local doc = moho.document
	local layer = moho.layer
	if layer:IsBoneType() then
		local boneLayer = moho:LayerAsBone(layer)
		local skel = boneLayer:Skeleton()
		if skel:CountBones() > 0 then
			local channels = {}
			for i = 0, skel:CountBones() - 1 do
				bone = skel:Bone(i)
				boneColor = bone:Tags()
				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
					local keyCount = channel:CountKeys()
					local keysFound = 0
					local frameNum = endKey
					while keysFound < keyCount do
						thisKey = channel:GetClosestKeyID(frameNum)
						keyFrameNum = channel:GetKeyWhen(thisKey)
						keysFound = 1 + keysFound
					   	local interp = MOHO.InterpSetting:new_local()
					    channel:GetKeyInterp(keyFrameNum, interp)
			   		    interp.tags = boneColor
			 		    channel:SetKeyInterp(keyFrameNum, interp)
						frameNum = keyFrameNum - 1
					end
				end
			end
		end
	end
end

function FO_Utilities:ColorizeBone(moho, bone, boneID, color)
	if FO_Draw:DoCustomSkel(moho) then
		bone:SetTags(0)
	else
		bone:SetTags(color)
	end
	local doubleDigitColor = (string.format("%02d" .. ".",color))
	local comment = moho.layer:UserComments()
	while string.len(comment) < skel:CountBones()*3 do
		comment = comment.."."
	end
	comment = (comment:sub(0, 3*boneID) .. doubleDigitColor .. (comment:sub(3*boneID+4)))
	moho.layer:SetUserComments(comment)
end

-- **************************************************************************
-- Dummy function to make sure no errors are thrown if Debug.lua is not found
-- **************************************************************************
Debug = {}
function Debug:Log()
	return
end
Last edited by Lukas on Thu Feb 10, 2022 4:00 pm, edited 1 time in total.
User avatar
synthsin75
Posts: 9973
Joined: Mon Jan 14, 2008 11:20 pm
Location: Oklahoma
Contact:

Re: HeyVern's scripts and tools - (focus on bones)

Post by synthsin75 »

I'll take a look when I get a chance.
User avatar
Lukas
Posts: 1297
Joined: Fri Apr 09, 2010 9:00 am
Location: Netherlands
Contact:

Re: HeyVern's scripts and tools - (focus on bones)

Post by Lukas »

I could not figure out what the issue was, but currently fixed it by simply undo'ing every single thing the script has done and saving it as the original file:

Code: Select all

-- **************************************************
-- Provide Moho with the name of this script object
-- ***************************************************

ScriptName = "LK_ExportActions"

-- ***************************************************
-- General information about this script
-- ***************************************************

LK_ExportActions = {}

function LK_ExportActions:Name()
	return "LK_ExportActions"
end

function LK_ExportActions:Version()
	return "0.2"
end

function LK_ExportActions:Description()
	return "Export Actions"
end

function LK_ExportActions:Creator()
	return "Lukas Krepel, Frame Order"
end

function LK_ExportActions:UILabel()
	return "Export Actions"
end

function LK_ExportActions:ColorizeIcon()
	return true
end

-- ***************************************************
-- The guts of this script
-- ***************************************************

function LK_ExportActions:IsRelevant(moho)
	if MohoMode ~= nil then
		return MohoMode.experimental
	else
		return true
	end
end

function LK_ExportActions:IsEnabled(moho)
	if MohoMode ~= nil then
		return MohoMode.experimental
	else
		return true
	end
end

function LK_ExportActions:Run(moho)
	moho.document:PrepMultiUndo(false)
	self.undoCount = 1
	-- * Directories:
	local path = moho.document:Path()
	if path == nil then
		print ("Save your file before exporting actions!")
		return
	end
	local layer = moho.layer
	local rigName = layer:Name()
	-- * Remove version from name. For example, if the rig layer is called "CrazyGuy (v003_L)", we are going to remove " (v003_L)" and use "CrazyGuy"
	if string.match(rigName, " %(") then
		rigName = string.sub(rigName, 0, string.find(rigName, " %(")-1)
	end
	local actionsDirectory = FO_Utilities:FileDirectory(path).."/Actions" -- * No last Slash here for FileExists check!
	if not FO_Utilities:FileExists(moho, actionsDirectory) then
		FO_Utilities:Alert({"Please make sure the 'Actions' directory exists:", "'"..actionsDirectory.."/'"})
		return
	end
	-- * Count poses and animations on current layer:
	local poses = {}
	local animations = {}
	for a=0, layer:CountActions()-1 do
		actionName = layer:ActionName(a)
		if not layer:IsSmartBoneAction(actionName) then
			local duration = layer:ActionDuration(actionName)
			if duration == 1 then
				table.insert(poses, actionName)
			elseif duration > 1 then
				table.insert(animations, actionName)
			end
		end
	end
	-- * Stop there's nothing to export:
	if #poses == 0 and #animations == 0 then
		FO_Utilities:Alert(layer:Name().." has no Poses / Animations.")
		return
	end
	-- * Directories:
	local rigActionsDirectory = actionsDirectory.."/"..rigName.."/"
	local posesDirectory = rigActionsDirectory.."Poses/"
	local animationsDirectory = rigActionsDirectory.."Animations/"
	-- * Create warning message:
	local messages = {}
	table.insert(messages, "CREATE and/or OVERWRITE these actions as .moho files in: '"..rigActionsDirectory.."'")
	if #poses > 0 then
		table.insert(messages, "POSES:\n")
		for i = 1, #poses do
			local actionName = poses[i]
			local actionPath = posesDirectory..actionName..".moho"
			if FO_Utilities:FileExists(moho, actionPath) then
				table.insert(messages, " - "..actionName.." (Overwrite?)")
			else
				table.insert(messages, " - "..actionName.." (NEW!)")
			end
		end
	end
	if #animations > 0 then
		table.insert(messages, "\nANIMATIONS:\n")
		for i = 1, #animations do
			local actionName = animations[i]
			local actionPath = animationsDirectory..actionName..".moho"
			if FO_Utilities:FileExists(moho, actionPath) then
				table.insert(messages, " - "..actionName.." (Overwrite?)")
			else
				table.insert(messages, " - "..actionName.." (NEW!)")
			end
		end
	end
	table.insert(messages, "\n(WARNING: WILL PROBABLY SUCCEED, BUT ALSO CRASH!)")
	-- * Convert messages:
	local bigMessage = ""
	for i = 2, #messages do
		bigMessage = bigMessage..messages[i].."\n"
	end
	local overwrite = false
	-- * Multiple choice dialog:
	local answer = LM.GUI.Alert(
		LM.GUI.ALERT_QUESTION,
		messages[1],
		bigMessage,
		nil,
		"Create New Files Only",
		"Create and Overwrite All",
		"Cancel"
	) -- * Returns 0, 1 or 2
	if answer == 0 then -- * Create New Files Only
		overwrite = false
	elseif answer == 1 then -- * Create and Overwrite All
		overwrite = true
	elseif answer == 2 then -- * Cancel
		return
	end
	-- * Delete all other layers from project:
	self:MakeLayerSolo(moho)
	FO_Utilities:CreateDirectory(moho, rigActionsDirectory)
	-- *************************
	-- *** Create Moho files ***
	-- *************************
	-- * Clear timeline, insert 'Pose' action, save .moho file:
	local posesSaved = {}
	local animationsSaved = {}
	for i = 1, #poses do
		local actionName = poses[i]
		local fileName = actionName..".moho"
		local actionPath = posesDirectory..fileName
		if not FO_Utilities:FileExists(moho, actionPath) or overwrite then
			moho.document:ClearAnimation(0, false)
			moho.layer:InsertAction(actionName, 1, false)
			moho:FileSaveAs(actionPath)
			table.insert(posesSaved, actionName)
		end
	end
	-- * Clear timeline, insert 'Animation' action, save .moho file:
	for i = 1, #animations do
		local actionName = animations[i]
		local fileName = actionName..".moho"
		local actionPath = animationsDirectory..fileName
		if not FO_Utilities:FileExists(moho, actionPath) or overwrite then
			moho.document:ClearAnimation(0, false)
			moho.layer:InsertAction(actionName, 1, false)
			moho:FileSaveAs(actionPath)
			table.insert(animationsSaved, actionName)
		end
	end
	-- * Give feedback summary to user:
	if (#posesSaved+#animationsSaved > 0) then
		messages = { "i", "Created "..(#posesSaved+#animationsSaved).." Moho-files in: '"..rigActionsDirectory.."'"}
		if #posesSaved > 0 then
			table.insert(messages, "Saved "..#posesSaved.." Poses.")
			for i = 1, #posesSaved do
				table.insert(messages, i.." - "..posesSaved[i])
			end
		end
		if #animationsSaved > 0 then
			table.insert(messages, "Saved "..#animationsSaved.." Animations.")
			for i = 1, #animationsSaved do
				table.insert(messages, i.." - "..animationsSaved[i])
			end
		end
	else
		messages = { "No Moho-files were created!" }
	end
	-- **************
	-- *** REVERT ***
	-- **************
	-- * Undo
	for i = 0, self.undoCount do
		moho.document:Undo()
	end
	-- * Save file as file on which we started:
	moho:FileSaveAs(path)
	-- * Give feedback to user:
	FO_Utilities:Alert(messages)
end

function LK_ExportActions:MakeLayerSolo(moho)
	local layer0 = moho.document:Layer(0)
	moho.document:PrepUndo(nil)
	self.undoCount = self.undoCount + 1
	moho:PlaceLayerBehindAnother(moho.layer, layer0)
	for i = moho.document:CountLayers()-1, 1, -1 do
		local otherLayer = moho.document:Layer(i)
		moho.document:PrepUndo(otherLayer)
		self.undoCount = self.undoCount + 1
		moho:DeleteLayer(otherLayer)
	end
end
So far it has worked for all files I've tested. Now I just need to clean up some UX stuff and start testing them in production.

I'm curious what exactly are the best practices with these undo functions in scripts: https://mohoscripting.com/search/?subject=undo
I fear I've been using it wrong a lot, which might have been causing crashes in some tools. What's the trick to getting these right?
User avatar
synthsin75
Posts: 9973
Joined: Mon Jan 14, 2008 11:20 pm
Location: Oklahoma
Contact:

Re: HeyVern's scripts and tools - (focus on bones)

Post by synthsin75 »

I believe the idea is that Undo undoes everything since the last PrepUndo, but Moho seems to add its own PrepUndo points after certain changes. I don't think I've ever used IsUndoable.
I've been know to use Undo for very unorthodox things, like this:

Code: Select all

	if (moho:IsPlaying()) then
	--weird hack needed to update while playing timeline
		moho.document:PrepUndo(moho.layer, true)
		moho.document:Undo()
	end
Used at the end of OnMouseUp. No idea how I came up with it.
Post Reply