Skip to content

Commit

Permalink
separate seg hierarchy/web from event-placement files
Browse files Browse the repository at this point in the history
  • Loading branch information
arshaw committed Jun 12, 2021
1 parent 9025f16 commit 8e56095
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 237 deletions.
2 changes: 1 addition & 1 deletion packages/common/src/main.ts
Expand Up @@ -167,7 +167,7 @@ export {
SegSpan, SegRect, SegHierarchy, SegEntry, SegInsertion, buildEntryKey,
getEntrySpanEnd, binarySearch, SegEntryGroup, groupIntersectingEntries,
joinSpans, intersectSpans,
} from './event-placement'
} from './seg-hierarchy'

export {
Interaction,
Expand Down
File renamed without changes.
5 changes: 3 additions & 2 deletions packages/timegrid/src/TimeCol.tsx
Expand Up @@ -6,7 +6,8 @@ import {
import { TimeColMoreLink } from './TimeColMoreLink'
import { TimeColsSeg } from './TimeColsSeg'
import { TimeColsSlatsCoords } from './TimeColsSlatsCoords'
import { computeFgSegPlacements, computeSegVCoords, WebSegRect } from './event-placement'
import { SegWebRect } from './seg-web'
import { computeFgSegPlacements, computeSegVCoords } from './event-placement'
import { TimeColEvent } from './TimeColEvent'
import { TimeColMisc } from './TimeColMisc'

Expand Down Expand Up @@ -252,7 +253,7 @@ export class TimeCol extends BaseComponent<TimeColProps> {
))
}

