Revert "Merge pull request #2637 from ousttrue/fix/node_name_unique"

This reverts commit ed5988e25a, reversing
changes made to cc1f8c0fc9.

# Conflicts:
#	Assets/UniGLTF/Runtime/UniGLTF/IO/GltfData.cs
#	Assets/UniGLTF/Runtime/UniGLTF/IO/GltfDuplicatedNameConversionRule.cs
#	Assets/UniGLTF/Runtime/UniGLTF/IO/ImporterContext.cs
This commit is contained in:
ousttrue
2025-04-17 18:10:07 +09:00
parent 885709fe06
commit 81f74e1e7f
15 changed files with 252 additions and 405 deletions

View File

@@ -81,14 +81,6 @@ namespace UniGLTF
_storage = storage;
MigrationFlags = migrationFlags;
// Version Compatibility
RestoreOlderVersionValues(json, GLTF);
foreach (var image in GLTF.images)
{
image.uri = PrepareImageUri(image.uri);
}
// init
if (Chunks != null)
{
@@ -105,51 +97,6 @@ namespace UniGLTF
_UriCache.Clear();
}
private static void RestoreOlderVersionValues(string Json, glTF GLTF)
{
if (string.IsNullOrEmpty(Json))
{
return;
}
var parsed = UniJSON.JsonParser.Parse(Json);
for (int i = 0; i < GLTF.images.Count; ++i)
{
if (string.IsNullOrEmpty(GLTF.images[i].name))
{
try
{
var extraName = parsed["images"][i]["extra"]["name"].Value.GetString();
if (!string.IsNullOrEmpty(extraName))
{
GLTF.images[i].name = extraName;
}
}
catch (Exception)
{
// do nothing
}
}
}
}
static string PrepareImageUri(string uri)
{
if (string.IsNullOrEmpty(uri))
{
return uri;
}
if (uri.StartsWith("./"))
{
// skip
uri = uri.Substring(2);
}
// %20 to ' ' etc...
var unescape = Uri.UnescapeDataString(uri);
return unescape;
}
public static GltfData CreateFromExportForTest(ExportingGltfData data)
{
return CreateFromGltfDataForTest(data.Gltf, data.BinBytes);

View File

@@ -1,286 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace UniGLTF
{
/// <summary>
/// `v0.129.0`
/// glTF は仕様として、node 等の名前の重複を許している。
/// しかし Unity にロードする上では、名前が重複すると困る場面が多い。
/// したがって UniVRM ではロード時に、名前を重複しないように破壊的変更を行う仕様とする。
/// ここではその命名変更規則を定義する。
/// </summary>
public static class GltfDuplicatedNameConversionRule
{
public static readonly string UniqueFixResourceSuffix = "__UNIGLTF__DUPLICATED__";
private static readonly Regex _removeUniqueFixResourceSuffix = new Regex($@"^(.+){UniqueFixResourceSuffix}(\d+)$");
/// <summary>
/// `v0.129.0` GlbLowLevelParser.Parse からこちらに移動。
/// glTF では許されるが Unity では都合の悪い名前に対する変更を行う。
/// </summary>
public static void FixNames(glTF GLTF)
{
FixMeshNameUnique(GLTF);
FixTextureNameUnique(GLTF);
FixMaterialNameUnique(GLTF);
FixEmptyNodeName(GLTF);
FixAnimationNameUnique(GLTF);
}
public static string FixNameUnique(HashSet<string> used, string originalName)
{
if (used.Add(originalName))
{
return originalName;
}
var duplicatedIdx = 2;
while (true)
{
var newName = $"{originalName}{UniqueFixResourceSuffix}{duplicatedIdx++}";
if (used.Add(newName))
{
return newName;
}
}
}
/// <summary>
/// GlbLowLevelParser.FixNameUnique で付与した Suffix を remove
/// </summary>
public static void FixName(glTF gltf)
{
var regex = new Regex($@"{UniqueFixResourceSuffix}\d+$");
foreach (var gltfImages in gltf.images)
{
if (regex.IsMatch(gltfImages.name))
{
gltfImages.name = regex.Replace(gltfImages.name, string.Empty);
}
}
foreach (var gltfMaterial in gltf.materials)
{
if (regex.IsMatch(gltfMaterial.name))
{
gltfMaterial.name = regex.Replace(gltfMaterial.name, string.Empty);
}
}
foreach (var gltfAnimation in gltf.animations)
{
if (regex.IsMatch(gltfAnimation.name))
{
gltfAnimation.name = regex.Replace(gltfAnimation.name, string.Empty);
}
}
}
public static bool TryGetOriginalName(string name, out string originalName)
{
var match = _removeUniqueFixResourceSuffix.Match(name);
if (match.Success)
{
originalName = match.Groups[1].Value;
return true;
}
else
{
originalName = name;
return false;
}
}
public static void FixMeshNameUnique(glTF GLTF)
{
var used = new HashSet<string>();
foreach (var mesh in GLTF.meshes)
{
if (string.IsNullOrEmpty(mesh.name))
{
// empty
mesh.name = "mesh_" + Guid.NewGuid().ToString("N");
used.Add(mesh.name);
}
else
{
var lower = mesh.name.ToLower();
if (used.Contains(lower))
{
// rename
var uname = lower + "_" + Guid.NewGuid().ToString("N");
mesh.name = uname;
lower = uname;
}
used.Add(lower);
}
}
}
public static void RenameImageFromTexture(glTF GLTF, int i)
{
foreach (var texture in GLTF.textures)
{
if (texture.source == i)
{
if (!string.IsNullOrEmpty(texture.name))
{
GLTF.images[i].name = texture.name;
return;
}
}
}
}
/// <summary>
/// gltfTexture.name を Unity Asset 名として運用する。
/// ユニークである必要がある。
/// </summary>
public static void FixTextureNameUnique(glTF GLTF)
{
// NOTE: Windows FileSystem は大文字小文字の違いは同名ファイルとして扱ってしまうため, IgnoreCase で評価する.
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var textureIdx = 0; textureIdx < GLTF.textures.Count; ++textureIdx)
{
var gltfTexture = GLTF.textures[textureIdx];
if (gltfTexture.source.HasValidIndex())
{
var gltfImage = GLTF.images[gltfTexture.source.Value];
if (!string.IsNullOrEmpty(gltfImage.uri) && !gltfImage.uri.StartsWith("data:"))
{
// from image uri
gltfTexture.name = Path.GetFileNameWithoutExtension(gltfImage.uri);
}
if (string.IsNullOrEmpty(gltfTexture.name))
{
// use image name
gltfTexture.name = gltfImage.name;
}
}
if (string.IsNullOrEmpty(gltfTexture.name))
{
gltfTexture.name = $"texture_{textureIdx}";
}
gltfTexture.name = FixNameUnique(used, gltfTexture.name);
}
}
public static void FixMaterialNameUnique(glTF GLTF)
{
// NOTE: Windows FileSystem は大文字小文字の違いは同名ファイルとして扱ってしまうため, IgnoreCase で評価する.
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var materialIdx = 0; materialIdx < GLTF.materials.Count; ++materialIdx)
{
var material = GLTF.materials[materialIdx];
if (string.IsNullOrEmpty(material.name))
{
material.name = $"material_{materialIdx}";
}
material.name = FixNameUnique(used, material.name);
}
}
/// <summary>
/// rename empty name to $"{index}"
/// 名前が null または 空文字列である場合に、連番による名前を付ける。
/// 名前の重複はそのままにする。
/// </summary>
public static void FixEmptyNodeName(glTF GLTF)
{
for (var i = 0; i < GLTF.nodes.Count; ++i)
{
var node = GLTF.nodes[i];
if (string.IsNullOrWhiteSpace(node.name))
{
node.name = $"{i}";
}
}
}
public static void FixAnimationNameUnique(glTF GLTF)
{
// NOTE: Windows FileSystem は大文字小文字の違いは同名ファイルとして扱ってしまうため, IgnoreCase で評価する.
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < GLTF.animations.Count; ++i)
{
var animation = GLTF.animations[i];
if (string.IsNullOrEmpty(animation.name))
{
animation.name = $"animation_{i}";
}
animation.name = FixNameUnique(used, animation.name);
}
}
/// <summary>
/// AvatarBuilder.BuildHumanAvatar のエラーを防止するため Node名を Unique にする。
/// 主に humanoid avatar を作成する vrm-0.x, vrm-1.0, vrma の import で使う。
/// </summary>
public static void FixNodeNameUnique(glTF GLTF)
{
FixEmptyNodeName(GLTF);
var m_uniqueNameSet = new HashSet<string>();
int counter = 1;
for (var i = 0; i < GLTF.nodes.Count; ++i)
{
RenameIfDupName(GLTF.nodes, i, m_uniqueNameSet, ref counter);
}
}
static void RenameIfDupName(List<glTFNode> nodes, int index, HashSet<string> m_uniqueNameSet, ref int m_counter)
{
var t = nodes[index];
if (!m_uniqueNameSet.Contains(t.name))
{
m_uniqueNameSet.Add(t.name);
return;
}
var parent = nodes.FirstOrDefault(x => x.children != null && x.children.Contains(index));
if (parent != null && (t.children == null || t.children.Length == 0))
{
/// AvatarBuilder:BuildHumanAvatar で同名の Transform があるとエラーになる。
///
/// AvatarBuilder 'GLTF': Ambiguous Transform '32/root/torso_1/torso_2/torso_3/torso_4/torso_5/torso_6/torso_7/neck_1/neck_2/head/ENDSITE' and '32/root/torso_1/torso_2/torso_3/torso_4/torso_5/torso_6/torso_7/l_shoulder/l_up_arm/l_low_arm/l_hand/ENDSITE' found in hierarchy for human bone 'Head'. Transform name mapped to a human bone must be unique.
/// UnityEngine.AvatarBuilder:BuildHumanAvatar (UnityEngine.GameObject,UnityEngine.HumanDescription)
/// UniHumanoid.AvatarDescription:CreateAvatar (UnityEngine.Transform)
///
/// 主に BVH の EndSite 由来の GameObject 名が重複することへの対策
/// ex: parent-ENDSITE
var newName = $"{parent.name}-{t.name}";
if (!m_uniqueNameSet.Contains(newName))
{
UniGLTFLogger.Warning($"force rename !!: {t.name} => {newName}");
t.name = newName;
m_uniqueNameSet.Add(newName);
return;
}
}
// 連番
for (int i = 0; i < 100; ++i)
{
// ex: name.1
var newName = $"{t.name}{m_counter++}";
if (!m_uniqueNameSet.Contains(newName))
{
UniGLTFLogger.Warning($"force rename: {t.name} => {newName}");
t.name = newName;
m_uniqueNameSet.Add(newName);
return;
}
}
throw new NotImplementedException();
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 8ded08d75fb22a94f80265d4403d5b3e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -14,7 +14,7 @@ namespace UniGLTF
{
public readonly bool IsAssetImport;
private readonly ImporterContextSettings _settings;
public ITextureDescriptorGenerator TextureDescriptorGenerator { get; protected set; }
public IMaterialDescriptorGenerator MaterialDescriptorGenerator { get; protected set; }
public TextureFactory TextureFactory { get; }
@@ -95,9 +95,6 @@ namespace UniGLTF
MeasureTime = new ImporterContextSpeedLog().MeasureTime;
}
// 前処理
await PreprocessAsync(awaitCaller);
if (GLTF.extensionsRequired != null)
{
var sb = new List<string>();
@@ -145,20 +142,6 @@ namespace UniGLTF
return instance;
}
/// <summary>
/// from `v0.129.0`
/// `GltfData` をロードする前に行う必要のある処理を記述する。
/// デフォルト実装では、glTF では許されるが Unity では問題となる名前の重複を解決する。
/// </summary>
protected virtual async Task PreprocessAsync(IAwaitCaller awaitCaller)
{
await awaitCaller.Run(() =>
{
// `v0.129.0` GlbLowLevelParser.Parse からこちらに移動
GltfDuplicatedNameConversionRule.FixNames(GLTF);
});
}
public virtual async Task LoadAnimationAsync(IAwaitCaller awaitCaller)
{
if (GLTF.animations != null && GLTF.animations.Any())

View File

@@ -13,6 +13,9 @@ namespace UniGLTF
/// </summary>
public sealed class GlbLowLevelParser
{
public static readonly string UniqueFixResourceSuffix = "__UNIGLTF__DUPLICATED__";
private static readonly Regex _removeUniqueFixResourceSuffix = new Regex($@"^(.+){UniqueFixResourceSuffix}(\d+)$");
private readonly string _path;
private readonly byte[] _binary;
@@ -76,9 +79,191 @@ namespace UniGLTF
throw new UniGLTFException("unknown gltf version {0}", GLTF.asset.version);
}
// Version Compatibility
RestoreOlderVersionValues(json, GLTF);
FixMeshNameUnique(GLTF);
foreach (var image in GLTF.images)
{
image.uri = PrepareUri(image.uri);
}
FixTextureNameUnique(GLTF);
FixMaterialNameUnique(GLTF);
FixNodeName(GLTF);
FixAnimationNameUnique(GLTF);
return new GltfData(path, json, GLTF, chunks, storage, migrationFlags);
}
private static void FixMeshNameUnique(glTF GLTF)
{
var used = new HashSet<string>();
foreach (var mesh in GLTF.meshes)
{
if (string.IsNullOrEmpty(mesh.name))
{
// empty
mesh.name = "mesh_" + Guid.NewGuid().ToString("N");
used.Add(mesh.name);
}
else
{
var lower = mesh.name.ToLower();
if (used.Contains(lower))
{
// rename
var uname = lower + "_" + Guid.NewGuid().ToString("N");
mesh.name = uname;
lower = uname;
}
used.Add(lower);
}
}
}
private static void RenameImageFromTexture(glTF GLTF, int i)
{
foreach (var texture in GLTF.textures)
{
if (texture.source == i)
{
if (!string.IsNullOrEmpty(texture.name))
{
GLTF.images[i].name = texture.name;
return;
}
}
}
}
/// <summary>
/// image.uri を前理
/// </summary>
/// <param name="uri"></param>
/// <returns></returns>
public static string PrepareUri(string uri)
{
if (string.IsNullOrEmpty(uri))
{
return uri;
}
if (uri.StartsWith("./"))
{
// skip
uri = uri.Substring(2);
}
// %20 to ' ' etc...
var unescape = Uri.UnescapeDataString(uri);
return unescape;
}
/// <summary>
/// gltfTexture.name を Unity Asset 名として運用する。
/// ユニークである必要がある。
/// </summary>
private static void FixTextureNameUnique(glTF GLTF)
{
// NOTE: Windows FileSystem は大文字小文字の違いは同名ファイルとして扱ってしまうため, IgnoreCase で評価する.
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var textureIdx = 0; textureIdx < GLTF.textures.Count; ++textureIdx)
{
var gltfTexture = GLTF.textures[textureIdx];
if (gltfTexture.source.HasValidIndex())
{
var gltfImage = GLTF.images[gltfTexture.source.Value];
if (!string.IsNullOrEmpty(gltfImage.uri) && !gltfImage.uri.StartsWith("data:"))
{
// from image uri
gltfTexture.name = Path.GetFileNameWithoutExtension(gltfImage.uri);
}
if (string.IsNullOrEmpty(gltfTexture.name))
{
// use image name
gltfTexture.name = gltfImage.name;
}
}
if (string.IsNullOrEmpty(gltfTexture.name))
{
gltfTexture.name = $"texture_{textureIdx}";
}
gltfTexture.name = FixNameUnique(used, gltfTexture.name);
}
}
private static void FixMaterialNameUnique(glTF GLTF)
{
// NOTE: Windows FileSystem は大文字小文字の違いは同名ファイルとして扱ってしまうため, IgnoreCase で評価する.
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var materialIdx = 0; materialIdx < GLTF.materials.Count; ++materialIdx)
{
var material = GLTF.materials[materialIdx];
if (string.IsNullOrEmpty(material.name))
{
material.name = $"material_{materialIdx}";
}
material.name = FixNameUnique(used, material.name);
}
}
/// <summary>
/// rename empty name to $"{index}"
/// </summary>
private static void FixNodeName(glTF GLTF)
{
for (var i = 0; i < GLTF.nodes.Count; ++i)
{
var node = GLTF.nodes[i];
if (string.IsNullOrWhiteSpace(node.name))
{
node.name = $"{i}";
}
}
}
private static void FixAnimationNameUnique(glTF GLTF)
{
// NOTE: Windows FileSystem は大文字小文字の違いは同名ファイルとして扱ってしまうため, IgnoreCase で評価する.
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < GLTF.animations.Count; ++i)
{
var animation = GLTF.animations[i];
if (string.IsNullOrEmpty(animation.name))
{
animation.name = $"animation_{i}";
}
animation.name = FixNameUnique(used, animation.name);
}
}
private static void RestoreOlderVersionValues(string Json, glTF GLTF)
{
var parsed = UniJSON.JsonParser.Parse(Json);
for (int i = 0; i < GLTF.images.Count; ++i)
{
if (string.IsNullOrEmpty(GLTF.images[i].name))
{
try
{
var extraName = parsed["images"][i]["extra"]["name"].Value.GetString();
if (!string.IsNullOrEmpty(extraName))
{
GLTF.images[i].name = extraName;
}
}
catch (Exception)
{
// do nothing
}
}
}
}
public static void AppendImageExtension(glTFImage texture, string extension)
{
@@ -87,5 +272,38 @@ namespace UniGLTF
texture.name = texture.name + extension;
}
}
public static string FixNameUnique(HashSet<string> used, string originalName)
{
if (used.Add(originalName))
{
return originalName;
}
var duplicatedIdx = 2;
while (true)
{
var newName = $"{originalName}{UniqueFixResourceSuffix}{duplicatedIdx++}";
if (used.Add(newName))
{
return newName;
}
}
}
public static bool TryGetOriginalName(string name, out string originalName)
{
var match = _removeUniqueFixResourceSuffix.Match(name);
if (match.Success)
{
originalName = match.Groups[1].Value;
return true;
}
else
{
originalName = name;
return false;
}
}
}
}

