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

state.ts 26KB

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