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

ScriptName = "AE_TransformBone"

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

AE_TransformBone = {}

AE_TransformBone.BASE_STR = 2365

function AE_TransformBone:Name()
	return "Transform Bone"
end

function AE_TransformBone:Version()
	return "6.44"
end

function AE_TransformBone:Description()
	return "Move bone (hold <shift> to constrain, <alt> to force translation, <ctrl/cmd> to force scale); Press (shift and) '+' to select children"
end

function AE_TransformBone:Creator()
	return "Smith Micro Software, Inc. improved by Alexandra Evseeva"
end

function AE_TransformBone:UILabel()
	return(MOHO.Localize("/Scripts/Tool/TransformBone/TransformBone=Transform Bone"))
end

function AE_TransformBone:LoadPrefs(prefs)
	self.showPath = prefs:GetBool("AE_TransformBone.showPath", true)
	self.showLabel = prefs:GetBool("AE_TransformBone.showLabel", true)	
	self.smartOnly = prefs:GetBool("AE_TransformBone.smartOnly", false)
	self.shiftDivide = prefs:GetInt("AE_TransformBone.shiftDivide", 4)
	self.nonBone = prefs:GetBool("AE_TransformBone.nonBone", false)
	self.posIncr = prefs:GetFloat("AE_TransformBone.posIncr", 0.1)	
	self.scaleIncr = prefs:GetFloat("AE_TransformBone.scaleIncr", 0.1)	
	self.angleIncr = prefs:GetFloat("AE_TransformBone.angleIncr", 5)
	self.localNudge = prefs:GetBool("AE_TransformBone.localNudge", false)
end

function AE_TransformBone:SavePrefs(prefs)
	prefs:SetBool("AE_TransformBone.showPath", self.showPath)
	prefs:SetBool("AE_TransformBone.showLabel", self.showLabel)	
	prefs:SetBool("AE_TransformBone.smartOnly", self.smartOnly)
	prefs:SetInt("AE_TransformBone.shiftDivide", self.shiftDivide)
	prefs:SetBool("AE_TransformBone.nonBone", self.nonBone)
	prefs:SetFloat("AE_TransformBone.posIncr", self.posIncr)	
	prefs:SetFloat("AE_TransformBone.scaleIncr", self.scaleIncr)	
	prefs:SetFloat("AE_TransformBone.angleIncr", self.angleIncr)
	prefs:SetBool("AE_TransformBone.localNudge", self.localNudge)
end

function AE_TransformBone:ResetPrefs()
	self.showPath = true
	self.showLabel = true
	self.smartOnly = false
	self.shiftDivide = 4
	self.nonBone = false
	self.posIncr = 0.1
	self.scaleIncr = 0.1
	self.angleIncr = 5
	self.localNudge = false
end

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

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

AE_TransformBone.dragging = false
AE_TransformBone.keyMovement = false
AE_TransformBone.mode = 0 -- 0:translate, 1:rotate, 2:scale, 3:manipulate bones
AE_TransformBone.numSel = 0
AE_TransformBone.selID = -1
AE_TransformBone.mousePickedID = -1
AE_TransformBone.boneEnd = -1
AE_TransformBone.lastVec = LM.Vector2:new_local()
AE_TransformBone.boneChanged = false
AE_TransformBone.showPath = true
AE_TransformBone.translationFrame = 0
AE_TransformBone.trPathBone = nil
AE_TransformBone.TOLERANCE = 10
AE_TransformBone.selectedLayerChanged = false
AE_TransformBone.nonBone = false
AE_TransformBone.shiftDivide = 4
AE_TransformBone.posIncr = 0.1
AE_TransformBone.scaleIncr = 0.1
AE_TransformBone.angleIncr = 5
AE_TransformBone.localNudge = false
AE_TransformBone.indMode = false

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

function AE_TransformBone:IsEnabled(moho)

	if self.nonBone then 
		self.lastTool = moho:CurrentTool()
		return true		
	end
	if moho:CountBones() < 1 then
		return false
	end
	return true		
end

function AE_TransformBone:IsRelevant(moho)

	if self.nonBone then
		self.lastTool = moho:CurrentTool()
		return true
	else
		local skel = moho:Skeleton()
		if (skel == nil) then
			return false
		end
		return true
	end
end

function AE_TransformBone:TestMousePoint(moho, mouseEvent)

	self.trPathBone = nil

	if (self.keyMovement) then
		return 0
	end

	local skel = moho:Skeleton()
	if (skel == nil) then
		return 1
	end

	if (self.mousePickedID >= 0) then
		local bone = skel:Bone(self.mousePickedID)
		if (bone:IsZeroLength()) then
			if (mouseEvent.altKey) then
				return 1 -- alt key forces a zero length bone into rotation mode
			elseif (mouseEvent.ctrlKey) then
				return 2
			end
			return 0 -- translation by default for zero-length bones
		end
	end


	if (mouseEvent.altKey) then
		return 0
	elseif (mouseEvent.ctrlKey) then
		return 2
	end

	local markerR = 6
	local v = LM.Vector2:new_local()
	local pt = LM.Point:new_local()
	local m = LM.Matrix:new_local()

	moho.layer:GetFullTransform(moho.frame, m, moho.document)

	-- first test for translation, as it's more common than scaling
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if ((bone.fSelected or i == self.mousePickedID) and not bone.fHidden) then
			v:Set(bone.fLength * 0.075, 0)
			if (moho.frame == 0) then
				bone.fRestMatrix:Transform(v)
			else
				bone.fMovedMatrix:Transform(v)
			end
			m:Transform(v)
			mouseEvent.view:Graphics():WorldToScreen(v, pt)
			if (math.abs(pt.x - mouseEvent.pt.x) < markerR and math.abs(pt.y - mouseEvent.pt.y) < markerR) then
				return 0
			end
		end
	end

	-- next test for scaling
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if ((bone.fSelected or i == self.mousePickedID) and not bone.fHidden) then
			v:Set(bone.fLength - bone.fLength * 0.075, 0)
			if (moho.frame == 0) then
				bone.fRestMatrix:Transform(v)
			else
				bone.fMovedMatrix:Transform(v)
			end
			m:Transform(v)
			mouseEvent.view:Graphics():WorldToScreen(v, pt)
			if (math.abs(pt.x - mouseEvent.pt.x) < markerR and math.abs(pt.y - mouseEvent.pt.y) < markerR) then
				return 2
			end
		end
	end

	-- finally, test for translation by dragging a curve
	local selCount = 0
	local selBone = nil
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if (bone.fSelected) then
			selCount = selCount + 1
			selBone = bone
			if (selCount > 1) then
				break
			end
		end
	end
	if (selCount == 1 and selBone ~= nil and self.showPath) then
		local translationWhen = -20000
		local g = mouseEvent.view:Graphics()
		local m = LM.Matrix:new_local()
		local vec = LM.Vector2:new_local()
		local pt = LM.Point:new_local()
		local totalTimingOffset = moho.layer:TotalTimingOffset()
		moho.layer:GetFullTransform(moho.frame, m, moho.document)
		-- First see if any keyframes were picked
		for i = 0, selBone.fAnimPos:CountKeys() - 1 do
			local frame = selBone.fAnimPos:GetKeyWhen(i)
			vec = selBone.fAnimPos:GetValue(frame)
			m:Transform(vec)
			g:WorldToScreen(vec, pt)
			if (math.abs(pt.x - mouseEvent.pt.x) < self.TOLERANCE and math.abs(pt.y - mouseEvent.pt.y) < self.TOLERANCE) then
				translationWhen = frame
				self.trPathBone = selBone
				break
			end
		end
		-- If no keyframes were picked, try picking a random point along the curve.
		if (translationWhen <= -10000) then
			local startFrame = selBone.fAnimPos:GetKeyWhen(0)
			local endFrame = selBone.fAnimPos:Duration()
			if (endFrame > startFrame) then
				local oldVec = LM.Vector2:new_local()
				g:Clear(0, 0, 0, 0)
				g:SetColor(255, 255, 255)
				g:BeginPicking(mouseEvent.pt, 4)
				for frame = startFrame, endFrame do
					vec = selBone.fAnimPos:GetValue(frame)
					m:Transform(vec)
					if (frame > startFrame) then
						g:DrawLine(oldVec.x, oldVec.y, vec.x, vec.y)
					end
					if (g:Pick()) then
						translationWhen = frame
						self.trPathBone = selBone
						break
					end
					oldVec:Set(vec)
				end
			end
		end
		if (translationWhen > -10000) then
			self.translationFrame = translationWhen
			return 0
		end
	end

	-- if no handle was clicked on, default to rotation
	return 1
