Cozy Wather + buld systeme
This commit is contained in:
@@ -0,0 +1,749 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Ashwild.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// The Screenshot Studio — a UI Toolkit window that renders a prefab, scene object or raw mesh in an
|
||||
/// isolated preview world and bakes polished PNGs from it. The left pane is a live, orbitable 3D
|
||||
/// preview (drag to orbit, scroll to zoom); the right pane hosts the camera, lighting, background and
|
||||
/// export controls. All rendering and capture is delegated to ScreenshotStage; this window is the
|
||||
/// view and the orchestrator that writes the stage config and triggers renders/exports.
|
||||
/// </summary>
|
||||
public class ScreenshotStudioWindow : EditorWindow
|
||||
{
|
||||
#region Types
|
||||
|
||||
/// <summary>
|
||||
/// The named lighting rigs offered in the presets dropdown; Apply pushes each into the stage.
|
||||
/// </summary>
|
||||
private enum LightingPreset { Studio, Soft, Dramatic, Flat }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
|
||||
private const string UssPath = "Assets/GAME/Script/Editor/ScreenshotStudio/ScreenshotStudio.uss";
|
||||
private const string FolderPrefKey = "Ashwild.ScreenshotStudio.Folder";
|
||||
private const string FilePrefKey = "Ashwild.ScreenshotStudio.FileName";
|
||||
private const int LivePreviewLongSide = 1024;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private ScreenshotStage stage;
|
||||
|
||||
private int exportWidth = 1024;
|
||||
private int exportHeight = 1024;
|
||||
private bool supersample = true;
|
||||
private int turntableFrames = 12;
|
||||
private string exportFolder;
|
||||
private string exportFileName = string.Empty;
|
||||
private bool nameEditedByUser;
|
||||
|
||||
private bool autoRotate;
|
||||
private IVisualElementScheduledItem rotateSchedule;
|
||||
|
||||
private bool dragging;
|
||||
private Vector2 lastPointer;
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI References
|
||||
|
||||
private Image previewImage;
|
||||
private Label emptyOverlay;
|
||||
private VisualElement controls;
|
||||
private ColorField backgroundColorField;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Menu
|
||||
|
||||
/// <summary>
|
||||
/// Opens (or focuses) the Screenshot Studio window.
|
||||
/// </summary>
|
||||
[MenuItem("Tools/Screenshot Studio")]
|
||||
public static void Open()
|
||||
{
|
||||
ScreenshotStudioWindow window = GetWindow<ScreenshotStudioWindow>();
|
||||
window.titleContent = new GUIContent("Screenshot Studio");
|
||||
window.minSize = new Vector2(880, 520);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Window Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Builds the window tree and the render stage. Loads the last export folder from EditorPrefs.
|
||||
/// </summary>
|
||||
private void CreateGUI()
|
||||
{
|
||||
stage = new ScreenshotStage();
|
||||
exportFolder = EditorPrefs.GetString(FolderPrefKey, Application.dataPath);
|
||||
exportFileName = EditorPrefs.GetString(FilePrefKey, string.Empty);
|
||||
|
||||
VisualElement root = rootVisualElement;
|
||||
root.AddToClassList("ss-root");
|
||||
|
||||
StyleSheet sheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||
if (sheet != null) root.styleSheets.Add(sheet);
|
||||
|
||||
VisualElement body = new VisualElement();
|
||||
body.AddToClassList("ss-body");
|
||||
body.Add(BuildControlsPane());
|
||||
body.Add(BuildPreviewPane());
|
||||
root.Add(body);
|
||||
|
||||
RebuildControls();
|
||||
RenderPreview();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tears the stage down so the preview scene and textures never survive the window.
|
||||
/// </summary>
|
||||
private void OnDisable()
|
||||
{
|
||||
rotateSchedule?.Pause();
|
||||
stage?.Dispose();
|
||||
stage = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Preview Pane
|
||||
|
||||
/// <summary>
|
||||
/// Builds the large live-preview surface on the left: an Image fed by the stage's render texture,
|
||||
/// wired for orbit (drag) and zoom (wheel), with an empty-state overlay shown until a target loads.
|
||||
/// </summary>
|
||||
private VisualElement BuildPreviewPane()
|
||||
{
|
||||
VisualElement pane = new VisualElement();
|
||||
pane.AddToClassList("ss-preview");
|
||||
|
||||
previewImage = new Image { scaleMode = ScaleMode.ScaleToFit };
|
||||
previewImage.AddToClassList("ss-preview__image");
|
||||
previewImage.RegisterCallback<PointerDownEvent>(OnPreviewPointerDown);
|
||||
previewImage.RegisterCallback<PointerMoveEvent>(OnPreviewPointerMove);
|
||||
previewImage.RegisterCallback<PointerUpEvent>(OnPreviewPointerUp);
|
||||
previewImage.RegisterCallback<WheelEvent>(OnPreviewWheel);
|
||||
pane.Add(previewImage);
|
||||
|
||||
emptyOverlay = new Label("Assign a prefab, scene object or mesh to begin.\nDrag to orbit · scroll to zoom.");
|
||||
emptyOverlay.AddToClassList("ss-preview__empty");
|
||||
pane.Add(emptyOverlay);
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Controls Pane
|
||||
|
||||
/// <summary>
|
||||
/// Builds the scrollable right pane container the control cards are rendered into.
|
||||
/// </summary>
|
||||
private VisualElement BuildControlsPane()
|
||||
{
|
||||
controls = new ScrollView();
|
||||
controls.AddToClassList("ss-controls");
|
||||
return controls;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds every control card from the current stage + window state. Called on open and whenever
|
||||
/// a preset changes several values at once, so the widgets always mirror the live config.
|
||||
/// </summary>
|
||||
private void RebuildControls()
|
||||
{
|
||||
controls.Clear();
|
||||
controls.Add(BuildTargetCard());
|
||||
controls.Add(BuildCameraCard());
|
||||
controls.Add(BuildLightingCard());
|
||||
controls.Add(BuildBackgroundCard());
|
||||
controls.Add(BuildExportCard());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Target card: the object field (prefab / scene object / mesh) plus quick Frame and auto-rotate
|
||||
/// controls that act on the loaded subject.
|
||||
/// </summary>
|
||||
private VisualElement BuildTargetCard()
|
||||
{
|
||||
VisualElement card = Card("Target");
|
||||
|
||||
ObjectField field = new ObjectField("Object") { objectType = typeof(UnityEngine.Object), allowSceneObjects = true };
|
||||
field.RegisterValueChangedCallback(evt => LoadTarget(evt.newValue));
|
||||
card.Add(field);
|
||||
|
||||
VisualElement buttons = new VisualElement();
|
||||
buttons.AddToClassList("ss-buttons");
|
||||
|
||||
Button frame = new Button(() => { stage.FrameTarget(); RenderPreview(); }) { text = "Frame" };
|
||||
frame.AddToClassList("ss-btn");
|
||||
buttons.Add(frame);
|
||||
|
||||
Toggle rotate = new Toggle("Auto-rotate") { value = autoRotate };
|
||||
rotate.RegisterValueChangedCallback(evt => SetAutoRotate(evt.newValue));
|
||||
buttons.Add(rotate);
|
||||
|
||||
card.Add(buttons);
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Camera card: projection, field of view and framing padding.
|
||||
/// </summary>
|
||||
private VisualElement BuildCameraCard()
|
||||
{
|
||||
VisualElement card = Card("Camera");
|
||||
|
||||
Toggle ortho = new Toggle("Orthographic") { value = stage.Orthographic };
|
||||
ortho.RegisterValueChangedCallback(evt => { stage.Orthographic = evt.newValue; RenderPreview(); });
|
||||
card.Add(ortho);
|
||||
|
||||
card.Add(SliderRow("Field of View", 10f, 80f, stage.FieldOfView, v => stage.FieldOfView = v));
|
||||
card.Add(SliderRow("Padding", 1f, 3f, stage.Padding, v => stage.Padding = v));
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lighting card: a preset picker plus fine control over the key light (colour, intensity,
|
||||
/// direction), the fill and rim intensities and the ambient wash.
|
||||
/// </summary>
|
||||
private VisualElement BuildLightingCard()
|
||||
{
|
||||
VisualElement card = Card("Lighting");
|
||||
|
||||
DropdownField presets = new DropdownField("Preset",
|
||||
new System.Collections.Generic.List<string> { "Studio", "Soft", "Dramatic", "Flat" }, 0);
|
||||
presets.RegisterValueChangedCallback(evt => ApplyPreset((LightingPreset)presets.index));
|
||||
card.Add(presets);
|
||||
|
||||
ColorField keyColor = new ColorField("Key Colour") { value = stage.KeyColor, showAlpha = false };
|
||||
keyColor.RegisterValueChangedCallback(evt => { stage.KeyColor = evt.newValue; RenderPreview(); });
|
||||
card.Add(keyColor);
|
||||
|
||||
card.Add(SliderRow("Key Intensity", 0f, 3f, stage.KeyIntensity, v => stage.KeyIntensity = v));
|
||||
card.Add(SliderRow("Key Yaw", 0f, 360f, stage.KeyYaw, v => stage.KeyYaw = v));
|
||||
card.Add(SliderRow("Key Pitch", -80f, 80f, stage.KeyPitch, v => stage.KeyPitch = v));
|
||||
card.Add(SliderRow("Fill Intensity", 0f, 2f, stage.FillIntensity, v => stage.FillIntensity = v));
|
||||
card.Add(SliderRow("Rim Intensity", 0f, 3f, stage.RimIntensity, v => stage.RimIntensity = v));
|
||||
|
||||
ColorField ambient = new ColorField("Ambient Colour") { value = stage.AmbientColor, showAlpha = false };
|
||||
ambient.RegisterValueChangedCallback(evt => { stage.AmbientColor = evt.newValue; RenderPreview(); });
|
||||
card.Add(ambient);
|
||||
|
||||
card.Add(SliderRow("Ambient Intensity", 0f, 1.5f, stage.AmbientIntensity, v => stage.AmbientIntensity = v));
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Background card: transparent vs solid, and the solid fill colour (disabled while transparent).
|
||||
/// </summary>
|
||||
private VisualElement BuildBackgroundCard()
|
||||
{
|
||||
VisualElement card = Card("Background");
|
||||
|
||||
EnumField mode = new EnumField("Mode", stage.Background);
|
||||
mode.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
stage.Background = (ScreenshotStage.BackgroundMode)evt.newValue;
|
||||
backgroundColorField.SetEnabled(stage.Background == ScreenshotStage.BackgroundMode.SolidColor);
|
||||
RenderPreview();
|
||||
});
|
||||
card.Add(mode);
|
||||
|
||||
backgroundColorField = new ColorField("Colour") { value = stage.BackgroundColor, showAlpha = false };
|
||||
backgroundColorField.SetEnabled(stage.Background == ScreenshotStage.BackgroundMode.SolidColor);
|
||||
backgroundColorField.RegisterValueChangedCallback(evt => { stage.BackgroundColor = evt.newValue; RenderPreview(); });
|
||||
card.Add(backgroundColorField);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export card: resolution (presets + custom), supersampling, the output folder and file name,
|
||||
/// and the PNG / turntable export actions.
|
||||
/// </summary>
|
||||
private VisualElement BuildExportCard()
|
||||
{
|
||||
VisualElement card = Card("Export");
|
||||
|
||||
DropdownField resolution = new DropdownField("Resolution",
|
||||
new System.Collections.Generic.List<string> { "512", "1024", "2048", "4096", "Custom" }, 1);
|
||||
resolution.RegisterValueChangedCallback(evt => ApplyResolutionPreset(resolution.value));
|
||||
card.Add(resolution);
|
||||
|
||||
VisualElement size = new VisualElement();
|
||||
size.AddToClassList("ss-row");
|
||||
IntegerField width = new IntegerField("Width") { value = exportWidth };
|
||||
width.RegisterValueChangedCallback(evt => exportWidth = Mathf.Clamp(evt.newValue, 16, 8192));
|
||||
IntegerField height = new IntegerField("Height") { value = exportHeight };
|
||||
height.RegisterValueChangedCallback(evt => exportHeight = Mathf.Clamp(evt.newValue, 16, 8192));
|
||||
width.AddToClassList("ss-half");
|
||||
height.AddToClassList("ss-half");
|
||||
size.Add(width);
|
||||
size.Add(height);
|
||||
card.Add(size);
|
||||
|
||||
Toggle ss = new Toggle("Supersample ×2") { value = supersample };
|
||||
ss.tooltip = "Renders at double resolution and downscales for crisper edges.";
|
||||
ss.RegisterValueChangedCallback(evt => supersample = evt.newValue);
|
||||
card.Add(ss);
|
||||
|
||||
card.Add(BuildFolderRow());
|
||||
|
||||
TextField file = new TextField("File Name") { value = exportFileName };
|
||||
file.RegisterValueChangedCallback(evt => { exportFileName = evt.newValue; nameEditedByUser = true; });
|
||||
card.Add(file);
|
||||
|
||||
Button export = new Button(ExportPng) { text = "Export PNG" };
|
||||
export.AddToClassList("ss-btn");
|
||||
export.AddToClassList("ss-btn--primary");
|
||||
card.Add(export);
|
||||
|
||||
VisualElement turntable = new VisualElement();
|
||||
turntable.AddToClassList("ss-row");
|
||||
IntegerField frames = new IntegerField("Turntable") { value = turntableFrames };
|
||||
frames.RegisterValueChangedCallback(evt => turntableFrames = Mathf.Clamp(evt.newValue, 2, 360));
|
||||
frames.style.flexGrow = 1;
|
||||
Button exportTurntable = new Button(ExportTurntable) { text = "Export Sequence" };
|
||||
exportTurntable.AddToClassList("ss-btn");
|
||||
turntable.Add(frames);
|
||||
turntable.Add(exportTurntable);
|
||||
card.Add(turntable);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the output-folder row: a read-only path label and a Browse button that opens a folder
|
||||
/// picker and remembers the choice in EditorPrefs.
|
||||
/// </summary>
|
||||
private VisualElement BuildFolderRow()
|
||||
{
|
||||
VisualElement row = new VisualElement();
|
||||
row.AddToClassList("ss-row");
|
||||
|
||||
Label path = new Label(ShortFolder());
|
||||
path.AddToClassList("ss-path");
|
||||
path.tooltip = exportFolder;
|
||||
|
||||
Button browse = new Button(() =>
|
||||
{
|
||||
string chosen = EditorUtility.OpenFolderPanel("Export folder", exportFolder, string.Empty);
|
||||
if (string.IsNullOrEmpty(chosen)) return;
|
||||
exportFolder = chosen;
|
||||
EditorPrefs.SetString(FolderPrefKey, exportFolder);
|
||||
path.text = ShortFolder();
|
||||
path.tooltip = exportFolder;
|
||||
})
|
||||
{ text = "Browse" };
|
||||
browse.AddToClassList("ss-btn");
|
||||
|
||||
row.Add(path);
|
||||
row.Add(browse);
|
||||
return row;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Control Builders
|
||||
|
||||
/// <summary>
|
||||
/// Builds a titled card container matching the studio styling; callers append their fields.
|
||||
/// </summary>
|
||||
private static VisualElement Card(string title)
|
||||
{
|
||||
VisualElement card = new VisualElement();
|
||||
card.AddToClassList("ss-card");
|
||||
|
||||
Label heading = new Label(title);
|
||||
heading.AddToClassList("ss-card__title");
|
||||
card.Add(heading);
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a labelled slider paired with a numeric field; both edit the same value and re-render
|
||||
/// the preview live. The setter writes straight into the stage config.
|
||||
/// </summary>
|
||||
private VisualElement SliderRow(string label, float min, float max, float value, Action<float> setter)
|
||||
{
|
||||
VisualElement row = new VisualElement();
|
||||
row.AddToClassList("ss-row");
|
||||
|
||||
Slider slider = new Slider(label, min, max) { value = value };
|
||||
slider.AddToClassList("ss-slider");
|
||||
slider.style.flexGrow = 1;
|
||||
|
||||
FloatField field = new FloatField { value = value };
|
||||
field.AddToClassList("ss-num");
|
||||
|
||||
slider.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
field.SetValueWithoutNotify(evt.newValue);
|
||||
setter(evt.newValue);
|
||||
RenderPreview();
|
||||
});
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
float clamped = Mathf.Clamp(evt.newValue, min, max);
|
||||
slider.SetValueWithoutNotify(clamped);
|
||||
setter(clamped);
|
||||
RenderPreview();
|
||||
});
|
||||
|
||||
row.Add(slider);
|
||||
row.Add(field);
|
||||
return row;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Target
|
||||
|
||||
/// <summary>
|
||||
/// Loads a new subject into the stage, defaults the export file name to its name (unless the user
|
||||
/// already typed one), rebuilds the controls to reflect the reset framing and re-renders.
|
||||
/// </summary>
|
||||
private void LoadTarget(UnityEngine.Object source)
|
||||
{
|
||||
stage.SetTarget(source);
|
||||
|
||||
if (source != null && !nameEditedByUser)
|
||||
exportFileName = source.name;
|
||||
|
||||
emptyOverlay.style.display = stage.HasTarget ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
RebuildControls();
|
||||
RenderPreview();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Presets
|
||||
|
||||
/// <summary>
|
||||
/// Applies a named lighting rig to the stage and rebuilds the lighting controls to match.
|
||||
/// </summary>
|
||||
private void ApplyPreset(LightingPreset preset)
|
||||
{
|
||||
switch (preset)
|
||||
{
|
||||
case LightingPreset.Studio:
|
||||
stage.KeyColor = Color.white; stage.KeyIntensity = 1.1f; stage.KeyYaw = 50f; stage.KeyPitch = 40f;
|
||||
stage.FillIntensity = 0.45f; stage.RimIntensity = 0.9f;
|
||||
stage.AmbientColor = new Color(0.5f, 0.54f, 0.62f); stage.AmbientIntensity = 0.35f;
|
||||
break;
|
||||
case LightingPreset.Soft:
|
||||
stage.KeyColor = new Color(1f, 0.97f, 0.92f); stage.KeyIntensity = 0.85f; stage.KeyYaw = 40f; stage.KeyPitch = 55f;
|
||||
stage.FillIntensity = 0.7f; stage.RimIntensity = 0.4f;
|
||||
stage.AmbientColor = new Color(0.62f, 0.64f, 0.7f); stage.AmbientIntensity = 0.6f;
|
||||
break;
|
||||
case LightingPreset.Dramatic:
|
||||
stage.KeyColor = new Color(1f, 0.95f, 0.85f); stage.KeyIntensity = 1.7f; stage.KeyYaw = 65f; stage.KeyPitch = 25f;
|
||||
stage.FillIntensity = 0.1f; stage.RimIntensity = 1.6f;
|
||||
stage.AmbientColor = new Color(0.3f, 0.34f, 0.45f); stage.AmbientIntensity = 0.12f;
|
||||
break;
|
||||
case LightingPreset.Flat:
|
||||
stage.KeyColor = Color.white; stage.KeyIntensity = 0.9f; stage.KeyYaw = 0f; stage.KeyPitch = 30f;
|
||||
stage.FillIntensity = 0.9f; stage.RimIntensity = 0f;
|
||||
stage.AmbientColor = Color.white; stage.AmbientIntensity = 0.8f;
|
||||
break;
|
||||
}
|
||||
|
||||
RebuildControls();
|
||||
RenderPreview();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the export resolution from the preset dropdown; "Custom" leaves the current width/height
|
||||
/// untouched so the fields stay editable.
|
||||
/// </summary>
|
||||
private void ApplyResolutionPreset(string choice)
|
||||
{
|
||||
if (choice == "Custom") return;
|
||||
if (int.TryParse(choice, out int size))
|
||||
{
|
||||
exportWidth = size;
|
||||
exportHeight = size;
|
||||
RebuildControls();
|
||||
RenderPreview();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Preview Rendering
|
||||
|
||||
/// <summary>
|
||||
/// Renders the subject into the live texture at the preview aspect and pushes it into the Image.
|
||||
/// A no-op (blank surface) until a target is loaded.
|
||||
/// </summary>
|
||||
private void RenderPreview()
|
||||
{
|
||||
if (stage == null) return;
|
||||
|
||||
if (!stage.HasTarget)
|
||||
{
|
||||
previewImage.image = null;
|
||||
previewImage.MarkDirtyRepaint();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2Int size = LiveSize();
|
||||
RenderTexture rt = stage.RenderLive(size.x, size.y);
|
||||
previewImage.image = rt;
|
||||
previewImage.MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The live render resolution: the export aspect ratio scaled so its long side is a fixed size,
|
||||
/// keeping the preview crisp without paying full export cost every frame.
|
||||
/// </summary>
|
||||
private Vector2Int LiveSize()
|
||||
{
|
||||
float aspect = (float)Mathf.Max(1, exportWidth) / Mathf.Max(1, exportHeight);
|
||||
int w = aspect >= 1f ? LivePreviewLongSide : Mathf.RoundToInt(LivePreviewLongSide * aspect);
|
||||
int h = aspect >= 1f ? Mathf.RoundToInt(LivePreviewLongSide / aspect) : LivePreviewLongSide;
|
||||
return new Vector2Int(Mathf.Max(16, w), Mathf.Max(16, h));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Preview Interaction
|
||||
|
||||
/// <summary>
|
||||
/// Begins an orbit drag, capturing the pointer so the drag continues even when it leaves the image.
|
||||
/// </summary>
|
||||
private void OnPreviewPointerDown(PointerDownEvent evt)
|
||||
{
|
||||
if (!stage.HasTarget) return;
|
||||
dragging = true;
|
||||
lastPointer = evt.position;
|
||||
previewImage.CapturePointer(evt.pointerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Orbits the camera by the pointer delta while dragging (horizontal → yaw, vertical → pitch).
|
||||
/// </summary>
|
||||
private void OnPreviewPointerMove(PointerMoveEvent evt)
|
||||
{
|
||||
if (!dragging) return;
|
||||
|
||||
Vector2 delta = (Vector2)evt.position - lastPointer;
|
||||
lastPointer = evt.position;
|
||||
|
||||
stage.Yaw += delta.x * 0.5f;
|
||||
stage.Pitch = Mathf.Clamp(stage.Pitch - delta.y * 0.5f, -89f, 89f);
|
||||
RenderPreview();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends the orbit drag and releases the pointer capture.
|
||||
/// </summary>
|
||||
private void OnPreviewPointerUp(PointerUpEvent evt)
|
||||
{
|
||||
dragging = false;
|
||||
previewImage.ReleasePointer(evt.pointerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zooms the framing on scroll, clamped so the subject can neither invert nor fly off.
|
||||
/// </summary>
|
||||
private void OnPreviewWheel(WheelEvent evt)
|
||||
{
|
||||
if (!stage.HasTarget) return;
|
||||
stage.Zoom = Mathf.Clamp(stage.Zoom * (1f - evt.delta.y * 0.05f), 0.2f, 5f);
|
||||
RenderPreview();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Auto-Rotate
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the turntable spin, driving a steady yaw increment through the window scheduler so the
|
||||
/// preview animates without a per-frame editor update hook.
|
||||
/// </summary>
|
||||
private void SetAutoRotate(bool enabled)
|
||||
{
|
||||
autoRotate = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
rotateSchedule = rootVisualElement.schedule.Execute(() =>
|
||||
{
|
||||
stage.Yaw += 1.2f;
|
||||
RenderPreview();
|
||||
}).Every(33);
|
||||
}
|
||||
else
|
||||
{
|
||||
rotateSchedule?.Pause();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Export
|
||||
|
||||
/// <summary>
|
||||
/// Captures the current view at the export resolution and writes it as a PNG to the chosen folder,
|
||||
/// avoiding overwrites by auto-numbering, refreshing the AssetDatabase when the path is in-project,
|
||||
/// and revealing the file. Guards a missing target and failed writes with clear logs.
|
||||
/// </summary>
|
||||
private void ExportPng()
|
||||
{
|
||||
if (!stage.HasTarget)
|
||||
{
|
||||
Debug.LogError("[ScreenshotStudio] No target assigned — nothing to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
Texture2D shot = stage.Capture(exportWidth, exportHeight, supersample);
|
||||
if (shot == null) return;
|
||||
|
||||
byte[] png = shot.EncodeToPNG();
|
||||
DestroyImmediate(shot);
|
||||
|
||||
string path = UniquePath(ResolveFolder(), ResolveFileName());
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(path, png);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ScreenshotStudio] Failed to write '{path}': {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
EditorPrefs.SetString(FilePrefKey, exportFileName);
|
||||
RefreshIfInProject(path);
|
||||
Debug.Log($"[ScreenshotStudio] Exported {exportWidth}×{exportHeight} PNG → {path}");
|
||||
ShowNotification(new GUIContent($"Exported {Path.GetFileName(path)}"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bakes a turntable: N evenly-spaced frames around the Y axis, each written as a numbered PNG.
|
||||
/// Restores the original yaw afterwards and shows a cancellable progress bar.
|
||||
/// </summary>
|
||||
private void ExportTurntable()
|
||||
{
|
||||
if (!stage.HasTarget)
|
||||
{
|
||||
Debug.LogError("[ScreenshotStudio] No target assigned — nothing to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
string folder = ResolveFolder();
|
||||
string baseName = Path.GetFileNameWithoutExtension(ResolveFileName());
|
||||
float startYaw = stage.Yaw;
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < turntableFrames; i++)
|
||||
{
|
||||
if (EditorUtility.DisplayCancelableProgressBar("Screenshot Studio",
|
||||
$"Rendering frame {i + 1}/{turntableFrames}", (float)i / turntableFrames))
|
||||
break;
|
||||
|
||||
stage.Yaw = startYaw + 360f * i / turntableFrames;
|
||||
Texture2D frame = stage.Capture(exportWidth, exportHeight, supersample);
|
||||
if (frame == null) break;
|
||||
|
||||
byte[] png = frame.EncodeToPNG();
|
||||
DestroyImmediate(frame);
|
||||
File.WriteAllBytes(Path.Combine(folder, $"{baseName}_{i:000}.png"), png);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ScreenshotStudio] Turntable export failed: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
stage.Yaw = startYaw;
|
||||
RefreshIfInProject(folder);
|
||||
RenderPreview();
|
||||
Debug.Log($"[ScreenshotStudio] Exported {turntableFrames}-frame turntable → {folder}");
|
||||
ShowNotification(new GUIContent($"Exported {turntableFrames} frames"));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Export Helpers
|
||||
|
||||
/// <summary>
|
||||
/// The resolved export folder, falling back to the project Assets folder when unset or missing.
|
||||
/// </summary>
|
||||
private string ResolveFolder()
|
||||
{
|
||||
if (string.IsNullOrEmpty(exportFolder) || !Directory.Exists(exportFolder))
|
||||
return Application.dataPath;
|
||||
return exportFolder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The export file name, defaulting to the target's name and always ending in ".png".
|
||||
/// </summary>
|
||||
private string ResolveFileName()
|
||||
{
|
||||
string name = string.IsNullOrWhiteSpace(exportFileName) ? "Screenshot" : exportFileName.Trim();
|
||||
if (!name.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) name += ".png";
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a non-colliding path in the folder, appending _001, _002, … when the name is taken.
|
||||
/// </summary>
|
||||
private static string UniquePath(string folder, string fileName)
|
||||
{
|
||||
string full = Path.Combine(folder, fileName);
|
||||
if (!File.Exists(full)) return full;
|
||||
|
||||
string stem = Path.GetFileNameWithoutExtension(fileName);
|
||||
string ext = Path.GetExtension(fileName);
|
||||
for (int i = 1; i < 10000; i++)
|
||||
{
|
||||
string candidate = Path.Combine(folder, $"{stem}_{i:000}{ext}");
|
||||
if (!File.Exists(candidate)) return candidate;
|
||||
}
|
||||
return full;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports the written file so it appears in the Project window when it lives under Assets.
|
||||
/// </summary>
|
||||
private static void RefreshIfInProject(string path)
|
||||
{
|
||||
string full = Path.GetFullPath(path).Replace('\\', '/');
|
||||
string root = Path.GetFullPath(Application.dataPath).Replace('\\', '/');
|
||||
if (!full.StartsWith(root, StringComparison.OrdinalIgnoreCase)) return;
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A compact, right-aligned form of the export folder for the path label.
|
||||
/// </summary>
|
||||
private string ShortFolder()
|
||||
{
|
||||
string folder = ResolveFolder();
|
||||
const int max = 34;
|
||||
return folder.Length <= max ? folder : "…" + folder.Substring(folder.Length - max);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user