【Godot4.3】绘图函数的类化封装尝试——CanvasShape

概述

这是2024年7月份的一项工作,在研究外XML和SVG解析与生成过后,想到可以将自己写的绘图函数库ShapePoints拆分为图形类,于是就有了CanvasShape类。它包含了从填充、轮廓、阴影、虚线、顶点和中心点绘制的全部要素。只需要给定points属性和调用draw()方法就可以在CanvasItem节点上绘制大多数多边形和折线了。
CanvasShape与原来的绘图函数库体系
CanvasShapeGroup是一个可以包含多个CanvasShapeCanvasShapeGroup的元素,基于它,可以创建树形的图形元素结构。方便动态管理和输出保存。

是否创建子类型的考虑

原本的打算是基于类的继承形式,创建CanvasShape的子类型,来表示各种具体的图形元素,如果是单纯的绘制图形,完全没有必要写这些子类型。但是如果要进行文档化处理,就需要设计各种子类。所以好的思路是不去破坏原来的ShapePoints函数库,而是建立一套独立的CanvasShape体系。

CanvasShape

作为所有CanvasItem可绘制图形的基类。提供共同的属性和方法,尤其是图形参数、点集合和绘制选项,以及绘制方法。

# =============================================
# 名称:CanvasShape
# 类型:类
# 描述:CanvasItem绘图函数图形基类
# 作者:巽星石
# 创建时间:202472515:25:24
# 最后修改时间:202472520:23:58
# =============================================
class_name CanvasShape
# ============================== 属性 ==============================
# -------------------- 基础样式 
var border_width:int = 1               # 轮廓线宽度
var border_color:Color = Color.BLACK   # 轮廓线颜色
var fill_color:Color = Color.WHITE     # 填充颜色
var dash = 0.0                         # 虚线间隔
# -------------------- 路径点 
var points:PackedVector2Array = []     # 点集合
var close_path:bool = false            # 是否闭合轮廓线
# -------------------- 变换 
var position:Vector2 = Vector2.ZERO    # 绘制位置
# -------------------- 阴影设置 
var has_shadow:bool = false                   # 是否显示阴影
var shadow_color:Color = Color(Color.BLACK.lightened(0.1),0.6)       # 阴影颜色
var shadow_offset:Vector2 = Vector2(10,10)    # 阴影偏移
# -------------------- 绘制设置 
var draw_border:bool = true            # 是否绘制轮廓
var draw_fill:bool = true              # 是否绘制填充
var draw_points:bool = false           # 是否绘制顶点
var draw_position:bool = false         # 是否绘制局部坐标系原点
var draw_rect2:bool = false            # 是否绘制矩形范围

# 顶点绘制参数
var point_r = 3
var point_border_width:int = 1         # 顶点轮廓线宽度
var point_border_color:Color = Color.BLACK   # 顶点轮廓线颜色
var point_fill_color:Color = Color.WHITE     # 顶点填充颜色
# 矩形范围绘制参数
var rect2_border_width:int = 1               # 矩形轮廓线宽度
var rect2_border_color:Color = Color.AQUAMARINE   # 矩形轮廓线颜色

# ============================== 方法 ==============================
func draw(canvas:CanvasItem) -> void:
	# 对所有点进行位移变换
	var points = Transform2D().translated(position) * points
	
	# 1.绘制阴影
	if has_shadow:
		var shadow_points = Transform2D().translated(shadow_offset) * points
		canvas.draw_colored_polygon(shadow_points,shadow_color) 
	# 2.绘制矩形范围
	if draw_rect2:
		canvas.draw_rect(get_rect(),Color(rect2_border_color,0.5),false,rect2_border_width)
	# 3.绘制填充
	if draw_fill and fill_color:
		canvas.draw_colored_polygon(points,fill_color)
	# 4.绘制轮廓
	if draw_border and border_width!=0 and border_color!=null:
		var close_points = points.duplicate()
		# 闭合
		if close_path:
			close_points.append(close_points[0])
		if dash>0.0:  # 虚线间隔大于0
			for seg in segments(close_points):
				canvas.draw_dashed_line(seg[0],seg[1],border_color,border_width,dash)
			pass
		else:
			canvas.draw_polyline(close_points,border_color,border_width)   # 绘制实现闭合轮廓
	# 5.绘制顶点
	if draw_points:
		for p in points:
			# 绘制圆点
			canvas.draw_circle(p,point_r,point_fill_color)
			# 绘制边线
			canvas.draw_arc(p,point_r,0,TAU,TAU * point_r,point_border_color,point_border_width/2.0,true)
	# 6.绘制位置点
	if draw_position:
		var line_count = 4
		var ang = 360.0/float(line_count)  # 每次旋转角度
		for i in range(line_count):
			var p1 = position
			var p2 = p1 + pVector2(ang * i,5)
			canvas.draw_line(p1,p2,border_color,border_width)

