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

ScriptName = "AM_Paint_Bucket"

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

AM_Paint_Bucket = {}

AM_Paint_Bucket.BASE_STR = 2740

function AM_Paint_Bucket:Name()
	return "Paint Bucket +"
end

function AM_Paint_Bucket:Version()
	return "1"
end

function AM_Paint_Bucket:IsBeginnerScript()
	return true
end

function AM_Paint_Bucket:Description()
	return "Click to fill a closed region with color (hold <alt> to pick fill color, <shift> to pick stroke color, <cmd/ctrl> to copy shape properties, <cmd/ctrl> + <alt> to pick fill color and fill a closed region with it)"
end

function AM_Paint_Bucket:BeginnerDescription()
	return "Click inside a closed shape to fill it with the current combination of colors and styles. Only completely closed shapes can be filled. The Fill and Stroke color can be changed in the Style window (Window > Style)."
end

function AM_Paint_Bucket:BeginnerDisabledDescription()
	return "You need to create a vector drawing first using the 'Add Point', 'Freehand' or 'Draw Shape' tool before you can use this tool. You need to be on Frame 0 in the Timeline to use this tool."
end

function AM_Paint_Bucket:Creator()
	return "Aleksei Maletin"
end

function AM_Paint_Bucket:UILabel()
	return("Paint Bucket +")
end

function AM_Paint_Bucket:LoadPrefs(prefs)
	self.creationMode = prefs:GetInt("AM_Paint_Bucket.creationMode", 2)
end

function AM_Paint_Bucket:SavePrefs(prefs)
	prefs:SetInt("AM_Paint_Bucket.creationMode", self.creationMode)
end

function AM_Paint_Bucket:ResetPrefs()
	self.creationMode = 2
end

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

AM_Paint_Bucket.prepUndo = false
AM_Paint_Bucket.extendEdges = false
AM_Paint_Bucket.dragMode = 0

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

function AM_Paint_Bucket:IsEnabled(moho)
	if (moho:DisableDrawingTools()) then
		return false
	end
	if (moho.drawingLayer:CurrentAction() ~= "") then
		return false
	end
	if (moho:CountEdges() > 0) then
		return true
	end
	return false
end

function AM_Paint_Bucket:IsRelevant(moho)
	if (moho:DisableDrawingTools()) then
		return false
	end
	local mesh = moho:DrawingMesh()
	if (mesh == nil) then
		return false
	end
	return true
end

function AM_Paint_Bucket:OnMouseDown(moho, mouseEvent)
	self.color = mouseEvent.view:SampleColor(mouseEvent.pt)
	self.mouseVec = mouseEvent.vec

	if (mouseEvent.ctrlKey) and not (mouseEvent.altKey) and not (mouseEvent.shiftKey) then -- control
		self.dragMode = 0 -- pick style
		self:OnMouseMoved(moho, mouseEvent)
	elseif (mouseEvent.altKey) and not (mouseEvent.ctrlKey) and not (mouseEvent.shiftKey) then -- alt
		self.dragMode = 1 -- pick fill color
		self:OnMouseMoved(moho, mouseEvent)
	elseif (mouseEvent.shiftKey) and not (mouseEvent.ctrlKey) and not (mouseEvent.altKey) then -- shift
		self.dragMode = 2 -- pick stroke color
		self:OnMouseMoved(moho, mouseEvent)
	elseif (mouseEvent.ctrlKey) and (mouseEvent.altKey) and not (mouseEvent.shiftKey) then -- control + alt
		self.dragMode = 3 -- pick fill color and create shape
		self:OnMouseMoved(moho, mouseEvent)
	elseif (mouseEvent.shiftKey) and (mouseEvent.ctrlKey) and not (mouseEvent.altKey) then -- control + shift
		self.dragMode = 4 -- pick stroke color and creaete shape
		self:OnMouseMoved(moho, mouseEvent)
	elseif (mouseEvent.shiftKey) and (mouseEvent.altKey) and not (mouseEvent.ctrlKey) then -- alt + shift
		self.dragMode = 6 -- do nothing	
	else
		self.dragMode = 5 -- just create shape with current settings
	end

