Phase 21J.1 - Story Intelligence Visualisation Fixes
This commit is contained in:
parent
4ebbfdab92
commit
b513bd7df0
@ -279,7 +279,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
foreach (var item in completed)
|
foreach (var item in completed)
|
||||||
{
|
{
|
||||||
var scene = item.Scene!;
|
var scene = item.Scene!;
|
||||||
storyMemory.Update(scene);
|
storyMemory.Update(scene, item);
|
||||||
var sourceSceneNumber = SceneNumber(item);
|
var sourceSceneNumber = SceneNumber(item);
|
||||||
var sceneNumber = continuousSceneNumbering ? continuousSceneNumberStart + displayIndex++ : sourceSceneNumber;
|
var sceneNumber = continuousSceneNumbering ? continuousSceneNumberStart + displayIndex++ : sourceSceneNumber;
|
||||||
var isCurrent = current?.Result.SceneResultID == item.Result.SceneResultID;
|
var isCurrent = current?.Result.SceneResultID == item.Result.SceneResultID;
|
||||||
@ -786,35 +786,60 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
{
|
{
|
||||||
private readonly Dictionary<string, MemoryCharacter> characters = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, MemoryCharacter> characters = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly Dictionary<string, StoryIntelligenceExperienceRelationshipState> relationships = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, StoryIntelligenceExperienceRelationshipState> relationships = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly Dictionary<string, PovResolution> chapterPov = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private string? narratorResolvedName;
|
private string? narratorResolvedName;
|
||||||
|
private string? bookNarratorName;
|
||||||
|
private string? previousPovName;
|
||||||
|
private string? previousChapterKey;
|
||||||
|
|
||||||
public IReadOnlyCollection<MemoryCharacter> Characters => characters.Values;
|
public IReadOnlyCollection<MemoryCharacter> Characters => characters.Values;
|
||||||
public IReadOnlyCollection<StoryIntelligenceExperienceRelationshipState> Relationships => relationships.Values;
|
public IReadOnlyCollection<StoryIntelligenceExperienceRelationshipState> Relationships => relationships.Values;
|
||||||
public decimal NarratorConfidence { get; private set; }
|
public decimal NarratorConfidence { get; private set; }
|
||||||
|
public string RawScenePov { get; private set; } = string.Empty;
|
||||||
|
public string InheritedChapterPov { get; private set; } = string.Empty;
|
||||||
|
public string ResolvedPovIdentity { get; private set; } = string.Empty;
|
||||||
|
public string PovResolutionReason { get; private set; } = string.Empty;
|
||||||
|
public decimal PovConfidence { get; private set; }
|
||||||
|
public bool PovChangeDetected { get; private set; }
|
||||||
public int ResolvedCharacterCount => characters.Values.Count(character => character.IsResolved);
|
public int ResolvedCharacterCount => characters.Values.Count(character => character.IsResolved);
|
||||||
public int UnresolvedCharacterCount => characters.Values.Count(character => !character.IsResolved);
|
public int UnresolvedCharacterCount => characters.Values.Count(character => !character.IsResolved);
|
||||||
|
private string CurrentChapterKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
public void Update(SceneIntelligenceScene scene)
|
public void Update(SceneIntelligenceScene scene, ParsedSceneResult result)
|
||||||
{
|
{
|
||||||
|
CurrentChapterKey = ChapterMemoryKey(scene, result);
|
||||||
|
RawScenePov = Clean(scene.PointOfView?.CharacterName);
|
||||||
|
InheritedChapterPov = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name ?? string.Empty;
|
||||||
|
PovChangeDetected = false;
|
||||||
var resolvedNarrator = ResolveNarratorName(scene);
|
var resolvedNarrator = ResolveNarratorName(scene);
|
||||||
if (!string.IsNullOrWhiteSpace(resolvedNarrator))
|
if (!string.IsNullOrWhiteSpace(resolvedNarrator))
|
||||||
{
|
{
|
||||||
narratorResolvedName = resolvedNarrator;
|
narratorResolvedName = resolvedNarrator;
|
||||||
|
bookNarratorName ??= resolvedNarrator;
|
||||||
NarratorConfidence = Math.Max(NarratorConfidence, 0.82m);
|
NarratorConfidence = Math.Max(NarratorConfidence, 0.82m);
|
||||||
|
SetChapterPov(CurrentChapterKey, resolvedNarrator, 0.86m, "explicit narrator identity in current scene", result);
|
||||||
}
|
}
|
||||||
else if (IsGenericNarratorName(scene.PointOfView?.CharacterName))
|
else if (IsGenericNarratorName(scene.PointOfView?.CharacterName))
|
||||||
{
|
{
|
||||||
NarratorConfidence = Math.Max(NarratorConfidence, 0.35m);
|
NarratorConfidence = Math.Max(NarratorConfidence, 0.35m);
|
||||||
}
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(RawScenePov))
|
||||||
|
{
|
||||||
|
var existing = chapterPov.GetValueOrDefault(CurrentChapterKey);
|
||||||
|
PovChangeDetected = existing is not null && !string.Equals(existing.Name, RawScenePov, StringComparison.OrdinalIgnoreCase);
|
||||||
|
bookNarratorName ??= RawScenePov;
|
||||||
|
SetChapterPov(CurrentChapterKey, RawScenePov, 0.92m, PovChangeDetected ? "explicit named POV change in current scene" : "explicit named POV in current scene", result);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var character in scene.Characters ?? [])
|
foreach (var character in scene.Characters ?? [])
|
||||||
{
|
{
|
||||||
var name = Clean(character.Name);
|
var name = Clean(character.Name);
|
||||||
if (string.IsNullOrWhiteSpace(name) || IsGenericNarratorName(name))
|
if (string.IsNullOrWhiteSpace(name) || IsGenericNarratorName(name))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(narratorResolvedName))
|
var inheritedPov = ResolvePointOfView(scene);
|
||||||
|
if (!string.IsNullOrWhiteSpace(inheritedPov))
|
||||||
{
|
{
|
||||||
name = narratorResolvedName;
|
name = inheritedPov;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -862,6 +887,9 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
Weight = ConfidenceWeight(relationship.Confidence, 64)
|
Weight = ConfidenceWeight(relationship.Confidence, 64)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousPovName = ResolvePointOfView(scene);
|
||||||
|
previousChapterKey = CurrentChapterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? ResolvePointOfView(SceneIntelligenceScene scene)
|
public string? ResolvePointOfView(SceneIntelligenceScene scene)
|
||||||
@ -869,16 +897,51 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
var raw = Clean(scene.PointOfView?.CharacterName);
|
var raw = Clean(scene.PointOfView?.CharacterName);
|
||||||
if (IsGenericNarratorName(raw))
|
if (IsGenericNarratorName(raw))
|
||||||
{
|
{
|
||||||
return narratorResolvedName ?? raw;
|
var resolved = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name
|
||||||
|
?? (string.Equals(previousChapterKey, CurrentChapterKey, StringComparison.OrdinalIgnoreCase) ? previousPovName : null)
|
||||||
|
?? bookNarratorName
|
||||||
|
?? narratorResolvedName;
|
||||||
|
ResolvedPovIdentity = resolved ?? raw;
|
||||||
|
PovResolutionReason = resolved is null ? "unresolved narrator" : "inherited resolved chapter POV";
|
||||||
|
PovConfidence = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Confidence ?? (resolved is null ? 0.35m : 0.74m);
|
||||||
|
return resolved ?? raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(raw) ? narratorResolvedName : raw;
|
var fallback = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name
|
||||||
|
?? (string.Equals(previousChapterKey, CurrentChapterKey, StringComparison.OrdinalIgnoreCase) ? previousPovName : null)
|
||||||
|
?? bookNarratorName
|
||||||
|
?? narratorResolvedName;
|
||||||
|
ResolvedPovIdentity = string.IsNullOrWhiteSpace(raw) ? fallback ?? string.Empty : raw;
|
||||||
|
PovResolutionReason = string.IsNullOrWhiteSpace(raw) ? "inherited previous POV" : "explicit named POV";
|
||||||
|
PovConfidence = string.IsNullOrWhiteSpace(raw) ? 0.72m : 0.92m;
|
||||||
|
return string.IsNullOrWhiteSpace(raw) ? fallback : raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetChapterPov(string chapterKey, string name, decimal confidence, string reason, ParsedSceneResult result)
|
||||||
|
{
|
||||||
|
chapterPov[chapterKey] = new PovResolution(name, confidence, reason, result.Result.SceneResultID);
|
||||||
|
InheritedChapterPov = name;
|
||||||
|
ResolvedPovIdentity = name;
|
||||||
|
PovResolutionReason = reason;
|
||||||
|
PovConfidence = confidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ChapterMemoryKey(SceneIntelligenceScene scene, ParsedSceneResult result)
|
||||||
|
=> (scene.SceneReference?.ChapterNumber ?? SceneSortChapter(result)).ToString("0.##");
|
||||||
|
|
||||||
private string ResolveCharacterName(string? raw)
|
private string ResolveCharacterName(string? raw)
|
||||||
{
|
{
|
||||||
var name = Clean(raw);
|
var name = Clean(raw);
|
||||||
return IsGenericNarratorName(name) && !string.IsNullOrWhiteSpace(narratorResolvedName) ? narratorResolvedName! : name;
|
if (!IsGenericNarratorName(name))
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name
|
||||||
|
?? (string.Equals(previousChapterKey, CurrentChapterKey, StringComparison.OrdinalIgnoreCase) ? previousPovName : null)
|
||||||
|
?? bookNarratorName
|
||||||
|
?? narratorResolvedName
|
||||||
|
?? name;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MemoryCharacter GetOrAddCharacter(string name)
|
private MemoryCharacter GetOrAddCharacter(string name)
|
||||||
@ -955,6 +1018,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
public bool IsResolved => !IsGenericNarratorName(Name);
|
public bool IsResolved => !IsGenericNarratorName(Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record PovResolution(string Name, decimal Confidence, string Reason, int LastConfirmedSceneResultId);
|
||||||
|
|
||||||
private static string CurrentSceneText(RunSnapshot snapshot)
|
private static string CurrentSceneText(RunSnapshot snapshot)
|
||||||
{
|
{
|
||||||
if (snapshot.Run.TotalDetectedScenes.HasValue)
|
if (snapshot.Run.TotalDetectedScenes.HasValue)
|
||||||
@ -1019,6 +1084,12 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ResolvedCharacterCount = storyMemory.ResolvedCharacterCount,
|
ResolvedCharacterCount = storyMemory.ResolvedCharacterCount,
|
||||||
UnresolvedCharacterCount = storyMemory.UnresolvedCharacterCount,
|
UnresolvedCharacterCount = storyMemory.UnresolvedCharacterCount,
|
||||||
NarratorConfidence = storyMemory.NarratorConfidence,
|
NarratorConfidence = storyMemory.NarratorConfidence,
|
||||||
|
RawScenePov = storyMemory.RawScenePov,
|
||||||
|
InheritedChapterPov = storyMemory.InheritedChapterPov,
|
||||||
|
ResolvedPovIdentity = storyMemory.ResolvedPovIdentity,
|
||||||
|
PovResolutionReason = storyMemory.PovResolutionReason,
|
||||||
|
PovConfidence = storyMemory.PovConfidence,
|
||||||
|
PovChangeDetected = storyMemory.PovChangeDetected,
|
||||||
SnapshotSource = "Real",
|
SnapshotSource = "Real",
|
||||||
RunStatus = SessionStatus(session, snapshots),
|
RunStatus = SessionStatus(session, snapshots),
|
||||||
PersistedCompletedSceneCount = snapshots.Sum(snapshot => snapshot.Run.CompletedScenes ?? snapshot.Completed.Count),
|
PersistedCompletedSceneCount = snapshots.Sum(snapshot => snapshot.Run.CompletedScenes ?? snapshot.Completed.Count),
|
||||||
@ -1066,6 +1137,12 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ResolvedCharacterCount = storyMemory.ResolvedCharacterCount,
|
ResolvedCharacterCount = storyMemory.ResolvedCharacterCount,
|
||||||
UnresolvedCharacterCount = storyMemory.UnresolvedCharacterCount,
|
UnresolvedCharacterCount = storyMemory.UnresolvedCharacterCount,
|
||||||
NarratorConfidence = storyMemory.NarratorConfidence,
|
NarratorConfidence = storyMemory.NarratorConfidence,
|
||||||
|
RawScenePov = storyMemory.RawScenePov,
|
||||||
|
InheritedChapterPov = storyMemory.InheritedChapterPov,
|
||||||
|
ResolvedPovIdentity = storyMemory.ResolvedPovIdentity,
|
||||||
|
PovResolutionReason = storyMemory.PovResolutionReason,
|
||||||
|
PovConfidence = storyMemory.PovConfidence,
|
||||||
|
PovChangeDetected = storyMemory.PovChangeDetected,
|
||||||
SnapshotSource = "Real",
|
SnapshotSource = "Real",
|
||||||
RunStatus = run.Status,
|
RunStatus = run.Status,
|
||||||
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
||||||
|
|||||||
@ -38,6 +38,12 @@ public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
|||||||
public decimal NarratorConfidence { get; init; }
|
public decimal NarratorConfidence { get; init; }
|
||||||
public int IllustrationAssignmentCount { get; set; }
|
public int IllustrationAssignmentCount { get; set; }
|
||||||
public int IllustrationChangeCount { get; set; }
|
public int IllustrationChangeCount { get; set; }
|
||||||
|
public string RawScenePov { get; init; } = string.Empty;
|
||||||
|
public string InheritedChapterPov { get; init; } = string.Empty;
|
||||||
|
public string ResolvedPovIdentity { get; init; } = string.Empty;
|
||||||
|
public string PovResolutionReason { get; init; } = string.Empty;
|
||||||
|
public decimal PovConfidence { get; init; }
|
||||||
|
public bool PovChangeDetected { get; init; }
|
||||||
public string SnapshotSource { get; init; } = string.Empty;
|
public string SnapshotSource { get; init; } = string.Empty;
|
||||||
public string RunStatus { get; init; } = string.Empty;
|
public string RunStatus { get; init; } = string.Empty;
|
||||||
public int PersistedCompletedSceneCount { get; init; }
|
public int PersistedCompletedSceneCount { get; init; }
|
||||||
|
|||||||
@ -44,7 +44,6 @@
|
|||||||
<strong>PlotDirector</strong>
|
<strong>PlotDirector</strong>
|
||||||
<span>Story Intelligence</span>
|
<span>Story Intelligence</span>
|
||||||
</div>
|
</div>
|
||||||
<ol class="story-exp-pipeline" data-analysis-stages aria-label="Analysis stages"></ol>
|
|
||||||
<div class="story-exp-context">
|
<div class="story-exp-context">
|
||||||
<span data-experience-status>@statusLabel</span>
|
<span data-experience-status>@statusLabel</span>
|
||||||
<span data-project-name>@Model.ProjectName</span>
|
<span data-project-name>@Model.ProjectName</span>
|
||||||
@ -253,6 +252,30 @@
|
|||||||
<dt>Narrator confidence</dt>
|
<dt>Narrator confidence</dt>
|
||||||
<dd data-diagnostic-key="narratorConfidence">@Model.Diagnostics.NarratorConfidence</dd>
|
<dd data-diagnostic-key="narratorConfidence">@Model.Diagnostics.NarratorConfidence</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Raw scene POV</dt>
|
||||||
|
<dd data-diagnostic-key="rawScenePov">@Model.Diagnostics.RawScenePov</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Inherited chapter POV</dt>
|
||||||
|
<dd data-diagnostic-key="inheritedChapterPov">@Model.Diagnostics.InheritedChapterPov</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Resolved POV identity</dt>
|
||||||
|
<dd data-diagnostic-key="resolvedPovIdentity">@Model.Diagnostics.ResolvedPovIdentity</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>POV resolution reason</dt>
|
||||||
|
<dd data-diagnostic-key="povResolutionReason">@Model.Diagnostics.PovResolutionReason</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>POV confidence</dt>
|
||||||
|
<dd data-diagnostic-key="povConfidence">@Model.Diagnostics.PovConfidence</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>POV change detected</dt>
|
||||||
|
<dd data-diagnostic-key="povChangeDetected">@Model.Diagnostics.PovChangeDetected</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Illustration assignments</dt>
|
<dt>Illustration assignments</dt>
|
||||||
<dd data-diagnostic-key="illustrationAssignmentCount">@Model.Diagnostics.IllustrationAssignmentCount</dd>
|
<dd data-diagnostic-key="illustrationAssignmentCount">@Model.Diagnostics.IllustrationAssignmentCount</dd>
|
||||||
|
|||||||
@ -54,7 +54,7 @@ input:focus-visible {
|
|||||||
.story-exp {
|
.story-exp {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 66px minmax(0, 1fr);
|
grid-template-rows: 56px minmax(0, 1fr);
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
@ -87,10 +87,10 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-header {
|
.story-exp-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(210px, 0.52fr) minmax(400px, 1.35fr) minmax(290px, 0.78fr) auto;
|
grid-template-columns: minmax(210px, auto) minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 18px;
|
gap: 16px;
|
||||||
padding: 10px 24px;
|
padding: 8px 24px;
|
||||||
border-bottom: 1px solid var(--story-line);
|
border-bottom: 1px solid var(--story-line);
|
||||||
background: rgba(4, 10, 20, 0.68);
|
background: rgba(4, 10, 20, 0.68);
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
@ -102,88 +102,12 @@ input:focus-visible {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-pipeline {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(11, minmax(38px, 1fr));
|
|
||||||
gap: 0;
|
|
||||||
min-width: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-pipeline::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 4%;
|
|
||||||
right: 4%;
|
|
||||||
top: 10px;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, rgba(109, 181, 255, 0.54), rgba(215, 168, 93, 0.24), rgba(255, 255, 255, 0.18));
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-pipeline-step {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
color: rgba(196, 214, 235, 0.54);
|
|
||||||
font-size: 0.62rem;
|
|
||||||
font-weight: 700;
|
|
||||||
min-width: 0;
|
|
||||||
transition: opacity 420ms ease, transform 560ms cubic-bezier(.16, .84, .28, 1), color 420ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-pipeline-step span {
|
|
||||||
z-index: 1;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border: 1px solid rgba(196, 214, 235, 0.36);
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(8, 18, 32, 0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-pipeline-step strong {
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 78px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-pipeline-step.is-complete span {
|
|
||||||
border-color: rgba(109, 181, 255, 0.9);
|
|
||||||
background: var(--story-blue);
|
|
||||||
box-shadow: 0 0 14px rgba(109, 181, 255, 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-pipeline-step.is-current {
|
|
||||||
color: var(--story-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-pipeline-step.is-current span {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
margin-top: -2px;
|
|
||||||
border-color: rgba(215, 168, 93, 0.9);
|
|
||||||
background: var(--story-gold);
|
|
||||||
box-shadow: 0 0 0 5px rgba(215, 168, 93, 0.12), 0 0 22px rgba(215, 168, 93, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1500px) {
|
@media (max-width: 1500px) {
|
||||||
.story-exp-header {
|
.story-exp-header {
|
||||||
grid-template-columns: minmax(188px, 0.44fr) minmax(360px, 1.2fr) minmax(230px, 0.66fr) auto;
|
grid-template-columns: minmax(188px, auto) minmax(0, 1fr) auto;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-pipeline-step {
|
|
||||||
font-size: 0.55rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-pipeline-step strong {
|
|
||||||
max-width: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-context {
|
.story-exp-context {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
@ -293,7 +217,7 @@ input:focus-visible {
|
|||||||
.story-exp-run-selector {
|
.story-exp-run-selector {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 86px;
|
top: 68px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@ -330,7 +254,7 @@ input:focus-visible {
|
|||||||
.story-exp-status-message {
|
.story-exp-status-message {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 86px;
|
top: 68px;
|
||||||
z-index: 19;
|
z-index: 19;
|
||||||
width: min(720px, calc(100vw - 48px));
|
width: min(720px, calc(100vw - 48px));
|
||||||
border: 1px solid rgba(215, 168, 93, 0.32);
|
border: 1px solid rgba(215, 168, 93, 0.32);
|
||||||
@ -669,22 +593,31 @@ input:focus-visible {
|
|||||||
stroke: rgba(182, 162, 255, 0.44);
|
stroke: rgba(182, 162, 255, 0.44);
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-connection-label-bg {
|
|
||||||
fill: rgba(4, 10, 20, 0.72);
|
|
||||||
stroke: rgba(151, 185, 218, 0.12);
|
|
||||||
stroke-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-connection-label {
|
.story-exp-connection-label {
|
||||||
paint-order: stroke;
|
display: -webkit-box;
|
||||||
stroke: rgba(4, 10, 20, 0.68);
|
width: 100%;
|
||||||
stroke-width: 3px;
|
max-height: 100%;
|
||||||
fill: var(--story-soft);
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(151, 185, 218, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 7px 5px;
|
||||||
|
background: rgba(4, 10, 20, 0.86);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.32), 0 0 14px rgba(4, 10, 20, 0.55);
|
||||||
|
color: var(--story-soft);
|
||||||
font-size: 0.66rem;
|
font-size: 0.66rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
line-height: 1.15;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
text-align: center;
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
filter: drop-shadow(0 0 9px rgba(0, 0, 0, 0.5));
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-connection-label-wrap {
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-zone {
|
.story-exp-zone {
|
||||||
@ -984,11 +917,12 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-ribbon-wrap {
|
.story-exp-ribbon-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
grid-template-rows: minmax(76px, auto) 54px;
|
||||||
|
gap: 10px;
|
||||||
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: 15px;
|
padding: 12px 14px 14px;
|
||||||
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);
|
||||||
@ -1000,18 +934,19 @@ input:focus-visible {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 94px;
|
min-height: 76px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card {
|
.story-scene-card {
|
||||||
flex: 0 0 136px;
|
flex: 0 0 136px;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: space-between;
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
gap: 7px;
|
align-content: start;
|
||||||
|
gap: 5px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
padding: 10px 11px;
|
padding: 9px 10px;
|
||||||
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;
|
||||||
@ -1024,7 +959,7 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card.is-current {
|
.story-scene-card.is-current {
|
||||||
flex-basis: 194px;
|
flex-basis: 186px;
|
||||||
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);
|
||||||
@ -1034,24 +969,39 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card strong {
|
.story-scene-card strong {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
|
line-height: 1.12;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-scene-card span {
|
.story-scene-card span {
|
||||||
color: var(--story-muted);
|
color: var(--story-muted);
|
||||||
font-size: 0.72rem;
|
font-size: 0.66rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.04em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-scene-card small {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.story-exp-timeline {
|
.story-exp-timeline {
|
||||||
position: relative;
|
position: relative;
|
||||||
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: 46px;
|
min-height: 54px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-timeline::before {
|
.story-exp-timeline::before {
|
||||||
@ -1069,14 +1019,26 @@ input:focus-visible {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
color: var(--story-muted);
|
color: var(--story-muted);
|
||||||
font-size: 0.73rem;
|
font-size: 0.66rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: opacity 560ms ease, transform 720ms cubic-bezier(.16, .84, .28, 1);
|
transition: opacity 560ms ease, transform 720ms cubic-bezier(.16, .84, .28, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-timeline-marker span {
|
||||||
|
display: block;
|
||||||
|
max-width: 72px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-timeline-marker.is-label-hidden span {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.story-timeline-marker::before {
|
.story-timeline-marker::before {
|
||||||
content: "";
|
content: "";
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -1273,16 +1235,9 @@ input:focus-visible {
|
|||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-pipeline {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
grid-row: 2;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-exp-context {
|
.story-exp-context {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
grid-row: 3;
|
grid-row: 2;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,26 +36,11 @@
|
|||||||
snapshotBuildMs: 0,
|
snapshotBuildMs: 0,
|
||||||
lastRefreshTime: "Initial"
|
lastRefreshTime: "Initial"
|
||||||
};
|
};
|
||||||
const analysisStages = [
|
|
||||||
{ key: "reading", label: "Reading" },
|
|
||||||
{ key: "chapters", label: "Chapters" },
|
|
||||||
{ key: "scenes", label: "Scenes" },
|
|
||||||
{ key: "content", label: "Content" },
|
|
||||||
{ key: "characters", label: "Characters" },
|
|
||||||
{ key: "locations", label: "Locations" },
|
|
||||||
{ key: "assets", label: "Assets" },
|
|
||||||
{ key: "relationships", label: "Relationships" },
|
|
||||||
{ key: "knowledge", label: "Knowledge" },
|
|
||||||
{ key: "continuity", label: "Continuity" },
|
|
||||||
{ key: "finalising", label: "Finalising" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const dom = {
|
const dom = {
|
||||||
stageLabel: root.querySelector("[data-stage-label]"),
|
stageLabel: root.querySelector("[data-stage-label]"),
|
||||||
projectName: root.querySelector("[data-project-name]"),
|
projectName: root.querySelector("[data-project-name]"),
|
||||||
bookTitle: root.querySelector("[data-book-title]"),
|
bookTitle: root.querySelector("[data-book-title]"),
|
||||||
snapshotMessage: root.querySelector("[data-snapshot-message]"),
|
snapshotMessage: root.querySelector("[data-snapshot-message]"),
|
||||||
stageRail: root.querySelector("[data-analysis-stages]"),
|
|
||||||
currentScene: root.querySelector("[data-current-scene]"),
|
currentScene: root.querySelector("[data-current-scene]"),
|
||||||
progressPercent: root.querySelector("[data-progress-percent]"),
|
progressPercent: root.querySelector("[data-progress-percent]"),
|
||||||
elapsedTime: root.querySelector("[data-elapsed-time]"),
|
elapsedTime: root.querySelector("[data-elapsed-time]"),
|
||||||
@ -110,7 +95,6 @@
|
|||||||
logImageDiagnostics(current);
|
logImageDiagnostics(current);
|
||||||
|
|
||||||
updateProgress(current);
|
updateProgress(current);
|
||||||
updateAnalysisStages(current);
|
|
||||||
updateSceneCopy(current);
|
updateSceneCopy(current);
|
||||||
const layout = sceneLayout(current);
|
const layout = sceneLayout(current);
|
||||||
reconcileCharacters(current, previous, layout);
|
reconcileCharacters(current, previous, layout);
|
||||||
@ -168,45 +152,6 @@
|
|||||||
if (dom.progressRing) dom.progressRing.style.setProperty("--progress-value", `${scene.progressPercent * 3.6}deg`);
|
if (dom.progressRing) dom.progressRing.style.setProperty("--progress-value", `${scene.progressPercent * 3.6}deg`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAnalysisStages(scene) {
|
|
||||||
if (!dom.stageRail) return;
|
|
||||||
const currentIndex = stageIndex(scene);
|
|
||||||
const activeKeys = new Set(analysisStages.map((stage) => stage.key));
|
|
||||||
analysisStages.forEach((stage, index) => {
|
|
||||||
let node = dom.stageRail.querySelector(`[data-stage-key="${stage.key}"]`);
|
|
||||||
if (!node) {
|
|
||||||
node = document.createElement("li");
|
|
||||||
node.className = "story-exp-pipeline-step is-entering";
|
|
||||||
node.dataset.stageKey = stage.key;
|
|
||||||
node.innerHTML = "<span></span><strong></strong>";
|
|
||||||
dom.stageRail.append(node);
|
|
||||||
settleEntering(node);
|
|
||||||
}
|
|
||||||
node.classList.toggle("is-complete", index < currentIndex || scene.progressPercent >= 100);
|
|
||||||
node.classList.toggle("is-current", index === currentIndex && scene.progressPercent < 100);
|
|
||||||
node.classList.toggle("is-future", index > currentIndex && scene.progressPercent < 100);
|
|
||||||
text(node.querySelector("strong"), stage.label);
|
|
||||||
});
|
|
||||||
removeInactive(dom.stageRail, "[data-stage-key]", "stageKey", activeKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stageIndex(scene) {
|
|
||||||
const stage = String(scene.stage || "").toLowerCase();
|
|
||||||
if (scene.progressPercent >= 100 || stage.includes("review")) return analysisStages.length - 1;
|
|
||||||
if (stage.includes("final")) return 10;
|
|
||||||
if (stage.includes("continuity")) return 9;
|
|
||||||
if (stage.includes("knowledge")) return 8;
|
|
||||||
if (stage.includes("relationship")) return 7;
|
|
||||||
if (stage.includes("asset")) return 6;
|
|
||||||
if (stage.includes("location")) return 5;
|
|
||||||
if (stage.includes("character")) return 4;
|
|
||||||
if (stage.includes("content") || stage.includes("inferring")) return 3;
|
|
||||||
if (stage.includes("scene")) return 2;
|
|
||||||
if (stage.includes("chapter")) return 1;
|
|
||||||
if (stage.includes("reading") || stage.includes("read ")) return 0;
|
|
||||||
return Math.max(0, Math.min(analysisStages.length - 1, Math.floor(scene.progressPercent / 10)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSceneCopy(scene) {
|
function updateSceneCopy(scene) {
|
||||||
text(dom.sceneTitle, `Scene ${scene.sceneNumber} • ${scene.title}`);
|
text(dom.sceneTitle, `Scene ${scene.sceneNumber} • ${scene.title}`);
|
||||||
text(dom.sceneSummary, scene.summary);
|
text(dom.sceneSummary, scene.summary);
|
||||||
@ -422,9 +367,9 @@
|
|||||||
}
|
}
|
||||||
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"} • Scene ${item.sceneNumber}`);
|
text(card.querySelector("span"), `${(item.chapterLabel || "Chapter").toUpperCase()} · SCENE ${item.sceneNumber}`);
|
||||||
text(card.querySelector("strong"), compactPhrase(item.title, item.id === scene.id ? 42 : 28));
|
text(card.querySelector("strong"), compactPhrase(cleanSceneTitle(item.title), item.id === scene.id ? 44 : 30));
|
||||||
text(card.querySelector("small"), item.timelineEvent);
|
text(card.querySelector("small"), compactPhrase(item.timelineEvent, 34));
|
||||||
dom.ribbon.append(card);
|
dom.ribbon.append(card);
|
||||||
});
|
});
|
||||||
removeInactive(dom.ribbon, "[data-scene-id]", "sceneId", activeIds);
|
removeInactive(dom.ribbon, "[data-scene-id]", "sceneId", activeIds);
|
||||||
@ -444,12 +389,36 @@
|
|||||||
settleEntering(marker);
|
settleEntering(marker);
|
||||||
}
|
}
|
||||||
marker.classList.toggle("is-current", item.id === scene.id);
|
marker.classList.toggle("is-current", item.id === scene.id);
|
||||||
text(marker.querySelector("span"), `${item.timelineLabel} - S${item.sceneNumber}`);
|
const shouldLabel = shouldShowTimelineLabel(completed, item, scene);
|
||||||
|
marker.classList.toggle("is-label-hidden", !shouldLabel);
|
||||||
|
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) {
|
||||||
|
const index = items.findIndex((candidate) => candidate.id === item.id);
|
||||||
|
if (index === 0 || index === items.length - 1 || item.id === scene.id) return true;
|
||||||
|
const interval = items.length > 12 ? 5 : items.length > 8 ? 4 : 3;
|
||||||
|
return index % interval === 0 && Math.abs(item.sceneNumber - scene.sceneNumber) > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortTimelineLabel(item) {
|
||||||
|
const value = String(item.timelineLabel || "").trim();
|
||||||
|
if (value && !/book\s+\d|alpha flame|chapter/i.test(value) && value.length <= 18) return value;
|
||||||
|
return `${abbreviateChapter(item.chapterLabel)} · S${item.sceneNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function abbreviateChapter(value) {
|
||||||
|
const match = String(value || "").match(/(\d+(?:\.\d+)?)/);
|
||||||
|
return match ? `Ch ${match[1]}` : "Ch";
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanSceneTitle(value) {
|
||||||
|
return String(value || "").replace(/^book\s+\d+\s*[:|-]\s*/i, "").replace(/^the alpha flame[^:|-]*[:|-]\s*/i, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
function updateFeeds(scene) {
|
function updateFeeds(scene) {
|
||||||
renderList(dom.discoveries, scene.latestDiscoveries);
|
renderList(dom.discoveries, scene.latestDiscoveries);
|
||||||
renderList(dom.activityFeed, scene.activityFeed);
|
renderList(dom.activityFeed, scene.activityFeed);
|
||||||
@ -607,27 +576,34 @@
|
|||||||
|
|
||||||
group.querySelector(".story-exp-connection-label-group")?.remove();
|
group.querySelector(".story-exp-connection-label-group")?.remove();
|
||||||
if (label) {
|
if (label) {
|
||||||
const labelPosition = relationshipLabelPosition(start, route, end, occupiedRects);
|
let displayLabel = label;
|
||||||
|
let labelBox = relationshipLabelBox(displayLabel);
|
||||||
|
let labelPosition = relationshipLabelPosition(start, route, end, occupiedRects, labelBox);
|
||||||
|
if (!labelPosition) {
|
||||||
|
displayLabel = compactPhrase(label, 24);
|
||||||
|
labelBox = relationshipLabelBox(displayLabel);
|
||||||
|
labelPosition = relationshipLabelPosition(start, route, end, occupiedRects, labelBox);
|
||||||
|
}
|
||||||
if (!labelPosition) {
|
if (!labelPosition) {
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
const labelGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
const labelGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||||
labelGroup.setAttribute("class", "story-exp-connection-label-group");
|
labelGroup.setAttribute("class", "story-exp-connection-label-group");
|
||||||
const labelNode = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
const labelNode = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
|
||||||
labelNode.setAttribute("class", "story-exp-connection-label");
|
labelNode.setAttribute("class", "story-exp-connection-label-wrap");
|
||||||
labelNode.setAttribute("x", String(labelPosition.x));
|
labelNode.setAttribute("x", String(labelPosition.x - labelBox.width / 2));
|
||||||
labelNode.setAttribute("y", String(labelPosition.y));
|
labelNode.setAttribute("y", String(labelPosition.y - labelBox.height / 2));
|
||||||
labelNode.setAttribute("text-anchor", "middle");
|
labelNode.setAttribute("width", String(labelBox.width));
|
||||||
labelNode.textContent = label;
|
labelNode.setAttribute("height", String(labelBox.height));
|
||||||
|
labelNode.setAttribute("title", label);
|
||||||
|
const labelDiv = document.createElement("div");
|
||||||
|
labelDiv.className = "story-exp-connection-label";
|
||||||
|
labelDiv.title = label;
|
||||||
|
labelDiv.textContent = displayLabel;
|
||||||
|
labelNode.append(labelDiv);
|
||||||
labelGroup.append(labelNode);
|
labelGroup.append(labelNode);
|
||||||
group.append(labelGroup);
|
group.append(labelGroup);
|
||||||
occupiedRects.push({
|
occupiedRects.push(labelRect(labelPosition, labelBox));
|
||||||
left: labelPosition.x - 78,
|
|
||||||
right: labelPosition.x + 78,
|
|
||||||
top: labelPosition.y - 22,
|
|
||||||
bottom: labelPosition.y + 10
|
|
||||||
});
|
|
||||||
window.requestAnimationFrame(() => fitConnectionLabelBackground(labelGroup));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNew) group.classList.add("is-entering");
|
if (isNew) group.classList.add("is-entering");
|
||||||
@ -970,29 +946,48 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function relationshipLabelPosition(start, route, end, occupiedRects) {
|
function relationshipLabelBox(label) {
|
||||||
|
const length = String(label || "").length;
|
||||||
|
const width = length > 42 ? 168 : length > 24 ? 144 : 118;
|
||||||
|
const lines = Math.min(3, Math.max(1, Math.ceil(length / 24)));
|
||||||
|
return { width, height: 18 + lines * 15 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function relationshipLabelPosition(start, route, end, occupiedRects, labelBox) {
|
||||||
const sign = Math.sign(route.offset || 1);
|
const sign = Math.sign(route.offset || 1);
|
||||||
const candidates = [0.42, 0.5, 0.58, 0.34, 0.66].flatMap((t) => {
|
const bounds = dom.visualStage?.getBoundingClientRect();
|
||||||
|
const boundary = {
|
||||||
|
left: 6,
|
||||||
|
top: 6,
|
||||||
|
right: Math.max(10, bounds?.width || 1000) - 6,
|
||||||
|
bottom: Math.max(10, bounds?.height || 560) - 6
|
||||||
|
};
|
||||||
|
const candidates = [0.5, 0.42, 0.58, 0.32, 0.68, 0.24, 0.76].flatMap((t) => {
|
||||||
const point = pointOnCubic(start, route.startControl, route.endControl, end, t);
|
const point = pointOnCubic(start, route.startControl, route.endControl, end, t);
|
||||||
return [34, 54, 76, -42, -64].map((distance) => ({
|
return [38, 62, 88, -42, -68, 112, -96].map((distance) => ({
|
||||||
x: point.x + route.normal.x * sign * distance,
|
x: point.x + route.normal.x * sign * distance,
|
||||||
y: point.y + route.normal.y * sign * distance - 5
|
y: point.y + route.normal.y * sign * distance - 5
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
const best = candidates.reduce((current, candidate) => {
|
const best = candidates.reduce((current, candidate) => {
|
||||||
const score = occupiedRects.reduce((sum, rect) => sum + labelRectPenalty(candidate, rect), 0);
|
const candidateRect = labelRect(candidate, labelBox);
|
||||||
|
const boundaryPenalty = candidateRect.left < boundary.left || candidateRect.right > boundary.right || candidateRect.top < boundary.top || candidateRect.bottom > boundary.bottom ? 2000 : 0;
|
||||||
|
const score = boundaryPenalty + occupiedRects.reduce((sum, rect) => sum + labelRectPenalty(candidateRect, rect), 0);
|
||||||
return score < current.score ? { point: candidate, score } : current;
|
return score < current.score ? { point: candidate, score } : current;
|
||||||
}, { point: candidates[0], score: Number.POSITIVE_INFINITY });
|
}, { point: candidates[0], score: Number.POSITIVE_INFINITY });
|
||||||
return best.score >= 1000 ? null : best.point;
|
return best.score >= 1000 ? null : best.point;
|
||||||
}
|
}
|
||||||
|
|
||||||
function labelRectPenalty(point, rect) {
|
function labelRect(point, labelBox) {
|
||||||
const labelRect = {
|
return {
|
||||||
left: point.x - 70,
|
left: point.x - labelBox.width / 2,
|
||||||
right: point.x + 70,
|
right: point.x + labelBox.width / 2,
|
||||||
top: point.y - 18,
|
top: point.y - labelBox.height / 2,
|
||||||
bottom: point.y + 8
|
bottom: point.y + labelBox.height / 2
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelRectPenalty(labelRect, rect) {
|
||||||
if (rectsIntersect(labelRect, rect)) return 1000;
|
if (rectsIntersect(labelRect, rect)) return 1000;
|
||||||
const dx = Math.max(rect.left - labelRect.right, 0, labelRect.left - rect.right);
|
const dx = Math.max(rect.left - labelRect.right, 0, labelRect.left - rect.right);
|
||||||
const dy = Math.max(rect.top - labelRect.bottom, 0, labelRect.top - rect.bottom);
|
const dy = Math.max(rect.top - labelRect.bottom, 0, labelRect.top - rect.bottom);
|
||||||
@ -1003,20 +998,6 @@
|
|||||||
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
|
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fitConnectionLabelBackground(group) {
|
|
||||||
const label = group.querySelector(".story-exp-connection-label");
|
|
||||||
if (!label || group.querySelector("rect")) return;
|
|
||||||
const box = label.getBBox();
|
|
||||||
const background = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
||||||
background.setAttribute("class", "story-exp-connection-label-bg");
|
|
||||||
background.setAttribute("x", String(box.x - 8));
|
|
||||||
background.setAttribute("y", String(box.y - 4));
|
|
||||||
background.setAttribute("width", String(box.width + 16));
|
|
||||||
background.setAttribute("height", String(box.height + 8));
|
|
||||||
background.setAttribute("rx", "7");
|
|
||||||
group.insertBefore(background, label);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pointRectPenalty(point, rect) {
|
function pointRectPenalty(point, rect) {
|
||||||
if (point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom) return 1000;
|
if (point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom) return 1000;
|
||||||
const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);
|
const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user