# 获取图形的矩形
func get_rect() -> Rect2:
	# 对所有点进行位移变换
	var points = Transform2D().translated(position) * points
	# 拆分出X坐标和Y坐标数组
	var x_arr = []
	var y_arr = []
	for p in points:
		x_arr.append(p.x)
		y_arr.append(p.y)
	# 最小值构成Rect2的position
	var pos = Vector2(x_arr.min(),y_arr.min())
	
	# 最大值 - pos = Rect2 的 size
	var siz = Vector2(x_arr.max(),y_arr.max()) - pos
	
	print(pos,"   ",siz)
	return Rect2(pos,siz)

# ============================== 子类通用函数 ==============================
# 极坐标点函数 - 通过角度和长度定义一个点
func pVector2(angle:float = 0.0,length:float =0.0) -> Vector2:
	var dir = Vector2.RIGHT.rotated(deg_to_rad(angle))
	return dir * length

# points的点按顺序两两相连的所有线段
func segments(points:PackedVector2Array) -> Array[PackedVector2Array]:
	var arr:Array[PackedVector2Array]
	if points.size() >1:  # 至少有两个点
		for i in range(points.size() -1):
			var seg:PackedVector2Array = [points[i],points[i+1]]
			arr.append(seg)
	return arr

绘制折线

extends Node2D

func _draw() -> void:
	var shape = CanvasShape.new()
	shape.points = [Vector2(0,0),Vector2(50,50),Vector2(150,10),Vector2(250,100)]  # 绘制的点集合
	# 形状参数
	shape.position = Vector2(200,200)
	shape.close_path = false         # 不闭合
	# 绘制参数
	shape.border_width=2
	shape.border_color = Color.ORANGE_RED
	# 绘制选项
	shape.draw_fill = false
	shape.draw_points = true
	shape.draw_position = true
	shape.draw_rect2 = true
	# 绘制
	shape.draw(self)

折线,绘制顶点、Rect2和Position

绘制多边形

extends Node2D

func _draw() -> void:
	var shape = CanvasShape.new()
	shape.points = [Vector2(0,0),Vector2(50,50),Vector2(150,10),Vector2(250,10),Vector2(50,-50)]  # 绘制的点集合
	# 形状参数
	shape.position = Vector2(200,200)
	shape.close_path = true         # 闭合
	# 绘制参数
	shape.border_width=2
	shape.border_color = Color.ORANGE_RED
	shape.fill_color = Color.AQUAMARINE
	# 绘制选项
	shape.has_shadow = true
	# 绘制
	shape.draw(self)
	

带有阴影、轮廓和填充的多边形

结合ShapePoints使用

ShapePoints本身就是一个常见几何图形顶点求取函数库。通过将求得的图形顶点赋值给CanvasShape实例的points属性,然后就可以调用绘制函数绘制了。

extends  Node2D

func _draw() -> void:
	var shape  = CanvasShape.new()
    # 为CanvasShape实例指定求取的圆角矩形顶点
	shape.points = ShapePoints.round_rect(Vector2(150,100),[5,5,5,5])
	draw_set_transform(Vector2(200,200))  # 设定绘制原点
	shape.draw(self)

image.png
另一个实例是绘制五角星:

extends  Node2D

func _draw() -> void:
	var shape  = CanvasShape.new()
	# 为CanvasShape实例指定求取的圆角矩形顶点
	shape.points = ShapePoints.star()
	shape.position = Vector2(300,300) # 设定绘制原点
	
	shape.draw_points = true
	shape.draw_position = true
	shape.draw(self)

image.png

CanvasShapeGroup