end

function AM_Paint_Bucket:OnMouseMoved(moho, mouseEvent)

	self.color = mouseEvent.view:SampleColor(mouseEvent.pt)

	if self.dragMode == 0 then
		local shape = mouseEvent.view:PickGlobalShape(mouseEvent.pt)
		if (shape ~= nil) then
			moho:PickStyleProperties(shape)
		else
			moho:PickStyleProperties(nil)
		end
	elseif (self.dragMode>=1 and self.dragMode<=4) then
		mouseEvent.view:DrawMe()
	else

	end
	
end

function AM_Paint_Bucket:OnMouseUp(moho, mouseEvent)
	self.mouseVec = mouseEvent.vec
	self.color = mouseEvent.view:SampleColor(mouseEvent.pt)

	if self.dragMode == 0 then -- pick style


	elseif self.dragMode == 1 or self.dragMode == 3 then -- pick fill color
		self.color = mouseEvent.view:SampleColor(mouseEvent.pt)
		local style = moho:CurrentEditStyle()
		style.fFillCol:SetValue(moho.drawingLayerFrame, self.color)
	elseif self.dragMode == 2 or self.dragMode == 4 then -- pick stroke color
		self.color = mouseEvent.view:SampleColor(mouseEvent.pt)
		local style = moho:CurrentEditStyle()
		style.fLineCol:SetValue(moho.drawingLayerFrame, self.color)
	end

	if self.dragMode == 3 or self.dragMode == 5 then
		moho.document:PrepUndo(moho.drawingLayer)
		moho.document:SetDirty()

		if (mouseEvent.altKey) then
			local style = moho:CurrentEditStyle()
			local col = mouseEvent.view:SampleColor(mouseEvent.pt)
			style.fFillCol:SetValue(moho.drawingLayerFrame, col)
		end
		local extension = 0
		if (self.extendEdges) then
			extension = 20
		end
		local mesh = moho:DrawingMesh()
		mesh:SelectNone()
		local curveID = -1
		local segID = -1
		curveID, segID = self:SelectEdge(mesh, mouseEvent)
		if (curveID >= 0) then
			local curve = mesh:Curve(curveID)
			if (not curve.fClosed) then
				mesh:SelectConnected()
			end
		else
			mouseEvent.view:FloodSelect(mouseEvent.pt, 1, true, extension)
			self:Weld(moho, mesh)
			mouseEvent.view:FloodSelect(mouseEvent.pt, 1, true, extension)
		end
	
		if (moho:CountSelectedEdges(true) > 0) then
			local mesh = moho:DrawingMesh()
			local sourcePoints = {}
			local shapeID = -1
			if (shapeID < 0) then
				local fillable = false
				-- first try to create a fill no matter what to see if it's valid
				local existingShapeID = mouseEvent.view:PickShape(mouseEvent.pt) -- see if there is already a shape at the clicked location
				local fillCol = nil
				local lineCol = nil

				if (existingShapeID >= 0) then -- try to create a fill normally
					if (self.creationMode == 0) then -- fill only
						shapeID = moho:CreateShape(true, false, 0, true, false, false, true)
					elseif (self.creationMode == 1) then -- stroke only
						shapeID = moho:CreateShape(true, false, 0, true, true, true, false)
					else -- both fill and stroke
						shapeID = moho:CreateShape(true, false, 0)
					end
				else -- try to create a fill behind the neighboring shapes
					shapeID = moho:CreateShape(true, true, 0)
				end
				if (shapeID >= 0) then
					fillable = true
				end
				if (existingShapeID >= 0 and existingShape == shapeID) then
					-- modifying an existing shape, don't change fill/stroke status
				else
					if (shapeID >= 0 and self.creationMode == 0) then
						local shape = mesh:Shape(shapeID)
						shape.fHasOutline = false
					end
					if (shapeID >= 0 and self.creationMode == 1) then
						local shape = mesh:Shape(shapeID)
						shape.fHasFill = false
					end
				end
				if (shapeID < 0 and curveID >= 0) then
					shapeID = moho:CreateShape(false, false, 0) -- try to create an outline
					if (shapeID >= 0) then
						local shape = mesh:Shape(shapeID)
						if (self.creationMode == 0) then
							shape.fHasOutline = false
						elseif (self.creationMode == 1) then
							shape.fHasFill = false
						end
					end
				end

				if (shapeID < 0) then -- create a shape by tracing the flood fill

					local existingShapeID = mouseEvent.view:PickShape(mouseEvent.pt)
					mouseEvent.view:FloodSelect(mouseEvent.pt, 1, true, extension)
					mesh:SelectConnected()

					self:Weld(moho, mesh)
					mouseEvent.view:FloodSelect(mouseEvent.pt, 1, true, extension)
					local frame = moho.drawingLayerFrame

					for a=0, mesh:CountCurves()-1 do
						local curve = mesh:Curve(a)
						for i=0, curve:CountPoints()-1 do
							local point = curve:Point(i)
							local curvature = curve:Curvature(i):GetValue(frame)
							local handle1 = curve:GetControlHandle(i, frame, true)
							local handle2 = curve:GetControlHandle(i, frame, false)
							table.insert(sourcePoints, {point = point, curvature = curvature, handle1 = handle1, handle2 = handle2, ptID = i, curve = curve})
						end
					end

					self:Split(moho, mesh)
					mouseEvent.view:FloodSelect(mouseEvent.pt, 1, true, extension)	


					if (self.creationMode == 0) then -- fill only
						shapeID = moho:CreateShape(true, false, 0, true, false, false, true)
					elseif (self.creationMode == 1) then -- stroke only
						shapeID = moho:CreateShape(true, false, 0, true, true, true, false)
					else -- both fill and stroke
						shapeID = moho:CreateShape(true, false, 0)
					end
					if (shapeID >= 0) then
	
						local shape = mesh:Shape(shapeID)
						if (self.creationMode == 0) then
							shape.fHasOutline = false;
						elseif (self.creationMode == 1) then
							shape.fHasFill = false;
						end
						if (existingShapeID >= 0) then
							mesh:PlaceShapeAbove(shapeID, existingShapeID)
						else
							mesh:LowerShape(shapeID, true)
						end
					end

					mesh:SelectNone()

					for i, point in ipairs(sourcePoints) do
						point['point'].fSelected = true
					end

					mesh:SelectInverse()
					MOHO.DeleteSelectedPoints(mesh)

					for i, point in ipairs(sourcePoints) do
						point['curve']:SetCurvature(point['ptID'], point['curvature'], frame)
						point['curve']:SetControlHandle(point['ptID'], point['handle1'], frame, true, false)
						point['curve']:SetControlHandle(point['ptID'], point['handle2'], frame, false, false)
					end
				end

				if (shapeID >= 0) then
					moho:NewKeyframe(CHANNEL_FILL)
					moho:NewKeyframe(CHANNEL_LINE)
					moho:UpdateSelectedChannels()
					moho:UpdateUI()
				end
			else
				for i = 0, mesh:CountShapes() - 1 do
					mesh:Shape(i).fSelected = false
				end
				mesh:Shape(shapeID).fSelected = true
				mesh:Shape(shapeID):CopyStyleProperties(moho:NewShapeProperties())
				moho:UpdateSelectedChannels()
				moho:UpdateUI()
			end

		end
	end

	self.dragMode = 0
	
