Adds shapesToRender to state, uses fast pan
This commit is contained in:
parent
fd4ea1eec0
commit
293edd7683
9 changed files with 242 additions and 106 deletions
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
32
components/tools-panel/back-to-content.tsx
Normal file
32
components/tools-panel/back-to-content.tsx
Normal 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',
|
||||
})
|
|
@ -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', {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
126
state/state.ts
126
state/state.ts
|
@ -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) {
|
||||
|
|
|
@ -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: {
|
||||
|
|
10
types.ts
10
types.ts
|
@ -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 */
|
||||
/* -------------------------------------------------- */
|
||||
|
|
38
utils/tld.ts
38
utils/tld.ts
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue