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

ScriptName = "AE_BoneMagnet"

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

AE_BoneMagnet = {}

function AE_BoneMagnet:Name()
	return 'Bone Magnet'
end

function AE_BoneMagnet:Version()
	return '1.1'
end

function AE_BoneMagnet:UILabel()
	return 'Bone Magnet'
end

function AE_BoneMagnet:Creator()
	return 'Alexandra Evseeva'
end

function AE_BoneMagnet:Description()
	return ''
end

function AE_BoneMagnet:ColorizeIcon()
	return true
end

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

function AE_BoneMagnet:IsRelevant(moho)
	if moho:CountBones() > 0 then
		return true
	end
	return false
end

function AE_BoneMagnet:IsEnabled(moho)
	if moho.frame == 0 then return false end 
	local skel = moho:Skeleton()
	if (skel == nil) then
		return false
	end
	return true
end

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

AE_BoneMagnet.newMethod = false
AE_BoneMagnet.magnetRadius = 0.5
AE_BoneMagnet.selectedOnly = true
AE_BoneMagnet.dragging = false
AE_BoneMagnet.dragVec = LM.Vector2:new_local()
AE_BoneMagnet.dragVec:Set(10000.0, 10000.0)
AE_BoneMagnet.dragResize = false
AE_BoneMagnet.selectionMode = false
AE_BoneMagnet.startRadius = 0

AE_BoneMagnet.minVec = LM.Vector2:new_local()
AE_BoneMagnet.maxVec = LM.Vector2:new_local()

AE_BoneMagnet.fixChildren = true

function AE_BoneMagnet:LoadPrefs(prefs)
	self.magnetRadius = prefs:GetFloat("AE_BoneMagnet.magnetRadius", 0.5)
	self.selectedOnly = prefs:GetBool("AE_BoneMagnet.selectedOnly", false)
	self.fixChildren = prefs:GetBool("AE_BoneMagnet.fixChildren", true)
end

function AE_BoneMagnet:SavePrefs(prefs)
	prefs:SetFloat("AE_BoneMagnet.magnetRadius", self.magnetRadius)
	prefs:SetBool("AE_BoneMagnet.selectedOnly", self.selectedOnly)
	prefs:SetBool("AE_BoneMagnet.fixChildren", self.fixChildren)
end

function AE_BoneMagnet:ResetPrefs()
	self.magnetRadius = 0.5
	self.selectedOnly = false
	self.fixChildren = true
end

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

function AE_BoneMagnet:NonDragMouseMove()
	return true -- Call MouseMoved() even if the mouse button is not down
end

function AE_BoneMagnet:OnInputDeviceEvent(moho, deviceEvent)
	return LM_TransformBone:OnInputDeviceEvent(moho, deviceEvent)
end