end

function AM_Paint_Bucket:Weld(moho, mesh)
	for i = 0, mesh:CountCurves() - 1 do
		local curve = mesh:Curve(i)
		if (curve:IsSelected()) then
			local awr = LM_TransformPoints.autoWeldRadius
			LM_TransformPoints.autoWeldRadius = 3
			MOHO.WeldFullCurve(moho, moho.drawingLayer, mesh, i, moho.view)
			LM_TransformPoints.autoWeldRadius = awr
		end
	end
end

function AM_Paint_Bucket:Split(moho, mesh)

	local points = {}

	for i=0, mesh:CountPoints()-1 do


		local point = mesh:Point(i)

		if point.fSelected and point:CountCurves()>1 then
			table.insert(points, point)
		end

		for a=0, moho:CountSelectedCurves()-1 do
			local curve = mesh:Curve(a)

		end
		point.fSelected = false

	end

	for i, point in ipairs(points) do
		point.fSelected = true
	end

	MOHO:SplitSelectedSegments(mesh, 1, moho.drawingLayerFrame)

	mesh:SelectNone()

end

function AM_Paint_Bucket:OnKeyDown(moho, keyEvent)
	LM_SelectPoints:OnKeyDown(moho, keyEvent)
	
end


function AM_Paint_Bucket:DrawMe(moho, view, mouseEvent)
	if (self.dragMode>=1 and self.dragMode<=4) then
		local zoom = moho.view:Graphics():ViewZoom()
		local g = view:Graphics()
		g:Push()
		g:SetSmoothing(true)	
		local matrix = LM.Matrix:new_local()
		moho.drawingLayer:GetFullTransform(moho.frame, matrix, moho.document)
		matrix:Transform(self.mouseVec)
		g:SetColor(128, 128, 128)
		g:SetPenWidth(10)
		g:FrameCirclePixelRadius(self.mouseVec, 74)
		g:SetColor(self.color.r, self.color.g, self.color.b, 255)
		g:SetPenWidth(20)
		g:FrameCirclePixelRadius(self.mouseVec, 64)
		g:Pop()
	end
