Skip to content
Merged
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
28 changes: 13 additions & 15 deletions src/Opc.Ua.OpenUsdScene.Server/UsdSceneExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,10 @@ private static UsdAttribute ExportAttribute(
// Infer liveness from the write access, so a Mode A attribute that is not
// historized still exports as live.
Live = (attributeNode.AccessLevel & Opc.Ua.AccessLevels.CurrentWrite) != 0,
// BoxingBehavior.Legacy unwraps an ArrayOf<T>/MatrixOf<T> to the T[]/T[,]
// shapes Decoerce consumes, so array, tuple and matrix values survive the
// round trip instead of being stringified as an opaque struct (§7.2).
Value = UsdValueCoercion.Decoerce(
attributeNode.Value.AsBoxedObject(Variant.BoxingBehavior.Legacy))
// Decoerce reads the Variant through its typed accessors, so an ArrayOf<T> or
// MatrixOf<T> keeps its shape and array, tuple and matrix values survive the
// round trip without a boxing accessor (§7.2).
Value = UsdValueCoercion.Decoerce(attributeNode.Value)
};

// §7.2: time samples are held by the materialization result, not the node, so recover
Expand Down Expand Up @@ -411,14 +410,14 @@ private static void ExportMetadata(

/// <summary>
/// Recovers a <c>Metadata/</c> folder into a metadata dictionary (§6.3), the inverse of the
/// materializer's typed authoring. A leaf Property is read back in its own type — Legacy
/// boxing unwraps an <c>ArrayOf&lt;T&gt;</c>/<c>MatrixOf&lt;T&gt;</c> and <c>Decoerce</c>
/// reverses the §6.2 coercion, so a value round-trips as itself rather than an opaque box
/// (the same fix as the attribute-value path). A nested Metadata folder is recovered as a
/// nested dictionary, so structured <c>customData</c> keeps its authored nesting to any depth.
/// materializer's typed authoring. A leaf Property is read back in its own type through
/// <c>Decoerce</c>, which reverses the §6.2 coercion off the Variant's typed accessors, so
/// a value round-trips as itself rather than an opaque box. A nested Metadata folder is
/// recovered as a nested dictionary, so structured <c>customData</c> keeps its authored
/// nesting to any depth.
/// </summary>
private static void ReadMetadataFolder(
ISystemContext context, NodeState folder, IDictionary<string, object?> into)
ISystemContext context, NodeState folder, IDictionary<string, UsdValue> into)
{
var children = new List<BaseInstanceState>();
folder.GetChildren(context, children);
Expand All @@ -432,13 +431,12 @@ private static void ReadMetadataFolder(
switch (child)
{
case BaseVariableState property:
into[key] = UsdValueCoercion.Decoerce(
property.Value.AsBoxedObject(Variant.BoxingBehavior.Legacy));
into[key] = UsdValueCoercion.Decoerce(property.Value);
break;
case FolderState subFolder:
var nested = new Dictionary<string, object?>(StringComparer.Ordinal);
var nested = new Dictionary<string, UsdValue>(StringComparer.Ordinal);
ReadMetadataFolder(context, subFolder, nested);
into[key] = nested;
into[key] = UsdValue.FromDictionary(nested);
break;
}
}
Expand Down
197 changes: 82 additions & 115 deletions src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.Properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,18 +345,18 @@ private static void MaterializeMetadata(
private static void MaterializeMetadataEntries(
ISystemContext context,
NodeState folder,
IEnumerable<KeyValuePair<string, object?>> entries,
IEnumerable<KeyValuePair<string, UsdValue>> entries,
ushort ns)
{
foreach (KeyValuePair<string, object?> entry in entries)
foreach (KeyValuePair<string, UsdValue> entry in entries)
{
if (string.IsNullOrEmpty(entry.Key))
{
continue;
}

if (TryAsNestedDictionary(
entry.Value, out IEnumerable<KeyValuePair<string, object?>> nested))
if (entry.Value.TryGetDictionary(
out IReadOnlyDictionary<string, UsdValue> nested))
{
var subFolder = new FolderState(folder)
{
Expand All @@ -380,13 +380,13 @@ private static void MaterializeMetadataEntries(
DataType = Opc.Ua.DataTypeIds.BaseDataType,
ValueRank = ValueRanks.Scalar
};
if (entry.Value != null)
if (!entry.Value.IsNull)
{
if (TryCoerceMetadataValue(
entry.Value, out Variant variant, out NodeId dataType, out int valueRank))
{
// A recognised value keeps its type through the round trip, so the
// exporter recovers the authored object rather than an opaque string (§6.3).
// exporter recovers the authored value rather than an opaque string (§6.3).
property.Value = variant;
property.DataType = dataType;
property.ValueRank = valueRank;
Expand All @@ -395,10 +395,7 @@ private static void MaterializeMetadataEntries(
{
// A value of no representable type is carried as its invariant textual
// form rather than dropped — the §6.3 last resort, not a typed guess.
property.Value = Variant.From(
entry.Value as string ??
Convert.ToString(entry.Value, CultureInfo.InvariantCulture) ??
string.Empty);
property.Value = Variant.From(entry.Value.ToString());
property.DataType = Opc.Ua.DataTypeIds.String;
}
}
Expand All @@ -408,52 +405,45 @@ entry.Value as string ??
}

/// <summary>
/// Whether a metadata value is a nested dictionary (structured <c>customData</c>), and if so
/// its entries. Only string-keyed dictionaries qualify; anything else is a leaf value.
/// </summary>
private static bool TryAsNestedDictionary(
object? value, out IEnumerable<KeyValuePair<string, object?>> entries)
{
switch (value)
{
case IReadOnlyDictionary<string, object?> readOnly:
entries = readOnly;
return true;
case IDictionary<string, object?> readWrite:
entries = readWrite;
return true;
default:
entries = Array.Empty<KeyValuePair<string, object?>>();
return false;
}
}

/// <summary>
/// Chooses a Variant, DataType and ValueRank for a leaf metadata value from its CLR type,
/// Chooses a Variant, DataType and ValueRank for a leaf metadata value from its kind,
/// so a scalar keeps its exact type and an array keeps its (inferred) element type through
/// the materialize→export round trip (§6.3). Returns <c>false</c> for a scalar whose type is
/// the materialize→export round trip (§6.3). Returns <c>false</c> for a value whose kind is
/// not representable, so the caller carries its textual form instead of guessing.
/// </summary>
private static bool TryCoerceMetadataValue(
object value, out Variant variant, out NodeId dataType, out int valueRank)
UsdValue value, out Variant variant, out NodeId dataType, out int valueRank)
{
valueRank = ValueRanks.Scalar;
switch (value)
switch (value.Kind)
{
case bool v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Boolean; return true;
case sbyte v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.SByte; return true;
case byte v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Byte; return true;
case short v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Int16; return true;
case ushort v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.UInt16; return true;
case int v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Int32; return true;
case uint v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.UInt32; return true;
case long v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Int64; return true;
case ulong v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.UInt64; return true;
case float v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Float; return true;
case double v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Double; return true;
case string v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.String; return true;
case System.Collections.IEnumerable sequence:
return TryCoerceMetadataArray(sequence, out variant, out dataType, out valueRank);
case UsdValueKind.Boolean:
value.TryGetBoolean(out bool b);
variant = Variant.From(b);
dataType = Opc.Ua.DataTypeIds.Boolean;
return true;
case UsdValueKind.Integer:
value.TryGetInteger(out long l);
variant = Variant.From(l);
dataType = Opc.Ua.DataTypeIds.Int64;
return true;
case UsdValueKind.Double:
value.TryGetDouble(out double d);
variant = Variant.From(d);
dataType = Opc.Ua.DataTypeIds.Double;
return true;
case UsdValueKind.String:
case UsdValueKind.Token:
case UsdValueKind.AssetPath:
case UsdValueKind.PathReference:
value.TryGetText(out string s);
variant = Variant.From(s);
dataType = Opc.Ua.DataTypeIds.String;
return true;
case UsdValueKind.Tuple:
case UsdValueKind.Array:
case UsdValueKind.Matrix:
value.TryGetItems(out ArrayOf<UsdValue> items);
return TryCoerceMetadataArray(items, out variant, out dataType, out valueRank);
default:
variant = default;
dataType = Opc.Ua.DataTypeIds.String;
Expand All @@ -462,91 +452,78 @@ private static bool TryCoerceMetadataValue(
}

/// <summary>
/// Coerces a metadata array or list into a typed 1-D Variant, inferring the element type
/// from the first non-null element. A homogeneous numeric or boolean array keeps its
/// Coerces a metadata array or tuple into a typed 1-D Variant, inferring the element type
/// from the first non-absent element. A homogeneous numeric or boolean sequence keeps its
/// element type; an empty or mixed sequence is carried as a string array so it round-trips
/// as a sequence rather than being dropped (§6.3, fail closed — no numeric guess).
/// </summary>
private static bool TryCoerceMetadataArray(
System.Collections.IEnumerable sequence,
ArrayOf<UsdValue> sequence,
out Variant variant,
out NodeId dataType,
out int valueRank)
{
valueRank = ValueRanks.OneDimension;
var items = new List<object?>();
foreach (object? item in sequence)
UsdValue[] items = sequence.ToArray() ?? [];
UsdValueKind first = UsdValueKind.Null;
for (int ii = 0; ii < items.Length; ii++)
{
items.Add(item);
}
object? first = null;
foreach (object? item in items)
{
if (item != null)
if (!items[ii].IsNull)
{
first = item;
first = items[ii].Kind;
break;
}
}
switch (first)
{
case bool _ when TryFillArray(items, v => Convert.ToBoolean(v, CultureInfo.InvariantCulture), out bool[] values):
variant = Variant.From((ArrayOf<bool>)values);
case UsdValueKind.Boolean
when TryFillArray(items, static (UsdValue v, out bool r) => v.TryGetBoolean(out r), out bool[] bools):
variant = Variant.From((ArrayOf<bool>)bools);
dataType = Opc.Ua.DataTypeIds.Boolean;
return true;
case sbyte _ or short _ or int _ when TryFillArray(items, v => Convert.ToInt32(v, CultureInfo.InvariantCulture), out int[] values):
variant = Variant.From((ArrayOf<int>)values);
dataType = Opc.Ua.DataTypeIds.Int32;
return true;
case long _ or uint _ when TryFillArray(items, v => Convert.ToInt64(v, CultureInfo.InvariantCulture), out long[] values):
variant = Variant.From((ArrayOf<long>)values);
case UsdValueKind.Integer
when TryFillArray(items, static (UsdValue v, out long r) => v.TryGetInteger(out r), out long[] longs):
variant = Variant.From((ArrayOf<long>)longs);
dataType = Opc.Ua.DataTypeIds.Int64;
return true;
case float _ or double _ when TryFillArray(items, v => Convert.ToDouble(v, CultureInfo.InvariantCulture), out double[] values):
variant = Variant.From((ArrayOf<double>)values);
case UsdValueKind.Double
when TryFillArray(items, static (UsdValue v, out double r) => v.TryGetNumber(out r), out double[] doubles):
variant = Variant.From((ArrayOf<double>)doubles);
dataType = Opc.Ua.DataTypeIds.Double;
return true;
default:
var strings = new string[items.Count];
for (int i = 0; i < items.Count; i++)
var strings = new string[items.Length];
for (int i = 0; i < items.Length; i++)
{
strings[i] = items[i] as string ??
Convert.ToString(items[i], CultureInfo.InvariantCulture) ??
string.Empty;
strings[i] = items[i].TryGetText(out string text)
? text
: items[i].ToString();
}
variant = Variant.From((ArrayOf<string>)strings);
dataType = Opc.Ua.DataTypeIds.String;
return true;
}
}

private delegate bool UsdValueReader<T>(UsdValue value, out T result);

/// <summary>
/// Fills a typed array by converting every element with <paramref name="convert"/>, failing
/// closed if any element cannot be converted so a heterogeneous array falls back to text.
/// Fills a typed array by reading every element with <paramref name="read"/>, failing
/// closed if any element cannot be read so a heterogeneous array falls back to text.
/// </summary>
/// <typeparam name="T">The element type of the array being filled.</typeparam>
private static bool TryFillArray<T>(
List<object?> items, Func<object, T> convert, out T[] result)
UsdValue[] items, UsdValueReader<T> read, out T[] result)
{
var array = new T[items.Count];
for (int i = 0; i < items.Count; i++)
var array = new T[items.Length];
for (int i = 0; i < items.Length; i++)
{
object? item = items[i];
if (item == null)
{
result = Array.Empty<T>();
return false;
}
try
{
array[i] = convert(item);
}
catch (Exception exception) when (
exception is InvalidCastException or FormatException or OverflowException)
if (!read(items[i], out T converted))
{
result = Array.Empty<T>();
return false;
}
array[i] = converted;
}
result = array;
return true;
Expand Down Expand Up @@ -700,30 +677,20 @@ private static bool TryReadAnchor(
return haveLatitude && haveLongitude && haveHeight;
}

private static bool TryToDouble(object? value, out double result)
private static bool TryToDouble(UsdValue value, out double result)
{
switch (value)
if (value.TryGetNumber(out result))
{
case double d:
result = d;
return true;
case float f:
result = f;
return true;
case long l:
result = l;
return true;
case int i:
result = i;
return true;
case string s when double.TryParse(
s, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed):
result = parsed;
return true;
default:
result = 0.0;
return false;
return true;
}
if (value.TryGetText(out string text) && double.TryParse(
text, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed))
{
result = parsed;
return true;
}
result = 0.0;
return false;
}

private static void Attach(
Expand Down Expand Up @@ -788,7 +755,7 @@ public UsdMaterializationRecorder(DateTime? epochUtc, double? timeCodesPerSecond
public void Record(UsdAttributeState node, UsdAttribute attribute)
{
var samples = new List<UsdTimeSample>(attribute.TimeSamples.Count);
foreach (KeyValuePair<double, object?> sample in attribute.TimeSamples)
foreach (KeyValuePair<double, UsdValue> sample in attribute.TimeSamples)
{
samples.Add(new UsdTimeSample(sample.Key, sample.Value));
}
Expand Down
6 changes: 3 additions & 3 deletions src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ public sealed class UsdMaterializationOptions
/// Creates a time sample.
/// </summary>
/// <param name="timeCode">The stage-timeline time code.</param>
/// <param name="value">The sampled value, in the same object shapes the reader produces.</param>
public UsdTimeSample(double timeCode, object? value)
/// <param name="value">The sampled value, in the same shape the reader produces.</param>
public UsdTimeSample(double timeCode, UsdValue value)
{
TimeCode = timeCode;
Value = value;
Expand All @@ -111,7 +111,7 @@ public UsdTimeSample(double timeCode, object? value)
/// <summary>
/// The sampled value.
/// </summary>
public object? Value { get; }
public UsdValue Value { get; }

/// <inheritdoc/>
public bool Equals(UsdTimeSample other)
Expand Down
Loading
Loading