end

function AE_TransformBone:OnMouseDown(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	self.dragging = true

	self.translationFrame = moho.layerFrame

	if (self.selID < 0) then
		self.mode = self:TestMousePoint(moho, mouseEvent)
		if (moho.frame == 0 and self.mode == 2) then
			self.mode = 0
		end
	else
		-- If selID already has a value, then this function is being called by LM_AddBone.
		-- In that case, the mode has already been determined.
		self.mode = 0
	end

	if (self.mode == 3) then
		LM_ManipulateBones:OnMouseDown(moho, mouseEvent)
		return
	end

	self.numSel = moho:CountSelectedBones(true)
	
	self.lastVec:Set(mouseEvent.vec)
	self.boneChanged = false

	if (self.mode == 0) then
		self:OnMouseDown_T(moho, mouseEvent)
	elseif (self.mode == 1) then
		self:OnMouseDown_R(moho, mouseEvent)
	else
		self:OnMouseDown_S(moho, mouseEvent)
	end

	self.numSel = moho:CountSelectedBones(true)

	moho.layer:UpdateCurFrame()
	moho:UpdateBonePointSelection()
	mouseEvent.view:DrawMe()
	moho:UpdateSelectedChannels()
	
	if self.indMode then 
		moho.document:PrepMultiUndo()
		self:StoreChildTransforms(moho)
	end
	self.initBone = skel:NearestBone(mouseEvent.vec)
	if self.initBone > -1 then self.initBone = skel:Bone(self.initBone) else self.initBone = nil end
end

function AE_TransformBone:OnMouseMoved(moho, mouseEvent)

	if (self.mode == 3) then
		LM_ManipulateBones:OnMouseMoved(moho, mouseEvent)
		return
	end

	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	if (not self.dragging) then
		if (moho:CountSelectedBones(true) < 2) then
			self.mousePickedID = mouseEvent.view:PickBone(mouseEvent.pt, mouseEvent.vec, moho.layer, false)
		else
			self.mousePickedID = -1
		end
		local mode = self:TestMousePoint(moho, mouseEvent)

		if (mode == 0) then
			mouseEvent.view:SetCursor(MOHO.moveCursor)
			if (self.trPathBone ~= nil) then
				self.mousePickedID = skel:BoneID(self.trPathBone)
			end
		elseif (mode == 2) then
			mouseEvent.view:SetCursor(MOHO.scaleCursor)
		else
			mouseEvent.view:SetCursor(MOHO.rotateCursor)
		end
		mouseEvent.view:DrawMe()
		return
	end
	if (self.numSel < 1) then
		return
	end

	if (self.mode == 0) then
		self:OnMouseMoved_T(moho, mouseEvent)
	elseif (self.mode == 1) then
		self:OnMouseMoved_R(moho, mouseEvent)
	else
		self:OnMouseMoved_S(moho, mouseEvent)
	end

	moho.layer:UpdateCurFrame()
	mouseEvent.view:DrawMe()
	if (self.mode ~= 0 or self.boneEnd ~= 1) then
		self.lastVec:Set(mouseEvent.vec)
	end
	
	if self.indMode then self:RestoreChildTransforms(moho, true) end
	
end

function AE_TransformBone:OnMouseUp(moho, mouseEvent)
	if (self.mode == 3) then
		LM_ManipulateBones:OnMouseUp(moho, mouseEvent)
		return
	end

	local skel = moho:Skeleton()
	if (skel == nil) then
		self.dragging = false
		return
	end

	if (self.mode == 0) then
		self:OnMouseUp_T(moho, mouseEvent)
	elseif (self.mode == 1) then
		self:OnMouseUp_R(moho, mouseEvent)
	else
		self:OnMouseUp_S(moho, mouseEvent)
	end

	self.selID = -1
	self.boneEnd = -1
	self.dragging = false
	
	if self.indMode then 
		self:RestoreChildTransforms(moho)
		self.indMode = false
		self:UpdateWidgets(moho)
	end
end

function AE_TransformBone:OnMouseDown_T(moho, mouseEvent)
	local skel = moho:Skeleton()

	if (self.selID < 0) then
		-- If selID already has a value, then this function is being called by LM_AddBone.
		-- In that case, PrepUndo has already been called, and the bone has been picked.
		moho.document:PrepUndo(moho.layer, true)
		moho.document:SetDirty()
		if (self.numSel > 1) then
			self.selID = -2
			self.boneEnd = 0
		elseif (self.trPathBone ~= nil) then
			self.selID = skel:BoneID(self.trPathBone)
		else
			self.selID = mouseEvent.view:PickBone(mouseEvent.pt, mouseEvent.vec, moho.layer, false)
		end
	end

	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)

		if (moho.frame == 0) then
			bone.fTempPos:Set(bone.fAnimPos:GetValue(self.translationFrame))
			bone.fTempLength = bone.fLength
			bone.fTempAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
		else
			--bone.fTempPos:Set(bone.fPos)
			bone.fTempPos:Set(bone.fAnimPos:GetValue(self.translationFrame))
		end

		if (self.numSel < 2) then
			if (i == self.selID) then
				bone.fSelected = true
				if (self.boneEnd < 0) then
					if (moho.frame == 0 and self.trPathBone == nil) then
						local boneVec = LM.Vector2:new_local()
						boneVec:Set(0, 0)
						bone.fRestMatrix:Transform(boneVec)
						boneVec = boneVec - mouseEvent.startVec
						local d = boneVec:Mag()
						self.boneEnd = 0
						boneVec:Set(bone.fLength, 0)
						bone.fRestMatrix:Transform(boneVec)
						boneVec = boneVec - mouseEvent.startVec
						if (boneVec:Mag() < d) then
							self.boneEnd = 1
						end
					else
						self.boneEnd = 0
					end
				end
			else
				bone.fSelected = false
			end
		end
		
		if (self.translationFrame ~= 0 and bone.fSelected) then
			self.boneChanged = true
			bone.fAnimPos:SetValue(self.translationFrame, bone.fTempPos)
		end
	end
end

function AE_TransformBone:OnMouseMoved_T(moho, mouseEvent)
	local skel = moho:Skeleton()

	for boneID = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(boneID)
		if (bone.fSelected and (not skel:IsAncestorSelected(boneID))) then
			if (moho.frame == 0) then
				bone.fAnimPos:SetValue(self.translationFrame, bone.fTempPos)
				bone.fLength = bone.fTempLength
				bone.fAnimAngle:SetValue(moho.layerFrame, bone.fTempAngle)
				self.boneChanged = true
			else
				bone.fPos:Set(bone.fTempPos)
			end
			skel:UpdateBoneMatrix(boneID)
		
			local offset = mouseEvent.vec
			if (self.boneEnd == 0) then
				offset = offset - mouseEvent.startVec
			elseif (self.boneEnd == 1) then
				offset = offset - self.lastVec
			end
		
			local boneVec = LM.Vector2:new_local()
			local inverseM = LM.Matrix:new_local()
		
			if (self.boneEnd == 0) then -- move the base of the bone
				local parent = nil
				boneVec:Set(0, 0)
				if (bone.fParent >= 0) then
					parent = skel:Bone(bone.fParent)
					if (moho.frame == 0) then
						parent.fRestMatrix:Transform(boneVec)
					else
						parent.fMovedMatrix:Transform(boneVec)
					end
				end
				boneVec = boneVec + offset
				if (parent) then
					if (moho.frame == 0) then
						inverseM:Set(parent.fRestMatrix)
					else
						inverseM:Set(parent.fMovedMatrix)
					end
					inverseM:Invert()
					inverseM:Transform(boneVec)
				end
		
				if (mouseEvent.shiftKey) then
					if (math.abs(boneVec.x) > math.abs(boneVec.y)) then
						boneVec.y = 0
					else
						boneVec.x = 0
					end
				end
		
				local v = nil
				if (moho.frame == 0) then
					v = bone.fAnimPos:GetValue(self.translationFrame) + boneVec
					if (parent) then
						parent.fMovedMatrix:Transform(v)
					end
					v = LM_AddBone:SnapBone(moho, mouseEvent, bone, v)
					if (parent) then
						inverseM:Set(parent.fMovedMatrix)
						inverseM:Invert()
						inverseM:Transform(v)
					end
				else
					v = bone.fPos + boneVec
				end
				if (moho.gridOn) then
					if (parent) then
						parent.fMovedMatrix:Transform(v)
					end
					moho:SnapToGrid(v)
					if (parent) then
						inverseM:Set(parent.fMovedMatrix)
						inverseM:Invert()
						inverseM:Transform(v)
					end
				end
		
				bone.fAnimPos:SetValue(self.translationFrame, v)
				self.boneChanged = true
			elseif (self.boneEnd == 1) then -- move the tip of the bone
				boneVec:Set(bone.fLength, 0)
				bone.fRestMatrix:Transform(boneVec)
				boneVec = boneVec + offset
				if (moho.gridOn) then
					moho:SnapToGrid(boneVec)
					self.lastVec:Set(boneVec)
				else
					self.lastVec:Set(mouseEvent.vec)
				end
		
				inverseM:Set(bone.fRestMatrix)
				inverseM:Invert()
				inverseM:Transform(boneVec)
				local dL = boneVec:Mag() - bone.fLength
				bone.fLength = bone.fLength + dL
				local angle = bone.fAnimAngle:GetValue(moho.layerFrame)
				angle = angle + math.atan2(boneVec.y, boneVec.x)
				while angle > 2 * math.pi do
					angle = angle - 2 * math.pi
				end
				while angle < 0 do
					angle = angle + 2 * math.pi
				end
				bone.fTempAngle = angle
				bone.fTempLength = bone.fLength
				if (mouseEvent.shiftKey) then
					angle = angle / (math.pi / self.shiftDivide)
					angle = (math.pi / self.shiftDivide) * LM.Round(angle)
				end
				bone.fAnimAngle:SetValue(moho.layerFrame, angle)
				self.boneChanged = true
				for i = 0, skel:CountBones() - 1 do
					bone = skel:Bone(i)
					if (bone.fParent == boneID) then
						boneVec:Set(bone.fTempPos)
						boneVec.x = boneVec.x + dL
						bone.fAnimPos:SetValue(moho.layerFrame, boneVec)
					end
				end
			end
		end -- if bone selected
	end -- for all bones