end

function AM_Paint_Bucket:SelectEdge(mesh, mouseEvent)
	local curveID = -1
	local segID = -1
	curveID, segID = mouseEvent.view:PickEdge(mouseEvent.pt, curveID, segID)
	if (curveID >= 0 and segID >= 0) then -- an edge was clicked on
		self.selMode = SELMODE_EDGE
		local curve = mesh:Curve(curveID)

		-- new method - select the points in the curve that was clicked on
		if (mouseEvent.shiftKey) then
			local isCurveSelected = true
			for i = 0, curve:CountPoints() - 1 do
				if (not curve:Point(i).fSelected) then
					isCurveSelected = false
					break
				end
			end
			for i = 0, curve:CountPoints() - 1 do
				local point = curve:Point(i)
				if ((not isCurveSelected) or (point:CountCurves() < 2)) then
					point.fSelected = not isCurveSelected
				end
			end
		else
			for i = 0, curve:CountPoints() - 1 do
				local point = curve:Point(i)
				point.fSelected = true
			end
		end
	end
	return curveID, segID
end

function AM_Paint_Bucket:AutoWeld(moho, selCurveID)
	-- returns true if a weld occurred, otherwise false
	local mesh = moho:DrawingMesh()
	if (mesh == nil) then
		return false
	end

	if (not self.prepUndo) then
		self.prepUndo = true
		moho.document:PrepUndo(moho.drawingLayer)
		moho.document:SetDirty()
	end

	local didWeld = false
	local m = LM.Matrix:new_local()
	local testVec1 = LM.Vector2:new_local()
	local testVec2 = LM.Vector2:new_local()
	local p1 = LM.Point:new_local()
	local p2 = LM.Point:new_local()
	local dist = 0

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

	-- First see if we can close an open curve. That's the cleanest auto-weld possible.
	local curve = mesh:Curve(selCurveID)
	if (not curve.fClosed and curve:CountPoints() > 2) then
		local pt1 = curve:Point(0)
		local pt2 = curve:Point(curve:CountPoints() - 1)
		testVec1:Set(pt1.fPos)
		testVec2:Set(pt2.fPos)
		m:Transform(testVec1)
		m:Transform(testVec2)
		moho.view:Graphics():WorldToScreen(testVec1, p1)
		moho.view:Graphics():WorldToScreen(testVec2, p2)
		p1.x = p1.x - p2.x
		p1.y = p1.y - p2.y
		dist = (p1.x * p1.x) + (p1.y * p1.y)
		if (dist < LM_TransformPoints.autoWeldRadius * LM_TransformPoints.autoWeldRadius) then -- found one close enough to weld
			if ((mesh:PointID(pt1) ~= mesh:PointID(pt2)) and mesh:WeldPoints(mesh:PointID(pt1), mesh:PointID(pt2), 0)) then
				return true
			end
		end
	end

	-- Next, try welding curve endpoints
	local doAgain = true
	while doAgain do
		doAgain = false
		for i = 0, mesh:CountPoints() - 1 do
			local pt1 = mesh:Point(i)
			if (pt1.fSelected and pt1:IsEndpoint() and (not pt1.fHidden)) then
				for j = 0, mesh:CountPoints() - 1 do
					if (j ~= i) then
						local pt2 = mesh:Point(j)
						if (pt2:IsEndpoint() and (not pt2.fHidden)) then
							testVec1:Set(pt1.fPos)
							testVec2:Set(pt2.fPos)
							m:Transform(testVec1)
							m:Transform(testVec2)
							moho.view:Graphics():WorldToScreen(testVec1, p1)
							moho.view:Graphics():WorldToScreen(testVec2, p2)
							p1.x = p1.x - p2.x
							p1.y = p1.y - p2.y
							dist = (p1.x * p1.x) + (p1.y * p1.y)
							if (dist < LM_TransformPoints.autoWeldRadius * LM_TransformPoints.autoWeldRadius) then -- found one close enough to weld
								if ((mesh:PointID(pt1) ~= mesh:PointID(pt2)) and mesh:WeldPoints(mesh:PointID(pt1), mesh:PointID(pt2), 0)) then
									didWeld = true
									doAgain = true
									break
								end
							end
						end
					end
					if (doAgain) then
						break
					end
				end
			end
			if (doAgain) then
				break
			end
		end
	end


	-- Finally, try welding dead-end points to the middle of a nearby curve.
	local curveID = -1
	local segID = -1
	local doAgain = true

	while doAgain do
		doAgain = false
		for i = 0, mesh:CountPoints() - 1 do
			local pt1 = mesh:Point(i)
			if (pt1.fSelected and pt1:IsEndpoint()) then
				local id2 = mesh:ClosestPoint(pt1.fPos, i)
				if (id2 >= 0) then
					local pt2 = mesh:Point(id2)
		
					testVec1:Set(pt1.fPos)
					testVec2:Set(pt2.fPos)
					m:Transform(testVec1)
					m:Transform(testVec2)
					moho.view:Graphics():WorldToScreen(testVec1, p1)
					moho.view:Graphics():WorldToScreen(testVec2, p2)
					p2.x = p1.x - p2.x
					p2.y = p1.y - p2.y
					dist = (p2.x * p2.x) + (p2.y * p2.y)
		
					local pickWidth = 3
					while (pickWidth < LM_TransformPoints.autoWeldRadius) do
						curveID, segID = pt1:GetEndpointEdge(curveID, segID)
						curveID, segID = moho.view:PickEdge(p1, curveID, segID, pickWidth)
						if (curveID >= 0) then -- add a point in the middle of some curve
							if ((not mesh:Curve(curveID):IsPointOnSegment(i, segID)) and
								(not mesh:Curve(curveID):IsPointOnSegment(i, segID - 1)) and
								(not mesh:Curve(curveID):IsPointOnSegment(i, segID - 2)) and
								(not mesh:Curve(curveID):IsPointOnSegment(i, segID + 1)) and
								(not mesh:Curve(curveID):IsPointOnSegment(i, segID + 2)) and
								(i ~= mesh:CountPoints() - 1)) then -- don't weld the point back on itself
								local curve = mesh:Curve(curveID)
								mesh:AddPoint(curve:ClosestPointOnSegment(segID, pt1.fPos, true, true), curveID, segID, 0)
								mesh:WeldPoints(i, mesh:CountPoints() - 1, 0)
								didWeld = true
								doAgain = true
								break
							end
						end
						pickWidth = pickWidth + 3
					end
	
				end
			end
			if (doAgain) then
				break
			end
		end
	end

	return didWeld
