Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

state.ts 26KB

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