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
91 changes: 91 additions & 0 deletions Quantum.PreviewViewer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,97 @@ public static bool IsPrimaryModelExtension(string extension)
}
}

public static class PreviewTrainStyleRoles
{
public const string TrainLeadRole = "train.lead";
public const string TrainMiddleRole = "train.middle";
public const string TrainRearRole = "train.rear";
public const string TrainBodyRole = "train.body";
public const string TrainBodyBankingProfileRole = "train.body.banking-profile";
public const string TrainWildcardRole = "train.*";

public static bool IsTrainBodyRole(string? snapshotRole)
{
return string.Equals(snapshotRole, TrainBodyRole, StringComparison.Ordinal) ||
string.Equals(snapshotRole, TrainBodyBankingProfileRole, StringComparison.Ordinal);
}

public static string ResolveTrainBodyStyleRole(int carIndex, int carCount)
{
if (carCount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(carCount), "Train car count must be positive.");
}

if (carIndex < 0 || carIndex >= carCount)
{
throw new ArgumentOutOfRangeException(nameof(carIndex), "Train car index must be inside the train.");
}

if (carIndex == 0)
{
return TrainLeadRole;
}

return carIndex == carCount - 1
? TrainRearRole
: TrainMiddleRole;
}

public static string? ResolveConfiguredStyleRole(
string? snapshotRole,
int carIndex,
int carCount,
IReadOnlyCollection<string> configuredRoles)
{
if (string.IsNullOrWhiteSpace(snapshotRole))
{
return null;
}

if (IsTrainBodyRole(snapshotRole))
{
string variantRole = ResolveTrainBodyStyleRole(carIndex, carCount);
if (ContainsRole(configuredRoles, variantRole))
{
return variantRole;
}

if (ContainsRole(configuredRoles, TrainBodyRole))
{
return TrainBodyRole;
}

if (ContainsRole(configuredRoles, snapshotRole))
{
return snapshotRole;
}
}
else if (ContainsRole(configuredRoles, snapshotRole))
{
return snapshotRole;
}

return snapshotRole.StartsWith("train.", StringComparison.Ordinal) &&
ContainsRole(configuredRoles, TrainWildcardRole)
? TrainWildcardRole
: null;
}

private static bool ContainsRole(IReadOnlyCollection<string> configuredRoles, string role)
{
foreach (string configuredRole in configuredRoles)
{
if (string.Equals(configuredRole, role, StringComparison.Ordinal))
{
return true;
}
}

return false;
}
}

public static class SnapshotCatalog
{
public const string ContractName = "quantum.debug_viewport_snapshot";
Expand Down
25 changes: 23 additions & 2 deletions Quantum.PreviewViewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Current capabilities:
- Renders frame axes from `frames`.
- Renders `lines` as colored diagnostics.
- Renders oriented train/debug boxes from `boxes`.
- Can replace configured train box roles with GLTF/GLB assets while preserving debug boxes as the fallback.
- Can replace configured train box roles with GLTF/GLB assets while preserving debug boxes as the fallback and optional overlay.
- Scrubs and plays a visual lead distance through the exported sampled frames with orbit and follow-train camera modes.
- Repositions train placeholder boxes by preserving their exported spacing offsets.
- Supports orbit, pan, zoom, camera reset, and optional follow camera.
Expand All @@ -43,9 +43,28 @@ Style manifest shape:
{
"id": "custom-train",
"name": "Custom train",
"debugBoxOverlay": false,
"roles": {
"train.lead": {
"asset": "assets/trains/custom-lead.glb",
"fitToBox": true,
"fitMode": "uniform",
"scale": 1.0,
"offset": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotationDegrees": { "x": 0.0, "y": 90.0, "z": 0.0 }
},
"train.middle": {
"asset": "assets/trains/custom-middle.glb",
"fitToBox": true,
"fitMode": "uniform"
},
"train.rear": {
"asset": "assets/trains/custom-rear.glb",
"fitToBox": true,
"fitMode": "uniform"
},
"train.body": {
"asset": "assets/trains/custom-car.glb",
"asset": "assets/trains/custom-fallback-car.glb",
"fitToBox": true,
"fitMode": "uniform",
"scale": 1.0,
Expand All @@ -59,6 +78,8 @@ Style manifest shape:
}
```

Train body boxes are still exported as `train.body` by `DebugViewportSnapshotV1`. The viewer resolves the visual asset role per car: first car uses `train.lead`, last car uses `train.rear`, interior cars use `train.middle`, and any missing variant falls back to `train.body`. A one-car train uses `train.lead`. See [preview-viewer-train-styles.md](../docs/visualization/preview-viewer-train-styles.md) for configuration examples.

Asset paths are resolved under the configured preview asset root, which defaults to the manifest directory. `PreviewStyleManifest` and `PreviewStyleAssetRoot` can be set through app configuration if the manifest or assets need to live elsewhere in the repository.

Limitations:
Expand Down
Loading
Loading