end

function AE_TransformBone:OnMouseUp_T(moho, mouseEvent)
	local skel = moho:Skeleton()
	
	if (self.numSel > 0 or self.boneChanged) then
		--for i = 0, skel:CountBones() - 1 do
		--	bone = skel:Bone(i)
		--	if (bone.fSelected and (not skel:IsAncestorSelected(i))) then
		--		bone.fAnimPos:SetValue(moho.layerFrame, bone.fPos)
		--	end
		--end
		moho.layer:UpdateCurFrame()
		if (self.boneChanged) then
			if (self.translationFrame == moho.layerFrame) then
				moho:NewKeyframe(CHANNEL_BONE_T)
			else
				moho:UpdateUI()
			end
		end
			
		if MOHO.MohoGlobals.EditMultipleKeys then
			for b=0, skel:CountBones()-1 do			
				local nextBone = skel:Bone(b)
				if nextBone.fSelected then
					--print(nextBone.fTempPos.x, ", ", nextBone.fTempPos.y)
					--print(nextBone.fPos.x, ", ", nextBone.fPos.y)
					local diffValue = nextBone.fPos - nextBone.fTempPos
					for k = 0, nextBone.fAnimPos:CountKeys()-1 do
						local keyWhen = nextBone.fAnimPos:GetKeyWhen(k) 
						if keyWhen ~= self.translationFrame and nextBone.fAnimPos:IsKeySelected(keyWhen) then
							local newValue = nextBone.fAnimPos:GetValue(keyWhen) + diffValue
							nextBone.fAnimPos:SetValue(keyWhen, newValue)
						end
					end
				end
			end
		end		
		
		
	end


end

function AE_TransformBone:OnMouseDown_R(moho, mouseEvent)
	local skel = moho:Skeleton()

	moho.document:PrepUndo(moho.layer, true)
	moho.document:SetDirty()
	if (moho:CountSelectedBones(true) < 2) then
		for i = 0, skel:CountBones() - 1 do
			skel:Bone(i).fSelected = false
		end
		local id = mouseEvent.view:PickBone(mouseEvent.pt, mouseEvent.vec, moho.layer, false)
		skel:Bone(id).fSelected = true
	end

	local selCount = 0
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)

		if (bone.fSelected) then
			self.selID = i
			selCount = selCount + 1
			if (moho.frame == 0) then
				bone.fTempPos:Set(bone.fAnimPos:GetValue(moho.layerFrame))
				bone.fTempLength = bone.fLength
				bone.fTempAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
			else
				self.boneChanged = true
				bone.fTempAngle = bone.fAnimAngle:GetValue(moho.layerFrame)--bone.fAngle
				bone.fAnimAngle:SetValue(moho.layerFrame, bone.fTempAngle)
			end
		end
	end

	if (selCount > 1) then
		self.selID = -1
	end
	
	if MOHO.MohoGlobals.EditMultipleKeys then
		self.tempAngles = {}
		for i = 0, skel:CountBones() - 1 do
			if skel:Bone(i).fSelected then 
				local bone = skel:Bone(i)
				self.tempAngles[i] = bone.fAngle
			end
		end
	end
	
end

function AE_TransformBone:OnMouseMoved_R(moho, mouseEvent)
	local riggingFrame = 0
	local skel = moho:Skeleton()

	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if (bone.fSelected) then
			if (moho.frame == 0) then
				bone.fAnimPos:SetValue(moho.layerFrame, bone.fTempPos)
				bone.fLength = bone.fTempLength
				bone.fAnimAngle:SetValue(moho.layerFrame, bone.fTempAngle)
				self.boneChanged = true
			else
				bone.fAngle = bone.fTempAngle
			end
		end
	end
		
	skel:UpdateBoneMatrix()

	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if (bone.fSelected) then
			local origin = LM.Vector2:new_local()
			origin:Set(0, 0)
			if (moho:CountSelectedBones(true) < 2) then				
				if (moho.frame == 0) then
					bone.fRestMatrix:Transform(origin)
				else
					bone.fMovedMatrix:Transform(origin)
				end
			else
				if self.initBone then
					if (moho.frame == 0) then
						self.initBone.fRestMatrix:Transform(origin)
					else
						self.initBone.fMovedMatrix:Transform(origin)
					end					
				else
					origin = moho.layer:Origin()
				end
			end
			local v1 = self.lastVec - origin
			local v2 = mouseEvent.vec - origin
			local angleSign = 1.0
			if (not bone.fFixedAngle) then
				angleSign = bone:ParentalFlipFactor()
			end
			v2:Rotate(-math.atan2(v1.y, v1.x))
			if (moho.frame == 0) then
				local angle = bone.fAnimAngle:GetValue(moho.layerFrame) + math.atan2(v2.y, v2.x) * angleSign
				bone.fAnimAngle:SetValue(moho.layerFrame, angle)
				self.boneChanged = true
			else
				if bone.fFixedAngle and skel:Bone(bone.fParent) and skel:Bone(bone.fParent).fSelected then
					bone.fAngle = bone.fAnimAngle.value
				end
				bone.fAngle = bone.fAngle + math.atan2(v2.y, v2.x) * angleSign
			end

			if (moho.frame == 0) then
				local angle = bone.fAnimAngle:GetValue(moho.layerFrame)
				while angle > 2 * math.pi do
					angle = angle - 2 * math.pi
				end
				while angle < 0 do
					angle = angle + 2 * math.pi
				end
				bone.fTempAngle = angle
				if (mouseEvent.shiftKey) then
					angle = angle / (math.pi / self.shiftDivide)
					angle = (math.pi / self.shiftDivide) * LM.Round(angle)
				end
				bone.fAnimAngle:SetValue(moho.layerFrame, angle)
				self.boneChanged = true
			else
				bone.fTempAngle = bone.fAngle
				if (mouseEvent.shiftKey) then
					bone.fAngle = bone.fAngle / (math.pi / self.shiftDivide)
					bone.fAngle = (math.pi / self.shiftDivide) * LM.Round(bone.fAngle)
				end
				bone.fAnimAngle:SetValue(moho.layerFrame, bone.fAngle)
				self.boneChanged = true
				if (bone.fConstraints and (not bone.fFixedAngle)) then
					local min = bone.fAnimAngle:GetValue(riggingFrame)
					local max = min + bone.fMaxConstraint
					min = min + bone.fMinConstraint
					bone.fAngle = LM.Clamp(bone.fAngle, min, max)
					bone.fAnimAngle:SetValue(moho.layerFrame, bone.fAngle)
				end
				moho.layer:UpdateCurFrame()		
			end

			bone.fAngle = bone.fAnimAngle.value	
			if (moho.frame ~= 0 and (not bone.fFixedAngle)) then
				if (bone.fConstraints) then
					local min = bone.fAnimAngle:GetValue(riggingFrame)
					local max = min + bone.fMaxConstraint
					min = min + bone.fMinConstraint
					bone.fAngle = LM.Clamp(bone.fAngle, min, max)
				end
			end
		end
	end

	