function AE_BoneMagnet:OnMouseDown(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end	

	if (mouseEvent.altKey) then
		self.dragResize = true
		self.startRadius = self.magnetRadius
		self.dragVec:Set(mouseEvent.drawingVec)
		mouseEvent.view:DrawMe()
		return	
	elseif (mouseEvent.ctrlKey) then
		self.selectionMode = true
		mouseEvent.ctrlKey = false
		LM_SelectBone:OnMouseDown(moho, mouseEvent)
		return
	end	
	
	self.dragging = true
	self.dragVec:Set(mouseEvent.drawingVec)

	moho.document:PrepUndo(moho.drawingLayer)
	moho.document:SetDirty()
	
	------------------------------------
	--prepare an array to move---------------
	--look at LM_Magnet same method structure (lines 119-158)
	local frame = moho.layerFrame
	local actionName = moho.document:CurrentDocAction()
	

	self.boneList = {}
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		local drawBone = self:IsBonePickable(moho, skel, bone)
		if drawBone then
			local v1 = LM.Vector2:new_local()
			local v2 = LM.Vector2:new_local()
			v1:Set(0,0)
			v2:Set(bone.fLength, 0)
			local boneInfo = {id=i, points={{start=v1},{start=v2}}}
			for i, point in pairs(boneInfo.points) do
				if (moho.frame == 0) then
					bone.fRestMatrix:Transform(point.start)
				else
					bone.fMovedMatrix:Transform(point.start)
				end
				local v = point.start - mouseEvent.drawingVec
				local magInfluence = v:Mag()
				if self.newMethod then
					magInfluence = magInfluence * magInfluence
					magInfluence = 0.5 / (1 + magInfluence * magInfluence * 1000)
					if (magInfluence <= 0.001) then magInfluence = 0 end
				else
					if magInfluence <= self.magnetRadius then
						magInfluence = magInfluence / self.magnetRadius
						if (magInfluence > 1.0) then
							magInfluence = 1.0
						end
						magInfluence = LM.Slerp(magInfluence, 1, 0)
					else 
						magInfluence = 0
					end
				end
				point.influence = magInfluence
				point.current = LM.Vector2:new_local()
				point.current:Set(point.start)
			end
			if boneInfo.points[1].influence > 0 or boneInfo.points[2].influence > 0 then
				local pos, angle, scale = AE_Utilities:GetGlobalBonePRS(moho, skel, bone, frame, actionName)
				boneInfo.startAngle = angle
				boneInfo.startPos = pos
				boneInfo.startScale = scale
				boneInfo.startMatrix = LM.Matrix:new_local()
				boneInfo.startMatrix:Set(bone.fMovedMatrix)
				if frame == 0 then boneInfo.startMatrix:Set(bone.fRestMatrix) end
			
				table.insert(self.boneList, boneInfo)
			end
		end
	end	

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

	mouseEvent.view:DrawMe()		
	
end

function AE_BoneMagnet:OnMouseMoved(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end

	if (self.dragResize) then
		self.magnetRadius = self.startRadius + (mouseEvent.pt.x - mouseEvent.startPt.x) / 600
		if (self.magnetRadius < 0.01) then
			self.magnetRadius = 0.01
		end
		if (self.magnetRadius > 5.0) then
			self.magnetRadius = 5.0
		end
		mouseEvent.view:DrawMe()
		return
	elseif (self.selectionMode) then
		mouseEvent.ctrlKey = false
		LM_SelectBone:OnMouseMoved(moho, mouseEvent)
		return
	end

	self.dragVec:Set(mouseEvent.drawingVec)
	
	if (not self.dragging) then
		mouseEvent.view:DrawMe()
		return
	end

	local offset = mouseEvent.drawingVec - mouseEvent.drawingStartVec

	--TODO:----------------------------------
	--apply motion to saved array items (self.selList or something like this)---------------
	--use lines 194-196 of LM_Magnet (but not very useful)
	
	for i, boneInfo in pairs(self.boneList) do
		--local bone = skel:Bone(boneInfo.id)
		for j, point in pairs(boneInfo.points) do
			point.current = point.start + offset * point.influence
		end
	end
	
	-----------------------------------------	

	mouseEvent.view:DrawMe()
	
end

function AE_BoneMagnet:OnMouseUp(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	
	self.dragging = false
	self.dragVec:Set(mouseEvent.drawingVec)
	
	if (self.dragResize) then
		self.dragResize = false
		mouseEvent.view:DrawMe()
		return
	elseif (self.selectionMode) then
		mouseEvent.ctrlKey = false
		LM_SelectBone:OnMouseUp(moho, mouseEvent)
		self.selectionMode = false
		mouseEvent.view:DrawMe()
		return
	end

	------------------------------------
	--apply postions from array to bones
	--use lines 223-228 of LM_Magnet (but not very useful)
	self.boneCheckList = {}
	
	for i, boneInfo in pairs(self.boneList) do
		self.boneCheckList[boneInfo.id] = boneInfo
	end
	for i=0, skel:CountBones()-1 do
		local bone = skel:Bone(i)
		if bone.fParent == -1 then
			self:ApplyToBone(moho,skel,bone,i)
		end
	end
	
	-----------------------------------------	
	moho.layer:UpdateCurFrame()
	moho:UpdateSelectedChannels()
	moho:UpdateUI()
	self.boneList = nil
	self.boneCheckList = nil
	
end

function AE_BoneMagnet:CropAngle(angle)
	if math.abs(angle) < math.pi then return angle end
	local new_angle = (math.abs(angle) % (2*math.pi)) * angle/math.abs(angle)
	if math.abs(new_angle) > math.pi then
		local pi = 2 * math.pi * new_angle/math.abs(new_angle)
		new_angle = new_angle - pi
	end
	return new_angle
end

function AE_BoneMagnet:ApplyToBone(moho, skel, bone, id)
	local boneInfo = self.boneCheckList[id]
	local frame = moho.layerFrame
	local actionName = moho.document:CurrentDocAction()
	if boneInfo then
		--apply position, angle and scale in moho.frame		
		local pos, angle, scale = AE_Utilities:GetGlobalBonePRS(moho, skel, bone, frame, actionName, moho.layer)
		local globalPos = LM.Vector2:new_local()
		globalPos:Set(boneInfo.points[1].current)
		if bone.fParent > -1 then
			--move globalPos to parent space
			local parentBone = skel:Bone(bone.fParent)
			local matrix = AE_Utilities:GetGlobalBoneMatrix(moho, skel, parentBone, frame, actionName)
			matrix:Invert()
			matrix:Transform(globalPos)
			matrix:Transform(pos)	
		end
		local posDif = globalPos - pos
		if posDif:Mag() > 0.0001 then 
			bone.fAnimPos:SetValue(frame, bone.fAnimPos:GetValue(frame) + posDif)
		end
		if bone.fLength > 0 then
			local vector = boneInfo.points[2].current - boneInfo.points[1].current
			--get desired angle
			local desiredAngle = math.pi/2 - math.atan2(vector.x, vector.y)
			local angleDif = desiredAngle - angle
			angleDif = (math.abs(angleDif)%(2*math.pi))*angleDif/math.abs(angleDif)
			if math.abs(angleDif) > 0.0001 then
				angleDif = self:CropAngle(angleDif)
				bone.fAnimAngle:SetValue(frame, bone.fAnimAngle:GetValue(frame) + angleDif)
				boneInfo.globalAngleDif = desiredAngle - boneInfo.startAngle				
			end
			--get desired scale
			if frame == 0 then
				boneInfo.lengthAdd = vector:Mag() - bone.fLength
				bone.fLength = vector:Mag()
			else
				local desiredScale = vector:Mag()/bone.fLength
				local scaleDif = desiredScale - scale
				if math.abs(scaleDif) > 0.001 then 
					bone.fAnimScale:SetValue(frame, bone.fAnimScale:GetValue(frame) + scaleDif)
				end
			end
		elseif bone.fParent > -1 and self.boneCheckList[bone.fParent] then
			local parentInfo = self.boneCheckList[bone.fParent]
			local parentBone = skel:Bone(bone.fParent)
			local parentAngleDif = parentInfo.globalAngleDif
			if parentAngleDif and not bone.fFixedAngle then
				parentAngleDif = self:CropAngle(parentAngleDif)
				bone.fAnimAngle:SetValue(frame, bone.fAnimAngle:GetValue(frame) - parentAngleDif)
			end			
		end
	elseif bone.fParent > -1 and self.boneCheckList[bone.fParent] and self:IsBonePickable(moho, skel, bone) then
		if self.fixChildren then 
			-- fix angle to old global (rotate inverse parent dif)
			local parentInfo = self.boneCheckList[bone.fParent]
			local parentBone = skel:Bone(bone.fParent)
			local parentAngleDif = parentInfo.globalAngleDif
			if parentAngleDif and not bone.fFixedAngle then
				parentAngleDif = self:CropAngle(parentAngleDif)
				bone.fAnimAngle:SetValue(frame, bone.fAnimAngle:GetValue(frame) - parentAngleDif)
			end
			--also fix child position calculating it from parent PRS change
			local oldPos = bone.fAnimPos:GetValue(frame)
			parentInfo.startMatrix:Transform(oldPos)
			local newMatrix = LM.Matrix:new_local()
			newMatrix = AE_Utilities:GetGlobalBoneMatrix(moho, skel, parentBone, frame, actionName)
			newMatrix:Invert()
			newMatrix:Transform(oldPos)
			bone.fAnimPos:SetValue(frame, oldPos)
		end
	end
	skel:UpdateBoneMatrix(id)
	for i=0, skel:CountBones()-1 do
		local childBone = skel:Bone(i)
		if childBone.fParent == id then
			self:ApplyToBone(moho, skel, childBone, i)
		end
	end
end

function AE_BoneMagnet:OnKeyDown(moho, keyEvent)
	if (not self.dragging) then
		LM_SelectBone:OnKeyDown(moho, keyEvent)
	end	
end

function AE_BoneMagnet:IsBonePickable(moho, skel, bone)
	if bone.fHidden then return false end
	if self.selectedOnly and not bone.fSelected then return false end
	if (bone.fAngleControlParent >= 0 or
		bone.fPosControlParent >= 0 or
		bone.fScaleControlParent >= 0 or
		bone.fBoneDynamics.value ) then
		return false
	end	
	return true
end

function AE_BoneMagnet:DrawMe(moho, view)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end

	if (self.selectionMode) then
		LM_SelectBone:DrawMe(moho, view)
		return
	end
	
	
	
	local vc1 = LM.ColorVector:new_local()
	local vc2 = LM.ColorVector:new_local()
	vc1:Set(MOHO.MohoGlobals.SelCol)
	vc2:Set(MOHO.MohoGlobals.BackCol)
	--vc1 = (vc1 * 3 + vc2 * 4) / 7
	vc2 = (vc1 + vc2) / 2
	local normOutlineCol = vc1:AsColorStruct()
	local normFillCol = vc2:AsColorStruct()
	local markerR = 6
	local v = LM.Vector2:new_local()
	local v2 = LM.Vector2:new_local()
	local g = view:Graphics()
	local layerMatrix = LM.Matrix:new_local()
	
	
	
	moho.layer:GetFullTransform(moho.frame, layerMatrix, moho.document)
	g:Push()
	g:ApplyMatrix(layerMatrix)
	g:SetSmoothing(true)
	g:SetBezierTolerance(2)
	
	local outlineCol = normOutlineCol
	local fillCol = normFillCol
	g:SetPenWidth(1)
	
	--TODO:----------------------------------
	if (not self.dragging) then
		--draw something like sketchbones do
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			local drawBone = self:IsBonePickable(moho, skel, bone)
			if drawBone then				
				v:Set(0, 0)
				if (moho.frame == 0) then
					bone.fRestMatrix:Transform(v)
				else
					bone.fMovedMatrix:Transform(v)
				end
				if bone.fLength > 0 then
					v2:Set(bone.fLength, 0)
					if (moho.frame == 0) then
						bone.fRestMatrix:Transform(v2)
					else
						bone.fMovedMatrix:Transform(v2)
					end
					g:SetColor(outlineCol)
					g:DrawLine(v.x, v.y, v2.x, v2.y)
					g:FrameCirclePixelRadius(v2, markerR)
					g:SetColor(fillCol)
					g:FillCirclePixelRadius(v2, markerR)
				end
				g:SetColor(outlineCol)
				g:FrameCirclePixelRadius(v, markerR)
				g:SetColor(fillCol)
				g:FillCirclePixelRadius(v, markerR)				
			end
		end
	
	else
		--draw same stucture, but from array, not real bones
		for i, boneInfo in pairs(self.boneList) do
			local bone = skel:Bone(boneInfo.id)
			local v = boneInfo.points[1].current
			local v2 = boneInfo.points[2].current
			if bone.fLength > 0 then
				g:SetColor(outlineCol)
				g:DrawLine(v.x, v.y, v2.x, v2.y)
				g:FrameCirclePixelRadius(v2, markerR)
				g:SetColor(fillCol)
				g:FillCirclePixelRadius(v2, markerR)				
			end
			g:SetColor(outlineCol)
			g:FrameCirclePixelRadius(v, markerR)
			g:SetColor(fillCol)
			g:FillCirclePixelRadius(v, markerR)				
		end
	end
	-----------------------------------------
	
	g:Pop()

	MOHO.DrawToolSizeCircle(moho, view, self.dragVec, self.magnetRadius, self.dragResize)

end

-- **************************************************
-- Tool options - create and respond to tool's UI
-- **************************************************

AE_BoneMagnet.CHANGE = MOHO.MSG_BASE
AE_BoneMagnet.DUMMY = MOHO.MSG_BASE + 1
AE_BoneMagnet.RESET = MOHO.MSG_BASE + 2
AE_BoneMagnet.SELECTITEM = MOHO.MSG_BASE + 3


function AE_BoneMagnet:DoLayout(moho, layout)
	layout:AddChild(LM.GUI.StaticText("Magnet radius"))
	self.radius = LM.GUI.TextControl(0, "00.0000", self.CHANGE, LM.GUI.FIELD_UFLOAT)
	self.radius:SetWheelInc(0.01)
	layout:AddChild(self.radius)

	self.selectedCheck = LM.GUI.CheckBox("Selected bones only", self.CHANGE)
	layout:AddChild(self.selectedCheck)
	
	self.fixChildrenCheck = LM.GUI.CheckBox("Fix Children", self.CHANGE)
	layout:AddChild(self.fixChildrenCheck)	

	--layout:AddChild(LM.GUI.Button(MOHO.Localize("/Scripts/Tool/Magnet/Reset=Reset"), self.RESET))
end

function AE_BoneMagnet:UpdateWidgets(moho)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end

	self.radius:SetValue(self.magnetRadius)
	self.selectedCheck:SetValue(self.selectedOnly)
	self.fixChildrenCheck:SetValue(self.fixChildren)
end

function AE_BoneMagnet:HandleMessage(moho, view, msg)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end

	if (msg == self.RESET) then
		self.magnetRadius = 0.5
		moho:UpdateUI()
	elseif (msg == self.CHANGE) then
		self.magnetRadius = self.radius:FloatValue()
		if (self.magnetRadius < 0.001) then
			self.magnetRadius = 0.001
		end
		self.selectedOnly = self.selectedCheck:Value()
		self.fixChildren = self.fixChildrenCheck:Value()
	elseif (msg >= self.SELECTITEM) then
		mesh:SelectNone()
		local i = msg - self.SELECTITEM
		local name = mesh:Group(i):Name()
		mesh:SelectGroup(name)
		moho:UpdateUI()
	end
end

Icon
Bone Magnet
Listed

Script type: Tool

Uploaded: Apr 17 2022, 14:24

Last modified: Apr 22 2022, 12:48

Script Version: 1.1

Just like point magnet, but for bones.
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: 1374