Adds zoom helpers, improves drag selection
This commit is contained in:
parent
76a1834e37
commit
f4e429af0e
9 changed files with 143 additions and 25 deletions
|
@ -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 }
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
107
state/state.ts
107
state/state.ts
|
@ -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
|
||||||
|
|
|
@ -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))`,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue