Repository: TheDuckCow/godot-road-generator
Pull request #241: Add double yellow alignment option
Author: login: antonkurkin
Created: 2025-05-03T23:31:26Z | Merged: 2025-05-07T06:16:00Z

Create Road Point alignment property

Modes supported:
1. road point is in the center of the road (original)
2. road point is on double yellow line. for one-way roads new alignment is on the leftmost lane

make alignment attribute visible and registered for copy (either in plugin GUI and RoadPoint copy method)
update lanes, edges and geometry to use offsets, according to the alignment property (including mixed alignment in segment's road points)
update custom gui handles for lanes to show arrows with respect to alignment attribute

related to https://github.com/TheDuckCow/godot-road-generator/issues/229

From my work, I see that it has to be a road-point centered feature, as it makes no sense if there are no FORWARD or no REVERSE lanes, makes the road left or right aligned and demanding that all such roads should be in different containers is probably not great in general
I think it also could be useful in some edge cases and there won't be problems with connections. so I'll try if it will work with road points of different alignments first, doesn't look like it should be too complicated

here is how it looks
[Kooha-2025-05-04-01-19-30.webm](https://github.com/user-attachments/assets/59494f27-1f71-4334-aa2f-d4384ee8ee4d)

and here are some issues with the intersection demo (fixed with support for mixed alignment)
![Screenshot From 2025-05-04 01-36-47](https://github.com/user-attachments/assets/acd5da5d-77ee-4588-8882-9f75ccb12c6f)




Diff:
diff --git a/addons/road-generator/nodes/road_point.gd b/addons/road-generator/nodes/road_point.gd
index 6306379e..a8f9751c 100644
--- a/addons/road-generator/nodes/road_point.gd
+++ b/addons/road-generator/nodes/road_point.gd
@@ -45,6 +45,11 @@ enum PointInit {
 	PRIOR,
 }
 
+enum Alignment {
+	GEOMETRIC,  ## Aligns equal amount of road to the left and right of the RoadPoint
+	DIVIDER,  ## Ensures the lane direction dividing line is aligned to the RoadPoint
+}
+
 const UI_TIMEOUT = 50 # Time in ms to delay further refresh updates.
 const COLOR_YELLOW = Color(0.7, 0.7, 0,7)
 const COLOR_RED = Color(0.7, 0.3, 0.3)
@@ -117,6 +122,8 @@ const SEG_DIST_MULT: float = 8.0 # How many road widths apart to add next RoadPo
 ## Use positive x-values to widen the road even further beyond the shoulder width.
 @export var gutter_profile := Vector2(2.0, -0.5): get = _get_profile, set = _set_profile
 
+## Determines how the geometry will center itself around the RoadPoint origin
+@export var alignment: Alignment: get = _get_alignment, set = _set_alignment
 
 # ------------------------------------------------------------------------------
 @export_group("Internal data")
@@ -132,7 +139,6 @@ const SEG_DIST_MULT: float = 8.0 # How many road widths apart to add next RoadPo
 ## Path to next [RoadPoint], relative to this [RoadPoint] itself.
 @export var next_pt_init: NodePath: get = _get_next_pt_init, set = _set_next_pt_init
 
-
 var rev_width_mag := -8.0
 var fwd_width_mag := 8.0
 # Ultimate assignment if any export path specified
@@ -354,6 +360,18 @@ func _set_create_geo(value: bool) -> void:
 		emit_transform()
 
 
+func _get_alignment() -> Alignment:
+	return alignment
+
+
+func _set_alignment(value: Alignment) -> void:
+	if value == alignment:
+		return
+	alignment = value
+	if not is_instance_valid(container):
+		return  # Might not be initialized yet.
+	emit_transform()
+
 # ------------------------------------------------------------------------------
 # Editor interactions
 # ------------------------------------------------------------------------------
@@ -667,6 +685,7 @@ func copy_settings_from(ref_road_point: RoadPoint, copy_transform: bool = true)
 	gutter_profile.y = ref_road_point.gutter_profile.y
 	create_geo = ref_road_point.create_geo
 	_last_update_ms = ref_road_point._last_update_ms
+	alignment = ref_road_point.alignment
 
 	if copy_transform:
 		prior_mag = ref_road_point.prior_mag
diff --git a/addons/road-generator/nodes/road_segment.gd b/addons/road-generator/nodes/road_segment.gd
index 41bfbcc6..e526f466 100644
--- a/addons/road-generator/nodes/road_segment.gd
+++ b/addons/road-generator/nodes/road_segment.gd
@@ -17,6 +17,9 @@ const EDGE_R_NAME = "edge_R" ## Name of reverse lane edge curve
 const EDGE_F_NAME = "edge_F" ## Name of forward lane edge curve
 const EDGE_C_NAME = "edge_C" ## Name of road center (direction divider) edge curve
 
+## Lookup for lane texture multiplier - corresponds to RoadPoint.LaneType enum
+const uv_mul = [7, 0, 1, 2, 3, 4, 5, 6, 7, 7]
+
 signal seg_ready(road_segment)
 
 @export var start_init: NodePath: get = _init_start_get, set = _init_start_set
@@ -49,6 +52,18 @@ var _end_flip: bool = false
 var _start_flip_mult: int = 1
 var _end_flip_mult: int = 1
 
+## For iteration on values related to Near(start) or Far(end) points of a segment
+enum NearFar {
+	NEAR,
+	FAR
+}
+
+## For iteration on values related to Left or Right sides of a segment
+enum LeftRight {
+	LEFT,
+	RIGHT
+}
+
 
 # ------------------------------------------------------------------------------
 # Setup and export setter/getters
@@ -217,16 +232,31 @@ func generate_edge_curves():
 		return
 
 	var _par = get_parent()
-	var start_half_width: float = len(start_point.lanes) * start_point.lane_width * 0.5
-	var end_half_width: float = len(end_point.lanes) * end_point.lane_width * 0.5
+
+	var start_offset_R
+	var start_offset_F
+	var end_offset_R
+	var end_offset_F
+	if start_point.alignment == RoadPoint.Alignment.GEOMETRIC:
+		var start_half_width: float = len(start_point.lanes) * start_point.lane_width * 0.5
+		start_offset_R = start_half_width
+		start_offset_F = start_half_width
+	else:
+		assert( start_point.alignment == RoadPoint.Alignment.DIVIDER )
+		start_offset_R = start_point.get_rev_lane_count() * start_point.lane_width
+		start_offset_F = start_point.get_fwd_lane_count() * start_point.lane_width
+	if end_point.alignment == RoadPoint.Alignment.GEOMETRIC:
+		var end_half_width: float = len(end_point.lanes) * end_point.lane_width * 0.5
+		end_offset_R = end_half_width
+		end_offset_F = end_half_width
+	else:
+		assert( end_point.alignment == RoadPoint.Alignment.DIVIDER )
+		end_offset_R = end_point.get_rev_lane_count() * end_point.lane_width
+		end_offset_F = end_point.get_fwd_lane_count() * end_point.lane_width
 
 	# Add edge curves
 	var edge_R: Path3D = _par.get_node_or_null(EDGE_R_NAME)
 	var edge_F: Path3D = _par.get_node_or_null(EDGE_F_NAME)
-	var start_offset_R := start_half_width
-	var start_offset_F := start_half_width
-	var end_offset_R := end_half_width
-	var end_offset_F := end_half_width
 	var extra_offset: float = 0.0
 	start_offset_R += start_point.shoulder_width_r + start_point.gutter_profile[0] + extra_offset
 	start_offset_F += start_point.shoulder_width_l + start_point.gutter_profile[0] + extra_offset
@@ -253,10 +283,8 @@ func generate_edge_curves():
 
 	# Add center curve
 	var edge_C: Path3D = _par.get_node_or_null(EDGE_C_NAME)
-	var start_width_R: float = start_point.get_rev_lane_count() * start_point.lane_width
-	var end_width_R: float = end_point.get_rev_lane_count() * end_point.lane_width
-	var start_offset_C: float = start_width_R - start_half_width
-	var end_offset_C: float = end_width_R - end_half_width
+	var start_offset_C: float = 0
+	var end_offset_C: float = 0
 
 	if edge_C == null or not is_instance_valid(edge_C):
 		edge_C = Path3D.new()
@@ -284,12 +312,24 @@ func generate_lane_segments(_debug: bool = false) -> bool:
 		_matched_lanes = self._match_lanes()
 	if len(_matched_lanes) == 0:
 		return false
+	
+	var start_lane_offset
+	var end_lane_offset
+	if start_point.alignment == RoadPoint.Alignment.DIVIDER:
+		start_lane_offset = start_point.get_rev_lane_count()
+	else:
+		assert( start_point.alignment == RoadPoint.Alignment.GEOMETRIC )
+		start_lane_offset = len(start_point.lanes) / 2.0
 
-	var any_generated = false
 	var manager:RoadManager = container.get_manager()
+	if end_point.alignment == RoadPoint.Alignment.DIVIDER:
+		end_lane_offset = end_point.get_rev_lane_count()
+	else:
+		assert( end_point.alignment == RoadPoint.Alignment.GEOMETRIC )
+		end_lane_offset = len(end_point.lanes) / 2.0
 
-	var start_offset = len(start_point.lanes) / 2.0 * start_point.lane_width - start_point.lane_width/2.0
-	var end_offset = len(end_point.lanes) / 2.0 * end_point.lane_width - end_point.lane_width/2.0
+	var start_offset = (start_lane_offset - 0.5) * start_point.lane_width
+	var end_offset = (end_lane_offset - 0.5) * end_point.lane_width
 
 	# Tracker used during the loop, to sum offset to apply.
 	var lanes_added := 0
@@ -385,13 +425,12 @@ func generate_lane_segments(_debug: bool = false) -> bool:
 				new_ln.lane_right = new_ln.get_path_to(last_ln)
 
 		# Assign that it was a success.
-		any_generated = true
 		lanes_added += 1
 		last_ln = new_ln # For the next loop iteration.
 		last_ln_reverse = new_ln_reverse
 	clear_lane_segments(active_lanes)
 
-	return any_generated
+	return lanes_added > 0
 
 
 ## Offsets a destination curve from a source curve by a specified distance.
@@ -624,7 +663,7 @@ func _rebuild():
 
 func _update_curve():
 	curve.clear_points()
-	curve.bake_interval = density # Specing in meters between loops.
+	curve.bake_interval = density # Spacing in meters between loops.
 	# path.transform.origin = Vector3.ZERO
 	# path.transform.scaled(Vector3.ONE)
 	# path.transform. clear rotation.
@@ -846,253 +885,183 @@ func _insert_geo_loop(
 		lane_uvs_length: Array,
 		per_loop_uv_size: float,
 		uv_width: float):
+	assert (loop < loops)
+	
 	# One loop = row of quads left to right across the road, spanning lanes.
-	var offset_s = float(loop) / float(loops)
-	var offset_e = float(loop + 1) / float(loops)
-
-	# Apply ease in and out across all attributes.
-	var offset_s_ease = ease(offset_s, smooth_amount)
-	var offset_e_ease = ease(offset_e, smooth_amount)
-
-	#if len(start_point.lanes) == len(end_point.lanes):
-	var start_loop:Vector3
-	var start_basis:Vector3
-	var end_loop:Vector3
-	var end_basis:Vector3
-	start_loop = curve.sample_baked(offset_s * clength)
-	start_basis = _normal_for_offset(curve, offset_s)
-	end_loop = curve.sample_baked(offset_e * clength)
-	end_basis = _normal_for_offset(curve, offset_e)
+	var offset = [float(loop) / float(loops), float(loop + 1) / float(loops)]
+	var point = [start_point, end_point]
+
+	var lane_offset = []
+	for nf in NearFar.values():
+		lane_offset.append(len(point[nf].lanes) / 2.0)
+	if start_point.alignment == RoadPoint.Alignment.DIVIDER || \
+		end_point.alignment == RoadPoint.Alignment.DIVIDER:
+		var nf_reverse = [0, 0]
+		for l in lanes:
+			if l[1] == RoadPoint.LaneDir.REVERSE:
+				if l[0] == RoadPoint.LaneType.TRANSITION_ADD:
+					nf_reverse[NearFar.FAR] += 1
+				elif l[0] == RoadPoint.LaneType.TRANSITION_REM:
+					nf_reverse[NearFar.NEAR] += 1
+				elif l[0] != RoadPoint.LaneType.SHOULDER:
+					nf_reverse[NearFar.NEAR] += 1
+					nf_reverse[NearFar.FAR] += 1
+			else:
+				assert (l[1] == RoadPoint.LaneDir.FORWARD)
+		for nf in NearFar.values():
+			if point[nf].alignment == RoadPoint.Alignment.DIVIDER:
+				lane_offset[nf] = nf_reverse[nf]
+
+	var nf_loop = [null, null]
+	var nf_basis = [null, null]
+	var nf_width = [null, null]
+	var add_width = [null, null]
+	var rem_width = [null, null]
+
+	var width_offset = [[null, null], [null, null]] #width_offset[LeftRight][NearFar]
+
+	for nf in NearFar.values():
+		# Apply ease in and out across all attributes.
+		var offset_ease = ease(offset[nf], smooth_amount)
+		nf_loop[nf] = curve.sample_baked(offset[nf] * clength)
+		nf_basis[nf] = _normal_for_offset(curve, offset[nf])
+
+		# Calculate lane widths
+		nf_width[nf] = lerp(start_point.lane_width, end_point.lane_width, offset_ease)
+		add_width[nf] = lerp(0.0, end_point.lane_width, offset_ease)
+		rem_width[nf] = lerp(start_point.lane_width, 0.0, offset_ease)
+		# Sum the lane widths and get position of left edge
+		width_offset[LeftRight.LEFT][nf] = lerp(
+				lane_offset[NearFar.NEAR] * start_point.lane_width,
+				lane_offset[NearFar.FAR] * end_point.lane_width,
+				offset_ease
+		)
+		width_offset[LeftRight.RIGHT][nf] = -width_offset[LeftRight.LEFT][nf]
 
 	#print("\tRunning loop %s: %s to %s; Start: %s,%s, end: %s,%s" % [
-	#	loop, offset_s, offset_e, start_loop, start_basis, end_loop, end_basis
+	#	loop, offset[NearFar.NEAR], offset[NearFar.FAR], nf_loop[NearFar.NEAR], nf_basis[NearFar.NEAR], nf_loop[NearFar.FAR], nf_basis[NearFar.FAR]
 	#])
 
-	# Calculate lane widths
-	var near_width = lerp(start_point.lane_width, end_point.lane_width, offset_s_ease)
-	var near_add_width = lerp(0.0, end_point.lane_width, offset_s_ease)
-	var near_rem_width = lerp(start_point.lane_width, 0.0, offset_s_ease)
-	var far_width = lerp(start_point.lane_width, end_point.lane_width, offset_e_ease)
-	var far_add_width = lerp(0.0, end_point.lane_width, offset_e_ease)
-	var far_rem_width = lerp(start_point.lane_width, 0.0, offset_e_ease)
-
-	# Sum the lane widths and get position of left edge
-	var near_width_offset
-	var far_width_offset
-
-	near_width_offset = -lerp(
-			len(start_point.lanes) * start_point.lane_width,
-			len(end_point.lanes) * end_point.lane_width,
-			offset_s_ease
-	) / 2.0
-	far_width_offset = -lerp(
-			len(start_point.lanes) * start_point.lane_width,
-			len(end_point.lanes) * end_point.lane_width,
-			offset_e_ease
-	) / 2.0
-
 	for i in range(lane_count):
 		# Create the contents of a single lane / quad within this quad loop.
-		var lane_offset_s = near_width_offset * start_basis
-		var lane_offset_e = far_width_offset * end_basis
-		var lane_near_width
-		var lane_far_width
-
-		# Set lane width for current lane type
-		if lanes[i][0] == RoadPoint.LaneType.TRANSITION_ADD:
-			lane_near_width = near_add_width
-			lane_far_width = far_add_width
-		elif lanes[i][0] == RoadPoint.LaneType.TRANSITION_REM:
-			lane_near_width = near_rem_width
-			lane_far_width = far_rem_width
-		else:
-			lane_near_width = near_width
-			lane_far_width = far_width
-
-		near_width_offset += lane_near_width
-		far_width_offset += lane_far_width
+		var lane_offset_nf = [0, 0]
+		var lane_width = [0, 0]
+		for nf in NearFar.values():
+			lane_offset_nf[nf] = width_offset[LeftRight.RIGHT][nf] * nf_basis[nf]
+
+			# Set lane width for current lane type
+			if lanes[i][0] == RoadPoint.LaneType.TRANSITION_ADD:
+				lane_width[nf] = add_width[nf]
+			elif lanes[i][0] == RoadPoint.LaneType.TRANSITION_REM:
+				lane_width[nf] = rem_width[nf]
+			else:
+				lane_width[nf] = nf_width[nf]
 
 		# Assume the start and end lanes are the same for now.
 		var uv_l:float # the left edge of the uv for this lane.
 		var uv_r:float
-		match lanes[i][0]:
-			RoadPoint.LaneType.NO_MARKING:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.SHOULDER:
-				uv_l = uv_width * 0
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.SLOW:
-				uv_l = uv_width * 1
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.MIDDLE:
-				uv_l = uv_width * 2
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.FAST:
-				uv_l = uv_width * 3
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.TWO_WAY:
-				# Flipped
-				uv_r = uv_width * 4
-				uv_l = uv_r + uv_width
-			RoadPoint.LaneType.ONE_WAY:
-				# Flipped
-				uv_r = uv_width * 5
-				uv_l = uv_r + uv_width
-			RoadPoint.LaneType.SINGLE_LINE:
-				uv_l = uv_width * 6
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.TRANSITION_ADD, RoadPoint.LaneType.TRANSITION_REM:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width - 0.002
-			_:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width
-		if lanes[i][1] == RoadPoint.LaneDir.REVERSE:
+		assert (len(uv_mul) == len(RoadPoint.LaneType.values()))
+		uv_l = uv_width * uv_mul[lanes[i][0]]
+		uv_r = uv_l + uv_width
+		if lanes[i][0] == RoadPoint.LaneType.TRANSITION_ADD || lanes[i][0] == RoadPoint.LaneType.TRANSITION_REM:
+			uv_r -= 0.002
+		var flipped = (lanes[i][0] == RoadPoint.LaneType.TWO_WAY || lanes[i][0] == RoadPoint.LaneType.ONE_WAY)
+		if (lanes[i][1] == RoadPoint.LaneDir.REVERSE) != flipped:
 			var tmp = uv_r
 			uv_r = uv_l
 			uv_l = tmp
 
 		# uv offset continuation for this lane.
-		var uv_y_start = lane_uvs_length[i]
-		var uv_y_end = lane_uvs_length[i] + per_loop_uv_size
-		lane_uvs_length[i] = uv_y_end # For next loop to use.
+		var uv_y = [lane_uvs_length[i], lane_uvs_length[i] + per_loop_uv_size]
+		lane_uvs_length[i] = uv_y[NearFar.FAR] # For next loop to use.
 		#print("Seg: %s, lane:%s, uv %s-%s" % [
-		#	self.name, loop, uv_y_start, uv_y_end])
+		#	self.name, loop, uv_y[NearFar.NEAR], uv_y[NearFar.FAR]])
 
 		# Prepare attributes for add_vertex.
 		# Long edge towards origin, p1
-		#st.add_normal(Vector3(0, 1, 0))
-		quad(
-			st,
-			[
-				Vector2(uv_l, uv_y_end),
-				Vector2(uv_r, uv_y_end),
-				Vector2(uv_r, uv_y_start),
-				Vector2(uv_l, uv_y_start),
-			],
-			[
-				end_loop + end_basis * lane_far_width + lane_offset_e,
-				end_loop + lane_offset_e,
-				start_loop + lane_offset_s,
-				start_loop + start_basis * lane_near_width + lane_offset_s,
-
-			])
-
-	#else:
-	#push_warning("Non-same number of lanes not implemented yet")
-
+		quad( st, uv_square(uv_l, uv_r, uv_y),
+			pts_square(nf_loop, nf_basis,
+				[(width_offset[LeftRight.RIGHT][NearFar.FAR] + lane_width[NearFar.FAR]),
+				width_offset[LeftRight.RIGHT][NearFar.FAR],
+				width_offset[LeftRight.RIGHT][NearFar.NEAR],
+				(width_offset[LeftRight.RIGHT][NearFar.NEAR] + lane_width[NearFar.NEAR])
+				]) )
+		for nf in NearFar.values():
+			width_offset[LeftRight.RIGHT][nf] += lane_width[nf]
+
+	var gutr_x = [null, null]
+	var gutr_y = [null, null]
+	var w_shoulder = [[null, null], [null, null]] #w_shoulder[LeftRight][NearFar]
+	for nf in NearFar.values():
+		w_shoulder[LeftRight.LEFT][nf]  = lerp(start_point.shoulder_width_l, end_point.shoulder_width_l, offset[nf])
+		w_shoulder[LeftRight.RIGHT][nf] = lerp(start_point.shoulder_width_r, end_point.shoulder_width_r, offset[nf])
+
+		# Gutter depth is the same for the left and right sides.
+		gutr_x[nf] = lerp(start_point.gutter_profile.x, end_point.gutter_profile.x, offset[nf])
+		gutr_y[nf] = lerp(start_point.gutter_profile.y, end_point.gutter_profile.y, offset[nf])
 	# Now create the shoulder geometry, including the "bevel" geo.
-
-	# Gutter depth is the same for the left and right sides.
-	var gutr_near = Vector2(
-		lerp(start_point.gutter_profile.x, end_point.gutter_profile.x, offset_s),
-		lerp(start_point.gutter_profile.y, end_point.gutter_profile.y, offset_s))
-	var gutr_far = Vector2(
-		lerp(start_point.gutter_profile.x, end_point.gutter_profile.x, offset_e),
-		lerp(start_point.gutter_profile.y, end_point.gutter_profile.y, offset_e))
-
-	for i in range(2):
-		var dir = -1 if i==0 else 1
-		var uv_y_start
-		var uv_y_end
+	for lr in LeftRight.values():
+		var dir = -1 if lr==0 else 1
+		var uv_y = [null, null]
+		var lane_uvx_ix = dir
 		if len(lane_uvs_length) == 1:
-			uv_y_start = lane_uvs_length[0]
-			uv_y_end = lane_uvs_length[0] + per_loop_uv_size
-		else:
-			uv_y_start = lane_uvs_length[dir]
-			uv_y_end = lane_uvs_length[dir] + per_loop_uv_size
+			lane_uvx_ix = 0
+		uv_y = [ lane_uvs_length[lane_uvx_ix], lane_uvs_length[lane_uvx_ix] + per_loop_uv_size ]
 
 		# Account for custom left/right shoulder width.
-		var near_w_shoulder
-		var far_w_shoulder
-		var pos_far_l
-		var pos_far_r
-		var pos_near_l
-		var pos_near_r
-		var pos_far_gutter
-		var pos_near_gutter
-		if dir == 1:
-			near_w_shoulder = lerp(start_point.shoulder_width_l, end_point.shoulder_width_l, offset_s)
-			far_w_shoulder = lerp(start_point.shoulder_width_l, end_point.shoulder_width_l, offset_e)
-			pos_far_l = far_width_offset + far_w_shoulder
-			pos_far_r = far_width_offset
-			pos_near_l = near_width_offset + near_w_shoulder
-			pos_near_r = near_width_offset
-			pos_far_gutter = pos_far_l
-			pos_near_gutter = pos_near_l
-		else:
-			near_w_shoulder = lerp(start_point.shoulder_width_r, end_point.shoulder_width_r, offset_s)
-			far_w_shoulder = lerp(start_point.shoulder_width_r, end_point.shoulder_width_r, offset_e)
-			pos_far_l = far_width_offset
-			pos_far_r = far_width_offset + far_w_shoulder
-			pos_near_l = near_width_offset
-			pos_near_r = near_width_offset + near_w_shoulder
-			pos_far_gutter = pos_far_r
-			pos_near_gutter = pos_near_r
+		var pos_l = [dir, dir]
+		var pos_r = [dir, dir]
+		for nf in NearFar.values():
+			var pos_gutter = width_offset[lr][nf] + w_shoulder[lr][nf]
+			if lr == LeftRight.RIGHT:
+				pos_l[nf] *= pos_gutter
+				pos_r[nf] *= width_offset[lr][nf]
+			else:
+				pos_l[nf] *= width_offset[lr][nf]
+				pos_r[nf] *= pos_gutter
 
 		# Assume the start and end lanes are the same for now.
-		var uv_l:float # the left edge of the uv for this lane.
-		var uv_m:float # The 'middle' vert, same level as shoulder but to edge.
-		var uv_r:float
 		var uv_mid = 0.8 # should be more like 0.9
-		if dir == 1:
-			uv_l = 0.0 * uv_width
-			uv_m = uv_mid * uv_width
-			uv_r = 1.0 * uv_width
-		else:
-			uv_l = 1.0 * uv_width
-			uv_m = uv_mid * uv_width
-			uv_r = 0.0 * uv_width
+		var uv_m = uv_mid * uv_width # The 'middle' vert, same level as shoulder but to edge.
 		# LEFT (between pos:_s and _m, and between uv:_l and _m)
 		# The flat part of the shoulder on both sides
-		quad(
-			st,
-			[
-				Vector2(uv_m if dir == 1 else uv_l, uv_y_end),
-				Vector2(uv_r if dir == 1 else uv_m, uv_y_end),
-				Vector2(uv_r if dir == 1 else uv_m, uv_y_start),
-				Vector2(uv_m if dir == 1 else uv_l, uv_y_start),
-			],
-			[
-				end_loop + end_basis * pos_far_l * dir,
-				end_loop + end_basis * pos_far_r * dir,
-				start_loop + start_basis * pos_near_r * dir,
-				start_loop + start_basis * pos_near_l * dir,
-			])
+
+		quad( st, uv_square(uv_m, uv_width, uv_y) if lr == LeftRight.RIGHT else uv_square(uv_width, uv_m, uv_y),
+			pts_square(nf_loop, nf_basis, [pos_l[NearFar.FAR], pos_r[NearFar.FAR], pos_r[NearFar.NEAR], pos_l[NearFar.NEAR]]) )
 
 		# The gutter, lower part of the shoulder on both sides.
-		if dir == 1:
-			quad(
-				st,
-				[
-					Vector2(uv_l, uv_y_end),
-					Vector2(uv_m, uv_y_end),
-					Vector2(uv_m, uv_y_start),
-					Vector2(uv_l, uv_y_start),
-				],
-				[
-					end_loop + end_basis * (pos_far_l + gutr_far.x) * dir + Vector3(0, gutr_far.y, 0),
-					end_loop + end_basis * pos_far_l * dir,
-					start_loop + start_basis * pos_near_l * dir,
-					start_loop + start_basis * (pos_near_l + gutr_near.x) * dir + Vector3(0, gutr_near.y, 0),
-				])
+		if lr == LeftRight.RIGHT:
+			quad( st, uv_square(0, uv_m, uv_y),
+				pts_square(nf_loop, nf_basis,
+					[pos_l[NearFar.FAR] + gutr_x[NearFar.FAR] * dir, pos_r[NearFar.FAR], pos_r[NearFar.NEAR], pos_l[NearFar.NEAR] + gutr_x[NearFar.NEAR] * dir],
+					[gutr_y[NearFar.FAR], 0, 0, gutr_y[NearFar.NEAR]]) )
 		else:
-			quad(
-				st,
-				[
-					Vector2(uv_m, uv_y_end),
-					Vector2(uv_r, uv_y_end),
-					Vector2(uv_r, uv_y_start),
-					Vector2(uv_m, uv_y_start),
-				],
-				[
-					end_loop + end_basis * pos_far_r * dir,
-					end_loop + end_basis * (pos_far_r + gutr_far.x) * dir + Vector3(0, gutr_far.y, 0),
-					start_loop + start_basis * (pos_near_r + gutr_near.x) * dir + Vector3(0, gutr_near.y, 0),
-					start_loop + start_basis * pos_near_r * dir,
-				])
-
+			quad( st, uv_square(uv_m, 0, uv_y),
+				pts_square(nf_loop, nf_basis,
+					[pos_r[NearFar.FAR], pos_r[NearFar.FAR] + gutr_x[NearFar.FAR] * dir, pos_r[NearFar.NEAR] + gutr_x[NearFar.NEAR] * dir, pos_r[NearFar.NEAR]],
+					[0, gutr_y[NearFar.FAR], gutr_y[NearFar.NEAR], 0]) )
+
+static func uv_square(uv_lmr1:float, uv_lmr2:float, uv_y: Array) -> Array:
+	assert( len(uv_y) == 2 )
+	return	[
+			Vector2(uv_lmr1, uv_y[NearFar.FAR]),
+			Vector2(uv_lmr2, uv_y[NearFar.FAR]),
+			Vector2(uv_lmr2, uv_y[NearFar.NEAR]),
+			Vector2(uv_lmr1, uv_y[NearFar.NEAR]),
+			]
+
+static func pts_square(nf_loop:Array, nf_basis:Array, width_offset: Array, y_offset = null) -> Array:
+	assert( len(nf_loop) == 2 && len(nf_basis) == 2 )
+	var ret = [
+			nf_loop[NearFar.FAR] + nf_basis[NearFar.FAR] * width_offset[0],
+			nf_loop[NearFar.FAR] + nf_basis[NearFar.FAR] * width_offset[1],
+			nf_loop[NearFar.NEAR] + nf_basis[NearFar.NEAR] * width_offset[2],
+			nf_loop[NearFar.NEAR] + nf_basis[NearFar.NEAR] * width_offset[3],
+			]
+	if y_offset != null:
+		for i in len(y_offset):
+			ret[i] += Vector3.UP * y_offset[i]
+	return ret
 
 # Generate a quad with two triangles for a list of 4 points/uvs in a row.
 # For convention, do cloclwise from top-left vert, where the diagonal
diff --git a/addons/road-generator/ui/road_point_edit.gd b/addons/road-generator/ui/road_point_edit.gd
index 75b430e1..6969f8cb 100644
--- a/addons/road-generator/ui/road_point_edit.gd
+++ b/addons/road-generator/ui/road_point_edit.gd
@@ -103,9 +103,11 @@ func _assign_copy_target(target) -> void:
 		"shoulder_width_l": target.shoulder_width_l,
 		"shoulder_width_r": target.shoulder_width_r,
 		"gutter_profile": target.gutter_profile,
-		"create_geo": target.create_geo
+		"create_geo": target.create_geo,
+		"alignment": target.alignment
 	}
 
+
 func _apply_settings_target(target, all:bool) -> void:
 	var undo_redo = _editor_plugin.get_undo_redo()
 
diff --git a/addons/road-generator/ui/road_point_gizmo.gd b/addons/road-generator/ui/road_point_gizmo.gd
index 6d3794d4..071758fa 100644
--- a/addons/road-generator/ui/road_point_gizmo.gd
+++ b/addons/road-generator/ui/road_point_gizmo.gd
@@ -125,8 +125,8 @@ func _redraw(gizmo) -> void:
 		collider.size = BaseColliderSize * width_scale
 
 	var lines = PackedVector3Array()
-	lines.push_back(Vector3(0, 1, 0))
-	lines.push_back(Vector3(0, 1, 0))
+	lines.push_back(Vector3.UP)
+	lines.push_back(Vector3.UP)
 	gizmo.add_lines(lines, get_material("main", gizmo), false)
 
 	gizmo.add_collision_triangles(collider_tri_mesh)
@@ -186,7 +186,7 @@ func _redraw(gizmo) -> void:
 		if i < lane_count:
 			div.visible = true
 			div.position = Vector3(x_pos, 0, 0)
-			div.scale = Vector3(width_scale, width_scale, width_scale)
+			div.scale = width_scale_v
 			x_pos += lane_width
 		else:
 			div.visible = false
@@ -213,6 +213,11 @@ func _get_handle_value(gizmo: EditorNode3DGizmo, index: int, secondary: bool) ->
 	var lane_width = point.lane_width
 	var lane_count = len(point.traffic_dir)
 	var width_mag: float = lane_count * lane_width / 2
+	if point.alignment == RoadPoint.Alignment.DIVIDER:
+		if index == HandleType.REV_WIDTH_MAG:
+			width_mag = point.get_rev_lane_count() * lane_width
+		elif index == HandleType.FWD_WIDTH_MAG:
+			width_mag = point.get_fwd_lane_count() * lane_width
 
 	match index:
 		HandleType.PRIOR_MAG:
@@ -311,9 +316,10 @@ func set_width_handle(gizmo: EditorNode3DGizmo, index: int, camera: Camera3D, po
 		# Prevent handles from crossing over road divider
 		var lane_width = roadpoint.lane_width
 		var lane_count = len(roadpoint.traffic_dir)
-		var half_road_width = lane_count * lane_width / 2
-		var rev_lane_width = roadpoint.get_rev_lane_count() * lane_width
-		var road_divider = -half_road_width + rev_lane_width
+
+		var road_divider = 0
+		if roadpoint.alignment == RoadPoint.Alignment.GEOMETRIC:
+			road_divider = (roadpoint.get_rev_lane_count() - lane_count / 2.0) * lane_width
 		var road_div_rev_limit = road_divider - (lane_width * LaneOffset)
 		var road_div_fwd_limit = road_divider + (lane_width * LaneOffset)
 		if index == HandleType.REV_WIDTH_MAG and new_mag > road_div_rev_limit:
diff --git a/demo/procedural_generator/ProceduralGenerator.tscn b/demo/procedural_generator/ProceduralGenerator.tscn
index 5169f052..99a4b64c 100644
--- a/demo/procedural_generator/ProceduralGenerator.tscn
+++ b/demo/procedural_generator/ProceduralGenerator.tscn
@@ -48,18 +48,20 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 21.9997)
 script = ExtResource("4")
 traffic_dir = Array[int]([2, 2, 1, 1])
 lanes = Array[int]([2, 4, 4, 2])
-prior_pt_init = NodePath("../RP_002")
 prior_mag = 15.0
 next_mag = 15.0
+alignment = 1
+prior_pt_init = NodePath("../RP_002")
 
 [node name="RP_002" type="Node3D" parent="RoadManager/Road_001"]
 transform = Transform3D(0.999842, -0.000116941, 0.0178009, 0, 0.999978, 0.00656924, -0.0178013, -0.0065682, 0.99982, -0.677433, 0.25, -16.0496)
 script = ExtResource("4")
 traffic_dir = Array[int]([2, 2, 1, 1])
 lanes = Array[int]([2, 4, 4, 2])
-next_pt_init = NodePath("../RP_001")
 prior_mag = 15.0
 next_mag = 15.0
+alignment = 1
+next_pt_init = NodePath("../RP_001")
 
 [node name="UI" type="Control" parent="."]
 layout_mode = 3


Reviews:
user: login: antonkurkin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T23:57:00Z
user: login: TheDuckCow
body: Approving with the understanding that we can make a few of the changes below. Some of the points are more discussion oriented.
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-06T05:43:44Z
user: login: antonkurkin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:52:22Z
user: login: antonkurkin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:54:01Z
user: login: antonkurkin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:56:16Z
user: login: antonkurkin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T10:01:53Z
user: login: TheDuckCow
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T05:12:34Z

Review comments:
diff_hunk: @@ -215,6 +215,7 @@ func generate_edge_curves():
 		return
 
 	var _par = get_parent()
+
path: addons/road-generator/nodes/road_segment.gd
user: login: antonkurkin
body: I wanted to ask what's the reason for edge curves - is it for prop creation (like border or signs) or something else?
is it possible to debug draw it easily (so I could check that they are fine)?
Also why are they seemingly not connected to geometry of the polygons?
And another question is - should we make edges direction the same way we now approach road lanes? I think it could be also easier to just think that for example left of the edge is road - right of the edge is not
created_at: 2025-05-03T23:57:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,6 +215,7 @@ func generate_edge_curves():
 		return
 
 	var _par = get_parent()
+
path: addons/road-generator/nodes/road_segment.gd
user: login: TheDuckCow
body: Yup, for prop creation like fencing and barriers. They are using curves and inherently had to be implemented differently. At the time, I couldn't think of a good way to make the actual roads utilize these curves for generation, since they rely on an actual approximation algorithm of offsetting a bezier curve (meaning, it is imperfect and cannot guarantee things like exact road widths), but having the curve is still very helpful for populating roadside decorations. My latest PR actually puts it under a section of that name.

In the future, we might subdivide the curves once or twice to help ensure they match the contours better, but that's a bigger point to tackle.
created_at: 2025-05-04T18:00:36Z
author_association: OWNER
diff_hunk: @@ -41,6 +41,11 @@ enum PointInit {
 	PRIOR,
 }
 
+enum Alignment {
+	CENTER,
+	CENTERLINE,
path: addons/road-generator/nodes/road_point.gd
user: login: TheDuckCow
body: What do you think about this adjustment? Comments will also appear in docstrings.

```suggestion
	GEOMETRIC,  ## Aligns equal amount of road to the left and right of the RoadPoint
	DIVIDER,  ## Ensures the lane direction dividing line is aligned to the RoadPoint
```
created_at: 2025-05-06T02:41:59Z
author_association: OWNER
diff_hunk: @@ -86,6 +91,10 @@ const SEG_DIST_MULT: float = 8.0 # How many road widths apart to add next RoadPo
 # If off, it indicates the developer will load in their own custom mesh + collision.
 @export var create_geo := true: set = _set_create_geo
 
+# Generate procedural road geometry
+# If off, it indicates the developer will load in their own custom mesh + collision.
+@export var alignment: Alignment: get = _get_alignment, set = _set_alignment
path: addons/road-generator/nodes/road_point.gd
user: login: TheDuckCow
body: ```suggestion
## Determines how the geometry will center itself around the RoadPoint origin 
@export var alignment: Alignment: get = _get_alignment, set = _set_alignment
```
created_at: 2025-05-06T02:48:23Z
author_association: OWNER
diff_hunk: @@ -86,6 +91,10 @@ const SEG_DIST_MULT: float = 8.0 # How many road widths apart to add next RoadPo
 # If off, it indicates the developer will load in their own custom mesh + collision.
 @export var create_geo := true: set = _set_create_geo
 
+# Generate procedural road geometry
+# If off, it indicates the developer will load in their own custom mesh + collision.
+@export var alignment: Alignment: get = _get_alignment, set = _set_alignment
path: addons/road-generator/nodes/road_point.gd
user: login: TheDuckCow
body: As a side note, I'm increasingly in favor of the in-line setters like we have over here, keeps all the code adjacent without needing to define extra functions that inevitably get placed elsewhere. Your choice to switch it over or not, not a huge deal.

Example, where we only have the setter and not the getter (which is all we'd need here I would say) https://github.com/TheDuckCow/godot-road-generator/blob/58f72f4bcae845e739a876463b4e4da6a25f2c4f/addons/road-generator/nodes/road_manager.gd#L24
created_at: 2025-05-06T04:06:25Z
author_association: OWNER
diff_hunk: @@ -833,6 +857,17 @@ func _create_collisions() -> void:
 		
 		sbody.set_meta("_edit_lock_", true)
 
+enum NearFar {
+	NEAR,
+	FAR
+}
+enum LeftRight {
+	LEFT,
+	RIGHT
+}
+
+#correspond to RoadPoint.LaneType enum - lookup for lane texture multiplier
+const uv_mul = [7, 0, 1, 2, 3, 4, 5, 6, 7, 7]
path: addons/road-generator/nodes/road_segment.gd
user: login: TheDuckCow
body: Though the repo doesn't adhere to it 100%, I do like moving at least closer to the official gdscript styleguide, whereby enums and consts are defined up top instead of in the middle of the file ([reference here](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html#code-order)).

For the same reason, might be worth providing a docstring or adjusting the name for which leftright/nearfar is meant to be used.
created_at: 2025-05-06T04:09:57Z
author_association: OWNER
diff_hunk: @@ -844,253 +879,183 @@ func _insert_geo_loop(
 		lane_uvs_length: Array,
 		per_loop_uv_size: float,
 		uv_width: float):
+	assert (loop < loops)
path: addons/road-generator/nodes/road_segment.gd
user: login: TheDuckCow
body: Is this just for catching impossible cases in our code, or is this a check that should actually run in a production case to control flow? Reminder that assert statements are stripped out for non debug builds, making it equivalent to not being there.
created_at: 2025-05-06T04:10:59Z
author_association: OWNER
diff_hunk: @@ -844,253 +879,183 @@ func _insert_geo_loop(
 		lane_uvs_length: Array,
 		per_loop_uv_size: float,
 		uv_width: float):
+	assert (loop < loops)
+	
 	# One loop = row of quads left to right across the road, spanning lanes.
-	var offset_s = float(loop) / float(loops)
-	var offset_e = float(loop + 1) / float(loops)
-
-	# Apply ease in and out across all attributes.
-	var offset_s_ease = ease(offset_s, smooth_amount)
-	var offset_e_ease = ease(offset_e, smooth_amount)
-
-	#if len(start_point.lanes) == len(end_point.lanes):
-	var start_loop:Vector3
-	var start_basis:Vector3
-	var end_loop:Vector3
-	var end_basis:Vector3
-	start_loop = curve.sample_baked(offset_s * clength)
-	start_basis = _normal_for_offset(curve, offset_s)
-	end_loop = curve.sample_baked(offset_e * clength)
-	end_basis = _normal_for_offset(curve, offset_e)
+	var offset = [float(loop) / float(loops), float(loop + 1) / float(loops)]
+	var point = [start_point, end_point]
+
+	var lane_offset = []
+	for nf in NearFar.values():
+		lane_offset.append(len(point[nf].lanes) / 2.0)
+	if start_point.alignment == RoadPoint.Alignment.CENTERLINE || \
+		end_point.alignment == RoadPoint.Alignment.CENTERLINE:
+		var nf_reverse = [0, 0]
+		for l in lanes:
+			if l[1] == RoadPoint.LaneDir.REVERSE:
+				if l[0] == RoadPoint.LaneType.TRANSITION_ADD:
+					nf_reverse[NearFar.FAR] += 1
+				elif l[0] == RoadPoint.LaneType.TRANSITION_REM:
+					nf_reverse[NearFar.NEAR] += 1
+				elif l[0] != RoadPoint.LaneType.SHOULDER:
+					nf_reverse[NearFar.NEAR] += 1
+					nf_reverse[NearFar.FAR] += 1
+			else:
+				assert (l[1] == RoadPoint.LaneDir.FORWARD)
+		for nf in NearFar.values():
+			if point[nf].alignment == RoadPoint.Alignment.CENTERLINE:
+				lane_offset[nf] = nf_reverse[nf]
+
+	var nf_loop = [null, null]
+	var nf_basis = [null, null]
+	var nf_width = [null, null]
+	var add_width = [null, null]
+	var rem_width = [null, null]
+
+	var width_offset = [[null, null], [null, null]] #width_offset[LeftRight][NearFar]
+
+	for nf in NearFar.values():
+		# Apply ease in and out across all attributes.
+		var offset_ease = ease(offset[nf], smooth_amount)
+		nf_loop[nf] = curve.sample_baked(offset[nf] * clength)
+		nf_basis[nf] = _normal_for_offset(curve, offset[nf])
+
+		# Calculate lane widths
+		nf_width[nf] = lerp(start_point.lane_width, end_point.lane_width, offset_ease)
+		add_width[nf] = lerp(0.0, end_point.lane_width, offset_ease)
+		rem_width[nf] = lerp(start_point.lane_width, 0.0, offset_ease)
+		# Sum the lane widths and get position of left edge
+		width_offset[LeftRight.LEFT][nf] = lerp(
+				lane_offset[NearFar.NEAR] * start_point.lane_width,
+				lane_offset[NearFar.FAR] * end_point.lane_width,
+				offset_ease
+		)
+		width_offset[LeftRight.RIGHT][nf] = -width_offset[LeftRight.LEFT][nf]
 
 	#print("\tRunning loop %s: %s to %s; Start: %s,%s, end: %s,%s" % [
-	#	loop, offset_s, offset_e, start_loop, start_basis, end_loop, end_basis
+	#	loop, offset[NearFar.NEAR], offset[NearFar.FAR], nf_loop[NearFar.NEAR], nf_basis[NearFar.NEAR], nf_loop[NearFar.FAR], nf_basis[NearFar.FAR]
 	#])
 
-	# Calculate lane widths
-	var near_width = lerp(start_point.lane_width, end_point.lane_width, offset_s_ease)
-	var near_add_width = lerp(0.0, end_point.lane_width, offset_s_ease)
-	var near_rem_width = lerp(start_point.lane_width, 0.0, offset_s_ease)
-	var far_width = lerp(start_point.lane_width, end_point.lane_width, offset_e_ease)
-	var far_add_width = lerp(0.0, end_point.lane_width, offset_e_ease)
-	var far_rem_width = lerp(start_point.lane_width, 0.0, offset_e_ease)
-
-	# Sum the lane widths and get position of left edge
-	var near_width_offset
-	var far_width_offset
-
-	near_width_offset = -lerp(
-			len(start_point.lanes) * start_point.lane_width,
-			len(end_point.lanes) * end_point.lane_width,
-			offset_s_ease
-	) / 2.0
-	far_width_offset = -lerp(
-			len(start_point.lanes) * start_point.lane_width,
-			len(end_point.lanes) * end_point.lane_width,
-			offset_e_ease
-	) / 2.0
-
 	for i in range(lane_count):
 		# Create the contents of a single lane / quad within this quad loop.
-		var lane_offset_s = near_width_offset * start_basis
-		var lane_offset_e = far_width_offset * end_basis
-		var lane_near_width
-		var lane_far_width
-
-		# Set lane width for current lane type
-		if lanes[i][0] == RoadPoint.LaneType.TRANSITION_ADD:
-			lane_near_width = near_add_width
-			lane_far_width = far_add_width
-		elif lanes[i][0] == RoadPoint.LaneType.TRANSITION_REM:
-			lane_near_width = near_rem_width
-			lane_far_width = far_rem_width
-		else:
-			lane_near_width = near_width
-			lane_far_width = far_width
-
-		near_width_offset += lane_near_width
-		far_width_offset += lane_far_width
+		var lane_offset_nf = [0, 0]
+		var lane_width = [0, 0]
+		for nf in NearFar.values():
+			lane_offset_nf[nf] = width_offset[LeftRight.RIGHT][nf] * nf_basis[nf]
+
+			# Set lane width for current lane type
+			if lanes[i][0] == RoadPoint.LaneType.TRANSITION_ADD:
+				lane_width[nf] = add_width[nf]
+			elif lanes[i][0] == RoadPoint.LaneType.TRANSITION_REM:
+				lane_width[nf] = rem_width[nf]
+			else:
+				lane_width[nf] = nf_width[nf]
 
 		# Assume the start and end lanes are the same for now.
 		var uv_l:float # the left edge of the uv for this lane.
 		var uv_r:float
-		match lanes[i][0]:
-			RoadPoint.LaneType.NO_MARKING:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.SHOULDER:
-				uv_l = uv_width * 0
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.SLOW:
-				uv_l = uv_width * 1
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.MIDDLE:
-				uv_l = uv_width * 2
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.FAST:
-				uv_l = uv_width * 3
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.TWO_WAY:
-				# Flipped
-				uv_r = uv_width * 4
-				uv_l = uv_r + uv_width
-			RoadPoint.LaneType.ONE_WAY:
-				# Flipped
-				uv_r = uv_width * 5
-				uv_l = uv_r + uv_width
-			RoadPoint.LaneType.SINGLE_LINE:
-				uv_l = uv_width * 6
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.TRANSITION_ADD, RoadPoint.LaneType.TRANSITION_REM:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width - 0.002
-			_:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width
-		if lanes[i][1] == RoadPoint.LaneDir.REVERSE:
+		assert (len(uv_mul) == len(RoadPoint.LaneType.values()))
path: addons/road-generator/nodes/road_segment.gd
user: login: TheDuckCow
body: Same here for the assert, should we be doing anything if this assert fails? Return or continue early?
created_at: 2025-05-06T04:11:59Z
author_association: OWNER
path: addons/road-generator/nodes/road_segment.gd
user: login: TheDuckCow
body: Seems to be working well. Geometric construction works nicely, blending between different variants. It works well with undo/redo, copy attributes seems to work well as well. The setup of RoadLanes is no worse than it was before regarding scenarios where the RoadLanes get flipped around if the orientations of RoadPoints are not normalized.
created_at: 2025-05-06T05:29:24Z
author_association: OWNER
diff_hunk: @@ -86,6 +91,10 @@ const SEG_DIST_MULT: float = 8.0 # How many road widths apart to add next RoadPo
 # If off, it indicates the developer will load in their own custom mesh + collision.
 @export var create_geo := true: set = _set_create_geo
 
+# Generate procedural road geometry
+# If off, it indicates the developer will load in their own custom mesh + collision.
+@export var alignment: Alignment: get = _get_alignment, set = _set_alignment
path: addons/road-generator/nodes/road_point.gd
user: login: TheDuckCow
body: Should probably also set a default value here to make it explicit, right now it's implicit I believe using the first entry in the enum list.

After playing around with this branch, I'm contemplating whether we _should_ actually make this alignment method on by default. It has the nice benefit that changing RoadLane counts using the 3D view widget not actually jostle the road back and forth, which was always a bit jarring. The downside would definitely be for 1-way streets, but then again one could make the argument that it makes it clearer which way the lanes of that one-way road are facing:

<img width="690" alt="Screen Shot 2025-05-05 at 10 36 25 PM" src="https://github.com/user-attachments/assets/71c8540f-1c97-49d5-83b0-3a96b38d8358" />

The downside of making this on by default is that the change wouldn't be backwards compatible, pre-existing scenes would get modified to the new default. There might be a way around this, such as checking for a property on the container for which version it was last saved. Could help us do some migration scripts in the future should it be necessary. **Nothing you need to worry about**, I'm just trying to think ahead a bit.
created_at: 2025-05-06T05:43:23Z
author_association: OWNER
diff_hunk: @@ -86,6 +91,10 @@ const SEG_DIST_MULT: float = 8.0 # How many road widths apart to add next RoadPo
 # If off, it indicates the developer will load in their own custom mesh + collision.
 @export var create_geo := true: set = _set_create_geo
 
+# Generate procedural road geometry
+# If off, it indicates the developer will load in their own custom mesh + collision.
+@export var alignment: Alignment: get = _get_alignment, set = _set_alignment
path: addons/road-generator/nodes/road_point.gd
user: login: antonkurkin
body: ah make sense. I think I'll leave it as is so it would be the same in the source file but I'll keep it in mind for the future refactoring
created_at: 2025-05-06T09:52:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -833,6 +857,17 @@ func _create_collisions() -> void:
 		
 		sbody.set_meta("_edit_lock_", true)
 
+enum NearFar {
+	NEAR,
+	FAR
+}
+enum LeftRight {
+	LEFT,
+	RIGHT
+}
+
+#correspond to RoadPoint.LaneType enum - lookup for lane texture multiplier
+const uv_mul = [7, 0, 1, 2, 3, 4, 5, 6, 7, 7]
path: addons/road-generator/nodes/road_segment.gd
user: login: antonkurkin
body: ok will move. as it is only used in one function was thinking that it makes sense to define it right before it
created_at: 2025-05-06T09:54:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -844,253 +879,183 @@ func _insert_geo_loop(
 		lane_uvs_length: Array,
 		per_loop_uv_size: float,
 		uv_width: float):
+	assert (loop < loops)
path: addons/road-generator/nodes/road_segment.gd
user: login: antonkurkin
body: no, just an additional sanity check for the arguments. as an internal function I don't think it should be possible for the user to make them wrong
created_at: 2025-05-06T09:56:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -844,253 +879,183 @@ func _insert_geo_loop(
 		lane_uvs_length: Array,
 		per_loop_uv_size: float,
 		uv_width: float):
+	assert (loop < loops)
+	
 	# One loop = row of quads left to right across the road, spanning lanes.
-	var offset_s = float(loop) / float(loops)
-	var offset_e = float(loop + 1) / float(loops)
-
-	# Apply ease in and out across all attributes.
-	var offset_s_ease = ease(offset_s, smooth_amount)
-	var offset_e_ease = ease(offset_e, smooth_amount)
-
-	#if len(start_point.lanes) == len(end_point.lanes):
-	var start_loop:Vector3
-	var start_basis:Vector3
-	var end_loop:Vector3
-	var end_basis:Vector3
-	start_loop = curve.sample_baked(offset_s * clength)
-	start_basis = _normal_for_offset(curve, offset_s)
-	end_loop = curve.sample_baked(offset_e * clength)
-	end_basis = _normal_for_offset(curve, offset_e)
+	var offset = [float(loop) / float(loops), float(loop + 1) / float(loops)]
+	var point = [start_point, end_point]
+
+	var lane_offset = []
+	for nf in NearFar.values():
+		lane_offset.append(len(point[nf].lanes) / 2.0)
+	if start_point.alignment == RoadPoint.Alignment.CENTERLINE || \
+		end_point.alignment == RoadPoint.Alignment.CENTERLINE:
+		var nf_reverse = [0, 0]
+		for l in lanes:
+			if l[1] == RoadPoint.LaneDir.REVERSE:
+				if l[0] == RoadPoint.LaneType.TRANSITION_ADD:
+					nf_reverse[NearFar.FAR] += 1
+				elif l[0] == RoadPoint.LaneType.TRANSITION_REM:
+					nf_reverse[NearFar.NEAR] += 1
+				elif l[0] != RoadPoint.LaneType.SHOULDER:
+					nf_reverse[NearFar.NEAR] += 1
+					nf_reverse[NearFar.FAR] += 1
+			else:
+				assert (l[1] == RoadPoint.LaneDir.FORWARD)
+		for nf in NearFar.values():
+			if point[nf].alignment == RoadPoint.Alignment.CENTERLINE:
+				lane_offset[nf] = nf_reverse[nf]
+
+	var nf_loop = [null, null]
+	var nf_basis = [null, null]
+	var nf_width = [null, null]
+	var add_width = [null, null]
+	var rem_width = [null, null]
+
+	var width_offset = [[null, null], [null, null]] #width_offset[LeftRight][NearFar]
+
+	for nf in NearFar.values():
+		# Apply ease in and out across all attributes.
+		var offset_ease = ease(offset[nf], smooth_amount)
+		nf_loop[nf] = curve.sample_baked(offset[nf] * clength)
+		nf_basis[nf] = _normal_for_offset(curve, offset[nf])
+
+		# Calculate lane widths
+		nf_width[nf] = lerp(start_point.lane_width, end_point.lane_width, offset_ease)
+		add_width[nf] = lerp(0.0, end_point.lane_width, offset_ease)
+		rem_width[nf] = lerp(start_point.lane_width, 0.0, offset_ease)
+		# Sum the lane widths and get position of left edge
+		width_offset[LeftRight.LEFT][nf] = lerp(
+				lane_offset[NearFar.NEAR] * start_point.lane_width,
+				lane_offset[NearFar.FAR] * end_point.lane_width,
+				offset_ease
+		)
+		width_offset[LeftRight.RIGHT][nf] = -width_offset[LeftRight.LEFT][nf]
 
 	#print("\tRunning loop %s: %s to %s; Start: %s,%s, end: %s,%s" % [
-	#	loop, offset_s, offset_e, start_loop, start_basis, end_loop, end_basis
+	#	loop, offset[NearFar.NEAR], offset[NearFar.FAR], nf_loop[NearFar.NEAR], nf_basis[NearFar.NEAR], nf_loop[NearFar.FAR], nf_basis[NearFar.FAR]
 	#])
 
-	# Calculate lane widths
-	var near_width = lerp(start_point.lane_width, end_point.lane_width, offset_s_ease)
-	var near_add_width = lerp(0.0, end_point.lane_width, offset_s_ease)
-	var near_rem_width = lerp(start_point.lane_width, 0.0, offset_s_ease)
-	var far_width = lerp(start_point.lane_width, end_point.lane_width, offset_e_ease)
-	var far_add_width = lerp(0.0, end_point.lane_width, offset_e_ease)
-	var far_rem_width = lerp(start_point.lane_width, 0.0, offset_e_ease)
-
-	# Sum the lane widths and get position of left edge
-	var near_width_offset
-	var far_width_offset
-
-	near_width_offset = -lerp(
-			len(start_point.lanes) * start_point.lane_width,
-			len(end_point.lanes) * end_point.lane_width,
-			offset_s_ease
-	) / 2.0
-	far_width_offset = -lerp(
-			len(start_point.lanes) * start_point.lane_width,
-			len(end_point.lanes) * end_point.lane_width,
-			offset_e_ease
-	) / 2.0
-
 	for i in range(lane_count):
 		# Create the contents of a single lane / quad within this quad loop.
-		var lane_offset_s = near_width_offset * start_basis
-		var lane_offset_e = far_width_offset * end_basis
-		var lane_near_width
-		var lane_far_width
-
-		# Set lane width for current lane type
-		if lanes[i][0] == RoadPoint.LaneType.TRANSITION_ADD:
-			lane_near_width = near_add_width
-			lane_far_width = far_add_width
-		elif lanes[i][0] == RoadPoint.LaneType.TRANSITION_REM:
-			lane_near_width = near_rem_width
-			lane_far_width = far_rem_width
-		else:
-			lane_near_width = near_width
-			lane_far_width = far_width
-
-		near_width_offset += lane_near_width
-		far_width_offset += lane_far_width
+		var lane_offset_nf = [0, 0]
+		var lane_width = [0, 0]
+		for nf in NearFar.values():
+			lane_offset_nf[nf] = width_offset[LeftRight.RIGHT][nf] * nf_basis[nf]
+
+			# Set lane width for current lane type
+			if lanes[i][0] == RoadPoint.LaneType.TRANSITION_ADD:
+				lane_width[nf] = add_width[nf]
+			elif lanes[i][0] == RoadPoint.LaneType.TRANSITION_REM:
+				lane_width[nf] = rem_width[nf]
+			else:
+				lane_width[nf] = nf_width[nf]
 
 		# Assume the start and end lanes are the same for now.
 		var uv_l:float # the left edge of the uv for this lane.
 		var uv_r:float
-		match lanes[i][0]:
-			RoadPoint.LaneType.NO_MARKING:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.SHOULDER:
-				uv_l = uv_width * 0
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.SLOW:
-				uv_l = uv_width * 1
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.MIDDLE:
-				uv_l = uv_width * 2
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.FAST:
-				uv_l = uv_width * 3
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.TWO_WAY:
-				# Flipped
-				uv_r = uv_width * 4
-				uv_l = uv_r + uv_width
-			RoadPoint.LaneType.ONE_WAY:
-				# Flipped
-				uv_r = uv_width * 5
-				uv_l = uv_r + uv_width
-			RoadPoint.LaneType.SINGLE_LINE:
-				uv_l = uv_width * 6
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.TRANSITION_ADD, RoadPoint.LaneType.TRANSITION_REM:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width - 0.002
-			_:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width
-		if lanes[i][1] == RoadPoint.LaneDir.REVERSE:
+		assert (len(uv_mul) == len(RoadPoint.LaneType.values()))
path: addons/road-generator/nodes/road_segment.gd
user: login: antonkurkin
body: I consider asserts to be like helpers for development - say, you're adding new lane type to the plugin and trying to build/test the project. you will see this assert failing and won't need to seek for why are textures broken for your new lane type - or won't forget to update  the uv_mul and the logic like texture flipping. this is not a user-facing issue so in my opinion doesn't have to be checked in production
created_at: 2025-05-06T10:01:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -844,253 +879,183 @@ func _insert_geo_loop(
 		lane_uvs_length: Array,
 		per_loop_uv_size: float,
 		uv_width: float):
+	assert (loop < loops)
+	
 	# One loop = row of quads left to right across the road, spanning lanes.
-	var offset_s = float(loop) / float(loops)
-	var offset_e = float(loop + 1) / float(loops)
-
-	# Apply ease in and out across all attributes.
-	var offset_s_ease = ease(offset_s, smooth_amount)
-	var offset_e_ease = ease(offset_e, smooth_amount)
-
-	#if len(start_point.lanes) == len(end_point.lanes):
-	var start_loop:Vector3
-	var start_basis:Vector3
-	var end_loop:Vector3
-	var end_basis:Vector3
-	start_loop = curve.sample_baked(offset_s * clength)
-	start_basis = _normal_for_offset(curve, offset_s)
-	end_loop = curve.sample_baked(offset_e * clength)
-	end_basis = _normal_for_offset(curve, offset_e)
+	var offset = [float(loop) / float(loops), float(loop + 1) / float(loops)]
+	var point = [start_point, end_point]
+
+	var lane_offset = []
+	for nf in NearFar.values():
+		lane_offset.append(len(point[nf].lanes) / 2.0)
+	if start_point.alignment == RoadPoint.Alignment.CENTERLINE || \
+		end_point.alignment == RoadPoint.Alignment.CENTERLINE:
+		var nf_reverse = [0, 0]
+		for l in lanes:
+			if l[1] == RoadPoint.LaneDir.REVERSE:
+				if l[0] == RoadPoint.LaneType.TRANSITION_ADD:
+					nf_reverse[NearFar.FAR] += 1
+				elif l[0] == RoadPoint.LaneType.TRANSITION_REM:
+					nf_reverse[NearFar.NEAR] += 1
+				elif l[0] != RoadPoint.LaneType.SHOULDER:
+					nf_reverse[NearFar.NEAR] += 1
+					nf_reverse[NearFar.FAR] += 1
+			else:
+				assert (l[1] == RoadPoint.LaneDir.FORWARD)
+		for nf in NearFar.values():
+			if point[nf].alignment == RoadPoint.Alignment.CENTERLINE:
+				lane_offset[nf] = nf_reverse[nf]
+
+	var nf_loop = [null, null]
+	var nf_basis = [null, null]
+	var nf_width = [null, null]
+	var add_width = [null, null]
+	var rem_width = [null, null]
+
+	var width_offset = [[null, null], [null, null]] #width_offset[LeftRight][NearFar]
+
+	for nf in NearFar.values():
+		# Apply ease in and out across all attributes.
+		var offset_ease = ease(offset[nf], smooth_amount)
+		nf_loop[nf] = curve.sample_baked(offset[nf] * clength)
+		nf_basis[nf] = _normal_for_offset(curve, offset[nf])
+
+		# Calculate lane widths
+		nf_width[nf] = lerp(start_point.lane_width, end_point.lane_width, offset_ease)
+		add_width[nf] = lerp(0.0, end_point.lane_width, offset_ease)
+		rem_width[nf] = lerp(start_point.lane_width, 0.0, offset_ease)
+		# Sum the lane widths and get position of left edge
+		width_offset[LeftRight.LEFT][nf] = lerp(
+				lane_offset[NearFar.NEAR] * start_point.lane_width,
+				lane_offset[NearFar.FAR] * end_point.lane_width,
+				offset_ease
+		)
+		width_offset[LeftRight.RIGHT][nf] = -width_offset[LeftRight.LEFT][nf]
 
 	#print("\tRunning loop %s: %s to %s; Start: %s,%s, end: %s,%s" % [
-	#	loop, offset_s, offset_e, start_loop, start_basis, end_loop, end_basis
+	#	loop, offset[NearFar.NEAR], offset[NearFar.FAR], nf_loop[NearFar.NEAR], nf_basis[NearFar.NEAR], nf_loop[NearFar.FAR], nf_basis[NearFar.FAR]
 	#])
 
-	# Calculate lane widths
-	var near_width = lerp(start_point.lane_width, end_point.lane_width, offset_s_ease)
-	var near_add_width = lerp(0.0, end_point.lane_width, offset_s_ease)
-	var near_rem_width = lerp(start_point.lane_width, 0.0, offset_s_ease)
-	var far_width = lerp(start_point.lane_width, end_point.lane_width, offset_e_ease)
-	var far_add_width = lerp(0.0, end_point.lane_width, offset_e_ease)
-	var far_rem_width = lerp(start_point.lane_width, 0.0, offset_e_ease)
-
-	# Sum the lane widths and get position of left edge
-	var near_width_offset
-	var far_width_offset
-
-	near_width_offset = -lerp(
-			len(start_point.lanes) * start_point.lane_width,
-			len(end_point.lanes) * end_point.lane_width,
-			offset_s_ease
-	) / 2.0
-	far_width_offset = -lerp(
-			len(start_point.lanes) * start_point.lane_width,
-			len(end_point.lanes) * end_point.lane_width,
-			offset_e_ease
-	) / 2.0
-
 	for i in range(lane_count):
 		# Create the contents of a single lane / quad within this quad loop.
-		var lane_offset_s = near_width_offset * start_basis
-		var lane_offset_e = far_width_offset * end_basis
-		var lane_near_width
-		var lane_far_width
-
-		# Set lane width for current lane type
-		if lanes[i][0] == RoadPoint.LaneType.TRANSITION_ADD:
-			lane_near_width = near_add_width
-			lane_far_width = far_add_width
-		elif lanes[i][0] == RoadPoint.LaneType.TRANSITION_REM:
-			lane_near_width = near_rem_width
-			lane_far_width = far_rem_width
-		else:
-			lane_near_width = near_width
-			lane_far_width = far_width
-
-		near_width_offset += lane_near_width
-		far_width_offset += lane_far_width
+		var lane_offset_nf = [0, 0]
+		var lane_width = [0, 0]
+		for nf in NearFar.values():
+			lane_offset_nf[nf] = width_offset[LeftRight.RIGHT][nf] * nf_basis[nf]
+
+			# Set lane width for current lane type
+			if lanes[i][0] == RoadPoint.LaneType.TRANSITION_ADD:
+				lane_width[nf] = add_width[nf]
+			elif lanes[i][0] == RoadPoint.LaneType.TRANSITION_REM:
+				lane_width[nf] = rem_width[nf]
+			else:
+				lane_width[nf] = nf_width[nf]
 
 		# Assume the start and end lanes are the same for now.
 		var uv_l:float # the left edge of the uv for this lane.
 		var uv_r:float
-		match lanes[i][0]:
-			RoadPoint.LaneType.NO_MARKING:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.SHOULDER:
-				uv_l = uv_width * 0
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.SLOW:
-				uv_l = uv_width * 1
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.MIDDLE:
-				uv_l = uv_width * 2
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.FAST:
-				uv_l = uv_width * 3
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.TWO_WAY:
-				# Flipped
-				uv_r = uv_width * 4
-				uv_l = uv_r + uv_width
-			RoadPoint.LaneType.ONE_WAY:
-				# Flipped
-				uv_r = uv_width * 5
-				uv_l = uv_r + uv_width
-			RoadPoint.LaneType.SINGLE_LINE:
-				uv_l = uv_width * 6
-				uv_r = uv_l + uv_width
-			RoadPoint.LaneType.TRANSITION_ADD, RoadPoint.LaneType.TRANSITION_REM:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width - 0.002
-			_:
-				uv_l = uv_width * 7
-				uv_r = uv_l + uv_width
-		if lanes[i][1] == RoadPoint.LaneDir.REVERSE:
+		assert (len(uv_mul) == len(RoadPoint.LaneType.values()))
path: addons/road-generator/nodes/road_segment.gd
user: login: TheDuckCow
body: Ok that's a perfectly fine answer to me, as long as it's intended for that kind of case. Thanks for explaining.
created_at: 2025-05-07T05:12:34Z
author_association: OWNER

PR comments:
user: login: antonkurkin
created_at: 2025-05-04T15:30:00Z
body: @TheDuckCow i tried to refactor _insert_geo_loop for code deduplication but I'm not sure if it's worth it as it make it a little bit more complicated. it's in a separate commit so I could revert it back if you think I should
author_association: CONTRIBUTOR
user: login: TheDuckCow
created_at: 2025-05-04T15:34:04Z
body: Hey just wanted to comment that this is looking great! Thanks once again for your contributions. I need to do more of a review and review your points above, but thought I'd leave an initial comment.

Regarding the demo, something that we'll have to do is handle RoadContainer edges specially, since the edge of one container might connect to another which is a saved subscene. There's logic around this in a few places, even a function to check if something belongs to a saved subscene. The idea is that in such scenarios, the RoadPoint belonging to the "dynamic" (editing scene) side should be the modified (ie if the saved scene has this alignment setting off, then the roadpoints connecting to it should be also turned off, even if the rest of the container are on). 

The other suggestion from my end: One way roads (the ones which look most shifted above) I think should still be center aligned, I feel like that would be more expected. But I'm open to hear your opinions on this matter. (hmmm though I guess it would still mean some "wiggle" if lanes are added/removed to a oneway road, so maybe it should stay as you have it...).
author_association: OWNER
user: login: antonkurkin
created_at: 2025-05-04T16:08:40Z
body: yeah I think I'll keep it possible to "centerline" alignment even for one-way roads. I agree that it looks a bit strange but as it's on RoadPoint basis, the user could make it any way they want. I already made it possible (with simple changes in offsets) to create segments out of different alignment roadpoints - only for road geometry in my latest push and it looks completely fine
author_association: CONTRIBUTOR
user: login: TheDuckCow
created_at: 2025-05-04T17:57:34Z
body: > but as it's on RoadPoint basis, the user could make it any way they want. 

Ok that's very fair. That being said, for yourself, how do you imagine controlling the setting? Are you using a script to force-set them all to be aligned? Or do you imagine using the "Copy Settings" button, where you can press shift to affect multiple at a time? (one change request I'll make formally later once I get to do more of a review, is to include this setting to be amongst those affected by the Copy/Apply Settings button in the inspector panel)

I imagine that it would be something that makes sense to include as an editor. It could either be an option on the RoadManager itself, or a setting state on the Road Toolbar in the 3D editor as a checkbox. In both cases, it would define the _default_ value applied, so that way if you only want center-aligned, then it happens by default for every new RoadPoint, but doesn't affect pre-existing ones.


author_association: OWNER
user: login: antonkurkin
created_at: 2025-05-04T18:16:09Z
body: ah yeah default value in road container is a good idea. copying from the previous road point - that i suppose is already implemented with me adding alignment to RoadPoint.copy_settings_from?
I need to check how changing alignment for multiple road points work
author_association: CONTRIBUTOR
user: login: antonkurkin
created_at: 2025-05-04T22:25:46Z
body: i updated the code so the lanes are also created according to the selected alignment. edges, undo and default value of Alignment are still TODO
author_association: CONTRIBUTOR
user: login: antonkurkin
created_at: 2025-05-04T23:18:35Z
body: I checked that assigning alignment for multiple road points from road point panel is working. for some reason selection of the road points are lost after the operation. And if selected points have different values, it doesn't show it as I would expect in the dropdown menu but I think it is out of scope for this PR

I also see that undo/redo works somehow. not sure if I need to do something more?

And copying attributes required small change in _assign_copy_target (though not sure how to do multi-point apply). which is may be a sign that we need to deduplicate it with copy_settings_from? But also not sure that undo/redo would be fine with that, so I'll leave it as is

Also about default value from Road Container  - it seems like adding a point already copies the attribute from the one it's being created from. so that is going to be only for completely new road point but I'm not quite sure - do we need that? are there other default values that are being used for that? sorry I still didn't get around to get comfortable with the plugin GUI
author_association: CONTRIBUTOR
user: login: antonkurkin
created_at: 2025-05-05T21:11:59Z
body: So I updated edges, and I feel like it all works good, so I think it's ready for review
@TheDuckCow but for some reason procedural demo stopped assigning lane to the player after the rebase to the latest godot4 branch
also I see a bunch of errors
```
 res://addons/road-generator/plugin.gd:682 - Invalid call. Nonexistent function 'get_selected_nodes' in base 'Nil'.
  res://addons/road-generator/plugin.gd:113 - Invalid access to property or key 'InputMode' on a base object of type 'Nil'
```
from editor

I still didn't do anything about undo/redo and default alignment value in road container. I'm waiting for your input.
maybe you want to try how it works the way it is and then we could decide if anything else is needed?
author_association: CONTRIBUTOR
user: login: TheDuckCow
created_at: 2025-05-05T21:13:39Z
body: Thanks for letting me know, yeah I see the force push there - I'll take a look and see, there's a non-zero chance my branch messed something up there so it could be on me. Thanks for refreshing your base.
author_association: OWNER
user: login: TheDuckCow
created_at: 2025-05-05T22:45:08Z
body: Just confirming that the issue is present on the Godot 4 branch, really need to prioritize getting those unit tests back in place. I thought my change should have been backwards compatible, but I noticed a diff showed up around the RoadContainer's ai lane group name, implying something got modified. I'm still looking into it a bit on the godot4 branch.
author_association: OWNER
user: login: antonkurkin
created_at: 2025-05-06T11:39:58Z
body: had to rebase manually (as the automatic merge moved alignment from road generation to internal section)
checked that it works, set new alignment in the procedural generation demo
i think it's ready for merge
author_association: CONTRIBUTOR
user: login: antonkurkin
created_at: 2025-05-06T11:51:14Z
body: BTW should we move development to dev branch as godot3 support is discontinued?
author_association: CONTRIBUTOR
user: login: TheDuckCow
created_at: 2025-05-06T20:59:45Z
body: Great, I'll work on merging this later and doing the final sanity check - yes, with this next release I'm going to switch the branches around. I'm going to do that closer to the moment of the release itself.
author_association: OWNER
user: login: TheDuckCow
created_at: 2025-05-07T06:15:55Z
body: Merging now after an additional check, no unexpected concerns or issues arose.

Created #247 and #246 as I ran into these scenarios while testing, and though mostly understood before, I figure it's time I acknowledge them in a ticket to be worked in the future (after this upcoming release). This would include handling the interfacing edge roadpoint where one side is using geometric alignment while the is using divider alignment (likewise applies to lane counts and gutter profile).
author_association: OWNER