@nasser-sw/fabric 7.0.1-beta24 → 7.0.1-beta26

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.
Files changed (42) hide show
  1. package/.claude/settings.local.json +8 -7
  2. package/.history/package_20251227124512.json +164 -0
  3. package/dist/index.js +765 -38
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +765 -38
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/index.node.cjs +765 -38
  8. package/dist/index.node.cjs.map +1 -1
  9. package/dist/index.node.mjs +765 -38
  10. package/dist/index.node.mjs.map +1 -1
  11. package/dist/package.json.mjs +1 -1
  12. package/dist/src/controls/expandControl.d.ts +81 -0
  13. package/dist/src/controls/expandControl.d.ts.map +1 -0
  14. package/dist/src/controls/expandControl.mjs +408 -0
  15. package/dist/src/controls/expandControl.mjs.map +1 -0
  16. package/dist/src/controls/index.d.ts +1 -0
  17. package/dist/src/controls/index.d.ts.map +1 -1
  18. package/dist/src/controls/index.mjs +1 -0
  19. package/dist/src/controls/index.mjs.map +1 -1
  20. package/dist/src/controls/skew.mjs +1 -1
  21. package/dist/src/shapes/Frame.d.ts +48 -0
  22. package/dist/src/shapes/Frame.d.ts.map +1 -1
  23. package/dist/src/shapes/Frame.mjs +146 -37
  24. package/dist/src/shapes/Frame.mjs.map +1 -1
  25. package/dist/src/shapes/Object/InteractiveObject.d.ts +69 -0
  26. package/dist/src/shapes/Object/InteractiveObject.d.ts.map +1 -1
  27. package/dist/src/shapes/Object/InteractiveObject.mjs +205 -0
  28. package/dist/src/shapes/Object/InteractiveObject.mjs.map +1 -1
  29. package/dist-extensions/src/controls/expandControl.d.ts +81 -0
  30. package/dist-extensions/src/controls/expandControl.d.ts.map +1 -0
  31. package/dist-extensions/src/controls/index.d.ts +1 -0
  32. package/dist-extensions/src/controls/index.d.ts.map +1 -1
  33. package/dist-extensions/src/shapes/Frame.d.ts +48 -0
  34. package/dist-extensions/src/shapes/Frame.d.ts.map +1 -1
  35. package/dist-extensions/src/shapes/Object/InteractiveObject.d.ts +69 -0
  36. package/dist-extensions/src/shapes/Object/InteractiveObject.d.ts.map +1 -1
  37. package/docs/EXPAND_MODE_API.md +585 -0
  38. package/package.json +1 -1
  39. package/src/controls/expandControl.ts +491 -0
  40. package/src/controls/index.ts +1 -0
  41. package/src/shapes/Frame.ts +153 -37
  42. package/src/shapes/Object/InteractiveObject.ts +219 -0
package/dist/index.mjs CHANGED
@@ -354,7 +354,7 @@ class Cache {
354
354
  }
355
355
  const cache = new Cache();
356
356
 
357
- var version = "7.0.1-beta23";
357
+ var version = "7.0.1-beta24";
358
358
 
359
359
  // use this syntax so babel plugin see this import here
360
360
  const VERSION = version;
@@ -9263,6 +9263,406 @@ const createTextboxDefaultControls = () => {
9263
9263
  return controls;
9264
9264
  };
9265
9265
 
