Phase 6E, scoped to the existing Plot Line SVG timeline renderer
This commit is contained in:
parent
5831172b1d
commit
f7cf477f70
@ -861,6 +861,8 @@ else
|
|||||||
<a class="thread-marker @markerClass"
|
<a class="thread-marker @markerClass"
|
||||||
data-plot-event-node
|
data-plot-event-node
|
||||||
data-plot-event-type="@threadEvent.PlotEventTypeName"
|
data-plot-event-type="@threadEvent.PlotEventTypeName"
|
||||||
|
data-target-plot-line-id="@threadEvent.TargetPlotLineID"
|
||||||
|
data-target-plot-line-ids="@string.Join(",", threadEvent.TargetPlotLineIDs)"
|
||||||
asp-controller="Timeline"
|
asp-controller="Timeline"
|
||||||
asp-action="Index"
|
asp-action="Index"
|
||||||
asp-route-ProjectID="@Model.Project.ProjectID"
|
asp-route-ProjectID="@Model.Project.ProjectID"
|
||||||
|
|||||||
@ -852,6 +852,13 @@ body.timeline-inspector-resizing {
|
|||||||
opacity: 0.72;
|
opacity: 0.72;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plot-line-svg-connector {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 3;
|
||||||
|
stroke-linecap: round;
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
.plot-event-svg-node circle,
|
.plot-event-svg-node circle,
|
||||||
.plot-event-svg-node path {
|
.plot-event-svg-node path {
|
||||||
stroke: #ffffff;
|
stroke: #ffffff;
|
||||||
|
|||||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1374,6 +1374,11 @@
|
|||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rowCenterY = (row) => {
|
||||||
|
const rowRect = row.getBoundingClientRect();
|
||||||
|
return rowRect.top - boardRect.top + rowRect.height / 2;
|
||||||
|
};
|
||||||
|
|
||||||
const pointFor = (row, marker) => {
|
const pointFor = (row, marker) => {
|
||||||
const slot = marker.closest("[data-plot-line-slot]");
|
const slot = marker.closest("[data-plot-line-slot]");
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
@ -1381,14 +1386,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const slotRect = slot.getBoundingClientRect();
|
const slotRect = slot.getBoundingClientRect();
|
||||||
const rowRect = row.getBoundingClientRect();
|
|
||||||
return {
|
return {
|
||||||
x: slotRect.left - boardRect.left + slotRect.width / 2,
|
x: slotRect.left - boardRect.left + slotRect.width / 2,
|
||||||
y: rowRect.top - boardRect.top + rowRect.height / 2,
|
y: rowCenterY(row),
|
||||||
type: (marker.dataset.plotEventType || "Progress").toLowerCase()
|
type: (marker.dataset.plotEventType || "Progress").toLowerCase(),
|
||||||
|
targetPlotLineId: marker.dataset.targetPlotLineId || "",
|
||||||
|
targetPlotLineIds: (marker.dataset.targetPlotLineIds || "")
|
||||||
|
.split(",")
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter(Boolean)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rowLookup = new Map();
|
||||||
|
board.querySelectorAll("[data-plot-line-row]").forEach((row) => {
|
||||||
|
if (row.dataset.plotLineId) {
|
||||||
|
rowLookup.set(row.dataset.plotLineId, row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const rowData = [...rowLookup.values()].map((row) => ({
|
||||||
|
row,
|
||||||
|
plotLineId: row.dataset.plotLineId,
|
||||||
|
colour: row.dataset.plotColour || "#2f6f63",
|
||||||
|
points: [...row.querySelectorAll("[data-plot-event-node]")]
|
||||||
|
.map((marker) => pointFor(row, marker))
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => a.x - b.x),
|
||||||
|
incomingStartXs: []
|
||||||
|
}));
|
||||||
|
const rowDataById = new Map(rowData.map((item) => [item.plotLineId, item]));
|
||||||
|
|
||||||
const drawNode = (point, colour) => {
|
const drawNode = (point, colour) => {
|
||||||
const group = makeSvg("g", { class: `plot-event-svg-node plot-event-svg-node-${point.type}` });
|
const group = makeSvg("g", { class: `plot-event-svg-node plot-event-svg-node-${point.type}` });
|
||||||
if (point.type === "twist") {
|
if (point.type === "twist") {
|
||||||
@ -1408,23 +1436,77 @@
|
|||||||
svg.append(group);
|
svg.append(group);
|
||||||
};
|
};
|
||||||
|
|
||||||
board.querySelectorAll("[data-plot-line-row]").forEach((row) => {
|
const drawConnector = (source, targetRow, colour, type) => {
|
||||||
const colour = row.dataset.plotColour || "#2f6f63";
|
if (!targetRow) {
|
||||||
const points = [...row.querySelectorAll("[data-plot-event-node]")]
|
if (window.console?.warn) {
|
||||||
.map((marker) => pointFor(row, marker))
|
window.console.warn(`Plot line ${type} connector skipped because target plot line data is missing.`);
|
||||||
.filter(Boolean)
|
}
|
||||||
.sort((a, b) => a.x - b.x);
|
return;
|
||||||
|
|
||||||
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
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 controlX = source.x + curveWidth;
|
||||||
|
svg.append(makeSvg("path", {
|
||||||
|
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}`,
|
||||||
|
stroke: colour
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
rowData.forEach((item) => {
|
||||||
|
item.points.forEach((point) => {
|
||||||
|
if (point.type === "branch" && point.targetPlotLineId) {
|
||||||
|
rowDataById.get(point.targetPlotLineId)?.incomingStartXs.push(point.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (point.type === "split") {
|
||||||
|
point.targetPlotLineIds.forEach((targetPlotLineId) => {
|
||||||
|
rowDataById.get(targetPlotLineId)?.incomingStartXs.push(point.x);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rowData.forEach(({ colour, points, incomingStartXs }) => {
|
||||||
|
if (points.length > 0) {
|
||||||
|
const explicitStart = points.find((point) => point.type === "start");
|
||||||
|
const incomingStartX = incomingStartXs.length ? Math.min(...incomingStartXs) : null;
|
||||||
|
const terminatingPoint = points.find((point) => point.type === "split" || point.type === "merge");
|
||||||
|
const first = points[0];
|
||||||
|
const last = points[points.length - 1];
|
||||||
|
const startX = incomingStartX !== null && !(explicitStart && explicitStart.x < incomingStartX)
|
||||||
|
? incomingStartX
|
||||||
|
: first.x;
|
||||||
|
const endX = terminatingPoint ? terminatingPoint.x : last.x;
|
||||||
|
|
||||||
|
if (endX > startX) {
|
||||||
|
svg.append(makeSvg("path", {
|
||||||
|
class: "plot-line-svg-path",
|
||||||
|
d: `M ${startX} ${first.y} L ${endX} ${first.y}`,
|
||||||
|
stroke: colour
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rowData.forEach(({ points, colour }) => {
|
||||||
|
points.forEach((point) => {
|
||||||
|
if (point.type === "branch" || point.type === "merge") {
|
||||||
|
drawConnector(point, rowLookup.get(point.targetPlotLineId), colour, point.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (point.type === "split" && point.targetPlotLineIds.length >= 2) {
|
||||||
|
point.targetPlotLineIds.forEach((targetPlotLineId) => {
|
||||||
|
drawConnector(point, rowLookup.get(targetPlotLineId), colour, point.type);
|
||||||
|
});
|
||||||
|
} else if (point.type === "split" && point.targetPlotLineIds.length > 0 && window.console?.warn) {
|
||||||
|
window.console.warn("Plot line split connector skipped because fewer than two targets are available.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rowData.forEach(({ points, colour }) => {
|
||||||
points.forEach((point) => drawNode(point, colour));
|
points.forEach((point) => drawNode(point, colour));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
2
PlotLine/wwwroot/js/site.min.js
vendored
2
PlotLine/wwwroot/js/site.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user