[feature] snapping (#168)
* defer cloning * basic snapping * Improves algorithm for snap points, rendering * Improves snapping, snaplines * Enables a clone to snap to its parent * Adds overlay * Fix overlay, zoom scaling for distance and speed
This commit is contained in:
parent
b1b9f901d3
commit
0cfc68b004
38 changed files with 799 additions and 222 deletions
|
@ -41,7 +41,7 @@
|
|||
"@types/react-dom": "^16.9.9",
|
||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||
"@typescript-eslint/parser": "^4.30.0",
|
||||
"esbuild": "^0.12.24",
|
||||
"esbuild": "^0.13.8",
|
||||
"eslint": "^7.32.0",
|
||||
"lerna": "^4.0.0",
|
||||
"react": ">=16.8",
|
||||
|
|
|
@ -6,10 +6,10 @@ const name = process.env.npm_package_name || ''
|
|||
async function main() {
|
||||
esbuild.build({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outdir: 'dist/cjs',
|
||||
outdir: 'dist/esm',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
format: 'esm',
|
||||
target: 'es6',
|
||||
jsxFactory: 'React.createElement',
|
||||
jsxFragment: 'React.Fragment',
|
||||
|
|
|
@ -13,10 +13,10 @@ async function main() {
|
|||
|
||||
esbuild.build({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outdir: 'dist/cjs',
|
||||
outdir: 'dist/esm',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
format: 'esm',
|
||||
target: 'es6',
|
||||
jsxFactory: 'React.createElement',
|
||||
jsxFragment: 'React.Fragment',
|
||||
|
|
|
@ -38,8 +38,10 @@ export const Bounds = React.memo(
|
|||
const smallDimension = Math.min(bounds.width, bounds.height) * zoom
|
||||
// If the bounds are small, don't show the rotate handle
|
||||
const showRotateHandle = !isHidden && !isLocked && smallDimension > 32
|
||||
// If the bounds are very small, don't show the corner handles
|
||||
const showHandles = !isHidden && !isLocked && smallDimension > 16
|
||||
// If the bounds are very small, don't show the edge handles
|
||||
const showEdgeHandles = !isHidden && !isLocked && smallDimension > 24
|
||||
// If the bounds are very very small, don't show the corner handles
|
||||
const showCornerHandles = !isHidden && !isLocked && smallDimension > 20
|
||||
|
||||
return (
|
||||
<Container bounds={bounds} rotation={rotation}>
|
||||
|
@ -50,62 +52,62 @@ export const Bounds = React.memo(
|
|||
size={size}
|
||||
bounds={bounds}
|
||||
edge={TLBoundsEdge.Top}
|
||||
isHidden={!showHandles}
|
||||
isHidden={!showEdgeHandles}
|
||||
/>
|
||||
<EdgeHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
edge={TLBoundsEdge.Right}
|
||||
isHidden={!showHandles}
|
||||
isHidden={!showEdgeHandles}
|
||||
/>
|
||||
<EdgeHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
edge={TLBoundsEdge.Bottom}
|
||||
isHidden={!showHandles}
|
||||
isHidden={!showEdgeHandles}
|
||||
/>
|
||||
<EdgeHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
edge={TLBoundsEdge.Left}
|
||||
isHidden={!showHandles}
|
||||
isHidden={!showEdgeHandles}
|
||||
/>
|
||||
<CornerHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
isHidden={isHidden}
|
||||
isHidden={isHidden || !showCornerHandles}
|
||||
corner={TLBoundsCorner.TopLeft}
|
||||
/>
|
||||
<CornerHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
isHidden={isHidden}
|
||||
isHidden={isHidden || !showCornerHandles}
|
||||
corner={TLBoundsCorner.TopRight}
|
||||
/>
|
||||
<CornerHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
isHidden={isHidden}
|
||||
isHidden={isHidden || !showCornerHandles}
|
||||
corner={TLBoundsCorner.BottomRight}
|
||||
/>
|
||||
<CornerHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
isHidden={isHidden}
|
||||
isHidden={isHidden || !showCornerHandles}
|
||||
corner={TLBoundsCorner.BottomLeft}
|
||||
/>
|
||||
<RotateHandle
|
||||
targetSize={targetSize}
|
||||
size={size}
|
||||
bounds={bounds}
|
||||
isHidden={!showHandles || !showRotateHandle}
|
||||
isHidden={!showEdgeHandles || !showRotateHandle}
|
||||
/>
|
||||
{showCloneButtons && <CloneButtons bounds={bounds} />}
|
||||
</SVGContainer>
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
useCameraCss,
|
||||
useKeyEvents,
|
||||
} from '+hooks'
|
||||
import type { TLBinding, TLPage, TLPageState, TLShape, TLUsers } from '+types'
|
||||
import type { TLBinding, TLPage, TLPageState, TLShape, TLSnapLine, TLUsers } from '+types'
|
||||
import { ErrorFallback } from '+components/error-fallback'
|
||||
import { ErrorBoundary } from '+components/error-boundary'
|
||||
import { Brush } from '+components/brush'
|
||||
|
@ -17,6 +17,8 @@ import { Users } from '+components/users'
|
|||
import { useResizeObserver } from '+hooks/useResizeObserver'
|
||||
import { inputs } from '+inputs'
|
||||
import { UsersIndicators } from '+components/users-indicators'
|
||||
import { SnapLines } from '+components/snap-lines/snap-lines'
|
||||
import { Overlay } from '+components/overlay'
|
||||
|
||||
function resetError() {
|
||||
void null
|
||||
|
@ -25,6 +27,7 @@ function resetError() {
|
|||
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||
page: TLPage<T, TLBinding>
|
||||
pageState: TLPageState
|
||||
snapLines?: TLSnapLine[]
|
||||
users?: TLUsers<T>
|
||||
userId?: string
|
||||
hideBounds?: boolean
|
||||
|
@ -39,6 +42,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
id,
|
||||
page,
|
||||
pageState,
|
||||
snapLines,
|
||||
users,
|
||||
userId,
|
||||
meta,
|
||||
|
@ -87,6 +91,9 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
{users && <Users userId={userId} users={users} />}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
<Overlay camera={pageState.camera}>
|
||||
{snapLines && <SnapLines snapLines={snapLines} />}
|
||||
</Overlay>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
1
packages/core/src/components/overlay/index.ts
Normal file
1
packages/core/src/components/overlay/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './overlay'
|
24
packages/core/src/components/overlay/overlay.tsx
Normal file
24
packages/core/src/components/overlay/overlay.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function Overlay({
|
||||
camera,
|
||||
children,
|
||||
}: {
|
||||
camera: { point: number[]; zoom: number }
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const l = 2.5 / camera.zoom
|
||||
return (
|
||||
<svg className="tl-overlay">
|
||||
<defs>
|
||||
<g id="tl-snap-point">
|
||||
<path
|
||||
className="tl-snap-point"
|
||||
d={`M ${-l},${-l} L ${l},${l} M ${-l},${l} L ${l},${-l}`}
|
||||
/>
|
||||
</g>
|
||||
</defs>
|
||||
<g transform={`scale(${camera.zoom}) translate(${camera.point})`}>{children}</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -12,7 +12,7 @@ import type {
|
|||
import { Canvas } from '../canvas'
|
||||
import { Inputs } from '../../inputs'
|
||||
import { useTLTheme, TLContext, TLContextType } from '../../hooks'
|
||||
import type { TLShapeUtil, TLUsers } from '+index'
|
||||
import type { TLShapeUtil, TLSnapLine, TLUsers } from '+index'
|
||||
|
||||
export interface RendererProps<T extends TLShape, E extends Element = any, M = any>
|
||||
extends Partial<TLCallbacks<T>> {
|
||||
|
@ -40,6 +40,10 @@ export interface RendererProps<T extends TLShape, E extends Element = any, M = a
|
|||
* (optional) The current users to render.
|
||||
*/
|
||||
users?: TLUsers<T>
|
||||
/**
|
||||
* (optional) The current snap lines to render.
|
||||
*/
|
||||
snapLines?: TLSnapLine[]
|
||||
/**
|
||||
* (optional) The current user's id, used to identify the user.
|
||||
*/
|
||||
|
@ -97,6 +101,7 @@ export function Renderer<T extends TLShape, E extends Element, M extends Record<
|
|||
userId,
|
||||
theme,
|
||||
meta,
|
||||
snapLines,
|
||||
containerRef,
|
||||
hideHandles = false,
|
||||
hideIndicators = false,
|
||||
|
@ -132,6 +137,7 @@ export function Renderer<T extends TLShape, E extends Element, M extends Record<
|
|||
id={id}
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
snapLines={snapLines}
|
||||
users={users}
|
||||
userId={userId}
|
||||
hideBounds={hideBounds}
|
||||
|
|
1
packages/core/src/components/snap-lines/index.ts
Normal file
1
packages/core/src/components/snap-lines/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './snap-lines'
|
32
packages/core/src/components/snap-lines/snap-lines.tsx
Normal file
32
packages/core/src/components/snap-lines/snap-lines.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import * as React from 'react'
|
||||
import type { TLSnapLine } from '+types'
|
||||
import Utils from '+utils'
|
||||
|
||||
export function SnapLines({ snapLines }: { snapLines: TLSnapLine[] }) {
|
||||
return (
|
||||
<>
|
||||
{snapLines.map((snapLine, i) => (
|
||||
<SnapLine key={i} snapLine={snapLine} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function SnapLine({ snapLine }: { snapLine: TLSnapLine }) {
|
||||
const bounds = Utils.getBoundsFromPoints(snapLine)
|
||||
|
||||
return (
|
||||
<>
|
||||
<line
|
||||
className="tl-snap-line"
|
||||
x1={bounds.minX}
|
||||
y1={bounds.minY}
|
||||
x2={bounds.maxX}
|
||||
y2={bounds.maxY}
|
||||
/>
|
||||
{snapLine.map(([x, y], i) => (
|
||||
<use key={i} href="#tl-snap-point" x={x} y={y} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -69,6 +69,8 @@ export function useShapeEvents(id: string, disable = false) {
|
|||
if (!inputs.pointerIsValid(e)) return
|
||||
if (disable) return
|
||||
|
||||
e.stopPropagation()
|
||||
|
||||
if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
|
||||
|
||||
const info = inputs.pointerMove(e, id)
|
||||
|
|
|
@ -66,6 +66,7 @@ const css = (strings: TemplateStringsArray, ...args: unknown[]) =>
|
|||
)
|
||||
|
||||
const defaultTheme: TLTheme = {
|
||||
accent: 'rgb(255, 0, 0)',
|
||||
brushFill: 'rgba(0,0,0,.05)',
|
||||
brushStroke: 'rgba(0,0,0,.25)',
|
||||
selectStroke: 'rgb(66, 133, 244)',
|
||||
|
@ -133,6 +134,24 @@ const tlcss = css`
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tl-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tl-snap-line {
|
||||
stroke: var(--tl-accent);
|
||||
stroke-width: calc(1px * var(--tl-scale));
|
||||
}
|
||||
|
||||
.tl-snap-point {
|
||||
stroke: var(--tl-accent);
|
||||
stroke-width: calc(1px * var(--tl-scale));
|
||||
}
|
||||
|
||||
.tl-canvas {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
|
|
@ -42,6 +42,8 @@ export interface TLUser<T extends TLShape> {
|
|||
|
||||
export type TLUsers<T extends TLShape, U extends TLUser<T> = TLUser<T>> = Record<string, U>
|
||||
|
||||
export type TLSnapLine = number[][]
|
||||
|
||||
export interface TLHandle {
|
||||
id: string
|
||||
index: number
|
||||
|
@ -112,6 +114,7 @@ export interface TLBinding<M = any> {
|
|||
}
|
||||
|
||||
export interface TLTheme {
|
||||
accent?: string
|
||||
brushFill?: string
|
||||
brushStroke?: string
|
||||
selectFill?: string
|
||||
|
@ -242,6 +245,11 @@ export interface TLBounds {
|
|||
rotation?: number
|
||||
}
|
||||
|
||||
export interface TLBoundsWithCenter extends TLBounds {
|
||||
midX: number
|
||||
midY: number
|
||||
}
|
||||
|
||||
export type TLIntersection = {
|
||||
didIntersect: boolean
|
||||
message: string
|
||||
|
|
|
@ -6,7 +6,7 @@ import type React from 'react'
|
|||
import { TLBezierCurveSegment, TLBounds, TLBoundsCorner, TLBoundsEdge } from '../types'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import './polyfills'
|
||||
import type { Patch } from '+index'
|
||||
import type { Patch, TLBoundsWithCenter } from '+index'
|
||||
|
||||
export class Utils {
|
||||
/* -------------------------------------------------- */
|
||||
|
@ -1519,6 +1519,146 @@ left past the initial left edge) then swap points on that axis.
|
|||
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bounding box with a midX and midY.
|
||||
* @param bounds
|
||||
*/
|
||||
static getBoundsWithCenter(bounds: TLBounds): TLBounds & { midX: number; midY: number } {
|
||||
return {
|
||||
...bounds,
|
||||
midX: bounds.minX + bounds.width / 2,
|
||||
midY: bounds.minY + bounds.height / 2,
|
||||
}
|
||||
}
|
||||
|
||||
static getSnapPoints = (
|
||||
bounds: TLBoundsWithCenter,
|
||||
others: TLBoundsWithCenter[],
|
||||
distance: number,
|
||||
isCareful: boolean
|
||||
) => {
|
||||
const A = { ...bounds }
|
||||
|
||||
const offset = [0, 0]
|
||||
const snapLines: number[][][] = []
|
||||
|
||||
// 1.
|
||||
// Find the snap points for the x and y axes
|
||||
|
||||
let xs = null as { B: TLBoundsWithCenter; i: number } | null
|
||||
let ys = null as { B: TLBoundsWithCenter; i: number } | null
|
||||
|
||||
const fxs = [A.midX, A.minX, A.maxX]
|
||||
const fys = [A.midY, A.minY, A.maxY]
|
||||
|
||||
for (const B of others) {
|
||||
if (!xs) {
|
||||
const txs = [B.midX, B.minX, B.maxX]
|
||||
|
||||
fxs.forEach((f, i) =>
|
||||
txs.forEach((t, k) => {
|
||||
// If we're not dragging carefully, only snap to
|
||||
// matching points, (e.g. min to min, mid to mid)
|
||||
if (xs || !(isCareful || i === k)) return
|
||||
|
||||
if (Math.abs(t - f) < distance) {
|
||||
xs = { B, i }
|
||||
|
||||
offset[0] = [
|
||||
// How far to offset the delta on the x axis in
|
||||
// order to "snap" the selection to the right place
|
||||
A.midX - t,
|
||||
A.midX - (t + A.width / 2),
|
||||
A.midX - (t - A.width / 2),
|
||||
][i]
|
||||
|
||||
// Also apply the offset to the bounds
|
||||
A.minX -= offset[0]
|
||||
A.midX -= offset[0]
|
||||
A.maxX -= offset[0]
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (!ys) {
|
||||
const tys = [B.midY, B.minY, B.maxY]
|
||||
|
||||
fys.forEach((f, i) =>
|
||||
tys.forEach((t, k) => {
|
||||
if (ys || !(isCareful || i === k)) return
|
||||
|
||||
if (Math.abs(t - f) < distance) {
|
||||
ys = { B, i }
|
||||
|
||||
offset[1] = [
|
||||
//
|
||||
A.midY - t,
|
||||
A.midY - (t + A.height / 2),
|
||||
A.midY - (t - A.height / 2),
|
||||
][i]
|
||||
|
||||
A.minY -= offset[1]
|
||||
A.midY -= offset[1]
|
||||
A.maxY -= offset[1]
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (xs && ys) break
|
||||
}
|
||||
|
||||
// 2.
|
||||
// Calculate snap lines based on adjusted bounds A. This has
|
||||
// to happen after we've adjusted both dimensions x and y of
|
||||
// the bounds A!
|
||||
|
||||
if (xs) {
|
||||
const { i, B } = xs
|
||||
const x = [A.midX, A.minX, A.maxX][i % 3]
|
||||
|
||||
// If A is snapped at its center, show include only the midY;
|
||||
// otherwise, include both its minY and maxY.
|
||||
snapLines.push(
|
||||
i === 0
|
||||
? [
|
||||
[x, A.midY],
|
||||
[x, B.minY],
|
||||
[x, B.maxY],
|
||||
]
|
||||
: [
|
||||
[x, A.minY],
|
||||
[x, A.maxY],
|
||||
[x, B.minY],
|
||||
[x, B.maxY],
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
if (ys) {
|
||||
const { i, B } = ys
|
||||
const y = [A.midY, A.minY, A.maxY][i % 3]
|
||||
|
||||
snapLines.push(
|
||||
i === 0
|
||||
? [
|
||||
[A.midX, y],
|
||||
[B.minX, y],
|
||||
[B.maxX, y],
|
||||
]
|
||||
: [
|
||||
[A.minX, y],
|
||||
[A.maxX, y],
|
||||
[B.minX, y],
|
||||
[B.maxX, y],
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
return { offset, snapLines }
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Lists and Collections */
|
||||
/* -------------------------------------------------- */
|
||||
|
@ -1684,6 +1824,59 @@ left past the initial left edge) then swap points on that axis.
|
|||
/* Browser and DOM */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get balanced dash-strokearray and dash-strokeoffset properties for a path of a given length.
|
||||
* @param length The length of the path.
|
||||
* @param strokeWidth The shape's stroke-width property.
|
||||
* @param style The stroke's style: "dashed" or "dotted" (default "dashed").
|
||||
* @param snap An interval for dashes (e.g. 4 will produce arrays with 4, 8, 16, etc dashes).
|
||||
*/
|
||||
static getPerfectDashProps(
|
||||
length: number,
|
||||
strokeWidth: number,
|
||||
style: 'dashed' | 'dotted' | string,
|
||||
snap = 1,
|
||||
outset = true
|
||||
): {
|
||||
strokeDasharray: string
|
||||
strokeDashoffset: string
|
||||
} {
|
||||
let dashLength: number
|
||||
let strokeDashoffset: string
|
||||
let ratio: number
|
||||
|
||||
if (style.toLowerCase() === 'dashed') {
|
||||
dashLength = strokeWidth * 2
|
||||
ratio = 1
|
||||
strokeDashoffset = outset ? (dashLength / 2).toString() : '0'
|
||||
} else if (style.toLowerCase() === 'dotted') {
|
||||
dashLength = strokeWidth / 100
|
||||
ratio = 100
|
||||
strokeDashoffset = '0'
|
||||
} else {
|
||||
return {
|
||||
strokeDasharray: 'none',
|
||||
strokeDashoffset: 'none',
|
||||
}
|
||||
}
|
||||
|
||||
let dashes = Math.floor(length / dashLength / (2 * ratio))
|
||||
|
||||
dashes -= dashes % snap
|
||||
|
||||
dashes = Math.max(dashes, 4)
|
||||
|
||||
const gapLength = Math.max(
|
||||
dashLength,
|
||||
(length - dashes * dashLength) / (outset ? dashes : dashes - 1)
|
||||
)
|
||||
|
||||
return {
|
||||
strokeDasharray: [dashLength, gapLength].join(' '),
|
||||
strokeDashoffset,
|
||||
}
|
||||
}
|
||||
|
||||
static isMobileSize() {
|
||||
if (typeof window === 'undefined') return false
|
||||
return window.innerWidth < 768
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"@types/react-router-dom": "^5.1.8",
|
||||
"concurrently": "6.0.1",
|
||||
"create-serve": "1.0.1",
|
||||
"esbuild": "^0.12.26",
|
||||
"esbuild": "^0.13.8",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "4.2.3"
|
||||
},
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"@types/node": "^16.7.10",
|
||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||
"@typescript-eslint/parser": "^4.30.0",
|
||||
"esbuild": "^0.12.24",
|
||||
"esbuild": "^0.13.8",
|
||||
"eslint": "^7.32.0",
|
||||
"lerna": "^4.0.0",
|
||||
"ts-node": "^10.2.1",
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
"@types/react-dom": "^16.9.9",
|
||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||
"@typescript-eslint/parser": "^4.30.0",
|
||||
"esbuild": "^0.12.24",
|
||||
"esbuild": "^0.13.8",
|
||||
"eslint": "^7.32.0",
|
||||
"lerna": "^4.0.0",
|
||||
"ts-node": "^10.2.1",
|
||||
|
|
|
@ -6,10 +6,10 @@ const name = process.env.npm_package_name || ''
|
|||
async function main() {
|
||||
esbuild.build({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outdir: 'dist/cjs',
|
||||
outdir: 'dist/esm',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
format: 'esm',
|
||||
target: 'es6',
|
||||
jsxFactory: 'React.createElement',
|
||||
jsxFragment: 'React.Fragment',
|
||||
|
|
|
@ -13,10 +13,10 @@ async function main() {
|
|||
|
||||
esbuild.build({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outdir: 'dist/cjs',
|
||||
outdir: 'dist/esm',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
format: 'esm',
|
||||
target: 'es6',
|
||||
jsxFactory: 'React.createElement',
|
||||
jsxFragment: 'React.Fragment',
|
||||
|
|
|
@ -29,6 +29,8 @@ const isHideBoundsShapeSelector = (s: Data) => {
|
|||
|
||||
const pageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
|
||||
|
||||
const snapLinesSelector = (s: Data) => s.appState.snapLines
|
||||
|
||||
const usersSelector = (s: Data) => s.room?.users
|
||||
|
||||
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
|
||||
|
@ -149,6 +151,8 @@ function InnerTldraw({
|
|||
|
||||
const pageState = useSelector(pageStateSelector)
|
||||
|
||||
const snapLines = useSelector(snapLinesSelector)
|
||||
|
||||
const users = useSelector(usersSelector)
|
||||
|
||||
const isDarkMode = useSelector(isDarkModeSelector)
|
||||
|
@ -217,6 +221,7 @@ function InnerTldraw({
|
|||
containerRef={rWrapper}
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
snapLines={snapLines}
|
||||
users={users}
|
||||
userId={tlstate.state.room?.userId}
|
||||
shapeUtils={tldrawShapeUtils}
|
||||
|
|
|
@ -150,56 +150,3 @@ export const defaultStyle: ShapeStyles = {
|
|||
isFilled: false,
|
||||
dash: DashStyle.Draw,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get balanced dash-strokearray and dash-strokeoffset properties for a path of a given length.
|
||||
* @param length The length of the path.
|
||||
* @param strokeWidth The shape's stroke-width property.
|
||||
* @param style The stroke's style: "dashed" or "dotted" (default "dashed").
|
||||
* @param snap An interval for dashes (e.g. 4 will produce arrays with 4, 8, 16, etc dashes).
|
||||
*/
|
||||
export function getPerfectDashProps(
|
||||
length: number,
|
||||
strokeWidth: number,
|
||||
style: DashStyle,
|
||||
snap = 1,
|
||||
outset = true
|
||||
): {
|
||||
strokeDasharray: string
|
||||
strokeDashoffset: string
|
||||
} {
|
||||
let dashLength: number
|
||||
let strokeDashoffset: string
|
||||
let ratio: number
|
||||
|
||||
if (style === DashStyle.Solid || style === DashStyle.Draw) {
|
||||
return {
|
||||
strokeDasharray: 'none',
|
||||
strokeDashoffset: 'none',
|
||||
}
|
||||
} else if (style === DashStyle.Dashed) {
|
||||
dashLength = strokeWidth * 2
|
||||
ratio = 1
|
||||
strokeDashoffset = outset ? (dashLength / 2).toString() : '0'
|
||||
} else {
|
||||
dashLength = strokeWidth / 100
|
||||
ratio = 100
|
||||
strokeDashoffset = '0'
|
||||
}
|
||||
|
||||
let dashes = Math.floor(length / dashLength / (2 * ratio))
|
||||
|
||||
dashes -= dashes % snap
|
||||
|
||||
dashes = Math.max(dashes, 4)
|
||||
|
||||
const gapLength = Math.max(
|
||||
dashLength,
|
||||
(length - dashes * dashLength) / (outset ? dashes : dashes - 1)
|
||||
)
|
||||
|
||||
return {
|
||||
strokeDasharray: [dashLength, gapLength].join(' '),
|
||||
strokeDashoffset,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react'
|
|||
import { ShapeUtil, SVGContainer, TLBounds, Utils, TLHandle } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import getStroke from 'perfect-freehand'
|
||||
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
|
||||
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||
import {
|
||||
ArrowShape,
|
||||
Decoration,
|
||||
|
@ -102,7 +102,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
? renderFreehandArrowShaft(shape)
|
||||
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
|
||||
arrowDist,
|
||||
strokeWidth * 1.618,
|
||||
shape.style.dash,
|
||||
|
@ -154,7 +154,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
? renderCurvedFreehandArrowShaft(shape, circle, length, easing)
|
||||
: getArrowArcPath(start, end, circle, shape.bend)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
|
||||
Math.abs(length),
|
||||
sw,
|
||||
shape.style.dash,
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react'
|
|||
import { SVGContainer, Utils, ShapeUtil, TLTransformInfo, TLBounds } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { DashStyle, EllipseShape, TLDrawShapeType, TLDrawMeta } from '~types'
|
||||
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
|
||||
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||
import { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
||||
import {
|
||||
intersectBoundsEllipse,
|
||||
|
@ -82,7 +82,7 @@ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(()
|
|||
|
||||
const perimeter = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
|
||||
perimeter < 64 ? perimeter * 2 : perimeter,
|
||||
strokeWidth * 1.618,
|
||||
shape.style.dash,
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import * as React from 'react'
|
||||
import { Utils, SVGContainer, ShapeUtil } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import getStroke, { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
||||
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
||||
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||
import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types'
|
||||
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
|
||||
import { EASINGS } from '~state/utils'
|
||||
|
||||
const pathCache = new WeakMap<number[], string>([])
|
||||
|
||||
export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -36,7 +33,7 @@ export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta
|
|||
const strokeWidth = +styles.strokeWidth
|
||||
|
||||
if (style.dash === DashStyle.Draw) {
|
||||
const pathData = Utils.getFromCache(pathCache, shape.size, () => getRectanglePath(shape))
|
||||
const pathData = getRectanglePath(shape)
|
||||
|
||||
return (
|
||||
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
|
||||
|
@ -80,7 +77,7 @@ export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta
|
|||
]
|
||||
|
||||
const paths = strokes.map(([start, end, length], i) => {
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
|
||||
length,
|
||||
strokeWidth * 1.618,
|
||||
shape.style.dash
|
||||
|
|
2
packages/tldraw/src/state/constants.ts
Normal file
2
packages/tldraw/src/state/constants.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const FIT_TO_SCREEN_PADDING = 128
|
||||
export const SNAP_DISTANCE = 5
|
|
@ -74,7 +74,7 @@ export class ArrowSession implements Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
const { initialShape } = this
|
||||
|
||||
const page = TLDR.getPage(data, data.appState.currentPageId)
|
||||
|
|
|
@ -31,7 +31,7 @@ export class DrawSession implements Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], shiftKey: boolean) => {
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
const { shapeId } = this
|
||||
|
||||
// Even if we're not locked yet, we base the future locking direction
|
||||
|
|
|
@ -60,7 +60,7 @@ export class GridSession implements Session {
|
|||
return clone
|
||||
}
|
||||
|
||||
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
const nextShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||
|
||||
const nextPageState: Patch<TLPageState> = {}
|
||||
|
|
|
@ -27,7 +27,7 @@ export class HandleSession implements Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
const { initialShape } = this
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ export class RotateSession implements Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], isLocked = false) => {
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
const { commonBoundsCenter, initialShapes } = this.snapshot
|
||||
|
||||
const pageId = data.appState.currentPageId
|
||||
|
@ -32,7 +32,7 @@ export class RotateSession implements Session {
|
|||
|
||||
let directionDelta = Vec.angle(commonBoundsCenter, point) - this.initialAngle
|
||||
|
||||
if (isLocked) {
|
||||
if (shiftKey) {
|
||||
directionDelta = Utils.snapAngleToSegments(directionDelta, 24) // 15 degrees
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ export class RotateSession implements Session {
|
|||
const { rotation = 0 } = shape
|
||||
let shapeDelta = 0
|
||||
|
||||
if (isLocked) {
|
||||
if (shiftKey) {
|
||||
const snappedRotation = Utils.snapAngleToSegments(rotation, 24)
|
||||
shapeDelta = snappedRotation - rotation
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ export class RotateSession implements Session {
|
|||
shape,
|
||||
center,
|
||||
commonBoundsCenter,
|
||||
isLocked ? directionDelta + shapeDelta : directionDelta
|
||||
shiftKey ? directionDelta + shapeDelta : directionDelta
|
||||
)
|
||||
|
||||
if (change) {
|
||||
|
|
|
@ -29,7 +29,7 @@ export class TransformSingleSession implements Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], shiftKey: boolean) => {
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
const { transformType } = this
|
||||
|
||||
const { initialShapeBounds, initialShape, id } = this.snapshot
|
||||
|
|
|
@ -32,7 +32,7 @@ export class TransformSession implements Session {
|
|||
start = () => void null
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
const {
|
||||
transformType,
|
||||
snapshot: { shapeBounds, initialBounds, isAllAspectRatioLocked },
|
||||
|
|
|
@ -326,3 +326,13 @@ describe('When creating with a translate session', () => {
|
|||
expect(tlstate.getShape('rect1')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When snapping', () => {
|
||||
it.todo('Does not snap when moving quicky')
|
||||
it.todo('Snaps only matching edges when moving slowly')
|
||||
it.todo('Snaps any edge to any edge when moving very slowly')
|
||||
it.todo('Snaps a clone to its parent')
|
||||
it.todo('Cleans up snap lines when cancelled')
|
||||
it.todo('Cleans up snap lines when completed')
|
||||
it.todo('Cleans up snap lines when starting to clone / not clone')
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TLPageState, Utils } from '@tldraw/core'
|
||||
import { TLPageState, Utils, TLBoundsWithCenter, TLSnapLine } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import {
|
||||
TLDrawShape,
|
||||
|
@ -11,19 +11,50 @@ import {
|
|||
ArrowShape,
|
||||
GroupShape,
|
||||
SessionType,
|
||||
ArrowBinding,
|
||||
} from '~types'
|
||||
import { SNAP_DISTANCE } from '~state/constants'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import type { Patch } from 'rko'
|
||||
|
||||
type CloneInfo =
|
||||
| {
|
||||
state: 'empty'
|
||||
}
|
||||
| {
|
||||
state: 'ready'
|
||||
clones: TLDrawShape[]
|
||||
clonedBindings: ArrowBinding[]
|
||||
}
|
||||
|
||||
type SnapInfo =
|
||||
| {
|
||||
state: 'empty'
|
||||
}
|
||||
| {
|
||||
state: 'ready'
|
||||
others: TLBoundsWithCenter[]
|
||||
bounds: TLBoundsWithCenter[]
|
||||
}
|
||||
|
||||
export class TranslateSession implements Session {
|
||||
type = SessionType.Translate
|
||||
status = TLDrawStatus.Translating
|
||||
delta = [0, 0]
|
||||
prev = [0, 0]
|
||||
prevPoint = [0, 0]
|
||||
speed = 1
|
||||
origin: number[]
|
||||
snapshot: TranslateSnapshot
|
||||
isCloning = false
|
||||
isCreate: boolean
|
||||
cloneInfo: CloneInfo = {
|
||||
state: 'empty',
|
||||
}
|
||||
snapInfo: SnapInfo = {
|
||||
state: 'empty',
|
||||
}
|
||||
snapLines: TLSnapLine[] = []
|
||||
|
||||
constructor(data: Data, point: number[], isCreate = false) {
|
||||
this.origin = point
|
||||
|
@ -34,6 +65,8 @@ export class TranslateSession implements Session {
|
|||
start = (data: Data) => {
|
||||
const { bindingsToDelete } = this.snapshot
|
||||
|
||||
this.createSnapInfo(data)
|
||||
|
||||
if (bindingsToDelete.length === 0) return data
|
||||
|
||||
const nextBindings: Patch<Record<string, TLDrawBinding>> = {}
|
||||
|
@ -51,16 +84,30 @@ export class TranslateSession implements Session {
|
|||
}
|
||||
}
|
||||
|
||||
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean) => {
|
||||
const { selectedIds, initialParentChildren, clones, initialShapes, bindingsToDelete } =
|
||||
this.snapshot
|
||||
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||
const { selectedIds, initialParentChildren, initialShapes, bindingsToDelete } = this.snapshot
|
||||
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const nextBindings: Patch<Record<string, TLDrawBinding>> = {}
|
||||
|
||||
const nextShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||
|
||||
const nextPageState: Patch<TLPageState> = {}
|
||||
|
||||
const delta = Vec.sub(point, this.origin)
|
||||
let delta = Vec.sub(point, this.origin)
|
||||
|
||||
let didChangeCloning = false
|
||||
|
||||
if (!this.isCreate) {
|
||||
if (altKey && !this.isCloning) {
|
||||
this.isCloning = true
|
||||
didChangeCloning = true
|
||||
} else if (!altKey && this.isCloning) {
|
||||
this.isCloning = false
|
||||
didChangeCloning = true
|
||||
}
|
||||
}
|
||||
|
||||
if (shiftKey) {
|
||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
||||
|
@ -70,15 +117,71 @@ export class TranslateSession implements Session {
|
|||
}
|
||||
}
|
||||
|
||||
const trueDelta = Vec.sub(delta, this.prev)
|
||||
// Should we snap?
|
||||
|
||||
// Speed is used to decide which snap points to use. At a high
|
||||
// speed, we don't use any snap points. At a low speed, we only
|
||||
// allow center-to-center snap points. At very low speed, we
|
||||
// enable all snap points (still preferring middle snaps). We're
|
||||
// using an acceleration function here to smooth the changes in
|
||||
// speed, but we also want the speed to accelerate faster than
|
||||
// it decelerates.
|
||||
|
||||
const speed = Vec.dist(point, this.prevPoint)
|
||||
|
||||
this.prevPoint = point
|
||||
|
||||
const change = speed - this.speed
|
||||
|
||||
this.speed = this.speed + change * (change > 1 ? 0.5 : 0.15)
|
||||
|
||||
this.snapLines = []
|
||||
|
||||
if (!metaKey && this.speed < 4 && this.snapInfo.state === 'ready') {
|
||||
const { zoom } = data.document.pageStates[currentPageId].camera
|
||||
|
||||
const bounds = Utils.getBoundsWithCenter(
|
||||
Utils.translateBounds(this.snapshot.commonBounds, delta)
|
||||
)
|
||||
|
||||
const snapResult = Utils.getSnapPoints(
|
||||
bounds,
|
||||
this.isCloning ? this.snapInfo.bounds : this.snapInfo.others,
|
||||
SNAP_DISTANCE / zoom,
|
||||
this.speed * zoom < 0.45
|
||||
)
|
||||
|
||||
if (snapResult) {
|
||||
this.snapLines = snapResult.snapLines
|
||||
delta = Vec.sub(delta, snapResult?.offset)
|
||||
}
|
||||
}
|
||||
|
||||
// We've now calculated the "delta", or difference between the
|
||||
// cursor's position (real or adjusted by snaps or axis locking)
|
||||
// and the cursor's original position ("origin").
|
||||
|
||||
// The "movement" is the actual change of position between this
|
||||
// computed position and the previous computed position.
|
||||
|
||||
const movement = Vec.sub(delta, this.prev)
|
||||
|
||||
this.delta = delta
|
||||
this.prev = delta
|
||||
|
||||
// If cloning...
|
||||
if (!this.isCreate && altKey) {
|
||||
if (this.isCloning) {
|
||||
// Not Cloning -> Cloning
|
||||
if (!this.isCloning) {
|
||||
if (didChangeCloning) {
|
||||
if (this.cloneInfo.state === 'empty') {
|
||||
this.createCloneInfo(data)
|
||||
}
|
||||
|
||||
if (this.cloneInfo.state === 'empty') {
|
||||
throw Error
|
||||
}
|
||||
|
||||
const { clones, clonedBindings } = this.cloneInfo
|
||||
|
||||
this.isCloning = true
|
||||
|
||||
// Put back any bindings we deleted
|
||||
|
@ -109,7 +212,7 @@ export class TranslateSession implements Session {
|
|||
})
|
||||
|
||||
// Add the cloned bindings
|
||||
for (const binding of this.snapshot.clonedBindings) {
|
||||
for (const binding of clonedBindings) {
|
||||
nextBindings[binding.id] = binding
|
||||
}
|
||||
|
||||
|
@ -117,6 +220,10 @@ export class TranslateSession implements Session {
|
|||
nextPageState.selectedIds = clones.map((clone) => clone.id)
|
||||
}
|
||||
|
||||
if (this.cloneInfo.state === 'empty') throw Error
|
||||
|
||||
const { clones } = this.cloneInfo
|
||||
|
||||
// Either way, move the clones
|
||||
clones.forEach((clone) => {
|
||||
const current = (nextShapes[clone.id] ||
|
||||
|
@ -126,14 +233,18 @@ export class TranslateSession implements Session {
|
|||
|
||||
nextShapes[clone.id] = {
|
||||
...nextShapes[clone.id],
|
||||
point: Vec.round(Vec.add(current.point, trueDelta)),
|
||||
point: Vec.round(Vec.add(current.point, movement)),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// If not cloning...
|
||||
|
||||
// Cloning -> Not Cloning
|
||||
if (this.isCloning) {
|
||||
if (didChangeCloning) {
|
||||
if (this.cloneInfo.state === 'empty') throw Error
|
||||
|
||||
const { clones, clonedBindings } = this.cloneInfo
|
||||
|
||||
this.isCloning = false
|
||||
|
||||
// Delete the bindings
|
||||
|
@ -159,7 +270,7 @@ export class TranslateSession implements Session {
|
|||
})
|
||||
|
||||
// Delete the cloned bindings
|
||||
for (const binding of this.snapshot.clonedBindings) {
|
||||
for (const binding of clonedBindings) {
|
||||
nextBindings[binding.id] = undefined
|
||||
}
|
||||
|
||||
|
@ -176,12 +287,15 @@ export class TranslateSession implements Session {
|
|||
|
||||
nextShapes[shape.id] = {
|
||||
...nextShapes[shape.id],
|
||||
point: Vec.round(Vec.add(current.point, trueDelta)),
|
||||
point: Vec.round(Vec.add(current.point, movement)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines: this.snapLines,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -197,7 +311,7 @@ export class TranslateSession implements Session {
|
|||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
const { initialShapes, clones, clonedBindings, bindingsToDelete } = this.snapshot
|
||||
const { initialShapes, bindingsToDelete } = this.snapshot
|
||||
|
||||
const nextBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
|
||||
const nextShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
|
||||
|
@ -218,13 +332,19 @@ export class TranslateSession implements Session {
|
|||
nextPageState.selectedIds = this.snapshot.selectedIds
|
||||
}
|
||||
|
||||
// Delete clones
|
||||
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
|
||||
if (this.cloneInfo.state === 'ready') {
|
||||
const { clones, clonedBindings } = this.cloneInfo
|
||||
// Delete clones
|
||||
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
|
||||
|
||||
// Delete cloned bindings
|
||||
clonedBindings.forEach((binding) => (nextBindings[binding.id] = undefined))
|
||||
// Delete cloned bindings
|
||||
clonedBindings.forEach((binding) => (nextBindings[binding.id] = undefined))
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -242,8 +362,7 @@ export class TranslateSession implements Session {
|
|||
complete(data: Data): TLDrawCommand {
|
||||
const pageId = data.appState.currentPageId
|
||||
|
||||
const { initialShapes, initialParentChildren, bindingsToDelete, clones, clonedBindings } =
|
||||
this.snapshot
|
||||
const { initialShapes, initialParentChildren, bindingsToDelete } = this.snapshot
|
||||
|
||||
const beforeBindings: Patch<Record<string, TLDrawBinding>> = {}
|
||||
const beforeShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||
|
@ -252,6 +371,13 @@ export class TranslateSession implements Session {
|
|||
const afterShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||
|
||||
if (this.isCloning) {
|
||||
if (this.cloneInfo.state === 'empty') {
|
||||
this.createCloneInfo(data)
|
||||
}
|
||||
|
||||
if (this.cloneInfo.state !== 'ready') throw Error
|
||||
const { clones, clonedBindings } = this.cloneInfo
|
||||
|
||||
// Update the clones
|
||||
clones.forEach((clone) => {
|
||||
beforeShapes[clone.id] = undefined
|
||||
|
@ -331,6 +457,9 @@ export class TranslateSession implements Session {
|
|||
return {
|
||||
id: 'translate',
|
||||
before: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -346,6 +475,9 @@ export class TranslateSession implements Session {
|
|||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
|
@ -362,6 +494,124 @@ export class TranslateSession implements Session {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
private createSnapInfo = async (data: Data) => {
|
||||
const { currentPageId } = data.appState
|
||||
const page = data.document.pages[currentPageId]
|
||||
const { selectedIds } = data.document.pageStates[currentPageId]
|
||||
|
||||
const allBounds: TLBoundsWithCenter[] = []
|
||||
const otherBounds: TLBoundsWithCenter[] = []
|
||||
|
||||
Object.values(page.shapes).forEach((shape) => {
|
||||
const bounds = Utils.getBoundsWithCenter(TLDR.getBounds(shape))
|
||||
allBounds.push(bounds)
|
||||
if (!selectedIds.includes(shape.id)) {
|
||||
otherBounds.push(bounds)
|
||||
}
|
||||
})
|
||||
|
||||
this.snapInfo = {
|
||||
state: 'ready',
|
||||
bounds: allBounds,
|
||||
others: otherBounds,
|
||||
}
|
||||
}
|
||||
|
||||
private createCloneInfo = (data: Data) => {
|
||||
// Create clones when as they're needed.
|
||||
// Consider doing this work in a worker.
|
||||
|
||||
const { currentPageId } = data.appState
|
||||
const page = data.document.pages[currentPageId]
|
||||
const { selectedIds, shapesToMove, initialParentChildren } = this.snapshot
|
||||
|
||||
const cloneMap: Record<string, string> = {}
|
||||
const clonedBindingsMap: Record<string, string> = {}
|
||||
const clonedBindings: TLDrawBinding[] = []
|
||||
|
||||
// Create clones of selected shapes
|
||||
const clones: TLDrawShape[] = []
|
||||
|
||||
shapesToMove.forEach((shape) => {
|
||||
const newId = Utils.uniqueId()
|
||||
|
||||
initialParentChildren[newId] = initialParentChildren[shape.id]
|
||||
|
||||
cloneMap[shape.id] = newId
|
||||
|
||||
const clone = {
|
||||
...Utils.deepClone(shape),
|
||||
id: newId,
|
||||
parentId: shape.parentId,
|
||||
childIndex: TLDR.getChildIndexAbove(data, shape.id, currentPageId),
|
||||
}
|
||||
|
||||
clones.push(clone)
|
||||
})
|
||||
|
||||
clones.forEach((clone) => {
|
||||
if (clone.children !== undefined) {
|
||||
clone.children = clone.children.map((childId) => cloneMap[childId])
|
||||
}
|
||||
})
|
||||
|
||||
clones.forEach((clone) => {
|
||||
if (selectedIds.includes(clone.parentId)) {
|
||||
clone.parentId = cloneMap[clone.parentId]
|
||||
}
|
||||
})
|
||||
|
||||
// Potentially confusing name here: these are the ids of the
|
||||
// original shapes that were cloned, not their clones' ids.
|
||||
const clonedShapeIds = new Set(Object.keys(cloneMap))
|
||||
|
||||
// Create cloned bindings for shapes where both to and from shapes are selected
|
||||
// (if the user clones, then we will create a new binding for the clones)
|
||||
Object.values(page.bindings)
|
||||
.filter((binding) => clonedShapeIds.has(binding.fromId) || clonedShapeIds.has(binding.toId))
|
||||
.forEach((binding) => {
|
||||
if (clonedShapeIds.has(binding.fromId)) {
|
||||
if (clonedShapeIds.has(binding.toId)) {
|
||||
const cloneId = Utils.uniqueId()
|
||||
|
||||
const cloneBinding = {
|
||||
...Utils.deepClone(binding),
|
||||
id: cloneId,
|
||||
fromId: cloneMap[binding.fromId] || binding.fromId,
|
||||
toId: cloneMap[binding.toId] || binding.toId,
|
||||
}
|
||||
|
||||
clonedBindingsMap[binding.id] = cloneId
|
||||
clonedBindings.push(cloneBinding)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Assign new binding ids to clones (or delete them!)
|
||||
clones.forEach((clone) => {
|
||||
if (clone.handles) {
|
||||
if (clone.handles) {
|
||||
for (const id in clone.handles) {
|
||||
const handle = clone.handles[id as keyof ArrowShape['handles']]
|
||||
handle.bindingId = handle.bindingId ? clonedBindingsMap[handle.bindingId] : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
clones.forEach((clone) => {
|
||||
if (page.shapes[clone.id]) {
|
||||
throw Error("uh oh, we didn't clone correctly")
|
||||
}
|
||||
})
|
||||
|
||||
this.cloneInfo = {
|
||||
state: 'ready',
|
||||
clones,
|
||||
clonedBindings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
|
@ -382,6 +632,20 @@ export function getTranslateSnapshot(data: Data) {
|
|||
: [shape]
|
||||
})
|
||||
|
||||
const idsToMove = new Set(shapesToMove.map((shape) => shape.id))
|
||||
|
||||
const bindingsToDelete: ArrowBinding[] = []
|
||||
|
||||
Object.values(page.bindings)
|
||||
.filter((binding) => idsToMove.has(binding.fromId) || idsToMove.has(binding.toId))
|
||||
.forEach((binding) => {
|
||||
if (idsToMove.has(binding.fromId)) {
|
||||
if (!idsToMove.has(binding.toId)) {
|
||||
bindingsToDelete.push(binding)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const initialParentChildren: Record<string, string[]> = {}
|
||||
|
||||
Array.from(new Set(shapesToMove.map((s) => s.parentId)).values())
|
||||
|
@ -391,102 +655,20 @@ export function getTranslateSnapshot(data: Data) {
|
|||
initialParentChildren[id] = shape.children!
|
||||
})
|
||||
|
||||
const cloneMap: Record<string, string> = {}
|
||||
const clonedBindingsMap: Record<string, string> = {}
|
||||
const clonedBindings: TLDrawBinding[] = []
|
||||
|
||||
// Create clones of selected shapes
|
||||
const clones: TLDrawShape[] = []
|
||||
|
||||
shapesToMove.forEach((shape) => {
|
||||
const newId = Utils.uniqueId()
|
||||
|
||||
initialParentChildren[newId] = initialParentChildren[shape.id]
|
||||
|
||||
cloneMap[shape.id] = newId
|
||||
|
||||
const clone = {
|
||||
...Utils.deepClone(shape),
|
||||
id: newId,
|
||||
parentId: shape.parentId,
|
||||
childIndex: TLDR.getChildIndexAbove(data, shape.id, currentPageId),
|
||||
}
|
||||
|
||||
clones.push(clone)
|
||||
})
|
||||
|
||||
clones.forEach((clone) => {
|
||||
if (clone.children !== undefined) {
|
||||
clone.children = clone.children.map((childId) => cloneMap[childId])
|
||||
}
|
||||
})
|
||||
|
||||
clones.forEach((clone) => {
|
||||
if (selectedIds.includes(clone.parentId)) {
|
||||
clone.parentId = cloneMap[clone.parentId]
|
||||
}
|
||||
})
|
||||
|
||||
// Potentially confusing name here: these are the ids of the
|
||||
// original shapes that were cloned, not their clones' ids.
|
||||
const clonedShapeIds = new Set(Object.keys(cloneMap))
|
||||
|
||||
const bindingsToDelete: TLDrawBinding[] = []
|
||||
|
||||
// Create cloned bindings for shapes where both to and from shapes are selected
|
||||
// (if the user clones, then we will create a new binding for the clones)
|
||||
Object.values(page.bindings)
|
||||
.filter((binding) => clonedShapeIds.has(binding.fromId) || clonedShapeIds.has(binding.toId))
|
||||
.forEach((binding) => {
|
||||
if (clonedShapeIds.has(binding.fromId)) {
|
||||
if (clonedShapeIds.has(binding.toId)) {
|
||||
const cloneId = Utils.uniqueId()
|
||||
|
||||
const cloneBinding = {
|
||||
...Utils.deepClone(binding),
|
||||
id: cloneId,
|
||||
fromId: cloneMap[binding.fromId] || binding.fromId,
|
||||
toId: cloneMap[binding.toId] || binding.toId,
|
||||
}
|
||||
|
||||
clonedBindingsMap[binding.id] = cloneId
|
||||
clonedBindings.push(cloneBinding)
|
||||
} else {
|
||||
bindingsToDelete.push(binding)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Assign new binding ids to clones (or delete them!)
|
||||
clones.forEach((clone) => {
|
||||
if (clone.handles) {
|
||||
if (clone.handles) {
|
||||
for (const id in clone.handles) {
|
||||
const handle = clone.handles[id as keyof ArrowShape['handles']]
|
||||
handle.bindingId = handle.bindingId ? clonedBindingsMap[handle.bindingId] : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
clones.forEach((clone) => {
|
||||
if (page.shapes[clone.id]) {
|
||||
throw Error("uh oh, we didn't clone correctly")
|
||||
}
|
||||
})
|
||||
const commonBounds = Utils.getCommonBounds(shapesToMove.map(TLDR.getBounds))
|
||||
|
||||
return {
|
||||
selectedIds,
|
||||
bindingsToDelete,
|
||||
hasUnlockedShapes,
|
||||
initialParentChildren,
|
||||
shapesToMove,
|
||||
bindingsToDelete,
|
||||
commonBounds,
|
||||
initialShapes: shapesToMove.map(({ id, point, parentId }) => ({
|
||||
id,
|
||||
point,
|
||||
parentId,
|
||||
})),
|
||||
clones,
|
||||
clonedBindings,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ import { ArgsOfType, getSession } from './session'
|
|||
import { sample, USER_COLORS } from './utils'
|
||||
import { createTools, ToolType } from './tool'
|
||||
import type { BaseTool } from './tool/BaseTool'
|
||||
import * as constants from './constants'
|
||||
|
||||
const uuid = Utils.uniqueId()
|
||||
|
||||
|
@ -1392,7 +1393,10 @@ export class TLDrawState extends StateManager<Data> {
|
|||
const bounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds))
|
||||
|
||||
let zoom = TLDR.getCameraZoom(
|
||||
Math.min((this.bounds.width - 128) / bounds.width, (this.bounds.height - 128) / bounds.height)
|
||||
Math.min(
|
||||
(this.bounds.width - constants.FIT_TO_SCREEN_PADDING) / bounds.width,
|
||||
(this.bounds.height - constants.FIT_TO_SCREEN_PADDING) / bounds.height
|
||||
)
|
||||
)
|
||||
|
||||
zoom =
|
||||
|
@ -1419,7 +1423,10 @@ export class TLDrawState extends StateManager<Data> {
|
|||
const bounds = TLDR.getSelectedBounds(this.state)
|
||||
|
||||
let zoom = TLDR.getCameraZoom(
|
||||
Math.min((this.bounds.width - 128) / bounds.width, (this.bounds.height - 128) / bounds.height)
|
||||
Math.min(
|
||||
(this.bounds.width - constants.FIT_TO_SCREEN_PADDING) / bounds.width,
|
||||
(this.bounds.height - constants.FIT_TO_SCREEN_PADDING) / bounds.height
|
||||
)
|
||||
)
|
||||
|
||||
zoom =
|
||||
|
@ -1645,10 +1652,13 @@ export class TLDrawState extends StateManager<Data> {
|
|||
* updateSession.
|
||||
* @param args The arguments of the current session's update method.
|
||||
*/
|
||||
updateSession = (point: number[], shiftKey = false, altKey = false, metaKey = false): this => {
|
||||
updateSession = <T extends Session>(...args: ExceptFirst<Parameters<T['update']>>): this => {
|
||||
const { session } = this
|
||||
if (!session) return this
|
||||
const patch = session.update(this.state, point, shiftKey, altKey, metaKey)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const patch = session.update(this.state, ...args)
|
||||
if (!patch) return this
|
||||
return this.patchState(patch, `session:${session?.constructor.name}`)
|
||||
}
|
||||
|
@ -1713,6 +1723,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
// the shape we just created.
|
||||
result.before = {
|
||||
appState: {
|
||||
...result.before.appState,
|
||||
status: TLDrawStatus.Idle,
|
||||
},
|
||||
document: {
|
||||
|
@ -1741,6 +1752,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
}
|
||||
|
||||
result.after.appState = {
|
||||
...result.after.appState,
|
||||
status: TLDrawStatus.Idle,
|
||||
}
|
||||
|
||||
|
@ -2373,6 +2385,23 @@ export class TLDrawState extends StateManager<Data> {
|
|||
return Vec.round([this.bounds.width / 2, this.bounds.height / 2])
|
||||
}
|
||||
|
||||
get viewport() {
|
||||
const { camera } = this.pageState
|
||||
const { width, height } = this.bounds
|
||||
|
||||
const [minX, minY] = Vec.sub(Vec.div([0, 0], camera.zoom), camera.point)
|
||||
const [maxX, maxY] = Vec.sub(Vec.div([width, height], camera.zoom), camera.point)
|
||||
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
height: maxX - minX,
|
||||
width: maxY - minY,
|
||||
}
|
||||
}
|
||||
|
||||
static version = 10.1
|
||||
|
||||
static defaultDocument: TLDrawDocument = {
|
||||
|
@ -2420,6 +2449,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
isStyleOpen: false,
|
||||
isEmptyCanvas: false,
|
||||
status: TLDrawStatus.Idle,
|
||||
snapLines: [],
|
||||
},
|
||||
document: TLDrawState.defaultDocument,
|
||||
room: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import type { TLBinding, TLShapeProps } from '@tldraw/core'
|
||||
import type { TLBinding, TLShapeProps, TLSnapLine } from '@tldraw/core'
|
||||
import type { TLShape, TLShapeUtil, TLHandle } from '@tldraw/core'
|
||||
import type { TLPage, TLUser, TLPageState } from '@tldraw/core'
|
||||
import type { StoreApi } from 'zustand'
|
||||
|
@ -63,6 +63,7 @@ export interface Data {
|
|||
isStyleOpen: boolean
|
||||
isEmptyCanvas: boolean
|
||||
status: string
|
||||
snapLines: TLSnapLine[]
|
||||
}
|
||||
document: TLDrawDocument
|
||||
room?: {
|
||||
|
@ -105,9 +106,9 @@ export abstract class Session {
|
|||
abstract update: (
|
||||
data: Readonly<Data>,
|
||||
point: number[],
|
||||
shiftKey: boolean,
|
||||
altKey: boolean,
|
||||
metaKey: boolean
|
||||
shiftKey?: boolean,
|
||||
altKey?: boolean,
|
||||
metaKey?: boolean
|
||||
) => TLDrawPatch | undefined
|
||||
abstract complete: (data: Readonly<Data>) => TLDrawPatch | TLDrawCommand | undefined
|
||||
abstract cancel: (data: Readonly<Data>) => TLDrawPatch | undefined
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"@types/node": "^16.7.10",
|
||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||
"@typescript-eslint/parser": "^4.30.0",
|
||||
"esbuild": "^0.12.24",
|
||||
"esbuild": "^0.13.8",
|
||||
"eslint": "^7.32.0",
|
||||
"lerna": "^4.0.0",
|
||||
"ts-node": "^10.2.1",
|
||||
|
|
118
yarn.lock
118
yarn.lock
|
@ -6319,10 +6319,113 @@ es6-promisify@^5.0.0:
|
|||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
esbuild@^0.12.24, esbuild@^0.12.26:
|
||||
version "0.12.28"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.28.tgz#84da0d2a0d0dee181281545271e0d65cf6fab1ef"
|
||||
integrity sha512-pZ0FrWZXlvQOATlp14lRSk1N9GkeJ3vLIwOcUoo3ICQn9WNR4rWoNi81pbn6sC1iYUy7QPqNzI3+AEzokwyVcA==
|
||||
esbuild-android-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.8.tgz#c20e875c3c98164b1ffba9b28637bdf96f5e9e7c"
|
||||
integrity sha512-AilbChndywpk7CdKkNSZ9klxl+9MboLctXd9LwLo3b0dawmOF/i/t2U5d8LM6SbT1Xw36F8yngSUPrd8yPs2RA==
|
||||
|
||||
esbuild-darwin-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.8.tgz#f46e6b471ddbf62265234808a6a1aa91df18a417"
|
||||
integrity sha512-b6sdiT84zV5LVaoF+UoMVGJzR/iE2vNUfUDfFQGrm4LBwM/PWXweKpuu6RD9mcyCq18cLxkP6w/LD/w9DtX3ng==
|
||||
|
||||
esbuild-darwin-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.8.tgz#a991157a6013facd4f2e14159b7da52626c90154"
|
||||
integrity sha512-R8YuPiiJayuJJRUBG4H0VwkEKo6AvhJs2m7Tl0JaIer3u1FHHXwGhMxjJDmK+kXwTFPriSysPvcobXC/UrrZCQ==
|
||||
|
||||
esbuild-freebsd-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.8.tgz#301601d2e443ad458960e359b402a17d9500be9d"
|
||||
integrity sha512-zBn6urrn8FnKC+YSgDxdof9jhPCeU8kR/qaamlV4gI8R3KUaUK162WYM7UyFVAlj9N0MyD3AtB+hltzu4cysTw==
|
||||
|
||||
esbuild-freebsd-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.8.tgz#039a63acc12ec0892006c147ea221e55f9125a9f"
|
||||
integrity sha512-pWW2slN7lGlkx0MOEBoUGwRX5UgSCLq3dy2c8RIOpiHtA87xAUpDBvZK10MykbT+aMfXc0NI2lu1X+6kI34xng==
|
||||
|
||||
esbuild-linux-32@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.8.tgz#c537b67d7e694b60bfa2786581412838c6ba0284"
|
||||
integrity sha512-T0I0ueeKVO/Is0CAeSEOG9s2jeNNb8jrrMwG9QBIm3UU18MRB60ERgkS2uV3fZ1vP2F8i3Z2e3Zju4lg9dhVmw==
|
||||
|
||||
esbuild-linux-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.8.tgz#0092fc8a064001a777bfa0e3b425bb8be8f96e6a"
|
||||
integrity sha512-Bm8SYmFtvfDCIu9sjKppFXzRXn2BVpuCinU1ChTuMtdKI/7aPpXIrkqBNOgPTOQO9AylJJc1Zw6EvtKORhn64w==
|
||||
|
||||
esbuild-linux-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.8.tgz#5cd3f2bb924212971482e8dbc25c4afd09b28110"
|
||||
integrity sha512-X4pWZ+SL+FJ09chWFgRNO3F+YtvAQRcWh0uxKqZSWKiWodAB20flsW/OWFYLXBKiVCTeoGMvENZS/GeVac7+tQ==
|
||||
|
||||
esbuild-linux-arm@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.8.tgz#ad634f96bf2975536907aeb9fdb75a3194f4ddce"
|
||||
integrity sha512-4/HfcC40LJ4GPyboHA+db0jpFarTB628D1ifU+/5bunIgY+t6mHkJWyxWxAAE8wl/ZIuRYB9RJFdYpu1AXGPdg==
|
||||
|
||||
esbuild-linux-mips64le@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.8.tgz#57857edfebf9bf65766dc8be1637f2179c990572"
|
||||
integrity sha512-o7e0D+sqHKT31v+mwFircJFjwSKVd2nbkHEn4l9xQ1hLR+Bv8rnt3HqlblY3+sBdlrOTGSwz0ReROlKUMJyldA==
|
||||
|
||||
esbuild-linux-ppc64le@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.8.tgz#fdb82a059a5b86bb10fb42091b4ebcf488b9cd46"
|
||||
integrity sha512-eZSQ0ERsWkukJp2px/UWJHVNuy0lMoz/HZcRWAbB6reoaBw7S9vMzYNUnflfL3XA6WDs+dZn3ekHE4Y2uWLGig==
|
||||
|
||||
esbuild-netbsd-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.8.tgz#d7879e7123d3b2c04754ece8bd061aa6866deeff"
|
||||
integrity sha512-gZX4kP7gVvOrvX0ZwgHmbuHczQUwqYppxqtoyC7VNd80t5nBHOFXVhWo2Ad/Lms0E8b+wwgI/WjZFTCpUHOg9Q==
|
||||
|
||||
esbuild-openbsd-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.8.tgz#88b280b6cb0a3f6adb60abf27fc506c506a35cf0"
|
||||
integrity sha512-afzza308X4WmcebexbTzAgfEWt9MUkdTvwIa8xOu4CM2qGbl2LanqEl8/LUs8jh6Gqw6WsicEK52GPrS9wvkcw==
|
||||
|
||||
esbuild-sunos-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.8.tgz#229ae7c7703196a58acd0f0291ad9bebda815d63"
|
||||
integrity sha512-mWPZibmBbuMKD+LDN23LGcOZ2EawMYBONMXXHmbuxeT0XxCNwadbCVwUQ/2p5Dp5Kvf6mhrlIffcnWOiCBpiVw==
|
||||
|
||||
esbuild-windows-32@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.8.tgz#892d093e32a21c0c9135e5a0ffdc380aeb70e763"
|
||||
integrity sha512-QsZ1HnWIcnIEApETZWw8HlOhDSWqdZX2SylU7IzGxOYyVcX7QI06ety/aDcn437mwyO7Ph4RrbhB+2ntM8kX8A==
|
||||
|
||||
esbuild-windows-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.8.tgz#7defd8d79ae3bb7e6f53b65a7190be7daf901686"
|
||||
integrity sha512-76Fb57B9eE/JmJi1QmUW0tRLQZfGo0it+JeYoCDTSlbTn7LV44ecOHIMJSSgZADUtRMWT9z0Kz186bnaB3amSg==
|
||||
|
||||
esbuild-windows-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.8.tgz#e59ae004496fd8a5ab67bfc7945a2e47480d6fb9"
|
||||
integrity sha512-HW6Mtq5eTudllxY2YgT62MrVcn7oq2o8TAoAvDUhyiEmRmDY8tPwAhb1vxw5/cdkbukM3KdMYtksnUhF/ekWeg==
|
||||
|
||||
esbuild@^0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.8.tgz#bd7cc51b881ab067789f88e17baca74724c1ec4f"
|
||||
integrity sha512-A4af7G7YZLfG5OnARJRMtlpEsCkq/zHZQXewgPA864l9D6VjjbH1SuFYK/OSV6BtHwDGkdwyRrX0qQFLnMfUcw==
|
||||
optionalDependencies:
|
||||
esbuild-android-arm64 "0.13.8"
|
||||
esbuild-darwin-64 "0.13.8"
|
||||
esbuild-darwin-arm64 "0.13.8"
|
||||
esbuild-freebsd-64 "0.13.8"
|
||||
esbuild-freebsd-arm64 "0.13.8"
|
||||
esbuild-linux-32 "0.13.8"
|
||||
esbuild-linux-64 "0.13.8"
|
||||
esbuild-linux-arm "0.13.8"
|
||||
esbuild-linux-arm64 "0.13.8"
|
||||
esbuild-linux-mips64le "0.13.8"
|
||||
esbuild-linux-ppc64le "0.13.8"
|
||||
esbuild-netbsd-64 "0.13.8"
|
||||
esbuild-openbsd-64 "0.13.8"
|
||||
esbuild-sunos-64 "0.13.8"
|
||||
esbuild-windows-32 "0.13.8"
|
||||
esbuild-windows-64 "0.13.8"
|
||||
esbuild-windows-arm64 "0.13.8"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
|
@ -7616,11 +7719,16 @@ idb-keyval@^5.1.3:
|
|||
dependencies:
|
||||
safari-14-idb-fix "^1.0.6"
|
||||
|
||||
idb@^6.0.0, idb@^6.1.2:
|
||||
idb@^6.0.0:
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.3.tgz#e6cd3b9c38f5c696a82a4b435754f3873c5a7891"
|
||||
integrity sha512-oIRDpVcs5KXpI1hRnTJUwkY63RB/7iqu9nSNuzXN8TLHjs7oO20IoPFbBTsqxIL5IjzIUDi+FXlVcK4zm26J8A==
|
||||
|
||||
idb@^6.1.2:
|
||||
version "6.1.4"
|
||||
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.4.tgz#ec77519fe2591be616eaf3bbdedc3662bb558a99"
|
||||
integrity sha512-DshI5yxIB3NYc47cPpfipYX8MSIgQPqVR+WoaGI9EDq6cnLGgGYR1fp6z8/Bq9vMS8Jq1bS3eWUgXpFO5+ypSA==
|
||||
|
||||
ieee754@^1.1.4, ieee754@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
|
|
Loading…
Reference in a new issue