From 956c2be4c56d8d232e7f80d38ac68e586fc95594 Mon Sep 17 00:00:00 2001 From: Coasterpete Design Date: Thu, 18 Jun 2026 16:22:17 -0400 Subject: [PATCH] Milestone 147: add layout package v2 dto schema skeleton Adds the TrackLayoutPackageV2 contract-only DTO, vocabulary, JSON helper, schema, and tests while deferring mapper, validator, runtime behavior, and V1 changes. Tests: 1,675/1,675 passed. git diff --check passed. --- .../V2/TrackLayoutPackageV2Dtos.cs | 126 ++++++ .../V2/TrackLayoutPackageV2Json.cs | 61 +++ .../V2/TrackLayoutPackageV2Vocabulary.cs | 67 ++++ Quantum.Tests/IO/TrackLayoutPackageV2Tests.cs | 253 ++++++++++++ .../track-layout-package-v2.schema.json | 370 ++++++++++++++++++ 5 files changed, 877 insertions(+) create mode 100644 Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Dtos.cs create mode 100644 Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Json.cs create mode 100644 Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Vocabulary.cs create mode 100644 Quantum.Tests/IO/TrackLayoutPackageV2Tests.cs create mode 100644 docs/contracts/track-layout-package-v2.schema.json diff --git a/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Dtos.cs b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Dtos.cs new file mode 100644 index 0000000..381a819 --- /dev/null +++ b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Dtos.cs @@ -0,0 +1,126 @@ +using System; +using System.Text.Json.Serialization; + +namespace Quantum.IO.TrackLayout.V2 +{ + /// + /// Versioned backend-only JSON DTO for authored coaster track layouts. + /// + public sealed class TrackLayoutPackageV2Dto + { + public const string ContractName = "quantum.track_layout_package"; + + public const int ContractVersion = 2; + + public string Contract { get; set; } = ContractName; + + public int Version { get; set; } = ContractVersion; + + public TrackLayoutMetadataV2Dto Metadata { get; set; } = new TrackLayoutMetadataV2Dto(); + + public TrackStartPoseV2Dto StartPose { get; set; } = new TrackStartPoseV2Dto(); + + public TrackLayoutSectionV2Dto[] Sections { get; set; } = + Array.Empty(); + + public TrackBankingV2Dto? Banking { get; set; } + + public TrackHeartlineV2Dto? Heartline { get; set; } + } + + public sealed class TrackLayoutMetadataV2Dto + { + public string Units { get; set; } = "meters"; + + public string? SourceName { get; set; } + + public string? LayoutId { get; set; } + } + + public sealed class TrackStartPoseV2Dto + { + public TrackLayoutVector3dV2Dto Position { get; set; } = new TrackLayoutVector3dV2Dto(); + + public TrackLayoutVector3dV2Dto Tangent { get; set; } = + new TrackLayoutVector3dV2Dto { X = 1.0 }; + + public TrackLayoutVector3dV2Dto Normal { get; set; } = + new TrackLayoutVector3dV2Dto { Y = 1.0 }; + + public TrackLayoutVector3dV2Dto Binormal { get; set; } = + new TrackLayoutVector3dV2Dto { Z = 1.0 }; + } + + public sealed class TrackLayoutVector3dV2Dto + { + public double X { get; set; } + + public double Y { get; set; } + + public double Z { get; set; } + } + + public sealed class TrackLayoutSectionV2Dto + { + public string Kind { get; set; } = string.Empty; + + public string Id { get; set; } = string.Empty; + + public double Length { get; set; } + + public double RollRadians { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Radius { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? StartCurvature { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? EndCurvature { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? InterpolationMode { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Degree { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TrackLayoutVector3dV2Dto[]? ControlPoints { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double[]? Weights { get; set; } + } + + public sealed class TrackBankingV2Dto + { + public TrackBankingKeyV2Dto[] Keys { get; set; } = + Array.Empty(); + } + + public sealed class TrackBankingKeyV2Dto + { + public double Distance { get; set; } + + public double RollRadians { get; set; } + + public string InterpolationToNext { get; set; } = + TrackLayoutPackageV2Vocabulary.BankingInterpolationConstant; + } + + public sealed class TrackHeartlineV2Dto + { + public string Kind { get; set; } = + TrackLayoutPackageV2Vocabulary.HeartlineKindConstantOffset; + + public string DistanceDomain { get; set; } = + TrackLayoutPackageV2Vocabulary.HeartlineDistanceDomainCenterlineStation; + + public string AxisSource { get; set; } = + TrackLayoutPackageV2Vocabulary.HeartlineAxisSourceSampledFrame; + + public double NormalOffset { get; set; } + + public double LateralOffset { get; set; } + } +} diff --git a/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Json.cs b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Json.cs new file mode 100644 index 0000000..8be6d99 --- /dev/null +++ b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Json.cs @@ -0,0 +1,61 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Quantum.IO.TrackLayout.V2 +{ + /// + /// JSON reader/writer for the TrackLayoutPackageV2 authored-layout contract. + /// + public static class TrackLayoutPackageV2Json + { + private static readonly JsonSerializerOptions CompactOptions = CreateOptions(indented: false); + private static readonly JsonSerializerOptions IndentedOptions = CreateOptions(indented: true); + + public static string Serialize(TrackLayoutPackageV2Dto dto, bool indented = false) + { + if (dto == null) + { + throw new ArgumentNullException(nameof(dto)); + } + + return JsonSerializer.Serialize(dto, indented ? IndentedOptions : CompactOptions); + } + + public static TrackLayoutPackageV2Dto Deserialize(string json) + { + if (json == null) + { + throw new ArgumentNullException(nameof(json)); + } + + TrackLayoutPackageV2Dto? dto; + + try + { + dto = JsonSerializer.Deserialize(json, CompactOptions); + } + catch (JsonException ex) + { + throw new JsonException("Failed to deserialize TrackLayoutPackageV2Dto: malformed JSON.", ex); + } + + if (dto == null) + { + throw new JsonException("Failed to deserialize TrackLayoutPackageV2Dto: JSON payload was null."); + } + + return dto; + } + + private static JsonSerializerOptions CreateOptions(bool indented) + { + return new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = indented, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow + }; + } + } +} diff --git a/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Vocabulary.cs b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Vocabulary.cs new file mode 100644 index 0000000..7749109 --- /dev/null +++ b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Vocabulary.cs @@ -0,0 +1,67 @@ +using System; + +namespace Quantum.IO.TrackLayout.V2 +{ + public static class TrackLayoutPackageV2Vocabulary + { + public const string StraightSectionKind = "straight"; + public const string ConstantCurvatureSectionKind = "constantCurvature"; + public const string CurvatureTransitionSectionKind = "curvatureTransition"; + public const string SpatialSectionKind = "spatial"; + + public const string CurvatureInterpolationLinear = "linear"; + + public const string BankingInterpolationConstant = "constant"; + public const string BankingInterpolationLinear = "linear"; + public const string BankingInterpolationSmoothStep = "smoothStep"; + public const string BankingInterpolationQuadratic = "quadratic"; + public const string BankingInterpolationCubic = "cubic"; + public const string BankingInterpolationQuartic = "quartic"; + public const string BankingInterpolationQuintic = "quintic"; + public const string BankingInterpolationSinusoidal = "sinusoidal"; + + public const string HeartlineKindConstantOffset = "constantOffset"; + public const string HeartlineDistanceDomainCenterlineStation = "centerlineStation"; + public const string HeartlineAxisSourceSampledFrame = "sampledFrame"; + + public static bool IsKnownSectionKind(string? value) + { + return string.Equals(value, StraightSectionKind, StringComparison.Ordinal) || + string.Equals(value, ConstantCurvatureSectionKind, StringComparison.Ordinal) || + string.Equals(value, CurvatureTransitionSectionKind, StringComparison.Ordinal) || + string.Equals(value, SpatialSectionKind, StringComparison.Ordinal); + } + + public static bool IsKnownCurvatureTransitionInterpolation(string? value) + { + return string.Equals(value, CurvatureInterpolationLinear, StringComparison.Ordinal); + } + + public static bool IsKnownBankingInterpolation(string? value) + { + return string.Equals(value, BankingInterpolationConstant, StringComparison.Ordinal) || + string.Equals(value, BankingInterpolationLinear, StringComparison.Ordinal) || + string.Equals(value, BankingInterpolationSmoothStep, StringComparison.Ordinal) || + string.Equals(value, BankingInterpolationQuadratic, StringComparison.Ordinal) || + string.Equals(value, BankingInterpolationCubic, StringComparison.Ordinal) || + string.Equals(value, BankingInterpolationQuartic, StringComparison.Ordinal) || + string.Equals(value, BankingInterpolationQuintic, StringComparison.Ordinal) || + string.Equals(value, BankingInterpolationSinusoidal, StringComparison.Ordinal); + } + + public static bool IsKnownHeartlineKind(string? value) + { + return string.Equals(value, HeartlineKindConstantOffset, StringComparison.Ordinal); + } + + public static bool IsKnownHeartlineDistanceDomain(string? value) + { + return string.Equals(value, HeartlineDistanceDomainCenterlineStation, StringComparison.Ordinal); + } + + public static bool IsKnownHeartlineAxisSource(string? value) + { + return string.Equals(value, HeartlineAxisSourceSampledFrame, StringComparison.Ordinal); + } + } +} diff --git a/Quantum.Tests/IO/TrackLayoutPackageV2Tests.cs b/Quantum.Tests/IO/TrackLayoutPackageV2Tests.cs new file mode 100644 index 0000000..55e26e1 --- /dev/null +++ b/Quantum.Tests/IO/TrackLayoutPackageV2Tests.cs @@ -0,0 +1,253 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; +using Json.Schema; +using Quantum.IO.TrackLayout.V2; + +namespace Quantum.Tests; + +public sealed class TrackLayoutPackageV2Tests +{ + private static readonly Lazy TrackLayoutPackageSchema = new(CreateTrackLayoutPackageSchema); + + [Fact] + public void ContractConstantsAndVocabulary_AreStable() + { + var dto = new TrackLayoutPackageV2Dto(); + + Assert.Equal("quantum.track_layout_package", TrackLayoutPackageV2Dto.ContractName); + Assert.Equal(2, TrackLayoutPackageV2Dto.ContractVersion); + Assert.Equal(TrackLayoutPackageV2Dto.ContractName, dto.Contract); + Assert.Equal(TrackLayoutPackageV2Dto.ContractVersion, dto.Version); + + Assert.Equal("straight", TrackLayoutPackageV2Vocabulary.StraightSectionKind); + Assert.Equal("constantCurvature", TrackLayoutPackageV2Vocabulary.ConstantCurvatureSectionKind); + Assert.Equal("curvatureTransition", TrackLayoutPackageV2Vocabulary.CurvatureTransitionSectionKind); + Assert.Equal("spatial", TrackLayoutPackageV2Vocabulary.SpatialSectionKind); + Assert.Equal("linear", TrackLayoutPackageV2Vocabulary.CurvatureInterpolationLinear); + Assert.Equal("constantOffset", TrackLayoutPackageV2Vocabulary.HeartlineKindConstantOffset); + Assert.Equal( + "centerlineStation", + TrackLayoutPackageV2Vocabulary.HeartlineDistanceDomainCenterlineStation); + Assert.Equal("sampledFrame", TrackLayoutPackageV2Vocabulary.HeartlineAxisSourceSampledFrame); + + Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownSectionKind("spatial")); + Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownCurvatureTransitionInterpolation("linear")); + Assert.False(TrackLayoutPackageV2Vocabulary.IsKnownCurvatureTransitionInterpolation("smoothStep")); + Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownBankingInterpolation("sinusoidal")); + Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownHeartlineKind("constantOffset")); + Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownHeartlineDistanceDomain("centerlineStation")); + Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownHeartlineAxisSource("sampledFrame")); + } + + [Fact] + public void DefaultDtoShape_IsStable() + { + var dto = new TrackLayoutPackageV2Dto(); + + Assert.Equal("meters", dto.Metadata.Units); + Assert.Null(dto.Metadata.SourceName); + Assert.Null(dto.Metadata.LayoutId); + Assert.Equal(1.0, dto.StartPose.Tangent.X); + Assert.Equal(1.0, dto.StartPose.Normal.Y); + Assert.Equal(1.0, dto.StartPose.Binormal.Z); + Assert.Empty(dto.Sections); + Assert.Null(dto.Banking); + Assert.Null(dto.Heartline); + + string json = TrackLayoutPackageV2Json.Serialize(dto, indented: true); + Assert.Contains("\"layoutId\": null", json); + Assert.Contains("\"banking\": null", json); + Assert.Contains("\"heartline\": null", json); + } + + [Fact] + public void Serialize_RepresentativePackage_IsDeterministicCamelCaseJson() + { + TrackLayoutPackageV2Dto dto = CreateConstantHeartlineDto(); + + string first = NormalizeLineEndings(TrackLayoutPackageV2Json.Serialize(dto, indented: true)).TrimEnd(); + string second = NormalizeLineEndings(TrackLayoutPackageV2Json.Serialize(dto, indented: true)).TrimEnd(); + string roundtrip = NormalizeLineEndings( + TrackLayoutPackageV2Json.Serialize( + TrackLayoutPackageV2Json.Deserialize(first), + indented: true)).TrimEnd(); + + Assert.Equal(first, second); + Assert.Equal(first, roundtrip); + Assert.Contains("\"startPose\":", first); + Assert.Contains("\"layoutId\":", first); + Assert.Contains("\"constantOffset\"", first); + Assert.Contains("\"normalOffset\":", first); + Assert.DoesNotContain("\"StartPose\":", first); + Assert.DoesNotContain("\"NormalOffset\":", first); + } + + [Fact] + public void SchemaValidation_MinimalV2Json_IsValid() + { + string json = TrackLayoutPackageV2Json.Serialize(CreateMinimalDto()); + + Assert.True(IsValidAgainstSchema(json)); + } + + [Fact] + public void SchemaValidation_ConstantHeartlineV2Json_IsValid() + { + string json = TrackLayoutPackageV2Json.Serialize(CreateConstantHeartlineDto()); + + Assert.True(IsValidAgainstSchema(json)); + } + + [Fact] + public void SchemaValidation_RejectsWrongVersion() + { + JsonObject json = ParseJsonObject(TrackLayoutPackageV2Json.Serialize(CreateMinimalDto())); + json["version"] = 1; + + Assert.False(IsValidAgainstSchema(json.ToJsonString())); + } + + [Fact] + public void SchemaValidation_RejectsUnknownRootFields() + { + JsonObject json = ParseJsonObject(TrackLayoutPackageV2Json.Serialize(CreateMinimalDto())); + json["train"] = new JsonObject(); + + Assert.False(IsValidAgainstSchema(json.ToJsonString())); + } + + private static TrackLayoutPackageV2Dto CreateMinimalDto() + { + return new TrackLayoutPackageV2Dto + { + Metadata = new TrackLayoutMetadataV2Dto + { + Units = "meters", + SourceName = "Minimal V2 layout", + LayoutId = null + }, + Sections = new[] + { + new TrackLayoutSectionV2Dto + { + Kind = TrackLayoutPackageV2Vocabulary.StraightSectionKind, + Id = "entry", + Length = 12.0, + RollRadians = 0.0 + } + }, + Banking = null, + Heartline = null + }; + } + + private static TrackLayoutPackageV2Dto CreateConstantHeartlineDto() + { + TrackLayoutPackageV2Dto dto = CreateMinimalDto(); + dto.Metadata.SourceName = "Constant heartline V2 layout"; + dto.Metadata.LayoutId = "layout.m147.constant-heartline"; + dto.Sections = new[] + { + new TrackLayoutSectionV2Dto + { + Kind = TrackLayoutPackageV2Vocabulary.StraightSectionKind, + Id = "entry", + Length = 12.0, + RollRadians = 0.0 + }, + new TrackLayoutSectionV2Dto + { + Kind = TrackLayoutPackageV2Vocabulary.ConstantCurvatureSectionKind, + Id = "turn", + Length = 18.0, + RollRadians = 0.2, + Radius = 30.0 + } + }; + dto.Banking = new TrackBankingV2Dto + { + Keys = new[] + { + new TrackBankingKeyV2Dto + { + Distance = 0.0, + RollRadians = 0.0, + InterpolationToNext = TrackLayoutPackageV2Vocabulary.BankingInterpolationLinear + }, + new TrackBankingKeyV2Dto + { + Distance = 12.0, + RollRadians = 0.2, + InterpolationToNext = TrackLayoutPackageV2Vocabulary.BankingInterpolationConstant + }, + new TrackBankingKeyV2Dto + { + Distance = 30.0, + RollRadians = 0.2, + InterpolationToNext = TrackLayoutPackageV2Vocabulary.BankingInterpolationConstant + } + } + }; + dto.Heartline = new TrackHeartlineV2Dto + { + NormalOffset = 1.1, + LateralOffset = 0.0 + }; + + return dto; + } + + private static bool IsValidAgainstSchema(string instanceJson) + { + using JsonDocument instanceDocument = JsonDocument.Parse(instanceJson); + + EvaluationResults results = TrackLayoutPackageSchema.Value.Evaluate( + instanceDocument.RootElement, + new EvaluationOptions + { + OutputFormat = OutputFormat.List + }); + + return results.IsValid; + } + + private static JsonSchema CreateTrackLayoutPackageSchema() + { + string schemaPath = FindContractFile("track-layout-package-v2.schema.json"); + string schemaJson = File.ReadAllText(schemaPath); + return JsonSchema.FromText(schemaJson); + } + + private static string FindContractFile(string fileName) + { + DirectoryInfo? current = new(AppContext.BaseDirectory); + + while (current != null) + { + string candidate = Path.Combine(current.FullName, "docs", "contracts", fileName); + if (File.Exists(candidate)) + { + return candidate; + } + + current = current.Parent; + } + + throw new FileNotFoundException( + "Contract file '" + fileName + "' was not found from '" + AppContext.BaseDirectory + "'."); + } + + private static JsonObject ParseJsonObject(string json) + { + JsonNode? node = JsonNode.Parse(json); + Assert.NotNull(node); + return Assert.IsType(node); + } + + private static string NormalizeLineEndings(string value) + { + return value.ReplaceLineEndings("\n"); + } +} diff --git a/docs/contracts/track-layout-package-v2.schema.json b/docs/contracts/track-layout-package-v2.schema.json new file mode 100644 index 0000000..f9a4551 --- /dev/null +++ b/docs/contracts/track-layout-package-v2.schema.json @@ -0,0 +1,370 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://quantumcoasterworks.dev/schemas/track-layout-package-v2.schema.json", + "title": "TrackLayoutPackageV2", + "description": "Backend-only JSON contract skeleton for authored coaster track layouts with nullable banking and heartline roots.", + "type": "object", + "additionalProperties": false, + "required": [ + "contract", + "version", + "metadata", + "startPose", + "sections", + "banking", + "heartline" + ], + "properties": { + "contract": { + "description": "Stable contract discriminator.", + "type": "string", + "const": "quantum.track_layout_package" + }, + "version": { + "description": "Contract version.", + "type": "integer", + "const": 2 + }, + "metadata": { + "$ref": "#/$defs/metadata" + }, + "startPose": { + "$ref": "#/$defs/startPose" + }, + "sections": { + "description": "Ordered authored geometric section definitions.", + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "$ref": "#/$defs/straightSection" + }, + { + "$ref": "#/$defs/constantCurvatureSection" + }, + { + "$ref": "#/$defs/curvatureTransitionSection" + }, + { + "$ref": "#/$defs/spatialSection" + } + ] + } + }, + "banking": { + "description": "Optional explicit authored banking. Null means no explicit banking is persisted.", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/banking" + } + ] + }, + "heartline": { + "description": "Optional authored heartline offset. Null means no authored heartline offset is persisted.", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/constantHeartline" + } + ] + } + }, + "$defs": { + "metadata": { + "type": "object", + "additionalProperties": false, + "required": [ + "units", + "sourceName" + ], + "properties": { + "units": { + "description": "Distance units used by authored scalar values.", + "type": "string" + }, + "sourceName": { + "description": "Optional source label for the authored layout package.", + "type": [ + "string", + "null" + ] + }, + "layoutId": { + "description": "Optional stable layout identity for companion contracts and manifests.", + "type": [ + "string", + "null" + ] + } + } + }, + "startPose": { + "type": "object", + "additionalProperties": false, + "required": [ + "position", + "tangent", + "normal", + "binormal" + ], + "properties": { + "position": { + "$ref": "#/$defs/vector3d" + }, + "tangent": { + "$ref": "#/$defs/vector3d" + }, + "normal": { + "$ref": "#/$defs/vector3d" + }, + "binormal": { + "$ref": "#/$defs/vector3d" + } + } + }, + "vector3d": { + "type": "object", + "additionalProperties": false, + "required": [ + "x", + "y", + "z" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "straightSection": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "length", + "rollRadians" + ], + "properties": { + "kind": { + "type": "string", + "const": "straight" + }, + "id": { + "type": "string" + }, + "length": { + "type": "number" + }, + "rollRadians": { + "type": "number" + } + } + }, + "constantCurvatureSection": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "length", + "rollRadians", + "radius" + ], + "properties": { + "kind": { + "type": "string", + "const": "constantCurvature" + }, + "id": { + "type": "string" + }, + "length": { + "type": "number" + }, + "rollRadians": { + "type": "number" + }, + "radius": { + "type": "number" + } + } + }, + "curvatureTransitionSection": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "length", + "rollRadians", + "startCurvature", + "endCurvature", + "interpolationMode" + ], + "properties": { + "kind": { + "type": "string", + "const": "curvatureTransition" + }, + "id": { + "type": "string" + }, + "length": { + "type": "number" + }, + "rollRadians": { + "type": "number" + }, + "startCurvature": { + "type": "number" + }, + "endCurvature": { + "type": "number" + }, + "interpolationMode": { + "type": "string", + "enum": [ + "linear" + ] + } + } + }, + "spatialSection": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "length", + "rollRadians", + "degree", + "controlPoints", + "weights" + ], + "properties": { + "kind": { + "type": "string", + "const": "spatial" + }, + "id": { + "type": "string" + }, + "length": { + "type": "number" + }, + "rollRadians": { + "type": "number" + }, + "degree": { + "type": "integer", + "minimum": 1 + }, + "controlPoints": { + "type": "array", + "minItems": 2, + "items": { + "$ref": "#/$defs/vector3d" + } + }, + "weights": { + "type": "array", + "minItems": 2, + "items": { + "type": "number", + "exclusiveMinimum": 0 + } + } + } + }, + "banking": { + "type": "object", + "additionalProperties": false, + "required": [ + "keys" + ], + "properties": { + "keys": { + "type": "array", + "minItems": 2, + "items": { + "$ref": "#/$defs/bankingKey" + } + } + } + }, + "bankingKey": { + "type": "object", + "additionalProperties": false, + "required": [ + "distance", + "rollRadians", + "interpolationToNext" + ], + "properties": { + "distance": { + "type": "number" + }, + "rollRadians": { + "type": "number" + }, + "interpolationToNext": { + "type": "string", + "enum": [ + "constant", + "linear", + "smoothStep", + "quadratic", + "cubic", + "quartic", + "quintic", + "sinusoidal" + ] + } + } + }, + "constantHeartline": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "distanceDomain", + "axisSource", + "normalOffset", + "lateralOffset" + ], + "properties": { + "kind": { + "type": "string", + "const": "constantOffset" + }, + "distanceDomain": { + "type": "string", + "const": "centerlineStation" + }, + "axisSource": { + "type": "string", + "const": "sampledFrame" + }, + "normalOffset": { + "type": "number" + }, + "lateralOffset": { + "type": "number" + } + } + } + } +}