8 | export const pointer = { x: 0, y: 0 }
9 | export const origin = { x: 0, y: 0 }
10 | export const cameraOrigin = { x: 0, y: 0 }
11 | export const camera = { x: 0, y: 0, cx: 0, cy: 0, width: 0, height: 0 }
12 |
13 | let dpr = 2
14 |
15 | export function viewBoxToCamera(
16 | point: IPoint,
17 | viewBox: IFrame,
18 | camera: { x: number; y: number; zoom: number }
19 | ) {
20 | return {
21 | x: (camera.x + point.x - viewBox.x) / camera.zoom,
22 | y: (camera.y + point.y - viewBox.y) / camera.zoom,
23 | }
24 | }
25 |
26 | export function getBoundingBox(boxes: IBox[]): IBounds {
27 | if (boxes.length === 0) {
28 | return {
29 | x: 0,
30 | y: 0,
31 | maxX: 0,
32 | maxY: 0,
33 | width: 0,
34 | height: 0,
35 | }
36 | }
37 |
38 | const first = boxes[0]
39 |
40 | let x = first.x
41 | let maxX = first.x + first.width
42 | let y = first.y
43 | let maxY = first.y + first.height
44 |
45 | for (let box of boxes) {
46 | x = Math.min(x, box.x)
47 | maxX = Math.max(maxX, box.x + box.width)
48 | y = Math.min(y, box.y)
49 | maxY = Math.max(maxY, box.y + box.height)
50 | }
51 |
52 | return {
53 | x,
54 | y,
55 | width: maxX - x,
56 | height: maxY - y,
57 | maxX,
58 | maxY,
59 | }
60 | }
61 |
62 | export function mapValues(
63 | obj: { [key: string]: T },
64 | fn: (value: T, index: number) => P
65 | ): { [key: string]: P } {
66 | return Object.fromEntries(
67 | Object.entries(obj).map(([id, value], index) => [id, fn(value, index)])
68 | )
69 | }
70 |
71 | export function getInitialIndex() {
72 | if (typeof window === undefined || !window.localStorage) return "0"
73 |
74 | let curIndex = "1"
75 | let prevIndex: any = localStorage.getItem("__index")
76 | if (prevIndex === null) {
77 | curIndex = "1"
78 | } else {
79 | const num = parseInt(JSON.parse(prevIndex), 10)
80 | curIndex = (num + 1).toString()
81 | }
82 |
83 | localStorage.setItem("__index", JSON.stringify(curIndex))
84 | }
85 |
86 | /**
87 | * Get an arrow between boxes.
88 | * @param a
89 | * @param b
90 | * @param options
91 | */
92 | export function getArrow(
93 | a: IBox,
94 | b: IBox,
95 | options: Partial = {}
96 | ) {
97 | const opts = {
98 | box: 0.05,
99 | stretchMax: 1200,
100 | padEnd: 12,
101 | ...options,
102 | }
103 | return getBoxToBoxArrow(
104 | a.x,
105 | a.y,
106 | a.width,
107 | a.height,
108 | b.x,
109 | b.y,
110 | b.width,
111 | b.height,
112 | opts
113 | )
114 | }
115 |
116 | const keyDownActions = {
117 | Escape: "CANCELLED",
118 | Alt: "ENTERED_ALT_MODE",
119 | " ": "ENTERED_SPACE_MODE",
120 | Backspace: "DELETED_SELECTED",
121 | Shift: "ENTERED_SHIFT_MODE",
122 | Control: "ENTERED_CONTROL_MODE",
123 | Meta: "ENTERED_META_MODE",
124 | f: "SELECTED_BOX_TOOL",
125 | v: "SELECTED_SELECT_TOOL",
126 | r: "INVERTED_ARROWS",
127 | t: "FLIPPED_ARROWS",
128 | a: "STARTED_PICKING_ARROW",
129 | }
130 |
131 | const keyUpActions = {
132 | Alt: "EXITED_ALT_MODE",
133 | " ": "EXITED_SPACE_MODE",
134 | Shift: "EXITED_SHIFT_MODE",
135 | Control: "EXITED_CONTROL_MODE",
136 | Meta: "EXITED_META_MODE",
137 | v: "SELECTED_SELECT_TOOL",
138 | r: "INVERTED_ARROWS",
139 | t: "FLIPPED_ARROWS",
140 | a: "STARTED_PICKING_ARROW",
141 | }
142 |
143 | export function testKeyCombo(event: string, ...keys: string[]) {
144 | if (keys.every((key) => pressedKeys[key])) state.send(event)
145 | }
146 |
147 | export function handleKeyDown(e: KeyboardEvent) {
148 | pressedKeys[e.key] = true
149 | const action = keyDownActions[e.key]
150 | if (action) state.send(action)
151 |
152 | // Handle shift here?
153 | }
154 |
155 | export function handleKeyUp(e: KeyboardEvent) {
156 | if (
157 | pressedKeys.Option ||
158 | pressedKeys.Shift ||
159 | pressedKeys.Meta ||
160 | pressedKeys.Control
161 | ) {
162 | testKeyCombo("ALIGNED_LEFT", "Option", "a")
163 | testKeyCombo("ALIGNED_CENTER_X", "Option", "h")
164 | testKeyCombo("ALIGNED_RIGHT", "Option", "d")
165 | testKeyCombo("ALIGNED_TOP", "Option", "w")
166 | testKeyCombo("ALIGNED_CENTER_Y", "Option", "v")
167 | testKeyCombo("ALIGNED_BOTTOM", "Option", "s")
168 | testKeyCombo("DISTRIBUTED_X", "Option", "Control", "h")
169 | testKeyCombo("DISTRIBUTED_Y", "Option", "Control", "v")
170 | testKeyCombo("STRETCHED_X", "Option", "Shift", "h")
171 | testKeyCombo("STRETCHED_Y", "Option", "Shift", "v")
172 | testKeyCombo("BROUGHT_FORWARD", "Meta", "]")
173 | testKeyCombo("SENT_BACKWARD", "Meta", "[")
174 | testKeyCombo("BROUGHT_TO_FRONT", "Meta", "Shift", "]")
175 | testKeyCombo("SENT_TO_BACK", "Meta", "Shift", "[")
176 | testKeyCombo("PASTED", "Meta", "v")
177 | testKeyCombo("COPIED", "Meta", "c")
178 | testKeyCombo("UNDO", "Meta", "z")
179 | testKeyCombo("REDO", "Meta", "Shift", "z")
180 | return
181 | } else {
182 | const action = keyUpActions[e.key]
183 | if (action) state.send(action)
184 | }
185 |
186 | pressedKeys[e.key] = false
187 | }
188 |
189 | export function handleKeyPress(e: KeyboardEvent) {
190 | if (e.key === " " && !state.isInAny("editingLabel", "editingArrowLabel")) {
191 | e.preventDefault()
192 | }
193 | }
194 |
195 | export function pointInRectangle(a: IPoint, b: IFrame, padding = 0) {
196 | const r = padding / 2
197 | return !(
198 | a.x > b.x + b.width + r ||
199 | a.y > b.y + b.height + r ||
200 | a.x < b.x - r ||
201 | a.y < b.y - r
202 | )
203 | }
204 |
205 | export function pointInCorner(a: IPoint, b: IFrame, padding = 4) {
206 | let cx: number, cy: number
207 | const r = padding / 2
208 | const corners = getCorners(b.x, b.y, b.width, b.height)
209 |
210 | for (let i = 0; i < corners.length; i++) {
211 | ;[cx, cy] = corners[i]
212 | if (
213 | pointInRectangle(
214 | a,
215 | {
216 | x: cx - 4,
217 | y: cy - 4,
218 | width: 8,
219 | height: 8,
220 | },
221 | 0
222 | )
223 | )
224 | return i
225 | }
226 | }
227 |
228 | export function lineToRectangle(
229 | x0: number,
230 | y0: number,
231 | x1: number,
232 | y1: number,
233 | padding = 8
234 | ) {
235 | const r = padding / 2
236 | if (x1 < x0) [x0, x1] = [x1, x0]
237 | if (y1 < y0) [y0, y1] = [y1, y0]
238 | return {
239 | x: x0 - r,
240 | y: y0 - r,
241 | width: x1 + r - (x0 - r),
242 | height: y1 + r - (y0 - r),
243 | }
244 | }
245 |
246 | export function pointInEdge(a: IPoint, b: IFrame, padding = 4) {
247 | const edges = getEdges(b.x, b.y, b.width, b.height)
248 |
249 | for (let i = 0; i < edges.length; i++) {
250 | const [[x0, y0], [x1, y1]] = edges[i]
251 | if (pointInRectangle(a, lineToRectangle(x0, y0, x1, y1), padding)) return i
252 | }
253 | }
254 |
255 | export function doBoxesCollide(a: IFrame, b: IFrame) {
256 | return !(
257 | a.x > b.x + b.width ||
258 | a.y > b.y + b.height ||
259 | a.x + a.width < b.x ||
260 | a.y + a.height < b.y
261 | )
262 | }
263 |
264 | export function getBox(
265 | x: number,
266 | y: number,
267 | z: number,
268 | width: number,
269 | height: number
270 | ): IBox {
271 | return {
272 | id: "box" + uniqueId(),
273 | x,
274 | y,
275 | z,
276 | width,
277 | height,
278 | label: "",
279 | color: "#ffffff",
280 | }
281 | }
282 |
283 | export function getEdges(x: number, y: number, w: number, h: number) {
284 | return [
285 | [
286 | [x, y],
287 | [x + w, y],
288 | ],
289 | [
290 | [x + w, y],
291 | [x + w, y + h],
292 | ],
293 | [
294 | [x + w, y + h],
295 | [x, y + h],
296 | ],
297 | [
298 | [x, y + h],
299 | [x, y],
300 | ],
301 | ]
302 | }
303 |
304 | export function getCorners(x: number, y: number, w: number, h: number) {
305 | return [
306 | [x, y],
307 | [x + w, y],
308 | [x + w, y + h],
309 | [x, y + h],
310 | ]
311 | }
312 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack(config, options) {
3 | config.module.rules.push({
4 | test: /\.worker\.js$/,
5 | loader: "worker-loader",
6 | // options: { inline: true }, // also works
7 | options: {
8 | name: "static/[hash].worker.js",
9 | publicPath: "/_next/",
10 | },
11 | })
12 | return config
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "arrows-playground",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@state-designer/react": "^1.2.31",
12 | "@stitches/react": "^0.0.2",
13 | "@types/rbush": "^3.0.0",
14 | "@types/uuid": "^8.3.0",
15 | "@zeit/next-workers": "^1.0.0",
16 | "comlink": "^4.3.0",
17 | "framer-motion": "^2.7.9",
18 | "lodash": "^4.17.20",
19 | "next": "9.5.4",
20 | "perfect-arrows": "^0.3.3",
21 | "pixi.js": "^5.3.3",
22 | "rbush": "^3.0.1",
23 | "react": "^16.14.0",
24 | "react-dom": "^16.14.0",
25 | "react-no-ssr": "^1.1.0",
26 | "use-resize-observer": "^6.1.0",
27 | "uuid": "^8.3.1",
28 | "worker-loader": "^3.0.5"
29 | },
30 | "devDependencies": {
31 | "@types/node": "^14.11.5",
32 | "typescript": "^4.0.3"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css'
2 |
3 | function MyApp({ Component, pageProps }) {
4 | return
5 | }
6 |
7 | export default MyApp
8 |
--------------------------------------------------------------------------------
/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | export default (req, res) => {
4 | res.statusCode = 200
5 | res.json({ name: 'John Doe' })
6 | }
7 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic"
2 |
3 | const App = dynamic(() => import("../components/app"), {
4 | ssr: false,
5 | })
6 |
7 | export default function Home() {
8 | return
9 | }
10 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steveruizok/arrows-playground/2a7160257f9fd0176d29bb4c61490e8c46c8b208/public/favicon.ico
--------------------------------------------------------------------------------
/public/service.worker.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | importScripts(
3 | "https://unpkg.com/comlink/dist/umd/comlink.js",
4 | "https://unpkg.com/rbush@3.0.1/rbush.min.js"
5 | )
6 |
7 | const tree = new RBush()
8 | const hitTree = new RBush()
9 |
10 | function updateTree({ boxes }) {
11 | tree.clear()
12 |
13 | tree.load(
14 | boxes.map((box) => ({
15 | id: box.id,
16 | minX: box.x,
17 | minY: box.y,
18 | maxX: box.x + box.width,
19 | maxY: box.y + box.height,
20 | }))
21 | )
22 |
23 | return tree
24 | }
25 |
26 | const throttle = (fn, wait) => {
27 | let inThrottle, lastFn, lastTime
28 | return function () {
29 | const context = this,
30 | args = arguments
31 | if (!inThrottle) {
32 | fn.apply(context, args)
33 | lastTime = Date.now()
34 | inThrottle = true
35 | } else {
36 | clearTimeout(lastFn)
37 | lastFn = setTimeout(function () {
38 | if (Date.now() - lastTime >= wait) {
39 | fn.apply(context, args)
40 | lastTime = Date.now()
41 | }
42 | }, Math.max(wait - (Date.now() - lastTime), 0))
43 | }
44 | }
45 | }
46 |
47 | function getBoundingBox(boxes) {
48 | if (boxes.length === 0) {
49 | return {
50 | x: 0,
51 | y: 0,
52 | maxX: 0,
53 | maxY: 0,
54 | width: 0,
55 | height: 0,
56 | }
57 | }
58 |
59 | const first = boxes[0]
60 |
61 | let x = first.minX
62 | let maxX = first.maxX
63 | let y = first.minX
64 | let maxY = first.maxY
65 |
66 | for (let box of boxes) {
67 | x = Math.min(x, box.minX)
68 | maxX = Math.max(maxX, box.maxX)
69 | y = Math.min(y, box.minY)
70 | maxY = Math.max(maxY, box.maxY)
71 | }
72 |
73 | return {
74 | x,
75 | y,
76 | width: maxX - x,
77 | height: maxY - y,
78 | maxX,
79 | maxY,
80 | }
81 | }
82 |
83 | let selected = []
84 | let bounds = {}
85 |
86 | function getBoxSelecter({ origin }) {
87 | let x0, y0, x1, y1, t
88 | const { x: ox, y: oy } = origin
89 |
90 | return function select(point) {
91 | x0 = ox
92 | y0 = oy
93 | x1 = point.x
94 | y1 = point.y
95 |
96 | if (x1 < x0) {
97 | t = x0
98 | x0 = x1
99 | x1 = t
100 | }
101 |
102 | if (y1 < y0) {
103 | t = y0
104 | y0 = y1
105 | y1 = t
106 | }
107 |
108 | const results = tree.search({ minX: x0, minY: y0, maxX: x1, maxY: y1 })
109 |
110 | selected = results.map((b) => b.id)
111 | bounds = getBoundingBox(results)
112 | return results
113 | }
114 | }
115 |
116 | function pointInRectangle(a, b, padding = 0) {
117 | const r = padding / 2
118 | return !(
119 | a.x > b.x + b.width + r ||
120 | a.y > b.y + b.height + r ||
121 | a.x < b.x - r ||
122 | a.y < b.y - r
123 | )
124 | }
125 |
126 | function getCorners(x, y, w, h) {
127 | return [
128 | [x, y],
129 | [x + w, y],
130 | [x + w, y + h],
131 | [x, y + h],
132 | ]
133 | }
134 |
135 | function pointInCorner(a, b, padding = 4) {
136 | let cx, cy
137 | const r = padding / 2
138 | const corners = getCorners(b.x, b.y, b.width, b.height)
139 |
140 | for (let i = 0; i < corners.length; i++) {
141 | ;[cx, cy] = corners[i]
142 | if (
143 | pointInRectangle(
144 | a,
145 | {
146 | x: cx - 4,
147 | y: cy - 4,
148 | width: 8,
149 | height: 8,
150 | },
151 | 0
152 | )
153 | )
154 | return i
155 | }
156 | }
157 |
158 | function lineToRectangle(x0, y0, x1, y1, padding = 8) {
159 | const r = padding / 2
160 | if (x1 < x0) [x0, x1] = [x1, x0]
161 | if (y1 < y0) [y0, y1] = [y1, y0]
162 | return {
163 | x: x0 - r,
164 | y: y0 - r,
165 | width: x1 + r - (x0 - r),
166 | height: y1 + r - (y0 - r),
167 | }
168 | }
169 |
170 | function getEdges(x, y, w, h) {
171 | return [
172 | [
173 | [x, y],
174 | [x + w, y],
175 | ],
176 | [
177 | [x + w, y],
178 | [x + w, y + h],
179 | ],
180 | [
181 | [x + w, y + h],
182 | [x, y + h],
183 | ],
184 | [
185 | [x, y + h],
186 | [x, y],
187 | ],
188 | ]
189 | }
190 |
191 | function pointInEdge(a, b, padding = 4) {
192 | const edges = getEdges(b.x, b.y, b.width, b.height)
193 |
194 | for (let i = 0; i < edges.length; i++) {
195 | const [[x0, y0], [x1, y1]] = edges[i]
196 | if (pointInRectangle(a, lineToRectangle(x0, y0, x1, y1), padding)) return i
197 | }
198 | }
199 |
200 | function doBoxesCollide(a, b) {
201 | return !(
202 | a.x > b.x + b.width ||
203 | a.y > b.y + b.height ||
204 | a.x + a.width < b.x ||
205 | a.y + a.height < b.y
206 | )
207 | }
208 |
209 | function stretchBoxesX(boxes) {
210 | const [first, ...rest] = boxes
211 | let min = first.x
212 | let max = first.x + first.width
213 | for (let box of rest) {
214 | min = Math.min(min, box.x)
215 | max = Math.max(max, box.x + box.width)
216 | }
217 | for (let box of boxes) {
218 | box.x = min
219 | box.width = max - min
220 | }
221 |
222 | return boxes
223 | }
224 |
225 | function stretchBoxesY(boxes) {
226 | const [first, ...rest] = boxes
227 | let min = first.y
228 | let max = first.y + first.height
229 | for (let box of rest) {
230 | min = Math.min(min, box.y)
231 | max = Math.max(max, box.y + box.height)
232 | }
233 | for (let box of boxes) {
234 | box.y = min
235 | box.height = max - min
236 | }
237 |
238 | return boxes
239 | }
240 |
241 | function updateHitTestTree(boxes) {
242 | hitTree.clear()
243 |
244 | hitTree.load(
245 | boxes.map((box) => ({
246 | id: box.id,
247 | minX: box.x,
248 | minY: box.y,
249 | maxX: box.x + box.width,
250 | maxY: box.y + box.height,
251 | z: box.z,
252 | }))
253 | )
254 | }
255 |
256 | function hitTest({ point, bounds, zoom }) {
257 | if (bounds) {
258 | // Test if point collides the (padded) bounds
259 | if (pointInRectangle(point, bounds, 16)) {
260 | const { x, y, width, height, maxX, maxY } = bounds
261 | const p = 5 / zoom
262 | const pp = p * 2
263 |
264 | const cornerBoxes = [
265 | { x: x - p, y: y - p, width: pp, height: pp },
266 | { x: maxX - p, y: y - p, width: pp, height: pp },
267 | { x: maxX - p, y: maxY - p, width: pp, height: pp },
268 | { x: x - p, y: maxY - p, width: pp, height: pp },
269 | ]
270 |
271 | for (let i = 0; i < cornerBoxes.length; i++) {
272 | if (pointInRectangle(point, cornerBoxes[i])) {
273 | return { type: "bounds-corner", corner: i }
274 | }
275 | }
276 |
277 | const edgeBoxes = [
278 | { x: x + p, y: y - p, width: width - pp, height: pp },
279 | { x: maxX - p, y: y + p, width: pp, height: height - pp },
280 | { x: x + p, y: maxY - p, width: width - pp, height: pp },
281 | { x: x - p, y: y + p, width: pp, height: height - pp },
282 | ]
283 |
284 | for (let i = 0; i < edgeBoxes.length; i++) {
285 | if (pointInRectangle(point, edgeBoxes[i])) {
286 | return { type: "bounds-edge", edge: i }
287 | }
288 | }
289 | // Point is in the middle of the bounds
290 | return { type: "bounds" }
291 | }
292 | }
293 |
294 | if (!point) return
295 |
296 | const hits = hitTree.search({
297 | minX: point.x,
298 | minY: point.y,
299 | maxX: point.x + 1,
300 | maxY: point.y + 1,
301 | })
302 |
303 | // Either we don't have bounds or we're out of bounds
304 | // for (let id in boxes) {
305 | // box = boxes[id]
306 | // // Test if point collides the (padded) box
307 | // if (pointInRectangle(point, box)) {
308 | // hits.push(box)
309 | // }
310 | // }
311 |
312 | if (hits.length > 0) {
313 | const hit = Object.values(hits).sort((a, b) => b.z - a.z)[0]
314 | return { type: "box", id: hit.id }
315 | }
316 |
317 | return { type: "canvas" }
318 | }
319 |
320 | let boxSelecter = undefined
321 |
322 | function getTransform(type, payload) {
323 | switch (type) {
324 | case "stretchBoxesX": {
325 | return stretchBoxesX(payload)
326 | }
327 | case "stretchBoxesY": {
328 | return stretchBoxesY(payload)
329 | }
330 | case "updateHitTree": {
331 | updateHitTestTree(payload)
332 | }
333 | case "hitTest": {
334 | return hitTest(payload)
335 | }
336 | case "updateTree": {
337 | return updateTree(payload)
338 | }
339 | case "selecter": {
340 | boxSelecter = getBoxSelecter(payload)
341 | const { minX, minY, maxX, maxY } = tree
342 | return { minX, minY, maxX, maxY }
343 | }
344 | case "selectedBounds": {
345 | return bounds
346 | }
347 | case "selected": {
348 | boxSelecter(payload)
349 |
350 | return selected
351 | }
352 | }
353 | }
354 |
355 | Comlink.expose(getTransform)
356 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | overscroll-behavior: none;
4 | padding: 0;
5 | margin: 0;
6 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
7 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
8 | }
9 |
10 | a {
11 | color: inherit;
12 | text-decoration: none;
13 | }
14 |
15 | * {
16 | box-sizing: border-box;
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve"
20 | },
21 | "include": [
22 | "next-env.d.ts",
23 | "**/*.ts",
24 | "**/*.tsx"
25 | ],
26 | "exclude": [
27 | "node_modules"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | export interface IPoint {
2 | x: number
3 | y: number
4 | }
5 |
6 | export interface IPointer extends IPoint {
7 | dx: number
8 | dy: number
9 | }
10 |
11 | export interface ISize {
12 | width: number
13 | height: number
14 | }
15 |
16 | export interface IBrush {
17 | x0: number
18 | y0: number
19 | x1: number
20 | y1: number
21 | }
22 |
23 | export interface IFrame extends IPoint, ISize {}
24 |
25 | export interface IBounds extends IPoint, ISize {
26 | maxX: number
27 | maxY: number
28 | }
29 |
30 | export interface IBox extends IFrame {
31 | id: string
32 | label: string
33 | color: string
34 | z: number
35 | }
36 |
37 | export interface IBoxSnapshot extends IFrame {
38 | id: string
39 | nx: number
40 | ny: number
41 | nmx: number
42 | nmy: number
43 | nw: number
44 | nh: number
45 | }
46 |
47 | export enum IArrowType {
48 | BoxToBox = "box-to-box",
49 | BoxToPoint = "box-to-point",
50 | PointToBox = "point-to-box",
51 | PointToPoint = "point-to-point",
52 | }
53 |
54 | export interface IArrowBase {
55 | type: IArrowType
56 | id: string
57 | flip: boolean
58 | label: string
59 | from: string | IPoint
60 | to: string | IPoint
61 | }
62 |
63 | export interface BoxToBox extends IArrowBase {
64 | type: IArrowType.BoxToBox
65 | from: string
66 | to: string
67 | }
68 |
69 | export interface BoxToPoint extends IArrowBase {
70 | type: IArrowType.BoxToPoint
71 | from: string
72 | to: IPoint
73 | }
74 |
75 | export interface PointToBox extends IArrowBase {
76 | type: IArrowType.PointToBox
77 | from: IPoint
78 | to: string
79 | }
80 |
81 | export interface PointToPoint extends IArrowBase {
82 | type: IArrowType.PointToPoint
83 | from: IPoint
84 | to: IPoint
85 | }
86 |
87 | export type IArrow = BoxToBox | BoxToPoint | PointToBox | PointToPoint
88 |
--------------------------------------------------------------------------------