diff --git a/assets/icons/icon/tool-highlight.svg b/assets/icons/icon/tool-highlight.svg
new file mode 100644
index 000000000..6fd96fc1a
--- /dev/null
+++ b/assets/icons/icon/tool-highlight.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/translations/main.json b/assets/translations/main.json
index a87b96075..7e611d15d 100644
--- a/assets/translations/main.json
+++ b/assets/translations/main.json
@@ -180,6 +180,7 @@
"tool.diamond": "Diamond",
"tool.ellipse": "Ellipse",
"tool.hexagon": "Hexagon",
+ "tool.highlight": "Highlight",
"tool.line": "Line",
"tool.octagon": "Octagon",
"tool.oval": "Oval",
diff --git a/e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-app.png b/e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-app.png
new file mode 100644
index 000000000..419380353
Binary files /dev/null and b/e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-app.png differ
diff --git a/e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-diff.png b/e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-diff.png
new file mode 100644
index 000000000..a868a2066
Binary files /dev/null and b/e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-diff.png differ
diff --git a/e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-svg.png b/e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-svg.png
new file mode 100644
index 000000000..419380353
Binary files /dev/null and b/e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-svg.png differ
diff --git a/packages/assets/imports.d.ts b/packages/assets/imports.d.ts
index 7c87e3e62..a36c6aa32 100644
--- a/packages/assets/imports.d.ts
+++ b/packages/assets/imports.d.ts
@@ -150,6 +150,7 @@ export function getAssetUrlsByImport(opts?: AssetUrlOptions): {
'tool-eraser': string
'tool-frame': string
'tool-hand': string
+ 'tool-highlight': string
'tool-highlighter': string
'tool-laser': string
'tool-line': string
diff --git a/packages/assets/imports.js b/packages/assets/imports.js
index 88ce70a1d..e7f7fef4f 100644
--- a/packages/assets/imports.js
+++ b/packages/assets/imports.js
@@ -161,6 +161,7 @@ import iconsToolEmbed from './icons/icon/tool-embed.svg'
import iconsToolEraser from './icons/icon/tool-eraser.svg'
import iconsToolFrame from './icons/icon/tool-frame.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 iconsToolLaser from './icons/icon/tool-laser.svg'
import iconsToolLine from './icons/icon/tool-line.svg'
@@ -392,6 +393,7 @@ export function getAssetUrlsByImport(opts) {
'tool-eraser': formatAssetUrl(iconsToolEraser, opts),
'tool-frame': formatAssetUrl(iconsToolFrame, opts),
'tool-hand': formatAssetUrl(iconsToolHand, opts),
+ 'tool-highlight': formatAssetUrl(iconsToolHighlight, opts),
'tool-highlighter': formatAssetUrl(iconsToolHighlighter, opts),
'tool-laser': formatAssetUrl(iconsToolLaser, opts),
'tool-line': formatAssetUrl(iconsToolLine, opts),
diff --git a/packages/assets/urls.d.ts b/packages/assets/urls.d.ts
index 98d70d8d2..0faabc890 100644
--- a/packages/assets/urls.d.ts
+++ b/packages/assets/urls.d.ts
@@ -150,6 +150,7 @@ export function getAssetUrlsByMetaUrl(opts?: AssetUrlOptions): {
'tool-eraser': string
'tool-frame': string
'tool-hand': string
+ 'tool-highlight': string
'tool-highlighter': string
'tool-laser': string
'tool-line': string
diff --git a/packages/assets/urls.js b/packages/assets/urls.js
index ef5c0b831..7d6781a48 100644
--- a/packages/assets/urls.js
+++ b/packages/assets/urls.js
@@ -494,6 +494,10 @@ export function getAssetUrlsByMetaUrl(opts) {
new URL('./icons/icon/tool-hand.svg', import.meta.url).href,
opts
),
+ 'tool-highlight': formatAssetUrl(
+ new URL('./icons/icon/tool-highlight.svg', import.meta.url).href,
+ opts
+ ),
'tool-highlighter': formatAssetUrl(
new URL('./icons/icon/tool-highlighter.svg', import.meta.url).href,
opts
diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md
index 89e59af1a..fe797a70d 100644
--- a/packages/editor/api-report.md
+++ b/packages/editor/api-report.md
@@ -64,6 +64,7 @@ import { TLFrameShape } from '@tldraw/tlschema';
import { TLGeoShape } from '@tldraw/tlschema';
import { TLGroupShape } from '@tldraw/tlschema';
import { TLHandle } from '@tldraw/tlschema';
+import { TLHighlightShape } from '@tldraw/tlschema';
import { TLImageAsset } from '@tldraw/tlschema';
import { TLImageShape } from '@tldraw/tlschema';
import { TLInstance } from '@tldraw/tlschema';
@@ -725,6 +726,7 @@ export const EVENT_NAME_MAP: Record, keyo
// @internal (undocumented)
export const featureFlags: {
peopleMenu: DebugFlag;
+ highlighterTool: DebugFlag;
};
// @public
@@ -2204,6 +2206,42 @@ export class TLGroupUtil extends TLShapeUtil {
static type: string;
}
+// @public (undocumented)
+export class TLHighlightUtil extends TLShapeUtil {
+ // (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;
+ // (undocumented)
+ render(shape: TLHighlightShape): JSX.Element;
+ // (undocumented)
+ toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement;
+ // (undocumented)
+ static type: string;
+}
+
// @public (undocumented)
export type TLHistoryEntry = TLCommand | TLMark;
diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts
index 0dd24d531..6abc65eef 100644
--- a/packages/editor/src/index.ts
+++ b/packages/editor/src/index.ts
@@ -35,6 +35,7 @@ export { TLEmbedUtil } from './lib/app/shapeutils/TLEmbedUtil/TLEmbedUtil'
export { TLFrameUtil } from './lib/app/shapeutils/TLFrameUtil/TLFrameUtil'
export { TLGeoUtil } from './lib/app/shapeutils/TLGeoUtil/TLGeoUtil'
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 { TLLineUtil, getSplineForLineShape } from './lib/app/shapeutils/TLLineUtil/TLLineUtil'
export { TLNoteUtil } from './lib/app/shapeutils/TLNoteUtil/TLNoteUtil'
diff --git a/packages/editor/src/lib/app/App.ts b/packages/editor/src/lib/app/App.ts
index eca1865bc..48e394535 100644
--- a/packages/editor/src/lib/app/App.ts
+++ b/packages/editor/src/lib/app/App.ts
@@ -4844,6 +4844,7 @@ export class App extends EventEmitter {
if (!prev) return null
let newRecord = null as null | TLShape
for (const [k, v] of Object.entries(partial)) {
+ if (v === undefined) continue
switch (k) {
case 'id':
case 'type':
@@ -4857,7 +4858,12 @@ export class App extends EventEmitter {
}
if (k === 'props') {
- newRecord!.props = { ...prev.props, ...(v as any) }
+ const nextProps = { ...prev.props } as Record
+ for (const [propKey, propValue] of Object.entries(v as object)) {
+ if (propValue === undefined) continue
+ nextProps[propKey] = propValue
+ }
+ newRecord!.props = nextProps
} else {
;(newRecord as any)[k] = v
}
diff --git a/packages/editor/src/lib/app/shapeutils/TLDrawUtil/TLDrawUtil.tsx b/packages/editor/src/lib/app/shapeutils/TLDrawUtil/TLDrawUtil.tsx
index 86fb3c0cb..fe0a7284a 100644
--- a/packages/editor/src/lib/app/shapeutils/TLDrawUtil/TLDrawUtil.tsx
+++ b/packages/editor/src/lib/app/shapeutils/TLDrawUtil/TLDrawUtil.tsx
@@ -137,7 +137,7 @@ export class TLDrawUtil extends TLShapeUtil {
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 solidStrokePath =
@@ -201,7 +201,7 @@ export class TLDrawUtil extends TLShapeUtil {
}
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 solidStrokePath =
strokePoints.length > 1
@@ -224,7 +224,7 @@ export class TLDrawUtil extends TLShapeUtil {
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 solidStrokePath =
strokePoints.length > 1
diff --git a/packages/editor/src/lib/app/shapeutils/TLDrawUtil/getPath.ts b/packages/editor/src/lib/app/shapeutils/TLDrawUtil/getPath.ts
index 7e5f218ea..f2ac228e4 100644
--- a/packages/editor/src/lib/app/shapeutils/TLDrawUtil/getPath.ts
+++ b/packages/editor/src/lib/app/shapeutils/TLDrawUtil/getPath.ts
@@ -1,5 +1,5 @@
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
@@ -37,7 +37,7 @@ const solidSettings = (strokeWidth: number): StrokeOptions => {
}
export function getFreehandOptions(
- shape: TLDrawShape,
+ shapeProps: { dash: TLDashType; isPen: boolean; isComplete: boolean },
strokeWidth: number,
forceComplete: boolean,
forceSolid: boolean
@@ -45,12 +45,12 @@ export function getFreehandOptions(
return {
...(forceSolid
? solidSettings(strokeWidth)
- : shape.props.dash === 'draw'
- ? shape.props.isPen
+ : shapeProps.dash === 'draw'
+ ? shapeProps.isPen
? realPressureSettings(strokeWidth)
: simulatePressureSettings(strokeWidth)
: solidSettings(strokeWidth)),
- last: shape.props.isComplete || forceComplete,
+ last: shapeProps.isComplete || forceComplete,
}
}
diff --git a/packages/editor/src/lib/app/shapeutils/TLHighlightUtil/TLHighlightUtil.tsx b/packages/editor/src/lib/app/shapeutils/TLHighlightUtil/TLHighlightUtil.tsx
new file mode 100644
index 000000000..1cc484ba9
--- /dev/null
+++ b/packages/editor/src/lib/app/shapeutils/TLHighlightUtil/TLHighlightUtil.tsx
@@ -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 {
+ 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 (
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ )
+ }
+
+ 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
+ }
+
+ 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 = (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`
+}
diff --git a/packages/editor/src/lib/app/statechart/RootState.ts b/packages/editor/src/lib/app/statechart/RootState.ts
index a6bdd5f27..45a825efd 100644
--- a/packages/editor/src/lib/app/statechart/RootState.ts
+++ b/packages/editor/src/lib/app/statechart/RootState.ts
@@ -2,6 +2,7 @@ import { TLEventHandlers } from '../types/event-types'
import { StateNode } from './StateNode'
import { TLArrowTool } from './TLArrowTool/TLArrowTool'
import { TLDrawTool } from './TLDrawTool/TLDrawTool'
+import { TLHighlightTool } from './TLDrawTool/TLHighlightTool'
import { TLEraserTool } from './TLEraserTool/TLEraserTool'
import { TLFrameTool } from './TLFrameTool/TLFrameTool'
import { TLGeoTool } from './TLGeoTool/TLGeoTool'
@@ -21,6 +22,7 @@ export class RootState extends StateNode {
TLHandTool,
TLEraserTool,
TLDrawTool,
+ TLHighlightTool,
TLTextTool,
TLLineTool,
TLArrowTool,
diff --git a/packages/editor/src/lib/app/statechart/TLDrawTool/TLHighlightTool.ts b/packages/editor/src/lib/app/statechart/TLDrawTool/TLHighlightTool.ts
new file mode 100644
index 000000000..53d6a53e3
--- /dev/null
+++ b/packages/editor/src/lib/app/statechart/TLDrawTool/TLHighlightTool.ts
@@ -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
+ }
+}
diff --git a/packages/editor/src/lib/app/statechart/TLDrawTool/children/Drawing.ts b/packages/editor/src/lib/app/statechart/TLDrawTool/children/Drawing.ts
index e7d7267f7..a87351e56 100644
--- a/packages/editor/src/lib/app/statechart/TLDrawTool/children/Drawing.ts
+++ b/packages/editor/src/lib/app/statechart/TLDrawTool/children/Drawing.ts
@@ -3,6 +3,7 @@ import {
createShapeId,
TLDrawShape,
TLDrawShapeSegment,
+ TLHighlightShape,
TLSizeType,
Vec2dModel,
} from '@tldraw/tlschema'
@@ -12,16 +13,24 @@ import { uniqueId } from '../../../../utils/data'
import { TLDrawUtil } from '../../../shapeutils/TLDrawUtil/TLDrawUtil'
import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types'
+import { TLHighlightUtil } from '../../../shapeutils/TLHighlightUtil/TLHighlightUtil'
import { StateNode } from '../../StateNode'
+type DrawableShape = TLDrawShape | TLHighlightShape
+
export class Drawing extends StateNode {
static override id = 'drawing'
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
@@ -127,7 +136,13 @@ export class Drawing extends StateNode {
this.pagePointWhereCurrentSegmentChanged = this.app.inputs.currentPagePoint.clone()
}
+ canClose() {
+ return this.shapeType !== 'highlight'
+ }
+
getIsClosed(segments: TLDrawShapeSegment[], size: TLSizeType) {
+ if (!this.canClose()) return false
+
const strokeWidth = this.app.getStrokeWidth(size)
const firstPoint = segments[0].points[0]
const lastSegment = segments[segments.length - 1]
@@ -158,7 +173,7 @@ export class Drawing extends StateNode {
this.lastRecordedPoint = originPagePoint.clone()
if (this.initialShape) {
- const shape = this.app.getShapeById(this.initialShape.id)
+ const shape = this.app.getShapeById(this.initialShape.id)
if (shape && this.segmentMode === 'straight') {
// Connect dots
@@ -204,10 +219,10 @@ export class Drawing extends StateNode {
this.app.updateShapes([
{
id: shape.id,
- type: 'draw',
+ type: this.shapeType,
props: {
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([
{
id,
- type: 'draw',
+ type: this.shapeType,
x: originPagePoint.x,
y: originPagePoint.y,
props: {
@@ -245,7 +260,7 @@ export class Drawing extends StateNode {
])
this.currentLineLength = 0
- this.initialShape = this.app.getShapeById(id)
+ this.initialShape = this.app.getShapeById(id)
}
private updateShapes() {
@@ -259,7 +274,7 @@ export class Drawing extends StateNode {
props: { size },
} = initialShape
- const shape = this.app.getShapeById(id)!
+ const shape = this.app.getShapeById(id)!
if (!shape) return
@@ -329,10 +344,10 @@ export class Drawing extends StateNode {
[
{
id,
- type: 'draw',
+ type: this.shapeType,
props: {
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,
- type: 'draw',
+ type: this.shapeType,
props: {
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,
- type: 'draw',
+ type: this.shapeType,
props: {
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,
- type: 'draw',
+ type: this.shapeType,
props: {
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.
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
@@ -588,7 +603,7 @@ export class Drawing extends StateNode {
this.app.createShapes([
{
id: newShapeId,
- type: 'draw',
+ type: this.shapeType,
x: currentPagePoint.x,
y: currentPagePoint.y,
props: {
@@ -603,7 +618,7 @@ export class Drawing extends StateNode {
},
])
- this.initialShape = structuredClone(this.app.getShapeById(newShapeId)!)
+ this.initialShape = structuredClone(this.app.getShapeById(newShapeId)!)
this.mergeNextPoint = false
this.lastRecordedPoint = this.app.inputs.currentPagePoint.clone()
this.currentLineLength = 0
diff --git a/packages/editor/src/lib/config/TldrawEditorConfig.tsx b/packages/editor/src/lib/config/TldrawEditorConfig.tsx
index d103e3661..1c258fcb8 100644
--- a/packages/editor/src/lib/config/TldrawEditorConfig.tsx
+++ b/packages/editor/src/lib/config/TldrawEditorConfig.tsx
@@ -20,6 +20,7 @@ import { TLEmbedUtil } from '../app/shapeutils/TLEmbedUtil/TLEmbedUtil'
import { TLFrameUtil } from '../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGeoUtil } from '../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TLGroupUtil } from '../app/shapeutils/TLGroupUtil/TLGroupUtil'
+import { TLHighlightUtil } from '../app/shapeutils/TLHighlightUtil/TLHighlightUtil'
import { TLImageUtil } from '../app/shapeutils/TLImageUtil/TLImageUtil'
import { TLLineUtil } from '../app/shapeutils/TLLineUtil/TLLineUtil'
import { TLNoteUtil } from '../app/shapeutils/TLNoteUtil/TLNoteUtil'
@@ -47,6 +48,7 @@ const DEFAULT_SHAPE_UTILS: {
note: TLNoteUtil,
text: TLTextUtil,
video: TLVideoUtil,
+ highlight: TLHighlightUtil,
}
/** @public */
diff --git a/packages/editor/src/lib/test/tools/drawing.test.ts b/packages/editor/src/lib/test/tools/drawing.test.ts
index 2f151fa2f..1f1733fad 100644
--- a/packages/editor/src/lib/test/tools/drawing.test.ts
+++ b/packages/editor/src/lib/test/tools/drawing.test.ts
@@ -1,4 +1,4 @@
-import { TLDrawShape } from '@tldraw/tlschema'
+import { TLDrawShape, TLHighlightShape } from '@tldraw/tlschema'
import { last } from '@tldraw/utils'
import { TestApp } from '../TestApp'
@@ -16,237 +16,248 @@ beforeEach(() => {
app.createShapes([])
})
-describe('When drawing...', () => {
- it('Creates a dot', () => {
- app
- .setSelectedTool('draw')
- .pointerDown(60, 60)
- .expectToBeIn('draw.drawing')
- .pointerUp()
- .expectToBeIn('draw.idle')
+type DrawableShape = TLDrawShape | TLHighlightShape
- 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(shape.props.segments.length).toBe(1)
+ expect(app.shapesArray).toHaveLength(1)
- const segment = shape.props.segments[0]
- expect(segment.type).toBe('free')
+ 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('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)
- })
-})
+}
diff --git a/packages/editor/src/lib/utils/debug-flags.ts b/packages/editor/src/lib/utils/debug-flags.ts
index 20fb9c0d5..bf22bd424 100644
--- a/packages/editor/src/lib/utils/debug-flags.ts
+++ b/packages/editor/src/lib/utils/debug-flags.ts
@@ -11,6 +11,7 @@ export const featureFlags = {
// todo: remove this. it's not used, but we only have one feature flag and i
// wanted an example :(
peopleMenu: createFeatureFlag('peopleMenu'),
+ highlighterTool: createFeatureFlag('highlighterTool'),
} satisfies Record>
/** @internal */
diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md
index 712d5a80b..f9f682a01 100644
--- a/packages/tlschema/api-report.md
+++ b/packages/tlschema/api-report.md
@@ -375,6 +375,12 @@ export const groupShapeTypeValidator: T.Validator;
// @public (undocumented)
export const handleTypeValidator: T.Validator;
+// @public (undocumented)
+export const highlightShapeMigrations: Migrations;
+
+// @public (undocumented)
+export const highlightShapeTypeValidator: T.Validator;
+
// @public (undocumented)
export const iconShapeTypeMigrations: Migrations;
@@ -760,7 +766,7 @@ export interface TLDashStyle extends TLBaseStyle {
export type TLDashType = SetValue;
// @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
export interface TLDocument extends BaseRecord<'document', ID> {
@@ -932,6 +938,19 @@ export interface TLHandlePartial {
// @public (undocumented)
export type TLHandleType = SetValue;
+// @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)
export type TLIconShape = TLBaseShape<'icon', TLIconShapeProps>;
diff --git a/packages/tlschema/src/createTLSchema.ts b/packages/tlschema/src/createTLSchema.ts
index adf93a116..36d660f74 100644
--- a/packages/tlschema/src/createTLSchema.ts
+++ b/packages/tlschema/src/createTLSchema.ts
@@ -20,6 +20,7 @@ import { embedShapeTypeMigrations, embedShapeTypeValidator } from './shapes/TLEm
import { frameShapeTypeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape'
import { geoShapeTypeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape'
import { groupShapeTypeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape'
+import { highlightShapeMigrations, highlightShapeTypeValidator } from './shapes/TLHighlightShape'
import { imageShapeTypeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape'
import { lineShapeTypeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape'
import { noteShapeTypeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape'
@@ -45,6 +46,7 @@ const DEFAULT_SHAPES: { [K in TLShape['type']]: DefaultShapeInfo = {
diff --git a/packages/tlschema/src/index.ts b/packages/tlschema/src/index.ts
index 56281aeca..d9dedcf63 100644
--- a/packages/tlschema/src/index.ts
+++ b/packages/tlschema/src/index.ts
@@ -156,6 +156,12 @@ export {
type TLGroupShape,
type TLGroupShapeProps,
} from './shapes/TLGroupShape'
+export {
+ highlightShapeMigrations,
+ highlightShapeTypeValidator,
+ type TLHighlightShape,
+ type TLHighlightShapeProps,
+} from './shapes/TLHighlightShape'
export {
iconShapeTypeMigrations,
iconShapeTypeValidator,
diff --git a/packages/tlschema/src/records/TLShape.ts b/packages/tlschema/src/records/TLShape.ts
index baa4b80dd..b3ac53bb3 100644
--- a/packages/tlschema/src/records/TLShape.ts
+++ b/packages/tlschema/src/records/TLShape.ts
@@ -8,6 +8,7 @@ import { TLEmbedShape } from '../shapes/TLEmbedShape'
import { TLFrameShape } from '../shapes/TLFrameShape'
import { TLGeoShape } from '../shapes/TLGeoShape'
import { TLGroupShape } from '../shapes/TLGroupShape'
+import { TLHighlightShape } from '../shapes/TLHighlightShape'
import { TLIconShape } from '../shapes/TLIconShape'
import { TLImageShape } from '../shapes/TLImageShape'
import { TLLineShape } from '../shapes/TLLineShape'
@@ -35,6 +36,7 @@ export type TLDefaultShape =
| TLTextShape
| TLVideoShape
| TLIconShape
+ | TLHighlightShape
/**
* A type for a shape that is available in the editor but whose type is
diff --git a/packages/tlschema/src/shapes/TLDrawShape.ts b/packages/tlschema/src/shapes/TLDrawShape.ts
index d2b17499b..f1f326290 100644
--- a/packages/tlschema/src/shapes/TLDrawShape.ts
+++ b/packages/tlschema/src/shapes/TLDrawShape.ts
@@ -21,6 +21,11 @@ export type TLDrawShapeSegment = {
points: Vec2dModel[]
}
+export const drawShapeSegmentValidator: T.Validator = T.object({
+ type: T.setEnum(TL_DRAW_SHAPE_SEGMENT_TYPE),
+ points: T.arrayOf(T.point),
+})
+
/** @public */
export type TLDrawShapeProps = {
color: TLColorType
@@ -46,12 +51,7 @@ export const drawShapeTypeValidator: T.Validator = createShapeValid
dash: dashValidator,
size: sizeValidator,
opacity: opacityValidator,
- segments: T.arrayOf(
- T.object({
- type: T.setEnum(TL_DRAW_SHAPE_SEGMENT_TYPE),
- points: T.arrayOf(T.point),
- })
- ),
+ segments: T.arrayOf(drawShapeSegmentValidator),
isComplete: T.boolean,
isClosed: T.boolean,
isPen: T.boolean,
diff --git a/packages/tlschema/src/shapes/TLHighlightShape.ts b/packages/tlschema/src/shapes/TLHighlightShape.ts
new file mode 100644
index 000000000..737114656
--- /dev/null
+++ b/packages/tlschema/src/shapes/TLHighlightShape.ts
@@ -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 = 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({})
diff --git a/packages/ui/api-report.md b/packages/ui/api-report.md
index a865c92c2..65ddb1d6f 100644
--- a/packages/ui/api-report.md
+++ b/packages/ui/api-report.md
@@ -712,7 +712,7 @@ export type TLTranslation = {
};
// @public (undocumented)
-export type TLTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open';
+export type TLTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open';
// @public (undocumented)
export type TLTranslationLocale = TLTranslations[number]['locale'];
@@ -732,10 +732,10 @@ export type TLUiEventHandler
export type TLUiEventSource = 'actions-menu' | 'context-menu' | 'debug-panel' | 'dialog' | 'export-menu' | 'help-menu' | 'helper-buttons' | 'kbd' | 'menu' | 'navigation-zone' | 'page-menu' | 'people-menu' | 'quick-actions' | 'share-menu' | 'toolbar' | 'unknown' | 'zoom-menu';
// @public (undocumented)
-export type TLUiIconType = 'align-bottom-center' | 'align-bottom-left' | 'align-bottom-right' | 'align-bottom' | 'align-center-center' | 'align-center-horizontal' | 'align-center-left' | 'align-center-right' | 'align-center-vertical' | 'align-left' | 'align-right' | 'align-top-center' | 'align-top-left' | 'align-top-right' | 'align-top' | 'arrow-left' | 'arrowhead-arrow' | 'arrowhead-bar' | 'arrowhead-diamond' | 'arrowhead-dot' | 'arrowhead-none' | 'arrowhead-square' | 'arrowhead-triangle-inverted' | 'arrowhead-triangle' | 'aspect-ratio' | 'avatar' | 'blob' | 'bring-forward' | 'bring-to-front' | 'check' | 'checkbox-checked' | 'checkbox-empty' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'chevrons-ne' | 'chevrons-sw' | 'clipboard-copied' | 'clipboard-copy' | 'code' | 'collab' | 'color' | 'comment' | 'cross-2' | 'cross' | 'dash-dashed' | 'dash-dotted' | 'dash-draw' | 'dash-solid' | 'discord' | 'distribute-horizontal' | 'distribute-vertical' | 'dot' | 'dots-horizontal' | 'dots-vertical' | 'drag-handle-dots' | 'duplicate' | 'edit' | 'external-link' | 'file' | 'fill-none' | 'fill-pattern' | 'fill-semi' | 'fill-solid' | 'follow' | 'following' | 'font-draw' | 'font-mono' | 'font-sans' | 'font-serif' | 'geo-arrow-down' | 'geo-arrow-left' | 'geo-arrow-right' | 'geo-arrow-up' | 'geo-check-box' | 'geo-diamond' | 'geo-ellipse' | 'geo-hexagon' | 'geo-octagon' | 'geo-oval' | 'geo-pentagon' | 'geo-rectangle' | 'geo-rhombus-2' | 'geo-rhombus' | 'geo-star' | 'geo-trapezoid' | 'geo-triangle' | 'geo-x-box' | 'github' | 'group' | 'hidden' | 'image' | 'info-circle' | 'leading' | 'link' | 'lock-small' | 'lock' | 'menu' | 'minus' | 'mixed' | 'pack' | 'page' | 'plus' | 'question-mark-circle' | 'question-mark' | 'redo' | 'reset-zoom' | 'rotate-ccw' | 'rotate-cw' | 'ruler' | 'search' | 'send-backward' | 'send-to-back' | 'settings-horizontal' | 'settings-vertical-1' | 'settings-vertical' | 'share-1' | 'share-2' | 'size-extra-large' | 'size-large' | 'size-medium' | 'size-small' | 'spline-cubic' | 'spline-line' | 'stack-horizontal' | 'stack-vertical' | 'stretch-horizontal' | 'stretch-vertical' | 'text-align-center' | 'text-align-justify' | 'text-align-left' | 'text-align-right' | 'tool-arrow' | 'tool-embed' | 'tool-eraser' | 'tool-frame' | 'tool-hand' | 'tool-highlighter' | 'tool-laser' | 'tool-line' | 'tool-media' | 'tool-note' | 'tool-pencil' | 'tool-pointer' | 'tool-text' | 'trash' | 'triangle-down' | 'triangle-up' | 'twitter' | 'undo' | 'ungroup' | 'unlock-small' | 'unlock' | 'vertical-align-center' | 'vertical-align-end' | 'vertical-align-start' | 'visible' | 'warning-triangle' | 'zoom-in' | 'zoom-out';
+export type TLUiIconType = 'align-bottom-center' | 'align-bottom-left' | 'align-bottom-right' | 'align-bottom' | 'align-center-center' | 'align-center-horizontal' | 'align-center-left' | 'align-center-right' | 'align-center-vertical' | 'align-left' | 'align-right' | 'align-top-center' | 'align-top-left' | 'align-top-right' | 'align-top' | 'arrow-left' | 'arrowhead-arrow' | 'arrowhead-bar' | 'arrowhead-diamond' | 'arrowhead-dot' | 'arrowhead-none' | 'arrowhead-square' | 'arrowhead-triangle-inverted' | 'arrowhead-triangle' | 'aspect-ratio' | 'avatar' | 'blob' | 'bring-forward' | 'bring-to-front' | 'check' | 'checkbox-checked' | 'checkbox-empty' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'chevrons-ne' | 'chevrons-sw' | 'clipboard-copied' | 'clipboard-copy' | 'code' | 'collab' | 'color' | 'comment' | 'cross-2' | 'cross' | 'dash-dashed' | 'dash-dotted' | 'dash-draw' | 'dash-solid' | 'discord' | 'distribute-horizontal' | 'distribute-vertical' | 'dot' | 'dots-horizontal' | 'dots-vertical' | 'drag-handle-dots' | 'duplicate' | 'edit' | 'external-link' | 'file' | 'fill-none' | 'fill-pattern' | 'fill-semi' | 'fill-solid' | 'follow' | 'following' | 'font-draw' | 'font-mono' | 'font-sans' | 'font-serif' | 'geo-arrow-down' | 'geo-arrow-left' | 'geo-arrow-right' | 'geo-arrow-up' | 'geo-check-box' | 'geo-diamond' | 'geo-ellipse' | 'geo-hexagon' | 'geo-octagon' | 'geo-oval' | 'geo-pentagon' | 'geo-rectangle' | 'geo-rhombus-2' | 'geo-rhombus' | 'geo-star' | 'geo-trapezoid' | 'geo-triangle' | 'geo-x-box' | 'github' | 'group' | 'hidden' | 'image' | 'info-circle' | 'leading' | 'link' | 'lock-small' | 'lock' | 'menu' | 'minus' | 'mixed' | 'pack' | 'page' | 'plus' | 'question-mark-circle' | 'question-mark' | 'redo' | 'reset-zoom' | 'rotate-ccw' | 'rotate-cw' | 'ruler' | 'search' | 'send-backward' | 'send-to-back' | 'settings-horizontal' | 'settings-vertical-1' | 'settings-vertical' | 'share-1' | 'share-2' | 'size-extra-large' | 'size-large' | 'size-medium' | 'size-small' | 'spline-cubic' | 'spline-line' | 'stack-horizontal' | 'stack-vertical' | 'stretch-horizontal' | 'stretch-vertical' | 'text-align-center' | 'text-align-justify' | 'text-align-left' | 'text-align-right' | 'tool-arrow' | 'tool-embed' | 'tool-eraser' | 'tool-frame' | 'tool-hand' | 'tool-highlight' | 'tool-highlighter' | 'tool-laser' | 'tool-line' | 'tool-media' | 'tool-note' | 'tool-pencil' | 'tool-pointer' | 'tool-text' | 'trash' | 'triangle-down' | 'triangle-up' | 'twitter' | 'undo' | 'ungroup' | 'unlock-small' | 'unlock' | 'vertical-align-center' | 'vertical-align-end' | 'vertical-align-start' | 'visible' | 'warning-triangle' | 'zoom-in' | 'zoom-out';
// @public (undocumented)
-export const TLUiIconTypes: readonly ["align-bottom-center", "align-bottom-left", "align-bottom-right", "align-bottom", "align-center-center", "align-center-horizontal", "align-center-left", "align-center-right", "align-center-vertical", "align-left", "align-right", "align-top-center", "align-top-left", "align-top-right", "align-top", "arrow-left", "arrowhead-arrow", "arrowhead-bar", "arrowhead-diamond", "arrowhead-dot", "arrowhead-none", "arrowhead-square", "arrowhead-triangle-inverted", "arrowhead-triangle", "aspect-ratio", "avatar", "blob", "bring-forward", "bring-to-front", "check", "checkbox-checked", "checkbox-empty", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "chevrons-ne", "chevrons-sw", "clipboard-copied", "clipboard-copy", "code", "collab", "color", "comment", "cross-2", "cross", "dash-dashed", "dash-dotted", "dash-draw", "dash-solid", "discord", "distribute-horizontal", "distribute-vertical", "dot", "dots-horizontal", "dots-vertical", "drag-handle-dots", "duplicate", "edit", "external-link", "file", "fill-none", "fill-pattern", "fill-semi", "fill-solid", "follow", "following", "font-draw", "font-mono", "font-sans", "font-serif", "geo-arrow-down", "geo-arrow-left", "geo-arrow-right", "geo-arrow-up", "geo-check-box", "geo-diamond", "geo-ellipse", "geo-hexagon", "geo-octagon", "geo-oval", "geo-pentagon", "geo-rectangle", "geo-rhombus-2", "geo-rhombus", "geo-star", "geo-trapezoid", "geo-triangle", "geo-x-box", "github", "group", "hidden", "image", "info-circle", "leading", "link", "lock-small", "lock", "menu", "minus", "mixed", "pack", "page", "plus", "question-mark-circle", "question-mark", "redo", "reset-zoom", "rotate-ccw", "rotate-cw", "ruler", "search", "send-backward", "send-to-back", "settings-horizontal", "settings-vertical-1", "settings-vertical", "share-1", "share-2", "size-extra-large", "size-large", "size-medium", "size-small", "spline-cubic", "spline-line", "stack-horizontal", "stack-vertical", "stretch-horizontal", "stretch-vertical", "text-align-center", "text-align-justify", "text-align-left", "text-align-right", "tool-arrow", "tool-embed", "tool-eraser", "tool-frame", "tool-hand", "tool-highlighter", "tool-laser", "tool-line", "tool-media", "tool-note", "tool-pencil", "tool-pointer", "tool-text", "trash", "triangle-down", "triangle-up", "twitter", "undo", "ungroup", "unlock-small", "unlock", "vertical-align-center", "vertical-align-end", "vertical-align-start", "visible", "warning-triangle", "zoom-in", "zoom-out"];
+export const TLUiIconTypes: readonly ["align-bottom-center", "align-bottom-left", "align-bottom-right", "align-bottom", "align-center-center", "align-center-horizontal", "align-center-left", "align-center-right", "align-center-vertical", "align-left", "align-right", "align-top-center", "align-top-left", "align-top-right", "align-top", "arrow-left", "arrowhead-arrow", "arrowhead-bar", "arrowhead-diamond", "arrowhead-dot", "arrowhead-none", "arrowhead-square", "arrowhead-triangle-inverted", "arrowhead-triangle", "aspect-ratio", "avatar", "blob", "bring-forward", "bring-to-front", "check", "checkbox-checked", "checkbox-empty", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "chevrons-ne", "chevrons-sw", "clipboard-copied", "clipboard-copy", "code", "collab", "color", "comment", "cross-2", "cross", "dash-dashed", "dash-dotted", "dash-draw", "dash-solid", "discord", "distribute-horizontal", "distribute-vertical", "dot", "dots-horizontal", "dots-vertical", "drag-handle-dots", "duplicate", "edit", "external-link", "file", "fill-none", "fill-pattern", "fill-semi", "fill-solid", "follow", "following", "font-draw", "font-mono", "font-sans", "font-serif", "geo-arrow-down", "geo-arrow-left", "geo-arrow-right", "geo-arrow-up", "geo-check-box", "geo-diamond", "geo-ellipse", "geo-hexagon", "geo-octagon", "geo-oval", "geo-pentagon", "geo-rectangle", "geo-rhombus-2", "geo-rhombus", "geo-star", "geo-trapezoid", "geo-triangle", "geo-x-box", "github", "group", "hidden", "image", "info-circle", "leading", "link", "lock-small", "lock", "menu", "minus", "mixed", "pack", "page", "plus", "question-mark-circle", "question-mark", "redo", "reset-zoom", "rotate-ccw", "rotate-cw", "ruler", "search", "send-backward", "send-to-back", "settings-horizontal", "settings-vertical-1", "settings-vertical", "share-1", "share-2", "size-extra-large", "size-large", "size-medium", "size-small", "spline-cubic", "spline-line", "stack-horizontal", "stack-vertical", "stretch-horizontal", "stretch-vertical", "text-align-center", "text-align-justify", "text-align-left", "text-align-right", "tool-arrow", "tool-embed", "tool-eraser", "tool-frame", "tool-hand", "tool-highlight", "tool-highlighter", "tool-laser", "tool-line", "tool-media", "tool-note", "tool-pencil", "tool-pointer", "tool-text", "trash", "triangle-down", "triangle-up", "twitter", "undo", "ungroup", "unlock-small", "unlock", "vertical-align-center", "vertical-align-end", "vertical-align-start", "visible", "warning-triangle", "zoom-in", "zoom-out"];
// @public (undocumented)
export const ToastsContext: Context;
diff --git a/packages/ui/src/lib/hooks/useToolbarSchema.tsx b/packages/ui/src/lib/hooks/useToolbarSchema.tsx
index 4caed0b89..32c43cee1 100644
--- a/packages/ui/src/lib/hooks/useToolbarSchema.tsx
+++ b/packages/ui/src/lib/hooks/useToolbarSchema.tsx
@@ -1,5 +1,6 @@
-import { App, useApp } from '@tldraw/editor'
+import { App, featureFlags, useApp } from '@tldraw/editor'
import React from 'react'
+import { useValue } from 'signia-react'
import { ToolItem, ToolsContextType, useTools } from './useTools'
/** @public */
@@ -41,6 +42,7 @@ export function ToolbarSchemaProvider({ overrides, children }: ToolbarSchemaProv
const app = useApp()
const tools = useTools()
+ const highlighterEnabled = useValue(featureFlags.highlighterTool)
const toolbarSchema = React.useMemo(() => {
const schema: ToolbarSchemaContextType = [
@@ -74,12 +76,16 @@ export function ToolbarSchemaProvider({ overrides, children }: ToolbarSchemaProv
toolbarItem(tools.laser),
]
+ if (highlighterEnabled) {
+ schema.push(toolbarItem(tools.highlight))
+ }
+
if (overrides) {
return overrides(app, schema, { tools })
}
return schema
- }, [app, overrides, tools])
+ }, [app, highlighterEnabled, overrides, tools])
return (
{children}
diff --git a/packages/ui/src/lib/hooks/useTools.tsx b/packages/ui/src/lib/hooks/useTools.tsx
index ab48d7432..095414591 100644
--- a/packages/ui/src/lib/hooks/useTools.tsx
+++ b/packages/ui/src/lib/hooks/useTools.tsx
@@ -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 { useValue } from 'signia-react'
import { EmbedDialog } from '../components/EmbedDialog'
import { TLUiIconType } from '../icon-types'
import { useDialogs } from './useDialogsProvider'
@@ -45,8 +46,10 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
const { addDialog } = useDialogs()
const insertMedia = useInsertMedia()
+ const highlighterEnabled = useValue(featureFlags.highlighterTool)
+
const tools = React.useMemo(() => {
- const tools = makeTools([
+ const toolsArray: ToolItem[] = [
{
id: 'select',
label: 'tool.select',
@@ -198,14 +201,31 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
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) {
return overrides(app, tools, { insertMedia })
}
return tools
- }, [app, trackEvent, overrides, insertMedia, addDialog])
+ }, [highlighterEnabled, overrides, app, trackEvent, insertMedia, addDialog])
return {children}
}
diff --git a/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts b/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts
index 2c179dc5e..cfcd8bada 100644
--- a/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts
+++ b/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts
@@ -184,6 +184,7 @@ export type TLTranslationKey =
| 'tool.diamond'
| 'tool.ellipse'
| 'tool.hexagon'
+ | 'tool.highlight'
| 'tool.line'
| 'tool.octagon'
| 'tool.oval'
diff --git a/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts b/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts
index 68495669a..10a00c30a 100644
--- a/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts
+++ b/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts
@@ -184,6 +184,7 @@ export const DEFAULT_TRANSLATION = {
'tool.diamond': 'Diamond',
'tool.ellipse': 'Ellipse',
'tool.hexagon': 'Hexagon',
+ 'tool.highlight': 'Highlight',
'tool.line': 'Line',
'tool.octagon': 'Octagon',
'tool.oval': 'Oval',
diff --git a/packages/ui/src/lib/icon-types.ts b/packages/ui/src/lib/icon-types.ts
index bb3d9da2f..4a7374544 100644
--- a/packages/ui/src/lib/icon-types.ts
+++ b/packages/ui/src/lib/icon-types.ts
@@ -141,6 +141,7 @@ export type TLUiIconType =
| 'tool-eraser'
| 'tool-frame'
| 'tool-hand'
+ | 'tool-highlight'
| 'tool-highlighter'
| 'tool-laser'
| 'tool-line'
@@ -305,6 +306,7 @@ export const TLUiIconTypes = [
'tool-eraser',
'tool-frame',
'tool-hand',
+ 'tool-highlight',
'tool-highlighter',
'tool-laser',
'tool-line',