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',