Phase 6D with the SVG rendering

This commit is contained in:
Nick Beckley 2026-06-11 20:06:23 +01:00
parent ba5960f907
commit 5831172b1d
5 changed files with 133 additions and 5 deletions

View File

@ -819,7 +819,8 @@ else
{
if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
{
<div class="plot-lane-board" data-timeline-section="plot-lines" style="--scene-count:@timelineColumns.Count">
<div class="plot-lane-board" data-timeline-section="plot-lines" data-plot-line-svg-board style="--scene-count:@timelineColumns.Count">
<svg class="plot-lane-svg-overlay" data-plot-line-svg aria-hidden="true"></svg>
<div class="lane-board-heading plot-lane-board-heading">
<button type="button" data-collapse-toggle="plot-lines">Plot line lanes</button>
<small>Thread event markers by scene</small>
@ -834,7 +835,7 @@ else
{
if (lane.PlotLine.PlotLineTypeName != group.Key) { continue; }
var laneFocusClass = Model.Filter.FocusType == "plotline" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.PlotLine.PlotLineID ? "lane-dim" : "";
<div class="plot-lane-row @laneFocusClass" style="--plot-colour:@lane.PlotLine.Colour">
<div class="plot-lane-row @laneFocusClass" data-plot-line-row data-plot-line-id="@lane.PlotLine.PlotLineID" data-plot-colour="@lane.PlotLine.Colour" style="--plot-colour:@lane.PlotLine.Colour">
<a class="plot-lane-label" asp-controller="PlotLines" asp-action="Edit" asp-route-id="@lane.PlotLine.PlotLineID">
<span class="colour-swatch" style="background:@lane.PlotLine.Colour"></span>
<strong>@lane.PlotLine.PlotLineName</strong>
@ -849,7 +850,7 @@ else
}
var slot = lane.SceneSlots.FirstOrDefault(x => x.Scene.SceneID == realPlotScene.SceneID);
<div class="plot-lane-slot" title="Scene @realPlotScene.SceneNumber: @realPlotScene.SceneTitle">
<div class="plot-lane-slot" data-plot-line-slot data-scene-id="@realPlotScene.SceneID" title="Scene @realPlotScene.SceneNumber: @realPlotScene.SceneTitle">
@foreach (var threadEvent in slot?.Events ?? Enumerable.Empty<ThreadEvent>())
{
var markerClass = threadEvent.EventTypeName is "Resolved" or "Payoff"
@ -858,6 +859,8 @@ else
? "contradicted"
: "";
<a class="thread-marker @markerClass"
data-plot-event-node
data-plot-event-type="@threadEvent.PlotEventTypeName"
asp-controller="Timeline"
asp-action="Index"
asp-route-ProjectID="@Model.Project.ProjectID"

View File

@ -827,6 +827,7 @@ body.timeline-inspector-resizing {
}
.plot-lane-board {
position: relative;
display: grid;
gap: 8px;
width: max-content;
@ -836,6 +837,42 @@ body.timeline-inspector-resizing {
padding-top: 14px;
}
.plot-lane-svg-overlay {
position: absolute;
inset: 0 auto auto 0;
z-index: 4;
pointer-events: none;
overflow: visible;
}
.plot-line-svg-path {
fill: none;
stroke-width: 4;
stroke-linecap: round;
opacity: 0.72;
}
.plot-event-svg-node circle,
.plot-event-svg-node path {
stroke: #ffffff;
stroke-width: 2;
}
.plot-event-svg-node line {
stroke: #8b1e2d;
stroke-width: 4;
stroke-linecap: round;
}
.plot-event-svg-node-reveal circle {
stroke-width: 3;
}
.plot-event-svg-node-resolve circle:last-child {
stroke: currentColor;
stroke-width: 2;
}
.lane-board-heading,
.metric-lane-board-heading,
.plot-lane-board-heading {
@ -927,6 +964,10 @@ body.timeline-inspector-resizing {
text-decoration: none;
}
.plot-lane-board [data-plot-event-node] {
opacity: 0;
}
.thread-marker:hover {
color: var(--plotline-ink);
background: color-mix(in srgb, var(--plot-colour), #ffffff 58%);

File diff suppressed because one or more lines are too long

View File

@ -1307,6 +1307,7 @@
densitySelect.value = nextValue;
}
localStorage.setItem(densityKey, nextValue);
window.requestAnimationFrame(renderPlotLineSvg);
};
const applyZoom = (value) => {
@ -1316,6 +1317,7 @@
zoomSelect.value = nextValue;
}
localStorage.setItem(zoomKey, nextValue);
window.requestAnimationFrame(renderPlotLineSvg);
};
const getScene = (sceneId) => root.querySelector(`[data-scene-id='${CSS.escape(String(sceneId))}']`);
@ -1351,6 +1353,83 @@
const saveCollapsed = () => localStorage.setItem(collapsedKey, JSON.stringify([...collapsed]));
const renderPlotLineSvg = () => {
root.querySelectorAll("[data-plot-line-svg-board]").forEach((board) => {
const svg = board.querySelector("[data-plot-line-svg]");
if (!svg) {
return;
}
svg.replaceChildren();
const boardRect = board.getBoundingClientRect();
const width = Math.max(board.scrollWidth, boardRect.width);
const height = Math.max(board.scrollHeight, boardRect.height);
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
svg.setAttribute("width", String(width));
svg.setAttribute("height", String(height));
const makeSvg = (name, attributes = {}) => {
const element = document.createElementNS("http://www.w3.org/2000/svg", name);
Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, String(value)));
return element;
};
const pointFor = (row, marker) => {
const slot = marker.closest("[data-plot-line-slot]");
if (!slot) {
return null;
}
const slotRect = slot.getBoundingClientRect();
const rowRect = row.getBoundingClientRect();
return {
x: slotRect.left - boardRect.left + slotRect.width / 2,
y: rowRect.top - boardRect.top + rowRect.height / 2,
type: (marker.dataset.plotEventType || "Progress").toLowerCase()
};
};
const drawNode = (point, colour) => {
const group = makeSvg("g", { class: `plot-event-svg-node plot-event-svg-node-${point.type}` });
if (point.type === "twist") {
group.append(makeSvg("path", {
d: `M ${point.x} ${point.y - 7} L ${point.x + 7} ${point.y} L ${point.x} ${point.y + 7} L ${point.x - 7} ${point.y} Z`,
fill: colour
}));
} else if (point.type === "abandon") {
group.append(makeSvg("line", { x1: point.x - 6, y1: point.y - 6, x2: point.x + 6, y2: point.y + 6 }));
group.append(makeSvg("line", { x1: point.x + 6, y1: point.y - 6, x2: point.x - 6, y2: point.y + 6 }));
} else {
group.append(makeSvg("circle", { cx: point.x, cy: point.y, r: 6, fill: colour }));
if (point.type === "resolve") {
group.append(makeSvg("circle", { cx: point.x, cy: point.y, r: 10, fill: "none", stroke: colour }));
}
}
svg.append(group);
};
board.querySelectorAll("[data-plot-line-row]").forEach((row) => {
const colour = row.dataset.plotColour || "#2f6f63";
const points = [...row.querySelectorAll("[data-plot-event-node]")]
.map((marker) => pointFor(row, marker))
.filter(Boolean)
.sort((a, b) => a.x - b.x);
if (points.length > 1) {
const first = points[0];
const last = points[points.length - 1];
svg.append(makeSvg("path", {
class: "plot-line-svg-path",
d: `M ${first.x} ${first.y} L ${last.x} ${last.y}`,
stroke: colour
}));
}
points.forEach((point) => drawNode(point, colour));
});
});
};
const setCollapsed = (id, shouldCollapse) => {
document.querySelectorAll(`[data-collapse-body='${CSS.escape(id)}']`).forEach((body) => {
body.classList.toggle("timeline-section-collapsed", shouldCollapse);
@ -1378,6 +1457,7 @@
}
setCollapsed(id, nextCollapsed);
saveCollapsed();
renderPlotLineSvg();
window.dispatchEvent(new Event("plotline:timeline-layout-changed"));
});
});
@ -1394,12 +1474,16 @@
}
})();
window.requestAnimationFrame(() => {
renderPlotLineSvg();
scrollActiveChapterIntoView("auto");
if (hadPendingScrollRestore) {
restoreScrollPosition();
}
});
window.addEventListener("resize", renderPlotLineSvg);
window.addEventListener("plotline:timeline-layout-changed", renderPlotLineSvg);
const saveScrollForSceneSelection = (event) => {
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
return;

File diff suppressed because one or more lines are too long