computeSegHStyle(segHCoords: WebSegRect) {
computeSegHStyle(segHCoords: SegWebRect) {
let { isRtl, options } = this.context
let shouldOverlap = options.slotEventOverlap
let nearCoord = segHCoords.levelCoord // the left side if LTR. the right side if RTL. floating-point
Expand Down
236 changes: 2 additions & 234 deletions packages/timegrid/src/event-placement.ts
@@ -1,24 +1,19 @@
import {
SegSpan,
SegEntry,
SegHierarchy,
SegRect,
buildEntryKey,
getEntrySpanEnd,
binarySearch,
SegEntryGroup,
groupIntersectingEntries,
DateMarker,
} from '@fullcalendar/common'
import { TimeColsSeg } from './TimeColsSeg'
import { TimeColsSlatsCoords } from './TimeColsSlatsCoords'
import { SegWebRect, buildPositioning } from './seg-web'

// public interface
// ------------------------------------------------------------------------------------------

export interface TimeColFgSegPlacement {
seg: TimeColsSeg
rect: WebSegRect | null
rect: SegWebRect | null
}

export function computeSegVCoords(
Expand Down Expand Up @@ -47,8 +42,6 @@ export function computeSegVCoords(
return vcoords
}

// TODO: lots of conversion between objects in here
// change internals to accept public objects
export function computeFgSegPlacements(
segs: TimeColsSeg[],
segVCoords: SegSpan[], // might not have for every seg
Expand Down Expand Up @@ -87,228 +80,3 @@ export function computeFgSegPlacements(

return { segPlacements, hiddenGroups }
}








// internals
// ------------------------------------------------------------------------------------------

interface SegNode extends SegEntry {
nextLevelNodes: SegNode[] // with highest-pressure first
}

type SegNodeAndPressure = [ SegNode, number ]

interface SegSiblingRange { // will ALWAYS have span of 1 or more items. if not, will be null
level: number
lateralStart: number
lateralEnd: number
}

export interface WebSegRect extends SegRect {
stackDepth: number
stackForward: number
}

// segInputs assumed sorted
function buildPositioning(
segInputs: SegEntry[],
strictOrder?: boolean,
maxStackCnt?: number,
): { segRects: WebSegRect[], hiddenGroups: SegEntryGroup[] } {
let hierarchy = new SegHierarchy()
if (strictOrder != null) {
hierarchy.strictOrder = strictOrder
}
if (maxStackCnt != null) {
hierarchy.maxStackCnt = maxStackCnt
}

let hiddenEntries = hierarchy.addSegs(segInputs)
let hiddenGroups = groupIntersectingEntries(hiddenEntries)

let web = buildWeb(hierarchy)
web = stretchWeb(web, 1) // all levelCoords/thickness will have 0.0-1.0
let segRects = webToRects(web)

return { segRects, hiddenGroups }
}

function buildWeb(hierarchy: SegHierarchy): SegNode[] {
const { entriesByLevel } = hierarchy

const buildNode = cacheable(
(level: number, lateral: number) => level + ':' + lateral,
(level: number, lateral: number): SegNodeAndPressure => {
let siblingRange = findNextLevelSegs(hierarchy, level, lateral)
let nextLevelRes = buildNodes(siblingRange, buildNode)
let entry = entriesByLevel[level][lateral]

return [
{ ...entry, nextLevelNodes: nextLevelRes[0] },
entry.thickness + nextLevelRes[1], // the pressure builds
]
},
)

return buildNodes(
entriesByLevel.length
? { level: 0, lateralStart: 0, lateralEnd: entriesByLevel[0].length }
: null,
buildNode,
)[0]
}

function buildNodes(
siblingRange: SegSiblingRange | null,
buildNode: (level: number, lateral: number) => SegNodeAndPressure,
): [SegNode[], number] { // number is maxPressure
if (!siblingRange) {
return [[], 0]
}

let { level, lateralStart, lateralEnd } = siblingRange
let lateral = lateralStart
let pairs: SegNodeAndPressure[] = []

while (lateral < lateralEnd) {
pairs.push(buildNode(level, lateral))
lateral += 1
}

pairs.sort(cmpDescPressures)

return [
pairs.map(extractNode),
pairs[0][1], // first item's pressure
]
}

function cmpDescPressures(a: SegNodeAndPressure, b: SegNodeAndPressure) { // sort pressure high -> low
return b[1] - a[1]
}

function extractNode(a: SegNodeAndPressure): SegNode {
return a[0]
}

function findNextLevelSegs(hierarchy: SegHierarchy, subjectLevel: number, subjectLateral: number): SegSiblingRange | null {
let { levelCoords, entriesByLevel } = hierarchy
let subjectEntry = entriesByLevel[subjectLevel][subjectLateral]
let afterSubject = levelCoords[subjectLevel] + subjectEntry.thickness
let levelCnt = levelCoords.length
let level = subjectLevel

// skip past levels that are too high up
for (; level < levelCnt && levelCoords[level] < afterSubject; level += 1) ; // do nothing

for (; level < levelCnt; level += 1) {
let entries = entriesByLevel[level]
let entry: SegEntry
let searchIndex = binarySearch(entries, subjectEntry.span.start, getEntrySpanEnd)
let lateralStart = searchIndex[0] + searchIndex[1] // if exact match (which doesn't collide), go to next one
let lateralEnd = lateralStart

while ( // loop through entries that horizontally intersect
(entry = entries[lateralEnd]) && // but not past the whole seg list
entry.span.start < subjectEntry.span.end
) { lateralEnd += 1 }

if (lateralStart < lateralEnd) {
return { level, lateralStart, lateralEnd }
}
}

return null
}

function stretchWeb(topLevelNodes: SegNode[], totalThickness: number): SegNode[] {
const stretchNode = cacheable(
(node: SegNode, startCoord: number, prevThickness: number) => buildEntryKey(node),
(node: SegNode, startCoord: number, prevThickness: number): [number, SegNode] => { // [startCoord, node]
let { nextLevelNodes, thickness } = node
let allThickness = thickness + prevThickness
let thicknessFraction = thickness / allThickness
let endCoord: number
let newChildren: SegNode[] = []

if (!nextLevelNodes.length) {
endCoord = totalThickness
} else {
for (let childNode of nextLevelNodes) {
if (endCoord === undefined) {
let res = stretchNode(childNode, startCoord, allThickness)
endCoord = res[0]
newChildren.push(res[1])
} else {
let res = stretchNode(childNode, endCoord, 0)
newChildren.push(res[1])
}
}
}

let newThickness = (endCoord - startCoord) * thicknessFraction
return [endCoord - newThickness, {
...node,
thickness: newThickness,
nextLevelNodes: newChildren,
}]
},
)

return topLevelNodes.map((node: SegNode) => stretchNode(node, 0, 0)[1])
}

// not sorted in any particular order
function webToRects(topLevelNodes: SegNode[]): WebSegRect[] {
let rects: WebSegRect[] = []

const processNode = cacheable(
(node: SegNode, levelCoord: number, stackDepth: number) => buildEntryKey(node),
(node: SegNode, levelCoord: number, stackDepth: number) => { // returns forwardPressure
let rect: WebSegRect = {
...node,
levelCoord,
stackDepth,
stackForward: 0, // will assign after recursing
}
rects.push(rect)

return (
rect.stackForward = processNodes(node.nextLevelNodes, levelCoord + node.thickness, stackDepth + 1) + 1
)
},
)

function processNodes(nodes: SegNode[], levelCoord: number, stackDepth: number) { // returns stackForward
let stackForward = 0
for (let node of nodes) {
stackForward = Math.max(processNode(node, levelCoord, stackDepth), stackForward)
}
return stackForward
}

processNodes(topLevelNodes, 0, 0)
return rects // TODO: sort rects by levelCoord to be consistent with toRects?
}

// TODO: move to general util

function cacheable<Args extends any[], Res>(
keyFunc: (...args: Args) => string,
workFunc: (...args: Args) => Res,
): ((...args: Args) => Res) {
const cache: { [key: string]: Res } = {}

return (...args: Args) => {
let key = keyFunc(...args)
return (key in cache)
? cache[key]
: (cache[key] = workFunc(...args))
}
}

0 comments on commit 8e56095

Please sign in to comment.