kopia lustrzana https://github.com/Tldraw/Tldraw
tool-with-ui
rodzic
91903c9761
commit
119e812612
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
ContextMenu,
|
||||
DefaultContextMenuContent,
|
||||
ErrorScreen,
|
||||
LoadingScreen,
|
||||
TLToolUtilConstructor,
|
||||
TldrawEditor,
|
||||
TldrawUi,
|
||||
defaultBindingUtils,
|
||||
defaultEditorAssetUrls,
|
||||
defaultShapeUtils,
|
||||
usePreloadAssets,
|
||||
} from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
import { SimpleSelectToolUtil } from './SimpleTool'
|
||||
|
||||
const tools: TLToolUtilConstructor<any, any>[] = [SimpleSelectToolUtil]
|
||||
|
||||
//[2]
|
||||
export default function NewToolExample() {
|
||||
const assetLoading = usePreloadAssets(defaultEditorAssetUrls)
|
||||
|
||||
if (assetLoading.error) {
|
||||
return <ErrorScreen>Could not load assets.</ErrorScreen>
|
||||
}
|
||||
|
||||
if (!assetLoading.done) {
|
||||
return <LoadingScreen>Loading assets...</LoadingScreen>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<TldrawEditor
|
||||
initialState="@simple/select"
|
||||
shapeUtils={defaultShapeUtils}
|
||||
bindingUtils={defaultBindingUtils}
|
||||
tools={tools}
|
||||
onMount={(e) => {
|
||||
e.createShapes([
|
||||
{ type: 'geo', x: 200, y: 200 },
|
||||
{ type: 'geo', x: 400, y: 400 },
|
||||
])
|
||||
}}
|
||||
>
|
||||
<TldrawUi>
|
||||
<ContextMenu>
|
||||
<DefaultContextMenuContent />
|
||||
</ContextMenu>
|
||||
</TldrawUi>
|
||||
</TldrawEditor>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: New tool
|
||||
component: ./NewToolExample.tsx
|
||||
category: editor-api
|
||||
---
|
||||
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
...
|
|
@ -0,0 +1,271 @@
|
|||
import { useRef } from 'react'
|
||||
import {
|
||||
Box,
|
||||
BoxLike,
|
||||
DefaultColorStyle,
|
||||
DefaultDashStyle,
|
||||
DefaultFillStyle,
|
||||
DefaultSizeStyle,
|
||||
SVGContainer,
|
||||
SharedStyleMap,
|
||||
TLEventInfo,
|
||||
TLShapeId,
|
||||
TLToolState,
|
||||
ToolUtil,
|
||||
dedupe,
|
||||
useEditor,
|
||||
useEditorComponents,
|
||||
useValue,
|
||||
} from 'tldraw'
|
||||
|
||||
interface SimpleSelectContext extends TLToolState {
|
||||
readonly type: '@simple/select'
|
||||
ids: TLShapeId[]
|
||||
brush: BoxLike | null
|
||||
state: 'idle' | 'pointing' | 'dragging'
|
||||
}
|
||||
|
||||
const simpleSelectStyles = new SharedStyleMap()
|
||||
simpleSelectStyles.applyValue(DefaultColorStyle, DefaultColorStyle.defaultValue)
|
||||
simpleSelectStyles.applyValue(DefaultFillStyle, DefaultFillStyle.defaultValue)
|
||||
simpleSelectStyles.applyValue(DefaultDashStyle, DefaultDashStyle.defaultValue)
|
||||
simpleSelectStyles.applyValue(DefaultSizeStyle, DefaultSizeStyle.defaultValue)
|
||||
|
||||
export class SimpleSelectToolUtil extends ToolUtil<SimpleSelectContext> {
|
||||
static override type = '@simple/select' as const
|
||||
|
||||
getDefaultContext(): SimpleSelectContext {
|
||||
return {
|
||||
type: '@simple/select',
|
||||
ids: [],
|
||||
brush: null,
|
||||
state: 'idle',
|
||||
}
|
||||
}
|
||||
|
||||
underlay() {
|
||||
return null
|
||||
}
|
||||
|
||||
overlay() {
|
||||
const { brush } = this.getContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShapeIndicators />
|
||||
<HintedShapeIndicator />
|
||||
<SelectionBrush brush={brush} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
getStyles() {
|
||||
return simpleSelectStyles
|
||||
}
|
||||
|
||||
// This object is used for events, it's kept in memory and updated as the user interacts with the tool
|
||||
private memo = {
|
||||
initialSelectedIds: [] as TLShapeId[],
|
||||
}
|
||||
|
||||
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
|
||||
const box = Box.FromPoints([originPagePoint, currentPagePoint])
|
||||
this.setContext({ state: 'dragging', brush: box.toJson() })
|
||||
|
||||
// Stash the selected ids so we can restore them later
|
||||
memo.initialSelectedIds = editor.getSelectedShapeIds()
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'dragging': {
|
||||
if (editor.inputs.isDragging) {
|
||||
if (
|
||||
event.name === 'pointer_move' ||
|
||||
// for modifiers
|
||||
event.name === 'key_down' ||
|
||||
event.name === 'key_up'
|
||||
) {
|
||||
const { originPagePoint, currentPagePoint } = editor.inputs
|
||||
const box = Box.FromPoints([originPagePoint, currentPagePoint])
|
||||
|
||||
// update the box in the context
|
||||
this.setContext({ brush: box.toJson() })
|
||||
|
||||
const hitIds = new Set<TLShapeId>()
|
||||
|
||||
// If we're holding shift, add the initial selected ids to the hitIds set
|
||||
if (editor.inputs.shiftKey) {
|
||||
for (const id of memo.initialSelectedIds) {
|
||||
hitIds.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Test the rest of the shapes on the page (broad phase only for simplifity)
|
||||
for (const shape of editor.getCurrentPageShapes()) {
|
||||
if (hitIds.has(shape.id)) continue
|
||||
const pageBounds = editor.getShapePageBounds(shape.id)
|
||||
if (!pageBounds) continue
|
||||
if (box.collides(pageBounds)) {
|
||||
hitIds.add(shape.id)
|
||||
}
|
||||
}
|
||||
|
||||
// If the selected ids have changed, update the selection
|
||||
const currentSelectedIds = editor.getSelectedShapeIds()
|
||||
if (
|
||||
currentSelectedIds.length !== hitIds.size ||
|
||||
currentSelectedIds.some((id) => !hitIds.has(id))
|
||||
) {
|
||||
editor.setSelectedShapes(Array.from(hitIds))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.setContext({ state: 'idle', brush: null })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Times when states talk to each-other
|
||||
- reading / writing to selection
|
||||
- reading / writing to styles
|
||||
- switching to other tools
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
function SelectionBrush({ brush }: { brush: BoxLike | null }) {
|
||||
if (!brush) return null
|
||||
return (
|
||||
<SVGContainer>
|
||||
<rect
|
||||
className="tl-brush tl-brush__default"
|
||||
x={brush.x}
|
||||
y={brush.y}
|
||||
width={brush.w}
|
||||
height={brush.h}
|
||||
/>
|
||||
</SVGContainer>
|
||||
)
|
||||
}
|
||||
|
||||
// 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])
|
||||
const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
|
||||
const idsToDisplay = useValue(
|
||||
'should display selected ids',
|
||||
() => {
|
||||
// todo: move to tldraw selected ids wrappe
|
||||
const prev = rPreviousSelectedShapeIds.current
|
||||
const next = new Set<TLShapeId>()
|
||||
if (
|
||||
editor.isInAny(
|
||||
'select.idle',
|
||||
'select.brushing',
|
||||
'select.scribble_brushing',
|
||||
'select.editing_shape',
|
||||
'select.pointing_shape',
|
||||
'select.pointing_selection',
|
||||
'select.pointing_handle'
|
||||
) &&
|
||||
!editor.getInstanceState().isChangingStyle
|
||||
) {
|
||||
const selected = editor.getSelectedShapeIds()
|
||||
for (const id of selected) {
|
||||
next.add(id)
|
||||
}
|
||||
if (editor.isInAny('select.idle', 'select.editing_shape')) {
|
||||
const instanceState = editor.getInstanceState()
|
||||
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
|
||||
const hovered = editor.getHoveredShapeId()
|
||||
if (hovered) next.add(hovered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prev.size !== next.size) {
|
||||
rPreviousSelectedShapeIds.current = next
|
||||
return next
|
||||
}
|
||||
|
||||
for (const id of next) {
|
||||
if (!prev.has(id)) {
|
||||
rPreviousSelectedShapeIds.current = next
|
||||
return next
|
||||
}
|
||||
}
|
||||
|
||||
return prev
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
||||
const { ShapeIndicator } = useEditorComponents()
|
||||
if (!ShapeIndicator) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderingShapes.map(({ id }) => (
|
||||
<ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function HintedShapeIndicator() {
|
||||
const editor = useEditor()
|
||||
const { ShapeIndicator } = useEditorComponents()
|
||||
|
||||
const ids = useValue('hinting shape ids', () => dedupe(editor.getHintingShapeIds()), [editor])
|
||||
|
||||
if (!ids.length) return null
|
||||
if (!ShapeIndicator) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{ids.map((id) => (
|
||||
<ShapeIndicator className="tl-user-indicator__hint" shapeId={id} key={id + '_hinting'} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -186,6 +186,7 @@ export { GroupShapeUtil } from './lib/editor/shapes/group/GroupShapeUtil'
|
|||
export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
|
||||
export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
|
||||
export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'
|
||||
export { ToolUtil, type TLToolState, type TLToolUtilConstructor } from './lib/editor/tools/ToolUtil'
|
||||
export {
|
||||
useSvgExportContext,
|
||||
type SvgExportContext,
|
||||
|
|
|
@ -18,7 +18,7 @@ import { TLUser, createTLUser } from './config/createTLUser'
|
|||
import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
|
||||
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
|
||||
import { Editor } from './editor/Editor'
|
||||
import { TLStateNodeConstructor } from './editor/tools/StateNode'
|
||||
import { TLToolUtilConstructor } from './editor/tools/ToolUtil'
|
||||
import { TLCameraOptions } from './editor/types/misc-types'
|
||||
import { ContainerProvider, useContainer } from './hooks/useContainer'
|
||||
import { useCursor } from './hooks/useCursor'
|
||||
|
@ -85,7 +85,7 @@ export interface TldrawEditorBaseProps {
|
|||
/**
|
||||
* An array of tools to add to the editor's state chart.
|
||||
*/
|
||||
tools?: readonly TLStateNodeConstructor[]
|
||||
tools?: readonly TLToolUtilConstructor<any, any>[]
|
||||
|
||||
/**
|
||||
* Whether to automatically focus the editor when it mounts.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { react, useQuickReactor, useValue } from '@tldraw/state'
|
||||
import { react, useQuickReactor, useStateTracking, useValue } from '@tldraw/state'
|
||||
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
|
||||
import { modulate, objectMapValues } from '@tldraw/utils'
|
||||
import classNames from 'classnames'
|
||||
import { Fragment, JSX, useEffect, useRef, useState } from 'react'
|
||||
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS, TEXT_SHADOW_LOD } from '../../constants'
|
||||
|
@ -152,6 +152,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
)}
|
||||
<GridWrapper />
|
||||
<div ref={rHtmlLayer} className="tl-html-layer tl-shapes" draggable={false}>
|
||||
<ActiveToolUnderlay />
|
||||
<OnTheCanvasWrapper />
|
||||
<SelectionBackgroundWrapper />
|
||||
{hideShapes ? null : debugSvg ? <ShapesWithSVGs /> : <ShapesToDisplay />}
|
||||
|
@ -159,12 +160,10 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
<div className="tl-overlays">
|
||||
<div ref={rHtmlLayer2} className="tl-html-layer">
|
||||
{debugGeometry ? <GeometryDebuggingView /> : null}
|
||||
<ActiveToolOverlay />
|
||||
<HandlesWrapper />
|
||||
<BrushWrapper />
|
||||
<ScribbleWrapper />
|
||||
<ZoomBrushWrapper />
|
||||
<ShapeIndicators />
|
||||
<HintedShapeIndicator />
|
||||
<SnapIndicatorWrapper />
|
||||
<SelectionForegroundWrapper />
|
||||
<LiveCollaborators />
|
||||
|
@ -175,6 +174,18 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
)
|
||||
}
|
||||
|
||||
function ActiveToolUnderlay() {
|
||||
const editor = useEditor()
|
||||
const activeTool = useValue('active tool', () => editor.getCurrentTool(), [editor])
|
||||
return useStateTracking('Active tool underlay', () => activeTool.underlay?.())
|
||||
}
|
||||
|
||||
function ActiveToolOverlay() {
|
||||
const editor = useEditor()
|
||||
const activeTool = useValue('active tool', () => editor.getCurrentTool(), [editor])
|
||||
return useStateTracking('Active tool overlay', () => activeTool.overlay?.())
|
||||
}
|
||||
|
||||
function GridWrapper() {
|
||||
const editor = useEditor()
|
||||
const gridSize = useValue('gridSize', () => editor.getDocumentSettings().gridSize, [editor])
|
||||
|
@ -209,16 +220,6 @@ function ScribbleWrapper() {
|
|||
)
|
||||
}
|
||||
|
||||
function BrushWrapper() {
|
||||
const editor = useEditor()
|
||||
const brush = useValue('brush', () => editor.getInstanceState().brush, [editor])
|
||||
const { Brush } = useEditorComponents()
|
||||
|
||||
if (!(Brush && brush)) return null
|
||||
|
||||
return <Brush className="tl-user-brush" brush={brush} />
|
||||
}
|
||||
|
||||
function ZoomBrushWrapper() {
|
||||
const editor = useEditor()
|
||||
const zoomBrush = useValue('zoomBrush', () => editor.getInstanceState().zoomBrush, [editor])
|
||||
|
@ -441,88 +442,6 @@ function ShapesToDisplay() {
|
|||
)
|
||||
}
|
||||
|
||||
function ShapeIndicators() {
|
||||
const editor = useEditor()
|
||||
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
|
||||
const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
|
||||
const idsToDisplay = useValue(
|
||||
'should display selected ids',
|
||||
() => {
|
||||
// todo: move to tldraw selected ids wrappe
|
||||
const prev = rPreviousSelectedShapeIds.current
|
||||
const next = new Set<TLShapeId>()
|
||||
if (
|
||||
editor.isInAny(
|
||||
'select.idle',
|
||||
'select.brushing',
|
||||
'select.scribble_brushing',
|
||||
'select.editing_shape',
|
||||
'select.pointing_shape',
|
||||
'select.pointing_selection',
|
||||
'select.pointing_handle'
|
||||
) &&
|
||||
!editor.getInstanceState().isChangingStyle
|
||||
) {
|
||||
const selected = editor.getSelectedShapeIds()
|
||||
for (const id of selected) {
|
||||
next.add(id)
|
||||
}
|
||||
if (editor.isInAny('select.idle', 'select.editing_shape')) {
|
||||
const instanceState = editor.getInstanceState()
|
||||
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
|
||||
const hovered = editor.getHoveredShapeId()
|
||||
if (hovered) next.add(hovered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prev.size !== next.size) {
|
||||
rPreviousSelectedShapeIds.current = next
|
||||
return next
|
||||
}
|
||||
|
||||
for (const id of next) {
|
||||
if (!prev.has(id)) {
|
||||
rPreviousSelectedShapeIds.current = next
|
||||
return next
|
||||
}
|
||||
}
|
||||
|
||||
return prev
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
||||
const { ShapeIndicator } = useEditorComponents()
|
||||
if (!ShapeIndicator) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderingShapes.map(({ id }) => (
|
||||
<ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function HintedShapeIndicator() {
|
||||
const editor = useEditor()
|
||||
const { ShapeIndicator } = useEditorComponents()
|
||||
|
||||
const ids = useValue('hinting shape ids', () => dedupe(editor.getHintingShapeIds()), [editor])
|
||||
|
||||
if (!ids.length) return null
|
||||
if (!ShapeIndicator) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{ids.map((id) => (
|
||||
<ShapeIndicator className="tl-user-indicator__hint" shapeId={id} key={id + '_hinting'} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function CursorDef() {
|
||||
return (
|
||||
<g id="cursor">
|
||||
|
|
|
@ -66,7 +66,6 @@ import {
|
|||
getIndicesAbove,
|
||||
getIndicesBetween,
|
||||
getOwnProperty,
|
||||
hasOwnProperty,
|
||||
last,
|
||||
sortById,
|
||||
sortByIndex,
|
||||
|
@ -133,8 +132,7 @@ import { TextManager } from './managers/TextManager'
|
|||
import { TickManager } from './managers/TickManager'
|
||||
import { UserPreferencesManager } from './managers/UserPreferencesManager'
|
||||
import { ShapeUtil, TLResizeMode, TLShapeUtilConstructor } from './shapes/ShapeUtil'
|
||||
import { RootState } from './tools/RootState'
|
||||
import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
|
||||
import { TLToolUtilConstructor, ToolUtil } from './tools/ToolUtil'
|
||||
import { TLContent } from './types/clipboard-types'
|
||||
import { TLEventMap } from './types/emit-types'
|
||||
import {
|
||||
|
@ -177,14 +175,14 @@ export interface TLEditorOptions {
|
|||
* An array of shapes to use in the editor. These will be used to create and manage shapes in the editor.
|
||||
*/
|
||||
shapeUtils: readonly TLShapeUtilConstructor<TLUnknownShape>[]
|
||||
/**
|
||||
* An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor.
|
||||
*/
|
||||
tools: readonly TLToolUtilConstructor<any>[]
|
||||
/**
|
||||
* An array of bindings to use in the editor. These will be used to create and manage bindings in the editor.
|
||||
*/
|
||||
bindingUtils: readonly TLBindingUtilConstructor<TLUnknownBinding>[]
|
||||
/**
|
||||
* An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor.
|
||||
*/
|
||||
tools: readonly TLStateNodeConstructor[]
|
||||
/**
|
||||
* Should return a containing html element which has all the styles applied to the editor. If not
|
||||
* given, the body element will be used.
|
||||
|
@ -213,12 +211,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
constructor({
|
||||
store,
|
||||
user,
|
||||
shapeUtils,
|
||||
bindingUtils,
|
||||
tools,
|
||||
shapeUtils = [],
|
||||
bindingUtils = [],
|
||||
tools = [],
|
||||
getContainer,
|
||||
cameraOptions,
|
||||
initialState,
|
||||
initialState: initialTool,
|
||||
inferDarkMode,
|
||||
}: TLEditorOptions) {
|
||||
super()
|
||||
|
@ -232,23 +230,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
},
|
||||
})
|
||||
|
||||
this.snaps = new SnapManager(this)
|
||||
|
||||
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
|
||||
|
||||
this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
|
||||
|
||||
this.getContainer = getContainer ?? (() => document.body)
|
||||
|
||||
this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
|
||||
this.snaps = new SnapManager(this)
|
||||
this.textMeasure = new TextManager(this)
|
||||
this._tickManager = new TickManager(this)
|
||||
this.environment = new EnvironmentManager(this)
|
||||
this.scribbles = new ScribbleManager(this)
|
||||
this.performanceTracker = new PerformanceTracker()
|
||||
|
||||
class NewRoot extends RootState {
|
||||
static override initial = initialState ?? ''
|
||||
}
|
||||
|
||||
this.root = new NewRoot(this)
|
||||
this.root.children = {}
|
||||
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
|
||||
|
||||
const allShapeUtils = checkShapesAndAddCore(shapeUtils)
|
||||
|
||||
|
@ -285,18 +277,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
this.bindingUtils = _bindingUtils
|
||||
|
||||
// Tools.
|
||||
// Accept tools from constructor parameters which may not conflict with the root note's default or
|
||||
// "baked in" tools, select and zoom.
|
||||
const toolMap: Record<string, ToolUtil<any>> = {}
|
||||
|
||||
for (const Tool of [...tools]) {
|
||||
if (hasOwnProperty(this.root.children!, Tool.id)) {
|
||||
throw Error(`Can't override tool with id "${Tool.id}"`)
|
||||
}
|
||||
this.root.children![Tool.id] = new Tool(this, this.root)
|
||||
const tool = new Tool(this)
|
||||
toolMap[Tool.type] = tool
|
||||
tool.setContext(tool.getDefaultContext())
|
||||
}
|
||||
|
||||
this.environment = new EnvironmentManager(this)
|
||||
this.scribbles = new ScribbleManager(this)
|
||||
if (!initialTool) throw Error('No initial tool provided')
|
||||
|
||||
this._tools.set(toolMap)
|
||||
this._currentToolId.set(initialTool)
|
||||
|
||||
// Cleanup
|
||||
|
||||
|
@ -626,12 +618,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
})
|
||||
})
|
||||
|
||||
if (initialState && this.root.children[initialState] === undefined) {
|
||||
throw Error(`No state found for initialState "${initialState}".`)
|
||||
}
|
||||
|
||||
this.root.enter(undefined, 'initial')
|
||||
|
||||
if (this.getInstanceState().followingUserId) {
|
||||
this.stopFollowingUser()
|
||||
}
|
||||
|
@ -641,8 +627,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
requestAnimationFrame(() => {
|
||||
this._tickManager.start()
|
||||
})
|
||||
|
||||
this.performanceTracker = new PerformanceTracker()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -652,13 +636,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*/
|
||||
readonly store: TLStore
|
||||
|
||||
/**
|
||||
* The root state of the statechart.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
readonly root: RootState
|
||||
|
||||
/**
|
||||
* A set of functions to call when the app is disposed.
|
||||
*
|
||||
|
@ -971,7 +948,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
willCrashApp,
|
||||
},
|
||||
extras: {
|
||||
activeStateNode: this.root.getPath(),
|
||||
activeStateNode: this.getCurrentToolId(),
|
||||
selectedShapes: this.getSelectedShapes(),
|
||||
editingShape: editingShapeId ? this.getShape(editingShapeId) : undefined,
|
||||
inputs: this.inputs,
|
||||
|
@ -1025,7 +1002,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
@computed getPath() {
|
||||
return this.root.getPath().split('root.')[1]
|
||||
return 'root'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1042,18 +1019,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
isIn(path: string): boolean {
|
||||
const ids = path.split('.').reverse()
|
||||
let state = this.root as StateNode
|
||||
while (ids.length > 0) {
|
||||
const id = ids.pop()
|
||||
if (!id) return true
|
||||
const current = state.getCurrent()
|
||||
if (current?.id === id) {
|
||||
if (ids.length === 0) return true
|
||||
state = current
|
||||
continue
|
||||
} else return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -1087,55 +1052,29 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
setCurrentTool(id: string, info = {}): this {
|
||||
this.root.transition(id, info)
|
||||
this._currentToolId.set(id)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* The current selected tool.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@computed getCurrentTool(): StateNode {
|
||||
return this.root.getCurrent()!
|
||||
}
|
||||
// /**
|
||||
// * The current selected tool.
|
||||
// *
|
||||
// * @public
|
||||
// */
|
||||
// @computed getCurrentTool(): StateNode {
|
||||
// return this.root.getCurrent()!
|
||||
// }
|
||||
|
||||
/**
|
||||
* The id of the current selected tool.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@computed getCurrentToolId(): string {
|
||||
const currentTool = this.getCurrentTool()
|
||||
if (!currentTool) return ''
|
||||
return currentTool.getCurrentToolIdMask() ?? currentTool.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a descendant by its path.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* state.getStateDescendant('select')
|
||||
* state.getStateDescendant('select.brushing')
|
||||
* ```
|
||||
*
|
||||
* @param path - The descendant's path of state ids, separated by periods.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getStateDescendant<T extends StateNode>(path: string): T | undefined {
|
||||
const ids = path.split('.').reverse()
|
||||
let state = this.root as StateNode
|
||||
while (ids.length > 0) {
|
||||
const id = ids.pop()
|
||||
if (!id) return state as T
|
||||
const childState = state.children?.[id]
|
||||
if (!childState) return undefined
|
||||
state = childState
|
||||
}
|
||||
return state as T
|
||||
}
|
||||
// /**
|
||||
// * The id of the current selected tool.
|
||||
// *
|
||||
// * @public
|
||||
// */
|
||||
// @computed getCurrentToolId(): string {
|
||||
// const currentTool = this.getCurrentTool()
|
||||
// if (!currentTool) return ''
|
||||
// return currentTool.getCurrentToolIdMask() ?? currentTool.id
|
||||
// }
|
||||
|
||||
/* ---------------- Document Settings --------------- */
|
||||
|
||||
|
@ -1962,6 +1901,24 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
return this
|
||||
}
|
||||
|
||||
/* ---------------------- Tool ---------------------- */
|
||||
|
||||
_tools = atom<Record<string, ToolUtil<any>>>('tools', {})
|
||||
|
||||
private _currentToolId = atom<string>('current tool id', '')
|
||||
|
||||
@computed getCurrentToolId() {
|
||||
return this._currentToolId.get()
|
||||
}
|
||||
|
||||
@computed getCurrentTool() {
|
||||
return this._tools.get()[this.getCurrentToolId()]
|
||||
}
|
||||
|
||||
getTool<T extends ToolUtil<any>>(id: string): T {
|
||||
return this._tools.get()[id] as T
|
||||
}
|
||||
|
||||
/* --------------------- Camera --------------------- */
|
||||
|
||||
/** @internal */
|
||||
|
@ -7081,17 +7038,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
// If the current tool is associated with a shape, return the styles for that shape.
|
||||
// Otherwise, just return an empty map.
|
||||
const currentTool = this.root.getCurrent()!
|
||||
const styles = new SharedStyleMap()
|
||||
|
||||
if (!currentTool) return styles
|
||||
|
||||
if (currentTool.shapeType) {
|
||||
for (const style of this.styleProps[currentTool.shapeType].keys()) {
|
||||
styles.applyValue(style, this.getStyleForNextShape(style))
|
||||
}
|
||||
}
|
||||
|
||||
return styles
|
||||
}
|
||||
|
||||
|
@ -8157,7 +8104,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
}
|
||||
if (elapsed > 0) {
|
||||
this.root.handleEvent({ type: 'misc', name: 'tick', elapsed })
|
||||
this.getCurrentTool().onEvent({ type: 'misc', name: 'tick', elapsed })
|
||||
}
|
||||
this.scribbles.tick(elapsed)
|
||||
})
|
||||
|
@ -8182,7 +8129,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
}
|
||||
|
||||
this.root.handleEvent(info)
|
||||
this.getCurrentTool().onEvent(info)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -8592,9 +8539,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// changed then hand both events to the statechart
|
||||
const clickInfo = this._clickManager.handlePointerEvent(info)
|
||||
if (info.name !== clickInfo.name) {
|
||||
this.root.handleEvent(info)
|
||||
this.getCurrentTool().onEvent(info)
|
||||
this.emit('event', info)
|
||||
this.root.handleEvent(clickInfo)
|
||||
this.getCurrentTool().onEvent(clickInfo)
|
||||
this.emit('event', clickInfo)
|
||||
return
|
||||
}
|
||||
|
@ -8603,7 +8550,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
// Send the event to the statechart. It will be handled by all
|
||||
// active states, starting at the root.
|
||||
this.root.handleEvent(info)
|
||||
this.getCurrentTool().onEvent(info)
|
||||
this.emit('event', info)
|
||||
|
||||
return this
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { atom, computed } from '@tldraw/state'
|
||||
import { ReactNode } from 'react'
|
||||
import { ReadonlySharedStyleMap } from '../../utils/SharedStylesMap'
|
||||
import { Editor } from '../Editor'
|
||||
import { TLEventInfo } from '../types/event-types'
|
||||
|
||||
// When the tool is loaded, it adds its state to the editor's state
|
||||
|
||||
export interface TLToolState {
|
||||
type: string
|
||||
}
|
||||
|
||||
export abstract class ToolUtil<T extends TLToolState> {
|
||||
constructor(public editor: Editor) {}
|
||||
|
||||
static type: TLToolState['type']
|
||||
|
||||
// does the default state need to differentiate between private state and sycned state, or is that a detail for the sync stuff?
|
||||
abstract getDefaultContext(): T
|
||||
|
||||
private _context = atom<T>('tool context', {} as T)
|
||||
|
||||
@computed getContext() {
|
||||
return this._context.get()
|
||||
}
|
||||
|
||||
setContext(context: Partial<T>) {
|
||||
this._context.set({ ...this._context.__unsafe__getWithoutCapture(), ...context })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the styles (if any) that are relevant to this tool, and which should be displayed when the tool is active.
|
||||
*/
|
||||
abstract getStyles(): ReadonlySharedStyleMap | null
|
||||
|
||||
/**
|
||||
* A react component to be displayed when the tool is active behind the shapes.
|
||||
*
|
||||
* @param shape - The shape.
|
||||
* @public
|
||||
*/
|
||||
abstract underlay(): ReactNode
|
||||
|
||||
/**
|
||||
* A react component to be displayed when the tool is active in front of the shapes.
|
||||
*
|
||||
* @param shape - The shape.
|
||||
* @public
|
||||
*/
|
||||
abstract overlay(): ReactNode
|
||||
|
||||
/**
|
||||
* Handle an event.
|
||||
*/
|
||||
abstract onEvent(event: TLEventInfo): void
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface TLToolUtilConstructor<T extends TLToolState, U extends ToolUtil<T> = ToolUtil<T>> {
|
||||
new (editor: Editor): U
|
||||
type: T['type']
|
||||
}
|
|
@ -18,31 +18,42 @@ const selectToolStyles: readonly StyleProp<any>[] = Object.freeze([
|
|||
])
|
||||
|
||||
/** @public */
|
||||
export function useRelevantStyles(stylesToCheck = selectToolStyles): ReadonlySharedStyleMap | null {
|
||||
export function useRelevantStyles(
|
||||
stylesToCheck: StyleProp<any>[] = []
|
||||
): ReadonlySharedStyleMap | null {
|
||||
const editor = useEditor()
|
||||
return useValue(
|
||||
'getRelevantStyles',
|
||||
() => {
|
||||
const styles = new SharedStyleMap(editor.getSharedStyles())
|
||||
const isInShapeSpecificTool = !!editor.root.getCurrent()?.shapeType
|
||||
const hasShapesSelected = editor.isIn('select') && editor.getSelectedShapeIds().length > 0
|
||||
const currentTool = editor.getCurrentTool()
|
||||
const currentToolStyles = currentTool.getStyles()
|
||||
|
||||
if (styles.size === 0 && editor.isIn('select') && editor.getSelectedShapeIds().length === 0) {
|
||||
for (const style of stylesToCheck) {
|
||||
styles.applyValue(style, editor.getStyleForNextShape(style))
|
||||
}
|
||||
console.log(stylesToCheck)
|
||||
if (!currentToolStyles) {
|
||||
return new SharedStyleMap(editor.getSharedStyles())
|
||||
}
|
||||
|
||||
if (isInShapeSpecificTool || hasShapesSelected || styles.size > 0) {
|
||||
// If size is 0 we may still want to return an empty styles map to allow
|
||||
// the opacity slider to show up.
|
||||
// This can happen in two situations:
|
||||
// 1. When the user is in the select tool and has multiple shapes selected but they have no supported styles (beyond opacity).
|
||||
// 2. When the user is in a shape-specific tool and the shape has no supported styles (beyond opacity obvs).
|
||||
return styles
|
||||
}
|
||||
return currentToolStyles
|
||||
|
||||
return null
|
||||
// const isInShapeSpecificTool = false
|
||||
// const hasShapesSelected = editor.isIn('select') && editor.getSelectedShapeIds().length > 0
|
||||
|
||||
// if (styles.size === 0 && editor.isIn('select') && editor.getSelectedShapeIds().length === 0) {
|
||||
// for (const style of stylesToCheck) {
|
||||
// styles.applyValue(style, editor.getStyleForNextShape(style))
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (isInShapeSpecificTool || hasShapesSelected || styles.size > 0) {
|
||||
// // If size is 0 we may still want to return an empty styles map to allow
|
||||
// // the opacity slider to show up.
|
||||
// // This can happen in two situations:
|
||||
// // 1. When the user is in the select tool and has multiple shapes selected but they have no supported styles (beyond opacity).
|
||||
// // 2. When the user is in a shape-specific tool and the shape has no supported styles (beyond opacity obvs).
|
||||
// return styles
|
||||
// }
|
||||
|
||||
// return null
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
|
Ładowanie…
Reference in New Issue