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

ScriptName = "MR_MoveTargetedJoint"

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

MR_MoveTargetedJoint = {}

function MR_MoveTargetedJoint:Name()
	return self:Localize('UILabel')
end

function MR_MoveTargetedJoint:Version()
	return '1.2'
end

function MR_MoveTargetedJoint:UILabel()
	return self:Localize('UILabel')
end

function MR_MoveTargetedJoint:Creator()
	return 'Eugene Babich'
end

function MR_MoveTargetedJoint:Description()
	return self:Localize('Description')
end

-- **************************************************
-- Is Relevant / Is Enabled
-- **************************************************

function MR_MoveTargetedJoint:IsRelevant(moho)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return false
	end
	return true
end

function MR_MoveTargetedJoint:IsEnabled(moho)
	if (moho:CountBones() < 3) then
		return false
	end
	if moho.frame == 0 then 
		return false
	end
	return true
end

-- **************************************************
-- Recurring Values
-- **************************************************

MR_MoveTargetedJoint.hideBonesWhileMoving = false
MR_MoveTargetedJoint.leaveBonesSelected = true
MR_MoveTargetedJoint.bakeAdjacentFrames = false
MR_MoveTargetedJoint.maintainProportions = false
MR_MoveTargetedJoint.firstBoneVisibility = true
MR_MoveTargetedJoint.secondBoneVisibility = true
MR_MoveTargetedJoint.firstBone = -1
MR_MoveTargetedJoint.firstBonePos = LM.Vector2:new_local()
MR_MoveTargetedJoint.firstBoneScalePercent = 50
MR_MoveTargetedJoint.clickOffset = LM.Vector2:new_local()
MR_MoveTargetedJoint.secondBone = -1
MR_MoveTargetedJoint.targetBonePos = LM.Vector2:new_local()
MR_MoveTargetedJoint.targetBone = 0
MR_MoveTargetedJoint.boneAngle = 0
MR_MoveTargetedJoint.isFlipped = false
MR_MoveTargetedJoint.isParentfliped = false
MR_MoveTargetedJoint.isMore = false
MR_MoveTargetedJoint.isActive = true
MR_MoveTargetedJoint.childsList = {}
MR_MoveTargetedJoint.firstBoneOffset = 0
MR_MoveTargetedJoint.secondBoneOffset = 0

-- **************************************************
-- Prefs
-- **************************************************

function MR_MoveTargetedJoint:LoadPrefs(prefs)
	self.hideBonesWhileMoving = prefs:GetBool("MR_MoveTargetedJoint.hideBonesWhileMoving", false)
	self.leaveBonesSelected = prefs:GetBool("MR_MoveTargetedJoint.leaveBonesSelected", false)
	self.bakeAdjacentFrames = prefs:GetBool("MR_MoveTargetedJoint.bakeAdjacentFrames", false)
	self.maintainProportions = prefs:GetBool("MR_MoveTargetedJoint.maintainProportions", false)
end

function MR_MoveTargetedJoint:SavePrefs(prefs)
	prefs:SetBool("MR_MoveTargetedJoint.hideBonesWhileMoving", self.hideBonesWhileMoving)
	prefs:SetBool("MR_MoveTargetedJoint.leaveBonesSelected", self.leaveBonesSelected)
	prefs:SetBool("MR_MoveTargetedJoint.bakeAdjacentFrames", self.bakeAdjacentFrames)
	prefs:SetBool("MR_MoveTargetedJoint.maintainProportions", self.maintainProportions)
end

function MR_MoveTargetedJoint:ResetPrefs()
	self.hideBonesWhileMoving = false
	self.leaveBonesSelected = true
	self.bakeAdjacentFrames = false
	self.maintainProportions = false
end

-- **************************************************
-- Keyboard/Mouse Control
-- **************************************************

