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