统一缩进为制表符

master
chendian 9 months ago
parent 71119f326e
commit e885c52f20

@ -1,9 +1,2 @@
# Editor configuration, see https://editorconfig.org
root = true
[*.gd]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
root = true

@ -1,99 +1,109 @@
const Faces = preload("./Faces.gd");
const vox_to_godot = Basis(Vector3.RIGHT, Vector3.FORWARD, Vector3.UP);
func generate(vox, voxel_data, scale, snaptoground):
var generator = VoxelMeshGenerator.new(vox, voxel_data, scale, snaptoground);
var generator = VoxelMeshGenerator.new(vox, voxel_data, scale, snaptoground);
return generator.generate_mesh();
return generator.generate_mesh();
class MeshGenerator:
var surfaces = {};
func ensure_surface_exists(surface_index: int, color: Color, material: Material):
if (surfaces.has(surface_index)): return;
var st = SurfaceTool.new();
st.begin(Mesh.PRIMITIVE_TRIANGLES);
st.set_color(color);
st.set_material(material);
surfaces[surface_index] = st;
func add_vertex(surface_index: int, vertex: Vector3):
var st = surfaces[surface_index] as SurfaceTool;
st.add_vertex(vertex);
func combine_surfaces():
var mesh = null;
for surface_index in surfaces:
var surface = surfaces[surface_index] as SurfaceTool;
surface.index();
surface.generate_normals();
mesh = surface.commit(mesh);
var new_surface_index = mesh.get_surface_count() - 1;
var name = str(surface_index);
mesh.surface_set_name(new_surface_index, name);
return mesh;
var surfaces = {};
func ensure_surface_exists(surface_index: int, color: Color, material: Material):
if (surfaces.has(surface_index)): return;
var st = SurfaceTool.new();
st.begin(Mesh.PRIMITIVE_TRIANGLES);
st.set_color(color);
st.set_material(material);
surfaces[surface_index] = st;
func add_vertex(surface_index: int, vertex: Vector3):
var st = surfaces[surface_index] as SurfaceTool;
st.add_vertex(vertex);
func combine_surfaces():
var mesh = null;
for surface_index in surfaces:
var surface = surfaces[surface_index] as SurfaceTool;
surface.index();
surface.generate_normals();
mesh = surface.commit(mesh);
var new_surface_index = mesh.get_surface_count() - 1;
var name = str(surface_index);
mesh.surface_set_name(new_surface_index, name);
return mesh;
class VoxelMeshGenerator:
var vox;
var voxel_data = {};
var scale:float;
var snaptoground:bool;
func _init(vox,voxel_data,scale,snaptoground):
self.vox = vox;
self.voxel_data = voxel_data;
self.scale = scale;
self.snaptoground = snaptoground;
func get_material(voxel):
var surface_index = voxel_data[voxel];
return vox.materials[surface_index]
func face_is_visible(voxel, face):
if (not voxel_data.has(voxel + face)):
return true;
var local_material = get_material(voxel);
var adj_material = get_material(voxel + face);
return adj_material.is_glass() && !local_material.is_glass();
func generate_mesh():
# Minimum extends of the volume
var mins :Vector3 = Vector3(1000000, 1000000, 1000000)
# Maximum extends of the volume
var maxs :Vector3 = Vector3(-1000000,-1000000,-1000000)
# Find bounds
for v in voxel_data:
mins.x = min(mins.x, v.x)
mins.y = min(mins.y, v.y)
mins.z = min(mins.z, v.z)
maxs.x = max(maxs.x, v.x)
maxs.y = max(maxs.y, v.y)
maxs.z = max(maxs.z, v.z)
var vox_to_godot = Basis(Vector3.RIGHT, Vector3.FORWARD, Vector3.UP);
var yoffset = Vector3(0,0,0);
if snaptoground : yoffset = Vector3(0, -mins.z * scale, 0);
var gen = MeshGenerator.new();
for voxel in voxel_data:
var voxelSides = []
if face_is_visible(voxel, Vector3.UP): voxelSides += Faces.Top
if face_is_visible(voxel, Vector3.DOWN): voxelSides += Faces.Bottom
if face_is_visible(voxel, Vector3.LEFT): voxelSides += Faces.Left
if face_is_visible(voxel, Vector3.RIGHT): voxelSides += Faces.Right
if face_is_visible(voxel, Vector3.BACK): voxelSides += Faces.Front
if face_is_visible(voxel, Vector3.FORWARD): voxelSides += Faces.Back
var surface_index = voxel_data[voxel];
var color = vox.colors[surface_index];
var material = vox.materials[surface_index].get_material(color);
gen.ensure_surface_exists(surface_index, color, material);
for t in voxelSides:
gen.add_vertex(surface_index, yoffset + vox_to_godot * (t + voxel) * scale);
return gen.combine_surfaces();
var vox;
var voxel_data = {};
var scale: float;
var snaptoground: bool;
func _init(vox, voxel_data, scale, snaptoground):
self.vox = vox;
self.voxel_data = voxel_data;
self.scale = scale;
self.snaptoground = snaptoground;
func get_material(voxel):
var surface_index = voxel_data[voxel];
return vox.materials[surface_index]
func face_is_visible(voxel, face):
if (not voxel_data.has(voxel + face)):
return true;
var local_material = get_material(voxel);
var adj_material = get_material(voxel + face);
return adj_material.is_glass() && !local_material.is_glass();
func generate_mesh():
# Minimum extends of the volume
var mins: Vector3 = Vector3(1000000, 1000000, 1000000)
# Maximum extends of the volume
var maxs: Vector3 = Vector3(-1000000, -1000000, -1000000)
# Find bounds
for v in voxel_data:
mins.x = min(mins.x, v.x)
mins.y = min(mins.y, v.y)
mins.z = min(mins.z, v.z)
maxs.x = max(maxs.x, v.x)
maxs.y = max(maxs.y, v.y)
maxs.z = max(maxs.z, v.z)
var vox_to_godot = Basis(Vector3.RIGHT, Vector3.FORWARD, Vector3.UP);
var yoffset = Vector3(0, 0, 0);
if snaptoground: yoffset = Vector3(0, -mins.z * scale, 0);
var gen = MeshGenerator.new();
for voxel in voxel_data:
var voxelSides = []
if face_is_visible(voxel, Vector3.UP): voxelSides += Faces.Top
if face_is_visible(voxel, Vector3.DOWN): voxelSides += Faces.Bottom
if face_is_visible(voxel, Vector3.LEFT): voxelSides += Faces.Left
if face_is_visible(voxel, Vector3.RIGHT): voxelSides += Faces.Right
if face_is_visible(voxel, Vector3.BACK): voxelSides += Faces.Front
if face_is_visible(voxel, Vector3.FORWARD): voxelSides += Faces.Back
var surface_index = voxel_data[voxel];
var color = vox.colors[surface_index];
var material = vox.materials[surface_index].get_material(color);
gen.ensure_surface_exists(surface_index, color, material);
for t in voxelSides:
gen.add_vertex(surface_index, yoffset + vox_to_godot * (t + voxel) * scale);
return gen.combine_surfaces();

@ -1,59 +1,54 @@
const Top = [
Vector3( 1.0000, 1.0000, 1.0000),
Vector3( 0.0000, 1.0000, 1.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 1.0000, 1.0000, 0.0000),
Vector3( 1.0000, 1.0000, 1.0000),
];
Vector3( 1.0000, 1.0000, 1.0000),
Vector3( 0.0000, 1.0000, 1.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 1.0000, 1.0000, 0.0000),
Vector3( 1.0000, 1.0000, 1.0000),
];
const Bottom = [
Vector3( 0.0000, 0.0000, 0.0000),
Vector3( 0.0000, 0.0000, 1.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 1.0000, 0.0000, 0.0000),
Vector3( 0.0000, 0.0000, 0.0000),
];
Vector3( 0.0000, 0.0000, 0.0000),
Vector3( 0.0000, 0.0000, 1.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 1.0000, 0.0000, 0.0000),
Vector3( 0.0000, 0.0000, 0.0000),
];
const Front = [
Vector3( 0.0000, 1.0000, 1.0000),
Vector3( 1.0000, 1.0000, 1.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 0.0000, 0.0000, 1.0000),
Vector3( 0.0000, 1.0000, 1.0000),
];
Vector3( 0.0000, 1.0000, 1.0000),
Vector3( 1.0000, 1.0000, 1.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 0.0000, 0.0000, 1.0000),
Vector3( 0.0000, 1.0000, 1.0000),
];
const Back = [
Vector3( 1.0000, 0.0000, 0.0000),
Vector3( 1.0000, 1.0000, 0.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 0.0000, 0.0000, 0.0000),
Vector3( 1.0000, 0.0000, 0.0000)
];
Vector3( 1.0000, 0.0000, 0.0000),
Vector3( 1.0000, 1.0000, 0.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 0.0000, 0.0000, 0.0000),
Vector3( 1.0000, 0.0000, 0.0000)
];
const Left = [
Vector3( 0.0000, 1.0000, 1.0000),
Vector3( 0.0000, 0.0000, 1.0000),
Vector3( 0.0000, 0.0000, 0.0000),
Vector3( 0.0000, 0.0000, 0.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 0.0000, 1.0000, 1.0000),
];
Vector3( 0.0000, 1.0000, 1.0000),
Vector3( 0.0000, 0.0000, 1.0000),
Vector3( 0.0000, 0.0000, 0.0000),
Vector3( 0.0000, 0.0000, 0.0000),
Vector3( 0.0000, 1.0000, 0.0000),
Vector3( 0.0000, 1.0000, 1.0000),
];
const Right = [
Vector3( 1.0000, 1.0000, 1.0000),
Vector3( 1.0000, 1.0000, 0.0000),
Vector3( 1.0000, 0.0000, 0.0000),
Vector3( 1.0000, 0.0000, 0.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 1.0000, 1.0000, 1.0000),
];
Vector3( 1.0000, 1.0000, 1.0000),
Vector3( 1.0000, 1.0000, 0.0000),
Vector3( 1.0000, 0.0000, 0.0000),
Vector3( 1.0000, 0.0000, 0.0000),
Vector3( 1.0000, 0.0000, 1.0000),
Vector3( 1.0000, 1.0000, 1.0000),
];

@ -1,227 +1,222 @@
const Faces = preload("./Faces.gd")
const VoxData = preload("./VoxFormat/VoxData.gd")
const Faces = preload("./Faces.gd")
const VoxData = preload("./VoxFormat/VoxData.gd")
const vox_to_godot = Basis(Vector3.RIGHT, Vector3.FORWARD, Vector3.UP)
# Names for the faces by orientation
enum FaceOrientation {
Top = 0,
Bottom = 1,
Left = 2,
Right = 3,
Front = 4,
Back = 5,
Top = 0,
Bottom = 1,
Left = 2,
Right = 3,
Front = 4,
Back = 5,
}
# An Array(FaceOrientation) of all possible face orientations
const face_orientations :Array = [
FaceOrientation.Top,
FaceOrientation.Bottom,
FaceOrientation.Left,
FaceOrientation.Right,
FaceOrientation.Front,
FaceOrientation.Back
]
const face_orientations: Array = [
FaceOrientation.Top,
FaceOrientation.Bottom,
FaceOrientation.Left,
FaceOrientation.Right,
FaceOrientation.Front,
FaceOrientation.Back
]
# An Array(int) of the depth axis by orientation
const depth_axis :Array = [
Vector3.AXIS_Z,
Vector3.AXIS_Z,
Vector3.AXIS_X,
Vector3.AXIS_X,
Vector3.AXIS_Y,
Vector3.AXIS_Y,
]
const depth_axis: Array = [
Vector3.AXIS_Z,
Vector3.AXIS_Z,
Vector3.AXIS_X,
Vector3.AXIS_X,
Vector3.AXIS_Y,
Vector3.AXIS_Y,
]
# An Array(int) of the width axis by orientation
const width_axis :Array = [
Vector3.AXIS_Y,
Vector3.AXIS_Y,
Vector3.AXIS_Z,
Vector3.AXIS_Z,
Vector3.AXIS_X,
Vector3.AXIS_X,
]
const width_axis: Array = [
Vector3.AXIS_Y,
Vector3.AXIS_Y,
Vector3.AXIS_Z,
Vector3.AXIS_Z,
Vector3.AXIS_X,
Vector3.AXIS_X,
]
# An Array(int) of height axis by orientation
const height_axis :Array = [
Vector3.AXIS_X,
Vector3.AXIS_X,
Vector3.AXIS_Y,
Vector3.AXIS_Y,
Vector3.AXIS_Z,
Vector3.AXIS_Z,
]
const height_axis: Array = [
Vector3.AXIS_X,
Vector3.AXIS_X,
Vector3.AXIS_Y,
Vector3.AXIS_Y,
Vector3.AXIS_Z,
Vector3.AXIS_Z,
]
# An Array(Vector3) describing what vectors to use to check for face occlusion
# by orientation
const face_checks :Array = [
Vector3(0, 0, 1),
Vector3(0, 0, -1),
Vector3(-1, 0, 0),
Vector3(1, 0, 0),
Vector3(0, -1, 0),
Vector3(0, 1, 0),
]
const face_checks: Array = [
Vector3(0, 0, 1),
Vector3(0, 0, -1),
Vector3(-1, 0, 0),
Vector3(1, 0, 0),
Vector3(0, -1, 0),
Vector3(0, 1, 0),
]
# An array of the face meshes by orientation
const face_meshes :Array = [
Faces.Front,
Faces.Back,
Faces.Left,
Faces.Right,
Faces.Bottom,
Faces.Top,
]
const face_meshes: Array = [
Faces.Front,
Faces.Back,
Faces.Left,
Faces.Right,
Faces.Bottom,
Faces.Top,
]
# An Array(Vector3) describing what normals to use by orientation
const normals :Array = [
Vector3(0, 1, 0),
Vector3(0, -1, 0),
Vector3(-1, 0, 0),
Vector3(1, 0, 0),
Vector3(0, 0, 1),
Vector3(0, 0, -1),
]
const normals: Array = [
Vector3(0, 1, 0),
Vector3(0, -1, 0),
Vector3(-1, 0, 0),
Vector3(1, 0, 0),
Vector3(0, 0, 1),
Vector3(0, 0, -1),
]
# The SurfaceTool the object will use to generate the mesh
var st :SurfaceTool = SurfaceTool.new()
var st: SurfaceTool = SurfaceTool.new()
# A Dictonary[Vector3]int of the voxel data for the visible faces of the
# current slice
var faces :Dictionary
var faces: Dictionary
# Minimum extends of the volume
var mins :Vector3 = Vector3(1000000, 1000000, 1000000)
var mins: Vector3 = Vector3(1000000, 1000000, 1000000)
# Maximum extends of the volume
var maxs :Vector3 = Vector3(-1000000,-1000000,-1000000)
var maxs: Vector3 = Vector3(-1000000, -1000000, -1000000)
# Generate a mesh for the given voxel_data with single-pass greedy face merging
# Primary RefCounted: https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/
# Secondary RefCounted: https://www.gedge.ca/dev/2014/08/17/greedy-voxel-meshing
# voxel_data is a dict[Vector3]int
func generate(vox :VoxData, voxel_data :Dictionary, scale :float, snaptoground : bool):
# Remeber, MagicaVoxel thinks Y is the depth axis. We convert to the correct
# coordinate space when we generate the faces.
st.begin(Mesh.PRIMITIVE_TRIANGLES)
# Short-circut empty models
if voxel_data.size() == 0:
return st.commit()
# Convert voxel data to raw color values
for v in voxel_data:
voxel_data[v] = vox.colors[voxel_data[v]]
# Find bounds
for v in voxel_data:
mins.x = min(mins.x, v.x)
mins.y = min(mins.y, v.y)
mins.z = min(mins.z, v.z)
maxs.x = max(maxs.x, v.x)
maxs.y = max(maxs.y, v.y)
maxs.z = max(maxs.z, v.z)
# Itterate over all face orientations to reduce problem to 3 dimensions
for o in face_orientations:
generate_geometry_for_orientation(voxel_data, o, scale, snaptoground)
# Finish the mesh and material and return
var material = StandardMaterial3D.new()
material.vertex_color_is_srgb = true
material.vertex_color_use_as_albedo = true
material.roughness = 1
st.set_material(material)
return st.commit()
func generate(vox: VoxData, voxel_data: Dictionary, scale: float, snaptoground: bool):
# Remeber, MagicaVoxel thinks Y is the depth axis. We convert to the correct
# coordinate space when we generate the faces.
st.begin(Mesh.PRIMITIVE_TRIANGLES)
# Short-circut empty models
if voxel_data.size() == 0:
return st.commit()
# Convert voxel data to raw color values
for v in voxel_data:
voxel_data[v] = vox.colors[voxel_data[v]]
# Find bounds
for v in voxel_data:
mins.x = min(mins.x, v.x)
mins.y = min(mins.y, v.y)
mins.z = min(mins.z, v.z)
maxs.x = max(maxs.x, v.x)
maxs.y = max(maxs.y, v.y)
maxs.z = max(maxs.z, v.z)
# Itterate over all face orientations to reduce problem to 3 dimensions
for o in face_orientations:
generate_geometry_for_orientation(voxel_data, o, scale, snaptoground)
# Finish the mesh and material and return
var material = StandardMaterial3D.new()
material.vertex_color_is_srgb = true
material.vertex_color_use_as_albedo = true
material.roughness = 1
st.set_material(material)
return st.commit()
# Generates all of the geometry for a given face orientation
func generate_geometry_for_orientation(voxel_data :Dictionary, o :int, scale :float, snaptoground :bool) -> void:
# Sweep through the volume along the depth reducing the problem to 2 dimensional
var da :int = depth_axis[o]
for slice in range(mins[da], maxs[da]+1):
var faces :Dictionary = query_slice_faces(voxel_data, o, slice)
if faces.size() > 0:
generate_geometry(faces, o, slice, scale, snaptoground)
func generate_geometry_for_orientation(voxel_data: Dictionary, o: int, scale: float, snaptoground: bool) -> void:
# Sweep through the volume along the depth reducing the problem to 2 dimensional
var da: int = depth_axis[o]
for slice in range(mins[da], maxs[da]+1):
var faces: Dictionary = query_slice_faces(voxel_data, o, slice)
if faces.size() > 0:
generate_geometry(faces, o, slice, scale, snaptoground)
# Returns the voxels in the set voxel_data with a visible face along the slice
# for the given orientation
func query_slice_faces(voxel_data :Dictionary, o :int, slice :float) -> Dictionary:
var ret :Dictionary = Dictionary()
var da = depth_axis[o]
for v in voxel_data:
if v[da] == slice and voxel_data.has(v + face_checks[o]) == false:
ret[v] = voxel_data[v]
return ret
func query_slice_faces(voxel_data: Dictionary, o: int, slice: float) -> Dictionary:
var ret: Dictionary = Dictionary()
var da = depth_axis[o]
for v in voxel_data:
if v[da] == slice and voxel_data.has(v + face_checks[o]) == false:
ret[v] = voxel_data[v]
return ret
# Generates geometry for the given orientation for the set of faces
func generate_geometry(faces :Dictionary, o :int, slice :float, scale :float, snaptoground :bool) -> void:
var da :int = depth_axis[o]
var wa :int = width_axis[o]
var ha :int = height_axis[o]
var v :Vector3 = Vector3()
v[da] = slice
# Itterate the rows of the sparse volume
v[ha] = mins[ha]
while v[ha] <= maxs[ha]:
# Itterate over the voxels of the row
v[wa] = mins[wa]
while v[wa] <= maxs[wa]:
if faces.has(v):
generate_geometry_for_face(faces, v, o, scale, snaptoground)
v[wa] += 1.0
v[ha] += 1.0
func generate_geometry(faces: Dictionary, o: int, slice: float, scale: float, snaptoground: bool) -> void:
var da: int = depth_axis[o]
var wa: int = width_axis[o]
var ha: int = height_axis[o]
var v: Vector3 = Vector3()
v[da] = slice
# Itterate the rows of the sparse volume
v[ha] = mins[ha]
while v[ha] <= maxs[ha]:
# Itterate over the voxels of the row
v[wa] = mins[wa]
while v[wa] <= maxs[wa]:
if faces.has(v):
generate_geometry_for_face(faces, v, o, scale, snaptoground)
v[wa] += 1.0
v[ha] += 1.0
# Generates the geometry for the given face and orientation and scale and returns
# the set of remaining faces
func generate_geometry_for_face(faces :Dictionary, face :Vector3, o :int, scale :float, snaptoground :bool) -> Dictionary:
var da :int = depth_axis[o]
var wa :int = width_axis[o]
var ha :int = height_axis[o]
# Greedy face merging
var width :int = width_query(faces, face, o)
var height :int = height_query(faces, face, o, width)
var grow :Vector3 = Vector3(1, 1, 1)
grow[wa] *= width
grow[ha] *= height
# Generate geometry
var yoffset = Vector3(0,0,0);
if snaptoground : yoffset = Vector3(0, -mins.z * scale, 0);
st.set_color(faces[face])
st.set_normal(normals[o])
for vert in face_meshes[o]:
st.add_vertex(yoffset + vox_to_godot * ((vert * grow) + face) * scale)
# Remove these faces from the pool
var v :Vector3 = Vector3()
v[da] = face[da]
for iy in range(height):
v[ha] = face[ha] + float(iy)
for ix in range(width):
v[wa] = face[wa] + float(ix)
faces.erase(v)
return faces
func generate_geometry_for_face(faces: Dictionary, face: Vector3, o: int, scale: float, snaptoground: bool) -> Dictionary:
var da: int = depth_axis[o]
var wa: int = width_axis[o]
var ha: int = height_axis[o]
# Greedy face merging
var width: int = width_query(faces, face, o)
var height: int = height_query(faces, face, o, width)
var grow: Vector3 = Vector3(1, 1, 1)
grow[wa] *= width
grow[ha] *= height
# Generate geometry
var yoffset = Vector3(0, 0, 0);
if snaptoground: yoffset = Vector3(0, -mins.z * scale, 0);
st.set_color(faces[face])
st.set_normal(normals[o])
for vert in face_meshes[o]:
st.add_vertex(yoffset + vox_to_godot * ((vert * grow) + face) * scale)
# Remove these faces from the pool
var v: Vector3 = Vector3()
v[da] = face[da]
for iy in range(height):
v[ha] = face[ha] + float(iy)
for ix in range(width):
v[wa] = face[wa] + float(ix)
faces.erase(v)
return faces
# Returns the number of voxels wide the run starting at face is with respect to
# the set of faces and orientation
func width_query(faces :Dictionary, face :Vector3, o :int) -> int:
var wd :int = width_axis[o]
var v :Vector3 = face
while faces.has(v) and faces[v] == faces[face]:
v[wd] += 1.0
return int(v[wd] - face[wd])
func width_query(faces: Dictionary, face: Vector3, o: int) -> int:
var wd: int = width_axis[o]
var v: Vector3 = face
while faces.has(v) and faces[v] == faces[face]:
v[wd] += 1.0
return int(v[wd] - face[wd])
# Returns the number of voxels high the run starting at face is with respect to
# the set of faces and orientation, with the given width
func height_query(faces :Dictionary, face :Vector3, o :int, width :int) -> int:
var hd :int = height_axis[o]
var c :Color = faces[face]
var v :Vector3 = face
v[hd] += 1.0
while faces.has(v) and faces[v] == c and width_query(faces, v, o) >= width:
v[hd] += 1.0
return int(v[hd] - face[hd])
func height_query(faces: Dictionary, face: Vector3, o: int, width: int) -> int:
var hd: int = height_axis[o]
var c: Color = faces[face]
var v: Vector3 = face
v[hd] += 1.0
while faces.has(v) and faces[v] == c and width_query(faces, v, o) >= width:
v[hd] += 1.0
return int(v[hd] - face[hd])

@ -1,5 +1,7 @@
# MagicaVoxel Importer with Extensions
Imports MagicaVoxel .vox files as meshes. Supports most node extensions, allowing multiple models and complex scene graphs.
Imports MagicaVoxel .vox files as meshes. Supports most node extensions, allowing multiple models and complex scene
graphs.
This is an adaptation of vox-importer by JohnCWakley (https://github.com/JohnCWakley/vox-importer)
This is an adaptation of MagicaVoxelImporter by Scayze (https://github.com/scayze/MagicaVoxel-Importer)

@ -1,41 +1,53 @@
var file: FileAccess;
var chunk_size = 0;
func _init(file: FileAccess):
self.file = file;
self.chunk_size = 0;
self.file = file;
self.chunk_size = 0;
func has_data_to_read(): return file.get_position() < file.get_length()
func set_chunk_size(size):
chunk_size = size;
chunk_size = size;
func get_8():
chunk_size -= 1;
return file.get_8();
func get_32():
chunk_size -= 4;
return file.get_32();
chunk_size -= 1;
return file.get_8();
func get_32():
chunk_size -= 4;
return file.get_32();
func get_buffer(length):
chunk_size -= length;
return file.get_buffer(length);
chunk_size -= length;
return file.get_buffer(length);
func read_remaining():
get_buffer(chunk_size);
chunk_size = 0;
get_buffer(chunk_size);
chunk_size = 0;
func get_string(length):
return get_buffer(length).get_string_from_ascii()
return get_buffer(length).get_string_from_ascii()
func get_vox_string():
var length = get_32();
return get_string(length);
var length = get_32();
return get_string(length);
func get_vox_dict():
var result = {};
var pairs = get_32();
for _p in range(pairs):
var key = get_vox_string();
var value = get_vox_string();
result[key] = value;
return result;
var result = {};
var pairs = get_32();
for _p in range(pairs):
var key = get_vox_string();
var value = get_vox_string();
result[key] = value;
return result;

@ -1,5 +1,4 @@
const Model = preload("./Model.gd");
var models = {0: Model.new()};
var current_index = -1;
var colors: Array;
@ -7,7 +6,8 @@ var nodes = {};
var materials = {};
var layers = {};
func get_model():
if (!models.has(current_index)):
models[current_index] = Model.new();
return models[current_index];
if (!models.has(current_index)):
models[current_index] = Model.new();
return models[current_index];

@ -1,6 +1,7 @@
var id: int;
var isVisible: bool;
func _init(id,isVisible):
self.id = id;
self.isVisible = isVisible;
func _init(id, isVisible):
self.id = id;
self.isVisible = isVisible;

@ -2,57 +2,60 @@ var properties = null;
var material: StandardMaterial3D = null;
var type:
get:
return properties.get("_type", "diffuse");
get:
return properties.get("_type", "diffuse");
var weight:
get:
return float(properties.get("_weight", 0));
get:
return float(properties.get("_weight", 0));
var specular:
get:
return float(properties.get("spec"));
get:
return float(properties.get("spec"));
var roughness:
get:
return float(properties.get("rough"));
get:
return float(properties.get("rough"));
var flux:
get:
return float(properties.get("flux"));
get:
return float(properties.get("flux"));
var refraction:
get:
return float(properties.get("ior"));
get:
return float(properties.get("ior"));
func _init(properties):
self.properties = properties;
self.properties = properties;
func is_glass():
return self.type == "_glass";
return self.type == "_glass";
func get_material(color: Color):
if (material != null): return material;
material = StandardMaterial3D.new();
material.vertex_color_is_srgb = true;
material.vertex_color_use_as_albedo = true;
match (self.type):
"_metal":
material.metallic = self.weight;
material.metallic_specular = self.specular;
material.roughness = self.roughness;
"_emit":
material.emission_enabled = true;
material.emission = Color(color.r, color.g, color.b, self.weight);
material.emission_energy = self.flux;
"_glass":
material.flags_transparent = true;
material.albedo_color = Color(1, 1, 1, 1 - self.weight);
material.refraction_enabled = true;
material.refraction_scale = self.refraction * 0.01;
material.roughness = self.roughness;
"_diffuse", _:
material.roughness = 1;
return material;
if (material != null): return material;
material = StandardMaterial3D.new();
material.vertex_color_is_srgb = true;
material.vertex_color_use_as_albedo = true;
match (self.type):
"_metal":
material.metallic = self.weight;
material.metallic_specular = self.specular;
material.roughness = self.roughness;
"_emit":
material.emission_enabled = true;
material.emission = Color(color.r, color.g, color.b, self.weight);
material.emission_energy = self.flux;
"_glass":
material.flags_transparent = true;
material.albedo_color = Color(1, 1, 1, 1 - self.weight);
material.refraction_enabled = true;
material.refraction_scale = self.refraction * 0.01;
material.roughness = self.roughness;
"_diffuse", _:
material.roughness = 1;
return material;

@ -5,6 +5,7 @@ var child_nodes = [];
var models = {};
var transforms = { 0: { "position": Vector3(), "rotation": Basis() } };
func _init(id,attributes):
self.id = id;
self.attributes = attributes;
func _init(id, attributes):
self.id = id;
self.attributes = attributes;

@ -1,23 +1,24 @@
@tool
extends MeshInstance3D
@export var frames: MeshLibrary = null : set = set_frames
@export var current_frame: int = 0 : set = set_current_frame
@export var frames: MeshLibrary = null: set = set_frames
@export var current_frame: int = 0: set = set_current_frame
var mesh_count = 0;
func set_frames(v):
frames = v;
current_frame = 0
if v == null:
mesh_count = 0;
self.mesh = null;
else:
mesh_count = v.get_item_list().size()
self.mesh = v.get_item_mesh(0)
frames = v;
current_frame = 0
if v == null:
mesh_count = 0;
self.mesh = null;
else:
mesh_count = v.get_item_list().size()
self.mesh = v.get_item_mesh(0)
func set_current_frame(v):
if v >= 0 and v < mesh_count:
current_frame = v
self.mesh = frames.get_item_mesh(v)
if v >= 0 and v < mesh_count:
current_frame = v
self.mesh = frames.get_item_mesh(v)

@ -1,7 +1,7 @@
[plugin]
name="MagicaVoxel Importer with Extensions"
description="Imports MagicaVoxel .vox files as meshes. Supports most node extensions, allowing multiple models and complex scene graphs."
author="Violgamba"
version="1.2"
script="plugin.gd"
name = "MagicaVoxel Importer with Extensions"
description = "Imports MagicaVoxel .vox files as meshes. Supports most node extensions, allowing multiple models and complex scene graphs."
author = "Violgamba"
version = "1.2"
script = "plugin.gd"

@ -4,20 +4,23 @@ extends EditorPlugin
var pluginToMesh
var pluginToMeshLibrary
func _enter_tree():
pluginToMesh = preload('vox-importer-mesh.gd').new()
pluginToMeshLibrary = preload('vox-importer-meshLibrary.gd').new()
add_import_plugin(pluginToMesh)
add_import_plugin(pluginToMeshLibrary)
add_custom_type("FramedMeshInstance", "MeshInstance3D",
preload("framed_mesh_instance.gd"), preload("framed_mesh_instance.png"))
pluginToMesh = preload('vox-importer-mesh.gd').new()
pluginToMeshLibrary = preload('vox-importer-meshLibrary.gd').new()
add_import_plugin(pluginToMesh)
add_import_plugin(pluginToMeshLibrary)
add_custom_type("FramedMeshInstance", "MeshInstance3D",
preload("framed_mesh_instance.gd"), preload("framed_mesh_instance.png"))
func _exit_tree():
remove_import_plugin(pluginToMesh)
remove_import_plugin(pluginToMeshLibrary)
pluginToMesh = null
pluginToMeshLibrary = null
remove_custom_type("FramedMeshInstance")
remove_import_plugin(pluginToMesh)
remove_import_plugin(pluginToMeshLibrary)
pluginToMesh = null
pluginToMeshLibrary = null
remove_custom_type("FramedMeshInstance")
func _get_priority() -> float:
return 1.0
return 1.0

@ -1,4 +1,3 @@
const VoxFile = preload("./VoxFile.gd");
const VoxData = preload("./VoxFormat/VoxData.gd");
const VoxNode = preload("./VoxFormat/VoxNode.gd");
@ -6,302 +5,310 @@ const VoxMaterial = preload("./VoxFormat/VoxMaterial.gd");
const VoxLayer = preload("./VoxFormat/VoxLayer.gd");
const CulledMeshGenerator = preload("./CulledMeshGenerator.gd");
const GreedyMeshGenerator = preload("./GreedyMeshGenerator.gd");
const debug_file = false;
const debug_models = false;
var fileKeyframeIds = [];
func import(source_path, destination_path, options, _platforms, _gen_files):
var scale = 0.1
if options.Scale:
scale = float(options.Scale)
var greedy = true
if options.has("GreedyMeshGenerator"):
greedy = bool(options.GreedyMeshGenerator)
var snaptoground = false
if options.has("SnapToGround"):
snaptoground = bool(options.SnapToGround)
var mergeKeyframes = false
if options.has("FirstKeyframeOnly"):
mergeKeyframes = not bool(options.FirstKeyframeOnly)
var file = FileAccess.open(source_path, FileAccess.READ)
if file == null:
return FileAccess.get_open_error()
var identifier = PackedByteArray([ file.get_8(), file.get_8(), file.get_8(), file.get_8() ]).get_string_from_ascii()
var version = file.get_32()
print('Importing: ', source_path, ' (scale: ', scale, ', file version: ', version, ', greedy mesh: ', greedy, ', snap to ground: ', snaptoground, ')');
var vox = VoxData.new();
if identifier == 'VOX ':
var voxFile = VoxFile.new(file);
while voxFile.has_data_to_read():
read_chunk(vox, voxFile);
file = null
fileKeyframeIds.sort()
var voxel_data = unify_voxels(vox, mergeKeyframes);
var meshes = []
for keyframeVoxels in voxel_data:
if greedy:
meshes.append(GreedyMeshGenerator.new().generate(vox, voxel_data[keyframeVoxels], scale, snaptoground))
else:
meshes.append(CulledMeshGenerator.new().generate(vox, voxel_data[keyframeVoxels], scale, snaptoground))
return meshes
var scale = 0.1
if options.Scale:
scale = float(options.Scale)
var greedy = true
if options.has("GreedyMeshGenerator"):
greedy = bool(options.GreedyMeshGenerator)
var snaptoground = false
if options.has("SnapToGround"):
snaptoground = bool(options.SnapToGround)
var mergeKeyframes = false
if options.has("FirstKeyframeOnly"):
mergeKeyframes = not bool(options.FirstKeyframeOnly)
var file = FileAccess.open(source_path, FileAccess.READ)
if file == null:
return FileAccess.get_open_error()
var identifier = PackedByteArray([ file.get_8(), file.get_8(), file.get_8(), file.get_8() ]).get_string_from_ascii()
var version = file.get_32()
print('Importing: ', source_path, ' (scale: ', scale, ', file version: ', version, ', greedy mesh: ', greedy, ', snap to ground: ', snaptoground, ')');
var vox = VoxData.new();
if identifier == 'VOX ':
var voxFile = VoxFile.new(file);
while voxFile.has_data_to_read():
read_chunk(vox, voxFile);
file = null
fileKeyframeIds.sort()
var voxel_data = unify_voxels(vox, mergeKeyframes);
var meshes = []
for keyframeVoxels in voxel_data:
if greedy:
meshes.append(GreedyMeshGenerator.new().generate(vox, voxel_data[keyframeVoxels], scale, snaptoground))
else:
meshes.append(CulledMeshGenerator.new().generate(vox, voxel_data[keyframeVoxels], scale, snaptoground))
return meshes
func string_to_vector3(input: String) -> Vector3:
var data = input.split_floats(' ');
return Vector3(data[0], data[1], data[2]);
var data = input.split_floats(' ');
return Vector3(data[0], data[1], data[2]);
func byte_to_basis(data: int):
var x_ind = ((data >> 0) & 0x03);
var y_ind = ((data >> 2) & 0x03);
var indexes = [0, 1, 2];
indexes.erase(x_ind);
indexes.erase(y_ind);
var z_ind = indexes[0];
var x_sign = 1 if ((data >> 4) & 0x01) == 0 else -1;
var y_sign = 1 if ((data >> 5) & 0x01) == 0 else -1;
var z_sign = 1 if ((data >> 6) & 0x01) == 0 else -1;
var result = Basis();
result.x[0] = x_sign if x_ind == 0 else 0;
result.x[1] = x_sign if x_ind == 1 else 0;
result.x[2] = x_sign if x_ind == 2 else 0;
result.y[0] = y_sign if y_ind == 0 else 0;
result.y[1] = y_sign if y_ind == 1 else 0;
result.y[2] = y_sign if y_ind == 2 else 0;
result.z[0] = z_sign if z_ind == 0 else 0;
result.z[1] = z_sign if z_ind == 1 else 0;
result.z[2] = z_sign if z_ind == 2 else 0;
return result;
var x_ind = ((data >> 0) & 0x03);
var y_ind = ((data >> 2) & 0x03);
var indexes = [0, 1, 2];
indexes.erase(x_ind);
indexes.erase(y_ind);
var z_ind = indexes[0];
var x_sign = 1 if ((data >> 4) & 0x01) == 0 else -1;
var y_sign = 1 if ((data >> 5) & 0x01) == 0 else -1;
var z_sign = 1 if ((data >> 6) & 0x01) == 0 else -1;
var result = Basis();
result.x[0] = x_sign if x_ind == 0 else 0;
result.x[1] = x_sign if x_ind == 1 else 0;
result.x[2] = x_sign if x_ind == 2 else 0;
result.y[0] = y_sign if y_ind == 0 else 0;
result.y[1] = y_sign if y_ind == 1 else 0;
result.y[2] = y_sign if y_ind == 2 else 0;
result.z[0] = z_sign if z_ind == 0 else 0;
result.z[1] = z_sign if z_ind == 1 else 0;
result.z[2] = z_sign if z_ind == 2 else 0;
return result;
func read_chunk(vox: VoxData, file: VoxFile):
var chunk_id = file.get_string(4);
var chunk_size = file.get_32();
var childChunks = file.get_32()
file.set_chunk_size(chunk_size);
match chunk_id:
'SIZE':
vox.current_index += 1;
var model = vox.get_model();
var x = file.get_32();
var y = file.get_32();
var z = file.get_32();
model.size = Vector3(x, y, z);
if debug_file: print('SIZE ', model.size);
'XYZI':
var model = vox.get_model();
if debug_file: print('XYZI');
for _i in range(file.get_32()):
var x = file.get_8()
var y = file.get_8()
var z = file.get_8()
var c = file.get_8()
var voxel = Vector3(x, y, z)
model.voxels[voxel] = c - 1
if debug_file && debug_models: print('\t', voxel, ' ', c-1);
'RGBA':
vox.colors = []
for _i in range(256):
var r = float(file.get_8() / 255.0)
var g = float(file.get_8() / 255.0)
var b = float(file.get_8() / 255.0)
var a = float(file.get_8() / 255.0)
vox.colors.append(Color(r, g, b, a))
'nTRN':
var node_id = file.get_32();
var attributes = file.get_vox_dict();
var node = VoxNode.new(node_id, attributes);
vox.nodes[node_id] = node;
var child = file.get_32();
node.child_nodes.append(child);
file.get_32();
node.layerId = file.get_32();
var num_of_frames = file.get_32();
if debug_file:
print('nTRN[', node_id, '] -> ', child);
if (!attributes.is_empty()): print('\t', attributes);
if num_of_frames > 0:
node.transforms = {};
for _frame in range(num_of_frames):
var keyframe = 0;
var newTransform = { "position": Vector3(), "rotation": Basis() };
var frame_attributes = file.get_vox_dict();
if (frame_attributes.has('_f')):
keyframe = int(frame_attributes['_f']);
if (frame_attributes.has('_t')):
var trans = frame_attributes['_t'];
newTransform.position = string_to_vector3(trans);
if debug_file: print('\tT: ', newTransform.position);
if (frame_attributes.has('_r')):
var rot = frame_attributes['_r'];
newTransform.rotation = byte_to_basis(int(rot)).inverse();
if debug_file: print('\tR: ', newTransform.rotation);
node.transforms[keyframe] = newTransform;
if not fileKeyframeIds.has(keyframe):
fileKeyframeIds.append(keyframe);
'nGRP':
var node_id = file.get_32();
var attributes = file.get_vox_dict();
var node = VoxNode.new(node_id, attributes);
vox.nodes[node_id] = node;
var num_children = file.get_32();
for _c in num_children:
node.child_nodes.append(file.get_32());
if debug_file:
print('nGRP[', node_id, '] -> ', node.child_nodes);
if (!attributes.is_empty()): print('\t', attributes);
'nSHP':
var node_id = file.get_32();
var attributes = file.get_vox_dict();
var node = VoxNode.new(node_id, attributes);
vox.nodes[node_id] = node;
var num_models = file.get_32();
for _i in range(num_models):
var keyframe = 0;
var modelId = file.get_32();
var model_attributes = file.get_vox_dict();
if (model_attributes.has('_f')):
keyframe = int(model_attributes['_f']);
node.models[keyframe] = modelId;
if not fileKeyframeIds.has(keyframe):
fileKeyframeIds.append(keyframe);
if debug_file:
print('nSHP[', node_id,'] -> ', node.models);
if (!attributes.is_empty()): print('\t', attributes);
'MATL':
var material_id = file.get_32() - 1;
var properties = file.get_vox_dict();
vox.materials[material_id] = VoxMaterial.new(properties);
if debug_file:
print("MATL ", material_id);
print("\t", properties);
'LAYR':
var layer_id = file.get_32();
var attributes = file.get_vox_dict();
var isVisible = true;
if '_hidden' in attributes and attributes['_hidden'] == '1':
isVisible = false;
var layer = VoxLayer.new(layer_id, isVisible);
vox.layers[layer_id] = layer;
_:
if debug_file: print(chunk_id);
file.read_remaining();
var chunk_id = file.get_string(4);
var chunk_size = file.get_32();
var childChunks = file.get_32()
file.set_chunk_size(chunk_size);
match chunk_id:
'SIZE':
vox.current_index += 1;
var model = vox.get_model();
var x = file.get_32();
var y = file.get_32();
var z = file.get_32();
model.size = Vector3(x, y, z);
if debug_file: print('SIZE ', model.size);
'XYZI':
var model = vox.get_model();
if debug_file: print('XYZI');
for _i in range(file.get_32()):
var x = file.get_8()
var y = file.get_8()
var z = file.get_8()
var c = file.get_8()
var voxel = Vector3(x, y, z)
model.voxels[voxel] = c - 1
if debug_file && debug_models: print('\t', voxel, ' ', c-1);
'RGBA':
vox.colors = []
for _i in range(256):
var r = float(file.get_8() / 255.0)
var g = float(file.get_8() / 255.0)
var b = float(file.get_8() / 255.0)
var a = float(file.get_8() / 255.0)
vox.colors.append(Color(r, g, b, a))
'nTRN':
var node_id = file.get_32();
var attributes = file.get_vox_dict();
var node = VoxNode.new(node_id, attributes);
vox.nodes[node_id] = node;
var child = file.get_32();
node.child_nodes.append(child);
file.get_32();
node.layerId = file.get_32();
var num_of_frames = file.get_32();
if debug_file:
print('nTRN[', node_id, '] -> ', child);
if (!attributes.is_empty()): print('\t', attributes);
if num_of_frames > 0:
node.transforms = {};
for _frame in range(num_of_frames):
var keyframe = 0;
var newTransform = { "position": Vector3(), "rotation": Basis() };
var frame_attributes = file.get_vox_dict();
if (frame_attributes.has('_f')):
keyframe = int(frame_attributes['_f']);
if (frame_attributes.has('_t')):
var trans = frame_attributes['_t'];
newTransform.position = string_to_vector3(trans);
if debug_file: print('\tT: ', newTransform.position);
if (frame_attributes.has('_r')):
var rot = frame_attributes['_r'];
newTransform.rotation = byte_to_basis(int(rot)).inverse();
if debug_file: print('\tR: ', newTransform.rotation);
node.transforms[keyframe] = newTransform;
if not fileKeyframeIds.has(keyframe):
fileKeyframeIds.append(keyframe);
'nGRP':
var node_id = file.get_32();
var attributes = file.get_vox_dict();
var node = VoxNode.new(node_id, attributes);
vox.nodes[node_id] = node;
var num_children = file.get_32();
for _c in num_children:
node.child_nodes.append(file.get_32());
if debug_file:
print('nGRP[', node_id, '] -> ', node.child_nodes);
if (!attributes.is_empty()): print('\t', attributes);
'nSHP':
var node_id = file.get_32();
var attributes = file.get_vox_dict();
var node = VoxNode.new(node_id, attributes);
vox.nodes[node_id] = node;
var num_models = file.get_32();
for _i in range(num_models):
var keyframe = 0;
var modelId = file.get_32();
var model_attributes = file.get_vox_dict();
if (model_attributes.has('_f')):
keyframe = int(model_attributes['_f']);
node.models[keyframe] = modelId;
if not fileKeyframeIds.has(keyframe):
fileKeyframeIds.append(keyframe);
if debug_file:
print('nSHP[', node_id, '] -> ', node.models);
if (!attributes.is_empty()): print('\t', attributes);
'MATL':
var material_id = file.get_32() - 1;
var properties = file.get_vox_dict();
vox.materials[material_id] = VoxMaterial.new(properties);
if debug_file:
print("MATL ", material_id);
print("\t", properties);
'LAYR':
var layer_id = file.get_32();
var attributes = file.get_vox_dict();
var isVisible = true;
if '_hidden' in attributes and attributes['_hidden'] == '1':
isVisible = false;
var layer = VoxLayer.new(layer_id, isVisible);
vox.layers[layer_id] = layer;
_:
if debug_file: print(chunk_id);
file.read_remaining();
func unify_voxels(vox: VoxData, mergeKeyframes: bool):
var node = vox.nodes[0];
var layeredVoxelData = get_layeredVoxels(node, vox, -1, mergeKeyframes)
return layeredVoxelData.getDataMergedFromLayers();
var node = vox.nodes[0];
var layeredVoxelData = get_layeredVoxels(node, vox, -1, mergeKeyframes)
return layeredVoxelData.getDataMergedFromLayers();
class LayeredVoxelData:
var data_keyframed_layered = {};
func combine(keyframeId, layerId, model):
# Make sure there's space
if not keyframeId in data_keyframed_layered:
data_keyframed_layered[keyframeId] = {}
if not layerId in data_keyframed_layered[keyframeId]:
data_keyframed_layered[keyframeId][layerId] = {}
# Add the model voxels to the data
var offset = (model.size / 2.0).floor();
for voxel in model.voxels:
data_keyframed_layered[keyframeId][layerId][voxel - offset] = model.voxels[voxel];
func combine_data(other):
for keyframeId in other.data_keyframed_layered:
if not keyframeId in data_keyframed_layered:
data_keyframed_layered[keyframeId] = {}
for layerId in other.data_keyframed_layered[keyframeId]:
if not layerId in data_keyframed_layered[keyframeId]:
data_keyframed_layered[keyframeId][layerId] = {}
for voxel in other.data_keyframed_layered[keyframeId][layerId]:
data_keyframed_layered[keyframeId][layerId][voxel] = (
other.data_keyframed_layered[keyframeId][layerId][voxel]);
func transform(transforms):
var new_data = {};
for keyframeId in data_keyframed_layered:
new_data[keyframeId] = {}
var transform = get_input_for_keyframe(keyframeId, transforms);
for layerId in data_keyframed_layered[keyframeId]:
new_data[keyframeId][layerId] = {}
for voxel in data_keyframed_layered[keyframeId][layerId]:
var half_step = Vector3(0.5, 0.5, 0.5);
var new_voxel = (
(transform.rotation * voxel+half_step-half_step).floor() +
transform.position);
new_data[keyframeId][layerId][new_voxel] = (
data_keyframed_layered[keyframeId][layerId][voxel]);
data_keyframed_layered = new_data;
func getDataMergedFromLayers():
# The result of this function
var result = {};
for keyframeId in data_keyframed_layered:
result[keyframeId] = {}
# Merge all layer data in layerId order (highest layer overrides all)
var layerIds = data_keyframed_layered[keyframeId].keys();
layerIds.sort();
for layerId in layerIds:
for voxel in data_keyframed_layered[keyframeId][layerId]:
result[keyframeId][voxel] = data_keyframed_layered[keyframeId][layerId][voxel];
# Return the merged data
return result;
static func get_input_for_keyframe(focusKeyframeId, inputCollection):
var inputKeyframeIds = inputCollection.keys();
inputKeyframeIds.sort();
inputKeyframeIds.reverse();
var result = inputKeyframeIds.back();
for inputKeyframeId in inputKeyframeIds:
if inputKeyframeId <= focusKeyframeId:
result = inputKeyframeId;
break;
return inputCollection[result];
var data_keyframed_layered = {};
func combine(keyframeId, layerId, model):
# Make sure there's space
if not keyframeId in data_keyframed_layered:
data_keyframed_layered[keyframeId] = {}
if not layerId in data_keyframed_layered[keyframeId]:
data_keyframed_layered[keyframeId][layerId] = {}
# Add the model voxels to the data
var offset = (model.size / 2.0).floor();
for voxel in model.voxels:
data_keyframed_layered[keyframeId][layerId][voxel - offset] = model.voxels[voxel];
func combine_data(other):
for keyframeId in other.data_keyframed_layered:
if not keyframeId in data_keyframed_layered:
data_keyframed_layered[keyframeId] = {}
for layerId in other.data_keyframed_layered[keyframeId]:
if not layerId in data_keyframed_layered[keyframeId]:
data_keyframed_layered[keyframeId][layerId] = {}
for voxel in other.data_keyframed_layered[keyframeId][layerId]:
data_keyframed_layered[keyframeId][layerId][voxel] = (
other.data_keyframed_layered[keyframeId][layerId][voxel]);
func transform(transforms):
var new_data = {};
for keyframeId in data_keyframed_layered:
new_data[keyframeId] = {}
var transform = get_input_for_keyframe(keyframeId, transforms);
for layerId in data_keyframed_layered[keyframeId]:
new_data[keyframeId][layerId] = {}
for voxel in data_keyframed_layered[keyframeId][layerId]:
var half_step = Vector3(0.5, 0.5, 0.5);
var new_voxel = (
(transform.rotation * voxel+half_step-half_step).floor() +
transform.position);
new_data[keyframeId][layerId][new_voxel] = (
data_keyframed_layered[keyframeId][layerId][voxel]);
data_keyframed_layered = new_data;
func getDataMergedFromLayers():
# The result of this function
var result = {};
for keyframeId in data_keyframed_layered:
result[keyframeId] = {}
# Merge all layer data in layerId order (highest layer overrides all)
var layerIds = data_keyframed_layered[keyframeId].keys();
layerIds.sort();
for layerId in layerIds:
for voxel in data_keyframed_layered[keyframeId][layerId]:
result[keyframeId][voxel] = data_keyframed_layered[keyframeId][layerId][voxel];
# Return the merged data
return result;
static func get_input_for_keyframe(focusKeyframeId, inputCollection):
var inputKeyframeIds = inputCollection.keys();
inputKeyframeIds.sort();
inputKeyframeIds.reverse();
var result = inputKeyframeIds.back();
for inputKeyframeId in inputKeyframeIds:
if inputKeyframeId <= focusKeyframeId:
result = inputKeyframeId;
break;
return inputCollection[result];
func get_layeredVoxels(node: VoxNode, vox: VoxData, layerId: int, mergeKeyframes: bool):
var result = LayeredVoxelData.new();
# Handle layers (keeping separated and ignoring hidden)
if node.layerId in vox.layers:
if vox.layers[node.layerId].isVisible:
layerId = node.layerId;
else:
return result;
# Add all models in this node
if mergeKeyframes:
for model_index in node.models:
var modelId = node.models[model_index];
var model = vox.models[modelId];
result.combine(0, layerId, model);
elif node.models.size() > 0:
for fileKeyframeId in fileKeyframeIds:
var frameModelId = LayeredVoxelData.get_input_for_keyframe(fileKeyframeId, node.models);
var model = vox.models[frameModelId];
result.combine(fileKeyframeId, layerId, model);
# Process child nodes
for child_index in node.child_nodes:
var child = vox.nodes[child_index];
var child_data = get_layeredVoxels(child, vox, layerId, mergeKeyframes);
result.combine_data(child_data);
# Run transforms
result.transform(node.transforms);
return result;
var result = LayeredVoxelData.new();
# Handle layers (keeping separated and ignoring hidden)
if node.layerId in vox.layers:
if vox.layers[node.layerId].isVisible:
layerId = node.layerId;
else:
return result;
# Add all models in this node
if mergeKeyframes:
for model_index in node.models:
var modelId = node.models[model_index];
var model = vox.models[modelId];
result.combine(0, layerId, model);
elif node.models.size() > 0:
for fileKeyframeId in fileKeyframeIds:
var frameModelId = LayeredVoxelData.get_input_for_keyframe(fileKeyframeId, node.models);
var model = vox.models[frameModelId];
result.combine(fileKeyframeId, layerId, model);
# Process child nodes
for child_index in node.child_nodes:
var child = vox.nodes[child_index];
var child_data = get_layeredVoxels(child, vox, layerId, mergeKeyframes);
result.combine_data(child_data);
# Run transforms
result.transform(node.transforms);
return result;

@ -3,62 +3,75 @@ extends EditorImportPlugin
const VoxImporterCommon = preload("./vox-importer-common.gd");
func _init():
print('MagicaVoxel Mesh Importer: Ready')
print('MagicaVoxel Mesh Importer: Ready')
func _get_importer_name():
return 'MagicaVoxel.With.Extensions.To.Mesh'
return 'MagicaVoxel.With.Extensions.To.Mesh'
func _get_visible_name():
return 'MagicaVoxel Mesh'
return 'MagicaVoxel Mesh'
func _get_recognized_extensions():
return [ 'vox' ]
return [ 'vox' ]
func _get_resource_type():
return 'Mesh'
return 'Mesh'
func _get_save_extension():
return 'mesh'
return 'mesh'
func _get_preset_count():
return 0
return 0
func _get_preset_name(_preset):
return 'Default'
return 'Default'
func _get_import_order():
return 0
return 0
func _get_priority() -> float:
return 1.0
return 1.0
func _get_import_options(path, preset):
return [
{
'name': 'Scale',
'default_value': 0.1
},
{
'name': 'GreedyMeshGenerator',
'default_value': true
},
{
'name': 'SnapToGround',
'default_value': false
},
{
'name': 'FirstKeyframeOnly',
'default_value': true
}
]
return [
{
'name': 'Scale',
'default_value': 0.1
},
{
'name': 'GreedyMeshGenerator',
'default_value': true
},
{
'name': 'SnapToGround',
'default_value': false
},
{
'name': 'FirstKeyframeOnly',
'default_value': true
}
]
func _get_option_visibility(path, option, options):
return true
return true
func _import(source_path, destination_path, options, _platforms, _gen_files):
var meshes = VoxImporterCommon.new().import(source_path, destination_path, options, _platforms, _gen_files);
var full_path = "%s.%s" % [ destination_path, _get_save_extension() ]
var res = ResourceSaver.save(meshes[0], full_path)
Util.refresh_mesh_library([full_path])
return res
var meshes = VoxImporterCommon.new().import(source_path, destination_path, options, _platforms, _gen_files);
var full_path = "%s.%s" % [ destination_path, _get_save_extension() ]
var res = ResourceSaver.save(meshes[0], full_path)
Util.refresh_mesh_library([full_path])
return res

@ -3,61 +3,74 @@ extends EditorImportPlugin
const VoxImporterCommon = preload("./vox-importer-common.gd");
func _init():
print('MagicaVoxel MeshLibrary Importer: Ready')
print('MagicaVoxel MeshLibrary Importer: Ready')
func _get_importer_name():
return 'MagicaVoxel.With.Extensions.To.MeshLibrary'
return 'MagicaVoxel.With.Extensions.To.MeshLibrary'
func _get_visible_name():
return 'MagicaVoxel MeshLibrary'
return 'MagicaVoxel MeshLibrary'
func _get_recognized_extensions():
return [ 'vox' ]
return [ 'vox' ]
func _get_resource_type():
return 'MeshLibrary'
return 'MeshLibrary'
func _get_save_extension():
return 'meshlib'
return 'meshlib'
func _get_preset_count():
return 0
return 0
func _get_preset_name(_preset):
return 'Default'
return 'Default'
func _get_import_order():
return 0
return 0
func _get_priority() -> float:
return 1.0
return 1.0
func _get_import_options(path, preset_index):
return [
{
'name': 'Scale',
'default_value': 0.1
},
{
'name': 'GreedyMeshGenerator',
'default_value': true
},
{
'name': 'SnapToGround',
'default_value': false
}
]
return [
{
'name': 'Scale',
'default_value': 0.1
},
{
'name': 'GreedyMeshGenerator',
'default_value': true
},
{
'name': 'SnapToGround',
'default_value': false
}
]
func _get_option_visibility(path, option, options):
return true
return true
func _import(source_path, destination_path, options, _platforms, _gen_files):
var meshes = VoxImporterCommon.new().import(source_path, destination_path, options, _platforms, _gen_files);
var meshLib = MeshLibrary.new()
for mesh in meshes:
var itemId = meshLib.get_last_unused_item_id()
meshLib.create_item(itemId)
meshLib.set_item_mesh(itemId, mesh)
var full_path = "%s.%s" % [ destination_path, _get_save_extension() ]
return ResourceSaver.save(meshLib, full_path)
var meshes = VoxImporterCommon.new().import(source_path, destination_path, options, _platforms, _gen_files);
var meshLib = MeshLibrary.new()
for mesh in meshes:
var itemId = meshLib.get_last_unused_item_id()
meshLib.create_item(itemId)
meshLib.set_item_mesh(itemId, mesh)
var full_path = "%s.%s" % [ destination_path, _get_save_extension() ]
return ResourceSaver.save(meshLib, full_path)

@ -1,33 +1,35 @@
## The blackboard is an object that can be used to store and access data between
## multiple nodes of the behavior tree.
@icon("icons/blackboard.svg")
class_name Blackboard extends Node
class_name Blackboard
extends Node
var blackboard: Dictionary = {}
func keys() -> Array[String]:
var keys: Array[String]
keys.assign(blackboard.keys().duplicate())
return keys
var keys: Array[String]
keys.assign(blackboard.keys().duplicate())
return keys
func set_value(key: Variant, value: Variant, blackboard_name: String = 'default') -> void:
if not blackboard.has(blackboard_name):
blackboard[blackboard_name] = {}
if not blackboard.has(blackboard_name):
blackboard[blackboard_name] = {}
blackboard[blackboard_name][key] = value
blackboard[blackboard_name][key] = value
func get_value(key: Variant, default_value: Variant = null, blackboard_name: String = 'default') -> Variant:
if has_value(key, blackboard_name):
return blackboard[blackboard_name].get(key, default_value)
return default_value
if has_value(key, blackboard_name):
return blackboard[blackboard_name].get(key, default_value)
return default_value
func has_value(key: Variant, blackboard_name: String = 'default') -> bool:
return blackboard.has(blackboard_name) and blackboard[blackboard_name].has(key) and blackboard[blackboard_name][key] != null
return blackboard.has(blackboard_name) and blackboard[blackboard_name].has(key) and blackboard[blackboard_name][key] != null
func erase_value(key: Variant, blackboard_name: String = 'default') -> void:
if blackboard.has(blackboard_name):
blackboard[blackboard_name][key] = null
if blackboard.has(blackboard_name):
blackboard[blackboard_name][key] = null

@ -2,90 +2,90 @@
extends EditorDebuggerPlugin
const DebuggerTab := preload("debugger_tab.gd")
var debugger_tab := DebuggerTab.new()
var debugger_tab := DebuggerTab.new()
var floating_window: Window
var session: EditorDebuggerSession
func _has_capture(prefix: String) -> bool:
return prefix == "beehave"
return prefix == "beehave"
func _capture(message: String, data: Array, session_id: int) -> bool:
# in case the behavior tree has invalid setup this might be null
if debugger_tab == null:
return false
if message == "beehave:register_tree":
debugger_tab.register_tree(data[0])
return true
if message == "beehave:unregister_tree":
debugger_tab.unregister_tree(data[0])
return true
if message == "beehave:process_tick":
debugger_tab.graph.process_tick(data[0], data[1])
return true
if message == "beehave:process_begin":
debugger_tab.graph.process_begin(data[0])
return true
if message == "beehave:process_end":
debugger_tab.graph.process_end(data[0])
return true
return false
# in case the behavior tree has invalid setup this might be null
if debugger_tab == null:
return false
if message == "beehave:register_tree":
debugger_tab.register_tree(data[0])
return true
if message == "beehave:unregister_tree":
debugger_tab.unregister_tree(data[0])
return true
if message == "beehave:process_tick":
debugger_tab.graph.process_tick(data[0], data[1])
return true
if message == "beehave:process_begin":
debugger_tab.graph.process_begin(data[0])
return true
if message == "beehave:process_end":
debugger_tab.graph.process_end(data[0])
return true
return false
func _setup_session(session_id: int) -> void:
session = get_session(session_id)
session.started.connect(debugger_tab.start)
session.stopped.connect(debugger_tab.stop)
session = get_session(session_id)
session.started.connect(debugger_tab.start)
session.stopped.connect(debugger_tab.stop)
debugger_tab.name = "🐝 Beehave"
debugger_tab.make_floating.connect(_on_make_floating)
debugger_tab.session = session
session.add_session_tab(debugger_tab)
debugger_tab.name = "🐝 Beehave"
debugger_tab.make_floating.connect(_on_make_floating)
debugger_tab.session = session
session.add_session_tab(debugger_tab)
func _on_make_floating() -> void:
var plugin := BeehaveUtils.get_plugin()
if not plugin:
return
if floating_window:
_on_window_close_requested()
return
var border_size := Vector2(4, 4) * BeehaveUtils.get_editor_scale()
var editor_interface: EditorInterface = plugin.get_editor_interface()
var editor_main_screen = editor_interface.get_editor_main_screen()
debugger_tab.get_parent().remove_child(debugger_tab)
floating_window = Window.new()
var panel := Panel.new()
panel.add_theme_stylebox_override("panel", editor_interface.get_base_control().get_theme_stylebox("PanelForeground", "EditorStyles"))
panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
floating_window.add_child(panel)
var margin := MarginContainer.new()
margin.add_child(debugger_tab)
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_right", border_size.x)
margin.add_theme_constant_override("margin_left", border_size.x)
margin.add_theme_constant_override("margin_top", border_size.y)
margin.add_theme_constant_override("margin_bottom", border_size.y)
panel.add_child(margin)
floating_window.title = "🐝 Beehave"
floating_window.wrap_controls = true
floating_window.min_size = Vector2i(600, 350)
floating_window.size = debugger_tab.size
floating_window.position = editor_main_screen.global_position
floating_window.transient = true
floating_window.close_requested.connect(_on_window_close_requested)
editor_interface.get_base_control().add_child(floating_window)
var plugin := BeehaveUtils.get_plugin()
if not plugin:
return
if floating_window:
_on_window_close_requested()
return
var border_size := Vector2(4, 4) * BeehaveUtils.get_editor_scale()
var editor_interface: EditorInterface = plugin.get_editor_interface()
var editor_main_screen = editor_interface.get_editor_main_screen()
debugger_tab.get_parent().remove_child(debugger_tab)
floating_window = Window.new()
var panel := Panel.new()
panel.add_theme_stylebox_override("panel", editor_interface.get_base_control().get_theme_stylebox("PanelForeground", "EditorStyles"))
panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
floating_window.add_child(panel)
var margin := MarginContainer.new()
margin.add_child(debugger_tab)
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_right", border_size.x)
margin.add_theme_constant_override("margin_left", border_size.x)
margin.add_theme_constant_override("margin_top", border_size.y)
margin.add_theme_constant_override("margin_bottom", border_size.y)
panel.add_child(margin)
floating_window.title = "🐝 Beehave"
floating_window.wrap_controls = true
floating_window.min_size = Vector2i(600, 350)
floating_window.size = debugger_tab.size
floating_window.position = editor_main_screen.global_position
floating_window.transient = true
floating_window.close_requested.connect(_on_window_close_requested)
editor_interface.get_base_control().add_child(floating_window)
func _on_window_close_requested() -> void:
debugger_tab.get_parent().remove_child(debugger_tab)
session.add_session_tab(debugger_tab)
floating_window.queue_free()
floating_window = null
debugger_tab.get_parent().remove_child(debugger_tab)
session.add_session_tab(debugger_tab)
floating_window.queue_free()
floating_window = null

@ -1,31 +1,30 @@
class_name BeehaveDebuggerMessages
static func can_send_message() -> bool:
return not Engine.is_editor_hint() and OS.has_feature("editor")
return not Engine.is_editor_hint() and OS.has_feature("editor")
static func register_tree(beehave_tree: Dictionary) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:register_tree", [beehave_tree])
if can_send_message():
EngineDebugger.send_message("beehave:register_tree", [beehave_tree])
static func unregister_tree(instance_id: int) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:unregister_tree", [instance_id])
if can_send_message():
EngineDebugger.send_message("beehave:unregister_tree", [instance_id])
static func process_tick(instance_id: int, status: int) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:process_tick", [instance_id, status])
if can_send_message():
EngineDebugger.send_message("beehave:process_tick", [instance_id, status])
static func process_begin(instance_id: int) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:process_begin", [instance_id])
if can_send_message():
EngineDebugger.send_message("beehave:process_begin", [instance_id])
static func process_end(instance_id: int) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:process_end", [instance_id])
if can_send_message():
EngineDebugger.send_message("beehave:process_end", [instance_id])

@ -2,106 +2,103 @@
extends PanelContainer
signal make_floating()
const BeehaveGraphEdit := preload("graph_edit.gd")
const TREE_ICON := preload("../icons/tree.svg")
const TREE_ICON := preload("../icons/tree.svg")
var container: HSplitContainer
var item_list: ItemList
var graph: BeehaveGraphEdit
var message: Label
var active_trees: Dictionary
var active_tree_id: int = -1
var session: EditorDebuggerSession
func _ready() -> void:
container = HSplitContainer.new()
add_child(container)
item_list = ItemList.new()
item_list.custom_minimum_size = Vector2(200, 0)
item_list.item_selected.connect(_on_item_selected)
container.add_child(item_list)
graph = BeehaveGraphEdit.new()
container.add_child(graph)
message = Label.new()
message.text = "Run Project for debugging"
message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
message.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
message.set_anchors_preset(Control.PRESET_CENTER)
add_child(message)
var button := Button.new()
button.flat = true
button.icon = get_theme_icon(&"ExternalLink", &"EditorIcons")
button.pressed.connect(func(): make_floating.emit())
button.tooltip_text = "Make floating"
button.focus_mode = Control.FOCUS_NONE
graph.get_menu_hbox().add_child(button)
var toggle_button := Button.new()
toggle_button.flat = true
toggle_button.icon = get_theme_icon(&"Back", &"EditorIcons")
toggle_button.pressed.connect(_on_toggle_button_pressed.bind(toggle_button))
toggle_button.tooltip_text = "Toggle Panel"
toggle_button.focus_mode = Control.FOCUS_NONE
graph.get_menu_hbox().add_child(toggle_button)
graph.get_menu_hbox().move_child(toggle_button, 0)
stop()
visibility_changed.connect(_on_visibility_changed)
container = HSplitContainer.new()
add_child(container)
item_list = ItemList.new()
item_list.custom_minimum_size = Vector2(200, 0)
item_list.item_selected.connect(_on_item_selected)
container.add_child(item_list)
graph = BeehaveGraphEdit.new()
container.add_child(graph)
message = Label.new()
message.text = "Run Project for debugging"
message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
message.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
message.set_anchors_preset(Control.PRESET_CENTER)
add_child(message)
var button := Button.new()
button.flat = true
button.icon = get_theme_icon(&"ExternalLink", &"EditorIcons")
button.pressed.connect(func(): make_floating.emit())
button.tooltip_text = "Make floating"
button.focus_mode = Control.FOCUS_NONE
graph.get_menu_hbox().add_child(button)
var toggle_button := Button.new()
toggle_button.flat = true
toggle_button.icon = get_theme_icon(&"Back", &"EditorIcons")
toggle_button.pressed.connect(_on_toggle_button_pressed.bind(toggle_button))
toggle_button.tooltip_text = "Toggle Panel"
toggle_button.focus_mode = Control.FOCUS_NONE
graph.get_menu_hbox().add_child(toggle_button)
graph.get_menu_hbox().move_child(toggle_button, 0)
stop()
visibility_changed.connect(_on_visibility_changed)
func start() -> void:
container.visible = true
message.visible = false
container.visible = true
message.visible = false
func stop() -> void:
container.visible = false
message.visible = true
container.visible = false
message.visible = true
active_trees.clear()
item_list.clear()
graph.beehave_tree = {}
active_trees.clear()
item_list.clear()
graph.beehave_tree = {}
func register_tree(data: Dictionary) -> void:
var idx := item_list.add_item(data.name, TREE_ICON)
item_list.set_item_tooltip(idx, data.path)
item_list.set_item_metadata(idx, data.id)
active_trees[data.id] = data
var idx := item_list.add_item(data.name, TREE_ICON)
item_list.set_item_tooltip(idx, data.path)
item_list.set_item_metadata(idx, data.id)
active_trees[data.id] = data
func unregister_tree(instance_id: int) -> void:
var id := str(instance_id)
for i in item_list.item_count:
if item_list.get_item_metadata(i) == id:
item_list.remove_item(i)
break
var id := str(instance_id)
for i in item_list.item_count:
if item_list.get_item_metadata(i) == id:
item_list.remove_item(i)
break
active_trees.erase(id)
active_trees.erase(id)
if graph.beehave_tree.get("id", "") == id:
graph.beehave_tree = {}
if graph.beehave_tree.get("id", "") == id:
graph.beehave_tree = {}
func _on_toggle_button_pressed(toggle_button: Button) -> void:
item_list.visible = !item_list.visible
toggle_button.icon = get_theme_icon(&"Back" if item_list.visible else &"Forward", &"EditorIcons")
item_list.visible = !item_list.visible
toggle_button.icon = get_theme_icon(&"Back" if item_list.visible else &"Forward", &"EditorIcons")
func _on_item_selected(idx: int) -> void:
var id: StringName = item_list.get_item_metadata(idx)
graph.beehave_tree = active_trees.get(id, {})
var id: StringName = item_list.get_item_metadata(idx)
graph.beehave_tree = active_trees.get(id, {})
active_tree_id = id.to_int()
session.send_message("beehave:activate_tree", [active_tree_id])
active_tree_id = id.to_int()
session.send_message("beehave:activate_tree", [active_tree_id])
func _on_visibility_changed() -> void:
session.send_message("beehave:visibility_changed", [visible and is_visible_in_tree()])
session.send_message("beehave:visibility_changed", [visible and is_visible_in_tree()])

@ -2,10 +2,9 @@
extends RefCounted
const SUCCESS_COLOR := Color("#009944c8")
const NORMAL_COLOR := Color("#15181e")
const NORMAL_COLOR := Color("#15181e")
const FAILURE_COLOR := Color("#cf000f80")
const RUNNING_COLOR := Color("#ffcc00c8")
var empty: StyleBoxEmpty
var normal: StyleBoxFlat
var success: StyleBoxFlat
@ -14,20 +13,20 @@ var running: StyleBoxFlat
func _init() -> void:
var plugin := BeehaveUtils.get_plugin()
if not plugin:
return
var plugin := BeehaveUtils.get_plugin()
if not plugin:
return
var editor_scale := BeehaveUtils.get_editor_scale()
var editor_scale := BeehaveUtils.get_editor_scale()
empty = StyleBoxEmpty.new()
empty = StyleBoxEmpty.new()
normal = plugin.get_editor_interface().get_base_control().get_theme_stylebox(&"frame", &"GraphNode").duplicate()
normal = plugin.get_editor_interface().get_base_control().get_theme_stylebox(&"frame", &"GraphNode").duplicate()
success = plugin.get_editor_interface().get_base_control().get_theme_stylebox(&"selected_frame", &"GraphNode").duplicate()
failure = success.duplicate()
running = success.duplicate()
success = plugin.get_editor_interface().get_base_control().get_theme_stylebox(&"selected_frame", &"GraphNode").duplicate()
failure = success.duplicate()
running = success.duplicate()
success.border_color = SUCCESS_COLOR
failure.border_color = FAILURE_COLOR
running.border_color = RUNNING_COLOR
success.border_color = SUCCESS_COLOR
failure.border_color = FAILURE_COLOR
running.border_color = RUNNING_COLOR

@ -5,34 +5,34 @@ var _active_tree: BeehaveTree
func _enter_tree() -> void:
EngineDebugger.register_message_capture("beehave", _on_debug_message)
EngineDebugger.register_message_capture("beehave", _on_debug_message)
func _on_debug_message(message: String, data: Array) -> bool:
if message == "activate_tree":
_set_active_tree(data[0])
return true
if message == "visibility_changed":
if _active_tree:
_active_tree._can_send_message = data[0]
return true
return false
if message == "activate_tree":
_set_active_tree(data[0])
return true
if message == "visibility_changed":
if _active_tree:
_active_tree._can_send_message = data[0]
return true
return false
func _set_active_tree(tree_id: int) -> void:
var tree: BeehaveTree = _registered_trees.get(tree_id, null)
if not tree:
return
var tree: BeehaveTree = _registered_trees.get(tree_id, null)
if not tree:
return
if _active_tree:
_active_tree._can_send_message = false
_active_tree = tree
_active_tree._can_send_message = true
if _active_tree:
_active_tree._can_send_message = false
_active_tree = tree
_active_tree._can_send_message = true
func register_tree(tree: BeehaveTree) -> void:
_registered_trees[tree.get_instance_id()] = tree
_registered_trees[tree.get_instance_id()] = tree
func unregister_tree(tree: BeehaveTree) -> void:
_registered_trees.erase(tree.get_instance_id())
_registered_trees.erase(tree.get_instance_id())

@ -2,258 +2,254 @@
extends GraphEdit
const BeehaveGraphNode := preload("graph_node.gd")
const HORIZONTAL_LAYOUT_ICON := preload("icons/horizontal_layout.svg")
const VERTICAL_LAYOUT_ICON := preload("icons/vertical_layout.svg")
const PROGRESS_SHIFT: int = 50
const VERTICAL_LAYOUT_ICON := preload("icons/vertical_layout.svg")
const PROGRESS_SHIFT: int = 50
const INACTIVE_COLOR: Color = Color("#898989aa")
const ACTIVE_COLOR: Color = Color("#ffcc00c8")
const SUCCESS_COLOR: Color = Color("#009944c8")
const ACTIVE_COLOR: Color = Color("#ffcc00c8")
const SUCCESS_COLOR: Color = Color("#009944c8")
var updating_graph: bool = false
var arraging_nodes: bool = false
var updating_graph: bool = false
var arraging_nodes: bool = false
var beehave_tree: Dictionary:
set(value):
if beehave_tree == value:
return
beehave_tree = value
active_nodes.clear()
_update_graph()
set(value):
if beehave_tree == value:
return
beehave_tree = value
active_nodes.clear()
_update_graph()
var horizontal_layout: bool = false:
set(value):
if updating_graph or arraging_nodes:
return
if horizontal_layout == value:
return
horizontal_layout = value
_update_layout_button()
_update_graph()
set(value):
if updating_graph or arraging_nodes:
return
if horizontal_layout == value:
return
horizontal_layout = value
_update_layout_button()
_update_graph()
var active_nodes: Array[String]
var progress: int = 0
var progress: int = 0
var layout_button: Button
func _ready() -> void:
custom_minimum_size = Vector2(100, 300)
#arrange_nodes_button_hidden = true
minimap_enabled = false
custom_minimum_size = Vector2(100, 300)
#arrange_nodes_button_hidden = true
minimap_enabled = false
layout_button = Button.new()
layout_button.flat = true
layout_button.focus_mode = Control.FOCUS_NONE
layout_button.pressed.connect(func(): horizontal_layout = not horizontal_layout)
get_menu_hbox().add_child(layout_button)
_update_layout_button()
layout_button = Button.new()
layout_button.flat = true
layout_button.focus_mode = Control.FOCUS_NONE
layout_button.pressed.connect(func(): horizontal_layout = not horizontal_layout)
get_menu_hbox().add_child(layout_button)
_update_layout_button()
func _update_graph() -> void:
if updating_graph:
return
if updating_graph:
return
updating_graph = true
updating_graph = true
clear_connections()
clear_connections()
for child in get_children():
remove_child(child)
child.queue_free()
for child in get_children():
remove_child(child)
child.queue_free()
if not beehave_tree.is_empty():
_add_nodes(beehave_tree)
_connect_nodes(beehave_tree)
_arrange_nodes.call_deferred(beehave_tree)
if not beehave_tree.is_empty():
_add_nodes(beehave_tree)
_connect_nodes(beehave_tree)
_arrange_nodes.call_deferred(beehave_tree)
updating_graph = false
updating_graph = false
func _add_nodes(node: Dictionary) -> void:
if node.is_empty():
return
var gnode := BeehaveGraphNode.new(horizontal_layout)
add_child(gnode)
gnode.title_text = node.name
gnode.name = node.id
gnode.icon = _get_icon(node.type.back())
if node.is_empty():
return
var gnode := BeehaveGraphNode.new(horizontal_layout)
add_child(gnode)
gnode.title_text = node.name
gnode.name = node.id
gnode.icon = _get_icon(node.type.back())
if node.type.has(&"BeehaveTree"):
gnode.set_slots(false, true)
elif node.type.has(&"Leaf"):
gnode.set_slots(true, false)
elif node.type.has(&"Composite") or node.type.has(&"Decorator"):
gnode.set_slots(true, true)
if node.type.has(&"BeehaveTree"):
gnode.set_slots(false, true)
elif node.type.has(&"Leaf"):
gnode.set_slots(true, false)
elif node.type.has(&"Composite") or node.type.has(&"Decorator"):
gnode.set_slots(true, true)
for child in node.get("children", []):
_add_nodes(child)
for child in node.get("children", []):
_add_nodes(child)
func _connect_nodes(node: Dictionary) -> void:
for child in node.get("children", []):
connect_node(node.id, 0, child.id, 0)
_connect_nodes(child)
for child in node.get("children", []):
connect_node(node.id, 0, child.id, 0)
_connect_nodes(child)
func _arrange_nodes(node: Dictionary) -> void:
if arraging_nodes:
return
if arraging_nodes:
return
arraging_nodes = true
arraging_nodes = true
var tree_node := _create_tree_nodes(node)
tree_node.update_positions(horizontal_layout)
_place_nodes(tree_node)
var tree_node := _create_tree_nodes(node)
tree_node.update_positions(horizontal_layout)
_place_nodes(tree_node)
arraging_nodes = false
arraging_nodes = false
func _create_tree_nodes(node: Dictionary, root: TreeNode = null) -> TreeNode:
var tree_node := TreeNode.new(get_node(node.id), root)
for child in node.get("children", []):
var child_node := _create_tree_nodes(child, tree_node)
tree_node.children.push_back(child_node)
return tree_node
var tree_node := TreeNode.new(get_node(node.id), root)
for child in node.get("children", []):
var child_node := _create_tree_nodes(child, tree_node)
tree_node.children.push_back(child_node)
return tree_node
func _place_nodes(node: TreeNode) -> void:
node.item.position_offset = Vector2(node.x, node.y)
for child in node.children:
_place_nodes(child)
node.item.position_offset = Vector2(node.x, node.y)
for child in node.children:
_place_nodes(child)
func _get_icon(type: StringName) -> Texture2D:
var classes := ProjectSettings.get_global_class_list()
for c in classes:
if c["class"] == type:
var icon_path := c.get("icon", String())
if not icon_path.is_empty():
return load(icon_path)
return null
var classes := ProjectSettings.get_global_class_list()
for c in classes:
if c["class"] == type:
var icon_path := c.get("icon", String())
if not icon_path.is_empty():
return load(icon_path)
return null
func get_status(status: int) -> String:
if status == 0:
return "SUCCESS"
elif status == 1:
return "FAILURE"
return "RUNNING"
if status == 0:
return "SUCCESS"
elif status == 1:
return "FAILURE"
return "RUNNING"
func process_begin(instance_id: int) -> void:
if not _is_same_tree(instance_id):
return
if not _is_same_tree(instance_id):
return
for child in get_children():
child.set_meta("status", -1)
for child in get_children():
child.set_meta("status", -1)
func process_tick(instance_id: int, status: int) -> void:
var node := get_node_or_null(str(instance_id))
if node:
node.text = "Status: %s" % get_status(status)
node.set_status(status)
node.set_meta("status", status)
if status == 0 or status == 2:
if not active_nodes.has(node.name):
active_nodes.push_back(node.name)
var node := get_node_or_null(str(instance_id))
if node:
node.text = "Status: %s" % get_status(status)
node.set_status(status)
node.set_meta("status", status)
if status == 0 or status == 2:
if not active_nodes.has(node.name):
active_nodes.push_back(node.name)
func process_end(instance_id: int) -> void:
if not _is_same_tree(instance_id):
return
for child in get_children():
var status := child.get_meta("status", -1)
match status:
0:
active_nodes.erase(child.name)
child.set_color(SUCCESS_COLOR)
1:
active_nodes.erase(child.name)
child.set_color(INACTIVE_COLOR)
2:
child.set_color(ACTIVE_COLOR)
_:
child.text = " "
child.set_status(status)
child.set_color(INACTIVE_COLOR)
if not _is_same_tree(instance_id):
return
for child in get_children():
var status := child.get_meta("status", -1)
match status:
0:
active_nodes.erase(child.name)
child.set_color(SUCCESS_COLOR)
1:
active_nodes.erase(child.name)
child.set_color(INACTIVE_COLOR)
2:
child.set_color(ACTIVE_COLOR)
_:
child.text = " "
child.set_status(status)
child.set_color(INACTIVE_COLOR)
func _is_same_tree(instance_id: int) -> bool:
return str(instance_id) == beehave_tree.get("id", "")
return str(instance_id) == beehave_tree.get("id", "")
func _get_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array:
var points: PackedVector2Array
var points: PackedVector2Array
from_position = from_position.round()
to_position = to_position.round()
from_position = from_position.round()
to_position = to_position.round()
points.push_back(from_position)
points.push_back(from_position)
var mid_position := ((to_position + from_position) / 2).round()
if horizontal_layout:
points.push_back(Vector2(mid_position.x, from_position.y))
points.push_back(Vector2(mid_position.x, to_position.y))
else:
points.push_back(Vector2(from_position.x, mid_position.y))
points.push_back(Vector2(to_position.x, mid_position.y))
var mid_position := ((to_position + from_position) / 2).round()
if horizontal_layout:
points.push_back(Vector2(mid_position.x, from_position.y))
points.push_back(Vector2(mid_position.x, to_position.y))
else:
points.push_back(Vector2(from_position.x, mid_position.y))
points.push_back(Vector2(to_position.x, mid_position.y))
points.push_back(to_position)
points.push_back(to_position)
return points
return points
func _process(delta: float) -> void:
if not active_nodes.is_empty():
progress += 10 if delta >= 0.05 else 1
if progress >= 1000:
progress = 0
queue_redraw()
if not active_nodes.is_empty():
progress += 10 if delta >= 0.05 else 1
if progress >= 1000:
progress = 0
queue_redraw()
func _draw() -> void:
if active_nodes.is_empty():
return
if active_nodes.is_empty():
return
var circle_size: float = max(3, 6 * zoom)
var progress_shift: float = PROGRESS_SHIFT * zoom
var circle_size: float = max(3, 6 * zoom)
var progress_shift: float = PROGRESS_SHIFT * zoom
var connections := get_connection_list()
for c in connections:
if not c.from in active_nodes or not c.to in active_nodes:
continue
var from := get_node(String(c.from))
var to := get_node(String(c.to))
var connections := get_connection_list()
for c in connections:
if not c.from in active_nodes or not c.to in active_nodes:
continue
var from := get_node(String(c.from))
var to := get_node(String(c.to))
if from.get_meta("status", -1) < 0 or to.get_meta("status", -1) < 0:
return
if from.get_meta("status", -1) < 0 or to.get_meta("status", -1) < 0:
return
var line := _get_connection_line(from.position + from.get_connection_output_position(c.from_port), to.position + to.get_connection_input_position(c.to_port))
var line := _get_connection_line(from.position + from.get_connection_output_position(c.from_port), to.position + to.get_connection_input_position(c.to_port))
var curve = Curve2D.new()
for l in line:
curve.add_point(l)
var curve = Curve2D.new()
for l in line:
curve.add_point(l)
var max_steps := int(curve.get_baked_length())
var current_shift := progress % max_steps
var p := curve.sample_baked(current_shift)
draw_circle(p, circle_size, ACTIVE_COLOR)
var max_steps := int(curve.get_baked_length())
var current_shift := progress % max_steps
var p := curve.sample_baked(current_shift)
draw_circle(p, circle_size, ACTIVE_COLOR)
var shift := current_shift - progress_shift
while shift >= 0:
draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR)
shift -= progress_shift
var shift := current_shift - progress_shift
while shift >= 0:
draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR)
shift -= progress_shift
shift = current_shift + progress_shift
while shift <= curve.get_baked_length():
draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR)
shift += progress_shift
shift = current_shift + progress_shift
while shift <= curve.get_baked_length():
draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR)
shift += progress_shift
func _update_layout_button() -> void:
layout_button.icon = VERTICAL_LAYOUT_ICON if horizontal_layout else HORIZONTAL_LAYOUT_ICON
layout_button.tooltip_text = "Switch to Vertical layout" if horizontal_layout else "Switch to Horizontal layout"
layout_button.icon = VERTICAL_LAYOUT_ICON if horizontal_layout else HORIZONTAL_LAYOUT_ICON
layout_button.tooltip_text = "Switch to Vertical layout" if horizontal_layout else "Switch to Horizontal layout"

@ -2,144 +2,140 @@
extends GraphNode
const DEFAULT_COLOR := Color("#dad4cb")
const PORT_TOP_ICON := preload("icons/port_top.svg")
const PORT_TOP_ICON := preload("icons/port_top.svg")
const PORT_BOTTOM_ICON := preload("icons/port_bottom.svg")
const PORT_LEFT_ICON := preload("icons/port_left.svg")
const PORT_RIGHT_ICON := preload("icons/port_right.svg")
const PORT_LEFT_ICON := preload("icons/port_left.svg")
const PORT_RIGHT_ICON := preload("icons/port_right.svg")
@export var title_text: String:
set(value):
title_text = value
if title_label:
title_label.text = value
set(value):
title_text = value
if title_label:
title_label.text = value
@export var text: String:
set(value):
text = value
if label:
label.text = " " if text.is_empty() else text
set(value):
text = value
if label:
label.text = " " if text.is_empty() else text
@export var icon: Texture2D:
set(value):
icon = value
if icon_rect:
icon_rect.texture = value
set(value):
icon = value
if icon_rect:
icon_rect.texture = value
var layout_size: float:
get:
return size.y if horizontal else size.x
get:
return size.y if horizontal else size.x
var panel: PanelContainer
var icon_rect: TextureRect
var title_label: Label
var container: VBoxContainer
var label: Label
var frames: RefCounted = BeehaveUtils.get_frames()
var horizontal: bool = false
var horizontal: bool = false
func _init(horizontal: bool = false) -> void:
self.horizontal = horizontal
self.horizontal = horizontal
func _ready() -> void:
custom_minimum_size = Vector2(50, 50) * BeehaveUtils.get_editor_scale()
draggable = false
custom_minimum_size = Vector2(50, 50) * BeehaveUtils.get_editor_scale()
draggable = false
add_theme_stylebox_override("frame", frames.empty)
add_theme_stylebox_override("selected_frame", frames.empty)
add_theme_color_override("close_color", Color.TRANSPARENT)
add_theme_icon_override("close", ImageTexture.new())
add_theme_stylebox_override("frame", frames.empty)
add_theme_stylebox_override("selected_frame", frames.empty)
add_theme_color_override("close_color", Color.TRANSPARENT)
add_theme_icon_override("close", ImageTexture.new())
# For top port
add_child(Control.new())
# For top port
add_child(Control.new())
panel = PanelContainer.new()
panel.mouse_filter = Control.MOUSE_FILTER_PASS
panel.add_theme_stylebox_override("panel", frames.normal)
add_child(panel)
panel = PanelContainer.new()
panel.mouse_filter = Control.MOUSE_FILTER_PASS
panel.add_theme_stylebox_override("panel", frames.normal)
add_child(panel)
var vbox_container := VBoxContainer.new()
panel.add_child(vbox_container)
var vbox_container := VBoxContainer.new()
panel.add_child(vbox_container)
var title_size := 24 * BeehaveUtils.get_editor_scale()
var margin_container := MarginContainer.new()
margin_container.add_theme_constant_override("margin_top", -title_size - 2 * BeehaveUtils.get_editor_scale())
margin_container.mouse_filter = Control.MOUSE_FILTER_PASS
vbox_container.add_child(margin_container)
var title_size := 24 * BeehaveUtils.get_editor_scale()
var margin_container := MarginContainer.new()
margin_container.add_theme_constant_override("margin_top", -title_size - 2 * BeehaveUtils.get_editor_scale())
margin_container.mouse_filter = Control.MOUSE_FILTER_PASS
vbox_container.add_child(margin_container)
var title_container := HBoxContainer.new()
title_container.add_child(Control.new())
title_container.mouse_filter = Control.MOUSE_FILTER_PASS
title_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
margin_container.add_child(title_container)
var title_container := HBoxContainer.new()
title_container.add_child(Control.new())
title_container.mouse_filter = Control.MOUSE_FILTER_PASS
title_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
margin_container.add_child(title_container)
icon_rect = TextureRect.new()
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
title_container.add_child(icon_rect)
icon_rect = TextureRect.new()
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
title_container.add_child(icon_rect)
title_label = Label.new()
title_label.add_theme_color_override("font_color", DEFAULT_COLOR)
title_label.add_theme_font_override("font", get_theme_font("title_font"))
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title_label.text = title_text
title_container.add_child(title_label)
title_label = Label.new()
title_label.add_theme_color_override("font_color", DEFAULT_COLOR)
title_label.add_theme_font_override("font", get_theme_font("title_font"))
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title_label.text = title_text
title_container.add_child(title_label)
title_container.add_child(Control.new())
title_container.add_child(Control.new())
container = VBoxContainer.new()
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.add_child(container)
container = VBoxContainer.new()
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.add_child(container)
label = Label.new()
label.text = " " if text.is_empty() else text
container.add_child(label)
label = Label.new()
label.text = " " if text.is_empty() else text
container.add_child(label)
# For bottom port
add_child(Control.new())
# For bottom port
add_child(Control.new())
minimum_size_changed.connect(_on_size_changed)
_on_size_changed.call_deferred()
minimum_size_changed.connect(_on_size_changed)
_on_size_changed.call_deferred()
func set_status(status: int) -> void:
panel.add_theme_stylebox_override("panel", _get_stylebox(status))
panel.add_theme_stylebox_override("panel", _get_stylebox(status))
func _get_stylebox(status: int) -> StyleBox:
match status:
0: return frames.success
1: return frames.failure
2: return frames.running
_: return frames.normal
match status:
0: return frames.success
1: return frames.failure
2: return frames.running
_: return frames.normal
func set_slots(left_enabled: bool, right_enabled: bool) -> void:
if horizontal:
set_slot(1, left_enabled, 0, Color.WHITE, right_enabled, 0, Color.WHITE, PORT_LEFT_ICON, PORT_RIGHT_ICON)
else:
set_slot(0, left_enabled, 0, Color.WHITE, false, -2, Color.TRANSPARENT, PORT_TOP_ICON, null)
set_slot(2, false, -1, Color.TRANSPARENT, right_enabled, 0, Color.WHITE, null, PORT_BOTTOM_ICON)
if horizontal:
set_slot(1, left_enabled, 0, Color.WHITE, right_enabled, 0, Color.WHITE, PORT_LEFT_ICON, PORT_RIGHT_ICON)
else:
set_slot(0, left_enabled, 0, Color.WHITE, false, -2, Color.TRANSPARENT, PORT_TOP_ICON, null)
set_slot(2, false, -1, Color.TRANSPARENT, right_enabled, 0, Color.WHITE, null, PORT_BOTTOM_ICON)
func set_color(color: Color) -> void:
set_input_color(color)
set_output_color(color)
set_input_color(color)
set_output_color(color)
func set_input_color(color: Color) -> void:
set_slot_color_left(1 if horizontal else 0, color)
set_slot_color_left(1 if horizontal else 0, color)
func set_output_color(color: Color) -> void:
set_slot_color_right(1 if horizontal else 2, color)
set_slot_color_right(1 if horizontal else 2, color)
func _on_size_changed():
add_theme_constant_override("port_offset", 12 * BeehaveUtils.get_editor_scale() if horizontal else round(size.x / 2.0))
add_theme_constant_override("port_offset", 12 * BeehaveUtils.get_editor_scale() if horizontal else round(size.x / 2.0))

@ -1 +1,11 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" transform="matrix(0 -1 1 0 0 16)"><rect height="6" rx="1" stroke-width=".6" width="6" y="10"/><rect height="6" rx="1" stroke-width=".780723" width="6" x="5"/><rect height="6" rx="1" stroke-width=".780723" width="6" x="10" y="10"/><path d="m7 5h2v4h-2z" stroke-width=".768491"/><rect height="4" rx="1" ry="0" stroke-width=".768491" width="2" x="12" y="7"/><rect height="5" rx="1" stroke-width=".859" width="2" x="2" y="7"/><path d="m3 7h10v2h-10z" stroke-width="1.09113"/></g></svg>
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg">
<g fill="#e0e0e0" transform="matrix(0 -1 1 0 0 16)">
<rect height="6" rx="1" stroke-width=".6" width="6" y="10"/>
<rect height="6" rx="1" stroke-width=".780723" width="6" x="5"/>
<rect height="6" rx="1" stroke-width=".780723" width="6" x="10" y="10"/>
<path d="m7 5h2v4h-2z" stroke-width=".768491"/>
<rect height="4" rx="1" ry="0" stroke-width=".768491" width="2" x="12" y="7"/>
<rect height="5" rx="1" stroke-width=".859" width="2" x="2" y="7"/>
<path d="m3 7h10v2h-10z" stroke-width="1.09113"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 562 B

After

Width:  |  Height:  |  Size: 636 B

@ -1 +1,4 @@
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg"><path d="m10 4a5 5 0 0 1 -2.5000001 4.3301271 5 5 0 0 1 -5-.0000002 5 5 0 0 1 -2.4999999-4.3301269" fill="#fff" fill-rule="evenodd"/></svg>
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg">
<path d="m10 4a5 5 0 0 1 -2.5000001 4.3301271 5 5 0 0 1 -5-.0000002 5 5 0 0 1 -2.4999999-4.3301269" fill="#fff"
fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 238 B

@ -1 +1,4 @@
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg"><path d="m5 0a5 5 0 0 0 -4.33012712 2.5000001 5 5 0 0 0 .0000002 5 5 5 0 0 0 4.33012692 2.4999999" fill="#fff" fill-rule="evenodd"/></svg>
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg">
<path d="m5 0a5 5 0 0 0 -4.33012712 2.5000001 5 5 0 0 0 .0000002 5 5 5 0 0 0 4.33012692 2.4999999" fill="#fff"
fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 221 B

After

Width:  |  Height:  |  Size: 237 B

@ -1 +1,4 @@
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg"><path d="m4.5 10a5 5 0 0 0 4.3301271-2.5000002 5 5 0 0 0 -.0000002-4.9999999 5 5 0 0 0 -4.3301269-2.4999999" fill="#fff" fill-rule="evenodd"/></svg>
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg">
<path d="m4.5 10a5 5 0 0 0 4.3301271-2.5000002 5 5 0 0 0 -.0000002-4.9999999 5 5 0 0 0 -4.3301269-2.4999999"
fill="#fff" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 231 B

After

Width:  |  Height:  |  Size: 247 B

@ -1 +1,4 @@
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg"><path d="m10-6a5 5 0 0 1 -2.5000001 4.3301271 5 5 0 0 1 -5-.0000002 5 5 0 0 1 -2.4999999-4.3301269" fill="#fff" fill-rule="evenodd" transform="scale(1 -1)"/></svg>
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg">
<path d="m10-6a5 5 0 0 1 -2.5000001 4.3301271 5 5 0 0 1 -5-.0000002 5 5 0 0 1 -2.4999999-4.3301269" fill="#fff"
fill-rule="evenodd" transform="scale(1 -1)"/>
</svg>

Before

Width:  |  Height:  |  Size: 246 B

After

Width:  |  Height:  |  Size: 262 B

@ -1 +1,11 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0"><rect height="6" rx="1" stroke-width=".6" width="6" y="10"/><rect height="6" rx="1" stroke-width=".780723" width="6" x="5"/><rect height="6" rx="1" stroke-width=".780723" width="6" x="10" y="10"/><path d="m7 5h2v4h-2z" stroke-width=".768491"/><rect height="4" rx="1" ry="0" stroke-width=".768491" width="2" x="12" y="7"/><rect height="5" rx="1" stroke-width=".859" width="2" x="2" y="7"/><path d="m3 7h10v2h-10z" stroke-width="1.09113"/></g></svg>
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg">
<g fill="#e0e0e0">
<rect height="6" rx="1" stroke-width=".6" width="6" y="10"/>
<rect height="6" rx="1" stroke-width=".780723" width="6" x="5"/>
<rect height="6" rx="1" stroke-width=".780723" width="6" x="10" y="10"/>
<path d="m7 5h2v4h-2z" stroke-width=".768491"/>
<rect height="4" rx="1" ry="0" stroke-width=".768491" width="2" x="12" y="7"/>
<rect height="5" rx="1" stroke-width=".859" width="2" x="2" y="7"/>
<path d="m3 7h10v2h-10z" stroke-width="1.09113"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 602 B

@ -4,252 +4,250 @@ extends RefCounted
# Based on https://rachel53461.wordpress.com/2014/04/20/algorithm-for-drawing-trees/
const SIBLING_DISTANCE: float = 20.0
const LEVEL_DISTANCE: float = 40.0
const LEVEL_DISTANCE: float = 40.0
var x: float
var y: float
var mod: float
var parent: TreeNode
var children: Array[TreeNode]
var item: GraphNode
func _init(p_item: GraphNode = null, p_parent: TreeNode = null) -> void:
parent = p_parent
item = p_item
parent = p_parent
item = p_item
func is_leaf() -> bool:
return children.is_empty()
return children.is_empty()
func is_most_left() -> bool:
if not parent:
return true
return parent.children.front() == self
if not parent:
return true
return parent.children.front() == self
func is_most_right() -> bool:
if not parent:
return true
return parent.children.back() == self
if not parent:
return true
return parent.children.back() == self
func get_previous_sibling() -> TreeNode:
if not parent or is_most_left():
return null
return parent.children[parent.children.find(self) - 1]
if not parent or is_most_left():
return null
return parent.children[parent.children.find(self) - 1]
func get_next_sibling() -> TreeNode:
if not parent or is_most_right():
return null
return parent.children[parent.children.find(self) + 1]
if not parent or is_most_right():
return null
return parent.children[parent.children.find(self) + 1]
func get_most_left_sibling() -> TreeNode:
if not parent:
return null
if not parent:
return null
if is_most_left():
return self
if is_most_left():
return self
return parent.children.front()
return parent.children.front()
func get_most_left_child() -> TreeNode:
if children.is_empty():
return null
return children.front()
if children.is_empty():
return null
return children.front()
func get_most_right_child() -> TreeNode:
if children.is_empty():
return null
return children.back()
if children.is_empty():
return null
return children.back()
func update_positions(horizontally: bool = false) -> void:
_initialize_nodes(self, 0)
_calculate_initial_x(self)
_initialize_nodes(self, 0)
_calculate_initial_x(self)
_check_all_children_on_screen(self)
_calculate_final_positions(self, 0)
_check_all_children_on_screen(self)
_calculate_final_positions(self, 0)
if horizontally:
_swap_x_y(self)
_calculate_x(self, 0)
else:
_calculate_y(self, 0)
if horizontally:
_swap_x_y(self)
_calculate_x(self, 0)
else:
_calculate_y(self, 0)
func _initialize_nodes(node: TreeNode, depth: int) -> void:
node.x = -1
node.y = depth
node.mod = 0
node.x = -1
node.y = depth
node.mod = 0
for child in node.children:
_initialize_nodes(child, depth + 1)
for child in node.children:
_initialize_nodes(child, depth + 1)
func _calculate_initial_x(node: TreeNode) -> void:
for child in node.children:
_calculate_initial_x(child)
if node.is_leaf():
if not node.is_most_left():
node.x = node.get_previous_sibling().x + node.get_previous_sibling().item.layout_size + SIBLING_DISTANCE
else:
node.x = 0
else:
var mid: float
if node.children.size() == 1:
var offset: float = (node.children.front().item.layout_size - node.item.layout_size) / 2
mid = node.children.front().x + offset
else:
var left_child := node.get_most_left_child()
var right_child := node.get_most_right_child()
mid = (left_child.x + right_child.x + right_child.item.layout_size - node.item.layout_size) / 2
if node.is_most_left():
node.x = mid
else:
node.x = node.get_previous_sibling().x + node.get_previous_sibling().item.layout_size + SIBLING_DISTANCE
node.mod = node.x - mid
if not node.is_leaf() and not node.is_most_left():
_check_for_conflicts(node)
for child in node.children:
_calculate_initial_x(child)
if node.is_leaf():
if not node.is_most_left():
node.x = node.get_previous_sibling().x + node.get_previous_sibling().item.layout_size + SIBLING_DISTANCE
else:
node.x = 0
else:
var mid: float
if node.children.size() == 1:
var offset: float = (node.children.front().item.layout_size - node.item.layout_size) / 2
mid = node.children.front().x + offset
else:
var left_child := node.get_most_left_child()
var right_child := node.get_most_right_child()
mid = (left_child.x + right_child.x + right_child.item.layout_size - node.item.layout_size) / 2
if node.is_most_left():
node.x = mid
else:
node.x = node.get_previous_sibling().x + node.get_previous_sibling().item.layout_size + SIBLING_DISTANCE
node.mod = node.x - mid
if not node.is_leaf() and not node.is_most_left():
_check_for_conflicts(node)
func _calculate_final_positions(node: TreeNode, mod_sum: float) -> void:
node.x += mod_sum
mod_sum += node.mod
node.x += mod_sum
mod_sum += node.mod
for child in node.children:
_calculate_final_positions(child, mod_sum)
for child in node.children:
_calculate_final_positions(child, mod_sum)
func _check_all_children_on_screen(node: TreeNode) -> void:
var node_contour: Dictionary = {}
_get_left_contour(node, 0, node_contour)
var node_contour: Dictionary = {}
_get_left_contour(node, 0, node_contour)
var shift_amount: float = 0
for y in node_contour.keys():
if node_contour[y] + shift_amount < 0:
shift_amount = (node_contour[y] * -1)
var shift_amount: float = 0
for y in node_contour.keys():
if node_contour[y] + shift_amount < 0:
shift_amount = (node_contour[y] * -1)
if shift_amount > 0:
node.x += shift_amount
node.mod += shift_amount
if shift_amount > 0:
node.x += shift_amount
node.mod += shift_amount
func _check_for_conflicts(node: TreeNode) -> void:
var min_distance := SIBLING_DISTANCE
var shift_value: float = 0
var shift_sibling: TreeNode = null
var min_distance := SIBLING_DISTANCE
var shift_value: float = 0
var shift_sibling: TreeNode = null
var node_contour: Dictionary = {}# { int, float }
_get_left_contour(node, 0, node_contour)
var node_contour: Dictionary = {}# { int, float }
_get_left_contour(node, 0, node_contour)
var sibling := node.get_most_left_sibling()
while sibling != null and sibling != node:
var sibling_contour: Dictionary = {}
_get_right_contour(sibling, 0, sibling_contour)
var sibling := node.get_most_left_sibling()
while sibling != null and sibling != node:
var sibling_contour: Dictionary = {}
_get_right_contour(sibling, 0, sibling_contour)
for level in range(node.y + 1, min(sibling_contour.keys().max(), node_contour.keys().max()) + 1):
var distance: float = node_contour[level] - sibling_contour[level]
if distance + shift_value < min_distance:
shift_value = min_distance - distance
shift_sibling = sibling
for level in range(node.y + 1, min(sibling_contour.keys().max(), node_contour.keys().max()) + 1):
var distance: float = node_contour[level] - sibling_contour[level]
if distance + shift_value < min_distance:
shift_value = min_distance - distance
shift_sibling = sibling
sibling = sibling.get_next_sibling()
sibling = sibling.get_next_sibling()
if shift_value > 0:
node.x += shift_value
node.mod += shift_value
_center_nodes_between(shift_sibling, node)
if shift_value > 0:
node.x += shift_value
node.mod += shift_value
_center_nodes_between(shift_sibling, node)
func _center_nodes_between(left_node: TreeNode, right_node: TreeNode) -> void:
var left_index := left_node.parent.children.find(left_node)
var right_index := left_node.parent.children.find(right_node)
var num_nodes_between: int = (right_index - left_index) - 1
if num_nodes_between > 0:
# The extra distance that needs to be split into num_nodes_between + 1
# in order to find the new node spacing so that nodes are equally spaced
var distance_to_allocate: float = right_node.x - left_node.x - left_node.item.layout_size
# Subtract sizes on nodes in between
for i in range(left_index + 1, right_index):
distance_to_allocate -= left_node.parent.children[i].item.layout_size
# Divide space equally
var distance_between_nodes: float = distance_to_allocate / (num_nodes_between + 1)
var prev_node := left_node
var middle_node := left_node.get_next_sibling()
while middle_node != right_node:
var desire_x: float = prev_node.x + prev_node.item.layout_size + distance_between_nodes
var offset := desire_x - middle_node.x
middle_node.x += offset
middle_node.mod += offset
prev_node = middle_node
middle_node = middle_node.get_next_sibling()
var left_index := left_node.parent.children.find(left_node)
var right_index := left_node.parent.children.find(right_node)
var num_nodes_between: int = (right_index - left_index) - 1
if num_nodes_between > 0:
# The extra distance that needs to be split into num_nodes_between + 1
# in order to find the new node spacing so that nodes are equally spaced
var distance_to_allocate: float = right_node.x - left_node.x - left_node.item.layout_size
# Subtract sizes on nodes in between
for i in range(left_index + 1, right_index):
distance_to_allocate -= left_node.parent.children[i].item.layout_size
# Divide space equally
var distance_between_nodes: float = distance_to_allocate / (num_nodes_between + 1)
var prev_node := left_node
var middle_node := left_node.get_next_sibling()
while middle_node != right_node:
var desire_x: float = prev_node.x + prev_node.item.layout_size + distance_between_nodes
var offset := desire_x - middle_node.x
middle_node.x += offset
middle_node.mod += offset
prev_node = middle_node
middle_node = middle_node.get_next_sibling()
func _get_left_contour(node: TreeNode, mod_sum: float, values: Dictionary) -> void:
var node_left: float = node.x + mod_sum
var depth := int(node.y)
if not values.has(depth):
values[depth] = node_left
else:
values[depth] = min(values[depth], node_left)
var node_left: float = node.x + mod_sum
var depth := int(node.y)
if not values.has(depth):
values[depth] = node_left
else:
values[depth] = min(values[depth], node_left)
mod_sum += node.mod
for child in node.children:
_get_left_contour(child, mod_sum, values)
mod_sum += node.mod
for child in node.children:
_get_left_contour(child, mod_sum, values)
func _get_right_contour(node: TreeNode, mod_sum: float, values: Dictionary) -> void:
var node_right: float = node.x + mod_sum + node.item.layout_size
var depth := int(node.y)
if not values.has(depth):
values[depth] = node_right
else:
values[depth] = max(values[depth], node_right)
var node_right: float = node.x + mod_sum + node.item.layout_size
var depth := int(node.y)
if not values.has(depth):
values[depth] = node_right
else:
values[depth] = max(values[depth], node_right)
mod_sum += node.mod
for child in node.children:
_get_right_contour(child, mod_sum, values)
mod_sum += node.mod
for child in node.children:
_get_right_contour(child, mod_sum, values)
func _swap_x_y(node: TreeNode) -> void:
for child in node.children:
_swap_x_y(child)
for child in node.children:
_swap_x_y(child)
var temp := node.x
node.x = node.y
node.y = temp
var temp := node.x
node.x = node.y
node.y = temp
func _calculate_x(node: TreeNode, offset: int) -> void:
node.x = offset
var sibling := node.get_most_left_sibling()
var max_size: int = node.item.size.x
while sibling != null:
max_size = max(sibling.item.size.x, max_size)
sibling = sibling.get_next_sibling()
node.x = offset
var sibling := node.get_most_left_sibling()
var max_size: int = node.item.size.x
while sibling != null:
max_size = max(sibling.item.size.x, max_size)
sibling = sibling.get_next_sibling()
for child in node.children:
_calculate_x(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale())
for child in node.children:
_calculate_x(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale())
func _calculate_y(node: TreeNode, offset: int) -> void:
node.y = offset
var sibling := node.get_most_left_sibling()
var max_size: int = node.item.size.y
while sibling != null:
max_size = max(sibling.item.size.y, max_size)
sibling = sibling.get_next_sibling()
for child in node.children:
_calculate_y(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale())
node.y = offset
var sibling := node.get_most_left_sibling()
var max_size: int = node.item.size.y
while sibling != null:
max_size = max(sibling.item.size.y, max_size)
sibling = sibling.get_next_sibling()
for child in node.children:
_calculate_y(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale())

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="18.512016"
width="14.055721"
version="1.1"
id="svg8741"
sodipodi:docname="action.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8745" />
<sodipodi:namedview
id="namedview8743"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="7.6367532"
inkscape:cx="-17.284833"
inkscape:cy="19.183545"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg8741" />
<path
d="M 10.176432,3.6912688 Q 9.4137999,3.5779847 8.9573199,2.9622304 8.5018279,2.346623 8.6151119,1.5839909 q 0.113137,-0.76164287 0.727903,-1.21827047 0.615754,-0.45648072 1.3783861,-0.34319658 0.762632,0.1132841 1.218124,0.72889148 0.45648,0.61575427 0.343343,1.37739717 -0.113284,0.7626321 -0.729038,1.2191128 -0.614766,0.4566276 -1.377398,0.3433435 z M 8.3557339,18.50048 q -0.412474,-0.06127 -0.675246,-0.415727 -0.263613,-0.355592 -0.2024895,-0.767077 l 0.5938965,-3.998131 -1.1673025,-1.499791 -0.492278,1.19059 Q 6.067908,13.906466 5.2412215,14.299263 4.4143881,14.693049 3.5141916,14.369273 L 0.68012778,13.316428 q -0.3971933,-0.164141 -0.5825748,-0.549563 -0.1843923,-0.385274 -0.0202507,-0.782468 0.1463082,-0.420062 0.5218382,-0.606912 0.3753831,-0.185862 0.79544492,-0.03955 L 4.2327238,12.370146 6.3483399,6.4922013 5.1950516,6.7424632 4.9226408,8.5763411 Q 4.8584317,9.0085983 4.5028393,9.2722113 4.1483829,9.534983 3.7359088,9.473712 3.3234346,9.412442 3.0497828,9.0563693 2.7772671,8.6994546 2.8414761,8.2671975 L 3.0925819,6.5767458 Q 3.1935238,5.897202 3.6684284,5.3884589 4.1443222,4.8798627 4.8023572,4.703636 L 6.9923039,4.1443377 q 0.743073,-0.1838143 1.140791,-0.2409975 0.398708,-0.057036 0.728094,-0.00811 0.515345,0.076551 0.87859,0.3943727 0.3633911,0.3168322 0.5670941,0.7898969 l 0.629089,1.4410733 q 0.295795,0.6970267 0.885176,1.2688312 0.589381,0.5718045 1.447044,0.8892676 0.421051,0.1464552 0.62705,0.4611386 0.205852,0.3156725 0.147667,0.7073745 -0.07347,0.494573 -0.448694,0.754259 -0.376218,0.259539 -0.813966,0.08937 Q 11.753166,10.370427 10.957563,9.778099 10.159981,9.1854775 9.5990659,8.4288493 l -0.661707,1.9024227 0.845301,1.115306 q 0.2835431,0.357542 0.3859911,0.804445 0.102448,0.446903 0.03501,0.900921 l -0.6642771,4.471933 q -0.06112,0.411485 -0.416716,0.675098 -0.355445,0.262624 -0.76693,0.201501 z"
id="path8739"
style="fill:#ffb649;fill-opacity:1" />
height="18.512016"
width="14.055721"
version="1.1"
id="svg8741"
sodipodi:docname="action.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8745"/>
<sodipodi:namedview
id="namedview8743"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="7.6367532"
inkscape:cx="-17.284833"
inkscape:cy="19.183545"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg8741"/>
<path
d="M 10.176432,3.6912688 Q 9.4137999,3.5779847 8.9573199,2.9622304 8.5018279,2.346623 8.6151119,1.5839909 q 0.113137,-0.76164287 0.727903,-1.21827047 0.615754,-0.45648072 1.3783861,-0.34319658 0.762632,0.1132841 1.218124,0.72889148 0.45648,0.61575427 0.343343,1.37739717 -0.113284,0.7626321 -0.729038,1.2191128 -0.614766,0.4566276 -1.377398,0.3433435 z M 8.3557339,18.50048 q -0.412474,-0.06127 -0.675246,-0.415727 -0.263613,-0.355592 -0.2024895,-0.767077 l 0.5938965,-3.998131 -1.1673025,-1.499791 -0.492278,1.19059 Q 6.067908,13.906466 5.2412215,14.299263 4.4143881,14.693049 3.5141916,14.369273 L 0.68012778,13.316428 q -0.3971933,-0.164141 -0.5825748,-0.549563 -0.1843923,-0.385274 -0.0202507,-0.782468 0.1463082,-0.420062 0.5218382,-0.606912 0.3753831,-0.185862 0.79544492,-0.03955 L 4.2327238,12.370146 6.3483399,6.4922013 5.1950516,6.7424632 4.9226408,8.5763411 Q 4.8584317,9.0085983 4.5028393,9.2722113 4.1483829,9.534983 3.7359088,9.473712 3.3234346,9.412442 3.0497828,9.0563693 2.7772671,8.6994546 2.8414761,8.2671975 L 3.0925819,6.5767458 Q 3.1935238,5.897202 3.6684284,5.3884589 4.1443222,4.8798627 4.8023572,4.703636 L 6.9923039,4.1443377 q 0.743073,-0.1838143 1.140791,-0.2409975 0.398708,-0.057036 0.728094,-0.00811 0.515345,0.076551 0.87859,0.3943727 0.3633911,0.3168322 0.5670941,0.7898969 l 0.629089,1.4410733 q 0.295795,0.6970267 0.885176,1.2688312 0.589381,0.5718045 1.447044,0.8892676 0.421051,0.1464552 0.62705,0.4611386 0.205852,0.3156725 0.147667,0.7073745 -0.07347,0.494573 -0.448694,0.754259 -0.376218,0.259539 -0.813966,0.08937 Q 11.753166,10.370427 10.957563,9.778099 10.159981,9.1854775 9.5990659,8.4288493 l -0.661707,1.9024227 0.845301,1.115306 q 0.2835431,0.357542 0.3859911,0.804445 0.102448,0.446903 0.03501,0.900921 l -0.6642771,4.471933 q -0.06112,0.411485 -0.416716,0.675098 -0.355445,0.262624 -0.76693,0.201501 z"
id="path8739"
style="fill:#ffb649;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="14.416"
width="15.416"
version="1.1"
id="svg1061"
sodipodi:docname="blackboard.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1065" />
<sodipodi:namedview
id="namedview1063"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="30.547013"
inkscape:cx="-0.42557352"
inkscape:cy="10.62297"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg1061" />
<path
d="M 0,4.333 V 1.937 Q 0,1.125 0.573,0.562 1.146,0 1.937,0 h 11.542 q 0.791,0 1.364,0.562 0.573,0.563 0.573,1.375 v 2.396 z m 4.354,1.396 v 8.687 H 1.937 q -0.791,0 -1.364,-0.573 Q 0,13.27 0,12.479 v -6.75 z m 6.708,0 h 4.354 v 6.75 q 0,0.791 -0.573,1.364 -0.573,0.573 -1.364,0.573 h -2.417 z m -1.396,0 v 8.687 H 5.75 V 5.729 Z"
id="path1059"
style="fill:#c689ff;fill-opacity:1;stroke:none;stroke-opacity:1" />
height="14.416"
width="15.416"
version="1.1"
id="svg1061"
sodipodi:docname="blackboard.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1065"/>
<sodipodi:namedview
id="namedview1063"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="30.547013"
inkscape:cx="-0.42557352"
inkscape:cy="10.62297"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg1061"/>
<path
d="M 0,4.333 V 1.937 Q 0,1.125 0.573,0.562 1.146,0 1.937,0 h 11.542 q 0.791,0 1.364,0.562 0.573,0.563 0.573,1.375 v 2.396 z m 4.354,1.396 v 8.687 H 1.937 q -0.791,0 -1.364,-0.573 Q 0,13.27 0,12.479 v -6.75 z m 6.708,0 h 4.354 v 6.75 q 0,0.791 -0.573,1.364 -0.573,0.573 -1.364,0.573 h -2.417 z m -1.396,0 v 8.687 H 5.75 V 5.729 Z"
id="path1059"
style="fill:#c689ff;fill-opacity:1;stroke:none;stroke-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16.375999"
width="14.126321"
version="1.1"
id="svg4"
sodipodi:docname="category_bt.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="7.071068"
inkscape:cy="10.377447"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="m 11.48968,7.688 q -0.354,0 -0.76,-0.219 -0.406,-0.219 -0.552,-0.51 L 9.5736798,6 q -0.167,-0.291 -0.167,-0.729 0,-0.437 0.167,-0.729 L 10.07368,3.667 q 0.146,-0.291 0.552,-0.531 0.406,-0.24 0.76,-0.24 h 0.875 q 0.375,0 0.792,0.219 0.417,0.219 0.562,0.552 l 0.355,0.854 q 0.145,0.334 0.156,0.761 0.01,0.427 -0.136,0.76 l -0.396,0.875 q -0.145,0.333 -0.552,0.552 -0.406,0.219 -0.781,0.219 z m -4.8120002,2.917 q -0.354,0 -0.761,-0.219 -0.406,-0.219 -0.573,-0.531 l -0.374,-0.917 q -0.146,-0.312 -0.146,-0.75 0,-0.438 0.146,-0.75 l 0.374,-0.917 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.917 q 0.146,0.312 0.146,0.75 0,0.438 -0.146,0.75 l -0.396,0.917 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m 0,-5.813 q -0.354,0 -0.761,-0.219 -0.406,-0.218 -0.573,-0.531 l -0.374,-0.875 q -0.146,-0.333 -0.146,-0.771 0,-0.437 0.146,-0.75 l 0.374,-0.896 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.896 q 0.146,0.313 0.146,0.75 0,0.438 -0.146,0.771 l -0.396,0.875 q -0.146,0.313 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m -4.813,2.896 q -0.375,0 -0.771,-0.219 Q 0.69867978,7.25 0.53167978,6.917 l -0.375,-0.875 Q -0.01032022,5.709 6.7977528e-4,5.282 0.01067978,4.855 0.17767978,4.521 l 0.333,-0.854 q 0.167,-0.333 0.57300002,-0.552 0.406,-0.219 0.781,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.573,0.552 l 0.334,0.854 q 0.166,0.334 0.177,0.761 0.01,0.427 -0.157,0.76 l -0.374,0.875 q -0.167,0.333 -0.563,0.552 -0.396,0.219 -0.771,0.219 z m 0,5.75 q -0.375,0 -0.781,-0.219 Q 0.67767978,13 0.51067978,12.667 l -0.333,-0.854 Q 0.01067978,11.48 6.7977528e-4,11.053 -0.01032022,10.626 0.15667978,10.292 l 0.375,-0.875 q 0.167,-0.333 0.56200002,-0.552 0.396,-0.219 0.771,-0.219 h 0.771 q 0.375,0 0.771,0.219 0.396,0.219 0.563,0.552 l 0.374,0.875 q 0.167,0.334 0.157,0.761 -0.011,0.427 -0.177,0.76 l -0.334,0.854 q -0.166,0.333 -0.573,0.552 -0.406,0.219 -0.781,0.219 z m 4.855,2.938 q -0.355,0 -0.761,-0.219 -0.406,-0.219 -0.594,-0.531 l -0.375,-0.896 q -0.166,-0.313 -0.166,-0.75 0,-0.438 0.146,-0.771 l 0.374,-0.875 q 0.167,-0.313 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.218 0.552,0.531 l 0.396,0.875 q 0.146,0.333 0.146,0.771 0,0.437 -0.146,0.75 l -0.396,0.896 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z M 11.48968,13.438 q -0.354,0 -0.77,-0.219 -0.417,-0.219 -0.563,-0.51 L 9.5936798,11.771 q -0.166,-0.291 -0.177,-0.718 -0.01,-0.427 0.157,-0.719 L 10.17768,9.376 q 0.146,-0.292 0.552,-0.511 0.406,-0.219 0.76,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.552,0.552 l 0.396,0.875 q 0.146,0.334 0.136,0.761 -0.011,0.427 -0.156,0.76 l -0.355,0.854 q -0.145,0.333 -0.562,0.552 -0.417,0.219 -0.792,0.219 z"
id="path2"
style="fill:#c689ff;fill-opacity:1" />
height="16.375999"
width="14.126321"
version="1.1"
id="svg4"
sodipodi:docname="category_bt.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8"/>
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="7.071068"
inkscape:cy="10.377447"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4"/>
<path
d="m 11.48968,7.688 q -0.354,0 -0.76,-0.219 -0.406,-0.219 -0.552,-0.51 L 9.5736798,6 q -0.167,-0.291 -0.167,-0.729 0,-0.437 0.167,-0.729 L 10.07368,3.667 q 0.146,-0.291 0.552,-0.531 0.406,-0.24 0.76,-0.24 h 0.875 q 0.375,0 0.792,0.219 0.417,0.219 0.562,0.552 l 0.355,0.854 q 0.145,0.334 0.156,0.761 0.01,0.427 -0.136,0.76 l -0.396,0.875 q -0.145,0.333 -0.552,0.552 -0.406,0.219 -0.781,0.219 z m -4.8120002,2.917 q -0.354,0 -0.761,-0.219 -0.406,-0.219 -0.573,-0.531 l -0.374,-0.917 q -0.146,-0.312 -0.146,-0.75 0,-0.438 0.146,-0.75 l 0.374,-0.917 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.917 q 0.146,0.312 0.146,0.75 0,0.438 -0.146,0.75 l -0.396,0.917 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m 0,-5.813 q -0.354,0 -0.761,-0.219 -0.406,-0.218 -0.573,-0.531 l -0.374,-0.875 q -0.146,-0.333 -0.146,-0.771 0,-0.437 0.146,-0.75 l 0.374,-0.896 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.896 q 0.146,0.313 0.146,0.75 0,0.438 -0.146,0.771 l -0.396,0.875 q -0.146,0.313 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m -4.813,2.896 q -0.375,0 -0.771,-0.219 Q 0.69867978,7.25 0.53167978,6.917 l -0.375,-0.875 Q -0.01032022,5.709 6.7977528e-4,5.282 0.01067978,4.855 0.17767978,4.521 l 0.333,-0.854 q 0.167,-0.333 0.57300002,-0.552 0.406,-0.219 0.781,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.573,0.552 l 0.334,0.854 q 0.166,0.334 0.177,0.761 0.01,0.427 -0.157,0.76 l -0.374,0.875 q -0.167,0.333 -0.563,0.552 -0.396,0.219 -0.771,0.219 z m 0,5.75 q -0.375,0 -0.781,-0.219 Q 0.67767978,13 0.51067978,12.667 l -0.333,-0.854 Q 0.01067978,11.48 6.7977528e-4,11.053 -0.01032022,10.626 0.15667978,10.292 l 0.375,-0.875 q 0.167,-0.333 0.56200002,-0.552 0.396,-0.219 0.771,-0.219 h 0.771 q 0.375,0 0.771,0.219 0.396,0.219 0.563,0.552 l 0.374,0.875 q 0.167,0.334 0.157,0.761 -0.011,0.427 -0.177,0.76 l -0.334,0.854 q -0.166,0.333 -0.573,0.552 -0.406,0.219 -0.781,0.219 z m 4.855,2.938 q -0.355,0 -0.761,-0.219 -0.406,-0.219 -0.594,-0.531 l -0.375,-0.896 q -0.166,-0.313 -0.166,-0.75 0,-0.438 0.146,-0.771 l 0.374,-0.875 q 0.167,-0.313 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.218 0.552,0.531 l 0.396,0.875 q 0.146,0.333 0.146,0.771 0,0.437 -0.146,0.75 l -0.396,0.896 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z M 11.48968,13.438 q -0.354,0 -0.77,-0.219 -0.417,-0.219 -0.563,-0.51 L 9.5936798,11.771 q -0.166,-0.291 -0.177,-0.718 -0.01,-0.427 0.157,-0.719 L 10.17768,9.376 q 0.146,-0.292 0.552,-0.511 0.406,-0.219 0.76,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.552,0.552 l 0.396,0.875 q 0.146,0.334 0.136,0.761 -0.011,0.427 -0.156,0.76 l -0.355,0.854 q -0.145,0.333 -0.562,0.552 -0.417,0.219 -0.792,0.219 z"
id="path2"
style="fill:#c689ff;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16.375999"
width="14.126321"
version="1.1"
id="svg4"
sodipodi:docname="category_composite.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="7.071068"
inkscape:cy="10.377447"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="m 11.48968,7.688 q -0.354,0 -0.76,-0.219 -0.406,-0.219 -0.552,-0.51 L 9.5736798,6 q -0.167,-0.291 -0.167,-0.729 0,-0.437 0.167,-0.729 L 10.07368,3.667 q 0.146,-0.291 0.552,-0.531 0.406,-0.24 0.76,-0.24 h 0.875 q 0.375,0 0.792,0.219 0.417,0.219 0.562,0.552 l 0.355,0.854 q 0.145,0.334 0.156,0.761 0.01,0.427 -0.136,0.76 l -0.396,0.875 q -0.145,0.333 -0.552,0.552 -0.406,0.219 -0.781,0.219 z m -4.8120002,2.917 q -0.354,0 -0.761,-0.219 -0.406,-0.219 -0.573,-0.531 l -0.374,-0.917 q -0.146,-0.312 -0.146,-0.75 0,-0.438 0.146,-0.75 l 0.374,-0.917 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.917 q 0.146,0.312 0.146,0.75 0,0.438 -0.146,0.75 l -0.396,0.917 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m 0,-5.813 q -0.354,0 -0.761,-0.219 -0.406,-0.218 -0.573,-0.531 l -0.374,-0.875 q -0.146,-0.333 -0.146,-0.771 0,-0.437 0.146,-0.75 l 0.374,-0.896 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.896 q 0.146,0.313 0.146,0.75 0,0.438 -0.146,0.771 l -0.396,0.875 q -0.146,0.313 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m -4.813,2.896 q -0.375,0 -0.771,-0.219 Q 0.69867978,7.25 0.53167978,6.917 l -0.375,-0.875 Q -0.01032022,5.709 6.7977528e-4,5.282 0.01067978,4.855 0.17767978,4.521 l 0.333,-0.854 q 0.167,-0.333 0.57300002,-0.552 0.406,-0.219 0.781,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.573,0.552 l 0.334,0.854 q 0.166,0.334 0.177,0.761 0.01,0.427 -0.157,0.76 l -0.374,0.875 q -0.167,0.333 -0.563,0.552 -0.396,0.219 -0.771,0.219 z m 0,5.75 q -0.375,0 -0.781,-0.219 Q 0.67767978,13 0.51067978,12.667 l -0.333,-0.854 Q 0.01067978,11.48 6.7977528e-4,11.053 -0.01032022,10.626 0.15667978,10.292 l 0.375,-0.875 q 0.167,-0.333 0.56200002,-0.552 0.396,-0.219 0.771,-0.219 h 0.771 q 0.375,0 0.771,0.219 0.396,0.219 0.563,0.552 l 0.374,0.875 q 0.167,0.334 0.157,0.761 -0.011,0.427 -0.177,0.76 l -0.334,0.854 q -0.166,0.333 -0.573,0.552 -0.406,0.219 -0.781,0.219 z m 4.855,2.938 q -0.355,0 -0.761,-0.219 -0.406,-0.219 -0.594,-0.531 l -0.375,-0.896 q -0.166,-0.313 -0.166,-0.75 0,-0.438 0.146,-0.771 l 0.374,-0.875 q 0.167,-0.313 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.218 0.552,0.531 l 0.396,0.875 q 0.146,0.333 0.146,0.771 0,0.437 -0.146,0.75 l -0.396,0.896 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z M 11.48968,13.438 q -0.354,0 -0.77,-0.219 -0.417,-0.219 -0.563,-0.51 L 9.5936798,11.771 q -0.166,-0.291 -0.177,-0.718 -0.01,-0.427 0.157,-0.719 L 10.17768,9.376 q 0.146,-0.292 0.552,-0.511 0.406,-0.219 0.76,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.552,0.552 l 0.396,0.875 q 0.146,0.334 0.136,0.761 -0.011,0.427 -0.156,0.76 l -0.355,0.854 q -0.145,0.333 -0.562,0.552 -0.417,0.219 -0.792,0.219 z"
id="path2"
style="fill:#40d29f;fill-opacity:1" />
height="16.375999"
width="14.126321"
version="1.1"
id="svg4"
sodipodi:docname="category_composite.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8"/>
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="7.071068"
inkscape:cy="10.377447"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4"/>
<path
d="m 11.48968,7.688 q -0.354,0 -0.76,-0.219 -0.406,-0.219 -0.552,-0.51 L 9.5736798,6 q -0.167,-0.291 -0.167,-0.729 0,-0.437 0.167,-0.729 L 10.07368,3.667 q 0.146,-0.291 0.552,-0.531 0.406,-0.24 0.76,-0.24 h 0.875 q 0.375,0 0.792,0.219 0.417,0.219 0.562,0.552 l 0.355,0.854 q 0.145,0.334 0.156,0.761 0.01,0.427 -0.136,0.76 l -0.396,0.875 q -0.145,0.333 -0.552,0.552 -0.406,0.219 -0.781,0.219 z m -4.8120002,2.917 q -0.354,0 -0.761,-0.219 -0.406,-0.219 -0.573,-0.531 l -0.374,-0.917 q -0.146,-0.312 -0.146,-0.75 0,-0.438 0.146,-0.75 l 0.374,-0.917 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.917 q 0.146,0.312 0.146,0.75 0,0.438 -0.146,0.75 l -0.396,0.917 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m 0,-5.813 q -0.354,0 -0.761,-0.219 -0.406,-0.218 -0.573,-0.531 l -0.374,-0.875 q -0.146,-0.333 -0.146,-0.771 0,-0.437 0.146,-0.75 l 0.374,-0.896 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.896 q 0.146,0.313 0.146,0.75 0,0.438 -0.146,0.771 l -0.396,0.875 q -0.146,0.313 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m -4.813,2.896 q -0.375,0 -0.771,-0.219 Q 0.69867978,7.25 0.53167978,6.917 l -0.375,-0.875 Q -0.01032022,5.709 6.7977528e-4,5.282 0.01067978,4.855 0.17767978,4.521 l 0.333,-0.854 q 0.167,-0.333 0.57300002,-0.552 0.406,-0.219 0.781,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.573,0.552 l 0.334,0.854 q 0.166,0.334 0.177,0.761 0.01,0.427 -0.157,0.76 l -0.374,0.875 q -0.167,0.333 -0.563,0.552 -0.396,0.219 -0.771,0.219 z m 0,5.75 q -0.375,0 -0.781,-0.219 Q 0.67767978,13 0.51067978,12.667 l -0.333,-0.854 Q 0.01067978,11.48 6.7977528e-4,11.053 -0.01032022,10.626 0.15667978,10.292 l 0.375,-0.875 q 0.167,-0.333 0.56200002,-0.552 0.396,-0.219 0.771,-0.219 h 0.771 q 0.375,0 0.771,0.219 0.396,0.219 0.563,0.552 l 0.374,0.875 q 0.167,0.334 0.157,0.761 -0.011,0.427 -0.177,0.76 l -0.334,0.854 q -0.166,0.333 -0.573,0.552 -0.406,0.219 -0.781,0.219 z m 4.855,2.938 q -0.355,0 -0.761,-0.219 -0.406,-0.219 -0.594,-0.531 l -0.375,-0.896 q -0.166,-0.313 -0.166,-0.75 0,-0.438 0.146,-0.771 l 0.374,-0.875 q 0.167,-0.313 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.218 0.552,0.531 l 0.396,0.875 q 0.146,0.333 0.146,0.771 0,0.437 -0.146,0.75 l -0.396,0.896 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z M 11.48968,13.438 q -0.354,0 -0.77,-0.219 -0.417,-0.219 -0.563,-0.51 L 9.5936798,11.771 q -0.166,-0.291 -0.177,-0.718 -0.01,-0.427 0.157,-0.719 L 10.17768,9.376 q 0.146,-0.292 0.552,-0.511 0.406,-0.219 0.76,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.552,0.552 l 0.396,0.875 q 0.146,0.334 0.136,0.761 -0.011,0.427 -0.156,0.76 l -0.355,0.854 q -0.145,0.333 -0.562,0.552 -0.417,0.219 -0.792,0.219 z"
id="path2"
style="fill:#40d29f;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16.375999"
width="14.126321"
version="1.1"
id="svg4"
sodipodi:docname="category_decorator.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="7.071068"
inkscape:cy="10.377447"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="m 11.48968,7.688 q -0.354,0 -0.76,-0.219 -0.406,-0.219 -0.552,-0.51 L 9.5736798,6 q -0.167,-0.291 -0.167,-0.729 0,-0.437 0.167,-0.729 L 10.07368,3.667 q 0.146,-0.291 0.552,-0.531 0.406,-0.24 0.76,-0.24 h 0.875 q 0.375,0 0.792,0.219 0.417,0.219 0.562,0.552 l 0.355,0.854 q 0.145,0.334 0.156,0.761 0.01,0.427 -0.136,0.76 l -0.396,0.875 q -0.145,0.333 -0.552,0.552 -0.406,0.219 -0.781,0.219 z m -4.8120002,2.917 q -0.354,0 -0.761,-0.219 -0.406,-0.219 -0.573,-0.531 l -0.374,-0.917 q -0.146,-0.312 -0.146,-0.75 0,-0.438 0.146,-0.75 l 0.374,-0.917 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.917 q 0.146,0.312 0.146,0.75 0,0.438 -0.146,0.75 l -0.396,0.917 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m 0,-5.813 q -0.354,0 -0.761,-0.219 -0.406,-0.218 -0.573,-0.531 l -0.374,-0.875 q -0.146,-0.333 -0.146,-0.771 0,-0.437 0.146,-0.75 l 0.374,-0.896 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.896 q 0.146,0.313 0.146,0.75 0,0.438 -0.146,0.771 l -0.396,0.875 q -0.146,0.313 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m -4.813,2.896 q -0.375,0 -0.771,-0.219 Q 0.69867978,7.25 0.53167978,6.917 l -0.375,-0.875 Q -0.01032022,5.709 6.7977528e-4,5.282 0.01067978,4.855 0.17767978,4.521 l 0.333,-0.854 q 0.167,-0.333 0.57300002,-0.552 0.406,-0.219 0.781,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.573,0.552 l 0.334,0.854 q 0.166,0.334 0.177,0.761 0.01,0.427 -0.157,0.76 l -0.374,0.875 q -0.167,0.333 -0.563,0.552 -0.396,0.219 -0.771,0.219 z m 0,5.75 q -0.375,0 -0.781,-0.219 Q 0.67767978,13 0.51067978,12.667 l -0.333,-0.854 Q 0.01067978,11.48 6.7977528e-4,11.053 -0.01032022,10.626 0.15667978,10.292 l 0.375,-0.875 q 0.167,-0.333 0.56200002,-0.552 0.396,-0.219 0.771,-0.219 h 0.771 q 0.375,0 0.771,0.219 0.396,0.219 0.563,0.552 l 0.374,0.875 q 0.167,0.334 0.157,0.761 -0.011,0.427 -0.177,0.76 l -0.334,0.854 q -0.166,0.333 -0.573,0.552 -0.406,0.219 -0.781,0.219 z m 4.855,2.938 q -0.355,0 -0.761,-0.219 -0.406,-0.219 -0.594,-0.531 l -0.375,-0.896 q -0.166,-0.313 -0.166,-0.75 0,-0.438 0.146,-0.771 l 0.374,-0.875 q 0.167,-0.313 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.218 0.552,0.531 l 0.396,0.875 q 0.146,0.333 0.146,0.771 0,0.437 -0.146,0.75 l -0.396,0.896 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z M 11.48968,13.438 q -0.354,0 -0.77,-0.219 -0.417,-0.219 -0.563,-0.51 L 9.5936798,11.771 q -0.166,-0.291 -0.177,-0.718 -0.01,-0.427 0.157,-0.719 L 10.17768,9.376 q 0.146,-0.292 0.552,-0.511 0.406,-0.219 0.76,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.552,0.552 l 0.396,0.875 q 0.146,0.334 0.136,0.761 -0.011,0.427 -0.156,0.76 l -0.355,0.854 q -0.145,0.333 -0.562,0.552 -0.417,0.219 -0.792,0.219 z"
id="path2"
style="fill:#46c0e1;fill-opacity:1" />
height="16.375999"
width="14.126321"
version="1.1"
id="svg4"
sodipodi:docname="category_decorator.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8"/>
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="7.071068"
inkscape:cy="10.377447"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4"/>
<path
d="m 11.48968,7.688 q -0.354,0 -0.76,-0.219 -0.406,-0.219 -0.552,-0.51 L 9.5736798,6 q -0.167,-0.291 -0.167,-0.729 0,-0.437 0.167,-0.729 L 10.07368,3.667 q 0.146,-0.291 0.552,-0.531 0.406,-0.24 0.76,-0.24 h 0.875 q 0.375,0 0.792,0.219 0.417,0.219 0.562,0.552 l 0.355,0.854 q 0.145,0.334 0.156,0.761 0.01,0.427 -0.136,0.76 l -0.396,0.875 q -0.145,0.333 -0.552,0.552 -0.406,0.219 -0.781,0.219 z m -4.8120002,2.917 q -0.354,0 -0.761,-0.219 -0.406,-0.219 -0.573,-0.531 l -0.374,-0.917 q -0.146,-0.312 -0.146,-0.75 0,-0.438 0.146,-0.75 l 0.374,-0.917 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.917 q 0.146,0.312 0.146,0.75 0,0.438 -0.146,0.75 l -0.396,0.917 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m 0,-5.813 q -0.354,0 -0.761,-0.219 -0.406,-0.218 -0.573,-0.531 l -0.374,-0.875 q -0.146,-0.333 -0.146,-0.771 0,-0.437 0.146,-0.75 l 0.374,-0.896 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.896 q 0.146,0.313 0.146,0.75 0,0.438 -0.146,0.771 l -0.396,0.875 q -0.146,0.313 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m -4.813,2.896 q -0.375,0 -0.771,-0.219 Q 0.69867978,7.25 0.53167978,6.917 l -0.375,-0.875 Q -0.01032022,5.709 6.7977528e-4,5.282 0.01067978,4.855 0.17767978,4.521 l 0.333,-0.854 q 0.167,-0.333 0.57300002,-0.552 0.406,-0.219 0.781,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.573,0.552 l 0.334,0.854 q 0.166,0.334 0.177,0.761 0.01,0.427 -0.157,0.76 l -0.374,0.875 q -0.167,0.333 -0.563,0.552 -0.396,0.219 -0.771,0.219 z m 0,5.75 q -0.375,0 -0.781,-0.219 Q 0.67767978,13 0.51067978,12.667 l -0.333,-0.854 Q 0.01067978,11.48 6.7977528e-4,11.053 -0.01032022,10.626 0.15667978,10.292 l 0.375,-0.875 q 0.167,-0.333 0.56200002,-0.552 0.396,-0.219 0.771,-0.219 h 0.771 q 0.375,0 0.771,0.219 0.396,0.219 0.563,0.552 l 0.374,0.875 q 0.167,0.334 0.157,0.761 -0.011,0.427 -0.177,0.76 l -0.334,0.854 q -0.166,0.333 -0.573,0.552 -0.406,0.219 -0.781,0.219 z m 4.855,2.938 q -0.355,0 -0.761,-0.219 -0.406,-0.219 -0.594,-0.531 l -0.375,-0.896 q -0.166,-0.313 -0.166,-0.75 0,-0.438 0.146,-0.771 l 0.374,-0.875 q 0.167,-0.313 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.218 0.552,0.531 l 0.396,0.875 q 0.146,0.333 0.146,0.771 0,0.437 -0.146,0.75 l -0.396,0.896 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z M 11.48968,13.438 q -0.354,0 -0.77,-0.219 -0.417,-0.219 -0.563,-0.51 L 9.5936798,11.771 q -0.166,-0.291 -0.177,-0.718 -0.01,-0.427 0.157,-0.719 L 10.17768,9.376 q 0.146,-0.292 0.552,-0.511 0.406,-0.219 0.76,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.552,0.552 l 0.396,0.875 q 0.146,0.334 0.136,0.761 -0.011,0.427 -0.156,0.76 l -0.355,0.854 q -0.145,0.333 -0.562,0.552 -0.417,0.219 -0.792,0.219 z"
id="path2"
style="fill:#46c0e1;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16.375999"
width="14.126321"
version="1.1"
id="svg4"
sodipodi:docname="category_leaf.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="7.071068"
inkscape:cy="10.377447"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="m 11.48968,7.688 q -0.354,0 -0.76,-0.219 -0.406,-0.219 -0.552,-0.51 L 9.5736798,6 q -0.167,-0.291 -0.167,-0.729 0,-0.437 0.167,-0.729 L 10.07368,3.667 q 0.146,-0.291 0.552,-0.531 0.406,-0.24 0.76,-0.24 h 0.875 q 0.375,0 0.792,0.219 0.417,0.219 0.562,0.552 l 0.355,0.854 q 0.145,0.334 0.156,0.761 0.01,0.427 -0.136,0.76 l -0.396,0.875 q -0.145,0.333 -0.552,0.552 -0.406,0.219 -0.781,0.219 z m -4.8120002,2.917 q -0.354,0 -0.761,-0.219 -0.406,-0.219 -0.573,-0.531 l -0.374,-0.917 q -0.146,-0.312 -0.146,-0.75 0,-0.438 0.146,-0.75 l 0.374,-0.917 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.917 q 0.146,0.312 0.146,0.75 0,0.438 -0.146,0.75 l -0.396,0.917 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m 0,-5.813 q -0.354,0 -0.761,-0.219 -0.406,-0.218 -0.573,-0.531 l -0.374,-0.875 q -0.146,-0.333 -0.146,-0.771 0,-0.437 0.146,-0.75 l 0.374,-0.896 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.896 q 0.146,0.313 0.146,0.75 0,0.438 -0.146,0.771 l -0.396,0.875 q -0.146,0.313 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m -4.813,2.896 q -0.375,0 -0.771,-0.219 Q 0.69867978,7.25 0.53167978,6.917 l -0.375,-0.875 Q -0.01032022,5.709 6.7977528e-4,5.282 0.01067978,4.855 0.17767978,4.521 l 0.333,-0.854 q 0.167,-0.333 0.57300002,-0.552 0.406,-0.219 0.781,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.573,0.552 l 0.334,0.854 q 0.166,0.334 0.177,0.761 0.01,0.427 -0.157,0.76 l -0.374,0.875 q -0.167,0.333 -0.563,0.552 -0.396,0.219 -0.771,0.219 z m 0,5.75 q -0.375,0 -0.781,-0.219 Q 0.67767978,13 0.51067978,12.667 l -0.333,-0.854 Q 0.01067978,11.48 6.7977528e-4,11.053 -0.01032022,10.626 0.15667978,10.292 l 0.375,-0.875 q 0.167,-0.333 0.56200002,-0.552 0.396,-0.219 0.771,-0.219 h 0.771 q 0.375,0 0.771,0.219 0.396,0.219 0.563,0.552 l 0.374,0.875 q 0.167,0.334 0.157,0.761 -0.011,0.427 -0.177,0.76 l -0.334,0.854 q -0.166,0.333 -0.573,0.552 -0.406,0.219 -0.781,0.219 z m 4.855,2.938 q -0.355,0 -0.761,-0.219 -0.406,-0.219 -0.594,-0.531 l -0.375,-0.896 q -0.166,-0.313 -0.166,-0.75 0,-0.438 0.146,-0.771 l 0.374,-0.875 q 0.167,-0.313 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.218 0.552,0.531 l 0.396,0.875 q 0.146,0.333 0.146,0.771 0,0.437 -0.146,0.75 l -0.396,0.896 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z M 11.48968,13.438 q -0.354,0 -0.77,-0.219 -0.417,-0.219 -0.563,-0.51 L 9.5936798,11.771 q -0.166,-0.291 -0.177,-0.718 -0.01,-0.427 0.157,-0.719 L 10.17768,9.376 q 0.146,-0.292 0.552,-0.511 0.406,-0.219 0.76,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.552,0.552 l 0.396,0.875 q 0.146,0.334 0.136,0.761 -0.011,0.427 -0.156,0.76 l -0.355,0.854 q -0.145,0.333 -0.562,0.552 -0.417,0.219 -0.792,0.219 z"
id="path2"
style="fill:#ffb649;fill-opacity:1" />
height="16.375999"
width="14.126321"
version="1.1"
id="svg4"
sodipodi:docname="category_leaf.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8"/>
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="7.071068"
inkscape:cy="10.377447"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4"/>
<path
d="m 11.48968,7.688 q -0.354,0 -0.76,-0.219 -0.406,-0.219 -0.552,-0.51 L 9.5736798,6 q -0.167,-0.291 -0.167,-0.729 0,-0.437 0.167,-0.729 L 10.07368,3.667 q 0.146,-0.291 0.552,-0.531 0.406,-0.24 0.76,-0.24 h 0.875 q 0.375,0 0.792,0.219 0.417,0.219 0.562,0.552 l 0.355,0.854 q 0.145,0.334 0.156,0.761 0.01,0.427 -0.136,0.76 l -0.396,0.875 q -0.145,0.333 -0.552,0.552 -0.406,0.219 -0.781,0.219 z m -4.8120002,2.917 q -0.354,0 -0.761,-0.219 -0.406,-0.219 -0.573,-0.531 l -0.374,-0.917 q -0.146,-0.312 -0.146,-0.75 0,-0.438 0.146,-0.75 l 0.374,-0.917 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.917 q 0.146,0.312 0.146,0.75 0,0.438 -0.146,0.75 l -0.396,0.917 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m 0,-5.813 q -0.354,0 -0.761,-0.219 -0.406,-0.218 -0.573,-0.531 l -0.374,-0.875 q -0.146,-0.333 -0.146,-0.771 0,-0.437 0.146,-0.75 l 0.374,-0.896 q 0.167,-0.312 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.219 0.552,0.531 l 0.396,0.896 q 0.146,0.313 0.146,0.75 0,0.438 -0.146,0.771 l -0.396,0.875 q -0.146,0.313 -0.552,0.531 -0.406,0.219 -0.781,0.219 z m -4.813,2.896 q -0.375,0 -0.771,-0.219 Q 0.69867978,7.25 0.53167978,6.917 l -0.375,-0.875 Q -0.01032022,5.709 6.7977528e-4,5.282 0.01067978,4.855 0.17767978,4.521 l 0.333,-0.854 q 0.167,-0.333 0.57300002,-0.552 0.406,-0.219 0.781,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.573,0.552 l 0.334,0.854 q 0.166,0.334 0.177,0.761 0.01,0.427 -0.157,0.76 l -0.374,0.875 q -0.167,0.333 -0.563,0.552 -0.396,0.219 -0.771,0.219 z m 0,5.75 q -0.375,0 -0.781,-0.219 Q 0.67767978,13 0.51067978,12.667 l -0.333,-0.854 Q 0.01067978,11.48 6.7977528e-4,11.053 -0.01032022,10.626 0.15667978,10.292 l 0.375,-0.875 q 0.167,-0.333 0.56200002,-0.552 0.396,-0.219 0.771,-0.219 h 0.771 q 0.375,0 0.771,0.219 0.396,0.219 0.563,0.552 l 0.374,0.875 q 0.167,0.334 0.157,0.761 -0.011,0.427 -0.177,0.76 l -0.334,0.854 q -0.166,0.333 -0.573,0.552 -0.406,0.219 -0.781,0.219 z m 4.855,2.938 q -0.355,0 -0.761,-0.219 -0.406,-0.219 -0.594,-0.531 l -0.375,-0.896 q -0.166,-0.313 -0.166,-0.75 0,-0.438 0.146,-0.771 l 0.374,-0.875 q 0.167,-0.313 0.573,-0.531 0.407,-0.219 0.761,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.406,0.218 0.552,0.531 l 0.396,0.875 q 0.146,0.333 0.146,0.771 0,0.437 -0.146,0.75 l -0.396,0.896 q -0.146,0.312 -0.552,0.531 -0.406,0.219 -0.781,0.219 z M 11.48968,13.438 q -0.354,0 -0.77,-0.219 -0.417,-0.219 -0.563,-0.51 L 9.5936798,11.771 q -0.166,-0.291 -0.177,-0.718 -0.01,-0.427 0.157,-0.719 L 10.17768,9.376 q 0.146,-0.292 0.552,-0.511 0.406,-0.219 0.76,-0.219 h 0.771 q 0.375,0 0.781,0.219 0.407,0.219 0.552,0.552 l 0.396,0.875 q 0.146,0.334 0.136,0.761 -0.011,0.427 -0.156,0.76 l -0.355,0.854 q -0.145,0.333 -0.562,0.552 -0.417,0.219 -0.792,0.219 z"
id="path2"
style="fill:#ffb649;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="18.084"
width="18.084"
version="1.1"
id="svg4180"
sodipodi:docname="condition.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4184" />
<sodipodi:namedview
id="namedview4182"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="7.6367532"
inkscape:cx="-10.737547"
inkscape:cy="24.290428"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4180" />
<path
d="m 8.938,14.063 q 0.542,0 0.927,-0.386 0.385,-0.385 0.385,-0.927 0,-0.541 -0.385,-0.916 -0.385,-0.375 -0.927,-0.375 -0.542,0 -0.927,0.375 -0.386,0.375 -0.386,0.916 0,0.542 0.386,0.927 0.385,0.386 0.927,0.386 z M 9.042,5.938 q 0.396,0 0.646,0.229 0.25,0.229 0.25,0.542 0,0.229 -0.177,0.562 Q 9.584,7.604 9.292,7.834 8.646,8.417 8.323,8.927 8,9.438 8,9.917 q 0,0.375 0.261,0.635 0.26,0.261 0.656,0.261 0.396,0 0.698,-0.24 0.302,-0.239 0.469,-0.698 0.104,-0.25 0.291,-0.51 0.188,-0.261 0.542,-0.615 0.604,-0.562 0.833,-1.073 0.23,-0.51 0.23,-1.135 0,-1.167 -0.761,-1.865 Q 10.459,3.98 9.188,3.98 8.25,3.98 7.552,4.365 6.854,4.75 6.292,5.48 6.084,5.75 6.198,6.125 6.313,6.5 6.584,6.709 6.938,6.98 7.365,6.886 7.792,6.792 8.104,6.417 8.313,6.209 8.563,6.073 8.813,5.938 9.042,5.938 Z m 0,12.146 q -1.875,0 -3.531,-0.709 Q 3.854,16.667 2.636,15.448 1.417,14.23 0.709,12.584 0,10.938 0,9.042 0,7.146 0.709,5.5 1.417,3.854 2.636,2.636 3.854,1.417 5.511,0.709 7.167,0 9.042,0 q 1.896,0 3.542,0.709 1.646,0.708 2.864,1.927 1.219,1.218 1.927,2.864 0.709,1.646 0.709,3.542 0,1.896 -0.709,3.542 -0.708,1.646 -1.927,2.864 -1.218,1.219 -2.864,1.927 -1.646,0.709 -3.542,0.709 z"
id="path4178"
style="fill:#ffb649;fill-opacity:1" />
height="18.084"
width="18.084"
version="1.1"
id="svg4180"
sodipodi:docname="condition.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4184"/>
<sodipodi:namedview
id="namedview4182"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="7.6367532"
inkscape:cx="-10.737547"
inkscape:cy="24.290428"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg4180"/>
<path
d="m 8.938,14.063 q 0.542,0 0.927,-0.386 0.385,-0.385 0.385,-0.927 0,-0.541 -0.385,-0.916 -0.385,-0.375 -0.927,-0.375 -0.542,0 -0.927,0.375 -0.386,0.375 -0.386,0.916 0,0.542 0.386,0.927 0.385,0.386 0.927,0.386 z M 9.042,5.938 q 0.396,0 0.646,0.229 0.25,0.229 0.25,0.542 0,0.229 -0.177,0.562 Q 9.584,7.604 9.292,7.834 8.646,8.417 8.323,8.927 8,9.438 8,9.917 q 0,0.375 0.261,0.635 0.26,0.261 0.656,0.261 0.396,0 0.698,-0.24 0.302,-0.239 0.469,-0.698 0.104,-0.25 0.291,-0.51 0.188,-0.261 0.542,-0.615 0.604,-0.562 0.833,-1.073 0.23,-0.51 0.23,-1.135 0,-1.167 -0.761,-1.865 Q 10.459,3.98 9.188,3.98 8.25,3.98 7.552,4.365 6.854,4.75 6.292,5.48 6.084,5.75 6.198,6.125 6.313,6.5 6.584,6.709 6.938,6.98 7.365,6.886 7.792,6.792 8.104,6.417 8.313,6.209 8.563,6.073 8.813,5.938 9.042,5.938 Z m 0,12.146 q -1.875,0 -3.531,-0.709 Q 3.854,16.667 2.636,15.448 1.417,14.23 0.709,12.584 0,10.938 0,9.042 0,7.146 0.709,5.5 1.417,3.854 2.636,2.636 3.854,1.417 5.511,0.709 7.167,0 9.042,0 q 1.896,0 3.542,0.709 1.646,0.708 2.864,1.927 1.219,1.218 1.927,2.864 0.709,1.646 0.709,3.542 0,1.896 -0.709,3.542 -0.708,1.646 -1.927,2.864 -1.218,1.219 -2.864,1.927 -1.646,0.709 -3.542,0.709 z"
id="path4178"
style="fill:#ffb649;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="17.666"
width="17.666"
version="1.1"
id="svg6751"
sodipodi:docname="failer.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs6755" />
<sodipodi:namedview
id="namedview6753"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="30.547013"
inkscape:cx="12.308896"
inkscape:cy="5.3851419"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg6751" />
<path
d="m 8.854,9.666 q 0.5,0 0.896,-0.395 0.395,-0.396 0.395,-0.917 v -2.75 q 0,-0.5 -0.395,-0.886 Q 9.354,4.333 8.854,4.333 8.333,4.333 7.937,4.729 7.541,5.125 7.541,5.625 v 2.75 q 0,0.52 0.396,0.906 0.396,0.385 0.917,0.385 z m -0.021,3.959 q 0.583,0 1.01,-0.407 0.428,-0.406 0.428,-1.031 0,-0.583 -0.428,-1.021 -0.427,-0.437 -1.01,-0.437 -0.583,0 -1.01,0.437 -0.428,0.438 -0.428,1.021 0,0.625 0.428,1.031 0.427,0.407 1.01,0.407 z m 0,4.041 q -1.854,0 -3.458,-0.687 Q 3.771,16.291 2.573,15.093 1.375,13.895 0.687,12.291 0,10.687 0,8.833 0,6.958 0.687,5.364 1.375,3.771 2.573,2.573 3.771,1.375 5.375,0.687 6.979,0 8.833,0 q 1.875,0 3.469,0.687 1.593,0.688 2.791,1.886 1.198,1.198 1.886,2.791 0.687,1.594 0.687,3.469 0,1.854 -0.687,3.458 -0.688,1.604 -1.886,2.802 -1.198,1.198 -2.791,1.886 -1.594,0.687 -3.469,0.687 z"
id="path6749"
style="fill:#46c0e1;fill-opacity:1" />
height="17.666"
width="17.666"
version="1.1"
id="svg6751"
sodipodi:docname="failer.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs6755"/>
<sodipodi:namedview
id="namedview6753"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="30.547013"
inkscape:cx="12.308896"
inkscape:cy="5.3851419"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg6751"/>
<path
d="m 8.854,9.666 q 0.5,0 0.896,-0.395 0.395,-0.396 0.395,-0.917 v -2.75 q 0,-0.5 -0.395,-0.886 Q 9.354,4.333 8.854,4.333 8.333,4.333 7.937,4.729 7.541,5.125 7.541,5.625 v 2.75 q 0,0.52 0.396,0.906 0.396,0.385 0.917,0.385 z m -0.021,3.959 q 0.583,0 1.01,-0.407 0.428,-0.406 0.428,-1.031 0,-0.583 -0.428,-1.021 -0.427,-0.437 -1.01,-0.437 -0.583,0 -1.01,0.437 -0.428,0.438 -0.428,1.021 0,0.625 0.428,1.031 0.427,0.407 1.01,0.407 z m 0,4.041 q -1.854,0 -3.458,-0.687 Q 3.771,16.291 2.573,15.093 1.375,13.895 0.687,12.291 0,10.687 0,8.833 0,6.958 0.687,5.364 1.375,3.771 2.573,2.573 3.771,1.375 5.375,0.687 6.979,0 8.833,0 q 1.875,0 3.469,0.687 1.593,0.688 2.791,1.886 1.198,1.198 1.886,2.791 0.687,1.594 0.687,3.469 0,1.854 -0.687,3.458 -0.688,1.604 -1.886,2.802 -1.198,1.198 -2.791,1.886 -1.594,0.687 -3.469,0.687 z"
id="path6749"
style="fill:#46c0e1;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="13.907205"
width="18.437"
version="1.1"
id="svg68"
sodipodi:docname="inverter.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs72" />
<sodipodi:namedview
id="namedview70"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="29.521708"
inkscape:cx="6.3343218"
inkscape:cy="7.8078138"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg68" />
<path
d="m 12.937,8.48 q -0.458,-0.459 -0.458,-1.032 0,-0.572 0.458,-1.031 L 13.416,5.938 H 9.812 Q 9.229,5.938 8.791,5.5 8.354,5.063 8.354,4.48 8.354,3.876 8.791,3.448 9.229,3.021 9.812,3.021 h 3.604 l -0.5,-0.5 Q 12.458,2.063 12.468,1.49 12.479,0.917 12.937,0.48 13.396,0 13.979,0 14.562,0 15,0.459 l 2.979,2.979 q 0.229,0.229 0.344,0.5 0.114,0.271 0.114,0.521 0,0.271 -0.114,0.541 -0.115,0.271 -0.344,0.5 l -3,3 Q 14.52,8.959 13.958,8.948 13.396,8.938 12.937,8.48 Z M 3.416,13.438 0.437,10.459 Q 0.229,10.25 0.114,9.98 0,9.709 0,9.438 0,9.167 0.114,8.896 0.229,8.626 0.437,8.396 l 3.021,-3 Q 3.916,4.938 4.468,4.948 5.02,4.959 5.479,5.417 5.937,5.876 5.937,6.459 5.937,7.042 5.479,7.48 L 5,7.959 h 3.604 q 0.583,0 1.021,0.437 0.437,0.438 0.437,1.021 0,0.604 -0.437,1.031 -0.438,0.428 -1.021,0.428 H 5 l 0.5,0.52 q 0.458,0.459 0.448,1.021 -0.011,0.563 -0.469,1.021 -0.459,0.458 -1.021,0.469 -0.562,0.01 -1.042,-0.469 z"
id="path66"
style="fill:#46c0e1;fill-opacity:1" />
height="13.907205"
width="18.437"
version="1.1"
id="svg68"
sodipodi:docname="inverter.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs72"/>
<sodipodi:namedview
id="namedview70"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="29.521708"
inkscape:cx="6.3343218"
inkscape:cy="7.8078138"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg68"/>
<path
d="m 12.937,8.48 q -0.458,-0.459 -0.458,-1.032 0,-0.572 0.458,-1.031 L 13.416,5.938 H 9.812 Q 9.229,5.938 8.791,5.5 8.354,5.063 8.354,4.48 8.354,3.876 8.791,3.448 9.229,3.021 9.812,3.021 h 3.604 l -0.5,-0.5 Q 12.458,2.063 12.468,1.49 12.479,0.917 12.937,0.48 13.396,0 13.979,0 14.562,0 15,0.459 l 2.979,2.979 q 0.229,0.229 0.344,0.5 0.114,0.271 0.114,0.521 0,0.271 -0.114,0.541 -0.115,0.271 -0.344,0.5 l -3,3 Q 14.52,8.959 13.958,8.948 13.396,8.938 12.937,8.48 Z M 3.416,13.438 0.437,10.459 Q 0.229,10.25 0.114,9.98 0,9.709 0,9.438 0,9.167 0.114,8.896 0.229,8.626 0.437,8.396 l 3.021,-3 Q 3.916,4.938 4.468,4.948 5.02,4.959 5.479,5.417 5.937,5.876 5.937,6.459 5.937,7.042 5.479,7.48 L 5,7.959 h 3.604 q 0.583,0 1.021,0.437 0.437,0.438 0.437,1.021 0,0.604 -0.437,1.031 -0.438,0.428 -1.021,0.428 H 5 l 0.5,0.52 q 0.458,0.459 0.448,1.021 -0.011,0.563 -0.469,1.021 -0.459,0.458 -1.021,0.469 -0.562,0.01 -1.042,-0.469 z"
id="path66"
style="fill:#46c0e1;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="14.75"
width="14.916"
version="1.1"
id="svg3323"
sodipodi:docname="repeater.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3327" />
<sodipodi:namedview
id="namedview3325"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="7.6367532"
inkscape:cx="-37.057633"
inkscape:cy="27.56407"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg3323" />
<path
d="M 7.375,14.75 Q 4.312,14.75 2.156,12.594 0,10.437 0,7.375 0,4.313 2.156,2.156 4.312,0 7.375,0 8.979,0 10.385,0.646 11.791,1.292 12.833,2.5 V 1.042 q 0,-0.417 0.313,-0.729 Q 13.458,0 13.875,0 q 0.416,0 0.729,0.313 0.312,0.312 0.312,0.729 v 4.583 q 0,0.542 -0.406,0.948 -0.406,0.406 -0.948,0.406 H 8.958 Q 8.541,6.979 8.229,6.677 7.916,6.375 7.916,5.958 7.916,5.542 8.229,5.229 8.541,4.917 8.958,4.917 h 2.354 Q 10.646,3.917 9.614,3.313 8.583,2.708 7.375,2.708 q -1.938,0 -3.302,1.365 -1.365,1.364 -1.365,3.302 0,1.938 1.365,3.302 1.364,1.365 3.302,1.365 1.25,0 2.291,-0.615 1.042,-0.614 1.688,-1.677 0.208,-0.313 0.562,-0.521 0.354,-0.208 0.688,-0.208 0.854,0 1.239,0.573 0.386,0.573 0.011,1.26 -0.979,1.771 -2.698,2.833 -1.719,1.063 -3.781,1.063 z"
id="path3321"
style="fill:#46c0e1;fill-opacity:1" />
height="14.75"
width="14.916"
version="1.1"
id="svg3323"
sodipodi:docname="repeater.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3327"/>
<sodipodi:namedview
id="namedview3325"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="7.6367532"
inkscape:cx="-37.057633"
inkscape:cy="27.56407"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg3323"/>
<path
d="M 7.375,14.75 Q 4.312,14.75 2.156,12.594 0,10.437 0,7.375 0,4.313 2.156,2.156 4.312,0 7.375,0 8.979,0 10.385,0.646 11.791,1.292 12.833,2.5 V 1.042 q 0,-0.417 0.313,-0.729 Q 13.458,0 13.875,0 q 0.416,0 0.729,0.313 0.312,0.312 0.312,0.729 v 4.583 q 0,0.542 -0.406,0.948 -0.406,0.406 -0.948,0.406 H 8.958 Q 8.541,6.979 8.229,6.677 7.916,6.375 7.916,5.958 7.916,5.542 8.229,5.229 8.541,4.917 8.958,4.917 h 2.354 Q 10.646,3.917 9.614,3.313 8.583,2.708 7.375,2.708 q -1.938,0 -3.302,1.365 -1.365,1.364 -1.365,3.302 0,1.938 1.365,3.302 1.364,1.365 3.302,1.365 1.25,0 2.291,-0.615 1.042,-0.614 1.688,-1.677 0.208,-0.313 0.562,-0.521 0.354,-0.208 0.688,-0.208 0.854,0 1.239,0.573 0.386,0.573 0.011,1.26 -0.979,1.771 -2.698,2.833 -1.719,1.063 -3.781,1.063 z"
id="path3321"
style="fill:#46c0e1;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="13.906356"
width="17.791"
version="1.1"
id="svg5037"
sodipodi:docname="selector.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5041" />
<sodipodi:namedview
id="namedview5039"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="7.6367532"
inkscape:cx="61.282588"
inkscape:cy="33.849463"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5037" />
<path
d="M 4.52,13.645 Q 4.25,13.374 4.25,13 4.25,12.624 4.5,12.354 L 5.208,11.666 Q 2.979,11.395 1.489,9.802 0,8.208 0,5.895 0,3.437 1.718,1.718 3.437,0 5.895,0 H 7.562 Q 7.916,0 8.187,0.27 8.458,0.541 8.458,0.895 8.458,1.27 8.187,1.541 7.916,1.812 7.562,1.812 H 5.895 Q 4.187,1.812 3,3 1.812,4.187 1.812,5.895 1.812,7.437 2.76,8.52 3.708,9.604 5.187,9.916 L 4.5,9.229 Q 4.229,8.937 4.239,8.583 4.25,8.229 4.52,7.958 4.77,7.687 5.145,7.687 q 0.375,0 0.646,0.271 l 2.167,2.187 q 0.125,0.125 0.208,0.302 0.084,0.177 0.084,0.365 0,0.167 -0.084,0.344 -0.083,0.177 -0.208,0.323 L 5.75,13.645 Q 5.479,13.916 5.124,13.906 4.77,13.895 4.52,13.645 Z M 12.041,5.958 Q 11.25,5.958 10.708,5.416 10.166,4.874 10.166,4.083 V 1.874 q 0,-0.77 0.542,-1.322 Q 11.25,0 12.041,0 h 3.875 q 0.771,0 1.323,0.552 0.552,0.552 0.552,1.322 v 2.209 q 0,0.791 -0.552,1.333 -0.552,0.542 -1.323,0.542 z m 0,-1.875 h 3.875 V 1.874 h -3.875 z m 0,9.541 q -0.791,0 -1.333,-0.552 Q 10.166,12.52 10.166,11.75 V 9.541 q 0,-0.791 0.542,-1.333 0.542,-0.542 1.333,-0.542 h 3.875 q 0.771,0 1.323,0.542 0.552,0.542 0.552,1.333 v 2.209 q 0,0.77 -0.552,1.322 -0.552,0.552 -1.323,0.552 z"
id="path5035"
style="fill:#40d29f;fill-opacity:1" />
height="13.906356"
width="17.791"
version="1.1"
id="svg5037"
sodipodi:docname="selector.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5041"/>
<sodipodi:namedview
id="namedview5039"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="7.6367532"
inkscape:cx="61.282588"
inkscape:cy="33.849463"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5037"/>
<path
d="M 4.52,13.645 Q 4.25,13.374 4.25,13 4.25,12.624 4.5,12.354 L 5.208,11.666 Q 2.979,11.395 1.489,9.802 0,8.208 0,5.895 0,3.437 1.718,1.718 3.437,0 5.895,0 H 7.562 Q 7.916,0 8.187,0.27 8.458,0.541 8.458,0.895 8.458,1.27 8.187,1.541 7.916,1.812 7.562,1.812 H 5.895 Q 4.187,1.812 3,3 1.812,4.187 1.812,5.895 1.812,7.437 2.76,8.52 3.708,9.604 5.187,9.916 L 4.5,9.229 Q 4.229,8.937 4.239,8.583 4.25,8.229 4.52,7.958 4.77,7.687 5.145,7.687 q 0.375,0 0.646,0.271 l 2.167,2.187 q 0.125,0.125 0.208,0.302 0.084,0.177 0.084,0.365 0,0.167 -0.084,0.344 -0.083,0.177 -0.208,0.323 L 5.75,13.645 Q 5.479,13.916 5.124,13.906 4.77,13.895 4.52,13.645 Z M 12.041,5.958 Q 11.25,5.958 10.708,5.416 10.166,4.874 10.166,4.083 V 1.874 q 0,-0.77 0.542,-1.322 Q 11.25,0 12.041,0 h 3.875 q 0.771,0 1.323,0.552 0.552,0.552 0.552,1.322 v 2.209 q 0,0.791 -0.552,1.333 -0.552,0.542 -1.323,0.542 z m 0,-1.875 h 3.875 V 1.874 h -3.875 z m 0,9.541 q -0.791,0 -1.333,-0.552 Q 10.166,12.52 10.166,11.75 V 9.541 q 0,-0.791 0.542,-1.333 0.542,-0.542 1.333,-0.542 h 3.875 q 0.771,0 1.323,0.542 0.552,0.542 0.552,1.333 v 2.209 q 0,0.77 -0.552,1.322 -0.552,0.552 -1.323,0.552 z"
id="path5035"
style="fill:#40d29f;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

@ -1,45 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="14.543364"
width="17.791"
version="1.1"
id="svg5037"
sodipodi:docname="selector_reactive.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs5041" /><sodipodi:namedview
id="namedview5039"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="18.59429"
inkscape:cy="12.014268"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5037" /><path
style="fill:#40d29f;fill-opacity:1;color:#000000;stroke-width:1.7354;-inkscape-stroke:none;paint-order:markers stroke fill"
d="m 15.63424,18.518399 q -0.218955,0 -0.373746,-0.150649 -0.155154,-0.150651 -0.155154,-0.363746 v -1.161448 l -0.860595,0.49993 q -0.196481,0.103026 -0.400572,0.05152 -0.20373,-0.05152 -0.309582,-0.227915 l -0.181253,-0.308712 q -0.105496,-0.183813 -0.05655,-0.385972 0.04894,-0.202159 0.237805,-0.312236 l 0.868569,-0.492527 -0.883432,-0.485112 q -0.188867,-0.110433 -0.241793,-0.312236 -0.05292,-0.202161 0.05292,-0.385974 l 0.196116,-0.301299 q 0.113465,-0.183814 0.321183,-0.238852 0.207716,-0.05539 0.388971,0.05504 l 0.868208,0.492523 v -1.161448 q 0,-0.213096 0.155154,-0.363747 0.154791,-0.150648 0.373746,-0.150648 h 0.347283 q 0.218955,0 0.373747,0.150648 0.155152,0.150651 0.155152,0.363747 v 1.161448 l 0.868209,-0.499931 q 0.188867,-0.103026 0.388973,-0.04763 0.200104,0.05504 0.313568,0.224034 l 0.211343,0.323526 q 0.113466,0.183813 0.05655,0.38562 -0.05655,0.20216 -0.253031,0.312589 l -0.87582,0.477704 0.86857,0.492521 q 0.181255,0.09561 0.245418,0.304826 0.06416,0.209569 -0.0493,0.385974 l -0.196119,0.316117 q -0.113464,0.176404 -0.313569,0.235323 -0.200105,0.05857 -0.388972,-0.05151 l -0.875822,-0.50734 v 1.161449 q 0,0.213096 -0.155152,0.363745 -0.154792,0.15065 -0.373747,0.15065 z"
id="path5291"
transform="matrix(1.2719165,0,0,1.2110461,-6.1455464,-7.8832739)"
inkscape:label="asterisk" /><path
style="fill:#40d29f;fill-opacity:1"
d="M 12.041,5.958 Q 11.25,5.958 10.708,5.416 10.166,4.874 10.166,4.083 V 1.874 q 0,-0.77 0.542,-1.322 Q 11.25,0 12.041,0 h 3.875 q 0.771,0 1.323,0.552 0.552,0.552 0.552,1.322 v 2.209 q 0,0.791 -0.552,1.333 -0.552,0.542 -1.323,0.542 z m 0,-1.875 h 3.875 V 1.874 h -3.875 z"
id="path923"
inkscape:label="square" /><path
style="fill:#40d29f;fill-opacity:1"
d="M 4.52,13.645 Q 4.25,13.374 4.25,13 4.25,12.624 4.5,12.354 L 5.208,11.666 Q 2.979,11.395 1.489,9.802 0,8.208 0,5.895 0,3.437 1.718,1.718 3.437,0 5.895,0 H 7.562 Q 7.916,0 8.187,0.27 8.458,0.541 8.458,0.895 8.458,1.27 8.187,1.541 7.916,1.812 7.562,1.812 H 5.895 Q 4.187,1.812 3,3 1.812,4.187 1.812,5.895 1.812,7.437 2.76,8.52 3.708,9.604 5.187,9.916 L 4.5,9.229 Q 4.229,8.937 4.239,8.583 4.25,8.229 4.52,7.958 4.77,7.687 5.145,7.687 q 0.375,0 0.646,0.271 l 2.167,2.187 q 0.125,0.125 0.208,0.302 0.084,0.177 0.084,0.365 0,0.167 -0.084,0.344 -0.083,0.177 -0.208,0.323 L 5.75,13.645 Q 5.479,13.916 5.124,13.906 4.77,13.895 4.52,13.645 Z"
id="path5035"
inkscape:label="arrow" /></svg>
height="14.543364"
width="17.791"
version="1.1"
id="svg5037"
sodipodi:docname="selector_reactive.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs5041" />
<sodipodi:namedview
id="namedview5039"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="15.273506"
inkscape:cx="18.59429"
inkscape:cy="12.014268"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5037"/>
<path
style="fill:#40d29f;fill-opacity:1;color:#000000;stroke-width:1.7354;-inkscape-stroke:none;paint-order:markers stroke fill"
d="m 15.63424,18.518399 q -0.218955,0 -0.373746,-0.150649 -0.155154,-0.150651 -0.155154,-0.363746 v -1.161448 l -0.860595,0.49993 q -0.196481,0.103026 -0.400572,0.05152 -0.20373,-0.05152 -0.309582,-0.227915 l -0.181253,-0.308712 q -0.105496,-0.183813 -0.05655,-0.385972 0.04894,-0.202159 0.237805,-0.312236 l 0.868569,-0.492527 -0.883432,-0.485112 q -0.188867,-0.110433 -0.241793,-0.312236 -0.05292,-0.202161 0.05292,-0.385974 l 0.196116,-0.301299 q 0.113465,-0.183814 0.321183,-0.238852 0.207716,-0.05539 0.388971,0.05504 l 0.868208,0.492523 v -1.161448 q 0,-0.213096 0.155154,-0.363747 0.154791,-0.150648 0.373746,-0.150648 h 0.347283 q 0.218955,0 0.373747,0.150648 0.155152,0.150651 0.155152,0.363747 v 1.161448 l 0.868209,-0.499931 q 0.188867,-0.103026 0.388973,-0.04763 0.200104,0.05504 0.313568,0.224034 l 0.211343,0.323526 q 0.113466,0.183813 0.05655,0.38562 -0.05655,0.20216 -0.253031,0.312589 l -0.87582,0.477704 0.86857,0.492521 q 0.181255,0.09561 0.245418,0.304826 0.06416,0.209569 -0.0493,0.385974 l -0.196119,0.316117 q -0.113464,0.176404 -0.313569,0.235323 -0.200105,0.05857 -0.388972,-0.05151 l -0.875822,-0.50734 v 1.161449 q 0,0.213096 -0.155152,0.363745 -0.154792,0.15065 -0.373747,0.15065 z"
id="path5291"
transform="matrix(1.2719165,0,0,1.2110461,-6.1455464,-7.8832739)"
inkscape:label="asterisk"/>
<path
style="fill:#40d29f;fill-opacity:1"
d="M 12.041,5.958 Q 11.25,5.958 10.708,5.416 10.166,4.874 10.166,4.083 V 1.874 q 0,-0.77 0.542,-1.322 Q 11.25,0 12.041,0 h 3.875 q 0.771,0 1.323,0.552 0.552,0.552 0.552,1.322 v 2.209 q 0,0.791 -0.552,1.333 -0.552,0.542 -1.323,0.542 z m 0,-1.875 h 3.875 V 1.874 h -3.875 z"
id="path923"
inkscape:label="square"/>
<path
style="fill:#40d29f;fill-opacity:1"
d="M 4.52,13.645 Q 4.25,13.374 4.25,13 4.25,12.624 4.5,12.354 L 5.208,11.666 Q 2.979,11.395 1.489,9.802 0,8.208 0,5.895 0,3.437 1.718,1.718 3.437,0 5.895,0 H 7.562 Q 7.916,0 8.187,0.27 8.458,0.541 8.458,0.895 8.458,1.27 8.187,1.541 7.916,1.812 7.562,1.812 H 5.895 Q 4.187,1.812 3,3 1.812,4.187 1.812,5.895 1.812,7.437 2.76,8.52 3.708,9.604 5.187,9.916 L 4.5,9.229 Q 4.229,8.937 4.239,8.583 4.25,8.229 4.52,7.958 4.77,7.687 5.145,7.687 q 0.375,0 0.646,0.271 l 2.167,2.187 q 0.125,0.125 0.208,0.302 0.084,0.177 0.084,0.365 0,0.167 -0.084,0.344 -0.083,0.177 -0.208,0.323 L 5.75,13.645 Q 5.479,13.916 5.124,13.906 4.77,13.895 4.52,13.645 Z"
id="path5035"
inkscape:label="arrow"/></svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="13.187"
width="16.583"
version="1.1"
id="svg5894"
sodipodi:docname="sequence.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5898" />
<sodipodi:namedview
id="namedview5896"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="43.2"
inkscape:cx="13.449074"
inkscape:cy="7.974537"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5894" />
<path
d="M 1.709,8.313 Q 1,8.313 0.5,7.823 0,7.333 0,6.604 0,5.896 0.5,5.396 q 0.5,-0.5 1.209,-0.5 0.708,0 1.208,0.5 0.5,0.5 0.5,1.229 0,0.708 -0.5,1.198 -0.5,0.49 -1.208,0.49 z m 0,-4.896 Q 1,3.417 0.5,2.927 0,2.437 0,1.708 0,1 0.5,0.5 1,0 1.709,0 q 0.708,0 1.208,0.5 0.5,0.5 0.5,1.229 0,0.708 -0.5,1.198 -0.5,0.49 -1.208,0.49 z m 0,9.77 Q 1,13.187 0.5,12.698 0,12.208 0,11.479 q 0,-0.708 0.5,-1.208 0.5,-0.5 1.209,-0.5 0.708,0 1.208,0.5 0.5,0.5 0.5,1.229 0,0.708 -0.5,1.198 -0.5,0.489 -1.208,0.489 z m 4.604,-0.395 q -0.5,0 -0.896,-0.396 Q 5.021,12 5.021,11.479 q 0,-0.5 0.396,-0.896 0.396,-0.396 0.896,-0.396 h 8.979 q 0.5,0 0.896,0.396 0.395,0.396 0.395,0.896 0,0.521 -0.395,0.917 -0.396,0.396 -0.896,0.396 z m 0,-4.875 q -0.5,0 -0.896,-0.396 -0.396,-0.396 -0.396,-0.917 0,-0.5 0.396,-0.896 0.396,-0.395 0.896,-0.395 h 8.979 q 0.5,0 0.896,0.395 0.395,0.396 0.395,0.896 0,0.521 -0.395,0.917 -0.396,0.396 -0.896,0.396 z m 0,-4.896 q -0.5,0 -0.896,-0.396 -0.396,-0.396 -0.396,-0.917 0,-0.5 0.396,-0.895 0.396,-0.396 0.896,-0.396 h 8.979 q 0.5,0 0.896,0.396 0.395,0.395 0.395,0.895 0,0.521 -0.395,0.917 -0.396,0.396 -0.896,0.396 z"
id="path5892"
style="fill:#40d29f;fill-opacity:1" />
height="13.187"
width="16.583"
version="1.1"
id="svg5894"
sodipodi:docname="sequence.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5898"/>
<sodipodi:namedview
id="namedview5896"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="43.2"
inkscape:cx="13.449074"
inkscape:cy="7.974537"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5894"/>
<path
d="M 1.709,8.313 Q 1,8.313 0.5,7.823 0,7.333 0,6.604 0,5.896 0.5,5.396 q 0.5,-0.5 1.209,-0.5 0.708,0 1.208,0.5 0.5,0.5 0.5,1.229 0,0.708 -0.5,1.198 -0.5,0.49 -1.208,0.49 z m 0,-4.896 Q 1,3.417 0.5,2.927 0,2.437 0,1.708 0,1 0.5,0.5 1,0 1.709,0 q 0.708,0 1.208,0.5 0.5,0.5 0.5,1.229 0,0.708 -0.5,1.198 -0.5,0.49 -1.208,0.49 z m 0,9.77 Q 1,13.187 0.5,12.698 0,12.208 0,11.479 q 0,-0.708 0.5,-1.208 0.5,-0.5 1.209,-0.5 0.708,0 1.208,0.5 0.5,0.5 0.5,1.229 0,0.708 -0.5,1.198 -0.5,0.489 -1.208,0.489 z m 4.604,-0.395 q -0.5,0 -0.896,-0.396 Q 5.021,12 5.021,11.479 q 0,-0.5 0.396,-0.896 0.396,-0.396 0.896,-0.396 h 8.979 q 0.5,0 0.896,0.396 0.395,0.396 0.395,0.896 0,0.521 -0.395,0.917 -0.396,0.396 -0.896,0.396 z m 0,-4.875 q -0.5,0 -0.896,-0.396 -0.396,-0.396 -0.396,-0.917 0,-0.5 0.396,-0.896 0.396,-0.395 0.896,-0.395 h 8.979 q 0.5,0 0.896,0.395 0.395,0.396 0.395,0.896 0,0.521 -0.395,0.917 -0.396,0.396 -0.896,0.396 z m 0,-4.896 q -0.5,0 -0.896,-0.396 -0.396,-0.396 -0.396,-0.917 0,-0.5 0.396,-0.895 0.396,-0.396 0.896,-0.396 h 8.979 q 0.5,0 0.896,0.396 0.395,0.395 0.395,0.895 0,0.521 -0.395,0.917 -0.396,0.396 -0.896,0.396 z"
id="path5892"
style="fill:#40d29f;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="12.183594"
width="16.582031"
version="1.1"
id="svg5894"
sodipodi:docname="sequence_random.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5898" />
<sodipodi:namedview
id="namedview5896"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="32"
inkscape:cx="-1.59375"
inkscape:cy="7.828125"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5894" />
<path
id="path414"
style="color:#000000;fill:#40d29f;stroke-width:0.4;stroke-linecap:round;paint-order:fill markers stroke"
d="M 1.7089844,0 C 1.2363186,0 0.83333267,0.16666733 0.5,0.5 0.16666733,0.83333267 0,1.2350322 0,1.7070312 c 0,0.4859992 0.16666733,0.8940371 0.5,1.2207032 0.33333267,0.3266659 0.7363186,0.4902343 1.2089844,0.4902344 0.471999,0 0.8756516,-0.1635685 1.2089844,-0.4902344 0.3333326,-0.3266661 0.4999999,-0.7272196 0.5,-1.1992188 0,-0.485999 -0.1666674,-0.89518293 -0.5,-1.2285156 C 2.5846361,0.16666733 2.1809834,0 1.7089844,0 Z M 6.3125,0.41796875 c -0.3333327,0 -0.6305318,0.13053177 -0.8945312,0.39453125 -0.2639996,0.2633328 -0.3964844,0.5611987 -0.3964844,0.8945312 0,0.3473327 0.1324848,0.6539694 0.3964844,0.9179688 C 5.6819682,2.8889994 5.9791673,3.0214844 6.3125,3.0214844 h 8.980469 C 15.626301,3.0214844 15.9235,2.8889994 16.1875,2.625 16.450833,2.3610006 16.582031,2.0543639 16.582031,1.7070312 16.582031,1.3736987 16.450833,1.0758328 16.1875,0.8125 15.9235,0.54850052 15.626301,0.41796875 15.292969,0.41796875 Z M 9.0878906,4.3007812 c -0.2828275,0 -0.5270087,0.1055895 -0.7324218,0.3164063 -0.2050874,0.2104687 -0.3066407,0.4597345 -0.3066407,0.75 v 5.7480465 c 0,0.290266 0.101563,0.543098 0.3066407,0.753907 0.2054131,0.210442 0.4495943,0.314453 0.7324218,0.314453 h 5.6015624 c 0.282829,0 0.527005,-0.103984 0.732422,-0.314453 0.20504,-0.210809 0.310547,-0.463641 0.310547,-0.753907 V 5.3671875 c 0,-0.2902655 -0.105468,-0.5395313 -0.310547,-0.75 -0.192576,-0.1976408 -0.419166,-0.3040153 -0.679687,-0.3164063 -0.01736,-8.263e-4 -0.03505,0 -0.05274,0 z M 1.9257812,5.3535156 C 1.6720837,5.3535062 1.4450691,5.4533683 1.2441406,5.6542969 1.043212,5.8547182 0.94335938,6.0822399 0.94335938,6.3359375 c 0,0.2643529 0.0998526,0.4982903 0.30078122,0.6992187 0.2009285,0.2009285 0.4279431,0.3007813 0.6816406,0.3007813 H 6.8886719 V 5.7070312 c 0,-0.1182132 0.017785,-0.2363441 0.048828,-0.3515624 z m 11.7246098,0.041016 c 0.275278,0 0.533582,0.1086393 0.724609,0.3046876 0.191017,0.1960469 0.294922,0.4582008 0.294922,0.7363281 0,0.2781252 -0.10358,0.53844 -0.294922,0.734375 -0.191911,0.1969508 -0.45114,0.3027343 -0.724609,0.3027343 -0.273472,0 -0.532817,-0.1054585 -0.72461,-0.3027343 -0.192232,-0.1968435 -0.291015,-0.4579656 -0.291015,-0.734375 0,-0.2764085 0.09911,-0.5393695 0.291015,-0.7363281 0.190906,-0.1963654 0.449327,-0.3046876 0.72461,-0.3046876 z M 11.888672,7.203125 c 0.0068,0 0.0244,-0.00128 0.05273,0 0.256852,0.011652 0.493747,0.1218832 0.671875,0.3046875 0.191019,0.1960465 0.296876,0.4562463 0.296875,0.734375 10e-7,0.2781245 -0.105853,0.5402803 -0.296875,0.7363281 C 12.42137,9.1754773 12.162139,9.28125 11.888672,9.28125 c -0.273461,0 -0.532699,-0.1057738 -0.72461,-0.3027344 -0.191923,-0.1969574 -0.291015,-0.4599183 -0.291015,-0.7363281 0,-0.2764094 0.0991,-0.5374165 0.291015,-0.734375 0.191024,-0.1960448 0.449335,-0.3046875 0.72461,-0.3046875 z m -1.75,1.8066406 c 0.0068,0 0.0244,6.732e-4 0.05273,0.00195 0.256848,0.011652 0.49375,0.1238283 0.671875,0.3066406 0.189491,0.1948428 0.294922,0.4530828 0.294922,0.7304688 0,0.278128 -0.103894,0.538327 -0.294922,0.734375 -0.191019,0.196026 -0.449319,0.306641 -0.724609,0.306641 -0.2750313,0 -0.5324,-0.111387 -0.7226564,-0.306641 C 9.2249894,10.587155 9.1210938,10.326955 9.1210938,10.048828 9.1210937,9.7714422 9.2265406,9.5132022 9.4160156,9.3183594 9.6062729,9.1231217 9.8636398,9.0097656 10.138672,9.0097656 Z m -8.216797,0.1875 c -0.2536975,-9.4e-6 -0.478759,0.0979 -0.6796875,0.2988282 -0.2009286,0.2004212 -0.30273437,0.4279427 -0.30273438,0.6816402 0,0.264353 0.10180578,0.498291 0.30273438,0.699219 0.2009285,0.200929 0.42599,0.300781 0.6796875,0.300781 H 6.8867188 V 9.5488281 c 0,-0.1182133 0.017785,-0.2363441 0.048828,-0.3515625 z" />
height="12.183594"
width="16.582031"
version="1.1"
id="svg5894"
sodipodi:docname="sequence_random.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5898"/>
<sodipodi:namedview
id="namedview5896"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="32"
inkscape:cx="-1.59375"
inkscape:cy="7.828125"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5894"/>
<path
id="path414"
style="color:#000000;fill:#40d29f;stroke-width:0.4;stroke-linecap:round;paint-order:fill markers stroke"
d="M 1.7089844,0 C 1.2363186,0 0.83333267,0.16666733 0.5,0.5 0.16666733,0.83333267 0,1.2350322 0,1.7070312 c 0,0.4859992 0.16666733,0.8940371 0.5,1.2207032 0.33333267,0.3266659 0.7363186,0.4902343 1.2089844,0.4902344 0.471999,0 0.8756516,-0.1635685 1.2089844,-0.4902344 0.3333326,-0.3266661 0.4999999,-0.7272196 0.5,-1.1992188 0,-0.485999 -0.1666674,-0.89518293 -0.5,-1.2285156 C 2.5846361,0.16666733 2.1809834,0 1.7089844,0 Z M 6.3125,0.41796875 c -0.3333327,0 -0.6305318,0.13053177 -0.8945312,0.39453125 -0.2639996,0.2633328 -0.3964844,0.5611987 -0.3964844,0.8945312 0,0.3473327 0.1324848,0.6539694 0.3964844,0.9179688 C 5.6819682,2.8889994 5.9791673,3.0214844 6.3125,3.0214844 h 8.980469 C 15.626301,3.0214844 15.9235,2.8889994 16.1875,2.625 16.450833,2.3610006 16.582031,2.0543639 16.582031,1.7070312 16.582031,1.3736987 16.450833,1.0758328 16.1875,0.8125 15.9235,0.54850052 15.626301,0.41796875 15.292969,0.41796875 Z M 9.0878906,4.3007812 c -0.2828275,0 -0.5270087,0.1055895 -0.7324218,0.3164063 -0.2050874,0.2104687 -0.3066407,0.4597345 -0.3066407,0.75 v 5.7480465 c 0,0.290266 0.101563,0.543098 0.3066407,0.753907 0.2054131,0.210442 0.4495943,0.314453 0.7324218,0.314453 h 5.6015624 c 0.282829,0 0.527005,-0.103984 0.732422,-0.314453 0.20504,-0.210809 0.310547,-0.463641 0.310547,-0.753907 V 5.3671875 c 0,-0.2902655 -0.105468,-0.5395313 -0.310547,-0.75 -0.192576,-0.1976408 -0.419166,-0.3040153 -0.679687,-0.3164063 -0.01736,-8.263e-4 -0.03505,0 -0.05274,0 z M 1.9257812,5.3535156 C 1.6720837,5.3535062 1.4450691,5.4533683 1.2441406,5.6542969 1.043212,5.8547182 0.94335938,6.0822399 0.94335938,6.3359375 c 0,0.2643529 0.0998526,0.4982903 0.30078122,0.6992187 0.2009285,0.2009285 0.4279431,0.3007813 0.6816406,0.3007813 H 6.8886719 V 5.7070312 c 0,-0.1182132 0.017785,-0.2363441 0.048828,-0.3515624 z m 11.7246098,0.041016 c 0.275278,0 0.533582,0.1086393 0.724609,0.3046876 0.191017,0.1960469 0.294922,0.4582008 0.294922,0.7363281 0,0.2781252 -0.10358,0.53844 -0.294922,0.734375 -0.191911,0.1969508 -0.45114,0.3027343 -0.724609,0.3027343 -0.273472,0 -0.532817,-0.1054585 -0.72461,-0.3027343 -0.192232,-0.1968435 -0.291015,-0.4579656 -0.291015,-0.734375 0,-0.2764085 0.09911,-0.5393695 0.291015,-0.7363281 0.190906,-0.1963654 0.449327,-0.3046876 0.72461,-0.3046876 z M 11.888672,7.203125 c 0.0068,0 0.0244,-0.00128 0.05273,0 0.256852,0.011652 0.493747,0.1218832 0.671875,0.3046875 0.191019,0.1960465 0.296876,0.4562463 0.296875,0.734375 10e-7,0.2781245 -0.105853,0.5402803 -0.296875,0.7363281 C 12.42137,9.1754773 12.162139,9.28125 11.888672,9.28125 c -0.273461,0 -0.532699,-0.1057738 -0.72461,-0.3027344 -0.191923,-0.1969574 -0.291015,-0.4599183 -0.291015,-0.7363281 0,-0.2764094 0.0991,-0.5374165 0.291015,-0.734375 0.191024,-0.1960448 0.449335,-0.3046875 0.72461,-0.3046875 z m -1.75,1.8066406 c 0.0068,0 0.0244,6.732e-4 0.05273,0.00195 0.256848,0.011652 0.49375,0.1238283 0.671875,0.3066406 0.189491,0.1948428 0.294922,0.4530828 0.294922,0.7304688 0,0.278128 -0.103894,0.538327 -0.294922,0.734375 -0.191019,0.196026 -0.449319,0.306641 -0.724609,0.306641 -0.2750313,0 -0.5324,-0.111387 -0.7226564,-0.306641 C 9.2249894,10.587155 9.1210938,10.326955 9.1210938,10.048828 9.1210937,9.7714422 9.2265406,9.5132022 9.4160156,9.3183594 9.6062729,9.1231217 9.8636398,9.0097656 10.138672,9.0097656 Z m -8.216797,0.1875 c -0.2536975,-9.4e-6 -0.478759,0.0979 -0.6796875,0.2988282 -0.2009286,0.2004212 -0.30273437,0.4279427 -0.30273438,0.6816402 0,0.264353 0.10180578,0.498291 0.30273438,0.699219 0.2009285,0.200929 0.42599,0.300781 0.6796875,0.300781 H 6.8867188 V 9.5488281 c 0,-0.1182133 0.017785,-0.2363441 0.048828,-0.3515625 z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

@ -1,60 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="14.874624"
width="16.583984"
version="1.1"
id="svg5894"
sodipodi:docname="sequence_reactive.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
height="14.874624"
width="16.583984"
version="1.1"
id="svg5894"
sodipodi:docname="sequence_reactive.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs5898"><linearGradient
id="linearGradient3589"
inkscape:swatch="solid"><stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop3587" /></linearGradient><inkscape:path-effect
effect="fill_between_many"
method="originald"
linkedpaths="#path5892,0,1|#path3531,0,1"
id="path-effect3720"
is_visible="true"
lpeversion="0"
join="true"
close="true"
autoreverse="true" /></defs><sodipodi:namedview
id="namedview5896"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="21.599999"
inkscape:cx="16.805556"
inkscape:cy="4.3287039"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5894" /><path
d="M 0,0"
id="path3722"
class="UnoptimicedTransforms"
inkscape:original-d="M 0,0"
inkscape:path-effect="#path-effect3720"
transform="translate(-1.7285156,-3.375)" /><path
id="path5892"
style="fill:#40d29f;fill-opacity:1"
class="UnoptimicedTransforms"
d="m 3.4375,3.375 c -0.4726662,0 -0.8756514,0.166667 -1.2089844,0.5 -0.333333,0.333333 -0.5,0.7350317 -0.5,1.2070312 0,0.4859996 0.166667,0.8940368 0.5,1.2207032 0.333333,0.3266663 0.7363182,0.4902344 1.2089844,0.4902344 0.4719995,-10e-8 0.8756514,-0.1635681 1.2089844,-0.4902344 0.333333,-0.3266664 0.5,-0.7272192 0.5,-1.1992188 0,-0.4859995 -0.166667,-0.8951826 -0.5,-1.2285156 C 4.3131514,3.541667 3.9094995,3.375 3.4375,3.375 Z M 8.0429688,3.7929688 C 7.7096358,3.7929687 7.4104841,3.9235003 7.1464844,4.1875 6.8824846,4.4508331 6.75,4.7486983 6.75,5.0820312 6.75,5.4293642 6.8824846,5.7360003 7.1464844,6 7.4104841,6.2639997 7.7096358,6.3964844 8.0429688,6.3964844 H 17.021484 C 17.354817,6.3964844 17.653969,6.2639997 17.917969,6 18.181302,5.7360003 18.3125,5.4293642 18.3125,5.0820312 18.3125,4.7486983 18.181302,4.4508331 17.917969,4.1875 17.653969,3.9235003 17.354817,3.7929688 17.021484,3.7929688 Z M 3.4375,8.2714844 c -0.4726662,0 -0.8756514,0.166667 -1.2089844,0.5 -0.333333,0.333333 -0.5,0.7350317 -0.5,1.2070312 0,0.4859994 0.166667,0.8920834 0.5,1.2187504 0.333333,0.326666 0.7363182,0.490234 1.2089844,0.490234 0.4719995,0 0.8756514,-0.163568 1.2089844,-0.490234 0.333333,-0.326667 0.5,-0.725266 0.5,-1.197266 0,-0.4859995 -0.166667,-0.8951826 -0.5,-1.2285156 -0.333333,-0.333333 -0.7369849,-0.5 -1.2089844,-0.5 z M 8.0429688,8.6875 c -0.333333,0 -0.6324847,0.1311982 -0.8964844,0.3945312 C 6.8824846,9.346031 6.75,9.6451826 6.75,9.9785156 c 0,0.3473334 0.1324846,0.6539694 0.3964844,0.9179684 0.2639997,0.264 0.5631514,0.396485 0.8964844,0.396485 H 12.582031 C 12.667811,11.029759 12.810428,10.78417 13,10.615234 c 0.314158,-0.280616 0.764135,-0.439453 1.171875,-0.439453 h 0.46875 c 0.407741,0 0.855765,0.158985 1.169922,0.439453 0.189414,0.168797 0.331535,0.414214 0.416015,0.677735 h 0.794922 c 0.333333,0 0.632485,-0.132485 0.896485,-0.396485 0.263333,-0.263999 0.394531,-0.570635 0.394531,-0.9179684 0,-0.333333 -0.131198,-0.6324846 -0.394531,-0.8964844 C 17.653969,8.8186982 17.354817,8.6875 17.021484,8.6875 Z M 3.4375,13.146484 c -0.4726662,0 -0.8756514,0.166667 -1.2089844,0.5 -0.333333,0.333333 -0.5,0.735032 -0.5,1.207032 0,0.485999 0.166667,0.892083 0.5,1.21875 0.333333,0.325999 0.7363182,0.490234 1.2089844,0.490234 0.4719995,0 0.8756514,-0.164235 1.2089844,-0.490234 0.333333,-0.326667 0.5,-0.725266 0.5,-1.197266 0,-0.486 -0.166667,-0.895183 -0.5,-1.228516 -0.333333,-0.333333 -0.7369849,-0.5 -1.2089844,-0.5 z M 8.0429688,13.5625 c -0.333333,0 -0.6324847,0.130532 -0.8964844,0.394531 C 6.8824846,14.221031 6.75,14.520183 6.75,14.853516 c 0,0.347333 0.1324846,0.653969 0.3964844,0.917968 0.2639997,0.264 0.5631514,0.396485 0.8964844,0.396485 H 10.03125 C 9.985538,15.92966 9.986027,15.678358 10.04492,15.455078 10.12589,15.148084 10.389523,14.941651 10.626951,14.732422 10.380403,14.52554 10.114609,14.310259 10.025389,13.998047 9.986266,13.860916 9.972079,13.712745 9.974607,13.5625 Z m 10.2500002,1.083984 c -0.03323,0.02743 -0.06681,0.05476 -0.09961,0.08203 0.03855,0.03356 0.07792,0.06815 0.117188,0.101562 -0.0012,-0.06277 -0.007,-0.123307 -0.01758,-0.183594 z"
transform="translate(-1.7285156,-3.375)" /><path
style="color:#000000;fill:#40d29f;stroke-width:2;paint-order:stroke markers fill;fill-opacity:1"
d="m 12.442996,14.874619 q -0.295876,0 -0.505045,-0.186837 -0.209662,-0.186838 -0.209662,-0.451125 v -1.440446 l -1.16293,0.620023 q -0.265504,0.127774 -0.541296,0.06389 -0.2753016,-0.06389 -0.4183406,-0.282664 l -0.244932,-0.382867 q -0.142553,-0.227969 -0.07642,-0.47869 0.06613,-0.250722 0.321348,-0.387241 L 10.779424,11.33783 9.5856344,10.736185 q -0.255217,-0.136961 -0.326737,-0.38724 -0.07153,-0.250722 0.07153,-0.478691 l 0.265013,-0.373676 q 0.153328,-0.227967 0.4340186,-0.296228 0.28069,-0.0687 0.52562,0.06827 l 1.173216,0.610833 V 8.439007 q 0,-0.264287 0.209662,-0.451125 0.209169,-0.186837 0.505047,-0.186837 h 0.469287 q 0.295874,0 0.505047,0.186837 0.209659,0.186838 0.209659,0.451125 v 1.440446 l 1.173216,-0.620024 q 0.255218,-0.127774 0.525622,-0.05908 0.270404,0.06827 0.423731,0.277851 l 0.285589,0.401242 q 0.153327,0.227969 0.07642,0.478254 -0.07642,0.25072 -0.341923,0.387677 l -1.183505,0.592455 1.173707,0.610833 q 0.24493,0.118573 0.331635,0.378052 0.08671,0.259911 -0.06662,0.478692 l -0.265012,0.392052 q -0.153326,0.21878 -0.423729,0.291853 -0.270405,0.07263 -0.525621,-0.06389 l -1.183504,-0.629204 v 1.440446 q 0,0.264286 -0.20966,0.451125 -0.209172,0.186837 -0.505048,0.186837 z"
id="path6912" /></svg>
effect="fill_between_many"
method="originald"
linkedpaths="#path5892,0,1|#path3531,0,1"
id="path-effect3720"
is_visible="true"
lpeversion="0"
join="true"
close="true"
autoreverse="true"/></defs>
<sodipodi:namedview
id="namedview5896"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="21.599999"
inkscape:cx="16.805556"
inkscape:cy="4.3287039"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5894"/>
<path
d="M 0,0"
id="path3722"
class="UnoptimicedTransforms"
inkscape:original-d="M 0,0"
inkscape:path-effect="#path-effect3720"
transform="translate(-1.7285156,-3.375)"/>
<path
id="path5892"
style="fill:#40d29f;fill-opacity:1"
class="UnoptimicedTransforms"
d="m 3.4375,3.375 c -0.4726662,0 -0.8756514,0.166667 -1.2089844,0.5 -0.333333,0.333333 -0.5,0.7350317 -0.5,1.2070312 0,0.4859996 0.166667,0.8940368 0.5,1.2207032 0.333333,0.3266663 0.7363182,0.4902344 1.2089844,0.4902344 0.4719995,-10e-8 0.8756514,-0.1635681 1.2089844,-0.4902344 0.333333,-0.3266664 0.5,-0.7272192 0.5,-1.1992188 0,-0.4859995 -0.166667,-0.8951826 -0.5,-1.2285156 C 4.3131514,3.541667 3.9094995,3.375 3.4375,3.375 Z M 8.0429688,3.7929688 C 7.7096358,3.7929687 7.4104841,3.9235003 7.1464844,4.1875 6.8824846,4.4508331 6.75,4.7486983 6.75,5.0820312 6.75,5.4293642 6.8824846,5.7360003 7.1464844,6 7.4104841,6.2639997 7.7096358,6.3964844 8.0429688,6.3964844 H 17.021484 C 17.354817,6.3964844 17.653969,6.2639997 17.917969,6 18.181302,5.7360003 18.3125,5.4293642 18.3125,5.0820312 18.3125,4.7486983 18.181302,4.4508331 17.917969,4.1875 17.653969,3.9235003 17.354817,3.7929688 17.021484,3.7929688 Z M 3.4375,8.2714844 c -0.4726662,0 -0.8756514,0.166667 -1.2089844,0.5 -0.333333,0.333333 -0.5,0.7350317 -0.5,1.2070312 0,0.4859994 0.166667,0.8920834 0.5,1.2187504 0.333333,0.326666 0.7363182,0.490234 1.2089844,0.490234 0.4719995,0 0.8756514,-0.163568 1.2089844,-0.490234 0.333333,-0.326667 0.5,-0.725266 0.5,-1.197266 0,-0.4859995 -0.166667,-0.8951826 -0.5,-1.2285156 -0.333333,-0.333333 -0.7369849,-0.5 -1.2089844,-0.5 z M 8.0429688,8.6875 c -0.333333,0 -0.6324847,0.1311982 -0.8964844,0.3945312 C 6.8824846,9.346031 6.75,9.6451826 6.75,9.9785156 c 0,0.3473334 0.1324846,0.6539694 0.3964844,0.9179684 0.2639997,0.264 0.5631514,0.396485 0.8964844,0.396485 H 12.582031 C 12.667811,11.029759 12.810428,10.78417 13,10.615234 c 0.314158,-0.280616 0.764135,-0.439453 1.171875,-0.439453 h 0.46875 c 0.407741,0 0.855765,0.158985 1.169922,0.439453 0.189414,0.168797 0.331535,0.414214 0.416015,0.677735 h 0.794922 c 0.333333,0 0.632485,-0.132485 0.896485,-0.396485 0.263333,-0.263999 0.394531,-0.570635 0.394531,-0.9179684 0,-0.333333 -0.131198,-0.6324846 -0.394531,-0.8964844 C 17.653969,8.8186982 17.354817,8.6875 17.021484,8.6875 Z M 3.4375,13.146484 c -0.4726662,0 -0.8756514,0.166667 -1.2089844,0.5 -0.333333,0.333333 -0.5,0.735032 -0.5,1.207032 0,0.485999 0.166667,0.892083 0.5,1.21875 0.333333,0.325999 0.7363182,0.490234 1.2089844,0.490234 0.4719995,0 0.8756514,-0.164235 1.2089844,-0.490234 0.333333,-0.326667 0.5,-0.725266 0.5,-1.197266 0,-0.486 -0.166667,-0.895183 -0.5,-1.228516 -0.333333,-0.333333 -0.7369849,-0.5 -1.2089844,-0.5 z M 8.0429688,13.5625 c -0.333333,0 -0.6324847,0.130532 -0.8964844,0.394531 C 6.8824846,14.221031 6.75,14.520183 6.75,14.853516 c 0,0.347333 0.1324846,0.653969 0.3964844,0.917968 0.2639997,0.264 0.5631514,0.396485 0.8964844,0.396485 H 10.03125 C 9.985538,15.92966 9.986027,15.678358 10.04492,15.455078 10.12589,15.148084 10.389523,14.941651 10.626951,14.732422 10.380403,14.52554 10.114609,14.310259 10.025389,13.998047 9.986266,13.860916 9.972079,13.712745 9.974607,13.5625 Z m 10.2500002,1.083984 c -0.03323,0.02743 -0.06681,0.05476 -0.09961,0.08203 0.03855,0.03356 0.07792,0.06815 0.117188,0.101562 -0.0012,-0.06277 -0.007,-0.123307 -0.01758,-0.183594 z"
transform="translate(-1.7285156,-3.375)"/>
<path
style="color:#000000;fill:#40d29f;stroke-width:2;paint-order:stroke markers fill;fill-opacity:1"
d="m 12.442996,14.874619 q -0.295876,0 -0.505045,-0.186837 -0.209662,-0.186838 -0.209662,-0.451125 v -1.440446 l -1.16293,0.620023 q -0.265504,0.127774 -0.541296,0.06389 -0.2753016,-0.06389 -0.4183406,-0.282664 l -0.244932,-0.382867 q -0.142553,-0.227969 -0.07642,-0.47869 0.06613,-0.250722 0.321348,-0.387241 L 10.779424,11.33783 9.5856344,10.736185 q -0.255217,-0.136961 -0.326737,-0.38724 -0.07153,-0.250722 0.07153,-0.478691 l 0.265013,-0.373676 q 0.153328,-0.227967 0.4340186,-0.296228 0.28069,-0.0687 0.52562,0.06827 l 1.173216,0.610833 V 8.439007 q 0,-0.264287 0.209662,-0.451125 0.209169,-0.186837 0.505047,-0.186837 h 0.469287 q 0.295874,0 0.505047,0.186837 0.209659,0.186838 0.209659,0.451125 v 1.440446 l 1.173216,-0.620024 q 0.255218,-0.127774 0.525622,-0.05908 0.270404,0.06827 0.423731,0.277851 l 0.285589,0.401242 q 0.153327,0.227969 0.07642,0.478254 -0.07642,0.25072 -0.341923,0.387677 l -1.183505,0.592455 1.173707,0.610833 q 0.24493,0.118573 0.331635,0.378052 0.08671,0.259911 -0.06662,0.478692 l -0.265012,0.392052 q -0.153326,0.21878 -0.423729,0.291853 -0.270405,0.07263 -0.525621,-0.06389 l -1.183504,-0.629204 v 1.440446 q 0,0.264286 -0.20966,0.451125 -0.209172,0.186837 -0.505048,0.186837 z"
id="path6912"/></svg>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="17.666"
width="17.666"
version="1.1"
id="svg7608"
sodipodi:docname="succedeer.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs7612" />
<sodipodi:namedview
id="namedview7610"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="10.8"
inkscape:cx="-0.092592592"
inkscape:cy="15.509259"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg7608" />
<path
d="M 7.666,9.479 6.479,8.271 Q 6.104,7.916 5.604,7.916 q -0.5,0 -0.875,0.375 -0.375,0.375 -0.375,0.865 0,0.489 0.375,0.865 l 2,2.02 q 0.416,0.396 0.927,0.396 0.51,0 0.906,-0.396 L 12.833,7.75 Q 13.166,7.416 13.052,6.958 12.937,6.5 12.833,6.041 12.458,5.666 11.968,5.666 q -0.489,0 -0.864,0.375 z m 1.167,8.187 q -1.854,0 -3.458,-0.687 Q 3.771,16.291 2.573,15.093 1.375,13.895 0.687,12.291 0,10.687 0,8.833 0,6.958 0.687,5.364 1.375,3.771 2.573,2.573 3.771,1.375 5.375,0.687 6.979,0 8.833,0 q 1.875,0 3.469,0.687 1.593,0.688 2.791,1.886 1.198,1.198 1.886,2.791 0.687,1.594 0.687,3.469 0,1.854 -0.687,3.458 -0.688,1.604 -1.886,2.802 -1.198,1.198 -2.791,1.886 -1.594,0.687 -3.469,0.687 z"
id="path7606"
style="fill:#46c0e1;fill-opacity:1" />
height="17.666"
width="17.666"
version="1.1"
id="svg7608"
sodipodi:docname="succedeer.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs7612"/>
<sodipodi:namedview
id="namedview7610"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="10.8"
inkscape:cx="-0.092592592"
inkscape:cy="15.509259"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg7608"/>
<path
d="M 7.666,9.479 6.479,8.271 Q 6.104,7.916 5.604,7.916 q -0.5,0 -0.875,0.375 -0.375,0.375 -0.375,0.865 0,0.489 0.375,0.865 l 2,2.02 q 0.416,0.396 0.927,0.396 0.51,0 0.906,-0.396 L 12.833,7.75 Q 13.166,7.416 13.052,6.958 12.937,6.5 12.833,6.041 12.458,5.666 11.968,5.666 q -0.489,0 -0.864,0.375 z m 1.167,8.187 q -1.854,0 -3.458,-0.687 Q 3.771,16.291 2.573,15.093 1.375,13.895 0.687,12.291 0,10.687 0,8.833 0,6.958 0.687,5.364 1.375,3.771 2.573,2.573 3.771,1.375 5.375,0.687 6.979,0 8.833,0 q 1.875,0 3.469,0.687 1.593,0.688 2.791,1.886 1.198,1.198 1.886,2.791 0.687,1.594 0.687,3.469 0,1.854 -0.687,3.458 -0.688,1.604 -1.886,2.802 -1.198,1.198 -2.791,1.886 -1.594,0.687 -3.469,0.687 z"
id="path7606"
style="fill:#46c0e1;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="15.063"
width="16.875999"
version="1.1"
id="svg12"
sodipodi:docname="tree.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs16" />
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="10.8"
inkscape:cx="-26.481481"
inkscape:cy="9.3981481"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg12" />
<path
d="m 12.709,15.063 q -0.771,0 -1.313,-0.521 -0.541,-0.521 -0.562,-1.271 V 12.604 H 9.376 q -0.792,0 -1.344,-0.552 Q 7.48,11.5 7.48,10.73 V 4.313 H 6.021 V 4.938 Q 6.021,5.709 5.469,6.23 4.917,6.75 4.146,6.75 H 1.876 Q 1.105,6.75 0.553,6.198 0,5.646 0,4.875 v -3 Q 0,1.104 0.553,0.552 1.105,0 1.876,0 h 2.27 q 0.771,0 1.323,0.521 0.552,0.521 0.552,1.271 v 0.646 h 4.813 V 1.792 q 0,-0.75 0.552,-1.271 Q 11.938,0 12.709,0 H 15 q 0.771,0 1.323,0.552 0.553,0.552 0.553,1.323 v 3 q 0,0.771 -0.553,1.323 Q 15.771,6.75 15,6.75 H 12.709 Q 11.938,6.75 11.386,6.23 10.834,5.709 10.834,4.938 V 4.313 H 9.376 v 6.417 h 1.458 v -0.626 q 0,-0.77 0.552,-1.291 0.552,-0.521 1.323,-0.521 H 15 q 0.771,0 1.323,0.552 0.553,0.552 0.553,1.323 v 3.021 q 0,0.771 -0.553,1.323 -0.552,0.552 -1.323,0.552 z"
id="path10"
style="fill:#c689ff;fill-opacity:1" />
height="15.063"
width="16.875999"
version="1.1"
id="svg12"
sodipodi:docname="tree.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs16"/>
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="10.8"
inkscape:cx="-26.481481"
inkscape:cy="9.3981481"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg12"/>
<path
d="m 12.709,15.063 q -0.771,0 -1.313,-0.521 -0.541,-0.521 -0.562,-1.271 V 12.604 H 9.376 q -0.792,0 -1.344,-0.552 Q 7.48,11.5 7.48,10.73 V 4.313 H 6.021 V 4.938 Q 6.021,5.709 5.469,6.23 4.917,6.75 4.146,6.75 H 1.876 Q 1.105,6.75 0.553,6.198 0,5.646 0,4.875 v -3 Q 0,1.104 0.553,0.552 1.105,0 1.876,0 h 2.27 q 0.771,0 1.323,0.521 0.552,0.521 0.552,1.271 v 0.646 h 4.813 V 1.792 q 0,-0.75 0.552,-1.271 Q 11.938,0 12.709,0 H 15 q 0.771,0 1.323,0.552 0.553,0.552 0.553,1.323 v 3 q 0,0.771 -0.553,1.323 Q 15.771,6.75 15,6.75 H 12.709 Q 11.938,6.75 11.386,6.23 10.834,5.709 10.834,4.938 V 4.313 H 9.376 v 6.417 h 1.458 v -0.626 q 0,-0.77 0.552,-1.291 0.552,-0.521 1.323,-0.521 H 15 q 0.771,0 1.323,0.552 0.553,0.552 0.553,1.323 v 3.021 q 0,0.771 -0.553,1.323 -0.552,0.552 -1.323,0.552 z"
id="path10"
style="fill:#c689ff;fill-opacity:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -1,54 +1,54 @@
extends Node
var _tree_count: int = 0
var _active_tree_count: int = 0
var _tree_count: int = 0
var _active_tree_count: int = 0
var _registered_trees: Array[BeehaveTree] = []
func _enter_tree() -> void:
Performance.add_custom_monitor("beehave/total_trees", _get_total_trees)
Performance.add_custom_monitor("beehave/total_enabled_trees", _get_total_enabled_trees)
Performance.add_custom_monitor("beehave/total_trees", _get_total_trees)
Performance.add_custom_monitor("beehave/total_enabled_trees", _get_total_enabled_trees)
func register_tree(tree: BeehaveTree) -> void:
if _registered_trees.has(tree):
return
_registered_trees.append(tree)
_tree_count += 1
if tree.enabled:
_active_tree_count += 1
tree.tree_enabled.connect(_on_tree_enabled)
tree.tree_disabled.connect(_on_tree_disabled)
if _registered_trees.has(tree):
return
_registered_trees.append(tree)
_tree_count += 1
if tree.enabled:
_active_tree_count += 1
tree.tree_enabled.connect(_on_tree_enabled)
tree.tree_disabled.connect(_on_tree_disabled)
func unregister_tree(tree: BeehaveTree) -> void:
if not _registered_trees.has(tree):
return
_registered_trees.erase(tree)
_tree_count -= 1
if tree.enabled:
_active_tree_count -= 1
tree.tree_enabled.disconnect(_on_tree_enabled)
tree.tree_disabled.disconnect(_on_tree_disabled)
if not _registered_trees.has(tree):
return
_registered_trees.erase(tree)
_tree_count -= 1
if tree.enabled:
_active_tree_count -= 1
tree.tree_enabled.disconnect(_on_tree_enabled)
tree.tree_disabled.disconnect(_on_tree_disabled)
func _get_total_trees() -> int:
return _tree_count
return _tree_count
func _get_total_enabled_trees() -> int:
return _active_tree_count
return _active_tree_count
func _on_tree_enabled() -> void:
_active_tree_count += 1
_active_tree_count += 1
func _on_tree_disabled() -> void:
_active_tree_count -= 1
_active_tree_count -= 1

@ -1,49 +1,50 @@
## A node in the behavior tree. Every node must return `SUCCESS`, `FAILURE` or
## `RUNNING` when ticked.
@tool
class_name BeehaveNode extends Node
class_name BeehaveNode
extends Node
enum {
SUCCESS,
FAILURE,
RUNNING
SUCCESS,
FAILURE,
RUNNING
}
func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = []
var warnings: PackedStringArray = []
if get_children().any(func(x): return not (x is BeehaveNode)):
warnings.append("All children of this node should inherit from BeehaveNode class.")
if get_children().any(func(x): return not (x is BeehaveNode)):
warnings.append("All children of this node should inherit from BeehaveNode class.")
return warnings
return warnings
## Executes this node and returns a status code.
## This method must be overwritten.
func tick(actor: Node, blackboard: Blackboard) -> int:
return SUCCESS
return SUCCESS
## Called when this node needs to be interrupted before it can return FAILURE or SUCCESS.
func interrupt(actor: Node, blackboard: Blackboard) -> void:
pass
pass
## Called before the first time it ticks by the parent.
func before_run(actor: Node, blackboard: Blackboard) -> void:
pass
pass
## Called after the last time it ticks and returns
## [code]SUCCESS[/code] or [code]FAILURE[/code].
func after_run(actor: Node, blackboard: Blackboard) -> void:
pass
pass
func get_class_name() -> Array[StringName]:
return [&"BeehaveNode"]
return [&"BeehaveNode"]
func can_send_message(blackboard: Blackboard) -> bool:
return blackboard.get_value("can_send_message", false)
return blackboard.get_value("can_send_message", false)

@ -1,224 +1,225 @@
## Controls the flow of execution of the entire behavior tree.
@tool
@icon("../icons/tree.svg")
class_name BeehaveTree extends Node
class_name BeehaveTree
extends Node
enum {
SUCCESS,
FAILURE,
RUNNING
SUCCESS,
FAILURE,
RUNNING
}
signal tree_enabled
signal tree_disabled
## Wether this behavior tree should be enabled or not.
@export var enabled: bool = true:
set(value):
enabled = value
set_physics_process(enabled)
set(value):
enabled = value
set_physics_process(enabled)
if value:
tree_enabled.emit()
else:
interrupt()
tree_disabled.emit()
if value:
tree_enabled.emit()
else:
interrupt()
tree_disabled.emit()
get:
return enabled
get:
return enabled
## An optional node path this behavior tree should apply to.
@export_node_path var actor_node_path : NodePath
@export_node_path var actor_node_path: NodePath
## Custom blackboard node. An internal blackboard will be used
## if no blackboard is provided explicitly.
@export var blackboard:Blackboard:
set(b):
blackboard = b
if blackboard and _internal_blackboard:
remove_child(_internal_blackboard)
_internal_blackboard.free()
_internal_blackboard = null
elif not blackboard and not _internal_blackboard:
_internal_blackboard = Blackboard.new()
add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK)
get:
return blackboard if blackboard else _internal_blackboard
@export var blackboard: Blackboard:
set(b):
blackboard = b
if blackboard and _internal_blackboard:
remove_child(_internal_blackboard)
_internal_blackboard.free()
_internal_blackboard = null
elif not blackboard and not _internal_blackboard:
_internal_blackboard = Blackboard.new()
add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK)
get:
return blackboard if blackboard else _internal_blackboard
## When enabled, this tree is tracked individually
## as a custom monitor.
@export var custom_monitor = false:
set(b):
custom_monitor = b
if custom_monitor and _process_time_metric_name != '':
Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value)
BeehaveGlobalMetrics.register_tree(self)
else:
if _process_time_metric_name != '':
# Remove tree metric from the engine
Performance.remove_custom_monitor(_process_time_metric_name)
BeehaveGlobalMetrics.unregister_tree(self)
BeehaveDebuggerMessages.unregister_tree(get_instance_id())
var actor : Node
var status : int = -1
set(b):
custom_monitor = b
if custom_monitor and _process_time_metric_name != '':
Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value)
BeehaveGlobalMetrics.register_tree(self)
else:
if _process_time_metric_name != '':
# Remove tree metric from the engine
Performance.remove_custom_monitor(_process_time_metric_name)
BeehaveGlobalMetrics.unregister_tree(self)
BeehaveDebuggerMessages.unregister_tree(get_instance_id())
var actor: Node
var status: int = -1
var _internal_blackboard: Blackboard
var _process_time_metric_name : String
var _process_time_metric_value : float = 0.0
var _can_send_message: bool = false
var _process_time_metric_name: String
var _process_time_metric_value: float = 0.0
var _can_send_message: bool = false
func init() -> void:
if Engine.is_editor_hint():
return
if Engine.is_editor_hint():
return
if self.get_child_count() > 0 and not self.get_child(0) is BeehaveNode:
push_warning("Beehave error: Root %s should have only one child of type BeehaveNode (NodePath: %s)" % [self.name, self.get_path()])
disable()
return
if self.get_child_count() > 0 and not self.get_child(0) is BeehaveNode:
push_warning("Beehave error: Root %s should have only one child of type BeehaveNode (NodePath: %s)" % [self.name, self.get_path()])
disable()
return
if not blackboard:
_internal_blackboard = Blackboard.new()
add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK)
if not blackboard:
_internal_blackboard = Blackboard.new()
add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK)
actor = get_parent()
if actor_node_path:
actor = get_node(actor_node_path)
actor = get_parent()
if actor_node_path:
actor = get_node(actor_node_path)
# Get the name of the parent node name for metric
var parent_name = actor.name
_process_time_metric_name = "beehave [microseconds]/process_time_%s-%s" % [parent_name, get_instance_id()]
# Get the name of the parent node name for metric
var parent_name = actor.name
_process_time_metric_name = "beehave [microseconds]/process_time_%s-%s" % [parent_name, get_instance_id()]
# Register custom metric to the engine
if custom_monitor:
Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value)
BeehaveGlobalMetrics.register_tree(self)
# Register custom metric to the engine
if custom_monitor:
Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value)
BeehaveGlobalMetrics.register_tree(self)
set_physics_process(enabled)
BeehaveGlobalDebugger.register_tree(self)
BeehaveDebuggerMessages.register_tree(_get_debugger_data(self))
set_physics_process(enabled)
BeehaveGlobalDebugger.register_tree(self)
BeehaveDebuggerMessages.register_tree(_get_debugger_data(self))
func process(delta: float) -> void:
if Engine.is_editor_hint():
return
if Engine.is_editor_hint():
return
# Start timing for metric
var start_time = Time.get_ticks_usec()
# Start timing for metric
var start_time = Time.get_ticks_usec()
blackboard.set_value("can_send_message", _can_send_message)
blackboard.set_value("can_send_message", _can_send_message)
if _can_send_message:
BeehaveDebuggerMessages.process_begin(get_instance_id())
if _can_send_message:
BeehaveDebuggerMessages.process_begin(get_instance_id())
if self.get_child_count() == 1:
tick()
if self.get_child_count() == 1:
tick()
if _can_send_message:
BeehaveDebuggerMessages.process_end(get_instance_id())
if _can_send_message:
BeehaveDebuggerMessages.process_end(get_instance_id())
# Check the cost for this frame and save it for metric report
_process_time_metric_value = Time.get_ticks_usec() - start_time
# Check the cost for this frame and save it for metric report
_process_time_metric_value = Time.get_ticks_usec() - start_time
func tick() -> int:
var child := self.get_child(0)
if status != RUNNING:
child.before_run(actor, blackboard)
var child := self.get_child(0)
if status != RUNNING:
child.before_run(actor, blackboard)
status = child.tick(actor, blackboard)
if _can_send_message:
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), status)
BeehaveDebuggerMessages.process_tick(get_instance_id(), status)
status = child.tick(actor, blackboard)
if _can_send_message:
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), status)
BeehaveDebuggerMessages.process_tick(get_instance_id(), status)
# Clear running action if nothing is running
if status != RUNNING:
blackboard.set_value("running_action", null, str(actor.get_instance_id()))
child.after_run(actor, blackboard)
# Clear running action if nothing is running
if status != RUNNING:
blackboard.set_value("running_action", null, str(actor.get_instance_id()))
child.after_run(actor, blackboard)
return status
return status
func _get_configuration_warnings() -> PackedStringArray:
var warnings:PackedStringArray = []
var warnings: PackedStringArray = []
if get_children().any(func(x): return not (x is BeehaveNode)):
warnings.append("All children of this node should inherit from BeehaveNode class.")
if get_children().any(func(x): return not (x is BeehaveNode)):
warnings.append("All children of this node should inherit from BeehaveNode class.")
if get_child_count() != 1:
warnings.append("BeehaveTree should have exactly one child node.")
if get_child_count() != 1:
warnings.append("BeehaveTree should have exactly one child node.")
return warnings
return warnings
## Returns the currently running action
func get_running_action() -> ActionLeaf:
return blackboard.get_value("running_action", null, str(actor.get_instance_id()))
return blackboard.get_value("running_action", null, str(actor.get_instance_id()))
## Returns the last condition that was executed
func get_last_condition() -> ConditionLeaf:
return blackboard.get_value("last_condition", null, str(actor.get_instance_id()))
return blackboard.get_value("last_condition", null, str(actor.get_instance_id()))
## Returns the status of the last executed condition
func get_last_condition_status() -> String:
if blackboard.has_value("last_condition_status", str(actor.get_instance_id())):
var status = blackboard.get_value("last_condition_status", null, str(actor.get_instance_id()))
if status == SUCCESS:
return "SUCCESS"
elif status == FAILURE:
return "FAILURE"
else:
return "RUNNING"
return ""
if blackboard.has_value("last_condition_status", str(actor.get_instance_id())):
var status = blackboard.get_value("last_condition_status", null, str(actor.get_instance_id()))
if status == SUCCESS:
return "SUCCESS"
elif status == FAILURE:
return "FAILURE"
else:
return "RUNNING"
return ""
## interrupts this tree if anything was running
func interrupt() -> void:
if self.get_child_count() != 0:
var first_child = self.get_child(0)
if "interrupt" in first_child:
first_child.interrupt(actor, blackboard)
if self.get_child_count() != 0:
var first_child = self.get_child(0)
if "interrupt" in first_child:
first_child.interrupt(actor, blackboard)
## Enables this tree.
func enable() -> void:
self.enabled = true
self.enabled = true
## Disables this tree.
func disable() -> void:
self.enabled = false
self.enabled = false
func _exit_tree() -> void:
if custom_monitor:
if _process_time_metric_name != '':
# Remove tree metric from the engine
Performance.remove_custom_monitor(_process_time_metric_name)
BeehaveGlobalMetrics.unregister_tree(self)
if custom_monitor:
if _process_time_metric_name != '':
# Remove tree metric from the engine
Performance.remove_custom_monitor(_process_time_metric_name)
BeehaveGlobalMetrics.unregister_tree(self)
BeehaveDebuggerMessages.unregister_tree(get_instance_id())
BeehaveDebuggerMessages.unregister_tree(get_instance_id())
# Called by the engine to profile this tree
func _get_process_time_metric_value() -> int:
return _process_time_metric_value
return _process_time_metric_value
func _get_debugger_data(node: Node) -> Dictionary:
if not node is BeehaveTree and not node is BeehaveNode:
return {}
var data := { path = node.get_path(), name = node.name, type = node.get_class_name(), id = str(node.get_instance_id()) }
if node.get_child_count() > 0:
data.children = []
for child in node.get_children():
var child_data := _get_debugger_data(child)
if not child_data.is_empty():
data.children.push_back(child_data)
return data
if not node is BeehaveTree and not node is BeehaveNode:
return {}
var data := { path = node.get_path(), name = node.name, type = node.get_class_name(), id = str(node.get_instance_id()) }
if node.get_child_count() > 0:
data.children = []
for child in node.get_children():
var child_data := _get_debugger_data(child)
if not child_data.is_empty():
data.children.push_back(child_data)
return data
func get_class_name() -> Array[StringName]:
return [&"BeehaveTree"]
return [&"BeehaveTree"]

@ -1,40 +1,40 @@
## A Composite node controls the flow of execution of its children in a specific manner.
@tool
@icon("../../icons/category_composite.svg")
class_name Composite extends BeehaveNode
class_name Composite
extends BeehaveNode
var running_child: BeehaveNode = null
func _ready():
if Engine.is_editor_hint():
return
if Engine.is_editor_hint():
return
if self.get_child_count() < 1:
push_warning("BehaviorTree Error: Composite %s should have at least one child (NodePath: %s)" % [self.name, self.get_path()])
if self.get_child_count() < 1:
push_warning("BehaviorTree Error: Composite %s should have at least one child (NodePath: %s)" % [self.name, self.get_path()])
func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = super._get_configuration_warnings()
var warnings: PackedStringArray = super._get_configuration_warnings()
if get_children().filter(func(x): return x is BeehaveNode).size() < 2:
warnings.append("Any composite node should have at least two children. Otherwise it is not useful.")
if get_children().filter(func(x): return x is BeehaveNode).size() < 2:
warnings.append("Any composite node should have at least two children. Otherwise it is not useful.")
return warnings
return warnings
func interrupt(actor: Node, blackboard: Blackboard) -> void:
if running_child != null:
running_child.interrupt(actor, blackboard)
running_child = null
if running_child != null:
running_child.interrupt(actor, blackboard)
running_child = null
func after_run(actor: Node, blackboard: Blackboard) -> void:
running_child = null
running_child = null
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"Composite")
return classes
var classes := super()
classes.push_back(&"Composite")
return classes

@ -1,152 +1,153 @@
@tool
class_name RandomizedComposite extends Composite
class_name RandomizedComposite
extends Composite
const WEIGHTS_PREFIX = "Weights/"
## Sets a predicable seed
@export var random_seed: int = 0:
set(rs):
random_seed = rs
if random_seed != 0:
seed(random_seed)
else:
randomize()
set(rs):
random_seed = rs
if random_seed != 0:
seed(random_seed)
else:
randomize()
## Wether to use weights for every child or not.
@export var use_weights: bool:
set(value):
use_weights = value
if use_weights:
_update_weights(get_children())
_connect_children_changing_signals()
notify_property_list_changed()
set(value):
use_weights = value
if use_weights:
_update_weights(get_children())
_connect_children_changing_signals()
notify_property_list_changed()
var _weights: Dictionary
func _ready():
_connect_children_changing_signals()
_connect_children_changing_signals()
func _connect_children_changing_signals():
if not child_entered_tree.is_connected(_on_child_entered_tree):
child_entered_tree.connect(_on_child_entered_tree)
if not child_exiting_tree.is_connected(_on_child_exiting_tree):
child_exiting_tree.connect(_on_child_exiting_tree)
if not child_entered_tree.is_connected(_on_child_entered_tree):
child_entered_tree.connect(_on_child_entered_tree)
if not child_exiting_tree.is_connected(_on_child_exiting_tree):
child_exiting_tree.connect(_on_child_exiting_tree)
func get_shuffled_children() -> Array[Node]:
var children_bag: Array[Node] = get_children().duplicate()
if use_weights:
var weights: Array[int]
weights.assign(children_bag.map(func (child): return _weights[child.name]))
children_bag.assign(_weighted_shuffle(children_bag, weights))
else:
children_bag.shuffle()
return children_bag
var children_bag: Array[Node] = get_children().duplicate()
if use_weights:
var weights: Array[int]
weights.assign(children_bag.map(func (child): return _weights[child.name]))
children_bag.assign(_weighted_shuffle(children_bag, weights))
else:
children_bag.shuffle()
return children_bag
## Returns a shuffled version of a given array using the supplied array of weights.
## Think of weights as the chance of a given item being the first in the array.
func _weighted_shuffle(items: Array, weights: Array[int]) -> Array:
if len(items) != len(weights):
push_error("items and weights size mismatch: expected %d weights, got %d instead." % [len(items), len(weights)])
return items
# This method is based on the weighted random sampling algorithm
# by Efraimidis, Spirakis; 2005. This runs in O(n log(n)).
# For each index, it will calculate random_value^(1/weight).
var chance_calc = func(i): return [i, randf() ** (1.0 / weights[i])]
var random_distribuition = range(len(items)).map(chance_calc)
# Now we just have to order by the calculated value, descending.
random_distribuition.sort_custom(func(a, b): return a[1] > b[1])
return random_distribuition.map(func(dist): return items[dist[0]])
if len(items) != len(weights):
push_error("items and weights size mismatch: expected %d weights, got %d instead." % [len(items), len(weights)])
return items
# This method is based on the weighted random sampling algorithm
# by Efraimidis, Spirakis; 2005. This runs in O(n log(n)).
# For each index, it will calculate random_value^(1/weight).
var chance_calc = func(i): return [i, randf() ** (1.0 / weights[i])]
var random_distribuition = range(len(items)).map(chance_calc)
# Now we just have to order by the calculated value, descending.
random_distribuition.sort_custom(func(a, b): return a[1] > b[1])
return random_distribuition.map(func(dist): return items[dist[0]])
func _get_property_list():
var properties = []
if use_weights:
for key in _weights.keys():
properties.append({
"name": WEIGHTS_PREFIX + key,
"type": TYPE_INT,
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "1,100"
})
return properties
var properties = []
if use_weights:
for key in _weights.keys():
properties.append({
"name": WEIGHTS_PREFIX + key,
"type": TYPE_INT,
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "1,100"
})
return properties
func _set(property: StringName, value: Variant) -> bool:
if property.begins_with(WEIGHTS_PREFIX):
var weight_name = property.trim_prefix(WEIGHTS_PREFIX)
_weights[weight_name] = value
return true
return false
if property.begins_with(WEIGHTS_PREFIX):
var weight_name = property.trim_prefix(WEIGHTS_PREFIX)
_weights[weight_name] = value
return true
return false
func _get(property: StringName):
if property.begins_with(WEIGHTS_PREFIX):
var weight_name = property.trim_prefix(WEIGHTS_PREFIX)
return _weights[weight_name]
return null
if property.begins_with(WEIGHTS_PREFIX):
var weight_name = property.trim_prefix(WEIGHTS_PREFIX)
return _weights[weight_name]
return null
func _update_weights(children: Array[Node]) -> void:
var new_weights = {}
for c in children:
if _weights.has(c.name):
new_weights[c.name] = _weights[c.name]
else:
new_weights[c.name] = 1
_weights = new_weights
notify_property_list_changed()
var new_weights = {}
for c in children:
if _weights.has(c.name):
new_weights[c.name] = _weights[c.name]
else:
new_weights[c.name] = 1
_weights = new_weights
notify_property_list_changed()
func _on_child_entered_tree(node: Node):
_update_weights(get_children())
_update_weights(get_children())
var renamed_callable = _on_child_renamed.bind(node.name, node)
if not node.renamed.is_connected(renamed_callable):
node.renamed.connect(renamed_callable)
var renamed_callable = _on_child_renamed.bind(node.name, node)
if not node.renamed.is_connected(renamed_callable):
node.renamed.connect(renamed_callable)
func _on_child_exiting_tree(node: Node):
var renamed_callable = _on_child_renamed.bind(node.name, node)
if node.renamed.is_connected(renamed_callable):
node.renamed.disconnect(renamed_callable)
var children = get_children()
children.erase(node)
_update_weights(children)
var renamed_callable = _on_child_renamed.bind(node.name, node)
if node.renamed.is_connected(renamed_callable):
node.renamed.disconnect(renamed_callable)
var children = get_children()
children.erase(node)
_update_weights(children)
func _on_child_renamed(old_name: String, renamed_child: Node):
if old_name == renamed_child.name:
return # No need to update the weights.
# Disconnect signal with old name...
renamed_child.renamed\
.disconnect(_on_child_renamed.bind(old_name, renamed_child))
# ...and connect with the new name.
renamed_child.renamed\
.connect(_on_child_renamed.bind(renamed_child.name, renamed_child))
var original_weight = _weights[old_name]
_weights.erase(old_name)
_weights[renamed_child.name] = original_weight
notify_property_list_changed()
if old_name == renamed_child.name:
return # No need to update the weights.
# Disconnect signal with old name...
renamed_child.renamed\
.disconnect(_on_child_renamed.bind(old_name, renamed_child))
# ...and connect with the new name.
renamed_child.renamed\
.connect(_on_child_renamed.bind(renamed_child.name, renamed_child))
var original_weight = _weights[old_name]
_weights.erase(old_name)
_weights[renamed_child.name] = original_weight
notify_property_list_changed()
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"RandomizedComposite")
return classes
var classes := super()
classes.push_back(&"RandomizedComposite")
return classes

@ -4,66 +4,66 @@
## If a child returns `RUNNING` it will tick again.
@tool
@icon("../../icons/selector.svg")
class_name SelectorComposite extends Composite
class_name SelectorComposite
extends Composite
var last_execution_index: int = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c.get_index() < last_execution_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_cleanup_running_task(c, actor, blackboard)
c.after_run(actor, blackboard)
return SUCCESS
FAILURE:
_cleanup_running_task(c, actor, blackboard)
last_execution_index += 1
c.after_run(actor, blackboard)
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
return FAILURE
for c in get_children():
if c.get_index() < last_execution_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_cleanup_running_task(c, actor, blackboard)
c.after_run(actor, blackboard)
return SUCCESS
FAILURE:
_cleanup_running_task(c, actor, blackboard)
last_execution_index += 1
c.after_run(actor, blackboard)
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
return FAILURE
func after_run(actor: Node, blackboard: Blackboard) -> void:
last_execution_index = 0
super(actor, blackboard)
last_execution_index = 0
super(actor, blackboard)
func interrupt(actor: Node, blackboard: Blackboard) -> void:
last_execution_index = 0
super(actor, blackboard)
last_execution_index = 0
super(actor, blackboard)
## Changes `running_action` and `running_child` after the node finishes executing.
func _cleanup_running_task(finished_action: Node, actor: Node, blackboard: Blackboard):
var blackboard_name = str(actor.get_instance_id())
if finished_action == running_child:
running_child = null
if finished_action == blackboard.get_value("running_action", null, blackboard_name):
blackboard.set_value("running_action", null, blackboard_name)
var blackboard_name = str(actor.get_instance_id())
if finished_action == running_child:
running_child = null
if finished_action == blackboard.get_value("running_action", null, blackboard_name):
blackboard.set_value("running_action", null, blackboard_name)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SelectorComposite")
return classes
var classes := super()
classes.push_back(&"SelectorComposite")
return classes

@ -3,78 +3,80 @@
## will be executed in a random order.
@tool
@icon("../../icons/selector_random.svg")
class_name SelectorRandomComposite extends RandomizedComposite
class_name SelectorRandomComposite
extends RandomizedComposite
## A shuffled list of the children that will be executed in reverse order.
var _children_bag: Array[Node] = []
var c: Node
func _ready() -> void:
super()
if random_seed == 0:
randomize()
super()
if random_seed == 0:
randomize()
func tick(actor: Node, blackboard: Blackboard) -> int:
if _children_bag.is_empty():
_reset()
if _children_bag.is_empty():
_reset()
# We need to traverse the array in reverse since we will be manipulating it.
for i in _get_reversed_indexes():
c = _children_bag[i]
# We need to traverse the array in reverse since we will be manipulating it.
for i in _get_reversed_indexes():
c = _children_bag[i]
if c != running_child:
c.before_run(actor, blackboard)
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_children_bag.erase(c)
c.after_run(actor, blackboard)
return SUCCESS
FAILURE:
_children_bag.erase(c)
c.after_run(actor, blackboard)
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
match response:
SUCCESS:
_children_bag.erase(c)
c.after_run(actor, blackboard)
return SUCCESS
FAILURE:
_children_bag.erase(c)
c.after_run(actor, blackboard)
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
return FAILURE
return FAILURE
func after_run(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
_reset()
super(actor, blackboard)
func interrupt(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
_reset()
super(actor, blackboard)
func _get_reversed_indexes() -> Array[int]:
var reversed: Array[int]
reversed.assign(range(_children_bag.size()))
reversed.reverse()
return reversed
var reversed: Array[int]
reversed.assign(range(_children_bag.size()))
reversed.reverse()
return reversed
func _reset() -> void:
var new_order = get_shuffled_children()
_children_bag = new_order.duplicate()
_children_bag.reverse() # It needs to run the children in reverse order.
var new_order = get_shuffled_children()
_children_bag = new_order.duplicate()
_children_bag.reverse() # It needs to run the children in reverse order.
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SelectorRandomComposite")
return classes
var classes := super()
classes.push_back(&"SelectorRandomComposite")
return classes

@ -4,42 +4,43 @@
## If a child returns `RUNNING` it will restart.
@tool
@icon("../../icons/selector_reactive.svg")
class_name SelectorReactiveComposite extends Composite
class_name SelectorReactiveComposite
extends Composite
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c != running_child:
c.before_run(actor, blackboard)
for c in get_children():
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
# Interrupt any child that was RUNNING before.
if c != running_child:
interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return SUCCESS
FAILURE:
c.after_run(actor, blackboard)
RUNNING:
if c != running_child:
interrupt(actor, blackboard)
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
match response:
SUCCESS:
# Interrupt any child that was RUNNING before.
if c != running_child:
interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return SUCCESS
FAILURE:
c.after_run(actor, blackboard)
RUNNING:
if c != running_child:
interrupt(actor, blackboard)
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
return FAILURE
return FAILURE
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SelectorReactiveComposite")
return classes
var classes := super()
classes.push_back(&"SelectorReactiveComposite")
return classes

@ -5,72 +5,72 @@
## In case a child returns `RUNNING` this node will tick again.
@tool
@icon("../../icons/sequence.svg")
class_name SequenceComposite extends Composite
class_name SequenceComposite
extends Composite
var successful_index: int = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c.get_index() < successful_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_cleanup_running_task(c, actor, blackboard)
successful_index += 1
c.after_run(actor, blackboard)
FAILURE:
_cleanup_running_task(c, actor, blackboard)
# Interrupt any child that was RUNNING before.
interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
if c != running_child:
if running_child != null:
running_child.interrupt(actor, blackboard)
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_reset()
return SUCCESS
for c in get_children():
if c.get_index() < successful_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_cleanup_running_task(c, actor, blackboard)
successful_index += 1
c.after_run(actor, blackboard)
FAILURE:
_cleanup_running_task(c, actor, blackboard)
# Interrupt any child that was RUNNING before.
interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
if c != running_child:
if running_child != null:
running_child.interrupt(actor, blackboard)
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_reset()
return SUCCESS
func interrupt(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
_reset()
super(actor, blackboard)
func _reset() -> void:
successful_index = 0
successful_index = 0
## Changes `running_action` and `running_child` after the node finishes executing.
func _cleanup_running_task(finished_action: Node, actor: Node, blackboard: Blackboard):
var blackboard_name = str(actor.get_instance_id())
if finished_action == running_child:
running_child = null
if finished_action == blackboard.get_value("running_action", null, blackboard_name):
blackboard.set_value("running_action", null, blackboard_name)
var blackboard_name = str(actor.get_instance_id())
if finished_action == running_child:
running_child = null
if finished_action == blackboard.get_value("running_action", null, blackboard_name):
blackboard.set_value("running_action", null, blackboard_name)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SequenceComposite")
return classes
var classes := super()
classes.push_back(&"SequenceComposite")
return classes

@ -3,11 +3,11 @@
## will be executed in a random order.
@tool
@icon("../../icons/sequence_random.svg")
class_name SequenceRandomComposite extends RandomizedComposite
class_name SequenceRandomComposite
extends RandomizedComposite
# Emitted whenever the children are shuffled.
signal reset(new_order: Array[Node])
## Whether the sequence should start where it left off after a previous failure.
@export var resume_on_failure: bool = false
## Whether the sequence should start where it left off after a previous interruption.
@ -19,74 +19,74 @@ var c: Node
func _ready() -> void:
super()
if random_seed == 0:
randomize()
super()
if random_seed == 0:
randomize()
func tick(actor: Node, blackboard: Blackboard) -> int:
if _children_bag.is_empty():
_reset()
if _children_bag.is_empty():
_reset()
# We need to traverse the array in reverse since we will be manipulating it.
for i in _get_reversed_indexes():
c = _children_bag[i]
# We need to traverse the array in reverse since we will be manipulating it.
for i in _get_reversed_indexes():
c = _children_bag[i]
if c != running_child:
c.before_run(actor, blackboard)
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_children_bag.erase(c)
c.after_run(actor, blackboard)
FAILURE:
_children_bag.erase(c)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
match response:
SUCCESS:
_children_bag.erase(c)
c.after_run(actor, blackboard)
FAILURE:
_children_bag.erase(c)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
return SUCCESS
return SUCCESS
func after_run(actor: Node, blackboard: Blackboard) -> void:
if not resume_on_failure:
_reset()
super(actor, blackboard)
if not resume_on_failure:
_reset()
super(actor, blackboard)
func interrupt(actor: Node, blackboard: Blackboard) -> void:
if not resume_on_interrupt:
_reset()
super(actor, blackboard)
if not resume_on_interrupt:
_reset()
super(actor, blackboard)
func _get_reversed_indexes() -> Array[int]:
var reversed: Array[int]
reversed.assign(range(_children_bag.size()))
reversed.reverse()
return reversed
var reversed: Array[int]
reversed.assign(range(_children_bag.size()))
reversed.reverse()
return reversed
func _reset() -> void:
var new_order = get_shuffled_children()
_children_bag = new_order.duplicate()
_children_bag.reverse() # It needs to run the children in reverse order.
reset.emit(new_order)
var new_order = get_shuffled_children()
_children_bag = new_order.duplicate()
_children_bag.reverse() # It needs to run the children in reverse order.
reset.emit(new_order)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SequenceRandomComposite")
return classes
var classes := super()
classes.push_back(&"SequenceRandomComposite")
return classes

@ -5,58 +5,59 @@
## In case a child returns `RUNNING` this node will restart.
@tool
@icon("../../icons/sequence_reactive.svg")
class_name SequenceReactiveComposite extends Composite
class_name SequenceReactiveComposite
extends Composite
var successful_index: int = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c.get_index() < successful_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
successful_index += 1
c.after_run(actor, blackboard)
FAILURE:
# Interrupt any child that was RUNNING before.
interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
_reset()
if running_child != c:
interrupt(actor, blackboard)
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_reset()
return SUCCESS
for c in get_children():
if c.get_index() < successful_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
successful_index += 1
c.after_run(actor, blackboard)
FAILURE:
# Interrupt any child that was RUNNING before.
interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
_reset()
if running_child != c:
interrupt(actor, blackboard)
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_reset()
return SUCCESS
func interrupt(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
_reset()
super(actor, blackboard)
func _reset() -> void:
successful_index = 0
successful_index = 0
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SequenceReactiveComposite")
return classes
var classes := super()
classes.push_back(&"SequenceReactiveComposite")
return classes

@ -5,54 +5,54 @@
## In case a child returns `RUNNING` this node will restart.
@tool
@icon("../../icons/sequence_reactive.svg")
class_name SequenceStarComposite extends Composite
class_name SequenceStarComposite
extends Composite
var successful_index: int = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c.get_index() < successful_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
successful_index += 1
c.after_run(actor, blackboard)
FAILURE:
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_reset()
return SUCCESS
for c in get_children():
if c.get_index() < successful_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
successful_index += 1
c.after_run(actor, blackboard)
FAILURE:
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_reset()
return SUCCESS
func interrupt(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
_reset()
super(actor, blackboard)
func _reset() -> void:
successful_index = 0
successful_index = 0
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SequenceStarComposite")
return classes
var classes := super()
classes.push_back(&"SequenceStarComposite")
return classes

@ -2,40 +2,40 @@
## Must only have one child.
@tool
@icon("../../icons/category_decorator.svg")
class_name Decorator extends BeehaveNode
class_name Decorator
extends BeehaveNode
var running_child: BeehaveNode = null
func _ready():
if Engine.is_editor_hint():
return
if Engine.is_editor_hint():
return
if self.get_child_count() != 1:
push_warning("Beehave Error: Decorator %s should have only one child (NodePath: %s)" % [self.name, self.get_path()])
if self.get_child_count() != 1:
push_warning("Beehave Error: Decorator %s should have only one child (NodePath: %s)" % [self.name, self.get_path()])
func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = super._get_configuration_warnings()
var warnings: PackedStringArray = super._get_configuration_warnings()
if get_child_count() != 1:
warnings.append("Decorator should have exactly one child node.")
if get_child_count() != 1:
warnings.append("Decorator should have exactly one child node.")
return warnings
return warnings
func interrupt(actor: Node, blackboard: Blackboard) -> void:
if running_child != null:
running_child.interrupt(actor, blackboard)
running_child = null
if running_child != null:
running_child.interrupt(actor, blackboard)
running_child = null
func after_run(actor: Node, blackboard: Blackboard) -> void:
running_child = null
running_child = null
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"Decorator")
return classes
var classes := super()
classes.push_back(&"Decorator")
return classes

@ -1,34 +1,34 @@
## A Failer node will always return a `FAILURE` status code.
@tool
@icon("../../icons/failer.svg")
class_name AlwaysFailDecorator extends Decorator
class_name AlwaysFailDecorator
extends Decorator
func tick(actor: Node, blackboard: Blackboard) -> int:
var c = get_child(0)
var c = get_child(0)
if c != running_child:
c.before_run(actor, blackboard)
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
else:
c.after_run(actor, blackboard)
return FAILURE
if response == RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
else:
c.after_run(actor, blackboard)
return FAILURE
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"AlwaysFailDecorator")
return classes
var classes := super()
classes.push_back(&"AlwaysFailDecorator")
return classes

@ -2,41 +2,41 @@
## code or `SUCCESS` in case its child returns a `FAILURE` status code.
@tool
@icon("../../icons/inverter.svg")
class_name InverterDecorator extends Decorator
class_name InverterDecorator
extends Decorator
func tick(actor: Node, blackboard: Blackboard) -> int:
var c = get_child(0)
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
c.after_run(actor, blackboard)
return FAILURE
FAILURE:
c.after_run(actor, blackboard)
return SUCCESS
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_:
push_error("This should be unreachable")
return -1
var c = get_child(0)
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
c.after_run(actor, blackboard)
return FAILURE
FAILURE:
c.after_run(actor, blackboard)
return SUCCESS
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_:
push_error("This should be unreachable")
return -1
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"InverterDecorator")
return classes
var classes := super()
classes.push_back(&"InverterDecorator")
return classes

@ -2,40 +2,42 @@
## maximum ticks is reached, it will return a `FAILURE` status code.
@tool
@icon("../../icons/limiter.svg")
class_name LimiterDecorator extends Decorator
class_name LimiterDecorator
extends Decorator
@onready var cache_key = 'limiter_%s' % self.get_instance_id()
@export var max_count : float = 0
@export var max_count: float = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
var child = self.get_child(0)
var current_count = blackboard.get_value(cache_key, 0, str(actor.get_instance_id()))
var child = self.get_child(0)
var current_count = blackboard.get_value(cache_key, 0, str(actor.get_instance_id()))
if current_count == 0:
child.before_run(actor, blackboard)
if current_count == 0:
child.before_run(actor, blackboard)
if current_count < max_count:
blackboard.set_value(cache_key, current_count + 1, str(actor.get_instance_id()))
var response = child.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response)
if current_count < max_count:
blackboard.set_value(cache_key, current_count + 1, str(actor.get_instance_id()))
var response = child.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response)
if child is ConditionLeaf:
blackboard.set_value("last_condition", child, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if child is ConditionLeaf:
blackboard.set_value("last_condition", child, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if child is ActionLeaf and response == RUNNING:
running_child = child
blackboard.set_value("running_action", child, str(actor.get_instance_id()))
if child is ActionLeaf and response == RUNNING:
running_child = child
blackboard.set_value("running_action", child, str(actor.get_instance_id()))
return response
else:
child.after_run(actor, blackboard)
return FAILURE
return response
else:
child.after_run(actor, blackboard)
return FAILURE
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"LimiterDecorator")
return classes
var classes := super()
classes.push_back(&"LimiterDecorator")
return classes

@ -1,34 +1,34 @@
## A succeeder node will always return a `SUCCESS` status code.
@tool
@icon("../../icons/succeeder.svg")
class_name AlwaysSucceedDecorator extends Decorator
class_name AlwaysSucceedDecorator
extends Decorator
func tick(actor: Node, blackboard: Blackboard) -> int:
var c = get_child(0)
var c = get_child(0)
if c != running_child:
c.before_run(actor, blackboard)
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
else:
c.after_run(actor, blackboard)
return SUCCESS
if response == RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
else:
c.after_run(actor, blackboard)
return SUCCESS
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"AlwaysSucceedDecorator")
return classes
var classes := super()
classes.push_back(&"AlwaysSucceedDecorator")
return classes

@ -3,7 +3,8 @@
## every time before the node runs.
@tool
@icon("../../icons/limiter.svg")
class_name TimeLimiterDecorator extends Decorator
class_name TimeLimiterDecorator
extends Decorator
@export var wait_time: = 0.0
@ -13,34 +14,34 @@ var time_left: = 0.0
func tick(actor: Node, blackboard: Blackboard) -> int:
if time_left < wait_time:
time_left += get_physics_process_delta_time()
var response = child.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response)
if child is ConditionLeaf:
blackboard.set_value("last_condition", child, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING:
running_child = child
if child is ActionLeaf:
blackboard.set_value("running_action", child, str(actor.get_instance_id()))
return response
else:
child.after_run(actor, blackboard)
interrupt(actor, blackboard)
return FAILURE
if time_left < wait_time:
time_left += get_physics_process_delta_time()
var response = child.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response)
if child is ConditionLeaf:
blackboard.set_value("last_condition", child, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING:
running_child = child
if child is ActionLeaf:
blackboard.set_value("running_action", child, str(actor.get_instance_id()))
return response
else:
child.after_run(actor, blackboard)
interrupt(actor, blackboard)
return FAILURE
func before_run(actor: Node, blackboard: Blackboard) -> void:
time_left = 0.0
child.before_run(actor, blackboard)
time_left = 0.0
child.before_run(actor, blackboard)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"TimeLimiterDecorator")
return classes
var classes := super()
classes.push_back(&"TimeLimiterDecorator")
return classes

@ -4,10 +4,10 @@
## action is completed.
@tool
@icon("../../icons/action.svg")
class_name ActionLeaf extends Leaf
class_name ActionLeaf
extends Leaf
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"ActionLeaf")
return classes
var classes := super()
classes.push_back(&"ActionLeaf")
return classes

@ -2,60 +2,59 @@
## Returns [code]FAILURE[/code] if any of the expression fails or the
## comparison operation returns [code]false[/code], otherwise it returns [code]SUCCESS[/code].
@tool
class_name BlackboardCompareCondition extends ConditionLeaf
class_name BlackboardCompareCondition
extends ConditionLeaf
enum Operators {
EQUAL,
NOT_EQUAL,
GREATER,
LESS,
GREATER_EQUAL,
LESS_EQUAL,
EQUAL,
NOT_EQUAL,
GREATER,
LESS,
GREATER_EQUAL,
LESS_EQUAL,
}
## Expression represetning left operand.
## This value can be any valid GDScript expression.
## In order to use the existing blackboard keys for comparison,
## use get_value("key_name") e.g. get_value("direction").length()
@export_placeholder(EXPRESSION_PLACEHOLDER) var left_operand: String = ""
## Comparison operator.
@export_enum("==", "!=", ">", "<", ">=", "<=") var operator: int = 0
## Expression represetning right operand.
## This value can be any valid GDScript expression.
## In order to use the existing blackboard keys for comparison,
## use get_value("key_name") e.g. get_value("direction").length()
@export_placeholder(EXPRESSION_PLACEHOLDER) var right_operand: String = ""
@onready var _left_expression: Expression = _parse_expression(left_operand)
@onready var _right_expression: Expression = _parse_expression(right_operand)
func tick(actor: Node, blackboard: Blackboard) -> int:
var left: Variant = _left_expression.execute([], blackboard)
if _left_expression.has_execute_failed():
return FAILURE
var right: Variant = _right_expression.execute([], blackboard)
if _right_expression.has_execute_failed():
return FAILURE
var result: bool = false
match operator:
Operators.EQUAL: result = left == right
Operators.NOT_EQUAL: result = left != right
Operators.GREATER: result = left > right
Operators.LESS: result = left < right
Operators.GREATER_EQUAL: result = left >= right
Operators.LESS_EQUAL: result = left <= right
return SUCCESS if result else FAILURE
var left: Variant = _left_expression.execute([], blackboard)
if _left_expression.has_execute_failed():
return FAILURE
var right: Variant = _right_expression.execute([], blackboard)
if _right_expression.has_execute_failed():
return FAILURE
var result: bool = false
match operator:
Operators.EQUAL: result = left == right
Operators.NOT_EQUAL: result = left != right
Operators.GREATER: result = left > right
Operators.LESS: result = left < right
Operators.GREATER_EQUAL: result = left >= right
Operators.LESS_EQUAL: result = left <= right
return SUCCESS if result else FAILURE
func _get_expression_sources() -> Array[String]:
return [left_operand, right_operand]
return [left_operand, right_operand]

@ -1,8 +1,8 @@
## Erases the specified key from the blackboard.
## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code].
@tool
class_name BlackboardEraseAction extends ActionLeaf
class_name BlackboardEraseAction
extends ActionLeaf
## Expression representing a blackboard key.
@export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = ""
@ -11,15 +11,15 @@ class_name BlackboardEraseAction extends ActionLeaf
func tick(actor: Node, blackboard: Blackboard) -> int:
var key_value: Variant = _key_expression.execute([], blackboard)
if _key_expression.has_execute_failed():
return FAILURE
blackboard.erase_value(key_value)
return SUCCESS
var key_value: Variant = _key_expression.execute([], blackboard)
if _key_expression.has_execute_failed():
return FAILURE
blackboard.erase_value(key_value)
return SUCCESS
func _get_expression_sources() -> Array[String]:
return [key]
return [key]

@ -1,8 +1,8 @@
## Returns [code]FAILURE[/code] if expression execution fails or the specified key doesn't exist.
## Returns [code]SUCCESS[/code] if blackboard has the specified key.
@tool
class_name BlackboardHasCondition extends ConditionLeaf
class_name BlackboardHasCondition
extends ConditionLeaf
## Expression representing a blackboard key.
@export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = ""
@ -11,13 +11,13 @@ class_name BlackboardHasCondition extends ConditionLeaf
func tick(actor: Node, blackboard: Blackboard) -> int:
var key_value: Variant = _key_expression.execute([], blackboard)
if _key_expression.has_execute_failed():
return FAILURE
return SUCCESS if blackboard.has_value(key_value) else FAILURE
var key_value: Variant = _key_expression.execute([], blackboard)
if _key_expression.has_execute_failed():
return FAILURE
return SUCCESS if blackboard.has_value(key_value) else FAILURE
func _get_expression_sources() -> Array[String]:
return [key]
return [key]

@ -1,34 +1,34 @@
## Sets the specified key to the specified value.
## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code].
@tool
class_name BlackboardSetAction extends ActionLeaf
class_name BlackboardSetAction
extends ActionLeaf
## Expression representing a blackboard key.
@export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = ""
## Expression representing a blackboard value to assign to the specified key.
@export_placeholder(EXPRESSION_PLACEHOLDER) var value: String = ""
@onready var _key_expression: Expression = _parse_expression(key)
@onready var _value_expression: Expression = _parse_expression(value)
func tick(actor: Node, blackboard: Blackboard) -> int:
var key_value: Variant = _key_expression.execute([], blackboard)
if _key_expression.has_execute_failed():
return FAILURE
var value_value: Variant = _value_expression.execute([], blackboard)
if _value_expression.has_execute_failed():
return FAILURE
blackboard.set_value(key_value, value_value)
return SUCCESS
var key_value: Variant = _key_expression.execute([], blackboard)
if _key_expression.has_execute_failed():
return FAILURE
var value_value: Variant = _value_expression.execute([], blackboard)
if _value_expression.has_execute_failed():
return FAILURE
blackboard.set_value(key_value, value_value)
return SUCCESS
func _get_expression_sources() -> Array[String]:
return [key, value]
return [key, value]

@ -2,10 +2,10 @@
## a single simple condition. They should never return `RUNNING`.
@tool
@icon("../../icons/condition.svg")
class_name ConditionLeaf extends Leaf
class_name ConditionLeaf
extends Leaf
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"ConditionLeaf")
return classes
var classes := super()
classes.push_back(&"ConditionLeaf")
return classes

@ -1,46 +1,46 @@
## Base class for all leaf nodes of the tree.
@tool
@icon("../../icons/category_leaf.svg")
class_name Leaf extends BeehaveNode
class_name Leaf
extends BeehaveNode
const EXPRESSION_PLACEHOLDER: String = "Insert an expression..."
func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = []
var warnings: PackedStringArray = []
var children: Array[Node] = get_children()
if children.any(func(x): return x is BeehaveNode):
warnings.append("Leaf nodes should not have any child nodes. They won't be ticked.")
var children: Array[Node] = get_children()
for source in _get_expression_sources():
var error_text: String = _parse_expression(source).get_error_text()
if not error_text.is_empty():
warnings.append("Expression `%s` is invalid! Error text: `%s`" % [source, error_text])
if children.any(func(x): return x is BeehaveNode):
warnings.append("Leaf nodes should not have any child nodes. They won't be ticked.")
for source in _get_expression_sources():
var error_text: String = _parse_expression(source).get_error_text()
if not error_text.is_empty():
warnings.append("Expression `%s` is invalid! Error text: `%s`" % [source, error_text])
return warnings
return warnings
func _parse_expression(source: String) -> Expression:
var result: Expression = Expression.new()
var error: int = result.parse(source)
if not Engine.is_editor_hint() and error != OK:
push_error(
"[Leaf] Couldn't parse expression with source: `%s` Error text: `%s`" %\
[source, result.get_error_text()]
)
return result
var result: Expression = Expression.new()
var error: int = result.parse(source)
if not Engine.is_editor_hint() and error != OK:
push_error(
"[Leaf] Couldn't parse expression with source: `%s` Error text: `%s`" %\
[source, result.get_error_text()]
)
return result
func _get_expression_sources() -> Array[String]: # virtual
return []
return []
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"Leaf")
return classes
var classes := super()
classes.push_back(&"Leaf")
return classes

@ -1,7 +1,7 @@
[plugin]
name="Beehave"
description="🐝 Behavior Tree addon for Godot Engine"
author="bitbrain"
version="2.7.6"
script="plugin.gd"
name = "Beehave"
description = "🐝 Behavior Tree addon for Godot Engine"
author = "bitbrain"
version = "2.7.6"
script = "plugin.gd"

@ -5,20 +5,21 @@ const BeehaveEditorDebugger := preload("debug/debugger.gd")
var editor_debugger: BeehaveEditorDebugger
var frames: RefCounted
func _init():
name = "BeehavePlugin"
add_autoload_singleton("BeehaveGlobalMetrics", "res://addons/beehave/metrics/beehave_global_metrics.gd")
add_autoload_singleton("BeehaveGlobalDebugger", "res://addons/beehave/debug/global_debugger.gd")
print("Beehave initialized!")
name = "BeehavePlugin"
add_autoload_singleton("BeehaveGlobalMetrics", "res://addons/beehave/metrics/beehave_global_metrics.gd")
add_autoload_singleton("BeehaveGlobalDebugger", "res://addons/beehave/debug/global_debugger.gd")
print("Beehave initialized!")
func _enter_tree() -> void:
editor_debugger = BeehaveEditorDebugger.new()
frames = preload("debug/frames.gd").new()
add_debugger_plugin(editor_debugger)
editor_debugger = BeehaveEditorDebugger.new()
frames = preload("debug/frames.gd").new()
add_debugger_plugin(editor_debugger)
func _exit_tree() -> void:
remove_debugger_plugin(editor_debugger)
editor_debugger.free()
frames.free()
remove_debugger_plugin(editor_debugger)
editor_debugger.free()
frames.free()

@ -1,22 +1,21 @@
@tool
class_name BeehaveUtils
static func get_plugin() -> EditorPlugin:
var tree: SceneTree = Engine.get_main_loop()
return tree.get_root().get_child(0).get_node_or_null("BeehavePlugin")
var tree: SceneTree = Engine.get_main_loop()
return tree.get_root().get_child(0).get_node_or_null("BeehavePlugin")
static func get_editor_scale() -> float:
var plugin := get_plugin()
if plugin:
return plugin.get_editor_interface().get_editor_scale()
return 1.0
var plugin := get_plugin()
if plugin:
return plugin.get_editor_interface().get_editor_scale()
return 1.0
static func get_frames() -> RefCounted:
var plugin := get_plugin()
if plugin:
return plugin.frames
push_error("Can't find Beehave Plugin")
return null
var plugin := get_plugin()
if plugin:
return plugin.frames
push_error("Can't find Beehave Plugin")
return null

@ -1,194 +1,190 @@
extends Object
const PLUGIN_BUNDLE_NAME: StringName = "aseprite_importers"
const PLUGIN_BUNDLE_NAME: StringName = "aseprite_importers"
const ASEPRITE_EXECUTABLE_PATH_SETTING_NAME: StringName = PLUGIN_BUNDLE_NAME + "/aseprite_executable_path"
enum CompressMode {
LOSSLESS = 0,
LOSSY = 1,
VRAM_COMPRESSED = 2,
VRAM_UNCOMPRESSED = 3,
BASIS_UNIVERSAL = 4,
LOSSLESS = 0,
LOSSY = 1,
VRAM_COMPRESSED = 2,
VRAM_UNCOMPRESSED = 3,
BASIS_UNIVERSAL = 4,
}
const COMPRESS_MODES_NAMES: PackedStringArray = [
"Lossless",
"Lossy",
"VRAM Compressed",
"VRAM Uncompressed",
"Basis Universal",
]
"Lossless",
"Lossy",
"VRAM Compressed",
"VRAM Uncompressed",
"Basis Universal",
]
# ONLY FOR VRAM_COMPRESSED
enum HdrCompression {
DISABLED = 0,
OPAQUE_ONLY = 1,
ALWAYS = 2,
DISABLED = 0,
OPAQUE_ONLY = 1,
ALWAYS = 2,
}
const HDR_COMPRESSION_NAMES: PackedStringArray = [
"Disabled",
"Opaque Only",
"Always",
]
"Disabled",
"Opaque Only",
"Always",
]
# EXCEPT LOSSLESS
enum NormalMap {
DETECT = 0,
ENABLE = 1,
DISABLED = 2,
DETECT = 0,
ENABLE = 1,
DISABLED = 2,
}
const NORMAL_MAP_NAMES: PackedStringArray = [
"Detect",
"Enable",
"Disabled",
]
"Detect",
"Enable",
"Disabled",
]
enum ChannelPack {
SRGB_FRIENDLY = 0,
OPTIMIZED = 1,
SRGB_FRIENDLY = 0,
OPTIMIZED = 1,
}
const CHANNEL_PACK_NAMES: PackedStringArray = [
"sRGB Friendly",
"Optimized",
]
"sRGB Friendly",
"Optimized",
]
enum Roughness {
DETECT = 0,
DISABLED = 1,
RED = 2,
GREEN = 3,
BLUE = 4,
ALPHA = 5,
GRAY = 6,
DETECT = 0,
DISABLED = 1,
RED = 2,
GREEN = 3,
BLUE = 4,
ALPHA = 5,
GRAY = 6,
}
const ROUGHNESS_NAMES: PackedStringArray = [
"Detect",
"Disabled",
"Red",
"Green",
"Blue",
"Alpha",
"Gray",
]
"Detect",
"Disabled",
"Red",
"Green",
"Blue",
"Alpha",
"Gray",
]
enum CompressMode3D {
DISABLED = 0,
VRAM_COMPRESSED = 1,
BASIS_UNIVERSAL = 2,
DISABLED = 0,
VRAM_COMPRESSED = 1,
BASIS_UNIVERSAL = 2,
}
const COMPRESS_MODE_3D_NAMES: PackedStringArray = [
"Disabled",
"VRAM Compressed",
"Basis Universal",
]
"Disabled",
"VRAM Compressed",
"Basis Universal",
]
const EMPTY_CALLABLE: Callable = Callable()
static func create_option(
name: String,
default_value: Variant,
property_hint: PropertyHint = PROPERTY_HINT_NONE,
hint_string: String = "",
usage: PropertyUsageFlags = PROPERTY_USAGE_NONE,
get_is_visible: Callable = EMPTY_CALLABLE
) -> Dictionary:
var option_data: Dictionary = {
name = name,
default_value = default_value,
}
if hint_string: option_data["hint_string"] = hint_string
if property_hint: option_data["property_hint"] = property_hint
if usage: option_data["usage"] = usage
if get_is_visible != EMPTY_CALLABLE: option_data["get_is_visible"] = get_is_visible
return option_data
name: String,
default_value: Variant,
property_hint: PropertyHint = PROPERTY_HINT_NONE,
hint_string: String = "",
usage: PropertyUsageFlags = PROPERTY_USAGE_NONE,
get_is_visible: Callable = EMPTY_CALLABLE
) -> Dictionary:
var option_data: Dictionary = {
name = name,
default_value = default_value,
}
if hint_string: option_data["hint_string"] = hint_string
if property_hint: option_data["property_hint"] = property_hint
if usage: option_data["usage"] = usage
if get_is_visible != EMPTY_CALLABLE: option_data["get_is_visible"] = get_is_visible
return option_data
enum BorderType {
None = 0,
Transparent = 1,
Extruded = 2,
None = 0,
Transparent = 1,
Extruded = 2,
}
const SPRITESHEET_BORDER_TYPES: PackedStringArray = [
"None",
"Transparent",
"Extruded",
]
"None",
"Transparent",
"Extruded",
]
enum AnimationDirection {
FORWARD = 0,
REVERSE = 1,
PING_PONG = 2,
PING_PONG_REVERSE = 3,
FORWARD = 0,
REVERSE = 1,
PING_PONG = 2,
PING_PONG_REVERSE = 3,
}
const ASEPRITE_OUTPUT_ANIMATION_DIRECTIONS: PackedStringArray = [
"forward", "reverse", "pingpong", "pingpong_reverse" ]
const PRESET_OPTIONS_ANIMATION_DIRECTIONS: PackedStringArray = [
"Forward", "Reverse", "Ping-pong", "Ping-pong reverse" ]
"forward", "reverse", "pingpong", "pingpong_reverse" ]
const PRESET_OPTIONS_ANIMATION_DIRECTIONS: PackedStringArray = [
"Forward", "Reverse", "Ping-pong", "Ping-pong reverse" ]
enum SpritesheetLayout {
PACKED = 0,
BY_ROWS = 1,
BY_COLUMNS = 2,
PACKED = 0,
BY_ROWS = 1,
BY_COLUMNS = 2,
}
const SPRITESHEET_LAYOUTS: PackedStringArray = ["Packed", "By rows", "By columns"]
const OPTION_SPRITESHEET_BORDER_TYPE: String = "spritesheet/border_type"
const OPTION_SPRITESHEET_TRIM: String = "spritesheet/trim"
const OPTION_SPRITESHEET_IGNORE_EMPTY: String = "spritesheet/ignore_empty"
const OPTION_SPRITESHEET_MERGE_DUPLICATES: String = "spritesheet/merge_duplicates"
const OPTION_SPRITESHEET_LAYOUT: String = "spritesheet/layout"
const OPTION_ANIMATION_DEFAULT_NAME: String = "animation/default/name"
const OPTION_ANIMATION_DEFAULT_DIRECTION: String = "animation/default/direction"
const OPTION_SPRITESHEET_BORDER_TYPE: String = "spritesheet/border_type"
const OPTION_SPRITESHEET_TRIM: String = "spritesheet/trim"
const OPTION_SPRITESHEET_IGNORE_EMPTY: String = "spritesheet/ignore_empty"
const OPTION_SPRITESHEET_MERGE_DUPLICATES: String = "spritesheet/merge_duplicates"
const OPTION_SPRITESHEET_LAYOUT: String = "spritesheet/layout"
const OPTION_ANIMATION_DEFAULT_NAME: String = "animation/default/name"
const OPTION_ANIMATION_DEFAULT_DIRECTION: String = "animation/default/direction"
const OPTION_ANIMATION_DEFAULT_REPEAT_COUNT: String = "animation/default/repeat_count"
const OPTION_ANIMATION_AUTOPLAY_NAME: String = "animation/autoplay"
const OPTION_ANIMATION_STRATEGY: String = "animation/strategy"
const OPTION_LAYERS_INCLUDE_REG_EX: String = "layers/include_reg_ex"
const OPTION_LAYERS_EXCLUDE_REG_EX: String = "layers/exclude_reg_ex"
const OPTION_TAGS_INCLUDE_REG_EX: String = "tags/include_reg_ex"
const OPTION_TAGS_EXCLUDE_REG_EX: String = "tags/exclude_reg_ex"
const SPRITESHEET_FIXED_ROWS_COUNT: String = "spritesheet/fixed_rows_count"
const SPRITESHEET_FIXED_COLUMNS_COUNT: String = "spritesheet/fixed_columns_count"
const OPTION_ANIMATION_AUTOPLAY_NAME: String = "animation/autoplay"
const OPTION_ANIMATION_STRATEGY: String = "animation/strategy"
const OPTION_LAYERS_INCLUDE_REG_EX: String = "layers/include_reg_ex"
const OPTION_LAYERS_EXCLUDE_REG_EX: String = "layers/exclude_reg_ex"
const OPTION_TAGS_INCLUDE_REG_EX: String = "tags/include_reg_ex"
const OPTION_TAGS_EXCLUDE_REG_EX: String = "tags/exclude_reg_ex"
const SPRITESHEET_FIXED_ROWS_COUNT: String = "spritesheet/fixed_rows_count"
const SPRITESHEET_FIXED_COLUMNS_COUNT: String = "spritesheet/fixed_columns_count"
class ParsedAnimationOptions:
var border_type: BorderType
var trim: bool
var ignore_empty: bool
var merge_duplicates: bool
var spritesheet_layout: SpritesheetLayout
var spritesheet_fixed_rows_count: int
var spritesheet_fixed_columns_count: int
var default_animation_name: String
var default_animation_direction: AnimationDirection
var default_animation_repeat_count: int
var animation_autoplay_name: String
func _init(options: Dictionary) -> void:
border_type = options[OPTION_SPRITESHEET_BORDER_TYPE]
trim = options[OPTION_SPRITESHEET_TRIM]
ignore_empty = options[OPTION_SPRITESHEET_IGNORE_EMPTY]
merge_duplicates = options[OPTION_SPRITESHEET_MERGE_DUPLICATES]
spritesheet_layout = options[OPTION_SPRITESHEET_LAYOUT]
spritesheet_fixed_rows_count = options[SPRITESHEET_FIXED_ROWS_COUNT]
spritesheet_fixed_columns_count = options[SPRITESHEET_FIXED_COLUMNS_COUNT]
default_animation_name = options[OPTION_ANIMATION_DEFAULT_NAME].strip_edges().strip_escapes()
if default_animation_name.is_empty(): default_animation_name = "default"
default_animation_direction = options[OPTION_ANIMATION_DEFAULT_DIRECTION]
default_animation_repeat_count = options[OPTION_ANIMATION_DEFAULT_REPEAT_COUNT]
animation_autoplay_name = options[OPTION_ANIMATION_AUTOPLAY_NAME].strip_edges().strip_escapes()
var border_type: BorderType
var trim: bool
var ignore_empty: bool
var merge_duplicates: bool
var spritesheet_layout: SpritesheetLayout
var spritesheet_fixed_rows_count: int
var spritesheet_fixed_columns_count: int
var default_animation_name: String
var default_animation_direction: AnimationDirection
var default_animation_repeat_count: int
var animation_autoplay_name: String
func _init(options: Dictionary) -> void:
border_type = options[OPTION_SPRITESHEET_BORDER_TYPE]
trim = options[OPTION_SPRITESHEET_TRIM]
ignore_empty = options[OPTION_SPRITESHEET_IGNORE_EMPTY]
merge_duplicates = options[OPTION_SPRITESHEET_MERGE_DUPLICATES]
spritesheet_layout = options[OPTION_SPRITESHEET_LAYOUT]
spritesheet_fixed_rows_count = options[SPRITESHEET_FIXED_ROWS_COUNT]
spritesheet_fixed_columns_count = options[SPRITESHEET_FIXED_COLUMNS_COUNT]
default_animation_name = options[OPTION_ANIMATION_DEFAULT_NAME].strip_edges().strip_escapes()
if default_animation_name.is_empty(): default_animation_name = "default"
default_animation_direction = options[OPTION_ANIMATION_DEFAULT_DIRECTION]
default_animation_repeat_count = options[OPTION_ANIMATION_DEFAULT_REPEAT_COUNT]
animation_autoplay_name = options[OPTION_ANIMATION_AUTOPLAY_NAME].strip_edges().strip_escapes()
static func create_common_animation_options() -> Array[Dictionary]:
return [
create_option(OPTION_SPRITESHEET_LAYOUT, SpritesheetLayout.PACKED, PROPERTY_HINT_ENUM, ",".join(SPRITESHEET_LAYOUTS), PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED ),
create_option(SPRITESHEET_FIXED_ROWS_COUNT, 1, PROPERTY_HINT_RANGE, "1,32,1,or_greater", PROPERTY_USAGE_EDITOR,
func(options): return options[OPTION_SPRITESHEET_LAYOUT] == SpritesheetLayout.BY_COLUMNS),
create_option(SPRITESHEET_FIXED_COLUMNS_COUNT, 1, PROPERTY_HINT_RANGE, "1,32,1,or_greater", PROPERTY_USAGE_EDITOR,
func(options): return options[OPTION_SPRITESHEET_LAYOUT] == SpritesheetLayout.BY_ROWS),
return [
create_option(OPTION_SPRITESHEET_LAYOUT, SpritesheetLayout.PACKED, PROPERTY_HINT_ENUM, ",".join(SPRITESHEET_LAYOUTS), PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED ),
create_option(SPRITESHEET_FIXED_ROWS_COUNT, 1, PROPERTY_HINT_RANGE, "1,32,1,or_greater", PROPERTY_USAGE_EDITOR,
func(options): return options[OPTION_SPRITESHEET_LAYOUT] == SpritesheetLayout.BY_COLUMNS),
create_option(SPRITESHEET_FIXED_COLUMNS_COUNT, 1, PROPERTY_HINT_RANGE, "1,32,1,or_greater", PROPERTY_USAGE_EDITOR,
func(options): return options[OPTION_SPRITESHEET_LAYOUT] == SpritesheetLayout.BY_ROWS),
create_option(OPTION_SPRITESHEET_BORDER_TYPE, BorderType.None, PROPERTY_HINT_ENUM, ",".join(SPRITESHEET_BORDER_TYPES), PROPERTY_USAGE_EDITOR),
create_option(OPTION_SPRITESHEET_TRIM, false, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR,
func(options): return options[OPTION_SPRITESHEET_LAYOUT] != SpritesheetLayout.PACKED),
create_option(OPTION_SPRITESHEET_TRIM, false, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR,
func(options): return options[OPTION_SPRITESHEET_LAYOUT] != SpritesheetLayout.PACKED),
create_option(OPTION_SPRITESHEET_IGNORE_EMPTY, false, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
create_option(OPTION_SPRITESHEET_MERGE_DUPLICATES, false, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
create_option(OPTION_ANIMATION_DEFAULT_NAME, "default", PROPERTY_HINT_PLACEHOLDER_TEXT, "default", PROPERTY_USAGE_EDITOR),
create_option(OPTION_ANIMATION_DEFAULT_DIRECTION, AnimationDirection.FORWARD, PROPERTY_HINT_ENUM, ",".join(PRESET_OPTIONS_ANIMATION_DIRECTIONS), PROPERTY_USAGE_EDITOR),
create_option(OPTION_ANIMATION_DEFAULT_REPEAT_COUNT, 0, PROPERTY_HINT_RANGE, "0,32,1,or_greater", PROPERTY_USAGE_EDITOR),
create_option(OPTION_ANIMATION_AUTOPLAY_NAME, "", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
]
create_option(OPTION_SPRITESHEET_MERGE_DUPLICATES, false, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
create_option(OPTION_ANIMATION_DEFAULT_NAME, "default", PROPERTY_HINT_PLACEHOLDER_TEXT, "default", PROPERTY_USAGE_EDITOR),
create_option(OPTION_ANIMATION_DEFAULT_DIRECTION, AnimationDirection.FORWARD, PROPERTY_HINT_ENUM, ",".join(PRESET_OPTIONS_ANIMATION_DIRECTIONS), PROPERTY_USAGE_EDITOR),
create_option(OPTION_ANIMATION_DEFAULT_REPEAT_COUNT, 0, PROPERTY_HINT_RANGE, "0,32,1,or_greater", PROPERTY_USAGE_EDITOR),
create_option(OPTION_ANIMATION_AUTOPLAY_NAME, "", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
]

@ -3,205 +3,214 @@ extends "_importer_base.gd"
# Base class for all nested animation import plugins
func set_preset(name: StringName, options: Array[Dictionary]) -> void:
var preset: Array[Dictionary] = []
preset.append_array(_parent_plugin.common_options)
preset.append_array(options)
__option_visibility_checkers.clear()
for option in preset:
var option_visibility_checker: Callable = option.get("get_is_visible", Common.EMPTY_CALLABLE)
if option_visibility_checker != Common.EMPTY_CALLABLE:
__option_visibility_checkers[option.name] = option_visibility_checker
_presets[name] = preset
var preset: Array[Dictionary] = []
preset.append_array(_parent_plugin.common_options)
preset.append_array(options)
__option_visibility_checkers.clear()
for option in preset:
var option_visibility_checker: Callable = option.get("get_is_visible", Common.EMPTY_CALLABLE)
if option_visibility_checker != Common.EMPTY_CALLABLE:
__option_visibility_checkers[option.name] = option_visibility_checker
_presets[name] = preset
func _init(parent_plugin: EditorPlugin) -> void:
super(parent_plugin)
super(parent_plugin)
class ExportResult:
var error: Error
var error_message: String
var raw_output: String
var parsed_json: JSON
var texture: Texture2D
var spritesheet_metadata: SpritesheetMetadata
var error: Error
var error_message: String
var raw_output: String
var parsed_json: JSON
var texture: Texture2D
var spritesheet_metadata: SpritesheetMetadata
class FrameData:
var region_rect: Rect2i
var region_rect_offset: Vector2i
var duration_ms: int
var region_rect: Rect2i
var region_rect_offset: Vector2i
var duration_ms: int
class AnimationTag:
var name: String
var frames: Array[FrameData]
var duration_ms: int
var looped: bool
var name: String
var frames: Array[FrameData]
var duration_ms: int
var looped: bool
class SpritesheetMetadata:
var source_size: Vector2i
var spritesheet_size: Vector2i
var animation_tags: Array[AnimationTag]
var source_size: Vector2i
var spritesheet_size: Vector2i
var animation_tags: Array[AnimationTag]
class TrackFrame:
var duration_ms: int
var value: Variant
func _init(duration_ms: int, value: Variant) -> void:
self.duration_ms = duration_ms
self.value = value
var duration_ms: int
var value: Variant
func _init(duration_ms: int, value: Variant) -> void:
self.duration_ms = duration_ms
self.value = value
const __sheet_types_by_spritesheet_layout: Dictionary = {
Common.SpritesheetLayout.PACKED: "packed",
Common.SpritesheetLayout.BY_ROWS: "rows",
Common.SpritesheetLayout.BY_COLUMNS: "columns",
}
Common.SpritesheetLayout.PACKED: "packed",
Common.SpritesheetLayout.BY_ROWS: "rows",
Common.SpritesheetLayout.BY_COLUMNS: "columns",
}
func _export_texture(source_file: String, options: Common.ParsedAnimationOptions, image_options: Dictionary, gen_files: Array[String]) -> ExportResult:
var export_result = ExportResult.new()
var spritesheet_metadata = SpritesheetMetadata.new()
var png_path: String = source_file.get_basename() + ".png"
var data_path: String = source_file.get_basename() + ".json"
var global_png_path: String = ProjectSettings.globalize_path(png_path)
var global_data_path: String = ProjectSettings.globalize_path(data_path)
var is_png_file_present = FileAccess.file_exists(png_path)
var aseprite_executable_path: String = ProjectSettings.get_setting(Common.ASEPRITE_EXECUTABLE_PATH_SETTING_NAME)
var variable_options: Array
if options.spritesheet_layout == Common.SpritesheetLayout.BY_ROWS:
variable_options += ["--sheet-columns", str(options.spritesheet_fixed_columns_count)]
if options.spritesheet_layout == Common.SpritesheetLayout.BY_COLUMNS:
variable_options += ["--sheet-rows", str(options.spritesheet_fixed_rows_count)]
match options.border_type:
Common.BorderType.Transparent: variable_options += ["--inner-padding", "1"]
Common.BorderType.Extruded: variable_options += ["--extrude"]
Common.BorderType.None: pass
_: push_error("unexpected border type")
if options.ignore_empty: variable_options += ["--ignore-empty"]
if options.merge_duplicates: variable_options += ["--merge-duplicates"]
if options.trim: variable_options += ["--trim" if options.spritesheet_layout == Common.SpritesheetLayout.PACKED else "--trim-sprite"]
var command_line_params: PackedStringArray = PackedStringArray([
"--batch",
"--filename-format", "{tag}{tagframe}",
"--format", "json-array",
"--list-tags",
"--trim" if options.spritesheet_layout == Common.SpritesheetLayout.PACKED else "--trim-sprite" if options.trim else "",
"--sheet-type", __sheet_types_by_spritesheet_layout[options.spritesheet_layout],
] + variable_options + [
"--sheet", global_png_path,
"--data", global_data_path,
"--inner-padding","1",
ProjectSettings.globalize_path(source_file)
])
var err: Error = OS.execute(
ProjectSettings.get_setting(Common.ASEPRITE_EXECUTABLE_PATH_SETTING_NAME),
command_line_params)
if err:
export_result.error = err
export_result.error_message = "There was an error while executing aseprite command: %s" % error_string(err)
return export_result
var json = JSON.new()
var data: String = FileAccess.get_file_as_string(global_data_path)
json.parse(data)
DirAccess.remove_absolute(global_data_path)
var sourceSizeData = json.data.frames[0].sourceSize
spritesheet_metadata.source_size = Vector2i(sourceSizeData.w, sourceSizeData.h)
spritesheet_metadata.spritesheet_size = Vector2i(json.data.meta.size.w, json.data.meta.size.h)
var frames_data: Array[FrameData]
for frame_data in json.data.frames:
var fd: FrameData = FrameData.new()
fd.region_rect = Rect2i(
frame_data.frame.x, frame_data.frame.y,
frame_data.frame.w, frame_data.frame.h)
fd.region_rect_offset = Vector2i(
frame_data.spriteSourceSize.x, frame_data.spriteSourceSize.y)
if options.border_type == Common.BorderType.Transparent:
fd.region_rect = fd.region_rect.grow(-1)
fd.region_rect_offset += Vector2i.ONE
fd.duration_ms = frame_data.duration
frames_data.append(fd)
var tags_data: Array = json.data.meta.frameTags
var unique_names: Array[String] = []
if tags_data.is_empty():
var default_animation_tag = AnimationTag.new()
default_animation_tag.name = options.default_animation_name
if options.default_animation_repeat_count > 0:
for cycle_index in options.default_animation_repeat_count:
default_animation_tag.frames.append_array(frames_data)
else:
default_animation_tag.frames = frames_data
default_animation_tag.looped = true
spritesheet_metadata.animation_tags.append(default_animation_tag)
else:
for tag_data in tags_data:
var animation_tag = AnimationTag.new()
animation_tag.name = tag_data.name.strip_edges().strip_escapes()
if animation_tag.name.is_empty():
push_error("Found empty tag name")
return null
if unique_names.has(animation_tag.name):
push_error("Found duplicated tag name")
return null
if animation_tag.name.begins_with("_"):
continue
unique_names.append(animation_tag.name)
var animation_direction = Common.ASEPRITE_OUTPUT_ANIMATION_DIRECTIONS.find(tag_data.direction)
var animation_frames: Array = frames_data.slice(tag_data.from, tag_data.to + 1)
# Apply animation direction
animation_tag.looped = animation_tag.name.ends_with("_loop")
animation_tag.frames.append_array(animation_frames)
spritesheet_metadata.animation_tags.append(animation_tag)
var image = Image.load_from_file(global_png_path)
image.save_png(global_png_path)
image = null
if false:
# This is for reload the image if it was changed by import processing
_parent_plugin.get_editor_interface().get_resource_filesystem().call_deferred("scan_sources")
else:
# This function does not import the file. But its call is needed
# so that the call to the "append" function passes without errors
_parent_plugin.get_editor_interface().get_resource_filesystem().update_file(png_path)
append_import_external_resource(png_path, image_options, "texture")
# This is a working way to reuse a previously imported resource. Don't change it!
var texture: Texture2D = ResourceLoader.load(png_path, "Texture2D", ResourceLoader.CACHE_MODE_REPLACE) as Texture2D
export_result.texture = texture
export_result.raw_output = data
export_result.parsed_json = json
export_result.spritesheet_metadata = spritesheet_metadata
return export_result
var export_result = ExportResult.new()
var spritesheet_metadata = SpritesheetMetadata.new()
var png_path: String = source_file.get_basename() + ".png"
var data_path: String = source_file.get_basename() + ".json"
var global_png_path: String = ProjectSettings.globalize_path(png_path)
var global_data_path: String = ProjectSettings.globalize_path(data_path)
var is_png_file_present = FileAccess.file_exists(png_path)
var aseprite_executable_path: String = ProjectSettings.get_setting(Common.ASEPRITE_EXECUTABLE_PATH_SETTING_NAME)
var variable_options: Array
if options.spritesheet_layout == Common.SpritesheetLayout.BY_ROWS:
variable_options += ["--sheet-columns", str(options.spritesheet_fixed_columns_count)]
if options.spritesheet_layout == Common.SpritesheetLayout.BY_COLUMNS:
variable_options += ["--sheet-rows", str(options.spritesheet_fixed_rows_count)]
match options.border_type:
Common.BorderType.Transparent: variable_options += ["--inner-padding", "1"]
Common.BorderType.Extruded: variable_options += ["--extrude"]
Common.BorderType.None: pass
_: push_error("unexpected border type")
if options.ignore_empty: variable_options += ["--ignore-empty"]
if options.merge_duplicates: variable_options += ["--merge-duplicates"]
if options.trim: variable_options += ["--trim" if options.spritesheet_layout == Common.SpritesheetLayout.PACKED else "--trim-sprite"]
var command_line_params: PackedStringArray = PackedStringArray([
"--batch",
"--filename-format", "{tag}{tagframe}",
"--format", "json-array",
"--list-tags",
"--trim" if options.spritesheet_layout == Common.SpritesheetLayout.PACKED else "--trim-sprite" if options.trim else "",
"--sheet-type", __sheet_types_by_spritesheet_layout[options.spritesheet_layout],
] + variable_options + [
"--sheet", global_png_path,
"--data", global_data_path,
"--inner-padding", "1",
ProjectSettings.globalize_path(source_file)
])
var err: Error = OS.execute(
ProjectSettings.get_setting(Common.ASEPRITE_EXECUTABLE_PATH_SETTING_NAME),
command_line_params)
if err:
export_result.error = err
export_result.error_message = "There was an error while executing aseprite command: %s" % error_string(err)
return export_result
var json = JSON.new()
var data: String = FileAccess.get_file_as_string(global_data_path)
json.parse(data)
DirAccess.remove_absolute(global_data_path)
var sourceSizeData = json.data.frames[0].sourceSize
spritesheet_metadata.source_size = Vector2i(sourceSizeData.w, sourceSizeData.h)
spritesheet_metadata.spritesheet_size = Vector2i(json.data.meta.size.w, json.data.meta.size.h)
var frames_data: Array[FrameData]
for frame_data in json.data.frames:
var fd: FrameData = FrameData.new()
fd.region_rect = Rect2i(
frame_data.frame.x, frame_data.frame.y,
frame_data.frame.w, frame_data.frame.h)
fd.region_rect_offset = Vector2i(
frame_data.spriteSourceSize.x, frame_data.spriteSourceSize.y)
if options.border_type == Common.BorderType.Transparent:
fd.region_rect = fd.region_rect.grow(-1)
fd.region_rect_offset += Vector2i.ONE
fd.duration_ms = frame_data.duration
frames_data.append(fd)
var tags_data: Array = json.data.meta.frameTags
var unique_names: Array[String] = []
if tags_data.is_empty():
var default_animation_tag = AnimationTag.new()
default_animation_tag.name = options.default_animation_name
if options.default_animation_repeat_count > 0:
for cycle_index in options.default_animation_repeat_count:
default_animation_tag.frames.append_array(frames_data)
else:
default_animation_tag.frames = frames_data
default_animation_tag.looped = true
spritesheet_metadata.animation_tags.append(default_animation_tag)
else:
for tag_data in tags_data:
var animation_tag = AnimationTag.new()
animation_tag.name = tag_data.name.strip_edges().strip_escapes()
if animation_tag.name.is_empty():
push_error("Found empty tag name")
return null
if unique_names.has(animation_tag.name):
push_error("Found duplicated tag name")
return null
if animation_tag.name.begins_with("_"):
continue
unique_names.append(animation_tag.name)
var animation_direction = Common.ASEPRITE_OUTPUT_ANIMATION_DIRECTIONS.find(tag_data.direction)
var animation_frames: Array = frames_data.slice(tag_data.from, tag_data.to + 1)
# Apply animation direction
animation_tag.looped = animation_tag.name.ends_with("_loop")
animation_tag.frames.append_array(animation_frames)
spritesheet_metadata.animation_tags.append(animation_tag)
var image = Image.load_from_file(global_png_path)
image.save_png(global_png_path)
image = null
if false:
# This is for reload the image if it was changed by import processing
_parent_plugin.get_editor_interface().get_resource_filesystem().call_deferred("scan_sources")
else:
# This function does not import the file. But its call is needed
# so that the call to the "append" function passes without errors
_parent_plugin.get_editor_interface().get_resource_filesystem().update_file(png_path)
append_import_external_resource(png_path, image_options, "texture")
# This is a working way to reuse a previously imported resource. Don't change it!
var texture: Texture2D = ResourceLoader.load(png_path, "Texture2D", ResourceLoader.CACHE_MODE_REPLACE) as Texture2D
export_result.texture = texture
export_result.raw_output = data
export_result.parsed_json = json
export_result.spritesheet_metadata = spritesheet_metadata
return export_result
static func _create_animation_player(
spritesheet_metadata: SpritesheetMetadata,
track_value_getters_by_property_path: Dictionary,
animation_autoplay_name: String = ""
) -> AnimationPlayer:
var animation_player: AnimationPlayer = AnimationPlayer.new()
animation_player.name = "AnimationPlayer"
var animation_library: AnimationLibrary = AnimationLibrary.new()
for animation_tag in spritesheet_metadata.animation_tags:
var animation: Animation = Animation.new()
for property_path in track_value_getters_by_property_path.keys():
__create_track(animation, property_path,
animation_tag, track_value_getters_by_property_path[property_path])
animation.length = animation_tag.frames.reduce(
func (accum: int, frame_data: FrameData):
return accum + frame_data.duration_ms, 0) * 0.001
animation.loop_mode = Animation.LOOP_LINEAR if animation_tag.looped else Animation.LOOP_NONE
animation_library.add_animation(animation_tag.name, animation)
animation_player.add_animation_library("", animation_library)
if not animation_autoplay_name.is_empty():
spritesheet_metadata: SpritesheetMetadata,
track_value_getters_by_property_path: Dictionary,
animation_autoplay_name: String = ""
) -> AnimationPlayer:
var animation_player: AnimationPlayer = AnimationPlayer.new()
animation_player.name = "AnimationPlayer"
var animation_library: AnimationLibrary = AnimationLibrary.new()
for animation_tag in spritesheet_metadata.animation_tags:
var animation: Animation = Animation.new()
for property_path in track_value_getters_by_property_path.keys():
__create_track(animation, property_path,
animation_tag, track_value_getters_by_property_path[property_path])
animation.length = animation_tag.frames.reduce(
func (accum: int, frame_data: FrameData):
return accum + frame_data.duration_ms, 0) * 0.001
animation.loop_mode = Animation.LOOP_LINEAR if animation_tag.looped else Animation.LOOP_NONE
animation_library.add_animation(animation_tag.name, animation)
animation_player.add_animation_library("", animation_library)
if not animation_autoplay_name.is_empty():
if animation_player.has_animation(animation_autoplay_name):
animation_player.autoplay = animation_autoplay_name
else:
@ -210,31 +219,40 @@ static func _create_animation_player(
return animation_player
static func __create_track(
animation: Animation,
property_path: NodePath,
animation_tag: AnimationTag,
track_value_getter: Callable # func(fd: FrameData) -> Variant for each fd in animation_tag.frames
) -> int:
animation: Animation,
property_path: NodePath,
animation_tag: AnimationTag,
track_value_getter: Callable # func(fd: FrameData) -> Variant for each fd in animation_tag.frames
) -> int:
var track_index = animation.add_track(Animation.TYPE_VALUE)
animation.track_set_path(track_index, property_path)
animation.value_track_set_update_mode(track_index, Animation.UPDATE_DISCRETE)
animation.track_set_interpolation_loop_wrap(track_index, false)
animation.track_set_interpolation_type(track_index, Animation.INTERPOLATION_NEAREST)
var track_frames = animation_tag.frames.map(
func (frame_data: FrameData):
return TrackFrame.new(
frame_data.duration_ms,
track_value_getter.call(frame_data)))
animation.track_set_path(track_index, property_path)
animation.value_track_set_update_mode(track_index, Animation.UPDATE_DISCRETE)
animation.track_set_interpolation_loop_wrap(track_index, false)
animation.track_set_interpolation_type(track_index, Animation.INTERPOLATION_NEAREST)
var track_frames = animation_tag.frames.map(
func (frame_data: FrameData):
return TrackFrame.new(
frame_data.duration_ms,
track_value_getter.call(frame_data)
))
var transition: float = 1
var track_length_ms: int = 0
var previous_track_frame: TrackFrame = null
for track_frame in track_frames:
var track_length_ms: int = 0
var previous_track_frame: TrackFrame = null
for
track_frame in track_frames:
if previous_track_frame == null or track_frame.value != previous_track_frame.value:
animation.track_insert_key(track_index,
track_length_ms * 0.001, track_frame.value, transition)
previous_track_frame = track_frame
track_length_ms += track_frame.duration_ms
track_length_ms += track_frame.duration_ms
return track_index

@ -4,12 +4,10 @@ extends EditorImportPlugin
# Base class for all nested import plugins
const Common = preload("../common.gd")
var _parent_plugin: EditorPlugin
var _import_order: int = 0
var _import_order: int = 0
var _importer_name: String = ""
var _priority: float = 1
var _priority: float = 1
var _recognized_extensions: PackedStringArray
var _resource_type: StringName
var _save_extension: String
@ -17,49 +15,62 @@ var _visible_name: String
var _presets: Dictionary
var __option_visibility_checkers: Dictionary
func _init(parent_plugin: EditorPlugin) -> void:
_parent_plugin = parent_plugin
_parent_plugin = parent_plugin
func _get_import_options(path: String, preset_index: int) -> Array[Dictionary]:
return _presets.values()[preset_index] as Array[Dictionary]
return _presets.values()[preset_index] as Array[Dictionary]
func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool:
var option_visibility_checker: Callable = __option_visibility_checkers.get(option_name, Common.EMPTY_CALLABLE)
if option_visibility_checker:
if option_visibility_checker == Common.EMPTY_CALLABLE:
return true
else:
return option_visibility_checker.call(options)
else:
return true
var option_visibility_checker: Callable = __option_visibility_checkers.get(option_name, Common.EMPTY_CALLABLE)
if option_visibility_checker:
if option_visibility_checker == Common.EMPTY_CALLABLE:
return true
else:
return option_visibility_checker.call(options)
else:
return true
func _get_import_order() -> int:
return _import_order
return _import_order
func _get_importer_name() -> String:
return _importer_name
return _importer_name
func _get_preset_count() -> int:
return _presets.size()
return _presets.size()
func _get_preset_name(preset_index: int) -> String:
return _presets.keys()[preset_index]
return _presets.keys()[preset_index]
func _get_priority() -> float:
return _priority
return _priority
func _get_recognized_extensions() -> PackedStringArray:
return _recognized_extensions
return _recognized_extensions
func _get_resource_type() -> String:
return _resource_type
return _resource_type
func _get_save_extension() -> String:
return _save_extension
return _save_extension
func _get_visible_name() -> String:
return _visible_name
return _visible_name
func _import(source_file: String, save_path: String, options: Dictionary,
platform_variants: Array[String], gen_files: Array[String]) -> Error:
return ERR_UNCONFIGURED
platform_variants: Array[String], gen_files: Array[String]) -> Error:
return ERR_UNCONFIGURED

@ -1,75 +1,77 @@
extends "_animation_importer_base.gd"
func _init(parent_plugin: EditorPlugin) -> void:
super(parent_plugin)
_import_order = 0
_importer_name = "Aseprite SpriteFrames Import"
_priority = 1
_recognized_extensions = ["ase", "aseprite"]
_resource_type = "SpriteFrames"
_save_extension = "res"
_visible_name = "SpriteFrames"
super(parent_plugin)
_import_order = 0
_importer_name = "Aseprite SpriteFrames Import"
_priority = 1
_recognized_extensions = ["ase", "aseprite"]
_resource_type = "SpriteFrames"
_save_extension = "res"
_visible_name = "SpriteFrames"
set_preset("Animation", [])
set_preset("Animation", [])
func _import(source_file: String, save_path: String, options: Dictionary,
platform_variants: Array[String], gen_files: Array[String]) -> Error:
print("importing:",source_file)
var status: Error = OK
var parsed_options = Common.ParsedAnimationOptions.new(options)
var export_result: ExportResult = _export_texture(source_file, parsed_options, options, gen_files)
if export_result.error:
push_error("There was an error during exporting texture: %s with message: %s" %
[error_string(export_result.error), export_result.error_message])
return export_result.error
platform_variants: Array[String], gen_files: Array[String]) -> Error:
print("importing:", source_file)
var status: Error = OK
var parsed_options = Common.ParsedAnimationOptions.new(options)
var export_result: ExportResult = _export_texture(source_file, parsed_options, options, gen_files)
if export_result.error:
push_error("There was an error during exporting texture: %s with message: %s" %
[error_string(export_result.error), export_result.error_message])
return export_result.error
var sprite_frames: SpriteFrames
if ResourceLoader.exists(source_file):
# This is a working way to reuse a previously imported resource. Don't change it!
sprite_frames = ResourceLoader.load(source_file, "SpriteFrames", ResourceLoader.CACHE_MODE_REPLACE) as SpriteFrames
if not sprite_frames:
sprite_frames = SpriteFrames.new()
var sprite_frames: SpriteFrames
if ResourceLoader.exists(source_file):
# This is a working way to reuse a previously imported resource. Don't change it!
sprite_frames = ResourceLoader.load(source_file, "SpriteFrames", ResourceLoader.CACHE_MODE_REPLACE) as SpriteFrames
if not sprite_frames:
sprite_frames = SpriteFrames.new()
status = update_sprite_frames(export_result, sprite_frames)
if status:
push_error("Cannot update SpriteFrames", status)
return status
status = update_sprite_frames(export_result, sprite_frames)
if status:
push_error("Cannot update SpriteFrames", status)
return status
status = ResourceSaver.save(
sprite_frames,
save_path + "." + _get_save_extension(),
ResourceSaver.FLAG_COMPRESS)
if status:
push_error("Can't save imported resource.", status)
else:
var sprite_frames_res = ResourceLoader.load(save_path + "." + _get_save_extension()) as SpriteFrames
Util.refresh_all_animation_by_sprite_frames(sprite_frames_res)
return status
status = ResourceSaver.save(
sprite_frames,
save_path + "." + _get_save_extension(),
ResourceSaver.FLAG_COMPRESS)
if status:
push_error("Can't save imported resource.", status)
else:
var sprite_frames_res = ResourceLoader.load(save_path + "." + _get_save_extension()) as SpriteFrames
Util.refresh_all_animation_by_sprite_frames(sprite_frames_res)
return status
static func update_sprite_frames(export_result: ExportResult, sprite_frames: SpriteFrames, animation_autoplay_name: String = "") -> Error:
var spritesheet_metadata: SpritesheetMetadata = export_result.spritesheet_metadata
var exported_animation_names: Array = export_result.spritesheet_metadata.animation_tags.map(
func (at: AnimationTag) -> String: return at.name)
var actual_animation_names: PackedStringArray = sprite_frames.get_animation_names()
for name in actual_animation_names:
if exported_animation_names.has(name):
sprite_frames.clear(name)
else:
sprite_frames.remove_animation(name)
var atlas_textures: Dictionary = {}
for animation_tag in spritesheet_metadata.animation_tags:
if not sprite_frames.has_animation(animation_tag.name):
sprite_frames.add_animation(animation_tag.name)
sprite_frames.set_animation_loop(animation_tag.name, animation_tag.looped)
sprite_frames.set_animation_speed(animation_tag.name, 1)
for frame_data in animation_tag.frames:
var atlas_texture = atlas_textures.get(frame_data.region_rect)
if atlas_texture == null:
atlas_texture = AtlasTexture.new()
atlas_texture.atlas = export_result.texture
atlas_texture.region = frame_data.region_rect
atlas_texture.margin = Rect2(frame_data.region_rect_offset, spritesheet_metadata.source_size - frame_data.region_rect.size)
atlas_textures[frame_data.region_rect] = atlas_texture
var spritesheet_metadata: SpritesheetMetadata = export_result.spritesheet_metadata
var exported_animation_names: Array = export_result.spritesheet_metadata.animation_tags.map(
func (at: AnimationTag) -> String: return at.name)
var actual_animation_names: PackedStringArray = sprite_frames.get_animation_names()
for name in actual_animation_names:
if exported_animation_names.has(name):
sprite_frames.clear(name)
else:
sprite_frames.remove_animation(name)
var atlas_textures: Dictionary = {}
for animation_tag in spritesheet_metadata.animation_tags:
if not sprite_frames.has_animation(animation_tag.name):
sprite_frames.add_animation(animation_tag.name)
sprite_frames.set_animation_loop(animation_tag.name, animation_tag.looped)
sprite_frames.set_animation_speed(animation_tag.name, 1)
for frame_data in animation_tag.frames:
var atlas_texture = atlas_textures.get(frame_data.region_rect)
if atlas_texture == null:
atlas_texture = AtlasTexture.new()
atlas_texture.atlas = export_result.texture
atlas_texture.region = frame_data.region_rect
atlas_texture.margin = Rect2(frame_data.region_rect_offset, spritesheet_metadata.source_size - frame_data.region_rect.size)
atlas_textures[frame_data.region_rect] = atlas_texture
sprite_frames.add_frame(animation_tag.name, atlas_texture, Setting.animation_frame_rate)
return OK
sprite_frames.add_frame(animation_tag.name, atlas_texture, Setting.animation_frame_rate)
return OK

@ -2,35 +2,35 @@
extends EditorPlugin
const PLUGIN_SCRIPTS: Array[GDScript] = [
preload("editor_import_plugins/sprite_frames.gd"),
]
const Common = preload("common.gd")
preload("editor_import_plugins/sprite_frames.gd"),
]
const Common = preload("common.gd")
const ImportPlugin = preload("editor_import_plugins/_animation_importer_base.gd")
var __import_plugins: Array[ImportPlugin]
var common_options: Array[Dictionary] = Common.create_common_animation_options()
func _enter_tree() -> void:
__register_project_setting(
Common.ASEPRITE_EXECUTABLE_PATH_SETTING_NAME, "",
TYPE_STRING, PROPERTY_HINT_GLOBAL_FILE, "*.exe")
__register_project_setting(
Common.ASEPRITE_EXECUTABLE_PATH_SETTING_NAME, "",
TYPE_STRING, PROPERTY_HINT_GLOBAL_FILE, "*.exe")
for plugin_script in PLUGIN_SCRIPTS:
var import_plugin = plugin_script.new(self)
add_import_plugin(import_plugin)
__import_plugins.append(import_plugin)
for plugin_script in PLUGIN_SCRIPTS:
var import_plugin = plugin_script.new(self)
add_import_plugin(import_plugin)
__import_plugins.append(import_plugin)
func _exit_tree() -> void:
for import_plugin in __import_plugins:
remove_import_plugin(import_plugin)
__import_plugins.clear()
for import_plugin in __import_plugins:
remove_import_plugin(import_plugin)
__import_plugins.clear()
func __register_project_setting(name: StringName, initial_value, type: int, hint: int, hint_string: String = "") -> void:
if not ProjectSettings.has_setting(name):
ProjectSettings.set_setting(name, initial_value)
ProjectSettings.set_initial_value(name, initial_value)
var property_info: Dictionary = { name = name, type = type, hint = hint }
if hint_string: property_info.hint_string = hint_string
ProjectSettings.add_property_info(property_info)
if not ProjectSettings.has_setting(name):
ProjectSettings.set_setting(name, initial_value)
ProjectSettings.set_initial_value(name, initial_value)
var property_info: Dictionary = { name = name, type = type, hint = hint }
if hint_string: property_info.hint_string = hint_string
ProjectSettings.add_property_info(property_info)

@ -1,7 +1,7 @@
[plugin]
name="Aseprite Importers"
description="A bundle of plug-ins for importing Aesprite files into different types of Godot resources"
author="Nikolay Lebedev aka nklbdev"
version="1.0.0"
script="editor_plugin.gd"
name = "Aseprite Importers"
description = "A bundle of plug-ins for importing Aesprite files into different types of Godot resources"
author = "Nikolay Lebedev aka nklbdev"
version = "1.0.0"
script = "editor_plugin.gd"

@ -2,8 +2,15 @@
Copyright 2022 Gennady "Don Tnowe" Krupenyov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,6 +1,5 @@
@tool
extends Control
func _ready():
modulate = get_theme_color("accent_color", "Editor")
modulate = get_theme_color("accent_color", "Editor")

@ -1,12 +1,12 @@
@tool
extends Button
@export var icon_name := "Node" :
set(v):
icon_name = v
if has_theme_icon(v, "EditorIcons"):
icon = get_theme_icon(v, "EditorIcons")
@export var icon_name := "Node":
set(v):
icon_name = v
if has_theme_icon(v, "EditorIcons"):
icon = get_theme_icon(v, "EditorIcons")
func _ready():
self.icon_name = (icon_name)
self.icon_name = (icon_name)

@ -2,420 +2,416 @@
extends Control
signal grid_updated()
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
@export @onready var node_folder_path: LineEdit = $"HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/Path"
@export @onready var node_recent_paths: OptionButton = $"HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2/RecentPaths"
@export @onready var node_table_root: GridContainer = $"HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid"
@export @onready var node_columns: HBoxContainer = $"HeaderContentSplit/VBoxContainer/Columns/Columns"
@export @onready var node_page_manager: Control = $"HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"
@export @onready var node_folder_path : LineEdit = $"HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/Path"
@export @onready var node_recent_paths : OptionButton = $"HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2/RecentPaths"
@export @onready var node_table_root : GridContainer = $"HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid"
@export @onready var node_columns : HBoxContainer = $"HeaderContentSplit/VBoxContainer/Columns/Columns"
@export @onready var node_page_manager : Control = $"HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"
@onready var _on_cell_gui_input : Callable = $"InputHandler"._on_cell_gui_input
@onready var _on_cell_gui_input: Callable = $"InputHandler"._on_cell_gui_input
@onready var _selection := $"SelectionManager"
var editor_interface : Object
var editor_plugin : EditorPlugin
var current_path := ""
var save_data_path : String = get_script().resource_path.get_base_dir() + "/saved_state.json"
var sorting_by := ""
var sorting_reverse := false
var columns := []
var column_types := []
var column_hints := []
var column_hint_strings := []
var rows := []
var remembered_paths := {}
var editor_interface: Object
var editor_plugin: EditorPlugin
var current_path := ""
var save_data_path: String = get_script().resource_path.get_base_dir() + "/saved_state.json"
var sorting_by := ""
var sorting_reverse := false
var columns := []
var column_types := []
var column_hints := []
var column_hint_strings := []
var rows := []
var remembered_paths := {}
var remembered_paths_total_count := 0
var table_functions_dict := {}
var search_cond : RefCounted
var io : RefCounted
var table_functions_dict := {}
var search_cond: RefCounted
var io: RefCounted
var first_row := 0
var last_row := 0
var last_row := 0
func _ready():
editor_interface.get_resource_filesystem().filesystem_changed.connect(_on_filesystem_changed)
if FileAccess.file_exists(save_data_path):
var file = FileAccess.open(save_data_path, FileAccess.READ)
var as_text = file.get_as_text()
var as_var = JSON.parse_string(as_text)
editor_interface.get_resource_filesystem().filesystem_changed.connect(_on_filesystem_changed)
if FileAccess.file_exists(save_data_path):
var file = FileAccess.open(save_data_path, FileAccess.READ)
var as_text = file.get_as_text()
var as_var = JSON.parse_string(as_text)
node_recent_paths.load_paths(as_var.get("recent_paths", []))
node_columns.hidden_columns = as_var.get("hidden_columns", {})
table_functions_dict = as_var.get("table_functions", {})
for x in $"HeaderContentSplit/VBoxContainer/Search/Search".get_children():
if x.has_method(&"load_saved_functions"):
x.load_saved_functions(table_functions_dict)
node_recent_paths.load_paths(as_var.get("recent_paths", []))
node_columns.hidden_columns = as_var.get("hidden_columns", {})
table_functions_dict = as_var.get("table_functions", {})
for x in $"HeaderContentSplit/VBoxContainer/Search/Search".get_children():
if x.has_method(&"load_saved_functions"):
x.load_saved_functions(table_functions_dict)
if node_recent_paths.recent_paths.size() >= 1:
display_folder(node_recent_paths.recent_paths[0], "resource_name", false, true)
if node_recent_paths.recent_paths.size() >= 1:
display_folder(node_recent_paths.recent_paths[0], "resource_name", false, true)
func _on_filesystem_changed():
var file_total_count := _get_file_count_recursive(current_path)
if file_total_count != remembered_paths_total_count:
refresh()
var file_total_count := _get_file_count_recursive(current_path)
if file_total_count != remembered_paths_total_count:
refresh()
else:
for k in remembered_paths:
if !is_instance_valid(remembered_paths[k]):
continue
else:
for k in remembered_paths:
if !is_instance_valid(remembered_paths[k]):
continue
if remembered_paths[k].resource_path != k:
var res = remembered_paths[k]
remembered_paths.erase(k)
remembered_paths[res.resource_path] = res
refresh()
break
if remembered_paths[k].resource_path != k:
var res = remembered_paths[k]
remembered_paths.erase(k)
remembered_paths[res.resource_path] = res
refresh()
break
func _get_file_count_recursive(path : String) -> int:
var editor_fs : EditorFileSystem = editor_interface.get_resource_filesystem()
var path_dir := editor_fs.get_filesystem_path(path)
if !path_dir: return 0
func _get_file_count_recursive(path: String) -> int:
var editor_fs: EditorFileSystem = editor_interface.get_resource_filesystem()
var path_dir := editor_fs.get_filesystem_path(path)
if !path_dir: return 0
var file_total_count := 0
var folder_stack : Array[EditorFileSystemDirectory] = [path_dir]
while folder_stack.size() > 0:
path_dir = folder_stack.pop_back()
file_total_count += path_dir.get_file_count()
for i in path_dir.get_subdir_count():
folder_stack.append(path_dir.get_subdir(i))
var file_total_count := 0
var folder_stack: Array[EditorFileSystemDirectory] = [path_dir]
while folder_stack.size() > 0:
path_dir = folder_stack.pop_back()
file_total_count += path_dir.get_file_count()
for i in path_dir.get_subdir_count():
folder_stack.append(path_dir.get_subdir(i))
return file_total_count
return file_total_count
func display_folder(folderpath : String, sort_by : String = "", sort_reverse : bool = false, force_rebuild : bool = false, is_echo : bool = false):
if folderpath == "": return # Root folder resources tend to have MANY properties.
$"HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Label".visible = false
if folderpath.get_extension() == "":
folderpath = folderpath.trim_suffix("/") + "/"
func display_folder(folderpath: String, sort_by: String = "", sort_reverse: bool = false, force_rebuild: bool = false, is_echo: bool = false):
if folderpath == "": return # Root folder resources tend to have MANY properties.
$"HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Label".visible = false
if folderpath.get_extension() == "":
folderpath = folderpath.trim_suffix("/") + "/"
if folderpath.ends_with(".tres") and !folderpath.ends_with(ResourceTablesImport.SUFFIX):
folderpath = folderpath.get_base_dir() + "/"
if folderpath.ends_with(".tres") and !folderpath.ends_with(ResourceTablesImport.SUFFIX):
folderpath = folderpath.get_base_dir() + "/"
node_recent_paths.add_path_to_recent(folderpath)
first_row = node_page_manager.first_row
_load_resources_from_path(folderpath, sort_by, sort_reverse)
last_row = min(first_row + node_page_manager.rows_per_page, rows.size())
node_recent_paths.add_path_to_recent(folderpath)
first_row = node_page_manager.first_row
_load_resources_from_path(folderpath, sort_by, sort_reverse)
last_row = min(first_row + node_page_manager.rows_per_page, rows.size())
if columns.size() == 0: return
if columns.size() == 0: return
node_folder_path.text = folderpath
_update_table(
force_rebuild
or current_path != folderpath
or columns.size() != node_columns.get_child_count()
)
current_path = folderpath
remembered_paths_total_count = _get_file_count_recursive(folderpath)
node_columns.update()
grid_updated.emit()
node_folder_path.text = folderpath
_update_table(
force_rebuild
or current_path != folderpath
or columns.size() != node_columns.get_child_count()
)
current_path = folderpath
remembered_paths_total_count = _get_file_count_recursive(folderpath)
node_columns.update()
grid_updated.emit()
func refresh(force_rebuild : bool = true):
display_folder(current_path, sorting_by, sorting_reverse, force_rebuild)
func refresh(force_rebuild: bool = true):
display_folder(current_path, sorting_by, sorting_reverse, force_rebuild)
func _load_resources_from_path(path : String, sort_by : String, sort_reverse : bool):
if path.ends_with("/"):
io = ResourceTablesEditFormatTres.new()
func _load_resources_from_path(path: String, sort_by: String, sort_reverse: bool):
if path.ends_with("/"):
io = ResourceTablesEditFormatTres.new()
else:
var loaded = load(path)
if loaded is ResourceTablesImport:
io = loaded.view_script.new()
else:
var loaded = load(path)
if loaded is ResourceTablesImport:
io = loaded.view_script.new()
else:
io = ResourceTablesEditFormatTres.new()
io.editor_view = self
remembered_paths.clear()
rows = io.import_from_path(path, insert_row_sorted, sort_by, sort_reverse)
else:
io = ResourceTablesEditFormatTres.new()
io.editor_view = self
remembered_paths.clear()
rows = io.import_from_path(path, insert_row_sorted, sort_by, sort_reverse)
func fill_property_data(res):
columns.clear()
column_types.clear()
column_hints.clear()
column_hint_strings.clear()
var column_values := []
var i := -1
for x in res.get_property_list():
if x["usage"] & PROPERTY_USAGE_EDITOR != 0 and x["name"] != "script":
i += 1
columns.append(x["name"])
column_types.append(x["type"])
column_hints.append(x["hint"])
column_hint_strings.append(x["hint_string"].split(","))
column_values.append(io.get_value(res, columns[i]))
_selection.initialize_editors(column_values, column_types, column_hints)
func insert_row_sorted(res : Resource, rows : Array, sort_by : String, sort_reverse : bool):
if search_cond != null and !search_cond.can_show(res, rows.size()):
return
for i in rows.size():
if sort_reverse == compare_values(io.get_value(res, sort_by), io.get_value(rows[i], sort_by)):
rows.insert(i, res)
return
remembered_paths[res.resource_path] = res
rows.append(res)
columns.clear()
column_types.clear()
column_hints.clear()
column_hint_strings.clear()
var column_values := []
var i := -1
for x in res.get_property_list():
if x["usage"] & PROPERTY_USAGE_EDITOR != 0 and x["name"] != "script":
i += 1
columns.append(x["name"])
column_types.append(x["type"])
column_hints.append(x["hint"])
column_hint_strings.append(x["hint_string"].split(","))
column_values.append(io.get_value(res, columns[i]))
_selection.initialize_editors(column_values, column_types, column_hints)
func insert_row_sorted(res: Resource, rows: Array, sort_by: String, sort_reverse: bool):
if search_cond != null and !search_cond.can_show(res, rows.size()):
return
for i in rows.size():
if sort_reverse == compare_values(io.get_value(res, sort_by), io.get_value(rows[i], sort_by)):
rows.insert(i, res)
return
remembered_paths[res.resource_path] = res
rows.append(res)
func compare_values(a, b) -> bool:
if a == null or b == null: return b == null
if a is Color:
return a.h > b.h if a.h != b.h else a.v > b.v
if a == null or b == null: return b == null
if a is Color:
return a.h > b.h if a.h != b.h else a.v > b.v
if a is Resource:
return a.resource_path > b.resource_path
if a is Array:
return a.size() > b.size()
return a > b
if a is Resource:
return a.resource_path > b.resource_path
if a is Array:
return a.size() > b.size()
return a > b
func _set_sorting(sort_by):
var sort_reverse : bool = !(sorting_by != sort_by or sorting_reverse)
sorting_reverse = sort_reverse
display_folder(current_path, sort_by, sort_reverse)
sorting_by = sort_by
func _update_table(columns_changed : bool):
if columns_changed:
node_table_root.columns = columns.size()
for x in node_table_root.get_children():
x.free()
node_columns.columns = columns
var to_free = node_table_root.get_child_count() - (last_row - first_row) * columns.size()
while to_free > 0:
node_table_root.get_child(0).free()
to_free -= 1
var color_rows = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "color_rows")
_update_row_range(
first_row,
last_row,
color_rows
)
func _update_row_range(first : int, last : int, color_rows : bool):
for i in last - first:
_update_row(first + i, color_rows)
func _update_row(row_index : int, color_rows : bool = true):
var current_node : Control
var next_color := Color.WHITE
var column_editors : Array = _selection.column_editors
var res_path : String = rows[row_index].resource_path.get_basename().substr(current_path.length())
for i in columns.size():
if node_table_root.get_child_count() <= (row_index - first_row) * columns.size() + i:
current_node = column_editors[i].create_cell(self)
current_node.gui_input.connect(_on_cell_gui_input.bind(current_node))
node_table_root.add_child(current_node)
else:
current_node = node_table_root.get_child((row_index - first_row) * columns.size() + i)
current_node.tooltip_text = (
columns[i].capitalize()
+ "\n---\n"
+ "Of " + res_path
)
column_editors[i].set_value(current_node, io.get_value(rows[row_index], columns[i]))
if columns[i] == "resource_path":
column_editors[i].set_value(current_node, res_path)
if color_rows and column_types[i] == TYPE_COLOR:
next_color = io.get_value(rows[row_index], columns[i])
column_editors[i].set_color(current_node, next_color)
var sort_reverse: bool = !(sorting_by != sort_by or sorting_reverse)
sorting_reverse = sort_reverse
display_folder(current_path, sort_by, sort_reverse)
sorting_by = sort_by
func _update_table(columns_changed: bool):
if columns_changed:
node_table_root.columns = columns.size()
for x in node_table_root.get_children():
x.free()
node_columns.columns = columns
var to_free = node_table_root.get_child_count() - (last_row - first_row) * columns.size()
while to_free > 0:
node_table_root.get_child(0).free()
to_free -= 1
var color_rows = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "color_rows")
_update_row_range(
first_row,
last_row,
color_rows
)
func _update_row_range(first: int, last: int, color_rows: bool):
for i in last - first:
_update_row(first + i, color_rows)
func _update_row(row_index: int, color_rows: bool = true):
var current_node: Control
var next_color := Color.WHITE
var column_editors: Array = _selection.column_editors
var res_path: String = rows[row_index].resource_path.get_basename().substr(current_path.length())
for i in columns.size():
if node_table_root.get_child_count() <= (row_index - first_row) * columns.size() + i:
current_node = column_editors[i].create_cell(self)
current_node.gui_input.connect(_on_cell_gui_input.bind(current_node))
node_table_root.add_child(current_node)
else:
current_node = node_table_root.get_child((row_index - first_row) * columns.size() + i)
current_node.tooltip_text = (
columns[i].capitalize()
+ "\n---\n"
+ "Of " + res_path
)
column_editors[i].set_value(current_node, io.get_value(rows[row_index], columns[i]))
if columns[i] == "resource_path":
column_editors[i].set_value(current_node, res_path)
if color_rows and column_types[i] == TYPE_COLOR:
next_color = io.get_value(rows[row_index], columns[i])
column_editors[i].set_color(current_node, next_color)
func save_data():
var file = FileAccess.open(save_data_path, FileAccess.WRITE)
file.store_string(JSON.stringify(
{
"recent_paths" : node_recent_paths.recent_paths,
"hidden_columns" : node_columns.hidden_columns,
"table_functions" : table_functions_dict,
}
, " "))
var file = FileAccess.open(save_data_path, FileAccess.WRITE)
file.store_string(JSON.stringify(
{
"recent_paths": node_recent_paths.recent_paths,
"hidden_columns": node_columns.hidden_columns,
"table_functions": table_functions_dict,
}
, " "))
func _on_path_text_submitted(new_text : String = ""):
if new_text != "":
current_path = new_text
display_folder(new_text, "", false, true)
func _on_path_text_submitted(new_text: String = ""):
if new_text != "":
current_path = new_text
display_folder(new_text, "", false, true)
else:
refresh()
else:
refresh()
func _on_FileDialog_dir_selected(dir : String):
node_folder_path.text = dir
display_folder(dir)
func _on_FileDialog_dir_selected(dir: String):
node_folder_path.text = dir
display_folder(dir)
func get_selected_column() -> int:
return _selection.get_cell_column(_selection.edited_cells[0])
func select_column(column_index : int):
_selection.deselect_all_cells()
_selection.select_cell(node_table_root.get_child(column_index))
_selection.select_cells_to(node_table_root.get_child(
column_index + columns.size()
* (last_row - first_row - 1))
)
func set_edited_cells_values(new_cell_values : Array):
var edited_rows = _selection.get_edited_rows()
var column = _selection.get_cell_column(_selection.edited_cells[0])
var edited_cells_resources = _get_row_resources(edited_rows)
# Duplicated here since if using text editing, edited_cells_text needs to modified
# but here, it would be converted from a String breaking editing
new_cell_values = new_cell_values.duplicate()
editor_plugin.undo_redo.create_action("Set Cell Values")
editor_plugin.undo_redo.add_undo_method(
self,
&"_update_resources",
edited_cells_resources.duplicate(),
edited_rows.duplicate(),
column,
get_edited_cells_values()
)
editor_plugin.undo_redo.add_undo_method(
_selection,
&"_update_selected_cells_text"
)
editor_plugin.undo_redo.add_do_method(
self,
&"_update_resources",
edited_cells_resources.duplicate(),
edited_rows.duplicate(),
column,
new_cell_values.duplicate()
)
editor_plugin.undo_redo.commit_action(true)
# editor_interface.get_resource_filesystem().scan()
return _selection.get_cell_column(_selection.edited_cells[0])
func select_column(column_index: int):
_selection.deselect_all_cells()
_selection.select_cell(node_table_root.get_child(column_index))
_selection.select_cells_to(node_table_root.get_child(
column_index + columns.size()
* (last_row - first_row - 1))
)
func set_edited_cells_values(new_cell_values: Array):
var edited_rows = _selection.get_edited_rows()
var column = _selection.get_cell_column(_selection.edited_cells[0])
var edited_cells_resources = _get_row_resources(edited_rows)
# Duplicated here since if using text editing, edited_cells_text needs to modified
# but here, it would be converted from a String breaking editing
new_cell_values = new_cell_values.duplicate()
editor_plugin.undo_redo.create_action("Set Cell Values")
editor_plugin.undo_redo.add_undo_method(
self,
&"_update_resources",
edited_cells_resources.duplicate(),
edited_rows.duplicate(),
column,
get_edited_cells_values()
)
editor_plugin.undo_redo.add_undo_method(
_selection,
&"_update_selected_cells_text"
)
editor_plugin.undo_redo.add_do_method(
self,
&"_update_resources",
edited_cells_resources.duplicate(),
edited_rows.duplicate(),
column,
new_cell_values.duplicate()
)
editor_plugin.undo_redo.commit_action(true)
# editor_interface.get_resource_filesystem().scan()
func rename_row(row, new_name):
if !has_row_names(): return
io.rename_row(row, new_name)
refresh()
if !has_row_names(): return
io.rename_row(row, new_name)
refresh()
func duplicate_selected_rows(new_name : String):
io.duplicate_rows(_get_row_resources(_selection.get_edited_rows()), new_name)
refresh()
func duplicate_selected_rows(new_name: String):
io.duplicate_rows(_get_row_resources(_selection.get_edited_rows()), new_name)
refresh()
func delete_selected_rows():
io.delete_rows(_get_row_resources(_selection.get_edited_rows()))
refresh()
refresh.call_deferred()
io.delete_rows(_get_row_resources(_selection.get_edited_rows()))
refresh()
refresh.call_deferred()
func has_row_names():
return io.has_row_names()
return io.has_row_names()
func get_last_selected_row():
return rows[_selection.get_cell_row(_selection.edited_cells[-1])]
return rows[_selection.get_cell_row(_selection.edited_cells[-1])]
func get_edited_cells_values() -> Array:
var cells : Array = _selection.edited_cells.duplicate()
var column_index : int = _selection.get_cell_column(_selection.edited_cells[0])
var cell_editor : Object = _selection.column_editors[column_index]
var result := []
result.resize(cells.size())
for i in cells.size():
result[i] = io.get_value(rows[_selection.get_cell_row(cells[i])], columns[column_index])
return result
func _update_resources(update_rows : Array, update_row_indices : Array[int], update_column : int, values : Array):
var column_editor = _selection.column_editors[update_column]
for i in update_rows.size():
var row = update_row_indices[i]
var update_cell = node_table_root.get_child((row - first_row) * columns.size() + update_column)
column_editor.set_value(update_cell, values[i])
if values[i] is String:
values[i] = try_convert(values[i], column_types[update_column])
if values[i] == null:
continue
io.set_value(
update_rows[i],
columns[update_column],
values[i],
row
)
continue
if column_types[update_column] == TYPE_COLOR:
for j in columns.size() - update_column:
if j != 0 and column_types[j + update_column] == TYPE_COLOR:
break
_selection.column_editors[j + update_column].set_color(
update_cell.get_parent().get_child(
row * columns.size() + update_column + j - first_row
),
values[i]
)
node_columns._update_column_sizes()
io.save_entries(rows, update_row_indices)
var cells: Array = _selection.edited_cells.duplicate()
var column_index: int = _selection.get_cell_column(_selection.edited_cells[0])
var cell_editor: Object = _selection.column_editors[column_index]
var result := []
result.resize(cells.size())
for i in cells.size():
result[i] = io.get_value(rows[_selection.get_cell_row(cells[i])], columns[column_index])
return result
func _update_resources(update_rows: Array, update_row_indices: Array[int], update_column: int, values: Array):
var column_editor = _selection.column_editors[update_column]
for i in update_rows.size():
var row = update_row_indices[i]
var update_cell = node_table_root.get_child((row - first_row) * columns.size() + update_column)
column_editor.set_value(update_cell, values[i])
if values[i] is String:
values[i] = try_convert(values[i], column_types[update_column])
if values[i] == null:
continue
io.set_value(
update_rows[i],
columns[update_column],
values[i],
row
)
continue
if column_types[update_column] == TYPE_COLOR:
for j in columns.size() - update_column:
if j != 0 and column_types[j + update_column] == TYPE_COLOR:
break
_selection.column_editors[j + update_column].set_color(
update_cell.get_parent().get_child(
row * columns.size() + update_column + j - first_row
),
values[i]
)
node_columns._update_column_sizes()
io.save_entries(rows, update_row_indices)
func try_convert(value, type):
if type == TYPE_BOOL:
# "off" displayed in lowercase, "ON" in uppercase.
return value[0] == "o"
if type == TYPE_BOOL:
# "off" displayed in lowercase, "ON" in uppercase.
return value[0] == "o"
# If it can't convert, throws exception and returns null.
return convert(value, type)
# If it can't convert, throws exception and returns null.
return convert(value, type)
func _get_row_resources(row_indices) -> Array:
var arr := []
arr.resize(row_indices.size())
for i in arr.size():
arr[i] = rows[row_indices[i]]
var arr := []
arr.resize(row_indices.size())
for i in arr.size():
arr[i] = rows[row_indices[i]]
return arr
return arr
func _on_File_pressed():
node_folder_path.get_parent().get_parent().visible = !node_folder_path.get_parent().get_parent().visible
node_folder_path.get_parent().get_parent().visible = !node_folder_path.get_parent().get_parent().visible
func _on_SearchProcess_pressed():
$"HeaderContentSplit/VBoxContainer/Search".visible = !$"HeaderContentSplit/VBoxContainer/Search".visible
$"HeaderContentSplit/VBoxContainer/Search".visible = !$"HeaderContentSplit/VBoxContainer/Search".visible

@ -1,36 +1,44 @@
class_name ResourceTablesEditFormat
extends RefCounted
var editor_view : Control
var editor_view: Control
## Override to define reading behaviour.
func get_value(entry, key : String):
pass
func get_value(entry, key: String):
pass
## Override to define writing behaviour. This is NOT supposed to save - use `save_entries`.
func set_value(entry, key : String, value, index : int):
pass
func set_value(entry, key: String, value, index: int):
pass
## Override to define how the data gets saved.
func save_entries(all_entries : Array, indices : Array):
pass
func save_entries(all_entries: Array, indices: Array):
pass
## Override to allow editing rows from the Inspector.
func create_resource(entry) -> Resource:
return Resource.new()
return Resource.new()
## Override to define duplication behaviour. `name_input` should be a suffix if multiple entries, and full name if one.
func duplicate_rows(rows : Array, name_input : String):
pass
func duplicate_rows(rows: Array, name_input: String):
pass
## Override to define removal behaviour.
func delete_rows(rows : Array):
pass
func delete_rows(rows: Array):
pass
## Override with `return true` if `resource_path` is defined and the Rename butoon should show.
func has_row_names():
return false
return false
## Override to define import behaviour. Must return the `rows` value for the editor view.
func import_from_path(folderpath : String, insert_func : Callable, sort_by : String, sort_reverse : bool = false) -> Array:
return []
func import_from_path(folderpath: String, insert_func: Callable, sort_by: String, sort_reverse: bool = false) -> Array:
return []

@ -2,87 +2,87 @@ class_name ResourceTablesEditFormatCsv
extends ResourceTablesEditFormatTres
var import_data
var csv_rows = []
var csv_rows = []
var resource_original_positions = {}
func get_value(entry, key : String):
return entry.get(key)
func get_value(entry, key: String):
return entry.get(key)
func set_value(entry, key : String, value, index : int):
entry.set(key, value)
csv_rows[resource_original_positions[entry]] = import_data.resource_to_strings(entry)
func set_value(entry, key: String, value, index: int):
entry.set(key, value)
csv_rows[resource_original_positions[entry]] = import_data.resource_to_strings(entry)
func save_entries(all_entries : Array, indices : Array, repeat : bool = true):
if timer == null or timer.time_left <= 0.0:
var space_after_delimeter = import_data.delimeter.ends_with(" ")
var file = FileAccess.open(import_data.edited_path, FileAccess.WRITE)
for x in csv_rows:
if space_after_delimeter:
for i in x.size():
if i == 0: continue
x[i] = " " + x[i]
func save_entries(all_entries: Array, indices: Array, repeat: bool = true):
if timer == null or timer.time_left <= 0.0:
var space_after_delimeter = import_data.delimeter.ends_with(" ")
var file = FileAccess.open(import_data.edited_path, FileAccess.WRITE)
for x in csv_rows:
if space_after_delimeter:
for i in x.size():
if i == 0: continue
x[i] = " " + x[i]
file.store_csv_line(x, import_data.delimeter[0])
file.store_csv_line(x, import_data.delimeter[0])
if repeat:
timer = editor_view.get_tree().create_timer(3.0)
timer.timeout.connect(save_entries.bind(all_entries, indices, false))
if repeat:
timer = editor_view.get_tree().create_timer(3.0)
timer.timeout.connect(save_entries.bind(all_entries, indices, false))
func create_resource(entry) -> Resource:
return entry
return entry
func duplicate_rows(rows : Array, name_input : String):
for x in rows:
var new_res = x.duplicate()
var index = resource_original_positions[x]
csv_rows.insert(index, import_data.resource_to_strings(new_res))
_bump_row_indices(index + 1, 1)
resource_original_positions[new_res] = index + 1
func duplicate_rows(rows: Array, name_input: String):
for x in rows:
var new_res = x.duplicate()
var index = resource_original_positions[x]
csv_rows.insert(index, import_data.resource_to_strings(new_res))
_bump_row_indices(index + 1, 1)
resource_original_positions[new_res] = index + 1
save_entries([], [])
save_entries([], [])
func delete_rows(rows):
for x in rows:
var index = resource_original_positions[x]
csv_rows.remove(index)
_bump_row_indices(index, -1)
resource_original_positions.erase(x)
for x in rows:
var index = resource_original_positions[x]
csv_rows.remove(index)
_bump_row_indices(index, -1)
resource_original_positions.erase(x)
save_entries([], [])
save_entries([], [])
func has_row_names():
return false
return false
func _bump_row_indices(from : int, increment : int = 1):
for k in resource_original_positions:
if resource_original_positions[k] >= from:
resource_original_positions[k] += increment
func _bump_row_indices(from: int, increment: int = 1):
for k in resource_original_positions:
if resource_original_positions[k] >= from:
resource_original_positions[k] += increment
func import_from_path(path : String, insert_func : Callable, sort_by : String, sort_reverse : bool = false) -> Array:
import_data = load(path)
var file = FileAccess.open(import_data.edited_path, FileAccess.READ)
csv_rows = ResourceTablesImportFormatCsv.import_as_arrays(import_data)
func import_from_path(path: String, insert_func: Callable, sort_by: String, sort_reverse: bool = false) -> Array:
import_data = load(path)
var file = FileAccess.open(import_data.edited_path, FileAccess.READ)
csv_rows = ResourceTablesImportFormatCsv.import_as_arrays(import_data)
var rows := []
var res : Resource
resource_original_positions.clear()
for i in csv_rows.size():
if import_data.remove_first_row and i == 0:
continue
var rows := []
var res: Resource
resource_original_positions.clear()
for i in csv_rows.size():
if import_data.remove_first_row and i == 0:
continue
res = import_data.strings_to_resource(csv_rows[i])
res.resource_path = ""
insert_func.call(res, rows, sort_by, sort_reverse)
resource_original_positions[res] = i
res = import_data.strings_to_resource(csv_rows[i])
res.resource_path = ""
insert_func.call(res, rows, sort_by, sort_reverse)
resource_original_positions[res] = i
editor_view.fill_property_data(rows[0])
return rows
editor_view.fill_property_data(rows[0])
return rows

@ -1,113 +1,113 @@
class_name ResourceTablesEditFormatTres
extends ResourceTablesEditFormat
var timer : SceneTreeTimer
var timer: SceneTreeTimer
func get_value(entry, key : String):
return entry[key]
func get_value(entry, key: String):
return entry[key]
func set_value(entry, key : String, value, index : int):
entry[key] = value
func set_value(entry, key: String, value, index: int):
entry[key] = value
func save_entries(all_entries : Array, indices : Array, repeat : bool = true):
# No need to save. Resources are saved with Ctrl+S
# (likely because plugin.edit_resource is called to show inspector)
return
func save_entries(all_entries: Array, indices: Array, repeat: bool = true):
# No need to save. Resources are saved with Ctrl+S
# (likely because plugin.edit_resource is called to show inspector)
return
func create_resource(entry) -> Resource:
return entry
return entry
func duplicate_rows(rows : Array, name_input : String):
if rows.size() == 1:
var new_row = rows[0].duplicate()
new_row.resource_path = rows[0].resource_path.get_base_dir() + "/" + name_input + ".tres"
ResourceSaver.save(new_row)
return
func duplicate_rows(rows: Array, name_input: String):
if rows.size() == 1:
var new_row = rows[0].duplicate()
new_row.resource_path = rows[0].resource_path.get_base_dir() + "/" + name_input + ".tres"
ResourceSaver.save(new_row)
return
var new_row
for x in rows:
new_row = x.duplicate()
new_row.resource_path = x.resource_path.get_basename() + name_input + ".tres"
ResourceSaver.save(new_row)
var new_row
for x in rows:
new_row = x.duplicate()
new_row.resource_path = x.resource_path.get_basename() + name_input + ".tres"
ResourceSaver.save(new_row)
func rename_row(row, new_name : String):
var new_row = row
DirAccess.open("res://").remove(row.resource_path)
new_row.resource_path = row.resource_path.get_base_dir() + "/" + new_name + ".tres"
ResourceSaver.save(new_row)
func rename_row(row, new_name: String):
var new_row = row
DirAccess.open("res://").remove(row.resource_path)
new_row.resource_path = row.resource_path.get_base_dir() + "/" + new_name + ".tres"
ResourceSaver.save(new_row)
func delete_rows(rows):
for x in rows:
DirAccess.open("res://").remove(x.resource_path)
for x in rows:
DirAccess.open("res://").remove(x.resource_path)
func has_row_names():
return true
func import_from_path(folderpath : String, insert_func : Callable, sort_by : String, sort_reverse : bool = false) -> Array:
var rows := []
var dir := DirAccess.open(folderpath)
if dir == null: return []
var cur_dir_types : Dictionary = {}
var file_stack : Array[String] = []
var folder_stack : Array[String] = [folderpath]
while folder_stack.size() > 0:
folderpath = folder_stack.pop_back()
for x in DirAccess.get_files_at(folderpath):
file_stack.append(folderpath.path_join(x))
for x in DirAccess.get_directories_at(folderpath):
folder_stack.append(folderpath.path_join(x))
var loaded_res : Array[Resource] = []
var res : Resource = null
loaded_res.resize(file_stack.size())
for i in file_stack.size():
res = null
if file_stack[i].ends_with(".tres"):
res = load(file_stack[i])
loaded_res[i] = res
cur_dir_types[res.get_class()] = cur_dir_types.get(res.get_class(), 0) + 1
var res_script := res.get_script()
if res_script != null:
cur_dir_types[res_script] = cur_dir_types.get(res_script, 0) + 1
var most_count_key = null
var most_count_count := 0
var most_count_is_base_class := false
for k in cur_dir_types:
var v : int = cur_dir_types[k]
if v > most_count_count || (v >= most_count_count && most_count_is_base_class):
most_count_key = k
most_count_count = v
most_count_is_base_class = k is String
var first_loadable_found := false
for x in loaded_res:
if x == null: continue
if !first_loadable_found:
first_loadable_found = true
editor_view.fill_property_data(x)
if !(sort_by in x):
sort_by = "resource_path"
if most_count_is_base_class:
if x.get_class() == most_count_key:
insert_func.call(x, rows, sort_by, sort_reverse)
elif x.get_script() == most_count_key:
insert_func.call(x, rows, sort_by, sort_reverse)
return rows
return true
func import_from_path(folderpath: String, insert_func: Callable, sort_by: String, sort_reverse: bool = false) -> Array:
var rows := []
var dir := DirAccess.open(folderpath)
if dir == null: return []
var cur_dir_types: Dictionary = {}
var file_stack: Array[String] = []
var folder_stack: Array[String] = [folderpath]
while folder_stack.size() > 0:
folderpath = folder_stack.pop_back()
for x in DirAccess.get_files_at(folderpath):
file_stack.append(folderpath.path_join(x))
for x in DirAccess.get_directories_at(folderpath):
folder_stack.append(folderpath.path_join(x))
var loaded_res: Array[Resource] = []
var res: Resource = null
loaded_res.resize(file_stack.size())
for i in file_stack.size():
res = null
if file_stack[i].ends_with(".tres"):
res = load(file_stack[i])
loaded_res[i] = res
cur_dir_types[res.get_class()] = cur_dir_types.get(res.get_class(), 0) + 1
var res_script := res.get_script()
if res_script != null:
cur_dir_types[res_script] = cur_dir_types.get(res_script, 0) + 1
var most_count_key = null
var most_count_count := 0
var most_count_is_base_class := false
for k in cur_dir_types:
var v: int = cur_dir_types[k]
if v > most_count_count || (v >= most_count_count && most_count_is_base_class):
most_count_key = k
most_count_count = v
most_count_is_base_class = k is String
var first_loadable_found := false
for x in loaded_res:
if x == null: continue
if !first_loadable_found:
first_loadable_found = true
editor_view.fill_property_data(x)
if !(sort_by in x):
sort_by = "resource_path"
if most_count_is_base_class:
if x.get_class() == most_count_key:
insert_func.call(x, rows, sort_by, sort_reverse)
elif x.get_script() == most_count_key:
insert_func.call(x, rows, sort_by, sort_reverse)
return rows

@ -1,32 +1,31 @@
class_name ResourceTablesExportFormatCsv
extends RefCounted
static func can_edit_path(path: String):
return path.ends_with(".csv")
static func can_edit_path(path : String):
return path.ends_with(".csv")
static func export_to_file(entries_array: Array, column_names: Array, into_path: String, import_data):
var file = FileAccess.open(into_path, FileAccess.WRITE)
static func export_to_file(entries_array : Array, column_names : Array, into_path : String, import_data):
var file = FileAccess.open(into_path, FileAccess.WRITE)
var line = PackedStringArray()
var space_after_delimeter = import_data.delimeter.ends_with(" ")
import_data.prop_names = column_names
import_data.prop_types = import_data.get_resource_property_types(entries_array[0], column_names)
import_data.resource_path = ""
line.resize(column_names.size())
if import_data.remove_first_row:
for j in column_names.size():
line[j] = column_names[j]
if space_after_delimeter and j != 0:
line[j] = " " + line[j]
var line = PackedStringArray()
var space_after_delimeter = import_data.delimeter.ends_with(" ")
import_data.prop_names = column_names
import_data.prop_types = import_data.get_resource_property_types(entries_array[0], column_names)
import_data.resource_path = ""
line.resize(column_names.size())
if import_data.remove_first_row:
for j in column_names.size():
line[j] = column_names[j]
if space_after_delimeter and j != 0:
line[j] = " " + line[j]
file.store_csv_line(line, import_data.delimeter[0])
file.store_csv_line(line, import_data.delimeter[0])
for i in entries_array.size():
for j in column_names.size():
line[j] = import_data.property_to_string((entries_array[i].get(column_names[j])), j)
if space_after_delimeter and j != 0:
line[j] = " " + line[j]
for i in entries_array.size():
for j in column_names.size():
line[j] = import_data.property_to_string((entries_array[i].get(column_names[j])), j)
if space_after_delimeter and j != 0:
line[j] = " " + line[j]
file.store_csv_line(line, import_data.delimeter[0])
file.store_csv_line(line, import_data.delimeter[0])

@ -1,58 +1,57 @@
class_name ResourceTablesImportFormatCsv
extends RefCounted
static func can_edit_path(path : String):
return path.ends_with(".csv")
static func can_edit_path(path: String):
return path.ends_with(".csv")
static func get_properties(entries, import_data):
return Array(entries[0])
return Array(entries[0])
static func import_as_arrays(import_data) -> Array:
var file = FileAccess.open(import_data.edited_path, FileAccess.READ)
import_data.delimeter = ";"
var text_lines := [file.get_line().split(import_data.delimeter)]
var space_after_delimeter = false
var line = text_lines[0]
if line.size() == 0:
return []
if line.size() == 1:
import_data.delimeter = ","
line = line[0].split(import_data.delimeter)
text_lines[0] = line
if line.size() <= 1:
return []
if line[1].begins_with(" "):
for i in line.size():
line[i] = line[i].trim_prefix(" ")
text_lines[0] = line
space_after_delimeter = true
import_data.delimeter += " "
while !file.eof_reached():
line = file.get_csv_line(import_data.delimeter[0])
if space_after_delimeter:
for i in line.size():
line[i] = line[i].trim_prefix(" ")
if line.size() == text_lines[0].size():
text_lines.append(line)
elif line.size() != 1:
line.resize(text_lines[0].size())
text_lines.append(line)
var entries = []
entries.resize(text_lines.size())
for i in entries.size():
entries[i] = text_lines[i]
import_data.prop_names = entries[0]
return entries
var file = FileAccess.open(import_data.edited_path, FileAccess.READ)
import_data.delimeter = ";"
var text_lines := [file.get_line().split(import_data.delimeter)]
var space_after_delimeter = false
var line = text_lines[0]
if line.size() == 0:
return []
if line.size() == 1:
import_data.delimeter = ","
line = line[0].split(import_data.delimeter)
text_lines[0] = line
if line.size() <= 1:
return []
if line[1].begins_with(" "):
for i in line.size():
line[i] = line[i].trim_prefix(" ")
text_lines[0] = line
space_after_delimeter = true
import_data.delimeter += " "
while !file.eof_reached():
line = file.get_csv_line(import_data.delimeter[0])
if space_after_delimeter:
for i in line.size():
line[i] = line[i].trim_prefix(" ")
if line.size() == text_lines[0].size():
text_lines.append(line)
elif line.size() != 1:
line.resize(text_lines[0].size())
text_lines.append(line)
var entries = []
entries.resize(text_lines.size())
for i in entries.size():
entries[i] = text_lines[i]
import_data.prop_names = entries[0]
return entries

@ -1,9 +1,9 @@
@tool
extends Control
@export var prop_list_item_scene : PackedScene
@export var formats_export : Array[Script]
@export var formats_import : Array[Script]
@export var prop_list_item_scene: PackedScene
@export var formats_export: Array[Script]
@export var formats_import: Array[Script]
@onready var editor_view := $"../../.."
@onready var filename_options := $"Import/Margins/Scroll/Box/Grid/UseAsFilename"
@ -12,211 +12,210 @@ extends Control
@onready var file_dialog = $"../../FileDialogText"
var format_extension := ".csv"
var entries := []
var entries := []
var property_used_as_filename := 0
var import_data : ResourceTablesImport
var import_data: ResourceTablesImport
func _ready():
var create_file_button = Button.new()
file_dialog.get_child(3, true).get_child(3, true).add_child(create_file_button)
create_file_button.get_parent().move_child(create_file_button, 2)
create_file_button.text = "Create File"
create_file_button.visible = true
create_file_button.icon = get_theme_icon(&"New", &"EditorIcons")
create_file_button.pressed.connect(_on_create_file_pressed)
hide()
show()
get_parent().min_size = Vector2(600, 400)
get_parent().size = Vector2(600, 400)
var create_file_button = Button.new()
file_dialog.get_child(3, true).get_child(3, true).add_child(create_file_button)
create_file_button.get_parent().move_child(create_file_button, 2)
create_file_button.text = "Create File"
create_file_button.visible = true
create_file_button.icon = get_theme_icon(&"New", &"EditorIcons")
create_file_button.pressed.connect(_on_create_file_pressed)
hide()
show()
get_parent().min_size = Vector2(600, 400)
get_parent().size = Vector2(600, 400)
func _on_create_file_pressed():
var new_name = (
file_dialog.get_child(3, true).get_child(3, true).get_child(1, true).text
)
if new_name == "":
new_name += editor_view.current_path.get_base_dir().get_file()
var new_name = (
file_dialog.get_child(3, true).get_child(3, true).get_child(1, true).text
)
if new_name == "":
new_name += editor_view.current_path.get_base_dir().get_file()
var file = FileAccess.open((
file_dialog.get_child(3, true).get_child(0, true).get_child(6, true).text
+ "/"
+ new_name.get_basename() + format_extension
), FileAccess.WRITE)
file_dialog.invalidate()
var file = FileAccess.open((
file_dialog.get_child(3, true).get_child(0, true).get_child(6, true).text
+ "/"
+ new_name.get_basename() + format_extension
), FileAccess.WRITE)
file_dialog.invalidate()
func _on_file_selected(path : String):
import_data = ResourceTablesImport.new()
import_data.initialize(path)
_reset_controls()
await get_tree().process_frame
_open_dialog(path)
get_parent().popup_centered()
position = Vector2.ZERO
func _on_file_selected(path: String):
import_data = ResourceTablesImport.new()
import_data.initialize(path)
_reset_controls()
await get_tree().process_frame
_open_dialog(path)
get_parent().popup_centered()
position = Vector2.ZERO
func _open_dialog(path : String):
classname_field.text = import_data.edited_path.get_file().get_basename()\
.capitalize().replace(" ", "")
import_data.script_classname = classname_field.text
func _open_dialog(path: String):
classname_field.text = import_data.edited_path.get_file().get_basename()\
.capitalize().replace(" ", "")
import_data.script_classname = classname_field.text
for x in formats_import:
if x.new().can_edit_path(path):
entries = x.new().import_as_arrays(import_data)
for x in formats_import:
if x.new().can_edit_path(path):
entries = x.new().import_as_arrays(import_data)
_load_property_names(path)
_create_prop_editors()
$"Import/Margins/Scroll/Box/StyleSettingsI"._send_signal()
_load_property_names(path)
_create_prop_editors()
$"Import/Margins/Scroll/Box/StyleSettingsI"._send_signal()
func _load_property_names(path):
var prop_types = import_data.prop_types
prop_types.resize(import_data.prop_names.size())
prop_types.fill(4)
for i in import_data.prop_names.size():
import_data.prop_names[i] = entries[0][i]\
.replace("\"", "")\
.replace(" ", "_")\
.replace("-", "_")\
.replace(".", "_")\
.replace(",", "_")\
.replace("\t", "_")\
.replace("/", "_")\
.replace("\\", "_")\
.to_lower()
# Don't guess Ints automatically - further rows might have floats
if entries[1][i].is_valid_float():
prop_types[i] = ResourceTablesImport.PropType.FLOAT
elif entries[1][i].begins_with("res://"):
prop_types[i] = ResourceTablesImport.PropType.OBJECT
else:
prop_types[i] = ResourceTablesImport.PropType.STRING
filename_options.clear()
for i in import_data.prop_names.size():
filename_options.add_item(import_data.prop_names[i], i)
var prop_types = import_data.prop_types
prop_types.resize(import_data.prop_names.size())
prop_types.fill(4)
for i in import_data.prop_names.size():
import_data.prop_names[i] = entries[0][i]\
.replace("\"", "")\
.replace(" ", "_")\
.replace("-", "_")\
.replace(".", "_")\
.replace(",", "_")\
.replace("\t", "_")\
.replace("/", "_")\
.replace("\\", "_")\
.to_lower()
# Don't guess Ints automatically - further rows might have floats
if entries[1][i].is_valid_float():
prop_types[i] = ResourceTablesImport.PropType.FLOAT
elif entries[1][i].begins_with("res://"):
prop_types[i] = ResourceTablesImport.PropType.OBJECT
else:
prop_types[i] = ResourceTablesImport.PropType.STRING
filename_options.clear()
for i in import_data.prop_names.size():
filename_options.add_item(import_data.prop_names[i], i)
func _create_prop_editors():
for x in prop_list.get_children():
if !x is GridContainer: x.free()
for x in prop_list.get_children():
if !x is GridContainer: x.free()
for i in import_data.prop_names.size():
var new_node = prop_list_item_scene.instantiate()
prop_list.add_child(new_node)
new_node.display(import_data.prop_names[i], import_data.prop_types[i])
new_node.connect_all_signals(self, i)
for i in import_data.prop_names.size():
var new_node = prop_list_item_scene.instantiate()
prop_list.add_child(new_node)
new_node.display(import_data.prop_names[i], import_data.prop_types[i])
new_node.connect_all_signals(self, i)
func _generate_class(save_script = true):
save_script = true # Built-ins didn't work in 3.x, won't change because dont wanna test rn
import_data.new_script = import_data.generate_script(entries, save_script)
if save_script:
import_data.new_script.resource_path = import_data.edited_path.get_basename() + ".gd"
ResourceSaver.save(import_data.new_script)
# Because when instanced, objects have a copy of the script
import_data.new_script = load(import_data.edited_path.get_basename() + ".gd")
save_script = true # Built-ins didn't work in 3.x, won't change because dont wanna test rn
import_data.new_script = import_data.generate_script(entries, save_script)
if save_script:
import_data.new_script.resource_path = import_data.edited_path.get_basename() + ".gd"
ResourceSaver.save(import_data.new_script)
# Because when instanced, objects have a copy of the script
import_data.new_script = load(import_data.edited_path.get_basename() + ".gd")
func _export_tres_folder():
DirAccess.open("res://").make_dir_recursive(import_data.edited_path.get_basename())
DirAccess.open("res://").make_dir_recursive(import_data.edited_path.get_basename())
import_data.prop_used_as_filename = import_data.prop_names[property_used_as_filename]
var new_res : Resource
for i in entries.size():
if import_data.remove_first_row and i == 0:
continue
import_data.prop_used_as_filename = import_data.prop_names[property_used_as_filename]
var new_res: Resource
for i in entries.size():
if import_data.remove_first_row and i == 0:
continue
new_res = import_data.strings_to_resource(entries[i])
ResourceSaver.save(new_res)
new_res = import_data.strings_to_resource(entries[i])
ResourceSaver.save(new_res)
func _on_import_to_tres_pressed():
_generate_class()
_export_tres_folder()
await get_tree().process_frame
editor_view.display_folder(import_data.edited_path.get_basename() + "/")
await get_tree().process_frame
editor_view.refresh()
close()
_generate_class()
_export_tres_folder()
await get_tree().process_frame
editor_view.display_folder(import_data.edited_path.get_basename() + "/")
await get_tree().process_frame
editor_view.refresh()
close()
func _on_import_edit_pressed():
_generate_class(false)
import_data.prop_used_as_filename = ""
import_data.save()
await get_tree().process_frame
editor_view.display_folder(import_data.resource_path)
editor_view.node_columns.hidden_columns[editor_view.current_path] = {
"resource_path" : true,
"resource_local_to_scene" : true,
}
editor_view.save_data()
await get_tree().process_frame
editor_view.refresh()
close()
_generate_class(false)
import_data.prop_used_as_filename = ""
import_data.save()
await get_tree().process_frame
editor_view.display_folder(import_data.resource_path)
editor_view.node_columns.hidden_columns[editor_view.current_path] = {
"resource_path": true,
"resource_local_to_scene": true,
}
editor_view.save_data()
await get_tree().process_frame
editor_view.refresh()
close()
func _on_export_csv_pressed():
var exported_cols = editor_view.columns.duplicate()
exported_cols.erase("resource_local_to_scene")
for x in editor_view.node_columns.hidden_columns[editor_view.current_path].keys():
exported_cols.erase(x)
var exported_cols = editor_view.columns.duplicate()
exported_cols.erase("resource_local_to_scene")
for x in editor_view.node_columns.hidden_columns[editor_view.current_path].keys():
exported_cols.erase(x)
ResourceTablesExportFormatCsv.export_to_file(editor_view.rows, exported_cols, import_data.edited_path, import_data)
await get_tree().process_frame
editor_view.refresh()
close()
ResourceTablesExportFormatCsv.export_to_file(editor_view.rows, exported_cols, import_data.edited_path, import_data)
await get_tree().process_frame
editor_view.refresh()
close()
# Input controls
func _on_classname_field_text_changed(new_text : String):
import_data.script_classname = new_text.replace(" ", "")
func _on_classname_field_text_changed(new_text: String):
import_data.script_classname = new_text.replace(" ", "")
func _on_remove_first_row_toggled(button_pressed : bool):
import_data.remove_first_row = button_pressed
# $"Export/Box2/Button".button_pressed = true
$"Export/Box3/CheckBox".button_pressed = button_pressed
func _on_remove_first_row_toggled(button_pressed: bool):
import_data.remove_first_row = button_pressed
# $"Export/Box2/Button".button_pressed = true
$"Export/Box3/CheckBox".button_pressed = button_pressed
func _on_filename_options_item_selected(index):
property_used_as_filename = index
property_used_as_filename = index
func _on_list_item_type_selected(type: int, index: int):
import_data.prop_types[index] = type
func _on_list_item_type_selected(type : int, index : int):
import_data.prop_types[index] = type
func _on_list_item_name_changed(name : String, index : int):
import_data.prop_names[index] = name.replace(" ", "")
func _on_list_item_name_changed(name: String, index: int):
import_data.prop_names[index] = name.replace(" ", "")
func _on_export_delimeter_pressed(del : String):
import_data.delimeter = del + import_data.delimeter.substr(1)
func _on_export_delimeter_pressed(del: String):
import_data.delimeter = del + import_data.delimeter.substr(1)
func _on_export_space_toggled(button_pressed : bool):
import_data.delimeter = (
import_data.delimeter[0]
if !button_pressed else
import_data.delimeter + " "
)
func _on_export_space_toggled(button_pressed: bool):
import_data.delimeter = (
import_data.delimeter[0]
if !button_pressed else
import_data.delimeter + " "
)
func _reset_controls():
$"Export/Box/CheckBox".button_pressed = false
_on_remove_first_row_toggled(true)
$"Export/Box/CheckBox".button_pressed = false
_on_remove_first_row_toggled(true)
func _on_enum_format_changed(case, delimiter, bool_yes, bool_no):
import_data.enum_format = [case, delimiter, bool_yes, bool_no]
import_data.enum_format = [case, delimiter, bool_yes, bool_no]
func close():
get_parent().hide()
get_parent().hide()

@ -5,16 +5,16 @@ signal format_changed(case, delimiter, bool_yes, bool_no)
func _send_signal(arg1 = null):
format_changed.emit(
$"HBoxContainer/Case".selected,
[" ", "_", "-"][$"HBoxContainer/Separator".selected],
$"HBoxContainer2/True".text,
$"HBoxContainer2/False".text
)
format_changed.emit(
$"HBoxContainer/Case".selected,
[" ", "_", "-"][$"HBoxContainer/Separator".selected],
$"HBoxContainer2/True".text,
$"HBoxContainer2/False".text
)
func _on_format_changed(case, delimiter, bool_yes, bool_no):
$"HBoxContainer/Case".selected = case
$"HBoxContainer/Separator".selected = [" ", "_", "-"].find(delimiter)
$"HBoxContainer2/True".text = bool_yes
$"HBoxContainer2/False".text = bool_no
$"HBoxContainer/Case".selected = case
$"HBoxContainer/Separator".selected = [" ", "_", "-"].find(delimiter)
$"HBoxContainer2/True".text = bool_yes
$"HBoxContainer2/False".text = bool_no

@ -1,12 +1,11 @@
@tool
extends HBoxContainer
func display(name: String, type: int):
$"LineEdit".text = name
$"OptionButton".selected = type
func display(name : String, type : int):
$"LineEdit".text = name
$"OptionButton".selected = type
func connect_all_signals(to : Object, index : int, prefix : String = "_on_list_item_"):
$"LineEdit".text_changed.connect(Callable(to, prefix + "name_changed").bind(index))
$"OptionButton".item_selected.connect(Callable(to, prefix + "type_selected").bind(index))
func connect_all_signals(to: Object, index: int, prefix: String = "_on_list_item_"):
$"LineEdit".text_changed.connect(Callable(to, prefix + "name_changed").bind(index))
$"OptionButton".item_selected.connect(Callable(to, prefix + "type_selected").bind(index))

@ -3,290 +3,284 @@ class_name ResourceTablesImport
extends Resource
enum PropType {
BOOL,
INT,
FLOAT,
STRING,
COLOR,
OBJECT,
ENUM,
MAX,
BOOL,
INT,
FLOAT,
STRING,
COLOR,
OBJECT,
ENUM,
MAX,
}
enum NameCasing {
ALL_LOWER,
CAPS_WORD_EXCEPT_FIRST,
CAPS_WORD,
ALL_CAPS,
ALL_LOWER,
CAPS_WORD_EXCEPT_FIRST,
CAPS_WORD,
ALL_CAPS,
}
const SUFFIX := "_spreadsheet_import.tres"
const SUFFIX := "_spreadsheet_import.tres"
const TYPE_MAP := {
TYPE_STRING : PropType.STRING,
TYPE_FLOAT : PropType.FLOAT,
TYPE_BOOL : PropType.BOOL,
TYPE_INT : PropType.INT,
TYPE_OBJECT : PropType.OBJECT,
TYPE_COLOR : PropType.COLOR,
}
@export var prop_types : Array
@export var prop_names : Array
TYPE_STRING: PropType.STRING,
TYPE_FLOAT: PropType.FLOAT,
TYPE_BOOL: PropType.BOOL,
TYPE_INT: PropType.INT,
TYPE_OBJECT: PropType.OBJECT,
TYPE_COLOR: PropType.COLOR,
}
@export var prop_types: Array
@export var prop_names: Array
@export var edited_path := "res://"
@export var prop_used_as_filename := ""
@export var script_classname := ""
@export var remove_first_row := true
@export var new_script : GDScript
@export var view_script : Script = ResourceTablesEditFormatCsv
@export var new_script: GDScript
@export var view_script: Script = ResourceTablesEditFormatCsv
@export var delimeter := ";"
@export var enum_format : Array = [NameCasing.CAPS_WORD, " ", "Yes", "No"]
@export var uniques : Dictionary
@export var enum_format: Array = [NameCasing.CAPS_WORD, " ", "Yes", "No"]
@export var uniques: Dictionary
func initialize(path):
edited_path = path
prop_types = []
prop_names = []
edited_path = path
prop_types = []
prop_names = []
func save():
resource_path = edited_path.get_basename() + SUFFIX
ResourceSaver.save.call_deferred(self)
resource_path = edited_path.get_basename() + SUFFIX
ResourceSaver.save.call_deferred(self)
func string_to_property(string: String, col_index: int):
match prop_types[col_index]:
PropType.STRING:
return string
PropType.BOOL:
string = string.to_lower()
if string == enum_format[2].to_lower(): return true
if string == enum_format[3].to_lower(): return false
return !string in ["no", "disabled", "-", "false", "absent", "wrong", "off", "0", ""]
func string_to_property(string : String, col_index : int):
match prop_types[col_index]:
PropType.STRING:
return string
PropType.FLOAT:
return string.to_float()
PropType.BOOL:
string = string.to_lower()
if string == enum_format[2].to_lower(): return true
if string == enum_format[3].to_lower(): return false
return !string in ["no", "disabled", "-", "false", "absent", "wrong", "off", "0", ""]
PropType.INT:
return string.to_int()
PropType.FLOAT:
return string.to_float()
PropType.COLOR:
return Color(string)
PropType.INT:
return string.to_int()
PropType.OBJECT:
return load(string)
PropType.COLOR:
return Color(string)
PropType.ENUM:
if string == "":
return int(uniques[col_index]["N_A"])
PropType.OBJECT:
return load(string)
else:
return int(uniques[col_index][string.capitalize().replace(" ", "_").to_upper()])
PropType.ENUM:
if string == "":
return int(uniques[col_index]["N_A"])
else:
return int(uniques[col_index][string.capitalize().replace(" ", "_").to_upper()])
func property_to_string(value, col_index: int) -> String:
if value == null: return ""
if col_index == 0:
if prop_names[col_index] == "resource_path":
return value.get_file().get_basename()
if prop_types[col_index] is PackedStringArray:
return prop_types[col_index][value].capitalize()
func property_to_string(value, col_index : int) -> String:
if value == null: return ""
if col_index == 0:
if prop_names[col_index] == "resource_path":
return value.get_file().get_basename()
if prop_types[col_index] is PackedStringArray:
return prop_types[col_index][value].capitalize()
match prop_types[col_index]:
PropType.STRING:
return str(value)
match prop_types[col_index]:
PropType.STRING:
return str(value)
PropType.BOOL:
return enum_format[2] if value else enum_format[3]
PropType.BOOL:
return enum_format[2] if value else enum_format[3]
PropType.FLOAT, PropType.INT:
return str(value)
PropType.FLOAT, PropType.INT:
return str(value)
PropType.COLOR:
return value.to_html()
PropType.COLOR:
return value.to_html()
PropType.OBJECT:
return value.resource_path
PropType.OBJECT:
return value.resource_path
PropType.ENUM:
var dict = uniques[col_index]
for k in dict:
if dict[k] == value:
return change_name_to_format(k, enum_format[0], enum_format[1])
PropType.ENUM:
var dict = uniques[col_index]
for k in dict:
if dict[k] == value:
return change_name_to_format(k, enum_format[0], enum_format[1])
return str(value)
return str(value)
func create_property_line_for_prop(col_index : int) -> String:
var result = "@export var " + prop_names[col_index] + " :"
match prop_types[col_index]:
PropType.STRING:
return result + "= \"\"\r\n"
func create_property_line_for_prop(col_index: int) -> String:
var result = "@export var " + prop_names[col_index] + " :"
match prop_types[col_index]:
PropType.STRING:
return result + "= \"\"\r\n"
PropType.BOOL:
return result + "= false\r\n"
PropType.BOOL:
return result + "= false\r\n"
PropType.FLOAT:
return result + "= 0.0\r\n"
PropType.FLOAT:
return result + "= 0.0\r\n"
PropType.INT:
return result + "= 0\r\n"
PropType.INT:
return result + "= 0\r\n"
PropType.COLOR:
return result + "= Color.WHITE\r\n"
PropType.COLOR:
return result + "= Color.WHITE\r\n"
PropType.OBJECT:
return result + " Resource\r\n"
PropType.OBJECT:
return result + " Resource\r\n"
PropType.ENUM:
return result + " %s\r\n" % _escape_forbidden_enum_names(prop_names[col_index].capitalize().replace(" ", ""))
# return result.replace(
# "@export var",
# "@export_enum(" + _escape_forbidden_enum_names(
# prop_names[col_index].capitalize()\
# .replace(" ", "")
# ) + ") var"
# ) + "= 0\r\n"
PropType.ENUM:
return result + " %s\r\n" % _escape_forbidden_enum_names(prop_names[col_index].capitalize().replace(" ", ""))
# return result.replace(
# "@export var",
# "@export_enum(" + _escape_forbidden_enum_names(
# prop_names[col_index].capitalize()\
# .replace(" ", "")
# ) + ") var"
# ) + "= 0\r\n"
return ""
return ""
func _escape_forbidden_enum_names(string : String) -> String:
if ClassDB.class_exists(string):
return string + "_"
# Not in ClassDB, but are engine types and can be property names
if string in [
"Color", "String", "Plane", "Projection",
"Basis", "Transform", "Variant",
]:
return string + "_"
func _escape_forbidden_enum_names(string: String) -> String:
if ClassDB.class_exists(string):
return string + "_"
return string
# Not in ClassDB, but are engine types and can be property names
if string in [
"Color", "String", "Plane", "Projection",
"Basis", "Transform", "Variant",
]:
return string + "_"
return string
func create_enum_for_prop(col_index) -> String:
var result := (
"enum "
+ _escape_forbidden_enum_names(
prop_names[col_index].capitalize().replace(" ", "")
) + " {\r\n"
)
for k in uniques[col_index]:
result += (
"\t"
+ k # Enum Entry
+ " = "
+ str(uniques[col_index][k]) # Value
+ ",\r\n"
)
result += "\tMAX,\r\n}\r\n\r\n"
return result
var result := (
"enum "
+ _escape_forbidden_enum_names(
prop_names[col_index].capitalize().replace(" ", "")
) + " {\r\n"
)
for k in uniques[col_index]:
result += (
"\t"
+ k # Enum Entry
+ " = "
+ str(uniques[col_index][k]) # Value
+ ",\r\n"
)
result += "\tMAX,\r\n}\r\n\r\n"
return result
func generate_script(entries, has_classname = true) -> GDScript:
var source = ""
# if has_classname and script_classname != "":
# source = "class_name " + script_classname + " \r\nextends Resource\r\n\r\n"
#
# else:
source = "extends Resource\r\n\r\n"
# Enums
uniques = get_uniques(entries)
for i in prop_types.size():
if prop_types[i] == PropType.ENUM:
source += create_enum_for_prop(i)
# Properties
for i in prop_names.size():
if (prop_names[i] != "resource_path") and (prop_names[i] != "resource_name"):
source += create_property_line_for_prop(i)
var created_script = GDScript.new()
created_script.source_code = source
created_script.reload()
return created_script
func strings_to_resource(strings : Array):
var new_res = new_script.new()
for j in min(prop_names.size(), strings.size()):
new_res.set(prop_names[j], string_to_property(strings[j], j))
if prop_used_as_filename != "":
new_res.resource_path = edited_path.get_basename() + "/" + new_res.get(prop_used_as_filename) + ".tres"
return new_res
func resource_to_strings(res : Resource):
var strings := []
strings.resize(prop_names.size())
for i in prop_names.size():
strings[i] = property_to_string(res.get(prop_names[i]), i)
return PackedStringArray(strings)
func get_uniques(entries : Array) -> Dictionary:
var result = {}
for i in prop_types.size():
if prop_types[i] == PropType.ENUM:
var cur_value := ""
result[i] = {}
for j in entries.size():
if j == 0 and remove_first_row: continue
cur_value = entries[j][i].capitalize().to_upper().replace(" ", "_")
if cur_value == "":
cur_value = "N_A"
if !result[i].has(cur_value):
result[i][cur_value] = result[i].size()
return result
static func change_name_to_format(name : String, case : int, delim : String):
var string = name.capitalize().replace(" ", delim)
if case == NameCasing.ALL_LOWER:
return string.to_lower()
if case == NameCasing.CAPS_WORD_EXCEPT_FIRST:
return string[0].to_lower() + string.substr(1)
if case == NameCasing.CAPS_WORD:
return string
if case == NameCasing.ALL_CAPS:
return string.to_upper()
static func get_resource_property_types(res : Resource, properties : Array) -> Array:
var result = []
result.resize(properties.size())
result.fill(PropType.STRING)
var cur_type := 0
for x in res.get_property_list():
var found = properties.find(x["name"])
if found == -1: continue
if x["usage"] & PROPERTY_USAGE_EDITOR != 0:
if x["hint"] == PROPERTY_HINT_ENUM:
var enum_values = x["hint_string"].split(",")
for i in enum_values.size():
var index_found : int = enum_values[i].find(":")
if index_found == -1: continue
enum_values[i] = enum_values[i].left(index_found)
result[found] = enum_values
else:
result[found] = TYPE_MAP.get(x["type"], PropType.STRING)
return result
var source = ""
# if has_classname and script_classname != "":
# source = "class_name " + script_classname + " \r\nextends Resource\r\n\r\n"
#
# else:
source = "extends Resource\r\n\r\n"
# Enums
uniques = get_uniques(entries)
for i in prop_types.size():
if prop_types[i] == PropType.ENUM:
source += create_enum_for_prop(i)
# Properties
for i in prop_names.size():
if (prop_names[i] != "resource_path") and (prop_names[i] != "resource_name"):
source += create_property_line_for_prop(i)
var created_script = GDScript.new()
created_script.source_code = source
created_script.reload()
return created_script
func strings_to_resource(strings: Array):
var new_res = new_script.new()
for j in min(prop_names.size(), strings.size()):
new_res.set(prop_names[j], string_to_property(strings[j], j))
if prop_used_as_filename != "":
new_res.resource_path = edited_path.get_basename() + "/" + new_res.get(prop_used_as_filename) + ".tres"
return new_res
func resource_to_strings(res: Resource):
var strings := []
strings.resize(prop_names.size())
for i in prop_names.size():
strings[i] = property_to_string(res.get(prop_names[i]), i)
return PackedStringArray(strings)
func get_uniques(entries: Array) -> Dictionary:
var result = {}
for i in prop_types.size():
if prop_types[i] == PropType.ENUM:
var cur_value := ""
result[i] = {}
for j in entries.size():
if j == 0 and remove_first_row: continue
cur_value = entries[j][i].capitalize().to_upper().replace(" ", "_")
if cur_value == "":
cur_value = "N_A"
if !result[i].has(cur_value):
result[i][cur_value] = result[i].size()
return result
static func change_name_to_format(name: String, case: int, delim: String):
var string = name.capitalize().replace(" ", delim)
if case == NameCasing.ALL_LOWER:
return string.to_lower()
if case == NameCasing.CAPS_WORD_EXCEPT_FIRST:
return string[0].to_lower() + string.substr(1)
if case == NameCasing.CAPS_WORD:
return string
if case == NameCasing.ALL_CAPS:
return string.to_upper()
static func get_resource_property_types(res: Resource, properties: Array) -> Array:
var result = []
result.resize(properties.size())
result.fill(PropType.STRING)
var cur_type := 0
for x in res.get_property_list():
var found = properties.find(x["name"])
if found == -1: continue
if x["usage"] & PROPERTY_USAGE_EDITOR != 0:
if x["hint"] == PROPERTY_HINT_ENUM:
var enum_values = x["hint_string"].split(",")
for i in enum_values.size():
var index_found: int = enum_values[i].find(":")
if index_found == -1: continue
enum_values[i] = enum_values[i].left(index_found)
result[found] = enum_values
else:
result[found] = TYPE_MAP.get(x["type"], PropType.STRING)
return result

@ -2,119 +2,122 @@
extends Control
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
@export var table_header_scene: PackedScene
@export var table_header_scene : PackedScene
@export @onready var editor_view : Control = $"../../../.."
@export @onready var hide_columns_button : BaseButton = $"../../MenuStrip/VisibleCols"
@export @onready var grid : GridContainer = $"../../../MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid"
@export @onready var editor_view: Control = $"../../../.."
@export @onready var hide_columns_button: BaseButton = $"../../MenuStrip/VisibleCols"
@export @onready var grid: GridContainer = $"../../../MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid"
var hidden_columns := {}
var columns := []:
set(v):
columns = v
for x in get_children():
x.queue_free()
var new_node : Control
for x in v:
new_node = table_header_scene.instantiate()
add_child(new_node)
new_node.manager = self
new_node.set_label(x)
new_node.get_node("Button").pressed.connect(editor_view._set_sorting.bind(x))
var columns := []:
set(v):
columns = v
for x in get_children():
x.queue_free()
var new_node: Control
for x in v:
new_node = table_header_scene.instantiate()
add_child(new_node)
new_node.manager = self
new_node.set_label(x)
new_node.get_node("Button").pressed.connect(editor_view._set_sorting.bind(x))
func _ready():
hide_columns_button\
.get_popup()\
.id_pressed\
.connect(_on_visible_cols_id_pressed)
$"../../../MarginContainer/FooterContentSplit/Panel/Scroll"\
.get_h_scroll_bar()\
.value_changed\
.connect(_on_h_scroll_changed)
hide_columns_button\
.get_popup()\
.id_pressed\
.connect(_on_visible_cols_id_pressed)
$"../../../MarginContainer/FooterContentSplit/Panel/Scroll"\
.get_h_scroll_bar()\
.value_changed\
.connect(_on_h_scroll_changed)
func update():
_update_hidden_columns()
_update_column_sizes()
_update_hidden_columns()
_update_column_sizes()
func hide_column(column_index : int):
hidden_columns[editor_view.current_path][editor_view.columns[column_index]] = true
editor_view.save_data()
update()
func hide_column(column_index: int):
hidden_columns[editor_view.current_path][editor_view.columns[column_index]] = true
editor_view.save_data()
update()
func select_column(column_index : int):
editor_view.select_column(column_index)
func select_column(column_index: int):
editor_view.select_column(column_index)
func _update_column_sizes():
if grid.get_child_count() == 0:
return
await get_tree().process_frame
var column_headers := get_children()
if grid.get_child_count() < column_headers.size(): return
if column_headers.size() != columns.size():
editor_view.refresh()
return
var clip_text : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "clip_headers")
var min_width := 0
var cell : Control
for i in column_headers.size():
var header = column_headers[i]
cell = grid.get_child(i)
header.get_child(0).clip_text = clip_text
header.custom_minimum_size.x = 0
cell.custom_minimum_size.x = 0
header.size.x = 0
min_width = max(header.size.x, cell.size.x)
header.custom_minimum_size.x = min_width
cell.custom_minimum_size.x = header.get_minimum_size().x
header.size.x = min_width
grid.hide()
grid.show()
hide()
show()
await get_tree().process_frame
# Abort if the node has been deleted since.
if !is_instance_valid(column_headers[0]):
return
get_parent().custom_minimum_size.y = column_headers[0].size.y
for i in column_headers.size():
column_headers[i].position.x = grid.get_child(i).position.x
column_headers[i].size.x = grid.get_child(i).size.x
if grid.get_child_count() == 0:
return
await get_tree().process_frame
var column_headers := get_children()
if grid.get_child_count() < column_headers.size(): return
if column_headers.size() != columns.size():
editor_view.refresh()
return
var clip_text: bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "clip_headers")
var min_width := 0
var cell: Control
for i in column_headers.size():
var header = column_headers[i]
cell = grid.get_child(i)
header.get_child(0).clip_text = clip_text
header.custom_minimum_size.x = 0
cell.custom_minimum_size.x = 0
header.size.x = 0
min_width = max(header.size.x, cell.size.x)
header.custom_minimum_size.x = min_width
cell.custom_minimum_size.x = header.get_minimum_size().x
header.size.x = min_width
grid.hide()
grid.show()
hide()
show()
await get_tree().process_frame
# Abort if the node has been deleted since.
if !is_instance_valid(column_headers[0]):
return
get_parent().custom_minimum_size.y = column_headers[0].size.y
for i in column_headers.size():
column_headers[i].position.x = grid.get_child(i).position.x
column_headers[i].size.x = grid.get_child(i).size.x
func _update_hidden_columns():
var current_path = editor_view.current_path
var rows_shown = editor_view.last_row - editor_view.first_row
if !hidden_columns.has(current_path):
hidden_columns[current_path] = {
"resource_local_to_scene" : true,
"resource_name" : true,
}
var current_path = editor_view.current_path
var rows_shown = editor_view.last_row - editor_view.first_row
if !hidden_columns.has(current_path):
hidden_columns[current_path] = {
"resource_local_to_scene": true,
"resource_name": true,
}
editor_view.save_data()
var visible_column_count = 0
for i in columns.size():
var visible_column_count = 0
for
i in columns.size():
var column_visible = !hidden_columns[current_path].has(columns[i])
get_child(i).visible = column_visible
for j in rows_shown:
get_child(i).visible = column_visible
for j in rows_shown:
grid.get_child(j * columns.size() + i).visible = column_visible
if column_visible:
@ -123,31 +126,34 @@ func _update_hidden_columns():
grid.columns = visible_column_count
func _on_h_scroll_changed(value):
position.x = -value
position.x = -value
func _on_visible_cols_about_to_popup():
var current_path = editor_view.current_path
var popup = hide_columns_button.get_popup()
popup.clear()
popup.hide_on_checkable_item_selection = false
for i in columns.size():
popup.add_check_item(columns[i].capitalize(), i)
popup.set_item_checked(i, not hidden_columns[current_path].has(columns[i]))
func _on_visible_cols_id_pressed(id : int):
var current_path = editor_view.current_path
var popup = hide_columns_button.get_popup()
if popup.is_item_checked(id):
popup.set_item_checked(id, false)
hidden_columns[current_path][columns[id]] = true
else:
popup.set_item_checked(id, true)
hidden_columns[current_path].erase(columns[id])
editor_view.save_data()
update()
var current_path = editor_view.current_path
var popup = hide_columns_button.get_popup()
popup.clear()
popup.hide_on_checkable_item_selection = false
for i in columns.size():
popup.add_check_item(columns[i].capitalize(), i)
popup.set_item_checked(i, not hidden_columns[current_path].has(columns[i]))
func _on_visible_cols_id_pressed(id: int):
var current_path = editor_view.current_path
var popup = hide_columns_button.get_popup()
if popup.is_item_checked(id):
popup.set_item_checked(id, false)
hidden_columns[current_path][columns[id]] = true
else:
popup.set_item_checked(id, true)
hidden_columns[current_path].erase(columns[id])
editor_view.save_data()
update()

@ -1,173 +1,174 @@
@tool
extends Control
@export var editor_view_path : NodePath
@export var editor_view_path: NodePath
@export_enum("Filter", "Process", "Sort") var mode := 0
@export var title := ""
@export var default_text := "":
set(v):
default_text = v
if _textfield == null:
await ready
set(v):
default_text = v
if _textfield == null:
await ready
_textfield.text = v
_textfield.text = v
@export_multiline var default_text_ml := "":
set(v):
default_text_ml = v
if _textfield_ml == null:
await ready
set(v):
default_text_ml = v
if _textfield_ml == null:
await ready
_textfield_ml.text = v
@export var function_save_key := ""
_textfield_ml.text = v
var _textfield : LineEdit
var _textfield_ml : TextEdit
var _togglable_popup : PopupPanel
var _saved_function_index_label : Label
@export var function_save_key := ""
var _saved_functions : Array = []
var _textfield: LineEdit
var _textfield_ml: TextEdit
var _togglable_popup: PopupPanel
var _saved_function_index_label: Label
var _saved_functions: Array = []
var _saved_function_selected := -1
func load_saved_functions(func_dict : Dictionary):
if !func_dict.has(function_save_key):
func_dict[function_save_key] = [default_text_ml]
func load_saved_functions(func_dict: Dictionary):
if !func_dict.has(function_save_key):
func_dict[function_save_key] = [default_text_ml]
_saved_functions = func_dict[function_save_key]
_on_saved_function_selected(0)
_saved_functions = func_dict[function_save_key]
_on_saved_function_selected(0)
func _ready():
var toggle_button := Button.new()
var popup_box := VBoxContainer.new()
var popup_buttons_box := HBoxContainer.new()
var title_label := Label.new()
var submit_button := Button.new()
var move_label := Label.new()
var move_button_l := Button.new()
var move_button_r := Button.new()
_textfield = LineEdit.new()
_togglable_popup = PopupPanel.new()
_textfield_ml = TextEdit.new()
_saved_function_index_label = Label.new()
add_child(_textfield)
add_child(toggle_button)
_textfield.add_child(_togglable_popup)
_togglable_popup.add_child(popup_box)
popup_box.add_child(title_label)
popup_box.add_child(_textfield_ml)
popup_box.add_child(popup_buttons_box)
popup_buttons_box.add_child(submit_button)
popup_buttons_box.add_child(move_label)
popup_buttons_box.add_child(move_button_l)
popup_buttons_box.add_child(_saved_function_index_label)
popup_buttons_box.add_child(move_button_r)
title_label.text = title
toggle_button.icon = get_theme_icon("Collapse", "EditorIcons")
toggle_button.pressed.connect(_on_expand_pressed)
_textfield.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_textfield.text_submitted.connect(_on_text_submitted.unbind(1))
_textfield_ml.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_textfield_ml.size_flags_vertical = Control.SIZE_EXPAND_FILL
submit_button.text = "Run multiline!"
submit_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
submit_button.pressed.connect(_on_text_submitted)
move_label.text = "Choose saved:"
move_button_l.icon = get_theme_icon("PagePrevious", "EditorIcons")
move_button_l.pressed.connect(_on_saved_function_bumped.bind(-1))
_on_saved_function_selected(0)
move_button_r.icon = get_theme_icon("PageNext", "EditorIcons")
move_button_r.pressed.connect(_on_saved_function_bumped.bind(+1))
var toggle_button := Button.new()
var popup_box := VBoxContainer.new()
var popup_buttons_box := HBoxContainer.new()
var title_label := Label.new()
var submit_button := Button.new()
var move_label := Label.new()
var move_button_l := Button.new()
var move_button_r := Button.new()
_textfield = LineEdit.new()
_togglable_popup = PopupPanel.new()
_textfield_ml = TextEdit.new()
_saved_function_index_label = Label.new()
add_child(_textfield)
add_child(toggle_button)
_textfield.add_child(_togglable_popup)
_togglable_popup.add_child(popup_box)
popup_box.add_child(title_label)
popup_box.add_child(_textfield_ml)
popup_box.add_child(popup_buttons_box)
popup_buttons_box.add_child(submit_button)
popup_buttons_box.add_child(move_label)
popup_buttons_box.add_child(move_button_l)
popup_buttons_box.add_child(_saved_function_index_label)
popup_buttons_box.add_child(move_button_r)
title_label.text = title
toggle_button.icon = get_theme_icon("Collapse", "EditorIcons")
toggle_button.pressed.connect(_on_expand_pressed)
_textfield.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_textfield.text_submitted.connect(_on_text_submitted.unbind(1))
_textfield_ml.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_textfield_ml.size_flags_vertical = Control.SIZE_EXPAND_FILL
submit_button.text = "Run multiline!"
submit_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
submit_button.pressed.connect(_on_text_submitted)
move_label.text = "Choose saved:"
move_button_l.icon = get_theme_icon("PagePrevious", "EditorIcons")
move_button_l.pressed.connect(_on_saved_function_bumped.bind(-1))
_on_saved_function_selected(0)
move_button_r.icon = get_theme_icon("PageNext", "EditorIcons")
move_button_r.pressed.connect(_on_saved_function_bumped.bind(+1))
func _on_expand_pressed():
_togglable_popup.popup(Rect2i(_textfield.get_screen_position(), Vector2(size.x, 256.0)))
_togglable_popup.popup(Rect2i(_textfield.get_screen_position(), Vector2(size.x, 256.0)))
func _on_text_submitted():
[_table_filter, _table_process][mode].call()
_saved_functions[_saved_function_selected] = _textfield_ml.text
get_node(editor_view_path).save_data.call_deferred()
[_table_filter, _table_process][mode].call()
_saved_functions[_saved_function_selected] = _textfield_ml.text
get_node(editor_view_path).save_data.call_deferred()
func _get_script_source_code(first_line : String):
var new_text := ""
if !_togglable_popup.visible:
new_text = _textfield.text
if new_text == "":
new_text = default_text
func _get_script_source_code(first_line: String):
var new_text := ""
if !_togglable_popup.visible:
new_text = _textfield.text
if new_text == "":
new_text = default_text
return first_line + "\treturn " + new_text
return first_line + "\treturn " + new_text
else:
new_text = _textfield_ml.text
if new_text == "":
new_text = default_text_ml
else:
new_text = _textfield_ml.text
if new_text == "":
new_text = default_text_ml
var text_split := new_text.split("\n")
for i in text_split.size():
text_split[i] = "\t" + text_split[i]
var text_split := new_text.split("\n")
for i in text_split.size():
text_split[i] = "\t" + text_split[i]
return first_line + "\n".join(text_split)
return first_line + "\n".join(text_split)
func _table_filter():
var new_script := GDScript.new()
new_script.source_code = _get_script_source_code("static func can_show(res, index):\n")
new_script.reload()
var new_script := GDScript.new()
new_script.source_code = _get_script_source_code("static func can_show(res, index):\n")
new_script.reload()
var editor_view := get_node(editor_view_path)
editor_view.search_cond = new_script
editor_view.refresh()
var editor_view := get_node(editor_view_path)
editor_view.search_cond = new_script
editor_view.refresh()
func _table_process():
var new_script := GDScript.new()
new_script.source_code = _get_script_source_code("static func get_result(value, res, all_res, row_index):\n")
new_script.reload()
var new_script := GDScript.new()
new_script.source_code = _get_script_source_code("static func get_result(value, res, all_res, row_index):\n")
new_script.reload()
var editor_view := get_node(editor_view_path)
var new_script_instance = new_script.new()
var values = editor_view.get_edited_cells_values()
var editor_view := get_node(editor_view_path)
var new_script_instance = new_script.new()
var values = editor_view.get_edited_cells_values()
var edited_rows : Array[int] = editor_view._selection.get_edited_rows()
var edited_resources := edited_rows.map(func(x): return editor_view.rows[x])
for i in values.size():
values[i] = new_script_instance.get_result(values[i], editor_view.rows[edited_rows[i]], edited_resources, i)
var edited_rows: Array[int] = editor_view._selection.get_edited_rows()
var edited_resources := edited_rows.map(func(x): return editor_view.rows[x])
for i in values.size():
values[i] = new_script_instance.get_result(values[i], editor_view.rows[edited_rows[i]], edited_resources, i)
editor_view.set_edited_cells_values(values)
editor_view.set_edited_cells_values(values)
func _on_saved_function_selected(new_index : int):
if new_index < 0:
new_index = 0
func _on_saved_function_selected(new_index: int):
if new_index < 0:
new_index = 0
if _saved_function_selected == _saved_functions.size() - 1 and _textfield_ml.text == default_text_ml:
_saved_functions.resize(_saved_functions.size() - 1)
if _saved_function_selected == _saved_functions.size() - 1 and _textfield_ml.text == default_text_ml:
_saved_functions.resize(_saved_functions.size() - 1)
elif _saved_function_selected >= 0:
_saved_functions[_saved_function_selected] = _textfield_ml.text
elif _saved_function_selected >= 0:
_saved_functions[_saved_function_selected] = _textfield_ml.text
_saved_function_selected = new_index
if new_index >= _saved_functions.size():
_saved_functions.resize(new_index + 1)
for i in _saved_functions.size():
if _saved_functions[i] == null:
_saved_functions[i] = default_text_ml
_saved_function_selected = new_index
if new_index >= _saved_functions.size():
_saved_functions.resize(new_index + 1)
for i in _saved_functions.size():
if _saved_functions[i] == null:
_saved_functions[i] = default_text_ml
_textfield_ml.text = _saved_functions[new_index]
_saved_function_index_label.text = "%d/%d" % [new_index + 1, _saved_functions.size()]
get_node(editor_view_path).save_data.call_deferred()
_textfield_ml.text = _saved_functions[new_index]
_saved_function_index_label.text = "%d/%d" % [new_index + 1, _saved_functions.size()]
get_node(editor_view_path).save_data.call_deferred()
func _on_saved_function_bumped(increment : int):
_on_saved_function_selected(_saved_function_selected + increment)
func _on_saved_function_bumped(increment: int):
_on_saved_function_selected(_saved_function_selected + increment)

@ -1,163 +1,162 @@
@tool
extends Node
const TablesPluginEditorViewClass = preload("res://addons/resources_spreadsheet_view/editor_view.gd")
const TablesPluginSelectionManagerClass = preload("res://addons/resources_spreadsheet_view/main_screen/selection_manager.gd")
const TextEditingUtilsClass := preload("res://addons/resources_spreadsheet_view/text_editing_utils.gd")
@onready var editor_view : TablesPluginEditorViewClass = get_parent()
@onready var selection : TablesPluginSelectionManagerClass = get_node("../SelectionManager")
func _on_cell_gui_input(event : InputEvent, cell : Control):
if event is InputEventMouseButton:
editor_view.grab_focus()
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
if !cell in selection.edited_cells:
selection.deselect_all_cells()
selection.select_cell(cell)
selection.rightclick_cells()
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
if Input.is_key_pressed(KEY_CTRL):
if cell in selection.edited_cells:
selection.deselect_cell(cell)
else:
selection.select_cell(cell)
elif Input.is_key_pressed(KEY_SHIFT):
selection.select_cells_to(cell)
else:
selection.deselect_all_cells()
selection.select_cell(cell)
func _gui_input(event : InputEvent):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
selection.rightclick_cells()
if event.button_index == MOUSE_BUTTON_LEFT:
editor_view.grab_focus()
if !event.pressed:
selection.deselect_all_cells()
func _input(event : InputEvent):
if !event is InputEventKey or !event.pressed:
return
if !editor_view.has_focus() or selection.edited_cells.size() == 0:
return
if event.keycode == KEY_CTRL or event.keycode == KEY_SHIFT:
# Modifier keys do not get processed.
return
# Ctrl + Z (before, and instead of, committing the action!)
if Input.is_key_pressed(KEY_CTRL):
if event.keycode == KEY_Z or event.keycode == KEY_Y:
return
_key_specific_action(event)
editor_view.grab_focus()
editor_view.editor_interface.get_resource_filesystem().scan()
func _key_specific_action(event : InputEvent):
var column = selection.get_cell_column(selection.edited_cells[0])
var ctrl_pressed := Input.is_key_pressed(KEY_CTRL)
# BETWEEN-CELL NAVIGATION
if event.keycode == KEY_UP:
_move_selection_on_grid(0, (-1 if !ctrl_pressed else -10))
elif event.keycode == KEY_DOWN:
_move_selection_on_grid(0, (1 if !ctrl_pressed else 10))
elif Input.is_key_pressed(KEY_SHIFT) and event.keycode == KEY_TAB:
_move_selection_on_grid((-1 if !ctrl_pressed else -10), 0)
elif event.keycode == KEY_TAB:
_move_selection_on_grid((1 if !ctrl_pressed else 10), 0)
# CURSOR MOVEMENT
if event.keycode == KEY_LEFT:
TextEditingUtilsClass.multi_move_left(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
)
elif event.keycode == KEY_RIGHT:
TextEditingUtilsClass.multi_move_right(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
)
elif event.keycode == KEY_HOME:
for i in selection.edit_cursor_positions.size():
selection.edit_cursor_positions[i] = 0
elif event.keycode == KEY_END:
for i in selection.edit_cursor_positions.size():
selection.edit_cursor_positions[i] = selection.edited_cells_text[i].length()
# Ctrl + C (so you can edit in a proper text editor instead of this wacky nonsense)
elif ctrl_pressed and event.keycode == KEY_C:
TextEditingUtilsClass.multi_copy(selection.edited_cells_text)
get_viewport().set_input_as_handled()
# The following actions do not work on non-editable cells.
if !selection.column_editors[column].is_text() or editor_view.columns[column] == "resource_path":
return
# Ctrl + V
elif ctrl_pressed and event.keycode == KEY_V:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_paste(
selection.edited_cells_text, selection.edit_cursor_positions
))
get_viewport().set_input_as_handled()
# ERASING
elif event.keycode == KEY_BACKSPACE:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_erase_left(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
))
elif event.keycode == KEY_DELETE:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_erase_right(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
))
get_viewport().set_input_as_handled()
# And finally, text typing.
elif event.keycode == KEY_ENTER:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_input(
"\n", selection.edited_cells_text, selection.edit_cursor_positions
))
elif event.unicode != 0 and event.unicode != 127:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_input(
char(event.unicode), selection.edited_cells_text, selection.edit_cursor_positions
))
selection.queue_redraw()
func _move_selection_on_grid(move_h : int, move_v : int):
var selected_cells := selection.edited_cells.duplicate()
var child_count := editor_view.node_table_root.get_child_count()
var new_child_index := 0
for i in selected_cells.size():
new_child_index = (
selected_cells[i].get_index()
+ move_h
+ move_v * editor_view.columns.size()
)
if child_count < new_child_index: continue
selected_cells[i] = editor_view.node_table_root.get_child(new_child_index)
editor_view.grab_focus()
selection.deselect_all_cells()
selection.select_cells(selected_cells)
const TablesPluginEditorViewClass = preload("res://addons/resources_spreadsheet_view/editor_view.gd")
const TablesPluginSelectionManagerClass = preload("res://addons/resources_spreadsheet_view/main_screen/selection_manager.gd")
const TextEditingUtilsClass := preload("res://addons/resources_spreadsheet_view/text_editing_utils.gd")
@onready var editor_view: TablesPluginEditorViewClass = get_parent()
@onready var selection: TablesPluginSelectionManagerClass = get_node("../SelectionManager")
func _on_cell_gui_input(event: InputEvent, cell: Control):
if event is InputEventMouseButton:
editor_view.grab_focus()
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
if !cell in selection.edited_cells:
selection.deselect_all_cells()
selection.select_cell(cell)
selection.rightclick_cells()
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
if Input.is_key_pressed(KEY_CTRL):
if cell in selection.edited_cells:
selection.deselect_cell(cell)
else:
selection.select_cell(cell)
elif Input.is_key_pressed(KEY_SHIFT):
selection.select_cells_to(cell)
else:
selection.deselect_all_cells()
selection.select_cell(cell)
func _gui_input(event: InputEvent):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
selection.rightclick_cells()
if event.button_index == MOUSE_BUTTON_LEFT:
editor_view.grab_focus()
if !event.pressed:
selection.deselect_all_cells()
func _input(event: InputEvent):
if !event is InputEventKey or !event.pressed:
return
if !editor_view.has_focus() or selection.edited_cells.size() == 0:
return
if event.keycode == KEY_CTRL or event.keycode == KEY_SHIFT:
# Modifier keys do not get processed.
return
# Ctrl + Z (before, and instead of, committing the action!)
if Input.is_key_pressed(KEY_CTRL):
if event.keycode == KEY_Z or event.keycode == KEY_Y:
return
_key_specific_action(event)
editor_view.grab_focus()
editor_view.editor_interface.get_resource_filesystem().scan()
func _key_specific_action(event: InputEvent):
var column = selection.get_cell_column(selection.edited_cells[0])
var ctrl_pressed := Input.is_key_pressed(KEY_CTRL)
# BETWEEN-CELL NAVIGATION
if event.keycode == KEY_UP:
_move_selection_on_grid(0, (-1 if !ctrl_pressed else -10))
elif event.keycode == KEY_DOWN:
_move_selection_on_grid(0, (1 if !ctrl_pressed else 10))
elif Input.is_key_pressed(KEY_SHIFT) and event.keycode == KEY_TAB:
_move_selection_on_grid((-1 if !ctrl_pressed else -10), 0)
elif event.keycode == KEY_TAB:
_move_selection_on_grid((1 if !ctrl_pressed else 10), 0)
# CURSOR MOVEMENT
if event.keycode == KEY_LEFT:
TextEditingUtilsClass.multi_move_left(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
)
elif event.keycode == KEY_RIGHT:
TextEditingUtilsClass.multi_move_right(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
)
elif event.keycode == KEY_HOME:
for i in selection.edit_cursor_positions.size():
selection.edit_cursor_positions[i] = 0
elif event.keycode == KEY_END:
for i in selection.edit_cursor_positions.size():
selection.edit_cursor_positions[i] = selection.edited_cells_text[i].length()
# Ctrl + C (so you can edit in a proper text editor instead of this wacky nonsense)
elif ctrl_pressed and event.keycode == KEY_C:
TextEditingUtilsClass.multi_copy(selection.edited_cells_text)
get_viewport().set_input_as_handled()
# The following actions do not work on non-editable cells.
if !selection.column_editors[column].is_text() or editor_view.columns[column] == "resource_path":
return
# Ctrl + V
elif ctrl_pressed and event.keycode == KEY_V:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_paste(
selection.edited_cells_text, selection.edit_cursor_positions
))
get_viewport().set_input_as_handled()
# ERASING
elif event.keycode == KEY_BACKSPACE:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_erase_left(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
))
elif event.keycode == KEY_DELETE:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_erase_right(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
))
get_viewport().set_input_as_handled()
# And finally, text typing.
elif event.keycode == KEY_ENTER:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_input(
"\n", selection.edited_cells_text, selection.edit_cursor_positions
))
elif event.unicode != 0 and event.unicode != 127:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_input(
char(event.unicode), selection.edited_cells_text, selection.edit_cursor_positions
))
selection.queue_redraw()
func _move_selection_on_grid(move_h: int, move_v: int):
var selected_cells := selection.edited_cells.duplicate()
var child_count := editor_view.node_table_root.get_child_count()
var new_child_index := 0
for i in selected_cells.size():
new_child_index = (
selected_cells[i].get_index()
+ move_h
+ move_v * editor_view.columns.size()
)
if child_count < new_child_index: continue
selected_cells[i] = editor_view.node_table_root.get_child(new_child_index)
editor_view.grab_focus()
selection.deselect_all_cells()
selection.select_cells(selected_cells)

@ -7,48 +7,48 @@ var recent_paths := []
func _ready():
item_selected.connect(_on_item_selected)
item_selected.connect(_on_item_selected)
func load_paths(paths):
clear()
for x in paths:
add_path_to_recent(x, true)
clear()
for x in paths:
add_path_to_recent(x, true)
selected = 0
selected = 0
func add_path_to_recent(path : String, is_loading : bool = false):
if path in recent_paths: return
func add_path_to_recent(path: String, is_loading: bool = false):
if path in recent_paths: return
var idx_in_array := recent_paths.find(path)
if idx_in_array != -1:
remove_item(idx_in_array)
recent_paths.remove_at(idx_in_array)
recent_paths.append(path)
add_item(path)
select(get_item_count() - 1)
var idx_in_array := recent_paths.find(path)
if idx_in_array != -1:
remove_item(idx_in_array)
recent_paths.remove_at(idx_in_array)
if !is_loading:
editor_view.save_data()
recent_paths.append(path)
add_item(path)
select(get_item_count() - 1)
if !is_loading:
editor_view.save_data()
func remove_selected_path_from_recent():
if get_item_count() == 0:
return
var idx_in_array = selected
recent_paths.remove_at(idx_in_array)
remove_item(idx_in_array)
if get_item_count() != 0:
select(0)
editor_view.display_folder(recent_paths[0])
editor_view.save_data()
func _on_item_selected(index : int):
editor_view.current_path = recent_paths[index]
editor_view.node_folder_path.text = recent_paths[index]
editor_view.refresh()
if get_item_count() == 0:
return
var idx_in_array = selected
recent_paths.remove_at(idx_in_array)
remove_item(idx_in_array)
if get_item_count() != 0:
select(0)
editor_view.display_folder(recent_paths[0])
editor_view.save_data()
func _on_item_selected(index: int):
editor_view.current_path = recent_paths[index]
editor_view.node_folder_path.text = recent_paths[index]
editor_view.refresh()

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save