View File

@@ -366,7 +366,36 @@ namespace UniGLTF
GltfTextureExporter.PushGltfTexture(_data, unityTexture, colorSpace, m_textureSerializer);
}
GltfDuplicatedNameConversionRule.FixName(_gltf);
FixName(_gltf);
}
/// <summary>
/// GlbLowLevelParser.FixNameUnique で付与した Suffix を remove
/// </summary>
public static void FixName(glTF gltf)
{
var regex = new Regex($@"{GlbLowLevelParser.UniqueFixResourceSuffix}\d+$");
foreach (var gltfImages in gltf.images)
{
if (regex.IsMatch(gltfImages.name))
{
gltfImages.name = regex.Replace(gltfImages.name, string.Empty);
}
}
foreach (var gltfMaterial in gltf.materials)
{
if (regex.IsMatch(gltfMaterial.name))
{
gltfMaterial.name = regex.Replace(gltfMaterial.name, string.Empty);
}
}
foreach (var gltfAnimation in gltf.animations)
{
if (regex.IsMatch(gltfAnimation.name))
{
gltfAnimation.name = regex.Replace(gltfAnimation.name, string.Empty);
}
}
}
#endregion
}

View File

@@ -35,8 +35,6 @@ namespace UniGLTF
var parser = new GlbLowLevelParser("Test", data.ToGlbBytes());
using (var parsed = parser.Parse())
{
GltfDuplicatedNameConversionRule.FixNames(parsed.GLTF);
Assert.AreEqual("FooBar", parsed.GLTF.textures[0].name);
// NOTE: 大文字小文字が違うだけの名前は、同一としてみなされ、Suffix が付く。
Assert.AreEqual("foobar__UNIGLTF__DUPLICATED__2", parsed.GLTF.textures[1].name);

View File

@@ -36,15 +36,6 @@ namespace VRM
_springBoneRuntime = springboneRuntime ?? new Vrm0XSpringBoneDefaultRuntime();
}
protected override async Task PreprocessAsync(IAwaitCaller awaitCaller)
{
await base.PreprocessAsync(awaitCaller);
await awaitCaller.Run(() =>
{
GltfDuplicatedNameConversionRule.FixNodeNameUnique(GLTF);
});
}
#region OnLoad
protected override async Task OnLoadHierarchy(IAwaitCaller awaitCaller, Func<string, IDisposable> MeasureTime)
{

View File

@@ -69,7 +69,6 @@ namespace VRM
var path = AliciaPath;
using (var data = new GlbFileParser(path).Parse())
{
GltfDuplicatedNameConversionRule.FixNames(data.GLTF);
var vrmImporter = new VRMImporterContext(new VRMData(data), null);
var materialParam = new BuiltInVrmMaterialDescriptorGenerator(vrmImporter.VRM).Get(data, 0);
Assert.AreEqual("VRM/MToon", materialParam.Shader.name);

View File

@@ -61,7 +61,6 @@ namespace VRM
new ArraySegment<byte>(Array.Empty<byte>())
))
{
GltfDuplicatedNameConversionRule.FixNames(data.GLTF);
var vrm = new glTF_VRM_extensions
{
materialProperties = new List<glTF_VRM_Material>
@@ -125,7 +124,6 @@ namespace VRM
new ArraySegment<byte>(Array.Empty<byte>())
))
{
GltfDuplicatedNameConversionRule.FixNames(data.GLTF);
var vrm = new glTF_VRM_extensions
{
materialProperties = new List<glTF_VRM_Material>

View File

@@ -95,7 +95,7 @@ namespace UniVRM10
// data.GLTF.textures は前処理によりユニーク性がある
// unique な名前を振り出す
var used = new HashSet<string>(data.GLTF.textures.Select(x => x.name));
var uniqueName = GltfDuplicatedNameConversionRule.FixNameUnique(used, UniqueThumbnailName);
var uniqueName = GlbLowLevelParser.FixNameUnique(used, UniqueThumbnailName);
value = GltfTextureImporter.CreateSrgbFromOnlyImage(data, imageIndex, uniqueName, gltfImage.uri);
return true;

View File

@@ -235,9 +235,10 @@ namespace UniVRM10
}
// Fix Duplicated name
GltfDuplicatedNameConversionRule.FixName(Storage.Gltf);
gltfExporter.FixName(Storage.Gltf);
}
/// <summary>
/// VRMコンポーネントのエクスポート
/// </summary>

