built more assets and started playing with foliage painting
This commit is contained in:
613
addons/dreadpon.spatial_gardener/arborist/arborist.gd
Normal file
613
addons/dreadpon.spatial_gardener/arborist/arborist.gd
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)]
|
||||
51
addons/dreadpon.spatial_gardener/arborist/placeform.gd
Normal file
51
addons/dreadpon.spatial_gardener/arborist/placeform.gd
Normal 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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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})
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
116
addons/dreadpon.spatial_gardener/arborist/transform_generator.gd
Normal file
116
addons/dreadpon.spatial_gardener/arborist/transform_generator.gd
Normal 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
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
213
addons/dreadpon.spatial_gardener/controls/theme_adapter.gd
Normal file
213
addons/dreadpon.spatial_gardener/controls/theme_adapter.gd
Normal 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
|
||||
|
||||
170
addons/dreadpon.spatial_gardener/gardener/data_import_export.gd
Normal file
170
addons/dreadpon.spatial_gardener/gardener/data_import_export.gd
Normal 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)
|
||||
261
addons/dreadpon.spatial_gardener/gardener/debug_viewer.gd
Normal file
261
addons/dreadpon.spatial_gardener/gardener/debug_viewer.gd
Normal 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)
|
||||
778
addons/dreadpon.spatial_gardener/gardener/gardener.gd
Normal file
778
addons/dreadpon.spatial_gardener/gardener/gardener.gd
Normal 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)
|
||||
534
addons/dreadpon.spatial_gardener/gardener/painter.gd
Normal file
534
addons/dreadpon.spatial_gardener/gardener/painter.gd
Normal 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)
|
||||
390
addons/dreadpon.spatial_gardener/greenhouse/greenhouse.gd
Normal file
390
addons/dreadpon.spatial_gardener/greenhouse/greenhouse.gd
Normal 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 ""
|
||||
@@ -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 ""
|
||||
1032
addons/dreadpon.spatial_gardener/greenhouse/greenhouse_plant.gd
Normal file
1032
addons/dreadpon.spatial_gardener/greenhouse/greenhouse_plant.gd
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 ""
|
||||
BIN
addons/dreadpon.spatial_gardener/icons/gardener_icon.png
Normal file
BIN
addons/dreadpon.spatial_gardener/icons/gardener_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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
|
||||
18
addons/dreadpon.spatial_gardener/icons/gardener_icon.svg
Normal file
18
addons/dreadpon.spatial_gardener/icons/gardener_icon.svg
Normal 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 |
@@ -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
|
||||
7
addons/dreadpon.spatial_gardener/plugin.cfg
Normal file
7
addons/dreadpon.spatial_gardener/plugin.cfg
Normal 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"
|
||||
482
addons/dreadpon.spatial_gardener/plugin.gd
Normal file
482
addons/dreadpon.spatial_gardener/plugin.gd
Normal 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)
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
0
addons/dreadpon.spatial_gardener/scene_converter/input_scenes/.gitignore
vendored
Normal file
0
addons/dreadpon.spatial_gardener/scene_converter/input_scenes/.gitignore
vendored
Normal file
0
addons/dreadpon.spatial_gardener/scene_converter/output_scenes/.gitignore
vendored
Normal file
0
addons/dreadpon.spatial_gardener/scene_converter/output_scenes/.gitignore
vendored
Normal 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
|
||||
@@ -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")
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
229
addons/dreadpon.spatial_gardener/toolshed/toolshed.gd
Normal file
229
addons/dreadpon.spatial_gardener/toolshed/toolshed.gd
Normal 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 ""
|
||||
296
addons/dreadpon.spatial_gardener/toolshed/toolshed_brush.gd
Normal file
296
addons/dreadpon.spatial_gardener/toolshed/toolshed_brush.gd
Normal 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 ""
|
||||
171
addons/dreadpon.spatial_gardener/utility/console/console.gd
Normal file
171
addons/dreadpon.spatial_gardener/utility/console/console.gd
Normal 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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 )
|
||||
@@ -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 )
|
||||
@@ -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")
|
||||
Binary file not shown.
@@ -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={}
|
||||
Binary file not shown.
@@ -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={}
|
||||
321
addons/dreadpon.spatial_gardener/utility/debug_draw.gd
Normal file
321
addons/dreadpon.spatial_gardener/utility/debug_draw.gd
Normal 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
|
||||
25
addons/dreadpon.spatial_gardener/utility/defaults.gd
Normal file
25
addons/dreadpon.spatial_gardener/utility/defaults.gd
Normal 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)
|
||||
])
|
||||
36
addons/dreadpon.spatial_gardener/utility/feature_manager.gd
Normal file
36
addons/dreadpon.spatial_gardener/utility/feature_manager.gd
Normal 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
|
||||
417
addons/dreadpon.spatial_gardener/utility/fun_lib.gd
Normal file
417
addons/dreadpon.spatial_gardener/utility/fun_lib.gd
Normal 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()
|
||||
331
addons/dreadpon.spatial_gardener/utility/globals.gd
Normal file
331
addons/dreadpon.spatial_gardener/utility/globals.gd
Normal 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",
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)]
|
||||
@@ -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)]
|
||||
@@ -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
|
||||
95
addons/dreadpon.spatial_gardener/utility/logger.gd
Normal file
95
addons/dreadpon.spatial_gardener/utility/logger.gd
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user