function MR_MoveTargetedJoint:OnMouseDown(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	
	if (moho:CountSelectedBones() > 0) then
		for i = 0, skel:CountBones() - 1 do
			skel:Bone(i).fSelected = false
		end
	end

	local id = mouseEvent.view:PickBone(mouseEvent.pt, mouseEvent.vec, moho.layer, false)
	local boneCandidate = skel:Bone(id)
	if not self:IsBoneIK(moho, boneCandidate) then
		self.isActive = false
		return
	end

	if boneCandidate.fFlipH:GetValue(moho.layerFrame) or boneCandidate.fFlipV:GetValue(moho.layerFrame) then
		self.isActive = false
		return
	end
	
	local boneCandidateTargetID = skel:TargetOfBone(id, moho.layerFrame) 
	if boneCandidateTargetID == -1 then
		local boneCandidateParentID = boneCandidate.fParent
		local isParentOk = true
		if boneCandidateParentID > -1 then
			local parentChilds = self:CountBoneChildren(moho, skel, boneCandidateParentID, true)
			if parentChilds == 1 then
				local bCandParent = skel:Bone(boneCandidateParentID)
				if self:IsBoneIK(moho, bCandParent) then
					self.isActive = false
					return
				end
			end	
		end
		
		local childs = self:CountBoneChildren(moho, skel, id, true)
		
		if childs ~= 1 then
			self.isActive = false
			return
		end
		
		local found = false
		for q in pairs(self.childsList) do
			local bone = skel:Bone(self.childsList[q])
			if self:IsBoneIK(moho, bone) then
				if skel:TargetOfBone(self.childsList[q], moho.layerFrame)  > -1 then
					local tBone = skel:Bone(self.childsList[q])
					if self:Round(tBone.fPos.y) == 0 then
						if not tBone.fFlipH:GetValue(moho.layerFrame) and not tBone.fFlipV:GetValue(moho.layerFrame) then
							skel:Bone(id).fSelected = true	
							tBone.fSelected = true	
							self.firstBone = id
							self.secondBone = self.childsList[q]
							found = true
						end	
					end
				end
			end
		end
		if found then
			self.isActive = true
		else
			self.isActive = false
			return
		end	
	else
		if self:Round(boneCandidate.fPos.y) ~= 0 then
			self.isActive = false
			return
		end
		local boneCandidateFirstID = boneCandidate.fParent
		if boneCandidateFirstID > -1 then
			local bCandFirst = skel:Bone(boneCandidateFirstID)
			if not self:IsBoneIK(moho, bCandFirst) then
				self.isActive = false
				return
			end
			local bCandFirstParentID = bCandFirst.fParent
			if bCandFirstParentID > -1 then
				local bCandFirstParent = skel:Bone(bCandFirstParentID)
				local parentFirstChilds = self:CountBoneChildren(moho, skel, bCandFirstParentID, true)
				if parentFirstChilds == 1 then
					if self:IsBoneIK(moho, bCandFirstParent) then
						self.isActive = false
						return
					end
				end
			end	

			local childs = self:CountBoneChildren(moho, skel, boneCandidateFirstID, true)
			
			if childs ~= 1 then
				self.isActive = false
				return
			end
			
			if bCandFirst.fFlipH:GetValue(moho.layerFrame) or bCandFirst.fFlipV:GetValue(moho.layerFrame) then
				self.isActive = false
				return
			end	
			
			if skel:TargetOfBone(boneCandidateFirstID, moho.layerFrame) > -1 then
				self.isActive = false
				return
			end
			
			bCandFirst.fSelected = true	
			skel:Bone(id).fSelected = true	
			self.firstBone = boneCandidateFirstID
			self.secondBone = id
			self.isActive = true
		else
			self.isActive = false
			return
		end	
	end
	
	if self.isActive == false then 
		return
	end
	
	moho.document:PrepUndo(moho.layer)
	moho.document:SetDirty()
	
	self.isFlipped = false
	
	local bone = skel:Bone(self.firstBone)
	local secondBone = skel:Bone(self.secondBone)
	
	if self.maintainProportions then
		local boneFirstLenght = secondBone.fPos.x
		local boneSecondLenght = secondBone.fLength
		local boneFirstScale = bone.fScale
		local boneSecondScale = secondBone.fScale
		self.firstBoneScalePercent = ((boneFirstLenght * boneFirstScale) / ((boneFirstLenght * boneFirstScale) + (boneSecondLenght * boneSecondScale))) * 100
	end
	
	local secondBonePos = LM.Vector2:new_local()
	secondBonePos:Set(secondBone.fPos)
	local firstBoneMatrix = LM.Matrix:new_local()
	firstBoneMatrix:Set(bone.fMovedMatrix)
	firstBoneMatrix:Transform(secondBonePos)
	
	local mousePos = mouseEvent.vec
	self.clickOffset = secondBonePos - mousePos
	
	if self.bakeAdjacentFrames then
		if moho.layerFrame - 1 > 0 then
			local boneFAngle = bone.fAnimAngle:GetValue(moho.layerFrame - 1)
			local boneFScale = bone.fAnimScale:GetValue(moho.layerFrame - 1)
			local boneSAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame - 1)
			local boneSScale = secondBone.fAnimScale:GetValue(moho.layerFrame - 1)
			bone.fAnimAngle:SetValue(moho.layerFrame - 1, boneFAngle)
			secondBone.fAnimAngle:SetValue(moho.layerFrame - 1, boneSAngle)
			bone.fAnimScale:SetValue(moho.layerFrame - 1, boneFScale)
			secondBone.fAnimScale:SetValue(moho.layerFrame - 1, boneSScale)
		end
		
		if moho.layerFrame + 1 > 0 then
			local boneFAngle = bone.fAnimAngle:GetValue(moho.layerFrame + 1)
			local boneFScale = bone.fAnimScale:GetValue(moho.layerFrame + 1)
			local boneSAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame + 1)
			local boneSScale = secondBone.fAnimScale:GetValue(moho.layerFrame + 1)
			bone.fAnimAngle:SetValue(moho.layerFrame + 1, boneFAngle)
			secondBone.fAnimAngle:SetValue(moho.layerFrame + 1, boneSAngle)
			bone.fAnimScale:SetValue(moho.layerFrame + 1, boneFScale)
			secondBone.fAnimScale:SetValue(moho.layerFrame + 1, boneSScale)
		end
	end
	
	if self.hideBonesWhileMoving then
		self.firstBoneVisibility = bone.fHidden
		self.secondBoneVisibility = secondBone.fHidden
		bone.fHidden = true
		secondBone.fHidden = true
	end
	
	local firstBoneStretching = bone.fMaxAutoScaling
	local secondBoneStretching = secondBone.fMaxAutoScaling
	bone.fMaxAutoScaling = 1
	secondBone.fMaxAutoScaling = 1
	
	moho.layer:UpdateCurFrame()
	
	self.firstBoneOffset = bone.fScale - bone.fAnimScale:GetValue(moho.layerFrame)
	self.secondBoneOffset = secondBone.fScale - secondBone.fAnimScale:GetValue(moho.layerFrame)
	bone.fMaxAutoScaling = firstBoneStretching
	secondBone.fMaxAutoScaling = secondBoneStretching
	
	local bonePose = LM.Vector2:new_local()
	bonePose:Set(bone.fPos)
	
	local boneTarget = skel:TargetOfBone(self.secondBone, moho.layerFrame) 
	self.targetBone = boneTarget
	local targetPos = LM.Vector2:new_local() 
	local target = skel:Bone(boneTarget)
	local targetParent = target.fParent
	targetPos:Set(target.fPos)
	
	if targetParent > -1 then
		local targetParentBone = skel:Bone(targetParent)
		local targetParentMatrix = LM.Matrix:new_local()
		targetParentMatrix:Set(targetParentBone.fMovedMatrix)
		targetParentMatrix:Transform(targetPos)
	end

	self.targetBonePos = targetPos
	local parentBoneID = bone.fParent
	if parentBoneID > -1 then
		local parentBone = skel:Bone(parentBoneID)
		local parentMatrix = LM.Matrix:new_local()
		parentMatrix:Set(parentBone.fMovedMatrix)
		parentMatrix:Transform(bonePose)
	end
	self.bonePose = bonePose
		
	local vectorBonePos = targetPos - bonePose
	local boneAngle = math.atan2(vectorBonePos.y, vectorBonePos.x)
	if boneAngle < 0 then
		boneAngle = (math.pi * 2) + boneAngle
	end
		
	self.boneAngle = boneAngle
	
	local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
	if secondBoneAngle > math.pi * 2 then
		secondBoneAngle = secondBoneAngle - (math.pi * 2)
	elseif secondBoneAngle < 0 then
		secondBoneAngle = secondBoneAngle + (math.pi * 2)
	end
	
	isParentfliped = false
	if parentBoneID > -1 then
		local nextBone = skel:Bone(parentBoneID)
		repeat 
			local prevBone = nextBone
			if nextBone.fFlipH:GetValue(moho.layerFrame) then
				isParentfliped =  not isParentfliped
			end
			if nextBone.fFlipV:GetValue(moho.layerFrame) then
				isParentfliped = not isParentfliped
			end
			
			if nextBone.fParent > -1 then
				nextBone = skel:Bone(nextBone.fParent)
			end
		until nextBone == prevBone
	end
	
	if secondBoneAngle < math.pi then
		self.isMore = true
	else
		self.isMore = false
	end
	
	if isParentfliped then
		self.isMore = not self.isMore
	end
	
	self:OnMouseMoved(moho, mouseEvent)
