-- **************************************************
-- 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.42"
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 self:StoreChildTransforms(moho) 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
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()
			if (moho:CountSelectedBones(true) < 2) then
				origin:Set(0, 0)
				if (moho.frame == 0) then
					bone.fRestMatrix:Transform(origin)
				else
					bone.fMovedMatrix:Transform(origin)
				end
			else
				origin = moho.layer:Origin()
			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
	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
	--print("Storing child transforms")
	
	--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			
			if layer:LayerParentBone() == skel:SelectedBoneID() then
				--TODO: get dependent layers
			elseif layer: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() 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)
						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
				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)
			--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: Oct 27 2021, 06:07

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


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