end

function AE_TransformBone:OnMouseUp_R(moho, mouseEvent)
	local skel = moho:Skeleton()

	--if ((moho.frame > 0) and (moho:CountSelectedBones(true) > 0)) then
	if (moho:CountSelectedBones(true) > 0) then
		--for i = 0, skel:CountBones() - 1 do
		--	local bone = skel:Bone(i)
		--	if (bone.fSelected) then
		--		bone.fAnimAngle:SetValue(moho.layerFrame, bone.fAngle)
		--	end
		--end
		moho.layer:UpdateCurFrame()
		if (self.boneChanged) then
			moho:NewKeyframe(CHANNEL_BONE)
		end
		
		if MOHO.MohoGlobals.EditMultipleKeys then
			for b=0, skel:CountBones()-1 do			
				local nextBone = skel:Bone(b)
				if nextBone.fSelected then
					local diffValue = nextBone.fAngle - self.tempAngles[b]
					for k = 1, nextBone.fAnimAngle:CountKeys()-1 do
						local keyWhen = nextBone.fAnimAngle:GetKeyWhen(k) 
						if keyWhen ~= self.translationFrame and nextBone.fAnimAngle:IsKeySelected(keyWhen) then
							local newValue = nextBone.fAnimAngle:GetValue(keyWhen) + diffValue
							nextBone.fAnimAngle:SetValue(keyWhen, newValue)
						end
					end
				end
			end
		end	
		
	end
end

function AE_TransformBone:OnMouseDown_S(moho, mouseEvent)
	local skel = moho:Skeleton()

	moho.document:PrepUndo(moho.layer, true)
	moho.document:SetDirty()
	if (moho:CountSelectedBones(true) < 2) then
		for i = 0, skel:CountBones() - 1 do
			skel:Bone(i).fSelected = false
		end
		local id = mouseEvent.view:PickBone(mouseEvent.pt, mouseEvent.vec, moho.layer, false)
		skel:Bone(id).fSelected = true
	end

	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)

		if (bone.fSelected) then
			if (moho.frame == 0) then
				bone.fTempScale = bone.fAnimScale:GetValue(moho.layerFrame)
			else
				self.boneChanged = true
				bone.fTempScale = bone.fAnimScale:GetValue(moho.layerFrame)--bone.fScale
				bone.fAnimScale:SetValue(moho.layerFrame, bone.fTempScale)
			end
		end
	end
end

function AE_TransformBone:OnMouseMoved_S(moho, mouseEvent)
	local skel = moho:Skeleton()

	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if (bone.fSelected) then
			local scaleFactor = (mouseEvent.pt.x - mouseEvent.startPt.x) / 100
			if (scaleFactor < 0) then
				scaleFactor = 1 / (-scaleFactor + 1)
			else
				scaleFactor = scaleFactor + 1
			end
			bone.fScale = bone.fTempScale * scaleFactor

			bone.fAnimScale:SetValue(moho.layerFrame, bone.fScale)
			self.boneChanged = true
		end
	end
end

function AE_TransformBone:OnMouseUp_S(moho, mouseEvent)
	local skel = moho:Skeleton()

	if ((moho.frame > 0) and (moho:CountSelectedBones(true) > 0)) then
		--for i = 0, skel:CountBones() - 1 do
		--	local bone = skel:Bone(i)
		--	if (bone.fSelected) then
		--		bone.fAnimScale:SetValue(moho.layerFrame, bone.fScale)
		--	end
		--end
		moho.layer:UpdateCurFrame()
		if (self.boneChanged) then
			moho:NewKeyframe(CHANNEL_BONE_S)
		end
	end
end

function AE_TransformBone:OnKeyDown(moho, keyEvent)

	if self.localNudge and self:KeyIncrements(moho, keyEvent) then
		keyEvent.view:DrawMe()
		return
	end

	if keyEvent.key == "f" and keyEvent.altKey then
		if self.freezeMode then self.freezeMode = false
		else self.freezeMode = true
		end
		return
	end

	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	
	if ((keyEvent.keyCode == LM.GUI.KEY_DELETE) or (keyEvent.keyCode == LM.GUI.KEY_BACKSPACE)) then
		local names2delete = {}
		local actions2delete = {}
		for b=0, skel:CountBones()-1 do
			local nextBone = skel:Bone(b)
			if nextBone.fSelected then 
				local name = nextBone:Name()
				table.insert(names2delete, name)
				-- find smartbone actions
				for a=0, moho.layer:CountActions()-1 do
					local actionName = moho.layer:ActionName(a)
					if string.find(actionName, "^"..name.."$") or 
					string.find(actionName, "^"..name.." 2$")then
						table.insert(actions2delete, actionName)
					elseif
					string.find(actionName, "^"..name.."[-]?%d+|") or 
					string.find(actionName, "|"..name.."[-]?%d+$") then
						table.insert(actions2delete, actionName)
						table.insert(names2delete, actionName)
					end
				end
			end
		end
		for i, nextAction in pairs(actions2delete) do
			if moho.layer:HasAction(nextAction) then moho.layer:DeleteAction(nextAction) end
		end
	end

	LM_SelectBone:OnKeyDown(moho, keyEvent)
	
	
	if keyEvent.key == "+" or keyEvent.key == "=" then --select all direct children
		local inDirectChildren = keyEvent.shiftKey -- all the children tree
		local parents = {}
		local children = {}
		for i = 0, skel:CountBones() - 1 do
			if (skel:Bone(i).fSelected) then
				table.insert(parents, i)
			end
		end
		for i = 0, skel:CountBones() - 1 do
			for j,b in pairs(parents) do
				if (inDirectChildren and skel:IsBoneChild(b,i)) or skel:Bone(i).fParent == b then 
					table.insert(children, i)
				end
			end
		end
		if #children > 0 then
			if not inDirectChildren then 
				for k,v in pairs(parents) do skel:Bone(v).fSelected = false end
			end
			for k,v in pairs(children) do skel:Bone(v).fSelected = true end
			moho:UpdateBonePointSelection()
			keyEvent.view:DrawMe()
			moho:UpdateSelectedChannels()
		end
	end
	
	
	if (keyEvent.ctrlKey) then
		local inc = 1
		if (keyEvent.shiftKey) then
			inc = 10
		end

		local m = LM.Matrix:new_local()
		moho.layer:GetFullTransform(moho.frame, m, moho.document)

		local fakeME = {}
		fakeME.view = keyEvent.view
		fakeME.pt = LM.Point:new_local()
		fakeME.pt:Set(keyEvent.view:Graphics():Width() / 2, keyEvent.view:Graphics():Height() / 2)
		fakeME.startPt = LM.Point:new_local()
		fakeME.startPt:Set(fakeME.pt)
		fakeME.vec = keyEvent.view:Point2Vec(fakeME.pt, m)
		fakeME.startVec = keyEvent.view:Point2Vec(fakeME.pt, m)
		fakeME.shiftKey = false
		fakeME.ctrlKey = false
		fakeME.altKey = keyEvent.altKey
		fakeME.penPressure = 0

		self.keyMovement = true

		if (keyEvent.keyCode == LM.GUI.KEY_UP) then
			self.selID = self:SelIDForNudge(moho, skel)
			self:OnMouseDown(moho, fakeME)
			self.boneEnd = 0
			fakeME.pt.y = fakeME.pt.y - inc
			fakeME.vec = keyEvent.view:Point2Vec(fakeME.pt, m)
			self:OnMouseMoved(moho, fakeME)
			self:OnMouseUp(moho, fakeME)
		elseif (keyEvent.keyCode == LM.GUI.KEY_DOWN) then
			self.selID = self:SelIDForNudge(moho, skel)
			self:OnMouseDown(moho, fakeME)
			self.boneEnd = 0
			fakeME.pt.y = fakeME.pt.y + inc
			fakeME.vec = keyEvent.view:Point2Vec(fakeME.pt, m)
			self:OnMouseMoved(moho, fakeME)
			self:OnMouseUp(moho, fakeME)
		elseif (keyEvent.keyCode == LM.GUI.KEY_LEFT) then
			self.selID = self:SelIDForNudge(moho, skel)
			self:OnMouseDown(moho, fakeME)
			self.boneEnd = 0
			fakeME.pt.x = fakeME.pt.x - inc
			fakeME.vec = keyEvent.view:Point2Vec(fakeME.pt, m)
			self:OnMouseMoved(moho, fakeME)
			self:OnMouseUp(moho, fakeME)
		elseif (keyEvent.keyCode == LM.GUI.KEY_RIGHT) then
			self.selID = self:SelIDForNudge(moho, skel)
			self:OnMouseDown(moho, fakeME)
			self.boneEnd = 0
			fakeME.pt.x = fakeME.pt.x + inc
			fakeME.vec = keyEvent.view:Point2Vec(fakeME.pt, m)
			self:OnMouseMoved(moho, fakeME)
			self:OnMouseUp(moho, fakeME)
		end

		self.keyMovement = false
	end
