Phase 6G UI and rendering fixes

This commit is contained in:
Nick Beckley 2026-06-11 21:37:37 +01:00
parent b346379de2
commit cb67c4bcd3
6 changed files with 88 additions and 20 deletions

View File

@ -1644,6 +1644,7 @@ public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory)
var metricPoints = (await result.ReadAsync<TimelineMetricPoint>()).ToList(); var metricPoints = (await result.ReadAsync<TimelineMetricPoint>()).ToList();
var plotLines = (await result.ReadAsync<PlotLineItem>()).ToList(); var plotLines = (await result.ReadAsync<PlotLineItem>()).ToList();
var threadEvents = (await result.ReadAsync<ThreadEvent>()).ToList(); var threadEvents = (await result.ReadAsync<ThreadEvent>()).ToList();
await PopulateThreadEventTargetsAsync(connection, threadEvents);
return new TimelineData return new TimelineData
{ {
@ -1658,6 +1659,36 @@ public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory)
ThreadEvents = threadEvents ThreadEvents = threadEvents
}; };
} }
private static async Task PopulateThreadEventTargetsAsync(IDbConnection connection, IReadOnlyList<ThreadEvent> threadEvents)
{
var threadEventIds = threadEvents.Select(x => x.ThreadEventID).ToList();
if (threadEventIds.Count == 0)
{
return;
}
var targetRows = await connection.QueryAsync<ThreadEventTargetRow>(
"SELECT PlotEventID, PlotLineID FROM dbo.PlotEventTargetPlotLines WHERE PlotEventID IN @ThreadEventIDs;",
new { ThreadEventIDs = threadEventIds });
var targetsByEvent = targetRows
.GroupBy(x => x.PlotEventID)
.ToDictionary(x => x.Key, x => (IReadOnlyList<int>)x.Select(row => row.PlotLineID).ToList());
foreach (var threadEvent in threadEvents)
{
if (targetsByEvent.TryGetValue(threadEvent.ThreadEventID, out var targets))
{
threadEvent.TargetPlotLineIDs = targets;
}
}
}
private sealed class ThreadEventTargetRow
{
public int PlotEventID { get; set; }
public int PlotLineID { get; set; }
}
} }
public sealed class TimelineSettingsRepository(ISqlConnectionFactory connectionFactory) : ITimelineSettingsRepository public sealed class TimelineSettingsRepository(ISqlConnectionFactory connectionFactory) : ITimelineSettingsRepository

View File

@ -734,6 +734,7 @@
} }
} }
</select> </select>
<div class="form-text">Branch starts another plot line while this one continues. Split ends this plot line and starts two or more others. Merge ends this plot line by feeding into another.</div>
</div> </div>
<div class="col-12" data-plot-event-target="branch"> <div class="col-12" data-plot-event-target="branch">
<label class="form-label" for="TargetPlotLineID-@threadEvent.ThreadEventID">Target plot line</label> <label class="form-label" for="TargetPlotLineID-@threadEvent.ThreadEventID">Target plot line</label>
@ -849,11 +850,11 @@
} }
</select> </select>
</div> </div>
<div class="col-8"> <div class="col-8" data-thread-event-new-thread-field>
<label class="form-label" for="NewThreadTitle">New thread title</label> <label class="form-label" for="NewThreadTitle">New thread title</label>
<input class="form-control form-control-sm" id="NewThreadTitle" name="NewThreadTitle" placeholder="Optional" /> <input class="form-control form-control-sm" id="NewThreadTitle" name="NewThreadTitle" placeholder="Optional" />
</div> </div>
<div class="col-4"> <div class="col-4" data-thread-event-new-thread-field>
<label class="form-label" for="NewThreadTypeID">Thread type</label> <label class="form-label" for="NewThreadTypeID">Thread type</label>
<select class="form-select form-select-sm" id="NewThreadTypeID" name="NewThreadTypeID" asp-items="Model.ThreadTypeOptions"> <select class="form-select form-select-sm" id="NewThreadTypeID" name="NewThreadTypeID" asp-items="Model.ThreadTypeOptions">
<option value="">Default</option> <option value="">Default</option>
@ -875,6 +876,7 @@
} }
} }
</select> </select>
<div class="form-text">Branch starts another plot line while this one continues. Split ends this plot line and starts two or more others. Merge ends this plot line by feeding into another.</div>
</div> </div>
<div class="col-12" data-plot-event-target="branch"> <div class="col-12" data-plot-event-target="branch">
<label class="form-label" for="TargetPlotLineID">Target plot line</label> <label class="form-label" for="TargetPlotLineID">Target plot line</label>
@ -931,6 +933,7 @@
const branchSection = form.querySelector("[data-plot-event-target='branch']"); const branchSection = form.querySelector("[data-plot-event-target='branch']");
const mergeSection = form.querySelector("[data-plot-event-target='merge']"); const mergeSection = form.querySelector("[data-plot-event-target='merge']");
const splitSection = form.querySelector("[data-plot-event-target='split']"); const splitSection = form.querySelector("[data-plot-event-target='split']");
const newThreadFields = form.querySelectorAll("[data-thread-event-new-thread-field]");
const branchTarget = form.querySelector("[data-thread-event-target-single]"); const branchTarget = form.querySelector("[data-thread-event-target-single]");
const mergeTarget = form.querySelector("[data-thread-event-target-merge]"); const mergeTarget = form.querySelector("[data-thread-event-target-merge]");
const splitTargets = form.querySelector("[data-thread-event-target-split]"); const splitTargets = form.querySelector("[data-thread-event-target-split]");
@ -950,17 +953,36 @@
const sync = () => { const sync = () => {
const type = eventTypeName(); const type = eventTypeName();
const isStructural = type === "branch" || type === "merge" || type === "split";
branchSection.hidden = type !== "branch"; branchSection.hidden = type !== "branch";
mergeSection.hidden = type !== "merge"; mergeSection.hidden = type !== "merge";
splitSection.hidden = type !== "split"; splitSection.hidden = type !== "split";
branchTarget.disabled = type !== "branch"; newThreadFields.forEach(field => {
mergeTarget.disabled = type !== "merge"; field.hidden = isStructural;
splitTargets.disabled = type !== "split"; field.querySelectorAll("input, select, textarea").forEach(input => input.disabled = isStructural);
branchTarget.name = type === "branch" ? "TargetPlotLineID" : ""; });
mergeTarget.name = type === "merge" ? "TargetPlotLineID" : ""; if (branchTarget) branchTarget.disabled = type !== "branch";
if (mergeTarget) mergeTarget.disabled = type !== "merge";
if (splitTargets) splitTargets.disabled = type !== "split";
if (branchTarget) branchTarget.name = type === "branch" ? "TargetPlotLineID" : "";
if (mergeTarget) mergeTarget.name = type === "merge" ? "TargetPlotLineID" : "";
syncTargetOptions(); syncTargetOptions();
}; };
form.addEventListener("submit", () => {
form.querySelectorAll("[data-split-target-mirror]").forEach(input => input.remove());
if (eventTypeName() !== "split" || !splitTargets) return;
[...splitTargets.selectedOptions].forEach(option => {
if (!option.value || option.disabled) return;
const mirror = document.createElement("input");
mirror.type = "hidden";
mirror.name = "SelectedTargetPlotLineIDs";
mirror.value = option.value;
mirror.dataset.splitTargetMirror = "true";
form.append(mirror);
});
});
plotEventType?.addEventListener("change", sync); plotEventType?.addEventListener("change", sync);
sourcePlotLine?.addEventListener("change", sync); sourcePlotLine?.addEventListener("change", sync);
sourceThread?.addEventListener("change", sync); sourceThread?.addEventListener("change", sync);

View File

@ -882,10 +882,13 @@ body.timeline-inspector-resizing {
} }
.plot-event-svg-node.plotline-state-abandoned circle, .plot-event-svg-node.plotline-state-abandoned circle,
.plot-event-svg-node.plotline-state-abandoned path, .plot-event-svg-node.plotline-state-abandoned path {
.plot-event-svg-node.plotline-state-abandoned line {
fill: #9b2f36; fill: #9b2f36;
stroke: #9b2f36; stroke: #ffffff;
}
.plot-event-svg-node.plotline-state-abandoned line {
stroke: #ffffff;
} }
.plotline-state-questionable-marker path { .plotline-state-questionable-marker path {

File diff suppressed because one or more lines are too long

View File

@ -1453,18 +1453,29 @@
const drawNode = (point, colour, state) => { const drawNode = (point, colour, state) => {
const group = makeSvg("g", { class: `plot-event-svg-node plot-event-svg-node-${point.type} plotline-state-${state}` }); const group = makeSvg("g", { class: `plot-event-svg-node plot-event-svg-node-${point.type} plotline-state-${state}` });
const isStructural = point.type === "branch" || point.type === "split" || point.type === "merge";
if (point.type === "twist") { if (point.type === "twist") {
group.append(makeSvg("path", { 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`, d: `M ${point.x} ${point.y - 9} L ${point.x + 9} ${point.y} L ${point.x} ${point.y + 9} L ${point.x - 9} ${point.y} Z`,
fill: colour fill: colour
})); }));
} else if (point.type === "abandon") { } 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("circle", { cx: point.x, cy: point.y, r: 9, fill: "#fff5f5" }));
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 - 7, y1: point.y - 7, x2: point.x + 7, y2: point.y + 7 }));
group.append(makeSvg("line", { x1: point.x + 7, y1: point.y - 7, x2: point.x - 7, y2: point.y + 7 }));
} else if (point.type === "reveal") {
group.append(makeSvg("path", {
d: `M ${point.x} ${point.y - 10} L ${point.x + 3} ${point.y - 3} L ${point.x + 10} ${point.y - 3} L ${point.x + 4} ${point.y + 2} L ${point.x + 6} ${point.y + 10} L ${point.x} ${point.y + 5} L ${point.x - 6} ${point.y + 10} L ${point.x - 4} ${point.y + 2} L ${point.x - 10} ${point.y - 3} L ${point.x - 3} ${point.y - 3} Z`,
fill: colour
}));
} else { } else {
group.append(makeSvg("circle", { cx: point.x, cy: point.y, r: 6, fill: colour })); group.append(makeSvg("circle", { cx: point.x, cy: point.y, r: isStructural ? 9 : 7.5, fill: colour }));
if (point.type === "resolve") { if (point.type === "resolve") {
group.append(makeSvg("circle", { cx: point.x, cy: point.y, r: 10, fill: "none", stroke: colour })); group.append(makeSvg("circle", { cx: point.x, cy: point.y, r: 12, fill: "none", stroke: colour }));
}
if (isStructural) {
group.append(makeSvg("circle", { cx: point.x, cy: point.y, r: 13, fill: "none", stroke: colour }));
} }
} }
svg.append(group); svg.append(group);
@ -1492,11 +1503,12 @@
} }
const target = { x: source.x, y: rowCenterY(targetRow) }; const target = { x: source.x, y: rowCenterY(targetRow) };
const curveWidth = Math.max(34, Math.min(78, Math.abs(target.y - source.y) * 0.42)); const flowX = Math.max(28, Math.min(46, Math.abs(target.y - source.y) * 0.28));
const controlX = source.x + curveWidth; const targetX = source.x + flowX;
const midX = source.x + flowX / 2;
svg.append(makeSvg("path", { svg.append(makeSvg("path", {
class: `plot-line-svg-connector plot-line-svg-connector-${type}`, class: `plot-line-svg-connector plot-line-svg-connector-${type}`,
d: `M ${source.x} ${source.y} C ${controlX} ${source.y}, ${controlX} ${target.y}, ${target.x} ${target.y}`, d: `M ${source.x} ${source.y} C ${midX} ${source.y}, ${midX} ${target.y}, ${targetX} ${target.y}`,
stroke: colour stroke: colour
})); }));
}; };

File diff suppressed because one or more lines are too long