Files
2026-07-13 13:13:17 +08:00

880 lines
33 KiB
C#

#nullable enable
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using ImGuiNET;
using T3.Core.DataTypes;
using T3.Core.DataTypes.Vector;
using T3.Core.Operator;
using T3.Core.Operator.Slots;
using T3.Editor.Gui.Interaction;
using T3.Editor.Gui.Interaction.Keyboard;
using T3.Editor.Gui.Interaction.Variations;
using T3.Editor.Gui.Interaction.Variations.Model;
using T3.Editor.Gui.Styling;
using T3.Editor.Gui.UiHelpers;
using T3.Editor.Gui.UiHelpers.DelaunayVoronoi;
using T3.Editor.Gui.UiHelpers.Thumbnails;
using T3.Editor.Gui.Windows.RenderExport;
using T3.Editor.UiModel;
using T3.Editor.UiModel.Selection;
using Point = T3.Editor.Gui.UiHelpers.DelaunayVoronoi.Point;
using Vector2 = System.Numerics.Vector2;
namespace T3.Editor.Gui.Windows.Variations;
/// <summary>
/// Controls arranging and blending variations on a canvas for Presets and Snapshots.
/// </summary>
internal abstract class VariationBaseCanvas : ScalableCanvas, ISelectionContainer
{
protected abstract string GetTitle();
private protected abstract Instance? InstanceForBlendOperations { get; }
private protected abstract SymbolVariationPool? PoolForBlendOperations { get; }
protected abstract void DrawAdditionalContextMenuContent(Instance instanceForBlendOperations);
public void DrawBaseCanvas(ImDrawListPtr drawList, bool hideHeader = false)
{
if (PoolForBlendOperations == null || InstanceForBlendOperations == null)
return;
UpdateCanvas(out _);
// Complete deferred actions
if (!T3Ui.IsCurrentlySaving && UserActions.DeleteSelection.Triggered())
DeleteSelectedElements();
bool pinnedOutputChanged = false;
// Render variations to pinned output
if (RenderProcess.OutputWindow != null)
{
var instanceForBlending = InstanceForBlendOperations;
if (instanceForBlending != _currentRenderInstance)
{
pinnedOutputChanged = true;
_currentRenderInstance = instanceForBlending;
}
// Skip rendering on the change frame — the render state (index, pending capture,
// rerender flags) still belongs to the previous pool until RefreshView resets it.
if (!pinnedOutputChanged && RenderProcess.State == RenderProcess.States.ReadyForExport)
{
if (RenderProcess.OutputWindow.ShownInstance is { Outputs.Count: > 0 } instanceForOutput)
{
var primaryOutput = instanceForOutput.Outputs[0];
if (primaryOutput is Slot<Texture2D> textureSlot2)
{
UpdateThumbnailRendering(instanceForBlending, textureSlot2);
}
}
}
}
// Get instance for variations
if (pinnedOutputChanged)
{
RefreshView();
}
if (UserActions.FocusSelection.Triggered() || _resetViewRequested)
{
ResetView();
}
HandleFenceSelection(_selectionFence);
// Blending...
HandleBlendingInteraction();
if (!hideHeader)
{
ImGui.PushFont(Fonts.FontLarge);
ImGui.SetCursorPos(new Vector2(10, 35));
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.Gray.Rgba);
ImGui.TextUnformatted(GetTitle());
ImGui.PopStyleColor();
ImGui.PopFont();
}
// Draw thumbnails...
var modified = false;
if (_currentRenderInstance != null)
{
// Live previews are volatile and load from the temp cache so the curated defaults
// in the package's .meta folder stay untouched. The defaults serve as fallback
// until the live rendering has produced a temp thumbnail.
var liveThumbnails = UserSettings.Config.VariationLiveThumbnails;
var thumbnailCategory = liveThumbnails
? ThumbnailManager.Categories.Temp
: ThumbnailManager.Categories.PackageMeta;
var fallbackCategory = liveThumbnails
? ThumbnailManager.Categories.PackageMeta
: (ThumbnailManager.Categories?)null;
for (var index = 0; index < PoolForBlendOperations.AllVariations.Count; index++)
{
var variation = PoolForBlendOperations.AllVariations[index];
var thumbnail = ThumbnailManager.GetThumbnail(variation.Id, _currentRenderInstance.Symbol.SymbolPackage, thumbnailCategory,
fallbackCategory: fallbackCategory);
modified |= VariationThumbnail.Draw(this,
variation,
InstanceForBlendOperations,
drawList,
ThumbnailManager.AtlasSrv, thumbnail);
}
}
DrawBlendingOverlay(drawList, InstanceForBlendOperations);
if (modified)
PoolForBlendOperations.SaveVariationsToFile();
DrawContextMenu(InstanceForBlendOperations);
}
private bool _rerenderRequested;
private bool _rerenderToFileRequested;
/// <summary>
/// Updates keeps rendering thumbnails until all are processed.
/// </summary>
private void UpdateThumbnailRendering(Instance instanceForBlending, Slot<Texture2D> textureOutputSlot)
{
if (!UserSettings.Config.VariationLiveThumbnails && !_rerenderRequested)
return;
var outputSymbolUi = textureOutputSlot.Parent.Symbol.GetSymbolUi();
if (!outputSymbolUi.OutputUis.TryGetValue(textureOutputSlot.Id, out var textureOutputUi))
return;
_ = textureOutputUi;
UpdateNextVariationThumbnail(instanceForBlending, textureOutputSlot);
}
private void DrawBlendingOverlay(ImDrawListPtr drawList, Instance instanceForBlending)
{
if (!IsBlendingActive || PoolForBlendOperations == null)
return;
var mousePos = ImGui.GetMousePos();
if (_blendPoints.Count == 1)
{
PoolForBlendOperations.BeginWeightedBlend(instanceForBlending, _blendVariations, _blendWeights);
if (ImGui.IsMouseReleased(ImGuiMouseButton.Left))
{
PoolForBlendOperations.ApplyCurrentBlend();
}
}
else if (_blendPoints.Count == 2)
{
foreach (var p in _blendPoints)
{
drawList.AddCircleFilled(p, 5, UiColors.BackgroundFull.Fade(0.5f));
drawList.AddCircleFilled(p, 3, UiColors.ForegroundFull);
}
drawList.AddLine(_blendPoints[0], _blendPoints[1], UiColors.ForegroundFull, 2);
var blendPosition = _blendPoints[0] * _blendWeights[0] + _blendPoints[1] * _blendWeights[1];
drawList.AddCircleFilled(blendPosition, 5, UiColors.ForegroundFull);
PoolForBlendOperations.BeginWeightedBlend(instanceForBlending, _blendVariations, _blendWeights);
if (ImGui.IsMouseReleased(ImGuiMouseButton.Left))
{
PoolForBlendOperations.ApplyCurrentBlend();
}
}
else if (_blendPoints.Count == 3)
{
drawList.AddTriangleFilled(_blendPoints[0], _blendPoints[1], _blendPoints[2], UiColors.BackgroundFull.Fade(0.3f));
foreach (var p in _blendPoints)
{
drawList.AddCircleFilled(p, 5, UiColors.BackgroundFull.Fade(0.5f));
drawList.AddLine(mousePos, p, UiColors.ForegroundFull, 2);
drawList.AddCircleFilled(p, 3, UiColors.ForegroundFull);
}
drawList.AddCircleFilled(mousePos, 5, UiColors.ForegroundFull);
PoolForBlendOperations.BeginWeightedBlend(instanceForBlending, _blendVariations, _blendWeights);
if (ImGui.IsMouseReleased(ImGuiMouseButton.Left))
{
PoolForBlendOperations.ApplyCurrentBlend();
}
}
}
/// <summary>
/// This will trigger a reset few on next update.
/// </summary>
/// <remark>
/// Calling ResetView within the current update might lead to invalid view configuration, because the
/// use FillWindow mode might use incorrect window references for determining the current window size and position.
/// </remark>
protected void RequestResetView()
{
_resetViewRequested = true;
}
private bool _resetViewRequested;
private void HandleBlendingInteraction()
{
IsBlendingActive = (ImGui.IsWindowHovered() || ImGui.IsWindowFocused()) && ImGui.GetIO().KeyAlt;
var mousePos = ImGui.GetMousePos();
_blendPoints.Clear();
_blendWeights.Clear();
_blendVariations.Clear();
if (!IsBlendingActive || PoolForBlendOperations == null)
return;
foreach (var s in CanvasElementSelection.SelectedElements)
{
_blendPoints.Add(GetNodeCenterOnScreen(s));
if (s is Variation v)
_blendVariations.Add(v);
}
if (CanvasElementSelection.SelectedElements.Count == 1)
{
var posOnScreen = TransformPosition(_blendVariations[0].PosOnCanvas);
var sizeOnScreen = TransformDirection(_blendVariations[0].Size);
var a = (mousePos.X - posOnScreen.X) / sizeOnScreen.X;
_blendWeights.Add(a);
}
else if (CanvasElementSelection.SelectedElements.Count == 2)
{
if (_blendPoints[0] == _blendPoints[1])
{
_blendWeights.Add(0.5f);
_blendWeights.Add(0.5f);
}
else
{
var v1 = _blendPoints[1] - _blendPoints[0];
var v2 = mousePos - _blendPoints[0];
var lengthV1 = v1.Length();
var a = Vector2.Dot(v1 / lengthV1, v2 / lengthV1);
_blendWeights.Add(1 - a);
_blendWeights.Add(a);
}
}
else if (CanvasElementSelection.SelectedElements.Count == 3)
{
Barycentric(mousePos, _blendPoints[0], _blendPoints[1], _blendPoints[2], out var u, out var v, out var w);
_blendWeights.Add(u);
_blendWeights.Add(v);
_blendWeights.Add(w);
}
else
{
var points = new List<Point>();
Vector2 minPos = new Vector2(float.PositiveInfinity, float.PositiveInfinity);
Vector2 maxPos = new Vector2(float.NegativeInfinity, float.NegativeInfinity);
foreach (var v in PoolForBlendOperations.AllVariations)
{
var vec2 = GetNodeCenterOnScreen(v);
minPos = Vector2.Min(vec2, minPos);
maxPos = Vector2.Max(vec2, maxPos);
points.Add(new Point(vec2.X, vec2.Y));
}
minPos -= Vector2.One * 100;
maxPos += Vector2.One * 100;
var triangulator = new DelaunayTriangulator();
var borderPoints = triangulator.SetBorder(new Point(minPos.X, minPos.Y), new Point(maxPos.X, maxPos.Y));
points.AddRange(borderPoints);
var triangles = triangulator.BowyerWatson(points);
foreach (var t in triangles)
{
var p0 = t.Vertices[0].ToVec2();
var p1 = t.Vertices[1].ToVec2();
var p2 = t.Vertices[2].ToVec2();
Barycentric(mousePos,
p0,
p1,
p2,
out var u,
out var v,
out var w);
var insideTriangle = u >= 0 && u <= 1 && v >= 0 && v <= 1 && w >= 0 && w <= 1;
if (!insideTriangle)
continue;
_blendPoints.Clear();
_blendWeights.Clear();
_blendVariations.Clear();
var weights = new[] { u, v, w };
for (var vertexIndex = 0; vertexIndex < t.Vertices.Length; vertexIndex++)
{
var vertex = t.Vertices[vertexIndex];
var variationIndex = points.IndexOf(vertex);
if (variationIndex < PoolForBlendOperations.AllVariations.Count)
{
_blendVariations.Add(PoolForBlendOperations.AllVariations[variationIndex]);
_blendWeights.Add(weights[vertexIndex]);
_blendPoints.Add(vertex.ToVec2());
}
}
if (_blendWeights.Count == 2)
{
var sum = _blendWeights[0] + _blendWeights[1];
_blendWeights[0] /= sum;
_blendWeights[1] /= sum;
}
else if (_blendWeights.Count == 1)
{
_blendWeights.Clear();
_blendPoints.Clear();
_blendVariations.Clear();
}
break;
}
}
}
public bool TryGetBlendWeight(Variation v, out float weight)
{
weight = 0;
if (_blendWeights.Count == 0)
return false;
var index = _blendVariations.IndexOf(v);
if (index == -1)
{
return false;
}
weight = _blendWeights[index];
return true;
}
/// <summary>
/// Live cross-fade weight of a variation in the pool's persistent mix (set by the picker faders,
/// MIDI, arrows). Reports a weight only during a genuine mix of 2+ variations, so the resting
/// single-active state stays unannotated.
/// </summary>
public bool TryGetLiveBlendWeight(Variation v, out float weight)
{
weight = 0;
var pool = PoolForBlendOperations;
if (pool == null || !pool.IsLiveBlendMix)
return false;
weight = pool.GetBlendWeight(v.Id);
return weight > 0.001f;
}
private Vector2 GetNodeCenterOnScreen(ISelectableCanvasObject node)
{
var min = TransformPosition(node.PosOnCanvas);
var max = TransformPosition(node.PosOnCanvas + node.Size);
return (min + max) * 0.5f;
}
private void DrawContextMenu(Instance instance)
{
if (FrameStats.Current.OpenedPopUpName == string.Empty)
{
CustomComponents.DrawContextMenuForScrollCanvas(() =>
{
var oneOrMoreSelected = CanvasElementSelection.SelectedElements.Count > 0;
var oneSelected = CanvasElementSelection.SelectedElements.Count == 1;
if (ImGui.MenuItem("Delete selected",
"Del", // We should use the correct assigned short cut, but "Del or Backspace" is too long for layout
false,
oneOrMoreSelected))
{
DeleteSelectedElements();
}
if (ImGui.MenuItem("Rename",
"",
false,
oneSelected))
{
VariationThumbnail.VariationForRenaming =
CanvasElementSelection.SelectedElements[0] as Variation;
}
if (ImGui.MenuItem("Update thumbnails",
""))
{
_rerenderRequested = true;
_rerenderToFileRequested = true;
TriggerThumbnailUpdate();
}
ImGui.Separator();
var liveThumbnails = UserSettings.Config.VariationLiveThumbnails;
if (ImGui.MenuItem("Live Render Previews", "", ref liveThumbnails, true))
{
EnableLiveThumbnails(liveThumbnails);
}
ImGui.MenuItem("Preview on Hover", "", ref UserSettings.Config.VariationHoverPreview, true);
DrawAdditionalContextMenuContent(instance);
}, ref _contextMenuIsOpen);
}
}
private bool _contextMenuIsOpen;
public void StartHover(Variation variation, Instance instanceForBlending)
{
PoolForBlendOperations?.BeginHover(instanceForBlending, variation);
}
public void Apply(Variation variation, Instance instanceForBlending)
{
PoolForBlendOperations?.StopHover();
PoolForBlendOperations?.Apply(instanceForBlending, variation);
}
public void StartBlendTo(Variation variation, float blend, Instance instanceForBlending)
{
if (variation.IsPreset)
{
PoolForBlendOperations?.BeginBlendToPresent(instanceForBlending, variation, blend);
}
}
public void StopHover()
{
PoolForBlendOperations?.StopHover();
}
/// <summary>
/// Switches live thumbnail rendering on or off. Disabling restores the default
/// thumbnails that live previews replaced in the atlas.
/// </summary>
internal void EnableLiveThumbnails(bool enabled)
{
UserSettings.Config.VariationLiveThumbnails = enabled;
if (enabled)
{
TriggerThumbnailUpdate();
}
else
{
RestoreDefaultThumbnails();
}
}
private void RestoreDefaultThumbnails()
{
foreach (var id in _liveThumbnailIds)
{
ThumbnailManager.InvalidateThumbnail(id);
}
_liveThumbnailIds.Clear();
// Thumbnails loaded from the temp cache of a previous session aren't in the tracking
// set, so also invalidate the visible pool to reload its defaults from .meta.
var pool = PoolForBlendOperations;
if (pool == null)
return;
foreach (var variation in pool.AllVariations)
{
ThumbnailManager.InvalidateThumbnail(variation.Id);
}
}
protected void TriggerThumbnailUpdate()
{
PoolForBlendOperations?.StopHover();
_pendingThumbnailVariation = null;
_thumbnailCaptureDelayFrames = 0;
_renderThumbnailIndex = 0;
_allThumbnailsRendered = false;
}
protected void TriggerThumbnailSave()
{
PoolForBlendOperations?.StopHover();
_pendingThumbnailVariation = null;
_thumbnailCaptureDelayFrames = 0;
_renderThumbnailIndex = 0;
_allThumbnailsRendered = false;
_rerenderToFileRequested = true;
_rerenderRequested = true;
}
protected void ResetView(bool hideHeader = false)
{
var pool = PoolForBlendOperations;
if (pool == null)
return;
if (TryToGetBoundingBox(pool.AllVariations, 40, out var area))
{
if (!hideHeader)
{
area.Min.Y -= 50;
}
FitAreaOnCanvas(area);
}
_resetViewRequested = false;
}
private void HandleFenceSelection(SelectionFence selectionFence)
{
switch (selectionFence.UpdateAndDraw(out var selectMode))
{
case SelectionFence.States.PressedButNotMoved:
if (selectMode == SelectionFence.SelectModes.Replace)
CanvasElementSelection.Clear();
break;
case SelectionFence.States.Updated:
HandleSelectionFenceUpdate(selectionFence.BoundsInScreen);
break;
case SelectionFence.States.CompletedAsClick:
CanvasElementSelection.Clear();
break;
}
}
private void HandleSelectionFenceUpdate(ImRect boundsInScreen)
{
if (PoolForBlendOperations == null)
return;
var boundsInCanvas = InverseTransformRect(boundsInScreen);
var elementsToSelect = (from child in PoolForBlendOperations.AllVariations
let rect = new ImRect(child.PosOnCanvas, child.PosOnCanvas + child.Size)
where rect.Overlaps(boundsInCanvas)
select child).ToList();
CanvasElementSelection.Clear();
foreach (var element in elementsToSelect)
{
CanvasElementSelection.AddSelection(element);
}
}
private void DeleteSelectedElements()
{
if (PoolForBlendOperations == null)
return;
if (CanvasElementSelection.SelectedElements.Count <= 0)
return;
var list = new List<Variation>();
foreach (var e in CanvasElementSelection.SelectedElements)
{
if (e is Variation v)
{
list.Add(v);
}
}
VariationsWindow.DeleteVariationsFromPool(PoolForBlendOperations, list);
PoolForBlendOperations.SaveVariationsToFile();
}
#region thumbnail rendering
private void UpdateNextVariationThumbnail(Instance instanceForBlending, Slot<Texture2D> textureOutputSlot)
{
Debug.Assert(PoolForBlendOperations != null);
if (_allThumbnailsRendered)
return;
//_thumbnailCanvasRendering.InitializeCanvasTexture(VariationThumbnail.ThumbnailSize);
if (PoolForBlendOperations.AllVariations.Count == 0)
{
PoolForBlendOperations.StopHover();
_pendingThumbnailVariation = null;
_allThumbnailsRendered = true;
_rerenderRequested = false;
return;
}
if (_pendingThumbnailVariation != null)
{
if (_thumbnailCaptureDelayFrames > 0)
{
_thumbnailCaptureDelayFrames--;
return;
}
CapturePendingThumbnail(instanceForBlending, textureOutputSlot, _pendingThumbnailVariation);
_pendingThumbnailVariation = null;
_renderThumbnailIndex++;
return;
}
if (!TryGetNextVariationForThumbnailRendering(out var variation))
{
PoolForBlendOperations.StopHover();
_pendingThumbnailVariation = null;
_allThumbnailsRendered = true;
_rerenderRequested = false;
_rerenderToFileRequested = false;
return;
}
ApplyVariationForThumbnail(instanceForBlending, textureOutputSlot, variation);
}
private bool TryGetNextVariationForThumbnailRendering([NotNullWhen(true)] out Variation? variation)
{
variation = null;
if (PoolForBlendOperations == null)
return false;
var variations = PoolForBlendOperations.AllVariations;
for (; _renderThumbnailIndex < variations.Count; _renderThumbnailIndex++)
{
var candidate = variations[_renderThumbnailIndex];
// If nothing is selected, we just take the first available item.
// Otherwise, we skip until we find a selected one.
if (CanvasElementSelection.SelectedElements.Count == 0 ||
CanvasElementSelection.SelectedElements.Contains(candidate))
{
variation = candidate;
return true;
}
}
return false;
}
private void RenderThumbnail(Instance instanceForBlending, Slot<Texture2D> textureOutputSlot, Variation variation)
{
if (PoolForBlendOperations == null)
return;
// if (instanceForBlending.Outputs.Count == 0)
// return;
textureOutputSlot.DirtyFlag.ForceInvalidate();
textureOutputSlot.Update(_imageContext);
// Only explicit "Update thumbnails" requests overwrite the curated defaults in the
// package's .meta folder. Live previews go to the temp cache and are tracked so the
// defaults can be restored when live previews get disabled.
var savesDefaultThumbnail = _rerenderToFileRequested;
var saveAs = savesDefaultThumbnail
? ThumbnailManager.Categories.PackageMeta
: ThumbnailManager.Categories.Temp;
ThumbnailManager.SaveThumbnail(variation.Id, instanceForBlending.Symbol.SymbolPackage, textureOutputSlot.Value, saveAs, saveToFile: true, cover: true);
if (savesDefaultThumbnail)
_liveThumbnailIds.Remove(variation.Id);
else
_liveThumbnailIds.Add(variation.Id);
PoolForBlendOperations.StopHover();
}
private void ApplyVariationForThumbnail(Instance instanceForBlending, Slot<Texture2D> textureOutputSlot, Variation variation)
{
if (PoolForBlendOperations == null)
return;
// Keep the variation active for at least one frame so render and readback don't race.
PoolForBlendOperations.BeginHover(instanceForBlending, variation);
textureOutputSlot.DirtyFlag.ForceInvalidate();
textureOutputSlot.Update(_imageContext);
// Let the standalone renderer stand aside while we drive the pinned output.
VariationThumbnailRenderer.NotifyCanvasRendered();
_pendingThumbnailVariation = variation;
_thumbnailCaptureDelayFrames = ThumbnailCaptureDelayInFrames;
}
private void CapturePendingThumbnail(Instance instanceForBlending, Slot<Texture2D> textureOutputSlot, Variation variation)
{
RenderThumbnail(instanceForBlending, textureOutputSlot, variation);
}
#endregion
#region layout and view
public void RefreshView()
{
// Drop any pending re-render request — it targeted the previous pool. If it survived here
// (e.g. no texture output was pinned when a preset was created), it would render and save
// thumbnails for whatever op gets selected next, overwriting its curated defaults.
_rerenderRequested = false;
_rerenderToFileRequested = false;
TriggerThumbnailUpdate();
CanvasElementSelection.Clear();
//ResetView();
_resetViewRequested = true;
}
private static bool TryToGetBoundingBox(IEnumerable<Variation>? variations, float extend, out ImRect area)
{
area = new ImRect();
if (variations == null)
return false;
var foundOne = false;
foreach (var v in variations)
{
if (!foundOne)
{
area = ImRect.RectWithSize(v.PosOnCanvas, v.Size);
foundOne = true;
}
else
{
area.Add(ImRect.RectWithSize(v.PosOnCanvas, v.Size));
}
}
if (!foundOne)
return false;
area.Expand(Vector2.One * extend);
return true;
}
/// <summary>
/// This uses a primitive algorithm: Look for the bottom edge of a all element bounding box
/// Then step through possible positions and check if a position would intersect with an existing element.
/// Wrap columns to enforce some kind of grid.
/// </summary>
internal static Vector2 FindFreePositionForNewThumbnail(IReadOnlyList<Variation> variations)
{
if (!TryToGetBoundingBox(variations, 0, out var area))
{
return Vector2.Zero;
}
const int columns = 3;
var columnIndex = 0;
var stepWidth = VariationThumbnail.ThumbnailSize.X + VariationThumbnail.SnapPadding.X;
var stepHeight = VariationThumbnail.ThumbnailSize.Y + VariationThumbnail.SnapPadding.Y;
var pos = new Vector2(area.Min.X,
area.Max.Y - VariationThumbnail.ThumbnailSize.Y);
var rowStartPos = pos;
while (true)
{
var intersects = false;
var targetArea = new ImRect(pos, pos + VariationThumbnail.ThumbnailSize);
foreach (var v in variations)
{
if (!targetArea.Overlaps(ImRect.RectWithSize(v.PosOnCanvas, v.Size)))
continue;
intersects = true;
break;
}
if (!intersects)
return pos;
columnIndex++;
if (columnIndex == columns)
{
columnIndex = 0;
rowStartPos += new Vector2(0, stepHeight);
pos = rowStartPos;
}
else
{
pos += new Vector2(stepWidth, 0);
}
}
}
/// <summary>
/// Sorts snapshots by their <see cref="Variation.ActivationIndex"/> — the same index the MIDI
/// controller grid uses — so the list, the controller grid and procedural triggering all share one
/// order. The 2D canvas keeps its free <see cref="Variation.PosOnCanvas"/> layout independently.
/// </summary>
internal static void SortByActivationIndex(List<Variation> variations)
{
variations.Sort((a, b) => a.ActivationIndex.CompareTo(b.ActivationIndex));
}
#endregion
// Compute barycentric coordinates (u, v, w) for
// point p with respect to triangle (a, b, c)
private static void Barycentric(Vector2 p, Vector2 a, Vector2 b, Vector2 c, out float u, out float v, out float w)
{
Vector2 v0 = b - a, v1 = c - a, v2 = p - a;
var den = v0.X * v1.Y - v1.X * v0.Y;
v = (v2.X * v1.Y - v1.X * v2.Y) / den;
w = (v0.X * v2.Y - v2.X * v0.Y) / den;
u = 1.0f - v - w;
}
/// <summary>
/// Implement selectionContainer
/// </summary>
public IEnumerable<ISelectableCanvasObject> GetSelectables()
{
return PoolForBlendOperations?.AllVariations ?? [];
}
protected override ScalableCanvas? Parent => null;
private static readonly EvaluationContext _imageContext = new() { RequestedResolution = new Int2(170,130)};
public bool IsBlendingActive { get; private set; }
private readonly List<float> _blendWeights = new(3);
private readonly List<Vector2> _blendPoints = new(3);
private readonly List<Variation> _blendVariations = new(3);
/// <summary>Variation ids whose atlas slot holds a live preview instead of the default thumbnail.</summary>
private static readonly HashSet<Guid> _liveThumbnailIds = new();
private int _renderThumbnailIndex;
private bool _allThumbnailsRendered;
private Variation? _pendingThumbnailVariation;
private int _thumbnailCaptureDelayFrames;
private const int ThumbnailCaptureDelayInFrames = 1;
internal readonly CanvasElementSelection CanvasElementSelection = new();
private Instance? _currentRenderInstance;
private readonly SelectionFence _selectionFence = new();
}