浏览代码

Splines: rework spline tool + spline data

lviguier 9 月之前
父节点
当前提交
408c651e26
共有 6 个文件被更改,包括 1326 次插入1624 次删除
  1. 70 0
      bin/style.css
  2. 95 0
      bin/style.less
  3. 0 653
      hide/prefab/SplineEditor.hx
  4. 976 600
      hrt/prefab/l3d/Spline.hx
  5. 185 259
      hrt/prefab/l3d/SplineMesh.hx
  6. 0 112
      hrt/prefab/l3d/SplineMover.hx

+ 70 - 0
bin/style.css

@@ -3865,3 +3865,73 @@ hide-popover hide-content {
   color: black;
   border-radius: 3px;
 }
+.points-inspector {
+  width: 100%;
+  margin-top: 10px;
+}
+.points-inspector .content {
+  width: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  flex-direction: row-reverse;
+}
+.points-inspector .content .buttons {
+  background: #222222;
+  border-radius: 5px 5px 0 0;
+  padding: 3px 10px 3px 10px;
+  cursor: pointer;
+}
+.points-inspector .content .buttons div {
+  margin: 0 2px 0 2px;
+}
+.points-inspector .content .buttons div:hover {
+  color: #535353;
+}
+.points-inspector .content .points-container {
+  min-height: 10px;
+  width: 100%;
+  border-radius: 5px 0 5px 5px;
+  background: #222222;
+  padding: 3px;
+}
+.points-inspector .content .points-container .point {
+  margin-bottom: 1px;
+}
+.points-inspector .content .points-container .point .header {
+  padding: 3px 0px 3px 8px;
+  background-color: #414141;
+  border-radius: 5px;
+  cursor: pointer;
+  display: flex;
+}
+.points-inspector .content .points-container .point .header .icon {
+  align-self: center;
+  margin-right: 5px;
+  cursor: pointer;
+}
+.points-inspector .content .points-container .point .header .icon:hover {
+  color: #ececec;
+}
+.points-inspector .content .points-container .point .header p {
+  margin: 0;
+}
+.points-inspector .content .points-container .point .header:hover {
+  background-color: #686868;
+}
+.points-inspector .content .points-container .point .body {
+  padding-bottom: 15px;
+  white-space: nowrap;
+}
+.points-inspector .content .points-container .point .body dd input {
+  width: 28%;
+  margin-right: 1px;
+}
+.points-inspector .content .points-container .point.folded .body {
+  display: none;
+}
+.points-inspector .content .points-container .point.selected .header {
+  background-color: #2c5d87;
+}
+.points-inspector .content .points-container .point.selected .body {
+  background-color: #4f657f;
+}

+ 95 - 0
bin/style.less

@@ -4526,5 +4526,100 @@ hide-popover {
 		}
 
 
+	}
+}
+
+.points-inspector {
+	@background-color : #222222;
+	@background-color-highlight : #535353;
+
+	width: 100%;
+	margin-top: 10px;
+
+	.content {
+		width: 100%;
+		display: flex;
+		flex-wrap: wrap;
+		flex-direction: row-reverse;
+
+		.buttons {
+			background: @background-color;
+			border-radius: 5px 5px 0 0;
+			padding: 3px 10px 3px 10px;
+			cursor: pointer;
+
+			div {
+				margin: 0 2px 0 2px;
+			}
+
+			div:hover {
+				color: @background-color-highlight;
+			}
+		}
+
+		.points-container {
+			min-height: 10px;
+			width: 100%;
+			border-radius: 5px 0 5px 5px;
+			background: @background-color;
+			padding: 3px;
+
+			.point {
+				margin-bottom: 1px;
+
+				.header {
+					padding: 3px 0px 3px 8px;
+					background-color: #414141;
+					border-radius: 5px;
+					cursor: pointer;
+					display: flex;
+
+					.icon {
+						align-self: center;
+						margin-right: 5px;
+						cursor: pointer;
+					}
+
+					.icon:hover {
+						color: #ececec;
+					}
+
+					p {
+						margin: 0;
+					}
+				}
+
+				.header:hover {
+					background-color: #686868;
+				}
+
+				.body {
+					padding-bottom: 15px;
+					white-space: nowrap;
+					dd {
+						input {
+							width: 28%;
+							margin-right: 1px;
+						}
+					}
+				}
+
+				&.folded {
+					.body {
+						display: none;
+					}
+				}
+
+				&.selected {
+					.header {
+						background-color: #2c5d87;
+					}
+
+					.body {
+						background-color: #4f657f;
+					}
+				}
+			}
+		}
 	}
 }

+ 0 - 653
hide/prefab/SplineEditor.hx

