-- WARNING: This script requires AE_Utilities.lua of version 1.12 or later!
-- **************************************************
-- Provide Moho with the name of this script object
-- **************************************************

ScriptName = "AE_MixSmartbones"

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

AE_MixSmartbones = {}

function AE_MixSmartbones:Name()
	return "Smartbone Correction"
end

function AE_MixSmartbones:Version()
	return "1.3.3"
end

function AE_MixSmartbones:UILabel()
	return "Mix smartbones"
end

function AE_MixSmartbones:Creator()
	return "Alexandra Evseeva"
end

function AE_MixSmartbones:Description()
	return "Edit smartbone driven tracks on main timeline. Select smartbone at frame with keys on driven tracks and with no keys on smartbone's tracks.With one selected smartbone it's behavior will be corrected. With two selected smartbones a correcting bone will be created.On success all keys at this frame will be deleted."
end




-- **************************************************
-- Recurring values
-- **************************************************

-- AE_MixSmartbones.value1 = false

-- **************************************************
-- Is Enabled
-- **************************************************

function AE_MixSmartbones:IsEnabled(moho)
	return moho:Skeleton()
end


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

function AE_MixSmartbones:Run(moho)

	moho.document:PrepUndo("mix smartbones", moho.layer)
	moho.document:SetDirty()
	
	--print("start")
	
	local skel = moho:Skeleton()
	if skel == nil then return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Selected non-bones layer", "Select a bone layer", "", "EXIT") end
	local selBones = {}
	local selChans = {}
	for b=0, skel:CountBones()-1 do
		local bone = skel:Bone(b)
		local name = bone:Name()
		if bone.fSelected then
			if (moho.layer:HasAction(name) or moho.layer:HasAction(name.." 2")) then
				table.insert(selBones, bone)
				table.insert(selChans, bone.fAnimAngle)
				for a = 0, bone.fAnimAngle:CountActions()-1 do 
					local boneName = bone.fAnimAngle:ActionName(a)
					if string.sub(boneName, -2) == " 2" then boneName = string.sub(1, -3) end
					local driveBone = skel:BoneByName(boneName)
					if driveBone then table.insert(selChans, driveBone.fAnimAngle) end
				end
			else 
				return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Selected bone", bone:Name(), "has no smart action", "EXIT")
			end
		end
	end
	
	self.keysCollection = self:CollectKeyValues(moho, selChans)
	if #self.keysCollection < 1 then return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "No keys at active frame", "Animate something in this frame", "", "EXIT") end
	
	if #selBones == 1 then self:CorrectSmartbone(moho, selBones[1])
	elseif #selBones == 2 then self:CreateCorrectingBone(moho, {selBones[1], selBones[2]})
	else return LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Wrong number of smartbones selected", "Select one or two smartbones for tuning", "", "EXIT")
	end
	
	--print("all done, total number of bones: ", moho:Skeleton():CountBones())
	
	moho.document:PrepUndo("update views", moho.layer)
	moho.document:SetDirty()
	
	for i, layer in AE_Utilities:IterateAllLayers(moho) do
		if not layer:IsReferencedLayer() then 
			layer:UpdateCurFrame(true)
		end
	end
	
	--print("all done, layers updated, total number of bones: ", moho:Skeleton():CountBones())
	
	moho.document:Undo()
	moho:UpdateUI()
	
	--print("all done, update undone, total number of bones: ", moho:Skeleton():CountBones())
	
end

function AE_MixSmartbones:CreateCorrectingBone(moho, smartbones)
	--print("create function entered")
	local skel = moho:Skeleton()
	if skel == nil then return end
	
	local sourceFrame = moho.layerFrame
	local angle1 = smartbones[1].fAngle
	local angle2 = smartbones[2].fAngle
	local corrName = smartbones[1]:Name()..self:RoundAngle(angle1).."|"..smartbones[2]:Name()..self:RoundAngle(angle2)
	local corrBone = skel:BoneByName(corrName)
	if corrBone then return self:CorrectSmartbone(moho, corrBone) end
	corrBone = skel:AddBone(0)
	corrBone:SetName(corrName)
  	corrBone.fParent = -1 
  	corrBone.fShy = true
    corrBone.fStrength = 0
  	corrBone.fAnimAngle:SetValue(0,0)
	local startPos = LM.Vector2:new_local()
	startPos:Set(-2,0)
	corrBone.fAnimPos:SetValue(0, startPos)	
	
	--- set angles for corrbone's own action--------
	moho.layer:ActivateAction(corrName)
  	local frame1 = 150
  	local frame2 = 300
  	corrBone.fAnimAngle:SetValue(frame1, math.pi/4)
  	corrBone.fAnimAngle:SetValue(frame2, math.pi/2)
	moho.layer:ActivateAction(nil)
	--print("corrBone created and animated. Total number of bones = ", moho:Skeleton():CountBones())
	--- set corrBone animation in corrected smartbones
	for i=1,2 do
		--print("analise parent no ", i, ": ", smartbones[i]:Name())
		local smartboneFrame, actName = self:FindFrameAndAction(moho, smartbones[i], smartbones[i].fAngle)
		if smartboneFrame > 0 then 
			moho.layer:ActivateAction(actName)
			corrBone.fAnimAngle:SetValue(smartboneFrame, math.pi/4)
			local secondZero = AE_Utilities:FindSmartBoneFrame(smartbones[i].fAnimAngle:GetValue(0), smartbones[i].fAnimAngle, true)
			if secondZero > 0 then
				corrBone.fAnimAngle:SetValue(secondZero, 0)
			end
			moho.layer:ActivateAction(nil)
		else
			local alertText = "No angle '" .. 180*smartbones[i].fAngle/math.pi .. "' in '" .. smartbones[i]:Name() .."' actions"
			LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Can not find angle", alertText, "", "EXIT")
			skel:DeleteBone(skel:BoneID(corrBone))
			moho.layer:DeleteAction(corrName)
			return
		end
	end
	--print("corrBone animated at parents. Total number of bones = ", moho:Skeleton():CountBones())
	skel:UpdateBoneMatrix()
	local keysCollection = self:EvalSmartboneCorrection(moho, frame2, corrBone:Name())
	for k,v in pairs(keysCollection) do
		local actChannel = v.channel:ActionByName(corrBone:Name())
		local actDerivedChannel = AE_Utilities:GetDerivedChannel(moho, actChannel)
		actDerivedChannel:SetValue(frame1, actDerivedChannel:GetValue(0))
		for i=0, actChannel:CountKeys()-1 do
			actChannel:SetKeyInterpByID(i,MOHO.INTERP_LINEAR)
		end
	end
	--print("Keys deleted. Total number of bones = ", moho:Skeleton():CountBones())	
