tool-with-ui
Steve Ruiz 2024-05-12 15:19:50 +01:00
rodzic 4c677b3876
commit 0c9bff6dde
8 zmienionych plików z 213 dodań i 68 usunięć

Wyświetl plik

@ -12,9 +12,10 @@ import {
usePreloadAssets,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { SimpleSelectToolUtil } from './SimpleTool'
import { SimpleEraserToolUtil } from './SimpleEraserTool'
import { SimpleSelectToolUtil } from './SimpleSelectTool'
const tools: TLToolUtilConstructor<any, any>[] = [SimpleSelectToolUtil]
const tools: TLToolUtilConstructor<any, any>[] = [SimpleSelectToolUtil, SimpleEraserToolUtil]
//[2]
export default function NewToolExample() {
@ -31,7 +32,7 @@ export default function NewToolExample() {
return (
<div className="tldraw__editor">
<TldrawEditor
initialState="@simple/select"
initialState="@simple/eraser"
shapeUtils={defaultShapeUtils}
bindingUtils={defaultBindingUtils}
tools={tools}
@ -39,10 +40,29 @@ export default function NewToolExample() {
e.createShapes([
{ type: 'geo', x: 200, y: 200 },
{ type: 'geo', x: 400, y: 400 },
{ type: 'text', x: 200, y: 400, props: { text: 'hello' } },
])
}}
>
<TldrawUi>
<TldrawUi
overrides={{
tools: (editor, tools) => {
tools['select'] = {
...tools['select'],
onSelect() {
editor.setCurrentTool('@simple/select')
},
}
tools['eraser'] = {
...tools['eraser'],
onSelect() {
editor.setCurrentTool('@simple/eraser')
},
}
return tools
},
}}
>
<ContextMenu>
<DefaultContextMenuContent />
</ContextMenu>

Wyświetl plik

@ -0,0 +1,130 @@
import { SVGContainer, TLEventInfo, TLShapeId, TLToolState, ToolUtil, Vec } from 'tldraw'
interface SimpleEraserContext extends TLToolState {
readonly type: '@simple/eraser'
state: 'idle' | 'pointing' | 'dragging'
line: Vec[]
}
export class SimpleEraserToolUtil extends ToolUtil<SimpleEraserContext> {
static override type = '@simple/eraser' as const
getDefaultContext(): SimpleEraserContext {
return {
type: '@simple/eraser',
state: 'idle',
line: [],
}
}
underlay() {
return null
}
overlay() {
const { line } = this.getContext()
if (line.length === 0) return null
return (
<>
<SVGContainer>
<polyline
points={line.map((p) => `${p.x},${p.y}`).join(' ')}
strokeWidth={10}
stroke={'lightgrey'}
fill="none"
/>
</SVGContainer>
</>
)
}
getStyles() {
return null
}
// This object is used for events, it's kept in memory and updated as the user interacts with the tool
private memo = {
A: new Vec(),
B: new Vec(),
C: new Vec(),
erasingIds: new Set<TLShapeId>(),
}
onEnter() {
return
}
onExit() {
return
}
onEvent(event: TLEventInfo) {
const { editor, memo } = this
const context = this.getContext()
switch (context.state) {
case 'idle': {
if (event.name === 'pointer_down') {
this.setContext({
state: 'pointing',
})
}
break
}
case 'pointing': {
if (editor.inputs.isDragging) {
const { originPagePoint, currentPagePoint } = editor.inputs
this.setContext({ state: 'dragging', line: [originPagePoint, currentPagePoint] })
memo.A = currentPagePoint.clone()
memo.B = currentPagePoint.clone()
memo.C = currentPagePoint.clone()
}
break
}
case 'dragging': {
if (editor.inputs.isDragging) {
if (event.name === 'tick') {
const { currentPagePoint } = editor.inputs
if (currentPagePoint.equals(memo.C)) return
memo.A = memo.B
memo.B = memo.C
memo.C = currentPagePoint.clone()
const { erasingIds } = memo
for (const id of editor.getCurrentPageShapeIds()) {
if (erasingIds.has(id)) continue
if (
editor
.getShapeGeometry(id)
.hitTestLineSegment(
editor.getPointInShapeSpace(id, memo.B),
editor.getPointInShapeSpace(id, memo.C)
)
) {
erasingIds.add(id)
}
}
this.setContext({ line: [memo.A, memo.B, memo.C] })
const currentSelectedIds = editor.getSelectedShapeIds()
if (
currentSelectedIds.length !== erasingIds.size ||
currentSelectedIds.some((id) => !erasingIds.has(id))
) {
editor.setErasingShapes(Array.from(erasingIds))
}
}
} else {
editor.deleteShapes(Array.from(memo.erasingIds))
this.memo.erasingIds.clear()
this.setContext({ state: 'idle', line: [] })
}
break
}
}
}
}

Wyświetl plik

@ -13,6 +13,7 @@ import {
TLToolState,
ToolUtil,
dedupe,
getOwnProperty,
useEditor,
useEditorComponents,
useValue,
@ -22,7 +23,7 @@ interface SimpleSelectContext extends TLToolState {
readonly type: '@simple/select'
ids: TLShapeId[]
brush: BoxLike | null
state: 'idle' | 'pointing' | 'dragging'
state: 'idle' | 'pointing' | 'brushing'
}
const simpleSelectStyles = new SharedStyleMap()
@ -60,7 +61,23 @@ export class SimpleSelectToolUtil extends ToolUtil<SimpleSelectContext> {
}
getStyles() {
return simpleSelectStyles
const { editor } = this
const selectedShapeIds = editor.getSelectedShapeIds()
if (selectedShapeIds.length === 0) {
return simpleSelectStyles
}
const sharedStyleMap = new SharedStyleMap()
for (const id of selectedShapeIds) {
const shape = editor.getShape(id)
if (!shape) continue
for (const [style, propKey] of editor.styleProps[shape.type]) {
sharedStyleMap.applyValue(style, getOwnProperty(shape.props, propKey))
}
}
return sharedStyleMap
}
// This object is used for events, it's kept in memory and updated as the user interacts with the tool
@ -68,6 +85,14 @@ export class SimpleSelectToolUtil extends ToolUtil<SimpleSelectContext> {
initialSelectedIds: [] as TLShapeId[],
}
onEnter() {
return
}
onExit() {
return
}
onEvent(event: TLEventInfo) {
const { editor, memo } = this
const context = this.getContext()
@ -85,14 +110,14 @@ export class SimpleSelectToolUtil extends ToolUtil<SimpleSelectContext> {
if (editor.inputs.isDragging) {
const { originPagePoint, currentPagePoint } = editor.inputs
const box = Box.FromPoints([originPagePoint, currentPagePoint])
this.setContext({ state: 'dragging', brush: box.toJson() })
this.setContext({ state: 'brushing', brush: box.toJson() })
// Stash the selected ids so we can restore them later
memo.initialSelectedIds = editor.getSelectedShapeIds()
}
break
}
case 'dragging': {
case 'brushing': {
if (editor.inputs.isDragging) {
if (
event.name === 'pointer_move' ||
@ -152,10 +177,14 @@ Times when states talk to each-other
Which pieces of state are "universal" vs which are "tool-specific"?
For example, the selection brush is only relevant to the select tool but the selected shape ids are relevant to all tools.
Therefore the selection brush should be stored in the select tool's context but the selected shape ids should be stored in the editor's context.
The erasing / cropping / hinted shapes are probably relevant only to their tools, and should be extracted from the default canvas into their tool's overlay / underlay.
Snapping seems to be a universal UI (because it's used while shapes are being resized, for example) but that happens while shape tools are being used to create a shape. In that case the select tool is technically active, however.
We could further separate this into tools vs. interactions, each with their own UI.
For example, here the indicators are tool-specific, and should be displayed when the tool is active.
But the brush is only shown when the tool is active and the user is dragging, so it's an interaction-specific UI.
In other words, we could extract the interaction out of the tool into its own concept, and have it be responsible for that part of the UI (i.e. its own overlay / underlay).
But the consequences of that interaction would not be re-usable. The "zoom brush" is almost identical but we use its box for something else (i.e. zooming in or out).
*/
function SelectionBrush({ brush }: { brush: BoxLike | null }) {
@ -173,23 +202,6 @@ function SelectionBrush({ brush }: { brush: BoxLike | null }) {
)
}
// function SelectedShapeIds({ shapeIds }: { shapeIds: TLShapeId[] }) {
// if (shapeIds.length === 0) return null
// const { ShapeIndicator } = useEditorComponents()
// if (!ShapeIndicator) return null
// return (
// <>
// {renderingShapes.map(({ id }) => (
// <ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
// ))}
// </>
// )
// return null
// }
function ShapeIndicators() {
const editor = useEditor()
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])

Wyświetl plik

@ -165,7 +165,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
<ScribbleWrapper />
<ZoomBrushWrapper />
<SnapIndicatorWrapper />
<SelectionForegroundWrapper />
{/* <SelectionForegroundWrapper /> */}
<LiveCollaborators />
</div>
</div>

Wyświetl plik

@ -622,6 +622,8 @@ export class Editor extends EventEmitter<TLEventMap> {
this.stopFollowingUser()
}
this.getCurrentTool().onEnter({})
this.on('tick', this._flushEventsForTick)
requestAnimationFrame(() => {
@ -1052,7 +1054,14 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
setCurrentTool(id: string, info = {}): this {
this._currentToolId.set(id)
const current = this.getCurrentTool()
const next = this.getTool(id)
if (current !== next) {
current.onExit(info)
console.log('setting', id)
this._currentToolId.set(id)
current.onEnter(info)
}
return this
}

Wyświetl plik

@ -49,6 +49,16 @@ export abstract class ToolUtil<T extends TLToolState> {
*/
abstract overlay(): ReactNode
/**
* Handle the tool becoming active.
*/
abstract onEnter(info: any): void
/**
* Handle the tool becoming inactive.
*/
abstract onExit(info: any): void
/**
* Handle an event.
*/

Wyświetl plik

@ -81,18 +81,7 @@ export const TldrawSelectionForeground = track(function TldrawSelectionForegroun
let shouldDisplayBox =
(showSelectionBounds &&
editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.pointing_canvas',
'select.pointing_selection',
'select.pointing_shape',
'select.crop.idle',
'select.crop.pointing_crop',
'select.pointing_resize_handle',
'select.pointing_crop_handle'
)) ||
) ||
(showSelectionBounds &&
editor.isIn('select.resizing') &&
onlyShape &&

Wyświetl plik

@ -1,38 +1,13 @@
import {
DefaultColorStyle,
DefaultDashStyle,
DefaultFillStyle,
DefaultSizeStyle,
ReadonlySharedStyleMap,
SharedStyleMap,
StyleProp,
useEditor,
useValue,
} from '@tldraw/editor'
const selectToolStyles: readonly StyleProp<any>[] = Object.freeze([
DefaultColorStyle,
DefaultDashStyle,
DefaultFillStyle,
DefaultSizeStyle,
])
import { ReadonlySharedStyleMap, useEditor, useValue } from '@tldraw/editor'
/** @public */
export function useRelevantStyles(
stylesToCheck: StyleProp<any>[] = []
): ReadonlySharedStyleMap | null {
export function useRelevantStyles(): ReadonlySharedStyleMap | null {
const editor = useEditor()
return useValue(
'getRelevantStyles',
() => {
const currentTool = editor.getCurrentTool()
const currentToolStyles = currentTool.getStyles()
console.log(stylesToCheck)
if (!currentToolStyles) {
return new SharedStyleMap(editor.getSharedStyles())
}
return currentToolStyles
// const isInShapeSpecificTool = false