end

function MR_MoveTargetedJoint:IsBoneIK(moho, bone)
	if (bone.fAngleControlParent >= 0 or bone.fPosControlParent >= 0 or bone.fScaleControlParent >= 0 or bone.fBoneDynamics.value or bone.fIgnoredByIK) then
		return false
	else
		return true
	end	
end

function MR_MoveTargetedJoint:OnMouseMoved(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	if self.isActive == false then
		return
	end
		
	local bone = skel:Bone(self.firstBone)
	local secondBone = skel:Bone(self.secondBone)
	local bonePose = LM.Vector2:new_local()
	bonePose = self.bonePose
	local targetPos = LM.Vector2:new_local()
	local boneLenght = secondBone.fPos.x
	local dist = self:GetDistance(bonePose, mouseEvent.vec + self.clickOffset)
	local boneNewScale = dist / boneLenght
	bone.fAnimScale:SetValue(moho.layerFrame, boneNewScale - self.firstBoneOffset)
	local secondDist = self:GetDistance(self.targetBonePos, mouseEvent.vec + self.clickOffset)
	local secondBoneLenght = secondBone.fLength
	local secondBoneNewScale = secondDist / secondBoneLenght
	secondBone.fAnimScale:SetValue(moho.layerFrame, secondBoneNewScale - self.secondBoneOffset)
	
	if self.maintainProportions then
		local totalDist = dist + secondDist
		local distP = (totalDist / 100) * self.firstBoneScalePercent
		distP = distP / boneLenght
		local secondDistP = (totalDist / 100) * (100 - self.firstBoneScalePercent)
		secondDistP = secondDistP / secondBoneLenght
		bone.fAnimScale:SetValue(moho.layerFrame, distP - self.firstBoneOffset)
		secondBone.fAnimScale:SetValue(moho.layerFrame, secondDistP - self.secondBoneOffset)
	end
	
	local vectorMousePos = (mouseEvent.vec + self.clickOffset) - bonePose
	local mouseAngle = math.atan2(vectorMousePos.y, vectorMousePos.x)
	
	if mouseAngle < 0 then
		mouseAngle = (math.pi * 2) + mouseAngle
	end
	
	mouseAngle = mouseAngle - self.boneAngle
	if mouseAngle < 0 then
		mouseAngle = (math.pi * 2) + mouseAngle
	end
	
	local turnToRight = 0.17
	local turnToLeft = 3.31
	if isParentfliped then
		turnToRight = 3.31
		turnToLeft = 0.17
	end
	
	if self.isMore then
		if mouseAngle < math.pi and not self.isFlipped then
			local boneAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
			local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
			bone.fAnimAngle:SetValue(moho.layerFrame, turnToLeft)
			secondBone.fAnimAngle:SetValue(moho.layerFrame, turnToLeft)
			self.isFlipped = true
		end
		if mouseAngle > math.pi and self.isFlipped then
			local boneAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
			local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
			bone.fAnimAngle:SetValue(moho.layerFrame, turnToRight)
			secondBone.fAnimAngle:SetValue(moho.layerFrame, turnToRight)
			self.isFlipped = false
		end
	else
		if mouseAngle < math.pi and self.isFlipped then
			local boneAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
			local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
			bone.fAnimAngle:SetValue(moho.layerFrame, turnToLeft)
			secondBone.fAnimAngle:SetValue(moho.layerFrame, turnToLeft)
			self.isFlipped = false
		end
		if mouseAngle > math.pi and not self.isFlipped then
			local boneAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
			local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
			bone.fAnimAngle:SetValue(moho.layerFrame, turnToRight)
			secondBone.fAnimAngle:SetValue(moho.layerFrame, turnToRight)
			self.isFlipped = true
		end
	end
	moho.layer:UpdateCurFrame()
	mouseEvent.view:DrawMe()
end

function MR_MoveTargetedJoint:Round(x, n)
	n = math.pow(10, n or 3)
	x = x * n
	if x >= 0 then x = math.floor(x + 0.5) else x = math.ceil(x - 0.5) end
	return x / n
end

function MR_MoveTargetedJoint:GetDistance(Pos1, Pos2)
	return math.sqrt((Pos2.x-Pos1.x)^2+(Pos2.y-Pos1.y)^2)
end

function MR_MoveTargetedJoint:CountBoneChildren(moho, skel, boneID, ignoreControlledBones)
	for q in pairs(self.childsList) do
		self.childsList[q] = nil
	end

	local n = 0
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if (bone.fParent == boneID) then
			n = n + 1
			table.insert(self.childsList, i)
			if (ignoreControlledBones) then
				if (bone.fAngleControlParent >= 0 or bone.fPosControlParent >= 0 or bone.fScaleControlParent >= 0 or bone.fBoneDynamics.value or bone.fIgnoredByIK) then
					n = n - 1
				end
			end
		end
	end
	return n
end

function MR_MoveTargetedJoint:OnMouseUp(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	
	if self.isActive == false then
		return
	end
	
	local bone = skel:Bone(self.firstBone)
	local secondBone = skel:Bone(self.secondBone)
	
	if self.hideBonesWhileMoving then
		bone.fHidden = self.firstBoneVisibility
		secondBone.fHidden = self.secondBoneVisibility
	end
	
	bone.fAnimAngle:SetValue(moho.layerFrame, bone.fAngle)
	secondBone.fAnimAngle:SetValue(moho.layerFrame, secondBone.fAngle)
	
	moho:NewKeyframe(CHANNEL_BONE_S)
	moho:NewKeyframe(CHANNEL_BONE)
	
	if not self.leaveBonesSelected then
		bone.fSelected = false
		secondBone.fSelected = false
	end
	
	moho:UpdateUI()
	mouseEvent.view:DrawMe()
	moho:UpdateSelectedChannels()
end

-- **************************************************
-- Tool Panel Layout
-- **************************************************

MR_MoveTargetedJoint.HIDE_BONES_WHILE_MOVING = MOHO.MSG_BASE
MR_MoveTargetedJoint.LEAVE_BONES_SELECTED = MOHO.MSG_BASE +1
MR_MoveTargetedJoint.BAKE_ADJACENT_FRAMES = MOHO.MSG_BASE +2
MR_MoveTargetedJoint.MAINTAIN_PROPORTIONS = MOHO.MSG_BASE +3

function MR_MoveTargetedJoint:DoLayout(moho, layout)
	self.hideBonesWhileMovingCheckbox = LM.GUI.CheckBox(self:Localize('Hide bones while moving'), self.HIDE_BONES_WHILE_MOVING)
    layout:AddChild(self.hideBonesWhileMovingCheckbox, LM.GUI.ALIGN_LEFT, 0)
	
	self.leaveBonesSelectedCheckbox = LM.GUI.CheckBox(self:Localize('Leave Bones Selected'), self.LEAVE_BONES_SELECTED)
    layout:AddChild(self.leaveBonesSelectedCheckbox, LM.GUI.ALIGN_LEFT, 0)
	
	self.bakeAdjacentFramesCheckbox = LM.GUI.CheckBox(self:Localize('Bake Adjacent Frames'), self.BAKE_ADJACENT_FRAMES)
    layout:AddChild(self.bakeAdjacentFramesCheckbox, LM.GUI.ALIGN_LEFT, 0)
	
	self.maintainProportionsCheckbox = LM.GUI.CheckBox(self:Localize('Maintain Proportions'), self.MAINTAIN_PROPORTIONS)
    layout:AddChild(self.maintainProportionsCheckbox, LM.GUI.ALIGN_LEFT, 0)
end

function MR_MoveTargetedJoint:UpdateWidgets(moho)
	MR_MoveTargetedJoint.hideBonesWhileMovingCheckbox:SetValue(self.hideBonesWhileMoving)
	MR_MoveTargetedJoint.leaveBonesSelectedCheckbox:SetValue(self.leaveBonesSelected)
	MR_MoveTargetedJoint.bakeAdjacentFramesCheckbox:SetValue(self.bakeAdjacentFrames)
	MR_MoveTargetedJoint.maintainProportionsCheckbox:SetValue(self.maintainProportions)
end

function MR_MoveTargetedJoint:HandleMessage(moho, view, msg)
	if msg == self.HIDE_BONES_WHILE_MOVING then
        self.hideBonesWhileMoving = self.hideBonesWhileMovingCheckbox:Value()
	elseif msg == self.LEAVE_BONES_SELECTED then
		self.leaveBonesSelected = self.leaveBonesSelectedCheckbox:Value()
	elseif msg == self.BAKE_ADJACENT_FRAMES then
		self.bakeAdjacentFrames = self.bakeAdjacentFramesCheckbox:Value()	
    elseif msg == self.MAINTAIN_PROPORTIONS then
		self.maintainProportions = self.maintainProportionsCheckbox:Value()	
    end
end

-- **************************************************
-- Localization
-- **************************************************

function MR_MoveTargetedJoint:Localize(text)
	local phrase = {}

	phrase['Description'] = 'Precisely control knees and elbows when using target bones.'
	phrase['UILabel'] = 'Move Targeted Joint'
	
	phrase['Hide bones while moving'] = 'Hide bones while moving'
	phrase['Leave Bones Selected'] = 'Leave bones selected'
	phrase['Bake Adjacent Frames'] = 'Bake adjacent frames'
	phrase['Maintain Proportions'] = 'Maintain proportions'

	local fileWord = MOHO.Localize("/Menus/File/File=File")
	if fileWord == "Файл" then
		phrase['Description'] = 'Тонко контролируйте колени и локти персонажей подконтрольные целевым костям.'
		phrase['UILabel'] = 'Контроль целевого сустава'
		
		phrase['Hide bones while moving'] = 'Прятать кости при движении'
		phrase['Leave Bones Selected'] = 'Оставить кости выделенными'
		phrase['Bake Adjacent Frames'] = 'Запечь соседние кадры'
		phrase['Maintain Proportions'] = 'Соблюдать пропорции'
	end

	return phrase[text]
end

Icon
Move Targeted Joint
Listed

Script type: Tool

Uploaded: Jan 08 2021, 09:07

Last modified: May 12 2021, 09:00

This tool allows you to easily fine-tune the elbows and knees of your character when target bones are in use.



To work correctly it requires a hierarchical pair of bones controlled by a target. The bones must not be flipped. The lower bone (the shin or the forearm) must not have any Y-axis translation.




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