Fixes a perf-killing deepCompare in context menu

Was deep comparing an array of actual selected shapes, rather than selected shape ids
This commit is contained in:
Steve Ruiz 2021-06-27 10:07:20 +01:00
parent ff58073d12
commit d1a3860bb1
10 changed files with 105 additions and 99 deletions

View file

@ -17,49 +17,31 @@
"name": "Arrow",
"parentId": "page1",
"childIndex": 16,
"point": [
100,
100
],
"point": [100, 100],
"rotation": 0,
"isAspectRatioLocked": false,
"isLocked": false,
"isHidden": false,
"bend": 0,
"points": [
[
6.503619523263069,
281.09020634582345
],
[
0,
0
]
[6.503619523263069, 281.09020634582345],
[0, 0]
],
"handles": {
"start": {
"id": "start",
"index": 0,
"point": [
6.503619523263069,
281.09020634582345
]
"point": [6.503619523263069, 281.09020634582345]
},
"end": {
"id": "end",
"index": 1,
"point": [
0,
0
]
"point": [0, 0]
},
"bend": {
"id": "bend",
"index": 2,
"point": [
3.2518097616315345,
140.54510317291172
]
"point": [3.2518097616315345, 140.54510317291172]
}
},
"decorations": {
@ -82,14 +64,8 @@
"name": "Rectangle",
"parentId": "page1",
"childIndex": 24,
"point": [
0,
0
],
"size": [
67.22075383450237,
72.92795609221832
],
"point": [0, 0],
"size": [67.22075383450237, 72.92795609221832],
"radius": 2,
"rotation": 0,
"isAspectRatioLocked": false,
@ -117,11 +93,8 @@
"id": "page1",
"selectedIds": {},
"camera": {
"point": [
-776.1126994855964,
-404.44260065511594
],
"point": [-776.1126994855964, -404.44260065511594],
"zoom": 0.260047458798192
}
}
}
}

View file

