class_name StudioInspectorDock extends VBoxContainer ## Property Inspector Dock for ModBison Studio. ## Renders editable properties for selected world object(s) with undo/redo support. signal property_changed(object_id: String, key: String, value: Variant) signal delete_requested(object_id: String) var _core: EditorCore = null var _selected_id: String = "" var _updating: bool = false var _title_label: Label var _empty_label: Label var _scroll: ScrollContainer var _content_vbox: VBoxContainer # Transform fields var _pos_x: SpinBox var _pos_y: SpinBox var _pos_z: SpinBox var _rot_y: SpinBox var _scale_x: SpinBox var _scale_y: SpinBox var _scale_z: SpinBox # Metadata var _name_edit: LineEdit var _asset_label: Label func _init() -> void: name = "StudioInspectorDock" custom_minimum_size = Vector2(240, 300) size_flags_vertical = Control.SIZE_EXPAND_FILL _build_ui() func setup(core: EditorCore) -> void: _core = core if _core != null and _core.selection != null: _core.selection.selection_changed.connect(_on_selection_changed) func _build_ui() -> void: _title_label = Label.new() _title_label.text = "Properties" _title_label.add_theme_font_size_override("font_size", 13) _title_label.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4)) add_child(_title_label) _empty_label = Label.new() _empty_label.text = "(No selection)" _empty_label.add_theme_font_size_override("font_size", 11) _empty_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) add_child(_empty_label) _scroll = ScrollContainer.new() _scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL _scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED add_child(_scroll) _content_vbox = VBoxContainer.new() _content_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL _content_vbox.visible = false _scroll.add_child(_content_vbox) # Name field var name_lbl := Label.new() name_lbl.text = "Display Name:" name_lbl.add_theme_font_size_override("font_size", 10) _content_vbox.add_child(name_lbl) _name_edit = LineEdit.new() _name_edit.text_submitted.connect(_on_name_changed) _content_vbox.add_child(_name_edit) _asset_label = Label.new() _asset_label.text = "Asset: None" _asset_label.add_theme_font_size_override("font_size", 10) _asset_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) _content_vbox.add_child(_asset_label) # Transform header var t_header := Label.new() t_header.text = "Transform" t_header.add_theme_font_size_override("font_size", 11) t_header.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0)) _content_vbox.add_child(t_header) _pos_x = _add_spin("Pos X", _on_pos_changed) _pos_y = _add_spin("Pos Y", _on_pos_changed) _pos_z = _add_spin("Pos Z", _on_pos_changed) _rot_y = _add_spin("Yaw (°)", _on_rot_changed, -360.0, 360.0, 1.0) _scale_x = _add_spin("Scale X", _on_scale_changed, 0.05, 50.0, 0.1) _scale_y = _add_spin("Scale Y", _on_scale_changed, 0.05, 50.0, 0.1) _scale_z = _add_spin("Scale Z", _on_scale_changed, 0.05, 50.0, 0.1) func _add_spin(label: String, callback: Callable, min_val: float = -500.0, max_val: float = 500.0, step: float = 0.5) -> SpinBox: var row := HBoxContainer.new() var lbl := Label.new() lbl.text = label + ":" lbl.custom_minimum_size = Vector2(70, 0) lbl.add_theme_font_size_override("font_size", 10) row.add_child(lbl) var spin := SpinBox.new() spin.min_value = min_val spin.max_value = max_val spin.step = step spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL spin.value_changed.connect(callback) row.add_child(spin) _content_vbox.add_child(row) return spin func _on_selection_changed(obj_id: String) -> void: _selected_id = obj_id if _selected_id == "" or _core == null: _empty_label.visible = true _content_vbox.visible = false return var obj = _core.model.find_object(_selected_id) if obj == null: _empty_label.visible = true _content_vbox.visible = false return _updating = true _empty_label.visible = false _content_vbox.visible = true var dname: String = str(obj.id) if obj.properties is Dictionary and obj.properties.has("display_name"): dname = str(obj.properties["display_name"]) _name_edit.text = dname _asset_label.text = "Asset: %s" % obj.asset_id _pos_x.value = obj.position.x _pos_y.value = obj.position.y _pos_z.value = obj.position.z var euler = Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w).get_euler() _rot_y.value = rad_to_deg(euler.y) _scale_x.value = obj.scale.x _scale_y.value = obj.scale.y _scale_z.value = obj.scale.z _updating = false func _on_name_changed(new_name: String) -> void: if _updating or _core == null or _selected_id == "": return _core.set_object_property(_selected_id, "display_name", new_name) property_changed.emit(_selected_id, "display_name", new_name) func _on_pos_changed(_val: float) -> void: if _updating or _core == null or _selected_id == "": return var new_pos := Vector3(_pos_x.value, _pos_y.value, _pos_z.value) _core.move_object(_selected_id, new_pos) property_changed.emit(_selected_id, "position", new_pos) func _on_rot_changed(_val: float) -> void: if _updating or _core == null or _selected_id == "": return var quat := Quaternion.from_euler(Vector3(0, deg_to_rad(_rot_y.value), 0)) var rot_vec := Vector4(quat.x, quat.y, quat.z, quat.w) _core.rotate_object(_selected_id, rot_vec) property_changed.emit(_selected_id, "rotation", rot_vec) func _on_scale_changed(_val: float) -> void: if _updating or _core == null or _selected_id == "": return var new_scale := Vector3(_scale_x.value, _scale_y.value, _scale_z.value) _core.scale_object(_selected_id, new_scale) property_changed.emit(_selected_id, "scale", new_scale)