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()