kopia lustrzana https://github.com/Tldraw/Tldraw
Merge remote-tracking branch 'origin/stickies-rc' into lu/frame-kickout-rotation
commit
bcd412bedb
|
@ -36,7 +36,8 @@ export function CustomRenderer() {
|
|||
const theme = getDefaultColorTheme({ isDarkMode: editor.user.getIsDarkMode() })
|
||||
const currentPageId = editor.getCurrentPageId()
|
||||
|
||||
for (const { shape, maskedPageBounds, opacity } of renderingShapes) {
|
||||
for (const { shape, opacity } of renderingShapes) {
|
||||
const maskedPageBounds = editor.getShapeMaskedPageBounds(shape)
|
||||
if (!maskedPageBounds) continue
|
||||
ctx.save()
|
||||
|
||||
|
|
|
@ -22,9 +22,11 @@ export function useChangedShapesReactor(
|
|||
if (!beforeInfo) {
|
||||
continue
|
||||
} else {
|
||||
if (afterInfo.isCulled && !beforeInfo.isCulled) {
|
||||
const isAfterCulled = editor.isShapeCulled(afterInfo.id)
|
||||
const isBeforeCulled = editor.isShapeCulled(beforeInfo.id)
|
||||
if (isAfterCulled && !isBeforeCulled) {
|
||||
culled.push(afterInfo.shape)
|
||||
} else if (!afterInfo.isCulled && beforeInfo.isCulled) {
|
||||
} else if (!isAfterCulled && isBeforeCulled) {
|
||||
restored.push(afterInfo.shape)
|
||||
}
|
||||
beforeToVisit.delete(beforeInfo)
|
||||
|
|
|
@ -514,7 +514,7 @@ export function degreesToRadians(d: number): number;
|
|||
export const DOUBLE_CLICK_DURATION = 450;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DRAG_DISTANCE = 4;
|
||||
export const DRAG_DISTANCE = 16;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EASINGS: {
|
||||
|
@ -721,8 +721,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
index: number;
|
||||
backgroundIndex: number;
|
||||
opacity: number;
|
||||
isCulled: boolean;
|
||||
maskedPageBounds: Box | undefined;
|
||||
}[];
|
||||
getSelectedShapeAtPoint(point: VecLike): TLShape | undefined;
|
||||
getSelectedShapeIds(): TLShapeId[];
|
||||
|
@ -783,8 +781,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
index: number;
|
||||
backgroundIndex: number;
|
||||
opacity: number;
|
||||
isCulled: boolean;
|
||||
maskedPageBounds: Box | undefined;
|
||||
}[];
|
||||
getViewportPageBounds(): Box;
|
||||
getViewportPageCenter(): Vec;
|
||||
|
@ -821,6 +817,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
margin?: number | undefined;
|
||||
hitInside?: boolean | undefined;
|
||||
}): boolean;
|
||||
isShapeCulled(shape: TLShape | TLShapeId): boolean;
|
||||
isShapeInPage(shape: TLShape | TLShapeId, pageId?: TLPageId): boolean;
|
||||
isShapeOfType<T extends TLUnknownShape>(shape: TLUnknownShape, type: T['type']): shape is T;
|
||||
// (undocumented)
|
||||
|
|
|
@ -11991,16 +11991,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">;\n index: number;\n backgroundIndex: number;\n opacity: number;\n isCulled: boolean;\n maskedPageBounds: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Box",
|
||||
"canonicalReference": "@tldraw/editor!Box:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": " | undefined;\n }[]"
|
||||
"text": ">;\n index: number;\n backgroundIndex: number;\n opacity: number;\n }[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -12010,7 +12001,7 @@
|
|||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 12
|
||||
"endIndex": 10
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
|
@ -14801,6 +14792,64 @@
|
|||
"isAbstract": false,
|
||||
"name": "isPointInShape"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#isShapeCulled:member(1)",
|
||||
"docComment": "/**\n * Get whether the shape is culled or not.\n *\n * @param shape - The shape (or shape id) to get the culled info for.\n *\n * @example\n * ```ts\n * editor.isShapeCulled(myShape)\n * editor.isShapeCulled(myShapeId)\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "isShapeCulled(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": " | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShapeId",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "shape",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "isShapeCulled"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#isShapeInPage:member(1)",
|
||||
|
|
|
@ -1102,9 +1102,12 @@ input,
|
|||
}
|
||||
/* This part of the rule helps preserve the occlusion rules for the shapes so we
|
||||
* don't click on shapes that are behind other shapes.
|
||||
* One extra nuance is arrows which have weird geometry and just gets in the way.
|
||||
* One extra nuance is we don't use this behavior for:
|
||||
* - arrows which have weird geometry and just gets in the way.
|
||||
* - draw shapes, because it feels restrictive to have them be 'in the way' of clicking on a textfield
|
||||
*/
|
||||
.tl-canvas[data-iseditinganything='true'] .tl-shape:not([data-shape-type='arrow']) {
|
||||
.tl-canvas[data-iseditinganything='true']
|
||||
.tl-shape:not([data-shape-type='arrow']):not([data-shape-type='draw']) {
|
||||
pointer-events: all;
|
||||
}
|
||||
/* But, re-disable the pointer-events rule for the svg container. */
|
||||
|
|
|
@ -112,8 +112,9 @@ export function CulledShapes() {
|
|||
const shapeVertices = computed('shape vertices', function calculateCulledShapeVertices() {
|
||||
const results: number[] = []
|
||||
|
||||
for (const { isCulled, maskedPageBounds } of editor.getRenderingShapes()) {
|
||||
if (isCulled && maskedPageBounds) {
|
||||
for (const { id } of editor.getUnorderedRenderingShapes(true)) {
|
||||
const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
|
||||
if (editor.isShapeCulled(id) && maskedPageBounds) {
|
||||
results.push(
|
||||
// triangle 1
|
||||
maskedPageBounds.minX,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useQuickReactor, useStateTracking } from '@tldraw/state'
|
||||
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import { memo, useCallback, useLayoutEffect, useRef } from 'react'
|
||||
import { memo, useCallback, useRef } from 'react'
|
||||
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
|
||||
import { useEditor } from '../hooks/useEditor'
|
||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||
|
@ -26,7 +26,6 @@ export const Shape = memo(function Shape({
|
|||
index,
|
||||
backgroundIndex,
|
||||
opacity,
|
||||
isCulled,
|
||||
dprMultiple,
|
||||
}: {
|
||||
id: TLShapeId
|
||||
|
@ -35,7 +34,6 @@ export const Shape = memo(function Shape({
|
|||
index: number
|
||||
backgroundIndex: number
|
||||
opacity: number
|
||||
isCulled: boolean
|
||||
dprMultiple: number
|
||||
}) {
|
||||
const editor = useEditor()
|
||||
|
@ -120,13 +118,18 @@ export const Shape = memo(function Shape({
|
|||
[opacity, index, backgroundIndex]
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current
|
||||
const bgContainer = bgContainerRef.current
|
||||
setStyleProperty(container, 'display', isCulled ? 'none' : 'block')
|
||||
setStyleProperty(bgContainer, 'display', isCulled ? 'none' : 'block')
|
||||
}, [isCulled])
|
||||
useQuickReactor(
|
||||
'set display',
|
||||
() => {
|
||||
const shape = editor.getShape(id)
|
||||
if (!shape) return // probably the shape was just deleted
|
||||
|
||||
const isCulled = editor.isShapeCulled(shape)
|
||||
setStyleProperty(containerRef.current, 'display', isCulled ? 'none' : 'block')
|
||||
setStyleProperty(bgContainerRef.current, 'display', isCulled ? 'none' : 'block')
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
const annotateError = useCallback(
|
||||
(error: any) => editor.annotateError(error, { origin: 'shape', willCrashApp: false }),
|
||||
[editor]
|
||||
|
|
|
@ -403,6 +403,7 @@ function SelectedIdIndicators() {
|
|||
'select.idle',
|
||||
'select.brushing',
|
||||
'select.scribble_brushing',
|
||||
'select.editing_shape',
|
||||
'select.pointing_shape',
|
||||
'select.pointing_selection',
|
||||
'select.pointing_handle'
|
||||
|
|
|
@ -34,10 +34,10 @@ export const DOUBLE_CLICK_DURATION = 450
|
|||
export const MULTI_CLICK_DURATION = 200
|
||||
|
||||
/** @internal */
|
||||
export const COARSE_DRAG_DISTANCE = 6
|
||||
export const COARSE_DRAG_DISTANCE = 36 // 6 squared
|
||||
|
||||
/** @internal */
|
||||
export const DRAG_DISTANCE = 4
|
||||
export const DRAG_DISTANCE = 16 // 4 squared
|
||||
|
||||
/** @internal */
|
||||
export const SVG_PADDING = 32
|
||||
|
|
|
@ -3124,48 +3124,26 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
index: number
|
||||
backgroundIndex: number
|
||||
opacity: number
|
||||
isCulled: boolean
|
||||
maskedPageBounds: Box | undefined
|
||||
}[] = []
|
||||
|
||||
let nextIndex = MAX_SHAPES_PER_PAGE * 2
|
||||
let nextBackgroundIndex = MAX_SHAPES_PER_PAGE
|
||||
|
||||
// We only really need these if we're using editor state, but that's ok
|
||||
const editingShapeId = this.getEditingShapeId()
|
||||
const selectedShapeIds = this.getSelectedShapeIds()
|
||||
const erasingShapeIds = this.getErasingShapeIds()
|
||||
const renderingBoundsExpanded = this.getRenderingBoundsExpanded()
|
||||
|
||||
// If renderingBoundsMargin is set to Infinity, then we won't cull offscreen shapes
|
||||
const isCullingOffScreenShapes = Number.isFinite(this.renderingBoundsMargin)
|
||||
|
||||
const addShapeById = (id: TLShapeId, opacity: number, isAncestorErasing: boolean) => {
|
||||
const shape = this.getShape(id)
|
||||
if (!shape) return
|
||||
|
||||
opacity *= shape.opacity
|
||||
let isCulled = false
|
||||
let isShapeErasing = false
|
||||
const util = this.getShapeUtil(shape)
|
||||
const maskedPageBounds = this.getShapeMaskedPageBounds(id)
|
||||
|
||||
if (useEditorState) {
|
||||
isShapeErasing = !isAncestorErasing && erasingShapeIds.includes(id)
|
||||
if (isShapeErasing) {
|
||||
opacity *= 0.32
|
||||
}
|
||||
|
||||
isCulled =
|
||||
isCullingOffScreenShapes &&
|
||||
// never cull editingg shapes
|
||||
editingShapeId !== id &&
|
||||
// if the shape is fully outside of its parent's clipping bounds...
|
||||
(maskedPageBounds === undefined ||
|
||||
// ...or if the shape is outside of the expanded viewport bounds...
|
||||
(!renderingBoundsExpanded.includes(maskedPageBounds) &&
|
||||
// ...and if it's not selected... then cull it
|
||||
!selectedShapeIds.includes(id)))
|
||||
}
|
||||
|
||||
renderingShapes.push({
|
||||
|
@ -3175,8 +3153,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
index: nextIndex,
|
||||
backgroundIndex: nextBackgroundIndex,
|
||||
opacity,
|
||||
isCulled,
|
||||
maskedPageBounds,
|
||||
})
|
||||
|
||||
nextIndex += 1
|
||||
|
@ -4270,6 +4246,51 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
return this.isShapeOrAncestorLocked(this.getShapeParent(shape))
|
||||
}
|
||||
|
||||
@computed
|
||||
private _getShapeCullingInfoCache(): ComputedCache<boolean, TLShape> {
|
||||
return this.store.createComputedCache(
|
||||
'shapeCullingInfo',
|
||||
({ id }) => {
|
||||
// We don't cull shapes that are being edited
|
||||
if (this.getEditingShapeId() === id) return false
|
||||
|
||||
const maskedPageBounds = this.getShapeMaskedPageBounds(id)
|
||||
// if the shape is fully outside of its parent's clipping bounds...
|
||||
if (maskedPageBounds === undefined) return true
|
||||
|
||||
// We don't cull selected shapes
|
||||
if (this.getSelectedShapeIds().includes(id)) return false
|
||||
const renderingBoundsExpanded = this.getRenderingBoundsExpanded()
|
||||
// the shape is outside of the expanded viewport bounds...
|
||||
return !renderingBoundsExpanded.includes(maskedPageBounds)
|
||||
},
|
||||
(a, b) => this.getShapeMaskedPageBounds(a) === this.getShapeMaskedPageBounds(b)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the shape is culled or not.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* editor.isShapeCulled(myShape)
|
||||
* editor.isShapeCulled(myShapeId)
|
||||
* ```
|
||||
*
|
||||
* @param shape - The shape (or shape id) to get the culled info for.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
isShapeCulled(shape: TLShape | TLShapeId): boolean {
|
||||
// If renderingBoundsMargin is set to Infinity, then we won't cull offscreen shapes
|
||||
const isCullingOffScreenShapes = Number.isFinite(this.renderingBoundsMargin)
|
||||
if (!isCullingOffScreenShapes) return false
|
||||
|
||||
const id = typeof shape === 'string' ? shape : shape.id
|
||||
|
||||
return this._getShapeCullingInfoCache().get(id)! as boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The bounds of the current page (the common bounds of all of the shapes on the page).
|
||||
*
|
||||
|
@ -4641,8 +4662,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
@computed getCurrentPageRenderingShapesSorted(): TLShape[] {
|
||||
return this.getRenderingShapes()
|
||||
.filter(({ isCulled }) => !isCulled)
|
||||
return this.getUnorderedRenderingShapes(true)
|
||||
.filter(({ id }) => !this.isShapeCulled(id))
|
||||
.sort((a, b) => a.index - b.index)
|
||||
.map(({ shape }) => shape)
|
||||
}
|
||||
|
@ -8579,7 +8600,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
if (
|
||||
!inputs.isDragging &&
|
||||
inputs.isPointing &&
|
||||
originPagePoint.dist(currentPagePoint) >
|
||||
Vec.Dist2(originPagePoint, currentPagePoint) >
|
||||
(this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) /
|
||||
this.getZoomLevel()
|
||||
) {
|
||||
|
@ -8669,7 +8690,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
if (
|
||||
!inputs.isDragging &&
|
||||
inputs.isPointing &&
|
||||
originPagePoint.dist(currentPagePoint) >
|
||||
Vec.Dist2(originPagePoint, currentPagePoint) >
|
||||
(this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) /
|
||||
this.getZoomLevel()
|
||||
) {
|
||||
|
|
|
@ -38,7 +38,8 @@ export async function getSvgJsx(
|
|||
if (opts.bounds) {
|
||||
bbox = opts.bounds
|
||||
} else {
|
||||
for (const { maskedPageBounds } of renderingShapes) {
|
||||
for (const { id } of renderingShapes) {
|
||||
const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
|
||||
if (!maskedPageBounds) continue
|
||||
if (bbox) {
|
||||
bbox.union(maskedPageBounds)
|
||||
|
|
|
@ -227,7 +227,7 @@ export class ClickManager {
|
|||
if (
|
||||
this._clickState !== 'idle' &&
|
||||
this._clickScreenPoint &&
|
||||
this._clickScreenPoint.dist(this.editor.inputs.currentScreenPoint) >
|
||||
Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
|
||||
(this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
|
||||
) {
|
||||
this.cancelDoubleClickTimeout()
|
||||
|
|
|
@ -309,18 +309,20 @@ export class Vec {
|
|||
return new Vec(A.y, -A.x)
|
||||
}
|
||||
|
||||
static Dist2(A: VecLike, B: VecLike): number {
|
||||
return (A.x - B.x) ** 2 + (A.y - B.y) ** 2
|
||||
}
|
||||
|
||||
static Abs(A: VecLike): Vec {
|
||||
return new Vec(Math.abs(A.x), Math.abs(A.y))
|
||||
}
|
||||
|
||||
// Get the distance between two points.
|
||||
static Dist(A: VecLike, B: VecLike): number {
|
||||
return Math.hypot(A.y - B.y, A.x - B.x)
|
||||
}
|
||||
|
||||
// Get the squared distance between two points. This is faster to calculate (no square root) so useful for "minimum distance" checks where the actual measurement does not matter.
|
||||
static Dist2(A: VecLike, B: VecLike): number {
|
||||
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dot product of two vectors which is used to calculate the angle between them.
|
||||
*/
|
||||
|
|
|
@ -52,15 +52,16 @@ export class Arc2d extends Geometry2d {
|
|||
// Get the point (P) on the arc, then pick the nearest of A, B, and P
|
||||
const P = _center.clone().add(point.clone().sub(_center).uni().mul(radius))
|
||||
|
||||
let distance = Infinity
|
||||
let nearest: Vec | undefined
|
||||
for (const pt of [A, B, P]) {
|
||||
if (point.dist(pt) < distance) {
|
||||
nearest = pt
|
||||
distance = point.dist(pt)
|
||||
let dist = Infinity
|
||||
let d: number
|
||||
for (const p of [A, B, P]) {
|
||||
d = Vec.Dist2(point, p)
|
||||
if (d < dist) {
|
||||
nearest = p
|
||||
dist = d
|
||||
}
|
||||
}
|
||||
|
||||
if (!nearest) throw Error('nearest point not found')
|
||||
return nearest
|
||||
}
|
||||
|
|
|
@ -55,9 +55,11 @@ export class CubicBezier2d extends Polyline2d {
|
|||
nearestPoint(A: Vec): Vec {
|
||||
let nearest: Vec | undefined
|
||||
let dist = Infinity
|
||||
let d: number
|
||||
let p: Vec
|
||||
for (const edge of this.segments) {
|
||||
const p = edge.nearestPoint(A)
|
||||
const d = p.dist(A)
|
||||
p = edge.nearestPoint(A)
|
||||
d = Vec.Dist2(p, A)
|
||||
if (d < dist) {
|
||||
nearest = p
|
||||
dist = d
|
||||
|
|
|
@ -67,15 +67,16 @@ export class CubicSpline2d extends Geometry2d {
|
|||
nearestPoint(A: Vec): Vec {
|
||||
let nearest: Vec | undefined
|
||||
let dist = Infinity
|
||||
let d: number
|
||||
let p: Vec
|
||||
for (const segment of this.segments) {
|
||||
const p = segment.nearestPoint(A)
|
||||
const d = p.dist(A)
|
||||
p = segment.nearestPoint(A)
|
||||
d = Vec.Dist2(p, A)
|
||||
if (d < dist) {
|
||||
nearest = p
|
||||
dist = d
|
||||
}
|
||||
}
|
||||
|
||||
if (!nearest) throw Error('nearest point not found')
|
||||
return nearest
|
||||
}
|
||||
|
|
|
@ -76,15 +76,16 @@ export class Ellipse2d extends Geometry2d {
|
|||
nearestPoint(A: Vec): Vec {
|
||||
let nearest: Vec | undefined
|
||||
let dist = Infinity
|
||||
let d: number
|
||||
let p: Vec
|
||||
for (const edge of this.edges) {
|
||||
const p = edge.nearestPoint(A)
|
||||
const d = p.dist(A)
|
||||
p = edge.nearestPoint(A)
|
||||
d = Vec.Dist2(p, A)
|
||||
if (d < dist) {
|
||||
nearest = p
|
||||
dist = d
|
||||
}
|
||||
}
|
||||
|
||||
if (!nearest) throw Error('nearest point not found')
|
||||
return nearest
|
||||
}
|
||||
|
|
|
@ -55,14 +55,17 @@ export abstract class Geometry2d {
|
|||
}
|
||||
|
||||
nearestPointOnLineSegment(A: Vec, B: Vec): Vec {
|
||||
let distance = Infinity
|
||||
const { vertices } = this
|
||||
let nearest: Vec | undefined
|
||||
for (let i = 0; i < this.vertices.length; i++) {
|
||||
const point = this.vertices[i]
|
||||
const d = Vec.DistanceToLineSegment(A, B, point)
|
||||
if (d < distance) {
|
||||
distance = d
|
||||
nearest = point
|
||||
let dist = Infinity
|
||||
let d: number
|
||||
let p: Vec
|
||||
for (let i = 0; i < vertices.length; i++) {
|
||||
p = vertices[i]
|
||||
d = Vec.DistanceToLineSegment(A, B, p)
|
||||
if (d < dist) {
|
||||
dist = d
|
||||
nearest = p
|
||||
}
|
||||
}
|
||||
if (!nearest) throw Error('nearest point not found')
|
||||
|
|
|
@ -30,8 +30,8 @@ export class Group2d extends Geometry2d {
|
|||
}
|
||||
|
||||
override nearestPoint(point: Vec): Vec {
|
||||
let d = Infinity
|
||||
let p: Vec | undefined
|
||||
let dist = Infinity
|
||||
let nearest: Vec | undefined
|
||||
|
||||
const { children } = this
|
||||
|
||||
|
@ -39,16 +39,18 @@ export class Group2d extends Geometry2d {
|
|||
throw Error('no children')
|
||||
}
|
||||
|
||||
let p: Vec
|
||||
let d: number
|
||||
for (const child of children) {
|
||||
const nearest = child.nearestPoint(point)
|
||||
const dist = nearest.dist(point)
|
||||
if (dist < d) {
|
||||
d = dist
|
||||
p = nearest
|
||||
p = child.nearestPoint(point)
|
||||
d = Vec.Dist2(p, point)
|
||||
if (d < dist) {
|
||||
dist = d
|
||||
nearest = p
|
||||
}
|
||||
}
|
||||
if (!p) throw Error('nearest point not found')
|
||||
return p
|
||||
if (!nearest) throw Error('nearest point not found')
|
||||
return nearest
|
||||
}
|
||||
|
||||
override distanceToPoint(point: Vec, hitInside = false) {
|
||||
|
|
|
@ -51,18 +51,17 @@ export class Polyline2d extends Geometry2d {
|
|||
const { segments } = this
|
||||
let nearest = this.points[0]
|
||||
let dist = Infinity
|
||||
|
||||
let p: Vec // current point on segment
|
||||
let d: number // distance from A to p
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
p = segments[i].nearestPoint(A)
|
||||
d = p.dist(A)
|
||||
d = Vec.Dist2(p, A)
|
||||
if (d < dist) {
|
||||
nearest = p
|
||||
dist = d
|
||||
}
|
||||
}
|
||||
|
||||
if (!nearest) throw Error('nearest point not found')
|
||||
return nearest
|
||||
}
|
||||
|
||||
|
|
|
@ -4231,7 +4231,7 @@
|
|||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/store!Store#onBeforeChange:member",
|
||||
"docComment": "/**\n * A callback before after each record's change.\n *\n * @param prev - The previous value, if any.\n *\n * @param next - The next value.\n */\n",
|
||||
"docComment": "/**\n * A callback fired before each record's change.\n *\n * @param prev - The previous value, if any.\n *\n * @param next - The next value.\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
|
|
@ -318,7 +318,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
onAfterCreate?: (record: R, source: 'remote' | 'user') => void
|
||||
|
||||
/**
|
||||
* A callback before after each record's change.
|
||||
* A callback fired before each record's change.
|
||||
*
|
||||
* @param prev - The previous value, if any.
|
||||
* @param next - The next value.
|
||||
|
|
|
@ -87,7 +87,6 @@ export const TldrawSelectionForeground = track(function TldrawSelectionForegroun
|
|||
'select.scribble_brushing',
|
||||
'select.pointing_canvas',
|
||||
'select.pointing_selection',
|
||||
'select.editing_shape',
|
||||
'select.pointing_shape',
|
||||
'select.crop.idle',
|
||||
'select.crop.pointing_crop',
|
||||
|
|
|
@ -309,7 +309,7 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
const hasMovedFarEnough =
|
||||
Vec.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
|
||||
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
|
||||
|
||||
// Find the distance from where the pointer was when shift was released and
|
||||
// where it is now; if it's far enough away, then update the page point where
|
||||
|
@ -382,7 +382,7 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
const hasMovedFarEnough =
|
||||
Vec.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
|
||||
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
|
||||
|
||||
// Find the distance from where the pointer was when shift was released and
|
||||
// where it is now; if it's far enough away, then update the page point where
|
||||
|
|
|
@ -160,9 +160,11 @@ export function useEditableText(id: TLShapeId, type: string, text: string) {
|
|||
|
||||
// This is important so that when dragging a shape using the text label,
|
||||
// the shape continues to be dragged, even if the cursor is over the UI.
|
||||
setPointerCapture(e.currentTarget, e)
|
||||
if (!isEditing) {
|
||||
setPointerCapture(e.currentTarget, e)
|
||||
}
|
||||
},
|
||||
[editor, id]
|
||||
[editor, id, isEditing]
|
||||
)
|
||||
|
||||
const handleDoubleClick = stopEventPropagation
|
||||
|
|
|
@ -38,9 +38,8 @@ export class PointingShape extends StateNode {
|
|||
outermostSelectingShape.id === focusedGroupId ||
|
||||
// ...or if the shape is within the selection
|
||||
selectedShapeIds.includes(outermostSelectingShape.id) ||
|
||||
// ...or if an ancestor of the shape is selected (except note shapes)...
|
||||
// todo: Consider adding a flag for this hardcoded behaviour
|
||||
(selectedAncestor && selectedAncestor.type !== 'note') ||
|
||||
// ...or if an ancestor of the shape is selected
|
||||
selectedAncestor ||
|
||||
// ...or if the current point is NOT within the selection bounds
|
||||
(selectedShapeIds.length > 1 && selectionBounds?.containsPoint(currentPagePoint))
|
||||
) {
|
||||
|
|
|
@ -19,9 +19,10 @@ export function BackToContent() {
|
|||
|
||||
// Rendering shapes includes all the shapes in the current page.
|
||||
// We have to filter them down to just the shapes that are inside the renderingBounds.
|
||||
const visibleShapes = renderingShapes.filter(
|
||||
(s) => s.maskedPageBounds && renderingBounds.includes(s.maskedPageBounds)
|
||||
)
|
||||
const visibleShapes = renderingShapes.filter((s) => {
|
||||
const maskedPageBounds = editor.getShapeMaskedPageBounds(s.id)
|
||||
return maskedPageBounds && renderingBounds.includes(maskedPageBounds)
|
||||
})
|
||||
const showBackToContentNow =
|
||||
visibleShapes.length === 0 && editor.getCurrentPageShapes().length > 0
|
||||
|
||||
|
|
|
@ -63,42 +63,50 @@ it('updates the rendering viewport when the camera stops moving', () => {
|
|||
it('lists shapes in viewport', () => {
|
||||
const ids = createShapes()
|
||||
editor.selectNone()
|
||||
expect(editor.getRenderingShapes().map(({ id, isCulled }) => [id, isCulled])).toStrictEqual([
|
||||
[ids.A, false], // A is within the expanded rendering bounds, so should not be culled; and it's in the regular viewport too, so it's on screen.
|
||||
[ids.B, false],
|
||||
[ids.C, false],
|
||||
[ids.D, true], // D is clipped and so should always be culled / outside of viewport
|
||||
])
|
||||
expect(editor.getRenderingShapes().map(({ id }) => [id, editor.isShapeCulled(id)])).toStrictEqual(
|
||||
[
|
||||
[ids.A, false], // A is within the expanded rendering bounds, so should not be culled; and it's in the regular viewport too, so it's on screen.
|
||||
[ids.B, false],
|
||||
[ids.C, false],
|
||||
[ids.D, true], // D is clipped and so should always be culled / outside of viewport
|
||||
]
|
||||
)
|
||||
|
||||
// Move the camera 201 pixels to the right and 201 pixels down
|
||||
editor.pan({ x: -201, y: -201 })
|
||||
jest.advanceTimersByTime(500)
|
||||
|
||||
expect(editor.getRenderingShapes().map(({ id, isCulled }) => [id, isCulled])).toStrictEqual([
|
||||
[ids.A, false], // A should not be culled, even though it's no longer in the viewport (because it's still in the EXPANDED viewport)
|
||||
[ids.B, false],
|
||||
[ids.C, false],
|
||||
[ids.D, true], // D is clipped and so should always be culled / outside of viewport
|
||||
])
|
||||
expect(editor.getRenderingShapes().map(({ id }) => [id, editor.isShapeCulled(id)])).toStrictEqual(
|
||||
[
|
||||
[ids.A, false], // A should not be culled, even though it's no longer in the viewport (because it's still in the EXPANDED viewport)
|
||||
[ids.B, false],
|
||||
[ids.C, false],
|
||||
[ids.D, true], // D is clipped and so should always be culled / outside of viewport
|
||||
]
|
||||
)
|
||||
|
||||
editor.pan({ x: -100, y: -100 })
|
||||
jest.advanceTimersByTime(500)
|
||||
|
||||
expect(editor.getRenderingShapes().map(({ id, isCulled }) => [id, isCulled])).toStrictEqual([
|
||||
[ids.A, true], // A should be culled now that it's outside of the expanded viewport too
|
||||
[ids.B, false],
|
||||
[ids.C, false],
|
||||
[ids.D, true], // D is clipped and so should always be culled, even if it's in the viewport
|
||||
])
|
||||
expect(editor.getRenderingShapes().map(({ id }) => [id, editor.isShapeCulled(id)])).toStrictEqual(
|
||||
[
|
||||
[ids.A, true], // A should be culled now that it's outside of the expanded viewport too
|
||||
[ids.B, false],
|
||||
[ids.C, false],
|
||||
[ids.D, true], // D is clipped and so should always be culled, even if it's in the viewport
|
||||
]
|
||||
)
|
||||
|
||||
editor.pan({ x: -900, y: -900 })
|
||||
jest.advanceTimersByTime(500)
|
||||
expect(editor.getRenderingShapes().map(({ id, isCulled }) => [id, isCulled])).toStrictEqual([
|
||||
[ids.A, true],
|
||||
[ids.B, true],
|
||||
[ids.C, true],
|
||||
[ids.D, true],
|
||||
])
|
||||
expect(editor.getRenderingShapes().map(({ id }) => [id, editor.isShapeCulled(id)])).toStrictEqual(
|
||||
[
|
||||
[ids.A, true],
|
||||
[ids.B, true],
|
||||
[ids.C, true],
|
||||
[ids.D, true],
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
it('lists shapes in viewport sorted by id with correct indexes & background indexes', () => {
|
||||
|
|
|
@ -176,6 +176,12 @@ export function mapObjectMapValues<Key extends string, ValueBefore, ValueAfter>(
|
|||
[K in Key]: ValueAfter;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
export function measureCbDuration(name: string, cb: () => any): any;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function measureDuration(_target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor;
|
||||
|
||||
// @public
|
||||
export class MediaHelpers {
|
||||
static getImageSize(blob: Blob): Promise<{
|
||||
|
|
|
@ -36,6 +36,7 @@ export {
|
|||
objectMapKeys,
|
||||
objectMapValues,
|
||||
} from './lib/object'
|
||||
export { measureCbDuration, measureDuration } from './lib/perf'
|
||||
export { PngHelpers } from './lib/png'
|
||||
export { type IndexKey } from './lib/reordering/IndexKey'
|
||||
export {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/** @internal */
|
||||
export function measureCbDuration(name: string, cb: () => any) {
|
||||
const now = performance.now()
|
||||
const result = cb()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${name} took`, performance.now() - now, 'ms')
|
||||
return result
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function measureDuration(_target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
const originalMethod = descriptor.value
|
||||
descriptor.value = function (...args: any[]) {
|
||||
const start = performance.now()
|
||||
const result = originalMethod.apply(this, args)
|
||||
const end = performance.now()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${propertyKey} took ${end - start}ms `)
|
||||
return result
|
||||
}
|
||||
return descriptor
|
||||
}
|
Ładowanie…
Reference in New Issue