end

function AE_TransformBone:DrawMe(moho, view)

	if self.nonBone then 
		local skel = moho:Skeleton()
		if (skel == nil) then
			if moho:CurrentTool() ~= self.lastTool then
				local boneLayer = moho.layer:ControllingBoneLayer()
				if boneLayer then 
					moho:SetSelLayer(boneLayer)
				end
			end 
		end
		self.lastTool = moho:CurrentTool()
	end
	
	if ((self.dragging or moho:IsPlaying()) and not self.showPath) then
		return
	end
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end

	local markerR = 6
	local v = LM.Vector2:new_local()
	local g = view:Graphics()
	local layerMatrix = LM.Matrix:new_local()
	local vc1 = LM.ColorVector:new_local()
	local vc2 = LM.ColorVector:new_local()
	local interp = MOHO.InterpSetting:new_local()

	vc1:Set(MOHO.MohoGlobals.SelCol)
	vc2:Set(MOHO.MohoGlobals.BackCol)
	--vc1 = (vc1 * 3 + vc2 * 4) / 7
	vc1 = (vc1 + vc2) / 2
	local fillCol = vc1:AsColorStruct()

	moho.layer:GetFullTransform(moho.frame, layerMatrix, moho.document)
	g:Push()
	g:ApplyMatrix(layerMatrix)
	g:SetSmoothing(true)
	g:SetBezierTolerance(2)

	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)

		if (bone.fSelected and self.showPath) then --and bone.fParent < 0) then
			-- draw path
			--local startFrame = bone.fAnimPos:GetKeyWhen(0)
			--local endFrame = bone.fAnimPos:Duration()

			local totalTimingOffset = moho.layer:TotalTimingOffset()
			local startFrame = moho.document:StartFrame() + totalTimingOffset
			local endFrame = moho.document:EndFrame() + totalTimingOffset
			--[[
			if (startFrame - totalTimingOffset < 0) then
				startFrame = totalTimingOffset
			end
			--]]
			if MOHO.MohoGlobals.PlayStart > 0 then startFrame = MOHO.MohoGlobals.PlayStart + totalTimingOffset end
			if MOHO.MohoGlobals.PlayEnd > 0 then endFrame = MOHO.MohoGlobals.PlayEnd + totalTimingOffset end
			--[[
			bone.fAnimPos:GetKeyInterp(endFrame, interp)
			if (interp:IsAdditiveCycle()) then
				endFrame = moho.document:EndFrame() + totalTimingOffset
			end
			--]]
			if (endFrame > startFrame) then
				local vec = LM.Vector2:new_local()
				
				local oldVec = LM.Vector2:new_local()
				
				g:SetColor(102, 152, 203)
				for frame = startFrame, endFrame do
					--vec = bone.fAnimPos:GetValue(frame)
					vec = AE_Utilities:GetGlobalBonePos(moho, skel, bone, frame)
					if (frame > startFrame) then
						g:DrawLine(oldVec.x, oldVec.y, vec.x, vec.y)
					end
					if (bone.fAnimPos:HasKey(frame)) then
						g:DrawFatMarker(vec.x, vec.y, 5)
					else
						g:DrawMarker(vec.x, vec.y)
					end
					oldVec:Set(vec)
				end
			end
		end

		if (((bone.fSelected and self.mousePickedID == -1) or i == self.mousePickedID) and not bone.fHidden) then
			-- draw handles
			if (not (self.dragging or moho:IsPlaying())) then
				v:Set(bone.fLength - bone.fLength * 0.075, 0)
				if (moho.frame == 0) then
					bone.fRestMatrix:Transform(v)
				else
					bone.fMovedMatrix:Transform(v)
				end
				g:SetColor(fillCol)
				g:FillCirclePixelRadius(v, markerR)
				g:SetColor(MOHO.MohoGlobals.SelCol)
				g:FrameCirclePixelRadius(v, markerR)

				if (not bone:IsZeroLength()) then
					v:Set(bone.fLength * 0.075, 0)
					if (moho.frame == 0) then
						bone.fRestMatrix:Transform(v)
					else
						bone.fMovedMatrix:Transform(v)
					end
					g:SetColor(fillCol)
					g:FillCirclePixelRadius(v, markerR)
					g:SetColor(MOHO.MohoGlobals.SelCol)
					g:FrameCirclePixelRadius(v, markerR)
				end
				
				v:Set(bone.fLength * 0.5, 0.05/g:CurrentScale())
				if (moho.frame == 0) then
					bone.fRestMatrix:Transform(v)
				else
					bone.fMovedMatrix:Transform(v)
				end	
				if self.showLabel then
					HV_Font:DrawLetters(moho, g, bone:Name(), 5/g:CurrentScale(), v.x, v.y)
				end
			end
		end
	end
	
	g:Pop()
end

function AE_TransformBone:SelIDForNudge(moho, skel)
	for i = 0, skel:CountBones() - 1 do
		if (skel:Bone(i).fSelected) then
			return i
		end
	end
	return -1
end

function AE_TransformBone:FlipBones(moho, skel, horizontal)
	if (moho:CountSelectedBones(true) < 1) then
		return
	end

	if (moho.layerFrame == 0) then -- flip on rigging frame
		moho.document:PrepUndo(moho.layer, true)
		moho.document:SetDirty()
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (bone.fSelected) then
				skel:FlipBone(i, horizontal)
			end
		end
		moho.layer:UpdateCurFrame()
	else -- animation flip
		moho.document:PrepUndo(moho.layer, true)
		moho.document:SetDirty()
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (bone.fSelected) then
				if (horizontal) then
					bone.fFlipH:SetValue(moho.layerFrame, not bone.fFlipH.value)
				else
					bone.fFlipV:SetValue(moho.layerFrame, not bone.fFlipV.value)
				end
			end
		end
		moho.layer:UpdateCurFrame()
		if (horizontal) then
			moho:NewKeyframe(CHANNEL_BONE_FLIPH)
		else
			moho:NewKeyframe(CHANNEL_BONE_FLIPV)
		end
	end
end

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

AE_TransformBone.CHANGE_T_X = MOHO.MSG_BASE
AE_TransformBone.CHANGE_T_Y = MOHO.MSG_BASE + 1
AE_TransformBone.RESET_T = MOHO.MSG_BASE + 2
AE_TransformBone.CHANGE_L = MOHO.MSG_BASE + 3
AE_TransformBone.CHANGE_S = MOHO.MSG_BASE + 4
AE_TransformBone.RESET_S = MOHO.MSG_BASE + 5
AE_TransformBone.CHANGE_R = MOHO.MSG_BASE + 6
AE_TransformBone.RESET_R = MOHO.MSG_BASE + 7
AE_TransformBone.SHOW_PATHS = MOHO.MSG_BASE + 8
AE_TransformBone.FLIP_H = MOHO.MSG_BASE + 9
AE_TransformBone.FLIP_V = MOHO.MSG_BASE + 10
AE_TransformBone.DUMMY = MOHO.MSG_BASE + 11
AE_TransformBone.SELECTITEM = MOHO.MSG_BASE + 30

AE_TransformBone.SMARTONLYCHECK = MOHO.MSG_BASE + 12
AE_TransformBone.SETTINGSBUTTON = MOHO.MSG_BASE + 13

