Phase 21J.3 - Illustration Style and Prototype Final Polish
This commit is contained in:
parent
69e4ac8201
commit
ca73c9e6f3
@ -16,7 +16,7 @@ public interface IIllustrationPromptBuilder
|
|||||||
|
|
||||||
public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||||
{
|
{
|
||||||
public const string CurrentTemplateVersion = "21D.2";
|
public const string CurrentTemplateVersion = "21J.3";
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
@ -73,14 +73,15 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
|||||||
Requirements:
|
Requirements:
|
||||||
- representative visual identifier only, not a canonical depiction of any manuscript
|
- representative visual identifier only, not a canonical depiction of any manuscript
|
||||||
- modern cinematic digital illustration with premium narrative game concept art quality
|
- modern cinematic digital illustration with premium narrative game concept art quality
|
||||||
- vivid natural colour, contemporary polished rendering, realistic anatomy and believable proportions
|
- subdued atmospheric background treatment with contemporary polished rendering, realistic anatomy and believable proportions
|
||||||
- bright balanced lighting, clear facial illumination where faces are present, and strong local contrast
|
- balanced cinematic lighting, clear facial illumination where faces are present, and strong local contrast
|
||||||
- crisp subject definition with strong foreground-background separation and clean readable silhouettes
|
- crisp subject definition with strong foreground-background separation and clean readable silhouettes
|
||||||
- natural but richer colour with bright blues, reds, greens, and warm skin tones where appropriate
|
- natural controlled colour with rich but restrained accents and warm skin tones where appropriate
|
||||||
- polished promotional artwork for a premium story-driven adventure game, not a literal photograph
|
- polished promotional artwork for a premium story-driven adventure game, not a literal photograph
|
||||||
- subtle depth of field, clean modern rendering, and enough exposure to remain clear inside PlotDirector's dark navy interface
|
- subtle depth of field, clean modern rendering, and enough exposure to remain clear inside PlotDirector's dark navy interface
|
||||||
- no visible text, captions, logos, watermarks, UI, signatures, or typography
|
- no visible text, captions, logos, watermarks, UI, signatures, or typography
|
||||||
- no anime, cartoon, Pixar-like, caricature, celebrity likeness, or photoreal identity claim
|
- no anime, cartoon, Pixar-like, caricature, celebrity likeness, or photoreal identity claim
|
||||||
|
- no bright flat studio backdrop, no plain colour-block background, no stock-photo lighting, and no cartoon simplicity
|
||||||
- avoid private manuscript details, real names, excerpts, or recognisable copyrighted characters
|
- avoid private manuscript details, real names, excerpts, or recognisable copyrighted characters
|
||||||
- avoid muddy olive and brown grading, heavy sepia tones, underexposure, excessive darkness, low-contrast lighting, flat colour, antique oil-painting texture, distressed canvas texture, rough brushwork, overly sombre default expressions, backgrounds that merge into the subject, and monochrome or near-monochrome results
|
- avoid muddy olive and brown grading, heavy sepia tones, underexposure, excessive darkness, low-contrast lighting, flat colour, antique oil-painting texture, distressed canvas texture, rough brushwork, overly sombre default expressions, backgrounds that merge into the subject, and monochrome or near-monochrome results
|
||||||
- square composition that still reads clearly as a small thumbnail
|
- square composition that still reads clearly as a small thumbnail
|
||||||
@ -100,7 +101,7 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
|||||||
private static string CategoryRules(string category)
|
private static string CategoryRules(string category)
|
||||||
=> category switch
|
=> category switch
|
||||||
{
|
{
|
||||||
IllustrationLibraryCategories.Character => "Square image; head-and-shoulders or upper body; one person only; face-forward enough to read at small size; naturally expressive; cleanly lit face; distinct from background; suitable for circular cropping; avoid dark collars vanishing into black backgrounds.",
|
IllustrationLibraryCategories.Character => "Square image; head-and-shoulders or upper body; one person only; face-forward enough to read at small size; naturally expressive; cleanly lit face; distinct from a subtle atmospheric story background; suitable for circular cropping; avoid bright flat studio backgrounds, plain colour blocks, dark collars vanishing into black backgrounds, and photographic headshot styling.",
|
||||||
IllustrationLibraryCategories.Location => "One clear focal environment; vivid and readable; clearly lit and visually inviting even when dramatic; rich colour and depth; strong composition for circular and landscape crops; no prominent people; no text or readable signage; do not default every location to nighttime, rain-soaked, blue-black, ominous, or haunted-looking.",
|
IllustrationLibraryCategories.Location => "One clear focal environment; vivid and readable; clearly lit and visually inviting even when dramatic; rich colour and depth; strong composition for circular and landscape crops; no prominent people; no text or readable signage; do not default every location to nighttime, rain-soaked, blue-black, ominous, or haunted-looking.",
|
||||||
IllustrationLibraryCategories.Asset => "Square image; one dominant centred object; bright clean high-contrast presentation; crisp silhouette; clear separation from background; immediately recognisable at small size; entire object visible where practical.",
|
IllustrationLibraryCategories.Asset => "Square image; one dominant centred object; bright clean high-contrast presentation; crisp silhouette; clear separation from background; immediately recognisable at small size; entire object visible where practical.",
|
||||||
_ => string.Empty
|
_ => string.Empty
|
||||||
@ -219,7 +220,8 @@ public static class IllustrationStarterBatchDefinition
|
|||||||
{
|
{
|
||||||
["libraryPurpose"] = "Story Intelligence representative identifier",
|
["libraryPurpose"] = "Story Intelligence representative identifier",
|
||||||
["privacy"] = "generic-no-manuscript-data",
|
["privacy"] = "generic-no-manuscript-data",
|
||||||
["style"] = "editorial-stylised-realism"
|
["style"] = "editorial-stylised-realism",
|
||||||
|
["generationSource"] = "Starter batch"
|
||||||
};
|
};
|
||||||
|
|
||||||
var common = new IllustrationGenerationSpecification
|
var common = new IllustrationGenerationSpecification
|
||||||
|
|||||||
@ -238,23 +238,25 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
|||||||
Title = $"{age} {presentation.ToLowerInvariant()} character",
|
Title = $"{age} {presentation.ToLowerInvariant()} character",
|
||||||
Description = "Demand-created reusable character illustration for Story Intelligence matching.",
|
Description = "Demand-created reusable character illustration for Story Intelligence matching.",
|
||||||
VisualSubject = $"{age} {presentation.ToLowerInvariant()} character with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair and {skinTone.ToLowerInvariant()} skin tone range.",
|
VisualSubject = $"{age} {presentation.ToLowerInvariant()} character with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair and {skinTone.ToLowerInvariant()} skin tone range.",
|
||||||
Composition = "clear head-and-shoulders portrait with bright readable face, clean background separation, suitable for circular cropping",
|
Composition = "clear head-and-shoulders cinematic portrait with readable face, subtle atmospheric story background, clean separation, suitable for circular cropping",
|
||||||
Mood = "neutral, story-ready, emotionally readable",
|
Mood = "neutral, story-ready, emotionally readable, subdued and cinematic",
|
||||||
ApparentAgeBand = age,
|
ApparentAgeBand = age,
|
||||||
Presentation = presentation,
|
Presentation = presentation,
|
||||||
HairColour = hairColour,
|
HairColour = hairColour,
|
||||||
HairLength = hairLength,
|
HairLength = hairLength,
|
||||||
SkinTone = skinTone,
|
SkinTone = skinTone,
|
||||||
ClothingEra = "Contemporary",
|
ClothingEra = "Contemporary",
|
||||||
ClothingStyle = "Plain reusable story wardrobe",
|
ClothingStyle = "Understated reusable story wardrobe, not studio fashion styling",
|
||||||
Features = features,
|
Features = features,
|
||||||
Palette = ["clear blue", "warm skin tones", "fresh accent colour"],
|
Palette = ["dark navy atmosphere", "warm skin tones", "restrained amber accent"],
|
||||||
MetadataTags = ["character", "demand-generated", "story-intelligence"],
|
MetadataTags = ["character", "demand-generated", "story-intelligence"],
|
||||||
Metadata = new(StringComparer.OrdinalIgnoreCase)
|
Metadata = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
["libraryPurpose"] = "Story Intelligence demand-generated identifier",
|
["libraryPurpose"] = "Story Intelligence demand-generated identifier",
|
||||||
["privacy"] = "generic-no-manuscript-data",
|
["privacy"] = "generic-no-manuscript-data",
|
||||||
["style"] = "editorial-stylised-realism",
|
["style"] = "editorial-stylised-realism",
|
||||||
|
["styleIntent"] = "match-approved-starter-library",
|
||||||
|
["generationSource"] = "Demand generation",
|
||||||
["demandKey"] = demand.DemandKey,
|
["demandKey"] = demand.DemandKey,
|
||||||
["requestCount"] = demand.RequestCount.ToString("N0")
|
["requestCount"] = demand.RequestCount.ToString("N0")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -190,13 +190,21 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
parsedScenes);
|
parsedScenes);
|
||||||
var sourceChapterCount = (await sources.ListChaptersAsync(session.BookID)).Count;
|
var sourceChapterCount = (await sources.ListChaptersAsync(session.BookID)).Count;
|
||||||
var bookChapterTotal = Math.Max(1, Math.Max(sourceChapterCount, runSnapshots.Count));
|
var bookChapterTotal = Math.Max(1, Math.Max(sourceChapterCount, runSnapshots.Count));
|
||||||
var overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isTerminal: false);
|
|
||||||
var completedChapterRuns = runSnapshots.Count(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
var completedChapterRuns = runSnapshots.Count(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
||||||
var isTerminal = runSnapshots.All(snapshot => IsTerminal(snapshot.Run.Status));
|
var sessionSuccessfullyComplete = IsSuccessfulSession(session);
|
||||||
var isSuccessfullyComplete = runSnapshots.All(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
var allKnownRunsSuccessfullyComplete = runSnapshots.Count > 0 && runSnapshots.All(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
||||||
overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isSuccessfullyComplete);
|
var isSuccessfullyComplete = sessionSuccessfullyComplete || allKnownRunsSuccessfullyComplete;
|
||||||
|
var isTerminal = isSuccessfullyComplete || IsTerminalSession(session) || runSnapshots.All(snapshot => IsTerminal(snapshot.Run.Status));
|
||||||
|
var calculatedVisualisationProgress = OverallProgress(parsedScenes, totalScenes, isSuccessfullyComplete);
|
||||||
|
var authoritativeImportProgress = BookProgress(runSnapshots, bookChapterTotal, isSuccessfullyComplete);
|
||||||
|
var displayedProgressReason = sessionSuccessfullyComplete
|
||||||
|
? "Successful terminal import session"
|
||||||
|
: allKnownRunsSuccessfullyComplete
|
||||||
|
? "All known chapter runs completed successfully"
|
||||||
|
: "Weighted chapter-run progress";
|
||||||
|
var overallProgressPercent = authoritativeImportProgress;
|
||||||
var overallElapsed = SessionElapsed(runSnapshots);
|
var overallElapsed = SessionElapsed(runSnapshots);
|
||||||
var overallRemaining = isTerminal ? "Ready to review" : EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes);
|
var overallRemaining = isSuccessfullyComplete ? "Ready to review" : isTerminal ? "Ready to review" : EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes);
|
||||||
var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run);
|
var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run);
|
||||||
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
||||||
var nextSceneNumber = 1;
|
var nextSceneNumber = 1;
|
||||||
@ -240,7 +248,21 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ChangeToken = SessionChangeToken(session, runSnapshots),
|
ChangeToken = SessionChangeToken(session, runSnapshots),
|
||||||
IsTerminal = isTerminal,
|
IsTerminal = isTerminal,
|
||||||
SnapshotMessage = SessionSnapshotMessage(session, runSnapshots),
|
SnapshotMessage = SessionSnapshotMessage(session, runSnapshots),
|
||||||
Diagnostics = BuildSessionDiagnostics(session, runSnapshots, activeSnapshot, current, parsedScenes, totalPersistedResults, totalScenes, overallProgressPercent, storyMemory),
|
Diagnostics = BuildSessionDiagnostics(
|
||||||
|
session,
|
||||||
|
runSnapshots,
|
||||||
|
activeSnapshot,
|
||||||
|
current,
|
||||||
|
parsedScenes,
|
||||||
|
totalPersistedResults,
|
||||||
|
totalScenes,
|
||||||
|
overallProgressPercent,
|
||||||
|
authoritativeImportProgress,
|
||||||
|
calculatedVisualisationProgress,
|
||||||
|
bookChapterTotal,
|
||||||
|
completedChapterRuns,
|
||||||
|
displayedProgressReason,
|
||||||
|
storyMemory),
|
||||||
ProjectName = DisplayTitle(session.ProjectName, "Untitled project"),
|
ProjectName = DisplayTitle(session.ProjectName, "Untitled project"),
|
||||||
BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"),
|
BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"),
|
||||||
Scenes = scenes
|
Scenes = scenes
|
||||||
@ -761,6 +783,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
|
|
||||||
private static string SessionStatus(StoryIntelligenceBookPipelineState session, IReadOnlyList<RunSnapshot> snapshots)
|
private static string SessionStatus(StoryIntelligenceBookPipelineState session, IReadOnlyList<RunSnapshot> snapshots)
|
||||||
{
|
{
|
||||||
|
if (IsSuccessfulSession(session))
|
||||||
|
{
|
||||||
|
return StoryIntelligenceRunStatuses.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
if (snapshots.Any(snapshot => !IsTerminal(snapshot.Run.Status)))
|
if (snapshots.Any(snapshot => !IsTerminal(snapshot.Run.Status)))
|
||||||
{
|
{
|
||||||
return StoryIntelligenceRunStatuses.Running;
|
return StoryIntelligenceRunStatuses.Running;
|
||||||
@ -773,6 +800,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
|
|
||||||
private static string SessionSnapshotMessage(StoryIntelligenceBookPipelineState session, IReadOnlyList<RunSnapshot> snapshots)
|
private static string SessionSnapshotMessage(StoryIntelligenceBookPipelineState session, IReadOnlyList<RunSnapshot> snapshots)
|
||||||
{
|
{
|
||||||
|
if (IsSuccessfulSession(session))
|
||||||
|
{
|
||||||
|
return "Book analysis complete and ready for review.";
|
||||||
|
}
|
||||||
|
|
||||||
var active = snapshots.FirstOrDefault(snapshot => !IsTerminal(snapshot.Run.Status));
|
var active = snapshots.FirstOrDefault(snapshot => !IsTerminal(snapshot.Run.Status));
|
||||||
if (active is not null)
|
if (active is not null)
|
||||||
{
|
{
|
||||||
@ -1224,6 +1256,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
int totalPersistedResults,
|
int totalPersistedResults,
|
||||||
int totalScenes,
|
int totalScenes,
|
||||||
int overallProgressPercent,
|
int overallProgressPercent,
|
||||||
|
int authoritativeImportProgress,
|
||||||
|
int calculatedVisualisationProgress,
|
||||||
|
int totalChapters,
|
||||||
|
int completedChapters,
|
||||||
|
string displayedProgressReason,
|
||||||
StoryMemory storyMemory)
|
StoryMemory storyMemory)
|
||||||
{
|
{
|
||||||
var failed = snapshots.Sum(snapshot => snapshot.Parsed.Count(item => item.Scene is null));
|
var failed = snapshots.Sum(snapshot => snapshot.Parsed.Count(item => item.Scene is null));
|
||||||
@ -1237,10 +1274,17 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ActiveChapterRunId = activeSnapshot.Run.StoryIntelligenceRunID,
|
ActiveChapterRunId = activeSnapshot.Run.StoryIntelligenceRunID,
|
||||||
SessionStatus = session.Status,
|
SessionStatus = session.Status,
|
||||||
OverallBookProgress = overallProgressPercent,
|
OverallBookProgress = overallProgressPercent,
|
||||||
|
AuthoritativeImportProgress = authoritativeImportProgress,
|
||||||
|
CalculatedVisualisationProgress = calculatedVisualisationProgress,
|
||||||
|
TotalChapters = totalChapters,
|
||||||
|
CompletedChapters = completedChapters,
|
||||||
CurrentChapter = activeSnapshot.Run.ChapterNumber.HasValue ? $"Chapter {activeSnapshot.Run.ChapterNumber:0.##}" : "Chapter unknown",
|
CurrentChapter = activeSnapshot.Run.ChapterNumber.HasValue ? $"Chapter {activeSnapshot.Run.ChapterNumber:0.##}" : "Chapter unknown",
|
||||||
CurrentScene = CurrentSceneText(activeSnapshot),
|
CurrentScene = CurrentSceneText(activeSnapshot),
|
||||||
CompletedScenes = parsedScenes,
|
CompletedScenes = parsedScenes,
|
||||||
TotalScenes = totalScenes,
|
TotalScenes = totalScenes,
|
||||||
|
ParsedScenes = parsedScenes,
|
||||||
|
TerminalRunSessionStatus = $"{session.Status}/{SessionStatus(session, snapshots)}",
|
||||||
|
DisplayedProgressReason = displayedProgressReason,
|
||||||
ProgressSource = "ImportSession",
|
ProgressSource = "ImportSession",
|
||||||
CurrentBackendStage = BackendStageDisplay(activeSnapshot.Run),
|
CurrentBackendStage = BackendStageDisplay(activeSnapshot.Run),
|
||||||
StoryMemoryCharacterCount = storyMemory.Characters.Count,
|
StoryMemoryCharacterCount = storyMemory.Characters.Count,
|
||||||
@ -1281,19 +1325,30 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
var warning = failed.Count == 0
|
var warning = failed.Count == 0
|
||||||
? null
|
? null
|
||||||
: $"{failed.Count:N0} scene result(s) skipped because they could not be parsed.";
|
: $"{failed.Count:N0} scene result(s) skipped because they could not be parsed.";
|
||||||
|
var totalScenes = Math.Max(run.TotalDetectedScenes ?? 0, results.Count);
|
||||||
|
var completedSceneCount = run.CompletedScenes ?? completed.Count;
|
||||||
|
var runProgress = Progress(run, completedSceneCount, totalScenes);
|
||||||
|
var calculatedProgress = OverallProgress(completed.Count, totalScenes, IsSuccessfulTerminal(run.Status));
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
RequestedMode = "real",
|
RequestedMode = "real",
|
||||||
ActiveChapterRunId = run.StoryIntelligenceRunID,
|
ActiveChapterRunId = run.StoryIntelligenceRunID,
|
||||||
SessionStatus = run.Status,
|
SessionStatus = run.Status,
|
||||||
OverallBookProgress = Progress(run, run.CompletedScenes ?? completed.Count, Math.Max(run.TotalDetectedScenes ?? 0, results.Count)),
|
OverallBookProgress = runProgress,
|
||||||
|
AuthoritativeImportProgress = runProgress,
|
||||||
|
CalculatedVisualisationProgress = calculatedProgress,
|
||||||
|
TotalChapters = 1,
|
||||||
|
CompletedChapters = IsSuccessfulTerminal(run.Status) ? 1 : 0,
|
||||||
CurrentChapter = run.ChapterNumber.HasValue ? $"Chapter {run.ChapterNumber:0.##}" : "Chapter unknown",
|
CurrentChapter = run.ChapterNumber.HasValue ? $"Chapter {run.ChapterNumber:0.##}" : "Chapter unknown",
|
||||||
CurrentScene = run.TotalDetectedScenes.HasValue
|
CurrentScene = run.TotalDetectedScenes.HasValue
|
||||||
? $"Scene {Math.Clamp((run.CompletedScenes ?? completed.Count) + (IsTerminal(run.Status) ? 0 : 1), 1, run.TotalDetectedScenes.Value):N0} of {run.TotalDetectedScenes.Value:N0}"
|
? $"Scene {Math.Clamp(completedSceneCount + (IsTerminal(run.Status) ? 0 : 1), 1, run.TotalDetectedScenes.Value):N0} of {run.TotalDetectedScenes.Value:N0}"
|
||||||
: (current is null ? "Waiting" : $"Scene {SceneSortNumber(current):0.##}"),
|
: (current is null ? "Waiting" : $"Scene {SceneSortNumber(current):0.##}"),
|
||||||
CompletedScenes = run.CompletedScenes ?? completed.Count,
|
CompletedScenes = completedSceneCount,
|
||||||
TotalScenes = Math.Max(run.TotalDetectedScenes ?? 0, results.Count),
|
TotalScenes = totalScenes,
|
||||||
|
ParsedScenes = completed.Count,
|
||||||
|
TerminalRunSessionStatus = run.Status,
|
||||||
|
DisplayedProgressReason = IsSuccessfulTerminal(run.Status) ? "Successful terminal chapter run" : "Chapter run scene progress",
|
||||||
ProgressSource = "ChapterRun",
|
ProgressSource = "ChapterRun",
|
||||||
CurrentBackendStage = BackendStageDisplay(run),
|
CurrentBackendStage = BackendStageDisplay(run),
|
||||||
StoryMemoryCharacterCount = storyMemory.Characters.Count,
|
StoryMemoryCharacterCount = storyMemory.Characters.Count,
|
||||||
@ -1397,6 +1452,15 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
private static bool IsSuccessfulTerminal(string status)
|
private static bool IsSuccessfulTerminal(string status)
|
||||||
=> status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings;
|
=> status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings;
|
||||||
|
|
||||||
|
private static bool IsSuccessfulSession(StoryIntelligenceBookPipelineState session)
|
||||||
|
=> string.Equals(session.Status, StoryIntelligencePipelineStatuses.Complete, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& (string.Equals(session.LastCompletedStage, StoryIntelligencePipelineStages.KnowledgeImport, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(session.CurrentStage, StoryIntelligencePipelineStages.Complete, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| session.CompletedUtc.HasValue);
|
||||||
|
|
||||||
|
private static bool IsTerminalSession(StoryIntelligenceBookPipelineState session)
|
||||||
|
=> IsSuccessfulSession(session);
|
||||||
|
|
||||||
private static int SceneNumber(ParsedSceneResult item)
|
private static int SceneNumber(ParsedSceneResult item)
|
||||||
=> item.Scene?.SceneReference?.SceneNumber.HasValue == true ? Convert.ToInt32(item.Scene.SceneReference.SceneNumber.Value) : item.Result.TemporarySceneNumber;
|
=> item.Scene?.SceneReference?.SceneNumber.HasValue == true ? Convert.ToInt32(item.Scene.SceneReference.SceneNumber.Value) : item.Result.TemporarySceneNumber;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
Phase 21J.3 demand illustration style consistency marker.
|
||||||
|
|
||||||
|
This does not queue generation or regenerate the library. It marks existing
|
||||||
|
demand-generated character illustrations produced before template 21J.3 so the
|
||||||
|
admin can deliberately regenerate them with the corrected art direction.
|
||||||
|
*/
|
||||||
|
|
||||||
|
UPDATE dbo.IllustrationLibraryItems
|
||||||
|
SET MetadataJson = JSON_MODIFY(
|
||||||
|
JSON_MODIFY(
|
||||||
|
JSON_MODIFY(
|
||||||
|
CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END,
|
||||||
|
'$.generationSource',
|
||||||
|
N'Demand generation'),
|
||||||
|
'$.styleReview',
|
||||||
|
N'Needs 21J.3 regeneration'),
|
||||||
|
'$.styleIntent',
|
||||||
|
N'match-approved-starter-library'),
|
||||||
|
AdminNotes = CONCAT(
|
||||||
|
COALESCE(NULLIF(AdminNotes, N''), N''),
|
||||||
|
CASE WHEN NULLIF(AdminNotes, N'') IS NULL THEN N'' ELSE N' ' END,
|
||||||
|
N'Phase 21J.3: demand-generated before the 21J.3 style template; regenerate with the current template before approval.'),
|
||||||
|
UpdatedUtc = SYSUTCDATETIME()
|
||||||
|
WHERE StableCode LIKE N'char-demand-%'
|
||||||
|
AND Status IN (N'Generated', N'Approved')
|
||||||
|
AND ISNULL(PromptTemplateVersion, N'') <> N'21J.3'
|
||||||
|
AND (AdminNotes IS NULL OR AdminNotes NOT LIKE N'%Phase 21J.3:%');
|
||||||
@ -26,10 +26,17 @@ public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
|||||||
public int? ActiveChapterRunId { get; init; }
|
public int? ActiveChapterRunId { get; init; }
|
||||||
public string SessionStatus { get; init; } = string.Empty;
|
public string SessionStatus { get; init; } = string.Empty;
|
||||||
public int OverallBookProgress { get; init; }
|
public int OverallBookProgress { get; init; }
|
||||||
|
public int AuthoritativeImportProgress { get; init; }
|
||||||
|
public int CalculatedVisualisationProgress { get; init; }
|
||||||
|
public int TotalChapters { get; init; }
|
||||||
|
public int CompletedChapters { get; init; }
|
||||||
public string CurrentChapter { get; init; } = string.Empty;
|
public string CurrentChapter { get; init; } = string.Empty;
|
||||||
public string CurrentScene { get; init; } = string.Empty;
|
public string CurrentScene { get; init; } = string.Empty;
|
||||||
public int CompletedScenes { get; init; }
|
public int CompletedScenes { get; init; }
|
||||||
public int TotalScenes { get; init; }
|
public int TotalScenes { get; init; }
|
||||||
|
public int ParsedScenes { get; init; }
|
||||||
|
public string TerminalRunSessionStatus { get; init; } = string.Empty;
|
||||||
|
public string DisplayedProgressReason { get; init; } = string.Empty;
|
||||||
public string ProgressSource { get; init; } = string.Empty;
|
public string ProgressSource { get; init; } = string.Empty;
|
||||||
public string CurrentBackendStage { get; init; } = string.Empty;
|
public string CurrentBackendStage { get; init; } = string.Empty;
|
||||||
public int StoryMemoryCharacterCount { get; init; }
|
public int StoryMemoryCharacterCount { get; init; }
|
||||||
|
|||||||
@ -118,6 +118,13 @@
|
|||||||
@foreach (var item in Model.Items)
|
@foreach (var item in Model.Items)
|
||||||
{
|
{
|
||||||
var preview = !string.IsNullOrWhiteSpace(item.ThumbnailPath) ? item.ThumbnailPath : item.FilePath;
|
var preview = !string.IsNullOrWhiteSpace(item.ThumbnailPath) ? item.ThumbnailPath : item.FilePath;
|
||||||
|
var generationSource = item.StableCode.StartsWith("char-demand-", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "Demand generation"
|
||||||
|
: item.ParentItemID.HasValue
|
||||||
|
? "Manual regeneration"
|
||||||
|
: IllustrationStarterBatchDefinition.StarterCodes.Contains(item.StableCode)
|
||||||
|
? "Starter batch"
|
||||||
|
: "Manual library item";
|
||||||
<article class="project-card">
|
<article class="project-card">
|
||||||
<div class="project-card__body">
|
<div class="project-card__body">
|
||||||
<div class="d-flex justify-content-between gap-2 align-items-start">
|
<div class="d-flex justify-content-between gap-2 align-items-start">
|
||||||
@ -137,6 +144,10 @@
|
|||||||
<dd><code>@item.StableCode</code></dd>
|
<dd><code>@item.StableCode</code></dd>
|
||||||
<dt>Provider</dt>
|
<dt>Provider</dt>
|
||||||
<dd>@(item.Provider ?? "Not generated") @(item.Model is null ? string.Empty : $" / {item.Model}")</dd>
|
<dd>@(item.Provider ?? "Not generated") @(item.Model is null ? string.Empty : $" / {item.Model}")</dd>
|
||||||
|
<dt>Template</dt>
|
||||||
|
<dd>@(item.PromptTemplateVersion ?? "Not generated")</dd>
|
||||||
|
<dt>Source</dt>
|
||||||
|
<dd>@generationSource</dd>
|
||||||
<dt>Updated</dt>
|
<dt>Updated</dt>
|
||||||
<dd>@item.UpdatedUtc.ToString("yyyy-MM-dd HH:mm") UTC</dd>
|
<dd>@item.UpdatedUtc.ToString("yyyy-MM-dd HH:mm") UTC</dd>
|
||||||
@if (item.ApprovedUtc.HasValue)
|
@if (item.ApprovedUtc.HasValue)
|
||||||
|
|||||||
@ -212,6 +212,22 @@
|
|||||||
<dt>Overall book progress</dt>
|
<dt>Overall book progress</dt>
|
||||||
<dd data-diagnostic-key="overallBookProgress">@Model.Diagnostics.OverallBookProgress</dd>
|
<dd data-diagnostic-key="overallBookProgress">@Model.Diagnostics.OverallBookProgress</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Authoritative import progress</dt>
|
||||||
|
<dd data-diagnostic-key="authoritativeImportProgress">@Model.Diagnostics.AuthoritativeImportProgress</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Calculated visualisation progress</dt>
|
||||||
|
<dd data-diagnostic-key="calculatedVisualisationProgress">@Model.Diagnostics.CalculatedVisualisationProgress</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Total chapters</dt>
|
||||||
|
<dd data-diagnostic-key="totalChapters">@Model.Diagnostics.TotalChapters</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Completed chapters</dt>
|
||||||
|
<dd data-diagnostic-key="completedChapters">@Model.Diagnostics.CompletedChapters</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Current chapter</dt>
|
<dt>Current chapter</dt>
|
||||||
<dd data-diagnostic-key="currentChapter">@Model.Diagnostics.CurrentChapter</dd>
|
<dd data-diagnostic-key="currentChapter">@Model.Diagnostics.CurrentChapter</dd>
|
||||||
@ -228,6 +244,18 @@
|
|||||||
<dt>Total scenes</dt>
|
<dt>Total scenes</dt>
|
||||||
<dd data-diagnostic-key="totalScenes">@Model.Diagnostics.TotalScenes</dd>
|
<dd data-diagnostic-key="totalScenes">@Model.Diagnostics.TotalScenes</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Parsed scenes</dt>
|
||||||
|
<dd data-diagnostic-key="parsedScenes">@Model.Diagnostics.ParsedScenes</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Terminal run/session status</dt>
|
||||||
|
<dd data-diagnostic-key="terminalRunSessionStatus">@Model.Diagnostics.TerminalRunSessionStatus</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Displayed progress reason</dt>
|
||||||
|
<dd data-diagnostic-key="displayedProgressReason">@Model.Diagnostics.DisplayedProgressReason</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Progress source</dt>
|
<dt>Progress source</dt>
|
||||||
<dd data-diagnostic-key="progressSource">@Model.Diagnostics.ProgressSource</dd>
|
<dd data-diagnostic-key="progressSource">@Model.Diagnostics.ProgressSource</dd>
|
||||||
|
|||||||
@ -124,7 +124,7 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-stage {
|
.story-exp-stage {
|
||||||
grid-template-rows: auto minmax(0, 1fr) 126px;
|
grid-template-rows: auto minmax(0, 1fr) 170px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-stage-copy {
|
.story-exp-stage-copy {
|
||||||
@ -133,24 +133,28 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-ribbon-wrap {
|
.story-exp-ribbon-wrap {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px;
|
grid-template-rows: minmax(92px, auto) 46px;
|
||||||
|
padding: 10px 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-ribbon {
|
.story-exp-ribbon {
|
||||||
min-height: 70px;
|
gap: 12px;
|
||||||
|
min-height: 92px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card {
|
.story-scene-card {
|
||||||
flex-basis: 126px;
|
flex-basis: clamp(132px, 14vw, 168px);
|
||||||
padding: 8px 10px;
|
min-height: 84px;
|
||||||
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card.is-current {
|
.story-scene-card.is-current {
|
||||||
flex-basis: 172px;
|
flex-basis: clamp(188px, 18vw, 224px);
|
||||||
|
min-height: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-timeline {
|
.story-exp-timeline {
|
||||||
min-height: 34px;
|
min-height: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-controls {
|
.story-exp-controls {
|
||||||
@ -160,6 +164,20 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1400px) and (min-width: 1181px) {
|
||||||
|
.story-exp-ribbon {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-card {
|
||||||
|
flex-basis: clamp(132px, 11vw, 160px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-card.is-current {
|
||||||
|
flex-basis: clamp(188px, 15vw, 220px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.story-exp-brand strong {
|
.story-exp-brand strong {
|
||||||
font-size: 1.12rem;
|
font-size: 1.12rem;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
@ -482,7 +500,7 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-stage {
|
.story-exp-stage {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr) 154px;
|
grid-template-rows: auto minmax(0, 1fr) 186px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
@ -917,12 +935,12 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-ribbon-wrap {
|
.story-exp-ribbon-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: minmax(76px, auto) 54px;
|
grid-template-rows: minmax(104px, auto) 56px;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 1px solid rgba(151, 185, 218, 0.1);
|
border: 1px solid rgba(151, 185, 218, 0.1);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
padding: 12px 14px 14px;
|
padding: 14px 16px 16px;
|
||||||
background: rgba(6, 16, 30, 0.54);
|
background: rgba(6, 16, 30, 0.54);
|
||||||
box-shadow: 0 20px 58px rgba(0, 0, 0, 0.22);
|
box-shadow: 0 20px 58px rgba(0, 0, 0, 0.22);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
@ -933,24 +951,25 @@ input:focus-visible {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
min-height: 76px;
|
min-height: 104px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card {
|
.story-scene-card {
|
||||||
flex: 0 0 136px;
|
flex: 0 0 clamp(174px, 16vw, 218px);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
gap: 5px;
|
gap: 7px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
border-radius: 15px;
|
min-height: 94px;
|
||||||
padding: 9px 10px;
|
border-radius: 14px;
|
||||||
|
padding: 12px 14px;
|
||||||
background: rgba(255, 255, 255, 0.032);
|
background: rgba(255, 255, 255, 0.032);
|
||||||
color: var(--story-muted);
|
color: var(--story-muted);
|
||||||
opacity: 0.68;
|
opacity: 0.68;
|
||||||
transform: scale(0.91) translateY(4px);
|
transform: scale(0.96) translateY(3px);
|
||||||
transition: transform 840ms cubic-bezier(.16, .84, .28, 1), opacity 760ms ease, background 760ms ease, border-color 760ms ease, box-shadow 760ms ease;
|
transition: transform 840ms cubic-bezier(.16, .84, .28, 1), opacity 760ms ease, background 760ms ease, border-color 760ms ease, box-shadow 760ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -959,12 +978,13 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card.is-current {
|
.story-scene-card.is-current {
|
||||||
flex-basis: 186px;
|
flex-basis: clamp(224px, 20vw, 280px);
|
||||||
|
min-height: 106px;
|
||||||
border-color: rgba(215, 168, 93, 0.64);
|
border-color: rgba(215, 168, 93, 0.64);
|
||||||
background: linear-gradient(180deg, rgba(215, 168, 93, 0.18), rgba(109, 181, 255, 0.075));
|
background: linear-gradient(180deg, rgba(215, 168, 93, 0.18), rgba(109, 181, 255, 0.075));
|
||||||
color: var(--story-text);
|
color: var(--story-text);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1.04) translateY(0);
|
transform: scale(1.02) translateY(0);
|
||||||
box-shadow: 0 18px 42px rgba(215, 168, 93, 0.16);
|
box-shadow: 0 18px 42px rgba(215, 168, 93, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -972,28 +992,28 @@ input:focus-visible {
|
|||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-size: 0.86rem;
|
font-size: 0.92rem;
|
||||||
line-height: 1.12;
|
line-height: 1.16;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card span {
|
.story-scene-card span {
|
||||||
color: var(--story-muted);
|
color: var(--story-muted);
|
||||||
font-size: 0.66rem;
|
font-size: 0.68rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card small {
|
.story-scene-card small {
|
||||||
display: -webkit-box;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--story-muted);
|
color: var(--story-muted);
|
||||||
font-size: 0.68rem;
|
font-size: 0.7rem;
|
||||||
line-height: 1.1;
|
line-height: 1.15;
|
||||||
-webkit-box-orient: vertical;
|
text-overflow: ellipsis;
|
||||||
-webkit-line-clamp: 1;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-timeline {
|
.story-exp-timeline {
|
||||||
@ -1001,7 +1021,7 @@ input:focus-visible {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(var(--timeline-count, 1), minmax(0, 1fr));
|
grid-template-columns: repeat(var(--timeline-count, 1), minmax(0, 1fr));
|
||||||
gap: 0;
|
gap: 0;
|
||||||
min-height: 54px;
|
min-height: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-timeline::before {
|
.story-exp-timeline::before {
|
||||||
|
|||||||
@ -352,9 +352,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateSceneRibbon(scene) {
|
function updateSceneRibbon(scene) {
|
||||||
const currentIndex = state.index;
|
const visible = visibleSceneWindow();
|
||||||
const start = Math.max(0, Math.min(scenes.length - 7, currentIndex - 3));
|
|
||||||
const visible = scenes.slice(start, start + 7);
|
|
||||||
const activeIds = new Set(visible.map((item) => item.id));
|
const activeIds = new Set(visible.map((item) => item.id));
|
||||||
visible.forEach((item) => {
|
visible.forEach((item) => {
|
||||||
let card = dom.ribbon.querySelector(`[data-scene-id="${cssEscape(item.id)}"]`);
|
let card = dom.ribbon.querySelector(`[data-scene-id="${cssEscape(item.id)}"]`);
|
||||||
@ -367,19 +365,19 @@
|
|||||||
}
|
}
|
||||||
card.classList.toggle("is-current", item.id === scene.id);
|
card.classList.toggle("is-current", item.id === scene.id);
|
||||||
card.classList.toggle("is-complete", item.sceneNumber < scene.sceneNumber);
|
card.classList.toggle("is-complete", item.sceneNumber < scene.sceneNumber);
|
||||||
text(card.querySelector("span"), `${(item.chapterLabel || "Chapter").toUpperCase()} · SCENE ${item.sceneNumber}`);
|
text(card.querySelector("span"), `${abbreviateChapter(item.chapterLabel)} · Scene ${item.sceneNumber}`);
|
||||||
text(card.querySelector("strong"), compactPhrase(cleanSceneTitle(item.title), item.id === scene.id ? 44 : 30));
|
text(card.querySelector("strong"), compactPhrase(cleanSceneTitle(item.title), 54));
|
||||||
text(card.querySelector("small"), compactPhrase(item.timelineEvent, 34));
|
text(card.querySelector("small"), compactPhrase(item.timelineEvent || item.summary, 48));
|
||||||
dom.ribbon.append(card);
|
dom.ribbon.append(card);
|
||||||
});
|
});
|
||||||
removeInactive(dom.ribbon, "[data-scene-id]", "sceneId", activeIds);
|
removeInactive(dom.ribbon, "[data-scene-id]", "sceneId", activeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTimeline(scene) {
|
function updateTimeline(scene) {
|
||||||
const completed = scenes.slice(0, state.index + 1);
|
const visible = visibleSceneWindow();
|
||||||
dom.timeline.style.setProperty("--timeline-count", String(completed.length));
|
dom.timeline.style.setProperty("--timeline-count", String(visible.length));
|
||||||
const activeIds = new Set(completed.map((item) => item.id));
|
const activeIds = new Set(visible.map((item) => item.id));
|
||||||
completed.forEach((item) => {
|
visible.forEach((item) => {
|
||||||
let marker = dom.timeline.querySelector(`[data-timeline-id="${cssEscape(item.id)}"]`);
|
let marker = dom.timeline.querySelector(`[data-timeline-id="${cssEscape(item.id)}"]`);
|
||||||
if (!marker) {
|
if (!marker) {
|
||||||
marker = document.createElement("div");
|
marker = document.createElement("div");
|
||||||
@ -389,19 +387,19 @@
|
|||||||
settleEntering(marker);
|
settleEntering(marker);
|
||||||
}
|
}
|
||||||
marker.classList.toggle("is-current", item.id === scene.id);
|
marker.classList.toggle("is-current", item.id === scene.id);
|
||||||
const shouldLabel = shouldShowTimelineLabel(completed, item, scene);
|
marker.classList.toggle("is-complete", item.sceneNumber < scene.sceneNumber);
|
||||||
marker.classList.toggle("is-label-hidden", !shouldLabel);
|
marker.classList.remove("is-label-hidden");
|
||||||
text(marker.querySelector("span"), shortTimelineLabel(item));
|
text(marker.querySelector("span"), shortTimelineLabel(item));
|
||||||
dom.timeline.append(marker);
|
dom.timeline.append(marker);
|
||||||
});
|
});
|
||||||
removeInactive(dom.timeline, "[data-timeline-id]", "timelineId", activeIds);
|
removeInactive(dom.timeline, "[data-timeline-id]", "timelineId", activeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldShowTimelineLabel(items, item, scene) {
|
function visibleSceneWindow() {
|
||||||
const index = items.findIndex((candidate) => candidate.id === item.id);
|
const visibleCount = Math.min(5, scenes.length);
|
||||||
if (index === 0 || index === items.length - 1 || item.id === scene.id) return true;
|
const currentIndex = Math.max(0, Math.min(state.index, scenes.length - 1));
|
||||||
const interval = items.length > 12 ? 5 : items.length > 8 ? 4 : 3;
|
const start = Math.max(0, Math.min(scenes.length - visibleCount, currentIndex - Math.floor(visibleCount / 2)));
|
||||||
return index % interval === 0 && Math.abs(item.sceneNumber - scene.sceneNumber) > 1;
|
return scenes.slice(start, start + visibleCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortTimelineLabel(item) {
|
function shortTimelineLabel(item) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user