-- Poly  Paint, a tool to paint polygos with a flat color from texture, setting all vertices of a UV face to a single point in texture, giving always a flat color
-- By Denys Almaral - 2022

-- Updates:

-- replace painting using another scene file.
-- replace color tool.
-- now work without disabling Skin modifier! or any modifier that doesn't change Face count.
-- Migrated from Unwrap UVW functions to direct polyOp functions. Faster smooth calls.
-- Paint-edit Multiple objects at the same time. 
-- Remappping textures: Swap old with new, remapping UV, keeping same colors look.
-- UI review
-- 2021: Auto-isert Unwrap UVW modifier. APPLY button to collapse to Unwrap UVW position in stack.
-- 2019: ability to copy paste UV colors coords from Win clipboard 
-- implmented Undo levels
-- added Flood Fill algorithm
-- Added Mirror functionality
-- Full repaint the whole object with selected color 
-- 2018: v1.0 basic functionality 

--- TODO:

-- undo all modifications done in current session.
-- Smart Fills: detect curvature Ambient Occluson, light/shadows.
-- What to do when multiple selected objects have different textures? Limitation or feature?
-- Need more UI room?, experiment with all inside subRollouts. Test inter-Rollout communications.


fn pxPolyPaint_GetSourceDir =
(
	local sourceFile = getThisScriptFilename()
	if sourceFile == undefined or sourceFile == "" then
		undefined
	else
		pathConfig.removePathLeaf sourceFile
)

fn pxPolyPaint_CopyDependency =
(
	local userMacroDir = getDir #userMacros
	local targetFile = userMacroDir + "\\pxtools_PolypaintHelpers.cs"

	local sourceDir = pxPolyPaint_GetSourceDir()
	if sourceDir == undefined then
	(
		format "pX Poly Paint: source file path is unavailable, cannot copy dependency.\n"
		false
	)
	else
	(
		local sourceFile = sourceDir + "\\dotnet_polypaint\\PolypaintHelpers.cs"
		if not doesFileExist sourceFile then
		(
			format "pX Poly Paint: source dependency not found: %\n" sourceFile
			false
		)
		else
		(
			if doesFileExist targetFile do deleteFile targetFile
			messageBox ("pX Poly Paint is copying its dependency to the user macros folder.\n\nFrom:\n" + sourceFile + "\n\nTo:\n" + targetFile) title:"pX Poly Paint"
			copyFile sourceFile targetFile
		)
	)
)

pxPolyPaint_CopyDependency()

macroScript PolyPaint buttonText:"Poly Paint" category:"pX Tools" tooltip:"pX Poly Paint"
(	
	global g_PolyPaint	
	global g_PolyPaint_rf
	if g_PolyPaint != undefined then
	(
		if g_PolyPaint.open then
		(
			destroyDialog g_PolyPaint			
		)		
	)
	
	global PolypaintHelpers = undefined
	function CompileCSharpHelper =
	(
		local userMacroDir = getDir #userMacros
		local targetFile = userMacroDir + "\\pxtools_PolypaintHelpers.cs"
		csharpProvider = dotNetObject "Microsoft.CSharp.CSharpCodeProvider"
		compilerParams = dotNetObject "System.CodeDom.Compiler.CompilerParameters"
		compilerParams.GenerateInMemory = true
		compilerParams.ReferencedAssemblies.Add "System.dll"
		compilerParams.ReferencedAssemblies.Add "System.Drawing.dll"
		compilerResults = csharpProvider.CompileAssemblyFromFile compilerParams #(targetFile)
		
		/* Quick check to catch compilation errors gracefully */
		if compilerResults.Errors.HasErrors then
		(
			for i = 0 to (compilerResults.Errors.Count - 1) do
			(
				print compilerResults.Errors.Item[i].ErrorText
			)
		)
		else
		(
			PolypaintHelpers = compilerResults.CompiledAssembly.CreateInstance "dotnet_polypaint.PolypaintHelpers"
			result = PolypaintHelpers.sum 5 10
			print "Testing compiled csharp"
			print result
		)
	)
	
	CompileCSharpHelper()
	
	
	-------------------------------------------------------------------- roll_polyPaint ------------------------------------ 
	rollout roll_polyPaint "Painter" 
	(

		local bmpSize = 320
		
		imgTag bmpColorPal bitmap:undefined pos:[0,0] style:#bmp_stretch transparent:(color 68 68 68) width:bmpSize height:bmpSize
	
		--label lblInfo "Select object and press Start Painting Tool" pos:[10,10]
		
			checkButton paint3D ">>>      START Paint Tool      <<<" width:(bmpSize - 5) height:25
			
			group "Paint" (
				checkbox chk_AutoStart "Auto-Start ^" across:2 checked:true
				checkbox chkMirror "Mirror Paint" across:2 checked:false
				button btnFullRepaint "Full Repaint" across:2 width:90
				checkbox chkSelectedOnly "Repaint selected faces only" across:2 
				checkButton btnFill "Fill Tool" across:2  width:90 
				spinner spnSpread "Spread" range:[0,10,0] type:#integer across:2
				checkButton btnReplace "Replace Tool" across:2
				button btnUndo ">> UNDO (use this!) <<" across:2
				timer timerFill "" interval:1000 active:false
			)
			group "Current Color" (
				imgTag imgCurrColor  width:100 height:10 across:2
				editText editUVs "UV" Text:"[0,0]" 
				editText editRGBColor "RGB" text:"[0,0,0]" readonly:true across:2
				editText editPixelCoords "xy" Text:"[0,0]" readonly:true 
				button btnUV_copy "Copy" across:3
				button btnUV_paste "Paste" 
				button btnUV_set "Set" 
			)						
			
			progressBar pbar "" value:0
			
			group "Texture remapping & swapping" (
				button btnSwapMap "Remap with -->" across:3 toolTip:"Remap UV based on texture target, keeping same colors as close as possible"				
				mapbutton mapBtnSwapMap "<none>" tooltip:"Select bitmap texture to be replaced with"								
				checkbox chkNoReplace "Without replace" checked:false tooltip:"Do not replace with new bitmap when remapping"
				
				checkbox chkLimitToSection "Limit to Section:" across:2 checked:false toolTip:"Remap limited to the area [x y width heigh]"
				editText edtSectionBox "x y w h" text:"0 0 200 100"
				
				button btnReplaceFromFile "Replace UVs from other file..." tooltip:"Get the UVs paint from same object in different file and replace"
			)			

			
		local paint_enable = #( btnFullRepaint, btnFill, btnReplace, btnUndo, btnUV_set, btnSwapMap)
		local lastTriFace = -1
		local Ctrl_Alt_undo = false
		local lastPolyIndex = -1
		local theObj = undefined
		local lastObj = undefined
		local theObjects = #()
		local mod_states = #()
		local masterObj = undefined
		local mapChannel = 1
		local curr_UV = [0.5,0.5,0]
		local currStrokeNum = 0
		local thePI = thePainterInterface
		local rmIntersect = RayMeshGridIntersect()
		local faceMapUndo = #()
		local undoLevels = #()
		local undoLevelsMax = 5
		local fill_tolerance = 1
		local triToPolyTable = #()
		
		local tempBitmap = bitmap bmpSize bmpSize color:(color 40 40 40)
		local colorBitmap = bitmap 50 50 	
		
		
		local ini_file = (getDir #userMacros) + "\\pxpolypaint.ini"
		local ini_section = "config"	

		
		function set_ini key val = (
			setINISetting ini_file ini_section key (val as string)
		)
		function get_ini key = (
			getINISetting ini_file ini_section key
		)
		
		function eval s = (
			try execute s catch ""
		)

				
		--============================ FUNCTIONS ===============================------------------------
		
		function enable_tools bool = 
		(
			for ui in paint_enable do ui.enabled = bool				
		)
			
		function updateColorBitmap diffMap = 
		(
			copy diffMap.bitmap tempBitmap
			bmpColorPal.bitmap = tempBitmap
			g_currBitmap = tempBitmap
		)
		
		function UVtoColor UV gamma:true =
		(
			local w = tempBitmap.width
			local h = tempBitmap.height			
			(getPixels tempBitmap [ UV.x * w ,  (1-UV.y) * h ] 1 linear:gamma )[1]
		)
		
		function UVgetPixel UV diffMap gamma:false = (
			local w = diffMap.bitmap.width
			local h = diffMap.bitmap.height				
			if UV.x>=1 or UV.y>=1 or UV.x<=0 or UV.y<=0 then
			(
				print "ERROR: pixel coord out of bounds"
				UV.x = 0.5
				UV.y = 0.5
			)
			(getPixels diffMap.bitmap [ UV.x * w ,  (1-UV.y) * h ] 1 linear:gamma )[1]
		)
		
		function findMatDiffuseMap mat = 
		(
			local diffMap = undefined
			if mat != undefined then 
            (
                diffMap = case classof(mat) of
				(
					blend: mat.map1
					Arch___Design__mi: mat.diffuse_Color_Map
					standard: mat.diffuseMap
					VRayMtl: mat.texmap_diffuse
					PhysicalMaterial: mat.base_color_map
					PBRMetalRough: mat.base_color_map
					PBRSpecGloss: mat.base_color_map
					ai_standard_surface: mat.base_color_shader
					default: undefined
				)
                
            )    
            (diffMap)
		)
		
		function replaceDiffuseMap mat diffmap =
		(
			case classof(mat) of
			(
				blend: mat.map1 = diffmap
				Arch___Design__mi: mat.diffuse_Color_Map = diffmap
				standard: mat.diffuseMap = diffmap
				VRayMtl: mat.texmap_diffuse = diffmap
				PhysicalMaterial: mat.base_color_map = diffmap
				PBRMetalRough: mat.base_color_map = diffmap
				PBRSpecGloss: mat.base_color_map = diffmap
				ai_standard_surface: mat.base_color_shader = diffmap
			)
		)
		
        function findDiffuseMap obj =
        (  
            local mat = obj.material
            findMatDiffuseMap mat
        )
		
        -- updates current Color box on interface.
        -- draw pixels over bitmap to show current color 
		function updateCurrColor =
		(	      
			local diffMap = findMatDiffuseMap masterObj.material
            copy diffMap.bitmap tempBitmap --This resize 1st into 2nd
						
			local p = UVtoColor curr_UV 								
			if p != undefined then
			(
				free colorBitmap
				colorBitmap = bitmap 50 50 color:p 
				imgCurrColor.bitmap = colorBitmap
				
				-- draw pixel marker                
				local xx = curr_UV.x * tempBitmap.width
				local yy = (1-curr_UV.y) * tempBitmap.height
                for i = 2 to 3 do 
                (
                    setPixels tempBitmap [xx+1*i, yy] #(color 255 255 255)
                    setPixels tempBitmap [xx-1*i, yy] #(color 255 255 255)
                    setPixels tempBitmap [xx, yy+1*i] #(color 0 0 0)
                    setPixels tempBitmap [xx, yy-1*i] #(color 0 0 0)
                )
				bmpColorPal.bitmap = tempBitmap                
                
			)
            
            --update UV coords on UI
            editUVs.text = [curr_UV.x, curr_UV.y] as string			
			editRGBColor.text = ((UVGetPixel curr_UV diffMap gamma:false) as point3) as string
			local mapx = curr_UV.x * diffMap.bitmap.width;
			local mapy = (1-curr_UV.y) * diffMap.bitmap.height
			editPixelCoords.text = [mapx, mapy] as string;						
		)
		
			
		function clearUndo = 
		(
			-- store it, actually, in undo levels
			--btnUndo.enabled = false
			append undoLevels faceMapUndo
			if undoLevels.count > undoLevelsMax then deleteItem undoLevels 1
			faceMapUndo = #()		
			--print ("undo levels" + (undoLevels.count as string) )
		)
		
		
		--DEPRECATED
		fn __getPolyFaceByTri__ EPolyObj tri =
		(
			-- convert a mesh Face Index to a editable_poly poly index
			-- need clarification: 
			-- will go each face guess its number of tirangles and make the sum.
			-- triangles are in order as polygons. there is a correspondance
			-- MAGIC ALERT!
			local triIndex = 1
			local polyIndex = 0			
			for k = 1 to polyop.getNumFaces EPolyObj while polyIndex == 0 do
			(
				--TODO: avoid this iterations, make a cached table				
				--current face tirangles is equal to the number of vertices - 2.
				local numTriangles = (polyop.getFaceDeg EPolyObj k) - 2
				if numTriangles == undefined then numTriangles = 0
				triIndex = triIndex +  numTriangles
				if (tri < triIndex) then polyIndex = k
				--
				--debug-- print ((index as string) + " | " + (k as string) + " | " + polyIndex as string)
			)
			--return:
			(polyIndex)
		)
		
		function getPolyFaceByTri EPolyObj tri = triToPolyTable[tri]
		
		function BuildTriToPolyTable EPolyObj =
		(
			--
			local list = #()
			local triIndex = 1
			local numTriangles = 0
			for k = 1 to polyop.getNumFaces EPolyObj do
			(
				-- Triangles are ordered in correspondence to polygons.
				-- We just need to count....
				local faceDeg = polyop.getFaceDeg EPolyObj K
				if faceDeg == undefined then 
				( numTriangles = 0 )
					else 
				(numTriangles = (faceDeg) - 2)
				
				for i=1 to numTriangles do
				(
					list[triIndex] = k
					triIndex = triIndex + 1
				)
			)
			--return
			(list)
			-- triToPolyTable[triangleN] = polygonN.
		)
		
		function unwrap_getMapFace obj poly = 
		(			
			local result = #()
			result.count = obj.Unwrap_UVW.numberPointsInFace poly		
			for i=1 to result.count do
			(				
				result[i] = obj.Unwrap_UVW.getVertexIndexFromFace poly i				
			)
			--return
			(result)
		)
		
		function setAllUVto poly UV UndoOn:true =
		(			
			if theObj != lastObj then
			(
				--changed target node
				modPanel.setCurrentObject theObj.modifiers[#Unwrap_UVW]
			)
			local mapFace = unwrap_getMapFace theObj poly					
			
			
			if UndoOn do 
			(
				-- UNDO DATA			
				local faceUV = #()
				faceUV[1] = poly 			
				--saving, only the first vertex					
				faceUV[2] = theObj.Unwrap_UVW.getVertexPosition 0 mapFace[1]
				faceUV[3] = theObj
				
				append faceMapUndo faceUV
			)
			
			--modifiying
			for i=1 to mapFace.Count do
			(
				--with unwrap_modifier
				theObj.Unwrap_UVW.setFaceVertex [ UV.x, UV.y,0] poly i false
				--directly to Editpoly
				--polyop.setMapVert theObj 1 mapFace[i] [UV.x, UV.y, 0]
			)	
			lastObj = theObj
		)
		
		function setFaceUV faceIdx UV undoOn:true Obj:undefined =
		(	
			if Obj == undefined then
			(
				Obj = theObj
			) 
			if UndoOn do 
			(
				-- UNDO DATA			
				local faceUV = #()
				faceUV[1] = faceIdx			
				--saving, only the first vertex					
				faceUV[2] = polyop.getMapVert Obj mapChannel faceIdx
				faceUV[3] = Obj
				
				append faceMapUndo faceUV
			)
			
			--modifiying, 
			--This works because the mapping was prepared with "prepare_for_polyops" function
			-- Map Face Indexes and Map Vert Indixes are paired 1 to 1.  NumMapFaces = NumMapVerts.
			-- Each face vert point to a single map vert.
			polyop.setMapVert Obj mapChannel faceIdx [UV.x, UV.y, 0]			
		)
		
		function putPlyxel faceIdx UV =
		(
			if spnSpread.value == 0 then setFaceUV faceIdx UV 
				else
			(
				local faces = #{faceIdx}
				local verts = #{}
				for j = 1 to spnSpread.value do
				(
					verts = verts + polyOp.getVertsUsingFace theObj faces
					faces = faces + polyOp.getFacesUsingVert theObj verts
				)
				for i in faces do
				(
					setFaceUV i UV
				)
				
			)
			
		)
		
		function unwrap_getPlyxel poly =
		(			
			--local face = polyop.getMapFace theObj 1 poly
			local face = unwrap_getMapFace theObj poly			
			--local vert = polyop.getMapVert theObj 1 face[1]
			local vert = theObj.Unwrap_UVW.getVertexPosition 0 Face[1]
			--return
			(vert)
		)
		
		function getPlyxel faceIdx =
		(				
			--return
			(polyop.getMapVert theObj mapChannel faceIdx)
		)
		
		
		function mirrorPaint =
		(
			local localHit = [0,0,0], localNormal=[0,0,0], worldHit=[0,0,0], worldNormal=[0,0,0]
			thePI.getMirrorHitPointData &localHit &localNormal &worldHit &worldNormal 0
			local p1 = worldHit - worldNormal
			local p2 = worldHit + worldNormal			
			local result = rmIntersect.intersectSegment p2 p1 false			
			if result>0 then
			(				
				local indexedHit = 1
				if result>1 then indexedHit = rmIntersect.getClosestHit()
				local Poly = getPolyFaceByTri theObj (rmIntersect.getHitFace indexedHit) 
								
				putPlyxel Poly curr_UV
			)			
			
		)	

		function SameColor c1 c2  =
		(			
			local result = (c1 != undefined) and (c2 != undefined)
			if result then 
			(
				local d = distance (c1 as point3) (c2 as point3)
				result = d < fill_tolerance			
			)
			result		
		)
		
		
		function fillArea iniPoly UV =
		(
			local faces = #{iniPoly}
			local bkColor = UVtoColor (getPlyxel iniPoly)  --storing the background color
			local fillColor = UVtoColor UV 
			putPlyxel iniPoly UV -- setting the color of the first Plyxel
			update theObj
			
			local paintLimit = 0 -- To avoid an unexpected ininite loop...  
			do
			(
			
				local edges = polyOp.getEdgesUsingFace theObj faces			
				local newFaces = polyOp.getFacesUsingEdge theObj edges
				local removeFaces = #{}
				for i in newFaces do
				(
					local c = UVtoColor (getPlyxel i)  --Check the current color of each face
					if (not ( sameColor c bkColor)) or (sameColor c fillColor)  then -- remove if not == bkColor, or == to fillColor
						( 
							removeFaces = removeFaces + #{i} 
						)
					else (putPlyxel i UV) --else paint				   
				)				
				newFaces = newFaces - removeFaces
				--print ( "filling faces " + ((newFaces as array).count as string) )			
				update theObj
				redrawViews()
				faces = newFaces
				paintLimit = paintLimit + 1
			
			) while ( ((faces as array).count > 0 ) and  (paintLimit < 1000 )  )
		)
		
		function replaceColor iniPoly UV =
		(
			local bkUV = getPlyxel iniPoly
			local bkColor = UVtoColor bkUV --storing the picked color
			for i=1 to polyOp.getNumFaces theObj do
			(
				local c = UVtoColor (getPlyxel i)
				if (sameColor c bkColor) then
				(
					putPlyxel i UV
				)
			)
			update theObj
			redrawViews()
		)
		
		function paintLoop_forward face1 face2 UV alreadyPainted:#{} =
		(
			--TODO: paint direction to the other side too
			local paintLimit = 0
			local paintedFaces = alreadyPainted			
			setFaceUV face1 UV
			setFaceUV face2 UV
			paintedFaces[face1] = true
			paintedFaces[face2] = true
			do 
			(				
				local verts1 = polyop.getFaceVerts theObj face1
				local verts2 = polyop.getFaceVerts theObj face2
				local sharedVerts = (verts1 as bitArray) * (verts2 as bitArray)				
				local face3 = undefined
				if (sharedVerts as array).count == 2 then
				(
					--ok:
					local OppositeVerts  = (verts2 as bitArray) - sharedVerts					
					if (OppositeVerts as array).count == 2 then
					(
						--ok ok:
						nearFaces = polyOp.getFacesUsingVert theObj (OppositeVerts as array)
						local found = -1
						for f in nearFaces do
						(
							--not face2
							if (f != face2) and (not paintedFaces[f]) then
							(
								--has the two verts?
								local verts = polyop.getFaceVerts theObj f
								if ((OppositeVerts * (verts as bitArray)) as array ).count == 2 then
								(
									--OKey!									
									face3 = f								
								)
							)
						)
					)
				)
				if face3 != undefined then
				(					
					setFaceUV face3 UV
					paintedFaces[face3]=true
					--looping step
					face1 = face2
					face2 = face3
				)
				paintLimit += 1 --paranoical stop
			) while (face3 != undefined) and (paintLimit < 1000)			
			--return
			(paintedFaces)
		)
		
		function paintLoop face1 face2 UV = 
		(
			local painted = paintLoop_forward face1 face2 UV
			paintLoop_forward face2 face1 UV alreadyPainted:painted
		)
		
		function do_undo =
		(
			local obj = undefined
			local prevObj = undefined
			print "doing undo thing..."
			print faceMapUndo.count
			for i=faceMapUndo.count to 1 by -1 do
			(				
				local faceUV = faceMapUndo[i]				
				obj = faceUV[3]
				local faceIdx = faceUV[1]
				local UVpoint = faceUV[2]
				--local faceMap = polyop.getMapFace theObj 1 faceUV[1] --get the vertex indices; may have changed				
/* 				if obj != prevObj then modPanel.setCurrentObject obj.modifiers[#Unwrap_UVW]
				local faceMap = unwrap_getMapFace obj faceIdx
				for j=1 to faceMap.count do
				(					
					obj.Unwrap_UVW.setFaceVertex UVpoint faceIdx j false				
				) */
				polyop.setMapVert obj mapChannel faceIdx UVpoint
				prevObj = obj
			)
			for obj in theObjects do update obj
			redrawViews() 
			--more undo levels?
			if undoLevels.count > 0 then
			(
				faceMapUndo = undoLevels[ undoLevels.count ] 
				deleteItem undoLevels undoLevels.count
			) else  
			(
				faceMapUndo = #()
				btnUndo.enabled = false
			)
		)
		
		--EVENT from painter interface
		function startStroke = 
		( 
			--thePI.undoStart() 			
			clearUndo()
			currStrokeNum = currStrokeNum + 1	
			--viewport.SetShowEdgeFaces true	
		)
		
		-- The main thing happens here ------------ PAINT STROKE -------------- ******************************		
		function paintStroke = 
		( 	
			--paintStroke: EVENT from painter interface
			local bary = [0,0,0]
			local faceIndex = 1			
			local hitObj = thePI.getHitNode 0
			
			if hitObj != undefined then
			(	
				theObj = hitObj
				thePI.getHitFaceData &bary &faceIndex theObj 0	
				
				if (faceIndex != lastTriFace) and (bary != [0,0,0]) then
				--do not repeat stuff if we are still in the same face
				-- bary != [0,0,0] dirty trick to detect ourside of node strokes
				(				
					local PolyIndex = (getPolyfaceByTri theObj faceIndex)												
					--print bary	
					local shift=false , ctrl=false, alt=false, pressure=0
					thePI.getHitPressureData  &shift &ctrl &alt &pressure 0
					
					if alt and ctrl then
					(
						Ctrl_Alt_Undo = true
					)
					else if alt then -- PICKING COLOR 
					(
					   curr_UV = getPlyxel polyIndex
					   updateCurrColor()
					) 
					else if (btnFill.checked or ctrl) then
					(
						fillArea polyIndex curr_UV					
					) 
					else if btnReplace.checked then
					(
						replaceColor polyIndex curr_UV
					)
					else if shift then
					(
						--detect face change to paint loop						
						if (polyIndex != lastPolyIndex) and (lastPolyIndex != -1 ) then
						(
							paintLoop lastPolyIndex polyIndex curr_UV
							update theObj
						)
					)
					else -- DRAWING COLOR
					(				
						
							putPlyxel polyIndex curr_UV
						
						if thePI.mirrorEnable == true then mirrorPaint()
						update theObj	
					)
					thePI.clearStroke()
					lastPolyIndex = PolyIndex
				)
				lastTriFace = faceIndex				
			)
		)
		

		
		function endStroke = 
		( 
			lastTriFace = -1		
			lastPolyIndex = -1			
			if faceMapUndo.count>0 then btnUndo.enabled = true
				
			---do undo if Ctrl+Alt+click:
			if Ctrl_Alt_Undo then
			(				
				print "Ctrl Alt Click = UNDO"
				if undoLevels.count > 0 then
				(
					faceMapUndo = undoLevels[ undoLevels.count ] 
					deleteItem undoLevels undoLevels.count
				)
				do_undo()
				Ctrl_Alt_Undo = false				
			)
			--viewport.SetShowEdgeFaces false
		)
		
		function cancelStroke = 
		( 
			--thePI.undoCancel()
			if faceMapUndo.count>0 then btnUndo.enabled = true
			print "cancel stroke?? please!"
			lastTriFace = -1
			lastPolyIndex = -1			
			do_undo()
		)
		
		function systemEndPaintSession = 
		( 
			paint3d.checked = false
			lastTriFace = -1
		)
		
		local mpanel_showEndResult = false
		local mpanel_subObjectLevel = 0
		local mpanel_currObject = undefined
		
		function save_modpanel_state =
		(
			max modify mode
			mpanel_showEndResult = showEndResult
			mpanel_subObjectLevel = subObjectLevel
			mpanel_currObject = modPanel.getCurrentObject()
			if mpanel_currObject == theObj.baseObject then
			(
				mpanel_currObject = "base"
			)
		)
		
		function restore_modpanel_state =
		(
			
			--max modify mode			
			if mpanel_currObject == "base" then
			(
				mpanel_currObject = masterObj.baseObject
			)
			modPanel.setCurrentObject mpanel_currObject			
			subObjectLevel = mpanel_subObjectLevel
			showEndResult = mpanel_showEndResult
		)
		
		--DEPRECATED 
		function prepareObj_for_unwrap obj = 
		(			
			if (obj.modifiers[#Unwrap_UVW] == undefined)  then
			(				
				if chkAutoUV.checked then
				(
					addModifier obj (Unwrap_UVW()) before:(obj.modifiers.count)
				)
			)
			
			if  (obj.modifiers[#Unwrap_UVW] != undefined) then
			(
				
				modPanel.setCurrentObject obj.modifiers[#Unwrap_UVW]
				obj.modifiers[#unwrap_uvw].unwrap5.setShowMapSeams off
			) else
			(
				messageBox "Unwrap_UVW not found in stack"
			)
		)
		
		--better than Unwrap_UVW
		function prepare_for_polyops obj = 
		(
			local meshFaces = polyop.getNumFaces obj
			if not (polyop.getMapSupport obj mapChannel) then
			(
				polyop.setMapSupport obj mapChannel true
			)
			local mapFaces = polyop.getNumMapFaces obj mapChannel
			if mapFaces != meshFaces then
			(
				--in case faces difer
				polyop.setNumMapFaces obj mapChannel meshFaces keep:true
				mapFaces = meshFaces
			)
			local mapVerts = polyOp.getNumMapVerts obj mapChannel
			
			-- rebuild map verts for our convenience, one unique vert for each face, each face vert point to that vert
			-- keep one old vert
			local vertsCopy = #()
			vertsCopy.count = mapFaces
			local mapFaceSizes = #()
			for i = 1 to mapFaces do 
			(
				local mapFace = polyop.getMapFace obj mapChannel i
				mapFaceSizes[i] = mapFace.count
				
					if mapFace[1] > mapVerts then 
						(vertsCopy[i] = polyop.getMapVert obj mapChannel 1)
					else 
						(vertsCopy[i] = polyop.getMapVert obj mapChannel mapFace[1])
			
				
			)
			polyop.setNumMapVerts obj mapChannel mapFaces keep:true
			for i = 1 to mapFaces do
			(
				polyop.setMapVert obj mapChannel i vertsCopy[i]
				local newFace = #()
				newFace.count = mapFaceSizes[i]
				for j = 1 to newFace.count do
				(
					newFace[j] = i
				)
				polyop.setMapFace obj mapChannel i newFace
			)
			
			--cache triangle to polygon conversion 
			triToPolyTable = BuildTriToPolyTable obj
			
			update obj
		)
		
		function prepareObj obj = 
		(
			--modPanel.setCurrentObject  obj.baseObject			
			prepare_for_polyops obj
		)
			
		
			
		
		function startPainting3D =
		(
			if  thePI.InPaintMode() or theObj == undefined then 
			(
				thePI.endPaintSession()
				paint3D.checked = false
				paint3D.caption = ">>>      START Paint Tool      <<<"
			) else
			(				
				updateCurrColor()				
				--lblInfo.visible = false
				paint3D.checked = true
				thePI.pointGatherEnable = false
				thePI.initializeNodes 0 theObjects
				thePI.offMeshHitType =2
				thePI.minSize = 0.1
		        thePI.maxSize = 2
				thePI.drawring = true
				thePI.drawTrace = false
				thePI.drawNormal = true
				thePI.normalScale = 5
				thePI.pressureEnable = true
				thePI.mirrorEnable = chkMirror.checked
				if chkMirror.checked then
				(
					rmIntersect.free
					rmIntersect.nodeList = #()
					rmIntersect.initialize 10
					rmIntersect.addNode( theObj )
					rmIntersect.buildGrid()
				)
				thePI.mirrorAxis = 1
				thePI.scriptFunctions startStroke paintStroke endStroke cancelStroke SystemEndPaintSession
				thePI.startPaintSession()
				paint3D.caption = "      END Paint Session      "
			)			
		)
		
		function endPainting3D =
		(
			thePI.endPaintSession()
			if paint3D.checked then paint3D.checked = false
			paint3D.caption = ">>>      START Paint Tool      <<<"
		)
		
		function findClosestColor thisColor fromBitmap = 
		(
			thisColor = thisColor as point3
			found = [0,0]
			otherColor = (color 0 0 0)
			min_dist = distance [0,0,0] [255,255,255]
			for y = 0 to (fromBitmap.height-1) do
			(
				local pixels = getPixels fromBitmap [0, y] fromBitmap.width linear:false
				for x = 0 to (pixels.count-1) do
				(
					local dist = distance (pixels[x+1] as point3) thisColor
					if dist<min_dist then 
					(
						min_dist = dist
						found = [x, y]
						otherColor = pixels[x+1]
					)
				)
			)
			--return
			( #( found, otherColor) )
		)
		
		--converts bitmap to 2D table -- DEPRECATED
		function __getPixelTable fromBitmap =
		(
			pixelTable = #()
			pixelTable.count = fromBitmap.height
			for y = 0 to (fromBitmap.height-1) do
			(
				pixelTable[y+1] = getPixels fromBitmap [0, y] fromBitmap.width linear:false
			)
			--return
			(pixelTable)
		)
		
		--convert bitmap to RLE compressed data
		function getPixelData__old fromBitmap  =
		(			
			local c = color 0 0 0
			local last_c = color 0 0 -1			
			local pixelData = #()			

			for y = 0 to (fromBitmap.height-1) do
			(
				local pixels = getPixels fromBitmap [0,y] fromBitmap.width linear:false
				for x = 0 to (fromBitmap.width-1) do
				(
					c = pixels[x+1]
					if c != last_c then 
					(
						append pixelData #(c, [x,y])
					)
					last_c = c
				)
			)
			--return
			(pixelData)
		)
		
		function getPixelData fromBitmap box2limited:undefined =
		(
			local c = color 0 0 0
			local last_c = color 0 0 -1
			local pixelData = #()
		
			local area
			if box2limited == undefined then
			(
				area = box2 0 0 fromBitmap.width fromBitmap.height
			)
			else
			(
				area = box2 box2limited.x box2limited.y box2limited.w box2limited.h
				format "Limiting area...\n"
			)
		
			local startX = area.x
			local startY = area.y
			local endX = area.x + area.w - 1
			local endY = area.y + area.h - 1
		
			for y = startY to endY do
			(
				local pixels = getPixels fromBitmap [startX, y] (endX - startX + 1) linear:false
				for x = 0 to (pixels.count-1) do
				(
					c = pixels[x+1]
					if c != last_c then
					(
						append pixelData #(c, [x + startX, y])
					)
					last_c = c
				)
			)
			(pixelData)
		)
		
		-- Find looking down and right diagonal pixels with the same color than
		-- pos [x,y], then return the one in the middle. 
		function improvePixelPos OldPos fromBitmap =
		(
			local c = (getPixels fromBitmap OldPos 1 linear:false)[1]
			local sameColor = true
			local count = 0
			local pos = [OldPos.x, OldPos.y]
			local maxCount = fromBitmap.width - pos.x - 1
			local heightLimit = fromBitmap.height - pos.y - 1
			if heightLimit < maxCount then maxCount = heightLimit
			if maxCount > 20 then maxCount = 20
			if maxCount > 2 then 
			(
				while sameColor do
				(
					count = count + 1
					--print count 
					c2 = (getPixels fromBitmap (pos+[count, count]) 1 linear:false)[1]
					sameColor = (c == c2) and (count < maxCount)
				)
				if count>2 then 
				(
					pos.x = floor (pos.x + count / 2.0)
					pos.y = floor (pos.y + count / 2.0)
				)
			)
			--return
			(pos)
		)
		
		--This one use what getPixelTable returns -- DEPRECATED
		function findClosestColorTable thisColor fromPixelTable = 
		(
			thisColor = thisColor as point3
			local found = [0,0]
			local otherColor = (color 0 0 0)
			local min_dist = distance [0,0,0] [255,255,255]
			local dist = 0.0
			for y = 0 to (fromPixelTable.count-1) do
			(
				local pixels = fromPixelTable[y+1]
				for x = 0 to (pixels.count-1) do
				(
					dist = distance (pixels[x+1] as point3) thisColor
					if dist<min_dist then 
					(
						min_dist = dist
						found = [x, y]
						otherColor = pixels[x+1]
						if dist < 0.01 then return #( found, otherColor)
					)
				)
			)
			--return
			( #( found, otherColor) )
		)
		
       function findClosestColorData thisColor fromPixelData = 
		(
			thisColor = thisColor as point3
			local found = [0,0]
			local otherColor = (color 0 0 0)
			local min_dist = distance [0,0,0] [255,255,255]
			local dist = 0.0
			local pixelData
			for pixelData in fromPixelData do
			(					
					dist = distance (pixelData[1] as point3) thisColor
					if dist<min_dist then 
					(
						min_dist = dist
						found = pixelData[2] --position [x,y]
						otherColor = pixelData[1] --color rgb
						if dist < 0.01 then return #( found, otherColor)
					)
				
			)
			--return
			( #( found, otherColor) )
		)
		
		function getLimitedSection =
		(
			local result = undefined
			if chkLimitToSection.checked == false then
				result = undefined
			else
			(
				local vals = filterString edtSectionBox.text " "
				if vals.count == 4 then
				(
					local x = try(vals[1] as float) catch(result = "ERROR")
					local y = try(vals[2] as float) catch(result = "ERROR")
					local w = try(vals[3] as float) catch(result = "ERROR")
					local h = try(vals[4] as float) catch(result = "ERROR")
					if x != undefined and y != undefined and w != undefined and h != undefined then
						result = box2 x y w h
					else
					(
						format "Invalid limit numbers: %\n" edtSectionBox.text
						result = "ERROR"
					)
				)
				else
				(
					format "Invalid limit numbers: %\n" edtSectionBox.text
					result = "ERROR"
				)
			)
			(result)
		)
		
		--- big deal, find closest similar colors from incoming map to remap all UVs
		function remapTo newBitmap = 
		(
			if theObj != undefined then
			(				
				local t1 = timeStamp()
				--local pixelTable = getPixelTable newBitmap				
				local pixelData 
				local findFunc 
				local diffMap = findDiffuseMap theObj
				
				local isBoxLimited = getLimitedSection()
				if isBoxLimited == "ERROR" then
				(
					messageBox "Invalid Section input x,y,w,h"	
					return()
				)
				
				pixelData = getPixelData newBitmap box2limited:isBoxLimited
				findFunc = findClosestColorData
				
				format "Preparing table, elapsed time: %\n" (timeStamp()-t1)
				t1 = timeStamp()
				for poly=1 to (polyOp.getNumFaces theObj) do
				(					
					local uv = (getPlyxel poly)
					local c1 = UVgetPixel uv diffMap gamma:false					
					local result = findFunc c1 pixelData
					local better = improvePixelPos result[1] newBitmap
					result[1] = better 
					local new_uv = [0,0]
					new_uv.x = (( result[1].x + 0.5 ) as float) / (newBitmap.width)
					new_uv.y = 1.0 - ( (( result[1].y + 0.5 ) as float) / (newBitmap.height) )					
					setFaceUV poly new_uv undoOn:false
					pbar.value =  (100*poly/(polyOp.getNumFaces theObj))
				)
				format "Elapsed time: %\n" (timeStamp() - t1)
				update theObj				
				pbar.value = 0
			)
		)
		
		--return array with Enabled/Disabled modifiers states.
		--while disabling all
		function saveMod_states obj disableThem:true =
		(
			local states = #()
			states.count = obj.modifiers.count
			for i=1 to states.count do
			(
				states[i] = obj.modifiers[i].enabled
				if disableThem then obj.modifiers[i].enabled = false
			)
			--return
			(states)
		)
	
		
		------------================== UI EVENTS HANDLERS =====================--------------------------------
		
		on paint3D changed state do 
		( 			
			if state == true then
			(
				local isOk = 0
				local objList = #()
				theObjects = #() --reseting
				mod_states = #()
				
				if ($==undefined) then 
				(
					messageBox "Object not Selected" title:"Warning" beep:true					
				) else
				(
					if (classof $) == objectSet then
					(
						objList = $ as array
					) else
					(
						objList = #($)
					)
					for Obj in objList do
					(
						theObj = obj
						if ((classof theObj.baseObject) == Editable_poly) then
						(
							--TODO: Maybe .baseObject is better than PolyMeshObject test
							if theObj.material != undefined then
							(
								local diffMap = findDiffuseMap(theObj)								
								
								if diffMap != undefined then
								( 						
									if isOk == 0 then
									(
										masterObj = theObj
										save_modpanel_state()
										updateColorBitmap(diffMap)
										updateCurrColor()
									)									
									append theObjects theObj
									local disable_mods = ( (polyop.getNumFaces theObj) != (polyOp.getNumFaces theObj.baseObject) )
									append mod_states (saveMod_states theObj disableThem:disable_mods) 
									prepareObj theObj
									isOk = isOk + 1
									
								) else messageBox  "Need material applied with diffuseMap bitmap"
							) else messageBox "Need material applied with diffuseMap bitmap"
						) else 
						( 
							messageBox "Object need to be  Editable Poly object." title:"Warning" beep:true						
						)
					)
				)
				
				if isOk > 0 then
				(
					paint3D.checked = true
					theObj = masterObj
					startPainting3D()
				) else paint3D.checked = false
				
			) else
			(
				endPainting3D()
				if false then --chkAutoUV_end.checked then
				(	
					for obj in theObjects do
					(
						local mods = Obj.modifiers
						local m = mods[#Unwrap_UVW]
						local idx = findItem mods m				
						print "collapsing"
						maxOps.CollapseNodeTo Obj idx off
					)
				)
				--restoring stacks and modify panel
				for i=1 to theObjects.count do
				(						
					local states = mod_states[i]
					if states != undefined then
					(
						for j = 1 to states.count do
						(
							theObjects[i].modifiers[j].enabled = states[j]
						)
					)
				)
				if theObjects.count==1 then restore_modpanel_state()
			)
			enable_tools (paint3D.checked == true)
		)
		

		on bmpColorPal lbuttondown pos flags do
		(
			if (pos.y <= bmpSize) then
			(
			   --	get the position of mouse clicks over the Texture Image and convert it to UV coordinates.
				curr_UV.x =  pos.x / bmpSize
				curr_UV.y = 1 - ( pos.y / bmpSize )				
				
				updateCurrColor()
				
				
			)			
		)
		
		on roll_polyPaint close do			
		(				
			print "Good bye!"
			paint3D.checked = false
			paint3D.changed false
			
			--ini settings
			local p = g_polyPaint_rf.pos
			print p
			set_ini "dialogPos" (p as string)
			set_ini "curr_uv" (curr_UV as string)
			set_ini "auto_start" (chk_AutoStart.checked as string)
		)
		
		on roll_polyPaint open do
		(
			-- loading ini settings
			local s = get_ini "dialogPos"
			local p = eval s
			print s
			if (classof p ) == point2 then
			(
				g_polyPaint_rf.pos =  p
			)
			s = get_ini "curr_uv"
			p = eval s
			if (classof p) == point3 then
			(
				curr_UV = p
				--updateCurrColor()
			)
			s = get_ini "auto_start"
			if s != "" then
			(
				p = eval s
				chk_AutoStart.checked = if (classof p) == BooleanClass then p else false
			)
			
			--
			enable_tools false
			
			-- auto start painting?
			if chk_AutoStart.checked then
			(
				print "auto starting..."
				paint3D.checked = true
				paint3D.changed true
			)
		)
		
		on btnFullRepaint pressed do
		(
			if theObj != undefined then
			(
				if chkSelectedOnly.checked then
				(
					-- TODO: make this work without unwrap and get selected faces from polyop
					--local sp = theObj.unwrap_UVW.getSelectedPolygons()
					local sp = polyop.getFaceSelection theObj
					for i in sp do 
					(
						putPlyxel i curr_UV
					)
				) else
				(
					for i=1 to (polyOp.getNumFaces theObj) do
					(
						putPlyxel i curr_UV
					)
				)
				update theObj
				redrawViews()
			)
		)
		
		on chkMirror changed state do
		(
			    thePI.mirrorEnable = chkMirror.checked
				if chkMirror.checked then
				(
					rmIntersect.free
					rmIntersect.nodeList = #()
					rmIntersect.initialize 10
					rmIntersect.addNode( theObj )
					rmIntersect.buildGrid()
				)
		)		
		
		
		on btnUndo pressed do
		(		
			do_undo()
		)
        
        on btnUV_copy pressed do
        (
           local ss = (stringStream "") 
           print spnU.value to:ss
           print spnV.value to:ss
           setclipboardText (ss as string)
        )
		
        on btnUV_paste pressed do
        (
            local s = getclipboardText()
            if s!=undefined then
            (
                local ss = (stringStream s)
                seek ss 0
                spnU.value = (readValue ss)
                spnV.value = (readValue ss)                
            )
        )
        
        on btnUV_set pressed do             
        (
            curr_UV.x =  spnU.value
			curr_UV.y = spnV.value
			updateCurrColor() 
        )
		
		on btnFill changed state do 
		(
			if state then
			(
				btnReplace.checked = false
				thePainterInterface.drawNormal = true
				thePainterInterface.drawRing = false
			) else
			(
				thePainterInterface.drawRing = true
				thePainterInterface.drawNormal = true
			)
		)
		
		on btnReplace changed state do
		(
			if state then
			(
				btnFill.checked = false				
			) 
		)
		
		on timerFill tick do
		(
			setSysCur #select
		)
		
		on mapBtnSwapMap picked textmap do 
		( 
			if (classof mapBtnSwapMap.map) ==Bitmaptexture then 
			(
					mapBtnSwapMap.text = textmap.name 
			) else
			(
				messageBox "Map needs to be Bitmaptexture class"				
				mapBtnSwapMap.map = undefined
				
			)
		)
		
		on btnSwapMap pressed do
		(
			if theObj != undefined then
			(
				local map = mapBtnSwapmap.map
				if map != undefined then
				(
					if (classof map) ==Bitmaptexture then 
					(
						remapTo map.bitmap
						if not chkNoReplace.checked then 	replaceDiffuseMap theObj.material map
						redrawViews()
						updateColorBitmap map						
					) else
					(
						messageBox "Map needs to be Bitmaptexture class"
					)
				)
			)
		)
		
		on btnReplaceFromFile pressed do
		(
			if $selection.count != 1 then 
			(
				messageBox "ERROR: An object need to be selected."
			) else 
			(
				local objName = $.name
				print objName
				if queryBox "Current scene NEEDS to be SAVED. Proceed?" then
				(					
					otherFile = getOpenFileName caption:("Open a scene with "+objName+" object...") types:"Max(*.max)|*.max"
					if otherFile != undefined then 
					(
						
						print otherFile 
						--saving current scene
						currMaxFileName = maxFilePath + maxFileName
						if Paint3D.checked == true then Paint3D.changed false
						
						saveMaxFile currMaxFileName quiet:true						
						loadMaxFile otherFile quiet:true
						local obj = getNodeByName objName
						if obj == undefined then
						(
							fn poly_filter obj = (classof obj.baseObject)==Editable_Poly
							obj = SelectByName title:(objName+" not found, select manually") single:true
						)
						if obj == undefined then 
						(
							messageBox ("Object name no found: "+objName)							
						) else 
						(
							select obj
							Paint3D.changed true
							-- get the paint data here
							local uvData = undefined
							
							print "numfaces from"
							uvData = #()
							uvData.count = polyOp.getNumMapFaces obj mapChannel
							print uvData.count
							for i=1 to uvData.count do
							(
								uvData[i] = polyOp.getMapVert obj mapChannel i
							)
						
							
							Paint3D.changed false
							--back to original file							
							loadMaxFile currMaxFileName quiet:true
							obj = getNodeByName objName
							select obj
							Paint3D.changed true
							if $selection.count == 1 then 							(
								
								-- set copied the paint data here
								if (theObj != undefined) and (uvData != undefined) then 
								(
									print "numfaces to"									
									local maxData = uvData.count
									local currFaces = polyOp.getNumMapFaces obj mapChannel
									if currFaces != uvData.count then print "WARNING: Dest Faces Count != Source Faces Count"
									if currFaces < maxData then maxData = currFaces										
									print currFaces
									for i=1 to maxData do
									(
										polyOp.setMapVert obj mapChannel i uvData[i]
									)
									update theObj
									redrawViews()
								)
							)
						)
					)
				)
			)
		)
	)
	
	rollout roll_help "Help" 
	(
			
		label l1 "Alt + Click = Pick color from mesh" align:#left
		label l2 "Shift + Click drag across edge = Paint loop" align:#left
		label l3 "Ctrl + Click = Fill Tool" align:#left
		label l4 "Ctrl + Alt + Click = Undo last" align:#left
		hyperLink hl_web "pX Poly Paint by Denys Almaral" align:#right address:@"https://denysalmaral.com/2018/09/free-polygon-painting-script-and-lowpoly-owl.html" align:#center
	)

	rollout roll_tools "Tools"
	(
		group "Convert To LowPoly Style" (
			button btnConvert "Convert Selected"
		)
		
		fn extractBitmapPixels bmp =
		(
			local pixelArray = #()
			pixelArray.count = bmp.width * bmp.height * 4
			local byteIdx = 1

			for y = 0 to (bmp.height - 1) do
			(
				local rowPixels = getPixels bmp [0, y] bmp.width linear:false
				for x = 1 to rowPixels.count do
				(
					local c = rowPixels[x]
					pixelArray[byteIdx] = c.r as integer
					pixelArray[byteIdx + 1] = c.g as integer
					pixelArray[byteIdx + 2] = c.b as integer
					pixelArray[byteIdx + 3] = 255
					byteIdx += 4
				)
			)

			local TByteArray = dotNetClass "System.Byte[]"
			dotNet.valueToDotNetObject pixelArray TByteArray
		)

		fn buildBitmapFromPixels width height csPixels =
		(
			width = width as integer
			height = height as integer
			local resultBmp = bitmap width height color:(color 0 0 0) gamma:2.2

			for y = 0 to (height - 1) do
			(
				local rowPixels = #()
				rowPixels.count = width

				for x = 0 to (width - 1) do
				(
					local byteIdx = ((y * width + x) * 4) + 1
					rowPixels[x + 1] = color (csPixels[byteIdx] as integer) (csPixels[byteIdx + 1] as integer) (csPixels[byteIdx + 2] as integer)
				)

				setPixels resultBmp [0, y] rowPixels
			)

			resultBmp
		)

		fn displayResultBitmap =
		(
			local resultPixels = PolypaintHelpers.GetResultBitmapPixels()
			local resultWidth = PolypaintHelpers.GetResultBitmapWidth()
			local resultHeight = PolypaintHelpers.GetResultBitmapHeight()
			if resultPixels != undefined and resultWidth > 0 and resultHeight > 0 then
			(
				local resultBmp = buildBitmapFromPixels resultWidth resultHeight resultPixels
				display resultBmp
				resultBmp
			)
			else
			(
				print "No C# result bitmap generated."
				undefined
			)
		)

		fn getResultTextureSavePath sourceBmp =
		(
			local sourceFilename = sourceBmp.filename
			local suggestedFilename = sysInfo.tempDir + "polypaint_lowpoly.png"

			if sourceFilename != undefined and sourceFilename != "" then
			(
				local sourcePath = getFilenamePath sourceFilename
				local sourceName = getFilenameFile sourceFilename
				if sourcePath != "" then
				(
					suggestedFilename = sourcePath + sourceName + "_lowpoly.png"
				)
				else
				(
					suggestedFilename = sourceName + "_lowpoly.png"
				)
			)

			getSaveFileName caption:"Save Generated LowPoly Texture" filename:suggestedFilename types:"PNG (*.png)|*.png|Bitmap (*.bmp)|*.bmp|All Files (*.*)|*.*|"
		)

		fn saveResultBitmap resultBmp sourceBmp =
		(
			local savePath = getResultTextureSavePath sourceBmp
			if savePath != undefined then
			(
				resultBmp.filename = savePath
				if (save resultBmp gamma:2.2) then
				(
					savePath
				)
				else
				(
					print ("Failed to save generated texture: " + savePath)
					undefined
				)
			)
			else
			(
				undefined
			)
		)

		fn applyPolypaintUVMapping obj mapChannel resultFaceUVs =
		(
			local faceCount = polyop.getNumFaces obj
			if not (polyop.getMapSupport obj mapChannel) then
			(
				polyop.setMapSupport obj mapChannel true
			)

			polyop.setNumMapFaces obj mapChannel faceCount keep:false
			polyop.setNumMapVerts obj mapChannel faceCount keep:false

			for faceIdx = 1 to faceCount do
			(
				local uvOffset = ((faceIdx - 1) * 2) + 1
				local u = resultFaceUVs[uvOffset]
				local v = resultFaceUVs[uvOffset + 1]
				polyop.setMapVert obj mapChannel faceIdx [u, v, 0]

				local faceDeg = polyop.getFaceDeg obj faceIdx
				local newFace = #()
				newFace.count = faceDeg
				for i = 1 to faceDeg do
				(
					newFace[i] = faceIdx
				)
				polyop.setMapFace obj mapChannel faceIdx newFace
			)

			update obj
		)

		fn applyGeneratedTexture obj mapChannel texturePath resultFaceUVs =
		(
			if not (doesFileExist texturePath) then
			(
				print ("Failed to load generated texture: " + texturePath)
				return false
			)

			local newMap = Bitmaptexture filename:texturePath
			newMap.name = (getFilenameFile texturePath + "_lowpoly")

			local newMat = copy obj.material
			newMat.name = (obj.material.name + "_LowPoly")
			roll_PolyPaint.replaceDiffuseMap newMat newMap

			with undo "PolyPaint LowPoly Convert" on
			(
				applyPolypaintUVMapping obj mapChannel resultFaceUVs
				obj.material = newMat
			)

			true
		)

		fn extractUVData obj mapChannel =
		(
			local numUVs = polyOp.getNumMapVerts obj mapChannel
			local uvArray = #()
			uvArray.count = numUVs * 2 
			
			local uvIdx = 1
			for i = 1 to numUVs do
			(
				local uv = polyOp.getMapVert obj mapChannel i
				uvArray[uvIdx] = uv.x
				uvArray[uvIdx+1] = uv.y
				uvIdx += 2
			)
			
			local TFloatArray = dotNetClass "System.Single[]"
			local csUVs = dotNet.valueToDotNetObject uvArray TFloatArray
			format "Extracted % UV coordinates to System.Single[]\n" numUVs
			csUVs
		)
		
		fn extractMapFaces obj mapChannel =
		(
			local numFaces = polyOp.getNumFaces obj
			local mapFacesArray = #()
			mapFacesArray.count = numFaces
			
			for i = 1 to numFaces do
			(
				local faceMapVerts = polyOp.getMapFace obj mapChannel i
				local csFace = #()
				csFace.count = faceMapVerts.count
				for j = 1 to faceMapVerts.count do
				(
					-- Crucial: Subtract 1 to convert MaxScript's 1-based index to C#'s 0-based index
					csFace[j] = faceMapVerts[j] - 1
				)
				mapFacesArray[i] = csFace
			)
			
			local TIntArrayArray = dotNetClass "System.Int32[][]"
			local csMapFaces = dotNet.valueToDotNetObject mapFacesArray TIntArrayArray
			format "Extracted % Map Faces to System.Int32[][]\n" numFaces
			csMapFaces
		)

		
		on btnConvert pressed do
		(
			local sel = selection as array
			if sel.count > 0 then
			(
				local obj = sel[1]
				if obj.material != undefined then
				(
					local diffMap = roll_PolyPaint.findMatDiffuseMap obj.material
					if diffMap != undefined and diffMap.bitmap != undefined then
					(
						local bmp = diffMap.bitmap
						local mapChannel = 1
						
						if polyOp.getMapSupport obj mapChannel then
						(
							if PolypaintHelpers != undefined then
							(
								if queryBox "This will modify the selected object's UV mapping and assign a new material with a generated diffuse texture.\n\nContinue?" title:"PolyPaint LowPoly Convert" beep:true then
								(
									local totalStart = timeStamp()
									local prepStart = timeStamp()
									local csPixels = extractBitmapPixels bmp
									local csUVs = extractUVData obj mapChannel
									local csMapFaces = extractMapFaces obj mapChannel
									local prepMs = timeStamp() - prepStart

									local csCallStart = timeStamp()
									local csResult = PolypaintHelpers.ProcessLowPolyPixels bmp.width bmp.height csPixels csUVs csMapFaces
									local csCallMs = timeStamp() - csCallStart
									print csResult

									local receiveStart = timeStamp()
									local resultBmp = displayResultBitmap()
									local resultFaceUVs = PolypaintHelpers.GetResultFaceUVs()
									local receiveMs = timeStamp() - receiveStart

									format "[PolyPaint LowPoly Timing] MaxScript prepare data: % ms\n" prepMs
									format "[PolyPaint LowPoly Timing] C# call total: % ms\n" csCallMs
									format "[PolyPaint LowPoly Timing] MaxScript receive/build result: % ms\n" receiveMs

									if resultBmp != undefined and resultFaceUVs != undefined then
									(
										local saveStart = timeStamp()
										local texturePath = saveResultBitmap resultBmp bmp
										local saveMs = timeStamp() - saveStart
										format "[PolyPaint LowPoly Timing] Save result texture: % ms\n" saveMs

										if texturePath != undefined then
										(
											local applyStart = timeStamp()
											applyGeneratedTexture obj mapChannel texturePath resultFaceUVs
											local applyMs = timeStamp() - applyStart
											format "[PolyPaint LowPoly Timing] Apply UV/material: % ms\n" applyMs
										)
										else
										(
											print "LowPoly conversion canceled: no texture save path selected."
										)
									)

									format "[PolyPaint LowPoly Timing] Total after confirmation: % ms\n" (timeStamp() - totalStart)
								)
							)
							else
							(
								print "Error: PolypaintHelpers C# assembly is not compiled or loaded."
							)
						)
						else
						(
							print "The selected object has no UV mapping on channel 1."
						)
					)
					else
					(
						print "The selected object's material does not have a valid diffuse bitmap."
					)
				)
				else
				(
					print "The selected object has no material."
				)
			)
			else
			(
				print "Please select an object first."
			)
		)
	)

	g_polypaint_rf = newRolloutFloater "pX Poly Paint" (roll_PolyPaint.bmpSize + 10) (roll_PolyPaint.bmpSize + 500 )
	addRollout roll_PolyPaint g_polypaint_rf
	addRollout roll_help g_polyPaint_rf rolledup:true
	addRollout roll_tools g_polyPaint_rf rolledup:false
	
	g_polyPaint = roll_PolyPaint

	--roll_help.height = 100
)
