Added footsteps, new tree, various other tweaks
This commit is contained in:
284
addons/proton_scatter/src/documentation/documentation.gd
Normal file
284
addons/proton_scatter/src/documentation/documentation.gd
Normal file
@@ -0,0 +1,284 @@
|
||||
@tool
|
||||
extends PopupPanel
|
||||
|
||||
|
||||
# Formats and displays the DocumentationData provided by other parts of the addon
|
||||
# TODO: Adjust title font size based on the editor font size / scaling
|
||||
|
||||
|
||||
const DocumentationInfo = preload("./documentation_info.gd")
|
||||
const SpecialPages = preload("./pages/special_pages.gd")
|
||||
|
||||
var _pages := {}
|
||||
var _items := {}
|
||||
var _categories_roots := {}
|
||||
|
||||
var _modifiers_root: TreeItem
|
||||
|
||||
var _edited_text: String
|
||||
var _accent_color := Color.CORNFLOWER_BLUE
|
||||
var _editor_scale := 1.0
|
||||
var _header_size := 20
|
||||
var _sub_header_size := 16
|
||||
|
||||
var _populated := false
|
||||
|
||||
|
||||
@onready var tree: Tree = $HSplitContainer/Tree
|
||||
@onready var label: RichTextLabel = $HSplitContainer/RichTextLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
tree.create_item() # Create tree root
|
||||
tree.hide_root = true
|
||||
tree.item_selected.connect(_on_item_selected)
|
||||
|
||||
add_page(SpecialPages.get_scatter_documentation(), tree.create_item())
|
||||
add_page(SpecialPages.get_item_documentation(), tree.create_item())
|
||||
add_page(SpecialPages.get_shape_documentation(), tree.create_item())
|
||||
add_page(SpecialPages.get_cache_documentation(), tree.create_item())
|
||||
|
||||
_modifiers_root = tree.create_item()
|
||||
add_page(SpecialPages.get_modifiers_documentation(), _modifiers_root)
|
||||
|
||||
_populate()
|
||||
|
||||
|
||||
# Fed from the StackPanel scene, before the ready function
|
||||
func set_editor_plugin(editor_plugin: EditorPlugin) -> void:
|
||||
if not editor_plugin:
|
||||
return
|
||||
|
||||
var editor_interface := editor_plugin.get_editor_interface()
|
||||
var editor_settings := editor_interface.get_editor_settings()
|
||||
|
||||
_accent_color = editor_settings.get("interface/theme/accent_color")
|
||||
_editor_scale = editor_interface.get_editor_scale()
|
||||
|
||||
|
||||
func show_page(page_name: String) -> void:
|
||||
if not page_name in _items:
|
||||
return
|
||||
|
||||
var item: TreeItem = _items[page_name]
|
||||
item.select(0)
|
||||
popup_centered(Vector2i(900, 600))
|
||||
|
||||
|
||||
# Generate a formatted string from the DocumentationInfo input.
|
||||
# This string will be stored and later displayed in the RichTextLabel so we
|
||||
# we don't have to regenerate it everytime we look at another page.
|
||||
func add_page(info: DocumentationInfo, item: TreeItem = null) -> void:
|
||||
if not item:
|
||||
var root: TreeItem = _get_or_create_tree_root(info.get_category())
|
||||
item = tree.create_item(root)
|
||||
|
||||
item.set_text(0, info.get_title())
|
||||
|
||||
_begin_formatting()
|
||||
|
||||
# Page title
|
||||
_format_title(info.get_title())
|
||||
|
||||
# Paragraphs
|
||||
for p in info.get_paragraphs():
|
||||
_format_paragraph(p)
|
||||
|
||||
# Parameters
|
||||
if not info.get_parameters().is_empty():
|
||||
_format_subtitle("Parameters")
|
||||
|
||||
for p in info.get_parameters():
|
||||
_format_parameter(p)
|
||||
|
||||
# Warnings
|
||||
if not info.get_warnings().is_empty():
|
||||
_format_subtitle("Warnings")
|
||||
|
||||
for w in info.get_warnings():
|
||||
_format_warning(w)
|
||||
|
||||
_pages[item] = _get_formatted_text()
|
||||
_items[info.get_title()] = item
|
||||
|
||||
|
||||
func _populate():
|
||||
if _populated: # Already generated the documentation pages
|
||||
return
|
||||
|
||||
var path = _get_root_folder() + "/src/modifiers/"
|
||||
var result := {}
|
||||
_discover_modifiers_recursive(path, result)
|
||||
|
||||
var names := result.keys()
|
||||
names.sort()
|
||||
|
||||
for n in names:
|
||||
var info = result[n]
|
||||
add_page(info)
|
||||
|
||||
_populated = true
|
||||
|
||||
|
||||
func _discover_modifiers_recursive(path, result) -> void:
|
||||
var dir = DirAccess.open(path)
|
||||
dir.list_dir_begin()
|
||||
var path_root = dir.get_current_dir() + "/"
|
||||
|
||||
while true:
|
||||
var file = dir.get_next()
|
||||
if file == "":
|
||||
break
|
||||
if file == "base_modifier.gd":
|
||||
continue
|
||||
if dir.current_is_dir():
|
||||
_discover_modifiers_recursive(path_root + file, result)
|
||||
continue
|
||||
if not file.ends_with(".gd") and not file.ends_with(".gdc"):
|
||||
continue
|
||||
|
||||
var full_path = path_root + file
|
||||
var script = load(full_path)
|
||||
if not script or not script.can_instantiate():
|
||||
print("Error: Failed to load script ", file)
|
||||
continue
|
||||
|
||||
var modifier = script.new()
|
||||
|
||||
var info: DocumentationInfo = modifier.documentation
|
||||
info.set_title(modifier.display_name)
|
||||
info.set_category(modifier.category)
|
||||
if modifier.use_edge_data:
|
||||
info.add_warning(
|
||||
"This modifier uses edge data (represented by the blue lines
|
||||
on the Scatter node). These edges are usually locked to the
|
||||
local XZ plane, (except for the Path shape when they are
|
||||
NOT closed). If you can't see these lines, make sure to have at
|
||||
least one Shape crossing the ProtonScatter local XZ plane.",
|
||||
1)
|
||||
|
||||
if modifier.deprecated:
|
||||
info.add_warning(
|
||||
"This modifier has been deprecated. It won't receive any updates
|
||||
and will be deleted in a future update.",
|
||||
2)
|
||||
|
||||
result[modifier.display_name] = info
|
||||
|
||||
dir.list_dir_end()
|
||||
|
||||
|
||||
func _get_root_folder() -> String:
|
||||
var script: Script = get_script()
|
||||
var path: String = script.get_path().get_base_dir()
|
||||
var folders = path.right(-6) # Remove the res://
|
||||
var tokens = folders.split('/')
|
||||
return "res://" + tokens[0] + "/" + tokens[1]
|
||||
|
||||
|
||||
func _get_or_create_tree_root(root_name: String) -> TreeItem:
|
||||
if root_name in _categories_roots:
|
||||
return _categories_roots[root_name]
|
||||
|
||||
var root = tree.create_item(_modifiers_root)
|
||||
root.set_text(0, root_name)
|
||||
root.set_selectable(0, false)
|
||||
_categories_roots[root_name] = root
|
||||
return root
|
||||
|
||||
|
||||
func _begin_formatting() -> void:
|
||||
_edited_text = ""
|
||||
|
||||
|
||||
func _get_formatted_text() -> String:
|
||||
return _edited_text
|
||||
|
||||
|
||||
func _format_title(text: String) -> void:
|
||||
_edited_text += "[font_size=" + var_to_str(_header_size * _editor_scale) + "]"
|
||||
_edited_text += "[color=" + _accent_color.to_html() + "]"
|
||||
_edited_text += "[center][b]"
|
||||
_edited_text += text
|
||||
_edited_text += "[/b][/center]"
|
||||
_edited_text += "[/color]"
|
||||
_edited_text += "[/font_size]"
|
||||
_format_line_break(2)
|
||||
|
||||
|
||||
func _format_subtitle(text: String) -> void:
|
||||
_edited_text += "[font_size=" + var_to_str(_header_size * _editor_scale) + "]"
|
||||
_edited_text += "[color=" + _accent_color.to_html() + "]"
|
||||
_edited_text += "[b]" + text + "[/b]"
|
||||
_edited_text += "[/color]"
|
||||
_edited_text += "[/font_size]"
|
||||
_format_line_break(2)
|
||||
|
||||
|
||||
func _format_line_break(count := 1) -> void:
|
||||
for i in count:
|
||||
_edited_text += "\n"
|
||||
|
||||
|
||||
func _format_paragraph(text: String) -> void:
|
||||
_edited_text += "[p]" + text + "[/p]"
|
||||
_format_line_break(2)
|
||||
|
||||
|
||||
func _format_parameter(p) -> void:
|
||||
var root_folder = _get_root_folder()
|
||||
|
||||
_edited_text += "[indent]"
|
||||
|
||||
if not p.type.is_empty():
|
||||
var file_name = p.type.to_lower() + ".svg"
|
||||
_edited_text += "[img]" + root_folder + "/icons/types/" + file_name + "[/img] "
|
||||
|
||||
_edited_text += "[b]" + p.name + "[/b] "
|
||||
|
||||
match p.cost:
|
||||
1:
|
||||
_edited_text += "[img]" + root_folder + "/icons/arrow_log.svg[/img]"
|
||||
2:
|
||||
_edited_text += "[img]" + root_folder + "/icons/arrow_linear.svg[/img]"
|
||||
3:
|
||||
_edited_text += "[img]" + root_folder + "/icons/arrow_exp.svg[/img]"
|
||||
|
||||
_format_line_break(2)
|
||||
_edited_text += "[indent]" + p.description + "[/indent]"
|
||||
_format_line_break(2)
|
||||
|
||||
for warning in p.warnings:
|
||||
if not warning.text.is_empty():
|
||||
_format_warning(warning)
|
||||
|
||||
_edited_text += "[/indent]"
|
||||
|
||||
|
||||
func _format_warning(w, indent := true) -> void:
|
||||
if indent:
|
||||
_edited_text += "[indent]"
|
||||
|
||||
var color := "Darkgray"
|
||||
match w.importance:
|
||||
1:
|
||||
color = "yellow"
|
||||
2:
|
||||
color = "red"
|
||||
|
||||
_edited_text += "[color=" + color + "][i]" + w.text + "[/i][/color]\n"
|
||||
|
||||
if indent:
|
||||
_edited_text += "[/indent]"
|
||||
|
||||
_format_line_break(1)
|
||||
|
||||
|
||||
func _on_item_selected() -> void:
|
||||
var selected: TreeItem = tree.get_selected()
|
||||
|
||||
if _pages.has(selected):
|
||||
var text: String = _pages[selected]
|
||||
label.set_text(text)
|
||||
else:
|
||||
label.set_text("[center] Under construction [/center]")
|
||||
29
addons/proton_scatter/src/documentation/documentation.tscn
Normal file
29
addons/proton_scatter/src/documentation/documentation.tscn
Normal file
@@ -0,0 +1,29 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://cfg8iqtuion8b"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/proton_scatter/src/documentation/documentation.gd" id="1_5c4lw"]
|
||||
[ext_resource type="PackedScene" uid="uid://cojoo2c73fpsb" path="res://addons/proton_scatter/src/documentation/panel.tscn" id="2_vpfxu"]
|
||||
|
||||
[node name="Documentation" type="PopupPanel"]
|
||||
title = "ProtonScatter documentation"
|
||||
exclusive = true
|
||||
unresizable = false
|
||||
borderless = false
|
||||
script = ExtResource("1_5c4lw")
|
||||
|
||||
[node name="HSplitContainer" parent="." instance=ExtResource("2_vpfxu")]
|
||||
offset_left = 4.0
|
||||
offset_top = 4.0
|
||||
offset_right = -1824.0
|
||||
offset_bottom = -984.0
|
||||
|
||||
[node name="Tree" parent="HSplitContainer" index="0"]
|
||||
offset_right = 80.0
|
||||
offset_bottom = 92.0
|
||||
hide_root = true
|
||||
|
||||
[node name="RichTextLabel" parent="HSplitContainer" index="1"]
|
||||
offset_left = 92.0
|
||||
offset_right = 92.0
|
||||
offset_bottom = 92.0
|
||||
|
||||
[editable path="HSplitContainer"]
|
||||
113
addons/proton_scatter/src/documentation/documentation_info.gd
Normal file
113
addons/proton_scatter/src/documentation/documentation_info.gd
Normal file
@@ -0,0 +1,113 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# Stores raw documentation data.
|
||||
|
||||
# The data is provided by any class that needs an entry in the documentation
|
||||
# panel. This was initially designed for all the modifiers, but might be expanded
|
||||
# to other parts of the addon as well.
|
||||
|
||||
# Formatting is handled by the main Documentation class.
|
||||
|
||||
const Util := preload("../common/util.gd")
|
||||
|
||||
|
||||
class Warning:
|
||||
var text: String
|
||||
var importance: int
|
||||
|
||||
class Parameter:
|
||||
var name: String
|
||||
var cost: int
|
||||
var type: String
|
||||
var description: String
|
||||
var warnings: Array[Warning] = []
|
||||
|
||||
func set_name(text: String) -> Parameter:
|
||||
name = Util.remove_line_breaks(text)
|
||||
return self
|
||||
|
||||
func set_description(text: String) -> Parameter:
|
||||
description = Util.remove_line_breaks(text)
|
||||
return self
|
||||
|
||||
func set_cost(val: int) -> Parameter:
|
||||
cost = val
|
||||
return self
|
||||
|
||||
func set_type(val: String) -> Parameter:
|
||||
type = Util.remove_line_breaks(val)
|
||||
return self
|
||||
|
||||
func add_warning(warning: String, warning_importance := -1) -> Parameter:
|
||||
var w = Warning.new()
|
||||
w.text = Util.remove_line_breaks(warning)
|
||||
w.importance = warning_importance
|
||||
warnings.push_back(w)
|
||||
return self
|
||||
|
||||
|
||||
var _category: String
|
||||
var _page_title: String
|
||||
var _paragraphs: Array[String] = []
|
||||
var _warnings: Array[Warning] = []
|
||||
var _parameters: Array[Parameter] = []
|
||||
|
||||
|
||||
func set_category(text: String) -> void:
|
||||
_category = text
|
||||
|
||||
|
||||
func set_title(text: String) -> void:
|
||||
_page_title = text
|
||||
|
||||
|
||||
func add_paragraph(text: String) -> void:
|
||||
_paragraphs.push_back(Util.remove_line_breaks(text))
|
||||
|
||||
|
||||
# Warning importance:
|
||||
# 0: Default (Grey)
|
||||
# 1: Mid (Yellow)
|
||||
# 2: Critical (Red)
|
||||
func add_warning(text: String, importance: int = 0) -> void:
|
||||
var w = Warning.new()
|
||||
w.text = Util.remove_line_breaks(text)
|
||||
w.importance = importance
|
||||
|
||||
_warnings.push_back(w)
|
||||
|
||||
|
||||
# Add documentation for a user exposed parameter.
|
||||
# Cost:
|
||||
# 0: None
|
||||
# 1: Log
|
||||
# 2: Linear
|
||||
# 3: Exponential
|
||||
func add_parameter(name := "") -> Parameter:
|
||||
var p = Parameter.new()
|
||||
p.name = name
|
||||
p.cost = 0
|
||||
_parameters.push_back(p)
|
||||
return p
|
||||
|
||||
|
||||
func get_title() -> String:
|
||||
return _page_title
|
||||
|
||||
|
||||
func get_category() -> String:
|
||||
return _category
|
||||
|
||||
|
||||
func get_paragraphs() -> Array[String]:
|
||||
return _paragraphs
|
||||
|
||||
|
||||
func get_warnings() -> Array[Warning]:
|
||||
return _warnings
|
||||
|
||||
|
||||
func get_parameters() -> Array[Parameter]:
|
||||
return _parameters
|
||||
150
addons/proton_scatter/src/documentation/pages/special_pages.gd
Normal file
150
addons/proton_scatter/src/documentation/pages/special_pages.gd
Normal file
@@ -0,0 +1,150 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
const DocumentationInfo = preload("../documentation_info.gd")
|
||||
|
||||
|
||||
static func get_scatter_documentation() -> DocumentationInfo:
|
||||
var info := DocumentationInfo.new()
|
||||
|
||||
info.set_title("ProtonScatter")
|
||||
info.add_paragraph(
|
||||
"ProtonScatter is a content positioning add-on. It is suited to place
|
||||
a large amount of objects in a procedural way.")
|
||||
info.add_paragraph(
|
||||
"This add-on is [color=red][b]IN BETA[/b][/color] which means breaking
|
||||
changes may happen. It is not recommended to use in production yet."
|
||||
)
|
||||
info.add_paragraph(
|
||||
"First, define [i]what[/i] you want to place using [b]ScatterItems[/b]
|
||||
nodes.")
|
||||
info.add_paragraph(
|
||||
"Then, define [i]where[/i] to place them using [b]ScatterShapes[/b]
|
||||
nodes.")
|
||||
info.add_paragraph(
|
||||
"Finaly, define [i]how[/i] the content should be placed using the
|
||||
[b]Modifier stack[/b] that's on the [b]ProtonScatter[/b] node.")
|
||||
info.add_paragraph(
|
||||
"Each of these components have their dedicated documenation page, but
|
||||
first, you should check out the example scenes in the demo folder.")
|
||||
|
||||
var p := info.add_parameter("General / Global seed")
|
||||
p.set_type("int")
|
||||
p.set_description(
|
||||
"The random seed to use on this node. Modifiers using random components
|
||||
can access this value and use it accordingly. You can also specify
|
||||
a custom seed for specific modifiers as well.")
|
||||
|
||||
p = info.add_parameter("General / Show output in tree")
|
||||
p.set_type("bool")
|
||||
p.set_description(
|
||||
"Show the generated items in the editor scene tree. By default this
|
||||
option is disabled as it creates quite a bit of clutter when instancing
|
||||
is disabled. It also increases the scene file size significantly.")
|
||||
|
||||
p = info.add_parameter("Performance / Use instancing")
|
||||
p.set_type("bool")
|
||||
p.set_description(
|
||||
"When enabled, ProtonScatter will use MultiMeshInstance3D nodes
|
||||
instead of duplicating the source nodes. This allows the GPU to render
|
||||
thousands of meshes in a single draw call.")
|
||||
p.add_warning("Collisions and attached scripts are ignored when this
|
||||
option is enabled.", 1)
|
||||
|
||||
return info
|
||||
|
||||
|
||||
static func get_item_documentation() -> DocumentationInfo:
|
||||
var info := DocumentationInfo.new()
|
||||
|
||||
info.set_title("ScatterItems")
|
||||
|
||||
info.add_paragraph("TODO: Write this page")
|
||||
|
||||
return info
|
||||
|
||||
|
||||
static func get_shape_documentation() -> DocumentationInfo:
|
||||
var info := DocumentationInfo.new()
|
||||
|
||||
info.set_title("ScatterShapes")
|
||||
|
||||
info.add_paragraph("TODO: Write this page")
|
||||
|
||||
return info
|
||||
|
||||
|
||||
static func get_cache_documentation() -> DocumentationInfo:
|
||||
var info := DocumentationInfo.new()
|
||||
|
||||
info.set_title("ScatterCache")
|
||||
|
||||
info.add_paragraph(
|
||||
"By default, Scatter nodes will recalculate their output on load,
|
||||
which can be slow in really complex scenes. The cache allows you to
|
||||
store these results in a file on your disk, and load these instead.")
|
||||
info.add_paragraph(
|
||||
"This can significantly speed up loading times, while also being VCS
|
||||
friendly since the transforms are stored in their own files, rather
|
||||
than your scenes files.")
|
||||
info.add_paragraph("[b]How to use:[/b]")
|
||||
info.add_paragraph(
|
||||
"[p]+ Disable the [code]Force rebuild on load[code] on every Scatter item you want to cache.[/p]
|
||||
[p]+ Add a ScatterCache node anywhere in your scene.[/p]
|
||||
[p]+ Press the 'Rebuild' button to scan for other ProtonScatter nodes
|
||||
and store their results in the cache.[/p]")
|
||||
info.add_paragraph("[i]A single cache per scene is enough.[/i]")
|
||||
|
||||
var p := info.add_parameter("Cache File")
|
||||
p.set_cost(0)
|
||||
p.set_description("Path to the cache file. By default they are store in the
|
||||
add-on folder. Their name has a random component to avoid naming collisions
|
||||
with scenes sharing the same file name. You are free to place this file
|
||||
anywhere, using any name you would like.")
|
||||
|
||||
return info
|
||||
|
||||
|
||||
static func get_modifiers_documentation() -> DocumentationInfo:
|
||||
var info := DocumentationInfo.new()
|
||||
|
||||
info.set_title("Modifiers")
|
||||
info.add_paragraph(
|
||||
"A modifier takes in a Transform3D list, create, modify or delete
|
||||
transforms, then pass it down to the next modifier. Remember that
|
||||
[b] modifiers are processed from top to bottom [/b]. A modifier
|
||||
down the stack will recieve a list processed by the modifiers above.")
|
||||
info.add_paragraph(
|
||||
"The initial transform list is empty, so it's necessary to start the
|
||||
stack with a [b] Create [/b] modifier.")
|
||||
info.add_paragraph(
|
||||
"When clicking the [b] Expand button [/b] (the little arrow on the left)
|
||||
you get access to this modifier's parameters. This is where you can
|
||||
adjust its behavior according to your needs.")
|
||||
info.add_paragraph(
|
||||
"Three common options might be found on these modifiers. (They may
|
||||
not appear if they are irrelevant). They are defined as follow:")
|
||||
|
||||
var p := info.add_parameter("Use local seed")
|
||||
p.set_type("bool")
|
||||
p.set_description(
|
||||
"The dice icon on the left allows you to force a specific seed for the
|
||||
modifier. If this option is not used then the Global seed from the
|
||||
ProtonScatter node will be used instead.")
|
||||
|
||||
p = info.add_parameter("Restrict height")
|
||||
p.set_type("bool")
|
||||
p.set_description(
|
||||
"When applicable, the modifier will remain within the local XZ plane
|
||||
instead of using the full volume described by the ScatterShape nodes.")
|
||||
|
||||
p = info.add_parameter("Reference frame")
|
||||
p.set_type("int")
|
||||
p.set_description(
|
||||
"[p]+ [b]Global[/b]: Modifier operates in Global space. [/p]
|
||||
[p]+ [b]Local[/b]: Modifier operates in local space, relative to the ProtonScatter node.[/p]
|
||||
[p]+ [b]Individual[/b]: Modifier operates on local space, relative to each
|
||||
individual transforms.[/p]"
|
||||
)
|
||||
|
||||
return info
|
||||
22
addons/proton_scatter/src/documentation/panel.tscn
Normal file
22
addons/proton_scatter/src/documentation/panel.tscn
Normal file
@@ -0,0 +1,22 @@
|
||||
[gd_scene format=3 uid="uid://cojoo2c73fpsb"]
|
||||
|
||||
[node name="HSplitContainer" type="HSplitContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
split_offset = 250
|
||||
|
||||
[node name="Tree" type="Tree" parent="."]
|
||||
layout_mode = 2
|
||||
offset_right = 250.0
|
||||
offset_bottom = 648.0
|
||||
|
||||
[node name="RichTextLabel" type="RichTextLabel" parent="."]
|
||||
layout_mode = 2
|
||||
offset_left = 262.0
|
||||
offset_right = 1152.0
|
||||
offset_bottom = 648.0
|
||||
bbcode_enabled = true
|
||||
text = "[center] [b] [i] Documentation page [/i] [/b] [/center]"
|
||||
Reference in New Issue
Block a user