@@ -1,653 +0,0 @@
-package hide.prefab;
-import hxd.Key as K;
-import hrt.prefab.l3d.Spline;
-
-#if editor
-
-class NewSplinePointViewer extends h3d.scene.Object {
-
-	var pointViewer : h3d.scene.Mesh;
-	var connectionViewer : h3d.scene.Graphics;
-	var tangentViewer : h3d.scene.Graphics;
-
-	public function new( parent : h3d.scene.Object ) {
-		super(parent);
-		name = "SplinePointViewer";
-		pointViewer = new h3d.scene.Mesh(h3d.prim.Sphere.defaultUnitSphere(), null, this);
-		pointViewer.name = "pointViewer";
-		pointViewer.material.setDefaultProps("ui");
-		pointViewer.material.color.set(1,1,0,1);
-		pointViewer.material.mainPass.depthTest = Always;
-
-		connectionViewer = new h3d.scene.Graphics(this);
-		connectionViewer.name = "connectionViewer";
-		connectionViewer.lineStyle(3, 0xFFFF00);
-		connectionViewer.material.mainPass.setPassName("ui");
-		connectionViewer.material.mainPass.depthTest = Always;
-		connectionViewer.clear();
-
-		tangentViewer = new h3d.scene.Graphics(this);
-		tangentViewer.name = "tangentViewerViewer";
-		tangentViewer.lineStyle(3, 0xFFFF00);
-		tangentViewer.material.mainPass.setPassName("ui");
-		tangentViewer.material.mainPass.depthTest = Always;
-		tangentViewer.clear();
-	}
-
-	override function sync( ctx : h3d.scene.RenderContext ) {
-		var cam = ctx.camera;
-		var gpos = pointViewer.getAbsPos().getPosition();
-		var distToCam = cam.pos.sub(gpos).length();
-		var engine = h3d.Engine.getCurrent();
-		var ratio = 18 / engine.height;
-		var correctionFromParents =  1.0 / getAbsPos().getScale().x;
-		pointViewer.setScale(correctionFromParents * ratio * distToCam * Math.tan(cam.fovY * 0.5 * Math.PI / 180.0));
-		calcAbsPos();
-		super.sync(ctx);
-	}
-
-	public function update( spd : SplinePointData ) {
-
-		pointViewer.setPosition(spd.pos.x, spd.pos.y, spd.pos.z);
-
-		tangentViewer.clear();
-		tangentViewer.visible = spd.tangent != null;
-		if( spd.tangent != null ) {
-			var scale = 1.0;
-			if( spd.prev != null && spd.next != null ) scale = (spd.prev.scaleX + spd.next.scaleX) * 0.5;
-			else if( spd.prev != null ) scale = spd.prev.scaleX;
-			else if( spd.next != null ) scale = spd.next.scaleX;
-			tangentViewer.moveTo(spd.pos.x - spd.tangent.x * scale, spd.pos.y - spd.tangent.y * scale, spd.pos.z - spd.tangent.z * scale);
-			tangentViewer.lineTo(spd.pos.x + spd.tangent.x * scale, spd.pos.y + spd.tangent.y * scale, spd.pos.z + spd.tangent.z * scale);
-		}
-
-		// Only display the connection if we are adding the new point at the end or the beggining fo the spline
-		connectionViewer.clear();
-		connectionViewer.visible = spd.prev == null || spd.next == null;
-		if( connectionViewer.visible ) {
-			var startPos = spd.prev == null ? spd.next.getPoint() : spd.prev.getPoint();
-			connectionViewer.moveTo(startPos.x, startPos.y, startPos.z);
-			connectionViewer.lineTo(spd.pos.x, spd.pos.y, spd.pos.z);
-		}
-	}
-}
-
-@:access(hrt.prefab.l3d.Spline)
-class SplineEditor {
-
-	public var prefab : Spline;
-	public var editContext : EditContext;
-	var editMode = false;
-	var undo : hide.ui.UndoHistory;
-
-	var interactive : h2d.Interactive;
-
-	 // Easy way to keep track of viewers
-	var gizmos : Array<hrt.tools.Gizmo> = [];
-	var newSplinePointViewer : NewSplinePointViewer;
-
-	var grid : h3d.scene.Object;
-
-	public function new( prefab : Spline, undo : hide.ui.UndoHistory ){
-		this.prefab = prefab;
-		this.undo = undo;
-	}
-
-	public function update(?propName : String ) {
-		if( editMode ) {
-			showViewers();
-		}
-	}
-
-	function reset() {
-		removeViewers();
-		removeGizmos();
-		if( interactive != null ) {
-			interactive.remove();
-			interactive = null;
-		}
-		if( newSplinePointViewer != null ) {
-			newSplinePointViewer.remove();
-			newSplinePointViewer = null;
-		}
-	}
-
-	function getClosestSplinePointFromMouse( mouseX : Float, mouseY : Float) : SplinePoint {
-		if(prefab == null || prefab.local3d == null || prefab.local3d.getScene() == null )
-			return null;
-
-		var mousePos = new h3d.Vector( mouseX / h3d.Engine.getCurrent().width, 1.0 - mouseY / h3d.Engine.getCurrent().height, 0);
-		var minDist = -1.0;
-		var result : SplinePoint = null;
-		for( sp in prefab.points ) {
-			var screenPos = sp.getPoint().toVector();
-			screenPos.project(prefab.local3d.getScene().camera.m);
-			screenPos.z = 0;
-			screenPos.scale(0.5);
-			screenPos = screenPos.add(new h3d.Vector(0.5,0.5));
-			var dist = screenPos.distance(mousePos);
-			if( dist < minDist || minDist == -1 ) {
-				minDist = dist;
-				result = sp;
-			}
-		}
-		return result;
-	}
-
-	function getNewPointPosition( mouseX : Float, mouseY : Float) : SplinePointData {
-		if( prefab.points.length == 0 ) {
-			return { pos : prefab.local3d.getAbsPos().getPosition().toPoint(), tangent : prefab.local3d.getAbsPos().right().toPoint() , prev : null, next : null };
-		}
-
-		var closestPt = getClosestPointFromMouse(mouseX, mouseY);
-
-		// If we are are adding a new point at the beginning/end, just make a raycast 'cursor -> plane' with the transform of the first/last SplinePoint
-		if( !prefab.loop && (closestPt.next == null || closestPt.prev == null) ) {
-			var camera = @:privateAccess prefab.local3d.getScene().camera;
-			var ray = camera.rayFromScreen(mouseX, mouseY);
-			var normal = getClosestPlanNormal();
-			var point = closestPt.next == null ? closestPt.prev.getAbsPos().getPosition().toPoint() : closestPt.next.getAbsPos().getPosition().toPoint();
-			var plane = h3d.col.Plane.fromNormalPoint(normal, point);
-			var pt = ray.intersect(plane);
-			return { pos : pt, tangent : closestPt.tangent, prev : closestPt.prev, next : closestPt.next };
-		}
-		else
-			return closestPt;
-	}
-
-	/**
-		Used to get the normal of the plan that is the more parallel to the viewport
-	**/
-	function getClosestPlanNormal() {
-		var camera = @:privateAccess prefab.local3d.getScene().camera;
-		var x = new h3d.Vector(1,0,0);
-		var y = new h3d.Vector(0,1,0);
-		var z = new h3d.Vector(0,0,1);
-
-		var normal = x;
-		var dot = Math.abs(camera.getForward().dot(x));
-
-		var tmpDot = Math.abs(camera.getForward().dot(y));
-		if (tmpDot > dot) {
-			normal = y;
-			dot = tmpDot;
-		}
-
-		tmpDot = Math.abs(camera.getForward().dot(z));
-		if (tmpDot > dot) {
-			normal = z;
-			dot = tmpDot;
-		}
-
-		return normal;
-	}
-
-	function getClosestPointFromMouse( mouseX : Float, mouseY : Float) : SplinePointData {
-
-		if(prefab == null || prefab.local3d == null || prefab.local3d.getScene() == null )
-			return null;
-
-		var result : SplinePointData = null;
-		var mousePos = new h3d.Vector( mouseX / h3d.Engine.getCurrent().width, 1.0 - mouseY / h3d.Engine.getCurrent().height, 0);
-		var minDist = -1.0;
-		for( s in prefab.data.samples ) {
-			var screenPos = s.pos.toVector();
-			screenPos.project(prefab.local3d.getScene().camera.m);
-			screenPos.z = 0;
-			screenPos.scale(0.5);
-			screenPos = screenPos.add(new h3d.Vector(0.5,0.5));
-			var dist = screenPos.distance(mousePos);
-			if( (dist < minDist || minDist == -1) && dist < 0.1 ) {
-				minDist = dist;
-				result = s;
-			}
-		}
-
-		if( result == null ) {
-			result = { pos : null, tangent : null, prev : null, next : null };
-
-			var firstSp = prefab.points[0];
-			var firstPt = firstSp.getPoint();
-			var firstPtScreenPos = firstPt.toVector();
-			firstPtScreenPos.project(prefab.local3d.getScene().camera.m);
-			firstPtScreenPos.z = 0;
-			firstPtScreenPos.scale(0.5);
-			firstPtScreenPos = firstPtScreenPos.add(new h3d.Vector(0.5,0.5));
-			var distToFirstPoint = firstPtScreenPos.distance(mousePos);
-
-			var lastSp = prefab.points[prefab.points.length - 1];
-			var lastPt = lastSp.getPoint();
-			var lastPtSreenPos = lastPt.toVector();
-			lastPtSreenPos.project(prefab.local3d.getScene().camera.m);
-			lastPtSreenPos.z = 0;
-			lastPtSreenPos.scale(0.5);
-			lastPtSreenPos = lastPtSreenPos.add(new h3d.Vector(0.5,0.5));
-			var distTolastPoint = lastPtSreenPos.distance(mousePos);
-
-			if( distTolastPoint < distToFirstPoint ) {
-				result.pos = lastPt;
-				result.tangent = lastSp.getAbsPos().right().toPoint();
-				result.prev = prefab.points[prefab.points.length - 1];
-				result.next = null;
-			}
-			else {
-				result.pos = firstPt;
-				result.tangent = firstSp.getAbsPos().right().toPoint();
-				result.prev = null;
-				result.next = prefab.points[0];
-			}
-		}
-
-		return result;
-	}
-
-	function addSplinePoint( spd : SplinePointData) : SplinePoint {
-
-		var invMatrix = prefab.local3d.getInvPos();
-
-		var pos = spd.pos.toVector();
-		pos.project(invMatrix);
-
-		var index = 0;
-		var scale = 1.0;
-		if( spd.prev == null && spd.next == null ) {
-			scale = 1.0;
-			index = 0;
-		}
-		else if( spd.prev == null ) {
-			index = 0;
-			scale = prefab.points[0].getAbsPos().getScale().x;
-		}
-		else if( spd.next == null ) {
-			index = prefab.points.length;
-			scale = prefab.points[prefab.points.length - 1].getAbsPos().getScale().x;
-		}
-		else {
-			index = prefab.points.indexOf(spd.next);
-			scale = (spd.prev.scaleX + spd.next.scaleX) * 0.5;
-		}
-
-		var sp = new SplinePoint(prefab, null);
-		sp.x = pos.x;
-		sp.y = pos.y;
-		sp.z = pos.z;
-		prefab.children.remove(sp);
-		prefab.children.insert(index, sp);
-		if( spd.tangent != null ) {
-			var dir = spd.tangent.toVector();
-			dir.transform3x3(invMatrix); // Don't take the translation
-			dir.scale(-1);
-			var m = h3d.Matrix.lookAtX(dir);
-			var euler = m.getEulerAngles();
-			sp.rotationX = hxd.Math.radToDeg(euler.x);
-			sp.rotationY = hxd.Math.radToDeg(euler.y);
-			sp.rotationZ = hxd.Math.radToDeg(euler.z);
-
-			trace(sp.rotationX, sp.rotationY, sp.rotationZ);
-
-		}
-		sp.scaleX = scale;
-		sp.scaleY = scale;
-		sp.scaleZ = scale;
-		editContext.scene.editor.addElements([sp], false, true, false);
-
-		prefab.updateInstance();
-		showViewers();
-		return sp;
-	}
-
-	function removeViewers() {
-		for( sp in prefab.points ) {
-			sp.setViewerVisible(false);
-		}
-	}
-
-	function showViewers() {
-		for( sp in prefab.points ) {
-			sp.setViewerVisible(true);
-		}
-	}
-
-	function removeGizmos() {
-		for( g in gizmos ) {
-			g.remove();
-			@:privateAccess editContext.scene.editor.updates.remove(g.updateLocal);
-		}
-		gizmos = [];
-	}
-
-	function createGizmos() {
-		removeGizmos(); // Security, avoid duplication
-		var sceneEditor = @:privateAccess editContext.scene.editor;
-		var sceneGizmo = @:privateAccess sceneEditor.gizmo;
-		for( sp in prefab.points ) {
-			var gizmo = new hrt.tools.Gizmo(editContext.scene.s3d, editContext.scene.s2d);
-			gizmo.snap = sceneEditor.gizmoSnap;
-			gizmo.shoudSnapOnGrid = function() {
-				return sceneEditor.snapForceOnGrid;
-			}
-
-			switch(sceneGizmo.editMode) {
-				case Translation:
-					gizmo.translationMode();
-				case Rotation:
-					gizmo.rotationMode();
-				case Scaling:
-					gizmo.scalingMode();
-			}
-			gizmo.getRotationQuat().identity();
-			gizmo.visible = true;
-			var tmpMat = new h3d.Matrix();
-			tmpMat.load(sp.getAbsPos());
-			var tmpScale = tmpMat.getScale();
-			tmpMat.prependScale(1.0/tmpScale.x, 1.0/tmpScale.y, 1.0/tmpScale.z);
-			gizmo.setTransform(tmpMat);
-			@:privateAccess sceneEditor.updates.push( gizmo.updateLocal );
-			gizmos.insert(gizmos.length, gizmo);
-			gizmo.visible = false; // Not visible by default, only show the closest in the onMove of interactive
-
-			var posQuant = @:privateAccess sceneEditor.view.config.get("sceneeditor.xyzPrecision");
-			var scaleQuant = @:privateAccess sceneEditor.view.config.get("sceneeditor.scalePrecision");
-			var rotQuant = @:privateAccess sceneEditor.view.config.get("sceneeditor.rotatePrecision");
-
-			inline function quantize(x: Float, step: Float) {
-				if(step > 0) {
-					x = Math.round(x / step) * step;
-					x = untyped parseFloat(x.toFixed(5)); // Snap to closest nicely displayed float :cold_sweat:
-				}
-				return x;
-			}
-
-			gizmo.onStartMove = function(mode) {
-
-				var sceneObj = sp.local3d;
-				var obj3d = sp.to(hrt.prefab.Object3D);
-				var pivotPt = sceneObj.getAbsPos().getPosition();
-				var pivot = new h3d.Matrix();
-				pivot.initTranslation(pivotPt.x, pivotPt.y, pivotPt.z);
-				var invPivot = pivot.clone();
-				invPivot.invert();
-
-				var localMat : h3d.Matrix = sceneEditor.worldMat(sceneObj).clone();
-				localMat.multiply(localMat, invPivot);
-
-				var prevState = obj3d.saveTransform();
-				gizmo.onMove = function(translate: h3d.Vector, rot: h3d.Quat, scale: h3d.Vector) {
-					var transf = new h3d.Matrix();
-					transf.identity();
-
-					if(rot != null) rot.toMatrix(transf);
-					if(translate != null) transf.translate(translate.x, translate.y, translate.z);
-
-					var newMat = localMat.clone();
-					newMat.multiply(newMat, transf);
-					newMat.multiply(newMat, pivot);
-					if(sceneEditor.snapToGround && (mode == MoveXY || mode == MoveX || mode == MoveY || mode == MoveZ || mode == MoveYZ || mode == MoveZX)) {
-						newMat.tz = sceneEditor.getZ(newMat.tx, newMat.ty);
-					}
-
-					var parentInvMat = sceneObj.parent.getAbsPos().clone();
-					parentInvMat.initInverse(parentInvMat);
-					newMat.multiply(newMat, parentInvMat);
-					if(scale != null) newMat.prependScale(scale.x, scale.y, scale.z);
-
-					var rot = newMat.getEulerAngles();
-					obj3d.x = quantize(newMat.tx, posQuant);
-					obj3d.y = quantize(newMat.ty, posQuant);
-					obj3d.z = quantize(newMat.tz, posQuant);
-					obj3d.rotationX = quantize(hxd.Math.radToDeg(rot.x), rotQuant);
-					obj3d.rotationY = quantize(hxd.Math.radToDeg(rot.y), rotQuant);
-					obj3d.rotationZ = quantize(hxd.Math.radToDeg(rot.z), rotQuant);
-					if(scale != null) {
-						inline function scaleSnap(x: Float) {
-							if(K.isDown(K.CTRL)) {
-								var step = K.isDown(K.SHIFT) ? 0.5 : 1.0;
-								x = Math.round(x / step) * step;
-							}
-							return x;
-						}
-						var s = newMat.getScale();
-						obj3d.scaleX = quantize(scaleSnap(s.x), scaleQuant);
-						obj3d.scaleY = quantize(scaleSnap(s.y), scaleQuant);
-						obj3d.scaleZ = quantize(scaleSnap(s.z), scaleQuant);
-					}
-					obj3d.applyTransform();
-				}
-
-				gizmo.onFinishMove = function() {
-					var newState = obj3d.saveTransform();
-					undo.change(Custom(function(undo) {
-						if( undo ) {
-							obj3d.loadTransform(prevState);
-							obj3d.applyTransform();
-							prefab.updateInstance();
-							showViewers();
-							@:privateAccess editContext.scene.editor.refreshTree();
-							showViewers();
-							createGizmos();
-						}
-						else {
-							obj3d.loadTransform(newState);
-							obj3d.applyTransform();
-							prefab.updateInstance();
-							showViewers();
-							createGizmos();
-						}
-					}));
-					var worldPos = prefab.local3d.localToGlobal(new h3d.col.Point(sp.x, sp.y, sp.z));
-					gizmo.setPosition(worldPos.x, worldPos.y, worldPos.z);
-				}
-			}
-		}
-		sceneGizmo.onChangeMode = function(mode) {
-			for (gizmo in gizmos) {
-				switch(mode) {
-					case Translation:
-						gizmo.translationMode();
-					case Rotation:
-						gizmo.rotationMode();
-					case Scaling:
-						gizmo.scalingMode();
-				}
-			}
-		};
-	}
-
-	public function setSelected(b : Bool ) {
-		reset();
-
-		if( !b ) {
-			editMode = false;
-			return;
-		}
-
-		if( editMode ) {
-			createGizmos();
-			var s2d = prefab.shared.root2d.getScene();
-			interactive = new h2d.Interactive(10000, 10000, s2d);
-			interactive.propagateEvents = true;
-			interactive.onPush =
-				function(e) {
-					// Add a new point
-					if( K.isDown( K.MOUSE_LEFT ) && K.isDown( K.CTRL )  ) {
-						e.propagate = false;
-						var pt = getNewPointPosition(s2d.mouseX, s2d.mouseY);
-						var sp = addSplinePoint(pt);
-						showViewers();
-						createGizmos();
-
-						undo.change(Custom(function(undo) {
-							if( undo ) {
-								editContext.scene.editor.deleteElements([sp], () -> {}, false, false);
-								for (sp in prefab.points)
-									sp.computeName();
-								@:privateAccess editContext.scene.editor.refreshTree();
-								prefab.updateInstance();
-								showViewers();
-								createGizmos();
-							}
-							else {
-								addSplinePoint(pt);
-								showViewers();
-								createGizmos();
-							}
-						}));
-
-					}
-					// Delete a point
-					if( K.isDown( K.MOUSE_LEFT ) && K.isDown( K.SHIFT )  ) {
-						e.propagate = false;
-						var sp = getClosestSplinePointFromMouse(s2d.mouseX, s2d.mouseY);
-						var index = prefab.points.indexOf(sp);
-						editContext.scene.editor.deleteElements([sp], () -> {}, false, false);
-						for (sp in prefab.points)
-							sp.computeName();
-						@:privateAccess editContext.scene.editor.refreshTree();
-
-						prefab.updateInstance();
-						showViewers();
-						createGizmos();
-
-						undo.change(Custom(function(undo) {
-							if( undo ) {
-								prefab.children.insert(index, sp);
-								editContext.scene.editor.addElements([sp], false, true, false);
-								prefab.updateInstance();
-								showViewers();
-								createGizmos();
-							}
-							else {
-								editContext.scene.editor.deleteElements([sp], () -> {}, false, false);
-								for (sp in prefab.points)
-									sp.computeName();
-								@:privateAccess editContext.scene.editor.refreshTree();
-								prefab.updateInstance();
-								showViewers();
-								createGizmos();
-							}
-						}));
-					}
-				};
-
-			interactive.onMove =
-				function(e) {
-
-					if( prefab.points.length == 0 )
-						return;
-
-					// Only show the gizmo of the closest splinePoint
-					var closetSp = getClosestSplinePointFromMouse(s2d.mouseX, s2d.mouseY);
-					var index = prefab.points.indexOf(closetSp);
-					for( g in gizmos ) {
-						g.visible = gizmos.indexOf(g) == index && !K.isDown( K.CTRL ) && !K.isDown( K.SHIFT );
-					}
-
-					if( K.isDown( K.CTRL ) ) {
-						if( newSplinePointViewer == null )
-							newSplinePointViewer = new NewSplinePointViewer(prefab.local3d.getScene());
-						newSplinePointViewer.visible = true;
-
-						var npt = getNewPointPosition(s2d.mouseX, s2d.mouseY);
-
-						if (grid != null) {
-							grid.remove();
-							grid = null;
-						}
-
-						var normal = getClosestPlanNormal();
-						var closest = getClosestPointFromMouse(s2d.mouseX, s2d.mouseY);
-						grid = editContext.scene.editor.createGrid(closest.pos , normal, 200, 1, normal * 0.2);
-
-						newSplinePointViewer.update(npt);
-					}
-					else {
-						if( newSplinePointViewer != null )
-							newSplinePointViewer.visible = false;
-
-						if (grid != null) {
-							grid.remove();
-							grid = null;
-						}
-					}
-
-					if( K.isDown( K.SHIFT ) ) {
-						var index = prefab.points.indexOf(getClosestSplinePointFromMouse(s2d.mouseX, s2d.mouseY));
-						for( sp in prefab.points ) {
-							if( index == prefab.points.indexOf(sp) )
-								sp.setColor(0xFFFF0000);
-							else
-								sp.setColor(0xFF0000FF);
-						}
-					}
-
-				};
-		}
-	}
-
-	public function edit( ctx : EditContext ) {
-
-		var props = new hide.Element('
-		<div class="spline-editor">
-			<div class="group" name="Utility">
-				<div align="center">
-					<input type="button" value="Reverse" class="reverse"/>
-				</div>
-			</div>
-			<div class="group" name="Description">
-				<div class="description">
-					<i>Ctrl + Left Click</i> Add a point on the spline <br>
-					<i>Shift + Left Click</i> Delete a point from the spline
-				</div>
-			</div>
-			<div class="group" name="Tool">
-				<div align="center">
-					<input type="button" value="Edit Mode : Disabled" class="editModeButton" />
-				</div>
-			</div>
-		</div>');
-
-		var reverseButton = props.find(".reverse");
-		reverseButton.click(function(_) {
-			prefab.children.reverse();
-			for (sp in prefab.points) {
-				sp.rotationZ += hxd.Math.degToRad(180);
-				sp.computeName();
-			}
-			@:privateAccess editContext.scene.editor.refreshTree();
-
-			undo.change(Custom(function(undo) {
-				prefab.children.reverse();
-				for (sp in prefab.points) {
-					sp.rotationZ += hxd.Math.degToRad(180);
-					sp.computeName();
-				}
-				@:privateAccess editContext.scene.editor.refreshTree();
-			}));
-			ctx.onChange(prefab, null);
-			removeGizmos();
-			createGizmos();
-		});
-
-		var editModeButton = props.find(".editModeButton");
-		editModeButton.toggleClass("editModeEnabled", editMode);
-		editModeButton.click(function(_) {
-			editMode = !editMode;
-			prefab.onEdit(editMode);
-			editModeButton.val(editMode ? "Edit Mode : Enabled" : "Edit Mode : Disabled");
-			editModeButton.toggleClass("editModeEnabled", editMode);
-			prefab.updateInstance();
-			setSelected(true);
-			@:privateAccess editContext.scene.editor.showGizmo = !editMode;
-			ctx.onChange(prefab, null);
-		});
-
-		ctx.properties.add(props, this, function(pname) {
-			ctx.onChange(prefab, pname);
-		});
-
-		return props;
-	}
-
-}
-
-#end

+ 976 - 600
hrt/prefab/l3d/Spline.hx

@@ -2,785 +2,1161 @@ package hrt.prefab.l3d;
 
 import hxd.Math;
 
-enum CurveShape {
+enum HandleType {
+	Point;
+	TangentIn(point : Spline.SplinePoint);
+	TangentOut(point : Spline.SplinePoint);
+}
+
+enum SplineShape {
 	Linear;
 	Quadratic;
 	Cubic;
 }
 
-typedef SplinePointData = {
-	pos : h3d.col.Point,
-	tangent : h3d.col.Point,
-	prev : SplinePoint,
-	next : SplinePoint,
-	?t : Float
-}
+class SplinePoint {
+	public var pos : h3d.col.Point; // Relative to the spline
+	public var up : h3d.Vector; // Relative to the spline
+	public var tangentIn : h3d.Vector; // Relative to point
+	public var tangentOut : h3d.Vector; // Relative to point
+	public var length : Float = 0;
+	public var t : Float = 0;
+
+	public function new(?pos: h3d.col.Point, ?up: h3d.Vector, ?tangentIn: h3d.Vector, ?tangentOut: h3d.Vector) {
+		this.pos = (pos == null) ? new h3d.col.Point(0, 0, 0) : pos.clone();
+		this.up = (up == null) ? new h3d.col.Point(0, 0, 1) : up.clone();
+		this.tangentIn = (tangentIn == null) ? new h3d.Vector(-1, 0, 0) : tangentIn.clone();
+		this.tangentOut = (tangentOut == null) ? new h3d.Vector(1, 0, 0) : tangentOut.clone();
+	}
 
-class MoveAlongSplineState {
-	public var currentPoint: Int = 0;
-	public var currentPointTime: Float = 0.0;
-	public var point: h3d.col.Point;
-	public var tangent: h3d.col.Point;
+	public function save() : Dynamic {
+		var obj = {
+			x: pos.x,
+			y: pos.y,
+			z: pos.z,
+			upX: up.x,
+			upY: up.y,
+			upZ: up.z,
+			tIn: tangentIn,
+			tOut: tangentOut,
+			t: t,
+			length: length,
+		}
 
-	public function reset() {
-		currentPoint = 0;
-		currentPointTime = 0.0;
-		point.set();
-		tangent.set(1.0, 0.0, 0.0);
+		return obj;
 	}
 
-	public function new(?point: h3d.col.Point, ?tangent:  h3d.col.Point) {
-		this.point = point != null ? point : new h3d.col.Point();
-		this.tangent = tangent != null ? tangent : new h3d.col.Point();
-		reset();
+	public function load(obj : Dynamic) {
+		pos = new h3d.col.Point(obj.x, obj.y, obj.z);
+		up = new h3d.col.Point(obj.upX, obj.upY, obj.upZ);
+		tangentIn = new h3d.Vector(obj.tIn.x, obj.tIn.y, obj.tIn.z);
+		tangentOut = new h3d.Vector(obj.tOut.x, obj.tOut.y, obj.tOut.z);
+		t = obj.t;
+		length = obj.length;
 	}
 }
 
-class SplineData {
-	public var length : Float;
-	public var step : Int;
-	public var samples : Array<SplinePointData> = [];
-	public function new() {}
-}
-
-class SplinePointObject extends h3d.scene.Object {
-	override function sync(ctx : h3d.scene.RenderContext)
-	{
-		onSync(ctx);
-		super.sync(ctx);
-	}
+@:allow(hrt.prefab.l3d.SplineMesh)
+class Spline extends hrt.prefab.Object3D {
+	static var OLD_CLASS_POINT = "splinePoint";
 
-	override function onRemove() {
-		super.onRemove();
-		onRemoveDynamic();
-	}
-	public dynamic function onRemoveDynamic() {}
-	public dynamic function onSync(rctx: h3d.scene.RenderContext) {}
-}
+	@:c public var points: Array<SplinePoint> = []; // Local to spline
+	@:c var samples: Array<SplinePoint> = null; // World relative
 
-class SplinePoint extends Object3D {
+	@:s public var loop : Bool = false;
+	@:s public var sampleResolution: Int = 16;
+	@:c public var shape : SplineShape = Linear;
 
-	var pointViewer : h3d.scene.Mesh;
-	var controlPointsViewer : h3d.scene.Graphics;
-	var indexText : h2d.ObjectFollower;
-	var spline(get, default) : Spline;
-	var obj : SplinePointObject;
-	var absPos : h3d.Matrix;
 	#if editor
-	var dirty = true;
+	static var UPDATE_DELAY_MS = 5;
+
+	var editMode = false;
+
+	var selected = -1;
+	var splineUpdateRequested : Bool = false;
+	var interactive : h2d.Interactive;
+	var grid : h3d.scene.Graphics;
+	var mousePos : h3d.Vector;
+	var previewSpline : Spline;
+	var previewPoint : SplinePoint;
+	var draggedObj : { pos: h3d.Vector, type: HandleType };
+	var prevPos : h3d.Vector;
 	#end
 
-	function get_spline() {
-		return parent.to(Spline);
-	}
+	// Spline display
+	@:s public var showSpline : Bool = true;
+	var graphics : h3d.scene.Graphics;
+	var lineThickness : Int = 2;
+	var handlesThickness : Int = 4;
+	var tangentThickness : Int = 1;
+	var lineColor : Int = 0xFF000000;
+	var pointColor : Int = 0xFF69B4;
+	var tangentColor : Int = 0xFF000000;
+
+	// Spline edition
+	var handles : Map<h3d.scene.Graphics, { pos : h3d.col.Point, type: HandleType }> = [];
+	var orphanHandles : Array<h3d.scene.Graphics> = [];
 
-	override function makeInstance() : Void {
-		#if editor
-		local3d = makeObject(shared.current3d);
-		pointViewer = new h3d.scene.Mesh(h3d.prim.Sphere.defaultUnitSphere(), null, local3d.getScene());
-		pointViewer.ignoreParentTransform = true;
-		pointViewer.follow = local3d;
-		pointViewer.followPositionOnly = true;
-		pointViewer.name = "pointViewer";
-		pointViewer.material.setDefaultProps("ui");
-		pointViewer.material.color.set(0,0,1,1);
-		pointViewer.material.mainPass.depthTest = Always;
-
-		controlPointsViewer = new h3d.scene.Graphics(local3d);
-		controlPointsViewer.name = "controlPointsViewer";
-		controlPointsViewer.lineStyle(4, 0xffffff);
-		controlPointsViewer.material.mainPass.setPassName("ui");
-		controlPointsViewer.material.mainPass.depthTest = Always;
-		controlPointsViewer.ignoreParentTransform = false;
-		controlPointsViewer.clear();
-		controlPointsViewer.moveTo(1, 0, 0);
-		controlPointsViewer.lineTo(-1, 0, 0);
-
-		indexText = new h2d.ObjectFollower(pointViewer, shared.current2d.getScene());
-		var t = new h2d.Text(hxd.res.DefaultFont.get(), indexText);
-		t.textColor = 0xff00ff;
-		t.textAlign = Center;
-		t.dropShadow = { dx : 0.5, dy : 0.5, color : 0x202020, alpha : 1.0 };
-		t.setScale(2.5);
-		applyTransform();
-		setViewerVisible(false);
-		obj = new SplinePointObject(local3d);
-		obj.onSync = function(rctx) {
-			var cam = rctx.camera;
-			var gpos = obj.getAbsPos().getPosition();
-			var distToCam = cam.pos.sub(gpos).length();
-			var engine = h3d.Engine.getCurrent();
-			var ratio = 18 / engine.height;
-			pointViewer.setScale(ratio * distToCam * Math.tan(cam.fovY * 0.5 * Math.PI / 180.0));
-			@:privateAccess obj.calcAbsPos();
-		}
-		obj.onRemoveDynamic = function() {
-			indexText.remove();
-			pointViewer.remove();
-		}
-		updateInstance();
-		#end
-	}
+	override function save() : Dynamic {
+		var obj = super.save();
+		obj.points = [ for (p in points) p.save()];
+		obj.shape = shape.getIndex();
 
-	override function applyTransform() {
-		super.applyTransform();
-		#if editor
-		dirty = true;
-		@:privateAccess spline.computeSpline();
-		#end
+		if (samples != null)
+			obj.samples = [ for (s in samples) s.save()];
+		return obj;
 	}
 
-	override function updateInstance(?propName : String) {
-		super.updateInstance(propName);
-		#if editor
-		dirty = true;
+	override function load( obj : Dynamic ) {
+		super.load(obj);
+		points = [];
+
+		// Backwards compatibility
+		var children = Reflect.field(obj, "children");
+		if (children != null) {
+			var i = children.length - 1;
+			while (i >= 0) {
+				if (Reflect.field(children[i], "type") == OLD_CLASS_POINT) {
+					var sp = new SplinePoint();
+					sp.pos = new h3d.col.Point(children[i].x, children[i].y, children[i].z);
+
+					var m = new h3d.Matrix();
+					m.identity();
+					m.scale(children[i].scaleX == null ? 1 : children[i].scaleX, children[i].scaleY == null ? 1 : children[i].scaleY, children[i].scaleZ == null ? 1 : children[i].scaleZ);
+					m.rotate(children[i].rotationX == null ? 0 : children[i].rotationX * Math.PI / 180.0,
+						children[i].rotationY == null ? 0 : children[i].rotationY * Math.PI / 180.0,
+						children[i].rotationZ == null ? 0 : children[i].rotationZ * Math.PI / 180.0);
+
+					sp.tangentIn.transform(m);
+					sp.tangentOut.transform(m);
+
+					points.push(sp);
+					children.remove(children[i]);
+				}
+				i--;
+			}
 
-			if( spline.editor != null ) {
-				spline.editor.setSelected(true);
-				spline.editor.update();
+			if (children.length == 0)
+				Reflect.deleteField(obj, "children");
+			points.reverse();
+		}
+
+		if (obj.points != null) {
+			var sPoints : Array<Dynamic> = obj.points;
+			for (p in sPoints) {
+				var sp = new SplinePoint();
+				sp.load(p);
+				points.push(sp);
 			}
-			for (sp in spline.points) {
-				sp.computeName();
+		}
+
+		if (obj.samples != null) {
+			samples = [];
+			var objSamples : Array<SplinePoint> = obj.samples;
+			for (s in objSamples) {
+				var objSample = new SplinePoint();
+				objSample.load(s);
+				samples.push(objSample);
 			}
-		#end
+		}
+
+		shape = obj.shape == null ? Linear : SplineShape.createByIndex(obj.shape);
 	}
 
-	// TODO(ces) : Restore
+	override function copy(obj : hrt.prefab.Prefab) {
+		super.copy(obj);
 
+		var s : Spline = cast obj;
+		this.load(s.save());
+	}
 
+	override function makeObject(parent3d: h3d.scene.Object) : h3d.scene.Object {
+		#if editor graphics = null; #end
+		return super.makeObject(parent3d);
+	}
 
-	#if editor
+	override function updateInstance(?propName : String ) {
+		super.updateInstance(propName);
 
-	override function editorRemoveInstanceObjects() : Void {
-		shared.editor.queueRebuild(spline);
-		super.editorRemoveInstanceObjects();
-	}
+		#if editor
+		samples = null;
+		drawSpline();
+		#end
 
-	public function computeName() {
-		if( local3d == null ) return;
-		var index = spline.points.indexOf(this);
-		name = "SplinePoint" + index;
-		local3d.name = name;
-		if (indexText != null) {
-			var t = Std.downcast(indexText.getChildAt(0), h2d.Text);
-			t.text = "" + index;
-		}
+		var splineMeshes = findAll(SplineMesh, true);
+		for ( s in splineMeshes )
+			s.updateInstance();
 	}
 
-	override function edit(ctx : hide.prefab.EditContext) {
-		super.edit(ctx);
-		if( spline.editor == null ) {
-			spline.editor = new hide.prefab.SplineEditor(spline, ctx.properties.undo);
-		}
-		spline.editor.editContext = ctx;
-	}
-	override function getHideProps() : hide.prefab.HideProps {
-		return { icon : "arrows-v", name : "SplinePoint", allowParent: function(p) return p.to(Spline) != null, allowChildren: function(s) return false};
-	}
-	#end
 
-	inline public function getPoint() : h3d.col.Point {
-		return getAbsPos(true).getPosition().toPoint();
-	}
+	inline public function getPoint(t: Float, ?out: h3d.col.Point) : h3d.col.Point {
+		if (samples == null)
+			sample( (this.shape == SplineShape.Linear) ? 1 : sampleResolution);
 
-	override function getAbsPos(followRefs: Bool = false)  {
-		var dirty = #if editor dirty #else false #end;
-		if (absPos == null) {
-			absPos = new h3d.Matrix();
-			dirty = true;
-		}
-		if (dirty) {
-			absPos.load(super.getAbsPos(true));
-			#if editor
-			this.dirty = false;
-			#end
+		if (samples.length <= 1)
+			return null;
+
+		t = hxd.Math.clamp(t, 0, 1);
+		var s1 = 0;
+		var sa = 0;
+		var sb = samples.length-1;
+		if( t >= 1 )
+			s1 = sb;
+		else {
+			do {
+				s1 = Math.floor((sa+sb)/2);
+				if (s1 == -1)
+					break;
+				if( samples[s1].t < t )
+					sa = s1+1;
+				else
+					sb = s1-1;
+			} while( !(samples[s1].t <= t && samples[s1+1].t >= t) );
 		}
-		return absPos;
-	}
+		var s2 : Int = s1 + 1;
+		s2 = hxd.Math.iclamp(s2, 0, samples.length - 1);
 
-	public function getTangent() : h3d.col.Point {
-		var tangent = getAbsPos(true).front().toPoint();
-		tangent.scale(-1);
-		return tangent;
-	}
+		if (out == null)
+			out = new h3d.col.Point();
 
-	public function getFirstControlPoint() : h3d.col.Point {
-		var absPos = getAbsPos(true);
-		var right = absPos.front();
-		right.scale(scaleX*scaleY);
-		var pos = new h3d.col.Point(absPos.tx, absPos.ty, absPos.tz);
-		pos = pos.add(right.toPoint());
-		return pos;
-	}
+		if (s1 == -1)
+			return null;
 
-	public function getSecondControlPoint() : h3d.col.Point {
-		var absPos = getAbsPos(true);
-		var left = absPos.front();
-		left.scale(-scaleX*scaleZ);
-		var pos = new h3d.col.Point(absPos.tx, absPos.ty, absPos.tz);
-		pos = pos.add(left.toPoint());
-		return pos;
-	}
+		// End/Beginning of the curve, just return the point
+		if( s1 == s2 )
+			out.load(samples[s1].pos);
 
-	public function setViewerVisible(visible : Bool) {
-		if (pointViewer == null)
-			return;
+		// Linear interpolation between the two samples
+		var segmentLength = samples[s1].pos.distance(samples[s2].pos);
+		if (segmentLength == 0) {
+			out.load(samples[s1].pos);
+		}
+		else {
+			var t = (t - samples[s1].t) / (samples[s2].t - samples[s1].t);
+			out.lerp(samples[s1].pos, samples[s2].pos, t);
+		}
 
-		pointViewer.visible = visible;
-		indexText.visible = visible;
-		controlPointsViewer.visible = visible;
-	}
-	public function setColor( color : Int ) {
-		controlPointsViewer.setColor(color);
-		pointViewer.material.color.setColor(color);
+		return out;
 	}
 
-	static var _ = Prefab.register("splinePoint", SplinePoint);
-}
+	inline public function getNearestPointProgressOnSpline(p: h3d.col.Point) : Float {
+		if (samples == null)
+			sample((this.shape == SplineShape.Linear) ? 1 : sampleResolution);
 
-class Spline extends Object3D {
+		var closestSq = hxd.Math.POSITIVE_INFINITY;
+		var closestT = 0.;
+		var c = p;
+		for (i in 0...samples.length-1) {
+			var s1 = samples[i];
+			var s2 = samples[i+1];
+			var a = s1.pos;
+			var b = s2.pos;
 
-	public var points(get, null) : Array<SplinePoint> = [];
-	function get_points() {
-		var recompute = false;
-		//in editor spline can change
-		#if editor
-		for (i in 0...children.length) {
-			if (children[i].to(SplinePoint) != points[i]) {
-				recompute = true;
-				break;
+			var d = inline new h3d.col.Point();
+
+			var ab = inline b.sub(a);
+			var ca = inline c.sub(a);
+			var t = inline ca.dot(ab);
+
+			if (t <= 0.0) {
+				t = 0.0;
+				d.load(a);
+			} else {
+				var denom = ab.dot(ab);
+				if (t >= denom) {
+					t = 1.0;
+					d.load(b);
+				} else {
+					t /= denom;
+					d.load(inline a.add(inline ab.scaled(t)));
+				}
 			}
-		}
-		#end
-		// spline never change at runtime, only compute at the beginning
-		#if !editor
-		if (points.length == 0)
-			recompute = true;
-		#end
-		if (recompute) {
-			points = [];
-			for (c in children) {
-				var sp = c.to(SplinePoint);
-				if (sp != null) points.push(sp);
+
+			var cd = inline d.sub(c);
+			var lenSq = cd.lengthSq();
+			if (lenSq < closestSq) {
+				closestSq = lenSq;
+				var tLength = s2.t - s1.t;
+				closestT = s1.t + t * tLength;
 			}
 		}
-		return points;
+		return closestT;
 	}
 
-	@:c public var shape : CurveShape = Linear;
+	public function getLength() {
+		if (samples == null)
+			sample( (this.shape == SplineShape.Linear) ? 1 : sampleResolution);
+		if (samples == null || samples.length == 0)
+			return 0.0;
+		return samples[samples.length - 1].length;
+	}
 
-	var data : SplineData;
-	@:s var step : Int = 1;
 
-	// Save/Load the curve as an array of local transform
-	@:c public var pointsData : Array<h3d.Matrix> = [];
+	public function addPoint(?idx : Int, ?point : SplinePoint) {
+		var newPoint = point;
+		if (newPoint == null)
+			newPoint = new SplinePoint();
 
-	// Graphic
-	@:s public var showSpline : Bool = true;
-	public var lineGraphics : h3d.scene.Graphics;
-	@:s public var lineThickness : Int = 4;
-	@:s public var color : Int = 0xFFFFFFFF;
-	@:s public var loop : Bool = false;
+		if (idx == null)
+			idx = points.length;
 
-	#if editor
-	public var editor : hide.prefab.SplineEditor;
-	public var loading : Bool = false;
-	#end
-	public var wasEdited = false;
+		points.insert(idx, newPoint);
+		this.updateInstance();
+	}
 
-	override function save() : Dynamic {
-		var obj = super.save();
+	public function removePoint(?idx : Int) {
+		if (points.length == 0 || points.length - 1 < idx)
+			return;
 
-		obj.shape = shape.getIndex();
-		return obj;
+		var idxToDelete = idx == null ? points.length - 1 : idx;
+		points.remove(points[idxToDelete]);
+		this.updateInstance();
 	}
 
-	override function load( obj : Dynamic ) {
-		super.load(obj);
-
-		// Backward compatibility
-		pointsData = [];
-		if( obj.points != null ) {
-			var points : Array<Dynamic> = obj.points;
-			for( p in points ) {
-				var m = new h3d.Matrix();
-				m.loadValues(p);
-				pointsData.push(m);
-			}
-		}
-		shape = obj.shape == null ? Linear : CurveShape.createByIndex(obj.shape);
+	public function localToGlobal(point : h3d.col.Point) {
+		return point.transformed(getAbsPos(true));
 	}
 
-	override function copy(obj : Prefab) {
-		super.copy(obj);
-		var p : Spline = cast obj;
-		this.shape = p.shape;
+	public function globalToLocal(point : h3d.col.Point) {
+		return point.transformed(getAbsPos(true).getInverse());
 	}
 
-	override function makeInstance() : Void {
-		local3d = makeObject(shared.current3d);
-		local3d.name = name;
+	public function localToGlobalSplinePoint(sp : SplinePoint) {
+		if (sp == null)
+			return null;
 
-		// Backward compatibility
-		for( pd in pointsData ) {
-			var sp = new SplinePoint(this, null);
-			sp.setTransform(pd);
+		var out = new SplinePoint(sp.pos, sp.up, sp.tangentIn, sp.tangentOut);
+		out.pos = localToGlobal(out.pos);
+		return out;
+	}
+
+	public function drawSpline() {
+		if( !showSpline || points == null || points.length <= 1) {
+			if( graphics != null ) {
+				graphics.remove();
+				graphics = null;
+			}
+			return;
 		}
 
-		if( points.length == 0 )
-			new SplinePoint(this, null);
+		if( graphics == null ) {
+			graphics = new h3d.scene.Graphics(local3d);
+			graphics.lineStyle(lineThickness, lineColor);
+			graphics.name = "lineGraphics";
+			graphics.material.mainPass.setPassName("overlay");
+			graphics.material.mainPass.depth(false, LessEqual);
+			graphics.ignoreParentTransform = false;
+		}
 
-		#if editor
-		lineGraphics = null;
-		loading = true;
-		#end
-	}
+		graphics.lineStyle(lineThickness, lineColor);
+		graphics.clear();
 
-	override function postMakeInstance() : Void {
-		#if editor
-		loading = false;
-		#end
-		updateInstance();
+		var precision = 100;
+		var b = true;
+		for (idx in 0...(precision)) {
+			var point = getPoint(idx/precision);
+			if (point == null)
+				continue;
+			point = point.transformed(getAbsPos(true).getInverse());
+			b ? graphics.moveTo(point.x, point.y, point.z) : graphics.lineTo(point.x, point.y, point.z);
+			b = false;
+		}
 	}
 
-	override function updateInstance(?propName : String ) {
-		#if editor
-		if (loading)
-			return;
-		#end
-		super.updateInstance(propName);
-		#if editor
-		if( editor != null )
-			editor.update(propName);
-		#end
+	public function drawHandle(point: SplinePoint) {
+		var precision = 100;
 
-		#if editor loading = true; #end // Avoid calling computeSpline from inside splinePoint.updateInstance()
-		for (sp in points)
-			sp.updateInstance();
-		#if editor loading = false; #end
+		function getPointOnCircle(center : h3d.Vector, radius : Float, t : Float) {
+			var angle = t * 2 * Math.PI;
+			var x = Math.sin(angle) * radius;
+			var y = Math.cos(angle) * radius;
 
-		computeSpline();
-	}
-
-	// Return an interpolation of two samples at t, 0 <= t <= 1
-	public function getPointAt( t : Float, ?pos: h3d.col.Point, ?tangent: h3d.col.Point ) : h3d.col.Point {
-		if( data == null )
-			computeSplineData();
+			return new h3d.Vector(center.x + x, center.y + y, center.z);
+		}
 
-		t = hxd.Math.clamp(t);
-		var s1 = 0;
-		var sa = 0;
-		var sb = data.samples.length-1;
-		if( t >= 1 )
-			s1 = sb;
-		else {
-			do {
-				s1 = Math.floor((sa+sb)/2);
-				if( data.samples[s1].t < t )
-					sa = s1+1;
+		function drawCircle(center : h3d.Vector, radius : Float, g : h3d.scene.Graphics) {
+			for (idx in 0...precision) {
+				var pos = getPointOnCircle(center, radius, 1.0 / precision * idx);
+				if (idx == 0)
+					g.moveTo(pos.x, pos.y, pos.z);
 				else
-					sb = s1-1;
-			} while( !(data.samples[s1].t <= t && data.samples[s1+1].t > t) );
+					g.lineTo(pos.x, pos.y, pos.z);
+			}
 		}
-		var s2 : Int = s1 + 1;
-		s2 = hxd.Math.iclamp(s2, 0, data.samples.length - 1);
-
-		if(pos == null)
-			pos = new h3d.col.Point();
 
-		// End/Beginning of the curve, just return the point
-		if( s1 == s2 ) {
-			pos.load(data.samples[s1].pos);
-			if(tangent != null)
-				tangent.load(data.samples[s1].tangent);
-		}
-		// Linear interpolation between the two samples
-		else {
-			var segmentLength = data.samples[s1].pos.distance(data.samples[s2].pos);
-			if (segmentLength == 0) {
-				pos.load(data.samples[s1].pos);
-				if(tangent != null)
-					tangent.load(data.samples[s1].tangent);
-			}
-			else {
-				var t = (t - data.samples[s1].t) / (data.samples[s2].t - data.samples[s1].t);
-				pos.lerp(data.samples[s1].pos, data.samples[s2].pos, t);
-				if(tangent != null)
-					tangent.lerp(data.samples[s1].tangent, data.samples[s2].tangent, t);
+		function getGraphicsHandle(pos: h3d.col.Point, type: HandleType) {
+			var g : h3d.scene.Graphics = null;
+			for (handle => obj in handles)
+				if (pos == obj.pos)
+					g = handle;
+
+			if (g == null) {
+				g = new h3d.scene.Graphics(local3d);
+				g.lineStyle(lineThickness, lineColor);
+				g.name = "handle";
+				g.material.mainPass.setPassName("overlay");
+				g.material.mainPass.depth(false, LessEqual);
+				g.ignoreParentTransform = false;
+				handles.set(g, { pos: pos, type: type });
 			}
 
+			g.clear();
+			return g;
 		}
-		return pos;
-	}
 
+		function getGraphics() {
+			var g = new h3d.scene.Graphics(local3d);
+			g.lineStyle(lineThickness, lineColor);
+			g.name = "orphan_handle";
+			g.material.mainPass.setPassName("overlay");
+			g.material.mainPass.depth(false, LessEqual);
+			g.ignoreParentTransform = false;
+			orphanHandles.push(g);
+			return g;
+		}
 
+		var center = point.pos.clone();
 
-	/* Move a point a given distance on the spline.
-	*/
-	public function moveAlongSpline(distance: Float, ?state: MoveAlongSplineState) : MoveAlongSplineState {
-		if( data == null )
-			computeSplineData();
+		var pointHandle = getGraphicsHandle(point.pos, Point);
+		pointHandle.lineStyle(handlesThickness, pointColor);
+		pointHandle.moveTo(0, 0, 0);
+		pointHandle.lineTo(0, 0, 0.5);
 
-		if (state == null) {
-			state = new MoveAlongSplineState();
-		}
+		var b = true;
+		var radius = 0.3;
+		var i = precision;
+		while (i > 0) {
+			var t = 1.0 / precision * i;
+			var center = new h3d.Vector(0, 0, 0);
+
+			if (t == 0.5) {
+				var direction = new h3d.Vector(1, 0, 0);
+				pointHandle.lineTo(direction.x, direction.y, direction.z);
+
+				var end = getPointOnCircle(center, radius, 1);
+				pointHandle.lineTo(end.x, end.y, end.z);
+				break;
+			}
 
-		if (data.samples.length <= 0) {
-			return state;
+			var pos = getPointOnCircle(center, radius, 1.0 / precision * i);
+			b ? pointHandle.moveTo(pos.x, pos.y, pos.z) : pointHandle.lineTo(pos.x, pos.y, pos.z);
+			b = false;
+			i--;
 		}
 
-		// if the spline is too small return just the state (we'll just sping inside the while loop for nothing)
-		if (data.length < distance / 10.0) {
-			state.point.load(data.samples[0].pos);
-			state.tangent.load(data.samples[0].tangent);
-			return state;
-		}
+		pointHandle.setDirection(point.tangentOut);
+		pointHandle.setPosition(center.x, center.y, center.z);
 
-		var dir = distance > 0.0 ? 1 : -1;
-		distance = Math.abs(distance);
+		if (this.shape == SplineShape.Linear)
+			return;
 
-		var numPoints = data.samples.length;
-		while (distance > 0.0) {
-			var p1i = state.currentPoint;
-			var p2i = (state.currentPoint + 1) % numPoints;
+		function drawTangent(pos : h3d.col.Point, tangent: h3d.Vector, graphics : h3d.scene.Graphics) {
+			var g = getGraphics();
+			g.lineStyle(tangentThickness, tangentColor);
+			g.moveTo(0, 0, 0);
+			g.lineTo(tangent.x, tangent.y, tangent.z);
+			g.setPosition(pos.x, pos.y, pos.z);
 
-			var p1 = data.samples[p1i];
-			var p2 = data.samples[p2i];
+			graphics.lineStyle(handlesThickness, pointColor);
+			drawCircle(new h3d.Vector(0,0,0), 0.2, graphics);
 
-			var segmentLength = p2.pos.distance(p1.pos);
-			var curDist = state.currentPointTime * segmentLength;
+			var abs = pos + tangent;
+			graphics.setPosition(abs.x, abs.y, abs.z);
+		}
 
-			var nextDist = curDist + dir*distance;
-			var remainder = dir > 0 ? segmentLength - nextDist : nextDist;
+		var tInGraphics = getGraphicsHandle(point.tangentIn, TangentIn(point));
+		drawTangent(point.pos, point.tangentIn, tInGraphics);
 
-			// If we moved past the current point
-			if (remainder < 0.0) {
-				distance += remainder;
-				state.currentPointTime = dir > 0.0 ? 0.0 : 1.0;
-				state.currentPoint = (state.currentPoint + dir + (numPoints-1)) % (numPoints-1);
-			} else {
-				state.currentPointTime = segmentLength > 0.0 ? nextDist / segmentLength : 0.0;
+		var tOutGraphics = getGraphicsHandle(point.tangentOut, TangentOut(point));
+		drawTangent(point.pos, point.tangentOut, tOutGraphics);
+	}
 
-				state.point.lerp(p1.pos, p2.pos, state.currentPointTime);
-				state.tangent.lerp(p1.tangent, p2.tangent, state.currentPointTime);
-				state.tangent.scale(dir);
-				break;
-			}
+	public function clearHandles(onlyOrphans = false) {
+		while(orphanHandles.length > 0) {
+			var h = orphanHandles[orphanHandles.length - 1];
+			h.clear();
+			orphanHandles.remove(h);
+			h.remove();
 		}
-		return state;
-	}
 
-	// Return the euclidean distance between the two points
-	inline function getMaxLengthBetween( p1 : SplinePoint, p2 : SplinePoint) : Float {
-		switch shape {
-			case Linear: return p1.getPoint().distance(p2.getPoint());
-			case Quadratic: return p1.getPoint().distance(p1.getSecondControlPoint()) + p1.getFirstControlPoint().distance(p2.getPoint());
-			case Cubic: return p1.getPoint().distance(p1.getSecondControlPoint()) + p1.getFirstControlPoint().distance(p2.getFirstControlPoint()) + p2.getFirstControlPoint().distance(p2.getPoint());
+		if (onlyOrphans)
+			return;
+
+		for (handle => obj in handles) {
+			handle.clear();
+			handles.remove(handle);
+			handle.remove();
 		}
 	}
 
-	// Return the sum of the euclidean distances between each control points
-	inline function getMinLengthBetween( p1 : SplinePoint, p2 : SplinePoint) : Float {
-		return p1.getPoint().distance(p2.getPoint());
-	}
+	public function getLocalCollider() : h3d.col.Collider {
+		if (points == null || points.length <= 1)
+			return new h3d.col.Bounds();
 
-	// Return the sum of the euclidean distances between each samples
-	public function getLength() {
-		if( data == null )
-			computeSplineData();
-		return data.length;
-	}
+		var colliders : Array<h3d.col.Collider> = [];
 
-	// Sample the spline with the step
-	function computeSplineData() {
+		function getOBCollider(p1: h3d.Vector, p2: h3d.Vector) {
+			var col = new h3d.col.OrientedBounds();
 
-		var sd = new SplineData();
-		data = sd;
+			var direction = p2 - p1;
+			var q = new h3d.Quat();
+			q.initDirection(direction);
 
-		if( step <= 0 )
-			return;
+			var m = new h3d.Matrix();
+			m.initScale(direction.length() + 0.5, 1, 1);
+			m = m.multiplied(q.toMatrix());
+			m.setPosition(p1 + direction * 0.5);
 
-		if( points == null || points.length <= 1 )
-			return;
+			col.setMatrix(m);
+			return col;
+		}
+
+		if (shape == Linear) {
+			for (idx in 0...points.length - 1)
+				colliders.push(getOBCollider(points[idx].pos, points[idx + 1].pos));
+		}
+		else {
+			var samplePerCollider = 4;
+			var idx = 0;
+			while (idx < samples.length) {
+				var p1 = samples[idx];
+				var p2 = idx + samplePerCollider < samples.length ? samples[idx + samplePerCollider] : samples[samples.length - 1];
+				colliders.push(getOBCollider(globalToLocal(p1.pos), globalToLocal(p2.pos)));
+				idx += samplePerCollider;
+			}
+		}
+
+		return new h3d.col.Collider.GroupCollider(colliders);
+	}
+
+	function sample(numPts: Int) {
+		samples = [];
+
+		if( numPts <= 0 ) return;
+		if( points == null || points.length <= 1 ) return;
 
-		// Sample the spline
-		var samples : Array<SplinePointData> = [{ pos : points[0].getPoint(), tangent : points[0].getTangent(), prev : points[0], next : points[1] }];
+		samples.push(localToGlobalSplinePoint(new SplinePoint(points[0].pos, points[0].up, points[0].tangentIn, points[0].tangentOut)));
 		var maxI = loop ? points.length : points.length - 1;
-		var curP = points[0];
-		var nextP = points[1];
+		var curP = localToGlobalSplinePoint(points[0]);
+		var nextP = localToGlobalSplinePoint(points[1]);
+		var stride = 1./numPts;
 		for (i in 1...maxI + 1) {
-			var t = 0.;
-			while (t <= 1.) {
+			for (i in 1...numPts-1) {
+				var t = stride * i;
 				var p = getPointBetween(t, curP, nextP);
-				if (p.distance(samples[samples.length - 1].pos) >= 1./step)
-					samples.insert(samples.length, { pos : p, tangent : getTangentBetween(t, curP, nextP), prev : curP, next : nextP });
-				t += 1./step;
+				if (p.distance(samples[samples.length - 1].pos) >= 1./numPts) {
+					var newP = new SplinePoint();
+					newP.pos = p;
+					var tangent = getTangentBetween(t, curP, nextP);
+					newP.tangentIn = -1 * tangent;
+					newP.tangentOut = tangent;
+					samples.push(newP);
+				}
+				t += stride;
 			}
-			if (nextP.getPoint().distance(samples[samples.length - 1].pos) >= 1./step)
-				samples.insert(samples.length, { pos : nextP.getPoint(), tangent : nextP.getTangent(), prev : curP, next : nextP });
-			curP = points[i];
-			nextP = points[(i + 1) % points.length];
 
+			samples.push(new SplinePoint(nextP.pos, nextP.up, nextP.tangentIn, nextP.tangentOut));
+
+			curP = localToGlobalSplinePoint(points[i]);
+			nextP = localToGlobalSplinePoint(points[(i + 1) % points.length]);
 		}
-		sd.samples = samples;
 
 		// Compute the average length of the spline
-		var lengthSum = 0.0;
-		for( i in 0 ... samples.length - 1 ) {
-			lengthSum += samples[i].pos.distance(samples[i+1].pos);
-		}
+		var length = 0.0;
+		for( i in 0 ... samples.length - 1 )
+			length += samples[i].pos.distance(samples[i+1].pos);
+
 		var l = 0.0;
 		for( i in 0 ... samples.length - 1 ) {
-			samples[i].t = l/lengthSum;
+			samples[i].t = l/length;
+			samples[i].length = length;
 			l += samples[i].pos.distance(samples[i+1].pos);
 		}
 		samples[samples.length - 1].t = 1;
-		sd.length = lengthSum;
+		samples[samples.length - 1].length = length;
 	}
 
-	// Return the closest spline point on the spline from p
-	function getClosestSplinePoint( p : h3d.col.Point ) : SplinePoint {
-		var minDist = -1.0;
-		var curPt : SplinePoint = null;
-		for( sp in points ) {
-			var dist = p.distance(sp.getPoint());
-			if( dist < minDist || minDist == -1 ) {
-				minDist = dist;
-				curPt = sp;
-			}
+
+	// -- Spline maths -- //
+	inline function getPointBetween( t : Float, p1 : SplinePoint, p2 : SplinePoint ) : h3d.col.Point {
+		return switch (shape) {
+			case Linear: getLinearBezierPoint( t, p1.pos, p2.pos );
+			case Quadratic: getQuadraticBezierPoint( t, p1.pos, p1.tangentOut + p1.pos, p2.pos );
+			case Cubic: getCubicBezierPoint( t, p1.pos, p1.tangentOut + p1.pos, p2.tangentIn + p2.pos, p2.pos );
 		}
-		return curPt;
 	}
 
-	public function getSplinePointDataAt( t : Float) : SplinePointData {
-		if( data == null )
-			computeSplineData();
+	inline function getLinearBezierPoint( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point ) : h3d.col.Point {
+		// Linear Interpolation : p(t) = p0 + (p1 - p0) * t
+		return p0.add((p1.sub(p0).scaled(t)));
+	}
 
-		var minDist = -1.0;
-		var result : SplinePointData = null;
-		for( s in data.samples ) {
-			var dist = Math.abs(s.t - t);
-			if( dist < minDist || minDist == -1 ) {
-				minDist = dist;
-				result = s;
-			}
+	inline function getQuadraticBezierPoint( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point, p2 : h3d.col.Point) : h3d.col.Point {
+		// Quadratic Interpolation : p(t) = p0 * (1 - t)² + p1 * t * 2 * (1 - t) + p2 * t²
+		return p0.scaled((1 - t) * (1 - t)).add(p1.scaled(t * 2 * (1 - t))).add(p2.scaled(t * t));
+	}
+
+	inline function getCubicBezierPoint( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point, p2 : h3d.col.Point, p3 : h3d.col.Point) : h3d.col.Point {
+		// Cubic Interpolation : p(t) = p0 * (1 - t)³ + p1 * t * 3 * (1 - t)² + p2 * t² * 3 * (1 - t) + p3 * t³
+		return p0.scaled((1 - t) * (1 - t) * (1 - t)).add(p1.scaled(t * 3 * (1 - t) * (1 - t))).add(p2.scaled(t * t * 3 * (1 - t))).add(p3.scaled(t * t * t));
+	}
+
+	inline function getTangentBetween( t : Float, p1 : SplinePoint, p2 : SplinePoint ) : h3d.col.Point {
+		return switch (shape) {
+			case Linear: getLinearBezierTangent( t, p1.pos, p2.pos );
+			case Quadratic: getQuadraticBezierTangent( t, p1.pos, p1.tangentOut + p1.pos, p2.pos );
+			case Cubic: getCubicBezierTangent( t, p1.pos, p1.tangentOut + p1.pos, p2.tangentIn + p2.pos, p2.pos );
 		}
-		return result;
 	}
 
-	inline function getClosestPointInfoOnSpline( p : h3d.col.Point, ?out: h3d.col.Point): Float {
-		if( data == null )
-			computeSplineData();
+	inline function getLinearBezierTangent( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point ) : h3d.col.Point {
+		// Linear Interpolation : p'(t) = (p1 - p0)
+		return p1.sub(p0).normalized();
+	}
 
-		var closestSq = hxd.Math.POSITIVE_INFINITY;
+	inline function getQuadraticBezierTangent( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point, p2 : h3d.col.Point) : h3d.col.Point {
+		// Quadratic Interpolation : p'(t) = 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1)
+		return p1.sub(p0).scaled(2 * (1 - t)).add(p2.sub(p1).scaled(2 * t)).normalized();
+	}
 
-		var closestT = 0.;
+	inline function getCubicBezierTangent( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point, p2 : h3d.col.Point, p3 : h3d.col.Point) : h3d.col.Point {
+		// Cubic Interpolation : p'(t) = 3 * (1 - t)² * (p1 - p0) + 6 * (1 - t) * t * (p2 - p1) + 3 * t² * (p3 - p2)
+		return p1.sub(p0).scaled(3 * (1 - t) * (1 - t)).add(p2.sub(p1).scaled(6 * (1 - t) * t)).add(p3.sub(p2).scaled(3 * t * t)).normalized();
+	}
 
-		var c = p;
 
-		for (i in 0...data.samples.length-1) {
-			var s1 = data.samples[i];
-			var s2 = data.samples[i+1];
-			var a = s1.pos;
-			var b = s2.pos;
+	#if editor
+	override function edit(ctx : hide.prefab.EditContext) {
+		super.edit(ctx);
 
-			var d = inline new h3d.col.Point();
+		var props = new hide.Element('
+			<div class="group" name="Spline">
+				<dl>
+					<dt>Type</dt>
+					<dd>
+						<select id="shape">
+							<option ${this.shape == SplineShape.Linear ? "selected" : ""} value="0">Linear</option>
+							<option ${this.shape == SplineShape.Quadratic ? "selected" : ""} value="1">Quadratic</option>
+							<option ${this.shape == SplineShape.Cubic ? "selected" : ""} value="2">Cubic</option>
+						</select>
+					</dd>
+				</dl>
+				<div align="center">
+					<input type="button" value="Edit Mode : Disabled" class="editModeButton" />
+				</div>
+				<div class="group points-inspector">
+					<div class="buttons">
+						<div class="icon ico ico-plus" id="add"></div>
+						<div class="icon ico ico-minus" id="remove"></div>
+					</div>
+					<div class="points-container">
+					</div>
+				</div>
+			</div>
+		');
+
+		var editModeButton = props.find(".editModeButton");
+		editModeButton.click(function(_) {
+			if (!enabled) return;
+			editMode = !editMode;
+			editModeButton.val(editMode ? "Edit Mode : Enabled" : "Edit Mode : Disabled");
+			editModeButton.toggleClass("editModeEnabled", editMode);
+			setSelected(true);
+			clearInteractive();
+			if(editMode)
+				createInteractive(ctx);
+		});
+
+		var selShape = props.find("#shape");
+		selShape.change((e) -> {
+			var oldV = this.shape;
+			var v = Std.parseInt(selShape.val());
+			var newV = haxe.EnumTools.createByIndex(SplineShape, v);
+			this.shape = newV;
+			this.updateInstance();
+			refreshHandles();
+			ctx.properties.undo.change(Custom(function(undo) {
+				this.shape = undo ? oldV : newV;
+				selShape.val(this.shape.getIndex());
+				this.updateInstance();
+				refreshHandles();
+			}));
+		});
+
+		props.find("#add").first().click((e) -> {
+			var newP = new SplinePoint();
+			if (points.length > 0)
+				newP.pos = points[points.length -1].pos;
+
+			newP.pos += new h3d.col.Point(1, 0, 0);
+			this.addPoint(null, newP);
+			refreshPointList(ctx);
+			updateSpline(this);
+		});
+
+		props.find("#remove").first().click((e) -> {
+			this.removePoint();
+			refreshPointList(ctx);
+			updateSpline(this);
+		});
+
+		ctx.properties.add(props, null, null);
+
+		refreshPointList(ctx);
+	}
 
-			var ab = inline b.sub(a);
-			var ca = inline c.sub(a);
-			var t = inline ca.dot(ab);
+	override function getHideProps() : hide.prefab.HideProps {
+		return { icon : "arrows-v", name : "Spline" };
+	}
 
-			if (t <= 0.0) {
-				t = 0.0;
-				d.load(a);
-			} else {
-				var denom = ab.dot(ab);
-				if (t >= denom) {
-					t = 1.0;
-					d.load(b);
-				} else {
-					t /= denom;
-					d.load(inline a.add(inline ab.scaled(t)));
-				}
-			}
+	override function setSelected(b: Bool) : Bool {
+		if (!b)
+			clearInteractive();
 
-			var cd = inline d.sub(c);
-			var lenSq = cd.lengthSq();
-			if (lenSq < closestSq) {
-				closestSq = lenSq;
-				var tLength = s2.t - s1.t;
-				if(out != null)
-					out.load(d);
-				closestT = s1.t + t * tLength;
+		return b;
+	}
+
+	function refreshPointList(ctx: hide.prefab.EditContext) {
+		var pointsContainer = ctx.properties.element.find(".points-container");
+		pointsContainer.empty();
+
+		for (pIdx => p in this.points) {
+			var pos = points[pIdx].pos;
+			var el = new hide.Element('<div class="point folded">
+				<div class="header">
+					<div class="icon ico ico-chevron-right"></div>
+					<p>Point [${pIdx}]</p>
+				</div>
+				<div class="body">
+					<dt>Position</dt><dd><input class="pos-x" type="number" value="${pos.x}"/><input class="pos-y" type="number" value="${pos.y}"/><input type="number" class="pos-z" value="${pos.z}"/></dd>
+				</div>
+			</div>').appendTo(pointsContainer);
+
+			el.find(".header").click((e) -> {
+				pointsContainer.find('.point').toggleClass("selected", false);
+				el.toggleClass("selected", true);
+				selected = pIdx;
+			});
+
+			var foldBtn = el.find(".icon");
+			foldBtn.click((e) -> {
+				var folded = !el.hasClass('folded');
+				el.toggleClass("folded", folded);
+				foldBtn.toggleClass("ico-chevron-down", !folded);
+				foldBtn.toggleClass("ico-chevron-right", folded);
+			});
+
+			var px = el.find(".pos-x");
+			var py = el.find(".pos-y");
+			var pz = el.find(".pos-z");
+			function onPosChange(oldPos : h3d.Vector, newPos : h3d.Vector) {
+				points[pIdx].pos.load(newPos);
+				this.updateInstance();
+				refreshHandles();
+				ctx.properties.undo.change(Custom(function(undo) {
+					var v = undo ? oldPos : newPos;
+					points[pIdx].pos.load(v);
+					pos = v;
+					this.updateInstance();
+					refreshHandles();
+					px.val(v.x);
+					py.val(v.y);
+					pz.val(v.z);
+				}));
 			}
+
+			px.change((e) -> {
+				var newPos = pos.clone();
+				newPos.x = Std.parseFloat(px.val());
+				onPosChange(pos, newPos);
+				pos = newPos;
+			});
+
+			py.change((e) -> {
+				var newPos = pos.clone();
+				newPos.y = Std.parseFloat(py.val());
+				onPosChange(pos, newPos);
+				pos = newPos;
+			});
+
+			pz.change((e) -> {
+				var newPos = pos.clone();
+				newPos.z = Std.parseFloat(pz.val());
+				onPosChange(pos, newPos);
+				pos = newPos;
+			});
 		}
-		return closestT;
 	}
 
-	public function getPointProgressOnSpline( p : h3d.col.Point ): Float {
-		return getClosestPointInfoOnSpline(p);
+	function refreshHandles() {
+		clearHandles();
+		for (p in points)
+			drawHandle(p);
 	}
 
-	function getClosestPointOnSpline(p : h3d.col.Point, ?out: h3d.col.Point) : h3d.col.Point {
-		var out = out ?? new h3d.col.Point();
-		getClosestPointInfoOnSpline(p, out);
-		return out;
-	}
+	function createInteractive(ctx : hide.prefab.EditContext) {
+		var s2d = shared.root2d.getScene();
+		var s3d = shared.root3d.getScene();
+		var cam = s3d.camera;
+
+		clearInteractive();
+		for (p in points)
+			drawHandle(p);
+
+		interactive = new h2d.Interactive(s2d.width, s2d.height, s2d);
+		interactive.propagateEvents = true;
+		interactive.cancelEvents = false;
+
+		interactive.onKeyDown = function(e) {
+			if (e.keyCode == hxd.Key.ALT) {
+				if (previewPoint == null) {
+					previewSpline = cast this.clone(this.parent, this.shared).make();
+					previewPoint = new SplinePoint();
+					previewSpline.addPoint(null, previewPoint);
+
+					this.local3d.visible = false;
+					if (mousePos == null)
+						mousePos = new h3d.Vector(e.relX, e.relY);
+
+					var splinePos = local3d.getAbsPos().getPosition();
+					var engine = ctx.scene.engine;
+					var width = engine.width;
+					var height = engine.height;
+					function updatePreview() {
+						if (previewSpline == null)
+							return;
+
+						var nearestSample = getClosestSplinePointFromMouse(mousePos, cam);
+						var nearestSamplePos = nearestSample == null ? splinePos : nearestSample.pos;
+						var plane = h3d.col.Plane.fromNormalPoint(getCameraClosestEulerPlaneNormal(cam), nearestSamplePos);
+
+						var r = getRay(mousePos.x, mousePos.y, cam, s2d);
+						var worldMousePos = r.intersect(plane);
+
+						// Find nearest point on spline
+						var nearestSampleScreenPos = cam.projectInline(nearestSamplePos.x, nearestSamplePos.y, nearestSamplePos.z, width, height);
+						nearestSampleScreenPos.z = 0;
+						if (nearestSampleScreenPos.distance(mousePos) < 20) {
+							worldMousePos = nearestSamplePos; // If user's mouse is near the spline, snap the new point on spline
+						}
+						else {
+							var addToEnd = false;
+							if (points != null && points.length > 0) {
+								var startP = points[0];
+								var endP = points[points.length - 1];
+								addToEnd = localToGlobal(startP.pos).distanceSq(nearestSamplePos) > localToGlobal(endP.pos).distanceSq(nearestSamplePos);
+								var previousP = addToEnd ? endP : startP;
+								previousP = localToGlobalSplinePoint(previousP);
+								nearestSamplePos = previousP.pos;
+							}
+
+							plane = h3d.col.Plane.fromNormalPoint(getCameraClosestEulerPlaneNormal(cam), worldMousePos);
+							drawGrid(nearestSamplePos, plane.getNormal(), cam, s3d);
+							previewSpline.points.remove(previewPoint);
+							previewSpline.addPoint(addToEnd ? previewSpline.points.length : 0, previewPoint);
+						}
+
+						worldMousePos = worldMousePos.transformed(getAbsPos(true).getInverse());
+						previewPoint.pos = worldMousePos;
+						updateSpline(previewSpline);
+					}
+
+					updatePreview();
+					@:privateAccess s3d.window.mouseMode = Relative(function(e) {
+						mousePos.x += e.relX;
+						mousePos.y += e.relY;
+						updatePreview();
+					}, false);
+				}
 
-	// Return the closest point on the spline from p
-	function getClosestPoint( p : h3d.col.Point ) : SplinePointData {
+				e.propagate = false;
+			}
+		}
 
-		if( data == null )
-			computeSplineData();
+		interactive.onKeyUp = function(e) {
+			if (e.keyCode == hxd.Key.ALT && previewPoint != null) {
+				local3d.visible = true;
+				clearGrid();
+				clearPreviewSpline();
+				mousePos = null;
+				hxd.Window.getInstance().mouseMode = Absolute;
+			}
+		}
 
-		var minDist = -1.0;
-		var result : SplinePointData = null;
-		for( s in data.samples ) {
-			var dist = s.pos.distanceSq(p);
-			if( dist < minDist || minDist == -1 ) {
-				minDist = dist;
-				result = s;
+		interactive.onClick = function(e) {
+			if (previewPoint != null) {
+				var p = previewPoint;
+				var pidx = previewSpline.points.indexOf(previewPoint);
+				addPoint(pidx, previewPoint);
+
+				ctx.properties.undo.change(Custom(function(undo) {
+					if (undo) {
+						removePoint(pidx);
+					}
+					else {
+						addPoint(pidx, p);
+					}
+
+					updateSpline(this);
+					refreshPointList(ctx);
+				}));
+
+				refreshPointList(ctx);
+				updateSpline(this);
+				clearPreviewSpline();
 			}
+
+			e.propagate = false;
 		}
-		return result;
-	}
 
-	// Return the point on the curve between p1 and p2 at t, 0 <= t <= 1
-	inline function getPointBetween( t : Float, p1 : SplinePoint, p2 : SplinePoint ) : h3d.col.Point {
-		return switch (shape) {
-			case Linear: getLinearBezierPoint( t, p1.getPoint(), p2.getPoint() );
-			case Quadratic: getQuadraticBezierPoint( t, p1.getPoint(), p1.getSecondControlPoint(), p2.getPoint() );
-			case Cubic: getCubicBezierPoint( t, p1.getPoint(), p1.getSecondControlPoint(), p2.getFirstControlPoint(), p2.getPoint() );
+		interactive.onPush = function(e) {
+			var ray = getRay(e.relX, e.relY, cam, s2d);
+			for (handle => obj in handles) {
+				var b = handle.getBounds();
+				if (b.rayIntersection(ray, true) >= 0.0) {
+					mousePos = new h3d.Vector(e.relX, e.relY, 0);
+					var plane = h3d.col.Plane.fromNormalPoint(getCameraClosestEulerPlaneNormal(cam), handle.getAbsPos().getPosition());
+					draggedObj = obj;
+					prevPos = obj.pos.clone();
+					drawGrid(handle.getAbsPos().getPosition(), plane.getNormal(), cam, s3d);
+
+					@:privateAccess s3d.window.mouseMode = Relative(function(e) {
+						mousePos.x += e.relX;
+						mousePos.y += e.relY;
+
+						var r = getRay(mousePos.x, mousePos.y, cam, s2d);
+						var pos = r.intersect(plane);
+						switch (obj.type) {
+							case HandleType.Point:
+								var newPos = pos.transformed(getAbsPos(true).getInverse());
+								obj.pos.set(newPos.x, newPos.y, newPos.z);
+							case HandleType.TangentIn(p):
+								var globalToSpline = pos.transformed(getAbsPos(true).getInverse());
+								var newPos = globalToSpline - p.pos;
+								obj.pos.set(newPos.x, newPos.y, newPos.z);
+								p.tangentOut = newPos * -1.;
+							case HandleType.TangentOut(p):
+								var globalToSpline = pos.transformed(getAbsPos(true).getInverse());
+								var newPos = globalToSpline - p.pos;
+								obj.pos.set(newPos.x, newPos.y, newPos.z);
+								p.tangentIn = newPos * -1.;
+						}
+
+						updateSpline(this);
+					}, false);
+				}
+			}
+
+			e.propagate = false;
 		}
-	}
 
-	// Return the tangent on the curve between p1 and p2 at t, 0 <= t <= 1
-	inline function getTangentBetween( t : Float, p1 : SplinePoint, p2 : SplinePoint ) : h3d.col.Point {
-		return switch (shape) {
-			case Linear: getLinearBezierTangent( t, p1.getPoint(), p2.getPoint() );
-			case Quadratic: getQuadraticBezierTangent( t, p1.getPoint(), p1.getSecondControlPoint(), p2.getPoint() );
-			case Cubic: getCubicBezierTangent( t, p1.getPoint(), p1.getSecondControlPoint(), p2.getFirstControlPoint(), p2.getPoint() );
+		interactive.onRelease = function(e) {
+			if (draggedObj != null) {
+				clearGrid();
+				var oldPos = prevPos.clone();
+				var newPos = draggedObj.pos.clone();
+				var obj = draggedObj;
+				@:privateAccess s3d.window.mouseMode = Absolute;
+				draggedObj = null;
+
+				ctx.properties.undo.change(Custom(function(undo) {
+					if (undo) {
+						obj.pos.set(oldPos.x, oldPos.y, oldPos.z);
+						switch(obj.type) {
+							case HandleType.TangentIn(p):
+								p.tangentOut = oldPos * -1.;
+							case HandleType.TangentOut(p):
+								p.tangentIn = oldPos * -1.;
+							default:
+						}
+					}
+					else {
+						obj.pos.set(newPos.x, newPos.y, newPos.z);
+						switch(obj.type) {
+							case HandleType.TangentIn(p):
+								p.tangentOut = newPos * -1.;
+							case HandleType.TangentOut(p):
+								p.tangentIn = newPos * -1.;
+							default:
+						}
+					}
+
+					updateSpline(this);
+				}));
+			}
 		}
-	}
 
-	// Linear Interpolation
-	// p(t) = p0 + (p1 - p0) * t
-	inline function getLinearBezierPoint( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point ) : h3d.col.Point {
-		return p0.add((p1.sub(p0).scaled(t)));
-	}
-	// p'(t) = (p1 - p0)
-	inline function getLinearBezierTangent( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point ) : h3d.col.Point {
-		return p1.sub(p0).normalized();
-	}
+		function cancelEventPropagation(e : hxd.Event) {
+			if (previewPoint != null)
+				e.propagate = false;
+		}
 
-	// Quadratic Interpolation
-	// p(t) = p0 * (1 - t)² + p1 * t * 2 * (1 - t) + p2 * t²
-	inline function getQuadraticBezierPoint( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point, p2 : h3d.col.Point) : h3d.col.Point {
-		return p0.scaled((1 - t) * (1 - t)).add(p1.scaled(t * 2 * (1 - t))).add(p2.scaled(t * t));
+		interactive.onWheel = cancelEventPropagation;
+		interactive.onMove = cancelEventPropagation;
 	}
-	// p'(t) = 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1)
-	inline function getQuadraticBezierTangent( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point, p2 : h3d.col.Point) : h3d.col.Point {
-		return p1.sub(p0).scaled(2 * (1 - t)).add(p2.sub(p1).scaled(2 * t)).normalized();
+
+	function clearInteractive() {
+		interactive.remove();
+		interactive = null;
+		clearHandles();
 	}
 
-	// Cubic Interpolation
-	// p(t) = p0 * (1 - t)³ + p1 * t * 3 * (1 - t)² + p2 * t² * 3 * (1 - t) + p3 * t³
-	inline function getCubicBezierPoint( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point, p2 : h3d.col.Point, p3 : h3d.col.Point) : h3d.col.Point {
-		return p0.scaled((1 - t) * (1 - t) * (1 - t)).add(p1.scaled(t * 3 * (1 - t) * (1 - t))).add(p2.scaled(t * t * 3 * (1 - t))).add(p3.scaled(t * t * t));
+	function updateSpline(splineToUpdate : Spline) {
+		if (splineToUpdate == null)
+			splineToUpdate = this;
+
+		if (splineUpdateRequested)
+			return;
+
+		splineUpdateRequested = true;
+		haxe.Timer.delay(() -> {
+			@:privateAccess splineToUpdate.updateInstance();
+			splineToUpdate.clearHandles();
+				for (p in splineToUpdate.points)
+					@:privateAccess splineToUpdate.drawHandle(p);
+			splineUpdateRequested = false;
+		}, UPDATE_DELAY_MS);
 	}
-	// p'(t) = 3 * (1 - t)² * (p1 - p0) + 6 * (1 - t) * t * (p2 - p1) + 3 * t² * (p3 - p2)
-	inline function getCubicBezierTangent( t : Float, p0 : h3d.col.Point, p1 : h3d.col.Point, p2 : h3d.col.Point, p3 : h3d.col.Point) : h3d.col.Point {
-		return p1.sub(p0).scaled(3 * (1 - t) * (1 - t)).add(p2.sub(p1).scaled(6 * (1 - t) * t)).add(p3.sub(p2).scaled(3 * t * t)).normalized();
+
+	function clearPreviewSpline() {
+		previewSpline?.graphics?.clear();
+		previewSpline?.local3d.remove();
+		previewSpline?.parent.children.remove(previewSpline);
+		previewSpline = null;
+		previewPoint = null;
 	}
 
-	function generateSplineGraph() {
+	function drawGrid(center : h3d.col.Point, normal : h3d.Vector, cam : h3d.Camera, s3d : h3d.scene.Scene) {
+		var gridStep = 1;
+		var gridSize = 100;
+
+		clearGrid();
+		if (grid == null) {
+			grid = new h3d.scene.Graphics(s3d);
+			grid.name = "gridGraphics";
+			grid.material.mainPass.setPassName("afterTonemapping");
+			grid.material.mainPass.depthWrite = false;
+			grid.material.mainPass.depthTest = LessEqual;
+			grid.ignoreParentTransform = false;
+		}
 
-		if( !showSpline ) {
-			if( lineGraphics != null ) {
-				lineGraphics.remove();
-				lineGraphics = null;
-			}
-			return;
+		grid.lineStyle(0.5, 0xC5C5C5);
+		var start = -1 * gridSize / 2;
+		for(i in 0...(hxd.Math.floor(gridSize / gridStep) + 1)) {
+			grid.moveTo(0, start + (i * gridStep), start);
+			grid.lineTo(0, start + (i * gridStep), start + gridSize);
+
+			grid.moveTo(0, start, start + (i * gridStep));
+			grid.lineTo(0, start + gridSize, start + (i * gridStep));
 		}
 
-		if( lineGraphics == null ) {
-			lineGraphics = new h3d.scene.Graphics(local3d);
-			lineGraphics.lineStyle(lineThickness, color);
-			lineGraphics.name = "lineGraphics";
-			lineGraphics.material.mainPass.setPassName("overlay");
-			lineGraphics.material.mainPass.depth(false, LessEqual);
-			lineGraphics.ignoreParentTransform = false;
+		// Draw the two axis of the plane
+		var colorX = 0xFF0000;
+		var colorY = 0x9DFF00;
+		var colorZ = 0x003CFF;
+
+		var vAxisColor = colorX;
+		var hAxisColor = colorX;
+
+		if (normal.x != 0) {
+			vAxisColor = colorZ;
+			hAxisColor = colorY;
 		}
 
-		lineGraphics.lineStyle(lineThickness, color);
-		lineGraphics.clear();
-		var b = true;
-		for( s in data.samples ) {
-			var localPos = lineGraphics.globalToLocal(s.pos.clone());
-			b ? lineGraphics.moveTo(localPos.x, localPos.y, localPos.z) : lineGraphics.lineTo(localPos.x, localPos.y, localPos.z);
-			b = false;
+		if (normal.y != 0) {
+			vAxisColor = colorZ;
+			hAxisColor = colorX;
 		}
-	}
 
-	public function computeSpline() {
-		#if editor
-		if (loading)
-			return;
-		#end
-		computeSplineData();
-		#if editor
-			generateSplineGraph();
-		#end
-	}
+		if (normal.z != 0) {
+			vAxisColor = colorX;
+			hAxisColor = colorY;
+		}
 
-	#if editor
+		grid.lineStyle(1.5, hAxisColor);
+		grid.moveTo(0, start, 0);
+		grid.lineTo(0, -start, 0);
 
-	public function onEdit( b : Bool ) {
-		if( b ) wasEdited = true;
+		grid.lineStyle(1.5, vAxisColor);
+		grid.moveTo(0, 0, start);
+		grid.lineTo(0, 0, -start);
+
+		grid.setPosition(center.x, center.y, center.z);
+		grid.setDirection(normal * -1.0, new h3d.Vector(0, 0, 1));
+
+		grid.setScale(getScaleWithCam(grid.getAbsPos().getPosition(), 70, cam));
 	}
 
-	override function setSelected(b : Bool ) {
-		super.setSelected(b);
+	function clearGrid() {
+		grid?.clear();
+		grid?.remove();
+		grid = null;
+	}
 
-		if( editor != null && this.enabled)
-			editor.setSelected(b);
+	function getScaleWithCam(origin : h3d.col.Point, ratio : Float, cam : h3d.Camera) {
+		var distToCam = cam.pos.sub(origin).length();
+		if (hxd.Math.isNaN(distToCam))
+			distToCam = 1000000000.0;
+		var objRatio = ratio / h3d.Engine.getCurrent().height;
+		var scale = objRatio * distToCam * Math.tan(cam.fovY * 0.5 * Math.PI / 180.0);
+		if (cam.orthoBounds != null)
+			scale = objRatio * (cam.orthoBounds.xSize) * 0.5;
+		return scale;
+	}
+
+	function getClosestSplinePointFromMouse(mousePos: h3d.Vector, cam : h3d.Camera) : SplinePoint {
+		if (samples == null)
+			return null;
 
-		return true;
+		var result : SplinePoint = null;
+		var minDist = -1.0;
+		var engine = h3d.Engine.getCurrent();
+		var height = engine.height;
+		var width =  engine.width;
+		for( sp in samples ) {
+			var screenPos = cam.projectInline(sp.pos.x, sp.pos.y, sp.pos.z, width, height);
+			screenPos.z = 0;
+			var dist = screenPos.distance(mousePos);
+			if( dist < minDist || minDist == -1 ) {
+				minDist = dist;
+				result = sp;
+			}
+		}
+		return result;
 	}
 
-	override function edit( ctx : hide.prefab.EditContext ) {
-		super.edit(ctx);
+	function getCameraClosestEulerPlaneNormal(cam : h3d.Camera) {
+		var x = new h3d.Vector(1,0,0);
+		var y = new h3d.Vector(0,1,0);
+		var z = new h3d.Vector(0,0,1);
 
-		ctx.properties.add( new hide.Element('
-			<div class="group" name="Spline">
-				<dl>
-					<dt>Color</dt><dd><input type="color" alpha="true" field="color"/></dd>
-					<dt>Thickness</dt><dd><input type="range" min="1" max="10" field="lineThickness"/></dd>
-					<dt>Step</dt><dd><input type="range" min="1" max="10" step="1" field="step"/></dd>
-					<dt>Loop</dt><dd><input type="checkbox" field="loop"/></dd>
-					<dt>Show Spline</dt><dd><input type="checkbox" field="showSpline"/></dd>
-					<dt>Type</dt>
-						<dd>
-							<select field="shape" >
-								<option value="Linear">Linear</option>
-								<option value="Cubic">Curve</option>
-							</select>
-						</dd>
-				</dl>
-			</div>'), this, function(pname) { ctx.onChange(this, pname); });
+		var normal = x;
+		var forward = cam.getForward();
+		var dot = Math.abs(forward.dot(x));
 
-		if( editor == null ) {
-			editor = new hide.prefab.SplineEditor(this, ctx.properties.undo);
+		var tmpDot = Math.abs(forward.dot(y));
+		if (tmpDot > dot) {
+			normal = y;
+			dot = tmpDot;
 		}
 
-		editor.editContext = ctx;
-		editor.edit(ctx);
+		tmpDot = Math.abs(forward.dot(z));
+		if (tmpDot > dot) {
+			normal = z;
+			dot = tmpDot;
+		}
+
+		return normal;
 	}
 
-	override function getHideProps() : hide.prefab.HideProps {
-		return { icon : "arrows-v", name : "Spline", allowChildren: function(s) return Prefab.isOfType(s, SplinePoint) };
+	function getRay(mx : Float, my : Float, cam : h3d.Camera, s2d : h2d.Scene) {
+		var screenPt = new h2d.col.Point( -1 + 2 * mx / s2d.width, 1 - 2 * my / s2d.height);
+		var nearPt = cam.unproject(screenPt.x, screenPt.y, 0);
+		var farPt = cam.unproject(screenPt.x, screenPt.y, 1);
+		var rayDir = farPt.sub(nearPt).normalized();
+		return h3d.col.Ray.fromValues(nearPt.x, nearPt.y, nearPt.z, rayDir.x, rayDir.y, rayDir.z);
 	}
 	#end
 
-	static var _ = Prefab.register("spline", Spline);
+	static var _ = hrt.prefab.Prefab.register("spline", Spline);
 }

+ 185 - 259
hrt/prefab/l3d/SplineMesh.hx

@@ -1,316 +1,239 @@
 package hrt.prefab.l3d;
 
-import h3d.scene.MeshBatch;
-import h3d.scene.Mesh;
-import hrt.prefab.l3d.Spline.SplinePoint;
-
-class SplineMeshShader extends hxsl.Shader {
-
+class SplineUV extends hxsl.Shader {
 	static var SRC = {
-		@:import h3d.shader.BaseMesh;
-
-		// Spline Infos
-		@const(4096) var POINT_COUNT : Int;
-		@const var SPLINE_UV_X : Bool;
-		@const var SPLINE_UV_Y : Bool;
-		@param var stepSize : Float;
-		@param var points : Buffer<Vec4, POINT_COUNT>;
-
-		// Instance Infos
-		@param var modelMat : Mat4;
-		@param var splinePos : Float;
-
+		@input var input : {
+			var uv : Vec2;
+		};
 		var calculatedUV : Vec2;
-
-		function vertex() {
-
-			var modelPos = relativePosition * modelMat.mat3x4();
-			var pos = splinePos + modelPos.y;
-			pos = clamp(pos, 0.0, POINT_COUNT * stepSize);
-			var offsetY = pos - splinePos;
-
-			// Linear Interpolation between two samples
-			var s1 = clamp(floor(pos / stepSize), 0.0, POINT_COUNT - 1.0).int();
-			var s2 = clamp(ceil(pos / stepSize), 0.0, POINT_COUNT - 1.0).int();
-			var t = saturate((pos - (s1 * stepSize)) / stepSize);
-			var point = mix(points[s1 * 2], points[s2 * 2], t).xyz;
-			var tangent = mix(points[s1 * 2 + 1], points[s2 * 2 + 1], t).xyz;
-
-			// Construct the new transform
-			var worldUp = vec3(0,0,1);
-			var front = -tangent;
-			var right = -front.cross(worldUp).normalize();
-			var up = front.cross(right).normalize();
-
-			var rotation = mat4(	vec4(right.x, front.x, up.x, 0),
-									vec4(right.y, front.y, up.y, 0),
-									vec4(right.z, front.z, up.z, 0),
-									vec4(0,0,0,1));
-
-			var translation = mat4(	vec4(1,0,0, point.x ),
-									vec4(0,1,0, point.y ),
-									vec4(0,0,1, point.z ),
-									vec4(0,0,0,1));
-
-			var transform = rotation * translation;
-			var localPos = (modelPos - vec3(0, offsetY, 0));
-			transformedPosition = localPos * transform.mat3x4();
-			transformedNormal = transformedNormal * modelMat.mat3x4() * rotation.mat3x4();
-
-			if( SPLINE_UV_X )
-				calculatedUV.x = pos;
-			if( SPLINE_UV_Y )
-				calculatedUV.y = pos;
+		function __init__() {
+			calculatedUV = input.uv;
 		}
 	}
 }
 
-enum SplineMeshMode {
-	MultiMesh;
-	Instanced;
-	BigGeometry;
-}
-
-// Need to dipose the GPU buffer manually
-class SplineMeshBatch extends h3d.scene.MeshBatch {
-
-	public var splineData : Spline.SplineData;
-
-	override function onRemove() {
-		super.onRemove();
-		var splinemeshShader = material.mainPass.getShader(SplineMeshShader);
-		if( splinemeshShader != null ) {
-			splinemeshShader.points.dispose();
-		}
-	}
-
-	override function sync(ctx) {
-		super.sync(ctx);
-		var s = material.mainPass.getShader(SplineMeshShader);
-		if( s != null && s.points == null || s.points.isDisposed() ) {
-			var bufferData = new hxd.FloatBuffer(s.POINT_COUNT * 4 * 2);
-			for( i in 0 ... splineData.samples.length ) {
-				var index = i * 2 * 4;
-				var s = splineData.samples[i];
-				bufferData[index] = s.pos.x; bufferData[index + 1] = s.pos.y; bufferData[index + 2] = s.pos.z; bufferData[index + 3] = 0.0;
-				bufferData[index + 4] = s.tangent.x; bufferData[index + 5] = s.tangent.y; bufferData[index + 6] = s.tangent.z; bufferData[index + 7] = 0.0;
-			}
-			s.points = new h3d.Buffer(s.POINT_COUNT * 2, @:privateAccess SplineMesh.SPLINE_FMT, [UniformBuffer,Dynamic]);
-			s.points.uploadFloats(bufferData, 0, s.points.vertices, 0);
-		}
-	}
-
-}
+class SplineMesh extends hrt.prefab.Object3D {
 
-class SplineMesh extends Spline {
+	static var SPLINE_FMT = hxd.BufferFormat.make([{ name : "position", type : DVec3 }, { name : "normal", type : DVec3 },  { name : "uv", type : DVec2 }]);
 
-	static var SPLINE_FMT = hxd.BufferFormat.make([{ name : "position", type : DVec4 },{ name : "tangent", type : DVec4 }]);
+	var spline : Spline = null;
 
-	@:s var meshPath : String;
-	var meshes : Array<h3d.scene.Mesh> = [];
+	@:s var scaleUVy : Float = 1.0;
+	@:s var scaleUVx : Float = 1.0;
 
-	@:s var splineUVx : Bool = false;
-	@:s var splineUVy : Bool = false;
+	@:s var subdivision: Int = 0;
+	@:s var thickness: Float = 2;
+	@:s var count: Int = 2;
+	@:s var spacing: Float = 2;
 
-	@:s var spacing: Float = 0.0;
-	@:c var meshScale = new h3d.Vector(1,1,1);
-	@:c var meshRotation = new h3d.Vector(0,0,0);
-	var modelMat = new h3d.Matrix();
+	@:s var previewPointCount = 16;
+	@:s var previewRadius = 2;
 
-	var meshBatch : SplineMeshBatch = null;
-	var meshPrimitive : h3d.prim.MeshPrimitive = null;
+	var meshPrimitive : h3d.prim.RawPrimitive = null;
 	var meshMaterial : h3d.mat.Material = null;
-	@:s var customPass : String;
-
-	override function save() : Dynamic {
-		var obj = super.save();
-		obj.meshScale = meshScale;
-		obj.meshRotation = meshRotation;
-		return obj;
-	}
-
-	override function load( obj : Dynamic ) {
-		super.load(obj);
-		meshScale = obj.meshScale == null ? new h3d.Vector(1,1,1) : new h3d.Vector(obj.meshScale.x, obj.meshScale.y, obj.meshScale.z);
-		meshRotation = obj.meshRotation == null ? new h3d.Vector(0,0,0) : new h3d.Vector(obj.meshRotation.x, obj.meshRotation.y, obj.meshRotation.z);
-	}
-
-	override function copy( obj : Dynamic ) {
-		super.copy(obj);
-		meshScale = obj.meshScale == null ? new h3d.Vector(1,1,1) : new h3d.Vector(obj.meshScale.x, obj.meshScale.y, obj.meshScale.z);
-		meshRotation = obj.meshRotation == null ? new h3d.Vector(0,0,0) : new h3d.Vector(obj.meshRotation.x, obj.meshRotation.y, obj.meshRotation.z);
-	}
-
-	override function make( ?sh:hrt.prefab.Prefab.ContextMake) : Prefab {
-		makeInstance();
-		postMakeInstance();
 
-		return this;
+	override function makeObject(parent3d: h3d.scene.Object) : h3d.scene.Object {
+		var mesh = new h3d.scene.Mesh(null, parent3d);
+		mesh.material.mainPass.addShader(new SplineUV());
+		return mesh;
 	}
 
 	override function updateInstance(?propName : String ) {
 		super.updateInstance(propName);
+		disposeSplineMesh();
 
-		var rot = new h3d.Matrix();
-		rot.initRotation(hxd.Math.degToRad(meshRotation.x), hxd.Math.degToRad(meshRotation.y), hxd.Math.degToRad(meshRotation.z));
-		var scale = new h3d.Matrix();
-		scale.initScale(meshScale.x, meshScale.y, meshScale.z);
-		modelMat.multiply(scale, rot);
-
-		createMeshPrimitive();
-		createMeshBatch();
-		createBatches();
-
-		// Remake the material
-		if( meshBatch != null ) {
-			for( c in @:privateAccess children ) {
-				var mat = Std.downcast(c, Material);
-				if( mat != null && mat.enabled )
-					@:privateAccess mat.makeInstance();
-				var shader = Std.downcast(c, Shader);
-				if( shader != null && shader.enabled )
-					shader.makeInstance();
-			}
-		}
+		spline = findParent(Spline, null, false, true);
+		if ( spline == null || spline.samples == null )
+			computeDefaultSplineMesh();
+		else
+			computeSplineMesh();
 	}
 
-	function createMeshPrimitive() {
-		meshPrimitive = null;
-		meshMaterial = null;
-		if( meshPath != null ) {
-			var meshTemplate : h3d.scene.Mesh = shared.loadModel(meshPath).toMesh();
-			if( meshTemplate != null ) {
-				meshPrimitive = cast meshTemplate.primitive;
-				meshMaterial = cast meshTemplate.material.clone();
-			}
+	function getLocalPoints() : Array<Float> {
+		var vertexPerPoint = ( 4 + subdivision );
+		var localPoints = [];
+		localPoints.resize(3 * vertexPerPoint );
+		var angle = 0.0;
+		var stepAngle = hxd.Math.degToRad(360.0 / ( vertexPerPoint- 1 ));
+		for ( i in 0...vertexPerPoint ) {
+			localPoints[i * 3 + 0] = hxd.Math.sin(angle) * thickness;
+			localPoints[i * 3 + 1] = hxd.Math.cos(angle) * thickness;
+			localPoints[i * 3 + 2] = angle / hxd.Math.degToRad(360);
+			angle += stepAngle;
 		}
+		return localPoints;
 	}
 
-	function createMeshBatch() {
-
-		if( meshBatch != null ) {
-			meshBatch.remove();
-			meshBatch = null;
+	function computeDefaultSplineMesh() {
+		var bounds = new h3d.col.Bounds();
+		var localPoints = getLocalPoints();
+
+		previewPointCount = hxd.Math.imax(4, previewPointCount);
+
+		var vertexPerPoint = ( 4 + subdivision );
+		var vertexCount = vertexPerPoint * previewPointCount;
+		var splineDataSize = 8 * vertexCount;
+		var vertexData = new hxd.FloatBuffer(splineDataSize * count);
+
+		for ( s in 0...count ) {
+			var spacing = s * spacing - spacing * (count - 1) * 0.5;
+			var uv = 0.0;
+			var stepAngle = hxd.Math.degToRad(360.0 / ( previewPointCount- 1 ));
+			var angle = 0.0;
+			for ( i in 0...previewPointCount ) {
+				var trs = new h3d.Matrix();
+				trs.initTranslation(0.0, previewRadius + spacing);
+				trs.rotateAxis(new h3d.Vector(0.0, 0.0, 1.0), angle);
+				angle += stepAngle;
+
+				for ( j in 0...vertexPerPoint ) {
+					var pos = new h3d.Vector( 0, localPoints[j * 3 + 0], localPoints[j * 3 + 1] );
+					pos.transform(trs);
+					var normal = new h3d.Vector( 0, localPoints[j * 3 + 0], localPoints[j * 3 + 1] );
+					normal.transform3x3(trs);
+					normal.normalize();
+					bounds.addPos(pos.x, pos.y, pos.z);
+
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 0] = pos.x;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 1] = pos.y;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 2] = pos.z;
+
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 3] = normal.x;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 4] = normal.y;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 5] = normal.z;
+
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 6] = uv * scaleUVx;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 7] = localPoints[j * 3 + 2] * scaleUVy;
+				}
+				uv += hxd.Math.atan(stepAngle) * previewRadius;
+			}
 		}
 
-		if( meshPrimitive == null || (meshBatch != null && meshBatch.primitive == meshPrimitive) )
-			return;
-
-		if( meshBatch == null ) {
-			var splineMaterial : h3d.mat.Material = meshMaterial;
-			var splineMeshShader = createShader();
-			splineMaterial.mainPass.addShader(splineMeshShader);
-			splineMaterial.castShadows = false;
-
-			if( customPass != null ) {
-				for( p in customPass.split(",") ) {
-					if( local3d.getScene().renderer.getPassByName(p) != null )
-						splineMaterial.allocPass(p);
+		var indexBuffer = new hxd.IndexBuffer();
+		for ( s in 0...count ) {
+			for ( i in 1...previewPointCount ) {
+				for ( j in 1...vertexPerPoint ) {
+					indexBuffer.push( s * vertexCount + vertexPerPoint * (i - 1) + j - 1 );
+					indexBuffer.push( s * vertexCount + vertexPerPoint * (i - 1) + j);
+					indexBuffer.push( s * vertexCount + vertexPerPoint * i + j - 1 );
+
+					indexBuffer.push( s * vertexCount + vertexPerPoint * (i - 1) + j);
+					indexBuffer.push( s * vertexCount + vertexPerPoint * i + j);
+					indexBuffer.push( s * vertexCount + vertexPerPoint * i + j - 1 );
 				}
 			}
-
-			meshBatch = new SplineMeshBatch(meshPrimitive, splineMaterial, local3d);
-			meshBatch.ignoreParentTransform = true;
-			meshBatch.splineData = this.data;
 		}
-	}
-
-	function createBatches() {
-
-		if( meshBatch == null )
-			return;
 
-		var localBounds = meshPrimitive.getBounds().clone();
-		localBounds.transform(modelMat);
-		var minOffset = hxd.Math.abs(localBounds.yMin);
-		var step = (hxd.Math.abs(localBounds.yMax) + hxd.Math.abs(localBounds.yMin)) + spacing;
-		var stepCount = hxd.Math.ceil((getLength() - minOffset) / step);
+		meshPrimitive = new h3d.prim.RawPrimitive( { vbuf : vertexData, format : SplineMesh.SPLINE_FMT, ibuf : indexBuffer, bounds : bounds } );
+		var mesh : h3d.scene.Mesh = cast local3d;
+		mesh.primitive = meshPrimitive;
+	}
 
-		if( stepCount > 4096 )
+	function computeSplineMesh() {
+		var samples = spline.samples;
+		if ( samples.length < 2 || local3d == null )
 			return;
 
-		meshBatch.begin(stepCount);
-		var splinemeshShader = meshBatch.material.mainPass.getShader(SplineMeshShader);
-		for( i in 0 ... stepCount ) {
-			if( splinemeshShader != null )
-				splinemeshShader.splinePos = i * step + minOffset;
-			meshBatch.emitInstance();
+		var bounds = new h3d.col.Bounds();
+
+		var vertexPerPoint = ( 4 + subdivision );
+		var vertexCount = vertexPerPoint * samples.length;
+		var splineDataSize = 8 * vertexCount;
+		var vertexData = new hxd.FloatBuffer(splineDataSize * count);
+
+		var localPoints = getLocalPoints();
+
+		for ( s in 0...count ) {
+			var spacing = s * spacing - spacing * (count - 1) * 0.5;
+			var uv = 0.0;
+			var prevPos = spline.globalToLocal(samples[0].pos);
+			for ( i in 0...samples.length ) {
+				var absPos = spline.globalToLocal(samples[i].pos);
+				var curPos = absPos.clone();
+				uv += curPos.distance(prevPos);
+				var tangent = samples[i].tangentOut.normalized();
+				var angle = hxd.Math.acos( tangent.dot(new h3d.Vector(1.0, 0.0, 0.0)) );
+				if ( tangent.dot(new h3d.Vector(0.0, 1.0, 0.0)) < 0.0)
+					angle *= -1.0;
+				var trs = new h3d.Matrix();
+				trs.initRotationAxis(new h3d.Vector(0.0, 0.0, 1.0), angle);
+				trs.translate(absPos.x, absPos.y, absPos.z);
+
+				for ( j in 0...vertexPerPoint ) {
+					var pos = new h3d.Vector( 0, localPoints[j * 3 + 0], localPoints[j * 3 + 1] );
+					pos.y += spacing;
+					pos.transform(trs);
+					var normal = new h3d.Vector( 0, localPoints[j * 3 + 0], localPoints[j * 3 + 1] );
+					normal.transform3x3(trs);
+					normal.normalize();
+					bounds.addPos(pos.x, pos.y, pos.z);
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 0] = pos.x;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 1] = pos.y;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 2] = pos.z;
+
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 3] = normal.x;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 4] = normal.y;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 5] = normal.z;
+
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 6] = uv * scaleUVx;
+					vertexData[ s * splineDataSize + i * 8 * vertexPerPoint + j * 8 + 7] = localPoints[j * 3 + 2] * scaleUVy;
+				}
+				prevPos = curPos;
+			}
 		}
-	}
-
-	function createMultiMeshes() {
-
-		for( m in meshes )
-			m.remove();
-
-		meshes = [];
 
-		if( meshPrimitive == null )
-			return;
-
-		var localBounds = meshPrimitive.getBounds().clone();
-		localBounds.transform(modelMat);
-		var minOffset = hxd.Math.abs(localBounds.yMin);
-		var step = (hxd.Math.abs(localBounds.yMax) + hxd.Math.abs(localBounds.yMin)) + spacing;
-
-		var length = getLength();
-		var stepCount = hxd.Math.ceil((length - minOffset) / step);
-		for( i in 0 ... stepCount ) {
-			var m = new h3d.scene.Mesh(meshPrimitive, cast meshMaterial.clone());
-			m.material.castShadows = false;
-			m.ignoreParentTransform = true;
-			m.material.mainPass.culling = None;
-			var s = createShader();
-			m.material.mainPass.addShader(s);
-			s.splinePos = i * step + minOffset;
-			local3d.addChild(m);
-			meshes.push(m);
+		var indexBuffer = new hxd.IndexBuffer();
+		for ( s in 0...count ) {
+			for ( i in 1...samples.length ) {
+				for ( j in 1...vertexPerPoint ) {
+					indexBuffer.push( s * vertexCount + vertexPerPoint * (i - 1) + j - 1 );
+					indexBuffer.push( s * vertexCount + vertexPerPoint * i + j - 1 );
+					indexBuffer.push( s * vertexCount + vertexPerPoint * (i - 1) + j);
+
+					indexBuffer.push( s * vertexCount + vertexPerPoint * (i - 1) + j);
+					indexBuffer.push( s * vertexCount + vertexPerPoint * i + j - 1 );
+					indexBuffer.push( s * vertexCount + vertexPerPoint * i + j);
+				}
+			}
 		}
+
+		meshPrimitive = new h3d.prim.RawPrimitive( { vbuf : vertexData, format : SplineMesh.SPLINE_FMT, ibuf : indexBuffer, bounds : bounds } );
+		var mesh : h3d.scene.Mesh = cast local3d;
+		mesh.primitive = meshPrimitive;
 	}
 
-	function createShader() {
-		var s = new SplineMeshShader();
-		s.POINT_COUNT = data.samples.length;
-		s.stepSize = step;
-		s.modelMat = modelMat;
-		var bufferData = new hxd.FloatBuffer(s.POINT_COUNT * 4 * 2);
-		for( i in 0 ... data.samples.length ) {
-			var index = i * 2 * 4;
-			var s = data.samples[i];
-			bufferData[index] = s.pos.x; bufferData[index + 1] = s.pos.y; bufferData[index + 2] = s.pos.z; bufferData[index + 3] = 0.0;
-			bufferData[index + 4] = s.tangent.x; bufferData[index + 5] = s.tangent.y; bufferData[index + 6] = s.tangent.z; bufferData[index + 7] = 0.0;
+	function disposeSplineMesh(){
+		if ( meshPrimitive != null ) {
+			meshPrimitive.dispose();
+			meshPrimitive = null;
 		}
-		s.points = new h3d.Buffer(s.POINT_COUNT * 2, SPLINE_FMT, [UniformBuffer,Dynamic]);
-		s.points.uploadFloats(bufferData, 0, s.points.vertices, 0);
-		s.SPLINE_UV_X = splineUVx;
-		s.SPLINE_UV_Y = splineUVy;
-		return s;
 	}
 
 	#if editor
-
 	override function edit( ctx : hide.prefab.EditContext ) {
 		super.edit(ctx);
 
 		var props = new hide.Element('
 			<div class="group" name="Material">
 				<dl>
-					<dt>Use spline UV on X</dt><dd><input type="checkbox" field="splineUVx"/></dd>
-					<dt>Use spline UV on Y</dt><dd><input type="checkbox" field="splineUVy"/></dd>
+					<dt>ScaleUVx</dt><dd><input type="range" min="0.0001" max="10" field="scaleUVx"/></dd>
+					<dt>ScaleUVy</dt><dd><input type="range" min="0.0001" max="10" field="scaleUVy"/></dd>
 					<dt>Custom Pass</dt><dd><input type="text" field="customPass"/></dd>
 					<div align="center"><input type="button" value="Refresh" class="refresh"/></div>
 				</dl>
 			</div>
 			<div class="group" name="Mesh">
 				<dl>
-					<dt>Mesh</dt><dd><input type="model" field="meshPath"/></dd>
+					<dt>Subdivision</dt><dd><input type="range" min="0" max="10" step="1" field="subdivision"/></dd>
+					<dt>Thickness</dt><dd><input type="range" min="1" max="10" field="thickness"/></dd>
+					<dt>Count</dt><dd><input type="range" min="1" max="10" step="1" field="count"/></dd>
 					<dt>Spacing</dt><dd><input type="range" min="0" max="10" field="spacing"/></dd>
-					<dt>Scale X</dt><dd><input type="range" min="0" max="5" value="1" field="meshScale.x"/></dd>
-					<dt>Scale Y</dt><dd><input type="range" min="0" max="5" value="1" field="meshScale.y"/></dd>
-					<dt>Scale Z</dt><dd><input type="range" min="0" max="5" value="1" field="meshScale.z"/></dd>
-					<dt>Rotation X</dt><dd><input type="range" min="-180" max="180" value="0" field="meshRotation.x" /></dd>
-					<dt>Rotation Y</dt><dd><input type="range" min="-180" max="180" value="0" field="meshRotation.y" /></dd>
-					<dt>Rotation Z</dt><dd><input type="range" min="-180" max="180" value="0" field="meshRotation.z" /></dd>
+				</dl>
+			</div>
+			<div class="group" name="Preview">
+				<dl>
+					<dt>Points</dt><dd><input type="range" min="4" max="64" step="1" field="previewPointCount"/></dd>
+					<dt>Radius</dt><dd><input type="range" min="1" max="10" field="previewRadius"/></dd>
 				</dl>
 			</div>
 			');
@@ -320,10 +243,13 @@ class SplineMesh extends Spline {
 	}
 
 	override function getHideProps() : hide.prefab.HideProps {
-		return { icon : "arrows-v", name : "SplineMesh" };
+		return {
+			icon : "arrows-v",
+			name : "SplineMesh",
+			allowParent: (p) -> Std.isOfType(p, Spline) || p.parent == null
+		};
 	}
-
 	#end
 
-	static var _ = Prefab.register("splineMesh", SplineMesh);
+	static var _ = hrt.prefab.Prefab.register("splineMesh", SplineMesh);
 }

+ 0 - 112
hrt/prefab/l3d/SplineMover.hx

@@ -1,112 +0,0 @@
-package hrt.prefab.l3d;
-import hrt.prefab.l3d.Spline;
-
-class SplineMoverObject extends h3d.scene.Object {
-	var prefab : SplineMover;
-
-	var state : Spline.MoveAlongSplineState = new Spline.MoveAlongSplineState();
-	public var movables : Array<h3d.scene.Object> = [];
-
-	#if editor
-	var debugViz : h3d.scene.Mesh = null;
-	#end
-
-	static var targetAbsPos = new h3d.Matrix();
-	static var tmpMat = new h3d.Matrix();
-
-	override public function new(?parent : h3d.scene.Object, prefab : SplineMover) {
-		super(parent);
-		this.prefab = prefab;
-
-		#if editor
-		var prim = new h3d.prim.Sphere();
-		prim.addNormals();
-		debugViz = new h3d.scene.Mesh(prim, this);
-		debugViz.material.color.setColor(0xFF0000);
-		debugViz.material.shadows = false;
-		movables.push(debugViz);
-		#end
-	}
-
-	override function syncRec(rctx:h3d.scene.RenderContext) {
-		super.syncRec(rctx);
-
-		#if editor
-		debugViz.visible = prefab.showDebug;
-		#end
-
-		updatePoint(rctx.elapsedTime);
-	}
-
-	public function updatePoint(dt: Float) {
-		if (prefab.points.length <= 1)
-			return;
-
-		state = prefab.moveAlongSpline(dt * prefab.speed, state);
-		var pt = state.point;
-
-		for (c in movables) {
-			c.getTransform(targetAbsPos);
-			targetAbsPos.identity();
-			targetAbsPos.setPosition(pt.toVector());
-
-			this.getAbsPos().getInverse(tmpMat);
-			tmpMat.multiply(targetAbsPos, tmpMat);
-			targetAbsPos.load(tmpMat);
-
-			c.setPosition(targetAbsPos.getPosition().x, targetAbsPos.getPosition().y, targetAbsPos.getPosition().z);
-			if( prefab.orientTangent ) c.setDirection(state.tangent);
-		}
-	}
-}
-
-class SplineMover extends Spline {
-
-	@:s public var speed = 1.0;
-	@:s public var orientTangent = false;
-	#if editor
-	@:s public var showDebug : Bool = true;
-	#end
-
-	override function makeObject(parent:h3d.scene.Object) {
-		return new SplineMoverObject(parent, this);
-	}
-
-	override function makeChild(p:Prefab) {
-		super.makeChild(p);
-		var l3d = Object3D.getLocal3d(p);
-		if (l3d != null && p.to(SplinePoint) == null) {
-			(cast local3d:SplineMoverObject).movables.push(l3d);
-		}
-	}
-
-	override function postMakeInstance() {
-		super.postMakeInstance();
-		(cast local3d:SplineMoverObject).updatePoint(1.0);
-	}
-
-	#if editor
-	override function getHideProps() : hide.prefab.HideProps {
-		return { icon : "arrows-v", name : "Spline Mover", allowChildren: function(s) return true};
-	}
-
-	override function edit(ctx:hide.prefab.EditContext) {
-		ctx.properties.add( new hide.Element('
-			<p>Important ! Save then reload when you add a child prefab to animate in order to see the animation ! It\'s a known bug.</p>
-		'));
-		super.edit(ctx);
-
-		ctx.properties.add( new hide.Element('
-		<div class="group" name="Mover">
-			<dl>
-				<dt>Speed</dt><dd><input type="range" min="-100" max="100" field="speed"/></dd>
-				<dt>Orient To Tangent</dt><dd><input type="checkbox" field="orientTangent"/></dd>
-				<dt>Show Debug</dt><dd><input type="checkbox" field="showDebug"/></dd>
-			</dl>
-		</div>'), this, function(pname) { ctx.onChange(this, pname); });
-	}
-	#end
-
-	static var _ = hrt.prefab.Prefab.register("splineMover", SplineMover);
-
-}