@tldraw/editor 3.9.0-canary.ffd990988638 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +1 -1
- package/dist-cjs/index.d.ts +39 -7
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +431 -249
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +7 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgAsImage.js +1 -1
- package/dist-cjs/lib/exports/getSvgAsImage.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/globals/environment.js +3 -1
- package/dist-cjs/lib/globals/environment.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +1 -1
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/utils/browserCanvasMaxSize.js +104 -28
- package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +3 -3
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +39 -7
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +427 -245
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +7 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgAsImage.mjs +1 -1
- package/dist-esm/lib/exports/getSvgAsImage.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/globals/environment.mjs +3 -1
- package/dist-esm/lib/globals/environment.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +1 -1
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/utils/browserCanvasMaxSize.mjs +104 -18
- package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -9
- package/src/index.ts +2 -0
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +5 -3
- package/src/lib/editor/Editor.ts +556 -273
- package/src/lib/editor/shapes/ShapeUtil.ts +32 -5
- package/src/lib/exports/getSvgAsImage.ts +1 -1
- package/src/lib/exports/getSvgJsx.tsx +1 -0
- package/src/lib/globals/environment.ts +3 -0
- package/src/lib/license/LicenseManager.test.ts +16 -13
- package/src/lib/license/LicenseManager.ts +2 -2
- package/src/lib/utils/browserCanvasMaxSize.ts +121 -21
- package/src/version.ts +3 -3
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
structuredClone,
|
|
84
84
|
uniqueId,
|
|
85
85
|
} from '@tldraw/utils'
|
|
86
|
+
import { Number } from 'core-js'
|
|
86
87
|
import EventEmitter from 'eventemitter3'
|
|
87
88
|
import {
|
|
88
89
|
TLEditorSnapshot,
|
|
@@ -116,7 +117,7 @@ import { EASINGS } from '../primitives/easings'
|
|
|
116
117
|
import { Geometry2d } from '../primitives/geometry/Geometry2d'
|
|
117
118
|
import { Group2d } from '../primitives/geometry/Group2d'
|
|
118
119
|
import { intersectPolygonPolygon } from '../primitives/intersect'
|
|
119
|
-
import {
|
|
120
|
+
import { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from '../primitives/utils'
|
|
120
121
|
import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'
|
|
121
122
|
import { dataUrlToFile } from '../utils/assets'
|
|
122
123
|
import { debugFlags } from '../utils/debug-flags'
|
|
@@ -144,7 +145,7 @@ import { SnapManager } from './managers/SnapManager/SnapManager'
|
|
|
144
145
|
import { TextManager } from './managers/TextManager'
|
|
145
146
|
import { TickManager } from './managers/TickManager'
|
|
146
147
|
import { UserPreferencesManager } from './managers/UserPreferencesManager'
|
|
147
|
-
import { ShapeUtil, TLResizeMode } from './shapes/ShapeUtil'
|
|
148
|
+
import { ShapeUtil, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
|
|
148
149
|
import { RootState } from './tools/RootState'
|
|
149
150
|
import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
|
|
150
151
|
import { TLContent } from './types/clipboard-types'
|
|
@@ -4203,14 +4204,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4203
4204
|
|
|
4204
4205
|
/* --------------------- Shapes --------------------- */
|
|
4205
4206
|
|
|
4206
|
-
|
|
4207
|
-
private _getShapeGeometryCache(): ComputedCache<Geometry2d, TLShape> {
|
|
4208
|
-
return this.store.createComputedCache(
|
|
4209
|
-
'bounds',
|
|
4210
|
-
(shape) => this.getShapeUtil(shape).getGeometry(shape),
|
|
4211
|
-
(a, b) => a.props === b.props
|
|
4212
|
-
)
|
|
4213
|
-
}
|
|
4207
|
+
private _shapeGeometryCaches: Record<string, ComputedCache<Geometry2d, TLShape>> = {}
|
|
4214
4208
|
|
|
4215
4209
|
/**
|
|
4216
4210
|
* Get the geometry of a shape.
|
|
@@ -4219,14 +4213,26 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4219
4213
|
* ```ts
|
|
4220
4214
|
* editor.getShapeGeometry(myShape)
|
|
4221
4215
|
* editor.getShapeGeometry(myShapeId)
|
|
4216
|
+
* editor.getShapeGeometry(myShapeId, { context: "arrow" })
|
|
4222
4217
|
* ```
|
|
4223
4218
|
*
|
|
4224
4219
|
* @param shape - The shape (or shape id) to get the geometry for.
|
|
4220
|
+
* @param opts - Additional options about the request for geometry. Passed to {@link ShapeUtil.getGeometry}.
|
|
4225
4221
|
*
|
|
4226
4222
|
* @public
|
|
4227
4223
|
*/
|
|
4228
|
-
getShapeGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId): T {
|
|
4229
|
-
|
|
4224
|
+
getShapeGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId, opts?: TLGeometryOpts): T {
|
|
4225
|
+
const context = opts?.context ?? 'none'
|
|
4226
|
+
if (!this._shapeGeometryCaches[context]) {
|
|
4227
|
+
this._shapeGeometryCaches[context] = this.store.createComputedCache(
|
|
4228
|
+
'bounds',
|
|
4229
|
+
(shape) => this.getShapeUtil(shape).getGeometry(shape, opts),
|
|
4230
|
+
{ areRecordsEqual: (a, b) => a.props === b.props }
|
|
4231
|
+
)
|
|
4232
|
+
}
|
|
4233
|
+
return this._shapeGeometryCaches[context].get(
|
|
4234
|
+
typeof shape === 'string' ? shape : shape.id
|
|
4235
|
+
)! as T
|
|
4230
4236
|
}
|
|
4231
4237
|
|
|
4232
4238
|
/** @internal */
|
|
@@ -5685,14 +5691,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5685
5691
|
return this
|
|
5686
5692
|
}
|
|
5687
5693
|
|
|
5694
|
+
// Gets a shape partial that includes life cycle changes: on translate start, on translate, on translate end
|
|
5688
5695
|
private getChangesToTranslateShape(initialShape: TLShape, newShapeCoords: VecLike): TLShape {
|
|
5689
5696
|
let workingShape = initialShape
|
|
5690
5697
|
const util = this.getShapeUtil(initialShape)
|
|
5691
5698
|
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5699
|
+
const afterTranslateStart = util.onTranslateStart?.(workingShape)
|
|
5700
|
+
if (afterTranslateStart) {
|
|
5701
|
+
workingShape = applyPartialToRecordWithProps(workingShape, afterTranslateStart)
|
|
5702
|
+
}
|
|
5696
5703
|
|
|
5697
5704
|
workingShape = applyPartialToRecordWithProps(workingShape, {
|
|
5698
5705
|
id: initialShape.id,
|
|
@@ -5701,15 +5708,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5701
5708
|
y: newShapeCoords.y,
|
|
5702
5709
|
})
|
|
5703
5710
|
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5711
|
+
const afterTranslate = util.onTranslate?.(initialShape, workingShape)
|
|
5712
|
+
if (afterTranslate) {
|
|
5713
|
+
workingShape = applyPartialToRecordWithProps(workingShape, afterTranslate)
|
|
5714
|
+
}
|
|
5708
5715
|
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5716
|
+
const afterTranslateEnd = util.onTranslateEnd?.(initialShape, workingShape)
|
|
5717
|
+
if (afterTranslateEnd) {
|
|
5718
|
+
workingShape = applyPartialToRecordWithProps(workingShape, afterTranslateEnd)
|
|
5719
|
+
}
|
|
5713
5720
|
|
|
5714
5721
|
return workingShape
|
|
5715
5722
|
}
|
|
@@ -6108,6 +6115,37 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6108
6115
|
return this
|
|
6109
6116
|
}
|
|
6110
6117
|
|
|
6118
|
+
/**
|
|
6119
|
+
* @internal
|
|
6120
|
+
*/
|
|
6121
|
+
private collectShapesViaArrowBindings(info: {
|
|
6122
|
+
initialShapes: TLShape[]
|
|
6123
|
+
resultShapes: TLShape[]
|
|
6124
|
+
resultBounds: Box[]
|
|
6125
|
+
bindings: TLBinding[]
|
|
6126
|
+
visited: Set<TLShapeId>
|
|
6127
|
+
}) {
|
|
6128
|
+
const { initialShapes, resultShapes, resultBounds, bindings, visited } = info
|
|
6129
|
+
for (const binding of bindings) {
|
|
6130
|
+
for (const id of [binding.fromId, binding.toId]) {
|
|
6131
|
+
if (!visited.has(id)) {
|
|
6132
|
+
const aligningShape = initialShapes.find((s) => s.id === id)
|
|
6133
|
+
if (aligningShape && !visited.has(aligningShape.id)) {
|
|
6134
|
+
visited.add(aligningShape.id)
|
|
6135
|
+
const shapePageBounds = this.getShapePageBounds(aligningShape)
|
|
6136
|
+
if (!shapePageBounds) continue
|
|
6137
|
+
resultShapes.push(aligningShape)
|
|
6138
|
+
resultBounds.push(shapePageBounds)
|
|
6139
|
+
this.collectShapesViaArrowBindings({
|
|
6140
|
+
...info,
|
|
6141
|
+
bindings: this.getBindingsInvolvingShape(aligningShape, 'arrow'),
|
|
6142
|
+
})
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
|
|
6111
6149
|
/**
|
|
6112
6150
|
* Flip shape positions.
|
|
6113
6151
|
*
|
|
@@ -6123,47 +6161,74 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6123
6161
|
* @public
|
|
6124
6162
|
*/
|
|
6125
6163
|
flipShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
|
|
6164
|
+
if (this.getIsReadonly()) return this
|
|
6165
|
+
|
|
6126
6166
|
const ids =
|
|
6127
6167
|
typeof shapes[0] === 'string'
|
|
6128
6168
|
? (shapes as TLShapeId[])
|
|
6129
6169
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6130
6170
|
|
|
6131
|
-
|
|
6171
|
+
// Collect a greedy list of shapes to flip
|
|
6172
|
+
const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6132
6173
|
|
|
6133
|
-
|
|
6174
|
+
for (const shape of shapesToFlipFirstPass) {
|
|
6175
|
+
if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
|
6176
|
+
const childrenOfGroups = compact(
|
|
6177
|
+
this.getSortedChildIdsForParent(shape.id).map((id) => this.getShape(id))
|
|
6178
|
+
)
|
|
6179
|
+
shapesToFlipFirstPass.push(...childrenOfGroups)
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
6134
6182
|
|
|
6135
|
-
|
|
6183
|
+
// exclude shapes that can't be flipped
|
|
6184
|
+
const shapesToFlip: {
|
|
6185
|
+
shape: TLShape
|
|
6186
|
+
localBounds: Box
|
|
6187
|
+
pageTransform: Mat
|
|
6188
|
+
isAspectRatioLocked: boolean
|
|
6189
|
+
}[] = []
|
|
6136
6190
|
|
|
6137
|
-
|
|
6138
|
-
shapesToFlip
|
|
6139
|
-
.map((shape) => {
|
|
6140
|
-
if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
|
6141
|
-
return this.getSortedChildIdsForParent(shape.id).map((id) => this.getShape(id))
|
|
6142
|
-
}
|
|
6191
|
+
const allBounds: Box[] = []
|
|
6143
6192
|
|
|
6144
|
-
|
|
6193
|
+
for (const shape of shapesToFlipFirstPass) {
|
|
6194
|
+
const util = this.getShapeUtil(shape)
|
|
6195
|
+
if (
|
|
6196
|
+
!util.canBeLaidOut(shape, {
|
|
6197
|
+
type: 'flip',
|
|
6198
|
+
shapes: shapesToFlipFirstPass,
|
|
6145
6199
|
})
|
|
6146
|
-
|
|
6147
|
-
|
|
6200
|
+
) {
|
|
6201
|
+
continue
|
|
6202
|
+
}
|
|
6203
|
+
|
|
6204
|
+
const pageBounds = this.getShapePageBounds(shape)
|
|
6205
|
+
const localBounds = this.getShapeGeometry(shape).bounds
|
|
6206
|
+
const pageTransform = this.getShapePageTransform(shape.id)
|
|
6207
|
+
if (!(pageBounds && localBounds && pageTransform)) continue
|
|
6208
|
+
shapesToFlip.push({
|
|
6209
|
+
shape,
|
|
6210
|
+
localBounds,
|
|
6211
|
+
pageTransform,
|
|
6212
|
+
isAspectRatioLocked: util.isAspectRatioLocked(shape),
|
|
6213
|
+
})
|
|
6214
|
+
allBounds.push(pageBounds)
|
|
6215
|
+
}
|
|
6216
|
+
|
|
6217
|
+
if (!shapesToFlip.length) return this
|
|
6148
6218
|
|
|
6149
|
-
const scaleOriginPage = Box.Common(
|
|
6150
|
-
compact(shapesToFlip.map((id) => this.getShapePageBounds(id)))
|
|
6151
|
-
).center
|
|
6219
|
+
const scaleOriginPage = Box.Common(allBounds).center
|
|
6152
6220
|
|
|
6153
6221
|
this.run(() => {
|
|
6154
|
-
for (const shape of shapesToFlip) {
|
|
6155
|
-
const bounds = this.getShapeGeometry(shape).bounds
|
|
6156
|
-
const initialPageTransform = this.getShapePageTransform(shape.id)
|
|
6157
|
-
if (!initialPageTransform) continue
|
|
6222
|
+
for (const { shape, localBounds, pageTransform, isAspectRatioLocked } of shapesToFlip) {
|
|
6158
6223
|
this.resizeShape(
|
|
6159
6224
|
shape.id,
|
|
6160
6225
|
{ x: operation === 'horizontal' ? -1 : 1, y: operation === 'vertical' ? -1 : 1 },
|
|
6161
6226
|
{
|
|
6162
|
-
initialBounds:
|
|
6163
|
-
initialPageTransform,
|
|
6227
|
+
initialBounds: localBounds,
|
|
6228
|
+
initialPageTransform: pageTransform,
|
|
6164
6229
|
initialShape: shape,
|
|
6230
|
+
isAspectRatioLocked,
|
|
6165
6231
|
mode: 'scale_shape',
|
|
6166
|
-
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
|
|
6167
6232
|
scaleOrigin: scaleOriginPage,
|
|
6168
6233
|
scaleAxisRotation: 0,
|
|
6169
6234
|
}
|
|
@@ -6200,21 +6265,58 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6200
6265
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6201
6266
|
if (this.getIsReadonly()) return this
|
|
6202
6267
|
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6268
|
+
// todo: this has a lot of extra code to handle stacking with custom gaps or auto gaps or other things like that. I don't think anyone has ever used this stuff.
|
|
6269
|
+
|
|
6270
|
+
// always fresh shapes
|
|
6271
|
+
const shapesToStackFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6272
|
+
|
|
6273
|
+
const shapeClustersToStack: {
|
|
6274
|
+
shapes: TLShape[]
|
|
6275
|
+
pageBounds: Box
|
|
6276
|
+
}[] = []
|
|
6277
|
+
const allBounds: Box[] = []
|
|
6278
|
+
const visited = new Set<TLShapeId>()
|
|
6279
|
+
|
|
6280
|
+
for (const shape of shapesToStackFirstPass) {
|
|
6281
|
+
if (visited.has(shape.id)) continue
|
|
6282
|
+
visited.add(shape.id)
|
|
6283
|
+
|
|
6284
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6285
|
+
if (!shapePageBounds) continue
|
|
6286
|
+
|
|
6287
|
+
if (
|
|
6288
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
6289
|
+
type: 'stack',
|
|
6290
|
+
shapes: shapesToStackFirstPass,
|
|
6291
|
+
})
|
|
6292
|
+
) {
|
|
6293
|
+
continue
|
|
6294
|
+
}
|
|
6295
|
+
|
|
6296
|
+
const shapesMovingTogether = [shape]
|
|
6297
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
6207
6298
|
|
|
6208
|
-
|
|
6299
|
+
this.collectShapesViaArrowBindings({
|
|
6300
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6301
|
+
initialShapes: shapesToStackFirstPass,
|
|
6302
|
+
resultShapes: shapesMovingTogether,
|
|
6303
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6304
|
+
visited,
|
|
6209
6305
|
})
|
|
6210
6306
|
|
|
6211
|
-
|
|
6307
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6308
|
+
if (!commonPageBounds) continue
|
|
6212
6309
|
|
|
6213
|
-
|
|
6310
|
+
shapeClustersToStack.push({
|
|
6311
|
+
shapes: shapesMovingTogether,
|
|
6312
|
+
pageBounds: commonPageBounds,
|
|
6313
|
+
})
|
|
6214
6314
|
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6315
|
+
allBounds.push(commonPageBounds)
|
|
6316
|
+
}
|
|
6317
|
+
|
|
6318
|
+
const len = shapeClustersToStack.length
|
|
6319
|
+
if ((gap === 0 && len < 3) || len < 2) return this
|
|
6218
6320
|
|
|
6219
6321
|
let val: 'x' | 'y'
|
|
6220
6322
|
let min: 'minX' | 'minY'
|
|
@@ -6233,46 +6335,45 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6233
6335
|
dim = 'height'
|
|
6234
6336
|
}
|
|
6235
6337
|
|
|
6236
|
-
let shapeGap: number
|
|
6338
|
+
let shapeGap: number = 0
|
|
6237
6339
|
|
|
6238
6340
|
if (gap === 0) {
|
|
6239
|
-
|
|
6341
|
+
// note: this is not used in the current tldraw.com; there we use a specified stack
|
|
6342
|
+
|
|
6343
|
+
const gaps: Record<number, number> = {}
|
|
6240
6344
|
|
|
6241
|
-
|
|
6345
|
+
shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])
|
|
6242
6346
|
|
|
6243
6347
|
// Collect all of the gaps between shapes. We want to find
|
|
6244
6348
|
// patterns (equal gaps between shapes) and use the most common
|
|
6245
6349
|
// one as the gap for all of the shapes.
|
|
6246
6350
|
for (let i = 0; i < len - 1; i++) {
|
|
6247
|
-
const
|
|
6248
|
-
const
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
const gap = nextBounds[min] - bounds[max]
|
|
6254
|
-
|
|
6255
|
-
const current = gaps.find((g) => g.gap === gap)
|
|
6256
|
-
|
|
6257
|
-
if (current) {
|
|
6258
|
-
current.count++
|
|
6259
|
-
} else {
|
|
6260
|
-
gaps.push({ gap, count: 1 })
|
|
6351
|
+
const currCluster = shapeClustersToStack[i]
|
|
6352
|
+
const nextCluster = shapeClustersToStack[i + 1]
|
|
6353
|
+
const gap = nextCluster.pageBounds[min] - currCluster.pageBounds[max]
|
|
6354
|
+
if (!gaps[gap]) {
|
|
6355
|
+
gaps[gap] = 0
|
|
6261
6356
|
}
|
|
6357
|
+
gaps[gap]++
|
|
6262
6358
|
}
|
|
6263
6359
|
|
|
6264
6360
|
// Which gap is the most common?
|
|
6265
|
-
let maxCount =
|
|
6266
|
-
|
|
6267
|
-
if (
|
|
6268
|
-
maxCount =
|
|
6269
|
-
shapeGap =
|
|
6361
|
+
let maxCount = 1
|
|
6362
|
+
for (const [gap, count] of Object.entries(gaps)) {
|
|
6363
|
+
if (count > maxCount) {
|
|
6364
|
+
maxCount = count
|
|
6365
|
+
shapeGap = parseFloat(gap)
|
|
6270
6366
|
}
|
|
6271
|
-
}
|
|
6367
|
+
}
|
|
6272
6368
|
|
|
6273
6369
|
// If there is no most-common gap, use the average gap.
|
|
6274
6370
|
if (maxCount === 1) {
|
|
6275
|
-
|
|
6371
|
+
let totalCount = 0
|
|
6372
|
+
for (const [gap, count] of Object.entries(gaps)) {
|
|
6373
|
+
shapeGap += parseFloat(gap) * count
|
|
6374
|
+
totalCount += count
|
|
6375
|
+
}
|
|
6376
|
+
shapeGap /= totalCount
|
|
6276
6377
|
}
|
|
6277
6378
|
} else {
|
|
6278
6379
|
// If a gap was provided, then use that instead.
|
|
@@ -6281,36 +6382,30 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6281
6382
|
|
|
6282
6383
|
const changes: TLShapePartial[] = []
|
|
6283
6384
|
|
|
6284
|
-
let v =
|
|
6385
|
+
let v = shapeClustersToStack[0].pageBounds[max]
|
|
6285
6386
|
|
|
6286
|
-
|
|
6287
|
-
|
|
6387
|
+
for (let i = 1; i < shapeClustersToStack.length; i++) {
|
|
6388
|
+
const { shapes, pageBounds } = shapeClustersToStack[i]
|
|
6389
|
+
const delta = new Vec()
|
|
6390
|
+
delta[val] = v + shapeGap - pageBounds[val]
|
|
6288
6391
|
|
|
6289
|
-
const
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
const parent = this.getShapeParent(shape)
|
|
6293
|
-
const localDelta = parent
|
|
6294
|
-
? Vec.Rot(delta, -this.getShapePageTransform(parent)!.decompose().rotation)
|
|
6295
|
-
: delta
|
|
6392
|
+
for (const shape of shapes) {
|
|
6393
|
+
const shapeDelta = delta.clone()
|
|
6296
6394
|
|
|
6297
|
-
|
|
6395
|
+
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
6396
|
+
// todo: ensure that the parent isn't being aligned together with its children
|
|
6397
|
+
const parent = this.getShapeParent(shape)
|
|
6398
|
+
if (parent) {
|
|
6399
|
+
const parentTransform = this.getShapePageTransform(parent)
|
|
6400
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6401
|
+
}
|
|
6298
6402
|
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
...translateStartChanges,
|
|
6303
|
-
[val]: shape[val] + localDelta[val],
|
|
6304
|
-
}
|
|
6305
|
-
: {
|
|
6306
|
-
id: shape.id as any,
|
|
6307
|
-
type: shape.type,
|
|
6308
|
-
[val]: shape[val] + localDelta[val],
|
|
6309
|
-
}
|
|
6310
|
-
)
|
|
6403
|
+
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
6404
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6405
|
+
}
|
|
6311
6406
|
|
|
6312
|
-
v += pageBounds[
|
|
6313
|
-
}
|
|
6407
|
+
v += pageBounds[dim] + shapeGap
|
|
6408
|
+
}
|
|
6314
6409
|
|
|
6315
6410
|
this.updateShapes(changes)
|
|
6316
6411
|
return this
|
|
@@ -6330,42 +6425,79 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6330
6425
|
* @param gap - The padding to apply to the packed shapes. Defaults to 16.
|
|
6331
6426
|
*/
|
|
6332
6427
|
packShapes(shapes: TLShapeId[] | TLShape[], gap: number): this {
|
|
6428
|
+
if (this.getIsReadonly()) return this
|
|
6429
|
+
|
|
6333
6430
|
const ids =
|
|
6334
6431
|
typeof shapes[0] === 'string'
|
|
6335
6432
|
? (shapes as TLShapeId[])
|
|
6336
6433
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6337
6434
|
|
|
6338
|
-
|
|
6339
|
-
|
|
6435
|
+
// Always fresh shapes
|
|
6436
|
+
const shapesToPackFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6340
6437
|
|
|
6341
|
-
const
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6438
|
+
const shapeClustersToPack: {
|
|
6439
|
+
shapes: TLShape[]
|
|
6440
|
+
pageBounds: Box
|
|
6441
|
+
nextPageBounds: Box
|
|
6442
|
+
}[] = []
|
|
6345
6443
|
|
|
6346
|
-
|
|
6444
|
+
const allBounds: Box[] = []
|
|
6445
|
+
const visited = new Set<TLShapeId>()
|
|
6446
|
+
|
|
6447
|
+
for (const shape of shapesToPackFirstPass) {
|
|
6448
|
+
if (visited.has(shape.id)) continue
|
|
6449
|
+
visited.add(shape.id)
|
|
6450
|
+
|
|
6451
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6452
|
+
if (!shapePageBounds) continue
|
|
6453
|
+
|
|
6454
|
+
if (
|
|
6455
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
6456
|
+
type: 'pack',
|
|
6457
|
+
shapes: shapesToPackFirstPass,
|
|
6458
|
+
})
|
|
6459
|
+
) {
|
|
6460
|
+
continue
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6463
|
+
const shapesMovingTogether = [shape]
|
|
6464
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
6465
|
+
|
|
6466
|
+
this.collectShapesViaArrowBindings({
|
|
6467
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6468
|
+
initialShapes: shapesToPackFirstPass,
|
|
6469
|
+
resultShapes: shapesMovingTogether,
|
|
6470
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6471
|
+
visited,
|
|
6347
6472
|
})
|
|
6348
|
-
const shapePageBounds: Record<string, Box> = {}
|
|
6349
|
-
const nextShapePageBounds: Record<string, Box> = {}
|
|
6350
6473
|
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6474
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6475
|
+
if (!commonPageBounds) continue
|
|
6476
|
+
|
|
6477
|
+
shapeClustersToPack.push({
|
|
6478
|
+
shapes: shapesMovingTogether,
|
|
6479
|
+
pageBounds: commonPageBounds,
|
|
6480
|
+
nextPageBounds: commonPageBounds.clone(),
|
|
6481
|
+
})
|
|
6482
|
+
|
|
6483
|
+
allBounds.push(commonPageBounds)
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
if (shapeClustersToPack.length < 2) return this
|
|
6354
6487
|
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
shapePageBounds[shape.id] = bounds
|
|
6359
|
-
nextShapePageBounds[shape.id] = bounds.clone()
|
|
6360
|
-
area += bounds.width * bounds.height
|
|
6488
|
+
let area = 0
|
|
6489
|
+
for (const { pageBounds } of shapeClustersToPack) {
|
|
6490
|
+
area += pageBounds.width * pageBounds.height
|
|
6361
6491
|
}
|
|
6362
6492
|
|
|
6363
|
-
const commonBounds = Box.Common(
|
|
6493
|
+
const commonBounds = Box.Common(allBounds)
|
|
6364
6494
|
|
|
6365
6495
|
const maxWidth = commonBounds.width
|
|
6366
6496
|
|
|
6367
|
-
// sort the
|
|
6368
|
-
|
|
6497
|
+
// sort the shape clusters by width and then height, descending
|
|
6498
|
+
shapeClustersToPack
|
|
6499
|
+
.sort((a, b) => a.pageBounds.width - b.pageBounds.width)
|
|
6500
|
+
.sort((a, b) => a.pageBounds.height - b.pageBounds.height)
|
|
6369
6501
|
|
|
6370
6502
|
// Start with is (sort of) the square of the area
|
|
6371
6503
|
const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth)
|
|
@@ -6378,85 +6510,69 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6378
6510
|
let space: Box
|
|
6379
6511
|
let last: Box
|
|
6380
6512
|
|
|
6381
|
-
for (
|
|
6382
|
-
shape = shapesToPack[i]
|
|
6383
|
-
bounds = nextShapePageBounds[shape.id]
|
|
6384
|
-
|
|
6513
|
+
for (const { nextPageBounds } of shapeClustersToPack) {
|
|
6385
6514
|
// starting at the back (smaller shapes)
|
|
6386
6515
|
for (let i = spaces.length - 1; i >= 0; i--) {
|
|
6387
6516
|
space = spaces[i]
|
|
6388
6517
|
|
|
6389
6518
|
// find a space that is big enough to contain the shape
|
|
6390
|
-
if (
|
|
6519
|
+
if (nextPageBounds.width > space.width || nextPageBounds.height > space.height) continue
|
|
6391
6520
|
|
|
6392
6521
|
// add the shape to its top-left corner
|
|
6393
|
-
|
|
6394
|
-
|
|
6522
|
+
nextPageBounds.x = space.x
|
|
6523
|
+
nextPageBounds.y = space.y
|
|
6395
6524
|
|
|
6396
|
-
height = Math.max(height,
|
|
6397
|
-
width = Math.max(width,
|
|
6525
|
+
height = Math.max(height, nextPageBounds.maxY)
|
|
6526
|
+
width = Math.max(width, nextPageBounds.maxX)
|
|
6398
6527
|
|
|
6399
|
-
if (
|
|
6528
|
+
if (nextPageBounds.width === space.width && nextPageBounds.height === space.height) {
|
|
6400
6529
|
// remove the space on a perfect fit
|
|
6401
6530
|
last = spaces.pop()!
|
|
6402
6531
|
if (i < spaces.length) spaces[i] = last
|
|
6403
|
-
} else if (
|
|
6532
|
+
} else if (nextPageBounds.height === space.height) {
|
|
6404
6533
|
// fit the shape into the space (width)
|
|
6405
|
-
space.x +=
|
|
6406
|
-
space.width -=
|
|
6407
|
-
} else if (
|
|
6534
|
+
space.x += nextPageBounds.width + gap
|
|
6535
|
+
space.width -= nextPageBounds.width + gap
|
|
6536
|
+
} else if (nextPageBounds.width === space.width) {
|
|
6408
6537
|
// fit the shape into the space (height)
|
|
6409
|
-
space.y +=
|
|
6410
|
-
space.height -=
|
|
6538
|
+
space.y += nextPageBounds.height + gap
|
|
6539
|
+
space.height -= nextPageBounds.height + gap
|
|
6411
6540
|
} else {
|
|
6412
6541
|
// split the space into two spaces
|
|
6413
6542
|
spaces.push(
|
|
6414
6543
|
new Box(
|
|
6415
|
-
space.x + (
|
|
6544
|
+
space.x + (nextPageBounds.width + gap),
|
|
6416
6545
|
space.y,
|
|
6417
|
-
space.width - (
|
|
6418
|
-
|
|
6546
|
+
space.width - (nextPageBounds.width + gap),
|
|
6547
|
+
nextPageBounds.height
|
|
6419
6548
|
)
|
|
6420
6549
|
)
|
|
6421
|
-
space.y +=
|
|
6422
|
-
space.height -=
|
|
6550
|
+
space.y += nextPageBounds.height + gap
|
|
6551
|
+
space.height -= nextPageBounds.height + gap
|
|
6423
6552
|
}
|
|
6424
6553
|
break
|
|
6425
6554
|
}
|
|
6426
6555
|
}
|
|
6427
6556
|
|
|
6428
|
-
const commonAfter = Box.Common(
|
|
6557
|
+
const commonAfter = Box.Common(shapeClustersToPack.map((s) => s.nextPageBounds))
|
|
6429
6558
|
const centerDelta = Vec.Sub(commonBounds.center, commonAfter.center)
|
|
6430
6559
|
|
|
6431
|
-
let nextBounds: Box
|
|
6432
|
-
|
|
6433
6560
|
const changes: TLShapePartial<any>[] = []
|
|
6434
6561
|
|
|
6435
|
-
for (
|
|
6436
|
-
|
|
6437
|
-
bounds = shapePageBounds[shape.id]
|
|
6438
|
-
nextBounds = nextShapePageBounds[shape.id]
|
|
6562
|
+
for (const { shapes, pageBounds, nextPageBounds } of shapeClustersToPack) {
|
|
6563
|
+
const delta = Vec.Sub(nextPageBounds.point, pageBounds.point).add(centerDelta)
|
|
6439
6564
|
|
|
6440
|
-
const
|
|
6441
|
-
|
|
6442
|
-
if (parentTransform) delta.rot(-parentTransform.rotation())
|
|
6443
|
-
|
|
6444
|
-
const change: TLShapePartial = {
|
|
6445
|
-
id: shape.id,
|
|
6446
|
-
type: shape.type,
|
|
6447
|
-
x: shape.x + delta.x,
|
|
6448
|
-
y: shape.y + delta.y,
|
|
6449
|
-
}
|
|
6565
|
+
for (const shape of shapes) {
|
|
6566
|
+
const shapeDelta = delta.clone()
|
|
6450
6567
|
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6568
|
+
const parent = this.getShapeParent(shape)
|
|
6569
|
+
if (parent) {
|
|
6570
|
+
const parentTransform = this.getShapeParentTransform(shape)
|
|
6571
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6572
|
+
}
|
|
6455
6573
|
|
|
6456
|
-
|
|
6457
|
-
changes.push(
|
|
6458
|
-
} else {
|
|
6459
|
-
changes.push(change)
|
|
6574
|
+
shapeDelta.add(shape)
|
|
6575
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6460
6576
|
}
|
|
6461
6577
|
}
|
|
6462
6578
|
|
|
@@ -6481,32 +6597,78 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6481
6597
|
*
|
|
6482
6598
|
* @public
|
|
6483
6599
|
*/
|
|
6484
|
-
|
|
6485
6600
|
alignShapes(
|
|
6486
6601
|
shapes: TLShapeId[] | TLShape[],
|
|
6487
6602
|
operation: 'left' | 'center-horizontal' | 'right' | 'top' | 'center-vertical' | 'bottom'
|
|
6488
6603
|
): this {
|
|
6604
|
+
if (this.getIsReadonly()) return this
|
|
6605
|
+
|
|
6489
6606
|
const ids =
|
|
6490
6607
|
typeof shapes[0] === 'string'
|
|
6491
6608
|
? (shapes as TLShapeId[])
|
|
6492
6609
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6493
6610
|
|
|
6494
|
-
|
|
6495
|
-
|
|
6611
|
+
// Always get fresh shapes
|
|
6612
|
+
const shapesToAlignFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6496
6613
|
|
|
6497
|
-
const
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
const
|
|
6614
|
+
const shapeClustersToAlign: {
|
|
6615
|
+
shapes: TLShape[]
|
|
6616
|
+
pageBounds: Box
|
|
6617
|
+
}[] = []
|
|
6618
|
+
const allBounds: Box[] = []
|
|
6619
|
+
const visited = new Set<TLShapeId>()
|
|
6502
6620
|
|
|
6503
|
-
const
|
|
6621
|
+
for (const shape of shapesToAlignFirstPass) {
|
|
6622
|
+
if (visited.has(shape.id)) continue
|
|
6623
|
+
visited.add(shape.id)
|
|
6504
6624
|
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6625
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6626
|
+
if (!shapePageBounds) continue
|
|
6627
|
+
|
|
6628
|
+
if (
|
|
6629
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
6630
|
+
type: 'align',
|
|
6631
|
+
shapes: shapesToAlignFirstPass,
|
|
6632
|
+
})
|
|
6633
|
+
) {
|
|
6634
|
+
continue
|
|
6635
|
+
}
|
|
6636
|
+
|
|
6637
|
+
// In this implementation, we want to create psuedo-groups out of shapes that
|
|
6638
|
+
// are moving together. At the moment shapes only move together if they're connected
|
|
6639
|
+
// by arrows. So let's say A -> B -> C -> D and A, B, and C are selected. If we're
|
|
6640
|
+
// aligning A, B, and C, then we want these to move together as one unit.
|
|
6641
|
+
|
|
6642
|
+
const shapesMovingTogether = [shape]
|
|
6643
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
6644
|
+
|
|
6645
|
+
this.collectShapesViaArrowBindings({
|
|
6646
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6647
|
+
initialShapes: shapesToAlignFirstPass,
|
|
6648
|
+
resultShapes: shapesMovingTogether,
|
|
6649
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6650
|
+
visited,
|
|
6651
|
+
})
|
|
6508
6652
|
|
|
6509
|
-
const
|
|
6653
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6654
|
+
if (!commonPageBounds) continue
|
|
6655
|
+
|
|
6656
|
+
shapeClustersToAlign.push({
|
|
6657
|
+
shapes: shapesMovingTogether,
|
|
6658
|
+
pageBounds: commonPageBounds,
|
|
6659
|
+
})
|
|
6660
|
+
|
|
6661
|
+
allBounds.push(commonPageBounds)
|
|
6662
|
+
}
|
|
6663
|
+
|
|
6664
|
+
if (shapeClustersToAlign.length < 2) return this
|
|
6665
|
+
|
|
6666
|
+
const commonBounds = Box.Common(allBounds)
|
|
6667
|
+
|
|
6668
|
+
const changes: TLShapePartial[] = []
|
|
6669
|
+
|
|
6670
|
+
shapeClustersToAlign.forEach(({ shapes, pageBounds }) => {
|
|
6671
|
+
const delta = new Vec()
|
|
6510
6672
|
|
|
6511
6673
|
switch (operation) {
|
|
6512
6674
|
case 'top': {
|
|
@@ -6535,12 +6697,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6535
6697
|
}
|
|
6536
6698
|
}
|
|
6537
6699
|
|
|
6538
|
-
const
|
|
6539
|
-
|
|
6540
|
-
? Vec.Rot(delta, -this.getShapePageTransform(parent)!.decompose().rotation)
|
|
6541
|
-
: delta
|
|
6700
|
+
for (const shape of shapes) {
|
|
6701
|
+
const shapeDelta = delta.clone()
|
|
6542
6702
|
|
|
6543
|
-
|
|
6703
|
+
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
6704
|
+
// todo: ensure that the parent isn't being aligned together with its children
|
|
6705
|
+
const parent = this.getShapeParent(shape)
|
|
6706
|
+
if (parent) {
|
|
6707
|
+
const parentTransform = this.getShapePageTransform(parent)
|
|
6708
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6711
|
+
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
6712
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6713
|
+
}
|
|
6544
6714
|
})
|
|
6545
6715
|
|
|
6546
6716
|
this.updateShapes(changes)
|
|
@@ -6562,65 +6732,137 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6562
6732
|
* @public
|
|
6563
6733
|
*/
|
|
6564
6734
|
distributeShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
|
|
6735
|
+
if (this.getIsReadonly()) return this
|
|
6736
|
+
|
|
6565
6737
|
const ids =
|
|
6566
6738
|
typeof shapes[0] === 'string'
|
|
6567
6739
|
? (shapes as TLShapeId[])
|
|
6568
6740
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6569
6741
|
|
|
6570
|
-
|
|
6571
|
-
|
|
6742
|
+
// always fresh shapes
|
|
6743
|
+
const shapesToDistributeFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6572
6744
|
|
|
6573
|
-
const
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6745
|
+
const shapeClustersToDistribute: {
|
|
6746
|
+
shapes: TLShape[]
|
|
6747
|
+
pageBounds: Box
|
|
6748
|
+
}[] = []
|
|
6749
|
+
|
|
6750
|
+
const allBounds: Box[] = []
|
|
6751
|
+
const visited = new Set<TLShapeId>()
|
|
6752
|
+
|
|
6753
|
+
for (const shape of shapesToDistributeFirstPass) {
|
|
6754
|
+
if (visited.has(shape.id)) continue
|
|
6755
|
+
visited.add(shape.id)
|
|
6756
|
+
|
|
6757
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6758
|
+
if (!shapePageBounds) continue
|
|
6759
|
+
|
|
6760
|
+
if (
|
|
6761
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
6762
|
+
type: 'distribute',
|
|
6763
|
+
shapes: shapesToDistributeFirstPass,
|
|
6764
|
+
})
|
|
6765
|
+
) {
|
|
6766
|
+
continue
|
|
6767
|
+
}
|
|
6768
|
+
|
|
6769
|
+
const shapesMovingTogether = [shape]
|
|
6770
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
6771
|
+
|
|
6772
|
+
this.collectShapesViaArrowBindings({
|
|
6773
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6774
|
+
initialShapes: shapesToDistributeFirstPass,
|
|
6775
|
+
resultShapes: shapesMovingTogether,
|
|
6776
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6777
|
+
visited,
|
|
6778
|
+
})
|
|
6779
|
+
|
|
6780
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6781
|
+
if (!commonPageBounds) continue
|
|
6782
|
+
|
|
6783
|
+
shapeClustersToDistribute.push({
|
|
6784
|
+
shapes: shapesMovingTogether,
|
|
6785
|
+
pageBounds: commonPageBounds,
|
|
6786
|
+
})
|
|
6787
|
+
|
|
6788
|
+
allBounds.push(commonPageBounds)
|
|
6789
|
+
}
|
|
6790
|
+
|
|
6791
|
+
if (shapeClustersToDistribute.length < 3) return this
|
|
6578
6792
|
|
|
6579
6793
|
let val: 'x' | 'y'
|
|
6580
6794
|
let min: 'minX' | 'minY'
|
|
6581
6795
|
let max: 'maxX' | 'maxY'
|
|
6582
|
-
let mid: 'midX' | 'midY'
|
|
6583
6796
|
let dim: 'width' | 'height'
|
|
6584
6797
|
|
|
6585
6798
|
if (operation === 'horizontal') {
|
|
6586
6799
|
val = 'x'
|
|
6587
6800
|
min = 'minX'
|
|
6588
6801
|
max = 'maxX'
|
|
6589
|
-
mid = 'midX'
|
|
6590
6802
|
dim = 'width'
|
|
6591
6803
|
} else {
|
|
6592
6804
|
val = 'y'
|
|
6593
6805
|
min = 'minY'
|
|
6594
6806
|
max = 'maxY'
|
|
6595
|
-
mid = 'midY'
|
|
6596
6807
|
dim = 'height'
|
|
6597
6808
|
}
|
|
6598
6809
|
const changes: TLShapePartial[] = []
|
|
6599
6810
|
|
|
6600
|
-
|
|
6601
|
-
const
|
|
6602
|
-
(a, b) => pageBounds[a.id][min] - pageBounds[b.id][min]
|
|
6603
|
-
)[0]
|
|
6604
|
-
const last = shapesToDistribute.sort((a, b) => pageBounds[b.id][max] - pageBounds[a.id][max])[0]
|
|
6811
|
+
const first = shapeClustersToDistribute.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])[0]
|
|
6812
|
+
const last = shapeClustersToDistribute.sort((a, b) => b.pageBounds[max] - a.pageBounds[max])[0]
|
|
6605
6813
|
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6814
|
+
// If the first shape group is also the last shape group, distribute without it
|
|
6815
|
+
if (first === last) {
|
|
6816
|
+
const excludedShapeIds = new Set(first.shapes.map((s) => s.id))
|
|
6817
|
+
return this.distributeShapes(
|
|
6818
|
+
ids.filter((id) => !excludedShapeIds.has(id)),
|
|
6819
|
+
operation
|
|
6820
|
+
)
|
|
6821
|
+
}
|
|
6609
6822
|
|
|
6610
|
-
|
|
6823
|
+
const shapeClustersToMove = shapeClustersToDistribute
|
|
6611
6824
|
.filter((shape) => shape !== first && shape !== last)
|
|
6612
|
-
.sort((a, b) =>
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6825
|
+
.sort((a, b) => {
|
|
6826
|
+
if (a.pageBounds[min] === b.pageBounds[min]) {
|
|
6827
|
+
return a.shapes[0].id < b.shapes[0].id ? -1 : 1
|
|
6828
|
+
}
|
|
6829
|
+
return a.pageBounds[min] - b.pageBounds[min]
|
|
6830
|
+
})
|
|
6831
|
+
|
|
6832
|
+
// The gap is the amount of space "left over" between the first and last shape. This can be a negative number if the shapes are overlapping.
|
|
6833
|
+
const maxFirst = first.pageBounds[max]
|
|
6834
|
+
const range = last.pageBounds[min] - maxFirst
|
|
6835
|
+
const summedShapeDimensions = shapeClustersToMove.reduce((acc, s) => acc + s.pageBounds[dim], 0)
|
|
6836
|
+
const gap = (range - summedShapeDimensions) / (shapeClustersToMove.length + 1)
|
|
6837
|
+
|
|
6838
|
+
for (let v = maxFirst + gap, i = 0; i < shapeClustersToMove.length; i++) {
|
|
6839
|
+
const { shapes, pageBounds } = shapeClustersToMove[i]
|
|
6840
|
+
const delta = new Vec()
|
|
6841
|
+
delta[val] = v - pageBounds[val]
|
|
6616
6842
|
|
|
6843
|
+
// If for some reason the new position would be more than the maximum, we need to adjust the delta
|
|
6844
|
+
// This will likely throw off some of the other placements but hey, it's better than changing the common bounds
|
|
6845
|
+
if (v + pageBounds[dim] > last.pageBounds[max] - 1) {
|
|
6846
|
+
delta[val] = last.pageBounds[max] - pageBounds[max] - 1
|
|
6847
|
+
}
|
|
6848
|
+
|
|
6849
|
+
for (const shape of shapes) {
|
|
6850
|
+
const shapeDelta = delta.clone()
|
|
6851
|
+
|
|
6852
|
+
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
6853
|
+
// todo: ensure that the parent isn't being aligned together with its children
|
|
6617
6854
|
const parent = this.getShapeParent(shape)
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6855
|
+
if (parent) {
|
|
6856
|
+
const parentTransform = this.getShapePageTransform(parent)
|
|
6857
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6858
|
+
}
|
|
6621
6859
|
|
|
6622
|
-
|
|
6623
|
-
|
|
6860
|
+
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
6861
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6862
|
+
}
|
|
6863
|
+
|
|
6864
|
+
v += pageBounds[dim] + gap
|
|
6865
|
+
}
|
|
6624
6866
|
|
|
6625
6867
|
this.updateShapes(changes)
|
|
6626
6868
|
return this
|
|
@@ -6647,65 +6889,106 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6647
6889
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6648
6890
|
|
|
6649
6891
|
if (this.getIsReadonly()) return this
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
const
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
}
|
|
6892
|
+
|
|
6893
|
+
// always fresh shapes, skip anything that isn't rotated 90 deg
|
|
6894
|
+
const shapesToStretchFirstPass = compact(ids.map((id) => this.getShape(id))).filter(
|
|
6895
|
+
(s) => this.getShapePageTransform(s)?.rotation() % (PI / 2) === 0
|
|
6896
|
+
)
|
|
6897
|
+
|
|
6898
|
+
const shapeClustersToStretch: {
|
|
6899
|
+
shapes: TLShape[]
|
|
6900
|
+
pageBounds: Box
|
|
6901
|
+
}[] = []
|
|
6902
|
+
|
|
6903
|
+
const allBounds: Box[] = []
|
|
6904
|
+
const visited = new Set<TLShapeId>()
|
|
6905
|
+
|
|
6906
|
+
for (const shape of shapesToStretchFirstPass) {
|
|
6907
|
+
if (visited.has(shape.id)) continue
|
|
6908
|
+
visited.add(shape.id)
|
|
6909
|
+
|
|
6910
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6911
|
+
if (!shapePageBounds) continue
|
|
6912
|
+
|
|
6913
|
+
const shapesMovingTogether = [shape]
|
|
6914
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
6915
|
+
|
|
6916
|
+
if (
|
|
6917
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
6918
|
+
type: 'stretch',
|
|
6919
|
+
shapes: shapesToStretchFirstPass,
|
|
6679
6920
|
})
|
|
6680
|
-
|
|
6921
|
+
) {
|
|
6922
|
+
continue
|
|
6681
6923
|
}
|
|
6682
|
-
case 'horizontal': {
|
|
6683
|
-
this.run(() => {
|
|
6684
|
-
for (const shape of shapesToStretch) {
|
|
6685
|
-
const bounds = shapeBounds[shape.id]
|
|
6686
|
-
const pageBounds = shapePageBounds[shape.id]
|
|
6687
|
-
const pageRotation = this.getShapePageTransform(shape)!.rotation()
|
|
6688
|
-
if (pageRotation % PI2) continue
|
|
6689
|
-
const localOffset = new Vec(commonBounds.minX - pageBounds.minX, 0)
|
|
6690
|
-
const parentTransform = this.getShapeParentTransform(shape)
|
|
6691
|
-
if (parentTransform) localOffset.rot(-parentTransform.rotation())
|
|
6692
|
-
|
|
6693
|
-
const { x, y } = Vec.Add(localOffset, shape)
|
|
6694
|
-
this.updateShapes([{ id: shape.id, type: shape.type, x, y }])
|
|
6695
|
-
const scale = new Vec(commonBounds.width / pageBounds.width, 1)
|
|
6696
|
-
this.resizeShape(shape.id, scale, {
|
|
6697
|
-
initialBounds: bounds,
|
|
6698
|
-
scaleOrigin: new Vec(commonBounds.minX, pageBounds.center.y),
|
|
6699
|
-
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
|
|
6700
|
-
scaleAxisRotation: 0,
|
|
6701
|
-
})
|
|
6702
|
-
}
|
|
6703
|
-
})
|
|
6704
6924
|
|
|
6705
|
-
|
|
6706
|
-
|
|
6925
|
+
this.collectShapesViaArrowBindings({
|
|
6926
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6927
|
+
initialShapes: shapesToStretchFirstPass,
|
|
6928
|
+
resultShapes: shapesMovingTogether,
|
|
6929
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6930
|
+
visited,
|
|
6931
|
+
})
|
|
6932
|
+
|
|
6933
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6934
|
+
if (!commonPageBounds) continue
|
|
6935
|
+
|
|
6936
|
+
shapeClustersToStretch.push({
|
|
6937
|
+
shapes: shapesMovingTogether,
|
|
6938
|
+
pageBounds: commonPageBounds,
|
|
6939
|
+
})
|
|
6940
|
+
|
|
6941
|
+
allBounds.push(commonPageBounds)
|
|
6942
|
+
}
|
|
6943
|
+
|
|
6944
|
+
if (shapeClustersToStretch.length < 2) return this
|
|
6945
|
+
|
|
6946
|
+
const commonBounds = Box.Common(allBounds)
|
|
6947
|
+
let val: 'x' | 'y'
|
|
6948
|
+
let min: 'minX' | 'minY'
|
|
6949
|
+
let dim: 'width' | 'height'
|
|
6950
|
+
|
|
6951
|
+
if (operation === 'horizontal') {
|
|
6952
|
+
val = 'x'
|
|
6953
|
+
min = 'minX'
|
|
6954
|
+
dim = 'width'
|
|
6955
|
+
} else {
|
|
6956
|
+
val = 'y'
|
|
6957
|
+
min = 'minY'
|
|
6958
|
+
dim = 'height'
|
|
6707
6959
|
}
|
|
6708
6960
|
|
|
6961
|
+
this.run(() => {
|
|
6962
|
+
shapeClustersToStretch.forEach(({ shapes, pageBounds }) => {
|
|
6963
|
+
const localOffset = new Vec()
|
|
6964
|
+
localOffset[val] = commonBounds[min] - pageBounds[min]
|
|
6965
|
+
|
|
6966
|
+
const scaleOrigin = pageBounds.center.clone()
|
|
6967
|
+
scaleOrigin[val] = commonBounds[min]
|
|
6968
|
+
|
|
6969
|
+
const scale = new Vec(1, 1)
|
|
6970
|
+
scale[val] = commonBounds[dim] / pageBounds[dim]
|
|
6971
|
+
|
|
6972
|
+
for (const shape of shapes) {
|
|
6973
|
+
// First translate
|
|
6974
|
+
const shapeLocalOffset = localOffset.clone()
|
|
6975
|
+
const parentTransform = this.getShapeParentTransform(shape)
|
|
6976
|
+
if (parentTransform) localOffset.rot(-parentTransform.rotation())
|
|
6977
|
+
shapeLocalOffset.add(shape)
|
|
6978
|
+
const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
|
|
6979
|
+
this.updateShape(changes)
|
|
6980
|
+
|
|
6981
|
+
// Then resize
|
|
6982
|
+
this.resizeShape(shape.id, scale, {
|
|
6983
|
+
initialBounds: this.getShapeGeometry(shape).bounds,
|
|
6984
|
+
scaleOrigin,
|
|
6985
|
+
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
|
|
6986
|
+
scaleAxisRotation: 0,
|
|
6987
|
+
})
|
|
6988
|
+
}
|
|
6989
|
+
})
|
|
6990
|
+
})
|
|
6991
|
+
|
|
6709
6992
|
return this
|
|
6710
6993
|
}
|
|
6711
6994
|
|
|
@@ -9973,7 +10256,7 @@ function withIsolatedShapes<T>(
|
|
|
9973
10256
|
}
|
|
9974
10257
|
})
|
|
9975
10258
|
|
|
9976
|
-
editor.store.applyDiff(reverseRecordsDiff(changes))
|
|
10259
|
+
editor.store.applyDiff(reverseRecordsDiff(changes), { runCallbacks: false })
|
|
9977
10260
|
},
|
|
9978
10261
|
{ history: 'ignore' }
|
|
9979
10262
|
)
|