@ -8,8 +8,10 @@ import {
import {
commandKey,
deepCompareArrays,
getSelectedShapes,
getSelectedIds,
getShape,
isMobile,
setToArray,
} from 'utils'
import state, { useSelector } from 'state'
import {
@ -79,22 +81,25 @@ export default function ContextMenu({
}: {
children: React.ReactNode
}): JSX.Element {
const selectedShapes = useSelector(
(s) => getSelectedShapes(s.data),
const selectedShapeIds = useSelector(
(s) => setToArray(getSelectedIds(s.data)),
deepCompareArrays
)
const rContent = useRef<HTMLDivElement>(null)
const hasGroupSelectd = selectedShapes.some((s) => s.type === ShapeType.Group)
const hasTwoOrMore = selectedShapes.length > 1
const hasThreeOrMore = selectedShapes.length > 2
const hasGroupSelected = useSelector((s) =>
selectedShapeIds.some((id) => getShape(s.data, id).type === ShapeType.Group)
)
const hasTwoOrMore = selectedShapeIds.length > 1
const hasThreeOrMore = selectedShapeIds.length > 2
return (
<_ContextMenu.Root>
<_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
<StyledContent ref={rContent} isMobile={isMobile()}>
{selectedShapes.length ? (
{selectedShapeIds.length ? (
<>
{/* <Button onSelect={() => state.send('COPIED')}>
<span>Copy</span>
@ -119,10 +124,10 @@ export default function ContextMenu({
</kbd>
</Button>
<StyledDivider />
{hasGroupSelectd ||
{hasGroupSelected ||
(hasTwoOrMore && (
<>
{hasGroupSelectd && (
{hasGroupSelected && (
<Button onSelect={() => state.send('UNGROUPED')}>
<span>Ungroup</span>
<kbd>

View file

@ -2,31 +2,22 @@ import { getShapeStyle } from 'state/shape-styles'
import { getShapeUtils } from 'state/shape-utils'
import React, { memo } from 'react'
import { useSelector } from 'state'
import { deepCompareArrays, getCurrentCamera, getPage } from 'utils'
import { getCurrentCamera } from 'utils'
import { DotCircle, Handle } from './misc'
import useShapeDef from 'hooks/useShape'
import useShapesToRender from 'hooks/useShapesToRender'
export default function Defs(): JSX.Element {
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
const currentPageShapeIds = useSelector(({ data }) => {
return Object.values(getPage(data).shapes)
.filter(Boolean)
.filter((shape) => !getShapeUtils(shape).isForeignObject)
.sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => shape.id)
}, deepCompareArrays)
const shapeIdsToRender = useShapesToRender()
return (
<defs>
{currentPageShapeIds.map((id) => (
{shapeIdsToRender.map((id) => (
<Def key={id} id={id} />
))}
<DotCircle id="dot" r={4} />
<Handle id="handle" r={4} />
<filter id="expand">
<feMorphology operator="dilate" radius={2 / zoom} />
</filter>
<ExpandDef />
</defs>
)
}
@ -43,3 +34,12 @@ const Def = memo(function Def({ id }: { id: string }) {
{ id, ...style }
)
})
function ExpandDef() {
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
return (
<filter id="expand">
<feMorphology operator="dilate" radius={2 / zoom} />
</filter>
)
}

View file

@ -1,14 +1,6 @@
import { getShapeUtils } from 'state/shape-utils'
import { useSelector } from 'state'
import { Bounds, PageState } from 'types'
import {
deepCompareArrays,
getPage,
getViewport,
boundsCollide,
boundsContain,
} from 'utils'
import Shape from './shape'
import usePageShapes from 'hooks/usePageShapes'
/*
On each state change, compare node ids of all shapes
@ -18,30 +10,8 @@ here; and still cheaper than any other pattern I've found.
const noOffset = [0, 0]
const viewportCache = new WeakMap<PageState, Bounds>()
export default function Page(): JSX.Element {
const currentPageShapeIds = useSelector((s) => {
const page = getPage(s.data)
const pageState = s.data.pageStates[page.id]
if (!viewportCache.has(pageState)) {
const viewport = getViewport(s.data)
viewportCache.set(pageState, viewport)
}
const viewport = viewportCache.get(pageState)
return s.values.currentShapes
.filter((shape) => {
const shapeBounds = getShapeUtils(shape).getBounds(shape)
return (
boundsContain(viewport, shapeBounds) ||
boundsCollide(viewport, shapeBounds)
)
})
.map((shape) => shape.id)
}, deepCompareArrays)
const currentPageShapeIds = usePageShapes()
const isSelecting = useSelector((s) => s.isIn('selecting'))

36
hooks/usePageShapes.ts Normal file
View file

@ -0,0 +1,36 @@
import { useSelector } from 'state'
import { getShapeUtils } from 'state/shape-utils'
import { PageState, Bounds } from 'types'
import {
boundsCollide,
boundsContain,
deepCompareArrays,
getPage,
getViewport,
} from 'utils'
const viewportCache = new WeakMap<PageState, Bounds>()
export default function usePageShapes(): string[] {
return useSelector((s) => {
const page = getPage(s.data)
const pageState = s.data.pageStates[page.id]
if (!viewportCache.has(pageState)) {
const viewport = getViewport(s.data)
viewportCache.set(pageState, viewport)
}
const viewport = viewportCache.get(pageState)
return s.values.currentShapes
.filter((shape) => {
const shapeBounds = getShapeUtils(shape).getBounds(shape)
return (
boundsContain(viewport, shapeBounds) ||
boundsCollide(viewport, shapeBounds)
)
})
.map((shape) => shape.id)
}, deepCompareArrays)
}

View file

@ -0,0 +1,13 @@
import { useSelector } from 'state'
import { getShapeUtils } from 'state/shape-utils'
import { deepCompareArrays, getPage } from 'utils'
export default function useShapesToRender(): string[] {
return useSelector(
(s) =>
Object.values(getPage(s.data).shapes)
.filter((shape) => shape && !getShapeUtils(shape).isForeignObject)
.map((shape) => shape.id),
deepCompareArrays
)
}

View file

@ -92,4 +92,4 @@
"tabWidth": 2,
"useTabs": false
}
}
}

View file

@ -1,4 +1,4 @@
import { PointerInfo } from 'types'
import { DrawShape, PointerInfo } from 'types'
import {
getCameraZoom,
getCurrentCamera,
@ -31,9 +31,14 @@ export function fastDrawUpdate(info: PointerInfo): void {
const selectedId = setToArray(getSelectedIds(data))[0]
const shape = data.document.pages[data.currentPageId].shapes[selectedId]
const shape = data.document.pages[data.currentPageId].shapes[
selectedId
] as DrawShape
data.document.pages[data.currentPageId].shapes[selectedId] = { ...shape }
;(data.document.pages[data.currentPageId].shapes[selectedId] as DrawShape) = {
...shape,
points: [...shape.points],
}
state.forceData(freeze(data))
}

View file

@ -104,7 +104,10 @@ export default class BrushSession extends BaseSession {
// Update the points and update the shape's parents.
const shape = getShape(data, snapshot.id) as DrawShape
getShapeUtils(shape).setProperty(shape, 'points', [...this.points])
// Note: Normally we would want to spread the points to create a new
// array, however we create the new array in hacks/fastDrawUpdate.
getShapeUtils(shape).setProperty(shape, 'points', this.points)
updateParents(data, [shape.id])
}

View file

@ -46,7 +46,6 @@ const draw = registerShapeUtils<DrawShape>({
},
shouldRender(shape, prev) {
// return true
return shape.points !== prev.points || shape.style !== prev.style
},
@ -61,7 +60,9 @@ const draw = registerShapeUtils<DrawShape>({
if (points.length > 0 && points.length < 3) {
return (
<circle id={id} r={+styles.strokeWidth * 0.618} fill={styles.stroke} />
<g id={id}>
<circle r={+styles.strokeWidth * 0.618} fill={styles.stroke} />
</g>
)
}