luboslenco 1 an în urmă
părinte
comite
ee67b50c0f
100 a modificat fișierele cu 11097 adăugiri și 5971 ștergeri
  1. 0 10
      armorforge/Sources/Manifest.hx
  2. 9 0
      armorforge/Sources/Manifest.ts
  3. 54 67
      armorforge/Sources/TabObjects.ts
  4. 0 5
      armorforge/Sources/import.hx
  5. 1 0
      armorforge/project.js
  6. 118 0
      armorlab/Sources/MakeMaterial.ts
  7. 22 23
      armorlab/Sources/MakeMesh.ts
  8. 47 51
      armorlab/Sources/MakePaint.ts
  9. 0 10
      armorlab/Sources/Manifest.hx
  10. 9 0
      armorlab/Sources/Manifest.ts
  11. 35 0
      armorlab/Sources/NodesBrush.ts
  12. 75 84
      armorlab/Sources/RenderPathPaint.ts
  13. 30 40
      armorlab/Sources/UINodesExt.ts
  14. 0 124
      armorlab/Sources/arm/MakeMaterial.hx
  15. 0 39
      armorlab/Sources/arm/NodesBrush.hx
  16. 0 228
      armorlab/Sources/arm/nodes/InpaintNode.hx
  17. 0 249
      armorlab/Sources/arm/nodes/PhotoToPBRNode.hx
  18. 0 69
      armorlab/Sources/arm/nodes/RGBNode.hx
  19. 0 478
      armorlab/Sources/arm/nodes/TextToPhotoNode.hx
  20. 0 132
      armorlab/Sources/arm/nodes/VarianceNode.hx
  21. 0 5
      armorlab/Sources/import.hx
  22. 15 20
      armorlab/Sources/nodes/BrushOutputNode.ts
  23. 13 20
      armorlab/Sources/nodes/ImageTextureNode.ts
  24. 218 0
      armorlab/Sources/nodes/InpaintNode.ts
  25. 241 0
      armorlab/Sources/nodes/PhotoToPBRNode.ts
  26. 61 0
      armorlab/Sources/nodes/RGBNode.ts
  27. 469 0
      armorlab/Sources/nodes/TextToPhotoNode.ts
  28. 33 41
      armorlab/Sources/nodes/TilingNode.ts
  29. 51 59
      armorlab/Sources/nodes/UpscaleNode.ts
  30. 122 0
      armorlab/Sources/nodes/VarianceNode.ts
  31. 1 0
      armorlab/project.js
  32. 32 38
      armorpaint/Sources/ImportFolder.ts
  33. 45 46
      armorpaint/Sources/MakeBake.ts
  34. 15 16
      armorpaint/Sources/MakeBlur.ts
  35. 15 16
      armorpaint/Sources/MakeBrush.ts
  36. 35 0
      armorpaint/Sources/MakeClone.ts
  37. 6 7
      armorpaint/Sources/MakeColorIdPicker.ts
  38. 17 18
      armorpaint/Sources/MakeDiscard.ts
  39. 488 0
      armorpaint/Sources/MakeMaterial.ts
  40. 80 81
      armorpaint/Sources/MakeMesh.ts
  41. 41 45
      armorpaint/Sources/MakeMeshPreview.ts
  42. 14 21
      armorpaint/Sources/MakeNodePreview.ts
  43. 105 106
      armorpaint/Sources/MakePaint.ts
  44. 12 13
      armorpaint/Sources/MakeParticle.ts
  45. 10 11
      armorpaint/Sources/MakeTexcoord.ts
  46. 0 10
      armorpaint/Sources/Manifest.hx
  47. 9 0
      armorpaint/Sources/Manifest.ts
  48. 43 0
      armorpaint/Sources/NodesBrush.ts
  49. 953 0
      armorpaint/Sources/RenderPathPaint.ts
  50. 167 0
      armorpaint/Sources/RenderPathPreview.ts
  51. 27 0
      armorpaint/Sources/SlotBrush.ts
  52. 16 0
      armorpaint/Sources/SlotFont.ts
  53. 685 0
      armorpaint/Sources/SlotLayer.ts
  54. 65 0
      armorpaint/Sources/SlotMaterial.ts
  55. 242 251
      armorpaint/Sources/TabLayers.ts
  56. 0 36
      armorpaint/Sources/arm/MakeClone.hx
  57. 0 498
      armorpaint/Sources/arm/MakeMaterial.hx
  58. 0 39
      armorpaint/Sources/arm/NodesBrush.hx
  59. 0 968
      armorpaint/Sources/arm/RenderPathPaint.hx
  60. 0 170
      armorpaint/Sources/arm/RenderPathPreview.hx
  61. 0 34
      armorpaint/Sources/arm/SlotBrush.hx
  62. 0 19
      armorpaint/Sources/arm/SlotFont.hx
  63. 0 690
      armorpaint/Sources/arm/SlotLayer.hx
  64. 0 73
      armorpaint/Sources/arm/SlotMaterial.hx
  65. 0 176
      armorpaint/Sources/arm/nodes/InputNode.hx
  66. 0 5
      armorpaint/Sources/import.hx
  67. 47 52
      armorpaint/Sources/nodes/BrushOutputNode.ts
  68. 167 0
      armorpaint/Sources/nodes/InputNode.ts
  69. 10 17
      armorpaint/Sources/nodes/TEX_IMAGE.ts
  70. 3 2
      armorpaint/project.js
  71. 98 0
      armorsculpt/Sources/ExportObj.ts
  72. 38 46
      armorsculpt/Sources/ImportMesh.ts
  73. 7 8
      armorsculpt/Sources/MakeBrush.ts
  74. 91 99
      armorsculpt/Sources/MakeMaterial.ts
  75. 38 39
      armorsculpt/Sources/MakeMesh.ts
  76. 41 45
      armorsculpt/Sources/MakeMeshPreview.ts
  77. 10 14
      armorsculpt/Sources/MakeSculpt.ts
  78. 0 10
      armorsculpt/Sources/Manifest.hx
  79. 9 0
      armorsculpt/Sources/Manifest.ts
  80. 81 88
      armorsculpt/Sources/TabLayers.ts
  81. 0 102
      armorsculpt/Sources/arm/ExportObj.hx
  82. 0 9
      armorsculpt/Sources/import.hx
  83. 32 39
      armorsculpt/Sources/nodes/BrushOutputNode.ts
  84. 1 0
      armorsculpt/project.js
  85. 0 22
      base/Assets/licenses/license_heapsfbx.md
  86. 192 0
      base/Sources/Args.ts
  87. 2287 0
      base/Sources/Base.ts
  88. 471 0
      base/Sources/BoxExport.ts
  89. 234 238
      base/Sources/BoxPreferences.ts
  90. 251 0
      base/Sources/BoxProjects.ts
  91. 248 0
      base/Sources/Camera.ts
  92. 320 0
      base/Sources/Config.ts
  93. 69 0
      base/Sources/ConfigFormat.ts
  94. 79 0
      base/Sources/Console.ts
  95. 441 0
      base/Sources/Context.ts
  96. 306 0
      base/Sources/ContextFormat.ts
  97. 290 0
      base/Sources/Enums.ts
  98. 464 0
      base/Sources/ExportArm.ts
  99. 17 0
      base/Sources/ExportGpl.ts
  100. 9 0
      base/Sources/ExportMesh.ts

+ 0 - 10
armorforge/Sources/Manifest.hx

@@ -1,10 +0,0 @@
-package ;
-
-class Manifest {
-
-	public static inline var title = "ArmorForge";
-	public static inline var version = "0.1";
-	public static inline var url = "https://armorforge.org";
-	public static inline var url_android = "";
-	public static inline var url_ios = "";
-}

+ 9 - 0
armorforge/Sources/Manifest.ts

@@ -0,0 +1,9 @@
+
+class Manifest {
+
+	static title = "ArmorForge";
+	static version = "0.1";
+	static url = "https://armorforge.org";
+	static url_android = "";
+	static url_ios = "";
+}

+ 54 - 67
armorforge/Sources/arm/TabObjects.hx → armorforge/Sources/TabObjects.ts

@@ -1,33 +1,20 @@
-package arm;
-
-import haxe.Json;
-import zui.Zui;
-import iron.Vec4;
-import iron.Object;
-import iron.Scene;
-import iron.SceneFormat;
-import iron.Data;
-import iron.MaterialData;
-import iron.MeshObject;
-import iron.LightObject;
-import iron.CameraObject;
 
 class TabObjects {
 
-	static var materialId = 0;
+	static materialId = 0;
 
-	static function roundfp(f: Float, precision = 2): Float {
-		f *= std.Math.pow(10, precision);
-		return std.Math.round(f) / std.Math.pow(10, precision);
+	static roundfp = (f: f32, precision = 2): f32 => {
+		f *= Math.pow(10, precision);
+		return Math.round(f) / Math.pow(10, precision);
 	}
 
-	public static function draw(htab: Handle) {
-		var ui = UIBase.inst.ui;
+	static draw = (htab: Handle) => {
+		let ui = UIBase.inst.ui;
 		if (ui.tab(htab, tr("Objects"))) {
 			ui.beginSticky();
 			ui.row([1 / 4]);
 			if (ui.button("Import")) {
-				Project.importMesh(false, function() {
+				Project.importMesh(false, () => {
 					Project.paintObjects.pop().setParent(null);
 				});
 			}
@@ -37,13 +24,13 @@ class TabObjects {
 				// ui.indent();
 				ui._y -= ui.ELEMENT_OFFSET();
 
-				var listX = ui._x;
-				var listW = ui._w;
+				let listX = ui._x;
+				let listW = ui._w;
 
-				var lineCounter = 0;
-				function drawList(listHandle: zui.Zui.Handle, currentObject: Object) {
+				let lineCounter = 0;
+				drawList(listHandle: zui.Zui.Handle, currentObject: BaseObject) => {
 					if (currentObject.name.charAt(0) == ".") return; // Hidden
-					var b = false;
+					let b = false;
 
 					// Highlight every other line
 					if (lineCounter % 2 == 0) {
@@ -85,22 +72,22 @@ class TabObjects {
 					}
 
 					if (ui.isHovered && ui.inputReleasedR) {
-						UIMenu.draw(function(ui: Zui) {
+						UIMenu.draw((ui: Zui) => {
 							if (UIMenu.menuButton(ui, "Assign Material")) {
 								materialId++;
 
-								for (sh in Scene.active.raw.shader_datas) {
+								for (let sh of Scene.active.raw.shader_datas) {
 									if (sh.name == "Material_data") {
-										var s: TShaderData = Json.parse(Json.stringify(sh));
+										let s: TShaderData = JSON.parse(JSON.stringify(sh));
 										s.name = "TempMaterial_data" + materialId;
 										Scene.active.raw.shader_datas.push(s);
 										break;
 									}
 								}
 
-								for (mat in Scene.active.raw.material_datas) {
+								for (let mat of Scene.active.raw.material_datas) {
 									if (mat.name == "Material") {
-										var m: TMaterialData = Json.parse(Json.stringify(mat));
+										let m: TMaterialData = JSON.parse(JSON.stringify(mat));
 										m.name = "TempMaterial" + materialId;
 										m.shader = "TempMaterial_data" + materialId;
 										Scene.active.raw.material_datas.push(m);
@@ -108,8 +95,8 @@ class TabObjects {
 									}
 								}
 
-								Data.getMaterial("Scene", "TempMaterial" + materialId, function(md: MaterialData) {
-									var mo: MeshObject = cast currentObject;
+								Data.getMaterial("Scene", "TempMaterial" + materialId, (md: MaterialData) => {
+									let mo: MeshObject = currentObject;
 									mo.materials = [md];
 									MakeMaterial.parseMeshPreviewMaterial(md);
 								});
@@ -118,8 +105,8 @@ class TabObjects {
 					}
 
 					if (b) {
-						var currentY = ui._y;
-						for (child in currentObject.children) {
+						let currentY = ui._y;
+						for (let child of currentObject.children) {
 							// ui.indent();
 							drawList(listHandle, child);
 							// ui.unindent();
@@ -131,7 +118,7 @@ class TabObjects {
 						ui.g.color = 0xffffffff;
 					}
 				}
-				for (c in Scene.active.root.children) {
+				for (let c of Scene.active.root.children) {
 					drawList(Zui.handle("tabobjects_1"), c);
 				}
 
@@ -142,35 +129,35 @@ class TabObjects {
 				// ui.indent();
 
 				if (Context.raw.selectedObject != null) {
-					var h = Zui.handle("tabobjects_3");
+					let h = Zui.handle("tabobjects_3");
 					h.selected = Context.raw.selectedObject.visible;
 					Context.raw.selectedObject.visible = ui.check(h, "Visible");
 
-					var t = Context.raw.selectedObject.transform;
-					var localPos = t.loc;
-					var worldPos = new Vec4(t.worldx(), t.worldy(), t.worldz(), 1.0);
-					var scale = t.scale;
-					var rot = t.rot.getEuler();
-					var dim = t.dim;
+					let t = Context.raw.selectedObject.transform;
+					let localPos = t.loc;
+					let worldPos = new Vec4(t.worldx(), t.worldy(), t.worldz(), 1.0);
+					let scale = t.scale;
+					let rot = t.rot.getEuler();
+					let dim = t.dim;
 					rot.mult(180 / 3.141592);
-					var f = 0.0;
+					let f = 0.0;
 
 					ui.row([1 / 4, 1 / 4, 1 / 4, 1 / 4]);
 					ui.text("Loc");
 
 					h = Zui.handle("tabobjects_4");
 					h.text = roundfp(localPos.x) + "";
-					f = Std.parseFloat(ui.textInput(h, "X"));
+					f = parseFloat(ui.textInput(h, "X"));
 					if (h.changed) localPos.x = f;
 
 					h = Zui.handle("tabobjects_5");
 					h.text = roundfp(localPos.y) + "";
-					f = Std.parseFloat(ui.textInput(h, "Y"));
+					f = parseFloat(ui.textInput(h, "Y"));
 					if (h.changed) localPos.y = f;
 
 					h = Zui.handle("tabobjects_6");
 					h.text = roundfp(localPos.z) + "";
-					f = Std.parseFloat(ui.textInput(h, "Z"));
+					f = parseFloat(ui.textInput(h, "Z"));
 					if (h.changed) localPos.z = f;
 
 					ui.row([1 / 4, 1 / 4, 1 / 4, 1 / 4]);
@@ -178,28 +165,28 @@ class TabObjects {
 
 					h = Zui.handle("tabobjects_7");
 					h.text = roundfp(rot.x) + "";
-					f = Std.parseFloat(ui.textInput(h, "X"));
-					var changed = false;
+					f = parseFloat(ui.textInput(h, "X"));
+					let changed = false;
 					if (h.changed) { changed = true; rot.x = f; }
 
 					h = Zui.handle("tabobjects_8");
 					h.text = roundfp(rot.y) + "";
-					f = Std.parseFloat(ui.textInput(h, "Y"));
+					f = parseFloat(ui.textInput(h, "Y"));
 					if (h.changed) { changed = true; rot.y = f; }
 
 					h = Zui.handle("tabobjects_9");
 					h.text = roundfp(rot.z) + "";
-					f = Std.parseFloat(ui.textInput(h, "Z"));
+					f = parseFloat(ui.textInput(h, "Z"));
 					if (h.changed) { changed = true; rot.z = f; }
 
 					if (changed && Context.raw.selectedObject.name != "Scene") {
 						rot.mult(3.141592 / 180);
 						Context.raw.selectedObject.transform.rot.fromEuler(rot.x, rot.y, rot.z);
 						Context.raw.selectedObject.transform.buildMatrix();
-						// #if arm_physics
-						// var rb = Context.raw.selectedObject.getTrait(RigidBody);
+						// ///if arm_physics
+						// let rb = Context.raw.selectedObject.getTrait(RigidBody);
 						// if (rb != null) rb.syncTransform();
-						// #end
+						// ///end
 					}
 
 					ui.row([1 / 4, 1 / 4, 1 / 4, 1 / 4]);
@@ -207,17 +194,17 @@ class TabObjects {
 
 					h = Zui.handle("tabobjects_10");
 					h.text = roundfp(scale.x) + "";
-					f = Std.parseFloat(ui.textInput(h, "X"));
+					f = parseFloat(ui.textInput(h, "X"));
 					if (h.changed) scale.x = f;
 
 					h = Zui.handle("tabobjects_11");
 					h.text = roundfp(scale.y) + "";
-					f = Std.parseFloat(ui.textInput(h, "Y"));
+					f = parseFloat(ui.textInput(h, "Y"));
 					if (h.changed) scale.y = f;
 
 					h = Zui.handle("tabobjects_12");
 					h.text = roundfp(scale.z) + "";
-					f = Std.parseFloat(ui.textInput(h, "Z"));
+					f = parseFloat(ui.textInput(h, "Z"));
 					if (h.changed) scale.z = f;
 
 					ui.row([1 / 4, 1 / 4, 1 / 4, 1 / 4]);
@@ -225,35 +212,35 @@ class TabObjects {
 
 					h = Zui.handle("tabobjects_13");
 					h.text = roundfp(dim.x) + "";
-					f = Std.parseFloat(ui.textInput(h, "X"));
+					f = parseFloat(ui.textInput(h, "X"));
 					if (h.changed) dim.x = f;
 
 					h = Zui.handle("tabobjects_14");
 					h.text = roundfp(dim.y) + "";
-					f = Std.parseFloat(ui.textInput(h, "Y"));
+					f = parseFloat(ui.textInput(h, "Y"));
 					if (h.changed) dim.y = f;
 
 					h = Zui.handle("tabobjects_15");
 					h.text = roundfp(dim.z) + "";
-					f = Std.parseFloat(ui.textInput(h, "Z"));
+					f = parseFloat(ui.textInput(h, "Z"));
 					if (h.changed) dim.z = f;
 
 					Context.raw.selectedObject.transform.dirty = true;
 
 					if (Context.raw.selectedObject.name == "Scene") {
-						var p = Scene.active.world.probe;
+						let p = Scene.active.world.probe;
 						p.raw.strength = ui.slider(Zui.handle("tabobjects_16", {value: p.raw.strength}), "Environment", 0.0, 5.0, true);
 					}
-					else if (Std.isOfType(Context.raw.selectedObject, LightObject)) {
-						var light = cast(Context.raw.selectedObject, LightObject);
-						var lightHandle = Zui.handle("tabobjects_17");
+					else if (Context.raw.selectedObject.constructor == LightObject) {
+						let light = cast(Context.raw.selectedObject, LightObject);
+						let lightHandle = Zui.handle("tabobjects_17");
 						lightHandle.value = light.data.raw.strength / 10;
 						light.data.raw.strength = ui.slider(lightHandle, "Strength", 0.0, 5.0, true) * 10;
 					}
-					else if (Std.isOfType(Context.raw.selectedObject, CameraObject)) {
-						var cam = cast(Context.raw.selectedObject, CameraObject);
-						var fovHandle = Zui.handle("tabobjects_18");
-						fovHandle.value = Std.int(cam.data.raw.fov * 100) / 100;
+					else if (Context.raw.selectedObject.constructor == CameraObject) {
+						let cam = cast(Context.raw.selectedObject, CameraObject);
+						let fovHandle = Zui.handle("tabobjects_18");
+						fovHandle.value = Math.floor(cam.data.raw.fov * 100) / 100;
 						cam.data.raw.fov = ui.slider(fovHandle, "FoV", 0.3, 2.0, true);
 						if (fovHandle.changed) {
 							cam.buildProjection();

+ 0 - 5
armorforge/Sources/import.hx

@@ -1,5 +0,0 @@
-// Global imports
-
-import arm.Translator.tr;
-import arm.Enums;
-using StringTools;

+ 1 - 0
armorforge/project.js

@@ -12,6 +12,7 @@ await project.addProject("../base");
 project.addSources("../armorpaint/Sources"); ////
 project.addShaders("../armorpaint/Shaders/*.glsl", { embed: flags.snapshot }); ////
 project.addSources("Sources");
+project.addSources("Sources/nodes");
 project.addShaders("Shaders/*.glsl", { embed: flags.snapshot });
 project.addAssets("Assets/*", { destination: "data/{name}", embed: flags.snapshot });
 project.addAssets("Assets/keymap_presets/*", { destination: "data/keymap_presets/{name}" });

+ 118 - 0
armorlab/Sources/MakeMaterial.ts

@@ -0,0 +1,118 @@
+
+class MakeMaterial {
+
+	static defaultScon: ShaderContext = null;
+	static defaultMcon: MaterialContext = null;
+	static heightUsed = false;
+
+	static parseMeshMaterial = () => {
+		let m = Project.materialData;
+
+		for (let c of m.shader.contexts) {
+			if (c.raw.name == "mesh") {
+				m.shader.raw.contexts.remove(c.raw);
+				m.shader.contexts.remove(c);
+				deleteContext(c);
+				break;
+			}
+		}
+
+		let con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }));
+		let scon = new ShaderContext(con.data, (scon: ShaderContext) => {});
+		scon.overrideContext = {};
+		if (con.frag.sharedSamplers.length > 0) {
+			let sampler = con.frag.sharedSamplers[0];
+			scon.overrideContext.shared_sampler = sampler.substr(sampler.lastIndexOf(" ") + 1);
+		}
+		if (!Context.raw.textureFilter) {
+			scon.overrideContext.filter = "point";
+		}
+		scon.overrideContext.addressing = "repeat";
+		m.shader.raw.contexts.push(scon.raw);
+		m.shader.contexts.push(scon);
+
+		Context.raw.ddirty = 2;
+
+		///if arm_voxels
+		makeVoxel(m);
+		///end
+
+		///if (krom_direct3d12 || krom_vulkan)
+		RenderPathRaytrace.dirty = 1;
+		///end
+	}
+
+	///if arm_voxels
+	static makeVoxel = (m: MaterialData) => {
+		let rebuild = true; // heightUsed;
+		if (Config.raw.rp_gi != false && rebuild) {
+			let scon: ShaderContext = null;
+			for (let c of m.shader.contexts) {
+				if (c.raw.name == "voxel") {
+					scon = c;
+					break;
+				}
+			}
+			if (scon != null) MakeVoxel.run(scon);
+		}
+	}
+	///end
+
+	static parsePaintMaterial = () => {
+		let m = Project.materialData;
+		let scon: ShaderContext = null;
+		let mcon: MaterialContext = null;
+		for (let c of m.shader.contexts) {
+			if (c.raw.name == "paint") {
+				m.shader.raw.contexts.remove(c.raw);
+				m.shader.contexts.remove(c);
+				if (c != defaultScon) deleteContext(c);
+				break;
+			}
+		}
+		for (let c of m.contexts) {
+			if (c.raw.name == "paint") {
+				m.raw.contexts.remove(c.raw);
+				m.contexts.remove(c);
+				break;
+			}
+		}
+
+		let sdata = new NodeShaderData({ name: "Material", canvas: null });
+		let mcon: TMaterialContext = { name: "paint", bind_textures: [] };
+		let con = MakePaint.run(sdata, mcon);
+
+		let compileError = false;
+		let scon = new ShaderContext(con.data, (scon: ShaderContext) => {
+			if (scon == null) compileError = true;
+		});
+		if (compileError) return;
+		scon.overrideContext = {};
+		scon.overrideContext.addressing = "repeat";
+		let mcon = new MaterialContext(mcon, (mcon: MaterialContext) => {});
+
+		m.shader.raw.contexts.push(scon.raw);
+		m.shader.contexts.push(scon);
+		m.raw.contexts.push(mcon.raw);
+		m.contexts.push(mcon);
+
+		if (defaultScon == null) defaultScon = scon;
+		if (defaultMcon == null) defaultMcon = mcon;
+	}
+
+	static getDisplaceStrength = (): f32 => {
+		let sc = Context.mainObject().transform.scale.x;
+		return Config.raw.displace_strength * 0.02 * sc;
+	}
+
+	static voxelgiHalfExtents = (): string => {
+		let ext = Context.raw.vxaoExt;
+		return `const vec3 voxelgiHalfExtents = vec3(${ext}, ${ext}, ${ext});`;
+	}
+
+	static deleteContext = (c: ShaderContext) => {
+		Base.notifyOnNextFrame(() => { // Ensure pipeline is no longer in use
+			c.delete();
+		});
+	}
+}

+ 22 - 23
armorlab/Sources/arm/MakeMesh.hx → armorlab/Sources/MakeMesh.ts

@@ -1,11 +1,10 @@
-package arm;
 
 class MakeMesh {
 
-	public static var layerPassCount = 1;
+	static layerPassCount = 1;
 
-	public static function run(data: NodeShaderData, layerPass = 0): NodeShaderContext {
-		var con_mesh: NodeShaderContext = data.add_context({
+	static run = (data: NodeShaderData, layerPass = 0): NodeShaderContext => {
+		let con_mesh: NodeShaderContext = data.add_context({
 			name: "mesh",
 			depth_write: layerPass == 0 ? true : false,
 			compare_mode: layerPass == 0 ? "less" : "equal",
@@ -15,8 +14,8 @@ class MakeMesh {
 			depth_attachment: "DEPTH32"
 		});
 
-		var vert = con_mesh.make_vert();
-		var frag = con_mesh.make_frag();
+		let vert = con_mesh.make_vert();
+		let frag = con_mesh.make_frag();
 		frag.ins = vert.outs;
 
 		vert.add_out('vec2 texCoord');
@@ -26,19 +25,19 @@ class MakeMesh {
 		vert.add_uniform('mat4 prevWVP', '_prevWorldViewProjectionMatrix');
 		vert.wposition = true;
 
-		var textureCount = 0;
-		var displaceStrength = MakeMaterial.getDisplaceStrength();
+		let textureCount = 0;
+		let displaceStrength = MakeMaterial.getDisplaceStrength();
 		if (MakeMaterial.heightUsed && displaceStrength > 0.0) {
 			vert.n = true;
 			vert.write('float height = 0.0;');
-			var numLayers = 1;
-			vert.write('wposition += wnormal * vec3(height, height, height) * vec3($displaceStrength, $displaceStrength, $displaceStrength);');
+			let numLayers = 1;
+			vert.write(`wposition += wnormal * vec3(height, height, height) * vec3(${displaceStrength}, ${displaceStrength}, ${displaceStrength});`);
 		}
 
 		vert.write('gl_Position = mul(vec4(wposition.xyz, 1.0), VP);');
-		var brushScale = Context.raw.brushScale;
+		let brushScale = Context.raw.brushScale;
 		vert.add_uniform('float texScale', '_texUnpack');
-		vert.write('texCoord = tex * $brushScale * texScale;');
+		vert.write(`texCoord = tex * ${brushScale} * texScale;`);
 		if (MakeMaterial.heightUsed && displaceStrength > 0) {
 			vert.add_uniform('mat4 invW', '_inverseWorldMatrix');
 			vert.write('prevwvpposition = mul(mul(vec4(wposition, 1.0), invW), prevWVP);');
@@ -104,15 +103,15 @@ class MakeMesh {
 		frag.write('height = texpaint_pack_sample.a * texpaint_opac;');
 
 		// if (l.paintHeight && MakeMaterial.heightUsed) {
-		// 	var assign = l.paintHeightBlend ? "+=" : "=";
-		// 	frag.write('height $assign texpaint_pack_sample.a * texpaint_opac;');
+		// 	let assign = l.paintHeightBlend ? "+=" : "=";
+		// 	frag.write(`height ${assign} texpaint_pack_sample.a * texpaint_opac;`);
 		// 	frag.write('{');
 		// 	frag.add_uniform('vec2 texpaintSize', '_texpaintSize');
 		// 	frag.write('float tex_step = 1.0 / texpaintSize.x;');
-		// 	frag.write('height0 $assign textureLodShared(texpaint_pack' + ', vec2(texCoord.x - tex_step, texCoord.y), 0.0).a * texpaint_opac;');
-		// 	frag.write('height1 $assign textureLodShared(texpaint_pack' + ', vec2(texCoord.x + tex_step, texCoord.y), 0.0).a * texpaint_opac;');
-		// 	frag.write('height2 $assign textureLodShared(texpaint_pack' + ', vec2(texCoord.x, texCoord.y - tex_step), 0.0).a * texpaint_opac;');
-		// 	frag.write('height3 $assign textureLodShared(texpaint_pack' + ', vec2(texCoord.x, texCoord.y + tex_step), 0.0).a * texpaint_opac;');
+		// 	frag.write(`height0 ${assign} textureLodShared(texpaint_pack` + ', vec2(texCoord.x - tex_step, texCoord.y), 0.0).a * texpaint_opac;');
+		// 	frag.write(`height1 ${assign} textureLodShared(texpaint_pack` + ', vec2(texCoord.x + tex_step, texCoord.y), 0.0).a * texpaint_opac;');
+		// 	frag.write(`height2 ${assign} textureLodShared(texpaint_pack` + ', vec2(texCoord.x, texCoord.y - tex_step), 0.0).a * texpaint_opac;');
+		// 	frag.write(`height3 ${assign} textureLodShared(texpaint_pack` + ', vec2(texCoord.x, texCoord.y + tex_step), 0.0).a * texpaint_opac;');
 		// 	frag.write('}');
 		// }
 
@@ -128,11 +127,11 @@ class MakeMesh {
 		}
 
 		frag.vVec = true;
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		frag.write('mat3 TBN = cotangentFrame(n, vVec, texCoord);');
-		#else
+		///else
 		frag.write('mat3 TBN = cotangentFrame(n, -vVec, texCoord);');
-		#end
+		///end
 		frag.write('n = ntex * 2.0 - 1.0;');
 		frag.write('n.y = -n.y;');
 		frag.write('n = normalize(mul(n, TBN));');
@@ -142,8 +141,8 @@ class MakeMesh {
 			frag.write('basecol = pow(basecol, vec3(2.2, 2.2, 2.2));');
 
 			if (Context.raw.viewportShader != null) {
-				var color = Context.raw.viewportShader(frag);
-				frag.write('fragColor[1] = vec4($color, 1.0);');
+				let color = Context.raw.viewportShader(frag);
+				frag.write(`fragColor[1] = vec4(${color}, 1.0);`);
 			}
 			else if (Context.raw.renderMode == RenderForward && Context.raw.viewportMode != ViewPathTrace) {
 				frag.wposition = true;

+ 47 - 51
armorlab/Sources/arm/MakePaint.hx → armorlab/Sources/MakePaint.ts

@@ -1,12 +1,8 @@
-package arm;
-
-import iron.SceneFormat;
-import zui.Zui.Nodes;
 
 class MakePaint {
 
-	public static function run(data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext {
-		var con_paint:NodeShaderContext = data.add_context({
+	static run = (data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext => {
+		let con_paint:NodeShaderContext = data.add_context({
 			name: "paint",
 			depth_write: false,
 			compare_mode: "always", // TODO: align texcoords winding order
@@ -24,8 +20,8 @@ class MakePaint {
 		con_paint.data.color_writes_alpha = [true, true, true, true];
 		con_paint.allow_vcols = Context.raw.paintObject.data.cols != null;
 
-		var vert = con_paint.make_vert();
-		var frag = con_paint.make_frag();
+		let vert = con_paint.make_vert();
+		let frag = con_paint.make_frag();
 		frag.ins = vert.outs;
 
 		if (Context.raw.tool == ToolPicker) {
@@ -36,11 +32,11 @@ class MakePaint {
 			frag.add_uniform('vec2 gbufferSize', '_gbufferSize');
 			frag.add_uniform('vec4 inp', '_inputBrush');
 
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(inp.x * gbufferSize.x, inp.y * gbufferSize.y), 0).ba;');
-			#else
+			///else
 			frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(inp.x * gbufferSize.x, (1.0 - inp.y) * gbufferSize.y), 0).ba;');
-			#end
+			///end
 
 			frag.add_out('vec4 fragColor[4]');
 			frag.add_uniform('sampler2D texpaint');
@@ -56,12 +52,12 @@ class MakePaint {
 			return con_paint;
 		}
 
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		vert.write('vec2 tpos = vec2(tex.x * 2.0 - 1.0, (1.0 - tex.y) * 2.0 - 1.0);');
 		// vert.write('vec2 tpos = vec2(frac(tex.x * texScale) * 2.0 - 1.0, (1.0 - frac(tex.y * texScale)) * 2.0 - 1.0);'); // 3D View
-		#else
+		///else
 		vert.write('vec2 tpos = vec2(tex.xy * 2.0 - 1.0);');
-		#end
+		///end
 
 		vert.write('gl_Position = vec4(tpos, 0.0, 1.0);');
 
@@ -96,11 +92,11 @@ class MakePaint {
 
 			frag.write('float dist = 0.0;');
 
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('float depth = textureLod(gbufferD, inp.xy, 0.0).r;');
-			#else
+			///else
 			frag.write('float depth = textureLod(gbufferD, vec2(inp.x, 1.0 - inp.y), 0.0).r;');
-			#end
+			///end
 
 			frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix');
 			frag.write('vec4 winp = vec4(vec2(inp.x, 1.0 - inp.y) * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);');
@@ -108,11 +104,11 @@ class MakePaint {
 			frag.write('winp.xyz /= winp.w;');
 			frag.wposition = true;
 
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('float depthlast = textureLod(gbufferD, inplast.xy, 0.0).r;');
-			#else
+			///else
 			frag.write('float depthlast = textureLod(gbufferD, vec2(inplast.x, 1.0 - inplast.y), 0.0).r;');
-			#end
+			///end
 
 			frag.write('vec4 winplast = vec4(vec2(inplast.x, 1.0 - inplast.y) * 2.0 - 1.0, depthlast * 2.0 - 1.0, 1.0);');
 			frag.write('winplast = mul(winplast, invVP);');
@@ -142,35 +138,35 @@ class MakePaint {
 
 			if (Context.raw.tool == ToolClone) {
 				// frag.add_uniform('vec2 cloneDelta', '_cloneDelta');
-				// #if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+				// ///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 				// frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2((sp.xy + cloneDelta) * gbufferSize), 0).ba;');
-				// #else
+				// ///else
 				// frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2((sp.x + cloneDelta.x) * gbufferSize.x, (1.0 - (sp.y + cloneDelta.y)) * gbufferSize.y), 0).ba;');
-				// #end
+				// ///end
 
 				// frag.write('vec3 texpaint_pack_sample = textureLod(texpaint_pack_undo, texCoordInp, 0.0).rgb;');
-				// var base = 'textureLod(texpaint_undo, texCoordInp, 0.0).rgb';
-				// var rough = 'texpaint_pack_sample.g';
-				// var met = 'texpaint_pack_sample.b';
-				// var occ = 'texpaint_pack_sample.r';
-				// var nortan = 'textureLod(texpaint_nor_undo, texCoordInp, 0.0).rgb';
-				// var height = '0.0';
-				// var opac = '1.0';
-				// frag.write('vec3 basecol = $base;');
-				// frag.write('float roughness = $rough;');
-				// frag.write('float metallic = $met;');
-				// frag.write('float occlusion = $occ;');
-				// frag.write('vec3 nortan = $nortan;');
-				// frag.write('float height = $height;');
-				// frag.write('float mat_opacity = $opac;');
+				// let base = 'textureLod(texpaint_undo, texCoordInp, 0.0).rgb';
+				// let rough = 'texpaint_pack_sample.g';
+				// let met = 'texpaint_pack_sample.b';
+				// let occ = 'texpaint_pack_sample.r';
+				// let nortan = 'textureLod(texpaint_nor_undo, texCoordInp, 0.0).rgb';
+				// let height = '0.0';
+				// let opac = '1.0';
+				// frag.write(`vec3 basecol = ${base};`);
+				// frag.write(`float roughness = ${rough};`);
+				// frag.write(`float metallic = ${met};`);
+				// frag.write(`float occlusion = ${occ};`);
+				// frag.write(`vec3 nortan = ${nortan};`);
+				// frag.write(`float height = ${height};`);
+				// frag.write(`float mat_opacity = ${opac};`);
 				// frag.write('float opacity = mat_opacity * brushOpacity;');
 			}
 			else { // Blur
-				// #if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+				// ///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 				// frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(sp.x * gbufferSize.x, sp.y * gbufferSize.y), 0).ba;');
-				// #else
+				// ///else
 				// frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(sp.x * gbufferSize.x, (1.0 - sp.y) * gbufferSize.y), 0).ba;');
-				// #end
+				// ///end
 
 				// frag.write('vec3 basecol = vec3(0.0, 0.0, 0.0);');
 				// frag.write('float roughness = 0.0;');
@@ -184,19 +180,19 @@ class MakePaint {
 				// frag.add_uniform('vec2 texpaintSize', '_texpaintSize');
 				// frag.write('float blur_step = 1.0 / texpaintSize.x;');
 				// if (Context.raw.blurDirectional) {
-				// 	#if (krom_direct3d11 || krom_direct3d12 || krom_metal)
+				// 	///if (krom_direct3d11 || krom_direct3d12 || krom_metal)
 				// 	frag.write('const float blur_weight[7] = {1.0 / 28.0, 2.0 / 28.0, 3.0 / 28.0, 4.0 / 28.0, 5.0 / 28.0, 6.0 / 28.0, 7.0 / 28.0};');
-				// 	#else
+				// 	///else
 				// 	frag.write('const float blur_weight[7] = float[](1.0 / 28.0, 2.0 / 28.0, 3.0 / 28.0, 4.0 / 28.0, 5.0 / 28.0, 6.0 / 28.0, 7.0 / 28.0);');
-				// 	#end
+				// 	///end
 				// 	frag.add_uniform('vec3 brushDirection', '_brushDirection');
 				// 	frag.write('vec2 blur_direction = brushDirection.yx;');
 				// 	frag.write('for (int i = 0; i < 7; ++i) {');
-				// 	#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+				// 	///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 				// 	frag.write('vec2 texCoordInp2 = texelFetch(gbuffer2, ivec2((sp.x + blur_direction.x * blur_step * float(i)) * gbufferSize.x, (sp.y + blur_direction.y * blur_step * float(i)) * gbufferSize.y), 0).ba;');
-				// 	#else
+				// 	///else
 				// 	frag.write('vec2 texCoordInp2 = texelFetch(gbuffer2, ivec2((sp.x + blur_direction.x * blur_step * float(i)) * gbufferSize.x, (1.0 - (sp.y + blur_direction.y * blur_step * float(i))) * gbufferSize.y), 0).ba;');
-				// 	#end
+				// 	///end
 				// 	frag.write('vec4 texpaint_sample = texture(texpaint_undo, texCoordInp2);');
 				// 	frag.write('opacity += texpaint_sample.a * blur_weight[i];');
 				// 	frag.write('basecol += texpaint_sample.rgb * blur_weight[i];');
@@ -209,11 +205,11 @@ class MakePaint {
 				// 	frag.write('}');
 				// }
 				// else {
-				// 	#if (krom_direct3d11 || krom_direct3d12 || krom_metal)
+				// 	///if (krom_direct3d11 || krom_direct3d12 || krom_metal)
 				// 	frag.write('const float blur_weight[15] = {0.034619 / 2.0, 0.044859 / 2.0, 0.055857 / 2.0, 0.066833 / 2.0, 0.076841 / 2.0, 0.084894 / 2.0, 0.090126 / 2.0, 0.09194 / 2.0, 0.090126 / 2.0, 0.084894 / 2.0, 0.076841 / 2.0, 0.066833 / 2.0, 0.055857 / 2.0, 0.044859 / 2.0, 0.034619 / 2.0};');
-				// 	#else
+				// 	///else
 				// 	frag.write('const float blur_weight[15] = float[](0.034619 / 2.0, 0.044859 / 2.0, 0.055857 / 2.0, 0.066833 / 2.0, 0.076841 / 2.0, 0.084894 / 2.0, 0.090126 / 2.0, 0.09194 / 2.0, 0.090126 / 2.0, 0.084894 / 2.0, 0.076841 / 2.0, 0.066833 / 2.0, 0.055857 / 2.0, 0.044859 / 2.0, 0.034619 / 2.0);');
-				// 	#end
+				// 	///end
 				// 	// X
 				// 	frag.write('for (int i = -7; i <= 7; ++i) {');
 				// 	frag.write('vec4 texpaint_sample = texture(texpaint_undo, texCoordInp + vec2(blur_step * float(i), 0.0));');
@@ -251,9 +247,9 @@ class MakePaint {
 		// Manual blending to preserve memory
 		frag.wvpposition = true;
 		frag.write('vec2 sample_tc = vec2(wvpposition.xy / wvpposition.w) * 0.5 + 0.5;');
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		frag.write('sample_tc.y = 1.0 - sample_tc.y;');
-		#end
+		///end
 		frag.add_uniform('sampler2D paintmask');
 		frag.write('float sample_mask = textureLod(paintmask, sample_tc, 0.0).r;');
 		frag.write('str = max(str, sample_mask);');

+ 0 - 10
armorlab/Sources/Manifest.hx

@@ -1,10 +0,0 @@
-package ;
-
-class Manifest {
-
-	public static inline var title = "ArmorLab";
-	public static inline var version = "0.1";
-	public static inline var url = "https://armorlab.org";
-	public static inline var url_android = "";
-	public static inline var url_ios = "";
-}

+ 9 - 0
armorlab/Sources/Manifest.ts

@@ -0,0 +1,9 @@
+
+class Manifest {
+
+	static title = "ArmorLab";
+	static version = "0.1";
+	static url = "https://armorlab.org";
+	static url_android = "";
+	static url_ios = "";
+}

+ 35 - 0
armorlab/Sources/NodesBrush.ts

@@ -0,0 +1,35 @@
+
+class NodesBrush {
+
+	static categories = [_tr("Input"), _tr("Model")];
+
+	static list: zui.Zui.TNode[][] = [
+		[ // Input
+			ImageTextureNode.def,
+			RGBNode.def,
+		],
+		[ // Model
+			InpaintNode.def,
+			PhotoToPBRNode.def,
+			TextToPhotoNode.def,
+			TilingNode.def,
+			UpscaleNode.def,
+			VarianceNode.def,
+		]
+	];
+
+	static createNode = (nodeType: string): zui.Zui.TNode => {
+		for (let c of list) {
+			for (let n of c) {
+				if (n.type == nodeType) {
+					let canvas = Project.canvas;
+					let nodes = Project.nodes;
+					let node = arm.UINodes.makeNode(n, nodes, canvas);
+					canvas.nodes.push(node);
+					return node;
+				}
+			}
+		}
+		return null;
+	}
+}

+ 75 - 84
armorlab/Sources/arm/RenderPathPaint.hx → armorlab/Sources/RenderPathPaint.ts

@@ -1,23 +1,14 @@
-package arm;
-
-import iron.System;
-import iron.Mat4;
-import iron.MeshObject;
-import iron.SceneFormat;
-import iron.RenderPath;
-import iron.Scene;
-import iron.App;
 
 class RenderPathPaint {
 
-	static var path: RenderPath;
-	public static var liveLayerDrawn = 0; ////
+	static path: RenderPath;
+	static liveLayerDrawn = 0; ////
 
-	public static function init(_path: RenderPath) {
+	static init = (_path: RenderPath) => {
 		path = _path;
 
 		{
-			var t = new RenderTargetRaw();
+			let t = new RenderTargetRaw();
 			t.name = "texpaint_blend0";
 			t.width = Config.getTextureResX();
 			t.height = Config.getTextureResY();
@@ -25,7 +16,7 @@ class RenderPathPaint {
 			path.createRenderTarget(t);
 		}
 		{
-			var t = new RenderTargetRaw();
+			let t = new RenderTargetRaw();
 			t.name = "texpaint_blend1";
 			t.width = Config.getTextureResX();
 			t.height = Config.getTextureResY();
@@ -33,7 +24,7 @@ class RenderPathPaint {
 			path.createRenderTarget(t);
 		}
 		{
-			var t = new RenderTargetRaw();
+			let t = new RenderTargetRaw();
 			t.name = "texpaint_picker";
 			t.width = 1;
 			t.height = 1;
@@ -41,7 +32,7 @@ class RenderPathPaint {
 			path.createRenderTarget(t);
 		}
 		{
-			var t = new RenderTargetRaw();
+			let t = new RenderTargetRaw();
 			t.name = "texpaint_nor_picker";
 			t.width = 1;
 			t.height = 1;
@@ -49,7 +40,7 @@ class RenderPathPaint {
 			path.createRenderTarget(t);
 		}
 		{
-			var t = new RenderTargetRaw();
+			let t = new RenderTargetRaw();
 			t.name = "texpaint_pack_picker";
 			t.width = 1;
 			t.height = 1;
@@ -57,7 +48,7 @@ class RenderPathPaint {
 			path.createRenderTarget(t);
 		}
 		{
-			var t = new RenderTargetRaw();
+			let t = new RenderTargetRaw();
 			t.name = "texpaint_uv_picker";
 			t.width = 1;
 			t.height = 1;
@@ -68,14 +59,14 @@ class RenderPathPaint {
 		path.loadShader("shader_datas/copy_mrt3_pass/copy_mrt3_pass");
 	}
 
-	public static function commandsPaint(dilation = true) {
-		var tid = "";
+	static commandsPaint = (dilation = true) => {
+		let tid = "";
 
 		if (Context.raw.pdirty > 0) {
 
 			if (Context.raw.tool == ToolPicker) {
 
-					#if krom_metal
+					///if krom_metal
 					//path.setTarget("texpaint_picker");
 					//path.clearTarget(0xff000000);
 					//path.setTarget("texpaint_nor_picker");
@@ -83,10 +74,10 @@ class RenderPathPaint {
 					//path.setTarget("texpaint_pack_picker");
 					//path.clearTarget(0xff000000);
 					path.setTarget("texpaint_picker", ["texpaint_nor_picker", "texpaint_pack_picker", "texpaint_uv_picker"]);
-					#else
+					///else
 					path.setTarget("texpaint_picker", ["texpaint_nor_picker", "texpaint_pack_picker", "texpaint_uv_picker"]);
 					//path.clearTarget(0xff000000);
-					#end
+					///end
 					path.bindTarget("gbuffer2", "gbuffer2");
 					// tid = Context.raw.layer.id;
 					path.bindTarget("texpaint" + tid, "texpaint");
@@ -95,21 +86,21 @@ class RenderPathPaint {
 					path.drawMeshes("paint");
 					UIHeader.inst.headerHandle.redraws = 2;
 
-					var texpaint_picker = path.renderTargets.get("texpaint_picker").image;
-					var texpaint_nor_picker = path.renderTargets.get("texpaint_nor_picker").image;
-					var texpaint_pack_picker = path.renderTargets.get("texpaint_pack_picker").image;
-					var texpaint_uv_picker = path.renderTargets.get("texpaint_uv_picker").image;
-					var a = texpaint_picker.getPixels();
-					var b = texpaint_nor_picker.getPixels();
-					var c = texpaint_pack_picker.getPixels();
-					var d = texpaint_uv_picker.getPixels();
+					let texpaint_picker = path.renderTargets.get("texpaint_picker").image;
+					let texpaint_nor_picker = path.renderTargets.get("texpaint_nor_picker").image;
+					let texpaint_pack_picker = path.renderTargets.get("texpaint_pack_picker").image;
+					let texpaint_uv_picker = path.renderTargets.get("texpaint_uv_picker").image;
+					let a = texpaint_picker.getPixels();
+					let b = texpaint_nor_picker.getPixels();
+					let c = texpaint_pack_picker.getPixels();
+					let d = texpaint_uv_picker.getPixels();
 
 					if (Context.raw.colorPickerCallback != null) {
 						Context.raw.colorPickerCallback(Context.raw.pickedColor);
 					}
 
 					// Picked surface values
-					// #if (krom_metal || krom_vulkan)
+					// ///if (krom_metal || krom_vulkan)
 					// Context.raw.pickedColor.base.Rb = a.get(2);
 					// Context.raw.pickedColor.base.Gb = a.get(1);
 					// Context.raw.pickedColor.base.Bb = a.get(0);
@@ -123,7 +114,7 @@ class RenderPathPaint {
 					// Context.raw.pickedColor.opacity = a.get(3) / 255;
 					// Context.raw.uvxPicked = d.get(2) / 255;
 					// Context.raw.uvyPicked = d.get(1) / 255;
-					// #else
+					// ///else
 					// Context.raw.pickedColor.base.Rb = a.get(0);
 					// Context.raw.pickedColor.base.Gb = a.get(1);
 					// Context.raw.pickedColor.base.Bb = a.get(2);
@@ -137,10 +128,10 @@ class RenderPathPaint {
 					// Context.raw.pickedColor.opacity = a.get(3) / 255;
 					// Context.raw.uvxPicked = d.get(0) / 255;
 					// Context.raw.uvyPicked = d.get(1) / 255;
-					// #end
+					// ///end
 			}
 			else {
-				var texpaint = "texpaint_node_target";
+				let texpaint = "texpaint_node_target";
 
 				path.setTarget("texpaint_blend1");
 				path.bindTarget("texpaint_blend0", "tex");
@@ -153,7 +144,7 @@ class RenderPathPaint {
 				path.bindTarget("texpaint_blend1", "paintmask");
 
 				// Read texcoords from gbuffer
-				var readTC = Context.raw.tool == ToolClone ||
+				let readTC = Context.raw.tool == ToolClone ||
 							 Context.raw.tool == ToolBlur ||
 							 Context.raw.tool == ToolSmudge;
 				if (readTC) {
@@ -165,8 +156,8 @@ class RenderPathPaint {
 		}
 	}
 
-	public static function commandsCursor() {
-		var tool = Context.raw.tool;
+	static commandsCursor = () => {
+		let tool = Context.raw.tool;
 		if (tool != ToolEraser &&
 			tool != ToolClone &&
 			tool != ToolBlur &&
@@ -174,52 +165,52 @@ class RenderPathPaint {
 			return;
 		}
 
-		var nodes = UINodes.inst.getNodes();
-		var canvas = UINodes.inst.getCanvas(true);
-		var inpaint = nodes.nodesSelectedId.length > 0 && nodes.getNode(canvas.nodes, nodes.nodesSelectedId[0]).type == "InpaintNode";
+		let nodes = UINodes.inst.getNodes();
+		let canvas = UINodes.inst.getCanvas(true);
+		let inpaint = nodes.nodesSelectedId.length > 0 && nodes.getNode(canvas.nodes, nodes.nodesSelectedId[0]).type == "InpaintNode";
 
 		if (!Base.uiEnabled || Base.isDragging || !inpaint) {
 			return;
 		}
 
-		var mx = Context.raw.paintVec.x;
-		var my = 1.0 - Context.raw.paintVec.y;
+		let mx = Context.raw.paintVec.x;
+		let my = 1.0 - Context.raw.paintVec.y;
 		if (Context.raw.brushLocked) {
 			mx = (Context.raw.lockStartedX - App.x()) / App.w();
 			my = 1.0 - (Context.raw.lockStartedY - App.y()) / App.h();
 		}
-		var radius = Context.raw.brushRadius;
+		let radius = Context.raw.brushRadius;
 		drawCursor(mx, my, radius / 3.4);
 	}
 
-	static function drawCursor(mx: Float, my: Float, radius: Float, tintR = 1.0, tintG = 1.0, tintB = 1.0) {
-		var plane = cast(Scene.active.getChild(".Plane"), MeshObject);
-		var geom = plane.data;
+	static drawCursor = (mx: f32, my: f32, radius: f32, tintR = 1.0, tintG = 1.0, tintB = 1.0) => {
+		let plane = cast(Scene.active.getChild(".Plane"), MeshObject);
+		let geom = plane.data;
 
-		var g = path.frameG;
+		let g = path.frameG;
 		if (Base.pipeCursor == null) Base.makeCursorPipe();
 
 		path.setTarget("");
 		g.setPipeline(Base.pipeCursor);
-		var img = Res.get("cursor.k");
+		let img = Res.get("cursor.k");
 		g.setTexture(Base.cursorTex, img);
-		var gbuffer0 = path.renderTargets.get("gbuffer0").image;
+		let gbuffer0 = path.renderTargets.get("gbuffer0").image;
 		g.setTextureDepth(Base.cursorGbufferD, gbuffer0);
 		g.setFloat2(Base.cursorMouse, mx, my);
 		g.setFloat2(Base.cursorTexStep, 1 / gbuffer0.width, 1 / gbuffer0.height);
 		g.setFloat(Base.cursorRadius, radius);
-		var right = Scene.active.camera.rightWorld().normalize();
+		let right = Scene.active.camera.rightWorld().normalize();
 		g.setFloat3(Base.cursorCameraRight, right.x, right.y, right.z);
 		g.setFloat3(Base.cursorTint, tintR, tintG, tintB);
 		g.setMatrix(Base.cursorVP, Scene.active.camera.VP);
-		var helpMat = Mat4.identity();
+		let helpMat = Mat4.identity();
 		helpMat.getInverse(Scene.active.camera.VP);
 		g.setMatrix(Base.cursorInvVP, helpMat);
-		#if (krom_metal || krom_vulkan)
+		///if (krom_metal || krom_vulkan)
 		g.setVertexBuffer(geom.get([{name: "tex", data: "short2norm"}]));
-		#else
+		///else
 		g.setVertexBuffer(geom.vertexBuffer);
-		#end
+		///end
 		g.setIndexBuffer(geom.indexBuffers[0]);
 		g.drawIndexedVertices();
 
@@ -227,15 +218,15 @@ class RenderPathPaint {
 		path.end();
 	}
 
-	static function paintEnabled(): Bool {
+	static paintEnabled = (): bool => {
 		return !Context.raw.foregroundEvent;
 	}
 
-	public static function begin() {
+	static begin = () => {
 		if (!paintEnabled()) return;
 	}
 
-	public static function end() {
+	static end = () => {
 		commandsCursor();
 		Context.raw.ddirty--;
 		Context.raw.rdirty--;
@@ -244,53 +235,53 @@ class RenderPathPaint {
 		Context.raw.pdirty--;
 	}
 
-	public static function draw() {
+	static draw = () => {
 		if (!paintEnabled()) return;
 
 		commandsPaint();
 
 		if (Context.raw.brushBlendDirty) {
 			Context.raw.brushBlendDirty = false;
-			#if krom_metal
+			///if krom_metal
 			path.setTarget("texpaint_blend0");
 			path.clearTarget(0x00000000);
 			path.setTarget("texpaint_blend1");
 			path.clearTarget(0x00000000);
-			#else
+			///else
 			path.setTarget("texpaint_blend0", ["texpaint_blend1"]);
 			path.clearTarget(0x00000000);
-			#end
+			///end
 		}
 	}
 
-	public static function bindLayers() {
-		var image: Image = null;
-		var nodes = UINodes.inst.getNodes();
-		var canvas = UINodes.inst.getCanvas(true);
+	static bindLayers = () => {
+		let image: Image = null;
+		let nodes = UINodes.inst.getNodes();
+		let canvas = UINodes.inst.getCanvas(true);
 		if (nodes.nodesSelectedId.length > 0) {
-			var node = nodes.getNode(canvas.nodes, nodes.nodesSelectedId[0]);
-			var brushNode = ParserLogic.getLogicNode(node);
+			let node = nodes.getNode(canvas.nodes, nodes.nodesSelectedId[0]);
+			let brushNode = ParserLogic.getLogicNode(node);
 			if (brushNode != null) {
 				image = brushNode.getCachedImage();
 			}
 		}
 		if (image != null) {
 			if (path.renderTargets.get("texpaint_node") == null) {
-				var t = new RenderTargetRaw();
+				let t = new RenderTargetRaw();
 				t.name = "texpaint_node";
 				t.width = Config.getTextureResX();
 				t.height = Config.getTextureResY();
 				t.format = "RGBA32";
-				var rt = new RenderTarget(t);
+				let rt = new RenderTarget(t);
 				path.renderTargets.set(t.name, rt);
 			}
 			if (path.renderTargets.get("texpaint_node_target") == null) {
-				var t = new RenderTargetRaw();
+				let t = new RenderTargetRaw();
 				t.name = "texpaint_node_target";
 				t.width = Config.getTextureResX();
 				t.height = Config.getTextureResY();
 				t.format = "RGBA32";
-				var rt = new RenderTarget(t);
+				let rt = new RenderTarget(t);
 				path.renderTargets.set(t.name, rt);
 			}
 			path.renderTargets.get("texpaint_node").image = image;
@@ -298,12 +289,12 @@ class RenderPathPaint {
 			path.bindTarget("texpaint_nor_empty", "texpaint_nor");
 			path.bindTarget("texpaint_pack_empty", "texpaint_pack");
 
-			var nodes = UINodes.inst.getNodes();
-			var canvas = UINodes.inst.getCanvas(true);
-			var node = nodes.getNode(canvas.nodes, nodes.nodesSelectedId[0]);
-			var inpaint = node.type == "InpaintNode";
+			let nodes = UINodes.inst.getNodes();
+			let canvas = UINodes.inst.getCanvas(true);
+			let node = nodes.getNode(canvas.nodes, nodes.nodesSelectedId[0]);
+			let inpaint = node.type == "InpaintNode";
 			if (inpaint) {
-				var brushNode = ParserLogic.getLogicNode(node);
+				let brushNode = ParserLogic.getLogicNode(node);
 				path.renderTargets.get("texpaint_node_target").image = cast(brushNode, arm.nodes.InpaintNode).getTarget();
 			}
 		}
@@ -314,19 +305,19 @@ class RenderPathPaint {
 		}
 	}
 
-	public static function unbindLayers() {
+	static unbindLayers = () => {
 
 	}
 
-	static function u32(ar: Array<Int>): js.lib.Uint32Array {
-		var res = new js.lib.Uint32Array(ar.length);
-		for (i in 0...ar.length) res[i] = ar[i];
+	static array_u32 = (ar: i32[]): Uint32Array => {
+		let res = new Uint32Array(ar.length);
+		for (let i = 0; i < ar.length; ++i) res[i] = ar[i];
 		return res;
 	}
 
-	static function i16(ar: Array<Int>): js.lib.Int16Array {
-		var res = new js.lib.Int16Array(ar.length);
-		for (i in 0...ar.length) res[i] = ar[i];
+	static array_i16 = (ar: i32[]): Int16Array => {
+		let res = new Int16Array(ar.length);
+		for (let i = 0; i < ar.length; ++i) res[i] = ar[i];
 		return res;
 	}
 }

+ 30 - 40
armorlab/Sources/arm/UINodesExt.hx → armorlab/Sources/UINodesExt.ts

@@ -1,63 +1,53 @@
-package arm;
-
-import js.lib.DataView;
-import js.lib.ArrayBuffer;
-import iron.App;
-import iron.System;
-import iron.Time;
-import iron.Scene;
-import iron.RenderPath;
-import iron.ConstData;
 
 class UINodesExt {
 
-	static var lastVertices: DataView = null; // Before displacement
+	static lastVertices: DataView = null; // Before displacement
 
-	public static function drawButtons(ew: Float, startY: Float) {
-		var ui = UINodes.inst.ui;
+	static drawButtons = (ew: f32, startY: f32) => {
+		let ui = UINodes.inst.ui;
 		if (ui.button(tr("Run"))) {
 			Console.progress(tr("Processing"));
 
-			function delayIdleSleep(_) {
+			let delayIdleSleep = (_) => {
 				Krom.delayIdleSleep();
 			}
 			App.notifyOnRender2D(delayIdleSleep);
 
-			var tasks = 1;
+			let tasks = 1;
 
-			function taskDone() {
+			let taskDone = () => {
 				tasks--;
 				if (tasks == 0) {
 					Console.progress(null);
 					Context.raw.ddirty = 2;
 					App.removeRender2D(delayIdleSleep);
 
-					#if (krom_direct3d12 || krom_vulkan || krom_metal)
+					///if (krom_direct3d12 || krom_vulkan || krom_metal)
 					RenderPathRaytrace.ready = false;
-					#end
+					///end
 				}
 			}
 
-			Base.notifyOnNextFrame(function() {
-				var timer = Time.time();
+			Base.notifyOnNextFrame(() => {
+				let timer = Time.time();
 				ParserLogic.parse(Project.canvas);
 
 				arm.nodes.PhotoToPBRNode.cachedSource = null;
-				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelBaseColor, function(texbase: Image) {
-				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelOcclusion, function(texocc: Image) {
-				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelRoughness, function(texrough: Image) {
-				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelNormalMap, function(texnor: Image) {
-				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelHeight, function(texheight: Image) {
+				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelBaseColor, (texbase: Image) => {
+				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelOcclusion, (texocc: Image) => {
+				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelRoughness, (texrough: Image) => {
+				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelNormalMap, (texnor: Image) => {
+				arm.nodes.BrushOutputNode.inst.getAsImage(ChannelHeight, (texheight: Image) => {
 
 					if (texbase != null) {
-						var texpaint = RenderPath.active.renderTargets.get("texpaint").image;
+						let texpaint = RenderPath.active.renderTargets.get("texpaint").image;
 						texpaint.g2.begin(false);
 						texpaint.g2.drawScaledImage(texbase, 0, 0, Config.getTextureResX(), Config.getTextureResY());
 						texpaint.g2.end();
 					}
 
 					if (texnor != null) {
-						var texpaint_nor = RenderPath.active.renderTargets.get("texpaint_nor").image;
+						let texpaint_nor = RenderPath.active.renderTargets.get("texpaint_nor").image;
 						texpaint_nor.g2.begin(false);
 						texpaint_nor.g2.drawScaledImage(texnor, 0, 0, Config.getTextureResX(), Config.getTextureResY());
 						texpaint_nor.g2.end();
@@ -67,7 +57,7 @@ class UINodesExt {
 					if (Base.pipeCopyA == null) Base.makePipeCopyA();
 					if (ConstData.screenAlignedVB == null) ConstData.createScreenAlignedData();
 
-					var texpaint_pack = RenderPath.active.renderTargets.get("texpaint_pack").image;
+					let texpaint_pack = RenderPath.active.renderTargets.get("texpaint_pack").image;
 
 					if (texocc != null) {
 						texpaint_pack.g2.begin(false);
@@ -95,20 +85,20 @@ class UINodesExt {
 						texpaint_pack.g4.end();
 
 						if (UIHeader.inst.worktab.position == Space3D &&
-							!Std.isOfType(arm.nodes.BrushOutputNode.inst.inputs[ChannelHeight].node, arm.nodes.FloatNode)) {
+							BrushOutputNode.inst.inputs[ChannelHeight].node.constructor != FloatNode) {
 
 							// Make copy of vertices before displacement
-							var o = Project.paintObjects[0];
-							var g = o.data;
-							var vertices = g.vertexBuffer.lock();
+							let o = Project.paintObjects[0];
+							let g = o.data;
+							let vertices = g.vertexBuffer.lock();
 							if (lastVertices == null || lastVertices.byteLength != vertices.byteLength) {
 								lastVertices = new DataView(new ArrayBuffer(vertices.byteLength));
-								for (i in 0...Std.int(vertices.byteLength / 2)) {
+								for (let i = 0; i < Math.floor(vertices.byteLength / 2); ++i) {
 									lastVertices.setInt16(i * 2, vertices.getInt16(i * 2, true), true);
 								}
 							}
 							else {
-								for (i in 0...Std.int(vertices.byteLength / 2)) {
+								for (let i = 0; i < Math.floor(vertices.byteLength / 2); ++i) {
 									vertices.setInt16(i * 2, lastVertices.getInt16(i * 2, true), true);
 								}
 							}
@@ -117,10 +107,10 @@ class UINodesExt {
 							// Apply displacement
 							if (Config.raw.displace_strength > 0) {
 								tasks++;
-								Base.notifyOnNextFrame(function() {
+								Base.notifyOnNextFrame(() => {
 									Console.progress(tr("Apply Displacement"));
-									Base.notifyOnNextFrame(function() {
-										var uv_scale = Scene.active.meshes[0].data.scaleTex * Context.raw.brushScale;
+									Base.notifyOnNextFrame(() => {
+										let uv_scale = Scene.active.meshes[0].data.scaleTex * Context.raw.brushScale;
 										UtilMesh.applyDisplacement(texpaint_pack, 0.05 * Config.raw.displace_strength, uv_scale);
 										UtilMesh.calcNormals();
 										taskDone();
@@ -144,11 +134,11 @@ class UINodesExt {
 		ui._x += ew + 3;
 		ui._y = 2 + startY;
 
-		#if (krom_android || krom_ios)
+		///if (krom_android || krom_ios)
 		ui.combo(Base.resHandle, ["2K", "4K"], tr("Resolution"));
-		#else
+		///else
 		ui.combo(Base.resHandle, ["2K", "4K", "8K", "16K"], tr("Resolution"));
-		#end
+		///end
 		if (Base.resHandle.changed) {
 			Base.onLayersResized();
 		}

+ 0 - 124
armorlab/Sources/arm/MakeMaterial.hx

@@ -1,124 +0,0 @@
-package arm;
-
-import zui.Zui.Nodes;
-import iron.SceneFormat;
-import iron.ShaderData;
-import iron.MaterialData;
-
-class MakeMaterial {
-
-	public static var defaultScon: ShaderContext = null;
-	public static var defaultMcon: MaterialContext = null;
-	public static var heightUsed = false;
-
-	public static function parseMeshMaterial() {
-		var m = Project.materialData;
-
-		for (c in m.shader.contexts) {
-			if (c.raw.name == "mesh") {
-				m.shader.raw.contexts.remove(c.raw);
-				m.shader.contexts.remove(c);
-				deleteContext(c);
-				break;
-			}
-		}
-
-		var con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }));
-		var scon = new ShaderContext(con.data, function(scon: ShaderContext){});
-		scon.overrideContext = {};
-		if (con.frag.sharedSamplers.length > 0) {
-			var sampler = con.frag.sharedSamplers[0];
-			scon.overrideContext.shared_sampler = sampler.substr(sampler.lastIndexOf(" ") + 1);
-		}
-		if (!Context.raw.textureFilter) {
-			scon.overrideContext.filter = "point";
-		}
-		scon.overrideContext.addressing = "repeat";
-		m.shader.raw.contexts.push(scon.raw);
-		m.shader.contexts.push(scon);
-
-		Context.raw.ddirty = 2;
-
-		#if arm_voxels
-		makeVoxel(m);
-		#end
-
-		#if (krom_direct3d12 || krom_vulkan)
-		RenderPathRaytrace.dirty = 1;
-		#end
-	}
-
-	#if arm_voxels
-	static function makeVoxel(m: MaterialData) {
-		var rebuild = true; // heightUsed;
-		if (Config.raw.rp_gi != false && rebuild) {
-			var scon: ShaderContext = null;
-			for (c in m.shader.contexts) {
-				if (c.raw.name == "voxel") {
-					scon = c;
-					break;
-				}
-			}
-			if (scon != null) MakeVoxel.run(scon);
-		}
-	}
-	#end
-
-	public static function parsePaintMaterial() {
-		var m = Project.materialData;
-		var scon: ShaderContext = null;
-		var mcon: MaterialContext = null;
-		for (c in m.shader.contexts) {
-			if (c.raw.name == "paint") {
-				m.shader.raw.contexts.remove(c.raw);
-				m.shader.contexts.remove(c);
-				if (c != defaultScon) deleteContext(c);
-				break;
-			}
-		}
-		for (c in m.contexts) {
-			if (c.raw.name == "paint") {
-				m.raw.contexts.remove(c.raw);
-				m.contexts.remove(c);
-				break;
-			}
-		}
-
-		var sdata = new NodeShaderData({ name: "Material", canvas: null });
-		var mcon: TMaterialContext = { name: "paint", bind_textures: [] };
-		var con = MakePaint.run(sdata, mcon);
-
-		var compileError = false;
-		var scon = new ShaderContext(con.data, function(scon: ShaderContext) {
-			if (scon == null) compileError = true;
-		});
-		if (compileError) return;
-		scon.overrideContext = {};
-		scon.overrideContext.addressing = "repeat";
-		var mcon = new MaterialContext(mcon, function(mcon: MaterialContext) {});
-
-		m.shader.raw.contexts.push(scon.raw);
-		m.shader.contexts.push(scon);
-		m.raw.contexts.push(mcon.raw);
-		m.contexts.push(mcon);
-
-		if (defaultScon == null) defaultScon = scon;
-		if (defaultMcon == null) defaultMcon = mcon;
-	}
-
-	public static inline function getDisplaceStrength():Float {
-		var sc = Context.mainObject().transform.scale.x;
-		return Config.raw.displace_strength * 0.02 * sc;
-	}
-
-	public static inline function voxelgiHalfExtents():String {
-		var ext = Context.raw.vxaoExt;
-		return 'const vec3 voxelgiHalfExtents = vec3($ext, $ext, $ext);';
-	}
-
-	static function deleteContext(c: ShaderContext) {
-		Base.notifyOnNextFrame(function() { // Ensure pipeline is no longer in use
-			c.delete();
-		});
-	}
-}

+ 0 - 39
armorlab/Sources/arm/NodesBrush.hx

@@ -1,39 +0,0 @@
-package arm;
-
-import zui.Zui.Nodes;
-import arm.Translator._tr;
-
-class NodesBrush {
-
-	public static var categories = [_tr("Input"), _tr("Model")];
-
-	public static var list: Array<Array<zui.Zui.TNode>> = [
-		[ // Input
-			arm.nodes.ImageTextureNode.def,
-			arm.nodes.RGBNode.def,
-		],
-		[ // Model
-			arm.nodes.InpaintNode.def,
-			arm.nodes.PhotoToPBRNode.def,
-			arm.nodes.TextToPhotoNode.def,
-			arm.nodes.TilingNode.def,
-			arm.nodes.UpscaleNode.def,
-			arm.nodes.VarianceNode.def,
-		]
-	];
-
-	public static function createNode(nodeType: String): zui.Zui.TNode {
-		for (c in list) {
-			for (n in c) {
-				if (n.type == nodeType) {
-					var canvas = Project.canvas;
-					var nodes = Project.nodes;
-					var node = arm.UINodes.makeNode(n, nodes, canvas);
-					canvas.nodes.push(node);
-					return node;
-				}
-			}
-		}
-		return null;
-	}
-}

+ 0 - 228
armorlab/Sources/arm/nodes/InpaintNode.hx

@@ -1,228 +0,0 @@
-package arm.nodes;
-
-import zui.Zui.Nodes;
-import iron.System;
-import iron.Data;
-import iron.ConstData;
-import arm.LogicNode;
-import arm.ParserLogic.f32;
-import arm.Translator._tr;
-
-@:keep
-class InpaintNode extends LogicNode {
-
-	public static var image: Image = null;
-	public static var mask: Image = null;
-	static var result: Image = null;
-
-	static var temp: Image = null;
-	public static var prompt = "";
-	public static var strength = 0.5;
-	static var auto = true;
-
-	public function new() {
-		super();
-
-		init();
-	}
-
-	public static function init() {
-		if (image == null) {
-			image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY());
-		}
-
-		if (mask == null) {
-			mask = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY(), TextureFormat.R8);
-			Base.notifyOnNextFrame(function() {
-				mask.g4.begin();
-				mask.g4.clear(Color.fromFloats(1.0, 1.0, 1.0, 1.0));
-				mask.g4.end();
-			});
-		}
-
-		if (temp == null) {
-			temp = Image.createRenderTarget(512, 512);
-		}
-
-		if (result == null) {
-			result = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY());
-		}
-	}
-
-	public static function buttons(ui: zui.Zui, nodes: zui.Zui.Nodes, node: zui.Zui.TNode) {
-		auto = node.buttons[0].default_value == 0 ? false : true;
-		if (!auto) {
-			strength = ui.slider(zui.Zui.handle("inpaintnode_0", {value: strength}), tr("strength"), 0, 1, true);
-			prompt = ui.textArea(zui.Zui.handle("inpaintnode_1"), true, tr("prompt"), true);
-			node.buttons[1].height = 1 + prompt.split("\n").length;
-		}
-		else node.buttons[1].height = 0;
-	}
-
-	override function getAsImage(from: Int, done: Image->Void) {
-		inputs[0].getAsImage(function(source: Image) {
-
-			Console.progress(tr("Processing") + " - " + tr("Inpaint"));
-			Base.notifyOnNextFrame(function() {
-				image.g2.begin(false);
-				image.g2.drawScaledImage(source, 0, 0, Config.getTextureResX(), Config.getTextureResY());
-				image.g2.end();
-
-				auto ? texsynthInpaint(image, false, mask, done) : sdInpaint(image, mask, done);
-			});
-		});
-	}
-
-	override public function getCachedImage(): Image {
-		Base.notifyOnNextFrame(function() {
-			inputs[0].getAsImage(function(source: Image) {
-				if (Base.pipeCopy == null) Base.makePipe();
-				if (ConstData.screenAlignedVB == null) ConstData.createScreenAlignedData();
-				image.g4.begin();
-				image.g4.setPipeline(Base.pipeInpaintPreview);
-				image.g4.setTexture(Base.tex0InpaintPreview, source);
-				image.g4.setTexture(Base.texaInpaintPreview, mask);
-				image.g4.setVertexBuffer(ConstData.screenAlignedVB);
-				image.g4.setIndexBuffer(ConstData.screenAlignedIB);
-				image.g4.drawIndexedVertices();
-				image.g4.end();
-			});
-		});
-		return image;
-	}
-
-	public function getTarget(): Image {
-		return mask;
-	}
-
-	public static function texsynthInpaint(image: Image, tiling: Bool, mask: Image/* = null*/, done: Image->Void) {
-		var w = arm.Config.getTextureResX();
-		var h = arm.Config.getTextureResY();
-
-		var bytes_img = image.getPixels();
-		var bytes_mask = mask != null ? mask.getPixels() : new js.lib.ArrayBuffer(w * h);
-		var bytes_out = new js.lib.ArrayBuffer(w * h * 4);
-		untyped Krom_texsynth.inpaint(w, h, bytes_out, bytes_img, bytes_mask, tiling);
-
-		result = Image.fromBytes(bytes_out, w, h);
-		done(result);
-	}
-
-	public static function sdInpaint(image: Image, mask: Image, done: Image->Void) {
-		init();
-
-		var bytes_img = untyped mask.getPixels().b.buffer;
-		var u8 = new js.lib.Uint8Array(untyped bytes_img);
-		var f32mask = new js.lib.Float32Array(4 * 64 * 64);
-
-		Data.getBlob("models/sd_vae_encoder.quant.onnx", function(vae_encoder_blob: js.lib.ArrayBuffer) {
-			// for (x in 0...Std.int(image.width / 512)) {
-				// for (y in 0...Std.int(image.height / 512)) {
-					var x = 0;
-					var y = 0;
-
-					for (xx in 0...64) {
-						for (yy in 0...64) {
-							// var step = Std.int(512 / 64);
-							// var j = (yy * step * mask.width + xx * step) + (y * 512 * mask.width + x * 512);
-							var step = Std.int(mask.width / 64);
-							var j = (yy * step * mask.width + xx * step);
-							var f = u8[j] / 255.0;
-							var i = yy * 64 + xx;
-							f32mask[i              ] = f;
-							f32mask[i + 64 * 64    ] = f;
-							f32mask[i + 64 * 64 * 2] = f;
-							f32mask[i + 64 * 64 * 3] = f;
-						}
-					}
-
-					temp.g2.begin(false);
-					// temp.g2.drawImage(image, -x * 512, -y * 512);
-					temp.g2.drawScaledImage(image, 0, 0, 512, 512);
-					temp.g2.end();
-
-					var bytes_img = untyped temp.getPixels().b.buffer;
-					var u8 = new js.lib.Uint8Array(untyped bytes_img);
-					var f32 = new js.lib.Float32Array(3 * 512 * 512);
-					for (i in 0...(512 * 512)) {
-						f32[i                ] = (u8[i * 4    ] / 255.0) * 2.0 - 1.0;
-						f32[i + 512 * 512    ] = (u8[i * 4 + 1] / 255.0) * 2.0 - 1.0;
-						f32[i + 512 * 512 * 2] = (u8[i * 4 + 2] / 255.0) * 2.0 - 1.0;
-					}
-
-					var latents_buf = Krom.mlInference(untyped vae_encoder_blob, [f32.buffer], [[1, 3, 512, 512]], [1, 4, 64, 64], Config.raw.gpu_inference);
-					var latents = new js.lib.Float32Array(latents_buf);
-					for (i in 0...latents.length) {
-						latents[i] = 0.18215 * latents[i];
-					}
-					var latents_orig = latents.slice(0);
-
-					var noise = new js.lib.Float32Array(latents.length);
-					for (i in 0...noise.length) noise[i] = Math.cos(2.0 * 3.14 * RandomNode.getFloat()) * Math.sqrt(-2.0 * Math.log(RandomNode.getFloat()));
-
-					var num_inference_steps = 50;
-					var init_timestep = Std.int(num_inference_steps * strength);
-					var timestep = TextToPhotoNode.timesteps[num_inference_steps - init_timestep];
-					var alphas_cumprod = TextToPhotoNode.alphas_cumprod;
-					var sqrt_alpha_prod = Math.pow(alphas_cumprod[timestep], 0.5);
-					var sqrt_one_minus_alpha_prod = Math.pow(1.0 - alphas_cumprod[timestep], 0.5);
-					for (i in 0...latents.length) {
-						latents[i] = sqrt_alpha_prod * latents[i] + sqrt_one_minus_alpha_prod * noise[i];
-					}
-
-					var start = num_inference_steps - init_timestep;
-
-					TextToPhotoNode.stableDiffusion(prompt, function(img: Image) {
-						// result.g2.begin(false);
-						// result.g2.drawImage(img, x * 512, y * 512);
-						// result.g2.end();
-						result = img;
-						done(img);
-					}, latents, start, true, f32mask, latents_orig);
-				// }
-			// }
-		});
-	}
-
-	public static var def: zui.Zui.TNode = {
-		id: 0,
-		name: _tr("Inpaint"),
-		type: "InpaintNode",
-		x: 0,
-		y: 0,
-		color: 0xff4982a0,
-		inputs: [
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Color"),
-				type: "RGBA",
-				color: 0xffc7c729,
-				default_value: f32([1.0, 1.0, 1.0, 1.0])
-			}
-		],
-		outputs: [
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Color"),
-				type: "RGBA",
-				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
-			}
-		],
-		buttons: [
-			{
-				name: _tr("auto"),
-				type: "BOOL",
-				default_value: true,
-				output: 0
-			},
-			{
-				name: "arm.nodes.InpaintNode.buttons",
-				type: "CUSTOM",
-				height: 0
-			}
-		]
-	};
-}

+ 0 - 249
armorlab/Sources/arm/nodes/PhotoToPBRNode.hx

@@ -1,249 +0,0 @@
-package arm.nodes;
-
-import zui.Zui.Nodes;
-import iron.System;
-import iron.Data;
-import arm.LogicNode;
-import arm.ParserLogic.f32;
-import arm.Translator._tr;
-
-@:keep
-class PhotoToPBRNode extends LogicNode {
-
-	static var temp: Image = null;
-	public static var images: Array<Image> = null;
-	static var modelNames = ["base", "occlusion", "roughness", "metallic", "normal", "height"];
-
-	public static var cachedSource: Image = null;
-	static inline var borderW = 64;
-	static inline var tileW = 2048;
-	static inline var tileWithBorderW = tileW + borderW * 2;
-
-	public function new() {
-		super();
-
-		if (temp == null) {
-			temp = Image.createRenderTarget(tileWithBorderW, tileWithBorderW);
-		}
-
-		init();
-	}
-
-	public static function init() {
-		if (images == null) {
-			images = [];
-			for (i in 0...modelNames.length) {
-				images.push(Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY()));
-			}
-		}
-	}
-
-	override function getAsImage(from: Int, done: Image->Void) {
-		function getSource(done: Image->Void) {
-			if (cachedSource != null) done(cachedSource);
-			else inputs[0].getAsImage(done);
-		}
-
-		getSource(function(source: Image) {
-			cachedSource = source;
-
-			Console.progress(tr("Processing") + " - " + tr("Photo to PBR"));
-			Base.notifyOnNextFrame(function() {
-				var tileFloats: Array<js.lib.Float32Array> = [];
-				var tilesX = Std.int(Config.getTextureResX() / tileW);
-				var tilesY = Std.int(Config.getTextureResY() / tileW);
-				var numTiles = tilesX * tilesY;
-				for (i in 0...numTiles) {
-					var x = i % tilesX;
-					var y = Std.int(i / tilesX);
-
-					temp.g2.begin(false);
-					temp.g2.drawScaledImage(source, borderW - x * tileW, borderW - y * tileW, -Config.getTextureResX(), Config.getTextureResY());
-					temp.g2.drawScaledImage(source, borderW - x * tileW, borderW - y * tileW, Config.getTextureResX(), -Config.getTextureResY());
-					temp.g2.drawScaledImage(source, borderW - x * tileW, borderW - y * tileW, -Config.getTextureResX(), -Config.getTextureResY());
-					temp.g2.drawScaledImage(source, borderW - x * tileW + tileW, borderW - y * tileW + tileW, Config.getTextureResX(), Config.getTextureResY());
-					temp.g2.drawScaledImage(source, borderW - x * tileW + tileW, borderW - y * tileW + tileW, -Config.getTextureResX(), Config.getTextureResY());
-					temp.g2.drawScaledImage(source, borderW - x * tileW + tileW, borderW - y * tileW + tileW, Config.getTextureResX(), -Config.getTextureResY());
-					temp.g2.drawScaledImage(source, borderW - x * tileW, borderW - y * tileW, Config.getTextureResX(), Config.getTextureResY());
-					temp.g2.end();
-
-					var bytes_img = temp.getPixels();
-					var u8 = new js.lib.Uint8Array(bytes_img);
-					var f32 = new js.lib.Float32Array(3 * tileWithBorderW * tileWithBorderW);
-					for (i in 0...(tileWithBorderW * tileWithBorderW)) {
-						f32[i                                        ] = (u8[i * 4    ] / 255 - 0.5) / 0.5;
-						f32[i + tileWithBorderW * tileWithBorderW    ] = (u8[i * 4 + 1] / 255 - 0.5) / 0.5;
-						f32[i + tileWithBorderW * tileWithBorderW * 2] = (u8[i * 4 + 2] / 255 - 0.5) / 0.5;
-					}
-
-					Data.getBlob("models/photo_to_" + modelNames[from] + ".quant.onnx", function(model_blob: js.lib.ArrayBuffer) {
-						var buf = Krom.mlInference(untyped model_blob, [f32.buffer], null, null, Config.raw.gpu_inference);
-						var ar = new js.lib.Float32Array(buf);
-						var u8 = new js.lib.Uint8Array(4 * tileW * tileW);
-						var offsetG = (from == ChannelBaseColor || from == ChannelNormalMap) ? tileWithBorderW * tileWithBorderW : 0;
-						var offsetB = (from == ChannelBaseColor || from == ChannelNormalMap) ? tileWithBorderW * tileWithBorderW * 2 : 0;
-						for (i in 0...(tileW * tileW)) {
-							var x = borderW + i % tileW;
-							var y = borderW + Std.int(i / tileW);
-							u8[i * 4    ] = Std.int((ar[y * tileWithBorderW + x          ] * 0.5 + 0.5) * 255);
-							u8[i * 4 + 1] = Std.int((ar[y * tileWithBorderW + x + offsetG] * 0.5 + 0.5) * 255);
-							u8[i * 4 + 2] = Std.int((ar[y * tileWithBorderW + x + offsetB] * 0.5 + 0.5) * 255);
-							u8[i * 4 + 3] = 255;
-						}
-						tileFloats.push(ar);
-
-						// Use border pixels to blend seams
-						if (i > 0) {
-							if (x > 0) {
-								var ar = tileFloats[i - 1];
-								for (yy in 0...tileW) {
-									for (xx in 0...borderW) {
-										var i = yy * tileW + xx;
-										var a = u8[i * 4];
-										var b = u8[i * 4 + 1];
-										var c = u8[i * 4 + 2];
-
-										var aa = Std.int((ar[(borderW + yy) * tileWithBorderW + borderW + tileW + xx          ] * 0.5 + 0.5) * 255);
-										var bb = Std.int((ar[(borderW + yy) * tileWithBorderW + borderW + tileW + xx + offsetG] * 0.5 + 0.5) * 255);
-										var cc = Std.int((ar[(borderW + yy) * tileWithBorderW + borderW + tileW + xx + offsetB] * 0.5 + 0.5) * 255);
-
-										var f = xx / borderW;
-										var invf = 1.0 - f;
-										a = Std.int(a * f + aa * invf);
-										b = Std.int(b * f + bb * invf);
-										c = Std.int(c * f + cc * invf);
-
-										u8[i * 4    ] = a;
-										u8[i * 4 + 1] = b;
-										u8[i * 4 + 2] = c;
-									}
-								}
-							}
-							if (y > 0) {
-								var ar = tileFloats[i - tilesX];
-								for (xx in 0...tileW) {
-									for (yy in 0...borderW) {
-										var i = yy * tileW + xx;
-										var a = u8[i * 4];
-										var b = u8[i * 4 + 1];
-										var c = u8[i * 4 + 2];
-
-										var aa = Std.int((ar[(borderW + tileW + yy) * tileWithBorderW + borderW + xx          ] * 0.5 + 0.5) * 255);
-										var bb = Std.int((ar[(borderW + tileW + yy) * tileWithBorderW + borderW + xx + offsetG] * 0.5 + 0.5) * 255);
-										var cc = Std.int((ar[(borderW + tileW + yy) * tileWithBorderW + borderW + xx + offsetB] * 0.5 + 0.5) * 255);
-
-										var f = yy / borderW;
-										var invf = 1.0 - f;
-										a = Std.int(a * f + aa * invf);
-										b = Std.int(b * f + bb * invf);
-										c = Std.int(c * f + cc * invf);
-
-										u8[i * 4    ] = a;
-										u8[i * 4 + 1] = b;
-										u8[i * 4 + 2] = c;
-									}
-								}
-							}
-						}
-
-						#if (krom_metal || krom_vulkan)
-						if (from == ChannelBaseColor) bgraSwap(u8.buffer);
-						#end
-
-						var temp2 = Image.fromBytes(u8.buffer, tileW, tileW);
-						images[from].g2.begin(false);
-						images[from].g2.drawImage(temp2, x * tileW, y * tileW);
-						images[from].g2.end();
-						Base.notifyOnNextFrame(function() {
-							temp2.unload();
-						});
-					});
-				}
-
-				done(images[from]);
-			});
-		});
-	}
-
-	#if (krom_metal || krom_vulkan)
-	static function bgraSwap(buffer: js.lib.ArrayBuffer) {
-		var u8 = new js.lib.Uint8Array(buffer);
-		for (i in 0...Std.int(buffer.byteLength / 4)) {
-			var r = u8[i * 4];
-			u8[i * 4] = u8[i * 4 + 2];
-			u8[i * 4 + 2] = r;
-		}
-		return buffer;
-	}
-	#end
-
-	public static var def: zui.Zui.TNode = {
-		id: 0,
-		name: _tr("Photo to PBR"),
-		type: "PhotoToPBRNode",
-		x: 0,
-		y: 0,
-		color: 0xff4982a0,
-		inputs: [
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Color"),
-				type: "RGBA",
-				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
-			}
-		],
-		outputs: [
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Base Color"),
-				type: "RGBA",
-				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
-			},
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Occlusion"),
-				type: "VALUE",
-				color: 0xffa1a1a1,
-				default_value: 1.0
-			},
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Roughness"),
-				type: "VALUE",
-				color: 0xffa1a1a1,
-				default_value: 1.0
-			},
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Metallic"),
-				type: "VALUE",
-				color: 0xffa1a1a1,
-				default_value: 0.0
-			},
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Normal Map"),
-				type: "VECTOR",
-				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
-			},
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Height"),
-				type: "VALUE",
-				color: 0xffa1a1a1,
-				default_value: 1.0
-			}
-		],
-		buttons: []
-	};
-}

+ 0 - 69
armorlab/Sources/arm/nodes/RGBNode.hx

@@ -1,69 +0,0 @@
-package arm.nodes;
-
-import zui.Zui.Nodes;
-import iron.System;
-import iron.Vec4;
-import arm.LogicNode;
-import arm.ParserLogic.f32;
-import arm.Translator._tr;
-
-@:keep
-class RGBNode extends LogicNode {
-
-	var image: Image = null;
-
-	public function new() {
-		super();
-	}
-
-	override function getAsImage(from: Int, done: Image->Void) {
-		if (image != null) {
-			Base.notifyOnNextFrame(function() {
-				image.unload();
-			});
-		}
-
-		var f32 = new js.lib.Float32Array(4);
-		var raw = ParserLogic.getRawNode(this);
-		var default_value = raw.outputs[0].default_value;
-		f32[0] = default_value[0];
-		f32[1] = default_value[1];
-		f32[2] = default_value[2];
-		f32[3] = default_value[3];
-		image = Image.fromBytes(f32.buffer, 1, 1, TextureFormat.RGBA128);
-		done(image);
-	}
-
-	override public function getCachedImage(): Image {
-		getAsImage(0, function(img: Image) {});
-		return image;
-	}
-
-	public static var def: zui.Zui.TNode = {
-		id: 0,
-		name: _tr("RGB"),
-		type: "RGBNode",
-		x: 0,
-		y: 0,
-		color: 0xffb34f5a,
-		inputs: [],
-		outputs: [
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Color"),
-				type: "RGBA",
-				color: 0xffc7c729,
-				default_value: f32([0.8, 0.8, 0.8, 1.0])
-			}
-		],
-		buttons: [
-			{
-				name: _tr("default_value"),
-				type: "RGBA",
-				output: 0,
-				default_value: f32([0.8, 0.8, 0.8, 1.0])
-			}
-		]
-	};
-}

Fișier diff suprimat deoarece este prea mare
+ 0 - 478
armorlab/Sources/arm/nodes/TextToPhotoNode.hx


+ 0 - 132
armorlab/Sources/arm/nodes/VarianceNode.hx

@@ -1,132 +0,0 @@
-package arm.nodes;
-
-import zui.Zui.Nodes;
-import iron.System;
-import iron.Data;
-import arm.LogicNode;
-import arm.ParserLogic.f32;
-import arm.Translator._tr;
-
-@:keep
-class VarianceNode extends LogicNode {
-
-	static var temp: Image = null;
-	static var image: Image = null;
-	static var inst: VarianceNode = null;
-	static var prompt = "";
-
-	public function new() {
-		super();
-
-		inst = this;
-
-		init();
-	}
-
-	public static function init() {
-		if (temp == null) {
-			temp = Image.createRenderTarget(512, 512);
-		}
-	}
-
-	public static function buttons(ui: zui.Zui, nodes: zui.Zui.Nodes, node: zui.Zui.TNode) {
-		prompt = ui.textArea(zui.Zui.handle("variancenode_0"), true, tr("prompt"), true);
-		node.buttons[0].height = prompt.split("\n").length;
-	}
-
-	override function getAsImage(from: Int, done: Image->Void) {
-		var strength = untyped inst.inputs[1].node.value;
-
-		inst.inputs[0].getAsImage(function(source: Image) {
-			temp.g2.begin(false);
-			temp.g2.drawScaledImage(source, 0, 0, 512, 512);
-			temp.g2.end();
-
-			var bytes_img = untyped temp.getPixels().b.buffer;
-			var u8 = new js.lib.Uint8Array(untyped bytes_img);
-			var f32 = new js.lib.Float32Array(3 * 512 * 512);
-			for (i in 0...(512 * 512)) {
-				f32[i                ] = (u8[i * 4    ] / 255) * 2.0 - 1.0;
-				f32[i + 512 * 512    ] = (u8[i * 4 + 1] / 255) * 2.0 - 1.0;
-				f32[i + 512 * 512 * 2] = (u8[i * 4 + 2] / 255) * 2.0 - 1.0;
-			}
-
-			Console.progress(tr("Processing") + " - " + tr("Variance"));
-			Base.notifyOnNextFrame(function() {
-				Data.getBlob("models/sd_vae_encoder.quant.onnx", function(vae_encoder_blob: js.lib.ArrayBuffer) {
-					var latents_buf = Krom.mlInference(vae_encoder_blob, [f32.buffer], [[1, 3, 512, 512]], [1, 4, 64, 64], Config.raw.gpu_inference);
-					var latents = new js.lib.Float32Array(latents_buf);
-					for (i in 0...latents.length) {
-						latents[i] = 0.18215 * latents[i];
-					}
-
-					var noise = new js.lib.Float32Array(latents.length);
-					for (i in 0...noise.length) noise[i] = Math.cos(2.0 * 3.14 * RandomNode.getFloat()) * Math.sqrt(-2.0 * Math.log(RandomNode.getFloat()));
-					var num_inference_steps = 50;
-					var init_timestep = Std.int(num_inference_steps * strength);
-					var timesteps = TextToPhotoNode.timesteps[num_inference_steps - init_timestep];
-					var alphas_cumprod = TextToPhotoNode.alphas_cumprod;
-					var sqrt_alpha_prod = Math.pow(alphas_cumprod[timesteps], 0.5);
-					var sqrt_one_minus_alpha_prod = Math.pow(1.0 - alphas_cumprod[timesteps], 0.5);
-					for (i in 0...latents.length) {
-						latents[i] = sqrt_alpha_prod * latents[i] + sqrt_one_minus_alpha_prod * noise[i];
-					}
-					var t_start = num_inference_steps - init_timestep;
-
-					TextToPhotoNode.stableDiffusion(prompt, function(_image: Image) {
-						image = _image;
-						done(image);
-					}, latents, t_start);
-				});
-			});
-		});
-	}
-
-	override public function getCachedImage(): Image {
-		return image;
-	}
-
-	public static var def: zui.Zui.TNode = {
-		id: 0,
-		name: _tr("Variance"),
-		type: "VarianceNode",
-		x: 0,
-		y: 0,
-		color: 0xff4982a0,
-		inputs: [
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Color"),
-				type: "RGBA",
-				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
-			},
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Strength"),
-				type: "VALUE",
-				color: 0xffa1a1a1,
-				default_value: 0.5
-			}
-		],
-		outputs: [
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Color"),
-				type: "RGBA",
-				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
-			}
-		],
-		buttons: [
-			{
-				name: "arm.nodes.VarianceNode.buttons",
-				type: "CUSTOM",
-				height: 1
-			}
-		]
-	};
-}

+ 0 - 5
armorlab/Sources/import.hx

@@ -1,5 +0,0 @@
-// Global imports
-
-import arm.Translator.tr;
-import arm.Enums;
-using StringTools;

+ 15 - 20
armorlab/Sources/arm/nodes/BrushOutputNode.hx → armorlab/Sources/nodes/BrushOutputNode.ts

@@ -1,27 +1,22 @@
-package arm.nodes;
 
-import iron.System;
-import iron.RenderPath;
-import arm.LogicNode;
-
-@:keep
+// @:keep
 class BrushOutputNode extends LogicNode {
 
-	public var id = 0;
-	public var texpaint: Image = null;
-	public var texpaint_nor: Image = null;
-	public var texpaint_pack: Image = null;
-	public var texpaint_nor_empty: Image = null;
-	public var texpaint_pack_empty: Image = null;
+	id = 0;
+	texpaint: Image = null;
+	texpaint_nor: Image = null;
+	texpaint_pack: Image = null;
+	texpaint_nor_empty: Image = null;
+	texpaint_pack_empty: Image = null;
 
-	public static var inst: BrushOutputNode = null;
+	static inst: BrushOutputNode = null;
 
-	public function new() {
+	constructor() {
 		super();
 
 		if (inst == null) {
 			{
-				var t = new RenderTargetRaw();
+				let t = new RenderTargetRaw();
 				t.name = "texpaint";
 				t.width = Config.getTextureResX();
 				t.height = Config.getTextureResY();
@@ -29,7 +24,7 @@ class BrushOutputNode extends LogicNode {
 				texpaint = RenderPath.active.createRenderTarget(t).image;
 			}
 			{
-				var t = new RenderTargetRaw();
+				let t = new RenderTargetRaw();
 				t.name = "texpaint_nor";
 				t.width = Config.getTextureResX();
 				t.height = Config.getTextureResY();
@@ -37,7 +32,7 @@ class BrushOutputNode extends LogicNode {
 				texpaint_nor = RenderPath.active.createRenderTarget(t).image;
 			}
 			{
-				var t = new RenderTargetRaw();
+				let t = new RenderTargetRaw();
 				t.name = "texpaint_pack";
 				t.width = Config.getTextureResX();
 				t.height = Config.getTextureResY();
@@ -45,7 +40,7 @@ class BrushOutputNode extends LogicNode {
 				texpaint_pack = RenderPath.active.createRenderTarget(t).image;
 			}
 			{
-				var t = new RenderTargetRaw();
+				let t = new RenderTargetRaw();
 				t.name = "texpaint_nor_empty";
 				t.width = 1;
 				t.height = 1;
@@ -53,7 +48,7 @@ class BrushOutputNode extends LogicNode {
 				texpaint_nor_empty = RenderPath.active.createRenderTarget(t).image;
 			}
 			{
-				var t = new RenderTargetRaw();
+				let t = new RenderTargetRaw();
 				t.name = "texpaint_pack_empty";
 				t.width = 1;
 				t.height = 1;
@@ -70,7 +65,7 @@ class BrushOutputNode extends LogicNode {
 		inst = this;
 	}
 
-	override function getAsImage(from: Int, done: Image->Void) {
+	override getAsImage = (from: i32, done: (img: Image)=>void) => {
 		inputs[from].getAsImage(done);
 	}
 }

+ 13 - 20
armorlab/Sources/arm/nodes/ImageTextureNode.hx → armorlab/Sources/nodes/ImageTextureNode.ts

@@ -1,34 +1,27 @@
-package arm.nodes;
 
-import zui.Zui.Nodes;
-import iron.System;
-import arm.LogicNode;
-import arm.ParserLogic.f32;
-import arm.Translator._tr;
-
-@:keep
+// @:keep
 class ImageTextureNode extends LogicNode {
 
-	public var file: String;
-	public var color_space: String;
+	file: string;
+	color_space: string;
 
-	public function new() {
+	constructor() {
 		super();
 	}
 
-	override function getAsImage(from: Int, done: Image->Void) {
-		var index = Project.assetNames.indexOf(file);
-		var asset = Project.assets[index];
+	override getAsImage = (from: i32, done: (img: Image)=>void) => {
+		let index = Project.assetNames.indexOf(file);
+		let asset = Project.assets[index];
 		done(Project.getImage(asset));
 	}
 
-	override public function getCachedImage(): Image {
-		var image: Image;
-		getAsImage(0, function(img: Image) { image = img; });
+	override getCachedImage = (): Image => {
+		let image: Image;
+		getAsImage(0, (img: Image) => { image = img; });
 		return image;
 	}
 
-	public static var def: zui.Zui.TNode = {
+	static def: zui.Zui.TNode = {
 		id: 0,
 		name: _tr("Image Texture"),
 		type: "ImageTextureNode",
@@ -42,7 +35,7 @@ class ImageTextureNode extends LogicNode {
 				name: _tr("Vector"),
 				type: "VECTOR",
 				color: 0xff6363c7,
-				default_value: f32([0.0, 0.0, 0.0])
+				default_value: array_f32([0.0, 0.0, 0.0])
 			}
 		],
 		outputs: [
@@ -52,7 +45,7 @@ class ImageTextureNode extends LogicNode {
 				name: _tr("Color"),
 				type: "RGBA",
 				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
 			},
 			{
 				id: 0,

+ 218 - 0
armorlab/Sources/nodes/InpaintNode.ts

@@ -0,0 +1,218 @@
+
+// @:keep
+class InpaintNode extends LogicNode {
+
+	static image: Image = null;
+	static mask: Image = null;
+	static result: Image = null;
+
+	static temp: Image = null;
+	static prompt = "";
+	static strength = 0.5;
+	static auto = true;
+
+	constructor() {
+		super();
+		init();
+	}
+
+	static init = () => {
+		if (image == null) {
+			image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY());
+		}
+
+		if (mask == null) {
+			mask = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY(), TextureFormat.R8);
+			Base.notifyOnNextFrame(() => {
+				mask.g4.begin();
+				mask.g4.clear(Color.fromFloats(1.0, 1.0, 1.0, 1.0));
+				mask.g4.end();
+			});
+		}
+
+		if (temp == null) {
+			temp = Image.createRenderTarget(512, 512);
+		}
+
+		if (result == null) {
+			result = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY());
+		}
+	}
+
+	static buttons = (ui: zui.Zui, nodes: zui.Zui.Nodes, node: zui.Zui.TNode) => {
+		auto = node.buttons[0].default_value == 0 ? false : true;
+		if (!auto) {
+			strength = ui.slider(zui.Zui.handle("inpaintnode_0", {value: strength}), tr("strength"), 0, 1, true);
+			prompt = ui.textArea(zui.Zui.handle("inpaintnode_1"), true, tr("prompt"), true);
+			node.buttons[1].height = 1 + prompt.split("\n").length;
+		}
+		else node.buttons[1].height = 0;
+	}
+
+	override getAsImage = (from: i32, done: (img: Image)=>void) => {
+		inputs[0].getAsImage((source: Image) => {
+
+			Console.progress(tr("Processing") + " - " + tr("Inpaint"));
+			Base.notifyOnNextFrame(() => {
+				image.g2.begin(false);
+				image.g2.drawScaledImage(source, 0, 0, Config.getTextureResX(), Config.getTextureResY());
+				image.g2.end();
+
+				auto ? texsynthInpaint(image, false, mask, done) : sdInpaint(image, mask, done);
+			});
+		});
+	}
+
+	override getCachedImage = (): Image => {
+		Base.notifyOnNextFrame(() => {
+			inputs[0].getAsImage((source: Image) => {
+				if (Base.pipeCopy == null) Base.makePipe();
+				if (ConstData.screenAlignedVB == null) ConstData.createScreenAlignedData();
+				image.g4.begin();
+				image.g4.setPipeline(Base.pipeInpaintPreview);
+				image.g4.setTexture(Base.tex0InpaintPreview, source);
+				image.g4.setTexture(Base.texaInpaintPreview, mask);
+				image.g4.setVertexBuffer(ConstData.screenAlignedVB);
+				image.g4.setIndexBuffer(ConstData.screenAlignedIB);
+				image.g4.drawIndexedVertices();
+				image.g4.end();
+			});
+		});
+		return image;
+	}
+
+	getTarget = (): Image => {
+		return mask;
+	}
+
+	static texsynthInpaint = (image: Image, tiling: bool, mask: Image/* = null*/, done: (img: Image)=>void) => {
+		let w = arm.Config.getTextureResX();
+		let h = arm.Config.getTextureResY();
+
+		let bytes_img = image.getPixels();
+		let bytes_mask = mask != null ? mask.getPixels() : new ArrayBuffer(w * h);
+		let bytes_out = new ArrayBuffer(w * h * 4);
+		Krom_texsynth.inpaint(w, h, bytes_out, bytes_img, bytes_mask, tiling);
+
+		result = Image.fromBytes(bytes_out, w, h);
+		done(result);
+	}
+
+	static sdInpaint = (image: Image, mask: Image, done: (img: Image)=>void) => {
+		init();
+
+		let bytes_img = mask.getPixels().b.buffer;
+		let u8 = new Uint8Array(bytes_img);
+		let f32mask = new Float32Array(4 * 64 * 64);
+
+		Data.getBlob("models/sd_vae_encoder.quant.onnx", (vae_encoder_blob: ArrayBuffer) => {
+			// for (let x = 0; x < Math.floor(image.width / 512); ++x) {
+				// for (let y = 0; y < Math.floor(image.height / 512); ++y) {
+					let x = 0;
+					let y = 0;
+
+					for (let xx = 0; xx < 64; ++xx) {
+						for (let yy = 0; yy < 64; ++yy;) {
+							// let step = Math.floor(512 / 64);
+							// let j = (yy * step * mask.width + xx * step) + (y * 512 * mask.width + x * 512);
+							let step = Math.floor(mask.width / 64);
+							let j = (yy * step * mask.width + xx * step);
+							let f = u8[j] / 255.0;
+							let i = yy * 64 + xx;
+							f32mask[i              ] = f;
+							f32mask[i + 64 * 64    ] = f;
+							f32mask[i + 64 * 64 * 2] = f;
+							f32mask[i + 64 * 64 * 3] = f;
+						}
+					}
+
+					temp.g2.begin(false);
+					// temp.g2.drawImage(image, -x * 512, -y * 512);
+					temp.g2.drawScaledImage(image, 0, 0, 512, 512);
+					temp.g2.end();
+
+					let bytes_img = temp.getPixels().b.buffer;
+					let u8a = new Uint8Array(bytes_img);
+					let f32a = new Float32Array(3 * 512 * 512);
+					for (let i = 0; i < (512 * 512); ++i) {
+						f32a[i                ] = (u8a[i * 4    ] / 255.0) * 2.0 - 1.0;
+						f32a[i + 512 * 512    ] = (u8a[i * 4 + 1] / 255.0) * 2.0 - 1.0;
+						f32a[i + 512 * 512 * 2] = (u8a[i * 4 + 2] / 255.0) * 2.0 - 1.0;
+					}
+
+					let latents_buf = Krom.mlInference(vae_encoder_blob, [f32a.buffer], [[1, 3, 512, 512]], [1, 4, 64, 64], Config.raw.gpu_inference);
+					let latents = new Float32Array(latents_buf);
+					for (let i = 0; i < latents.length; ++i) {
+						latents[i] = 0.18215 * latents[i];
+					}
+					let latents_orig = latents.slice(0);
+
+					let noise = new Float32Array(latents.length);
+					for (let i = 0; i < noise.length; ++i) noise[i] = Math.cos(2.0 * 3.14 * RandomNode.getFloat()) * Math.sqrt(-2.0 * Math.log(RandomNode.getFloat()));
+
+					let num_inference_steps = 50;
+					let init_timestep = Math.floor(num_inference_steps * strength);
+					let timestep = TextToPhotoNode.timesteps[num_inference_steps - init_timestep];
+					let alphas_cumprod = TextToPhotoNode.alphas_cumprod;
+					let sqrt_alpha_prod = Math.pow(alphas_cumprod[timestep], 0.5);
+					let sqrt_one_minus_alpha_prod = Math.pow(1.0 - alphas_cumprod[timestep], 0.5);
+					for (let i = 0; i < latents.length; ++i) {
+						latents[i] = sqrt_alpha_prod * latents[i] + sqrt_one_minus_alpha_prod * noise[i];
+					}
+
+					let start = num_inference_steps - init_timestep;
+
+					TextToPhotoNode.stableDiffusion(prompt, (img: Image) => {
+						// result.g2.begin(false);
+						// result.g2.drawImage(img, x * 512, y * 512);
+						// result.g2.end();
+						result = img;
+						done(img);
+					}, latents, start, true, f32mask, latents_orig);
+				// }
+			// }
+		});
+	}
+
+	static def: zui.Zui.TNode = {
+		id: 0,
+		name: _tr("Inpaint"),
+		type: "InpaintNode",
+		x: 0,
+		y: 0,
+		color: 0xff4982a0,
+		inputs: [
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Color"),
+				type: "RGBA",
+				color: 0xffc7c729,
+				default_value: array_f32([1.0, 1.0, 1.0, 1.0])
+			}
+		],
+		outputs: [
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Color"),
+				type: "RGBA",
+				color: 0xffc7c729,
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
+			}
+		],
+		buttons: [
+			{
+				name: _tr("auto"),
+				type: "BOOL",
+				default_value: true,
+				output: 0
+			},
+			{
+				name: "arm.nodes.InpaintNode.buttons",
+				type: "CUSTOM",
+				height: 0
+			}
+		]
+	};
+}

+ 241 - 0
armorlab/Sources/nodes/PhotoToPBRNode.ts

@@ -0,0 +1,241 @@
+
+// @:keep
+class PhotoToPBRNode extends LogicNode {
+
+	static temp: Image = null;
+	static images: Image[] = null;
+	static modelNames = ["base", "occlusion", "roughness", "metallic", "normal", "height"];
+
+	static cachedSource: Image = null;
+	static borderW = 64;
+	static tileW = 2048;
+	static tileWithBorderW = tileW + borderW * 2;
+
+	constructor() {
+		super();
+
+		if (temp == null) {
+			temp = Image.createRenderTarget(tileWithBorderW, tileWithBorderW);
+		}
+
+		init();
+	}
+
+	static init = () => {
+		if (images == null) {
+			images = [];
+			for (let i = 0; i < modelNames.length; ++i) {
+				images.push(Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY()));
+			}
+		}
+	}
+
+	override getAsImage = (from: i32, done: (img: Image)=>void) => {
+		let getSource = (done: (img: Image)=>void) => {
+			if (cachedSource != null) done(cachedSource);
+			else inputs[0].getAsImage(done);
+		}
+
+		getSource((source: Image) => {
+			cachedSource = source;
+
+			Console.progress(tr("Processing") + " - " + tr("Photo to PBR"));
+			Base.notifyOnNextFrame(() => {
+				let tileFloats: Float32Array[] = [];
+				let tilesX = Math.floor(Config.getTextureResX() / tileW);
+				let tilesY = Math.floor(Config.getTextureResY() / tileW);
+				let numTiles = tilesX * tilesY;
+				for (let i = 0; i < numTiles; ++i) {
+					let x = i % tilesX;
+					let y = Math.floor(i / tilesX);
+
+					temp.g2.begin(false);
+					temp.g2.drawScaledImage(source, borderW - x * tileW, borderW - y * tileW, -Config.getTextureResX(), Config.getTextureResY());
+					temp.g2.drawScaledImage(source, borderW - x * tileW, borderW - y * tileW, Config.getTextureResX(), -Config.getTextureResY());
+					temp.g2.drawScaledImage(source, borderW - x * tileW, borderW - y * tileW, -Config.getTextureResX(), -Config.getTextureResY());
+					temp.g2.drawScaledImage(source, borderW - x * tileW + tileW, borderW - y * tileW + tileW, Config.getTextureResX(), Config.getTextureResY());
+					temp.g2.drawScaledImage(source, borderW - x * tileW + tileW, borderW - y * tileW + tileW, -Config.getTextureResX(), Config.getTextureResY());
+					temp.g2.drawScaledImage(source, borderW - x * tileW + tileW, borderW - y * tileW + tileW, Config.getTextureResX(), -Config.getTextureResY());
+					temp.g2.drawScaledImage(source, borderW - x * tileW, borderW - y * tileW, Config.getTextureResX(), Config.getTextureResY());
+					temp.g2.end();
+
+					let bytes_img = temp.getPixels();
+					let u8a = new Uint8Array(bytes_img);
+					let f32a = new Float32Array(3 * tileWithBorderW * tileWithBorderW);
+					for (let i = 0; i < (tileWithBorderW * tileWithBorderW)) {
+						f32a[i                                        ] = (u8a[i * 4    ] / 255 - 0.5) / 0.5;
+						f32a[i + tileWithBorderW * tileWithBorderW    ] = (u8a[i * 4 + 1] / 255 - 0.5) / 0.5;
+						f32a[i + tileWithBorderW * tileWithBorderW * 2] = (u8a[i * 4 + 2] / 255 - 0.5) / 0.5;
+					}
+
+					Data.getBlob("models/photo_to_" + modelNames[from] + ".quant.onnx", (model_blob: ArrayBuffer) => {
+						let buf = Krom.mlInference(model_blob, [f32a.buffer], null, null, Config.raw.gpu_inference);
+						let ar = new Float32Array(buf);
+						let u8a = new Uint8Array(4 * tileW * tileW);
+						let offsetG = (from == ChannelBaseColor || from == ChannelNormalMap) ? tileWithBorderW * tileWithBorderW : 0;
+						let offsetB = (from == ChannelBaseColor || from == ChannelNormalMap) ? tileWithBorderW * tileWithBorderW * 2 : 0;
+						for (let i = 0; i < (tileW * tileW); ++i) {
+							let x = borderW + i % tileW;
+							let y = borderW + Math.floor(i / tileW);
+							u8a[i * 4    ] = Math.floor((ar[y * tileWithBorderW + x          ] * 0.5 + 0.5) * 255);
+							u8a[i * 4 + 1] = Math.floor((ar[y * tileWithBorderW + x + offsetG] * 0.5 + 0.5) * 255);
+							u8a[i * 4 + 2] = Math.floor((ar[y * tileWithBorderW + x + offsetB] * 0.5 + 0.5) * 255);
+							u8a[i * 4 + 3] = 255;
+						}
+						tileFloats.push(ar);
+
+						// Use border pixels to blend seams
+						if (i > 0) {
+							if (x > 0) {
+								let ar = tileFloats[i - 1];
+								for (let yy = 0; yy < tileW; ++yy) {
+									for (let xx = 0; xx < borderW; ++xx) {
+										let i = yy * tileW + xx;
+										let a = u8a[i * 4];
+										let b = u8a[i * 4 + 1];
+										let c = u8a[i * 4 + 2];
+
+										let aa = Math.floor((ar[(borderW + yy) * tileWithBorderW + borderW + tileW + xx          ] * 0.5 + 0.5) * 255);
+										let bb = Math.floor((ar[(borderW + yy) * tileWithBorderW + borderW + tileW + xx + offsetG] * 0.5 + 0.5) * 255);
+										let cc = Math.floor((ar[(borderW + yy) * tileWithBorderW + borderW + tileW + xx + offsetB] * 0.5 + 0.5) * 255);
+
+										let f = xx / borderW;
+										let invf = 1.0 - f;
+										a = Math.floor(a * f + aa * invf);
+										b = Math.floor(b * f + bb * invf);
+										c = Math.floor(c * f + cc * invf);
+
+										u8a[i * 4    ] = a;
+										u8a[i * 4 + 1] = b;
+										u8a[i * 4 + 2] = c;
+									}
+								}
+							}
+							if (y > 0) {
+								let ar = tileFloats[i - tilesX];
+								for (let xx = 0; xx < tileW; ++xx) {
+									for (let yy = 0; yy < borderW; ++yy) {
+										let i = yy * tileW + xx;
+										let a = u8a[i * 4];
+										let b = u8a[i * 4 + 1];
+										let c = u8a[i * 4 + 2];
+
+										let aa = Math.floor((ar[(borderW + tileW + yy) * tileWithBorderW + borderW + xx          ] * 0.5 + 0.5) * 255);
+										let bb = Math.floor((ar[(borderW + tileW + yy) * tileWithBorderW + borderW + xx + offsetG] * 0.5 + 0.5) * 255);
+										let cc = Math.floor((ar[(borderW + tileW + yy) * tileWithBorderW + borderW + xx + offsetB] * 0.5 + 0.5) * 255);
+
+										let f = yy / borderW;
+										let invf = 1.0 - f;
+										a = Math.floor(a * f + aa * invf);
+										b = Math.floor(b * f + bb * invf);
+										c = Math.floor(c * f + cc * invf);
+
+										u8a[i * 4    ] = a;
+										u8a[i * 4 + 1] = b;
+										u8a[i * 4 + 2] = c;
+									}
+								}
+							}
+						}
+
+						///if (krom_metal || krom_vulkan)
+						if (from == ChannelBaseColor) bgraSwap(u8a.buffer);
+						///end
+
+						let temp2 = Image.fromBytes(u8a.buffer, tileW, tileW);
+						images[from].g2.begin(false);
+						images[from].g2.drawImage(temp2, x * tileW, y * tileW);
+						images[from].g2.end();
+						Base.notifyOnNextFrame(() => {
+							temp2.unload();
+						});
+					});
+				}
+
+				done(images[from]);
+			});
+		});
+	}
+
+	///if (krom_metal || krom_vulkan)
+	static bgraSwap = (buffer: ArrayBuffer) => {
+		let u8a = new Uint8Array(buffer);
+		for (let i = 0; i < Math.floor(buffer.byteLength / 4); ++i) {
+			let r = u8a[i * 4];
+			u8a[i * 4] = u8a[i * 4 + 2];
+			u8a[i * 4 + 2] = r;
+		}
+		return buffer;
+	}
+	///end
+
+	static def: zui.Zui.TNode = {
+		id: 0,
+		name: _tr("Photo to PBR"),
+		type: "PhotoToPBRNode",
+		x: 0,
+		y: 0,
+		color: 0xff4982a0,
+		inputs: [
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Color"),
+				type: "RGBA",
+				color: 0xffc7c729,
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
+			}
+		],
+		outputs: [
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Base Color"),
+				type: "RGBA",
+				color: 0xffc7c729,
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
+			},
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Occlusion"),
+				type: "VALUE",
+				color: 0xffa1a1a1,
+				default_value: 1.0
+			},
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Roughness"),
+				type: "VALUE",
+				color: 0xffa1a1a1,
+				default_value: 1.0
+			},
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Metallic"),
+				type: "VALUE",
+				color: 0xffa1a1a1,
+				default_value: 0.0
+			},
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Normal Map"),
+				type: "VECTOR",
+				color: 0xffc7c729,
+				default_value: f32([0.0, 0.0, 0.0, 1.0])
+			},
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Height"),
+				type: "VALUE",
+				color: 0xffa1a1a1,
+				default_value: 1.0
+			}
+		],
+		buttons: []
+	};
+}

+ 61 - 0
armorlab/Sources/nodes/RGBNode.ts

@@ -0,0 +1,61 @@
+
+// @:keep
+class RGBNode extends LogicNode {
+
+	image: Image = null;
+
+	constructor() {
+		super();
+	}
+
+	override getAsImage = (from: i32, done: (img: Image)=>void) => {
+		if (image != null) {
+			Base.notifyOnNextFrame(() => {
+				image.unload();
+			});
+		}
+
+		let f32a = new Float32Array(4);
+		let raw = ParserLogic.getRawNode(this);
+		let default_value = raw.outputs[0].default_value;
+		f32a[0] = default_value[0];
+		f32a[1] = default_value[1];
+		f32a[2] = default_value[2];
+		f32a[3] = default_value[3];
+		image = Image.fromBytes(f32a.buffer, 1, 1, TextureFormat.RGBA128);
+		done(image);
+	}
+
+	override getCachedImage = (): Image => {
+		getAsImage(0, (img: Image) => {});
+		return image;
+	}
+
+	static def: zui.Zui.TNode = {
+		id: 0,
+		name: _tr("RGB"),
+		type: "RGBNode",
+		x: 0,
+		y: 0,
+		color: 0xffb34f5a,
+		inputs: [],
+		outputs: [
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Color"),
+				type: "RGBA",
+				color: 0xffc7c729,
+				default_value: array_f32([0.8, 0.8, 0.8, 1.0])
+			}
+		],
+		buttons: [
+			{
+				name: _tr("default_value"),
+				type: "RGBA",
+				output: 0,
+				default_value: array_f32([0.8, 0.8, 0.8, 1.0])
+			}
+		]
+	};
+}

Fișier diff suprimat deoarece este prea mare
+ 469 - 0
armorlab/Sources/nodes/TextToPhotoNode.ts


+ 33 - 41
armorlab/Sources/arm/nodes/TilingNode.hx → armorlab/Sources/nodes/TilingNode.ts

@@ -1,33 +1,25 @@
-package arm.nodes;
 
-import zui.Zui.Nodes;
-import iron.System;
-import arm.LogicNode;
-import arm.ParserLogic.f32;
-import arm.Translator._tr;
-
-@:keep
+// @:keep
 class TilingNode extends LogicNode {
 
-	var result: Image = null;
-	public static var image: Image = null;
-	public static var prompt = "";
-	static var strength = 0.5;
-	static var auto = true;
+	result: Image = null;
+	static image: Image = null;
+	static prompt = "";
+	static strength = 0.5;
+	static auto = true;
 
-	public function new() {
+	constructor() {
 		super();
-
 		init();
 	}
 
-	public static function init() {
+	static init = () => {
 		if (image == null) {
 			image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY());
 		}
 	}
 
-	public static function buttons(ui: zui.Zui, nodes: zui.Zui.Nodes, node: zui.Zui.TNode) {
+	static buttons = (ui: zui.Zui, nodes: zui.Zui.Nodes, node: zui.Zui.TNode) => {
 		auto = node.buttons[0].default_value == 0 ? false : true;
 		if (!auto) {
 			strength = ui.slider(zui.Zui.handle("tilingnode_0", {value: strength}), tr("strength"), 0, 1, true);
@@ -37,15 +29,15 @@ class TilingNode extends LogicNode {
 		else node.buttons[1].height = 0;
 	}
 
-	override function getAsImage(from: Int, done: Image->Void) {
-		inputs[0].getAsImage(function(source: Image) {
+	override getAsImage = (from: i32, done: (img: Image)=>void) => {
+		inputs[0].getAsImage((source: Image) => {
 			image.g2.begin(false);
 			image.g2.drawScaledImage(source, 0, 0, Config.getTextureResX(), Config.getTextureResY());
 			image.g2.end();
 
 			Console.progress(tr("Processing") + " - " + tr("Tiling"));
-			Base.notifyOnNextFrame(function() {
-				function _done(image: Image) {
+			Base.notifyOnNextFrame(() => {
+				let _done = (image: Image) => {
 					result = image;
 					done(image);
 				}
@@ -54,13 +46,13 @@ class TilingNode extends LogicNode {
 		});
 	}
 
-	override public function getCachedImage(): Image {
+	override getCachedImage = (): Image => {
 		return result;
 	}
 
-	public static function sdTiling(image: Image, seed: Int/* = -1*/, done: Image->Void) {
+	static sdTiling = (image: Image, seed: i32/* = -1*/, done: (img: Image)=>void) => {
 		TextToPhotoNode.tiling = false;
-		var tile = Image.createRenderTarget(512, 512);
+		let tile = Image.createRenderTarget(512, 512);
 		tile.g2.begin(false);
 		tile.g2.drawScaledImage(image, -256, -256, 512, 512);
 		tile.g2.drawScaledImage(image, 256, -256, 512, 512);
@@ -68,25 +60,25 @@ class TilingNode extends LogicNode {
 		tile.g2.drawScaledImage(image, 256, 256, 512, 512);
 		tile.g2.end();
 
-		var u8 = new js.lib.Uint8Array(512 * 512);
-		for (i in 0...512 * 512) {
-			var x = i % 512;
-			var y = Std.int(i / 512);
-			var l = y < 256 ? y : (511 - y);
-			u8[i] = (x > 256 - l && x < 256 + l) ? 0 : 255;
+		let u8a = new Uint8Array(512 * 512);
+		for (let i = 0; i < 512 * 512; ++i) {
+			let x = i % 512;
+			let y = Math.floor(i / 512);
+			let l = y < 256 ? y : (511 - y);
+			u8a[i] = (x > 256 - l && x < 256 + l) ? 0 : 255;
 		}
-		// for (i in 0...512 * 512) u8[i] = 255;
-		// for (x in (256 - 32)...(256 + 32)) {
-		// 	for (y in 0...512) {
-		// 		u8[y * 512 + x] = 0;
+		// for (let i = 0; i < 512 * 512; ++i) u8a[i] = 255;
+		// for (let x = (256 - 32); x < (256 + 32); ++x) {
+		// 	for (let y = 0; y < 512; ++y) {
+		// 		u8a[y * 512 + x] = 0;
 		// 	}
 		// }
-		// for (x in 0...512) {
-		// 	for (y in (256 - 32)...(256 + 32)) {
-		// 		u8[y * 512 + x] = 0;
+		// for (let x = 0; x < 512; ++x) {
+		// 	for (let y = (256 - 32); y < 256 + 32); ++y) {
+		// 		u8a[y * 512 + x] = 0;
 		// 	}
 		// }
-		var mask = Image.fromBytes(u8.buffer, 512, 512, TextureFormat.R8);
+		let mask = Image.fromBytes(u8a.buffer, 512, 512, TextureFormat.R8);
 
 		InpaintNode.prompt = prompt;
 		InpaintNode.strength = strength;
@@ -94,7 +86,7 @@ class TilingNode extends LogicNode {
 		InpaintNode.sdInpaint(tile, mask, done);
 	}
 
-	public static var def: zui.Zui.TNode = {
+	static def: zui.Zui.TNode = {
 		id: 0,
 		name: _tr("Tiling"),
 		type: "TilingNode",
@@ -108,7 +100,7 @@ class TilingNode extends LogicNode {
 				name: _tr("Color"),
 				type: "RGBA",
 				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
 			}
 		],
 		outputs: [
@@ -118,7 +110,7 @@ class TilingNode extends LogicNode {
 				name: _tr("Color"),
 				type: "RGBA",
 				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
 			}
 		],
 		buttons: [

+ 51 - 59
armorlab/Sources/arm/nodes/UpscaleNode.hx → armorlab/Sources/nodes/UpscaleNode.ts

@@ -1,34 +1,26 @@
-package arm.nodes;
 
-import zui.Zui.Nodes;
-import iron.System;
-import iron.Data;
-import arm.LogicNode;
-import arm.ParserLogic.f32;
-import arm.Translator._tr;
-
-@:keep
+// @:keep
 class UpscaleNode extends LogicNode {
 
-	static var temp: Image = null;
-	static var image: Image = null;
-	static var esrgan_blob: js.lib.ArrayBuffer;
+	static temp: Image = null;
+	static image: Image = null;
+	static esrgan_blob: ArrayBuffer;
 
-	public function new() {
+	constructor() {
 		super();
 	}
 
-	override function getAsImage(from: Int, done: Image->Void) {
-		inputs[0].getAsImage(function(_image: Image) {
+	override getAsImage = (from: i32, done: (img: Image)=>void) => {
+		inputs[0].getAsImage((_image: Image) => {
 			image = _image;
 
 			Console.progress(tr("Processing") + " - " + tr("Upscale"));
-			Base.notifyOnNextFrame(function() {
-				loadBlob(function() {
+			Base.notifyOnNextFrame(() => {
+				loadBlob(() => {
 					if (image.width < Config.getTextureResX()) {
 						image = esrgan(image);
 						while (image.width < Config.getTextureResX()) {
-							var lastImage = image;
+							let lastImage = image;
 							image = esrgan(image);
 							lastImage.unload();
 						}
@@ -39,23 +31,23 @@ class UpscaleNode extends LogicNode {
 		});
 	}
 
-	public static function loadBlob(done: Void->Void) {
-		Data.getBlob("models/esrgan.quant.onnx", function(_esrgan_blob: js.lib.ArrayBuffer) {
+	static loadBlob = (done: ()=>void) => {
+		Data.getBlob("models/esrgan.quant.onnx", (_esrgan_blob: ArrayBuffer) => {
 			esrgan_blob = _esrgan_blob;
 			done();
 		});
 	}
 
-	override public function getCachedImage(): Image {
+	override getCachedImage = (): Image => {
 		return image;
 	}
 
-	static function doTile(source: Image) {
-		var result: Image = null;
-		var size1w = source.width;
-		var size1h = source.height;
-		var size2w = Std.int(size1w * 2);
-		var size2h = Std.int(size1h * 2);
+	static doTile = (source: Image) => {
+		let result: Image = null;
+		let size1w = source.width;
+		let size1h = source.height;
+		let size2w = Math.floor(size1w * 2);
+		let size2h = Math.floor(size1h * 2);
 		if (temp != null) {
 			temp.unload();
 		}
@@ -64,48 +56,48 @@ class UpscaleNode extends LogicNode {
 		temp.g2.drawScaledImage(source, 0, 0, size1w, size1h);
 		temp.g2.end();
 
-		var bytes_img = temp.getPixels();
-		var u8 = new js.lib.Uint8Array(bytes_img);
-		var f32 = new js.lib.Float32Array(3 * size1w * size1h);
-		for (i in 0...(size1w * size1h)) {
-			f32[i                      ] = (u8[i * 4    ] / 255);
-			f32[i + size1w * size1w    ] = (u8[i * 4 + 1] / 255);
-			f32[i + size1w * size1w * 2] = (u8[i * 4 + 2] / 255);
+		let bytes_img = temp.getPixels();
+		let u8a = new Uint8Array(bytes_img);
+		let f32a = new Float32Array(3 * size1w * size1h);
+		for (let i = 0; i < (size1w * size1h); ++i) {
+			f32a[i                      ] = (u8a[i * 4    ] / 255);
+			f32a[i + size1w * size1w    ] = (u8a[i * 4 + 1] / 255);
+			f32a[i + size1w * size1w * 2] = (u8a[i * 4 + 2] / 255);
 		}
 
-		var esrgan2x_buf = Krom.mlInference(esrgan_blob, [f32.buffer], [[1, 3, size1w, size1h]], [1, 3, size2w, size2h], Config.raw.gpu_inference);
-		var esrgan2x = new js.lib.Float32Array(esrgan2x_buf);
-		for (i in 0...esrgan2x.length) {
+		let esrgan2x_buf = Krom.mlInference(esrgan_blob, [f32a.buffer], [[1, 3, size1w, size1h]], [1, 3, size2w, size2h], Config.raw.gpu_inference);
+		let esrgan2x = new Float32Array(esrgan2x_buf);
+		for (let i = 0; i < esrgan2x.length; ++i) {
 			if (esrgan2x[i] < 0) esrgan2x[i] = 0;
 			else if (esrgan2x[i] > 1) esrgan2x[i] = 1;
 		}
 
-		var u8 = new js.lib.Uint8Array(4 * size2w * size2h);
-		for (i in 0...(size2w * size2h)) {
-			u8[i * 4    ] = Std.int(esrgan2x[i                      ] * 255);
-			u8[i * 4 + 1] = Std.int(esrgan2x[i + size2w * size2w    ] * 255);
-			u8[i * 4 + 2] = Std.int(esrgan2x[i + size2w * size2w * 2] * 255);
-			u8[i * 4 + 3] = 255;
+		let u8a = new Uint8Array(4 * size2w * size2h);
+		for (let i = 0; i < (size2w * size2h); ++i) {
+			u8a[i * 4    ] = Math.floor(esrgan2x[i                      ] * 255);
+			u8a[i * 4 + 1] = Math.floor(esrgan2x[i + size2w * size2w    ] * 255);
+			u8a[i * 4 + 2] = Math.floor(esrgan2x[i + size2w * size2w * 2] * 255);
+			u8a[i * 4 + 3] = 255;
 		}
 
-		result = Image.fromBytes(u8.buffer, size2w, size2h);
+		result = Image.fromBytes(u8a.buffer, size2w, size2h);
 		return result;
 	}
 
-	public static function esrgan(source: Image): Image {
-		var result: Image = null;
-		var size1w = source.width;
-		var size1h = source.height;
-		var tileSize = 512;
-		var tileSize2x = Std.int(tileSize * 2);
+	static esrgan = (source: Image): Image => {
+		let result: Image = null;
+		let size1w = source.width;
+		let size1h = source.height;
+		let tileSize = 512;
+		let tileSize2x = Math.floor(tileSize * 2);
 
 		if (size1w >= tileSize2x || size1h >= tileSize2x) { // Split into tiles
-			var size2w = Std.int(size1w * 2);
-			var size2h = Std.int(size1h * 2);
+			let size2w = Math.floor(size1w * 2);
+			let size2h = Math.floor(size1h * 2);
 			result = Image.createRenderTarget(size2w, size2h);
-			var tileSource = Image.createRenderTarget(tileSize + 32 * 2, tileSize + 32 * 2);
-			for (x in 0...Std.int(size1w / tileSize)) {
-				for (y in 0...Std.int(size1h / tileSize)) {
+			let tileSource = Image.createRenderTarget(tileSize + 32 * 2, tileSize + 32 * 2);
+			for (let x = 0; x < Math.floor(size1w / tileSize); ++x) {
+				for (let y = 0; y < Math.floor(size1h / tileSize); ++y) {
 					tileSource.g2.begin(false);
 					tileSource.g2.drawScaledImage(source, 32 - x * tileSize, 32 - y * tileSize, -source.width, source.height);
 					tileSource.g2.drawScaledImage(source, 32 - x * tileSize, 32 - y * tileSize, source.width, -source.height);
@@ -115,7 +107,7 @@ class UpscaleNode extends LogicNode {
 					tileSource.g2.drawScaledImage(source, 32 - x * tileSize + tileSize, 32 - y * tileSize + tileSize, source.width, -source.height);
 					tileSource.g2.drawScaledImage(source, 32 - x * tileSize, 32 - y * tileSize, source.width, source.height);
 					tileSource.g2.end();
-					var tileResult = doTile(tileSource);
+					let tileResult = doTile(tileSource);
 					result.g2.begin(false);
 					result.g2.drawSubImage(tileResult, x * tileSize2x, y * tileSize2x, 64, 64, tileSize2x, tileSize2x);
 					result.g2.end();
@@ -128,7 +120,7 @@ class UpscaleNode extends LogicNode {
 		return result;
 	}
 
-	public static var def: zui.Zui.TNode = {
+	static def: zui.Zui.TNode = {
 		id: 0,
 		name: _tr("Upscale"),
 		type: "UpscaleNode",
@@ -142,7 +134,7 @@ class UpscaleNode extends LogicNode {
 				name: _tr("Color"),
 				type: "RGBA",
 				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
 			}
 		],
 		outputs: [
@@ -152,7 +144,7 @@ class UpscaleNode extends LogicNode {
 				name: _tr("Color"),
 				type: "RGBA",
 				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
 			}
 		],
 		buttons: []

+ 122 - 0
armorlab/Sources/nodes/VarianceNode.ts

@@ -0,0 +1,122 @@
+
+// @:keep
+class VarianceNode extends LogicNode {
+
+	static temp: Image = null;
+	static image: Image = null;
+	static inst: VarianceNode = null;
+	static prompt = "";
+
+	constructor() {
+		super();
+		inst = this;
+		init();
+	}
+
+	static init = () => {
+		if (temp == null) {
+			temp = Image.createRenderTarget(512, 512);
+		}
+	}
+
+	static buttons = (ui: zui.Zui, nodes: zui.Zui.Nodes, node: zui.Zui.TNode) => {
+		prompt = ui.textArea(zui.Zui.handle("variancenode_0"), true, tr("prompt"), true);
+		node.buttons[0].height = prompt.split("\n").length;
+	}
+
+	override getAsImage = (from: i32, done: (img: Image)=>void) => {
+		let strength = inst.inputs[1].node.value;
+
+		inst.inputs[0].getAsImage((source: Image) => {
+			temp.g2.begin(false);
+			temp.g2.drawScaledImage(source, 0, 0, 512, 512);
+			temp.g2.end();
+
+			let bytes_img = temp.getPixels().b.buffer;
+			let u8a = new Uint8Array(bytes_img);
+			let f32a = new Float32Array(3 * 512 * 512);
+			for (let i = 0; i < (512 * 512); ++i) {
+				f32a[i                ] = (u8a[i * 4    ] / 255) * 2.0 - 1.0;
+				f32a[i + 512 * 512    ] = (u8a[i * 4 + 1] / 255) * 2.0 - 1.0;
+				f32a[i + 512 * 512 * 2] = (u8a[i * 4 + 2] / 255) * 2.0 - 1.0;
+			}
+
+			Console.progress(tr("Processing") + " - " + tr("Variance"));
+			Base.notifyOnNextFrame(() => {
+				Data.getBlob("models/sd_vae_encoder.quant.onnx", (vae_encoder_blob: ArrayBuffer) => {
+					let latents_buf = Krom.mlInference(vae_encoder_blob, [f32a.buffer], [[1, 3, 512, 512]], [1, 4, 64, 64], Config.raw.gpu_inference);
+					let latents = new Float32Array(latents_buf);
+					for (let i = 0; i < latents.length; ++i) {
+						latents[i] = 0.18215 * latents[i];
+					}
+
+					let noise = new Float32Array(latents.length);
+					for (let i = 0; i < noise.length; ++i) noise[i] = Math.cos(2.0 * 3.14 * RandomNode.getFloat()) * Math.sqrt(-2.0 * Math.log(RandomNode.getFloat()));
+					let num_inference_steps = 50;
+					let init_timestep = Math.floor(num_inference_steps * strength);
+					let timesteps = TextToPhotoNode.timesteps[num_inference_steps - init_timestep];
+					let alphas_cumprod = TextToPhotoNode.alphas_cumprod;
+					let sqrt_alpha_prod = Math.pow(alphas_cumprod[timesteps], 0.5);
+					let sqrt_one_minus_alpha_prod = Math.pow(1.0 - alphas_cumprod[timesteps], 0.5);
+					for (let i = 0; i < latents.length; ++i) {
+						latents[i] = sqrt_alpha_prod * latents[i] + sqrt_one_minus_alpha_prod * noise[i];
+					}
+					let t_start = num_inference_steps - init_timestep;
+
+					TextToPhotoNode.stableDiffusion(prompt, (_image: Image) => {
+						image = _image;
+						done(image);
+					}, latents, t_start);
+				});
+			});
+		});
+	}
+
+	override getCachedImage = (): Image => {
+		return image;
+	}
+
+	static def: zui.Zui.TNode = {
+		id: 0,
+		name: _tr("Variance"),
+		type: "VarianceNode",
+		x: 0,
+		y: 0,
+		color: 0xff4982a0,
+		inputs: [
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Color"),
+				type: "RGBA",
+				color: 0xffc7c729,
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
+			},
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Strength"),
+				type: "VALUE",
+				color: 0xffa1a1a1,
+				default_value: 0.5
+			}
+		],
+		outputs: [
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Color"),
+				type: "RGBA",
+				color: 0xffc7c729,
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
+			}
+		],
+		buttons: [
+			{
+				name: "arm.nodes.VarianceNode.buttons",
+				type: "CUSTOM",
+				height: 1
+			}
+		]
+	};
+}

+ 1 - 0
armorlab/project.js

@@ -10,6 +10,7 @@ project.addDefine("is_lab");
 await project.addProject("../base");
 
 project.addSources("Sources");
+project.addSources("Sources/nodes");
 project.addShaders("Shaders/*.glsl", { embed: flags.snapshot });
 project.addAssets("Assets/*", { destination: "data/{name}", embed: flags.snapshot });
 project.addAssets("Assets/export_presets/*", { destination: "data/export_presets/{name}" });

+ 32 - 38
armorpaint/Sources/arm/ImportFolder.hx → armorpaint/Sources/ImportFolder.ts

@@ -1,31 +1,25 @@
-package arm;
-
-import zui.Zui.Nodes;
-import zui.Zui.TNodeCanvas;
-import zui.Zui.TNode;
-import zui.Zui.TNodeLink;
 
 class ImportFolder {
 
-	public static function run(path: String) {
-		var files = File.readDirectory(path);
-		var mapbase = "";
-		var mapopac = "";
-		var mapnor = "";
-		var mapocc = "";
-		var maprough = "";
-		var mapmet = "";
-		var mapheight = "";
+	static run = (path: string) => {
+		let files = File.readDirectory(path);
+		let mapbase = "";
+		let mapopac = "";
+		let mapnor = "";
+		let mapocc = "";
+		let maprough = "";
+		let mapmet = "";
+		let mapheight = "";
 
-		var foundTexture = false;
+		let foundTexture = false;
 		// Import maps
-		for (f in files) {
+		for (let f of files) {
 			if (!Path.isTexture(f)) continue;
 
 			// TODO: handle -albedo
 
-			var base = f.substr(0, f.lastIndexOf(".")).toLowerCase();
-			var valid = false;
+			let base = f.substr(0, f.lastIndexOf(".")).toLowerCase();
+			let valid = false;
 			if (mapbase == "" && Path.isBaseColorTex(base)) {
 				mapbase = f;
 				valid = true;
@@ -69,18 +63,18 @@ class ImportFolder {
 		// Create material
 		Context.raw.material = new SlotMaterial(Project.materials[0].data);
 		Project.materials.push(Context.raw.material);
-		var nodes = Context.raw.material.nodes;
-		var canvas = Context.raw.material.canvas;
-		var dirs = path.split(Path.sep);
+		let nodes = Context.raw.material.nodes;
+		let canvas = Context.raw.material.canvas;
+		let dirs = path.split(Path.sep);
 		canvas.name = dirs[dirs.length - 1];
-		var nout: TNode = null;
-		for (n in canvas.nodes) {
+		let nout: TNode = null;
+		for (let n of canvas.nodes) {
 			if (n.type == "OUTPUT_MATERIAL_PBR") {
 				nout = n;
 				break;
 			}
 		}
-		for (n in canvas.nodes) {
+		for (let n of canvas.nodes) {
 			if (n.name == "RGB") {
 				nodes.removeNode(n, canvas);
 				break;
@@ -88,35 +82,35 @@ class ImportFolder {
 		}
 
 		// Place nodes
-		var pos = 0;
-		var startY = 100;
-		var nodeH = 164;
+		let pos = 0;
+		let startY = 100;
+		let nodeH = 164;
 		if (mapbase != "") {
-			placeImageNode(nodes, canvas, mapbase, startY + nodeH * pos, nout.id, 0);
+			ImportFolder.placeImageNode(nodes, canvas, mapbase, startY + nodeH * pos, nout.id, 0);
 			pos++;
 		}
 		if (mapopac != "") {
-			placeImageNode(nodes, canvas, mapopac, startY + nodeH * pos, nout.id, 1);
+			ImportFolder.placeImageNode(nodes, canvas, mapopac, startY + nodeH * pos, nout.id, 1);
 			pos++;
 		}
 		if (mapocc != "") {
-			placeImageNode(nodes, canvas, mapocc, startY + nodeH * pos, nout.id, 2);
+			ImportFolder.placeImageNode(nodes, canvas, mapocc, startY + nodeH * pos, nout.id, 2);
 			pos++;
 		}
 		if (maprough != "") {
-			placeImageNode(nodes, canvas, maprough, startY + nodeH * pos, nout.id, 3);
+			ImportFolder.placeImageNode(nodes, canvas, maprough, startY + nodeH * pos, nout.id, 3);
 			pos++;
 		}
 		if (mapmet != "") {
-			placeImageNode(nodes, canvas, mapmet, startY + nodeH * pos, nout.id, 4);
+			ImportFolder.placeImageNode(nodes, canvas, mapmet, startY + nodeH * pos, nout.id, 4);
 			pos++;
 		}
 		if (mapnor != "") {
-			placeImageNode(nodes, canvas, mapnor, startY + nodeH * pos, nout.id, 5);
+			ImportFolder.placeImageNode(nodes, canvas, mapnor, startY + nodeH * pos, nout.id, 5);
 			pos++;
 		}
 		if (mapheight != "") {
-			placeImageNode(nodes, canvas, mapheight, startY + nodeH * pos, nout.id, 7);
+			ImportFolder.placeImageNode(nodes, canvas, mapheight, startY + nodeH * pos, nout.id, 7);
 			pos++;
 		}
 
@@ -126,12 +120,12 @@ class ImportFolder {
 		History.newMaterial();
 	}
 
-	static function placeImageNode(nodes: Nodes, canvas: TNodeCanvas, asset: String, ny: Int, to_id: Int, to_socket: Int) {
-		var n = NodesMaterial.createNode("TEX_IMAGE");
+	static placeImageNode = (nodes: Nodes, canvas: TNodeCanvas, asset: string, ny: i32, to_id: i32, to_socket: i32) => {
+		let n = NodesMaterial.createNode("TEX_IMAGE");
 		n.buttons[0].default_value = Base.getAssetIndex(asset);
 		n.x = 72;
 		n.y = ny;
-		var l: TNodeLink = { id: nodes.getLinkId(canvas.links), from_id: n.id, from_socket: 0, to_id: to_id, to_socket: to_socket };
+		let l: TNodeLink = { id: nodes.getLinkId(canvas.links), from_id: n.id, from_socket: 0, to_id: to_id, to_socket: to_socket };
 		canvas.links.push(l);
 	}
 }

+ 45 - 46
armorpaint/Sources/arm/MakeBake.hx → armorpaint/Sources/MakeBake.ts

@@ -1,20 +1,19 @@
-package arm;
 
 class MakeBake {
 
-	public static function run(con: NodeShaderContext, vert: NodeShader, frag: NodeShader) {
-		if (Context.raw.bakeType == BakeAO) { // Voxel
-			#if arm_voxels
+	static run = (con: NodeShaderContext, vert: NodeShader, frag: NodeShader) => {
+		if (Context.raw.bakeType == BakeType.BakeAO) { // Voxel
+			///if arm_voxels
 			// Apply normal channel
 			frag.wposition = true;
 			frag.n = true;
 			frag.vVec = true;
 			frag.add_function(ShaderFunctions.str_cotangentFrame);
-			#if krom_direct3d11
+			///if krom_direct3d11
 			frag.write('mat3 TBN = cotangentFrame(n, vVec, texCoord);');
-			#else
+			///else
 			frag.write('mat3 TBN = cotangentFrame(n, -vVec, texCoord);');
-			#end
+			///end
 			frag.write('n = nortan * 2.0 - 1.0;');
 			frag.write('n.y = -n.y;');
 			frag.write('n = normalize(mul(n, TBN));');
@@ -24,38 +23,38 @@ class MakeBake {
 			frag.add_uniform('sampler3D voxels');
 			frag.add_function(ShaderFunctions.str_traceAO);
 			frag.n = true;
-			var strength = Context.raw.bakeAoStrength;
-			var radius = Context.raw.bakeAoRadius;
-			var offset = Context.raw.bakeAoOffset;
-			frag.write('float ao = traceAO(voxpos, n, $radius, $offset) * $strength;');
-			if (Context.raw.bakeAxis != BakeXYZ) {
-				var axis = axisString(Context.raw.bakeAxis);
-				frag.write('ao *= dot(n, $axis);');
+			let strength = Context.raw.bakeAoStrength;
+			let radius = Context.raw.bakeAoRadius;
+			let offset = Context.raw.bakeAoOffset;
+			frag.write(`float ao = traceAO(voxpos, n, ${radius}, ${offset}) * ${strength};`);
+			if (Context.raw.bakeAxis != BakeAxis.BakeXYZ) {
+				let axis = MakeBake.axisString(Context.raw.bakeAxis);
+				frag.write(`ao *= dot(n, ${axis});`);
 			}
 			frag.write('ao = 1.0 - ao;');
 			frag.write('fragColor[0] = vec4(ao, ao, ao, 1.0);');
-			#end
+			///end
 		}
-		else if (Context.raw.bakeType == BakeCurvature) {
-			var pass = ParserMaterial.bake_passthrough;
-			var strength = pass ? ParserMaterial.bake_passthrough_strength : Context.raw.bakeCurvStrength + "";
-			var radius = pass ? ParserMaterial.bake_passthrough_radius : Context.raw.bakeCurvRadius + "";
-			var offset = pass ? ParserMaterial.bake_passthrough_offset : Context.raw.bakeCurvOffset + "";
-			strength = 'float($strength)';
-			radius = 'float($radius)';
-			offset = 'float($offset)';
+		else if (Context.raw.bakeType == BakeType.BakeCurvature) {
+			let pass = ParserMaterial.bake_passthrough;
+			let strength = pass ? ParserMaterial.bake_passthrough_strength : Context.raw.bakeCurvStrength + "";
+			let radius = pass ? ParserMaterial.bake_passthrough_radius : Context.raw.bakeCurvRadius + "";
+			let offset = pass ? ParserMaterial.bake_passthrough_offset : Context.raw.bakeCurvOffset + "";
+			strength = `float(${strength})`;
+			radius = `float(${radius})`;
+			offset = `float(${offset})`;
 			frag.n = true;
 			frag.write('vec3 dx = dFdx(n);');
 			frag.write('vec3 dy = dFdy(n);');
 			frag.write('float curvature = max(dot(dx, dx), dot(dy, dy));');
 			frag.write('curvature = clamp(pow(curvature, (1.0 / ' + radius + ') * 0.25) * ' + strength + ' * 2.0 + ' + offset + ' / 10.0, 0.0, 1.0);');
-			if (Context.raw.bakeAxis != BakeXYZ) {
-				var axis = axisString(Context.raw.bakeAxis);
-				frag.write('curvature *= dot(n, $axis);');
+			if (Context.raw.bakeAxis != BakeAxis.BakeXYZ) {
+				let axis = MakeBake.axisString(Context.raw.bakeAxis);
+				frag.write(`curvature *= dot(n, ${axis});`);
 			}
 			frag.write('fragColor[0] = vec4(curvature, curvature, curvature, 1.0);');
 		}
-		else if (Context.raw.bakeType == BakeNormal) { // Tangent
+		else if (Context.raw.bakeType == BakeType.BakeNormal) { // Tangent
 			frag.n = true;
 			frag.add_uniform('sampler2D texpaint_undo', '_texpaint_undo'); // Baked high-poly normals
 			frag.write('vec3 n0 = textureLod(texpaint_undo, texCoord, 0.0).rgb * vec3(2.0, 2.0, 2.0) - vec3(1.0, 1.0, 1.0);');
@@ -64,21 +63,21 @@ class MakeBake {
 			frag.write('vec3 res = normalize(mul(n0, invTBN)) * vec3(0.5, 0.5, 0.5) + vec3(0.5, 0.5, 0.5);');
 			frag.write('fragColor[0] = vec4(res, 1.0);');
 		}
-		else if (Context.raw.bakeType == BakeNormalObject) {
+		else if (Context.raw.bakeType == BakeType.BakeNormalObject) {
 			frag.n = true;
 			frag.write('fragColor[0] = vec4(n * vec3(0.5, 0.5, 0.5) + vec3(0.5, 0.5, 0.5), 1.0);');
-			if (Context.raw.bakeUpAxis == BakeUpY) {
+			if (Context.raw.bakeUpAxis == BakeUpAxis.BakeUpY) {
 				frag.write('fragColor[0].rgb = vec3(fragColor[0].r, fragColor[0].b, 1.0 - fragColor[0].g);');
 			}
 		}
-		else if (Context.raw.bakeType == BakeHeight) {
+		else if (Context.raw.bakeType == BakeType.BakeHeight) {
 			frag.wposition = true;
 			frag.add_uniform('sampler2D texpaint_undo', '_texpaint_undo'); // Baked high-poly positions
 			frag.write('vec3 wpos0 = textureLod(texpaint_undo, texCoord, 0.0).rgb * vec3(2.0, 2.0, 2.0) - vec3(1.0, 1.0, 1.0);');
 			frag.write('float res = distance(wpos0, wposition) * 10.0;');
 			frag.write('fragColor[0] = vec4(res, res, res, 1.0);');
 		}
-		else if (Context.raw.bakeType == BakeDerivative) {
+		else if (Context.raw.bakeType == BakeType.BakeDerivative) {
 			frag.add_uniform('sampler2D texpaint_undo', '_texpaint_undo'); // Baked height
 			frag.write('vec2 texDx = dFdx(texCoord);');
 			frag.write('vec2 texDy = dFdy(texCoord);');
@@ -87,17 +86,17 @@ class MakeBake {
 			frag.write('float h2 = textureLod(texpaint_undo, texCoord + texDy, 0.0).r * 100;');
 			frag.write('fragColor[0] = vec4((h1 - h0) * 0.5 + 0.5, (h2 - h0) * 0.5 + 0.5, 0.0, 1.0);');
 		}
-		else if (Context.raw.bakeType == BakePosition) {
+		else if (Context.raw.bakeType == BakeType.BakePosition) {
 			frag.wposition = true;
 			frag.write('fragColor[0] = vec4(wposition * vec3(0.5, 0.5, 0.5) + vec3(0.5, 0.5, 0.5), 1.0);');
-			if (Context.raw.bakeUpAxis == BakeUpY) {
+			if (Context.raw.bakeUpAxis == BakeUpAxis.BakeUpY) {
 				frag.write('fragColor[0].rgb = vec3(fragColor[0].r, fragColor[0].b, 1.0 - fragColor[0].g);');
 			}
 		}
-		else if (Context.raw.bakeType == BakeTexCoord) {
+		else if (Context.raw.bakeType == BakeType.BakeTexCoord) {
 			frag.write('fragColor[0] = vec4(texCoord.xy, 0.0, 1.0);');
 		}
-		else if (Context.raw.bakeType == BakeMaterialID) {
+		else if (Context.raw.bakeType == BakeType.BakeMaterialID) {
 			frag.add_uniform('sampler2D texpaint_nor_undo', '_texpaint_nor_undo');
 			frag.write('float sample_matid = textureLod(texpaint_nor_undo, texCoord, 0.0).a + 1.0 / 255.0;');
 			frag.write('float matid_r = fract(sin(dot(vec2(sample_matid, sample_matid * 20.0), vec2(12.9898, 78.233))) * 43758.5453);');
@@ -105,7 +104,7 @@ class MakeBake {
 			frag.write('float matid_b = fract(sin(dot(vec2(sample_matid, sample_matid * 40.0), vec2(12.9898, 78.233))) * 43758.5453);');
 			frag.write('fragColor[0] = vec4(matid_r, matid_g, matid_b, 1.0);');
 		}
-		else if (Context.raw.bakeType == BakeObjectID) {
+		else if (Context.raw.bakeType == BakeType.BakeObjectID) {
 			frag.add_uniform('float objectId', '_objectId');
 			frag.write('float obid = objectId + 1.0 / 255.0;');
 			frag.write('float id_r = fract(sin(dot(vec2(obid, obid * 20.0), vec2(12.9898, 78.233))) * 43758.5453);');
@@ -113,7 +112,7 @@ class MakeBake {
 			frag.write('float id_b = fract(sin(dot(vec2(obid, obid * 40.0), vec2(12.9898, 78.233))) * 43758.5453);');
 			frag.write('fragColor[0] = vec4(id_r, id_g, id_b, 1.0);');
 		}
-		else if (Context.raw.bakeType == BakeVertexColor) {
+		else if (Context.raw.bakeType == BakeType.BakeVertexColor) {
 			if (con.allow_vcols) {
 				con.add_elem("col", "short4norm");
 				frag.write('fragColor[0] = vec4(vcolor.r, vcolor.g, vcolor.b, 1.0);');
@@ -124,7 +123,7 @@ class MakeBake {
 		}
 	}
 
-	public static function positionAndNormal(vert: NodeShader, frag: NodeShader) {
+	static positionAndNormal = (vert: NodeShader, frag: NodeShader) => {
 		vert.add_out('vec3 position');
 		vert.add_out('vec3 normal');
 		vert.add_uniform('mat4 W', '_worldMatrix');
@@ -137,7 +136,7 @@ class MakeBake {
 		frag.write('fragColor[1] = vec4(normal, 1.0);');
 	}
 
-	public static function setColorWrites(con_paint: NodeShaderContext) {
+	static setColorWrites = (con_paint: NodeShaderContext) => {
 		// Bake into base color, disable other slots
 		con_paint.data.color_writes_red[1] = false;
 		con_paint.data.color_writes_green[1] = false;
@@ -149,12 +148,12 @@ class MakeBake {
 		con_paint.data.color_writes_alpha[2] = false;
 	}
 
-	static function axisString(i:Int):String {
-		return i == BakeX  ? "vec3(1,0,0)"  :
-			   i == BakeY  ? "vec3(0,1,0)"  :
-			   i == BakeZ  ? "vec3(0,0,1)"  :
-			   i == BakeMX ? "vec3(-1,0,0)" :
-			   i == BakeMY ? "vec3(0,-1,0)" :
-							 "vec3(0,0,-1)";
+	static axisString = (i: i32): string => {
+		return i == BakeAxis.BakeX  ? "vec3(1,0,0)"  :
+			   i == BakeAxis.BakeY  ? "vec3(0,1,0)"  :
+			   i == BakeAxis.BakeZ  ? "vec3(0,0,1)"  :
+			   i == BakeAxis.BakeMX ? "vec3(-1,0,0)" :
+			   i == BakeAxis.BakeMY ? "vec3(0,-1,0)" :
+							 		  "vec3(0,0,-1)";
 	}
 }

+ 15 - 16
armorpaint/Sources/arm/MakeBlur.hx → armorpaint/Sources/MakeBlur.ts

@@ -1,13 +1,12 @@
-package arm;
 
 class MakeBlur {
 
-	public static function run(vert: NodeShader, frag: NodeShader) {
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+	static run = (vert: NodeShader, frag: NodeShader) => {
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(sp.x * gbufferSize.x, sp.y * gbufferSize.y), 0).ba;');
-		#else
+		///else
 		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(sp.x * gbufferSize.x, (1.0 - sp.y) * gbufferSize.y), 0).ba;');
-		#end
+		///end
 
 		frag.write('vec3 basecol = vec3(0.0, 0.0, 0.0);');
 		frag.write('float roughness = 0.0;');
@@ -16,7 +15,7 @@ class MakeBlur {
 		frag.write('vec3 nortan = vec3(0.0, 0.0, 0.0);');
 		frag.write('float height = 0.0;');
 		frag.write('float mat_opacity = 1.0;');
-		var isMask = Context.raw.layer.isMask();
+		let isMask = Context.raw.layer.isMask();
 		if (isMask) {
 			frag.write('float opacity = 1.0;');
 		}
@@ -32,20 +31,20 @@ class MakeBlur {
 
 		frag.add_uniform('vec2 texpaintSize', '_texpaintSize');
 		frag.write('float blur_step = 1.0 / texpaintSize.x;');
-		if (Context.raw.tool == ToolSmudge) {
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal)
+		if (Context.raw.tool == WorkspaceTool.ToolSmudge) {
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal)
 			frag.write('const float blur_weight[7] = {1.0 / 28.0, 2.0 / 28.0, 3.0 / 28.0, 4.0 / 28.0, 5.0 / 28.0, 6.0 / 28.0, 7.0 / 28.0};');
-			#else
+			///else
 			frag.write('const float blur_weight[7] = float[](1.0 / 28.0, 2.0 / 28.0, 3.0 / 28.0, 4.0 / 28.0, 5.0 / 28.0, 6.0 / 28.0, 7.0 / 28.0);');
-			#end
+			///end
 			frag.add_uniform('vec3 brushDirection', '_brushDirection');
 			frag.write('vec2 blur_direction = brushDirection.yx;');
 			frag.write('for (int i = 0; i < 7; ++i) {');
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('vec2 texCoordInp2 = texelFetch(gbuffer2, ivec2((sp.x + blur_direction.x * blur_step * float(i)) * gbufferSize.x, (sp.y + blur_direction.y * blur_step * float(i)) * gbufferSize.y), 0).ba;');
-			#else
+			///else
 			frag.write('vec2 texCoordInp2 = texelFetch(gbuffer2, ivec2((sp.x + blur_direction.x * blur_step * float(i)) * gbufferSize.x, (1.0 - (sp.y + blur_direction.y * blur_step * float(i))) * gbufferSize.y), 0).ba;');
-			#end
+			///end
 			frag.write('vec4 texpaint_sample = texture(texpaint_undo, texCoordInp2);');
 			frag.write('opacity += texpaint_sample.a * blur_weight[i];');
 			frag.write('basecol += texpaint_sample.rgb * blur_weight[i];');
@@ -58,11 +57,11 @@ class MakeBlur {
 			frag.write('}');
 		}
 		else {
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal)
 			frag.write('const float blur_weight[15] = {0.034619 / 2.0, 0.044859 / 2.0, 0.055857 / 2.0, 0.066833 / 2.0, 0.076841 / 2.0, 0.084894 / 2.0, 0.090126 / 2.0, 0.09194 / 2.0, 0.090126 / 2.0, 0.084894 / 2.0, 0.076841 / 2.0, 0.066833 / 2.0, 0.055857 / 2.0, 0.044859 / 2.0, 0.034619 / 2.0};');
-			#else
+			///else
 			frag.write('const float blur_weight[15] = float[](0.034619 / 2.0, 0.044859 / 2.0, 0.055857 / 2.0, 0.066833 / 2.0, 0.076841 / 2.0, 0.084894 / 2.0, 0.090126 / 2.0, 0.09194 / 2.0, 0.090126 / 2.0, 0.084894 / 2.0, 0.076841 / 2.0, 0.066833 / 2.0, 0.055857 / 2.0, 0.044859 / 2.0, 0.034619 / 2.0);');
-			#end
+			///end
 			// X
 			frag.write('for (int i = -7; i <= 7; ++i) {');
 			frag.write('vec4 texpaint_sample = texture(texpaint_undo, texCoordInp + vec2(blur_step * float(i), 0.0));');

+ 15 - 16
armorpaint/Sources/arm/MakeBrush.hx → armorpaint/Sources/MakeBrush.ts

@@ -1,23 +1,22 @@
-package arm;
 
 class MakeBrush {
 
-	public static function run(vert: NodeShader, frag: NodeShader) {
+	static run = (vert: NodeShader, frag: NodeShader) => {
 
 		frag.write('float dist = 0.0;');
 
-		if (Context.raw.tool == ToolParticle) return;
+		if (Context.raw.tool == WorkspaceTool.ToolParticle) return;
 
-		var fillLayer = Context.raw.layer.fill_layer != null;
-		var decal = Context.raw.tool == ToolDecal || Context.raw.tool == ToolText;
+		let fillLayer = Context.raw.layer.fill_layer != null;
+		let decal = Context.raw.tool == WorkspaceTool.ToolDecal || Context.raw.tool == WorkspaceTool.ToolText;
 		if (decal && !fillLayer) frag.write('if (decalMask.z > 0.0) {');
 
 		if (Config.raw.brush_3d) {
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('float depth = textureLod(gbufferD, inp.xy, 0.0).r;');
-			#else
+			///else
 			frag.write('float depth = textureLod(gbufferD, vec2(inp.x, 1.0 - inp.y), 0.0).r;');
-			#end
+			///end
 
 			frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix');
 			frag.write('vec4 winp = vec4(vec2(inp.x, 1.0 - inp.y) * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);');
@@ -28,11 +27,11 @@ class MakeBrush {
 			if (Config.raw.brush_angle_reject || Context.raw.xray) {
 				frag.add_function(ShaderFunctions.str_octahedronWrap);
 				frag.add_uniform('sampler2D gbuffer0');
-				#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+				///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 				frag.write('vec2 g0 = textureLod(gbuffer0, inp.xy, 0.0).rg;');
-				#else
+				///else
 				frag.write('vec2 g0 = textureLod(gbuffer0, vec2(inp.x, 1.0 - inp.y), 0.0).rg;');
-				#end
+				///end
 				frag.write('vec3 wn;');
 				frag.write('wn.z = 1.0 - abs(g0.x) - abs(g0.y);');
 				frag.write('wn.xy = wn.z >= 0.0 ? g0.xy : octahedronWrap(g0.xy);');
@@ -42,16 +41,16 @@ class MakeBrush {
 				if (Config.raw.brush_angle_reject && !Context.raw.xray) {
 					frag.write('if (planeDist < -0.01) discard;');
 					frag.n = true;
-					var angle = Context.raw.brushAngleRejectDot;
-					frag.write('if (dot(wn, n) < $angle) discard;');
+					let angle = Context.raw.brushAngleRejectDot;
+					frag.write(`if (dot(wn, n) < ${angle}) discard;`);
 				}
 			}
 
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('float depthlast = textureLod(gbufferD, inplast.xy, 0.0).r;');
-			#else
+			///else
 			frag.write('float depthlast = textureLod(gbufferD, vec2(inplast.x, 1.0 - inplast.y), 0.0).r;');
-			#end
+			///end
 
 			frag.write('vec4 winplast = vec4(vec2(inplast.x, 1.0 - inplast.y) * 2.0 - 1.0, depthlast * 2.0 - 1.0, 1.0);');
 			frag.write('winplast = mul(winplast, invVP);');

+ 35 - 0
armorpaint/Sources/MakeClone.ts

@@ -0,0 +1,35 @@
+
+class MakeClone {
+
+	static run = (vert: NodeShader, frag: NodeShader) => {
+		frag.add_uniform('vec2 cloneDelta', '_cloneDelta');
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2((sp.xy + cloneDelta) * gbufferSize), 0).ba;');
+		///else
+		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2((sp.x + cloneDelta.x) * gbufferSize.x, (1.0 - (sp.y + cloneDelta.y)) * gbufferSize.y), 0).ba;');
+		///end
+
+		frag.write('vec3 texpaint_pack_sample = textureLod(texpaint_pack_undo, texCoordInp, 0.0).rgb;');
+		let base = 'textureLod(texpaint_undo, texCoordInp, 0.0).rgb';
+		let rough = 'texpaint_pack_sample.g';
+		let met = 'texpaint_pack_sample.b';
+		let occ = 'texpaint_pack_sample.r';
+		let nortan = 'textureLod(texpaint_nor_undo, texCoordInp, 0.0).rgb';
+		let height = '0.0';
+		let opac = '1.0';
+		frag.write(`vec3 basecol = ${base};`);
+		frag.write(`float roughness = ${rough};`);
+		frag.write(`float metallic = ${met};`);
+		frag.write(`float occlusion = ${occ};`);
+		frag.write(`vec3 nortan = ${nortan};`);
+		frag.write(`float height = ${height};`);
+		frag.write(`float mat_opacity = ${opac};`);
+		frag.write('float opacity = mat_opacity * brushOpacity;');
+		if (Context.raw.material.paintEmis) {
+			frag.write('float emis = 0.0;');
+		}
+		if (Context.raw.material.paintSubs) {
+			frag.write('float subs = 0.0;');
+		}
+	}
+}

+ 6 - 7
armorpaint/Sources/arm/MakeColorIdPicker.hx → armorpaint/Sources/MakeColorIdPicker.ts

@@ -1,8 +1,7 @@
-package arm;
 
 class MakeColorIdPicker {
 
-	public static function run(vert: NodeShader, frag: NodeShader) {
+	static run = (vert: NodeShader, frag: NodeShader) => {
 		// Mangle vertices to form full screen triangle
 		vert.write('gl_Position = vec4(-1.0 + float((gl_VertexID & 1) << 2), -1.0 + float((gl_VertexID & 2) << 1), 0.0, 1.0);');
 
@@ -10,19 +9,19 @@ class MakeColorIdPicker {
 		frag.add_uniform('vec2 gbufferSize', '_gbufferSize');
 		frag.add_uniform('vec4 inp', '_inputBrush');
 
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(inp.x * gbufferSize.x, inp.y * gbufferSize.y), 0).ba;');
-		#else
+		///else
 		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(inp.x * gbufferSize.x, (1.0 - inp.y) * gbufferSize.y), 0).ba;');
-		#end
+		///end
 
-		if (Context.raw.tool == ToolColorId) {
+		if (Context.raw.tool == WorkspaceTool.ToolColorId) {
 			frag.add_out('vec4 fragColor');
 			frag.add_uniform('sampler2D texcolorid', '_texcolorid');
 			frag.write('vec3 idcol = textureLod(texcolorid, texCoordInp, 0.0).rgb;');
 			frag.write('fragColor = vec4(idcol, 1.0);');
 		}
-		else if (Context.raw.tool == ToolPicker || Context.raw.tool == ToolMaterial) {
+		else if (Context.raw.tool == WorkspaceTool.ToolPicker || Context.raw.tool == WorkspaceTool.ToolMaterial) {
 			if (Context.raw.pickPosNorTex) {
 				frag.add_out('vec4 fragColor[2]');
 				frag.add_uniform('sampler2D gbufferD');

+ 17 - 18
armorpaint/Sources/arm/MakeDiscard.hx → armorpaint/Sources/MakeDiscard.ts

@@ -1,51 +1,50 @@
-package arm;
 
 class MakeDiscard {
 
-	public static function colorId(vert: NodeShader, frag: NodeShader) {
+	static colorId = (vert: NodeShader, frag: NodeShader) => {
 		frag.add_uniform('sampler2D texpaint_colorid'); // 1x1 picker
 		frag.add_uniform('sampler2D texcolorid', '_texcolorid'); // color map
 		frag.write('vec3 colorid_c1 = texelFetch(texpaint_colorid, ivec2(0, 0), 0).rgb;');
 		frag.write('vec3 colorid_c2 = textureLod(texcolorid, texCoordPick, 0).rgb;');
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal)
 		frag.write('if (any(colorid_c1 != colorid_c2)) discard;');
-		#else
+		///else
 		frag.write('if (colorid_c1 != colorid_c2) discard;');
-		#end
+		///end
 	}
 
-	public static function face(vert: NodeShader, frag: NodeShader) {
+	static face = (vert: NodeShader, frag: NodeShader) => {
 		frag.add_uniform('sampler2D gbuffer2');
 		frag.add_uniform('sampler2D textrianglemap', '_textrianglemap');
 		frag.add_uniform('vec2 textrianglemapSize', '_texpaintSize');
 		frag.add_uniform('vec2 gbufferSize', '_gbufferSize');
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(inp.x * gbufferSize.x, inp.y * gbufferSize.y), 0).ba;');
-		#else
+		///else
 		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2(inp.x * gbufferSize.x, (1.0 - inp.y) * gbufferSize.y), 0).ba;');
-		#end
+		///end
 		frag.write('vec4 face_c1 = texelFetch(textrianglemap, ivec2(texCoordInp * textrianglemapSize), 0);');
 		frag.write('vec4 face_c2 = textureLod(textrianglemap, texCoordPick, 0);');
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal)
 		frag.write('if (any(face_c1 != face_c2)) discard;');
-		#else
+		///else
 		frag.write('if (face_c1 != face_c2) discard;');
-		#end
+		///end
 	}
 
-	public static function uvIsland(vert: NodeShader, frag: NodeShader) {
+	static uvIsland = (vert: NodeShader, frag: NodeShader) => {
 		frag.add_uniform('sampler2D texuvislandmap', '_texuvislandmap');
 		frag.write('if (textureLod(texuvislandmap, texCoordPick, 0).r == 0.0) discard;');
 	}
 
-	public static function materialId(vert: NodeShader, frag: NodeShader) {
+	static materialId = (vert: NodeShader, frag: NodeShader) => {
 		frag.wvpposition = true;
 		frag.write('vec2 picker_sample_tc = vec2(wvpposition.x / wvpposition.w, wvpposition.y / wvpposition.w) * 0.5 + 0.5;');
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		frag.write('picker_sample_tc.y = 1.0 - picker_sample_tc.y;');
-		#end
+		///end
 		frag.add_uniform('sampler2D texpaint_nor_undo', '_texpaint_nor_undo');
-		var matid = Context.raw.materialIdPicked / 255;
-		frag.write('if ($matid != textureLod(texpaint_nor_undo, picker_sample_tc, 0.0).a) discard;');
+		let matid = Context.raw.materialIdPicked / 255;
+		frag.write(`if (${matid} != textureLod(texpaint_nor_undo, picker_sample_tc, 0.0).a) discard;`);
 	}
 }

+ 488 - 0
armorpaint/Sources/MakeMaterial.ts

@@ -0,0 +1,488 @@
+
+class MakeMaterial {
+
+	static defaultScon: ShaderContext = null;
+	static defaultMcon: MaterialContext = null;
+
+	static heightUsed = false;
+	static emisUsed = false;
+	static subsUsed = false;
+
+	static getMOut = (): bool => {
+		for (let n of UINodes.inst.getCanvasMaterial().nodes) if (n.type == "OUTPUT_MATERIAL_PBR") return true;
+		return false;
+	}
+
+	static parseMeshMaterial = () => {
+		let m = Project.materials[0].data;
+
+		for (let c of m.shader.contexts) {
+			if (c.raw.name == "mesh") {
+				array_remove(m.shader.raw.contexts, c.raw);
+				array_remove(m.shader.contexts, c);
+				MakeMaterial.deleteContext(c);
+				break;
+			}
+		}
+
+		if (MakeMesh.layerPassCount > 1) {
+			let i = 0;
+			while (i < m.shader.contexts.length) {
+				let c = m.shader.contexts[i];
+				for (let j = 1; j < MakeMesh.layerPassCount; ++j) {
+					if (c.raw.name == "mesh" + j) {
+						array_remove(m.shader.raw.contexts, c.raw);
+						array_remove(m.shader.contexts, c);
+						MakeMaterial.deleteContext(c);
+						i--;
+						break;
+					}
+				}
+				i++;
+			}
+
+			i = 0;
+			while (i < m.contexts.length) {
+				let c = m.contexts[i];
+				for (let j = 1; j < MakeMesh.layerPassCount; ++j) {
+					if (c.raw.name == "mesh" + j) {
+						array_remove(m.raw.contexts, c.raw);
+						array_remove(m.contexts, c);
+						i--;
+						break;
+					}
+				}
+				i++;
+			}
+		}
+
+		let con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }));
+		let scon = new ShaderContext(con.data, (scon: ShaderContext) => {});
+		scon.overrideContext = {};
+		if (con.frag.sharedSamplers.length > 0) {
+			let sampler = con.frag.sharedSamplers[0];
+			scon.overrideContext.shared_sampler = sampler.substr(sampler.lastIndexOf(" ") + 1);
+		}
+		if (!Context.raw.textureFilter) {
+			scon.overrideContext.filter = "point";
+		}
+		m.shader.raw.contexts.push(scon.raw);
+		m.shader.contexts.push(scon);
+
+		for (let i = 1; i < MakeMesh.layerPassCount; ++i) {
+			let con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }), i);
+			let scon = new ShaderContext(con.data, (scon: ShaderContext) => {});
+			scon.overrideContext = {};
+			if (con.frag.sharedSamplers.length > 0) {
+				let sampler = con.frag.sharedSamplers[0];
+				scon.overrideContext.shared_sampler = sampler.substr(sampler.lastIndexOf(" ") + 1);
+			}
+			if (!Context.raw.textureFilter) {
+				scon.overrideContext.filter = "point";
+			}
+			m.shader.raw.contexts.push(scon.raw);
+			m.shader.contexts.push(scon);
+
+			let mcon = new MaterialContext({ name: "mesh" + i, bind_textures: [] }, (self: MaterialContext) => {});
+			m.raw.contexts.push(mcon.raw);
+			m.contexts.push(mcon);
+		}
+
+		Context.raw.ddirty = 2;
+
+		///if arm_voxels
+		MakeMaterial.makeVoxel(m);
+		///end
+
+		///if (krom_direct3d12 || krom_vulkan || krom_metal)
+		RenderPathRaytrace.dirty = 1;
+		///end
+	}
+
+	static parseParticleMaterial = () => {
+		let m = Context.raw.particleMaterial;
+		let sc: ShaderContext = null;
+		for (let c of m.shader.contexts) {
+			if (c.raw.name == "mesh") {
+				sc = c;
+				break;
+			}
+		}
+		if (sc != null) {
+			array_remove(m.shader.raw.contexts, sc.raw);
+			array_remove(m.shader.contexts, sc);
+		}
+		let con = MakeParticle.run(new NodeShaderData({ name: "MaterialParticle", canvas: null }));
+		if (sc != null) MakeMaterial.deleteContext(sc);
+		sc = new ShaderContext(con.data, (sc: ShaderContext) => {});
+		m.shader.raw.contexts.push(sc.raw);
+		m.shader.contexts.push(sc);
+	}
+
+	static parseMeshPreviewMaterial = (md: MaterialData = null) => {
+		if (!MakeMaterial.getMOut()) return;
+
+		let m = md == null ? Project.materials[0].data : md;
+		let scon: ShaderContext = null;
+		for (let c of m.shader.contexts) {
+			if (c.raw.name == "mesh") {
+				scon = c;
+				break;
+			}
+		}
+		array_remove(m.shader.raw.contexts, scon.raw);
+		array_remove(m.shader.contexts, scon);
+
+		let mcon: TMaterialContext = { name: "mesh", bind_textures: [] };
+
+		let sd = new NodeShaderData({ name: "Material", canvas: null });
+		let con = MakeMeshPreview.run(sd, mcon);
+
+		for (let i = 0; i < m.contexts.length; ++i) {
+			if (m.contexts[i].raw.name == "mesh") {
+				m.contexts[i] = new MaterialContext(mcon, (self: MaterialContext) => {});
+				break;
+			}
+		}
+
+		if (scon != null) MakeMaterial.deleteContext(scon);
+
+		let compileError = false;
+		scon = new ShaderContext(con.data, (scon: ShaderContext) => {
+			if (scon == null) compileError = true;
+		});
+		if (compileError) return;
+
+		m.shader.raw.contexts.push(scon.raw);
+		m.shader.contexts.push(scon);
+	}
+
+	///if arm_voxels
+	static makeVoxel = (m: MaterialData) => {
+		let rebuild = MakeMaterial.heightUsed;
+		if (Config.raw.rp_gi != false && rebuild) {
+			let scon: ShaderContext = null;
+			for (let c of m.shader.contexts) {
+				if (c.raw.name == "voxel") {
+					scon = c;
+					break;
+				}
+			}
+			if (scon != null) MakeVoxel.run(scon);
+		}
+	}
+	///end
+
+	static parsePaintMaterial = (bakePreviews = true) => {
+		if (!MakeMaterial.getMOut()) return;
+
+		if (bakePreviews) {
+			let current = Graphics2.current;
+			if (current != null) current.end();
+			MakeMaterial.bakeNodePreviews();
+			if (current != null) current.begin(false);
+		}
+
+		let m = Project.materials[0].data;
+		// let scon: ShaderContext = null;
+		// let mcon: MaterialContext = null;
+		for (let c of m.shader.contexts) {
+			if (c.raw.name == "paint") {
+				array_remove(m.shader.raw.contexts, c.raw);
+				array_remove(m.shader.contexts, c);
+				if (c != MakeMaterial.defaultScon) MakeMaterial.deleteContext(c);
+				break;
+			}
+		}
+		for (let c of m.contexts) {
+			if (c.raw.name == "paint") {
+				array_remove(m.raw.contexts, c.raw);
+				array_remove(m.contexts, c);
+				break;
+			}
+		}
+
+		let sdata = new NodeShaderData({ name: "Material", canvas: UINodes.inst.getCanvasMaterial() });
+		let tmcon: TMaterialContext = { name: "paint", bind_textures: [] };
+		let con = MakePaint.run(sdata, tmcon);
+
+		let compileError = false;
+		let scon = new ShaderContext(con.data, (scon: ShaderContext) => {
+			if (scon == null) compileError = true;
+		});
+		if (compileError) return;
+		scon.overrideContext = {};
+		scon.overrideContext.addressing = "repeat";
+		let mcon = new MaterialContext(tmcon, (mcon: MaterialContext) => {});
+
+		m.shader.raw.contexts.push(scon.raw);
+		m.shader.contexts.push(scon);
+		m.raw.contexts.push(mcon.raw);
+		m.contexts.push(mcon);
+
+		if (MakeMaterial.defaultScon == null) MakeMaterial.defaultScon = scon;
+		if (MakeMaterial.defaultMcon == null) MakeMaterial.defaultMcon = mcon;
+	}
+
+	static bakeNodePreviews = () => {
+		Context.raw.nodePreviewsUsed = [];
+		if (Context.raw.nodePreviews == null) Context.raw.nodePreviews = new Map();
+		MakeMaterial.traverseNodes(UINodes.inst.getCanvasMaterial().nodes, null, []);
+		for (let key of Context.raw.nodePreviews.keys()) {
+			if (Context.raw.nodePreviewsUsed.indexOf(key) == -1) {
+				let image = Context.raw.nodePreviews.get(key);
+				Base.notifyOnNextFrame(image.unload);
+				Context.raw.nodePreviews.delete(key);
+			}
+		}
+	}
+
+	static traverseNodes = (nodes: TNode[], group: TNodeCanvas, parents: TNode[]) => {
+		for (let node of nodes) {
+			MakeMaterial.bakeNodePreview(node, group, parents);
+			if (node.type == "GROUP") {
+				for (let g of Project.materialGroups) {
+					if (g.canvas.name == node.name) {
+						parents.push(node);
+						MakeMaterial.traverseNodes(g.canvas.nodes, g.canvas, parents);
+						parents.pop();
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	static bakeNodePreview = (node: TNode, group: TNodeCanvas, parents: TNode[]) => {
+		if (node.type == "BLUR") {
+			let id = ParserMaterial.node_name(node, parents);
+			let image = Context.raw.nodePreviews.get(id);
+			Context.raw.nodePreviewsUsed.push(id);
+			let resX = Math.floor(Config.getTextureResX() / 4);
+			let resY = Math.floor(Config.getTextureResY() / 4);
+			if (image == null || image.width != resX || image.height != resY) {
+				if (image != null) image.unload();
+				image = Image.createRenderTarget(resX, resY);
+				Context.raw.nodePreviews.set(id, image);
+			}
+
+			ParserMaterial.blur_passthrough = true;
+			UtilRender.makeNodePreview(UINodes.inst.getCanvasMaterial(), node, image, group, parents);
+			ParserMaterial.blur_passthrough = false;
+		}
+		else if (node.type == "DIRECT_WARP") {
+			let id = ParserMaterial.node_name(node, parents);
+			let image = Context.raw.nodePreviews.get(id);
+			Context.raw.nodePreviewsUsed.push(id);
+			let resX = Math.floor(Config.getTextureResX());
+			let resY = Math.floor(Config.getTextureResY());
+			if (image == null || image.width != resX || image.height != resY) {
+				if (image != null) image.unload();
+				image = Image.createRenderTarget(resX, resY);
+				Context.raw.nodePreviews.set(id, image);
+			}
+
+			ParserMaterial.warp_passthrough = true;
+			UtilRender.makeNodePreview(UINodes.inst.getCanvasMaterial(), node, image, group, parents);
+			ParserMaterial.warp_passthrough = false;
+		}
+		else if (node.type == "BAKE_CURVATURE") {
+			let id = ParserMaterial.node_name(node, parents);
+			let image = Context.raw.nodePreviews.get(id);
+			Context.raw.nodePreviewsUsed.push(id);
+			let resX = Math.floor(Config.getTextureResX());
+			let resY = Math.floor(Config.getTextureResY());
+			if (image == null || image.width != resX || image.height != resY) {
+				if (image != null) image.unload();
+				image = Image.createRenderTarget(resX, resY, TextureFormat.R8);
+				Context.raw.nodePreviews.set(id, image);
+			}
+
+			if (RenderPathPaint.liveLayer == null) {
+				RenderPathPaint.liveLayer = new SlotLayer("_live");
+			}
+
+			let _space = UIHeader.inst.worktab.position;
+			let _tool = Context.raw.tool;
+			let _bakeType = Context.raw.bakeType;
+			UIHeader.inst.worktab.position = SpaceType.Space3D;
+			Context.raw.tool = WorkspaceTool.ToolBake;
+			Context.raw.bakeType = BakeType.BakeCurvature;
+
+			ParserMaterial.bake_passthrough = true;
+			ParserMaterial.start_node = node;
+			ParserMaterial.start_group = group;
+			ParserMaterial.start_parents = parents;
+			MakeMaterial.parsePaintMaterial(false);
+			ParserMaterial.bake_passthrough = false;
+			ParserMaterial.start_node = null;
+			ParserMaterial.start_group = null;
+			ParserMaterial.start_parents = null;
+			Context.raw.pdirty = 1;
+			RenderPathPaint.useLiveLayer(true);
+			RenderPathPaint.commandsPaint(false);
+			RenderPathPaint.dilate(true, false);
+			RenderPathPaint.useLiveLayer(false);
+			Context.raw.pdirty = 0;
+
+			UIHeader.inst.worktab.position = _space;
+			Context.raw.tool = _tool;
+			Context.raw.bakeType = _bakeType;
+			MakeMaterial.parsePaintMaterial(false);
+
+			let rts = RenderPath.active.renderTargets;
+			let texpaint_live = rts.get("texpaint_live");
+
+			image.g2.begin(false);
+			image.g2.drawImage(texpaint_live.image, 0, 0);
+			image.g2.end();
+		}
+	}
+
+	static parseNodePreviewMaterial = (node: TNode, group: TNodeCanvas = null, parents: TNode[] = null): { scon: ShaderContext, mcon: MaterialContext } => {
+		if (node.outputs.length == 0) return null;
+		let sdata = new NodeShaderData({ name: "Material", canvas: UINodes.inst.getCanvasMaterial() });
+		let mcon_raw: TMaterialContext = { name: "mesh", bind_textures: [] };
+		let con = MakeNodePreview.run(sdata, mcon_raw, node, group, parents);
+		let compileError = false;
+		let scon = new ShaderContext(con.data, (scon: ShaderContext) => {
+			if (scon == null) compileError = true;
+		});
+		if (compileError) return null;
+		let mcon = new MaterialContext(mcon_raw, (mcon: MaterialContext) => {});
+		return { scon: scon, mcon: mcon };
+	}
+
+	static parseBrush = () => {
+		ParserLogic.parse(Context.raw.brush.canvas);
+	}
+
+	static blendMode = (frag: NodeShader, blending: i32, cola: string, colb: string, opac: string): string => {
+		if (blending == BlendType.BlendMix) {
+			return `mix(${cola}, ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendDarken) {
+			return `mix(${cola}, min(${cola}, ${colb}), ${opac})`;
+		}
+		else if (blending == BlendType.BlendMultiply) {
+			return `mix(${cola}, ${cola} * ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendBurn) {
+			return `mix(${cola}, vec3(1.0, 1.0, 1.0) - (vec3(1.0, 1.0, 1.0) - ${cola}) / ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendLighten) {
+			return `max(${cola}, ${colb} * ${opac})`;
+		}
+		else if (blending == BlendType.BlendScreen) {
+			return `(vec3(1.0, 1.0, 1.0) - (vec3(1.0 - ${opac}, 1.0 - ${opac}, 1.0 - ${opac}) + ${opac} * (vec3(1.0, 1.0, 1.0) - ${colb})) * (vec3(1.0, 1.0, 1.0) - ${cola}))`;
+		}
+		else if (blending == BlendType.BlendDodge) {
+			return `mix(${cola}, ${cola} / (vec3(1.0, 1.0, 1.0) - ${colb}), ${opac})`;
+		}
+		else if (blending == BlendType.BlendAdd) {
+			return `mix(${cola}, ${cola} + ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendOverlay) {
+			return `mix(${cola}, vec3(
+				${cola}.r < 0.5 ? 2.0 * ${cola}.r * ${colb}.r : 1.0 - 2.0 * (1.0 - ${cola}.r) * (1.0 - ${colb}.r),
+				${cola}.g < 0.5 ? 2.0 * ${cola}.g * ${colb}.g : 1.0 - 2.0 * (1.0 - ${cola}.g) * (1.0 - ${colb}.g),
+				${cola}.b < 0.5 ? 2.0 * ${cola}.b * ${colb}.b : 1.0 - 2.0 * (1.0 - ${cola}.b) * (1.0 - ${colb}.b)
+			), ${opac})`;
+		}
+		else if (blending == BlendType.BlendSoftLight) {
+			return `((1.0 - ${opac}) * ${cola} + ${opac} * ((vec3(1.0, 1.0, 1.0) - ${cola}) * ${colb} * ${cola} + ${cola} * (vec3(1.0, 1.0, 1.0) - (vec3(1.0, 1.0, 1.0) - ${colb}) * (vec3(1.0, 1.0, 1.0) - ${cola}))))`;
+		}
+		else if (blending == BlendType.BlendLinearLight) {
+			return `(${cola} + ${opac} * (vec3(2.0, 2.0, 2.0) * (${colb} - vec3(0.5, 0.5, 0.5))))`;
+		}
+		else if (blending == BlendType.BlendDifference) {
+			return `mix(${cola}, abs(${cola} - ${colb}), ${opac})`;
+		}
+		else if (blending == BlendType.BlendSubtract) {
+			return `mix(${cola}, ${cola} - ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendDivide) {
+			return `vec3(1.0 - ${opac}, 1.0 - ${opac}, 1.0 - ${opac}) * ${cola} + vec3(${opac}, ${opac}, ${opac}) * ${cola} / ${colb}`;
+		}
+		else if (blending == BlendType.BlendHue) {
+			frag.add_function(ShaderFunctions.str_hue_sat);
+			return `mix(${cola}, hsv_to_rgb(vec3(rgb_to_hsv(${colb}).r, rgb_to_hsv(${cola}).g, rgb_to_hsv(${cola}).b)), ${opac})`;
+		}
+		else if (blending == BlendType.BlendSaturation) {
+			frag.add_function(ShaderFunctions.str_hue_sat);
+			return `mix(${cola}, hsv_to_rgb(vec3(rgb_to_hsv(${cola}).r, rgb_to_hsv(${colb}).g, rgb_to_hsv(${cola}).b)), ${opac})`;
+		}
+		else if (blending == BlendType.BlendColor) {
+			frag.add_function(ShaderFunctions.str_hue_sat);
+			return `mix(${cola}, hsv_to_rgb(vec3(rgb_to_hsv(${colb}).r, rgb_to_hsv(${colb}).g, rgb_to_hsv(${cola}).b)), ${opac})`;
+		}
+		else { // BlendValue
+			frag.add_function(ShaderFunctions.str_hue_sat);
+			return `mix(${cola}, hsv_to_rgb(vec3(rgb_to_hsv(${cola}).r, rgb_to_hsv(${cola}).g, rgb_to_hsv(${colb}).b)), ${opac})`;
+		}
+	}
+
+	static blendModeMask = (frag: NodeShader, blending: i32, cola: string, colb: string, opac: string): string => {
+		if (blending == BlendType.BlendMix) {
+			return `mix(${cola}, ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendDarken) {
+			return `mix(${cola}, min(${cola}, ${colb}), ${opac})`;
+		}
+		else if (blending == BlendType.BlendMultiply) {
+			return `mix(${cola}, ${cola} * ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendBurn) {
+			return `mix(${cola}, 1.0 - (1.0 - ${cola}) / ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendLighten) {
+			return `max(${cola}, ${colb} * ${opac})`;
+		}
+		else if (blending == BlendType.BlendScreen) {
+			return `(1.0 - ((1.0 - ${opac}) + ${opac} * (1.0 - ${colb})) * (1.0 - ${cola}))`;
+		}
+		else if (blending == BlendType.BlendDodge) {
+			return `mix(${cola}, ${cola} / (1.0 - ${colb}), ${opac})`;
+		}
+		else if (blending == BlendType.BlendAdd) {
+			return `mix(${cola}, ${cola} + ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendOverlay) {
+			return `mix(${cola}, ${cola} < 0.5 ? 2.0 * ${cola} * ${colb} : 1.0 - 2.0 * (1.0 - ${cola}) * (1.0 - ${colb}), ${opac})`;
+		}
+		else if (blending == BlendType.BlendSoftLight) {
+			return `((1.0 - ${opac}) * ${cola} + ${opac} * ((1.0 - ${cola}) * ${colb} * ${cola} + ${cola} * (1.0 - (1.0 - ${colb}) * (1.0 - ${cola}))))`;
+		}
+		else if (blending == BlendType.BlendLinearLight) {
+			return `(${cola} + ${opac} * (2.0 * (${colb} - 0.5)))`;
+		}
+		else if (blending == BlendType.BlendDifference) {
+			return `mix(${cola}, abs(${cola} - ${colb}), ${opac})`;
+		}
+		else if (blending == BlendType.BlendSubtract) {
+			return `mix(${cola}, ${cola} - ${colb}, ${opac})`;
+		}
+		else if (blending == BlendType.BlendDivide) {
+			return `(1.0 - ${opac}) * ${cola} + ${opac} * ${cola} / ${colb}`;
+		}
+		else { // BlendHue, BlendSaturation, BlendColor, BlendValue
+			return `mix(${cola}, ${colb}, ${opac})`;
+		}
+	}
+
+	static getDisplaceStrength = (): f32 => {
+		let sc = Context.mainObject().transform.scale.x;
+		return Config.raw.displace_strength * 0.02 * sc;
+	}
+
+	static voxelgiHalfExtents = (): string => {
+		let ext = Context.raw.vxaoExt;
+		return `const vec3 voxelgiHalfExtents = vec3(${ext}, ${ext}, ${ext});`;
+	}
+
+	static deleteContext = (c: ShaderContext) => {
+		Base.notifyOnNextFrame(() => { // Ensure pipeline is no longer in use
+			c.delete();
+		});
+	}
+}

+ 80 - 81
armorpaint/Sources/arm/MakeMesh.hx → armorpaint/Sources/MakeMesh.ts

@@ -1,12 +1,11 @@
-package arm;
 
 class MakeMesh {
 
-	public static var layerPassCount = 1;
+	static layerPassCount = 1;
 
-	public static function run(data: NodeShaderData, layerPass = 0): NodeShaderContext {
-		var context_id = layerPass == 0 ? "mesh" : "mesh" + layerPass;
-		var con_mesh: NodeShaderContext = data.add_context({
+	static run = (data: NodeShaderData, layerPass = 0): NodeShaderContext => {
+		let context_id = layerPass == 0 ? "mesh" : "mesh" + layerPass;
+		let con_mesh: NodeShaderContext = data.add_context({
 			name: context_id,
 			depth_write: layerPass == 0 ? true : false,
 			compare_mode: layerPass == 0 ? "less" : "equal",
@@ -16,8 +15,8 @@ class MakeMesh {
 			depth_attachment: "DEPTH32"
 		});
 
-		var vert = con_mesh.make_vert();
-		var frag = con_mesh.make_frag();
+		let vert = con_mesh.make_vert();
+		let frag = con_mesh.make_frag();
 		frag.ins = vert.outs;
 
 		vert.add_out('vec2 texCoord');
@@ -27,22 +26,22 @@ class MakeMesh {
 		vert.add_uniform('mat4 prevWVP', '_prevWorldViewProjectionMatrix');
 		vert.wposition = true;
 
-		var textureCount = 0;
-		var displaceStrength = MakeMaterial.getDisplaceStrength();
+		let textureCount = 0;
+		let displaceStrength = MakeMaterial.getDisplaceStrength();
 		if (MakeMaterial.heightUsed && displaceStrength > 0.0) {
 			vert.n = true;
 			vert.write('float height = 0.0;');
-			var numLayers = 0;
-			for (l in Project.layers) {
+			let numLayers = 0;
+			for (let l of Project.layers) {
 				if (!l.isVisible() || !l.paintHeight || !l.isLayer()) continue;
 				if (numLayers > 16) break;
 				numLayers++;
 				textureCount++;
 				vert.add_uniform('sampler2D texpaint_pack_vert' + l.id, '_texpaint_pack_vert' + l.id);
 				vert.write('height += textureLod(texpaint_pack_vert' + l.id + ', tex, 0.0).a;');
-				var masks = l.getMasks();
+				let masks = l.getMasks();
 				if (masks != null) {
-					for (m in masks) {
+					for (let m of masks) {
 						if (!m.isVisible()) continue;
 						textureCount++;
 						vert.add_uniform('sampler2D texpaint_vert' + m.id, '_texpaint_vert' + m.id);
@@ -50,7 +49,7 @@ class MakeMesh {
 					}
 				}
 			}
-			vert.write('wposition += wnormal * vec3(height, height, height) * vec3($displaceStrength, $displaceStrength, $displaceStrength);');
+			vert.write(`wposition += wnormal * vec3(height, height, height) * vec3(${displaceStrength}, ${displaceStrength}, ${displaceStrength});`);
 		}
 
 		vert.write('gl_Position = mul(vec4(wposition.xyz, 1.0), VP);');
@@ -67,7 +66,7 @@ class MakeMesh {
 		frag.n = true;
 		frag.add_function(ShaderFunctions.str_packFloatInt16);
 
-		if (Context.raw.tool == ToolColorId) {
+		if (Context.raw.tool == WorkspaceTool.ToolColorId) {
 			textureCount++;
 			frag.add_uniform('sampler2D texcolorid', '_texcolorid');
 			frag.write('fragColor[0] = vec4(n.xy, 1.0, packFloatInt16(0.0, uint(0)));');
@@ -82,9 +81,9 @@ class MakeMesh {
 				frag.add_uniform('sampler2D gbuffer1');
 				frag.add_uniform('sampler2D gbuffer2');
 				frag.write('vec2 fragcoord = (wvpposition.xy / wvpposition.w) * 0.5 + 0.5;');
-				#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+				///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 				frag.write('fragcoord.y = 1.0 - fragcoord.y;');
-				#end
+				///end
 				frag.write('vec4 gbuffer0_sample = textureLod(gbuffer0, fragcoord, 0.0);');
 				frag.write('vec4 gbuffer1_sample = textureLod(gbuffer1, fragcoord, 0.0);');
 				frag.write('vec4 gbuffer2_sample = textureLod(gbuffer2, fragcoord, 0.0);');
@@ -124,15 +123,15 @@ class MakeMesh {
 				frag.add_uniform('sampler2D texuvmap', '_texuvmap');
 			}
 
-			if (Context.raw.viewportMode == ViewMask && Context.raw.layer.getMasks() != null) {
-				for (m in Context.raw.layer.getMasks()) {
+			if (Context.raw.viewportMode == ViewportMode.ViewMask && Context.raw.layer.getMasks() != null) {
+				for (let m of Context.raw.layer.getMasks()) {
 					if (!m.isVisible()) continue;
 					textureCount++;
 					frag.add_uniform('sampler2D texpaint_view_mask' + m.id, '_texpaint' + Project.layers.indexOf(m));
 				}
 			}
 
-			if (Context.raw.viewportMode == ViewLit && Context.raw.renderMode == RenderForward) {
+			if (Context.raw.viewportMode == ViewportMode.ViewLit && Context.raw.renderMode == RenderMode.RenderForward) {
 				textureCount += 4;
 				frag.add_uniform('sampler2D senvmapBrdf', "$brdf.k");
 				frag.add_uniform('sampler2D senvmapRadiance', '_envmapRadiance');
@@ -141,82 +140,82 @@ class MakeMesh {
 			}
 
 			// Get layers for this pass
-			layerPassCount = 1;
-			var layers: Array<SlotLayer> = [];
-			var startCount = textureCount;
-			var isMaterialTool = Context.raw.tool == ToolMaterial;
-			for (l in Project.layers) {
+			MakeMesh.layerPassCount = 1;
+			let layers: SlotLayer[] = [];
+			let startCount = textureCount;
+			let isMaterialTool = Context.raw.tool == WorkspaceTool.ToolMaterial;
+			for (let l of Project.layers) {
 				if (isMaterialTool && l != Context.raw.layer) continue;
 				if (!l.isLayer() || !l.isVisible()) continue;
 
-				var count = 3;
-				var masks = l.getMasks();
+				let count = 3;
+				let masks = l.getMasks();
 				if (masks != null) count += masks.length;
 				textureCount += count;
-				if (textureCount >= getMaxTextures()) {
+				if (textureCount >= MakeMesh.getMaxTextures()) {
 					textureCount = startCount + count + 3; // gbuffer0_copy, gbuffer1_copy, gbuffer2_copy
-					layerPassCount++;
+					MakeMesh.layerPassCount++;
 				}
-				if (layerPass == layerPassCount - 1) {
+				if (layerPass == MakeMesh.layerPassCount - 1) {
 					layers.push(l);
 				}
 			}
 
-			var lastPass = layerPass == layerPassCount - 1;
+			let lastPass = layerPass == MakeMesh.layerPassCount - 1;
 
-			for (l in layers) {
+			for (let l of layers) {
 				if (l.getObjectMask() > 0) {
 					frag.add_uniform('int uid', '_uid');
 					if (l.getObjectMask() > Project.paintObjects.length) { // Atlas
-						var visibles = Project.getAtlasObjects(l.getObjectMask());
+						let visibles = Project.getAtlasObjects(l.getObjectMask());
 						frag.write('if (');
-						for (i in 0...visibles.length) {
+						for (let i = 0; i < visibles.length; ++i) {
 							if (i > 0) frag.write(' || ');
-							frag.write('${visibles[i].uid} == uid');
+							frag.write(`${visibles[i].uid} == uid`);
 						}
 						frag.write(') {');
 					}
 					else { // Object mask
-						var uid = Project.paintObjects[l.getObjectMask() - 1].uid;
-						frag.write('if ($uid == uid) {');
+						let uid = Project.paintObjects[l.getObjectMask() - 1].uid;
+						frag.write(`if (${uid} == uid) {`);
 					}
 				}
 
 				frag.add_shared_sampler('sampler2D texpaint' + l.id);
 				frag.write('texpaint_sample = textureLodShared(texpaint' + l.id + ', texCoord, 0.0);');
 				frag.write('texpaint_opac = texpaint_sample.a;');
-				// #if (krom_direct3d12 || krom_vulkan)
+				// ///if (krom_direct3d12 || krom_vulkan)
 				// if (Context.raw.viewportMode == ViewLit) {
 				// 	frag.write('if (texpaint_opac < 0.1) discard;');
 				// }
-				// #end
+				// ///end
 
-				var masks = l.getMasks();
+				let masks = l.getMasks();
 				if (masks != null) {
-					var hasVisible = false;
-					for (m in masks) {
+					let hasVisible = false;
+					for (let m of masks) {
 						if (m.isVisible()) {
 							hasVisible = true;
 							break;
 						}
 					}
 					if (hasVisible) {
-						var texpaint_mask = 'texpaint_mask' + l.id;
-						frag.write('float $texpaint_mask = 0.0;');
-						for (m in masks) {
+						let texpaint_mask = 'texpaint_mask' + l.id;
+						frag.write(`float ${texpaint_mask} = 0.0;`);
+						for (let m of masks) {
 							if (!m.isVisible()) continue;
 							frag.add_shared_sampler('sampler2D texpaint' + m.id);
 							frag.write('{'); // Group mask is sampled across multiple layers
 							frag.write('float texpaint_mask_sample' + m.id + ' = textureLodShared(texpaint' + m.id + ', texCoord, 0.0).r;');
-							frag.write('$texpaint_mask = ' + MakeMaterial.blendModeMask(frag, m.blending, '$texpaint_mask', 'texpaint_mask_sample' + m.id, 'float(' + m.getOpacity() + ')') + ';');
+							frag.write(`${texpaint_mask} = ` + MakeMaterial.blendModeMask(frag, m.blending, `${texpaint_mask}`, 'texpaint_mask_sample' + m.id, 'float(' + m.getOpacity() + ')') + ';');
 							frag.write('}');
 						}
-						frag.write('texpaint_opac *= clamp($texpaint_mask, 0.0, 1.0);');
+						frag.write(`texpaint_opac *= clamp(${texpaint_mask}, 0.0, 1.0);`);
 					}
 				}
 
 				if (l.getOpacity() < 1) {
-					frag.write('texpaint_opac *= ${l.getOpacity()};');
+					frag.write(`texpaint_opac *= ${l.getOpacity()};`);
 				}
 
 				if (l.paintBase) {
@@ -265,15 +264,15 @@ class MakeMesh {
 						frag.write('metallic = mix(metallic, texpaint_pack_sample.b, texpaint_opac);');
 					}
 					if (l.paintHeight && MakeMaterial.heightUsed) {
-						var assign = l.paintHeightBlend ? "+=" : "=";
-						frag.write('height $assign texpaint_pack_sample.a * texpaint_opac;');
+						let assign = l.paintHeightBlend ? "+=" : "=";
+						frag.write(`height ${assign} texpaint_pack_sample.a * texpaint_opac;`);
 						frag.write('{');
 						frag.add_uniform('vec2 texpaintSize', '_texpaintSize');
 						frag.write('float tex_step = 1.0 / texpaintSize.x;');
-						frag.write('height0 $assign textureLodShared(texpaint_pack' + l.id + ', vec2(texCoord.x - tex_step, texCoord.y), 0.0).a * texpaint_opac;');
-						frag.write('height1 $assign textureLodShared(texpaint_pack' + l.id + ', vec2(texCoord.x + tex_step, texCoord.y), 0.0).a * texpaint_opac;');
-						frag.write('height2 $assign textureLodShared(texpaint_pack' + l.id + ', vec2(texCoord.x, texCoord.y - tex_step), 0.0).a * texpaint_opac;');
-						frag.write('height3 $assign textureLodShared(texpaint_pack' + l.id + ', vec2(texCoord.x, texCoord.y + tex_step), 0.0).a * texpaint_opac;');
+						frag.write(`height0 ${assign} textureLodShared(texpaint_pack` + l.id + ', vec2(texCoord.x - tex_step, texCoord.y), 0.0).a * texpaint_opac;');
+						frag.write(`height1 ${assign} textureLodShared(texpaint_pack` + l.id + ', vec2(texCoord.x + tex_step, texCoord.y), 0.0).a * texpaint_opac;');
+						frag.write(`height2 ${assign} textureLodShared(texpaint_pack` + l.id + ', vec2(texCoord.x, texCoord.y - tex_step), 0.0).a * texpaint_opac;');
+						frag.write(`height3 ${assign} textureLodShared(texpaint_pack` + l.id + ', vec2(texCoord.x, texCoord.y + tex_step), 0.0).a * texpaint_opac;');
 						frag.write('}');
 					}
 				}
@@ -322,23 +321,23 @@ class MakeMesh {
 			}
 
 			frag.vVec = true;
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('mat3 TBN = cotangentFrame(n, vVec, texCoord);');
-			#else
+			///else
 			frag.write('mat3 TBN = cotangentFrame(n, -vVec, texCoord);');
-			#end
+			///end
 			frag.write('n = ntex * 2.0 - 1.0;');
 			frag.write('n.y = -n.y;');
 			frag.write('n = normalize(mul(n, TBN));');
 
-			if (Context.raw.viewportMode == ViewLit || Context.raw.viewportMode == ViewPathTrace) {
+			if (Context.raw.viewportMode == ViewportMode.ViewLit || Context.raw.viewportMode == ViewportMode.ViewPathTrace) {
 				frag.write('basecol = pow(basecol, vec3(2.2, 2.2, 2.2));');
 
 				if (Context.raw.viewportShader != null) {
-					var color = Context.raw.viewportShader(frag);
-					frag.write('fragColor[1] = vec4($color, 1.0);');
+					let color = Context.raw.viewportShader(frag);
+					frag.write(`fragColor[1] = vec4(${color}, 1.0);`);
 				}
-				else if (Context.raw.renderMode == RenderForward && Context.raw.viewportMode != ViewPathTrace) {
+				else if (Context.raw.renderMode == RenderMode.RenderForward && Context.raw.viewportMode != ViewportMode.ViewPathTrace) {
 					frag.wposition = true;
 					frag.write('vec3 albedo = mix(basecol, vec3(0.0, 0.0, 0.0), metallic);');
 					frag.write('vec3 f0 = mix(vec3(0.04, 0.04, 0.04), basecol, metallic);');
@@ -388,43 +387,43 @@ class MakeMesh {
 					frag.write('fragColor[1] = vec4(basecol, occlusion);');
 				}
 			}
-			else if (Context.raw.viewportMode == ViewBaseColor && Context.raw.layer.paintBase) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewBaseColor && Context.raw.layer.paintBase) {
 				frag.write('fragColor[1] = vec4(basecol, 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewNormalMap && Context.raw.layer.paintNor) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewNormalMap && Context.raw.layer.paintNor) {
 				frag.write('fragColor[1] = vec4(ntex.rgb, 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewOcclusion && Context.raw.layer.paintOcc) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewOcclusion && Context.raw.layer.paintOcc) {
 				frag.write('fragColor[1] = vec4(vec3(occlusion, occlusion, occlusion), 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewRoughness && Context.raw.layer.paintRough) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewRoughness && Context.raw.layer.paintRough) {
 				frag.write('fragColor[1] = vec4(vec3(roughness, roughness, roughness), 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewMetallic && Context.raw.layer.paintMet) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewMetallic && Context.raw.layer.paintMet) {
 				frag.write('fragColor[1] = vec4(vec3(metallic, metallic, metallic), 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewOpacity && Context.raw.layer.paintOpac) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewOpacity && Context.raw.layer.paintOpac) {
 				frag.write('fragColor[1] = vec4(vec3(texpaint_sample.a, texpaint_sample.a, texpaint_sample.a), 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewHeight && Context.raw.layer.paintHeight) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewHeight && Context.raw.layer.paintHeight) {
 				frag.write('fragColor[1] = vec4(vec3(height, height, height), 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewEmission) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewEmission) {
 				frag.write('float emis = int(matid * 255.0) % 3 == 1 ? 1.0 : 0.0;');
 				frag.write('fragColor[1] = vec4(vec3(emis, emis, emis), 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewSubsurface) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewSubsurface) {
 				frag.write('float subs = int(matid * 255.0) % 3 == 2 ? 1.0 : 0.0;');
 				frag.write('fragColor[1] = vec4(vec3(subs, subs, subs), 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewTexCoord) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewTexCoord) {
 				frag.write('fragColor[1] = vec4(texCoord, 0.0, 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewObjectNormal) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewObjectNormal) {
 				frag.nAttr = true;
 				frag.write('fragColor[1] = vec4(nAttr, 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewMaterialID) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewMaterialID) {
 				frag.add_shared_sampler('sampler2D texpaint_nor' + Context.raw.layer.id);
 				frag.add_uniform('vec2 texpaintSize', '_texpaintSize');
 				frag.write('float sample_matid = texelFetch(texpaint_nor' + Context.raw.layer.id + ', ivec2(texCoord * texpaintSize), 0).a + 1.0 / 255.0;');
@@ -433,7 +432,7 @@ class MakeMesh {
 				frag.write('float matid_b = fract(sin(dot(vec2(sample_matid, sample_matid * 40.0), vec2(12.9898, 78.233))) * 43758.5453);');
 				frag.write('fragColor[1] = vec4(matid_r, matid_g, matid_b, 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewObjectID) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewObjectID) {
 				frag.add_uniform('float objectId', '_objectId');
 				frag.write('float obid = objectId + 1.0 / 255.0;');
 				frag.write('float id_r = fract(sin(dot(vec2(obid, obid * 20.0), vec2(12.9898, 78.233))) * 43758.5453);');
@@ -441,13 +440,13 @@ class MakeMesh {
 				frag.write('float id_b = fract(sin(dot(vec2(obid, obid * 40.0), vec2(12.9898, 78.233))) * 43758.5453);');
 				frag.write('fragColor[1] = vec4(id_r, id_g, id_b, 1.0);');
 			}
-			else if (Context.raw.viewportMode == ViewMask && (Context.raw.layer.getMasks() != null || Context.raw.layer.isMask())) {
+			else if (Context.raw.viewportMode == ViewportMode.ViewMask && (Context.raw.layer.getMasks() != null || Context.raw.layer.isMask())) {
 				if (Context.raw.layer.isMask()) {
 					frag.write('float mask_view = textureLodShared(texpaint' + Context.raw.layer.id + ', texCoord, 0.0).r;');
 				}
 				else {
 					frag.write('float mask_view = 0.0;');
-					for (m in Context.raw.layer.getMasks()) {
+					for (let m of Context.raw.layer.getMasks()) {
 						if (!m.isVisible()) continue;
 						frag.write('float mask_sample' + m.id + ' = textureLodShared(texpaint_view_mask' + m.id + ', texCoord, 0.0).r;');
 						frag.write('mask_view = ' + MakeMaterial.blendModeMask(frag, m.blending, 'mask_view', 'mask_sample' + m.id, 'float(' + m.getOpacity() + ')') + ';');
@@ -459,7 +458,7 @@ class MakeMesh {
 				frag.write('fragColor[1] = vec4(1.0, 0.0, 1.0, 1.0);'); // Pink
 			}
 
-			if (Context.raw.viewportMode != ViewLit && Context.raw.viewportMode != ViewPathTrace) {
+			if (Context.raw.viewportMode != ViewportMode.ViewLit && Context.raw.viewportMode != ViewportMode.ViewPathTrace) {
 				frag.write('fragColor[1].rgb = pow(fragColor[1].rgb, vec3(2.2, 2.2, 2.2));');
 			}
 
@@ -479,11 +478,11 @@ class MakeMesh {
 		return con_mesh;
 	}
 
-	static inline function getMaxTextures(): Int {
-		#if krom_direct3d11
+	static getMaxTextures = (): i32 => {
+		///if krom_direct3d11
 		return 128 - 66;
-		#else
+		///else
 		return 16 - 3; // G4onG5/G4.c.h MAX_TEXTURES
-		#end
+		///end
 	}
 }

+ 41 - 45
armorpaint/Sources/arm/MakeMeshPreview.hx → armorpaint/Sources/MakeMeshPreview.ts

@@ -1,15 +1,11 @@
-package arm;
-
-import iron.MeshObject;
-import iron.SceneFormat;
 
 class MakeMeshPreview {
 
-	public static var opacityDiscardDecal = 0.05;
+	static opacityDiscardDecal = 0.05;
 
-	public static function run(data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext {
-		var context_id = "mesh";
-		var con_mesh: NodeShaderContext = data.add_context({
+	static run = (data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext => {
+		let context_id = "mesh";
+		let con_mesh: NodeShaderContext = data.add_context({
 			name: context_id,
 			depth_write: true,
 			compare_mode: "less",
@@ -19,13 +15,13 @@ class MakeMeshPreview {
 			depth_attachment: "DEPTH32"
 		});
 
-		var vert = con_mesh.make_vert();
-		var frag = con_mesh.make_frag();
+		let vert = con_mesh.make_vert();
+		let frag = con_mesh.make_frag();
 		frag.ins = vert.outs;
-		var pos = "pos";
+		let pos = "pos";
 
-		#if arm_skin
-		var skin = Context.raw.paintObject.data.geom.getVArray("bone") != null;
+		///if arm_skin
+		let skin = Context.raw.paintObject.data.getVArray("bone") != null;
 		if (skin) {
 			pos = "spos";
 			con_mesh.add_elem("bone", 'short4norm');
@@ -42,58 +38,58 @@ class MakeMeshPreview {
 			vert.write_attrib('spos.xyz += 2.0 * (skinA.w * skinB.xyz - skinB.w * skinA.xyz + cross(skinA.xyz, skinB.xyz));');
 			vert.write_attrib('spos.xyz /= posUnpack;');
 		}
-		#end
+		///end
 
 		vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix');
-		vert.write_attrib('gl_Position = mul(vec4($pos.xyz, 1.0), WVP);');
+		vert.write_attrib(`gl_Position = mul(vec4(${pos}.xyz, 1.0), WVP);`);
 
-		var brushScale = (Context.raw.brushScale * Context.raw.brushNodesScale) + "";
+		let brushScale = (Context.raw.brushScale * Context.raw.brushNodesScale) + "";
 		vert.add_out('vec2 texCoord');
-		vert.write_attrib('texCoord = tex * float(${brushScale});');
+		vert.write_attrib(`texCoord = tex * float(${brushScale});`);
 
-		var decal = Context.raw.decalPreview;
+		let decal = Context.raw.decalPreview;
 		ParserMaterial.sample_keep_aspect = decal;
 		ParserMaterial.sample_uv_scale = brushScale;
 		ParserMaterial.parse_height = MakeMaterial.heightUsed;
 		ParserMaterial.parse_height_as_channel = true;
-		var sout = ParserMaterial.parse(UINodes.inst.getCanvasMaterial(), con_mesh, vert, frag, matcon);
+		let sout = ParserMaterial.parse(UINodes.inst.getCanvasMaterial(), con_mesh, vert, frag, matcon);
 		ParserMaterial.parse_height = false;
 		ParserMaterial.parse_height_as_channel = false;
 		ParserMaterial.sample_keep_aspect = false;
-		var base = sout.out_basecol;
-		var rough = sout.out_roughness;
-		var met = sout.out_metallic;
-		var occ = sout.out_occlusion;
-		var opac = sout.out_opacity;
-		var height = sout.out_height;
-		var nortan = ParserMaterial.out_normaltan;
-		frag.write('vec3 basecol = pow($base, vec3(2.2, 2.2, 2.2));');
-		frag.write('float roughness = $rough;');
-		frag.write('float metallic = $met;');
-		frag.write('float occlusion = $occ;');
-		frag.write('float opacity = $opac;');
-		frag.write('vec3 nortan = $nortan;');
-		frag.write('float height = $height;');
+		let base = sout.out_basecol;
+		let rough = sout.out_roughness;
+		let met = sout.out_metallic;
+		let occ = sout.out_occlusion;
+		let opac = sout.out_opacity;
+		let height = sout.out_height;
+		let nortan = ParserMaterial.out_normaltan;
+		frag.write(`vec3 basecol = pow(${base}, vec3(2.2, 2.2, 2.2));`);
+		frag.write(`float roughness = ${rough};`);
+		frag.write(`float metallic = ${met};`);
+		frag.write(`float occlusion = ${occ};`);
+		frag.write(`float opacity = ${opac};`);
+		frag.write(`vec3 nortan = ${nortan};`);
+		frag.write(`float height = ${height};`);
 
 		// ParserMaterial.parse_height_as_channel = false;
-		// vert.write('float vheight = $height;');
+		// vert.write(`float vheight = ${height};`);
 		// vert.add_out('float height');
 		// vert.write('height = vheight;');
-		// var displaceStrength = 0.1;
+		// let displaceStrength = 0.1;
 		// if (MakeMaterial.heightUsed && displaceStrength > 0.0) {
-		// 	vert.write('vec3 pos2 = $pos.xyz + vec3(nor.xy, pos.w) * vec3($height, $height, $height) * vec3($displaceStrength, $displaceStrength, $displaceStrength);');
+		// 	vert.write(`vec3 pos2 = ${pos}.xyz + vec3(nor.xy, pos.w) * vec3(${height}, ${height}, ${height}) * vec3(${displaceStrength}, ${displaceStrength}, ${displaceStrength});`);
 		// 	vert.write('gl_Position = mul(vec4(pos2.xyz, 1.0), WVP);');
 		// }
 
 		if (decal) {
-			if (Context.raw.tool == ToolText) {
+			if (Context.raw.tool == WorkspaceTool.ToolText) {
 				frag.add_uniform('sampler2D textexttool', '_textexttool');
-				frag.write('opacity *= textureLod(textexttool, texCoord / float(${brushScale}), 0.0).r;');
+				frag.write(`opacity *= textureLod(textexttool, texCoord / float(${brushScale}), 0.0).r;`);
 			}
 		}
 		if (decal) {
-			var opac = opacityDiscardDecal;
-			frag.write('if (opacity < $opac) discard;');
+			let opac = MakeMeshPreview.opacityDiscardDecal;
+			frag.write(`if (opacity < ${opac}) discard;`);
 		}
 
 		frag.add_out('vec4 fragColor[3]');
@@ -122,11 +118,11 @@ class MakeMeshPreview {
 		}
 		else {
 			frag.vVec = true;
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('mat3 TBN = cotangentFrame(n, vVec, texCoord);');
-			#else
+			///else
 			frag.write('mat3 TBN = cotangentFrame(n, -vVec, texCoord);');
-			#end
+			///end
 			frag.write('n = nortan * 2.0 - 1.0;');
 			frag.write('n.y = -n.y;');
 			frag.write('n = normalize(mul(n, TBN));');
@@ -148,11 +144,11 @@ class MakeMeshPreview {
 
 		ParserMaterial.finalize(con_mesh);
 
-		#if arm_skin
+		///if arm_skin
 		if (skin) {
 			vert.write('wnormal = normalize(mul(vec3(nor.xy, pos.w) + 2.0 * cross(skinA.xyz, cross(skinA.xyz, vec3(nor.xy, pos.w)) + skinA.w * vec3(nor.xy, pos.w)), N));');
 		}
-		#end
+		///end
 
 		con_mesh.data.shader_from_source = true;
 		con_mesh.data.vertex_shader = vert.get();

+ 14 - 21
armorpaint/Sources/arm/MakeNodePreview.hx → armorpaint/Sources/MakeNodePreview.ts

@@ -1,16 +1,9 @@
-package arm;
-
-import zui.Zui.Nodes;
-import zui.Zui.TNode;
-import zui.Zui.TNodeCanvas;
-import zui.Zui.TNodeLink;
-import iron.SceneFormat;
 
 class MakeNodePreview {
 
-	public static function run(data: NodeShaderData, matcon: TMaterialContext, node: TNode, group: TNodeCanvas, parents: Array<TNode>): NodeShaderContext {
-		var context_id = "mesh";
-		var con_mesh: NodeShaderContext = data.add_context({
+	static run = (data: NodeShaderData, matcon: TMaterialContext, node: TNode, group: TNodeCanvas, parents: TNode[]): NodeShaderContext => {
+		let context_id = "mesh";
+		let con_mesh: NodeShaderContext = data.add_context({
 			name: context_id,
 			depth_write: false,
 			compare_mode: "always",
@@ -20,17 +13,17 @@ class MakeNodePreview {
 		});
 
 		con_mesh.allow_vcols = true;
-		var vert = con_mesh.make_vert();
-		var frag = con_mesh.make_frag();
+		let vert = con_mesh.make_vert();
+		let frag = con_mesh.make_frag();
 		frag.ins = vert.outs;
 
 		vert.write_attrib('gl_Position = vec4(pos.xy * 3.0, 0.0, 1.0);'); // Pos unpack
 		vert.write_attrib('const vec2 madd = vec2(0.5, 0.5);');
 		vert.add_out('vec2 texCoord');
 		vert.write_attrib('texCoord = gl_Position.xy * madd + madd;');
-		#if (!krom_opengl)
+		///if (!krom_opengl)
 		vert.write_attrib('texCoord.y = 1.0 - texCoord.y;');
-		#end
+		///end
 
 		ParserMaterial.init();
 		ParserMaterial.canvases = [Context.raw.material.canvas];
@@ -40,10 +33,10 @@ class MakeNodePreview {
 			ParserMaterial.push_group(group);
 			ParserMaterial.parents = parents;
 		}
-		var links = ParserMaterial.links;
-		var nodes = Context.raw.material.nodes;
+		let links = ParserMaterial.links;
+		let nodes = Context.raw.material.nodes;
 
-		var link: TNodeLink = { id: nodes.getLinkId(links), from_id: node.id, from_socket: Context.raw.nodePreviewSocket, to_id: -1, to_socket: -1 };
+		let link: TNodeLink = { id: nodes.getLinkId(links), from_id: node.id, from_socket: Context.raw.nodePreviewSocket, to_id: -1, to_socket: -1 };
 		links.push(link);
 
 		ParserMaterial.con = con_mesh;
@@ -53,16 +46,16 @@ class MakeNodePreview {
 		ParserMaterial.matcon = matcon;
 
 		ParserMaterial.transform_color_space = false;
-		var res = ParserMaterial.write_result(link);
+		let res = ParserMaterial.write_result(link);
 		ParserMaterial.transform_color_space = true;
-		var st = node.outputs[link.from_socket].type;
+		let st = node.outputs[link.from_socket].type;
 		if (st != "RGB" && st != "RGBA" && st != "VECTOR") {
 			res = ParserMaterial.to_vec3(res);
 		}
-		links.remove(link);
+		array_remove(links, link);
 
 		frag.add_out('vec4 fragColor');
-		frag.write('vec3 basecol = $res;');
+		frag.write(`vec3 basecol = ${res};`);
 		frag.write('fragColor = vec4(basecol.rgb, 1.0);');
 
 		// frag.ndcpos = true;

+ 105 - 106
armorpaint/Sources/arm/MakePaint.hx → armorpaint/Sources/MakePaint.ts

@@ -1,19 +1,18 @@
-package arm;
-
-import iron.SceneFormat;
 
 class MakePaint {
 
-	public static function run(data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext {
-		var context_id = "paint";
+	static get isRaytracedBake(): bool {
+		///if (krom_direct3d12 || krom_vulkan || krom_metal)
+		return Context.raw.bakeType == BakeType.BakeInit;
+		///else
+		return false;
+		///end
+	}
 
-		#if (krom_direct3d12 || krom_vulkan || krom_metal)
-		var isRaytracedBake = Context.raw.bakeType == BakeInit;
-		#else
-		var isRaytracedBake = false;
-		#end
+	static run = (data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext => {
+		let context_id = "paint";
 
-		var con_paint:NodeShaderContext = data.add_context({
+		let con_paint: NodeShaderContext = data.add_context({
 			name: context_id,
 			depth_write: false,
 			compare_mode: "always", // TODO: align texcoords winding order
@@ -21,10 +20,10 @@ class MakePaint {
 			cull_mode: "none",
 			vertex_elements: [{name: "pos", data: "short4norm"}, {name: "nor", data: "short2norm"}, {name: "tex", data: "short2norm"}],
 			color_attachments:
-				Context.raw.tool == ToolColorId ? ["RGBA32"] :
-				(Context.raw.tool == ToolPicker && Context.raw.pickPosNorTex) ? ["RGBA128", "RGBA128"] :
-				(Context.raw.tool == ToolPicker || Context.raw.tool == ToolMaterial) ? ["RGBA32", "RGBA32", "RGBA32", "RGBA32"] :
-				(Context.raw.tool == ToolBake && isRaytracedBake) ? ["RGBA64", "RGBA64"] :
+				Context.raw.tool == WorkspaceTool.ToolColorId ? ["RGBA32"] :
+				(Context.raw.tool == WorkspaceTool.ToolPicker && Context.raw.pickPosNorTex) ? ["RGBA128", "RGBA128"] :
+				(Context.raw.tool == WorkspaceTool.ToolPicker || Context.raw.tool == WorkspaceTool.ToolMaterial) ? ["RGBA32", "RGBA32", "RGBA32", "RGBA32"] :
+				(Context.raw.tool == WorkspaceTool.ToolBake && MakePaint.isRaytracedBake) ? ["RGBA64", "RGBA64"] :
 					["RGBA32", "RGBA32", "RGBA32", "R8"]
 		});
 
@@ -34,12 +33,12 @@ class MakePaint {
 		con_paint.data.color_writes_alpha = [true, true, true, true];
 		con_paint.allow_vcols = Context.raw.paintObject.data.cols != null;
 
-		var vert = con_paint.make_vert();
-		var frag = con_paint.make_frag();
+		let vert = con_paint.make_vert();
+		let frag = con_paint.make_frag();
 		frag.ins = vert.outs;
 
-		#if (krom_direct3d12 || krom_vulkan || krom_metal)
-		if (Context.raw.tool == ToolBake && Context.raw.bakeType == BakeInit) {
+		///if (krom_direct3d12 || krom_vulkan || krom_metal)
+		if (Context.raw.tool == WorkspaceTool.ToolBake && Context.raw.bakeType == BakeType.BakeInit) {
 			// Init raytraced bake
 			MakeBake.positionAndNormal(vert, frag);
 			con_paint.data.shader_from_source = true;
@@ -47,13 +46,13 @@ class MakePaint {
 			con_paint.data.fragment_shader = frag.get();
 			return con_paint;
 		}
-		#end
+		///end
 
-		if (Context.raw.tool == ToolBake) {
+		if (Context.raw.tool == WorkspaceTool.ToolBake) {
 			MakeBake.setColorWrites(con_paint);
 		}
 
-		if (Context.raw.tool == ToolColorId || Context.raw.tool == ToolPicker || Context.raw.tool == ToolMaterial) {
+		if (Context.raw.tool == WorkspaceTool.ToolColorId || Context.raw.tool == WorkspaceTool.ToolPicker || Context.raw.tool == WorkspaceTool.ToolMaterial) {
 			MakeColorIdPicker.run(vert, frag);
 			con_paint.data.shader_from_source = true;
 			con_paint.data.vertex_shader = vert.get();
@@ -61,19 +60,19 @@ class MakePaint {
 			return con_paint;
 		}
 
-		var faceFill = Context.raw.tool == ToolFill && Context.raw.fillTypeHandle.position == FillFace;
-		var uvIslandFill = Context.raw.tool == ToolFill && Context.raw.fillTypeHandle.position == FillUVIsland;
-		var decal = Context.raw.tool == ToolDecal || Context.raw.tool == ToolText;
+		let faceFill = Context.raw.tool == WorkspaceTool.ToolFill && Context.raw.fillTypeHandle.position == FillType.FillFace;
+		let uvIslandFill = Context.raw.tool == WorkspaceTool.ToolFill && Context.raw.fillTypeHandle.position == FillType.FillUVIsland;
+		let decal = Context.raw.tool == WorkspaceTool.ToolDecal || Context.raw.tool == WorkspaceTool.ToolText;
 
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		vert.write('vec2 tpos = vec2(tex.x * 2.0 - 1.0, (1.0 - tex.y) * 2.0 - 1.0);');
-		#else
+		///else
 		vert.write('vec2 tpos = vec2(tex.xy * 2.0 - 1.0);');
-		#end
+		///end
 
 		vert.write('gl_Position = vec4(tpos, 0.0, 1.0);');
 
-		var decalLayer = Context.raw.layer.fill_layer != null && Context.raw.layer.uvType == UVProject;
+		let decalLayer = Context.raw.layer.fill_layer != null && Context.raw.layer.uvType == UVType.UVProject;
 		if (decalLayer) {
 			vert.add_uniform('mat4 WVP', '_decalLayerMatrix');
 		}
@@ -88,8 +87,8 @@ class MakePaint {
 		frag.write_attrib('sp.y = 1.0 - sp.y;');
 		frag.write_attrib('sp.z -= 0.0001;'); // small bias
 
-		var uvType = Context.raw.layer.fill_layer != null ? Context.raw.layer.uvType : Context.raw.brushPaint;
-		if (uvType == UVProject) frag.ndcpos = true;
+		let uvType = Context.raw.layer.fill_layer != null ? Context.raw.layer.uvType : Context.raw.brushPaint;
+		if (uvType == UVType.UVProject) frag.ndcpos = true;
 
 		frag.add_uniform('vec4 inp', '_inputBrush');
 		frag.add_uniform('vec4 inplast', '_inputBrushLast');
@@ -106,36 +105,36 @@ class MakePaint {
 		frag.add_uniform('float brushOpacity', '_brushOpacity');
 		frag.add_uniform('float brushHardness', '_brushHardness');
 
-		if (Context.raw.tool == ToolBrush  ||
-			Context.raw.tool == ToolEraser ||
-			Context.raw.tool == ToolClone  ||
-			Context.raw.tool == ToolBlur   ||
-			Context.raw.tool == ToolSmudge   ||
-			Context.raw.tool == ToolParticle ||
+		if (Context.raw.tool == WorkspaceTool.ToolBrush  ||
+			Context.raw.tool == WorkspaceTool.ToolEraser ||
+			Context.raw.tool == WorkspaceTool.ToolClone  ||
+			Context.raw.tool == WorkspaceTool.ToolBlur   ||
+			Context.raw.tool == WorkspaceTool.ToolSmudge   ||
+			Context.raw.tool == WorkspaceTool.ToolParticle ||
 			decal) {
 
-			var depthReject = !Context.raw.xray;
+			let depthReject = !Context.raw.xray;
 			if (Config.raw.brush_3d && !Config.raw.brush_depth_reject) depthReject = false;
 
 			// TODO: sp.z needs to take height channel into account
-			var particle = Context.raw.tool == ToolParticle;
+			let particle = Context.raw.tool == WorkspaceTool.ToolParticle;
 			if (Config.raw.brush_3d && !decal && !particle) {
 				if (MakeMaterial.heightUsed || Context.raw.symX || Context.raw.symY || Context.raw.symZ) depthReject = false;
 			}
 
 			if (depthReject) {
-				#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+				///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 				frag.write('if (sp.z > textureLod(gbufferD, sp.xy, 0.0).r + 0.0005) discard;');
-				#else
+				///else
 				frag.write('if (sp.z > textureLod(gbufferD, vec2(sp.x, 1.0 - sp.y), 0.0).r + 0.0005) discard;');
-				#end
+				///end
 			}
 
 			MakeBrush.run(vert, frag);
 		}
 		else { // Fill, Bake
 			frag.write('float dist = 0.0;');
-			var angleFill = Context.raw.tool == ToolFill && Context.raw.fillTypeHandle.position == FillAngle;
+			let angleFill = Context.raw.tool == WorkspaceTool.ToolFill && Context.raw.fillTypeHandle.position == FillType.FillAngle;
 			if (angleFill) {
 				frag.add_function(ShaderFunctions.str_octahedronWrap);
 				frag.add_uniform('sampler2D gbuffer0');
@@ -145,16 +144,16 @@ class MakePaint {
 				frag.write('wn.xy = wn.z >= 0.0 ? g0.xy : octahedronWrap(g0.xy);');
 				frag.write('wn = normalize(wn);');
 				frag.n = true;
-				var angle = Context.raw.brushAngleRejectDot;
-				frag.write('if (dot(wn, n) < $angle) discard;');
+				let angle = Context.raw.brushAngleRejectDot;
+				frag.write(`if (dot(wn, n) < ${angle}) discard;`);
 			}
-			var stencilFill = Context.raw.tool == ToolFill && Context.raw.brushStencilImage != null;
+			let stencilFill = Context.raw.tool == WorkspaceTool.ToolFill && Context.raw.brushStencilImage != null;
 			if (stencilFill) {
-				#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+				///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 				frag.write('if (sp.z > textureLod(gbufferD, sp.xy, 0.0).r + 0.0005) discard;');
-				#else
+				///else
 				frag.write('if (sp.z > textureLod(gbufferD, vec2(sp.x, 1.0 - sp.y), 0.0).r + 0.0005) discard;');
-				#end
+				///end
 			}
 		}
 
@@ -172,20 +171,20 @@ class MakePaint {
 			}
 		}
 
-		if (Context.raw.pickerMaskHandle.position == MaskMaterial) {
+		if (Context.raw.pickerMaskHandle.position == PickerMask.MaskMaterial) {
 			MakeDiscard.materialId(vert, frag);
 		}
 
 		MakeTexcoord.run(vert, frag);
 
-		if (Context.raw.tool == ToolClone || Context.raw.tool == ToolBlur || Context.raw.tool == ToolSmudge) {
+		if (Context.raw.tool == WorkspaceTool.ToolClone || Context.raw.tool == WorkspaceTool.ToolBlur || Context.raw.tool == WorkspaceTool.ToolSmudge) {
 			frag.add_uniform('sampler2D gbuffer2');
 			frag.add_uniform('vec2 gbufferSize', '_gbufferSize');
 			frag.add_uniform('sampler2D texpaint_undo', '_texpaint_undo');
 			frag.add_uniform('sampler2D texpaint_nor_undo', '_texpaint_nor_undo');
 			frag.add_uniform('sampler2D texpaint_pack_undo', '_texpaint_pack_undo');
 
-			if (Context.raw.tool == ToolClone) {
+			if (Context.raw.tool == WorkspaceTool.ToolClone) {
 				MakeClone.run(vert, frag);
 			}
 			else { // Blur, Smudge
@@ -197,51 +196,51 @@ class MakePaint {
 			ParserMaterial.parse_subsurface = Context.raw.material.paintSubs;
 			ParserMaterial.parse_height = Context.raw.material.paintHeight;
 			ParserMaterial.parse_height_as_channel = true;
-			var uvType = Context.raw.layer.fill_layer != null ? Context.raw.layer.uvType : Context.raw.brushPaint;
-			ParserMaterial.triplanar = uvType == UVTriplanar && !decal;
+			let uvType = Context.raw.layer.fill_layer != null ? Context.raw.layer.uvType : Context.raw.brushPaint;
+			ParserMaterial.triplanar = uvType == UVType.UVTriplanar && !decal;
 			ParserMaterial.sample_keep_aspect = decal;
 			ParserMaterial.sample_uv_scale = 'brushScale';
-			var sout = ParserMaterial.parse(UINodes.inst.getCanvasMaterial(), con_paint, vert, frag, matcon);
+			let sout = ParserMaterial.parse(UINodes.inst.getCanvasMaterial(), con_paint, vert, frag, matcon);
 			ParserMaterial.parse_emission = false;
 			ParserMaterial.parse_subsurface = false;
 			ParserMaterial.parse_height_as_channel = false;
 			ParserMaterial.parse_height = false;
-			var base = sout.out_basecol;
-			var rough = sout.out_roughness;
-			var met = sout.out_metallic;
-			var occ = sout.out_occlusion;
-			var nortan = ParserMaterial.out_normaltan;
-			var height = sout.out_height;
-			var opac = sout.out_opacity;
-			var emis = sout.out_emission;
-			var subs = sout.out_subsurface;
-			frag.write('vec3 basecol = $base;');
-			frag.write('float roughness = $rough;');
-			frag.write('float metallic = $met;');
-			frag.write('float occlusion = $occ;');
-			frag.write('vec3 nortan = $nortan;');
-			frag.write('float height = $height;');
-			frag.write('float mat_opacity = $opac;');
+			let base = sout.out_basecol;
+			let rough = sout.out_roughness;
+			let met = sout.out_metallic;
+			let occ = sout.out_occlusion;
+			let nortan = ParserMaterial.out_normaltan;
+			let height = sout.out_height;
+			let opac = sout.out_opacity;
+			let emis = sout.out_emission;
+			let subs = sout.out_subsurface;
+			frag.write(`vec3 basecol = ${base};`);
+			frag.write(`float roughness = ${rough};`);
+			frag.write(`float metallic = ${met};`);
+			frag.write(`float occlusion = ${occ};`);
+			frag.write(`vec3 nortan = ${nortan};`);
+			frag.write(`float height = ${height};`);
+			frag.write(`float mat_opacity = ${opac};`);
 			frag.write('float opacity = mat_opacity;');
 			if (Context.raw.layer.fill_layer == null) {
 				frag.write('opacity *= brushOpacity;');
 			}
 			if (Context.raw.material.paintEmis) {
-				frag.write('float emis = $emis;');
+				frag.write(`float emis = ${emis};`);
 			}
 			if (Context.raw.material.paintSubs) {
-				frag.write('float subs = $subs;');
+				frag.write(`float subs = ${subs};`);
 			}
-			if (Std.parseFloat(height) != 0.0 && !MakeMaterial.heightUsed) {
+			if (parseFloat(height) != 0.0 && !MakeMaterial.heightUsed) {
 				MakeMaterial.heightUsed = true;
 				// Height used for the first time, also rebuild vertex shader
-				return run(data, matcon);
+				return MakePaint.run(data, matcon);
 			}
-			if (Std.parseFloat(emis) != 0.0) MakeMaterial.emisUsed = true;
-			if (Std.parseFloat(subs) != 0.0) MakeMaterial.subsUsed = true;
+			if (parseFloat(emis) != 0.0) MakeMaterial.emisUsed = true;
+			if (parseFloat(subs) != 0.0) MakeMaterial.subsUsed = true;
 		}
 
-		if (Context.raw.brushMaskImage != null && Context.raw.tool == ToolDecal) {
+		if (Context.raw.brushMaskImage != null && Context.raw.tool == WorkspaceTool.ToolDecal) {
 			frag.add_uniform('sampler2D texbrushmask', '_texbrushmask');
 			frag.write('vec4 mask_sample = textureLod(texbrushmask, uvsp, 0.0);');
 			if (Context.raw.brushMaskImageIsAlpha) {
@@ -251,19 +250,19 @@ class MakePaint {
 				frag.write('opacity *= mask_sample.r * mask_sample.a;');
 			}
 		}
-		else if (Context.raw.tool == ToolText) {
+		else if (Context.raw.tool == WorkspaceTool.ToolText) {
 			frag.add_uniform('sampler2D textexttool', '_textexttool');
 			frag.write('opacity *= textureLod(textexttool, uvsp, 0.0).r;');
 		}
 
 		if (Context.raw.brushStencilImage != null && (
-			Context.raw.tool == ToolBrush  ||
-			Context.raw.tool == ToolEraser ||
-			Context.raw.tool == ToolFill ||
-			Context.raw.tool == ToolClone  ||
-			Context.raw.tool == ToolBlur   ||
-			Context.raw.tool == ToolSmudge   ||
-			Context.raw.tool == ToolParticle ||
+			Context.raw.tool == WorkspaceTool.ToolBrush  ||
+			Context.raw.tool == WorkspaceTool.ToolEraser ||
+			Context.raw.tool == WorkspaceTool.ToolFill ||
+			Context.raw.tool == WorkspaceTool.ToolClone  ||
+			Context.raw.tool == WorkspaceTool.ToolBlur   ||
+			Context.raw.tool == WorkspaceTool.ToolSmudge   ||
+			Context.raw.tool == WorkspaceTool.ToolParticle ||
 			decal)) {
 			frag.add_uniform('sampler2D texbrushstencil', '_texbrushstencil');
 			frag.add_uniform('vec4 stencilTransform', '_stencilTransform');
@@ -271,8 +270,8 @@ class MakePaint {
 			frag.write('vec2 stencil_size = vec2(textureSize(texbrushstencil, 0));');
 			frag.write('float stencil_ratio = stencil_size.y / stencil_size.x;');
 			frag.write('stencil_uv -= vec2(0.5 / stencil_ratio, 0.5);');
-			frag.write('stencil_uv = vec2(stencil_uv.x * cos(stencilTransform.w) - stencil_uv.y * sin(stencilTransform.w),
-										  stencil_uv.x * sin(stencilTransform.w) + stencil_uv.y * cos(stencilTransform.w));');
+			frag.write(`stencil_uv = vec2(stencil_uv.x * cos(stencilTransform.w) - stencil_uv.y * sin(stencilTransform.w),
+										  stencil_uv.x * sin(stencilTransform.w) + stencil_uv.y * cos(stencilTransform.w));`);
 			frag.write('stencil_uv += vec2(0.5 / stencil_ratio, 0.5);');
 			frag.write('stencil_uv.x *= stencil_ratio;');
 			frag.write('if (stencil_uv.x < 0 || stencil_uv.x > 1 || stencil_uv.y < 0 || stencil_uv.y > 1) discard;');
@@ -285,7 +284,7 @@ class MakePaint {
 			}
 		}
 
-		if (Context.raw.brushMaskImage != null && (Context.raw.tool == ToolBrush || Context.raw.tool == ToolEraser)) {
+		if (Context.raw.brushMaskImage != null && (Context.raw.tool == WorkspaceTool.ToolBrush || Context.raw.tool == WorkspaceTool.ToolEraser)) {
 			frag.add_uniform('sampler2D texbrushmask', '_texbrushmask');
 			frag.write('vec2 binp_mask = inp.xy * 2.0 - 1.0;');
 			frag.write('binp_mask.x *= aspectRatio;');
@@ -296,7 +295,7 @@ class MakePaint {
 				frag.write('if (brushDirection.z == 0.0) discard;');
 				frag.write('pa_mask = vec2(pa_mask.x * brushDirection.x - pa_mask.y * brushDirection.y, pa_mask.x * brushDirection.y + pa_mask.y * brushDirection.x);');
 			}
-			var angle = Context.raw.brushAngle + Context.raw.brushNodesAngle;
+			let angle = Context.raw.brushAngle + Context.raw.brushNodesAngle;
 			if (angle != 0.0) {
 				frag.add_uniform('vec2 brushAngle', '_brushAngle');
 				frag.write('pa_mask.xy = vec2(pa_mask.x * brushAngle.x - pa_mask.y * brushAngle.y, pa_mask.x * brushAngle.y + pa_mask.y * brushAngle.x);');
@@ -318,7 +317,7 @@ class MakePaint {
 
 		frag.write('if (opacity == 0.0) discard;');
 
-		if (Context.raw.tool == ToolParticle) { // Particle mask
+		if (Context.raw.tool == WorkspaceTool.ToolParticle) { // Particle mask
 			MakeParticle.mask(vert, frag);
 		}
 		else { // Brush cursor mask
@@ -328,9 +327,9 @@ class MakePaint {
 		// Manual blending to preserve memory
 		frag.wvpposition = true;
 		frag.write('vec2 sample_tc = vec2(wvpposition.xy / wvpposition.w) * 0.5 + 0.5;');
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		frag.write('sample_tc.y = 1.0 - sample_tc.y;');
-		#end
+		///end
 		frag.add_uniform('sampler2D paintmask');
 		frag.write('float sample_mask = textureLod(paintmask, sample_tc, 0.0).r;');
 		frag.write('str = max(str, sample_mask);');
@@ -339,12 +338,12 @@ class MakePaint {
 		frag.add_uniform('sampler2D texpaint_undo', '_texpaint_undo');
 		frag.write('vec4 sample_undo = textureLod(texpaint_undo, sample_tc, 0.0);');
 
-		var matid = Context.raw.material.id / 255;
-		if (Context.raw.pickerMaskHandle.position == MaskMaterial) {
+		let matid = Context.raw.material.id / 255;
+		if (Context.raw.pickerMaskHandle.position == PickerMask.MaskMaterial) {
 			matid = Context.raw.materialIdPicked / 255; // Keep existing material id in place when mask is set
 		}
-		var matidString = ParserMaterial.vec1(matid * 3.0);
-		frag.write('float matid = $matidString;');
+		let matidString = ParserMaterial.vec1(matid * 3.0);
+		frag.write(`float matid = ${matidString};`);
 
 		// matid % 3 == 0 - normal, 1 - emission, 2 - subsurface
 		if (Context.raw.material.paintEmis) {
@@ -360,10 +359,10 @@ class MakePaint {
 			frag.write('}');
 		}
 
-		var isMask = Context.raw.layer.isMask();
-		var layered = Context.raw.layer != Project.layers[0];
+		let isMask = Context.raw.layer.isMask();
+		let layered = Context.raw.layer != Project.layers[0];
 		if (layered && !isMask) {
-			if (Context.raw.tool == ToolEraser) {
+			if (Context.raw.tool == WorkspaceTool.ToolEraser) {
 				frag.write('fragColor[0] = vec4(mix(sample_undo.rgb, vec3(0.0, 0.0, 0.0), str), sample_undo.a - str);');
 				frag.write('nortan = vec3(0.5, 0.5, 1.0);');
 				frag.write('occlusion = 1.0;');
@@ -371,7 +370,7 @@ class MakePaint {
 				frag.write('metallic = 0.0;');
 				frag.write('matid = 0.0;');
 			}
-			else if (Context.raw.tool == ToolParticle || decal || Context.raw.brushMaskImage != null) {
+			else if (Context.raw.tool == WorkspaceTool.ToolParticle || decal || Context.raw.brushMaskImage != null) {
 				frag.write('fragColor[0] = vec4(' + MakeMaterial.blendMode(frag, Context.raw.brushBlending, 'sample_undo.rgb', 'basecol', 'str') + ', max(str, sample_undo.a));');
 			}
 			else {
@@ -384,7 +383,7 @@ class MakePaint {
 			}
 			frag.write('fragColor[1] = vec4(nortan, matid);');
 
-			var height = "0.0";
+			let height = "0.0";
 			if (Context.raw.material.paintHeight && MakeMaterial.heightUsed) {
 				height = "height";
 			}
@@ -392,14 +391,14 @@ class MakePaint {
 			if (decal) {
 				frag.add_uniform('sampler2D texpaint_pack_undo', '_texpaint_pack_undo');
 				frag.write('vec4 sample_pack_undo = textureLod(texpaint_pack_undo, sample_tc, 0.0);');
-				frag.write('fragColor[2] = mix(sample_pack_undo, vec4(occlusion, roughness, metallic, $height), str);');
+				frag.write(`fragColor[2] = mix(sample_pack_undo, vec4(occlusion, roughness, metallic, ${height}), str);`);
 			}
 			else {
-				frag.write('fragColor[2] = vec4(occlusion, roughness, metallic, $height);');
+				frag.write(`fragColor[2] = vec4(occlusion, roughness, metallic, ${height});`);
 			}
 		}
 		else {
-			if (Context.raw.tool == ToolEraser) {
+			if (Context.raw.tool == WorkspaceTool.ToolEraser) {
 				frag.write('fragColor[0] = vec4(mix(sample_undo.rgb, vec3(0.0, 0.0, 0.0), str), sample_undo.a - str);');
 				frag.write('fragColor[1] = vec4(0.5, 0.5, 1.0, 0.0);');
 				frag.write('fragColor[2] = vec4(1.0, 0.0, 0.0, 0.0);');
@@ -464,7 +463,7 @@ class MakePaint {
 			con_paint.data.color_writes_alpha[2] = false;
 		}
 
-		if (Context.raw.tool == ToolBake) {
+		if (Context.raw.tool == WorkspaceTool.ToolBake) {
 			MakeBake.run(con_paint, vert, frag);
 		}
 

+ 12 - 13
armorpaint/Sources/arm/MakeParticle.hx → armorpaint/Sources/MakeParticle.ts

@@ -1,10 +1,9 @@
-package arm;
 
 class MakeParticle {
 
-	public static function run(data: NodeShaderData): NodeShaderContext {
-		var context_id = "mesh";
-		var con_part:NodeShaderContext = data.add_context({
+	static run = (data: NodeShaderData): NodeShaderContext => {
+		let context_id = "mesh";
+		let con_part: NodeShaderContext = data.add_context({
 			name: context_id,
 			depth_write: false,
 			compare_mode: "always",
@@ -13,8 +12,8 @@ class MakeParticle {
 			color_attachments: ["R8"]
 		});
 
-		var vert = con_part.make_vert();
-		var frag = con_part.make_frag();
+		let vert = con_part.make_vert();
+		let frag = con_part.make_frag();
 		frag.ins = vert.outs;
 
 		vert.write_attrib('vec4 spos = vec4(pos.xyz, 1.0);');
@@ -26,7 +25,7 @@ class MakeParticle {
 
 		vert.add_uniform('mat4 pd', '_particleData');
 
-		var str_tex_hash = "float fhash(int n) { return fract(sin(float(n)) * 43758.5453); }\n";
+		let str_tex_hash = "float fhash(int n) { return fract(sin(float(n)) * 43758.5453); }\n";
 		vert.add_function(str_tex_hash);
 		vert.add_out('float p_age');
 		vert.write('p_age = pd[3][3] - float(gl_InstanceID) * pd[0][1];');
@@ -86,8 +85,8 @@ class MakeParticle {
 		return con_part;
 	}
 
-	public static function mask(vert: NodeShader, frag: NodeShader) {
-		#if arm_physics
+	static mask = (vert: NodeShader, frag: NodeShader) => {
+		///if arm_physics
 		if (Context.raw.particlePhysics) {
 			vert.add_out('vec4 wpos');
 			vert.add_uniform('mat4 W', '_worldMatrix');
@@ -107,13 +106,13 @@ class MakeParticle {
 			frag.write('if (str == 0.0) discard;');
 			return;
 		}
-		#end
+		///end
 
 		frag.add_uniform('sampler2D texparticle', '_texparticle');
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		frag.write('float str = textureLod(texparticle, sp.xy, 0.0).r;');
-		#else
+		///else
 		frag.write('float str = textureLod(texparticle, vec2(sp.x, (1.0 - sp.y)), 0.0).r;');
-		#end
+		///end
 	}
 }

+ 10 - 11
armorpaint/Sources/arm/MakeTexcoord.hx → armorpaint/Sources/MakeTexcoord.ts

@@ -1,16 +1,15 @@
-package arm;
 
 class MakeTexcoord {
 
-	public static function run(vert: NodeShader, frag: NodeShader) {
+	static run = (vert: NodeShader, frag: NodeShader) => {
 
-		var fillLayer = Context.raw.layer.fill_layer != null;
-		var uvType = fillLayer ? Context.raw.layer.uvType : Context.raw.brushPaint;
-		var decal = Context.raw.tool == ToolDecal || Context.raw.tool == ToolText;
-		var angle = Context.raw.brushAngle + Context.raw.brushNodesAngle;
-		var uvAngle = fillLayer ? Context.raw.layer.angle : angle;
+		let fillLayer = Context.raw.layer.fill_layer != null;
+		let uvType = fillLayer ? Context.raw.layer.uvType : Context.raw.brushPaint;
+		let decal = Context.raw.tool == WorkspaceTool.ToolDecal || Context.raw.tool == WorkspaceTool.ToolText;
+		let angle = Context.raw.brushAngle + Context.raw.brushNodesAngle;
+		let uvAngle = fillLayer ? Context.raw.layer.angle : angle;
 
-		if (uvType == UVProject || decal) { // TexCoords - project
+		if (uvType == UVType.UVProject || decal) { // TexCoords - project
 			frag.add_uniform('float brushScale', '_brushScale');
 			frag.write_attrib('vec2 uvsp = sp.xy;');
 
@@ -24,8 +23,8 @@ class MakeTexcoord {
 
 				frag.n = true;
 				frag.add_uniform('vec3 decalLayerNor', '_decalLayerNor');
-				var dotAngle = Context.raw.brushAngleRejectDot;
-				frag.write('if (abs(dot(n, decalLayerNor) - 1.0) > $dotAngle) discard;');
+				let dotAngle = Context.raw.brushAngleRejectDot;
+				frag.write(`if (abs(dot(n, decalLayerNor) - 1.0) > ${dotAngle}) discard;`);
 
 				frag.wposition = true;
 				frag.add_uniform('vec3 decalLayerLoc', '_decalLayerLoc');
@@ -67,7 +66,7 @@ class MakeTexcoord {
 
 			frag.write_attrib('vec2 texCoord = uvsp * brushScale;');
 		}
-		else if (uvType == UVMap) { // TexCoords - uvmap
+		else if (uvType == UVType.UVMap) { // TexCoords - uvmap
 			vert.add_uniform('float brushScale', '_brushScale');
 			vert.add_out('vec2 texCoord');
 			vert.write('texCoord = tex * brushScale;');

+ 0 - 10
armorpaint/Sources/Manifest.hx

@@ -1,10 +0,0 @@
-package ;
-
-class Manifest {
-
-	public static inline var title = "ArmorPaint";
-	public static inline var version = "1.0 alpha";
-	public static inline var url = "https://armorpaint.org";
-	public static inline var url_android = "https://play.google.com/store/apps/details?id=org.armorpaint";
-	public static inline var url_ios = "https://apps.apple.com/app/armorpaint/id1533967534";
-}

+ 9 - 0
armorpaint/Sources/Manifest.ts

@@ -0,0 +1,9 @@
+
+class Manifest {
+
+	static title = "ArmorPaint";
+	static version = "1.0 alpha";
+	static url = "https://armorpaint.org";
+	static url_android = "https://play.google.com/store/apps/details?id=org.armorpaint";
+	static url_ios = "https://apps.apple.com/app/armorpaint/id1533967534";
+}

+ 43 - 0
armorpaint/Sources/NodesBrush.ts

@@ -0,0 +1,43 @@
+/// <reference path='./nodes/TEX_IMAGE.ts'/>
+/// <reference path='./nodes/InputNode.ts'/>
+/// <reference path='./../../base/Sources/nodes/MathNode.ts'/>
+/// <reference path='./../../base/Sources/nodes/RandomNode.ts'/>
+/// <reference path='./../../base/Sources/nodes/SeparateVectorNode.ts'/>
+/// <reference path='./../../base/Sources/nodes/TimeNode.ts'/>
+/// <reference path='./../../base/Sources/nodes/FloatNode.ts'/>
+/// <reference path='./../../base/Sources/nodes/VectorNode.ts'/>
+/// <reference path='./../../base/Sources/nodes/VectorMathNode.ts'/>
+
+class NodesBrush {
+
+	static categories = [_tr("Nodes")];
+
+	static list: TNode[][] = [
+		[ // Category 0
+			TEX_IMAGE.def,
+			InputNode.def,
+			MathNode.def,
+			RandomNode.def,
+			SeparateVectorNode.def,
+			TimeNode.def,
+			FloatNode.def,
+			VectorNode.def,
+			VectorMathNode.def
+		]
+	];
+
+	static createNode = (nodeType: string): TNode => {
+		for (let c of NodesBrush.list) {
+			for (let n of c) {
+				if (n.type == nodeType) {
+					let canvas = Context.raw.brush.canvas;
+					let nodes = Context.raw.brush.nodes;
+					let node = UINodes.makeNode(n, nodes, canvas);
+					canvas.nodes.push(node);
+					return node;
+				}
+			}
+		}
+		return null;
+	}
+}

+ 953 - 0
armorpaint/Sources/RenderPathPaint.ts

@@ -0,0 +1,953 @@
+
+class RenderPathPaint {
+
+	static liveLayer: SlotLayer = null;
+	static liveLayerDrawn = 0;
+	static liveLayerLocked = false;
+	static path: RenderPath;
+	static dilated = true;
+	static initVoxels = true; // Bake AO
+	static pushUndoLast: bool;
+	static painto: MeshObject = null;
+	static planeo: MeshObject = null;
+	static visibles: bool[] = null;
+	static mergedObjectVisible = false;
+	static savedFov = 0.0;
+	static baking = false;
+	static _texpaint: RenderTarget;
+	static _texpaint_nor: RenderTarget;
+	static _texpaint_pack: RenderTarget;
+	static _texpaint_undo: RenderTarget;
+	static _texpaint_nor_undo: RenderTarget;
+	static _texpaint_pack_undo: RenderTarget;
+	static lastX = -1.0;
+	static lastY = -1.0;
+
+	static init = (_path: RenderPath) => {
+		RenderPathPaint.path = _path;
+
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpaint_blend0";
+			t.width = Config.getTextureResX();
+			t.height = Config.getTextureResY();
+			t.format = "R8";
+			RenderPathPaint.path.createRenderTarget(t);
+		}
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpaint_blend1";
+			t.width = Config.getTextureResX();
+			t.height = Config.getTextureResY();
+			t.format = "R8";
+			RenderPathPaint.path.createRenderTarget(t);
+		}
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpaint_colorid";
+			t.width = 1;
+			t.height = 1;
+			t.format = "RGBA32";
+			RenderPathPaint.path.createRenderTarget(t);
+		}
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpaint_picker";
+			t.width = 1;
+			t.height = 1;
+			t.format = "RGBA32";
+			RenderPathPaint.path.createRenderTarget(t);
+		}
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpaint_nor_picker";
+			t.width = 1;
+			t.height = 1;
+			t.format = "RGBA32";
+			RenderPathPaint.path.createRenderTarget(t);
+		}
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpaint_pack_picker";
+			t.width = 1;
+			t.height = 1;
+			t.format = "RGBA32";
+			RenderPathPaint.path.createRenderTarget(t);
+		}
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpaint_uv_picker";
+			t.width = 1;
+			t.height = 1;
+			t.format = "RGBA32";
+			RenderPathPaint.path.createRenderTarget(t);
+		}
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpaint_posnortex_picker0";
+			t.width = 1;
+			t.height = 1;
+			t.format = "RGBA128";
+			RenderPathPaint.path.createRenderTarget(t);
+		}
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpaint_posnortex_picker1";
+			t.width = 1;
+			t.height = 1;
+			t.format = "RGBA128";
+			RenderPathPaint.path.createRenderTarget(t);
+		}
+
+		RenderPathPaint.path.loadShader("shader_datas/copy_mrt3_pass/copy_mrt3_pass");
+		RenderPathPaint.path.loadShader("shader_datas/copy_mrt3_pass/copy_mrt3RGBA64_pass");
+		///if is_paint
+		RenderPathPaint.path.loadShader("shader_datas/dilate_pass/dilate_pass");
+		///end
+	}
+
+	static commandsPaint = (dilation = true) => {
+		let tid = Context.raw.layer.id;
+
+		if (Context.raw.pdirty > 0) {
+			///if arm_physics
+			let particlePhysics = Context.raw.particlePhysics;
+			///else
+			let particlePhysics = false;
+			///end
+			if (Context.raw.tool == WorkspaceTool.ToolParticle && !particlePhysics) {
+				RenderPathPaint.path.setTarget("texparticle");
+				RenderPathPaint.path.clearTarget(0x00000000);
+				RenderPathPaint.path.bindTarget("_main", "gbufferD");
+				if ((Context.raw.xray || Config.raw.brush_angle_reject) && Config.raw.brush_3d) {
+					RenderPathPaint.path.bindTarget("gbuffer0", "gbuffer0");
+				}
+
+				let mo: MeshObject = Scene.active.getChild(".ParticleEmitter") as MeshObject;
+				mo.visible = true;
+				mo.render(RenderPathPaint.path.currentG, "mesh", RenderPathPaint.path.bindParams);
+				mo.visible = false;
+
+				mo = Scene.active.getChild(".Particle")as MeshObject;
+				mo.visible = true;
+				mo.render(RenderPathPaint.path.currentG, "mesh", RenderPathPaint.path.bindParams);
+				mo.visible = false;
+				RenderPathPaint.path.end();
+			}
+
+			///if is_paint
+			if (Context.raw.tool == WorkspaceTool.ToolColorId) {
+				RenderPathPaint.path.setTarget("texpaint_colorid");
+				RenderPathPaint.path.clearTarget(0xff000000);
+				RenderPathPaint.path.bindTarget("gbuffer2", "gbuffer2");
+				RenderPathPaint.path.drawMeshes("paint");
+				UIHeader.inst.headerHandle.redraws = 2;
+			}
+			else if (Context.raw.tool == WorkspaceTool.ToolPicker || Context.raw.tool == WorkspaceTool.ToolMaterial) {
+				if (Context.raw.pickPosNorTex) {
+					if (Context.raw.paint2d) {
+						RenderPathPaint.path.setTarget("gbuffer0", ["gbuffer1", "gbuffer2"]);
+						RenderPathPaint.path.drawMeshes("mesh");
+					}
+					RenderPathPaint.path.setTarget("texpaint_posnortex_picker0", ["texpaint_posnortex_picker1"]);
+					RenderPathPaint.path.bindTarget("gbuffer2", "gbuffer2");
+					RenderPathPaint.path.bindTarget("_main", "gbufferD");
+					RenderPathPaint.path.drawMeshes("paint");
+					let texpaint_posnortex_picker0 = RenderPathPaint.path.renderTargets.get("texpaint_posnortex_picker0").image;
+					let texpaint_posnortex_picker1 = RenderPathPaint.path.renderTargets.get("texpaint_posnortex_picker1").image;
+					let a = new DataView(texpaint_posnortex_picker0.getPixels());
+					let b = new DataView(texpaint_posnortex_picker1.getPixels());
+					Context.raw.posXPicked = a.getFloat32(0, true);
+					Context.raw.posYPicked = a.getFloat32(4, true);
+					Context.raw.posZPicked = a.getFloat32(8, true);
+					Context.raw.uvxPicked = a.getFloat32(12, true);
+					Context.raw.norXPicked = b.getFloat32(0, true);
+					Context.raw.norYPicked = b.getFloat32(4, true);
+					Context.raw.norZPicked = b.getFloat32(8, true);
+					Context.raw.uvyPicked = b.getFloat32(12, true);
+				}
+				else {
+					RenderPathPaint.path.setTarget("texpaint_picker", ["texpaint_nor_picker", "texpaint_pack_picker", "texpaint_uv_picker"]);
+					RenderPathPaint.path.bindTarget("gbuffer2", "gbuffer2");
+					tid = Context.raw.layer.id;
+					let useLiveLayer = Context.raw.tool == WorkspaceTool.ToolMaterial;
+					if (useLiveLayer) RenderPathPaint.useLiveLayer(true);
+					RenderPathPaint.path.bindTarget("texpaint" + tid, "texpaint");
+					RenderPathPaint.path.bindTarget("texpaint_nor" + tid, "texpaint_nor");
+					RenderPathPaint.path.bindTarget("texpaint_pack" + tid, "texpaint_pack");
+					RenderPathPaint.path.drawMeshes("paint");
+					if (useLiveLayer) RenderPathPaint.useLiveLayer(false);
+					UIHeader.inst.headerHandle.redraws = 2;
+					UIBase.inst.hwnds[2].redraws = 2;
+
+					let texpaint_picker = RenderPathPaint.path.renderTargets.get("texpaint_picker").image;
+					let texpaint_nor_picker = RenderPathPaint.path.renderTargets.get("texpaint_nor_picker").image;
+					let texpaint_pack_picker = RenderPathPaint.path.renderTargets.get("texpaint_pack_picker").image;
+					let texpaint_uv_picker = RenderPathPaint.path.renderTargets.get("texpaint_uv_picker").image;
+					let a = new DataView(texpaint_picker.getPixels());
+					let b = new DataView(texpaint_nor_picker.getPixels());
+					let c = new DataView(texpaint_pack_picker.getPixels());
+					let d = new DataView(texpaint_uv_picker.getPixels());
+
+					if (Context.raw.colorPickerCallback != null) {
+						Context.raw.colorPickerCallback(Context.raw.pickedColor);
+					}
+
+					// Picked surface values
+					///if (krom_metal || krom_vulkan)
+					let i0 = 2;
+					let i1 = 1;
+					let i2 = 0;
+					///else
+					let i0 = 0;
+					let i1 = 1;
+					let i2 = 2;
+					///end
+					let i3 = 3;
+					Context.raw.pickedColor.base = color_set_rb(Context.raw.pickedColor.base, a.getUint8(i0));
+					Context.raw.pickedColor.base = color_set_gb(Context.raw.pickedColor.base, a.getUint8(i1));
+					Context.raw.pickedColor.base = color_set_bb(Context.raw.pickedColor.base, a.getUint8(i2));
+					Context.raw.pickedColor.normal = color_set_rb(Context.raw.pickedColor.normal, b.getUint8(i0));
+					Context.raw.pickedColor.normal = color_set_gb(Context.raw.pickedColor.normal, b.getUint8(i1));
+					Context.raw.pickedColor.normal = color_set_bb(Context.raw.pickedColor.normal, b.getUint8(i2));
+					Context.raw.pickedColor.occlusion = c.getUint8(i0) / 255;
+					Context.raw.pickedColor.roughness = c.getUint8(i1) / 255;
+					Context.raw.pickedColor.metallic = c.getUint8(i2) / 255;
+					Context.raw.pickedColor.height = c.getUint8(i3) / 255;
+					Context.raw.pickedColor.opacity = a.getUint8(i3) / 255;
+					Context.raw.uvxPicked = d.getUint8(i0) / 255;
+					Context.raw.uvyPicked = d.getUint8(i1) / 255;
+					// Pick material
+					if (Context.raw.pickerSelectMaterial && Context.raw.colorPickerCallback == null) {
+						// matid % 3 == 0 - normal, 1 - emission, 2 - subsurface
+						let matid = Math.floor((b.getUint8(3) - (b.getUint8(3) % 3)) / 3);
+						for (let m of Project.materials) {
+							if (m.id == matid) {
+								Context.setMaterial(m);
+								Context.raw.materialIdPicked = matid;
+								break;
+							}
+						}
+					}
+				}
+			}
+			else {
+				///if arm_voxels
+				if (Context.raw.tool == WorkspaceTool.ToolBake && Context.raw.bakeType == BakeType.BakeAO) {
+					if (RenderPathPaint.initVoxels) {
+						RenderPathPaint.initVoxels = false;
+						let _rp_gi = Config.raw.rp_gi;
+						Config.raw.rp_gi = true;
+						RenderPathBase.initVoxels();
+						Config.raw.rp_gi = _rp_gi;
+					}
+					RenderPathPaint.path.clearImage("voxels", 0x00000000);
+					RenderPathPaint.path.setTarget("");
+					RenderPathPaint.path.setViewport(256, 256);
+					RenderPathPaint.path.bindTarget("voxels", "voxels");
+					RenderPathPaint.path.drawMeshes("voxel");
+					RenderPathPaint.path.generateMipmaps("voxels");
+				}
+				///end
+
+				let texpaint = "texpaint" + tid;
+				if (Context.raw.tool == WorkspaceTool.ToolBake && Context.raw.brushTime == Time.delta) {
+					// Clear to black on bake start
+					RenderPathPaint.path.setTarget(texpaint);
+					RenderPathPaint.path.clearTarget(0xff000000);
+				}
+
+				RenderPathPaint.path.setTarget("texpaint_blend1");
+				RenderPathPaint.path.bindTarget("texpaint_blend0", "tex");
+				RenderPathPaint.path.drawShader("shader_datas/copy_pass/copyR8_pass");
+				let isMask = Context.raw.layer.isMask();
+				if (isMask) {
+					let ptid = Context.raw.layer.parent.id;
+					if (Context.raw.layer.parent.isGroup()) { // Group mask
+						for (let c of Context.raw.layer.parent.getChildren()) {
+							ptid = c.id;
+							break;
+						}
+					}
+					RenderPathPaint.path.setTarget(texpaint, ["texpaint_nor" + ptid, "texpaint_pack" + ptid, "texpaint_blend0"]);
+				}
+				else {
+					RenderPathPaint.path.setTarget(texpaint, ["texpaint_nor" + tid, "texpaint_pack" + tid, "texpaint_blend0"]);
+				}
+				RenderPathPaint.path.bindTarget("_main", "gbufferD");
+				if ((Context.raw.xray || Config.raw.brush_angle_reject) && Config.raw.brush_3d) {
+					RenderPathPaint.path.bindTarget("gbuffer0", "gbuffer0");
+				}
+				RenderPathPaint.path.bindTarget("texpaint_blend1", "paintmask");
+				///if arm_voxels
+				if (Context.raw.tool == WorkspaceTool.ToolBake && Context.raw.bakeType == BakeType.BakeAO) {
+					RenderPathPaint.path.bindTarget("voxels", "voxels");
+				}
+				///end
+				if (Context.raw.colorIdPicked) {
+					RenderPathPaint.path.bindTarget("texpaint_colorid", "texpaint_colorid");
+				}
+
+				// Read texcoords from gbuffer
+				let readTC = (Context.raw.tool == WorkspaceTool.ToolFill && Context.raw.fillTypeHandle.position == FillType.FillFace) ||
+							  Context.raw.tool == WorkspaceTool.ToolClone ||
+							  Context.raw.tool == WorkspaceTool.ToolBlur ||
+							  Context.raw.tool == WorkspaceTool.ToolSmudge;
+				if (readTC) {
+					RenderPathPaint.path.bindTarget("gbuffer2", "gbuffer2");
+				}
+
+				RenderPathPaint.path.drawMeshes("paint");
+
+				if (Context.raw.tool == WorkspaceTool.ToolBake && Context.raw.bakeType == BakeType.BakeCurvature && Context.raw.bakeCurvSmooth > 0) {
+					if (RenderPathPaint.path.renderTargets.get("texpaint_blur") == null) {
+						let t = new RenderTargetRaw();
+						t.name = "texpaint_blur";
+						t.width = Math.floor(Config.getTextureResX() * 0.95);
+						t.height = Math.floor(Config.getTextureResY() * 0.95);
+						t.format = "RGBA32";
+						RenderPathPaint.path.createRenderTarget(t);
+					}
+					let blurs = Math.round(Context.raw.bakeCurvSmooth);
+					for (let i = 0; i < blurs; ++i) {
+						RenderPathPaint.path.setTarget("texpaint_blur");
+						RenderPathPaint.path.bindTarget(texpaint, "tex");
+						RenderPathPaint.path.drawShader("shader_datas/copy_pass/copy_pass");
+						RenderPathPaint.path.setTarget(texpaint);
+						RenderPathPaint.path.bindTarget("texpaint_blur", "tex");
+						RenderPathPaint.path.drawShader("shader_datas/copy_pass/copy_pass");
+					}
+				}
+
+				if (dilation && Config.raw.dilate == DilateType.DilateInstant) {
+					RenderPathPaint.dilate(true, false);
+				}
+			}
+			///end
+
+			///if is_sculpt
+			let texpaint = "texpaint" + tid;
+			RenderPathPaint.path.setTarget("texpaint_blend1");
+			RenderPathPaint.path.bindTarget("texpaint_blend0", "tex");
+			RenderPathPaint.path.drawShader("shader_datas/copy_pass/copyR8_pass");
+			RenderPathPaint.path.setTarget(texpaint, ["texpaint_blend0"]);
+			RenderPathPaint.path.bindTarget("gbufferD_undo", "gbufferD");
+			if ((Context.raw.xray || Config.raw.brush_angle_reject) && Config.raw.brush_3d) {
+				RenderPathPaint.path.bindTarget("gbuffer0", "gbuffer0");
+			}
+			RenderPathPaint.path.bindTarget("texpaint_blend1", "paintmask");
+
+			// Read texcoords from gbuffer
+			let readTC = (Context.raw.tool == WorkspaceTool.ToolFill && Context.raw.fillTypeHandle.position == FillType.FillFace) ||
+						  Context.raw.tool == WorkspaceTool.ToolClone ||
+						  Context.raw.tool == WorkspaceTool.ToolBlur ||
+						  Context.raw.tool == WorkspaceTool.ToolSmudge;
+			if (readTC) {
+				RenderPathPaint.path.bindTarget("gbuffer2", "gbuffer2");
+			}
+			RenderPathPaint.path.bindTarget("gbuffer0_undo", "gbuffer0_undo");
+
+			let materialContexts: MaterialContext[] = [];
+			let shaderContexts: ShaderContext[] = [];
+			let mats = Project.paintObjects[0].materials;
+			Project.paintObjects[0].getContexts("paint", mats, materialContexts, shaderContexts);
+
+			let cc_context = shaderContexts[0];
+			if (ConstData.screenAlignedVB == null) ConstData.createScreenAlignedData();
+			RenderPathPaint.path.currentG.setPipeline(cc_context.pipeState);
+			Uniforms.setContextConstants(RenderPathPaint.path.currentG, cc_context, RenderPathPaint.path.bindParams);
+			Uniforms.setObjectConstants(RenderPathPaint.path.currentG, cc_context, Project.paintObjects[0]);
+			Uniforms.setMaterialConstants(RenderPathPaint.path.currentG, cc_context, materialContexts[0]);
+			RenderPathPaint.path.currentG.setVertexBuffer(ConstData.screenAlignedVB);
+			RenderPathPaint.path.currentG.setIndexBuffer(ConstData.screenAlignedIB);
+			RenderPathPaint.path.currentG.drawIndexedVertices();
+			RenderPathPaint.path.end();
+			///end
+		}
+	}
+
+	static useLiveLayer = (use: bool) => {
+		let tid = Context.raw.layer.id;
+		let hid = History.undoI - 1 < 0 ? Config.raw.undo_steps - 1 : History.undoI - 1;
+		if (use) {
+			RenderPathPaint._texpaint = RenderPathPaint.path.renderTargets.get("texpaint" + tid);
+			RenderPathPaint._texpaint_undo = RenderPathPaint.path.renderTargets.get("texpaint_undo" + hid);
+			RenderPathPaint._texpaint_nor_undo = RenderPathPaint.path.renderTargets.get("texpaint_nor_undo" + hid);
+			RenderPathPaint._texpaint_pack_undo = RenderPathPaint.path.renderTargets.get("texpaint_pack_undo" + hid);
+			RenderPathPaint._texpaint_nor = RenderPathPaint.path.renderTargets.get("texpaint_nor" + tid);
+			RenderPathPaint._texpaint_pack = RenderPathPaint.path.renderTargets.get("texpaint_pack" + tid);
+			RenderPathPaint.path.renderTargets.set("texpaint_undo" + hid, RenderPathPaint.path.renderTargets.get("texpaint" + tid));
+			RenderPathPaint.path.renderTargets.set("texpaint" + tid, RenderPathPaint.path.renderTargets.get("texpaint_live"));
+			if (Context.raw.layer.isLayer()) {
+				RenderPathPaint.path.renderTargets.set("texpaint_nor_undo" + hid, RenderPathPaint.path.renderTargets.get("texpaint_nor" + tid));
+				RenderPathPaint.path.renderTargets.set("texpaint_pack_undo" + hid, RenderPathPaint.path.renderTargets.get("texpaint_pack" + tid));
+				RenderPathPaint.path.renderTargets.set("texpaint_nor" + tid, RenderPathPaint.path.renderTargets.get("texpaint_nor_live"));
+				RenderPathPaint.path.renderTargets.set("texpaint_pack" + tid, RenderPathPaint.path.renderTargets.get("texpaint_pack_live"));
+			}
+		}
+		else {
+			RenderPathPaint.path.renderTargets.set("texpaint" + tid, RenderPathPaint._texpaint);
+			RenderPathPaint.path.renderTargets.set("texpaint_undo" + hid, RenderPathPaint._texpaint_undo);
+			if (Context.raw.layer.isLayer()) {
+				RenderPathPaint.path.renderTargets.set("texpaint_nor_undo" + hid, RenderPathPaint._texpaint_nor_undo);
+				RenderPathPaint.path.renderTargets.set("texpaint_pack_undo" + hid, RenderPathPaint._texpaint_pack_undo);
+				RenderPathPaint.path.renderTargets.set("texpaint_nor" + tid, RenderPathPaint._texpaint_nor);
+				RenderPathPaint.path.renderTargets.set("texpaint_pack" + tid, RenderPathPaint._texpaint_pack);
+			}
+		}
+		RenderPathPaint.liveLayerLocked = use;
+	}
+
+	static commandsLiveBrush = () => {
+		let tool = Context.raw.tool;
+		if (tool != WorkspaceTool.ToolBrush &&
+			tool != WorkspaceTool.ToolEraser &&
+			tool != WorkspaceTool.ToolClone &&
+			tool != WorkspaceTool.ToolDecal &&
+			tool != WorkspaceTool.ToolText &&
+			tool != WorkspaceTool.ToolBlur &&
+			tool != WorkspaceTool.ToolSmudge) {
+				return;
+		}
+
+		if (RenderPathPaint.liveLayerLocked) return;
+
+		if (RenderPathPaint.liveLayer == null) {
+			RenderPathPaint.liveLayer = new SlotLayer("_live");
+		}
+
+		let tid = Context.raw.layer.id;
+		if (Context.raw.layer.isMask()) {
+			RenderPathPaint.path.setTarget("texpaint_live");
+			RenderPathPaint.path.bindTarget("texpaint" + tid, "tex");
+			RenderPathPaint.path.drawShader("shader_datas/copy_pass/copy_pass");
+		}
+		else {
+			RenderPathPaint.path.setTarget("texpaint_live", ["texpaint_nor_live", "texpaint_pack_live"]);
+			RenderPathPaint.path.bindTarget("texpaint" + tid, "tex0");
+			RenderPathPaint.path.bindTarget("texpaint_nor" + tid, "tex1");
+			RenderPathPaint.path.bindTarget("texpaint_pack" + tid, "tex2");
+			RenderPathPaint.path.drawShader("shader_datas/copy_mrt3_pass/copy_mrt3_pass");
+		}
+
+		RenderPathPaint.useLiveLayer(true);
+
+		RenderPathPaint.liveLayerDrawn = 2;
+
+		UIView2D.inst.hwnd.redraws = 2;
+		let _x = Context.raw.paintVec.x;
+		let _y = Context.raw.paintVec.y;
+		if (Context.raw.brushLocked) {
+			Context.raw.paintVec.x = (Context.raw.lockStartedX - App.x()) / App.w();
+			Context.raw.paintVec.y = (Context.raw.lockStartedY - App.y()) / App.h();
+		}
+		let _lastX = Context.raw.lastPaintVecX;
+		let _lastY = Context.raw.lastPaintVecY;
+		let _pdirty = Context.raw.pdirty;
+		Context.raw.lastPaintVecX = Context.raw.paintVec.x;
+		Context.raw.lastPaintVecY = Context.raw.paintVec.y;
+		if (Operator.shortcut(Config.keymap.brush_ruler)) {
+			Context.raw.lastPaintVecX = Context.raw.lastPaintX;
+			Context.raw.lastPaintVecY = Context.raw.lastPaintY;
+		}
+		Context.raw.pdirty = 2;
+
+		RenderPathPaint.commandsSymmetry();
+		RenderPathPaint.commandsPaint();
+
+		RenderPathPaint.useLiveLayer(false);
+
+		Context.raw.paintVec.x = _x;
+		Context.raw.paintVec.y = _y;
+		Context.raw.lastPaintVecX = _lastX;
+		Context.raw.lastPaintVecY = _lastY;
+		Context.raw.pdirty = _pdirty;
+		Context.raw.brushBlendDirty = true;
+	}
+
+	static commandsCursor = () => {
+		if (!Config.raw.brush_3d) return;
+		let decal = Context.raw.tool == WorkspaceTool.ToolDecal || Context.raw.tool == WorkspaceTool.ToolText;
+		let decalMask = decal && Operator.shortcut(Config.keymap.decal_mask, ShortcutType.ShortcutDown);
+		let tool = Context.raw.tool;
+		if (tool != WorkspaceTool.ToolBrush &&
+			tool != WorkspaceTool.ToolEraser &&
+			tool != WorkspaceTool.ToolClone &&
+			tool != WorkspaceTool.ToolBlur &&
+			tool != WorkspaceTool.ToolSmudge &&
+			tool != WorkspaceTool.ToolParticle &&
+			!decalMask) {
+				return;
+		}
+
+		let fillLayer = Context.raw.layer.fill_layer != null;
+		let groupLayer = Context.raw.layer.isGroup();
+		if (!Base.uiEnabled || Base.isDragging || fillLayer || groupLayer) {
+			return;
+		}
+
+		let mx = Context.raw.paintVec.x;
+		let my = 1.0 - Context.raw.paintVec.y;
+		if (Context.raw.brushLocked) {
+			mx = (Context.raw.lockStartedX - App.x()) / App.w();
+			my = 1.0 - (Context.raw.lockStartedY - App.y()) / App.h();
+		}
+		let radius = decalMask ? Context.raw.brushDecalMaskRadius : Context.raw.brushRadius;
+		RenderPathPaint.drawCursor(mx, my, Context.raw.brushNodesRadius * radius / 3.4);
+	}
+
+	static drawCursor = (mx: f32, my: f32, radius: f32, tintR = 1.0, tintG = 1.0, tintB = 1.0) => {
+		let plane = (Scene.active.getChild(".Plane") as MeshObject);
+		let geom = plane.data;
+
+		let g = RenderPathPaint.path.frameG;
+		if (Base.pipeCursor == null) Base.makeCursorPipe();
+
+		RenderPathPaint.path.setTarget("");
+		g.setPipeline(Base.pipeCursor);
+		let decal = Context.raw.tool == WorkspaceTool.ToolDecal || Context.raw.tool == WorkspaceTool.ToolText;
+		let decalMask = decal && Operator.shortcut(Config.keymap.decal_mask, ShortcutType.ShortcutDown);
+		let img = (decal && !decalMask) ? Context.raw.decalImage : Res.get("cursor.k");
+		g.setTexture(Base.cursorTex, img);
+		let gbuffer0 = RenderPathPaint.path.renderTargets.get("gbuffer0").image;
+		g.setTextureDepth(Base.cursorGbufferD, gbuffer0);
+		g.setFloat2(Base.cursorMouse, mx, my);
+		g.setFloat2(Base.cursorTexStep, 1 / gbuffer0.width, 1 / gbuffer0.height);
+		g.setFloat(Base.cursorRadius, radius);
+		let right = Scene.active.camera.rightWorld().normalize();
+		g.setFloat3(Base.cursorCameraRight, right.x, right.y, right.z);
+		g.setFloat3(Base.cursorTint, tintR, tintG, tintB);
+		g.setMatrix(Base.cursorVP, Scene.active.camera.VP);
+		let helpMat = Mat4.identity();
+		helpMat.getInverse(Scene.active.camera.VP);
+		g.setMatrix(Base.cursorInvVP, helpMat);
+		///if (krom_metal || krom_vulkan)
+		g.setVertexBuffer(geom.get([{name: "tex", data: "short2norm"}]));
+		///else
+		g.setVertexBuffer(geom.vertexBuffer);
+		///end
+		g.setIndexBuffer(geom.indexBuffers[0]);
+		g.drawIndexedVertices();
+
+		g.disableScissor();
+		RenderPathPaint.path.end();
+	}
+
+	static commandsSymmetry = () => {
+		if (Context.raw.symX || Context.raw.symY || Context.raw.symZ) {
+			Context.raw.ddirty = 2;
+			let t = Context.raw.paintObject.transform;
+			let sx = t.scale.x;
+			let sy = t.scale.y;
+			let sz = t.scale.z;
+			if (Context.raw.symX) {
+				t.scale.set(-sx, sy, sz);
+				t.buildMatrix();
+				RenderPathPaint.commandsPaint(false);
+			}
+			if (Context.raw.symY) {
+				t.scale.set(sx, -sy, sz);
+				t.buildMatrix();
+				RenderPathPaint.commandsPaint(false);
+			}
+			if (Context.raw.symZ) {
+				t.scale.set(sx, sy, -sz);
+				t.buildMatrix();
+				RenderPathPaint.commandsPaint(false);
+			}
+			if (Context.raw.symX && Context.raw.symY) {
+				t.scale.set(-sx, -sy, sz);
+				t.buildMatrix();
+				RenderPathPaint.commandsPaint(false);
+			}
+			if (Context.raw.symX && Context.raw.symZ) {
+				t.scale.set(-sx, sy, -sz);
+				t.buildMatrix();
+				RenderPathPaint.commandsPaint(false);
+			}
+			if (Context.raw.symY && Context.raw.symZ) {
+				t.scale.set(sx, -sy, -sz);
+				t.buildMatrix();
+				RenderPathPaint.commandsPaint(false);
+			}
+			if (Context.raw.symX && Context.raw.symY && Context.raw.symZ) {
+				t.scale.set(-sx, -sy, -sz);
+				t.buildMatrix();
+				RenderPathPaint.commandsPaint(false);
+			}
+			t.scale.set(sx, sy, sz);
+			t.buildMatrix();
+		}
+	}
+
+	static paintEnabled = (): bool => {
+		///if is_paint
+		let fillLayer = Context.raw.layer.fill_layer != null && Context.raw.tool != WorkspaceTool.ToolPicker && Context.raw.tool != WorkspaceTool.ToolMaterial && Context.raw.tool != WorkspaceTool.ToolColorId;
+		///end
+
+		///if is_sculpt
+		let fillLayer = Context.raw.layer.fill_layer != null && Context.raw.tool != WorkspaceTool.ToolPicker && Context.raw.tool != WorkspaceTool.ToolMaterial;
+		///end
+
+		let groupLayer = Context.raw.layer.isGroup();
+		return !fillLayer && !groupLayer && !Context.raw.foregroundEvent;
+	}
+
+	static liveBrushDirty = () => {
+		let mouse = Input.getMouse();
+		let mx = RenderPathPaint.lastX;
+		let my = RenderPathPaint.lastY;
+		RenderPathPaint.lastX = mouse.viewX;
+		RenderPathPaint.lastY = mouse.viewY;
+		if (Config.raw.brush_live && Context.raw.pdirty <= 0) {
+			let moved = (mx != RenderPathPaint.lastX || my != RenderPathPaint.lastY) && (Context.inViewport() || Context.in2dView());
+			if (moved || Context.raw.brushLocked) {
+				Context.raw.rdirty = 2;
+			}
+		}
+	}
+
+	static begin = () => {
+
+		///if is_paint
+		if (!RenderPathPaint.dilated) {
+			RenderPathPaint.dilate(Config.raw.dilate == DilateType.DilateDelayed, true);
+			RenderPathPaint.dilated = true;
+		}
+		///end
+
+		if (!RenderPathPaint.paintEnabled()) return;
+
+		///if is_paint
+		RenderPathPaint.pushUndoLast = History.pushUndo;
+		///end
+
+		if (History.pushUndo && History.undoLayers != null) {
+			History.paint();
+
+			///if is_sculpt
+			RenderPathPaint.path.setTarget("gbuffer0_undo");
+			RenderPathPaint.path.bindTarget("gbuffer0", "tex");
+			RenderPathPaint.path.drawShader("shader_datas/copy_pass/copy_pass");
+
+			RenderPathPaint.path.setTarget("gbufferD_undo");
+			RenderPathPaint.path.bindTarget("_main", "tex");
+			RenderPathPaint.path.drawShader("shader_datas/copy_pass/copy_pass");
+			///end
+		}
+
+		///if is_sculpt
+		if (History.pushUndo2 && History.undoLayers != null) {
+			History.paint();
+		}
+		///end
+
+		if (Context.raw.paint2d) {
+			RenderPathPaint.setPlaneMesh();
+		}
+
+		if (RenderPathPaint.liveLayerDrawn > 0) RenderPathPaint.liveLayerDrawn--;
+
+		if (Config.raw.brush_live && Context.raw.pdirty <= 0 && Context.raw.ddirty <= 0 && Context.raw.brushTime == 0) {
+			// Depth is unchanged, draw before gbuffer gets updated
+			RenderPathPaint.commandsLiveBrush();
+		}
+	}
+
+	static end = () => {
+		RenderPathPaint.commandsCursor();
+		Context.raw.ddirty--;
+		Context.raw.rdirty--;
+
+		if (!RenderPathPaint.paintEnabled()) return;
+		Context.raw.pdirty--;
+	}
+
+	static draw = () => {
+		if (!RenderPathPaint.paintEnabled()) return;
+
+		///if (!krom_ios) // No hover on iPad, decals are painted by pen release
+		if (Config.raw.brush_live && Context.raw.pdirty <= 0 && Context.raw.ddirty > 0 && Context.raw.brushTime == 0) {
+			// gbuffer has been updated now but brush will lag 1 frame
+			RenderPathPaint.commandsLiveBrush();
+		}
+		///end
+
+		if (History.undoLayers != null) {
+			RenderPathPaint.commandsSymmetry();
+
+			if (Context.raw.pdirty > 0) RenderPathPaint.dilated = false;
+
+			///if is_paint
+			if (Context.raw.tool == WorkspaceTool.ToolBake) {
+
+				///if (krom_direct3d12 || krom_vulkan || krom_metal)
+				let isRaytracedBake = (Context.raw.bakeType == BakeType.BakeAO  ||
+					Context.raw.bakeType == BakeType.BakeLightmap ||
+					Context.raw.bakeType == BakeType.BakeBentNormal ||
+					Context.raw.bakeType == BakeType.BakeThickness);
+				///end
+
+				if (Context.raw.bakeType == BakeType.BakeNormal || Context.raw.bakeType == BakeType.BakeHeight || Context.raw.bakeType == BakeType.BakeDerivative) {
+					if (!RenderPathPaint.baking && Context.raw.pdirty > 0) {
+						RenderPathPaint.baking = true;
+						let _bakeType = Context.raw.bakeType;
+						Context.raw.bakeType = Context.raw.bakeType == BakeType.BakeNormal ? BakeType.BakeNormalObject : BakeType.BakePosition; // Bake high poly data
+						MakeMaterial.parsePaintMaterial();
+						let _paintObject = Context.raw.paintObject;
+						let highPoly = Project.paintObjects[Context.raw.bakeHighPoly];
+						let _visible = highPoly.visible;
+						highPoly.visible = true;
+						Context.selectPaintObject(highPoly);
+						RenderPathPaint.commandsPaint();
+						highPoly.visible = _visible;
+						if (RenderPathPaint.pushUndoLast) History.paint();
+						Context.selectPaintObject(_paintObject);
+
+						let _renderFinal = () => {
+							Context.raw.bakeType = _bakeType;
+							MakeMaterial.parsePaintMaterial();
+							Context.raw.pdirty = 1;
+							RenderPathPaint.commandsPaint();
+							Context.raw.pdirty = 0;
+							RenderPathPaint.baking = false;
+						}
+						let _renderDeriv = () => {
+							Context.raw.bakeType = BakeType.BakeHeight;
+							MakeMaterial.parsePaintMaterial();
+							Context.raw.pdirty = 1;
+							RenderPathPaint.commandsPaint();
+							Context.raw.pdirty = 0;
+							if (RenderPathPaint.pushUndoLast) History.paint();
+							App.notifyOnInit(_renderFinal);
+						}
+						let bakeType = Context.raw.bakeType as BakeType;
+						App.notifyOnInit(bakeType == BakeType.BakeDerivative ? _renderDeriv : _renderFinal);
+					}
+				}
+				else if (Context.raw.bakeType == BakeType.BakeObjectID) {
+					let _layerFilter = Context.raw.layerFilter;
+					let _paintObject = Context.raw.paintObject;
+					let isMerged = Context.raw.mergedObject != null;
+					let _visible = isMerged && Context.raw.mergedObject.visible;
+					Context.raw.layerFilter = 1;
+					if (isMerged) Context.raw.mergedObject.visible = false;
+
+					for (let p of Project.paintObjects) {
+						Context.selectPaintObject(p);
+						RenderPathPaint.commandsPaint();
+					}
+
+					Context.raw.layerFilter = _layerFilter;
+					Context.selectPaintObject(_paintObject);
+					if (isMerged) Context.raw.mergedObject.visible = _visible;
+				}
+				///if (krom_direct3d12 || krom_vulkan || krom_metal)
+				else if (isRaytracedBake) {
+					let dirty = RenderPathRaytraceBake.commands(MakeMaterial.parsePaintMaterial);
+					if (dirty) UIHeader.inst.headerHandle.redraws = 2;
+					if (Config.raw.dilate == DilateType.DilateInstant) { // && Context.raw.pdirty == 1
+						RenderPathPaint.dilate(true, false);
+					}
+				}
+				///end
+				else {
+					RenderPathPaint.commandsPaint();
+				}
+			}
+			else { // Paint
+				RenderPathPaint.commandsPaint();
+			}
+			///end
+
+			///if is_sculpt
+			RenderPathPaint.commandsPaint();
+			///end
+		}
+
+		if (Context.raw.brushBlendDirty) {
+			Context.raw.brushBlendDirty = false;
+			///if krom_metal
+			RenderPathPaint.path.setTarget("texpaint_blend0");
+			RenderPathPaint.path.clearTarget(0x00000000);
+			RenderPathPaint.path.setTarget("texpaint_blend1");
+			RenderPathPaint.path.clearTarget(0x00000000);
+			///else
+			RenderPathPaint.path.setTarget("texpaint_blend0", ["texpaint_blend1"]);
+			RenderPathPaint.path.clearTarget(0x00000000);
+			///end
+		}
+
+		if (Context.raw.paint2d) {
+			RenderPathPaint.restorePlaneMesh();
+		}
+	}
+
+	static setPlaneMesh = () => {
+		Context.raw.paint2dView = true;
+		RenderPathPaint.painto = Context.raw.paintObject;
+		RenderPathPaint.visibles = [];
+		for (let p of Project.paintObjects) {
+			RenderPathPaint.visibles.push(p.visible);
+			p.visible = false;
+		}
+		if (Context.raw.mergedObject != null) {
+			RenderPathPaint.mergedObjectVisible = Context.raw.mergedObject.visible;
+			Context.raw.mergedObject.visible = false;
+		}
+
+		let cam = Scene.active.camera;
+		Context.raw.savedCamera.setFrom(cam.transform.local);
+		RenderPathPaint.savedFov = cam.data.raw.fov;
+		Viewport.updateCameraType(CameraType.CameraPerspective);
+		let m = Mat4.identity();
+		m.translate(0, 0, 0.5);
+		cam.transform.setMatrix(m);
+		cam.data.raw.fov = Base.defaultFov;
+		cam.buildProjection();
+		cam.buildMatrix();
+
+		let tw = 0.95 * UIView2D.inst.panScale;
+		let tx = UIView2D.inst.panX / UIView2D.inst.ww;
+		let ty = UIView2D.inst.panY / App.h();
+		m.setIdentity();
+		m.scale(new Vec4(tw, tw, 1));
+		m.setLoc(new Vec4(tx, ty, 0));
+		let m2 = Mat4.identity();
+		m2.getInverse(Scene.active.camera.VP);
+		m.multmat(m2);
+
+		let tiled = UIView2D.inst.tiledShow;
+		if (tiled && Scene.active.getChild(".PlaneTiled") == null) {
+			// 3x3 planes
+			let posa = [32767,0,-32767,0,10922,0,-10922,0,10922,0,-32767,0,10922,0,-10922,0,-10922,0,10922,0,-10922,0,-10922,0,-10922,0,10922,0,-32767,0,32767,0,-32767,0,10922,0,10922,0,10922,0,-10922,0,32767,0,-10922,0,10922,0,32767,0,10922,0,10922,0,32767,0,10922,0,10922,0,-10922,0,-10922,0,-32767,0,10922,0,-32767,0,-10922,0,32767,0,-10922,0,10922,0,10922,0,10922,0,-10922,0,-10922,0,-32767,0,-32767,0,-10922,0,-32767,0,-32767,0,10922,0,-32767,0,-10922,0,-10922,0,-10922,0,-32767,0,32767,0,-32767,0,32767,0,-10922,0,10922,0,-10922,0,10922,0,-10922,0,10922,0,10922,0,-10922,0,10922,0,-10922,0,10922,0,-10922,0,32767,0,-32767,0,32767,0,10922,0,10922,0,10922,0,32767,0,-10922,0,32767,0,32767,0,10922,0,32767,0,32767,0,10922,0,32767,0,-10922,0,-10922,0,-10922,0,10922,0,-32767,0,10922,0,32767,0,-10922,0,32767,0,10922,0,10922,0,10922,0,-10922,0,-32767,0,-10922,0,-10922,0,-32767,0,-10922,0,10922,0,-32767,0,10922,0,-10922,0,-10922,0,-10922,0];
+			let nora = [0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767];
+			let texa = [32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0];
+			let inda = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53];
+			let raw: TMeshData = {
+				name: ".PlaneTiled",
+				vertex_arrays: [
+					{ attrib: "pos", values: RenderPathPaint.array_i16(posa), data: "short4norm" },
+					{ attrib: "nor", values: RenderPathPaint.array_i16(nora), data: "short2norm" },
+					{ attrib: "tex", values: RenderPathPaint.array_i16(texa), data: "short2norm" }
+				],
+				index_arrays: [
+					{ values: RenderPathPaint.array_u32(inda), material: 0 }
+				],
+				scale_pos: 1.5,
+				scale_tex: 1.0
+			};
+			new MeshData(raw, (md: MeshData) => {
+				let materials = (Scene.active.getChild(".Plane") as MeshObject).materials;
+				let o = Scene.active.addMeshObject(md, materials);
+				o.name = ".PlaneTiled";
+			});
+		}
+
+		RenderPathPaint.planeo = Scene.active.getChild(tiled ? ".PlaneTiled" : ".Plane") as MeshObject;
+		RenderPathPaint.planeo.visible = true;
+		Context.raw.paintObject = RenderPathPaint.planeo;
+
+		let v = new Vec4();
+		let sx = v.set(m._00, m._01, m._02).length();
+		RenderPathPaint.planeo.transform.rot.fromEuler(-Math.PI / 2, 0, 0);
+		RenderPathPaint.planeo.transform.scale.set(sx, 1.0, sx);
+		RenderPathPaint.planeo.transform.scale.z *= Config.getTextureResY() / Config.getTextureResX();
+		RenderPathPaint.planeo.transform.loc.set(m._30, -m._31, 0.0);
+		RenderPathPaint.planeo.transform.buildMatrix();
+	}
+
+	static restorePlaneMesh = () => {
+		Context.raw.paint2dView = false;
+		RenderPathPaint.planeo.visible = false;
+		RenderPathPaint.planeo.transform.loc.set(0.0, 0.0, 0.0);
+		for (let i = 0; i < Project.paintObjects.length; ++i) {
+			Project.paintObjects[i].visible = RenderPathPaint.visibles[i];
+		}
+		if (Context.raw.mergedObject != null) {
+			Context.raw.mergedObject.visible = RenderPathPaint.mergedObjectVisible;
+		}
+		Context.raw.paintObject = RenderPathPaint.painto;
+		Scene.active.camera.transform.setMatrix(Context.raw.savedCamera);
+		Scene.active.camera.data.raw.fov = RenderPathPaint.savedFov;
+		Viewport.updateCameraType(Context.raw.cameraType);
+		Scene.active.camera.buildProjection();
+		Scene.active.camera.buildMatrix();
+
+		RenderPathBase.drawGbuffer();
+	}
+
+	static bindLayers = () => {
+		///if is_paint
+		let isLive = Config.raw.brush_live && RenderPathPaint.liveLayerDrawn > 0;
+		let isMaterialTool = Context.raw.tool == WorkspaceTool.ToolMaterial;
+		if (isLive || isMaterialTool) RenderPathPaint.useLiveLayer(true);
+		///end
+
+		for (let i = 0; i < Project.layers.length; ++i) {
+			let l = Project.layers[i];
+			RenderPathPaint.path.bindTarget("texpaint" + l.id, "texpaint" + l.id);
+
+			///if is_paint
+			if (l.isLayer()) {
+				RenderPathPaint.path.bindTarget("texpaint_nor" + l.id, "texpaint_nor" + l.id);
+				RenderPathPaint.path.bindTarget("texpaint_pack" + l.id, "texpaint_pack" + l.id);
+			}
+			///end
+		}
+	}
+
+	static unbindLayers = () => {
+		///if is_paint
+		let isLive = Config.raw.brush_live && RenderPathPaint.liveLayerDrawn > 0;
+		let isMaterialTool = Context.raw.tool == WorkspaceTool.ToolMaterial;
+		if (isLive || isMaterialTool) RenderPathPaint.useLiveLayer(false);
+		///end
+	}
+
+	static dilate = (base: bool, nor_pack: bool) => {
+		///if is_paint
+		if (Config.raw.dilate_radius > 0 && !Context.raw.paint2d) {
+			UtilUV.cacheDilateMap();
+			Base.makeTempImg();
+			let tid = Context.raw.layer.id;
+			if (base) {
+				let texpaint = "texpaint";
+				RenderPathPaint.path.setTarget("temptex0");
+				RenderPathPaint.path.bindTarget(texpaint + tid, "tex");
+				RenderPathPaint.path.drawShader("shader_datas/copy_pass/copy_pass");
+				RenderPathPaint.path.setTarget(texpaint + tid);
+				RenderPathPaint.path.bindTarget("temptex0", "tex");
+				RenderPathPaint.path.drawShader("shader_datas/dilate_pass/dilate_pass");
+			}
+			if (nor_pack && !Context.raw.layer.isMask()) {
+				RenderPathPaint.path.setTarget("temptex0");
+				RenderPathPaint.path.bindTarget("texpaint_nor" + tid, "tex");
+				RenderPathPaint.path.drawShader("shader_datas/copy_pass/copy_pass");
+				RenderPathPaint.path.setTarget("texpaint_nor" + tid);
+				RenderPathPaint.path.bindTarget("temptex0", "tex");
+				RenderPathPaint.path.drawShader("shader_datas/dilate_pass/dilate_pass");
+
+				RenderPathPaint.path.setTarget("temptex0");
+				RenderPathPaint.path.bindTarget("texpaint_pack" + tid, "tex");
+				RenderPathPaint.path.drawShader("shader_datas/copy_pass/copy_pass");
+				RenderPathPaint.path.setTarget("texpaint_pack" + tid);
+				RenderPathPaint.path.bindTarget("temptex0", "tex");
+				RenderPathPaint.path.drawShader("shader_datas/dilate_pass/dilate_pass");
+			}
+		}
+		///end
+	}
+
+	static array_u32 = (ar: i32[]): Uint32Array => {
+		let res = new Uint32Array(ar.length);
+		for (let i = 0; i < ar.length; ++i) res[i] = ar[i];
+		return res;
+	}
+
+	static array_i16 = (ar: i32[]): Int16Array => {
+		let res = new Int16Array(ar.length);
+		for (let i = 0; i < ar.length; ++i) res[i] = ar[i];
+		return res;
+	}
+}

+ 167 - 0
armorpaint/Sources/RenderPathPreview.ts

@@ -0,0 +1,167 @@
+
+class RenderPathPreview {
+
+	static path: RenderPath;
+
+	static init = (_path: RenderPath) => {
+		RenderPathPreview.path = _path;
+
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpreview";
+			t.width = 1;
+			t.height = 1;
+			t.format = "RGBA32";
+			RenderPathPreview.path.createRenderTarget(t);
+		}
+		{
+			let t = new RenderTargetRaw();
+			t.name = "texpreview_icon";
+			t.width = 1;
+			t.height = 1;
+			t.format = "RGBA32";
+			RenderPathPreview.path.createRenderTarget(t);
+		}
+
+		RenderPathPreview.path.createDepthBuffer("mmain", "DEPTH24");
+
+		{
+			let t = new RenderTargetRaw();
+			t.name = "mtex";
+			t.width = Math.floor(UtilRender.materialPreviewSize * 2.0);
+			t.height = Math.floor(UtilRender.materialPreviewSize * 2.0);
+			t.format = "RGBA64";
+			t.scale = RenderPathBase.getSuperSampling();
+			///if krom_opengl
+			t.depth_buffer = "mmain";
+			///end
+			RenderPathPreview.path.createRenderTarget(t);
+		}
+
+		{
+			let t = new RenderTargetRaw();
+			t.name = "mgbuffer0";
+			t.width = Math.floor(UtilRender.materialPreviewSize * 2.0);
+			t.height = Math.floor(UtilRender.materialPreviewSize * 2.0);
+			t.format = "RGBA64";
+			t.scale = RenderPathBase.getSuperSampling();
+			t.depth_buffer = "mmain";
+			RenderPathPreview.path.createRenderTarget(t);
+		}
+
+		{
+			let t = new RenderTargetRaw();
+			t.name = "mgbuffer1";
+			t.width = Math.floor(UtilRender.materialPreviewSize * 2.0);
+			t.height = Math.floor(UtilRender.materialPreviewSize * 2.0);
+			t.format = "RGBA64";
+			t.scale = RenderPathBase.getSuperSampling();
+			RenderPathPreview.path.createRenderTarget(t);
+		}
+
+		{
+			let t = new RenderTargetRaw();
+			t.name = "mgbuffer2";
+			t.width = Math.floor(UtilRender.materialPreviewSize * 2.0);
+			t.height = Math.floor(UtilRender.materialPreviewSize * 2.0);
+			t.format = "RGBA64";
+			t.scale = RenderPathBase.getSuperSampling();
+			RenderPathPreview.path.createRenderTarget(t);
+		}
+	}
+
+	static commandsPreview = () => {
+		RenderPathPreview.path.setTarget("mgbuffer2");
+		RenderPathPreview.path.clearTarget(0xff000000);
+
+		///if (krom_metal)
+		let clearColor = 0xffffffff;
+		///else
+		let clearColor: Null<i32> = null;
+		///end
+
+		RenderPathPreview.path.setTarget("mgbuffer0");
+		RenderPathPreview.path.clearTarget(clearColor, 1.0);
+		RenderPathPreview.path.setTarget("mgbuffer0", ["mgbuffer1", "mgbuffer2"]);
+		RenderPathPreview.path.drawMeshes("mesh");
+
+		// Deferred light
+		RenderPathPreview.path.setTarget("mtex");
+		RenderPathPreview.path.bindTarget("_mmain", "gbufferD");
+		RenderPathPreview.path.bindTarget("mgbuffer0", "gbuffer0");
+		RenderPathPreview.path.bindTarget("mgbuffer1", "gbuffer1");
+		{
+			RenderPathPreview.path.bindTarget("empty_white", "ssaotex");
+		}
+		RenderPathPreview.path.drawShader("shader_datas/deferred_light/deferred_light");
+
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		RenderPathPreview.path.setDepthFrom("mtex", "mgbuffer0"); // Bind depth for world pass
+		///end
+
+		RenderPathPreview.path.setTarget("mtex"); // Re-binds depth
+		RenderPathPreview.path.drawSkydome("shader_datas/world_pass/world_pass");
+
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		RenderPathPreview.path.setDepthFrom("mtex", "mgbuffer1"); // Unbind depth
+		///end
+
+		let framebuffer = "texpreview";
+		let selectedMat = Context.raw.material;
+		RenderPath.active.renderTargets.get("texpreview").image = selectedMat.image;
+		RenderPath.active.renderTargets.get("texpreview_icon").image = selectedMat.imageIcon;
+
+		RenderPathPreview.path.setTarget(framebuffer);
+		RenderPathPreview.path.bindTarget("mtex", "tex");
+		RenderPathPreview.path.drawShader("shader_datas/compositor_pass/compositor_pass");
+
+		RenderPathPreview.path.setTarget("texpreview_icon");
+		RenderPathPreview.path.bindTarget("texpreview", "tex");
+		RenderPathPreview.path.drawShader("shader_datas/supersample_resolve/supersample_resolve");
+	}
+
+	static commandsDecal = () => {
+		RenderPathPreview.path.setTarget("gbuffer2");
+		RenderPathPreview.path.clearTarget(0xff000000);
+
+		///if (krom_metal)
+		let clearColor = 0xffffffff;
+		///else
+		let clearColor: Null<i32> = null;
+		///end
+
+		RenderPathPreview.path.setTarget("gbuffer0");
+		RenderPathPreview.path.clearTarget(clearColor, 1.0);
+		RenderPathPreview.path.setTarget("gbuffer0", ["gbuffer1", "gbuffer2"]);
+		RenderPathPreview.path.drawMeshes("mesh");
+
+		// Deferred light
+		RenderPathPreview.path.setTarget("tex");
+		RenderPathPreview.path.bindTarget("_main", "gbufferD");
+		RenderPathPreview.path.bindTarget("gbuffer0", "gbuffer0");
+		RenderPathPreview.path.bindTarget("gbuffer1", "gbuffer1");
+		{
+			RenderPathPreview.path.bindTarget("empty_white", "ssaotex");
+		}
+		RenderPathPreview.path.drawShader("shader_datas/deferred_light/deferred_light");
+
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		RenderPathPreview.path.setDepthFrom("tex", "gbuffer0"); // Bind depth for world pass
+		///end
+
+		RenderPathPreview.path.setTarget("tex");
+		RenderPathPreview.path.drawSkydome("shader_datas/world_pass/world_pass");
+
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		RenderPathPreview.path.setDepthFrom("tex", "gbuffer1"); // Unbind depth
+		///end
+
+		let framebuffer = "texpreview";
+		RenderPath.active.renderTargets.get("texpreview").image = Context.raw.decalImage;
+
+		RenderPathPreview.path.setTarget(framebuffer);
+
+		RenderPathPreview.path.bindTarget("tex", "tex");
+		RenderPathPreview.path.drawShader("shader_datas/compositor_pass/compositor_pass");
+	}
+}

+ 27 - 0
armorpaint/Sources/SlotBrush.ts

@@ -0,0 +1,27 @@
+
+class SlotBrush {
+	nodes = new Nodes();
+	canvas: TNodeCanvas;
+	image: Image = null; // 200px
+	imageIcon: Image = null; // 50px
+	previewReady = false;
+	id = 0;
+	static defaultCanvas: ArrayBuffer = null;
+
+	constructor(c: TNodeCanvas = null) {
+		for (let brush of Project.brushes) if (brush.id >= this.id) this.id = brush.id + 1;
+
+		if (c == null) {
+			if (SlotBrush.defaultCanvas == null) { // Synchronous
+				Data.getBlob("default_brush.arm", (b: ArrayBuffer) => {
+					SlotBrush.defaultCanvas = b;
+				});
+			}
+			this.canvas = ArmPack.decode(SlotBrush.defaultCanvas);
+			this.canvas.name = "Brush " + (this.id + 1);
+		}
+		else {
+			this.canvas = c;
+		}
+	}
+}

+ 16 - 0
armorpaint/Sources/SlotFont.ts

@@ -0,0 +1,16 @@
+
+class SlotFont {
+	image: Image = null; // 200px
+	previewReady = false;
+	id = 0;
+	font: Font;
+	name: string;
+	file: string;
+
+	constructor(name: string, font: Font, file = "") {
+		for (let slot of Project.fonts) if (slot.id >= this.id) this.id = slot.id + 1;
+		this.name = name;
+		this.font = font;
+		this.file = file;
+	}
+}

+ 685 - 0
armorpaint/Sources/SlotLayer.ts

@@ -0,0 +1,685 @@
+
+class SlotLayer {
+	id = 0;
+	name: string;
+	ext = "";
+	visible = true;
+	parent: SlotLayer = null; // Group (for layers) or layer (for masks)
+
+	texpaint: Image = null; // Base or mask
+	///if is_paint
+	texpaint_nor: Image = null;
+	texpaint_pack: Image = null;
+	texpaint_preview: Image = null; // Layer preview
+	///end
+
+	maskOpacity = 1.0; // Opacity mask
+	fill_layer: SlotMaterial = null;
+	show_panel = true;
+	blending = BlendType.BlendMix;
+	objectMask = 0;
+	scale = 1.0;
+	angle = 0.0;
+	uvType = UVType.UVMap;
+	paintBase = true;
+	paintOpac = true;
+	paintOcc = true;
+	paintRough = true;
+	paintMet = true;
+	paintNor = true;
+	paintNorBlend = true;
+	paintHeight = true;
+	paintHeightBlend = true;
+	paintEmis = true;
+	paintSubs = true;
+	decalMat = Mat4.identity(); // Decal layer
+
+	constructor(ext = "", type = LayerSlotType.SlotLayer, parent: SlotLayer = null) {
+		if (ext == "") {
+			this.id = 0;
+			for (let l of Project.layers) if (l.id >= this.id) this.id = l.id + 1;
+			ext = this.id + "";
+		}
+		this.ext = ext;
+		this.parent = parent;
+
+		if (type == LayerSlotType.SlotGroup) {
+			this.name = "Group " + (this.id + 1);
+		}
+		else if (type == LayerSlotType.SlotLayer) {
+			this.name = "Layer " + (this.id + 1);
+			///if is_paint
+			let format = Base.bitsHandle.position == TextureBits.Bits8  ? "RGBA32" :
+						 Base.bitsHandle.position == TextureBits.Bits16 ? "RGBA64" :
+						 									  			  "RGBA128";
+			///end
+
+			///if is_sculpt
+			let format = "RGBA128";
+			///end
+
+			{
+				let t = new RenderTargetRaw();
+				t.name = "texpaint" + ext;
+				t.width = Config.getTextureResX();
+				t.height = Config.getTextureResY();
+				t.format = format;
+				this.texpaint = RenderPath.active.createRenderTarget(t).image;
+			}
+
+			///if is_paint
+			{
+				let t = new RenderTargetRaw();
+				t.name = "texpaint_nor" + ext;
+				t.width = Config.getTextureResX();
+				t.height = Config.getTextureResY();
+				t.format = format;
+				this.texpaint_nor = RenderPath.active.createRenderTarget(t).image;
+			}
+			{
+				let t = new RenderTargetRaw();
+				t.name = "texpaint_pack" + ext;
+				t.width = Config.getTextureResX();
+				t.height = Config.getTextureResY();
+				t.format = format;
+				this.texpaint_pack = RenderPath.active.createRenderTarget(t).image;
+			}
+
+			this.texpaint_preview = Image.createRenderTarget(UtilRender.layerPreviewSize, UtilRender.layerPreviewSize, TextureFormat.RGBA32);
+			///end
+		}
+
+		///if is_paint
+		else { // Mask
+			this.name = "Mask " + (this.id + 1);
+			let format = "RGBA32"; // Full bits for undo support, R8 is used
+			this.blending = BlendType.BlendAdd;
+
+			{
+				let t = new RenderTargetRaw();
+				t.name = "texpaint" + ext;
+				t.width = Config.getTextureResX();
+				t.height = Config.getTextureResY();
+				t.format = format;
+				this.texpaint = RenderPath.active.createRenderTarget(t).image;
+			}
+
+			this.texpaint_preview = Image.createRenderTarget(UtilRender.layerPreviewSize, UtilRender.layerPreviewSize, TextureFormat.RGBA32);
+		}
+		///end
+	}
+
+	delete = () => {
+		this.unload();
+
+		if (this.isLayer()) {
+			let masks = this.getMasks(false); // Prevents deleting group masks
+			if (masks != null) for (let m of masks) m.delete();
+		}
+		else if (this.isGroup()) {
+			let children = this.getChildren();
+			if (children != null) for (let c of children) c.delete();
+			let masks = this.getMasks();
+			if (masks != null) for (let m of masks) m.delete();
+		}
+
+		let lpos = Project.layers.indexOf(this);
+		array_remove(Project.layers, this);
+		// Undo can remove base layer and then restore it from undo layers
+		if (Project.layers.length > 0) {
+			Context.setLayer(Project.layers[lpos > 0 ? lpos - 1 : 0]);
+		}
+
+		// Do not remove empty groups if the last layer is deleted as this prevents redo from working properly
+	}
+
+	unload = () => {
+		if (this.isGroup()) return;
+
+		let _texpaint = this.texpaint;
+		///if is_paint
+		let _texpaint_nor = this.texpaint_nor;
+		let _texpaint_pack = this.texpaint_pack;
+		let _texpaint_preview = this.texpaint_preview;
+		///end
+
+		let _next = () => {
+			_texpaint.unload();
+			///if is_paint
+			if (_texpaint_nor != null) _texpaint_nor.unload();
+			if (_texpaint_pack != null) _texpaint_pack.unload();
+			_texpaint_preview.unload();
+			///end
+		}
+		Base.notifyOnNextFrame(_next);
+
+		RenderPath.active.renderTargets.delete("texpaint" + this.ext);
+		///if is_paint
+		if (this.isLayer()) {
+			RenderPath.active.renderTargets.delete("texpaint_nor" + this.ext);
+			RenderPath.active.renderTargets.delete("texpaint_pack" + this.ext);
+		}
+		///end
+	}
+
+	swap = (other: SlotLayer) => {
+		if ((this.isLayer() || this.isMask()) && (other.isLayer() || other.isMask())) {
+			RenderPath.active.renderTargets.get("texpaint" + this.ext).image = other.texpaint;
+			RenderPath.active.renderTargets.get("texpaint" + other.ext).image = this.texpaint;
+			let _texpaint = this.texpaint;
+			this.texpaint = other.texpaint;
+			other.texpaint = _texpaint;
+
+			///if is_paint
+			let _texpaint_preview = this.texpaint_preview;
+			this.texpaint_preview = other.texpaint_preview;
+			other.texpaint_preview = _texpaint_preview;
+			///end
+		}
+
+		///if is_paint
+		if (this.isLayer() && other.isLayer()) {
+			RenderPath.active.renderTargets.get("texpaint_nor" + this.ext).image = other.texpaint_nor;
+			RenderPath.active.renderTargets.get("texpaint_pack" + this.ext).image = other.texpaint_pack;
+			RenderPath.active.renderTargets.get("texpaint_nor" + other.ext).image = this.texpaint_nor;
+			RenderPath.active.renderTargets.get("texpaint_pack" + other.ext).image = this.texpaint_pack;
+			let _texpaint_nor = this.texpaint_nor;
+			let _texpaint_pack = this.texpaint_pack;
+			this.texpaint_nor = other.texpaint_nor;
+			this.texpaint_pack = other.texpaint_pack;
+			other.texpaint_nor = _texpaint_nor;
+			other.texpaint_pack = _texpaint_pack;
+		}
+		///end
+	}
+
+	clear = (baseColor = 0x00000000, baseImage: Image = null, occlusion = 1.0, roughness = Base.defaultRough, metallic = 0.0) => {
+		this.texpaint.g4.begin();
+		this.texpaint.g4.clear(baseColor); // Base
+		this.texpaint.g4.end();
+		if (baseImage != null) {
+			this.texpaint.g2.begin(false);
+			this.texpaint.g2.drawScaledImage(baseImage, 0, 0, this.texpaint.width, this.texpaint.height);
+			this.texpaint.g2.end();
+		}
+
+		///if is_paint
+		if (this.isLayer()) {
+			this.texpaint_nor.g4.begin();
+			this.texpaint_nor.g4.clear(color_from_floats(0.5, 0.5, 1.0, 0.0)); // Nor
+			this.texpaint_nor.g4.end();
+			this.texpaint_pack.g4.begin();
+			this.texpaint_pack.g4.clear(color_from_floats(occlusion, roughness, metallic, 0.0)); // Occ, rough, met
+			this.texpaint_pack.g4.end();
+		}
+		///end
+
+		Context.raw.layerPreviewDirty = true;
+		Context.raw.ddirty = 3;
+	}
+
+	invertMask = () => {
+		if (Base.pipeInvert8 == null) Base.makePipe();
+		let inverted = Image.createRenderTarget(this.texpaint.width, this.texpaint.height, TextureFormat.RGBA32);
+		inverted.g2.begin(false);
+		inverted.g2.pipeline = Base.pipeInvert8;
+		inverted.g2.drawImage(this.texpaint, 0, 0);
+		inverted.g2.pipeline = null;
+		inverted.g2.end();
+		let _texpaint = this.texpaint;
+		let _next = () => {
+			_texpaint.unload();
+		}
+		Base.notifyOnNextFrame(_next);
+		this.texpaint = RenderPath.active.renderTargets.get("texpaint" + this.id).image = inverted;
+		Context.raw.layerPreviewDirty = true;
+		Context.raw.ddirty = 3;
+	}
+
+	applyMask = () => {
+		if (this.parent.fill_layer != null) {
+			this.parent.toPaintLayer();
+		}
+		if (this.parent.isGroup()) {
+			for (let c of this.parent.getChildren()) {
+				Base.applyMask(c, this);
+			}
+		}
+		else {
+			Base.applyMask(this.parent, this);
+		}
+		this.delete();
+	}
+
+	duplicate = (): SlotLayer => {
+		let layers = Project.layers;
+		let i = layers.indexOf(this) + 1;
+		let l = new SlotLayer("", this.isLayer() ? LayerSlotType.SlotLayer : this.isMask() ? LayerSlotType.SlotMask : LayerSlotType.SlotGroup, this.parent);
+		layers.splice(i, 0, l);
+
+		if (Base.pipeMerge == null) Base.makePipe();
+		if (this.isLayer()) {
+			l.texpaint.g2.begin(false);
+			l.texpaint.g2.pipeline = Base.pipeCopy;
+			l.texpaint.g2.drawImage(this.texpaint, 0, 0);
+			l.texpaint.g2.pipeline = null;
+			l.texpaint.g2.end();
+			///if is_paint
+			l.texpaint_nor.g2.begin(false);
+			l.texpaint_nor.g2.pipeline = Base.pipeCopy;
+			l.texpaint_nor.g2.drawImage(this.texpaint_nor, 0, 0);
+			l.texpaint_nor.g2.pipeline = null;
+			l.texpaint_nor.g2.end();
+			l.texpaint_pack.g2.begin(false);
+			l.texpaint_pack.g2.pipeline = Base.pipeCopy;
+			l.texpaint_pack.g2.drawImage(this.texpaint_pack, 0, 0);
+			l.texpaint_pack.g2.pipeline = null;
+			l.texpaint_pack.g2.end();
+			///end
+		}
+		else if (this.isMask()) {
+			l.texpaint.g2.begin(false);
+			l.texpaint.g2.pipeline = Base.pipeCopy8;
+			l.texpaint.g2.drawImage(this.texpaint, 0, 0);
+			l.texpaint.g2.pipeline = null;
+			l.texpaint.g2.end();
+		}
+
+		///if is_paint
+		l.texpaint_preview.g2.begin(true, 0x00000000);
+		l.texpaint_preview.g2.pipeline = Base.pipeCopy;
+		l.texpaint_preview.g2.drawScaledImage(this.texpaint_preview, 0, 0, this.texpaint_preview.width, this.texpaint_preview.height);
+		l.texpaint_preview.g2.pipeline = null;
+		l.texpaint_preview.g2.end();
+		///end
+
+		l.visible = this.visible;
+		l.maskOpacity = this.maskOpacity;
+		l.fill_layer = this.fill_layer;
+		l.objectMask = this.objectMask;
+		l.blending = this.blending;
+		l.uvType = this.uvType;
+		l.scale = this.scale;
+		l.angle = this.angle;
+		l.paintBase = this.paintBase;
+		l.paintOpac = this.paintOpac;
+		l.paintOcc = this.paintOcc;
+		l.paintRough = this.paintRough;
+		l.paintMet = this.paintMet;
+		l.paintNor = this.paintNor;
+		l.paintNorBlend = this.paintNorBlend;
+		l.paintHeight = this.paintHeight;
+		l.paintHeightBlend = this.paintHeightBlend;
+		l.paintEmis = this.paintEmis;
+		l.paintSubs = this.paintSubs;
+
+		return l;
+	}
+
+	resizeAndSetBits = () => {
+		let resX = Config.getTextureResX();
+		let resY = Config.getTextureResY();
+		let rts = RenderPath.active.renderTargets;
+		if (Base.pipeMerge == null) Base.makePipe();
+
+		if (this.isLayer()) {
+			///if is_paint
+			let format = Base.bitsHandle.position == TextureBits.Bits8  ? TextureFormat.RGBA32 :
+						 Base.bitsHandle.position == TextureBits.Bits16 ? TextureFormat.RGBA64 :
+						 									  			  TextureFormat.RGBA128;
+			///end
+
+			///if is_sculpt
+			let format = TextureFormat.RGBA128;
+			///end
+
+			let _texpaint = this.texpaint;
+			this.texpaint = Image.createRenderTarget(resX, resY, format);
+			this.texpaint.g2.begin(false);
+			this.texpaint.g2.pipeline = Base.pipeCopy;
+			this.texpaint.g2.drawScaledImage(_texpaint, 0, 0, resX, resY);
+			this.texpaint.g2.pipeline = null;
+			this.texpaint.g2.end();
+
+			///if is_paint
+			let _texpaint_nor = this.texpaint_nor;
+			let _texpaint_pack = this.texpaint_pack;
+			this.texpaint_nor = Image.createRenderTarget(resX, resY, format);
+			this.texpaint_pack = Image.createRenderTarget(resX, resY, format);
+
+			this.texpaint_nor.g2.begin(false);
+			this.texpaint_nor.g2.pipeline = Base.pipeCopy;
+			this.texpaint_nor.g2.drawScaledImage(_texpaint_nor, 0, 0, resX, resY);
+			this.texpaint_nor.g2.pipeline = null;
+			this.texpaint_nor.g2.end();
+
+			this.texpaint_pack.g2.begin(false);
+			this.texpaint_pack.g2.pipeline = Base.pipeCopy;
+			this.texpaint_pack.g2.drawScaledImage(_texpaint_pack, 0, 0, resX, resY);
+			this.texpaint_pack.g2.pipeline = null;
+			this.texpaint_pack.g2.end();
+			///end
+
+			let _next = () => {
+				_texpaint.unload();
+				///if is_paint
+				_texpaint_nor.unload();
+				_texpaint_pack.unload();
+				///end
+			}
+			Base.notifyOnNextFrame(_next);
+
+			rts.get("texpaint" + this.ext).image = this.texpaint;
+			///if is_paint
+			rts.get("texpaint_nor" + this.ext).image = this.texpaint_nor;
+			rts.get("texpaint_pack" + this.ext).image = this.texpaint_pack;
+			///end
+		}
+		else if (this.isMask()) {
+			let _texpaint = this.texpaint;
+			this.texpaint = Image.createRenderTarget(resX, resY, TextureFormat.RGBA32);
+
+			this.texpaint.g2.begin(false);
+			this.texpaint.g2.pipeline = Base.pipeCopy8;
+			this.texpaint.g2.drawScaledImage(_texpaint, 0, 0, resX, resY);
+			this.texpaint.g2.pipeline = null;
+			this.texpaint.g2.end();
+
+			let _next = () => {
+				_texpaint.unload();
+			}
+			Base.notifyOnNextFrame(_next);
+
+			rts.get("texpaint" + this.ext).image = this.texpaint;
+		}
+	}
+
+	toFillLayer = () => {
+		Context.setLayer(this);
+		this.fill_layer = Context.raw.material;
+		Base.updateFillLayer();
+		let _next = () => {
+			MakeMaterial.parsePaintMaterial();
+			Context.raw.layerPreviewDirty = true;
+			UIBase.inst.hwnds[TabArea.TabSidebar0].redraws = 2;
+		}
+		Base.notifyOnNextFrame(_next);
+	}
+
+	toPaintLayer = () => {
+		Context.setLayer(this);
+		this.fill_layer = null;
+		MakeMaterial.parsePaintMaterial();
+		Context.raw.layerPreviewDirty = true;
+		UIBase.inst.hwnds[TabArea.TabSidebar0].redraws = 2;
+	}
+
+	isVisible = (): bool => {
+		return this.visible && (this.parent == null || this.parent.visible);
+	}
+
+	getChildren = (): SlotLayer[] => {
+		let children: SlotLayer[] = null; // Child layers of a group
+		for (let l of Project.layers) {
+			if (l.parent == this && l.isLayer()) {
+				if (children == null) children = [];
+				children.push(l);
+			}
+		}
+		return children;
+	}
+
+	getRecursiveChildren = (): SlotLayer[] => {
+		let children: SlotLayer[] = null;
+		for (let l of Project.layers) {
+			if (l.parent == this) { // Child layers and group masks
+				if (children == null) children = [];
+				children.push(l);
+			}
+			if (l.parent != null && l.parent.parent == this) { // Layer masks
+				if (children == null) children = [];
+				children.push(l);
+			}
+		}
+		return children;
+	}
+
+	getMasks = (includeGroupMasks = true): SlotLayer[] => {
+		if (this.isMask()) return null;
+
+		let children: SlotLayer[] = null;
+		// Child masks of a layer
+		for (let l of Project.layers) {
+			if (l.parent == this && l.isMask()) {
+				if (children == null) children = [];
+				children.push(l);
+			}
+		}
+		// Child masks of a parent group
+		if (includeGroupMasks) {
+			if (this.parent != null && this.parent.isGroup()) {
+				for (let l of Project.layers) {
+					if (l.parent == this.parent && l.isMask()) {
+						if (children == null) children = [];
+						children.push(l);
+					}
+				}
+			}
+		}
+		return children;
+	}
+
+	hasMasks = (includeGroupMasks = true): bool => {
+		// Layer mask
+		for (let l of Project.layers) {
+			if (l.parent == this && l.isMask()) {
+				return true;
+			}
+		}
+		// Group mask
+		if (includeGroupMasks && this.parent != null && this.parent.isGroup()) {
+			for (let l of Project.layers) {
+				if (l.parent == this.parent && l.isMask()) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+
+	getOpacity = (): f32 => {
+		let f = this.maskOpacity;
+		if (this.isLayer() && this.parent != null) f *= this.parent.maskOpacity;
+		return f;
+	}
+
+	getObjectMask = (): i32 => {
+		return this.isMask() ? this.parent.objectMask : this.objectMask;
+	}
+
+	isLayer = (): bool => {
+		///if is_paint
+		return this.texpaint != null && this.texpaint_nor != null;
+		///end
+		///if is_sculpt
+		return this.texpaint != null;
+		///end
+	}
+
+	isGroup = (): bool => {
+		return this.texpaint == null;
+	}
+
+	getContainingGroup = (): SlotLayer => {
+		if (this.parent != null && this.parent.isGroup())
+			return this.parent;
+		else if (this.parent != null && this.parent.parent != null && this.parent.parent.isGroup())
+			return this.parent.parent;
+		else return null;
+	}
+
+	isMask = (): bool => {
+		///if is_paint
+		return this.texpaint != null && this.texpaint_nor == null;
+		///end
+		///if is_sculpt
+		return false;
+		///end
+	}
+
+	isGroupMask = (): bool => {
+		///if is_paint
+		return this.texpaint != null && this.texpaint_nor == null && this.parent.isGroup();
+		///end
+		///if is_sculpt
+		return false;
+		///end
+	}
+
+	isLayerMask = (): bool => {
+		///if is_paint
+		return this.texpaint != null && this.texpaint_nor == null && this.parent.isLayer();
+		///end
+		///if is_sculpt
+		return false;
+		///end
+	}
+
+	isInGroup = (): bool => {
+		return this.parent != null && (this.parent.isGroup() || (this.parent.parent != null && this.parent.parent.isGroup()));
+	}
+
+	canMove = (to: i32): bool => {
+		let oldIndex = Project.layers.indexOf(this);
+
+		let delta = to - oldIndex; // If delta > 0 the layer is moved up, otherwise down
+		if (to < 0 || to > Project.layers.length - 1 || delta == 0) return false;
+
+		// If the layer is moved up, all layers between the old position and the new one move one down.
+		// The layers above the new position stay where they are.
+		// If the new position is on top or on bottom no upper resp. lower layer exists.
+		let newUpperLayer = delta > 0 ? (to < Project.layers.length - 1 ? Project.layers[to + 1] : null) : Project.layers[to];
+
+		// Group or layer is collapsed so we check below and update the upper layer.
+		if (newUpperLayer != null && !newUpperLayer.show_panel) {
+			let children = newUpperLayer.getRecursiveChildren();
+			to -= children != null ? children.length : 0;
+			delta = to - oldIndex;
+			newUpperLayer = delta > 0 ? (to < Project.layers.length - 1 ? Project.layers[to + 1] : null) : Project.layers[to];
+		}
+
+		let newLowerLayer = delta > 0 ? Project.layers[to] : (to > 0 ? Project.layers[to - 1] : null);
+
+		if (this.isMask()) {
+			// Masks can not be on top.
+			if (newUpperLayer == null) return false;
+			// Masks should not be placed below a collapsed group. This condition can be savely removed.
+			if (newUpperLayer.isInGroup() && !newUpperLayer.getContainingGroup().show_panel) return false;
+			// Masks should not be placed below a collapsed layer. This condition can be savely removed.
+			if (newUpperLayer.isMask() && !newUpperLayer.parent.show_panel) return false;
+		}
+
+		if (this.isLayer()) {
+			// Layers can not be moved directly below its own mask(s).
+			if (newUpperLayer != null && newUpperLayer.isMask() && newUpperLayer.parent == this) return false;
+			// Layers can not be placed above a mask as the mask would be reparented.
+			if (newLowerLayer != null && newLowerLayer.isMask()) return false;
+		}
+
+		// Currently groups can not be nested. Thus valid positions for groups are:
+		if (this.isGroup()) {
+			// At the top.
+			if (newUpperLayer == null) return true;
+			// NOT below its own children.
+			if (newUpperLayer.getContainingGroup() == this) return false;
+			// At the bottom.
+			if (newLowerLayer == null) return true;
+			// Above a group.
+			if (newLowerLayer.isGroup()) return true;
+			// Above a non-grouped layer.
+			if (newLowerLayer.isLayer() && !newLowerLayer.isInGroup()) return true;
+			else return false;
+		}
+
+		return true;
+	}
+
+	move = (to: i32) => {
+		if (!this.canMove(to)) {
+			return;
+		}
+
+		let pointers = TabLayers.initLayerMap();
+		let oldIndex = Project.layers.indexOf(this);
+		let delta = to - oldIndex;
+		let newUpperLayer = delta > 0 ? (to < Project.layers.length - 1 ? Project.layers[to + 1] : null) : Project.layers[to];
+
+		// Group or layer is collapsed so we check below and update the upper layer.
+		if (newUpperLayer != null && !newUpperLayer.show_panel) {
+			let children = newUpperLayer.getRecursiveChildren();
+			to -= children != null ? children.length : 0;
+			delta = to - oldIndex;
+			newUpperLayer = delta > 0 ? (to < Project.layers.length - 1 ? Project.layers[to + 1] : null) : Project.layers[to];
+		}
+
+		Context.setLayer(this);
+		History.orderLayers(to);
+		UIBase.inst.hwnds[TabArea.TabSidebar0].redraws = 2;
+
+		array_remove(Project.layers, this);
+		Project.layers.splice(to, 0, this);
+
+		if (this.isLayer()) {
+			let oldParent = this.parent;
+
+			if (newUpperLayer == null)
+				this.parent = null; // Placed on top.
+			else if (newUpperLayer.isInGroup() && !newUpperLayer.getContainingGroup().show_panel)
+				this.parent = null; // Placed below a collapsed group.
+			else if (newUpperLayer.isLayer())
+				this.parent = newUpperLayer.parent; // Placed below a layer, use the same parent.
+			else if (newUpperLayer.isGroup())
+				this.parent = newUpperLayer; // Placed as top layer in a group.
+			else if (newUpperLayer.isGroupMask())
+				this.parent = newUpperLayer.parent; // Placed in a group below the lowest group mask.
+			else if (newUpperLayer.isLayerMask())
+				this.parent = newUpperLayer.getContainingGroup(); // Either the group the mask belongs to or null.
+
+			// Layers can have masks as children. These have to be moved, too.
+			let layerMasks = this.getMasks(false);
+			if (layerMasks != null) {
+				for (let idx = 0; idx < layerMasks.length; ++idx) {
+					let mask = layerMasks[idx];
+					array_remove(Project.layers, mask);
+					// If the masks are moved down each step increases the index below the layer by one.
+					Project.layers.splice(delta > 0 ? oldIndex + delta - 1 : oldIndex + delta + idx, 0, mask);
+				}
+			}
+
+			// The layer is the last layer in the group, remove it. Notice that this might remove group masks.
+			if (oldParent != null && oldParent.getChildren() == null)
+				oldParent.delete();
+		}
+		else if (this.isMask()) {
+			// Precondition newUpperLayer != null, ensured in canMove.
+			if (newUpperLayer.isLayer() || newUpperLayer.isGroup())
+				this.parent = newUpperLayer;
+			else if (newUpperLayer.isMask()) { // Group mask or layer mask.
+				this.parent = newUpperLayer.parent;
+			}
+		}
+		else if (this.isGroup()) {
+			let children = this.getRecursiveChildren();
+			if (children != null) {
+				for (let idx = 0; idx < children.length; ++idx) {
+					let child = children[idx];
+					array_remove(Project.layers, child);
+					// If the children are moved down each step increases the index below the layer by one.
+					Project.layers.splice(delta > 0 ? oldIndex + delta - 1 : oldIndex + delta + idx, 0, child);
+				}
+			}
+		}
+
+		for (let m of Project.materials) TabLayers.remapLayerPointers(m.canvas.nodes, TabLayers.fillLayerMap(pointers));
+	}
+}

+ 65 - 0
armorpaint/Sources/SlotMaterial.ts

@@ -0,0 +1,65 @@
+
+class SlotMaterial {
+	nodes = new Nodes();
+	canvas: TNodeCanvas;
+	image: Image = null;
+	imageIcon: Image = null;
+	previewReady = false;
+	data: MaterialData;
+	id = 0;
+	static defaultCanvas: ArrayBuffer = null;
+
+	paintBase = true;
+	paintOpac = true;
+	paintOcc = true;
+	paintRough = true;
+	paintMet = true;
+	paintNor = true;
+	paintHeight = true;
+	paintEmis = true;
+	paintSubs = true;
+
+	constructor(m: MaterialData = null, c: TNodeCanvas = null) {
+		for (let mat of Project.materials) if (mat.id >= this.id) this.id = mat.id + 1;
+		this.data = m;
+
+		let w = UtilRender.materialPreviewSize;
+		let wIcon = 50;
+		this.image = Image.createRenderTarget(w, w);
+		this.imageIcon = Image.createRenderTarget(wIcon, wIcon);
+
+		if (c == null) {
+			if (SlotMaterial.defaultCanvas == null) { // Synchronous
+				Data.getBlob("default_material.arm", (b: ArrayBuffer) => {
+					SlotMaterial.defaultCanvas = b;
+				});
+			}
+			this.canvas = ArmPack.decode(SlotMaterial.defaultCanvas);
+			this.canvas.name = "Material " + (this.id + 1);
+		}
+		else {
+			this.canvas = c;
+		}
+
+		///if (krom_android || krom_ios)
+		this.nodes.panX -= 50; // Center initial position
+		///end
+	}
+
+	unload = () => {
+		let _next = () => {
+			this.image.unload();
+			this.imageIcon.unload();
+		}
+		Base.notifyOnNextFrame(_next);
+	}
+
+	delete = () => {
+		this.unload();
+		let mpos = Project.materials.indexOf(this);
+		array_remove(Project.materials, this);
+		if (Project.materials.length > 0) {
+			Context.setMaterial(Project.materials[mpos > 0 ? mpos - 1 : 0]);
+		}
+	}
+}

Fișier diff suprimat deoarece este prea mare
+ 242 - 251
armorpaint/Sources/TabLayers.ts


+ 0 - 36
armorpaint/Sources/arm/MakeClone.hx

@@ -1,36 +0,0 @@
-package arm;
-
-class MakeClone {
-
-	public static function run(vert: NodeShader, frag: NodeShader) {
-		frag.add_uniform('vec2 cloneDelta', '_cloneDelta');
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
-		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2((sp.xy + cloneDelta) * gbufferSize), 0).ba;');
-		#else
-		frag.write('vec2 texCoordInp = texelFetch(gbuffer2, ivec2((sp.x + cloneDelta.x) * gbufferSize.x, (1.0 - (sp.y + cloneDelta.y)) * gbufferSize.y), 0).ba;');
-		#end
-
-		frag.write('vec3 texpaint_pack_sample = textureLod(texpaint_pack_undo, texCoordInp, 0.0).rgb;');
-		var base = 'textureLod(texpaint_undo, texCoordInp, 0.0).rgb';
-		var rough = 'texpaint_pack_sample.g';
-		var met = 'texpaint_pack_sample.b';
-		var occ = 'texpaint_pack_sample.r';
-		var nortan = 'textureLod(texpaint_nor_undo, texCoordInp, 0.0).rgb';
-		var height = '0.0';
-		var opac = '1.0';
-		frag.write('vec3 basecol = $base;');
-		frag.write('float roughness = $rough;');
-		frag.write('float metallic = $met;');
-		frag.write('float occlusion = $occ;');
-		frag.write('vec3 nortan = $nortan;');
-		frag.write('float height = $height;');
-		frag.write('float mat_opacity = $opac;');
-		frag.write('float opacity = mat_opacity * brushOpacity;');
-		if (Context.raw.material.paintEmis) {
-			frag.write('float emis = 0.0;');
-		}
-		if (Context.raw.material.paintSubs) {
-			frag.write('float subs = 0.0;');
-		}
-	}
-}

+ 0 - 498
armorpaint/Sources/arm/MakeMaterial.hx

@@ -1,498 +0,0 @@
-package arm;
-
-import zui.Zui.Nodes;
-import zui.Zui.TNode;
-import zui.Zui.TNodeCanvas;
-import iron.System;
-import iron.SceneFormat;
-import iron.ShaderData;
-import iron.MaterialData;
-import iron.RenderPath;
-
-class MakeMaterial {
-
-	public static var defaultScon: ShaderContext = null;
-	public static var defaultMcon: MaterialContext = null;
-
-	public static var heightUsed = false;
-	public static var emisUsed = false;
-	public static var subsUsed = false;
-
-	static function getMOut(): Bool {
-		for (n in UINodes.inst.getCanvasMaterial().nodes) if (n.type == "OUTPUT_MATERIAL_PBR") return true;
-		return false;
-	}
-
-	public static function parseMeshMaterial() {
-		var m = Project.materials[0].data;
-
-		for (c in m.shader.contexts) {
-			if (c.raw.name == "mesh") {
-				m.shader.raw.contexts.remove(c.raw);
-				m.shader.contexts.remove(c);
-				deleteContext(c);
-				break;
-			}
-		}
-
-		if (MakeMesh.layerPassCount > 1) {
-			var i = 0;
-			while (i < m.shader.contexts.length) {
-				var c = m.shader.contexts[i];
-				for (j in 1...MakeMesh.layerPassCount) {
-					if (c.raw.name == "mesh" + j) {
-						m.shader.raw.contexts.remove(c.raw);
-						m.shader.contexts.remove(c);
-						deleteContext(c);
-						i--;
-						break;
-					}
-				}
-				i++;
-			}
-
-			i = 0;
-			while (i < m.contexts.length) {
-				var c = m.contexts[i];
-				for (j in 1...MakeMesh.layerPassCount) {
-					if (c.raw.name == "mesh" + j) {
-						m.raw.contexts.remove(c.raw);
-						m.contexts.remove(c);
-						i--;
-						break;
-					}
-				}
-				i++;
-			}
-		}
-
-		var con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }));
-		var scon = new ShaderContext(con.data, function(scon: ShaderContext){});
-		scon.overrideContext = {};
-		if (con.frag.sharedSamplers.length > 0) {
-			var sampler = con.frag.sharedSamplers[0];
-			scon.overrideContext.shared_sampler = sampler.substr(sampler.lastIndexOf(" ") + 1);
-		}
-		if (!Context.raw.textureFilter) {
-			scon.overrideContext.filter = "point";
-		}
-		m.shader.raw.contexts.push(scon.raw);
-		m.shader.contexts.push(scon);
-
-		for (i in 1...MakeMesh.layerPassCount) {
-			var con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }), i);
-			var scon = new ShaderContext(con.data, function(scon: ShaderContext){});
-			scon.overrideContext = {};
-			if (con.frag.sharedSamplers.length > 0) {
-				var sampler = con.frag.sharedSamplers[0];
-				scon.overrideContext.shared_sampler = sampler.substr(sampler.lastIndexOf(" ") + 1);
-			}
-			if (!Context.raw.textureFilter) {
-				scon.overrideContext.filter = "point";
-			}
-			m.shader.raw.contexts.push(scon.raw);
-			m.shader.contexts.push(scon);
-
-			var mcon = new MaterialContext({ name: "mesh" + i, bind_textures: [] }, function(self: MaterialContext) {});
-			m.raw.contexts.push(mcon.raw);
-			m.contexts.push(mcon);
-		}
-
-		Context.raw.ddirty = 2;
-
-		#if arm_voxels
-		makeVoxel(m);
-		#end
-
-		#if (krom_direct3d12 || krom_vulkan || krom_metal)
-		RenderPathRaytrace.dirty = 1;
-		#end
-	}
-
-	public static function parseParticleMaterial() {
-		var m = Context.raw.particleMaterial;
-		var sc: ShaderContext = null;
-		for (c in m.shader.contexts) {
-			if (c.raw.name == "mesh") {
-				sc = c;
-				break;
-			}
-		}
-		if (sc != null) {
-			m.shader.raw.contexts.remove(sc.raw);
-			m.shader.contexts.remove(sc);
-		}
-		var con = MakeParticle.run(new NodeShaderData({ name: "MaterialParticle", canvas: null }));
-		if (sc != null) deleteContext(sc);
-		sc = new ShaderContext(con.data, function(sc: ShaderContext){});
-		m.shader.raw.contexts.push(sc.raw);
-		m.shader.contexts.push(sc);
-	}
-
-	public static function parseMeshPreviewMaterial(md: MaterialData = null) {
-		if (!getMOut()) return;
-
-		var m = md == null ? Project.materials[0].data : md;
-		var scon: ShaderContext = null;
-		for (c in m.shader.contexts) {
-			if (c.raw.name == "mesh") {
-				scon = c;
-				break;
-			}
-		}
-		m.shader.raw.contexts.remove(scon.raw);
-		m.shader.contexts.remove(scon);
-
-		var mcon: TMaterialContext = { name: "mesh", bind_textures: [] };
-
-		var sd = new NodeShaderData({ name: "Material", canvas: null });
-		var con = MakeMeshPreview.run(sd, mcon);
-
-		for (i in 0...m.contexts.length) {
-			if (m.contexts[i].raw.name == "mesh") {
-				m.contexts[i] = new MaterialContext(mcon, function(self: MaterialContext) {});
-				break;
-			}
-		}
-
-		if (scon != null) deleteContext(scon);
-
-		var compileError = false;
-		scon = new ShaderContext(con.data, function(scon: ShaderContext) {
-			if (scon == null) compileError = true;
-		});
-		if (compileError) return;
-
-		m.shader.raw.contexts.push(scon.raw);
-		m.shader.contexts.push(scon);
-	}
-
-	#if arm_voxels
-	static function makeVoxel(m: MaterialData) {
-		var rebuild = heightUsed;
-		if (Config.raw.rp_gi != false && rebuild) {
-			var scon: ShaderContext = null;
-			for (c in m.shader.contexts) {
-				if (c.raw.name == "voxel") {
-					scon = c;
-					break;
-				}
-			}
-			if (scon != null) MakeVoxel.run(scon);
-		}
-	}
-	#end
-
-	public static function parsePaintMaterial(bakePreviews = true) {
-		if (!getMOut()) return;
-
-		if (bakePreviews) {
-			var current = Graphics2.current;
-			if (current != null) current.end();
-			bakeNodePreviews();
-			if (current != null) current.begin(false);
-		}
-
-		var m = Project.materials[0].data;
-		var scon: ShaderContext = null;
-		var mcon: MaterialContext = null;
-		for (c in m.shader.contexts) {
-			if (c.raw.name == "paint") {
-				m.shader.raw.contexts.remove(c.raw);
-				m.shader.contexts.remove(c);
-				if (c != defaultScon) deleteContext(c);
-				break;
-			}
-		}
-		for (c in m.contexts) {
-			if (c.raw.name == "paint") {
-				m.raw.contexts.remove(c.raw);
-				m.contexts.remove(c);
-				break;
-			}
-		}
-
-		var sdata = new NodeShaderData({ name: "Material", canvas: UINodes.inst.getCanvasMaterial() });
-		var mcon: TMaterialContext = { name: "paint", bind_textures: [] };
-		var con = MakePaint.run(sdata, mcon);
-
-		var compileError = false;
-		var scon = new ShaderContext(con.data, function(scon: ShaderContext) {
-			if (scon == null) compileError = true;
-		});
-		if (compileError) return;
-		scon.overrideContext = {};
-		scon.overrideContext.addressing = "repeat";
-		var mcon = new MaterialContext(mcon, function(mcon: MaterialContext) {});
-
-		m.shader.raw.contexts.push(scon.raw);
-		m.shader.contexts.push(scon);
-		m.raw.contexts.push(mcon.raw);
-		m.contexts.push(mcon);
-
-		if (defaultScon == null) defaultScon = scon;
-		if (defaultMcon == null) defaultMcon = mcon;
-	}
-
-	static function bakeNodePreviews() {
-		Context.raw.nodePreviewsUsed = [];
-		if (Context.raw.nodePreviews == null) Context.raw.nodePreviews = [];
-		traverseNodes(UINodes.inst.getCanvasMaterial().nodes, null, []);
-		for (key in Context.raw.nodePreviews.keys()) {
-			if (Context.raw.nodePreviewsUsed.indexOf(key) == -1) {
-				var image = Context.raw.nodePreviews.get(key);
-				Base.notifyOnNextFrame(image.unload);
-				Context.raw.nodePreviews.remove(key);
-			}
-		}
-	}
-
-	static function traverseNodes(nodes: Array<TNode>, group: TNodeCanvas, parents: Array<TNode>) {
-		for (node in nodes) {
-			bakeNodePreview(node, group, parents);
-			if (node.type == "GROUP") {
-				for (g in Project.materialGroups) {
-					if (g.canvas.name == node.name) {
-						parents.push(node);
-						traverseNodes(g.canvas.nodes, g.canvas, parents);
-						parents.pop();
-						break;
-					}
-				}
-			}
-		}
-	}
-
-	static function bakeNodePreview(node: TNode, group: TNodeCanvas, parents: Array<TNode>) {
-		if (node.type == "BLUR") {
-			var id = ParserMaterial.node_name(node, parents);
-			var image = Context.raw.nodePreviews.get(id);
-			Context.raw.nodePreviewsUsed.push(id);
-			var resX = Std.int(Config.getTextureResX() / 4);
-			var resY = Std.int(Config.getTextureResY() / 4);
-			if (image == null || image.width != resX || image.height != resY) {
-				if (image != null) image.unload();
-				image = Image.createRenderTarget(resX, resY);
-				Context.raw.nodePreviews.set(id, image);
-			}
-
-			ParserMaterial.blur_passthrough = true;
-			UtilRender.makeNodePreview(UINodes.inst.getCanvasMaterial(), node, image, group, parents);
-			ParserMaterial.blur_passthrough = false;
-		}
-		else if (node.type == "DIRECT_WARP") {
-			var id = ParserMaterial.node_name(node, parents);
-			var image = Context.raw.nodePreviews.get(id);
-			Context.raw.nodePreviewsUsed.push(id);
-			var resX = Std.int(Config.getTextureResX());
-			var resY = Std.int(Config.getTextureResY());
-			if (image == null || image.width != resX || image.height != resY) {
-				if (image != null) image.unload();
-				image = Image.createRenderTarget(resX, resY);
-				Context.raw.nodePreviews.set(id, image);
-			}
-
-			ParserMaterial.warp_passthrough = true;
-			UtilRender.makeNodePreview(UINodes.inst.getCanvasMaterial(), node, image, group, parents);
-			ParserMaterial.warp_passthrough = false;
-		}
-		else if (node.type == "BAKE_CURVATURE") {
-			var id = ParserMaterial.node_name(node, parents);
-			var image = Context.raw.nodePreviews.get(id);
-			Context.raw.nodePreviewsUsed.push(id);
-			var resX = Std.int(Config.getTextureResX());
-			var resY = Std.int(Config.getTextureResY());
-			if (image == null || image.width != resX || image.height != resY) {
-				if (image != null) image.unload();
-				image = Image.createRenderTarget(resX, resY, TextureFormat.R8);
-				Context.raw.nodePreviews.set(id, image);
-			}
-
-			if (RenderPathPaint.liveLayer == null) {
-				RenderPathPaint.liveLayer = new SlotLayer("_live");
-			}
-
-			var _space = UIHeader.inst.worktab.position;
-			var _tool = Context.raw.tool;
-			var _bakeType = Context.raw.bakeType;
-			UIHeader.inst.worktab.position = Space3D;
-			Context.raw.tool = ToolBake;
-			Context.raw.bakeType = BakeCurvature;
-
-			ParserMaterial.bake_passthrough = true;
-			ParserMaterial.start_node = node;
-			ParserMaterial.start_group = group;
-			ParserMaterial.start_parents = parents;
-			parsePaintMaterial(false);
-			ParserMaterial.bake_passthrough = false;
-			ParserMaterial.start_node = null;
-			ParserMaterial.start_group = null;
-			ParserMaterial.start_parents = null;
-			Context.raw.pdirty = 1;
-			RenderPathPaint.useLiveLayer(true);
-			RenderPathPaint.commandsPaint(false);
-			RenderPathPaint.dilate(true, false);
-			RenderPathPaint.useLiveLayer(false);
-			Context.raw.pdirty = 0;
-
-			UIHeader.inst.worktab.position = _space;
-			Context.raw.tool = _tool;
-			Context.raw.bakeType = _bakeType;
-			parsePaintMaterial(false);
-
-			var rts = RenderPath.active.renderTargets;
-			var texpaint_live = rts.get("texpaint_live");
-
-			image.g2.begin(false);
-			image.g2.drawImage(texpaint_live.image, 0, 0);
-			image.g2.end();
-		}
-	}
-
-	public static function parseNodePreviewMaterial(node: TNode, group: TNodeCanvas = null, parents: Array<TNode> = null): { scon: ShaderContext, mcon: MaterialContext } {
-		if (node.outputs.length == 0) return null;
-		var sdata = new NodeShaderData({ name: "Material", canvas: UINodes.inst.getCanvasMaterial() });
-		var mcon_raw: TMaterialContext = { name: "mesh", bind_textures: [] };
-		var con = MakeNodePreview.run(sdata, mcon_raw, node, group, parents);
-		var compileError = false;
-		var scon = new ShaderContext(con.data, function(scon: ShaderContext) {
-			if (scon == null) compileError = true;
-		});
-		if (compileError) return null;
-		var mcon = new MaterialContext(mcon_raw, function(mcon: MaterialContext) {});
-		return { scon: scon, mcon: mcon };
-	}
-
-	public static function parseBrush() {
-		ParserLogic.parse(Context.raw.brush.canvas);
-	}
-
-	public static function blendMode(frag: NodeShader, blending: Int, cola: String, colb: String, opac: String): String {
-		if (blending == BlendMix) {
-			return 'mix($cola, $colb, $opac)';
-		}
-		else if (blending == BlendDarken) {
-			return 'mix($cola, min($cola, $colb), $opac)';
-		}
-		else if (blending == BlendMultiply) {
-			return 'mix($cola, $cola * $colb, $opac)';
-		}
-		else if (blending == BlendBurn) {
-			return 'mix($cola, vec3(1.0, 1.0, 1.0) - (vec3(1.0, 1.0, 1.0) - $cola) / $colb, $opac)';
-		}
-		else if (blending == BlendLighten) {
-			return 'max($cola, $colb * $opac)';
-		}
-		else if (blending == BlendScreen) {
-			return '(vec3(1.0, 1.0, 1.0) - (vec3(1.0 - $opac, 1.0 - $opac, 1.0 - $opac) + $opac * (vec3(1.0, 1.0, 1.0) - $colb)) * (vec3(1.0, 1.0, 1.0) - $cola))';
-		}
-		else if (blending == BlendDodge) {
-			return 'mix($cola, $cola / (vec3(1.0, 1.0, 1.0) - $colb), $opac)';
-		}
-		else if (blending == BlendAdd) {
-			return 'mix($cola, $cola + $colb, $opac)';
-		}
-		else if (blending == BlendOverlay) {
-			return 'mix($cola, vec3(
-				$cola.r < 0.5 ? 2.0 * $cola.r * $colb.r : 1.0 - 2.0 * (1.0 - $cola.r) * (1.0 - $colb.r),
-				$cola.g < 0.5 ? 2.0 * $cola.g * $colb.g : 1.0 - 2.0 * (1.0 - $cola.g) * (1.0 - $colb.g),
-				$cola.b < 0.5 ? 2.0 * $cola.b * $colb.b : 1.0 - 2.0 * (1.0 - $cola.b) * (1.0 - $colb.b)
-			), $opac)';
-		}
-		else if (blending == BlendSoftLight) {
-			return '((1.0 - $opac) * $cola + $opac * ((vec3(1.0, 1.0, 1.0) - $cola) * $colb * $cola + $cola * (vec3(1.0, 1.0, 1.0) - (vec3(1.0, 1.0, 1.0) - $colb) * (vec3(1.0, 1.0, 1.0) - $cola))))';
-		}
-		else if (blending == BlendLinearLight) {
-			return '($cola + $opac * (vec3(2.0, 2.0, 2.0) * ($colb - vec3(0.5, 0.5, 0.5))))';
-		}
-		else if (blending == BlendDifference) {
-			return 'mix($cola, abs($cola - $colb), $opac)';
-		}
-		else if (blending == BlendSubtract) {
-			return 'mix($cola, $cola - $colb, $opac)';
-		}
-		else if (blending == BlendDivide) {
-			return 'vec3(1.0 - $opac, 1.0 - $opac, 1.0 - $opac) * $cola + vec3($opac, $opac, $opac) * $cola / $colb';
-		}
-		else if (blending == BlendHue) {
-			frag.add_function(ShaderFunctions.str_hue_sat);
-			return 'mix($cola, hsv_to_rgb(vec3(rgb_to_hsv($colb).r, rgb_to_hsv($cola).g, rgb_to_hsv($cola).b)), $opac)';
-		}
-		else if (blending == BlendSaturation) {
-			frag.add_function(ShaderFunctions.str_hue_sat);
-			return 'mix($cola, hsv_to_rgb(vec3(rgb_to_hsv($cola).r, rgb_to_hsv($colb).g, rgb_to_hsv($cola).b)), $opac)';
-		}
-		else if (blending == BlendColor) {
-			frag.add_function(ShaderFunctions.str_hue_sat);
-			return 'mix($cola, hsv_to_rgb(vec3(rgb_to_hsv($colb).r, rgb_to_hsv($colb).g, rgb_to_hsv($cola).b)), $opac)';
-		}
-		else { // BlendValue
-			frag.add_function(ShaderFunctions.str_hue_sat);
-			return 'mix($cola, hsv_to_rgb(vec3(rgb_to_hsv($cola).r, rgb_to_hsv($cola).g, rgb_to_hsv($colb).b)), $opac)';
-		}
-	}
-
-	public static function blendModeMask(frag: NodeShader, blending: Int, cola: String, colb: String, opac: String): String {
-		if (blending == BlendMix) {
-			return 'mix($cola, $colb, $opac)';
-		}
-		else if (blending == BlendDarken) {
-			return 'mix($cola, min($cola, $colb), $opac)';
-		}
-		else if (blending == BlendMultiply) {
-			return 'mix($cola, $cola * $colb, $opac)';
-		}
-		else if (blending == BlendBurn) {
-			return 'mix($cola, 1.0 - (1.0 - $cola) / $colb, $opac)';
-		}
-		else if (blending == BlendLighten) {
-			return 'max($cola, $colb * $opac)';
-		}
-		else if (blending == BlendScreen) {
-			return '(1.0 - ((1.0 - $opac) + $opac * (1.0 - $colb)) * (1.0 - $cola))';
-		}
-		else if (blending == BlendDodge) {
-			return 'mix($cola, $cola / (1.0 - $colb), $opac)';
-		}
-		else if (blending == BlendAdd) {
-			return 'mix($cola, $cola + $colb, $opac)';
-		}
-		else if (blending == BlendOverlay) {
-			return 'mix($cola, $cola < 0.5 ? 2.0 * $cola * $colb : 1.0 - 2.0 * (1.0 - $cola) * (1.0 - $colb), $opac)';
-		}
-		else if (blending == BlendSoftLight) {
-			return '((1.0 - $opac) * $cola + $opac * ((1.0 - $cola) * $colb * $cola + $cola * (1.0 - (1.0 - $colb) * (1.0 - $cola))))';
-		}
-		else if (blending == BlendLinearLight) {
-			return '($cola + $opac * (2.0 * ($colb - 0.5)))';
-		}
-		else if (blending == BlendDifference) {
-			return 'mix($cola, abs($cola - $colb), $opac)';
-		}
-		else if (blending == BlendSubtract) {
-			return 'mix($cola, $cola - $colb, $opac)';
-		}
-		else if (blending == BlendDivide) {
-			return '(1.0 - $opac) * $cola + $opac * $cola / $colb';
-		}
-		else { // BlendHue, BlendSaturation, BlendColor, BlendValue
-			return 'mix($cola, $colb, $opac)';
-		}
-	}
-
-	public static inline function getDisplaceStrength():Float {
-		var sc = Context.mainObject().transform.scale.x;
-		return Config.raw.displace_strength * 0.02 * sc;
-	}
-
-	public static inline function voxelgiHalfExtents():String {
-		var ext = Context.raw.vxaoExt;
-		return 'const vec3 voxelgiHalfExtents = vec3($ext, $ext, $ext);';
-	}
-
-	static function deleteContext(c: ShaderContext) {
-		Base.notifyOnNextFrame(function() { // Ensure pipeline is no longer in use
-			c.delete();
-		});
-	}
-}

+ 0 - 39
armorpaint/Sources/arm/NodesBrush.hx

@@ -1,39 +0,0 @@
-package arm;
-
-import zui.Zui.Nodes;
-import zui.Zui.TNode;
-import arm.Translator._tr;
-
-class NodesBrush {
-
-	public static var categories = [_tr("Nodes")];
-
-	public static var list: Array<Array<TNode>> = [
-		[ // Category 0
-			arm.nodes.TEX_IMAGE.def,
-			arm.nodes.InputNode.def,
-			arm.nodes.MathNode.def,
-			arm.nodes.RandomNode.def,
-			arm.nodes.SeparateVectorNode.def,
-			arm.nodes.TimeNode.def,
-			arm.nodes.FloatNode.def,
-			arm.nodes.VectorNode.def,
-			arm.nodes.VectorMathNode.def
-		]
-	];
-
-	public static function createNode(nodeType: String): TNode {
-		for (c in list) {
-			for (n in c) {
-				if (n.type == nodeType) {
-					var canvas = Context.raw.brush.canvas;
-					var nodes = Context.raw.brush.nodes;
-					var node = arm.UINodes.makeNode(n, nodes, canvas);
-					canvas.nodes.push(node);
-					return node;
-				}
-			}
-		}
-		return null;
-	}
-}

+ 0 - 968
armorpaint/Sources/arm/RenderPathPaint.hx

@@ -1,968 +0,0 @@
-package arm;
-
-import iron.App;
-import iron.Mat4;
-import iron.Vec4;
-import iron.MeshObject;
-import iron.SceneFormat;
-import iron.MeshData;
-import iron.Input;
-import iron.RenderPath;
-import iron.Scene;
-import iron.Time;
-import iron.Uniforms;
-import iron.MaterialData;
-import iron.ShaderData;
-import iron.ConstData;
-
-class RenderPathPaint {
-
-	public static var liveLayer: SlotLayer = null;
-	public static var liveLayerDrawn = 0;
-	public static var liveLayerLocked = false;
-	static var path: RenderPath;
-	static var dilated = true;
-	static var initVoxels = true; // Bake AO
-	static var pushUndoLast: Bool;
-	static var painto: MeshObject = null;
-	static var planeo: MeshObject = null;
-	static var visibles: Array<Bool> = null;
-	static var mergedObjectVisible = false;
-	static var savedFov = 0.0;
-	static var baking = false;
-	static var _texpaint: RenderTarget;
-	static var _texpaint_nor: RenderTarget;
-	static var _texpaint_pack: RenderTarget;
-	static var _texpaint_undo: RenderTarget;
-	static var _texpaint_nor_undo: RenderTarget;
-	static var _texpaint_pack_undo: RenderTarget;
-	static var lastX = -1.0;
-	static var lastY = -1.0;
-
-	public static function init(_path: RenderPath) {
-		path = _path;
-
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpaint_blend0";
-			t.width = Config.getTextureResX();
-			t.height = Config.getTextureResY();
-			t.format = "R8";
-			path.createRenderTarget(t);
-		}
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpaint_blend1";
-			t.width = Config.getTextureResX();
-			t.height = Config.getTextureResY();
-			t.format = "R8";
-			path.createRenderTarget(t);
-		}
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpaint_colorid";
-			t.width = 1;
-			t.height = 1;
-			t.format = "RGBA32";
-			path.createRenderTarget(t);
-		}
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpaint_picker";
-			t.width = 1;
-			t.height = 1;
-			t.format = "RGBA32";
-			path.createRenderTarget(t);
-		}
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpaint_nor_picker";
-			t.width = 1;
-			t.height = 1;
-			t.format = "RGBA32";
-			path.createRenderTarget(t);
-		}
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpaint_pack_picker";
-			t.width = 1;
-			t.height = 1;
-			t.format = "RGBA32";
-			path.createRenderTarget(t);
-		}
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpaint_uv_picker";
-			t.width = 1;
-			t.height = 1;
-			t.format = "RGBA32";
-			path.createRenderTarget(t);
-		}
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpaint_posnortex_picker0";
-			t.width = 1;
-			t.height = 1;
-			t.format = "RGBA128";
-			path.createRenderTarget(t);
-		}
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpaint_posnortex_picker1";
-			t.width = 1;
-			t.height = 1;
-			t.format = "RGBA128";
-			path.createRenderTarget(t);
-		}
-
-		path.loadShader("shader_datas/copy_mrt3_pass/copy_mrt3_pass");
-		path.loadShader("shader_datas/copy_mrt3_pass/copy_mrt3RGBA64_pass");
-		#if is_paint
-		path.loadShader("shader_datas/dilate_pass/dilate_pass");
-		#end
-	}
-
-	public static function commandsPaint(dilation = true) {
-		var tid = Context.raw.layer.id;
-
-		if (Context.raw.pdirty > 0) {
-			#if arm_physics
-			var particlePhysics = Context.raw.particlePhysics;
-			#else
-			var particlePhysics = false;
-			#end
-			if (Context.raw.tool == ToolParticle && !particlePhysics) {
-				path.setTarget("texparticle");
-				path.clearTarget(0x00000000);
-				path.bindTarget("_main", "gbufferD");
-				if ((Context.raw.xray || Config.raw.brush_angle_reject) && Config.raw.brush_3d) {
-					path.bindTarget("gbuffer0", "gbuffer0");
-				}
-
-				var mo: MeshObject = cast Scene.active.getChild(".ParticleEmitter");
-				mo.visible = true;
-				mo.render(path.currentG, "mesh", path.bindParams);
-				mo.visible = false;
-
-				mo = cast Scene.active.getChild(".Particle");
-				mo.visible = true;
-				mo.render(path.currentG, "mesh", path.bindParams);
-				mo.visible = false;
-				path.end();
-			}
-
-			#if is_paint
-			if (Context.raw.tool == ToolColorId) {
-				path.setTarget("texpaint_colorid");
-				path.clearTarget(0xff000000);
-				path.bindTarget("gbuffer2", "gbuffer2");
-				path.drawMeshes("paint");
-				UIHeader.inst.headerHandle.redraws = 2;
-			}
-			else if (Context.raw.tool == ToolPicker || Context.raw.tool == ToolMaterial) {
-				if (Context.raw.pickPosNorTex) {
-					if (Context.raw.paint2d) {
-						path.setTarget("gbuffer0", ["gbuffer1", "gbuffer2"]);
-						path.drawMeshes("mesh");
-					}
-					path.setTarget("texpaint_posnortex_picker0", ["texpaint_posnortex_picker1"]);
-					path.bindTarget("gbuffer2", "gbuffer2");
-					path.bindTarget("_main", "gbufferD");
-					path.drawMeshes("paint");
-					var texpaint_posnortex_picker0 = path.renderTargets.get("texpaint_posnortex_picker0").image;
-					var texpaint_posnortex_picker1 = path.renderTargets.get("texpaint_posnortex_picker1").image;
-					var a = new js.lib.DataView(texpaint_posnortex_picker0.getPixels());
-					var b = new js.lib.DataView(texpaint_posnortex_picker1.getPixels());
-					Context.raw.posXPicked = a.getFloat32(0, true);
-					Context.raw.posYPicked = a.getFloat32(4, true);
-					Context.raw.posZPicked = a.getFloat32(8, true);
-					Context.raw.uvxPicked = a.getFloat32(12, true);
-					Context.raw.norXPicked = b.getFloat32(0, true);
-					Context.raw.norYPicked = b.getFloat32(4, true);
-					Context.raw.norZPicked = b.getFloat32(8, true);
-					Context.raw.uvyPicked = b.getFloat32(12, true);
-				}
-				else {
-					path.setTarget("texpaint_picker", ["texpaint_nor_picker", "texpaint_pack_picker", "texpaint_uv_picker"]);
-					path.bindTarget("gbuffer2", "gbuffer2");
-					tid = Context.raw.layer.id;
-					var useLiveLayer = Context.raw.tool == ToolMaterial;
-					if (useLiveLayer) RenderPathPaint.useLiveLayer(true);
-					path.bindTarget("texpaint" + tid, "texpaint");
-					path.bindTarget("texpaint_nor" + tid, "texpaint_nor");
-					path.bindTarget("texpaint_pack" + tid, "texpaint_pack");
-					path.drawMeshes("paint");
-					if (useLiveLayer) RenderPathPaint.useLiveLayer(false);
-					UIHeader.inst.headerHandle.redraws = 2;
-					UIBase.inst.hwnds[2].redraws = 2;
-
-					var texpaint_picker = path.renderTargets.get("texpaint_picker").image;
-					var texpaint_nor_picker = path.renderTargets.get("texpaint_nor_picker").image;
-					var texpaint_pack_picker = path.renderTargets.get("texpaint_pack_picker").image;
-					var texpaint_uv_picker = path.renderTargets.get("texpaint_uv_picker").image;
-					var a = new js.lib.DataView(texpaint_picker.getPixels());
-					var b = new js.lib.DataView(texpaint_nor_picker.getPixels());
-					var c = new js.lib.DataView(texpaint_pack_picker.getPixels());
-					var d = new js.lib.DataView(texpaint_uv_picker.getPixels());
-
-					if (Context.raw.colorPickerCallback != null) {
-						Context.raw.colorPickerCallback(Context.raw.pickedColor);
-					}
-
-					// Picked surface values
-					#if (krom_metal || krom_vulkan)
-					var i0 = 2;
-					var i1 = 1;
-					var i2 = 0;
-					#else
-					var i0 = 0;
-					var i1 = 1;
-					var i2 = 2;
-					#end
-					var i3 = 3;
-					Context.raw.pickedColor.base.Rb = a.getUint8(i0);
-					Context.raw.pickedColor.base.Gb = a.getUint8(i1);
-					Context.raw.pickedColor.base.Bb = a.getUint8(i2);
-					Context.raw.pickedColor.normal.Rb = b.getUint8(i0);
-					Context.raw.pickedColor.normal.Gb = b.getUint8(i1);
-					Context.raw.pickedColor.normal.Bb = b.getUint8(i2);
-					Context.raw.pickedColor.occlusion = c.getUint8(i0) / 255;
-					Context.raw.pickedColor.roughness = c.getUint8(i1) / 255;
-					Context.raw.pickedColor.metallic = c.getUint8(i2) / 255;
-					Context.raw.pickedColor.height = c.getUint8(i3) / 255;
-					Context.raw.pickedColor.opacity = a.getUint8(i3) / 255;
-					Context.raw.uvxPicked = d.getUint8(i0) / 255;
-					Context.raw.uvyPicked = d.getUint8(i1) / 255;
-					// Pick material
-					if (Context.raw.pickerSelectMaterial && Context.raw.colorPickerCallback == null) {
-						// matid % 3 == 0 - normal, 1 - emission, 2 - subsurface
-						var matid = Std.int((b.getUint8(3) - (b.getUint8(3) % 3)) / 3);
-						for (m in Project.materials) {
-							if (m.id == matid) {
-								Context.setMaterial(m);
-								Context.raw.materialIdPicked = matid;
-								break;
-							}
-						}
-					}
-				}
-			}
-			else {
-				#if arm_voxels
-				if (Context.raw.tool == ToolBake && Context.raw.bakeType == BakeAO) {
-					if (initVoxels) {
-						initVoxels = false;
-						var _rp_gi = Config.raw.rp_gi;
-						Config.raw.rp_gi = true;
-						RenderPathBase.initVoxels();
-						Config.raw.rp_gi = _rp_gi;
-					}
-					path.clearImage("voxels", 0x00000000);
-					path.setTarget("");
-					path.setViewport(256, 256);
-					path.bindTarget("voxels", "voxels");
-					path.drawMeshes("voxel");
-					path.generateMipmaps("voxels");
-				}
-				#end
-
-				var texpaint = "texpaint" + tid;
-				if (Context.raw.tool == ToolBake && Context.raw.brushTime == Time.delta) {
-					// Clear to black on bake start
-					path.setTarget(texpaint);
-					path.clearTarget(0xff000000);
-				}
-
-				path.setTarget("texpaint_blend1");
-				path.bindTarget("texpaint_blend0", "tex");
-				path.drawShader("shader_datas/copy_pass/copyR8_pass");
-				var isMask = Context.raw.layer.isMask();
-				if (isMask) {
-					var ptid = Context.raw.layer.parent.id;
-					if (Context.raw.layer.parent.isGroup()) { // Group mask
-						for (c in Context.raw.layer.parent.getChildren()) {
-							ptid = c.id;
-							break;
-						}
-					}
-					path.setTarget(texpaint, ["texpaint_nor" + ptid, "texpaint_pack" + ptid, "texpaint_blend0"]);
-				}
-				else {
-					path.setTarget(texpaint, ["texpaint_nor" + tid, "texpaint_pack" + tid, "texpaint_blend0"]);
-				}
-				path.bindTarget("_main", "gbufferD");
-				if ((Context.raw.xray || Config.raw.brush_angle_reject) && Config.raw.brush_3d) {
-					path.bindTarget("gbuffer0", "gbuffer0");
-				}
-				path.bindTarget("texpaint_blend1", "paintmask");
-				#if arm_voxels
-				if (Context.raw.tool == ToolBake && Context.raw.bakeType == BakeAO) {
-					path.bindTarget("voxels", "voxels");
-				}
-				#end
-				if (Context.raw.colorIdPicked) {
-					path.bindTarget("texpaint_colorid", "texpaint_colorid");
-				}
-
-				// Read texcoords from gbuffer
-				var readTC = (Context.raw.tool == ToolFill && Context.raw.fillTypeHandle.position == FillFace) ||
-							  Context.raw.tool == ToolClone ||
-							  Context.raw.tool == ToolBlur ||
-							  Context.raw.tool == ToolSmudge;
-				if (readTC) {
-					path.bindTarget("gbuffer2", "gbuffer2");
-				}
-
-				path.drawMeshes("paint");
-
-				if (Context.raw.tool == ToolBake && Context.raw.bakeType == BakeCurvature && Context.raw.bakeCurvSmooth > 0) {
-					if (path.renderTargets.get("texpaint_blur") == null) {
-						var t = new RenderTargetRaw();
-						t.name = "texpaint_blur";
-						t.width = Std.int(Config.getTextureResX() * 0.95);
-						t.height = Std.int(Config.getTextureResY() * 0.95);
-						t.format = "RGBA32";
-						path.createRenderTarget(t);
-					}
-					var blurs = Math.round(Context.raw.bakeCurvSmooth);
-					for (i in 0...blurs) {
-						path.setTarget("texpaint_blur");
-						path.bindTarget(texpaint, "tex");
-						path.drawShader("shader_datas/copy_pass/copy_pass");
-						path.setTarget(texpaint);
-						path.bindTarget("texpaint_blur", "tex");
-						path.drawShader("shader_datas/copy_pass/copy_pass");
-					}
-				}
-
-				if (dilation && Config.raw.dilate == DilateInstant) {
-					dilate(true, false);
-				}
-			}
-			#end
-
-			#if is_sculpt
-			var texpaint = "texpaint" + tid;
-			path.setTarget("texpaint_blend1");
-			path.bindTarget("texpaint_blend0", "tex");
-			path.drawShader("shader_datas/copy_pass/copyR8_pass");
-			path.setTarget(texpaint, ["texpaint_blend0"]);
-			path.bindTarget("gbufferD_undo", "gbufferD");
-			if ((Context.raw.xray || Config.raw.brush_angle_reject) && Config.raw.brush_3d) {
-				path.bindTarget("gbuffer0", "gbuffer0");
-			}
-			path.bindTarget("texpaint_blend1", "paintmask");
-
-			// Read texcoords from gbuffer
-			var readTC = (Context.raw.tool == ToolFill && Context.raw.fillTypeHandle.position == FillFace) ||
-						  Context.raw.tool == ToolClone ||
-						  Context.raw.tool == ToolBlur ||
-						  Context.raw.tool == ToolSmudge;
-			if (readTC) {
-				path.bindTarget("gbuffer2", "gbuffer2");
-			}
-			path.bindTarget("gbuffer0_undo", "gbuffer0_undo");
-
-			var materialContexts: Array<MaterialContext> = [];
-			var shaderContexts: Array<ShaderContext> = [];
-			var mats = Project.paintObjects[0].materials;
-			Project.paintObjects[0].getContexts("paint", mats, materialContexts, shaderContexts);
-
-			var cc_context = shaderContexts[0];
-			if (ConstData.screenAlignedVB == null) ConstData.createScreenAlignedData();
-			path.currentG.setPipeline(cc_context.pipeState);
-			Uniforms.setContextConstants(path.currentG, cc_context, path.bindParams);
-			Uniforms.setObjectConstants(path.currentG, cc_context, Project.paintObjects[0]);
-			Uniforms.setMaterialConstants(path.currentG, cc_context, materialContexts[0]);
-			path.currentG.setVertexBuffer(ConstData.screenAlignedVB);
-			path.currentG.setIndexBuffer(ConstData.screenAlignedIB);
-			path.currentG.drawIndexedVertices();
-			path.end();
-			#end
-		}
-	}
-
-	public static function useLiveLayer(use: Bool) {
-		var tid = Context.raw.layer.id;
-		var hid = History.undoI - 1 < 0 ? Config.raw.undo_steps - 1 : History.undoI - 1;
-		if (use) {
-			_texpaint = path.renderTargets.get("texpaint" + tid);
-			_texpaint_undo = path.renderTargets.get("texpaint_undo" + hid);
-			_texpaint_nor_undo = path.renderTargets.get("texpaint_nor_undo" + hid);
-			_texpaint_pack_undo = path.renderTargets.get("texpaint_pack_undo" + hid);
-			_texpaint_nor = path.renderTargets.get("texpaint_nor" + tid);
-			_texpaint_pack = path.renderTargets.get("texpaint_pack" + tid);
-			path.renderTargets.set("texpaint_undo" + hid, path.renderTargets.get("texpaint" + tid));
-			path.renderTargets.set("texpaint" + tid, path.renderTargets.get("texpaint_live"));
-			if (Context.raw.layer.isLayer()) {
-				path.renderTargets.set("texpaint_nor_undo" + hid, path.renderTargets.get("texpaint_nor" + tid));
-				path.renderTargets.set("texpaint_pack_undo" + hid, path.renderTargets.get("texpaint_pack" + tid));
-				path.renderTargets.set("texpaint_nor" + tid, path.renderTargets.get("texpaint_nor_live"));
-				path.renderTargets.set("texpaint_pack" + tid, path.renderTargets.get("texpaint_pack_live"));
-			}
-		}
-		else {
-			path.renderTargets.set("texpaint" + tid, _texpaint);
-			path.renderTargets.set("texpaint_undo" + hid, _texpaint_undo);
-			if (Context.raw.layer.isLayer()) {
-				path.renderTargets.set("texpaint_nor_undo" + hid, _texpaint_nor_undo);
-				path.renderTargets.set("texpaint_pack_undo" + hid, _texpaint_pack_undo);
-				path.renderTargets.set("texpaint_nor" + tid, _texpaint_nor);
-				path.renderTargets.set("texpaint_pack" + tid, _texpaint_pack);
-			}
-		}
-		liveLayerLocked = use;
-	}
-
-	static function commandsLiveBrush() {
-		var tool = Context.raw.tool;
-		if (tool != ToolBrush &&
-			tool != ToolEraser &&
-			tool != ToolClone &&
-			tool != ToolDecal &&
-			tool != ToolText &&
-			tool != ToolBlur &&
-			tool != ToolSmudge) {
-				return;
-		}
-
-		if (liveLayerLocked) return;
-
-		if (liveLayer == null) {
-			liveLayer = new SlotLayer("_live");
-		}
-
-		var tid = Context.raw.layer.id;
-		if (Context.raw.layer.isMask()) {
-			path.setTarget("texpaint_live");
-			path.bindTarget("texpaint" + tid, "tex");
-			path.drawShader("shader_datas/copy_pass/copy_pass");
-		}
-		else {
-			path.setTarget("texpaint_live", ["texpaint_nor_live", "texpaint_pack_live"]);
-			path.bindTarget("texpaint" + tid, "tex0");
-			path.bindTarget("texpaint_nor" + tid, "tex1");
-			path.bindTarget("texpaint_pack" + tid, "tex2");
-			path.drawShader("shader_datas/copy_mrt3_pass/copy_mrt3_pass");
-		}
-
-		useLiveLayer(true);
-
-		liveLayerDrawn = 2;
-
-		UIView2D.inst.hwnd.redraws = 2;
-		var _x = Context.raw.paintVec.x;
-		var _y = Context.raw.paintVec.y;
-		if (Context.raw.brushLocked) {
-			Context.raw.paintVec.x = (Context.raw.lockStartedX - App.x()) / App.w();
-			Context.raw.paintVec.y = (Context.raw.lockStartedY - App.y()) / App.h();
-		}
-		var _lastX = Context.raw.lastPaintVecX;
-		var _lastY = Context.raw.lastPaintVecY;
-		var _pdirty = Context.raw.pdirty;
-		Context.raw.lastPaintVecX = Context.raw.paintVec.x;
-		Context.raw.lastPaintVecY = Context.raw.paintVec.y;
-		if (Operator.shortcut(Config.keymap.brush_ruler)) {
-			Context.raw.lastPaintVecX = Context.raw.lastPaintX;
-			Context.raw.lastPaintVecY = Context.raw.lastPaintY;
-		}
-		Context.raw.pdirty = 2;
-
-		commandsSymmetry();
-		commandsPaint();
-
-		useLiveLayer(false);
-
-		Context.raw.paintVec.x = _x;
-		Context.raw.paintVec.y = _y;
-		Context.raw.lastPaintVecX = _lastX;
-		Context.raw.lastPaintVecY = _lastY;
-		Context.raw.pdirty = _pdirty;
-		Context.raw.brushBlendDirty = true;
-	}
-
-	public static function commandsCursor() {
-		if (!Config.raw.brush_3d) return;
-		var decal = Context.raw.tool == ToolDecal || Context.raw.tool == ToolText;
-		var decalMask = decal && Operator.shortcut(Config.keymap.decal_mask, ShortcutDown);
-		var tool = Context.raw.tool;
-		if (tool != ToolBrush &&
-			tool != ToolEraser &&
-			tool != ToolClone &&
-			tool != ToolBlur &&
-			tool != ToolSmudge &&
-			tool != ToolParticle &&
-			!decalMask) {
-				return;
-		}
-
-		var fillLayer = Context.raw.layer.fill_layer != null;
-		var groupLayer = Context.raw.layer.isGroup();
-		if (!Base.uiEnabled || Base.isDragging || fillLayer || groupLayer) {
-			return;
-		}
-
-		var mx = Context.raw.paintVec.x;
-		var my = 1.0 - Context.raw.paintVec.y;
-		if (Context.raw.brushLocked) {
-			mx = (Context.raw.lockStartedX - App.x()) / App.w();
-			my = 1.0 - (Context.raw.lockStartedY - App.y()) / App.h();
-		}
-		var radius = decalMask ? Context.raw.brushDecalMaskRadius : Context.raw.brushRadius;
-		drawCursor(mx, my, Context.raw.brushNodesRadius * radius / 3.4);
-	}
-
-	static function drawCursor(mx: Float, my: Float, radius: Float, tintR = 1.0, tintG = 1.0, tintB = 1.0) {
-		var plane = cast(Scene.active.getChild(".Plane"), MeshObject);
-		var geom = plane.data;
-
-		var g = path.frameG;
-		if (Base.pipeCursor == null) Base.makeCursorPipe();
-
-		path.setTarget("");
-		g.setPipeline(Base.pipeCursor);
-		var decal = Context.raw.tool == ToolDecal || Context.raw.tool == ToolText;
-		var decalMask = decal && Operator.shortcut(Config.keymap.decal_mask, ShortcutDown);
-		var img = (decal && !decalMask) ? Context.raw.decalImage : Res.get("cursor.k");
-		g.setTexture(Base.cursorTex, img);
-		var gbuffer0 = path.renderTargets.get("gbuffer0").image;
-		g.setTextureDepth(Base.cursorGbufferD, gbuffer0);
-		g.setFloat2(Base.cursorMouse, mx, my);
-		g.setFloat2(Base.cursorTexStep, 1 / gbuffer0.width, 1 / gbuffer0.height);
-		g.setFloat(Base.cursorRadius, radius);
-		var right = Scene.active.camera.rightWorld().normalize();
-		g.setFloat3(Base.cursorCameraRight, right.x, right.y, right.z);
-		g.setFloat3(Base.cursorTint, tintR, tintG, tintB);
-		g.setMatrix(Base.cursorVP, Scene.active.camera.VP);
-		var helpMat = Mat4.identity();
-		helpMat.getInverse(Scene.active.camera.VP);
-		g.setMatrix(Base.cursorInvVP, helpMat);
-		#if (krom_metal || krom_vulkan)
-		g.setVertexBuffer(geom.get([{name: "tex", data: "short2norm"}]));
-		#else
-		g.setVertexBuffer(geom.vertexBuffer);
-		#end
-		g.setIndexBuffer(geom.indexBuffers[0]);
-		g.drawIndexedVertices();
-
-		g.disableScissor();
-		path.end();
-	}
-
-	static function commandsSymmetry() {
-		if (Context.raw.symX || Context.raw.symY || Context.raw.symZ) {
-			Context.raw.ddirty = 2;
-			var t = Context.raw.paintObject.transform;
-			var sx = t.scale.x;
-			var sy = t.scale.y;
-			var sz = t.scale.z;
-			if (Context.raw.symX) {
-				t.scale.set(-sx, sy, sz);
-				t.buildMatrix();
-				commandsPaint(false);
-			}
-			if (Context.raw.symY) {
-				t.scale.set(sx, -sy, sz);
-				t.buildMatrix();
-				commandsPaint(false);
-			}
-			if (Context.raw.symZ) {
-				t.scale.set(sx, sy, -sz);
-				t.buildMatrix();
-				commandsPaint(false);
-			}
-			if (Context.raw.symX && Context.raw.symY) {
-				t.scale.set(-sx, -sy, sz);
-				t.buildMatrix();
-				commandsPaint(false);
-			}
-			if (Context.raw.symX && Context.raw.symZ) {
-				t.scale.set(-sx, sy, -sz);
-				t.buildMatrix();
-				commandsPaint(false);
-			}
-			if (Context.raw.symY && Context.raw.symZ) {
-				t.scale.set(sx, -sy, -sz);
-				t.buildMatrix();
-				commandsPaint(false);
-			}
-			if (Context.raw.symX && Context.raw.symY && Context.raw.symZ) {
-				t.scale.set(-sx, -sy, -sz);
-				t.buildMatrix();
-				commandsPaint(false);
-			}
-			t.scale.set(sx, sy, sz);
-			t.buildMatrix();
-		}
-	}
-
-	static function paintEnabled(): Bool {
-		#if is_paint
-		var fillLayer = Context.raw.layer.fill_layer != null && Context.raw.tool != ToolPicker && Context.raw.tool != ToolMaterial && Context.raw.tool != ToolColorId;
-		#end
-
-		#if is_sculpt
-		var fillLayer = Context.raw.layer.fill_layer != null && Context.raw.tool != ToolPicker && Context.raw.tool != ToolMaterial;
-		#end
-
-		var groupLayer = Context.raw.layer.isGroup();
-		return !fillLayer && !groupLayer && !Context.raw.foregroundEvent;
-	}
-
-	public static function liveBrushDirty() {
-		var mouse = Input.getMouse();
-		var mx = lastX;
-		var my = lastY;
-		lastX = mouse.viewX;
-		lastY = mouse.viewY;
-		if (Config.raw.brush_live && Context.raw.pdirty <= 0) {
-			var moved = (mx != lastX || my != lastY) && (Context.inViewport() || Context.in2dView());
-			if (moved || Context.raw.brushLocked) {
-				Context.raw.rdirty = 2;
-			}
-		}
-	}
-
-	public static function begin() {
-
-		#if is_paint
-		if (!dilated) {
-			dilate(Config.raw.dilate == DilateDelayed, true);
-			dilated = true;
-		}
-		#end
-
-		if (!paintEnabled()) return;
-
-		#if is_paint
-		pushUndoLast = History.pushUndo;
-		#end
-
-		if (History.pushUndo && History.undoLayers != null) {
-			History.paint();
-
-			#if is_sculpt
-			path.setTarget("gbuffer0_undo");
-			path.bindTarget("gbuffer0", "tex");
-			path.drawShader("shader_datas/copy_pass/copy_pass");
-
-			path.setTarget("gbufferD_undo");
-			path.bindTarget("_main", "tex");
-			path.drawShader("shader_datas/copy_pass/copy_pass");
-			#end
-		}
-
-		#if is_sculpt
-		if (History.pushUndo2 && History.undoLayers != null) {
-			History.paint();
-		}
-		#end
-
-		if (Context.raw.paint2d) {
-			setPlaneMesh();
-		}
-
-		if (liveLayerDrawn > 0) liveLayerDrawn--;
-
-		if (Config.raw.brush_live && Context.raw.pdirty <= 0 && Context.raw.ddirty <= 0 && Context.raw.brushTime == 0) {
-			// Depth is unchanged, draw before gbuffer gets updated
-			commandsLiveBrush();
-		}
-	}
-
-	public static function end() {
-		commandsCursor();
-		Context.raw.ddirty--;
-		Context.raw.rdirty--;
-
-		if (!paintEnabled()) return;
-		Context.raw.pdirty--;
-	}
-
-	public static function draw() {
-		if (!paintEnabled()) return;
-
-		#if (!krom_ios) // No hover on iPad, decals are painted by pen release
-		if (Config.raw.brush_live && Context.raw.pdirty <= 0 && Context.raw.ddirty > 0 && Context.raw.brushTime == 0) {
-			// gbuffer has been updated now but brush will lag 1 frame
-			commandsLiveBrush();
-		}
-		#end
-
-		if (History.undoLayers != null) {
-			commandsSymmetry();
-
-			if (Context.raw.pdirty > 0) dilated = false;
-
-			#if is_paint
-			if (Context.raw.tool == ToolBake) {
-
-				#if (krom_direct3d12 || krom_vulkan || krom_metal)
-				var isRaytracedBake = (Context.raw.bakeType == BakeAO  ||
-					Context.raw.bakeType == BakeLightmap ||
-					Context.raw.bakeType == BakeBentNormal ||
-					Context.raw.bakeType == BakeThickness);
-				#end
-
-				if (Context.raw.bakeType == BakeNormal || Context.raw.bakeType == BakeHeight || Context.raw.bakeType == BakeDerivative) {
-					if (!baking && Context.raw.pdirty > 0) {
-						baking = true;
-						var _bakeType = Context.raw.bakeType;
-						Context.raw.bakeType = Context.raw.bakeType == BakeNormal ? BakeNormalObject : BakePosition; // Bake high poly data
-						MakeMaterial.parsePaintMaterial();
-						var _paintObject = Context.raw.paintObject;
-						var highPoly = Project.paintObjects[Context.raw.bakeHighPoly];
-						var _visible = highPoly.visible;
-						highPoly.visible = true;
-						Context.selectPaintObject(highPoly);
-						commandsPaint();
-						highPoly.visible = _visible;
-						if (pushUndoLast) History.paint();
-						Context.selectPaintObject(_paintObject);
-
-						function _renderFinal() {
-							Context.raw.bakeType = _bakeType;
-							MakeMaterial.parsePaintMaterial();
-							Context.raw.pdirty = 1;
-							commandsPaint();
-							Context.raw.pdirty = 0;
-							baking = false;
-						}
-						function _renderDeriv() {
-							Context.raw.bakeType = BakeHeight;
-							MakeMaterial.parsePaintMaterial();
-							Context.raw.pdirty = 1;
-							commandsPaint();
-							Context.raw.pdirty = 0;
-							if (pushUndoLast) History.paint();
-							App.notifyOnInit(_renderFinal);
-						}
-						App.notifyOnInit(Context.raw.bakeType == BakeDerivative ? _renderDeriv : _renderFinal);
-					}
-				}
-				else if (Context.raw.bakeType == BakeObjectID) {
-					var _layerFilter = Context.raw.layerFilter;
-					var _paintObject = Context.raw.paintObject;
-					var isMerged = Context.raw.mergedObject != null;
-					var _visible = isMerged && Context.raw.mergedObject.visible;
-					Context.raw.layerFilter = 1;
-					if (isMerged) Context.raw.mergedObject.visible = false;
-
-					for (p in Project.paintObjects) {
-						Context.selectPaintObject(p);
-						commandsPaint();
-					}
-
-					Context.raw.layerFilter = _layerFilter;
-					Context.selectPaintObject(_paintObject);
-					if (isMerged) Context.raw.mergedObject.visible = _visible;
-				}
-				#if (krom_direct3d12 || krom_vulkan || krom_metal)
-				else if (isRaytracedBake) {
-					var dirty = RenderPathRaytraceBake.commands(MakeMaterial.parsePaintMaterial);
-					if (dirty) UIHeader.inst.headerHandle.redraws = 2;
-					if (Config.raw.dilate == DilateInstant) { // && Context.raw.pdirty == 1
-						dilate(true, false);
-					}
-				}
-				#end
-				else {
-					commandsPaint();
-				}
-			}
-			else { // Paint
-				commandsPaint();
-			}
-			#end
-
-			#if is_sculpt
-			commandsPaint();
-			#end
-		}
-
-		if (Context.raw.brushBlendDirty) {
-			Context.raw.brushBlendDirty = false;
-			#if krom_metal
-			path.setTarget("texpaint_blend0");
-			path.clearTarget(0x00000000);
-			path.setTarget("texpaint_blend1");
-			path.clearTarget(0x00000000);
-			#else
-			path.setTarget("texpaint_blend0", ["texpaint_blend1"]);
-			path.clearTarget(0x00000000);
-			#end
-		}
-
-		if (Context.raw.paint2d) {
-			restorePlaneMesh();
-		}
-	}
-
-	public static function setPlaneMesh() {
-		Context.raw.paint2dView = true;
-		painto = Context.raw.paintObject;
-		visibles = [];
-		for (p in Project.paintObjects) {
-			visibles.push(p.visible);
-			p.visible = false;
-		}
-		if (Context.raw.mergedObject != null) {
-			mergedObjectVisible = Context.raw.mergedObject.visible;
-			Context.raw.mergedObject.visible = false;
-		}
-
-		var cam = Scene.active.camera;
-		Context.raw.savedCamera.setFrom(cam.transform.local);
-		savedFov = cam.data.raw.fov;
-		Viewport.updateCameraType(CameraPerspective);
-		var m = Mat4.identity();
-		m.translate(0, 0, 0.5);
-		cam.transform.setMatrix(m);
-		cam.data.raw.fov = Base.defaultFov;
-		cam.buildProjection();
-		cam.buildMatrix();
-
-		var tw = 0.95 * UIView2D.inst.panScale;
-		var tx = UIView2D.inst.panX / UIView2D.inst.ww;
-		var ty = UIView2D.inst.panY / App.h();
-		m.setIdentity();
-		m.scale(new Vec4(tw, tw, 1));
-		m.setLoc(new Vec4(tx, ty, 0));
-		var m2 = Mat4.identity();
-		m2.getInverse(Scene.active.camera.VP);
-		m.multmat(m2);
-
-		var tiled = UIView2D.inst.tiledShow;
-		if (tiled && Scene.active.getChild(".PlaneTiled") == null) {
-			// 3x3 planes
-			var posa = [32767,0,-32767,0,10922,0,-10922,0,10922,0,-32767,0,10922,0,-10922,0,-10922,0,10922,0,-10922,0,-10922,0,-10922,0,10922,0,-32767,0,32767,0,-32767,0,10922,0,10922,0,10922,0,-10922,0,32767,0,-10922,0,10922,0,32767,0,10922,0,10922,0,32767,0,10922,0,10922,0,-10922,0,-10922,0,-32767,0,10922,0,-32767,0,-10922,0,32767,0,-10922,0,10922,0,10922,0,10922,0,-10922,0,-10922,0,-32767,0,-32767,0,-10922,0,-32767,0,-32767,0,10922,0,-32767,0,-10922,0,-10922,0,-10922,0,-32767,0,32767,0,-32767,0,32767,0,-10922,0,10922,0,-10922,0,10922,0,-10922,0,10922,0,10922,0,-10922,0,10922,0,-10922,0,10922,0,-10922,0,32767,0,-32767,0,32767,0,10922,0,10922,0,10922,0,32767,0,-10922,0,32767,0,32767,0,10922,0,32767,0,32767,0,10922,0,32767,0,-10922,0,-10922,0,-10922,0,10922,0,-32767,0,10922,0,32767,0,-10922,0,32767,0,10922,0,10922,0,10922,0,-10922,0,-32767,0,-10922,0,-10922,0,-32767,0,-10922,0,10922,0,-32767,0,10922,0,-10922,0,-10922,0,-10922,0];
-			var nora = [0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767,0,-32767];
-			var texa = [32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0,32767,32767,32767,0,0,0];
-			var inda = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53];
-			var raw: TMeshData = {
-				name: ".PlaneTiled",
-				vertex_arrays: [
-					{ attrib: "pos", values: i16(posa), data: "short4norm" },
-					{ attrib: "nor", values: i16(nora), data: "short2norm" },
-					{ attrib: "tex", values: i16(texa), data: "short2norm" }
-				],
-				index_arrays: [
-					{ values: u32(inda), material: 0 }
-				],
-				scale_pos: 1.5,
-				scale_tex: 1.0
-			};
-			new MeshData(raw, function(md: MeshData) {
-				var materials = cast(Scene.active.getChild(".Plane"), MeshObject).materials;
-				var o = Scene.active.addMeshObject(md, materials);
-				o.name = ".PlaneTiled";
-			});
-		}
-
-		planeo = cast Scene.active.getChild(tiled ? ".PlaneTiled" : ".Plane");
-		planeo.visible = true;
-		Context.raw.paintObject = planeo;
-
-		var v = new Vec4();
-		var sx = v.set(m._00, m._01, m._02).length();
-		planeo.transform.rot.fromEuler(-Math.PI / 2, 0, 0);
-		planeo.transform.scale.set(sx, 1.0, sx);
-		planeo.transform.scale.z *= Config.getTextureResY() / Config.getTextureResX();
-		planeo.transform.loc.set(m._30, -m._31, 0.0);
-		planeo.transform.buildMatrix();
-	}
-
-	public static function restorePlaneMesh() {
-		Context.raw.paint2dView = false;
-		planeo.visible = false;
-		planeo.transform.loc.set(0.0, 0.0, 0.0);
-		for (i in 0...Project.paintObjects.length) {
-			Project.paintObjects[i].visible = visibles[i];
-		}
-		if (Context.raw.mergedObject != null) {
-			Context.raw.mergedObject.visible = mergedObjectVisible;
-		}
-		Context.raw.paintObject = painto;
-		Scene.active.camera.transform.setMatrix(Context.raw.savedCamera);
-		Scene.active.camera.data.raw.fov = savedFov;
-		Viewport.updateCameraType(Context.raw.cameraType);
-		Scene.active.camera.buildProjection();
-		Scene.active.camera.buildMatrix();
-
-		RenderPathBase.drawGbuffer();
-	}
-
-	public static function bindLayers() {
-		#if is_paint
-		var isLive = Config.raw.brush_live && liveLayerDrawn > 0;
-		var isMaterialTool = Context.raw.tool == ToolMaterial;
-		if (isLive || isMaterialTool) useLiveLayer(true);
-		#end
-
-		for (i in 0...Project.layers.length) {
-			var l = Project.layers[i];
-			path.bindTarget("texpaint" + l.id, "texpaint" + l.id);
-
-			#if is_paint
-			if (l.isLayer()) {
-				path.bindTarget("texpaint_nor" + l.id, "texpaint_nor" + l.id);
-				path.bindTarget("texpaint_pack" + l.id, "texpaint_pack" + l.id);
-			}
-			#end
-		}
-	}
-
-	public static function unbindLayers() {
-		#if is_paint
-		var isLive = Config.raw.brush_live && liveLayerDrawn > 0;
-		var isMaterialTool = Context.raw.tool == ToolMaterial;
-		if (isLive || isMaterialTool) useLiveLayer(false);
-		#end
-	}
-
-	public static function dilate(base: Bool, nor_pack: Bool) {
-		#if is_paint
-		if (Config.raw.dilate_radius > 0 && !Context.raw.paint2d) {
-			UtilUV.cacheDilateMap();
-			Base.makeTempImg();
-			var tid = Context.raw.layer.id;
-			if (base) {
-				var texpaint = "texpaint";
-				path.setTarget("temptex0");
-				path.bindTarget(texpaint + tid, "tex");
-				path.drawShader("shader_datas/copy_pass/copy_pass");
-				path.setTarget(texpaint + tid);
-				path.bindTarget("temptex0", "tex");
-				path.drawShader("shader_datas/dilate_pass/dilate_pass");
-			}
-			if (nor_pack && !Context.raw.layer.isMask()) {
-				path.setTarget("temptex0");
-				path.bindTarget("texpaint_nor" + tid, "tex");
-				path.drawShader("shader_datas/copy_pass/copy_pass");
-				path.setTarget("texpaint_nor" + tid);
-				path.bindTarget("temptex0", "tex");
-				path.drawShader("shader_datas/dilate_pass/dilate_pass");
-
-				path.setTarget("temptex0");
-				path.bindTarget("texpaint_pack" + tid, "tex");
-				path.drawShader("shader_datas/copy_pass/copy_pass");
-				path.setTarget("texpaint_pack" + tid);
-				path.bindTarget("temptex0", "tex");
-				path.drawShader("shader_datas/dilate_pass/dilate_pass");
-			}
-		}
-		#end
-	}
-
-	static function u32(ar: Array<Int>): js.lib.Uint32Array {
-		var res = new js.lib.Uint32Array(ar.length);
-		for (i in 0...ar.length) res[i] = ar[i];
-		return res;
-	}
-
-	static function i16(ar: Array<Int>): js.lib.Int16Array {
-		var res = new js.lib.Int16Array(ar.length);
-		for (i in 0...ar.length) res[i] = ar[i];
-		return res;
-	}
-}

+ 0 - 170
armorpaint/Sources/arm/RenderPathPreview.hx

@@ -1,170 +0,0 @@
-package arm;
-
-import iron.RenderPath;
-
-class RenderPathPreview {
-
-	public static var path: RenderPath;
-
-	public static function init(_path: RenderPath) {
-		path = _path;
-
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpreview";
-			t.width = 1;
-			t.height = 1;
-			t.format = "RGBA32";
-			path.createRenderTarget(t);
-		}
-		{
-			var t = new RenderTargetRaw();
-			t.name = "texpreview_icon";
-			t.width = 1;
-			t.height = 1;
-			t.format = "RGBA32";
-			path.createRenderTarget(t);
-		}
-
-		path.createDepthBuffer("mmain", "DEPTH24");
-
-		{
-			var t = new RenderTargetRaw();
-			t.name = "mtex";
-			t.width = Std.int(UtilRender.materialPreviewSize * 2.0);
-			t.height = Std.int(UtilRender.materialPreviewSize * 2.0);
-			t.format = "RGBA64";
-			t.scale = RenderPathBase.getSuperSampling();
-			#if krom_opengl
-			t.depth_buffer = "mmain";
-			#end
-			path.createRenderTarget(t);
-		}
-
-		{
-			var t = new RenderTargetRaw();
-			t.name = "mgbuffer0";
-			t.width = Std.int(UtilRender.materialPreviewSize * 2.0);
-			t.height = Std.int(UtilRender.materialPreviewSize * 2.0);
-			t.format = "RGBA64";
-			t.scale = RenderPathBase.getSuperSampling();
-			t.depth_buffer = "mmain";
-			path.createRenderTarget(t);
-		}
-
-		{
-			var t = new RenderTargetRaw();
-			t.name = "mgbuffer1";
-			t.width = Std.int(UtilRender.materialPreviewSize * 2.0);
-			t.height = Std.int(UtilRender.materialPreviewSize * 2.0);
-			t.format = "RGBA64";
-			t.scale = RenderPathBase.getSuperSampling();
-			path.createRenderTarget(t);
-		}
-
-		{
-			var t = new RenderTargetRaw();
-			t.name = "mgbuffer2";
-			t.width = Std.int(UtilRender.materialPreviewSize * 2.0);
-			t.height = Std.int(UtilRender.materialPreviewSize * 2.0);
-			t.format = "RGBA64";
-			t.scale = RenderPathBase.getSuperSampling();
-			path.createRenderTarget(t);
-		}
-	}
-
-	public static function commandsPreview() {
-		path.setTarget("mgbuffer2");
-		path.clearTarget(0xff000000);
-
-		#if (krom_metal)
-		var clearColor = 0xffffffff;
-		#else
-		var clearColor: Null<Int> = null;
-		#end
-
-		path.setTarget("mgbuffer0");
-		path.clearTarget(clearColor, 1.0);
-		path.setTarget("mgbuffer0", ["mgbuffer1", "mgbuffer2"]);
-		path.drawMeshes("mesh");
-
-		// Deferred light
-		path.setTarget("mtex");
-		path.bindTarget("_mmain", "gbufferD");
-		path.bindTarget("mgbuffer0", "gbuffer0");
-		path.bindTarget("mgbuffer1", "gbuffer1");
-		{
-			path.bindTarget("empty_white", "ssaotex");
-		}
-		path.drawShader("shader_datas/deferred_light/deferred_light");
-
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
-		path.setDepthFrom("mtex", "mgbuffer0"); // Bind depth for world pass
-		#end
-
-		path.setTarget("mtex"); // Re-binds depth
-		path.drawSkydome("shader_datas/world_pass/world_pass");
-
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
-		path.setDepthFrom("mtex", "mgbuffer1"); // Unbind depth
-		#end
-
-		var framebuffer = "texpreview";
-		var selectedMat = Context.raw.material;
-		RenderPath.active.renderTargets.get("texpreview").image = selectedMat.image;
-		RenderPath.active.renderTargets.get("texpreview_icon").image = selectedMat.imageIcon;
-
-		path.setTarget(framebuffer);
-		path.bindTarget("mtex", "tex");
-		path.drawShader("shader_datas/compositor_pass/compositor_pass");
-
-		path.setTarget("texpreview_icon");
-		path.bindTarget("texpreview", "tex");
-		path.drawShader("shader_datas/supersample_resolve/supersample_resolve");
-	}
-
-	public static function commandsDecal() {
-		path.setTarget("gbuffer2");
-		path.clearTarget(0xff000000);
-
-		#if (krom_metal)
-		var clearColor = 0xffffffff;
-		#else
-		var clearColor: Null<Int> = null;
-		#end
-
-		path.setTarget("gbuffer0");
-		path.clearTarget(clearColor, 1.0);
-		path.setTarget("gbuffer0", ["gbuffer1", "gbuffer2"]);
-		path.drawMeshes("mesh");
-
-		// Deferred light
-		path.setTarget("tex");
-		path.bindTarget("_main", "gbufferD");
-		path.bindTarget("gbuffer0", "gbuffer0");
-		path.bindTarget("gbuffer1", "gbuffer1");
-		{
-			path.bindTarget("empty_white", "ssaotex");
-		}
-		path.drawShader("shader_datas/deferred_light/deferred_light");
-
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
-		path.setDepthFrom("tex", "gbuffer0"); // Bind depth for world pass
-		#end
-
-		path.setTarget("tex");
-		path.drawSkydome("shader_datas/world_pass/world_pass");
-
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
-		path.setDepthFrom("tex", "gbuffer1"); // Unbind depth
-		#end
-
-		var framebuffer = "texpreview";
-		RenderPath.active.renderTargets.get("texpreview").image = Context.raw.decalImage;
-
-		path.setTarget(framebuffer);
-
-		path.bindTarget("tex", "tex");
-		path.drawShader("shader_datas/compositor_pass/compositor_pass");
-	}
-}

+ 0 - 34
armorpaint/Sources/arm/SlotBrush.hx

@@ -1,34 +0,0 @@
-package arm;
-
-import zui.Zui.Nodes;
-import zui.Zui.TNodeCanvas;
-import iron.System;
-import iron.Data;
-import iron.ArmPack;
-
-class SlotBrush {
-	public var nodes = new Nodes();
-	public var canvas: TNodeCanvas;
-	public var image: Image = null; // 200px
-	public var imageIcon: Image = null; // 50px
-	public var previewReady = false;
-	public var id = 0;
-	static var defaultCanvas: js.lib.ArrayBuffer = null;
-
-	public function new(c: TNodeCanvas = null) {
-		for (brush in Project.brushes) if (brush.id >= id) id = brush.id + 1;
-
-		if (c == null) {
-			if (defaultCanvas == null) { // Synchronous
-				Data.getBlob("default_brush.arm", function(b: js.lib.ArrayBuffer) {
-					defaultCanvas = b;
-				});
-			}
-			canvas = ArmPack.decode(defaultCanvas);
-			canvas.name = "Brush " + (id + 1);
-		}
-		else {
-			canvas = c;
-		}
-	}
-}

+ 0 - 19
armorpaint/Sources/arm/SlotFont.hx

@@ -1,19 +0,0 @@
-package arm;
-
-import iron.System;
-
-class SlotFont {
-	public var image: Image = null; // 200px
-	public var previewReady = false;
-	public var id = 0;
-	public var font: Font;
-	public var name: String;
-	public var file: String;
-
-	public function new(name: String, font: Font, file = "") {
-		for (slot in Project.fonts) if (slot.id >= id) id = slot.id + 1;
-		this.name = name;
-		this.font = font;
-		this.file = file;
-	}
-}

+ 0 - 690
armorpaint/Sources/arm/SlotLayer.hx

@@ -1,690 +0,0 @@
-package arm;
-
-import iron.System;
-import iron.RenderPath;
-import iron.Mat4;
-
-class SlotLayer {
-	public var id = 0;
-	public var name: String;
-	public var ext = "";
-	public var visible = true;
-	public var parent: SlotLayer = null; // Group (for layers) or layer (for masks)
-
-	public var texpaint: Image = null; // Base or mask
-	#if is_paint
-	public var texpaint_nor: Image = null;
-	public var texpaint_pack: Image = null;
-	public var texpaint_preview: Image = null; // Layer preview
-	#end
-
-	public var maskOpacity = 1.0; // Opacity mask
-	public var fill_layer: SlotMaterial = null;
-	public var show_panel = true;
-	public var blending = BlendMix;
-	public var objectMask = 0;
-	public var scale = 1.0;
-	public var angle = 0.0;
-	public var uvType = UVMap;
-	public var paintBase = true;
-	public var paintOpac = true;
-	public var paintOcc = true;
-	public var paintRough = true;
-	public var paintMet = true;
-	public var paintNor = true;
-	public var paintNorBlend = true;
-	public var paintHeight = true;
-	public var paintHeightBlend = true;
-	public var paintEmis = true;
-	public var paintSubs = true;
-	public var decalMat = Mat4.identity(); // Decal layer
-
-	public function new(ext = "", type = SlotLayer, parent: SlotLayer = null) {
-		if (ext == "") {
-			id = 0;
-			for (l in Project.layers) if (l.id >= id) id = l.id + 1;
-			ext = id + "";
-		}
-		this.ext = ext;
-		this.parent = parent;
-
-		if (type == SlotGroup) {
-			name = "Group " + (id + 1);
-		}
-		else if (type == SlotLayer) {
-			name = "Layer " + (id + 1);
-			#if is_paint
-			var format = Base.bitsHandle.position == Bits8  ? "RGBA32" :
-						 Base.bitsHandle.position == Bits16 ? "RGBA64" :
-						 									  "RGBA128";
-			#end
-
-			#if is_sculpt
-			var format = "RGBA128";
-			#end
-
-			{
-				var t = new RenderTargetRaw();
-				t.name = "texpaint" + ext;
-				t.width = Config.getTextureResX();
-				t.height = Config.getTextureResY();
-				t.format = format;
-				texpaint = RenderPath.active.createRenderTarget(t).image;
-			}
-
-			#if is_paint
-			{
-				var t = new RenderTargetRaw();
-				t.name = "texpaint_nor" + ext;
-				t.width = Config.getTextureResX();
-				t.height = Config.getTextureResY();
-				t.format = format;
-				texpaint_nor = RenderPath.active.createRenderTarget(t).image;
-			}
-			{
-				var t = new RenderTargetRaw();
-				t.name = "texpaint_pack" + ext;
-				t.width = Config.getTextureResX();
-				t.height = Config.getTextureResY();
-				t.format = format;
-				texpaint_pack = RenderPath.active.createRenderTarget(t).image;
-			}
-
-			texpaint_preview = Image.createRenderTarget(UtilRender.layerPreviewSize, UtilRender.layerPreviewSize, TextureFormat.RGBA32);
-			#end
-		}
-
-		#if is_paint
-		else { // Mask
-			name = "Mask " + (id + 1);
-			var format = "RGBA32"; // Full bits for undo support, R8 is used
-			blending = BlendAdd;
-
-			{
-				var t = new RenderTargetRaw();
-				t.name = "texpaint" + ext;
-				t.width = Config.getTextureResX();
-				t.height = Config.getTextureResY();
-				t.format = format;
-				texpaint = RenderPath.active.createRenderTarget(t).image;
-			}
-
-			texpaint_preview = Image.createRenderTarget(UtilRender.layerPreviewSize, UtilRender.layerPreviewSize, TextureFormat.RGBA32);
-		}
-		#end
-	}
-
-	public function delete() {
-		unload();
-
-		if (isLayer()) {
-			var masks = getMasks(false); // Prevents deleting group masks
-			if (masks != null) for (m in masks) m.delete();
-		}
-		else if (isGroup()) {
-			var children = getChildren();
-			if (children != null) for (c in children) c.delete();
-			var masks = getMasks();
-			if (masks != null) for (m in masks) m.delete();
-		}
-
-		var lpos = Project.layers.indexOf(this);
-		Project.layers.remove(this);
-		// Undo can remove base layer and then restore it from undo layers
-		if (Project.layers.length > 0) {
-			Context.setLayer(Project.layers[lpos > 0 ? lpos - 1 : 0]);
-		}
-
-		// Do not remove empty groups if the last layer is deleted as this prevents redo from working properly
-	}
-
-	public function unload() {
-		if (isGroup()) return;
-
-		var _texpaint = texpaint;
-		#if is_paint
-		var _texpaint_nor = texpaint_nor;
-		var _texpaint_pack = texpaint_pack;
-		var _texpaint_preview = texpaint_preview;
-		#end
-
-		function _next() {
-			_texpaint.unload();
-			#if is_paint
-			if (_texpaint_nor != null) _texpaint_nor.unload();
-			if (_texpaint_pack != null) _texpaint_pack.unload();
-			_texpaint_preview.unload();
-			#end
-		}
-		Base.notifyOnNextFrame(_next);
-
-		RenderPath.active.renderTargets.remove("texpaint" + ext);
-		#if is_paint
-		if (isLayer()) {
-			RenderPath.active.renderTargets.remove("texpaint_nor" + ext);
-			RenderPath.active.renderTargets.remove("texpaint_pack" + ext);
-		}
-		#end
-	}
-
-	public function swap(other: SlotLayer) {
-		if ((isLayer() || isMask()) && (other.isLayer() || other.isMask())) {
-			RenderPath.active.renderTargets.get("texpaint" + ext).image = other.texpaint;
-			RenderPath.active.renderTargets.get("texpaint" + other.ext).image = texpaint;
-			var _texpaint = texpaint;
-			texpaint = other.texpaint;
-			other.texpaint = _texpaint;
-
-			#if is_paint
-			var _texpaint_preview = texpaint_preview;
-			texpaint_preview = other.texpaint_preview;
-			other.texpaint_preview = _texpaint_preview;
-			#end
-		}
-
-		#if is_paint
-		if (isLayer() && other.isLayer()) {
-			RenderPath.active.renderTargets.get("texpaint_nor" + ext).image = other.texpaint_nor;
-			RenderPath.active.renderTargets.get("texpaint_pack" + ext).image = other.texpaint_pack;
-			RenderPath.active.renderTargets.get("texpaint_nor" + other.ext).image = texpaint_nor;
-			RenderPath.active.renderTargets.get("texpaint_pack" + other.ext).image = texpaint_pack;
-			var _texpaint_nor = texpaint_nor;
-			var _texpaint_pack = texpaint_pack;
-			texpaint_nor = other.texpaint_nor;
-			texpaint_pack = other.texpaint_pack;
-			other.texpaint_nor = _texpaint_nor;
-			other.texpaint_pack = _texpaint_pack;
-		}
-		#end
-	}
-
-	public function clear(baseColor = 0x00000000, baseImage: Image = null, occlusion = 1.0, roughness = Base.defaultRough, metallic = 0.0) {
-		texpaint.g4.begin();
-		texpaint.g4.clear(baseColor); // Base
-		texpaint.g4.end();
-		if (baseImage != null) {
-			texpaint.g2.begin(false);
-			texpaint.g2.drawScaledImage(baseImage, 0, 0, texpaint.width, texpaint.height);
-			texpaint.g2.end();
-		}
-
-		#if is_paint
-		if (isLayer()) {
-			texpaint_nor.g4.begin();
-			texpaint_nor.g4.clear(Color.fromFloats(0.5, 0.5, 1.0, 0.0)); // Nor
-			texpaint_nor.g4.end();
-			texpaint_pack.g4.begin();
-			texpaint_pack.g4.clear(Color.fromFloats(occlusion, roughness, metallic, 0.0)); // Occ, rough, met
-			texpaint_pack.g4.end();
-		}
-		#end
-
-		Context.raw.layerPreviewDirty = true;
-		Context.raw.ddirty = 3;
-	}
-
-	public function invertMask() {
-		if (Base.pipeInvert8 == null) Base.makePipe();
-		var inverted = Image.createRenderTarget(texpaint.width, texpaint.height, TextureFormat.RGBA32);
-		inverted.g2.begin(false);
-		inverted.g2.pipeline = Base.pipeInvert8;
-		inverted.g2.drawImage(texpaint, 0, 0);
-		inverted.g2.pipeline = null;
-		inverted.g2.end();
-		var _texpaint = texpaint;
-		function _next() {
-			_texpaint.unload();
-		}
-		Base.notifyOnNextFrame(_next);
-		texpaint = RenderPath.active.renderTargets.get("texpaint" + id).image = inverted;
-		Context.raw.layerPreviewDirty = true;
-		Context.raw.ddirty = 3;
-	}
-
-	public function applyMask() {
-		if (parent.fill_layer != null) {
-			parent.toPaintLayer();
-		}
-		if (parent.isGroup()) {
-			for (c in parent.getChildren()) {
-				Base.applyMask(c, this);
-			}
-		}
-		else {
-			Base.applyMask(parent, this);
-		}
-		delete();
-	}
-
-	public function duplicate(): SlotLayer {
-		var layers = Project.layers;
-		var i = layers.indexOf(this) + 1;
-		var l = new SlotLayer("", isLayer() ? SlotLayer : isMask() ? SlotMask : SlotGroup, parent);
-		layers.insert(i, l);
-
-		if (Base.pipeMerge == null) Base.makePipe();
-		if (isLayer()) {
-			l.texpaint.g2.begin(false);
-			l.texpaint.g2.pipeline = Base.pipeCopy;
-			l.texpaint.g2.drawImage(texpaint, 0, 0);
-			l.texpaint.g2.pipeline = null;
-			l.texpaint.g2.end();
-			#if is_paint
-			l.texpaint_nor.g2.begin(false);
-			l.texpaint_nor.g2.pipeline = Base.pipeCopy;
-			l.texpaint_nor.g2.drawImage(texpaint_nor, 0, 0);
-			l.texpaint_nor.g2.pipeline = null;
-			l.texpaint_nor.g2.end();
-			l.texpaint_pack.g2.begin(false);
-			l.texpaint_pack.g2.pipeline = Base.pipeCopy;
-			l.texpaint_pack.g2.drawImage(texpaint_pack, 0, 0);
-			l.texpaint_pack.g2.pipeline = null;
-			l.texpaint_pack.g2.end();
-			#end
-		}
-		else if (isMask()) {
-			l.texpaint.g2.begin(false);
-			l.texpaint.g2.pipeline = Base.pipeCopy8;
-			l.texpaint.g2.drawImage(texpaint, 0, 0);
-			l.texpaint.g2.pipeline = null;
-			l.texpaint.g2.end();
-		}
-
-		#if is_paint
-		l.texpaint_preview.g2.begin(true, 0x00000000);
-		l.texpaint_preview.g2.pipeline = Base.pipeCopy;
-		l.texpaint_preview.g2.drawScaledImage(texpaint_preview, 0, 0, texpaint_preview.width, texpaint_preview.height);
-		l.texpaint_preview.g2.pipeline = null;
-		l.texpaint_preview.g2.end();
-		#end
-
-		l.visible = visible;
-		l.maskOpacity = maskOpacity;
-		l.fill_layer = fill_layer;
-		l.objectMask = objectMask;
-		l.blending = blending;
-		l.uvType = uvType;
-		l.scale = scale;
-		l.angle = angle;
-		l.paintBase = paintBase;
-		l.paintOpac = paintOpac;
-		l.paintOcc = paintOcc;
-		l.paintRough = paintRough;
-		l.paintMet = paintMet;
-		l.paintNor = paintNor;
-		l.paintNorBlend = paintNorBlend;
-		l.paintHeight = paintHeight;
-		l.paintHeightBlend = paintHeightBlend;
-		l.paintEmis = paintEmis;
-		l.paintSubs = paintSubs;
-
-		return l;
-	}
-
-	public function resizeAndSetBits() {
-		var resX = Config.getTextureResX();
-		var resY = Config.getTextureResY();
-		var rts = RenderPath.active.renderTargets;
-		if (Base.pipeMerge == null) Base.makePipe();
-
-		if (isLayer()) {
-			#if is_paint
-			var format = Base.bitsHandle.position == Bits8  ? TextureFormat.RGBA32 :
-						 Base.bitsHandle.position == Bits16 ? TextureFormat.RGBA64 :
-						 									  TextureFormat.RGBA128;
-			#end
-
-			#if is_sculpt
-			var format = TextureFormat.RGBA128;
-			#end
-
-			var _texpaint = this.texpaint;
-			this.texpaint = Image.createRenderTarget(resX, resY, format);
-			this.texpaint.g2.begin(false);
-			this.texpaint.g2.pipeline = Base.pipeCopy;
-			this.texpaint.g2.drawScaledImage(_texpaint, 0, 0, resX, resY);
-			this.texpaint.g2.pipeline = null;
-			this.texpaint.g2.end();
-
-			#if is_paint
-			var _texpaint_nor = this.texpaint_nor;
-			var _texpaint_pack = this.texpaint_pack;
-			this.texpaint_nor = Image.createRenderTarget(resX, resY, format);
-			this.texpaint_pack = Image.createRenderTarget(resX, resY, format);
-
-			this.texpaint_nor.g2.begin(false);
-			this.texpaint_nor.g2.pipeline = Base.pipeCopy;
-			this.texpaint_nor.g2.drawScaledImage(_texpaint_nor, 0, 0, resX, resY);
-			this.texpaint_nor.g2.pipeline = null;
-			this.texpaint_nor.g2.end();
-
-			this.texpaint_pack.g2.begin(false);
-			this.texpaint_pack.g2.pipeline = Base.pipeCopy;
-			this.texpaint_pack.g2.drawScaledImage(_texpaint_pack, 0, 0, resX, resY);
-			this.texpaint_pack.g2.pipeline = null;
-			this.texpaint_pack.g2.end();
-			#end
-
-			function _next() {
-				_texpaint.unload();
-				#if is_paint
-				_texpaint_nor.unload();
-				_texpaint_pack.unload();
-				#end
-			}
-			Base.notifyOnNextFrame(_next);
-
-			rts.get("texpaint" + this.ext).image = this.texpaint;
-			#if is_paint
-			rts.get("texpaint_nor" + this.ext).image = this.texpaint_nor;
-			rts.get("texpaint_pack" + this.ext).image = this.texpaint_pack;
-			#end
-		}
-		else if (isMask()) {
-			var _texpaint = this.texpaint;
-			this.texpaint = Image.createRenderTarget(resX, resY, TextureFormat.RGBA32);
-
-			this.texpaint.g2.begin(false);
-			this.texpaint.g2.pipeline = Base.pipeCopy8;
-			this.texpaint.g2.drawScaledImage(_texpaint, 0, 0, resX, resY);
-			this.texpaint.g2.pipeline = null;
-			this.texpaint.g2.end();
-
-			function _next() {
-				_texpaint.unload();
-			}
-			Base.notifyOnNextFrame(_next);
-
-			rts.get("texpaint" + this.ext).image = this.texpaint;
-		}
-	}
-
-	public function toFillLayer() {
-		Context.setLayer(this);
-		fill_layer = Context.raw.material;
-		Base.updateFillLayer();
-		function _next() {
-			MakeMaterial.parsePaintMaterial();
-			Context.raw.layerPreviewDirty = true;
-			UIBase.inst.hwnds[TabSidebar0].redraws = 2;
-		}
-		Base.notifyOnNextFrame(_next);
-	}
-
-	public function toPaintLayer() {
-		Context.setLayer(this);
-		fill_layer = null;
-		MakeMaterial.parsePaintMaterial();
-		Context.raw.layerPreviewDirty = true;
-		UIBase.inst.hwnds[TabSidebar0].redraws = 2;
-	}
-
-	public function isVisible(): Bool {
-		return visible && (parent == null || parent.visible);
-	}
-
-	public function getChildren(): Array<SlotLayer> {
-		var children: Array<SlotLayer> = null; // Child layers of a group
-		for (l in Project.layers) {
-			if (l.parent == this && l.isLayer()) {
-				if (children == null) children = [];
-				children.push(l);
-			}
-		}
-		return children;
-	}
-
-	public function getRecursiveChildren(): Array<SlotLayer> {
-		var children: Array<SlotLayer> = null;
-		for (l in Project.layers) {
-			if (l.parent == this) { // Child layers and group masks
-				if (children == null) children = [];
-				children.push(l);
-			}
-			if (l.parent != null && l.parent.parent == this) { // Layer masks
-				if (children == null) children = [];
-				children.push(l);
-			}
-		}
-		return children;
-	}
-
-	public function getMasks(includeGroupMasks = true): Array<SlotLayer> {
-		if (this.isMask()) return null;
-
-		var children: Array<SlotLayer> = null;
-		// Child masks of a layer
-		for (l in Project.layers) {
-			if (l.parent == this && l.isMask()) {
-				if (children == null) children = [];
-				children.push(l);
-			}
-		}
-		// Child masks of a parent group
-		if (includeGroupMasks) {
-			if (this.parent != null && this.parent.isGroup()) {
-				for (l in Project.layers) {
-					if (l.parent == this.parent && l.isMask()) {
-						if (children == null) children = [];
-						children.push(l);
-					}
-				}
-			}
-		}
-		return children;
-	}
-
-	public function hasMasks(includeGroupMasks = true): Bool {
-		// Layer mask
-		for (l in Project.layers) {
-			if (l.parent == this && l.isMask()) {
-				return true;
-			}
-		}
-		// Group mask
-		if (includeGroupMasks && this.parent != null && this.parent.isGroup()) {
-			for (l in Project.layers) {
-				if (l.parent == this.parent && l.isMask()) {
-					return true;
-				}
-			}
-		}
-		return false;
-	}
-
-	public function getOpacity(): Float {
-		var f = maskOpacity;
-		if (isLayer() && parent != null) f *= parent.maskOpacity;
-		return f;
-	}
-
-	public function getObjectMask(): Int {
-		return isMask() ? parent.objectMask : objectMask;
-	}
-
-	public function isLayer(): Bool {
-		#if is_paint
-		return texpaint != null && texpaint_nor != null;
-		#end
-		#if is_sculpt
-		return texpaint != null;
-		#end
-	}
-
-	public function isGroup(): Bool {
-		return texpaint == null;
-	}
-
-	public function getContainingGroup(): SlotLayer {
-		if (parent != null && parent.isGroup())
-			return parent;
-		else if (parent != null && parent.parent != null && parent.parent.isGroup())
-			return parent.parent;
-		else return null;
-	}
-
-	public function isMask(): Bool {
-		#if is_paint
-		return texpaint != null && texpaint_nor == null;
-		#end
-		#if is_sculpt
-		return false;
-		#end
-	}
-
-	public function isGroupMask(): Bool {
-		#if is_paint
-		return texpaint != null && texpaint_nor == null && parent.isGroup();
-		#end
-		#if is_sculpt
-		return false;
-		#end
-	}
-
-	public function isLayerMask(): Bool {
-		#if is_paint
-		return texpaint != null && texpaint_nor == null && parent.isLayer();
-		#end
-		#if is_sculpt
-		return false;
-		#end
-	}
-
-	public function isInGroup(): Bool {
-		return parent != null && (parent.isGroup() || (parent.parent != null && parent.parent.isGroup()));
-	}
-
-	public function canMove(to: Int): Bool {
-		var oldIndex = Project.layers.indexOf(this);
-
-		var delta = to - oldIndex; // If delta > 0 the layer is moved up, otherwise down
-		if (to < 0 || to > Project.layers.length - 1 || delta == 0) return false;
-
-		// If the layer is moved up, all layers between the old position and the new one move one down.
-		// The layers above the new position stay where they are.
-		// If the new position is on top or on bottom no upper resp. lower layer exists.
-		var newUpperLayer = delta > 0 ? (to < Project.layers.length - 1 ? Project.layers[to + 1] : null) : Project.layers[to];
-
-		// Group or layer is collapsed so we check below and update the upper layer.
-		if (newUpperLayer != null && !newUpperLayer.show_panel) {
-			var children = newUpperLayer.getRecursiveChildren();
-			to -= children != null ? children.length : 0;
-			delta = to - oldIndex;
-			newUpperLayer = delta > 0 ? (to < Project.layers.length - 1 ? Project.layers[to + 1] : null) : Project.layers[to];
-		}
-
-		var newLowerLayer = delta > 0 ? Project.layers[to] : (to > 0 ? Project.layers[to - 1] : null);
-
-		if (this.isMask()) {
-			// Masks can not be on top.
-			if (newUpperLayer == null) return false;
-			// Masks should not be placed below a collapsed group. This condition can be savely removed.
-			if (newUpperLayer.isInGroup() && !newUpperLayer.getContainingGroup().show_panel) return false;
-			// Masks should not be placed below a collapsed layer. This condition can be savely removed.
-			if (newUpperLayer.isMask() && !newUpperLayer.parent.show_panel) return false;
-		}
-
-		if (this.isLayer()) {
-			// Layers can not be moved directly below its own mask(s).
-			if (newUpperLayer != null && newUpperLayer.isMask() && newUpperLayer.parent == this) return false;
-			// Layers can not be placed above a mask as the mask would be reparented.
-			if (newLowerLayer != null && newLowerLayer.isMask()) return false;
-		}
-
-		// Currently groups can not be nested. Thus valid positions for groups are:
-		if (this.isGroup()) {
-			// At the top.
-			if (newUpperLayer == null) return true;
-			// NOT below its own children.
-			if (newUpperLayer.getContainingGroup() == this) return false;
-			// At the bottom.
-			if (newLowerLayer == null) return true;
-			// Above a group.
-			if (newLowerLayer.isGroup()) return true;
-			// Above a non-grouped layer.
-			if (newLowerLayer.isLayer() && !newLowerLayer.isInGroup()) return true;
-			else return false;
-		}
-
-		return true;
-	}
-
-	public function move(to: Int) {
-		if (!canMove(to)) {
-			return;
-		}
-
-		var pointers = TabLayers.initLayerMap();
-		var oldIndex = Project.layers.indexOf(this);
-		var delta = to - oldIndex;
-		var newUpperLayer = delta > 0 ? (to < Project.layers.length - 1 ? Project.layers[to + 1] : null) : Project.layers[to];
-
-		// Group or layer is collapsed so we check below and update the upper layer.
-		if (newUpperLayer != null && !newUpperLayer.show_panel) {
-			var children = newUpperLayer.getRecursiveChildren();
-			to -= children != null ? children.length : 0;
-			delta = to - oldIndex;
-			newUpperLayer = delta > 0 ? (to < Project.layers.length - 1 ? Project.layers[to + 1] : null) : Project.layers[to];
-		}
-
-		Context.setLayer(this);
-		History.orderLayers(to);
-		UIBase.inst.hwnds[TabSidebar0].redraws = 2;
-
-		Project.layers.remove(this);
-		Project.layers.insert(to, this);
-
-		if (this.isLayer()) {
-			var oldParent = this.parent;
-
-			if (newUpperLayer == null)
-				this.parent = null; // Placed on top.
-			else if (newUpperLayer.isInGroup() && !newUpperLayer.getContainingGroup().show_panel)
-				this.parent = null; // Placed below a collapsed group.
-			else if (newUpperLayer.isLayer())
-				this.parent = newUpperLayer.parent; // Placed below a layer, use the same parent.
-			else if (newUpperLayer.isGroup())
-				this.parent = newUpperLayer; // Placed as top layer in a group.
-			else if (newUpperLayer.isGroupMask())
-				this.parent = newUpperLayer.parent; // Placed in a group below the lowest group mask.
-			else if (newUpperLayer.isLayerMask())
-				this.parent = newUpperLayer.getContainingGroup(); // Either the group the mask belongs to or null.
-
-			// Layers can have masks as children. These have to be moved, too.
-			var layerMasks = this.getMasks(false);
-			if (layerMasks != null) {
-				for (idx in 0...layerMasks.length) {
-					var mask = layerMasks[idx];
-					Project.layers.remove(mask);
-					// If the masks are moved down each step increases the index below the layer by one.
-					Project.layers.insert(delta > 0 ? oldIndex + delta - 1 : oldIndex + delta + idx, mask);
-				}
-			}
-
-			// The layer is the last layer in the group, remove it. Notice that this might remove group masks.
-			if (oldParent != null && oldParent.getChildren() == null)
-				oldParent.delete();
-		}
-		else if (this.isMask()) {
-			// Precondition newUpperLayer != null, ensured in canMove.
-			if (newUpperLayer.isLayer() || newUpperLayer.isGroup())
-				this.parent = newUpperLayer;
-			else if (newUpperLayer.isMask()) { // Group mask or layer mask.
-				this.parent = newUpperLayer.parent;
-			}
-		}
-		else if (this.isGroup()) {
-			var children = this.getRecursiveChildren();
-			if (children != null) {
-				for (idx in 0...children.length) {
-					var child = children[idx];
-					Project.layers.remove(child);
-					// If the children are moved down each step increases the index below the layer by one.
-					Project.layers.insert(delta > 0 ? oldIndex + delta - 1 : oldIndex + delta + idx, child);
-				}
-			}
-		}
-
-		for (m in Project.materials) TabLayers.remapLayerPointers(m.canvas.nodes, TabLayers.fillLayerMap(pointers));
-	}
-}

+ 0 - 73
armorpaint/Sources/arm/SlotMaterial.hx

@@ -1,73 +0,0 @@
-package arm;
-
-import zui.Zui.Nodes;
-import zui.Zui.TNodeCanvas;
-import iron.System;
-import iron.MaterialData;
-import iron.Data;
-import iron.ArmPack;
-
-class SlotMaterial {
-	public var nodes = new Nodes();
-	public var canvas: TNodeCanvas;
-	public var image: Image = null;
-	public var imageIcon: Image = null;
-	public var previewReady = false;
-	public var data: MaterialData;
-	public var id = 0;
-	static var defaultCanvas: js.lib.ArrayBuffer = null;
-
-	public var paintBase = true;
-	public var paintOpac = true;
-	public var paintOcc = true;
-	public var paintRough = true;
-	public var paintMet = true;
-	public var paintNor = true;
-	public var paintHeight = true;
-	public var paintEmis = true;
-	public var paintSubs = true;
-
-	public function new(m: MaterialData = null, c: TNodeCanvas = null) {
-		for (mat in Project.materials) if (mat.id >= id) id = mat.id + 1;
-		data = m;
-
-		var w = UtilRender.materialPreviewSize;
-		var wIcon = 50;
-		image = Image.createRenderTarget(w, w);
-		imageIcon = Image.createRenderTarget(wIcon, wIcon);
-
-		if (c == null) {
-			if (defaultCanvas == null) { // Synchronous
-				Data.getBlob("default_material.arm", function(b: js.lib.ArrayBuffer) {
-					defaultCanvas = b;
-				});
-			}
-			canvas = ArmPack.decode(defaultCanvas);
-			canvas.name = "Material " + (id + 1);
-		}
-		else {
-			canvas = c;
-		}
-
-		#if (krom_android || krom_ios)
-		nodes.panX -= 50; // Center initial position
-		#end
-	}
-
-	public function unload() {
-		function _next() {
-			image.unload();
-			imageIcon.unload();
-		}
-		Base.notifyOnNextFrame(_next);
-	}
-
-	public function delete() {
-		unload();
-		var mpos = Project.materials.indexOf(this);
-		Project.materials.remove(this);
-		if (Project.materials.length > 0) {
-			Context.setMaterial(Project.materials[mpos > 0 ? mpos - 1 : 0]);
-		}
-	}
-}

+ 0 - 176
armorpaint/Sources/arm/nodes/InputNode.hx

@@ -1,176 +0,0 @@
-package arm.nodes;
-
-import iron.App;
-import iron.Input;
-import iron.Vec4;
-import zui.Zui.Nodes;
-import zui.Zui.TNode;
-import arm.LogicNode;
-import arm.Translator._tr;
-
-@:keep
-class InputNode extends LogicNode {
-
-	static var coords = new Vec4();
-
-	static var startX = 0.0;
-	static var startY = 0.0;
-
-	// Brush ruler
-	static var lockBegin = false;
-	static var lockX = false;
-	static var lockY = false;
-	static var lockStartX = 0.0;
-	static var lockStartY = 0.0;
-
-	static var registered = false;
-
-	public function new() {
-		super();
-
-		if (!registered) {
-			registered = true;
-			App.notifyOnUpdate(update);
-		}
-	}
-
-	function update() {
-		if (Context.raw.splitView) {
-			Context.raw.viewIndex = Input.getMouse().viewX > Base.w() / 2 ? 1 : 0;
-		}
-
-		var decal = Context.raw.tool == ToolDecal || Context.raw.tool == ToolText;
-		var decalMask = decal && Operator.shortcut(Config.keymap.decal_mask + "+" + Config.keymap.action_paint, ShortcutDown);
-
-		var lazyPaint = Context.raw.brushLazyRadius > 0 &&
-			(Operator.shortcut(Config.keymap.action_paint, ShortcutDown) ||
-			 Operator.shortcut(Config.keymap.brush_ruler + "+" + Config.keymap.action_paint, ShortcutDown) ||
-			 decalMask);
-
-		var mouse = Input.getMouse();
-		var paintX = mouse.viewX / App.w();
-		var paintY = mouse.viewY / App.h();
-		if (mouse.started()) {
-			startX = mouse.viewX / App.w();
-			startY = mouse.viewY / App.h();
-		}
-
-		var pen = Input.getPen();
-		if (pen.down()) {
-			paintX = pen.viewX / App.w();
-			paintY = pen.viewY / App.h();
-		}
-		if (pen.started()) {
-			startX = pen.viewX / App.w();
-			startY = pen.viewY / App.h();
-		}
-
-		if (Operator.shortcut(Config.keymap.brush_ruler + "+" + Config.keymap.action_paint, ShortcutDown)) {
-			if (lockX) paintX = startX;
-			if (lockY) paintY = startY;
-		}
-
-		if (Context.raw.brushLazyRadius > 0) {
-			Context.raw.brushLazyX = paintX;
-			Context.raw.brushLazyY = paintY;
-		}
-		if (!lazyPaint) {
-			coords.x = paintX;
-			coords.y = paintY;
-		}
-
-		if (Context.raw.splitView) {
-			Context.raw.viewIndex = -1;
-		}
-
-		if (lockBegin) {
-			var dx = Math.abs(lockStartX - mouse.viewX);
-			var dy = Math.abs(lockStartY - mouse.viewY);
-			if (dx > 1 || dy > 1) {
-				lockBegin = false;
-				dx > dy ? lockY = true : lockX = true;
-			}
-		}
-
-		var kb = Input.getKeyboard();
-		if (kb.started(Config.keymap.brush_ruler)) {
-			lockStartX = mouse.viewX;
-			lockStartY = mouse.viewY;
-			lockBegin = true;
-		}
-		else if (kb.released(Config.keymap.brush_ruler)) {
-			lockX = lockY = lockBegin = false;
-		}
-
-		if (Context.raw.brushLazyRadius > 0) {
-			var v1 = new Vec4(Context.raw.brushLazyX * App.w(), Context.raw.brushLazyY * App.h(), 0.0);
-			var v2 = new Vec4(coords.x * App.w(), coords.y * App.h(), 0.0);
-			var d = Vec4.distance(v1, v2);
-			var r = Context.raw.brushLazyRadius * 85;
-			if (d > r) {
-				var v3 = new Vec4();
-				v3.subvecs(v2, v1);
-				v3.normalize();
-				v3.mult(1.0 - Context.raw.brushLazyStep);
-				v3.mult(r);
-				v2.addvecs(v1, v3);
-				coords.x = v2.x / App.w();
-				coords.y = v2.y / App.h();
-				// Parse brush inputs once on next draw
-				Context.raw.painted = -1;
-			}
-			Context.raw.lastPaintX = -1;
-			Context.raw.lastPaintY = -1;
-		}
-
-		Context.raw.parseBrushInputs();
-	}
-
-	override function get(from: Int, done: Dynamic->Void) {
-		inputs[0].get(function(value) {
-			Context.raw.brushLazyRadius = value;
-			inputs[1].get(function(value) {
-				Context.raw.brushLazyStep = value;
-				done(coords);
-			});
-		});
-	}
-
-	public static var def: TNode = {
-		id: 0,
-		name: _tr("Input"),
-		type: "InputNode",
-		x: 0,
-		y: 0,
-		color: 0xff4982a0,
-		inputs: [
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Lazy Radius"),
-				type: "VALUE",
-				color: 0xffa1a1a1,
-				default_value: 0.0
-			},
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Lazy Step"),
-				type: "VALUE",
-				color: 0xffa1a1a1,
-				default_value: 0.0
-			}
-		],
-		outputs: [
-			{
-				id: 0,
-				node_id: 0,
-				name: _tr("Position"),
-				type: "VECTOR",
-				color: 0xff63c763,
-				default_value: null
-			}
-		],
-		buttons: []
-	};
-}

+ 0 - 5
armorpaint/Sources/import.hx

@@ -1,5 +0,0 @@
-// Global imports
-
-import arm.Translator.tr;
-import arm.Enums;
-using StringTools;

+ 47 - 52
armorpaint/Sources/arm/nodes/BrushOutputNode.hx → armorpaint/Sources/nodes/BrushOutputNode.ts

@@ -1,42 +1,34 @@
-package arm.nodes;
 
-import iron.Input;
-import arm.LogicNode;
-import arm.MakeMaterial;
-import arm.UIToolbar;
-import arm.UIBase;
-import arm.UIView2D;
-
-@:keep
+// @:keep
 class BrushOutputNode extends LogicNode {
 
-	public var Directional = false; // button 0
+	Directional = false; // button 0
 
-	public function new() {
+	constructor() {
 		super();
-		Context.raw.runBrush = run;
-		Context.raw.parseBrushInputs = parseInputs;
+		Context.raw.runBrush = this.run;
+		Context.raw.parseBrushInputs = this.parseInputs;
 	}
 
-	function parseInputs() {
-		var lastMask = Context.raw.brushMaskImage;
-		var lastStencil = Context.raw.brushStencilImage;
-
-		var input0: Dynamic;
-		var input1: Dynamic;
-		var input2: Dynamic;
-		var input3: Dynamic;
-		var input4: Dynamic;
-		var input5: Dynamic;
-		var input6: Dynamic;
+	parseInputs = () => {
+		let lastMask = Context.raw.brushMaskImage;
+		let lastStencil = Context.raw.brushStencilImage;
+
+		let input0: any;
+		let input1: any;
+		let input2: any;
+		let input3: any;
+		let input4: any;
+		let input5: any;
+		let input6: any;
 		try {
-			inputs[0].get(function(value) { input0 = value; });
-			inputs[1].get(function(value) { input1 = value; });
-			inputs[2].get(function(value) { input2 = value; });
-			inputs[3].get(function(value) { input3 = value; });
-			inputs[4].get(function(value) { input4 = value; });
-			inputs[5].get(function(value) { input5 = value; });
-			inputs[6].get(function(value) { input6 = value; });
+			this.inputs[0].get((value) => { input0 = value; });
+			this.inputs[1].get((value) => { input1 = value; });
+			this.inputs[2].get((value) => { input2 = value; });
+			this.inputs[3].get((value) => { input3 = value; });
+			this.inputs[4].get((value) => { input4 = value; });
+			this.inputs[5].get((value) => { input5 = value; });
+			this.inputs[6].get((value) => { input6 = value; });
 		}
 		catch (_) {
 			return;
@@ -47,14 +39,14 @@ class BrushOutputNode extends LogicNode {
 		Context.raw.brushNodesScale = input2;
 		Context.raw.brushNodesAngle = input3;
 
-		var opac: Dynamic = input4; // Float or texture name
+		let opac: any = input4; // Float or texture name
 		if (opac == null) opac = 1.0;
-		if (Std.isOfType(opac, String)) {
+		if (typeof opac == "string") {
 			Context.raw.brushMaskImageIsAlpha = opac.endsWith(".a");
 			opac = opac.substr(0, opac.lastIndexOf("."));
 			Context.raw.brushNodesOpacity = 1.0;
-			var index = Project.assetNames.indexOf(opac);
-			var asset = Project.assets[index];
+			let index = Project.assetNames.indexOf(opac);
+			let asset = Project.assets[index];
 			Context.raw.brushMaskImage = Project.getImage(asset);
 		}
 		else {
@@ -64,13 +56,13 @@ class BrushOutputNode extends LogicNode {
 
 		Context.raw.brushNodesHardness = input5;
 
-		var stencil: Dynamic = input6; // Float or texture name
+		let stencil: any = input6; // Float or texture name
 		if (stencil == null) stencil = 1.0;
-		if (Std.isOfType(stencil, String)) {
+		if (typeof stencil == "string") {
 			Context.raw.brushStencilImageIsAlpha = stencil.endsWith(".a");
 			stencil = stencil.substr(0, stencil.lastIndexOf("."));
-			var index = Project.assetNames.indexOf(stencil);
-			var asset = Project.assets[index];
+			let index = Project.assetNames.indexOf(stencil);
+			let asset = Project.assets[index];
 			Context.raw.brushStencilImage = Project.getImage(asset);
 		}
 		else {
@@ -82,12 +74,12 @@ class BrushOutputNode extends LogicNode {
 			MakeMaterial.parsePaintMaterial();
 		}
 
-		Context.raw.brushDirectional = Directional;
+		Context.raw.brushDirectional = this.Directional;
 	}
 
-	function run(from: Int) {
-		var left = 0.0;
-		var right = 1.0;
+	run = (from: i32) => {
+		let left = 0.0;
+		let right = 1.0;
 		if (Context.raw.paint2d) {
 			left = 1.0;
 			right = (Context.raw.splitView ? 2.0 : 1.0) + UIView2D.inst.ww / Base.w();
@@ -100,10 +92,10 @@ class BrushOutputNode extends LogicNode {
 		}
 
 		// Do not paint over fill layer
-		var fillLayer = Context.raw.layer.fill_layer != null && Context.raw.tool != ToolPicker && Context.raw.tool != ToolMaterial && Context.raw.tool != ToolColorId;
+		let fillLayer = Context.raw.layer.fill_layer != null && Context.raw.tool != WorkspaceTool.ToolPicker && Context.raw.tool != WorkspaceTool.ToolMaterial && Context.raw.tool != WorkspaceTool.ToolColorId;
 
 		// Do not paint over groups
-		var groupLayer = Context.raw.layer.isGroup();
+		let groupLayer = Context.raw.layer.isGroup();
 
 		// Paint bounds
 		if (Context.raw.paintVec.x > left &&
@@ -120,15 +112,15 @@ class BrushOutputNode extends LogicNode {
 			!Base.isComboSelected()) {
 
 			// Set color pick
-			var down = Input.getMouse().down() || Input.getPen().down();
-			if (down && Context.raw.tool == ToolColorId && Project.assets.length > 0) {
+			let down = Input.getMouse().down() || Input.getPen().down();
+			if (down && Context.raw.tool == WorkspaceTool.ToolColorId && Project.assets.length > 0) {
 				Context.raw.colorIdPicked = true;
 				UIToolbar.inst.toolbarHandle.redraws = 1;
 			}
 
 			// Prevent painting the same spot
-			var sameSpot = Context.raw.paintVec.x == Context.raw.lastPaintX && Context.raw.paintVec.y == Context.raw.lastPaintY;
-			var lazy = Context.raw.tool == ToolBrush && Context.raw.brushLazyRadius > 0;
+			let sameSpot = Context.raw.paintVec.x == Context.raw.lastPaintX && Context.raw.paintVec.y == Context.raw.lastPaintY;
+			let lazy = Context.raw.tool == WorkspaceTool.ToolBrush && Context.raw.brushLazyRadius > 0;
 			if (down && (sameSpot || lazy)) {
 				Context.raw.painted++;
 			}
@@ -138,12 +130,12 @@ class BrushOutputNode extends LogicNode {
 			Context.raw.lastPaintX = Context.raw.paintVec.x;
 			Context.raw.lastPaintY = Context.raw.paintVec.y;
 
-			if (Context.raw.tool == ToolParticle) {
+			if (Context.raw.tool == WorkspaceTool.ToolParticle) {
 				Context.raw.painted = 0; // Always paint particles
 			}
 
 			if (Context.raw.painted == 0) {
-				parseInputs();
+				this.parseInputs();
 			}
 
 			if (Context.raw.painted <= 1) {
@@ -153,7 +145,7 @@ class BrushOutputNode extends LogicNode {
 		}
 	}
 
-	// public static var def: TNode = {
+	// static def: TNode = {
 	// 	id: 0,
 	// 	name: _tr("Brush Output"),
 	// 	type: "BrushOutputNode",
@@ -229,3 +221,6 @@ class BrushOutputNode extends LogicNode {
 	// 	]
 	// };
 }
+
+
+Main.main();////

+ 167 - 0
armorpaint/Sources/nodes/InputNode.ts

@@ -0,0 +1,167 @@
+
+// @:keep
+class InputNode extends LogicNode {
+
+	static coords = new Vec4();
+
+	static startX = 0.0;
+	static startY = 0.0;
+
+	// Brush ruler
+	static lockBegin = false;
+	static lockX = false;
+	static lockY = false;
+	static lockStartX = 0.0;
+	static lockStartY = 0.0;
+
+	static registered = false;
+
+	constructor() {
+		super();
+
+		if (!InputNode.registered) {
+			InputNode.registered = true;
+			App.notifyOnUpdate(this.update);
+		}
+	}
+
+	update = () => {
+		if (Context.raw.splitView) {
+			Context.raw.viewIndex = Input.getMouse().viewX > Base.w() / 2 ? 1 : 0;
+		}
+
+		let decal = Context.raw.tool == WorkspaceTool.ToolDecal || Context.raw.tool == WorkspaceTool.ToolText;
+		let decalMask = decal && Operator.shortcut(Config.keymap.decal_mask + "+" + Config.keymap.action_paint, ShortcutType.ShortcutDown);
+
+		let lazyPaint = Context.raw.brushLazyRadius > 0 &&
+			(Operator.shortcut(Config.keymap.action_paint, ShortcutType.ShortcutDown) ||
+			 Operator.shortcut(Config.keymap.brush_ruler + "+" + Config.keymap.action_paint, ShortcutType.ShortcutDown) ||
+			 decalMask);
+
+		let mouse = Input.getMouse();
+		let paintX = mouse.viewX / App.w();
+		let paintY = mouse.viewY / App.h();
+		if (mouse.started()) {
+			InputNode.startX = mouse.viewX / App.w();
+			InputNode.startY = mouse.viewY / App.h();
+		}
+
+		let pen = Input.getPen();
+		if (pen.down()) {
+			paintX = pen.viewX / App.w();
+			paintY = pen.viewY / App.h();
+		}
+		if (pen.started()) {
+			InputNode.startX = pen.viewX / App.w();
+			InputNode.startY = pen.viewY / App.h();
+		}
+
+		if (Operator.shortcut(Config.keymap.brush_ruler + "+" + Config.keymap.action_paint, ShortcutType.ShortcutDown)) {
+			if (InputNode.lockX) paintX = InputNode.startX;
+			if (InputNode.lockY) paintY = InputNode.startY;
+		}
+
+		if (Context.raw.brushLazyRadius > 0) {
+			Context.raw.brushLazyX = paintX;
+			Context.raw.brushLazyY = paintY;
+		}
+		if (!lazyPaint) {
+			InputNode.coords.x = paintX;
+			InputNode.coords.y = paintY;
+		}
+
+		if (Context.raw.splitView) {
+			Context.raw.viewIndex = -1;
+		}
+
+		if (InputNode.lockBegin) {
+			let dx = Math.abs(InputNode.lockStartX - mouse.viewX);
+			let dy = Math.abs(InputNode.lockStartY - mouse.viewY);
+			if (dx > 1 || dy > 1) {
+				InputNode.lockBegin = false;
+				dx > dy ? InputNode.lockY = true : InputNode.lockX = true;
+			}
+		}
+
+		let kb = Input.getKeyboard();
+		if (kb.started(Config.keymap.brush_ruler)) {
+			InputNode.lockStartX = mouse.viewX;
+			InputNode.lockStartY = mouse.viewY;
+			InputNode.lockBegin = true;
+		}
+		else if (kb.released(Config.keymap.brush_ruler)) {
+			InputNode.lockX = InputNode.lockY = InputNode.lockBegin = false;
+		}
+
+		if (Context.raw.brushLazyRadius > 0) {
+			let v1 = new Vec4(Context.raw.brushLazyX * App.w(), Context.raw.brushLazyY * App.h(), 0.0);
+			let v2 = new Vec4(InputNode.coords.x * App.w(), InputNode.coords.y * App.h(), 0.0);
+			let d = Vec4.distance(v1, v2);
+			let r = Context.raw.brushLazyRadius * 85;
+			if (d > r) {
+				let v3 = new Vec4();
+				v3.subvecs(v2, v1);
+				v3.normalize();
+				v3.mult(1.0 - Context.raw.brushLazyStep);
+				v3.mult(r);
+				v2.addvecs(v1, v3);
+				InputNode.coords.x = v2.x / App.w();
+				InputNode.coords.y = v2.y / App.h();
+				// Parse brush inputs once on next draw
+				Context.raw.painted = -1;
+			}
+			Context.raw.lastPaintX = -1;
+			Context.raw.lastPaintY = -1;
+		}
+
+		Context.raw.parseBrushInputs();
+	}
+
+	override get = (from: i32, done: (a: any)=>void) => {
+		this.inputs[0].get((value) => {
+			Context.raw.brushLazyRadius = value;
+			this.inputs[1].get((value) => {
+				Context.raw.brushLazyStep = value;
+				done(InputNode.coords);
+			});
+		});
+	}
+
+	static def: TNode = {
+		id: 0,
+		name: _tr("Input"),
+		type: "InputNode",
+		x: 0,
+		y: 0,
+		color: 0xff4982a0,
+		inputs: [
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Lazy Radius"),
+				type: "VALUE",
+				color: 0xffa1a1a1,
+				default_value: 0.0
+			},
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Lazy Step"),
+				type: "VALUE",
+				color: 0xffa1a1a1,
+				default_value: 0.0
+			}
+		],
+		outputs: [
+			{
+				id: 0,
+				node_id: 0,
+				name: _tr("Position"),
+				type: "VECTOR",
+				color: 0xff63c763,
+				default_value: null
+			}
+		],
+		buttons: []
+	};
+}

+ 10 - 17
armorpaint/Sources/arm/nodes/TEX_IMAGE.hx → armorpaint/Sources/nodes/TEX_IMAGE.ts

@@ -1,27 +1,20 @@
-package arm.nodes;
 
-import zui.Zui.Nodes;
-import zui.Zui.TNode;
-import arm.LogicNode;
-import arm.ParserLogic.f32;
-import arm.Translator._tr;
-
-@:keep
+// @:keep
 class TEX_IMAGE extends LogicNode {
 
-	public var file: String;
-	public var color_space: String;
+	file: string;
+	color_space: string;
 
-	public function new() {
+	constructor() {
 		super();
 	}
 
-	override function get(from: Int, done: Dynamic->Void) {
-		if (from == 0) done(file + ".rgb");
-		else done(file + ".a");
+	override get = (from: i32, done: (a: any)=>void) => {
+		if (from == 0) done(this.file + ".rgb");
+		else done(this.file + ".a");
 	}
 
-	public static var def: TNode = {
+	static def: TNode = {
 		id: 0,
 		name: _tr("Image Texture"),
 		type: "TEX_IMAGE",
@@ -35,7 +28,7 @@ class TEX_IMAGE extends LogicNode {
 				name: _tr("Vector"),
 				type: "VECTOR",
 				color: 0xff6363c7,
-				default_value: f32([0.0, 0.0, 0.0])
+				default_value: array_f32([0.0, 0.0, 0.0])
 			}
 		],
 		outputs: [
@@ -45,7 +38,7 @@ class TEX_IMAGE extends LogicNode {
 				name: _tr("Color"),
 				type: "VALUE", // Match brush output socket type
 				color: 0xffc7c729,
-				default_value: f32([0.0, 0.0, 0.0, 1.0])
+				default_value: array_f32([0.0, 0.0, 0.0, 1.0])
 			},
 			{
 				id: 0,

+ 3 - 2
armorpaint/project.js

@@ -8,6 +8,7 @@ project.addDefine("is_paint");
 await project.addProject("../base");
 
 project.addSources("Sources");
+project.addSources("Sources/nodes");
 project.addShaders("Shaders/*.glsl", { embed: flags.snapshot });
 project.addAssets("Assets/*", { destination: "data/{name}", embed: flags.snapshot });
 project.addAssets("Assets/export_presets/*", { destination: "data/export_presets/{name}" });
@@ -32,8 +33,8 @@ else {
 }
 
 if (flags.physics) {
-	project.addDefine("arm_physics");
-	project.addAssets("Assets/plugins/wasm/ammo/*", { destination: "data/plugins/{name}" });
+	// project.addDefine("arm_physics");
+	// project.addAssets("Assets/plugins/wasm/ammo/*", { destination: "data/plugins/{name}" });
 }
 
 if (flags.raytrace) {

+ 98 - 0
armorsculpt/Sources/ExportObj.ts

@@ -0,0 +1,98 @@
+
+class ExportObj {
+
+	static writeString = (out: i32[], str: string) => {
+		for (let i = 0; i < str.length; ++i) {
+			out.push(str.charCodeAt(i));
+		}
+	}
+
+	static run = (path: string, paintObjects: MeshObject[], applyDisplacement = false) => {
+		let o: i32[] = [];
+		writeString(o, "# armorsculpt.org\n");
+
+		let texpaint = Project.layers[0].texpaint;
+		let pixels = texpaint.getPixels();
+		let pixelsView = new DataView(pixels);
+		let mesh = paintObjects[0].data.raw;
+		let inda = mesh.index_arrays[0].values;
+
+		let posa = new Int16Array(inda.length * 4);
+		for (let i = 0; i < inda.length; ++i) {
+			let index = inda[i];
+			posa[index * 4    ] = Math.floor(pixelsView.getFloat32(i * 16    , true) * 32767);
+			posa[index * 4 + 1] = Math.floor(pixelsView.getFloat32(i * 16 + 4, true) * 32767);
+			posa[index * 4 + 2] = Math.floor(pixelsView.getFloat32(i * 16 + 8, true) * 32767);
+		}
+
+		let poff = 0;
+		// for (let p of paintObjects) {
+			let p = paintObjects[0];
+			// let mesh = p.data.raw;
+			let inv = 1 / 32767;
+			let sc = p.data.scalePos * inv;
+			// let posa = mesh.vertex_arrays[0].values;
+			let len = Math.floor(posa.length / 4);
+			// let len = Math.floor(inda.length);
+
+			// Merge shared vertices and remap indices
+			let posa2 = new Int16Array(len * 3);
+			let posmap = new Map<i32, i32>();
+
+			let pi = 0;
+			for (let i = 0; i < len; ++i) {
+				let found = false;
+				for (let j = 0; j < pi; ++j) {
+					if (posa2[j * 3    ] == posa[i * 4    ] &&
+						posa2[j * 3 + 1] == posa[i * 4 + 1] &&
+						posa2[j * 3 + 2] == posa[i * 4 + 2]) {
+						posmap.set(i, j);
+						found = true;
+						break;
+					}
+				}
+				if (!found) {
+					posmap.set(i, pi);
+					posa2[pi * 3    ] = posa[i * 4    ];
+					posa2[pi * 3 + 1] = posa[i * 4 + 1];
+					posa2[pi * 3 + 2] = posa[i * 4 + 2];
+					pi++;
+				}
+			}
+
+			writeString(o, "o " + p.name + "\n");
+			for (let i = 0; i < pi; ++i) {
+				writeString(o, "v ");
+				let vx = posa2[i * 3] * sc + "";
+				writeString(o, vx.substr(0, vx.indexOf(".") + 7));
+				writeString(o, " ");
+				let vy = posa2[i * 3 + 2] * sc + "";
+				writeString(o, vy.substr(0, vy.indexOf(".") + 7));
+				writeString(o, " ");
+				let vz = -posa2[i * 3 + 1] * sc + "";
+				writeString(o, vz.substr(0, vz.indexOf(".") + 7));
+				writeString(o, "\n");
+			}
+
+			// let inda = mesh.index_arrays[0].values;
+			for (let i = 0; i < Math.floor(inda.length / 3); ++i) {
+				let pi1 = posmap.get(inda[i * 3    ]) + 1 + poff;
+				let pi2 = posmap.get(inda[i * 3 + 1]) + 1 + poff;
+				let pi3 = posmap.get(inda[i * 3 + 2]) + 1 + poff;
+				writeString(o, "f ");
+				writeString(o, pi1 + "");
+				writeString(o, " ");
+				writeString(o, pi2 + "");
+				writeString(o, " ");
+				writeString(o, pi3 + "");
+				writeString(o, "\n");
+			}
+			poff += pi;
+		// }
+
+		if (!path.endsWith(".obj")) path += ".obj";
+
+		let b = Uint8Array.from(o).buffer;
+		Krom.fileSaveBytes(path, b, b.byteLength);
+	}
+}

+ 38 - 46
armorsculpt/Sources/arm/ImportMesh.hx → armorsculpt/Sources/ImportMesh.ts

@@ -1,17 +1,9 @@
-package arm;
-
-import iron.SceneFormat;
-import iron.MeshData;
-import iron.Data;
-import iron.Scene;
-import iron.System;
-import iron.App;
 
 class ImportMesh {
 
-	static var clearLayers = true;
+	static clearLayers = true;
 
-	public static function run(path: String, _clearLayers = true, replaceExisting = true) {
+	static run = (path: string, _clearLayers = true, replaceExisting = true) => {
 		if (!Path.isMesh(path)) {
 			if (!Context.enableImportPlugin(path)) {
 				Console.error(Strings.error1());
@@ -22,26 +14,26 @@ class ImportMesh {
 		clearLayers = _clearLayers;
 		Context.raw.layerFilter = 0;
 
-		var p = path.toLowerCase();
+		let p = path.toLowerCase();
 		if (p.endsWith(".obj")) ImportObj.run(path, replaceExisting);
 		else if (p.endsWith(".fbx")) ImportFbx.run(path, replaceExisting);
 		else if (p.endsWith(".blend")) ImportBlendMesh.run(path, replaceExisting);
 		else {
-			var ext = path.substr(path.lastIndexOf(".") + 1);
-			var importer = Path.meshImporters.get(ext);
-			importer(path, function(mesh: Dynamic) {
+			let ext = path.substr(path.lastIndexOf(".") + 1);
+			let importer = Path.meshImporters.get(ext);
+			importer(path, (mesh: any) => {
 				replaceExisting ? makeMesh(mesh, path) : addMesh(mesh);
 			});
 		}
 
 		Project.meshAssets = [path];
 
-		#if (krom_android || krom_ios)
+		///if (krom_android || krom_ios)
 		System.title = path.substring(path.lastIndexOf(Path.sep) + 1, path.lastIndexOf("."));
-		#end
+		///end
 	}
 
-	static function finishImport() {
+	static finishImport = () => {
 		if (Context.raw.mergedObject != null) {
 			Context.raw.mergedObject.remove();
 			Data.deleteMesh(Context.raw.mergedObject.data.handle);
@@ -52,14 +44,14 @@ class ImportMesh {
 
 		if (Project.paintObjects.length > 1) {
 			// Sort by name
-			Project.paintObjects.sort(function(a, b): Int {
+			Project.paintObjects.sort((a, b): i32 => {
 				if (a.name < b.name) return -1;
 				else if (a.name > b.name) return 1;
 				return 0;
 			});
 
 			// No mask by default
-			for (p in Project.paintObjects) p.visible = true;
+			for (let p of Project.paintObjects) p.visible = true;
 			if (Context.raw.mergedObject == null) UtilMesh.mergeMesh();
 			Context.raw.paintObject.skip_context = "paint";
 			Context.raw.mergedObject.visible = true;
@@ -73,39 +65,39 @@ class ImportMesh {
 
 		UIView2D.inst.hwnd.redraws = 2;
 
-		#if arm_physics
+		///if arm_physics
 		Context.raw.paintBody = null;
-		#end
+		///end
 	}
 
-	public static function makeMesh(mesh: Dynamic, path: String) {
+	static makeMesh = (mesh: any, path: string) => {
 		if (mesh == null || mesh.posa == null || mesh.nora == null || mesh.inda == null || mesh.posa.length == 0) {
 			Console.error(Strings.error3());
 			return;
 		}
 
-		function _makeMesh() {
-			var raw = rawMesh(mesh);
+		let _makeMesh = () => {
+			let raw = rawMesh(mesh);
 			if (mesh.cola != null) raw.vertex_arrays.push({ values: mesh.cola, attrib: "col", data: "short4norm", padding: 1 });
 
-			new MeshData(raw, function(md: MeshData) {
+			new MeshData(raw, (md: MeshData) => {
 				Context.raw.paintObject = Context.mainObject();
 
 				Context.selectPaintObject(Context.mainObject());
-				for (i in 0...Project.paintObjects.length) {
-					var p = Project.paintObjects[i];
+				for (let i = 0; i < Project.paintObjects.length; ++i) {
+					let p = Project.paintObjects[i];
 					if (p == Context.raw.paintObject) continue;
 					Data.deleteMesh(p.data.handle);
 					p.remove();
 				}
-				var handle = Context.raw.paintObject.data.handle;
+				let handle = Context.raw.paintObject.data.handle;
 				if (handle != "SceneSphere" && handle != "ScenePlane") {
 					Data.deleteMesh(handle);
 				}
 
 				if (clearLayers) {
 					while (Project.layers.length > 0) {
-						var l = Project.layers.pop();
+						let l = Project.layers.pop();
 						l.unload();
 					}
 					Base.newLayer(false);
@@ -127,17 +119,17 @@ class ImportMesh {
 				// Wait for addMesh calls to finish
 				App.notifyOnInit(finishImport);
 
-				Base.notifyOnNextFrame(function() {
-					var f32 = new js.lib.Float32Array(Config.getTextureResX() * Config.getTextureResY() * 4);
-					for (i in 0...Std.int(mesh.inda.length)) {
-						var index = mesh.inda[i];
+				Base.notifyOnNextFrame(() => {
+					let f32 = new Float32Array(Config.getTextureResX() * Config.getTextureResY() * 4);
+					for (let i = 0; i < Math.floor(mesh.inda.length); ++i) {
+						let index = mesh.inda[i];
 						f32[i * 4]     = mesh.posa[index * 4]     / 32767;
 						f32[i * 4 + 1] = mesh.posa[index * 4 + 1] / 32767;
 						f32[i * 4 + 2] = mesh.posa[index * 4 + 2] / 32767;
 						f32[i * 4 + 3] = 1.0;
 					}
-					var imgmesh = Image.fromBytes(f32.buffer, Config.getTextureResX(), Config.getTextureResY(), TextureFormat.RGBA128);
-					var texpaint = Project.layers[0].texpaint;
+					let imgmesh = Image.fromBytes(f32.buffer, Config.getTextureResX(), Config.getTextureResY(), TextureFormat.RGBA128);
+					let texpaint = Project.layers[0].texpaint;
 					texpaint.g2.begin(false);
 					texpaint.g2.pipeline = Base.pipeCopy128;
 					texpaint.g2.drawScaledImage(imgmesh, 0, 0, Config.getTextureResX(), Config.getTextureResY());
@@ -150,20 +142,20 @@ class ImportMesh {
 		_makeMesh();
 	}
 
-	public static function addMesh(mesh: Dynamic) {
+	static addMesh = (mesh: any) => {
 
-		function _addMesh() {
-			var raw = rawMesh(mesh);
+		let _addMesh = () => {
+			let raw = rawMesh(mesh);
 			if (mesh.cola != null) raw.vertex_arrays.push({ values: mesh.cola, attrib: "col", data: "short4norm", padding: 1 });
 
-			new MeshData(raw, function(md: MeshData) {
+			new MeshData(raw, (md: MeshData) => {
 
-				var object = Scene.active.addMeshObject(md, Context.raw.paintObject.materials, Context.raw.paintObject);
+				let object = Scene.active.addMeshObject(md, Context.raw.paintObject.materials, Context.raw.paintObject);
 				object.name = mesh.name;
 				object.skip_context = "paint";
 
 				// Ensure unique names
-				for (p in Project.paintObjects) {
+				for (let p of Project.paintObjects) {
 					if (p.name == object.name) {
 						p.name += ".001";
 						p.data.handle += ".001";
@@ -184,11 +176,11 @@ class ImportMesh {
 		_addMesh();
 	}
 
-	public static function rawMesh(mesh: Dynamic): TMeshData {
-		var posa = new js.lib.Int16Array(Std.int(mesh.inda.length * 4));
-		for (i in 0...posa.length) posa[i] = 32767;
-		var inda = new js.lib.Uint32Array(mesh.inda.length);
-		for (i in 0...inda.length) inda[i] = i;
+	static rawMesh = (mesh: any): TMeshData => {
+		let posa = new Int16Array(Math.floor(mesh.inda.length * 4));
+		for (let i = 0; i < posa.length; ++i) posa[i] = 32767;
+		let inda = new Uint32Array(mesh.inda.length);
+		for (let i = 0; i < inda.length; ++i) inda[i] = i;
 		return {
 			name: mesh.name,
 			vertex_arrays: [

+ 7 - 8
armorsculpt/Sources/arm/MakeBrush.hx → armorsculpt/Sources/MakeBrush.ts

@@ -1,17 +1,16 @@
-package arm;
 
 class MakeBrush {
 
-	public static function run(vert: NodeShader, frag: NodeShader) {
+	static run = (vert: NodeShader, frag: NodeShader) => {
 
 		frag.write('float dist = 0.0;');
 
 		if (Config.raw.brush_3d) {
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('float depth = textureLod(gbufferD, inp.xy, 0.0).r;');
-			#else
+			///else
 			frag.write('float depth = textureLod(gbufferD, vec2(inp.x, 1.0 - inp.y), 0.0).r;');
-			#end
+			///end
 
 			frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix');
 			frag.write('vec4 winp = vec4(vec2(inp.x, 1.0 - inp.y) * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);');
@@ -22,11 +21,11 @@ class MakeBrush {
 
 			frag.write_attrib('vec3 wposition = mul(texelFetch(texpaint_undo, ivec2(texCoord.x * textureSize(texpaint_undo, 0).x, texCoord.y * textureSize(texpaint_undo, 0).y), 0), W).xyz;');
 
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('float depthlast = textureLod(gbufferD, inplast.xy, 0.0).r;');
-			#else
+			///else
 			frag.write('float depthlast = textureLod(gbufferD, vec2(inplast.x, 1.0 - inplast.y), 0.0).r;');
-			#end
+			///end
 
 			frag.write('vec4 winplast = vec4(vec2(inplast.x, 1.0 - inplast.y) * 2.0 - 1.0, depthlast * 2.0 - 1.0, 1.0);');
 			frag.write('winplast = mul(winplast, invVP);');

+ 91 - 99
armorsculpt/Sources/arm/MakeMaterial.hx → armorsculpt/Sources/MakeMaterial.ts

@@ -1,30 +1,22 @@
-package arm;
-
-import zui.Zui;
-import zui.Zui.Nodes;
-import iron.System;
-import iron.SceneFormat;
-import iron.ShaderData;
-import iron.MaterialData;
 
 class MakeMaterial {
 
-	public static var defaultScon: ShaderContext = null;
-	public static var defaultMcon: MaterialContext = null;
+	static defaultScon: ShaderContext = null;
+	static defaultMcon: MaterialContext = null;
 
-	public static var heightUsed = false;
-	public static var emisUsed = false;
-	public static var subsUsed = false;
+	static heightUsed = false;
+	static emisUsed = false;
+	static subsUsed = false;
 
-	static function getMOut(): Bool {
-		for (n in UINodes.inst.getCanvasMaterial().nodes) if (n.type == "OUTPUT_MATERIAL_PBR") return true;
+	static getMOut = (): bool => {
+		for (let n of UINodes.inst.getCanvasMaterial().nodes) if (n.type == "OUTPUT_MATERIAL_PBR") return true;
 		return false;
 	}
 
-	public static function parseMeshMaterial() {
-		var m = Project.materials[0].data;
+	static parseMeshMaterial = () => {
+		let m = Project.materials[0].data;
 
-		for (c in m.shader.contexts) {
+		for (let c of m.shader.contexts) {
 			if (c.raw.name == "mesh") {
 				m.shader.raw.contexts.remove(c.raw);
 				m.shader.contexts.remove(c);
@@ -34,10 +26,10 @@ class MakeMaterial {
 		}
 
 		if (MakeMesh.layerPassCount > 1) {
-			var i = 0;
+			let i = 0;
 			while (i < m.shader.contexts.length) {
-				var c = m.shader.contexts[i];
-				for (j in 1...MakeMesh.layerPassCount) {
+				let c = m.shader.contexts[i];
+				for (let j = 1; j < MakeMesh.layerPassCount; ++j) {
 					if (c.raw.name == "mesh" + j) {
 						m.shader.raw.contexts.remove(c.raw);
 						m.shader.contexts.remove(c);
@@ -51,8 +43,8 @@ class MakeMaterial {
 
 			i = 0;
 			while (i < m.contexts.length) {
-				var c = m.contexts[i];
-				for (j in 1...MakeMesh.layerPassCount) {
+				let c = m.contexts[i];
+				for (let j = 1; j < MakeMesh.layerPassCount; ++j) {
 					if (c.raw.name == "mesh" + j) {
 						m.raw.contexts.remove(c.raw);
 						m.contexts.remove(c);
@@ -64,11 +56,11 @@ class MakeMaterial {
 			}
 		}
 
-		var con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }));
-		var scon = new ShaderContext(con.data, function(scon: ShaderContext){});
+		let con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }));
+		let scon = new ShaderContext(con.data, (scon: ShaderContext) => {});
 		scon.overrideContext = {};
 		if (con.frag.sharedSamplers.length > 0) {
-			var sampler = con.frag.sharedSamplers[0];
+			let sampler = con.frag.sharedSamplers[0];
 			scon.overrideContext.shared_sampler = sampler.substr(sampler.lastIndexOf(" ") + 1);
 		}
 		if (!Context.raw.textureFilter) {
@@ -77,12 +69,12 @@ class MakeMaterial {
 		m.shader.raw.contexts.push(scon.raw);
 		m.shader.contexts.push(scon);
 
-		for (i in 1...MakeMesh.layerPassCount) {
-			var con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }), i);
-			var scon = new ShaderContext(con.data, function(scon: ShaderContext){});
+		for (let i = 1; i < MakeMesh.layerPassCount; ++i) {
+			let con = MakeMesh.run(new NodeShaderData({ name: "Material", canvas: null }), i);
+			let scon = new ShaderContext(con.data, (scon: ShaderContext) => {});
 			scon.overrideContext = {};
 			if (con.frag.sharedSamplers.length > 0) {
-				var sampler = con.frag.sharedSamplers[0];
+				let sampler = con.frag.sharedSamplers[0];
 				scon.overrideContext.shared_sampler = sampler.substr(sampler.lastIndexOf(" ") + 1);
 			}
 			if (!Context.raw.textureFilter) {
@@ -91,22 +83,22 @@ class MakeMaterial {
 			m.shader.raw.contexts.push(scon.raw);
 			m.shader.contexts.push(scon);
 
-			var mcon = new MaterialContext({ name: "mesh" + i, bind_textures: [] }, function(self: MaterialContext) {});
+			let mcon = new MaterialContext({ name: "mesh" + i, bind_textures: [] }, (self: MaterialContext) => {});
 			m.raw.contexts.push(mcon.raw);
 			m.contexts.push(mcon);
 		}
 
 		Context.raw.ddirty = 2;
 
-		#if arm_voxels
+		///if arm_voxels
 		makeVoxel(m);
-		#end
+		///end
 	}
 
-	public static function parseParticleMaterial() {
-		var m = Context.raw.particleMaterial;
-		var sc: ShaderContext = null;
-		for (c in m.shader.contexts) {
+	static parseParticleMaterial = () => {
+		let m = Context.raw.particleMaterial;
+		let sc: ShaderContext = null;
+		for (let c of m.shader.contexts) {
 			if (c.raw.name == "mesh") {
 				sc = c;
 				break;
@@ -116,19 +108,19 @@ class MakeMaterial {
 			m.shader.raw.contexts.remove(sc.raw);
 			m.shader.contexts.remove(sc);
 		}
-		var con = MakeParticle.run(new NodeShaderData({ name: "MaterialParticle", canvas: null }));
+		let con = MakeParticle.run(new NodeShaderData({ name: "MaterialParticle", canvas: null }));
 		if (sc != null) deleteContext(sc);
-		sc = new ShaderContext(con.data, function(sc: ShaderContext){});
+		sc = new ShaderContext(con.data, (sc: ShaderContext) => {});
 		m.shader.raw.contexts.push(sc.raw);
 		m.shader.contexts.push(sc);
 	}
 
-	public static function parseMeshPreviewMaterial() {
+	static parseMeshPreviewMaterial = () => {
 		if (!getMOut()) return;
 
-		var m = Project.materials[0].data;
-		var scon: ShaderContext = null;
-		for (c in m.shader.contexts) {
+		let m = Project.materials[0].data;
+		let scon: ShaderContext = null;
+		for (let c of m.shader.contexts) {
 			if (c.raw.name == "mesh") {
 				scon = c;
 				break;
@@ -137,22 +129,22 @@ class MakeMaterial {
 		m.shader.raw.contexts.remove(scon.raw);
 		m.shader.contexts.remove(scon);
 
-		var mcon: TMaterialContext = { name: "mesh", bind_textures: [] };
+		let mcon: TMaterialContext = { name: "mesh", bind_textures: [] };
 
-		var sd = new NodeShaderData({ name: "Material", canvas: null });
-		var con = MakeMeshPreview.run(sd, mcon);
+		let sd = new NodeShaderData({ name: "Material", canvas: null });
+		let con = MakeMeshPreview.run(sd, mcon);
 
-		for (i in 0...m.contexts.length) {
+		for (let i = 0; i < m.contexts.length; ++i) {
 			if (m.contexts[i].raw.name == "mesh") {
-				m.contexts[i] = new MaterialContext(mcon, function(self: MaterialContext) {});
+				m.contexts[i] = new MaterialContext(mcon, (self: MaterialContext) => {});
 				break;
 			}
 		}
 
 		if (scon != null) deleteContext(scon);
 
-		var compileError = false;
-		scon = new ShaderContext(con.data, function(scon: ShaderContext) {
+		let compileError = false;
+		scon = new ShaderContext(con.data, (scon: ShaderContext) => {
 			if (scon == null) compileError = true;
 		});
 		if (compileError) return;
@@ -161,12 +153,12 @@ class MakeMaterial {
 		m.shader.contexts.push(scon);
 	}
 
-	#if arm_voxels
-	static function makeVoxel(m: MaterialData) {
-		var rebuild = heightUsed;
+	///if arm_voxels
+	static makeVoxel = (m: MaterialData) => {
+		let rebuild = heightUsed;
 		if (Config.raw.rp_gi != false && rebuild) {
-			var scon: ShaderContext = null;
-			for (c in m.shader.contexts) {
+			let scon: ShaderContext = null;
+			for (let c of m.shader.contexts) {
 				if (c.raw.name == "voxel") {
 					scon = c;
 					break;
@@ -175,22 +167,22 @@ class MakeMaterial {
 			if (scon != null) MakeVoxel.run(scon);
 		}
 	}
-	#end
+	///end
 
-	public static function parsePaintMaterial(bakePreviews = true) {
+	static parsePaintMaterial = (bakePreviews = true) => {
 		if (!getMOut()) return;
 
 		if (bakePreviews) {
-			var current = Graphics2.current;
+			let current = Graphics2.current;
 			if (current != null) current.end();
 			bakeNodePreviews();
 			if (current != null) current.begin(false);
 		}
 
-		var m = Project.materials[0].data;
-		var scon: ShaderContext = null;
-		var mcon: MaterialContext = null;
-		for (c in m.shader.contexts) {
+		let m = Project.materials[0].data;
+		let scon: ShaderContext = null;
+		let mcon: MaterialContext = null;
+		for (let c of m.shader.contexts) {
 			if (c.raw.name == "paint") {
 				m.shader.raw.contexts.remove(c.raw);
 				m.shader.contexts.remove(c);
@@ -198,7 +190,7 @@ class MakeMaterial {
 				break;
 			}
 		}
-		for (c in m.contexts) {
+		for (let c of m.contexts) {
 			if (c.raw.name == "paint") {
 				m.raw.contexts.remove(c.raw);
 				m.contexts.remove(c);
@@ -206,18 +198,18 @@ class MakeMaterial {
 			}
 		}
 
-		var sdata = new NodeShaderData({ name: "Material", canvas: UINodes.inst.getCanvasMaterial() });
-		var mcon: TMaterialContext = { name: "paint", bind_textures: [] };
-		var con = MakeSculpt.run(sdata, mcon);
+		let sdata = new NodeShaderData({ name: "Material", canvas: UINodes.inst.getCanvasMaterial() });
+		let mcon: TMaterialContext = { name: "paint", bind_textures: [] };
+		let con = MakeSculpt.run(sdata, mcon);
 
-		var compileError = false;
-		var scon = new ShaderContext(con.data, function(scon: ShaderContext) {
+		let compileError = false;
+		let scon = new ShaderContext(con.data, (scon: ShaderContext) => {
 			if (scon == null) compileError = true;
 		});
 		if (compileError) return;
 		scon.overrideContext = {};
 		scon.overrideContext.addressing = "repeat";
-		var mcon = new MaterialContext(mcon, function(mcon: MaterialContext) {});
+		let mcon = new MaterialContext(mcon, (mcon: MaterialContext) => {});
 
 		m.shader.raw.contexts.push(scon.raw);
 		m.shader.contexts.push(scon);
@@ -228,24 +220,24 @@ class MakeMaterial {
 		if (defaultMcon == null) defaultMcon = mcon;
 	}
 
-	static function bakeNodePreviews() {
+	static bakeNodePreviews = () => {
 		Context.raw.nodePreviewsUsed = [];
 		if (Context.raw.nodePreviews == null) Context.raw.nodePreviews = [];
 		traverseNodes(UINodes.inst.getCanvasMaterial().nodes, null, []);
-		for (key in Context.raw.nodePreviews.keys()) {
+		for (let key of Context.raw.nodePreviews.keys()) {
 			if (Context.raw.nodePreviewsUsed.indexOf(key) == -1) {
-				var image = Context.raw.nodePreviews.get(key);
+				let image = Context.raw.nodePreviews.get(key);
 				Base.notifyOnNextFrame(image.unload);
 				Context.raw.nodePreviews.remove(key);
 			}
 		}
 	}
 
-	static function traverseNodes(nodes: Array<TNode>, group: TNodeCanvas, parents: Array<TNode>) {
-		for (node in nodes) {
+	static traverseNodes = (nodes: TNode[], group: TNodeCanvas, parents: TNode[]) => {
+		for (let node of nodes) {
 			bakeNodePreview(node, group, parents);
 			if (node.type == "GROUP") {
-				for (g in Project.materialGroups) {
+				for (let g of Project.materialGroups) {
 					if (g.canvas.name == node.name) {
 						parents.push(node);
 						traverseNodes(g.canvas.nodes, g.canvas, parents);
@@ -257,13 +249,13 @@ class MakeMaterial {
 		}
 	}
 
-	static function bakeNodePreview(node: TNode, group: TNodeCanvas, parents: Array<TNode>) {
+	static bakeNodePreview = (node: TNode, group: TNodeCanvas, parents: TNode[]) => {
 		if (node.type == "BLUR") {
-			var id = ParserMaterial.node_name(node, parents);
-			var image = Context.raw.nodePreviews.get(id);
+			let id = ParserMaterial.node_name(node, parents);
+			let image = Context.raw.nodePreviews.get(id);
 			Context.raw.nodePreviewsUsed.push(id);
-			var resX = Std.int(Config.getTextureResX() / 4);
-			var resY = Std.int(Config.getTextureResY() / 4);
+			let resX = Math.floor(Config.getTextureResX() / 4);
+			let resY = Math.floor(Config.getTextureResY() / 4);
 			if (image == null || image.width != resX || image.height != resY) {
 				if (image != null) image.unload();
 				image = Image.createRenderTarget(resX, resY);
@@ -275,11 +267,11 @@ class MakeMaterial {
 			ParserMaterial.blur_passthrough = false;
 		}
 		else if (node.type == "DIRECT_WARP") {
-			var id = ParserMaterial.node_name(node, parents);
-			var image = Context.raw.nodePreviews.get(id);
+			let id = ParserMaterial.node_name(node, parents);
+			let image = Context.raw.nodePreviews.get(id);
 			Context.raw.nodePreviewsUsed.push(id);
-			var resX = Std.int(Config.getTextureResX());
-			var resY = Std.int(Config.getTextureResY());
+			let resX = Math.floor(Config.getTextureResX());
+			let resY = Math.floor(Config.getTextureResY());
 			if (image == null || image.width != resX || image.height != resY) {
 				if (image != null) image.unload();
 				image = Image.createRenderTarget(resX, resY);
@@ -292,36 +284,36 @@ class MakeMaterial {
 		}
 	}
 
-	public static function parseNodePreviewMaterial(node: TNode, group: TNodeCanvas = null, parents: Array<TNode> = null): { scon: ShaderContext, mcon: MaterialContext } {
+	static parseNodePreviewMaterial = (node: TNode, group: TNodeCanvas = null, parents: TNode[] = null): { scon: ShaderContext, mcon: MaterialContext } => {
 		if (node.outputs.length == 0) return null;
-		var sdata = new NodeShaderData({ name: "Material", canvas: UINodes.inst.getCanvasMaterial() });
-		var mcon_raw: TMaterialContext = { name: "mesh", bind_textures: [] };
-		var con = MakeNodePreview.run(sdata, mcon_raw, node, group, parents);
-		var compileError = false;
-		var scon = new ShaderContext(con.data, function(scon: ShaderContext) {
+		let sdata = new NodeShaderData({ name: "Material", canvas: UINodes.inst.getCanvasMaterial() });
+		let mcon_raw: TMaterialContext = { name: "mesh", bind_textures: [] };
+		let con = MakeNodePreview.run(sdata, mcon_raw, node, group, parents);
+		let compileError = false;
+		let scon = new ShaderContext(con.data, (scon: ShaderContext) => {
 			if (scon == null) compileError = true;
 		});
 		if (compileError) return null;
-		var mcon = new MaterialContext(mcon_raw, function(mcon: MaterialContext) {});
+		let mcon = new MaterialContext(mcon_raw, (mcon: MaterialContext) => {});
 		return { scon: scon, mcon: mcon };
 	}
 
-	public static function parseBrush() {
+	static parseBrush = () => {
 		ParserLogic.parse(Context.raw.brush.canvas);
 	}
 
-	public static inline function getDisplaceStrength():Float {
-		var sc = Context.mainObject().transform.scale.x;
+	static getDisplaceStrength = (): f32 => {
+		let sc = Context.mainObject().transform.scale.x;
 		return Config.raw.displace_strength * 0.02 * sc;
 	}
 
-	public static inline function voxelgiHalfExtents():String {
-		var ext = Context.raw.vxaoExt;
-		return 'const vec3 voxelgiHalfExtents = vec3($ext, $ext, $ext);';
+	static voxelgiHalfExtents = (): string => {
+		let ext = Context.raw.vxaoExt;
+		return `const vec3 voxelgiHalfExtents = vec3(${ext}, ${ext}, ${ext});`;
 	}
 
-	static function deleteContext(c: ShaderContext) {
-		Base.notifyOnNextFrame(function() { // Ensure pipeline is no longer in use
+	static deleteContext = (c: ShaderContext) => {
+		Base.notifyOnNextFrame(() => { // Ensure pipeline is no longer in use
 			c.delete();
 		});
 	}

+ 38 - 39
armorsculpt/Sources/arm/MakeMesh.hx → armorsculpt/Sources/MakeMesh.ts

@@ -1,12 +1,11 @@
-package arm;
 
 class MakeMesh {
 
-	public static var layerPassCount = 1;
+	static layerPassCount = 1;
 
-	public static function run(data: NodeShaderData, layerPass = 0): NodeShaderContext {
-		var context_id = layerPass == 0 ? "mesh" : "mesh" + layerPass;
-		var con_mesh: NodeShaderContext = data.add_context({
+	static run = (data: NodeShaderData, layerPass = 0): NodeShaderContext => {
+		let context_id = layerPass == 0 ? "mesh" : "mesh" + layerPass;
+		let con_mesh: NodeShaderContext = data.add_context({
 			name: context_id,
 			depth_write: layerPass == 0 ? true : false,
 			compare_mode: layerPass == 0 ? "less" : "equal",
@@ -16,8 +15,8 @@ class MakeMesh {
 			depth_attachment: "DEPTH32"
 		});
 
-		var vert = con_mesh.make_vert();
-		var frag = con_mesh.make_frag();
+		let vert = con_mesh.make_vert();
+		let frag = con_mesh.make_frag();
 		frag.ins = vert.outs;
 
 		vert.add_out('vec2 texCoord');
@@ -27,7 +26,7 @@ class MakeMesh {
 		vert.add_uniform('mat4 prevWVP', '_prevWorldViewProjectionMatrix');
 		vert.wposition = true;
 
-		var textureCount = 0;
+		let textureCount = 0;
 
 		vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix');
 		vert.add_uniform('sampler2D texpaint_vert', '_texpaint_vert' + Project.layers[0].id);
@@ -62,9 +61,9 @@ class MakeMesh {
 			frag.add_uniform('sampler2D gbuffer1');
 			frag.add_uniform('sampler2D gbuffer2');
 			frag.write('vec2 fragcoord = (wvpposition.xy / wvpposition.w) * 0.5 + 0.5;');
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('fragcoord.y = 1.0 - fragcoord.y;');
-			#end
+			///end
 			frag.write('vec4 gbuffer0_sample = textureLod(gbuffer0, fragcoord, 0.0);');
 			frag.write('vec4 gbuffer1_sample = textureLod(gbuffer1, fragcoord, 0.0);');
 			frag.write('vec4 gbuffer2_sample = textureLod(gbuffer2, fragcoord, 0.0);');
@@ -114,13 +113,13 @@ class MakeMesh {
 
 		// Get layers for this pass
 		layerPassCount = 1;
-		var layers: Array<SlotLayer> = [];
-		var startCount = textureCount;
-		for (l in Project.layers) {
+		let layers: SlotLayer[] = [];
+		let startCount = textureCount;
+		for (let l of Project.layers) {
 			if (!l.isLayer() || !l.isVisible()) continue;
 
-			var count = 3;
-			var masks = l.getMasks();
+			let count = 3;
+			let masks = l.getMasks();
 			if (masks != null) count += masks.length;
 			textureCount += count;
 			if (textureCount >= getMaxTextures()) {
@@ -132,23 +131,23 @@ class MakeMesh {
 			}
 		}
 
-		var lastPass = layerPass == layerPassCount - 1;
+		let lastPass = layerPass == layerPassCount - 1;
 
-		for (l in layers) {
+		for (let l of layers) {
 			if (l.getObjectMask() > 0) {
 				frag.add_uniform('int uid', '_uid');
 				if (l.getObjectMask() > Project.paintObjects.length) { // Atlas
-					var visibles = Project.getAtlasObjects(l.getObjectMask());
+					let visibles = Project.getAtlasObjects(l.getObjectMask());
 					frag.write('if (');
-					for (i in 0...visibles.length) {
+					for (let i = 0; i < visibles.length; ++i) {
 						if (i > 0) frag.write(' || ');
-						frag.write('${visibles[i].uid} == uid');
+						frag.write(`${visibles[i].uid} == uid`);
 					}
 					frag.write(') {');
 				}
 				else { // Object mask
-					var uid = Project.paintObjects[l.getObjectMask() - 1].uid;
-					frag.write('if ($uid == uid) {');
+					let uid = Project.paintObjects[l.getObjectMask() - 1].uid;
+					frag.write(`if (${uid} == uid) {`);
 				}
 			}
 
@@ -156,31 +155,31 @@ class MakeMesh {
 			frag.write('texpaint_sample = vec4(0.8, 0.8, 0.8, 1.0);');
 			frag.write('texpaint_opac = texpaint_sample.a;');
 
-			var masks = l.getMasks();
+			let masks = l.getMasks();
 			if (masks != null) {
-				var hasVisible = false;
-				for (m in masks) {
+				let hasVisible = false;
+				for (let m of masks) {
 					if (m.isVisible()) {
 						hasVisible = true;
 						break;
 					}
 				}
 				if (hasVisible) {
-					var texpaint_mask = 'texpaint_mask' + l.id;
-					frag.write('float $texpaint_mask = 0.0;');
-					for (m in masks) {
+					let texpaint_mask = 'texpaint_mask' + l.id;
+					frag.write(`float ${texpaint_mask} = 0.0;`);
+					for (let m of masks) {
 						if (!m.isVisible()) continue;
 						frag.add_shared_sampler('sampler2D texpaint' + m.id);
 						frag.write('{'); // Group mask is sampled across multiple layers
 						frag.write('float texpaint_mask_sample' + m.id + ' = textureLodShared(texpaint' + m.id + ', texCoord, 0.0).r;');
 						frag.write('}');
 					}
-					frag.write('texpaint_opac *= clamp($texpaint_mask, 0.0, 1.0);');
+					frag.write(`texpaint_opac *= clamp(${texpaint_mask}, 0.0, 1.0);`);
 				}
 			}
 
 			if (l.getOpacity() < 1) {
-				frag.write('texpaint_opac *= ${l.getOpacity()};');
+				frag.write(`texpaint_opac *= ${l.getOpacity()};`);
 			}
 
 			if (l == Project.layers[0]) {
@@ -228,11 +227,11 @@ class MakeMesh {
 			}
 
 			frag.vVec = true;
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('mat3 TBN = cotangentFrame(n, vVec, texCoord);');
-			#else
+			///else
 			frag.write('mat3 TBN = cotangentFrame(n, -vVec, texCoord);');
-			#end
+			///end
 			frag.write('n = ntex * 2.0 - 1.0;');
 			frag.write('n.y = -n.y;');
 			frag.write('n = normalize(mul(n, TBN));');
@@ -242,8 +241,8 @@ class MakeMesh {
 				frag.write('basecol = pow(basecol, vec3(2.2, 2.2, 2.2));');
 
 				if (Context.raw.viewportShader != null) {
-					var color = Context.raw.viewportShader(frag);
-					frag.write('fragColor[1] = vec4($color, 1.0);');
+					let color = Context.raw.viewportShader(frag);
+					frag.write(`fragColor[1] = vec4(${color}, 1.0);`);
 				}
 				else if (Context.raw.renderMode == RenderForward) {
 					frag.wposition = true;
@@ -329,11 +328,11 @@ class MakeMesh {
 		return con_mesh;
 	}
 
-	static inline function getMaxTextures(): Int {
-		#if krom_direct3d11
+	static getMaxTextures = (): i32 => {
+		///if krom_direct3d11
 		return 128 - 66;
-		#else
+		///else
 		return 16 - 3; // G4onG5/G4.c.h MAX_TEXTURES
-		#end
+		///end
 	}
 }

+ 41 - 45
armorsculpt/Sources/arm/MakeMeshPreview.hx → armorsculpt/Sources/MakeMeshPreview.ts

@@ -1,15 +1,11 @@
-package arm;
-
-import iron.MeshObject;
-import iron.SceneFormat;
 
 class MakeMeshPreview {
 
-	public static var opacityDiscardDecal = 0.05;
+	static opacityDiscardDecal = 0.05;
 
-	public static function run(data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext {
-		var context_id = "mesh";
-		var con_mesh: NodeShaderContext = data.add_context({
+	static run = (data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext => {
+		let context_id = "mesh";
+		let con_mesh: NodeShaderContext = data.add_context({
 			name: context_id,
 			depth_write: true,
 			compare_mode: "less",
@@ -19,14 +15,14 @@ class MakeMeshPreview {
 			depth_attachment: "DEPTH32"
 		});
 
-		var vert = con_mesh.make_vert();
-		var frag = con_mesh.make_frag();
+		let vert = con_mesh.make_vert();
+		let frag = con_mesh.make_frag();
 		frag.ins = vert.outs;
-		var pos = "pos";
+		let pos = "pos";
 
-		#if arm_skin
-		var isMesh = Std.isOfType(Context.raw.object, MeshObject);
-		var skin = isMesh && cast(Context.raw.object, MeshObject).data.geom.getVArray("bone") != null;
+		///if arm_skin
+		let isMesh = Context.raw.object.constructor == MeshObject;
+		let skin = isMesh && cast(Context.raw.object, MeshObject).data.geom.getVArray("bone") != null;
 		if (skin) {
 			pos = "spos";
 			con_mesh.add_elem("bone", 'short4norm');
@@ -43,58 +39,58 @@ class MakeMeshPreview {
 			vert.write_attrib('spos.xyz += 2.0 * (skinA.w * skinB.xyz - skinB.w * skinA.xyz + cross(skinA.xyz, skinB.xyz));');
 			vert.write_attrib('spos.xyz /= posUnpack;');
 		}
-		#end
+		///end
 
 		vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix');
-		vert.write_attrib('gl_Position = mul(vec4($pos.xyz, 1.0), WVP);');
+		vert.write_attrib(`gl_Position = mul(vec4(${pos}.xyz, 1.0), WVP);`);
 
-		var brushScale = (Context.raw.brushScale * Context.raw.brushNodesScale) + "";
+		let brushScale = (Context.raw.brushScale * Context.raw.brushNodesScale) + "";
 		vert.add_out('vec2 texCoord');
-		vert.write_attrib('texCoord = tex * float(${brushScale});');
+		vert.write_attrib(`texCoord = tex * float(${brushScale});`);
 
-		var decal = Context.raw.decalPreview;
+		let decal = Context.raw.decalPreview;
 		ParserMaterial.sample_keep_aspect = decal;
 		ParserMaterial.sample_uv_scale = brushScale;
 		ParserMaterial.parse_height = MakeMaterial.heightUsed;
 		ParserMaterial.parse_height_as_channel = true;
-		// var sout = ParserMaterial.parse(UINodes.inst.getCanvasMaterial(), con_mesh, vert, frag, matcon);
+		// let sout = ParserMaterial.parse(UINodes.inst.getCanvasMaterial(), con_mesh, vert, frag, matcon);
 		ParserMaterial.parse_height = false;
 		ParserMaterial.parse_height_as_channel = false;
 		ParserMaterial.sample_keep_aspect = false;
-		var base = "vec3(1.0, 1.0, 1.0)";//sout.out_basecol;
-		var rough = "0.0";//sout.out_roughness;
-		var met = "0.0";//sout.out_metallic;
-		var occ = "0.0";//sout.out_occlusion;
-		var opac = "0.0";//sout.out_opacity;
-		var height = "0.0";//sout.out_height;
-		var nortan = "vec3(1.0, 1.0, 1.0)";//ParserMaterial.out_normaltan;
-		frag.write('vec3 basecol = pow($base, vec3(2.2, 2.2, 2.2));');
-		frag.write('float roughness = $rough;');
-		frag.write('float metallic = $met;');
-		frag.write('float occlusion = $occ;');
-		frag.write('float opacity = $opac;');
-		frag.write('vec3 nortan = $nortan;');
-		frag.write('float height = $height;');
+		let base = "vec3(1.0, 1.0, 1.0)";//sout.out_basecol;
+		let rough = "0.0";//sout.out_roughness;
+		let met = "0.0";//sout.out_metallic;
+		let occ = "0.0";//sout.out_occlusion;
+		let opac = "0.0";//sout.out_opacity;
+		let height = "0.0";//sout.out_height;
+		let nortan = "vec3(1.0, 1.0, 1.0)";//ParserMaterial.out_normaltan;
+		frag.write(`vec3 basecol = pow(${base}, vec3(2.2, 2.2, 2.2));`);
+		frag.write(`float roughness = ${rough};`);
+		frag.write(`float metallic = ${met};`);
+		frag.write(`float occlusion = ${occ};`);
+		frag.write(`float opacity = ${opac};`);
+		frag.write(`vec3 nortan = ${nortan};`);
+		frag.write(`float height = ${height};`);
 
 		// ParserMaterial.parse_height_as_channel = false;
-		// vert.write('float vheight = $height;');
+		// vert.write(`float vheight = ${height};`);
 		// vert.add_out('float height');
 		// vert.write('height = vheight;');
-		// var displaceStrength = 0.1;
+		// let displaceStrength = 0.1;
 		// if (MakeMaterial.heightUsed && displaceStrength > 0.0) {
-		// 	vert.write('vec3 pos2 = $pos.xyz + vec3(nor.xy, pos.w) * vec3($height, $height, $height) * vec3($displaceStrength, $displaceStrength, $displaceStrength);');
+		// 	vert.write(`vec3 pos2 = ${pos}.xyz + vec3(nor.xy, pos.w) * vec3(${height}, ${height}, ${height}) * vec3(${displaceStrength}, ${displaceStrength}, ${displaceStrength});`);
 		// 	vert.write('gl_Position = mul(vec4(pos2.xyz, 1.0), WVP);');
 		// }
 
 		if (decal) {
 			if (Context.raw.tool == ToolText) {
 				frag.add_uniform('sampler2D textexttool', '_textexttool');
-				frag.write('opacity *= textureLod(textexttool, texCoord / float(${brushScale}), 0.0).r;');
+				frag.write(`opacity *= textureLod(textexttool, texCoord / float(${brushScale}), 0.0).r;`);
 			}
 		}
 		if (decal) {
-			var opac = opacityDiscardDecal;
-			frag.write('if (opacity < $opac) discard;');
+			let opac = opacityDiscardDecal;
+			frag.write(`if (opacity < ${opac}) discard;`);
 		}
 
 		frag.add_out('vec4 fragColor[3]');
@@ -123,11 +119,11 @@ class MakeMeshPreview {
 		}
 		else {
 			frag.vVec = true;
-			#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 			frag.write('mat3 TBN = cotangentFrame(n, vVec, texCoord);');
-			#else
+			///else
 			frag.write('mat3 TBN = cotangentFrame(n, -vVec, texCoord);');
-			#end
+			///end
 			frag.write('n = nortan * 2.0 - 1.0;');
 			frag.write('n.y = -n.y;');
 			frag.write('n = normalize(mul(n, TBN));');
@@ -149,11 +145,11 @@ class MakeMeshPreview {
 
 		ParserMaterial.finalize(con_mesh);
 
-		#if arm_skin
+		///if arm_skin
 		if (skin) {
 			vert.write('wnormal = normalize(mul(vec3(nor.xy, pos.w) + 2.0 * cross(skinA.xyz, cross(skinA.xyz, vec3(nor.xy, pos.w)) + skinA.w * vec3(nor.xy, pos.w)), N));');
 		}
-		#end
+		///end
 
 		con_mesh.data.shader_from_source = true;
 		con_mesh.data.vertex_shader = vert.get();

+ 10 - 14
armorsculpt/Sources/arm/MakeSculpt.hx → armorsculpt/Sources/MakeSculpt.ts

@@ -1,13 +1,9 @@
-package arm;
-
-import iron.SceneFormat;
-import zui.Zui.Nodes;
 
 class MakeSculpt {
 
-	public static function run(data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext {
-		var context_id = "paint";
-		var con_paint:NodeShaderContext = data.add_context({
+	static run = (data: NodeShaderData, matcon: TMaterialContext): NodeShaderContext => {
+		let context_id = "paint";
+		let con_paint:NodeShaderContext = data.add_context({
 			name: context_id,
 			depth_write: false,
 			compare_mode: "always", // TODO: align texcoords winding order
@@ -23,19 +19,19 @@ class MakeSculpt {
 		con_paint.data.color_writes_alpha = [true, true, true, true];
 		con_paint.allow_vcols = Context.raw.paintObject.data.cols != null;
 
-		var vert = con_paint.make_vert();
-		var frag = con_paint.make_frag();
+		let vert = con_paint.make_vert();
+		let frag = con_paint.make_frag();
 		frag.ins = vert.outs;
 
-		var faceFill = Context.raw.tool == ToolFill && Context.raw.fillTypeHandle.position == FillFace;
-		var decal = Context.raw.tool == ToolDecal || Context.raw.tool == ToolText;
+		let faceFill = Context.raw.tool == ToolFill && Context.raw.fillTypeHandle.position == FillFace;
+		let decal = Context.raw.tool == ToolDecal || Context.raw.tool == ToolText;
 
 		vert.add_out('vec2 texCoord');
 		vert.write('const vec2 madd = vec2(0.5, 0.5);');
 		vert.write('texCoord = pos.xy * madd + madd;');
-		#if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+		///if (krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
 		vert.write('texCoord.y = 1.0 - texCoord.y;');
-		#end
+		///end
 		vert.write('gl_Position = vec4(pos.xy, 0.0, 1.0);');
 
 		frag.add_uniform('vec4 inp', '_inputBrush');
@@ -57,7 +53,7 @@ class MakeSculpt {
 			Context.raw.tool == ToolParticle ||
 			decal) {
 
-			var depthReject = !Context.raw.xray;
+			let depthReject = !Context.raw.xray;
 
 			MakeBrush.run(vert, frag);
 		}

+ 0 - 10
armorsculpt/Sources/Manifest.hx

@@ -1,10 +0,0 @@
-package ;
-
-class Manifest {
-
-	public static inline var title = "ArmorSculpt";
-	public static inline var version = "0.1";
-	public static inline var url = "https://armorsculpt.org";
-	public static inline var url_android = "";
-	public static inline var url_ios = "";
-}

+ 9 - 0
armorsculpt/Sources/Manifest.ts

@@ -0,0 +1,9 @@
+
+class Manifest {
+
+	static title = "ArmorSculpt";
+	static version = "0.1";
+	static url = "https://armorsculpt.org";
+	static url_android = "";
+	static url_ios = "";
+}

+ 81 - 88
armorsculpt/Sources/arm/TabLayers.hx → armorsculpt/Sources/TabLayers.ts

@@ -1,28 +1,21 @@
-package arm;
-
-import zui.Zui;
-import zui.Zui.Nodes;
-import iron.Time;
-import iron.Input;
-import iron.MeshObject;
 
 class TabLayers {
 
-	static var layerNameEdit = -1;
-	static var layerNameHandle = new Handle();
-	static var showContextMenu = false;
+	static layerNameEdit = -1;
+	static layerNameHandle = new Handle();
+	static showContextMenu = false;
 
-	public static function draw(htab: Handle) {
-		var mini = Config.raw.layout[LayoutSidebarW] <= UIBase.sidebarMiniW;
+	static draw = (htab: Handle) => {
+		let mini = Config.raw.layout[LayoutSidebarW] <= UIBase.sidebarMiniW;
 		mini ? drawMini(htab) : drawFull(htab);
 	}
 
-	static function drawMini(htab: Handle) {
-		var ui = UIBase.inst.ui;
+	static drawMini = (htab: Handle) => {
+		let ui = UIBase.inst.ui;
 		ui.setHoveredTabName(tr("Layers"));
 
-		var _ELEMENT_H = ui.t.ELEMENT_H;
-		ui.t.ELEMENT_H = Std.int(UIBase.sidebarMiniW / 2 / ui.SCALE());
+		let _ELEMENT_H = ui.t.ELEMENT_H;
+		ui.t.ELEMENT_H = Math.floor(UIBase.sidebarMiniW / 2 / ui.SCALE());
 
 		ui.beginSticky();
 		ui.separator(5);
@@ -39,8 +32,8 @@ class TabLayers {
 		ui.t.ELEMENT_H = _ELEMENT_H;
 	}
 
-	static function drawFull(htab: Handle) {
-		var ui = UIBase.inst.ui;
+	static drawFull = (htab: Handle) => {
+		let ui = UIBase.inst.ui;
 		if (ui.tab(htab, tr("Layers"))) {
 			ui.beginSticky();
 			ui.row([1 / 4, 3 / 4]);
@@ -56,31 +49,31 @@ class TabLayers {
 		}
 	}
 
-	static function drawSlots(mini: Bool) {
-		for (i in 0...Project.layers.length) {
+	static drawSlots = (mini: bool) => {
+		for (let i = 0; i < Project.layers.length; ++i) {
 			if (i >= Project.layers.length) break; // Layer was deleted
-			var j = Project.layers.length - 1 - i;
-			var l = Project.layers[j];
+			let j = Project.layers.length - 1 - i;
+			let l = Project.layers[j];
 			drawLayerSlot(l, j, mini);
 		}
 	}
 
-	static function highlightOddLines() {
-		var ui = UIBase.inst.ui;
-		var step = ui.t.ELEMENT_H * 2;
-		var fullH = ui._windowH - UIBase.inst.hwnds[0].scrollOffset;
-		for (i in 0...Std.int(fullH / step)) {
+	static highlightOddLines = () => {
+		let ui = UIBase.inst.ui;
+		let step = ui.t.ELEMENT_H * 2;
+		let fullH = ui._windowH - UIBase.inst.hwnds[0].scrollOffset;
+		for (let i = 0; i < Math.floor(fullH / step); ++i) {
 			if (i % 2 == 0) {
 				ui.fill(0, i * step, (ui._w / ui.SCALE() - 2), step, ui.t.WINDOW_BG_COL - 0x00040404);
 			}
 		}
 	}
 
-	static function buttonNew(text: String) {
-		var ui = UIBase.inst.ui;
+	static buttonNew = (text: string) => {
+		let ui = UIBase.inst.ui;
 		if (ui.button(text)) {
-			UIMenu.draw(function(ui: Zui) {
-				var l = Context.raw.layer;
+			UIMenu.draw((ui: Zui) => {
+				let l = Context.raw.layer;
 				if (UIMenu.menuButton(ui, tr("Paint Layer"))) {
 					Base.newLayer();
 					History.newLayer();
@@ -89,18 +82,18 @@ class TabLayers {
 		}
 	}
 
-	static function comboFilter() {
-		var ui = UIBase.inst.ui;
-		var ar = [tr("All")];
-		var filterHandle = Zui.handle("tablayers_0");
+	static comboFilter = () => {
+		let ui = UIBase.inst.ui;
+		let ar = [tr("All")];
+		let filterHandle = Zui.handle("tablayers_0");
 		filterHandle.position = Context.raw.layerFilter;
 		Context.raw.layerFilter = ui.combo(filterHandle, ar, tr("Filter"), false, Left);
 	}
 
-	public static function remapLayerPointers(nodes: Array<TNode>, pointerMap: Map<Int, Int>) {
-		for (n in nodes) {
+	static remapLayerPointers = (nodes: TNode[], pointerMap: Map<i32, i32>) => {
+		for (let n of nodes) {
 			if (n.type == "LAYER" || n.type == "LAYER_MASK") {
-				var i = n.buttons[0].default_value;
+				let i = n.buttons[0].default_value;
 				if (pointerMap.exists(i)) {
 					n.buttons[0].default_value = pointerMap.get(i);
 				}
@@ -108,27 +101,27 @@ class TabLayers {
 		}
 	}
 
-	public static function initLayerMap(): Map<SlotLayer, Int> {
-		var res: Map<SlotLayer, Int> = [];
-		for (i in 0...Project.layers.length) res.set(Project.layers[i], i);
+	static initLayerMap = (): Map<SlotLayer, i32> => {
+		let res: Map<SlotLayer, i32> = [];
+		for (let i = 0; i < Project.layers.length; ++i) res.set(Project.layers[i], i);
 		return res;
 	}
 
-	public static function fillLayerMap(map: Map<SlotLayer, Int>): Map<Int, Int> {
-		var res: Map<Int, Int> = [];
-		for (l in map.keys()) res.set(map.get(l), Project.layers.indexOf(l) > -1 ? Project.layers.indexOf(l) : 9999);
+	static fillLayerMap = (map: Map<SlotLayer, i32>): Map<i32, i32> => {
+		let res: Map<i32, i32> = new Map();
+		for (let l of map.keys()) res.set(map.get(l), Project.layers.indexOf(l) > -1 ? Project.layers.indexOf(l) : 9999);
 		return res;
 	}
 
-	static function setDragLayer(layer: SlotLayer, offX: Float, offY: Float) {
+	static setDragLayer = (layer: SlotLayer, offX: f32, offY: f32) => {
 		Base.dragOffX = offX;
 		Base.dragOffY = offY;
 		Base.dragLayer = layer;
 		Context.raw.dragDestination = Project.layers.indexOf(layer);
 	}
 
-	static function drawLayerSlot(l: SlotLayer, i: Int, mini: Bool) {
-		var ui = UIBase.inst.ui;
+	static drawLayerSlot = (l: SlotLayer, i: i32, mini: bool) => {
+		let ui = UIBase.inst.ui;
 
 		if (Context.raw.layerFilter > 0 &&
 			l.getObjectMask() > 0 &&
@@ -143,21 +136,21 @@ class TabLayers {
 			return;
 		}
 
-		var step = ui.t.ELEMENT_H;
-		var checkw = (ui._windowW / 100 * 8) / ui.SCALE();
+		let step = ui.t.ELEMENT_H;
+		let checkw = (ui._windowW / 100 * 8) / ui.SCALE();
 
 		// Highlight drag destination
-		var mouse = Input.getMouse();
-		var absy = ui._windowY + ui._y;
+		let mouse = Input.getMouse();
+		let absy = ui._windowY + ui._y;
 		if (Base.isDragging && Base.dragLayer != null && Context.inLayers()) {
 			if (mouse.y > absy + step && mouse.y < absy + step * 3) {
-				var down = Project.layers.indexOf(Base.dragLayer) >= i;
+				let down = Project.layers.indexOf(Base.dragLayer) >= i;
 				Context.raw.dragDestination = down ? i : i - 1;
 
-				var ls = Project.layers;
-				var dest = Context.raw.dragDestination;
-				var toGroup = down ? dest > 0 && ls[dest - 1].parent != null && ls[dest - 1].parent.show_panel : dest < ls.length && ls[dest].parent != null && ls[dest].parent.show_panel;
-				var nestedGroup = Base.dragLayer.isGroup() && toGroup;
+				let ls = Project.layers;
+				let dest = Context.raw.dragDestination;
+				let toGroup = down ? dest > 0 && ls[dest - 1].parent != null && ls[dest - 1].parent.show_panel : dest < ls.length && ls[dest].parent != null && ls[dest].parent.show_panel;
+				let nestedGroup = Base.dragLayer.isGroup() && toGroup;
 				if (!nestedGroup) {
 					if (Context.raw.layer.canMove(Context.raw.dragDestination)) {
 						ui.fill(checkw, step * 2, (ui._windowW / ui.SCALE() - 2) - checkw, 2 * ui.SCALE(), ui.t.HIGHLIGHT_COL);
@@ -193,12 +186,12 @@ class TabLayers {
 		}
 	}
 
-	static function drawLayerSlotMini(l: SlotLayer, i: Int) {
-		var ui = UIBase.inst.ui;
+	static drawLayerSlotMini = (l: SlotLayer, i: i32) => {
+		let ui = UIBase.inst.ui;
 
 		ui.row([1, 1]);
-		var uix = ui._x;
-		var uiy = ui._y;
+		let uix = ui._x;
+		let uiy = ui._y;
 		ui.endElement();
 		ui.endElement();
 
@@ -206,12 +199,12 @@ class TabLayers {
 		ui._y -= ui.ELEMENT_OFFSET();
 	}
 
-	static function drawLayerSlotFull(l: SlotLayer, i: Int) {
-		var ui = UIBase.inst.ui;
+	static drawLayerSlotFull = (l: SlotLayer, i: i32) => {
+		let ui = UIBase.inst.ui;
 
-		var step = ui.t.ELEMENT_H;
+		let step = ui.t.ELEMENT_H;
 
-		var hasPanel = l.isGroup() || (l.isLayer() && l.getMasks(false) != null);
+		let hasPanel = l.isGroup() || (l.isLayer() && l.getMasks(false) != null);
 		if (hasPanel) {
 			ui.row([8 / 100, 52 / 100, 30 / 100, 10 / 100]);
 		}
@@ -220,14 +213,14 @@ class TabLayers {
 		}
 
 		// Draw eye icon
-		var icons = Res.get("icons.k");
-		var r = Res.tile18(icons, l.visible ? 0 : 1, 0);
-		var center = (step / 2) * ui.SCALE();
+		let icons = Res.get("icons.k");
+		let r = Res.tile18(icons, l.visible ? 0 : 1, 0);
+		let center = (step / 2) * ui.SCALE();
 		ui._x += 2;
 		ui._y += 3;
 		ui._y += center;
-		var col = ui.t.ACCENT_SELECT_COL;
-		var parentHidden = l.parent != null && (!l.parent.visible || (l.parent.parent != null && !l.parent.parent.visible));
+		let col = ui.t.ACCENT_SELECT_COL;
+		let parentHidden = l.parent != null && (!l.parent.visible || (l.parent.parent != null && !l.parent.parent.visible));
 		if (parentHidden) col -= 0x99000000;
 
 		if (ui.image(icons, col, null, r.x, r.y, r.w, r.h) == Released) {
@@ -237,8 +230,8 @@ class TabLayers {
 		ui._y -= 3;
 		ui._y -= center;
 
-		var uix = ui._x;
-		var uiy = ui._y;
+		let uix = ui._x;
+		let uiy = ui._y;
 
 		// Draw layer name
 		ui._y += center;
@@ -253,7 +246,7 @@ class TabLayers {
 				ui.inputY > ui._windowY + ui._y - center && ui.inputY < ui._windowY + ui._y - center + (step * ui.SCALE()) * 2) {
 				if (ui.inputStarted) {
 					Context.setLayer(l);
-					var mouse = Input.getMouse();
+					let mouse = Input.getMouse();
 					setDragLayer(Context.raw.layer, -(mouse.x - uix - ui._windowX - 3), -(mouse.y - uiy - ui._windowY + 1));
 				}
 				else if (ui.inputReleased) {
@@ -267,9 +260,9 @@ class TabLayers {
 				}
 			}
 
-			var state = ui.text(l.name);
+			let state = ui.text(l.name);
 			if (state == State.Released) {
-				var td = Time.time() - Context.raw.selectTime;
+				let td = Time.time() - Context.raw.selectTime;
 				if (td < 0.2 && td > 0.0) {
 					layerNameEdit = l.id;
 					layerNameHandle.text = l.name;
@@ -277,11 +270,11 @@ class TabLayers {
 				}
 			}
 
-			// var inFocus = ui.inputX > ui._windowX && ui.inputX < ui._windowX + ui._windowW &&
+			// let inFocus = ui.inputX > ui._windowX && ui.inputX < ui._windowX + ui._windowW &&
 			// 			  ui.inputY > ui._windowY && ui.inputY < ui._windowY + ui._windowH;
 			// if (inFocus && ui.isDeleteDown && canDelete(Context.raw.layer)) {
 			// 	ui.isDeleteDown = false;
-			// 	function _init() {
+			// 	let _init() = () => {
 			// 		deleteLayer(Context.raw.layer);
 			// 	}
 			// 	App.notifyOnInit(_init);
@@ -312,7 +305,7 @@ class TabLayers {
 
 		if (hasPanel) {
 			ui._y += center;
-			var layerPanel = Zui.handle("tablayers_1").nest(l.id);
+			let layerPanel = Zui.handle("tablayers_1").nest(l.id);
 			layerPanel.selected = l.show_panel;
 			l.show_panel = ui.panel(layerPanel, "", true, false, false);
 			ui._y -= center;
@@ -344,23 +337,23 @@ class TabLayers {
 		ui._y -= ui.ELEMENT_OFFSET();
 	}
 
-	static function comboObject(ui: Zui, l: SlotLayer, label = false): Handle {
-		var ar = [tr("Shared")];
-		var objectHandle = Zui.handle("tablayers_2").nest(l.id);
+	static comboObject = (ui: Zui, l: SlotLayer, label = false): Handle => {
+		let ar = [tr("Shared")];
+		let objectHandle = Zui.handle("tablayers_2").nest(l.id);
 		objectHandle.position = l.objectMask;
 		l.objectMask = ui.combo(objectHandle, ar, tr("Object"), label, Left);
 		return objectHandle;
 	}
 
-	static function layerToggleVisible(l: SlotLayer) {
+	static layerToggleVisible = (l: SlotLayer) => {
 		l.visible = !l.visible;
 		UIView2D.inst.hwnd.redraws = 2;
 		MakeMaterial.parseMeshMaterial();
 	}
 
-	static function drawLayerHighlight(l: SlotLayer, mini: Bool) {
-		var ui = UIBase.inst.ui;
-		var step = ui.t.ELEMENT_H;
+	static drawLayerHighlight = (l: SlotLayer, mini: bool) => {
+		let ui = UIBase.inst.ui;
+		let step = ui.t.ELEMENT_H;
 
 		// Separator line
 		ui.fill(0, 0, (ui._w / ui.SCALE() - 2), 1 * ui.SCALE(), ui.t.SEPARATOR_COL);
@@ -376,8 +369,8 @@ class TabLayers {
 		}
 	}
 
-	static function canMergeDown(l: SlotLayer) : Bool {
-		var index = Project.layers.indexOf(l);
+	static canMergeDown = (l: SlotLayer) : bool => {
+		let index = Project.layers.indexOf(l);
 		// Lowest layer
 		if (index == 0) return false;
 		// Lowest layer that has masks
@@ -389,11 +382,11 @@ class TabLayers {
 		return true;
 	}
 
-	static function drawLayerContextMenu(l: SlotLayer, mini: Bool) {
+	static drawLayerContextMenu = (l: SlotLayer, mini: bool) => {
 
 	}
 
-	public static function canDropNewLayer(position: Int) {
+	static canDropNewLayer = (position: i32) => {
 		if (position > 0 && position < Project.layers.length && Project.layers[position - 1].isMask()) {
 			// 1. The layer to insert is inserted in the middle
 			// 2. The layer below is a mask, i.e. the layer would have to be a (group) mask, too.

+ 0 - 102
armorsculpt/Sources/arm/ExportObj.hx

@@ -1,102 +0,0 @@
-package arm;
-
-import js.lib.Int16Array;
-import iron.MeshObject;
-
-class ExportObj {
-
-	static function writeString(out: Array<Int>, str: String) {
-		for (i in 0...str.length) {
-			out.push(str.charCodeAt(i));
-		}
-	}
-
-	public static function run(path: String, paintObjects: Array<MeshObject>, applyDisplacement = false) {
-		var o: Array<Int> = [];
-		writeString(o, "# armorsculpt.org\n");
-
-		var texpaint = Project.layers[0].texpaint;
-		var pixels = texpaint.getPixels();
-		var pixelsView = new js.lib.DataView(pixels);
-		var mesh = paintObjects[0].data.raw;
-		var inda = mesh.index_arrays[0].values;
-
-		var posa = new Int16Array(inda.length * 4);
-		for (i in 0...inda.length) {
-			var index = inda[i];
-			posa[index * 4    ] = Std.int(pixelsView.getFloat32(i * 16    , true) * 32767);
-			posa[index * 4 + 1] = Std.int(pixelsView.getFloat32(i * 16 + 4, true) * 32767);
-			posa[index * 4 + 2] = Std.int(pixelsView.getFloat32(i * 16 + 8, true) * 32767);
-		}
-
-		var poff = 0;
-		// for (p in paintObjects) {
-			var p = paintObjects[0];
-			// var mesh = p.data.raw;
-			var inv = 1 / 32767;
-			var sc = p.data.scalePos * inv;
-			// var posa = mesh.vertex_arrays[0].values;
-			var len = Std.int(posa.length / 4);
-			// var len = Std.int(inda.length);
-
-			// Merge shared vertices and remap indices
-			var posa2 = new Int16Array(len * 3);
-			var posmap = new Map<Int, Int>();
-
-			var pi = 0;
-			for (i in 0...len) {
-				var found = false;
-				for (j in 0...pi) {
-					if (posa2[j * 3    ] == posa[i * 4    ] &&
-						posa2[j * 3 + 1] == posa[i * 4 + 1] &&
-						posa2[j * 3 + 2] == posa[i * 4 + 2]) {
-						posmap.set(i, j);
-						found = true;
-						break;
-					}
-				}
-				if (!found) {
-					posmap.set(i, pi);
-					posa2[pi * 3    ] = posa[i * 4    ];
-					posa2[pi * 3 + 1] = posa[i * 4 + 1];
-					posa2[pi * 3 + 2] = posa[i * 4 + 2];
-					pi++;
-				}
-			}
-
-			writeString(o, "o " + p.name + "\n");
-			for (i in 0...pi) {
-				writeString(o, "v ");
-				var vx = posa2[i * 3] * sc + "";
-				writeString(o, vx.substr(0, vx.indexOf(".") + 7));
-				writeString(o, " ");
-				var vy = posa2[i * 3 + 2] * sc + "";
-				writeString(o, vy.substr(0, vy.indexOf(".") + 7));
-				writeString(o, " ");
-				var vz = -posa2[i * 3 + 1] * sc + "";
-				writeString(o, vz.substr(0, vz.indexOf(".") + 7));
-				writeString(o, "\n");
-			}
-
-			// var inda = mesh.index_arrays[0].values;
-			for (i in 0...Std.int(inda.length / 3)) {
-				var pi1 = posmap.get(inda[i * 3    ]) + 1 + poff;
-				var pi2 = posmap.get(inda[i * 3 + 1]) + 1 + poff;
-				var pi3 = posmap.get(inda[i * 3 + 2]) + 1 + poff;
-				writeString(o, "f ");
-				writeString(o, pi1 + "");
-				writeString(o, " ");
-				writeString(o, pi2 + "");
-				writeString(o, " ");
-				writeString(o, pi3 + "");
-				writeString(o, "\n");
-			}
-			poff += pi;
-		// }
-
-		if (!path.endsWith(".obj")) path += ".obj";
-
-		var b = js.lib.Uint8Array.from(o).buffer;
-		Krom.fileSaveBytes(path, b, b.byteLength);
-	}
-}

+ 0 - 9
armorsculpt/Sources/import.hx

@@ -1,9 +0,0 @@
-// Global imports
-
-#if (!macro)
-
-import arm.Translator.tr;
-import arm.Enums;
-using StringTools;
-
-#end

+ 32 - 39
armorsculpt/Sources/arm/nodes/BrushOutputNode.hx → armorsculpt/Sources/nodes/BrushOutputNode.ts

@@ -1,37 +1,30 @@
-package arm.nodes;
 
-import iron.Input;
-import arm.LogicNode;
-import arm.MakeMaterial;
-import arm.UIBase;
-import arm.UIView2D;
-
-@:keep
+// @:keep
 class BrushOutputNode extends LogicNode {
 
-	public var Directional = false; // button 0
+	Directional = false; // button 0
 
-	public function new() {
+	constructor() {
 		super();
 		Context.raw.runBrush = run;
 		Context.raw.parseBrushInputs = parseInputs;
 	}
 
-	function parseInputs() {
-		var lastMask = Context.raw.brushMaskImage;
-		var lastStencil = Context.raw.brushStencilImage;
+	parseInputs = () => {
+		let lastMask = Context.raw.brushMaskImage;
+		let lastStencil = Context.raw.brushStencilImage;
 
-		var input0: Dynamic;
-		var input1: Dynamic;
-		var input2: Dynamic;
-		var input3: Dynamic;
-		var input4: Dynamic;
+		let input0: any;
+		let input1: any;
+		let input2: any;
+		let input3: any;
+		let input4: any;
 		try {
-			inputs[0].get(function(value) { input0 = value; });
-			inputs[1].get(function(value) { input1 = value; });
-			inputs[2].get(function(value) { input2 = value; });
-			inputs[3].get(function(value) { input3 = value; });
-			inputs[4].get(function(value) { input4 = value; });
+			inputs[0].get((value) => { input0 = value; });
+			inputs[1].get((value) => { input1 = value; });
+			inputs[2].get((value) => { input2 = value; });
+			inputs[3].get((value) => { input3 = value; });
+			inputs[4].get((value) => { input4 = value; });
 		}
 		catch (_) {
 			return;
@@ -40,14 +33,14 @@ class BrushOutputNode extends LogicNode {
 		Context.raw.paintVec = input0;
 		Context.raw.brushNodesRadius = input1;
 
-		var opac: Dynamic = input2; // Float or texture name
+		let opac: any = input2; // Float or texture name
 		if (opac == null) opac = 1.0;
-		if (Std.isOfType(opac, String)) {
+		if (typeof opac == "string") {
 			Context.raw.brushMaskImageIsAlpha = opac.endsWith(".a");
 			opac = opac.substr(0, opac.lastIndexOf("."));
 			Context.raw.brushNodesOpacity = 1.0;
-			var index = Project.assetNames.indexOf(opac);
-			var asset = Project.assets[index];
+			let index = Project.assetNames.indexOf(opac);
+			let asset = Project.assets[index];
 			Context.raw.brushMaskImage = Project.getImage(asset);
 		}
 		else {
@@ -57,13 +50,13 @@ class BrushOutputNode extends LogicNode {
 
 		Context.raw.brushNodesHardness = input3;
 
-		var stencil: Dynamic = input4; // Float or texture name
+		let stencil: any = input4; // Float or texture name
 		if (stencil == null) stencil = 1.0;
-		if (Std.isOfType(stencil, String)) {
+		if (typeof stencil == "string") {
 			Context.raw.brushStencilImageIsAlpha = stencil.endsWith(".a");
 			stencil = stencil.substr(0, stencil.lastIndexOf("."));
-			var index = Project.assetNames.indexOf(stencil);
-			var asset = Project.assets[index];
+			let index = Project.assetNames.indexOf(stencil);
+			let asset = Project.assets[index];
 			Context.raw.brushStencilImage = Project.getImage(asset);
 		}
 		else {
@@ -78,9 +71,9 @@ class BrushOutputNode extends LogicNode {
 		Context.raw.brushDirectional = Directional;
 	}
 
-	function run(from: Int) {
-		var left = 0.0;
-		var right = 1.0;
+	run = (from: i32) => {
+		let left = 0.0;
+		let right = 1.0;
 		if (Context.raw.paint2d) {
 			left = 1.0;
 			right = (Context.raw.splitView ? 2.0 : 1.0) + UIView2D.inst.ww / Base.w();
@@ -93,10 +86,10 @@ class BrushOutputNode extends LogicNode {
 		}
 
 		// Do not paint over fill layer
-		var fillLayer = Context.raw.layer.fill_layer != null;
+		let fillLayer = Context.raw.layer.fill_layer != null;
 
 		// Do not paint over groups
-		var groupLayer = Context.raw.layer.isGroup();
+		let groupLayer = Context.raw.layer.isGroup();
 
 		// Paint bounds
 		if (Context.raw.paintVec.x > left &&
@@ -112,11 +105,11 @@ class BrushOutputNode extends LogicNode {
 			!Base.isScrolling() &&
 			!Base.isComboSelected()) {
 
-			var down = Input.getMouse().down() || Input.getPen().down();
+			let down = Input.getMouse().down() || Input.getPen().down();
 
 			// Prevent painting the same spot
-			var sameSpot = Context.raw.paintVec.x == Context.raw.lastPaintX && Context.raw.paintVec.y == Context.raw.lastPaintY;
-			var lazy = Context.raw.tool == ToolBrush && Context.raw.brushLazyRadius > 0;
+			let sameSpot = Context.raw.paintVec.x == Context.raw.lastPaintX && Context.raw.paintVec.y == Context.raw.lastPaintY;
+			let lazy = Context.raw.tool == ToolBrush && Context.raw.brushLazyRadius > 0;
 			if (down && (sameSpot || lazy)) {
 				Context.raw.painted++;
 			}

+ 1 - 0
armorsculpt/project.js

@@ -9,6 +9,7 @@ await project.addProject("../base");
 
 project.addSources("../armorpaint/Sources"); ////
 project.addSources("Sources");
+project.addSources("Sources/nodes");
 project.addShaders("Shaders/*.glsl", { embed: flags.snapshot });
 project.addAssets("Assets/*", { destination: "data/{name}", embed: flags.snapshot });
 project.addAssets("Assets/keymap_presets/*", { destination: "data/keymap_presets/{name}" });

+ 0 - 22
base/Assets/licenses/license_heapsfbx.md

@@ -1,22 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2013 Nicolas Cannasse
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-https://github.com/HeapsIO/heaps/tree/master/hxd/fmt/fbx

+ 192 - 0
base/Sources/Args.ts

@@ -0,0 +1,192 @@
+
+class Args {
+
+	static useArgs = false;
+	static assetPath = "";
+	static background = false;
+	///if (is_paint || is_lab)
+	static exportTextures = false;
+	static exportTexturesType = "";
+	static exportTexturesPreset = "";
+	static exportTexturesPath = "";
+	///end
+	///if (is_paint || is_sculpt)
+	static reimportMesh = false;
+	static exportMesh = false;
+	static exportMeshPath = "";
+	///end
+	///if is_paint
+	static exportMaterial = false;
+	static exportMaterialPath = "";
+	///end
+
+	static parse = () => {
+		if (Krom.getArgCount() > 1) {
+			Args.useArgs = true;
+
+			let i = 0;
+			while (i < Krom.getArgCount()) {
+				// Process each arg
+				let currentArg = Krom.getArg(i);
+
+				if (Path.isProject(currentArg)) {
+					Project.filepath = currentArg;
+				}
+				else if (currentArg == "--b" || currentArg == "--background") {
+					Args.background = true;
+				}
+
+				///if (is_paint || is_lab)
+				else if (Path.isTexture(currentArg)) {
+					Args.assetPath = currentArg;
+				}
+				else if (currentArg == "--export-textures" && (i + 3) <= Krom.getArgCount()) {
+					Args.exportTextures = true;
+					++i;
+					Args.exportTexturesType = Krom.getArg(i);
+					++i;
+					Args.exportTexturesPreset = Krom.getArg(i);
+					++i;
+					Args.exportTexturesPath = Krom.getArg(i);
+				}
+				///end
+
+				///if (is_paint || is_sculpt)
+				else if (currentArg == "--reload-mesh") {
+					Args.reimportMesh = true;
+				}
+				else if (currentArg == "--export-mesh" && (i + 1) <= Krom.getArgCount()) {
+					Args.exportMesh = true;
+					++i;
+					Args.exportMeshPath = Krom.getArg(i);
+				}
+				else if (Path.isMesh(currentArg) ||
+						(i > 1 && !currentArg.startsWith("-") && Path.isFolder(currentArg))) {
+					Args.assetPath = currentArg;
+				}
+				///end
+
+				///if is_paint
+				else if (currentArg == "--export-material" && (i + 1) <= Krom.getArgCount()) {
+					Args.exportMaterial = true;
+					++i;
+					Args.exportMaterialPath = Krom.getArg(i);
+				}
+				///end
+
+				++i;
+			}
+		}
+	}
+
+	static run = () => {
+		if (Args.useArgs) {
+			App.notifyOnInit(() => {
+				if (Project.filepath != "") {
+					ImportArm.runProject(Project.filepath);
+				}
+				else if (Args.assetPath != "") {
+					ImportAsset.run(Args.assetPath, -1, -1, false);
+					///if is_paint
+					if (Path.isTexture(Args.assetPath)) {
+						UIBase.inst.show2DView(View2DType.View2DAsset);
+					}
+					///end
+				}
+				///if (is_paint || is_sculpt)
+				else if (Args.reimportMesh) {
+					Project.reimportMesh();
+				}
+				///end
+
+				///if (is_paint || is_lab)
+				if (Args.exportTextures) {
+					if (Args.exportTexturesType == "png" ||
+						Args.exportTexturesType == "jpg" ||
+						Args.exportTexturesType == "exr16" ||
+						Args.exportTexturesType == "exr32") {
+						if (Path.isFolder(Args.exportTexturesPath)) {
+							// Applying the correct format type from args
+							if (Args.exportTexturesType == "png") {
+								///if is_paint
+								Base.bitsHandle.position = TextureBits.Bits8;
+								///end
+								Context.raw.formatType = TextureLdrFormat.FormatPng;
+							}
+							else if (Args.exportTexturesType == "jpg") {
+								///if is_paint
+								Base.bitsHandle.position = TextureBits.Bits8;
+								///end
+								Context.raw.formatType = TextureLdrFormat.FormatJpg;
+							}
+							else if (Args.exportTexturesType == "exr16") {
+								///if is_paint
+								Base.bitsHandle.position = TextureBits.Bits16;
+								///end
+							}
+							else if (Args.exportTexturesType == "exr32") {
+								///if is_paint
+								Base.bitsHandle.position = TextureBits.Bits32;
+								///end
+							}
+
+							///if is_paint
+							Context.raw.layersExport = ExportMode.ExportVisible;
+							///end
+
+							// Get export preset and apply the correct one from args
+							BoxExport.files = File.readDirectory(Path.data() + Path.sep + "export_presets");
+							for (let i = 0; i < BoxExport.files.length; ++i) {
+								BoxExport.files[i] = BoxExport.files[i].substr(0, BoxExport.files[i].length - 5); // Strip .json
+							}
+
+							let file = "export_presets/" + BoxExport.files[0] + ".json";
+							for (let f of BoxExport.files) if (f == Args.exportTexturesPreset) {
+								file = "export_presets/" + BoxExport.files[BoxExport.files.indexOf(f)] + ".json";
+							}
+
+							Data.getBlob(file, (blob: ArrayBuffer) => {
+								BoxExport.preset = JSON.parse(System.bufferToString(blob));
+								Data.deleteBlob("export_presets/" + file);
+							});
+
+							// Export queue
+							App.notifyOnInit(() => {
+								ExportTexture.run(Args.exportTexturesPath);
+							});
+						}
+						else {
+							Krom.log(tr("Invalid export directory"));
+						}
+					}
+					else {
+						Krom.log(tr("Invalid texture type"));
+					}
+				}
+				///end
+
+				///if (is_paint || is_sculpt)
+				else if (Args.exportMesh) {
+					if (Path.isFolder(Args.exportMeshPath)) {
+						let f = UIFiles.filename;
+						if (f == "") f = tr("untitled");
+						ExportMesh.run(Args.exportMeshPath + Path.sep + f, null, false);
+					}
+					else {
+						Krom.log(tr("Invalid export directory"));
+					}
+				}
+				///end
+
+				///if is_paint
+				else if (Args.exportMaterial) {
+					Context.raw.writeIconOnExport = true;
+					ExportArm.runMaterial(Args.exportMaterialPath);
+				}
+				///end
+
+				if (Args.background) System.stop();
+			});
+		}
+	}
+}

+ 2287 - 0
base/Sources/Base.ts

@@ -0,0 +1,2287 @@
+
+class Base {
+
+	static uiEnabled = true;
+	static isDragging = false;
+	static isResizing = false;
+	static dragAsset: TAsset = null;
+	static dragSwatch: TSwatchColor = null;
+	static dragFile: string = null;
+	static dragFileIcon: Image = null;
+	static dragTint = 0xffffffff;
+	static dragSize = -1;
+	static dragRect: TRect = null;
+	static dragOffX = 0.0;
+	static dragOffY = 0.0;
+	static dragStart = 0.0;
+	static dropX = 0.0;
+	static dropY = 0.0;
+	static font: Font = null;
+	static theme: Theme;
+	static colorWheel: Image;
+	static colorWheelGradient: Image;
+	static uiBox: Zui;
+	static uiMenu: Zui;
+	static defaultElementW = 100;
+	static defaultElementH = 28;
+	static defaultFontSize = 13;
+	static resHandle = new Handle();
+	static bitsHandle = new Handle();
+	static dropPaths: string[] = [];
+	static appx = 0;
+	static appy = 0;
+	static lastWindowWidth = 0;
+	static lastWindowHeight = 0;
+	///if (is_paint || is_sculpt)
+	static dragMaterial: SlotMaterial = null;
+	static dragLayer: SlotLayer = null;
+	///end
+
+	static pipeCopy: PipelineState;
+	static pipeCopy8: PipelineState;
+	static pipeCopy128: PipelineState;
+	static pipeCopyBGRA: PipelineState;
+	static pipeCopyRGB: PipelineState = null;
+	///if (is_paint || is_sculpt)
+	static pipeMerge: PipelineState = null;
+	static pipeMergeR: PipelineState = null;
+	static pipeMergeG: PipelineState = null;
+	static pipeMergeB: PipelineState = null;
+	static pipeMergeA: PipelineState = null;
+	static pipeInvert8: PipelineState;
+	static pipeApplyMask: PipelineState;
+	static pipeMergeMask: PipelineState;
+	static pipeColorIdToMask: PipelineState;
+	static tex0: TextureUnit;
+	static tex1: TextureUnit;
+	static texmask: TextureUnit;
+	static texa: TextureUnit;
+	static opac: ConstantLocation;
+	static blending: ConstantLocation;
+	static tex0Mask: TextureUnit;
+	static texaMask: TextureUnit;
+	static tex0MergeMask: TextureUnit;
+	static texaMergeMask: TextureUnit;
+	static texColorId: TextureUnit;
+	static texpaintColorId: TextureUnit;
+	static opacMergeMask: ConstantLocation;
+	static blendingMergeMask: ConstantLocation;
+	static tempMaskImage: Image = null;
+	///end
+	///if is_lab
+	static pipeCopyR: PipelineState;
+	static pipeCopyG: PipelineState;
+	static pipeCopyB: PipelineState;
+	static pipeCopyA: PipelineState;
+	static pipeCopyATex: TextureUnit;
+	static pipeInpaintPreview: PipelineState;
+	static tex0InpaintPreview: TextureUnit;
+	static texaInpaintPreview: TextureUnit;
+	///end
+	static tempImage: Image = null;
+	static expa: Image = null;
+	static expb: Image = null;
+	static expc: Image = null;
+	static pipeCursor: PipelineState;
+	static cursorVP: ConstantLocation;
+	static cursorInvVP: ConstantLocation;
+	static cursorMouse: ConstantLocation;
+	static cursorTexStep: ConstantLocation;
+	static cursorRadius: ConstantLocation;
+	static cursorCameraRight: ConstantLocation;
+	static cursorTint: ConstantLocation;
+	static cursorTex: TextureUnit;
+	static cursorGbufferD: TextureUnit;
+
+	///if (is_paint || is_sculpt)
+	static defaultBase = 0.5;
+	static defaultRough = 0.4;
+	///if (krom_android || krom_ios)
+	static maxLayers = 18;
+	///else
+	static maxLayers = 255;
+	///end
+	///end
+	static defaultFov = 0.69;
+
+	constructor() {
+		Base.lastWindowWidth = System.width;
+		Base.lastWindowHeight = System.height;
+
+		System.notifyOnDropFiles((dropPath: string) => {
+			///if krom_linux
+			dropPath = decodeURIComponent(dropPath);
+			///end
+			dropPath = trim_end(dropPath);
+			Base.dropPaths.push(dropPath);
+		});
+
+		System.notifyOnApplicationState(
+			() => { // Foreground
+				Context.raw.foregroundEvent = true;
+				Context.raw.lastPaintX = -1;
+				Context.raw.lastPaintY = -1;
+			},
+			() => {}, // Resume
+			() => {}, // Pause
+			() => { // Background
+				// Release keys after alt-tab / win-tab
+				Input.getKeyboard().upListener(KeyCode.Alt);
+				Input.getKeyboard().upListener(KeyCode.Win);
+			},
+			() => { // Shutdown
+				///if (krom_android || krom_ios)
+				Project.projectSave();
+				///end
+			}
+		);
+
+		Krom.setSaveAndQuitCallback(Base.saveAndQuitCallback);
+
+		Data.getFont("font.ttf", (f: Font) => {
+			Data.getImage("color_wheel.k", (imageColorWheel: Image) => {
+				Data.getImage("color_wheel_gradient.k", (imageColorWheelGradient: Image) => {
+
+					Base.font = f;
+					Config.loadTheme(Config.raw.theme, false);
+					Base.defaultElementW = Base.theme.ELEMENT_W;
+					Base.defaultFontSize = Base.theme.FONT_SIZE;
+					Translator.loadTranslations(Config.raw.locale);
+					UIFiles.filename = tr("untitled");
+					///if (krom_android || krom_ios)
+					System.title = tr("untitled");
+					///end
+
+					// Baked font for fast startup
+					if (Config.raw.locale == "en") {
+						Base.font.font_ = Krom.g2_font_13(Base.font.blob);
+						Base.font.fontGlyphs = Graphics2.fontGlyphs;
+					}
+					else Base.font.init();
+
+					Base.colorWheel = imageColorWheel;
+					Base.colorWheelGradient = imageColorWheelGradient;
+					Nodes.enumTexts = Base.enumTexts;
+					Nodes.tr = tr;
+					Base.uiBox = new Zui({ theme: Base.theme, font: f, scaleFactor: Config.raw.window_scale, color_wheel: Base.colorWheel, black_white_gradient: Base.colorWheelGradient });
+					Base.uiMenu = new Zui({ theme: Base.theme, font: f, scaleFactor: Config.raw.window_scale, color_wheel: Base.colorWheel, black_white_gradient: Base.colorWheelGradient });
+					Base.defaultElementH = Base.uiMenu.t.ELEMENT_H;
+
+					// Init plugins
+					Plugin.init();
+					if (Config.raw.plugins != null) {
+						for (let plugin of Config.raw.plugins) {
+							Plugin.start(plugin);
+						}
+					}
+
+					Args.parse();
+
+					new Camera();
+					new UIBase();
+					new UINodes();
+					new UIView2D();
+
+					///if is_lab
+					RandomNode.setSeed(Math.floor(Time.time() * 4294967295));
+					///end
+
+					App.notifyOnUpdate(Base.update);
+					App.notifyOnRender2D(UIView2D.inst.render);
+					App.notifyOnUpdate(UIView2D.inst.update);
+					///if (is_paint || is_sculpt)
+					App.notifyOnRender2D(UIBase.inst.renderCursor);
+					///end
+					App.notifyOnUpdate(UINodes.inst.update);
+					App.notifyOnRender2D(UINodes.inst.render);
+					App.notifyOnUpdate(UIBase.inst.update);
+					App.notifyOnRender2D(UIBase.inst.render);
+					App.notifyOnUpdate(Camera.inst.update);
+					App.notifyOnRender2D(Base.render);
+
+					///if (is_paint || is_sculpt)
+					Base.appx = UIToolbar.inst.toolbarw;
+					///end
+					///if is_lab
+					Base.appx = 0;
+					///end
+
+					Base.appy = UIHeader.headerh;
+					if (Config.raw.layout[LayoutSize.LayoutHeader] == 1) Base.appy += UIHeader.headerh;
+					let cam = Scene.active.camera;
+					cam.data.raw.fov = Math.floor(cam.data.raw.fov * 100) / 100;
+					cam.buildProjection();
+
+					Args.run();
+
+					///if (krom_android || krom_ios)
+					let hasProjects = Config.raw.recent_projects.length > 0;
+					///else
+					let hasProjects = true;
+					///end
+
+					if (Config.raw.splash_screen && hasProjects) {
+						BoxProjects.show();
+					}
+				});
+			});
+		});
+	}
+
+	static saveAndQuitCallback = (save: bool) => {
+		Base.saveWindowRect();
+		if (save) Project.projectSave(true);
+		else System.stop();
+	}
+
+	///if (is_paint || is_sculpt)
+	static w = (): i32 => {
+		// Drawing material preview
+		if (UIBase.inst != null && Context.raw.materialPreview) {
+			return UtilRender.materialPreviewSize;
+		}
+
+		// Drawing decal preview
+		if (UIBase.inst != null && Context.raw.decalPreview) {
+			return UtilRender.decalPreviewSize;
+		}
+
+		let res = 0;
+		if (UINodes.inst == null || UIBase.inst == null) {
+			let sidebarw = Config.raw.layout == null ? UIBase.defaultSidebarW : Config.raw.layout[LayoutSize.LayoutSidebarW];
+			res = System.width - sidebarw - UIToolbar.defaultToolbarW;
+		}
+		else if (UINodes.inst.show || UIView2D.inst.show) {
+			res = System.width - Config.raw.layout[LayoutSize.LayoutSidebarW] - Config.raw.layout[LayoutSize.LayoutNodesW] - UIToolbar.inst.toolbarw;
+		}
+		else if (UIBase.inst.show) {
+			res = System.width - Config.raw.layout[LayoutSize.LayoutSidebarW] - UIToolbar.inst.toolbarw;
+		}
+		else { // Distract free
+			res = System.width;
+		}
+		if (UIBase.inst != null && Context.raw.viewIndex > -1) {
+			res = Math.floor(res / 2);
+		}
+		if (Context.raw.paint2dView) {
+			res = UIView2D.inst.ww;
+		}
+
+		return res > 0 ? res : 1; // App was minimized, force render path resize
+	}
+
+	static h = (): i32 => {
+		// Drawing material preview
+		if (UIBase.inst != null && Context.raw.materialPreview) {
+			return UtilRender.materialPreviewSize;
+		}
+
+		// Drawing decal preview
+		if (UIBase.inst != null && Context.raw.decalPreview) {
+			return UtilRender.decalPreviewSize;
+		}
+
+		let res = System.height;
+
+		if (UIBase.inst == null) {
+			res -= UIHeader.defaultHeaderH * 2 + UIStatus.defaultStatusH;
+
+			///if (krom_android || krom_ios)
+			let layoutHeader = 0;
+			///else
+			let layoutHeader = 1;
+			///end
+			if (layoutHeader == 0) {
+				res += UIHeader.headerh;
+			}
+		}
+		else if (UIBase.inst != null && UIBase.inst.show && res > 0) {
+			let statush = Config.raw.layout[LayoutSize.LayoutStatusH];
+			res -= Math.floor(UIHeader.defaultHeaderH * 2 * Config.raw.window_scale) + statush;
+
+			if (Config.raw.layout[LayoutSize.LayoutHeader] == 0) {
+				res += UIHeader.headerh;
+			}
+		}
+
+		return res > 0 ? res : 1; // App was minimized, force render path resize
+	}
+	///end
+
+	///if is_lab
+	static w = (): i32 => {
+		let res = 0;
+		if (UINodes.inst == null) {
+			res = System.width;
+		}
+		else if (UINodes.inst.show || UIView2D.inst.show) {
+			res = System.width - Config.raw.layout[LayoutSize.LayoutNodesW];
+		}
+		else { // Distract free
+			res = System.width;
+		}
+
+		return res > 0 ? res : 1; // App was minimized, force render path resize
+	}
+
+	static h = (): i32 => {
+		let res = System.height;
+		if (UIBase.inst == null) {
+			res -= UIHeader.defaultHeaderH * 2 + UIStatus.defaultStatusH;
+		}
+		else if (UIBase.inst != null && res > 0) {
+			let statush = Config.raw.layout[LayoutSize.LayoutStatusH];
+			res -= Math.floor(UIHeader.defaultHeaderH * 2 * Config.raw.window_scale) + statush;
+		}
+
+		return res > 0 ? res : 1; // App was minimized, force render path resize
+	}
+	///end
+
+	static x = (): i32 => {
+		///if (is_paint || is_sculpt)
+		return Context.raw.viewIndex == 1 ? Base.appx + Base.w() : Base.appx;
+		///end
+		///if is_lab
+		return Base.appx;
+		///end
+	}
+
+	static y = (): i32 => {
+		return Base.appy;
+	}
+
+	static onResize = () => {
+		if (System.width == 0 || System.height == 0) return;
+
+		let ratioW = System.width / Base.lastWindowWidth;
+		Base.lastWindowWidth = System.width;
+		let ratioH = System.height / Base.lastWindowHeight;
+		Base.lastWindowHeight = System.height;
+
+		Config.raw.layout[LayoutSize.LayoutNodesW] = Math.floor(Config.raw.layout[LayoutSize.LayoutNodesW] * ratioW);
+		///if (is_paint || is_sculpt)
+		Config.raw.layout[LayoutSize.LayoutSidebarH0] = Math.floor(Config.raw.layout[LayoutSize.LayoutSidebarH0] * ratioH);
+		Config.raw.layout[LayoutSize.LayoutSidebarH1] = System.height - Config.raw.layout[LayoutSize.LayoutSidebarH0];
+		///end
+
+		Base.resize();
+
+		///if (krom_linux || krom_darwin)
+		Base.saveWindowRect();
+		///end
+	}
+
+	static saveWindowRect = () => {
+		///if (krom_windows || krom_linux || krom_darwin)
+		Config.raw.window_w = System.width;
+		Config.raw.window_h = System.height;
+		Config.raw.window_x = System.x;
+		Config.raw.window_y = System.y;
+		Config.save();
+		///end
+	}
+
+	static resize = () => {
+		if (System.width == 0 || System.height == 0) return;
+
+		let cam = Scene.active.camera;
+		if (cam.data.raw.ortho != null) {
+			cam.data.raw.ortho[2] = -2 * (App.h() / App.w());
+			cam.data.raw.ortho[3] =  2 * (App.h() / App.w());
+		}
+		cam.buildProjection();
+
+		if (Context.raw.cameraType == CameraType.CameraOrthographic) {
+			Viewport.updateCameraType(Context.raw.cameraType);
+		}
+
+		Context.raw.ddirty = 2;
+
+		if (UIBase.inst.show) {
+			///if (is_paint || is_sculpt)
+			Base.appx = UIToolbar.inst.toolbarw;
+			///end
+			///if is_lab
+			Base.appx = 0;
+			///end
+			Base.appy = UIHeader.headerh * 2;
+			if (Config.raw.layout[LayoutSize.LayoutHeader] == 0) {
+				Base.appy -= UIHeader.headerh;
+			}
+		}
+		else {
+			Base.appx = 0;
+			Base.appy = 0;
+		}
+
+		if (UINodes.inst.grid != null) {
+			let _grid = UINodes.inst.grid;
+			let _next = () => {
+				_grid.unload();
+			}
+			Base.notifyOnNextFrame(_next);
+			UINodes.inst.grid = null;
+		}
+
+		Base.redrawUI();
+	}
+
+	static redrawUI = () => {
+		UIHeader.inst.headerHandle.redraws = 2;
+		UIBase.inst.hwnds[TabArea.TabStatus].redraws = 2;
+		UIMenubar.inst.menuHandle.redraws = 2;
+		UIMenubar.inst.workspaceHandle.redraws = 2;
+		UINodes.inst.hwnd.redraws = 2;
+		UIBox.hwnd.redraws = 2;
+		UIView2D.inst.hwnd.redraws = 2;
+		if (Context.raw.ddirty < 0) Context.raw.ddirty = 0; // Redraw viewport
+		///if (is_paint || is_sculpt)
+		UIBase.inst.hwnds[TabArea.TabSidebar0].redraws = 2;
+		UIBase.inst.hwnds[TabArea.TabSidebar1].redraws = 2;
+		UIToolbar.inst.toolbarHandle.redraws = 2;
+		if (Context.raw.splitView) Context.raw.ddirty = 1;
+		///end
+	}
+
+	static update = () => {
+
+		let mouse = Input.getMouse();
+
+		if (mouse.movementX != 0 || mouse.movementY != 0) {
+			Krom.setMouseCursor(0); // Arrow
+		}
+
+		///if (is_paint || is_sculpt)
+		let hasDrag = Base.dragAsset != null || Base.dragMaterial != null || Base.dragLayer != null || Base.dragFile != null || Base.dragSwatch != null;
+		///end
+		///if is_lab
+		let hasDrag = Base.dragAsset != null || Base.dragFile != null || Base.dragSwatch != null;
+		///end
+
+		if (Config.raw.touch_ui) {
+			// Touch and hold to activate dragging
+			if (Base.dragStart < 0.2) {
+				if (hasDrag && mouse.down()) Base.dragStart += Time.realDelta;
+				else Base.dragStart = 0;
+				hasDrag = false;
+			}
+			if (mouse.released()) {
+				Base.dragStart = 0;
+			}
+			let moved = Math.abs(mouse.movementX) > 1 && Math.abs(mouse.movementY) > 1;
+			if ((mouse.released() || moved) && !hasDrag) {
+				Base.dragAsset = null;
+				Base.dragSwatch = null;
+				Base.dragFile = null;
+				Base.dragFileIcon = null;
+				Base.isDragging = false;
+				///if (is_paint || is_sculpt)
+				Base.dragMaterial = null;
+				Base.dragLayer = null;
+				///end
+			}
+			// Disable touch scrolling while dragging is active
+			Zui.touchScroll = !Base.isDragging;
+		}
+
+		if (hasDrag && (mouse.movementX != 0 || mouse.movementY != 0)) {
+			Base.isDragging = true;
+		}
+		if (mouse.released() && hasDrag) {
+			if (Base.dragAsset != null) {
+				if (Context.inNodes()) { // Create image texture
+					UINodes.inst.acceptAssetDrag(Project.assets.indexOf(Base.dragAsset));
+				}
+				else if (Context.inViewport()) {
+					if (Base.dragAsset.file.toLowerCase().endsWith(".hdr")) {
+						let image = Project.getImage(Base.dragAsset);
+						ImportEnvmap.run(Base.dragAsset.file, image);
+					}
+				}
+				///if (is_paint || is_sculpt)
+				else if (Context.inLayers() || Context.in2dView()) { // Create mask
+					Base.createImageMask(Base.dragAsset);
+				}
+				///end
+				Base.dragAsset = null;
+			}
+			else if (Base.dragSwatch != null) {
+				if (Context.inNodes()) { // Create RGB node
+					UINodes.inst.acceptSwatchDrag(Base.dragSwatch);
+				}
+				else if (Context.inSwatches()) {
+					TabSwatches.acceptSwatchDrag(Base.dragSwatch);
+				}
+				///if (is_paint || is_sculpt)
+				else if (Context.inMaterials()) {
+					TabMaterials.acceptSwatchDrag(Base.dragSwatch);
+				}
+				else if (Context.inViewport()) {
+					let color = Base.dragSwatch.base;
+					color = color_set_ab(color, Base.dragSwatch.opacity * 255);
+					Base.createColorLayer(color, Base.dragSwatch.occlusion, Base.dragSwatch.roughness, Base.dragSwatch.metallic);
+				}
+				else if (Context.inLayers() && TabLayers.canDropNewLayer(Context.raw.dragDestination)) {
+					let color = Base.dragSwatch.base;
+					color = color_set_ab(color, Base.dragSwatch.opacity * 255);
+					Base.createColorLayer(color, Base.dragSwatch.occlusion, Base.dragSwatch.roughness, Base.dragSwatch.metallic, Context.raw.dragDestination);
+				}
+				///end
+
+				Base.dragSwatch = null;
+			}
+			else if (Base.dragFile != null) {
+				if (!Context.inBrowser()) {
+					Base.dropX = mouse.x;
+					Base.dropY = mouse.y;
+
+					///if (is_paint || is_sculpt)
+					let materialCount = Project.materials.length;
+					ImportAsset.run(Base.dragFile, Base.dropX, Base.dropY, true, true, () => {
+						// Asset was material
+						if (Project.materials.length > materialCount) {
+							Base.dragMaterial = Context.raw.material;
+							Base.materialDropped();
+						}
+					});
+					///end
+
+					///if is_lab
+					ImportAsset.run(Base.dragFile, Base.dropX, Base.dropY);
+					///end
+				}
+				Base.dragFile = null;
+				Base.dragFileIcon = null;
+			}
+			///if (is_paint || is_sculpt)
+			else if (Base.dragMaterial != null) {
+				Base.materialDropped();
+			}
+			else if (Base.dragLayer != null) {
+				if (Context.inNodes()) {
+					UINodes.inst.acceptLayerDrag(Project.layers.indexOf(Base.dragLayer));
+				}
+				else if (Context.inLayers() && Base.isDragging) {
+					Base.dragLayer.move(Context.raw.dragDestination);
+					MakeMaterial.parseMeshMaterial();
+				}
+				Base.dragLayer = null;
+			}
+			///end
+
+			Krom.setMouseCursor(0); // Arrow
+			Base.isDragging = false;
+		}
+		if (Context.raw.colorPickerCallback != null && (mouse.released() || mouse.released("right"))) {
+			Context.raw.colorPickerCallback = null;
+			Context.selectTool(Context.raw.colorPickerPreviousTool);
+		}
+
+		Base.handleDropPaths();
+
+		///if (is_paint || is_sculpt)
+		///if krom_windows
+		let isPicker = Context.raw.tool == WorkspaceTool.ToolPicker || Context.raw.tool == WorkspaceTool.ToolMaterial;
+		let decal = Context.raw.tool == WorkspaceTool.ToolDecal || Context.raw.tool == WorkspaceTool.ToolText;
+		Zui.alwaysRedrawWindow = !Context.raw.cacheDraws ||
+			UIMenu.show ||
+			UIBox.show ||
+			Base.isDragging ||
+			isPicker ||
+			decal ||
+			UIView2D.inst.show ||
+			!Config.raw.brush_3d ||
+			Context.raw.frame < 3;
+		///end
+		///end
+
+		if (Zui.alwaysRedrawWindow && Context.raw.ddirty < 0) Context.raw.ddirty = 0;
+	}
+
+	///if (is_paint || is_sculpt)
+	static materialDropped = () => {
+		// Material drag and dropped onto viewport or layers tab
+		if (Context.inViewport()) {
+			let uvType = Input.getKeyboard().down("control") ? UVType.UVProject : UVType.UVMap;
+			let decalMat = uvType == UVType.UVProject ? UtilRender.getDecalMat() : null;
+			Base.createFillLayer(uvType, decalMat);
+		}
+		if (Context.inLayers() && TabLayers.canDropNewLayer(Context.raw.dragDestination)) {
+			let uvType = Input.getKeyboard().down("control") ? UVType.UVProject : UVType.UVMap;
+			let decalMat = uvType == UVType.UVProject ? UtilRender.getDecalMat() : null;
+			Base.createFillLayer(uvType, decalMat, Context.raw.dragDestination);
+		}
+		else if (Context.inNodes()) {
+			UINodes.inst.acceptMaterialDrag(Project.materials.indexOf(Base.dragMaterial));
+		}
+		Base.dragMaterial = null;
+	}
+	///end
+
+	static handleDropPaths = () => {
+		if (Base.dropPaths.length > 0) {
+			let mouse = Input.getMouse();
+			///if (krom_linux || krom_darwin)
+			let wait = !mouse.moved; // Mouse coords not updated during drag
+			///else
+			let wait = false;
+			///end
+			if (!wait) {
+				Base.dropX = mouse.x;
+				Base.dropY = mouse.y;
+				let dropPath = Base.dropPaths.shift();
+				ImportAsset.run(dropPath, Base.dropX, Base.dropY);
+			}
+		}
+	}
+
+	///if (is_paint || is_sculpt)
+	static getDragBackground = (): TRect => {
+		let icons = Res.get("icons.k");
+		if (Base.dragLayer != null && !Base.dragLayer.isGroup() && Base.dragLayer.fill_layer == null) {
+			return Res.tile50(icons, 4, 1);
+		}
+		return null;
+	}
+	///end
+
+	static getDragImage = (): Image => {
+		Base.dragTint = 0xffffffff;
+		Base.dragSize = -1;
+		Base.dragRect = null;
+		if (Base.dragAsset != null) {
+			return Project.getImage(Base.dragAsset);
+		}
+		if (Base.dragSwatch != null) {
+			Base.dragTint = Base.dragSwatch.base;
+			Base.dragSize = 26;
+			return TabSwatches.empty;
+		}
+		if (Base.dragFile != null) {
+			if (Base.dragFileIcon != null) return Base.dragFileIcon;
+			let icons = Res.get("icons.k");
+			Base.dragRect = Base.dragFile.indexOf(".") > 0 ? Res.tile50(icons, 3, 1) : Res.tile50(icons, 2, 1);
+			Base.dragTint = UIBase.inst.ui.t.HIGHLIGHT_COL;
+			return icons;
+		}
+
+		///if is_paint
+		if (Base.dragMaterial != null) {
+			return Base.dragMaterial.imageIcon;
+		}
+		if (Base.dragLayer != null && Base.dragLayer.isGroup()) {
+			let icons = Res.get("icons.k");
+			let folderClosed = Res.tile50(icons, 2, 1);
+			let folderOpen = Res.tile50(icons, 8, 1);
+			Base.dragRect = Base.dragLayer.show_panel ? folderOpen : folderClosed;
+			Base.dragTint = UIBase.inst.ui.t.LABEL_COL - 0x00202020;
+			return icons;
+		}
+		if (Base.dragLayer != null && Base.dragLayer.isMask() && Base.dragLayer.fill_layer == null) {
+			TabLayers.makeMaskPreviewRgba32(Base.dragLayer);
+			return Context.raw.maskPreviewRgba32;
+		}
+		if (Base.dragLayer != null) {
+			return Base.dragLayer.fill_layer != null ? Base.dragLayer.fill_layer.imageIcon : Base.dragLayer.texpaint_preview;
+		}
+		///end
+
+		return null;
+	}
+
+	static render = (g: Graphics2) => {
+		if (System.width == 0 || System.height == 0) return;
+
+		if (Context.raw.frame == 2) {
+			///if (is_paint || is_sculpt)
+			UtilRender.makeMaterialPreview();
+			UIBase.inst.hwnds[TabArea.TabSidebar1].redraws = 2;
+			///end
+
+			MakeMaterial.parseMeshMaterial();
+			MakeMaterial.parsePaintMaterial();
+			Context.raw.ddirty = 0;
+
+			///if (is_paint || is_sculpt)
+			if (History.undoLayers == null) {
+				History.undoLayers = [];
+				for (let i = 0; i < Config.raw.undo_steps; ++i) {
+					let l = new SlotLayer("_undo" + History.undoLayers.length);
+					History.undoLayers.push(l);
+				}
+			}
+			///end
+
+			// Default workspace
+			if (Config.raw.workspace != 0) {
+				UIHeader.inst.worktab.position = Config.raw.workspace;
+				UIMenubar.inst.workspaceHandle.redraws = 2;
+				UIHeader.inst.worktab.changed = true;
+			}
+
+			// Default camera controls
+			Context.raw.cameraControls = Config.raw.camera_controls;
+
+			///if is_lab
+			Base.notifyOnNextFrame(() => {
+				Base.notifyOnNextFrame(() => {
+					TabMeshes.setDefaultMesh(".Sphere");
+				});
+			});
+			///end
+
+			///if is_sculpt
+			Base.notifyOnNextFrame(() => {
+				Base.notifyOnNextFrame(() => {
+					Context.raw.projectType = ProjectModel.ModelSphere;
+					Project.projectNew();
+				});
+			});
+			///end
+		}
+		else if (Context.raw.frame == 3) {
+			Context.raw.ddirty = 3;
+		}
+		Context.raw.frame++;
+
+		let mouse = Input.getMouse();
+		if (Base.isDragging) {
+			Krom.setMouseCursor(1); // Hand
+			let img = Base.getDragImage();
+
+			///if (is_paint || is_sculpt)
+			let scaleFactor = UIBase.inst.ui.SCALE();
+			///end
+			///if is_lab
+			let scaleFactor = Base.uiBox.SCALE();
+			///end
+
+			let size = (Base.dragSize == -1 ? 50 : Base.dragSize) * scaleFactor;
+			let ratio = size / img.width;
+			let h = img.height * ratio;
+
+			///if (is_lab || krom_direct3d11 || krom_direct3d12 || krom_metal || krom_vulkan)
+			let inv = 0;
+			///else
+			let inv = (Base.dragMaterial != null || (Base.dragLayer != null && Base.dragLayer.fill_layer != null)) ? h : 0;
+			///end
+
+			g.color = Base.dragTint;
+
+			///if (is_paint || is_sculpt)
+			let bgRect = Base.getDragBackground();
+			if (bgRect != null) {
+				g.drawScaledSubImage(Res.get("icons.k"), bgRect.x, bgRect.y, bgRect.w, bgRect.h, mouse.x + Base.dragOffX, mouse.y + Base.dragOffY + inv, size, h - inv * 2);
+			}
+			///end
+
+			Base.dragRect == null ?
+				g.drawScaledImage(img, mouse.x + Base.dragOffX, mouse.y + Base.dragOffY + inv, size, h - inv * 2) :
+				g.drawScaledSubImage(img, Base.dragRect.x, Base.dragRect.y, Base.dragRect.w, Base.dragRect.h, mouse.x + Base.dragOffX, mouse.y + Base.dragOffY + inv, size, h - inv * 2);
+			g.color = 0xffffffff;
+		}
+
+		let usingMenu = UIMenu.show && mouse.y > UIHeader.headerh;
+		Base.uiEnabled = !UIBox.show && !usingMenu && !Base.isComboSelected();
+		if (UIBox.show) UIBox.render(g);
+		if (UIMenu.show) UIMenu.render(g);
+
+		// Save last pos for continuos paint
+		Context.raw.lastPaintVecX = Context.raw.paintVec.x;
+		Context.raw.lastPaintVecY = Context.raw.paintVec.y;
+
+		///if (krom_android || krom_ios)
+		// No mouse move events for touch, re-init last paint position on touch start
+		if (!mouse.down()) {
+			Context.raw.lastPaintX = -1;
+			Context.raw.lastPaintY = -1;
+		}
+		///end
+	}
+
+	static enumTexts = (nodeType: string): string[] => {
+		///if (is_paint || is_sculpt)
+		if (nodeType == "TEX_IMAGE") {
+			return Project.assetNames.length > 0 ? Project.assetNames : [""];
+		}
+		if (nodeType == "LAYER" || nodeType == "LAYER_MASK") {
+			let layerNames: string[] = [];
+			for (let l of Project.layers) layerNames.push(l.name);
+			return layerNames;
+		}
+		if (nodeType == "MATERIAL") {
+			let materialNames: string[] = [];
+			for (let m of Project.materials) materialNames.push(m.canvas.name);
+			return materialNames;
+		}
+		///end
+
+		///if is_lab
+		if (nodeType == "ImageTextureNode") {
+			return Project.assetNames.length > 0 ? Project.assetNames : [""];
+		}
+		///end
+
+		return null;
+	}
+
+	static getAssetIndex = (fileName: string): i32 => {
+		let i = Project.assetNames.indexOf(fileName);
+		return i >= 0 ? i : 0;
+	}
+
+	static notifyOnNextFrame = (f: ()=>void) => {
+		let _render = (_: any) => {
+			App.notifyOnInit(() => {
+				let _update = () => {
+					App.notifyOnInit(f);
+					App.removeUpdate(_update);
+				}
+				App.notifyOnUpdate(_update);
+			});
+			App.removeRender(_render);
+		}
+		App.notifyOnRender(_render);
+	}
+
+	static toggleFullscreen = () => {
+		if (System.mode == WindowMode.Windowed) {
+			///if (krom_windows || krom_linux || krom_darwin)
+			Config.raw.window_w = System.width;
+			Config.raw.window_h = System.height;
+			Config.raw.window_x = System.x;
+			Config.raw.window_y = System.y;
+			///end
+			System.mode = WindowMode.Fullscreen;
+		}
+		else {
+			System.mode = WindowMode.Windowed;
+			System.resize(Config.raw.window_w, Config.raw.window_h);
+			System.move(Config.raw.window_x, Config.raw.window_y);
+		}
+	}
+
+	static isScrolling = (): bool => {
+		for (let ui of Base.getUIs()) if (ui.isScrolling) return true;
+		return false;
+	}
+
+	static isComboSelected = (): bool => {
+		for (let ui of Base.getUIs()) if (ui.comboSelectedHandle_ptr != null) return true;
+		return false;
+	}
+
+	static getUIs = (): Zui[] => {
+		return [Base.uiBox, Base.uiMenu, UIBase.inst.ui, UINodes.inst.ui, UIView2D.inst.ui];
+	}
+
+	static isDecalLayer = (): bool => {
+		///if is_paint
+		let isPaint = Context.raw.tool != WorkspaceTool.ToolMaterial && Context.raw.tool != WorkspaceTool.ToolBake;
+		return isPaint && Context.raw.layer.fill_layer != null && Context.raw.layer.uvType == UVType.UVProject;
+		///end
+
+		///if (is_sculpt || is_lab)
+		return false;
+		///end
+	}
+
+	static redrawStatus = () => {
+		if (UIStatus.inst != null) {
+			UIBase.inst.hwnds[TabArea.TabStatus].redraws = 2;
+		}
+	}
+
+	static redrawConsole = () => {
+		let statush = Config.raw.layout[LayoutSize.LayoutStatusH];
+		if (UIStatus.inst != null && UIBase.inst != null && UIBase.inst.ui != null && statush > UIStatus.defaultStatusH * UIBase.inst.ui.SCALE()) {
+			UIBase.inst.hwnds[TabArea.TabStatus].redraws = 2;
+		}
+	}
+
+	static initLayout = () => {
+		let show2d = (UINodes.inst != null && UINodes.inst.show) || (UIView2D.inst != null && UIView2D.inst.show);
+
+		let raw = Config.raw;
+		raw.layout = [
+			///if (is_paint || is_sculpt)
+			Math.floor(UIBase.defaultSidebarW * raw.window_scale), // LayoutSidebarW
+			Math.floor(System.height / 2), // LayoutSidebarH0
+			Math.floor(System.height / 2), // LayoutSidebarH1
+			///end
+
+			///if krom_ios
+			show2d ? Math.floor((App.w() + raw.layout[LayoutSize.LayoutNodesW]) * 0.473) : Math.floor(App.w() * 0.473), // LayoutNodesW
+			///elseif krom_android
+			show2d ? Math.floor((App.w() + raw.layout[LayoutSize.LayoutNodesW]) * 0.473) : Math.floor(App.w() * 0.473),
+			///else
+			show2d ? Math.floor((App.w() + raw.layout[LayoutSize.LayoutNodesW]) * 0.515) : Math.floor(App.w() * 0.515), // Align with ui header controls
+			///end
+
+			Math.floor(App.h() / 2), // LayoutNodesH
+			Math.floor(UIStatus.defaultStatusH * raw.window_scale), // LayoutStatusH
+
+			///if (krom_android || krom_ios)
+			0, // LayoutHeader
+			///else
+			1,
+			///end
+		];
+
+		raw.layout_tabs = [
+			///if (is_paint || is_sculpt)
+			0,
+			0,
+			///end
+			0
+		];
+	}
+
+	static initConfig = () => {
+		let raw = Config.raw;
+		raw.recent_projects = [];
+		raw.bookmarks = [];
+		raw.plugins = [];
+		///if (krom_android || krom_ios)
+		raw.keymap = "touch.json";
+		///else
+		raw.keymap = "default.json";
+		///end
+		raw.theme = "default.json";
+		raw.server = "https://armorpaint.fra1.digitaloceanspaces.com";
+		raw.undo_steps = 4;
+		raw.pressure_radius = true;
+		raw.pressure_sensitivity = 1.0;
+		raw.camera_zoom_speed = 1.0;
+		raw.camera_pan_speed = 1.0;
+		raw.camera_rotation_speed = 1.0;
+		raw.zoom_direction = ZoomDirection.ZoomVertical;
+		///if (is_paint || is_sculpt)
+		raw.displace_strength = 0.0;
+		///else
+		raw.displace_strength = 1.0;
+		///end
+		raw.wrap_mouse = false;
+		///if is_paint
+		raw.workspace = SpaceType.Space3D;
+		///end
+		///if is_sculpt
+		raw.workspace = SpaceType.Space3D;
+		///end
+		///if is_lab
+		raw.workspace = SpaceType.Space2D;
+		///end
+		///if (krom_android || krom_ios)
+		raw.camera_controls = CameraControls.ControlsRotate;
+		///else
+		raw.camera_controls = CameraControls.ControlsOrbit;
+		///end
+		raw.layer_res = TextureRes.Res2048;
+		///if (krom_android || krom_ios)
+		raw.touch_ui = true;
+		raw.splash_screen = true;
+		///else
+		raw.touch_ui = false;
+		raw.splash_screen = false;
+		///end
+		///if (is_paint || is_sculpt)
+		raw.node_preview = true;
+		///else
+		raw.node_preview = false;
+		///end
+
+		///if (is_paint || is_sculpt)
+		raw.pressure_hardness = true;
+		raw.pressure_angle = false;
+		raw.pressure_opacity = false;
+		///if (krom_vulkan || krom_ios)
+		raw.material_live = false;
+		///else
+		raw.material_live = true;
+		///end
+		raw.brush_3d = true;
+		raw.brush_depth_reject = true;
+		raw.brush_angle_reject = true;
+		raw.brush_live = false;
+		raw.show_asset_names = false;
+		///end
+
+		///if is_paint
+		raw.dilate = DilateType.DilateInstant;
+		raw.dilate_radius = 2;
+		///end
+
+		///if is_lab
+		raw.gpu_inference = true;
+		///end
+	}
+
+	static initLayers = () => {
+		///if (is_paint || is_sculpt)
+		Project.layers[0].clear(color_from_floats(Base.defaultBase, Base.defaultBase, Base.defaultBase, 1.0));
+		///end
+
+		///if is_lab
+		let texpaint = RenderPath.active.renderTargets.get("texpaint").image;
+		let texpaint_nor = RenderPath.active.renderTargets.get("texpaint_nor").image;
+		let texpaint_pack = RenderPath.active.renderTargets.get("texpaint_pack").image;
+		texpaint.g2.begin(false);
+		texpaint.g2.drawScaledImage(Res.get("placeholder.k"), 0, 0, Config.getTextureResX(), Config.getTextureResY()); // Base
+		texpaint.g2.end();
+		texpaint_nor.g4.begin();
+		texpaint_nor.g4.clear(color_from_floats(0.5, 0.5, 1.0, 0.0)); // Nor
+		texpaint_nor.g4.end();
+		texpaint_pack.g4.begin();
+		texpaint_pack.g4.clear(color_from_floats(1.0, 0.4, 0.0, 0.0)); // Occ, rough, met
+		texpaint_pack.g4.end();
+		let texpaint_nor_empty = RenderPath.active.renderTargets.get("texpaint_nor_empty").image;
+		let texpaint_pack_empty = RenderPath.active.renderTargets.get("texpaint_pack_empty").image;
+		texpaint_nor_empty.g4.begin();
+		texpaint_nor_empty.g4.clear(color_from_floats(0.5, 0.5, 1.0, 0.0)); // Nor
+		texpaint_nor_empty.g4.end();
+		texpaint_pack_empty.g4.begin();
+		texpaint_pack_empty.g4.clear(color_from_floats(1.0, 0.4, 0.0, 0.0)); // Occ, rough, met
+		texpaint_pack_empty.g4.end();
+		///end
+	}
+
+	///if (is_paint || is_sculpt)
+	static resizeLayers = () => {
+		let C = Config.raw;
+		if (Base.resHandle.position >= Math.floor(TextureRes.Res16384)) { // Save memory for >=16k
+			C.undo_steps = 1;
+			if (Context.raw.undoHandle != null) {
+				Context.raw.undoHandle.value = C.undo_steps;
+			}
+			while (History.undoLayers.length > C.undo_steps) {
+				let l = History.undoLayers.pop();
+				Base.notifyOnNextFrame(() => {
+					l.unload();
+				});
+			}
+		}
+		for (let l of Project.layers) l.resizeAndSetBits();
+		for (let l of History.undoLayers) l.resizeAndSetBits();
+		let rts = RenderPath.active.renderTargets;
+		let _texpaint_blend0 = rts.get("texpaint_blend0").image;
+		Base.notifyOnNextFrame(() => {
+			_texpaint_blend0.unload();
+		});
+		rts.get("texpaint_blend0").raw.width = Config.getTextureResX();
+		rts.get("texpaint_blend0").raw.height = Config.getTextureResY();
+		rts.get("texpaint_blend0").image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY(), TextureFormat.R8);
+		let _texpaint_blend1 = rts.get("texpaint_blend1").image;
+		Base.notifyOnNextFrame(() => {
+			_texpaint_blend1.unload();
+		});
+		rts.get("texpaint_blend1").raw.width = Config.getTextureResX();
+		rts.get("texpaint_blend1").raw.height = Config.getTextureResY();
+		rts.get("texpaint_blend1").image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY(), TextureFormat.R8);
+		Context.raw.brushBlendDirty = true;
+		if (rts.get("texpaint_blur") != null) {
+			let _texpaint_blur = rts.get("texpaint_blur").image;
+			Base.notifyOnNextFrame(() => {
+				_texpaint_blur.unload();
+			});
+			let sizeX = Math.floor(Config.getTextureResX() * 0.95);
+			let sizeY = Math.floor(Config.getTextureResY() * 0.95);
+			rts.get("texpaint_blur").raw.width = sizeX;
+			rts.get("texpaint_blur").raw.height = sizeY;
+			rts.get("texpaint_blur").image = Image.createRenderTarget(sizeX, sizeY);
+		}
+		if (RenderPathPaint.liveLayer != null) RenderPathPaint.liveLayer.resizeAndSetBits();
+		///if (krom_direct3d12 || krom_vulkan || krom_metal)
+		RenderPathRaytrace.ready = false; // Rebuild baketex
+		///end
+		Context.raw.ddirty = 2;
+	}
+
+	static setLayerBits = () => {
+		for (let l of Project.layers) l.resizeAndSetBits();
+		for (let l of History.undoLayers) l.resizeAndSetBits();
+	}
+
+	static makeMergePipe = (red: bool, green: bool, blue: bool, alpha: bool): PipelineState => {
+		let pipe = new PipelineState();
+		pipe.vertexShader = System.getShader("pass.vert");
+		pipe.fragmentShader = System.getShader("layer_merge.frag");
+		let vs = new VertexStructure();
+		vs.add("pos", VertexData.F32_2X);
+		pipe.inputLayout = [vs];
+		pipe.colorWriteMasksRed = [red];
+		pipe.colorWriteMasksGreen = [green];
+		pipe.colorWriteMasksBlue = [blue];
+		pipe.colorWriteMasksAlpha = [alpha];
+		pipe.compile();
+		return pipe;
+	}
+	///end
+
+	static makePipe = () => {
+		///if (is_paint || is_sculpt)
+		Base.pipeMerge = Base.makeMergePipe(true, true, true, true);
+		Base.pipeMergeR = Base.makeMergePipe(true, false, false, false);
+		Base.pipeMergeG = Base.makeMergePipe(false, true, false, false);
+		Base.pipeMergeB = Base.makeMergePipe(false, false, true, false);
+		Base.pipeMergeA = Base.makeMergePipe(false, false, false, true);
+		Base.tex0 = Base.pipeMerge.getTextureUnit("tex0"); // Always binding texpaint.a for blending
+		Base.tex1 = Base.pipeMerge.getTextureUnit("tex1");
+		Base.texmask = Base.pipeMerge.getTextureUnit("texmask");
+		Base.texa = Base.pipeMerge.getTextureUnit("texa");
+		Base.opac = Base.pipeMerge.getConstantLocation("opac");
+		Base.blending = Base.pipeMerge.getConstantLocation("blending");
+		///end
+
+		{
+			Base.pipeCopy = new PipelineState();
+			Base.pipeCopy.vertexShader = System.getShader("layer_view.vert");
+			Base.pipeCopy.fragmentShader = System.getShader("layer_copy.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_3X);
+			vs.add("tex", VertexData.F32_2X);
+			vs.add("col", VertexData.U8_4X_Normalized);
+			Base.pipeCopy.inputLayout = [vs];
+			Base.pipeCopy.compile();
+		}
+
+		{
+			Base.pipeCopyBGRA = new PipelineState();
+			Base.pipeCopyBGRA.vertexShader = System.getShader("layer_view.vert");
+			Base.pipeCopyBGRA.fragmentShader = System.getShader("layer_copy_bgra.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_3X);
+			vs.add("tex", VertexData.F32_2X);
+			vs.add("col", VertexData.U8_4X_Normalized);
+			Base.pipeCopyBGRA.inputLayout = [vs];
+			Base.pipeCopyBGRA.compile();
+		}
+
+		///if (krom_metal || krom_vulkan || krom_direct3d12)
+		{
+			Base.pipeCopy8 = new PipelineState();
+			Base.pipeCopy8.vertexShader = System.getShader("layer_view.vert");
+			Base.pipeCopy8.fragmentShader = System.getShader("layer_copy.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_3X);
+			vs.add("tex", VertexData.F32_2X);
+			vs.add("col", VertexData.U8_4X_Normalized);
+			Base.pipeCopy8.inputLayout = [vs];
+			Base.pipeCopy8.colorAttachmentCount = 1;
+			Base.pipeCopy8.colorAttachments[0] = TextureFormat.R8;
+			Base.pipeCopy8.compile();
+		}
+
+		{
+			Base.pipeCopy128 = new PipelineState();
+			Base.pipeCopy128.vertexShader = System.getShader("layer_view.vert");
+			Base.pipeCopy128.fragmentShader = System.getShader("layer_copy.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_3X);
+			vs.add("tex", VertexData.F32_2X);
+			vs.add("col", VertexData.U8_4X_Normalized);
+			Base.pipeCopy128.inputLayout = [vs];
+			Base.pipeCopy128.colorAttachmentCount = 1;
+			Base.pipeCopy128.colorAttachments[0] = TextureFormat.RGBA128;
+			Base.pipeCopy128.compile();
+		}
+		///else
+		Base.pipeCopy8 = Base.pipeCopy;
+		Base.pipeCopy128 = Base.pipeCopy;
+		///end
+
+		///if (is_paint || is_sculpt)
+		{
+			Base.pipeInvert8 = new PipelineState();
+			Base.pipeInvert8.vertexShader = System.getShader("layer_view.vert");
+			Base.pipeInvert8.fragmentShader = System.getShader("layer_invert.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_3X);
+			vs.add("tex", VertexData.F32_2X);
+			vs.add("col", VertexData.U8_4X_Normalized);
+			Base.pipeInvert8.inputLayout = [vs];
+			Base.pipeInvert8.colorAttachmentCount = 1;
+			Base.pipeInvert8.colorAttachments[0] = TextureFormat.R8;
+			Base.pipeInvert8.compile();
+		}
+
+		{
+			Base.pipeApplyMask = new PipelineState();
+			Base.pipeApplyMask.vertexShader = System.getShader("pass.vert");
+			Base.pipeApplyMask.fragmentShader = System.getShader("mask_apply.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_2X);
+			Base.pipeApplyMask.inputLayout = [vs];
+			Base.pipeApplyMask.compile();
+			Base.tex0Mask = Base.pipeApplyMask.getTextureUnit("tex0");
+			Base.texaMask = Base.pipeApplyMask.getTextureUnit("texa");
+		}
+
+		{
+			Base.pipeMergeMask = new PipelineState();
+			Base.pipeMergeMask.vertexShader = System.getShader("pass.vert");
+			Base.pipeMergeMask.fragmentShader = System.getShader("mask_merge.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_2X);
+			Base.pipeMergeMask.inputLayout = [vs];
+			Base.pipeMergeMask.compile();
+			Base.tex0MergeMask = Base.pipeMergeMask.getTextureUnit("tex0");
+			Base.texaMergeMask = Base.pipeMergeMask.getTextureUnit("texa");
+			Base.opacMergeMask = Base.pipeMergeMask.getConstantLocation("opac");
+			Base.blendingMergeMask = Base.pipeMergeMask.getConstantLocation("blending");
+		}
+
+		{
+			Base.pipeColorIdToMask = new PipelineState();
+			Base.pipeColorIdToMask.vertexShader = System.getShader("pass.vert");
+			Base.pipeColorIdToMask.fragmentShader = System.getShader("mask_colorid.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_2X);
+			Base.pipeColorIdToMask.inputLayout = [vs];
+			Base.pipeColorIdToMask.compile();
+			Base.texpaintColorId = Base.pipeColorIdToMask.getTextureUnit("texpaint_colorid");
+			Base.texColorId = Base.pipeColorIdToMask.getTextureUnit("texcolorid");
+		}
+		///end
+
+		///if is_lab
+		{
+			Base.pipeCopyR = new PipelineState();
+			Base.pipeCopyR.vertexShader = System.getShader("layer_view.vert");
+			Base.pipeCopyR.fragmentShader = System.getShader("layer_copy.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_3X);
+			vs.add("tex", VertexData.F32_2X);
+			vs.add("col", VertexData.U8_4X_Normalized);
+			Base.pipeCopyR.inputLayout = [vs];
+			Base.pipeCopyR.colorWriteMasksGreen = [false];
+			Base.pipeCopyR.colorWriteMasksBlue = [false];
+			Base.pipeCopyR.colorWriteMasksAlpha = [false];
+			Base.pipeCopyR.compile();
+		}
+
+		{
+			Base.pipeCopyG = new PipelineState();
+			Base.pipeCopyG.vertexShader = System.getShader("layer_view.vert");
+			Base.pipeCopyG.fragmentShader = System.getShader("layer_copy.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_3X);
+			vs.add("tex", VertexData.F32_2X);
+			vs.add("col", VertexData.U8_4X_Normalized);
+			Base.pipeCopyG.inputLayout = [vs];
+			Base.pipeCopyG.colorWriteMasksRed = [false];
+			Base.pipeCopyG.colorWriteMasksBlue = [false];
+			Base.pipeCopyG.colorWriteMasksAlpha = [false];
+			Base.pipeCopyG.compile();
+		}
+
+		{
+			Base.pipeCopyB = new PipelineState();
+			Base.pipeCopyB.vertexShader = System.getShader("layer_view.vert");
+			Base.pipeCopyB.fragmentShader = System.getShader("layer_copy.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_3X);
+			vs.add("tex", VertexData.F32_2X);
+			vs.add("col", VertexData.U8_4X_Normalized);
+			Base.pipeCopyB.inputLayout = [vs];
+			Base.pipeCopyB.colorWriteMasksRed = [false];
+			Base.pipeCopyB.colorWriteMasksGreen = [false];
+			Base.pipeCopyB.colorWriteMasksAlpha = [false];
+			Base.pipeCopyB.compile();
+		}
+
+		{
+			Base.pipeInpaintPreview = new PipelineState();
+			Base.pipeInpaintPreview.vertexShader = System.getShader("pass.vert");
+			Base.pipeInpaintPreview.fragmentShader = System.getShader("inpaint_preview.frag");
+			let vs = new VertexStructure();
+			vs.add("pos", VertexData.F32_2X);
+			Base.pipeInpaintPreview.inputLayout = [vs];
+			Base.pipeInpaintPreview.compile();
+			Base.tex0InpaintPreview = Base.pipeInpaintPreview.getTextureUnit("tex0");
+			Base.texaInpaintPreview = Base.pipeInpaintPreview.getTextureUnit("texa");
+		}
+		///end
+	}
+
+	static makePipeCopyRGB = () => {
+		Base.pipeCopyRGB = new PipelineState();
+		Base.pipeCopyRGB.vertexShader = System.getShader("layer_view.vert");
+		Base.pipeCopyRGB.fragmentShader = System.getShader("layer_copy.frag");
+		let vs = new VertexStructure();
+		vs.add("pos", VertexData.F32_3X);
+		vs.add("tex", VertexData.F32_2X);
+		vs.add("col", VertexData.U8_4X_Normalized);
+		Base.pipeCopyRGB.inputLayout = [vs];
+		Base.pipeCopyRGB.colorWriteMasksAlpha = [false];
+		Base.pipeCopyRGB.compile();
+	}
+
+	///if is_lab
+	static makePipeCopyA = () => {
+		Base.pipeCopyA = new PipelineState();
+		Base.pipeCopyA.vertexShader = System.getShader("pass.vert");
+		Base.pipeCopyA.fragmentShader = System.getShader("layer_copy_rrrr.frag");
+		let vs = new VertexStructure();
+		vs.add("pos", VertexData.F32_2X);
+		Base.pipeCopyA.inputLayout = [vs];
+		Base.pipeCopyA.colorWriteMasksRed = [false];
+		Base.pipeCopyA.colorWriteMasksGreen = [false];
+		Base.pipeCopyA.colorWriteMasksBlue = [false];
+		Base.pipeCopyA.compile();
+		Base.pipeCopyATex = Base.pipeCopyA.getTextureUnit("tex");
+	}
+	///end
+
+	static makeCursorPipe = () => {
+		Base.pipeCursor = new PipelineState();
+		Base.pipeCursor.vertexShader = System.getShader("cursor.vert");
+		Base.pipeCursor.fragmentShader = System.getShader("cursor.frag");
+		let vs = new VertexStructure();
+		///if (krom_metal || krom_vulkan)
+		vs.add("tex", VertexData.I16_2X_Normalized);
+		///else
+		vs.add("pos", VertexData.I16_4X_Normalized);
+		vs.add("nor", VertexData.I16_2X_Normalized);
+		vs.add("tex", VertexData.I16_2X_Normalized);
+		///end
+		Base.pipeCursor.inputLayout = [vs];
+		Base.pipeCursor.blendSource = BlendingFactor.SourceAlpha;
+		Base.pipeCursor.blendDestination = BlendingFactor.InverseSourceAlpha;
+		Base.pipeCursor.depthWrite = false;
+		Base.pipeCursor.depthMode = CompareMode.Always;
+		Base.pipeCursor.compile();
+		Base.cursorVP = Base.pipeCursor.getConstantLocation("VP");
+		Base.cursorInvVP = Base.pipeCursor.getConstantLocation("invVP");
+		Base.cursorMouse = Base.pipeCursor.getConstantLocation("mouse");
+		Base.cursorTexStep = Base.pipeCursor.getConstantLocation("texStep");
+		Base.cursorRadius = Base.pipeCursor.getConstantLocation("radius");
+		Base.cursorCameraRight = Base.pipeCursor.getConstantLocation("cameraRight");
+		Base.cursorTint = Base.pipeCursor.getConstantLocation("tint");
+		Base.cursorGbufferD = Base.pipeCursor.getTextureUnit("gbufferD");
+		Base.cursorTex = Base.pipeCursor.getTextureUnit("tex");
+	}
+
+	static makeTempImg = () => {
+		///if (is_paint || is_sculpt)
+		let l = Project.layers[0];
+		///end
+		///if is_lab
+		let l = BrushOutputNode.inst;
+		///end
+
+		if (Base.tempImage != null && (Base.tempImage.width != l.texpaint.width || Base.tempImage.height != l.texpaint.height || Base.tempImage.format != l.texpaint.format)) {
+			let _temptex0 = RenderPath.active.renderTargets.get("temptex0");
+			Base.notifyOnNextFrame(() => {
+				_temptex0.unload();
+			});
+			RenderPath.active.renderTargets.delete("temptex0");
+			Base.tempImage = null;
+		}
+		if (Base.tempImage == null) {
+			///if (is_paint || is_sculpt)
+			let format = Base.bitsHandle.position == TextureBits.Bits8  ? "RGBA32" :
+					 	 Base.bitsHandle.position == TextureBits.Bits16 ? "RGBA64" :
+					 										  			  "RGBA128";
+			///end
+			///if is_lab
+			let format = "RGBA32";
+			///end
+
+			let t = new RenderTargetRaw();
+			t.name = "temptex0";
+			t.width = l.texpaint.width;
+			t.height = l.texpaint.height;
+			t.format = format;
+			let rt = RenderPath.active.createRenderTarget(t);
+			Base.tempImage = rt.image;
+		}
+	}
+
+	///if (is_paint || is_sculpt)
+	static makeTempMaskImg = () => {
+		if (Base.tempMaskImage != null && (Base.tempMaskImage.width != Config.getTextureResX() || Base.tempMaskImage.height != Config.getTextureResY())) {
+			let _tempMaskImage = Base.tempMaskImage;
+			Base.notifyOnNextFrame(() => {
+				_tempMaskImage.unload();
+			});
+			Base.tempMaskImage = null;
+		}
+		if (Base.tempMaskImage == null) {
+			Base.tempMaskImage = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY(), TextureFormat.R8);
+		}
+	}
+	///end
+
+	static makeExportImg = () => {
+		///if (is_paint || is_sculpt)
+		let l = Project.layers[0];
+		///end
+		///if is_lab
+		let l = BrushOutputNode.inst;
+		///end
+
+		if (Base.expa != null && (Base.expa.width != l.texpaint.width || Base.expa.height != l.texpaint.height || Base.expa.format != l.texpaint.format)) {
+			let _expa = Base.expa;
+			let _expb = Base.expb;
+			let _expc = Base.expc;
+			Base.notifyOnNextFrame(() => {
+				_expa.unload();
+				_expb.unload();
+				_expc.unload();
+			});
+			Base.expa = null;
+			Base.expb = null;
+			Base.expc = null;
+			RenderPath.active.renderTargets.delete("expa");
+			RenderPath.active.renderTargets.delete("expb");
+			RenderPath.active.renderTargets.delete("expc");
+		}
+		if (Base.expa == null) {
+			///if (is_paint || is_sculpt)
+			let format = Base.bitsHandle.position == TextureBits.Bits8  ? "RGBA32" :
+					 	 Base.bitsHandle.position == TextureBits.Bits16 ? "RGBA64" :
+					 										  			  "RGBA128";
+			///end
+			///if is_lab
+			let format = "RGBA32";
+			///end
+
+			{
+				let t = new RenderTargetRaw();
+				t.name = "expa";
+				t.width = l.texpaint.width;
+				t.height = l.texpaint.height;
+				t.format = format;
+				let rt = RenderPath.active.createRenderTarget(t);
+				Base.expa = rt.image;
+			}
+
+			{
+				let t = new RenderTargetRaw();
+				t.name = "expb";
+				t.width = l.texpaint.width;
+				t.height = l.texpaint.height;
+				t.format = format;
+				let rt = RenderPath.active.createRenderTarget(t);
+				Base.expb = rt.image;
+			}
+
+			{
+				let t = new RenderTargetRaw();
+				t.name = "expc";
+				t.width = l.texpaint.width;
+				t.height = l.texpaint.height;
+				t.format = format;
+				let rt = RenderPath.active.createRenderTarget(t);
+				Base.expc = rt.image;
+			}
+		}
+	}
+
+	///if (is_paint || is_sculpt)
+	static duplicateLayer = (l: SlotLayer) => {
+		if (!l.isGroup()) {
+			let newLayer = l.duplicate();
+			Context.setLayer(newLayer);
+			let masks = l.getMasks(false);
+			if (masks != null) {
+				for (let m of masks) {
+					m = m.duplicate();
+					m.parent = newLayer;
+					array_remove(Project.layers, m);
+					Project.layers.splice(Project.layers.indexOf(newLayer), 0, m);
+				}
+			}
+			Context.setLayer(newLayer);
+		}
+		else {
+			let newGroup = Base.newGroup();
+			array_remove(Project.layers, newGroup);
+			Project.layers.splice(Project.layers.indexOf(l) + 1, 0, newGroup);
+			// group.show_panel = true;
+			for (let c of l.getChildren()) {
+				let masks = c.getMasks(false);
+				let newLayer = c.duplicate();
+				newLayer.parent = newGroup;
+				array_remove(Project.layers, newLayer);
+				Project.layers.splice(Project.layers.indexOf(newGroup), 0, newLayer);
+				if (masks != null) {
+					for (let m of masks) {
+						let newMask = m.duplicate();
+						newMask.parent = newLayer;
+						array_remove(Project.layers, newMask);
+						Project.layers.splice(Project.layers.indexOf(newLayer), 0, newMask);
+					}
+				}
+			}
+			let groupMasks = l.getMasks();
+			if (groupMasks != null) {
+				for (let m of groupMasks) {
+					let newMask = m.duplicate();
+					newMask.parent = newGroup;
+					array_remove(Project.layers, newMask);
+					Project.layers.splice(Project.layers.indexOf(newGroup), 0, newMask);
+				}
+			}
+			Context.setLayer(newGroup);
+		}
+	}
+
+	static applyMasks = (l: SlotLayer) => {
+		let masks = l.getMasks();
+
+		if (masks != null) {
+			for (let i = 0; i < masks.length - 1; ++i) {
+				Base.mergeLayer(masks[i + 1], masks[i]);
+				masks[i].delete();
+			}
+			masks[masks.length - 1].applyMask();
+			Context.raw.layerPreviewDirty = true;
+		}
+	}
+
+	static mergeDown = () => {
+		let l1 = Context.raw.layer;
+
+		if (l1.isGroup()) {
+			l1 = Base.mergeGroup(l1);
+		}
+		else if (l1.hasMasks()) { // It is a layer
+			Base.applyMasks(l1);
+			Context.setLayer(l1);
+		}
+
+		let l0 = Project.layers[Project.layers.indexOf(l1) - 1];
+
+		if (l0.isGroup()) {
+			l0 = Base.mergeGroup(l0);
+		}
+		else if (l0.hasMasks()) { // It is a layer
+			Base.applyMasks(l0);
+			Context.setLayer(l0);
+		}
+
+		Base.mergeLayer(l0, l1);
+		l1.delete();
+		Context.setLayer(l0);
+		Context.raw.layerPreviewDirty = true;
+	}
+
+	static mergeGroup = (l: SlotLayer) => {
+		if (!l.isGroup()) return null;
+
+		let children = l.getChildren();
+
+		if (children.length == 1 && children[0].hasMasks(false)) {
+			Base.applyMasks(children[0]);
+		}
+
+		for (let i = 0; i < children.length - 1; ++i) {
+			Context.setLayer(children[children.length - 1 - i]);
+			History.mergeLayers();
+			Base.mergeDown();
+		}
+
+		// Now apply the group masks
+		let masks = l.getMasks();
+		if (masks != null) {
+			for (let i = 0; i < masks.length - 1; ++i) {
+				Base.mergeLayer(masks[i + 1], masks[i]);
+				masks[i].delete();
+			}
+			Base.applyMask(children[0], masks[masks.length - 1]);
+		}
+
+		children[0].parent = null;
+		children[0].name = l.name;
+		if (children[0].fill_layer != null) children[0].toPaintLayer();
+		l.delete();
+		return children[0];
+	}
+
+	static mergeLayer = (l0 : SlotLayer, l1: SlotLayer, use_mask = false) => {
+		if (!l1.visible || l1.isGroup()) return;
+
+		if (Base.pipeMerge == null) Base.makePipe();
+		Base.makeTempImg();
+		if (ConstData.screenAlignedVB == null) ConstData.createScreenAlignedData();
+
+		Base.tempImage.g2.begin(false); // Copy to temp
+		Base.tempImage.g2.pipeline = Base.pipeCopy;
+		Base.tempImage.g2.drawImage(l0.texpaint, 0, 0);
+		Base.tempImage.g2.pipeline = null;
+		Base.tempImage.g2.end();
+
+		let empty = RenderPath.active.renderTargets.get("empty_white").image;
+		let mask = empty;
+		let l1masks =  use_mask ? l1.getMasks() : null;
+		if (l1masks != null) {
+			// for (let i = 1; i < l1masks.length - 1; ++i) {
+			// 	mergeLayer(l1masks[i + 1], l1masks[i]);
+			// }
+			mask = l1masks[0].texpaint;
+		}
+
+		if (l1.isMask()) {
+			l0.texpaint.g4.begin();
+			l0.texpaint.g4.setPipeline(Base.pipeMergeMask);
+			l0.texpaint.g4.setTexture(Base.tex0MergeMask, l1.texpaint);
+			l0.texpaint.g4.setTexture(Base.texaMergeMask, Base.tempImage);
+			l0.texpaint.g4.setFloat(Base.opacMergeMask, l1.getOpacity());
+			l0.texpaint.g4.setInt(Base.blendingMergeMask, l1.blending);
+			l0.texpaint.g4.setVertexBuffer(ConstData.screenAlignedVB);
+			l0.texpaint.g4.setIndexBuffer(ConstData.screenAlignedIB);
+			l0.texpaint.g4.drawIndexedVertices();
+			l0.texpaint.g4.end();
+		}
+
+		if (l1.isLayer()) {
+			if (l1.paintBase) {
+				l0.texpaint.g4.begin();
+				l0.texpaint.g4.setPipeline(Base.pipeMerge);
+				l0.texpaint.g4.setTexture(Base.tex0, l1.texpaint);
+				l0.texpaint.g4.setTexture(Base.tex1, empty);
+				l0.texpaint.g4.setTexture(Base.texmask, mask);
+				l0.texpaint.g4.setTexture(Base.texa, Base.tempImage);
+				l0.texpaint.g4.setFloat(Base.opac, l1.getOpacity());
+				l0.texpaint.g4.setInt(Base.blending, l1.blending);
+				l0.texpaint.g4.setVertexBuffer(ConstData.screenAlignedVB);
+				l0.texpaint.g4.setIndexBuffer(ConstData.screenAlignedIB);
+				l0.texpaint.g4.drawIndexedVertices();
+				l0.texpaint.g4.end();
+			}
+
+			///if is_paint
+			Base.tempImage.g2.begin(false);
+			Base.tempImage.g2.pipeline = Base.pipeCopy;
+			Base.tempImage.g2.drawImage(l0.texpaint_nor, 0, 0);
+			Base.tempImage.g2.pipeline = null;
+			Base.tempImage.g2.end();
+
+			if (l1.paintNor) {
+				l0.texpaint_nor.g4.begin();
+				l0.texpaint_nor.g4.setPipeline(Base.pipeMerge);
+				l0.texpaint_nor.g4.setTexture(Base.tex0, l1.texpaint);
+				l0.texpaint_nor.g4.setTexture(Base.tex1, l1.texpaint_nor);
+				l0.texpaint_nor.g4.setTexture(Base.texmask, mask);
+				l0.texpaint_nor.g4.setTexture(Base.texa, Base.tempImage);
+				l0.texpaint_nor.g4.setFloat(Base.opac, l1.getOpacity());
+				l0.texpaint_nor.g4.setInt(Base.blending, l1.paintNorBlend ? -2 : -1);
+				l0.texpaint_nor.g4.setVertexBuffer(ConstData.screenAlignedVB);
+				l0.texpaint_nor.g4.setIndexBuffer(ConstData.screenAlignedIB);
+				l0.texpaint_nor.g4.drawIndexedVertices();
+				l0.texpaint_nor.g4.end();
+			}
+
+			Base.tempImage.g2.begin(false);
+			Base.tempImage.g2.pipeline = Base.pipeCopy;
+			Base.tempImage.g2.drawImage(l0.texpaint_pack, 0, 0);
+			Base.tempImage.g2.pipeline = null;
+			Base.tempImage.g2.end();
+
+			if (l1.paintOcc || l1.paintRough || l1.paintMet || l1.paintHeight) {
+				if (l1.paintOcc && l1.paintRough && l1.paintMet && l1.paintHeight) {
+					Base.commandsMergePack(Base.pipeMerge, l0.texpaint_pack, l1.texpaint, l1.texpaint_pack, l1.getOpacity(), mask, l1.paintHeightBlend ? -3 : -1);
+				}
+				else {
+					if (l1.paintOcc) Base.commandsMergePack(Base.pipeMergeR, l0.texpaint_pack, l1.texpaint, l1.texpaint_pack, l1.getOpacity(), mask);
+					if (l1.paintRough) Base.commandsMergePack(Base.pipeMergeG, l0.texpaint_pack, l1.texpaint, l1.texpaint_pack, l1.getOpacity(), mask);
+					if (l1.paintMet) Base.commandsMergePack(Base.pipeMergeB, l0.texpaint_pack, l1.texpaint, l1.texpaint_pack, l1.getOpacity(), mask);
+				}
+			}
+			///end
+		}
+	}
+
+	static flatten = (heightToNormal = false, layers: SlotLayer[] = null): any => {
+		if (layers == null) layers = Project.layers;
+		Base.makeTempImg();
+		Base.makeExportImg();
+		if (Base.pipeMerge == null) Base.makePipe();
+		if (ConstData.screenAlignedVB == null) ConstData.createScreenAlignedData();
+		let empty = RenderPath.active.renderTargets.get("empty_white").image;
+
+		// Clear export layer
+		Base.expa.g4.begin();
+		Base.expa.g4.clear(color_from_floats(0.0, 0.0, 0.0, 0.0));
+		Base.expa.g4.end();
+		Base.expb.g4.begin();
+		Base.expb.g4.clear(color_from_floats(0.5, 0.5, 1.0, 0.0));
+		Base.expb.g4.end();
+		Base.expc.g4.begin();
+		Base.expc.g4.clear(color_from_floats(1.0, 0.0, 0.0, 0.0));
+		Base.expc.g4.end();
+
+		// Flatten layers
+		for (let l1 of layers) {
+			if (!l1.isVisible()) continue;
+			if (!l1.isLayer()) continue;
+
+			let mask = empty;
+			let l1masks = l1.getMasks();
+			if (l1masks != null) {
+				if (l1masks.length > 1) {
+					Base.makeTempMaskImg();
+					Base.tempMaskImage.g2.begin(true, 0x00000000);
+					Base.tempMaskImage.g2.end();
+					let l1: any = { texpaint: Base.tempMaskImage };
+					for (let i = 0; i < l1masks.length; ++i) {
+						Base.mergeLayer(l1, l1masks[i]);
+					}
+					mask = Base.tempMaskImage;
+				}
+				else mask = l1masks[0].texpaint;
+			}
+
+			if (l1.paintBase) {
+				Base.tempImage.g2.begin(false); // Copy to temp
+				Base.tempImage.g2.pipeline = Base.pipeCopy;
+				Base.tempImage.g2.drawImage(Base.expa, 0, 0);
+				Base.tempImage.g2.pipeline = null;
+				Base.tempImage.g2.end();
+
+				Base.expa.g4.begin();
+				Base.expa.g4.setPipeline(Base.pipeMerge);
+				Base.expa.g4.setTexture(Base.tex0, l1.texpaint);
+				Base.expa.g4.setTexture(Base.tex1, empty);
+				Base.expa.g4.setTexture(Base.texmask, mask);
+				Base.expa.g4.setTexture(Base.texa, Base.tempImage);
+				Base.expa.g4.setFloat(Base.opac, l1.getOpacity());
+				Base.expa.g4.setInt(Base.blending, layers.length > 1 ? l1.blending : 0);
+				Base.expa.g4.setVertexBuffer(ConstData.screenAlignedVB);
+				Base.expa.g4.setIndexBuffer(ConstData.screenAlignedIB);
+				Base.expa.g4.drawIndexedVertices();
+				Base.expa.g4.end();
+			}
+
+			///if is_paint
+			if (l1.paintNor) {
+				Base.tempImage.g2.begin(false);
+				Base.tempImage.g2.pipeline = Base.pipeCopy;
+				Base.tempImage.g2.drawImage(Base.expb, 0, 0);
+				Base.tempImage.g2.pipeline = null;
+				Base.tempImage.g2.end();
+
+				Base.expb.g4.begin();
+				Base.expb.g4.setPipeline(Base.pipeMerge);
+				Base.expb.g4.setTexture(Base.tex0, l1.texpaint);
+				Base.expb.g4.setTexture(Base.tex1, l1.texpaint_nor);
+				Base.expb.g4.setTexture(Base.texmask, mask);
+				Base.expb.g4.setTexture(Base.texa, Base.tempImage);
+				Base.expb.g4.setFloat(Base.opac, l1.getOpacity());
+				Base.expb.g4.setInt(Base.blending, l1.paintNorBlend ? -2 : -1);
+				Base.expb.g4.setVertexBuffer(ConstData.screenAlignedVB);
+				Base.expb.g4.setIndexBuffer(ConstData.screenAlignedIB);
+				Base.expb.g4.drawIndexedVertices();
+				Base.expb.g4.end();
+			}
+
+			if (l1.paintOcc || l1.paintRough || l1.paintMet || l1.paintHeight) {
+				Base.tempImage.g2.begin(false);
+				Base.tempImage.g2.pipeline = Base.pipeCopy;
+				Base.tempImage.g2.drawImage(Base.expc, 0, 0);
+				Base.tempImage.g2.pipeline = null;
+				Base.tempImage.g2.end();
+
+				if (l1.paintOcc && l1.paintRough && l1.paintMet && l1.paintHeight) {
+					Base.commandsMergePack(Base.pipeMerge, Base.expc, l1.texpaint, l1.texpaint_pack, l1.getOpacity(), mask, l1.paintHeightBlend ? -3 : -1);
+				}
+				else {
+					if (l1.paintOcc) Base.commandsMergePack(Base.pipeMergeR, Base.expc, l1.texpaint, l1.texpaint_pack, l1.getOpacity(), mask);
+					if (l1.paintRough) Base.commandsMergePack(Base.pipeMergeG, Base.expc, l1.texpaint, l1.texpaint_pack, l1.getOpacity(), mask);
+					if (l1.paintMet) Base.commandsMergePack(Base.pipeMergeB, Base.expc, l1.texpaint, l1.texpaint_pack, l1.getOpacity(), mask);
+				}
+			}
+			///end
+		}
+
+		///if krom_metal
+		// Flush command list
+		Base.expa.g2.begin(false);
+		Base.expa.g2.end();
+		Base.expb.g2.begin(false);
+		Base.expb.g2.end();
+		Base.expc.g2.begin(false);
+		Base.expc.g2.end();
+		///end
+
+		let l0 = { texpaint: Base.expa, texpaint_nor: Base.expb, texpaint_pack: Base.expc };
+
+		// Merge height map into normal map
+		if (heightToNormal && MakeMaterial.heightUsed) {
+
+			Base.tempImage.g2.begin(false);
+			Base.tempImage.g2.pipeline = Base.pipeCopy;
+			Base.tempImage.g2.drawImage(l0.texpaint_nor, 0, 0);
+			Base.tempImage.g2.pipeline = null;
+			Base.tempImage.g2.end();
+
+			l0.texpaint_nor.g4.begin();
+			l0.texpaint_nor.g4.setPipeline(Base.pipeMerge);
+			l0.texpaint_nor.g4.setTexture(Base.tex0, Base.tempImage);
+			l0.texpaint_nor.g4.setTexture(Base.tex1, l0.texpaint_pack);
+			l0.texpaint_nor.g4.setTexture(Base.texmask, empty);
+			l0.texpaint_nor.g4.setTexture(Base.texa, empty);
+			l0.texpaint_nor.g4.setFloat(Base.opac, 1.0);
+			l0.texpaint_nor.g4.setInt(Base.blending, -4);
+			l0.texpaint_nor.g4.setVertexBuffer(ConstData.screenAlignedVB);
+			l0.texpaint_nor.g4.setIndexBuffer(ConstData.screenAlignedIB);
+			l0.texpaint_nor.g4.drawIndexedVertices();
+			l0.texpaint_nor.g4.end();
+		}
+
+		return l0;
+	}
+
+	static applyMask = (l: SlotLayer, m: SlotLayer) => {
+		if (!l.isLayer() || !m.isMask()) return;
+
+		if (Base.pipeMerge == null) Base.makePipe();
+		Base.makeTempImg();
+
+		// Copy layer to temp
+		Base.tempImage.g2.begin(false);
+		Base.tempImage.g2.pipeline = Base.pipeCopy;
+		Base.tempImage.g2.drawImage(l.texpaint, 0, 0);
+		Base.tempImage.g2.pipeline = null;
+		Base.tempImage.g2.end();
+
+		// Apply mask
+		if (ConstData.screenAlignedVB == null) ConstData.createScreenAlignedData();
+		l.texpaint.g4.begin();
+		l.texpaint.g4.setPipeline(Base.pipeApplyMask);
+		l.texpaint.g4.setTexture(Base.tex0Mask, Base.tempImage);
+		l.texpaint.g4.setTexture(Base.texaMask, m.texpaint);
+		l.texpaint.g4.setVertexBuffer(ConstData.screenAlignedVB);
+		l.texpaint.g4.setIndexBuffer(ConstData.screenAlignedIB);
+		l.texpaint.g4.drawIndexedVertices();
+		l.texpaint.g4.end();
+	}
+
+	static commandsMergePack = (pipe: PipelineState, i0: Image, i1: Image, i1pack: Image, i1maskOpacity: f32, i1texmask: Image, i1blending = -1) => {
+		i0.g4.begin();
+		i0.g4.setPipeline(pipe);
+		i0.g4.setTexture(Base.tex0, i1);
+		i0.g4.setTexture(Base.tex1, i1pack);
+		i0.g4.setTexture(Base.texmask, i1texmask);
+		i0.g4.setTexture(Base.texa, Base.tempImage);
+		i0.g4.setFloat(Base.opac, i1maskOpacity);
+		i0.g4.setInt(Base.blending, i1blending);
+		i0.g4.setVertexBuffer(ConstData.screenAlignedVB);
+		i0.g4.setIndexBuffer(ConstData.screenAlignedIB);
+		i0.g4.drawIndexedVertices();
+		i0.g4.end();
+	}
+
+	static isFillMaterial = (): bool => {
+		///if is_paint
+		if (Context.raw.tool == WorkspaceTool.ToolMaterial) return true;
+		///end
+
+		let m = Context.raw.material;
+		for (let l of Project.layers) if (l.fill_layer == m) return true;
+		return false;
+	}
+
+	static updateFillLayers = () => {
+		let _layer = Context.raw.layer;
+		let _tool = Context.raw.tool;
+		let _fillType = Context.raw.fillTypeHandle.position;
+		let current: Graphics2 = null;
+
+		///if is_paint
+		if (Context.raw.tool == WorkspaceTool.ToolMaterial) {
+			if (RenderPathPaint.liveLayer == null) {
+				RenderPathPaint.liveLayer = new SlotLayer("_live");
+			}
+
+			current = Graphics2.current;
+			if (current != null) current.end();
+
+			Context.raw.tool = WorkspaceTool.ToolFill;
+			Context.raw.fillTypeHandle.position = FillType.FillObject;
+			MakeMaterial.parsePaintMaterial(false);
+			Context.raw.pdirty = 1;
+			RenderPathPaint.useLiveLayer(true);
+			RenderPathPaint.commandsPaint(false);
+			RenderPathPaint.dilate(true, true);
+			RenderPathPaint.useLiveLayer(false);
+			Context.raw.tool = _tool;
+			Context.raw.fillTypeHandle.position = _fillType;
+			Context.raw.pdirty = 0;
+			Context.raw.rdirty = 2;
+
+			if (current != null) current.begin(false);
+			return;
+		}
+		///end
+
+		let hasFillLayer = false;
+		let hasFillMask = false;
+		for (let l of Project.layers) if (l.isLayer() && l.fill_layer == Context.raw.material) hasFillLayer = true;
+		for (let l of Project.layers) if (l.isMask() && l.fill_layer == Context.raw.material) hasFillMask = true;
+
+		if (hasFillLayer || hasFillMask) {
+			current = Graphics2.current;
+			if (current != null) current.end();
+			Context.raw.pdirty = 1;
+			Context.raw.tool = WorkspaceTool.ToolFill;
+			Context.raw.fillTypeHandle.position = FillType.FillObject;
+
+			if (hasFillLayer) {
+				let first = true;
+				for (let l of Project.layers) {
+					if (l.isLayer() && l.fill_layer == Context.raw.material) {
+						Context.raw.layer = l;
+						if (first) {
+							first = false;
+							MakeMaterial.parsePaintMaterial(false);
+						}
+						Base.setObjectMask();
+						l.clear();
+						RenderPathPaint.commandsPaint(false);
+						RenderPathPaint.dilate(true, true);
+					}
+				}
+			}
+			if (hasFillMask) {
+				let first = true;
+				for (let l of Project.layers) {
+					if (l.isMask() && l.fill_layer == Context.raw.material) {
+						Context.raw.layer = l;
+						if (first) {
+							first = false;
+							MakeMaterial.parsePaintMaterial(false);
+						}
+						Base.setObjectMask();
+						l.clear();
+						RenderPathPaint.commandsPaint(false);
+						RenderPathPaint.dilate(true, true);
+					}
+				}
+			}
+
+			Context.raw.pdirty = 0;
+			Context.raw.ddirty = 2;
+			Context.raw.rdirty = 2;
+			Context.raw.layersPreviewDirty = true; // Repaint all layer previews as multiple layers might have changed.
+			if (current != null) current.begin(false);
+			Context.raw.layer = _layer;
+			Base.setObjectMask();
+			Context.raw.tool = _tool;
+			Context.raw.fillTypeHandle.position = _fillType;
+			MakeMaterial.parsePaintMaterial(false);
+		}
+	}
+
+	static updateFillLayer = (parsePaint = true) => {
+		let current = Graphics2.current;
+		if (current != null) current.end();
+
+		let _tool = Context.raw.tool;
+		let _fillType = Context.raw.fillTypeHandle.position;
+		Context.raw.tool = WorkspaceTool.ToolFill;
+		Context.raw.fillTypeHandle.position = FillType.FillObject;
+		Context.raw.pdirty = 1;
+
+		Context.raw.layer.clear();
+
+		if (parsePaint) MakeMaterial.parsePaintMaterial(false);
+		RenderPathPaint.commandsPaint(false);
+		RenderPathPaint.dilate(true, true);
+
+		Context.raw.rdirty = 2;
+		Context.raw.tool = _tool;
+		Context.raw.fillTypeHandle.position = _fillType;
+		if (current != null) current.begin(false);
+	}
+
+	static setObjectMask = () => {
+		///if is_sculpt
+		return;
+		///end
+
+		let ar = [tr("None")];
+		for (let p of Project.paintObjects) ar.push(p.name);
+
+		let mask = Context.objectMaskUsed() ? Context.raw.layer.getObjectMask() : 0;
+		if (Context.layerFilterUsed()) mask = Context.raw.layerFilter;
+		if (mask > 0) {
+			if (Context.raw.mergedObject != null) {
+				Context.raw.mergedObject.visible = false;
+			}
+			let o = Project.paintObjects[0];
+			for (let p of Project.paintObjects) {
+				if (p.name == ar[mask]) {
+					o = p;
+					break;
+				}
+			}
+			Context.selectPaintObject(o);
+		}
+		else {
+			let isAtlas = Context.raw.layer.getObjectMask() > 0 && Context.raw.layer.getObjectMask() <= Project.paintObjects.length;
+			if (Context.raw.mergedObject == null || isAtlas || Context.raw.mergedObjectIsAtlas) {
+				let visibles = isAtlas ? Project.getAtlasObjects(Context.raw.layer.getObjectMask()) : null;
+				UtilMesh.mergeMesh(visibles);
+			}
+			Context.selectPaintObject(Context.mainObject());
+			Context.raw.paintObject.skip_context = "paint";
+			Context.raw.mergedObject.visible = true;
+		}
+		UtilUV.dilatemapCached = false;
+	}
+
+	static newLayer = (clear = true, position = -1): SlotLayer => {
+		if (Project.layers.length > Base.maxLayers) return null;
+		let l = new SlotLayer();
+		l.objectMask = Context.raw.layerFilter;
+		if (position == -1) {
+			if (Context.raw.layer.isMask()) Context.setLayer(Context.raw.layer.parent);
+			Project.layers.splice(Project.layers.indexOf(Context.raw.layer) + 1, 0, l);
+		}
+		else {
+			Project.layers.splice(position, 0, l);
+		}
+
+		Context.setLayer(l);
+		let li = Project.layers.indexOf(Context.raw.layer);
+		if (li > 0) {
+			let below = Project.layers[li - 1];
+			if (below.isLayer()) {
+				Context.raw.layer.parent = below.parent;
+			}
+		}
+		if (clear) App.notifyOnInit(() => { l.clear(); });
+		Context.raw.layerPreviewDirty = true;
+		return l;
+	}
+
+	static newMask = (clear = true, parent: SlotLayer, position = -1): SlotLayer => {
+		if (Project.layers.length > Base.maxLayers) return null;
+		let l = new SlotLayer("", LayerSlotType.SlotMask, parent);
+		if (position == -1) position = Project.layers.indexOf(parent);
+		Project.layers.splice(position, 0, l);
+		Context.setLayer(l);
+		if (clear) App.notifyOnInit(() => { l.clear(); });
+		Context.raw.layerPreviewDirty = true;
+		return l;
+	}
+
+	static newGroup = (): SlotLayer => {
+		if (Project.layers.length > Base.maxLayers) return null;
+		let l = new SlotLayer("", LayerSlotType.SlotGroup);
+		Project.layers.push(l);
+		Context.setLayer(l);
+		return l;
+	}
+
+	static createFillLayer = (uvType = UVType.UVMap, decalMat: Mat4 = null, position = -1) => {
+		let _init = () => {
+			let l = Base.newLayer(false, position);
+			History.newLayer();
+			l.uvType = uvType;
+			if (decalMat != null) l.decalMat = decalMat;
+			l.objectMask = Context.raw.layerFilter;
+			History.toFillLayer();
+			l.toFillLayer();
+		}
+		App.notifyOnInit(_init);
+	}
+
+	static createImageMask = (asset: TAsset) => {
+		let l = Context.raw.layer;
+		if (l.isMask() || l.isGroup()) {
+			return;
+		}
+
+		History.newLayer();
+		let m = Base.newMask(false, l);
+		m.clear(0x00000000, Project.getImage(asset));
+		Context.raw.layerPreviewDirty = true;
+	}
+
+	static createColorLayer = (baseColor: i32, occlusion = 1.0, roughness = Base.defaultRough, metallic = 0.0, position = -1) => {
+		let _init = () => {
+			let l = Base.newLayer(false, position);
+			History.newLayer();
+			l.uvType = UVType.UVMap;
+			l.objectMask = Context.raw.layerFilter;
+			l.clear(baseColor, null, occlusion, roughness, metallic);
+		}
+		App.notifyOnInit(_init);
+	}
+
+	static onLayersResized = () => {
+		App.notifyOnInit(() => {
+			Base.resizeLayers();
+			let _layer = Context.raw.layer;
+			let _material = Context.raw.material;
+			for (let l of Project.layers) {
+				if (l.fill_layer != null) {
+					Context.raw.layer = l;
+					Context.raw.material = l.fill_layer;
+					Base.updateFillLayer();
+				}
+			}
+			Context.raw.layer = _layer;
+			Context.raw.material = _material;
+			MakeMaterial.parsePaintMaterial();
+		});
+		UtilUV.uvmap = null;
+		UtilUV.uvmapCached = false;
+		UtilUV.trianglemap = null;
+		UtilUV.trianglemapCached = false;
+		UtilUV.dilatemapCached = false;
+		///if (krom_direct3d12 || krom_vulkan || krom_metal)
+		RenderPathRaytrace.ready = false;
+		///end
+	}
+	///end
+
+	///if is_lab
+	static flatten = (heightToNormal = false): any => {
+		let texpaint = BrushOutputNode.inst.texpaint;
+		let texpaint_nor = BrushOutputNode.inst.texpaint_nor;
+		let texpaint_pack = BrushOutputNode.inst.texpaint_pack;
+
+		let nodes = UINodes.inst.getNodes();
+		let canvas = UINodes.inst.getCanvas(true);
+		if (nodes.nodesSelectedId.length > 0) {
+			let node = nodes.getNode(canvas.nodes, nodes.nodesSelectedId[0]);
+			let brushNode = ParserLogic.getLogicNode(node);
+			if (brushNode != null && brushNode.getCachedImage() != null) {
+				texpaint = brushNode.getCachedImage();
+				texpaint_nor = RenderPath.active.renderTargets.get("texpaint_nor_empty").image;
+				texpaint_pack = RenderPath.active.renderTargets.get("texpaint_pack_empty").image;
+			}
+		}
+
+		return { texpaint: texpaint, texpaint_nor: texpaint_nor, texpaint_pack: texpaint_pack };
+	}
+
+	static onLayersResized = () => {
+		BrushOutputNode.inst.texpaint.unload();
+		BrushOutputNode.inst.texpaint = RenderPath.active.renderTargets.get("texpaint").image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY());
+		BrushOutputNode.inst.texpaint_nor.unload();
+		BrushOutputNode.inst.texpaint_nor = RenderPath.active.renderTargets.get("texpaint_nor").image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY());
+		BrushOutputNode.inst.texpaint_pack.unload();
+		BrushOutputNode.inst.texpaint_pack = RenderPath.active.renderTargets.get("texpaint_pack").image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY());
+
+		if (InpaintNode.image != null) {
+			InpaintNode.image.unload();
+			InpaintNode.image = null;
+			InpaintNode.mask.unload();
+			InpaintNode.mask = null;
+			InpaintNode.init();
+		}
+
+		if (PhotoToPBRNode.images != null) {
+			for (let image of PhotoToPBRNode.images) image.unload();
+			PhotoToPBRNode.images = null;
+			PhotoToPBRNode.init();
+		}
+
+		if (TilingNode.image != null) {
+			TilingNode.image.unload();
+			TilingNode.image = null;
+			TilingNode.init();
+		}
+
+		RenderPath.active.renderTargets.get("texpaint_blend0").image.unload();
+		RenderPath.active.renderTargets.get("texpaint_blend0").image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY(), TextureFormat.R8);
+		RenderPath.active.renderTargets.get("texpaint_blend1").image.unload();
+		RenderPath.active.renderTargets.get("texpaint_blend1").image = Image.createRenderTarget(Config.getTextureResX(), Config.getTextureResY(), TextureFormat.R8);
+
+		if (RenderPath.active.renderTargets.get("texpaint_node") != null) {
+			RenderPath.active.renderTargets.delete("texpaint_node");
+		}
+		if (RenderPath.active.renderTargets.get("texpaint_node_target") != null) {
+			RenderPath.active.renderTargets.delete("texpaint_node_target");
+		}
+
+		Base.notifyOnNextFrame(() => {
+			Base.initLayers();
+		});
+
+		///if (krom_direct3d12 || krom_vulkan || krom_metal)
+		RenderPathRaytrace.ready = false;
+		///end
+	}
+	///end
+
+	static defaultKeymap = {
+		action_paint: "left",
+		action_rotate: "alt+left",
+		action_pan: "alt+middle",
+		action_zoom: "alt+right",
+		rotate_light: "shift+middle",
+		rotate_envmap: "ctrl+middle",
+		set_clone_source: "alt",
+		stencil_transform: "ctrl",
+		stencil_hide: "z",
+		brush_radius: "f",
+		brush_radius_decrease: "[",
+		brush_radius_increase: "]",
+		brush_ruler: "shift",
+		file_new: "ctrl+n",
+		file_open: "ctrl+o",
+		file_open_recent: "ctrl+shift+o",
+		file_save: "ctrl+s",
+		file_save_as: "ctrl+shift+s",
+		file_reimport_mesh: "ctrl+r",
+		file_reimport_textures: "ctrl+shift+r",
+		file_import_assets: "ctrl+i",
+		file_export_textures: "ctrl+e",
+		file_export_textures_as: "ctrl+shift+e",
+		edit_undo: "ctrl+z",
+		edit_redo: "ctrl+shift+z",
+		edit_prefs: "ctrl+k",
+		view_reset: "0",
+		view_front: "1",
+		view_back: "ctrl+1",
+		view_right: "3",
+		view_left: "ctrl+3",
+		view_top: "7",
+		view_bottom: "ctrl+7",
+		view_camera_type: "5",
+		view_orbit_left: "4",
+		view_orbit_right: "6",
+		view_orbit_up: "8",
+		view_orbit_down: "2",
+		view_orbit_opposite: "9",
+		view_zoom_in: "",
+		view_zoom_out: "",
+		view_distract_free: "f11",
+		viewport_mode: "ctrl+m",
+		toggle_node_editor: "tab",
+		toggle_2d_view: "shift+tab",
+		toggle_browser: "`",
+		node_search: "space",
+		operator_search: "space",
+
+		///if (is_paint || is_sculpt)
+		decal_mask: "ctrl",
+		select_material: "shift+number",
+		select_layer: "alt+number",
+		brush_opacity: "shift+f",
+		brush_angle: "alt+f",
+		tool_brush: "b",
+		tool_eraser: "e",
+		tool_fill: "g",
+		tool_decal: "d",
+		tool_text: "t",
+		tool_clone: "l",
+		tool_blur: "u",
+		tool_smudge: "m",
+		tool_particle: "p",
+		tool_colorid: "c",
+		tool_picker: "v",
+		tool_bake: "k",
+		tool_gizmo: "",
+		tool_material: "",
+		swap_brush_eraser: "",
+		///end
+	};
+}

+ 471 - 0
base/Sources/BoxExport.ts

@@ -0,0 +1,471 @@
+
+class BoxExport {
+
+	static htab = new Handle();
+	static files: string[] = null;
+	static exportMeshHandle = new Handle();
+
+	///if (is_paint || is_lab)
+	static hpreset = new Handle();
+	static preset: TExportPreset = null;
+	static channels = ["base_r", "base_g", "base_b", "height", "metal", "nor_r", "nor_g", "nor_g_directx", "nor_b", "occ", "opac", "rough", "smooth", "emis", "subs", "0.0", "1.0"];
+	static colorSpaces = ["linear", "srgb"];
+	///end
+
+	///if (is_paint || is_lab)
+	static showTextures = () => {
+		UIBox.showCustom((ui: Zui) => {
+
+			if (BoxExport.files == null) {
+				BoxExport.fetchPresets();
+				BoxExport.hpreset.position = BoxExport.files.indexOf("generic");
+			}
+			if (BoxExport.preset == null) {
+				BoxExport.parsePreset();
+				BoxExport.hpreset.children = null;
+			}
+
+			BoxExport.tabExportTextures(ui, tr("Export Textures"));
+			BoxExport.tabPresets(ui);
+
+			///if is_paint
+			BoxExport.tabAtlases(ui);
+			///if (krom_android || krom_ios)
+			BoxExport.tabExportMesh(ui, BoxExport.htab);
+			///end
+			///end
+
+		}, 540, 310);
+	}
+	///end
+
+	///if is_paint
+	static showBakeMaterial = () => {
+		UIBox.showCustom((ui: Zui) => {
+
+			if (BoxExport.files == null) {
+				BoxExport.fetchPresets();
+				BoxExport.hpreset.position = BoxExport.files.indexOf("generic");
+			}
+			if (BoxExport.preset == null) {
+				BoxExport.parsePreset();
+				BoxExport.hpreset.children = null;
+			}
+
+			BoxExport.tabExportTextures(ui, tr("Bake to Textures"), true);
+			BoxExport.tabPresets(ui);
+
+		}, 540, 310);
+	}
+	///end
+
+	///if (is_paint || is_lab)
+	static tabExportTextures = (ui: Zui, title: string, bakeMaterial = false) => {
+		let tabVertical = Config.raw.touch_ui;
+		if (ui.tab(BoxExport.htab, title, tabVertical)) {
+
+			ui.row([0.5, 0.5]);
+
+			///if is_paint
+			///if (krom_android || krom_ios)
+			ui.combo(Base.resHandle, ["128", "256", "512", "1K", "2K", "4K"], tr("Resolution"), true);
+			///else
+			ui.combo(Base.resHandle, ["128", "256", "512", "1K", "2K", "4K", "8K", "16K"], tr("Resolution"), true);
+			///end
+			///end
+
+			///if is_lab
+			///if (krom_android || krom_ios)
+			ui.combo(Base.resHandle, ["2K", "4K"], tr("Resolution"), true);
+			///else
+			ui.combo(Base.resHandle, ["2K", "4K", "8K", "16K"], tr("Resolution"), true);
+			///end
+			///end
+
+			if (Base.resHandle.changed) {
+				Base.onLayersResized();
+			}
+
+			///if (is_lab || krom_android || krom_ios)
+			ui.combo(Base.bitsHandle, ["8bit"], tr("Color"), true);
+			///else
+			ui.combo(Base.bitsHandle, ["8bit", "16bit", "32bit"], tr("Color"), true);
+			///end
+
+			///if is_paint
+			if (Base.bitsHandle.changed) {
+				App.notifyOnInit(Base.setLayerBits);
+			}
+			///end
+
+			ui.row([0.5, 0.5]);
+			if (Base.bitsHandle.position == TextureBits.Bits8) {
+				Context.raw.formatType = ui.combo(Zui.handle("boxexport_0", { position: Context.raw.formatType }), ["png", "jpg"], tr("Format"), true);
+			}
+			else {
+				Context.raw.formatType = ui.combo(Zui.handle("boxexport_1", { position: Context.raw.formatType }), ["exr"], tr("Format"), true);
+			}
+
+			ui.enabled = Context.raw.formatType == TextureLdrFormat.FormatJpg && Base.bitsHandle.position == TextureBits.Bits8;
+			Context.raw.formatQuality = ui.slider(Zui.handle("boxexport_2", { value: Context.raw.formatQuality }), tr("Quality"), 0.0, 100.0, true, 1);
+			ui.enabled = true;
+
+			///if is_paint
+			ui.row([0.5, 0.5]);
+			ui.enabled = !bakeMaterial;
+			let layersExportHandle = Zui.handle("boxexport_3");
+			layersExportHandle.position = Context.raw.layersExport;
+			Context.raw.layersExport = ui.combo(layersExportHandle, [tr("Visible"), tr("Selected"), tr("Per Object"), tr("Per Udim Tile")], tr("Layers"), true);
+			ui.enabled = true;
+			///end
+
+			ui.combo(BoxExport.hpreset, BoxExport.files, tr("Preset"), true);
+			if (BoxExport.hpreset.changed) BoxExport.preset = null;
+
+			let layersDestinationHandle = Zui.handle("boxexport_4");
+			layersDestinationHandle.position = Context.raw.layersDestination;
+			Context.raw.layersDestination = ui.combo(layersDestinationHandle, [tr("Disk"), tr("Packed")], tr("Destination"), true);
+
+			ui.endElement();
+
+			ui.row([0.5, 0.5]);
+			if (ui.button(tr("Cancel"))) {
+				UIBox.hide();
+			}
+			if (ui.button(tr("Export"))) {
+				UIBox.hide();
+				if (Context.raw.layersDestination == ExportDestination.DestinationPacked) {
+					Context.raw.textureExportPath = "/";
+					let _init = () => {
+						///if is_paint
+						ExportTexture.run(Context.raw.textureExportPath, bakeMaterial);
+						///end
+						///if is_lab
+						ExportTexture.run(Context.raw.textureExportPath);
+						///end
+					}
+					App.notifyOnInit(_init);
+				}
+				else {
+					let filters = Base.bitsHandle.position != TextureBits.Bits8 ? "exr" : Context.raw.formatType == TextureLdrFormat.FormatPng ? "png" : "jpg";
+					UIFiles.show(filters, true, false, (path: string) => {
+						Context.raw.textureExportPath = path;
+						let doExport = () => {
+							let _init = () => {
+								///if is_paint
+								ExportTexture.run(Context.raw.textureExportPath, bakeMaterial);
+								///end
+								///if is_lab
+								ExportTexture.run(Context.raw.textureExportPath);
+								///end
+							}
+							App.notifyOnInit(_init);
+						}
+						///if (krom_android || krom_ios)
+						Base.notifyOnNextFrame(() => {
+							Console.toast(tr("Exporting textures"));
+							Base.notifyOnNextFrame(doExport);
+						});
+						///else
+						doExport();
+						///end
+					});
+				}
+			}
+			if (ui.isHovered) ui.tooltip(tr("Export texture files") + ` (${Config.keymap.file_export_textures})`);
+		}
+	}
+
+	static tabPresets = (ui: Zui) => {
+		let tabVertical = Config.raw.touch_ui;
+		if (ui.tab(BoxExport.htab, tr("Presets"), tabVertical)) {
+			ui.row([3 / 5, 1 / 5, 1 / 5]);
+
+			ui.combo(BoxExport.hpreset, BoxExport.files, tr("Preset"));
+			if (BoxExport.hpreset.changed) BoxExport.preset = null;
+
+			if (ui.button(tr("New"))) {
+				UIBox.showCustom((ui: Zui) => {
+					let tabVertical = Config.raw.touch_ui;
+					if (ui.tab(Zui.handle("boxexport_5"), tr("New Preset"), tabVertical)) {
+						ui.row([0.5, 0.5]);
+						let presetName = ui.textInput(Zui.handle("boxexport_6", { text: "new_preset" }), tr("Name"));
+						if (ui.button(tr("OK")) || ui.isReturnDown) {
+							BoxExport.newPreset(presetName);
+							BoxExport.fetchPresets();
+							BoxExport.preset = null;
+							BoxExport.hpreset.position = BoxExport.files.indexOf(presetName);
+							UIBox.hide();
+							BoxExport.htab.position = 1; // Presets
+							BoxExport.showTextures();
+						}
+					}
+				});
+			}
+
+			if (ui.button(tr("Import"))) {
+				UIFiles.show("json", false, false, (path: string) => {
+					path = path.toLowerCase();
+					if (path.endsWith(".json")) {
+						let filename = path.substr(path.lastIndexOf(Path.sep) + 1);
+						let dstPath = Path.data() + Path.sep + "export_presets" + Path.sep + filename;
+						File.copy(path, dstPath); // Copy to presets folder
+						BoxExport.fetchPresets();
+						BoxExport.preset = null;
+						BoxExport.hpreset.position = BoxExport.files.indexOf(filename.substr(0, filename.length - 5)); // Strip .json
+						Console.info(tr("Preset imported:") + " " + filename);
+					}
+					else Console.error(Strings.error1());
+				});
+			}
+
+			if (BoxExport.preset == null) {
+				BoxExport.parsePreset();
+				BoxExport.hpreset.children = null;
+			}
+
+			// Texture list
+			ui.separator(10, false);
+			ui.row([1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6]);
+			ui.text(tr("Texture"));
+			ui.text(tr("R"));
+			ui.text(tr("G"));
+			ui.text(tr("B"));
+			ui.text(tr("A"));
+			ui.text(tr("Color Space"));
+			ui.changed = false;
+			for (let i = 0; i < BoxExport.preset.textures.length; ++i) {
+				let t = BoxExport.preset.textures[i];
+				ui.row([1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6]);
+				let htex = BoxExport.hpreset.nest(i);
+				htex.text = t.name;
+				t.name = ui.textInput(htex);
+
+				if (ui.isHovered && ui.inputReleasedR) {
+					UIMenu.draw((ui: Zui) => {
+						if (UIMenu.menuButton(ui, tr("Delete"))) {
+							array_remove(BoxExport.preset.textures, t);
+							BoxExport.savePreset();
+						}
+					}, 1);
+				}
+
+				let hr = htex.nest(0);
+				hr.position = BoxExport.channels.indexOf(t.channels[0]);
+				let hg = htex.nest(1);
+				hg.position = BoxExport.channels.indexOf(t.channels[1]);
+				let hb = htex.nest(2);
+				hb.position = BoxExport.channels.indexOf(t.channels[2]);
+				let ha = htex.nest(3);
+				ha.position = BoxExport.channels.indexOf(t.channels[3]);
+
+				ui.combo(hr, BoxExport.channels, tr("R"));
+				if (hr.changed) t.channels[0] = BoxExport.channels[hr.position];
+				ui.combo(hg, BoxExport.channels, tr("G"));
+				if (hg.changed) t.channels[1] = BoxExport.channels[hg.position];
+				ui.combo(hb, BoxExport.channels, tr("B"));
+				if (hb.changed) t.channels[2] = BoxExport.channels[hb.position];
+				ui.combo(ha, BoxExport.channels, tr("A"));
+				if (ha.changed) t.channels[3] = BoxExport.channels[ha.position];
+
+				let hspace = htex.nest(4);
+				hspace.position = BoxExport.colorSpaces.indexOf(t.color_space);
+				ui.combo(hspace, BoxExport.colorSpaces, tr("Color Space"));
+				if (hspace.changed) t.color_space = BoxExport.colorSpaces[hspace.position];
+			}
+
+			if (ui.changed) {
+				BoxExport.savePreset();
+			}
+
+			ui.row([1 / 8]);
+			if (ui.button(tr("Add"))) {
+				BoxExport.preset.textures.push({ name: "base", channels: ["base_r", "base_g", "base_b", "1.0"], color_space: "linear" });
+				BoxExport.hpreset.children = null;
+				BoxExport.savePreset();
+			}
+		}
+	}
+	///end
+
+	///if is_paint
+	static tabAtlases = (ui: Zui) => {
+		let tabVertical = Config.raw.touch_ui;
+		if (ui.tab(BoxExport.htab, tr("Atlases"), tabVertical)) {
+			if (Project.atlasObjects == null || Project.atlasObjects.length != Project.paintObjects.length) {
+				Project.atlasObjects = [];
+				Project.atlasNames = [];
+				for (let i = 0; i < Project.paintObjects.length; ++i) {
+					Project.atlasObjects.push(0);
+					Project.atlasNames.push(tr("Atlas") + " " + (i + 1));
+				}
+			}
+			for (let i = 0; i < Project.paintObjects.length; ++i) {
+				ui.row([1 / 2, 1 / 2]);
+				ui.text(Project.paintObjects[i].name);
+				let hatlas = Zui.handle("boxexport_7").nest(i);
+				hatlas.position = Project.atlasObjects[i];
+				Project.atlasObjects[i] = ui.combo(hatlas, Project.atlasNames, tr("Atlas"));
+			}
+		}
+	}
+	///end
+
+	static showMesh = () => {
+		BoxExport.exportMeshHandle.position = Context.raw.exportMeshIndex;
+		UIBox.showCustom((ui: Zui) => {
+			let htab = Zui.handle("boxexport_8");
+			BoxExport.tabExportMesh(ui, htab);
+		});
+	}
+
+	static tabExportMesh = (ui: Zui, htab: Handle) => {
+		let tabVertical = Config.raw.touch_ui;
+		if (ui.tab(htab, tr("Export Mesh"), tabVertical)) {
+
+			ui.row([1 / 2, 1 / 2]);
+
+			Context.raw.exportMeshFormat = ui.combo(Zui.handle("boxexport_9", { position: Context.raw.exportMeshFormat }), ["obj", "arm"], tr("Format"), true);
+
+			let ar = [tr("All")];
+			for (let p of Project.paintObjects) ar.push(p.name);
+			ui.combo(BoxExport.exportMeshHandle, ar, tr("Meshes"), true);
+
+			let applyDisplacement = ui.check(Zui.handle("boxexport_10"), tr("Apply Displacement"));
+
+			let tris = 0;
+			let pos = BoxExport.exportMeshHandle.position;
+			let paintObjects = pos == 0 ? Project.paintObjects : [Project.paintObjects[pos - 1]];
+			for (let po of paintObjects) {
+				for (let inda of po.data.raw.index_arrays) {
+					tris += Math.floor(inda.values.length / 3);
+				}
+			}
+			ui.text(tris + " " + tr("triangles"));
+
+			ui.row([0.5, 0.5]);
+			if (ui.button(tr("Cancel"))) {
+				UIBox.hide();
+			}
+			if (ui.button(tr("Export"))) {
+				UIBox.hide();
+				UIFiles.show(Context.raw.exportMeshFormat == MeshFormat.FormatObj ? "obj" : "arm", true, false, (path: string) => {
+					///if (krom_android || krom_ios)
+					let f = System.title;
+					///else
+					let f = UIFiles.filename;
+					///end
+					if (f == "") f = tr("untitled");
+					let doExport = () => {
+						ExportMesh.run(path + Path.sep + f, BoxExport.exportMeshHandle.position == 0 ? null : [Project.paintObjects[BoxExport.exportMeshHandle.position - 1]], applyDisplacement);
+					}
+					///if (krom_android || krom_ios)
+					Base.notifyOnNextFrame(() => {
+						Console.toast(tr("Exporting mesh"));
+						Base.notifyOnNextFrame(doExport);
+					});
+					///else
+					doExport();
+					///end
+				});
+			}
+		}
+	}
+
+	///if (is_paint || is_sculpt)
+	static showMaterial = () => {
+		UIBox.showCustom((ui: Zui) => {
+			let htab = Zui.handle("boxexport_11");
+			let tabVertical = Config.raw.touch_ui;
+			if (ui.tab(htab, tr("Export Material"), tabVertical)) {
+				let h1 = Zui.handle("boxexport_12");
+				let h2 = Zui.handle("boxexport_13");
+				h1.selected = Context.raw.packAssetsOnExport;
+				h2.selected = Context.raw.writeIconOnExport;
+				Context.raw.packAssetsOnExport = ui.check(h1, tr("Pack Assets"));
+				Context.raw.writeIconOnExport = ui.check(h2, tr("Export Icon"));
+				ui.row([0.5, 0.5]);
+				if (ui.button(tr("Cancel"))) {
+					UIBox.hide();
+				}
+				if (ui.button(tr("Export"))) {
+					UIBox.hide();
+					UIFiles.show("arm", true, false, (path: string) => {
+						let f = UIFiles.filename;
+						if (f == "") f = tr("untitled");
+						App.notifyOnInit(() => {
+							ExportArm.runMaterial(path + Path.sep + f);
+						});
+					});
+				}
+			}
+		});
+	}
+
+	static showBrush = () => {
+		UIBox.showCustom((ui: Zui) => {
+			let htab = Zui.handle("boxexport_14");
+			let tabVertical = Config.raw.touch_ui;
+			if (ui.tab(htab, tr("Export Brush"), tabVertical)) {
+				let h1 = Zui.handle("boxexport_15");
+				let h2 = Zui.handle("boxexport_16");
+				h1.selected = Context.raw.packAssetsOnExport;
+				h2.selected = Context.raw.writeIconOnExport;
+				Context.raw.packAssetsOnExport = ui.check(h1, tr("Pack Assets"));
+				Context.raw.writeIconOnExport = ui.check(h2, tr("Export Icon"));
+				ui.row([0.5, 0.5]);
+				if (ui.button(tr("Cancel"))) {
+					UIBox.hide();
+				}
+				if (ui.button(tr("Export"))) {
+					UIBox.hide();
+					UIFiles.show("arm", true, false, (path: string) => {
+						let f = UIFiles.filename;
+						if (f == "") f = tr("untitled");
+						App.notifyOnInit(() => {
+							ExportArm.runBrush(path + Path.sep + f);
+						});
+					});
+				}
+			}
+		});
+	}
+	///end
+
+	///if (is_paint || is_lab)
+	static fetchPresets = () => {
+		BoxExport.files = File.readDirectory(Path.data() + Path.sep + "export_presets");
+		for (let i = 0; i < BoxExport.files.length; ++i) {
+			BoxExport.files[i] = BoxExport.files[i].substr(0, BoxExport.files[i].length - 5); // Strip .json
+		}
+	}
+
+	static parsePreset = () => {
+		let file = "export_presets/" + BoxExport.files[BoxExport.hpreset.position] + ".json";
+		Data.getBlob(file, (blob: ArrayBuffer) => {
+			BoxExport.preset = JSON.parse(System.bufferToString(blob));
+			Data.deleteBlob("export_presets/" + file);
+		});
+	}
+
+	static newPreset = (name: string) => {
+		let template =
+`{
+	"textures": [
+		{ "name": "base", "channels": ["base_r", "base_g", "base_b", "1.0"], "color_space": "linear" }
+	]
+}
+`;
+		if (!name.endsWith(".json")) name += ".json";
+		let path = Path.data() + Path.sep + "export_presets" + Path.sep + name;
+		Krom.fileSaveBytes(path, System.stringToBuffer(template));
+	}
+
+	static savePreset = () => {
+		let name = BoxExport.files[BoxExport.hpreset.position];
+		if (name == "generic") return; // generic is const
+		let path = Path.data() + Path.sep + "export_presets" + Path.sep + name + ".json";
+		Krom.fileSaveBytes(path, System.stringToBuffer(JSON.stringify(BoxExport.preset)));
+	}
+	///end
+}

+ 234 - 238
base/Sources/arm/BoxPreferences.hx → base/Sources/BoxPreferences.ts

@@ -1,52 +1,44 @@
-package arm;
-
-import haxe.Json;
-import zui.Zui;
-import iron.App;
-import iron.System;
-import iron.Data;
-import iron.Scene;
 
 class BoxPreferences {
 
-	public static var htab = new Handle();
-	public static var filesPlugin: Array<String> = null;
-	public static var filesKeymap: Array<String> = null;
-	public static var themeHandle: Handle;
-	public static var presetHandle: Handle;
-	static var locales: Array<String> = null;
-	static var themes: Array<String> = null;
-	static var worldColor = Color.fromValue(0xff080808);
+	static htab = new Handle();
+	static filesPlugin: string[] = null;
+	static filesKeymap: string[] = null;
+	static themeHandle: Handle;
+	static presetHandle: Handle;
+	static locales: string[] = null;
+	static themes: string[] = null;
+	static worldColor = 0xff080808;
 
-	public static function show() {
+	static show = () => {
 
-		UIBox.showCustom(function(ui: Zui) {
-			if (ui.tab(htab, tr("Interface"), true)) {
+		UIBox.showCustom((ui: Zui) => {
+			if (ui.tab(BoxPreferences.htab, tr("Interface"), true)) {
 
-				if (locales == null) {
-					locales = Translator.getSupportedLocales();
+				if (BoxPreferences.locales == null) {
+					BoxPreferences.locales = Translator.getSupportedLocales();
 				}
 
-				var localeHandle = Zui.handle("boxpreferences_0", { position: locales.indexOf(Config.raw.locale) });
-				ui.combo(localeHandle, locales, tr("Language"), true);
+				let localeHandle = Zui.handle("boxpreferences_0", { position: BoxPreferences.locales.indexOf(Config.raw.locale) });
+				ui.combo(localeHandle, BoxPreferences.locales, tr("Language"), true);
 				if (localeHandle.changed) {
-					var localeCode = locales[localeHandle.position];
+					let localeCode = BoxPreferences.locales[localeHandle.position];
 					Config.raw.locale = localeCode;
 					Translator.loadTranslations(localeCode);
 					UIBase.inst.tagUIRedraw();
 				}
 
-				var hscale = Zui.handle("boxpreferences_1", { value: Config.raw.window_scale });
+				let hscale = Zui.handle("boxpreferences_1", { value: Config.raw.window_scale });
 				ui.slider(hscale, tr("UI Scale"), 1.0, 4.0, true, 10);
 				if (Context.raw.hscaleWasChanged && !ui.inputDown) {
 					Context.raw.hscaleWasChanged = false;
-					if (hscale.value == null || Math.isNaN(hscale.value)) hscale.value = 1.0;
+					if (hscale.value == null || isNaN(hscale.value)) hscale.value = 1.0;
 					Config.raw.window_scale = hscale.value;
-					setScale();
+					BoxPreferences.setScale();
 				}
 				if (hscale.changed) Context.raw.hscaleWasChanged = true;
 
-				var hspeed = Zui.handle("boxpreferences_2", { value: Config.raw.camera_zoom_speed });
+				let hspeed = Zui.handle("boxpreferences_2", { value: Config.raw.camera_zoom_speed });
 				Config.raw.camera_zoom_speed = ui.slider(hspeed, tr("Camera Zoom Speed"), 0.1, 4.0, true);
 
 				hspeed = Zui.handle("boxpreferences_3", { value: Config.raw.camera_rotation_speed });
@@ -55,7 +47,7 @@ class BoxPreferences {
 				hspeed = Zui.handle("boxpreferences_4", { value: Config.raw.camera_pan_speed });
 				Config.raw.camera_pan_speed = ui.slider(hspeed, tr("Camera Pan Speed"), 0.1, 4.0, true);
 
-				var zoomDirectionHandle = Zui.handle("boxpreferences_5", { position: Config.raw.zoom_direction });
+				let zoomDirectionHandle = Zui.handle("boxpreferences_5", { position: Config.raw.zoom_direction });
 				ui.combo(zoomDirectionHandle, [tr("Vertical"), tr("Vertical Inverted"), tr("Horizontal"), tr("Horizontal Inverted"), tr("Vertical and Horizontal"), tr("Vertical and Horizontal Inverted")], tr("Direction to Zoom"), true);
 				if (zoomDirectionHandle.changed) {
 					Config.raw.zoom_direction = zoomDirectionHandle.position;
@@ -72,46 +64,46 @@ class BoxPreferences {
 					UIBase.inst.tagUIRedraw();
 				}
 
-				#if !(krom_android || krom_ios)
+				///if !(krom_android || krom_ios)
 				ui.changed = false;
 				Config.raw.touch_ui = ui.check(Zui.handle("boxpreferences_9", { selected: Config.raw.touch_ui }), tr("Touch UI"));
 				if (ui.changed) {
 					Zui.touchScroll = Zui.touchHold = Zui.touchTooltip = Config.raw.touch_ui;
 					Config.loadTheme(Config.raw.theme);
-					setScale();
+					BoxPreferences.setScale();
 					UIBase.inst.tagUIRedraw();
 				}
-				#end
+				///end
 
 				Config.raw.splash_screen = ui.check(Zui.handle("boxpreferences_10", { selected: Config.raw.splash_screen }), tr("Splash Screen"));
 
 				// ui.text("Node Editor");
-				// var gridSnap = ui.check(Zui.handle("boxpreferences_11", { selected: false }), "Grid Snap");
+				// let gridSnap = ui.check(Zui.handle("boxpreferences_11", { selected: false }), "Grid Snap");
 
 				ui.endElement();
 				ui.row([0.5, 0.5]);
 				if (ui.button(tr("Restore")) && !UIMenu.show) {
-					UIMenu.draw(function(ui: Zui) {
+					UIMenu.draw((ui: Zui) => {
 						if (UIMenu.menuButton(ui, tr("Confirm"))) {
-							App.notifyOnInit(function() {
+							App.notifyOnInit(() => {
 								ui.t.ELEMENT_H = Base.defaultElementH;
 								Config.restore();
-								setScale();
-								if (filesPlugin != null) for (f in filesPlugin) Plugin.stop(f);
-								filesPlugin = null;
-								filesKeymap = null;
+								BoxPreferences.setScale();
+								if (BoxPreferences.filesPlugin != null) for (let f of BoxPreferences.filesPlugin) Plugin.stop(f);
+								BoxPreferences.filesPlugin = null;
+								BoxPreferences.filesKeymap = null;
 								MakeMaterial.parseMeshMaterial();
 								MakeMaterial.parsePaintMaterial();
 							});
 						}
 						if (UIMenu.menuButton(ui, tr("Import..."))) {
-							UIFiles.show("json", false, false, function(path: String) {
-								Data.getBlob(path, function(b: js.lib.ArrayBuffer) {
-									var raw = Json.parse(System.bufferToString(b));
-									App.notifyOnInit(function() {
+							UIFiles.show("json", false, false, (path: string) => {
+								Data.getBlob(path, (b: ArrayBuffer) => {
+									let raw = JSON.parse(System.bufferToString(b));
+									App.notifyOnInit(() => {
 										ui.t.ELEMENT_H = Base.defaultElementH;
 										Config.importFrom(raw);
-										setScale();
+										BoxPreferences.setScale();
 										MakeMaterial.parseMeshMaterial();
 										MakeMaterial.parsePaintMaterial();
 									});
@@ -121,7 +113,7 @@ class BoxPreferences {
 					}, 2);
 				}
 				if (ui.button(tr("Reset Layout")) && !UIMenu.show) {
-					UIMenu.draw(function(ui: Zui) {
+					UIMenu.draw((ui: Zui) => {
 						if (UIMenu.menuButton(ui, tr("Confirm"))) {
 							Base.initLayout();
 							Config.save();
@@ -130,35 +122,35 @@ class BoxPreferences {
 				}
 			}
 
-			if (ui.tab(htab, tr("Theme"), true)) {
+			if (ui.tab(BoxPreferences.htab, tr("Theme"), true)) {
 
-				if (themes == null) {
-					fetchThemes();
+				if (BoxPreferences.themes == null) {
+					BoxPreferences.fetchThemes();
 				}
-				themeHandle = Zui.handle("boxpreferences_12", { position: getThemeIndex() });
+				BoxPreferences.themeHandle = Zui.handle("boxpreferences_12", { position: BoxPreferences.getThemeIndex() });
 
 				ui.beginSticky();
 				ui.row([1 / 4, 1 / 4, 1 / 4, 1 / 4]);
 
-				ui.combo(themeHandle, themes, tr("Theme"));
-				if (themeHandle.changed) {
-					Config.raw.theme = themes[themeHandle.position] + ".json";
+				ui.combo(BoxPreferences.themeHandle, BoxPreferences.themes, tr("Theme"));
+				if (BoxPreferences.themeHandle.changed) {
+					Config.raw.theme = BoxPreferences.themes[BoxPreferences.themeHandle.position] + ".json";
 					Config.loadTheme(Config.raw.theme);
 				}
 
 				if (ui.button(tr("New"))) {
-					UIBox.showCustom(function(ui: Zui) {
+					UIBox.showCustom((ui: Zui) => {
 						if (ui.tab(Zui.handle("boxpreferences_13"), tr("New Theme"))) {
 							ui.row([0.5, 0.5]);
-							var themeName = ui.textInput(Zui.handle("boxpreferences_14", { text: "new_theme" }), tr("Name"));
+							let themeName = ui.textInput(Zui.handle("boxpreferences_14", { text: "new_theme" }), tr("Name"));
 							if (ui.button(tr("OK")) || ui.isReturnDown) {
-								var template = Json.stringify(Base.theme);
+								let template = JSON.stringify(Base.theme);
 								if (!themeName.endsWith(".json")) themeName += ".json";
-								var path = Path.data() + Path.sep + "themes" + Path.sep + themeName;
+								let path = Path.data() + Path.sep + "themes" + Path.sep + themeName;
 								Krom.fileSaveBytes(path, System.stringToBuffer(template));
-								fetchThemes(); // Refresh file list
+								BoxPreferences.fetchThemes(); // Refresh file list
 								Config.raw.theme = themeName;
-								themeHandle.position = getThemeIndex();
+								BoxPreferences.themeHandle.position = BoxPreferences.getThemeIndex();
 								UIBox.hide();
 								BoxPreferences.htab.position = 1; // Themes
 								BoxPreferences.show();
@@ -168,48 +160,48 @@ class BoxPreferences {
 				}
 
 				if (ui.button(tr("Import"))) {
-					UIFiles.show("json", false, false, function(path: String) {
+					UIFiles.show("json", false, false, (path: string) => {
 						ImportTheme.run(path);
 					});
 				}
 
 				if (ui.button(tr("Export"))) {
-					UIFiles.show("json", true, false, function(path) {
+					UIFiles.show("json", true, false, (path: string) => {
 						path += Path.sep + UIFiles.filename;
 						if (!path.endsWith(".json")) path += ".json";
-						Krom.fileSaveBytes(path, System.stringToBuffer(Json.stringify(Base.theme)));
+						Krom.fileSaveBytes(path, System.stringToBuffer(JSON.stringify(Base.theme)));
 					});
 				}
 
 				ui.endSticky();
 
-				var i = 0;
-				var theme = Base.theme;
-				var hlist = Zui.handle("boxpreferences_15");
+				let i = 0;
+				let theme: any = Base.theme;
+				let hlist = Zui.handle("boxpreferences_15");
 
 				// Viewport color
-				var h = hlist.nest(i++, { color: worldColor });
+				let h = hlist.nest(i++, { color: BoxPreferences.worldColor });
 				ui.row([1 / 8, 7 / 8]);
 				ui.text("", 0, h.color);
 				if (ui.isHovered && ui.inputReleased) {
-					UIMenu.draw(function(ui) {
+					UIMenu.draw((ui) => {
 						ui.changed = false;
 						ui.colorWheel(h, false, null, 11 * ui.t.ELEMENT_H * ui.SCALE(), true);
 						if (ui.changed) UIMenu.keepOpen = true;
 					}, 11);
 				}
-				var val = untyped h.color;
-				if (val < 0) val += untyped 4294967296;
-				h.text = untyped val.toString(16);
+				let val = h.color;
+				if (val < 0) val += 4294967296;
+				h.text = val.toString(16);
 				ui.textInput(h, "VIEWPORT_COL");
-				h.color = untyped parseInt(h.text, 16);
-
-				if (worldColor != h.color) {
-					worldColor = h.color;
-					var b = new js.lib.Uint8Array(4);
-					b[0] = worldColor.Rb;
-					b[1] = worldColor.Gb;
-					b[2] = worldColor.Bb;
+				h.color = parseInt(h.text, 16);
+
+				if (BoxPreferences.worldColor != h.color) {
+					BoxPreferences.worldColor = h.color;
+					let b = new Uint8Array(4);
+					b[0] = color_get_rb(BoxPreferences.worldColor);
+					b[1] = color_get_gb(BoxPreferences.worldColor);
+					b[2] = color_get_bb(BoxPreferences.worldColor);
 					b[3] = 255;
 					Context.raw.emptyEnvmap = Image.fromBytes(b.buffer, 1, 1);
 					Context.raw.ddirty = 2;
@@ -219,26 +211,26 @@ class BoxPreferences {
 				}
 
 				// Theme fields
-				for (key in Type.getInstanceFields(zui.Zui.Theme)) {
+				for (let key in theme) {
 					if (key == "theme_") continue;
 					if (key.startsWith("set_")) continue;
 					if (key.startsWith("get_")) key = key.substr(4);
 
-					var h = hlist.nest(i++);
-					var val: Dynamic = Reflect.getProperty(theme, key);
+					let h = hlist.nest(i++);
+					let val: any = theme[key];
 
-					var isHex = key.endsWith("_COL");
-					if (isHex && val < 0) val += untyped 4294967296;
+					let isHex = key.endsWith("_COL");
+					if (isHex && val < 0) val += 4294967296;
 
 					if (isHex) {
 						ui.row([1 / 8, 7 / 8]);
 						ui.text("", 0, val);
 						if (ui.isHovered && ui.inputReleased) {
-							h.color = Reflect.getProperty(theme, key);
-							UIMenu.draw(function(ui) {
+							h.color = theme[key];
+							UIMenu.draw((ui) => {
 								ui.changed = false;
-								var color = ui.colorWheel(h, false, null, 11 * ui.t.ELEMENT_H * ui.SCALE(), true);
-								Reflect.setProperty(theme, key, color);
+								let color = ui.colorWheel(h, false, null, 11 * ui.t.ELEMENT_H * ui.SCALE(), true);
+								theme[key] = color;
 								if (ui.changed) UIMenu.keepOpen = true;
 							}, 11);
 						}
@@ -246,169 +238,172 @@ class BoxPreferences {
 
 					ui.changed = false;
 
-					if (Std.isOfType(val, Bool)) {
+					if (typeof val == "boolean") {
 						h.selected = val;
-						var b = ui.check(h, key);
-						Reflect.setProperty(theme, key, b);
+						let b = ui.check(h, key);
+						theme[key] = b;
 					}
 					else if (key == "LINK_STYLE") {
-						var styles = [tr("Straight"), tr("Curved")];
+						let styles = [tr("Straight"), tr("Curved")];
 						h.position = val;
-						var i = ui.combo(h, styles, key, true);
-						Reflect.setProperty(theme, key, i);
+						let i = ui.combo(h, styles, key, true);
+						theme[key] = i;
 					}
 					else {
-						h.text = isHex ? untyped val.toString(16) : untyped val.toString();
-						var res = ui.textInput(h, key);
-						if (isHex) Reflect.setProperty(theme, key, untyped parseInt(h.text, 16));
-						else Reflect.setProperty(theme, key, untyped parseInt(h.text));
+						h.text = isHex ? val.toString(16) : val.toString();
+						let res = ui.textInput(h, key);
+						if (isHex) theme[key] = parseInt(h.text, 16);
+						else theme[key] = parseInt(h.text);
 					}
 
 					if (ui.changed) {
-						for (ui in Base.getUIs()) {
+						for (let ui of Base.getUIs()) {
 							ui.elementsBaked = false;
 						}
 					}
 				}
 			}
 
-			if (ui.tab(htab, tr("Usage"), true)) {
+			if (ui.tab(BoxPreferences.htab, tr("Usage"), true)) {
 				Context.raw.undoHandle = Zui.handle("boxpreferences_16", { value: Config.raw.undo_steps });
-				Config.raw.undo_steps = Std.int(ui.slider(Context.raw.undoHandle, tr("Undo Steps"), 1, 64, false, 1));
+				Config.raw.undo_steps = Math.floor(ui.slider(Context.raw.undoHandle, tr("Undo Steps"), 1, 64, false, 1));
 				if (Config.raw.undo_steps < 1) {
-					Config.raw.undo_steps = Std.int(Context.raw.undoHandle.value = 1);
+					Config.raw.undo_steps = Math.floor(Context.raw.undoHandle.value = 1);
 				}
 				if (Context.raw.undoHandle.changed) {
 					ui.g.end();
 
-					#if (is_paint || is_sculpt)
+					///if (is_paint || is_sculpt)
 					while (History.undoLayers.length < Config.raw.undo_steps) {
-						var l = new SlotLayer("_undo" + History.undoLayers.length);
+						let l = new SlotLayer("_undo" + History.undoLayers.length);
 						History.undoLayers.push(l);
 					}
 					while (History.undoLayers.length > Config.raw.undo_steps) {
-						var l = History.undoLayers.pop();
+						let l = History.undoLayers.pop();
 						l.unload();
 					}
-					#end
+					///end
 
 					History.reset();
 					ui.g.begin(false);
 				}
 
-				#if is_paint
-				Config.raw.dilate_radius = Std.int(ui.slider(Zui.handle("boxpreferences_17", { value: Config.raw.dilate_radius }), tr("Dilate Radius"), 0.0, 16.0, true, 1));
+				///if is_paint
+				Config.raw.dilate_radius = Math.floor(ui.slider(Zui.handle("boxpreferences_17", { value: Config.raw.dilate_radius }), tr("Dilate Radius"), 0.0, 16.0, true, 1));
 				if (ui.isHovered) ui.tooltip(tr("Dilate painted textures to prevent seams"));
 
-				var dilateHandle = Zui.handle("boxpreferences_18", { position: Config.raw.dilate });
+				let dilateHandle = Zui.handle("boxpreferences_18", { position: Config.raw.dilate });
 				ui.combo(dilateHandle, [tr("Instant"), tr("Delayed")], tr("Dilate"), true);
 				if (dilateHandle.changed) {
 					Config.raw.dilate = dilateHandle.position;
 				}
-				#end
+				///end
 
-				#if is_lab
-				var workspaceHandle = Zui.handle("boxpreferences_19", { position: Config.raw.workspace });
+				///if is_lab
+				let workspaceHandle = Zui.handle("boxpreferences_19", { position: Config.raw.workspace });
 				ui.combo(workspaceHandle, [tr("3D View"), tr("2D View")], tr("Default Workspace"), true);
 				if (workspaceHandle.changed) {
 					Config.raw.workspace = workspaceHandle.position;
 				}
-				#end
+				///end
 
-				var cameraControlsHandle = Zui.handle("boxpreferences_20", { position: Config.raw.camera_controls });
+				let cameraControlsHandle = Zui.handle("boxpreferences_20", { position: Config.raw.camera_controls });
 				ui.combo(cameraControlsHandle, [tr("Orbit"), tr("Rotate"), tr("Fly")], tr("Default Camera Controls"), true);
 				if (cameraControlsHandle.changed) {
 					Config.raw.camera_controls = cameraControlsHandle.position;
 				}
 
-				var layerResHandle = Zui.handle("boxpreferences_21", { position: Config.raw.layer_res });
+				let layerResHandle = Zui.handle("boxpreferences_21", { position: Config.raw.layer_res });
 
-				#if is_paint
-				#if (krom_android || krom_ios)
+				///if is_paint
+				///if (krom_android || krom_ios)
 				ui.combo(layerResHandle, ["128", "256", "512", "1K", "2K", "4K"], tr("Default Layer Resolution"), true);
-				#else
+				///else
 				ui.combo(layerResHandle, ["128", "256", "512", "1K", "2K", "4K", "8K"], tr("Default Layer Resolution"), true);
-				#end
-				#end
+				///end
+				///end
 
-				#if is_lab
-				#if (krom_android || krom_ios)
+				///if is_lab
+				///if (krom_android || krom_ios)
 				ui.combo(layerResHandle, ["2K", "4K"], tr("Default Layer Resolution"), true);
-				#else
+				///else
 				ui.combo(layerResHandle, ["2K", "4K", "8K", "16K"], tr("Default Layer Resolution"), true);
-				#end
-				#end
+				///end
+				///end
 
 				if (layerResHandle.changed) {
 					Config.raw.layer_res = layerResHandle.position;
 				}
 
-				var serverHandle = Zui.handle("boxpreferences_22", { text: Config.raw.server });
+				let serverHandle = Zui.handle("boxpreferences_22", { text: Config.raw.server });
 				Config.raw.server = ui.textInput(serverHandle, tr("Cloud Server"));
 
-				#if (is_paint || is_sculpt)
-				var materialLiveHandle = Zui.handle("boxpreferences_23", {selected: Config.raw.material_live });
+				///if (is_paint || is_sculpt)
+				let materialLiveHandle = Zui.handle("boxpreferences_23", {selected: Config.raw.material_live });
 				Config.raw.material_live = ui.check(materialLiveHandle, tr("Live Material Preview"));
 				if (ui.isHovered) ui.tooltip(tr("Instantly update material preview on node change"));
 
-				var brushLiveHandle = Zui.handle("boxpreferences_24", { selected: Config.raw.brush_live });
+				let brushLiveHandle = Zui.handle("boxpreferences_24", { selected: Config.raw.brush_live });
 				Config.raw.brush_live = ui.check(brushLiveHandle, tr("Live Brush Preview"));
 				if (ui.isHovered) ui.tooltip(tr("Draw live brush preview in viewport"));
 				if (brushLiveHandle.changed) Context.raw.ddirty = 2;
 
-				var brush3dHandle = Zui.handle("boxpreferences_25", { selected: Config.raw.brush_3d });
+				let brush3dHandle = Zui.handle("boxpreferences_25", { selected: Config.raw.brush_3d });
 				Config.raw.brush_3d = ui.check(brush3dHandle, tr("3D Cursor"));
 				if (brush3dHandle.changed) MakeMaterial.parsePaintMaterial();
 
 				ui.enabled = Config.raw.brush_3d;
-				var brushDepthRejectHandle = Zui.handle("boxpreferences_26", { selected: Config.raw.brush_depth_reject });
+				let brushDepthRejectHandle = Zui.handle("boxpreferences_26", { selected: Config.raw.brush_depth_reject });
 				Config.raw.brush_depth_reject = ui.check(brushDepthRejectHandle, tr("Depth Reject"));
 				if (brushDepthRejectHandle.changed) MakeMaterial.parsePaintMaterial();
 
 				ui.row([0.5, 0.5]);
 
-				var brushAngleRejectHandle = Zui.handle("boxpreferences_27", { selected: Config.raw.brush_angle_reject });
+				let brushAngleRejectHandle = Zui.handle("boxpreferences_27", { selected: Config.raw.brush_angle_reject });
 				Config.raw.brush_angle_reject = ui.check(brushAngleRejectHandle, tr("Angle Reject"));
 				if (brushAngleRejectHandle.changed) MakeMaterial.parsePaintMaterial();
 
 				if (!Config.raw.brush_angle_reject) ui.enabled = false;
-				var angleDotHandle = Zui.handle("boxpreferences_28", { value: Context.raw.brushAngleRejectDot });
+				let angleDotHandle = Zui.handle("boxpreferences_28", { value: Context.raw.brushAngleRejectDot });
 				Context.raw.brushAngleRejectDot = ui.slider(angleDotHandle, tr("Angle"), 0.0, 1.0, true);
 				if (angleDotHandle.changed) {
 					MakeMaterial.parsePaintMaterial();
 				}
 				ui.enabled = true;
-				#end
+				///end
 
-				#if is_lab
+				///if is_lab
 				Config.raw.gpu_inference = ui.check(Zui.handle("boxpreferences_29", { selected: Config.raw.gpu_inference }), tr("Use GPU"));
 				if (ui.isHovered) ui.tooltip(tr("Use GPU to accelerate node graph processing"));
-				#end
+				///end
 			}
 
-			#if krom_ios
-			if (ui.tab(htab, tr("Pencil"), true)) {
-			#else
-			if (ui.tab(htab, tr("Pen"), true)) {
-			#end
+			let penName;
+			///if krom_ios
+			penName = tr("Pencil");
+			///else
+			penName = tr("Pen");
+			///end
+
+			if (ui.tab(BoxPreferences.htab, penName, true)) {
 				ui.text(tr("Pressure controls"));
 				Config.raw.pressure_radius = ui.check(Zui.handle("boxpreferences_30", { selected: Config.raw.pressure_radius }), tr("Brush Radius"));
 				Config.raw.pressure_sensitivity = ui.slider(Zui.handle("boxpreferences_31", { value: Config.raw.pressure_sensitivity }), tr("Sensitivity"), 0.0, 10.0, true);
-				#if (is_paint || is_sculpt)
+				///if (is_paint || is_sculpt)
 				Config.raw.pressure_hardness = ui.check(Zui.handle("boxpreferences_32", { selected: Config.raw.pressure_hardness }), tr("Brush Hardness"));
 				Config.raw.pressure_opacity = ui.check(Zui.handle("boxpreferences_33", { selected: Config.raw.pressure_opacity }), tr("Brush Opacity"));
 				Config.raw.pressure_angle = ui.check(Zui.handle("boxpreferences_34", { selected: Config.raw.pressure_angle }), tr("Brush Angle"));
-				#end
+				///end
 
 				ui.endElement();
 				ui.row([0.5]);
 				if (ui.button(tr("Help"))) {
-					#if (is_paint || is_sculpt)
-					File.loadUrl("https://github.com/armory3d/armorpaint_docs#pen");
-					#end
-					#if is_lab
-					File.loadUrl("https://github.com/armory3d/armorlab_docs#pen");
-					#end
+					///if (is_paint || is_sculpt)
+					File.loadUrl("https://github.com/armory3d/armorpaint_docs///pen");
+					///end
+					///if is_lab
+					File.loadUrl("https://github.com/armory3d/armorlab_docs///pen");
+					///end
 				}
 			}
 
@@ -417,18 +412,18 @@ class BoxPreferences {
 			Context.raw.hbloom = Zui.handle("boxpreferences_37", { selected: Config.raw.rp_bloom });
 			Context.raw.hsupersample = Zui.handle("boxpreferences_38", { position: Config.getSuperSampleQuality(Config.raw.rp_supersample) });
 			Context.raw.hvxao = Zui.handle("boxpreferences_39", { selected: Config.raw.rp_gi });
-			if (ui.tab(htab, tr("Viewport"), true)) {
-				#if (krom_direct3d12 || krom_vulkan || krom_metal)
+			if (ui.tab(BoxPreferences.htab, tr("Viewport"), true)) {
+				///if (krom_direct3d12 || krom_vulkan || krom_metal)
 
-				var hpathtracemode = Zui.handle("boxpreferences_40", { position: Context.raw.pathTraceMode });
+				let hpathtracemode = Zui.handle("boxpreferences_40", { position: Context.raw.pathTraceMode });
 				Context.raw.pathTraceMode = ui.combo(hpathtracemode, [tr("Core"), tr("Full")], tr("Path Tracer"), true);
 				if (hpathtracemode.changed) {
 					RenderPathRaytrace.ready = false;
 				}
 
-				#end
+				///end
 
-				var hrendermode = Zui.handle("boxpreferences_41", { position: Context.raw.renderMode });
+				let hrendermode = Zui.handle("boxpreferences_41", { position: Context.raw.renderMode });
 				Context.raw.renderMode = ui.combo(hrendermode, [tr("Full"), tr("Mobile")], tr("Renderer"), true);
 				if (hrendermode.changed) {
 					Context.setRenderPath();
@@ -437,8 +432,8 @@ class BoxPreferences {
 				ui.combo(Context.raw.hsupersample, ["0.25x", "0.5x", "1.0x", "1.5x", "2.0x", "4.0x"], tr("Super Sample"), true);
 				if (Context.raw.hsupersample.changed) Config.applyConfig();
 
-				if (Context.raw.renderMode == RenderDeferred) {
-					#if arm_voxels
+				if (Context.raw.renderMode == RenderMode.RenderDeferred) {
+					///if arm_voxels
 					ui.check(Context.raw.hvxao, tr("Voxel AO"));
 					if (ui.isHovered) ui.tooltip(tr("Cone-traced AO and shadows"));
 					if (Context.raw.hvxao.changed) {
@@ -446,14 +441,14 @@ class BoxPreferences {
 					}
 
 					ui.enabled = Context.raw.hvxao.selected;
-					var h = Zui.handle("boxpreferences_42", { value: Context.raw.vxaoOffset });
+					let h = Zui.handle("boxpreferences_42", { value: Context.raw.vxaoOffset });
 					Context.raw.vxaoOffset = ui.slider(h, tr("Cone Offset"), 1.0, 4.0, true);
 					if (h.changed) Context.raw.ddirty = 2;
-					var h = Zui.handle("boxpreferences_43", { value: Context.raw.vxaoAperture });
+					h = Zui.handle("boxpreferences_43", { value: Context.raw.vxaoAperture });
 					Context.raw.vxaoAperture = ui.slider(h, tr("Aperture"), 1.0, 4.0, true);
 					if (h.changed) Context.raw.ddirty = 2;
 					ui.enabled = true;
-					#end
+					///end
 
 					ui.check(Context.raw.hssao, tr("SSAO"));
 					if (Context.raw.hssao.changed) Config.applyConfig();
@@ -463,67 +458,67 @@ class BoxPreferences {
 					if (Context.raw.hbloom.changed) Config.applyConfig();
 				}
 
-				var h = Zui.handle("boxpreferences_44", { value: Config.raw.rp_vignette });
+				let h = Zui.handle("boxpreferences_44", { value: Config.raw.rp_vignette });
 				Config.raw.rp_vignette = ui.slider(h, tr("Vignette"), 0.0, 1.0, true);
 				if (h.changed) Context.raw.ddirty = 2;
 
-				var h = Zui.handle("boxpreferences_45", { value: Config.raw.rp_grain });
+				h = Zui.handle("boxpreferences_45", { value: Config.raw.rp_grain });
 				Config.raw.rp_grain = ui.slider(h, tr("Noise Grain"), 0.0, 1.0, true);
 				if (h.changed) Context.raw.ddirty = 2;
 
-				// var h = Zui.handle("boxpreferences_46", { value: Context.raw.autoExposureStrength });
+				// let h = Zui.handle("boxpreferences_46", { value: Context.raw.autoExposureStrength });
 				// Context.raw.autoExposureStrength = ui.slider(h, "Auto Exposure", 0.0, 2.0, true);
 				// if (h.changed) Context.raw.ddirty = 2;
 
-				var cam = Scene.active.camera;
-				var camRaw = cam.data.raw;
-				var near_handle = Zui.handle("boxpreferences_47");
-				var far_handle = Zui.handle("boxpreferences_48");
-				near_handle.value = Std.int(camRaw.near_plane * 1000) / 1000;
-				far_handle.value = Std.int(camRaw.far_plane * 100) / 100;
+				let cam = Scene.active.camera;
+				let camRaw = cam.data.raw;
+				let near_handle = Zui.handle("boxpreferences_47");
+				let far_handle = Zui.handle("boxpreferences_48");
+				near_handle.value = Math.floor(camRaw.near_plane * 1000) / 1000;
+				far_handle.value = Math.floor(camRaw.far_plane * 100) / 100;
 				camRaw.near_plane = ui.slider(near_handle, tr("Clip Start"), 0.001, 1.0, true);
 				camRaw.far_plane = ui.slider(far_handle, tr("Clip End"), 50.0, 100.0, true);
 				if (near_handle.changed || far_handle.changed) {
 					cam.buildProjection();
 				}
 
-				var dispHandle = Zui.handle("boxpreferences_49", { value: Config.raw.displace_strength });
+				let dispHandle = Zui.handle("boxpreferences_49", { value: Config.raw.displace_strength });
 				Config.raw.displace_strength = ui.slider(dispHandle, tr("Displacement Strength"), 0.0, 10.0, true);
 				if (dispHandle.changed) {
 					Context.raw.ddirty = 2;
 					MakeMaterial.parseMeshMaterial();
 				}
 			}
-			if (ui.tab(htab, tr("Keymap"), true)) {
+			if (ui.tab(BoxPreferences.htab, tr("Keymap"), true)) {
 
-				if (filesKeymap == null) {
-					fetchKeymaps();
+				if (BoxPreferences.filesKeymap == null) {
+					BoxPreferences.fetchKeymaps();
 				}
 
 				ui.beginSticky();
 				ui.row([1 / 4, 1 / 4, 1 / 4, 1 / 4]);
 
-				presetHandle = Zui.handle("boxpreferences_50", { position: getPresetIndex() });
-				ui.combo(presetHandle, filesKeymap, tr("Preset"));
-				if (presetHandle.changed) {
-					Config.raw.keymap = filesKeymap[presetHandle.position] + ".json";
+				BoxPreferences.presetHandle = Zui.handle("boxpreferences_50", { position: BoxPreferences.getPresetIndex() });
+				ui.combo(BoxPreferences.presetHandle, BoxPreferences.filesKeymap, tr("Preset"));
+				if (BoxPreferences.presetHandle.changed) {
+					Config.raw.keymap = BoxPreferences.filesKeymap[BoxPreferences.presetHandle.position] + ".json";
 					Config.applyConfig();
 					Config.loadKeymap();
 				}
 
 				if (ui.button(tr("New"))) {
-					UIBox.showCustom(function(ui: Zui) {
+					UIBox.showCustom((ui: Zui) => {
 						if (ui.tab(Zui.handle("boxpreferences_51"), tr("New Keymap"))) {
 							ui.row([0.5, 0.5]);
-							var keymapName = ui.textInput(Zui.handle("boxpreferences_52", { text: "new_keymap" }), tr("Name"));
+							let keymapName = ui.textInput(Zui.handle("boxpreferences_52", { text: "new_keymap" }), tr("Name"));
 							if (ui.button(tr("OK")) || ui.isReturnDown) {
-								var template = Json.stringify(Base.defaultKeymap);
+								let template = JSON.stringify(Base.defaultKeymap);
 								if (!keymapName.endsWith(".json")) keymapName += ".json";
-								var path = Path.data() + Path.sep + "keymap_presets" + Path.sep + keymapName;
+								let path = Path.data() + Path.sep + "keymap_presets" + Path.sep + keymapName;
 								Krom.fileSaveBytes(path, System.stringToBuffer(template));
-								fetchKeymaps(); // Refresh file list
+								BoxPreferences.fetchKeymaps(); // Refresh file list
 								Config.raw.keymap = keymapName;
-								presetHandle.position = getPresetIndex();
+								BoxPreferences.presetHandle.position = BoxPreferences.getPresetIndex();
 								UIBox.hide();
 								BoxPreferences.htab.position = 5; // Keymap
 								BoxPreferences.show();
@@ -533,14 +528,14 @@ class BoxPreferences {
 				}
 
 				if (ui.button(tr("Import"))) {
-					UIFiles.show("json", false, false, function(path: String) {
+					UIFiles.show("json", false, false, (path: string) => {
 						ImportKeymap.run(path);
 					});
 				}
 				if (ui.button(tr("Export"))) {
-					UIFiles.show("json", true, false, function(dest: String) {
+					UIFiles.show("json", true, false, (dest: string) => {
 						if (!UIFiles.filename.endsWith(".json")) UIFiles.filename += ".json";
-						var path = Path.data() + Path.sep + "keymap_presets" + Path.sep + Config.raw.keymap;
+						let path = Path.data() + Path.sep + "keymap_presets" + Path.sep + Config.raw.keymap;
 						File.copy(path, dest + Path.sep + UIFiles.filename);
 					});
 				}
@@ -549,43 +544,43 @@ class BoxPreferences {
 
 				ui.separator(8, false);
 
-				var i = 0;
+				let i = 0;
 				ui.changed = false;
-				for (key in Reflect.fields(Config.keymap)) {
-					var h = Zui.handle("boxpreferences_53").nest(i++);
-					h.text = Reflect.field(Config.keymap, key);
-					var text = ui.textInput(h, key, Left);
-					Reflect.setField(Config.keymap, key, text);
+				for (let key in Config.keymap) {
+					let h = Zui.handle("boxpreferences_53").nest(i++);
+					h.text = Config.keymap[key];
+					let text = ui.textInput(h, key, Align.Left);
+					Config.keymap[key] = text;
 				}
 				if (ui.changed) {
 					Config.applyConfig();
 					Config.saveKeymap();
 				}
 			}
-			if (ui.tab(htab, tr("Plugins"), true)) {
+			if (ui.tab(BoxPreferences.htab, tr("Plugins"), true)) {
 				ui.beginSticky();
 				ui.row([1 / 4, 1 / 4]);
 				if (ui.button(tr("New"))) {
-					UIBox.showCustom(function(ui: Zui) {
+					UIBox.showCustom((ui: Zui) => {
 						if (ui.tab(Zui.handle("boxpreferences_54"), tr("New Plugin"))) {
 							ui.row([0.5, 0.5]);
-							var pluginName = ui.textInput(Zui.handle("boxpreferences_55", { text: "new_plugin" }), tr("Name"));
+							let pluginName = ui.textInput(Zui.handle("boxpreferences_55", { text: "new_plugin" }), tr("Name"));
 							if (ui.button(tr("OK")) || ui.isReturnDown) {
-								var template =
-"let plugin = new arm.Plugin();
+								let template =
+`let plugin = new arm.Plugin();
 let h1 = new zui.Handle();
-plugin.drawUI = function(ui) {
+plugin.drawUI = (ui) { =>
 	if (ui.panel(h1, 'New Plugin')) {
 		if (ui.button('Button')) {
 			console.error('Hello');
 		}
 	}
 }
-";
+`;
 								if (!pluginName.endsWith(".js")) pluginName += ".js";
-								var path = Path.data() + Path.sep + "plugins" + Path.sep + pluginName;
+								let path = Path.data() + Path.sep + "plugins" + Path.sep + pluginName;
 								Krom.fileSaveBytes(path, System.stringToBuffer(template));
-								filesPlugin = null; // Refresh file list
+								BoxPreferences.filesPlugin = null; // Refresh file list
 								UIBox.hide();
 								BoxPreferences.htab.position = 6; // Plugins
 								BoxPreferences.show();
@@ -594,38 +589,38 @@ plugin.drawUI = function(ui) {
 					});
 				}
 				if (ui.button(tr("Import"))) {
-					UIFiles.show("js,wasm,zip", false, false, function(path: String) {
+					UIFiles.show("js,wasm,zip", false, false, (path: string) => {
 						ImportPlugin.run(path);
 					});
 				}
 				ui.endSticky();
 
-				if (filesPlugin == null) {
-					fetchPlugins();
+				if (BoxPreferences.filesPlugin == null) {
+					BoxPreferences.fetchPlugins();
 				}
 
 				if (Config.raw.plugins == null) Config.raw.plugins = [];
-				var h = Zui.handle("boxpreferences_56", { selected: false });
-				for (f in filesPlugin) {
-					var isJs = f.endsWith(".js");
-					var isWasm = false; //f.endsWith(".wasm");
+				let h = Zui.handle("boxpreferences_56", { selected: false });
+				for (let f of BoxPreferences.filesPlugin) {
+					let isJs = f.endsWith(".js");
+					let isWasm = false; //f.endsWith(".wasm");
 					if (!isJs && !isWasm) continue;
-					var enabled = Config.raw.plugins.indexOf(f) >= 0;
+					let enabled = Config.raw.plugins.indexOf(f) >= 0;
 					h.selected = enabled;
-					var tag = isJs ? f.split(".")[0] : f;
+					let tag = isJs ? f.split(".")[0] : f;
 					ui.check(h, tag);
 					if (h.changed && h.selected != enabled) {
 						h.selected ? Config.enablePlugin(f) : Config.disablePlugin(f);
 						Base.redrawUI();
 					}
 					if (ui.isHovered && ui.inputReleasedR) {
-						UIMenu.draw(function(ui: Zui) {
-							var path = Path.data() + Path.sep + "plugins" + Path.sep + f;
+						UIMenu.draw((ui: Zui) => {
+							let path = Path.data() + Path.sep + "plugins" + Path.sep + f;
 							if (UIMenu.menuButton(ui, tr("Edit in Text Editor"))) {
 								File.start(path);
 							}
 							if (UIMenu.menuButton(ui, tr("Edit in Script Tab"))) {
-								Data.getBlob("plugins/" + f, function(blob: js.lib.ArrayBuffer) {
+								Data.getBlob("plugins/" + f, (blob: ArrayBuffer) => {
 									TabScript.hscript.text = System.bufferToString(blob);
 									Data.deleteBlob("plugins/" + f);
 									Console.info(tr("Script opened"));
@@ -633,67 +628,68 @@ plugin.drawUI = function(ui) {
 
 							}
 							if (UIMenu.menuButton(ui, tr("Export"))) {
-								UIFiles.show("js", true, false, function(dest: String) {
+								UIFiles.show("js", true, false, (dest: string) => {
 									if (!UIFiles.filename.endsWith(".js")) UIFiles.filename += ".js";
 									File.copy(path, dest + Path.sep + UIFiles.filename);
 								});
 							}
 							if (UIMenu.menuButton(ui, tr("Delete"))) {
 								if (Config.raw.plugins.indexOf(f) >= 0) {
-									Config.raw.plugins.remove(f);
+									array_remove(Config.raw.plugins, f);
 									Plugin.stop(f);
 								}
-								filesPlugin.remove(f);
+								array_remove(BoxPreferences.filesPlugin, f);
 								File.delete(path);
 							}
 						}, 4);
 					}
 				}
 			}
-		}, 620, Config.raw.touch_ui ? 480 : 420, function() { Config.save(); });
+
+		}, 620, Config.raw.touch_ui ? 480 : 420, () => { Config.save(); });
 	}
 
-	public static function fetchThemes() {
-		themes = File.readDirectory(Path.data() + Path.sep + "themes");
-		for (i in 0...themes.length) themes[i] = themes[i].substr(0, themes[i].length - 5); // Strip .json
-		themes.unshift("default");
+	static fetchThemes = () => {
+		BoxPreferences.themes = File.readDirectory(Path.data() + Path.sep + "themes");
+		for (let i = 0; i < BoxPreferences.themes.length; ++i) BoxPreferences.themes[i] = BoxPreferences.themes[i].substr(0, BoxPreferences.themes[i].length - 5); // Strip .json
+		BoxPreferences.themes.unshift("default");
 	}
 
-	public static function fetchKeymaps() {
-		filesKeymap = File.readDirectory(Path.data() + Path.sep + "keymap_presets");
-		for (i in 0...filesKeymap.length) {
-			filesKeymap[i] = filesKeymap[i].substr(0, filesKeymap[i].length - 5); // Strip .json
+	static fetchKeymaps = () => {
+		BoxPreferences.filesKeymap = File.readDirectory(Path.data() + Path.sep + "keymap_presets");
+		for (let i = 0; i < BoxPreferences.filesKeymap.length; ++i) {
+			BoxPreferences.filesKeymap[i] = BoxPreferences.filesKeymap[i].substr(0, BoxPreferences.filesKeymap[i].length - 5); // Strip .json
 		}
-		filesKeymap.unshift("default");
+		BoxPreferences.filesKeymap.unshift("default");
 	}
 
-	public static function fetchPlugins() {
-		filesPlugin = File.readDirectory(Path.data() + Path.sep + "plugins");
+	static fetchPlugins = () => {
+		BoxPreferences.filesPlugin = File.readDirectory(Path.data() + Path.sep + "plugins");
 	}
 
-	public static function getThemeIndex(): Int {
-		return themes.indexOf(Config.raw.theme.substr(0, Config.raw.theme.length - 5)); // Strip .json
+	static getThemeIndex = (): i32 => {
+		return BoxPreferences.themes.indexOf(Config.raw.theme.substr(0, Config.raw.theme.length - 5)); // Strip .json
 	}
 
-	public static function getPresetIndex(): Int {
-		return filesKeymap.indexOf(Config.raw.keymap.substr(0, Config.raw.keymap.length - 5)); // Strip .json
+	static getPresetIndex = (): i32 => {
+		return BoxPreferences.filesKeymap.indexOf(Config.raw.keymap.substr(0, Config.raw.keymap.length - 5)); // Strip .json
 	}
 
-	static function setScale() {
-		var scale = Config.raw.window_scale;
+	static setScale = () => {
+		let scale = Config.raw.window_scale;
 		UIBase.inst.ui.setScale(scale);
-		UIHeader.headerh = Std.int(UIHeader.defaultHeaderH * scale);
-		Config.raw.layout[LayoutStatusH] = Std.int(UIStatus.defaultStatusH * scale);
-		UIMenubar.inst.menubarw = Std.int(UIMenubar.defaultMenubarW * scale);
+		UIHeader.headerh = Math.floor(UIHeader.defaultHeaderH * scale);
+		Config.raw.layout[LayoutSize.LayoutStatusH] = Math.floor(UIStatus.defaultStatusH * scale);
+		UIMenubar.inst.menubarw = Math.floor(UIMenubar.defaultMenubarW * scale);
 		UIBase.inst.setIconScale();
 		UINodes.inst.ui.setScale(scale);
 		UIView2D.inst.ui.setScale(scale);
 		Base.uiBox.setScale(scale);
 		Base.uiMenu.setScale(scale);
 		Base.resize();
-		#if (is_paint || is_sculpt)
-		Config.raw.layout[LayoutSidebarW] = Std.int(UIBase.defaultSidebarW * scale);
-		UIToolbar.inst.toolbarw = Std.int(UIToolbar.defaultToolbarW * scale);
-		#end
+		///if (is_paint || is_sculpt)
+		Config.raw.layout[LayoutSize.LayoutSidebarW] = Math.floor(UIBase.defaultSidebarW * scale);
+		UIToolbar.inst.toolbarw = Math.floor(UIToolbar.defaultToolbarW * scale);
+		///end
 	}
 }

+ 251 - 0
base/Sources/BoxProjects.ts

@@ -0,0 +1,251 @@
+
+class BoxProjects {
+
+	static htab = new Handle();
+	static hsearch = new Handle();
+	static iconMap: Map<string, Image> = null;
+
+	static show = () => {
+		if (BoxProjects.iconMap != null) {
+			for (let handle of BoxProjects.iconMap.keys()) {
+				Data.deleteImage(handle);
+			}
+			BoxProjects.iconMap = null;
+		}
+
+		let draggable;
+		///if (krom_android || krom_ios)
+		draggable = false;
+		///else
+		draggable = true;
+		///end
+
+		UIBox.showCustom((ui: Zui) => {
+			///if (krom_android || krom_ios)
+			BoxProjects.alignToFullScreen();
+			///end
+
+			///if (krom_android || krom_ios)
+			BoxProjects.projectsTab(ui);
+			BoxProjects.getStartedTab(ui);
+			///else
+			BoxProjects.recentProjectsTab(ui);
+			///end
+
+		}, 600, 400, null, draggable);
+	}
+
+	static projectsTab = (ui: Zui) => {
+		if (ui.tab(BoxProjects.htab, tr("Projects"), true)) {
+			ui.beginSticky();
+
+			BoxProjects.drawBadge(ui);
+
+			if (ui.button(tr("New"))) {
+				Project.projectNew();
+				Viewport.scaleToBounds();
+				UIBox.hide();
+				// Pick unique name
+				let i = 0;
+				let j = 0;
+				let title = tr("untitled") + i;
+				while (j < Config.raw.recent_projects.length) {
+					let base = Config.raw.recent_projects[j];
+					base = base.substring(base.lastIndexOf(Path.sep) + 1, base.lastIndexOf("."));
+					j++;
+					if (title == base) {
+						i++;
+						title = tr("untitled") + i;
+						j = 0;
+					}
+				}
+				System.title = title;
+			}
+			ui.endSticky();
+			ui.separator(3, false);
+
+			let slotw = Math.floor(150 * ui.SCALE());
+			let num = Math.floor(System.width / slotw);
+			let recent_projects = Config.raw.recent_projects;
+			let show_asset_names = true;
+
+			for (let row = 0; row < Math.ceil(recent_projects.length / num); ++row) {
+				let mult = show_asset_names ? 2 : 1;
+				let ar = [];
+				for (let i = 0; i < num * mult; ++i) ar.push(1 / num);
+				ui.row(ar);
+
+				ui._x += 2;
+				let off = show_asset_names ? ui.ELEMENT_OFFSET() * 16.0 : 6;
+				if (row > 0) ui._y += off;
+
+				for (let j = 0; j < num; ++j) {
+					let imgw = Math.floor(128 * ui.SCALE());
+					let i = j + row * num;
+					if (i >= recent_projects.length) {
+						ui.endElement(imgw);
+						if (show_asset_names) ui.endElement(0);
+						continue;
+					}
+
+					let path = recent_projects[i];
+
+					///if krom_ios
+					let documentDirectory = Krom.saveDialog("", "");
+					documentDirectory = documentDirectory.substr(0, documentDirectory.length - 8); // Strip /'untitled'
+					path = documentDirectory + path;
+					///end
+
+					let iconPath = path.substr(0, path.length - 4) + "_icon.png";
+					if (BoxProjects.iconMap == null) BoxProjects.iconMap = new Map();
+					let icon = BoxProjects.iconMap.get(iconPath);
+					if (icon == null) {
+						Data.getImage(iconPath, (image: Image) => {
+							icon = image;
+							BoxProjects.iconMap.set(iconPath, icon);
+						});
+					}
+
+					let uix = ui._x;
+					if (icon != null) {
+						ui.fill(0, 0, 128, 128, ui.t.SEPARATOR_COL);
+
+						let state = ui.image(icon, 0xffffffff, 128  * ui.SCALE());
+						if (state == State.Released) {
+							let _uix = ui._x;
+							ui._x = uix;
+							ui.fill(0, 0, 128, 128, 0x66000000);
+							ui._x = _uix;
+							let doImport = () => {
+								App.notifyOnInit(() => {
+									UIBox.hide();
+									ImportArm.runProject(path);
+								});
+							}
+
+							///if (krom_android || krom_ios)
+							Base.notifyOnNextFrame(() => {
+								Console.toast(tr("Opening project"));
+								Base.notifyOnNextFrame(doImport);
+							});
+							///else
+							doImport();
+							///end
+						}
+
+						let name = path.substring(path.lastIndexOf(Path.sep) + 1, path.lastIndexOf("."));
+						if (ui.isHovered && ui.inputReleasedR) {
+							UIMenu.draw((ui: Zui) => {
+								// if (UIMenu.menuButton(ui, tr("Duplicate"))) {}
+								if (UIMenu.menuButton(ui, tr("Delete"))) {
+									App.notifyOnInit(() => {
+										File.delete(path);
+										File.delete(iconPath);
+										let dataPath = path.substr(0, path.length - 4);
+										File.delete(dataPath);
+										recent_projects.splice(i, 1);
+									});
+								}
+							}, 1);
+						}
+
+						if (show_asset_names) {
+							ui._x = uix - (150 - 128) / 2;
+							ui._y += slotw * 0.9;
+							ui.text(name, Align.Center);
+							if (ui.isHovered) ui.tooltip(name);
+							ui._y -= slotw * 0.9;
+							if (i == recent_projects.length - 1) {
+								ui._y += j == num - 1 ? imgw : imgw + ui.ELEMENT_H() + ui.ELEMENT_OFFSET();
+							}
+						}
+					}
+					else {
+						ui.endElement(0);
+						if (show_asset_names) ui.endElement(0);
+						ui._x = uix;
+					}
+				}
+
+				ui._y += 150;
+			}
+		}
+	}
+
+	static recentProjectsTab = (ui: Zui) => {
+		if (ui.tab(BoxProjects.htab, tr("Recent"), true)) {
+
+			BoxProjects.drawBadge(ui);
+
+			ui.enabled = Config.raw.recent_projects.length > 0;
+			BoxProjects.hsearch.text = ui.textInput(BoxProjects.hsearch, tr("Search"), Align.Left, true, true);
+			ui.enabled = true;
+
+			for (let path of Config.raw.recent_projects) {
+				let file = path;
+				///if krom_windows
+				file = path.replace("/", "\\");
+				///else
+				file = path.replace("\\", "/");
+				///end
+				file = file.substr(file.lastIndexOf(Path.sep) + 1);
+
+				if (file.toLowerCase().indexOf(BoxProjects.hsearch.text.toLowerCase()) < 0) continue; // Search filter
+
+				if (ui.button(file, Align.Left) && File.exists(path)) {
+					let current = Graphics2.current;
+					if (current != null) current.end();
+
+					ImportArm.runProject(path);
+
+					if (current != null) current.begin(false);
+					UIBox.hide();
+				}
+				if (ui.isHovered) ui.tooltip(path);
+			}
+
+			ui.enabled = Config.raw.recent_projects.length > 0;
+			if (ui.button(tr("Clear"), Align.Left)) {
+				Config.raw.recent_projects = [];
+				Config.save();
+			}
+			ui.enabled = true;
+
+			ui.endElement();
+			if (ui.button(tr("New Project..."), Align.Left)) Project.projectNewBox();
+			if (ui.button(tr("Open..."), Align.Left)) Project.projectOpen();
+		}
+	}
+
+	static drawBadge = (ui: Zui) => {
+		Data.getImage("badge.k", (img: Image) => {
+			ui.image(img);
+			ui.endElement();
+		});
+	}
+
+	static getStartedTab = (ui: Zui) => {
+		if (ui.tab(BoxProjects.htab, tr("Get Started"), true)) {
+			if (ui.button(tr("Manual"))) {
+				File.loadUrl(Manifest.url + "/manual");
+			}
+			if (ui.button(tr("How To"))) {
+				File.loadUrl(Manifest.url + "/howto");
+			}
+			if (ui.button(tr("What's New"))) {
+				File.loadUrl(Manifest.url + "/notes");
+			}
+		}
+	}
+
+	static alignToFullScreen = () => {
+		UIBox.modalW = Math.floor(System.width / Base.uiBox.SCALE());
+		UIBox.modalH = Math.floor(System.height / Base.uiBox.SCALE());
+		let appw = System.width;
+		let apph = System.height;
+		let mw = appw;
+		let mh = apph;
+		UIBox.hwnd.dragX = Math.floor(-appw / 2 + mw / 2);
+		UIBox.hwnd.dragY = Math.floor(-apph / 2 + mh / 2);
+	}
+}

+ 248 - 0
base/Sources/Camera.ts

@@ -0,0 +1,248 @@
+
+class Camera {
+
+	static inst: Camera;
+	origins: Vec4[];
+	views: Mat4[];
+	redraws = 0;
+	first = true;
+	dir = new Vec4();
+	ease = 1.0;
+	controlsDown = false;
+
+	constructor() {
+		Camera.inst = this;
+	}
+
+	update = () => {
+		let mouse = Input.getMouse();
+		let kb = Input.getKeyboard();
+		let camera = Scene.active.camera;
+
+		if (this.first) {
+			this.first = false;
+			this.reset();
+		}
+
+		if (mouse.viewX < 0 ||
+			mouse.viewX > App.w() ||
+			mouse.viewY < 0 ||
+			mouse.viewY > App.h()) {
+
+			if (Config.raw.wrap_mouse && this.controlsDown) {
+				if (mouse.viewX < 0) {
+					mouse.x = mouse.lastX = App.x() + App.w();
+					Krom.setMousePosition(Math.floor(mouse.x), Math.floor(mouse.y));
+				}
+				else if (mouse.viewX > App.w()) {
+					mouse.x = mouse.lastX = App.x();
+					Krom.setMousePosition(Math.floor(mouse.x), Math.floor(mouse.y));
+				}
+				else if (mouse.viewY < 0) {
+					mouse.y = mouse.lastY = App.y() + App.h();
+					Krom.setMousePosition(Math.floor(mouse.x), Math.floor(mouse.y));
+				}
+				else if (mouse.viewY > App.h()) {
+					mouse.y = mouse.lastY = App.y();
+					Krom.setMousePosition(Math.floor(mouse.x), Math.floor(mouse.y));
+				}
+			}
+			else {
+				return;
+			}
+		}
+
+		let modifKey = kb.down("alt") || kb.down("shift") || kb.down("control");
+		let modif = modifKey || Config.keymap.action_rotate == "middle";
+		let defaultKeymap = Config.raw.keymap == "default.json";
+
+		if (Operator.shortcut(Config.keymap.action_rotate, ShortcutType.ShortcutStarted) ||
+			Operator.shortcut(Config.keymap.action_zoom, ShortcutType.ShortcutStarted) ||
+			Operator.shortcut(Config.keymap.action_pan, ShortcutType.ShortcutStarted) ||
+			Operator.shortcut(Config.keymap.rotate_envmap, ShortcutType.ShortcutStarted) ||
+			Operator.shortcut(Config.keymap.rotate_light, ShortcutType.ShortcutStarted) ||
+			(mouse.started("right") && !modif) ||
+			(mouse.started("middle") && !modif) ||
+			(mouse.wheelDelta != 0 && !modifKey)) {
+			this.controlsDown = true;
+		}
+		else if (!Operator.shortcut(Config.keymap.action_rotate, ShortcutType.ShortcutDown) &&
+			!Operator.shortcut(Config.keymap.action_zoom, ShortcutType.ShortcutDown) &&
+			!Operator.shortcut(Config.keymap.action_pan, ShortcutType.ShortcutDown) &&
+			!Operator.shortcut(Config.keymap.rotate_envmap, ShortcutType.ShortcutDown) &&
+			!Operator.shortcut(Config.keymap.rotate_light, ShortcutType.ShortcutDown) &&
+			!(mouse.down("right") && !modif) &&
+			!(mouse.down("middle") && !modif) &&
+			(mouse.wheelDelta == 0 && !modifKey)) {
+			this.controlsDown = false;
+		}
+
+		if (Input.occupied ||
+			!Base.uiEnabled ||
+			Base.isDragging ||
+			Base.isScrolling() ||
+			Base.isComboSelected() ||
+			!this.controlsDown) {
+			return;
+		}
+
+		let controls = Context.raw.cameraControls;
+		if (controls == CameraControls.ControlsOrbit && (Operator.shortcut(Config.keymap.action_rotate, ShortcutType.ShortcutDown) || (mouse.down("right") && !modif && defaultKeymap))) {
+			this.redraws = 2;
+			let dist = this.distance();
+			camera.transform.move(camera.lookWorld(), dist);
+			camera.transform.rotate(Vec4.zAxis(), -mouse.movementX / 100 * Config.raw.camera_rotation_speed);
+			camera.transform.rotate(camera.rightWorld(), -mouse.movementY / 100 * Config.raw.camera_rotation_speed);
+			if (camera.upWorld().z < 0) {
+				camera.transform.rotate(camera.rightWorld(), mouse.movementY / 100 * Config.raw.camera_rotation_speed);
+			}
+			camera.transform.move(camera.lookWorld(), -dist);
+		}
+		else if (controls == CameraControls.ControlsRotate && (Operator.shortcut(Config.keymap.action_rotate, ShortcutType.ShortcutDown) || (mouse.down("right") && !modif && defaultKeymap))) {
+			this.redraws = 2;
+			let t = Context.mainObject().transform;
+			let up = t.up().normalize();
+			t.rotate(up, mouse.movementX / 100 * Config.raw.camera_rotation_speed);
+			let right = camera.rightWorld().normalize();
+			t.rotate(right, mouse.movementY / 100 * Config.raw.camera_rotation_speed);
+			t.buildMatrix();
+			if (t.up().z < 0) {
+				t.rotate(right, -mouse.movementY / 100 * Config.raw.camera_rotation_speed);
+			}
+		}
+
+		if (controls == CameraControls.ControlsRotate || controls == CameraControls.ControlsOrbit) {
+			this.panAction(modif, defaultKeymap);
+
+			if (Operator.shortcut(Config.keymap.action_zoom, ShortcutType.ShortcutDown)) {
+				this.redraws = 2;
+				let f = Camera.getZoomDelta() / 150;
+				f *= this.getCameraZoomSpeed();
+				camera.transform.move(camera.look(), f);
+			}
+
+			if (mouse.wheelDelta != 0 && !modifKey) {
+				this.redraws = 2;
+				let f = mouse.wheelDelta * (-0.1);
+				f *= this.getCameraZoomSpeed();
+				camera.transform.move(camera.look(), f);
+			}
+		}
+		else if (controls == CameraControls.ControlsFly && mouse.down("right")) {
+			let moveForward = kb.down("w") || kb.down("up") || mouse.wheelDelta < 0;
+			let moveBackward = kb.down("s") || kb.down("down") || mouse.wheelDelta > 0;
+			let strafeLeft = kb.down("a") || kb.down("left");
+			let strafeRight = kb.down("d") || kb.down("right");
+			let strafeUp = kb.down("e");
+			let strafeDown = kb.down("q");
+			let fast = kb.down("shift") ? 2.0 : (kb.down("alt") ? 0.5 : 1.0);
+			if (mouse.wheelDelta != 0) {
+				fast *= Math.abs(mouse.wheelDelta) * 4.0;
+			}
+
+			if (moveForward || moveBackward || strafeRight || strafeLeft || strafeUp || strafeDown) {
+				this.ease += Time.delta * 15;
+				if (this.ease > 1.0) this.ease = 1.0;
+				this.dir.set(0, 0, 0);
+				if (moveForward) this.dir.addf(camera.look().x, camera.look().y, camera.look().z);
+				if (moveBackward) this.dir.addf(-camera.look().x, -camera.look().y, -camera.look().z);
+				if (strafeRight) this.dir.addf(camera.right().x, camera.right().y, camera.right().z);
+				if (strafeLeft) this.dir.addf(-camera.right().x, -camera.right().y, -camera.right().z);
+				if (strafeUp) this.dir.addf(0, 0, 1);
+				if (strafeDown) this.dir.addf(0, 0, -1);
+			}
+			else {
+				this.ease -= Time.delta * 20.0 * this.ease;
+				if (this.ease < 0.0) this.ease = 0.0;
+			}
+
+
+			let d = Time.delta * fast * this.ease * 2.0 * ((moveForward || moveBackward) ? Config.raw.camera_zoom_speed : Config.raw.camera_pan_speed);
+			if (d > 0.0) {
+				camera.transform.move(this.dir, d);
+				if (Context.raw.cameraType == CameraType.CameraOrthographic) {
+					Viewport.updateCameraType(Context.raw.cameraType);
+				}
+			}
+
+			this.redraws = 2;
+			camera.transform.rotate(Vec4.zAxis(), -mouse.movementX / 200 * Config.raw.camera_rotation_speed);
+			camera.transform.rotate(camera.right(), -mouse.movementY / 200 * Config.raw.camera_rotation_speed);
+		}
+
+		if (Operator.shortcut(Config.keymap.rotate_light, ShortcutType.ShortcutDown)) {
+			this.redraws = 2;
+			let light = Scene.active.lights[0];
+			Context.raw.lightAngle = (Context.raw.lightAngle + ((mouse.movementX / 100) % (2 * Math.PI) + 2 * Math.PI)) % (2 * Math.PI);
+			let m = Mat4.rotationZ(mouse.movementX / 100);
+			light.transform.local.multmat(m);
+			light.transform.decompose();
+		}
+
+		if (Operator.shortcut(Config.keymap.rotate_envmap, ShortcutType.ShortcutDown)) {
+			this.redraws = 2;
+			Context.raw.envmapAngle -= mouse.movementX / 100;
+		}
+
+		if (this.redraws > 0) {
+			this.redraws--;
+			Context.raw.ddirty = 2;
+
+			if (Context.raw.cameraType == CameraType.CameraOrthographic) {
+				Viewport.updateCameraType(Context.raw.cameraType);
+			}
+		}
+	}
+
+	distance = (): f32 => {
+		let camera = Scene.active.camera;
+		return Vec4.distance(this.origins[this.index()], camera.transform.loc);
+	}
+
+	index = (): i32 => {
+		return Context.raw.viewIndexLast > 0 ? 1 : 0;
+	}
+
+	getCameraZoomSpeed = (): f32 => {
+		let sign = Config.raw.zoom_direction == ZoomDirection.ZoomVerticalInverted ||
+				   Config.raw.zoom_direction == ZoomDirection.ZoomHorizontalInverted ||
+				   Config.raw.zoom_direction == ZoomDirection.ZoomVerticalAndHorizontalInverted ? -1 : 1;
+		return Config.raw.camera_zoom_speed * sign;
+	}
+
+	reset = (viewIndex = -1) => {
+		let camera = Scene.active.camera;
+		if (viewIndex == -1) {
+			this.origins = [new Vec4(0, 0, 0), new Vec4(0, 0, 0)];
+			this.views = [camera.transform.local.clone(), camera.transform.local.clone()];
+		}
+		else {
+			this.origins[viewIndex] = new Vec4(0, 0, 0);
+			this.views[viewIndex] = camera.transform.local.clone();
+		}
+	}
+
+	panAction = (modif: bool, defaultKeymap: bool) => {
+		let camera = Scene.active.camera;
+		let mouse = Input.getMouse();
+		if (Operator.shortcut(Config.keymap.action_pan, ShortcutType.ShortcutDown) || (mouse.down("middle") && !modif && defaultKeymap)) {
+			this.redraws = 2;
+			let look = camera.transform.look().normalize().mult(mouse.movementY / 150 * Config.raw.camera_pan_speed);
+			let right = camera.transform.right().normalize().mult(-mouse.movementX / 150 * Config.raw.camera_pan_speed);
+			camera.transform.loc.add(look);
+			camera.transform.loc.add(right);
+			this.origins[this.index()].add(look);
+			this.origins[this.index()].add(right);
+			camera.buildMatrix();
+		}
+	}
+
+	static getZoomDelta = (): f32 => {
+		let mouse = Input.getMouse();
+		return Config.raw.zoom_direction == ZoomDirection.ZoomVertical ? -mouse.movementY :
+			   Config.raw.zoom_direction == ZoomDirection.ZoomVerticalInverted ? -mouse.movementY :
+			   Config.raw.zoom_direction == ZoomDirection.ZoomHorizontal ? mouse.movementX :
+			   Config.raw.zoom_direction == ZoomDirection.ZoomHorizontalInverted ? mouse.movementX :
+			   -(mouse.movementY - mouse.movementX);
+	}
+}

+ 320 - 0
base/Sources/Config.ts

@@ -0,0 +1,320 @@
+
+class Config {
+
+	static raw: TConfig = null;
+	static keymap: any;
+	static configLoaded = false;
+	static buttonAlign = Align.Left;
+	static defaultButtonSpacing = "       ";
+	static buttonSpacing = Config.defaultButtonSpacing;
+
+	static load = (done: ()=>void) => {
+		try {
+			Data.getBlob((Path.isProtected() ? Krom.savePath() : "") + "config.json", (blob: ArrayBuffer) => {
+				Config.configLoaded = true;
+				Config.raw = JSON.parse(System.bufferToString(blob));
+
+				done();
+			});
+		}
+		catch (e: any) {
+			///if krom_linux
+			try { // Protected directory
+				Data.getBlob(Krom.savePath() + "config.json", (blob: ArrayBuffer) => {
+					Config.configLoaded = true;
+					Config.raw = JSON.parse(System.bufferToString(blob));
+					done();
+				});
+			}
+			catch (e: any) {
+				done();
+			}
+			///else
+			done();
+			///end
+		}
+	}
+
+	static save = () => {
+		// Use system application data folder
+		// when running from protected path like "Program Files"
+		let path = (Path.isProtected() ? Krom.savePath() : Path.data() + Path.sep) + "config.json";
+		let buffer = System.stringToBuffer(JSON.stringify(Config.raw));
+		Krom.fileSaveBytes(path, buffer);
+
+		///if krom_linux // Protected directory
+		if (!File.exists(path)) Krom.fileSaveBytes(Krom.savePath() + "config.json", buffer);
+		///end
+	}
+
+	static init = () => {
+		if (!Config.configLoaded || Config.raw == null) {
+			Config.raw = {};
+			Config.raw.locale = "system";
+			Config.raw.window_mode = 0;
+			Config.raw.window_resizable = true;
+			Config.raw.window_minimizable = true;
+			Config.raw.window_maximizable = true;
+			Config.raw.window_w = 1600;
+			Config.raw.window_h = 900;
+			///if krom_darwin
+			Config.raw.window_w *= 2;
+			Config.raw.window_h *= 2;
+			///end
+			Config.raw.window_x = -1;
+			Config.raw.window_y = -1;
+			Config.raw.window_scale = 1.0;
+			if (System.displayWidth() >= 2560 && System.displayHeight() >= 1600) {
+				Config.raw.window_scale = 2.0;
+			}
+			///if (krom_android || krom_ios || krom_darwin)
+			Config.raw.window_scale = 2.0;
+			///end
+			Config.raw.window_vsync = true;
+			Config.raw.window_frequency = System.displayFrequency();
+			Config.raw.rp_bloom = false;
+			Config.raw.rp_gi = false;
+			Config.raw.rp_vignette = 0.2;
+			Config.raw.rp_grain = 0.09;
+			Config.raw.rp_motionblur = false;
+			///if (krom_android || krom_ios)
+			Config.raw.rp_ssao = false;
+			///else
+			Config.raw.rp_ssao = true;
+			///end
+			Config.raw.rp_ssr = false;
+			Config.raw.rp_supersample = 1.0;
+			Config.raw.version = Manifest.version;
+			Config.raw.sha = Config.getSha();
+			Base.initConfig();
+		}
+		else {
+			// Upgrade config format created by older ArmorPaint build
+			// if (Config.raw.version != Manifest.version) {
+			// 	Config.raw.version = Manifest.version;
+			// 	save();
+			// }
+			if (Config.raw.sha != Config.getSha()) {
+				Config.configLoaded = false;
+				Config.init();
+				return;
+			}
+		}
+
+		Zui.touchScroll = Zui.touchHold = Zui.touchTooltip = Config.raw.touch_ui;
+		Base.resHandle.position = Config.raw.layer_res;
+		Config.loadKeymap();
+	}
+
+	static getSha = (): string => {
+		let sha = "";
+		Data.getBlob("version.json", (blob: ArrayBuffer) => {
+			sha = JSON.parse(System.bufferToString(blob)).sha;
+		});
+		return sha;
+	}
+
+	static getDate = (): string => {
+		let date = "";
+		Data.getBlob("version.json", (blob: ArrayBuffer) => {
+			date = JSON.parse(System.bufferToString(blob)).date;
+		});
+		return date;
+	}
+
+	static getOptions = (): SystemOptions => {
+		let windowMode = Config.raw.window_mode == 0 ? WindowMode.Windowed : WindowMode.Fullscreen;
+		let windowFeatures = WindowFeatures.FeatureNone;
+		if (Config.raw.window_resizable) windowFeatures |= WindowFeatures.FeatureResizable;
+		if (Config.raw.window_maximizable) windowFeatures |= WindowFeatures.FeatureMaximizable;
+		if (Config.raw.window_minimizable) windowFeatures |= WindowFeatures.FeatureMinimizable;
+		let title = "untitled - " + Manifest.title;
+		return {
+			title: title,
+			width: Config.raw.window_w,
+			height: Config.raw.window_h,
+			x: Config.raw.window_x,
+			y: Config.raw.window_y,
+			mode: windowMode,
+			features: windowFeatures,
+			vsync: Config.raw.window_vsync,
+			frequency: Config.raw.window_frequency
+		};
+	}
+
+	static restore = () => {
+		Zui.children = new Map(); // Reset ui handles
+		Config.configLoaded = false;
+		let _layout = Config.raw.layout;
+		Config.init();
+		Config.raw.layout = _layout;
+		Base.initLayout();
+		Translator.loadTranslations(Config.raw.locale);
+		Config.applyConfig();
+		Config.loadTheme(Config.raw.theme);
+	}
+
+	static importFrom = (from: TConfig) => {
+		let _sha = Config.raw.sha;
+		let _version = Config.raw.version;
+		Config.raw = from;
+		Config.raw.sha = _sha;
+		Config.raw.version = _version;
+		Zui.children = new Map(); // Reset ui handles
+		Config.loadKeymap();
+		Base.initLayout();
+		Translator.loadTranslations(Config.raw.locale);
+		Config.applyConfig();
+		Config.loadTheme(Config.raw.theme);
+	}
+
+	static applyConfig = () => {
+		Config.raw.rp_ssao = Context.raw.hssao.selected;
+		Config.raw.rp_ssr = Context.raw.hssr.selected;
+		Config.raw.rp_bloom = Context.raw.hbloom.selected;
+		Config.raw.rp_gi = Context.raw.hvxao.selected;
+		Config.raw.rp_supersample = Config.getSuperSampleSize(Context.raw.hsupersample.position);
+		Config.save();
+		Context.raw.ddirty = 2;
+
+		let current = Graphics2.current;
+		if (current != null) current.end();
+		RenderPathBase.applyConfig();
+		if (current != null) current.begin(false);
+	}
+
+	static loadKeymap = () => {
+		if (Config.raw.keymap == "default.json") { // Built-in default
+			Config.keymap = Base.defaultKeymap;
+		}
+		else {
+			Data.getBlob("keymap_presets/" + Config.raw.keymap, (blob: ArrayBuffer) => {
+				Config.keymap = JSON.parse(System.bufferToString(blob));
+				// Fill in undefined keys with defaults
+				for (let field in Base.defaultKeymap) {
+					if (!(field in Config.keymap)) {
+						let adefaultKeymap: any = Base.defaultKeymap;
+						Config.keymap[field] = adefaultKeymap[field];
+					}
+				}
+			});
+		}
+	}
+
+	static saveKeymap = () => {
+		if (Config.raw.keymap == "default.json") return;
+		let path = Data.dataPath + "keymap_presets/" + Config.raw.keymap;
+		let buffer = System.stringToBuffer(JSON.stringify(Config.keymap));
+		Krom.fileSaveBytes(path, buffer);
+	}
+
+	static getSuperSampleQuality = (f: f32): i32 => {
+		return f == 0.25 ? 0 :
+			   f == 0.5 ? 1 :
+			   f == 1.0 ? 2 :
+			   f == 1.5 ? 3 :
+			   f == 2.0 ? 4 : 5;
+	}
+
+	static getSuperSampleSize = (i: i32): f32 => {
+		return i == 0 ? 0.25 :
+			   i == 1 ? 0.5 :
+			   i == 2 ? 1.0 :
+			   i == 3 ? 1.5 :
+			   i == 4 ? 2.0 : 4.0;
+	}
+
+	static getTextureRes = (): i32 => {
+		let res = Base.resHandle.position;
+		return res == TextureRes.Res128 ? 128 :
+			   res == TextureRes.Res256 ? 256 :
+			   res == TextureRes.Res512 ? 512 :
+			   res == TextureRes.Res1024 ? 1024 :
+			   res == TextureRes.Res2048 ? 2048 :
+			   res == TextureRes.Res4096 ? 4096 :
+			   res == TextureRes.Res8192 ? 8192 :
+			   res == TextureRes.Res16384 ? 16384 : 0;
+	}
+
+	static getTextureResX = (): i32 => {
+		return Context.raw.projectAspectRatio == 2 ? Math.floor(Config.getTextureRes() / 2) : Config.getTextureRes();
+	}
+
+	static getTextureResY = (): i32 => {
+		return Context.raw.projectAspectRatio == 1 ? Math.floor(Config.getTextureRes() / 2) : Config.getTextureRes();
+	}
+
+	static getTextureResBias = (): f32 => {
+		let res = Base.resHandle.position;
+		return res == TextureRes.Res128 ? 16.0 :
+			   res == TextureRes.Res256 ? 8.0 :
+			   res == TextureRes.Res512 ? 4.0 :
+			   res == TextureRes.Res1024 ? 2.0 :
+			   res == TextureRes.Res2048 ? 1.5 :
+			   res == TextureRes.Res4096 ? 1.0 :
+			   res == TextureRes.Res8192 ? 0.5 :
+			   res == TextureRes.Res16384 ? 0.25 : 1.0;
+	}
+
+	static getTextureResPos = (i: i32): i32 => {
+		return i == 128 ? TextureRes.Res128 :
+			   i == 256 ? TextureRes.Res256 :
+			   i == 512 ? TextureRes.Res512 :
+			   i == 1024 ? TextureRes.Res1024 :
+			   i == 2048 ? TextureRes.Res2048 :
+			   i == 4096 ? TextureRes.Res4096 :
+			   i == 8192 ? TextureRes.Res8192 :
+			   i == 16384 ? TextureRes.Res16384 : 0;
+	}
+
+	static loadTheme = (theme: string, tagRedraw = true) => {
+		if (theme == "default.json") { // Built-in default
+			Base.theme = new Theme();
+		}
+		else {
+			Data.getBlob("themes/" + theme, (b: ArrayBuffer) => {
+				let parsed = JSON.parse(System.bufferToString(b));
+				Base.theme = new Theme();
+				for (let key in Base.theme) {
+					if (key == "theme_") continue;
+					if (key.startsWith("set_")) continue;
+					if (key.startsWith("get_")) key = key.substr(4);
+					let atheme: any = Base.theme;
+					atheme[key] = parsed[key];
+				}
+			});
+		}
+		Base.theme.FILL_WINDOW_BG = true;
+		if (tagRedraw) {
+			for (let ui of Base.getUIs()) ui.t = Base.theme;
+			UIBase.inst.tagUIRedraw();
+		}
+		if (Config.raw.touch_ui) {
+			// Enlarge elements
+			Base.theme.FULL_TABS = true;
+			Base.theme.ELEMENT_H = 24 + 6;
+			Base.theme.BUTTON_H = 22 + 6;
+			Base.theme.FONT_SIZE = 13 + 2;
+			Base.theme.ARROW_SIZE = 5 + 2;
+			Base.theme.CHECK_SIZE = 15 + 4;
+			Base.theme.CHECK_SELECT_SIZE = 8 + 2;
+			Config.buttonAlign = Align.Left;
+			Config.buttonSpacing = "";
+		}
+		else {
+			Base.theme.FULL_TABS = false;
+			Config.buttonAlign = Align.Left;
+			Config.buttonSpacing = Config.defaultButtonSpacing;
+		}
+	}
+
+	static enablePlugin = (f: string) => {
+		Config.raw.plugins.push(f);
+		Plugin.start(f);
+	}
+
+	static disablePlugin = (f: string) => {
+		array_remove(Config.raw.plugins, f);
+		Plugin.stop(f);
+	}
+}

+ 69 - 0
base/Sources/ConfigFormat.ts

@@ -0,0 +1,69 @@
+
+type TConfig = {
+	// The locale should be specified in ISO 639-1 format: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+	// "system" is a special case that will use the system locale
+	locale?: string;
+	// Window
+	window_mode?: Null<i32>; // window, fullscreen
+	window_w?: Null<i32>;
+	window_h?: Null<i32>;
+	window_x?: Null<i32>;
+	window_y?: Null<i32>;
+	window_resizable?: Null<bool>;
+	window_maximizable?: Null<bool>;
+	window_minimizable?: Null<bool>;
+	window_vsync?: Null<bool>;
+	window_frequency?: Null<i32>;
+	window_scale?: Null<f32>;
+	// Render path
+	rp_supersample?: Null<f32>;
+	rp_ssao?: Null<bool>;
+	rp_ssr?: Null<bool>;
+	rp_bloom?: Null<bool>;
+	rp_motionblur?: Null<bool>;
+	rp_gi?: Null<bool>;
+	rp_vignette?: Null<f32>;
+	rp_grain?: Null<f32>;
+	// Application
+	version?: string;
+	sha?: string; // Commit id
+	recent_projects?: string[]; // Recently opened projects
+	bookmarks?: string[]; // Bookmarked folders in browser
+	plugins?: string[]; // List of enabled plugins
+	keymap?: string; // Link to keymap file
+	theme?: string; // Link to theme file
+	undo_steps?: Null<i32>; // Number of undo steps to preserve
+	camera_pan_speed?: Null<f32>;
+	camera_zoom_speed?: Null<f32>;
+	camera_rotation_speed?: Null<f32>;
+	zoom_direction?: Null<i32>;
+	wrap_mouse?: Null<bool>;
+	show_asset_names?: Null<bool>;
+	touch_ui?: Null<bool>;
+	splash_screen?: Null<bool>;
+	layout?: i32[]; // Sizes
+	layout_tabs?: i32[]; // Active tabs
+	workspace?: Null<i32>;
+	camera_controls?: Null<i32>; // Orbit, rotate
+	server?: string;
+
+	pressure_radius?: Null<bool>; // Pen pressure controls
+	pressure_sensitivity?: Null<f32>;
+	displace_strength?: Null<f32>;
+	layer_res?: Null<i32>;
+	brush_live?: Null<bool>;
+	brush_3d?: Null<bool>;
+	node_preview?: Null<bool>;
+
+	pressure_hardness?: Null<bool>;
+	pressure_angle?: Null<bool>;
+	pressure_opacity?: Null<bool>;
+	material_live?: Null<bool>;
+	brush_depth_reject?: Null<bool>;
+	brush_angle_reject?: Null<bool>;
+
+	dilate?: Null<i32>;
+	dilate_radius?: Null<i32>;
+
+	gpu_inference?: Null<bool>;
+}

+ 79 - 0
base/Sources/Console.ts

@@ -0,0 +1,79 @@
+
+// @:keep
+class Console {
+
+	static message = "";
+	static messageTimer = 0.0;
+	static messageColor = 0x00000000;
+	static lastTraces: string[] = [""];
+	static progressText: string = null;
+
+	static drawToast = (s: string, g: Graphics2) => {
+		g.color = 0x55000000;
+		g.fillRect(0, 0, System.width, System.height);
+		let scale = Base.getUIs()[0].SCALE();
+		let x = System.width / 2;
+		let y = System.height - 200 * scale;
+		g.fillRect(x - 200 * scale, y, 400 * scale, 80 * scale);
+		g.font = Base.font;
+		g.fontSize = Math.floor(22 * scale);
+		g.color = 0xffffffff;
+		g.drawString(s, x - g.font.width(g.fontSize, s) / 2, y + 40 * scale - g.font.height(g.fontSize) / 2);
+	}
+
+	static toast = (s: string, g2: Graphics2 = null) => {
+		// Show a popup message
+		let _render = (g: Graphics2) => {
+			Console.drawToast(s, g);
+			if (g2 == null) {
+				Base.notifyOnNextFrame(() => {
+					App.removeRender2D(_render);
+				});
+			}
+		}
+		g2 != null ? _render(g2) : App.notifyOnRender2D(_render);
+		Console.consoleTrace(s);
+	}
+
+	static drawProgress = (g: Graphics2) => {
+		Console.drawToast(Console.progressText, g);
+	}
+
+	static progress = (s: string) => {
+		// Keep popup message displayed until s == null
+		if (s == null) {
+			App.removeRender2D(Console.drawProgress);
+		}
+		else if (Console.progressText == null) {
+			App.notifyOnRender2D(Console.drawProgress);
+		}
+		if (s != null) Console.consoleTrace(s);
+		Console.progressText = s;
+	}
+
+	static info = (s: string) => {
+		Console.messageTimer = 5.0;
+		Console.message = s;
+		Console.messageColor = 0x00000000;
+		Base.redrawStatus();
+		Console.consoleTrace(s);
+	}
+
+	static error = (s: string) => {
+		Console.messageTimer = 8.0;
+		Console.message = s;
+		Console.messageColor = 0xffaa0000;
+		Base.redrawStatus();
+		Console.consoleTrace(s);
+	}
+
+	static log = (s: string) => {
+		Console.consoleTrace(s);
+	}
+
+	static consoleTrace = (v: any) => {
+		Base.redrawConsole();
+		Console.lastTraces.unshift(String(v));
+		if (Console.lastTraces.length > 100) Console.lastTraces.pop();
+	}
+}

+ 441 - 0
base/Sources/Context.ts

@@ -0,0 +1,441 @@
+/// <reference path='./ContextFormat.ts'/>
+
+class Context {
+
+	static raw: TContext = new TContext(); //{};
+
+	static useDeferred = (): bool => {
+		///if is_paint
+		return Context.raw.renderMode != RenderMode.RenderForward && (Context.raw.viewportMode == ViewportMode.ViewLit || Context.raw.viewportMode == ViewportMode.ViewPathTrace) && Context.raw.tool != WorkspaceTool.ToolColorId;
+		///end
+
+		///if (is_sculpt || is_lab)
+		return Context.raw.renderMode != RenderMode.RenderForward && (Context.raw.viewportMode == ViewportMode.ViewLit || Context.raw.viewportMode == ViewportMode.ViewPathTrace);
+		///end
+	}
+
+	///if (is_paint || is_sculpt)
+	static selectMaterial = (i: i32) => {
+		if (Project.materials.length <= i) return;
+		Context.setMaterial(Project.materials[i]);
+	}
+
+	static setMaterial = (m: SlotMaterial) => {
+		if (Project.materials.indexOf(m) == -1) return;
+		Context.raw.material = m;
+		MakeMaterial.parsePaintMaterial();
+		UIBase.inst.hwnds[TabArea.TabSidebar1].redraws = 2;
+		UIHeader.inst.headerHandle.redraws = 2;
+		UINodes.inst.hwnd.redraws = 2;
+		UINodes.inst.groupStack = [];
+
+		let decal = Context.raw.tool == WorkspaceTool.ToolDecal || Context.raw.tool == WorkspaceTool.ToolText;
+		if (decal) {
+			let _next = () => {
+				UtilRender.makeDecalPreview();
+			}
+			Base.notifyOnNextFrame(_next);
+		}
+	}
+
+	static selectBrush = (i: i32) => {
+		if (Project.brushes.length <= i) return;
+		Context.setBrush(Project.brushes[i]);
+	}
+
+	static setBrush = (b: SlotBrush) => {
+		if (Project.brushes.indexOf(b) == -1) return;
+		Context.raw.brush = b;
+		MakeMaterial.parseBrush();
+		UIBase.inst.hwnds[TabArea.TabSidebar1].redraws = 2;
+		UINodes.inst.hwnd.redraws = 2;
+	}
+
+	static selectFont = (i: i32) => {
+		if (Project.fonts.length <= i) return;
+		Context.setFont(Project.fonts[i]);
+	}
+
+	static setFont = (f: SlotFont) => {
+		if (Project.fonts.indexOf(f) == -1) return;
+		Context.raw.font = f;
+		UtilRender.makeTextPreview();
+		UtilRender.makeDecalPreview();
+		UIBase.inst.hwnds[TabArea.TabStatus].redraws = 2;
+		UIView2D.inst.hwnd.redraws = 2;
+	}
+
+	static selectLayer = (i: i32) => {
+		if (Project.layers.length <= i) return;
+		Context.setLayer(Project.layers[i]);
+	}
+
+	static setLayer = (l: SlotLayer) => {
+		if (l == Context.raw.layer) return;
+		Context.raw.layer = l;
+		UIHeader.inst.headerHandle.redraws = 2;
+
+		let current = Graphics2.current;
+		if (current != null) current.end();
+
+		Base.setObjectMask();
+		MakeMaterial.parseMeshMaterial();
+		MakeMaterial.parsePaintMaterial();
+
+		if (current != null) current.begin(false);
+
+		UIBase.inst.hwnds[TabArea.TabSidebar0].redraws = 2;
+		UIView2D.inst.hwnd.redraws = 2;
+	}
+	///end
+
+	static selectTool = (i: i32) => {
+		Context.raw.tool = i;
+		MakeMaterial.parsePaintMaterial();
+		MakeMaterial.parseMeshMaterial();
+		Context.raw.ddirty = 3;
+		let _viewportMode = Context.raw.viewportMode;
+		Context.raw.viewportMode = -1 as ViewportMode;
+		Context.setViewportMode(_viewportMode);
+
+		///if (is_paint || is_sculpt)
+		Context.initTool();
+		UIHeader.inst.headerHandle.redraws = 2;
+		UIToolbar.inst.toolbarHandle.redraws = 2;
+		///end
+	}
+
+	///if (is_paint || is_sculpt)
+	static initTool = () => {
+		let decal = Context.raw.tool == WorkspaceTool.ToolDecal || Context.raw.tool == WorkspaceTool.ToolText;
+		if (decal) {
+			if (Context.raw.tool == WorkspaceTool.ToolText) {
+				UtilRender.makeTextPreview();
+			}
+			UtilRender.makeDecalPreview();
+		}
+
+		else if (Context.raw.tool == WorkspaceTool.ToolParticle) {
+			UtilParticle.initParticle();
+			MakeMaterial.parseParticleMaterial();
+		}
+
+		else if (Context.raw.tool == WorkspaceTool.ToolBake) {
+			///if (krom_direct3d12 || krom_vulkan || krom_metal)
+			// Bake in lit mode for now
+			if (Context.raw.viewportMode == ViewportMode.ViewPathTrace) {
+				Context.raw.viewportMode = ViewportMode.ViewLit;
+			}
+			///end
+		}
+
+		else if (Context.raw.tool == WorkspaceTool.ToolMaterial) {
+			Base.updateFillLayers();
+			Context.mainObject().skip_context = null;
+		}
+
+		///if krom_ios
+		// No hover on iPad, decals are painted by pen release
+		Config.raw.brush_live = decal;
+		///end
+	}
+	///end
+
+	static selectPaintObject = (o: MeshObject) => {
+		///if (is_paint || is_sculpt)
+		UIHeader.inst.headerHandle.redraws = 2;
+		for (let p of Project.paintObjects) p.skip_context = "paint";
+		Context.raw.paintObject = o;
+
+		let mask = Context.raw.layer.getObjectMask();
+		if (Context.layerFilterUsed()) mask = Context.raw.layerFilter;
+
+		if (Context.raw.mergedObject == null || mask > 0) {
+			Context.raw.paintObject.skip_context = "";
+		}
+		UtilUV.uvmapCached = false;
+		UtilUV.trianglemapCached = false;
+		UtilUV.dilatemapCached = false;
+		///end
+
+		///if is_lab
+		Context.raw.paintObject = o;
+		///end
+	}
+
+	static mainObject = (): MeshObject => {
+		///if (is_paint || is_sculpt)
+		for (let po of Project.paintObjects) if (po.children.length > 0) return po;
+		return Project.paintObjects[0];
+		///end
+
+		///if is_lab
+		return Project.paintObjects[0];
+		///end
+	}
+
+	static layerFilterUsed = (): bool => {
+		///if (is_paint || is_sculpt)
+		return Context.raw.layerFilter > 0 && Context.raw.layerFilter <= Project.paintObjects.length;
+		///end
+
+		///if is_lab
+		return true;
+		///end
+	}
+
+	static objectMaskUsed = (): bool => {
+		return Context.raw.layer.getObjectMask() > 0 && Context.raw.layer.getObjectMask() <= Project.paintObjects.length;
+	}
+
+	static inViewport = (): bool => {
+		return Context.raw.paintVec.x < 1 && Context.raw.paintVec.x > 0 &&
+			   Context.raw.paintVec.y < 1 && Context.raw.paintVec.y > 0;
+	}
+
+	static inPaintArea = (): bool => {
+		///if (is_paint || is_sculpt)
+		let mouse = Input.getMouse();
+		let right = App.w();
+		if (UIView2D.inst.show) right += UIView2D.inst.ww;
+		return mouse.viewX > 0 && mouse.viewX < right &&
+			   mouse.viewY > 0 && mouse.viewY < App.h();
+		///end
+
+		///if is_lab
+		return Context.inViewport();
+		///end
+	}
+
+	static inLayers = (): bool => {
+		return UIBase.inst.ui.getHoveredTabName() == tr("Layers");
+	}
+
+	static inMaterials = (): bool => {
+		return UIBase.inst.ui.getHoveredTabName() == tr("Materials");
+	}
+
+	///if (is_paint || is_sculpt)
+	static in2dView = (type = View2DType.View2DLayer): bool => {
+		let mouse = Input.getMouse();
+		return UIView2D.inst.show && UIView2D.inst.type == type &&
+			   mouse.x > UIView2D.inst.wx && mouse.x < UIView2D.inst.wx + UIView2D.inst.ww &&
+			   mouse.y > UIView2D.inst.wy && mouse.y < UIView2D.inst.wy + UIView2D.inst.wh;
+	}
+	///end
+
+	static inNodes = (): bool => {
+		let mouse = Input.getMouse();
+		return UINodes.inst.show &&
+			   mouse.x > UINodes.inst.wx && mouse.x < UINodes.inst.wx + UINodes.inst.ww &&
+			   mouse.y > UINodes.inst.wy && mouse.y < UINodes.inst.wy + UINodes.inst.wh;
+	}
+
+	static inSwatches = (): bool => {
+		return UIBase.inst.ui.getHoveredTabName() == tr("Swatches");
+	}
+
+	static inBrowser = (): bool => {
+		return UIBase.inst.ui.getHoveredTabName() == tr("Browser");
+	}
+
+	static getAreaType = (): AreaType => {
+		if (Context.inViewport()) return AreaType.AreaViewport;
+		if (Context.inNodes()) return AreaType.AreaNodes;
+		if (Context.inBrowser()) return AreaType.AreaBrowser;
+		///if (is_paint || is_sculpt)
+		if (Context.in2dView()) return AreaType.Area2DView;
+		if (Context.inLayers()) return AreaType.AreaLayers;
+		if (Context.inMaterials()) return AreaType.AreaMaterials;
+		///end
+		return -1 as AreaType;
+	}
+
+	static setViewportMode = (mode: ViewportMode) => {
+		if (mode == Context.raw.viewportMode) return;
+
+		Context.raw.viewportMode = mode;
+		if (Context.useDeferred()) {
+			RenderPath.active.commands = RenderPathDeferred.commands;
+		}
+		else {
+			if (RenderPathForward.path == null) {
+				RenderPathForward.init(RenderPath.active);
+			}
+			RenderPath.active.commands = RenderPathForward.commands;
+		}
+		let _workspace = UIHeader.inst.worktab.position;
+		UIHeader.inst.worktab.position = 0;
+		MakeMaterial.parseMeshMaterial();
+		UIHeader.inst.worktab.position = _workspace;
+	}
+
+	static loadEnvmap = () => {
+		if (!Context.raw.envmapLoaded) {
+			// TODO: Unable to share texture for both radiance and envmap - reload image
+			Context.raw.envmapLoaded = true;
+			Data.cachedImages.delete("World_radiance.k");
+		}
+		Scene.active.world.loadEnvmap((_) => {});
+		if (Context.raw.savedEnvmap == null) Context.raw.savedEnvmap = Scene.active.world.envmap;
+	}
+
+	static updateEnvmap = () => {
+		if (Context.raw.showEnvmap) {
+			Scene.active.world.envmap = Context.raw.showEnvmapBlur ? Scene.active.world.probe.radianceMipmaps[0] : Context.raw.savedEnvmap;
+		}
+		else {
+			Scene.active.world.envmap = Context.raw.emptyEnvmap;
+		}
+	}
+
+	// @:keep
+	static setViewportShader = (viewportShader: (ns: NodeShader)=>string) => {
+		Context.raw.viewportShader = viewportShader;
+		Context.setRenderPath();
+	}
+
+	static setRenderPath = () => {
+		if (Context.raw.renderMode == RenderMode.RenderForward || Context.raw.viewportShader != null) {
+			if (RenderPathForward.path == null) {
+				RenderPathForward.init(RenderPath.active);
+			}
+			RenderPath.active.commands = RenderPathForward.commands;
+		}
+		else {
+			RenderPath.active.commands = RenderPathDeferred.commands;
+		}
+		App.notifyOnInit(() => {
+			MakeMaterial.parseMeshMaterial();
+		});
+	}
+
+	static enableImportPlugin = (file: string): bool => {
+		// Return plugin name suitable for importing the specified file
+		if (BoxPreferences.filesPlugin == null) {
+			BoxPreferences.fetchPlugins();
+		}
+		let ext = file.substr(file.lastIndexOf(".") + 1);
+		for (let f of BoxPreferences.filesPlugin) {
+			if (f.startsWith("import_") && f.indexOf(ext) >= 0) {
+				Config.enablePlugin(f);
+				Console.info(f + " " + tr("plugin enabled"));
+				return true;
+			}
+		}
+		return false;
+	}
+
+	static setSwatch = (s: TSwatchColor) => {
+		Context.raw.swatch = s;
+	}
+
+	///if is_lab
+	static runBrush = (from: i32) => {
+		let left = 0.0;
+		let right = 1.0;
+
+		// First time init
+		if (Context.raw.lastPaintX < 0 || Context.raw.lastPaintY < 0) {
+			Context.raw.lastPaintVecX = Context.raw.paintVec.x;
+			Context.raw.lastPaintVecY = Context.raw.paintVec.y;
+		}
+
+		let nodes = UINodes.inst.getNodes();
+		let canvas = UINodes.inst.getCanvas(true);
+		let inpaint = nodes.nodesSelectedId.length > 0 && nodes.getNode(canvas.nodes, nodes.nodesSelectedId[0]).type == "InpaintNode";
+
+		// Paint bounds
+		if (inpaint &&
+			Context.raw.paintVec.x > left &&
+			Context.raw.paintVec.x < right &&
+			Context.raw.paintVec.y > 0 &&
+			Context.raw.paintVec.y < 1 &&
+			!Base.isDragging &&
+			!Base.isResizing &&
+			!Base.isScrolling() &&
+			!Base.isComboSelected()) {
+
+			let down = Input.getMouse().down() || Input.getPen().down();
+
+			// Prevent painting the same spot
+			let sameSpot = Context.raw.paintVec.x == Context.raw.lastPaintX && Context.raw.paintVec.y == Context.raw.lastPaintY;
+			if (down && sameSpot) {
+				Context.raw.painted++;
+			}
+			else {
+				Context.raw.painted = 0;
+			}
+			Context.raw.lastPaintX = Context.raw.paintVec.x;
+			Context.raw.lastPaintY = Context.raw.paintVec.y;
+
+			if (Context.raw.painted == 0) {
+				Context.parseBrushInputs();
+			}
+
+			if (Context.raw.painted <= 1) {
+				Context.raw.pdirty = 1;
+				Context.raw.rdirty = 2;
+			}
+		}
+	}
+
+	static parseBrushInputs = () => {
+		if (!Context.raw.registered) {
+			Context.raw.registered = true;
+			App.notifyOnUpdate(Context.update);
+		}
+
+		Context.raw.paintVec = Context.raw.coords;
+	}
+
+	static update = () => {
+		let mouse = Input.getMouse();
+		let paintX = mouse.viewX / App.w();
+		let paintY = mouse.viewY / App.h();
+		if (mouse.started()) {
+			Context.raw.startX = mouse.viewX / App.w();
+			Context.raw.startY = mouse.viewY / App.h();
+		}
+
+		let pen = Input.getPen();
+		if (pen.down()) {
+			paintX = pen.viewX / App.w();
+			paintY = pen.viewY / App.h();
+		}
+		if (pen.started()) {
+			Context.raw.startX = pen.viewX / App.w();
+			Context.raw.startY = pen.viewY / App.h();
+		}
+
+		if (Operator.shortcut(Config.keymap.brush_ruler + "+" + Config.keymap.action_paint, ShortcutType.ShortcutDown)) {
+			if (Context.raw.lockX) paintX = Context.raw.startX;
+			if (Context.raw.lockY) paintY = Context.raw.startY;
+		}
+
+		Context.raw.coords.x = paintX;
+		Context.raw.coords.y = paintY;
+
+		if (Context.raw.lockBegin) {
+			let dx = Math.abs(Context.raw.lockStartX - mouse.viewX);
+			let dy = Math.abs(Context.raw.lockStartY - mouse.viewY);
+			if (dx > 1 || dy > 1) {
+				Context.raw.lockBegin = false;
+				dx > dy ? Context.raw.lockY = true : Context.raw.lockX = true;
+			}
+		}
+
+		let kb = Input.getKeyboard();
+		if (kb.started(Config.keymap.brush_ruler)) {
+			Context.raw.lockStartX = mouse.viewX;
+			Context.raw.lockStartY = mouse.viewY;
+			Context.raw.lockBegin = true;
+		}
+		else if (kb.released(Config.keymap.brush_ruler)) {
+			Context.raw.lockX = Context.raw.lockY = Context.raw.lockBegin = false;
+		}
+
+		Context.parseBrushInputs();
+	}
+	///end
+}

+ 306 - 0
base/Sources/ContextFormat.ts

@@ -0,0 +1,306 @@
+/// <reference path='./Project.ts'/>
+/// <reference path='./Enums.ts'/>
+
+// type TContext = {
+class TContext {
+	texture?: TAsset = null;
+	paintObject?: MeshObject;
+	mergedObject?: MeshObject = null; // For object mask
+	mergedObjectIsAtlas? = false; // Only objects referenced by atlas are merged
+
+	ddirty? = 0; // depth
+	pdirty? = 0; // paint
+	rdirty? = 0; // render
+	brushBlendDirty? = true;
+	nodePreviewSocket? = 0;
+
+	splitView? = false;
+	viewIndex? = -1;
+	viewIndexLast? = -1;
+
+	swatch?: TSwatchColor;
+	pickedColor?: TSwatchColor = Project.makeSwatch();
+	colorPickerCallback?: (sc: TSwatchColor)=>void = null;
+
+	defaultIrradiance?: Float32Array = null;
+	defaultRadiance?: Image = null;
+	defaultRadianceMipmaps?: Image[] = null;
+	savedEnvmap?: Image = null;
+	emptyEnvmap?: Image = null;
+	previewEnvmap?: Image = null;
+	envmapLoaded? = false;
+	showEnvmap? = false;
+	showEnvmapHandle? = new Handle({ selected: false });
+	showEnvmapBlur? = false;
+	showEnvmapBlurHandle? = new Handle({ selected: false });
+	envmapAngle? = 0.0;
+	lightAngle? = 0.0;
+	cullBackfaces? = true;
+	textureFilter? = true;
+
+	formatType? = TextureLdrFormat.FormatPng;
+	formatQuality? = 100.0;
+	layersDestination? = ExportDestination.DestinationDisk;
+	splitBy? = SplitType.SplitObject;
+	parseTransform? = true;
+	parseVCols? = false;
+
+	selectTime? = 0.0;
+	///if (krom_direct3d12 || krom_vulkan || krom_metal)
+	pathTraceMode? = PathTraceMode.TraceCore;
+	///end
+	///if (krom_direct3d12 || krom_vulkan) // || krom_metal)
+	viewportMode? = ViewportMode.ViewPathTrace;
+	///else
+	viewportMode? = ViewportMode.ViewLit;
+	///end
+	///if (krom_android || krom_ios)
+	renderMode? = RenderMode.RenderForward;
+	///else
+	renderMode? = RenderMode.RenderDeferred;
+	///end
+
+	viewportShader?: (ns: NodeShader)=>string = null;
+	hscaleWasChanged? = false;
+	exportMeshFormat? = MeshFormat.FormatObj;
+	exportMeshIndex? = 0;
+	packAssetsOnExport? = true;
+
+	paintVec? = new Vec4();
+	lastPaintX? = -1.0;
+	lastPaintY? = -1.0;
+	foregroundEvent? = false;
+	painted? = 0;
+	brushTime? = 0.0;
+	cloneStartX? = -1.0;
+	cloneStartY? = -1.0;
+	cloneDeltaX? = 0.0;
+	cloneDeltaY? = 0.0;
+
+	showCompass? = true;
+	projectType? = ProjectModel.ModelRoundedCube;
+	projectAspectRatio? = 0; // 1:1, 2:1, 1:2
+	projectObjects?: MeshObject[];
+
+	lastPaintVecX? = -1.0;
+	lastPaintVecY? = -1.0;
+	prevPaintVecX? = -1.0;
+	prevPaintVecY? = -1.0;
+	frame? = 0;
+	paint2dView? = false;
+
+	lockStartedX? = -1.0;
+	lockStartedY? = -1.0;
+	brushLocked? = false;
+	brushCanLock? = false;
+	brushCanUnlock? = false;
+	cameraType? = CameraType.CameraPerspective;
+	camHandle? = new Handle();
+	fovHandle?: Handle = null;
+	undoHandle?: Handle = null;
+	hssao?: Handle = null;
+	hssr?: Handle = null;
+	hbloom?: Handle = null;
+	hsupersample?: Handle = null;
+	hvxao?: Handle = null;
+	///if is_forge
+	vxaoExt? = 2.0;
+	///else
+	vxaoExt? = 1.0;
+	///end
+	vxaoOffset? = 1.5;
+	vxaoAperture? = 1.2;
+	textureExportPath? = "";
+	lastStatusPosition? = 0;
+	cameraControls? = CameraControls.ControlsOrbit;
+	penPaintingOnly? = false; // Reject painting with finger when using pen
+
+	///if (is_paint || is_sculpt)
+	material?: SlotMaterial;
+	layer?: SlotLayer;
+	brush?: SlotBrush;
+	font?: SlotFont;
+	tool? = WorkspaceTool.ToolBrush;
+
+	layerPreviewDirty? = true;
+	layersPreviewDirty? = false;
+	nodePreviewDirty? = false;
+	nodePreview?: Image = null;
+	nodePreviews?: Map<string, Image> = null;
+	nodePreviewsUsed?: string[] = null;
+	nodePreviewName? = "";
+	maskPreviewRgba32?: Image = null;
+	maskPreviewLast?: SlotLayer = null;
+
+	colorIdPicked? = false;
+	materialPreview? = false; // Drawing material previews
+	savedCamera? = Mat4.identity();
+
+	colorPickerPreviousTool? = WorkspaceTool.ToolBrush;
+	materialIdPicked? = 0;
+	uvxPicked? = 0.0;
+	uvyPicked? = 0.0;
+	pickerSelectMaterial? = true;
+	pickerMaskHandle? = new Handle();
+	pickPosNorTex? = false;
+	posXPicked? = 0.0;
+	posYPicked? = 0.0;
+	posZPicked? = 0.0;
+	norXPicked? = 0.0;
+	norYPicked? = 0.0;
+	norZPicked? = 0.0;
+
+	drawWireframe? = false;
+	wireframeHandle? = new Handle({ selected: false });
+	drawTexels? = false;
+	texelsHandle? = new Handle({ selected: false });
+
+	colorIdHandle? = new Handle();
+	layersExport? = ExportMode.ExportVisible;
+
+	decalImage?: Image = null;
+	decalPreview? = false;
+	decalX? = 0.0;
+	decalY? = 0.0;
+
+	cacheDraws? = false;
+	writeIconOnExport? = false;
+
+	textToolImage?: Image = null;
+	textToolText?: string;
+	particleMaterial?: MaterialData = null;
+	///if arm_physics
+	particlePhysics? = false;
+	particleHitX? = 0.0;
+	particleHitY? = 0.0;
+	particleHitZ? = 0.0;
+	lastParticleHitX? = 0.0;
+	lastParticleHitY? = 0.0;
+	lastParticleHitZ? = 0.0;
+	particleTimer?: TAnim = null;
+	paintBody?: PhysicsBody = null;
+	///end
+
+	layerFilter? = 0;
+	runBrush?: (i: i32)=>void = null;
+	parseBrushInputs?: ()=>void = null;
+
+	gizmo?: BaseObject = null;
+	gizmoTranslateX?: BaseObject = null;
+	gizmoTranslateY?: BaseObject = null;
+	gizmoTranslateZ?: BaseObject = null;
+	gizmoScaleX?: BaseObject = null;
+	gizmoScaleY?: BaseObject = null;
+	gizmoScaleZ?: BaseObject = null;
+	gizmoRotateX?: BaseObject = null;
+	gizmoRotateY?: BaseObject = null;
+	gizmoRotateZ?: BaseObject = null;
+	gizmoStarted? = false;
+	gizmoOffset? = 0.0;
+	gizmoDrag? = 0.0;
+	gizmoDragLast? = 0.0;
+	translateX? = false;
+	translateY? = false;
+	translateZ? = false;
+	scaleX? = false;
+	scaleY? = false;
+	scaleZ? = false;
+	rotateX? = false;
+	rotateY? = false;
+	rotateZ? = false;
+
+	brushNodesRadius? = 1.0;
+	brushNodesOpacity? = 1.0;
+	brushMaskImage?: Image = null;
+	brushMaskImageIsAlpha? = false;
+	brushStencilImage?: Image = null;
+	brushStencilImageIsAlpha? = false;
+	brushStencilX? = 0.02;
+	brushStencilY? = 0.02;
+	brushStencilScale? = 0.9;
+	brushStencilScaling? = false;
+	brushStencilAngle? = 0.0;
+	brushStencilRotating? = false;
+	brushNodesScale? = 1.0;
+	brushNodesAngle? = 0.0;
+	brushNodesHardness? = 1.0;
+	brushDirectional? = false;
+
+	brushRadius? = 0.5;
+	brushRadiusHandle? = new Handle({ value: 0.5 });
+	brushScaleX? = 1.0;
+	brushDecalMaskRadius? = 0.5;
+	brushDecalMaskRadiusHandle? = new Handle({ value: 0.5 });
+	brushScaleXHandle? = new Handle({ value: 1.0 });
+	brushBlending? = BlendType.BlendMix;
+	brushOpacity? = 1.0;
+	brushOpacityHandle? = new Handle({ value: 1.0 });
+	brushScale? = 1.0;
+	brushAngle? = 0.0;
+	brushAngleHandle? = new Handle({ value: 0.0 });
+	///if is_paint
+	brushHardness? = 0.8;
+	///end
+	///if is_sculpt
+	brushHardness? = 0.05;
+	///end
+	brushLazyRadius? = 0.0;
+	brushLazyStep? = 0.0;
+	brushLazyX? = 0.0;
+	brushLazyY? = 0.0;
+	brushPaint? = UVType.UVMap;
+	brushAngleRejectDot? = 0.5;
+	bakeType? = BakeType.BakeAO;
+	bakeAxis? = BakeAxis.BakeXYZ;
+	bakeUpAxis? = BakeUpAxis.BakeUpZ;
+	bakeSamples? = 128;
+	bakeAoStrength? = 1.0;
+	bakeAoRadius? = 1.0;
+	bakeAoOffset? = 1.0;
+	bakeCurvStrength? = 1.0;
+	bakeCurvRadius? = 1.0;
+	bakeCurvOffset? = 0.0;
+	bakeCurvSmooth? = 1;
+	bakeHighPoly? = 0;
+
+	xray? = false;
+	symX? = false;
+	symY? = false;
+	symZ? = false;
+	fillTypeHandle? = new Handle();
+
+	paint2d? = false;
+
+	lastHtab0Position? = 0;
+	maximizedSidebarWidth? = 0;
+	dragDestination? = 0;
+	///end
+
+	///if is_lab
+	material?: any; ////
+	layer?: any; ////
+	tool? = WorkspaceTool.ToolEraser;
+
+	colorPickerPreviousTool? = WorkspaceTool.ToolEraser;
+
+	brushRadius? = 0.25;
+	brushRadiusHandle? = new Handle({ value: 0.25 });
+	brushScale? = 1.0;
+
+	coords? = new Vec4();
+	startX? = 0.0;
+	startY? = 0.0;
+
+	// Brush ruler
+	lockBegin? = false;
+	lockX? = false;
+	lockY? = false;
+	lockStartX? = 0.0;
+	lockStartY? = 0.0;
+	registered? = false;
+	///end
+
+	///if is_forge
+	selectedObject?: BaseObject = null;
+	///end
+}

+ 290 - 0
base/Sources/Enums.ts

@@ -0,0 +1,290 @@
+
+enum DilateType {
+	DilateInstant,
+	DilateDelayed,
+}
+
+enum BakeType {
+	BakeInit = -1,
+	BakeAO = 0,
+	BakeCurvature = 1,
+	BakeNormal = 2,
+	BakeNormalObject = 3,
+	BakeHeight = 4,
+	BakeDerivative = 5,
+	BakePosition = 6,
+	BakeTexCoord = 7,
+	BakeMaterialID = 8,
+	BakeObjectID = 9,
+	BakeVertexColor = 10,
+	BakeLightmap = 11,
+	BakeBentNormal = 12,
+	BakeThickness = 13,
+}
+
+enum SplitType {
+	SplitObject = 0,
+	SplitGroup = 1,
+	SplitMaterial = 2,
+	SplitUdim = 3,
+}
+
+enum BakeAxis {
+	BakeXYZ = 0,
+	BakeX = 1,
+	BakeY = 2,
+	BakeZ = 3,
+	BakeMX = 4,
+	BakeMY = 5,
+	BakeMZ = 6,
+}
+
+enum BakeUpAxis {
+	BakeUpZ = 0,
+	BakeUpY = 1,
+	BakeUpX = 2,
+}
+
+enum ViewportMode {
+	ViewLit = 0,
+	ViewBaseColor = 1,
+	ViewNormalMap = 2,
+	ViewOcclusion = 3,
+	ViewRoughness = 4,
+	ViewMetallic = 5,
+	ViewOpacity = 6,
+	ViewHeight = 7,
+	ViewPathTrace = 8,
+	ViewEmission = 9,
+	ViewSubsurface = 10,
+	ViewTexCoord = 11,
+	ViewObjectNormal = 12,
+	ViewMaterialID = 13,
+	ViewObjectID = 14,
+	ViewMask = 15,
+}
+
+enum ChannelType {
+	ChannelBaseColor = 0,
+	ChannelOcclusion = 1,
+	ChannelRoughness = 2,
+	ChannelMetallic = 3,
+	ChannelNormalMap = 4,
+	ChannelHeight = 5,
+}
+
+enum RenderMode {
+	RenderDeferred = 0,
+	RenderForward = 1,
+	RenderPathTrace = 2,
+}
+
+enum ExportMode {
+	ExportVisible = 0,
+	ExportSelected = 1,
+	ExportPerObject = 2,
+	ExportPerUdimTile = 3,
+}
+
+enum ExportDestination {
+	DestinationDisk = 0,
+	DestinationPacked = 1,
+}
+
+enum PathTraceMode {
+	TraceCore = 0,
+	TraceFull = 1,
+}
+
+enum FillType {
+	FillObject = 0,
+	FillFace = 1,
+	FillAngle = 2,
+	FillUVIsland = 3,
+}
+
+enum UVType {
+	UVMap = 0,
+	UVTriplanar = 1,
+	UVProject = 2,
+}
+
+enum PickerMask {
+	MaskNone = 0,
+	MaskMaterial = 1,
+}
+
+enum BlendType {
+	BlendMix = 0,
+	BlendDarken = 1,
+	BlendMultiply = 2,
+	BlendBurn = 3,
+	BlendLighten = 4,
+	BlendScreen = 5,
+	BlendDodge = 6,
+	BlendAdd = 7,
+	BlendOverlay = 8,
+	BlendSoftLight = 9,
+	BlendLinearLight = 10,
+	BlendDifference = 11,
+	BlendSubtract = 12,
+	BlendDivide = 13,
+	BlendHue = 14,
+	BlendSaturation = 15,
+	BlendColor = 16,
+	BlendValue = 17,
+}
+
+enum CameraControls {
+	ControlsOrbit = 0,
+	ControlsRotate = 1,
+	ControlsFly = 2,
+}
+
+enum CameraType {
+	CameraPerspective = 0,
+	CameraOrthographic = 1,
+}
+
+enum TextureBits {
+	Bits8 = 0,
+	Bits16 = 1,
+	Bits32 = 2,
+}
+
+enum TextureLdrFormat {
+	FormatPng = 0,
+	FormatJpg = 1,
+}
+
+enum TextureHdrFormat {
+	FormatExr = 0,
+}
+
+enum MeshFormat {
+	FormatObj = 0,
+	FormatArm = 1,
+}
+
+enum MenuCategory {
+	MenuFile = 0,
+	MenuEdit = 1,
+	MenuViewport = 2,
+	MenuMode = 3,
+	MenuCamera = 4,
+	MenuHelp = 5,
+}
+
+enum CanvasType {
+	CanvasMaterial = 0,
+	CanvasBrush = 1,
+}
+
+enum View2DType {
+	View2DAsset = 0,
+	View2DNode = 1,
+	View2DFont = 2,
+	View2DLayer = 3,
+}
+
+enum View2DLayerMode {
+	View2DVisible = 0,
+	View2DSelected = 1,
+}
+
+enum BorderSide {
+	SideLeft = 0,
+	SideRight = 1,
+	SideTop = 2,
+	SideBottom = 3,
+}
+
+enum PaintTex {
+	TexBase = 0,
+	TexNormal = 1,
+	TexOcclusion = 2,
+	TexRoughness = 3,
+	TexMetallic = 4,
+	TexOpacity = 5,
+	TexHeight = 6,
+}
+
+enum ProjectModel {
+	ModelRoundedCube = 0,
+	ModelSphere = 1,
+	ModelTessellatedPlane = 2,
+	ModelCustom = 3,
+}
+
+enum ZoomDirection {
+	ZoomVertical = 0,
+	ZoomVerticalInverted = 1,
+	ZoomHorizontal = 2,
+	ZoomHorizontalInverted = 3,
+	ZoomVerticalAndHorizontal = 4,
+	ZoomVerticalAndHorizontalInverted = 5,
+}
+
+enum LayerSlotType {
+	SlotLayer = 0,
+	SlotMask = 1,
+	SlotGroup = 2,
+}
+
+enum SpaceType {
+	Space3D = 0,
+	Space2D = 1,
+}
+
+enum WorkspaceTool {
+	ToolBrush = 0,
+	ToolEraser = 1,
+	ToolFill = 2,
+	ToolDecal = 3,
+	ToolText = 4,
+	ToolClone = 5,
+	ToolBlur = 6,
+	ToolSmudge = 7,
+	ToolParticle = 8,
+	ToolColorId = 9,
+	ToolPicker = 10,
+	ToolBake = 11,
+	ToolGizmo = 12,
+	ToolMaterial = 13,
+}
+
+enum AreaType {
+	AreaViewport = 0,
+	Area2DView = 1,
+	AreaLayers = 2,
+	AreaMaterials = 3,
+	AreaNodes = 4,
+	AreaBrowser = 5,
+}
+
+enum TabArea {
+	TabSidebar0 = 0,
+	TabSidebar1 = 1,
+	TabStatus = 2,
+}
+
+enum TextureRes {
+	Res128 = 0,
+	Res256 = 1,
+	Res512 = 2,
+	Res1024 = 3,
+	Res2048 = 4,
+	Res4096 = 5,
+	Res8192 = 6,
+	Res16384 = 7,
+}
+
+enum LayoutSize {
+	LayoutSidebarW = 0,
+	LayoutSidebarH0 = 1,
+	LayoutSidebarH1 = 2,
+	LayoutNodesW = 3,
+	LayoutNodesH = 4,
+	LayoutStatusH = 5,
+	LayoutHeader = 6, // 0 - hidden, 1 - visible
+}

+ 464 - 0
base/Sources/ExportArm.ts

@@ -0,0 +1,464 @@
+
+class ExportArm {
+
+	static runMesh = (path: string, paintObjects: MeshObject[]) => {
+		let mesh_datas: TMeshData[] = [];
+		for (let p of paintObjects) mesh_datas.push(p.data.raw);
+		let raw: TSceneFormat = { mesh_datas: mesh_datas };
+		let b = ArmPack.encode(raw);
+		if (!path.endsWith(".arm")) path += ".arm";
+		Krom.fileSaveBytes(path, b, b.byteLength + 1);
+	}
+
+	static runProject = () => {
+		///if (is_paint || is_sculpt)
+		let mnodes: TNodeCanvas[] = [];
+		for (let m of Project.materials) {
+			let c: TNodeCanvas = JSON.parse(JSON.stringify(m.canvas));
+			for (let n of c.nodes) ExportArm.exportNode(n);
+			mnodes.push(c);
+		}
+
+		let bnodes: TNodeCanvas[] = [];
+		for (let b of Project.brushes) bnodes.push(b.canvas);
+		///end
+
+		///if is_lab
+		let c: TNodeCanvas = JSON.parse(JSON.stringify(Project.canvas));
+		for (let n of c.nodes) ExportArm.exportNode(n);
+		///end
+
+		let mgroups: TNodeCanvas[] = null;
+		if (Project.materialGroups.length > 0) {
+			mgroups = [];
+			for (let g of Project.materialGroups) {
+				let c: TNodeCanvas = JSON.parse(JSON.stringify(g.canvas));
+				for (let n of c.nodes) ExportArm.exportNode(n);
+				mgroups.push(c);
+			}
+		}
+
+		///if (is_paint || is_sculpt)
+		let md: TMeshData[] = [];
+		for (let p of Project.paintObjects) md.push(p.data.raw);
+		///end
+
+		///if is_lab
+		let md = Project.paintObjects[0].data.raw;
+		///end
+
+		let texture_files = ExportArm.assetsToFiles(Project.filepath, Project.assets);
+
+		///if (is_paint || is_sculpt)
+		let font_files = ExportArm.fontsToFiles(Project.filepath, Project.fonts);
+		let mesh_files = ExportArm.meshesToFiles(Project.filepath);
+
+		let bitsPos = Base.bitsHandle.position;
+		let bpp = bitsPos == TextureBits.Bits8 ? 8 : bitsPos == TextureBits.Bits16 ? 16 : 32;
+
+		let ld: TLayerData[] = [];
+		for (let l of Project.layers) {
+			ld.push({
+				name: l.name,
+				res: l.texpaint != null ? l.texpaint.width : Project.layers[0].texpaint.width,
+				bpp: bpp,
+				texpaint: l.texpaint != null ? Lz4.encode(l.texpaint.getPixels()) : null,
+				uv_scale: l.scale,
+				uv_rot: l.angle,
+				uv_type: l.uvType,
+				decal_mat: l.uvType == UVType.UVProject ? l.decalMat.toFloat32Array() : null,
+				opacity_mask: l.maskOpacity,
+				fill_layer: l.fill_layer != null ? Project.materials.indexOf(l.fill_layer) : -1,
+				object_mask: l.objectMask,
+				blending: l.blending,
+				parent: l.parent != null ? Project.layers.indexOf(l.parent) : -1,
+				visible: l.visible,
+				///if is_paint
+				texpaint_nor: l.texpaint_nor != null ? Lz4.encode(l.texpaint_nor.getPixels()) : null,
+				texpaint_pack: l.texpaint_pack != null ? Lz4.encode(l.texpaint_pack.getPixels()) : null,
+				paint_base: l.paintBase,
+				paint_opac: l.paintOpac,
+				paint_occ: l.paintOcc,
+				paint_rough: l.paintRough,
+				paint_met: l.paintMet,
+				paint_nor: l.paintNor,
+				paint_nor_blend: l.paintNorBlend,
+				paint_height: l.paintHeight,
+				paint_height_blend: l.paintHeightBlend,
+				paint_emis: l.paintEmis,
+				paint_subs: l.paintSubs
+				///end
+			});
+		}
+		///end
+
+		let packed_assets = (Project.raw.packed_assets == null || Project.raw.packed_assets.length == 0) ? null : Project.raw.packed_assets;
+		///if krom_ios
+		let sameDrive = false;
+		///else
+		let sameDrive = Project.raw.envmap != null ? Project.filepath.charAt(0) == Project.raw.envmap.charAt(0) : true;
+		///end
+
+		Project.raw = {
+			version: Manifest.version,
+			material_groups: mgroups,
+			assets: texture_files,
+			packed_assets: packed_assets,
+			swatches: Project.raw.swatches,
+			envmap: Project.raw.envmap != null ? (sameDrive ? Path.toRelative(Project.filepath, Project.raw.envmap) : Project.raw.envmap) : null,
+			envmap_strength: Scene.active.world.probe.raw.strength,
+			camera_world: Scene.active.camera.transform.local.toFloat32Array(),
+			camera_origin: ExportArm.vec3f32(Camera.inst.origins[0]),
+			camera_fov: Scene.active.camera.data.raw.fov,
+
+			///if (is_paint || is_sculpt)
+			mesh_datas: md,
+			material_nodes: mnodes,
+			brush_nodes: bnodes,
+			layer_datas: ld,
+			font_assets: font_files,
+			mesh_assets: mesh_files,
+			///end
+
+			///if is_paint
+			atlas_objects: Project.atlasObjects,
+			atlas_names: Project.atlasNames,
+			///end
+
+			///if is_lab
+			mesh_data: md,
+			material: c,
+			///end
+
+			///if (krom_metal || krom_vulkan)
+			is_bgra: true
+			///else
+			is_bgra: false
+			///end
+		};
+
+		///if (krom_android || krom_ios)
+		let tex = RenderPath.active.renderTargets.get(Context.raw.renderMode == RenderMode.RenderForward ? "buf" : "tex").image;
+		let mesh_icon = Image.createRenderTarget(256, 256);
+		let r = App.w() / App.h();
+		mesh_icon.g2.begin(false);
+		///if krom_opengl
+		mesh_icon.g2.drawScaledImage(tex, -(256 * r - 256) / 2, 256, 256 * r, -256);
+		///else
+		mesh_icon.g2.drawScaledImage(tex, -(256 * r - 256) / 2, 0, 256 * r, 256);
+		///end
+		mesh_icon.g2.end();
+		///if krom_metal
+		// Flush command list
+		mesh_icon.g2.begin(false);
+		mesh_icon.g2.end();
+		///end
+		let mesh_icon_pixels = mesh_icon.getPixels();
+		let u8a = new Uint8Array(mesh_icon_pixels);
+		for (let i = 0; i < 256 * 256 * 4; ++i) {
+			u8a[i] = Math.floor(Math.pow(u8a[i] / 255, 1.0 / 2.2) * 255);
+		}
+		///if (krom_metal || krom_vulkan)
+		ExportArm.bgraSwap(mesh_icon_pixels);
+		///end
+		Base.notifyOnNextFrame(() => {
+			mesh_icon.unload();
+		});
+		// Project.raw.mesh_icons =
+		// 	///if (krom_metal || krom_vulkan)
+		// 	[Lz4.encode(bgraSwap(mesh_icon_pixels)];
+		// 	///else
+		// 	[Lz4.encode(mesh_icon_pixels)];
+		// 	///end
+		Krom.writePng(Project.filepath.substr(0, Project.filepath.length - 4) + "_icon.png", mesh_icon_pixels, 256, 256, 0);
+		///end
+
+		///if (is_paint || is_sculpt)
+		let isPacked = Project.filepath.endsWith("_packed_.arm");
+		if (isPacked) { // Pack textures
+			ExportArm.packAssets(Project.raw, Project.assets);
+		}
+		///end
+
+		let buffer = ArmPack.encode(Project.raw);
+		Krom.fileSaveBytes(Project.filepath, buffer, buffer.byteLength + 1);
+
+		// Save to recent
+		///if krom_ios
+		let recent_path = Project.filepath.substr(Project.filepath.lastIndexOf("/") + 1);
+		///else
+		let recent_path = Project.filepath;
+		///end
+		let recent = Config.raw.recent_projects;
+		array_remove(recent, recent_path);
+		recent.unshift(recent_path);
+		Config.save();
+
+		Console.info(tr("Project saved"));
+	}
+
+	static textureNodeName = (): string => {
+		///if (is_paint || is_sculpt)
+		return "TEX_IMAGE";
+		///else
+		return "ImageTextureNode";
+		///end
+	}
+
+	static exportNode = (n: TNode, assets: TAsset[] = null) => {
+		if (n.type == ExportArm.textureNodeName()) {
+			let index = n.buttons[0].default_value;
+			n.buttons[0].data = Base.enumTexts(n.type)[index];
+
+			if (assets != null) {
+				let asset = Project.assets[index];
+				if (assets.indexOf(asset) == -1) {
+					assets.push(asset);
+				}
+			}
+		}
+		// Pack colors
+		if (n.color > 0) n.color -= 4294967296;
+		for (let inp of n.inputs) if (inp.color > 0) inp.color -= 4294967296;
+		for (let out of n.outputs) if (out.color > 0) out.color -= 4294967296;
+	}
+
+	///if (is_paint || is_sculpt)
+	static runMaterial = (path: string) => {
+		if (!path.endsWith(".arm")) path += ".arm";
+		let mnodes: TNodeCanvas[] = [];
+		let mgroups: TNodeCanvas[] = null;
+		let m = Context.raw.material;
+		let c: TNodeCanvas = JSON.parse(JSON.stringify(m.canvas));
+		let assets: TAsset[] = [];
+		if (UINodes.hasGroup(c)) {
+			mgroups = [];
+			UINodes.traverseGroup(mgroups, c);
+			for (let gc of mgroups) for (let n of gc.nodes) ExportArm.exportNode(n, assets);
+		}
+		for (let n of c.nodes) ExportArm.exportNode(n, assets);
+		mnodes.push(c);
+
+		let texture_files = ExportArm.assetsToFiles(path, assets);
+		let isCloud = path.endsWith("_cloud_.arm");
+		if (isCloud) path = path.replace("_cloud_", "");
+		let packed_assets: TPackedAsset[] = null;
+		if (!Context.raw.packAssetsOnExport) {
+			packed_assets = ExportArm.getPackedAssets(path, texture_files);
+		}
+
+		let raw: TProjectFormat = {
+			version: Manifest.version,
+			material_nodes: mnodes,
+			material_groups: mgroups,
+			material_icons: isCloud ? null :
+				///if (krom_metal || krom_vulkan)
+				[Lz4.encode(ExportArm.bgraSwap(m.image.getPixels()))],
+				///else
+				[Lz4.encode(m.image.getPixels())],
+				///end
+			assets: texture_files,
+			packed_assets: packed_assets
+		};
+
+		if (Context.raw.writeIconOnExport) { // Separate icon files
+			Krom.writePng(path.substr(0, path.length - 4) + "_icon.png", m.image.getPixels(), m.image.width, m.image.height, 0);
+			if (isCloud) {
+				Krom.writeJpg(path.substr(0, path.length - 4) + "_icon.jpg", m.image.getPixels(), m.image.width, m.image.height, 0, 50);
+			}
+		}
+
+		if (Context.raw.packAssetsOnExport) { // Pack textures
+			ExportArm.packAssets(raw, assets);
+		}
+
+		let buffer = ArmPack.encode(raw);
+		Krom.fileSaveBytes(path, buffer, buffer.byteLength + 1);
+	}
+	///end
+
+	///if (krom_metal || krom_vulkan)
+	static bgraSwap = (buffer: ArrayBuffer) => {
+		let view = new DataView(buffer);
+		for (let i = 0; i < Math.floor(buffer.byteLength / 4); ++i) {
+			let r = view.getUint8(i * 4);
+			view.setUint8(i * 4, view.getUint8(i * 4 + 2));
+			view.setUint8(i * 4 + 2, r);
+		}
+		return buffer;
+	}
+	///end
+
+	///if (is_paint || is_sculpt)
+	static runBrush = (path: string) => {
+		if (!path.endsWith(".arm")) path += ".arm";
+		let bnodes: TNodeCanvas[] = [];
+		let b = Context.raw.brush;
+		let c: TNodeCanvas = JSON.parse(JSON.stringify(b.canvas));
+		let assets: TAsset[] = [];
+		for (let n of c.nodes) ExportArm.exportNode(n, assets);
+		bnodes.push(c);
+
+		let texture_files = ExportArm.assetsToFiles(path, assets);
+		let isCloud = path.endsWith("_cloud_.arm");
+		if (isCloud) path = path.replace("_cloud_", "");
+		let packed_assets: TPackedAsset[] = null;
+		if (!Context.raw.packAssetsOnExport) {
+			packed_assets = ExportArm.getPackedAssets(path, texture_files);
+		}
+
+		let raw: TProjectFormat = {
+			version: Manifest.version,
+			brush_nodes: bnodes,
+			brush_icons: isCloud ? null :
+			///if (krom_metal || krom_vulkan)
+			[Lz4.encode(ExportArm.bgraSwap(b.image.getPixels()))],
+			///else
+			[Lz4.encode(b.image.getPixels())],
+			///end
+			assets: texture_files,
+			packed_assets: packed_assets
+		};
+
+		if (Context.raw.writeIconOnExport) { // Separate icon file
+			Krom.writePng(path.substr(0, path.length - 4) + "_icon.png", b.image.getPixels(), b.image.width, b.image.height, 0);
+		}
+
+		if (Context.raw.packAssetsOnExport) { // Pack textures
+			ExportArm.packAssets(raw, assets);
+		}
+
+		let buffer = ArmPack.encode(raw);
+		Krom.fileSaveBytes(path, buffer, buffer.byteLength + 1);
+	}
+	///end
+
+	static assetsToFiles = (projectPath: string, assets: TAsset[]): string[] => {
+		let texture_files: string[] = [];
+		for (let a of assets) {
+			///if krom_ios
+			let sameDrive = false;
+			///else
+			let sameDrive = projectPath.charAt(0) == a.file.charAt(0);
+			///end
+			// Convert image path from absolute to relative
+			if (sameDrive) {
+				texture_files.push(Path.toRelative(projectPath, a.file));
+			}
+			else {
+				texture_files.push(a.file);
+			}
+		}
+		return texture_files;
+	}
+
+	///if (is_paint || is_sculpt)
+	static meshesToFiles = (projectPath: string): string[] => {
+		let mesh_files: string[] = [];
+		for (let file of Project.meshAssets) {
+			///if krom_ios
+			let sameDrive = false;
+			///else
+			let sameDrive = projectPath.charAt(0) == file.charAt(0);
+			///end
+			// Convert mesh path from absolute to relative
+			if (sameDrive) {
+				mesh_files.push(Path.toRelative(projectPath, file));
+			}
+			else {
+				mesh_files.push(file);
+			}
+		}
+		return mesh_files;
+	}
+
+	static fontsToFiles = (projectPath: string, fonts: SlotFont[]): string[] => {
+		let font_files: string[] = [];
+		for (let i = 1; i <fonts.length; ++i) {
+			let f = fonts[i];
+			///if krom_ios
+			let sameDrive = false;
+			///else
+			let sameDrive = projectPath.charAt(0) == f.file.charAt(0);
+			///end
+			// Convert font path from absolute to relative
+			if (sameDrive) {
+				font_files.push(Path.toRelative(projectPath, f.file));
+			}
+			else {
+				font_files.push(f.file);
+			}
+		}
+		return font_files;
+	}
+	///end
+
+	static getPackedAssets = (projectPath: string, texture_files: string[]): TPackedAsset[] => {
+		let packed_assets: TPackedAsset[] = null;
+		if (Project.raw.packed_assets != null) {
+			for (let pa of Project.raw.packed_assets) {
+				///if krom_ios
+				let sameDrive = false;
+				///else
+				let sameDrive = projectPath.charAt(0) == pa.name.charAt(0);
+				///end
+				// Convert path from absolute to relative
+				pa.name = sameDrive ? Path.toRelative(projectPath, pa.name) : pa.name;
+				for (let tf of texture_files) {
+					if (pa.name == tf) {
+						if (packed_assets == null) {
+							packed_assets = [];
+						}
+						packed_assets.push(pa);
+						break;
+					}
+				}
+			}
+		}
+		return packed_assets;
+	}
+
+	static packAssets = (raw: TProjectFormat, assets: TAsset[]) => {
+		if (raw.packed_assets == null) {
+			raw.packed_assets = [];
+		}
+		let tempImages: Image[] = [];
+		for (let i = 0; i < assets.length; ++i) {
+			if (!Project.packedAssetExists(raw.packed_assets, assets[i].file)) {
+				let image = Project.getImage(assets[i]);
+				let temp = Image.createRenderTarget(image.width, image.height);
+				temp.g2.begin(false);
+				temp.g2.drawImage(image, 0, 0);
+				temp.g2.end();
+				tempImages.push(temp);
+				raw.packed_assets.push({
+					name: assets[i].file,
+					bytes: assets[i].file.endsWith(".jpg") ?
+						Krom.encodeJpg(temp.getPixels(), temp.width, temp.height, 0, 80) :
+						Krom.encodePng(temp.getPixels(), temp.width, temp.height, 0)
+				});
+			}
+		}
+		Base.notifyOnNextFrame(() => {
+			for (let image of tempImages) image.unload();
+		});
+	}
+
+	static runSwatches = (path: string) => {
+		if (!path.endsWith(".arm")) path += ".arm";
+		let raw = {
+			version: Manifest.version,
+			swatches: Project.raw.swatches
+		};
+		let buffer = ArmPack.encode(raw);
+		Krom.fileSaveBytes(path, buffer, buffer.byteLength + 1);
+	}
+
+	static vec3f32 = (v: Vec4): Float32Array => {
+		let res = new Float32Array(3);
+		res[0] = v.x;
+		res[1] = v.y;
+		res[2] = v.z;
+		return res;
+	}
+}

+ 17 - 0
base/Sources/ExportGpl.ts

@@ -0,0 +1,17 @@
+
+class ExportGpl {
+
+	static run = (path: string, name: string, swatches: TSwatchColor[]) => {
+		let o = "";
+		o += "GIMP Palette\n";
+		o += "Name: " + name + "\n";
+		o += "# armorpaint.org\n";
+		o += "#\n";
+
+		for (let swatch of swatches) {
+			o += String(color_get_rb(swatch.base)) + " " + String(color_get_gb(swatch.base)) + " " + String(color_get_bb(swatch.base)) + "\n";
+		}
+
+		Krom.fileSaveBytes(path, System.stringToBuffer(o), o.length);
+	}
+}

+ 9 - 0
base/Sources/ExportMesh.ts

@@ -0,0 +1,9 @@
+
+class ExportMesh {
+
+	static run = (path: string, paintObjects: MeshObject[] = null, applyDisplacement = false) => {
+		if (paintObjects == null) paintObjects = Project.paintObjects;
+		if (Context.raw.exportMeshFormat == MeshFormat.FormatObj) ExportObj.run(path, paintObjects, applyDisplacement);
+		else ExportArm.runMesh(path, paintObjects);
+	}
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff