diff --git a/src/PlateauResoniteLink/Application/Importing/CityGmlAppearanceStore.cs b/src/PlateauResoniteLink/Application/Importing/CityGmlAppearanceStore.cs index 01c1e6c5..6d84d860 100644 --- a/src/PlateauResoniteLink/Application/Importing/CityGmlAppearanceStore.cs +++ b/src/PlateauResoniteLink/Application/Importing/CityGmlAppearanceStore.cs @@ -191,13 +191,11 @@ private void ApplyGeoreferencedTexture(XElement textureElement) texturePayload = new EncodedImageTexturePayload( null, null, - "sRGB", TextureImportSourceFactory.CreateDatasetEncodedImage( datasetSource, resolvedTexturePath, "sRGB", - $"dataset:{resolvedTexturePath}"), - $"dataset:{resolvedTexturePath}"); + $"dataset:{resolvedTexturePath}")); texturePayloadsByResolvedPath[resolvedTexturePath] = texturePayload; } diff --git a/src/PlateauResoniteLink/Application/Importing/MaterialGroupingPolicy.cs b/src/PlateauResoniteLink/Application/Importing/MaterialGroupingPolicy.cs index 1f189310..c50e54b2 100644 --- a/src/PlateauResoniteLink/Application/Importing/MaterialGroupingPolicy.cs +++ b/src/PlateauResoniteLink/Application/Importing/MaterialGroupingPolicy.cs @@ -33,7 +33,7 @@ internal static MaterialGroupingKey CreateKey( return new MaterialGroupingKey( material.MaterialType, - material.TexturePayload?.Identity, + material.TexturePayload?.Source.Identity, material.TextureSourceKind, material.Projection, depthOffset, diff --git a/src/PlateauResoniteLink/Application/Importing/SceneImportContractTypes.cs b/src/PlateauResoniteLink/Application/Importing/SceneImportContractTypes.cs index fbf46231..3f3fc31b 100644 --- a/src/PlateauResoniteLink/Application/Importing/SceneImportContractTypes.cs +++ b/src/PlateauResoniteLink/Application/Importing/SceneImportContractTypes.cs @@ -98,24 +98,13 @@ public sealed record MeshSubmesh( public abstract record TexturePayload { - private protected TexturePayload( - string? colorProfile, - string identity, - ITextureImportSource source) + private protected TexturePayload(ITextureImportSource source) { - if (string.IsNullOrWhiteSpace(identity)) - { - throw new ArgumentException("Texture payload identity must be provided.", nameof(identity)); - } - - ColorProfile = colorProfile; - Identity = identity; - Source = source ?? throw new ArgumentNullException(nameof(source)); - } - - public string? ColorProfile { get; } + ArgumentNullException.ThrowIfNull(source); + ArgumentException.ThrowIfNullOrWhiteSpace(source.Identity); - public string Identity { get; } + Source = source; + } public ITextureImportSource Source { get; } } @@ -142,8 +131,8 @@ private RawRgba32TexturePayload( int height, string? colorProfile, byte[] binaryPayload, - (string Identity, ITextureImportSource Source) source) - : base(colorProfile, source.Identity, source.Source) + ITextureImportSource source) + : base(source) { ArgumentNullException.ThrowIfNull(binaryPayload); Width = width; @@ -157,7 +146,7 @@ private RawRgba32TexturePayload( public ImmutableArray BinaryPayload { get; } - private static (string Identity, ITextureImportSource Source) CreateSource( + private static ITextureImportSource CreateSource( int width, int height, string? colorProfile, @@ -166,14 +155,12 @@ private static (string Identity, ITextureImportSource Source) CreateSource( { ArgumentNullException.ThrowIfNull(binaryPayload); string effectiveIdentity = identity ?? Guid.NewGuid().ToString("N"); - return ( - effectiveIdentity, - TextureImportSourceFactory.CreateInMemoryRaw( - width, - height, - colorProfile, - binaryPayload, - effectiveIdentity)); + return TextureImportSourceFactory.CreateInMemoryRaw( + width, + height, + colorProfile, + binaryPayload, + effectiveIdentity); } } @@ -182,19 +169,8 @@ public sealed record EncodedImageTexturePayload : TexturePayload public EncodedImageTexturePayload( int? width, int? height, - string? colorProfile, - ITextureImportSource source, - string? identity = null) - : this(width, height, colorProfile, CreateSource(source, identity)) - { - } - - private EncodedImageTexturePayload( - int? width, - int? height, - string? colorProfile, - (string Identity, ITextureImportSource Source) source) - : base(colorProfile, source.Identity, source.Source) + ITextureImportSource source) + : base(source) { Width = width; Height = height; @@ -203,14 +179,6 @@ private EncodedImageTexturePayload( public int? Width { get; } public int? Height { get; } - - private static (string Identity, ITextureImportSource Source) CreateSource( - ITextureImportSource source, - string? identity) - { - ArgumentNullException.ThrowIfNull(source); - return (identity ?? source.Identity, source); - } } public enum TextureSourceKind diff --git a/src/PlateauResoniteLink/Targets/Resonite/ResoniteTexturePayload.cs b/src/PlateauResoniteLink/Targets/Resonite/ResoniteTexturePayload.cs index b322e354..0acf67fe 100644 --- a/src/PlateauResoniteLink/Targets/Resonite/ResoniteTexturePayload.cs +++ b/src/PlateauResoniteLink/Targets/Resonite/ResoniteTexturePayload.cs @@ -7,25 +7,15 @@ namespace PlateauResoniteLink.Targets.Resonite; public abstract class ResoniteTexturePayload { - private protected ResoniteTexturePayload( - string? colorProfile, - string identity, - ITextureImportSource source) + private protected ResoniteTexturePayload(ITextureImportSource source) { - if (string.IsNullOrWhiteSpace(identity)) + Source = source ?? throw new ArgumentNullException(nameof(source)); + if (string.IsNullOrWhiteSpace(Source.Identity)) { - throw new ArgumentException("Resonite texture payload identity must be provided.", nameof(identity)); + throw new ArgumentException("Texture source identity must be non-empty.", nameof(source)); } - - ColorProfile = colorProfile; - Identity = identity; - Source = source ?? throw new ArgumentNullException(nameof(source)); } - public string? ColorProfile { get; } - - public string Identity { get; } - public ITextureImportSource Source { get; } } @@ -51,8 +41,8 @@ private RawRgba32ResoniteTexturePayload( int height, string? colorProfile, byte[] binaryPayload, - (string Identity, ITextureImportSource Source) source) - : base(colorProfile, source.Identity, source.Source) + ITextureImportSource source) + : base(source) { ArgumentNullException.ThrowIfNull(binaryPayload); Width = width; @@ -66,7 +56,7 @@ private RawRgba32ResoniteTexturePayload( public ImmutableArray BinaryPayload { get; } - private static (string Identity, ITextureImportSource Source) CreateSource( + private static ITextureImportSource CreateSource( int width, int height, string? colorProfile, @@ -75,14 +65,12 @@ private static (string Identity, ITextureImportSource Source) CreateSource( { ArgumentNullException.ThrowIfNull(binaryPayload); string effectiveIdentity = identity ?? Guid.NewGuid().ToString("N"); - return ( - effectiveIdentity, - TextureImportSourceFactory.CreateInMemoryRaw( - width, - height, - colorProfile, - binaryPayload, - effectiveIdentity)); + return TextureImportSourceFactory.CreateInMemoryRaw( + width, + height, + colorProfile, + binaryPayload, + effectiveIdentity); } } @@ -91,19 +79,8 @@ public sealed class EncodedImageResoniteTexturePayload : ResoniteTexturePayload public EncodedImageResoniteTexturePayload( int? width, int? height, - string? colorProfile, - ITextureImportSource source, - string? identity = null) - : this(width, height, colorProfile, CreateSource(source, identity)) - { - } - - private EncodedImageResoniteTexturePayload( - int? width, - int? height, - string? colorProfile, - (string Identity, ITextureImportSource Source) source) - : base(colorProfile, source.Identity, source.Source) + ITextureImportSource source) + : base(source) { Width = width; Height = height; @@ -112,12 +89,4 @@ private EncodedImageResoniteTexturePayload( public int? Width { get; } public int? Height { get; } - - private static (string Identity, ITextureImportSource Source) CreateSource( - ITextureImportSource source, - string? identity) - { - ArgumentNullException.ThrowIfNull(source); - return (identity ?? source.Identity, source); - } } diff --git a/src/PlateauResoniteLink/Targets/Resonite/SceneImportContractMapper.cs b/src/PlateauResoniteLink/Targets/Resonite/SceneImportContractMapper.cs index b2fd47b2..b6552450 100644 --- a/src/PlateauResoniteLink/Targets/Resonite/SceneImportContractMapper.cs +++ b/src/PlateauResoniteLink/Targets/Resonite/SceneImportContractMapper.cs @@ -138,15 +138,13 @@ private static ResoniteTexturePayload ToInternal(TexturePayload payload) RawRgba32TexturePayload raw => new RawRgba32ResoniteTexturePayload( raw.Width, raw.Height, - raw.ColorProfile, + raw.Source.ColorProfile, raw.BinaryPayload.AsSpan().ToArray(), - raw.Identity), + raw.Source.Identity), EncodedImageTexturePayload encoded => new EncodedImageResoniteTexturePayload( encoded.Width, encoded.Height, - encoded.ColorProfile, - encoded.Source, - encoded.Identity), + encoded.Source), _ => throw new ArgumentOutOfRangeException(nameof(payload), payload.GetType(), "Unsupported texture payload type."), }; } diff --git a/tests/PlateauResoniteLink.Tests/Application/TexturePayloadTests.cs b/tests/PlateauResoniteLink.Tests/Application/TexturePayloadTests.cs index 0fb7fe65..79d7d4ce 100644 --- a/tests/PlateauResoniteLink.Tests/Application/TexturePayloadTests.cs +++ b/tests/PlateauResoniteLink.Tests/Application/TexturePayloadTests.cs @@ -1,3 +1,5 @@ +using System; + using PlateauResoniteLink.Application.Importing; namespace PlateauResoniteLink.Tests.Application; @@ -14,4 +16,31 @@ public void ConstructorCopiesBinaryPayloadBytes() Assert.Equal([1, 2, 3, 4], payload.BinaryPayload); } + + [Fact] + public void ConstructorCarriesIdentityAndColorProfileOnSourceOnly() + { + RawRgba32TexturePayload payload = new(1, 1, "sRGB", [1, 2, 3, 4], "dataset:texture"); + + Assert.Equal("dataset:texture", payload.Source.Identity); + Assert.Equal("sRGB", payload.Source.ColorProfile); + } + + [Fact] + public void ConstructorRejectsTextureSourceWithoutIdentity() + { + Assert.Throws( + () => new EncodedImageTexturePayload(null, null, new BlankIdentityTextureImportSource())); + } + + private sealed class BlankIdentityTextureImportSource : ITextureImportSource + { + public string Identity => " "; + + public string Description => "blank"; + + public string? ColorProfile => null; + + public long? EstimatedByteLength => null; + } } diff --git a/tests/PlateauResoniteLink.Tests/Profiles/LocalCityGmlObjectProjectionTests.cs b/tests/PlateauResoniteLink.Tests/Profiles/LocalCityGmlObjectProjectionTests.cs index 47095100..490c7f66 100644 --- a/tests/PlateauResoniteLink.Tests/Profiles/LocalCityGmlObjectProjectionTests.cs +++ b/tests/PlateauResoniteLink.Tests/Profiles/LocalCityGmlObjectProjectionTests.cs @@ -2073,10 +2073,10 @@ public void ProjectCityObjectCullsBottomBandBuildingSurfacesBySemanticOrDownward materialResolver: new DefaultMaterialResolver(CommonMaterialCatalog.Create())); Assert.Equal(3, projected.Materials.Count); - Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Identity == "ground"); - Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Identity == "ground-reversed"); - Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Identity == "outer-floor"); - Assert.Contains(projected.Materials, static material => material.TexturePayload?.Identity == "high-outer-floor"); + Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Source.Identity == "ground"); + Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Source.Identity == "ground-reversed"); + Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Source.Identity == "outer-floor"); + Assert.Contains(projected.Materials, static material => material.TexturePayload?.Source.Identity == "high-outer-floor"); } [Fact] @@ -2113,7 +2113,7 @@ public void ProjectCityObjectKeepsNonBuildingDownwardHorizontalGroundSurface() materialResolver: new DefaultMaterialResolver(CommonMaterialCatalog.Create())); Assert.Single(projected.Materials); - Assert.Equal("tran-ground", projected.Materials[0].TexturePayload?.Identity); + Assert.Equal("tran-ground", projected.Materials[0].TexturePayload?.Source.Identity); Assert.NotEmpty(projected.Mesh.Vertices); } @@ -2171,10 +2171,10 @@ public void ProjectCityObjectCullsBuildingLod1UnknownBottomBandSurfaces() materialResolver: new DefaultMaterialResolver(CommonMaterialCatalog.Create())); Assert.Equal(2, projected.Materials.Count); - Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Identity == "lod1-bottom"); - Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Identity == "lod1-bottom-reversed"); - Assert.Contains(projected.Materials, static material => material.TexturePayload?.Identity == "lod1-roof"); - Assert.Contains(projected.Materials, static material => material.TexturePayload?.Identity == "lod1-wall"); + Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Source.Identity == "lod1-bottom"); + Assert.DoesNotContain(projected.Materials, static material => material.TexturePayload?.Source.Identity == "lod1-bottom-reversed"); + Assert.Contains(projected.Materials, static material => material.TexturePayload?.Source.Identity == "lod1-roof"); + Assert.Contains(projected.Materials, static material => material.TexturePayload?.Source.Identity == "lod1-wall"); } [Fact] @@ -2246,7 +2246,7 @@ public void ProjectCityObjectKeepsSingleDownwardHorizontalBuildingSurfaceWhenNoH materialResolver: new DefaultMaterialResolver(CommonMaterialCatalog.Create())); Assert.Single(projected.Materials); - Assert.Contains(projected.Materials, static material => material.TexturePayload?.Identity == "only-surface"); + Assert.Contains(projected.Materials, static material => material.TexturePayload?.Source.Identity == "only-surface"); } [Fact] @@ -2378,7 +2378,7 @@ await service.ExecuteAsync( Assert.NotNull(explicitMaterial.TexturePayload); Assert.Contains( "udx/dem/53394525/appearance/mixed_surface.png", - explicitMaterial.TexturePayload!.Identity, + explicitMaterial.TexturePayload!.Source.Identity, StringComparison.Ordinal); } diff --git a/tests/PlateauResoniteLink.Tests/Targets/NonDemCityObjectBakerTests.cs b/tests/PlateauResoniteLink.Tests/Targets/NonDemCityObjectBakerTests.cs index dc8ebe9b..b4d72ec7 100644 --- a/tests/PlateauResoniteLink.Tests/Targets/NonDemCityObjectBakerTests.cs +++ b/tests/PlateauResoniteLink.Tests/Targets/NonDemCityObjectBakerTests.cs @@ -118,7 +118,7 @@ await AssertBufferedAsync( ResoniteConstructionCityObject cityObject = Assert.Single(await baker.FlushAllAsync()); string?[] identities = cityObject.Materials - .Select(static material => material.TexturePayload?.Identity) + .Select(static material => material.TexturePayload?.Source.Identity) .ToArray(); Assert.Collection( identities, @@ -147,7 +147,7 @@ await AssertBufferedAsync( Assert.Equal(CommonMaterialCatalog.Create().Generic.Uv, material.CommonMaterial); Assert.NotSame(payload, material.TexturePayload); Assert.NotNull(material.TexturePayload); - Assert.Contains("atlastex-", material.TexturePayload.Identity, StringComparison.Ordinal); + Assert.Contains("atlastex-", material.TexturePayload.Source.Identity, StringComparison.Ordinal); Assert.IsType(material.TexturePayload); Assert.Null(material.TextureScale); Assert.Null(material.TextureOffset); @@ -189,7 +189,7 @@ await AssertBufferedAsync( double averageX = submesh.TriangleVertexIndices .Select(index => cityObject.Mesh.Vertices[index].Position.X) .Average(); - averageXByPayloadIdentity.Add(material.TexturePayload?.Identity ?? string.Empty, averageX); + averageXByPayloadIdentity.Add(material.TexturePayload?.Source.Identity ?? string.Empty, averageX); } Assert.True(averageXByPayloadIdentity["textures/left.png"] < averageXByPayloadIdentity["textures/right.png"]); @@ -585,7 +585,7 @@ public async Task FlushAllAsyncFallsBackToOriginalCityObjectWhenSingleCandidateC Assert.Equal(oversizedCandidate.DisplayName, cityObject.DisplayName); Assert.Equal(oversizedCandidate.Materials.Count, cityObject.Materials.Count); Assert.All(cityObject.Materials, static material => Assert.NotNull(material.TexturePayload)); - Assert.DoesNotContain(cityObject.Materials, static material => material.TexturePayload?.Identity?.Contains("generated/lod2-atlas/", StringComparison.Ordinal) == true); + Assert.DoesNotContain(cityObject.Materials, static material => material.TexturePayload?.Source.Identity?.Contains("generated/lod2-atlas/", StringComparison.Ordinal) == true); } [Fact] @@ -610,7 +610,7 @@ public async Task FlushAllAsyncFallsBackToNormalizedCityObjectWhenSingleCandidat Assert.Equal(new ResoniteFloat2(0.25, 0.75), cityObject.Mesh.Vertices[0].UV0); Assert.Equal(new ResoniteFloat2(2.25, 0.75), cityObject.Mesh.Vertices[1].UV0); Assert.Equal(new ResoniteFloat2(0.25, 1.25), cityObject.Mesh.Vertices[2].UV0); - Assert.DoesNotContain(cityObject.Materials, static candidate => candidate.TexturePayload?.Identity?.Contains("generated/lod2-atlas/", StringComparison.Ordinal) == true); + Assert.DoesNotContain(cityObject.Materials, static candidate => candidate.TexturePayload?.Source.Identity?.Contains("generated/lod2-atlas/", StringComparison.Ordinal) == true); } [Fact] @@ -786,7 +786,7 @@ public async Task FlushAllAsyncFallsBackWhenSingleCandidateNeedsNonPowerOfTwoEdg ResoniteConstructionCityObject cityObject = Assert.Single(await baker.FlushAllAsync()); Assert.Equal(oversizedCandidate.SlotKey, cityObject.SlotKey); - Assert.DoesNotContain(cityObject.Materials, static material => material.TexturePayload?.Identity?.Contains("generated/lod2-atlas/", StringComparison.Ordinal) == true); + Assert.DoesNotContain(cityObject.Materials, static material => material.TexturePayload?.Source.Identity?.Contains("generated/lod2-atlas/", StringComparison.Ordinal) == true); } [Fact] @@ -1050,7 +1050,7 @@ await AssertBufferedAsync( Assert.Equal("atlasbake-unit-a-bldg-lod2-3", cityObject.SlotKey); Assert.Equal( "atlastex-unit-a.gml-3", - Assert.IsType(Assert.Single(cityObject.Materials).TexturePayload).Identity); + Assert.IsType(Assert.Single(cityObject.Materials).TexturePayload).Source.Identity); } private static NonDemCityObjectBaker CreateBaker( diff --git a/tests/PlateauResoniteLink.Tests/Targets/ResoniteLiveSceneImportTargetTestSupport.cs b/tests/PlateauResoniteLink.Tests/Targets/ResoniteLiveSceneImportTargetTestSupport.cs index bd23d64d..86e1dc8b 100644 --- a/tests/PlateauResoniteLink.Tests/Targets/ResoniteLiveSceneImportTargetTestSupport.cs +++ b/tests/PlateauResoniteLink.Tests/Targets/ResoniteLiveSceneImportTargetTestSupport.cs @@ -546,15 +546,13 @@ private static TexturePayload ToContractTexturePayload(ResoniteTexturePayload pa RawRgba32ResoniteTexturePayload raw => new RawRgba32TexturePayload( raw.Width, raw.Height, - raw.ColorProfile, + raw.Source.ColorProfile, raw.BinaryPayload.AsSpan().ToArray(), - raw.Identity), + raw.Source.Identity), EncodedImageResoniteTexturePayload encoded => new EncodedImageTexturePayload( encoded.Width, encoded.Height, - encoded.ColorProfile, - encoded.Source, - encoded.Identity), + encoded.Source), _ => throw new ArgumentOutOfRangeException(nameof(payload), payload.GetType(), "Unsupported texture payload type."), }; diff --git a/tests/PlateauResoniteLink.Tests/Targets/ResoniteTexturePayloadTests.cs b/tests/PlateauResoniteLink.Tests/Targets/ResoniteTexturePayloadTests.cs index 86b8aac0..2f520535 100644 --- a/tests/PlateauResoniteLink.Tests/Targets/ResoniteTexturePayloadTests.cs +++ b/tests/PlateauResoniteLink.Tests/Targets/ResoniteTexturePayloadTests.cs @@ -1,3 +1,6 @@ +using System; + +using PlateauResoniteLink.Application.Importing; using PlateauResoniteLink.Targets.Resonite; namespace PlateauResoniteLink.Tests.Targets; @@ -14,4 +17,31 @@ public void ConstructorCopiesBinaryPayloadBytes() Assert.Equal([4, 3, 2, 1], payload.BinaryPayload); } + + [Fact] + public void ConstructorCarriesIdentityAndColorProfileOnSourceOnly() + { + RawRgba32ResoniteTexturePayload payload = new(1, 1, "sRGB", [4, 3, 2, 1], "dataset:texture"); + + Assert.Equal("dataset:texture", payload.Source.Identity); + Assert.Equal("sRGB", payload.Source.ColorProfile); + } + + [Fact] + public void ConstructorRejectsTextureSourceWithoutIdentity() + { + Assert.Throws( + () => new EncodedImageResoniteTexturePayload(null, null, new BlankIdentityTextureImportSource())); + } + + private sealed class BlankIdentityTextureImportSource : ITextureImportSource + { + public string Identity => " "; + + public string Description => "blank"; + + public string? ColorProfile => null; + + public long? EstimatedByteLength => null; + } } diff --git a/tests/PlateauResoniteLink.Tests/Targets/SceneImportContractMapperTests.cs b/tests/PlateauResoniteLink.Tests/Targets/SceneImportContractMapperTests.cs index b79d6ac2..4dbe41a0 100644 --- a/tests/PlateauResoniteLink.Tests/Targets/SceneImportContractMapperTests.cs +++ b/tests/PlateauResoniteLink.Tests/Targets/SceneImportContractMapperTests.cs @@ -33,7 +33,7 @@ public void ToInternalMaterialBindingsPreservesNeutralContractFields() Assert.Equal(0.1, mapped.BaseColor.R, 9); Assert.Equal(0.2, mapped.BaseColor.G, 9); EncodedImageResoniteTexturePayload encodedPayload = Assert.IsType(mapped.TexturePayload); - Assert.Equal("dataset:texture", encodedPayload.Identity); + Assert.Equal("dataset:texture", encodedPayload.Source.Identity); Assert.Equal(-1.5, mapped.DepthOffset!.Factor, 9); Assert.Equal(2.5, mapped.DepthOffset.Units, 9); Assert.Equal(0.25, mapped.TextureScale!.X, 9); @@ -67,7 +67,7 @@ public void ToInternalMaterialBindingsMapsRawTextureDimensionsWithoutNullableGua RawRgba32ResoniteTexturePayload rawPayload = Assert.IsType(mapped.TexturePayload); Assert.Equal(2, rawPayload.Width); Assert.Equal(1, rawPayload.Height); - Assert.Equal("raw:texture", rawPayload.Identity); + Assert.Equal("raw:texture", rawPayload.Source.Identity); Assert.Equal([1, 2, 3, 4, 5, 6, 7, 8], rawPayload.BinaryPayload); } @@ -174,7 +174,6 @@ private static TexturePayload CreateEncodedTexturePayload() return new EncodedImageTexturePayload( width: 2, height: 2, - colorProfile: "sRGB", source: TextureImportSourceFactory.CreateInMemoryEncodedImage( colorProfile: "sRGB", bytes: [1, 2, 3, 4],