以往的CanvasItem绘图,都是在_draw()中调用低级的绘图函数绘制。先调用的绘图函数先绘制。在实现CanvasShape之后,第一次有了图形的参数化和对象化。在此基础之上,可以创建一个列表形式,用于管理多个图形,按顺序绘制。并且我们将可以随时修改图形的绘制顺序。
基于这样的想法,我设计了一个名为CanvasShapeGroup的类,它的内部维护一个对象数组_shapes,用来存储CanvasShapeCanvasShapeGroup实例。CanvasShapeGroup也拥有draw()方法,不过其调用后是遍历并调用每个子元素CanvasShapeCanvasShapeGroupdraw()方法。
其实基于CanvasShapeGroup,已经可以实现树状的父子结构,可以用于创建和管理复杂的图形文档。
在这里插入图片描述

源代码

# =============================================
# 名称:CanvasShapeGroup
# 类型:类
# 描述:存放CanvasShapeCanvasShapeGroup的列表形式
#      多层嵌套可以组成树状结构
# 作者:巽星石
# 创建时间:202472522:55:00
# 最后修改时间:202472523:27:31
# =============================================
class_name CanvasShapeGroup
# ============================== 信号 ==============================
signal order_changed()   # 元素顺序发生改变时触发

# ============================== 属性 ==============================

var _shapes:Array  # 图形或图形组的列表

func _init() -> void:
	_shapes = []

# ============================== 方法 ==============================
# ---------------------------- 添加
# 追加形状
func append_shape(shape:CanvasShape) -> void:
	_shapes.append(shape)

# 追加形状分组
func append_shape_group(gup:CanvasShapeGroup) -> void:
	_shapes.append(gup)
# ---------------------------- 移动
# 移动至最上层
func move_to_topst(index:int) -> void:
	var ele = _shapes[index] # 暂存元素
	_shapes.remove_at(index) # 移除
	_shapes.push_back(ele)  # 添加到最后面
	emit_signal("order_changed")

#  移动至最下层
func move_to_bottomst(index:int) -> void:
	var ele = _shapes[index] # 暂存元素
	_shapes.remove_at(index) # 移除
	_shapes.push_front(ele)  # 添加到最前面
	emit_signal("order_changed")

# 往上一层
func move_to_top(index:int) -> void:
	var ele = _shapes[index] # 暂存元素
	_shapes.remove_at(index) # 移除
	_shapes.push_back(ele)  # 添加到最后面
	emit_signal("order_changed")

# 往下一层
func move_to_bottom(index:int) -> void:
	var ele = _shapes[index] # 暂存元素
	_shapes.remove_at(index) # 移除
	_shapes.push_back(ele)  # 添加到最后面
	emit_signal("order_changed")
# ---------------------------- 绘制
# 遍历调用图形和分组的draw()方法
func draw(canvas:CanvasItem) -> void:
	for shape in _shapes:
		shape.draw(canvas)

绘制测试

extends Node2D

var shapes = CanvasShapeGroup.new()  # 图形列表


func _ready() -> void:
	# 处理 CanvasShapeGroup 的 order_changed 信号
	shapes.order_changed.connect(func():
		queue_redraw()
	)
	
	# 六边形
	var shape1 = CanvasRegularPolygon.new()
	shape1.position = Vector2(200,200)
	shape1.edges = 6
	shape1.r = 50
	shape1.close_path = true
	shapes.append_shape(shape1)
	# 五边形
	var shape2 = CanvasRegularPolygon.new()
	shape2.position = Vector2(230,250)
	shape2.edges = 5
	shape2.r = 50
	shape2.close_path = true
	shapes.append_shape(shape2)


func _draw() -> void:
	# 绘制
	shapes.draw(self)


func _on_top_btn_pressed() -> void:
	shapes.move_to_topst(0)
	pass

我们创建了包含两个图形(一个正六边形,一个正五边形)的CanvasShapeGroup,默认会按先后顺序进行绘制。
我们用一个按钮测试,对CanvasShapeGroup中最前面的图形,执行置顶操作。因为只有两个图形,所以两个图形的顺序会反复交替。
image.png

后期设想

  • 通过矩形检测鼠标位置是否在矩形内,可以判断图形是否被选择
  • 通过绘制矩形选框,可以判断图形的矩形是否完全在选框内,或者与选框有部分交集,从而实现矩形框的选择形式
  • 通过右键菜单或快捷键形式,可以设定图形的绘制顺序
  • CanvasShapeGroup可以实现嵌套的树形结构,可以通过遍历和递归来获取图形的SVG或自定义XML、JSON或ConfigFile形式
  • 可以用Tree控件查看和编辑整个页面的元素。
  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巽星石

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值