geometry.client.spec.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /**
  2. * Drag geometry: point tests, dock-zone resolution against a real rectangle,
  3. * tab-strip insertion slots, the drag threshold, and divider arithmetic.
  4. */
  5. import { describe, expect, it } from 'vitest'
  6. import {
  7. containsPoint, dividerSizes, DRAG_THRESHOLD, floatRectAt, insertionIndex, movedRect,
  8. passedThreshold, resizedRect, zoneInRect,
  9. } from '../src/engine/geometry.ts'
  10. import type { PaneMeasure, Rect } from '../src/engine/geometry.ts'
  11. import { halvesFit, SPLIT_MINIMUMS } from '../src/engine/geometry.ts'
  12. import { FLOAT_DEFAULT_SIZE, FLOAT_MIN_SIZE } from '../src/index.ts'
  13. const PANE: Rect = { x: 100, y: 200, width: 400, height: 300 }
  14. describe('containsPoint', () => {
  15. it('includes the edges and excludes anything outside', () => {
  16. expect(containsPoint(PANE, 100, 200)).toBe(true)
  17. expect(containsPoint(PANE, 500, 500)).toBe(true)
  18. expect(containsPoint(PANE, 300, 350)).toBe(true)
  19. expect(containsPoint(PANE, 99, 350)).toBe(false)
  20. expect(containsPoint(PANE, 300, 501)).toBe(false)
  21. })
  22. })
  23. describe('zoneInRect', () => {
  24. it('reads the middle of a pane as its centre', () => {
  25. expect(zoneInRect(PANE, 300, 350)).toBe('center')
  26. })
  27. it('reads each edge band as its own region', () => {
  28. expect(zoneInRect(PANE, 110, 350)).toBe('left')
  29. expect(zoneInRect(PANE, 490, 350)).toBe('right')
  30. expect(zoneInRect(PANE, 300, 210)).toBe('top')
  31. expect(zoneInRect(PANE, 300, 490)).toBe('bottom')
  32. })
  33. it('treats a collapsed rectangle as all centre', () => {
  34. expect(zoneInRect({ x: 0, y: 0, width: 0, height: 0 }, 0, 0)).toBe('center')
  35. })
  36. })
  37. describe('insertionIndex', () => {
  38. const tabs: Rect[] = [
  39. { x: 0, y: 0, width: 100, height: 30 },
  40. { x: 100, y: 0, width: 100, height: 30 },
  41. { x: 200, y: 0, width: 100, height: 30 },
  42. ]
  43. it('places before a tab while left of its midpoint', () => {
  44. expect(insertionIndex(tabs, 10)).toBe(0)
  45. expect(insertionIndex(tabs, 49)).toBe(0)
  46. })
  47. it('places after a tab once past its midpoint', () => {
  48. expect(insertionIndex(tabs, 51)).toBe(1)
  49. expect(insertionIndex(tabs, 151)).toBe(2)
  50. expect(insertionIndex(tabs, 400)).toBe(3)
  51. })
  52. it('places at the start of an empty strip', () => {
  53. expect(insertionIndex([], 400)).toBe(0)
  54. })
  55. })
  56. describe('passedThreshold', () => {
  57. it('ignores travel under the threshold on both axes', () => {
  58. expect(passedThreshold(10, 10, 10, 10)).toBe(false)
  59. expect(passedThreshold(10, 10, 10 + DRAG_THRESHOLD - 1, 10)).toBe(false)
  60. expect(passedThreshold(10, 10, 10 + DRAG_THRESHOLD, 10)).toBe(true)
  61. expect(passedThreshold(10, 10, 10, 10 - DRAG_THRESHOLD)).toBe(true)
  62. })
  63. })
  64. describe('dividerSizes', () => {
  65. it('moves the change between the two neighbours only', () => {
  66. expect(dividerSizes([0.5, 0.5], 0, 0.1)).toEqual([0.6, 0.4])
  67. expect(dividerSizes([0.25, 0.25, 0.5], 1, -0.05)).toEqual([0.25, 0.2, 0.55])
  68. })
  69. it('leaves the fractions alone at a boundary that has no pair', () => {
  70. expect(dividerSizes([0.5, 0.5], 1, 0.1)).toEqual([0.5, 0.5])
  71. })
  72. })
  73. describe('floating rectangles', () => {
  74. const rect = { x: 100, y: 80, width: 300, height: 200 }
  75. it('moves without changing the size', () => {
  76. expect(movedRect(rect, 25, -15)).toEqual({ x: 125, y: 65, width: 300, height: 200 })
  77. })
  78. it('resizes from the origin and honours the minimum', () => {
  79. expect(resizedRect(rect, 40, 30, FLOAT_MIN_SIZE))
  80. .toEqual({ x: 100, y: 80, width: 340, height: 230 })
  81. expect(resizedRect(rect, -1000, -1000, FLOAT_MIN_SIZE))
  82. .toEqual({ x: 100, y: 80, ...FLOAT_MIN_SIZE })
  83. })
  84. it('places a new panel with its header under the drop point', () => {
  85. const dropped = floatRectAt(400, 300, FLOAT_DEFAULT_SIZE)
  86. expect(dropped.width).toBe(FLOAT_DEFAULT_SIZE.width)
  87. expect(dropped.x).toBeLessThan(400)
  88. expect(dropped.y).toBeLessThan(300)
  89. expect(floatRectAt(5, 5, FLOAT_DEFAULT_SIZE)).toMatchObject({ x: 0, y: 0 })
  90. })
  91. })
  92. describe('halvesFit — the room rule', () => {
  93. /** A pane whose strip shows `fixed` px of controls and one chip box of 60px; the fill takes the rest. */
  94. const measure = (width: number, height: number, fixed: number): PaneMeasure => {
  95. const strip = { x: 1, y: 1, width: width - 2, height: 36 }
  96. return { pane: { x: 0, y: 0, width, height }, strip, chipsWidth: 60, fillWidth: Math.max(0, strip.width - 60 - fixed) }
  97. }
  98. it('lets an unmeasured pane split: the rule only blocks on a positive reading', () => {
  99. expect(halvesFit(measure(0, 0, 104))).toEqual({ row: true, column: true })
  100. })
  101. it('needs each half to hold the strip\'s fixed controls plus one minimum chip', () => {
  102. // 520px: halves of 258px inside the borders, against 104 + 100.
  103. expect(halvesFit(measure(520, 600, 104)).row).toBe(true)
  104. // 208px: halves of 102px, short of 204.
  105. expect(halvesFit(measure(208, 600, 104)).row).toBe(false)
  106. // The boundary is inclusive: 2 * (204 + 2 borders) = 412; the divider takes no room.
  107. expect(halvesFit(measure(412, 600, 104)).row).toBe(true)
  108. expect(halvesFit(measure(411, 600, 104)).row).toBe(false)
  109. // A strip with fewer controls needs less: 2 * (144 + 2) = 292.
  110. expect(halvesFit(measure(291, 600, 44)).row).toBe(false)
  111. expect(halvesFit(measure(292, 600, 44)).row).toBe(true)
  112. })
  113. it('needs each half to hold the strip plus a minimum body for a column split', () => {
  114. // Halves of h / 2 - 2 against 36 + 48 = 84.
  115. expect(halvesFit(measure(420, 172, 104)).column).toBe(true)
  116. expect(halvesFit(measure(420, 171, 104)).column).toBe(false)
  117. })
  118. it('takes the minimums it is given, and the stylesheet\'s by default', () => {
  119. expect(SPLIT_MINIMUMS).toEqual({ divider: 0, chip: 100, body: 48 })
  120. expect(halvesFit(measure(208, 600, 104), { divider: 0, chip: 0, body: 0 }).row).toBe(false)
  121. expect(halvesFit(measure(220, 600, 104), { divider: 0, chip: 0, body: 0 }).row).toBe(true)
  122. })
  123. })