kopia lustrzana https://github.com/Tldraw/Tldraw
tool-with-ui
rodzic
4c677b3876
commit
0c9bff6dde
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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])
|
|
@ -165,7 +165,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
<ScribbleWrapper />
|
||||
<ZoomBrushWrapper />
|
||||
<SnapIndicatorWrapper />
|
||||
<SelectionForegroundWrapper />
|
||||
{/* <SelectionForegroundWrapper /> */}
|
||||
<LiveCollaborators />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue