( initialStart: number, initialEnd: number, constraints?: ZoomSpanConstraints, )
| 78 | const copyRange = (r: ZoomRange): ZoomRange => ({ start: r.start, end: r.end }); |
| 79 | |
| 80 | export function createZoomState( |
| 81 | initialStart: number, |
| 82 | initialEnd: number, |
| 83 | constraints?: ZoomSpanConstraints, |
| 84 | ): ZoomStateWithConstraints { |
| 85 | let start = 0; |
| 86 | let end = 100; |
| 87 | let lastEmitted: ZoomRange | null = null; |
| 88 | |
| 89 | const listeners = new Set<ZoomRangeChangeCallback>(); |
| 90 | |
| 91 | let minSpan = (() => { |
| 92 | const v = Number.isFinite(constraints?.minSpan) ? (constraints!.minSpan as number) : DEFAULT_MIN_SPAN; |
| 93 | return clamp(Number.isFinite(v) ? v : 0, 0, 100); |
| 94 | })(); |
| 95 | |
| 96 | let maxSpan = (() => { |
| 97 | const v = Number.isFinite(constraints?.maxSpan) ? (constraints!.maxSpan as number) : DEFAULT_MAX_SPAN; |
| 98 | return clamp(Number.isFinite(v) ? v : 100, 0, 100); |
| 99 | })(); |
| 100 | |
| 101 | let normalizedMinSpan = Math.min(minSpan, maxSpan); |
| 102 | let normalizedMaxSpan = Math.max(minSpan, maxSpan); |
| 103 | |
| 104 | const emit = (): void => { |
| 105 | const next: ZoomRange = { start, end }; |
| 106 | if ( |
| 107 | lastEmitted !== null && |
| 108 | lastEmitted.start === next.start && |
| 109 | lastEmitted.end === next.end |
| 110 | ) { |
| 111 | return; |
| 112 | } |
| 113 | |
| 114 | lastEmitted = copyRange(next); |
| 115 | |
| 116 | // Emit to a snapshot so additions/removals during emit don't affect this flush. |
| 117 | const snapshot = Array.from(listeners); |
| 118 | for (const cb of snapshot) cb({ start, end }); |
| 119 | }; |
| 120 | |
| 121 | const toAnchor = (nextStart: number, nextEnd: number, spec?: ZoomRangeAnchor): { readonly center: number; readonly ratio: number } | undefined => { |
| 122 | if (!spec) return undefined; |
| 123 | if (typeof spec === 'string') { |
| 124 | switch (spec) { |
| 125 | case 'start': |
| 126 | return { center: nextStart, ratio: 0 }; |
| 127 | case 'end': |
| 128 | return { center: nextEnd, ratio: 1 }; |
| 129 | case 'center': |
| 130 | return { center: (nextStart + nextEnd) * 0.5, ratio: 0.5 }; |
| 131 | } |
| 132 | } |
| 133 | if (spec && Number.isFinite(spec.center) && Number.isFinite(spec.ratio)) { |
| 134 | return { center: spec.center, ratio: spec.ratio }; |
| 135 | } |
| 136 | return undefined; |
| 137 | }; |
no test coverage detected