您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

brush-session.ts 5.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import { current } from "immer"
  2. import { Bounds, Data, Shape, ShapeType } from "types"
  3. import BaseSession from "./base-session"
  4. import shapeUtils from "utils/shape-utils"
  5. import { getBoundsFromPoints } from "utils/utils"
  6. import * as vec from "utils/vec"
  7. import {
  8. intersectCircleBounds,
  9. intersectPolylineBounds,
  10. } from "utils/intersections"
  11. interface BrushSnapshot {
  12. selectedIds: Set<string>
  13. shapes: { id: string; test: (bounds: Bounds) => boolean }[]
  14. }
  15. export default class BrushSession extends BaseSession {
  16. origin: number[]
  17. snapshot: BrushSnapshot
  18. constructor(data: Data, point: number[]) {
  19. super(data)
  20. this.origin = vec.round(point)
  21. this.snapshot = BrushSession.getSnapshot(data)
  22. }
  23. update = (data: Data, point: number[]) => {
  24. const { origin, snapshot } = this
  25. const brushBounds = getBoundsFromPoints(origin, point)
  26. for (let { test, id } of snapshot.shapes) {
  27. if (test(brushBounds)) {
  28. data.selectedIds.add(id)
  29. } else if (data.selectedIds.has(id)) {
  30. data.selectedIds.delete(id)
  31. }
  32. }
  33. data.brush = brushBounds
  34. }
  35. cancel = (data: Data) => {
  36. data.brush = undefined
  37. data.selectedIds = new Set(this.snapshot.selectedIds)
  38. }
  39. complete = (data: Data) => {
  40. data.brush = undefined
  41. }
  42. /**
  43. * Get a snapshot of the current selected ids, for each shape that is
  44. * not already selected, the shape's id and a test to see whether the
  45. * brush will intersect that shape. For tests, start broad -> fine.
  46. * @param data
  47. * @returns
  48. */
  49. static getSnapshot(data: Data): BrushSnapshot {
  50. const {
  51. selectedIds,
  52. document: { pages },
  53. currentPageId,
  54. } = current(data)
  55. return {
  56. selectedIds: new Set(data.selectedIds),
  57. shapes: Object.values(pages[currentPageId].shapes)
  58. .filter((shape) => !selectedIds.has(shape.id))
  59. .map((shape) => {
  60. switch (shape.type) {
  61. case ShapeType.Dot: {
  62. const bounds = shapeUtils[shape.type].getBounds(shape)
  63. return {
  64. id: shape.id,
  65. test: (brushBounds: Bounds) =>
  66. boundsContained(bounds, brushBounds) ||
  67. intersectCircleBounds(shape.point, 4, brushBounds).length > 0,
  68. }
  69. }
  70. case ShapeType.Circle: {
  71. const bounds = shapeUtils[shape.type].getBounds(shape)
  72. return {
  73. id: shape.id,
  74. test: (brushBounds: Bounds) =>
  75. boundsContained(bounds, brushBounds) ||
  76. intersectCircleBounds(
  77. vec.addScalar(shape.point, shape.radius),
  78. shape.radius,
  79. brushBounds
  80. ).length > 0,
  81. }
  82. }
  83. case ShapeType.Rectangle: {
  84. const bounds = shapeUtils[shape.type].getBounds(shape)
  85. return {
  86. id: shape.id,
  87. test: (brushBounds: Bounds) =>
  88. boundsContained(bounds, brushBounds) ||
  89. boundsCollide(bounds, brushBounds),
  90. }
  91. }
  92. case ShapeType.Polyline: {
  93. const bounds = shapeUtils[shape.type].getBounds(shape)
  94. const points = shape.points.map((point) =>
  95. vec.add(point, shape.point)
  96. )
  97. return {
  98. id: shape.id,
  99. test: (brushBounds: Bounds) =>
  100. boundsContained(bounds, brushBounds) ||
  101. (boundsCollide(bounds, brushBounds) &&
  102. intersectPolylineBounds(points, brushBounds).length > 0),
  103. }
  104. }
  105. default: {
  106. return undefined
  107. }
  108. }
  109. })
  110. .filter(Boolean),
  111. }
  112. }
  113. }
  114. /**
  115. * Get whether two bounds collide.
  116. * @param a Bounds
  117. * @param b Bounds
  118. * @returns
  119. */
  120. export function boundsCollide(a: Bounds, b: Bounds) {
  121. return !(
  122. a.maxX < b.minX ||
  123. a.minX > b.maxX ||
  124. a.maxY < b.minY ||
  125. a.minY > b.maxY
  126. )
  127. }
  128. /**
  129. * Get whether the bounds of A contain the bounds of B. A perfect match will return true.
  130. * @param a Bounds
  131. * @param b Bounds
  132. * @returns
  133. */
  134. export function boundsContain(a: Bounds, b: Bounds) {
  135. return (
  136. a.minX < b.minX && a.minY < b.minY && a.maxY > b.maxY && a.maxX > b.maxX
  137. )
  138. }
  139. /**
  140. * Get whether the bounds of A are contained by the bounds of B.
  141. * @param a Bounds
  142. * @param b Bounds
  143. * @returns
  144. */
  145. export function boundsContained(a: Bounds, b: Bounds) {
  146. return boundsContain(b, a)
  147. }
  148. /**
  149. * Get whether two bounds are identical.
  150. * @param a Bounds
  151. * @param b Bounds
  152. * @returns
  153. */
  154. export function boundsAreEqual(a: Bounds, b: Bounds) {
  155. return !(
  156. b.maxX !== a.maxX ||
  157. b.minX !== a.minX ||
  158. b.maxY !== a.maxY ||
  159. b.minY !== a.minY
  160. )
  161. }
  162. /**
  163. * Get whether a point is inside of a bounds.
  164. * @param A
  165. * @param b
  166. * @returns
  167. */
  168. export function pointInBounds(A: number[], b: Bounds) {
  169. return !(A[0] < b.minX || A[0] > b.maxX || A[1] < b.minY || A[1] > b.maxY)
  170. }