export type NStep = { id: number; parentIds: number[]; name: string; bound: { width: number; height: number; left: number; top: number }; prevId: number; raw: any; level: number; }; const idFactory = () => { let id = 0; return () => id++; }; const getId = idFactory(); const _flatSteps = ( steps: any, parentIds: number[] = [], level = 0, parallel = false, nsteps: NStep[] = [] ): number[] => { const lonelyStepIds: number[] = []; let tempLevel = level; let tempParentIds = parentIds; for (const step of steps) { const id = getId(); const stepParallel = parallel; if (!stepParallel && lonelyStepIds.length) { tempParentIds = [...lonelyStepIds]; tempLevel = Math.max( ...nsteps .filter((nstep) => lonelyStepIds.includes(nstep.id)) .map((nstep) => nstep.level) ) + 1; lonelyStepIds.length = 0; } let tempPrevId = -1; for (let i = nsteps.length - 1; i >= 0; i--) { if (nsteps[i].level === tempLevel) { tempPrevId = nsteps[i].id; break; } } const nstep = { id, name: step.name, parentIds: tempParentIds, prevId: tempPrevId, raw: step, level: tempLevel, } as NStep; nsteps.push(nstep); if (step.steps && step.steps.length) { lonelyStepIds.push( ..._flatSteps( step.steps as any, [id], tempLevel + 1, step.subStepsParallel === "True" || step.serviceTypeParallel === "True", nsteps ) ); } else { lonelyStepIds.push(nstep.id); } } return lonelyStepIds; }; type Size = { width: number; height: number }; export const attachBoundAttrib = ( steps: NStep[], getStepSize: (step: any) => { width: number; height: number } ) => { steps.sort((a, b) => a.level - b.level); const stepSizeMap = new Map(); for (const step of steps) { const size = getStepSize(step.raw); stepSizeMap.set(step, size); } const treeSizeMap = new Map(); const levelHeights: number[] = new Array( steps[steps.length - 1].level + 1 ).fill(0); for (let i = steps.length - 1; i >= 0; i--) { const root = steps[i]; const child = steps.filter( (oStep) => oStep.parentIds.length === 1 && oStep.parentIds.some((id) => root.id === id) ); const rootBound = stepSizeMap.get(steps[i])!; const topLevel = root.level; let treeWidth = rootBound.width; let treeHeight = rootBound.height; levelHeights[topLevel] = Math.max(levelHeights[topLevel], treeHeight); if (child.length) { const bottomLevel = child[child.length - 1]?.level; for (let i = topLevel; i <= bottomLevel; i++) { let width = 0; let height = 0; for (let j = 0; j < child.length; j++) { if (child[j].level === i) { const childTreeSize = treeSizeMap.get(child[j])!; width += childTreeSize.width; height = Math.max(childTreeSize.height + rootBound.height, height); } } treeWidth = Math.max(treeWidth, width); treeHeight = Math.max(treeHeight, height); } } treeSizeMap.set(root, { width: treeWidth, height: treeHeight }); } const levelsSteps: NStep[][] = []; let level = 0; while (true) { const levelSteps = steps.filter((step) => step.level === level); if (levelSteps.length === 0) { break; } else { levelsSteps[level] = levelSteps; } level++; } const getStepOffset = (step: NStep) => { const stepBound = stepSizeMap.get(step)!; const treeBound = treeSizeMap.get(step)!; let offset = 0; let prevId = step.prevId; while (prevId !== -1) { const prevStep = steps.find((tstep) => tstep.id === prevId)!; const treeBound = treeSizeMap.get(prevStep)!; offset += treeBound.width; prevId = prevStep.prevId; } const prevStep = steps.find((tstep) => tstep.id === step.prevId)!; if (prevStep) { offset = Math.max(prevStep.bound.left + prevStep.bound.width, offset); } let left = offset + (treeBound.width - stepBound.width) / 2; // 如果超出预设范围则修正 if (step.parentIds.length === 1 && step.parentIds[0] !== -1) { if (step.parentIds[0] !== -1) { const parent = steps.find((pstep) => pstep.id === step.parentIds[0])!; const paretnBound = treeSizeMap.get(parent)!; if (parent.bound.left - left > paretnBound.width / 2) { // const width = stepBound.width; const width = steps .filter((step) => step.parentIds.includes(parent.id)) .reduce((t, c) => t + stepSizeMap.get(c)!.width, 0); left = Math.max( left, parent.bound.left + (parent.bound.width - width) / 2 ); } } } return left; }; let top = 0; let width = 0; for (let i = 0; i < levelsSteps.length; i++) { let levelHeight = levelHeights[i]; for (const step of levelsSteps[i]) { const stepSize = stepSizeMap.get(step)!; step.bound = { left: getStepOffset(step), top: top + (levelHeight - stepSize.height) / 2, ...stepSize, }; width = Math.max(step.bound.left + step.bound.width, width); } top += levelHeight; } const getParentBound = (step: NStep) => { const parentSteps = step.parentIds.map( (id) => steps.find((parentStep) => parentStep.id === id)! ); let left = 0; let top = 0; let width = 0, height = 0; for (const step of parentSteps) { left = Math.min(step.bound.left, left); top = Math.min(step.bound.top, top); width += step.bound.width; height += step.bound.height; } return { width, height, left, top, }; }; const getCompleteTree = (root: NStep) => { const completeTree = [root]; const completeTreeIds = [root.id]; const levelsChilds = levelsSteps.slice(root.level); for (const childs of levelsChilds) { for (const step of childs) { if (step.parentIds.some((id) => completeTreeIds.includes(id))) { completeTree.push(step); completeTreeIds.push(step.id); } } } return completeTree; }; // 偏移所有多父级树 for (let i = 0; i < levelsSteps.length; i++) { for (const step of levelsSteps[i]) { if (step.parentIds.length <= 1) continue; const parentBound = getParentBound(step); const offset = (parentBound.width - treeSizeMap.get(step)!.width) / 2; getCompleteTree(step).forEach((ctStep) => { ctStep.bound.left += offset; }); } } let left = 0, right = 0; for (let i = 0; i < steps.length; i++) { left = Math.min(steps[i].bound.left, left); right = Math.max(steps[i].bound.left + steps[i].bound.width, right); } return { left, right, top: 0, bottom: top, levelHeights }; }; export const flatSteps = (data: any) => { const nsteps: NStep[] = []; _flatSteps(data, [], 0, false, nsteps); return nsteps; }; const ctx = document.createElement("canvas").getContext("2d")!; export const getTextBound = ( text: string, padding: number[], margin: number[], font: string ) => { ctx.font = font; const textMetrics = ctx.measureText(text); const width = textMetrics.width + (padding[1] + margin[1]) * 2; const height = textMetrics.hangingBaseline + (padding[0] + margin[0]) * 2; return { width, height }; };