Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/MaterialEditor.Base/MaterialEditor.Base.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<Import_RootNamespace>MaterialEditor.API</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)UI\Sections\Properties\PropertyRowBuilders.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\MaterialTextureImportTransaction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\MaterialCubemapImportTransaction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\MaterialEditResult.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\MaterialEditRequestQueue.cs" />
<Compile Include="$(MSBuildThisFileDirectory)NormalMapManager.cs" />
Expand Down Expand Up @@ -81,7 +84,6 @@
<Compile Include="$(MSBuildThisFileDirectory)UI\UI.Presentation.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\UI.ShaderUiMetadata.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\UI.PropertyDescriptor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\Sections\Properties\PropertyRowBuilders.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\UI.AutoScrollToCenter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\UI.FloatLabelDragTrigger.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\UI.DropdownFilter.cs" />
Expand Down
105 changes: 105 additions & 0 deletions src/MaterialEditor.Base/Operations/MaterialCubemapImportTransaction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using UnityEngine;

namespace MaterialEditorAPI
{
/// <summary>Controller-owned byte storage and leases; no serialization types cross this boundary.</summary>
internal sealed class MaterialCubemapImportStorage
{
internal Func<int> Count;
internal Func<byte[], int> StoreData;
internal Action<int, MaterialEditorCubemapLease> StoreLease;
internal Action<int> RemoveCreated;
internal Action PurgeLeases;
}

internal sealed class MaterialCubemapRecordAccess<T>
{
internal Func<T, int?> GetId;
internal Action<T, int?> SetId;
internal Func<T, MaterialCubemapOriginalState> Original;
}

/// <summary>One transaction for Chara and Studio; candidates stay detached until application succeeds.</summary>
internal static class MaterialCubemapImportTransaction
{
private static void Cleanup(Action action)
{
try { action(); }
catch (Exception ex) { MaterialEditorPluginBase.Logger?.LogWarning("Cubemap cleanup: " + ex.Message); }
}

internal static MaterialEditResult Execute<T>(byte[] data, MaterialEditorCubemapContentKey key,
GameObject root, string materialName, string property, bool logNormalization,
MaterialCubemapImportStorage storage, IList<T> records, T existing,
Func<int, T> create, MaterialCubemapRecordAccess<T> access, Func<T, bool> apply) where T : class
{
if (data == null || root == null)
return new MaterialEditResult(MaterialEditStatus.Failed, "Validate");

MaterialEditorCubemapLease lease = null;
MaterialTextureSnapshot runtime = null;
T candidate = null;
var created = false;
var committed = false;
var texId = 0;
var stage = "Acquire";
var previousId = existing == null ? null : access.GetId(existing);
var previousState = existing == null ? null : access.Original(existing).CaptureCheckpoint();
try
{
string warning;
string error;
var acquired = key == null
? MaterialEditorCubemapCache.TryAcquire(data, out lease, out warning, out error)
: MaterialEditorCubemapCache.TryAcquire(data, key, out lease, out warning, out error);
if (!acquired) return new MaterialEditResult(MaterialEditStatus.Failed, stage, error);
if (logNormalization && !string.IsNullOrEmpty(warning))
MaterialEditorPluginBase.Logger?.LogWarning(warning);
stage = "Store";
var count = storage.Count();
texId = storage.StoreData(data);
created = storage.Count() > count;
storage.StoreLease(texId, lease);
lease = null;

stage = "Snapshot";
runtime = new MaterialTextureSnapshot(root, materialName, property);
candidate = create(texId);
// Checkpoints are copy-on-replace state; Synchronize builds new collections.
if (existing != null) access.Original(candidate).RestoreCheckpoint(previousState, false);
stage = "Apply";
if (!apply(candidate))
return new MaterialEditResult(MaterialEditStatus.Failed, stage, "Previous override preserved.");
stage = "Commit";
if (existing == null) records.Add(candidate);
else
{
access.SetId(existing, texId);
access.Original(existing).RestoreCheckpoint(access.Original(candidate).CaptureCheckpoint(), false);
}
committed = true;
return new MaterialEditResult(MaterialEditStatus.Succeeded, stage);
}
catch (Exception ex) { return new MaterialEditResult(MaterialEditStatus.Failed, stage, ex.Message); }
finally
{
if (!committed)
{
runtime?.Restore();
if (existing != null)
{
access.SetId(existing, previousId);
access.Original(existing).RestoreCheckpoint(previousState, true);
}
else if (candidate != null) records.Remove(candidate);
if (candidate != null) access.Original(candidate).Clear();
if (created) Cleanup(() => storage.RemoveCreated(texId));
}
if (lease != null) Cleanup(lease.Dispose);
Cleanup(storage.PurgeLeases);
}
}
}
}
108 changes: 108 additions & 0 deletions src/MaterialEditor.Base/Operations/MaterialTextureImportTransaction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using UnityEngine;

