kopia lustrzana https://github.com/Tldraw/Tldraw
[1/3] initial highlighter shape/tool (#1401)
This diff adds an initial version of the highlighter shape. At this stage, it's a complete copy of the draw tool minus the following features: * Fills * Stroke types * Closed shapes I've created a new shape util (a copy-paste of the draw one with stuff renamed/deleted) but reused the state chart nodes for the draw shape. Currently this new tool looks exactly like the draw tool, but that'll be changing soon! ![Kapture 2023-05-17 at 15 37 33](https://github.com/tldraw/tldraw/assets/1489520/982e78f4-6495-4a68-aa51-c8f7b5bcdd01) The UI here is extremely WIP. The highlighter tool is behind a feature flag, but once enabled is accessible through the tool bar. There's a first-draft highlighter icon (i didn't spend much time on this, it's not super legible on non-retina displays yet imo), and the tool is bound to the `i` key (any better suggestions? `h` is taken by the hand tool) ### The plan 1. initial highlighter shape/tool #1401 **>you are here<** 2. sandwich rendering for highlighter shapes #1418 3. shape styling - new colours and sizes, lightweight perfect freehand changes ### Change Type - [x] `minor` — New Feature ### Test Plan (not yet) ### Release Notes [internal only change layout ground work for highlighter]pull/1418/head
rodzic
2992ad85d9
commit
674a829d1f
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.5 24.5C8 24 9.3 23 10.5 23C11.3229 23 12.1927 23.1567 12.8195 23.309C13.2406 23.4113 13.6936 23.3064 14 23V23M7.5 24.5L6.5 23.5L5.5 22.5M7.5 24.5L6.5 25.5L3.5 24.5L5.5 22.5M7.5 24.5L5.5 22.5M5.5 22.5C6 22 7 20.7 7 19.5C7 18.6771 6.84326 17.8073 6.69101 17.1805C6.5887 16.7594 6.69357 16.3064 7 16V16M7 16L18.5858 4.41421C19.3668 3.63317 20.6332 3.63317 21.4142 4.41421L25.5858 8.58579C26.3668 9.36684 26.3668 10.6332 25.5858 11.4142L14 23M7 16L14 23" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3.5 24.5L5.5 22.5L7.5 24.5L6.5 25.5L3.5 24.5Z" fill="black"/>
|
||||||
|
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 719 B |
|
@ -180,6 +180,7 @@
|
||||||
"tool.diamond": "Diamond",
|
"tool.diamond": "Diamond",
|
||||||
"tool.ellipse": "Ellipse",
|
"tool.ellipse": "Ellipse",
|
||||||
"tool.hexagon": "Hexagon",
|
"tool.hexagon": "Hexagon",
|
||||||
|
"tool.highlight": "Highlight",
|
||||||
"tool.line": "Line",
|
"tool.line": "Line",
|
||||||
"tool.octagon": "Octagon",
|
"tool.octagon": "Octagon",
|
||||||
"tool.oval": "Oval",
|
"tool.oval": "Oval",
|
||||||
|
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 9.8 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 11 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 9.8 KiB |
|
@ -150,6 +150,7 @@ export function getAssetUrlsByImport(opts?: AssetUrlOptions): {
|
||||||
'tool-eraser': string
|
'tool-eraser': string
|
||||||
'tool-frame': string
|
'tool-frame': string
|
||||||
'tool-hand': string
|
'tool-hand': string
|
||||||
|
'tool-highlight': string
|
||||||
'tool-highlighter': string
|
'tool-highlighter': string
|
||||||
'tool-laser': string
|
'tool-laser': string
|
||||||
'tool-line': string
|
'tool-line': string
|
||||||
|
|
|
@ -161,6 +161,7 @@ import iconsToolEmbed from './icons/icon/tool-embed.svg'
|
||||||
import iconsToolEraser from './icons/icon/tool-eraser.svg'
|
import iconsToolEraser from './icons/icon/tool-eraser.svg'
|
||||||
import iconsToolFrame from './icons/icon/tool-frame.svg'
|
import iconsToolFrame from './icons/icon/tool-frame.svg'
|
||||||
import iconsToolHand from './icons/icon/tool-hand.svg'
|
import iconsToolHand from './icons/icon/tool-hand.svg'
|
||||||
|
import iconsToolHighlight from './icons/icon/tool-highlight.svg'
|
||||||
import iconsToolHighlighter from './icons/icon/tool-highlighter.svg'
|
import iconsToolHighlighter from './icons/icon/tool-highlighter.svg'
|
||||||
import iconsToolLaser from './icons/icon/tool-laser.svg'
|
import iconsToolLaser from './icons/icon/tool-laser.svg'
|
||||||
import iconsToolLine from './icons/icon/tool-line.svg'
|
import iconsToolLine from './icons/icon/tool-line.svg'
|
||||||
|
@ -392,6 +393,7 @@ export function getAssetUrlsByImport(opts) {
|
||||||
'tool-eraser': formatAssetUrl(iconsToolEraser, opts),
|
'tool-eraser': formatAssetUrl(iconsToolEraser, opts),
|
||||||
'tool-frame': formatAssetUrl(iconsToolFrame, opts),
|
'tool-frame': formatAssetUrl(iconsToolFrame, opts),
|
||||||
'tool-hand': formatAssetUrl(iconsToolHand, opts),
|
'tool-hand': formatAssetUrl(iconsToolHand, opts),
|
||||||
|
'tool-highlight': formatAssetUrl(iconsToolHighlight, opts),
|
||||||
'tool-highlighter': formatAssetUrl(iconsToolHighlighter, opts),
|
'tool-highlighter': formatAssetUrl(iconsToolHighlighter, opts),
|
||||||
'tool-laser': formatAssetUrl(iconsToolLaser, opts),
|
'tool-laser': formatAssetUrl(iconsToolLaser, opts),
|
||||||
'tool-line': formatAssetUrl(iconsToolLine, opts),
|
'tool-line': formatAssetUrl(iconsToolLine, opts),
|
||||||
|
|
|
@ -150,6 +150,7 @@ export function getAssetUrlsByMetaUrl(opts?: AssetUrlOptions): {
|
||||||
'tool-eraser': string
|
'tool-eraser': string
|
||||||
'tool-frame': string
|
'tool-frame': string
|
||||||
'tool-hand': string
|
'tool-hand': string
|
||||||
|
'tool-highlight': string
|
||||||
'tool-highlighter': string
|
'tool-highlighter': string
|
||||||
'tool-laser': string
|
'tool-laser': string
|
||||||
'tool-line': string
|
'tool-line': string
|
||||||
|
|
|
@ -494,6 +494,10 @@ export function getAssetUrlsByMetaUrl(opts) {
|
||||||
new URL('./icons/icon/tool-hand.svg', import.meta.url).href,
|
new URL('./icons/icon/tool-hand.svg', import.meta.url).href,
|
||||||
opts
|
opts
|
||||||
),
|
),
|
||||||
|
'tool-highlight': formatAssetUrl(
|
||||||
|
new URL('./icons/icon/tool-highlight.svg', import.meta.url).href,
|
||||||
|
opts
|
||||||
|
),
|
||||||
'tool-highlighter': formatAssetUrl(
|
'tool-highlighter': formatAssetUrl(
|
||||||
new URL('./icons/icon/tool-highlighter.svg', import.meta.url).href,
|
new URL('./icons/icon/tool-highlighter.svg', import.meta.url).href,
|
||||||
opts
|
opts
|
||||||
|
|
|
@ -64,6 +64,7 @@ import { TLFrameShape } from '@tldraw/tlschema';
|
||||||
import { TLGeoShape } from '@tldraw/tlschema';
|
import { TLGeoShape } from '@tldraw/tlschema';
|
||||||
import { TLGroupShape } from '@tldraw/tlschema';
|
import { TLGroupShape } from '@tldraw/tlschema';
|
||||||
import { TLHandle } from '@tldraw/tlschema';
|
import { TLHandle } from '@tldraw/tlschema';
|
||||||
|
import { TLHighlightShape } from '@tldraw/tlschema';
|
||||||
import { TLImageAsset } from '@tldraw/tlschema';
|
import { TLImageAsset } from '@tldraw/tlschema';
|
||||||
import { TLImageShape } from '@tldraw/tlschema';
|
import { TLImageShape } from '@tldraw/tlschema';
|
||||||
import { TLInstance } from '@tldraw/tlschema';
|
import { TLInstance } from '@tldraw/tlschema';
|
||||||
|
@ -725,6 +726,7 @@ export const EVENT_NAME_MAP: Record<Exclude<TLEventName, TLPinchEventName>, keyo
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export const featureFlags: {
|
export const featureFlags: {
|
||||||
peopleMenu: DebugFlag<boolean>;
|
peopleMenu: DebugFlag<boolean>;
|
||||||
|
highlighterTool: DebugFlag<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
|
@ -2204,6 +2206,42 @@ export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
|
||||||
static type: string;
|
static type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export class TLHighlightUtil extends TLShapeUtil<TLHighlightShape> {
|
||||||
|
// (undocumented)
|
||||||
|
defaultProps(): TLHighlightShape['props'];
|
||||||
|
// (undocumented)
|
||||||
|
expandSelectionOutlinePx(shape: TLHighlightShape): number;
|
||||||
|
// (undocumented)
|
||||||
|
getBounds(shape: TLHighlightShape): Box2d;
|
||||||
|
// (undocumented)
|
||||||
|
getCenter(shape: TLHighlightShape): Vec2d;
|
||||||
|
// (undocumented)
|
||||||
|
getOutline(shape: TLHighlightShape): Vec2d[];
|
||||||
|
// (undocumented)
|
||||||
|
hideResizeHandles: (shape: TLHighlightShape) => boolean;
|
||||||
|
// (undocumented)
|
||||||
|
hideRotateHandle: (shape: TLHighlightShape) => boolean;
|
||||||
|
// (undocumented)
|
||||||
|
hideSelectionBoundsBg: (shape: TLHighlightShape) => boolean;
|
||||||
|
// (undocumented)
|
||||||
|
hideSelectionBoundsFg: (shape: TLHighlightShape) => boolean;
|
||||||
|
// (undocumented)
|
||||||
|
hitTestLineSegment(shape: TLHighlightShape, A: VecLike, B: VecLike): boolean;
|
||||||
|
// (undocumented)
|
||||||
|
hitTestPoint(shape: TLHighlightShape, point: VecLike): boolean;
|
||||||
|
// (undocumented)
|
||||||
|
indicator(shape: TLHighlightShape): JSX.Element;
|
||||||
|
// (undocumented)
|
||||||
|
onResize: OnResizeHandler<TLHighlightShape>;
|
||||||
|
// (undocumented)
|
||||||
|
render(shape: TLHighlightShape): JSX.Element;
|
||||||
|
// (undocumented)
|
||||||
|
toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement;
|
||||||
|
// (undocumented)
|
||||||
|
static type: string;
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLHistoryEntry = TLCommand | TLMark;
|
export type TLHistoryEntry = TLCommand | TLMark;
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ export { TLEmbedUtil } from './lib/app/shapeutils/TLEmbedUtil/TLEmbedUtil'
|
||||||
export { TLFrameUtil } from './lib/app/shapeutils/TLFrameUtil/TLFrameUtil'
|
export { TLFrameUtil } from './lib/app/shapeutils/TLFrameUtil/TLFrameUtil'
|
||||||
export { TLGeoUtil } from './lib/app/shapeutils/TLGeoUtil/TLGeoUtil'
|
export { TLGeoUtil } from './lib/app/shapeutils/TLGeoUtil/TLGeoUtil'
|
||||||
export { TLGroupUtil } from './lib/app/shapeutils/TLGroupUtil/TLGroupUtil'
|
export { TLGroupUtil } from './lib/app/shapeutils/TLGroupUtil/TLGroupUtil'
|
||||||
|
export { TLHighlightUtil } from './lib/app/shapeutils/TLHighlightUtil/TLHighlightUtil'
|
||||||
export { TLImageUtil } from './lib/app/shapeutils/TLImageUtil/TLImageUtil'
|
export { TLImageUtil } from './lib/app/shapeutils/TLImageUtil/TLImageUtil'
|
||||||
export { TLLineUtil, getSplineForLineShape } from './lib/app/shapeutils/TLLineUtil/TLLineUtil'
|
export { TLLineUtil, getSplineForLineShape } from './lib/app/shapeutils/TLLineUtil/TLLineUtil'
|
||||||
export { TLNoteUtil } from './lib/app/shapeutils/TLNoteUtil/TLNoteUtil'
|
export { TLNoteUtil } from './lib/app/shapeutils/TLNoteUtil/TLNoteUtil'
|
||||||
|
|
|
@ -4844,6 +4844,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
if (!prev) return null
|
if (!prev) return null
|
||||||
let newRecord = null as null | TLShape
|
let newRecord = null as null | TLShape
|
||||||
for (const [k, v] of Object.entries(partial)) {
|
for (const [k, v] of Object.entries(partial)) {
|
||||||
|
if (v === undefined) continue
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case 'id':
|
case 'id':
|
||||||
case 'type':
|
case 'type':
|
||||||
|
@ -4857,7 +4858,12 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (k === 'props') {
|
if (k === 'props') {
|
||||||
newRecord!.props = { ...prev.props, ...(v as any) }
|
const nextProps = { ...prev.props } as Record<string, unknown>
|
||||||
|
for (const [propKey, propValue] of Object.entries(v as object)) {
|
||||||
|
if (propValue === undefined) continue
|
||||||
|
nextProps[propKey] = propValue
|
||||||
|
}
|
||||||
|
newRecord!.props = nextProps
|
||||||
} else {
|
} else {
|
||||||
;(newRecord as any)[k] = v
|
;(newRecord as any)[k] = v
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
|
||||||
sw += rng(shape.id)() * (strokeWidth / 6)
|
sw += rng(shape.id)() * (strokeWidth / 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = getFreehandOptions(shape, sw, showAsComplete, forceSolid)
|
const options = getFreehandOptions(shape.props, sw, showAsComplete, forceSolid)
|
||||||
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
||||||
|
|
||||||
const solidStrokePath =
|
const solidStrokePath =
|
||||||
|
@ -201,7 +201,7 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
||||||
const options = getFreehandOptions(shape, sw, showAsComplete, true)
|
const options = getFreehandOptions(shape.props, sw, showAsComplete, true)
|
||||||
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
||||||
const solidStrokePath =
|
const solidStrokePath =
|
||||||
strokePoints.length > 1
|
strokePoints.length > 1
|
||||||
|
@ -224,7 +224,7 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
|
||||||
sw += rng(shape.id)() * (strokeWidth / 6)
|
sw += rng(shape.id)() * (strokeWidth / 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = getFreehandOptions(shape, sw, showAsComplete, false)
|
const options = getFreehandOptions(shape.props, sw, showAsComplete, false)
|
||||||
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
||||||
const solidStrokePath =
|
const solidStrokePath =
|
||||||
strokePoints.length > 1
|
strokePoints.length > 1
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { EASINGS, PI, SIN, StrokeOptions, Vec2d } from '@tldraw/primitives'
|
import { EASINGS, PI, SIN, StrokeOptions, Vec2d } from '@tldraw/primitives'
|
||||||
import { TLDrawShape, TLDrawShapeSegment } from '@tldraw/tlschema'
|
import { TLDashType, TLDrawShape, TLDrawShapeSegment } from '@tldraw/tlschema'
|
||||||
|
|
||||||
const PEN_EASING = (t: number) => t * 0.65 + SIN((t * PI) / 2) * 0.35
|
const PEN_EASING = (t: number) => t * 0.65 + SIN((t * PI) / 2) * 0.35
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ const solidSettings = (strokeWidth: number): StrokeOptions => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFreehandOptions(
|
export function getFreehandOptions(
|
||||||
shape: TLDrawShape,
|
shapeProps: { dash: TLDashType; isPen: boolean; isComplete: boolean },
|
||||||
strokeWidth: number,
|
strokeWidth: number,
|
||||||
forceComplete: boolean,
|
forceComplete: boolean,
|
||||||
forceSolid: boolean
|
forceSolid: boolean
|
||||||
|
@ -45,12 +45,12 @@ export function getFreehandOptions(
|
||||||
return {
|
return {
|
||||||
...(forceSolid
|
...(forceSolid
|
||||||
? solidSettings(strokeWidth)
|
? solidSettings(strokeWidth)
|
||||||
: shape.props.dash === 'draw'
|
: shapeProps.dash === 'draw'
|
||||||
? shape.props.isPen
|
? shapeProps.isPen
|
||||||
? realPressureSettings(strokeWidth)
|
? realPressureSettings(strokeWidth)
|
||||||
: simulatePressureSettings(strokeWidth)
|
: simulatePressureSettings(strokeWidth)
|
||||||
: solidSettings(strokeWidth)),
|
: solidSettings(strokeWidth)),
|
||||||
last: shape.props.isComplete || forceComplete,
|
last: shapeProps.isComplete || forceComplete,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
|
import {
|
||||||
|
Box2d,
|
||||||
|
getStrokeOutlinePoints,
|
||||||
|
getStrokePoints,
|
||||||
|
linesIntersect,
|
||||||
|
setStrokePointRadii,
|
||||||
|
Vec2d,
|
||||||
|
VecLike,
|
||||||
|
} from '@tldraw/primitives'
|
||||||
|
import { TLDrawShapeSegment, TLHighlightShape } from '@tldraw/tlschema'
|
||||||
|
import { last, rng } from '@tldraw/utils'
|
||||||
|
import { SVGContainer } from '../../../components/SVGContainer'
|
||||||
|
import { getSvgPathFromStroke, getSvgPathFromStrokePoints } from '../../../utils/svg'
|
||||||
|
import { ShapeFill } from '../shared/ShapeFill'
|
||||||
|
import { TLExportColors } from '../shared/TLExportColors'
|
||||||
|
import { useForceSolid } from '../shared/useForceSolid'
|
||||||
|
import { getFreehandOptions, getPointsFromSegments } from '../TLDrawUtil/getPath'
|
||||||
|
import { OnResizeHandler, TLShapeUtil } from '../TLShapeUtil'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export class TLHighlightUtil extends TLShapeUtil<TLHighlightShape> {
|
||||||
|
static type = 'highlight'
|
||||||
|
|
||||||
|
hideResizeHandles = (shape: TLHighlightShape) => this.getIsDot(shape)
|
||||||
|
hideRotateHandle = (shape: TLHighlightShape) => this.getIsDot(shape)
|
||||||
|
hideSelectionBoundsBg = (shape: TLHighlightShape) => this.getIsDot(shape)
|
||||||
|
hideSelectionBoundsFg = (shape: TLHighlightShape) => this.getIsDot(shape)
|
||||||
|
|
||||||
|
override defaultProps(): TLHighlightShape['props'] {
|
||||||
|
return {
|
||||||
|
segments: [],
|
||||||
|
color: 'black',
|
||||||
|
size: 'm',
|
||||||
|
opacity: '1',
|
||||||
|
isComplete: false,
|
||||||
|
isPen: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIsDot(shape: TLHighlightShape) {
|
||||||
|
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2
|
||||||
|
}
|
||||||
|
|
||||||
|
getBounds(shape: TLHighlightShape) {
|
||||||
|
return Box2d.FromPoints(this.outline(shape))
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutline(shape: TLHighlightShape) {
|
||||||
|
return getPointsFromSegments(shape.props.segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCenter(shape: TLHighlightShape): Vec2d {
|
||||||
|
return this.bounds(shape).center
|
||||||
|
}
|
||||||
|
|
||||||
|
hitTestPoint(shape: TLHighlightShape, point: VecLike): boolean {
|
||||||
|
const outline = this.outline(shape)
|
||||||
|
const zoomLevel = this.app.zoomLevel
|
||||||
|
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||||
|
|
||||||
|
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||||
|
if (shape.props.segments[0].points.some((pt) => Vec2d.Dist(point, pt) < offsetDist * 1.5)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.bounds(shape).containsPoint(point)) {
|
||||||
|
for (let i = 0; i < outline.length; i++) {
|
||||||
|
const C = outline[i]
|
||||||
|
const D = outline[(i + 1) % outline.length]
|
||||||
|
|
||||||
|
if (Vec2d.DistanceToLineSegment(C, D, point) < offsetDist) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
hitTestLineSegment(shape: TLHighlightShape, A: VecLike, B: VecLike): boolean {
|
||||||
|
const outline = this.outline(shape)
|
||||||
|
|
||||||
|
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||||
|
const zoomLevel = this.app.zoomLevel
|
||||||
|
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||||
|
|
||||||
|
if (
|
||||||
|
shape.props.segments[0].points.some(
|
||||||
|
(pt) => Vec2d.DistanceToLineSegment(A, B, pt) < offsetDist * 1.5
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < outline.length - 1; i++) {
|
||||||
|
const C = outline[i]
|
||||||
|
const D = outline[i + 1]
|
||||||
|
if (linesIntersect(A, B, C, D)) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
render(shape: TLHighlightShape) {
|
||||||
|
const forceSolid = useForceSolid()
|
||||||
|
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||||
|
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||||
|
|
||||||
|
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
||||||
|
|
||||||
|
let sw = strokeWidth
|
||||||
|
if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) {
|
||||||
|
sw += rng(shape.id)() * (strokeWidth / 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = getFreehandOptions(
|
||||||
|
{ isComplete: shape.props.isComplete, isPen: shape.props.isPen, dash: 'draw' },
|
||||||
|
sw,
|
||||||
|
showAsComplete,
|
||||||
|
forceSolid
|
||||||
|
)
|
||||||
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
||||||
|
|
||||||
|
const solidStrokePath =
|
||||||
|
strokePoints.length > 1
|
||||||
|
? getSvgPathFromStrokePoints(strokePoints, false)
|
||||||
|
: getDot(allPointsFromSegments[0], sw)
|
||||||
|
|
||||||
|
if (!forceSolid || strokePoints.length < 2) {
|
||||||
|
setStrokePointRadii(strokePoints, options)
|
||||||
|
const strokeOutlinePoints = getStrokeOutlinePoints(strokePoints, options)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SVGContainer id={shape.id}>
|
||||||
|
<ShapeFill fill="none" color={shape.props.color} d={solidStrokePath} />
|
||||||
|
<path
|
||||||
|
d={getSvgPathFromStroke(strokeOutlinePoints, true)}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</SVGContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SVGContainer id={shape.id}>
|
||||||
|
<ShapeFill fill="none" color={shape.props.color} d={solidStrokePath} />
|
||||||
|
<path
|
||||||
|
d={solidStrokePath}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDashoffset="0"
|
||||||
|
/>
|
||||||
|
</SVGContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: TLHighlightShape) {
|
||||||
|
const forceSolid = useForceSolid()
|
||||||
|
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||||
|
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||||
|
|
||||||
|
let sw = strokeWidth
|
||||||
|
if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) {
|
||||||
|
sw += rng(shape.id)() * (strokeWidth / 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
||||||
|
const options = getFreehandOptions(
|
||||||
|
{ dash: 'draw', isComplete: shape.props.isComplete, isPen: shape.props.isPen },
|
||||||
|
sw,
|
||||||
|
showAsComplete,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
||||||
|
const solidStrokePath =
|
||||||
|
strokePoints.length > 1
|
||||||
|
? getSvgPathFromStrokePoints(strokePoints, false)
|
||||||
|
: getDot(allPointsFromSegments[0], sw)
|
||||||
|
|
||||||
|
return <path d={solidStrokePath} />
|
||||||
|
}
|
||||||
|
|
||||||
|
toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors) {
|
||||||
|
const { color } = shape.props
|
||||||
|
|
||||||
|
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||||
|
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||||
|
|
||||||
|
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
||||||
|
|
||||||
|
let sw = strokeWidth
|
||||||
|
if (!shape.props.isPen && allPointsFromSegments.length === 1) {
|
||||||
|
sw += rng(shape.id)() * (strokeWidth / 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = getFreehandOptions(
|
||||||
|
{ dash: 'draw', isComplete: shape.props.isComplete, isPen: shape.props.isPen },
|
||||||
|
sw,
|
||||||
|
showAsComplete,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
||||||
|
|
||||||
|
setStrokePointRadii(strokePoints, options)
|
||||||
|
const strokeOutlinePoints = getStrokeOutlinePoints(strokePoints, options)
|
||||||
|
|
||||||
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||||
|
path.setAttribute('d', getSvgPathFromStroke(strokeOutlinePoints, true))
|
||||||
|
path.setAttribute('fill', colors.fill[color])
|
||||||
|
path.setAttribute('stroke-linecap', 'round')
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
override onResize: OnResizeHandler<TLHighlightShape> = (shape, info) => {
|
||||||
|
const { scaleX, scaleY } = info
|
||||||
|
|
||||||
|
const newSegments: TLDrawShapeSegment[] = []
|
||||||
|
|
||||||
|
for (const segment of shape.props.segments) {
|
||||||
|
newSegments.push({
|
||||||
|
...segment,
|
||||||
|
points: segment.points.map(({ x, y, z }) => {
|
||||||
|
return {
|
||||||
|
x: scaleX * x,
|
||||||
|
y: scaleY * y,
|
||||||
|
z,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
segments: newSegments,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expandSelectionOutlinePx(shape: TLHighlightShape): number {
|
||||||
|
return (this.app.getStrokeWidth(shape.props.size) * 1.6) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDot(point: VecLike, sw: number) {
|
||||||
|
const r = (sw + 1) * 0.5
|
||||||
|
return `M ${point.x} ${point.y} m -${r}, 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${
|
||||||
|
r * 2
|
||||||
|
},0`
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { TLEventHandlers } from '../types/event-types'
|
||||||
import { StateNode } from './StateNode'
|
import { StateNode } from './StateNode'
|
||||||
import { TLArrowTool } from './TLArrowTool/TLArrowTool'
|
import { TLArrowTool } from './TLArrowTool/TLArrowTool'
|
||||||
import { TLDrawTool } from './TLDrawTool/TLDrawTool'
|
import { TLDrawTool } from './TLDrawTool/TLDrawTool'
|
||||||
|
import { TLHighlightTool } from './TLDrawTool/TLHighlightTool'
|
||||||
import { TLEraserTool } from './TLEraserTool/TLEraserTool'
|
import { TLEraserTool } from './TLEraserTool/TLEraserTool'
|
||||||
import { TLFrameTool } from './TLFrameTool/TLFrameTool'
|
import { TLFrameTool } from './TLFrameTool/TLFrameTool'
|
||||||
import { TLGeoTool } from './TLGeoTool/TLGeoTool'
|
import { TLGeoTool } from './TLGeoTool/TLGeoTool'
|
||||||
|
@ -21,6 +22,7 @@ export class RootState extends StateNode {
|
||||||
TLHandTool,
|
TLHandTool,
|
||||||
TLEraserTool,
|
TLEraserTool,
|
||||||
TLDrawTool,
|
TLDrawTool,
|
||||||
|
TLHighlightTool,
|
||||||
TLTextTool,
|
TLTextTool,
|
||||||
TLLineTool,
|
TLLineTool,
|
||||||
TLArrowTool,
|
TLArrowTool,
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { TLStyleType } from '@tldraw/tlschema'
|
||||||
|
import { StateNode } from '../StateNode'
|
||||||
|
|
||||||
|
import { Drawing } from './children/Drawing'
|
||||||
|
import { Idle } from './children/Idle'
|
||||||
|
|
||||||
|
export class TLHighlightTool extends StateNode {
|
||||||
|
static override id = 'highlight'
|
||||||
|
static initial = 'idle'
|
||||||
|
static children = () => [Idle, Drawing]
|
||||||
|
|
||||||
|
styles = ['color', 'opacity', 'size'] as TLStyleType[]
|
||||||
|
|
||||||
|
onExit = () => {
|
||||||
|
const drawingState = this.children!['drawing'] as Drawing
|
||||||
|
drawingState.initialShape = undefined
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import {
|
||||||
createShapeId,
|
createShapeId,
|
||||||
TLDrawShape,
|
TLDrawShape,
|
||||||
TLDrawShapeSegment,
|
TLDrawShapeSegment,
|
||||||
|
TLHighlightShape,
|
||||||
TLSizeType,
|
TLSizeType,
|
||||||
Vec2dModel,
|
Vec2dModel,
|
||||||
} from '@tldraw/tlschema'
|
} from '@tldraw/tlschema'
|
||||||
|
@ -12,16 +13,24 @@ import { uniqueId } from '../../../../utils/data'
|
||||||
import { TLDrawUtil } from '../../../shapeutils/TLDrawUtil/TLDrawUtil'
|
import { TLDrawUtil } from '../../../shapeutils/TLDrawUtil/TLDrawUtil'
|
||||||
import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types'
|
import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types'
|
||||||
|
|
||||||
|
import { TLHighlightUtil } from '../../../shapeutils/TLHighlightUtil/TLHighlightUtil'
|
||||||
import { StateNode } from '../../StateNode'
|
import { StateNode } from '../../StateNode'
|
||||||
|
|
||||||
|
type DrawableShape = TLDrawShape | TLHighlightShape
|
||||||
|
|
||||||
export class Drawing extends StateNode {
|
export class Drawing extends StateNode {
|
||||||
static override id = 'drawing'
|
static override id = 'drawing'
|
||||||
|
|
||||||
info = {} as TLPointerEventInfo
|
info = {} as TLPointerEventInfo
|
||||||
|
|
||||||
initialShape?: TLDrawShape
|
initialShape?: DrawableShape
|
||||||
|
|
||||||
util = this.app.getShapeUtil(TLDrawUtil)
|
shapeType: 'draw' | 'highlight' = this.parent.id === 'highlight' ? 'highlight' : 'draw'
|
||||||
|
|
||||||
|
util =
|
||||||
|
this.shapeType === 'highlight'
|
||||||
|
? this.app.getShapeUtil(TLHighlightUtil)
|
||||||
|
: this.app.getShapeUtil(TLDrawUtil)
|
||||||
|
|
||||||
isPen = false
|
isPen = false
|
||||||
|
|
||||||
|
@ -127,7 +136,13 @@ export class Drawing extends StateNode {
|
||||||
this.pagePointWhereCurrentSegmentChanged = this.app.inputs.currentPagePoint.clone()
|
this.pagePointWhereCurrentSegmentChanged = this.app.inputs.currentPagePoint.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canClose() {
|
||||||
|
return this.shapeType !== 'highlight'
|
||||||
|
}
|
||||||
|
|
||||||
getIsClosed(segments: TLDrawShapeSegment[], size: TLSizeType) {
|
getIsClosed(segments: TLDrawShapeSegment[], size: TLSizeType) {
|
||||||
|
if (!this.canClose()) return false
|
||||||
|
|
||||||
const strokeWidth = this.app.getStrokeWidth(size)
|
const strokeWidth = this.app.getStrokeWidth(size)
|
||||||
const firstPoint = segments[0].points[0]
|
const firstPoint = segments[0].points[0]
|
||||||
const lastSegment = segments[segments.length - 1]
|
const lastSegment = segments[segments.length - 1]
|
||||||
|
@ -158,7 +173,7 @@ export class Drawing extends StateNode {
|
||||||
this.lastRecordedPoint = originPagePoint.clone()
|
this.lastRecordedPoint = originPagePoint.clone()
|
||||||
|
|
||||||
if (this.initialShape) {
|
if (this.initialShape) {
|
||||||
const shape = this.app.getShapeById<TLDrawShape>(this.initialShape.id)
|
const shape = this.app.getShapeById<DrawableShape>(this.initialShape.id)
|
||||||
|
|
||||||
if (shape && this.segmentMode === 'straight') {
|
if (shape && this.segmentMode === 'straight') {
|
||||||
// Connect dots
|
// Connect dots
|
||||||
|
@ -204,10 +219,10 @@ export class Drawing extends StateNode {
|
||||||
this.app.updateShapes([
|
this.app.updateShapes([
|
||||||
{
|
{
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: 'draw',
|
type: this.shapeType,
|
||||||
props: {
|
props: {
|
||||||
segments,
|
segments,
|
||||||
isClosed: this.getIsClosed(segments, shape.props.size),
|
isClosed: this.canClose() ? this.getIsClosed(segments, shape.props.size) : undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
@ -223,7 +238,7 @@ export class Drawing extends StateNode {
|
||||||
this.app.createShapes([
|
this.app.createShapes([
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'draw',
|
type: this.shapeType,
|
||||||
x: originPagePoint.x,
|
x: originPagePoint.x,
|
||||||
y: originPagePoint.y,
|
y: originPagePoint.y,
|
||||||
props: {
|
props: {
|
||||||
|
@ -245,7 +260,7 @@ export class Drawing extends StateNode {
|
||||||
])
|
])
|
||||||
|
|
||||||
this.currentLineLength = 0
|
this.currentLineLength = 0
|
||||||
this.initialShape = this.app.getShapeById<TLDrawShape>(id)
|
this.initialShape = this.app.getShapeById<DrawableShape>(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateShapes() {
|
private updateShapes() {
|
||||||
|
@ -259,7 +274,7 @@ export class Drawing extends StateNode {
|
||||||
props: { size },
|
props: { size },
|
||||||
} = initialShape
|
} = initialShape
|
||||||
|
|
||||||
const shape = this.app.getShapeById<TLDrawShape>(id)!
|
const shape = this.app.getShapeById<DrawableShape>(id)!
|
||||||
|
|
||||||
if (!shape) return
|
if (!shape) return
|
||||||
|
|
||||||
|
@ -329,10 +344,10 @@ export class Drawing extends StateNode {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'draw',
|
type: this.shapeType,
|
||||||
props: {
|
props: {
|
||||||
segments: [...segments, newSegment],
|
segments: [...segments, newSegment],
|
||||||
isClosed: this.getIsClosed(segments, size),
|
isClosed: this.canClose() ? this.getIsClosed(segments, size) : undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -386,10 +401,10 @@ export class Drawing extends StateNode {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'draw',
|
type: this.shapeType,
|
||||||
props: {
|
props: {
|
||||||
segments: finalSegments,
|
segments: finalSegments,
|
||||||
isClosed: this.getIsClosed(finalSegments, size),
|
isClosed: this.canClose() ? this.getIsClosed(finalSegments, size) : undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -525,10 +540,10 @@ export class Drawing extends StateNode {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'draw',
|
type: this.shapeType,
|
||||||
props: {
|
props: {
|
||||||
segments: newSegments,
|
segments: newSegments,
|
||||||
isClosed: this.getIsClosed(segments, size),
|
isClosed: this.canClose() ? this.getIsClosed(segments, size) : undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -567,10 +582,10 @@ export class Drawing extends StateNode {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: 'draw',
|
type: this.shapeType,
|
||||||
props: {
|
props: {
|
||||||
segments: newSegments,
|
segments: newSegments,
|
||||||
isClosed: this.getIsClosed(segments, size),
|
isClosed: this.canClose() ? this.getIsClosed(segments, size) : undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -579,7 +594,7 @@ export class Drawing extends StateNode {
|
||||||
|
|
||||||
// Set a maximum length for the lines array; after 200 points, complete the line.
|
// Set a maximum length for the lines array; after 200 points, complete the line.
|
||||||
if (newPoints.length > 500) {
|
if (newPoints.length > 500) {
|
||||||
this.app.updateShapes([{ id, type: 'draw', props: { isComplete: true } }])
|
this.app.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }])
|
||||||
|
|
||||||
const { currentPagePoint } = this.app.inputs
|
const { currentPagePoint } = this.app.inputs
|
||||||
|
|
||||||
|
@ -588,7 +603,7 @@ export class Drawing extends StateNode {
|
||||||
this.app.createShapes([
|
this.app.createShapes([
|
||||||
{
|
{
|
||||||
id: newShapeId,
|
id: newShapeId,
|
||||||
type: 'draw',
|
type: this.shapeType,
|
||||||
x: currentPagePoint.x,
|
x: currentPagePoint.x,
|
||||||
y: currentPagePoint.y,
|
y: currentPagePoint.y,
|
||||||
props: {
|
props: {
|
||||||
|
@ -603,7 +618,7 @@ export class Drawing extends StateNode {
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
this.initialShape = structuredClone(this.app.getShapeById<TLDrawShape>(newShapeId)!)
|
this.initialShape = structuredClone(this.app.getShapeById<DrawableShape>(newShapeId)!)
|
||||||
this.mergeNextPoint = false
|
this.mergeNextPoint = false
|
||||||
this.lastRecordedPoint = this.app.inputs.currentPagePoint.clone()
|
this.lastRecordedPoint = this.app.inputs.currentPagePoint.clone()
|
||||||
this.currentLineLength = 0
|
this.currentLineLength = 0
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { TLEmbedUtil } from '../app/shapeutils/TLEmbedUtil/TLEmbedUtil'
|
||||||
import { TLFrameUtil } from '../app/shapeutils/TLFrameUtil/TLFrameUtil'
|
import { TLFrameUtil } from '../app/shapeutils/TLFrameUtil/TLFrameUtil'
|
||||||
import { TLGeoUtil } from '../app/shapeutils/TLGeoUtil/TLGeoUtil'
|
import { TLGeoUtil } from '../app/shapeutils/TLGeoUtil/TLGeoUtil'
|
||||||
import { TLGroupUtil } from '../app/shapeutils/TLGroupUtil/TLGroupUtil'
|
import { TLGroupUtil } from '../app/shapeutils/TLGroupUtil/TLGroupUtil'
|
||||||
|
import { TLHighlightUtil } from '../app/shapeutils/TLHighlightUtil/TLHighlightUtil'
|
||||||
import { TLImageUtil } from '../app/shapeutils/TLImageUtil/TLImageUtil'
|
import { TLImageUtil } from '../app/shapeutils/TLImageUtil/TLImageUtil'
|
||||||
import { TLLineUtil } from '../app/shapeutils/TLLineUtil/TLLineUtil'
|
import { TLLineUtil } from '../app/shapeutils/TLLineUtil/TLLineUtil'
|
||||||
import { TLNoteUtil } from '../app/shapeutils/TLNoteUtil/TLNoteUtil'
|
import { TLNoteUtil } from '../app/shapeutils/TLNoteUtil/TLNoteUtil'
|
||||||
|
@ -47,6 +48,7 @@ const DEFAULT_SHAPE_UTILS: {
|
||||||
note: TLNoteUtil,
|
note: TLNoteUtil,
|
||||||
text: TLTextUtil,
|
text: TLTextUtil,
|
||||||
video: TLVideoUtil,
|
video: TLVideoUtil,
|
||||||
|
highlight: TLHighlightUtil,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLDrawShape } from '@tldraw/tlschema'
|
import { TLDrawShape, TLHighlightShape } from '@tldraw/tlschema'
|
||||||
import { last } from '@tldraw/utils'
|
import { last } from '@tldraw/utils'
|
||||||
import { TestApp } from '../TestApp'
|
import { TestApp } from '../TestApp'
|
||||||
|
|
||||||
|
@ -16,237 +16,248 @@ beforeEach(() => {
|
||||||
app.createShapes([])
|
app.createShapes([])
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When drawing...', () => {
|
type DrawableShape = TLDrawShape | TLHighlightShape
|
||||||
it('Creates a dot', () => {
|
|
||||||
app
|
|
||||||
.setSelectedTool('draw')
|
|
||||||
.pointerDown(60, 60)
|
|
||||||
.expectToBeIn('draw.drawing')
|
|
||||||
.pointerUp()
|
|
||||||
.expectToBeIn('draw.idle')
|
|
||||||
|
|
||||||
expect(app.shapesArray).toHaveLength(1)
|
for (const toolType of ['draw', 'highlight'] as const) {
|
||||||
|
describe(`When ${toolType}ing...`, () => {
|
||||||
|
it('Creates a dot', () => {
|
||||||
|
app
|
||||||
|
.setSelectedTool(toolType)
|
||||||
|
.pointerDown(60, 60)
|
||||||
|
.expectToBeIn(`${toolType}.drawing`)
|
||||||
|
.pointerUp()
|
||||||
|
.expectToBeIn(`${toolType}.idle`)
|
||||||
|
|
||||||
const shape = app.shapesArray[0] as TLDrawShape
|
expect(app.shapesArray).toHaveLength(1)
|
||||||
expect(shape.props.segments.length).toBe(1)
|
|
||||||
|
|
||||||
const segment = shape.props.segments[0]
|
const shape = app.shapesArray[0] as DrawableShape
|
||||||
expect(segment.type).toBe('free')
|
expect(shape.type).toBe(toolType)
|
||||||
|
expect(shape.props.segments.length).toBe(1)
|
||||||
|
|
||||||
|
const segment = shape.props.segments[0]
|
||||||
|
expect(segment.type).toBe('free')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Creates a dot when shift is held down', () => {
|
||||||
|
app
|
||||||
|
.setSelectedTool(toolType)
|
||||||
|
.keyDown('Shift')
|
||||||
|
.pointerDown(60, 60)
|
||||||
|
.expectToBeIn(`${toolType}.drawing`)
|
||||||
|
.pointerUp()
|
||||||
|
.expectToBeIn(`${toolType}.idle`)
|
||||||
|
|
||||||
|
expect(app.shapesArray).toHaveLength(1)
|
||||||
|
|
||||||
|
const shape = app.shapesArray[0] as DrawableShape
|
||||||
|
expect(shape.type).toBe(toolType)
|
||||||
|
expect(shape.props.segments.length).toBe(1)
|
||||||
|
|
||||||
|
const segment = shape.props.segments[0]
|
||||||
|
expect(segment.type).toBe('straight')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Creates a free draw line when shift is not held', () => {
|
||||||
|
app.setSelectedTool(toolType).pointerDown(10, 10).pointerMove(20, 20)
|
||||||
|
|
||||||
|
const shape = app.shapesArray[0] as DrawableShape
|
||||||
|
expect(shape.props.segments.length).toBe(1)
|
||||||
|
|
||||||
|
const segment = shape.props.segments[0]
|
||||||
|
expect(segment.type).toBe('free')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Creates a straight line when shift is held', () => {
|
||||||
|
app.setSelectedTool(toolType).keyDown('Shift').pointerDown(10, 10).pointerMove(20, 20)
|
||||||
|
|
||||||
|
const shape = app.shapesArray[0] as DrawableShape
|
||||||
|
expect(shape.props.segments.length).toBe(1)
|
||||||
|
|
||||||
|
const segment = shape.props.segments[0]
|
||||||
|
expect(segment.type).toBe('straight')
|
||||||
|
|
||||||
|
const points = segment.points
|
||||||
|
expect(points.length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Switches between segment types when shift is pressed / released (starting with shift up)', () => {
|
||||||
|
app
|
||||||
|
.setSelectedTool(toolType)
|
||||||
|
.pointerDown(10, 10)
|
||||||
|
.pointerMove(20, 20)
|
||||||
|
.keyDown('Shift')
|
||||||
|
.pointerMove(30, 30)
|
||||||
|
.keyUp('Shift')
|
||||||
|
.pointerMove(40, 40)
|
||||||
|
.pointerUp()
|
||||||
|
|
||||||
|
const shape = app.shapesArray[0] as DrawableShape
|
||||||
|
expect(shape.props.segments.length).toBe(3)
|
||||||
|
|
||||||
|
expect(shape.props.segments[0].type).toBe('free')
|
||||||
|
expect(shape.props.segments[1].type).toBe('straight')
|
||||||
|
expect(shape.props.segments[2].type).toBe('free')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Switches between segment types when shift is pressed / released (starting with shift down)', () => {
|
||||||
|
app
|
||||||
|
.setSelectedTool(toolType)
|
||||||
|
.keyDown('Shift')
|
||||||
|
.pointerDown(10, 10)
|
||||||
|
.pointerMove(20, 20)
|
||||||
|
.keyUp('Shift')
|
||||||
|
.pointerMove(30, 30)
|
||||||
|
.keyDown('Shift')
|
||||||
|
.pointerMove(40, 40)
|
||||||
|
.pointerUp()
|
||||||
|
|
||||||
|
const shape = app.shapesArray[0] as DrawableShape
|
||||||
|
expect(shape.props.segments.length).toBe(3)
|
||||||
|
|
||||||
|
expect(shape.props.segments[0].type).toBe('straight')
|
||||||
|
expect(shape.props.segments[1].type).toBe('free')
|
||||||
|
expect(shape.props.segments[2].type).toBe('straight')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Extends previously drawn line when shift is held', () => {
|
||||||
|
app
|
||||||
|
.setSelectedTool(toolType)
|
||||||
|
.keyDown('Shift')
|
||||||
|
.pointerDown(10, 10)
|
||||||
|
.pointerUp()
|
||||||
|
.pointerDown(20, 20)
|
||||||
|
|
||||||
|
const shape1 = app.shapesArray[0] as DrawableShape
|
||||||
|
expect(shape1.props.segments.length).toBe(2)
|
||||||
|
expect(shape1.props.segments[0].type).toBe('straight')
|
||||||
|
expect(shape1.props.segments[1].type).toBe('straight')
|
||||||
|
|
||||||
|
app.pointerUp().pointerDown(30, 30).pointerUp()
|
||||||
|
|
||||||
|
const shape2 = app.shapesArray[0] as DrawableShape
|
||||||
|
expect(shape2.props.segments.length).toBe(3)
|
||||||
|
expect(shape2.props.segments[2].type).toBe('straight')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Does not extends previously drawn line after switching to another tool', () => {
|
||||||
|
app
|
||||||
|
.setSelectedTool(toolType)
|
||||||
|
.pointerDown(10, 10)
|
||||||
|
.pointerUp()
|
||||||
|
.setSelectedTool('select')
|
||||||
|
.setSelectedTool(toolType)
|
||||||
|
.keyDown('Shift')
|
||||||
|
.pointerDown(20, 20)
|
||||||
|
.pointerMove(30, 30)
|
||||||
|
|
||||||
|
expect(app.shapesArray).toHaveLength(2)
|
||||||
|
|
||||||
|
const shape1 = app.shapesArray[0] as DrawableShape
|
||||||
|
expect(shape1.props.segments.length).toBe(1)
|
||||||
|
expect(shape1.props.segments[0].type).toBe('free')
|
||||||
|
|
||||||
|
const shape2 = app.shapesArray[1] as DrawableShape
|
||||||
|
expect(shape2.props.segments.length).toBe(1)
|
||||||
|
expect(shape2.props.segments[0].type).toBe('straight')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Snaps to 15 degree angle when shift is held', () => {
|
||||||
|
const magnitude = 10
|
||||||
|
const angle = (17 * Math.PI) / 180
|
||||||
|
const x = magnitude * Math.cos(angle)
|
||||||
|
const y = magnitude * Math.sin(angle)
|
||||||
|
|
||||||
|
const snappedAngle = (15 * Math.PI) / 180
|
||||||
|
const snappedX = magnitude * Math.cos(snappedAngle)
|
||||||
|
const snappedY = magnitude * Math.sin(snappedAngle)
|
||||||
|
|
||||||
|
app.setSelectedTool(toolType).keyDown('Shift').pointerDown(0, 0).pointerMove(x, y)
|
||||||
|
|
||||||
|
const shape = app.shapesArray[0] as DrawableShape
|
||||||
|
const segment = shape.props.segments[0]
|
||||||
|
expect(segment.points[1].x).toBeCloseTo(snappedX)
|
||||||
|
expect(segment.points[1].y).toBeCloseTo(snappedY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Doesnt snap to 15 degree angle when cmd is held', () => {
|
||||||
|
const magnitude = 10
|
||||||
|
const angle = (17 * Math.PI) / 180
|
||||||
|
const x = magnitude * Math.cos(angle)
|
||||||
|
const y = magnitude * Math.sin(angle)
|
||||||
|
|
||||||
|
app.setSelectedTool(toolType).keyDown('Meta').pointerDown(0, 0).pointerMove(x, y)
|
||||||
|
|
||||||
|
const shape = app.shapesArray[0] as DrawableShape
|
||||||
|
const segment = shape.props.segments[0]
|
||||||
|
expect(segment.points[1].x).toBeCloseTo(x)
|
||||||
|
expect(segment.points[1].y).toBeCloseTo(y)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Snaps to start or end of straight segments in self when shift + cmd is held', () => {
|
||||||
|
app
|
||||||
|
.setSelectedTool(toolType)
|
||||||
|
.keyDown('Shift')
|
||||||
|
.pointerDown(0, 0)
|
||||||
|
.pointerUp()
|
||||||
|
.pointerDown(0, 10)
|
||||||
|
.pointerUp()
|
||||||
|
.pointerDown(10, 0)
|
||||||
|
.pointerUp()
|
||||||
|
.pointerDown(10, 0)
|
||||||
|
.pointerMove(1, 0)
|
||||||
|
|
||||||
|
const shape1 = app.shapesArray[0] as DrawableShape
|
||||||
|
const segment1 = last(shape1.props.segments)!
|
||||||
|
const point1 = last(segment1.points)!
|
||||||
|
expect(point1.x).toBe(1)
|
||||||
|
|
||||||
|
app.keyDown('Meta')
|
||||||
|
const shape2 = app.shapesArray[0] as DrawableShape
|
||||||
|
const segment2 = last(shape2.props.segments)!
|
||||||
|
const point2 = last(segment2.points)!
|
||||||
|
expect(point2.x).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Snaps to position along straight segments in self when shift + cmd is held', () => {
|
||||||
|
app
|
||||||
|
.setSelectedTool(toolType)
|
||||||
|
.keyDown('Shift')
|
||||||
|
.pointerDown(0, 0)
|
||||||
|
.pointerUp()
|
||||||
|
.pointerDown(0, 10)
|
||||||
|
.pointerUp()
|
||||||
|
.pointerDown(10, 5)
|
||||||
|
.pointerUp()
|
||||||
|
.pointerDown(10, 5)
|
||||||
|
.pointerMove(1, 5)
|
||||||
|
|
||||||
|
const shape1 = app.shapesArray[0] as DrawableShape
|
||||||
|
const segment1 = last(shape1.props.segments)!
|
||||||
|
const point1 = last(segment1.points)!
|
||||||
|
expect(point1.x).toBe(1)
|
||||||
|
|
||||||
|
app.keyDown('Meta')
|
||||||
|
const shape2 = app.shapesArray[0] as DrawableShape
|
||||||
|
const segment2 = last(shape2.props.segments)!
|
||||||
|
const point2 = last(segment2.points)!
|
||||||
|
expect(point2.x).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Deletes very short lines on interrupt', () => {
|
||||||
|
app.setSelectedTool(toolType).pointerDown(0, 0).pointerMove(0.1, 0.1).interrupt()
|
||||||
|
expect(app.shapesArray).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Does not delete longer lines on interrupt', () => {
|
||||||
|
app.setSelectedTool(toolType).pointerDown(0, 0).pointerMove(5, 5).interrupt()
|
||||||
|
expect(app.shapesArray).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Completes on cancel', () => {
|
||||||
|
app.setSelectedTool(toolType).pointerDown(0, 0).pointerMove(5, 5).cancel()
|
||||||
|
expect(app.shapesArray).toHaveLength(1)
|
||||||
|
const shape = app.shapesArray[0] as DrawableShape
|
||||||
|
expect(shape.props.segments.length).toBe(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
}
|
||||||
it('Creates a dot when shift is held down', () => {
|
|
||||||
app
|
|
||||||
.setSelectedTool('draw')
|
|
||||||
.keyDown('Shift')
|
|
||||||
.pointerDown(60, 60)
|
|
||||||
.expectToBeIn('draw.drawing')
|
|
||||||
.pointerUp()
|
|
||||||
.expectToBeIn('draw.idle')
|
|
||||||
|
|
||||||
expect(app.shapesArray).toHaveLength(1)
|
|
||||||
|
|
||||||
const shape = app.shapesArray[0] as TLDrawShape
|
|
||||||
expect(shape.props.segments.length).toBe(1)
|
|
||||||
|
|
||||||
const segment = shape.props.segments[0]
|
|
||||||
expect(segment.type).toBe('straight')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Creates a free draw line when shift is not held', () => {
|
|
||||||
app.setSelectedTool('draw').pointerDown(10, 10).pointerMove(20, 20)
|
|
||||||
|
|
||||||
const shape = app.shapesArray[0] as TLDrawShape
|
|
||||||
expect(shape.props.segments.length).toBe(1)
|
|
||||||
|
|
||||||
const segment = shape.props.segments[0]
|
|
||||||
expect(segment.type).toBe('free')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Creates a straight line when shift is held', () => {
|
|
||||||
app.setSelectedTool('draw').keyDown('Shift').pointerDown(10, 10).pointerMove(20, 20)
|
|
||||||
|
|
||||||
const shape = app.shapesArray[0] as TLDrawShape
|
|
||||||
expect(shape.props.segments.length).toBe(1)
|
|
||||||
|
|
||||||
const segment = shape.props.segments[0]
|
|
||||||
expect(segment.type).toBe('straight')
|
|
||||||
|
|
||||||
const points = segment.points
|
|
||||||
expect(points.length).toBe(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Switches between segment types when shift is pressed / released (starting with shift up)', () => {
|
|
||||||
app
|
|
||||||
.setSelectedTool('draw')
|
|
||||||
.pointerDown(10, 10)
|
|
||||||
.pointerMove(20, 20)
|
|
||||||
.keyDown('Shift')
|
|
||||||
.pointerMove(30, 30)
|
|
||||||
.keyUp('Shift')
|
|
||||||
.pointerMove(40, 40)
|
|
||||||
.pointerUp()
|
|
||||||
|
|
||||||
const shape = app.shapesArray[0] as TLDrawShape
|
|
||||||
expect(shape.props.segments.length).toBe(3)
|
|
||||||
|
|
||||||
expect(shape.props.segments[0].type).toBe('free')
|
|
||||||
expect(shape.props.segments[1].type).toBe('straight')
|
|
||||||
expect(shape.props.segments[2].type).toBe('free')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Switches between segment types when shift is pressed / released (starting with shift down)', () => {
|
|
||||||
app
|
|
||||||
.setSelectedTool('draw')
|
|
||||||
.keyDown('Shift')
|
|
||||||
.pointerDown(10, 10)
|
|
||||||
.pointerMove(20, 20)
|
|
||||||
.keyUp('Shift')
|
|
||||||
.pointerMove(30, 30)
|
|
||||||
.keyDown('Shift')
|
|
||||||
.pointerMove(40, 40)
|
|
||||||
.pointerUp()
|
|
||||||
|
|
||||||
const shape = app.shapesArray[0] as TLDrawShape
|
|
||||||
expect(shape.props.segments.length).toBe(3)
|
|
||||||
|
|
||||||
expect(shape.props.segments[0].type).toBe('straight')
|
|
||||||
expect(shape.props.segments[1].type).toBe('free')
|
|
||||||
expect(shape.props.segments[2].type).toBe('straight')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Extends previously drawn line when shift is held', () => {
|
|
||||||
app.setSelectedTool('draw').keyDown('Shift').pointerDown(10, 10).pointerUp().pointerDown(20, 20)
|
|
||||||
|
|
||||||
const shape1 = app.shapesArray[0] as TLDrawShape
|
|
||||||
expect(shape1.props.segments.length).toBe(2)
|
|
||||||
expect(shape1.props.segments[0].type).toBe('straight')
|
|
||||||
expect(shape1.props.segments[1].type).toBe('straight')
|
|
||||||
|
|
||||||
app.pointerUp().pointerDown(30, 30).pointerUp()
|
|
||||||
|
|
||||||
const shape2 = app.shapesArray[0] as TLDrawShape
|
|
||||||
expect(shape2.props.segments.length).toBe(3)
|
|
||||||
expect(shape2.props.segments[2].type).toBe('straight')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Does not extends previously drawn line after switching to another tool', () => {
|
|
||||||
app
|
|
||||||
.setSelectedTool('draw')
|
|
||||||
.pointerDown(10, 10)
|
|
||||||
.pointerUp()
|
|
||||||
.setSelectedTool('select')
|
|
||||||
.setSelectedTool('draw')
|
|
||||||
.keyDown('Shift')
|
|
||||||
.pointerDown(20, 20)
|
|
||||||
.pointerMove(30, 30)
|
|
||||||
|
|
||||||
expect(app.shapesArray).toHaveLength(2)
|
|
||||||
|
|
||||||
const shape1 = app.shapesArray[0] as TLDrawShape
|
|
||||||
expect(shape1.props.segments.length).toBe(1)
|
|
||||||
expect(shape1.props.segments[0].type).toBe('free')
|
|
||||||
|
|
||||||
const shape2 = app.shapesArray[1] as TLDrawShape
|
|
||||||
expect(shape2.props.segments.length).toBe(1)
|
|
||||||
expect(shape2.props.segments[0].type).toBe('straight')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Snaps to 15 degree angle when shift is held', () => {
|
|
||||||
const magnitude = 10
|
|
||||||
const angle = (17 * Math.PI) / 180
|
|
||||||
const x = magnitude * Math.cos(angle)
|
|
||||||
const y = magnitude * Math.sin(angle)
|
|
||||||
|
|
||||||
const snappedAngle = (15 * Math.PI) / 180
|
|
||||||
const snappedX = magnitude * Math.cos(snappedAngle)
|
|
||||||
const snappedY = magnitude * Math.sin(snappedAngle)
|
|
||||||
|
|
||||||
app.setSelectedTool('draw').keyDown('Shift').pointerDown(0, 0).pointerMove(x, y)
|
|
||||||
|
|
||||||
const shape = app.shapesArray[0] as TLDrawShape
|
|
||||||
const segment = shape.props.segments[0]
|
|
||||||
expect(segment.points[1].x).toBeCloseTo(snappedX)
|
|
||||||
expect(segment.points[1].y).toBeCloseTo(snappedY)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Doesnt snap to 15 degree angle when cmd is held', () => {
|
|
||||||
const magnitude = 10
|
|
||||||
const angle = (17 * Math.PI) / 180
|
|
||||||
const x = magnitude * Math.cos(angle)
|
|
||||||
const y = magnitude * Math.sin(angle)
|
|
||||||
|
|
||||||
app.setSelectedTool('draw').keyDown('Meta').pointerDown(0, 0).pointerMove(x, y)
|
|
||||||
|
|
||||||
const shape = app.shapesArray[0] as TLDrawShape
|
|
||||||
const segment = shape.props.segments[0]
|
|
||||||
expect(segment.points[1].x).toBeCloseTo(x)
|
|
||||||
expect(segment.points[1].y).toBeCloseTo(y)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Snaps to start or end of straight segments in self when shift + cmd is held', () => {
|
|
||||||
app
|
|
||||||
.setSelectedTool('draw')
|
|
||||||
.keyDown('Shift')
|
|
||||||
.pointerDown(0, 0)
|
|
||||||
.pointerUp()
|
|
||||||
.pointerDown(0, 10)
|
|
||||||
.pointerUp()
|
|
||||||
.pointerDown(10, 0)
|
|
||||||
.pointerUp()
|
|
||||||
.pointerDown(10, 0)
|
|
||||||
.pointerMove(1, 0)
|
|
||||||
|
|
||||||
const shape1 = app.shapesArray[0] as TLDrawShape
|
|
||||||
const segment1 = last(shape1.props.segments)!
|
|
||||||
const point1 = last(segment1.points)!
|
|
||||||
expect(point1.x).toBe(1)
|
|
||||||
|
|
||||||
app.keyDown('Meta')
|
|
||||||
const shape2 = app.shapesArray[0] as TLDrawShape
|
|
||||||
const segment2 = last(shape2.props.segments)!
|
|
||||||
const point2 = last(segment2.points)!
|
|
||||||
expect(point2.x).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Snaps to position along straight segments in self when shift + cmd is held', () => {
|
|
||||||
app
|
|
||||||
.setSelectedTool('draw')
|
|
||||||
.keyDown('Shift')
|
|
||||||
.pointerDown(0, 0)
|
|
||||||
.pointerUp()
|
|
||||||
.pointerDown(0, 10)
|
|
||||||
.pointerUp()
|
|
||||||
.pointerDown(10, 5)
|
|
||||||
.pointerUp()
|
|
||||||
.pointerDown(10, 5)
|
|
||||||
.pointerMove(1, 5)
|
|
||||||
|
|
||||||
const shape1 = app.shapesArray[0] as TLDrawShape
|
|
||||||
const segment1 = last(shape1.props.segments)!
|
|
||||||
const point1 = last(segment1.points)!
|
|
||||||
expect(point1.x).toBe(1)
|
|
||||||
|
|
||||||
app.keyDown('Meta')
|
|
||||||
const shape2 = app.shapesArray[0] as TLDrawShape
|
|
||||||
const segment2 = last(shape2.props.segments)!
|
|
||||||
const point2 = last(segment2.points)!
|
|
||||||
expect(point2.x).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Deletes very short lines on interrupt', () => {
|
|
||||||
app.setSelectedTool('draw').pointerDown(0, 0).pointerMove(0.1, 0.1).interrupt()
|
|
||||||
expect(app.shapesArray).toHaveLength(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Does not delete longer lines on interrupt', () => {
|
|
||||||
app.setSelectedTool('draw').pointerDown(0, 0).pointerMove(5, 5).interrupt()
|
|
||||||
expect(app.shapesArray).toHaveLength(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Completes on cancel', () => {
|
|
||||||
app.setSelectedTool('draw').pointerDown(0, 0).pointerMove(5, 5).cancel()
|
|
||||||
expect(app.shapesArray).toHaveLength(1)
|
|
||||||
const shape = app.shapesArray[0] as TLDrawShape
|
|
||||||
expect(shape.props.segments.length).toBe(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const featureFlags = {
|
||||||
// todo: remove this. it's not used, but we only have one feature flag and i
|
// todo: remove this. it's not used, but we only have one feature flag and i
|
||||||
// wanted an example :(
|
// wanted an example :(
|
||||||
peopleMenu: createFeatureFlag('peopleMenu'),
|
peopleMenu: createFeatureFlag('peopleMenu'),
|
||||||
|
highlighterTool: createFeatureFlag('highlighterTool'),
|
||||||
} satisfies Record<string, DebugFlag<boolean>>
|
} satisfies Record<string, DebugFlag<boolean>>
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
|
|
@ -375,6 +375,12 @@ export const groupShapeTypeValidator: T.Validator<TLGroupShape>;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const handleTypeValidator: T.Validator<TLHandle>;
|
export const handleTypeValidator: T.Validator<TLHandle>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const highlightShapeMigrations: Migrations;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const highlightShapeTypeValidator: T.Validator<TLHighlightShape>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const iconShapeTypeMigrations: Migrations;
|
export const iconShapeTypeMigrations: Migrations;
|
||||||
|
|
||||||
|
@ -760,7 +766,7 @@ export interface TLDashStyle extends TLBaseStyle {
|
||||||
export type TLDashType = SetValue<typeof TL_DASH_TYPES>;
|
export type TLDashType = SetValue<typeof TL_DASH_TYPES>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export type TLDefaultShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLIconShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLVideoShape;
|
export type TLDefaultShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLHighlightShape | TLIconShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLVideoShape;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export interface TLDocument extends BaseRecord<'document', ID<TLDocument>> {
|
export interface TLDocument extends BaseRecord<'document', ID<TLDocument>> {
|
||||||
|
@ -932,6 +938,19 @@ export interface TLHandlePartial {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLHandleType = SetValue<typeof TL_HANDLE_TYPES>;
|
export type TLHandleType = SetValue<typeof TL_HANDLE_TYPES>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type TLHighlightShapeProps = {
|
||||||
|
color: TLColorType;
|
||||||
|
size: TLSizeType;
|
||||||
|
opacity: TLOpacityType;
|
||||||
|
segments: TLDrawShapeSegment[];
|
||||||
|
isComplete: boolean;
|
||||||
|
isPen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLIconShape = TLBaseShape<'icon', TLIconShapeProps>;
|
export type TLIconShape = TLBaseShape<'icon', TLIconShapeProps>;
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { embedShapeTypeMigrations, embedShapeTypeValidator } from './shapes/TLEm
|
||||||
import { frameShapeTypeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape'
|
import { frameShapeTypeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape'
|
||||||
import { geoShapeTypeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape'
|
import { geoShapeTypeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape'
|
||||||
import { groupShapeTypeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape'
|
import { groupShapeTypeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape'
|
||||||
|
import { highlightShapeMigrations, highlightShapeTypeValidator } from './shapes/TLHighlightShape'
|
||||||
import { imageShapeTypeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape'
|
import { imageShapeTypeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape'
|
||||||
import { lineShapeTypeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape'
|
import { lineShapeTypeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape'
|
||||||
import { noteShapeTypeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape'
|
import { noteShapeTypeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape'
|
||||||
|
@ -45,6 +46,7 @@ const DEFAULT_SHAPES: { [K in TLShape['type']]: DefaultShapeInfo<Extract<TLShape
|
||||||
note: { migrations: noteShapeTypeMigrations, validator: noteShapeTypeValidator },
|
note: { migrations: noteShapeTypeMigrations, validator: noteShapeTypeValidator },
|
||||||
text: { migrations: textShapeTypeMigrations, validator: textShapeTypeValidator },
|
text: { migrations: textShapeTypeMigrations, validator: textShapeTypeValidator },
|
||||||
video: { migrations: videoShapeTypeMigrations, validator: videoShapeTypeValidator },
|
video: { migrations: videoShapeTypeMigrations, validator: videoShapeTypeValidator },
|
||||||
|
highlight: { migrations: highlightShapeMigrations, validator: highlightShapeTypeValidator },
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomShapeInfo<T extends TLUnknownShape> = {
|
type CustomShapeInfo<T extends TLUnknownShape> = {
|
||||||
|
|
|
@ -156,6 +156,12 @@ export {
|
||||||
type TLGroupShape,
|
type TLGroupShape,
|
||||||
type TLGroupShapeProps,
|
type TLGroupShapeProps,
|
||||||
} from './shapes/TLGroupShape'
|
} from './shapes/TLGroupShape'
|
||||||
|
export {
|
||||||
|
highlightShapeMigrations,
|
||||||
|
highlightShapeTypeValidator,
|
||||||
|
type TLHighlightShape,
|
||||||
|
type TLHighlightShapeProps,
|
||||||
|
} from './shapes/TLHighlightShape'
|
||||||
export {
|
export {
|
||||||
iconShapeTypeMigrations,
|
iconShapeTypeMigrations,
|
||||||
iconShapeTypeValidator,
|
iconShapeTypeValidator,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { TLEmbedShape } from '../shapes/TLEmbedShape'
|
||||||
import { TLFrameShape } from '../shapes/TLFrameShape'
|
import { TLFrameShape } from '../shapes/TLFrameShape'
|
||||||
import { TLGeoShape } from '../shapes/TLGeoShape'
|
import { TLGeoShape } from '../shapes/TLGeoShape'
|
||||||
import { TLGroupShape } from '../shapes/TLGroupShape'
|
import { TLGroupShape } from '../shapes/TLGroupShape'
|
||||||
|
import { TLHighlightShape } from '../shapes/TLHighlightShape'
|
||||||
import { TLIconShape } from '../shapes/TLIconShape'
|
import { TLIconShape } from '../shapes/TLIconShape'
|
||||||
import { TLImageShape } from '../shapes/TLImageShape'
|
import { TLImageShape } from '../shapes/TLImageShape'
|
||||||
import { TLLineShape } from '../shapes/TLLineShape'
|
import { TLLineShape } from '../shapes/TLLineShape'
|
||||||
|
@ -35,6 +36,7 @@ export type TLDefaultShape =
|
||||||
| TLTextShape
|
| TLTextShape
|
||||||
| TLVideoShape
|
| TLVideoShape
|
||||||
| TLIconShape
|
| TLIconShape
|
||||||
|
| TLHighlightShape
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type for a shape that is available in the editor but whose type is
|
* A type for a shape that is available in the editor but whose type is
|
||||||
|
|
|
@ -21,6 +21,11 @@ export type TLDrawShapeSegment = {
|
||||||
points: Vec2dModel[]
|
points: Vec2dModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const drawShapeSegmentValidator: T.Validator<TLDrawShapeSegment> = T.object({
|
||||||
|
type: T.setEnum(TL_DRAW_SHAPE_SEGMENT_TYPE),
|
||||||
|
points: T.arrayOf(T.point),
|
||||||
|
})
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLDrawShapeProps = {
|
export type TLDrawShapeProps = {
|
||||||
color: TLColorType
|
color: TLColorType
|
||||||
|
@ -46,12 +51,7 @@ export const drawShapeTypeValidator: T.Validator<TLDrawShape> = createShapeValid
|
||||||
dash: dashValidator,
|
dash: dashValidator,
|
||||||
size: sizeValidator,
|
size: sizeValidator,
|
||||||
opacity: opacityValidator,
|
opacity: opacityValidator,
|
||||||
segments: T.arrayOf(
|
segments: T.arrayOf(drawShapeSegmentValidator),
|
||||||
T.object({
|
|
||||||
type: T.setEnum(TL_DRAW_SHAPE_SEGMENT_TYPE),
|
|
||||||
points: T.arrayOf(T.point),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
isComplete: T.boolean,
|
isComplete: T.boolean,
|
||||||
isClosed: T.boolean,
|
isClosed: T.boolean,
|
||||||
isPen: T.boolean,
|
isPen: T.boolean,
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { defineMigrations } from '@tldraw/tlstore'
|
||||||
|
import { T } from '@tldraw/tlvalidate'
|
||||||
|
import { TLColorType, TLOpacityType, TLSizeType } from '../style-types'
|
||||||
|
import { colorValidator, opacityValidator, sizeValidator } from '../validation'
|
||||||
|
import { TLDrawShapeSegment, drawShapeSegmentValidator } from './TLDrawShape'
|
||||||
|
import { TLBaseShape, createShapeValidator } from './shape-validation'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLHighlightShapeProps = {
|
||||||
|
color: TLColorType
|
||||||
|
size: TLSizeType
|
||||||
|
opacity: TLOpacityType
|
||||||
|
segments: TLDrawShapeSegment[]
|
||||||
|
isComplete: boolean
|
||||||
|
isPen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
|
||||||
|
|
||||||
|
// --- VALIDATION ---
|
||||||
|
/** @public */
|
||||||
|
export const highlightShapeTypeValidator: T.Validator<TLHighlightShape> = createShapeValidator(
|
||||||
|
'highlight',
|
||||||
|
T.object({
|
||||||
|
color: colorValidator,
|
||||||
|
size: sizeValidator,
|
||||||
|
opacity: opacityValidator,
|
||||||
|
segments: T.arrayOf(drawShapeSegmentValidator),
|
||||||
|
isComplete: T.boolean,
|
||||||
|
isPen: T.boolean,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- MIGRATIONS ---
|
||||||
|
/** @public */
|
||||||
|
export const highlightShapeMigrations = defineMigrations({})
|
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,6 @@
|
||||||
import { App, useApp } from '@tldraw/editor'
|
import { App, featureFlags, useApp } from '@tldraw/editor'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useValue } from 'signia-react'
|
||||||
import { ToolItem, ToolsContextType, useTools } from './useTools'
|
import { ToolItem, ToolsContextType, useTools } from './useTools'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -41,6 +42,7 @@ export function ToolbarSchemaProvider({ overrides, children }: ToolbarSchemaProv
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
|
|
||||||
const tools = useTools()
|
const tools = useTools()
|
||||||
|
const highlighterEnabled = useValue(featureFlags.highlighterTool)
|
||||||
|
|
||||||
const toolbarSchema = React.useMemo<ToolbarSchemaContextType>(() => {
|
const toolbarSchema = React.useMemo<ToolbarSchemaContextType>(() => {
|
||||||
const schema: ToolbarSchemaContextType = [
|
const schema: ToolbarSchemaContextType = [
|
||||||
|
@ -74,12 +76,16 @@ export function ToolbarSchemaProvider({ overrides, children }: ToolbarSchemaProv
|
||||||
toolbarItem(tools.laser),
|
toolbarItem(tools.laser),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (highlighterEnabled) {
|
||||||
|
schema.push(toolbarItem(tools.highlight))
|
||||||
|
}
|
||||||
|
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
return overrides(app, schema, { tools })
|
return overrides(app, schema, { tools })
|
||||||
}
|
}
|
||||||
|
|
||||||
return schema
|
return schema
|
||||||
}, [app, overrides, tools])
|
}, [app, highlighterEnabled, overrides, tools])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolbarSchemaContext.Provider value={toolbarSchema}>{children}</ToolbarSchemaContext.Provider>
|
<ToolbarSchemaContext.Provider value={toolbarSchema}>{children}</ToolbarSchemaContext.Provider>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { App, TL_GEO_TYPES, useApp } from '@tldraw/editor'
|
import { App, TL_GEO_TYPES, featureFlags, useApp } from '@tldraw/editor'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { useValue } from 'signia-react'
|
||||||
import { EmbedDialog } from '../components/EmbedDialog'
|
import { EmbedDialog } from '../components/EmbedDialog'
|
||||||
import { TLUiIconType } from '../icon-types'
|
import { TLUiIconType } from '../icon-types'
|
||||||
import { useDialogs } from './useDialogsProvider'
|
import { useDialogs } from './useDialogsProvider'
|
||||||
|
@ -45,8 +46,10 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
||||||
const { addDialog } = useDialogs()
|
const { addDialog } = useDialogs()
|
||||||
const insertMedia = useInsertMedia()
|
const insertMedia = useInsertMedia()
|
||||||
|
|
||||||
|
const highlighterEnabled = useValue(featureFlags.highlighterTool)
|
||||||
|
|
||||||
const tools = React.useMemo<ToolsContextType>(() => {
|
const tools = React.useMemo<ToolsContextType>(() => {
|
||||||
const tools = makeTools([
|
const toolsArray: ToolItem[] = [
|
||||||
{
|
{
|
||||||
id: 'select',
|
id: 'select',
|
||||||
label: 'tool.select',
|
label: 'tool.select',
|
||||||
|
@ -198,14 +201,31 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
||||||
trackEvent('select-tool', { source, id: 'embed' })
|
trackEvent('select-tool', { source, id: 'embed' })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
|
|
||||||
|
if (highlighterEnabled) {
|
||||||
|
toolsArray.push({
|
||||||
|
id: 'highlight',
|
||||||
|
label: 'tool.highlight',
|
||||||
|
readonlyOk: true,
|
||||||
|
icon: 'tool-highlight',
|
||||||
|
// TODO: pick a better shortcut
|
||||||
|
kbd: 'i',
|
||||||
|
onSelect(source) {
|
||||||
|
app.setSelectedTool('highlight')
|
||||||
|
trackEvent('select-tool', { source, id: 'highlight' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools = makeTools(toolsArray)
|
||||||
|
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
return overrides(app, tools, { insertMedia })
|
return overrides(app, tools, { insertMedia })
|
||||||
}
|
}
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
}, [app, trackEvent, overrides, insertMedia, addDialog])
|
}, [highlighterEnabled, overrides, app, trackEvent, insertMedia, addDialog])
|
||||||
|
|
||||||
return <ToolsContext.Provider value={tools}>{children}</ToolsContext.Provider>
|
return <ToolsContext.Provider value={tools}>{children}</ToolsContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,6 +184,7 @@ export type TLTranslationKey =
|
||||||
| 'tool.diamond'
|
| 'tool.diamond'
|
||||||
| 'tool.ellipse'
|
| 'tool.ellipse'
|
||||||
| 'tool.hexagon'
|
| 'tool.hexagon'
|
||||||
|
| 'tool.highlight'
|
||||||
| 'tool.line'
|
| 'tool.line'
|
||||||
| 'tool.octagon'
|
| 'tool.octagon'
|
||||||
| 'tool.oval'
|
| 'tool.oval'
|
||||||
|
|
|
@ -184,6 +184,7 @@ export const DEFAULT_TRANSLATION = {
|
||||||
'tool.diamond': 'Diamond',
|
'tool.diamond': 'Diamond',
|
||||||
'tool.ellipse': 'Ellipse',
|
'tool.ellipse': 'Ellipse',
|
||||||
'tool.hexagon': 'Hexagon',
|
'tool.hexagon': 'Hexagon',
|
||||||
|
'tool.highlight': 'Highlight',
|
||||||
'tool.line': 'Line',
|
'tool.line': 'Line',
|
||||||
'tool.octagon': 'Octagon',
|
'tool.octagon': 'Octagon',
|
||||||
'tool.oval': 'Oval',
|
'tool.oval': 'Oval',
|
||||||
|
|
|
@ -141,6 +141,7 @@ export type TLUiIconType =
|
||||||
| 'tool-eraser'
|
| 'tool-eraser'
|
||||||
| 'tool-frame'
|
| 'tool-frame'
|
||||||
| 'tool-hand'
|
| 'tool-hand'
|
||||||
|
| 'tool-highlight'
|
||||||
| 'tool-highlighter'
|
| 'tool-highlighter'
|
||||||
| 'tool-laser'
|
| 'tool-laser'
|
||||||
| 'tool-line'
|
| 'tool-line'
|
||||||
|
@ -305,6 +306,7 @@ export const TLUiIconTypes = [
|
||||||
'tool-eraser',
|
'tool-eraser',
|
||||||
'tool-frame',
|
'tool-frame',
|
||||||
'tool-hand',
|
'tool-hand',
|
||||||
|
'tool-highlight',
|
||||||
'tool-highlighter',
|
'tool-highlighter',
|
||||||
'tool-laser',
|
'tool-laser',
|
||||||
'tool-line',
|
'tool-line',
|
||||||
|
|
Ładowanie…
Reference in New Issue