built more assets and started playing with foliage painting

This commit is contained in:
derek
2024-12-04 17:02:46 -06:00
parent dd960cc00e
commit 478e2822d2
359 changed files with 34172 additions and 178 deletions

View File

@@ -0,0 +1,613 @@
@tool
extends Node3D
#-------------------------------------------------------------------------------
# Handles managing OctreeManager objects and changes applied when painting
# Instigates updates to OctreeManager MutliMeshInstance (MMI) objects
# To show the correct LOD variant when moving closer/further to plants
#-------------------------------------------------------------------------------
# It's worth considering to split this object into mutliple:
# Octree management
# Updating positions of individual plants (through painting)
# Threaded updates to LODs (if possible)
# However, these functions are very closely related, so maybe I'm overthinking this
const Logger = preload("../utility/logger.gd")
const Globals = preload("../utility/globals.gd")
const FunLib = preload("../utility/fun_lib.gd")
const Greenhouse_Plant = preload("../greenhouse/greenhouse_plant.gd")
const Toolshed_Brush = preload("../toolshed/toolshed_brush.gd")
const PaintingChanges = preload("painting_changes.gd")
const MMIOctreeManager = preload("mmi_octree/mmi_octree_manager.gd")
const UndoRedoInterface = preload("../utility/undo_redo_interface.gd")
const StrokeHandler = preload("stroke_handler/stroke_handler.gd")
const SH_Paint = preload("stroke_handler/sh_paint.gd")
const SH_Erase = preload("stroke_handler/sh_erase.gd")
const SH_Single = preload("stroke_handler/sh_single.gd")
const SH_Reapply = preload("stroke_handler/sh_reapply.gd")
const SH_Manual = preload("stroke_handler/sh_manual.gd")
var MMI_container:Node3D = null
var octree_managers:Array
var gardening_collision_mask:int = 0
# A manual override fot the camera (mainly used in Editor)
var active_camera_override:Camera3D = null
var active_stroke_handler:StrokeHandler = null
var active_painting_changes:PaintingChanges = null
# Threading LOD updates is not working for some reason. Gives error "Condition "!multimesh" is true." when closing a scene
# This might be related to https://github.com/godotengine/godot/pull/54650
# Possibly, there are some leftover references after closing a scene and idk how I'm supposed to clean them up
#var mutex_placement:Mutex = null
#var thread_instance_placement:Thread
#var semaphore_instance_placement:Semaphore
#var exit_instance_placement:bool
#var done_instance_placement:bool
var _undo_redo = null
var debug_redraw_requested_managers:Array = []
var logger = null
signal req_debug_redraw(octree_managers, requested_indexes)
signal member_count_updated(octree_index, new_count)
#-------------------------------------------------------------------------------
# Lifecycle and initialization
#-------------------------------------------------------------------------------
func _init():
set_meta("class", "Arborist")
func _ready():
# Workaround below fixes the problem of instanced nodes "sharing" exported arrays (and resources inside them)
# When instanced in the editor
# See https://github.com/godotengine/godot/issues/16478
# This fix is needed, so we can have multiple instances of same terrain with same plant placement
# But have LOD switch independently for each of these terrains
if octree_managers == null:
octree_managers = []
else:
var octree_managers_copy = octree_managers.duplicate()
octree_managers = []
for octree_manager in octree_managers_copy:
octree_managers.append(octree_manager.duplicate_tree())
logger = Logger.get_for(self, name)
owner = get_tree().get_edited_scene_root()
MMI_container = get_node_or_null("MMI_container")
if MMI_container && !is_instance_of(MMI_container, Node3D):
remove_child(MMI_container)
MMI_container.queue_free()
MMI_container = null
if !MMI_container:
FunLib.free_children(self)
MMI_container = Node3D.new()
MMI_container.name = "MMI_container"
add_child(MMI_container)
MMI_container.owner = owner
for octree_manager in octree_managers:
octree_manager.restore_after_load(MMI_container)
func _enter_tree():
pass
# thread_instance_placement = Thread.new()
# mutex_placement = Mutex.new()
# semaphore_instance_placement = Semaphore.new()
#
# exit_instance_placement = false
# done_instance_placement = true
#
# thread_instance_placement.start(Callable(self,"thread_update_LODs"))
func _notification(what):
match what:
NOTIFICATION_PREDELETE:
for octree_manager in octree_managers:
octree_manager.free_refs()
func _exit_tree():
pass
# This is... weird
# Apparently I need to free any Resources that are left after closing a scene
# I'm not exactly sure why
# And it *might* be destructive to do so in editor
#if Engine.is_editor_hint(): return
#for octree_manager in octree_managers:
#octree_manager.destroy()
# mutex_placement.lock()
# exit_instance_placement = true
# done_instance_placement = false
# mutex_placement.unlock()
#
# semaphore_instance_placement.post()
# thread_instance_placement.wait_to_finish()
#
# thread_instance_placement = null
# mutex_placement = null
# semaphore_instance_placement = null
# Expected to be called inside or after a parent's _ready()
func setup(plant_states):
verify_all_plants(plant_states)
# Restore all OctreeManager objects after load
# Create missing ones
func verify_all_plants(plant_states_to_verify:Array):
if !is_inside_tree(): return
debug_print_lifecycle("verifying for plant_states: " + str(plant_states_to_verify))
for plant_index in range(0, plant_states_to_verify.size()):
if octree_managers.size() - 1 >= plant_index:
octree_managers[plant_index].restore_after_load(MMI_container)
connect_octree_manager(octree_managers[plant_index])
else:
add_plant_octree_manager(plant_states_to_verify[plant_index], plant_index)
#-------------------------------------------------------------------------------
# Management of plant OctreeManager objects
#-------------------------------------------------------------------------------
# Instigate the OctreeManager adding process in response to an external signal
func on_plant_added(plant_state, plant_index:int):
debug_print_lifecycle("plant: %s added at plant_index %d" % [str(plant_state), plant_index])
add_plant_octree_manager(plant_state, plant_index)
request_debug_redraw_from_index(plant_index)
call_deferred("emit_member_count", plant_index)
# Instigate the OctreeManager removal process in response to an external signal
func on_plant_removed(plant_state, plant_index:int):
debug_print_lifecycle("plant: %s removed at plant_index %d" % [str(plant_state), plant_index])
remove_plant_octree_manager(plant_state, plant_index)
request_debug_redraw_from_index(plant_index)
# Up-to-date LOD variants of an OctreeManager
func on_LOD_variant_added(plant_index:int, mesh_index:int, LOD_variant):
debug_print_lifecycle("LOD Variant: %s added at plant_index %d and mesh_index %d" % [str(LOD_variant), plant_index, mesh_index])
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.insert_LOD_variant(LOD_variant, mesh_index)
octree_manager.set_LODs_to_active_index()
# Up-to-date LOD variants of an OctreeManager
func on_LOD_variant_removed(plant_index:int, mesh_index:int):
debug_print_lifecycle("LOD Variant: removed at plant_index %d and mesh_index %d" % [plant_index, mesh_index])
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.remove_LOD_variant(mesh_index)
octree_manager.set_LODs_to_active_index()
# Up-to-date LOD variants of an OctreeManager
func on_LOD_variant_set(plant_index:int, mesh_index:int, LOD_variant):
debug_print_lifecycle("LOD Variant: %s set at plant_index %d and mesh_index %d" % [str(LOD_variant), plant_index, mesh_index])
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.set_LOD_variant(LOD_variant, mesh_index)
octree_manager.set_LODs_to_active_index()
# Up-to-date LOD variants of an OctreeManager
func on_LOD_variant_prop_changed_spawned_spatial(plant_index:int, mesh_index:int, LOD_variant):
debug_print_lifecycle("LOD Variant: %s spawned spatial changed at plant_index %d and mesh_index %d" % [str(LOD_variant), plant_index, mesh_index])
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.set_LOD_variant_spawned_spatial(LOD_variant, mesh_index)
octree_manager.reset_member_spatials()
# Make sure LODs in OctreeNodes correspond to their active_LOD_index
# This is the preffered way to 'refresh' MMIs inside OctreeNodes
func set_LODs_to_active_index(plant_index:int):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.set_LODs_to_active_index()
# Initialize an OctreeManager for a given plant
func add_plant_octree_manager(plant_state, plant_index:int):
var octree_manager:MMIOctreeManager = MMIOctreeManager.new()
octree_manager.init_octree(
plant_state.plant.mesh_LOD_max_capacity, plant_state.plant.mesh_LOD_min_size,
Vector3.ZERO, MMI_container, plant_state.plant.mesh_LOD_min_size)
octree_manager.LOD_max_distance = plant_state.plant.mesh_LOD_max_distance
octree_manager.LOD_kill_distance = plant_state.plant.mesh_LOD_kill_distance
octree_managers.insert(plant_index, octree_manager)
for mesh_index in range (0, plant_state.plant.mesh_LOD_variants.size()):
var LOD_variant = plant_state.plant.mesh_LOD_variants[mesh_index]
octree_manager.insert_LOD_variant(LOD_variant, mesh_index)
connect_octree_manager(octree_manager)
# Remove an OctreeManager for a given plant
func remove_plant_octree_manager(plant_state, plant_index:int):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
disconnect_octree_manager(octree_manager)
octree_manager.prepare_for_removal()
octree_managers.remove_at(plant_index)
# A request to reconfigure an octree
func reconfigure_octree(plant_state, plant_index:int):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.rebuild_octree(plant_state.plant.mesh_LOD_max_capacity, plant_state.plant.mesh_LOD_min_size)
# A request to recenter an octree
func recenter_octree(plant_state, plant_index:int):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.recenter_octree()
# Connect all OctreeManager signals
func connect_octree_manager(octree_manager:MMIOctreeManager):
if !octree_manager.req_debug_redraw.is_connected(on_req_debug_redraw):
octree_manager.req_debug_redraw.connect(on_req_debug_redraw.bind(octree_manager))
# Disconnect all OctreeManager signals
func disconnect_octree_manager(octree_manager:MMIOctreeManager):
if octree_manager.req_debug_redraw.is_connected(on_req_debug_redraw):
octree_manager.req_debug_redraw.disconnect(on_req_debug_redraw)
#-------------------------------------------------------------------------------
# Setting/updating variables to outside signals
#-------------------------------------------------------------------------------
# To be called by a signal from Greenhouse_PlantState -> Gardener -> Arborist
func update_plant_LOD_max_distance(plant_index, val):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.LOD_max_distance = val
# To be called by a signal from Greenhouse_PlantState -> Gardener -> Arborist
func update_plant_LOD_kill_distance(plant_index, val):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
octree_manager.LOD_kill_distance = val
# To be called by a signal from Gardener -> Arborist
func set_gardening_collision_mask(_gardening_collision_mask):
gardening_collision_mask = _gardening_collision_mask
#-------------------------------------------------------------------------------
# Application of brushes and transform generation
#-------------------------------------------------------------------------------
# Create PaintingChanges and a StrokeHandler for this specific brush stroke
func on_stroke_started(brush:Toolshed_Brush, plant_states:Array):
var space_state := get_world_3d().direct_space_state
var camera = get_camera_3d()
active_painting_changes = PaintingChanges.new()
match brush.behavior_brush_type:
brush.BrushType.PAINT:
active_stroke_handler = SH_Paint.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
brush.BrushType.ERASE:
active_stroke_handler = SH_Erase.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
brush.BrushType.SINGLE:
active_stroke_handler = SH_Single.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
brush.BrushType.REAPPLY:
active_stroke_handler = SH_Reapply.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
_:
active_stroke_handler = StrokeHandler.new(brush, plant_states, octree_managers, space_state, camera, gardening_collision_mask)
debug_print_lifecycle("Stroke %s started" % [active_stroke_handler.get_meta("class")])
# Draw instances at the new brush position
# And collect them all into one PaintingChanges object
func on_stroke_updated(brush_data:Dictionary):
assert(active_stroke_handler)
assert(active_painting_changes)
debug_print_lifecycle("Stroke %s updating..." % [active_stroke_handler.get_meta("class")])
var msec_start = FunLib.get_msec()
# mutex_placement.lock()
var changes = active_stroke_handler.get_stroke_update_changes(brush_data, global_transform)
apply_stroke_update_changes(changes)
# mutex_placement.unlock()
active_painting_changes.append_changes(changes)
var msec_end = FunLib.get_msec()
debug_print_lifecycle("Total stroke %s update took: %s" % [active_stroke_handler.get_meta("class"), FunLib.msec_to_time(msec_end - msec_start)])
# Use collected PaintingChanges to add UndoRedo actions
func on_stroke_finished():
assert(active_stroke_handler)
assert(active_painting_changes)
UndoRedoInterface.create_action(_undo_redo, "Apply Arborist MMI changes", 0, false, self)
UndoRedoInterface.add_do_method(_undo_redo, _action_apply_changes.bind(active_painting_changes))
UndoRedoInterface.add_undo_method(_undo_redo, _action_apply_changes.bind(active_painting_changes.pop_opposite()))
# We toggle this flag to avoid reapplying already commited changes all over again
UndoRedoInterface.commit_action(_undo_redo, false)
debug_print_lifecycle("Stroke %s finished, total changes made: %d" % [active_stroke_handler.get_meta("class"), active_painting_changes.changes.size()])
active_stroke_handler = null
active_painting_changes = null
# A wrapper for applying changes to avoid reaplying UndoRedo actions on commit_action()
func _action_apply_changes(changes):
# mutex_placement.lock()
apply_stroke_update_changes(changes)
# mutex_placement.unlock()
#-------------------------------------------------------------------------------
# Updating OctreeManager objects
#-------------------------------------------------------------------------------
# Replace LOD_Variants inside of a shared array owned by this OctreeManager
func refresh_octree_shared_LOD_variants(plant_index:int, LOD_variants:Array):
if octree_managers.size() > plant_index:
octree_managers[plant_index].set_LOD_variants(LOD_variants)
# Add changes to corresponding OctreeManager queues
# Then process them all at once
func apply_stroke_update_changes(changes:PaintingChanges):
debug_print_lifecycle(" Applying %d stroke changes" % [changes.changes.size()])
var msec_start = FunLib.get_msec()
var affected_octree_managers := []
for change in changes.changes:
var octree_manager:MMIOctreeManager = octree_managers[change.at_index]
match change.change_type:
0:
octree_manager.queue_placeforms_add(change.new_val)
1:
octree_manager.queue_placeforms_remove(change.new_val)
2:
octree_manager.queue_placeforms_set(change.new_val)
if !affected_octree_managers.has(change.at_index):
affected_octree_managers.append(change.at_index)
for index in affected_octree_managers:
var octree_manager = octree_managers[index]
octree_manager.process_queues()
emit_member_count(index)
var msec_end = FunLib.get_msec()
debug_print_lifecycle(" Applying stroke changes took: %s" % [FunLib.msec_to_time(msec_end - msec_start)])
func emit_member_count(octree_index:int):
member_count_updated.emit(octree_index, octree_managers[octree_index].root_octree_node.get_nested_member_count())
func _process(delta):
# try_update_LODs()
if visible:
update_LODs()
request_debug_redraw()
# Trigger a threaded LOD update
#func try_update_LODs():
# var should_post = false
#
# mutex_placement.lock()
# if done_instance_placement:
# done_instance_placement = false
# should_post = true
# mutex_placement.unlock()
#
# if should_post:
# semaphore_instance_placement.post()
# A function that carries out threaded LOD updates
#func thread_update_LODs(arg = null):
# while true:
# semaphore_instance_placement.wait()
#
# var should_exit = false
# mutex_placement.lock()
# if exit_instance_placement:
# should_exit = true
# mutex_placement.unlock()
# if should_exit: break
#
# mutex_placement.lock()
# update_LODs()
# done_instance_placement = true
# mutex_placement.unlock()
# Instigate LOD updates in OctreeManager objects
func update_LODs():
var camera_to_use:Camera3D = get_camera_3d()
if camera_to_use:
var camera_pos := camera_to_use.global_transform.origin
for octree_manager in octree_managers:
octree_manager.update_LODs(camera_pos, global_transform)
# This exists to properly render instances in editor even if there is no forwarded_input()
else:
for octree_manager in octree_managers:
octree_manager.update_LODs_no_camera()
# Add instances as a batch (mostly, as a result of importing Greenhouse data)
func batch_add_instances(placeforms: Array, plant_idx: int):
active_painting_changes = PaintingChanges.new()
active_stroke_handler = SH_Manual.new()
for placeform in placeforms:
active_stroke_handler.add_instance_placeform(placeform, plant_idx, active_painting_changes)
apply_stroke_update_changes(active_painting_changes)
on_stroke_finished()
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
func _unhandled_input(event):
if is_instance_of(event, InputEventKey) && !event.pressed:
if event.keycode == debug_get_dump_tree_key():
for octree_manager in octree_managers:
logger.info(octree_manager.root_octree_node.debug_dump_tree())
#-------------------------------------------------------------------------------
# Utility
#-------------------------------------------------------------------------------
# A hack to get editor camera
# active_camera_override should be set by a Gardener
# In-game just gets an active viewport's camera
func get_camera_3d():
if is_instance_valid(active_camera_override):
return active_camera_override
else:
active_camera_override = null
return get_viewport().get_camera_3d()
#-------------------------------------------------------------------------------
# Property export
#-------------------------------------------------------------------------------
func _get(property):
match property:
"octree_managers":
return octree_managers
return null
func _set(property, val):
var return_val = true
match property:
"octree_managers":
octree_managers = val
_:
return_val = false
return return_val
func _get_property_list():
var props := [
{
"name": "octree_managers",
"type": TYPE_ARRAY,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE
},
]
return props
func _get_configuration_warnings():
var MMI_container_check = get_node("MMI_container")
if MMI_container_check && is_instance_of(MMI_container_check, Node3D):
return ""
else:
return "Arborist is missing a valid MMI_container child\nSince it should be created automatically, try reloading a scene or recreating a Gardener"
func add_child(node:Node, legible_unique_name:bool = false, internal:InternalMode=0) -> void:
super.add_child(node, legible_unique_name)
update_configuration_warnings()
#-------------------------------------------------------------------------------
# Debug
#-------------------------------------------------------------------------------
# A wrapper to request debug redraw for a specific OctreeManager
func request_debug_redraw_from_index(plant_index):
for index in range(plant_index, octree_managers.size()):
on_req_debug_redraw(octree_managers[index])
# Add an OctreeManager to the debug redraw waiting list
func on_req_debug_redraw(octree_manager:MMIOctreeManager):
if debug_redraw_requested_managers.has(octree_manager): return
debug_redraw_requested_managers.append(octree_manager)
# Request a debug redraw for all OctreeManager objects in a waiting list using a signal
# We manually get all indexes here instead of when an OctreeManager is added to the waiting list
# Because we expect the order of managers might change and indexes will become inaccurate
# Typically called from _process()
func request_debug_redraw():
if debug_redraw_requested_managers.is_empty(): return
var requested_indexes := []
for octree_manager in debug_redraw_requested_managers:
requested_indexes.append(octree_managers.find(octree_manager))
if !requested_indexes.is_empty():
req_debug_redraw.emit(octree_managers)
debug_redraw_requested_managers = []
func debug_get_dump_tree_key():
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/dump_all_octrees_key", 0)
return Globals.index_to_enum(key, Globals.KeyboardKey)
func debug_print_lifecycle(string:String):
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/arborist_log_lifecycle", false): return
logger.info(string)

View File

@@ -0,0 +1,405 @@
@tool
extends RefCounted
#-------------------------------------------------------------------------------
# A helper class that creates a placement area (grid)
# And makes sure all instances of a given plant are evenly spaced
#-------------------------------------------------------------------------------
# It uses a 2D grid-based algorithm to maintain both:
# close-to-desired instance density
# an even instance distribution
# This article was used as a main reference (grid uses a simple logic and gives good-enough results)
# https://www.gamedeveloper.com/disciplines/random-scattering-creating-realistic-landscapes
# However, I was not able to derive a solution that creates a grid that evenly projects on a surface of any angle
# When you approach 45 degrees you start seeing bigger spacing, which becomes unusable by 90 degrees
# So I thought of trying to project a 3D grid, but that raised a lot of questions on it's own
# This is still an open question really
# So I dug through some code online and decided on a system I discuss in BrushPlacementArea section
# Algorithm:
# Use the brush radius, position and the surface normal under it's center to describe a flat circle in 3D space
# This circle provides a plane and bounds for us to generate the transforms. Most other operations operate in 2D space aligned to said plane
# Generate a virtual 2D grid with distance between points representing the plant density (less density == bigger distance)
# Find all "overlapping" plants of the same type in the sphere's radius
# We only check if the placements (origin positions) of plants are within the sphere. No actual boundary tests
# Add a max instance check (how many instances total can fit in a circle) and pad the max distance from center slightly OUTSIDE the circle
# Last one is needed to prevent spawning at half-distance near the circle's edge
# Project the overlaps to our plane and snap them to the closest point on the grid
# These grid points become occupied and cannot spawn a new instance
# All the points that remain will spawn an instance
# Add a random jitter to each placement (it should be smaller than 0.5 to prevent occasional overlap)
# Get the raycast start and end positions
# They are aligned to the plane's normal and limit_length at the sphere's bounds
# Last one prevents out raycast from going outside the sphere
# Back in the StrokeHandler, use these positions to raycast to surface and determine actual placement positions
# The problem with this method is that the grid CAN and WILL be generated at uneven intervals as the brush moves
# An example, where "( )" - last grid, "[ ]" - current grid, "P" - point
# (P)(P)
# (P)(P) <- align
# [P][P] <- align
# [P][P]
# /\
# do not align
# That leads to some points spawning closer than they should
# While not ideal, this is somewhat mitigated with max instances check
# And snapping brush to the virtual grid
# Ideally I would like the grid to be independent from the brush position
# But I don't know how to accurately project 3D grid points on an angled surface while keeping them EVENLY SPACED no matter the angle
# 2D grids give me a close enough result
# TODO Write an article discussing this method that is actually readable by a normal human being
const Globals = preload("../utility/globals.gd")
const FunLib = preload("../utility/fun_lib.gd")
const Logger = preload("../utility/logger.gd")
const MMIOctreeManager = preload("mmi_octree/mmi_octree_manager.gd")
const MMIOctreeNode = preload("mmi_octree/mmi_octree_node.gd")
var sphere_pos:Vector3 = Vector3.ZERO
var sphere_radius:float = 0.0
var sphere_diameter:float = 0.0
var plane_axis_vectors:Array = []
var point_distance:float = 0.0
var grid_linear_size:int = 0
var grid_offset:float = 0.0
var placement_grid:Array = []
var diagonal_dilation:float = 1.0
var placement_overlaps:Array = []
var overlapped_member_data:Array = []
var overdense_member_data:Array = []
var raycast_positions:Array = []
var max_placements_allowed:int = 0
var jitter_fraction:float = 0.0
var logger = null
#-------------------------------------------------------------------------------
# Initialization
#-------------------------------------------------------------------------------
func _init(__sphere_pos:Vector3,__sphere_radius:float,__plane_normal:Vector3,__jitter_fraction:float = 0.6):
logger = Logger.get_for(self)
sphere_pos = __sphere_pos
sphere_radius = __sphere_radius
sphere_diameter = sphere_radius * 2.0
jitter_fraction = __jitter_fraction * 0.5
init_axis_vectors(__plane_normal)
# Find two perpedicular vectors, so all 3 describe a plane/flat circle
func init_axis_vectors(source:Vector3):
var nx: float = abs(source.x)
var ny: float = abs(source.y)
var nz: float = abs(source.z)
var axis1:Vector3
var axis2:Vector3
if nz > nx && nz > ny:
axis1 = Vector3(1,0,0)
else:
axis1 = Vector3(0,0,1)
axis1 = (axis1 - source * axis1.dot(source)).normalized()
axis2 = axis1.cross(source).normalized()
plane_axis_vectors = [source, axis1, axis2]
#-------------------------------------------------------------------------------
# Grid data
#-------------------------------------------------------------------------------
# Initialize vital grid data
# Grid is used to maintain plant spacing defined by density
# Grid data helps to work with that spacing inside a given brush sphere
func init_grid_data(plant_density:float, brush_strength:float):
point_distance = get_point_distance(plant_density, brush_strength)
# Find how many linear segments fit within a diameter
if point_distance > 0.0:
grid_linear_size = floor(sphere_diameter / point_distance)
# Then offset grid_linear_size by half a segment if it's an odd number
if grid_linear_size % 2 == 0:
grid_linear_size += 1
point_distance *= (sphere_diameter / point_distance * 1) / (sphere_diameter / point_distance)
# Subtract 1 because number of segments on a line is always 1 less than the number of points
# Get max length all points occupy and take half of it
grid_offset = (grid_linear_size - 1) * point_distance * -0.5
else:
grid_linear_size = 0
grid_offset = 0.0
init_placement_grid()
debug_visualize_placement_grid("Grid initialized:")
debug_log_grid("point_distance: %f, grid_linear_size: %d, grid_offset: %f" % [point_distance, grid_linear_size, grid_offset])
# Initialize the grid itself excluding points outside the sphere/plane circle
func init_placement_grid():
placement_grid = []
var radius_squared = pow(sphere_radius, 2)
for x in range(0, grid_linear_size):
placement_grid.append([])
placement_grid[x].resize(grid_linear_size)
for y in range(0, grid_linear_size):
var local_pos = grid_coord_to_local_pos(Vector2(x, y))
var dist = local_pos.length_squared()
if dist <= radius_squared:
placement_grid[x][y] = true
max_placements_allowed += 1
else:
placement_grid[x][y] = false
#-------------------------------------------------------------------------------
# Overlaps
#-------------------------------------------------------------------------------
# Initialize placements overlapping with the given sphere
# Resulting placements will be in range (-sphere_radius, sphere_radius)
# Edge extension is used to grad one more/one less loop of overlaps
# Typically used to grab members that were displaced with jitter_fraction
# And happen to be outside our sphere_radius, but still belong to overlapped grid cells
func init_placement_overlaps(octree_manager:MMIOctreeManager, edge_extension:int = 0):
placement_overlaps = []
overlapped_member_data = []
var max_dist = sphere_radius + point_distance * edge_extension
get_overlap_member_data(octree_manager.root_octree_node, max_dist)
# Recursively calculate placement overlaps in an octree
func get_overlap_member_data(octree_node:MMIOctreeNode, max_dist:float):
var max_bounds_to_center_dist = octree_node.max_bounds_to_center_dist
var dist_node: float = clamp((octree_node.center_pos - sphere_pos).length() - max_bounds_to_center_dist - sphere_radius, 0.0, INF)
if dist_node >= max_dist: return
if !octree_node.is_leaf:
for child_node in octree_node.child_nodes:
get_overlap_member_data(child_node, max_dist)
else:
var max_dist_squared = pow(max_dist, 2.0)
var node_address = octree_node.get_address()
for member_idx in range(0, octree_node.member_count()):
var placeform = octree_node.get_placeform(member_idx)
var placement = placeform[0] - sphere_pos
var dist_squared = placement.length_squared()
if dist_squared <= max_dist_squared:
placement_overlaps.append(placement)
overlapped_member_data.append({"node_address": node_address, "member_idx": member_idx})
# Get all overlaps that don't fit into the density grid
func get_placeforms_for_deletion():
if overdense_member_data.size() <= 0: return []
var placeforms_for_deletion := []
# Don't delete more than is exessive or actually overdense
var deletion_count: int = min(overlapped_member_data.size() - max_placements_allowed, overdense_member_data.size())
var deletion_increment := float(deletion_count) / float(overdense_member_data.size())
var deletion_progress := 0.0
if deletion_increment <= 0: return []
# # This part picks every [N]th member for deletion
# # [N] is defined by deletion_increment and can be fractional
# # E.g. we can delete an approximation of every 0.3th, 0.75th, etc. element
for index in range(0, overdense_member_data.size()):
deletion_progress += deletion_increment
if deletion_progress >= 1.0:
deletion_progress -= 1.0
placeforms_for_deletion.append(overdense_member_data[index])
return placeforms_for_deletion
# Mark grid coordinates as invalid if they are already occupied
# (if there is a plant origin near a grid point)
func invalidate_occupied_points():
for placement_index in range(0, placement_overlaps.size()):
var placement_overlap = placement_overlaps[placement_index]
# Project a local-space overlap to our plane
var projected_overlap = Vector2(plane_axis_vectors[1].dot(placement_overlap), plane_axis_vectors[2].dot(placement_overlap))
var grid_coord = local_pos_to_grid_coord(projected_overlap)
grid_coord = Vector2(clamp(grid_coord.x, 0.0, grid_linear_size - 1), clamp(grid_coord.y, 0.0, grid_linear_size - 1))
if placement_grid.size() > 0 && placement_grid[grid_coord.x][grid_coord.y]:
invalidate_self_or_neighbor(grid_coord)
else:
overdense_member_data.append(overlapped_member_data[placement_index])
func invalidate_self_or_neighbor(grid_coord:Vector2):
if placement_grid[grid_coord.x][grid_coord.y]:
placement_grid[grid_coord.x][grid_coord.y] = false
# Because our grid depends on brush position, sometimes we have cells that appear unoccupied
# (Due to placements being *slightly* outside the cell)
# So we nudge our overlaps one cell in whatever direction
# CURRENT VERSION DOESN'T SOLVE THE ISSUE
# THIS NEEDS TO APPROXIMATE THE BEST FITTING CELL BY COMPARING DISTANCES
# AND AT THIS POINT IT'S TOO MANY CALCULATIONS
# SO WE SUSPEND THIS FOR NOW
# else:
# var new_grid_coord
# var offset_lookup = [
# Vector2(-1,-1), Vector2(0,-1), Vector2(1,-1),
# Vector2(-1,0), Vector2(1,0),
# Vector2(-1,1), Vector2(0,1), Vector2(1,1)]
# for offset in offset_lookup:
# new_grid_coord = grid_coord + offset
# if placement_grid[new_grid_coord.x][new_grid_coord.y]:
# placement_grid[new_grid_coord.x][new_grid_coord.y] = false
# break
#-------------------------------------------------------------------------------
# Raycast setup
#-------------------------------------------------------------------------------
# Get raycast start and end positions that go through the whole sphere
# Rays will start and end at the opposite sphere borders
# And will have the same direction as the brush normal
func get_valid_raycast_positions() -> Array:
invalidate_occupied_points()
debug_visualize_placement_grid("Grid invalidated occupied points:")
generate_raycast_positions()
return raycast_positions
# Generate a randomized placement for each point on the grid
# And deproject it onto our brush sphere using the surface normal
func generate_raycast_positions():
raycast_positions = []
for x in range(0, grid_linear_size):
for y in range(0, grid_linear_size):
if !placement_grid[x][y]: continue
var grid_coord := Vector2(x, y)
var UV_jitter := Vector2(randf_range(-jitter_fraction, jitter_fraction), randf_range(-jitter_fraction, jitter_fraction))
grid_coord += UV_jitter
var centered_UV := grid_coord_to_centered_UV(grid_coord)
# Compensating a floating point error by padding the value a bit
if centered_UV.length_squared() > 0.999:
centered_UV = centered_UV.limit_length(0.999)
var UV_distance_to_surface:Vector3 = sqrt(1.0 - (pow(centered_UV.x, 2) + pow(centered_UV.y, 2))) * plane_axis_vectors[0]
var UV_point_on_plane:Vector3 = centered_UV.x * plane_axis_vectors[1] + centered_UV.y * plane_axis_vectors[2]
var raycast_start:Vector3 = sphere_pos + sphere_radius * (UV_point_on_plane + UV_distance_to_surface)
var raycast_end:Vector3 = sphere_pos + sphere_radius * (UV_point_on_plane - UV_distance_to_surface)
raycast_positions.append([raycast_start, raycast_end])
# This was made to make sure we don't go over a max instance limit
# I refactored placement logic to snap brush_pos to a density grid
# Yet it doesn't 100% work on angled surfaces
# We still might go over max placements, hence the limit check below
# The percieved visual density should be unaffected though, especially at high (>= 0.5) jitter
while raycast_positions.size() + placement_overlaps.size() > max_placements_allowed && !raycast_positions.is_empty():
raycast_positions.remove_at(randi() % raycast_positions.size())
#-------------------------------------------------------------------------------
# Utility
#-------------------------------------------------------------------------------
# Get a linear distance between two points
# Separated into a function because we need this in StrokeHandler as well
static func get_point_distance(plant_density, brush_strength) -> float:
# Convert square density to linear density
# Then density per PLANT_DENSITY_UNITS to density per 1 unit
# That is a distance between two points
if brush_strength <= 0.0:
return 0.0
return (Globals.PLANT_DENSITY_UNITS / sqrt(plant_density * brush_strength))
# Convert grid coordinates to local position around the sphere center
# Resulting vector will be in range (-sphere_radius, sphere_radius)
func grid_coord_to_local_pos(grid_coord:Vector2) -> Vector2:
return Vector2(
grid_coord.x * point_distance + grid_offset,
grid_coord.y * point_distance + grid_offset
)
# Convert local position to grid coordinates
# Resulting vector will try to fit in range (0, grid_linear_size)
# (indexes might be outside the grid if positions lie outside, so the result usually should be limit_length or rejected manually)
func local_pos_to_grid_coord(local_pos:Vector2) -> Vector2:
if point_distance <= 0.0:
return Vector2.ZERO
return Vector2(
round((local_pos.x - grid_offset) / point_distance),
round((local_pos.y - grid_offset) / point_distance)
)
# Convert grid coordinates to UV space in range (-1.0, 1.0)
func grid_coord_to_centered_UV(grid_coord:Vector2) -> Vector2:
var local_pos = grid_coord_to_local_pos(grid_coord)
return local_pos / sphere_radius
#-------------------------------------------------------------------------------
# Debug
#-------------------------------------------------------------------------------
# Print a nicely formatted placement grid to console
func debug_visualize_placement_grid(prefix:String = ""):
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/brush_placement_area_log_grid", false): return
if prefix != "":
logger.info(prefix)
if placement_grid.size() > 0:
for y in range(0, placement_grid[0].size()):
var string = "|"
for x in range(0, placement_grid.size()):
if placement_grid[x][y]:
string += " "
else:
string += "X"
if x < placement_grid[x].size() - 1:
string += "|"
string += "|"
logger.info(string)
print("\n")
func debug_log_grid(string:String):
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/brush_placement_area_log_grid", false): return
logger.info(string)

View File

@@ -0,0 +1,369 @@
@tool
extends Resource
#-------------------------------------------------------------------------------
# Handles higher-level management of OctreeNode objects
# Creation of new trees (octree roots), some of the growing/collapsing functionality
# Exposes lifecycle management to outside programs
# And passes changes made to members/plants to its OctreeNodes
#-------------------------------------------------------------------------------
const MMIOctreeNode = preload("mmi_octree_node.gd")
const FunLib = preload("../../utility/fun_lib.gd")
const DponDebugDraw = preload("../../utility/debug_draw.gd")
const GreenhouseLODVariant = preload("../../greenhouse/greenhouse_LOD_variant.gd")
@export var root_octree_node: Resource = null
@export var LOD_variants : Array[GreenhouseLODVariant] : set = set_LOD_variants
@export var LOD_max_distance:float
@export var LOD_kill_distance:float
var add_placeforms_queue:Array
var remove_placeforms_queue:Array
var set_placeforms_queue:Array
signal req_debug_redraw
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init():
resource_local_to_scene = true
set_meta("class", "MMIOctreeManager")
resource_name = "MMIOctreeManager"
if LOD_variants == null || LOD_variants.is_empty():
LOD_variants = []
add_placeforms_queue = []
remove_placeforms_queue = []
set_placeforms_queue = []
func _notification(what):
if what == NOTIFICATION_PREDELETE:
if is_instance_valid(root_octree_node):
# Avoid memory leaks when OctreeNode leaks MMI nodes and spawned spatials
root_octree_node.prepare_for_removal()
# Duplictes the octree structure
func duplicate_tree():
var copy = duplicate(false)
copy.root_octree_node = copy.root_octree_node.duplicate_tree()
copy.connect_node(copy.root_octree_node)
LOD_variants = LOD_variants.duplicate()
return copy
func deep_copy():
var copy = duplicate(false)
copy.root_octree_node = copy.root_octree_node.deep_copy()
copy.connect_node(copy.root_octree_node)
LOD_variants = LOD_variants.duplicate()
return copy
# Restore any states that might be broken after loading OctreeNode objects
func restore_after_load(__MMI_container:Node3D):
if is_instance_valid(root_octree_node):
root_octree_node.restore_after_load(__MMI_container, LOD_variants)
connect_node(root_octree_node)
request_debug_redraw()
func init_octree(members_per_node:int, root_extent:float, center:Vector3 = Vector3.ZERO, MMI_container:Node3D = null, min_leaf_extent:float = 0.0):
root_octree_node = MMIOctreeNode.new(null, members_per_node, root_extent, center, -1, min_leaf_extent, MMI_container, LOD_variants)
connect_node(root_octree_node)
request_debug_redraw()
func connect_node(octree_node:MMIOctreeNode):
assert(octree_node)
FunLib.ensure_signal(octree_node.placeforms_rejected, grow_to_members)
FunLib.ensure_signal(octree_node.collapse_self_possible, collapse_root)
FunLib.ensure_signal(octree_node.req_debug_redraw, request_debug_redraw)
func disconnect_node(octree_node:MMIOctreeNode):
assert(octree_node)
octree_node.placeforms_rejected.disconnect(grow_to_members)
octree_node.collapse_self_possible.disconnect(collapse_root)
octree_node.req_debug_redraw.disconnect(request_debug_redraw)
func prepare_for_removal():
if root_octree_node:
root_octree_node.prepare_for_removal()
# Free anything that might incur a circular reference or a memory leak
# Anything that is @export'ed is NOT touched here
# We count on Godot's own systems to handle that in whatever way works best
# TODO: this is very similar to prepare_for_removal(), need to determine how best to combine the two
# will need to happen around v2.0.0, since it's a very risky change
func free_refs():
if !root_octree_node: return
root_octree_node.free_refs()
#-------------------------------------------------------------------------------
# Restructuring
#-------------------------------------------------------------------------------
# Rebuild the tree with new extent and member limitations
# The resulting octree node layout depends on the order of members in which they are added
# Hence the layout may difer if the members are the same, but belong to different nodes each time
# I.e. it can't be predicted with members_per_node and min_leaf_extent alone, for now it is (as far as it matters) non-deterministic
func rebuild_octree(members_per_node:int, min_leaf_extent:float):
assert(root_octree_node)
var all_placeforms:Array = []
root_octree_node.get_nested_placeforms(all_placeforms)
root_octree_node.prepare_for_removal()
init_octree(members_per_node, min_leaf_extent, Vector3.ZERO,
root_octree_node.MMI_container, min_leaf_extent)
if !all_placeforms.is_empty():
queue_placeforms_add_bulk(all_placeforms)
process_queues()
request_debug_redraw()
debug_manual_root_logger("rebuilt root")
# Recenter a tree and shrink to fit it's current members
func recenter_octree():
assert(root_octree_node)
var last_root:MMIOctreeNode = root_octree_node
var all_placeforms:Array = []
last_root.get_nested_placeforms(all_placeforms)
last_root.prepare_for_removal()
var new_center:Vector3 = Vector3.ZERO
var new_extent:float = last_root.min_leaf_extent
if all_placeforms.size() > 0:
for placeform in all_placeforms:
new_center += placeform[0]
new_center /= all_placeforms.size()
for placeform in all_placeforms:
var delta_pos = (placeform[0] - new_center).abs()
new_extent = max(new_extent, max(delta_pos.x, max(delta_pos.y, delta_pos.z)))
init_octree(last_root.max_members, new_extent, new_center,
root_octree_node.MMI_container, last_root.min_leaf_extent)
if !all_placeforms.is_empty():
queue_placeforms_add_bulk(all_placeforms)
process_queues()
request_debug_redraw()
debug_manual_root_logger("recentered root")
# Grow the tree to fit any members outside it's current bounds (by creating a whole new layer on top)
func grow_to_members(placeforms:Array):
assert(root_octree_node) # 'root_octree_node' is not initialized
assert(placeforms.size() > 0) # 'placeforms' is empty
var target_point = placeforms[0][0]
var last_root:MMIOctreeNode = root_octree_node
disconnect_node(last_root)
var last_octant = last_root._map_point_to_opposite_octant(target_point)
var new_center = last_root.center_pos - last_root._get_octant_center_offset(last_octant)
init_octree(last_root.max_members, last_root.extent * 2.0, new_center, last_root.MMI_container, last_root.min_leaf_extent)
debug_manual_root_logger("grew to members")
root_octree_node.adopt_child(last_root, last_octant)
var root_copy = root_octree_node
add_placeforms(placeforms)
root_copy.try_collapse_children(0)
# Make one of the root's children the new root
func collapse_root(new_root_octant):
assert(root_octree_node) # 'root_octree_node' is not initialized
var last_root:MMIOctreeNode = root_octree_node
disconnect_node(last_root)
root_octree_node = last_root.child_nodes[new_root_octant]
last_root.collapse_self(new_root_octant)
connect_node(root_octree_node)
root_octree_node.safe_init_root()
root_octree_node.try_collapse_self(0)
func get_all_placeforms(target_array: Array = []):
if root_octree_node:
return root_octree_node.get_nested_placeforms(target_array)
#-------------------------------------------------------------------------------
# Processing members
#-------------------------------------------------------------------------------
# Queue changes for bulk processing
func queue_placeforms_add(placeform):
add_placeforms_queue.append(placeform)
func queue_placeforms_add_bulk(placeforms: Array):
add_placeforms_queue.append_array(placeforms)
# Queue changes for bulk processing
func queue_placeforms_remove(placeform):
remove_placeforms_queue.append(placeform)
# Queue changes for bulk processing
func queue_placeforms_set(change):
set_placeforms_queue.append(change)
# Bulk process the queues
func process_queues():
assert(root_octree_node) # 'root_octree_node' is not initialized
var affected_addressed := []
if !add_placeforms_queue.is_empty():
add_placeforms(add_placeforms_queue)
if !remove_placeforms_queue.is_empty():
remove_placeforms(remove_placeforms_queue)
if !set_placeforms_queue.is_empty():
set_placeforms(set_placeforms_queue)
add_placeforms_queue = []
remove_placeforms_queue = []
set_placeforms_queue = []
# Make sure we update LODs even for nodes at max LOD index
# Since we changed their children most likely
set_LODs_to_active_index()
func add_placeforms(placeforms:Array):
assert(root_octree_node) # 'root_octree_node' is not initialized
assert(placeforms.size() > 0) # 'placeforms' is empty
root_octree_node.add_members(placeforms)
root_octree_node.MMI_refresh_instance_placements_recursive()
request_debug_redraw()
func remove_placeforms(placeforms:Array):
assert(root_octree_node) # 'root_octree_node' is not initialized
assert(placeforms.size() > 0) # 'placeforms' is empty
root_octree_node.remove_members(placeforms)
root_octree_node.process_collapse_children()
root_octree_node.process_collapse_self()
root_octree_node.MMI_refresh_instance_placements_recursive()
request_debug_redraw()
# if root_octree_node.child_nodes.size() <= 0 && root_octree_node.members.size() <= 0:
# reset_root_size()
func set_placeforms(changes:Array):
assert(root_octree_node) # 'root_octree_node' is not initialized
assert(changes.size() > 0) # 'changes' is empty
root_octree_node.set_members(changes)
#-------------------------------------------------------------------------------
# LOD management
#-------------------------------------------------------------------------------
func set_LOD_variants(val):
LOD_variants.resize(0)
for LOD_variant in val:
LOD_variants.append(LOD_variant)
# Up-to-date LOD variants of an OctreeNode
func insert_LOD_variant(variant, index:int):
LOD_variants.insert(index, variant)
# Up-to-date LOD variants of an OctreeNode
func remove_LOD_variant(index:int):
LOD_variants.remove_at(index)
# Up-to-date LOD variants of an OctreeNode
func set_LOD_variant(variant, index:int):
LOD_variants[index] = variant
# Up-to-date LOD variants of an OctreeNode
func set_LOD_variant_spawned_spatial(variant, index:int):
# No need to manually set spawned_spatial, it will be inherited from parent resource
# /\ I don't quite remember what this comment meant, but since LOD_Variants are shared
# It seems to imply that the line below in not neccessary
# So I commented it out for now
# LOD_variants[index].spawned_spatial = variant
pass
func reset_member_spatials():
root_octree_node.reset_member_spatials()
# Make sure LODs in OctreeNodes correspond to their active_LOD_index
# This is the preffered way to 'refresh' MMIs inside OctreeNodes
func set_LODs_to_active_index():
root_octree_node.set_LODs_to_active_index()
# Update LODs in OctreeNodes depending on their distance to camera
func update_LODs(camera_pos:Vector3, container_transform:Transform3D):
camera_pos = container_transform.affine_inverse() * camera_pos
root_octree_node.update_LODs(camera_pos, LOD_max_distance, LOD_kill_distance)
func update_LODs_no_camera():
root_octree_node.update_LODs(Vector3.ZERO, -1.0, -1.0)
#-------------------------------------------------------------------------------
# Debug
#-------------------------------------------------------------------------------
# A callback to request a debug redraw
func request_debug_redraw():
req_debug_redraw.emit()
# Manually trigger a Logger message when an OctreeNode doesn't know an important action happened
func debug_manual_root_logger(message:String):
root_octree_node.print_address(message)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
@tool
extends RefCounted
#-------------------------------------------------------------------------------
# A storage object for changes to the octree members
# To be passed to UndoRedo or executed on the spot
# Can also generate opposite actions (provided it's set up correctly)
#-------------------------------------------------------------------------------
enum ChangeType {APPEND, ERASE, SET}
var changes:Array = []
var _opposite_changes:Array = []
func _init(__changes:Array = []):
changes = __changes
# Add both the current and opposite changes
func add_change(change_type:int, at_index:int, new_val, old_val):
var change:Change = Change.new(change_type, at_index, new_val, old_val)
changes.append(change)
_opposite_changes.append(get_opposite_change(change))
# Append second PaintingChanges non-destructively to the second object
func append_changes(painting_changes):
for change in painting_changes.changes:
add_change(change.change_type, change.at_index, change.new_val, change.old_val)
# Generate an opposite action
# For now it really just swaps new_val and old_val
# But I'm keeping it as-is just in case I need something more complex
func get_opposite_change(change):
var opposite_change:Change = null
match change.change_type:
ChangeType.APPEND:
opposite_change = Change.new(ChangeType.ERASE, change.at_index, change.old_val, change.new_val)
ChangeType.ERASE:
opposite_change = Change.new(ChangeType.APPEND, change.at_index, change.old_val, change.new_val)
ChangeType.SET:
opposite_change = Change.new(ChangeType.SET, change.at_index, change.old_val, change.new_val)
return opposite_change
# Get all opposite changes as a new PaintingChanges object and remove them from the current one
func pop_opposite():
var opposite = get_script().new(_opposite_changes)
_opposite_changes = []
return opposite
func _to_string():
var string = "["
for change in changes:
string += str(change) + ","
string.trim_suffix(",")
string += "]"
return string
#-------------------------------------------------------------------------------
# A storage object for a specific octree member change
# To be generated and stored by PaintingChanges
#-------------------------------------------------------------------------------
class Change extends RefCounted:
var change_type:int = -1
var at_index:int = -1
var new_val = null
var old_val = null
func _init(_change_type:int = -1, _at_index:int = -1, _new_val = null, _old_val = null):
change_type = _change_type
at_index = _at_index
new_val = _new_val
old_val = _old_val
func _to_string():
return "[%d, %d, %s, %s]" % [change_type, at_index, str(new_val), str(old_val)]

View File

@@ -0,0 +1,51 @@
@tool
extends RefCounted
#-------------------------------------------------------------------------------
# PLACEment transFORM
# A pseudo-struct meant to store a placement (initial position), surface normal
# Final Transform3D and an occupied octree octant (what part of the 2x2x2 cube it's in)
#
# Originally was a resource, but after some quick tests, the overhead of having
# Thousands of Resources as simple containers became apparent
# It was decided to use an Array as a fastest and most compact built-in container
#
# This script provides an function library to more easily construct such arrays
# And provide access to methods that were formerly part of this Resource
#-------------------------------------------------------------------------------
# [0] - placement,
# [1] - surface_normal
# [2] - transform
# [3] - octree_octant
static func mk(placement:Vector3 = Vector3(), surface_normal:Vector3 = Vector3(), transform:Transform3D = Transform3D(), octree_octant:int = 0) -> Array:
return [
# A designated position for an instance
placement,
# A normal of the surface
surface_normal,
# An actual transform derived from placement including random offsets, rotations, scaling, etc.
transform,
# Occupied octant is mostly used to quick access the child node of an octree node
# E.g. when aplying PaintingChanges
octree_octant
]
static func to_str(placeform: Array) -> String:
return '[%s, %s, %s, %s, %d]' % [str(placeform[0]), str(placeform[1]), str(placeform[2].basis), str(placeform[2].origin), placeform[3]]
static func get_origin_offset(placeform: Array) -> float:
var difference = placeform[2].origin - placeform[0]
var offset = placeform[1].dot(difference.normalized()) * difference.length()
return offset
static func set_placement_from_origin_offset(placeform: Array, offset: float):
placeform[0] = placeform[2].origin - placeform[1] * offset
return placeform

View File

@@ -0,0 +1,49 @@
@tool
extends Resource
#-------------------------------------------------------------------------------
# A resource that stores a placement (initial position), surface normal
# Final Transform3D and an occupied octree octant (what part of the 2x2x2 cube it's in)
# Ideally this should be a struct to avoid the overhead of saving/loading thousands of small resources
#-------------------------------------------------------------------------------
# TODO find out if there *is* any overhead to this and if yes - make this a C++ struct or at least a GDScript dictionary
# A designated position for an instance
@export var placement:Vector3 = Vector3()
# A normal of the surface
@export var surface_normal:Vector3 = Vector3()
# An actual transform derived from placement including random offsets, rotations, scaling, etc.
@export var transform:Transform3D = Transform3D()
# Occupied octant is mostly used to quick access the child node of an octree node
# E.g. when aplying PaintingChanges
@export var octree_octant:int = 0
func _init(_placement:Vector3 = Vector3(), _surface_normal:Vector3 = Vector3(), _transform:Transform3D = Transform3D(), _octree_octant:int = 0):
set_meta("class", "PlacementTransform")
resource_name = "PlacementTransform"
placement = _placement
surface_normal = _surface_normal
transform = _transform
octree_octant = _octree_octant
func _to_string():
return '[%s, %s, %s, %s, %d]' % [str(placement), str(surface_normal), str(transform.origin), str(transform.basis), octree_octant]
func get_origin_offset() -> float:
var difference = transform.origin - placement
var offset = surface_normal.dot(difference.normalized()) * difference.length()
return offset
func set_placement_from_origin_offset(offset: float) -> Resource:
placement = transform.origin - surface_normal * offset
return self

View File

@@ -0,0 +1,40 @@
@tool
extends "stroke_handler.gd"
#-------------------------------------------------------------------------------
# Handle a regular erasing brush stroke
#-------------------------------------------------------------------------------
# Remove members from an octree according to the target density
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
super(_brush, _plant_states, _octree_managers, _space_state, _camera, _collision_mask)
set_meta("class", "SH_Erase")
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
# We create a grid and detect overlaps
brush_placement_area.init_grid_data(plant.density_per_units, 1.0 - brush.behavior_strength)
# brush_placement_area.max_placements_allowed *= 1.0 - brush.behavior_strength
brush_placement_area.init_placement_overlaps(octree_manager)
# We get overdense members - those that can't fit in a grid since their cells are already occupied
# Then erase all of them
brush_placement_area.invalidate_occupied_points()
var placeforms_data_for_deletion = brush_placement_area.get_placeforms_for_deletion()
for placeform_data_for_deletion in placeforms_data_for_deletion:
var octree_node = octree_manager.root_octree_node.find_child_by_address(placeform_data_for_deletion.node_address)
var placeform = octree_node.get_placeform(placeform_data_for_deletion.member_idx)
painting_changes.add_change(PaintingChanges.ChangeType.ERASE, plant_index, placeform, placeform)
# No brush strength - no member filtering needed
# Just make changes with ALL overlapped points
func proj_get_stroke_update_changes(placeforms_data_in_brush: Array, plant:Greenhouse_Plant, plant_index: int, octree_manager:MMIOctreeManager, painting_changes:PaintingChanges):
for placeform_data in placeforms_data_in_brush:
painting_changes.add_change(PaintingChanges.ChangeType.ERASE, plant_index, placeform_data.placeform, placeform_data.placeform)

View File

@@ -0,0 +1,28 @@
@tool
extends "stroke_handler.gd"
#-------------------------------------------------------------------------------
# Make painting changes by manually supplying data
#-------------------------------------------------------------------------------
# Meant to be used from code to add or remove instances
func _init():
super(null, [], [], null, null, -1)
set_meta("class", "SH_Manual")
func add_instance(placement: Vector3, surface_normal: Vector3, transform: Transform3D, plant_index: int, painting_changes: PaintingChanges):
var placeform:Array = Placeform.mk(placement, surface_normal, transform)
painting_changes.add_change(PaintingChanges.ChangeType.APPEND, plant_index, placeform, placeform)
func add_instance_placeform(placeform: Array, plant_index: int, painting_changes: PaintingChanges):
painting_changes.add_change(PaintingChanges.ChangeType.APPEND, plant_index, placeform, placeform)
func remove_instance(placeform: Array, plant_index: int, painting_changes: PaintingChanges):
painting_changes.add_change(PaintingChanges.ChangeType.ERASE, plant_index, placeform, placeform)

View File

@@ -0,0 +1,48 @@
@tool
extends "stroke_handler.gd"
#-------------------------------------------------------------------------------
# Handle a regular painting brush stroke
#-------------------------------------------------------------------------------
# Add members to an octree according to the target density
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
super(_brush, _plant_states, _octree_managers, _space_state, _camera, _collision_mask)
set_meta("class", "SH_Paint")
func should_abort_early(brush_data:Dictionary):
if brush.behavior_overlap_mode == Toolshed_Brush.OverlapMode.PROJECTION: return true
if brush.behavior_strength <= 0.0: return true
return false
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
# We create a grid, detect overlaps and get a list of raycast positions that aren't occupied
brush_placement_area.init_grid_data(plant.density_per_units, brush.behavior_strength)
# Previously we expanded the search area by 1 unit to eliminate placing instances right outside our area as it moves
# (since these would seem onoccupied to the placement logic)
# But I turned on placement amount limiter and it looks surprisingly fine
# And as a result doesn't cause a bug where small brushes with small density place plants *too* rarely (because of that search expansion)
brush_placement_area.init_placement_overlaps(octree_manager, 0)#1)
var raycast_positions = brush_placement_area.get_valid_raycast_positions()
for raycast_position in raycast_positions:
# We raycast along the surface normal using brush sphere as our bounds
raycast_position[0] = container_transform * raycast_position[0]
raycast_position[1] = container_transform * raycast_position[1]
var params = PhysicsRayQueryParameters3D.create(raycast_position[0], raycast_position[1])
var ray_result = space_state.intersect_ray(params)
if !ray_result.is_empty() && ray_result.collider.collision_layer & collision_mask:
if !TransformGenerator.is_plant_slope_allowed(ray_result.normal, plant): continue
# Generate transforms and add them to the array
var member_pos = container_transform.affine_inverse() * ray_result.position
var plant_transform:Transform3D = TransformGenerator.generate_plant_transform(member_pos, ray_result.normal, plant, randomizer)
var placeform: Array = Placeform.mk(member_pos, ray_result.normal, plant_transform)
painting_changes.add_change(PaintingChanges.ChangeType.APPEND, plant_index, placeform, placeform)

View File

@@ -0,0 +1,57 @@
@tool
extends "stroke_handler.gd"
#-------------------------------------------------------------------------------
# Handle a reapply transforms brush stroke
#-------------------------------------------------------------------------------
# Get overlapping placements and generate a new Transform3D for each of them
# We keep references to placements we already reapplied as to not continously regenerate them
var reapplied_octree_members:Array
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
super(_brush, _plant_states, _octree_managers, _space_state, _camera, _collision_mask)
set_meta("class", "SH_Reapply")
reapplied_octree_members = []
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
# We detect overlaps first
brush_placement_area.init_placement_overlaps(octree_manager)
# For each overlap we generate a new Transform3D and add it to the PaintingChange
create_painting_changes(brush_placement_area.overlapped_member_data, plant, plant_index, octree_manager, painting_changes)
func proj_get_stroke_update_changes(placeform_data_array: Array, plant:Greenhouse_Plant, plant_index: int, octree_manager:MMIOctreeManager, painting_changes:PaintingChanges):
create_painting_changes(placeform_data_array, plant, plant_index, octree_manager, painting_changes)
func create_painting_changes(placeform_data_array: Array, plant:Greenhouse_Plant, plant_index: int, octree_manager:MMIOctreeManager, painting_changes:PaintingChanges):
for placeform_data in placeform_data_array:
var octree_node = octree_manager.root_octree_node.find_child_by_address(placeform_data.node_address)
var placeform = octree_node.get_placeform(placeform_data.member_idx)
# We use Vector3.to_string() to generate our reference keys
# I assume it's fully deterministic at least in the scope of an individual OS
var octree_member_key = str(placeform[0])
if reapplied_octree_members.has(octree_member_key): continue
reapplied_octree_members.append(octree_member_key)
var plant_transform := TransformGenerator.generate_plant_transform(placeform[0], placeform[1], plant, randomizer)
var new_placeform := Placeform.mk(placeform[0], placeform[1], plant_transform, placeform[3])
# Painting changes here are non-standart: they actually have an octree node address and member index bundled
# We can't reliably use an address when adding/removing members since the octree might grow/collapse
# But here it's fine since we don't change the amount of members
painting_changes.add_change(PaintingChanges.ChangeType.SET, plant_index,
{"placeform": new_placeform, "index": placeform_data.member_idx, "address": placeform_data.node_address},
{"placeform": placeform, "index": placeform_data.member_idx, "address": placeform_data.node_address})

View File

@@ -0,0 +1,32 @@
@tool
extends "stroke_handler.gd"
#-------------------------------------------------------------------------------
# Handle a single placement brush stroke
#-------------------------------------------------------------------------------
# Meant to be used with single mouse clicks
# Dragging while pressing will place as many instances as the framerate allows
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
super(_brush, _plant_states, _octree_managers, _space_state, _camera, _collision_mask)
set_meta("class", "SH_Single")
func should_abort_early(brush_data:Dictionary):
if brush.behavior_overlap_mode == Toolshed_Brush.OverlapMode.PROJECTION: return true
return false
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
var member_pos = brush_data.brush_pos
var plant_transform: Transform3D = TransformGenerator.generate_plant_transform(member_pos, brush_data.brush_normal, plant, randomizer)
var placeform: Array = Placeform.mk(member_pos, brush_data.brush_normal, plant_transform)
painting_changes.add_change(PaintingChanges.ChangeType.APPEND, plant_index, placeform, placeform)

View File

@@ -0,0 +1,417 @@
@tool
extends RefCounted
#-------------------------------------------------------------------------------
# A base object that gathers plant positions/overlaps
# And generates neccessary PaintingChange depending on the stroke type it handles
#-------------------------------------------------------------------------------
const Logger = preload("../../utility/logger.gd")
const FunLib = preload("../../utility/fun_lib.gd")
const DponDebugDraw = preload("../../utility/debug_draw.gd")
const Greenhouse_Plant = preload("../../greenhouse/greenhouse_plant.gd")
const Placeform = preload("../placeform.gd")
const Toolshed_Brush = preload("../../toolshed/toolshed_brush.gd")
const BrushPlacementArea = preload("../brush_placement_area.gd")
const TransformGenerator = preload("../transform_generator.gd")
const PaintingChanges = preload("../painting_changes.gd")
const MMIOctreeManager = preload("../mmi_octree/mmi_octree_manager.gd")
const MMIOctreeNode = preload("../mmi_octree/mmi_octree_node.gd")
var randomizer:RandomNumberGenerator
var transform_generator:TransformGenerator
var brush:Toolshed_Brush
var plant_states:Array
var octree_managers:Array
var space_state:PhysicsDirectSpaceState3D
var camera: Camera3D
var collision_mask:int
var debug_draw_enabled: bool = false
var simplify_projection_frustum: bool = false
# Mouse position in screen space cached for one update cycle
var _cached_mouse_pos: Vector2 = Vector2()
var logger = null
#-------------------------------------------------------------------------------
# Initialization
#-------------------------------------------------------------------------------
func _init(_brush:Toolshed_Brush, _plant_states:Array, _octree_managers:Array, _space_state:PhysicsDirectSpaceState3D, _camera: Camera3D, _collision_mask:int):
set_meta("class", "StrokeHandler")
brush = _brush
plant_states = _plant_states
octree_managers = _octree_managers
space_state = _space_state
camera = _camera
collision_mask = _collision_mask
randomizer = RandomNumberGenerator.new()
randomizer.seed = Time.get_ticks_msec()
logger = Logger.get_for(self)
debug_draw_enabled = FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/stroke_handler_debug_draw", true)
simplify_projection_frustum = FunLib.get_setting_safe("dreadpons_spatial_gardener/painting/simplify_projection_frustum", true)
if debug_draw_enabled:
debug_mk_debug_draw()
#-------------------------------------------------------------------------------
# PaintingChange management
#-------------------------------------------------------------------------------
# A method for building PaintingChanges
# If we have 'brush' in _init(), we do we need 'brush_data' you might ask
# That's because 'brush' gives us brush settings (size, strength, etc.)
# While 'brush_data' gives up-to-date transformations and surface normal of a brush in world-space
func get_stroke_update_changes(brush_data:Dictionary, container_transform:Transform3D) -> PaintingChanges:
var painting_changes = PaintingChanges.new()
if should_abort_early(brush_data): return painting_changes
var msec_start = FunLib.get_msec()
brush_data = modify_brush_data_to_container(brush_data, container_transform)
for plant_index in range(0, plant_states.size()):
if !plant_states[plant_index].plant_brush_active: continue
var plant = plant_states[plant_index].plant
handle_plant_stroke(brush_data, container_transform, plant, plant_index, painting_changes)
var msec_end = FunLib.get_msec()
debug_print_lifecycle(" Stroke %s changes update took: %s" % [get_meta("class"), FunLib.msec_to_time(msec_end - msec_start)])
return painting_changes
# Are there any conditions that force us to abort before anything starts?
func should_abort_early(brush_data:Dictionary):
return false
func modify_brush_data_to_container(brush_data:Dictionary, container_transform:Transform3D) -> Dictionary:
match brush.behavior_overlap_mode:
Toolshed_Brush.OverlapMode.VOLUME:
return volume_modify_brush_data_to_container(brush_data, container_transform)
return brush_data
func handle_plant_stroke(brush_data:Dictionary, container_transform:Transform3D, plant:Greenhouse_Plant, plant_index:int, painting_changes:PaintingChanges):
var octree_manager:MMIOctreeManager = octree_managers[plant_index]
match brush.behavior_overlap_mode:
Toolshed_Brush.OverlapMode.VOLUME:
var plant_brush_data = volume_modify_brush_data_to_plant(brush_data, plant)
# BrushPlacementArea is the "brains" of my placement logic, so we initialize it here
var brush_placement_area := BrushPlacementArea.new(plant_brush_data.brush_pos, brush.shape_volume_size * 0.5, plant_brush_data.brush_normal, plant.offset_jitter_fraction)
volume_get_stroke_update_changes(plant_brush_data, plant, plant_index, octree_manager, brush_placement_area, container_transform, painting_changes)
Toolshed_Brush.OverlapMode.PROJECTION:
# This will store dictionaries so it shares the same data structure as brush_placement_area.overlapped_member_data
# But with member itself added into the mix. It'll look like this:
# [{"node_address": node_address, "member_idx": member_idx, "placeform": placeform}, ...]
var placeforms_in_brush := []
var frustum_planes := []
_cached_mouse_pos = camera.get_viewport().get_mouse_position()
proj_define_frustum(brush_data, frustum_planes)
proj_get_placeforms_data_in_frustum(frustum_planes, placeforms_in_brush, octree_manager.root_octree_node, container_transform)
proj_filter_placeforms_to_brush_circle(placeforms_in_brush, container_transform)
if !brush.behavior_passthrough:
proj_filter_obstructed_placeforms(placeforms_in_brush, container_transform)
proj_get_stroke_update_changes(placeforms_in_brush, plant, plant_index, octree_manager, painting_changes)
#-------------------------------------------------------------------------------
# Volume brush handling
#-------------------------------------------------------------------------------
func volume_modify_brush_data_to_container(brush_data:Dictionary, container_transform:Transform3D):
brush_data = brush_data.duplicate()
brush_data.brush_pos = container_transform.affine_inverse() * brush_data.brush_pos
return brush_data
# Modify the brush data according to the plant
func volume_modify_brush_data_to_plant(brush_data:Dictionary, plant) -> Dictionary:
# Here used to be code to snap the brush_pos to the virtual density grid of a plant
return brush_data.duplicate()
# Called when the Painter brush stroke is updated (moved)
# To be overridden
func volume_get_stroke_update_changes(brush_data:Dictionary, plant:Greenhouse_Plant, plant_index:int, octree_manager:MMIOctreeManager,
brush_placement_area:BrushPlacementArea, container_transform:Transform3D, painting_changes:PaintingChanges):
return null
#-------------------------------------------------------------------------------
# Projection brush handling
#-------------------------------------------------------------------------------
func proj_filter_placeforms_to_brush_circle(placeforms_data_in_frustum: Array, container_transform:Transform3D):
var brush_radius_squared: float = pow(brush.shape_projection_size * 0.5, 2.0)
var viewport_size: Vector2i = camera.get_viewport().size
for i in range(placeforms_data_in_frustum.size() -1, -1, -1):
var placeform_data = placeforms_data_in_frustum[i]
var placement = container_transform * placeform_data.placeform[0]
var screen_space_pos := camera.unproject_position(placement)
var dist_squared = (screen_space_pos - _cached_mouse_pos).length_squared()
# Remove those outside brush radius
if dist_squared > brush_radius_squared:
placeforms_data_in_frustum.remove_at(i)
# Remove those outside viewport
elif screen_space_pos.x < 0 || screen_space_pos.y < 0 || screen_space_pos.x > viewport_size.x || screen_space_pos.y > viewport_size.y:
placeforms_data_in_frustum.remove_at(i)
func proj_define_frustum(brush_data:Dictionary, frustum_planes: Array) -> Array:
var brush_half_size = brush.shape_projection_size * 0.5
var brush_rect := Rect2(-brush_half_size, -brush_half_size, brush.shape_projection_size, brush.shape_projection_size)
var frustum_points := []
frustum_points.resize(8)
frustum_planes.resize(6)
project_frustum_points(frustum_points, brush_rect)
define_frustum_plane_array(frustum_planes, frustum_points)
debug_draw_point_array(frustum_points)
debug_draw_plane_array(frustum_planes, [frustum_points[0], frustum_points[2], frustum_points[4], frustum_points[6], frustum_points[4], frustum_points[7]])
return frustum_planes
func proj_filter_obstructed_placeforms(placeforms_data_in_frustum: Array, container_transform:Transform3D):
# This is a margin to offset our cast due to miniscule errors in placement
# And built-in collision shape margin
# Not off-setting an endpoint will mark some visible instances as obstructed
var raycast_margin = FunLib.get_setting_safe("dreadpons_spatial_gardener/painting/projection_raycast_margin", 0.1)
for i in range(placeforms_data_in_frustum.size() -1, -1, -1):
var placeform_data = placeforms_data_in_frustum[i]
var ray_start: Vector3 = camera.global_transform.origin
var ray_vector = container_transform * placeform_data.placeform[0] - ray_start
var ray_end: Vector3 = ray_start + ray_vector.normalized() * (ray_vector.length() - raycast_margin)
var params = PhysicsRayQueryParameters3D.create(ray_start, ray_end, collision_mask)
var ray_result = space_state.intersect_ray(params)
if !ray_result.is_empty():# && ray_result.collider.collision_layer & collision_mask:
placeforms_data_in_frustum.remove_at(i)
# Called when the Painter brush stroke is updated (moved)
# To be overridden
func proj_get_stroke_update_changes(placeforms_in_brush: Array, plant:Greenhouse_Plant, plant_index: int, octree_manager:MMIOctreeManager, painting_changes:PaintingChanges):
return null
#-------------------------------------------------------------------------------
# Projection, intersection and other Projection-brush geometry
#-------------------------------------------------------------------------------
# Recursively iterate over octree nodes to find nodes and members within brush frustum
func proj_get_placeforms_data_in_frustum(frustum_planes: Array, placeforms_data_in_frustum: Array, octree_node: MMIOctreeNode, container_transform:Transform3D):
var octree_node_transform := Transform3D(container_transform.basis, container_transform * octree_node.center_pos)
var octree_node_extents := Vector3(octree_node.extent, octree_node.extent, octree_node.extent)
debug_draw_cube(octree_node_transform.origin, octree_node_extents * 2.0, octree_node_transform.basis.get_rotation_quaternion(), octree_node_transform.basis)
if is_box_intersecting_frustum(frustum_planes, octree_node_transform, octree_node_extents):
if octree_node.is_leaf:
var node_address = octree_node.get_address()
for member_idx in range(0, octree_node.member_count()):
var placeform = octree_node.get_placeform(member_idx)
placeforms_data_in_frustum.append({"node_address": node_address, "member_idx": member_idx, "placeform": placeform})
else:
for child_node in octree_node.child_nodes:
proj_get_placeforms_data_in_frustum(frustum_planes, placeforms_data_in_frustum, child_node, container_transform)
# This is an approximation (but THIS frustum check IS MEANT to be an approximation, so it's fine)
# This WILL fail on cube's screen-projected 'corners'
# Since technically it will intersect some planes of our frustum
# Which is fine because we perform distance checks to individual members later on
func is_box_intersecting_frustum(frustum_planes: Array, box_transform: Transform3D, box_extents: Vector3):
# Extents == half-size
var oriented_box_extents = [box_extents.x * box_transform.basis.x, box_extents.y * box_transform.basis.y, box_extents.z * box_transform.basis.z]
var box_points := [
box_transform.origin + oriented_box_extents[0] + oriented_box_extents[1] + oriented_box_extents[2],
box_transform.origin + oriented_box_extents[0] + oriented_box_extents[1] - oriented_box_extents[2],
box_transform.origin + oriented_box_extents[0] - oriented_box_extents[1] + oriented_box_extents[2],
box_transform.origin + oriented_box_extents[0] - oriented_box_extents[1] - oriented_box_extents[2],
box_transform.origin - oriented_box_extents[0] + oriented_box_extents[1] + oriented_box_extents[2],
box_transform.origin - oriented_box_extents[0] - oriented_box_extents[1] + oriented_box_extents[2],
box_transform.origin - oriented_box_extents[0] + oriented_box_extents[1] - oriented_box_extents[2],
box_transform.origin - oriented_box_extents[0] - oriented_box_extents[1] - oriented_box_extents[2],
]
debug_draw_point_array(box_points, Color.YELLOW)
for plane in frustum_planes:
var points_inside := 0
for point in box_points:
points_inside += 1 if plane.is_point_over(point) else 0
if points_inside == 0:
return false
return true
# Project frustum points from screen-space to world-space
func project_frustum_points(frustum_points: Array, brush_rect: Rect2):
# A simple version for frustum projection which assumes four corners of the screen in world-space
# To be roughly at camera's origin (camera.near is the default 0.05 and thus negligible)
if simplify_projection_frustum:
var origin_point = project_point(0.0)
# Upper left, near + far
frustum_points[0] = origin_point
frustum_points[1] = project_point(camera.far - 0.1, Vector2(brush_rect.position.x, brush_rect.position.y))
# Lower left, near + far
frustum_points[2] = origin_point
frustum_points[3] = project_point(camera.far - 0.1, Vector2(brush_rect.position.x, brush_rect.end.y))
# Lower right, near + far
frustum_points[4] = origin_point
frustum_points[5] = project_point(camera.far - 0.1, Vector2(brush_rect.end.x, brush_rect.end.y))
# Upper right, near + far
frustum_points[6] = origin_point
frustum_points[7] = project_point(camera.far - 0.1, Vector2(brush_rect.end.x, brush_rect.position.y))
# A complex version for frustum projection which uses camera.near for bigger accuracy
# Relevant when camera.near is greater than default of 0.05 (like 1.0 or 2.0)
else:
# Upper left, near + far
project_frustum_point_pair(frustum_points, 0, 1, Vector2(brush_rect.position.x, brush_rect.position.y))
# Lower left, near + far
project_frustum_point_pair(frustum_points, 2, 3, Vector2(brush_rect.position.x, brush_rect.end.y))
# Lower right, near + far
project_frustum_point_pair(frustum_points, 4, 5, Vector2(brush_rect.end.x, brush_rect.end.y))
# Upper right, near + far
project_frustum_point_pair(frustum_points, 6, 7, Vector2(brush_rect.end.x, brush_rect.position.y))
func project_frustum_point_pair(frustum_points: Array, idx_0: int, idx_1: int, offset: Vector2):
frustum_points[idx_0] = project_point(camera.near, offset)
frustum_points[idx_1] = project_point(camera.far - 0.1, offset)
# Define an array of 6 frustum planes
func define_frustum_plane_array(frustum_planes: Array, frustum_points: Array):
frustum_planes[0] = define_frustum_plane(frustum_points[0], frustum_points[3], frustum_points[1])
frustum_planes[1] = define_frustum_plane(frustum_points[2], frustum_points[5], frustum_points[3])
frustum_planes[2] = define_frustum_plane(frustum_points[4], frustum_points[7], frustum_points[5])
frustum_planes[3] = define_frustum_plane(frustum_points[6], frustum_points[1], frustum_points[7])
if simplify_projection_frustum:
# Since all points involved here will be the same point due to simplification (see 'project_frustum_points()')
# Approximate the points forming a plane by using camera's basis vectors
frustum_planes[4] = define_frustum_plane(frustum_points[4], frustum_points[2] - camera.global_transform.basis.x, frustum_points[0] + camera.global_transform.basis.y)
else:
# Here all our points are different, so use them as-is
frustum_planes[4] = define_frustum_plane(frustum_points[4], frustum_points[2], frustum_points[0])
frustum_planes[5] = define_frustum_plane(frustum_points[7], frustum_points[1], frustum_points[3])
# Plane equation is
# ax + by + xz + d = 0
# Or this with a common point on a plane
# a(x - x0) + b(y - y0) + c(z - z0) = 0
# Where (x0, y0, z0) is any point on the plane (lets use common_point)
#
# So plane equation becomes
# normal.x * ux - normal.x * common_point.x +
# normal.y * uy - normal.y * common_point.y +
# normal.z * uz - normal.z * common_point.z
# = 0
# Where ux, uy, uz are unkown variables that are substituted when solving a plane equations
#
# Now we combine our scalar values and move them to the right side
# normal.x * ux + normal.y * uy + normal.z * uz
# = normal.x * common_point.x + normal.y * common_point.y + normal.z * common_point.z
#
# That should be it, distance to origin is
# d = normal.x * common_point.x + normal.y * common_point.y + normal.z * common_point.z
# Which is esentially a dot product :)
func define_frustum_plane(common_point: Vector3, point_0: Vector3, point_1: Vector3):
var normal := (point_0 - common_point).cross(point_1 - common_point).normalized()
var dist_to_origin = normal.dot(common_point)
var plane = Plane(normal, dist_to_origin)
return plane
func project_point(distance: float, offset: Vector2 = Vector2.ZERO) -> Vector3:
return camera.project_position(_cached_mouse_pos + offset, distance)
#-------------------------------------------------------------------------------
# Debug
#-------------------------------------------------------------------------------
func debug_print_lifecycle(string:String):
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/arborist_log_lifecycle", false): return
logger.info(string)
func debug_mk_debug_draw():
var context = camera.get_tree().edited_scene_root#.find_child('Gardener').get_parent()
if !context.has_node('DponDebugDraw'):
var debug_draw := DponDebugDraw.new()
debug_draw.name = 'DponDebugDraw'
context.add_child(debug_draw)
func debug_draw_plane_array(planes: Array, origin_points: Array, color: Color = Color.RED):
if !debug_draw_enabled: return
for idx in range(0, planes.size()):
debug_draw_plane(origin_points[idx], planes[idx], color)
func debug_draw_point_array(points: Array, color: Color = Color.GREEN):
if !debug_draw_enabled: return
for point in points:
debug_draw_point(point, color)
func debug_draw_plane(draw_origin: Vector3, plane: Plane, color: Color = Color.RED):
if !debug_draw_enabled: return
var context = camera.get_tree().edited_scene_root.find_child('Gardener').get_parent()
context.get_node('DponDebugDraw').draw_plane(draw_origin, camera.far * 0.5, plane.normal, color, context, 2.0, camera.global_transform.basis.y, 10.0)
func debug_draw_point(draw_origin: Vector3, color: Color = Color.GREEN):
if !debug_draw_enabled: return
var context = camera.get_tree().edited_scene_root.find_child('Gardener').get_parent()
context.get_node('DponDebugDraw').draw_cube(draw_origin, Vector3.ONE, Quaternion(), color, context, 10.0)
func debug_draw_cube(draw_origin: Vector3, size: Vector3, rotation: Quaternion, basis: Basis = Basis(), color: Color = Color.BLUE):
if !debug_draw_enabled: return
var context = camera.get_tree().edited_scene_root.find_child('Gardener').get_parent()
size = Vector3(size.x * basis.x.length(), size.y * basis.y.length(), size.z * basis.z.length())
context.get_node('DponDebugDraw').draw_cube(draw_origin, size, rotation, color, context, 10.0)

View File

@@ -0,0 +1,116 @@
@tool
#-------------------------------------------------------------------------------
# A function library for generating individual plant tranforms
# Based on the given placement position and Greenhouse_Plant settings
#-------------------------------------------------------------------------------
const FunLib = preload("../utility/fun_lib.gd")
const Greenhouse_Plant = preload("../greenhouse/greenhouse_plant.gd")
# Randomize an instance transform according to its Greenhouse_Plant settings
static func generate_plant_transform(placement, normal, plant, randomizer) -> Transform3D:
var up_vector_primary:Vector3 = get_dir_vector(plant.up_vector_primary_type, plant.up_vector_primary, normal)
var up_vector_secondary:Vector3 = get_dir_vector(plant.up_vector_secondary_type, plant.up_vector_secondary, normal)
var plant_up_vector:Vector3 = lerp(up_vector_primary, up_vector_secondary, plant.up_vector_blending).normalized()
var fwd_vector_primary:Vector3 = get_dir_vector(plant.fwd_vector_primary_type, plant.fwd_vector_primary, normal)
var fwd_vector_secondary:Vector3 = get_dir_vector(plant.fwd_vector_secondary_type, plant.fwd_vector_secondary, normal)
var plant_fwd_vector:Vector3 = lerp(fwd_vector_primary, fwd_vector_secondary, plant.fwd_vector_blending).normalized()
var plant_scale:Vector3 = FunLib.vector_tri_lerp(
plant.scale_range[0],
plant.scale_range[1],
get_scaling_randomized_weight(plant.scale_scaling_type, randomizer)
)
var plant_y_offset = lerp(plant.offset_y_range[0], plant.offset_y_range[1], randomizer.randf_range(0.0, 1.0)) * plant_scale
var plant_rotation = Vector3(
deg_to_rad(lerp(-plant.rotation_random_x, plant.rotation_random_x, randomizer.randf_range(0.0, 1.0))),
deg_to_rad(lerp(-plant.rotation_random_y, plant.rotation_random_y, randomizer.randf_range(0.0, 1.0))),
deg_to_rad(lerp(-plant.rotation_random_z, plant.rotation_random_z, randomizer.randf_range(0.0, 1.0)))
)
var plant_basis:Basis = Basis()
plant_basis.y = plant_up_vector
# If one of the forward vectors is unused and contributes to a blend
if ((plant.fwd_vector_primary_type == Greenhouse_Plant.DirectionVectorType.UNUSED && plant.fwd_vector_blending != 1.0)
|| (plant.fwd_vector_secondary_type == Greenhouse_Plant.DirectionVectorType.UNUSED && plant.fwd_vector_blending != 0.0)):
# Use automatic forward vector
plant_basis.z = Vector3.FORWARD.rotated(plant_up_vector, plant_rotation.y)
else:
plant_basis.z = plant_fwd_vector.rotated(plant_up_vector, plant_rotation.y)
plant_basis.x = plant_basis.y.cross(plant_basis.z)
plant_basis.z = plant_basis.x.cross(plant_basis.y)
plant_basis = plant_basis.orthonormalized()
plant_basis = plant_basis.rotated(plant_basis.x, plant_rotation.x)
plant_basis = plant_basis.rotated(plant_basis.z, plant_rotation.z)
plant_basis.x *= plant_scale.x
plant_basis.y *= plant_scale.y
plant_basis.z *= plant_scale.z
var plant_origin = placement + plant_y_offset * plant_basis.y.normalized()
var plant_transform = Transform3D(plant_basis, plant_origin)
return plant_transform
# See slope_allowedRange in Greenhouse_Plant
static func is_plant_slope_allowed(normal, plant) -> bool:
var up_vector_primary:Vector3 = get_dir_vector(plant.up_vector_primary_type, plant.up_vector_primary, normal)
var slope_angle = abs(rad_to_deg(up_vector_primary.angle_to(normal)))
return slope_angle >= plant.slope_allowed_range[0] && slope_angle <= plant.slope_allowed_range[1]
# Choose the appropriate direction vector
static func get_dir_vector(dir_vector_type, custom_vector:Vector3, normal:Vector3) -> Vector3:
match dir_vector_type:
Greenhouse_Plant.DirectionVectorType.WORLD_X:
return Vector3.RIGHT
Greenhouse_Plant.DirectionVectorType.WORLD_Y:
return Vector3.UP
Greenhouse_Plant.DirectionVectorType.WORLD_Z:
return Vector3.FORWARD
Greenhouse_Plant.DirectionVectorType.NORMAL:
return normal
Greenhouse_Plant.DirectionVectorType.CUSTOM:
return custom_vector.normalized()
return Vector3.UP
# Enforce the scaling plane lock if present
# The scaling itself is already enforced by Greenhouse_Plant
# But we need to enforce the randomization as well
static func get_scaling_randomized_weight(scaling_type, randomizer) -> Vector3:
var scale_weight = Vector3()
match scaling_type:
Greenhouse_Plant.ScalingType.UNIFORM:
scale_weight.x = randomizer.randf_range(0.0, 1.0)
scale_weight.y = scale_weight.x
scale_weight.z = scale_weight.x
Greenhouse_Plant.ScalingType.FREE:
scale_weight.x = randomizer.randf_range(0.0, 1.0)
scale_weight.y = randomizer.randf_range(0.0, 1.0)
scale_weight.z = randomizer.randf_range(0.0, 1.0)
Greenhouse_Plant.ScalingType.LOCK_XY:
scale_weight.x = randomizer.randf_range(0.0, 1.0)
scale_weight.y = scale_weight.x
scale_weight.z = randomizer.randf_range(0.0, 1.0)
Greenhouse_Plant.ScalingType.LOCK_ZY:
scale_weight.x = randomizer.randf_range(0.0, 1.0)
scale_weight.y = randomizer.randf_range(0.0, 1.0)
scale_weight.z = scale_weight.y
Greenhouse_Plant.ScalingType.LOCK_XZ:
scale_weight.x = randomizer.randf_range(0.0, 1.0)
scale_weight.y = randomizer.randf_range(0.0, 1.0)
scale_weight.z = scale_weight.x
return scale_weight

View File

@@ -0,0 +1,13 @@
@tool
extends LineEdit
# Release focus from a child node when pressing enter
func _gui_input(event):
if has_focus():
if is_instance_of(event, InputEventKey) && !event.pressed:
if event.keycode == KEY_ENTER || event.keycode == KEY_ESCAPE:
release_focus()
if is_instance_of(self, LineEdit):
caret_column = 0

View File

@@ -0,0 +1,31 @@
@tool
extends Button
#-------------------------------------------------------------------------------
# A button that accepts drop events
# Kind of surprised I need to attach a separate script for that functionality :/
#-------------------------------------------------------------------------------
signal dropped
func _init():
set_meta("class", "UI_DropButton")
#-------------------------------------------------------------------------------
# Drag'n'drop handling
#-------------------------------------------------------------------------------
func _can_drop_data(position, data):
if typeof(data) == TYPE_DICTIONARY && data.has("files") && data["files"].size() == 1:
return true
func _drop_data(position, data):
dropped.emit(data["files"][0])

View File

@@ -0,0 +1,60 @@
@tool
extends GridContainer
#-------------------------------------------------------------------------------
# A grid that automatically changes number of columns based on it's max width
# For now works only when inside a ScrollContainer with both size flags set to SIZE_EXPAND_FILL
# lol
#-------------------------------------------------------------------------------
# TODO make this independent from a ScrollContainer or replace with someone else's solution
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init():
set_meta("class", "UI_FlexGridContainer")
func _ready():
resized.connect(on_resized)
get_parent().resized.connect(on_resized)
func _enter_tree():
on_resized()
#-------------------------------------------------------------------------------
# Resize
#-------------------------------------------------------------------------------
func on_resized():
recalc_columns()
func recalc_columns():
var target_size := get_parent_area_size()
var factual_size := size
if columns > 1 && factual_size.x > target_size.x:
columns -= 1
var biggest_child_size := Vector2.ZERO
for child in get_children():
if child.size.x > biggest_child_size.x:
biggest_child_size.x = child.size.x
if child.size.y > biggest_child_size.y:
biggest_child_size.y = child.size.y
if biggest_child_size.x * (columns + 1) + get_theme_constant("h_separation") * columns < target_size.x:
columns += 1

View File

@@ -0,0 +1,276 @@
@tool
extends Control
#-------------------------------------------------------------------------------
# A button with multiple children buttons corresponding to various possible interactions
# It's main purpose is to display a thumbnail and respond to UI inputs
#-------------------------------------------------------------------------------
# These flags define what sort of signals and broadcast
enum InteractionFlags {DELETE, SET_DIALOG, SET_DRAG, PRESS, CHECK, CLEAR, SHOW_COUNT, EDIT_LABEL}
const PRESET_ALL:Array = [ InteractionFlags.DELETE, InteractionFlags.SET_DIALOG, InteractionFlags.SET_DRAG, InteractionFlags.PRESS,
InteractionFlags.CHECK, InteractionFlags.CLEAR, InteractionFlags.SHOW_COUNT, InteractionFlags.EDIT_LABEL]
const ThemeAdapter = preload("../../../controls/theme_adapter.gd")
const FunLib = preload("../../../utility/fun_lib.gd")
var active_interaction_flags:Array = [] : set = set_active_interaction_flags
@export var thumb_size:int = 100 : set = set_thumb_size
var root_button_nd:Control = null
var texture_rect_nd:Control = null
var selection_panel_nd:Control = null
var check_box_nd:Control = null
var counter_label_nd:Control = null
var label_line_container_nd:Control = null
var label_line_edit_nd:Control = null
var menu_button_nd:Control = null
var alt_text_label_nd:Control = null
var default_button_sizes: Dictionary = {}
@export var clear_texture: Texture2D = null
@export var delete_texture: Texture2D = null
@export var new_texture: Texture2D = null
@export var options_texture: Texture2D = null
var def_rect_size:Vector2 = Vector2(100.0, 100.0)
var def_button_size:Vector2 = Vector2(24.0, 24.0)
var def_max_title_chars:int = 8
signal requested_delete
signal requested_set_dialog
signal requested_set_drag
signal requested_press
signal requested_check
signal requested_label_edit
signal requested_clear
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func init(_thumb_size:int, _button_size:int, _active_interaction_flags:Array):
set_meta("class", "UI_ActionThumbnail")
thumb_size = _thumb_size
active_interaction_flags = _active_interaction_flags.duplicate()
# We have some conditional checks here
# Because inheriting nodes might ditch some of the functionality
func _ready():
var Label_font_size = get_theme_font_size("font_size", "Label")
_set_default_textures()
if has_node("%RootButton"):
root_button_nd = %RootButton
if root_button_nd.has_signal("dropped"):
root_button_nd.dropped.connect(on_set_drag)
root_button_nd.pressed.connect(on_set_dialog)
root_button_nd.pressed.connect(on_press)
root_button_nd.theme_type_variation = "InspectorButton"
if has_node("%TextureRect"):
texture_rect_nd = %TextureRect
texture_rect_nd.visible = true
if has_node("%SelectionPanel"):
selection_panel_nd = %SelectionPanel
selection_panel_nd.theme_type_variation = "ActionThumbnail_SelectionPanel"
selection_panel_nd.visible = false
if has_node("%CheckBox"):
check_box_nd = %CheckBox
check_box_nd.pressed.connect(on_check)
check_box_nd.visible = false
default_button_sizes[check_box_nd] = check_box_nd.size
if has_node("%CounterLabel"):
counter_label_nd = %CounterLabel
counter_label_nd.visible = false
if has_node("%AltTextLabel"):
alt_text_label_nd = %AltTextLabel
alt_text_label_nd.visible = false
if has_node('%LabelLineEdit'):
label_line_container_nd = %LabelLineContainer
label_line_edit_nd = %LabelLineEdit
label_line_edit_nd.theme_type_variation = "PlantTitleLineEdit"
label_line_edit_nd.text_changed.connect(on_label_edit)
label_line_container_nd.visible = false
if has_node('%MenuButton'):
menu_button_nd = %MenuButton
menu_button_nd.theme_type_variation = "MenuButton"
menu_button_nd.get_popup().id_pressed.connect(on_popup_menu_press)
menu_button_nd.visible = true
default_button_sizes[menu_button_nd] = menu_button_nd.size
if counter_label_nd:
counter_label_nd.add_theme_font_size_override('font_size', Label_font_size)
if label_line_edit_nd:
label_line_edit_nd.add_theme_font_size_override('font_size', Label_font_size)
if alt_text_label_nd:
alt_text_label_nd.add_theme_font_size_override('font_size', Label_font_size)
update_size()
set_active_interaction_flags(active_interaction_flags)
func _set_default_textures():
if !clear_texture || !delete_texture || !new_texture || !options_texture:
var editor_theme = ThemeAdapter.editor_theme
clear_texture = editor_theme.get_theme_item(Theme.DATA_TYPE_ICON, "Clear", "EditorIcons")
delete_texture = editor_theme.get_theme_item(Theme.DATA_TYPE_ICON, "ImportFail", "EditorIcons")
new_texture = editor_theme.get_theme_item(Theme.DATA_TYPE_ICON, "Add", "EditorIcons")
options_texture = editor_theme.get_theme_item(Theme.DATA_TYPE_ICON, "CodeFoldDownArrow", "EditorIcons")
if has_node("%MenuButton"):
%MenuButton.icon = options_texture
#-------------------------------------------------------------------------------
# Resizing
#-------------------------------------------------------------------------------
func set_thumb_size(val:int):
thumb_size = val
update_size()
func update_size():
if !is_node_ready(): return
var thumb_rect = Vector2(thumb_size, thumb_size)
custom_minimum_size = thumb_rect
size = thumb_rect
func set_counter_val(val:int):
if !is_node_ready():
await ready
if !counter_label_nd: return
counter_label_nd.text = str(val)
#-------------------------------------------------------------------------------
# Interaction flags
#-------------------------------------------------------------------------------
func set_active_interaction_flags(flags:Array):
var ownFlagsCopy = active_interaction_flags.duplicate()
var flagsCopy = flags.duplicate()
for flag in ownFlagsCopy:
set_interaction_flag(flag, false)
for flag in flagsCopy:
set_interaction_flag(flag, true)
func set_interaction_flag(flag:int, state:bool):
if state:
if !active_interaction_flags.has(flag):
active_interaction_flags.append(flag)
else:
active_interaction_flags.erase(flag)
enable_features_to_flag(flag, state)
func enable_features_to_flag(flag:int, state:bool):
if is_node_ready():
match flag:
InteractionFlags.CHECK:
check_box_nd.visible = state
InteractionFlags.CLEAR:
if state:
menu_button_nd.get_popup().remove_item(menu_button_nd.get_popup().get_item_index(0))
menu_button_nd.get_popup().add_icon_item(clear_texture, 'Clear', 0)
InteractionFlags.DELETE:
if state:
menu_button_nd.get_popup().remove_item(menu_button_nd.get_popup().get_item_index(1))
menu_button_nd.get_popup().add_icon_item(delete_texture, 'Delete', 1)
InteractionFlags.SHOW_COUNT:
counter_label_nd.visible = state
InteractionFlags.EDIT_LABEL:
label_line_container_nd.visible = state
func set_features_val_to_flag(flag:int, val):
if is_node_ready():
match flag:
InteractionFlags.PRESS:
selection_panel_nd.visible = val
InteractionFlags.CHECK:
check_box_nd.button_pressed = val
InteractionFlags.EDIT_LABEL:
if label_line_edit_nd.text != val:
label_line_edit_nd.text = val
func on_set_dialog():
if active_interaction_flags.has(InteractionFlags.SET_DIALOG):
requested_set_dialog.emit()
func on_set_drag(path):
if active_interaction_flags.has(InteractionFlags.SET_DRAG):
requested_set_drag.emit(path)
func on_press():
if active_interaction_flags.has(InteractionFlags.PRESS):
requested_press.emit()
func on_check():
if active_interaction_flags.has(InteractionFlags.CHECK):
requested_check.emit(check_box_nd.button_pressed)
func on_label_edit(label_text: String):
if active_interaction_flags.has(InteractionFlags.EDIT_LABEL):
requested_label_edit.emit(label_text)
func on_popup_menu_press(id: int):
match id:
0:
call_deferred("on_clear")
1:
call_deferred("on_delete")
func on_clear():
if active_interaction_flags.has(InteractionFlags.CLEAR):
requested_clear.emit()
func on_delete():
if active_interaction_flags.has(InteractionFlags.DELETE):
requested_delete.emit()
#-------------------------------------------------------------------------------
# Thumbnail itself and other visuals
#-------------------------------------------------------------------------------
func set_thumbnail(texture:Texture2D):
texture_rect_nd.visible = true
alt_text_label_nd.visible = false
texture_rect_nd.texture = texture
alt_text_label_nd.text = ""
func set_alt_text(alt_text:String):
if !is_instance_valid(alt_text_label_nd) || !is_instance_valid(texture_rect_nd): return
alt_text_label_nd.visible = true
texture_rect_nd.visible = false
alt_text_label_nd.text = alt_text
texture_rect_nd.texture = null

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,35 @@
@tool
extends "ui_action_thumbnail.gd"
#-------------------------------------------------------------------------------
# An action thumbnail version that is pressed to create new action thumbnails
#-------------------------------------------------------------------------------
func _init():
set_meta("class", "UI_ActionThumbnailCreateInst")
#-------------------------------------------------------------------------------
# Resizing
#-------------------------------------------------------------------------------
func _set_default_textures():
super._set_default_textures()
%TextureRect.texture = new_texture
#-------------------------------------------------------------------------------
# Interaction flags
#-------------------------------------------------------------------------------
# Overrides parent function, since it has no features to set
func enable_features_to_flag(flag:int, state:bool):
return

View File

@@ -0,0 +1,94 @@
[gd_scene load_steps=11 format=3 uid="uid://v8fq3xw2l3ba"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/controls/extensions/ui_drop_button.gd" id="1"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/controls/input_fields/action_thumbnail/ui_action_thumbnail_create_inst.gd" id="2"]
[sub_resource type="Image" id="Image_pqe3h"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 184, 224, 224, 224, 181, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 75, 224, 224, 224, 228, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 227, 224, 224, 224, 73, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 228, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 226, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 243, 224, 224, 224, 217, 225, 225, 225, 191, 225, 225, 225, 166, 224, 224, 224, 140, 224, 224, 224, 114, 224, 224, 224, 89, 227, 227, 227, 63, 229, 229, 229, 38, 234, 234, 234, 12, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_r3fkh"]
image = SubResource("Image_pqe3h")
[sub_resource type="Image" id="Image_2t4vb"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 120, 107, 196, 255, 120, 107, 198, 255, 128, 116, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 117, 24, 255, 120, 107, 202, 255, 120, 107, 193, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 203, 255, 120, 107, 255, 255, 120, 108, 213, 255, 128, 116, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 117, 24, 255, 120, 107, 215, 255, 120, 107, 255, 255, 120, 108, 199, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 120, 108, 213, 255, 120, 107, 255, 255, 120, 108, 213, 255, 128, 116, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 122, 111, 23, 255, 120, 107, 214, 255, 120, 107, 255, 255, 120, 107, 210, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 120, 108, 213, 255, 120, 107, 255, 255, 120, 108, 213, 255, 128, 116, 22, 255, 122, 111, 23, 255, 120, 107, 214, 255, 120, 107, 255, 255, 120, 107, 210, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 120, 108, 213, 255, 120, 107, 255, 255, 120, 108, 213, 255, 120, 108, 213, 255, 120, 107, 255, 255, 120, 107, 210, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 120, 108, 213, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 210, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 122, 111, 23, 255, 120, 108, 213, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 211, 255, 121, 109, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 122, 111, 23, 255, 120, 107, 214, 255, 120, 107, 255, 255, 120, 107, 210, 255, 120, 107, 211, 255, 120, 107, 255, 255, 120, 107, 211, 255, 121, 109, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 117, 24, 255, 120, 107, 214, 255, 120, 107, 255, 255, 120, 107, 210, 255, 128, 115, 20, 255, 121, 109, 21, 255, 120, 107, 211, 255, 120, 107, 255, 255, 120, 107, 212, 255, 128, 116, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 117, 24, 255, 120, 107, 215, 255, 120, 107, 255, 255, 120, 107, 210, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 109, 21, 255, 120, 107, 212, 255, 120, 107, 255, 255, 120, 107, 212, 255, 128, 116, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 207, 255, 120, 107, 255, 255, 120, 107, 210, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 120, 107, 212, 255, 120, 107, 255, 255, 120, 107, 202, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 120, 107, 197, 255, 120, 107, 200, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 120, 107, 202, 255, 120, 107, 193, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_dmple"]
image = SubResource("Image_2t4vb")
[sub_resource type="Image" id="Image_f3rjy"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_utaay"]
image = SubResource("Image_f3rjy")
[sub_resource type="Image" id="Image_7h1t5"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 180, 255, 255, 255, 195, 255, 255, 255, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 21, 255, 255, 255, 195, 255, 255, 255, 178, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 195, 255, 255, 255, 255, 255, 255, 255, 212, 255, 255, 255, 22, 255, 255, 255, 22, 255, 255, 255, 212, 255, 255, 255, 255, 255, 255, 255, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 21, 255, 255, 255, 210, 255, 255, 255, 255, 255, 255, 255, 212, 255, 255, 255, 212, 255, 255, 255, 255, 255, 255, 255, 210, 255, 255, 255, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 21, 255, 255, 255, 210, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 210, 255, 255, 255, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 21, 255, 255, 255, 194, 255, 255, 255, 194, 255, 255, 255, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_sj7sv"]
image = SubResource("Image_7h1t5")
[node name="ActionThumbnail" type="Control"]
custom_minimum_size = Vector2(100, 100)
layout_mode = 3
anchors_preset = 0
offset_right = 100.0
offset_bottom = 100.0
mouse_filter = 1
script = ExtResource("2")
clear_texture = SubResource("ImageTexture_r3fkh")
delete_texture = SubResource("ImageTexture_dmple")
new_texture = SubResource("ImageTexture_utaay")
options_texture = SubResource("ImageTexture_sj7sv")
metadata/class = "UI_ActionThumbnailCreateInst"
[node name="RootButton" type="Button" parent="."]
unique_name_in_owner = true
texture_filter = 1
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_type_variation = &"InspectorButton"
icon_alignment = 1
script = ExtResource("1")
metadata/class = "UI_DropButton"
[node name="TextureRect" type="TextureRect" parent="."]
unique_name_in_owner = true
texture_filter = 1
layout_mode = 1
anchors_preset = -1
anchor_left = 0.25
anchor_top = 0.25
anchor_right = 0.75
anchor_bottom = 0.75
mouse_filter = 2
texture = SubResource("ImageTexture_utaay")
expand_mode = 1
stretch_mode = 5

View File

@@ -0,0 +1,23 @@
@tool
extends ConfirmationDialog
#-------------------------------------------------------------------------------
# A dialog that displays InputField controls
# Has confirmation and cancellation buttons
#-------------------------------------------------------------------------------
@onready var panel_container_fields_nd: Control = $VBoxContainer_Main/PanelContainer_Fields
@onready var fields = $VBoxContainer_Main/PanelContainer_Fields/VBoxContainer_Fields
func _init():
set_meta("class", "UI_Dialog_IF")
ok_button_text = "Apply"
cancel_button_text = "Cancel"
close_requested.connect(hide)

View File

@@ -0,0 +1,24 @@
[gd_scene load_steps=2 format=3 uid="uid://b1r8m47n3hvh4"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/controls/input_fields/dialog_if/ui_dialog_if.gd" id="1"]
[node name="UI_Dialog_IF" type="ConfirmationDialog"]
ok_button_text = "Apply"
script = ExtResource("1")
metadata/class = "UI_Dialog_IF"
[node name="VBoxContainer_Main" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
[node name="PanelContainer_Fields" type="PanelContainer" parent="VBoxContainer_Main"]
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer_Fields" type="VBoxContainer" parent="VBoxContainer_Main/PanelContainer_Fields"]
layout_mode = 2

View File

@@ -0,0 +1,154 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# Shows a dialog with InputField controls when button is pressed
# InputField controls will be set with PA_PropSet if dialog was confirmed
# InputField controls will be reverted to initial values if dialog was canceled
#-------------------------------------------------------------------------------
const UI_Dialog_IF = preload("dialog_if/ui_dialog_if.tscn")
var button:Button = null
var _base_control:Control = null
var apply_dialog:Window = null
var bound_input_fields:Array = []
var initial_values:Array = []
var final_values:Array = []
signal applied_changes(initial_values, final_values)
signal canceled_changes
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_ApplyChanges")
button = Button.new()
button.name = "button"
button.size_flags_horizontal = SIZE_EXPAND_FILL
button.size_flags_vertical = SIZE_SHRINK_CENTER
button.text = settings.button_text
button.pressed.connect(on_pressed)
apply_dialog = UI_Dialog_IF.instantiate()
apply_dialog.title = settings.button_text
apply_dialog.confirmed.connect(on_dialog_confirmed)
apply_dialog.canceled.connect(on_dialog_canceled)
apply_dialog.close_requested.connect(on_dialog_hidden)
bound_input_fields = settings.bound_input_fields
button.theme_type_variation = "InspectorButton"
container_box.add_child(button)
func prepare_input_field(__init_val, __base_control:Control, __resource_previewer):
super(__init_val, __base_control, __resource_previewer)
_base_control = __base_control
func _ready():
super()
for input_field in bound_input_fields:
input_field.disable_history = true
apply_dialog.fields.add_child(input_field)
func _enter_tree():
if _base_control:
_base_control.add_child(apply_dialog)
super()
func _exit_tree():
if _base_control && _base_control.get_children().has(apply_dialog):
_base_control.remove_child(apply_dialog)
apply_dialog.queue_free()
func _cleanup():
super()
if is_instance_valid(button):
button.queue_free()
if is_instance_valid(apply_dialog):
apply_dialog.queue_free()
for bound_input_field in bound_input_fields:
if is_instance_valid(bound_input_field):
bound_input_field.queue_free()
func reset_dialog():
initial_values = []
final_values = []
if apply_dialog.visible:
apply_dialog.visible = false
#-------------------------------------------------------------------------------
# Button presses
#-------------------------------------------------------------------------------
func on_pressed():
initial_values = gather_values()
apply_dialog.popup_centered(Vector2(400, 200))# popup_centered_ratio(0.5)
func on_dialog_confirmed():
final_values = gather_values()
applied_changes.emit(initial_values.duplicate(), final_values.duplicate())
reset_dialog()
func on_dialog_canceled():
set_values(initial_values)
canceled_changes.emit()
reset_dialog()
func on_dialog_hidden():
on_dialog_canceled()
#-------------------------------------------------------------------------------
# Value management
#-------------------------------------------------------------------------------
func gather_values() -> Array:
var values := []
for input_field in bound_input_fields:
values.append(input_field.val_cache)
return values
func set_values(values):
for i in range(0, values.size()):
var input_field = bound_input_fields[i]
var val = values[i]
if val is Array || val is Dictionary:
val = val.duplicate()
var prop_action:PropAction = PA_PropSet.new(input_field.prop_name, val)
prop_action.can_create_history = false
debug_print_prop_action("Requesting prop action: %s from \"%s\"" % [str(prop_action), name])
prop_action_requested.emit(prop_action)

View File

@@ -0,0 +1,55 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# Stores a bool value
#-------------------------------------------------------------------------------
var bool_check:CheckBox = null
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_Bool")
bool_check = CheckBox.new()
bool_check.name = "bool_check"
bool_check.text = "On"
bool_check.size_flags_horizontal = SIZE_EXPAND_FILL
bool_check.size_flags_vertical = SIZE_SHRINK_CENTER
bool_check.toggled.connect(_request_prop_action.bind("PA_PropSet"))
bool_check.theme_type_variation = "InspectorCheckBox"
container_box.add_child(bool_check)
func _cleanup():
super()
if is_instance_valid(bool_check):
bool_check.queue_free()
#-------------------------------------------------------------------------------
# Updaing the UI
#-------------------------------------------------------------------------------
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
_update_ui_to_val(final_val)
func _update_ui_to_val(val):
bool_check.button_pressed = val
super._update_ui_to_val(val)

View File

@@ -0,0 +1,54 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# Emits a signal when button is pressed
#-------------------------------------------------------------------------------
var button:Button = null
signal pressed
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_Button")
button = Button.new()
button.name = "button"
button.size_flags_horizontal = SIZE_EXPAND_FILL
button.size_flags_vertical = SIZE_SHRINK_CENTER
button.text = settings.button_text
button.pressed.connect(on_pressed)
button.theme_type_variation = "InspectorButton"
container_box.add_child(button)
func _cleanup():
super()
if is_instance_valid(button):
button.queue_free()
#-------------------------------------------------------------------------------
# Button presses
#-------------------------------------------------------------------------------
func on_pressed():
pressed.emit()

View File

@@ -0,0 +1,58 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# Stores an enum value
# Uses an OptionButton as a selection dropdown
#-------------------------------------------------------------------------------
var enum_selector:OptionButton = null
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_Enum")
enum_selector = OptionButton.new()
enum_selector.name = "enum_selector"
enum_selector.size_flags_horizontal = SIZE_EXPAND_FILL
enum_selector.size_flags_vertical = SIZE_SHRINK_CENTER
for i in range(0, settings.enum_list.size()):
enum_selector.add_item(settings.enum_list[i], i)
enum_selector.item_selected.connect(_request_prop_action.bind("PA_PropSet"))
enum_selector.theme_type_variation = "InspectorOptionButton"
container_box.add_child(enum_selector)
func _cleanup():
super()
if is_instance_valid(enum_selector):
enum_selector.queue_free()
#-------------------------------------------------------------------------------
# Updaing the UI
#-------------------------------------------------------------------------------
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
_update_ui_to_val(final_val)
func _update_ui_to_val(val):
enum_selector.selected = val
super._update_ui_to_val(val)

View File

@@ -0,0 +1,98 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# Stores an int value
# Has a slider + line_edit for convinience
#-------------------------------------------------------------------------------
var value_input:LineEdit = null
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_IntLineEdit")
value_input = LineEdit.new()
value_input.name = "value_input"
value_input.size_flags_horizontal = SIZE_EXPAND_FILL
value_input.custom_minimum_size.x = 25.0
value_input.size_flags_vertical = SIZE_SHRINK_CENTER
value_input.focus_entered.connect(select_line_edit.bind(value_input, true))
value_input.focus_exited.connect(select_line_edit.bind(value_input, false))
# focus_exited is our main signal to commit the value in LineEdit
# release_focus() is expected to be called when pressing enter and only then we commit the value
value_input.focus_exited.connect(focus_lost.bind(value_input))
value_input.gui_input.connect(on_node_received_input.bind(value_input))
value_input.theme_type_variation = "IF_LineEdit"
container_box.add_child(value_input)
func _cleanup():
super()
if is_instance_valid(value_input):
value_input.queue_free()
#-------------------------------------------------------------------------------
# Property management
#-------------------------------------------------------------------------------
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
_update_ui_to_val(final_val)
func _update_ui_to_val(val):
val = _string_to_val(val)
value_input.text = str(val)
super._update_ui_to_val(val)
func _string_to_val(string) -> int:
if string is String:
if string.is_valid_int():
return string.to_int()
else:
logger.warn("String cannot be converted to int!")
elif string is int:
return string
else:
logger.warn("Passed variable is not a string!")
return 0
func _convert_and_request(val, prop_action_class:String):
_request_prop_action(_string_to_val(val), prop_action_class)
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
func focus_lost(line_edit:LineEdit):
_convert_and_request(line_edit.text, "PA_PropSet")
# Auto select all text when user clicks inside
func select_line_edit(line_edit:LineEdit, state:bool):
if state:
line_edit.call_deferred("select_all")
else:
line_edit.call_deferred("deselect")

View File

@@ -0,0 +1,273 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# Stores a struct with multiple real (float) values
# Possibly represents a min-max range
#-------------------------------------------------------------------------------
# Describes what data input_field receives and returns
# Does not affect how this data is stored internally (always in an array)
enum RepresentationType {
VECTOR, # will input/output_array float/float[2], Vector2/Vector2[2], Vector3/Vector3[2], float[4]/float[4][2]
VALUE, # will input/output_array float/float[2], float[2]/float[2][2], float[3]/float[3][2], float[4]/float[4][2]
COLOR # will input/output_array float/float[2], float[2]/float[2][2], Color/Color[2], Color/Color[2]
# COLOR CURRENTLY DOESN'T DO ANYTHING
# AND SHOULD NOT BE USED
# TODO add color support if it needed
}
const prop_label_text_colors:Array = ["97695c", "568268", "6b76b0", "a3a3a3"]
const prop_label_text:Dictionary = {
RepresentationType.VECTOR: ["x", "y", "z", "w"],
RepresentationType.VALUE: ["a", "b", "c", "d"],
RepresentationType.COLOR: ["r", "g", "b", "a"]
}
var representation_type:int = RepresentationType.VECTOR
var value_count:int = 3
var is_range:bool = false
var field_container:GridContainer = null
var field_editable_controls:Array = []
# Internal (actual) value format:
# [range_index][value_index]
# i.e: range of Vector3:
# [[x1, y1, z1], [x2, y2, z2]]
# Rule of thumb:
# FIRST comes range_index
# SECOND comes value_index
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_RealSlider")
is_range = settings.is_range
value_count = settings.value_count
representation_type = settings.representation_type
field_container = GridContainer.new()
field_container.name = "field_container"
field_container.add_theme_constant_override("h_separation", 0)
field_container.add_theme_constant_override("v_separation", 2)
field_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
field_container.columns = 4 if is_range else 2
for range_index in range(0, 2 if is_range else 1):
field_editable_controls.append([])
for value_index in range(0, value_count):
var prop_label := Label.new()
prop_label.name = "prop_label_-_%s" % [str(value_index)]
prop_label.text = prop_label_text[representation_type][value_index]
prop_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
prop_label.size_flags_vertical = Control.SIZE_FILL
prop_label.custom_minimum_size = Vector2i(20, 0)
field_container.add_child(prop_label)
prop_label.theme_type_variation = "MultiRangePropLabel"
prop_label.add_theme_color_override("font_color", Color(prop_label_text_colors[value_index]))
for range_index in range(0, 2 if is_range else 1):
var value_input = LineEdit.new()
value_input.name = "value_input_%s_%s" % [str(range_index), str(value_index)]
value_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
value_input.size_flags_vertical = Control.SIZE_FILL
value_input.focus_entered.connect(select_line_edit.bind(value_input, true))
value_input.focus_exited.connect(select_line_edit.bind(value_input, false))
value_input.focus_exited.connect(focus_lost.bind(value_input, range_index, value_index))
value_input.gui_input.connect(on_node_received_input.bind(value_input))
field_editable_controls[range_index].append(value_input)
field_container.add_child(value_input)
value_input.theme_type_variation = "MultiRangeValue"
if is_range && range_index == 0:
var dash_label := Label.new()
dash_label.name = "dash_label_-_%s" % [str(value_index)]
dash_label.text = ""
dash_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
dash_label.size_flags_vertical = Control.SIZE_FILL
dash_label.custom_minimum_size = Vector2i(20, 0)
field_container.add_child(dash_label)
dash_label.theme_type_variation = "MultiRangeDashLabel"
dash_label.add_theme_color_override("font_color", Color(prop_label_text_colors[value_index]))
container_box.add_child(field_container)
func _cleanup():
super()
if is_instance_valid(field_container):
field_container.queue_free()
for node in field_editable_controls:
if is_instance_valid(node):
node.queue_free()
#-------------------------------------------------------------------------------
# Property management
#-------------------------------------------------------------------------------
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
_update_ui_to_val(final_val)
func _update_ui_to_val(val):
val = _represented_to_actual(val)
for range_index in range(0, val.size()):
for value_index in range(0, val[range_index].size()):
var value_val = val[range_index][value_index]
field_editable_controls[range_index][value_index].text = str(float(str("%.3f" % value_val)))
super._update_ui_to_val(val.duplicate())
func _string_to_val(string) -> float:
if string is String:
if string.is_valid_float():
return string.to_float()
else:
logger.warn("String cannot be converted to float!")
elif string is float:
return string
else:
logger.warn("Passed variable is not a string!")
return 0.0
func _gather_and_request_prop_action(value_val, range_index, value_index, prop_action_class):
value_val = _string_to_val(value_val)
var val = _gather_val()
val[range_index][value_index] = value_val
val = _actual_to_represented(val)
_request_prop_action(val, prop_action_class)
func _gather_val() -> Array:
var val := []
for range_index in range(0, field_editable_controls.size()):
val.append([])
for value_index in range(0, field_editable_controls[range_index].size()):
var value_val = _string_to_val(field_editable_controls[range_index][value_index].text)
val[range_index].append(value_val)
return val
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
func focus_lost(control, range_index, value_index):
_gather_and_request_prop_action(control.text, range_index, value_index, "PA_PropSet")
# Auto select all text when user clicks inside
func select_line_edit(line_edit:LineEdit, state:bool):
if state:
line_edit.call_deferred("select_all")
else:
line_edit.call_deferred("deselect")
#-------------------------------------------------------------------------------
# Conversion to/from internal format
#-------------------------------------------------------------------------------
func _represented_to_actual(input):
var output_array := []
var range_array := []
if input is Array:
range_array = input.slice(0, 2)
else:
range_array.append(input)
for value_array in range_array:
var output_value_array := []
match value_count:
1:
if !(value_array is Array):
output_value_array = [value_array]
else:
output_value_array = value_array
2:
if representation_type == RepresentationType.VECTOR && value_array is Vector2:
output_value_array = [value_array.x, value_array.y]
elif representation_type == RepresentationType.VALUE && value_array is Array:
output_value_array = value_array.slice(0, 2)
elif value_array is Array: # this enables correct output_array when passing array-based currentVal as an input
output_value_array = value_array.slice(0, 2)
3:
if representation_type == RepresentationType.VECTOR && value_array is Vector3:
output_value_array = [value_array.x, value_array.y, value_array.z]
elif representation_type == RepresentationType.VALUE && value_array is Array:
output_value_array = value_array.slice(0, 3)
elif value_array is Array: # this enables correct output_array when passing array-based currentVal as an input
output_value_array = value_array.slice(0, 3)
4:
if value_array is Array:
output_value_array = value_array.slice(0, 4)
output_array.append(output_value_array)
return output_array
func _actual_to_represented(range_array:Array):
var output_array = []
for value_array in range_array:
var output_value = null
match value_count:
1:
output_value = value_array[0]
2:
if representation_type == RepresentationType.VECTOR:
output_value = Vector2(value_array[0], value_array[1])
elif representation_type == RepresentationType.VALUE:
output_value = value_array.slice(0, 1 + 1)
3:
if representation_type == RepresentationType.VECTOR:
output_value = Vector3(value_array[0], value_array[1], value_array[2])
elif representation_type == RepresentationType.VALUE:
output_value = value_array.slice(0, 2 + 1)
4:
if value_array is Array:
output_value = value_array.slice(0, 3 + 1)
output_array.append(output_value)
if output_array.size() == 1:
output_array = output_array[0]
elif output_array.size() == 0:
output_array = null
return output_array

View File

@@ -0,0 +1,137 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# Shows a dialog with InputField controls when button is pressed
# InputField controls will be set with PA_PropSet if dialog was confirmed
# InputField controls will be reverted to initial values if dialog was canceled
#-------------------------------------------------------------------------------
const UI_FoldableSection_SCN = preload('../side_panel/ui_foldable_section.tscn')
var margin_container:PanelContainer = null
var input_field_container:VBoxContainer = null
var _base_control:Control = null
var _resource_previewer = null
var property_sections: Dictionary = {}
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_Object")
margin_container = PanelContainer.new()
margin_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
margin_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
margin_container.name = "margin_container"
input_field_container = VBoxContainer.new()
input_field_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
input_field_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
input_field_container.add_theme_constant_override("separation", 0)
if settings.has("label_visibility"):
label.visible = settings.label_visibility
margin_container.add_child(input_field_container)
container_box.add_child(margin_container)
func prepare_input_field(__init_val, __base_control:Control, __resource_previewer):
super(__init_val, __base_control, __resource_previewer)
_base_control = __base_control
_resource_previewer = __resource_previewer
func _ready():
super()
if tab_index > 0:
margin_container.theme_type_variation = "PanelContainer"
else:
margin_container.add_theme_stylebox_override('panel', StyleBoxEmpty.new())
func _cleanup():
super()
if is_instance_valid(margin_container):
margin_container.queue_free()
if is_instance_valid(input_field_container):
input_field_container.queue_free()
func rebuild_object_input_fields(object:Object):
if !is_node_ready():
await ready
FunLib.free_children(input_field_container)
if is_instance_valid(object):
property_sections = {}
var section_dict = {}
var subsection_dict = {}
var nest_section_name = ""
var nest_subsection_name = ""
var input_fields = object.create_input_fields(_base_control, _resource_previewer)
for input_field in input_fields.values():
var nesting := (input_field.prop_name as String).split('/')
if nesting.size() >= 2:
nest_section_name = nesting[0]
section_dict = property_sections.get(nest_section_name, null)
if section_dict == null:
var section = UI_FoldableSection_SCN.instantiate()
input_field_container.add_child(section)
section.set_button_text(nest_section_name.capitalize())
section.set_nesting_level(0)
section_dict = {'section': section, 'subsections': {}}
property_sections[nest_section_name] = section_dict
if nesting.size() >= 3:
nest_subsection_name = nesting[1]
subsection_dict = section_dict.subsections.get(nest_subsection_name, null)
if subsection_dict == null:
var subsection = UI_FoldableSection_SCN.instantiate()
section_dict.section.add_child(subsection)
subsection.set_button_text(nest_subsection_name.capitalize())
subsection.set_nesting_level(1)
subsection_dict = {'subsection': subsection}
section_dict.subsections[nest_subsection_name] = subsection_dict
subsection_dict.add_prop_node(input_field)
else:
section_dict.section.add_prop_node(input_field)
else:
input_field_container.add_child(input_field)
# print("sections %d end" % [Time.get_ticks_msec()])
#-------------------------------------------------------------------------------
# Updaing the UI
#-------------------------------------------------------------------------------
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
_update_ui_to_val(final_val)
func _update_ui_to_val(val):
if is_instance_valid(val):
rebuild_object_input_fields(val)
visibility_forced = -1
visible = true
else:
rebuild_object_input_fields(null)
visibility_forced = 0
visible = false
super._update_ui_to_val(val)

View File

@@ -0,0 +1,54 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# Displays some text
#-------------------------------------------------------------------------------
var displayed_label: Label = null
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_PlainText")
displayed_label = Label.new()
displayed_label.name = "displayed_label"
displayed_label.size_flags_horizontal = SIZE_EXPAND_FILL
displayed_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
if settings.has("label_visibility"):
label.visible = settings.label_visibility
container_box.add_child(displayed_label)
func _cleanup():
super()
if is_instance_valid(displayed_label):
displayed_label.queue_free()
#-------------------------------------------------------------------------------
# Updaing the UI
#-------------------------------------------------------------------------------
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
_update_ui_to_val(final_val)
func _update_ui_to_val(val):
displayed_label.text = val
super._update_ui_to_val(val)

View File

@@ -0,0 +1,127 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# Stores a real (float) value
# Has a slider + line_edit for convinience
#-------------------------------------------------------------------------------
var real_slider:HSlider = null
var value_input:LineEdit = null
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_RealSlider")
real_slider = HSlider.new()
real_slider.name = "real_slider"
real_slider.size_flags_horizontal = SIZE_EXPAND_FILL
real_slider.min_value = settings.min
real_slider.max_value = settings.max
real_slider.step = settings.step
real_slider.allow_greater = settings.allow_greater
real_slider.allow_lesser = settings.allow_lesser
real_slider.size_flags_vertical = SIZE_SHRINK_CENTER
real_slider.value_changed.connect(_convert_and_request.bind("PA_PropEdit"))
real_slider.drag_ended.connect(_slider_drag_ended.bind("PA_PropSet"))
value_input = LineEdit.new()
value_input.name = "value_input"
value_input.size_flags_horizontal = SIZE_EXPAND_FILL
value_input.custom_minimum_size.x = 25.0
value_input.size_flags_vertical = SIZE_SHRINK_CENTER
value_input.focus_entered.connect(select_line_edit.bind(value_input, true))
value_input.focus_exited.connect(select_line_edit.bind(value_input, false))
# focus_exited is our main signal to commit the value in LineEdit
# release_focus() is expected to be called when pressing enter and only then we commit the value
value_input.focus_exited.connect(focus_lost.bind(value_input))
value_input.gui_input.connect(on_node_received_input.bind(value_input))
value_input.theme_type_variation = "IF_LineEdit"
real_slider.size_flags_stretch_ratio = 0.67
value_input.size_flags_stretch_ratio = 0.33
container_box.add_child(real_slider)
container_box.add_child(value_input)
func _cleanup():
super()
if is_instance_valid(real_slider):
real_slider.queue_free()
if is_instance_valid(value_input):
value_input.queue_free()
#-------------------------------------------------------------------------------
# Property management
#-------------------------------------------------------------------------------
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
_update_ui_to_val(final_val)
func _update_ui_to_val(val):
val = _string_to_val(val)
# So uhm... the signal is emitted when setting value through a variable too
# And I only want to emit it on UI interaction, so disconnect and then reconnect the signal
real_slider.value_changed.disconnect(_convert_and_request)
real_slider.value = val
real_slider.value_changed.connect(_convert_and_request.bind("PA_PropEdit"))
value_input.text = str(float(str("%.3f" % val)))
super._update_ui_to_val(val)
func _slider_drag_ended(value_changed: bool, prop_action_class:String):
_convert_and_request(str(real_slider.value), prop_action_class)
func _convert_and_request(val, prop_action_class:String):
_request_prop_action(_string_to_val(val), prop_action_class)
func _string_to_val(string) -> float:
if string is String:
if string.is_valid_float():
return string.to_float()
else:
logger.warn("String cannot be converted to float!")
elif string is float:
return string
else:
logger.warn("Passed variable is not a string!")
return 0.0
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
func focus_lost(line_edit:LineEdit):
_convert_and_request(line_edit.text, "PA_PropSet")
# Auto select all text when user clicks inside
func select_line_edit(line_edit:LineEdit, state:bool):
if state:
line_edit.call_deferred("select_all")
else:
line_edit.call_deferred("deselect")

View File

@@ -0,0 +1,227 @@
@tool
extends "ui_if_thumbnail_base.gd"
#-------------------------------------------------------------------------------
# Stores an array of thumbnailable resources
# Allows to assign existing project files through a browsing popup or drag'n'drop
#-------------------------------------------------------------------------------
const UI_FlexGridContainer = preload("../extensions/ui_flex_grid_container.gd")
var add_create_inst_button:bool = true
# Needed to make flex_grid functional...
var scroll_intermediary:ScrollContainer = null
var flex_grid:UI_FlexGridContainer = null
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_ThumbnailArray")
add_create_inst_button = settings.add_create_inst_button
scroll_intermediary = ScrollContainer.new()
scroll_intermediary.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll_intermediary.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll_intermediary.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
flex_grid = UI_FlexGridContainer.new()
scroll_intermediary.add_child(flex_grid)
container_box.add_child(scroll_intermediary)
func prepare_input_field(__init_val, __base_control:Control, __resource_previewer):
super(__init_val, __base_control, __resource_previewer)
func _cleanup():
super()
if is_instance_valid(scroll_intermediary):
scroll_intermediary.queue_free()
if is_instance_valid(flex_grid):
flex_grid.queue_free()
#-------------------------------------------------------------------------------
# Property management
#-------------------------------------------------------------------------------
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
_update_ui_to_val(final_val)
elif is_instance_of(prop_action, PA_ArrayInsert):
insert_element(final_val[prop_action.index], prop_action.index)
elif is_instance_of(prop_action, PA_ArrayRemove):
remove_element(prop_action.index)
elif is_instance_of(prop_action, PA_ArraySet):
set_element(final_val[prop_action.index], prop_action.index)
func _update_ui_to_val(val):
if !is_node_ready():
await ready
FunLib.free_children(flex_grid)
if add_create_inst_button:
_add_thumb_create_inst()
for i in range(0, val.size()):
var thumb = _add_thumb()
var element = val[i]
if is_instance_of(element, Resource):
_queue_thumbnail(element, thumb)
else:
thumb.set_thumbnail(null)
super._update_ui_to_val(val.duplicate())
# Set possible interaction features for an action thumbnail
func set_thumb_interaction_feature_with_data(interaction_flag:int, val, data:Dictionary):
if !is_node_ready():
await ready
if data.index >= flex_grid.get_child_count(): return
if data.index < 0: return
var thumb = flex_grid.get_child(data.index)
set_thumb_interaction_feature(thumb, interaction_flag, val)
#-------------------------------------------------------------------------------
# Manage elements
#-------------------------------------------------------------------------------
# Add an action thumbnail that allows to add new elements
func _add_thumb_create_inst():
if add_create_inst_button:
var thumb = _generate_thumbnail_create_inst()
flex_grid.add_child(thumb)
# Add a regular action thumbnail
func _add_thumb(index:int = -1):
if !is_node_ready(): return
var thumb = _generate_thumbnail()
flex_grid.add_child(thumb)
if index >= flex_grid.get_child_count():
logger.warn("_add_thumb index %d is beyond maximum of %d. Clamping..." % [index, flex_grid.get_child_count() - 1])
index = flex_grid.get_child_count() - 1
if add_create_inst_button:
if index < 0:
flex_grid.move_child(thumb, flex_grid.get_child_count() - 2)
else:
flex_grid.move_child(thumb, index)
return thumb
# Remove a regular action thumbnail
func _remove_thumb(index:int):
if index >= flex_grid.get_child_count(): return
if index < 0: return
var thumb = flex_grid.get_child(index)
flex_grid.remove_child(thumb)
thumb.queue_free()
#-------------------------------------------------------------------------------
# Request PropActions
#-------------------------------------------------------------------------------
func on_requested_add():
var index = flex_grid.get_child_count()
if add_create_inst_button:
index -= 1
_request_prop_action(null, "PA_ArrayInsert", {"index": index})
func on_requested_delete(thumb):
var index = thumb.get_index()
_request_prop_action(null, "PA_ArrayRemove", {"index": index})
func on_requested_clear(thumb):
var index = thumb.get_index()
_request_prop_action(null, "PA_ArraySet", {"index": index})
func on_check(state, thumb):
requested_check.emit(thumb.get_index(), state)
func on_label_edit(label_text, thumb):
requested_label_edit.emit(thumb.get_index(), label_text)
func on_press(thumb):
requested_press.emit(thumb.get_index())
#-------------------------------------------------------------------------------
# Manage elements of the current val
#-------------------------------------------------------------------------------
func insert_element(element, index:int):
_add_thumb(index)
_update_thumbnail(element, index)
func remove_element(index:int):
_remove_thumb(index)
func set_element(element, index:int):
_update_thumbnail(element, index)
#-------------------------------------------------------------------------------
# Assign/clear project files to thumbnails
#-------------------------------------------------------------------------------
# Request a custom prop action to set the property of an owning object
func set_res_for_thumbnail(res:Resource, thumb):
var index = thumb.get_index()
_request_prop_action(res, "PA_ArraySet", {"index": index})
#-------------------------------------------------------------------------------
# Manage thumbnails
#-------------------------------------------------------------------------------
func _update_thumbnail(res, index:int):
if !is_node_ready(): return
if index >= flex_grid.get_child_count(): return
if index < 0: return
_queue_thumbnail(res, flex_grid.get_child(index))

View File

@@ -0,0 +1,299 @@
@tool
extends "ui_input_field.gd"
#-------------------------------------------------------------------------------
# A base class for storing thumbnailable resources
#-------------------------------------------------------------------------------
const UI_ActionThumbnail_GD = preload("action_thumbnail/ui_action_thumbnail.gd")
const UI_ActionThumbnail = preload("action_thumbnail/ui_action_thumbnail.tscn")
const UI_ActionThumbnailCreateInst_GD = preload("action_thumbnail/ui_action_thumbnail_create_inst.gd")
const UI_ActionThumbnailCreateInst = preload("action_thumbnail/ui_action_thumbnail_create_inst.tscn")
const PRESET_NEW:Array = [UI_ActionThumbnail_GD.InteractionFlags.PRESS]
const PRESET_DELETE:Array = [UI_ActionThumbnail_GD.InteractionFlags.CLEAR, UI_ActionThumbnail_GD.InteractionFlags.DELETE]
const PRESET_PLANT_STATE:Array = [UI_ActionThumbnail_GD.InteractionFlags.DELETE, UI_ActionThumbnail_GD.InteractionFlags.SET_DRAG, UI_ActionThumbnail_GD.InteractionFlags.PRESS, UI_ActionThumbnail_GD.InteractionFlags.CHECK, UI_ActionThumbnail_GD.InteractionFlags.SHOW_COUNT, UI_ActionThumbnail_GD.InteractionFlags.EDIT_LABEL]
const PRESET_LOD_VARIANT:Array = [UI_ActionThumbnail_GD.InteractionFlags.DELETE, UI_ActionThumbnail_GD.InteractionFlags.PRESS, UI_ActionThumbnail_GD.InteractionFlags.SET_DRAG, UI_ActionThumbnail_GD.InteractionFlags.CLEAR]
const PRESET_RESOURCE:Array = [UI_ActionThumbnail_GD.InteractionFlags.SET_DIALOG, UI_ActionThumbnail_GD.InteractionFlags.SET_DRAG, UI_ActionThumbnail_GD.InteractionFlags.CLEAR]
var element_interaction_flags:Array = []
var accepted_classes:Array = []
var element_display_size:int = 100
var _base_control:Control = null
var _resource_previewer = null
var file_dialog:ConfirmationDialog = null
signal requested_press
signal requested_check
signal requested_label_edit
signal requested_edit_input_fields
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_ThumbnailArray")
accepted_classes = settings.accepted_classes
element_interaction_flags = settings.element_interaction_flags
element_display_size = settings.element_display_size
if Engine.is_editor_hint():
# Editor raises error everytime you run the game with F5 because of "abstract native class"
# https://github.com/godotengine/godot/issues/73525
file_dialog = DPON_FM.ED_EditorFileDialog.new()
else:
file_dialog = FileDialog.new()
file_dialog.file_mode = file_dialog.FILE_MODE_OPEN_FILE
add_file_dialog_filter()
file_dialog.current_dir = "res://"
file_dialog.current_path = "res://"
file_dialog.close_requested.connect(file_dialog_hidden)
func prepare_input_field(__init_val, __base_control:Control, __resource_previewer):
super(__init_val, __base_control, __resource_previewer)
_base_control = __base_control
_resource_previewer = __resource_previewer
func _enter_tree():
super()
if _base_control:
_base_control.add_child(file_dialog)
func _exit_tree():
if _base_control:
if _base_control.get_children().has(file_dialog):
_base_control.remove_child(file_dialog)
file_dialog.queue_free()
func _cleanup():
super()
if is_instance_valid(file_dialog):
file_dialog.queue_free()
# Add filters for all accepted classes
# Wish we could automatically infer extensions :/
func add_file_dialog_filter():
for accepted_class in accepted_classes:
var extensions := ""
var ext_name := ""
var inst = accepted_class.new()
if is_instance_of(inst, Mesh):
extensions = "*.tres, *.res, *.mesh, .*obj"
ext_name = "Mesh"
elif is_instance_of(inst, PackedScene):
extensions = "*.tscn, *.scn, *.gltf"
ext_name = "PackedScene"
elif is_instance_of(inst, Resource):
extensions = "*.tres, *.res"
ext_name = "Resource"
if extensions != "":
file_dialog.add_filter("%s ; %s" % [extensions, ext_name])
#-------------------------------------------------------------------------------
# Property management
#-------------------------------------------------------------------------------
# Callback for signals emitted from input_field_resource
func on_changed_interaction_feature(prop:String, interaction_flag:int, val, data:Dictionary):
if prop_name == prop:
set_thumb_interaction_feature_with_data(interaction_flag, val, data)
# Set possible interaction features for an action thumbnail
# To be overridden
func set_thumb_interaction_feature_with_data(interaction_flag:int, val, data:Dictionary):
pass
# Shorthand for setting action thumbnail features
func set_thumb_interaction_feature(thumb, interaction_flag:int, val):
if thumb && !is_instance_of(thumb, UI_ActionThumbnailCreateInst_GD):
thumb.set_features_val_to_flag(interaction_flag, val)
#-------------------------------------------------------------------------------
# Manage elements
#-------------------------------------------------------------------------------
# Generate a regular action thumbnail
func _generate_thumbnail():
var thumb := UI_ActionThumbnail.instantiate()
thumb.init(element_display_size, int(float(element_display_size) * 0.24), element_interaction_flags)
thumb.requested_delete.connect(on_requested_delete.bind(thumb))
thumb.requested_clear.connect(on_requested_clear.bind(thumb))
thumb.requested_set_dialog.connect(on_set_dialog.bind(thumb))
thumb.requested_set_drag.connect(on_set_drag.bind(thumb))
thumb.requested_press.connect(on_press.bind(thumb))
thumb.requested_check.connect(on_check.bind(thumb))
thumb.requested_label_edit.connect(on_label_edit.bind(thumb))
return thumb
# Generate an action thumbnail that creates new action thumbnails
func _generate_thumbnail_create_inst():
var thumb := UI_ActionThumbnailCreateInst.instantiate()
thumb.init(element_display_size, float(element_display_size) * 0.5, PRESET_NEW)
thumb.requested_press.connect(on_requested_add)
return thumb
#-------------------------------------------------------------------------------
# Request PropActions
#-------------------------------------------------------------------------------
# Action thumbnail callback
func on_requested_add():
pass
# Action thumbnail callback
func on_requested_delete(thumb):
pass
# Action thumbnail callback
func on_requested_clear(thumb):
pass
# Action thumbnail callback
func on_set_dialog(thumb):
file_dialog.popup_centered_ratio(0.5)
file_dialog.file_selected.connect(on_file_selected.bind(thumb))
# Action thumbnail callback
func on_set_drag(path, thumb):
on_file_selected(path, thumb)
# Action thumbnail callback
func on_check(state, thumb):
pass
# Action thumbnail callback
func on_label_edit(label_text, thumb):
pass
# Action thumbnail callback
func on_press(thumb):
pass
#-------------------------------------------------------------------------------
# Assign/clear project files to thumbnails
#-------------------------------------------------------------------------------
func file_dialog_hidden():
if file_dialog.file_selected.is_connected(on_file_selected):
file_dialog.file_selected.disconnect(on_file_selected)
# Load and try to assign a choosen resource
func on_file_selected(path, thumb):
var res = load(path)
var found_example = false
for accepted_class in accepted_classes:
if is_instance_of(res, accepted_class):
found_example = true
break
if !found_example:
logger.error("Selected a wrong resource class!")
return
set_res_for_thumbnail(res, thumb)
# Request a custom prop action to set the property of an owning object
# To be overridden
func set_res_for_thumbnail(res:Resource, thumb):
pass
#-------------------------------------------------------------------------------
# Manage thumbnails
#-------------------------------------------------------------------------------
# Queue a resource for preview generation in a resource previewer
func _queue_thumbnail(res:Resource, thumb: Node):
if !is_node_ready() || !is_instance_valid(thumb): return
var resource_path = _get_resource_path_for_resource(res)
if resource_path == "":
thumb.set_thumbnail(null)
if res:
thumb.set_alt_text(res.resource_name)
else:
thumb.set_alt_text("None")
else:
_resource_previewer.queue_resource_preview(resource_path, self, "try_assign_to_thumbnail",
{'thumb': thumb, 'thumb_res': res})
# Find a path to use as preview for a given resource
# TODO optimize this into a custom EditorResourcePreview
func _get_resource_path_for_resource(resource:Resource):
match FunLib.get_obj_class_string(resource):
"Greenhouse_PlantState":
if resource.plant.mesh_LOD_variants.size() >= 1 && resource.plant.mesh_LOD_variants[0].mesh:
return resource.plant.mesh_LOD_variants[0].mesh.resource_path
"Greenhouse_LODVariant":
if resource.mesh:
return resource.mesh.resource_path
if resource:
return resource.resource_path
else:
return ""
# Callback to assign a thumbnail after it was generated
func try_assign_to_thumbnail(path:String, preview:Texture2D, thumbnail_preview:Texture2D, userdata: Dictionary):
if !is_node_ready(): return
if preview:
userdata.thumb.set_thumbnail(preview)
else:
var alt_name = path.get_file()
if userdata.thumb_res:
alt_name = userdata.thumb_res.resource_name
userdata.thumb.set_alt_text(alt_name)

View File

@@ -0,0 +1,92 @@
@tool
extends "ui_if_thumbnail_base.gd"
#-------------------------------------------------------------------------------
# Stores a thumbnailable resource
# Allows to assign existing project files through a browsing popup or drag'n'drop
#-------------------------------------------------------------------------------
# TODO make it accept most thumbnailable Variants?
var _thumb = null
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}):
super(__init_val, __labelText, __prop_name, settings)
set_meta("class", "UI_IF_ThumbnailObject")
_thumb = _generate_thumbnail()
_thumb.size_flags_horizontal = SIZE_EXPAND
container_box.add_child(_thumb)
func _cleanup():
super()
if is_instance_valid(_thumb):
_thumb.queue_free()
#-------------------------------------------------------------------------------
# Property management
#-------------------------------------------------------------------------------
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
_update_ui_to_val(final_val)
func _update_ui_to_val(val):
if !_thumb || !_thumb.is_node_ready():
await ready
_queue_thumbnail(val, _thumb)
super(val)
func set_thumb_interaction_feature_with_data(interaction_flag:int, val, data:Dictionary):
set_thumb_interaction_feature(_thumb, interaction_flag, val)
#-------------------------------------------------------------------------------
# Request PropActions
#-------------------------------------------------------------------------------
func on_requested_clear(thumb):
_request_prop_action(null, "PA_PropSet")
func on_check(state, thumb):
requested_check.emit(0, state)
func on_label_edit(label_text, thumb):
requested_label_edit.emit(0, label_text)
func on_press(thumb):
requested_press.emit(0)
#-------------------------------------------------------------------------------
# Assign/clear project files to thumbnails
#-------------------------------------------------------------------------------
# Request a custom prop action to set the property of an owning object
func set_res_for_thumbnail(res:Resource, thumb):
_request_prop_action(res, "PA_PropSet")

View File

@@ -0,0 +1,242 @@
@tool
extends PanelContainer
#-------------------------------------------------------------------------------
# A parent class for name-value pairs similar to built-in inspector properties
# Is bound to a given property of a given object
# Will update this property if changed
# And will change if this property is updated elsewhere
#
# TODO: convert to premade scenes?
# this might speed up creation and setup of these elements
#-------------------------------------------------------------------------------
const ThemeAdapter = preload("../theme_adapter.gd")
const FunLib = preload("../../utility/fun_lib.gd")
const Logger = preload("../../utility/logger.gd")
const PropAction = preload("../../utility/input_field_resource/prop_action.gd")
const PA_PropSet = preload("../../utility/input_field_resource/pa_prop_set.gd")
const PA_PropEdit = preload("../../utility/input_field_resource/pa_prop_edit.gd")
const PA_ArrayInsert = preload("../../utility/input_field_resource/pa_array_insert.gd")
const PA_ArrayRemove = preload("../../utility/input_field_resource/pa_array_remove.gd")
const PA_ArraySet = preload("../../utility/input_field_resource/pa_array_set.gd")
const UndoRedoInterface = preload("../../utility/undo_redo_interface.gd")
const tab_size:float = 5.0
# A container for all displayed controls
var container_box:HBoxContainer = HBoxContainer.new()
# Gives a visual offset whenever neccessary
# Also sets the background color
var tab_spacer:Control = Control.new()
# Stores the name of our property
var label:Label = Label.new()
# Bound prop name
var prop_name:String = ""
# Value used to initialize UI for the first time
var init_val = null
# Cache the latest set value
# Can be reverted to from script
var val_cache = null
# A visual offset index
var tab_index:int = 0
# An override for input field's visibility
# -1 - don't force any visibility state
# 0/1 force invisible/visible state
var visibility_forced:int = -1
var _undo_redo = null
var disable_history:bool = false
var logger = null
signal prop_action_requested(prop_action)
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(__init_val, __labelText:String = "NONE", __prop_name:String = "", settings:Dictionary = {}, tooltip:String = ""):
set_meta("class", "UI_InputField")
logger = Logger.get_for(self)
init_val = __init_val
prop_name = __prop_name
size_flags_horizontal = SIZE_EXPAND_FILL
container_box.size_flags_horizontal = SIZE_EXPAND_FILL
label.name = "label"
label.text = __labelText
label.size_flags_horizontal = SIZE_EXPAND_FILL
if settings.has("tab"):
tab_index = settings.tab
set_stylebox(get_theme_stylebox('panel', 'PanelContainer'))
set_tooltip(tooltip)
add_child(container_box)
container_box.add_child(tab_spacer)
container_box.add_child(label)
func _notification(what):
match what:
NOTIFICATION_PREDELETE:
# Make sure we don't have memory leaks of keeping removed nodes in memory
_cleanup()
# Clean up to avoid memory leaks of keeping removed nodes in memory
func _cleanup():
if is_instance_valid(container_box):
container_box.queue_free()
if is_instance_valid(tab_spacer):
tab_spacer.queue_free()
if is_instance_valid(label):
label.queue_free()
func prepare_input_field(__init_val, __base_control:Control, __resource_previewer):
init_val = __init_val
func _ready():
_set_tab(tab_index)
func _enter_tree():
_update_ui_to_val(init_val)
init_val = null
# Set tabulation offset and color
func _set_tab(index:int):
tab_index = index
tab_spacer.custom_minimum_size.x = tab_index * tab_size
tab_spacer.size.x = tab_spacer.custom_minimum_size.x
tab_spacer.visible = false if tab_index <= 0 else true
if tab_index > 0:
var styleboxes = ThemeAdapter.lookup_sub_inspector_styleboxes(self, tab_index - 1)
set_stylebox(styleboxes.sub_inspector_bg)
else:
var stylebox = StyleBoxFlat.new()
stylebox.bg_color = Color.TRANSPARENT
set_stylebox(stylebox)
func set_tooltip(tooltip:String):
if tooltip.length() > 0:
label.mouse_filter = MOUSE_FILTER_STOP
label.mouse_default_cursor_shape = Control.CURSOR_HELP
label.tooltip_text = tooltip
else:
label.mouse_filter = MOUSE_FILTER_IGNORE
func set_stylebox(stylebox:StyleBox):
stylebox = stylebox.duplicate()
stylebox.content_margin_bottom = 1
stylebox.content_margin_top = 1
stylebox.content_margin_right = 0
stylebox.content_margin_left = 0
add_theme_stylebox_override("panel", stylebox)
#-------------------------------------------------------------------------------
# Updaing the UI
#-------------------------------------------------------------------------------
# Property changed outside of this InputField
# Update the UI
func on_prop_action_executed(prop_action:PropAction, final_val):
if prop_action.prop == prop_name:
_update_ui_to_prop_action(prop_action, final_val)
func on_prop_list_changed(prop_dict: Dictionary):
if visibility_forced >= 0:
visible = true if visibility_forced == 1 else false
else:
visible = prop_dict[prop_name].usage & PROPERTY_USAGE_EDITOR
# Actually respond to different PropActions
# To be overridden
func _update_ui_to_prop_action(prop_action:PropAction, final_val):
pass
# Specific implementation of updating UI
# To be overridden
func _update_ui_to_val(val):
val_cache = val
# Property changed by this InputField
# Request a PropAction
func _request_prop_action(val, prop_action_class:String, optional:Dictionary = {}):
var prop_action:PropAction = null
match prop_action_class:
"PA_PropSet":
prop_action = PA_PropSet.new(prop_name, val)
"PA_PropEdit":
prop_action = PA_PropEdit.new(prop_name, val)
"PA_ArrayInsert":
prop_action = PA_ArrayInsert.new(prop_name, val, optional.index)
"PA_ArrayRemove":
prop_action = PA_ArrayRemove.new(prop_name, val, optional.index)
"PA_ArraySet":
prop_action = PA_ArraySet.new(prop_name, val, optional.index)
if disable_history:
prop_action.can_create_history = false
debug_print_prop_action("Requesting prop action: %s from \"%s\"" % [str(prop_action), name])
prop_action_requested.emit(prop_action)
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
# Release focus from a child node when pressing enter
func on_node_received_input(event, node):
if node.has_focus():
if is_instance_of(event, InputEventKey) && !event.pressed:
if event.keycode == KEY_ENTER || event.keycode == KEY_ESCAPE:
node.release_focus()
#-------------------------------------------------------------------------------
# Debug
#-------------------------------------------------------------------------------
func debug_print_prop_action(string:String):
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/input_field_resource_log_prop_actions", false): return
logger.info(string)

View File

@@ -0,0 +1,15 @@
[gd_scene format=3 uid="uid://2fwp1t7pk0r"]
[node name="UI_Category_Brushes" type="VBoxContainer"]
offset_right = 348.0
offset_bottom = 112.0
[node name="Label_Category" type="Label" parent="."]
layout_mode = 2
text = "Brushes"
horizontal_alignment = 1
vertical_alignment = 1
[node name="TabContainer_Brushes" type="TabContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3

View File

@@ -0,0 +1,37 @@
[gd_scene format=3 uid="uid://c1llamk0isnv"]
[node name="UI_Category_Greenhouse" type="VBoxContainer"]
offset_top = 120.0
offset_right = 348.0
offset_bottom = 582.0
[node name="Label_Category_Plants" type="Label" parent="."]
layout_mode = 2
text = "Plants"
horizontal_alignment = 1
vertical_alignment = 1
[node name="TabContainer" type="TabContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
tabs_visible = false
[node name="VSplitContainer" type="VSplitContainer" parent="TabContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
split_offset = 150
[node name="ScrollContainer_PlantThumbnails" type="ScrollContainer" parent="TabContainer/VSplitContainer"]
layout_mode = 2
horizontal_scroll_mode = 0
[node name="VBoxContainer" type="VBoxContainer" parent="TabContainer/VSplitContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="ScrollContainer_Properties" type="ScrollContainer" parent="TabContainer/VSplitContainer/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
horizontal_scroll_mode = 0

View File

@@ -0,0 +1,51 @@
@tool
extends MarginContainer
@export var arrow_down:ImageTexture = null
@export var arrow_right:ImageTexture = null
var folded: bool = false : set = set_folded
var button_text: String = 'Section' : set = set_button_text
var nesting_level: int = 0 : set = set_nesting_level
signal folding_state_changed(new_state)
func _ready():
set_folded(folded)
set_button_text(button_text)
set_nesting_level(nesting_level)
func toggle_folded():
set_folded(!folded)
func set_folded(val):
folded = val
$VBoxContainer_Main/HBoxContainer_Offset.visible = !folded
$VBoxContainer_Main/Button_Fold.icon = arrow_right if folded else arrow_down
folding_state_changed.emit(folded)
func set_button_text(val):
button_text = val
$VBoxContainer_Main/Button_Fold.text = button_text
func add_prop_node(prop_node: Control):
$VBoxContainer_Main/HBoxContainer_Offset/VBoxContainer_Properties.add_child(prop_node)
func set_nesting_level(val):
nesting_level = val
match nesting_level:
0:
$VBoxContainer_Main/Button_Fold.theme_type_variation = "PropertySection"
1:
$VBoxContainer_Main/Button_Fold.theme_type_variation = "PropertySubsection"

View File

@@ -0,0 +1,60 @@
[gd_scene load_steps=6 format=3 uid="uid://cntl0a50ubjlr"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/controls/side_panel/ui_foldable_section.gd" id="2"]
[sub_resource type="Image" id="Image_4r0cm"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 70, 255, 255, 255, 76, 255, 255, 255, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 9, 255, 255, 255, 76, 255, 255, 255, 69, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 76, 255, 255, 255, 99, 255, 255, 255, 82, 255, 255, 255, 9, 255, 255, 255, 9, 255, 255, 255, 82, 255, 255, 255, 99, 255, 255, 255, 76, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 9, 255, 255, 255, 82, 255, 255, 255, 99, 255, 255, 255, 82, 255, 255, 255, 82, 255, 255, 255, 99, 255, 255, 255, 82, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 9, 255, 255, 255, 82, 255, 255, 255, 99, 255, 255, 255, 99, 255, 255, 255, 82, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 9, 255, 255, 255, 76, 255, 255, 255, 76, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_btpaa"]
image = SubResource("Image_4r0cm")
[sub_resource type="Image" id="Image_1q5kb"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 70, 255, 255, 255, 76, 255, 255, 255, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 76, 255, 255, 255, 99, 255, 255, 255, 82, 255, 255, 255, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 9, 255, 255, 255, 82, 255, 255, 255, 99, 255, 255, 255, 82, 255, 255, 255, 9, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 9, 255, 255, 255, 82, 255, 255, 255, 99, 255, 255, 255, 76, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 9, 255, 255, 255, 82, 255, 255, 255, 99, 255, 255, 255, 76, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 9, 255, 255, 255, 82, 255, 255, 255, 99, 255, 255, 255, 82, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 76, 255, 255, 255, 99, 255, 255, 255, 82, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 69, 255, 255, 255, 76, 255, 255, 255, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_cwp62"]
image = SubResource("Image_1q5kb")
[node name="UI_FoldableSection" type="MarginContainer"]
offset_right = 322.0
offset_bottom = 146.0
script = ExtResource("2")
arrow_down = SubResource("ImageTexture_btpaa")
arrow_right = SubResource("ImageTexture_cwp62")
[node name="VBoxContainer_Main" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="Button_Fold" type="Button" parent="VBoxContainer_Main"]
layout_mode = 2
theme_type_variation = &"PropertySection"
text = "Section"
icon = SubResource("ImageTexture_btpaa")
alignment = 0
[node name="HBoxContainer_Offset" type="HBoxContainer" parent="VBoxContainer_Main"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Offset" type="Control" parent="VBoxContainer_Main/HBoxContainer_Offset"]
custom_minimum_size = Vector2(4, 0)
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer_Properties" type="VBoxContainer" parent="VBoxContainer_Main/HBoxContainer_Offset"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[connection signal="pressed" from="VBoxContainer_Main/Button_Fold" to="." method="toggle_folded"]

View File

@@ -0,0 +1,20 @@
[gd_scene format=3 uid="uid://blw02gc85o6ro"]
[node name="UI_Section_Brush" type="PanelContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 4.0
offset_top = 31.0
offset_right = -4.0
offset_bottom = -4.0
grow_horizontal = 2
grow_vertical = 2
[node name="ScrollContainer_Properties" type="ScrollContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="VBoxContainer_Properties" type="VBoxContainer" parent="ScrollContainer_Properties"]
layout_mode = 2
size_flags_horizontal = 3

View File

@@ -0,0 +1,149 @@
@tool
extends TabContainer
#-------------------------------------------------------------------------------
# Displays the UI for Greenhouse + its plants and Toolshed + its brushes
#-------------------------------------------------------------------------------
const FunLib = preload("../../utility/fun_lib.gd")
const FoldableSection = preload("ui_foldable_section.gd")
const UI_IF_Object = preload("../input_fields/ui_if_object.gd")
const Greenhouse = preload("../../greenhouse/greenhouse.gd")
const PropAction = preload('../../utility/input_field_resource/prop_action.gd')
const PA_PropSet = preload("../../utility/input_field_resource/pa_prop_set.gd")
const PA_ArrayInsert = preload("../../utility/input_field_resource/pa_array_insert.gd")
const PA_ArrayRemove = preload("../../utility/input_field_resource/pa_array_remove.gd")
const PA_ArraySet = preload("../../utility/input_field_resource/pa_array_set.gd")
@onready var panel_container_tools_nd = $PanelContainer
@onready var panel_container_tools_split_nd = $PanelContainer/PanelContainer_Tools_Split
@onready var label_error_nd = $MarginContainer/Label_Error
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _ready():
set_meta("class", "UI_SidePanel")
panel_container_tools_nd.theme_type_variation = "InspectorPanelContainer"
#-------------------------------------------------------------------------------
# Updating the UI
#-------------------------------------------------------------------------------
# Set Greenhouse/Toolshed UI as a child
# Can pass an index to specify child order
func set_tool_ui(control:Control, index:int):
if panel_container_tools_split_nd.get_child_count() > index:
var last_tool = panel_container_tools_split_nd.get_child(index)
panel_container_tools_split_nd.remove_child(last_tool)
last_tool.queue_free()
panel_container_tools_split_nd.add_child(control)
if panel_container_tools_split_nd.get_child_count() > index:
panel_container_tools_split_nd.move_child(control, index)
# Switch between invalid setup error and normal tool view
func set_main_control_state(state):
current_tab = 0 if state else 1
#-------------------------------------------------------------------------------
# Folding sections
#-------------------------------------------------------------------------------
# Not a fan of how brute-force it is
# TODO: this WILL NOT WORK with nested foldables or in any slightly-different configuration
# in the future, we need to associated foldables directly with their input_field_resource
# and bake that association into foldable states
# Remove states that represent deleted resources
func cleanup_folding_states(folding_states:Dictionary):
for greenhouse_id in folding_states.keys().duplicate():
# Remove not found resource paths, but keep resource names until converted to paths
if !is_res_name(greenhouse_id) && !ResourceLoader.exists(greenhouse_id):
folding_states.erase(greenhouse_id)
else:
for plant_id in folding_states[greenhouse_id].keys().duplicate():
if !is_res_name(plant_id) && !ResourceLoader.exists(plant_id):
folding_states[greenhouse_id].erase(plant_id)
# Selected a new plant for edit. Update it's folding and bind foldables
func on_greenhouse_prop_action_executed(folding_states:Dictionary, greenhouse:Greenhouse, prop_action: PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet) && prop_action.prop == 'plant_types/selected_for_edit_resource':
refresh_folding_states_for_greenhouse(folding_states, greenhouse)
# Something caused a folding update (typically a gardener selected for edit)
func refresh_folding_states_for_greenhouse(folding_states:Dictionary, greenhouse:Greenhouse):
if !greenhouse.selected_for_edit_resource: return
var greenhouse_id = get_res_name_or_path(folding_states, greenhouse)
var plant_id = get_res_name_or_path(folding_states[greenhouse_id], greenhouse.selected_for_edit_resource)
if folding_states.has(greenhouse_id) && folding_states[greenhouse_id].has(plant_id):
call_deferred('set_folding_states', self, folding_states[greenhouse_id][plant_id])
call_deferred('bind_foldables', self, folding_states, greenhouse_id, plant_id)
# Restore folding states
func set_folding_states(node:Node, states: Dictionary):
if is_instance_of(node, UI_IF_Object):
var section_node = null
for section_name in node.property_sections:
section_node = node.property_sections[section_name].section
section_node.folded = states.get(section_name, false)
for child in node.get_children():
set_folding_states(child, states)
# Bind foldable ui elements to update the relevant folding states
func bind_foldables(node:Node, folding_states: Dictionary, greenhouse_id: String, plant_id: String):
if is_instance_of(node, UI_IF_Object):
var section_node = null
for section_name in node.property_sections:
section_node = node.property_sections[section_name].section
section_node.folding_state_changed.connect(on_foldable_folding_state_changed.bind(section_name, folding_states, greenhouse_id, plant_id))
on_foldable_folding_state_changed(section_node.folded, section_name, folding_states, greenhouse_id, plant_id)
for child in node.get_children():
bind_foldables(child, folding_states, greenhouse_id, plant_id)
# Foldable signal callback. Save it's state to plugin state
func on_foldable_folding_state_changed(folded:bool, section_name:String, folding_states: Dictionary, greenhouse_id: String, plant_id: String):
folding_states[greenhouse_id][plant_id][section_name] = folded
# Get resource path to use as ID. If resource hasn't been saved yet - use it's 'name' instead
# Also acts as a replacer when folding_states have resource names instead of paths, but paths became available
func get_res_name_or_path(target_dict:Dictionary, res):
var res_name = str(res)
if target_dict.has(res_name) && res.resource_path != '':
target_dict[res.resource_path] = target_dict[res_name]
target_dict.erase(res_name)
var res_id = str(res) if res.resource_path == '' else res.resource_path
if !target_dict.has(res_id):
target_dict[res_id] = {}
return res_id
# Check if giver string represents a resource name (e.g. [Resource:9000])
func is_res_name(string: String):
var result = string.begins_with('[') && string.ends_with(']')
return result

View File

@@ -0,0 +1,32 @@
[gd_scene load_steps=2 format=3 uid="uid://t2apc6cgn7ud"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/controls/side_panel/ui_side_panel.gd" id="3"]
[node name="SidePanel" type="TabContainer"]
custom_minimum_size = Vector2(300, 0)
offset_right = 450.0
offset_bottom = 600.0
size_flags_horizontal = 3
size_flags_vertical = 3
size_flags_stretch_ratio = 0.3
tabs_visible = false
script = ExtResource("3")
metadata/class = "UI_SidePanel"
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 2
theme_type_variation = &"InspectorPanelContainer"
[node name="PanelContainer_Tools_Split" type="VSplitContainer" parent="PanelContainer"]
layout_mode = 2
split_offset = 200
[node name="MarginContainer" type="MarginContainer" parent="."]
visible = false
layout_mode = 2
[node name="Label_Error" type="Label" parent="MarginContainer"]
layout_mode = 2
text = "To begin, set the Gardener's Work Directory in the Inspector"
horizontal_alignment = 1
vertical_alignment = 1

View File

@@ -0,0 +1,213 @@
@tool
#-------------------------------------------------------------------------------
# An function library to search through themes, adapt them and assign to controls
#-------------------------------------------------------------------------------
static var editor_theme: Theme = null
func _init():
set_meta("class", "ThemeAdapter")
# Create all custom node types for this plugin
static func adapt_theme(theme:Theme) -> Theme:
editor_theme = Theme.new()
editor_theme.merge_with(theme)
var base_color = editor_theme.get_color('base_color', 'Editor')
var dark_color_1 = editor_theme.get_color('dark_color_1', 'Editor')
var dark_color_2 = editor_theme.get_color('dark_color_2', 'Editor')
var dark_color_3 = editor_theme.get_color('dark_color_3', 'Editor')
var property_font_color = editor_theme.get_color('property_color', 'Editor')
var constant_background_margin := editor_theme.get_stylebox("Background", "EditorStyles").content_margin_top
var stylebox_content := editor_theme.get_stylebox("Content", "EditorStyles")
var stylebox_background := editor_theme.get_stylebox("Background", "EditorStyles")
var LineEdit_stylebox_normal := editor_theme.get_stylebox("normal", "LineEdit")
var PanelContainer_stylebox_panel = editor_theme.get_stylebox('panel', 'PanelContainer')
var Panel_stylebox_panel = editor_theme.get_stylebox('panel', 'Panel')
var Window_stylebox_panel = editor_theme.get_stylebox('panel', 'Window')
var Button_stylebox_focus := editor_theme.get_stylebox('focus', 'Button')
var EditorInspectorCategory_stylebox_bg := editor_theme.get_stylebox('bg', 'EditorInspectorCategory')
var EditorFonts_bold = editor_theme.get_font('bold', 'EditorFonts')
var EditorFonts_bold_size = editor_theme.get_font_size('bold_size', 'EditorFonts')
var Tree_font_color = editor_theme.get_color('font_color', 'Tree')
var Tree_v_separation = editor_theme.get_constant('v_separation', 'Tree')
var Tree_panel = editor_theme.get_stylebox('panel', 'Tree')
var Editor_font_color = editor_theme.get_color('font_color', 'Editor')
var Editor_accent_color = editor_theme.get_color('accent_color', 'Editor')
var Editor_dark_color_1 = editor_theme.get_color("dark_color_1", 'Editor')
# NoMargin -> MarginContainer
editor_theme.set_constant("offset_top", "NoMargin", 0)
editor_theme.set_constant("offset_left", "NoMargin", 0)
editor_theme.set_constant("offset_bottom", "NoMargin", 0)
editor_theme.set_constant("offset_right", "NoMargin", 0)
editor_theme.set_type_variation("NoMargin", "MarginContainer")
# ExternalMargin -> MarginContainer
editor_theme.set_constant("offset_top", "ExternalMargin", constant_background_margin)
editor_theme.set_constant("offset_left", "ExternalMargin", constant_background_margin)
editor_theme.set_constant("offset_bottom", "ExternalMargin", constant_background_margin)
editor_theme.set_constant("offset_right", "ExternalMargin", constant_background_margin)
editor_theme.set_type_variation("ExternalMargin", "MarginContainer")
# IF_LineEdit -> LineEdit
var IF_LineEdit_stylebox := LineEdit_stylebox_normal.duplicate(true)
IF_LineEdit_stylebox.bg_color = dark_color_2
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "normal", "IF_LineEdit", IF_LineEdit_stylebox)
editor_theme.set_type_variation("IF_LineEdit", "LineEdit")
# MultiRangeValuePanel -> PanelContainer
var MultiRangeValuePanel_stylebox_panel := PanelContainer_stylebox_panel.duplicate(true)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "panel", "MultiRangeValuePanel", MultiRangeValuePanel_stylebox_panel)
editor_theme.set_type_variation("MultiRangeValuePanel", "PanelContainer")
# MultiRangeValue -> LineEdit
var MultiRangeValue_stylebox := IF_LineEdit_stylebox.duplicate(true)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "normal", "MultiRangeValue", MultiRangeValue_stylebox)
editor_theme.set_type_variation("MultiRangeValue", "LineEdit")
# MultiRangePropLabel -> Label
var MultiRangePropLabel_stylebox_panel := PanelContainer_stylebox_panel.duplicate(true)
# var MultiRangePropLabel_stylebox_panel := LineEdit_stylebox_normal.duplicate(true)
MultiRangePropLabel_stylebox_panel.bg_color = dark_color_3
MultiRangePropLabel_stylebox_panel.draw_center = true
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "normal", "MultiRangePropLabel", MultiRangePropLabel_stylebox_panel)
editor_theme.set_type_variation("MultiRangePropLabel", "Label")
# MultiRangeDashLabel -> Label
var MultiRangeDashLabel_stylebox_panel := MultiRangePropLabel_stylebox_panel.duplicate(true)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "normal", "MultiRangeDashLabel", MultiRangeDashLabel_stylebox_panel)
editor_theme.set_type_variation("MultiRangeDashLabel", "Label")
# PlantTitleLineEdit -> LineEdit
var PlantTitleLineEdit_stylebox := StyleBoxFlat.new()
PlantTitleLineEdit_stylebox.bg_color = dark_color_3
PlantTitleLineEdit_stylebox.content_margin_left = 1
PlantTitleLineEdit_stylebox.content_margin_right = 1
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "normal", "PlantTitleLineEdit", PlantTitleLineEdit_stylebox)
editor_theme.set_type_variation("PlantTitleLineEdit", "LineEdit")
# InspectorPanelContainer -> PanelContainer
var InspectorPanelContainer_stylebox := Tree_panel.duplicate(true)
InspectorPanelContainer_stylebox.draw_center = true
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "panel", "InspectorPanelContainer", InspectorPanelContainer_stylebox)
editor_theme.set_type_variation("InspectorPanelContainer", "PanelContainer")
# InspectorWindowDialog -> Window
var InspectorWindowDialog_stylebox := Window_stylebox_panel.duplicate(true)
InspectorWindowDialog_stylebox.draw_center = true
InspectorWindowDialog_stylebox.bg_color = dark_color_1
InspectorWindowDialog_stylebox.border_color = dark_color_3
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "panel", "InspectorWindowDialog", InspectorWindowDialog_stylebox)
editor_theme.set_type_variation("InspectorWindowDialog", "Window")
# InspectorInnerPanelContainer -> PanelContainer
var InspectorInnerPanelContainer_stylebox := PanelContainer_stylebox_panel.duplicate(true)
InspectorInnerPanelContainer_stylebox.draw_center = false
InspectorInnerPanelContainer_stylebox.set_border_width_all(1)
InspectorInnerPanelContainer_stylebox.border_color = dark_color_3
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "panel", "InspectorInnerPanelContainer", InspectorInnerPanelContainer_stylebox)
editor_theme.set_theme_item(Theme.DATA_TYPE_FONT, "panel", "InspectorInnerPanelContainer", InspectorInnerPanelContainer_stylebox)
editor_theme.set_type_variation("InspectorInnerPanelContainer", "PanelContainer")
# PropertyCategory -> Label
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "normal", "PropertyCategory", EditorInspectorCategory_stylebox_bg)
editor_theme.set_theme_item(Theme.DATA_TYPE_FONT, "font", "PropertyCategory", EditorFonts_bold)
editor_theme.set_theme_item(Theme.DATA_TYPE_FONT_SIZE, "font_size", "PropertyCategory", EditorFonts_bold_size)
editor_theme.set_theme_item(Theme.DATA_TYPE_COLOR, "font_color", "PropertyCategory", Tree_font_color)
editor_theme.set_type_variation("PropertyCategory", "PanelContainer")
# PropertySection -> Button
var PropertySection_stylebox_bg_color = EditorInspectorCategory_stylebox_bg.bg_color
PropertySection_stylebox_bg_color.a *= 0.4
var PropertySection_stylebox_normal := StyleBoxFlat.new()
PropertySection_stylebox_normal.bg_color = PropertySection_stylebox_bg_color
PropertySection_stylebox_normal.set_content_margin_all(Tree_v_separation * 0.5)
var PropertySection_stylebox_hover := PropertySection_stylebox_normal.duplicate(true)
PropertySection_stylebox_hover.bg_color =PropertySection_stylebox_bg_color.lightened(0.2)
var PropertySection_stylebox_pressed := PropertySection_stylebox_normal.duplicate(true)
PropertySection_stylebox_pressed.bg_color = PropertySection_stylebox_bg_color.lightened(-0.05)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "normal", "PropertySection", PropertySection_stylebox_normal)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "hover", "PropertySection", PropertySection_stylebox_hover)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "pressed", "PropertySection", PropertySection_stylebox_pressed)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "focus", "PropertySection", StyleBoxEmpty.new())
editor_theme.set_theme_item(Theme.DATA_TYPE_FONT, "font", "PropertySection", EditorFonts_bold)
editor_theme.set_theme_item(Theme.DATA_TYPE_FONT_SIZE, "font_size", "PropertySection", EditorFonts_bold_size)
editor_theme.set_theme_item(Theme.DATA_TYPE_COLOR, "font_color", "PropertySection", Editor_font_color)
editor_theme.set_theme_item(Theme.DATA_TYPE_COLOR, "font_pressed_color", "PropertySection", Editor_font_color)
editor_theme.set_theme_item(Theme.DATA_TYPE_COLOR, "icon_color", "PropertySection", Editor_font_color)
editor_theme.set_theme_item(Theme.DATA_TYPE_COLOR, "icon_pressed_color", "PropertySection", Editor_font_color)
editor_theme.set_type_variation("PropertySection", "Button")
# PropertySubsection -> PanelContainer
var PropertySubsection_stylebox := PanelContainer_stylebox_panel.duplicate(true)
PropertySubsection_stylebox.draw_center = true
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "panel", "PropertySubsection", PropertySubsection_stylebox)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "normal", "PropertySubsection", PropertySubsection_stylebox)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "hover", "PropertySubsection", PropertySubsection_stylebox)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "pressed", "PropertySubsection", PropertySubsection_stylebox)
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "focus", "PropertySubsection", PropertySubsection_stylebox)
editor_theme.set_type_variation("PropertySubsection", "PanelContainer")
# ActionThumbnail_SelectionPanel -> Panel
var ActionThumbnail_SelectionPanel_stylebox := Button_stylebox_focus.duplicate(true)
ActionThumbnail_SelectionPanel_stylebox.bg_color = Color8(255, 255, 255, 51)
ActionThumbnail_SelectionPanel_stylebox.border_color = Color8(255, 255, 255, 255)
ActionThumbnail_SelectionPanel_stylebox.draw_center = true
editor_theme.set_theme_item(Theme.DATA_TYPE_STYLEBOX, "panel", "ActionThumbnail_SelectionPanel", ActionThumbnail_SelectionPanel_stylebox)
editor_theme.set_type_variation("ActionThumbnail_SelectionPanel", "PanelContainer")
# InspectorButton -> Button
# InspectorCheckBox -> CheckBox
# InspectorOptionButton -> OptionButton
# InspectorMenuButton -> MenuButton
for theme_type in ["Button", "CheckBox", "OptionButton", "MenuButton"]:
for data_type in range(0, editor_theme.DATA_TYPE_MAX):
for theme_item in editor_theme.get_theme_item_list(data_type, theme_type):
var item = editor_theme.get_theme_item(data_type, theme_item, theme_type)
if is_instance_of(item, Resource):
item = item.duplicate(true)
if data_type == editor_theme.DATA_TYPE_STYLEBOX:
match theme_item:
"normal", "pressed", "focus":
item.bg_color = dark_color_2
item.draw_center = true
"hover":
item.bg_color = dark_color_2 * 1.2
item.draw_center = true
"disabled":
item.bg_color = dark_color_2 * 1.5
item.draw_center = true
editor_theme.set_theme_item(data_type, theme_item, "Inspector" + theme_type, item)
editor_theme.set_type_variation("Inspector" + theme_type, theme_type)
return editor_theme
# Get styleboxes associated with nested objects
static func lookup_sub_inspector_styleboxes(search_node:Node, sub_index:int):
var styleboxes := {}
var sub_inspector_bg = "sub_inspector_bg%d" % [sub_index]
var sub_inspector_property_bg = "sub_inspector_property_bg%d" % [sub_index]
var sub_inspector_property_bg_selected = "sub_inspector_property_bg_selected%d" % [sub_index]
var stylebox_names := editor_theme.get_stylebox_list("Editor")
for stylebox_name in stylebox_names:
if stylebox_name == sub_inspector_bg:
styleboxes.sub_inspector_bg = editor_theme.get_stylebox(sub_inspector_bg, "Editor")
elif stylebox_name == sub_inspector_property_bg:
styleboxes.sub_inspector_property_bg = editor_theme.get_stylebox(sub_inspector_property_bg, "Editor")
elif stylebox_name == sub_inspector_property_bg_selected:
styleboxes.sub_inspector_property_bg_selected = editor_theme.get_stylebox(sub_inspector_property_bg_selected, "Editor")
return styleboxes

View File

@@ -0,0 +1,170 @@
extends RefCounted
const Logger = preload("../utility/logger.gd")
const Globals = preload("../utility/globals.gd")
const FunLib = preload("../utility/fun_lib.gd")
const Defaults = preload("../utility/defaults.gd")
const Greenhouse = preload("../greenhouse/greenhouse.gd")
const Toolshed = preload("../toolshed/toolshed.gd")
const Painter = preload("painter.gd")
const Arborist = preload("../arborist/arborist.gd")
const Placeform = preload("../arborist/placeform.gd")
const InputFieldResource = preload("../utility/input_field_resource/input_field_resource.gd")
var logger = null
var arborist: Arborist = null
var greenhouse: Greenhouse = null
var toolshed: Toolshed = null
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init(_arborist: Arborist, _greenhouse: Greenhouse, _toolshed: Toolshed = null):
logger = Logger.get_for(self)
arborist = _arborist
greenhouse = _greenhouse
toolshed = _toolshed
#-------------------------------------------------------------------------------
# Importing/exporting data
#-------------------------------------------------------------------------------
# Import data of a single plant (Greenhouse_Plant + placeforms)
func import_plant_data(file_path: String, plant_idx: int):
var file := FileAccess.open(file_path, FileAccess.READ)
if !file:
logger.error("Could not import '%s', error %s!" % [file_path, Globals.get_err_message(FileAccess.get_open_error())])
var test_json_conv = JSON.new()
var err = test_json_conv.parse(file.get_as_text())
if err != OK:
logger.error("Could not parse json at '%s', error %s!" % [file_path, Globals.get_err_message(err)])
return
var import_data = test_json_conv.data
file.close()
_import_process_data(plant_idx, import_data)
if import_data is Dictionary && !import_data.get("plant_data", {}).is_empty():
logger.info("Successfully imported plant settings and placeform(s) from '%s'" % [file_path])
else:
logger.info("Successfully imported placeform(s) from '%s'" % [file_path])
# Export data of a single plant (Greenhouse_Plant + placeforms)
func export_plant_data(file_path: String, plant_idx: int):
DirAccess.make_dir_recursive_absolute(file_path.get_base_dir())
var file := FileAccess.open(file_path, FileAccess.WRITE)
if !file:
logger.error("Could not export '%s', error %s!" % [file_path, Globals.get_err_message(FileAccess.get_open_error())])
var data = _export_gather_data(plant_idx)
var json_string = JSON.stringify(data)
file.store_string(json_string)
file.close()
logger.info("Successfully exported plant settings and placeform(s) to '%s'" % [file_path])
# Import data of an entire Greenhouse + placeforms
func import_greenhouse_data(file_path: String):
var file := FileAccess.open(file_path, FileAccess.READ)
if !file:
logger.error("Could not import '%s', error %s!" % [file_path, Globals.get_err_message(FileAccess.get_open_error())])
var test_json_conv = JSON.new()
var err = test_json_conv.parse(file.get_as_text())
if err != OK:
logger.error("Could not parse json at '%s', error %s!" % [file_path, Globals.get_err_message(err)])
return
var import_data = test_json_conv.data
file.close()
for i in import_data.size():
_import_process_data(i, import_data[i])
logger.info("Successfully imported entire greenhouse of %d plants from '%s" % [import_data.size(), file_path])
# Export data of an entire Greenhouse + placeforms
func export_greenhouse_data(file_path: String):
DirAccess.make_dir_recursive_absolute(file_path.get_base_dir())
var file := FileAccess.open(file_path, FileAccess.WRITE)
if !file:
logger.error("Could not export '%s', error %s!" % [file_path, Globals.get_err_message(FileAccess.get_open_error())])
var data = []
for plant_idx in range(greenhouse.greenhouse_plant_states.size()):
data.append(_export_gather_data(plant_idx))
var json_string = JSON.stringify(data)
file.store_string(json_string)
file.close()
logger.info("Successfully exported entire greenhouse of %d plants to '%s'" % [data.size(), file_path])
func _export_gather_data(plant_idx: int) -> Dictionary:
var plant_data = greenhouse.greenhouse_plant_states[plant_idx].ifr_to_dict(true)
var placeforms: Array = []
arborist.octree_managers[plant_idx].get_all_placeforms(placeforms)
var placeform_data := []
for placeform in placeforms:
placeform_data.append({
'placement': FunLib.vec3_to_str(placeform[0]),
'surface_normal': FunLib.vec3_to_str(placeform[1]),
'transform': FunLib.transform3d_to_str(placeform[2]),
})
logger.info("Successfully gathered plant settings and %d placeform(s) at index %d" % [placeform_data.size(), plant_idx])
return {
plant_data = plant_data,
placeform_data = placeform_data
}
func _import_process_data(plant_idx: int, data):
var placeform_data := []
var plant_data := {}
# New version, plant settings + transforms
if data is Dictionary:
placeform_data = data.placeform_data
plant_data = data.plant_data
# Old version, supports transforms-only, for Spatial Gardener 1.2.0 compatability
else:
placeform_data = data
var str_version = 1
if !placeform_data.is_empty():
var placeforms := []
if placeform_data[0].transform.contains(" - "):
str_version = 0
if !plant_data.is_empty():
plant_idx = greenhouse.add_plant_from_dict(plant_data, str_version)
if !placeform_data.is_empty():
var placeforms := []
for placeform_dict in placeform_data:
placeforms.append(Placeform.mk(
FunLib.str_to_vec3(placeform_dict.placement, str_version),
FunLib.str_to_vec3(placeform_dict.surface_normal, str_version),
FunLib.str_to_transform3d(placeform_dict.transform, str_version)))
arborist.batch_add_instances(placeforms, plant_idx)
arborist.call_deferred("emit_member_count", plant_idx)

View File

@@ -0,0 +1,261 @@
@tool
extends Node3D
#-------------------------------------------------------------------------------
# A previewer for octree structure
#-------------------------------------------------------------------------------
const FunLib = preload("../utility/fun_lib.gd")
const DponDebugDraw = preload("../utility/debug_draw.gd")
const MMIOctreeManager = preload("../arborist/mmi_octree/mmi_octree_manager.gd")
const MMIOctreeNode = preload("../arborist/mmi_octree/mmi_octree_node.gd")
# How many/which plants we want to preview
enum PlantViewModeFlags {
VIEW_NONE = 0,
VIEW_SELECTED_PLANT = 1,
VIEW_ALL_ACTIVE_PLANTS = 2,
VIEW_MAX = 3,
}
# What parts of an octree we want to preview
enum RenderModeFlags {
DRAW_OCTREE_NODES = 101,
DRAW_OCTREE_MEMBERS = 102,
}
var octree_MMIs:Array = []
var active_plant_view_mode:int = PlantViewModeFlags.VIEW_NONE
var active_render_modes:Array = [RenderModeFlags.DRAW_OCTREE_NODES]
var brush_active_plants:Array = []
var prop_edit_selected_plant: int = -1
#-------------------------------------------------------------------------------
# Debug view menu
#-------------------------------------------------------------------------------
# Create and initialize a debug view menu
static func make_debug_view_menu():
var debug_view_menu := MenuButton.new()
debug_view_menu.text = "Gardener Debug Viewer"
debug_view_menu.get_popup().hide_on_checkable_item_selection = false
debug_view_menu.get_popup().hide_on_item_selection = false
for i in range(0, PlantViewModeFlags.size() - 1):
debug_view_menu.get_popup().add_radio_check_item(PlantViewModeFlags.keys()[i].capitalize(), PlantViewModeFlags.values()[i])
debug_view_menu.get_popup().add_separator()
for i in range(0, RenderModeFlags.size()):
debug_view_menu.get_popup().add_check_item(RenderModeFlags.keys()[i].capitalize(), RenderModeFlags.values()[i])
return debug_view_menu
# Callback when flag is checked on a menu
func flag_checked(debug_view_menu:MenuButton, flag:int):
var flag_group = flag <= PlantViewModeFlags.VIEW_MAX
if flag_group:
active_plant_view_mode = flag
else:
if active_render_modes.has(flag):
active_render_modes.erase(flag)
else:
active_render_modes.append(flag)
up_to_date_debug_view_menu(debug_view_menu)
# Reset a menu to the current state of this DebugViewer
func up_to_date_debug_view_menu(debug_view_menu:MenuButton):
for i in range(0, debug_view_menu.get_popup().get_item_count()):
debug_view_menu.get_popup().set_item_checked(i, false)
update_debug_view_menu_to_flag(debug_view_menu, active_plant_view_mode)
for render_mode in active_render_modes:
update_debug_view_menu_to_flag(debug_view_menu, render_mode)
# Tick a flag in a menu
# TODO Decide if this should be simplified and moved to up_to_date_debug_view_menu
# Since flag checks happen in flag_checked anyways
func update_debug_view_menu_to_flag(debug_view_menu:MenuButton, flag:int):
var flag_group = flag <= PlantViewModeFlags.VIEW_MAX
for i in range(0, debug_view_menu.get_popup().get_item_count()):
var item_id = debug_view_menu.get_popup().get_item_id(i)
var id_group = item_id <= PlantViewModeFlags.VIEW_MAX
var opposite_state = !debug_view_menu.get_popup().is_item_checked(i)
if item_id == flag:
if flag_group:
debug_view_menu.get_popup().set_item_checked(i, true)
else:
debug_view_menu.get_popup().set_item_checked(i, opposite_state)
elif flag_group == id_group && flag_group:
debug_view_menu.get_popup().set_item_checked(i, false)
#-------------------------------------------------------------------------------
# Brush active plants
#-------------------------------------------------------------------------------
# Keep a local copy of selected for brush plant indexes
func set_brush_active_plant(is_brush_active, plant_index:int):
if is_brush_active:
if !brush_active_plants.has(plant_index):
brush_active_plants.append(plant_index)
else:
if brush_active_plants.has(plant_index):
brush_active_plants.erase(plant_index)
brush_active_plants.sort()
func reset_brush_active_plants():
brush_active_plants = []
#-------------------------------------------------------------------------------
# Selected for prop edit plants
#-------------------------------------------------------------------------------
func set_prop_edit_selected_plant(plant_index:int):
prop_edit_selected_plant = plant_index
func reset_prop_edit_selected_plant():
prop_edit_selected_plant = -1
#-------------------------------------------------------------------------------
# Debug redraw requests
#-------------------------------------------------------------------------------
func request_debug_redraw(octree_managers:Array):
debug_redraw(octree_managers)
#-------------------------------------------------------------------------------
# Drawing the structure
#-------------------------------------------------------------------------------
# Redraw every fitting octree
func debug_redraw(octree_managers:Array):
var used_octree_managers = []
match active_plant_view_mode:
# Don't draw anything
PlantViewModeFlags.VIEW_NONE:
ensure_MMIs(0)
# Draw only the plant selected for prop edit
PlantViewModeFlags.VIEW_SELECTED_PLANT:
if prop_edit_selected_plant >= 0:
ensure_MMIs(1)
used_octree_managers.append(octree_managers[prop_edit_selected_plant])
else:
ensure_MMIs(0)
# Draw all brush active plants
PlantViewModeFlags.VIEW_ALL_ACTIVE_PLANTS:
ensure_MMIs(brush_active_plants.size())
for plant_index in brush_active_plants:
used_octree_managers.append(octree_managers[plant_index])
for i in range(0, used_octree_managers.size()):
var MMI:MultiMeshInstance3D = octree_MMIs[i]
var octree_mamager:MMIOctreeManager = used_octree_managers[i]
debug_draw_node(octree_mamager.root_octree_node, MMI)
func erase_all():
ensure_MMIs(0)
# Make sure there is an MMI for every octree we're about to draw
# Passing 0 effectively erases any debug renders
func ensure_MMIs(amount:int):
if octree_MMIs.size() < amount:
for i in range(octree_MMIs.size(), amount):
var MMI = MultiMeshInstance3D.new()
add_child(MMI)
MMI.cast_shadow = false
MMI.multimesh = MultiMesh.new()
MMI.multimesh.transform_format = 1
MMI.multimesh.use_colors = true
MMI.multimesh.mesh = DponDebugDraw.generate_cube(Vector3.ONE, Color.WHITE)
octree_MMIs.append(MMI)
elif octree_MMIs.size() > amount:
var MMI = null
while octree_MMIs.size() > amount:
MMI = octree_MMIs.pop_back()
remove_child(MMI)
MMI.queue_free()
# Recursively draw an octree node
func debug_draw_node(octree_node:MMIOctreeNode, MMI:MultiMeshInstance3D):
var draw_node := active_render_modes.has(RenderModeFlags.DRAW_OCTREE_NODES)
var draw_members := active_render_modes.has(RenderModeFlags.DRAW_OCTREE_MEMBERS)
# Reset the instance counts if this node is a root
if !octree_node.parent:
MMI.multimesh.instance_count = 0
MMI.multimesh.visible_instance_count = 0
set_debug_redraw_instance_count(octree_node, MMI, draw_node, draw_members)
var extents:Vector3
var render_transform:Transform3D
var index:int
if draw_node:
extents = Vector3(octree_node.extent, octree_node.extent, octree_node.extent) * 0.999 * 2.0
render_transform = Transform3D(Basis.IDENTITY.scaled(extents), octree_node.center_pos)
index = MMI.multimesh.visible_instance_count
MMI.multimesh.visible_instance_count += 1
MMI.multimesh.set_instance_transform(index, render_transform)
MMI.multimesh.set_instance_color(index, octree_node.debug_get_color())
if draw_members && octree_node.is_leaf:
var member_extent = FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/debug_viewer_octree_member_size", 0.0) * 0.5
extents = Vector3(member_extent, member_extent, member_extent)
var basis = Basis.IDENTITY.scaled(extents)
for placeform in octree_node.get_placeforms():
render_transform = Transform3D(basis, placeform[0])
index = MMI.multimesh.visible_instance_count
MMI.multimesh.visible_instance_count += 1
MMI.multimesh.set_instance_transform(index, render_transform)
MMI.multimesh.set_instance_color(index, Color.WHITE)
for child in octree_node.child_nodes:
debug_draw_node(child, MMI)
# Recursively set the appropriate instance count for an MMI
func set_debug_redraw_instance_count(octree_node:MMIOctreeNode, MMI:MultiMeshInstance3D, draw_node:bool, draw_members:bool):
if draw_node:
MMI.multimesh.instance_count += 1
if octree_node.is_leaf && draw_members:
MMI.multimesh.instance_count += octree_node.member_count()
for child in octree_node.child_nodes:
set_debug_redraw_instance_count(child, MMI, draw_node, draw_members)

View File

@@ -0,0 +1,778 @@
@tool
extends Node3D
#-------------------------------------------------------------------------------
# Manages the lifecycles and connection of all components:
# Greenhouse plants, Toolshed brushes, Painter controller
# And the Arborist plant placement manager
#
# A lot of these connections go through the Gardener
# Because some signal receivers need additional data the signal senders don't know about
# E.g. painter doesn't know about plant states, but arborist needs them to apply painting changes
#-------------------------------------------------------------------------------
const FunLib = preload("../utility/fun_lib.gd")
const Logger = preload("../utility/logger.gd")
const Defaults = preload("../utility/defaults.gd")
const Greenhouse = preload("../greenhouse/greenhouse.gd")
const Toolshed = preload("../toolshed/toolshed.gd")
const Painter = preload("painter.gd")
const Arborist = preload("../arborist/arborist.gd")
const DebugViewer = preload("debug_viewer.gd")
const UI_SidePanel_SCN = preload("../controls/side_panel/ui_side_panel.tscn")
const UI_SidePanel = preload("../controls/side_panel/ui_side_panel.gd")
const Globals = preload("../utility/globals.gd")
const DataImportExport = preload("data_import_export.gd")
const PropAction = preload("../utility/input_field_resource/prop_action.gd")
const PA_PropSet = preload("../utility/input_field_resource/pa_prop_set.gd")
const PA_PropEdit = preload("../utility/input_field_resource/pa_prop_edit.gd")
const PA_ArrayInsert = preload("../utility/input_field_resource/pa_array_insert.gd")
const PA_ArrayRemove = preload("../utility/input_field_resource/pa_array_remove.gd")
const PA_ArraySet = preload("../utility/input_field_resource/pa_array_set.gd")
var plugin_version: String = ""
var storage_version: int = 0
#export
var refresh_octree_shared_LOD_variants:bool = false : set = set_refresh_octree_shared_LOD_variants
# file_management
var garden_work_directory:String : set = set_garden_work_directory
# gardening
var gardening_collision_mask := pow(2, 0) : set = set_gardening_collision_mask
var initialized_for_edit:bool = false : set = set_initialized_for_edit
var is_edited: bool = false
var toolshed:Toolshed = null
var greenhouse:Greenhouse = null
var painter:Painter = null
var arborist:Arborist = null
var debug_viewer:DebugViewer = null
var _resource_previewer = null
var _base_control:Control = null
var _undo_redo = null
var _side_panel:UI_SidePanel = null
var ui_category_brushes:Control = null
var ui_category_plants:Control = null
var painting_node:Node3D = null
var logger = null
var forward_input_events:bool = true
signal changed_initialized_for_edit(state)
signal greenhouse_prop_action_executed(prop_action, final_val)
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init():
set_meta("class", "Gardener")
# Update plugin/storage versions that might have been stored inside a .tscn file for this Gardener
# In case it was created in an older version of this plugin
func update_plugin_ver():
plugin_version = get_plugin_ver()
storage_version = get_storage_ver()
static func get_plugin_ver():
return '1.3.3'
static func get_storage_ver():
return 3
func _ready():
update_plugin_ver()
logger = Logger.get_for(self, name)
# Without editor we only care about an Arborist
# But it is already self-sufficient, so no need to initialize it
if !Engine.is_editor_hint(): return
if has_node('painting'):
painting_node = get_node('painting')
else:
painting_node = Node3D.new()
painting_node.name = "painting"
add_child(painting_node)
if has_node('debug_viewer'):
debug_viewer = get_node('debug_viewer')
else:
debug_viewer = DebugViewer.new()
debug_viewer.name = "debug_viewer"
add_child(debug_viewer)
init_painter()
painter.set_brush_collision_mask(gardening_collision_mask)
reload_resources()
init_arborist()
set_gardening_collision_mask(gardening_collision_mask)
func _enter_tree():
pass
func _exit_tree():
if !Engine.is_editor_hint(): return
_apply_changes()
stop_editing()
func _process(delta):
if painter:
painter.update(delta)
func _apply_changes():
if !Engine.is_editor_hint(): return
if !FunLib.is_dir_valid(garden_work_directory): return
save_toolshed()
save_greenhouse()
toolshed.set_undo_redo(_undo_redo)
greenhouse.set_undo_redo(_undo_redo)
func add_child(node:Node, legible_unique_name:bool = false, internal:InternalMode = 0):
super.add_child(node, legible_unique_name)
update_configuration_warnings()
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
func forwarded_input(camera, event):
if !forward_input_events: return false
var handled = painter.forwarded_input(camera, event)
if !handled:
handled = toolshed.forwarded_input(camera, event)
if !handled:
handled = arborist._unhandled_input(event)
return handled
# A hack to propagate editor camera
# Should be called by plugin.gd
func propagate_camera(camera:Camera3D):
if arborist:
arborist.active_camera_override = camera
#-------------------------------------------------------------------------------
# Initialization
#-------------------------------------------------------------------------------
# This is supposed to address a problem decribed in "start_gardener_edit()" of "plugin.gd"
# Instead of recalculating everything, we hope it's enough to just restore the member references
func restore_references():
logger = Logger.get_for(self, name)
if !Engine.is_editor_hint(): return
if has_node('painting'):
painting_node = get_node('painting')
if has_node('debug_viewer'):
debug_viewer = get_node('debug_viewer')
init_painter()
painter.set_brush_collision_mask(gardening_collision_mask)
reload_resources()
if has_node("Arborist") && is_instance_of(get_node("Arborist"), Arborist):
arborist = get_node("Arborist")
set_gardening_collision_mask(gardening_collision_mask)
# Initialize a Painter
# Assumed to be the first manager to initialize
func init_painter():
FunLib.free_children(painting_node)
painter = Painter.new(painting_node)
painter.stroke_updated.connect(on_painter_stroke_updated)
painter.changed_active_brush_prop.connect(on_changed_active_brush_prop)
painter.stroke_started.connect(on_painter_stroke_started)
painter.stroke_finished.connect(on_painter_stroke_finished)
# Initialize the Arborist and connect it to other objects
# Won't be called without editor, as Arborist is already self-sufficient
func init_arborist():
# A fancy way of saying
# "Make sure there is a correct node with a correct name"
if has_node("Arborist") && is_instance_of(get_node("Arborist"), Arborist):
arborist = get_node("Arborist")
logger.info("Found existing Arborist")
else:
if has_node("Arborist"):
var old_arborist = get_node("Arborist")
old_arborist.owner = null
remove_child(old_arborist)
old_arborist.queue_free()
logger.info("Removed invalid Arborist")
arborist = Arborist.new()
arborist.name = "Arborist"
add_child(arborist)
logger.info("Added new Arborist")
if greenhouse:
pair_arborist_greenhouse()
pair_debug_viewer_arborist()
pair_debug_viewer_greenhouse()
# Initialize a Greenhouse and a Toolshed
# Rebuild UI if needed
func reload_resources():
var last_toolshed = toolshed
var last_greenhouse = greenhouse
var created_new_toolshed := false
var created_new_greenhouse := false
if !FunLib.is_dir_valid(garden_work_directory):
logger.warn("Skipped loading Toolshed and Greenhouse, please specify a working directory for this Gardener (%s)" % [str(self)])
else:
toolshed = FunLib.load_res(garden_work_directory, "toolshed.tres", false)
greenhouse = FunLib.load_res(garden_work_directory, "greenhouse.tres", false)
if !toolshed:
logger.warn("Unable to load Toolshed, created a new one")
toolshed = Defaults.DEFAULT_TOOLSHED()
created_new_toolshed = true
if !greenhouse:
logger.warn("Unable to load Greenhouse, created a new one")
greenhouse = Greenhouse.new()
created_new_greenhouse = true
toolshed.set_undo_redo(_undo_redo)
greenhouse.set_undo_redo(_undo_redo)
if last_toolshed:
last_toolshed.prop_action_executed.disconnect(on_toolshed_prop_action_executed)
last_toolshed.prop_action_executed_on_brush.disconnect(on_toolshed_prop_action_executed_on_brush)
FunLib.ensure_signal(toolshed.prop_action_executed, on_toolshed_prop_action_executed)
FunLib.ensure_signal(toolshed.prop_action_executed_on_brush, on_toolshed_prop_action_executed_on_brush)
if last_greenhouse:
last_greenhouse.prop_action_executed.disconnect(on_greenhouse_prop_action_executed)
last_greenhouse.prop_action_executed_on_plant_state.disconnect(on_greenhouse_prop_action_executed_on_plant_state)
last_greenhouse.prop_action_executed_on_plant_state_plant.disconnect(on_greenhouse_prop_action_executed_on_plant_state_plant)
last_greenhouse.prop_action_executed_on_LOD_variant.disconnect(on_greenhouse_prop_action_executed_on_LOD_variant)
last_greenhouse.req_octree_reconfigure.disconnect(on_greenhouse_req_octree_reconfigure)
last_greenhouse.req_octree_recenter.disconnect(on_greenhouse_req_octree_recenter)
last_greenhouse.req_import_plant_data.disconnect(on_greenhouse_req_import_plant_data)
last_greenhouse.req_export_plant_data.disconnect(on_greenhouse_req_export_plant_data)
last_greenhouse.req_import_greenhouse_data.disconnect(on_greenhouse_req_import_greenhouse_data)
last_greenhouse.req_export_greenhouse_data.disconnect(on_greenhouse_req_export_greenhouse_data)
FunLib.ensure_signal(greenhouse.prop_action_executed, on_greenhouse_prop_action_executed)
FunLib.ensure_signal(greenhouse.prop_action_executed_on_plant_state, on_greenhouse_prop_action_executed_on_plant_state)
FunLib.ensure_signal(greenhouse.prop_action_executed_on_plant_state_plant, on_greenhouse_prop_action_executed_on_plant_state_plant)
FunLib.ensure_signal(greenhouse.prop_action_executed_on_LOD_variant, on_greenhouse_prop_action_executed_on_LOD_variant)
FunLib.ensure_signal(greenhouse.req_octree_reconfigure, on_greenhouse_req_octree_reconfigure)
FunLib.ensure_signal(greenhouse.req_octree_recenter, on_greenhouse_req_octree_recenter)
FunLib.ensure_signal(greenhouse.req_import_plant_data, on_greenhouse_req_import_plant_data)
FunLib.ensure_signal(greenhouse.req_export_plant_data, on_greenhouse_req_export_plant_data)
FunLib.ensure_signal(greenhouse.req_import_greenhouse_data, on_greenhouse_req_import_greenhouse_data)
FunLib.ensure_signal(greenhouse.req_export_greenhouse_data, on_greenhouse_req_export_greenhouse_data)
if arborist:
pair_arborist_greenhouse()
if toolshed && toolshed != last_toolshed && _side_panel:
ui_category_brushes = toolshed.create_ui(_base_control, _resource_previewer)
_side_panel.set_tool_ui(ui_category_brushes, 0)
if greenhouse && greenhouse != last_greenhouse && _side_panel:
ui_category_plants = greenhouse.create_ui(_base_control, _resource_previewer)
_side_panel.set_tool_ui(ui_category_plants, 1)
if arborist:
for i in range(0, arborist.octree_managers.size()):
arborist.emit_member_count(i)
if created_new_toolshed:
save_toolshed()
if created_new_greenhouse:
save_greenhouse()
# It's possible we load a different Greenhouse while an Arborist is already initialized
# So collapse that into a function
func pair_arborist_greenhouse():
if !arborist || !greenhouse:
if !arborist: logger.warn("Arborist->Greenhouse: Arborist is not initialized!")
if !greenhouse: logger.warn("Arborist->Greenhouse: Greenhouse is not initialized!")
return
# We could duplicate an array, but that's additional overhead so we assume Arborist won't change it
arborist.setup(greenhouse.greenhouse_plant_states)
if !arborist.member_count_updated.is_connected(greenhouse.plant_count_updated):
arborist.member_count_updated.connect(greenhouse.plant_count_updated)
func pair_debug_viewer_greenhouse():
if !debug_viewer || !greenhouse:
if !debug_viewer: logger.warn("DebugViewer->Greenhouse: DebugViewer is not initialized!")
if !greenhouse: logger.warn("DebugViewer->Greenhouse: Greenhouse is not initialized!")
return
debug_viewer.set_prop_edit_selected_plant(greenhouse.greenhouse_plant_states.find(greenhouse.selected_for_edit_resource))
reinit_debug_draw_brush_active()
func pair_debug_viewer_arborist():
if !debug_viewer || !arborist:
if !debug_viewer: logger.warn("DebugViewer->Arborist: DebugViewer is not initialized!")
if !arborist: logger.warn("DebugViewer->Arborist: Arborist is not initialized!")
return
if !arborist.req_debug_redraw.is_connected(debug_viewer.request_debug_redraw):
arborist.req_debug_redraw.connect(debug_viewer.request_debug_redraw)
#-------------------------------------------------------------------------------
# Start/stop editing lifecycle
#-------------------------------------------------------------------------------
# Start editing (painting) a scene
func start_editing(__base_control:Control, __resource_previewer, __undoRedo, __side_panel:UI_SidePanel):
_base_control = __base_control
_resource_previewer = __resource_previewer
_undo_redo = __undoRedo
_side_panel = __side_panel
changed_initialized_for_edit.connect(_side_panel.set_main_control_state)
ui_category_brushes = toolshed.create_ui(_base_control, _resource_previewer)
ui_category_plants = greenhouse.create_ui(_base_control, _resource_previewer)
_side_panel.set_tool_ui(ui_category_brushes, 0)
_side_panel.set_tool_ui(ui_category_plants, 1)
toolshed.set_undo_redo(_undo_redo)
greenhouse.set_undo_redo(_undo_redo)
arborist._undo_redo = _undo_redo
# # Making sure we and UI are on the same page (setting property values and checkboxes/tabs)
painter_update_to_active_brush(toolshed.active_brush)
_side_panel.set_main_control_state(initialized_for_edit)
painter.start_editing()
for i in range(0, arborist.octree_managers.size()):
arborist.emit_member_count(i)
# Make sure LOD_Variants in a shared Octree array are up-to-date
set_refresh_octree_shared_LOD_variants(true)
is_edited = true
# Stop editing (painting) a scene
func stop_editing():
if is_instance_valid(_side_panel):
changed_initialized_for_edit.disconnect(_side_panel.set_main_control_state)
_side_panel = null
if is_instance_valid(painter):
painter.stop_editing()
is_edited = false
# We can properly start editing only when a workDirectory is set
func validate_initialized_for_edit():
var work_directory_valid = FunLib.is_dir_valid(garden_work_directory)
# Originally there were two conditions to fulfill, not just the workDirectory
# Keeping this in case it will be needed in the future
var _initialized_for_edit = work_directory_valid
if initialized_for_edit != _initialized_for_edit:
set_initialized_for_edit(_initialized_for_edit)
# Pass a request for updating a debug view menu
func up_to_date_debug_view_menu(debug_view_menu:MenuButton):
assert(debug_viewer)
debug_viewer.up_to_date_debug_view_menu(debug_view_menu)
debug_viewer.request_debug_redraw(arborist.octree_managers)
# Pass a request for checking a debug view menu flag
func debug_view_flag_checked(debug_view_menu:MenuButton, flag:int):
assert(debug_viewer)
debug_viewer.flag_checked(debug_view_menu, flag)
debug_viewer.request_debug_redraw(arborist.octree_managers)
#-------------------------------------------------------------------------------
# Handle changes in owned properties
#-------------------------------------------------------------------------------
func set_gardening_collision_mask(val):
gardening_collision_mask = val
if painter:
painter.set_brush_collision_mask(gardening_collision_mask)
if arborist:
arborist.set_gardening_collision_mask(gardening_collision_mask)
func set_garden_work_directory(val):
if !val.is_empty() && !val.ends_with("/"):
val += "/"
var changed = garden_work_directory != val
garden_work_directory = val
if !Engine.is_editor_hint(): return
# If we changed a directory, reload everything that resides there
if changed:
if is_inside_tree():
reload_resources()
validate_initialized_for_edit()
func set_initialized_for_edit(val):
initialized_for_edit = val
changed_initialized_for_edit.emit(initialized_for_edit)
#-------------------------------------------------------------------------------
# Handle communication with the Greenhouse
#-------------------------------------------------------------------------------
# When Greenhouse properties are changed
func on_greenhouse_prop_action_executed(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_ArrayInsert):
arborist.on_plant_added(final_val[prop_action.index], prop_action.index)
reinit_debug_draw_brush_active()
elif is_instance_of(prop_action, PA_ArrayRemove):
arborist.on_plant_removed(prop_action.val, prop_action.index)
reinit_debug_draw_brush_active()
elif is_instance_of(prop_action, PA_PropSet) && prop_action.prop == "plant_types/selected_for_edit_resource":
debug_viewer.set_prop_edit_selected_plant(greenhouse.greenhouse_plant_states.find(final_val))
debug_viewer.request_debug_redraw(arborist.octree_managers)
greenhouse_prop_action_executed.emit(prop_action, final_val)
# When Greenhouse_PlantState properties are changed
func on_greenhouse_prop_action_executed_on_plant_state(prop_action:PropAction, final_val, plant_state):
var plant_index = greenhouse.greenhouse_plant_states.find(plant_state)
match prop_action.prop:
"plant/plant_brush_active":
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
debug_viewer.set_brush_active_plant(plant_state.plant_brush_active, plant_index)
debug_viewer.request_debug_redraw(arborist.octree_managers)
# When Greenhouse_Plant properties are changed
func on_greenhouse_prop_action_executed_on_plant_state_plant(prop_action:PropAction, final_val, plant, plant_state):
var plant_index = greenhouse.greenhouse_plant_states.find(plant_state)
match prop_action.prop:
"mesh/mesh_LOD_variants":
if is_instance_of(prop_action, PA_ArrayInsert):
var mesh_index = prop_action.index
arborist.on_LOD_variant_added(plant_index, mesh_index, final_val[mesh_index])
elif is_instance_of(prop_action, PA_ArrayRemove):
var mesh_index = prop_action.index
arborist.on_LOD_variant_removed(plant_index, mesh_index)
elif is_instance_of(prop_action, PA_ArraySet):
var mesh_index = prop_action.index
arborist.on_LOD_variant_set(plant_index, mesh_index, final_val[mesh_index])
"mesh/mesh_LOD_max_distance":
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
arborist.update_plant_LOD_max_distance(plant_index, final_val)
"mesh/mesh_LOD_kill_distance":
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
arborist.update_plant_LOD_kill_distance(plant_index, final_val)
# When Greenhouse_LODVariant properties are changed
func on_greenhouse_prop_action_executed_on_LOD_variant(prop_action:PropAction, final_val, LOD_variant, plant, plant_state):
var plant_index = greenhouse.greenhouse_plant_states.find(plant_state)
var mesh_index = plant.mesh_LOD_variants.find(LOD_variant)
match prop_action.prop:
"spawned_spatial":
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
arborist.on_LOD_variant_prop_changed_spawned_spatial(plant_index, mesh_index, final_val)
"cast_shadow":
if is_instance_of(prop_action, PA_PropSet) || is_instance_of(prop_action, PA_PropEdit):
arborist.set_LODs_to_active_index(plant_index)
# A request to reconfigure an octree
func on_greenhouse_req_octree_reconfigure(plant, plant_state):
if !is_edited: return
var plant_index = greenhouse.greenhouse_plant_states.find(plant_state)
arborist.reconfigure_octree(plant_state, plant_index)
# A request to recenter an octree
func on_greenhouse_req_octree_recenter(plant, plant_state):
if !is_edited: return
var plant_index = greenhouse.greenhouse_plant_states.find(plant_state)
arborist.recenter_octree(plant_state, plant_index)
# Update brush active indexes for DebugViewer
func reinit_debug_draw_brush_active():
debug_viewer.reset_brush_active_plants()
for plant_index in range(0, greenhouse.greenhouse_plant_states.size()):
var plant_state = greenhouse.greenhouse_plant_states[plant_index]
debug_viewer.set_brush_active_plant(plant_state.plant_brush_active, plant_index)
debug_viewer.request_debug_redraw(arborist.octree_managers)
#-------------------------------------------------------------------------------
# Importing/exporting data
#-------------------------------------------------------------------------------
# A request to import plant data
func on_greenhouse_req_import_plant_data(file_path: String, plant_idx: int):
if !is_edited: return
var import_export = DataImportExport.new(arborist, greenhouse)
import_export.import_plant_data(file_path, plant_idx)
# A request to export plant data
func on_greenhouse_req_export_plant_data(file_path: String, plant_idx: int):
if !is_edited: return
var import_export = DataImportExport.new(arborist, greenhouse)
import_export.export_plant_data(file_path, plant_idx)
# A request to import entire greenhouse data
func on_greenhouse_req_import_greenhouse_data(file_path: String):
if !is_edited: return
var import_export = DataImportExport.new(arborist, greenhouse)
import_export.import_greenhouse_data(file_path)
# A request to export entire greenhouse data
func on_greenhouse_req_export_greenhouse_data(file_path: String):
if !is_edited: return
var import_export = DataImportExport.new(arborist, greenhouse)
import_export.export_greenhouse_data(file_path)
#-------------------------------------------------------------------------------
# Painter stroke lifecycle
#-------------------------------------------------------------------------------
func on_painter_stroke_started(brush_data:Dictionary):
var active_brush = toolshed.active_brush
arborist.on_stroke_started(active_brush, greenhouse.greenhouse_plant_states)
func on_painter_stroke_finished(brush_data:Dictionary):
arborist.on_stroke_finished()
func on_painter_stroke_updated(brush_data:Dictionary):
arborist.on_stroke_updated(brush_data)
#-------------------------------------------------------------------------------
# Painter - Toolshed relations
#-------------------------------------------------------------------------------
# Changed active brush from Toolshed. Update the painter
func on_toolshed_prop_action_executed(prop_action:PropAction, final_val):
assert(painter)
if prop_action.prop != "brush/active_brush": return
if !(is_instance_of(prop_action, PA_PropSet)) && !(is_instance_of(prop_action, PA_PropEdit)): return
if final_val != toolshed.active_brush:
logger.error("Passed final_val is not equal to toolshed.active_brush!")
return
painter_update_to_active_brush(final_val)
func painter_update_to_active_brush(active_brush):
assert(active_brush)
painter.queue_call_when_camera('update_all_props_to_active_brush', [active_brush])
#-------------------------------------------------------------------------------
# Quick edit for brush properties
#-------------------------------------------------------------------------------
# Property change instigated by Painter
func on_changed_active_brush_prop(prop: String, val, final:bool):
var prop_action: PropAction = null
if final:
prop_action = PA_PropSet.new(prop, val)
else:
prop_action = PA_PropEdit.new(prop, val)
if prop_action:
toolshed.active_brush.request_prop_action(prop_action)
# Propagate active_brush property changes to Painter
func on_toolshed_prop_action_executed_on_brush(prop_action:PropAction, final_val, brush):
assert(painter)
if !(is_instance_of(prop_action, PA_PropSet)) && !(is_instance_of(prop_action, PA_PropEdit)): return
if brush != toolshed.active_brush: return
match prop_action.prop:
"shape/shape_volume_size":
painter.set_active_brush_size(final_val)
"shape/shape_projection_size":
painter.set_active_brush_size(final_val)
"behavior/behavior_strength":
painter.set_active_brush_strength(final_val)
"behavior/behavior_overlap_mode":
painter_update_to_active_brush(brush)
#-------------------------------------------------------------------------------
# Saving, loading and file management
#-------------------------------------------------------------------------------
func save_toolshed():
if FunLib.is_dir_valid(garden_work_directory):
FunLib.save_res(toolshed, garden_work_directory, "toolshed.tres")
func save_greenhouse():
if FunLib.is_dir_valid(garden_work_directory):
FunLib.save_res(greenhouse, garden_work_directory, "greenhouse.tres")
#-------------------------------------------------------------------------------
# Property export
#-------------------------------------------------------------------------------
# Writing this by hand THRICE for each property is honestly tiring
# Built-in Godot reflection would go a long way
func _get(property):
match property:
"file_management/garden_work_directory":
return garden_work_directory
"gardening/gardening_collision_mask":
return gardening_collision_mask
"plugin_version":
return
"storage_version":
return storage_version
func _set(property, val):
var return_val = true
match property:
"file_management/garden_work_directory":
set_garden_work_directory(val)
"gardening/gardening_collision_mask":
set_gardening_collision_mask(val)
_:
return_val = false
return return_val
func _get_property_list():
return [
{
"name": "file_management/garden_work_directory",
"type": TYPE_STRING,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_DIR
},
{
"name": "gardening/gardening_collision_mask",
"type": TYPE_INT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_LAYERS_3D_PHYSICS
},
{
"name": "plugin_version",
"type": TYPE_STRING,
"usage": PROPERTY_USAGE_NO_EDITOR,
},
{
"name": "storage_version",
"type": TYPE_STRING,
"usage": PROPERTY_USAGE_NO_EDITOR,
},
]
# Warning to be displayed in editor SceneTree
func _get_configuration_warnings():
var arborist_check = get_node("Arborist")
if arborist_check && is_instance_of(arborist_check, Arborist):
return ""
else:
return "Gardener is missing a valid Arborist child\nSince it should be created automatically, try reloading a scene or recreating a Gardener"
func set_refresh_octree_shared_LOD_variants(val):
refresh_octree_shared_LOD_variants = false
if val && arborist && greenhouse:
for i in range(0, greenhouse.greenhouse_plant_states.size()):
arborist.refresh_octree_shared_LOD_variants(i, greenhouse.greenhouse_plant_states[i].plant.mesh_LOD_variants)

View File

@@ -0,0 +1,534 @@
@tool
extends RefCounted
#-------------------------------------------------------------------------------
# Handles keeping track of brush strokes, brush position and some of the brush settings
# Also notifies others of painting lifecycle updates
#-------------------------------------------------------------------------------
const FunLib = preload("../utility/fun_lib.gd")
const DponDebugDraw = preload("../utility/debug_draw.gd")
const Toolshed_Brush = preload("../toolshed/toolshed_brush.gd")
const Globals = preload("../utility/globals.gd")
enum ModifierKeyboardKey {KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_TAB}
enum BrushPrimaryKeyboardKey {MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_XBUTTON1, MOUSE_BUTTON_XBUTTON2}
enum BrushPropEditFlag {MODIFIER, NONE, SIZE, STRENGTH}
var owned_spatial:Node3D = null
# Used for immediate updates when changes happen to the brush
# This should NOT be used in update() or each frame in general
var _cached_camera: Camera3D = null
const sphere_brush_material = preload("../shaders/shm_sphere_brush.tres")
const circle_brush_material = preload("../shaders/shm_circle_brush.tres")
var paint_brush_node:MeshInstance3D = null
var detached_paint_brush_container:Node = null
# Temporary variables to store current quick prop edit state
var brush_prop_edit_flag = BrushPropEditFlag.NONE
const brush_prop_edit_max_dist:float = 500.0
var brush_prop_edit_max_val:float = 0.0
var brush_prop_edit_cur_val:float = 0.0
var brush_prop_edit_start_pos:Vector2 = Vector2.ZERO
var brush_prop_edit_offset:float = 0.0
var can_draw:bool = false
var is_drawing:bool = false
var pending_movement_update:bool = false
var brush_collision_mask:int : set = set_brush_collision_mask
# Used to pass during stroke-state signals sent to Gardener/Arborist
# Meant to avoid retrieving transform from an actual 3D node
# And more importantly to cache a raycast normal at every given point in time
var active_brush_data:Dictionary = {'brush_pos': Vector3.ZERO, 'brush_normal': Vector3.UP, 'brush_basis': Basis()}
# Variables to sync quick brush property edit with UI and vice-versa
# And also for keeping brush state up-to-date without needing a reference to actual active brush
var active_brush_overlap_mode: int = Toolshed_Brush.OverlapMode.VOLUME
var active_brush_size:float : set = set_active_brush_size
var active_brush_strength:float : set = set_active_brush_strength
var active_brush_max_size:float : set = set_active_brush_max_size
var active_brush_max_strength:float : set = set_active_brush_max_strength
# A queue of methods to be called once _cached_camera becomes available
var when_camera_queue: Array = []
# Ooooh boy
# Go to finish_brush_prop_edit() for explanation
var mouse_move_call_delay: int = 0
signal changed_active_brush_prop(prop, val, final)
signal stroke_started
signal stroke_finished
signal stroke_updated
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
# When working with this object, we assume it does not exist outside the editor
func _init(_owned_spatial):
set_meta("class", "Painter")
owned_spatial = _owned_spatial
#
paint_brush_node = MeshInstance3D.new()
paint_brush_node.name = "active_brush"
set_brush_mesh()
#owned_spatial.add_child(paint_brush_node)
detached_paint_brush_container = Node.new()
owned_spatial.add_child(detached_paint_brush_container)
detached_paint_brush_container.add_child(paint_brush_node)
set_can_draw(false)
func update(delta):
if _cached_camera:
# Handle queue of methods that need a _cached_camera
for queue_item in when_camera_queue.duplicate():
callv(queue_item.method_name, queue_item.args)
when_camera_queue.erase(queue_item)
consume_brush_drawing_update(delta)
func set_brush_mesh(is_sphere: bool = false):
if is_sphere:
paint_brush_node.mesh = SphereMesh.new()
paint_brush_node.mesh.radial_segments = 32
paint_brush_node.mesh.rings = 16
paint_brush_node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
paint_brush_node.material_override = sphere_brush_material.duplicate()
else:
paint_brush_node.mesh = QuadMesh.new()
paint_brush_node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
paint_brush_node.material_override = circle_brush_material.duplicate()
# Queue a call to method that needs a _cached_camera to be set
func queue_call_when_camera(method_name: String, args: Array = []):
when_camera_queue.append({'method_name': method_name, 'args': args})
#-------------------------------------------------------------------------------
# Editing lifecycle
#-------------------------------------------------------------------------------
func start_editing():
set_can_draw(true)
func stop_editing():
stop_brush_stroke()
set_can_draw(false)
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
func forwarded_input(camera:Camera3D, event):
if !can_draw: return
_cached_camera = camera
var handled = false
# If inactive property edit
# And event == mouseMotion
# -> move the brush
if brush_prop_edit_flag <= BrushPropEditFlag.NONE:
if (is_instance_of(event, InputEventMouseMotion)
|| (is_instance_of(event, InputEventMouseButton) && event.button_index == MOUSE_BUTTON_WHEEL_UP)
|| (is_instance_of(event, InputEventMouseButton) && event.button_index == MOUSE_BUTTON_WHEEL_DOWN)):
if mouse_move_call_delay > 0:
mouse_move_call_delay -= 1
else:
move_brush()
pending_movement_update = true
# Don't handle input - moving a brush is not destructive
# If inactive property edit
# And event == overlap mode key
# -> cycle overlap modes
if brush_prop_edit_flag <= BrushPropEditFlag.NONE && is_instance_of(event, InputEventKey) && event.keycode == get_overlap_mode_key():
if event.pressed && !event.is_echo():
cycle_overlap_modes()
handled = true
# If inactive property edit/modifier key pressed
# And event == modifier key pressed
# -> remember/forget the modifier
if brush_prop_edit_flag <= BrushPropEditFlag.NONE && is_instance_of(event, InputEventKey) && event.keycode == get_property_edit_modifier():
if event.pressed:
brush_prop_edit_flag = BrushPropEditFlag.MODIFIER
if !event.pressed:
brush_prop_edit_flag = BrushPropEditFlag.NONE
handled = true
# If inactive property edit or modifier key pressed
# And event == property edit trigger pressed
# -> start property edit
if brush_prop_edit_flag <= BrushPropEditFlag.NONE && is_instance_of(event, InputEventMouseButton) && event.button_index == get_property_edit_button():
if event.pressed:
brush_prop_edit_flag = BrushPropEditFlag.SIZE if brush_prop_edit_flag != BrushPropEditFlag.MODIFIER else BrushPropEditFlag.STRENGTH
start_brush_prop_edit(event.global_position)
handled = true
# If editing property
# And event == property edit trigger released
# -> stop property edit
if brush_prop_edit_flag > BrushPropEditFlag.NONE && is_instance_of(event, InputEventMouseButton) && event.button_index == get_property_edit_button():
if !event.pressed:
finish_brush_prop_edit(camera)
brush_prop_edit_flag = BrushPropEditFlag.NONE
handled = true
# If editing property
# And event == mouseMotion
# -> update property value
if brush_prop_edit_flag > BrushPropEditFlag.NONE && is_instance_of(event, InputEventMouseMotion):
brush_prop_edit_calc_val(event.global_position)
handled = true
# If editing property
# And event == paint trigger pressed/releasedq
# -> start/stop the brush stroke
if brush_prop_edit_flag == BrushPropEditFlag.NONE && is_instance_of(event, InputEventMouseButton) && event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
move_brush()
start_brush_stroke()
else:
stop_brush_stroke()
handled = true
return handled
func get_property_edit_modifier():
# This convolution exists because a project setting with default value is not saved for some reason and load as "null"
# See https://github.com/godotengine/godot/issues/56598
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_prop_edit_modifier", Globals.KeyboardKey.KEY_SHIFT)
return Globals.index_to_enum(key, Globals.KeyboardKey)
func get_property_edit_button():
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_prop_edit_button", Globals.MouseButton.MOUSE_BUTTON_XBUTTON1)
return Globals.index_to_enum(key, Globals.MouseButton)
func get_overlap_mode_key():
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_overlap_mode_button", Globals.KeyboardKey.KEY_QUOTELEFT)
return Globals.index_to_enum(key, Globals.KeyboardKey)
#-------------------------------------------------------------------------------
# Painting lifecycle
#-------------------------------------------------------------------------------
func set_can_draw(state):
can_draw = state
if state:
paint_brush_node.visible = true
else:
paint_brush_node.visible = false
func start_brush_stroke():
if is_drawing: return
is_drawing = true
stroke_started.emit(active_brush_data)
func stop_brush_stroke():
if !is_drawing: return
is_drawing = false
active_brush_data = {'brush_pos': Vector3.ZERO, 'brush_normal': Vector3.UP, 'brush_basis': Basis()}
stroke_finished.emit(active_brush_data)
# Actually update the stroke only if it was preceeded by the input event
func consume_brush_drawing_update(delta):
if !can_draw: return
if !is_drawing: return
if !pending_movement_update: return
pending_movement_update = false
stroke_updated.emit(active_brush_data)
#-------------------------------------------------------------------------------
# Brush movement
#-------------------------------------------------------------------------------
func move_brush():
if !_cached_camera: return
update_active_brush_data()
refresh_brush_transform()
# Update brush data that is passed through signals to Gardener/Arborist
# Raycast overrides exist for compatability with gardener tests
func update_active_brush_data(raycast_overrides: Dictionary = {}):
var space_state = paint_brush_node.get_world_3d().direct_space_state
var start = project_mouse_near() if !raycast_overrides.has('start') else raycast_overrides.start
var end = project_mouse_far() if !raycast_overrides.has('end') else raycast_overrides.end
var params = PhysicsRayQueryParameters3D.create(start, end, brush_collision_mask, [])
var ray_result:Dictionary = space_state.intersect_ray(params)
if !ray_result.is_empty():
active_brush_data.brush_pos = ray_result.position
active_brush_data.brush_normal = ray_result.normal
else:
# If raycast failed - align to camera plane, retaining current distance to camera
var camera_normal = -_cached_camera.global_transform.basis.z
var planar_dist_to_camera = (active_brush_data.brush_pos - _cached_camera.global_transform.origin).dot(camera_normal)
var brush_pos:Vector3 = project_mouse(planar_dist_to_camera)
active_brush_data.brush_pos = brush_pos
# It's possible we don't have _cached_camera defined here since
# Gardener tests might call update_active_brush_data() without setting it
if _cached_camera:
# Cache to use with Projection brush
active_brush_data.brush_basis = _cached_camera.global_transform.basis
# Update transform of a paint brush 3D node
func refresh_brush_transform():
if active_brush_data.is_empty(): return
match active_brush_overlap_mode:
Toolshed_Brush.OverlapMode.VOLUME:
paint_brush_node.global_transform.origin = active_brush_data.brush_pos
paint_brush_node.global_transform.basis = Basis()
Toolshed_Brush.OverlapMode.PROJECTION:
paint_brush_node.global_transform.origin = active_brush_data.brush_pos
paint_brush_node.global_transform.basis = active_brush_data.brush_basis
# Projection brush size is in viewport-space, but it will move forward and backward
# Thus appearing smaller or bigger
# So we need to update it's size to keep it consistent
set_brush_diameter(active_brush_size)
#-------------------------------------------------------------------------------
# Brush quick property edit lifecycle
#-------------------------------------------------------------------------------
# Quickly edit a brush property without using the UI (aka like in Blender)
# The flow here is as follows:
# 1. Respond to mouse events, calculate property value, emit a signal
# 2. Signal is received in the Gardener, passed to an active Toolshed_Brush
# 3. Active brush updates it's values
# 4. Toolshed notifies Painter of a value change
# 5. Painter updates it's helper variables and visual representation
# Switching between Volume/Projection brush is here too, but it's not connected to the whole Blender-like process
# It's just a hotkey handling
# Set the initial value of edited property and mouse offset
func start_brush_prop_edit(mouse_pos):
match brush_prop_edit_flag:
BrushPropEditFlag.SIZE:
brush_prop_edit_cur_val = active_brush_size
brush_prop_edit_max_val = active_brush_max_size
BrushPropEditFlag.STRENGTH:
brush_prop_edit_cur_val = active_brush_strength
brush_prop_edit_max_val = active_brush_max_strength
brush_prop_edit_start_pos = mouse_pos
brush_prop_edit_offset = brush_prop_edit_cur_val / brush_prop_edit_max_val * brush_prop_edit_max_dist
# Calculate edited property value based on mouse offset
func brush_prop_edit_calc_val(mouse_pos):
brush_prop_edit_cur_val = clamp((mouse_pos.x - brush_prop_edit_start_pos.x + brush_prop_edit_offset) / brush_prop_edit_max_dist, 0.0, 1.0) * brush_prop_edit_max_val
match active_brush_overlap_mode:
Toolshed_Brush.OverlapMode.VOLUME:
match brush_prop_edit_flag:
BrushPropEditFlag.SIZE:
changed_active_brush_prop.emit("shape/shape_volume_size", brush_prop_edit_cur_val, false)
BrushPropEditFlag.STRENGTH:
changed_active_brush_prop.emit("behavior/behavior_strength", brush_prop_edit_cur_val, false)
Toolshed_Brush.OverlapMode.PROJECTION:
match brush_prop_edit_flag:
BrushPropEditFlag.SIZE:
changed_active_brush_prop.emit("shape/shape_projection_size", brush_prop_edit_cur_val, false)
# Stop editing brush property and reset helper variables and mouse position
func finish_brush_prop_edit(camera:Camera3D):
match active_brush_overlap_mode:
Toolshed_Brush.OverlapMode.VOLUME:
match brush_prop_edit_flag:
BrushPropEditFlag.SIZE:
changed_active_brush_prop.emit("shape/shape_volume_size", brush_prop_edit_cur_val, true)
BrushPropEditFlag.STRENGTH:
changed_active_brush_prop.emit("behavior/behavior_strength", brush_prop_edit_cur_val, true)
Toolshed_Brush.OverlapMode.PROJECTION:
match brush_prop_edit_flag:
BrushPropEditFlag.SIZE:
changed_active_brush_prop.emit("shape/shape_projection_size", brush_prop_edit_cur_val, true)
Input.warp_mouse(brush_prop_edit_start_pos)
brush_prop_edit_flag = BrushPropEditFlag.NONE
brush_prop_edit_start_pos = Vector2.ZERO
brush_prop_edit_max_val = 0.0
brush_prop_edit_cur_val = 0.0
# Apparently warp_mouse() sometimes takes a few mouse motion events to actually take place
# Sometimes it's instant, sometimes it takes 1, and sometimes 2 events (at least on my machine)
# This leads to brush jumping to position used in prop edit and then back. Like it's on a string
# As an workaround, we delay processing motion input for 2 events (which should be enough for 99% of cases?)
mouse_move_call_delay = 2
# Cycle between brush overlap modes on a button press
func cycle_overlap_modes():
active_brush_overlap_mode += 1
if active_brush_overlap_mode > Toolshed_Brush.OverlapMode.PROJECTION:
active_brush_overlap_mode = Toolshed_Brush.OverlapMode.VOLUME
changed_active_brush_prop.emit("behavior/behavior_overlap_mode", active_brush_overlap_mode, true)
#-------------------------------------------------------------------------------
# Setters for brush parameters meant to be accessed from outside
# In response to UI inputs
#-------------------------------------------------------------------------------
func update_all_props_to_active_brush(brush: Toolshed_Brush):
var max_size = 1.0
var max_strength = 1.0
var curr_size = 1.0
var curr_strength = brush.behavior_strength
match brush.behavior_overlap_mode:
Toolshed_Brush.OverlapMode.VOLUME:
max_size = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_volume_size_slider_max_value", 100.0)
curr_size = brush.shape_volume_size
Toolshed_Brush.OverlapMode.PROJECTION:
max_size = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_projection_size_slider_max_value", 1000.0)
curr_size = brush.shape_projection_size
set_active_brush_overlap_mode(brush.behavior_overlap_mode)
set_active_brush_max_size(max_size)
set_active_brush_max_strength(max_strength)
set_active_brush_size(curr_size)
set_active_brush_strength(curr_strength)
# Update helper variables and visuals
func set_active_brush_size(val):
active_brush_size = val
paint_brush_node.material_override.set_shader_parameter("proximity_multiplier", active_brush_size * 0.5)
queue_call_when_camera('set_brush_diameter', [active_brush_size])
# Update helper variables and visuals
func set_active_brush_max_size(val):
active_brush_max_size = val
queue_call_when_camera('set_brush_diameter', [active_brush_size])
# Update helper variables
func set_active_brush_strength(val):
active_brush_strength = val
# Update helper variables
func set_active_brush_max_strength(val):
active_brush_max_strength = val
# Update visuals
func set_brush_diameter(diameter: float):
match active_brush_overlap_mode:
Toolshed_Brush.OverlapMode.VOLUME:
paint_brush_node.mesh.radius = diameter * 0.5
paint_brush_node.mesh.height = diameter
Toolshed_Brush.OverlapMode.PROJECTION:
var camera_normal = -_cached_camera.global_transform.basis.z
var planar_dist_to_camera = (active_brush_data.brush_pos - _cached_camera.global_transform.origin).dot(camera_normal)
var circle_center:Vector3 = active_brush_data.brush_pos
var circle_edge:Vector3
# If we're editing props (or just finished it as indicated by 'mouse_move_call_delay')
# Then to prevent size doubling/overflow use out brush position as mouse position
# (Since out mouse WILL be offset due to us dragging it to the side)
if brush_prop_edit_flag > BrushPropEditFlag.NONE || mouse_move_call_delay > 0:
var screen_space_brush_pos = _cached_camera.unproject_position(active_brush_data.brush_pos)
circle_edge = _cached_camera.project_position(screen_space_brush_pos + Vector2(diameter * 0.5, 0), planar_dist_to_camera)
else:
circle_edge = project_mouse(planar_dist_to_camera, Vector2(diameter * 0.5, 0))
var size = (circle_edge - circle_center).length()
paint_brush_node.mesh.size = Vector2(size, size) * 2.0
func set_brush_collision_mask(val):
brush_collision_mask = val
# Update helper variables and visuals
func set_active_brush_overlap_mode(val):
active_brush_overlap_mode = val
match active_brush_overlap_mode:
Toolshed_Brush.OverlapMode.VOLUME:
set_brush_mesh(true)
Toolshed_Brush.OverlapMode.PROJECTION:
set_brush_mesh(false)
# Since we are rebuilding the mesh here
# It means that we need to move it in a proper position as well
move_brush()
#-------------------------------------------------------------------------------
# Camera3D/raycasting methods
#-------------------------------------------------------------------------------
func project_mouse_near() -> Vector3:
return project_mouse(_cached_camera.near)
func project_mouse_far() -> Vector3:
return project_mouse(_cached_camera.far - 0.1)
func project_mouse(distance: float, offset: Vector2 = Vector2.ZERO) -> Vector3:
return _cached_camera.project_position(_cached_camera.get_viewport().get_mouse_position() + offset, distance)

View File

@@ -0,0 +1,390 @@
@tool
extends "../utility/input_field_resource/input_field_resource.gd"
#-------------------------------------------------------------------------------
# The manager of all plant types for a given Gardener
# Handles interfacing between Greenhouse_PlantState, UI and plant placement
#-------------------------------------------------------------------------------
const Greenhouse_PlantState = preload("greenhouse_plant_state.gd")
const ui_category_greenhouse_SCN = preload("../controls/side_panel/ui_category_greenhouse.tscn")
# All the plants (plant states) we have
var greenhouse_plant_states:Array = []
# Keep a reference to selected resource to easily display it
var selected_for_edit_resource:Resource = null
var ui_category_greenhouse: Control = null
var scroll_container_plant_thumbnails_nd:Control = null
var scroll_container_properties_nd: Control = null
var panel_container_category_nd:Control = null
var grid_container_plant_thumbnails_nd:UI_IF_ThumbnailArray = null
var vbox_container_properties_nd:Control = null
var _base_control:Control = null
var _resource_previewer = null
var _file_dialog: ConfirmationDialog = null
signal prop_action_executed_on_plant_state(prop_action, final_val, plant_state)
signal prop_action_executed_on_plant_state_plant(prop_action, final_val, plant, plant_state)
signal prop_action_executed_on_LOD_variant(prop_action, final_val, LOD_variant, plant, plant_stat)
signal req_octree_reconfigure(plant, plant_state)
signal req_octree_recenter(plant, plant_state)
signal req_import_plant_data(plant_idx, file)
signal req_export_plant_data(plant_idx, file)
signal req_import_greenhouse_data(file)
signal req_export_greenhouse_data(file)
#-------------------------------------------------------------------------------
# Initialization
#-------------------------------------------------------------------------------
func _init():
super()
set_meta("class", "Greenhouse")
resource_name = "Greenhouse"
_add_res_edit_source_array("plant_types/greenhouse_plant_states", "plant_types/selected_for_edit_resource")
if Engine.is_editor_hint():
# Editor raises error everytime you run the game with F5 because of "abstract native class"
# https://github.com/godotengine/godot/issues/73525
_file_dialog = DPON_FM.ED_EditorFileDialog.new()
else:
_file_dialog = FileDialog.new()
_file_dialog.close_requested.connect(on_file_dialog_hide)
func _notification(what):
match what:
NOTIFICATION_PREDELETE:
if is_instance_valid(_file_dialog):
# Avoid memory leaks
_file_dialog.queue_free()
# The UI is created here because we need to manage it afterwards
# And I see no reason to get lost in a signal spaghetti of delegating it
func create_ui(__base_control:Control, __resource_previewer):
_base_control = __base_control
_resource_previewer = __resource_previewer
# Avoid memory leaks
if is_instance_valid(ui_category_greenhouse):
ui_category_greenhouse.queue_free()
if is_instance_valid(grid_container_plant_thumbnails_nd):
grid_container_plant_thumbnails_nd.queue_free()
if is_instance_valid(vbox_container_properties_nd):
vbox_container_properties_nd.queue_free()
ui_category_greenhouse = ui_category_greenhouse_SCN.instantiate()
scroll_container_plant_thumbnails_nd = ui_category_greenhouse.find_child('ScrollContainer_PlantThumbnails')
scroll_container_properties_nd = ui_category_greenhouse.find_child('ScrollContainer_Properties')
panel_container_category_nd = ui_category_greenhouse.find_child('Label_Category_Plants')
panel_container_category_nd.theme_type_variation = "PropertyCategory"
scroll_container_plant_thumbnails_nd.theme_type_variation = "InspectorPanelContainer"
scroll_container_properties_nd.theme_type_variation = "InspectorPanelContainer"
ui_category_greenhouse.theme_type_variation = "InspectorPanelContainer"
grid_container_plant_thumbnails_nd = create_input_field(_base_control, _resource_previewer, "plant_types/greenhouse_plant_states")
grid_container_plant_thumbnails_nd.label.visible = false
grid_container_plant_thumbnails_nd.name = "GridContainer_PlantThumbnails"
grid_container_plant_thumbnails_nd.size_flags_vertical = Control.SIZE_EXPAND_FILL
grid_container_plant_thumbnails_nd.size_flags_horizontal = Control.SIZE_EXPAND_FILL
grid_container_plant_thumbnails_nd.requested_check.connect(on_plant_state_check)
grid_container_plant_thumbnails_nd.requested_label_edit.connect(on_plant_label_edit)
vbox_container_properties_nd = create_input_field(_base_control, _resource_previewer, "plant_types/selected_for_edit_resource")
scroll_container_plant_thumbnails_nd.add_child(grid_container_plant_thumbnails_nd)
scroll_container_properties_nd.add_child(vbox_container_properties_nd)
return ui_category_greenhouse
func _create_input_field(_base_control:Control, _resource_previewer, prop:String) -> UI_InputField:
var input_field:UI_InputField = null
match prop:
"plant_types/greenhouse_plant_states":
var settings := {
"add_create_inst_button": true,
"accepted_classes": [Greenhouse_PlantState],
"element_display_size": 100 * FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/greenhouse_thumbnail_scale", 1.0),
"element_interaction_flags": UI_IF_ThumbnailArray.PRESET_PLANT_STATE,
}
input_field = UI_IF_ThumbnailArray.new(greenhouse_plant_states, "Plant Types", prop, settings)
"plant_types/selected_for_edit_resource":
var settings := {
"label_visibility": false,
"tab": 0}
input_field = UI_IF_Object.new(selected_for_edit_resource, "Plant State", prop, settings)
return input_field
func add_plant_from_dict(plant_data: Dictionary, str_version: int = 1) -> int:
var new_idx = greenhouse_plant_states.size()
request_prop_action(PA_ArrayInsert.new(
"plant_types/greenhouse_plant_states",
Greenhouse_PlantState.new().ifr_from_dict(plant_data, true, str_version),
new_idx
))
return new_idx
#-------------------------------------------------------------------------------
# UI management
#-------------------------------------------------------------------------------
# Select a Greenhouse_PlantState for painting
func on_plant_state_check(index:int, state:bool):
var plant_state = greenhouse_plant_states[index]
var prop_action = PA_PropSet.new("plant/plant_brush_active", state)
plant_state.request_prop_action(prop_action)
# Edit Greenhouse_PlantState's label
func on_plant_label_edit(index:int, label_text:String):
var plant_state = greenhouse_plant_states[index]
var prop_action = PA_PropSet.new("plant/plant_label", label_text)
plant_state.request_prop_action(prop_action)
func select_plant_state_for_brush(index:int, state:bool):
if is_instance_valid(grid_container_plant_thumbnails_nd):
grid_container_plant_thumbnails_nd.set_thumb_interaction_feature_with_data(UI_ActionThumbnail_GD.InteractionFlags.CHECK, state, {"index": index})
func set_plant_state_label(index:int, label_text:String):
if is_instance_valid(grid_container_plant_thumbnails_nd):
grid_container_plant_thumbnails_nd.set_thumb_interaction_feature_with_data(UI_ActionThumbnail_GD.InteractionFlags.EDIT_LABEL, label_text, {"index": index})
func on_if_tree_entered(input_field:UI_InputField):
super.on_if_tree_entered(input_field)
if input_field.prop_name == "plant_types/greenhouse_plant_states":
for i in range(0, greenhouse_plant_states.size()):
select_plant_state_for_brush(i, greenhouse_plant_states[i].plant_brush_active)
set_plant_state_label(i, greenhouse_plant_states[i].plant_label)
func plant_count_updated(plant_index, new_count):
if is_instance_valid(grid_container_plant_thumbnails_nd) && grid_container_plant_thumbnails_nd.flex_grid.get_child_count() > plant_index:
grid_container_plant_thumbnails_nd.flex_grid.get_child(plant_index).set_counter_val(new_count)
func show_transform_import(type: String):
if _file_dialog.get_parent() != _base_control:
_base_control.add_child(_file_dialog)
_file_dialog.popup_centered_ratio(0.5)
_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
_file_dialog.filters = PackedStringArray(['*.json ; JSON'])
match type:
'import':
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
'export':
_file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
func on_file_dialog_hide():
FunLib.disconnect_all(_file_dialog.file_selected)
#-------------------------------------------------------------------------------
# Signal forwarding
#-------------------------------------------------------------------------------
func on_changed_plant_state():
emit_changed()
func on_req_octree_reconfigure(plant, plant_state):
req_octree_reconfigure.emit(plant, plant_state)
func on_req_octree_recenter(plant, plant_state):
req_octree_recenter.emit(plant, plant_state)
func on_req_import_plant_data(plant, plant_state):
show_transform_import('import')
var plant_idx = greenhouse_plant_states.find(plant_state)
FunLib.disconnect_all(_file_dialog.file_selected)
_file_dialog.file_selected.connect(on_req_import_export_plant_data_file.bind(req_import_plant_data, plant_idx))
func on_req_export_plant_data(plant, plant_state):
show_transform_import('export')
var plant_idx = greenhouse_plant_states.find(plant_state)
FunLib.disconnect_all(_file_dialog.file_selected)
_file_dialog.file_selected.connect(on_req_import_export_plant_data_file.bind(req_export_plant_data, plant_idx))
func on_req_import_greenhouse_data():
show_transform_import('import')
FunLib.disconnect_all(_file_dialog.file_selected)
_file_dialog.file_selected.connect(on_req_import_export_greenhouse_data_file.bind(req_import_greenhouse_data))
func on_req_export_greenhouse_data():
show_transform_import('export')
FunLib.disconnect_all(_file_dialog.file_selected)
_file_dialog.file_selected.connect(on_req_import_export_greenhouse_data_file.bind(req_export_greenhouse_data))
func on_req_import_export_plant_data_file(file_path: String, signal_obj: Signal, plant_idx: int):
signal_obj.emit(file_path, plant_idx)
func on_req_import_export_greenhouse_data_file(file_path: String, signal_obj: Signal):
signal_obj.emit(file_path)
#-------------------------------------------------------------------------------
# Prop Actions
#-------------------------------------------------------------------------------
func on_prop_action_executed(prop_action:PropAction, final_val):
var prop_action_class = prop_action.get_meta("class")
match prop_action.prop:
"plant_types/greenhouse_plant_states":
match prop_action_class:
"PA_ArrayInsert":
select_plant_state_for_brush(prop_action.index, final_val[prop_action.index].plant_brush_active)
set_plant_state_label(prop_action.index, final_val[prop_action.index].plant_label)
func on_prop_action_executed_on_plant_state(prop_action, final_val, plant_state):
if is_instance_of(prop_action, PA_PropSet):
var plant_index = greenhouse_plant_states.find(plant_state)
match prop_action.prop:
"plant/plant_brush_active":
select_plant_state_for_brush(plant_index, final_val)
"plant/plant_label":
set_plant_state_label(plant_index, final_val)
prop_action_executed_on_plant_state.emit(prop_action, final_val, plant_state)
func on_prop_action_executed_on_plant_state_plant(prop_action, final_val, plant, plant_state):
var plant_index = greenhouse_plant_states.find(plant_state)
# Any prop action on LOD variants - update thumbnail
var update_thumbnail = prop_action.prop == "mesh/mesh_LOD_variants"
if update_thumbnail && grid_container_plant_thumbnails_nd:
grid_container_plant_thumbnails_nd._update_thumbnail(plant_state, plant_index)
prop_action_executed_on_plant_state_plant.emit(prop_action, final_val, plant, plant_state)
func on_prop_action_executed_on_LOD_variant(prop_action, final_val, LOD_variant, plant, plant_state):
prop_action_executed_on_LOD_variant.emit(prop_action, final_val, LOD_variant, plant, plant_state)
#-------------------------------------------------------------------------------
# Property export
#-------------------------------------------------------------------------------
func set_undo_redo(val):
super.set_undo_redo(val)
for plant_state in greenhouse_plant_states:
plant_state.set_undo_redo(_undo_redo)
func _get(prop):
match prop:
"plant_types/greenhouse_plant_states":
return greenhouse_plant_states
"plant_types/selected_for_edit_resource":
return selected_for_edit_resource
return null
func _modify_prop(prop:String, val):
match prop:
"plant_types/greenhouse_plant_states":
for i in range(0, val.size()):
if !is_instance_of(val[i], Greenhouse_PlantState):
val[i] = Greenhouse_PlantState.new()
FunLib.ensure_signal(val[i].changed, on_changed_plant_state)
FunLib.ensure_signal(val[i].prop_action_executed, on_prop_action_executed_on_plant_state, [val[i]])
FunLib.ensure_signal(val[i].prop_action_executed_on_plant, on_prop_action_executed_on_plant_state_plant, [val[i]])
FunLib.ensure_signal(val[i].prop_action_executed_on_LOD_variant, on_prop_action_executed_on_LOD_variant, [val[i]])
FunLib.ensure_signal(val[i].req_octree_reconfigure, on_req_octree_reconfigure, [val[i]])
FunLib.ensure_signal(val[i].req_octree_recenter, on_req_octree_recenter, [val[i]])
FunLib.ensure_signal(val[i].req_import_plant_data, on_req_import_plant_data, [val[i]])
FunLib.ensure_signal(val[i].req_export_plant_data, on_req_export_plant_data, [val[i]])
FunLib.ensure_signal(val[i].req_import_greenhouse_data, on_req_import_greenhouse_data)
FunLib.ensure_signal(val[i].req_export_greenhouse_data, on_req_export_greenhouse_data)
if val[i]._undo_redo != _undo_redo:
val[i].set_undo_redo(_undo_redo)
return val
func _set(prop, val):
var return_val = true
val = _modify_prop(prop, val)
match prop:
"plant_types/greenhouse_plant_states":
greenhouse_plant_states = val
"plant_types/selected_for_edit_resource":
selected_for_edit_resource = val
_:
return_val = false
if return_val:
emit_changed()
return return_val
func _get_prop_dictionary():
return {
"plant_types/greenhouse_plant_states":
{
"name": "plant_types/greenhouse_plant_states",
"type": TYPE_ARRAY,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE
},
"plant_types/selected_for_edit_resource":
{
"name": "plant_types/selected_for_edit_resource",
"type": TYPE_OBJECT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE
},
}
func _fix_duplicate_signals(copy):
copy._modify_prop("plant_types/greenhouse_plant_states", copy.greenhouse_plant_states)
copy.selected_for_edit_resource = null
func get_prop_tooltip(prop:String) -> String:
match prop:
"plant_types/greenhouse_plant_states":
return "All the plants in this Greenhouse"
"plant_types/selected_for_edit_resource":
return "The plant currently selected for edit"
return ""

View File

@@ -0,0 +1,140 @@
@tool
extends "../utility/input_field_resource/input_field_resource.gd"
#-------------------------------------------------------------------------------
# A storage object for meshes to be shown as plants
# And spatials to be spawned at their position (typically a StaticBody3D)
#-------------------------------------------------------------------------------
var Globals = preload("../utility/globals.gd")
var mesh:Mesh = null
var spawned_spatial:PackedScene = null
# Toggle for shadow casting mode on multimeshes
var cast_shadow:int = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
#-------------------------------------------------------------------------------
# Initialization
#-------------------------------------------------------------------------------
func _init(__mesh:Mesh = null, __spawned_spatial:PackedScene = null):
super()
set_meta("class", "Greenhouse_LODVariant")
resource_name = "Greenhouse_LODVariant"
mesh = __mesh
spawned_spatial = __spawned_spatial
func _create_input_field(_base_control:Control, _resource_previewer, prop:String) -> UI_InputField:
var input_field:UI_InputField = null
match prop:
"mesh":
var settings := {
"accepted_classes": [Mesh],
"element_display_size": 75 * FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/greenhouse_thumbnail_scale", 1.0),
"element_interaction_flags": UI_IF_ThumbnailArray.PRESET_RESOURCE,
}
input_field = UI_IF_ThumbnailObject.new(mesh, "Mesh", prop, settings)
"spawned_spatial":
var settings := {
"accepted_classes": [PackedScene],
"element_display_size": 75 * FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/greenhouse_thumbnail_scale", 1.0),
"element_interaction_flags": UI_IF_ThumbnailArray.PRESET_RESOURCE,
}
input_field = UI_IF_ThumbnailObject.new(spawned_spatial, "Spawned Node3D", prop, settings)
"cast_shadow":
var settings := {"enum_list": ["Off", "On", "Double-Sided", "Shadows Only"]}
input_field = UI_IF_Enum.new(cast_shadow, "Shadow Casting Mode", prop, settings)
return input_field
#-------------------------------------------------------------------------------
# Property export
#-------------------------------------------------------------------------------
func _set(prop, val):
var return_val = true
val = _modify_prop(prop, val)
match prop:
"mesh":
mesh = val
"spawned_spatial":
spawned_spatial = val
"cast_shadow":
cast_shadow = val
_:
return_val = false
if return_val:
emit_changed()
return return_val
func _get(prop):
match prop:
"mesh":
return mesh
"spawned_spatial":
return spawned_spatial
"cast_shadow":
return cast_shadow
return null
func _get_prop_dictionary():
return {
"mesh" : {
"name": "mesh",
"type": TYPE_OBJECT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE,
},
"spawned_spatial" : {
"name": "spawned_spatial",
"type": TYPE_OBJECT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE,
},
"cast_shadow":
{
"name": "cast_shadow",
"type": TYPE_INT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "Off,On,Double-Sided,Shadows Only"
}
}
func get_prop_tooltip(prop:String) -> String:
match prop:
"mesh":
return "The mesh (.mesh) resource used to display the plant"
"spawned_spatial":
return "The PackedScene (assumed to be Node3D) that spawns alongside the mesh\n" \
+ "They are separate because mesh rendering is optimized using Godot's MultiMesh\n" \
+ "Spawned Spatials are used to define custom behavior (excluding rendering) for each instance, mainly collision\n" \
+ "This should be used sparingly, as thousands of physics bodies will surely approach a limit of what Godot can handle\n" \
+ "\n" \
+ "NOTE: switching LODs with Spawned Spatials can be expensive due to removing and adding hundreds of nodes at once\n" \
+ "But if all your LODs reference the same PackedScene - they will persist across the LOD changes and won't cause any lag spikes\n" \
+ "The alternative would be to optimise yout octrees to contain only a small amount of Spawned Spatials - 10-20 at most\n" \
+ "Then the process of switching LODs will go a lot smoother"
"cast_shadow":
return "Shadow casting mode for this specific LOD\n" \
+ "Disabling shadow casting slightly improves performance and is recommended for higher LODs (those further away)"
return ""

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,194 @@
@tool
extends "../utility/input_field_resource/input_field_resource.gd"
#-------------------------------------------------------------------------------
# A middle-man between the plant and the UI/painting/placement logic
#-------------------------------------------------------------------------------
const Greenhouse_Plant = preload("greenhouse_plant.gd")
var plant_brush_active:bool = false
var plant_label:String = ''
var plant:Greenhouse_Plant = null
signal prop_action_executed_on_plant(prop_action, final_val, plant)
signal prop_action_executed_on_LOD_variant(prop_action, final_val, LOD_variant, plant)
signal req_octree_reconfigure(plant)
signal req_octree_recenter(plant)
signal req_import_plant_data(plant)
signal req_export_plant_data(plant)
signal req_import_greenhouse_data()
signal req_export_greenhouse_data()
#-------------------------------------------------------------------------------
# Initialization
#-------------------------------------------------------------------------------
func _init():
super()
set_meta("class", "Greenhouse_PlantState")
resource_name = "Greenhouse_PlantState"
# A workaround to trigger the initial creation of a plant
_set("plant/plant", plant)
#-------------------------------------------------------------------------------
# Signal forwarding
#-------------------------------------------------------------------------------
func on_changed_plant():
emit_changed()
func on_prop_action_executed_on_plant(prop_action, final_val, plant):
prop_action_executed_on_plant.emit(prop_action, final_val, plant)
func on_req_octree_reconfigure(plant):
req_octree_reconfigure.emit(plant)
func on_req_octree_recenter(plant):
req_octree_recenter.emit(plant)
func on_req_import_plant_data(plant):
req_import_plant_data.emit(plant)
func on_req_export_plant_data(plant):
req_export_plant_data.emit(plant)
func on_req_import_greenhouse_data():
req_import_greenhouse_data.emit()
func on_req_export_greenhouse_data():
req_export_greenhouse_data.emit()
func on_prop_action_executed_on_LOD_variant(prop_action, final_val, LOD_variant, plant):
prop_action_executed_on_LOD_variant.emit(prop_action, final_val, LOD_variant, plant)
#-------------------------------------------------------------------------------
# Property management
#-------------------------------------------------------------------------------
func _modify_prop(prop:String, val):
match prop:
"plant/plant":
if !is_instance_of(val, Greenhouse_Plant):
val = Greenhouse_Plant.new()
FunLib.ensure_signal(val.changed, on_changed_plant)
FunLib.ensure_signal(val.prop_action_executed, on_prop_action_executed_on_plant, [val])
FunLib.ensure_signal(val.prop_action_executed_on_LOD_variant, on_prop_action_executed_on_LOD_variant, [val])
FunLib.ensure_signal(val.req_octree_reconfigure, on_req_octree_reconfigure, [val])
FunLib.ensure_signal(val.req_octree_recenter, on_req_octree_recenter, [val])
FunLib.ensure_signal(val.req_import_plant_data, on_req_import_plant_data, [val])
FunLib.ensure_signal(val.req_export_plant_data, on_req_export_plant_data, [val])
FunLib.ensure_signal(val.req_import_greenhouse_data, on_req_import_greenhouse_data)
FunLib.ensure_signal(val.req_export_greenhouse_data, on_req_export_greenhouse_data)
if val._undo_redo != _undo_redo:
val.set_undo_redo(_undo_redo)
return val
#-------------------------------------------------------------------------------
# Property export
#-------------------------------------------------------------------------------
func set_undo_redo(val):
super.set_undo_redo(val)
plant.set_undo_redo(_undo_redo)
func _get(prop):
match prop:
"plant/plant_brush_active":
return plant_brush_active
"plant/plant_label":
return plant_label
"plant/plant":
return plant
return null
func _set(prop, val):
var return_val = true
val = _modify_prop(prop, val)
match prop:
"plant/plant_brush_active":
plant_brush_active = val
"plant/plant_label":
plant_label = val
"plant/plant":
plant = val
_:
return_val = false
if return_val:
emit_changed()
return return_val
func _get_prop_dictionary():
return {
"plant/plant_brush_active":
{
"name": "plant/plant_brush_active",
"type": TYPE_BOOL,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE
},
"plant/plant_label":
{
"name": "plant/plant_label",
"type": TYPE_STRING,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE
},
"plant/plant":
{
"name": "plant/plant",
"type": TYPE_OBJECT ,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_RESOURCE_TYPE
},
}
func create_input_fields(_base_control:Control, _resource_previewer, whitelist:Array = []) -> Dictionary:
if plant:
return plant.create_input_fields(_base_control, _resource_previewer, whitelist)
return {}
func _fix_duplicate_signals(copy):
copy._modify_prop("plant/plant", copy.plant)
func get_prop_tooltip(prop:String) -> String:
match prop:
"plant/plant_brush_active":
return "The flag that defines if plant will be used during painting or not"
"plant/plant_brush_active":
return "The label to be displayed on top of the plant's thumbnail"
"plant/plant":
return "The contained plant itself"
return ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://84iw3q6l1k5f"
path="res://.godot/imported/gardener_icon.png-b8b1c0b5d01a5cce6b6cd6fe7b505a18.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/dreadpon.spatial_gardener/icons/gardener_icon.png"
dest_files=["res://.godot/imported/gardener_icon.png-b8b1c0b5d01a5cce6b6cd6fe7b505a18.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:#EE7778;}
.st1{fill:none;stroke:#EE7778;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;}
.st2{fill:none;stroke:#EE7778;stroke-width:1.5;stroke-miterlimit:10;}
</style>
<path class="st0" d="M8,9.6c0,0,0,2.2,3.7,2.2c2.3,0,2.6,1.6,2.3,2.1c-2.9-0.7-4,1.1-5.9,1.1S5,13.3,2.1,14
c-0.3-0.6-0.1-2.1,2.3-2.1C8,11.9,8,9.6,8,9.6z"/>
<g>
<path class="st1" d="M7.9,11.2c0.5-1.3,0.3-2.8-0.5-3.9"/>
<path class="st2" d="M10.8,5.3c-0.4,0.4-0.7,0.9-0.8,1.5c0.5,0.3,1.1,0.4,1.7,0.3c1-0.2,2.1-2.1,2.1-2.1S11.6,4.7,10.8,5.3z"/>
<path class="st2" d="M6.8,4.9C6.2,3.8,5,3.9,3.5,3.3C3,3.1,2.5,2.8,2.1,2.4C1.9,2.8,1.8,3.3,1.8,3.8C1.4,5.3,2.2,6.9,3.6,7.5
C5.1,8.2,6.8,7.8,7,7.4C7.4,6.6,7.4,5.7,6.8,4.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dlwmeq6qr0vqn"
path="res://.godot/imported/gardener_icon.svg-e2120aea76615a63bbcfff6c6cc8c0a1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/dreadpon.spatial_gardener/icons/gardener_icon.svg"
dest_files=["res://.godot/imported/gardener_icon.svg-e2120aea76615a63bbcfff6c6cc8c0a1.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,7 @@
[plugin]
name="Spatial Gardener"
description="A vegetation and foliage painting tool for any surface in 3D space"
author="Dreadpon"
version="1.3.3"
script="plugin.gd"

View File

@@ -0,0 +1,482 @@
@tool
extends EditorPlugin
#-------------------------------------------------------------------------------
# Handles the inception of all editor-specific processes:
# Plant creation, painting, UI
# Controls the editing lifecycle of a Gardener
#-------------------------------------------------------------------------------
const Logger = preload("utility/logger.gd")
const Globals = preload("utility/globals.gd")
const FunLib = preload("utility/fun_lib.gd")
const ProjectSettingsManager = preload("utility/project_settings_manager.gd")
const Gardener = preload("gardener/gardener.gd")
const DebugViewer = preload("gardener/debug_viewer.gd")
const UI_SidePanel_SCN = preload("controls/side_panel/ui_side_panel.tscn")
const UI_SidePanel = preload("controls/side_panel/ui_side_panel.gd")
const ThemeAdapter = preload("controls/theme_adapter.gd")
const SceneConverter = preload("scene_converter/scene_converter.gd")
const Greenhouse = preload("greenhouse/greenhouse.gd")
const Greenhouse_Plant = preload("greenhouse/greenhouse_plant.gd")
const Greenhouse_PlantState = preload("greenhouse/greenhouse_plant_state.gd")
const Greenhouse_LODVariant = preload("greenhouse/greenhouse_LOD_variant.gd")
const Toolshed = preload("toolshed/toolshed.gd")
const Toolshed_Brush = preload("toolshed/toolshed_brush.gd")
const Console_SCN = preload("utility/console/console.tscn")
const Console = preload("utility/console/console.gd")
const gardener_icon:Texture2D = preload("icons/gardener_icon.svg")
var _side_panel:UI_SidePanel = null
var _base_control:Control = null
var _resource_previewer = null
var control_theme:Theme = null
var toolbar:HBoxContainer = null
var debug_view_menu:MenuButton
var active_gardener = null
var gardeners_in_tree:Array = []
var folding_states: Dictionary = {}
var scene_converter: SceneConverter = null
var _editor_camera_cache: Camera3D = null
var logger = null
var undo_redo = null
#-------------------------------------------------------------------------------
# Lifecycle
#-------------------------------------------------------------------------------
func _init():
DPON_FM.setup()
# Most lifecycle functions here and later on are restricted as editor-only
# Editing plants without an editor is not currently supported
func _ready():
# Is calling it from _ready() the correct way to use it?
# See https://github.com/godotengine/godot/pull/9099
# And https://github.com/godotengine/godot/issues/6869
set_input_event_forwarding_always_enabled()
if !Engine.is_editor_hint(): return
logger = Logger.get_for(self)
if Engine.is_editor_hint():
undo_redo = get_undo_redo()
else:
undo_redo = UndoRedo.new()
# Using selection to start/stop editing of chosen Gardener
get_editor_interface().get_selection().selection_changed.connect(selection_changed)
get_tree().node_added.connect(on_tree_node_added)
get_tree().node_removed.connect(on_tree_node_removed)
func _enter_tree():
# We need settings without editor too
ProjectSettingsManager.add_plugin_project_settings()
if !Engine.is_editor_hint(): return
_base_control = get_editor_interface().get_base_control()
_resource_previewer = get_editor_interface().get_resource_previewer()
adapt_editor_theme()
# TODO: reimplement once functionality is merged in Godot 4.1
# https://github.com/godotengine/godot/pull/62038
# ProjectSettings.project_settings_changed.connect(_on_project_settings_changed)
scene_converter = SceneConverter.new()
scene_converter.setup(_base_control)
_side_panel = UI_SidePanel_SCN.instantiate()
_side_panel.theme = control_theme
make_debug_view_menu()
toolbar = HBoxContainer.new()
toolbar.add_child(VSeparator.new())
toolbar.add_child(debug_view_menu)
toolbar.visible = false
add_custom_types()
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT, _side_panel)
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, toolbar)
selection_changed()
func _exit_tree():
if !Engine.is_editor_hint(): return
if is_instance_valid(scene_converter):
scene_converter.destroy()
scene_converter.queue_free()
set_gardener_edit_state(null)
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT, _side_panel)
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, toolbar)
remove_custom_types()
if is_instance_valid(_side_panel):
_side_panel.queue_free()
if is_instance_valid(toolbar):
toolbar.queue_free()
# Previously here was '_apply_changes', but it fired even when scene was closed without saving
# '_save_external_data' respects saving/not saving choice
func _save_external_data():
if !Engine.is_editor_hint(): return
apply_changes_to_gardeners()
func add_custom_types():
add_custom_type("Gardener", "Node3D", Gardener, gardener_icon)
add_custom_type("Greenhouse", "Resource", Greenhouse, null)
add_custom_type("Greenhouse_Plant", "Resource", Greenhouse_Plant, null)
add_custom_type("Greenhouse_PlantState", "Resource", Greenhouse_PlantState, null)
add_custom_type("Greenhouse_LODVariant", "Resource", Greenhouse_LODVariant, null)
add_custom_type("Toolshed", "Resource", Toolshed, null)
add_custom_type("Toolshed_Brush", "Resource", Toolshed_Brush, null)
func remove_custom_types():
remove_custom_type("Gardener")
remove_custom_type("Greenhouse")
remove_custom_type("Greenhouse_Plant")
remove_custom_type("Greenhouse_PlantState")
remove_custom_type("Greenhouse_LODVariant")
remove_custom_type("Toolshed")
remove_custom_type("Toolshed_Brush")
func on_tree_node_added(node:Node):
if FunLib.obj_is_script(node, Gardener):
gardeners_in_tree.append(node)
if node.has_method("dpon_testing_set_undo_redo"):
node.dpon_testing_set_undo_redo(undo_redo)
if node.has_method("dpon_testing_set_editor_selection"):
node.dpon_testing_set_editor_selection(get_editor_interface().get_selection())
func on_tree_node_removed(node:Node):
if FunLib.obj_is_script(node, Gardener):
gardeners_in_tree.erase(node)
# Call _apply_changes on all Gardeners in the scene
func apply_changes_to_gardeners():
for gardener in gardeners_in_tree:
if is_instance_valid(gardener) && is_instance_of(gardener, Gardener):
gardener._apply_changes()
func _get_plugin_name() -> String:
return 'SpatialGardener'
func _on_project_settings_changed():
if scene_converter:
scene_converter._on_project_settings_changed()
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
# Allows editor to forward us the spatial GUI input for any Gardener
func handles(object):
return is_instance_of(object, Gardener)
# Handle events
# Propagate editor camera
# Forward input to Gardener if selected
func _forward_3d_gui_input(camera, event):
_editor_camera_cache = camera
propagate_camera()
var handled = false
if is_instance_valid(active_gardener):
handled = active_gardener.forwarded_input(camera, event)
if !handled:
plugin_input(event)
return handled
func plugin_input(event):
if is_instance_of(event, InputEventKey) && !event.pressed:
if event.keycode == debug_get_dump_editor_tree_key():
debug_dump_editor_tree()
elif (event.keycode == get_focus_painter_key()
&& !Input.is_key_pressed(KEY_SHIFT) && !Input.is_key_pressed(KEY_CTRL) && !Input.is_key_pressed(KEY_ALT) && !Input.is_key_pressed(KEY_SYSREQ)):
focus_painter()
# A hack to propagate editor camera using _forward_3d_gui_input
func propagate_camera():
for gardener in gardeners_in_tree:
if is_instance_valid(gardener):
gardener.propagate_camera(_editor_camera_cache)
func on_debug_view_menu_id_pressed(id):
if is_instance_valid(active_gardener):
active_gardener.debug_view_flag_checked(debug_view_menu, id)
# A somewhat hacky way to focus editor camera on the painter
func focus_painter():
if !Engine.is_editor_hint(): return
if !active_gardener: return
var editor_selection:EditorSelection = get_editor_interface().get_selection()
if get_editor_interface().get_selection().selection_changed.is_connected(selection_changed):
get_editor_interface().get_selection().selection_changed.disconnect(selection_changed)
editor_selection.clear()
editor_selection.add_node(active_gardener.painter.paint_brush_node)
simulate_key(KEY_F)
# Have to delay that so input has time to process
call_deferred("restore_gardener_selection")
func simulate_key(keycode):
var event = InputEventKey.new()
event.keycode = keycode
event.pressed = true
Input.parse_input_event(event)
# Restore selection to seamlessly continue gardener editing
func restore_gardener_selection():
if !Engine.is_editor_hint(): return
if !get_editor_interface().get_selection().selection_changed.is_connected(selection_changed):
get_editor_interface().get_selection().selection_changed.connect(selection_changed)
if !active_gardener: return
var editor_selection:EditorSelection = get_editor_interface().get_selection()
editor_selection.clear()
editor_selection.add_node(active_gardener)
func get_focus_painter_key():
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/focus_painter_key", KEY_Q)
return Globals.index_to_enum(key, Globals.KeyboardKey)
#-------------------------------------------------------------------------------
# UI
#-------------------------------------------------------------------------------
func make_debug_view_menu():
debug_view_menu = DebugViewer.make_debug_view_menu()
debug_view_menu.get_popup().id_pressed.connect(on_debug_view_menu_id_pressed)
# Modify editor theme to use proper colors, margins, etc.
func adapt_editor_theme():
if !Engine.is_editor_hint(): return
var editorTheme = get_editor_interface().get_editor_theme()
control_theme = ThemeAdapter.adapt_theme(editorTheme)
# Gather folding states from side panel
func _get_state() -> Dictionary:
_side_panel.cleanup_folding_states(folding_states)
return {'folding_states': folding_states}
# Restore folding states for side panel
func _set_state(state: Dictionary):
folding_states = state.folding_states
func on_greenhouse_prop_action_executed(prop_action, final_val):
_side_panel.on_greenhouse_prop_action_executed(folding_states, active_gardener.greenhouse, prop_action, final_val)
func refresh_folding_state_for_greenhouse(greenhouse):
if greenhouse:
_side_panel.refresh_folding_states_for_greenhouse(folding_states, greenhouse)
#-------------------------------------------------------------------------------
# Gardener editing lifecycle
#-------------------------------------------------------------------------------
# Selection changed. Check if we should start/stop editing a Gardener
func selection_changed():
assert(get_editor_interface() && get_editor_interface().get_selection())
var selection = get_editor_interface().get_selection().get_selected_nodes()
handle_selected_gardener(selection)
func handle_selected_gardener(selection:Array):
var gardener = null
if selection.size() == 1:
# Find a Gardener in selection. If found more than one - abort because of ambiguity
for selected in selection:
if is_instance_of(selected, Gardener):
if gardener:
gardener = null
logger.warn("Cannot edit multiple Gardeners at once!")
if !gardener:
gardener = selected
if gardener:
if gardener == active_gardener: return
set_gardener_edit_state(selection[0])
else:
set_gardener_edit_state(null)
# Start/stop editing an active Gardener
func set_gardener_edit_state(gardener):
if (is_instance_valid(active_gardener) && active_gardener != gardener) || !gardener:
stop_gardener_edit()
if gardener:
start_gardener_edit(gardener)
func start_gardener_edit(gardener):
active_gardener = gardener
# TODO: figure out this weird bug :/
# basically, when having 2 scenes open, one with gardener and another NOT SAVED PREVIOUSLY (new empty scene)
# if you switch to an empty scene and save it, gardener loses all references (this doesnt happen is was saved at least once)
# To prevent that we call _ready each time we start gardener editing
# But this leads to some nodes being instanced again, even though they already exist
# This is a workaround that I haven't tested extensively, so it might backfire in the future
#
# There's more.
# Foldable states are reset two (maybe even all resource incuding gardeners and toolsheds, etc.?)
# Doesn't seem so, but still weird
#
# Worth noting, that this leads to a severe slowdown when clicking through gardeners in a scene
# Since stuff like "restoring after load" has to run again
# active_gardener._ready()
# I am testing a workaround of just restoring references, to avoid the unneccesary operations caused be previous solution
# UPD: Actually seems to work even without calling the method below. I'm confused
# I'll keep it here *just in case* the bug still persists but hides well
#
# UPD: when converting to Godot 4.0, this method resulted in enormous delay when selecting a Gardener for edit (3 seconds for empty Gardener)
# UPD: it seems most of the lag came from UI nodes, and method below is actually more-or-less fine
# it's still extra work, so wouldn't hurt to actually find a solution without it
active_gardener.restore_references()
active_gardener.tree_exited.connect(set_gardener_edit_state.bind(null))
active_gardener.greenhouse_prop_action_executed.connect(on_greenhouse_prop_action_executed)
active_gardener.start_editing(_base_control, _resource_previewer, undo_redo, _side_panel)
_side_panel.visible = true
toolbar.visible = true
active_gardener.up_to_date_debug_view_menu(debug_view_menu)
refresh_folding_state_for_greenhouse(active_gardener.greenhouse)
active_gardener.propagate_camera(_editor_camera_cache)
func stop_gardener_edit():
_get_state()
_side_panel.visible = false
toolbar.visible = false
if active_gardener:
active_gardener.stop_editing()
if active_gardener.tree_exited.is_connected(set_gardener_edit_state):
active_gardener.tree_exited.disconnect(set_gardener_edit_state)
if active_gardener.greenhouse_prop_action_executed.is_connected(on_greenhouse_prop_action_executed):
active_gardener.greenhouse_prop_action_executed.disconnect(on_greenhouse_prop_action_executed)
active_gardener = null
#-------------------------------------------------------------------------------
# Debug
#-------------------------------------------------------------------------------
# Dump the whole editor tree to console
func debug_dump_editor_tree():
debug_dump_node_descendants(get_editor_interface().get_editor_main_screen())
func debug_dump_node_descendants(node:Node, intendation:int = 0):
var intend_str = ""
for i in range(0, intendation):
intend_str += " "
var string = ""
if is_instance_of(node, Control):
string = "%s%s %s" % [intend_str, str(node), str(node.size)]
else:
string = "%s%s" % [intend_str, str(node)]
logger.info(string)
intendation += 1
for child in node.get_children():
debug_dump_node_descendants(child, intendation)
func debug_save_node_descendants(node:Node, owner_node: Node):
print("Adding %s" % [str(node)])
for child in node.get_children():
child.owner = owner_node
debug_save_node_descendants(child, owner_node)
if node == owner_node:
print("Saving dump...")
var packed_editor := PackedScene.new()
packed_editor.pack(node)
ResourceSaver.save(packed_editor, "res://packed_editor.tscn")
func debug_get_dump_editor_tree_key():
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/dump_editor_tree_key", 0)
return Globals.index_to_enum(key, Globals.KeyboardKey)
func debug_toggle_console():
var current_scene := get_tree().get_current_scene()
if current_scene.has_node("Console") && is_instance_of(current_scene.get_node("Console"), Console):
current_scene.get_node("Console").queue_free()
else:
var console = Console_SCN.instantiate()
current_scene.add_child(console)

View File

@@ -0,0 +1,68 @@
@tool
extends Window
signal confirm_pressed
signal cancel_pressed
signal dont_ask_again_toggled(state)
func _init():
close_requested.connect(hide)
func _ready():
$'%TreeScenes'.item_selected.connect(_on_tree_item_selected)
func _on_tree_item_selected():
var selected_item: TreeItem = $'%TreeScenes'.get_selected()
if !selected_item: return
selected_item.set_checked(0, !selected_item.is_checked(0))
selected_item.deselect(0)
func add_scenes(scenes: Array):
$'%TreeScenes'.clear()
$'%TreeScenes'.hide_root = true
var root = $'%TreeScenes'.create_item()
for scene in scenes:
var item: TreeItem = $'%TreeScenes'.create_item(root)
item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
item.set_checked(0, true)
item.set_text(0, scene)
func get_selected_scenes() -> Array:
var selected_scenes = []
var child_item: TreeItem = $'%TreeScenes'.get_root().get_children()
while child_item != null:
if child_item.is_checked(0):
selected_scenes.append(child_item.get_text(0))
child_item = child_item.get_next()
return selected_scenes
func should_mk_backups():
return $'%ButtonBackup'.button_pressed
func _on_ButtonConfirm_pressed():
confirm_pressed.emit()
func _on_ButtonCancel_pressed():
cancel_pressed.emit()
func _on_ButtonDontAskAgain_toggled(pressed):
dont_ask_again_toggled.emit(pressed)
func _on_ConvertDialog_about_to_show():
$'%ButtonBackup'.button_pressed = true
$'%ButtonDontAskAgain'.button_pressed = false

View File

@@ -0,0 +1,75 @@
[gd_scene load_steps=2 format=3 uid="uid://uhmxpabnq061"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/scene_converter/convert_dialog.gd" id="1"]
[node name="ConvertDialog" type="Window"]
size = Vector2i(500, 300)
script = ExtResource("1")
[node name="PanelContainer" type="PanelContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer"]
layout_mode = 2
[node name="LabelInfo" type="Label" parent="PanelContainer/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "The following outdated Node3D Gardener scenes were found:"
[node name="TreeScenes" type="Tree" parent="PanelContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
allow_reselect = true
[node name="LabelPrompt" type="Label" parent="PanelContainer/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Would you like to convert them to the current version?
(results wll be in the console/output)"
[node name="ButtonBackup" type="CheckBox" parent="PanelContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Create backup duplicates"
[node name="ButtonDontAskAgain" type="CheckBox" parent="PanelContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Don't ask me again"
[node name="ActionButtons" type="HBoxContainer" parent="PanelContainer/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
alignment = 1
[node name="Spacer" type="Control" parent="PanelContainer/VBoxContainer/ActionButtons"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ButtonConfirm" type="Button" parent="PanelContainer/VBoxContainer/ActionButtons"]
unique_name_in_owner = true
layout_mode = 2
text = "Convert"
[node name="Spacer2" type="Control" parent="PanelContainer/VBoxContainer/ActionButtons"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ButtonCancel" type="Button" parent="PanelContainer/VBoxContainer/ActionButtons"]
unique_name_in_owner = true
layout_mode = 2
text = "Cancel"
[node name="Spacer3" type="Control" parent="PanelContainer/VBoxContainer/ActionButtons"]
layout_mode = 2
size_flags_horizontal = 3
[connection signal="about_to_popup" from="." to="." method="_on_ConvertDialog_about_to_show"]
[connection signal="toggled" from="PanelContainer/VBoxContainer/ButtonDontAskAgain" to="." method="_on_ButtonDontAskAgain_toggled"]
[connection signal="pressed" from="PanelContainer/VBoxContainer/ActionButtons/ButtonConfirm" to="." method="_on_ButtonConfirm_pressed"]
[connection signal="pressed" from="PanelContainer/VBoxContainer/ActionButtons/ButtonCancel" to="." method="_on_ButtonCancel_pressed"]

View File

@@ -0,0 +1,147 @@
extends RefCounted
enum Tokens {
NONE,
STMT_SEPARATOR,
EQL_SIGN,
OPEN_PRNTS,
CLSD_PRNTS,
OPEN_SQR_BRKT,
CLSD_SQR_BRKT,
OPEN_CLY_BRKT,
CLSD_CLY_BRKT,
SGL_QUOTE,
DBL_QUOTE,
COLON,
COMMA,
SUB_RES,
EXT_RES,
PROP_NAME,
VAL_NIL,
VAL_BOOL,
VAL_INT,
VAL_REAL,
VAL_STRING,
VAL_VECTOR2,
VAL_RECT,
VAL_VECTOR3,
VAL_TRANSFORM2D,
VAL_PLANE,
VAL_QUAT,
VAL_AABB,
VAL_BASIS,
VAL_TRANSFORM,
VAL_COLOR,
VAL_NODE_PATH,
VAL_RID,
VAL_OBJECT,
VAL_DICTIONARY,
VAL_ARRAY,
VAL_RAW_ARRAY,
VAL_INT_ARRAY,
VAL_REAL_ARRAY,
VAL_STRING_ARRAY,
VAL_VECTOR2_ARRAY,
VAL_VECTOR3_ARRAY,
VAL_COLOR_ARRAY,
VAL_STRUCT,
}
static func get_val_for_export(val):
match typeof(val):
TYPE_NIL:
return 'null'
TYPE_STRING:
return '"%s"' % [val]
TYPE_FLOAT:
if is_equal_approx(val - int(val), 0.0):
return '%d.0' % [int(val)]
return str(val)
TYPE_BOOL:
return 'true' if val == true else 'false'
TYPE_ARRAY:
var string = '[ '
for element in val:
string += get_val_for_export(element) + ', '
if val.size() != 0:
string = string.trim_suffix(', ')
string += ' ]'
return string
TYPE_DICTIONARY:
var string = '{\n'
for key in val:
string += '%s: %s,\n' % [get_val_for_export(key), get_val_for_export(val[key])]
if val.size() != 0:
string = string.trim_suffix(',\n')
string += '\n}'
return string
return str(val)
static func to_bool(string: String):
return string.to_lower() == 'true'
class TokenVal extends RefCounted:
var type: int = Tokens.NONE
var val = null
func _init(__type: int = Tokens.NONE, __val = null):
type = __type
val = __val
func _to_string():
return "[%s:'%s']" % [Tokens.keys()[type], str(val)]
func is_token(token_type: int):
return type == token_type
class PropStruct extends RefCounted:
var content = null
func _init(__content = null):
content = __content
func _to_string():
return str(content)
class PS_Vector3 extends PropStruct:
func _init(__content = null):
super(__content)
func variant():
var split = content.trim_prefix('Vector3( ').trim_suffix(' )').split(', ')
return Vector3(split[0], split[1], split[2])
class PS_Transform extends PropStruct:
func _init(__content = null):
super(__content)
func variant():
var split = content.trim_prefix('Transform3D( ').trim_suffix(' )').split(', ')
return Transform3D(Vector3(split[0], split[3], split[6]), Vector3(split[1], split[4], split[7]), Vector3(split[2], split[5], split[8]), Vector3(split[9], split[10], split[11]))
class SubResource extends PropStruct:
var id: int = -1
func _init(__id: int = -1):
id = __id
func _to_string():
return 'SubResource( %d )' % [id]
class ExtResource extends SubResource:
func _init(__id: int = -1):
super(__id)
func _to_string():
return 'ExtResource( %d )' % [id]

View File

@@ -0,0 +1,18 @@
extends RefCounted
enum RunMode {RECREATE, DRY, CONVERT}
const Types = preload('../converter_types.gd')
var Logger = preload('../../utility/logger.gd')
var logger
func _init():
logger = Logger.get_for(self)
func convert_gardener(parsed_scene: Array, run_mode: int, ext_res: Dictionary, sub_res: Dictionary):
pass

View File

@@ -0,0 +1,69 @@
extends 'base_ver_converter.gd'
const Placeform = preload('../../arborist/placeform.gd')
func convert_gardener(parsed_scene: Array, run_mode: int, ext_res: Dictionary, sub_res: Dictionary):
var to_erase = []
for section in ext_res.values():
if section.header.path.ends_with('dreadpon.spatial_gardener/arborist/placement_transform.gd'):
to_erase.append(section)
var total_sections = float(parsed_scene.size())
var progress_milestone = 0
var section_num = 0
var found_octree_nodes = 0
var found_placement_transforms = 0
for section in parsed_scene:
section_num += 1
var file_progress = floor(section_num / total_sections * 100)
if file_progress >= progress_milestone * 10:
logger.info('Iterating sections: %02d%%' % [progress_milestone * 10])
progress_milestone += 1
if section.props.get('__meta__', {}).get('class', '') == 'Gardener':
section.props['storage_version'] = 2
continue
if section.props.get('__meta__', {}).get('class', '') != 'MMIOctreeNode': continue
found_octree_nodes += 1
section.props.member_origin_offsets = Types.PropStruct.new('PoolRealArray( ')
section.props.member_surface_normals = Types.PropStruct.new('PoolVector3Array( ')
section.props.member_octants = Types.PropStruct.new('PoolByteArray( ')
found_placement_transforms += section.props.members.size()
for member_ref in section.props.members:
var placeform_section = sub_res[member_ref.id]
to_erase.append(placeform_section)
var placeform := Placeform.mk(
placeform_section.props.placement.variant(),
placeform_section.props.surface_normal.variant(),
placeform_section.props.transform.variant(),
placeform_section.props.octree_octant
)
section.props.member_origin_offsets.content += Types.get_val_for_export(Placeform.get_origin_offset(placeform)) + ', '
section.props.member_surface_normals.content += '%s, %s, %s, ' % [placeform[1][0], placeform[1][1], placeform[1][2]]
section.props.member_octants.content += Types.get_val_for_export(placeform[3]) + ', '
section.props.member_origin_offsets.content = section.props.member_origin_offsets.content.trim_suffix(', ') + ' )'
section.props.member_surface_normals.content = section.props.member_surface_normals.content.trim_suffix(', ') + ' )'
section.props.member_octants.content = section.props.member_octants.content.trim_suffix(', ') + ' )'
section.props.erase('members')
logger.info('Found OctreeNode objects: %d' % [found_octree_nodes])
logger.info('Found PlacementTransform objects: %d' % [found_placement_transforms])
for section in to_erase:
parsed_scene.erase(section)
var res_id = section.get('header', {}).get('id', -1)
if res_id >= 0:
sub_res.erase(res_id)
ext_res.erase(res_id)

View File

@@ -0,0 +1,625 @@
@tool
extends Node
#-------------------------------------------------------------------------------
# NOTE: automatic conversion from Godot 3.5 to Godot 4.0 will not be supported
# instead, open the original project in Godot 3.5, export transforms to JSON for each plant
# recreate plants in Godot 4.0 and import transforms one by one for each plant
#
# NOTE: most types that are represented as strings are kept in Godot 3.5 format
# this is deliberate, to preserve the state of converter as much as possible
#
# To use this converter:
# 1. Make sure the plugin is updated to the most recent version
# 2. Copy your scenes to addons/dreadpon.spatial_gardener/scene_converter/input_scenes folder.
# - Make sure they have a plain text scene file format (.tscn).
# - The scene converter automatically makes backups of your scenes. But you should make your own, in case anything goes wrong.
# 3. Editor might scream that there are resources missing. This is expected.
# - You might see a message that some plugin scripts are missing. Ignore, since some things *did* get removed in a plugin.
# - That's why you should *not* open these scenes for now.
# 4. Open the scene found at addons/dreadpon.spatial_gardener/scene_converter/scene_converter.tscn.
# 5. Launch it (F6 by default): it will start the conversion process.
# - The process takes about 1-10 minutes per scene, depending on it's size.
# 6. If any errors occured, you'll be notified in the console.
# - The editor will freeze for a while: the best way to keep track of your progress is by launching the editor from console
# - (or by running Godot_v***-stable_win64_console.cmd included in the official download).
# 7. If conversion was successful, grab your converted scenes from addons/dreadpon.spatial_gardener/scene_converter/output_scenes folder
# and move them to their intended places.
# 8. You should be able to launch your converted scenes now.
# - Optionally, you might have to relaunch the project and re-enable the plugin.
# - Make sure to move backups elsewhere before committing to source control.
#
# NOTE: your original scenes (in 'input_scenes' folder) should be intact
# but please keep a backup elsewhere just in case
#
# NOTE: to see the conversion status in real-time
# you'll need to launch editor with console, which you can then inspect
# this is done by launching Godot executable from native console/terminal
#-------------------------------------------------------------------------------
const Types = preload('converter_types.gd')
const Globals = preload("../utility/globals.gd")
const C_1_To_2 = preload('converters/c_1_to_2.gd')
const FunLib = preload("../utility/fun_lib.gd")
const Gardener = preload("../gardener/gardener.gd")
const Logger = preload('../utility/logger.gd')
const ConvertDialog_SCN = preload("convert_dialog.tscn")
const number_char_list = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-']
enum RunMode {RECREATE, DRY, CONVERT}
var logger = null
var conversion_map: Dictionary = {
1: {'target': 2, 'script': C_1_To_2.new()}
}
var run_mode = RunMode.CONVERT
var _base_control: Control = null
var _convert_dialog = null
var _result_dialog: AcceptDialog = null
#-------------------------------------------------------------------------------
# Lifecycle and events
#-------------------------------------------------------------------------------
func setup(__base_control: Control):
_base_control = __base_control
_scan_for_outdated_scenes()
func destroy():
if is_instance_valid(_convert_dialog):
_base_control.remove_child(_convert_dialog)
_convert_dialog.queue_free()
if is_instance_valid(_result_dialog):
_result_dialog.queue_free()
func _hide_dialog():
_convert_dialog.hide()
func _on_project_settings_changed():
_scan_for_outdated_scenes()
func _set_dont_scan_setting(val):
ProjectSettings.set("dreadpons_spatial_gardener/plugin/scan_for_outdated_scenes", !val)
func _ready():
if Engine.is_editor_hint(): return
_convert_from_input_dir()
#-------------------------------------------------------------------------------
# Conversion initiation
#-------------------------------------------------------------------------------
func _convert_from_input_dir():
var self_base_dir = get_script().resource_path.get_base_dir()
var in_path = self_base_dir + '/input_scenes'
var out_path = self_base_dir + '/output_scenes'
var canditate_scenes = _get_candidate_scenes(in_path, false)
if canditate_scenes.is_empty(): return
_run_conversion(canditate_scenes, true, out_path)
func _scan_for_outdated_scenes():
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/plugin/scan_for_outdated_scenes", true): return
var canditate_scenes = _get_candidate_scenes('res://')
if canditate_scenes.is_empty(): return
if !_convert_dialog:
_convert_dialog = ConvertDialog_SCN.instantiate()
_convert_dialog.confirm_pressed.connect(_convert_from_dialog)
_convert_dialog.confirm_pressed.connect(_hide_dialog)
_convert_dialog.cancel_pressed.connect(_hide_dialog)
_convert_dialog.dont_ask_again_toggled.connect(_set_dont_scan_setting)
if _convert_dialog.get_parent() != _base_control:
_base_control.add_child(_convert_dialog)
if !_result_dialog:
_result_dialog = AcceptDialog.new()
_result_dialog.title = 'Node3D Gardener conversion finished'
if _result_dialog.get_parent() != _base_control:
_base_control.add_child(_result_dialog)
_convert_dialog.add_scenes(canditate_scenes)
_convert_dialog.popup_centered()
func _convert_from_dialog():
var result = _run_conversion(_convert_dialog.get_selected_scenes(), _convert_dialog.should_mk_backups())
_result_dialog.dialog_text = (
"""Node3D Gardener conversion finished.
Please check the console/output for errors to see if conversion went successfully.
Don\'t forget to move the backups elsewhere before committing to version control.""")
_result_dialog.popup_centered()
#-------------------------------------------------------------------------------
# Scene candidate gathering
#-------------------------------------------------------------------------------
func _get_candidate_scenes(root_dir: String, check_gardeners: bool = true) -> Array:
var scene_file_paths = []
var gardener_file_paths = []
FunLib.iterate_files(root_dir, true, self, 'add_scene_file', scene_file_paths)
if !check_gardeners:
return scene_file_paths
var file = null
var text = ''
var gardener_regex = RegEx.new()
gardener_regex.compile('"class": "Gardener"')
var storage_regex = RegEx.new()
storage_regex.compile('storage_version = ([0-9])*?\n')
for scene_file in scene_file_paths:
file = FileAccess.open(scene_file, FileAccess.READ)
text = file.get_as_text()
file.close()
var results = gardener_regex.search_all(text)
if results.is_empty(): continue
results = storage_regex.search_all(text)
if results.is_empty():
gardener_file_paths.append(scene_file)
continue
for result in results:
if int(result.strings[1]) != Gardener.get_storage_ver() && conversion_map.has(result.strings[1]):
gardener_file_paths.append(scene_file)
continue
return gardener_file_paths
func add_scene_file(file_path: String, scenes: Array):
if file_path.get_extension() == 'tscn':
scenes.append(file_path)
#-------------------------------------------------------------------------------
# High-level conversion process
#-------------------------------------------------------------------------------
func _run_conversion(in_filepaths: Array, mk_backups: bool = true, out_base_dir: String = '') -> bool:
var timestamp = Time.get_datetime_string_from_system(false, true).replace(' ', '_').replace(':', '.')
logger = Logger.get_for(self, '', 'user://sg_tscn_conversion_%s.txt' % [timestamp])
logger.info('Found %d valid scenes for conversion' % [in_filepaths.size()])
for in_filepath in in_filepaths:
if mk_backups:
var num = 0
while FileAccess.file_exists('%s.backup_%d' % [in_filepath, num]):
num += 1
DirAccess.copy_absolute (in_filepath, '%s.backup_%d' % [in_filepath, num])
var out_filepath = in_filepath
if !out_base_dir.is_empty():
out_filepath = out_base_dir + '/' + in_filepath.get_file()
var start_time = Time.get_ticks_msec()
logger.info('Converting scene: "%s", to file: %s, backup: %s' % [in_filepath, out_filepath, mk_backups])
var in_size = 0
if run_mode == RunMode.CONVERT || run_mode == RunMode.RECREATE:
var file = FileAccess.open(in_filepath, FileAccess.READ)
in_size = file.get_length() * 0.000001
file.close()
var ext_res := {}
var sub_res := {}
logger.info('Parsing scene...')
var parsed_scene = parse_scene(in_filepath, ext_res, sub_res)
if run_mode == RunMode.CONVERT || run_mode == RunMode.DRY:
var storage_vers = get_vers(parsed_scene)
if storage_vers.size() < 1:
logger.warn('No Gardeners found in this scene')
continue
elif storage_vers.size() > 1:
logger.error('Gardeners in this scene have multiple mismatched storage versions. All Gardeners must be of the same version')
continue
var curr_ver = storage_vers[0]
while curr_ver != Gardener.get_storage_ver():
var conversion_data = conversion_map[curr_ver]
logger.info('Converting Gardener data from storage v.%s to v.%s...' % [curr_ver, conversion_data.target])
conversion_data.script.convert_gardener(parsed_scene, run_mode, ext_res, sub_res)
curr_ver = conversion_data.target
if run_mode == RunMode.CONVERT || run_mode == RunMode.RECREATE:
logger.info('Reconstructing scene...')
reconstruct_scene(parsed_scene, out_filepath)
var time_took = float(Time.get_ticks_msec() - start_time) / 1000
logger.info('Finished converting scene: "%s"' % [in_filepath])
logger.info('Took: %.2fs' % [ time_took])
if run_mode == RunMode.CONVERT || run_mode == RunMode.RECREATE:
var file = FileAccess.open(out_filepath, FileAccess.READ)
var out_size = file.get_length() * 0.000001
file.close()
logger.info('Size changed from %.2fMb to %.2fMb' % [in_size, out_size])
logger.info('Finished %d scene(s) conversions' % [in_filepaths.size()])
return true
func get_vers(parsed_scene):
var vers = []
for section in parsed_scene:
if section.props.get('__meta__', {}).get('class', '') == 'Gardener':
var ver = section.props.get('storage_version', 1)
if vers.has(ver): continue
vers.append(ver)
return vers
func reconstruct_scene(parsed_scene: Array, out_path: String):
var file = FileAccess.open(out_path, FileAccess.WRITE)
if !file:
logger.error('Unable to write to file "%s", with error: %s' % [out_path, Globals.get_err_message(FileAccess.get_open_error())])
var total_sections = float(parsed_scene.size())
var progress_milestone = 0
var last_type = ''
var section_num = 0
for section in parsed_scene:
if ['sub_resource', 'node'].has(last_type) || !last_type.is_empty() && last_type != section.type:
file.store_line('')
var line = '[' + section.type
for section_prop in section.header:
line += ' %s=%s' % [section_prop, Types.get_val_for_export(section.header[section_prop])]
line += ']'
file.store_line(line)
for prop in section.props:
line = '%s = %s' % [prop, Types.get_val_for_export(section.props[prop])]
file.store_line(line)
last_type = section.type
section_num += 1
var file_progress = floor(section_num / total_sections * 100)
if file_progress >= progress_milestone * 10:
logger.info('Reconstructed: %02d%%' % [progress_milestone * 10])
progress_milestone += 1
file.close()
#-------------------------------------------------------------------------------
# Low-level parsing
#-------------------------------------------------------------------------------
func parse_scene(filepath: String, ext_res: Dictionary = {}, sub_res: Dictionary = {}) -> Array:
var result := []
var file: FileAccess = FileAccess.open(filepath, FileAccess.READ)
if !file:
logger.error('Unable to open file "%s", with error: %s' % [filepath, Globals.get_err_message(FileAccess.get_open_error())])
var file_len = float(file.get_length())
var progress_milestone = 0
var section_string: PackedStringArray = PackedStringArray()
var section_active := false
var section = {}
var sections_parts = []
var open_square_brackets = 0
var header_start = 0
var header_active = false
var first_line = true
var line_byte_offset = 1
var line: String
while !file.eof_reached():
line = file.get_line()
var no_brackets = open_square_brackets == 0
var position = file.get_position()
var line_size = line.to_utf8_buffer().size() + line_byte_offset
# If first line size not equal to position - then we're dealing with CRLF
if first_line && position != line_size:
line_byte_offset = 2
line_size = line.to_utf8_buffer().size() + line_byte_offset
open_square_brackets += line.count('[')
open_square_brackets -= line.count(']')
if line.begins_with('['):
header_active = true
header_start = position - line_size
if header_active && open_square_brackets == 0:
open_square_brackets = 0
header_active = false
var header_end = position
file.seek(header_start)
var header_str = file.get_buffer(header_end - header_start).get_string_from_utf8().strip_edges()
file.seek(header_end)
section = {'type': '', 'header': {}, 'props': {}}
sections_parts = Array(header_str.trim_prefix('[').trim_suffix(']').split(' '))
section.type = sections_parts.pop_front()
section.header = parse_resource(" ".join(PackedStringArray(sections_parts)) + ' ', ' ')
result.append(section)
section_string = PackedStringArray()
if section.type == 'ext_resource':
ext_res[section.header.id] = section
elif section.type == 'sub_resource':
sub_res[section.header.id] = section
section_active = true
elif section_active && line.strip_escapes().is_empty() && !result.is_empty():
result[-1].props = parse_resource(''.join(section_string))
section_active = false
elif !line.strip_escapes().is_empty():
section_string.append(line + '\n')
var file_progress = floor(position / file_len * 100)
if file_progress >= progress_milestone * 10:
logger.info('Parsed: %02d%%' % [progress_milestone * 10])
progress_milestone += 1
if first_line:
first_line = false
return result
func parse_resource(res_string: String, separator: String = '\n') -> Dictionary:
if res_string.is_empty(): return {}
var result := {}
var tokens := tokenize_string(res_string, separator)
result = tokens_to_dict(tokens)
return result
func tokenize_string(string: String, separator: String = '\n') -> Array:
var tokens = Array()
var current_token = Types.Tokens.NONE
var character = ''
var status_bundle = {
'idx': 0,
'string': string,
'last_tokenized_idx': 0
}
for idx in string.length():
status_bundle.idx = idx
character = string[idx]
if current_token == Types.Tokens.NONE:
# All chars so far were numerical, and next one IS NOT
if ((string.length() <= idx + 1 || !number_char_list.has(string[idx + 1]))
&& str_has_only_numbers(str_last_inclusive(status_bundle))):
# Number string has a dot - is a float
if str_last_inclusive_stripped(status_bundle).find('.') >= 0:
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_REAL, float(str_last_inclusive_stripped(status_bundle))))
# Else - int
else:
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_INT,int(str_last_inclusive_stripped(status_bundle))))
status_bundle.last_tokenized_idx = idx + 1
if character == '=':
var prop_name = str_last_stripped(status_bundle)
while tokens.size() > 0:
var token_val = tokens[-1]
if token_val.type == Types.Tokens.STMT_SEPARATOR: break
tokens.pop_back()
prop_name = str(token_val.val) + prop_name
tokens.append(Types.TokenVal.new(Types.Tokens.PROP_NAME, prop_name))
tokens.append(Types.TokenVal.new(Types.Tokens.EQL_SIGN, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == '"' && (idx == 0 || string[idx - 1] != '\\'):
current_token = Types.Tokens.DBL_QUOTE
status_bundle.last_tokenized_idx = idx + 1
elif character == ',':
tokens.append(Types.TokenVal.new(Types.Tokens.COMMA, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == ':':
tokens.append(Types.TokenVal.new(Types.Tokens.COLON, character))
status_bundle.last_tokenized_idx = idx + 1
# Parentheses not representing a "struct" are impossible
# So we don't parse them separately
elif character == '[':
tokens.append(Types.TokenVal.new(Types.Tokens.OPEN_SQR_BRKT, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == ']':
tokens.append(Types.TokenVal.new(Types.Tokens.CLSD_SQR_BRKT, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == '{':
tokens.append(Types.TokenVal.new(Types.Tokens.OPEN_CLY_BRKT, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == '}':
tokens.append(Types.TokenVal.new(Types.Tokens.CLSD_CLY_BRKT, character))
status_bundle.last_tokenized_idx = idx + 1
elif character == '(':
current_token = Types.Tokens.VAL_STRUCT
elif ['false', 'true'].has(str_last_inclusive_stripped(status_bundle).to_lower()):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_BOOL, Types.to_bool(str_last_inclusive_stripped(status_bundle))))
status_bundle.last_tokenized_idx = idx + 1
elif str_last_inclusive_stripped(status_bundle) == 'null':
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_NIL, null))
status_bundle.last_tokenized_idx = idx + 1
elif character == separator:
tokens.append(Types.TokenVal.new(Types.Tokens.STMT_SEPARATOR, ''))
elif current_token == Types.Tokens.DBL_QUOTE:
if character == '"' && (idx == 0 || string[idx - 1] != '\\'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_STRING, str_last(status_bundle)))
status_bundle.last_tokenized_idx = idx + 1
current_token = Types.Tokens.NONE
elif current_token == Types.Tokens.VAL_STRUCT && character == ')':
var str_struct = str_last_inclusive_stripped(status_bundle)
if str_struct.begins_with('SubResource'):
tokens.append(Types.TokenVal.new(Types.Tokens.SUB_RES, Types.SubResource.new(int(str_struct))))
elif str_struct.begins_with('ExtResource'):
tokens.append(Types.TokenVal.new(Types.Tokens.EXT_RES, Types.ExtResource.new(int(str_struct))))
elif str_struct.begins_with('Vector2'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_VECTOR2, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Rect'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_RECT, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Vector3'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_VECTOR3, Types.PS_Vector3.new(str_struct)))
elif str_struct.begins_with('Transform2D'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_TRANSFORM2D, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Plane'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_PLANE, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Quat'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_QUAT, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('AABB'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_AABB, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Basis'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_BASIS, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('Transform'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_TRANSFORM, Types.PS_Transform.new(str_struct)))
elif str_struct.begins_with('Color'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_COLOR, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('NodePath'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_NODE_PATH, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolByteArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_RAW_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolIntArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_INT_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolRealArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_REAL_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolStringArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_STRING_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolVector2Array'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_VECTOR2_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolVector3Array'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_VECTOR3_ARRAY, Types.PropStruct.new(str_struct)))
elif str_struct.begins_with('PoolColorArray'):
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_COLOR_ARRAY, Types.PropStruct.new(str_struct)))
else:
tokens.append(Types.TokenVal.new(Types.Tokens.VAL_STRUCT, str_last_inclusive_stripped(status_bundle)))
status_bundle.last_tokenized_idx = idx + 1
current_token = Types.Tokens.NONE
return tokens
func str_last_stripped(status_bundle: Dictionary) -> String:
return str_last(status_bundle).strip_edges()
func str_last(status_bundle: Dictionary) -> String:
return status_bundle.string.substr(status_bundle.last_tokenized_idx, status_bundle.idx - status_bundle.last_tokenized_idx)
func str_last_inclusive_stripped(status_bundle: Dictionary) -> String:
return str_last_inclusive(status_bundle).strip_edges()
func str_last_inclusive(status_bundle: Dictionary) -> String:
return status_bundle.string.substr(status_bundle.last_tokenized_idx, status_bundle.idx - status_bundle.last_tokenized_idx + 1)
func str_has_only_numbers(string: String) -> bool:
string = string.strip_escapes().strip_edges()
if string.is_empty(): return false
for character in string:
if !number_char_list.has(character):
return false
return true
func tokens_to_dict(tokens: Array) -> Dictionary:
var result := {}
var keys := []
var nest_level := 1
var values := [result]
var dest_string = ''
var idx := 0
while idx < tokens.size():
var push_to_values := false
var token: Types.TokenVal = tokens[idx]
match token.type:
Types.Tokens.EQL_SIGN, Types.Tokens.COLON:
var key = values.pop_back()
keys.append(key)
Types.Tokens.CLSD_CLY_BRKT:
if values.size() > nest_level:
push_to_values = true
nest_level -= 1
Types.Tokens.CLSD_SQR_BRKT:
if values.size() > nest_level:
push_to_values = true
nest_level -= 1
Types.Tokens.COMMA:
push_to_values = true
Types.Tokens.PROP_NAME:
values.append(token.val)
Types.Tokens.OPEN_CLY_BRKT:
values.append({})
nest_level += 1
Types.Tokens.OPEN_SQR_BRKT:
values.append([])
nest_level += 1
Types.Tokens.STMT_SEPARATOR:
if tokens.size() <= idx + 1 || tokens[idx + 1].is_token(Types.Tokens.PROP_NAME):
push_to_values = true
_:
values.append(token.val)
if push_to_values:
var destination = values[-2]
var val = values.pop_back()
if destination is Array:
destination.append(val)
elif !keys.is_empty():
var key = keys.pop_back()
destination[key] = val
idx += 1
return result

View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://d4gravhvudlcd"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/scene_converter/scene_converter.gd" id="1"]
[node name="SceneConverter" type="Node"]
script = ExtResource("1")

View File

@@ -0,0 +1,44 @@
shader_type spatial;
render_mode blend_mix,cull_disabled,unshaded,depth_draw_opaque,depth_test_disabled;
// Base color + opacity
uniform vec4 albedo : source_color;
void fragment() {
ALBEDO = albedo.rgb;
if (length(UV - vec2(0.5)) > 0.5) {
discard;
}
// Fancy dithered alpha stuff
float opacity = albedo.a;
int x = int(FRAGCOORD.x) % 4;
int y = int(FRAGCOORD.y) % 4;
int index = x + y * 4;
float limit = 0.0;
if (x < 8) {
if (index == 0) limit = 0.0625;
if (index == 1) limit = 0.5625;
if (index == 2) limit = 0.1875;
if (index == 3) limit = 0.6875;
if (index == 4) limit = 0.8125;
if (index == 5) limit = 0.3125;
if (index == 6) limit = 0.9375;
if (index == 7) limit = 0.4375;
if (index == 8) limit = 0.25;
if (index == 9) limit = 0.75;
if (index == 10) limit = 0.125;
if (index == 11) limit = 0.625;
if (index == 12) limit = 1.0;
if (index == 13) limit = 0.5;
if (index == 14) limit = 0.875;
if (index == 15) limit = 0.375;
}
// Skip drawing a pixel below the opacity limit
if (opacity < limit)
discard;
}

View File

@@ -0,0 +1,58 @@
shader_type spatial;
render_mode blend_mix,cull_disabled,unshaded,depth_draw_opaque;
// Base color + opacity
uniform vec4 albedo : source_color;
// Brush diameter
uniform float proximity_multiplier = 1.0;
// Distance at which proximity highlight occurs
uniform float proximity_treshold = 0.4;
// Depth texture
uniform sampler2D depth_texture : hint_depth_texture;
void fragment() {
ALBEDO = albedo.rgb;
// Fancy dithered alpha stuff
float opacity = albedo.a;
int x = int(FRAGCOORD.x) % 4;
int y = int(FRAGCOORD.y) % 4;
int index = x + y * 4;
float limit = 0.0;
if (x < 8) {
if (index == 0) limit = 0.0625;
if (index == 1) limit = 0.5625;
if (index == 2) limit = 0.1875;
if (index == 3) limit = 0.6875;
if (index == 4) limit = 0.8125;
if (index == 5) limit = 0.3125;
if (index == 6) limit = 0.9375;
if (index == 7) limit = 0.4375;
if (index == 8) limit = 0.25;
if (index == 9) limit = 0.75;
if (index == 10) limit = 0.125;
if (index == 11) limit = 0.625;
if (index == 12) limit = 1.0;
if (index == 13) limit = 0.5;
if (index == 14) limit = 0.875;
if (index == 15) limit = 0.375;
}
// Skip drawing a pixel below the opacity limit
if (opacity < limit)
discard;
// Proximity highlight to make brush bounds more visible in the scene
float depth_tex = texture(depth_texture, SCREEN_UV).x;
// fix from https://www.reddit.com/r/godot/comments/wb0jw7/godot_4_alpha_12_depth_texture_not_working/
// depth texture in 4.0 is automaticaly within neccessary range
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, texture(depth_texture,SCREEN_UV).x);
vec4 view = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, depth_tex, 1.0);
view.xyz /= view.w;
float proximity = 1.0 - clamp(1.0 - smoothstep(view.z + proximity_treshold * proximity_multiplier, view.z, VERTEX.z), 0.0, 1.0);
// Highlight pixels that are close to other geometry
ALBEDO = clamp(ALBEDO + vec3(proximity * 0.5), 0.0, 1.0);
}

View File

@@ -0,0 +1,8 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://dgfk5ryw7qatd"]
[ext_resource type="Shader" path="res://addons/dreadpon.spatial_gardener/shaders/sh_circle_brush.gdshader" id="1"]
[resource]
render_priority = 0
shader = ExtResource("1")
shader_parameter/albedo = Color(0.172549, 0.32549, 0.65098, 0.501961)

View File

@@ -0,0 +1,10 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://ldawlc0e5u8w"]
[ext_resource type="Shader" path="res://addons/dreadpon.spatial_gardener/shaders/sh_sphere_brush.gdshader" id="1"]
[resource]
render_priority = 0
shader = ExtResource("1")
shader_parameter/albedo = Color(0.172549, 0.32549, 0.65098, 0.501961)
shader_parameter/proximity_multiplier = 1.0
shader_parameter/proximity_treshold = 0.4

View File

@@ -0,0 +1,229 @@
@tool
extends "../utility/input_field_resource/input_field_resource.gd"
#-------------------------------------------------------------------------------
# The manager of all brush types for a given Gardener
# Handles interfacing between Toolshed_Brush, UI and plant painting
#-------------------------------------------------------------------------------
const Toolshed_Brush = preload("toolshed_brush.gd")
const ui_category_brushes_SCN = preload("../controls/side_panel/ui_category_brushes.tscn")
const ui_section_brush_SCN = preload("../controls/side_panel/ui_section_brush.tscn")
var brushes:Array = []
var active_brush:Toolshed_Brush = null
var ui_category_brushes_nd:Control = null
var tab_container_brushes_nd:Control = null
var panel_container_category_nd:Control = null
var _base_control:Control = null
var _resource_previewer = null
signal prop_action_executed_on_brush(prop_action, final_val, brush)
#-------------------------------------------------------------------------------
# Initialization
#-------------------------------------------------------------------------------
func _init(__brushes:Array = []):
super()
set_meta("class", "Toolshed")
resource_name = "Toolshed"
brushes = _modify_prop("brush/brushes", __brushes)
if brushes.size() > 0:
active_brush = _modify_prop("brush/active_brush", brushes[0])
_add_prop_dependency("brush/active_brush", ["brush/brushes"])
# The UI is created here because we need to manage it afterwards
# And I see no reason to get lost in a signal spaghetti of delegating it
func create_ui(__base_control:Control, __resource_previewer):
_base_control = __base_control
_resource_previewer = __resource_previewer
if is_instance_valid(ui_category_brushes_nd):
ui_category_brushes_nd.queue_free()
ui_category_brushes_nd = ui_category_brushes_SCN.instantiate()
tab_container_brushes_nd = ui_category_brushes_nd.find_child('TabContainer_Brushes')
panel_container_category_nd = ui_category_brushes_nd.find_child('Label_Category')
ui_category_brushes_nd.theme_type_variation = "InspectorPanelContainer"
panel_container_category_nd.theme_type_variation = "PropertyCategory"
for brush in brushes:
var section_brush = ui_section_brush_SCN.instantiate()
var vbox_container_properties = section_brush.find_child('VBoxContainer_Properties')
section_brush.name = FunLib.capitalize_string_array(brush.BrushType.keys())[brush.behavior_brush_type]
tab_container_brushes_nd.add_child(section_brush)
for input_field in brush.create_input_fields(_base_control, _resource_previewer).values():
vbox_container_properties.add_child(input_field)
section_brush.theme_type_variation = "InspectorPanelContainer"
if brushes.size() > 0:
tab_container_brushes_nd.current_tab = brushes.find(active_brush)
tab_container_brushes_nd.tab_changed.connect(on_active_brush_tab_changed)
return ui_category_brushes_nd
func _fix_duplicate_signals(copy):
copy._modify_prop("brush/brushes", copy.brushes)
copy.active_brush = copy.brushes[0]
#-------------------------------------------------------------------------------
# Input
#-------------------------------------------------------------------------------
func forwarded_input(camera, event):
var handled := false
var index_tab = -1
if is_instance_of(event, InputEventKey) && !event.pressed:
var index_map := [KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0]
index_tab = index_map.find(event.keycode)
if index_tab >= 0 && index_tab < brushes.size():
handled = true
on_active_brush_tab_changed(index_tab)
return
#-------------------------------------------------------------------------------
# Syncing the Toolshed with it's UI
#-------------------------------------------------------------------------------
func on_active_brush_tab_changed(active_tab):
var prop_action:PropAction = PA_PropSet.new("brush/active_brush", brushes[active_tab])
request_prop_action(prop_action)
func on_prop_action_executed(prop_action:PropAction, final_val):
if is_instance_of(prop_action, PA_PropSet):
if prop_action.prop == "brush/active_brush":
if tab_container_brushes_nd:
tab_container_brushes_nd.tab_changed.disconnect(on_active_brush_tab_changed)
tab_container_brushes_nd.current_tab = brushes.find(final_val)
tab_container_brushes_nd.tab_changed.connect(on_active_brush_tab_changed)
#-------------------------------------------------------------------------------
# Broadcast changes within the brushes themselves
#-------------------------------------------------------------------------------
func on_changed_brush():
emit_changed()
func on_prop_action_executed_on_brush(prop_action:PropAction, final_val, brush):
prop_action_executed_on_brush.emit(prop_action, final_val, brush)
#-------------------------------------------------------------------------------
# Property export
#-------------------------------------------------------------------------------
func set_undo_redo(val):
super.set_undo_redo(val)
for brush in brushes:
brush.set_undo_redo(_undo_redo)
func _modify_prop(prop:String, val):
match prop:
"brush/brushes":
for i in range(0, val.size()):
if !is_instance_of(val[i], Toolshed_Brush):
val[i] = Toolshed_Brush.new()
FunLib.ensure_signal(val[i].changed, on_changed_brush)
FunLib.ensure_signal(val[i].prop_action_executed, on_prop_action_executed_on_brush, [val[i]])
if val[i]._undo_redo != _undo_redo:
val[i].set_undo_redo(_undo_redo)
"brush/active_brush":
if !brushes.has(val):
if brushes.size() > 0:
val = brushes[0]
else:
val = null
return val
func _get(property):
match property:
"brush/brushes":
return brushes
"brush/active_brush":
return active_brush
return null
func _set(prop, val):
var return_val = true
val = _modify_prop(prop, val)
match prop:
"brush/brushes":
brushes = val
"brush/active_brush":
active_brush = val
_:
return_val = false
if return_val:
emit_changed()
return return_val
func _get_prop_dictionary():
return {
"brush/brushes":
{
"name": "brush/brushes",
"type": TYPE_ARRAY,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE
},
"brush/active_brush":
{
"name": "brush/active_brush",
"type": TYPE_OBJECT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE
},
}
func get_prop_tooltip(prop:String) -> String:
match prop:
"brush/brushes":
return "The list of all brushes available in this toolshed"
"brush/active_brush":
return "The brush that is currently selected and used in the painting process"
return ""

View File

@@ -0,0 +1,296 @@
@tool
extends "../utility/input_field_resource/input_field_resource.gd"
#-------------------------------------------------------------------------------
# All the data that reflects a brush behavior
#-------------------------------------------------------------------------------
const Globals = preload("../utility/globals.gd")
enum BrushType {PAINT, ERASE, SINGLE, REAPPLY}
enum OverlapMode {VOLUME, PROJECTION}
var behavior_brush_type:int = BrushType.PAINT
var shape_volume_size:float = 1.0
var shape_projection_size:float = 1.0
var behavior_strength:float = 1.0
var behavior_passthrough: bool = false
var behavior_overlap_mode: int = OverlapMode.VOLUME
var behavior_no_settings_text: String = 'This brush has no additional settings'
#-------------------------------------------------------------------------------
# Initialization
#-------------------------------------------------------------------------------
func _init(__behavior_brush_type:int = BrushType.PAINT, __behavior_strength:float = 1.0, __shape_volume_size:float = 1.0,
__shape_projection_size:float = 1.0, __behavior_passthrough: bool = false, __behavior_overlap_mode: int = OverlapMode.VOLUME):
input_field_blacklist = ['behavior/behavior_brush_type']
super()
set_meta("class", "Toolshed_Brush")
resource_name = "Toolshed_Brush"
behavior_brush_type = __behavior_brush_type
behavior_strength = __behavior_strength
shape_volume_size = __shape_volume_size
shape_projection_size = __shape_projection_size
behavior_passthrough = __behavior_passthrough
behavior_overlap_mode = __behavior_overlap_mode
func _create_input_field(_base_control:Control, _resource_previewer, prop:String) -> UI_InputField:
var input_field:UI_InputField = null
match prop:
"shape/shape_volume_size":
var max_value = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_volume_size_slider_max_value", 100.0)
var settings := {"min": 0.0, "max": max_value, "step": 0.01, "allow_greater": true, "allow_lesser": false,}
input_field = UI_IF_RealSlider.new(shape_volume_size, "Volume Size", prop, settings)
"shape/shape_projection_size":
var max_value = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_projection_size_slider_max_value", 1000.0)
var settings := {"min": 1.0, "max": max_value, "step": 1.0, "allow_greater": true, "allow_lesser": false,}
input_field = UI_IF_RealSlider.new(shape_projection_size, "Projection Size", prop, settings)
"behavior/behavior_strength":
var settings := {"min": 0.0, "max": 1.0, "step": 0.01, "allow_greater": false, "allow_lesser": false,}
input_field = UI_IF_RealSlider.new(behavior_strength, "Strength", prop, settings)
"behavior/behavior_passthrough":
input_field = UI_IF_Bool.new(behavior_passthrough, "Passthrough", prop)
"behavior/behavior_overlap_mode":
var settings := {"enum_list": FunLib.capitalize_string_array(OverlapMode.keys())}
input_field = UI_IF_Enum.new(behavior_overlap_mode, "Overlap Mode", prop, settings)
"behavior/behavior_no_settings_text":
var settings := {"label_visibility": false}
input_field = UI_IF_PlainText.new(behavior_no_settings_text, "No Settings Text", prop, settings)
return input_field
#-------------------------------------------------------------------------------
# Property export
#-------------------------------------------------------------------------------
func _modify_prop(prop:String, val):
match prop:
"behavior/behavior_strength":
val = clamp(val, 0.0, 1.0)
"behavior/behavior_overlap_mode":
match behavior_brush_type:
BrushType.PAINT, BrushType.SINGLE:
val = OverlapMode.VOLUME
"shape/shape_volume_size":
match behavior_brush_type:
BrushType.SINGLE:
val = 1.0
return val
func _set(prop, val):
var return_val = true
val = _modify_prop(prop, val)
match prop:
"behavior/behavior_brush_type":
behavior_brush_type = val
_emit_property_list_changed_notify()
"shape/shape_volume_size":
shape_volume_size = val
"shape/shape_projection_size":
shape_projection_size = val
"behavior/behavior_strength":
behavior_strength = val
"behavior/behavior_passthrough":
behavior_passthrough = val
"behavior/behavior_overlap_mode":
behavior_overlap_mode = val
_emit_property_list_changed_notify()
"behavior/behavior_no_settings_text":
behavior_no_settings_text = val
_:
return_val = false
if return_val:
emit_changed()
return return_val
func _get(prop):
match prop:
"behavior/behavior_brush_type":
return behavior_brush_type
"shape/shape_volume_size":
return shape_volume_size
"shape/shape_projection_size":
return shape_projection_size
"behavior/behavior_strength":
return behavior_strength
"behavior/behavior_passthrough":
return behavior_passthrough
"behavior/behavior_overlap_mode":
return behavior_overlap_mode
"behavior/behavior_no_settings_text":
return behavior_no_settings_text
return null
func _filter_prop_dictionary(prop_dict: Dictionary) -> Dictionary:
var props_to_hide := ["behavior/behavior_brush_type"]
match behavior_overlap_mode:
OverlapMode.VOLUME:
match behavior_brush_type:
BrushType.PAINT:
props_to_hide.append_array([
"shape/shape_projection_size",
"behavior/behavior_passthrough",
"behavior/behavior_overlap_mode",
"behavior/behavior_no_settings_text"
])
BrushType.ERASE:
props_to_hide.append_array([
"shape/shape_projection_size",
"behavior/behavior_passthrough",
"behavior/behavior_no_settings_text"
])
BrushType.SINGLE:
props_to_hide.append_array([
"shape/shape_volume_size",
"shape/shape_projection_size",
"behavior/behavior_strength",
"behavior/behavior_passthrough",
"behavior/behavior_overlap_mode",
])
BrushType.REAPPLY:
props_to_hide.append_array([
"shape/shape_projection_size",
"behavior/behavior_passthrough",
"behavior/behavior_no_settings_text"
])
OverlapMode.PROJECTION:
match behavior_brush_type:
BrushType.ERASE:
props_to_hide.append_array([
"shape/shape_volume_size",
"behavior/behavior_strength",
"behavior/behavior_no_settings_text"
])
BrushType.REAPPLY:
props_to_hide.append_array([
"shape/shape_volume_size",
"behavior/behavior_strength",
"behavior/behavior_no_settings_text"
])
for prop in props_to_hide:
prop_dict[prop].usage = PROPERTY_USAGE_NO_EDITOR
return prop_dict
func _get_prop_dictionary():
return {
"behavior/behavior_brush_type" : {
"name": "behavior/behavior_brush_type",
"type": TYPE_INT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "Paint,Erase,Single,Reapply"
},
"shape/shape_volume_size" : {
"name": "shape/shape_volume_size",
"type": TYPE_FLOAT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0.0,100.0,0.01,or_greater"
},
"shape/shape_projection_size" : {
"name": "shape/shape_projection_size",
"type": TYPE_FLOAT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "1.0,1000.0,1.0,or_greater"
},
"behavior/behavior_strength" : {
"name": "behavior/behavior_strength",
"type": TYPE_FLOAT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0.0,1.0,0.01"
},
"behavior/behavior_passthrough" : {
"name": "behavior/behavior_passthrough",
"type": TYPE_BOOL,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE,
},
"behavior/behavior_overlap_mode" : {
"name": "behavior/behavior_overlap_mode",
"type": TYPE_INT,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "Volume,Projection"
},
"behavior/behavior_no_settings_text" : {
"name": "behavior/behavior_no_settings_text",
"type": TYPE_STRING,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_NONE,
},
}
func get_prop_tooltip(prop:String) -> String:
match prop:
"behavior/behavior_brush_type":
return "The brush type enum, that defines it's behavior (paint, erase, etc.)"
"shape/shape_volume_size":
return "The diameter of this brush, in world units\n" \
+ "\n" \
+ "Can be edited by dragging in the editor viewport while holding\n" \
+ "[brush_prop_edit_button]\n" \
+ Globals.AS_IN_SETTINGS_STRING
"shape/shape_projection_size":
return "The diameter of this brush, in screen pixels\n" \
+ "\n" \
+ "Can be edited by dragging in the editor viewport while holding\n" \
+ "[brush_prop_edit_button]\n" \
+ Globals.AS_IN_SETTINGS_STRING
"behavior/behavior_strength":
return "The plant density multiplier of this brush\n" \
+ "\n" \
+ "Can be edited by dragging in the editor viewport while holding\n" \
+ "[brush_prop_edit_modifier] + [brush_prop_edit_button]\n" \
+ Globals.AS_IN_SETTINGS_STRING
"behavior/behavior_passthrough":
return "The flag, that defines whether this brush can affect plants hidden behind terrain\n" \
+ "Only active physics bodies masked by 'Gardening Collision Mask' can occlude plants\n" \
+ "In simpler terms: whatever surface volume-brush sticks to, will block a projection-brush as well\n" \
+ "\n" \
+ "Enabling Passthrough will allow this brush to ignore any collision whatsoever\n" \
+ "It also gives better performance when painting since it disables additional collision checks\n"
"behavior/behavior_overlap_mode":
return "The overlap mode enum, that defines how brush finds which plants to affect\n" \
+ "Volume brush exists in 3D world and affects whichever plants it overlaps\n" \
+ "Projection brush exists in screen-space and affects all plants that are visually inside it's area\n" \
+ "\n" \
+ "For normal painting use a Volumetric brush\n" \
+ "If you have plants stuck in mid-air (say, you moved the ground beneath them),\n" \
+ "Use a Projection brush to remove them (Volumetric brush simply won't reach them)\n" \
+ "\n" \
+ "Can be edited by pressing\n" \
+ "[brush_overlap_mode_button]\n" \
+ Globals.AS_IN_SETTINGS_STRING
return ""

View File

@@ -0,0 +1,171 @@
extends Control
const Gardener = preload("../../gardener/gardener.gd")
@onready var input_field:TextEdit = $VBoxContainer/InputField
@onready var output_field:RichTextLabel = $VBoxContainer/OutputField
@export var block_input_PTH:Array = [] # (Array, NodePath)
var block_input:Array = []
var last_mouse_mode:int
func _ready():
for node_pth in block_input_PTH:
if has_node(node_pth):
block_input.append(get_node(node_pth))
if visible:
input_field.grab_focus()
func _unhandled_input(event):
if is_instance_of(event, InputEventKey) && event.keycode == KEY_QUOTELEFT && !event.pressed:
toggle_console()
if !visible: return
if is_instance_of(event, InputEventKey):
get_viewport().set_input_as_handled()
if !event.pressed:
match event.keycode:
KEY_ENTER:
input_field.text = input_field.text.trim_suffix("\n")
try_execute_command()
KEY_ESCAPE:
toggle_console()
func toggle_console():
if !visible:
visible = true
last_mouse_mode = Input.get_mouse_mode()
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
input_field.grab_focus()
else:
visible = false
clear_command()
Input.set_mouse_mode(last_mouse_mode)
set_nodes_input_state(!visible)
func set_nodes_input_state(state:bool):
for node in block_input:
node.set_process_input(state)
func try_execute_command():
if input_field.text.is_empty(): return
var result = parse_and_execute(input_field.text)
clear_command()
print_output(result)
func clear_command():
input_field.text = ""
func print_output(string:String):
output_field.append_bbcode(string + "\n\n")
func parse_and_execute(string:String):
var args:PackedStringArray = string.split(" ")
match args[0]:
"dump_octrees":
return dump_octrees(args)
"dump_scene_tree":
return debug_scene_tree()
"clear":
output_field.text = ""
return ""
_:
return "[color=red]Undefined command[/color]"
func dump_octrees(args:Array = []):
var current_scene := get_tree().get_current_scene()
var gardener_path := ""
var octree_index := -1
if args.size() > 1:
if current_scene.has_node(args[1]) && is_instance_of(current_scene.get_node(args[1]), Gardener):
gardener_path = args[1]
else:
return "[color=red]'%s' wrong node path in argument '%d'[/color]" % [args[0], 1]
if args.size() > 2:
if args[2].is_valid_int():
octree_index = args[2].to_int()
else:
return "[color=red]'%s' wrong type in argument '%d'[/color]" % [args[0], 2]
if gardener_path.is_empty():
return dump_octrees_from_node(current_scene)
elif octree_index < 0:
return dump_octrees_from_gardener(current_scene.get_node(args[1]))
else:
return dump_octrees_at_index(current_scene.get_node(args[1]), octree_index)
func dump_octrees_from_node(node:Node):
var output := ""
if is_instance_of(node, Gardener):
output += dump_octrees_from_gardener(node)
else:
for child in node.get_children():
output += dump_octrees_from_node(child)
return output
func dump_octrees_from_gardener(gardener:Gardener):
var output := ""
for i in range(0, gardener.get_node("Arborist").octree_managers.size()):
output += dump_octrees_at_index(gardener, i)
return output
func dump_octrees_at_index(gardener:Gardener, index:int):
var output := ""
var octree_manager = gardener.get_node("Arborist").octree_managers[index]
output += octree_manager.root_octree_node.debug_dump_tree() + "\n"
return output
func debug_scene_tree():
var current_scene := get_tree().get_current_scene()
return dump_node_descendants(current_scene)
func dump_node_descendants(node:Node, intendation:int = 0):
var output := ""
var intend_str = ""
for i in range(0, intendation):
intend_str += " "
var string = "%s%s" % [intend_str, str(node)]
output += string + "\n"
intendation += 1
for child in node.get_children():
output += dump_node_descendants(child, intendation)
return output

View File

@@ -0,0 +1,35 @@
[gd_scene load_steps=4 format=3 uid="uid://cpcmbwh1aqb4x"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/utility/console/console.gd" id="1"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/utility/console/console_input_field.gd" id="4"]
[ext_resource type="Script" path="res://addons/dreadpon.spatial_gardener/utility/console/console_output.gd" id="5"]
[node name="Console" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="OutputField" type="RichTextLabel" parent="VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("5")
[node name="InputField" type="TextEdit" parent="VBoxContainer"]
custom_minimum_size = Vector2(0, 80)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
caret_blink = true
script = ExtResource("4")

View File

@@ -0,0 +1,17 @@
@tool
extends TextEdit
func _process(delta):
if visible:
call_deferred("_hide_scrollbar")
func _hide_scrollbar():
for child in get_children():
if is_instance_of(child, VScrollBar):
child.visible = false
elif is_instance_of(child, HScrollBar):
child.visible = false

View File

@@ -0,0 +1,12 @@
@tool
extends RichTextLabel
@export var scrollbar_size = 24
func _ready():
for child in get_children():
if is_instance_of(child, VScrollBar):
child.custom_minimum_size.x = scrollbar_size
elif is_instance_of(child, HScrollBar):
child.custom_minimum_size.y = scrollbar_size

View File

@@ -0,0 +1,9 @@
[gd_resource type="FontFile" load_steps=2 format=2]
[ext_resource path="res://addons/dreadpon.spatial_gardener/utility/console/fonts/ttf/AnonymousPro-Regular.ttf" type="FontFile" id=1]
[resource]
size = 100
use_filter = true
extra_spacing_char = 4
font_data = ExtResource( 1 )

View File

@@ -0,0 +1,8 @@
[gd_resource type="FontFile" load_steps=2 format=2]
[ext_resource path="res://addons/dreadpon.spatial_gardener/utility/console/fonts/ttf/Urbanist-Regular.ttf" type="FontFile" id=1]
[resource]
size = 64
use_filter = true
font_data = ExtResource( 1 )

View File

@@ -0,0 +1,6 @@
[gd_resource type="FontVariation" load_steps=2 format=3 uid="uid://dokuyg8sikplj"]
[ext_resource type="FontFile" path="res://addons/dreadpon.spatial_gardener/utility/console/fonts/dynamic/urbanist_regular.tres" id="1_h16sp"]
[resource]
base_font = ExtResource("1_h16sp")

View File

@@ -0,0 +1,34 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://btfidab8p3wbu"
path="res://.godot/imported/AnonymousPro-Regular.ttf-208ecbb66c73e515f403daaeae3d36ae.fontdata"
[deps]
source_file="res://addons/dreadpon.spatial_gardener/utility/console/fonts/ttf/AnonymousPro-Regular.ttf"
dest_files=["res://.godot/imported/AnonymousPro-Regular.ttf-208ecbb66c73e515f403daaeae3d36ae.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,34 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://btvadi4yk0a3y"
path="res://.godot/imported/Urbanist-Regular.ttf-43977583ef0eb0baed07360c77f30dfb.fontdata"
[deps]
source_file="res://addons/dreadpon.spatial_gardener/utility/console/fonts/ttf/Urbanist-Regular.ttf"
dest_files=["res://.godot/imported/Urbanist-Regular.ttf-43977583ef0eb0baed07360c77f30dfb.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,321 @@
extends Node
#-------------------------------------------------------------------------------
# A debug tool that draws lines and shapes in 3D space
# Can be used as an autoload script and remove drawn shapes after a delay
# Or as a static tool for creating geometry and attaching it to a scene
#-------------------------------------------------------------------------------
var active_geometry:Array = []
var cached_geometry:Array = []
func _init():
set_meta("class", "DponDebugDraw")
# Instantiation through autoload allows to clear geometry after a delay
func _process(delta):
var removed_active_geometry := []
for data in active_geometry:
if data.lifetime < 0.0:
continue
data.lifetime -= delta
if data.lifetime <= 0.0:
removed_active_geometry.append(data)
for data in removed_active_geometry:
active_geometry.erase(data)
if is_instance_valid(data.geometry):
data.geometry.queue_free()
for data in cached_geometry:
active_geometry.append(data)
cached_geometry = []
func _notification(what):
if what == NOTIFICATION_PREDELETE:
# Avoid memory leaks
clear_cached_geometry()
# Manual clear for active geometry
func clear_cached_geometry():
var removed_active_geometry := []
for data in active_geometry:
removed_active_geometry.append(data)
for data in removed_active_geometry:
active_geometry.erase(data)
if is_instance_valid(data.geometry):
data.geometry.queue_free()
# Draw a polygonal 3D line
# And set it on a timer
func draw_line(start:Vector3, end:Vector3, color:Color, node_context:Node3D, width:float = 0.1, lifetime := 0.0):
var geom = static_draw_line(start,end,color,node_context)
cached_geometry.append({"geometry": geom, "lifetime": lifetime})
# Draw a polygonal 3D line
# Origin represents line's start position, not it's center
static func static_draw_line(start:Vector3, end:Vector3, color:Color, node_context:Node3D, width:float = 0.1) -> MeshInstance3D:
if node_context == null: return null
var geom = ImmediateMesh.new()
var mesh_inst := MeshInstance3D.new()
var half_width = width * 0.5
var length = (end - start).length()
var z_axis = (end - start).normalized()
var y_axis = Vector3(0, 1, 0)
if abs(z_axis.dot(y_axis)) >= 0.9:
y_axis = Vector3(1, 0, 0)
var x_axis = y_axis.cross(z_axis)
y_axis = z_axis.cross(x_axis)
geom.global_transform.origin = start
geom.global_transform.basis = Basis(x_axis, y_axis, z_axis).orthonormalized()
var points := PackedVector3Array()
points.append_array([
Vector3(-half_width, half_width, 0),
Vector3(half_width, half_width, 0),
Vector3(half_width, -half_width, 0),
Vector3(-half_width, -half_width, 0),
Vector3(-half_width, half_width, length),
Vector3(half_width, half_width, length),
Vector3(half_width, -half_width, length),
Vector3(-half_width, -half_width, length)
])
geom.begin(PrimitiveMesh.PRIMITIVE_TRIANGLES)
geom.add_vertex(points[0])
geom.add_vertex(points[5])
geom.add_vertex(points[4])
geom.add_vertex(points[0])
geom.add_vertex(points[1])
geom.add_vertex(points[5])
geom.add_vertex(points[1])
geom.add_vertex(points[6])
geom.add_vertex(points[5])
geom.add_vertex(points[1])
geom.add_vertex(points[2])
geom.add_vertex(points[6])
geom.add_vertex(points[2])
geom.add_vertex(points[7])
geom.add_vertex(points[6])
geom.add_vertex(points[2])
geom.add_vertex(points[3])
geom.add_vertex(points[7])
geom.add_vertex(points[4])
geom.add_vertex(points[3])
geom.add_vertex(points[0])
geom.add_vertex(points[4])
geom.add_vertex(points[7])
geom.add_vertex(points[3])
geom.add_vertex(points[0])
geom.add_vertex(points[2])
geom.add_vertex(points[1])
geom.add_vertex(points[0])
geom.add_vertex(points[3])
geom.add_vertex(points[2])
geom.add_vertex(points[5])
geom.add_vertex(points[7])
geom.add_vertex(points[4])
geom.add_vertex(points[5])
geom.add_vertex(points[6])
geom.add_vertex(points[7])
geom.end()
geom.material_override = StandardMaterial3D.new()
geom.material_override.flags_unshaded = true
geom.material_override.albedo_color = color
mesh_inst.mesh = geom
node_context.add_child(mesh_inst)
return mesh_inst
# Draw a line cube
# And set it on a timer
func draw_cube(pos:Vector3, size:Vector3, rotation:Quaternion, color:Color, node_context:Node3D, lifetime := 0.0):
var geom = static_draw_cube(pos, size, rotation, color, node_context)
cached_geometry.append({"geometry": geom, "lifetime": lifetime})
# Draw a line cube
static func static_draw_cube(pos:Vector3, size:Vector3, rotation:Quaternion, color:Color, node_context:Node3D):
if node_context == null: return
var mesh_instance = MeshInstance3D.new()
mesh_instance.transform.basis = Basis(rotation)
mesh_instance.transform.origin = pos
node_context.add_child(mesh_instance)
mesh_instance.mesh = generate_cube(size, color)
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
return mesh_instance
# Generate a line cube's ArrayMesh
static func generate_cube(size:Vector3, color:Color):
var mesh := ArrayMesh.new()
var extents = size * 0.5
var points := PackedVector3Array()
points.append_array([
Vector3(-extents.x, -extents.y, -extents.z),
Vector3(-extents.x, -extents.y, extents.z),
Vector3(-extents.x, extents.y, extents.z),
Vector3(-extents.x, extents.y, -extents.z),
Vector3(extents.x, -extents.y, -extents.z),
Vector3(extents.x, -extents.y, extents.z),
Vector3(extents.x, extents.y, extents.z),
Vector3(extents.x, extents.y, -extents.z),
])
var vertices := PackedVector3Array()
vertices.append_array([
points[0], points[1],
points[1], points[2],
points[2], points[3],
points[3], points[0],
points[4], points[5],
points[5], points[6],
points[6], points[7],
points[7], points[4],
points[0], points[4],
points[1], points[5],
points[2], points[6],
points[3], points[7],
])
var colors := PackedColorArray()
for i in range(0, 24):
colors.append(color)
var arrays = []
arrays.resize(ArrayMesh.ARRAY_MAX)
arrays[ArrayMesh.ARRAY_VERTEX] = vertices
arrays[ArrayMesh.ARRAY_COLOR] = colors
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays)
var material := StandardMaterial3D.new()
material.flags_unshaded = true
material.vertex_color_use_as_albedo = true
mesh.surface_set_material(0, material)
return mesh
# Draw a line plane
# And set it on a timer
func draw_plane(pos:Vector3, size:float, normal:Vector3, color:Color, node_context:Node3D, normal_length: float = 1.0, up_vector: Vector3 = Vector3.UP, lifetime := 0.0):
var geom = static_draw_plane(pos, size, normal, color, node_context)
cached_geometry.append({"geometry": geom, "lifetime": lifetime})
# Draw a line cube
static func static_draw_plane(pos:Vector3, size:float, normal:Vector3, color:Color, node_context:Node3D, normal_length: float = 1.0, up_vector: Vector3 = Vector3.UP):
if node_context == null: return
normal = normal.normalized()
var mesh_instance = MeshInstance3D.new()
var basis = Basis()
basis.z = normal
basis.x = normal.cross(up_vector)
basis.y = basis.x.cross(normal)
basis.x = normal.cross(basis.y)
mesh_instance.transform.basis = basis.orthonormalized()
mesh_instance.transform.origin = pos
node_context.add_child(mesh_instance)
mesh_instance.mesh = generate_plane(size, color, normal_length)
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
return mesh_instance
# Generate a line cube's ArrayMesh
static func generate_plane(size:float, color:Color, normal_length: float):
var mesh := ArrayMesh.new()
var extent = size * 0.5
var points := PackedVector3Array()
points.append_array([
Vector3(-extent, -extent, 0),
Vector3(-extent, extent, 0),
Vector3(extent, extent, 0),
Vector3(extent, -extent, 0),
Vector3(0, 0, 0),
Vector3(0, 0, normal_length),
Vector3(-extent, -extent, 0),
Vector3(-extent, -extent, normal_length),
Vector3(-extent, extent, 0),
Vector3(-extent, extent, normal_length),
Vector3(extent, extent, 0),
Vector3(extent, extent, normal_length),
Vector3(extent, -extent, 0),
Vector3(extent, -extent, normal_length),
])
var vertices := PackedVector3Array()
vertices.append_array([
points[0], points[1],
points[1], points[2],
points[2], points[3],
points[3], points[0],
points[0], points[2],
points[1], points[3],
points[4], points[5],
points[6], points[7],
points[8], points[9],
points[10], points[11],
points[12], points[13],
])
var colors := PackedColorArray()
for i in range(0, vertices.size()):
colors.append(color)
var arrays = []
arrays.resize(ArrayMesh.ARRAY_MAX)
arrays[ArrayMesh.ARRAY_VERTEX] = vertices
arrays[ArrayMesh.ARRAY_COLOR] = colors
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays)
var material := StandardMaterial3D.new()
material.flags_unshaded = true
material.vertex_color_use_as_albedo = true
mesh.surface_set_material(0, material)
return mesh

View File

@@ -0,0 +1,25 @@
@tool
#-------------------------------------------------------------------------------
# A list of default variables
#-------------------------------------------------------------------------------
const Toolshed = preload("../toolshed/toolshed.gd")
const Toolshed_Brush = preload("../toolshed/toolshed_brush.gd")
# A default Toolshed
# TODO: this belongs in toolshed.gd, but for now calling new() from a static function isn't possible
# This seems to be the most recent pull request, but it's almost a year old and still isn't merged yet...
# https://github.com/godotengine/godot/pull/54457
static func DEFAULT_TOOLSHED():
return Toolshed.new([
Toolshed_Brush.new(Toolshed_Brush.BrushType.PAINT, 1.0, 10.0),
Toolshed_Brush.new(Toolshed_Brush.BrushType.ERASE, 1.0, 10.0, 100.0),
Toolshed_Brush.new(Toolshed_Brush.BrushType.SINGLE, 1.0, 1.0),
Toolshed_Brush.new(Toolshed_Brush.BrushType.REAPPLY, 1.0, 10.0, 100.0)
])

View File

@@ -0,0 +1,36 @@
extends RefCounted
class_name DPON_FM
static var ED_EditorUndoRedoManager = null
static var ED_EditorFileDialog = null
static var _class_map: Dictionary = {}
static func setup():
ED_EditorUndoRedoManager = get_native_class("EditorUndoRedoManager")
ED_EditorFileDialog = get_native_class("EditorFileDialog")
_class_map = {}
if ED_EditorUndoRedoManager:
_class_map["EditorUndoRedoManager"] = ED_EditorUndoRedoManager
if ED_EditorFileDialog:
_class_map["EditorFileDialog"] = ED_EditorFileDialog
static func is_instance_of_ed(instance: Variant, str_lass_name: String) -> bool:
if _class_map.has(str_lass_name):
return is_instance_of(instance, _class_map[str_lass_name])
return false
static func get_native_class(str_lass_name: String) -> Variant:
if ClassDB.class_exists(str_lass_name):
var script := GDScript.new()
var func_name := &"get_class_by_str"
script.source_code = "@tool\nextends RefCounted\nstatic func %s() -> Variant: return %s\n" % [func_name, str_lass_name]
script.reload()
return script.call(func_name)
return null

View File

@@ -0,0 +1,417 @@
@tool
#-------------------------------------------------------------------------------
# A miscellaneous FUNction LIBrary
#-------------------------------------------------------------------------------
const Logger = preload("logger.gd")
const Globals = preload("globals.gd")
enum TimeTrimMode {NONE, EXACT, EXTRA_ONE, KEEP_ONE, KEEP_TWO, KEEP_THREE}
#-------------------------------------------------------------------------------
# Nodes
#-------------------------------------------------------------------------------
# Remove all children from node and free them
static func free_children(node):
if !is_instance_valid(node): return
for child in node.get_children().duplicate():
node.remove_child(child)
child.queue_free()
# Remove all children from node
static func remove_children(node):
if !is_instance_valid(node): return
for child in node.get_children().duplicate():
node.remove_child(child)
# A shorthand for checking/connecting a signal
# Kinda wish Godot had a built-in one
static func ensure_signal(_signal:Signal, callable: Callable, binds:Array = [], flags:int = 0):
if !_signal.is_connected(callable):
_signal.connect(callable.bindv(binds), flags)
static func disconnect_all(_signal: Signal):
for connection_data in _signal.get_connections():
connection_data["signal"].disconnect(connection_data.callable)
#-------------------------------------------------------------------------------
# Strings
#-------------------------------------------------------------------------------
# Capitalize all strings in an array
static func capitalize_string_array(array:Array):
var narray = array.duplicate()
for i in range(0, narray.size()):
if narray[i] is String:
narray[i] = narray[i].capitalize()
return narray
# Build a property hint_string out of strings in an array
static func make_hint_string(array:Array):
var string = ""
for i in range(0, array.size()):
if array[i] is String:
string += array[i]
if i < array.size() - 1:
string += ","
return string
# Convert to custom string format, context-dependent but independednt to changes to Godot's var_to_str
static func vec3_to_str(val: Vector3) -> String:
return "%f, %f, %f" % [val.x, val.y, val.z]
# Convert to custom string format, context-dependent but independednt to changes to Godot's var_to_str
static func transform3d_to_str(val: Transform3D) -> String:
return "%f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f" % [
val.basis.x.x, val.basis.x.y, val.basis.x.z,
val.basis.y.x, val.basis.y.y, val.basis.y.z,
val.basis.z.x, val.basis.z.y, val.basis.z.z,
val.origin.x, val.origin.y, val.origin.z
]
# Convert from custom string format
static func str_to_vec3(string: String, str_version: int) -> Vector3:
match str_version:
0:
var split = string.trim_prefix('(').trim_suffix(')').split_floats(', ')
return Vector3(split[0], split[1], split[2])
1:
var split = string.split_floats(', ')
return Vector3(split[0], split[1], split[2])
_:
push_error("Unsupported str version: %d" % [str_version])
return Vector3.ZERO
# Convert from custom string format
static func str_to_transform3d(string: String, str_version: int) -> Transform3D:
match str_version:
0:
string = string.replace(' - ', ', ')
var split = string.split_floats(', ')
return Transform3D(
Vector3(split[0], split[3], split[6]),
Vector3(split[1], split[4], split[7]),
Vector3(split[2], split[5], split[8]),
Vector3(split[9], split[10], split[11]))
1:
var split = string.split_floats(', ')
return Transform3D(
Vector3(split[0], split[3], split[6]),
Vector3(split[1], split[4], split[7]),
Vector3(split[2], split[5], split[8]),
Vector3(split[9], split[10], split[11]))
_:
push_error("Unsupported str version: %d" % [str_version])
return Transform3D()
#-------------------------------------------------------------------------------
# Math
#-------------------------------------------------------------------------------
# Clamp a value
# Automatically decide which value is min and which is max
static func clamp_auto(value, min_value, max_value):
var direction = 1.0 if min_value <= max_value else -1.0
if direction >= 0:
if value < min_value:
return min_value
elif value > max_value:
return max_value
else:
if value > min_value:
return min_value
elif value < max_value:
return max_value
return value
# Clamp all Vector3 properties individually
static func clamp_vector3(value:Vector3, min_value:Vector3, max_value:Vector3):
var result = Vector3()
result.x = clamp_auto(value.x, min_value.x, max_value.x)
result.y = clamp_auto(value.y, min_value.y, max_value.y)
result.z = clamp_auto(value.z, min_value.z, max_value.z)
return result
# Lerp all Vector3 properties by 3 independent weights
static func vector_tri_lerp(from:Vector3, to:Vector3, weight:Vector3):
return Vector3(
lerp(from.x, to.x, weight.x),
lerp(from.y, to.y, weight.y),
lerp(from.z, to.z, weight.z)
)
#-------------------------------------------------------------------------------
# Time
#-------------------------------------------------------------------------------
static func get_msec():
return Time.get_ticks_msec()
static func msec_to_time(msec:int = -1, include_msec:bool = true, trim_mode:int = TimeTrimMode.NONE):
if msec < 0:
msec = get_msec()
var time_units := [msec % 1000, msec / 1000 % 60, msec / 1000 / 60 % 60, msec / 1000 / 60 / 60 % 24]
var string = ""
if trim_mode != TimeTrimMode.NONE:
for i in range(time_units.size() - 1, -1, -1):
match trim_mode:
TimeTrimMode.EXACT:
if time_units[i] <= 0:
time_units.remove_at(i)
else:
break
TimeTrimMode.EXTRA_ONE:
if time_units[i] > 0:
break
if i + 1 < time_units.size() && time_units[i + 1] <= 0:
time_units.remove_at(i + 1)
TimeTrimMode.KEEP_ONE:
if i >= 1:
time_units.remove_at(i)
TimeTrimMode.KEEP_TWO:
if i >= 2:
time_units.remove_at(i)
TimeTrimMode.KEEP_THREE:
if i >= 3:
time_units.remove_at(i)
for i in range(0, time_units.size()):
var time_unit:int = time_units[i]
if i == 0:
if !include_msec: continue
string = "%03d" % [time_units[i]]
else:
string = string.insert(0, "%02d:" % time_units[i])
string = string.trim_suffix(":")
return string
static func print_system_time(suffix:String = ""):
print("[%s] : %s" % [Time.get_time_string_from_system(), suffix])
#-------------------------------------------------------------------------------
# Object class comparison
#-------------------------------------------------------------------------------
static func get_obj_class_string(obj:Object) -> String:
if obj == null: return ""
assert(is_instance_of(obj, Object))
if obj.has_meta("class"):
return obj.get_meta("class")
elif obj.get_script():
return obj.get_script().get_instance_base_type()
else:
return obj.get_class()
static func are_same_class(one:Object, two:Object) -> bool:
if one == null: return false
if two == null: return false
assert(is_instance_of(one, Object) && is_instance_of(two, Object))
# print("1 %s, 2 %s" % [one.get_class(), two.get_class()])
if one.get_script() && two.get_script() && one.get_script() == two.get_script():
return true
elif one.has_meta("class") && two.has_meta("class") && one.get_meta("class") == two.get_meta("class"):
return true
elif one.get_class() == two.get_class():
return true
# elif !one.is_class(two.get_class()):
# return true
return false
static func obj_is_script(obj:Object, script:Script) -> bool:
if obj == null: return false
assert(is_instance_of(obj, Object))
return obj.get_script() && obj.get_script() == script
static func obj_is_class_string(obj:Object, class_string:String) -> bool:
if obj == null: return false
assert(is_instance_of(obj, Object))
if obj.get_class() == class_string:
return true
elif obj.has_meta("class") && obj.get_meta("class") == class_string:
return true
return false
#-------------------------------------------------------------------------------
# Configuration
#-------------------------------------------------------------------------------
# This is here to avoid circular reference lol
static func get_setting_safe(setting:String, default_value = null):
if ProjectSettings.has_setting(setting):
return ProjectSettings.get_setting(setting)
return default_value
#-------------------------------------------------------------------------------
# Asset management
#-------------------------------------------------------------------------------
static func save_res(res:Resource, dir:String, res_name:String):
assert(res)
var logger = Logger.get_for_string("FunLib")
var full_path = combine_dir_and_file(dir, res_name)
if !is_dir_valid(dir):
logger.warn("Unable to save '%s', directory is invalid!" % [full_path])
return
# Abort explicit saving if our resource and an existing one are the same instance
# Since it will be saved on 'Ctrl+S' implicitly by the editor
# And allows reverting resource by exiting the editor
var loaded_res = load_res(dir, res_name, false, true)
if res == loaded_res:
return
# There was a wall of text here regarding problems of saving and re-saving custom resources
# But curiously, seems like it went away
# These comments and previous state of saving/loading logic is available on commit '7b127ad'
# Taking over path and subpaths is still required
# Still keeping FLAG_CHANGE_PATH in case we want to save to a different location
res.take_over_path(full_path)
var err = ResourceSaver.save(res, full_path, ResourceSaver.FLAG_CHANGE_PATH | ResourceSaver.FLAG_REPLACE_SUBRESOURCE_PATHS)
if err != OK:
logger.error("Could not save '%s', error %s!" % [full_path, Globals.get_err_message(err)])
# Passing 'true' as 'no_cache' is important to bypass this cache
# We use it by default, but want to allow loading a cache to check if resource exists at path
static func load_res(dir:String, res_name:String, no_cache: bool = true, silent: bool = false) -> Resource:
var full_path = combine_dir_and_file(dir, res_name)
var res = null
var logger = Logger.get_for_string("FunLib")
if ResourceLoader.exists(full_path):
res = ResourceLoader.load(full_path, "", ResourceLoader.CacheMode.CACHE_MODE_REPLACE if no_cache else ResourceLoader.CacheMode.CACHE_MODE_REUSE)
else:
if !silent: logger.warn("Path '%s', doesn't exist!" % [full_path])
if !res:
if !is_dir_valid(dir) || res_name == "":
if !silent: logger.warn("Could not load '%s', error %s!" % [full_path, Globals.get_err_message(ERR_FILE_BAD_PATH)])
else:
if !silent: logger.warn("Could not load '%s'!" % [full_path])
return res
static func remove_res(dir:String, res_name:String):
var full_path = combine_dir_and_file(dir, res_name)
var abs_path = ProjectSettings.globalize_path(full_path)
var err = DirAccess.remove_absolute(abs_path)
var logger = Logger.get_for_string("FunLib")
if err != OK:
logger.error("Could not remove '%s', error %s!" % [abs_path, Globals.get_err_message(err)])
static func combine_dir_and_file(dir_path: String, file_name: String):
if !dir_path.is_empty() && !dir_path.ends_with("/"):
dir_path += "/"
return "%s%s" % [dir_path, file_name]
static func is_dir_valid(dir):
return !dir.is_empty() && dir != "/" && DirAccess.dir_exists_absolute(dir)
#-------------------------------------------------------------------------------
# Filesystem
#-------------------------------------------------------------------------------
static func remove_dir_recursive(path, keep_first:bool = false) -> bool:
var dir = DirAccess.open(path)
if dir:
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
if dir.current_is_dir():
if !remove_dir_recursive(path + "/" + file_name, false):
return false
else:
dir.remove(file_name)
file_name = dir.get_next()
if !keep_first:
dir.remove(path)
return true
return false
static func iterate_files(dir_path: String, deep: bool, obj: Object, method_name: String, payload):
if !is_instance_valid(obj):
assert('Object instace invalid!')
return
if !obj.has_method(method_name):
assert('%s does not have a method named "%s"!' % [str(obj), method_name])
return
var dir = DirAccess.open(dir_path)
if dir_path.ends_with('/'):
dir_path = dir_path.trim_suffix('/')
if dir:
dir.list_dir_begin()
var full_path = ''
var file_name = dir.get_next()
while file_name != '':
full_path = dir_path + "/" + file_name
if deep && dir.current_is_dir():
iterate_files(full_path, deep, obj, method_name, payload)
else:
obj.call(method_name, full_path, payload)
file_name = dir.get_next()

View File

@@ -0,0 +1,331 @@
@tool
#-------------------------------------------------------------------------------
# A list of global consts with methods to work with them
# A mirror of some of GlobalScope enums
# Because they can't be accessed as "enum" and only as "const int"
# And I need "enums" to expose them to ProjectSettings
#-------------------------------------------------------------------------------
# Convert index starting from "0" to an enum value, where first index is the first enum value
# E.g. for KeyboardKey, index of "0" would represent a value of "SPKEY | 0x01" or simply "16777217")
static func index_to_enum(index:int, enum_dict:Dictionary):
return enum_dict.values()[index]
# The opposite of index_to_enum()
static func enum_to_index(enum_val:int, enum_dict:Dictionary):
return enum_dict.values().find(enum_val)
# Access and format an error message
static func get_err_message(err_code):
return str("[", err_code, "]: ", Error[err_code])
# Controls per how many units is density calculated
const PLANT_DENSITY_UNITS:int = 100
# A string to be used in tooltips/hints regarding plugin settings
const AS_IN_SETTINGS_STRING:String = "As specified in 'Project' -> 'Project Settings' -> 'Dreadpons Node3D Gardener'"
# KeyboardKey
# Taken from https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html
enum KeyboardKey {
KEY_NONE = 0,
KEY_SPECIAL = 4194304,
KEY_ESCAPE = 4194305,
KEY_TAB = 4194306,
KEY_BACKTAB = 4194307,
KEY_BACKSPACE = 4194308,
KEY_ENTER = 4194309,
KEY_KP_ENTER = 4194310,
KEY_INSERT = 4194311,
KEY_DELETE = 4194312,
KEY_PAUSE = 4194313,
KEY_PRINT = 4194314,
KEY_SYSREQ = 4194315,
KEY_CLEAR = 4194316,
KEY_HOME = 4194317,
KEY_END = 4194318,
KEY_LEFT = 4194319,
KEY_UP = 4194320,
KEY_RIGHT = 4194321,
KEY_DOWN = 4194322,
KEY_PAGEUP = 4194323,
KEY_PAGEDOWN = 4194324,
KEY_SHIFT = 4194325,
KEY_CTRL = 4194326,
KEY_META = 4194327,
KEY_ALT = 4194328,
KEY_CAPSLOCK = 4194329,
KEY_NUMLOCK = 4194330,
KEY_SCROLLLOCK = 4194331,
KEY_F1 = 4194332,
KEY_F2 = 4194333,
KEY_F3 = 4194334,
KEY_F4 = 4194335,
KEY_F5 = 4194336,
KEY_F6 = 4194337,
KEY_F7 = 4194338,
KEY_F8 = 4194339,
KEY_F9 = 4194340,
KEY_F10 = 4194341,
KEY_F11 = 4194342,
KEY_F12 = 4194343,
KEY_F13 = 4194344,
KEY_F14 = 4194345,
KEY_F15 = 4194346,
KEY_F16 = 4194347,
KEY_F17 = 4194348,
KEY_F18 = 4194349,
KEY_F19 = 4194350,
KEY_F20 = 4194351,
KEY_F21 = 4194352,
KEY_F22 = 4194353,
KEY_F23 = 4194354,
KEY_F24 = 4194355,
KEY_F25 = 4194356,
KEY_F26 = 4194357,
KEY_F27 = 4194358,
KEY_F28 = 4194359,
KEY_F29 = 4194360,
KEY_F30 = 4194361,
KEY_F31 = 4194362,
KEY_F32 = 4194363,
KEY_F33 = 4194364,
KEY_F34 = 4194365,
KEY_F35 = 4194366,
KEY_KP_MULTIPLY = 4194433,
KEY_KP_DIVIDE = 4194434,
KEY_KP_SUBTRACT = 4194435,
KEY_KP_PERIOD = 4194436,
KEY_KP_ADD = 4194437,
KEY_KP_0 = 4194438,
KEY_KP_1 = 4194439,
KEY_KP_2 = 4194440,
KEY_KP_3 = 4194441,
KEY_KP_4 = 4194442,
KEY_KP_5 = 4194443,
KEY_KP_6 = 4194444,
KEY_KP_7 = 4194445,
KEY_KP_8 = 4194446,
KEY_KP_9 = 4194447,
KEY_MENU = 4194370,
KEY_HYPER = 4194371,
KEY_HELP = 4194373,
KEY_BACK = 4194376,
KEY_FORWARD = 4194377,
KEY_STOP = 4194378,
KEY_REFRESH = 4194379,
KEY_VOLUMEDOWN = 4194380,
KEY_VOLUMEMUTE = 4194381,
KEY_VOLUMEUP = 4194382,
KEY_MEDIAPLAY = 4194388,
KEY_MEDIASTOP = 4194389,
KEY_MEDIAPREVIOUS = 4194390,
KEY_MEDIANEXT = 4194391,
KEY_MEDIARECORD = 4194392,
KEY_HOMEPAGE = 4194393,
KEY_FAVORITES = 4194394,
KEY_SEARCH = 4194395,
KEY_STANDBY = 4194396,
KEY_OPENURL = 4194397,
KEY_LAUNCHMAIL = 4194398,
KEY_LAUNCHMEDIA = 4194399,
KEY_LAUNCH0 = 4194400,
KEY_LAUNCH1 = 4194401,
KEY_LAUNCH2 = 4194402,
KEY_LAUNCH3 = 4194403,
KEY_LAUNCH4 = 4194404,
KEY_LAUNCH5 = 4194405,
KEY_LAUNCH6 = 4194406,
KEY_LAUNCH7 = 4194407,
KEY_LAUNCH8 = 4194408,
KEY_LAUNCH9 = 4194409,
KEY_LAUNCHA = 4194410,
KEY_LAUNCHB = 4194411,
KEY_LAUNCHC = 4194412,
KEY_LAUNCHD = 4194413,
KEY_LAUNCHE = 4194414,
KEY_LAUNCHF = 4194415,
KEY_UNKNOWN = 8388607,
KEY_SPACE = 32,
KEY_EXCLAM = 33,
KEY_QUOTEDBL = 34,
KEY_NUMBERSIGN = 35,
KEY_DOLLAR = 36,
KEY_PERCENT = 37,
KEY_AMPERSAND = 38,
KEY_APOSTROPHE = 39,
KEY_PARENLEFT = 40,
KEY_PARENRIGHT = 41,
KEY_ASTERISK = 42,
KEY_PLUS = 43,
KEY_COMMA = 44,
KEY_MINUS = 45,
KEY_PERIOD = 46,
KEY_SLASH = 47,
KEY_0 = 48,
KEY_1 = 49,
KEY_2 = 50,
KEY_3 = 51,
KEY_4 = 52,
KEY_5 = 53,
KEY_6 = 54,
KEY_7 = 55,
KEY_8 = 56,
KEY_9 = 57,
KEY_COLON = 58,
KEY_SEMICOLON = 59,
KEY_LESS = 60,
KEY_EQUAL = 61,
KEY_GREATER = 62,
KEY_QUESTION = 63,
KEY_AT = 64,
KEY_A = 65,
KEY_B = 66,
KEY_C = 67,
KEY_D = 68,
KEY_E = 69,
KEY_F = 70,
KEY_G = 71,
KEY_H = 72,
KEY_I = 73,
KEY_J = 74,
KEY_K = 75,
KEY_L = 76,
KEY_M = 77,
KEY_N = 78,
KEY_O = 79,
KEY_P = 80,
KEY_Q = 81,
KEY_R = 82,
KEY_S = 83,
KEY_T = 84,
KEY_U = 85,
KEY_V = 86,
KEY_W = 87,
KEY_X = 88,
KEY_Y = 89,
KEY_Z = 90,
KEY_BRACKETLEFT = 91,
KEY_BACKSLASH = 92,
KEY_BRACKETRIGHT = 93,
KEY_ASCIICIRCUM = 94,
KEY_UNDERSCORE = 95,
KEY_QUOTELEFT = 96,
KEY_BRACELEFT = 123,
KEY_BAR = 124,
KEY_BRACERIGHT = 125,
KEY_ASCIITILDE = 126,
KEY_YEN = 165,
KEY_SECTION = 167,
KEY_GLOBE = 4194416,
KEY_KEYBOARD = 4194417,
KEY_JIS_EISU = 4194418,
KEY_JIS_KANA = 4194419
}
# KeyModifierMask
# Taken from https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html
enum {
KEY_CODE_MASK = 8388607,
KEY_MODIFIER_MASK = 532676608,
KEY_MASK_CMD_OR_CTRL = 16777216,
KEY_MASK_SHIFT = 33554432,
KEY_MASK_ALT = 67108864,
KEY_MASK_META = 134217728,
KEY_MASK_CTRL = 268435456,
KEY_MASK_KPAD = 536870912,
KEY_MASK_GROUP_SWITCH = 1073741824
}
# MouseButton
# Taken from https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html
enum MouseButton {
MOUSE_BUTTON_NONE = 0,
MOUSE_BUTTON_LEFT = 1,
MOUSE_BUTTON_RIGHT = 2,
MOUSE_BUTTON_MIDDLE = 3,
MOUSE_BUTTON_WHEEL_UP = 4,
MOUSE_BUTTON_WHEEL_DOWN = 5,
MOUSE_BUTTON_WHEEL_LEFT = 6,
MOUSE_BUTTON_WHEEL_RIGHT = 7,
MOUSE_BUTTON_XBUTTON1 = 8,
MOUSE_BUTTON_XBUTTON2 = 9,
}
# MouseButtonMask
# Taken from https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html
enum MouseButtonMask {
MOUSE_BUTTON_MASK_LEFT = (1 << (MOUSE_BUTTON_LEFT - 1)),
MOUSE_BUTTON_MASK_RIGHT = (1 << (MOUSE_BUTTON_RIGHT - 1)),
MOUSE_BUTTON_MASK_MIDDLE = (1 << (MOUSE_BUTTON_MIDDLE - 1)),
MOUSE_BUTTON_MASK_XBUTTON1 = (1 << (MOUSE_BUTTON_XBUTTON1 - 1)),
MOUSE_BUTTON_MASK_XBUTTON2 = (1 << (MOUSE_BUTTON_XBUTTON2 - 1))
}
# Error
# Taken from https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html
const Error = {
OK: "OK",
FAILED: "Generic error",
ERR_UNAVAILABLE: "Unavailable error",
ERR_UNCONFIGURED: "Unconfigured error",
ERR_UNAUTHORIZED: "Unauthorized error",
ERR_PARAMETER_RANGE_ERROR: "Parameter range error",
ERR_OUT_OF_MEMORY: "Out of memory (OOM) error",
ERR_FILE_NOT_FOUND: "File: Not found error",
ERR_FILE_BAD_DRIVE: "File: Bad drive error",
ERR_FILE_BAD_PATH: "File: Bad path error",
ERR_FILE_NO_PERMISSION: "File: No permission error",
ERR_FILE_ALREADY_IN_USE: "File: Already in use error",
ERR_FILE_CANT_OPEN: "File: Can't open error",
ERR_FILE_CANT_WRITE: "File: Can't write error",
ERR_FILE_CANT_READ: "File: Can't read error",
ERR_FILE_UNRECOGNIZED: "File: Unrecognized error",
ERR_FILE_CORRUPT: "File: Corrupt error",
ERR_FILE_MISSING_DEPENDENCIES: "File: Missing dependencies error",
ERR_FILE_EOF: "File: End of file (EOF) error",
ERR_CANT_OPEN: "Can't open error",
ERR_CANT_CREATE: "Can't create error",
ERR_QUERY_FAILED: "Query failed error",
ERR_ALREADY_IN_USE: "Already in use error",
ERR_LOCKED: "Locked error",
ERR_TIMEOUT: "Timeout error",
ERR_CANT_CONNECT: "Can't connect error",
ERR_CANT_RESOLVE: "Can't resolve error",
ERR_CONNECTION_ERROR: "Connection error",
ERR_CANT_ACQUIRE_RESOURCE: "Can't acquire resource error",
ERR_CANT_FORK: "Can't fork process error",
ERR_INVALID_DATA: "Invalid data error",
ERR_INVALID_PARAMETER: "Invalid parameter error",
ERR_ALREADY_EXISTS: "Already exists error",
ERR_DOES_NOT_EXIST: "Does not exist error",
ERR_DATABASE_CANT_READ: "Database: Read error",
ERR_DATABASE_CANT_WRITE: "Database: Write error",
ERR_COMPILATION_FAILED: "Compilation failed error",
ERR_METHOD_NOT_FOUND: "Method not found error",
ERR_LINK_FAILED: "Linking failed error",
ERR_SCRIPT_FAILED: "Script failed error",
ERR_CYCLIC_LINK: "Cycling link (import cycle) error",
ERR_INVALID_DECLARATION: "Invalid declaration error",
ERR_DUPLICATE_SYMBOL: "Duplicate symbol error",
ERR_PARSE_ERROR: "Parse error",
ERR_BUSY: "Busy error",
ERR_SKIP: "Skip error",
ERR_HELP: "Help error",
ERR_BUG: "Bug error",
ERR_PRINTER_ON_FIRE: "Printer on fire error",
}

View File

@@ -0,0 +1,706 @@
@tool
extends Resource
#-------------------------------------------------------------------------------
# A base class for resources bound with InputFields and suporting UndoRedo
# All properties are suppposed to be set using PropAction
# That helps to easily update UI and do/undo actions in editor
# There's also a bit of property management sprinkled on top (conditional display, modified values, etc.)
#
# TODO: reduce amount of abstractions and indirections.
# overhead for function calls and container usage is the most demanding part of this thing
#-------------------------------------------------------------------------------
enum PropActionLifecycle {BEFORE_DO, AFTER_DO, AFTER_UNDO}
const Logger = preload("../logger.gd")
const FunLib = preload("../fun_lib.gd")
const PropAction = preload("prop_action.gd")
const PA_PropSet = preload("pa_prop_set.gd")
const PA_PropEdit = preload("pa_prop_edit.gd")
const PA_ArrayInsert = preload("pa_array_insert.gd")
const PA_ArrayRemove = preload("pa_array_remove.gd")
const PA_ArraySet = preload("pa_array_set.gd")
const UI_ActionThumbnail_GD = preload("../../controls/input_fields/action_thumbnail/ui_action_thumbnail.gd")
const UI_InputField = preload("../../controls/input_fields/ui_input_field.gd")
const UI_IF_Bool = preload("../../controls/input_fields/ui_if_bool.gd")
const UI_IF_Enum = preload("../../controls/input_fields/ui_if_enum.gd")
const UI_IF_MultiRange = preload("../../controls/input_fields/ui_if_multi_range.gd")
const UI_IF_RealSlider = preload("../../controls/input_fields/ui_if_real_slider.gd")
const UI_IF_IntLineEdit = preload("../../controls/input_fields/ui_if_int_line_edit.gd")
const UI_IF_ThumbnailArray = preload("../../controls/input_fields/ui_if_thumbnail_array.gd")
const UI_IF_ApplyChanges = preload("../../controls/input_fields/ui_if_apply_changes.gd")
const UI_IF_Button = preload("../../controls/input_fields/ui_if_button.gd")
const UI_IF_PlainText = preload("../../controls/input_fields/ui_if_plain_text.gd")
const UI_IF_Object = preload("../../controls/input_fields/ui_if_object.gd")
const UI_IF_ThumbnailObject = preload("../../controls/input_fields/ui_if_thumbnail_object.gd")
const UndoRedoInterface = preload("../../utility/undo_redo_interface.gd")
var _undo_redo = null : set = set_undo_redo
# Backups that can be restored when using non-destructive PA_PropEdit
var prop_edit_backups:Dictionary = {}
# Properties added here will be ignored when creating input fields
# NOTE: this is meant to exclude properties from generating an input field AT ALL
# it's NOT a conditional check to show/hide fields
# it will be used once when generating a UI layout, but not to modify it
# NOTE: for conditional checks see 'visibility_tracked_properties' in ui_input_filed.gd
# to hide properties from editor's inspector see _get_prop_dictionary()
var input_field_blacklist:Array = []
# All properties that are linked together for showing an element of an Array
var res_edit_data:Array = []
# All properties that are affected by other properties
var prop_dependency_data:Array = []
var logger = null
signal prop_action_executed(prop_action, final_val)
signal req_change_interaction_feature(prop, index, feature, val)
signal prop_list_changed(prop_names)
#-------------------------------------------------------------------------------
# Initialization
#-------------------------------------------------------------------------------
func _init():
set_meta("class", "InputFieldResource")
resource_name = "InputFieldResource"
logger = Logger.get_for(self)
FunLib.ensure_signal(self.prop_action_executed, _on_prop_action_executed)
func set_undo_redo(val):
_undo_redo = val
# This doesn't account for resources inside nested Arrays/Dictionaries (i.e. [[Resource:1, Resource:2], [Resource:3]])
func duplicate_ifr(subresources:bool = false, ifr_subresources:bool = false) -> Resource:
var copy = super.duplicate(false)
if subresources || ifr_subresources:
var property_list = copy.get_property_list()
for prop_dict in property_list:
var prop = prop_dict.name
var prop_val = copy.get(prop)
if prop_val is Array || prop_val is Dictionary:
prop_val = prop_val.duplicate(true)
copy._set(prop, prop_val)
if prop_val is Array:
for i in range(0, prop_val.size()):
var element = prop_val[i]
if is_instance_of(element, Resource):
if element.has_method("duplicate_ifr") && ifr_subresources:
prop_val[i] = element.duplicate_ifr(subresources, ifr_subresources)
elif subresources:
prop_val[i] = element.duplicate(subresources)
elif prop_val is Dictionary:
for key in prop_val.keys():
var element = prop_val[key]
if is_instance_of(element, Resource):
if element.has_method("duplicate_ifr") && ifr_subresources:
prop_val[key] = element.duplicate_ifr(subresources, ifr_subresources)
elif subresources:
prop_val[key] = element.duplicate(subresources)
# Script check makes sure we don't try to duplicate Script properties
# This... shouldn't be happening normally
# TODO the whole InputFieldResource is kind of a mess, would be great if we could fit that into existing inspector workflow
elif is_instance_of(prop_val, Resource) && !is_instance_of(prop_val, Script):
if prop_val.has_method("duplicate_ifr") && ifr_subresources:
prop_val = prop_val.duplicate_ifr(subresources, ifr_subresources)
elif subresources:
prop_val = prop_val.duplicate(subresources)
copy._set(prop, prop_val)
return copy
func duplicate(subresources:bool = false):
var copy = duplicate_ifr(subresources, true)
_fix_duplicate_signals(copy)
return copy
# Convert Input Field Resource to a dictionary
func ifr_to_dict(ifr_subresources:bool = false):
var dict = {}
for prop_dict in _get_property_list():
if find_res_edit_by_res_prop(prop_dict.name):
continue
var prop_val = _get(prop_dict.name)
if prop_val is Array || prop_val is Dictionary:
prop_val = prop_val.duplicate(true)
if prop_val is Array:
for i in range(0, prop_val.size()):
var element = prop_val[i]
prop_val[i] = _ifr_val_to_dict_compatible(element, ifr_subresources)
elif prop_val is Dictionary:
for key in prop_val.keys():
var element = prop_val[key]
prop_val[key] = _ifr_val_to_dict_compatible(element, ifr_subresources)
else:
prop_val = _ifr_val_to_dict_compatible(prop_val, ifr_subresources)
dict[prop_dict.name] = prop_val
return dict
# Convert Input Field Resource value from native to dictionary-compatible (and independent of native var_to_str)
func _ifr_val_to_dict_compatible(val, ifr_subresources):
if ifr_subresources && is_instance_of(val, Resource) && !is_instance_of(val, Script):
if val.has_method("ifr_to_dict") && ifr_subresources:
val = val.ifr_to_dict(ifr_subresources)
else:
val = val.resource_path
elif typeof(val) == TYPE_VECTOR3:
val = FunLib.vec3_to_str(val)
elif typeof(val) == TYPE_TRANSFORM3D:
val = FunLib.transform3d_to_str(val)
return val
# Convert dictionary to an Input Field Resource
func ifr_from_dict(dict: Dictionary, ifr_subresources:bool = false, str_version: int = 1) -> Resource:
for prop_dict in _get_property_list():
if find_res_edit_by_res_prop(prop_dict.name):
continue
var prop_val = dict.get(prop_dict.name, null)
var existing_prop_val = _get(prop_dict.name)
if prop_val is Array:
existing_prop_val.resize(prop_val.size())
# Trigger automatic creation of default Resource
_set(prop_dict.name, existing_prop_val)
for i in range(0, prop_val.size()):
var element = existing_prop_val[i]
prop_val[i] = _dict_compatible_to_ifr_val(element, prop_val[i], ifr_subresources, str_version)
elif prop_val is Dictionary:
prop_val = _dict_compatible_to_ifr_val(existing_prop_val, prop_val, ifr_subresources, str_version)
if prop_val is Dictionary:
for key in prop_val.keys():
existing_prop_val[key] = prop_val.get(key, null)
# Trigger automatic creation of default Resource
_set(prop_dict.name, existing_prop_val)
for key in prop_val.keys():
var element = existing_prop_val[key]
prop_val[key] = _dict_compatible_to_ifr_val(element, prop_val[key], ifr_subresources, str_version)
else:
prop_val = _dict_compatible_to_ifr_val(existing_prop_val, prop_val, ifr_subresources, str_version)
_set(prop_dict.name, prop_val)
return self
# Convert dictionary-compatible (and independent of native var_to_str) value to an Input Field Resource value
func _dict_compatible_to_ifr_val(template_val, val, ifr_subresources, str_version):
if ifr_subresources && is_instance_of(template_val, Resource) && !is_instance_of(template_val, Script):
if template_val.has_method("ifr_from_dict") && ifr_subresources:
val = template_val.ifr_from_dict(val, ifr_subresources, str_version)
elif val is String && ResourceLoader.exists(val):
val = ResourceLoader.load(val)
elif typeof(template_val) == TYPE_VECTOR3:
val = FunLib.str_to_vec3(val, str_version)
elif typeof(template_val) == TYPE_TRANSFORM3D:
val = FunLib.str_to_transform3d(val, str_version)
return val
# It turns out, duplicating subresources implies we need to reconnect them to any *other* duplicated resources
# e.g. brushes to the toolshed (Obvious in retrospective, I know)
# Ideally they would reconnect automatically, and possibly that's what Godot's native duplicate() does (but I haven't checked)
# For now we will fix this by hand for any resource that inherits from InputFieldResource
# TODO explore if Godot handles subresource signal reconnection. If yes - try to utilize the native code. If not - write my own
func _fix_duplicate_signals(copy):
pass
#-------------------------------------------------------------------------------
# Handling property actions
#-------------------------------------------------------------------------------
# A wrapper with a better name
func request_prop_action(prop_action:PropAction):
on_prop_action_requested(prop_action)
# A callback for any requests to change the properties
func on_prop_action_requested(prop_action:PropAction):
debug_print_prop_action("Requested prop action: %s..." % [str(prop_action)])
if _undo_redo && _can_prop_action_create_history(prop_action):
var prop_action_class = prop_action.get_meta("class")
UndoRedoInterface.create_action(_undo_redo, "%s: on '%s'" % [prop_action_class, prop_action.prop], 0, false, self)
_prop_action_request_lifecycle(prop_action, PropActionLifecycle.BEFORE_DO)
UndoRedoInterface.add_do_method(_undo_redo, self._perform_prop_action.bind(prop_action))
_prop_action_request_lifecycle(prop_action, PropActionLifecycle.AFTER_DO)
UndoRedoInterface.add_undo_method(_undo_redo, self._perform_prop_action.bind(_get_opposite_prop_action(prop_action)))
_prop_action_request_lifecycle(prop_action, PropActionLifecycle.AFTER_UNDO)
UndoRedoInterface.commit_action(_undo_redo, true)
# But we don't *have* to use UndoRedo system
else:
_prop_action_request_lifecycle(prop_action, PropActionLifecycle.BEFORE_DO)
_perform_prop_action(prop_action)
_prop_action_request_lifecycle(prop_action, PropActionLifecycle.AFTER_DO)
# A wrapper for prop_action_request_lifecycle() with default logic
func _prop_action_request_lifecycle(prop_action:PropAction, lifecycle_stage:int):
_handle_res_edit_prop_action_lifecycle(prop_action, lifecycle_stage)
_handle_dependency_prop_action_lifecycle(prop_action, lifecycle_stage)
prop_action_request_lifecycle(prop_action, lifecycle_stage)
# Custom logic after a PropAction was requested/done/undone
# To be overridden
func prop_action_request_lifecycle(prop_action:PropAction, lifecycle_stage:int):
pass
# Can a given prop action create UndoRedo history?
# Most of the time we need this is when using a UI slider
# To avoid commiting dozens of history actions while dragging
func _can_prop_action_create_history(prop_action:PropAction):
var enable_undo_redo = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/greenhouse_ui_enable_undo_redo", true)
return prop_action.can_create_history && enable_undo_redo
# Performs the prop action
func _perform_prop_action(prop_action:PropAction):
var prop_action_class = prop_action.get_meta("class")
var current_val_copy = _get_current_val_copy(prop_action.prop)
debug_print_prop_action("Performing prop action: %s..." % [str(prop_action)])
# 'prop_action.val = get(prop_action.prop)' and it's variations
# Account for _modify_prop() modifying the property
# E.g. an array replacing null elements with actual instances
# This does not apply to PA_ArrayRemove since we assume a removed element will not be changed
match prop_action_class:
"PA_PropSet":
_erase_prop_edit_backup(prop_action.prop)
_set(prop_action.prop, prop_action.val)
prop_action.val = get(prop_action.prop)
"PA_PropEdit":
_make_prop_edit_backup(prop_action.prop)
_set(prop_action.prop, prop_action.val)
prop_action.val = get(prop_action.prop)
"PA_ArrayInsert":
current_val_copy.insert(prop_action.index, prop_action.val)
_set(prop_action.prop, current_val_copy)
prop_action.val = get(prop_action.prop)[prop_action.index]
"PA_ArrayRemove":
prop_action.val = current_val_copy[prop_action.index]
current_val_copy.remove_at(prop_action.index)
_set(prop_action.prop, current_val_copy)
"PA_ArraySet":
current_val_copy[prop_action.index] = prop_action.val
_set(prop_action.prop, current_val_copy)
prop_action.val = get(prop_action.prop)[prop_action.index]
_:
logger.error("Error: PropAction class \"%s\" is not accounted for" % [prop_action_class])
return
res_edit_update_interaction_features(prop_action.prop)
prop_action_executed.emit(prop_action, get(prop_action.prop))
# Reverses the prop action (used for undo actions)
func _get_opposite_prop_action(prop_action:PropAction) -> PropAction:
var prop_action_class = prop_action.get_meta("class")
var current_val_copy = _get_current_val_copy(prop_action.prop)
match prop_action_class:
"PA_PropSet":
return PA_PropSet.new(prop_action.prop, current_val_copy)
"PA_PropEdit":
return PA_PropEdit.new(prop_action.prop, current_val_copy)
"PA_ArrayInsert":
return PA_ArrayRemove.new(prop_action.prop, null, prop_action.index)
"PA_ArrayRemove":
return PA_ArrayInsert.new(prop_action.prop, current_val_copy[prop_action.index], prop_action.index)
"PA_ArraySet":
return PA_ArraySet.new(prop_action.prop, current_val_copy[prop_action.index], prop_action.index)
_:
logger.error("Error: PropAction class \"%s\" is not accounted for" % [prop_action_class])
return null
# Backup a current property before a PA_PropEdit
# Since PA_PropEdit is non-destructive to UndoRedo history, we need a separate PA_PropSet to make do/undo actions
# This backup is used to cache the initial property value and retrieve it when setting an undo action
func _make_prop_edit_backup(prop:String):
if prop_edit_backups.has(prop): return
prop_edit_backups[prop] = _get_current_val_copy(prop)
# Cleanup the backup
func _erase_prop_edit_backup(prop:String):
prop_edit_backups.erase(prop)
# Get the copy of CURRENT state of the value
# Does not copy objects because of possible abiguity of intention
func _get_current_val_copy(prop:String):
var copy
if prop_edit_backups.has(prop):
copy = prop_edit_backups[prop]
else:
copy = get(prop)
if copy is Array || copy is Dictionary:
copy = copy.duplicate()
return copy
# A wrapper for on_prop_action_executed() with default logic
func _on_prop_action_executed(prop_action:PropAction, final_val):
on_prop_action_executed(prop_action, final_val)
# A built-in callback for when a PropAction was executed
# To be overridden
func on_prop_action_executed(prop_action:PropAction, final_val):
pass
#-------------------------------------------------------------------------------
# Property export
#-------------------------------------------------------------------------------
# Modify a property
# Mostly used to initialize a newly added array/dictionary value when setting array size from Engine Inspector
# To be overridden and (usually) called inside a _set()
func _modify_prop(prop:String, val):
return val
# Map property info to a dictionary for convinience
# To be overridden and (usually) called inside a _get_property_list()
func _get_prop_dictionary() -> Dictionary:
return {}
# Get property data from a dictionary and filter it
# Allows easier management of hidden/shown properties based on arbitrary conditions in a subclass
# To be overridden and (usually) called inside a _get_property_list()
# With a dictionary created by _get_prop_dictionary()
# Return the same prop_dict passed to it (for convenience in function calls)
func _filter_prop_dictionary(prop_dict: Dictionary) -> Dictionary:
return prop_dict
func _set(property, val):
pass
func _get(property):
pass
# Default functionality for _get_property_list():
# Get all {prop_name: prop_data_dictionary} defined by _get_prop_dictionary()
# Filter them (optionally rejecting some of them based on arbitrary conditions)
# Return a prop_dict values array
func _get_property_list():
var prop_dict = _get_prop_dictionary()
_filter_prop_dictionary(prop_dict)
return prop_dict.values()
# A wrapper around built-in notify_property_list_changed()
# To support a custom signal we can bind manually
func _emit_property_list_changed_notify():
notify_property_list_changed()
prop_list_changed.emit(_filter_prop_dictionary(_get_prop_dictionary()))
#-------------------------------------------------------------------------------
# UI Management
#-------------------------------------------------------------------------------
# Create all the UI input fields
# input_field_blacklist is responsible for excluding certain props
# Optionally specify a whitelist to use instead of an object-wide blacklist
# They both allow to conditionally hide/show input fields
func create_input_fields(_base_control:Control, _resource_previewer, whitelist:Array = []) -> Dictionary:
# print("create_input_fields %s %s %d start" % [str(self), get_meta("class"), Time.get_ticks_msec()])
var prop_names = _get_prop_dictionary().keys()
var input_fields := {}
for prop in prop_names:
# Conditional rejection of a property
if whitelist.is_empty():
if input_field_blacklist.has(prop): continue
else:
if !whitelist.has(prop): continue
var input_field:UI_InputField = create_input_field(_base_control, _resource_previewer, prop)
if input_field:
input_fields[prop] = input_field
return input_fields
func create_input_field(_base_control:Control, _resource_previewer, prop:String) -> UI_InputField:
var input_field = _create_input_field(_base_control, _resource_previewer, prop)
if input_field:
input_field.name = prop
input_field.set_tooltip(get_prop_tooltip(prop))
input_field.on_prop_list_changed(_filter_prop_dictionary(_get_prop_dictionary()))
input_field.prop_action_requested.connect(request_prop_action)
prop_action_executed.connect(input_field.on_prop_action_executed)
prop_list_changed.connect(input_field.on_prop_list_changed)
input_field.tree_entered.connect(on_if_tree_entered.bind(input_field))
if is_instance_of(input_field, UI_IF_ThumbnailArray):
input_field.requested_press.connect(on_if_thumbnail_array_press.bind(input_field))
req_change_interaction_feature.connect(input_field.on_changed_interaction_feature)
# NOTE: below is a leftover abstraction from an attempt to create ui nodes only once and reuse them
# but it introduced to many unknowns to be viable as a part of Godot 3.5 -> Godot 4.0 transition
# yet it stays, as a layer of abstraction
# TODO: implement proper reuse of ui nodes
# or otherwise speed up their creation
input_field.prepare_input_field(_get(prop), _base_control, _resource_previewer)
return input_field
# Creates a specified input field
# To be overridden
func _create_input_field(_base_control:Control, _resource_previewer, prop:String) -> UI_InputField:
return null
# Do something with an input field when it's _ready()
func on_if_tree_entered(input_field:UI_InputField):
var res_edit = find_res_edit_by_array_prop(input_field.prop_name)
if res_edit:
var res_val = get(res_edit.res_prop)
# We assume that input field that displays the resource is initialized during infput field creation
# And hense only update the array interaction features
res_edit_update_interaction_features(res_edit.res_prop)
# An array thumbnail representing a resource was pressed
func on_if_thumbnail_array_press(pressed_index:int, input_field:Control):
var res_edit = find_res_edit_by_array_prop(input_field.prop_name)
if res_edit:
var array_val = get(res_edit.array_prop)
var new_res_val = array_val[pressed_index]
_res_edit_select(res_edit.array_prop, [new_res_val], true)
# Get a tooltip string for each property to be used in it's InputField
func get_prop_tooltip(prop:String) -> String:
return ""
#-------------------------------------------------------------------------------
# Prop dependency
#-------------------------------------------------------------------------------
# Register a property dependency (where any of the controlling_props might change the dependent_prop)
# This is needed for correct UndoRedo functionality
func _add_prop_dependency(dependent_prop:String, controlling_props:Array):
prop_dependency_data.append({"dependent_prop": dependent_prop, "controlling_props": controlling_props})
# React to lifecycle stages for properties that are affected by other properties
func _handle_dependency_prop_action_lifecycle(prop_action:PropAction, lifecycle_stage:int):
var prop_action_class = prop_action.get_meta("class")
var dependency = find_dependency_by_controlling_prop(prop_action.prop)
if dependency && prop_action_class == "PA_PropSet":
var new_prop_action = PA_PropSet.new(dependency.dependent_prop, get(dependency.dependent_prop))
if _undo_redo && _can_prop_action_create_history(new_prop_action):
if lifecycle_stage == PropActionLifecycle.AFTER_DO:
UndoRedoInterface.add_do_method(_undo_redo, self._perform_prop_action.bind(new_prop_action))
elif lifecycle_stage == PropActionLifecycle.AFTER_UNDO:
UndoRedoInterface.add_undo_method(_undo_redo, self._perform_prop_action.bind(_get_opposite_prop_action(new_prop_action)))
else:
if lifecycle_stage == PropActionLifecycle.AFTER_DO:
_perform_prop_action(new_prop_action)
#-------------------------------------------------------------------------------
# Res edit
#-------------------------------------------------------------------------------
# Register a property array with resources that can be individually shown for property editing
# Since new ones are added as 'null' and initialized in _modify_prop(), so they WILL NOT be equal to cached ones in UndoRedo actions
func _add_res_edit_source_array(array_prop:String, res_prop:String):
res_edit_data.append({"array_prop": array_prop, "res_prop": res_prop})
# React to lifecycle stages for actions executed on res_edit_data members
func _handle_res_edit_prop_action_lifecycle(prop_action:PropAction, lifecycle_stage:int):
var prop_action_class = prop_action.get_meta("class")
var res_edit = find_res_edit_by_array_prop(prop_action.prop)
if res_edit:
var array_prop = res_edit.array_prop
var array_val = get(array_prop)
var res_val = get(res_edit.res_prop)
var current_index = array_val.find(res_val)
match prop_action_class:
"PA_ArrayRemove":
if current_index == prop_action.index:
if _undo_redo && _can_prop_action_create_history(prop_action):
if lifecycle_stage == PropActionLifecycle.AFTER_DO:
_undo_redo.add_do_method(self, "_res_edit_select", array_prop, [null])
elif lifecycle_stage == PropActionLifecycle.AFTER_UNDO:
_undo_redo.add_undo_method(self, "_res_edit_select", array_prop, [res_val])
else:
if lifecycle_stage == PropActionLifecycle.AFTER_DO:
_res_edit_select(array_prop, [null])
"PA_ArraySet":
var new_res_val = prop_action.val
if current_index == prop_action.index:
if _undo_redo && _can_prop_action_create_history(prop_action):
if lifecycle_stage == PropActionLifecycle.AFTER_DO:
_undo_redo.add_do_method(self, "_res_edit_select", array_prop, [new_res_val])
elif lifecycle_stage == PropActionLifecycle.AFTER_UNDO:
_undo_redo.add_undo_method(self, "_res_edit_select", array_prop, [res_val])
else:
if lifecycle_stage == PropActionLifecycle.AFTER_DO:
_res_edit_select(array_prop, [new_res_val])
# Requests a prop action that updates the needed property
func _res_edit_select(array_prop:String, new_res_array:Array, create_history:bool = false):
var res_edit = find_res_edit_by_array_prop(array_prop)
if res_edit:
var array_val = get(res_edit.array_prop)
var res_val = get(res_edit.res_prop)
var new_res_val = new_res_array[0]
if res_val == new_res_val:
new_res_val = null
var prop_action = PA_PropSet.new(res_edit.res_prop, new_res_val)
prop_action.can_create_history = create_history
request_prop_action(prop_action)
#-------------------------------------------------------------------------------
# Prop dependency misc
#-------------------------------------------------------------------------------
func find_dependency_by_dependent_prop(dependent_prop:String):
for dependency in prop_dependency_data:
if dependency.dependent_prop == dependent_prop:
return dependency
return null
func find_dependency_by_controlling_prop(controlling_prop:String):
for dependency in prop_dependency_data:
if dependency.controlling_props.has(controlling_prop):
return dependency
return null
#-------------------------------------------------------------------------------
# Res edit misc
#-------------------------------------------------------------------------------
func find_res_edit_by_array_prop(array_prop:String):
for res_edit in res_edit_data:
if res_edit.array_prop == array_prop:
return res_edit
return null
func find_res_edit_by_res_prop(res_prop:String):
for res_edit in res_edit_data:
if res_edit.res_prop == res_prop:
return res_edit
return null
func res_edit_update_interaction_features(res_prop:String):
var res_edit = find_res_edit_by_res_prop(res_prop)
if res_edit == null || res_edit.is_empty(): return
var array_val = get(res_edit.array_prop)
for i in range(0, array_val.size()):
var res_val = get(res_edit.res_prop)
var res_val_at_index = array_val[i]
if res_val_at_index == res_val:
req_change_interaction_feature.emit(res_edit.array_prop, UI_ActionThumbnail_GD.InteractionFlags.PRESS, true, {"index": i})
else:
req_change_interaction_feature.emit(res_edit.array_prop, UI_ActionThumbnail_GD.InteractionFlags.PRESS, false, {"index": i})
#-------------------------------------------------------------------------------
# Debug
#-------------------------------------------------------------------------------
# Debug print with a ProjectSettings check
func debug_print_prop_action(string:String):
if !FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/input_field_resource_log_prop_actions", false): return
logger.info(string)

View File

@@ -0,0 +1,32 @@
@tool
extends "prop_action.gd"
#-------------------------------------------------------------------------------
# Insert an array element at index
#-------------------------------------------------------------------------------
var index:int = -1
func _init(__prop:String, __val, __index:int):
super(__prop, __val)
set_meta("class", "PA_ArrayInsert")
index = __index
can_create_history = true
func _to_string():
return "%s: [prop: %s, val: %s, index: %d, can_create_history: %s]" % [get_meta("class"), prop, str(val), index, str(can_create_history)]
func duplicate(deep:bool = false):
var copy = self.get_script().new(prop, val, index)
copy.can_create_history = can_create_history
return copy

View File

@@ -0,0 +1,32 @@
@tool
extends "prop_action.gd"
#-------------------------------------------------------------------------------
# Remove an array element at index
#-------------------------------------------------------------------------------
var index:int = -1
func _init(__prop:String, __val, __index:int):
super(__prop, __val)
set_meta("class", "PA_ArrayRemove")
index = __index
can_create_history = true
func _to_string():
return "%s: [prop: %s, val: %s, index: %d, can_create_history: %s]" % [get_meta("class"), prop, str(val), index, str(can_create_history)]
func duplicate(deep:bool = false):
var copy = self.get_script().new(prop, val, index)
copy.can_create_history = can_create_history
return copy

View File

@@ -0,0 +1,32 @@
@tool
extends "prop_action.gd"
#-------------------------------------------------------------------------------
# Set an array element at index
#-------------------------------------------------------------------------------
var index:int = -1
func _init(__prop:String, __val, __index:int):
super(__prop, __val)
set_meta("class", "PA_ArraySet")
index = __index
can_create_history = true
func _to_string():
return "%s: [prop: %s, val: %s, index: %d, can_create_history: %s]" % [get_meta("class"), prop, str(val), index, str(can_create_history)]
func duplicate(deep:bool = false):
var copy = self.get_script().new(prop, val, index)
copy.can_create_history = can_create_history
return copy

View File

@@ -0,0 +1,21 @@
@tool
extends "prop_action.gd"
#-------------------------------------------------------------------------------
# Edit a property
# It is implied that these changes are cosmetic/in progress/not permanent
# The value that persists should be set from PA_PropSet
#-------------------------------------------------------------------------------
func _init(__prop:String, __val):
super(__prop, __val)
set_meta("class", "PA_PropEdit")
can_create_history = false
func _to_string():
return "%s: [prop: %s, val: %s, can_create_history: %s]" % [get_meta("class"), prop, str(val), str(can_create_history)]

View File

@@ -0,0 +1,19 @@
@tool
extends "prop_action.gd"
#-------------------------------------------------------------------------------
# Set a property
#-------------------------------------------------------------------------------
func _init(__prop:String, __val):
super(__prop, __val)
set_meta("class", "PA_PropSet")
can_create_history = true
func _to_string():
return "%s: [prop: %s, val: %s, can_create_history: %s]" % [get_meta("class"), prop, str(val), str(can_create_history)]

View File

@@ -0,0 +1,37 @@
@tool
extends RefCounted
#-------------------------------------------------------------------------------
# A base storage object for actions that affect properties in some way
#-------------------------------------------------------------------------------
var prop:String = ""
var val = null
var can_create_history:bool
func _init(__prop:String,__val):
set_meta("class", "PropAction")
prop = __prop
val = __val
can_create_history = true
func _to_string():
return "%s: [prop: %s, val: %s, can_create_history: %s]" % [get_meta("class"), prop, str(val), str(can_create_history)]
func duplicate(deep:bool = false):
var copy = self.get_script().new(prop, val)
copy.can_create_history = can_create_history
if deep:
if copy.val is Array || copy.val is Dictionary:
copy.val = copy.val.duplicate()
return copy

View File

@@ -0,0 +1,95 @@
@tool
#-------------------------------------------------------------------------------
# A modifed version of Zylann's "logger.gd" from "zylann.hterrain" plugin
# Guidelines for printing errors:
# assert() - a built-in for terminal failures. Only works in debug builds/editor
# logger.debug() - nuanced logging when engine was launched with "-v" (verbose stdout)
# logger.info() - important info/notes for the user to keep in mind
# logger.warn() - something isn't excatly by the book, but we allow it/can work around it
# logger.error() - something is wrong and current task will fail. Has to be corrected to continue normal use
#-------------------------------------------------------------------------------
# A Base Logger type
class Base extends RefCounted:
var _context := ""
var _log_filepath := ''
func _init(__context:String, __log_filepath:String = ''):
_context = __context
_log_filepath = __log_filepath
if !_log_filepath.is_empty():
DirAccess.make_dir_recursive_absolute(_log_filepath.get_base_dir())
if !FileAccess.file_exists(_log_filepath):
var file = FileAccess.open(_log_filepath, FileAccess.WRITE)
file.close()
# func debug(msg:String):
# pass
func info(msg):
msg = "{0}: {1}".format([_context, str(msg)])
print("INFO: " + msg)
log_to_file(msg)
func warn(msg):
msg = "{0}: {1}".format([_context, str(msg)])
push_warning(msg)
# msg = 'WARNING: ' + msg
# print(msg)
log_to_file(msg)
func error(msg):
msg = "{0}: {1}".format([_context, str(msg)])
push_error(msg)
# msg = 'ERROR: ' + msg
# printerr(msg)
log_to_file(msg)
func assert_error(msg):
msg = "{0}: {1}".format([_context, str(msg)])
msg = 'ERROR: ' + msg
print(msg)
assert(msg)
log_to_file(msg)
# We need to route that through a logger manager of some kind,
# So we won't have to reopen FileAccess each time
func log_to_file(msg: String):
if _log_filepath.is_empty(): return
var file = FileAccess.open(_log_filepath, FileAccess.READ_WRITE)
file.seek_end()
file.store_line(msg)
file.close()
# A Verbose Logger type
# Meant to display verbose debug messages
#class Verbose extends Base:
# func _init(__context:String):
# super(__context)
#
# func debug(msg:String):
# print("DEBUG: {0}: {1}".format([_context, msg]))
# As opposed to original, for now we don't have separate "Verbose" logging
# Instead we use ProjectSettings to toggle frequently used logging domains
static func get_for(owner:Object, name:String = "", log_filepath: String = '') -> Base:
# Note: don't store the owner. If it's a RefCounted, it could create a cycle
var context = owner.get_script().resource_path.get_file()
if name != "":
context += " (%s)" % [name]
return get_for_string(context, log_filepath)
# Get logger with a string context
static func get_for_string(context:String, log_filepath: String = '') -> Base:
# if OS.is_stdout_verbose():
# return Verbose.new(string_context)
return Base.new(context, log_filepath)

View File

@@ -0,0 +1,150 @@
@tool
#-------------------------------------------------------------------------------
# Manages adding all plugin project settings
#-------------------------------------------------------------------------------
const Globals = preload("globals.gd")
const FunLib = preload("fun_lib.gd")
const Logger = preload("logger.gd")
# Add all settings for this plugin
static func add_plugin_project_settings():
# Remove settings from the previous plugin version
if ProjectSettings.has_setting("dreadpons_spatial_gardener/input_and_ui/brush_property_edit_button"):
ProjectSettings.clear("dreadpons_spatial_gardener/input_and_ui/brush_property_edit_button")
if ProjectSettings.has_setting("dreadpons_spatial_gardener/input_and_ui/brush_property_edit_modifier_key"):
ProjectSettings.clear("dreadpons_spatial_gardener/input_and_ui/brush_property_edit_modifier_key")
if ProjectSettings.has_setting("dreadpons_spatial_gardener/input_and_ui/brush_size_slider_max_value"):
ProjectSettings.clear("dreadpons_spatial_gardener/input_and_ui/brush_size_slider_max_value")
# Painting
add_project_setting(
"dreadpons_spatial_gardener/painting/projection_raycast_margin",
0.1,
TYPE_FLOAT)
add_project_setting(
"dreadpons_spatial_gardener/painting/simplify_projection_frustum",
true,
TYPE_BOOL)
# Input and UI
add_project_setting(
"dreadpons_spatial_gardener/input_and_ui/greenhouse_ui_enable_undo_redo",
true,
TYPE_BOOL)
add_project_setting(
"dreadpons_spatial_gardener/input_and_ui/greenhouse_thumbnail_scale",
1.0,
TYPE_FLOAT)
add_project_setting_globals_enum(
"dreadpons_spatial_gardener/input_and_ui/brush_prop_edit_button",
Globals.MouseButton.MOUSE_BUTTON_XBUTTON1, Globals.MouseButton)
add_project_setting_globals_enum(
"dreadpons_spatial_gardener/input_and_ui/brush_prop_edit_modifier",
Globals.KeyboardKey.KEY_SHIFT, Globals.KeyboardKey)
add_project_setting_globals_enum(
"dreadpons_spatial_gardener/input_and_ui/brush_overlap_mode_button",
Globals.KeyboardKey.KEY_QUOTELEFT, Globals.KeyboardKey)
add_project_setting_globals_enum(
"dreadpons_spatial_gardener/input_and_ui/focus_painter_key",
Globals.KeyboardKey.KEY_Q, Globals.KeyboardKey)
add_project_setting(
"dreadpons_spatial_gardener/input_and_ui/brush_volume_size_slider_max_value",
100.0,
TYPE_FLOAT)
add_project_setting(
"dreadpons_spatial_gardener/input_and_ui/brush_projection_size_slider_max_value",
1000.0,
TYPE_FLOAT)
add_project_setting(
"dreadpons_spatial_gardener/input_and_ui/plant_max_distance_slider_max_value",
1000.0,
TYPE_FLOAT)
add_project_setting(
"dreadpons_spatial_gardener/input_and_ui/plant_kill_distance_slider_max_value",
2000.0,
TYPE_FLOAT)
add_project_setting(
"dreadpons_spatial_gardener/input_and_ui/plant_density_slider_max_value",
2000.0,
TYPE_FLOAT)
add_project_setting(
"dreadpons_spatial_gardener/input_and_ui/octree_min_node_size_slider_max_value",
500.0,
TYPE_FLOAT)
# Plugin
add_project_setting(
"dreadpons_spatial_gardener/plugin/scan_for_outdated_scenes",
true,
TYPE_BOOL)
# Debug
add_project_setting_globals_enum(
"dreadpons_spatial_gardener/debug/dump_editor_tree_key",
Globals.KeyboardKey.KEY_NONE, Globals.KeyboardKey)
add_project_setting_globals_enum(
"dreadpons_spatial_gardener/debug/dump_all_octrees_key",
Globals.KeyboardKey.KEY_NONE, Globals.KeyboardKey)
add_project_setting(
"dreadpons_spatial_gardener/debug/arborist_log_lifecycle",
false,
TYPE_BOOL)
add_project_setting(
"dreadpons_spatial_gardener/debug/octree_log_lifecycle",
false,
TYPE_BOOL)
add_project_setting(
"dreadpons_spatial_gardener/debug/brush_placement_area_log_grid",
false,
TYPE_BOOL)
add_project_setting(
"dreadpons_spatial_gardener/debug/input_field_resource_log_prop_actions",
false,
TYPE_BOOL)
add_project_setting(
"dreadpons_spatial_gardener/debug/debug_viewer_octree_member_size",
2.0,
TYPE_FLOAT)
add_project_setting(
"dreadpons_spatial_gardener/debug/stroke_handler_debug_draw",
false,
TYPE_BOOL)
# Saving settings
var err: int = ProjectSettings.save()
if err:
var logger = Logger.get_for_string("ProjectSettingsManager")
logger.error("Encountered error %s when saving project settings" % [Globals.get_err_message(err)])
# Shorthand for adding enum setting and generating it's info
static func add_project_setting_globals_enum(setting_name:String, default_value:int, enum_dict:Dictionary):
add_project_setting(
setting_name,
Globals.enum_to_index(default_value, enum_dict),
TYPE_INT, PROPERTY_HINT_ENUM,
FunLib.make_hint_string(enum_dict.keys()))
# Shorthand for adding a setting, setting it's info and initial value
static func add_project_setting(setting_name:String, default_value, type:int, hint:int = PROPERTY_HINT_NONE, hintString:String = ""):
var setting_info: Dictionary = {
"name": setting_name,
"type": type,
"hint": hint,
"hint_string": hintString
}
if !ProjectSettings.has_setting(setting_name):
ProjectSettings.set_setting(setting_name, default_value)
ProjectSettings.add_property_info(setting_info)
ProjectSettings.set_initial_value(setting_name, default_value)

View File

@@ -0,0 +1,86 @@
#-------------------------------------------------------------------------------
# An interface for common functionality between
# Editor-specific and runtime UndoRedo systems
#-------------------------------------------------------------------------------
extends Object
static func clear_history(undo_redo):
if DPON_FM.is_instance_of_ed(undo_redo, "EditorUndoRedoManager"):
push_error("Unable to clear history on EditorUndoRedoManager!")
else:
undo_redo.clear_history()
static func create_action(undo_redo, name: String, merge_mode := 0, backward_undo_ops := false, custom_context: Object = null):
if DPON_FM.is_instance_of_ed(undo_redo, "EditorUndoRedoManager"):
undo_redo.create_action(name, merge_mode, custom_context, backward_undo_ops)
else:
undo_redo.create_action(name, merge_mode, backward_undo_ops)
static func add_do_method(undo_redo, callable: Callable):
if DPON_FM.is_instance_of_ed(undo_redo, "EditorUndoRedoManager"):
var bound_args = callable.get_bound_arguments()
match bound_args.size():
0: undo_redo.add_do_method(callable.get_object(), callable.get_method())
1: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0])
2: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1])
3: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2])
4: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3])
5: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4])
6: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5])
7: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5], bound_args[6])
8: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5], bound_args[6], bound_args[7])
9: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5], bound_args[6], bound_args[7], bound_args[8])
10: undo_redo.add_do_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5], bound_args[6], bound_args[7], bound_args[8], bound_args[9])
_: push_error("Too many arguments!")
else:
undo_redo.add_do_method(callable)
static func add_undo_method(undo_redo, callable: Callable):
if DPON_FM.is_instance_of_ed(undo_redo, "EditorUndoRedoManager"):
var bound_args = callable.get_bound_arguments()
match bound_args.size():
0: undo_redo.add_undo_method(callable.get_object(), callable.get_method())
1: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0])
2: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1])
3: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2])
4: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3])
5: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4])
6: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5])
7: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5], bound_args[6])
8: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5], bound_args[6], bound_args[7])
9: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5], bound_args[6], bound_args[7], bound_args[8])
10: undo_redo.add_undo_method(callable.get_object(), callable.get_method(), bound_args[0], bound_args[1], bound_args[2], bound_args[3], bound_args[4], bound_args[5], bound_args[6], bound_args[7], bound_args[8], bound_args[9])
_: push_error("Too many arguments!")
else:
undo_redo.add_undo_method(callable)
static func commit_action(undo_redo, execute := true):
undo_redo.commit_action(execute)
static func undo(undo_redo, custom_context: Object = null):
if DPON_FM.is_instance_of_ed(undo_redo, "EditorUndoRedoManager"):
undo_redo = undo_redo.get_history_undo_redo(undo_redo.get_object_history_id(custom_context))
return undo_redo.undo()
static func redo(undo_redo, custom_context: Object = null):
if DPON_FM.is_instance_of_ed(undo_redo, "EditorUndoRedoManager"):
undo_redo = undo_redo.get_history_undo_redo(undo_redo.get_object_history_id(custom_context))
return undo_redo.redo()
static func get_current_action_name(undo_redo, custom_context: Object = null):
if DPON_FM.is_instance_of_ed(undo_redo, "EditorUndoRedoManager"):
undo_redo = undo_redo.get_history_undo_redo(undo_redo.get_object_history_id(custom_context))
return undo_redo.get_current_action_name()

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Cory Petkovsek, Roope Palmroos, and Contributors.
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 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.

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