From 674a829d1f5d57aa61135fa47bc41f5fe3f6025e Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 1 Jun 2023 13:46:13 +0100 Subject: [PATCH] [1/3] initial highlighter shape/tool (#1401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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] --- assets/icons/icon/tool-highlight.svg | 4 + assets/translations/main.json | 1 + ...ome-desktop-multiline-align-center-app.png | Bin 0 -> 10022 bytes ...me-desktop-multiline-align-center-diff.png | Bin 0 -> 10853 bytes ...ome-desktop-multiline-align-center-svg.png | Bin 0 -> 10022 bytes packages/assets/imports.d.ts | 1 + packages/assets/imports.js | 2 + packages/assets/urls.d.ts | 1 + packages/assets/urls.js | 4 + packages/editor/api-report.md | 38 ++ packages/editor/src/index.ts | 1 + packages/editor/src/lib/app/App.ts | 8 +- .../app/shapeutils/TLDrawUtil/TLDrawUtil.tsx | 6 +- .../lib/app/shapeutils/TLDrawUtil/getPath.ts | 10 +- .../TLHighlightUtil/TLHighlightUtil.tsx | 254 ++++++++++ .../src/lib/app/statechart/RootState.ts | 2 + .../statechart/TLDrawTool/TLHighlightTool.ts | 18 + .../statechart/TLDrawTool/children/Drawing.ts | 53 +- .../src/lib/config/TldrawEditorConfig.tsx | 2 + .../editor/src/lib/test/tools/drawing.test.ts | 473 +++++++++--------- packages/editor/src/lib/utils/debug-flags.ts | 1 + packages/tlschema/api-report.md | 21 +- packages/tlschema/src/createTLSchema.ts | 2 + packages/tlschema/src/index.ts | 6 + packages/tlschema/src/records/TLShape.ts | 2 + packages/tlschema/src/shapes/TLDrawShape.ts | 12 +- .../tlschema/src/shapes/TLHighlightShape.ts | 37 ++ packages/ui/api-report.md | 6 +- .../ui/src/lib/hooks/useToolbarSchema.tsx | 10 +- packages/ui/src/lib/hooks/useTools.tsx | 28 +- .../hooks/useTranslation/TLTranslationKey.ts | 1 + .../useTranslation/defaultTranslation.ts | 1 + packages/ui/src/lib/icon-types.ts | 2 + 33 files changed, 732 insertions(+), 275 deletions(-) create mode 100644 assets/icons/icon/tool-highlight.svg create mode 100644 e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-app.png create mode 100644 e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-diff.png create mode 100644 e2e/screenshots/local-darwin-chrome-desktop-multiline-align-center-svg.png create mode 100644 packages/editor/src/lib/app/shapeutils/TLHighlightUtil/TLHighlightUtil.tsx create mode 100644 packages/editor/src/lib/app/statechart/TLDrawTool/TLHighlightTool.ts create mode 100644 packages/tlschema/src/shapes/TLHighlightShape.ts 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 0000000000000000000000000000000000000000..419380353c9e3de92a13b8ea366dec4565fa898d GIT binary patch literal 10022 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 JM%paw{{WlUG=KmA literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a868a206698524e1d0699c1cc2d8fa3e256482c4 GIT binary patch literal 10853 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..419380353c9e3de92a13b8ea366dec4565fa898d GIT binary patch literal 10022 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 JM%paw{{WlUG=KmA literal 0 HcmV?d00001 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',