Establish canonical story position context
This commit is contained in:
parent
f6b4263f31
commit
9f0fdbc366
@ -70,7 +70,10 @@ var tests = new (string Name, Action Test)[]
|
||||
("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
|
||||
("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel),
|
||||
("Live visualisation strips illustration diagnostics", LiveVisualisationStripsIllustrationDiagnostics),
|
||||
("Illustration evidence trace does not recursively store previous evidence", IllustrationEvidenceTraceDoesNotRecursivelyStorePreviousEvidence)
|
||||
("Illustration evidence trace does not recursively store previous evidence", IllustrationEvidenceTraceDoesNotRecursivelyStorePreviousEvidence),
|
||||
("StoryPosition rejects incomplete or invalid runtime scene positions", StoryPositionRejectsInvalidRuntimeScenePositions),
|
||||
("StoryPosition builds canonical runtime scene navigation", StoryPositionBuildsCanonicalRuntimeSceneNavigation),
|
||||
("StoryPosition derives canonical scene context identity", StoryPositionDerivesCanonicalSceneContextIdentity)
|
||||
};
|
||||
|
||||
foreach (var test in tests)
|
||||
@ -86,6 +89,53 @@ static void RepairsTrailingDecimalConfidence()
|
||||
Assert(result.RepairedJson.Contains("\"confidence\":0.0", StringComparison.Ordinal), result.RepairedJson);
|
||||
}
|
||||
|
||||
static void StoryPositionRejectsInvalidRuntimeScenePositions()
|
||||
{
|
||||
Assert(StoryPosition.FromRuntimeCurrentScene(new WordCompanionRuntimeCurrentSceneRequest
|
||||
{
|
||||
DocumentGuid = Guid.NewGuid(),
|
||||
ProjectId = 1,
|
||||
BookId = 2,
|
||||
ChapterId = 3,
|
||||
SceneId = 0
|
||||
}) is null, "Runtime scene positions require a valid SceneId.");
|
||||
|
||||
Assert(StoryPosition.FromTimelineSelection(0, 2, 3, 4) is null, "Story positions require a valid ProjectId.");
|
||||
}
|
||||
|
||||
static void StoryPositionBuildsCanonicalRuntimeSceneNavigation()
|
||||
{
|
||||
var response = new WordCompanionRuntimeCurrentSceneResponse
|
||||
{
|
||||
ProjectId = 10,
|
||||
BookId = 20,
|
||||
ChapterId = 30,
|
||||
SceneId = 40,
|
||||
SceneUrl = "/Scenes/Edit/40"
|
||||
};
|
||||
|
||||
var position = response.Position;
|
||||
Assert(position is not null, "Runtime response should expose a canonical StoryPosition.");
|
||||
Assert(position!.ProjectId == 10 && position.BookId == 20 && position.ChapterId == 30 && position.SceneId == 40, "Runtime response position IDs were not preserved.");
|
||||
Assert(position.SceneEditUrl == "/Scenes/Edit/40", "Scene positions must use the canonical Scene Edit route.");
|
||||
}
|
||||
|
||||
static void StoryPositionDerivesCanonicalSceneContextIdentity()
|
||||
{
|
||||
var model = new SceneEditViewModel
|
||||
{
|
||||
SceneID = 7,
|
||||
ChapterID = 6,
|
||||
Project = new Project { ProjectID = 4 },
|
||||
Book = new Book { BookID = 5 },
|
||||
Chapter = new Chapter { ChapterID = 6 }
|
||||
};
|
||||
|
||||
var position = StoryPosition.FromScene(model);
|
||||
Assert(position is not null, "Scene workspace should expose a canonical StoryPosition.");
|
||||
Assert(position!.ProjectId == 4 && position.BookId == 5 && position.ChapterId == 6 && position.SceneId == 7, "Scene workspace position IDs were not preserved.");
|
||||
}
|
||||
|
||||
static void StripsMarkdownFence()
|
||||
{
|
||||
var result = StoryIntelligenceJsonRepair.Repair("""
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
@ -9,7 +10,10 @@ public sealed class RelationshipMapController(IRelationshipMapService relationsh
|
||||
{
|
||||
public async Task<IActionResult> Index(int projectId, int? bookId, int? chapterId, int? sceneId, int? focusCharacterId, bool showFullNetwork = false)
|
||||
{
|
||||
var model = await relationshipMap.GetAsync(projectId, bookId, chapterId, sceneId, focusCharacterId, showFullNetwork);
|
||||
var position = StoryPosition.FromTimelineSelection(projectId, bookId, chapterId, sceneId);
|
||||
var model = position is null
|
||||
? null
|
||||
: await relationshipMap.GetAsync(position, focusCharacterId, showFullNetwork);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
}
|
||||
|
||||
59
PlotLine/Models/StoryPosition.cs
Normal file
59
PlotLine/Models/StoryPosition.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using PlotLine.ViewModels;
|
||||
|
||||
namespace PlotLine.Models;
|
||||
|
||||
public sealed record StoryPosition(int ProjectId, int? BookId = null, int? ChapterId = null, int? SceneId = null)
|
||||
{
|
||||
public bool IsValid => ProjectId > 0 && (BookId is null or > 0) && (ChapterId is null or > 0) && (SceneId is null or > 0);
|
||||
public bool IsScenePosition => SceneId.HasValue;
|
||||
|
||||
public static StoryPosition? FromScene(SceneEditViewModel? scene)
|
||||
{
|
||||
if (scene?.Project is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StoryPosition(scene.Project.ProjectID, scene.Book?.BookID, scene.ChapterID, scene.SceneID > 0 ? scene.SceneID : null);
|
||||
}
|
||||
|
||||
public static StoryPosition? FromScene(Scene? scene, int projectId)
|
||||
{
|
||||
if (scene is null || projectId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StoryPosition(projectId, scene.BookID > 0 ? scene.BookID : null, scene.ChapterID, scene.SceneID);
|
||||
}
|
||||
|
||||
public static StoryPosition? FromRuntimeCurrentScene(WordCompanionRuntimeCurrentSceneRequest request)
|
||||
{
|
||||
var position = new StoryPosition(request.ProjectId, request.BookId, request.ChapterId, request.SceneId);
|
||||
return position.IsValid ? position : null;
|
||||
}
|
||||
|
||||
public static StoryPosition? FromRuntimeCurrentScene(WordCompanionRuntimeCurrentSceneResponse? response)
|
||||
{
|
||||
if (response is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var position = new StoryPosition(response.ProjectId, response.BookId, response.ChapterId, response.SceneId);
|
||||
return position.IsValid ? position : null;
|
||||
}
|
||||
|
||||
public static StoryPosition? FromTimelineSelection(int projectId, int? bookId, int? chapterId, int? sceneId)
|
||||
{
|
||||
if (projectId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var position = new StoryPosition(projectId, bookId, chapterId, sceneId);
|
||||
return position.IsValid ? position : null;
|
||||
}
|
||||
|
||||
public string? SceneEditUrl => SceneId.HasValue ? $"/Scenes/Edit/{SceneId.Value}" : null;
|
||||
}
|
||||
@ -167,6 +167,7 @@ public class Program
|
||||
builder.Services.AddScoped<IChapterService, ChapterService>();
|
||||
builder.Services.AddScoped<ISceneService, SceneService>();
|
||||
builder.Services.AddScoped<IStoryStateService, StoryStateService>();
|
||||
builder.Services.AddScoped<IStoryContextService, StoryContextService>();
|
||||
builder.Services.AddScoped<ITimelineService, TimelineService>();
|
||||
builder.Services.AddScoped<ISceneMetricTypeService, SceneMetricTypeService>();
|
||||
builder.Services.AddScoped<ISceneMetricValueService, SceneMetricValueService>();
|
||||
|
||||
@ -146,6 +146,23 @@ public interface IStoryStateService
|
||||
Task<List<ContinuityWarningAcknowledgement>> GetAcknowledgementsAsync(int projectId);
|
||||
}
|
||||
|
||||
public interface IStoryContextService
|
||||
{
|
||||
Task<StoryContextViewModel?> GetAsync(StoryPosition position);
|
||||
Task<StoryContextViewModel?> GetForSceneAsync(int sceneId, int? projectId = null, int? bookId = null);
|
||||
}
|
||||
|
||||
public sealed class StoryContextViewModel
|
||||
{
|
||||
public StoryPosition Position { get; init; } = new(0);
|
||||
public Project? Project { get; init; }
|
||||
public Book? Book { get; init; }
|
||||
public Chapter? Chapter { get; init; }
|
||||
public SceneEditViewModel? Scene { get; init; }
|
||||
public StoryStateViewModel StoryState { get; init; } = new();
|
||||
public string? CanonicalUrl => Position.SceneEditUrl;
|
||||
}
|
||||
|
||||
public interface IPlotService
|
||||
{
|
||||
Task<PlotLineListViewModel?> GetPlotLinesAsync(int projectId);
|
||||
@ -313,6 +330,7 @@ public interface IStoryBibleService
|
||||
|
||||
public interface IRelationshipMapService
|
||||
{
|
||||
Task<RelationshipMapViewModel?> GetAsync(StoryPosition position, int? focusCharacterId, bool showFullNetwork);
|
||||
Task<RelationshipMapViewModel?> GetAsync(int projectId, int? bookId, int? chapterId, int? sceneId, int? focusCharacterId, bool showFullNetwork);
|
||||
}
|
||||
|
||||
@ -2017,12 +2035,69 @@ public sealed class SceneService(
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StoryContextService(ISceneService scenes) : IStoryContextService
|
||||
{
|
||||
public async Task<StoryContextViewModel?> GetAsync(StoryPosition position)
|
||||
{
|
||||
if (!position.IsValid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (position.SceneId.HasValue)
|
||||
{
|
||||
return await GetForSceneAsync(position.SceneId.Value, position.ProjectId, position.BookId);
|
||||
}
|
||||
|
||||
return new StoryContextViewModel
|
||||
{
|
||||
Position = position
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<StoryContextViewModel?> GetForSceneAsync(int sceneId, int? projectId = null, int? bookId = null)
|
||||
{
|
||||
if (sceneId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var scene = await scenes.GetEditAsync(sceneId);
|
||||
var position = StoryPosition.FromScene(scene);
|
||||
if (position is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (projectId.HasValue && projectId.Value != position.ProjectId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bookId.HasValue && position.BookId.HasValue && bookId.Value != position.BookId.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StoryContextViewModel
|
||||
{
|
||||
Position = position,
|
||||
Project = scene!.Project,
|
||||
Book = scene.Book,
|
||||
Chapter = scene.Chapter,
|
||||
Scene = scene,
|
||||
StoryState = scene.StoryState
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TimelineService(
|
||||
ITimelineRepository timelineRepository,
|
||||
ITimelinePresetRepository presets,
|
||||
ITimelineSettingsRepository timelineSettings,
|
||||
IProjectRepository projects,
|
||||
IBookRepository books,
|
||||
IStoryContextService storyContext,
|
||||
ISceneService scenes,
|
||||
IAssetRepository assets,
|
||||
ICharacterRepository characters,
|
||||
@ -2141,9 +2216,10 @@ public sealed class TimelineService(
|
||||
var characterLanes = settings.ShowCharacterAppearances
|
||||
? await TimeTimelineLoadAsync("TimelineService.BuildCharacterLanes", () => Task.FromResult(BuildCharacterLanes(characterTimeline.Characters, characterTimeline.Appearances, orderedScenes)), projectId, bookId)
|
||||
: [];
|
||||
var selectedScene = selectedSceneId.HasValue
|
||||
? await TimeTimelineLoadAsync("SceneService.GetEditAsync", () => scenes.GetEditAsync(selectedSceneId.Value), projectId, bookId)
|
||||
var selectedContext = selectedSceneId.HasValue
|
||||
? await TimeTimelineLoadAsync("StoryContextService.GetForSceneAsync", () => storyContext.GetForSceneAsync(selectedSceneId.Value, projectId, bookId), projectId, bookId)
|
||||
: null;
|
||||
var selectedScene = selectedContext?.Scene;
|
||||
if (selectedScene is not null)
|
||||
{
|
||||
selectedScene.ReturnProjectID = projectId;
|
||||
@ -5340,6 +5416,13 @@ public sealed class StoryBibleService(IProjectRepository projects, IStoryBibleRe
|
||||
|
||||
public sealed class RelationshipMapService(IProjectRepository projects, ICharacterRepository characters) : IRelationshipMapService
|
||||
{
|
||||
public Task<RelationshipMapViewModel?> GetAsync(StoryPosition position, int? focusCharacterId, bool showFullNetwork)
|
||||
{
|
||||
return !position.IsValid
|
||||
? Task.FromResult<RelationshipMapViewModel?>(null)
|
||||
: GetAsync(position.ProjectId, position.BookId, position.ChapterId, position.SceneId, focusCharacterId, showFullNetwork);
|
||||
}
|
||||
|
||||
public async Task<RelationshipMapViewModel?> GetAsync(int projectId, int? bookId, int? chapterId, int? sceneId, int? focusCharacterId, bool showFullNetwork)
|
||||
{
|
||||
if (projectId <= 0)
|
||||
|
||||
@ -416,6 +416,7 @@ public sealed class SceneEditViewModel
|
||||
public int? ReturnProjectID { get; set; }
|
||||
public int? ReturnBookID { get; set; }
|
||||
public bool ReturnToTimeline { get; set; }
|
||||
public StoryPosition? Position => StoryPosition.FromScene(this);
|
||||
|
||||
public Chapter? Chapter { get; set; }
|
||||
public Book? Book { get; set; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user