Adds shapesToRender to state, uses fast pan

This commit is contained in:
Steve Ruiz 2021-07-11 08:03:20 +01:00
parent fd4ea1eec0
commit 293edd7683
9 changed files with 242 additions and 106 deletions

View file

@ -1,8 +1,5 @@
import { useSelector } from 'state'
import tld from 'utils/tld'
import { Data, Shape, ShapeType } from 'types'
import { getShapeUtils } from 'state/shape-utils'
import { boundsCollide, boundsContain } from 'utils'
import { ShapeTreeNode } from 'types'
import ShapeComponent from './shape'
/*
@ -10,59 +7,25 @@ On each state change, populate a tree structure with all of
the shapes that we need to render..
*/
interface Node {
shape: Shape
children: Node[]
isEditing: boolean
isHovered: boolean
isSelected: boolean
isDarkMode: boolean
isCurrentParent: boolean
}
export default function Page(): JSX.Element {
// Get a tree of shapes to render
const shapeTree = useSelector((s) => {
// Get the shapes that fit into the current viewport
const viewport = tld.getViewport(s.data)
const shapesToShow = s.values.currentShapes.filter((shape) => {
const shapeBounds = getShapeUtils(shape).getBounds(shape)
return (
shape.type === ShapeType.Ray ||
shape.type === ShapeType.Line ||
boundsContain(viewport, shapeBounds) ||
boundsCollide(viewport, shapeBounds)
)
})
// Should we allow shapes to be hovered?
const allowHovers = s.isInAny('selecting', 'text', 'editingShape')
// Populate the shape tree
const tree: Node[] = []
shapesToShow.forEach((shape) =>
addToTree(s.data, s.values.selectedIds, allowHovers, tree, shape)
)
return tree
})
const shapesToRender = useSelector((s) => s.values.shapesToRender)
const allowHovers = useSelector((s) =>
s.isInAny('selecting', 'text', 'editingShape')
)
return (
<>
{shapeTree.map((node) => (
<ShapeNode key={node.shape.id} node={node} />
{shapesToRender.map((node) => (
<ShapeNode key={node.shape.id} node={node} allowHovers={allowHovers} />
))}
</>
)
}
interface ShapeNodeProps {
node: Node
parentPoint?: number[]
node: ShapeTreeNode
allowHovers: boolean
}
const ShapeNode = ({
@ -75,58 +38,25 @@ const ShapeNode = ({
isSelected,
isCurrentParent,
},
allowHovers,
}: ShapeNodeProps) => {
return (
<>
<ShapeComponent
shape={shape}
isEditing={isEditing}
isHovered={isHovered}
isHovered={allowHovers && isHovered}
isSelected={isSelected}
isDarkMode={isDarkMode}
isCurrentParent={isCurrentParent}
/>
{children.map((childNode) => (
<ShapeNode key={childNode.shape.id} node={childNode} />
<ShapeNode
key={childNode.shape.id}
node={childNode}
allowHovers={allowHovers}
/>
))}
</>
)
}
/**
* Populate the shape tree. This helper is recursive and only one call is needed.
*
* ### Example
*
*```ts
* addDataToTree(data, selectedIds, allowHovers, branch, shape)
*```
*/
function addToTree(
data: Data,
selectedIds: string[],
allowHovers: boolean,
branch: Node[],
shape: Shape
): void {
const node = {
shape,
children: [],
isHovered: data.hoveredId === shape.id,
isCurrentParent: data.currentParentId === shape.id,
isEditing: data.editingId === shape.id,
isDarkMode: data.settings.isDarkMode,
isSelected: selectedIds.includes(shape.id),
}
branch.push(node)
if (shape.children) {
shape.children
.map((id) => tld.getShape(data, id))
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((childShape) => {
addToTree(data, selectedIds, allowHovers, node.children, childShape)
})
}
}

View file

@ -0,0 +1,32 @@
import { FloatingContainer, RowButton } from 'components/shared'
import { motion } from 'framer-motion'
import { memo } from 'react'
import state, { useSelector } from 'state'
import styled from 'styles'
function BackToContent() {
const shouldDisplay = useSelector((s) => {
const { currentShapes, shapesToRender } = s.values
return currentShapes.length > 0 && shapesToRender.length === 0
})
if (!shouldDisplay) return null
return (
<BackToContentButton initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<RowButton onClick={() => state.send('ZOOMED_TO_CONTENT')}>
Back to content
</RowButton>
</BackToContentButton>
)
}
export default memo(BackToContent)
const BackToContentButton = styled(motion(FloatingContainer), {
pointerEvents: 'all',
width: 'fit-content',
gridRow: 1,
flexGrow: 2,
display: 'block',
})

View file

@ -16,6 +16,7 @@ import styled from 'styles'
import { ShapeType } from 'types'
import UndoRedo from './undo-redo'
import Zoom from './zoom'
import BackToContent from './back-to-content'
const selectArrowTool = () => state.send('SELECTED_ARROW_TOOL')
const selectDrawTool = () => state.send('SELECTED_DRAW_TOOL')
@ -45,6 +46,7 @@ export default function ToolsPanel(): JSX.Element {
</FloatingContainer>
</LeftWrap>
<CenterWrap>
<BackToContent />
<FloatingContainer>
<PrimaryButton
label={ShapeType.Draw}
@ -120,7 +122,10 @@ const CenterWrap = styled('div', {
gridColumn: 2,
display: 'flex',
width: 'fit-content',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
gap: 12,
})
const LeftWrap = styled('div', {

View file

@ -4,6 +4,7 @@ import state from 'state'
import {
fastBrushSelect,
fastDrawUpdate,
fastPanUpdate,
fastTransform,
fastTranslate,
} from 'state/hacks'
@ -43,6 +44,8 @@ export default function useCanvasEvents(
const info = inputs.pointerMove(e)
if (prev && state.isIn('selecting') && inputs.keys[' ']) {
const delta = Vec.sub(prev, info.point)
fastPanUpdate(delta)
state.send('KEYBOARD_PANNED_CAMERA', { delta: Vec.sub(prev, info.point) })
return
}

View file

@ -4,7 +4,15 @@ import state from 'state'
import inputs from 'state/inputs'
import vec from 'utils/vec'
import { useGesture } from 'react-use-gesture'
import { fastPinchCamera, fastZoomUpdate } from 'state/hacks'
import {
fastBrushSelect,
fastDrawUpdate,
fastPanUpdate,
fastPinchCamera,
fastTransform,
fastTranslate,
fastZoomUpdate,
} from 'state/hacks'
/**
* Capture zoom gestures (pinches, wheels and pans) and send to the state.
@ -24,6 +32,20 @@ export default function useZoomEvents() {
return
}
fastPanUpdate(delta)
const info = inputs.pointer
if (state.isIn('draw.editing')) {
fastDrawUpdate(info)
} else if (state.isIn('brushSelecting')) {
fastBrushSelect(info.point)
} else if (state.isIn('translatingSelection')) {
fastTranslate(info)
} else if (state.isIn('transformingSelection')) {
fastTransform(info)
}
state.send('PANNED_CAMERA', {
delta,
...inputs.wheel(event as WheelEvent),

View file

@ -17,6 +17,8 @@ import {
deepClone,
pointInBounds,
uniqueId,
boundsContain,
boundsCollide,
} from 'utils'
import tld from '../utils/tld'
import {
@ -35,6 +37,7 @@ import {
DashStyle,
SizeStyle,
ColorStyle,
ShapeTreeNode,
} from 'types'
import { getFontSize } from './shape-styles'
import logger from './logger'
@ -196,7 +199,10 @@ const state = createState({
DISABLED_PEN_LOCK: 'disablePenLock',
TOGGLED_CODE_PANEL_OPEN: ['toggleCodePanel', 'saveAppState'],
TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
PANNED_CAMERA: 'panCamera',
PANNED_CAMERA: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'panCamera',
},
POINTED_CANVAS: ['closeStylePanel', 'clearCurrentParentId'],
COPIED_STATE_TO_CLIPBOARD: 'copyStateToClipboard',
COPIED: { if: 'hasSelection', do: 'copyToClipboard' },
@ -332,8 +338,10 @@ const state = createState({
ZOOMED_TO_SELECTION: {
if: 'hasSelection',
do: 'zoomCameraToSelection',
else: 'zoomCameraToFit',
},
ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
ZOOMED_TO_CONTENT: 'zoomCameraToContent',
ZOOMED_TO_FIT: 'zoomCameraToFit',
ZOOMED_IN: 'zoomIn',
ZOOMED_OUT: 'zoomOut',
RESET_CAMERA: 'resetCamera',
@ -357,7 +365,10 @@ const state = createState({
selecting: {
onEnter: ['setActiveToolSelect', 'clearInputs'],
on: {
KEYBOARD_PANNED_CAMERA: 'panCamera',
KEYBOARD_PANNED_CAMERA: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'panCamera',
},
STARTED_PINCHING: {
unless: 'isInSession',
to: 'pinching.selectPinching',
@ -601,7 +612,10 @@ const state = createState({
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateTransformSession',
},
PANNED_CAMERA: 'updateTransformSession',
PANNED_CAMERA: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateTransformSession',
},
PRESSED_SHIFT_KEY: 'keyUpdateTransformSession',
RELEASED_SHIFT_KEY: 'keyUpdateTransformSession',
STOPPED_POINTING: { to: 'selecting' },
@ -613,8 +627,14 @@ const state = createState({
onExit: 'completeSession',
on: {
STARTED_PINCHING: { to: 'pinching' },
MOVED_POINTER: 'updateTranslateSession',
PANNED_CAMERA: 'updateTranslateSession',
MOVED_POINTER: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateTranslateSession',
},
PANNED_CAMERA: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateTranslateSession',
},
PRESSED_SHIFT_KEY: 'keyUpdateTranslateSession',
RELEASED_SHIFT_KEY: 'keyUpdateTranslateSession',
PRESSED_ALT_KEY: 'keyUpdateTranslateSession',
@ -650,8 +670,14 @@ const state = createState({
'startBrushSession',
],
on: {
MOVED_POINTER: { if: 'isTestMode', do: 'updateBrushSession' },
PANNED_CAMERA: 'updateBrushSession',
MOVED_POINTER: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateBrushSession',
},
PANNED_CAMERA: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateBrushSession',
},
STOPPED_POINTING: { to: 'selecting' },
STARTED_PINCHING: { to: 'pinching' },
CANCELLED: { do: 'cancelSession', to: 'selecting' },
@ -780,7 +806,10 @@ const state = createState({
},
PRESSED_SHIFT: 'keyUpdateDrawSession',
RELEASED_SHIFT: 'keyUpdateDrawSession',
PANNED_CAMERA: 'updateDrawSession',
PANNED_CAMERA: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateDrawSession',
},
MOVED_POINTER: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateDrawSession',
@ -839,8 +868,14 @@ const state = createState({
onExit: 'completeSession',
onEnter: 'startTranslateSession',
on: {
MOVED_POINTER: 'updateTranslateSession',
PANNED_CAMERA: 'updateTranslateSession',
MOVED_POINTER: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateTranslateSession',
},
PANNED_CAMERA: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateTranslateSession',
},
},
},
},
@ -1084,16 +1119,24 @@ const state = createState({
bounds: {
onEnter: 'startDrawTransformSession',
on: {
MOVED_POINTER: 'updateTransformSession',
PANNED_CAMERA: 'updateTransformSession',
MOVED_POINTER: {
do: 'updateTransformSession',
},
PANNED_CAMERA: {
do: 'updateTransformSession',
},
},
},
direction: {
onEnter: 'startDirectionSession',
onExit: 'completeSession',
on: {
MOVED_POINTER: 'updateDirectionSession',
PANNED_CAMERA: 'updateDirectionSession',
MOVED_POINTER: {
do: 'updateDirectionSession',
},
PANNED_CAMERA: {
do: 'updateDirectionSession',
},
},
},
},
@ -1900,6 +1943,28 @@ const state = createState({
tld.setZoomCSS(camera.zoom)
},
zoomCameraToContent(data) {
const camera = tld.getCurrentCamera(data)
const page = tld.getPage(data)
const shapes = Object.values(page.shapes)
if (shapes.length === 0) {
return
}
const bounds = getCommonBounds(
...Object.values(shapes).map((shape) =>
getShapeUtils(shape).getBounds(shape)
)
)
const { zoom } = camera
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
},
zoomCamera(data, payload: { delta: number; point: number[] }) {
const camera = tld.getCurrentCamera(data)
const next = camera.zoom - (payload.delta / 100) * camera.zoom
@ -2180,6 +2245,37 @@ const state = createState({
return commonStyle
},
shapesToRender(data) {
const viewport = tld.getViewport(data)
const page = tld.getPage(data)
const currentShapes = Object.values(page.shapes)
.filter((shape) => shape.parentId === page.id)
.sort((a, b) => a.childIndex - b.childIndex)
const shapesToShow = currentShapes.filter((shape) => {
const shapeBounds = getShapeUtils(shape).getBounds(shape)
return (
shape.type === ShapeType.Ray ||
shape.type === ShapeType.Line ||
boundsContain(viewport, shapeBounds) ||
boundsCollide(viewport, shapeBounds)
)
})
// Populate the shape tree
const tree: ShapeTreeNode[] = []
const selectedIds = tld.getSelectedIds(data)
shapesToShow.forEach((shape) =>
tld.addToShapeTree(data, selectedIds, tree, shape)
)
return tree
},
},
options: {
onSend(eventName, payload, didCauseUpdate) {

View file

@ -15,15 +15,15 @@ const { styled, global, css, theme, getCssString } = createCss({
boundsBg: 'rgba(65, 132, 244, 0.05)',
highlight: 'rgba(65, 132, 244, 0.15)',
overlay: 'rgba(0, 0, 0, 0.15)',
border: '#aaa',
border: '#aaaaaa',
canvas: '#f8f9fa',
panel: '#fefefe',
inactive: '#cccccf',
hover: '#efefef',
text: '#333',
muted: '#777',
text: '#333333',
muted: '#777777',
input: '#f3f3f3',
inputBorder: '#ddd',
inputBorder: '#dddddd',
lineError: 'rgba(255, 0, 0, .1)',
},
shadows: {

View file

@ -278,6 +278,16 @@ export interface CodeResult {
error: CodeError
}
export interface ShapeTreeNode {
shape: Shape
children: ShapeTreeNode[]
isEditing: boolean
isHovered: boolean
isSelected: boolean
isDarkMode: boolean
isCurrentParent: boolean
}
/* -------------------------------------------------- */
/* Editor UI */
/* -------------------------------------------------- */

View file

@ -12,6 +12,7 @@ import {
PageState,
ShapeUtility,
ParentShape,
ShapeTreeNode,
} from 'types'
import { AssertionError } from 'assert'
@ -537,4 +538,41 @@ export default class StateUtils {
this.updateParents(data, parentToUpdateIds)
}
/**
* Populate the shape tree. This helper is recursive and only one call is needed.
*
* ### Example
*
*```ts
* addDataToTree(data, selectedIds, allowHovers, branch, shape)
*```
*/
static addToShapeTree(
data: Data,
selectedIds: string[],
branch: ShapeTreeNode[],
shape: Shape
): void {
const node = {
shape,
children: [],
isHovered: data.hoveredId === shape.id,
isCurrentParent: data.currentParentId === shape.id,
isEditing: data.editingId === shape.id,
isDarkMode: data.settings.isDarkMode,
isSelected: selectedIds.includes(shape.id),
}
branch.push(node)
if (shape.children) {
shape.children
.map((id) => this.getShape(data, id))
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((childShape) => {
this.addToShapeTree(data, selectedIds, node.children, childShape)
})
}
}
}