@dcrackel/meyersquaredui
Version:
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
211 lines (174 loc) • 6.96 kB
JavaScript
// TableauLayoutEngine.js
export const TableauLayoutTuning = {
baseGapPx: 0,
padTopBiasPx: 0,
padByDepthPx: [0, 32, 96, 224, 480, 992, 2016],
gapByDepthPx: [20, 84, 212, 468, 980, 2004, 0],
padByDepthPxCompact: [0, 26, 76, 176, 376, 365, 0],
gapByDepthPxCompact: [10, 60, 160, 360, 760, 1000, 0],
finalPadBiasPx: 0,
thirdPlacePadBiasPx: 120,
splitFinalPadTopByBracket: {
"Table of 64": -45,
"Table of 128": 730,
"Table of 32": 0,
},
compact: {
enabledFor: ["Table of 64", "Table of 128", "Table of 256"],
interFencerGapPx: 1,
},
splitFinalPadBiasPx: 0,
};
// layout/TableauLayoutEngine.js
export function createDomAnchorLayoutProvider({ rootEl, getOutAnchorEl, getInAnchorEl }) {
function rootRect() {
return rootEl.getBoundingClientRect();
}
function snap(v) {
return Math.round(v) + 0.5;
}
function pointRelativeToRoot(el) {
const rr = rootRect();
const r = el.getBoundingClientRect();
return {
x: snap((r.left - rr.left) + r.width / 2),
y: snap((r.top - rr.top) + r.height / 2),
};
}
function getCanvasSize() {
const rr = rootRect();
// IMPORTANT: pad the canvas so strokes at the edges don't get clipped
const pad = 8; // try 8–16
return {
w: Math.ceil(rr.width) + pad,
h: Math.ceil(rr.height) + pad,
};
}
function getOutPoint(nodeId) {
const el = getOutAnchorEl(nodeId);
if (!el) return null;
return pointRelativeToRoot(el);
}
function getInPoint(nodeId) {
const el = getInAnchorEl(nodeId);
if (!el) return null;
return pointRelativeToRoot(el);
}
return { getOutPoint, getInPoint, getCanvasSize };
}
export function buildBracketPaths({ bouts, getOutPoint, getInPoint }) {
const paths = [];
// group children by their parent (DENextBoutId)
const childrenByParent = new Map();
for (const bout of bouts) {
const fromId = bout?.DEBoutId;
const toId = bout?.DENextBoutId;
if (!fromId || !toId) continue;
if (!childrenByParent.has(toId)) childrenByParent.set(toId, []);
childrenByParent.get(toId).push(fromId);
}
// tuning knobs
const joinFromParent = 120; // how far "inside" the round the trunk sits
const minJoinFromChild = 16; // keep trunk away from child anchor
const joinGap = 6; // keep trunk away from parent anchor
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
let missing = 0;
for (const [parentId, childIds] of childrenByParent.entries()) {
const parent = getInPoint(parentId);
if (!parent) { missing++; continue; }
// we only really care about the first 2 (standard bracket)
const c1 = getOutPoint(childIds[0]);
const c2 = childIds.length > 1 ? getOutPoint(childIds[1]) : null;
// fallback: 1-child connection (bye / incomplete)
if (!c1 || !c2) {
if (!c1) { missing++; continue; }
// Determine if child is left or right of parent
const sign = (c1.x > parent.x) ? +1 : -1;
// place trunk between child and parent
let trunkX;
if (sign === -1) {
// parent to the right
trunkX = clamp(
parent.x - joinFromParent,
c1.x + minJoinFromChild,
parent.x - joinGap
);
} else {
// parent to the left
trunkX = clamp(
parent.x + joinFromParent,
parent.x + joinGap,
c1.x - minJoinFromChild
);
}
paths.push({
key: `${childIds[0]}->${parentId}`,
d: `M ${c1.x} ${c1.y} L ${trunkX} ${c1.y} L ${trunkX} ${parent.y} L ${parent.x} ${parent.y}`
});
continue;
}
// Two-child merged connector
const top = c1.y <= c2.y ? c1 : c2;
const bot = c1.y <= c2.y ? c2 : c1;
// SPECIAL CASE: children are on opposite sides of parent (split-final situation)
// i.e., one child left of parent and one child right of parent
const oppositeSides = (c1.x - parent.x) * (c2.x - parent.x) < 0;
if (oppositeSides) {
// draw two separate elbows into the parent (no shared trunk)
const elbowFor = (childPoint, childKey) => {
const sign = (childPoint.x > parent.x) ? +1 : -1;
const midX = parent.x + sign * joinGap; // approach parent from correct side
return {
key: `${childKey}->final(${parentId})`,
d: `M ${childPoint.x} ${childPoint.y} L ${midX} ${childPoint.y} L ${midX} ${parent.y} L ${parent.x} ${parent.y}`
};
};
paths.push(elbowFor(c1, childIds[0]));
paths.push(elbowFor(c2, childIds[1]));
continue;
}
// NORMAL CASE: both children are on same side of parent (LTR or RTL half)
const avgChildX = (c1.x + c2.x) / 2;
const sign = (avgChildX > parent.x) ? +1 : -1;
const minChildX = Math.min(c1.x, c2.x);
const maxChildX = Math.max(c1.x, c2.x);
let trunkX;
if (sign === -1) {
// children left, parent right
trunkX = clamp(
parent.x - joinFromParent,
maxChildX + minJoinFromChild,
parent.x - joinGap
);
} else {
// children right, parent left
trunkX = clamp(
parent.x + joinFromParent,
parent.x + joinGap,
minChildX - minJoinFromChild
);
}
// 1) horizontal from each child to trunk
paths.push({
key: `${childIds[0]}->join(${parentId})`,
d: `M ${c1.x} ${c1.y} L ${trunkX} ${c1.y}`
});
paths.push({
key: `${childIds[1]}->join(${parentId})`,
d: `M ${c2.x} ${c2.y} L ${trunkX} ${c2.y}`
});
// 2) vertical trunk
paths.push({
key: `trunk(${parentId})`,
d: `M ${trunkX} ${top.y} L ${trunkX} ${bot.y}`
});
// 3) run from trunk midpoint to parent
const midY = (top.y + bot.y) / 2;
paths.push({
key: `join(${parentId})->parent`,
d: `M ${trunkX} ${midY} L ${parent.x} ${midY}`
});
}
console.log({ parents: childrenByParent.size, paths: paths.length, missing });
return paths;
}