end

function AE_MixSmartbones:CorrectSmartbone(moho, smartbone)
	local corrFrame, actName = self:FindFrameAndAction(moho, smartbone, smartbone.fAngle)
	if corrFrame > 0 then 
		self:EvalSmartboneCorrection(moho, corrFrame, actName)
	else LM.GUI.Alert(LM.GUI.ALERT_WARNING, "Unused angle", "Selected smartbone has no action for this frame's angle ("..(180*smartbone.fAngle/math.pi)..")", "", "EXIT")
	end
end

function AE_MixSmartbones:CollectKeyValues(moho, excludeList)
	local function has_value(tab, val)
		for k,v in pairs(tab) do 
			if v == val then return true end
		end
		return false
	end
	local keysCollection = {}
	for i, layer in AE_Utilities:IterateAllLayers(moho) do
		-------------------------------------
		--if not layer:IsReferencedLayer() then
		-------------------------------------
			if layer == moho.layer or layer:IsAncestorSelected() then
				local layerFrame = moho.frame + layer:TotalTimingOffset()
				for subId, channel in AE_Utilities:IterateLayerSubChannels(moho, layer) do
					if (not has_value(excludeList, channel)) and channel:HasKey(layerFrame) then
						--print(layer:Name())
						local derivedChannel = AE_Utilities:GetDerivedChannel(moho,channel, true)
						if derivedChannel then
							local newValue = derivedChannel:GetValue(layerFrame)
							local v = {["channel"] = derivedChannel, ["keyVal"] = newValue, ["frame"] = layerFrame, ["layer"] = layer}	
							table.insert(keysCollection, v)
						end
					end
				end
			end
		-------------------------------------
		--end
		-------------------------------------
	end
	return keysCollection
end

function AE_MixSmartbones:EvalSmartboneCorrection(moho, corrFrame, actName)

	local keysCollection = self.keysCollection
	
	moho.layer:ActivateAction(actName)
	for k,v in pairs(keysCollection) do
		--actionChannel = v.channel:ActionByName(actName)
		--local derivedChannel = AE_Utilities:GetDerivedChannel(moho,actionChannel, true)
		local oldValue = v.channel:GetValue(corrFrame) - v.channel:GetValue(0)
		v.channel:SetValue(corrFrame, v.keyVal + oldValue)
		v.channel:SetKeyInterp(corrFrame,MOHO.INTERP_LINEAR)
	end
	moho.layer:ActivateAction(nil)
	--[[--]]
	for k,v in pairs(keysCollection) do
		if not v.layer:IsReferencedLayer() then
			v.channel:DeleteKey(v.frame)
			--v.layer:DeleteKeysAtFrame(true, v.frame)
		end
	end
	
	return keysCollection
	
end

function AE_MixSmartbones:FindFrameAndAction(moho, smartbone, angle)
	--print("finding ", angle*180/math.pi, " at ", smartbone:Name())
	local actName = smartbone:Name()
	local smartboneTrack = moho:ChannelAsAnimVal(smartbone.fAnimAngle:ActionByName(actName))
	local smartboneFrame, subFrame = AE_Utilities:FindSmartBoneFrame(angle, smartboneTrack)
	--print("found at ", smartboneFrame, " ", subFrame)
	if smartboneFrame < 0 then
		actName = actName.." 2"
		--print("finding ", angle*180/math.pi, " at ", actName)
		if(smartbone.fAnimAngle:ActionByName(actName))then
			smartboneTrack = moho:ChannelAsAnimVal(smartbone.fAnimAngle:ActionByName(actName))
			smartboneFrame, subFrame = AE_Utilities:FindSmartBoneFrame(angle, smartboneTrack)
		end
		--print("found at ", smartboneFrame, " ", subFrame)
	end
	if (subFrame-smartboneFrame) > 0.5 then smartboneFrame = smartboneFrame + 1 end
	return smartboneFrame, actName, smartboneTrack
end

function AE_MixSmartbones:RoundAngle(angle)
	local degrees = 180*angle/math.pi
	if math.abs(degrees-math.ceil(degrees)) <  math.abs(degrees-math.floor(degrees))then
		return math.ceil(degrees)
	else 
		return math.floor(degrees)
	end
end



Icon
AE Mix Smartbones
Listed

Script type: Button/Menu

Uploaded: Jul 14 2020, 07:54

Last modified: Oct 13 2020, 01:26

Mix two smartbones or tune one smartbone without opening its action
When two smartbones in non-zero position at the same time make unwanted deformations, You can fix it, modifying related tracks and saving the modification to reproduce it each time these two smartbones get saved angles.
Also You can fix one selected smartbone's behavior at its current angle, without entering into its timeline.

Installation Options:

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