You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

state.ts 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  1. import { createSelectorHook, createState } from "@state-designer/react"
  2. import {
  3. clamp,
  4. getChildren,
  5. getCommonBounds,
  6. getPage,
  7. getShape,
  8. getSiblings,
  9. screenToWorld,
  10. } from "utils/utils"
  11. import * as vec from "utils/vec"
  12. import {
  13. Data,
  14. PointerInfo,
  15. Shape,
  16. ShapeType,
  17. Corner,
  18. Edge,
  19. CodeControl,
  20. } from "types"
  21. import inputs from "./inputs"
  22. import { defaultDocument } from "./data"
  23. import shapeUtilityMap, { getShapeUtils } from "lib/shape-utils"
  24. import history from "state/history"
  25. import * as Sessions from "./sessions"
  26. import commands from "./commands"
  27. import { updateFromCode } from "lib/code/generate"
  28. const initialData: Data = {
  29. isReadOnly: false,
  30. settings: {
  31. fontSize: 13,
  32. isDarkMode: false,
  33. isCodeOpen: false,
  34. },
  35. camera: {
  36. point: [0, 0],
  37. zoom: 1,
  38. },
  39. brush: undefined,
  40. boundsRotation: 0,
  41. pointedId: null,
  42. hoveredId: null,
  43. selectedIds: new Set([]),
  44. currentPageId: "page0",
  45. currentCodeFileId: "file0",
  46. codeControls: {},
  47. document: defaultDocument,
  48. }
  49. const state = createState({
  50. data: initialData,
  51. on: {
  52. ZOOMED_CAMERA: {
  53. do: "zoomCamera",
  54. },
  55. PANNED_CAMERA: {
  56. do: "panCamera",
  57. },
  58. SELECTED_SELECT_TOOL: { to: "selecting" },
  59. SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "dot" },
  60. SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "circle" },
  61. SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "ellipse" },
  62. SELECTED_RAY_TOOL: { unless: "isReadOnly", to: "ray" },
  63. SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
  64. SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
  65. SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
  66. TOGGLED_CODE_PANEL_OPEN: "toggleCodePanel",
  67. RESET_CAMERA: "resetCamera",
  68. },
  69. initial: "loading",
  70. states: {
  71. loading: {
  72. on: {
  73. MOUNTED: {
  74. do: "restoreSavedData",
  75. to: "ready",
  76. },
  77. },
  78. },
  79. ready: {
  80. on: {
  81. UNMOUNTED: [
  82. { unless: "isReadOnly", do: "forceSave" },
  83. { to: "loading" },
  84. ],
  85. },
  86. initial: "selecting",
  87. states: {
  88. selecting: {
  89. on: {
  90. SAVED: "forceSave",
  91. UNDO: { do: "undo" },
  92. REDO: { do: "redo" },
  93. CANCELLED: { do: "clearSelectedIds" },
  94. DELETED: { do: "deleteSelectedIds" },
  95. SAVED_CODE: "saveCode",
  96. GENERATED_FROM_CODE: ["setCodeControls", "setGeneratedShapes"],
  97. INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
  98. DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
  99. CHANGED_CODE_CONTROL: "updateControls",
  100. MOVED_TO_FRONT: "moveSelectionToFront",
  101. MOVED_TO_BACK: "moveSelectionToBack",
  102. MOVED_FORWARD: "moveSelectionForward",
  103. MOVED_BACKWARD: "moveSelectionBackward",
  104. },
  105. initial: "notPointing",
  106. states: {
  107. notPointing: {
  108. on: {
  109. SELECTED_ALL: "selectAll",
  110. POINTED_CANVAS: { to: "brushSelecting" },
  111. POINTED_BOUNDS: { to: "pointingBounds" },
  112. POINTED_BOUNDS_HANDLE: {
  113. if: "isPointingRotationHandle",
  114. to: "rotatingSelection",
  115. else: { to: "transformingSelection" },
  116. },
  117. MOVED_OVER_SHAPE: {
  118. if: "pointHitsShape",
  119. then: {
  120. unless: "shapeIsHovered",
  121. do: "setHoveredId",
  122. },
  123. else: { if: "shapeIsHovered", do: "clearHoveredId" },
  124. },
  125. UNHOVERED_SHAPE: "clearHoveredId",
  126. POINTED_SHAPE: [
  127. {
  128. if: "isPressingMetaKey",
  129. to: "brushSelecting",
  130. },
  131. "setPointedId",
  132. {
  133. unless: "isPointedShapeSelected",
  134. then: {
  135. if: "isPressingShiftKey",
  136. do: ["pushPointedIdToSelectedIds", "clearPointedId"],
  137. else: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
  138. },
  139. },
  140. {
  141. to: "pointingBounds",
  142. },
  143. ],
  144. },
  145. },
  146. pointingBounds: {
  147. on: {
  148. STOPPED_POINTING: [
  149. {
  150. if: "isPressingShiftKey",
  151. then: {
  152. if: "isPointedShapeSelected",
  153. do: "pullPointedIdFromSelectedIds",
  154. },
  155. else: {
  156. unless: "isPointingBounds",
  157. do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
  158. },
  159. },
  160. { to: "notPointing" },
  161. ],
  162. MOVED_POINTER: {
  163. unless: "isReadOnly",
  164. if: "distanceImpliesDrag",
  165. to: "draggingSelection",
  166. },
  167. },
  168. },
  169. rotatingSelection: {
  170. onEnter: "startRotateSession",
  171. onExit: "clearBoundsRotation",
  172. on: {
  173. MOVED_POINTER: "updateRotateSession",
  174. PANNED_CAMERA: "updateRotateSession",
  175. PRESSED_SHIFT_KEY: "keyUpdateRotateSession",
  176. RELEASED_SHIFT_KEY: "keyUpdateRotateSession",
  177. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  178. CANCELLED: { do: "cancelSession", to: "selecting" },
  179. },
  180. },
  181. transformingSelection: {
  182. onEnter: "startTransformSession",
  183. on: {
  184. MOVED_POINTER: "updateTransformSession",
  185. PANNED_CAMERA: "updateTransformSession",
  186. PRESSED_SHIFT_KEY: "keyUpdateTransformSession",
  187. RELEASED_SHIFT_KEY: "keyUpdateTransformSession",
  188. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  189. CANCELLED: { do: "cancelSession", to: "selecting" },
  190. },
  191. },
  192. draggingSelection: {
  193. onEnter: "startTranslateSession",
  194. on: {
  195. MOVED_POINTER: "updateTranslateSession",
  196. PANNED_CAMERA: "updateTranslateSession",
  197. PRESSED_SHIFT_KEY: "keyUpdateTranslateSession",
  198. RELEASED_SHIFT_KEY: "keyUpdateTranslateSession",
  199. PRESSED_ALT_KEY: "keyUpdateTranslateSession",
  200. RELEASED_ALT_KEY: "keyUpdateTranslateSession",
  201. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  202. CANCELLED: { do: "cancelSession", to: "selecting" },
  203. },
  204. },
  205. brushSelecting: {
  206. onEnter: [
  207. {
  208. unless: ["isPressingMetaKey", "isPressingShiftKey"],
  209. do: "clearSelectedIds",
  210. },
  211. "clearBoundsRotation",
  212. "startBrushSession",
  213. ],
  214. on: {
  215. MOVED_POINTER: "updateBrushSession",
  216. PANNED_CAMERA: "updateBrushSession",
  217. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  218. CANCELLED: { do: "cancelSession", to: "selecting" },
  219. },
  220. },
  221. },
  222. },
  223. dot: {
  224. initial: "creating",
  225. states: {
  226. creating: {
  227. on: {
  228. POINTED_CANVAS: {
  229. get: "newDot",
  230. do: "createShape",
  231. to: "dot.editing",
  232. },
  233. },
  234. },
  235. editing: {
  236. on: {
  237. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  238. CANCELLED: {
  239. do: ["cancelSession", "deleteSelectedIds"],
  240. to: "selecting",
  241. },
  242. },
  243. initial: "inactive",
  244. states: {
  245. inactive: {
  246. on: {
  247. MOVED_POINTER: {
  248. if: "distanceImpliesDrag",
  249. to: "dot.editing.active",
  250. },
  251. },
  252. },
  253. active: {
  254. onEnter: "startTranslateSession",
  255. on: {
  256. MOVED_POINTER: "updateTranslateSession",
  257. PANNED_CAMERA: "updateTranslateSession",
  258. },
  259. },
  260. },
  261. },
  262. },
  263. },
  264. circle: {
  265. initial: "creating",
  266. states: {
  267. creating: {
  268. on: {
  269. POINTED_CANVAS: {
  270. to: "circle.editing",
  271. },
  272. },
  273. },
  274. editing: {
  275. on: {
  276. STOPPED_POINTING: { to: "selecting" },
  277. CANCELLED: { to: "selecting" },
  278. MOVED_POINTER: {
  279. if: "distanceImpliesDrag",
  280. then: {
  281. get: "newDot",
  282. do: "createShape",
  283. to: "drawingShape.bounds",
  284. },
  285. },
  286. },
  287. },
  288. },
  289. },
  290. ellipse: {
  291. initial: "creating",
  292. states: {
  293. creating: {
  294. on: {
  295. CANCELLED: { to: "selecting" },
  296. POINTED_CANVAS: {
  297. to: "ellipse.editing",
  298. },
  299. },
  300. },
  301. editing: {
  302. on: {
  303. STOPPED_POINTING: { to: "selecting" },
  304. CANCELLED: { to: "selecting" },
  305. MOVED_POINTER: {
  306. if: "distanceImpliesDrag",
  307. then: {
  308. get: "newEllipse",
  309. do: "createShape",
  310. to: "drawingShape.bounds",
  311. },
  312. },
  313. },
  314. },
  315. },
  316. },
  317. rectangle: {
  318. initial: "creating",
  319. states: {
  320. creating: {
  321. on: {
  322. CANCELLED: { to: "selecting" },
  323. POINTED_CANVAS: {
  324. to: "rectangle.editing",
  325. },
  326. },
  327. },
  328. editing: {
  329. on: {
  330. STOPPED_POINTING: { to: "selecting" },
  331. CANCELLED: { to: "selecting" },
  332. MOVED_POINTER: {
  333. if: "distanceImpliesDrag",
  334. then: {
  335. get: "newRectangle",
  336. do: "createShape",
  337. to: "drawingShape.bounds",
  338. },
  339. },
  340. },
  341. },
  342. },
  343. },
  344. ray: {
  345. initial: "creating",
  346. states: {
  347. creating: {
  348. on: {
  349. CANCELLED: { to: "selecting" },
  350. POINTED_CANVAS: {
  351. get: "newRay",
  352. do: "createShape",
  353. to: "ray.editing",
  354. },
  355. },
  356. },
  357. editing: {
  358. on: {
  359. STOPPED_POINTING: { to: "selecting" },
  360. CANCELLED: { to: "selecting" },
  361. MOVED_POINTER: {
  362. if: "distanceImpliesDrag",
  363. to: "drawingShape.direction",
  364. },
  365. },
  366. },
  367. },
  368. },
  369. line: {
  370. initial: "creating",
  371. states: {
  372. creating: {
  373. on: {
  374. CANCELLED: { to: "selecting" },
  375. POINTED_CANVAS: {
  376. get: "newLine",
  377. do: "createShape",
  378. to: "line.editing",
  379. },
  380. },
  381. },
  382. editing: {
  383. on: {
  384. STOPPED_POINTING: { to: "selecting" },
  385. CANCELLED: { to: "selecting" },
  386. MOVED_POINTER: {
  387. if: "distanceImpliesDrag",
  388. to: "drawingShape.direction",
  389. },
  390. },
  391. },
  392. },
  393. },
  394. polyline: {},
  395. },
  396. },
  397. drawingShape: {
  398. on: {
  399. STOPPED_POINTING: {
  400. do: "completeSession",
  401. to: "selecting",
  402. },
  403. CANCELLED: {
  404. do: ["cancelSession", "deleteSelectedIds"],
  405. to: "selecting",
  406. },
  407. },
  408. initial: "drawingShapeBounds",
  409. states: {
  410. bounds: {
  411. onEnter: "startDrawTransformSession",
  412. on: {
  413. MOVED_POINTER: "updateTransformSession",
  414. PANNED_CAMERA: "updateTransformSession",
  415. },
  416. },
  417. direction: {
  418. onEnter: "startDirectionSession",
  419. on: {
  420. MOVED_POINTER: "updateDirectionSession",
  421. PANNED_CAMERA: "updateDirectionSession",
  422. },
  423. },
  424. },
  425. },
  426. },
  427. results: {
  428. // Dot
  429. newDot(data, payload: PointerInfo) {
  430. return shapeUtilityMap[ShapeType.Dot].create({
  431. point: screenToWorld(payload.point, data),
  432. })
  433. },
  434. // Ray
  435. newRay(data, payload: PointerInfo) {
  436. return shapeUtilityMap[ShapeType.Ray].create({
  437. point: screenToWorld(payload.point, data),
  438. })
  439. },
  440. // Line
  441. newLine(data, payload: PointerInfo) {
  442. return shapeUtilityMap[ShapeType.Line].create({
  443. point: screenToWorld(payload.point, data),
  444. direction: [0, 1],
  445. })
  446. },
  447. newCircle(data, payload: PointerInfo) {
  448. return shapeUtilityMap[ShapeType.Circle].create({
  449. point: screenToWorld(payload.point, data),
  450. radius: 1,
  451. })
  452. },
  453. newEllipse(data, payload: PointerInfo) {
  454. return shapeUtilityMap[ShapeType.Ellipse].create({
  455. point: screenToWorld(payload.point, data),
  456. radiusX: 1,
  457. radiusY: 1,
  458. })
  459. },
  460. newRectangle(data, payload: PointerInfo) {
  461. return shapeUtilityMap[ShapeType.Rectangle].create({
  462. point: screenToWorld(payload.point, data),
  463. size: [1, 1],
  464. })
  465. },
  466. },
  467. conditions: {
  468. isPointingBounds(data, payload: PointerInfo) {
  469. return payload.target === "bounds"
  470. },
  471. isReadOnly(data) {
  472. return data.isReadOnly
  473. },
  474. distanceImpliesDrag(data, payload: PointerInfo) {
  475. return vec.dist2(payload.origin, payload.point) > 16
  476. },
  477. isPointedShapeSelected(data) {
  478. return data.selectedIds.has(data.pointedId)
  479. },
  480. isPressingShiftKey(data, payload: PointerInfo) {
  481. return payload.shiftKey
  482. },
  483. isPressingMetaKey(data, payload: PointerInfo) {
  484. return payload.metaKey
  485. },
  486. shapeIsHovered(data, payload: { target: string }) {
  487. return data.hoveredId === payload.target
  488. },
  489. pointHitsShape(data, payload: { target: string; point: number[] }) {
  490. const shape = getShape(data, payload.target)
  491. return getShapeUtils(shape).hitTest(
  492. shape,
  493. screenToWorld(payload.point, data)
  494. )
  495. },
  496. isPointingRotationHandle(
  497. data,
  498. payload: { target: Edge | Corner | "rotate" }
  499. ) {
  500. return payload.target === "rotate"
  501. },
  502. },
  503. actions: {
  504. /* --------------------- Shapes --------------------- */
  505. createShape(data, payload: PointerInfo, shape: Shape) {
  506. const siblings = getChildren(data, shape.parentId)
  507. shape.childIndex =
  508. siblings.length > 0 ? siblings[siblings.length - 1].childIndex + 1 : 1
  509. getPage(data).shapes[shape.id] = shape
  510. data.selectedIds.clear()
  511. data.selectedIds.add(shape.id)
  512. },
  513. /* -------------------- Sessions -------------------- */
  514. // Shared
  515. cancelSession(data) {
  516. session?.cancel(data)
  517. session = undefined
  518. },
  519. completeSession(data) {
  520. session?.complete(data)
  521. session = undefined
  522. },
  523. // Brushing
  524. startBrushSession(data, payload: PointerInfo) {
  525. session = new Sessions.BrushSession(
  526. data,
  527. screenToWorld(payload.point, data)
  528. )
  529. },
  530. updateBrushSession(data, payload: PointerInfo) {
  531. session.update(data, screenToWorld(payload.point, data))
  532. },
  533. // Rotating
  534. startRotateSession(data, payload: PointerInfo) {
  535. session = new Sessions.RotateSession(
  536. data,
  537. screenToWorld(payload.point, data)
  538. )
  539. },
  540. keyUpdateRotateSession(data, payload: PointerInfo) {
  541. session.update(
  542. data,
  543. screenToWorld(inputs.pointer.point, data),
  544. payload.shiftKey
  545. )
  546. },
  547. updateRotateSession(data, payload: PointerInfo) {
  548. session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
  549. },
  550. // Dragging / Translating
  551. startTranslateSession(data, payload: PointerInfo) {
  552. session = new Sessions.TranslateSession(
  553. data,
  554. screenToWorld(payload.point, data),
  555. payload.altKey
  556. )
  557. },
  558. keyUpdateTranslateSession(
  559. data,
  560. payload: { shiftKey: boolean; altKey: boolean }
  561. ) {
  562. session.update(
  563. data,
  564. screenToWorld(inputs.pointer.point, data),
  565. payload.shiftKey,
  566. payload.altKey
  567. )
  568. },
  569. updateTranslateSession(data, payload: PointerInfo) {
  570. session.update(
  571. data,
  572. screenToWorld(payload.point, data),
  573. payload.shiftKey,
  574. payload.altKey
  575. )
  576. },
  577. // Dragging / Translating
  578. startTransformSession(
  579. data,
  580. payload: PointerInfo & { target: Corner | Edge }
  581. ) {
  582. session =
  583. data.selectedIds.size === 1
  584. ? new Sessions.TransformSingleSession(
  585. data,
  586. payload.target,
  587. screenToWorld(payload.point, data),
  588. false
  589. )
  590. : new Sessions.TransformSession(
  591. data,
  592. payload.target,
  593. screenToWorld(payload.point, data)
  594. )
  595. },
  596. startDrawTransformSession(data, payload: PointerInfo) {
  597. session = new Sessions.TransformSingleSession(
  598. data,
  599. Corner.BottomRight,
  600. screenToWorld(payload.point, data),
  601. true
  602. )
  603. },
  604. keyUpdateTransformSession(data, payload: PointerInfo) {
  605. session.update(
  606. data,
  607. screenToWorld(inputs.pointer.point, data),
  608. payload.shiftKey,
  609. payload.altKey
  610. )
  611. },
  612. updateTransformSession(data, payload: PointerInfo) {
  613. session.update(
  614. data,
  615. screenToWorld(payload.point, data),
  616. payload.shiftKey,
  617. payload.altKey
  618. )
  619. },
  620. // Direction
  621. startDirectionSession(data, payload: PointerInfo) {
  622. session = new Sessions.DirectionSession(
  623. data,
  624. screenToWorld(payload.point, data)
  625. )
  626. },
  627. updateDirectionSession(data, payload: PointerInfo) {
  628. session.update(data, screenToWorld(payload.point, data))
  629. },
  630. /* -------------------- Selection ------------------- */
  631. selectAll(data) {
  632. const { selectedIds } = data
  633. const page = getPage(data)
  634. selectedIds.clear()
  635. for (let id in page.shapes) {
  636. selectedIds.add(id)
  637. }
  638. },
  639. setHoveredId(data, payload: PointerInfo) {
  640. data.hoveredId = payload.target
  641. },
  642. clearHoveredId(data) {
  643. data.hoveredId = undefined
  644. },
  645. setPointedId(data, payload: PointerInfo) {
  646. data.pointedId = payload.target
  647. },
  648. clearPointedId(data) {
  649. data.pointedId = undefined
  650. },
  651. clearSelectedIds(data) {
  652. data.selectedIds.clear()
  653. },
  654. pullPointedIdFromSelectedIds(data) {
  655. const { selectedIds, pointedId } = data
  656. selectedIds.delete(pointedId)
  657. },
  658. pushPointedIdToSelectedIds(data) {
  659. data.selectedIds.add(data.pointedId)
  660. },
  661. moveSelectionToFront(data) {
  662. const { selectedIds } = data
  663. },
  664. moveSelectionToBack(data) {
  665. const { selectedIds } = data
  666. },
  667. moveSelectionForward(data) {
  668. const { selectedIds } = data
  669. const page = getPage(data)
  670. const shapes = Array.from(selectedIds.values()).map(
  671. (id) => page.shapes[id]
  672. )
  673. const shapesByParentId = shapes.reduce<Record<string, Shape[]>>(
  674. (acc, shape) => {
  675. if (acc[shape.parentId] === undefined) {
  676. acc[shape.parentId] = []
  677. }
  678. acc[shape.parentId].push(shape)
  679. return acc
  680. },
  681. {}
  682. )
  683. const visited = new Set<string>()
  684. for (let id in shapesByParentId) {
  685. const children = getChildren(data, id)
  686. shapesByParentId[id]
  687. .sort((a, b) => b.childIndex - a.childIndex)
  688. .forEach((shape) => {
  689. visited.add(shape.id)
  690. children.sort((a, b) => a.childIndex - b.childIndex)
  691. const index = children.indexOf(shape)
  692. const nextSibling = children[index + 1]
  693. if (!nextSibling || visited.has(nextSibling.id)) {
  694. // At the top already, no change
  695. return
  696. }
  697. const nextNextSibling = children[index + 2]
  698. if (!nextNextSibling) {
  699. // Moving to the top
  700. shape.childIndex = nextSibling.childIndex + 1
  701. return
  702. }
  703. shape.childIndex =
  704. (nextSibling.childIndex + nextNextSibling.childIndex) / 2
  705. })
  706. }
  707. },
  708. moveSelectionBackward(data) {
  709. const { selectedIds } = data
  710. const page = getPage(data)
  711. const shapes = Array.from(selectedIds.values()).map(
  712. (id) => page.shapes[id]
  713. )
  714. const shapesByParentId = shapes.reduce<Record<string, Shape[]>>(
  715. (acc, shape) => {
  716. if (acc[shape.parentId] === undefined) {
  717. acc[shape.parentId] = []
  718. }
  719. acc[shape.parentId].push(shape)
  720. return acc
  721. },
  722. {}
  723. )
  724. const visited = new Set<string>()
  725. for (let id in shapesByParentId) {
  726. const children = getChildren(data, id)
  727. shapesByParentId[id]
  728. .sort((a, b) => a.childIndex - b.childIndex)
  729. .forEach((shape) => {
  730. visited.add(shape.id)
  731. children.sort((a, b) => a.childIndex - b.childIndex)
  732. const index = children.indexOf(shape)
  733. const nextSibling = children[index - 1]
  734. if (!nextSibling || visited.has(nextSibling.id)) {
  735. // At the bottom already, no change
  736. return
  737. }
  738. const nextNextSibling = children[index - 2]
  739. if (!nextNextSibling) {
  740. // Moving to the bottom
  741. shape.childIndex = nextSibling.childIndex / 2
  742. return
  743. }
  744. shape.childIndex =
  745. (nextSibling.childIndex + nextNextSibling.childIndex) / 2
  746. })
  747. }
  748. },
  749. /* --------------------- Camera --------------------- */
  750. resetCamera(data) {
  751. data.camera.zoom = 1
  752. data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
  753. document.documentElement.style.setProperty("--camera-zoom", "1")
  754. },
  755. centerCamera(data) {
  756. const { shapes } = getPage(data)
  757. getCommonBounds()
  758. data.camera.zoom = 1
  759. data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
  760. document.documentElement.style.setProperty("--camera-zoom", "1")
  761. },
  762. zoomCamera(data, payload: { delta: number; point: number[] }) {
  763. const { camera } = data
  764. const p0 = screenToWorld(payload.point, data)
  765. camera.zoom = clamp(
  766. camera.zoom - (payload.delta / 100) * camera.zoom,
  767. 0.5,
  768. 3
  769. )
  770. const p1 = screenToWorld(payload.point, data)
  771. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  772. document.documentElement.style.setProperty(
  773. "--camera-zoom",
  774. camera.zoom.toString()
  775. )
  776. },
  777. panCamera(data, payload: { delta: number[]; point: number[] }) {
  778. const { camera } = data
  779. data.camera.point = vec.sub(
  780. camera.point,
  781. vec.div(payload.delta, camera.zoom)
  782. )
  783. },
  784. deleteSelectedIds(data) {
  785. const page = getPage(data)
  786. data.hoveredId = undefined
  787. data.pointedId = undefined
  788. data.selectedIds.forEach((id) => {
  789. delete page.shapes[id]
  790. // TODO: recursively delete children
  791. })
  792. data.selectedIds.clear()
  793. },
  794. /* ---------------------- History ---------------------- */
  795. // History
  796. popHistory() {
  797. history.pop()
  798. },
  799. forceSave(data) {
  800. history.save(data)
  801. },
  802. enableHistory() {
  803. history.enable()
  804. },
  805. disableHistory() {
  806. history.disable()
  807. },
  808. undo(data) {
  809. history.undo(data)
  810. },
  811. redo(data) {
  812. history.redo(data)
  813. },
  814. /* ---------------------- Code ---------------------- */
  815. closeCodePanel(data) {
  816. data.settings.isCodeOpen = false
  817. },
  818. openCodePanel(data) {
  819. data.settings.isCodeOpen = true
  820. },
  821. toggleCodePanel(data) {
  822. data.settings.isCodeOpen = !data.settings.isCodeOpen
  823. },
  824. setGeneratedShapes(
  825. data,
  826. payload: { shapes: Shape[]; controls: CodeControl[] }
  827. ) {
  828. commands.generate(data, data.currentPageId, payload.shapes)
  829. },
  830. setCodeControls(data, payload: { controls: CodeControl[] }) {
  831. data.codeControls = Object.fromEntries(
  832. payload.controls.map((control) => [control.id, control])
  833. )
  834. },
  835. increaseCodeFontSize(data) {
  836. data.settings.fontSize++
  837. },
  838. decreaseCodeFontSize(data) {
  839. data.settings.fontSize--
  840. },
  841. updateControls(data, payload: { [key: string]: any }) {
  842. for (let key in payload) {
  843. data.codeControls[key].value = payload[key]
  844. }
  845. history.disable()
  846. data.selectedIds.clear()
  847. try {
  848. const { shapes } = updateFromCode(
  849. data.document.code[data.currentCodeFileId].code,
  850. data.codeControls
  851. )
  852. commands.generate(data, data.currentPageId, shapes)
  853. } catch (e) {
  854. console.error(e)
  855. }
  856. history.enable()
  857. },
  858. // Data
  859. saveCode(data, payload: { code: string }) {
  860. data.document.code[data.currentCodeFileId].code = payload.code
  861. history.save(data)
  862. },
  863. restoreSavedData(data) {
  864. history.load(data)
  865. },
  866. clearBoundsRotation(data) {
  867. data.boundsRotation = 0
  868. },
  869. },
  870. values: {
  871. selectedIds(data) {
  872. return new Set(data.selectedIds)
  873. },
  874. selectedBounds(data) {
  875. const { selectedIds } = data
  876. const page = getPage(data)
  877. const shapes = Array.from(selectedIds.values())
  878. .map((id) => page.shapes[id])
  879. .filter(Boolean)
  880. if (selectedIds.size === 0) return null
  881. if (selectedIds.size === 1) {
  882. if (!shapes[0]) {
  883. console.error("Could not find that shape! Clearing selected IDs.")
  884. data.selectedIds.clear()
  885. return null
  886. }
  887. const shapeUtils = getShapeUtils(shapes[0])
  888. if (!shapeUtils.canTransform) return null
  889. return shapeUtils.getBounds(shapes[0])
  890. }
  891. return getCommonBounds(
  892. ...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
  893. )
  894. },
  895. },
  896. })
  897. let session: Sessions.BaseSession
  898. export default state
  899. export const useSelector = createSelectorHook(state)