9266
+ /**
9267
+ * Expansion state for AI outpainting
9268
+ */
9269
+
9270
+ /**
9271
+ * Direction(s) that an expand control affects
9272
+ */
9273
+
9274
+ /**
9275
+ * Styling constants for expand controls
9276
+ */
9277
+ const EXPAND_HANDLE_FILL = '#ffffff';
9278
+ const EXPAND_HANDLE_STROKE = '#8b5cf6'; // Purple
9279
+ const EXPAND_PREVIEW_FILL = 'rgba(139, 92, 246, 0.1)';
9280
+ const EXPAND_PREVIEW_STROKE = '#8b5cf6';
9281
+ const EXPAND_PREVIEW_DASH = [6, 4];
9282
+
9283
+ // Handle sizes - similar to default Fabric controls
9284
+ const EDGE_HANDLE_WIDTH = 6;
9285
+ const EDGE_HANDLE_HEIGHT = 20;
9286
+ const CORNER_HANDLE_SIZE = 10;
9287
+ const HANDLE_RADIUS = 2;
9288
+
9289
+ /**
9290
+ * Default expansion state
9291
+ */
9292
+ const createDefaultExpansion = () => ({
9293
+ expandLeft: 0,
9294
+ expandRight: 0,
9295
+ expandTop: 0,
9296
+ expandBottom: 0
9297
+ });
9298
+
9299
+ /**
9300
+ * Get expansion state from object, initializing if needed
9301
+ */
9302
+ function getExpansion(obj) {
9303
+ if (!obj.expansion) {
9304
+ obj.expansion = createDefaultExpansion();
9305
+ }
9306
+ return obj.expansion;
9307
+ }
9308
+
9309
+ /**
9310
+ * Set expansion state on object
9311
+ */
9312
+ function setExpansion(obj, expansion) {
9313
+ const current = getExpansion(obj);
9314
+ Object.assign(current, expansion);
9315
+ }
9316
+
9317
+ /**
9318
+ * Custom control for AI expansion handles
9319
+ */
9320
+ class ExpandControl extends Control {
9321
+ /**
9322
+ * Which direction(s) this control affects
9323
+ */
9324
+
9325
+ constructor(options) {
9326
+ super(options);
9327
+ this.expandDirections = options.expandDirections;
9328
+ this.actionName = 'expand';
9329
+ }
9330
+
9331
+ /**
9332
+ * Position handler for expand controls
9333
+ * Positions controls at the expansion boundary, not the object boundary
9334
+ * Note: expansion values are stored in screen pixels (already scaled)
9335
+ */
9336
+ positionHandler(dim, finalMatrix, fabricObject, currentControl) {
9337
+ const expansion = getExpansion(fabricObject);
9338
+ const {
9339
+ expandLeft,
9340
+ expandRight,
9341
+ expandTop,
9342
+ expandBottom
9343
+ } = expansion;
9344
+
9345
+ // dim already includes scale, so use it directly for base size
9346
+ // expansion values are also in screen pixels, so use directly
9347
+ const halfWidth = dim.x / 2;
9348
+ const halfHeight = dim.y / 2;
9349
+
9350
+ // Calculate the expanded half dimensions
9351
+ const expandedHalfWidth = halfWidth + (expandLeft + expandRight) / 2;
9352
+ const expandedHalfHeight = halfHeight + (expandTop + expandBottom) / 2;
9353
+
9354
+ // Calculate offset from center based on asymmetric expansion
9355
+ const centerOffsetX = (expandRight - expandLeft) / 2;
9356
+ const centerOffsetY = (expandBottom - expandTop) / 2;
9357
+ let posX;
9358
+ let posY;
9359
+
9360
+ // Position based on which side this control is on
9361
+ if (this.x < 0) {
9362
+ // Left side controls
9363
+ posX = -expandedHalfWidth + centerOffsetX;
9364
+ } else if (this.x > 0) {
9365
+ // Right side controls
9366
+ posX = expandedHalfWidth + centerOffsetX;
9367
+ } else {
9368
+ // Center (top/bottom only)
9369
+ posX = centerOffsetX;
9370
+ }
9371
+ if (this.y < 0) {
9372
+ // Top side controls
9373
+ posY = -expandedHalfHeight + centerOffsetY;
9374
+ } else if (this.y > 0) {
9375
+ // Bottom side controls
9376
+ posY = expandedHalfHeight + centerOffsetY;
9377
+ } else {
9378
+ // Center (left/right only)
9379
+ posY = centerOffsetY;
9380
+ }
9381
+ return new Point(posX, posY).transform(finalMatrix);
9382
+ }
9383
+
9384
+ /**
9385
+ * Custom render for expand handles - purple border with white fill
9386
+ */
9387
+ render(ctx, left, top, styleOverride, fabricObject) {
9388
+ ctx.save();
9389
+ ctx.translate(left, top);
9390
+ const angle = fabricObject.getTotalAngle();
9391
+ ctx.rotate(degreesToRadians(angle));
9392
+
9393
+ // Add subtle shadow
9394
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
9395
+ ctx.shadowBlur = 3;
9396
+ ctx.shadowOffsetX = 0;
9397
+ ctx.shadowOffsetY = 1;
9398
+ ctx.fillStyle = EXPAND_HANDLE_FILL;
9399
+ ctx.strokeStyle = EXPAND_HANDLE_STROKE;
9400
+ ctx.lineWidth = 2;
9401
+
9402
+ // Determine if this is a corner or edge control
9403
+ const isCorner = this.x !== 0 && this.y !== 0;
9404
+ if (isCorner) {
9405
+ // Corner: draw a circle
9406
+ ctx.beginPath();
9407
+ ctx.arc(0, 0, CORNER_HANDLE_SIZE / 2, 0, Math.PI * 2);
9408
+ ctx.fill();
9409
+ ctx.stroke();
9410
+ } else if (this.y === 0) {
9411
+ // Horizontal edge (left/right): vertical pill
9412
+ ctx.beginPath();
9413
+ ctx.roundRect(-EDGE_HANDLE_WIDTH / 2, -EDGE_HANDLE_HEIGHT / 2, EDGE_HANDLE_WIDTH, EDGE_HANDLE_HEIGHT, HANDLE_RADIUS);
9414
+ ctx.fill();
9415
+ ctx.stroke();
9416
+ } else {
9417
+ // Vertical edge (top/bottom): horizontal pill
9418
+ ctx.beginPath();
9419
+ ctx.roundRect(-EDGE_HANDLE_HEIGHT / 2, -EDGE_HANDLE_WIDTH / 2, EDGE_HANDLE_HEIGHT, EDGE_HANDLE_WIDTH, HANDLE_RADIUS);
9420
+ ctx.fill();
9421
+ ctx.stroke();
9422
+ }
9423
+ ctx.restore();
9424
+ }
9425
+ }
9426
+
9427
+ /**
9428
+ * Action handler for expanding left edge
9429
+ */
9430
+ const expandLeftHandler = (eventData, transform, x, y) => {
9431
+ const {
9432
+ target
9433
+ } = transform;
9434
+ const expansion = getExpansion(target);
9435
+
9436
+ // Use Fabric's getLocalPoint with center origin (returns scaled coordinates)
9437
+ const localPoint = getLocalPoint(transform, CENTER, CENTER, x, y);
9438
+
9439
+ // Use SCALED half width since localPoint is in scaled coords
9440
+ const scaleX = target.scaleX || 1;
9441
+ const scaledHalfWidth = target.width * scaleX / 2;
9442
+
9443
+ // Calculate new expansion (how far left of the object's left edge)
9444
+ // localPoint.x is negative when left of center
9445
+ // Expansion starts when localPoint.x < -scaledHalfWidth
9446
+ const newExpandLeft = Math.max(0, -scaledHalfWidth - localPoint.x);
9447
+ if (Math.abs(newExpandLeft - expansion.expandLeft) > 0.5) {
9448
+ var _target$canvas;
9449
+ expansion.expandLeft = newExpandLeft;
9450
+ target.setCoords();
9451
+ (_target$canvas = target.canvas) === null || _target$canvas === void 0 || _target$canvas.requestRenderAll();
9452
+ return true;
9453
+ }
9454
+ return false;
9455
+ };
9456
+
9457
+ /**
9458
+ * Action handler for expanding right edge
9459
+ */
9460
+ const expandRightHandler = (eventData, transform, x, y) => {
9461
+ const {
9462
+ target
9463
+ } = transform;
9464
+ const expansion = getExpansion(target);
9465
+
9466
+ // Use Fabric's getLocalPoint with center origin (returns scaled coordinates)
9467
+ const localPoint = getLocalPoint(transform, CENTER, CENTER, x, y);
9468
+
9469
+ // Use SCALED half width since localPoint is in scaled coords
9470
+ const scaleX = target.scaleX || 1;
9471
+ const scaledHalfWidth = target.width * scaleX / 2;
9472
+
9473
+ // Calculate new expansion (how far right of the object's right edge)
9474
+ // localPoint.x is positive when right of center
9475
+ // Expansion starts when localPoint.x > scaledHalfWidth
9476
+ const newExpandRight = Math.max(0, localPoint.x - scaledHalfWidth);
9477
+ if (Math.abs(newExpandRight - expansion.expandRight) > 0.5) {
9478
+ var _target$canvas2;
9479
+ expansion.expandRight = newExpandRight;
9480
+ target.setCoords();
9481
+ (_target$canvas2 = target.canvas) === null || _target$canvas2 === void 0 || _target$canvas2.requestRenderAll();
9482
+ return true;
9483
+ }
9484
+ return false;
9485
+ };
9486
+
9487
+ /**
9488
+ * Action handler for expanding top edge
9489
+ */
9490
+ const expandTopHandler = (eventData, transform, x, y) => {
9491
+ const {
9492
+ target
9493
+ } = transform;
9494
+ const expansion = getExpansion(target);
9495
+
9496
+ // Use Fabric's getLocalPoint with center origin (returns scaled coordinates)
9497
+ const localPoint = getLocalPoint(transform, CENTER, CENTER, x, y);
9498
+
9499
+ // Use SCALED half height since localPoint is in scaled coords
9500
+ const scaleY = target.scaleY || 1;
9501
+ const scaledHalfHeight = target.height * scaleY / 2;
9502
+
9503
+ // Calculate new expansion (how far above the object's top edge)
9504
+ const newExpandTop = Math.max(0, -scaledHalfHeight - localPoint.y);
9505
+ if (Math.abs(newExpandTop - expansion.expandTop) > 0.5) {
9506
+ var _target$canvas3;
9507
+ expansion.expandTop = newExpandTop;
9508
+ target.setCoords();
9509
+ (_target$canvas3 = target.canvas) === null || _target$canvas3 === void 0 || _target$canvas3.requestRenderAll();
9510
+ return true;
9511
+ }
9512
+ return false;
9513
+ };
9514
+
9515
+ /**
9516
+ * Action handler for expanding bottom edge
9517
+ */
9518
+ const expandBottomHandler = (eventData, transform, x, y) => {
9519
+ const {
9520
+ target
9521
+ } = transform;
9522
+ const expansion = getExpansion(target);
9523
+
9524
+ // Use Fabric's getLocalPoint with center origin (returns scaled coordinates)
9525
+ const localPoint = getLocalPoint(transform, CENTER, CENTER, x, y);
9526
+
9527
+ // Use SCALED half height since localPoint is in scaled coords
9528
+ const scaleY = target.scaleY || 1;
9529
+ const scaledHalfHeight = target.height * scaleY / 2;
9530
+
9531
+ // Calculate new expansion (how far below the object's bottom edge)
9532
+ const newExpandBottom = Math.max(0, localPoint.y - scaledHalfHeight);
9533
+ if (Math.abs(newExpandBottom - expansion.expandBottom) > 0.5) {
9534
+ var _target$canvas4;
9535
+ expansion.expandBottom = newExpandBottom;
9536
+ target.setCoords();
9537
+ (_target$canvas4 = target.canvas) === null || _target$canvas4 === void 0 || _target$canvas4.requestRenderAll();
9538
+ return true;
9539
+ }
9540
+ return false;
9541
+ };
9542
+
9543
+ /**
9544
+ * Combined handler for corner controls (affects two directions)
9545
+ */
9546
+ function createCornerExpandHandler(horizontalHandler, verticalHandler) {
9547
+ return (eventData, transform, x, y) => {
9548
+ const h = horizontalHandler(eventData, transform, x, y);
9549
+ const v = verticalHandler(eventData, transform, x, y);
9550
+ return h || v;
9551
+ };
9552
+ }
9553
+
9554
+ /**
9555
+ * Cursor style handler for expand controls
9556
+ */
9557
+ function expandCursorStyleHandler(eventData, control, fabricObject) {
9558
+ const expandControl = control;
9559
+ const directions = expandControl.expandDirections;
9560
+
9561
+ // Determine cursor based on direction
9562
+ if (directions.includes('left') && directions.includes('right')) {
9563
+ return 'ew-resize';
9564
+ }
9565
+ if (directions.includes('top') && directions.includes('bottom')) {
9566
+ return 'ns-resize';
9567
+ }
9568
+ if (directions.includes('left') || directions.includes('right')) {
9569
+ return 'ew-resize';
9570
+ }
9571
+ if (directions.includes('top') || directions.includes('bottom')) {
9572
+ return 'ns-resize';
9573
+ }
9574
+ if (directions.length === 2) {
9575
+ // Corner
9576
+ if (directions.includes('left') && directions.includes('top') || directions.includes('right') && directions.includes('bottom')) {
9577
+ return 'nwse-resize';
9578
+ }
9579
+ return 'nesw-resize';
9580
+ }
9581
+ return 'move';
9582
+ }
9583
+
9584
+ /**
9585
+ * Create the set of expand controls for an object
9586
+ */
9587
+ function createExpandControls() {
9588
+ return {
9589
+ // Edge controls
9590
+ ml: new ExpandControl({
9591
+ x: -0.5,
9592
+ y: 0,
9593
+ expandDirections: ['left'],
9594
+ actionHandler: expandLeftHandler,
9595
+ cursorStyleHandler: expandCursorStyleHandler,
9596
+ sizeX: EDGE_HANDLE_WIDTH,
9597
+ sizeY: EDGE_HANDLE_HEIGHT
9598
+ }),
9599
+ mr: new ExpandControl({
9600
+ x: 0.5,
9601
+ y: 0,
9602
+ expandDirections: ['right'],
9603
+ actionHandler: expandRightHandler,
9604
+ cursorStyleHandler: expandCursorStyleHandler,
9605
+ sizeX: EDGE_HANDLE_WIDTH,
9606
+ sizeY: EDGE_HANDLE_HEIGHT
9607
+ }),
9608
+ mt: new ExpandControl({
9609
+ x: 0,
9610
+ y: -0.5,
9611
+ expandDirections: ['top'],
9612
+ actionHandler: expandTopHandler,
9613
+ cursorStyleHandler: expandCursorStyleHandler,
9614
+ sizeX: EDGE_HANDLE_HEIGHT,
9615
+ sizeY: EDGE_HANDLE_WIDTH
9616
+ }),
9617
+ mb: new ExpandControl({
9618
+ x: 0,
9619
+ y: 0.5,
9620
+ expandDirections: ['bottom'],
9621
+ actionHandler: expandBottomHandler,
9622
+ cursorStyleHandler: expandCursorStyleHandler,
9623
+ sizeX: EDGE_HANDLE_HEIGHT,
9624
+ sizeY: EDGE_HANDLE_WIDTH
9625
+ }),
9626
+ // Corner controls
9627
+ tl: new ExpandControl({
9628
+ x: -0.5,
9629
+ y: -0.5,
9630
+ expandDirections: ['left', 'top'],
9631
+ actionHandler: createCornerExpandHandler(expandLeftHandler, expandTopHandler),
9632
+ cursorStyleHandler: expandCursorStyleHandler,
9633
+ sizeX: CORNER_HANDLE_SIZE,
9634
+ sizeY: CORNER_HANDLE_SIZE
9635
+ }),
9636
+ tr: new ExpandControl({
9637
+ x: 0.5,
9638
+ y: -0.5,
9639
+ expandDirections: ['right', 'top'],
9640
+ actionHandler: createCornerExpandHandler(expandRightHandler, expandTopHandler),
9641
+ cursorStyleHandler: expandCursorStyleHandler,
9642
+ sizeX: CORNER_HANDLE_SIZE,
9643
+ sizeY: CORNER_HANDLE_SIZE
9644
+ }),
9645
+ bl: new ExpandControl({
9646
+ x: -0.5,
9647
+ y: 0.5,
9648
+ expandDirections: ['left', 'bottom'],
9649
+ actionHandler: createCornerExpandHandler(expandLeftHandler, expandBottomHandler),
9650
+ cursorStyleHandler: expandCursorStyleHandler,
9651
+ sizeX: CORNER_HANDLE_SIZE,
9652
+ sizeY: CORNER_HANDLE_SIZE
9653
+ }),
9654
+ br: new ExpandControl({
9655
+ x: 0.5,
9656
+ y: 0.5,
9657
+ expandDirections: ['right', 'bottom'],
9658
+ actionHandler: createCornerExpandHandler(expandRightHandler, expandBottomHandler),
9659
+ cursorStyleHandler: expandCursorStyleHandler,
9660
+ sizeX: CORNER_HANDLE_SIZE,
9661
+ sizeY: CORNER_HANDLE_SIZE
9662
+ })
9663
+ };
9664
+ }
9665
+
9266
9666
  class InteractiveFabricObject extends FabricObject$1 {
9267
9667
  static getDefaults() {
9268
9668
  return {
@@ -9279,6 +9679,9 @@ class InteractiveFabricObject extends FabricObject$1 {
9279
9679
  super();
9280
9680
  Object.assign(this, this.constructor.createControls(), InteractiveFabricObject.ownDefaults);
9281
9681
  this.setOptions(options);
9682
+ // Initialize expansion state
9683
+ this.expandMode = false;
9684
+ this.expansion = createDefaultExpansion();
9282
9685
  }
9283
9686
 
9284
9687
  /**
@@ -9561,6 +9964,11 @@ class InteractiveFabricObject extends FabricObject$1 {
9561
9964
  } else {
9562
9965
  size = this._calculateCurrentDimensions().scalarAdd(this.borderScaleFactor);
9563
9966
  }
9967
+
9968
+ // Draw expansion preview if in expand mode or has expansion
9969
+ if (this.expandMode || this.hasExpansion()) {
9970
+ this.drawExpandPreview(ctx, size);
9971
+ }
9564
9972
  this._drawBorders(ctx, size, styleOverride);
9565
9973
  }
9566
9974
 
@@ -9813,6 +10221,192 @@ class InteractiveFabricObject extends FabricObject$1 {
9813
10221
  renderDropTargetEffect(_e) {
9814
10222
  // for subclasses
9815
10223
  }
10224
+
10225
+ // ==================== EXPAND MODE API ====================
10226
+
10227
+ /**
10228
+ * Saved lock state before expand mode
10229
+ * @private
10230
+ */
10231
+
10232
+ /**
10233
+ * Enter expand mode - switches controls to expansion handles
10234
+ * for defining AI outpainting areas
10235
+ */
10236
+ enterExpandMode() {
10237
+ var _this$canvas2;
10238
+ if (this.expandMode) return;
10239
+
10240
+ // Save current controls
10241
+ this._savedControls = this.controls;
10242
+
10243
+ // Save and lock movement/scaling to prevent default behaviors
10244
+ this._savedLockMovementX = this.lockMovementX;
10245
+ this._savedLockMovementY = this.lockMovementY;
10246
+ this._savedLockScalingX = this.lockScalingX;
10247
+ this._savedLockScalingY = this.lockScalingY;
10248
+ this.lockMovementX = true;
10249
+ this.lockMovementY = true;
10250
+ this.lockScalingX = true;
10251
+ this.lockScalingY = true;
10252
+
10253
+ // Switch to expand controls
10254
+ this.controls = createExpandControls();
10255
+ this.expandMode = true;
10256
+
10257
+ // Recalculate control positions
10258
+ this.setCoords();
10259
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
10260
+ }
10261
+
10262
+ /**
10263
+ * Exit expand mode - restores normal controls
10264
+ */
10265
+ exitExpandMode() {
10266
+ var _this$canvas3;
10267
+ if (!this.expandMode) return;
10268
+
10269
+ // Restore saved controls
10270
+ if (this._savedControls) {
10271
+ this.controls = this._savedControls;
10272
+ this._savedControls = undefined;
10273
+ } else {
10274
+ // Fallback to default controls
10275
+ this.controls = createObjectDefaultControls();
10276
+ }
10277
+
10278
+ // Restore lock states
10279
+ if (this._savedLockMovementX !== undefined) {
10280
+ this.lockMovementX = this._savedLockMovementX;
10281
+ this._savedLockMovementX = undefined;
10282
+ }
10283
+ if (this._savedLockMovementY !== undefined) {
10284
+ this.lockMovementY = this._savedLockMovementY;
10285
+ this._savedLockMovementY = undefined;
10286
+ }
10287
+ if (this._savedLockScalingX !== undefined) {
10288
+ this.lockScalingX = this._savedLockScalingX;
10289
+ this._savedLockScalingX = undefined;
10290
+ }
10291
+ if (this._savedLockScalingY !== undefined) {
10292
+ this.lockScalingY = this._savedLockScalingY;
10293
+ this._savedLockScalingY = undefined;
10294
+ }
10295
+ this.expandMode = false;
10296
+
10297
+ // Recalculate control positions
10298
+ this.setCoords();
10299
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
10300
+ }
10301
+
10302
+ /**
10303
+ * Toggle expand mode
10304
+ */
10305
+ toggleExpandMode() {
10306
+ if (this.expandMode) {
10307
+ this.exitExpandMode();
10308
+ } else {
10309
+ this.enterExpandMode();
10310
+ }
10311
+ }
10312
+
10313
+ /**
10314
+ * Get the current expansion state
10315
+ * @returns ExpansionState with expandLeft, expandRight, expandTop, expandBottom
10316
+ */
10317
+ getExpansion() {
10318
+ return {
10319
+ ...this.expansion
10320
+ };
10321
+ }
10322
+
10323
+ /**
10324
+ * Set expansion values
10325
+ * @param expansion Partial expansion state to merge
10326
+ */
10327
+ setExpansion(expansion) {
10328
+ var _this$canvas4;
10329
+ Object.assign(this.expansion, expansion);
10330
+ this.setCoords();
10331
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
10332
+ }
10333
+
10334
+ /**
10335
+ * Reset expansion to zero in all directions
10336
+ */
10337
+ resetExpansion() {
10338
+ var _this$canvas5;
10339
+ this.expansion = createDefaultExpansion();
10340
+ this.setCoords();
10341
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
10342
+ }
10343
+
10344
+ /**
10345
+ * Check if object has any expansion set
10346
+ */
10347
+ hasExpansion() {
10348
+ const {
10349
+ expandLeft,
10350
+ expandRight,
10351
+ expandTop,
10352
+ expandBottom
10353
+ } = this.expansion;
10354
+ return expandLeft > 0 || expandRight > 0 || expandTop > 0 || expandBottom > 0;
10355
+ }
10356
+
10357
+ /**
10358
+ * Get the total expanded dimensions
10359
+ * @returns Object with expandedWidth and expandedHeight
10360
+ */
10361
+ getExpandedDimensions() {
10362
+ const {
10363
+ expandLeft,
10364
+ expandRight,
10365
+ expandTop,
10366
+ expandBottom
10367
+ } = this.expansion;
10368
+ return {
10369
+ expandedWidth: this.width + expandLeft + expandRight,
10370
+ expandedHeight: this.height + expandTop + expandBottom
10371
+ };
10372
+ }
10373
+
10374
+ /**
10375
+ * Draw the expansion preview area
10376
+ * Called during border rendering when in expand mode
10377
+ * @param ctx Canvas rendering context
10378
+ * @param size Object dimensions (already includes scale)
10379
+ * Note: expansion values are stored in screen pixels (already scaled)
10380
+ */
10381
+ drawExpandPreview(ctx, size) {
10382
+ if (!this.expandMode && !this.hasExpansion()) return;
10383
+ const {
10384
+ expandLeft,
10385
+ expandRight,
10386
+ expandTop,
10387
+ expandBottom
10388
+ } = this.expansion;
10389
+
10390
+ // Expansion values are already in screen pixels, use directly
10391
+ const expandedWidth = size.x + expandLeft + expandRight;
10392
+ const expandedHeight = size.y + expandTop + expandBottom;
10393
+
10394
+ // Offset to account for asymmetric expansion
10395
+ const offsetX = (expandRight - expandLeft) / 2;
10396
+ const offsetY = (expandBottom - expandTop) / 2;
10397
+ ctx.save();
10398
+
10399
+ // Draw expansion area fill
10400
+ ctx.fillStyle = EXPAND_PREVIEW_FILL;
10401
+ ctx.fillRect(-expandedWidth / 2 + offsetX, -expandedHeight / 2 + offsetY, expandedWidth, expandedHeight);
10402
+
10403
+ // Draw expansion border
10404
+ ctx.strokeStyle = EXPAND_PREVIEW_STROKE;
10405
+ ctx.lineWidth = 1;
10406
+ ctx.setLineDash(EXPAND_PREVIEW_DASH);
10407
+ ctx.strokeRect(-expandedWidth / 2 + offsetX, -expandedHeight / 2 + offsetY, expandedWidth, expandedHeight);
10408
+ ctx.restore();
10409
+ }
9816
10410
  }
9817
10411
  /**
9818
10412
  * The object's controls' position in viewport coordinates
@@ -9849,6 +10443,16 @@ class InteractiveFabricObject extends FabricObject$1 {
9849
10443
  * @TODO use git blame to investigate why it was added
9850
10444
  * DON'T USE IT. WE WILL TRY TO REMOVE IT
9851
10445
  */
10446
+ /**
10447
+ * Whether the object is in expand mode (for AI outpainting)
10448
+ */
10449
+ /**
10450
+ * Expansion state for AI outpainting (pixels to expand in each direction)
10451
+ */
10452
+ /**
10453
+ * Saved controls when switching to expand mode
10454
+ * @private
10455
+ */
9852
10456
  _defineProperty(InteractiveFabricObject, "ownDefaults", interactiveObjectDefaultValues);
9853
10457
 
9854
10458
  /***
@@ -30408,6 +31012,11 @@ class Frame extends Group {
30408
31012
  * @private
30409
31013
  */
30410
31014
  _defineProperty(this, "_editModeClipPath", void 0);
31015
+ // ==================== CONTENT EXPAND MODE ====================
31016
+ /**
31017
+ * Whether the content image is in expand mode
31018
+ */
31019
+ _defineProperty(this, "_contentExpandMode", false);
30411
31020
  Object.assign(this, Frame.ownDefaults);
30412
31021
 
30413
31022
  // Apply user options
@@ -31150,60 +31759,69 @@ class Frame extends Group {
31150
31759
 
31151
31760
  // Draw edit mode overlay if in edit mode
31152
31761
  if (this.isEditMode && this._editModeClipPath) {
31153
- this._renderEditModeOverlay(ctx);
31762
+ this._renderEditModeOverlay(ctx, false);
31763
+
31764
+ // In expand mode, re-render the content image's controls ON TOP of the overlay
31765
+ if (this._contentExpandMode && this._contentImage) {
31766
+ this._contentImage._renderControls(ctx);
31767
+ }
31154
31768
  }
31155
31769
  }
31156
31770
 
31157
31771
  /**
31158
31772
  * Renders the edit mode overlay - dims area outside frame, shows frame border
31159
31773
  * @private
31774
+ * @param skipDimOverlay - if true, skip the dark overlay (for expand mode)
31160
31775
  */
31161
31776
  _renderEditModeOverlay(ctx) {
31777
+ let skipDimOverlay = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
31162
31778
  ctx.save();
31163
31779
 
31164
31780
  // Apply the group's transform
31165
31781
  const m = this.calcTransformMatrix();
31166
31782
  ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
31167
31783
 
31168
- // Draw semi-transparent overlay on the OUTSIDE of the frame
31169
- // We do this by drawing a large rect and cutting out the frame shape
31170
- ctx.beginPath();
31171
-
31172
- // Large outer rectangle (covers the whole image area)
31173
- const padding = 2000; // Large enough to cover any overflow
31174
- ctx.rect(-padding, -padding, padding * 2, padding * 2);
31784
+ // Draw semi-transparent overlay on the OUTSIDE of the frame (skip in expand mode)
31785
+ if (!skipDimOverlay) {
31786
+ // We do this by drawing a large rect and cutting out the frame shape
31787
+ ctx.beginPath();
31175
31788
 
31176
- // Cut out the frame shape (counter-clockwise to create hole)
31177
- if (this.frameShape === 'circle') {
31178
- const radius = Math.min(this.frameWidth, this.frameHeight) / 2;
31179
- ctx.moveTo(radius, 0);
31180
- ctx.arc(0, 0, radius, 0, Math.PI * 2, true);
31181
- } else if (this.frameShape === 'rounded-rect') {
31182
- const w = this.frameWidth / 2;
31183
- const h = this.frameHeight / 2;
31184
- const r = Math.min(this.frameBorderRadius, w, h);
31185
- ctx.moveTo(w, h - r);
31186
- ctx.arcTo(w, -h, w - r, -h, r);
31187
- ctx.arcTo(-w, -h, -w, -h + r, r);
31188
- ctx.arcTo(-w, h, -w + r, h, r);
31189
- ctx.arcTo(w, h, w, h - r, r);
31190
- ctx.closePath();
31191
- } else {
31192
- // Rectangle
31193
- const w = this.frameWidth / 2;
31194
- const h = this.frameHeight / 2;
31195
- ctx.moveTo(w, -h);
31196
- ctx.lineTo(-w, -h);
31197
- ctx.lineTo(-w, h);
31198
- ctx.lineTo(w, h);
31199
- ctx.closePath();
31789
+ // Large outer rectangle (covers the whole image area)
31790
+ const padding = 2000; // Large enough to cover any overflow
31791
+ ctx.rect(-padding, -padding, padding * 2, padding * 2);
31792
+
31793
+ // Cut out the frame shape (counter-clockwise to create hole)
31794
+ if (this.frameShape === 'circle') {
31795
+ const radius = Math.min(this.frameWidth, this.frameHeight) / 2;
31796
+ ctx.moveTo(radius, 0);
31797
+ ctx.arc(0, 0, radius, 0, Math.PI * 2, true);
31798
+ } else if (this.frameShape === 'rounded-rect') {
31799
+ const w = this.frameWidth / 2;
31800
+ const h = this.frameHeight / 2;
31801
+ const r = Math.min(this.frameBorderRadius, w, h);
31802
+ ctx.moveTo(w, h - r);
31803
+ ctx.arcTo(w, -h, w - r, -h, r);
31804
+ ctx.arcTo(-w, -h, -w, -h + r, r);
31805
+ ctx.arcTo(-w, h, -w + r, h, r);
31806
+ ctx.arcTo(w, h, w, h - r, r);
31807
+ ctx.closePath();
31808
+ } else {
31809
+ // Rectangle
31810
+ const w = this.frameWidth / 2;
31811
+ const h = this.frameHeight / 2;
31812
+ ctx.moveTo(w, -h);
31813
+ ctx.lineTo(-w, -h);
31814
+ ctx.lineTo(-w, h);
31815
+ ctx.lineTo(w, h);
31816
+ ctx.closePath();
31817
+ }
31818
+
31819
+ // Fill with semi-transparent dark overlay
31820
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
31821
+ ctx.fill('evenodd');
31200
31822
  }
31201
31823
 
31202
- // Fill with semi-transparent dark overlay
31203
- ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
31204
- ctx.fill('evenodd');
31205
-
31206
- // Draw frame border
31824
+ // Draw frame border (always visible)
31207
31825
  ctx.beginPath();
31208
31826
  if (this.frameShape === 'circle') {
31209
31827
  const radius = Math.min(this.frameWidth, this.frameHeight) / 2;
@@ -31243,6 +31861,11 @@ class Frame extends Group {
31243
31861
  if (!this._contentImage || !this.isEditMode) {
31244
31862
  return;
31245
31863
  }
31864
+
31865
+ // Exit content expand mode first if active
31866
+ if (this._contentExpandMode) {
31867
+ this.exitContentExpandMode();
31868
+ }
31246
31869
  this.isEditMode = false;
31247
31870
 
31248
31871
  // Remove constraint handlers
@@ -31331,6 +31954,96 @@ class Frame extends Group {
31331
31954
  this.enterEditMode();
31332
31955
  }
31333
31956
  }
31957
+ /**
31958
+ * Enter expand mode for the content image (for AI outpainting)
31959
+ * Must be in edit mode first
31960
+ */
31961
+ enterContentExpandMode() {
31962
+ if (!this._contentImage || !this.isEditMode) {
31963
+ console.warn('Frame: Must be in edit mode with content to enter expand mode');
31964
+ return;
31965
+ }
31966
+ if (this._contentExpandMode) return;
31967
+ this._contentExpandMode = true;
31968
+
31969
+ // Remove constraints - in expand mode we don't enforce image covering frame
31970
+ this._removeEditModeConstraints();
31971
+
31972
+ // Enter expand mode on the content image
31973
+ this._contentImage.enterExpandMode();
31974
+ if (this.canvas) {
31975
+ this.canvas.requestRenderAll();
31976
+ }
31977
+ }
31978
+
31979
+ /**
31980
+ * Exit expand mode for the content image
31981
+ */
31982
+ exitContentExpandMode() {
31983
+ if (!this._contentImage || !this._contentExpandMode) return;
31984
+ this._contentExpandMode = false;
31985
+
31986
+ // Exit expand mode on the content image
31987
+ this._contentImage.exitExpandMode();
31988
+
31989
+ // Re-add constraints if still in edit mode
31990
+ if (this.isEditMode) {
31991
+ this._setupEditModeConstraints();
31992
+ }
31993
+ if (this.canvas) {
31994
+ this.canvas.requestRenderAll();
31995
+ }
31996
+ }
31997
+
31998
+ /**
31999
+ * Toggle expand mode for content image
32000
+ */
32001
+ toggleContentExpandMode() {
32002
+ if (this._contentExpandMode) {
32003
+ this.exitContentExpandMode();
32004
+ } else {
32005
+ this.enterContentExpandMode();
32006
+ }
32007
+ }
32008
+
32009
+ /**
32010
+ * Check if content is in expand mode
32011
+ */
32012
+ isContentExpandMode() {
32013
+ return this._contentExpandMode;
32014
+ }
32015
+
32016
+ /**
32017
+ * Get expansion values from the content image
32018
+ */
32019
+ getContentExpansion() {
32020
+ if (!this._contentImage) return null;
32021
+ return this._contentImage.getExpansion();
32022
+ }
32023
+
32024
+ /**
32025
+ * Set expansion values on the content image
32026
+ */
32027
+ setContentExpansion(expansion) {
32028
+ if (!this._contentImage) return;
32029
+ this._contentImage.setExpansion(expansion);
32030
+ }
32031
+
32032
+ /**
32033
+ * Reset content expansion to zero
32034
+ */
32035
+ resetContentExpansion() {
32036
+ if (!this._contentImage) return;
32037
+ this._contentImage.resetExpansion();
32038
+ }
32039
+
32040
+ /**
32041
+ * Check if content has any expansion
32042
+ */
32043
+ hasContentExpansion() {
32044
+ if (!this._contentImage) return false;
32045
+ return this._contentImage.hasExpansion();
32046
+ }
31334
32047
 
31335
32048
  /**
31336
32049
  * Resizes the frame to new dimensions (Canva-like behavior)
@@ -32737,7 +33450,15 @@ function createPathControls(path) {
32737
33450
 
32738
33451
  var index = /*#__PURE__*/Object.freeze({
32739
33452
  __proto__: null,
33453
+ EXPAND_HANDLE_FILL: EXPAND_HANDLE_FILL,
33454
+ EXPAND_HANDLE_STROKE: EXPAND_HANDLE_STROKE,
33455
+ EXPAND_PREVIEW_DASH: EXPAND_PREVIEW_DASH,
33456
+ EXPAND_PREVIEW_FILL: EXPAND_PREVIEW_FILL,
33457
+ EXPAND_PREVIEW_STROKE: EXPAND_PREVIEW_STROKE,
33458
+ ExpandControl: ExpandControl,
32740
33459
  changeWidth: changeWidth,
33460
+ createDefaultExpansion: createDefaultExpansion,
33461
+ createExpandControls: createExpandControls,
32741
33462
  createObjectDefaultControls: createObjectDefaultControls,
32742
33463
  createPathControls: createPathControls,
32743
33464
  createPolyActionHandler: createPolyActionHandler,
@@ -32746,7 +33467,12 @@ var index = /*#__PURE__*/Object.freeze({
32746
33467
  createResizeControls: createResizeControls,
32747
33468
  createTextboxDefaultControls: createTextboxDefaultControls,
32748
33469
  dragHandler: dragHandler,
33470
+ expandBottomHandler: expandBottomHandler,
33471
+ expandLeftHandler: expandLeftHandler,
33472
+ expandRightHandler: expandRightHandler,
33473
+ expandTopHandler: expandTopHandler,
32749
33474
  factoryPolyActionHandler: factoryPolyActionHandler,
33475
+ getExpansion: getExpansion,
32750
33476
  getLocalPoint: getLocalPoint,
32751
33477
  polyActionHandler: polyActionHandler,
32752
33478
  renderCircleControl: renderCircleControl,
@@ -32761,6 +33487,7 @@ var index = /*#__PURE__*/Object.freeze({
32761
33487
  scalingXOrSkewingY: scalingXOrSkewingY,
32762
33488
  scalingY: scalingY,
32763
33489
  scalingYOrSkewingX: scalingYOrSkewingX,
33490
+ setExpansion: setExpansion,
32764
33491
  skewCursorStyleHandler: skewCursorStyleHandler,
32765
33492
  skewHandlerX: skewHandlerX,
32766
33493
  skewHandlerY: skewHandlerY,