Explosion_der_Spiele/scripts/dev/scale_lab.gd

473 lines
14 KiB
GDScript3
Raw Normal View History

extends Node3D
## Editor scale lab: tune ModelProp fit_* vs measuring wall + 1.74 m player.
## Open scenes/dev/scale_lab.tscn — Save writes data/model_scales.json (runtime SSOT).
const MODELS_ROOT := "res://assets/3DModelle"
const MODEL_PROP_SCENE := "res://scenes/prefabs/model_prop.tscn"
const WALL_MAX_M := 4.0
const PREVIEW_POS := Vector3(0.0, 0.0, 0.0)
const PLAYER_REF_POS := Vector3(-1.6, 0.0, 0.6)
var _models: Array[Dictionary] = [] ## {key, path, label}
var _selected: int = -1
var _preview: ModelProp
var _working: Dictionary = {} ## key -> {fit_height, fit_max_width, notes?}
var _status_label: Label
var _height_spin: SpinBox
var _width_spin: SpinBox
var _list: ItemList
var _updating_ui: bool = false
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_build_environment()
_build_measuring_wall()
_build_player_reference()
_build_ui()
_load_working_from_catalog()
_scan_models()
_populate_list()
if not _models.is_empty():
_list.select(0)
_select_index(0)
_update_camera()
func _build_environment() -> void:
var env := Environment.new()
env.background_mode = Environment.BG_COLOR
env.background_color = Color(0.55, 0.72, 0.88)
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.ambient_light_color = Color(0.85, 0.88, 0.92)
env.ambient_light_energy = 0.65
$WorldEnvironment.environment = env
func _build_measuring_wall() -> void:
var wall_root := Node3D.new()
wall_root.name = "MeasuringWall"
wall_root.position = Vector3(0.0, 0.0, -2.2)
add_child(wall_root)
## Back panel
var panel := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(6.0, WALL_MAX_M + 0.4, 0.08)
panel.mesh = box
panel.position = Vector3(0.0, (WALL_MAX_M + 0.4) * 0.5, 0.0)
var panel_mat := StandardMaterial3D.new()
panel_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
panel_mat.albedo_color = Color(0.92, 0.92, 0.88)
panel.material_override = panel_mat
wall_root.add_child(panel)
## Tick marks every 0.1 m; stronger at 0.5 / 1.0
for i in range(int(WALL_MAX_M * 10.0) + 1):
var y := float(i) * 0.1
var major := (i % 10) == 0
var mid := (i % 5) == 0
var tick := MeshInstance3D.new()
var tbox := BoxMesh.new()
var len := 0.35 if major else (0.22 if mid else 0.12)
var thick := 0.025 if major else (0.018 if mid else 0.01)
tbox.size = Vector3(len, thick, 0.04)
tick.mesh = tbox
tick.position = Vector3(-2.6 + len * 0.5, y, 0.06)
var tmat := StandardMaterial3D.new()
tmat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
if absf(y - SpriteFit.WORLD_PLAYER_HEIGHT) < 0.001:
tmat.albedo_color = Color(0.85, 0.15, 0.1)
tbox.size = Vector3(0.55, 0.035, 0.05)
tick.position.x = -2.6 + 0.275
elif major:
tmat.albedo_color = Color(0.12, 0.12, 0.14)
else:
tmat.albedo_color = Color(0.35, 0.35, 0.38)
tick.material_override = tmat
wall_root.add_child(tick)
if major or absf(y - SpriteFit.WORLD_PLAYER_HEIGHT) < 0.001:
var label := Label3D.new()
if absf(y - SpriteFit.WORLD_PLAYER_HEIGHT) < 0.001:
label.text = "1.74 m player"
label.modulate = Color(0.9, 0.2, 0.15)
else:
label.text = "%.0f m" % y
label.modulate = Color(0.1, 0.1, 0.12)
label.font_size = 48
label.pixel_size = 0.0045
label.position = Vector3(-2.95, y, 0.08)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
label.billboard = BaseMaterial3D.BILLBOARD_DISABLED
wall_root.add_child(label)
## Floor grid strip in front of wall
var floor_grid := MeshInstance3D.new()
var fbox := BoxMesh.new()
fbox.size = Vector3(6.0, 0.02, 4.0)
floor_grid.mesh = fbox
floor_grid.position = Vector3(0.0, 0.01, 0.0)
var fmat := StandardMaterial3D.new()
fmat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
fmat.albedo_color = Color(0.45, 0.5, 0.48)
floor_grid.material_override = fmat
add_child(floor_grid)
func _build_player_reference() -> void:
var root := Node3D.new()
root.name = "PlayerReference"
root.position = PLAYER_REF_POS
add_child(root)
var capsule := MeshInstance3D.new()
var mesh := CapsuleMesh.new()
mesh.radius = 0.22
mesh.height = SpriteFit.WORLD_PLAYER_HEIGHT
capsule.mesh = mesh
capsule.position = Vector3(0.0, SpriteFit.WORLD_PLAYER_HEIGHT * 0.5, 0.0)
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.albedo_color = Color(0.2, 0.45, 0.95, 0.55)
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
capsule.material_override = mat
root.add_child(capsule)
var tag := Label3D.new()
tag.text = "Player 1.74 m"
tag.font_size = 42
tag.pixel_size = 0.004
tag.position = Vector3(0.0, SpriteFit.WORLD_PLAYER_HEIGHT + 0.15, 0.0)
tag.modulate = Color(0.15, 0.35, 0.9)
root.add_child(tag)
func _build_ui() -> void:
var layer := CanvasLayer.new()
layer.name = "LabUI"
layer.layer = 80
add_child(layer)
var panel := PanelContainer.new()
panel.set_anchors_preset(Control.PRESET_TOP_LEFT)
panel.offset_left = 12
panel.offset_top = 12
panel.offset_right = 360
panel.offset_bottom = 520
layer.add_child(panel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 10)
margin.add_theme_constant_override("margin_right", 10)
margin.add_theme_constant_override("margin_top", 8)
margin.add_theme_constant_override("margin_bottom", 8)
panel.add_child(margin)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 6)
margin.add_child(vbox)
var title := Label.new()
title.text = "Scale Lab"
title.add_theme_font_size_override("font_size", 20)
vbox.add_child(title)
var hint := Label.new()
hint.text = "Wall ticks = meters. Red = player 1.74 m.\nSave → data/model_scales.json"
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
vbox.add_child(hint)
_list = ItemList.new()
_list.custom_minimum_size = Vector2(0, 220)
_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
_list.item_selected.connect(_on_list_selected)
vbox.add_child(_list)
vbox.add_child(_make_spin_row("fit_height (m)", 0.01, 20.0, 0.01, true))
vbox.add_child(_make_spin_row("fit_max_width (m)", 0.0, 30.0, 0.05, false))
var buttons := HBoxContainer.new()
buttons.add_theme_constant_override("separation", 6)
vbox.add_child(buttons)
var save_btn := Button.new()
save_btn.text = "Save"
save_btn.pressed.connect(_on_save)
buttons.add_child(save_btn)
var reset_btn := Button.new()
reset_btn.text = "Reset defaults"
reset_btn.pressed.connect(_on_reset)
buttons.add_child(reset_btn)
var reload_btn := Button.new()
reload_btn.text = "Reload JSON"
reload_btn.pressed.connect(_on_reload)
buttons.add_child(reload_btn)
_status_label = Label.new()
_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_status_label.text = "Ready."
vbox.add_child(_status_label)
var cam_hint := Label.new()
cam_hint.text = "Orbit: RMB drag · Zoom: wheel · WASD move cam"
cam_hint.modulate = Color(0.7, 0.7, 0.75)
vbox.add_child(cam_hint)
func _make_spin_row(label_text: String, min_v: float, max_v: float, step: float, is_height: bool) -> Control:
var row := VBoxContainer.new()
var lab := Label.new()
lab.text = label_text
row.add_child(lab)
var spin := SpinBox.new()
spin.min_value = min_v
spin.max_value = max_v
spin.step = step
spin.allow_greater = true
spin.allow_lesser = false
if is_height:
spin.value_changed.connect(_on_height_changed)
_height_spin = spin
else:
spin.value_changed.connect(_on_width_changed)
_width_spin = spin
row.add_child(spin)
return row
func _load_working_from_catalog() -> void:
SpriteFit.reload_scale_catalog()
_working.clear()
## Seed from defaults, then overlay file.
var defaults: Dictionary = SpriteFit.default_scale_catalog().get("scales", {})
for key in defaults.keys():
_working[str(key)] = (defaults[key] as Dictionary).duplicate(true)
if FileAccess.file_exists(SpriteFit.SCALE_CATALOG_PATH):
var file := FileAccess.open(SpriteFit.SCALE_CATALOG_PATH, FileAccess.READ)
if file:
var parsed: Variant = JSON.parse_string(file.get_as_text())
if typeof(parsed) == TYPE_DICTIONARY:
var scales: Variant = (parsed as Dictionary).get("scales", {})
if typeof(scales) == TYPE_DICTIONARY:
for key in (scales as Dictionary).keys():
var entry: Variant = scales[key]
if typeof(entry) == TYPE_DICTIONARY:
_working[str(key)] = (entry as Dictionary).duplicate(true)
func _scan_models() -> void:
_models.clear()
for category in ["animals", "houses", "props"]:
var cat_dir := "%s/%s" % [MODELS_ROOT, category]
var dir := DirAccess.open(cat_dir)
if dir == null:
continue
dir.list_dir_begin()
var folder := dir.get_next()
while folder != "":
if dir.current_is_dir() and not folder.begins_with("."):
var sub := "%s/%s" % [cat_dir, folder]
var subdir := DirAccess.open(sub)
if subdir:
subdir.list_dir_begin()
var fname := subdir.get_next()
while fname != "":
if not subdir.current_is_dir() and fname.to_lower().ends_with(".glb"):
var path := "%s/%s" % [sub, fname]
var key := "%s/%s" % [category, folder]
_models.append({
"key": key,
"path": path,
"label": "%s (%s)" % [key, fname],
})
## Ensure working entry exists.
if not _working.has(key):
_working[key] = {
"fit_height": SpriteFit.fit_height_for(path),
"fit_max_width": SpriteFit.fit_max_width_for(path),
}
break
fname = subdir.get_next()
folder = dir.get_next()
_models.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return str(a["key"]) < str(b["key"]))
func _populate_list() -> void:
_list.clear()
for m in _models:
_list.add_item(str(m["label"]))
func _on_list_selected(index: int) -> void:
_select_index(index)
func _select_index(index: int) -> void:
if index < 0 or index >= _models.size():
return
_selected = index
var m: Dictionary = _models[index]
var key: String = m["key"]
var path: String = m["path"]
var entry: Dictionary = _working.get(key, {
"fit_height": SpriteFit.fit_height_for(path),
"fit_max_width": SpriteFit.fit_max_width_for(path),
})
_updating_ui = true
_height_spin.value = float(entry.get("fit_height", 1.0))
_width_spin.value = float(entry.get("fit_max_width", 1.0))
_updating_ui = false
_spawn_preview(path, float(_height_spin.value), float(_width_spin.value))
_status_label.text = "Selected %s" % key
func _spawn_preview(path: String, fit_h: float, fit_w: float) -> void:
if is_instance_valid(_preview):
_preview.queue_free()
_preview = null
var packed: PackedScene = load(MODEL_PROP_SCENE)
if packed == null:
return
_preview = packed.instantiate() as ModelProp
if _preview == null:
return
_preview.name = "PreviewProp"
_preview.use_scale_catalog = false
_preview.add_collision = false
_preview.model_path = path
_preview.fit_height = fit_h
_preview.fit_max_width = fit_w
_preview.ground_align = true
_preview.can_interact = false
add_child(_preview)
_preview.global_position = PREVIEW_POS
func _on_height_changed(v: float) -> void:
if _updating_ui or _selected < 0:
return
_commit_working(v, float(_width_spin.value))
func _on_width_changed(v: float) -> void:
if _updating_ui or _selected < 0:
return
_commit_working(float(_height_spin.value), v)
func _commit_working(fit_h: float, fit_w: float) -> void:
var key: String = _models[_selected]["key"]
var entry: Dictionary = _working.get(key, {}).duplicate(true)
entry["fit_height"] = fit_h
entry["fit_max_width"] = fit_w
_working[key] = entry
if is_instance_valid(_preview):
_preview.set_fit_params(fit_h, fit_w, true)
func _on_save() -> void:
var doc := {
"_meta": {
"player_height_m": SpriteFit.WORLD_PLAYER_HEIGHT,
"notes": "Edited by scenes/dev/scale_lab.tscn. Keys = category/folder under assets/3DModelle/.",
},
"scales": _working.duplicate(true),
}
var err := SpriteFit.save_scale_catalog(doc)
if err == OK:
_status_label.text = "Saved %s (%d entries)." % [SpriteFit.SCALE_CATALOG_PATH, _working.size()]
else:
_status_label.text = "Save FAILED (err %s). Run from editor so res:// is writable." % err
func _on_reset() -> void:
var defaults: Dictionary = SpriteFit.default_scale_catalog().get("scales", {})
_working.clear()
for key in defaults.keys():
_working[str(key)] = (defaults[key] as Dictionary).duplicate(true)
## Keep any extra scanned models with current SpriteFit fallbacks.
for m in _models:
var key: String = m["key"]
if not _working.has(key):
_working[key] = {
"fit_height": SpriteFit.fit_height_for(str(m["path"])),
"fit_max_width": SpriteFit.fit_max_width_for(str(m["path"])),
}
if _selected >= 0:
_select_index(_selected)
_status_label.text = "Reset to built-in defaults (not saved yet)."
func _on_reload() -> void:
_load_working_from_catalog()
if _selected >= 0:
_select_index(_selected)
_status_label.text = "Reloaded from JSON."
## Simple free camera (WASD + RMB look + wheel).
var _yaw: float = 0.35
var _pitch: float = -0.25
var _cam_dist: float = 5.5
var _cam_focus := Vector3(0.0, 1.0, 0.0)
var _rmb_down: bool = false
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_RIGHT:
_rmb_down = mb.pressed
elif mb.pressed and mb.button_index == MOUSE_BUTTON_WHEEL_UP:
_cam_dist = maxf(1.5, _cam_dist * 0.9)
_update_camera()
elif mb.pressed and mb.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_cam_dist = minf(18.0, _cam_dist * 1.1)
_update_camera()
elif event is InputEventMouseMotion and _rmb_down:
var mm := event as InputEventMouseMotion
_yaw -= mm.relative.x * 0.005
_pitch = clampf(_pitch - mm.relative.y * 0.005, -1.2, 0.6)
_update_camera()
func _process(delta: float) -> void:
var move := Vector3.ZERO
if Input.is_key_pressed(KEY_W):
move.z -= 1.0
if Input.is_key_pressed(KEY_S):
move.z += 1.0
if Input.is_key_pressed(KEY_A):
move.x -= 1.0
if Input.is_key_pressed(KEY_D):
move.x += 1.0
if Input.is_key_pressed(KEY_Q):
move.y -= 1.0
if Input.is_key_pressed(KEY_E):
move.y += 1.0
if move != Vector3.ZERO:
var basis := Basis.from_euler(Vector3(_pitch, _yaw, 0.0))
_cam_focus += basis * move.normalized() * 3.0 * delta
_update_camera()
func _update_camera() -> void:
var cam: Camera3D = $Camera3D
if cam == null:
return
var offset := Vector3(
sin(_yaw) * cos(_pitch),
sin(_pitch),
cos(_yaw) * cos(_pitch)
) * _cam_dist
## Orbit behind focus (wall at -Z).
cam.global_position = _cam_focus + Vector3(offset.x, -offset.y + 1.2, offset.z)
cam.look_at(_cam_focus, Vector3.UP)
func _enter_tree() -> void:
call_deferred("_update_camera")