Browse Source

Portals - add gizmo handles for editing portals and rooms

Gizmo handles are added for much more user friendly editing of portals and room bounds.
lawnjelly 4 năm trước cách đây
mục cha
commit
770d9f8220

+ 8 - 0
doc/classes/Portal.xml

@@ -12,6 +12,14 @@
 	<tutorials>
 	</tutorials>
 	<methods>
+		<method name="set_point">
+			<return type="void" />
+			<argument index="0" name="index" type="int" />
+			<argument index="1" name="position" type="Vector2" />
+			<description>
+				Sets individual points. Primarily for use by the editor.
+			</description>
+		</method>
 	</methods>
 	<members>
 		<member name="linked_room" type="NodePath" setter="set_linked_room" getter="get_linked_room" default="NodePath(&quot;&quot;)">

+ 8 - 0
doc/classes/Room.xml

@@ -13,6 +13,14 @@
 	<tutorials>
 	</tutorials>
 	<methods>
+		<method name="set_point">
+			<return type="void" />
+			<argument index="0" name="index" type="int" />
+			<argument index="1" name="position" type="Vector3" />
+			<description>
+				Sets individual points. Primarily for use by the editor.
+			</description>
+		</method>
 	</methods>
 	<members>
 		<member name="points" type="PoolVector3Array" setter="set_points" getter="get_points" default="PoolVector3Array(  )">

+ 408 - 183
editor/spatial_editor_gizmos.cpp

