Bladeren bron

Add closed property to Line2D

MewPurPur 2 jaren geleden
bovenliggende
commit
e39875a20d
5 gewijzigde bestanden met toevoegingen van 216 en 210 verwijderingen
  1. 9 4
      doc/classes/Line2D.xml
  2. 33 30
      scene/2d/line_2d.cpp
  3. 4 0
      scene/2d/line_2d.h
  4. 169 175
      scene/2d/line_builder.cpp
  5. 1 1
      scene/2d/line_builder.h

+ 9 - 4
doc/classes/Line2D.xml

@@ -63,16 +63,21 @@
 			[b]Note:[/b] [Line2D] is not accelerated by batching when being anti-aliased.
 		</member>
 		<member name="begin_cap_mode" type="int" setter="set_begin_cap_mode" getter="get_begin_cap_mode" enum="Line2D.LineCapMode" default="0">
-			The style of the beginning of the polyline. Use [enum LineCapMode] constants.
+			The style of the beginning of the polyline, if [member closed] is [code]false[/code]. Use [enum LineCapMode] constants.
+		</member>
+		<member name="closed" type="bool" setter="set_closed" getter="is_closed" default="false">
+			If [code]true[/code] and the polyline has more than 2 points, the last point and the first one will be connected by a segment.
+			[b]Note:[/b] The shape of the closing segment is not guaranteed to be seamless if a [member width_curve] is provided.
+			[b]Note:[/b] The joint between the closing segment and the first segment is drawn first and it samples the [member gradient] and the [member width_curve] at the beginning. This is an implementation detail that might change in a future version.
 		</member>
 		<member name="default_color" type="Color" setter="set_default_color" getter="get_default_color" default="Color(1, 1, 1, 1)">
 			The color of the polyline. Will not be used if a gradient is set.
 		</member>
 		<member name="end_cap_mode" type="int" setter="set_end_cap_mode" getter="get_end_cap_mode" enum="Line2D.LineCapMode" default="0">
-			The style of the end of the polyline. Use [enum LineCapMode] constants.
+			The style of the end of the polyline, if [member closed] is [code]false[/code]. Use [enum LineCapMode] constants.
 		</member>
 		<member name="gradient" type="Gradient" setter="set_gradient" getter="get_gradient">
-			The gradient is drawn through the whole line from start to finish. The default color will not be used if a gradient is set.
+			The gradient is drawn through the whole line from start to finish. The [member default_color] will not be used if this property is set.
 		</member>
 		<member name="joint_mode" type="int" setter="set_joint_mode" getter="get_joint_mode" enum="Line2D.LineJointMode" default="0">
 			The style of the connections between segments of the polyline. Use [enum LineJointMode] constants.
@@ -93,7 +98,7 @@
 			The style to render the [member texture] of the polyline. Use [enum LineTextureMode] constants.
 		</member>
 		<member name="width" type="float" setter="set_width" getter="get_width" default="10.0">
-			The polyline's width
+			The polyline's width.
 		</member>
 		<member name="width_curve" type="Curve" setter="set_curve" getter="get_curve">
 			The polyline's width curve. The width of the polyline over its length will be equivalent to the value of the width curve over its domain.

+ 33 - 30
scene/2d/line_2d.cpp

@@ -42,12 +42,12 @@ Rect2 Line2D::_edit_get_rect() const {
 		return Rect2(0, 0, 0, 0);
 	}
 	Vector2 d = Vector2(_width, _width);
-	Rect2 aabb = Rect2(_points[0] - d, 2 * d);
+	Rect2 bounding_rect = Rect2(_points[0] - d, 2 * d);
 	for (int i = 1; i < _points.size(); i++) {
-		aabb.expand_to(_points[i] - d);
-		aabb.expand_to(_points[i] + d);
+		bounding_rect.expand_to(_points[i] - d);
+		bounding_rect.expand_to(_points[i] + d);
 	}
