浏览代码

Merge branch 'master' of https://github.com/HeapsIO/hide

Jed974 3 年之前
父节点
当前提交
d6dee9d6a0
共有 5 个文件被更改,包括 972 次插入35 次删除
  1. 37 4
      hide/comp/SceneEditor.hx
  2. 4 3
      hide/ui/View.hx
  3. 24 7
      hrt/prefab/l3d/Camera.hx
  4. 832 0
      hrt/prefab/l3d/Layers2D.hx
  5. 75 21
      hrt/prefab/l3d/MeshSpray.hx

+ 37 - 4
hide/comp/SceneEditor.hx

@@ -951,7 +951,7 @@ class SceneEditor {
 					if(obj3d.follow != null) {
 					if(obj3d.follow != null) {
 						if(obj3d.followPositionOnly)
 						if(obj3d.followPositionOnly)
 							parentMat.setPosition(obj3d.follow.getAbsPos().getPosition());
 							parentMat.setPosition(obj3d.follow.getAbsPos().getPosition());
-						else 
+						else
 							parentMat = obj3d.follow.getAbsPos().clone();
 							parentMat = obj3d.follow.getAbsPos().clone();
 					}
 					}
 					var invParent = parentMat;
 					var invParent = parentMat;
@@ -1861,7 +1861,8 @@ class SceneEditor {
 	function onCopy() {
 	function onCopy() {
 		if(curEdit == null) return;
 		if(curEdit == null) return;
 		if(curEdit.rootElements.length == 1) {
 		if(curEdit.rootElements.length == 1) {
-			view.setClipboard(curEdit.rootElements[0].saveData(), "prefab");
+			var prefab = curEdit.rootElements[0];
+			view.setClipboard(prefab.saveData(), "prefab", { source : view.state.path, name : prefab.name });
 		}
 		}
 		else {
 		else {
 			var lib = new hrt.prefab.Library();
 			var lib = new hrt.prefab.Library();
@@ -1872,15 +1873,44 @@ class SceneEditor {
 		}
 		}
 	}
 	}
 
 
+	function getDataPath( prefabName : String, ?sourceFile : String ) {
+		if( sourceFile == null ) sourceFile = view.state.path;
+		var datPath = new haxe.io.Path(sourceFile);
+		datPath.ext = "dat";
+		return ide.getPath(datPath.toString()+"/"+prefabName);
+	}
+
 	function onPaste() {
 	function onPaste() {
 		var parent : PrefabElement = sceneData;
 		var parent : PrefabElement = sceneData;
 		if(curEdit != null && curEdit.elements.length > 0) {
 		if(curEdit != null && curEdit.elements.length > 0) {
 			parent = curEdit.elements[0];
 			parent = curEdit.elements[0];
 		}
 		}
-		var obj = haxe.Json.parse(haxe.Json.stringify(view.getClipboard("prefab")));
+		var opts : { ref : {source:String,name:String} } = { ref : null };
+		var obj = view.getClipboard("prefab",opts);
 		if(obj != null) {
 		if(obj != null) {
 			var p = hrt.prefab.Prefab.loadPrefab(obj, parent);
 			var p = hrt.prefab.Prefab.loadPrefab(obj, parent);
 			autoName(p);
 			autoName(p);
+
+			if( opts.ref != null && opts.ref.source != null && opts.ref.name != null ) {
+				// copy data
+				var srcDir = getDataPath(opts.ref.name, opts.ref.source);
+				if( sys.FileSystem.exists(srcDir) && sys.FileSystem.isDirectory(srcDir) ) {
+					var dstDir = getDataPath(p.name);
+					function copyRec( src : String, dst : String ) {
+						if( !sys.FileSystem.exists(dst) ) sys.FileSystem.createDirectory(dst);
+						for( f in sys.FileSystem.readDirectory(src) ) {
+							var file = src+"/"+f;
+							if( sys.FileSystem.isDirectory(file) ) {
+								copyRec(file,dst+"/"+f);
+								continue;
+							}
+							sys.io.File.copy(file,dst+"/"+f);
+						}
+					}
+					copyRec(srcDir, dstDir);
+				}
+			}
+
 			addElements([p]);
 			addElements([p]);
 		}
 		}
 		else {
 		else {
@@ -2258,12 +2288,15 @@ class SceneEditor {
 		}
 		}
 	}
 	}
 
 
-	function autoName(p : PrefabElement) {
+	function autoName(p : PrefabElement ) {
 
 
 		var uniqueName = false;
 		var uniqueName = false;
 		if( p.type == "volumetricLightmap" || p.type == "light" )
 		if( p.type == "volumetricLightmap" || p.type == "light" )
 			uniqueName = true;
 			uniqueName = true;
 
 
+		if( !uniqueName && sys.FileSystem.exists(getDataPath(p.name)) )
+			uniqueName = true;
+
 		var prefix = null;
 		var prefix = null;
 		if(p.name != null && p.name.length > 0) {
 		if(p.name != null && p.name.length > 0) {
 			if(uniqueName)
 			if(uniqueName)

+ 4 - 3
hide/ui/View.hx

@@ -73,8 +73,8 @@ class View<T> extends hide.comp.Component {
 		return Type.getClassName(Type.getClass(this));
 		return Type.getClassName(Type.getClass(this));
 	}
 	}
 
 
-	public function setClipboard( v : Dynamic, ?type : String ) {
-		nw.Clipboard.get().set(ide.toJSON({ type : type == null ? viewClass : type, value : v }));
+	public function setClipboard( v : Dynamic, ?type : String, ?opts : {} ) {
+		nw.Clipboard.get().set(ide.toJSON({ type : type == null ? viewClass : type, value : v, opts : opts }));
 	}
 	}
 
 
 	public function hasClipboard( ?type : String ) {
 	public function hasClipboard( ?type : String ) {
@@ -83,9 +83,10 @@ class View<T> extends hide.comp.Component {
 		return v != null && v.type == type;
 		return v != null && v.type == type;
 	}
 	}
 
 
-	public function getClipboard( ?type : String ) : Dynamic {
+	public function getClipboard( ?type : String, ?opts : { ref : Dynamic } ) : Dynamic {
 		if( type == null ) type = viewClass;
 		if( type == null ) type = viewClass;
 		var v : Dynamic = try haxe.Json.parse(nw.Clipboard.get().get()) catch( e : Dynamic ) null;
 		var v : Dynamic = try haxe.Json.parse(nw.Clipboard.get().get()) catch( e : Dynamic ) null;
+		if( v != null && opts != null ) opts.ref = v.opts;
 		return v == null || v.type != type ? null : v.value;
 		return v == null || v.type != type ? null : v.value;
 	}
 	}
 
 

+ 24 - 7
hrt/prefab/l3d/Camera.hx

@@ -6,8 +6,6 @@ import hrt.prefab.Library;
 class CameraSyncObject extends h3d.scene.Object {
 class CameraSyncObject extends h3d.scene.Object {
 
 
 	public var enable : Bool;
 	public var enable : Bool;
-	public var dir : h3d.Vector;
-	public var pos : h3d.Vector;
 	public var fovY : Float;
 	public var fovY : Float;
 	public var zFar : Float;
 	public var zFar : Float;
 	public var zNear : Float;
 	public var zNear : Float;
@@ -16,8 +14,6 @@ class CameraSyncObject extends h3d.scene.Object {
 		if( enable ) {
 		if( enable ) {
 			var c = getScene().camera;
 			var c = getScene().camera;
 			if( c != null ) {
 			if( c != null ) {
-				c.pos.load(pos);
-				c.target.load(pos.add(dir));
 				c.fovY = fovY;
 				c.fovY = fovY;
 				c.zFar = zFar;
 				c.zFar = zFar;
 				c.zNear = zNear;
 				c.zNear = zNear;
@@ -114,8 +110,6 @@ class Camera extends Object3D {
 		drawFrustum(ctx);
 		drawFrustum(ctx);
 		var cso = Std.downcast(ctx.local3d, CameraSyncObject);
 		var cso = Std.downcast(ctx.local3d, CameraSyncObject);
 		if( cso != null ) {
 		if( cso != null ) {
-			cso.pos = getTransform().getPosition();
-			cso.dir = getTransform().front();
 			cso.fovY = fovY;
 			cso.fovY = fovY;
 			cso.zFar = zFar;
 			cso.zFar = zFar;
 			cso.zNear = zNear;
 			cso.zNear = zNear;
@@ -181,7 +175,30 @@ class Camera extends Object3D {
 			preview = !preview;
 			preview = !preview;
 			editModeButton.val(preview ? "Preview Mode : Enabled" : "Preview Mode : Disabled");
 			editModeButton.val(preview ? "Preview Mode : Enabled" : "Preview Mode : Disabled");
 			editModeButton.toggleClass("editModeEnabled", preview);
 			editModeButton.toggleClass("editModeEnabled", preview);
-			updateInstance(ctx.getContext(this));
+			var cam = ctx.scene.s3d.camera;
+			if (preview) {
+				updateInstance(ctx.getContext(this));
+				applyTo(cam);
+				ctx.scene.editor.cameraController.lockZPlanes = true;
+				ctx.scene.editor.cameraController.loadFromCamera();
+			}
+			else {
+				ctx.makeChanges(this, function() {
+					var q = new h3d.Quat();
+					q.initDirection(cam.target.sub(cam.pos));
+					var angles = q.toEuler();
+					this.rotationX = hxd.Math.fmt(angles.x * 180 / Math.PI);
+					this.rotationY = hxd.Math.fmt(angles.y * 180 / Math.PI);
+					this.rotationZ = hxd.Math.fmt(angles.z * 180 / Math.PI);
+					this.scaleX = this.scaleY = this.scaleZ = 1;
+					this.x = hxd.Math.fmt(cam.pos.x);
+					this.y = hxd.Math.fmt(cam.pos.y);
+					this.z = hxd.Math.fmt(cam.pos.z);
+					this.zFar = cam.zFar;
+					this.zNear = cam.zNear;
+					this.fovY = cam.fovY;
+				});
+			}
 		});
 		});
 
 
 		props.find(".copy").click(function(e) {
 		props.find(".copy").click(function(e) {

+ 832 - 0
hrt/prefab/l3d/Layers2D.hx

@@ -0,0 +1,832 @@
+package hrt.prefab.l3d;
+
+typedef Layer2DValue = {
+	index	: Int,
+	name 	: String,
+	color	: Int
+}
+
+typedef Layer2D = {
+	name	: String,
+	values 	: Array<Layer2DValue>
+}
+
+class LayerView2DRFXShader extends h3d.shader.ScreenShader {
+
+	static var SRC = {
+
+		@param var layerAlpha : Float;
+		@param var worldSize : Float;
+
+		@param var collideMap : Sampler2D;
+		@const var collideEnable : Bool;
+		@param var collideMask : Vec4;
+		@param var collideScale : Float;
+
+		@const var layerEnable : Bool;
+		@param var layerMap : Sampler2D;
+		@param var layerScale : Float;
+
+		@param var colors : Sampler2D;
+		@param var nbColorsIndexes : Int;
+
+		@param var curFrame : Sampler2D;
+		@param var cameraInverseViewProj : Mat4;
+
+		@const var PACKED_DEPTH : Bool;
+		@param var depthChannel : Channel;
+		@param var depthTexture : Sampler2D;
+
+		@const var highlightNoPixels : Bool;
+		@param var highlightColor : Vec4;
+
+		var isSky : Bool;
+
+		function getPixelPosition( uv : Vec2 ) : Vec3 {
+			var d = PACKED_DEPTH ? unpack(depthTexture.get(uv)) : depthChannel.get(uv).r;
+			var tmp = vec4(uvToScreen(uv), d, 1) * cameraInverseViewProj;
+			tmp.xyz /= tmp.w;
+			isSky = d == 1.0;
+			return tmp.xyz;
+		}
+
+		function floatToInt( v : Float ) : Int {
+			return int(v * 255);
+		}
+
+		function fragment() {
+			var curColor = curFrame.get(calculatedUV);
+
+			pixelColor = curColor;
+
+			var curPos = getPixelPosition(calculatedUV).xy;
+
+			var collide = false;
+
+			if ( collideEnable ) {
+				var tex = collideMap.get( floor( curPos / collideScale ) * collideScale / worldSize );
+				if ( (tex.rgb-collideMask.rgb).length() < 1e-3 )
+					collide = true;
+			}
+
+			if ( layerEnable ) {
+				var layer = layerMap.get( floor( curPos / layerScale ) * layerScale / worldSize );
+				var index = (floatToInt(layer.a) << 24) + (floatToInt(layer.r) << 16) + (floatToInt(layer.g) << 8) + (floatToInt(layer.b));
+				if ( index > 0 )
+					pixelColor.rgba = colors.get(vec2((index + 0.5) / nbColorsIndexes, 0.5));
+				else if ( !collide && highlightNoPixels )
+					pixelColor.rgba = highlightColor;
+			}
+
+			if ( collide )
+				pixelColor = mix(mix(curColor, collideMask, 0.5), pixelColor, layerAlpha/2);
+			else
+				pixelColor = mix(curColor, pixelColor, layerAlpha);
+
+		}
+
+	}
+}
+
+class Layer2DRFX extends hrt.prefab.rfx.RendererFX {
+
+	public var pass = new h3d.pass.ScreenFx(new LayerView2DRFXShader());
+
+	var tmp = new h3d.Matrix();
+	var curMatNoJitter = new h3d.Matrix();
+
+	override function begin( r:h3d.scene.Renderer, step:h3d.impl.RendererFX.Step ) {
+		if( step == MainDraw ) {
+			var ctx = r.ctx;
+			var s = pass.shader;
+
+			curMatNoJitter.load(ctx.camera.m);
+			s.cameraInverseViewProj.initInverse(curMatNoJitter);
+		}
+	}
+
+	override function end( r:h3d.scene.Renderer, step:h3d.impl.RendererFX.Step ) {
+		var ctx = r.ctx;
+		if( step == AfterTonemapping ) {
+			r.mark("TemporalFiltering");
+			var output : h3d.mat.Texture = ctx.engine.getCurrentTarget();
+			var depthMap : Dynamic = ctx.getGlobal("depthMap");
+
+			var curFrame = r.allocTarget("curFrame", false, 1.0, output.format);
+			h3d.pass.Copy.run(output, curFrame);
+
+			var s = pass.shader;
+			s.curFrame = curFrame;
+
+			s.PACKED_DEPTH = depthMap.packed != null && depthMap.packed == true;
+			if( s.PACKED_DEPTH ) {
+				s.depthTexture = depthMap.texture;
+			}
+			else {
+				s.depthChannel = depthMap.texture;
+				s.depthChannelChannel = depthMap.channel == null ? hxsl.Channel.R : depthMap.channel;
+			}
+
+			r.setTarget(output);
+			pass.render();
+		}
+	}
+
+}
+
+@:allow(hrt.prefab.l3d)
+class Layers2D extends hrt.prefab.Object3D {
+
+	@:s var layers : Array<Layer2D> = [];
+	@:s var layerScale : Int = 1;
+	@:s var collidePath : String;
+	@:s var collideMask : Int = 0xff0000;
+	@:s var worldSize : Int = 4096;
+	var layerTextures : Map<String, hxd.Pixels> = [];
+
+	@:s var highlightColor : Int = 0xff00ff;
+
+	#if editor
+	var storedCtx : hrt.prefab.Context;
+
+	var currentPixels : hxd.Pixels = null;
+	var currentTexture : h3d.mat.Texture = null;
+	var interactive : h2d.Interactive;
+	var gBrushes : Array<h3d.scene.Mesh>;
+
+	var brushRadius : Float = 20;
+	var eraseRadius : Float = 10;
+	var paintOverride : Bool = true;
+	var layerAlpha : Float = 0.6;
+
+	var collideEnable : Bool = true;
+	var currentLayer : String;
+	var currentLayerValue : Int;
+
+	var collideMap : h3d.mat.Texture;
+	var collidePixels : hxd.Pixels;
+	var colorMap : h3d.mat.Texture;
+
+	var highlightNotPaintedPixels : Bool = false;
+
+	var rfx : Layer2DRFX;
+
+	var sceneEditor : hide.comp.SceneEditor;
+	var editorCtx : hide.prefab.EditContext;
+
+	var undo(get, null):hide.ui.UndoHistory;
+	function get_undo() { return sceneEditor.view.undo; }
+
+	#end
+
+	#if editor
+	override function save() : {} {
+		var o : Dynamic = super.save();
+
+		for ( l in layers ) {
+			var pixels = layerTextures.get(l.name);
+			if ( pixels != null ) {
+				var contextShared = storedCtx.shared;
+				var path = new haxe.io.Path(contextShared.currentPath);
+				path.ext = "dat";
+				var fileName = "layer_" + l.name;
+				contextShared.saveTexture(fileName, pixels.toPNG(), path.toString() + "/" + this.name, "png");
+			}
+		}
+
+		return o;
+	}
+	#end
+
+	override function makeInstance( ctx : hrt.prefab.Context ) : hrt.prefab.Context {
+		ctx = super.makeInstance(ctx);
+		#if editor
+		storedCtx = ctx;
+		#end
+
+		for ( l in layers ) {
+			var path = new haxe.io.Path(ctx.shared.currentPath);
+			path.ext = "dat";
+			var datDir = path.toString();
+			var fileName = "layer_" + l.name + ".png";
+			var pixels = loadPixels(datDir + "/" + this.name + "/" + fileName);
+			if ( pixels != null )
+				layerTextures.set(l.name, pixels);
+		}
+		return ctx;
+	}
+
+	function loadPixels(path : String) {
+		return try hxd.res.Loader.currentInstance.load(path).toImage().getPixels() catch( e : hxd.res.NotFound )
+			#if editor try hxd.res.Any.fromBytes(path, sys.io.File.getBytes(hide.Ide.inst.getPath(path))).toImage().getPixels() catch( e : Dynamic ) #end
+			null;
+	}
+
+	public function getLayerValue(key : String, x : Float, y : Float) {
+		var tex = layerTextures.get(key);
+		if ( tex == null ) return null;
+		var ix = Std.int(x / layerScale);
+		var iy = Std.int(y / layerScale);
+		if ( ix < 0 || ix > tex.width || iy < 0 || iy > tex.height )
+			return null;
+
+		for ( layer in layers ) {
+			if ( layer.name == key ) {
+				var color = tex.getPixel(ix, iy);
+				for ( lv in layer.values ) {
+					if ( lv.index == color ) {
+						return lv.name;
+					}
+				}
+			}
+		}
+
+		return null;
+	}
+
+	#if editor
+
+	function removeInteractiveBrush() {
+		if( interactive != null ) interactive.remove();
+		clearBrushes();
+	}
+
+	function clearBrushes() {
+		if( gBrushes != null ) {
+			for (g in gBrushes) g.remove();
+			gBrushes = null;
+		}
+	}
+
+	function setupRfx( ectx : hide.prefab.EditContext, b : Bool ) {
+		if ( ectx == null )
+			return;
+		if ( b ) {
+			if ( rfx == null ) {
+				rfx = new Layer2DRFX();
+				var renderer = Std.downcast(ectx.scene.s3d.renderer, h3d.scene.pbr.Renderer);
+				renderer.effects.push(rfx);
+			}
+		} else if ( rfx != null ) {
+			var renderer = Std.downcast(ectx.scene.s3d.renderer, h3d.scene.pbr.Renderer);
+			renderer.effects.remove(rfx);
+			rfx = null;
+		}
+	}
+
+	function updateVisuals( ctx : hrt.prefab.Context ) {
+		if ( rfx != null ) {
+			var sh : LayerView2DRFXShader = cast rfx.pass.shader;
+			if( collideMap == null || collideMap.isDisposed() ) {
+				if ( collidePath != null ) {
+					collideMap = ctx.loadTexture(collidePath);
+					collideMap.filter = Nearest;
+					collidePixels = loadPixels(collidePath);
+				}
+			}
+
+			sh.layerAlpha = layerAlpha;
+			sh.worldSize = worldSize;
+			sh.collideScale = (collideMap != null) ? worldSize / collideMap.width : 1;
+			sh.collideMap = collideMap;
+
+			sh.collideEnable = collideEnable && collideMap != null;
+			sh.collideMask = h3d.Vector.fromColor(collideMask);
+
+			sh.layerEnable = currentTexture != null;
+			sh.layerMap = currentTexture;
+			sh.layerScale = layerScale;
+
+			if ( sh.layerEnable ) {
+				sh.colors = colorMap;
+				sh.nbColorsIndexes = colorMap.width;
+			}
+
+			sh.highlightNoPixels = highlightNotPaintedPixels;
+			sh.highlightColor = h3d.Vector.fromColor(highlightColor);
+		}
+	}
+
+	var revertList : Array<{ pixels : hxd.Pixels, layer : String }> = [];
+	var revertCurrentIdx : Int = 0;
+	var currentRevertData : { pixels : hxd.Pixels, layer : String };
+	function prepareUploadPixels() {
+		if ( revertList.length == 0 ) {
+			saveUploadPixels();
+		}
+	}
+
+	function saveUploadPixels() {
+		currentTexture.uploadPixels(currentPixels);
+
+		if ( revertCurrentIdx < revertList.length )
+			revertList.resize(revertCurrentIdx);
+
+		revertList.push({ pixels : currentPixels.clone(), layer: currentLayer });
+
+		revertCurrentIdx = revertList.length;
+
+		undo.change(Custom(function(undo) {
+
+			var revertDataToApply = if ( undo )
+										revertList[--revertCurrentIdx];
+									else
+										revertList[revertCurrentIdx++];
+
+			currentPixels = revertDataToApply.pixels;
+
+			layerTextures.set(revertDataToApply.layer, currentPixels);
+
+			if ( currentLayer == revertDataToApply.layer ) {
+				currentTexture.uploadPixels(currentPixels);
+				updateVisuals(editorCtx.getContext(this));
+			}
+		}));
+	}
+
+	function createInteractiveBrush(ectx : hide.prefab.EditContext) {
+		editorCtx = ectx;
+		var ctx = ectx.getContext(this);
+		var s2d = @:privateAccess ctx.local2d.getScene();
+		interactive = new h2d.Interactive(10000, 10000, s2d);
+		interactive.propagateEvents = true;
+		interactive.cancelEvents = false;
+
+		var modified = false;
+
+		var layer = layers.filter(l -> l.name == currentLayer)[0];
+
+		var layerValue = layer.values.filter( vl -> vl.index == currentLayerValue )[0];
+
+		function drawBrush() {
+			var worldPos = ectx.screenToGround(s2d.mouseX, s2d.mouseY);
+
+			if( worldPos == null ) {
+				clearBrushes();
+				return;
+			}
+
+			var radius = brushRadius;
+			var color = layerValue.color;
+			if ( hxd.Key.isDown(hxd.Key.SHIFT) ) {
+				radius = eraseRadius;
+				color = 0xff0000;
+			}
+
+			drawCircle(ctx, worldPos.x, worldPos.y, worldPos.z, radius, 5, color);
+		}
+
+		interactive.onWheel = function(e) {
+			if ( hxd.Key.isDown(hxd.Key.CTRL) ) {
+				if ( hxd.Key.isDown(hxd.Key.SHIFT) ) {
+					eraseRadius += e.wheelDelta * 2;
+					if ( eraseRadius < 1 )
+						eraseRadius = 1;
+					@:privateAccess ectx.properties.fields.filter( f -> f.fname == "eraseRadius")[0].range.value = eraseRadius;
+				} else {
+					brushRadius += e.wheelDelta * 2;
+					if ( brushRadius < 1 )
+						brushRadius = 1;
+					@:privateAccess ectx.properties.fields.filter( f -> f.fname == "brushRadius")[0].range.value = brushRadius;
+				}
+				e.propagate = false;
+				drawBrush();
+			}
+		};
+
+		function paint() {
+			var clean = hxd.Key.isDown( hxd.Key.SHIFT);
+			var worldPos = ectx.screenToGround(s2d.mouseX, s2d.mouseY);
+
+			var collideScale = collidePixels.width / worldSize;
+
+			if ( worldPos != null ) {
+				var startX = worldPos.x;
+				var startY = worldPos.y;
+
+				var brRadius = ( clean ) ? eraseRadius : brushRadius;
+				var radiusSq = brRadius * brRadius;
+
+				var minX = Math.floor(startX - brRadius);
+				var maxX = Math.ceil(startX + brRadius);
+				var minY = Math.floor(startY - brRadius);
+				var maxY = Math.ceil(startY + brRadius);
+
+				if ( clean ) {
+					for ( iy in minY...maxY ) {
+						for ( ix in minX...maxX ) {
+							var vec = new h2d.col.Point(ix - startX, iy - startY);
+							var distSq = vec.x*vec.x + vec.y*vec.y;
+							var tx = Math.floor(ix / layerScale);
+							var ty = Math.floor(iy / layerScale);
+							if ( distSq <= radiusSq && currentPixels.getPixel(tx, ty) != 0 ) {
+								currentPixels.setPixel(tx, ty, 0x00000000);
+								modified = true;
+							}
+						}
+					}
+				} else {
+					var distances : Array<Int> = [];
+					var queue : Array<Int> = [];
+
+					inline function isCollide( ix : Int, iy : Int ) {
+						return collideEnable && collidePixels.getPixel(Math.floor(ix * collideScale), Math.floor(iy * collideScale)) & collideMask == collideMask;
+					}
+
+					function add( ix : Int, iy : Int, d : Int ) {
+						var flagIdx = (iy - minY) * (maxX - minX) + ix;
+						var dist = distances[ flagIdx ];
+						if ( dist != null && dist <= d )
+							return;
+
+						distances[ flagIdx ] = d;
+
+						queue.push(ix);
+						queue.push(iy);
+						queue.push(d);
+					}
+
+					function process( ix : Int, iy : Int, d : Int ) {
+						var dx = Math.abs(ix - startX);
+						var dy = Math.abs(iy - startY);
+
+						var distSq = dx * dx + dy * dy;
+
+						if ( distSq > radiusSq )
+							return;
+
+						var collide = isCollide(ix, iy);
+
+						if ( d == 0 && collide )
+							return;
+
+						if ( d > brushRadius * 1.414 )
+							return;
+
+						var tx = Math.floor(ix / layerScale);
+						var ty = Math.floor(iy / layerScale);
+						var currentColor = currentPixels.getPixel(tx, ty);
+						if ( (paintOverride || currentColor == 0) && currentColor != currentLayerValue ) {
+							currentPixels.setPixel(tx, ty, currentLayerValue);
+							modified = true;
+						}
+
+						if ( collide )
+							return;
+
+						var newD = d + 1;
+
+						add(ix-layerScale, iy, newD);
+						add(ix+layerScale, iy, newD);
+						add(ix, iy-layerScale, newD);
+						add(ix, iy+layerScale, newD);
+					}
+
+					add(Math.round(startX), Math.round(startY), 0);
+
+					while ( queue.length > 0 ) {
+						process(queue.shift(), queue.shift(), queue.shift());
+					}
+
+				}
+
+				currentTexture.uploadPixels(currentPixels);
+			}
+		}
+
+		interactive.onPush = function(e) {
+			e.propagate = false;
+
+			modified = false;
+
+			prepareUploadPixels();
+
+			drawBrush();
+			paint();
+		};
+
+		interactive.onRelease = function(e) {
+			e.propagate = false;
+			drawBrush();
+			if ( modified )
+				saveUploadPixels();
+		};
+
+		var layerChanged = false;
+		interactive.onKeyDown = function(e) {
+			if ( layerChanged )
+				return;
+			var NB_KEYS = layer.values.length+1;
+			for ( k in hxd.Key.NUMBER_0...hxd.Key.NUMBER_0+NB_KEYS ) {
+				if ( hxd.Key.isPressed(k) ) {
+					layerChanged = true;
+					currentLayerValue = layer.values[(((k-hxd.Key.NUMBER_0)-1+NB_KEYS)%NB_KEYS)].index;
+				}
+			}
+
+			drawBrush();
+
+			if ( layerChanged ) {
+				haxe.Timer.delay(function() ectx.rebuildProperties(), 0);
+			}
+		}
+
+		interactive.onMove = function(e) {
+			drawBrush();
+
+			if( hxd.Key.isDown( hxd.Key.MOUSE_LEFT) ) {
+				e.propagate = false;
+
+				paint();
+			}
+		};
+		updateVisuals(ctx);
+
+	}
+
+	public function drawCircle(ctx : hrt.prefab.Context, originX : Float, originY : Float, originZ : Float, radius: Float, thickness: Float, color) {
+		var newColor = h3d.Vector.fromColor(color);
+		if (gBrushes == null || gBrushes.length == 0 || gBrushes[0].scaleX != radius || gBrushes[0].material.color != newColor) {
+			clearBrushes();
+			gBrushes = [];
+			var gBrush = new h3d.scene.Mesh(hrt.prefab.l3d.MeshSpray.makePrimCircle(64, 0.95), ctx.local3d);
+			gBrush.scaleX = gBrush.scaleY = radius;
+			gBrush.ignoreParentTransform = true;
+			var pass = gBrush.material.mainPass;
+			pass.setPassName("outline");
+			pass.depthTest = Always;
+			pass.depthWrite = false;
+			gBrush.material.shadows = false;
+			gBrush.material.color = newColor;
+			gBrushes.push(gBrush);
+			gBrush = new h3d.scene.Mesh(new h3d.prim.Sphere(Math.min(radius*0.05, 0.35)), ctx.local3d);
+			gBrush.ignoreParentTransform = true;
+			var pass = gBrush.material.mainPass;
+			pass.setPassName("outline");
+			pass.depthTest = Always;
+			pass.depthWrite = false;
+			gBrush.material.shadows = false;
+			gBrush.material.color = newColor;
+			gBrushes.push(gBrush);
+		}
+		for (g in gBrushes) g.visible = true;
+		for (g in gBrushes) {
+			g.x = originX;
+			g.y = originY;
+			g.z = originZ + 0.025;
+		}
+	}
+
+	override function setSelected( ctx : hrt.prefab.Context, b : Bool ) {
+		if( !b ) {
+			removeInteractiveBrush();
+		}
+		setupRfx(editorCtx, b);
+		updateVisuals(ctx);
+		return false;
+	}
+
+	function updateColors() {
+
+		var layer = layers.filter(l -> l.name == currentLayer)[0];
+
+		var maxIndex = 0;
+		for ( v in layer.values ) {
+			if ( maxIndex < v.index )
+				maxIndex = v.index;
+		}
+		var pixels = hxd.Pixels.alloc(maxIndex+1, 1, RGBA);
+		for ( v in layer.values ) {
+			pixels.setPixel(v.index, 0, v.color);
+		}
+
+		if ( colorMap != null )
+			colorMap.dispose();
+
+		colorMap = h3d.mat.Texture.fromPixels(pixels);
+		colorMap.filter = Nearest;
+	}
+
+	override function edit( ectx : hide.prefab.EditContext ) {
+		super.edit(ectx);
+		var ctx = ectx.getContext(this);
+
+		sceneEditor = ectx.scene.editor;
+
+		var props = new hide.Element('<div>
+					<div class="group" name="Layers">
+						<dl>
+							<dt>Layer Scale</dt><dd><input type="text" value="$layerScale" style="width:110px" disabled /><button id="changeLayerScale" >Change</button></dd>
+						</dl>
+						<dl>
+							<dt>Brush Radius</dt><dd><input type="range" min="1" max="100" field="brushRadius"/></dd>
+							<dt>Erase Radius</dt><dd><input type="range" min="1" max="100" field="eraseRadius"/></dd>
+							<dt>Layer Alpha</dt><dd><input type="range" min="0" max="1" field="layerAlpha"/></dd>
+							<dt>Collide</dt>
+							<dd>
+								<input type="checkbox" field="collideEnable"/>
+								<input type="texturepath" style="width:125px" field="collidePath"/>
+								<input type="color" field="collideMask"/>
+							</dd>
+							<dt>World Size</dt><dd><input type="range" min="1" max="4096" field="worldSize"/></dd>
+							<dt>Highlight Unpainted</dt>
+							<dd>
+								<input type="checkbox" field="highlightNotPaintedPixels"/>
+								<input type="color" field="highlightColor"/>
+							</dd>
+						</dl>
+						<dl>
+							<dt>Paint override</dt><dd><input type="checkbox" field="paintOverride" /></dd>
+						<dl>
+						<p>
+							<b><i>
+							Hold down SHIFT to erase<br />
+							Hold down CTRL + Wheel to adjust brush radius
+						</p>
+						<ul id="layers"></ul>
+					</div>
+				</div>');
+
+		props.find("#changeLayerScale").click(function(_) {
+			var input = hide.Ide.inst.ask("Change layer scale will erase all layers data ! Are you sure ?");
+			if ( input == null || input.length == 0 )  return;
+			var value = Std.parseInt(input);
+			if ( Std.string(value) != input ) return;
+			if ( value == layerScale) return;
+			layerScale = value;
+			layerTextures = [];
+			currentLayer = null;
+			currentLayerValue = null;
+			setupRfx(ectx, false);
+			ectx.rebuildProperties();
+		});
+		var list = props.find("ul#layers");
+		ectx.properties.add(props,this, (pname) -> {
+			if ( pname == "collidePath" ) {
+				if ( collideMap != null ) {
+					collideMap.dispose();
+					collideMap = null;
+				}
+			}
+			updateVisuals(ctx);
+		});
+
+		function selectLayer( name : String ) {
+			if ( currentLayer != name ) {
+				currentLayer = name;
+
+				currentPixels = layerTextures.get(currentLayer);
+				if ( currentPixels == null ) {
+					currentPixels = hxd.Pixels.alloc(Math.floor(worldSize / layerScale), Math.floor(worldSize / layerScale), RGBA);
+					layerTextures.set(currentLayer, currentPixels);
+				}
+				currentTexture = h3d.mat.Texture.fromPixels(currentPixels, RGBA);
+				currentTexture.filter = Nearest;
+				updateColors();
+
+				currentLayerValue = null;
+			} else {
+				currentLayer = null;
+				currentLayerValue = null;
+			}
+			ectx.rebuildProperties();
+		}
+
+		for( layer in layers ) {
+			var borderColor = currentLayer == layer.name ? "green" : "darkgrey";
+			var e = new hide.Element('<div style="margin-top: 5px; border: 1px solid $borderColor; padding: 6px;">
+				<input type="text" style="width:100px" field="name"/> <button id="selectLayer">Select</button>
+				<div class="layerValues" style="margin-top: 5px; vspacing:" />
+			</div>
+			');
+			ectx.properties.build(e, layer, (pname) -> { });
+
+			if ( currentLayer == layer.name ) {
+				var layerValues = e.find(".layerValues");
+				for ( vLayer in layer.values ) {
+					var rowStyle = (currentLayerValue == vLayer.index) ? "border: 2px solid green; background: #001d00;" : "border: 2px solid #111111;";
+					var lValueContent = new hide.Element('<div style="margin: 3px; padding: 2px; $rowStyle" >
+						<input type="color" field="color"/>
+						<input type="text" style="width:110px" field="name"/>
+						<button id="paintVLayer">Paint</button>
+						<button id="delVLayer">Del.</button>
+					</div>');
+					lValueContent.appendTo(layerValues);
+					ectx.properties.build(lValueContent, vLayer, (pname) -> {
+						if (pname == "color") {
+							updateColors();
+							updateVisuals(ctx);
+						}
+					});
+
+					lValueContent.find("#paintVLayer").click(function(_) {
+						if ( currentLayerValue == vLayer.index )
+							currentLayerValue = null;
+						else {
+							currentLayerValue = vLayer.index;
+						}
+						ectx.rebuildProperties();
+					});
+
+					lValueContent.find("#delVLayer").click(function(_) {
+						if ( hide.Ide.inst.confirm('Delete value "${vLayer.name}" ?') ) {
+							layer.values.remove(vLayer);
+							prepareUploadPixels();
+							for ( iy in 0...currentPixels.height ) {
+								for ( ix in 0...currentPixels.width ) {
+									if ( currentPixels.getPixel(ix, iy) & vLayer.index == vLayer.index )
+										currentPixels.setPixel(ix, iy, 0);
+								}
+							}
+							saveUploadPixels();
+							currentTexture.uploadPixels(currentPixels);
+							if ( currentLayerValue == vLayer.index )
+								currentLayerValue = null;
+
+							ectx.rebuildProperties();
+						}
+					});
+				}
+
+				var vLayerAction = new hide.Element('<div class="btn-list" align="center">
+					<input type="button" value="Add value" id="addVLayer" />
+					<input type="button" value="Delete layer" id="delLayer" />
+				</div>').appendTo(layerValues);
+
+				vLayerAction.find("#addVLayer").click(function(_) {
+					var name = hide.Ide.inst.ask("Layer value name:");
+					if (name == null || name.length == 0) return;
+					var maxIndex = 0;
+					for ( v in layer.values ) {
+						if ( maxIndex < v.index )
+							maxIndex = v.index;
+					}
+					var validIdx = maxIndex+1;
+					for ( i in 1...maxIndex ) {
+						if ( layer.values.filter( v -> v.index == i ).length == 0 ) {
+							validIdx = i;
+							break;
+						}
+					}
+					layer.values.push({
+						index	: validIdx,
+						name 	: name,
+						color	: 0x00ff00
+					});
+					updateColors();
+					ectx.rebuildProperties();
+				});
+
+				vLayerAction.find("#delLayer").click(function(_) {
+					if ( hide.Ide.inst.confirm('Delete layer "${layer.name}" ?') ) {
+						layers.remove(layer);
+						currentLayer = null;
+						currentLayerValue = null;
+						ectx.rebuildProperties();
+					}
+				});
+			}
+			e.find("#selectLayer").click(function(_) {
+				selectLayer(layer.name);
+			});
+			e.appendTo(list);
+		}
+
+		if ( currentLayerValue != null ) {
+			createInteractiveBrush(ectx);
+		} else {
+			removeInteractiveBrush();
+		}
+
+		setupRfx(ectx, true);
+		updateVisuals(ctx);
+
+		var actions = new hide.Element('<div class="btn-list" align="center" style="margin-top: 5px;" ></div>').appendTo(list);
+		var addLayer = new hide.Element('<input type="button" value="Create a new layer" />').appendTo(actions);
+		addLayer.click(function(_) {
+			var name = hide.Ide.inst.ask("Layer name:");
+			if (name == null || name.length == 0) return;
+			for ( l in layers ) {
+				if ( l.name == name ) {
+					hide.Ide.inst.message("Another layer already has this name");
+					return;
+				}
+			}
+			layers.push({
+				name 	: name,
+				values	: []
+			});
+			selectLayer(name);
+			ectx.rebuildProperties();
+		});
+	}
+
+	override function getHideProps() : hide.prefab.HideProps {
+		return { icon : "industry", name : "Layers 2D", isGround : true };
+	}
+
+	#end
+
+	static var _ = hrt.prefab.Library.register("layers2D", Layers2D);
+
+}

+ 75 - 21
hrt/prefab/l3d/MeshSpray.hx

@@ -448,17 +448,29 @@ class MeshSpray extends Object3D {
 		sys.io.File.saveContent(MESH_SPRAY_CONFIG_PATH, hide.Ide.inst.toJSON(allSetGroups));
 		sys.io.File.saveContent(MESH_SPRAY_CONFIG_PATH, hide.Ide.inst.toJSON(allSetGroups));
 	}
 	}
 
 
-	function setGroundPos( ectx : EditContext, obj : Object3D ) {
-		var pos = obj.getAbsPos();
+	function setGroundPos( ectx : EditContext, obj : Object3D = null, absPos : h3d.col.Point = null ) : { mz : Float, rotX : Float, rotY : Float, rotZ : Float } {
+		if (absPos == null && obj == null)
+			throw "setGroundPos should use either object or absPos";
+		var tx : Float; var ty : Float; var tz : Float;
+		if ( absPos != null ) {
+			tx = absPos.x;
+			ty = absPos.y;
+			tz = absPos.z;
+		} else { // obj != null
+			tx = obj.getAbsPos().tx;
+			ty = obj.getAbsPos().ty;
+			tz = obj.getAbsPos().tz;
+		}
 		var config = currentConfig;
 		var config = currentConfig;
-		var tz = ectx.positionToGroundZ(pos.tx, pos.ty);
-		var mz = config.zOffset + tz - pos.tz;
-		obj.z += mz;
+		var groundZ = ectx.positionToGroundZ(tx, ty);
+		var mz = config.zOffset + groundZ - tz;
+		if ( obj != null )
+			obj.z += mz;
 		var orient = config.orientTerrain;
 		var orient = config.orientTerrain;
 		var tilt = config.tiltAmount;
 		var tilt = config.tiltAmount;
 
 
 		inline function getPoint(dx,dy) {
 		inline function getPoint(dx,dy) {
-			var dz = ectx.positionToGroundZ(pos.tx + 0.1 * dx, pos.ty + 0.1 * dy) - tz;
+			var dz = ectx.positionToGroundZ(tx + 0.1 * dx, ty + 0.1 * dy) - groundZ;
 			return new h3d.col.Point(dx*0.1, dy*0.1, dz * orient);
 			return new h3d.col.Point(dx*0.1, dy*0.1, dz * orient);
 		}
 		}
 
 
@@ -470,14 +482,21 @@ class MeshSpray extends Object3D {
 		var m = q.toMatrix();
 		var m = q.toMatrix();
 		m.prependRotation(Math.random()*tilt*Math.PI/8,0,  (config.rotation + (Std.random(2) == 0 ? -1 : 1) * Math.round(Math.random() * config.rotationOffset)) * Math.PI / 180);
 		m.prependRotation(Math.random()*tilt*Math.PI/8,0,  (config.rotation + (Std.random(2) == 0 ? -1 : 1) * Math.round(Math.random() * config.rotationOffset)) * Math.PI / 180);
 		var a = m.getEulerAngles();
 		var a = m.getEulerAngles();
-		obj.rotationX = hxd.Math.fmt(a.x * 180 / Math.PI);
-		obj.rotationY = hxd.Math.fmt(a.y * 180 / Math.PI);
-		obj.rotationZ = hxd.Math.fmt(a.z * 180 / Math.PI);
+		var rotX = hxd.Math.fmt(a.x * 180 / Math.PI);
+		var rotY = hxd.Math.fmt(a.y * 180 / Math.PI);
+		var rotZ = hxd.Math.fmt(a.z * 180 / Math.PI);
+		if ( obj != null ) {
+			obj.rotationX = rotX;
+			obj.rotationY = rotY;
+			obj.rotationZ = rotZ;	
+		}
+		return { mz : mz, rotX : rotX, rotY : rotY, rotZ : rotZ };
 	}
 	}
 
 
 	var wasEdited = false;
 	var wasEdited = false;
 	var previewModels : Array<hrt.prefab.Prefab> = [];
 	var previewModels : Array<hrt.prefab.Prefab> = [];
 	var sprayedModels : Array<hrt.prefab.Prefab> = [];
 	var sprayedModels : Array<hrt.prefab.Prefab> = [];
+	var selectElement : hide.Element;
 	override function edit( ectx : EditContext ) {
 	override function edit( ectx : EditContext ) {
 
 
 		invParent = getAbsPos().clone();
 		invParent = getAbsPos().clone();
@@ -518,7 +537,7 @@ class MeshSpray extends Object3D {
 
 
 		var setsList = new hide.Element('<div align="center" ></div>').appendTo(preset);
 		var setsList = new hide.Element('<div align="center" ></div>').appendTo(preset);
 
 
-		var selectElement = new hide.Element('<select multiple size="6" style="width: 300px" ></select>').appendTo(props);
+		selectElement = new hide.Element('<select multiple size="6" style="width: 300px" ></select>').appendTo(props);
 
 
 		function onChangeSet() {
 		function onChangeSet() {
 			selectElement.empty();
 			selectElement.empty();
@@ -708,16 +727,30 @@ class MeshSpray extends Object3D {
 		});
 		});
 
 
 		options.find("#toground").click(function(_) {
 		options.find("#toground").click(function(_) {
-			var mso = null;
-			for( c in children ) {
+			var ctx = ectx.getContext(this);
+			var mso = cast(ctx.local3d,MeshSprayObject);
+			for( c in this.children ) {
 				var obj = c.to(Object3D);
 				var obj = c.to(Object3D);
 				if( obj == null ) continue;
 				if( obj == null ) continue;
 				setGroundPos(ectx, obj);
 				setGroundPos(ectx, obj);
 				var ctx = ectx.getContext(obj);
 				var ctx = ectx.getContext(obj);
 				if( ctx != null ) obj.applyTransform(ctx.local3d);
 				if( ctx != null ) obj.applyTransform(ctx.local3d);
-				if ( ctx.local3d.parent != null && Std.is(ctx.local3d.parent, MeshSprayObject))
-					mso = cast(ctx.local3d.parent,MeshSprayObject);
 			}
 			}
+			if ( this.binaryMeshes != null ) {
+				var pos = new h3d.col.Point(0,0,0);
+				for ( bm in this.binaryMeshes ) {
+					var pivot = mso.getAbsPos();
+					pos.x = bm.x + pivot.tx;
+					pos.y = bm.y + pivot.ty;
+					pos.z = bm.z + pivot.tz;
+					var ground = setGroundPos(ectx, null, pos);
+					bm.z += ground.mz;
+					bm.rotX = ground.rotX;
+					bm.rotY = ground.rotY;
+					bm.rotZ = ground.rotZ;
+				}
+			}
+			
 			mso.redraw();
 			mso.redraw();
 		});
 		});
 
 
@@ -827,6 +860,7 @@ class MeshSpray extends Object3D {
 				undo.change(Custom(function(undo) {
 				undo.change(Custom(function(undo) {
 					if(undo) {
 					if(undo) {
 						sceneEditor.deleteElements(addedModels, () -> removeInteractiveBrush(), true, false);
 						sceneEditor.deleteElements(addedModels, () -> removeInteractiveBrush(), true, false);
+						clearPreview();
 					}
 					}
 					else {
 					else {
 						sceneEditor.addElements(addedModels, false, true, false);
 						sceneEditor.addElements(addedModels, false, true, false);
@@ -1003,10 +1037,31 @@ class MeshSpray extends Object3D {
 				} while (nbTry-- > 0);
 				} while (nbTry-- > 0);
 
 
 				var meshId = 0;
 				var meshId = 0;
-				if(currentMeshes.length > 1) {
-					do
-						meshId = Std.random(currentMeshes.length)
-					while(CONFIG.dontRepeatMesh && meshId == lastMeshId);
+				var meshUsed = null;
+				var options = selectElement.children().elements();
+				var selectedMeshes = [];
+				for (opt in options) {
+					if (opt.prop("selected")) {
+						var findMesh = currentMeshes.filter((m) -> m.path == opt.val());
+						if (findMesh.length > 0)
+							selectedMeshes.push(findMesh[0]);
+					}
+				}
+				if (selectedMeshes.length > 0) {
+					if(selectedMeshes.length > 1) {
+						do
+							meshId = Std.random(selectedMeshes.length)
+						while(CONFIG.dontRepeatMesh && meshId == lastMeshId);
+					}
+					meshUsed = selectedMeshes[meshId];
+				}
+				else {
+					if(currentMeshes.length > 1) {
+						do
+							meshId = Std.random(currentMeshes.length)
+						while(CONFIG.dontRepeatMesh && meshId == lastMeshId);
+					}
+					meshUsed = currentMeshes[meshId];
 				}
 				}
 				lastIndexMesh = meshId;
 				lastIndexMesh = meshId;
 				if (computedDensity == 1)
 				if (computedDensity == 1)
@@ -1014,7 +1069,6 @@ class MeshSpray extends Object3D {
 				else
 				else
 					lastMeshId = -1;
 					lastMeshId = -1;
 
 
-				var meshUsed = currentMeshes[meshId];
 
 
 				var newPrefab : hrt.prefab.Object3D = null;
 				var newPrefab : hrt.prefab.Object3D = null;
 
 
@@ -1125,7 +1179,7 @@ class MeshSpray extends Object3D {
 			gBrush.scaleX = gBrush.scaleY = radius;
 			gBrush.scaleX = gBrush.scaleY = radius;
 			gBrush.ignoreParentTransform = true;
 			gBrush.ignoreParentTransform = true;
 			var pass = gBrush.material.mainPass;
 			var pass = gBrush.material.mainPass;
-			pass.setPassName("outline");
+			pass.setPassName("overlay");
 			pass.depthTest = Always;
 			pass.depthTest = Always;
 			pass.depthWrite = false;
 			pass.depthWrite = false;
 			gBrush.material.shadows = false;
 			gBrush.material.shadows = false;
@@ -1134,7 +1188,7 @@ class MeshSpray extends Object3D {
 			gBrush = new h3d.scene.Mesh(new h3d.prim.Sphere(Math.min(radius*0.05, 0.35)), ctx.local3d);
 			gBrush = new h3d.scene.Mesh(new h3d.prim.Sphere(Math.min(radius*0.05, 0.35)), ctx.local3d);
 			gBrush.ignoreParentTransform = true;
 			gBrush.ignoreParentTransform = true;
 			var pass = gBrush.material.mainPass;
 			var pass = gBrush.material.mainPass;
-			pass.setPassName("outline");
+			pass.setPassName("overlay");
 			pass.depthTest = Always;
 			pass.depthTest = Always;
 			pass.depthWrite = false;
 			pass.depthWrite = false;
 			gBrush.material.shadows = false;
 			gBrush.material.shadows = false;