Adds zoom helpers, improves drag selection

This commit is contained in:
Steve Ruiz 2021-05-24 18:01:27 +01:00
parent 76a1834e37
commit f4e429af0e
9 changed files with 143 additions and 25 deletions

View file

@ -2,6 +2,8 @@ import styled from "styles"
const DotCircle = styled("circle", { const DotCircle = styled("circle", {
transform: "scale(var(--scale))", transform: "scale(var(--scale))",
strokeWidth: "2",
fill: "#000",
}) })
export { DotCircle } export { DotCircle }

View file

@ -95,7 +95,6 @@ const Indicator = styled("path", {
const HoverIndicator = styled("path", { const HoverIndicator = styled("path", {
fill: "none", fill: "none",
stroke: "transparent", stroke: "transparent",
zStrokeWidth: [8, 4],
pointerEvents: "all", pointerEvents: "all",
strokeLinecap: "round", strokeLinecap: "round",
strokeLinejoin: "round", strokeLinejoin: "round",

View file

@ -10,6 +10,27 @@ export default function useKeyboardEvents() {
} }
switch (e.key) { switch (e.key) {
case "!": {
// Shift + 1
if (e.shiftKey) {
state.send("ZOOMED_TO_FIT")
}
break
}
case "@": {
// Shift + 2
if (e.shiftKey) {
state.send("ZOOMED_TO_SELECTION")
}
break
}
case ")": {
// Shift + 0
if (e.shiftKey) {
state.send("ZOOMED_TO_ACTUAL")
}
break
}
case "Escape": { case "Escape": {
state.send("CANCELLED") state.send("CANCELLED")
break break

View file

@ -4,7 +4,6 @@ import { DotShape, ShapeType } from "types"
import { registerShapeUtils } from "./index" import { registerShapeUtils } from "./index"
import { boundsContained } from "utils/bounds" import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections" import { intersectCircleBounds } from "utils/intersections"
import styled from "styles"
import { DotCircle } from "components/canvas/misc" import { DotCircle } from "components/canvas/misc"
import { translateBounds } from "utils/utils" import { translateBounds } from "utils/utils"
@ -23,7 +22,7 @@ const dot = registerShapeUtils<DotShape>({
rotation: 0, rotation: 0,
style: { style: {
fill: "#c6cacb", fill: "#c6cacb",
stroke: "#000", strokeWidth: "0",
}, },
...props, ...props,
} }

View file

@ -36,7 +36,7 @@ const line = registerShapeUtils<LineShape>({
return ( return (
<g id={id}> <g id={id}>
<line x1={x1} y1={y1} x2={x2} y2={y2} /> <line x1={x1} y1={y1} x2={x2} y2={y2} />
<DotCircle cx={0} cy={0} r={4} /> <DotCircle cx={0} cy={0} r={3} />
</g> </g>
) )
}, },

View file

@ -36,7 +36,7 @@ const ray = registerShapeUtils<RayShape>({
return ( return (
<g id={id}> <g id={id}>
<line x1={0} y1={0} x2={x2} y2={y2} /> <line x1={0} y1={0} x2={x2} y2={y2} />
<DotCircle cx={0} cy={0} r={4} /> <DotCircle cx={0} cy={0} r={3} />
</g> </g>
) )
}, },

View file

@ -1,12 +1,16 @@
import { createSelectorHook, createState } from "@state-designer/react" import { createSelectorHook, createState } from "@state-designer/react"
import { import {
clamp, clamp,
getBoundsCenter,
getChildren, getChildren,
getCommonBounds, getCommonBounds,
getPage, getPage,
getSelectedBounds,
getSelectedShapes,
getShape, getShape,
getSiblings, getSiblings,
screenToWorld, screenToWorld,
setZoomCSS,
} from "utils/utils" } from "utils/utils"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { import {
@ -68,6 +72,13 @@ const state = createState({
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" }, SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
TOGGLED_CODE_PANEL_OPEN: "toggleCodePanel", TOGGLED_CODE_PANEL_OPEN: "toggleCodePanel",
RESET_CAMERA: "resetCamera", RESET_CAMERA: "resetCamera",
ZOOMED_TO_FIT: "zoomCameraToFit",
ZOOMED_TO_SELECTION: { if: "hasSelection", do: "zoomCameraToSelection" },
ZOOMED_TO_ACTUAL: {
if: "hasSelection",
do: "zoomCameraToSelectionActual",
else: "zoomCameraToActual",
},
}, },
initial: "loading", initial: "loading",
states: { states: {
@ -480,7 +491,7 @@ const state = createState({
return data.isReadOnly return data.isReadOnly
}, },
distanceImpliesDrag(data, payload: PointerInfo) { distanceImpliesDrag(data, payload: PointerInfo) {
return vec.dist2(payload.origin, payload.point) > 16 return vec.dist2(payload.origin, payload.point) > 8
}, },
isPointedShapeSelected(data) { isPointedShapeSelected(data) {
return data.selectedIds.has(data.pointedId) return data.selectedIds.has(data.pointedId)
@ -508,6 +519,9 @@ const state = createState({
) { ) {
return payload.target === "rotate" return payload.target === "rotate"
}, },
hasSelection(data) {
return data.selectedIds.size > 0
},
}, },
actions: { actions: {
/* --------------------- Shapes --------------------- */ /* --------------------- Shapes --------------------- */
@ -565,7 +579,7 @@ const state = createState({
startTranslateSession(data, payload: PointerInfo) { startTranslateSession(data, payload: PointerInfo) {
session = new Sessions.TranslateSession( session = new Sessions.TranslateSession(
data, data,
screenToWorld(payload.point, data), screenToWorld(inputs.pointer.origin, data),
payload.altKey payload.altKey
) )
}, },
@ -697,29 +711,96 @@ const state = createState({
document.documentElement.style.setProperty("--camera-zoom", "1") document.documentElement.style.setProperty("--camera-zoom", "1")
}, },
centerCamera(data) { zoomCameraToSelection(data) {
const { shapes } = getPage(data) const { camera } = data
getCommonBounds()
data.camera.zoom = 1
data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
document.documentElement.style.setProperty("--camera-zoom", "1") const bounds = getSelectedBounds(data)
const zoom =
bounds.width > bounds.height
? (window.innerWidth - 128) / bounds.width
: (window.innerHeight - 128) / bounds.height
const mx = window.innerWidth - bounds.width * zoom
const my = window.innerHeight - bounds.height * zoom
camera.zoom = zoom
camera.point = vec.add(
[-bounds.minX, -bounds.minY],
[mx / 2 / zoom, my / 2 / zoom]
)
setZoomCSS(camera.zoom)
},
zoomCameraToSelectionActual(data) {
const { camera } = data
const bounds = getSelectedBounds(data)
const zoom = 1
const mx = window.innerWidth - 128 - bounds.width * zoom
const my = window.innerHeight - 128 - bounds.height * zoom
camera.zoom = zoom
camera.point = vec.add(
[-bounds.minX, -bounds.minY],
[mx / 2 / zoom, my / 2 / zoom]
)
setZoomCSS(camera.zoom)
},
zoomCameraToActual(data) {
const { camera } = data
const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = screenToWorld(center, data)
camera.zoom = 1
const p1 = screenToWorld(center, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
setZoomCSS(camera.zoom)
},
zoomCameraToFit(data) {
const { camera } = data
const { shapes } = getPage(data)
const bounds = getCommonBounds(
...Object.values(shapes).map((shape) =>
getShapeUtils(shape).getBounds(shape)
)
)
const zoom =
bounds.width > bounds.height
? (window.innerWidth - 104) / bounds.width
: (window.innerHeight - 104) / bounds.height
const mx = window.innerWidth - bounds.width * zoom
const my = window.innerHeight - bounds.height * zoom
camera.zoom = zoom
camera.point = vec.add(
[-bounds.minX, -bounds.minY],
[mx / 2 / zoom, my / 2 / zoom]
)
setZoomCSS(camera.zoom)
}, },
zoomCamera(data, payload: { delta: number; point: number[] }) { zoomCamera(data, payload: { delta: number; point: number[] }) {
const { camera } = data const { camera } = data
const p0 = screenToWorld(payload.point, data) const p0 = screenToWorld(payload.point, data)
camera.zoom = clamp( camera.zoom = clamp(
camera.zoom - (payload.delta / 100) * camera.zoom, camera.zoom - (payload.delta / 100) * camera.zoom,
0.5, 0.1,
3 3
) )
const p1 = screenToWorld(payload.point, data) const p1 = screenToWorld(payload.point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0)) camera.point = vec.add(camera.point, vec.sub(p1, p0))
document.documentElement.style.setProperty( setZoomCSS(camera.zoom)
"--camera-zoom",
camera.zoom.toString()
)
}, },
panCamera(data, payload: { delta: number[]; point: number[] }) { panCamera(data, payload: { delta: number[]; point: number[] }) {
const { camera } = data const { camera } = data

View file

@ -43,17 +43,22 @@ const { styled, global, css, theme, getCssString } = createCss({
utils: { utils: {
zStrokeWidth: () => (value: number | number[]) => { zStrokeWidth: () => (value: number | number[]) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
const [val, min, max] = value
return { return {
strokeWidth: strokeWidth: `calc(${value[0]} / var(--camera-zoom))`,
min !== undefined && max !== undefined
? `clamp(${min}, ${val} / var(--camera-zoom), ${max})`
: min !== undefined
? `min(${min}, ${val} / var(--camera-zoom))`
: `calc(${val} / var(--camera-zoom))`,
} }
} }
// const [val, min, max] = value
// return {
// strokeWidth:
// min !== undefined && max !== undefined
// ? `clamp(${min}, ${val} / var(--camera-zoom), ${max})`
// : min !== undefined
// ? `min(${min}, ${val} / var(--camera-zoom))`
// : `calc(${val} / var(--camera-zoom))`,
// }
// }
return { return {
strokeWidth: `calc(${value} / var(--camera-zoom))`, strokeWidth: `calc(${value} / var(--camera-zoom))`,
} }

View file

@ -1355,6 +1355,14 @@ export function getSelectedShapes(data: Data, pageId = data.currentPageId) {
return ids.map((id) => page.shapes[id]) return ids.map((id) => page.shapes[id])
} }
export function getSelectedBounds(data: Data) {
return getCommonBounds(
...getSelectedShapes(data).map((shape) =>
getShapeUtils(shape).getBounds(shape)
)
)
}
export function isMobile() { export function isMobile() {
return _isMobile() return _isMobile()
} }
@ -1474,3 +1482,6 @@ export function forceIntegerChildIndices(shapes: Shape[]) {
shapes[i].childIndex = i + 1 shapes[i].childIndex = i + 1
} }
} }
export function setZoomCSS(zoom: number) {
document.documentElement.style.setProperty("--camera-zoom", zoom.toString())
}