built more assets and started playing with foliage painting

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

View File

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

View File

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

View File

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

View File

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