Commit Graph

3 Commits

Author SHA1 Message Date
Marc Schier c84c168585 Merge OpenUSD scene packages (#4384)
# Description

Merge the draft OpenUSD Scene Materialization projects into the existing
OpenUSD core and server
packages:

- move the scene companion model, document/value model, and `.usda`
conversion APIs into
  `Opc.Ua.OpenUsd`;
- move discovery, materialization, export, and Part 1 interop into
`Opc.Ua.OpenUsd.Server`;
- rename the CLR APIs to `Opc.Ua.OpenUsd.Scene`,
`Opc.Ua.OpenUsd.Scene.Conversion`, and
  `Opc.Ua.OpenUsd.Server.Scene`;
- remove the `Opc.Ua.OpenUsdScene` and `Opc.Ua.OpenUsdScene.Server`
projects and NuGet package IDs;
- update the connector, the consolidated OpenUSD test project, package
READMEs, and developer docs.

This is an intentional source- and binary-breaking cleanup for the draft
Scene packages. The OPC UA
Scene model URI, version, NodeIds, and wire behavior remain unchanged.

## Related Issues

- Fixes #4383

## Validation

- `UA.slnx` Release builds completed for `CustomTestTarget=net10.0` and
`CustomTestTarget=net48`.
- OpenUSD tests passed on net10.0 (915) and net48 (879), excluding the
existing host-culture
assertion; the isolated assertion passed under invariant globalization.
- The generated Release pack contains the expected 75 packages, emits
neither retired Scene
package, and passes
`.azurepipelines\validate-source-generator-packages.ps1`.
- The two surviving packages expose the new nested namespaces and
contain no retired package
  dependencies.

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-02 12:06:52 +00:00
Marc Schier 0f257c05f0 OpenUSD scene: replace object in the USD value model with a UsdValue union (#4160)
Fixes #4151.

## The problem

```csharp
public object? Value { get; set; }
public SortedList<double, object?> TimeSamples { get; }
public IDictionary<string, object?> Metadata { get; }
```

`object` in public API, and every consumer forced into type tests and
casts. It was also why `UsdSceneExporter` had to call
`Variant.AsBoxedObject(Variant.BoxingBehavior.Legacy)`, itself against
guidance.

`Variant` cannot stand in for it. The USD value model is recursive and
**ragged**: tuples (`float3`), arrays, *arrays of tuples* (`color3f[]`),
matrices authored as a tuple of row tuples, plus asset paths and prim
path references that must round-trip as their own syntax.
`Variant`/`MatrixOf<T>` cover rectangular scalar arrays but not the
nested "array of tuple" shape.

## What this does

Introduces `UsdValue`, a readonly struct scoping a value to the shapes a
`.usda` document can express, and converts the model to it end to end -
reader, writer, coercion and signature all speak `UsdValue`, so there is
one representation rather than conversions at the edges.

- `INullable`, so absence is `UsdValue.Null` / `.IsNull` - never
`UsdValue?`.
- `TryGet*` accessors only. No boxing accessor, nothing returning
`object`.
- `ArrayOf<UsdValue>` for the recursive tuple/array/matrix cases.
- `Tuple` and `Array` stay distinct kinds because USD prints `(1, 2, 3)`
and `[1, 2, 3]` differently, as do `Token` and `String`.
- `Decoerce` now takes the `Variant` directly and reads it through its
typed accessors, which is what **removes the `AsBoxedObject` call**.

**The attribute's `TypeName` stays authoritative for rendering**, so the
kinds add type safety without changing a single byte of emitted `.usda`.
That is what makes this provably lossless: the existing fidelity and
round-trip suites pass **with their expectations untouched**.

## Three defects the tests caught

Worth calling out, because each would have shipped silently:

1. **Georeference dual authoring** read values through an `object`-typed
`TryToDouble`, which stopped seeing CLR primitives once values became
`UsdValue`.
2. **A `double3` scalar was emitted as `[1.0, 2.0, 3.0]`** instead of
`(1.0, 2.0, 3.0)`, because the writer picked brackets from the value's
kind. The shape is decided by the TypeName, not the container - so
`UsdVal` now renders a composite as a parenthesised tuple unless the
type is an array type. This is the same contract the H-1 regression
guard documents.
3. **`UsdValue` did not load at all on .NET Framework.** Storing
components as `ArrayOf<UsdValue>` made the struct layout recursive
(`UsdValue` -> `ArrayOf<UsdValue>` -> `ReadOnlyMemory<UsdValue>`);
CoreCLR resolves that, the net48 type loader throws `TypeLoadException`
on every use. Only the net48 run surfaced it - net10.0 was entirely
green. The payload is now a plain array, a reference type, which breaks
the cycle while the public accessors still hand out `ArrayOf<UsdValue>`.

## Review feedback addressed

- `UsdValue.GetHashCode` folds the dictionary entries (an
order-independent sum of key/value hashes) instead of only the entry
count, so same-size dictionaries no longer all collide.
- `UsdValue.ToString` renders a `Dictionary` as its entries ordered by
key instead of falling through to the empty-string default, which the
metadata fallback paths rely on.
- A `uint64` above `long.MaxValue` is no longer cast to `long` (which
wrapped `ulong.MaxValue` into `-1`) - it is preserved as its invariant
decimal text for scalars, arrays and matrices alike. `TryAsUInt64` reads
that form back, and `UsdaReader` now parses an over-long integral
literal as a token holding its exact digits instead of throwing
`OverflowException`, so the value survives the whole round trip.

## Validation

- Full `dotnet build UA.slnx -m:1 /p:UseSharedCompilation=false`: **0
errors, 0 warnings**.
- `Opc.Ua.OpenUsd.Tests`: **772 passing on net10.0 and on net48** (632
existing + 140 new tests), with 99.0% patch coverage over the changed
source lines.
- `Opc.Ua.Di.Tests` 364, `Opc.Ua.Robotics.Tests` 103 - unchanged.
- No `object`/`object?` left in the `Opc.Ua.OpenUsdScene` public API
except the `Equals(object?)` override; no `AsBoxedObject`/`AsBoxedValue`
anywhere in the OpenUSD assemblies.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
Copilot-Session: d089bd1c-b795-4f2b-a872-eb3090c11536
2026-08-03 18:46:42 +00:00
Marc Schier fb160a8f39 OpenUSD: the OPC UA to OpenUSD binding, scene materialization and connector (#4126)
**Stack 4/5** — base #4125. Part of the #4096 split; see #4096 for the
overall picture.

> Review after #4123, #4124 and #4125.

## What this is

The generic OPC UA to OpenUSD binding, scene materialization, and the
connector that drives a live stage from a running server.

## Contents

**Binding (Part 1)** — `Opc.Ua.OpenUsd` and `Opc.Ua.OpenUsd.Server`. A
server describes how its Variables drive USD prims, and serves the USD
layers it refers to as Part 5 files with a content digest.

**Scene materialization (Part 2)** — `Opc.Ua.OpenUsdScene` and
`Opc.Ua.OpenUsdScene.Server`, including the USDA writer.
- **Escaping fix**: string values, composition-arc references, variant
set names and selections, `doc` and prim names were all emitted
unescaped, so a value carrying a quote or a newline could author its own
layer syntax. Every emit site now goes through the escapers. The tests
assert the right property — not that the payload is absent (it
legitimately survives inside the escaped literal) but that no payload
becomes its own line of layer syntax.

**Client** — `Opc.Ua.OpenUsd.Client` streams live values into an
`IUsdSink`. `CompositeUsdSink` fans out to several sinks; the file sink
interpolates between server samples so motion is continuous rather than
stepped at the publishing interval.

**Connector** — `tools/Opc.Ua.OpenUsd.Connector` composes a stage from a
live server. The optional in-process viewport lives in a separate
assembly probed at run time, so the connector still runs — and says why
— when it is absent.
- **Fix**: the asset cache and PKI root moved off the shared temp
directory to `LocalApplicationData`.

**Sample** — `PumpDeviceIntegrationServer` gains an OpenUSD
representation of the pump.

## Notes for the reviewer

- The viewport assembly is net10.0-only. It is what exercises the
`Directory.Build.targets` change from 1/5: without it, the `net8.0` leg
of the per-TFM CI build fails with `CS0433` because the
`System.Threading.Lock` polyfill collides with the real type.
- `UsdAssetDelivery` hashes through `SHA256.Create` rather than the
static `HashData`, which does not exist on the net472/net48 targets this
test project also builds for.

## Validation

`dotnet build UA.slnx` — 0 errors, 0 warnings.

| Suite | Result |
| --- | --- |
| Opc.Ua.OpenUsd.Tests | 621 / 0 |
| Opc.Ua.Di.Tests | 321 / 0 |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
2026-08-01 07:55:58 +02:00