From 588a223b4482e9c74de6e09487b6c6b965d5e333 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sun, 26 Jul 2026 15:21:06 +0000 Subject: [PATCH] Focus story visualisation on insights --- PlotLine.Tests/Program.cs | 4 + .../StoryIntelligenceExperience.cshtml | 4 - ...tory-intelligence-experience-prototype.css | 127 ++--------------- ...story-intelligence-experience-prototype.js | 134 ++---------------- 4 files changed, 27 insertions(+), 242 deletions(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index f142590..b514c77 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -1179,6 +1179,10 @@ static void Phase21RSceneBrowserAndPanelsAreReplayFriendly() Assert(css.Contains("scrollbar-width: none", StringComparison.Ordinal), "Side-panel native scrollbar tracks should be suppressed."); Assert(css.Contains(".story-exp-rebuild {\n position: fixed;", StringComparison.Ordinal), "Replay rebuild controls should be fixed so they do not resize the whole screen."); Assert(css.Contains(".story-exp-panel:has(.story-exp-insight-list:empty)", StringComparison.Ordinal), "Empty side panels should collapse instead of reserving large blank space."); + Assert(css.Contains("grid-template-columns: minmax(180px, 0.42fr) minmax(660px, 1.55fr) minmax(380px, 0.84fr);", StringComparison.Ordinal), "Right insight rail should be widened now that assets are not stage nodes."); + Assert(css.Contains("--story-side-body-font-size: 0.72rem;", StringComparison.Ordinal), "Left and right side panels should share body text sizing."); + Assert(!view.Contains("data-assets", StringComparison.Ordinal), "Asset visualisation lane should not be rendered on the main stage."); + 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(!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."); Assert(script.Contains("shouldUseCharacterPortrait", StringComparison.Ordinal), "Characters should use evidence cards until portrait evidence is strong enough."); diff --git a/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml b/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml index 2cccfb5..c4a58c5 100644 --- a/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml +++ b/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml @@ -197,10 +197,6 @@

Location

-
-

Assets

-
-
diff --git a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css index b47920f..f29b045 100644 --- a/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css +++ b/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css @@ -16,6 +16,8 @@ --story-rose: #e38ca8; --story-radius: 18px; --story-shadow: 0 28px 84px rgba(0, 0, 0, 0.32); + --story-side-body-font-size: 0.72rem; + --story-side-title-font-size: 0.76rem; --progress-value: 0deg; } @@ -341,7 +343,7 @@ input:focus-visible { .story-exp-shell { display: grid; - grid-template-columns: minmax(180px, 0.46fr) minmax(820px, 2.72fr) minmax(188px, 0.48fr); + grid-template-columns: minmax(180px, 0.42fr) minmax(660px, 1.55fr) minmax(380px, 0.84fr); gap: 12px; min-height: 0; padding: 12px 18px 58px; @@ -538,7 +540,7 @@ input:focus-visible { .story-exp-panel h2 { margin: 0 0 5px; - font-size: 0.82rem; + font-size: var(--story-side-title-font-size); letter-spacing: 0.01em; } @@ -561,6 +563,7 @@ input:focus-visible { padding: 6px 7px; background: rgba(255, 255, 255, 0.035); color: var(--story-soft); + font-size: var(--story-side-body-font-size); min-width: 0; overflow-wrap: anywhere; } @@ -778,11 +781,6 @@ input:focus-visible { top: 22px; } -.story-exp-zone--assets .story-exp-zone-label { - right: 8%; - top: 22px; -} - .story-exp-characters { position: relative; z-index: 120; @@ -811,8 +809,7 @@ input:focus-visible { } .story-character__image, -.story-location__image, -.story-asset__image { +.story-location__image { position: relative; z-index: 1; display: block; @@ -829,8 +826,7 @@ input:focus-visible { } .story-character__image img, -.story-location__image img, -.story-asset__image img { +.story-location__image img { width: 100%; height: 100%; object-fit: cover; @@ -1027,108 +1023,6 @@ input:focus-visible { white-space: nowrap; } -.story-exp-assets { - position: relative; - width: 100%; - height: 100%; -} - -.story-asset { - position: absolute; - display: grid; - justify-items: center; - gap: 6px; - width: max(var(--node-size, 78px), 104px); - opacity: var(--opacity, 1); - transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1)); - z-index: calc(var(--z, 3) + 30); - pointer-events: auto; - transition: transform 860ms cubic-bezier(.16, .84, .28, 1), opacity 620ms ease; -} - -.story-asset__image { - width: var(--node-size, 78px); - height: var(--node-size, 78px); - border-radius: 50%; - border-color: rgba(215, 168, 93, 0.68); - box-shadow: 0 0 0 4px rgba(215, 168, 93, 0.08), 0 18px 38px rgba(0, 0, 0, 0.28), 0 0 24px rgba(215, 168, 93, 0.13); -} - -.story-asset__text { - position: relative; - z-index: 80; - display: grid; - justify-items: center; - gap: 2px; - width: min(140px, calc(var(--node-size, 78px) + 52px)); - border: 1px solid rgba(215, 168, 93, 0.11); - border-radius: 10px; - padding: 4px 7px 5px; - background: rgba(4, 10, 20, 0.6); - box-shadow: 0 12px 26px rgba(0, 0, 0, 0.22); - backdrop-filter: blur(8px); - text-align: center; -} - -.story-asset.is-evidence-card { - width: var(--node-size, 78px); - height: var(--node-size, 78px); -} - -.story-asset.is-evidence-card .story-asset__image { - position: absolute; - inset: 0; - opacity: 0; - pointer-events: none; -} - -.story-asset.is-evidence-card .story-asset__text { - position: absolute; - inset: 0; - align-content: center; - justify-items: start; - width: 100%; - min-height: 100%; - border-color: rgba(215, 168, 93, 0.18); - padding: 7px 8px; - background: - linear-gradient(180deg, rgba(34, 29, 18, 0.88), rgba(7, 13, 21, 0.84)); - text-align: left; -} - -.story-asset.is-evidence-card .story-asset__text strong, -.story-asset.is-evidence-card .story-asset__text span { - text-align: left; -} - -.story-asset strong, -.story-asset span { - display: block; - overflow: hidden; - max-width: 100%; - line-height: 1.08; - text-overflow: ellipsis; - white-space: nowrap; -} - -.story-asset strong { - font-size: 0.78rem; -} - -.story-asset span { - color: var(--story-muted); - font-size: 0.64rem; -} - -.story-asset .story-entity-facts span { - display: inline-flex; - width: auto; - max-width: 100%; - border-radius: 999px; - line-height: 1.2; - text-overflow: ellipsis; -} - .story-exp-ribbon-wrap { display: grid; grid-template-rows: auto minmax(104px, auto) 56px; @@ -1342,7 +1236,7 @@ input:focus-visible { display: -webkit-box; overflow: hidden; margin: 0; - font-size: 0.72rem; + font-size: var(--story-side-body-font-size); line-height: 1.16; -webkit-box-orient: vertical; -webkit-line-clamp: 3; @@ -1359,7 +1253,7 @@ input:focus-visible { .story-exp-insight strong { color: var(--story-text); - font-size: 0.76rem; + font-size: var(--story-side-title-font-size); line-height: 1.12; } @@ -1518,8 +1412,7 @@ input:focus-visible { transform: translateY(12px) scale(0.96); } -.story-character.is-entering, -.story-asset.is-entering { +.story-character.is-entering { opacity: 0; transform: translate(var(--x, 0), var(--y, 0)) scale(0.82); } diff --git a/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js index 4817325..226ab4e 100644 --- a/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js +++ b/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js @@ -110,7 +110,7 @@ const layout = sceneLayout(current); reconcileCharacters(current, previous, layout); reconcileLocation(current, previous); - reconcileAssets(current, previous, layout); + clearAssetStage(); reconcileRelationships(current); updateKnowledgeThreads(current); updateSceneRibbon(current); @@ -381,7 +381,7 @@ return [ scene.location?.imagePath, ...(scene.characters || []).map((character) => character.imagePath), - ...(scene.assets || []).map((asset) => asset.imagePath) + // Assets are recorded in insights/diagnostics but not rendered as stage nodes. ].filter(Boolean); } @@ -490,43 +490,8 @@ node.title = `${scene.location.name}: ${scene.location.label}`; } - function reconcileAssets(scene, previous, layout) { - const activeIds = new Set(scene.assets.map((asset) => asset.id)); - scene.assets.forEach((asset, index) => { - let node = dom.assets.querySelector(`[data-asset-id="${cssEscape(asset.id)}"]`); - if (!node) { - node = createAssetNode(asset); - node.classList.add("is-entering"); - dom.assets.append(node); - settleEntering(node); - } - - const position = layout.assets.get(asset.id) || assetPosition(asset, index, scene.assets.length); - node.style.setProperty("--node-size", `${position.size}px`); - node.style.setProperty("--x", `${position.x}px`); - node.style.setProperty("--y", `${position.y}px`); - node.style.setProperty("--scale", String(position.scale)); - node.style.setProperty("--z", String(position.z)); - node.style.setProperty("--opacity", "1"); - const assetImageReady = shouldUseAssetImage(asset); - node.classList.toggle("is-evidence-card", !assetImageReady); - setEntityImage(node.querySelector("img"), assetImageReady ? asset.imagePath : "", asset.imageResolution, `${asset.name} prototype asset`); - text(node.querySelector("[data-asset-name]"), asset.name); - text(node.querySelector("[data-asset-label]"), conciseAssetLabel(asset)); - renderFactChips(node.querySelector("[data-asset-facts]"), assetFacts(asset)); - node.title = `${asset.name}: ${asset.label}`; - }); - - dom.assets.querySelectorAll("[data-asset-id]").forEach((node) => { - if (!activeIds.has(node.dataset.assetId)) { - node.style.setProperty("--opacity", "0"); - node.style.setProperty("--scale", "0.8"); - window.setTimeout(() => { - const currentIds = new Set(loadSceneState(state.index).assets.map((asset) => asset.id)); - if (!currentIds.has(node.dataset.assetId)) node.remove(); - }, reducedMotion ? 0 : 520); - } - }); + function clearAssetStage() { + dom.assets?.replaceChildren(); } function reconcileRelationships(scene) { @@ -750,14 +715,6 @@ } }); - scene.assets.forEach((asset, index) => { - const assetNode = dom.visualStage.querySelector(`[data-asset-id="${cssEscape(asset.id)}"]`); - if (!assetNode || !location) return; - const id = `asset-${location.dataset.locationId}-${asset.id}`; - activeIds.add(id); - drawn.push(drawConnection(id, connectionPoint(location, stageRect, assetNode), connectionPoint(assetNode, stageRect, location), "evidence", asset.weight, index === 0 ? assetConnectionLabel(asset) : "", drawn.length, connectionObstacles(stageRect, location, assetNode), occupiedRects)); - }); - removeInactiveSvgConnections(activeIds); } @@ -835,27 +792,12 @@ return node; } - function createAssetNode(asset) { - const node = document.createElement("article"); - node.className = "story-asset"; - node.dataset.assetId = asset.id; - node.innerHTML = ` -
-
- - -
-
`; - return node; - } - function sceneLayout(scene) { const bounds = dom.visualStage?.getBoundingClientRect(); const width = Math.max(760, bounds?.width || 1040); const height = Math.max(500, bounds?.height || 560); return { - characters: characterLayout(scene, width, height), - assets: assetLayout(scene, width, height) + characters: characterLayout(scene, width, height) }; } @@ -971,56 +913,6 @@ }; } - function assetPosition(asset, index, total) { - const bounds = dom.visualStage?.getBoundingClientRect(); - const width = Math.max(820, bounds?.width || 1040); - const height = Math.max(500, bounds?.height || 560); - const anchors = [ - { x: 0.83, y: 0.26 }, - { x: 0.84, y: 0.51 }, - { x: 0.82, y: 0.74 }, - { x: 0.72, y: 0.19 } - ]; - const anchor = anchors[index % anchors.length]; - const size = Math.min(104, Math.max(68, (64 + Math.min(asset.weight, 100) * 0.42) * 0.9)); - return { - x: Math.round(width * anchor.x - size / 2), - y: Math.round(height * anchor.y - size / 2), - size: Math.round(size), - scale: 1 - }; - } - - function assetLayout(scene, width, height) { - const positions = new Map(); - const total = scene.assets.length; - const anchorsByTotal = { - 1: [{ x: 0.83, y: 0.48 }], - 2: [{ x: 0.83, y: 0.34 }, { x: 0.83, y: 0.62 }], - 3: [{ x: 0.82, y: 0.25 }, { x: 0.85, y: 0.50 }, { x: 0.81, y: 0.75 }], - 4: [{ x: 0.78, y: 0.16 }, { x: 0.90, y: 0.37 }, { x: 0.86, y: 0.62 }, { x: 0.73, y: 0.82 }], - 5: [{ x: 0.76, y: 0.13 }, { x: 0.91, y: 0.30 }, { x: 0.80, y: 0.48 }, { x: 0.92, y: 0.66 }, { x: 0.72, y: 0.84 }] - }; - const anchors = anchorsByTotal[Math.min(5, Math.max(1, total))] || anchorsByTotal[5]; - const placed = []; - scene.assets.forEach((asset, index) => { - const anchor = anchors[index] || anchors[anchors.length - 1]; - const important = index === 0 || asset.weight >= 90; - const size = Math.round(important - ? Math.min(104, Math.max(83, (70 + Math.min(asset.weight, 100) * 0.42) * 0.9)) - : Math.min(88, Math.max(68, (58 + Math.min(asset.weight, 100) * 0.32) * 0.9))); - const desired = { - x: width * anchor.x - size / 2, - y: height * anchor.y - size / 2 - }; - const resolved = avoidNodeCollisions(desired, size, placed, width, height); - const position = { x: resolved.x, y: resolved.y, size, scale: 1, z: important ? 4 : 3 }; - placed.push(position); - positions.set(asset.id, position); - }); - return positions; - } - function avoidNodeCollisions(desired, size, placed, width, height) { const padding = Math.max(16, size * 0.12); const candidates = [ @@ -1066,11 +958,11 @@ } function connectionPoint(node, parentRect, targetNode) { - const image = node.querySelector(".story-character__image, .story-location__image, .story-asset__image") || node; + const image = node.querySelector(".story-character__image, .story-location__image") || node; const centre = centrePoint(image, parentRect); const rect = image.getBoundingClientRect(); const target = targetNode ? centrePoint(targetNode, parentRect) : centre; - const targetImage = targetNode?.querySelector?.(".story-character__image, .story-location__image, .story-asset__image"); + const targetImage = targetNode?.querySelector?.(".story-character__image, .story-location__image"); const targetCentre = targetImage ? centrePoint(targetImage, parentRect) : target; const radius = Math.min(rect.width, rect.height) * 0.5; const angle = Math.atan2(targetCentre.y - centre.y, targetCentre.x - centre.x); @@ -1121,14 +1013,14 @@ } function layoutRects(parentRect) { - return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-asset__image, .story-character__text, .story-location__text, .story-asset__text")] + return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-character__text, .story-location__text")] .map((node) => rectRelativeTo(node, parentRect, node.className.includes("__text") ? 8 : 10)); } function connectionObstacles(parentRect, source, target) { - const sourceImage = source.querySelector(".story-character__image, .story-location__image, .story-asset__image") || source; - const targetImage = target.querySelector(".story-character__image, .story-location__image, .story-asset__image") || target; - return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-asset__image, .story-character__text, .story-location__text, .story-asset__text")] + const sourceImage = source.querySelector(".story-character__image, .story-location__image") || source; + const targetImage = target.querySelector(".story-character__image, .story-location__image") || target; + return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-character__text, .story-location__text")] .filter((node) => node !== sourceImage && node !== targetImage) .map((node) => rectRelativeTo(node, parentRect, node.className.includes("__text") ? 8 : 12)); } @@ -1659,7 +1551,7 @@ const layout = sceneLayout(current); reconcileCharacters(current, current, layout); reconcileLocation(current, current); - reconcileAssets(current, current, layout); + clearAssetStage(); scheduleConnectionDraw(); }); @@ -1669,7 +1561,7 @@ const current = loadSceneState(state.index); const layout = sceneLayout(current); reconcileCharacters(current, current, layout); - reconcileAssets(current, current, layout); + clearAssetStage(); scheduleConnectionDraw(); }); observer.observe(dom.visualStage);