namespace MaterialEditorAPI
{
/// <summary>Texture2D prepare/apply/commit boundary; animation-specific rollback stays with its controller.</summary>
internal static class MaterialTextureImportTransaction
{
internal static MaterialEditResult Execute<T>(GameObject root, string materialName, string property,
Func<T> prepare, Func<T, bool> apply, Action<T> commit, Action<T> discard) where T : class
{
T candidate = null;
MaterialTextureSnapshot snapshot = null;
var committed = false;
var stage = "Snapshot";
try
{
snapshot = new MaterialTextureSnapshot(root, materialName, property);
stage = "Prepare texture/animation";
candidate = prepare();
stage = "Apply";
if (!apply(candidate))
return new MaterialEditResult(MaterialEditStatus.Failed, stage);
stage = "Commit";
commit(candidate);
committed = true;
return new MaterialEditResult(MaterialEditStatus.Succeeded, stage);
}
catch (Exception ex)
{
return new MaterialEditResult(MaterialEditStatus.Failed, stage, ex.Message);
}
finally
{
if (!committed)
{
snapshot?.Restore();
try { discard(candidate); }
catch (Exception ex) { MaterialEditorPluginBase.Logger?.LogWarning("Texture cleanup: " + ex.Message); }
}
}
}

// Keep the existing DTO identity: animation bindings may still reference it.
internal static void Commit<T, TController, TAnimation>(IList<T> records, IDictionary<T, TController> animations,
T existing, T candidate, Func<T, int?> getId, Action<T, int?> setId,
Func<T, TAnimation> getAnimation, Action<T, TAnimation> setAnimation) where T : class
{
if (existing == null)
{
records.Add(candidate);
return;
}
var previousId = getId(existing);
var previousAnimation = getAnimation(existing);
TController previousController;
TController candidateController;
var hadPrevious = animations.TryGetValue(existing, out previousController);
var hasCandidate = animations.TryGetValue(candidate, out candidateController);
try
{
setId(existing, getId(candidate));
setAnimation(existing, getAnimation(candidate));
animations.Remove(candidate);
if (hasCandidate) animations[existing] = candidateController;
else animations.Remove(existing);
}
catch
{
setId(existing, previousId);
setAnimation(existing, previousAnimation);
animations.Remove(candidate);
if (hadPrevious) animations[existing] = previousController;
else animations.Remove(existing);
throw;
}
}
}

/// <summary>Rollback values for every runtime material touched by SetTexture, including projectors.</summary>
internal sealed class MaterialTextureSnapshot
{
private readonly Dictionary<Material, Texture> _values = new Dictionary<Material, Texture>();
private readonly string _property;

internal MaterialTextureSnapshot(GameObject root, string name, string property)
{
_property = "_" + property;
foreach (var material in MaterialAPI.GetObjectMaterials(root, name))
if (material != null && material.HasProperty(_property))
_values[material] = material.GetTexture(_property);
}

internal void Restore()
{
foreach (var entry in _values)
{
try
{
if (entry.Key != null && entry.Key.HasProperty(_property))
entry.Key.SetTexture(_property, entry.Value);
}
catch (Exception ex) { MaterialEditorPluginBase.Logger?.LogWarning("Texture rollback: " + ex.Message); }
}
}
}
}
Loading
Loading