View File

@@ -79,15 +79,6 @@ namespace UniVRM10
m_springboneRuntime = springboneRuntime;
}
protected override async Task PreprocessAsync(IAwaitCaller awaitCaller)
{
await base.PreprocessAsync(awaitCaller);
await awaitCaller.Run(() =>
{
GltfDuplicatedNameConversionRule.FixNodeNameUnique(GLTF);
});
}
static void AssignHumanoid(List<VrmLib.Node> nodes, UniGLTF.Extensions.VRMC_vrm.HumanBone humanBone, VrmLib.HumanoidBones key)
{
if (nodes == null)

View File

@@ -26,15 +26,6 @@ namespace UniVRM10
m_vrma = GetExtension(Data);
}
protected override async Task PreprocessAsync(IAwaitCaller awaitCaller)
{
await base.PreprocessAsync(awaitCaller);
await awaitCaller.Run(() =>
{
GltfDuplicatedNameConversionRule.FixNodeNameUnique(GLTF);
});
}
private static VRMC_vrm_animation GetExtension(GltfData data)
{
if (data.GLTF.extensions is UniGLTF.glTFExtensionImport extensions)

View File

@@ -23,8 +23,6 @@ namespace UniVRM10
var migratedBytes = MigrationVrm.Migrate(File.ReadAllBytes(AliciaPath));
using (var data = new GlbLowLevelParser(AliciaPath, migratedBytes).Parse())
{
GltfDuplicatedNameConversionRule.FixNames(data.GLTF);
var matDesc = new BuiltInVrm10MaterialDescriptorGenerator().Get(data, 0);
Assert.AreEqual("Alicia_body", matDesc.Name);
Assert.AreEqual("VRM10/MToon10", matDesc.Shader.name);