選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

state.ts 27KB

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