end

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

AM_Paint_Bucket.FILL = MOHO.MSG_BASE
AM_Paint_Bucket.OUTLINE = MOHO.MSG_BASE + 1
AM_Paint_Bucket.FILLOUTLINE = MOHO.MSG_BASE + 2
AM_Paint_Bucket.CHANGE_SIMPLIFY = MOHO.MSG_BASE + 3
AM_Paint_Bucket.CLOSEGAPS = MOHO.MSG_BASE + 4
AM_Paint_Bucket.creationMode = 2 -- 0:fill, 1:outline, 2:both
AM_Paint_Bucket.DELETE = MOHO.MSG_BASE + 5

function AM_Paint_Bucket:DoLayout(moho, layout)
	self.fillRadio = LM.GUI.RadioButton("Fill", self.FILL)
	layout:AddChild(self.fillRadio)

	self.outlineRadio = LM.GUI.RadioButton("Stroke", self.OUTLINE)
	layout:AddChild(self.outlineRadio)

	self.fillOutlineRadio = LM.GUI.RadioButton("Both", self.FILLOUTLINE)
	layout:AddChild(self.fillOutlineRadio)

	layout:AddChild(LM.GUI.Button('Delete unused points', self.DELETE))
end

function AM_Paint_Bucket:UpdateWidgets(moho)
	self.fillRadio:SetValue(self.creationMode == 0)
	self.outlineRadio:SetValue(self.creationMode == 1)
	self.fillOutlineRadio:SetValue(self.creationMode == 2)
