summaryrefslogtreecommitdiff
path: root/src/SomhairlesDream.Shared
diff options
context:
space:
mode:
Diffstat (limited to 'src/SomhairlesDream.Shared')
-rw-r--r--src/SomhairlesDream.Shared/Artifact.fs218
-rw-r--r--src/SomhairlesDream.Shared/Domain.fs102
-rw-r--r--src/SomhairlesDream.Shared/Replay.fs107
-rw-r--r--src/SomhairlesDream.Shared/SomhairlesDream.Shared.fsproj12
4 files changed, 439 insertions, 0 deletions
diff --git a/src/SomhairlesDream.Shared/Artifact.fs b/src/SomhairlesDream.Shared/Artifact.fs
new file mode 100644
index 0000000..d1cb4f0
--- /dev/null
+++ b/src/SomhairlesDream.Shared/Artifact.fs
@@ -0,0 +1,218 @@
+namespace SomhairlesDream.Shared
+
+open System
+#if !FABLE_COMPILER
+open System.Text.Json
+open System.Text.Json.Nodes
+#endif
+
+[<CLIMutable>]
+type RunIds =
+ { ProjectId: string
+ RunId: string
+ BaseId: string
+ TargetId: string }
+
+module Ids =
+ open System.Text.RegularExpressions
+
+ let private resourcePattern = Regex("^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", RegexOptions.CultureInvariant)
+ let private objectPattern = Regex("^[a-z0-9]+(?:[._-][a-z0-9]+)*$", RegexOptions.CultureInvariant)
+
+ let validateResourceId value =
+ if isNull value || not (resourcePattern.IsMatch(value)) then
+ Error "resource id must contain only lower-case letters, digits, and hyphens"
+ else
+ Ok ()
+
+ let validateObjectId value =
+ if isNull value || not (objectPattern.IsMatch(value)) then
+ Error "object id must use the dotted lower-case namespace"
+ else
+ Ok ()
+
+[<CLIMutable>]
+type RunStart =
+ { Ids: RunIds
+ At: DateTimeOffset
+ TotalSteps: int }
+
+[<CLIMutable>]
+type RunHeartbeat =
+ { Ids: RunIds
+ At: DateTimeOffset
+ Message: string }
+
+[<CLIMutable>]
+type RunCheckpoint =
+ { Ids: RunIds
+ At: DateTimeOffset
+ StepId: string
+ StepIndex: int
+ TotalSteps: int
+ Message: string
+ ArtifactPath: string }
+
+[<CLIMutable>]
+type RunComplete =
+ { Ids: RunIds
+ At: DateTimeOffset
+ ManifestPath: string }
+
+[<CLIMutable>]
+type RunFail =
+ { Ids: RunIds
+ At: DateTimeOffset
+ StepId: string option
+ Message: string }
+
+type RunEvent =
+ | Start of RunStart
+ | Heartbeat of RunHeartbeat
+ | Checkpoint of RunCheckpoint
+ | Complete of RunComplete
+ | Fail of RunFail
+
+[<CLIMutable>]
+type RunEventWire =
+ { Kind: string
+ At: DateTimeOffset
+ ProjectId: string
+ RunId: string
+ BaseId: string
+ TargetId: string
+ TotalSteps: int option
+ StepId: string option
+ StepIndex: int option
+ Message: string option
+ ArtifactPath: string option
+ ManifestPath: string option }
+
+module RunEvent =
+ let private ids (value: RunIds) =
+ value.ProjectId, value.RunId, value.BaseId, value.TargetId
+
+ let toWire event =
+ let kind, eventIds, at, totalSteps, stepId, stepIndex, message, artifactPath, manifestPath =
+ match event with
+ | Start value ->
+ "start", value.Ids, value.At, Some value.TotalSteps, None, None, None, None, None
+ | Heartbeat value ->
+ "heartbeat", value.Ids, value.At, None, None, None, Some value.Message, None, None
+ | Checkpoint value ->
+ "checkpoint",
+ value.Ids,
+ value.At,
+ Some value.TotalSteps,
+ Some value.StepId,
+ Some value.StepIndex,
+ Some value.Message,
+ Some value.ArtifactPath,
+ None
+ | Complete value ->
+ "complete", value.Ids, value.At, None, None, None, None, None, Some value.ManifestPath
+ | Fail value ->
+ "fail", value.Ids, value.At, None, value.StepId, None, Some value.Message, None, None
+
+ let projectId, runId, baseId, targetId = ids eventIds
+
+ { Kind = kind
+ At = at
+ ProjectId = projectId
+ RunId = runId
+ BaseId = baseId
+ TargetId = targetId
+ TotalSteps = totalSteps
+ StepId = stepId
+ StepIndex = stepIndex
+ Message = message
+ ArtifactPath = artifactPath
+ ManifestPath = manifestPath }
+
+#if !FABLE_COMPILER
+ let private addOptional (json: JsonObject) (name: string) value create =
+ match value with
+ | Some item -> json[name] <- create item
+ | None -> ()
+
+ let private jsonOptions () =
+ let options = JsonSerializerOptions()
+ options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase
+ options.WriteIndented <- false
+ options
+
+ let toJson event =
+ let wire = toWire event
+ let json = JsonObject()
+ json["kind"] <- JsonValue.Create(wire.Kind)
+ json["at"] <- JsonValue.Create(wire.At)
+ json["projectId"] <- JsonValue.Create(wire.ProjectId)
+ json["runId"] <- JsonValue.Create(wire.RunId)
+ json["baseId"] <- JsonValue.Create(wire.BaseId)
+ json["targetId"] <- JsonValue.Create(wire.TargetId)
+ addOptional json "totalSteps" wire.TotalSteps (fun value -> JsonValue.Create(value))
+ addOptional json "stepId" wire.StepId (fun value -> JsonValue.Create(value))
+ addOptional json "stepIndex" wire.StepIndex (fun value -> JsonValue.Create(value))
+ addOptional json "message" wire.Message (fun value -> JsonValue.Create(value))
+ addOptional json "artifactPath" wire.ArtifactPath (fun value -> JsonValue.Create(value))
+ addOptional json "manifestPath" wire.ManifestPath (fun value -> JsonValue.Create(value))
+ json.ToJsonString(jsonOptions())
+#endif
+
+[<CLIMutable>]
+type ArtifactChange =
+ { ObjectId: string
+ Kind: string
+ Summary: string }
+
+[<CLIMutable>]
+type RenderArtifact =
+ { ArtifactPath: string
+ Sha256: string
+ Bytes: int64
+ ExportedAt: DateTimeOffset }
+
+[<CLIMutable>]
+type StepArtifact =
+ { StepId: string
+ StepIndex: int
+ ArtifactPath: string
+ LogPath: string
+ Sha256: string
+ Bytes: int64
+ ExportedAt: DateTimeOffset
+ ObjectIds: string array
+ VerifiedObjectIds: string array
+ Changes: ArtifactChange array
+ Render: RenderArtifact option }
+
+[<CLIMutable>]
+type ArtifactManifest =
+ { SchemaVersion: int
+ ProjectId: string
+ RunId: string
+ BaseId: string
+ TargetId: string
+ Status: string
+ StartedAt: DateTimeOffset
+ CompletedAt: DateTimeOffset
+ BlenderVersion: string
+ Steps: StepArtifact array }
+
+[<CLIMutable>]
+type RunSnapshot =
+ { ProjectId: string
+ RunId: string
+ BaseId: string
+ TargetId: string
+ Status: string
+ CurrentStepId: string option
+ CurrentStepIndex: int
+ TotalSteps: int
+ LastHeartbeatAt: DateTimeOffset option
+ UpdatedAt: DateTimeOffset
+ Message: string
+ ManifestPath: string option
+ ManifestUrl: string option
+ Error: string option
+ IsStale: bool }
diff --git a/src/SomhairlesDream.Shared/Domain.fs b/src/SomhairlesDream.Shared/Domain.fs
new file mode 100644
index 0000000..fd229fa
--- /dev/null
+++ b/src/SomhairlesDream.Shared/Domain.fs
@@ -0,0 +1,102 @@
+namespace SomhairlesDream.Shared
+
+open System
+
+[<RequireQualifiedAccess>]
+type RunStatus =
+ | Idle = 0
+ | Queued = 1
+ | Running = 2
+ | Succeeded = 3
+ | Failed = 4
+ | Stale = 5
+
+type Vector3 =
+ { X: float
+ Y: float
+ Z: float }
+
+type MeshSnapshot =
+ { Version: int
+ Label: string
+ Vertices: Vector3 array
+ Faces: int array array }
+
+type LiveSnapshot =
+ { ProjectId: string
+ RunId: string
+ Status: RunStatus
+ CurrentVersion: int
+ TargetVersion: int
+ HeartbeatAt: DateTimeOffset option
+ UpdatedAt: DateTimeOffset
+ Message: string
+ Mesh: MeshSnapshot option
+ NextDesignRunScheduled: bool }
+
+module MeshData =
+ let positionData (mesh: MeshSnapshot) =
+ mesh.Vertices
+ |> Array.collect (fun vertex -> [| vertex.X; vertex.Y; vertex.Z |])
+
+ let triangleIndices (mesh: MeshSnapshot) =
+ mesh.Faces
+ |> Array.collect (fun face ->
+ if face.Length < 3 then
+ [||]
+ else
+ [| for index in 1 .. face.Length - 2 do
+ yield face[0]
+ yield face[index]
+ yield face[index + 1] |])
+
+module RunState =
+ let initial projectId runId now =
+ { ProjectId = projectId
+ RunId = runId
+ Status = RunStatus.Idle
+ CurrentVersion = 0
+ TargetVersion = 0
+ HeartbeatAt = None
+ UpdatedAt = now
+ Message = "等待设计运行"
+ Mesh = None
+ NextDesignRunScheduled = false }
+
+ let start now state =
+ let targetVersion = state.TargetVersion + 1
+
+ { state with
+ Status = RunStatus.Running
+ TargetVersion = targetVersion
+ HeartbeatAt = Some now
+ UpdatedAt = now
+ Message = $"正在生成第 {targetVersion} 版几何"
+ NextDesignRunScheduled = false }
+
+ let heartbeat now state =
+ if state.Status = RunStatus.Running then
+ { state with
+ HeartbeatAt = Some now
+ UpdatedAt = now }
+ else
+ state
+
+ let statusAt timeout now state =
+ match state.HeartbeatAt with
+ | Some heartbeatAt when state.Status = RunStatus.Running && now - heartbeatAt > timeout ->
+ { state with
+ Status = RunStatus.Stale
+ UpdatedAt = now
+ Message = "心跳超时,等待重连" }
+ | _ -> state
+
+ let publish now mesh state =
+ { state with
+ Status = RunStatus.Succeeded
+ CurrentVersion = mesh.Version
+ HeartbeatAt = Some now
+ UpdatedAt = now
+ Message = $"已发布{mesh.Label}"
+ Mesh = Some mesh
+ NextDesignRunScheduled = false }
diff --git a/src/SomhairlesDream.Shared/Replay.fs b/src/SomhairlesDream.Shared/Replay.fs
new file mode 100644
index 0000000..782e632
--- /dev/null
+++ b/src/SomhairlesDream.Shared/Replay.fs
@@ -0,0 +1,107 @@
+namespace SomhairlesDream.Shared
+
+type ReplayFrame =
+ { Version: int
+ Label: string
+ Mesh: MeshSnapshot }
+
+module Replay =
+ let private vertex x y z =
+ { X = x
+ Y = y
+ Z = z }
+
+ let private frame version label vertices faces =
+ { Version = version
+ Label = label
+ Mesh =
+ { Version = version
+ Label = label
+ Vertices = vertices
+ Faces = faces } }
+
+ let frames () =
+ [| frame
+ 1
+ "重建起点"
+ [| vertex -2.0 0.0 -1.0
+ vertex 2.0 0.0 -1.0
+ vertex 2.0 0.0 1.0
+ vertex -2.0 0.0 1.0
+ vertex -1.0 2.0 -0.5
+ vertex 1.0 2.0 -0.5
+ vertex 1.0 2.0 0.5
+ vertex -1.0 2.0 0.5 |]
+ [| [| 0; 1; 2 |]
+ [| 0; 2; 3 |]
+ [| 4; 6; 5 |]
+ [| 4; 7; 6 |]
+ [| 0; 4; 5 |]
+ [| 0; 5; 1 |]
+ [| 1; 5; 6 |]
+ [| 1; 6; 2 |]
+ [| 2; 6; 7 |]
+ [| 2; 7; 3 |]
+ [| 3; 7; 4 |]
+ [| 3; 4; 0 |] |]
+ frame
+ 2
+ "主承力骨架"
+ [| vertex -2.5 0.0 -1.0
+ vertex 2.5 0.0 -1.0
+ vertex 2.5 0.0 1.0
+ vertex -2.5 0.0 1.0
+ vertex -1.5 2.2 -0.7
+ vertex 1.5 2.2 -0.7
+ vertex 1.5 2.2 0.7
+ vertex -1.5 2.2 0.7
+ vertex -0.45 0.0 -0.45
+ vertex 0.45 0.0 -0.45
+ vertex 0.45 3.0 0.45
+ vertex -0.45 3.0 0.45 |]
+ [| [| 0; 1; 2 |]
+ [| 0; 2; 3 |]
+ [| 4; 6; 5 |]
+ [| 4; 7; 6 |]
+ [| 8; 9; 10 |]
+ [| 8; 10; 11 |]
+ [| 0; 4; 8 |]
+ [| 4; 11; 8 |]
+ [| 1; 9; 5 |]
+ [| 1; 5; 2 |]
+ [| 2; 5; 10 |]
+ [| 2; 10; 3 |]
+ [| 3; 10; 7 |]
+ [| 3; 7; 0 |] |]
+ frame
+ 3
+ "舱段重构"
+ [| vertex -3.0 0.0 -1.3
+ vertex 3.0 0.0 -1.3
+ vertex 3.0 0.0 1.3
+ vertex -3.0 0.0 1.3
+ vertex -2.0 2.0 -1.0
+ vertex 2.0 2.0 -1.0
+ vertex 2.0 2.0 1.0
+ vertex -2.0 2.0 1.0
+ vertex -1.0 3.4 -0.65
+ vertex 1.0 3.4 -0.65
+ vertex 1.0 3.4 0.65
+ vertex -1.0 3.4 0.65
+ vertex -3.8 0.6 0.0
+ vertex 3.8 0.6 0.0 |]
+ [| [| 0; 1; 2 |]
+ [| 0; 2; 3 |]
+ [| 4; 6; 5 |]
+ [| 4; 7; 6 |]
+ [| 8; 9; 10 |]
+ [| 8; 10; 11 |]
+ [| 0; 4; 8 |]
+ [| 0; 8; 12 |]
+ [| 1; 13; 5 |]
+ [| 1; 2; 13 |]
+ [| 2; 6; 10 |]
+ [| 2; 10; 13 |]
+ [| 3; 11; 7 |]
+ [| 3; 12; 11 |]
+ [| 3; 0; 12 |] |] |]
diff --git a/src/SomhairlesDream.Shared/SomhairlesDream.Shared.fsproj b/src/SomhairlesDream.Shared/SomhairlesDream.Shared.fsproj
new file mode 100644
index 0000000..7a7e853
--- /dev/null
+++ b/src/SomhairlesDream.Shared/SomhairlesDream.Shared.fsproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <RootNamespace>SomhairlesDream.Shared</RootNamespace>
+ <AssemblyName>SomhairlesDream.Shared</AssemblyName>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="Domain.fs" />
+ <Compile Include="Artifact.fs" />
+ <Compile Include="Replay.fs" />
+ </ItemGroup>
+</Project>