[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",
|
"@types/react-dom": "^16.9.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||||
"@typescript-eslint/parser": "^4.30.0",
|
"@typescript-eslint/parser": "^4.30.0",
|
||||||
"esbuild": "^0.12.24",
|
"esbuild": "^0.13.8",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^4.0.0",
|
||||||
"react": ">=16.8",
|
"react": ">=16.8",
|
||||||
|
|
|
@ -6,10 +6,10 @@ const name = process.env.npm_package_name || ''
|
||||||
async function main() {
|
async function main() {
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
entryPoints: ['./src/index.ts'],
|
entryPoints: ['./src/index.ts'],
|
||||||
outdir: 'dist/cjs',
|
outdir: 'dist/esm',
|
||||||
minify: false,
|
minify: false,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: 'cjs',
|
format: 'esm',
|
||||||
target: 'es6',
|
target: 'es6',
|
||||||
jsxFactory: 'React.createElement',
|
jsxFactory: 'React.createElement',
|
||||||
jsxFragment: 'React.Fragment',
|
jsxFragment: 'React.Fragment',
|
||||||
|
|
|
@ -13,10 +13,10 @@ async function main() {
|
||||||
|
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
entryPoints: ['./src/index.ts'],
|
entryPoints: ['./src/index.ts'],
|
||||||
outdir: 'dist/cjs',
|
outdir: 'dist/esm',
|
||||||
minify: false,
|
minify: false,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: 'cjs',
|
format: 'esm',
|
||||||
target: 'es6',
|
target: 'es6',
|
||||||
jsxFactory: 'React.createElement',
|
jsxFactory: 'React.createElement',
|
||||||
jsxFragment: 'React.Fragment',
|
jsxFragment: 'React.Fragment',
|
||||||
|
|
|
@ -38,8 +38,10 @@ export const Bounds = React.memo(
|
||||||
const smallDimension = Math.min(bounds.width, bounds.height) * zoom
|
const smallDimension = Math.min(bounds.width, bounds.height) * zoom
|
||||||
// If the bounds are small, don't show the rotate handle
|
// If the bounds are small, don't show the rotate handle
|
||||||
const showRotateHandle = !isHidden && !isLocked && smallDimension > 32
|
const showRotateHandle = !isHidden && !isLocked && smallDimension > 32
|
||||||
// If the bounds are very small, don't show the corner handles
|
// If the bounds are very small, don't show the edge handles
|
||||||
const showHandles = !isHidden && !isLocked && smallDimension > 16
|
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 (
|
return (
|
||||||
<Container bounds={bounds} rotation={rotation}>
|
<Container bounds={bounds} rotation={rotation}>
|
||||||
|
@ -50,62 +52,62 @@ export const Bounds = React.memo(
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
edge={TLBoundsEdge.Top}
|
edge={TLBoundsEdge.Top}
|
||||||
isHidden={!showHandles}
|
isHidden={!showEdgeHandles}
|
||||||
/>
|
/>
|
||||||
<EdgeHandle
|
<EdgeHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
edge={TLBoundsEdge.Right}
|
edge={TLBoundsEdge.Right}
|
||||||
isHidden={!showHandles}
|
isHidden={!showEdgeHandles}
|
||||||
/>
|
/>
|
||||||
<EdgeHandle
|
<EdgeHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
edge={TLBoundsEdge.Bottom}
|
edge={TLBoundsEdge.Bottom}
|
||||||
isHidden={!showHandles}
|
isHidden={!showEdgeHandles}
|
||||||
/>
|
/>
|
||||||
<EdgeHandle
|
<EdgeHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
edge={TLBoundsEdge.Left}
|
edge={TLBoundsEdge.Left}
|
||||||
isHidden={!showHandles}
|
isHidden={!showEdgeHandles}
|
||||||
/>
|
/>
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
isHidden={isHidden}
|
isHidden={isHidden || !showCornerHandles}
|
||||||
corner={TLBoundsCorner.TopLeft}
|
corner={TLBoundsCorner.TopLeft}
|
||||||
/>
|
/>
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
isHidden={isHidden}
|
isHidden={isHidden || !showCornerHandles}
|
||||||
corner={TLBoundsCorner.TopRight}
|
corner={TLBoundsCorner.TopRight}
|
||||||
/>
|
/>
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
isHidden={isHidden}
|
isHidden={isHidden || !showCornerHandles}
|
||||||
corner={TLBoundsCorner.BottomRight}
|
corner={TLBoundsCorner.BottomRight}
|
||||||
/>
|
/>
|
||||||
<CornerHandle
|
<CornerHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
isHidden={isHidden}
|
isHidden={isHidden || !showCornerHandles}
|
||||||
corner={TLBoundsCorner.BottomLeft}
|
corner={TLBoundsCorner.BottomLeft}
|
||||||
/>
|
/>
|
||||||
<RotateHandle
|
<RotateHandle
|
||||||
targetSize={targetSize}
|
targetSize={targetSize}
|
||||||
size={size}
|
size={size}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
isHidden={!showHandles || !showRotateHandle}
|
isHidden={!showEdgeHandles || !showRotateHandle}
|
||||||
/>
|
/>
|
||||||
{showCloneButtons && <CloneButtons bounds={bounds} />}
|
{showCloneButtons && <CloneButtons bounds={bounds} />}
|
||||||
</SVGContainer>
|
</SVGContainer>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
useCameraCss,
|
useCameraCss,
|
||||||
useKeyEvents,
|
useKeyEvents,
|
||||||
} from '+hooks'
|
} 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 { ErrorFallback } from '+components/error-fallback'
|
||||||
import { ErrorBoundary } from '+components/error-boundary'
|
import { ErrorBoundary } from '+components/error-boundary'
|
||||||
import { Brush } from '+components/brush'
|
import { Brush } from '+components/brush'
|
||||||
|
@ -17,6 +17,8 @@ import { Users } from '+components/users'
|
||||||
import { useResizeObserver } from '+hooks/useResizeObserver'
|
import { useResizeObserver } from '+hooks/useResizeObserver'
|
||||||
import { inputs } from '+inputs'
|
import { inputs } from '+inputs'
|
||||||
import { UsersIndicators } from '+components/users-indicators'
|
import { UsersIndicators } from '+components/users-indicators'
|
||||||
|
import { SnapLines } from '+components/snap-lines/snap-lines'
|
||||||
|
import { Overlay } from '+components/overlay'
|
||||||
|
|
||||||
function resetError() {
|
function resetError() {
|
||||||
void null
|
void null
|
||||||
|
@ -25,6 +27,7 @@ function resetError() {
|
||||||
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
page: TLPage<T, TLBinding>
|
page: TLPage<T, TLBinding>
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
|
snapLines?: TLSnapLine[]
|
||||||
users?: TLUsers<T>
|
users?: TLUsers<T>
|
||||||
userId?: string
|
userId?: string
|
||||||
hideBounds?: boolean
|
hideBounds?: boolean
|
||||||
|
@ -39,6 +42,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
id,
|
id,
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
|
snapLines,
|
||||||
users,
|
users,
|
||||||
userId,
|
userId,
|
||||||
meta,
|
meta,
|
||||||
|
@ -87,6 +91,9 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
{users && <Users userId={userId} users={users} />}
|
{users && <Users userId={userId} users={users} />}
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
<Overlay camera={pageState.camera}>
|
||||||
|
{snapLines && <SnapLines snapLines={snapLines} />}
|
||||||
|
</Overlay>
|
||||||
</div>
|
</div>
|
||||||
</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 { Canvas } from '../canvas'
|
||||||
import { Inputs } from '../../inputs'
|
import { Inputs } from '../../inputs'
|
||||||
import { useTLTheme, TLContext, TLContextType } from '../../hooks'
|
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>
|
export interface RendererProps<T extends TLShape, E extends Element = any, M = any>
|
||||||
extends Partial<TLCallbacks<T>> {
|
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.
|
* (optional) The current users to render.
|
||||||
*/
|
*/
|
||||||
users?: TLUsers<T>
|
users?: TLUsers<T>
|
||||||
|
/**
|
||||||
|
* (optional) The current snap lines to render.
|
||||||
|
*/
|
||||||
|
snapLines?: TLSnapLine[]
|
||||||
/**
|
/**
|
||||||
* (optional) The current user's id, used to identify the user.
|
* (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,
|
userId,
|
||||||
theme,
|
theme,
|
||||||
meta,
|
meta,
|
||||||
|
snapLines,
|
||||||
containerRef,
|
containerRef,
|
||||||
hideHandles = false,
|
hideHandles = false,
|
||||||
hideIndicators = false,
|
hideIndicators = false,
|
||||||
|
@ -132,6 +137,7 @@ export function Renderer<T extends TLShape, E extends Element, M extends Record<
|
||||||
id={id}
|
id={id}
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
|
snapLines={snapLines}
|
||||||
users={users}
|
users={users}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
hideBounds={hideBounds}
|
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 (!inputs.pointerIsValid(e)) return
|
||||||
if (disable) return
|
if (disable) return
|
||||||
|
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
|
if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
|
||||||
|
|
||||||
const info = inputs.pointerMove(e, id)
|
const info = inputs.pointerMove(e, id)
|
||||||
|
|
|
@ -66,6 +66,7 @@ const css = (strings: TemplateStringsArray, ...args: unknown[]) =>
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultTheme: TLTheme = {
|
const defaultTheme: TLTheme = {
|
||||||
|
accent: 'rgb(255, 0, 0)',
|
||||||
brushFill: 'rgba(0,0,0,.05)',
|
brushFill: 'rgba(0,0,0,.05)',
|
||||||
brushStroke: 'rgba(0,0,0,.25)',
|
brushStroke: 'rgba(0,0,0,.25)',
|
||||||
selectStroke: 'rgb(66, 133, 244)',
|
selectStroke: 'rgb(66, 133, 244)',
|
||||||
|
@ -133,6 +134,24 @@ const tlcss = css`
|
||||||
box-sizing: border-box;
|
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 {
|
.tl-canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
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 TLUsers<T extends TLShape, U extends TLUser<T> = TLUser<T>> = Record<string, U>
|
||||||
|
|
||||||
|
export type TLSnapLine = number[][]
|
||||||
|
|
||||||
export interface TLHandle {
|
export interface TLHandle {
|
||||||
id: string
|
id: string
|
||||||
index: number
|
index: number
|
||||||
|
@ -112,6 +114,7 @@ export interface TLBinding<M = any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLTheme {
|
export interface TLTheme {
|
||||||
|
accent?: string
|
||||||
brushFill?: string
|
brushFill?: string
|
||||||
brushStroke?: string
|
brushStroke?: string
|
||||||
selectFill?: string
|
selectFill?: string
|
||||||
|
@ -242,6 +245,11 @@ export interface TLBounds {
|
||||||
rotation?: number
|
rotation?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TLBoundsWithCenter extends TLBounds {
|
||||||
|
midX: number
|
||||||
|
midY: number
|
||||||
|
}
|
||||||
|
|
||||||
export type TLIntersection = {
|
export type TLIntersection = {
|
||||||
didIntersect: boolean
|
didIntersect: boolean
|
||||||
message: string
|
message: string
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type React from 'react'
|
||||||
import { TLBezierCurveSegment, TLBounds, TLBoundsCorner, TLBoundsEdge } from '../types'
|
import { TLBezierCurveSegment, TLBounds, TLBoundsCorner, TLBoundsEdge } from '../types'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import './polyfills'
|
import './polyfills'
|
||||||
import type { Patch } from '+index'
|
import type { Patch, TLBoundsWithCenter } from '+index'
|
||||||
|
|
||||||
export class Utils {
|
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]
|
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 */
|
/* Lists and Collections */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
@ -1684,6 +1824,59 @@ left past the initial left edge) then swap points on that axis.
|
||||||
/* Browser and DOM */
|
/* 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() {
|
static isMobileSize() {
|
||||||
if (typeof window === 'undefined') return false
|
if (typeof window === 'undefined') return false
|
||||||
return window.innerWidth < 768
|
return window.innerWidth < 768
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
"@types/react-router-dom": "^5.1.8",
|
"@types/react-router-dom": "^5.1.8",
|
||||||
"concurrently": "6.0.1",
|
"concurrently": "6.0.1",
|
||||||
"create-serve": "1.0.1",
|
"create-serve": "1.0.1",
|
||||||
"esbuild": "^0.12.26",
|
"esbuild": "^0.13.8",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"typescript": "4.2.3"
|
"typescript": "4.2.3"
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"@types/node": "^16.7.10",
|
"@types/node": "^16.7.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||||
"@typescript-eslint/parser": "^4.30.0",
|
"@typescript-eslint/parser": "^4.30.0",
|
||||||
"esbuild": "^0.12.24",
|
"esbuild": "^0.13.8",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^4.0.0",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
"@types/react-dom": "^16.9.9",
|
"@types/react-dom": "^16.9.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||||
"@typescript-eslint/parser": "^4.30.0",
|
"@typescript-eslint/parser": "^4.30.0",
|
||||||
"esbuild": "^0.12.24",
|
"esbuild": "^0.13.8",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^4.0.0",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
|
|
|
@ -6,10 +6,10 @@ const name = process.env.npm_package_name || ''
|
||||||
async function main() {
|
async function main() {
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
entryPoints: ['./src/index.ts'],
|
entryPoints: ['./src/index.ts'],
|
||||||
outdir: 'dist/cjs',
|
outdir: 'dist/esm',
|
||||||
minify: false,
|
minify: false,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: 'cjs',
|
format: 'esm',
|
||||||
target: 'es6',
|
target: 'es6',
|
||||||
jsxFactory: 'React.createElement',
|
jsxFactory: 'React.createElement',
|
||||||
jsxFragment: 'React.Fragment',
|
jsxFragment: 'React.Fragment',
|
||||||
|
|
|
@ -13,10 +13,10 @@ async function main() {
|
||||||
|
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
entryPoints: ['./src/index.ts'],
|
entryPoints: ['./src/index.ts'],
|
||||||
outdir: 'dist/cjs',
|
outdir: 'dist/esm',
|
||||||
minify: false,
|
minify: false,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: 'cjs',
|
format: 'esm',
|
||||||
target: 'es6',
|
target: 'es6',
|
||||||
jsxFactory: 'React.createElement',
|
jsxFactory: 'React.createElement',
|
||||||
jsxFragment: 'React.Fragment',
|
jsxFragment: 'React.Fragment',
|
||||||
|
|
|
@ -29,6 +29,8 @@ const isHideBoundsShapeSelector = (s: Data) => {
|
||||||
|
|
||||||
const pageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
|
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 usersSelector = (s: Data) => s.room?.users
|
||||||
|
|
||||||
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
|
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
|
||||||
|
@ -149,6 +151,8 @@ function InnerTldraw({
|
||||||
|
|
||||||
const pageState = useSelector(pageStateSelector)
|
const pageState = useSelector(pageStateSelector)
|
||||||
|
|
||||||
|
const snapLines = useSelector(snapLinesSelector)
|
||||||
|
|
||||||
const users = useSelector(usersSelector)
|
const users = useSelector(usersSelector)
|
||||||
|
|
||||||
const isDarkMode = useSelector(isDarkModeSelector)
|
const isDarkMode = useSelector(isDarkModeSelector)
|
||||||
|
@ -217,6 +221,7 @@ function InnerTldraw({
|
||||||
containerRef={rWrapper}
|
containerRef={rWrapper}
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
|
snapLines={snapLines}
|
||||||
users={users}
|
users={users}
|
||||||
userId={tlstate.state.room?.userId}
|
userId={tlstate.state.room?.userId}
|
||||||
shapeUtils={tldrawShapeUtils}
|
shapeUtils={tldrawShapeUtils}
|
||||||
|
|
|
@ -150,56 +150,3 @@ export const defaultStyle: ShapeStyles = {
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
dash: DashStyle.Draw,
|
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 { ShapeUtil, SVGContainer, TLBounds, Utils, TLHandle } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
|
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||||
import {
|
import {
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
Decoration,
|
Decoration,
|
||||||
|
@ -102,7 +102,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
||||||
? renderFreehandArrowShaft(shape)
|
? renderFreehandArrowShaft(shape)
|
||||||
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
|
||||||
arrowDist,
|
arrowDist,
|
||||||
strokeWidth * 1.618,
|
strokeWidth * 1.618,
|
||||||
shape.style.dash,
|
shape.style.dash,
|
||||||
|
@ -154,7 +154,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
||||||
? renderCurvedFreehandArrowShaft(shape, circle, length, easing)
|
? renderCurvedFreehandArrowShaft(shape, circle, length, easing)
|
||||||
: getArrowArcPath(start, end, circle, shape.bend)
|
: getArrowArcPath(start, end, circle, shape.bend)
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
|
||||||
Math.abs(length),
|
Math.abs(length),
|
||||||
sw,
|
sw,
|
||||||
shape.style.dash,
|
shape.style.dash,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react'
|
||||||
import { SVGContainer, Utils, ShapeUtil, TLTransformInfo, TLBounds } from '@tldraw/core'
|
import { SVGContainer, Utils, ShapeUtil, TLTransformInfo, TLBounds } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { DashStyle, EllipseShape, TLDrawShapeType, TLDrawMeta } from '~types'
|
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 { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
||||||
import {
|
import {
|
||||||
intersectBoundsEllipse,
|
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 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,
|
perimeter < 64 ? perimeter * 2 : perimeter,
|
||||||
strokeWidth * 1.618,
|
strokeWidth * 1.618,
|
||||||
shape.style.dash,
|
shape.style.dash,
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Utils, SVGContainer, ShapeUtil } from '@tldraw/core'
|
import { Utils, SVGContainer, ShapeUtil } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import getStroke, { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
||||||
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||||
import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types'
|
import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types'
|
||||||
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
|
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>(() => ({
|
export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||||
type: TLDrawShapeType.Rectangle,
|
type: TLDrawShapeType.Rectangle,
|
||||||
|
@ -36,7 +33,7 @@ export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta
|
||||||
const strokeWidth = +styles.strokeWidth
|
const strokeWidth = +styles.strokeWidth
|
||||||
|
|
||||||
if (style.dash === DashStyle.Draw) {
|
if (style.dash === DashStyle.Draw) {
|
||||||
const pathData = Utils.getFromCache(pathCache, shape.size, () => getRectanglePath(shape))
|
const pathData = getRectanglePath(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
|
<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 paths = strokes.map(([start, end, length], i) => {
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
|
||||||
length,
|
length,
|
||||||
strokeWidth * 1.618,
|
strokeWidth * 1.618,
|
||||||
shape.style.dash
|
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
|
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 { initialShape } = this
|
||||||
|
|
||||||
const page = TLDR.getPage(data, data.appState.currentPageId)
|
const page = TLDR.getPage(data, data.appState.currentPageId)
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class DrawSession implements Session {
|
||||||
|
|
||||||
start = () => void null
|
start = () => void null
|
||||||
|
|
||||||
update = (data: Data, point: number[], shiftKey: boolean) => {
|
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||||
const { shapeId } = this
|
const { shapeId } = this
|
||||||
|
|
||||||
// Even if we're not locked yet, we base the future locking direction
|
// Even if we're not locked yet, we base the future locking direction
|
||||||
|
|
|
@ -60,7 +60,7 @@ export class GridSession implements Session {
|
||||||
return clone
|
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 nextShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||||
|
|
||||||
const nextPageState: Patch<TLPageState> = {}
|
const nextPageState: Patch<TLPageState> = {}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export class HandleSession implements Session {
|
||||||
|
|
||||||
start = () => void null
|
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 { initialShape } = this
|
||||||
const { currentPageId } = data.appState
|
const { currentPageId } = data.appState
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class RotateSession implements Session {
|
||||||
|
|
||||||
start = () => void null
|
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 { commonBoundsCenter, initialShapes } = this.snapshot
|
||||||
|
|
||||||
const pageId = data.appState.currentPageId
|
const pageId = data.appState.currentPageId
|
||||||
|
@ -32,7 +32,7 @@ export class RotateSession implements Session {
|
||||||
|
|
||||||
let directionDelta = Vec.angle(commonBoundsCenter, point) - this.initialAngle
|
let directionDelta = Vec.angle(commonBoundsCenter, point) - this.initialAngle
|
||||||
|
|
||||||
if (isLocked) {
|
if (shiftKey) {
|
||||||
directionDelta = Utils.snapAngleToSegments(directionDelta, 24) // 15 degrees
|
directionDelta = Utils.snapAngleToSegments(directionDelta, 24) // 15 degrees
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ export class RotateSession implements Session {
|
||||||
const { rotation = 0 } = shape
|
const { rotation = 0 } = shape
|
||||||
let shapeDelta = 0
|
let shapeDelta = 0
|
||||||
|
|
||||||
if (isLocked) {
|
if (shiftKey) {
|
||||||
const snappedRotation = Utils.snapAngleToSegments(rotation, 24)
|
const snappedRotation = Utils.snapAngleToSegments(rotation, 24)
|
||||||
shapeDelta = snappedRotation - rotation
|
shapeDelta = snappedRotation - rotation
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export class RotateSession implements Session {
|
||||||
shape,
|
shape,
|
||||||
center,
|
center,
|
||||||
commonBoundsCenter,
|
commonBoundsCenter,
|
||||||
isLocked ? directionDelta + shapeDelta : directionDelta
|
shiftKey ? directionDelta + shapeDelta : directionDelta
|
||||||
)
|
)
|
||||||
|
|
||||||
if (change) {
|
if (change) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class TransformSingleSession implements Session {
|
||||||
|
|
||||||
start = () => void null
|
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 { transformType } = this
|
||||||
|
|
||||||
const { initialShapeBounds, initialShape, id } = this.snapshot
|
const { initialShapeBounds, initialShape, id } = this.snapshot
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class TransformSession implements Session {
|
||||||
start = () => void null
|
start = () => void null
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// 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 {
|
const {
|
||||||
transformType,
|
transformType,
|
||||||
snapshot: { shapeBounds, initialBounds, isAllAspectRatioLocked },
|
snapshot: { shapeBounds, initialBounds, isAllAspectRatioLocked },
|
||||||
|
|
|
@ -326,3 +326,13 @@ describe('When creating with a translate session', () => {
|
||||||
expect(tlstate.getShape('rect1')).toBe(undefined)
|
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 */
|
/* 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 { Vec } from '@tldraw/vec'
|
||||||
import {
|
import {
|
||||||
TLDrawShape,
|
TLDrawShape,
|
||||||
|
@ -11,19 +11,50 @@ import {
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
GroupShape,
|
GroupShape,
|
||||||
SessionType,
|
SessionType,
|
||||||
|
ArrowBinding,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
|
import { SNAP_DISTANCE } from '~state/constants'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
import type { Patch } from 'rko'
|
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 {
|
export class TranslateSession implements Session {
|
||||||
type = SessionType.Translate
|
type = SessionType.Translate
|
||||||
status = TLDrawStatus.Translating
|
status = TLDrawStatus.Translating
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
prev = [0, 0]
|
prev = [0, 0]
|
||||||
|
prevPoint = [0, 0]
|
||||||
|
speed = 1
|
||||||
origin: number[]
|
origin: number[]
|
||||||
snapshot: TranslateSnapshot
|
snapshot: TranslateSnapshot
|
||||||
isCloning = false
|
isCloning = false
|
||||||
isCreate: boolean
|
isCreate: boolean
|
||||||
|
cloneInfo: CloneInfo = {
|
||||||
|
state: 'empty',
|
||||||
|
}
|
||||||
|
snapInfo: SnapInfo = {
|
||||||
|
state: 'empty',
|
||||||
|
}
|
||||||
|
snapLines: TLSnapLine[] = []
|
||||||
|
|
||||||
constructor(data: Data, point: number[], isCreate = false) {
|
constructor(data: Data, point: number[], isCreate = false) {
|
||||||
this.origin = point
|
this.origin = point
|
||||||
|
@ -34,6 +65,8 @@ export class TranslateSession implements Session {
|
||||||
start = (data: Data) => {
|
start = (data: Data) => {
|
||||||
const { bindingsToDelete } = this.snapshot
|
const { bindingsToDelete } = this.snapshot
|
||||||
|
|
||||||
|
this.createSnapInfo(data)
|
||||||
|
|
||||||
if (bindingsToDelete.length === 0) return data
|
if (bindingsToDelete.length === 0) return data
|
||||||
|
|
||||||
const nextBindings: Patch<Record<string, TLDrawBinding>> = {}
|
const nextBindings: Patch<Record<string, TLDrawBinding>> = {}
|
||||||
|
@ -51,16 +84,30 @@ export class TranslateSession implements Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean) => {
|
update = (data: Data, point: number[], shiftKey = false, altKey = false, metaKey = false) => {
|
||||||
const { selectedIds, initialParentChildren, clones, initialShapes, bindingsToDelete } =
|
const { selectedIds, initialParentChildren, initialShapes, bindingsToDelete } = this.snapshot
|
||||||
this.snapshot
|
|
||||||
|
|
||||||
const { currentPageId } = data.appState
|
const { currentPageId } = data.appState
|
||||||
|
|
||||||
const nextBindings: Patch<Record<string, TLDrawBinding>> = {}
|
const nextBindings: Patch<Record<string, TLDrawBinding>> = {}
|
||||||
|
|
||||||
const nextShapes: Patch<Record<string, TLDrawShape>> = {}
|
const nextShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||||
|
|
||||||
const nextPageState: Patch<TLPageState> = {}
|
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 (shiftKey) {
|
||||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
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
|
this.prev = delta
|
||||||
|
|
||||||
// If cloning...
|
// If cloning...
|
||||||
if (!this.isCreate && altKey) {
|
if (this.isCloning) {
|
||||||
// Not Cloning -> Cloning
|
// 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
|
this.isCloning = true
|
||||||
|
|
||||||
// Put back any bindings we deleted
|
// Put back any bindings we deleted
|
||||||
|
@ -109,7 +212,7 @@ export class TranslateSession implements Session {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add the cloned bindings
|
// Add the cloned bindings
|
||||||
for (const binding of this.snapshot.clonedBindings) {
|
for (const binding of clonedBindings) {
|
||||||
nextBindings[binding.id] = binding
|
nextBindings[binding.id] = binding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,6 +220,10 @@ export class TranslateSession implements Session {
|
||||||
nextPageState.selectedIds = clones.map((clone) => clone.id)
|
nextPageState.selectedIds = clones.map((clone) => clone.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.cloneInfo.state === 'empty') throw Error
|
||||||
|
|
||||||
|
const { clones } = this.cloneInfo
|
||||||
|
|
||||||
// Either way, move the clones
|
// Either way, move the clones
|
||||||
clones.forEach((clone) => {
|
clones.forEach((clone) => {
|
||||||
const current = (nextShapes[clone.id] ||
|
const current = (nextShapes[clone.id] ||
|
||||||
|
@ -126,14 +233,18 @@ export class TranslateSession implements Session {
|
||||||
|
|
||||||
nextShapes[clone.id] = {
|
nextShapes[clone.id] = {
|
||||||
...nextShapes[clone.id],
|
...nextShapes[clone.id],
|
||||||
point: Vec.round(Vec.add(current.point, trueDelta)),
|
point: Vec.round(Vec.add(current.point, movement)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// If not cloning...
|
// If not cloning...
|
||||||
|
|
||||||
// Cloning -> 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
|
this.isCloning = false
|
||||||
|
|
||||||
// Delete the bindings
|
// Delete the bindings
|
||||||
|
@ -159,7 +270,7 @@ export class TranslateSession implements Session {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Delete the cloned bindings
|
// Delete the cloned bindings
|
||||||
for (const binding of this.snapshot.clonedBindings) {
|
for (const binding of clonedBindings) {
|
||||||
nextBindings[binding.id] = undefined
|
nextBindings[binding.id] = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,12 +287,15 @@ export class TranslateSession implements Session {
|
||||||
|
|
||||||
nextShapes[shape.id] = {
|
nextShapes[shape.id] = {
|
||||||
...nextShapes[shape.id],
|
...nextShapes[shape.id],
|
||||||
point: Vec.round(Vec.add(current.point, trueDelta)),
|
point: Vec.round(Vec.add(current.point, movement)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
appState: {
|
||||||
|
snapLines: this.snapLines,
|
||||||
|
},
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
[data.appState.currentPageId]: {
|
[data.appState.currentPageId]: {
|
||||||
|
@ -197,7 +311,7 @@ export class TranslateSession implements Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel = (data: Data) => {
|
cancel = (data: Data) => {
|
||||||
const { initialShapes, clones, clonedBindings, bindingsToDelete } = this.snapshot
|
const { initialShapes, bindingsToDelete } = this.snapshot
|
||||||
|
|
||||||
const nextBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
|
const nextBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
|
||||||
const nextShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
|
const nextShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
|
||||||
|
@ -218,13 +332,19 @@ export class TranslateSession implements Session {
|
||||||
nextPageState.selectedIds = this.snapshot.selectedIds
|
nextPageState.selectedIds = this.snapshot.selectedIds
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete clones
|
if (this.cloneInfo.state === 'ready') {
|
||||||
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
|
const { clones, clonedBindings } = this.cloneInfo
|
||||||
|
// Delete clones
|
||||||
|
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
|
||||||
|
|
||||||
// Delete cloned bindings
|
// Delete cloned bindings
|
||||||
clonedBindings.forEach((binding) => (nextBindings[binding.id] = undefined))
|
clonedBindings.forEach((binding) => (nextBindings[binding.id] = undefined))
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
appState: {
|
||||||
|
snapLines: [],
|
||||||
|
},
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
[data.appState.currentPageId]: {
|
[data.appState.currentPageId]: {
|
||||||
|
@ -242,8 +362,7 @@ export class TranslateSession implements Session {
|
||||||
complete(data: Data): TLDrawCommand {
|
complete(data: Data): TLDrawCommand {
|
||||||
const pageId = data.appState.currentPageId
|
const pageId = data.appState.currentPageId
|
||||||
|
|
||||||
const { initialShapes, initialParentChildren, bindingsToDelete, clones, clonedBindings } =
|
const { initialShapes, initialParentChildren, bindingsToDelete } = this.snapshot
|
||||||
this.snapshot
|
|
||||||
|
|
||||||
const beforeBindings: Patch<Record<string, TLDrawBinding>> = {}
|
const beforeBindings: Patch<Record<string, TLDrawBinding>> = {}
|
||||||
const beforeShapes: Patch<Record<string, TLDrawShape>> = {}
|
const beforeShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||||
|
@ -252,6 +371,13 @@ export class TranslateSession implements Session {
|
||||||
const afterShapes: Patch<Record<string, TLDrawShape>> = {}
|
const afterShapes: Patch<Record<string, TLDrawShape>> = {}
|
||||||
|
|
||||||
if (this.isCloning) {
|
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
|
// Update the clones
|
||||||
clones.forEach((clone) => {
|
clones.forEach((clone) => {
|
||||||
beforeShapes[clone.id] = undefined
|
beforeShapes[clone.id] = undefined
|
||||||
|
@ -331,6 +457,9 @@ export class TranslateSession implements Session {
|
||||||
return {
|
return {
|
||||||
id: 'translate',
|
id: 'translate',
|
||||||
before: {
|
before: {
|
||||||
|
appState: {
|
||||||
|
snapLines: [],
|
||||||
|
},
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
[data.appState.currentPageId]: {
|
[data.appState.currentPageId]: {
|
||||||
|
@ -346,6 +475,9 @@ export class TranslateSession implements Session {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
after: {
|
after: {
|
||||||
|
appState: {
|
||||||
|
snapLines: [],
|
||||||
|
},
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
[data.appState.currentPageId]: {
|
[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
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
@ -382,6 +632,20 @@ export function getTranslateSnapshot(data: Data) {
|
||||||
: [shape]
|
: [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[]> = {}
|
const initialParentChildren: Record<string, string[]> = {}
|
||||||
|
|
||||||
Array.from(new Set(shapesToMove.map((s) => s.parentId)).values())
|
Array.from(new Set(shapesToMove.map((s) => s.parentId)).values())
|
||||||
|
@ -391,102 +655,20 @@ export function getTranslateSnapshot(data: Data) {
|
||||||
initialParentChildren[id] = shape.children!
|
initialParentChildren[id] = shape.children!
|
||||||
})
|
})
|
||||||
|
|
||||||
const cloneMap: Record<string, string> = {}
|
const commonBounds = Utils.getCommonBounds(shapesToMove.map(TLDR.getBounds))
|
||||||
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")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedIds,
|
selectedIds,
|
||||||
bindingsToDelete,
|
|
||||||
hasUnlockedShapes,
|
hasUnlockedShapes,
|
||||||
initialParentChildren,
|
initialParentChildren,
|
||||||
|
shapesToMove,
|
||||||
|
bindingsToDelete,
|
||||||
|
commonBounds,
|
||||||
initialShapes: shapesToMove.map(({ id, point, parentId }) => ({
|
initialShapes: shapesToMove.map(({ id, point, parentId }) => ({
|
||||||
id,
|
id,
|
||||||
point,
|
point,
|
||||||
parentId,
|
parentId,
|
||||||
})),
|
})),
|
||||||
clones,
|
|
||||||
clonedBindings,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ import { ArgsOfType, getSession } from './session'
|
||||||
import { sample, USER_COLORS } from './utils'
|
import { sample, USER_COLORS } from './utils'
|
||||||
import { createTools, ToolType } from './tool'
|
import { createTools, ToolType } from './tool'
|
||||||
import type { BaseTool } from './tool/BaseTool'
|
import type { BaseTool } from './tool/BaseTool'
|
||||||
|
import * as constants from './constants'
|
||||||
|
|
||||||
const uuid = Utils.uniqueId()
|
const uuid = Utils.uniqueId()
|
||||||
|
|
||||||
|
@ -1392,7 +1393,10 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
const bounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds))
|
const bounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds))
|
||||||
|
|
||||||
let zoom = TLDR.getCameraZoom(
|
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 =
|
zoom =
|
||||||
|
@ -1419,7 +1423,10 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
const bounds = TLDR.getSelectedBounds(this.state)
|
const bounds = TLDR.getSelectedBounds(this.state)
|
||||||
|
|
||||||
let zoom = TLDR.getCameraZoom(
|
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 =
|
zoom =
|
||||||
|
@ -1645,10 +1652,13 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
* updateSession.
|
* updateSession.
|
||||||
* @param args The arguments of the current session's update method.
|
* @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
|
const { session } = this
|
||||||
if (!session) return 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
|
if (!patch) return this
|
||||||
return this.patchState(patch, `session:${session?.constructor.name}`)
|
return this.patchState(patch, `session:${session?.constructor.name}`)
|
||||||
}
|
}
|
||||||
|
@ -1713,6 +1723,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
// the shape we just created.
|
// the shape we just created.
|
||||||
result.before = {
|
result.before = {
|
||||||
appState: {
|
appState: {
|
||||||
|
...result.before.appState,
|
||||||
status: TLDrawStatus.Idle,
|
status: TLDrawStatus.Idle,
|
||||||
},
|
},
|
||||||
document: {
|
document: {
|
||||||
|
@ -1741,6 +1752,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
}
|
}
|
||||||
|
|
||||||
result.after.appState = {
|
result.after.appState = {
|
||||||
|
...result.after.appState,
|
||||||
status: TLDrawStatus.Idle,
|
status: TLDrawStatus.Idle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2373,6 +2385,23 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
return Vec.round([this.bounds.width / 2, this.bounds.height / 2])
|
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 version = 10.1
|
||||||
|
|
||||||
static defaultDocument: TLDrawDocument = {
|
static defaultDocument: TLDrawDocument = {
|
||||||
|
@ -2420,6 +2449,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
isStyleOpen: false,
|
isStyleOpen: false,
|
||||||
isEmptyCanvas: false,
|
isEmptyCanvas: false,
|
||||||
status: TLDrawStatus.Idle,
|
status: TLDrawStatus.Idle,
|
||||||
|
snapLines: [],
|
||||||
},
|
},
|
||||||
document: TLDrawState.defaultDocument,
|
document: TLDrawState.defaultDocument,
|
||||||
room: {
|
room: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/ban-types */
|
/* 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 { TLShape, TLShapeUtil, TLHandle } from '@tldraw/core'
|
||||||
import type { TLPage, TLUser, TLPageState } from '@tldraw/core'
|
import type { TLPage, TLUser, TLPageState } from '@tldraw/core'
|
||||||
import type { StoreApi } from 'zustand'
|
import type { StoreApi } from 'zustand'
|
||||||
|
@ -63,6 +63,7 @@ export interface Data {
|
||||||
isStyleOpen: boolean
|
isStyleOpen: boolean
|
||||||
isEmptyCanvas: boolean
|
isEmptyCanvas: boolean
|
||||||
status: string
|
status: string
|
||||||
|
snapLines: TLSnapLine[]
|
||||||
}
|
}
|
||||||
document: TLDrawDocument
|
document: TLDrawDocument
|
||||||
room?: {
|
room?: {
|
||||||
|
@ -105,9 +106,9 @@ export abstract class Session {
|
||||||
abstract update: (
|
abstract update: (
|
||||||
data: Readonly<Data>,
|
data: Readonly<Data>,
|
||||||
point: number[],
|
point: number[],
|
||||||
shiftKey: boolean,
|
shiftKey?: boolean,
|
||||||
altKey: boolean,
|
altKey?: boolean,
|
||||||
metaKey: boolean
|
metaKey?: boolean
|
||||||
) => TLDrawPatch | undefined
|
) => TLDrawPatch | undefined
|
||||||
abstract complete: (data: Readonly<Data>) => TLDrawPatch | TLDrawCommand | undefined
|
abstract complete: (data: Readonly<Data>) => TLDrawPatch | TLDrawCommand | undefined
|
||||||
abstract cancel: (data: Readonly<Data>) => TLDrawPatch | undefined
|
abstract cancel: (data: Readonly<Data>) => TLDrawPatch | undefined
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"@types/node": "^16.7.10",
|
"@types/node": "^16.7.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||||
"@typescript-eslint/parser": "^4.30.0",
|
"@typescript-eslint/parser": "^4.30.0",
|
||||||
"esbuild": "^0.12.24",
|
"esbuild": "^0.13.8",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^4.0.0",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
|
|
118
yarn.lock
118
yarn.lock
|
@ -6319,10 +6319,113 @@ es6-promisify@^5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es6-promise "^4.0.3"
|
es6-promise "^4.0.3"
|
||||||
|
|
||||||
esbuild@^0.12.24, esbuild@^0.12.26:
|
esbuild-android-arm64@0.13.8:
|
||||||
version "0.12.28"
|
version "0.13.8"
|
||||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.28.tgz#84da0d2a0d0dee181281545271e0d65cf6fab1ef"
|
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.8.tgz#c20e875c3c98164b1ffba9b28637bdf96f5e9e7c"
|
||||||
integrity sha512-pZ0FrWZXlvQOATlp14lRSk1N9GkeJ3vLIwOcUoo3ICQn9WNR4rWoNi81pbn6sC1iYUy7QPqNzI3+AEzokwyVcA==
|
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:
|
escalade@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
|
@ -7616,11 +7719,16 @@ idb-keyval@^5.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
safari-14-idb-fix "^1.0.6"
|
safari-14-idb-fix "^1.0.6"
|
||||||
|
|
||||||
idb@^6.0.0, idb@^6.1.2:
|
idb@^6.0.0:
|
||||||
version "6.1.3"
|
version "6.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.3.tgz#e6cd3b9c38f5c696a82a4b435754f3873c5a7891"
|
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.3.tgz#e6cd3b9c38f5c696a82a4b435754f3873c5a7891"
|
||||||
integrity sha512-oIRDpVcs5KXpI1hRnTJUwkY63RB/7iqu9nSNuzXN8TLHjs7oO20IoPFbBTsqxIL5IjzIUDi+FXlVcK4zm26J8A==
|
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:
|
ieee754@^1.1.4, ieee754@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
|
|
Loading…
Reference in a new issue