AE_TransformBone.NONEBONE = MOHO.MSG_BASE + 15
AE_TransformBone.localNudge = MOHO.MSG_BASE + 16
AE_TransformBone.SHOW_LABELS = MOHO.MSG_BASE + 17
AE_TransformBone.INDMODE = MOHO.MSG_BASE + 18

-- **************************************************
-- IncrDialog
-- **************************************************

local IncrDialog = {}

IncrDialog.POSINCRINPUT = MOHO.MSG_BASE + 20
IncrDialog.ANGLEINCRINPUT = MOHO.MSG_BASE + 21
IncrDialog.SCALEINCRINPUT = MOHO.MSG_BASE + 22
IncrDialog.SHIFTDIVIDE = MOHO.MSG_BASE + 14
IncrDialog.LOCALNUDGE = MOHO.MSG_BASE + 23

function IncrDialog:new()
    local d = LM.GUI.SimpleDialog('', IncrDialog)
    local l = d:GetLayout()

    d.posIncrInput = LM.GUI.TextControl(0, '0.1', d.POSINCRINPUT, LM.GUI.FIELD_FLOAT, 'Position increment')
    l:AddChild(d.posIncrInput, LM.GUI.ALIGN_LEFT, 0)

	
    d.scaleIncrInput = LM.GUI.TextControl(0, '0.1', d.SCALEINCRINPUT, LM.GUI.FIELD_FLOAT, 'Scale increment')
    l:AddChild(d.scaleIncrInput, LM.GUI.ALIGN_LEFT, 0)

    d.angleIncrInput = LM.GUI.TextControl(0, '0.1', d.ANGLEINCRINPUT, LM.GUI.FIELD_FLOAT, 'Angle increment')
    l:AddChild(d.angleIncrInput, LM.GUI.ALIGN_LEFT, 0)
	
	d.shiftDivideText = LM.GUI.TextControl(0, "00", d.SHIFTDIVIDE, LM.GUI.FIELD_INT, "On Shift:")
	l:AddChild(d.shiftDivideText)
	d.shiftDivideText:SetToolTip("180°/x = shift-rotate increment")
	
	d.localNudgeChk = LM.GUI.CheckBox("Local nudge bones", d.LOCALNUDGE)
	l:AddChild(d.localNudgeChk)
	
    return d
end

function IncrDialog:UpdateWidgets(moho)
    self.posIncrInput:SetValue(AE_TransformBone.posIncr)
    self.scaleIncrInput:SetValue(AE_TransformBone.scaleIncr)
    self.angleIncrInput:SetValue(AE_TransformBone.angleIncr)
	self.shiftDivideText:SetValue(AE_TransformBone.shiftDivide)	
	self.localNudgeChk:SetValue(AE_TransformBone.localNudge)
end

function IncrDialog:OnOK(moho)
    AE_TransformBone.posIncr = self.posIncrInput:FloatValue()
	AE_TransformBone.textX:SetWheelInc(AE_TransformBone.posIncr)
	AE_TransformBone.textY:SetWheelInc(AE_TransformBone.posIncr)
    AE_TransformBone.scaleIncr = self.scaleIncrInput:FloatValue()
	AE_TransformBone.angle:SetWheelInc(AE_TransformBone.angleIncr)
    AE_TransformBone.angleIncr = self.angleIncrInput:FloatValue()	
	AE_TransformBone.angle:SetWheelInc(AE_TransformBone.angleIncr)
	AE_TransformBone.shiftDivide = math.max(self.shiftDivideText:IntValue(), 1)
	AE_TransformBone.localNudge = self.localNudgeChk:Value()
end

function IncrDialog:HandleMessage(msg)
    if msg == self.SHIFTDIVIDE then
        if self.shiftDivideText:IntValue() < 1 then self.shiftDivideText:SetValue(1) end
    else
        
    end
end

-- **************************************************

function AE_TransformBone:DoLayout(moho, layout)

	self.menu = LM.GUI.Menu(MOHO.Localize("/Scripts/Tool/TransformBone/SelectBone=Select Bone"))

	self.popup = LM.GUI.PopupMenu(120, false)
	self.popup:SetMenu(self.menu)
	layout:AddChild(self.popup)
	
	self.smartonlyCheck = LM.GUI.CheckBox("SM", self.SMARTONLYCHECK)
	layout:AddChild(self.smartonlyCheck)
	self.smartonlyCheck:SetToolTip("Show only smartbones in select pop-up")

	layout:AddChild(LM.GUI.StaticText("Pos:"))

	self.textX = LM.GUI.TextControl(0, "00.000", self.CHANGE_T_X, LM.GUI.FIELD_FLOAT, MOHO.Localize("/Scripts/Tool/TransformBone/X=X:"))
	self.textX:SetWheelInc(self.posIncr)
	layout:AddChild(self.textX)

	self.textY = LM.GUI.TextControl(0, "00.000", self.CHANGE_T_Y, LM.GUI.FIELD_FLOAT, MOHO.Localize("/Scripts/Tool/TransformBone/Y=Y:"))
	self.textY:SetWheelInc(self.posIncr)
	layout:AddChild(self.textY)

	self.textL = LM.GUI.TextControl(0, "00.000", self.CHANGE_L, LM.GUI.FIELD_UFLOAT, "L:")
	self.textL:SetWheelInc(0.1)
	layout:AddChild(self.textL)

	self.resetT = LM.GUI.Button("R", self.RESET_T)
	layout:AddChild(self.resetT)
	self.resetT:SetToolTip("Reset translate")

	self.scale = LM.GUI.TextControl(0, "00.000", self.CHANGE_S, LM.GUI.FIELD_FLOAT, MOHO.Localize("/Scripts/Tool/TransformBone/Scale=Scale:"))
	self.scale:SetWheelInc(self.scaleIncr)
	layout:AddChild(self.scale)

	self.resetS = LM.GUI.Button("R", self.RESET_S)
	layout:AddChild(self.resetS)
	self.resetS:SetToolTip("Reset scale")

	self.angle = LM.GUI.TextControl(0, "000.000", self.CHANGE_R, LM.GUI.FIELD_FLOAT, MOHO.Localize("/Scripts/Tool/TransformBone/Angle=Angle:"))
	self.angle:SetWheelInc(self.angleIncr)
	layout:AddChild(self.angle)

	self.resetR = LM.GUI.Button("R", self.RESET_R)
	layout:AddChild(self.resetR)
	self.resetR:SetToolTip("Reset angle")
	
	self.pathCheck = LM.GUI.CheckBox("Path", self.SHOW_PATHS)
	layout:AddChild(self.pathCheck)
	
	self.labelCheck = LM.GUI.CheckBox("Label", self.SHOW_LABELS)
	layout:AddChild(self.labelCheck)	

	if (MOHO.IsMohoPro()) then
		layout:AddChild(LM.GUI.ImageButton("ScriptResources/flip_bone_h", MOHO.Localize("/Scripts/Tool/TransformBone/EndFlip=End Flip"), false, self.FLIP_H, true))
		layout:AddChild(LM.GUI.ImageButton("ScriptResources/flip_bone_v", MOHO.Localize("/Scripts/Tool/TransformBone/SideFlip=Side Flip"), false, self.FLIP_V, true))
	end
	
	self.indModeCheck = LM.GUI.ImageButton("ScriptResources/fixed_handles", "Transform independently", true, self.INDMODE, true)
	layout:AddChild(self.indModeCheck)
	
	self.dlog = IncrDialog:new()
    self.Popup = LM.GUI.PopupDialog('...', false, 0)
    self.Popup:SetDialog(self.dlog)
    layout:AddChild(self.Popup, LM.GUI.ALIGN_LEFT, 0)
    self.Popup:SetToolTip('Set increments')
	
	--[[
	--TODO: fix DrawMe behavior, handle Mouse presses and uncomment
	self.nonBoneCheck = LM.GUI.CheckBox("With non-bone", self.NONEBONE)
	layout:AddChild(self.nonBoneCheck)
	self.nonBoneCheck:SetToolTip("Use with non-bone layers")	
	--]]
end