@@ -4441,6 +4441,19 @@ RoomGizmoPlugin::RoomGizmoPlugin() {
 
 	create_material("room", color_room, false, true, false);
 	create_material("room_overlap", color_overlap, false, false, false);
+
+	create_handle_material("room_handle");
+}
+
+Ref<EditorSpatialGizmo> RoomGizmoPlugin::create_gizmo(Spatial *p_spatial) {
+	Ref<RoomSpatialGizmo> ref;
+
+	Room *room = Object::cast_to<Room>(p_spatial);
+	if (room) {
+		ref = Ref<RoomSpatialGizmo>(memnew(RoomSpatialGizmo(room)));
+	}
+
+	return ref;
 }
 
 bool RoomGizmoPlugin::has_gizmo(Spatial *p_spatial) {
@@ -4459,68 +4472,173 @@ int RoomGizmoPlugin::get_priority() const {
 	return -1;
 }
 
-void RoomGizmoPlugin::redraw(EditorSpatialGizmo *p_gizmo) {
-	p_gizmo->clear();
+//////////////////////
 
-	Room *room = Object::cast_to<Room>(p_gizmo->get_spatial_node());
+String RoomSpatialGizmo::get_handle_name(int p_idx) const {
+	return "Point " + itos(p_idx);
+}
 
-	if (room) {
-		const Geometry::MeshData &md = room->_bound_mesh_data;
-		if (!md.edges.size())
-			return;
+Variant RoomSpatialGizmo::get_handle_value(int p_idx) {
+	if (!_room) {
+		return Vector3(0, 0, 0);
+	}
 
-		Vector<Vector3> lines;
-		Transform tr = room->get_global_transform();
-		tr.affine_invert();
+	int num_points = _room->_bound_pts.size();
+	if (p_idx >= num_points) {
+		return Vector3(0, 0, 0);
+	}
 
-		Ref<Material> material = get_material("room", p_gizmo);
-		Ref<Material> material_overlap = get_material("room_overlap", p_gizmo);
-		Color color(1, 1, 1, 1);
+	return _room->_bound_pts[p_idx];
+}
 
-		for (int n = 0; n < md.edges.size(); n++) {
-			Vector3 a = md.vertices[md.edges[n].a];
-			Vector3 b = md.vertices[md.edges[n].b];
+void RoomSpatialGizmo::set_handle(int p_idx, Camera *p_camera, const Point2 &p_point) {
+	if (!_room || (p_idx >= _room->_bound_pts.size())) {
+		return;
+	}
 
-			// xform
-			a = tr.xform(a);
-			b = tr.xform(b);
+	Transform tr = _room->get_global_transform();
+	Transform tr_inv = tr.affine_inverse();
 
-			lines.push_back(a);
-			lines.push_back(b);
-		}
+	Vector3 pt_world = _room->_bound_pts[p_idx];
+	pt_world = tr.xform(pt_world);
 
-		p_gizmo->add_lines(lines, material, false, color);
+	Vector3 ray_from = p_camera->project_ray_origin(p_point);
+	Vector3 ray_dir = p_camera->project_ray_normal(p_point);
 
-		// overlap zones
-		for (int z = 0; z < room->_gizmo_overlap_zones.size(); z++) {
-			const Geometry::MeshData &md_overlap = room->_gizmo_overlap_zones[z];
-			Vector<Vector3> pts;
+	Vector3 camera_dir = p_camera->get_transform().basis.get_axis(2);
+
+	// find the smallest camera axis, we will only transform the handles on 2 axes max,
+	// to try and make things more user friendly (it is confusing trying to change 3d position
+	// from a 2d view)
+	int biggest_axis = 0;
+	real_t biggest = 0.0;
+	for (int n = 0; n < 3; n++) {
+		real_t val = Math::abs(camera_dir.get_axis(n));
+		if (val > biggest) {
+			biggest = val;
+			biggest_axis = n;
+		}
+	}
 
-			for (int f = 0; f < md_overlap.faces.size(); f++) {
-				const Geometry::MeshData::Face &face = md_overlap.faces[f];
+	{
+		Plane plane(pt_world, camera_dir);
+		Vector3 inters;
 
-				for (int c = 0; c < face.indices.size() - 2; c++) {
-					pts.push_back(tr.xform(md_overlap.vertices[face.indices[0]]));
-					pts.push_back(tr.xform(md_overlap.vertices[face.indices[c + 1]]));
-					pts.push_back(tr.xform(md_overlap.vertices[face.indices[c + 2]]));
+		if (plane.intersects_ray(ray_from, ray_dir, &inters)) {
+			if (SpatialEditor::get_singleton()->is_snap_enabled()) {
+				float snap = SpatialEditor::get_singleton()->get_translate_snap();
+				inters.snap(Vector3(snap, snap, snap));
+			}
+
+			for (int n = 0; n < 3; n++) {
+				if (n != biggest_axis) {
+					pt_world.set_axis(n, inters.get_axis(n));
 				}
 			}
 
-			Ref<ArrayMesh> mesh = memnew(ArrayMesh);
-			Array array;
-			array.resize(Mesh::ARRAY_MAX);
-			array[Mesh::ARRAY_VERTEX] = pts;
-			mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array);
-			p_gizmo->add_mesh(mesh, false, Ref<SkinReference>(), material_overlap);
+			Vector3 pt_local = tr_inv.xform(pt_world);
+			_room->set_point(p_idx, pt_local);
+		}
+
+		return;
+	}
+}
+
+void RoomSpatialGizmo::commit_handle(int p_idx, const Variant &p_restore, bool p_cancel) {
+	if (!_room || (p_idx >= _room->_bound_pts.size())) {
+		return;
+	}
+
+	UndoRedo *ur = SpatialEditor::get_singleton()->get_undo_redo();
+
+	ur->create_action(TTR("Set Room Point Position"));
+	ur->add_do_method(_room, "set_point", p_idx, _room->_bound_pts[p_idx]);
+	ur->add_undo_method(_room, "set_point", p_idx, p_restore);
+	ur->commit_action();
+
+	_room->property_list_changed_notify();
+}
+
+void RoomSpatialGizmo::redraw() {
+	clear();
+
+	if (!_room) {
+		return;
+	}
+
+	const Geometry::MeshData &md = _room->_bound_mesh_data;
+	if (!md.edges.size())
+		return;
+
+	Vector<Vector3> lines;
+	Transform tr = _room->get_global_transform();
+	Transform tr_inv = tr.affine_inverse();
+
+	Ref<Material> material = gizmo_plugin->get_material("room", this);
+	Ref<Material> material_overlap = gizmo_plugin->get_material("room_overlap", this);
+	Color color(1, 1, 1, 1);
+
+	for (int n = 0; n < md.edges.size(); n++) {
+		Vector3 a = md.vertices[md.edges[n].a];
+		Vector3 b = md.vertices[md.edges[n].b];
+
+		// xform
+		a = tr_inv.xform(a);
+		b = tr_inv.xform(b);
+
+		lines.push_back(a);
+		lines.push_back(b);
+	}
+
+	if (lines.size()) {
+		add_lines(lines, material, false, color);
+	}
+
+	// overlap zones
+	for (int z = 0; z < _room->_gizmo_overlap_zones.size(); z++) {
+		const Geometry::MeshData &md_overlap = _room->_gizmo_overlap_zones[z];
+		Vector<Vector3> pts;
+
+		for (int f = 0; f < md_overlap.faces.size(); f++) {
+			const Geometry::MeshData::Face &face = md_overlap.faces[f];
+
+			for (int c = 0; c < face.indices.size() - 2; c++) {
+				pts.push_back(tr_inv.xform(md_overlap.vertices[face.indices[0]]));
+				pts.push_back(tr_inv.xform(md_overlap.vertices[face.indices[c + 1]]));
+				pts.push_back(tr_inv.xform(md_overlap.vertices[face.indices[c + 2]]));
+			}
 		}
+
+		Ref<ArrayMesh> mesh = memnew(ArrayMesh);
+		Array array;
+		array.resize(Mesh::ARRAY_MAX);
+		array[Mesh::ARRAY_VERTEX] = pts;
+		mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array);
+		add_mesh(mesh, false, Ref<SkinReference>(), material_overlap);
 	}
+
+	Vector<Vector3> handles;
+	// draw the handles separately because these must correspond to the raw points
+	// for editing
+	for (int n = 0; n < _room->_bound_pts.size(); n++) {
+		handles.push_back(_room->_bound_pts[n]);
+	}
+
+	// handles
+	if (handles.size()) {
+		Ref<Material> material_handle = gizmo_plugin->get_material("room_handle", this);
+		add_handles(handles, material_handle);
+	}
+}
+
+RoomSpatialGizmo::RoomSpatialGizmo(Room *p_room) {
+	_room = p_room;
+	set_spatial_node(p_room);
 }
 
 ////
 
 PortalGizmoPlugin::PortalGizmoPlugin() {
-	_color_portal_front = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/portal_front", Color(0.05, 0.05, 1.0, 0.3));
-	_color_portal_back = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/portal_back", Color(1.0, 1.0, 0.0, 0.15));
 	Color color_portal_margin = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/portal_margin", Color(1.0, 0.1, 0.1, 0.3));
 	Color color_portal_edge = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/portal_edge", Color(0.0, 0.0, 0.0, 0.3));
 	Color color_portal_arrow = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/portal_arrow", Color(1.0, 1.0, 1.0, 1.0));
@@ -4530,6 +4648,19 @@ PortalGizmoPlugin::PortalGizmoPlugin() {
 	create_material("portal_margin", color_portal_margin, false, false, false);
 	create_material("portal_edge", color_portal_edge, false, false, false);
 	create_material("portal_arrow", color_portal_arrow, false, false, false);
+
+	create_handle_material("portal_handle");
+}
+
+Ref<EditorSpatialGizmo> PortalGizmoPlugin::create_gizmo(Spatial *p_spatial) {
+	Ref<PortalSpatialGizmo> ref;
+
+	Portal *portal = Object::cast_to<Portal>(p_spatial);
+	if (portal) {
+		ref = Ref<PortalSpatialGizmo>(memnew(PortalSpatialGizmo(portal)));
+	}
+
+	return ref;
 }
 
 bool PortalGizmoPlugin::has_gizmo(Spatial *p_spatial) {
@@ -4548,168 +4679,262 @@ int PortalGizmoPlugin::get_priority() const {
 	return -1;
 }
 
-void PortalGizmoPlugin::redraw(EditorSpatialGizmo *p_gizmo) {
-	p_gizmo->clear();
+//////////////////////
 
-	Portal *portal = Object::cast_to<Portal>(p_gizmo->get_spatial_node());
+String PortalSpatialGizmo::get_handle_name(int p_idx) const {
+	return "Point " + itos(p_idx);
+}
 
-	if (portal) {
-		// warnings
-		if (portal->_warning_outside_room_aabb || portal->_warning_facing_wrong_way || portal->_warning_autolink_failed) {
-			Ref<Material> icon = get_material("portal_icon", p_gizmo);
-			p_gizmo->add_unscaled_billboard(icon, 0.05);
-		}
+Variant PortalSpatialGizmo::get_handle_value(int p_idx) {
+	if (!_portal) {
+		return Vector2(0, 0);
+	}
 
-		Transform tr = portal->get_global_transform();
-		tr.affine_invert();
+	int num_points = _portal->_pts_local_raw.size();
+	if (p_idx >= num_points) {
+		return Vector2(0, 0);
+	}
 
-		Ref<Material> material_portal = get_material("portal", p_gizmo);
-		Ref<Material> material_margin = get_material("portal_margin", p_gizmo);
-		Ref<Material> material_edge = get_material("portal_edge", p_gizmo);
-		Ref<Material> material_arrow = get_material("portal_arrow", p_gizmo);
-		Color color(1, 1, 1, 1);
+	return _portal->_pts_local_raw[p_idx];
+}
 
-		// make sure world points are up to date
-		portal->portal_update();
+void PortalSpatialGizmo::set_handle(int p_idx, Camera *p_camera, const Point2 &p_point) {
+	if (!_portal || (p_idx >= _portal->_pts_local_raw.size())) {
+		return;
+	}
 
-		int num_points = portal->_pts_world.size();
+	Transform tr = _portal->get_global_transform();
+	Transform tr_inv = tr.affine_inverse();
 
-		// prevent compiler warnings later on
-		if (num_points < 3) {
-			return;
-		}
+	Vector3 pt_local = Portal::_vec2to3(_portal->_pts_local_raw[p_idx]);
+	Vector3 pt_world = tr.xform(pt_local);
 
-		// margins
-		real_t margin = portal->get_active_portal_margin();
-		bool show_margins = Portal::_settings_gizmo_show_margins;
+	Vector3 ray_from = p_camera->project_ray_origin(p_point);
+	Vector3 ray_dir = p_camera->project_ray_normal(p_point);
+
+	// get a normal from the global transform
+	Plane plane(Vector3(0, 0, 0), Vector3(0, 0, 1));
+	plane = tr.xform(plane);
+
+	// construct the plane that the 2d portal is defined in
+	plane = Plane(pt_world, plane.normal);
+
+	Vector3 inters;
+
+	if (plane.intersects_ray(ray_from, ray_dir, &inters)) {
+		// back calculate from the 3d intersection to the 2d portal plane
+		inters = tr_inv.xform(inters);
 
-		if (margin < 0.05f) {
-			show_margins = false;
+		// snapping will be in 2d for portals, and the scale may make less sense,
+		// but better to offer at least some functionality
+		if (SpatialEditor::get_singleton()->is_snap_enabled()) {
+			float snap = SpatialEditor::get_singleton()->get_translate_snap();
+			inters.snap(Vector3(snap, snap, snap));
 		}
 
-		PoolVector<Vector3> pts_portal;
-		PoolVector<Color> cols_portal;
-		PoolVector<Vector3> pts_margin;
-		Vector<Vector3> edge_pts;
-
-		Vector3 portal_normal_world_space = portal->_plane.normal;
-		portal_normal_world_space *= margin;
-
-		// this may not be necessary, dealing with non uniform scales,
-		// possible the affine_invert dealt with this earlier .. but it's just for
-		// the editor so not performance critical
-		Basis normal_basis = tr.basis;
-
-		Vector3 portal_normal = normal_basis.xform(portal_normal_world_space);
-		Vector3 pt_portal_first = tr.xform(portal->_pts_world[0]);
-
-		for (int n = 0; n < num_points; n++) {
-			Vector3 pt = portal->_pts_world[n];
-			pt = tr.xform(pt);
-
-			// CI for visual studio can't seem to get around the possibility
-			// that this could cause a divide by zero, so using a local to preclude the
-			// possibility of aliasing from another thread
-			int m = (n + 1) % num_points;
-			Vector3 pt_next = portal->_pts_world[m];
-			pt_next = tr.xform(pt_next);
-
-			// don't need the first and last triangles
-			if ((n != 0) && (n != (num_points - 1))) {
-				pts_portal.push_back(pt_portal_first);
-				pts_portal.push_back(pt);
-				pts_portal.push_back(pt_next);
-				cols_portal.push_back(_color_portal_front);
-				cols_portal.push_back(_color_portal_front);
-				cols_portal.push_back(_color_portal_front);
-
-				pts_portal.push_back(pt_next);
-				pts_portal.push_back(pt);
-				pts_portal.push_back(pt_portal_first);
-				cols_portal.push_back(_color_portal_back);
-				cols_portal.push_back(_color_portal_back);
-				cols_portal.push_back(_color_portal_back);
-			}
+		_portal->set_point(p_idx, Vector2(inters.x, inters.y));
+
+		return;
+	}
+}
 
-			if (show_margins) {
-				Vector3 pt0 = pt - portal_normal;
-				Vector3 pt1 = pt + portal_normal;
-				Vector3 pt2 = pt_next - portal_normal;
-				Vector3 pt3 = pt_next + portal_normal;
+void PortalSpatialGizmo::commit_handle(int p_idx, const Variant &p_restore, bool p_cancel) {
+	if (!_portal || (p_idx >= _portal->_pts_local_raw.size())) {
+		return;
+	}
 
-				pts_margin.push_back(pt0);
-				pts_margin.push_back(pt2);
-				pts_margin.push_back(pt1);
+	UndoRedo *ur = SpatialEditor::get_singleton()->get_undo_redo();
 
-				pts_margin.push_back(pt2);
-				pts_margin.push_back(pt3);
-				pts_margin.push_back(pt1);
+	ur->create_action(TTR("Set Portal Point Position"));
+	ur->add_do_method(_portal, "set_point", p_idx, _portal->_pts_local_raw[p_idx]);
+	ur->add_undo_method(_portal, "set_point", p_idx, p_restore);
+	ur->commit_action();
 
-				edge_pts.push_back(pt0);
-				edge_pts.push_back(pt2);
-				edge_pts.push_back(pt1);
-				edge_pts.push_back(pt3);
-			}
-		}
+	_portal->property_list_changed_notify();
+}
 
-		// portal itself
-		{
-			Ref<ArrayMesh> mesh = memnew(ArrayMesh);
-			Array array;
-			array.resize(Mesh::ARRAY_MAX);
-			array[Mesh::ARRAY_VERTEX] = pts_portal;
-			array[Mesh::ARRAY_COLOR] = cols_portal;
-			mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array);
-			p_gizmo->add_mesh(mesh, false, Ref<SkinReference>(), material_portal);
+void PortalSpatialGizmo::redraw() {
+	clear();
+
+	if (!_portal) {
+		return;
+	}
+
+	// warnings
+	if (_portal->_warning_outside_room_aabb || _portal->_warning_facing_wrong_way || _portal->_warning_autolink_failed) {
+		Ref<Material> icon = gizmo_plugin->get_material("portal_icon", this);
+		add_unscaled_billboard(icon, 0.05);
+	}
+
+	Transform tr = _portal->get_global_transform();
+	Transform tr_inv = tr.affine_inverse();
+
+	Ref<Material> material_portal = gizmo_plugin->get_material("portal", this);
+	Ref<Material> material_margin = gizmo_plugin->get_material("portal_margin", this);
+	Ref<Material> material_edge = gizmo_plugin->get_material("portal_edge", this);
+	Ref<Material> material_arrow = gizmo_plugin->get_material("portal_arrow", this);
+	Color color(1, 1, 1, 1);
+
+	// make sure world points are up to date
+	_portal->portal_update();
+
+	int num_points = _portal->_pts_world.size();
+
+	// prevent compiler warnings later on
+	if (num_points < 3) {
+		return;
+	}
+
+	// margins
+	real_t margin = _portal->get_active_portal_margin();
+	bool show_margins = Portal::_settings_gizmo_show_margins;
+
+	if (margin < 0.05f) {
+		show_margins = false;
+	}
+
+	PoolVector<Vector3> pts_portal;
+	PoolVector<Color> cols_portal;
+	PoolVector<Vector3> pts_margin;
+	Vector<Vector3> edge_pts;
+
+	Vector<Vector3> handles;
+
+	Vector3 portal_normal_world_space = _portal->_plane.normal;
+	portal_normal_world_space *= margin;
+
+	// this may not be necessary, dealing with non uniform scales,
+	// possible the affine_invert dealt with this earlier .. but it's just for
+	// the editor so not performance critical
+	Basis normal_basis = tr_inv.basis;
+
+	Vector3 portal_normal = normal_basis.xform(portal_normal_world_space);
+	Vector3 pt_portal_first = tr_inv.xform(_portal->_pts_world[0]);
+
+	for (int n = 0; n < num_points; n++) {
+		Vector3 pt = _portal->_pts_world[n];
+		pt = tr_inv.xform(pt);
+
+		// CI for visual studio can't seem to get around the possibility
+		// that this could cause a divide by zero, so using a local to preclude the
+		// possibility of aliasing from another thread
+		int m = (n + 1) % num_points;
+		Vector3 pt_next = _portal->_pts_world[m];
+		pt_next = tr_inv.xform(pt_next);
+
+		// don't need the first and last triangles
+		if ((n != 0) && (n != (num_points - 1))) {
+			pts_portal.push_back(pt_portal_first);
+			pts_portal.push_back(pt);
+			pts_portal.push_back(pt_next);
+			cols_portal.push_back(_color_portal_front);
+			cols_portal.push_back(_color_portal_front);
+			cols_portal.push_back(_color_portal_front);
+
+			pts_portal.push_back(pt_next);
+			pts_portal.push_back(pt);
+			pts_portal.push_back(pt_portal_first);
+			cols_portal.push_back(_color_portal_back);
+			cols_portal.push_back(_color_portal_back);
+			cols_portal.push_back(_color_portal_back);
 		}
 
 		if (show_margins) {
-			Ref<ArrayMesh> mesh = memnew(ArrayMesh);
-			Array array;
-			array.resize(Mesh::ARRAY_MAX);
-			array[Mesh::ARRAY_VERTEX] = pts_margin;
-			mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array);
-			p_gizmo->add_mesh(mesh, false, Ref<SkinReference>(), material_margin);
-
-			// lines around the outside of mesh
-			p_gizmo->add_lines(edge_pts, material_edge, false, color);
-		} // only if the margin is sufficient to be worth drawing
-
-		// arrow
-		if (show_margins) {
-			const int arrow_points = 7;
-			const float arrow_length = 0.5; // 1.5
-			const float arrow_width = 0.1; // 0.3
-			const float arrow_barb = 0.27; // 0.8
-
-			Vector3 arrow[arrow_points] = {
-				Vector3(0, 0, -1),
-				Vector3(0, arrow_barb, 0),
-				Vector3(0, arrow_width, 0),
-				Vector3(0, arrow_width, arrow_length),
-				Vector3(0, -arrow_width, arrow_length),
-				Vector3(0, -arrow_width, 0),
-				Vector3(0, -arrow_barb, 0)
-			};
-
-			int arrow_sides = 2;
-
-			Vector<Vector3> lines;
-
-			for (int i = 0; i < arrow_sides; i++) {
-				for (int j = 0; j < arrow_points; j++) {
-					Basis ma(Vector3(0, 0, 1), Math_PI * i / arrow_sides);
-
-					Vector3 v1 = arrow[j] - Vector3(0, 0, arrow_length);
-					Vector3 v2 = arrow[(j + 1) % arrow_points] - Vector3(0, 0, arrow_length);
-
-					lines.push_back(ma.xform(v1));
-					lines.push_back(ma.xform(v2));
-				}
-			}
+			Vector3 pt0 = pt - portal_normal;
+			Vector3 pt1 = pt + portal_normal;
+			Vector3 pt2 = pt_next - portal_normal;
+			Vector3 pt3 = pt_next + portal_normal;
+
+			pts_margin.push_back(pt0);
+			pts_margin.push_back(pt2);
+			pts_margin.push_back(pt1);
+
+			pts_margin.push_back(pt2);
+			pts_margin.push_back(pt3);
+			pts_margin.push_back(pt1);
+
+			edge_pts.push_back(pt0);
+			edge_pts.push_back(pt2);
+			edge_pts.push_back(pt1);
+			edge_pts.push_back(pt3);
+		}
+	}
+
+	// draw the handles separately because these must correspond to the raw points
+	// for editing
+	for (int n = 0; n < _portal->_pts_local_raw.size(); n++) {
+		Vector3 pt = Portal::_vec2to3(_portal->_pts_local_raw[n]);
+		handles.push_back(pt);
+	}
+
+	// portal itself
+	{
+		Ref<ArrayMesh> mesh = memnew(ArrayMesh);
+		Array array;
+		array.resize(Mesh::ARRAY_MAX);
+		array[Mesh::ARRAY_VERTEX] = pts_portal;
+		array[Mesh::ARRAY_COLOR] = cols_portal;
+		mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array);
+		add_mesh(mesh, false, Ref<SkinReference>(), material_portal);
+
+		// handles
+		Ref<Material> material_handle = gizmo_plugin->get_material("portal_handle", this);
+		add_handles(handles, material_handle);
+	}
+
+	if (show_margins) {
+		Ref<ArrayMesh> mesh = memnew(ArrayMesh);
+		Array array;
+		array.resize(Mesh::ARRAY_MAX);
+		array[Mesh::ARRAY_VERTEX] = pts_margin;
+		mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array);
+		add_mesh(mesh, false, Ref<SkinReference>(), material_margin);
+
+		// lines around the outside of mesh
+		add_lines(edge_pts, material_edge, false, color);
+	} // only if the margin is sufficient to be worth drawing
+
+	// arrow
+	if (show_margins) {
+		const int arrow_points = 7;
+		const float arrow_length = 0.5; // 1.5
+		const float arrow_width = 0.1; // 0.3
+		const float arrow_barb = 0.27; // 0.8
+
+		Vector3 arrow[arrow_points] = {
+			Vector3(0, 0, -1),
+			Vector3(0, arrow_barb, 0),
+			Vector3(0, arrow_width, 0),
+			Vector3(0, arrow_width, arrow_length),
+			Vector3(0, -arrow_width, arrow_length),
+			Vector3(0, -arrow_width, 0),
+			Vector3(0, -arrow_barb, 0)
+		};
 
-			p_gizmo->add_lines(lines, material_arrow, false, color);
+		int arrow_sides = 2;
+
+		Vector<Vector3> lines;
+
+		for (int i = 0; i < arrow_sides; i++) {
+			for (int j = 0; j < arrow_points; j++) {
+				Basis ma(Vector3(0, 0, 1), Math_PI * i / arrow_sides);
+
+				Vector3 v1 = arrow[j] - Vector3(0, 0, arrow_length);
+				Vector3 v2 = arrow[(j + 1) % arrow_points] - Vector3(0, 0, arrow_length);
+
+				lines.push_back(ma.xform(v1));
+				lines.push_back(ma.xform(v2));
+			}
 		}
 
-	} // was portal
+		add_lines(lines, material_arrow, false, color);
+	}
+}
+
+PortalSpatialGizmo::PortalSpatialGizmo(Portal *p_portal) {
+	_portal = p_portal;
+	set_spatial_node(p_portal);
+
+	_color_portal_front = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/portal_front", Color(0.05, 0.05, 1.0, 0.3));
+	_color_portal_back = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/portal_back", Color(1.0, 1.0, 0.0, 0.15));
 }

+ 38 - 6
editor/spatial_editor_gizmos.h

@@ -429,6 +429,23 @@ public:
 	JointSpatialGizmoPlugin();
 };
 
+class Room;
+
+class RoomSpatialGizmo : public EditorSpatialGizmo {
+	GDCLASS(RoomSpatialGizmo, EditorSpatialGizmo);
+
+	Room *_room = nullptr;
+
+public:
+	virtual String get_handle_name(int p_idx) const;
+	virtual Variant get_handle_value(int p_idx);
+	virtual void set_handle(int p_idx, Camera *p_camera, const Point2 &p_point);
+	virtual void commit_handle(int p_idx, const Variant &p_restore, bool p_cancel = false);
+	virtual void redraw();
+
+	RoomSpatialGizmo(Room *p_room = nullptr);
+};
+
 class RoomGizmoPlugin : public EditorSpatialGizmoPlugin {
 	GDCLASS(RoomGizmoPlugin, EditorSpatialGizmoPlugin);
 
@@ -436,12 +453,31 @@ protected:
 	virtual bool has_gizmo(Spatial *p_spatial);
 	String get_name() const;
 	int get_priority() const;
-	void redraw(EditorSpatialGizmo *p_gizmo);
+	Ref<EditorSpatialGizmo> create_gizmo(Spatial *p_spatial);
 
 public:
 	RoomGizmoPlugin();
 };
 
+class Portal;
+
+class PortalSpatialGizmo : public EditorSpatialGizmo {
+	GDCLASS(PortalSpatialGizmo, EditorSpatialGizmo);
+
+	Portal *_portal = nullptr;
+	Color _color_portal_front;
+	Color _color_portal_back;
+
+public:
+	virtual String get_handle_name(int p_idx) const;
+	virtual Variant get_handle_value(int p_idx);
+	virtual void set_handle(int p_idx, Camera *p_camera, const Point2 &p_point);
+	virtual void commit_handle(int p_idx, const Variant &p_restore, bool p_cancel = false);
+	virtual void redraw();
+
+	PortalSpatialGizmo(Portal *p_portal = nullptr);
+};
+
 class PortalGizmoPlugin : public EditorSpatialGizmoPlugin {
 	GDCLASS(PortalGizmoPlugin, EditorSpatialGizmoPlugin);
 
@@ -449,11 +485,7 @@ protected:
 	virtual bool has_gizmo(Spatial *p_spatial);
 	String get_name() const;
 	int get_priority() const;
-	void redraw(EditorSpatialGizmo *p_gizmo);
-
-private:
-	Color _color_portal_front;
-	Color _color_portal_back;
+	Ref<EditorSpatialGizmo> create_gizmo(Spatial *p_spatial);
 
 public:
 	PortalGizmoPlugin();

+ 12 - 0
scene/3d/portal.cpp

@@ -114,6 +114,16 @@ String Portal::get_configuration_warning() const {
 	return warning;
 }
 
+void Portal::set_point(int p_idx, const Vector2 &p_point) {
+	if (p_idx >= _pts_local_raw.size()) {
+		return;
+	}
+
+	_pts_local_raw.set(p_idx, p_point);
+	_sanitize_points();
+	update_gizmo();
+}
+
 void Portal::set_points(const PoolVector<Vector2> &p_points) {
 	_pts_local_raw = p_points;
 	_sanitize_points();
@@ -670,6 +680,8 @@ void Portal::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_points", "points"), &Portal::set_points);
 	ClassDB::bind_method(D_METHOD("get_points"), &Portal::get_points);
 
+	ClassDB::bind_method(D_METHOD("set_point", "index", "position"), &Portal::set_point);
+
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "portal_active"), "set_portal_active", "get_portal_active");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "two_way"), "set_two_way", "is_two_way");
 	ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "linked_room", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Room"), "set_linked_room", "get_linked_room");

+ 5 - 1
scene/3d/portal.h

@@ -47,6 +47,7 @@ class Portal : public Spatial {
 	friend class RoomManager;
 	friend class PortalGizmoPlugin;
 	friend class PortalEditorPlugin;
+	friend class PortalSpatialGizmo;
 
 public:
 	// ui interface .. will have no effect after room conversion
@@ -83,6 +84,9 @@ public:
 	void set_points(const PoolVector<Vector2> &p_points);
 	PoolVector<Vector2> get_points() const;
 
+	// primarily for the gizmo
+	void set_point(int p_idx, const Vector2 &p_point);
+
 	String get_configuration_warning() const;
 
 	Portal();
@@ -105,7 +109,7 @@ private:
 	void flip();
 	void _sanitize_points();
 	void _update_aabb();
-	Vector3 _vec2to3(const Vector2 &p_pt) const { return Vector3(p_pt.x, p_pt.y, 0.0); }
+	static Vector3 _vec2to3(const Vector2 &p_pt) { return Vector3(p_pt.x, p_pt.y, 0.0); }
 	void _sort_verts_clockwise(bool portal_plane_convention, Vector<Vector3> &r_verts);
 	Plane _plane_from_points_newell(const Vector<Vector3> &p_pts);
 	void resolve_links(const LocalVector<Room *, int32_t> &p_rooms, const RID &p_from_room_rid);

+ 14 - 0
scene/3d/room.cpp

@@ -130,6 +130,18 @@ void Room::set_use_default_simplify(bool p_use) {
 	_use_default_simplify = p_use;
 }
 
+void Room::set_point(int p_idx, const Vector3 &p_point) {
+	if (p_idx >= _bound_pts.size()) {
+		return;
+	}
+
+	_bound_pts.set(p_idx, p_point);
+
+#ifdef TOOLS_ENABLED
+	_changed(true);
+#endif
+}
+
 void Room::set_points(const PoolVector<Vector3> &p_points) {
 	_bound_pts = p_points;
 
@@ -273,6 +285,8 @@ void Room::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_points", "points"), &Room::set_points);
 	ClassDB::bind_method(D_METHOD("get_points"), &Room::get_points);
 
+	ClassDB::bind_method(D_METHOD("set_point", "index", "position"), &Room::set_point);
+
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_default_simplify"), "set_use_default_simplify", "get_use_default_simplify");
 	ADD_PROPERTY(PropertyInfo(Variant::REAL, "room_simplify", PROPERTY_HINT_RANGE, "0.0,1.0,0.005"), "set_room_simplify", "get_room_simplify");
 

+ 4 - 0
scene/3d/room.h

@@ -45,6 +45,7 @@ class Room : public Spatial {
 	friend class Portal;
 	friend class RoomGizmoPlugin;
 	friend class RoomEditorPlugin;
+	friend class RoomSpatialGizmo;
 
 	RID _room_rid;
 
@@ -71,6 +72,9 @@ public:
 	void set_points(const PoolVector<Vector3> &p_points);
 	PoolVector<Vector3> get_points() const;
 
+	// primarily for the gizmo
+	void set_point(int p_idx, const Vector3 &p_point);
+
 	// editor only
 	PoolVector<Vector3> generate_points();