@icon("icon.svg")
extends RefCounted
class_name Brush

## Brush settings.

enum BrushProjection {
	SCREEN, ## The brush is projected from the camera view.
	SURFACE, ## The brush is projected into the tangent space of the mesh.
}

enum SizeSpace {
	SCREEN,
	SURFACE,
	UV,
}

enum Symmetry {
	NONE, ## No symmetry.
	MIRROR, ## The brush is mirrored on each non-null axis of `symmetry_axis`.
	RADIAL, ## The brush is rotated around the `symmetry_axis`. The amount is specified by `radial_symmetry_count`.
}

## The scale of the brush tip.
var size := 1.0
## The projection which projects the mouse position into brush space.
var projection := BrushProjection.SURFACE
## If the result should be removed by the opacity of the stroke.
var size_space := SizeSpace.SURFACE
var erase := false
## The texture of each channel.
var textures: Array[Texture2D]
## The color each channel ist tinted.
var colors: Array[Color]
## The scale of the textures.
var pattern_scale := 1.0
## If the size of the tip is affected by pen pressure.
var size_pen_pressure := false
## If the opacity of the tip is affected by pen pressure.
var flow_pen_pressure := false
## The opacity of a single brush.
var flow := 1.0
## The maximimum opacity of a single brush strokes.
var stroke_opacity := 1.0
## The randomness of the size of the brush.
var size_jitter := 0.0
## The random offset of the painting position.
var position_jitter := 0.0
## The type of symmetry to use.
var symmetry := Symmetry.NONE
## An axis is enabled if it is anything other than zero.
## This is used as the axis for radial symmetry.
var symmetry_axis: Vector3
## The amount of symmetry axis.
var radial_symmetry_count := 2
## The randomness of the angle.
var angle_jitter := 0.0
## The minimum distance between dots.
var spacing := 0.3
## The angle of the brush tip.
var angle := 0.0
## If the angle of the tip should point from the last brush stroke to the next.
## If the angle isn't zero, it is added ontop of that.
var follow_path := false
## The texture that determines the opacity of the stroke.
var tip: Texture2D

## A screen-space texture which opacity is multiplied by the strength of the
## stroke.
var stencil : Texture2D
## The stencil transform in view-space.
var stencil_transform : Transform2D

## Returns the color a channel texture is tintet, white if not specified.
func get_color(channel : int) -> Color:
	return Color.WHITE if colors.size() <= channel or colors[channel] == null else colors[channel]


## Returns the texture a channel texture is tintet, white if not specified.
func get_texture(channel : int) -> Texture2D:
	return null if textures.size() <= channel else textures[channel]


## Returns a new brush with the same settings.
func duplicate() -> Object:
	return dict_to_inst(inst_to_dict(self))

# TODO: probably move these somewhere else, some util class maybe?

## Returns the list of transforms that result when the given transform is
## mirrored using this brush's symmetry options.
func apply_symmetry(transform : Transform3D) -> Array[Transform3D]:
	if symmetry and symmetry_axis == Vector3():
		push_warning("Using symmetry but no symmetry axis is set.")
	match symmetry:
		Symmetry.MIRROR:
			var transforms : Array[Transform3D] = [transform]
			if symmetry_axis.x:
				transforms = _get_mirrored(transforms, Basis.FLIP_X)
			if symmetry_axis.y:
				transforms = _get_mirrored(transforms, Basis.FLIP_Y)
			if symmetry_axis.z:
				transforms = _get_mirrored(transforms, Basis.FLIP_Z)
			return transforms
		Symmetry.RADIAL:
			var transforms : Array[Transform3D] = []
			for symmetry_num in radial_symmetry_count:
				var angle : float = TAU / radial_symmetry_count * symmetry_num
				var rotated := transform.rotated(symmetry_axis, angle)
				transforms.append(rotated)
			return transforms
	var transforms : Array[Transform3D]
	transforms.append(transform)
	return transforms


## Returns the given transform with its rotation and origin mirrored using a
## basis.
static func _get_mirrored(transforms : Array, flip_basis : Basis) -> Array:
	var new := transforms.duplicate()
	for transform in transforms:
		var new_transform : Transform3D = transform
		new_transform.origin = flip_basis * transform.origin
		new_transform.basis = flip_basis * transform.basis
		new.append(new_transform)
	return new