function AE_TransformBone:UpdateWidgets(moho)

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

	local selID = skel:SelectedBoneID()

	MOHO.BuildBoneMenu(self.menu, skel, self.SELECTITEM, self.DUMMY)
	if not self.smartOnly then
		MOHO.BuildBoneMenu(self.menu, skel, self.SELECTITEM, self.DUMMY)	
	else
		self.menu:RemoveAllItems()
		local smartBoneList = {}
		for b=0, skel:CountBones()-1 do
			local bone = skel:Bone(b)
			if self:IsASmartBone(moho, skel, bone) then
				table.insert(smartBoneList, {ID = b, name = bone:Name()})
			end
		end
		if #smartBoneList > 0 then 
			table.sort(smartBoneList, function(a,b) return string.lower(a.name) < string.lower(b.name) end)
			for k,v in pairs(smartBoneList) do
				self.menu:AddItem(v.name, 0, self.SELECTITEM + v.ID)
			end
		end
	end
	
	
	

	if (selID >= 0) then
		local bone = skel:Bone(selID)
		self.textX:SetValue(bone.fPos.x)
		self.textY:SetValue(bone.fPos.y)
		self.textL:SetValue(bone.fLength)
	else
		self.textX:SetValue("")
		self.textY:SetValue("")
		self.textL:SetValue("")
	end

	if (moho:CountSelectedBones(true) > 0) then
		local selCount = 0
		local scale = 0
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (bone.fSelected) then
				selCount = selCount + 1
				scale = scale + bone.fScale
			end
		end
		self.scale:SetValue(scale / selCount)
	else
		self.scale:SetValue("")
	end

	if (moho:CountSelectedBones(true) > 0) then
		local selCount = 0
		local angle = 0
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (bone.fSelected) then
				selCount = selCount + 1
				angle = angle + bone.fAngle
			end
		end
		self.angle:SetValue(math.deg(angle) / selCount)
	else
		self.angle:SetValue("")
	end

	if (moho.frame == 0) then
		self.textL:Enable(true)
		self.scale:Enable(false)
		self.resetT:Enable(false)
		self.resetS:Enable(false)
		self.resetR:Enable(false)
	else
		self.textL:Enable(false)
		self.scale:Enable(true)
		self.resetT:Enable(true)
		self.resetS:Enable(true)
		self.resetR:Enable(true)
	end

	self.pathCheck:SetValue(self.showPath)
	self.smartonlyCheck:SetValue(self.smartOnly)
	--self.nonBoneCheck:SetValue(self.nonBone)
	self.labelCheck:SetValue(self.showLabel)	
	
	if moho.frame == 0 then 
		self.indMode = false
		self.indModeCheck:Enable(false)
	else 
		self.indModeCheck:Enable(true)
	end
	self.indModeCheck:SetValue(self.indMode)
end

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

	if (msg == self.RESET_T) then
		if (moho:CountSelectedBones(true) > 0) then
			moho.document:PrepUndo(moho.layer, true)
			moho.document:SetDirty()
			for i = 0, skel:CountBones() - 1 do
				local bone = skel:Bone(i)
				if (bone.fSelected) then
					bone.fAnimPos:SetValue(moho.layerFrame, bone.fAnimPos:GetValue(0))
				end
			end
			moho.layer:UpdateCurFrame()
			moho:NewKeyframe(CHANNEL_BONE_T)
			self:UpdateWidgets(moho)
		end
	elseif (msg == self.CHANGE_T_X) then
		self:ChangePosX(moho)
	elseif (msg == self.CHANGE_T_Y) then
		self:ChangePosY(moho)
	elseif (msg == self.CHANGE_L) then
		if (moho:CountSelectedBones(true) > 0) then
			moho.document:PrepUndo(moho.layer, true)
			moho.document:SetDirty()
			for i = 0, skel:CountBones() - 1 do
				local bone = skel:Bone(i)
				if (bone.fSelected) then
					bone.fLength = self.textL:FloatValue()
				end
			end
			moho.layer:UpdateCurFrame()
			moho:NewKeyframe(CHANNEL_BONE)
		end
	elseif (msg == self.RESET_S) then
		if (moho:CountSelectedBones(true) > 0) then
			moho.document:PrepUndo(moho.layer, true)
			moho.document:SetDirty()
			for i = 0, skel:CountBones() - 1 do
				local bone = skel:Bone(i)
				if (bone.fSelected) then
					bone.fAnimScale:SetValue(moho.layerFrame, bone.fAnimScale:GetValue(0))
				end
			end
			moho.layer:UpdateCurFrame()
			moho:NewKeyframe(CHANNEL_BONE_S)
			self:UpdateWidgets(moho)
		end
	elseif (msg == self.CHANGE_S) then
		self:ChangeScale(moho)
	elseif (msg == self.RESET_R) then
		if (moho:CountSelectedBones(true) > 0) then
			moho.document:PrepUndo(moho.layer, true)
			moho.document:SetDirty()
			for i = 0, skel:CountBones() - 1 do
				local bone = skel:Bone(i)
				if (bone.fSelected) then
					bone.fAnimAngle:SetValue(moho.layerFrame, bone.fAnimAngle:GetValue(0))
				end
			end
			moho.layer:UpdateCurFrame()
			moho:NewKeyframe(CHANNEL_BONE)
			self:UpdateWidgets(moho)
		end
	elseif (msg == self.CHANGE_R) then
		self:ChangeAngle(moho)
	elseif (msg == self.SHOW_PATHS) then
		self.showPath = self.pathCheck:Value()
		moho:UpdateUI()
	elseif (msg == self.SHOW_LABELS) then
		self.showLabel = self.labelCheck:Value()
		moho:UpdateUI()		
	elseif (msg == self.SMARTONLYCHECK) then
		self.smartOnly = self.smartonlyCheck:Value()
		moho:UpdateUI()		
	elseif (msg == self.FLIP_H) then
		self:FlipBones(moho, skel, true)
	elseif (msg == self.FLIP_V) then
		self:FlipBones(moho, skel, false)
	elseif (msg >= self.SELECTITEM) then
		for i = 0, skel:CountBones() - 1 do
			skel:Bone(i).fSelected = (i == msg - self.SELECTITEM)
		end
		moho:UpdateUI()
	elseif (msg == self.SETTINGSBUTTON) then
		self.dlog.document = moho.document
		self.dlog.layer = moho.layer
		self.dlog.layerFrame = moho.layerFrame
		self.dlog.skel = skel
		if (skel:SelectedBoneID() >= 0) then
			self.dlog.bone = skel:Bone(skel:SelectedBoneID())
		else
			self.dlog.bone = nil
		end
	elseif (msg == self.NONEBONE) then	
		self.nonBone = self.nonBoneCheck:Value()
	elseif (msg == self.INDMODE) then
		self.indMode = self.indModeCheck:Value()
	end
end

function AE_TransformBone: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)
	for i, layer in AE_Utilities:IterateAllLayers(moho) do
		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

function AE_TransformBone:ChangePosX(moho, increment)
	local pos = self.textX:FloatValue()
	if increment == 1 then 
		pos = self.textX:FloatValue() + self.posIncr
	elseif increment == -1 then
		pos = self.textX:FloatValue() - self.posIncr
	end	
	local skel = moho:Skeleton()
	if (moho:CountSelectedBones(true) > 0) then
		moho.document:PrepUndo(moho.layer, true)
		moho.document:SetDirty()
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (bone.fSelected) then
				bone.fPos.x = pos
				bone.fAnimPos:SetValue(moho.layerFrame, bone.fPos)
			end
		end
		moho.layer:UpdateCurFrame()
		moho:NewKeyframe(CHANNEL_BONE_T)
	end
	self.textX:SetValue(pos)	
end

function AE_TransformBone:ChangePosY(moho, increment)
	local pos = self.textY:FloatValue()
	if increment == 1 then 
		pos = self.textY:FloatValue() + self.posIncr
	elseif increment == -1 then
		pos = self.textY:FloatValue() - self.posIncr
	end	
	local skel = moho:Skeleton()
	if (moho:CountSelectedBones(true) > 0) then
		moho.document:PrepUndo(moho.layer, true)
		moho.document:SetDirty()
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (bone.fSelected) then
				bone.fPos.y = pos
				bone.fAnimPos:SetValue(moho.layerFrame, bone.fPos)
			end
		end
		moho.layer:UpdateCurFrame()
		moho:NewKeyframe(CHANNEL_BONE_T)
	end
	self.textY:SetValue(pos)
end

function AE_TransformBone:ChangeScale(moho, increment)
	local scale = self.scale:FloatValue()
	if increment == 1 then 
		scale = self.scale:FloatValue() + self.scaleIncr
	elseif increment == -1 then
		scale = self.scale:FloatValue() - self.scaleIncr
	end
	local skel = moho:Skeleton()
	if (moho:CountSelectedBones(true) > 0) then
		moho.document:PrepUndo(moho.layer, true)
		moho.document:SetDirty()
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (bone.fSelected) then
				bone.fScale = scale
				bone.fAnimScale:SetValue(moho.layerFrame, bone.fScale)
			end
		end
		moho.layer:UpdateCurFrame()
		moho:NewKeyframe(CHANNEL_BONE_S)
	end
	
	self.scale:SetValue(scale)
