@tldraw/editor 3.9.0-internal.7f0e15f4f7d9 → 3.10.0-canary.15f6aaa3d2d3
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 +43 -7
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +2 -3
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/LiveCollaborators.js +5 -0
- package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCursor.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/components/default-components/DefaultScribble.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +435 -252
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/managers/FontManager.js +25 -26
- package/dist-cjs/lib/editor/managers/FontManager.js.map +2 -2
- 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 +43 -7
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +2 -3
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/LiveCollaborators.mjs +5 -0
- package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCursor.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/components/default-components/DefaultScribble.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +431 -248
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/managers/FontManager.mjs +26 -27
- package/dist-esm/lib/editor/managers/FontManager.mjs.map +2 -2
- 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 +2 -0
- package/src/lib/TldrawEditor.tsx +3 -3
- package/src/lib/components/LiveCollaborators.tsx +5 -0
- package/src/lib/components/default-components/DefaultBrush.tsx +1 -0
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -0
- package/src/lib/components/default-components/DefaultCursor.tsx +1 -0
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +5 -3
- package/src/lib/components/default-components/DefaultScribble.tsx +1 -0
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -0
- package/src/lib/editor/Editor.ts +560 -276
- package/src/lib/editor/managers/FontManager.ts +26 -27
- package/src/lib/editor/shapes/ShapeUtil.ts +32 -5
- package/src/lib/exports/getSvgJsx.tsx +1 -0
- package/src/version.ts +3 -3
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
structuredClone,
|
|
93
93
|
uniqueId,
|
|
94
94
|
} from '@tldraw/utils'
|
|
95
|
+
import { Number } from 'core-js'
|
|
95
96
|
import EventEmitter from 'eventemitter3'
|
|
96
97
|
import {
|
|
97
98
|
TLEditorSnapshot,
|
|
@@ -125,7 +126,7 @@ import { EASINGS } from '../primitives/easings'
|
|
|
125
126
|
import { Geometry2d } from '../primitives/geometry/Geometry2d'
|
|
126
127
|
import { Group2d } from '../primitives/geometry/Group2d'
|
|
127
128
|
import { intersectPolygonPolygon } from '../primitives/intersect'
|
|
128
|
-
import {
|
|
129
|
+
import { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from '../primitives/utils'
|
|
129
130
|
import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'
|
|
130
131
|
import { dataUrlToFile } from '../utils/assets'
|
|
131
132
|
import { debugFlags } from '../utils/debug-flags'
|
|
@@ -155,7 +156,7 @@ import { SnapManager } from './managers/SnapManager/SnapManager'
|
|
|
155
156
|
import { TextManager } from './managers/TextManager'
|
|
156
157
|
import { TickManager } from './managers/TickManager'
|
|
157
158
|
import { UserPreferencesManager } from './managers/UserPreferencesManager'
|
|
158
|
-
import { ShapeUtil, TLResizeMode } from './shapes/ShapeUtil'
|
|
159
|
+
import { ShapeUtil, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
|
|
159
160
|
import { RootState } from './tools/RootState'
|
|
160
161
|
import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
|
|
161
162
|
import { TLContent } from './types/clipboard-types'
|
|
@@ -267,6 +268,7 @@ export interface TLRenderingShape {
|
|
|
267
268
|
|
|
268
269
|
/** @public */
|
|
269
270
|
export class Editor extends EventEmitter<TLEventMap> {
|
|
271
|
+
readonly id = uniqueId()
|
|
270
272
|
constructor({
|
|
271
273
|
store,
|
|
272
274
|
user,
|
|
@@ -4283,17 +4285,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4283
4285
|
|
|
4284
4286
|
/* --------------------- Shapes --------------------- */
|
|
4285
4287
|
|
|
4286
|
-
|
|
4287
|
-
private _getShapeGeometryCache(): ComputedCache<Geometry2d, TLShape> {
|
|
4288
|
-
return this.store.createComputedCache(
|
|
4289
|
-
'bounds',
|
|
4290
|
-
(shape) => {
|
|
4291
|
-
this.fonts.trackFontsForShape(shape)
|
|
4292
|
-
return this.getShapeUtil(shape).getGeometry(shape)
|
|
4293
|
-
},
|
|
4294
|
-
{ areRecordsEqual: (a, b) => a.props === b.props }
|
|
4295
|
-
)
|
|
4296
|
-
}
|
|
4288
|
+
private _shapeGeometryCaches: Record<string, ComputedCache<Geometry2d, TLShape>> = {}
|
|
4297
4289
|
|
|
4298
4290
|
/**
|
|
4299
4291
|
* Get the geometry of a shape.
|
|
@@ -4302,14 +4294,29 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4302
4294
|
* ```ts
|
|
4303
4295
|
* editor.getShapeGeometry(myShape)
|
|
4304
4296
|
* editor.getShapeGeometry(myShapeId)
|
|
4297
|
+
* editor.getShapeGeometry(myShapeId, { context: "arrow" })
|
|
4305
4298
|
* ```
|
|
4306
4299
|
*
|
|
4307
4300
|
* @param shape - The shape (or shape id) to get the geometry for.
|
|
4301
|
+
* @param opts - Additional options about the request for geometry. Passed to {@link ShapeUtil.getGeometry}.
|
|
4308
4302
|
*
|
|
4309
4303
|
* @public
|
|
4310
4304
|
*/
|
|
4311
|
-
getShapeGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId): T {
|
|
4312
|
-
|
|
4305
|
+
getShapeGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId, opts?: TLGeometryOpts): T {
|
|
4306
|
+
const context = opts?.context ?? 'none'
|
|
4307
|
+
if (!this._shapeGeometryCaches[context]) {
|
|
4308
|
+
this._shapeGeometryCaches[context] = this.store.createComputedCache(
|
|
4309
|
+
'bounds',
|
|
4310
|
+
(shape) => {
|
|
4311
|
+
this.fonts.trackFontsForShape(shape)
|
|
4312
|
+
return this.getShapeUtil(shape).getGeometry(shape, opts)
|
|
4313
|
+
},
|
|
4314
|
+
{ areRecordsEqual: (a, b) => a.props === b.props }
|
|
4315
|
+
)
|
|
4316
|
+
}
|
|
4317
|
+
return this._shapeGeometryCaches[context].get(
|
|
4318
|
+
typeof shape === 'string' ? shape : shape.id
|
|
4319
|
+
)! as T
|
|
4313
4320
|
}
|
|
4314
4321
|
|
|
4315
4322
|
/** @internal */
|
|
@@ -5769,14 +5776,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5769
5776
|
return this
|
|
5770
5777
|
}
|
|
5771
5778
|
|
|
5779
|
+
// Gets a shape partial that includes life cycle changes: on translate start, on translate, on translate end
|
|
5772
5780
|
private getChangesToTranslateShape(initialShape: TLShape, newShapeCoords: VecLike): TLShape {
|
|
5773
5781
|
let workingShape = initialShape
|
|
5774
5782
|
const util = this.getShapeUtil(initialShape)
|
|
5775
5783
|
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5784
|
+
const afterTranslateStart = util.onTranslateStart?.(workingShape)
|
|
5785
|
+
if (afterTranslateStart) {
|
|
5786
|
+
workingShape = applyPartialToRecordWithProps(workingShape, afterTranslateStart)
|
|
5787
|
+
}
|
|
5780
5788
|
|
|
5781
5789
|
workingShape = applyPartialToRecordWithProps(workingShape, {
|
|
5782
5790
|
id: initialShape.id,
|
|
@@ -5785,15 +5793,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5785
5793
|
y: newShapeCoords.y,
|
|
5786
5794
|
})
|
|
5787
5795
|
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5796
|
+
const afterTranslate = util.onTranslate?.(initialShape, workingShape)
|
|
5797
|
+
if (afterTranslate) {
|
|
5798
|
+
workingShape = applyPartialToRecordWithProps(workingShape, afterTranslate)
|
|
5799
|
+
}
|
|
5792
5800
|
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5801
|
+
const afterTranslateEnd = util.onTranslateEnd?.(initialShape, workingShape)
|
|
5802
|
+
if (afterTranslateEnd) {
|
|
5803
|
+
workingShape = applyPartialToRecordWithProps(workingShape, afterTranslateEnd)
|
|
5804
|
+
}
|
|
5797
5805
|
|
|
5798
5806
|
return workingShape
|
|
5799
5807
|
}
|
|
@@ -6192,6 +6200,37 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6192
6200
|
return this
|
|
6193
6201
|
}
|
|
6194
6202
|
|
|
6203
|
+
/**
|
|
6204
|
+
* @internal
|
|
6205
|
+
*/
|
|
6206
|
+
private collectShapesViaArrowBindings(info: {
|
|
6207
|
+
initialShapes: TLShape[]
|
|
6208
|
+
resultShapes: TLShape[]
|
|
6209
|
+
resultBounds: Box[]
|
|
6210
|
+
bindings: TLBinding[]
|
|
6211
|
+
visited: Set<TLShapeId>
|
|
6212
|
+
}) {
|
|
6213
|
+
const { initialShapes, resultShapes, resultBounds, bindings, visited } = info
|
|
6214
|
+
for (const binding of bindings) {
|
|
6215
|
+
for (const id of [binding.fromId, binding.toId]) {
|
|
6216
|
+
if (!visited.has(id)) {
|
|
6217
|
+
const aligningShape = initialShapes.find((s) => s.id === id)
|
|
6218
|
+
if (aligningShape && !visited.has(aligningShape.id)) {
|
|
6219
|
+
visited.add(aligningShape.id)
|
|
6220
|
+
const shapePageBounds = this.getShapePageBounds(aligningShape)
|
|
6221
|
+
if (!shapePageBounds) continue
|
|
6222
|
+
resultShapes.push(aligningShape)
|
|
6223
|
+
resultBounds.push(shapePageBounds)
|
|
6224
|
+
this.collectShapesViaArrowBindings({
|
|
6225
|
+
...info,
|
|
6226
|
+
bindings: this.getBindingsInvolvingShape(aligningShape, 'arrow'),
|
|
6227
|
+
})
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6195
6234
|
/**
|
|
6196
6235
|
* Flip shape positions.
|
|
6197
6236
|
*
|
|
@@ -6207,47 +6246,74 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6207
6246
|
* @public
|
|
6208
6247
|
*/
|
|
6209
6248
|
flipShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
|
|
6249
|
+
if (this.getIsReadonly()) return this
|
|
6250
|
+
|
|
6210
6251
|
const ids =
|
|
6211
6252
|
typeof shapes[0] === 'string'
|
|
6212
6253
|
? (shapes as TLShapeId[])
|
|
6213
6254
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6214
6255
|
|
|
6215
|
-
|
|
6256
|
+
// Collect a greedy list of shapes to flip
|
|
6257
|
+
const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6216
6258
|
|
|
6217
|
-
|
|
6259
|
+
for (const shape of shapesToFlipFirstPass) {
|
|
6260
|
+
if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
|
6261
|
+
const childrenOfGroups = compact(
|
|
6262
|
+
this.getSortedChildIdsForParent(shape.id).map((id) => this.getShape(id))
|
|
6263
|
+
)
|
|
6264
|
+
shapesToFlipFirstPass.push(...childrenOfGroups)
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6218
6267
|
|
|
6219
|
-
|
|
6268
|
+
// exclude shapes that can't be flipped
|
|
6269
|
+
const shapesToFlip: {
|
|
6270
|
+
shape: TLShape
|
|
6271
|
+
localBounds: Box
|
|
6272
|
+
pageTransform: Mat
|
|
6273
|
+
isAspectRatioLocked: boolean
|
|
6274
|
+
}[] = []
|
|
6220
6275
|
|
|
6221
|
-
|
|
6222
|
-
shapesToFlip
|
|
6223
|
-
.map((shape) => {
|
|
6224
|
-
if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
|
6225
|
-
return this.getSortedChildIdsForParent(shape.id).map((id) => this.getShape(id))
|
|
6226
|
-
}
|
|
6276
|
+
const allBounds: Box[] = []
|
|
6227
6277
|
|
|
6228
|
-
|
|
6278
|
+
for (const shape of shapesToFlipFirstPass) {
|
|
6279
|
+
const util = this.getShapeUtil(shape)
|
|
6280
|
+
if (
|
|
6281
|
+
!util.canBeLaidOut(shape, {
|
|
6282
|
+
type: 'flip',
|
|
6283
|
+
shapes: shapesToFlipFirstPass,
|
|
6229
6284
|
})
|
|
6230
|
-
|
|
6231
|
-
|
|
6285
|
+
) {
|
|
6286
|
+
continue
|
|
6287
|
+
}
|
|
6288
|
+
|
|
6289
|
+
const pageBounds = this.getShapePageBounds(shape)
|
|
6290
|
+
const localBounds = this.getShapeGeometry(shape).bounds
|
|
6291
|
+
const pageTransform = this.getShapePageTransform(shape.id)
|
|
6292
|
+
if (!(pageBounds && localBounds && pageTransform)) continue
|
|
6293
|
+
shapesToFlip.push({
|
|
6294
|
+
shape,
|
|
6295
|
+
localBounds,
|
|
6296
|
+
pageTransform,
|
|
6297
|
+
isAspectRatioLocked: util.isAspectRatioLocked(shape),
|
|
6298
|
+
})
|
|
6299
|
+
allBounds.push(pageBounds)
|
|
6300
|
+
}
|
|
6301
|
+
|
|
6302
|
+
if (!shapesToFlip.length) return this
|
|
6232
6303
|
|
|
6233
|
-
const scaleOriginPage = Box.Common(
|
|
6234
|
-
compact(shapesToFlip.map((id) => this.getShapePageBounds(id)))
|
|
6235
|
-
).center
|
|
6304
|
+
const scaleOriginPage = Box.Common(allBounds).center
|
|
6236
6305
|
|
|
6237
6306
|
this.run(() => {
|
|
6238
|
-
for (const shape of shapesToFlip) {
|
|
6239
|
-
const bounds = this.getShapeGeometry(shape).bounds
|
|
6240
|
-
const initialPageTransform = this.getShapePageTransform(shape.id)
|
|
6241
|
-
if (!initialPageTransform) continue
|
|
6307
|
+
for (const { shape, localBounds, pageTransform, isAspectRatioLocked } of shapesToFlip) {
|
|
6242
6308
|
this.resizeShape(
|
|
6243
6309
|
shape.id,
|
|
6244
6310
|
{ x: operation === 'horizontal' ? -1 : 1, y: operation === 'vertical' ? -1 : 1 },
|
|
6245
6311
|
{
|
|
6246
|
-
initialBounds:
|
|
6247
|
-
initialPageTransform,
|
|
6312
|
+
initialBounds: localBounds,
|
|
6313
|
+
initialPageTransform: pageTransform,
|
|
6248
6314
|
initialShape: shape,
|
|
6315
|
+
isAspectRatioLocked,
|
|
6249
6316
|
mode: 'scale_shape',
|
|
6250
|
-
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
|
|
6251
6317
|
scaleOrigin: scaleOriginPage,
|
|
6252
6318
|
scaleAxisRotation: 0,
|
|
6253
6319
|
}
|
|
@@ -6284,21 +6350,58 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6284
6350
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6285
6351
|
if (this.getIsReadonly()) return this
|
|
6286
6352
|
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6353
|
+
// 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.
|
|
6354
|
+
|
|
6355
|
+
// always fresh shapes
|
|
6356
|
+
const shapesToStackFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6357
|
+
|
|
6358
|
+
const shapeClustersToStack: {
|
|
6359
|
+
shapes: TLShape[]
|
|
6360
|
+
pageBounds: Box
|
|
6361
|
+
}[] = []
|
|
6362
|
+
const allBounds: Box[] = []
|
|
6363
|
+
const visited = new Set<TLShapeId>()
|
|
6364
|
+
|
|
6365
|
+
for (const shape of shapesToStackFirstPass) {
|
|
6366
|
+
if (visited.has(shape.id)) continue
|
|
6367
|
+
visited.add(shape.id)
|
|
6368
|
+
|
|
6369
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6370
|
+
if (!shapePageBounds) continue
|
|
6371
|
+
|
|
6372
|
+
if (
|
|
6373
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
6374
|
+
type: 'stack',
|
|
6375
|
+
shapes: shapesToStackFirstPass,
|
|
6376
|
+
})
|
|
6377
|
+
) {
|
|
6378
|
+
continue
|
|
6379
|
+
}
|
|
6380
|
+
|
|
6381
|
+
const shapesMovingTogether = [shape]
|
|
6382
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
6291
6383
|
|
|
6292
|
-
|
|
6384
|
+
this.collectShapesViaArrowBindings({
|
|
6385
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6386
|
+
initialShapes: shapesToStackFirstPass,
|
|
6387
|
+
resultShapes: shapesMovingTogether,
|
|
6388
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6389
|
+
visited,
|
|
6293
6390
|
})
|
|
6294
6391
|
|
|
6295
|
-
|
|
6392
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6393
|
+
if (!commonPageBounds) continue
|
|
6296
6394
|
|
|
6297
|
-
|
|
6395
|
+
shapeClustersToStack.push({
|
|
6396
|
+
shapes: shapesMovingTogether,
|
|
6397
|
+
pageBounds: commonPageBounds,
|
|
6398
|
+
})
|
|
6298
6399
|
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6400
|
+
allBounds.push(commonPageBounds)
|
|
6401
|
+
}
|
|
6402
|
+
|
|
6403
|
+
const len = shapeClustersToStack.length
|
|
6404
|
+
if ((gap === 0 && len < 3) || len < 2) return this
|
|
6302
6405
|
|
|
6303
6406
|
let val: 'x' | 'y'
|
|
6304
6407
|
let min: 'minX' | 'minY'
|
|
@@ -6317,46 +6420,45 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6317
6420
|
dim = 'height'
|
|
6318
6421
|
}
|
|
6319
6422
|
|
|
6320
|
-
let shapeGap: number
|
|
6423
|
+
let shapeGap: number = 0
|
|
6321
6424
|
|
|
6322
6425
|
if (gap === 0) {
|
|
6323
|
-
|
|
6426
|
+
// note: this is not used in the current tldraw.com; there we use a specified stack
|
|
6427
|
+
|
|
6428
|
+
const gaps: Record<number, number> = {}
|
|
6324
6429
|
|
|
6325
|
-
|
|
6430
|
+
shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])
|
|
6326
6431
|
|
|
6327
6432
|
// Collect all of the gaps between shapes. We want to find
|
|
6328
6433
|
// patterns (equal gaps between shapes) and use the most common
|
|
6329
6434
|
// one as the gap for all of the shapes.
|
|
6330
6435
|
for (let i = 0; i < len - 1; i++) {
|
|
6331
|
-
const
|
|
6332
|
-
const
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
const gap = nextBounds[min] - bounds[max]
|
|
6338
|
-
|
|
6339
|
-
const current = gaps.find((g) => g.gap === gap)
|
|
6340
|
-
|
|
6341
|
-
if (current) {
|
|
6342
|
-
current.count++
|
|
6343
|
-
} else {
|
|
6344
|
-
gaps.push({ gap, count: 1 })
|
|
6436
|
+
const currCluster = shapeClustersToStack[i]
|
|
6437
|
+
const nextCluster = shapeClustersToStack[i + 1]
|
|
6438
|
+
const gap = nextCluster.pageBounds[min] - currCluster.pageBounds[max]
|
|
6439
|
+
if (!gaps[gap]) {
|
|
6440
|
+
gaps[gap] = 0
|
|
6345
6441
|
}
|
|
6442
|
+
gaps[gap]++
|
|
6346
6443
|
}
|
|
6347
6444
|
|
|
6348
6445
|
// Which gap is the most common?
|
|
6349
|
-
let maxCount =
|
|
6350
|
-
|
|
6351
|
-
if (
|
|
6352
|
-
maxCount =
|
|
6353
|
-
shapeGap =
|
|
6446
|
+
let maxCount = 1
|
|
6447
|
+
for (const [gap, count] of Object.entries(gaps)) {
|
|
6448
|
+
if (count > maxCount) {
|
|
6449
|
+
maxCount = count
|
|
6450
|
+
shapeGap = parseFloat(gap)
|
|
6354
6451
|
}
|
|
6355
|
-
}
|
|
6452
|
+
}
|
|
6356
6453
|
|
|
6357
6454
|
// If there is no most-common gap, use the average gap.
|
|
6358
6455
|
if (maxCount === 1) {
|
|
6359
|
-
|
|
6456
|
+
let totalCount = 0
|
|
6457
|
+
for (const [gap, count] of Object.entries(gaps)) {
|
|
6458
|
+
shapeGap += parseFloat(gap) * count
|
|
6459
|
+
totalCount += count
|
|
6460
|
+
}
|
|
6461
|
+
shapeGap /= totalCount
|
|
6360
6462
|
}
|
|
6361
6463
|
} else {
|
|
6362
6464
|
// If a gap was provided, then use that instead.
|
|
@@ -6365,36 +6467,30 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6365
6467
|
|
|
6366
6468
|
const changes: TLShapePartial[] = []
|
|
6367
6469
|
|
|
6368
|
-
let v =
|
|
6369
|
-
|
|
6370
|
-
shapesToStack.forEach((shape, i) => {
|
|
6371
|
-
if (i === 0) return
|
|
6470
|
+
let v = shapeClustersToStack[0].pageBounds[max]
|
|
6372
6471
|
|
|
6373
|
-
|
|
6374
|
-
|
|
6472
|
+
for (let i = 1; i < shapeClustersToStack.length; i++) {
|
|
6473
|
+
const { shapes, pageBounds } = shapeClustersToStack[i]
|
|
6474
|
+
const delta = new Vec()
|
|
6475
|
+
delta[val] = v + shapeGap - pageBounds[val]
|
|
6375
6476
|
|
|
6376
|
-
const
|
|
6377
|
-
|
|
6378
|
-
? Vec.Rot(delta, -this.getShapePageTransform(parent)!.decompose().rotation)
|
|
6379
|
-
: delta
|
|
6477
|
+
for (const shape of shapes) {
|
|
6478
|
+
const shapeDelta = delta.clone()
|
|
6380
6479
|
|
|
6381
|
-
|
|
6480
|
+
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
6481
|
+
// todo: ensure that the parent isn't being aligned together with its children
|
|
6482
|
+
const parent = this.getShapeParent(shape)
|
|
6483
|
+
if (parent) {
|
|
6484
|
+
const parentTransform = this.getShapePageTransform(parent)
|
|
6485
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6486
|
+
}
|
|
6382
6487
|
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
...translateStartChanges,
|
|
6387
|
-
[val]: shape[val] + localDelta[val],
|
|
6388
|
-
}
|
|
6389
|
-
: {
|
|
6390
|
-
id: shape.id as any,
|
|
6391
|
-
type: shape.type,
|
|
6392
|
-
[val]: shape[val] + localDelta[val],
|
|
6393
|
-
}
|
|
6394
|
-
)
|
|
6488
|
+
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
6489
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6490
|
+
}
|
|
6395
6491
|
|
|
6396
|
-
v += pageBounds[
|
|
6397
|
-
}
|
|
6492
|
+
v += pageBounds[dim] + shapeGap
|
|
6493
|
+
}
|
|
6398
6494
|
|
|
6399
6495
|
this.updateShapes(changes)
|
|
6400
6496
|
return this
|
|
@@ -6414,42 +6510,79 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6414
6510
|
* @param gap - The padding to apply to the packed shapes. Defaults to 16.
|
|
6415
6511
|
*/
|
|
6416
6512
|
packShapes(shapes: TLShapeId[] | TLShape[], gap: number): this {
|
|
6513
|
+
if (this.getIsReadonly()) return this
|
|
6514
|
+
|
|
6417
6515
|
const ids =
|
|
6418
6516
|
typeof shapes[0] === 'string'
|
|
6419
6517
|
? (shapes as TLShapeId[])
|
|
6420
6518
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6421
6519
|
|
|
6422
|
-
|
|
6423
|
-
|
|
6520
|
+
// Always fresh shapes
|
|
6521
|
+
const shapesToPackFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6522
|
+
|
|
6523
|
+
const shapeClustersToPack: {
|
|
6524
|
+
shapes: TLShape[]
|
|
6525
|
+
pageBounds: Box
|
|
6526
|
+
nextPageBounds: Box
|
|
6527
|
+
}[] = []
|
|
6528
|
+
|
|
6529
|
+
const allBounds: Box[] = []
|
|
6530
|
+
const visited = new Set<TLShapeId>()
|
|
6531
|
+
|
|
6532
|
+
for (const shape of shapesToPackFirstPass) {
|
|
6533
|
+
if (visited.has(shape.id)) continue
|
|
6534
|
+
visited.add(shape.id)
|
|
6535
|
+
|
|
6536
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6537
|
+
if (!shapePageBounds) continue
|
|
6538
|
+
|
|
6539
|
+
if (
|
|
6540
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
6541
|
+
type: 'pack',
|
|
6542
|
+
shapes: shapesToPackFirstPass,
|
|
6543
|
+
})
|
|
6544
|
+
) {
|
|
6545
|
+
continue
|
|
6546
|
+
}
|
|
6547
|
+
|
|
6548
|
+
const shapesMovingTogether = [shape]
|
|
6549
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
6550
|
+
|
|
6551
|
+
this.collectShapesViaArrowBindings({
|
|
6552
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6553
|
+
initialShapes: shapesToPackFirstPass,
|
|
6554
|
+
resultShapes: shapesMovingTogether,
|
|
6555
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6556
|
+
visited,
|
|
6557
|
+
})
|
|
6424
6558
|
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
.filter((shape): shape is TLShape => {
|
|
6428
|
-
if (!shape) return false
|
|
6559
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6560
|
+
if (!commonPageBounds) continue
|
|
6429
6561
|
|
|
6430
|
-
|
|
6562
|
+
shapeClustersToPack.push({
|
|
6563
|
+
shapes: shapesMovingTogether,
|
|
6564
|
+
pageBounds: commonPageBounds,
|
|
6565
|
+
nextPageBounds: commonPageBounds.clone(),
|
|
6431
6566
|
})
|
|
6432
|
-
const shapePageBounds: Record<string, Box> = {}
|
|
6433
|
-
const nextShapePageBounds: Record<string, Box> = {}
|
|
6434
6567
|
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6568
|
+
allBounds.push(commonPageBounds)
|
|
6569
|
+
}
|
|
6570
|
+
|
|
6571
|
+
if (shapeClustersToPack.length < 2) return this
|
|
6438
6572
|
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
shapePageBounds[shape.id] = bounds
|
|
6443
|
-
nextShapePageBounds[shape.id] = bounds.clone()
|
|
6444
|
-
area += bounds.width * bounds.height
|
|
6573
|
+
let area = 0
|
|
6574
|
+
for (const { pageBounds } of shapeClustersToPack) {
|
|
6575
|
+
area += pageBounds.width * pageBounds.height
|
|
6445
6576
|
}
|
|
6446
6577
|
|
|
6447
|
-
const commonBounds = Box.Common(
|
|
6578
|
+
const commonBounds = Box.Common(allBounds)
|
|
6448
6579
|
|
|
6449
6580
|
const maxWidth = commonBounds.width
|
|
6450
6581
|
|
|
6451
|
-
// sort the
|
|
6452
|
-
|
|
6582
|
+
// sort the shape clusters by width and then height, descending
|
|
6583
|
+
shapeClustersToPack
|
|
6584
|
+
.sort((a, b) => a.pageBounds.width - b.pageBounds.width)
|
|
6585
|
+
.sort((a, b) => a.pageBounds.height - b.pageBounds.height)
|
|
6453
6586
|
|
|
6454
6587
|
// Start with is (sort of) the square of the area
|
|
6455
6588
|
const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth)
|
|
@@ -6462,85 +6595,69 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6462
6595
|
let space: Box
|
|
6463
6596
|
let last: Box
|
|
6464
6597
|
|
|
6465
|
-
for (
|
|
6466
|
-
shape = shapesToPack[i]
|
|
6467
|
-
bounds = nextShapePageBounds[shape.id]
|
|
6468
|
-
|
|
6598
|
+
for (const { nextPageBounds } of shapeClustersToPack) {
|
|
6469
6599
|
// starting at the back (smaller shapes)
|
|
6470
6600
|
for (let i = spaces.length - 1; i >= 0; i--) {
|
|
6471
6601
|
space = spaces[i]
|
|
6472
6602
|
|
|
6473
6603
|
// find a space that is big enough to contain the shape
|
|
6474
|
-
if (
|
|
6604
|
+
if (nextPageBounds.width > space.width || nextPageBounds.height > space.height) continue
|
|
6475
6605
|
|
|
6476
6606
|
// add the shape to its top-left corner
|
|
6477
|
-
|
|
6478
|
-
|
|
6607
|
+
nextPageBounds.x = space.x
|
|
6608
|
+
nextPageBounds.y = space.y
|
|
6479
6609
|
|
|
6480
|
-
height = Math.max(height,
|
|
6481
|
-
width = Math.max(width,
|
|
6610
|
+
height = Math.max(height, nextPageBounds.maxY)
|
|
6611
|
+
width = Math.max(width, nextPageBounds.maxX)
|
|
6482
6612
|
|
|
6483
|
-
if (
|
|
6613
|
+
if (nextPageBounds.width === space.width && nextPageBounds.height === space.height) {
|
|
6484
6614
|
// remove the space on a perfect fit
|
|
6485
6615
|
last = spaces.pop()!
|
|
6486
6616
|
if (i < spaces.length) spaces[i] = last
|
|
6487
|
-
} else if (
|
|
6617
|
+
} else if (nextPageBounds.height === space.height) {
|
|
6488
6618
|
// fit the shape into the space (width)
|
|
6489
|
-
space.x +=
|
|
6490
|
-
space.width -=
|
|
6491
|
-
} else if (
|
|
6619
|
+
space.x += nextPageBounds.width + gap
|
|
6620
|
+
space.width -= nextPageBounds.width + gap
|
|
6621
|
+
} else if (nextPageBounds.width === space.width) {
|
|
6492
6622
|
// fit the shape into the space (height)
|
|
6493
|
-
space.y +=
|
|
6494
|
-
space.height -=
|
|
6623
|
+
space.y += nextPageBounds.height + gap
|
|
6624
|
+
space.height -= nextPageBounds.height + gap
|
|
6495
6625
|
} else {
|
|
6496
6626
|
// split the space into two spaces
|
|
6497
6627
|
spaces.push(
|
|
6498
6628
|
new Box(
|
|
6499
|
-
space.x + (
|
|
6629
|
+
space.x + (nextPageBounds.width + gap),
|
|
6500
6630
|
space.y,
|
|
6501
|
-
space.width - (
|
|
6502
|
-
|
|
6631
|
+
space.width - (nextPageBounds.width + gap),
|
|
6632
|
+
nextPageBounds.height
|
|
6503
6633
|
)
|
|
6504
6634
|
)
|
|
6505
|
-
space.y +=
|
|
6506
|
-
space.height -=
|
|
6635
|
+
space.y += nextPageBounds.height + gap
|
|
6636
|
+
space.height -= nextPageBounds.height + gap
|
|
6507
6637
|
}
|
|
6508
6638
|
break
|
|
6509
6639
|
}
|
|
6510
6640
|
}
|
|
6511
6641
|
|
|
6512
|
-
const commonAfter = Box.Common(
|
|
6642
|
+
const commonAfter = Box.Common(shapeClustersToPack.map((s) => s.nextPageBounds))
|
|
6513
6643
|
const centerDelta = Vec.Sub(commonBounds.center, commonAfter.center)
|
|
6514
6644
|
|
|
6515
|
-
let nextBounds: Box
|
|
6516
|
-
|
|
6517
6645
|
const changes: TLShapePartial<any>[] = []
|
|
6518
6646
|
|
|
6519
|
-
for (
|
|
6520
|
-
|
|
6521
|
-
bounds = shapePageBounds[shape.id]
|
|
6522
|
-
nextBounds = nextShapePageBounds[shape.id]
|
|
6647
|
+
for (const { shapes, pageBounds, nextPageBounds } of shapeClustersToPack) {
|
|
6648
|
+
const delta = Vec.Sub(nextPageBounds.point, pageBounds.point).add(centerDelta)
|
|
6523
6649
|
|
|
6524
|
-
const
|
|
6525
|
-
|
|
6526
|
-
if (parentTransform) delta.rot(-parentTransform.rotation())
|
|
6527
|
-
|
|
6528
|
-
const change: TLShapePartial = {
|
|
6529
|
-
id: shape.id,
|
|
6530
|
-
type: shape.type,
|
|
6531
|
-
x: shape.x + delta.x,
|
|
6532
|
-
y: shape.y + delta.y,
|
|
6533
|
-
}
|
|
6650
|
+
for (const shape of shapes) {
|
|
6651
|
+
const shapeDelta = delta.clone()
|
|
6534
6652
|
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6653
|
+
const parent = this.getShapeParent(shape)
|
|
6654
|
+
if (parent) {
|
|
6655
|
+
const parentTransform = this.getShapeParentTransform(shape)
|
|
6656
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6657
|
+
}
|
|
6539
6658
|
|
|
6540
|
-
|
|
6541
|
-
changes.push(
|
|
6542
|
-
} else {
|
|
6543
|
-
changes.push(change)
|
|
6659
|
+
shapeDelta.add(shape)
|
|
6660
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6544
6661
|
}
|
|
6545
6662
|
}
|
|
6546
6663
|
|
|
@@ -6565,32 +6682,78 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6565
6682
|
*
|
|
6566
6683
|
* @public
|
|
6567
6684
|
*/
|
|
6568
|
-
|
|
6569
6685
|
alignShapes(
|
|
6570
6686
|
shapes: TLShapeId[] | TLShape[],
|
|
6571
6687
|
operation: 'left' | 'center-horizontal' | 'right' | 'top' | 'center-vertical' | 'bottom'
|
|
6572
6688
|
): this {
|
|
6689
|
+
if (this.getIsReadonly()) return this
|
|
6690
|
+
|
|
6573
6691
|
const ids =
|
|
6574
6692
|
typeof shapes[0] === 'string'
|
|
6575
6693
|
? (shapes as TLShapeId[])
|
|
6576
6694
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6577
6695
|
|
|
6578
|
-
|
|
6579
|
-
|
|
6696
|
+
// Always get fresh shapes
|
|
6697
|
+
const shapesToAlignFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6580
6698
|
|
|
6581
|
-
const
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
const
|
|
6699
|
+
const shapeClustersToAlign: {
|
|
6700
|
+
shapes: TLShape[]
|
|
6701
|
+
pageBounds: Box
|
|
6702
|
+
}[] = []
|
|
6703
|
+
const allBounds: Box[] = []
|
|
6704
|
+
const visited = new Set<TLShapeId>()
|
|
6586
6705
|
|
|
6587
|
-
const
|
|
6706
|
+
for (const shape of shapesToAlignFirstPass) {
|
|
6707
|
+
if (visited.has(shape.id)) continue
|
|
6708
|
+
visited.add(shape.id)
|
|
6588
6709
|
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6710
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6711
|
+
if (!shapePageBounds) continue
|
|
6712
|
+
|
|
6713
|
+
if (
|
|
6714
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
6715
|
+
type: 'align',
|
|
6716
|
+
shapes: shapesToAlignFirstPass,
|
|
6717
|
+
})
|
|
6718
|
+
) {
|
|
6719
|
+
continue
|
|
6720
|
+
}
|
|
6721
|
+
|
|
6722
|
+
// In this implementation, we want to create psuedo-groups out of shapes that
|
|
6723
|
+
// are moving together. At the moment shapes only move together if they're connected
|
|
6724
|
+
// by arrows. So let's say A -> B -> C -> D and A, B, and C are selected. If we're
|
|
6725
|
+
// aligning A, B, and C, then we want these to move together as one unit.
|
|
6726
|
+
|
|
6727
|
+
const shapesMovingTogether = [shape]
|
|
6728
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
6729
|
+
|
|
6730
|
+
this.collectShapesViaArrowBindings({
|
|
6731
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6732
|
+
initialShapes: shapesToAlignFirstPass,
|
|
6733
|
+
resultShapes: shapesMovingTogether,
|
|
6734
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6735
|
+
visited,
|
|
6736
|
+
})
|
|
6592
6737
|
|
|
6593
|
-
const
|
|
6738
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6739
|
+
if (!commonPageBounds) continue
|
|
6740
|
+
|
|
6741
|
+
shapeClustersToAlign.push({
|
|
6742
|
+
shapes: shapesMovingTogether,
|
|
6743
|
+
pageBounds: commonPageBounds,
|
|
6744
|
+
})
|
|
6745
|
+
|
|
6746
|
+
allBounds.push(commonPageBounds)
|
|
6747
|
+
}
|
|
6748
|
+
|
|
6749
|
+
if (shapeClustersToAlign.length < 2) return this
|
|
6750
|
+
|
|
6751
|
+
const commonBounds = Box.Common(allBounds)
|
|
6752
|
+
|
|
6753
|
+
const changes: TLShapePartial[] = []
|
|
6754
|
+
|
|
6755
|
+
shapeClustersToAlign.forEach(({ shapes, pageBounds }) => {
|
|
6756
|
+
const delta = new Vec()
|
|
6594
6757
|
|
|
6595
6758
|
switch (operation) {
|
|
6596
6759
|
case 'top': {
|
|
@@ -6619,12 +6782,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6619
6782
|
}
|
|
6620
6783
|
}
|
|
6621
6784
|
|
|
6622
|
-
const
|
|
6623
|
-
|
|
6624
|
-
? Vec.Rot(delta, -this.getShapePageTransform(parent)!.decompose().rotation)
|
|
6625
|
-
: delta
|
|
6785
|
+
for (const shape of shapes) {
|
|
6786
|
+
const shapeDelta = delta.clone()
|
|
6626
6787
|
|
|
6627
|
-
|
|
6788
|
+
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
6789
|
+
// todo: ensure that the parent isn't being aligned together with its children
|
|
6790
|
+
const parent = this.getShapeParent(shape)
|
|
6791
|
+
if (parent) {
|
|
6792
|
+
const parentTransform = this.getShapePageTransform(parent)
|
|
6793
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6794
|
+
}
|
|
6795
|
+
|
|
6796
|
+
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
6797
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6798
|
+
}
|
|
6628
6799
|
})
|
|
6629
6800
|
|
|
6630
6801
|
this.updateShapes(changes)
|
|
@@ -6646,65 +6817,137 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6646
6817
|
* @public
|
|
6647
6818
|
*/
|
|
6648
6819
|
distributeShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
|
|
6820
|
+
if (this.getIsReadonly()) return this
|
|
6821
|
+
|
|
6649
6822
|
const ids =
|
|
6650
6823
|
typeof shapes[0] === 'string'
|
|
6651
6824
|
? (shapes as TLShapeId[])
|
|
6652
6825
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6653
6826
|
|
|
6654
|
-
|
|
6655
|
-
|
|
6827
|
+
// always fresh shapes
|
|
6828
|
+
const shapesToDistributeFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
6656
6829
|
|
|
6657
|
-
const
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6830
|
+
const shapeClustersToDistribute: {
|
|
6831
|
+
shapes: TLShape[]
|
|
6832
|
+
pageBounds: Box
|
|
6833
|
+
}[] = []
|
|
6834
|
+
|
|
6835
|
+
const allBounds: Box[] = []
|
|
6836
|
+
const visited = new Set<TLShapeId>()
|
|
6837
|
+
|
|
6838
|
+
for (const shape of shapesToDistributeFirstPass) {
|
|
6839
|
+
if (visited.has(shape.id)) continue
|
|
6840
|
+
visited.add(shape.id)
|
|
6841
|
+
|
|
6842
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6843
|
+
if (!shapePageBounds) continue
|
|
6844
|
+
|
|
6845
|
+
if (
|
|
6846
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
6847
|
+
type: 'distribute',
|
|
6848
|
+
shapes: shapesToDistributeFirstPass,
|
|
6849
|
+
})
|
|
6850
|
+
) {
|
|
6851
|
+
continue
|
|
6852
|
+
}
|
|
6853
|
+
|
|
6854
|
+
const shapesMovingTogether = [shape]
|
|
6855
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
6856
|
+
|
|
6857
|
+
this.collectShapesViaArrowBindings({
|
|
6858
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
6859
|
+
initialShapes: shapesToDistributeFirstPass,
|
|
6860
|
+
resultShapes: shapesMovingTogether,
|
|
6861
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
6862
|
+
visited,
|
|
6863
|
+
})
|
|
6864
|
+
|
|
6865
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
6866
|
+
if (!commonPageBounds) continue
|
|
6867
|
+
|
|
6868
|
+
shapeClustersToDistribute.push({
|
|
6869
|
+
shapes: shapesMovingTogether,
|
|
6870
|
+
pageBounds: commonPageBounds,
|
|
6871
|
+
})
|
|
6872
|
+
|
|
6873
|
+
allBounds.push(commonPageBounds)
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6876
|
+
if (shapeClustersToDistribute.length < 3) return this
|
|
6662
6877
|
|
|
6663
6878
|
let val: 'x' | 'y'
|
|
6664
6879
|
let min: 'minX' | 'minY'
|
|
6665
6880
|
let max: 'maxX' | 'maxY'
|
|
6666
|
-
let mid: 'midX' | 'midY'
|
|
6667
6881
|
let dim: 'width' | 'height'
|
|
6668
6882
|
|
|
6669
6883
|
if (operation === 'horizontal') {
|
|
6670
6884
|
val = 'x'
|
|
6671
6885
|
min = 'minX'
|
|
6672
6886
|
max = 'maxX'
|
|
6673
|
-
mid = 'midX'
|
|
6674
6887
|
dim = 'width'
|
|
6675
6888
|
} else {
|
|
6676
6889
|
val = 'y'
|
|
6677
6890
|
min = 'minY'
|
|
6678
6891
|
max = 'maxY'
|
|
6679
|
-
mid = 'midY'
|
|
6680
6892
|
dim = 'height'
|
|
6681
6893
|
}
|
|
6682
6894
|
const changes: TLShapePartial[] = []
|
|
6683
6895
|
|
|
6684
|
-
|
|
6685
|
-
const
|
|
6686
|
-
(a, b) => pageBounds[a.id][min] - pageBounds[b.id][min]
|
|
6687
|
-
)[0]
|
|
6688
|
-
const last = shapesToDistribute.sort((a, b) => pageBounds[b.id][max] - pageBounds[a.id][max])[0]
|
|
6896
|
+
const first = shapeClustersToDistribute.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])[0]
|
|
6897
|
+
const last = shapeClustersToDistribute.sort((a, b) => b.pageBounds[max] - a.pageBounds[max])[0]
|
|
6689
6898
|
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6899
|
+
// If the first shape group is also the last shape group, distribute without it
|
|
6900
|
+
if (first === last) {
|
|
6901
|
+
const excludedShapeIds = new Set(first.shapes.map((s) => s.id))
|
|
6902
|
+
return this.distributeShapes(
|
|
6903
|
+
ids.filter((id) => !excludedShapeIds.has(id)),
|
|
6904
|
+
operation
|
|
6905
|
+
)
|
|
6906
|
+
}
|
|
6693
6907
|
|
|
6694
|
-
|
|
6908
|
+
const shapeClustersToMove = shapeClustersToDistribute
|
|
6695
6909
|
.filter((shape) => shape !== first && shape !== last)
|
|
6696
|
-
.sort((a, b) =>
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6910
|
+
.sort((a, b) => {
|
|
6911
|
+
if (a.pageBounds[min] === b.pageBounds[min]) {
|
|
6912
|
+
return a.shapes[0].id < b.shapes[0].id ? -1 : 1
|
|
6913
|
+
}
|
|
6914
|
+
return a.pageBounds[min] - b.pageBounds[min]
|
|
6915
|
+
})
|
|
6916
|
+
|
|
6917
|
+
// 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.
|
|
6918
|
+
const maxFirst = first.pageBounds[max]
|
|
6919
|
+
const range = last.pageBounds[min] - maxFirst
|
|
6920
|
+
const summedShapeDimensions = shapeClustersToMove.reduce((acc, s) => acc + s.pageBounds[dim], 0)
|
|
6921
|
+
const gap = (range - summedShapeDimensions) / (shapeClustersToMove.length + 1)
|
|
6922
|
+
|
|
6923
|
+
for (let v = maxFirst + gap, i = 0; i < shapeClustersToMove.length; i++) {
|
|
6924
|
+
const { shapes, pageBounds } = shapeClustersToMove[i]
|
|
6925
|
+
const delta = new Vec()
|
|
6926
|
+
delta[val] = v - pageBounds[val]
|
|
6700
6927
|
|
|
6928
|
+
// If for some reason the new position would be more than the maximum, we need to adjust the delta
|
|
6929
|
+
// This will likely throw off some of the other placements but hey, it's better than changing the common bounds
|
|
6930
|
+
if (v + pageBounds[dim] > last.pageBounds[max] - 1) {
|
|
6931
|
+
delta[val] = last.pageBounds[max] - pageBounds[max] - 1
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6934
|
+
for (const shape of shapes) {
|
|
6935
|
+
const shapeDelta = delta.clone()
|
|
6936
|
+
|
|
6937
|
+
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
6938
|
+
// todo: ensure that the parent isn't being aligned together with its children
|
|
6701
6939
|
const parent = this.getShapeParent(shape)
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6940
|
+
if (parent) {
|
|
6941
|
+
const parentTransform = this.getShapePageTransform(parent)
|
|
6942
|
+
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
6943
|
+
}
|
|
6705
6944
|
|
|
6706
|
-
|
|
6707
|
-
|
|
6945
|
+
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
6946
|
+
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
6947
|
+
}
|
|
6948
|
+
|
|
6949
|
+
v += pageBounds[dim] + gap
|
|
6950
|
+
}
|
|
6708
6951
|
|
|
6709
6952
|
this.updateShapes(changes)
|
|
6710
6953
|
return this
|
|
@@ -6731,65 +6974,106 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6731
6974
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6732
6975
|
|
|
6733
6976
|
if (this.getIsReadonly()) return this
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
const
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
}
|
|
6977
|
+
|
|
6978
|
+
// always fresh shapes, skip anything that isn't rotated 90 deg
|
|
6979
|
+
const shapesToStretchFirstPass = compact(ids.map((id) => this.getShape(id))).filter(
|
|
6980
|
+
(s) => this.getShapePageTransform(s)?.rotation() % (PI / 2) === 0
|
|
6981
|
+
)
|
|
6982
|
+
|
|
6983
|
+
const shapeClustersToStretch: {
|
|
6984
|
+
shapes: TLShape[]
|
|
6985
|
+
pageBounds: Box
|
|
6986
|
+
}[] = []
|
|
6987
|
+
|
|
6988
|
+
const allBounds: Box[] = []
|
|
6989
|
+
const visited = new Set<TLShapeId>()
|
|
6990
|
+
|
|
6991
|
+
for (const shape of shapesToStretchFirstPass) {
|
|
6992
|
+
if (visited.has(shape.id)) continue
|
|
6993
|
+
visited.add(shape.id)
|
|
6994
|
+
|
|
6995
|
+
const shapePageBounds = this.getShapePageBounds(shape)
|
|
6996
|
+
if (!shapePageBounds) continue
|
|
6997
|
+
|
|
6998
|
+
const shapesMovingTogether = [shape]
|
|
6999
|
+
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
7000
|
+
|
|
7001
|
+
if (
|
|
7002
|
+
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
7003
|
+
type: 'stretch',
|
|
7004
|
+
shapes: shapesToStretchFirstPass,
|
|
6763
7005
|
})
|
|
6764
|
-
|
|
7006
|
+
) {
|
|
7007
|
+
continue
|
|
6765
7008
|
}
|
|
6766
|
-
case 'horizontal': {
|
|
6767
|
-
this.run(() => {
|
|
6768
|
-
for (const shape of shapesToStretch) {
|
|
6769
|
-
const bounds = shapeBounds[shape.id]
|
|
6770
|
-
const pageBounds = shapePageBounds[shape.id]
|
|
6771
|
-
const pageRotation = this.getShapePageTransform(shape)!.rotation()
|
|
6772
|
-
if (pageRotation % PI2) continue
|
|
6773
|
-
const localOffset = new Vec(commonBounds.minX - pageBounds.minX, 0)
|
|
6774
|
-
const parentTransform = this.getShapeParentTransform(shape)
|
|
6775
|
-
if (parentTransform) localOffset.rot(-parentTransform.rotation())
|
|
6776
|
-
|
|
6777
|
-
const { x, y } = Vec.Add(localOffset, shape)
|
|
6778
|
-
this.updateShapes([{ id: shape.id, type: shape.type, x, y }])
|
|
6779
|
-
const scale = new Vec(commonBounds.width / pageBounds.width, 1)
|
|
6780
|
-
this.resizeShape(shape.id, scale, {
|
|
6781
|
-
initialBounds: bounds,
|
|
6782
|
-
scaleOrigin: new Vec(commonBounds.minX, pageBounds.center.y),
|
|
6783
|
-
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
|
|
6784
|
-
scaleAxisRotation: 0,
|
|
6785
|
-
})
|
|
6786
|
-
}
|
|
6787
|
-
})
|
|
6788
7009
|
|
|
6789
|
-
|
|
6790
|
-
|
|
7010
|
+
this.collectShapesViaArrowBindings({
|
|
7011
|
+
bindings: this.getBindingsToShape(shape.id, 'arrow'),
|
|
7012
|
+
initialShapes: shapesToStretchFirstPass,
|
|
7013
|
+
resultShapes: shapesMovingTogether,
|
|
7014
|
+
resultBounds: boundsOfShapesMovingTogether,
|
|
7015
|
+
visited,
|
|
7016
|
+
})
|
|
7017
|
+
|
|
7018
|
+
const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
|
|
7019
|
+
if (!commonPageBounds) continue
|
|
7020
|
+
|
|
7021
|
+
shapeClustersToStretch.push({
|
|
7022
|
+
shapes: shapesMovingTogether,
|
|
7023
|
+
pageBounds: commonPageBounds,
|
|
7024
|
+
})
|
|
7025
|
+
|
|
7026
|
+
allBounds.push(commonPageBounds)
|
|
7027
|
+
}
|
|
7028
|
+
|
|
7029
|
+
if (shapeClustersToStretch.length < 2) return this
|
|
7030
|
+
|
|
7031
|
+
const commonBounds = Box.Common(allBounds)
|
|
7032
|
+
let val: 'x' | 'y'
|
|
7033
|
+
let min: 'minX' | 'minY'
|
|
7034
|
+
let dim: 'width' | 'height'
|
|
7035
|
+
|
|
7036
|
+
if (operation === 'horizontal') {
|
|
7037
|
+
val = 'x'
|
|
7038
|
+
min = 'minX'
|
|
7039
|
+
dim = 'width'
|
|
7040
|
+
} else {
|
|
7041
|
+
val = 'y'
|
|
7042
|
+
min = 'minY'
|
|
7043
|
+
dim = 'height'
|
|
6791
7044
|
}
|
|
6792
7045
|
|
|
7046
|
+
this.run(() => {
|
|
7047
|
+
shapeClustersToStretch.forEach(({ shapes, pageBounds }) => {
|
|
7048
|
+
const localOffset = new Vec()
|
|
7049
|
+
localOffset[val] = commonBounds[min] - pageBounds[min]
|
|
7050
|
+
|
|
7051
|
+
const scaleOrigin = pageBounds.center.clone()
|
|
7052
|
+
scaleOrigin[val] = commonBounds[min]
|
|
7053
|
+
|
|
7054
|
+
const scale = new Vec(1, 1)
|
|
7055
|
+
scale[val] = commonBounds[dim] / pageBounds[dim]
|
|
7056
|
+
|
|
7057
|
+
for (const shape of shapes) {
|
|
7058
|
+
// First translate
|
|
7059
|
+
const shapeLocalOffset = localOffset.clone()
|
|
7060
|
+
const parentTransform = this.getShapeParentTransform(shape)
|
|
7061
|
+
if (parentTransform) localOffset.rot(-parentTransform.rotation())
|
|
7062
|
+
shapeLocalOffset.add(shape)
|
|
7063
|
+
const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
|
|
7064
|
+
this.updateShape(changes)
|
|
7065
|
+
|
|
7066
|
+
// Then resize
|
|
7067
|
+
this.resizeShape(shape.id, scale, {
|
|
7068
|
+
initialBounds: this.getShapeGeometry(shape).bounds,
|
|
7069
|
+
scaleOrigin,
|
|
7070
|
+
isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
|
|
7071
|
+
scaleAxisRotation: 0,
|
|
7072
|
+
})
|
|
7073
|
+
}
|
|
7074
|
+
})
|
|
7075
|
+
})
|
|
7076
|
+
|
|
6793
7077
|
return this
|
|
6794
7078
|
}
|
|
6795
7079
|
|
|
@@ -10057,7 +10341,7 @@ function withIsolatedShapes<T>(
|
|
|
10057
10341
|
}
|
|
10058
10342
|
})
|
|
10059
10343
|
|
|
10060
|
-
editor.store.applyDiff(reverseRecordsDiff(changes))
|
|
10344
|
+
editor.store.applyDiff(reverseRecordsDiff(changes), { runCallbacks: false })
|
|
10061
10345
|
},
|
|
10062
10346
|
{ history: 'ignore' }
|
|
10063
10347
|
)
|