Editor/IconEditor.cs
using Editor;
using Sandbox;
using System;
using System.Linq;
namespace GeneralGame.Editor;
/// <summary>
/// ���������� ��������� ��� �������� ������ ������� ��� ���������.
/// ��������� �������� ������, ��������, ����������� ������� ������ ������ � ��������� �����������.
/// </summary>
[Dock("Editor", "Model Icon Generator", "camera_alt")]
public class ModelIconGenerator : Widget
{
// ����� ��� ����������
private Scene _scene;
private CameraComponent _camera;
private SkinnedModelRenderer _modelRenderer;
private AmbientLight _ambientLight;
private DirectionalLight _sunLight;
// ������� UI
private SceneRenderingWidget _sceneWidget;
private ModelDropWidget _modelDropWidget;
private LineEdit _materialPathEdit;
private LineEdit _widthEdit;
private LineEdit _heightEdit;
private LineEdit _outputPathEdit;
private FloatSlider _cameraDistanceSlider;
private FloatSlider _cameraFovSlider;
private FloatSlider _modelYawSlider;
private FloatSlider _modelPitchSlider;
// �������� ������
private float _modelYaw = 0f;
private float _modelPitch = 0f;
// ��������� ������
private float _cameraYaw = 45f;
private float _cameraPitch = 20f;
private float _cameraDistance = 100f;
private Vector3 _cameraTarget = Vector3.Zero;
// ��������� ����
private Vector2 _lastMousePos;
private bool _isDragging = false;
private bool _isPanning = false;
// ������� �������
private Model _currentModel;
private Material _currentMaterial;
private Color _backgroundColor = Color.Transparent;
// ������
private Widget _previewContainer;
private Widget _previewWidget;
private Label _previewSizeLabel;
private int _previewWidth = 256;
private int _previewHeight = 256;
public ModelIconGenerator(Widget parent) : base(parent)
{
WindowTitle = "Model Icon Generator";
MinimumSize = new Vector2(800, 600);
SetSizeMode(SizeMode.CanGrow, SizeMode.CanGrow);
CreateScene();
CreateUI();
UpdateCamera();
}
private void CreateScene()
{
_scene = Scene.CreateEditorScene();
_scene.Name = "Model Icon Generator Scene";
using (_scene.Push())
{
// ������
{
var go = new GameObject(true, "camera");
_camera = go.AddComponent<CameraComponent>();
_camera.BackgroundColor = _backgroundColor;
_camera.FieldOfView = 50f;
_camera.ZNear = 0.1f;
_camera.ZFar = 10000f;
_camera.Enabled = true;
}
// Ambient light
{
var go = new GameObject(true, "ambient");
_ambientLight = go.AddComponent<AmbientLight>();
_ambientLight.Color = Color.White * 0.3f;
_ambientLight.Enabled = true;
}
// Directional light (sun)
{
var go = new GameObject(true, "sun");
_sunLight = go.AddComponent<DirectionalLight>();
_sunLight.Shadows = true;
_sunLight.WorldRotation = new Angles(50, 45, 0);
_sunLight.LightColor = Color.White * 0.8f;
_sunLight.Enabled = true;
}
// Secondary light
{
var go = new GameObject(true, "fill_light");
var light = go.AddComponent<DirectionalLight>();
light.Shadows = false;
light.WorldRotation = new Angles(30, -135, 0);
light.LightColor = Color.Cyan * 0.2f;
light.Enabled = true;
}
// Envmap
{
var go = new GameObject(true, "envmap");
var envmap = go.AddComponent<EnvmapProbe>();
envmap.Texture = Texture.Load("textures/cubemaps/default2.vtex");
envmap.Bounds = BBox.FromPositionAndSize(Vector3.Zero, 100000);
}
// Model (������ ����������)
{
var go = new GameObject(true, "model");
_modelRenderer = go.AddComponent<SkinnedModelRenderer>();
_modelRenderer.Enabled = true;
}
}
}
private void CreateUI()
{
Layout = Layout.Column();
Layout.Margin = 8;
Layout.Spacing = 8;
// ������� ������ � �����������
var topPanel = Layout.Add(new Widget(this));
topPanel.Layout = Layout.Row();
topPanel.Layout.Spacing = 8;
topPanel.FixedHeight = 32;
// ����� ������ (drag-drop �� ����� ��� Asset Browser)
topPanel.Layout.Add(new Label("Model:", this));
_modelDropWidget = topPanel.Layout.Add(new ModelDropWidget(this));
_modelDropWidget.FixedWidth = 250;
_modelDropWidget.FixedHeight = 26;
_modelDropWidget.OnModelChanged = (path) => LoadModel(path);
// ����� ���������
topPanel.Layout.Add(new Label("Material:", this));
_materialPathEdit = topPanel.Layout.Add(new LineEdit(this));
_materialPathEdit.PlaceholderText = "materials/example.vmat";
_materialPathEdit.FixedWidth = 200;
_materialPathEdit.ReturnPressed += OnMaterialPathChanged;
var materialBrowseBtn = topPanel.Layout.Add(new Button("...", this));
materialBrowseBtn.FixedWidth = 30;
materialBrowseBtn.Clicked += BrowseMaterial;
topPanel.Layout.AddStretchCell();
// ������ ������ ��������
var settingsPanel = Layout.Add(new Widget(this));
settingsPanel.Layout = Layout.Row();
settingsPanel.Layout.Spacing = 8;
settingsPanel.FixedHeight = 32;
// ���������� (������ x ������)
settingsPanel.Layout.Add(new Label("Size:", this));
_widthEdit = settingsPanel.Layout.Add(new LineEdit(this));
_widthEdit.Text = "256";
_widthEdit.FixedWidth = 50;
settingsPanel.Layout.Add(new Label("x", this));
_heightEdit = settingsPanel.Layout.Add(new LineEdit(this));
_heightEdit.Text = "256";
_heightEdit.FixedWidth = 50;
// FOV ������
settingsPanel.Layout.Add(new Label("FOV:", this));
_cameraFovSlider = settingsPanel.Layout.Add(new FloatSlider(this));
_cameraFovSlider.Minimum = 10;
_cameraFovSlider.Maximum = 120;
_cameraFovSlider.Value = 50;
_cameraFovSlider.FixedWidth = 80;
_cameraFovSlider.OnValueEdited = () => { if (_camera.IsValid()) _camera.FieldOfView = _cameraFovSlider.Value; };
// ��������� ������
settingsPanel.Layout.Add(new Label("Distance:", this));
_cameraDistanceSlider = settingsPanel.Layout.Add(new FloatSlider(this));
_cameraDistanceSlider.Minimum = 10;
_cameraDistanceSlider.Maximum = 1000;
_cameraDistanceSlider.Value = 100;
_cameraDistanceSlider.FixedWidth = 100;
_cameraDistanceSlider.OnValueEdited = () => { _cameraDistance = _cameraDistanceSlider.Value; UpdateCamera(); };
// �������� ������ (Yaw - �������������)
settingsPanel.Layout.Add(new Label("Yaw:", this));
_modelYawSlider = settingsPanel.Layout.Add(new FloatSlider(this));
_modelYawSlider.Minimum = -180;
_modelYawSlider.Maximum = 180;
_modelYawSlider.Value = 0;
_modelYawSlider.FixedWidth = 80;
_modelYawSlider.OnValueEdited = () => { _modelYaw = _modelYawSlider.Value; UpdateModelRotation(); };
// �������� ������ (Pitch - ������)
settingsPanel.Layout.Add(new Label("Pitch:", this));
_modelPitchSlider = settingsPanel.Layout.Add(new FloatSlider(this));
_modelPitchSlider.Minimum = -90;
_modelPitchSlider.Maximum = 90;
_modelPitchSlider.Value = 0;
_modelPitchSlider.FixedWidth = 80;
_modelPitchSlider.OnValueEdited = () => { _modelPitch = _modelPitchSlider.Value; UpdateModelRotation(); };
// ���� ����
settingsPanel.Layout.Add(new Label("Background:", this));
var bgColorBtn = settingsPanel.Layout.Add(new Button("Color", this));
bgColorBtn.FixedWidth = 60;
bgColorBtn.Clicked += ShowBackgroundColorPicker;
var transparentBtn = settingsPanel.Layout.Add(new Button("Transparent", this));
transparentBtn.FixedWidth = 80;
transparentBtn.Clicked += () => SetBackgroundColor(Color.Transparent);
settingsPanel.Layout.AddStretchCell();
// ������ ��������
var resetCameraBtn = settingsPanel.Layout.Add(new Button("Reset Camera", this));
resetCameraBtn.FixedWidth = 100;
resetCameraBtn.Clicked += ResetCamera;
// ������ ������ - ���� ����������
var savePanel = Layout.Add(new Widget(this));
savePanel.Layout = Layout.Row();
savePanel.Layout.Spacing = 8;
savePanel.FixedHeight = 32;
savePanel.Layout.Add(new Label("Output Path:", this));
_outputPathEdit = savePanel.Layout.Add(new LineEdit(this));
_outputPathEdit.PlaceholderText = "ui/icons/model_icon.png";
_outputPathEdit.Text = "ui/icons/model_icon.png";
savePanel.Layout.AddStretchCell();
var saveBtn = savePanel.Layout.Add(new Button.Primary("Save Icon", this));
saveBtn.FixedWidth = 100;
saveBtn.Clicked += SaveIcon;
var copyBtn = savePanel.Layout.Add(new Button("Copy to Clipboard", this));
copyBtn.FixedWidth = 120;
copyBtn.Clicked += CopyToClipboard;
// �������� �������: ����� �����, ������ ������
var mainArea = Layout.Add(new Widget(this));
mainArea.Layout = Layout.Row();
mainArea.Layout.Spacing = 8;
mainArea.SetSizeMode(SizeMode.CanGrow, SizeMode.CanGrow);
// ����� ���������� (�������� �� ���������� �����)
_sceneWidget = mainArea.Layout.Add(new SceneRenderingWidget(this));
_sceneWidget.SetSizeMode(SizeMode.CanGrow, SizeMode.CanGrow);
_sceneWidget.Scene = _scene;
_sceneWidget.MouseTracking = true;
// ������ ������
_previewContainer = mainArea.Layout.Add(new Widget(this));
_previewContainer.Layout = Layout.Column();
_previewContainer.Layout.Spacing = 4;
_previewContainer.SetSizeMode(SizeMode.Default, SizeMode.CanGrow);
_previewSizeLabel = _previewContainer.Layout.Add(new Label("Preview (256x256)", this));
_previewSizeLabel.SetStyles("color: #aaa; font-size: 11px;");
_previewWidget = _previewContainer.Layout.Add(new Widget(this));
_previewWidget.FixedSize = new Vector2(_previewWidth, _previewHeight);
_previewWidget.SetStyles("background-color: #1a1a1a; border: 1px solid #444;");
_previewWidget.OnPaintOverride = () =>
{
RenderPreview();
return true;
};
_previewContainer.Layout.AddStretchCell();
// ����������
var infoLabel = Layout.Add(new Label("LMB: Rotate | RMB: Pan | Scroll: Zoom | Drag model file to load", this));
infoLabel.SetStyles("color: #888; font-size: 11px;");
// �������� drag-drop
AcceptDrops = true;
}
[EditorEvent.Frame]
public void OnFrame()
{
if (!_scene.IsValid())
return;
_scene.EditorTick(RealTime.Now, RealTime.Delta);
_sceneWidget.Scene = _scene;
// ��������� ������ ������ ���� ���������
UpdatePreviewSize();
_previewWidget?.Update();
}
private void UpdatePreviewSize()
{
var newWidth = int.TryParse(_widthEdit.Text, out var w) ? Math.Clamp(w, 16, 1024) : 256;
var newHeight = int.TryParse(_heightEdit.Text, out var h) ? Math.Clamp(h, 16, 1024) : 256;
if (newWidth != _previewWidth || newHeight != _previewHeight)
{
_previewWidth = newWidth;
_previewHeight = newHeight;
_previewWidget.FixedSize = new Vector2(_previewWidth, _previewHeight);
_previewSizeLabel.Text = $"Preview ({_previewWidth}x{_previewHeight})";
}
}
private void RenderPreview()
{
if (!_camera.IsValid() || !_scene.IsValid())
return;
var pixmap = new Pixmap(_previewWidth, _previewHeight);
_camera.RenderToPixmap(pixmap);
Paint.Draw(_previewWidget.LocalRect, pixmap);
}
protected override void OnMousePress(MouseEvent e)
{
base.OnMousePress(e);
if (!_sceneWidget.ScreenRect.IsInside(e.ScreenPosition))
return;
_lastMousePos = e.LocalPosition;
if (e.LeftMouseButton)
{
_isDragging = true;
}
else if (e.RightMouseButton)
{
_isPanning = true;
}
}
protected override void OnMouseReleased(MouseEvent e)
{
base.OnMouseReleased(e);
_isDragging = false;
_isPanning = false;
}
protected override void OnMouseMove(MouseEvent e)
{
base.OnMouseMove(e);
if (!_sceneWidget.ScreenRect.IsInside(e.ScreenPosition) && !_isDragging && !_isPanning)
return;
var delta = e.LocalPosition - _lastMousePos;
_lastMousePos = e.LocalPosition;
if (_isDragging)
{
// �������� ������
_cameraYaw += delta.x * 0.5f;
_cameraPitch += delta.y * 0.5f;
_cameraPitch = Math.Clamp(_cameraPitch, -89f, 89f);
UpdateCamera();
}
else if (_isPanning)
{
// ���������������
var right = _camera.WorldRotation.Right;
var up = _camera.WorldRotation.Up;
var panSpeed = _cameraDistance * 0.002f;
_cameraTarget -= right * delta.x * panSpeed;
_cameraTarget += up * delta.y * panSpeed;
UpdateCamera();
}
}
protected override void OnWheel(WheelEvent e)
{
base.OnWheel(e);
// ���
_cameraDistance *= 1f - (e.Delta * 0.001f);
_cameraDistance = Math.Clamp(_cameraDistance, 10f, 1000f);
_cameraDistanceSlider.Value = _cameraDistance;
UpdateCamera();
e.Accept();
}
public override void OnDragHover(DragEvent ev)
{
base.OnDragHover(ev);
if (ev.Data?.HasFileOrFolder == true || ev.Data?.Object is Asset)
{
ev.Action = DropAction.Copy;
}
}
public override void OnDragDrop(DragEvent ev)
{
base.OnDragDrop(ev);
if (ev.Data?.Object is Asset asset)
{
HandleDroppedAsset(asset);
ev.Action = DropAction.Copy;
}
else if (ev.Data?.HasFileOrFolder == true)
{
var droppedAsset = AssetSystem.FindByPath(ev.Data.FileOrFolder);
if (droppedAsset != null)
{
HandleDroppedAsset(droppedAsset);
ev.Action = DropAction.Copy;
}
}
}
private void HandleDroppedAsset(Asset asset)
{
if (asset.AssetType?.FileExtension == "vmdl")
{
LoadModel(asset.Path);
}
else if (asset.AssetType?.FileExtension == "vmat")
{
_materialPathEdit.Text = asset.Path;
LoadMaterial(asset.Path);
}
}
private void UpdateCamera()
{
if (!_camera.IsValid())
return;
var yawRad = _cameraYaw * MathF.PI / 180f;
var pitchRad = _cameraPitch * MathF.PI / 180f;
var direction = new Vector3(
MathF.Cos(pitchRad) * MathF.Sin(yawRad),
MathF.Cos(pitchRad) * MathF.Cos(yawRad),
MathF.Sin(pitchRad)
).Normal;
_camera.WorldPosition = _cameraTarget + direction * _cameraDistance;
_camera.WorldRotation = Rotation.LookAt(-direction, Vector3.Up);
}
private void UpdateModelRotation()
{
if (!_modelRenderer.IsValid())
return;
using (_scene.Push())
{
// ����������� yaw � pitch ��� ������� �������� ������
var rotation = Rotation.From(_modelPitch, _modelYaw, 0);
_modelRenderer.WorldRotation = rotation;
}
}
private void OnMaterialPathChanged()
{
LoadMaterial(_materialPathEdit.Text);
}
private async void LoadModel(string path)
{
if (string.IsNullOrEmpty(path))
return;
try
{
_currentModel = await Model.LoadAsync(path);
if (_currentModel == null)
{
Log.Warning($"Failed to load model: {path}");
return;
}
using (_scene.Push())
{
_modelRenderer.Model = _currentModel;
_modelRenderer.WorldPosition = Vector3.Zero;
_modelRenderer.WorldRotation = Rotation.From(_modelPitch, _modelYaw, 0);
// ��������� �������� ���� ����
if (_currentMaterial != null)
{
_modelRenderer.MaterialOverride = _currentMaterial;
}
}
Log.Info($"Loaded model: {path}");
}
catch (Exception ex)
{
Log.Error($"Error loading model: {ex.Message}");
}
}
private void LoadMaterial(string path)
{
if (string.IsNullOrEmpty(path))
{
_currentMaterial = null;
if (_modelRenderer.IsValid())
_modelRenderer.MaterialOverride = null;
return;
}
try
{
_currentMaterial = Material.Load(path);
if (_currentMaterial == null)
{
Log.Warning($"Failed to load material: {path}");
return;
}
if (_modelRenderer.IsValid())
{
_modelRenderer.MaterialOverride = _currentMaterial;
}
Log.Info($"Loaded material: {path}");
}
catch (Exception ex)
{
Log.Error($"Error loading material: {ex.Message}");
}
}
private void BrowseMaterial()
{
var picker = AssetPicker.Create(null, AssetType.Material, new AssetPicker.PickerOptions()
{
EnableMultiselect = false
});
picker.Title = "Select Material";
picker.OnAssetPicked = assets =>
{
var asset = assets?.FirstOrDefault();
if (asset != null)
{
_materialPathEdit.Text = asset.Path;
LoadMaterial(asset.Path);
}
};
picker.Show();
}
private void ShowBackgroundColorPicker()
{
var picker = ColorPicker.OpenColorPopup(_backgroundColor, c =>
{
SetBackgroundColor(c);
}, ScreenRect.BottomLeft);
}
private void SetBackgroundColor(Color color)
{
_backgroundColor = color;
if (_camera.IsValid())
{
_camera.BackgroundColor = _backgroundColor;
}
}
private void ResetCamera()
{
_cameraYaw = 45f;
_cameraPitch = 20f;
_cameraDistance = 100f;
_cameraTarget = Vector3.Zero;
_modelYaw = 0f;
_modelPitch = 0f;
_cameraDistanceSlider.Value = _cameraDistance;
_modelYawSlider.Value = _modelYaw;
_modelPitchSlider.Value = _modelPitch;
UpdateCamera();
UpdateModelRotation();
}
private void SaveIcon()
{
if (!_camera.IsValid())
{
Log.Warning("Camera not valid");
return;
}
var width = int.TryParse(_widthEdit.Text, out var w) ? w : 256;
var height = int.TryParse(_heightEdit.Text, out var h) ? h : 256;
var outputPath = _outputPathEdit.Text;
if (string.IsNullOrEmpty(outputPath))
{
Log.Warning("Output path is empty");
return;
}
try
{
var bitmap = new Bitmap(width, height);
_camera.RenderToBitmap(bitmap);
var pngBytes = bitmap.ToPng();
// ��������� � ����� �������
var projectPath = Project.Current?.GetRootPath() ?? "";
var fullPath = System.IO.Path.Combine(projectPath, outputPath);
var directory = System.IO.Path.GetDirectoryName(fullPath);
if (!System.IO.Directory.Exists(directory))
System.IO.Directory.CreateDirectory(directory);
System.IO.File.WriteAllBytes(fullPath, pngBytes);
Log.Info($"Icon saved to: {fullPath}");
Log.Info($"Size: {width}x{height}, {pngBytes.Length} bytes");
}
catch (Exception ex)
{
Log.Error($"Failed to save icon: {ex.Message}");
}
}
private void CopyToClipboard()
{
if (!_camera.IsValid())
return;
var width = int.TryParse(_widthEdit.Text, out var w) ? w : 256;
var height = int.TryParse(_heightEdit.Text, out var h) ? h : 256;
try
{
var pixmap = new Pixmap(width, height);
_camera.RenderToPixmap(pixmap);
// �������� ��� PNG ������
var bitmap = new Bitmap(width, height);
_camera.RenderToBitmap(bitmap);
var pngData = bitmap.ToPng();
var base64 = Convert.ToBase64String(pngData);
EditorUtility.Clipboard.Copy(base64);
Log.Info($"Icon data copied to clipboard (base64 PNG, {width}x{height})");
}
catch (Exception ex)
{
Log.Error($"Failed to copy to clipboard: {ex.Message}");
}
}
public override void OnDestroyed()
{
base.OnDestroyed();
_scene?.Destroy();
_scene = null;
}
}
/// <summary>
/// ������� ������ ��� �������� ������
/// </summary>
public class ModelHolder
{
public Model TargetModel { get; set; }
}
/// <summary>
/// ������ ��� ������ ������ ����� ControlSheet (������������ drag-drop)
/// </summary>
public class ModelDropWidget : Widget
{
public Action<string> OnModelChanged;
private ModelHolder _holder;
private ControlSheet _controlSheet;
private SerializedObject _serializedObject;
public string ModelPath => _holder?.TargetModel?.ResourcePath;
public ModelDropWidget(Widget parent) : base(parent)
{
_holder = new ModelHolder();
_serializedObject = EditorTypeLibrary.GetSerializedObject(_holder);
Layout = Layout.Row();
_controlSheet = new ControlSheet();
_controlSheet.AddRow(_serializedObject.GetProperty(nameof(ModelHolder.TargetModel)));
Layout.Add(_controlSheet);
// ������������� �� ���������
_serializedObject.OnPropertyChanged += OnPropertyChanged;
}
private void OnPropertyChanged(SerializedProperty prop)
{
if (prop.Name == nameof(ModelHolder.TargetModel))
{
OnModelChanged?.Invoke(_holder.TargetModel?.ResourcePath);
}
}
}