end

function AE_TransformBone:ChangeAngle(moho, increment)
	local angle = self.angle:FloatValue()
	if increment == 1 then 
		angle = self.angle:FloatValue() + self.angleIncr
	elseif increment == -1 then
		angle = self.angle:FloatValue() - self.angleIncr
	end
	
	local skel = moho:Skeleton()
	if (moho:CountSelectedBones(true) > 0) then
		moho.document:PrepUndo(moho.layer, true)
		moho.document:SetDirty()
		for i = 0, skel:CountBones() - 1 do
			local bone = skel:Bone(i)
			if (bone.fSelected) then
				bone.fAngle = math.rad(angle)
				bone.fAnimAngle:SetValue(moho.layerFrame, bone.fAngle)
			end
		end
		moho.layer:UpdateCurFrame()
		moho:NewKeyframe(CHANNEL_BONE)
	end
	
	self.angle:SetValue(angle)
end

function AE_TransformBone:KeyIncrements(moho, keyEvent)
	if keyEvent.keyCode == LM.GUI.KEY_UP and keyEvent.shiftKey  and keyEvent.ctrlKey then
		self:ChangePosY(moho, 1)
		return true
	elseif keyEvent.keyCode == LM.GUI.KEY_DOWN and keyEvent.shiftKey and keyEvent.ctrlKey then
		self:ChangePosY(moho, -1)
		return true
	elseif keyEvent.keyCode == LM.GUI.KEY_LEFT and keyEvent.shiftKey and keyEvent.ctrlKey  then
		self:ChangePosX(moho, -1)
		return true
	elseif keyEvent.keyCode == LM.GUI.KEY_RIGHT and keyEvent.shiftKey and keyEvent.ctrlKey  then
		self:ChangePosX(moho, 1)
		return true
	elseif keyEvent.keyCode == LM.GUI.KEY_LEFT and keyEvent.ctrlKey  then
		self:ChangeAngle(moho, 1)
		return true
	elseif keyEvent.keyCode == LM.GUI.KEY_RIGHT and keyEvent.ctrlKey then
		self:ChangeAngle(moho, -1)
		return true		
	elseif keyEvent.keyCode == LM.GUI.KEY_UP and keyEvent.ctrlKey then
		self:ChangeScale(moho, 1)
		return true
	elseif keyEvent.keyCode == LM.GUI.KEY_DOWN and keyEvent.ctrlKey then
		self:ChangeScale(moho, -1)
		return true
	else
		return false
	end
end

function AE_TransformBone:StoreChildTransforms(moho)
	self.storedChildTransforms = {}
	if moho:CountSelectedBones() ~= 1 then return end
	
	--TODO: get time segment for active bone
	local skel = moho:Skeleton()
	local activeBone = skel:Bone(skel:SelectedBoneID())	
	local testChannel = activeBone.fAnimScale
	if (self.mode == 0) then
		testChannel = activeBone.fAnimPos
	elseif (self.mode == 1) then
		testChannel = activeBone.fAnimAngle
	end
	
	--get time segment where bone values changes
	local startKeyID = testChannel:GetClosestKeyID(moho.layerFrame)
	local startFrame = testChannel:GetKeyWhen(startKeyID)
	if startFrame == moho.layerFrame then 
		startKeyID = startKeyID - 1
		startFrame = testChannel:GetKeyWhen(startKeyID)
	end
	if startFrame == 0 then startFrame = 1 end
	local endKeyID = startKeyID + 1
	local endFrame = 0
	if endKeyID < testChannel:CountKeys() then
		endFrame = testChannel:GetKeyWhen(endKeyID)
		if endFrame == moho.layerFrame then
			endKeyID = endKeyID + 1
			if endKeyID < testChannel:CountKeys() then
				endFrame = testChannel:GetKeyWhen(endKeyID)
			else 
				endFrame = 0
			end
		end
	end
	--startFrame = startFrame - moho.layer:TotalTimingOffset()
	--if endFrame ~= 0 then endFrame = endFrame - moho.layer:TotalTimingOffset()
	
	for i, layer in AE_Utilities:IterateAllLayers(moho) do
		if layer:ControllingSkeleton() == skel then
			local localStart = startFrame -- + layer.TotalTimingOffset()
			local localEnd = endFrame -- + layer.TotalTimingOffset()	
			local curFrame = moho.frame -- + layer.TotalTimingOffset
			local layerParentBone = layer:LayerParentBone()
			if layerParentBone == skel:SelectedBoneID() or 
			  	(layerParentBone > -1 and skel:IsAncestorSelected(layerParentBone)) then
				--TODO: get dependent layers
			elseif layerParentBone == -1 and layer:LayerType() == MOHO.LT_VECTOR then
				--get dependent points for active bone
				local mesh = moho:LayerAsVector(layer):Mesh()
				for p = 0, mesh:CountPoints()-1 do
					local point = mesh:Point(p)			
					if point.fParent == skel:SelectedBoneID() or (point.fParent > -1 and skel:IsAncestorSelected(point.fParent)) then
						if endFrame == 0 then localEnd = point.fAnimPos:Duration() end
						--store global point position for current frame						
						local curFrameObj = {["frame"] = curFrame, ["type"] = "point", ["layer"] = layer, ["point"] = point }
						curFrameObj.pos = AE_Utilities:GetPointBoneTransformedPos(moho, layer, point, curFrame)
						local fPosContainer = LM.Vector2:new_local()
						fPosContainer:Set(point.fPos)
						curFrameObj.fPos = fPosContainer
						curFrameObj.channelPos = point.fAnimPos:GetValue(curFrame)
						table.insert(self.storedChildTransforms, curFrameObj)
						--store global point position for every key in segment
						for f = localStart, localEnd do
							if f ~= curFrame and point.fAnimPos:HasKey(f) then
								nextFrameObj = {["frame"] = f, ["type"] = "point",  ["layer"] = layer, ["point"] = point }
								nextFrameObj.pos = AE_Utilities:GetPointBoneTransformedPos(moho, layer, point, f)
								table.insert(self.storedChildTransforms, nextFrameObj)
							end
						end
					end
				end
			end
		end
	end
	
	--TODO: get dependent bones
end

function AE_TransformBone:RestoreChildTransforms(moho, curFrameOnly)
	--print("Restoring child transforms from array of ", #self.storedChildTransforms)
	for k, v in pairs(self.storedChildTransforms) do
		--print(k, " frame ", v.frame)
		if (not curFrameOnly) or (v.frame == moho.layerFrame) then
			--print(tostring(v.point), " to ", v.pos.x, " ", v.pos.y, " at ", v.frame)
			if v.type == "point" then
				if v.fPos then
					local dif = v.point.fPos - v.fPos
					local channelPos = v.point.fAnimPos:GetValue(v.frame)
					v.point.fAnimPos:SetValue(v.frame, channelPos - dif)
				else
					local curPos = AE_Utilities:GetPointBoneTransformedPos(moho, v.layer, v.point, v.frame)
					local dif = v.pos - curPos
					local channelPos = v.point.fAnimPos:GetValue(v.frame)
					v.point.fAnimPos:SetValue(v.frame, channelPos + dif)
					v.layer:UpdateCurFrame()
				end
			--else if v.type == 
			end
		end
	end
	if not curFrameOnly then self.storedChildTransforms = {} end
end

Modified transform bone tool
Listed

Script type: Tool

Uploaded: Apr 30 2021, 06:19

Last modified: May 20 2022, 13:41

Script Version: 6.44

Labels, paths, custom step and so on
Added features:
* display bone names on mouse move even in "Show Curves" mode off (thanks to Heyvern vector font)
* display true paths for any bone, independent or parented, inside active area
* filter select bone menu with smartbones only
* customizable step for shift+mouse bone rotation, and other hotkey steps
* use "Relative keyframes" timeline mode to translate (or rotate) group of independent bones over multiply keys by same distance
* new "Transform independently" mode lets transform bone in any frame or action without transforming its dependent vertices
* + and = keys select all direct children of selected bone; With shift modifier -- select all the tree of children.
* key DELETE also deletes selected bones' smart actions 
* fixed rotation of multiple independent-angled bones
* when rotate muliple selected bones origin is set to nearest picked bone's origin




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