-	return aabb;
+	return bounding_rect;
 }
 
 bool Line2D::_edit_use_rect() const {
@@ -59,7 +59,14 @@ bool Line2D::_edit_is_selected_on_click(const Point2 &p_point, double p_toleranc
 	const Vector2 *points = _points.ptr();
 	for (int i = 0; i < _points.size() - 1; i++) {
 		Vector2 p = Geometry2D::get_closest_point_to_segment(p_point, &points[i]);
-		if (p.distance_to(p_point) <= d) {
+		if (p_point.distance_to(p) <= d) {
+			return true;
+		}
+	}
+	if (_closed && _points.size() > 2) {
+		const Vector2 closing_segment[2] = { points[0], points[_points.size() - 1] };
+		Vector2 p = Geometry2D::get_closest_point_to_segment(p_point, closing_segment);
+		if (p_point.distance_to(p) <= d) {
 			return true;
 		}
 	}
@@ -73,6 +80,15 @@ void Line2D::set_points(const Vector<Vector2> &p_points) {
 	queue_redraw();
 }
 
+void Line2D::set_closed(bool p_closed) {
+	_closed = p_closed;
+	queue_redraw();
+}
+
+bool Line2D::is_closed() const {
+	return _closed;
+}
+
 void Line2D::set_width(float p_width) {
 	if (p_width < 0.0) {
 		p_width = 0.0;
@@ -86,14 +102,12 @@ float Line2D::get_width() const {
 }
 
 void Line2D::set_curve(const Ref<Curve> &p_curve) {
-	// Cleanup previous connection if any
 	if (_curve.is_valid()) {
 		_curve->disconnect_changed(callable_mp(this, &Line2D::_curve_changed));
 	}
 
 	_curve = p_curve;
 
-	// Connect to the curve so the line will update when it is changed
 	if (_curve.is_valid()) {
 		_curve->connect_changed(callable_mp(this, &Line2D::_curve_changed));
 	}
@@ -156,14 +170,12 @@ Color Line2D::get_default_color() const {
 }
 
 void Line2D::set_gradient(const Ref<Gradient> &p_gradient) {
-	// Cleanup previous connection if any
 	if (_gradient.is_valid()) {
 		_gradient->disconnect_changed(callable_mp(this, &Line2D::_gradient_changed));
 	}
 
 	_gradient = p_gradient;
 
-	// Connect to the gradient so the line will update when the Gradient is changed
 	if (_gradient.is_valid()) {
 		_gradient->connect_changed(callable_mp(this, &Line2D::_gradient_changed));
 	}
@@ -264,20 +276,10 @@ void Line2D::_draw() {
 		return;
 	}
 
-	// TODO Is this really needed?
-	// Copy points for faster access
-	Vector<Vector2> points;
-	points.resize(len);
-	{
-		const Vector2 *points_read = _points.ptr();
-		for (int i = 0; i < len; ++i) {
-			points.write[i] = points_read[i];
-		}
-	}
-
 	// TODO Maybe have it as member rather than copying parameters and allocating memory?
 	LineBuilder lb;
-	lb.points = points;
+	lb.points = _points;
+	lb.closed = _closed;
 	lb.default_color = _default_color;
 	lb.gradient = *_gradient;
 	lb.texture_mode = _texture_mode;
@@ -306,13 +308,10 @@ void Line2D::_draw() {
 			lb.uvs, Vector<int>(), Vector<float>(),
 			texture_rid);
 
-	// DEBUG
-	// Draw wireframe
-	//	if(lb.indices.size() % 3 == 0) {
-	//		Color col(0,0,0);
-	//		for(int i = 0; i < lb.indices.size(); i += 3) {
-	//			int vi = lb.indices[i];
-	//			int lbvsize = lb.vertices.size();
+	// DEBUG: Draw wireframe
+	//	if (lb.indices.size() % 3 == 0) {
+	//		Color col(0, 0, 0);
+	//		for (int i = 0; i < lb.indices.size(); i += 3) {
 	//			Vector2 a = lb.vertices[lb.indices[i]];
 	//			Vector2 b = lb.vertices[lb.indices[i+1]];
 	//			Vector2 c = lb.vertices[lb.indices[i+2]];
@@ -320,9 +319,9 @@ void Line2D::_draw() {
 	//			draw_line(b, c, col);
 	//			draw_line(c, a, col);
 	//		}
-	//		for(int i = 0; i < lb.vertices.size(); ++i) {
+	//		for (int i = 0; i < lb.vertices.size(); ++i) {
 	//			Vector2 p = lb.vertices[i];
-	//			draw_rect(Rect2(p.x-1, p.y-1, 2, 2), Color(0,0,0,0.5));
+	//			draw_rect(Rect2(p.x - 1, p.y - 1, 2, 2), Color(0, 0, 0, 0.5));
 	//		}
 	//	}
 }
@@ -350,6 +349,9 @@ void Line2D::_bind_methods() {
 
 	ClassDB::bind_method(D_METHOD("clear_points"), &Line2D::clear_points);
 
+	ClassDB::bind_method(D_METHOD("set_closed", "closed"), &Line2D::set_closed);
+	ClassDB::bind_method(D_METHOD("is_closed"), &Line2D::is_closed);
+
 	ClassDB::bind_method(D_METHOD("set_width", "width"), &Line2D::set_width);
 	ClassDB::bind_method(D_METHOD("get_width"), &Line2D::get_width);
 
@@ -387,6 +389,7 @@ void Line2D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_antialiased"), &Line2D::get_antialiased);
 
 	ADD_PROPERTY(PropertyInfo(Variant::PACKED_VECTOR2_ARRAY, "points"), "set_points", "get_points");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "closed"), "set_closed", "is_closed");
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "width", PROPERTY_HINT_NONE, "suffix:px"), "set_width", "get_width");
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "width_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_curve", "get_curve");
 	ADD_PROPERTY(PropertyInfo(Variant::COLOR, "default_color"), "set_default_color", "get_default_color");

+ 4 - 0
scene/2d/line_2d.h

@@ -76,6 +76,9 @@ public:
 	void add_point(Vector2 pos, int atpos = -1);
 	void remove_point(int i);
 
+	void set_closed(bool p_closed);
+	bool is_closed() const;
+
 	void set_width(float width);
 	float get_width() const;
 
@@ -127,6 +130,7 @@ private:
 	LineJointMode _joint_mode = LINE_JOINT_SHARP;
 	LineCapMode _begin_cap_mode = LINE_CAP_NONE;
 	LineCapMode _end_cap_mode = LINE_CAP_NONE;
+	bool _closed = false;
 	float _width = 10.0;
 	Ref<Curve> _curve;
 	Color _default_color = Color(1, 1, 1);

+ 169 - 175
scene/2d/line_builder.cpp

@@ -30,75 +30,25 @@
 
 #include "line_builder.h"
 
-//----------------------------------------------------------------------------
-// Util
-//----------------------------------------------------------------------------
-
-enum SegmentIntersectionResult {
-	SEGMENT_PARALLEL = 0,
-	SEGMENT_NO_INTERSECT = 1,
-	SEGMENT_INTERSECT = 2
-};
-
-static SegmentIntersectionResult segment_intersection(
-		Vector2 a, Vector2 b, Vector2 c, Vector2 d,
-		Vector2 *out_intersection) {
-	// http://paulbourke.net/geometry/pointlineplane/ <-- Good stuff
-	Vector2 cd = d - c;
-	Vector2 ab = b - a;
-	float div = cd.y * ab.x - cd.x * ab.y;
-
-	if (Math::abs(div) > 0.001f) {
-		float ua = (cd.x * (a.y - c.y) - cd.y * (a.x - c.x)) / div;
-		float ub = (ab.x * (a.y - c.y) - ab.y * (a.x - c.x)) / div;
-		*out_intersection = a + ua * ab;
-		if (ua >= 0.f && ua <= 1.f &&
-				ub >= 0.f && ub <= 1.f) {
-			return SEGMENT_INTERSECT;
-		}
-		return SEGMENT_NO_INTERSECT;
-	}
-
-	return SEGMENT_PARALLEL;
-}
-
-static float calculate_total_distance(const Vector<Vector2> &points) {
-	float d = 0.f;
-	for (int i = 1; i < points.size(); ++i) {
-		d += points[i].distance_to(points[i - 1]);
-	}
-	return d;
-}
-
-static inline Vector2 rotate90(const Vector2 &v) {
-	// Note: the 2D referential is X-right, Y-down
-	return Vector2(v.y, -v.x);
-}
+#include "core/math/geometry_2d.h"
 
+// Utility method.
 static inline Vector2 interpolate(const Rect2 &r, const Vector2 &v) {
 	return Vector2(
 			Math::lerp(r.position.x, r.position.x + r.get_size().x, v.x),
 			Math::lerp(r.position.y, r.position.y + r.get_size().y, v.y));
 }
 
-//----------------------------------------------------------------------------
-// LineBuilder
-//----------------------------------------------------------------------------
-
 LineBuilder::LineBuilder() {
 }
 
-void LineBuilder::clear_output() {
-	vertices.clear();
-	colors.clear();
-	indices.clear();
-	uvs.clear();
-}
-
 void LineBuilder::build() {
-	// Need at least 2 points to draw a line
+	// Need at least 2 points to draw a line, so clear the output and return.
 	if (points.size() < 2) {
-		clear_output();
+		vertices.clear();
+		colors.clear();
+		indices.clear();
+		uvs.clear();
 		return;
 	}
 
@@ -107,14 +57,21 @@ void LineBuilder::build() {
 	const float hw = width / 2.f;
 	const float hw_sq = hw * hw;
 	const float sharp_limit_sq = sharp_limit * sharp_limit;
-	const int len = points.size();
+	const int point_count = points.size();
+	const bool wrap_around = closed && point_count > 2;
+
+	_interpolate_color = gradient != nullptr;
+	const bool retrieve_curve = curve != nullptr;
+	const bool distance_required = _interpolate_color || retrieve_curve ||
+			texture_mode == Line2D::LINE_TEXTURE_TILE ||
+			texture_mode == Line2D::LINE_TEXTURE_STRETCH;
 
 	// Initial values
 
 	Vector2 pos0 = points[0];
 	Vector2 pos1 = points[1];
 	Vector2 f0 = (pos1 - pos0).normalized();
-	Vector2 u0 = rotate90(f0);
+	Vector2 u0 = f0.orthogonal();
 	Vector2 pos_up0 = pos0;
 	Vector2 pos_down0 = pos0;
 
@@ -124,32 +81,37 @@ void LineBuilder::build() {
 	float current_distance0 = 0.f;
 	float current_distance1 = 0.f;
 	float total_distance = 0.f;
+
 	float width_factor = 1.f;
-	_interpolate_color = gradient != nullptr;
-	bool retrieve_curve = curve != nullptr;
-	bool distance_required = _interpolate_color ||
-			retrieve_curve ||
-			texture_mode == Line2D::LINE_TEXTURE_TILE ||
-			texture_mode == Line2D::LINE_TEXTURE_STRETCH;
+	float modified_hw = hw;
+	if (retrieve_curve) {
+		width_factor = curve->sample_baked(0.f);
+		modified_hw = hw * width_factor;
+	}
+
 	if (distance_required) {
-		total_distance = calculate_total_distance(points);
-		//Adjust totalDistance.
-		// The line's outer length will be a little higher due to begin and end caps
-		if (begin_cap_mode == Line2D::LINE_CAP_BOX || begin_cap_mode == Line2D::LINE_CAP_ROUND) {
-			if (retrieve_curve) {
-				total_distance += width * curve->sample_baked(0.f) * 0.5f;
-			} else {
-				total_distance += width * 0.5f;
-			}
+		// Calculate the total distance.
+		for (int i = 1; i < point_count; ++i) {
+			total_distance += points[i].distance_to(points[i - 1]);
 		}
-		if (end_cap_mode == Line2D::LINE_CAP_BOX || end_cap_mode == Line2D::LINE_CAP_ROUND) {
-			if (retrieve_curve) {
-				total_distance += width * curve->sample_baked(1.f) * 0.5f;
-			} else {
-				total_distance += width * 0.5f;
+		if (wrap_around) {
+			total_distance += points[point_count - 1].distance_to(pos0);
+		} else {
+			// Adjust the total distance.
+			// The line's outer length may be a little higher due to the end caps.
+			if (begin_cap_mode == Line2D::LINE_CAP_BOX || begin_cap_mode == Line2D::LINE_CAP_ROUND) {
+				total_distance += modified_hw;
+			}
+			if (end_cap_mode == Line2D::LINE_CAP_BOX || end_cap_mode == Line2D::LINE_CAP_ROUND) {
+				if (retrieve_curve) {
+					total_distance += hw * curve->sample_baked(1.f);
+				} else {
+					total_distance += hw;
+				}
 			}
 		}
 	}
+
 	if (_interpolate_color) {
 		color0 = gradient->get_color(0);
 	} else {
@@ -159,34 +121,31 @@ void LineBuilder::build() {
 	float uvx0 = 0.f;
 	float uvx1 = 0.f;
 
-	if (retrieve_curve) {
-		width_factor = curve->sample_baked(0.f);
-	}
-
-	pos_up0 += u0 * hw * width_factor;
-	pos_down0 -= u0 * hw * width_factor;
+	pos_up0 += u0 * modified_hw;
+	pos_down0 -= u0 * modified_hw;
 
 	// Begin cap
-	if (begin_cap_mode == Line2D::LINE_CAP_BOX) {
-		// Push back first vertices a little bit
-		pos_up0 -= f0 * hw * width_factor;
-		pos_down0 -= f0 * hw * width_factor;
-
-		current_distance0 += hw * width_factor;
-		current_distance1 = current_distance0;
-	} else if (begin_cap_mode == Line2D::LINE_CAP_ROUND) {
-		if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
-			uvx0 = width_factor * 0.5f / tile_aspect;
-		} else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
-			uvx0 = width * width_factor / total_distance;
+	if (!wrap_around) {
+		if (begin_cap_mode == Line2D::LINE_CAP_BOX) {
+			// Push back first vertices a little bit.
+			pos_up0 -= f0 * modified_hw;
+			pos_down0 -= f0 * modified_hw;
+
+			current_distance0 += modified_hw;
+			current_distance1 = current_distance0;
+		} else if (begin_cap_mode == Line2D::LINE_CAP_ROUND) {
+			if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
+				uvx0 = width_factor * 0.5f / tile_aspect;
+			} else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
+				uvx0 = width * width_factor / total_distance;
+			}
+			new_arc(pos0, pos_up0 - pos0, -Math_PI, color0, Rect2(0.f, 0.f, uvx0 * 2, 1.f));
+			current_distance0 += modified_hw;
+			current_distance1 = current_distance0;
 		}
-		new_arc(pos0, pos_up0 - pos0, -Math_PI, color0, Rect2(0.f, 0.f, uvx0 * 2, 1.f));
-		current_distance0 += hw * width_factor;
-		current_distance1 = current_distance0;
+		strip_begin(pos_up0, pos_down0, color0, uvx0);
 	}
 
-	strip_begin(pos_up0, pos_down0, color0, uvx0);
-
 	/*
 	 *  pos_up0 ------------- pos_up1 --------------------
 	 *     |                     |
@@ -200,19 +159,30 @@ void LineBuilder::build() {
 	// http://labs.hyperandroid.com/tag/opengl-lines
 	// (not the same implementation but visuals help a lot)
 
+	// If the polyline wraps around, then draw two more segments with joints:
+	// The last one, which should normally end with an end cap, and the one that matches the end and the beginning.
+	int segments_count = wrap_around ? point_count : (point_count - 2);
+	// The wraparound case starts with a "fake walk" from the end of the polyline
+	// to its beginning, so that its first joint is correct, without drawing anything.
+	int first_point = wrap_around ? -1 : 1;
+
+	// If the line wraps around, these variables will be used for the final segment.
+	Vector2 first_pos_up, first_pos_down;
+	bool is_first_joint_sharp = false;
+
 	// For each additional segment
-	for (int i = 1; i < len - 1; ++i) {
-		pos1 = points[i];
-		Vector2 pos2 = points[i + 1];
+	for (int i = first_point; i <= segments_count; ++i) {
+		pos1 = points[(i == -1) ? point_count - 1 : i % point_count]; // First point.
+		Vector2 pos2 = points[(i + 1) % point_count]; // Second point.
 
 		Vector2 f1 = (pos2 - pos1).normalized();
-		Vector2 u1 = rotate90(f1);
+		Vector2 u1 = f1.orthogonal();
 
-		// Determine joint orientation
-		const float dp = u0.dot(f1);
+		// Determine joint orientation.
+		float dp = u0.dot(f1);
 		const Orientation orientation = (dp > 0.f ? UP : DOWN);
 
-		if (distance_required) {
+		if (distance_required && i >= 1) {
 			current_distance1 += pos0.distance_to(pos1);
 		}
 		if (_interpolate_color) {
@@ -220,15 +190,14 @@ void LineBuilder::build() {
 		}
 		if (retrieve_curve) {
 			width_factor = curve->sample_baked(current_distance1 / total_distance);
+			modified_hw = hw * width_factor;
 		}
 
-		Vector2 inner_normal0, inner_normal1;
-		if (orientation == UP) {
-			inner_normal0 = u0 * hw * width_factor;
-			inner_normal1 = u1 * hw * width_factor;
-		} else {
-			inner_normal0 = -u0 * hw * width_factor;
-			inner_normal1 = -u1 * hw * width_factor;
+		Vector2 inner_normal0 = u0 * modified_hw;
+		Vector2 inner_normal1 = u1 * modified_hw;
+		if (orientation == DOWN) {
+			inner_normal0 = -inner_normal0;
+			inner_normal1 = -inner_normal1;
 		}
 
 		/*
@@ -245,18 +214,18 @@ void LineBuilder::build() {
 		 *                          /
 		 */
 
-		// Find inner intersection at the joint
+		// Find inner intersection at the joint.
 		Vector2 corner_pos_in, corner_pos_out;
-		SegmentIntersectionResult intersection_result = segment_intersection(
+		bool is_intersecting = Geometry2D::segment_intersects_segment(
 				pos0 + inner_normal0, pos1 + inner_normal0,
 				pos1 + inner_normal1, pos2 + inner_normal1,
 				&corner_pos_in);
 
-		if (intersection_result == SEGMENT_INTERSECT) {
-			// Inner parts of the segments intersect
+		if (is_intersecting) {
+			// Inner parts of the segments intersect.
 			corner_pos_out = 2.f * pos1 - corner_pos_in;
 		} else {
-			// No intersection, segments are either parallel or too sharp
+			// No intersection, segments are too sharp or they overlap.
 			corner_pos_in = pos1 + inner_normal0;
 			corner_pos_out = pos1 - inner_normal0;
 		}
@@ -273,8 +242,8 @@ void LineBuilder::build() {
 		Line2D::LineJointMode current_joint_mode = joint_mode;
 
 		Vector2 pos_up1, pos_down1;
-		if (intersection_result == SEGMENT_INTERSECT) {
-			// Fallback on bevel if sharp angle is too high (because it would produce very long miters)
+		if (is_intersecting) {
+			// Fallback on bevel if sharp angle is too high (because it would produce very long miters).
 			float width_factor_sq = width_factor * width_factor;
 			if (current_joint_mode == Line2D::LINE_JOINT_SHARP && corner_pos_out.distance_squared_to(pos1) / (hw_sq * width_factor_sq) > sharp_limit_sq) {
 				current_joint_mode = Line2D::LINE_JOINT_BEVEL;
@@ -288,57 +257,78 @@ void LineBuilder::build() {
 				// Bevel or round
 				if (orientation == UP) {
 					pos_up1 = corner_pos_up;
-					pos_down1 = pos1 - u0 * hw * width_factor;
+					pos_down1 = pos1 - u0 * modified_hw;
 				} else {
-					pos_up1 = pos1 + u0 * hw * width_factor;
+					pos_up1 = pos1 + u0 * modified_hw;
 					pos_down1 = corner_pos_down;
 				}
 			}
 		} else {
 			// No intersection: fallback
 			if (current_joint_mode == Line2D::LINE_JOINT_SHARP) {
-				// There is no fallback implementation for LINE_JOINT_SHARP so switch to the LINE_JOINT_BEVEL
+				// There is no fallback implementation for LINE_JOINT_SHARP so switch to the LINE_JOINT_BEVEL.
 				current_joint_mode = Line2D::LINE_JOINT_BEVEL;
 			}
 			pos_up1 = corner_pos_up;
 			pos_down1 = corner_pos_down;
 		}
 
-		// Add current line body quad
-		// Triangles are clockwise
+		// Triangles are clockwise.
 		if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
 			uvx1 = current_distance1 / (width * tile_aspect);
 		} else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
 			uvx1 = current_distance1 / total_distance;
 		}
 
-		strip_add_quad(pos_up1, pos_down1, color1, uvx1);
-
-		// Swap vars for use in the next line
+		// Swap vars for use in the next line.
 		color0 = color1;
 		u0 = u1;
 		f0 = f1;
 		pos0 = pos1;
-		if (intersection_result == SEGMENT_INTERSECT) {
+		if (is_intersecting) {
 			if (current_joint_mode == Line2D::LINE_JOINT_SHARP) {
 				pos_up0 = pos_up1;
 				pos_down0 = pos_down1;
 			} else {
 				if (orientation == UP) {
 					pos_up0 = corner_pos_up;
-					pos_down0 = pos1 - u1 * hw * width_factor;
+					pos_down0 = pos1 - u1 * modified_hw;
 				} else {
-					pos_up0 = pos1 + u1 * hw * width_factor;
+					pos_up0 = pos1 + u1 * modified_hw;
 					pos_down0 = corner_pos_down;
 				}
 			}
 		} else {
-			pos_up0 = pos1 + u1 * hw * width_factor;
-			pos_down0 = pos1 - u1 * hw * width_factor;
+			pos_up0 = pos1 + u1 * modified_hw;
+			pos_down0 = pos1 - u1 * modified_hw;
+		}
+
+		// End the "fake pass" in the closed line case before the drawing subroutine.
+		if (i == -1) {
+			continue;
+		}
+
+		// For wrap-around polylines, store some kind of start positions of the first joint for the final connection.
+		if (wrap_around && i == 0) {
+			Vector2 first_pos_center = (pos_up1 + pos_down1) / 2;
+			float lerp_factor = 1.0 / width_factor;
+			first_pos_up = first_pos_center.lerp(pos_up1, lerp_factor);
+			first_pos_down = first_pos_center.lerp(pos_down1, lerp_factor);
+			is_first_joint_sharp = current_joint_mode == Line2D::LINE_JOINT_SHARP;
+		}
+
+		// Add current line body quad.
+		if (wrap_around && retrieve_curve && !is_first_joint_sharp && i == segments_count) {
+			// If the width curve is not seamless, we might need to fetch the line's start points to use them for the final connection.
+			Vector2 first_pos_center = (first_pos_up + first_pos_down) / 2;
+			strip_add_quad(first_pos_center.lerp(first_pos_up, width_factor), first_pos_center.lerp(first_pos_down, width_factor), color1, uvx1);
+			return;
+		} else {
+			strip_add_quad(pos_up1, pos_down1, color1, uvx1);
 		}
-		// From this point, bu0 and bd0 concern the next segment
 
-		// Add joint geometry
+		// From this point, bu0 and bd0 concern the next segment.
+		// Add joint geometry.
 		if (current_joint_mode != Line2D::LINE_JOINT_SHARP) {
 			/* ________________ cbegin
 			 *               / \
@@ -358,64 +348,68 @@ void LineBuilder::build() {
 				cend = pos_up0;
 			}
 
-			if (current_joint_mode == Line2D::LINE_JOINT_BEVEL) {
+			if (current_joint_mode == Line2D::LINE_JOINT_BEVEL && !(wrap_around && i == segments_count)) {
 				strip_add_tri(cend, orientation);
-			} else if (current_joint_mode == Line2D::LINE_JOINT_ROUND) {
+			} else if (current_joint_mode == Line2D::LINE_JOINT_ROUND && !(wrap_around && i == segments_count)) {
 				Vector2 vbegin = cbegin - pos1;
 				Vector2 vend = cend - pos1;
 				strip_add_arc(pos1, vbegin.angle_to(vend), orientation);
 			}
 
-			if (intersection_result != SEGMENT_INTERSECT) {
+			if (!is_intersecting) {
 				// In this case the joint is too corrupted to be re-used,
 				// start again the strip with fallback points
 				strip_begin(pos_up0, pos_down0, color1, uvx1);
 			}
 		}
 	}
-	// Last (or only) segment
-	pos1 = points[points.size() - 1];
 
-	if (distance_required) {
-		current_distance1 += pos0.distance_to(pos1);
-	}
-	if (_interpolate_color) {
-		color1 = gradient->get_color(gradient->get_point_count() - 1);
-	}
-	if (retrieve_curve) {
-		width_factor = curve->sample_baked(1.f);
-	}
+	// Draw the last (or only) segment, with its end cap logic.
+	if (!wrap_around) {
+		pos1 = points[point_count - 1];
 
-	Vector2 pos_up1 = pos1 + u0 * hw * width_factor;
-	Vector2 pos_down1 = pos1 - u0 * hw * width_factor;
+		if (distance_required) {
+			current_distance1 += pos0.distance_to(pos1);
+		}
+		if (_interpolate_color) {
+			color1 = gradient->get_color(gradient->get_point_count() - 1);
+		}
+		if (retrieve_curve) {
+			width_factor = curve->sample_baked(1.f);
+			modified_hw = hw * width_factor;
+		}
 
-	// End cap (box)
-	if (end_cap_mode == Line2D::LINE_CAP_BOX) {
-		pos_up1 += f0 * hw * width_factor;
-		pos_down1 += f0 * hw * width_factor;
+		Vector2 pos_up1 = pos1 + u0 * modified_hw;
+		Vector2 pos_down1 = pos1 - u0 * modified_hw;
 
-		current_distance1 += hw * width_factor;
-	}
+		// Add extra distance for a box end cap.
+		if (end_cap_mode == Line2D::LINE_CAP_BOX) {
+			pos_up1 += f0 * modified_hw;
+			pos_down1 += f0 * modified_hw;
 
-	if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
-		uvx1 = current_distance1 / (width * tile_aspect);
-	} else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
-		uvx1 = current_distance1 / total_distance;
-	}
-
-	strip_add_quad(pos_up1, pos_down1, color1, uvx1);
+			current_distance1 += modified_hw;
+		}
 
-	// End cap (round)
-	if (end_cap_mode == Line2D::LINE_CAP_ROUND) {
-		// Note: color is not used in case we don't interpolate...
-		Color color = _interpolate_color ? gradient->get_color(gradient->get_point_count() - 1) : Color(0, 0, 0);
-		float dist = 0;
 		if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
-			dist = width_factor / tile_aspect;
+			uvx1 = current_distance1 / (width * tile_aspect);
 		} else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
-			dist = width * width_factor / total_distance;
+			uvx1 = current_distance1 / total_distance;
+		}
+
+		strip_add_quad(pos_up1, pos_down1, color1, uvx1);
+
+		// Custom drawing for a round end cap.
+		if (end_cap_mode == Line2D::LINE_CAP_ROUND) {
+			// Note: color is not used in case we don't interpolate.
+			Color color = _interpolate_color ? gradient->get_color(gradient->get_point_count() - 1) : Color(0, 0, 0);
+			float dist = 0;
+			if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
+				dist = width_factor / tile_aspect;
+			} else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
+				dist = width * width_factor / total_distance;
+			}
+			new_arc(pos1, pos_up1 - pos1, Math_PI, color, Rect2(uvx1 - 0.5f * dist, 0.f, dist, 1.f));
 		}
-		new_arc(pos1, pos_up1 - pos1, Math_PI, color, Rect2(uvx1 - 0.5f * dist, 0.f, dist, 1.f));
 	}
 }
 

+ 1 - 1
scene/2d/line_builder.h

@@ -41,6 +41,7 @@ public:
 	Line2D::LineJointMode joint_mode = Line2D::LINE_JOINT_SHARP;
 	Line2D::LineCapMode begin_cap_mode = Line2D::LINE_CAP_NONE;
 	Line2D::LineCapMode end_cap_mode = Line2D::LINE_CAP_NONE;
+	bool closed = false;
 	float width = 10.0;
 	Curve *curve = nullptr;
 	Color default_color = Color(0.4, 0.5, 1);
@@ -61,7 +62,6 @@ public:
 	LineBuilder();
 
 	void build();
-	void clear_output();
 
 private:
 	enum Orientation {