diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index d320bbc..9e8589f 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -1177,9 +1177,9 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules() Assert(adminDiagnostics.Contains("Reset my Story Intelligence data", StringComparison.Ordinal), "Admin diagnostics page must show the reset button."); Assert(developmentReset.Contains("PreservedIllustrationItems", StringComparison.Ordinal), "Admin reset result must report preserved generated images."); Assert(!developmentReset.Contains("DELETE FROM dbo.IllustrationLibraryItems", StringComparison.Ordinal), "Admin reset must preserve generated illustration library records."); - Assert(script.Contains("renderPresenceCallout", StringComparison.Ordinal), "Stage should show a presence callout for characters in the active location."); - Assert(script.Contains("drawConnection(id, cluster, locationPoint, \"presence-cluster\", 54, \"\"", StringComparison.Ordinal), "Presence connector should not show a tiny count label."); - Assert(css.Contains(".story-exp-presence-callout", StringComparison.Ordinal), "Presence callout styling is missing."); + Assert(!script.Contains("renderPresenceCallout", StringComparison.Ordinal), "Stage should not draw a bordered character container."); + Assert(script.Contains("drawConnection(id, cluster, locationPoint, \"presence-cluster\", 54, `${presentCharacters.length} here`", StringComparison.Ordinal), "Presence connector should show the location join label."); + Assert(!css.Contains(".story-exp-presence-callout", StringComparison.Ordinal), "Character container styling should be removed."); Assert(css.Contains("white-space: normal", StringComparison.Ordinal), "Location captions should wrap rather than ellipsise."); Assert(!script.Contains("renderOverflowSummary", StringComparison.Ordinal), "Side panels must not use hidden '+N more' overflow controls."); Assert(css.Contains("-webkit-line-clamp: 3", StringComparison.Ordinal), "Insight body text should be clamped inside its card."); @@ -1214,7 +1214,8 @@ static void Phase21RSceneBrowserAndPanelsAreReplayFriendly() Assert(script.Contains("Assets are recorded in insights/diagnostics but not rendered as stage nodes.", StringComparison.Ordinal), "Asset data should remain available without drawing asset stage nodes."); Assert(script.Contains("width * 0.82 - size / 2", StringComparison.Ordinal), "Location should be a larger right-side anchor once asset nodes are removed."); Assert(script.Contains("{ x: 0.68, y: 0.26, role: \"context\" }", StringComparison.Ordinal), "Character layout should use more of the stage width."); - Assert(script.Contains("presence-cluster", StringComparison.Ordinal), "Presence should be shown as a single location callout instead of many confusing location lines."); + Assert(script.Contains("scene.relationships", StringComparison.Ordinal), "Relationship lines should be rendered on the stage."); + Assert(script.Contains("presence-cluster", StringComparison.Ordinal), "Presence should still be shown as a location join."); Assert(css.Contains(".story-exp-zone--location .story-exp-zone-label {\n left: 80%;", StringComparison.Ordinal), "Location lane label should follow the compact right-side anchor."); Assert(!css.Contains("grid-template-rows: minmax(218px, 1.35fr) minmax(185px, 1.08fr) auto", StringComparison.Ordinal), "Right rail must not use fixed-height panel tracks."); Assert(!css.Contains("grid-template-rows: auto auto minmax(170px, 1fr) minmax(96px, 0.56fr)", StringComparison.Ordinal), "Left rail must not use fixed-height panel tracks."); diff --git a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css index 59df34c..aaaa76b 100644 --- a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css +++ b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css @@ -792,23 +792,6 @@ input:focus-visible { height: 100%; } -.story-exp-presence-callout { - position: absolute; - left: var(--presence-left, 28px); - top: var(--presence-top, 58px); - z-index: 34; - width: var(--presence-width, 520px); - height: var(--presence-height, 320px); - border: 1px solid rgba(125, 193, 255, 0.22); - border-radius: 28px; - background: - radial-gradient(circle at 62% 45%, rgba(87, 149, 205, 0.14), transparent 58%), - linear-gradient(135deg, rgba(17, 41, 70, 0.22), rgba(4, 10, 20, 0.06)); - box-shadow: inset 0 0 44px rgba(125, 193, 255, 0.08), 0 22px 80px rgba(0, 0, 0, 0.16); - pointer-events: none; - transition: left 850ms cubic-bezier(.16, .84, .28, 1), top 850ms cubic-bezier(.16, .84, .28, 1), width 850ms cubic-bezier(.16, .84, .28, 1), height 850ms cubic-bezier(.16, .84, .28, 1); -} - .story-character { position: absolute; display: block; diff --git a/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js index 3104749..b160638 100644 --- a/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js +++ b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js @@ -682,47 +682,38 @@ const activeIds = new Set(); const occupiedRects = layoutRects(stageRect); + scene.relationships + .filter((relationship) => relationship.weight >= 72) + .slice(0, 2) + .forEach((relationship, index) => { + const source = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.sourceId)}"]`); + const target = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.targetId)}"]`); + if (!source || !target) return; + + const start = connectionPoint(source, stageRect, target); + const end = connectionPoint(target, stageRect, source); + const obstacles = connectionObstacles(stageRect, source, target); + const id = relationship.id || `relationship-${relationship.sourceId}-${relationship.targetId}`; + activeIds.add(id); + drawn.push(drawConnection(id, start, end, relationshipTone(relationship), relationship.weight, relationship.label || "", index, obstacles, occupiedRects)); + }); + const location = dom.visualStage.querySelector("[data-location-id]"); const presentCharacters = scene.characters .filter((character) => !character.status || !character.status.toLowerCase().includes("absent")) .map((character) => dom.visualStage.querySelector(`[data-character-id="${cssEscape(character.id)}"]`)) .filter(Boolean); const cluster = characterClusterPoint(presentCharacters, stageRect); - renderPresenceCallout(presentCharacters, location, stageRect); if (cluster && location) { const id = `presence-cluster-${location.dataset.locationId}`; activeIds.add(id); const locationPoint = connectionPoint(location, stageRect, { getBoundingClientRect: () => ({ left: stageRect.left + cluster.x, top: stageRect.top + cluster.y, width: 0, height: 0 }) }); - drawn.push(drawConnection(id, cluster, locationPoint, "presence-cluster", 54, "", drawn.length, [], occupiedRects)); + drawn.push(drawConnection(id, cluster, locationPoint, "presence-cluster", 54, `${presentCharacters.length} here`, drawn.length, [], occupiedRects)); } removeInactiveSvgConnections(activeIds); } - function renderPresenceCallout(characterNodes, locationNode, stageRect) { - let callout = dom.visualStage.querySelector("[data-presence-callout]"); - if (!characterNodes.length || !locationNode) { - callout?.remove(); - return; - } - - if (!callout) { - callout = document.createElement("div"); - callout.className = "story-exp-presence-callout"; - callout.dataset.presenceCallout = "true"; - dom.visualStage.append(callout); - } - - const left = Math.max(14, stageRect.width * 0.018); - const top = Math.max(38, stageRect.height * 0.062); - const right = Math.min(stageRect.width * 0.71, stageRect.width - 330); - const bottom = Math.min(stageRect.height - 62, stageRect.height * 0.78); - callout.style.setProperty("--presence-left", `${Math.round(left)}px`); - callout.style.setProperty("--presence-top", `${Math.round(top)}px`); - callout.style.setProperty("--presence-width", `${Math.round(Math.max(520, right - left))}px`); - callout.style.setProperty("--presence-height", `${Math.round(Math.max(300, bottom - top))}px`); - } - function drawConnection(id, start, end, tone, weight, label, index, obstacles, occupiedRects) { let group = dom.connections.querySelector(`[data-connection-id="${cssEscape(id)}"]`); const isNew = !group;