UNPKG

tldraw

Version:

A tiny little drawing editor.

8 lines (7 loc) • 9.86 kB
{ "version": 3, "sources": ["../../../../../src/lib/tools/SelectTool/childStates/Brushing.ts"], "sourcesContent": ["import {\n\tBox,\n\tMat,\n\tStateNode,\n\tTLCancelEventInfo,\n\tTLFrameShape,\n\tTLGroupShape,\n\tTLKeyboardEventInfo,\n\tTLPageId,\n\tTLPointerEventInfo,\n\tTLShape,\n\tTLShapeId,\n\tTLTickEventInfo,\n\tVec,\n\tpointInPolygon,\n\tpolygonsIntersect,\n} from '@tldraw/editor'\n\nexport class Brushing extends StateNode {\n\tstatic override id = 'brushing'\n\n\tinfo = {} as TLPointerEventInfo & { target: 'canvas' }\n\n\tinitialSelectedShapeIds: TLShapeId[] = []\n\texcludedShapeIds = new Set<TLShapeId>()\n\tisWrapMode = false\n\n\t// The shape that the brush started on\n\tinitialStartShape: TLShape | null = null\n\n\toverride onEnter(info: TLPointerEventInfo & { target: 'canvas' }) {\n\t\tconst { altKey, currentPagePoint } = this.editor.inputs\n\n\t\tthis.isWrapMode = this.editor.user.getIsWrapMode()\n\n\t\tif (altKey) {\n\t\t\tthis.parent.transition('scribble_brushing', info)\n\t\t\treturn\n\t\t}\n\n\t\tthis.excludedShapeIds = new Set(\n\t\t\tthis.editor\n\t\t\t\t.getCurrentPageShapes()\n\t\t\t\t.filter(\n\t\t\t\t\t(shape) =>\n\t\t\t\t\t\tthis.editor.isShapeOfType<TLGroupShape>(shape, 'group') ||\n\t\t\t\t\t\tthis.editor.isShapeOrAncestorLocked(shape)\n\t\t\t\t)\n\t\t\t\t.map((shape) => shape.id)\n\t\t)\n\n\t\tthis.info = info\n\t\tthis.initialSelectedShapeIds = this.editor.getSelectedShapeIds().slice()\n\t\tthis.initialStartShape = this.editor.getShapesAtPoint(currentPagePoint)[0]\n\t\tthis.hitTestShapes()\n\t}\n\n\toverride onExit() {\n\t\tthis.initialSelectedShapeIds = []\n\t\tthis.editor.updateInstanceState({ brush: null })\n\t}\n\n\toverride onTick({ elapsed }: TLTickEventInfo) {\n\t\tconst { editor } = this\n\t\teditor.edgeScrollManager.updateEdgeScrolling(elapsed)\n\t}\n\n\toverride onPointerMove() {\n\t\tthis.hitTestShapes()\n\t}\n\n\toverride onPointerUp() {\n\t\tthis.complete()\n\t}\n\n\toverride onComplete() {\n\t\tthis.complete()\n\t}\n\n\toverride onCancel(info: TLCancelEventInfo) {\n\t\tthis.editor.setSelectedShapes(this.initialSelectedShapeIds)\n\t\tthis.parent.transition('idle', info)\n\t}\n\n\toverride onKeyDown(info: TLKeyboardEventInfo) {\n\t\tif (this.editor.inputs.altKey) {\n\t\t\tthis.parent.transition('scribble_brushing', info)\n\t\t} else {\n\t\t\tthis.hitTestShapes()\n\t\t}\n\t}\n\n\toverride onKeyUp() {\n\t\tthis.hitTestShapes()\n\t}\n\n\tprivate complete() {\n\t\tthis.hitTestShapes()\n\t\tthis.parent.transition('idle')\n\t}\n\n\tprivate hitTestShapes() {\n\t\tconst { editor, excludedShapeIds, isWrapMode } = this\n\t\tconst {\n\t\t\tinputs: { originPagePoint, currentPagePoint, shiftKey, ctrlKey },\n\t\t} = editor\n\n\t\t// We'll be collecting shape ids of selected shapes; if we're holding shift key, we start from our initial shapes\n\t\tconst results = new Set(shiftKey ? this.initialSelectedShapeIds : [])\n\n\t\t// In wrap mode, we need to completely enclose a shape to select it\n\t\tconst isWrapping = isWrapMode ? !ctrlKey : ctrlKey\n\n\t\t// Set the brush to contain the current and origin points\n\t\tconst brush = Box.FromPoints([originPagePoint, currentPagePoint])\n\n\t\t// We'll be testing the corners of the brush against the shapes\n\t\tconst { corners } = brush\n\n\t\tlet A: Vec,\n\t\t\tB: Vec,\n\t\t\tshape: TLShape,\n\t\t\tpageBounds: Box | undefined,\n\t\t\tpageTransform: Mat | undefined,\n\t\t\tlocalCorners: Vec[]\n\n\t\tconst currentPageShapes = editor.getCurrentPageRenderingShapesSorted()\n\t\tconst currentPageId = editor.getCurrentPageId()\n\n\t\ttestAllShapes: for (let i = 0, n = currentPageShapes.length; i < n; i++) {\n\t\t\tshape = currentPageShapes[i]\n\t\t\tif (excludedShapeIds.has(shape.id) || results.has(shape.id)) continue testAllShapes\n\n\t\t\tpageBounds = editor.getShapePageBounds(shape)\n\t\t\tif (!pageBounds) continue testAllShapes\n\n\t\t\t// If the brush fully wraps a shape, it's almost certainly a hit\n\t\t\tif (brush.contains(pageBounds)) {\n\t\t\t\tthis.handleHit(shape, currentPagePoint, currentPageId, results, corners)\n\t\t\t\tcontinue testAllShapes\n\t\t\t}\n\n\t\t\t// If we're in wrap mode and the brush did not fully encloses the shape, it's a miss\n\t\t\t// We also skip frames unless we've completely selected the frame.\n\t\t\tif (isWrapping || editor.isShapeOfType<TLFrameShape>(shape, 'frame')) {\n\t\t\t\tcontinue testAllShapes\n\t\t\t}\n\n\t\t\t// If the brush collides the page bounds, then do hit tests against\n\t\t\t// each of the brush's four sides.\n\t\t\tif (brush.collides(pageBounds)) {\n\t\t\t\t// Shapes expect to hit test line segments in their own coordinate system,\n\t\t\t\t// so we first need to get the brush corners in the shape's local space.\n\t\t\t\tpageTransform = editor.getShapePageTransform(shape)\n\t\t\t\tif (!pageTransform) continue testAllShapes\n\t\t\t\tlocalCorners = pageTransform.clone().invert().applyToPoints(corners)\n\t\t\t\t// See if any of the edges intersect the shape's geometry\n\t\t\t\tconst geometry = editor.getShapeGeometry(shape)\n\t\t\t\thitTestBrushEdges: for (let i = 0; i < 4; i++) {\n\t\t\t\t\tA = localCorners[i]\n\t\t\t\t\tB = localCorners[(i + 1) % 4]\n\t\t\t\t\tif (geometry.hitTestLineSegment(A, B, 0)) {\n\t\t\t\t\t\tthis.handleHit(shape, currentPagePoint, currentPageId, results, corners)\n\t\t\t\t\t\tbreak hitTestBrushEdges\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst currentBrush = editor.getInstanceState().brush\n\t\tif (!currentBrush || !brush.equals(currentBrush)) {\n\t\t\teditor.updateInstanceState({ brush: { ...brush.toJson() } })\n\t\t}\n\n\t\tconst current = editor.getSelectedShapeIds()\n\t\tif (current.length !== results.size || current.some((id) => !results.has(id))) {\n\t\t\teditor.setSelectedShapes(Array.from(results))\n\t\t}\n\t}\n\n\toverride onInterrupt() {\n\t\tthis.editor.updateInstanceState({ brush: null })\n\t}\n\n\tprivate handleHit(\n\t\tshape: TLShape,\n\t\tcurrentPagePoint: Vec,\n\t\tcurrentPageId: TLPageId,\n\t\tresults: Set<TLShapeId>,\n\t\tcorners: Vec[]\n\t) {\n\t\tif (shape.parentId === currentPageId) {\n\t\t\tresults.add(shape.id)\n\t\t\treturn\n\t\t}\n\n\t\t// Find the outermost selectable shape, check to see if it has a\n\t\t// page mask; and if so, check to see if the brush intersects it\n\t\tconst selectedShape = this.editor.getOutermostSelectableShape(shape)\n\t\tconst pageMask = this.editor.getShapeMask(selectedShape.id)\n\t\tif (\n\t\t\tpageMask &&\n\t\t\t!polygonsIntersect(pageMask, corners) &&\n\t\t\t!pointInPolygon(currentPagePoint, pageMask)\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tresults.add(selectedShape.id)\n\t}\n}\n"], "mappings": "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", "names": ["i"] }