229 lines
7.1 KiB
GDScript
229 lines
7.1 KiB
GDScript
@tool
|
|
class_name ModelLocalizerSettings
|
|
extends RefCounted
|
|
|
|
## Configuration for Model Localizer.
|
|
##
|
|
## Everything lives in `project.godot` under `model_localizer/*`, so settings are
|
|
## per-project and show up in **Project Settings → Model Localizer**. The plugin
|
|
## UI writes these for you.
|
|
|
|
const ROOT := "model_localizer/"
|
|
|
|
const KEY_AUTO_PROCESS := ROOT + "auto_process_scene_instances"
|
|
const KEY_SWAP := ROOT + "auto_swap_to_local"
|
|
const KEY_MOVE_DROPPED := ROOT + "auto_import_dropped_files"
|
|
const KEY_DROP_DIR := ROOT + "dropped_files_dir"
|
|
const KEY_OUTPUT_DIR := ROOT + "output_dir"
|
|
const KEY_SUFFIX := ROOT + "output_suffix"
|
|
const KEY_COLLISION := ROOT + "add_collision"
|
|
const KEY_SHAPE := ROOT + "collision_shape"
|
|
const KEY_BODY := ROOT + "collision_body"
|
|
const KEY_FORCE_CONVEX := ROOT + "force_convex_hull"
|
|
const KEY_LAYER := ROOT + "collision_layer"
|
|
const KEY_MASK := ROOT + "collision_mask"
|
|
const KEY_SKIP_DIRS := ROOT + "skip_dirs"
|
|
|
|
## NOTE: this is a function rather than a `const` dictionary on purpose.
|
|
## GDScript requires constant dictionary *values* to be constant expressions too,
|
|
## so referencing other constants or building a PackedStringArray inside a const
|
|
## dictionary fails to compile ("isn't a constant expression"). A static function
|
|
## has no such restriction and is equally cheap to call.
|
|
static func project_info() -> Dictionary:
|
|
return {
|
|
KEY_AUTO_PROCESS: {
|
|
"name": KEY_AUTO_PROCESS,
|
|
"type": TYPE_BOOL,
|
|
"default": true,
|
|
"hint_string": "Process models dragged into the open scene",
|
|
},
|
|
KEY_SWAP: {
|
|
"name": KEY_SWAP,
|
|
"type": TYPE_BOOL,
|
|
"default": true,
|
|
"hint_string": "Replace the dropped instance with the generated local scene",
|
|
},
|
|
KEY_MOVE_DROPPED: {
|
|
"name": KEY_MOVE_DROPPED,
|
|
"type": TYPE_BOOL,
|
|
"default": true,
|
|
"hint_string": "Move model files dropped in from outside into Dropped Files Dir",
|
|
},
|
|
KEY_DROP_DIR: {
|
|
"name": KEY_DROP_DIR,
|
|
"type": TYPE_STRING,
|
|
"default": "res://models/",
|
|
"hint": PROPERTY_HINT_DIR,
|
|
"hint_string": "",
|
|
},
|
|
KEY_OUTPUT_DIR: {
|
|
"name": KEY_OUTPUT_DIR,
|
|
"type": TYPE_STRING,
|
|
"default": "presets",
|
|
"hint": PROPERTY_HINT_DIR,
|
|
"hint_string": "Relative -> a subfolder of the model's own directory; "
|
|
+ "absolute res:// path -> used as-is; empty -> next to the model",
|
|
},
|
|
KEY_SUFFIX: {
|
|
"name": KEY_SUFFIX,
|
|
"type": TYPE_STRING,
|
|
"default": "_local",
|
|
},
|
|
KEY_COLLISION: {
|
|
"name": KEY_COLLISION,
|
|
"type": TYPE_BOOL,
|
|
"default": true,
|
|
"hint_string": "Bake collision shapes from the model's meshes",
|
|
},
|
|
KEY_SHAPE: {
|
|
"name": KEY_SHAPE,
|
|
"type": TYPE_INT,
|
|
"default": 1,
|
|
"hint": PROPERTY_HINT_ENUM,
|
|
"hint_string": "Convex Hull,Trimesh,Box,Sphere,Capsule,Cylinder",
|
|
},
|
|
KEY_BODY: {
|
|
"name": KEY_BODY,
|
|
"type": TYPE_INT,
|
|
"default": 0,
|
|
"hint": PROPERTY_HINT_ENUM,
|
|
"hint_string": "StaticBody3D,RigidBody3D,Area3D,CharacterBody3D",
|
|
},
|
|
KEY_FORCE_CONVEX: {
|
|
"name": KEY_FORCE_CONVEX,
|
|
"type": TYPE_BOOL,
|
|
"default": true,
|
|
"hint_string": "Convex Hull only: build the hull from real triangles "
|
|
+ "(recommended for open meshes). Ignored by other shape types.",
|
|
},
|
|
KEY_LAYER: {
|
|
"name": KEY_LAYER,
|
|
"type": TYPE_INT,
|
|
"default": 1,
|
|
"hint": PROPERTY_HINT_LAYERS_3D_PHYSICS,
|
|
},
|
|
KEY_MASK: {
|
|
"name": KEY_MASK,
|
|
"type": TYPE_INT,
|
|
"default": 1,
|
|
"hint": PROPERTY_HINT_LAYERS_3D_PHYSICS,
|
|
},
|
|
KEY_SKIP_DIRS: {
|
|
"name": KEY_SKIP_DIRS,
|
|
"type": TYPE_PACKED_STRING_ARRAY,
|
|
"default": PackedStringArray(["res://addons/", "res://.godot/"]),
|
|
"hint": PROPERTY_HINT_ARRAY_TYPE,
|
|
"hint_string": "String",
|
|
},
|
|
}
|
|
|
|
|
|
static func _info(key: String) -> Dictionary:
|
|
var all := project_info()
|
|
if all.has(key):
|
|
return all[key]
|
|
return {"name": key, "type": TYPE_NIL, "default": null}
|
|
|
|
|
|
## Register every setting with ProjectSettings so they appear in the UI and carry
|
|
## a usable default even before the user touches anything.
|
|
static func install() -> void:
|
|
var all := project_info()
|
|
for key in all:
|
|
var info: Dictionary = all[key]
|
|
if not ProjectSettings.has_setting(key):
|
|
ProjectSettings.set_setting(key, info["default"])
|
|
ProjectSettings.add_property_info(info)
|
|
# Mark the default as the initial value so an untouched setting is not
|
|
# reported as a modified project setting.
|
|
ProjectSettings.set_initial_value(key, info["default"])
|
|
|
|
|
|
static func get_bool(key: String) -> bool:
|
|
return bool(ProjectSettings.get_setting(key, _info(key)["default"]))
|
|
|
|
|
|
static func get_int(key: String) -> int:
|
|
return int(ProjectSettings.get_setting(key, _info(key)["default"]))
|
|
|
|
|
|
static func get_string(key: String) -> String:
|
|
return String(ProjectSettings.get_setting(key, _info(key)["default"]))
|
|
|
|
|
|
static func get_dirs(key: String) -> PackedStringArray:
|
|
var value = ProjectSettings.get_setting(key, _info(key)["default"])
|
|
if value is PackedStringArray:
|
|
return value
|
|
var out := PackedStringArray()
|
|
if value is Array:
|
|
for v in value:
|
|
out.append(String(v))
|
|
return out
|
|
|
|
|
|
static func set_value(key: String, value) -> void:
|
|
ProjectSettings.set_setting(key, value)
|
|
|
|
|
|
static func shape_type() -> ModelLocalizer.ShapeType:
|
|
return clampi(get_int(KEY_SHAPE), 0,
|
|
ModelLocalizer.SHAPE_TYPE_NAMES.size() - 1) as ModelLocalizer.ShapeType
|
|
|
|
|
|
static func body_type() -> ModelLocalizer.BodyType:
|
|
return clampi(get_int(KEY_BODY), 0,
|
|
ModelLocalizer.BODY_TYPE_NAMES.size() - 1) as ModelLocalizer.BodyType
|
|
|
|
|
|
## The resolved output directory for `model_res_path`, with a trailing slash.
|
|
##
|
|
## `output_dir` may be:
|
|
## * empty -> the model's own directory
|
|
## * a relativ path -> a subfolder of the model's directory (default: `presets/`)
|
|
## * a `res://` path -> used exactly as given
|
|
static func resolved_output_dir(model_res_path: String) -> String:
|
|
var model_dir := model_res_path.get_base_dir()
|
|
if model_dir.is_empty():
|
|
model_dir = "res://"
|
|
var configured := get_string(KEY_OUTPUT_DIR).strip_edges().replace("\\", "/")
|
|
|
|
if configured.is_empty():
|
|
return _with_trailing_slash(model_dir)
|
|
if configured.begins_with("res://"):
|
|
return _with_trailing_slash(configured)
|
|
var relative := configured.trim_prefix("./").trim_prefix("/")
|
|
if relative.is_empty():
|
|
return _with_trailing_slash(model_dir)
|
|
return _with_trailing_slash(model_dir.path_join(relative))
|
|
|
|
|
|
static func _with_trailing_slash(path: String) -> String:
|
|
if path.ends_with("/"):
|
|
return path
|
|
return path + "/"
|
|
|
|
|
|
## Where the generated scene for `model_res_path` should be written.
|
|
static func output_path_for(model_res_path: String, fallback_suffix: String = "_local") -> String:
|
|
var suffix := get_string(KEY_SUFFIX)
|
|
if suffix.is_empty():
|
|
suffix = fallback_suffix
|
|
return resolved_output_dir(model_res_path) \
|
|
+ model_res_path.get_file().get_basename() + suffix + ".tscn"
|
|
|
|
|
|
## True when `path` is inside a directory the plugin must never touch.
|
|
##
|
|
## The generated scenes do not need protecting here: the filesystem scan only
|
|
## looks at model extensions, and a generated scene is always a `.tscn`.
|
|
static func should_skip(path: String) -> bool:
|
|
var p := path.replace("\\", "/")
|
|
for dir in get_dirs(KEY_SKIP_DIRS):
|
|
var d := dir.replace("\\", "/")
|
|
if not d.ends_with("/"):
|
|
d += "/"
|
|
if p.begins_with(d):
|
|
return true
|
|
return false
|