@tldraw/editor 3.9.0-canary.d799df28e99e → 3.9.0-canary.ef20f7307209
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 +20 -0
- package/README.md +1 -1
- package/dist-cjs/index.d.ts +23 -6
- 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 +413 -237
- 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/getSvgJsx.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +23 -6
- 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 +409 -233
- 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/getSvgJsx.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +1 -0
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +5 -3
- package/src/lib/editor/Editor.ts +540 -262
- package/src/lib/editor/shapes/ShapeUtil.ts +21 -5
- package/src/lib/exports/getSvgJsx.tsx +1 -0
- 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'
|
|
@@ -4226,7 +4227,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4226
4227
|
this._shapeGeometryCaches[context] = this.store.createComputedCache(
|
|
4227
4228
|
'bounds',
|
|
4228
4229
|
(shape) => this.getShapeUtil(shape).getGeometry(shape, opts),
|
|
4229
|
-
(a, b) => a.props === b.props
|
|
4230
|
+
{ areRecordsEqual: (a, b) => a.props === b.props }
|
|
4230
4231
|
)
|
|
4231
4232
|
}
|
|
4232
4233
|
return this._shapeGeometryCaches[context].get(
|
|
@@ -5690,14 +5691,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5690
5691
|
return this
|
|
5691
5692
|
}
|
|
5692
5693
|
|
|
5694
|
+
// Gets a shape partial that includes life cycle changes: on translate start, on translate, on translate end
|
|
5693
5695
|
private getChangesToTranslateShape(initialShape: TLShape, newShapeCoords: VecLike): TLShape {
|
|
5694
5696
|
let workingShape = initialShape
|
|
5695
5697
|
const util = this.getShapeUtil(initialShape)
|
|
5696
5698
|
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5699
|
+
const afterTranslateStart = util.onTranslateStart?.(workingShape)
|
|
5700
|
+
if (afterTranslateStart) {
|
|
5701
|
+
workingShape = applyPartialToRecordWithProps(workingShape, afterTranslateStart)
|
|
5702
|
+
}
|
|
5701
5703
|
|
|
5702
5704
|
workingShape = applyPartialToRecordWithProps(workingShape, {
|
|
5703
5705
|
id: initialShape.id,
|
|
@@ -5706,15 +5708,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5706
5708
|
y: newShapeCoords.y,
|
|
5707
5709
|
})
|
|
5708
5710
|
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5711
|
+
const afterTranslate = util.onTranslate?.(initialShape, workingShape)
|
|
5712
|
+
if (afterTranslate) {
|
|
5713
|
+
workingShape = applyPartialToRecordWithProps(workingShape, afterTranslate)
|
|
5714
|
+
}
|
|
5713
5715
|
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5716
|
+
const afterTranslateEnd = util.onTranslateEnd?.(initialShape, workingShape)
|
|
5717
|
+
if (afterTranslateEnd) {
|
|
5718
|
+
workingShape = applyPartialToRecordWithProps(workingShape, afterTranslateEnd)
|
|
5719
|
+
}
|
|
5718
5720
|
|
|
5719
5721
|
return workingShape
|
|
5720
5722
|
}
|
|
@@ -6113,6 +6115,37 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6113
6115
|
return this
|
|
6114
6116
|
}
|
|
6115
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
|
+
|
|
6116
6149
|
/**
|
|
6117
6150
|
* Flip shape positions.
|
|
6118
6151
|
*
|
|
@@ -6128,47 +6161,74 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6128
6161
|
* @public
|
|
6129
6162
|
*/
|
|
6130
6163
|
flipShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
|
|
6164
|
+
if (this.getIsReadonly()) return this
|
|
6165
|
+
|
|
6131
6166
|
const ids =
|
|
6132
6167
|
typeof shapes[0] === 'string'
|
|
6133
6168
|
? (shapes as TLShapeId[])
|
|
6134
6169
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6135
6170
|
|
|
6136
|
-
|
|
6171
|
+
// Collect a greedy list of shapes to flip
|
|
6172
|
+
const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6137
6173
|
|
|
6138
|
-
|
|
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
|
+
}
|
|
6139
6182
|
|
|
6140
|
-
|
|
6183
|
+
// exclude shapes that can't be flipped
|
|
6184
|
+
const shapesToFlip: {
|
|
6185
|
+
shape: TLShape
|
|
6186
|
+
localBounds: Box
|
|
6187
|
+
pageTransform: Mat
|
|
6188
|
+
isAspectRatioLocked: boolean
|
|
6189
|
+
}[] = []
|
|
6141
6190
|
|
|
6142
|
-
|
|
6143
|
-
shapesToFlip
|
|
6144
|
-
.map((shape) => {
|
|
6145
|
-
if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
|
6146
|
-
return this.getSortedChildIdsForParent(shape.id).map((id) => this.getShape(id))
|
|
6147
|
-
}
|
|
6191
|
+
const allBounds: Box[] = []
|
|
6148
6192
|
|
|
6149
|
-
|
|
6193
|
+
for (const shape of shapesToFlipFirstPass) {
|
|
6194
|
+
const util = this.getShapeUtil(shape)
|
|
6195
|
+
if (
|
|
6196
|
+
!util.canBeLaidOut(shape, {
|
|
6197
|
+
type: 'flip',
|
|
6198
|
+
shapes: shapesToFlipFirstPass,
|
|
6150
6199
|
})
|
|
6151
|
-
|
|
6152
|
-
|
|
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
|
|
6153
6218
|
|
|
6154
|
-
const scaleOriginPage = Box.Common(
|
|
6155
|
-
compact(shapesToFlip.map((id) => this.getShapePageBounds(id)))
|
|
6156
|
-
).center
|
|
6219
|
+
const scaleOriginPage = Box.Common(allBounds).center
|
|
6157
6220
|
|
|
6158
6221
|
this.run(() => {
|
|
6159
|
-
for (const shape of shapesToFlip) {
|
|
6160
|
-
const bounds = this.getShapeGeometry(shape).bounds
|
|
6161
|
-
const initialPageTransform = this.getShapePageTransform(shape.id)
|
|
6162
|
-
if (!initialPageTransform) continue
|
|
6222
|
+
for (const { shape, localBounds, pageTransform, isAspectRatioLocked } of shapesToFlip) {
|
|
6163
6223
|
this.resizeShape(
|
|
6164
6224
|
shape.id,
|
|
6165
6225
|
{ x: operation === 'horizontal' ? -1 : 1, y: operation === 'vertical' ? -1 : 1 },
|
|
6166
6226
|
{
|
|
6167
|
-
initialBounds:
|
|
6168
|
-
initialPageTransform,
|
|
6227
|
+
initialBounds: localBounds,
|
|
6228
|
+
initialPageTransform: pageTransform,
|
|
6169
6229
|
initialShape: shape,
|
|
6230
|
+
isAspectRatioLocked,
|
|
6170
6231
|
mode: 'scale_shape',
|
|
6171
|
-
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
|
|
6172
6232
|
scaleOrigin: scaleOriginPage,
|
|
6173
6233
|
scaleAxisRotation: 0,
|
|
6174
6234
|
}
|
|
@@ -6205,21 +6265,58 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6205
6265
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6206
6266
|
if (this.getIsReadonly()) return this
|
|
6207
6267
|
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
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>()
|
|
6212
6279
|
|
|
6213
|
-
|
|
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]
|
|
6298
|
+
|
|
6299
|
+
this.collectShapesViaArrowBindings({
|
|
6300
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6301
|
+
initialShapes: shapesToStackFirstPass,
|
|
6302
|
+
resultShapes: shapesMovingTogether,
|
|
6303
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6304
|
+
visited,
|
|
6214
6305
|
})
|
|
6215
6306
|
|
|
6216
|
-
|
|
6307
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6308
|
+
if (!commonPageBounds) continue
|
|
6217
6309
|
|
|
6218
|
-
|
|
6310
|
+
shapeClustersToStack.push({
|
|
6311
|
+
shapes: shapesMovingTogether,
|
|
6312
|
+
pageBounds: commonPageBounds,
|
|
6313
|
+
})
|
|
6219
6314
|
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6315
|
+
allBounds.push(commonPageBounds)
|
|
6316
|
+
}
|
|
6317
|
+
|
|
6318
|
+
const len = shapeClustersToStack.length
|
|
6319
|
+
if ((gap === 0 && len < 3) || len < 2) return this
|
|
6223
6320
|
|
|
6224
6321
|
let val: 'x' | 'y'
|
|
6225
6322
|
let min: 'minX' | 'minY'
|
|
@@ -6238,46 +6335,45 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6238
6335
|
dim = 'height'
|
|
6239
6336
|
}
|
|
6240
6337
|
|
|
6241
|
-
let shapeGap: number
|
|
6338
|
+
let shapeGap: number = 0
|
|
6242
6339
|
|
|
6243
6340
|
if (gap === 0) {
|
|
6244
|
-
|
|
6341
|
+
// note: this is not used in the current tldraw.com; there we use a specified stack
|
|
6245
6342
|
|
|
6246
|
-
|
|
6343
|
+
const gaps: Record<number, number> = {}
|
|
6344
|
+
|
|
6345
|
+
shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])
|
|
6247
6346
|
|
|
6248
6347
|
// Collect all of the gaps between shapes. We want to find
|
|
6249
6348
|
// patterns (equal gaps between shapes) and use the most common
|
|
6250
6349
|
// one as the gap for all of the shapes.
|
|
6251
6350
|
for (let i = 0; i < len - 1; i++) {
|
|
6252
|
-
const
|
|
6253
|
-
const
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
const gap = nextBounds[min] - bounds[max]
|
|
6259
|
-
|
|
6260
|
-
const current = gaps.find((g) => g.gap === gap)
|
|
6261
|
-
|
|
6262
|
-
if (current) {
|
|
6263
|
-
current.count++
|
|
6264
|
-
} else {
|
|
6265
|
-
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
|
|
6266
6356
|
}
|
|
6357
|
+
gaps[gap]++
|
|
6267
6358
|
}
|
|
6268
6359
|
|
|
6269
6360
|
// Which gap is the most common?
|
|
6270
|
-
let maxCount =
|
|
6271
|
-
|
|
6272
|
-
if (
|
|
6273
|
-
maxCount =
|
|
6274
|
-
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)
|
|
6275
6366
|
}
|
|
6276
|
-
}
|
|
6367
|
+
}
|
|
6277
6368
|
|
|
6278
6369
|
// If there is no most-common gap, use the average gap.
|
|
6279
6370
|
if (maxCount === 1) {
|
|
6280
|
-
|
|
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
|
|
6281
6377
|
}
|
|
6282
6378
|
} else {
|
|
6283
6379
|
// If a gap was provided, then use that instead.
|
|
@@ -6286,36 +6382,30 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6286
6382
|
|
|
6287
6383
|
const changes: TLShapePartial[] = []
|
|
6288
6384
|
|
|
6289
|
-
let v =
|
|
6385
|
+
let v = shapeClustersToStack[0].pageBounds[max]
|
|
6290
6386
|
|
|
6291
|
-
|
|
6292
|
-
|
|
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]
|
|
6293
6391
|
|
|
6294
|
-
const
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
const parent = this.getShapeParent(shape)
|
|
6298
|
-
const localDelta = parent
|
|
6299
|
-
? Vec.Rot(delta, -this.getShapePageTransform(parent)!.decompose().rotation)
|
|
6300
|
-
: delta
|
|
6392
|
+
for (const shape of shapes) {
|
|
6393
|
+
const shapeDelta = delta.clone()
|
|
6301
6394
|
|
|
6302
|
-
|
|
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
|
+
}
|
|
6303
6402
|
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
...translateStartChanges,
|
|
6308
|
-
[val]: shape[val] + localDelta[val],
|
|
6309
|
-
}
|
|
6310
|
-
: {
|
|
6311
|
-
id: shape.id as any,
|
|
6312
|
-
type: shape.type,
|
|
6313
|
-
[val]: shape[val] + localDelta[val],
|
|
6314
|
-
}
|
|
6315
|
-
)
|
|
6403
|
+
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
6404
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6405
|
+
}
|
|
6316
6406
|
|
|
6317
|
-
v += pageBounds[
|
|
6318
|
-
}
|
|
6407
|
+
v += pageBounds[dim] + shapeGap
|
|
6408
|
+
}
|
|
6319
6409
|
|
|
6320
6410
|
this.updateShapes(changes)
|
|
6321
6411
|
return this
|
|
@@ -6335,42 +6425,79 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6335
6425
|
* @param gap - The padding to apply to the packed shapes. Defaults to 16.
|
|
6336
6426
|
*/
|
|
6337
6427
|
packShapes(shapes: TLShapeId[] | TLShape[], gap: number): this {
|
|
6428
|
+
if (this.getIsReadonly()) return this
|
|
6429
|
+
|
|
6338
6430
|
const ids =
|
|
6339
6431
|
typeof shapes[0] === 'string'
|
|
6340
6432
|
? (shapes as TLShapeId[])
|
|
6341
6433
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6342
6434
|
|
|
6343
|
-
|
|
6344
|
-
|
|
6435
|
+
// Always fresh shapes
|
|
6436
|
+
const shapesToPackFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6437
|
+
|
|
6438
|
+
const shapeClustersToPack: {
|
|
6439
|
+
shapes: TLShape[]
|
|
6440
|
+
pageBounds: Box
|
|
6441
|
+
nextPageBounds: Box
|
|
6442
|
+
}[] = []
|
|
6443
|
+
|
|
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)
|
|
6345
6450
|
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
.filter((shape): shape is TLShape => {
|
|
6349
|
-
if (!shape) return false
|
|
6451
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6452
|
+
if (!shapePageBounds) continue
|
|
6350
6453
|
|
|
6351
|
-
|
|
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,
|
|
6352
6472
|
})
|
|
6353
|
-
const shapePageBounds: Record<string, Box> = {}
|
|
6354
|
-
const nextShapePageBounds: Record<string, Box> = {}
|
|
6355
6473
|
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
area = 0
|
|
6474
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6475
|
+
if (!commonPageBounds) continue
|
|
6359
6476
|
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6477
|
+
shapeClustersToPack.push({
|
|
6478
|
+
shapes: shapesMovingTogether,
|
|
6479
|
+
pageBounds: commonPageBounds,
|
|
6480
|
+
nextPageBounds: commonPageBounds.clone(),
|
|
6481
|
+
})
|
|
6482
|
+
|
|
6483
|
+
allBounds.push(commonPageBounds)
|
|
6366
6484
|
}
|
|
6367
6485
|
|
|
6368
|
-
|
|
6486
|
+
if (shapeClustersToPack.length < 2) return this
|
|
6487
|
+
|
|
6488
|
+
let area = 0
|
|
6489
|
+
for (const { pageBounds } of shapeClustersToPack) {
|
|
6490
|
+
area += pageBounds.width * pageBounds.height
|
|
6491
|
+
}
|
|
6492
|
+
|
|
6493
|
+
const commonBounds = Box.Common(allBounds)
|
|
6369
6494
|
|
|
6370
6495
|
const maxWidth = commonBounds.width
|
|
6371
6496
|
|
|
6372
|
-
// sort the
|
|
6373
|
-
|
|
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)
|
|
6374
6501
|
|
|
6375
6502
|
// Start with is (sort of) the square of the area
|
|
6376
6503
|
const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth)
|
|
@@ -6383,85 +6510,69 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6383
6510
|
let space: Box
|
|
6384
6511
|
let last: Box
|
|
6385
6512
|
|
|
6386
|
-
for (
|
|
6387
|
-
shape = shapesToPack[i]
|
|
6388
|
-
bounds = nextShapePageBounds[shape.id]
|
|
6389
|
-
|
|
6513
|
+
for (const { nextPageBounds } of shapeClustersToPack) {
|
|
6390
6514
|
// starting at the back (smaller shapes)
|
|
6391
6515
|
for (let i = spaces.length - 1; i >= 0; i--) {
|
|
6392
6516
|
space = spaces[i]
|
|
6393
6517
|
|
|
6394
6518
|
// find a space that is big enough to contain the shape
|
|
6395
|
-
if (
|
|
6519
|
+
if (nextPageBounds.width > space.width || nextPageBounds.height > space.height) continue
|
|
6396
6520
|
|
|
6397
6521
|
// add the shape to its top-left corner
|
|
6398
|
-
|
|
6399
|
-
|
|
6522
|
+
nextPageBounds.x = space.x
|
|
6523
|
+
nextPageBounds.y = space.y
|
|
6400
6524
|
|
|
6401
|
-
height = Math.max(height,
|
|
6402
|
-
width = Math.max(width,
|
|
6525
|
+
height = Math.max(height, nextPageBounds.maxY)
|
|
6526
|
+
width = Math.max(width, nextPageBounds.maxX)
|
|
6403
6527
|
|
|
6404
|
-
if (
|
|
6528
|
+
if (nextPageBounds.width === space.width && nextPageBounds.height === space.height) {
|
|
6405
6529
|
// remove the space on a perfect fit
|
|
6406
6530
|
last = spaces.pop()!
|
|
6407
6531
|
if (i < spaces.length) spaces[i] = last
|
|
6408
|
-
} else if (
|
|
6532
|
+
} else if (nextPageBounds.height === space.height) {
|
|
6409
6533
|
// fit the shape into the space (width)
|
|
6410
|
-
space.x +=
|
|
6411
|
-
space.width -=
|
|
6412
|
-
} else if (
|
|
6534
|
+
space.x += nextPageBounds.width + gap
|
|
6535
|
+
space.width -= nextPageBounds.width + gap
|
|
6536
|
+
} else if (nextPageBounds.width === space.width) {
|
|
6413
6537
|
// fit the shape into the space (height)
|
|
6414
|
-
space.y +=
|
|
6415
|
-
space.height -=
|
|
6538
|
+
space.y += nextPageBounds.height + gap
|
|
6539
|
+
space.height -= nextPageBounds.height + gap
|
|
6416
6540
|
} else {
|
|
6417
6541
|
// split the space into two spaces
|
|
6418
6542
|
spaces.push(
|
|
6419
6543
|
new Box(
|
|
6420
|
-
space.x + (
|
|
6544
|
+
space.x + (nextPageBounds.width + gap),
|
|
6421
6545
|
space.y,
|
|
6422
|
-
space.width - (
|
|
6423
|
-
|
|
6546
|
+
space.width - (nextPageBounds.width + gap),
|
|
6547
|
+
nextPageBounds.height
|
|
6424
6548
|
)
|
|
6425
6549
|
)
|
|
6426
|
-
space.y +=
|
|
6427
|
-
space.height -=
|
|
6550
|
+
space.y += nextPageBounds.height + gap
|
|
6551
|
+
space.height -= nextPageBounds.height + gap
|
|
6428
6552
|
}
|
|
6429
6553
|
break
|
|
6430
6554
|
}
|
|
6431
6555
|
}
|
|
6432
6556
|
|
|
6433
|
-
const commonAfter = Box.Common(
|
|
6557
|
+
const commonAfter = Box.Common(shapeClustersToPack.map((s) => s.nextPageBounds))
|
|
6434
6558
|
const centerDelta = Vec.Sub(commonBounds.center, commonAfter.center)
|
|
6435
6559
|
|
|
6436
|
-
let nextBounds: Box
|
|
6437
|
-
|
|
6438
6560
|
const changes: TLShapePartial<any>[] = []
|
|
6439
6561
|
|
|
6440
|
-
for (
|
|
6441
|
-
|
|
6442
|
-
bounds = shapePageBounds[shape.id]
|
|
6443
|
-
nextBounds = nextShapePageBounds[shape.id]
|
|
6562
|
+
for (const { shapes, pageBounds, nextPageBounds } of shapeClustersToPack) {
|
|
6563
|
+
const delta = Vec.Sub(nextPageBounds.point, pageBounds.point).add(centerDelta)
|
|
6444
6564
|
|
|
6445
|
-
const
|
|
6446
|
-
|
|
6447
|
-
if (parentTransform) delta.rot(-parentTransform.rotation())
|
|
6448
|
-
|
|
6449
|
-
const change: TLShapePartial = {
|
|
6450
|
-
id: shape.id,
|
|
6451
|
-
type: shape.type,
|
|
6452
|
-
x: shape.x + delta.x,
|
|
6453
|
-
y: shape.y + delta.y,
|
|
6454
|
-
}
|
|
6565
|
+
for (const shape of shapes) {
|
|
6566
|
+
const shapeDelta = delta.clone()
|
|
6455
6567
|
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6568
|
+
const parent = this.getShapeParent(shape)
|
|
6569
|
+
if (parent) {
|
|
6570
|
+
const parentTransform = this.getShapeParentTransform(shape)
|
|
6571
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6572
|
+
}
|
|
6460
6573
|
|
|
6461
|
-
|
|
6462
|
-
changes.push(
|
|
6463
|
-
} else {
|
|
6464
|
-
changes.push(change)
|
|
6574
|
+
shapeDelta.add(shape)
|
|
6575
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6465
6576
|
}
|
|
6466
6577
|
}
|
|
6467
6578
|
|
|
@@ -6486,32 +6597,78 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6486
6597
|
*
|
|
6487
6598
|
* @public
|
|
6488
6599
|
*/
|
|
6489
|
-
|
|
6490
6600
|
alignShapes(
|
|
6491
6601
|
shapes: TLShapeId[] | TLShape[],
|
|
6492
6602
|
operation: 'left' | 'center-horizontal' | 'right' | 'top' | 'center-vertical' | 'bottom'
|
|
6493
6603
|
): this {
|
|
6604
|
+
if (this.getIsReadonly()) return this
|
|
6605
|
+
|
|
6494
6606
|
const ids =
|
|
6495
6607
|
typeof shapes[0] === 'string'
|
|
6496
6608
|
? (shapes as TLShapeId[])
|
|
6497
6609
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6498
6610
|
|
|
6499
|
-
|
|
6500
|
-
|
|
6611
|
+
// Always get fresh shapes
|
|
6612
|
+
const shapesToAlignFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6501
6613
|
|
|
6502
|
-
const
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
const
|
|
6614
|
+
const shapeClustersToAlign: {
|
|
6615
|
+
shapes: TLShape[]
|
|
6616
|
+
pageBounds: Box
|
|
6617
|
+
}[] = []
|
|
6618
|
+
const allBounds: Box[] = []
|
|
6619
|
+
const visited = new Set<TLShapeId>()
|
|
6507
6620
|
|
|
6508
|
-
const
|
|
6621
|
+
for (const shape of shapesToAlignFirstPass) {
|
|
6622
|
+
if (visited.has(shape.id)) continue
|
|
6623
|
+
visited.add(shape.id)
|
|
6509
6624
|
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
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
|
+
})
|
|
6652
|
+
|
|
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[] = []
|
|
6513
6669
|
|
|
6514
|
-
|
|
6670
|
+
shapeClustersToAlign.forEach(({ shapes, pageBounds }) => {
|
|
6671
|
+
const delta = new Vec()
|
|
6515
6672
|
|
|
6516
6673
|
switch (operation) {
|
|
6517
6674
|
case 'top': {
|
|
@@ -6540,12 +6697,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6540
6697
|
}
|
|
6541
6698
|
}
|
|
6542
6699
|
|
|
6543
|
-
const
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6700
|
+
for (const shape of shapes) {
|
|
6701
|
+
const shapeDelta = delta.clone()
|
|
6702
|
+
|
|
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
|
+
}
|
|
6547
6710
|
|
|
6548
|
-
|
|
6711
|
+
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
6712
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6713
|
+
}
|
|
6549
6714
|
})
|
|
6550
6715
|
|
|
6551
6716
|
this.updateShapes(changes)
|
|
@@ -6567,65 +6732,137 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6567
6732
|
* @public
|
|
6568
6733
|
*/
|
|
6569
6734
|
distributeShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
|
|
6735
|
+
if (this.getIsReadonly()) return this
|
|
6736
|
+
|
|
6570
6737
|
const ids =
|
|
6571
6738
|
typeof shapes[0] === 'string'
|
|
6572
6739
|
? (shapes as TLShapeId[])
|
|
6573
6740
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6574
6741
|
|
|
6575
|
-
|
|
6576
|
-
|
|
6742
|
+
// always fresh shapes
|
|
6743
|
+
const shapesToDistributeFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6577
6744
|
|
|
6578
|
-
const
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
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
|
|
6583
6792
|
|
|
6584
6793
|
let val: 'x' | 'y'
|
|
6585
6794
|
let min: 'minX' | 'minY'
|
|
6586
6795
|
let max: 'maxX' | 'maxY'
|
|
6587
|
-
let mid: 'midX' | 'midY'
|
|
6588
6796
|
let dim: 'width' | 'height'
|
|
6589
6797
|
|
|
6590
6798
|
if (operation === 'horizontal') {
|
|
6591
6799
|
val = 'x'
|
|
6592
6800
|
min = 'minX'
|
|
6593
6801
|
max = 'maxX'
|
|
6594
|
-
mid = 'midX'
|
|
6595
6802
|
dim = 'width'
|
|
6596
6803
|
} else {
|
|
6597
6804
|
val = 'y'
|
|
6598
6805
|
min = 'minY'
|
|
6599
6806
|
max = 'maxY'
|
|
6600
|
-
mid = 'midY'
|
|
6601
6807
|
dim = 'height'
|
|
6602
6808
|
}
|
|
6603
6809
|
const changes: TLShapePartial[] = []
|
|
6604
6810
|
|
|
6605
|
-
|
|
6606
|
-
const
|
|
6607
|
-
(a, b) => pageBounds[a.id][min] - pageBounds[b.id][min]
|
|
6608
|
-
)[0]
|
|
6609
|
-
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]
|
|
6610
6813
|
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
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
|
+
}
|
|
6614
6822
|
|
|
6615
|
-
|
|
6823
|
+
const shapeClustersToMove = shapeClustersToDistribute
|
|
6616
6824
|
.filter((shape) => shape !== first && shape !== last)
|
|
6617
|
-
.sort((a, b) =>
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
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]
|
|
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()
|
|
6621
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
|
|
6622
6854
|
const parent = this.getShapeParent(shape)
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6855
|
+
if (parent) {
|
|
6856
|
+
const parentTransform = this.getShapePageTransform(parent)
|
|
6857
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6858
|
+
}
|
|
6626
6859
|
|
|
6627
|
-
|
|
6628
|
-
|
|
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
|
+
}
|
|
6629
6866
|
|
|
6630
6867
|
this.updateShapes(changes)
|
|
6631
6868
|
return this
|
|
@@ -6652,65 +6889,106 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6652
6889
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6653
6890
|
|
|
6654
6891
|
if (this.getIsReadonly()) return this
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
const
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
}
|
|
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,
|
|
6684
6920
|
})
|
|
6685
|
-
|
|
6921
|
+
) {
|
|
6922
|
+
continue
|
|
6686
6923
|
}
|
|
6687
|
-
case 'horizontal': {
|
|
6688
|
-
this.run(() => {
|
|
6689
|
-
for (const shape of shapesToStretch) {
|
|
6690
|
-
const bounds = shapeBounds[shape.id]
|
|
6691
|
-
const pageBounds = shapePageBounds[shape.id]
|
|
6692
|
-
const pageRotation = this.getShapePageTransform(shape)!.rotation()
|
|
6693
|
-
if (pageRotation % PI2) continue
|
|
6694
|
-
const localOffset = new Vec(commonBounds.minX - pageBounds.minX, 0)
|
|
6695
|
-
const parentTransform = this.getShapeParentTransform(shape)
|
|
6696
|
-
if (parentTransform) localOffset.rot(-parentTransform.rotation())
|
|
6697
|
-
|
|
6698
|
-
const { x, y } = Vec.Add(localOffset, shape)
|
|
6699
|
-
this.updateShapes([{ id: shape.id, type: shape.type, x, y }])
|
|
6700
|
-
const scale = new Vec(commonBounds.width / pageBounds.width, 1)
|
|
6701
|
-
this.resizeShape(shape.id, scale, {
|
|
6702
|
-
initialBounds: bounds,
|
|
6703
|
-
scaleOrigin: new Vec(commonBounds.minX, pageBounds.center.y),
|
|
6704
|
-
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
|
|
6705
|
-
scaleAxisRotation: 0,
|
|
6706
|
-
})
|
|
6707
|
-
}
|
|
6708
|
-
})
|
|
6709
6924
|
|
|
6710
|
-
|
|
6711
|
-
|
|
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)
|
|
6712
6942
|
}
|
|
6713
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'
|
|
6959
|
+
}
|
|
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
|
+
|
|
6714
6992
|
return this
|
|
6715
6993
|
}
|
|
6716
6994
|
|