From 7b29dbff7e43e87741476f47df17ad1582b16546 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 00:58:15 +0800 Subject: feat(pipeline): add runnable artifact pipeline increment [Change Nature] - This commit adds the first runnable increment of the F# artifact pipeline: CLI, server, frontend bundle, stage fixture, and tests. It is feature work, not a bug fix. [New Capability] - CLI `run` executes the three-step Blender fixture through the process bridge and writes GLBs, step logs, events, and a manifest; `verify` validates a manifest without Blender. - Server exposes run start, run-scoped SSE, artifact manifest/file routes, and serves the Fable frontend from its build output. - Frontend starts runs, follows run-scoped SSE progress, and links the artifact manifest on completion. [Implementation] - Shared layer holds domain, run state, replay, and artifact contracts; Modeling holds the pipeline, 120s bridge timeout, and result-file protocol; stage/blender/fixture.py owns geometry in Python (bpy owns geometry, F# owns orchestration). - Fable bundle is regenerated into public/ and copied by the server project; solution includes all projects with warnings-as-errors. - README documents build, Fable, server, interpreter/env, fixture limitations, and current verification status. [Impact] - Verified: 40/40 solution tests pass; Release build 0 warnings/errors; real CLI run with bpy 5.0.1 produced 3 GLBs, 3 logs, and a verify-clean manifest; isolated-port server smoke flow succeeded. - Not yet verified: browser end-to-end behavior and production fidelity. Artifacts and caches are gitignored. --- src/SomhairlesDream.Shared/Domain.fs | 102 +++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/SomhairlesDream.Shared/Domain.fs (limited to 'src/SomhairlesDream.Shared/Domain.fs') 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 + +[] +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 } -- cgit v1.2.3