class_name PropThumbnail extends Node const PROP_BLOCK := preload("res://scripts/prop_block.gd") const SIZE := Vector2i(200, 200) var _viewport: SubViewport var _world: Node2D var _camera: Camera2D func _ready() -> void: _viewport = SubViewport.new() _viewport.size = SIZE _viewport.transparent_bg = true _viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS add_child(_viewport) _world = Node2D.new() _viewport.add_child(_world) _camera = Camera2D.new() _camera.enabled = true _viewport.add_child(_camera) _camera.make_current() func render(payload: Dictionary, material_preset: int) -> Texture2D: _clear_world() var points := _compute_points(payload) var fill := Color(payload.get("fill_color", Color.WHITE)) var outline := Color(payload.get("outline_color", Color.BLACK)) if material_preset != PROP_BLOCK.MaterialPreset.NONE: var tint := PROP_BLOCK.tint_for(material_preset) fill = tint outline = tint.darkened(0.55) var outline_width := float(payload.get("outline_width", 2.0)) var visual := Node2D.new() var poly := Polygon2D.new() poly.polygon = points poly.color = fill var line := Line2D.new() var loop := points.duplicate() if not loop.is_empty(): loop.append(points[0]) line.points = loop line.default_color = outline line.width = outline_width line.joint_mode = Line2D.LINE_JOINT_ROUND line.begin_cap_mode = Line2D.LINE_CAP_ROUND line.end_cap_mode = Line2D.LINE_CAP_ROUND visual.add_child(poly) visual.add_child(line) _world.add_child(visual) _frame_camera(_points_bbox(points)) await RenderingServer.frame_post_draw await RenderingServer.frame_post_draw var tex := _viewport.get_texture() visual.queue_free() if tex == null: return null var img := tex.get_image() if _is_blank(img): return null return ImageTexture.create_from_image(img) func _compute_points(payload: Dictionary) -> PackedVector2Array: var shape_type: int = int(payload.get("type", PROP_BLOCK.ShapeType.POLYGON)) if shape_type == PROP_BLOCK.ShapeType.CIRCLE: var radius := float(payload.get("radius", 24.0)) var loop := PackedVector2Array() for i: int in PROP_BLOCK.CIRCLE_SEGMENTS: var angle: float = TAU * float(i) / float(PROP_BLOCK.CIRCLE_SEGMENTS) loop.append(Vector2(cos(angle), sin(angle)) * radius) return loop return payload.get("points", PackedVector2Array()) func _points_bbox(points: PackedVector2Array) -> Rect2: if points.is_empty(): return Rect2(Vector2(-24, -24), Vector2(48, 48)) var rect := Rect2(points[0], Vector2.ZERO) for p: Vector2 in points: rect = rect.expand(p) if rect.size.x < 1.0 or rect.size.y < 1.0: return Rect2(Vector2(-24, -24), Vector2(48, 48)) return rect func _clear_world() -> void: for child: Node in _world.get_children(): child.queue_free() func _frame_camera(bbox: Rect2) -> void: var margin := 12.0 var fit := minf((SIZE.x - margin * 2.0) / bbox.size.x, (SIZE.y - margin * 2.0) / bbox.size.y) _camera.position = bbox.get_center() _camera.zoom = Vector2(maxf(fit, 0.05), maxf(fit, 0.05)) func _is_blank(img: Image) -> bool: if img == null or img.is_empty(): return true var used := img.get_used_rect() if used.size.x <= 0 or used.size.y <= 0: return true for y: int in range(used.position.y, used.end.y): for x: int in range(used.position.x, used.end.x): if img.get_pixel(x, y).a > 0.0: return false return true