end

function AM_Paint_Bucket:HandleMessage(moho, view, msg)
	if (msg == self.FILL) then
		self.creationMode = 0
	elseif (msg == self.OUTLINE) then
		self.creationMode = 1
	elseif (msg == self.FILLOUTLINE) then
		self.creationMode = 2
	elseif msg == self.DELETE then
		self:DeleteUnusedPoints(moho)
	end
end

function AM_Paint_Bucket:DeleteUnusedPoints(moho)
	local mesh = moho:Mesh()
	mesh:SelectNone()
	for i=0, mesh:CountPoints()-1 do
		local point = mesh:Point(i)
		for a=0, mesh:CountShapes()-1 do
			local shape = mesh:Shape(a)
			if shape:ContainsPoint(i) then
				point.fSelected = true
			end
		end
	end
	mesh:SelectInverse()
	moho.document:PrepUndo(moho.drawingLayer)
	moho.document:SetDirty()
	MOHO.DeleteSelectedPoints(mesh)
	moho:UpdateUI()
end

Icon
Paint Bucket +
Listed

Script type: Tool

Uploaded: Mar 16 2024, 19:30

Last modified: Mar 17 2024, 07:53

Script Version: 1

Modified Paint Bucket + Eyedropper
Click to fill a closed region with color. Hold <alt> to pick fill color, <shift> to pick stroke color, <cmd/ctrl> to copy shape properties, <cmd/ctrl> + <alt> to pick fill color and fill a closed region with it.

Press "Delete unused points" to remove points that are not used in shapes.

You can support the author at the link



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