Recreating Similar View to Godot's 2D Viewport Rulers & Grids

Godot Version

4.6.stable.official [89cea1439]

Question

Hello! I am attempting to implement a UI similar to Godot’s 2D viewport, in which rulers and a grid-system will assist the player in finding objects in the scene. I have some basic working functionality, but I’m struggling to make it so that the ruler “notches” and “degrees” maintain consistent positioning when the player zooms using a Camera2D. My current approach is to multiply the spacing by the camera zoom, but that doesn’t work as desired as the lines shift too much from their initial positions and thus become inaccurate for locations they’re meant to be associated with.

I’m thinking I may need to switch to an approach that aligns the ruler lines with screen coordinates rather than relying on my own more arbitrary spacing, but I haven’t yet thought of a good way to do that (I’m not sure how to account for the camera’s zoom shifting the positions of visuals on the screen while not actually changing the positions of objects). I thought I’d post here to see if anyone had suggestions or ideas on how to best implement such a system.

Below is my current code if you want some idea of where I’m currently at in the process, but I am open to scraping it all if there’s a better approach overall.

Any input would be appreciated, as I tried searching for similar implementations and couldn’t find any posts or guides on how to go about doing something like this.

func camera_zoom_change(zoom_level):
	if focused_on:
		#If zoomed in, higher number, we need to move the degrees panel faster, a smaller number for us
		#if zoomed out, lower zoom level number, we need to move the degrees panel slower, a higher number for us
		for zoom_key in zoom_speed_match:
			if zoom_key == str(zoom_level):
				speed = zoom_speed_match[zoom_key]
				
		current_zoom = zoom_level # Update the current zoom level so we can change our tick distances
		#Que the redraw function so that we update our lines per zoom change. We really only need to update
		#our drawn lines in this instance if there's a zoom change? I think.
		queue_redraw()

func _draw() -> void:
	#Our draw function handles drawing the notch and degree lines for the vertical bar.
	var psize = get_parent().size.y #Our total size of the Panel node so we can make sure to cover it all
	var ypos_start = 2.00 # For the vertical line running down through all the notches and degrees
	var ypos_end = 0.0 # For the vertical line running through, where it'll eventually stop
	var ypos_iterate = 2.00 # Our shifting y notch/degree line, will change as we iterate down. Formerly self.position.y - 14
	var xpos_start_notch = get_parent().position.x + 25.0 # notches are shorter ones between each major degree
	var xpos_end_notch = xpos_start_notch + 5.0 #End point for our horizontal notch line
	var xpos_start_degree = get_parent().position.x + 18.0 #Start point for our horizontal degree lines (longer than notch)
	var xpos_end_degree = xpos_start_degree + 20.0 #end point for horizontal degree line
	#We build two PackedVector2Arrays which will be added to as we iterate, each addition is a new horizontal x line drawn at the next vertical y position
	var notch = PackedVector2Array([Vector2(xpos_start_notch, ypos_iterate), Vector2(xpos_end_notch, ypos_iterate)])
	var degree = PackedVector2Array([Vector2(xpos_start_degree, ypos_iterate), Vector2(xpos_end_degree, ypos_iterate)])
	var degree_notch = 2 # We'll use this to determine how many notches between a degree, we want 4, but we start at 2 to get the correct centering
	var degree_count = 1 # A running count of how many degrees we've added so far
	
	# We subtract iterate down the total size of the panel to make sure we're drawing along its full length
	while psize > 0:
		psize -= 1 # Subtract 1 each iteration
		ypos_iterate += 6 * current_zoom # This is the distance in y position cordinates between each notch/degree
		notch.append(Vector2(xpos_start_notch, ypos_iterate)) #Add a new start pos for line to our notch array
		notch.append(Vector2(xpos_end_notch, ypos_iterate)) #Add a new end pos for the line
		degree_notch += 1 #Add 1 to our degree count as we want to add a degree every 4 notches
		#Here though we're saying 5 because we want 4 notches between each degree, so every 5th notch is a degree
		if degree_notch == 5:
			# Add the current position to our degree array then reset our degree count for next iteration
			degree.append(Vector2(xpos_start_degree, ypos_iterate))
			degree.append(Vector2(xpos_end_degree, ypos_iterate))
			degree_notch = 0
			degree_count += 1
			position_degree_text(degree_count, ypos_iterate) #Send our count and the yposition to the function for label placement in relation to degree notches
	ypos_end = ypos_iterate # End position is just the last ypos_iterate, may not need
	
	#Using this to then implement the above array, drawing two separate lines using those coordinates of beginning and end for each, then using color and width variables for qualities of line.
	draw_multiline(notch, color, width) # multilines for all our notches
	draw_multiline(degree, color, width) # multilines for all our degrees
	# We then draw one singular long line vertically through all notches/degrees
	draw_line(Vector2(xpos_start_notch, ypos_start), Vector2(xpos_start_notch, ypos_end), color, width)

Multiply viewport’s rect with the inverse of viewport’s canvas_transform. This will give you the world space rect that is currently displayed by the viewport.